Files
FamReynaBrain/projects/ckmath-interactive-pilot/scripts/build_grade3_catalog.py
T
2026-09-14 22:38:47 -04:00

130 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Build a local-only Grade 3 catalog from verified Core Knowledge archives."""
import argparse
import hashlib
import json
import re
import zipfile
from pathlib import Path
ROLE_ORDER = {'Student Reader': 0, 'Teacher Guide': 1, 'Reference': 2}
def is_grade_three_source(item):
title = str(item.get('title', ''))
filename = str(item.get('filename', ''))
return bool(re.search(r'\bGrade 3\b', title, re.I) or re.search(r'(?:^|[_-])G3(?:[_-]|$)', filename, re.I))
def classify_unit(filename):
if re.search(r'CKMath', filename, re.I):
return 'Math'
if re.search(r'CKSci', filename, re.I):
return 'Science'
if re.search(r'CKLA', filename, re.I):
return 'Language Arts'
if re.search(r'CKHG|CKIYS|LABB_CKHG', filename, re.I):
return 'History & Geography'
if re.search(r'CKMusic|CKVA', filename, re.I):
return 'Arts'
return 'Other'
def role_for_pdf(filename):
if re.search(r'(?:^|[_-])(TG|TeacherGuide|Teacher[_ -]?Guide)(?:[_-]|\.)', filename, re.I):
return 'Teacher Guide'
if re.search(r'(?:^|[_-])(SR|StudentReader|Student[_ -]?Reader|ActivityBook)(?:[_-]|\.)', filename, re.I):
return 'Student Reader'
return 'Reference'
def material_id(archive_name, internal_path):
raw = '%s\0%s' % (archive_name, internal_path)
return hashlib.sha256(raw.encode('utf-8')).hexdigest()[:20]
def pdf_page_count(archive_path, internal_path):
try:
import fitz
with zipfile.ZipFile(archive_path) as archive:
document = fitz.open(stream=archive.read(internal_path), filetype='pdf')
count = document.page_count
document.close()
return count
except Exception:
return None
def inspect_archive(archive_path, include_page_counts=False):
materials = []
with zipfile.ZipFile(archive_path) as archive:
for info in archive.infolist():
internal_path = info.filename
if internal_path.startswith('__MACOSX/') or not internal_path.lower().endswith('.pdf'):
continue
filename = Path(internal_path).name
material = {
'id': material_id(archive_path.name, internal_path),
'filename': filename,
'internal_path': internal_path,
'role': role_for_pdf(filename),
}
if include_page_counts:
material['pages'] = pdf_page_count(archive_path, internal_path)
materials.append(material)
materials.sort(key=lambda item: (ROLE_ORDER[item['role']], item['filename'].lower()))
return materials
def build_catalog(manifest_path, downloads_dir, include_page_counts=False):
entries = json.loads(Path(manifest_path).read_text())
units = []
for item in entries:
if item.get('type') != 'zip' or not is_grade_three_source(item):
continue
archive_path = Path(downloads_dir) / item['filename']
if not archive_path.is_file():
continue
materials = inspect_archive(archive_path, include_page_counts)
units.append({
'id': hashlib.sha256(item['filename'].encode('utf-8')).hexdigest()[:16],
'title': item['title'],
'filename': item['filename'],
'source_url': item['source'],
'subject': classify_unit(item['filename']),
'materials': materials,
'teacher_guide_count': sum(material['role'] == 'Teacher Guide' for material in materials),
'student_material_count': sum(material['role'] == 'Student Reader' for material in materials),
})
units.sort(key=lambda item: (item['subject'], item['title'].lower()))
return {
'grade': 3,
'unit_count': len(units),
'teacher_guide_count': sum(unit['teacher_guide_count'] for unit in units),
'student_material_count': sum(unit['student_material_count'] for unit in units),
'units': units,
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--manifest', type=Path, required=True)
parser.add_argument('--downloads-dir', type=Path, required=True)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--page-counts', action='store_true')
args = parser.parse_args()
catalog = build_catalog(args.manifest, args.downloads_dir, args.page_counts)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(catalog, indent=2) + '\n')
print(json.dumps({
'output': str(args.output),
'units': catalog['unit_count'],
'teacher_guides': catalog['teacher_guide_count'],
'student_materials': catalog['student_material_count'],
}))
if __name__ == '__main__':
main()