explainer-video 0.0.0-stage → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -2
  4. package/cordis.patch.yml +7 -0
  5. package/lib/index.js +35 -0
  6. package/package.json +56 -4
  7. package/plugin.json +13 -0
  8. package/skills/coursework-author/.synced-from +5 -0
  9. package/skills/coursework-author/SKILL.md +209 -0
  10. package/skills/craft-codeflow-flows/SKILL.md +116 -0
  11. package/skills/craft-codeflow-mine/SKILL.md +207 -0
  12. package/skills/craft-codeflow-plan/SKILL.md +65 -0
  13. package/skills/craft-codeflow-scene/SKILL.md +168 -0
  14. package/skills/craft-video-align/SKILL.md +109 -0
  15. package/skills/craft-video-assemble/SKILL.md +70 -0
  16. package/skills/craft-video-audio/SKILL.md +59 -0
  17. package/skills/craft-video-enrich/SKILL.md +46 -0
  18. package/skills/craft-video-plan/SKILL.md +57 -0
  19. package/skills/craft-video-publish/SKILL.md +79 -0
  20. package/skills/craft-video-render/SKILL.md +68 -0
  21. package/skills/craft-video-scene/SKILL.md +112 -0
  22. package/skills/craft-video-script/SKILL.md +80 -0
  23. package/skills/craft-video-thumbnail/SKILL.md +168 -0
  24. package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
  25. package/skills/explainer-video-from-codebase/SKILL.md +698 -0
  26. package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
  27. package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
  28. package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
  29. package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
  30. package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
  31. package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
  32. package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
  33. package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
  34. package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
  35. package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
  36. package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
  37. package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
  38. package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
  39. package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
  40. package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
  41. package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
  42. package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
  43. package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
  44. package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
  45. package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
  46. package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
  47. package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
  48. package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
  49. package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
  50. package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
  51. package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
  52. package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
  53. package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
  54. package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
  55. package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
  56. package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
  57. package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
  58. package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
  59. package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
  60. package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
  61. package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
  62. package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
  63. package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
  64. package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
  65. package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
  66. package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
  67. package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
  68. package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
  69. package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
  70. package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
  71. package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
  72. package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
  73. package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
  74. package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
  75. package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
  76. package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
  77. package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
  78. package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
  79. package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
  80. package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
  81. package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
  82. package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
  83. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
  84. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
  85. package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
  86. package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
  87. package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
  88. package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
  89. package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
  90. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
  91. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
  92. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
  93. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
  94. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
  95. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
  96. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
  97. package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
  98. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
  99. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
  100. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
  101. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
  102. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
  103. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
  104. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
  105. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
  106. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
  107. package/skills/explainer-video-from-coursework/ARCHITECTURE.md +61 -0
  108. package/skills/explainer-video-from-coursework/PIPELINE.md +369 -0
  109. package/skills/explainer-video-from-coursework/ROADMAP.md +727 -0
  110. package/skills/explainer-video-from-coursework/SERIES.md +243 -0
  111. package/skills/explainer-video-from-coursework/SETUP.md +105 -0
  112. package/skills/explainer-video-from-coursework/SKILL.md +998 -0
  113. package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
  114. package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
  115. package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
  116. package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
  117. package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
  118. package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
  119. package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
  120. package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
  121. package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
  122. package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
  123. package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
  124. package/skills/explainer-video-from-coursework/src/thumbnail.html +119 -0
  125. package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
  126. package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
  127. package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
  128. package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
  129. package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
  130. package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
  131. package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
  132. package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
  133. package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
  134. package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
  135. package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
  136. package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
  137. package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
  138. package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
  139. package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
  140. package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
  141. package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
  142. package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
  143. package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
  144. package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
  145. package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
  146. package/skills/explainer-video-from-coursework/tools/thumbnail.mjs +224 -0
  147. package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
  148. package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
  149. package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
  150. package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
  151. package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
  152. package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
  153. package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js.LICENSE.txt +16 -0
@@ -0,0 +1,61 @@
1
+ # Architecture — who does what
2
+
3
+ The single rule this project is built on:
4
+
5
+ > **Code does what must be exact. The host agent does what requires judgment.
6
+ > Gemini is used for one thing only: text-to-speech.**
7
+
8
+ ## Why
9
+
10
+ An LLM is already in the loop — it is the agent running this skill. Delegating
11
+ judgment to a *second* LLM via an API call is strictly worse:
12
+
13
+ - **Less context.** A fresh API call sees only the prompt we construct. The agent
14
+ has the plan, the sources, the conversation, and the user's stated preferences.
15
+ - **More failure modes.** Rate limits, 503s, quota exhaustion, a second vendor, a
16
+ key on disk. During development, an optional text adapter cost ~10 wasted API
17
+ calls and three model fallbacks to do something the agent could do natively.
18
+ - **No reproducibility gain.** Judgment is not the part that needs to be
19
+ deterministic — the *merge and validation* around it is, and that stays in code.
20
+
21
+ ## Division of labour
22
+
23
+ | Stage | Owner | Network? |
24
+ |---|---|---|
25
+ | `planner.mjs` — parse, count, pack, assert coverage | **code** | none |
26
+ | enrichment — titles, forms, curation (skip / merge) | **agent** | none |
27
+ | `apply-enrichment.mjs` — merge + validate | **code** | none |
28
+ | script writing — narration, styles, beats | **agent** | none |
29
+ | audio — TTS | **code** | **Gemini TTS API** ← the only API call in the pipeline |
30
+ | publish config — chapter grouping, titles, blurb | **agent** | none |
31
+ | `publish.mjs` — resolve timecodes, emit chapters/captions, validate | **code** | none |
32
+ | thumbnail config — the hook, the still, the crop | **agent** | none |
33
+ | `thumbnail.mjs` — series label, Chromium screenshot, PNG check | **code** | none |
34
+ | `render.mjs` — Chromium frames, ffmpeg | **code** | none |
35
+
36
+ Everything except audio runs offline. The planner and the merge/validate step
37
+ require **no API key at all** — verified by running them with no key set.
38
+
39
+ ## Why the determinism lives in code
40
+
41
+ The agent's output is *checked*, not trusted. `apply-enrichment.mjs` rejects
42
+ empty titles, `Part N` placeholders, ellipses, directory-slug titles
43
+ (`load-balancing`), duplicate titles, forms outside the enum, references to videos
44
+ that don't exist, and `skip`/`mergeInto` entries without a stated reason. That is
45
+ the same discipline as
46
+ `compare-shots.mjs` for the scene port and the coverage assertion in the planner:
47
+ **assert, don't assume.**
48
+
49
+ ## The trap this rule avoids
50
+
51
+ A pipeline that reaches for an API whenever it needs "an LLM step" ends up with
52
+ two models, two quotas, two failure modes, and a reasoning step that is worse
53
+ than the agent it already had. If a stage needs judgment, the skill's `SKILL.md`
54
+ should say *what* to decide and let the host agent decide it — not shell out.
55
+
56
+ ## If you ever genuinely need unattended batch runs
57
+
58
+ Running 40 topics overnight with no agent present is a real use case, and it is
59
+ the *only* justification for an API-backed enrichment step. If that need appears,
60
+ add it back deliberately — as a clearly separate adapter, not as the default path
61
+ — and accept the quota and context costs knowingly.
@@ -0,0 +1,369 @@
1
+ # Pipeline — the stage sequence
2
+
3
+ How a directory of notes becomes a finished video. Eight stages.
4
+
5
+ **Bold = a stage where the agent supplies judgment.** Everything else is code.
6
+
7
+ > **The `/plan` … `/assemble` names are stage labels, not executable commands — there
8
+ > is still no dispatcher, and `package.json` declares no `bin`.** Every stage is a
9
+ > `node tools/<tool>.mjs` invocation, which is what each section shows.
10
+ >
11
+ > [`SKILL.md`](./SKILL.md) is the **agent-facing contract** — what to do, and each
12
+ > stage's `done when` gate. This file is the **flow and its rationale**: what each
13
+ > stage consumes and produces, and why it sits where it does. Where the two could
14
+ > drift, `SKILL.md` wins.
15
+
16
+ > **Path note.** The stage commands below still show the older single-work-dir form
17
+ > (`out/<name>`) in places; the per-video layout is `out/<name>/<NN>/`. Tracked as
18
+ > ROADMAP §2.9.8.
19
+
20
+ Companion docs: [`ARCHITECTURE.md`](./ARCHITECTURE.md) (who does what, and why no
21
+ LLM API is used for reasoning) · [`ROADMAP.md`](./ROADMAP.md) (what's missing).
22
+
23
+ ---
24
+
25
+ ## Prerequisites
26
+
27
+ | | |
28
+ |---|---|
29
+ | Node ≥ 22 | `render.mjs` uses the global `WebSocket` for CDP |
30
+ | `ffmpeg`, `ffprobe` | encoding, muxing, measurement |
31
+ | A Chromium build | headless frame capture |
32
+ | A Gemini API key | TTS only — stages 1–3 and 5–8 need no network; stored in the OS secret store, never in env or a file |
33
+
34
+ | Tool | Role |
35
+ |---|---|
36
+ | `tools/key-source.mjs` | resolves the key from the OS secret store — macOS Keychain · Linux `secret-tool` · Windows Credential Manager — never env or a plaintext file |
37
+ | `tools/tts-gemini.mjs` | Gemini TTS client — **the only file that calls an API** |
38
+
39
+ The full, cited, per-OS setup for all four prerequisites lives in
40
+ [`SETUP.md`](./SETUP.md) — the single source, kept in the skill folder so the skill is
41
+ self-contained. The per-stage files point at it too, and the tools print the relevant
42
+ command when a prerequisite is missing.
43
+
44
+ ---
45
+
46
+ ## The stages
47
+
48
+ ### 1 · `/plan` — decompose the source
49
+
50
+ ```bash
51
+ node tools/planner.mjs <notesDir> --out out/<name> --max-min 25
52
+ ```
53
+
54
+ There is **no dedupe flag and no name-based inference**. The planner excludes
55
+ exactly one category — navigation headings ("Table of Contents", "Contents",
56
+ "TOC", "Index") — because that is unambiguous in any tree. Everything else is
57
+ planned.
58
+
59
+ An earlier version inferred "index pointers" by matching section titles against
60
+ directory names. It was deleted: it silently dropped real content on unfamiliar
61
+ trees, gating it behind a flag only moved that burden onto the user, and on the
62
+ reference tree it changed no video count, no orphan count, and no longest video —
63
+ only the series total, by 3.2 min out of 62.5.
64
+
65
+ Errors (exit 1) on: a path that doesn't exist, a path that isn't a directory, or a
66
+ tree with no markdown at all.
67
+
68
+ | | |
69
+ |---|---|
70
+ | Tool | `tools/planner.mjs` |
71
+ | In | a directory of notes (markdown + code artefacts) |
72
+ | Out | `out/<name>/plan.json`, `out/<name>/plan.md` |
73
+ | Asserts | **coverage** — every content-bearing heading is assigned to a video, or is a navigation heading. Orphans exit non-zero. |
74
+ | Network | none |
75
+
76
+ Also emits a parser self-check: raw heading lines vs parsed headings, so a
77
+ swallowed section can't pass coverage vacuously.
78
+
79
+ **The duration model is a guardrail, not a prediction.** Both constants are now
80
+ **measured, not guessed**, and live in one place (`tools/model.mjs`): `wpm 153`
81
+ (Gemini Aoede, 508 words → 199s of audio) and `expansion 1.10` (205.7s picture /
82
+ 199s narration = 1.03, plus a small buffer). The planner, `script-check`, and
83
+ `pacing-check` all import from there, so the stages cannot disagree about the
84
+ budget. Use the plan to partition the source and keep each video under the cap;
85
+ take real durations from stage 4, which *measures* them.
86
+
87
+ ### 2 · `/enrich` — titles, forms, and curation
88
+
89
+ ```bash
90
+ node tools/apply-enrichment.mjs --plan out/<name>/plan.json \
91
+ --enrichment out/<name>/enrichment.json
92
+ ```
93
+
94
+ | | |
95
+ |---|---|
96
+ | **Agent writes** | `out/<name>/enrichment.json` — one entry per video |
97
+ | Tool | `tools/apply-enrichment.mjs` |
98
+ | Out | `plan.enriched.json`, `plan.enriched.md` |
99
+ | Network | none |
100
+
101
+ **Title contract.** The planner's titles are placeholders; the agent replaces them
102
+ with real ones — descriptive, **unique**, written for a human scrolling YouTube,
103
+ not a directory name. The content is given precisely: `plan.json` carries
104
+ `sectionSpans` (each section's `src`, `title`, and 1-based `line`/`endLine`), so the
105
+ agent titles from the substance it reads at those spans, not from a slug.
106
+ `apply-enrichment` enforces the floor: it rejects empty titles, `Part N`
107
+ placeholders, ellipses, **directory-slug titles** (`load-balancing`), **duplicates**,
108
+ forms outside the enum, and references to non-existent videos.
109
+
110
+ **Curation.** A deterministic planner cannot tell an overview page from real
111
+ content, so it plans both — the agent decides. Each entry may carry `skip: true` or
112
+ `mergeInto: <index>`, both requiring a stated `why`. `skip` drops the video and
113
+ records its sections; `mergeInto` folds the video's sections and words into the
114
+ target. Both land in `plan.removed` with the reason, so nothing is dropped silently
115
+ and coverage stays auditable.
116
+
117
+ ### 3 · `/script` — narration
118
+
119
+ | | |
120
+ |---|---|
121
+ | **Agent writes** | `out/<name>/scripts/<NN>-<slug>.json` — per segment: `key`, `text`, `style`, `visual`, `covers` |
122
+ | Tool | `tools/script-check.mjs` |
123
+ | Asserts | unique keys · speakability (no URLs, markdown, code) · duration budget vs the plan · **every source section the plan assigned is actually narrated** |
124
+ | Network | none |
125
+
126
+ `style` is sent to TTS as `speech_metadata` (delivery direction); `text` is sent
127
+ **verbatim**.
128
+
129
+ **Script contract.** The agent composes the spoken narration; `script-check` gates
130
+ it. The rules:
131
+
132
+ - **Register** — a calm, methodical teacher who explains through story: each
133
+ concept opens as a concrete scenario ("a request arrives…", "one server falls
134
+ over…"), then unpacks step by step. No edge, no hot takes; steady and warm;
135
+ analogies are welcome as teaching devices.
136
+ - **Granularity** — one segment per visual beat (one idea, one reveal). Roughly
137
+ 40–80 words; **≤130 words** hard ("one breath"). Split rather than bloat.
138
+ - **Per segment** — `key` (unique, short), `text` (spoken **verbatim**; no URLs,
139
+ markdown, or code), `style` (delivery direction), `visual` (a hint for the scene
140
+ stage), `covers` (the plan sections this segment narrates).
141
+ - **`covers`** — copy the exact section identity from `plan.enriched.json`
142
+ (`"src :: title @L<line>"`). Every assigned section must appear in some segment's
143
+ `covers`; `script-check` errors on any uncovered section.
144
+ - **Budget** — target `planTargetMinutes` from the plan; stay within its 55%–125%
145
+ band (`script-check` errors below, warns above).
146
+
147
+ ### 4 · `/audio` — TTS and the measured timeline
148
+
149
+ ```bash
150
+ node tools/build-audio.mjs --script out/<name>/scripts/NN-*.json --out out/<name>/<NN> --voice Aoede
151
+ ```
152
+
153
+ | | |
154
+ |---|---|
155
+ | Tool | `tools/build-audio.mjs` → `tools/tts-gemini.mjs` |
156
+ | Out | `narration/<seg>.wav`, `narration.wav`, `timeline.json`, `timeline.js` |
157
+ | Network | **yes — the only API-spending stage** |
158
+ | Notes | durations are **measured** with ffprobe, never estimated. Emits per-segment **windows** that tile the timeline. |
159
+
160
+ The cache is keyed on the **spoken text only** — `narration/<key>.src` stores the
161
+ exact text the `.wav` was built from — so re-running after a wording tweak costs 0
162
+ calls. It does **not** cover `--voice`, `style`, or `--model`: change one of those
163
+ and the previous audio is silently reused. Pass `--force` when you mean to
164
+ re-synthesize.
165
+
166
+ ### 5 · `/scene` — author the visuals
167
+
168
+ | | |
169
+ |---|---|
170
+ | **Agent writes** | `scenes/<name>.js` — one config per segment |
171
+ | Binding | `scenes/manifest.json` maps segment → `{host, scene}` |
172
+ | Tool | `tools/pacing-check.mjs` |
173
+ | Asserts | reveal rate 0.45–1.6 lines/sec · scene fits the **measured** narration · **dead air** (no visual change *and* no narration) ≤ 3.5s |
174
+ | Network | none |
175
+
176
+ Timings are written **relative to narration** — `"srp@4.6"` resolves to
177
+ `segment_start + 4.6`, so regenerating audio re-locks the choreography.
178
+
179
+ ### 6 · `/publish` — chapters and captions
180
+
181
+ | | |
182
+ |---|---|
183
+ | **Agent writes** | `out/<name>/<NN>/publish.json` — `chapters[]` (segment keys + `title`) and a `blurb` |
184
+ | Tool | `tools/publish.mjs` |
185
+ | Out | `description.md` (blurb + chapter block), `captions.srt` — copied to `--dest` |
186
+ | Asserts | first timecode `0:00` · ≥3 chapters · each ≥10s · ascending · every segment key resolves · titles present, ≤40 chars, not duplicated or near-duplicated · cues ascend, don't overlap, one per segment |
187
+ | Network | none |
188
+
189
+ Placed **before** the expensive stages on purpose. Both artifacts are pure functions of
190
+ data already frozen when stage 5 exits — the measured windows in `timeline.json`, and
191
+ the verbatim narration — so the metadata can be reviewed while a fix is still cheap.
192
+ No TTS, no Chromium, so re-running it is free.
193
+
194
+ Chapter **titles are authored** (read the chapter's narration, then name it); the
195
+ **times are resolved** from the windows, so a re-render regenerates correct timecodes
196
+ for free. Reusing the scene `stageNames` badges would cost nothing but is wrong: they
197
+ are presentation labels — all uppercase, one missing (the title card disables its
198
+ badge), near-duplicate pairs — and they resist mechanical title-casing, because a video
199
+ contains `IT · TOOLS`, which would become `It · Tools`.
200
+
201
+ Captions are one cue per segment, text taken from `narration/<key>.src` (the exact text
202
+ the WAV was built from — the audio is the truth, not `script.json`), emitted as **SRT
203
+ only**. YouTube accepts both SRT and VTT, so one file covers the upload; VTT matters
204
+ only for an HTML5 `<track>` in a web embed, and re-running this stage is free, so it is
205
+ deferred rather than pre-built. Finer cue splitting is possible only by estimation — the TTS
206
+ returns audio with **no word timings** — so it is deliberately not attempted.
207
+
208
+ **Chapters come from the description, nothing else.** YouTube builds them only from
209
+ timestamp lines in the **description**, start-time only. A caption file (SRT/VTT) does
210
+ **not** create chapters, and neither does embedded MP4 chapter metadata. The Data API
211
+ confirms the asymmetry: captions get a whole resource with an `insert` verb
212
+ ([`captions`](https://developers.google.com/youtube/v3/docs/captions)), while the
213
+ [`videos`](https://developers.google.com/youtube/v3/docs/videos) resource has no
214
+ chapters field anywhere — `snippet.description` is a plain string, and
215
+ `contentDetails.caption` is only a yes/no flag. Chapters therefore have nowhere to live
216
+ except the description text, which is why the block is emitted **inside**
217
+ `description.md` rather than as its own file or a caption by-product.
218
+
219
+ **Chapters also need channel eligibility.** They are a YouTube advanced feature: a channel
220
+ that has not qualified for advanced-feature eligibility renders **no** chapters at all,
221
+ however valid the list. Its description timestamps still work as jump-to-section links —
222
+ what is missing is the segmented progress bar and Google key moments. Nothing in
223
+ `description.md` can change this, so absent chapters are not a formatting fault.
224
+
225
+ ### 6b · `/thumbnail` — the upload thumbnail
226
+
227
+ | | |
228
+ |---|---|
229
+ | **Agent writes** | `out/<name>/<NN>/thumbnail.json` — the `hook` (≤5 words), a `still` rendered from one of the video's own scenes, an optional `focus` crop, `accent` and `badge` |
230
+ | Tool | `tools/thumbnail.mjs` + the host `src/thumbnail.html` |
231
+ | Out | `thumbnail.png` (1280x720, ≤2,000,000 bytes) — copied to `--dest/<NN>-<slug>/` beside `title.txt`; `thumbnail.small.png` (320x180) in the work dir for the phone-size look |
232
+ | Asserts | the hook is ≤5 words / 32 chars and does not repeat the upload title · a code-flow hook names no code · the still exists · the PNG is a real PNG, exactly 1280x720, within the upload's 2 MB · a series has a playlist decision (exit 3 otherwise) |
233
+ | Network | none |
234
+
235
+ Placed **after** `/publish` because the label carries the final upload title, read from
236
+ `title.txt`, and **before** `/render` because it needs only the stage-5 scenes, not the
237
+ finished MP4. It is one headless-Chromium screenshot of an HTML template, so iterating on
238
+ it is as cheap as stage 6.
239
+
240
+ For a series, a band reads `<playlist name>-Part X, <title>`, the first link of chronology
241
+ a new viewer sees. The playlist name is only ever the user's Stage 0 answer: the tool has
242
+ no default, and a plan of more than one video with neither `--playlist` nor `--standalone`
243
+ exits 3 so the agent asks. Part X is the video's 1-based position in
244
+ `plan.enriched.json`'s `videos`, after curation, never the raw `videoIndex`.
245
+
246
+ ### 7 · `/render` — frames to MP4
247
+
248
+ | | |
249
+ |---|---|
250
+ | Tool | `tools/render.mjs` (+ `tools/frame-dedup.mjs`) |
251
+ | Does | headless Chromium via CDP → PNG sequence → ffmpeg. Each **distinct** frame is captured once; repeats are hardlinked from their predecessor (measured 82% on a diagram scene, 90% on an svg scene, 99.7% on a held card). Guards: scenes declaring CSS/web animations take the plain capture path, and an audit every 30 frames diffs a real screenshot against the frame written. No flag — it is the only capture path. |
252
+ | Flags | `--scene`, `--shots` (QA stills), `--range t0,t1`, `--outdir`, `--name`, `--narration` · also `--fps`, `--crf`, `--shot-dir`, `--keep-frames` |
253
+ | Diagnose | `tools/debug-page.mjs` — prints boot-time console errors; `render.mjs` only reports "never exposed VIDEO_META" |
254
+
255
+ ### 8 · `/assemble` — the finished video
256
+
257
+ ```bash
258
+ node tools/assemble.mjs --script out/<name>/scripts/NN-*.json \
259
+ --work out/<name>/<NN> --name <output>
260
+ ```
261
+
262
+ | | |
263
+ |---|---|
264
+ | Tool | `tools/assemble.mjs` + `tools/verify-audio.mjs` |
265
+ | Does | per-segment time-sliced render (**silent**) → concat → mux narration **once** → verify |
266
+ | Asserts | windows tile the timeline with no gaps · finished audio matches the narration at **sample level** (residual energy ≤ 0.25) |
267
+ | Network | none |
268
+
269
+ Parts are rendered silent deliberately. Muxing narration per part restarts the
270
+ whole track at every segment boundary — a bug this stage used to have.
271
+
272
+ ---
273
+
274
+ ## Renderers
275
+
276
+ | File | Role |
277
+ |---|---|
278
+ | `src/scene-core.js` | diagram renderer — node kinds `client`/`server`/`hub`/`box` (UML), edges, flows, pings, rules, banners, overlays |
279
+ | `src/scene-diagram.html` | host; selects the config via `?scene=<name>` |
280
+ | `src/scene-code.js` | code + refactor renderer — code panels, before/after |
281
+ | `src/scene-code.html` | host; selects the config via `?scene=<name>` |
282
+ | `src/thumbnail.html` | stage 6b's 1280x720 thumbnail template; `tools/thumbnail.mjs` passes its config as `?c=<json>` |
283
+ | `<work>/timeline.js` | **generated** by stage 4 into the per-video work dir; browser-loadable measured timeline |
284
+
285
+ ---
286
+
287
+ ## End-to-end (video 6, as run)
288
+
289
+ ```bash
290
+ # 1 + 2 — plan, then enrich
291
+ node tools/planner.mjs <lld-notes> --out out/lld --max-min 25
292
+ # → agent writes out/lld/enrichment.json
293
+ node tools/apply-enrichment.mjs --plan out/lld/plan.json --enrichment out/lld/enrichment.json
294
+
295
+ # 3 — script (agent writes scripts/06-solid-principles-violated-and-fixed.json)
296
+ node tools/script-check.mjs --script out/lld/scripts/06-*.json --plan out/lld/plan.enriched.json
297
+
298
+ # 4 — audio
299
+ node tools/build-audio.mjs --script out/lld/scripts/06-*.json --out out/lld/06 --voice Aoede
300
+
301
+ # 5 — scenes (agent writes 7 configs in scenes/, registered in scenes/manifest.json)
302
+ node tools/pacing-check.mjs --scene out/lld/06/scenes/<name>.js --timeline out/lld/06/timeline.json --segment <key>
303
+
304
+ # 6 — publish (agent writes publish.json: chapters + blurb)
305
+ node tools/publish.mjs --work out/lld/06 --script out/lld/scripts/06-*.json --dest <destination>
306
+
307
+ # 6b — thumbnail (agent renders a still, writes thumbnail.json: hook + still)
308
+ node tools/thumbnail.mjs --work out/lld/06 --script out/lld/scripts/06-*.json \
309
+ --plan out/lld/plan.enriched.json --dest <destination> --playlist "<name>"
310
+
311
+ # 7 + 8 — render and assemble
312
+ node tools/assemble.mjs --script out/lld/scripts/06-*.json --work out/lld/06 --name video6-solid
313
+ ```
314
+
315
+ Result: `out/lld/06/video6-solid.mp4` — 7 segments, 205.70s of picture against
316
+ 199.00s of measured narration (the final segment holds past its narration).
317
+
318
+ ---
319
+
320
+ ## The authoring loop (where the real time goes)
321
+
322
+ Stage 5 is not a single command. The inner loop is:
323
+
324
+ ```
325
+ write / edit a scene config
326
+ → node tools/pacing-check.mjs … (timing, readability, dead air)
327
+ → node tools/render.mjs --shots 6,20,34 … (a handful of stills)
328
+ → LOOK at the stills ← the step nothing automates
329
+ → fix → repeat
330
+ ```
331
+
332
+ That loop ran roughly **eight times** for video 6.
333
+
334
+ **The checkers caught timing and structure. They never once caught a visual fault.**
335
+ Every visual bug — missing chips, orphaned connectors, green clients, an absent
336
+ fail marker, a box drawn from an empty API field — was found by looking at a
337
+ rendered frame.
338
+
339
+ ---
340
+
341
+ ## Bug log
342
+
343
+ What went wrong, and what actually surfaced it.
344
+
345
+ | Bug | Surfaced by |
346
+ |---|---|
347
+ | Diagram renderer read the SDK's convenience field instead of the REST path | dumping the real response |
348
+ | Retry logic fought a *daily* quota for 8 minutes | reading the error text |
349
+ | A `<script>` fence behind a list marker swallowed 3 headings | parser self-check added afterwards |
350
+ | Coverage passing *vacuously* (a subtree over-claim masked real orphans) | fixing the over-claim |
351
+ | Reveals 2.5× too fast to read | **the user's eye** |
352
+ | 7.3s of dead air at the end | **the user's eye** |
353
+ | Audio restarting at every segment boundary | **the user's ear** |
354
+ | A patch that silently matched nothing (`+ 30` had become `+ 32`) | the rendered frame still showing no marker |
355
+
356
+ The last four are the lesson: **cheap checks passed while the real question went
357
+ unasked.** Declaring "verified" off a per-stage check is not verification of the
358
+ artefact a human consumes.
359
+
360
+ ---
361
+
362
+ ## Current limitations
363
+
364
+ See [`ROADMAP.md`](./ROADMAP.md) §2. In short: renders run **one at a time** (the
365
+ shared Chromium `--port` — per-video work dirs mean the videos themselves are
366
+ independent), the Chromium and key locations are per-platform but have only been
367
+ verified on macOS (§2.1), and `SKILL.md` has not yet passed the **acceptance test** in §4:
368
+ hand a fresh agent the skill with no conversation history and see whether it ships a
369
+ video unaided.