@jokerized/decksmith 0.1.4 → 0.2.0

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.
package/README.md CHANGED
@@ -6,7 +6,7 @@ storyboard rendered to a 9:16 short or an MP4 without replanning.
6
6
 
7
7
  Rendering is bought, not built. A deck is a
8
8
  [HyperFrames](https://github.com/heygen-com/hyperframes) composition (Apache-2.0, pinned
9
- at 0.7.71), so the animation runtime, the headless capture and the FFmpeg encode are
9
+ at 0.8.27), so the animation runtime, the headless capture and the FFmpeg encode are
10
10
  upstream's. What DeckSmith owns is the part nothing else does well: turning a document
11
11
  into a *good explanation*, and being able to check that the explanation is true to its
12
12
  source.
@@ -20,8 +20,9 @@ source.
20
20
  - A Chromium build and ffmpeg to **render or check** one. `verify` drives the HyperFrames
21
21
  gates in a headless browser, and the MP4 encode is ffmpeg's. Both arrive with the
22
22
  hyperframes toolchain during `npm install`; there is nothing to install by hand.
23
- - Two commands reach outside for their own reasons: `plan` runs the Codex CLI, and
24
- `narrate` runs edge-tts over the network.
23
+ - Three commands reach outside for their own reasons: `plan` runs the Codex CLI, `narrate`
24
+ runs edge-tts over the network, and `illustrate` asks an image backend or that same Codex
25
+ account for pictures — and draws its own when neither will.
25
26
 
26
27
  ```sh
27
28
  npm install
@@ -37,9 +38,11 @@ hand-edit everything in between.
37
38
  ```sh
38
39
  decksmith ingest analysis.md -o source.json # structure, figures, equations, provenance
39
40
  decksmith plan source.json -o storyboard.json # beats — this is the one to read
41
+ decksmith illustrate storyboard.json --source source.json # optional, after plan --images
40
42
  decksmith narrate storyboard.json --source source.json -o audio/ # optional
41
43
  decksmith build storyboard.json --source source.json --format deck-16x9 -o out/
42
44
  decksmith verify out/
45
+ decksmith frames out/ # PNGs of the holds, to look at
43
46
  decksmith render out/ -o talk.mp4 # picture + narration + subtitles
44
47
  decksmith drift out/ # render twice, compare frame by frame
45
48
 
@@ -62,15 +65,31 @@ decksmith unpack talk.deck -o reopened/
62
65
 
63
66
  You can also skip it entirely. `storyboard.json` is just a file: hand-write it, or have
64
67
  any assistant write it, and `build` cannot tell the difference.
68
+ - **illustrate** — the pictures the plan asked for. With `plan --images`, a beat that has
69
+ no figure to show may carry a brief instead of a `figureId`; this turns each brief into
70
+ a file under `assets/`, registers it as an ordinary figure in `source.json`, and points
71
+ the beat at it. Optional, and nothing downstream knows the picture was generated. See
72
+ "Illustrations" below.
65
73
  - **narrate** — each beat's `narration` text, spoken by edge-tts, one audio file and one
66
- set of subtitle cues per *stop*. Optional, and the only command that needs the network.
67
- See "Narration" below.
74
+ set of subtitle cues per *stop*. Optional, and needs the network. See "Narration" below.
68
75
  - **build** — `Storyboard` to a composition, per format profile. Beats become scenes,
69
76
  scenes become their own paused GSAP timelines, hold points become slideshow fragments.
70
77
  Narration sitting beside the storyboard is picked up automatically.
71
78
  - **verify** — runs the HyperFrames gates over a built directory and returns a `Verdict`:
72
79
  lint, runtime, layout, motion and contrast. Read what that does *not* cover before
73
80
  trusting it — see "What the gates do not check" below.
81
+ - **frames** — writes a PNG per hold (or per `--at <seconds>`) so you can look at the deck
82
+ rather than at a verdict. It makes the same three calls the `fidelity` gate makes — the
83
+ pinned runtime, `renderSeek(t, { suppressEvents: true })`, then a screenshot at the
84
+ renderer's own clip — which is the path that was measured against a real mp4 to within
85
+ 0.11 percentage points of ink. It is the gate's own view of a deck, which is the thing
86
+ worth looking at when you are asking why a gate said what it said.
87
+ **It is not a substitute for watching the render.** On callback-driven motion the three
88
+ available views disagree, measured on a deck with a band painted only from a GSAP
89
+ `onUpdate`: `frames` leaves it at the background's RGB (11,13,17) because
90
+ `suppressEvents` stops the callback; `hyperframes snapshot` draws it mid-tween at
91
+ (143,4,5); and the render animates it smoothly to (205,0,0) over the tween's own three
92
+ seconds. See "What the gates do not check".
74
93
  - **render** — a built deck to a finished video: capture, retime, mux, subtitles. The
75
94
  retiming is the interesting part. The composition reveals everything a beat has to show
76
95
  and then sits still for the rest of its window, so playing it back linearly puts the
@@ -79,7 +98,9 @@ decksmith unpack talk.deck -o reopened/
79
98
  whatever the scene actually ends on. Needs Chromium and ffmpeg, and needs the
80
99
  `timing.json` that `build` writes.
81
100
  - **drift** — renders the deck twice and compares every frame. `--identical` fails on any
82
- differing byte, which is only honest for an image-free deck with no camera;
101
+ differing byte, which is only honest for an image-free deck with no camera — the demo
102
+ deck is not one, and fails it on both hyperframes pins (see
103
+ `.planning/2026-09-04-hyperframes-0.8.27.md`);
83
104
  the default compares PSNR against a 40 dB floor. Costs two full renders, so it is a
84
105
  thing you schedule rather than a thing you run per build. See
85
106
  `.planning/EXPERIMENT-006-diagrams.md` for why byte-identical is not available in
@@ -94,7 +115,9 @@ out/
94
115
  index.html the composition — what check, snapshot and render consume
95
116
  deck.html open this: the navigable deck
96
117
  hyperframes-player.global.js copied from the hyperframes package, so nothing needs a CDN
97
- hyperframes.json assets/
118
+ hyperframes.json
119
+ assets/ every figure a beat cites — generated ones included, under
120
+ the same name source.json records
98
121
  audio/ only when the deck is narrated
99
122
  ```
100
123
 
@@ -116,7 +139,8 @@ exist only on the deck page: ambient motion is gated behind a class the composit
116
139
  sets, so `render` output is byte-identical with or without it.
117
140
 
118
141
  Navigation is ours rather than upstream's because upstream's is broken at 0.7.71/0.7.72
119
- — `player.scenes` never populates, reproduced on HeyGen's own reference example. The
142
+ and still broken at 0.8.27 — `player.scenes` never populates, reproduced on HeyGen's own
143
+ reference example, and at 0.8.27 the property is not on the player at all. The
120
144
  same layer also paints the composition, because the standalone player moves its clock
121
145
  without driving scene timelines or clip visibility. See
122
146
  `.planning/EXPERIMENT-003-deck-mode.md` and `-004-step-layer.md`.
@@ -172,18 +196,19 @@ cut runs 4m07s.
172
196
  ```sh
173
197
  decksmith build storyboard.json --source source.json --format short-9x16 -o short/
174
198
  # build: 9 of 12 beats at 1080×1920 in ink → short/index.html
175
- # build: cut b03 (annotated-figure, 27.5s) — Cut to fit short-9x16's 3m00s: 27.5s for
176
- # weight 0.9 is 0.033 weight per second, and the beats kept buy more per second.
199
+ # build: cut b04 (grid, 19.7s) — Cut to fit short-9x16's 3m00s: 19.7s for weight 0.85
200
+ # is 0.043 weight per second, and the beats kept buy more per second.
177
201
  # Its family (structure) still has 3 beat(s) in the cut.
178
202
  # build: cut b06 (stack, 24.7s) — …
179
- # build: cut b11 (claim-figure, 15.5s) — …
203
+ # build: cut b09 (data-table, 25.4s) — …
180
204
  # PASS — 0 error(s), 2 warning(s)
181
205
  ```
182
206
 
183
207
  The rule is `selectBeats` (`src/plan/select.ts`), and it is not "drop the lightest". In
184
208
  priority order it keeps a cut **coherent**, then **covered** — the deck's first and last
185
- beat, and one beat of every archetype family the full deck used then **fitting**, and
186
- only then heaviest. Author weight is the last tiebreak, because weight says how much a
209
+ beat, one beat of every archetype family the full deck used, and one beat carrying a
210
+ figure of the source's, because a deck that cuts its way to no picture at all cannot get
211
+ one back by rewording — then **fitting**, and only then heaviest. Author weight is the last tiebreak, because weight says how much a
187
212
  beat matters and nothing about what it costs: two 14-second beats at 0.80 are worth more
188
213
  to a three-minute budget than one 39-second beat at 0.95, and a threshold cannot say so.
189
214
 
@@ -238,8 +263,13 @@ decksmith: http://127.0.0.1:8475
238
263
  decksmith: work /tmp/decksmith-server, one job at a time, 8 may wait
239
264
  decksmith: no auth, no TLS. Bound to 127.0.0.1 — set DECKSMITH_HOST to open it, knowing that.
240
265
  decksmith: codex, edge-tts and ffmpeg all found
266
+ decksmith: images via codex, then svg
241
267
  ```
242
268
 
269
+ The last line is where `illustrate` would get its pictures — `via openai` when a backend
270
+ is configured, or the reason it cannot be used. Reported, never fatal: a job that asks
271
+ for pictures still finishes, on the tool's own SVG if it has to.
272
+
243
273
  ### The HTTP surface
244
274
 
245
275
  | Route | What it does |
@@ -251,7 +281,8 @@ decksmith: codex, edge-tts and ffmpeg all found
251
281
  | `GET /d/:id/...` | the built deck, served statically; `/d/:id/deck.html` is the player |
252
282
 
253
283
  Options on `POST`, all optional, all defaulted server-side: `format`, `width`+`height`,
254
- `theme`, `slides`, `lang`, `tone`, `density`, `speed`, `narrate`, `voice`, `video`.
284
+ `theme`, `slides`, `lang`, `tone`, `density`, `speed`, `narrate`, `voice`, `images`,
285
+ `video`. `images` lets the plan ask for pictures and runs the `illustrate` stage after it.
255
286
 
256
287
  `width`/`height` override the named preset's canvas and **keep its pacing but not its
257
288
  name** — `--format short-9x16 --width 1080 --height 1350` builds `custom-1080x1350` with
@@ -280,6 +311,10 @@ generous one.
280
311
  | `DECKSMITH_REQS_PER_MIN` | `240` | Per IP. |
281
312
  | `DECKSMITH_FETCH_FIGURES` | off | See below. |
282
313
  | `DECKSMITH_DECK_SANDBOX` | on | Serves decks under `CSP: sandbox`. |
314
+ | `DECKSMITH_IMAGES` | unset | `openai` names a separate image backend for `illustrate`. Unset, pictures come from the Codex account, then the tool's own SVG. |
315
+ | `DECKSMITH_IMAGES_API_KEY` | | The backend's key. Environment only — never a preference, a config file, a `.deck`, or an error message. |
316
+ | `DECKSMITH_IMAGES_BASE_URL` | `https://api.openai.com/v1` | Any OpenAI-compatible `images/generations` endpoint. |
317
+ | `DECKSMITH_IMAGES_MODEL` | `gpt-image-2` | The backend's model. The Codex rung draws with the account's own. |
283
318
 
284
319
  **Remote figures are off by default, and that is a security decision.** `fetchFigures`
285
320
  does `readFile(src)` on anything that is not an http URL, so an uploaded document
@@ -344,7 +379,7 @@ Four tools:
344
379
 
345
380
  | tool | what it does |
346
381
  |---|---|
347
- | `decksmith_capabilities` | formats, themes, every setting's range, and which of Codex, edge-tts, ffmpeg and Chrome are installed |
382
+ | `decksmith_capabilities` | formats, themes, every setting's range, which of Codex, edge-tts, ffmpeg and Chrome are installed, and where `illustrate` would get its pictures (`images: { backend, ok, why }`) |
348
383
  | `decksmith_estimate_length` | what a duration/slides/density combination costs, and what it cannot buy — instant, no job |
349
384
  | `decksmith_create_deck` | document + settings → a deck, and optionally an mp4 |
350
385
  | `decksmith_job_status` | where a job got to |
@@ -362,7 +397,9 @@ they are the product telling you what your settings cost.
362
397
 
363
398
  No setting has a default in the tool schema. Absence is the signal: a theme you did not
364
399
  mention loses to the storyboard's own, and a language you did not mention loses to the
365
- document's.
400
+ document's. `images: true` lets the plan ask for pictures and draws them before the build;
401
+ it is refused only when a backend is named in the environment and broken, because the
402
+ tool's own SVG means a request never fails for lack of one.
366
403
 
367
404
  ## Using it as a library
368
405
 
@@ -468,6 +505,7 @@ for it; call `verify` yourself when you want it.
468
505
  |---|---|
469
506
  | ingest | `parseMarkdown`, `fetchFigures`, `bundleFont` |
470
507
  | plan | `codexPlanner` with its `Runner` type, `assertRefsResolve`, `systemPrompt`, `renderSource` |
508
+ | images | `illustrate`, `imageChain`, `resolveImageBackend`, `hasIllustrations`, with `ImageProvider`, `ImageRequest`, `ImageResult`, `IllustrateOpts` |
471
509
  | narrate | `narrate`, `pickVoice`, `narratableLangs` |
472
510
  | emit | `buildDeck`, `emitDeck`, `emitComposition`, `resolveTheme`, `THEMES`, `THEME_NAMES` |
473
511
  | verify | `verify`, `check`, `parseCheckReport` |
@@ -480,6 +518,9 @@ an SDK instead of a subprocess, which is what a server actually wants — `codex
480
518
  shells to the Codex CLI only because that is the right default at a terminal. And
481
519
  `emitDeck` sits underneath `buildDeck` for callers writing to object storage or a response
482
520
  body rather than a filesystem: it is pure, taking strings in and returning strings out.
521
+ `ImageProvider` is the same kind of seam as `Runner`: `illustrate` takes a `chain` of them,
522
+ so a server's tests draw with a fake instead of spawning Codex, and a deployment can add a
523
+ backend this package does not ship.
483
524
 
484
525
  Anything not listed is deliberately absent, and adding to the list is a promise we cannot
485
526
  quietly take back. `prefsFromFlags` is the clearest example: it translates commander's flag
@@ -504,6 +545,11 @@ invalidates a storyboard you have already edited.
504
545
  | `narration.rate` | `+0%` | narrate | edge-tts prosody |
505
546
  | `narration.pitch` | `+0Hz` | narrate | edge-tts prosody |
506
547
  | `narration.subtitles` | `true` | narrate | write subtitle cues alongside the audio |
548
+ | `images.enabled` | `false` | plan, illustrate | the planner may ask for a picture where no figure fits |
549
+ | `images.provider` | `codex` | illustrate | `auto` · `codex` · `svg` — where the chain of rungs starts |
550
+ | `images.model` | unset | illustrate | model for the separate backend only; the Codex rung uses the account's own |
551
+ | `images.style` | `flat vector illustration` | illustrate | one phrase folded into every picture prompt |
552
+ | `images.max` | `4` | illustrate | most pictures drawn through the chain; the rest are the tool's SVG |
507
553
 
508
554
  Three layers, in increasing precedence: these defaults, then the nearest
509
555
  `decksmith.config.json` found by walking up from the working directory, then flags.
@@ -523,6 +569,12 @@ governs a deck built from a subdirectory without anyone naming a path.
523
569
  "voice": "ko-KR-HyunsuMultilingualNeural",
524
570
  "rate": "+8%",
525
571
  "subtitles": true
572
+ },
573
+ "images": {
574
+ "enabled": true,
575
+ "provider": "codex",
576
+ "style": "woodcut, two colours",
577
+ "max": 4
526
578
  }
527
579
  }
528
580
  ```
@@ -536,8 +588,11 @@ decksmith.config.json: unknown preference "narration.speed". Valid: enabled, voi
536
588
  ```
537
589
 
538
590
  On the command line: `--slides --lang --tone --density` on `plan`, `--theme --speed` on
539
- `build`, `--voice --rate --pitch --no-subtitles` on `narrate`, and all of them on `pack`,
540
- which records the preferences the deck was made under.
591
+ `build`, `--voice --rate --pitch --no-subtitles` on `narrate`, `--images --image-provider
592
+ --image-model --image-style --image-max` on `plan` and `illustrate`, and all but the image
593
+ flags on `pack`, which records the preferences the deck was made under — whether it was
594
+ illustrated is read off the storyboard itself, the way `narration.enabled` is read off the
595
+ narration beside it.
541
596
 
542
597
  A preference sitting at its default says nothing, so a stored artifact wins over it and
543
598
  loses to anything you type. `plan` stamps `lang` and `theme` into the storyboard it
@@ -596,8 +651,81 @@ directory; nothing else notices, because a missing file looks identical to a bro
596
651
  declined to play.
597
652
 
598
653
  `edge-tts` must be on your PATH, or installable as `python3 -m edge_tts`; set
599
- `DECKSMITH_EDGE_TTS` to point at it directly. It is the only part of DeckSmith that needs
600
- the network, which is why it is its own command and not a step inside `build`.
654
+ `DECKSMITH_EDGE_TTS` to point at it directly. Narration and illustration are the two parts
655
+ of DeckSmith that need the network, which is why each is its own command and not a step
656
+ inside `build`.
657
+
658
+ ## Illustrations
659
+
660
+ ```sh
661
+ decksmith plan source.json -o storyboard.json --images
662
+ decksmith illustrate storyboard.json --source source.json
663
+ decksmith build storyboard.json --source source.json -o out/
664
+ ```
665
+
666
+ With `--images`, a beat that has nothing in the inventory to show — a `claim-figure`, or
667
+ either side of a `split-compare` — may carry an `illustration: { prompt, caption }` in
668
+ place of a `figureId`. The prompt describes a scene, never text, labels, numbers or charts:
669
+ nothing inside a picture can be read or checked, so the picture illustrates and the beat's
670
+ `evidence` still points at the section. `plan` says how many pictures the storyboard asks
671
+ for and the command to run; `build` and `pack` refuse the file until they exist.
672
+
673
+ `illustrate` turns each brief into a file under `assets/` beside `source.json`, registers
674
+ it as an ordinary figure, and sets the beat's `figureId`; the brief stays on the beat as
675
+ provenance. From there nothing downstream knows the picture was generated — `build`,
676
+ `pack`, `verify` and the server see a figure like any other. A deck that never asks for a
677
+ picture builds exactly as it always did, byte for byte.
678
+
679
+ Three rungs, tried in order, and every hop down is printed rather than swallowed:
680
+
681
+ 1. **A separate image backend**, if you configured one AND asked for it with
682
+ `--image-provider auto`. `DECKSMITH_IMAGES=openai` names
683
+ any OpenAI-compatible `images/generations` endpoint (`DECKSMITH_IMAGES_BASE_URL`,
684
+ default `https://api.openai.com/v1` — LocalAI and gateways speak it too),
685
+ `DECKSMITH_IMAGES_API_KEY` is the key, and `DECKSMITH_IMAGES_MODEL` overrides the model
686
+ (default `gpt-image-2`). Environment only, like edge-tts: the key is never a preference,
687
+ never in a config file, never in a `.deck`, and never part of an error message. Naming
688
+ a backend without its key is an error where the backend is resolved — at `illustrate`,
689
+ in the server's startup banner, in `decksmith_capabilities` — and nowhere at import.
690
+ 2. **The Codex account that planned the deck.** Codex 0.149 ships `image_generation` as a
691
+ stable feature, so the same `codex exec` that wrote the storyboard can draw a PNG.
692
+ `illustrate` runs it in a scratch directory it cannot write outside of and reads the
693
+ picture back — and checks it: the agent looks at what it drew, redraws once if the
694
+ picture has letters, words or numbers in it, and refuses rather than handing back a
695
+ picture with text. **Nothing downstream can see text inside a picture** — the 40px
696
+ audience floor measures the document's own text — so the check happens where the
697
+ image and an eye are in the same place, and a refusal simply falls to the next rung.
698
+ An account without the image tool says so, once, and the run falls through too.
699
+ Which ACCOUNT is the shell's business, not the deck's: `codex exec` inherits the
700
+ environment, so `CODEX_HOME=~/.codex-other decksmith illustrate …` draws on that home.
701
+ Note that Codex keeps its own copy of every picture it makes, under
702
+ `$CODEX_HOME/generated_images/<session>/`, and `--ephemeral` does not remove it — the
703
+ deck's own copy is the one under `assets/`, and that directory grows by roughly a
704
+ megabyte per picture until you empty it.
705
+ 3. **An SVG the tool draws itself**: a deterministic, text-free composition seeded from
706
+ the brief. It cannot fail, so `illustrate` always finishes.
707
+
708
+ `images.provider` says where the chain starts. **`codex` is the default** — the account
709
+ that planned the deck draws the picture, and falls through to the tool's SVG; a metered
710
+ backend is never reached unless you ask for it, because a default should not be the
711
+ branch that spends money. `auto` puts a configured backend in front, and `svg` is the
712
+ tool alone — no network, no spend, and a deck whose every
713
+ picture is reproducible. A rung that fails is not asked again in the same run, so an
714
+ account with no image tool pays one `codex exec` to find out, not one per picture.
715
+
716
+ Pictures are content-addressed on the rung, model, aspect, style and prompt, the way
717
+ narration is on its text and voice: re-running after an edit redraws only the briefs that
718
+ moved, and a second run over a finished storyboard calls nothing at all. `images.max` caps
719
+ how many pictures go through the chain — a `split-compare` with two briefs spends two —
720
+ and the rest are the tool's SVG. **Pictures cost what narration does not**: a backend
721
+ bills per image and the Codex rung spends the account's own usage, so the cap is there
722
+ to bound a plan that asks for twelve. `--image-provider svg` is the way to try the layout
723
+ for free.
724
+
725
+ Not a rung, on purpose: asking the model to write SVG. An SVG shown through `<img>` can
726
+ carry SMIL or CSS animation, which runs on wall-clock time under capture — a
727
+ nondeterministic render that every gate passes. The tool's own SVG has no text, no
728
+ animation and no external references, and the same brief always draws the same bytes.
601
729
 
602
730
  ## The `.deck` container
603
731
 
@@ -693,7 +821,9 @@ learns what a camera frustum or an orderbook is.
693
821
  surprise.
694
822
  2. Write `src/emit/archetypes/<name>.ts` as an `Emitter<"<name>">`. It returns a `Scene`:
695
823
  inner HTML, GSAP statements, hold points, and its own CSS. It owns one scene's insides
696
- and nothing else.
824
+ and nothing else. If it draws labelled parts a camera could fly into, fill `Scene.parts`
825
+ in the same loop that gives them their ids — `inside.element` is an index, and that map
826
+ is the only thing that can tell the plan's third stage from the picture's.
697
827
  3. Register it in `src/emit/archetypes/index.ts`.
698
828
 
699
829
  Nothing else changes. The document shell, the deck runtime, the format profiles and the
@@ -702,9 +832,12 @@ verify gates never learn the new name.
702
832
  ## Deck navigation is ours
703
833
 
704
834
  The step layer in `src/deck/` is DeckSmith code, not upstream's, because upstream deck
705
- navigation does not work at 0.7.71. `player.scenes` never populates, so
835
+ navigation does not work. `player.scenes` never populates, so
706
836
  `SlideshowController` has no slide-to-time map to bind and every key press is a no-op —
707
- reproduced on HeyGen's own unmodified reference example, on both 0.7.71 and 0.7.72. What
837
+ reproduced on HeyGen's own unmodified reference example, on 0.7.71 and 0.7.72,
838
+ measured again on 0.7.90 when the pin moved (`player.scenes` still 0; the slideshow
839
+ bundle byte-identical to 0.7.71's), and again on 0.8.27, where `scenes` is not a
840
+ property of the player at all — thirty minor versions and the hole is wider. What
708
841
  does work, exactly as documented, is `player.seek(t)`. So we read the slideshow island,
709
842
  map steps to absolute times, and drive `seek()` ourselves. It is about a hundred lines and
710
843
  it stops the primary deliverable from being blocked on someone else's roadmap.
@@ -766,16 +899,17 @@ mechanically rather than free-hand.
766
899
 
767
900
  ```
768
901
  src/index.ts the library surface — the only file consumers import
769
- src/cli.ts the nine verbs, argv and stderr
902
+ src/cli.ts the ten verbs, argv and stderr
770
903
  src/types.ts the contract: Source, Storyboard, Beat, Format, Verdict
771
904
  src/prefs.ts the three-layer preference resolver
772
905
  src/emit/kit.ts the seam between the deck shell and the archetype emitters
773
906
  src/emit/archetypes/ one emitter per archetype
774
907
  src/emit/themes/ one palette per file; the registry is the extension point
908
+ src/images/ the three rungs a brief is drawn through, and the illustrate step
775
909
  src/narrate/ edge-tts, one segment per stop
776
910
  src/pack/ the .deck container and its bake/link/embed policy
777
911
  src/deck/ our step layer over player.seek(), and the subtitle reader
778
- src/server/ `npm run serve` — routes, queue, the five stages, and the one page
912
+ src/server/ `npm run serve` — routes, queue, the six stages, and the one page
779
913
  (ui.ts). Everything here reaches the library through ../index.js
780
914
  only; see the note at the top of ui.ts for what breaks otherwise.
781
915
  .planning/ the design sketch and the experiment writeups