Download scripts/video-engine/build-math-lesson-draft.ts from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 13.6 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/scripts/video-engine/build-math-lesson-draft.ts
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/scripts/video-engine/build-math-lesson-draft.ts
-
curl -L -o build-math-lesson-draft.ts https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/scripts/video-engine/build-math-lesson-draft.ts
13.6 kB
| // Convert a SCERT Chemistry blueprint (math-blueprints/chem-*.json) into an | |
| // ordered lesson draft of teaching scenes β the render pipeline's front-end, | |
| // mirroring the Physics scene shape so the same rendering/TTS/HLS stages can | |
| // consume it. This is content ONLY (narration + on-screen + intent + source | |
| // lineage); timing and audio are added by later stages. | |
| // | |
| // CHAPTER_ID=math-p1-c1 npx tsx scripts/video-engine/build-math-lesson-draft.ts | |
| // (no CHAPTER_ID => all seven chapters) | |
| import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; | |
| import { join } from "node:path"; | |
| const ROOT = process.cwd(); | |
| const BLUEPRINT_DIR = join(ROOT, "data/curriculum/kerala-sslc/math-blueprints"); | |
| const OUT_DIR = join(ROOT, "outputs/video/math-course-2025/lessons"); | |
| const CHAPTERS = [ | |
| "math-p1-c1", "math-p1-c2", "math-p1-c3", "math-p1-c4", "math-p1-c5", | |
| "math-p1-c6", "math-p1-c7", "math-p1-c8", "math-p1-c9", "math-p1-c10", "math-p1-c11", "math-p1-c12", "math-p1-c13", | |
| ]; | |
| type Block = { heading?: string; explanation?: string; key_terms?: string[]; teacher?: string }; | |
| type BoardFrame = { title?: string; teacher_action?: string; board_text?: string[]; diagram_description?: string; student_focus?: string; source_page?: number }; | |
| type Exam = { marks?: number; question?: string; answer_points?: string[]; keywords?: string[]; common_mistakes?: string[] }; | |
| type Quick = { question?: string; answer?: string; explanation?: string }; | |
| type Mission = { | |
| mission_id: string; title: string; source_pages?: number[]; hook?: string; teacher_intro?: string; | |
| explanation_blocks?: Block[]; analogy_or_observation?: string; board_frames?: BoardFrame[]; | |
| exam_practice?: Exam[]; quick_check?: Quick; misconception_check?: string; recap?: string | string[]; | |
| // Alternate ("checkpoint") schema used by some pre-authored chapters. | |
| checkpoint_question?: string; checkpoint_options?: string[]; checkpoint_answer?: string; | |
| }; | |
| type Blueprint = { | |
| chapter_id: string; subject: string; chapter_title: string; source_book: string; | |
| source_pdf_pages?: number[]; learning_objectives?: string[]; missions: Mission[]; | |
| chapter_recap?: string[]; render_guidance?: Record<string, unknown>; | |
| }; | |
| type VisualFamily = | |
| | "MOLECULAR_STRUCTURE" | "REACTION_EQUATION" | "FUNCTIONAL_GROUP" | "ISOMERISM" | |
| | "PERIODIC_TREND" | "ELECTROLYSIS_CELL" | "METALLURGY_PROCESS" | "GAS_LAW" | |
| | "MOLE_CALCULATION" | "LAB_EXPERIMENT" | "BOARD_ANSWER" | "RECAP" | "CONCEPT_TEXT"; | |
| const clean = (s: string | undefined) => | |
| (s ?? "") | |
| .replace(/\bnamaste,?\s*/gi, "") // drop the repeated robotic opener anywhere | |
| .replace(/\bmalayalam_rescue\b/gi, "") | |
| .replace(/\bpause\b\.?/gi, "") | |
| .replace(/\s+/g, " ") | |
| .replace(/^\s*([a-z])/, (_, c) => c.toUpperCase()) | |
| .trim(); | |
| // Route a scene to a deterministic math visual family from its topic text. | |
| function routeVisual(text: string, kind: string): VisualFamily { | |
| const t = text.toLowerCase(); | |
| if (kind === "board_answer") return "BOARD_ANSWER"; | |
| if (kind === "recap_revision") return "RECAP"; | |
| if (/isomer/.test(t)) return "ISOMERISM"; | |
| if (/functional group|hydroxyl|carboxyl|aldehyde|keto|amine|ester/.test(t)) return "FUNCTIONAL_GROUP"; | |
| if (/electrolysis|electrode|anode|cathode|electroplat|galvanic|electrochem/.test(t)) return "ELECTROLYSIS_CELL"; | |
| if (/periodic|group|period|valenc|electron configuration|atomic (size|radius)/.test(t)) return "PERIODIC_TREND"; | |
| if (/ore|roast|calcin|smelt|reduction|metallurg|refin|reactivity series|corros|alloy/.test(t)) return "METALLURGY_PROCESS"; | |
| if (/gas law|boyle|charles|pressure|volume|temperature of a gas|avogadro/.test(t)) return "GAS_LAW"; | |
| if (/mole|molar mass|number of moles|percentage composition|stoichiometr/.test(t)) return "MOLE_CALCULATION"; | |
| if (/reaction|equation|β|->|combustion|displacement|precipit|neutralis|oxidation|reduction/.test(t)) return "REACTION_EQUATION"; | |
| if (/structure|structural formula|chain|bond|molecule|alkane|alkene|alkyne|hydrocarbon/.test(t)) return "MOLECULAR_STRUCTURE"; | |
| if (/experiment|apparatus|test tube|observe|beaker|heat|flame/.test(t)) return "LAB_EXPERIMENT"; | |
| return "CONCEPT_TEXT"; | |
| } | |
| type Scene = { | |
| sceneId: string; missionId: string; segmentKind: string; title: string; | |
| narration: string; onScreenText: string[]; keyTerms: string[]; | |
| visual: { family: VisualFamily; topicKey: string; boardText?: string[]; diagramHint?: string }; | |
| examAnswer?: { question: string; marks: number; answerPoints: string[]; keywords: string[]; commonMistakes: string[] }; | |
| sourcePage: number; | |
| }; | |
| function buildChapter(bp: Blueprint) { | |
| const scenes: Scene[] = []; | |
| const firstPage = bp.source_pdf_pages?.[0] ?? 0; | |
| let n = 0; | |
| const id = (kind: string) => `${kind}-${String(++n).padStart(3, "0")}`; | |
| // Chapter opening / roadmap. | |
| scenes.push({ | |
| sceneId: id("opening"), missionId: "chapter", segmentKind: "opening", | |
| title: clean(bp.chapter_title), | |
| narration: `Welcome to ${clean(bp.chapter_title)}. By the end of today you will: ${(bp.learning_objectives ?? []).map((o) => o.toLowerCase()).join("; ")}.`, | |
| onScreenText: (bp.missions ?? []).map((m) => clean(m.title)), | |
| keyTerms: [], visual: { family: "CONCEPT_TEXT", topicKey: "roadmap" }, sourcePage: firstPage, | |
| }); | |
| for (const m of bp.missions ?? []) { | |
| const page = m.source_pages?.[0] ?? firstPage; | |
| // Mission intro. | |
| const intro = clean([m.hook, m.teacher_intro].filter(Boolean).join(" ")); | |
| if (intro) { | |
| scenes.push({ | |
| sceneId: id("concept"), missionId: m.mission_id, segmentKind: "concept", | |
| title: clean(m.title), narration: intro, onScreenText: [clean(m.title)], keyTerms: [], | |
| visual: { family: routeVisual(m.title, "concept"), topicKey: m.title }, sourcePage: page, | |
| }); | |
| } | |
| // Explanation blocks. A block whose explanation spans multiple lines (a | |
| // formula/worked-steps block, common in the "checkpoint" schema) becomes a | |
| // visual_board scene so those exact lines render as real board_text β never | |
| // dropped as plain narration. "teacher" (spoken-style elaboration) is | |
| // folded into the narration alongside "explanation". | |
| for (const b of m.explanation_blocks ?? []) { | |
| if (!clean(b.explanation) && !clean(b.teacher)) continue; | |
| const lines = (b.explanation ?? "").split(/\n+/).map((l) => l.trim()).filter(Boolean); | |
| const narration = clean([b.explanation && lines.length <= 1 ? b.explanation : "", b.teacher].filter(Boolean).join(" ")) || clean(b.explanation); | |
| if (lines.length > 1) { | |
| scenes.push({ | |
| sceneId: id("visual_board"), missionId: m.mission_id, segmentKind: "visual_board", | |
| title: clean(b.heading) || clean(m.title), narration, | |
| onScreenText: lines.map(clean), keyTerms: b.key_terms ?? [], | |
| visual: { family: routeVisual(`${b.heading} ${lines.join(" ")}`, "visual_board"), topicKey: b.heading || m.title, boardText: lines }, | |
| sourcePage: page, | |
| }); | |
| } else { | |
| scenes.push({ | |
| sceneId: id("concept"), missionId: m.mission_id, segmentKind: "concept", | |
| title: clean(b.heading) || clean(m.title), narration, | |
| onScreenText: [clean(b.heading)].filter(Boolean), keyTerms: b.key_terms ?? [], | |
| visual: { family: routeVisual(`${b.heading} ${b.explanation}`, "concept"), topicKey: b.heading || m.title }, | |
| sourcePage: page, | |
| }); | |
| } | |
| } | |
| // Analogy / observation as a short concept beat. | |
| if (clean(m.analogy_or_observation)) { | |
| scenes.push({ | |
| sceneId: id("concept"), missionId: m.mission_id, segmentKind: "concept", | |
| title: "Think about it", narration: clean(m.analogy_or_observation), onScreenText: [], keyTerms: [], | |
| visual: { family: "CONCEPT_TEXT", topicKey: "analogy" }, sourcePage: page, | |
| }); | |
| } | |
| // Board frames (structures, equations, observations from the textbook). | |
| for (const bf of m.board_frames ?? []) { | |
| scenes.push({ | |
| sceneId: id("visual_board"), missionId: m.mission_id, segmentKind: "visual_board", | |
| title: clean(bf.title) || clean(m.title), | |
| narration: clean([bf.teacher_action, bf.student_focus].filter(Boolean).join(" ")), | |
| onScreenText: (bf.board_text ?? []).map(clean).filter(Boolean), keyTerms: [], | |
| visual: { family: routeVisual(`${bf.title} ${bf.diagram_description} ${(bf.board_text ?? []).join(" ")}`, "visual_board"), topicKey: bf.title || m.title, boardText: bf.board_text, diagramHint: bf.diagram_description }, | |
| sourcePage: bf.source_page ?? page, | |
| }); | |
| } | |
| // Misconception check. | |
| if (clean(m.misconception_check)) { | |
| scenes.push({ | |
| sceneId: id("common_mistakes"), missionId: m.mission_id, segmentKind: "common_mistakes", | |
| title: "Common mistake", narration: clean(m.misconception_check), | |
| onScreenText: [clean(m.misconception_check)], keyTerms: [], | |
| visual: { family: "CONCEPT_TEXT", topicKey: "misconception" }, sourcePage: page, | |
| }); | |
| } | |
| // Exam practice β board-answer scenes. | |
| for (const ex of m.exam_practice ?? []) { | |
| if (!clean(ex.question)) continue; | |
| scenes.push({ | |
| sceneId: id("board_answer"), missionId: m.mission_id, segmentKind: "board_answer", | |
| title: clean(ex.question), narration: `Let us write the ${ex.marks ?? 2}-mark answer. ${clean(ex.question)}`, | |
| onScreenText: [], keyTerms: ex.keywords ?? [], | |
| visual: { family: "BOARD_ANSWER", topicKey: ex.question || m.title }, | |
| examAnswer: { question: clean(ex.question), marks: ex.marks ?? 2, answerPoints: (ex.answer_points ?? []).map(clean), keywords: ex.keywords ?? [], commonMistakes: ex.common_mistakes ?? [] }, | |
| sourcePage: page, | |
| }); | |
| } | |
| // Quick check (active recall). | |
| if (clean(m.quick_check?.question)) { | |
| scenes.push({ | |
| sceneId: id("recall"), missionId: m.mission_id, segmentKind: "recall", | |
| title: "Quick check", narration: `${clean(m.quick_check?.question)} β¦ ${clean(m.quick_check?.answer)}.`, | |
| onScreenText: [clean(m.quick_check?.question), clean(m.quick_check?.answer)].filter(Boolean), keyTerms: [], | |
| visual: { family: "CONCEPT_TEXT", topicKey: "quick_check" }, sourcePage: page, | |
| }); | |
| } | |
| // Checkpoint question (alternate schema) β a worked board-answer scene, so | |
| // it gets the same exam-Q&A treatment as exam_practice, not just a text card. | |
| if (clean(m.checkpoint_question)) { | |
| const q = clean(m.checkpoint_question); | |
| const ans = clean(m.checkpoint_answer); | |
| scenes.push({ | |
| sceneId: id("board_answer"), missionId: m.mission_id, segmentKind: "board_answer", | |
| title: q, narration: `Check yourself. ${q} β¦ The answer is ${ans}.`, | |
| onScreenText: [], keyTerms: [], | |
| visual: { family: "BOARD_ANSWER", topicKey: q }, | |
| examAnswer: { question: q, marks: 2, answerPoints: [ans].filter(Boolean), keywords: [], commonMistakes: [] }, | |
| sourcePage: page, | |
| }); | |
| } | |
| } | |
| // Chapter recap. Fall back to mission titles when no explicit chapter_recap | |
| // is authored (the "checkpoint" schema chapters don't have one). | |
| const recapPoints = ((bp.chapter_recap?.length ? bp.chapter_recap : (bp.missions ?? []).map((m) => m.title)) ?? []) | |
| .map(clean).filter(Boolean).slice(0, 8); | |
| if (recapPoints.length) { | |
| scenes.push({ | |
| sceneId: id("recap_revision"), missionId: "chapter", segmentKind: "recap_revision", | |
| title: `${clean(bp.chapter_title)} β recap`, narration: `Let us revise the key points. ${recapPoints.join(". ")}.`, | |
| onScreenText: recapPoints, keyTerms: [], visual: { family: "RECAP", topicKey: "recap" }, sourcePage: firstPage, | |
| }); | |
| } | |
| // Chapter-scoped homework (2-4 tasks from this chapter's own exam questions, | |
| // falling back to checkpoint questions when exam_practice is absent). | |
| const examQs = (bp.missions ?? []).flatMap((m) => (m.exam_practice ?? []).map((e) => clean(e.question)).filter(Boolean)); | |
| const checkpointQs = (bp.missions ?? []).map((m) => clean(m.checkpoint_question)).filter(Boolean); | |
| const homework = (examQs.length ? examQs : checkpointQs).slice(0, 3).map((q, i) => `Task ${i + 1}: ${q}`); | |
| if (homework.length) { | |
| scenes.push({ | |
| sceneId: id("homework"), missionId: "chapter", segmentKind: "homework", | |
| title: "Homework and next step", narration: `For homework: ${homework.join(" ")}`, | |
| onScreenText: homework, keyTerms: [], visual: { family: "CONCEPT_TEXT", topicKey: "homework" }, sourcePage: firstPage, | |
| }); | |
| } | |
| return { | |
| chapterId: bp.chapter_id, chapterTitle: bp.chapter_title, subject: "Mathematics", | |
| sourceBook: bp.source_book, sourcePages: bp.source_pdf_pages, renderGuidance: bp.render_guidance, | |
| sceneCount: scenes.length, scenes, homework, | |
| }; | |
| } | |
| const requested = process.env.CHAPTER_ID ? [process.env.CHAPTER_ID] : CHAPTERS; | |
| mkdirSync(OUT_DIR, { recursive: true }); | |
| for (const chapterId of requested) { | |
| const bpPath = join(BLUEPRINT_DIR, `${chapterId}.json`); | |
| if (!existsSync(bpPath)) { console.error(`missing blueprint: ${bpPath}`); continue; } | |
| const bp = JSON.parse(readFileSync(bpPath, "utf8")) as Blueprint; | |
| const draft = buildChapter(bp); | |
| const outPath = join(OUT_DIR, `${chapterId}-lesson-draft.json`); | |
| writeFileSync(outPath, `${JSON.stringify(draft, null, 2)}\n`); | |
| const kinds = draft.scenes.reduce<Record<string, number>>((a, s) => { a[s.segmentKind] = (a[s.segmentKind] ?? 0) + 1; return a; }, {}); | |
| console.log(`${chapterId}: ${draft.sceneCount} scenes ${JSON.stringify(kinds)}`); | |
| } | |