@jokerized/decksmith 0.8.0 → 0.9.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 (64) hide show
  1. package/README.md +333 -4
  2. package/dist/cli.js +14057 -4749
  3. package/dist/deck-player-element.js +1 -1
  4. package/dist/deck-player.js +1 -1
  5. package/dist/deck-runtime.js +70 -2
  6. package/dist/index.js +13309 -4178
  7. package/dist/mcp.js +5131 -765
  8. package/dist/types/bespoke/art.d.ts +173 -0
  9. package/dist/types/bespoke/cache.d.ts +69 -0
  10. package/dist/types/bespoke/callouts.d.ts +76 -0
  11. package/dist/types/bespoke/contract.d.ts +57 -0
  12. package/dist/types/bespoke/inspect.d.ts +69 -0
  13. package/dist/types/bespoke/pipeline.d.ts +207 -0
  14. package/dist/types/bespoke/probe.d.ts +34 -0
  15. package/dist/types/bespoke/prompt.d.ts +182 -0
  16. package/dist/types/bespoke/references.d.ts +73 -0
  17. package/dist/types/bespoke/repair.d.ts +121 -0
  18. package/dist/types/bespoke/scene.d.ts +97 -0
  19. package/dist/types/bespoke/select.d.ts +52 -0
  20. package/dist/types/bespoke/sheet.d.ts +26 -0
  21. package/dist/types/bespoke/shots.d.ts +128 -0
  22. package/dist/types/bespoke/untangle.d.ts +5 -0
  23. package/dist/types/build/files.d.ts +13 -1
  24. package/dist/types/deck/motion.d.ts +64 -0
  25. package/dist/types/deck/playback.d.ts +206 -0
  26. package/dist/types/deck/repack.d.ts +10 -0
  27. package/dist/types/deck/runtime.d.ts +96 -1
  28. package/dist/types/deck/subtitles.d.ts +29 -1
  29. package/dist/types/emit/archetypes/annotated-figure.d.ts +1 -1
  30. package/dist/types/emit/archetypes/bar-compare.d.ts +0 -28
  31. package/dist/types/emit/archetypes/callout.d.ts +0 -5
  32. package/dist/types/emit/archetypes/claim-figure.d.ts +15 -0
  33. package/dist/types/emit/archetypes/grid.d.ts +0 -14
  34. package/dist/types/emit/archetypes/index.d.ts +0 -7
  35. package/dist/types/emit/archetypes/line-chart.d.ts +0 -8
  36. package/dist/types/emit/archetypes/pipeline.d.ts +15 -1
  37. package/dist/types/emit/archetypes/stack.d.ts +1 -1
  38. package/dist/types/emit/composition.d.ts +83 -3
  39. package/dist/types/emit/faces.d.ts +22 -0
  40. package/dist/types/emit/fit.d.ts +127 -0
  41. package/dist/types/emit/kit.d.ts +121 -2
  42. package/dist/types/emit/look.d.ts +138 -0
  43. package/dist/types/emit/motion.d.ts +244 -0
  44. package/dist/types/emit/svg.d.ts +54 -16
  45. package/dist/types/emit/theme.d.ts +1 -1
  46. package/dist/types/emit/themes/index.d.ts +19 -1
  47. package/dist/types/emit/themes/packs.d.ts +15 -0
  48. package/dist/types/emit/themes/pick.d.ts +107 -0
  49. package/dist/types/emit/type.d.ts +111 -0
  50. package/dist/types/index.d.ts +52 -1
  51. package/dist/types/plan/codex.d.ts +36 -0
  52. package/dist/types/plan/direct.d.ts +66 -0
  53. package/dist/types/prefs.d.ts +25 -2
  54. package/dist/types/render/capture.d.ts +6 -0
  55. package/dist/types/render/timing.d.ts +3 -0
  56. package/dist/types/source/fonts.d.ts +11 -1
  57. package/dist/types/types.d.ts +109 -0
  58. package/dist/types/verify/check.d.ts +1 -1
  59. package/dist/types/verify/fidelity.d.ts +34 -0
  60. package/dist/types/verify/fill.d.ts +205 -0
  61. package/dist/types/verify/index.d.ts +14 -0
  62. package/dist/types/verify/motion.d.ts +33 -0
  63. package/dist/types/verify/scenes.d.ts +325 -0
  64. package/package.json +5 -1
package/README.md CHANGED
@@ -220,6 +220,49 @@ Arrow keys, Space and PageUp/PageDown step; clicking the left or right third doe
220
220
  notes; `f` is fullscreen; `m` mutes the voice and `s` hides the subtitles. Every step is
221
221
  deep-linkable (`#3` is slide 3, `#3.2` its second reveal).
222
222
 
223
+ ### The v2 player (`--design v2`)
224
+
225
+ `build --design v2` (or `"design": "v2"` in the storyboard or config) gives `deck.html` the
226
+ v2 player; everything else about the build is unchanged. A deck that is already built
227
+ gets it without rebuilding:
228
+
229
+ ```bash
230
+ decksmith repack out/ # writes out/deck2.html beside deck.html, never over it
231
+ ```
232
+
233
+ `repack` swaps the inlined runtime, marks the page and checks that every JSON island is
234
+ byte-identical — no Chrome, no TTS, no re-render.
235
+
236
+ - **One control bar**: play/pause, the counter, narration speed (0.75–2×), `CC`, caption
237
+ size (S/M/L/XL) and fullscreen — or, in a window too narrow for those beside a caption,
238
+ play and one settings menu. **It never covers text.** It docks under the slide in spare
239
+ letterbox (a portrait phone); over the slide's empty bottom padding when that holds a
240
+ 40px target (a slide about 720px tall or more), where it hides 2.5s after the last
241
+ pointer move while the deck plays; otherwise in a band under the slide that it shares
242
+ side by side with the captions, whose text is padded in past the buttons.
243
+ `test/deck-page.test.ts` measures controls against caption text and against every glyph
244
+ the slide draws, at six viewports.
245
+ - **Play/pause** pauses the voice where it is and carries on from the same word. A deck
246
+ opens paused; stepping while paused moves the slide silently.
247
+ - **Speed** sets the narration's `playbackRate` live, on the sentence already playing, and
248
+ divides the reveal glides and every autoplay wait by the same factor so they keep pace.
249
+ A stop with no narration holds for its reveal and 1.5s at 1×, not the author's gap.
250
+ - **Captions** are sized from the slide (3% of its height, 13px floor; L and XL keep floors
251
+ of 16 and 19px so the size control works on a phone) and the slide gives up only one
252
+ two-line strip for them: 84% of the frame at 1080p, 83% in an 800×450 embed (v0.8.0: 77%
253
+ and 69%), 100% in a portrait letterbox. Long cues are cut to two lines of the strip's
254
+ actual width, which also splits Japanese and Chinese.
255
+ - **Keys:** Space steps forward (Space is the presenter's key, not play); `Enter` plays and
256
+ pauses; `c` (or `s`) captions; `<`/`>` speed; `+`/`-` caption size. Letter keys fall back
257
+ to the physical key under a Korean or Japanese IME. Anything held with Cmd, Ctrl or Alt
258
+ is left to the browser.
259
+ - **Preferences** — speed, captions on/off, size — come from `?speed=1.25&cc=0&ccsize=l`
260
+ (this visit only), else from `localStorage` (`decksmith.prefs.v1`), else the defaults.
261
+ When the viewer changes one, the deck posts
262
+ `{ type: "decksmith:prefs", speed, cc, ccsize }` to its parent so an embedding site can
263
+ keep it per user (a cross-site iframe's storage is partitioned), and it applies the same
264
+ message when the parent sends it. Check `event.origin` on your side.
265
+
223
266
  Stepping forward *plays* the reveal rather than cutting to it — the step layer sweeps the
224
267
  composition's timelines across frames instead of seeking once. Backward steps, `Home`/`End`
225
268
  and deep links cut, because entrance tweens run in reverse look like elements un-drawing
@@ -784,8 +827,11 @@ invalidates a storyboard you have already edited.
784
827
  | `lang` | the source's | plan | BCP-47. Drives the copy, the voice, and the font subset |
785
828
  | `tone` | `plain` | plan | `plain` · `academic` · `conversational` · `punchy` |
786
829
  | `density` | `normal` | plan | `sparse` · `normal` · `dense` — how much text a slide may carry |
787
- | `theme` | `ink` | emit | `ink` · `paper` · `mono` |
830
+ | `theme` | `ink` | emit | `ink` · `paper` · `mono`, or a v2 style pack by name (see Themes) |
831
+ | `design` | `classic` | narrate, emit | `classic` is the v0.8.0 look byte for byte · `v2` is the redesign: the v2 player (see "The v2 player") and a style pack per deck when no theme is named |
832
+ | `packSeed` | the source id | narrate, emit | what `design: v2` hashes to pick a pack; pass a paper id so its languages share one |
788
833
  | `animationSpeed` | `1` | emit | multiplies every duration, hold and beat length. Below 1 is faster |
834
+ | `design` | `classic` | emit | `classic` (the 0.8 look, byte-identical) · `v2` (the redesign — see [v2 motion](#v2-motion)) |
789
835
  | `narration.voice` | picked for `lang`+`tone` | narrate | an explicit edge-tts voice id |
790
836
  | `narration.rate` | `+0%` | narrate | edge-tts prosody |
791
837
  | `narration.pitch` | `+0Hz` | narrate | edge-tts prosody |
@@ -832,8 +878,8 @@ why `slideCount` did nothing.
832
878
  decksmith.config.json: unknown preference "narration.speed". Valid: enabled, voice, rate, pitch, subtitles.
833
879
  ```
834
880
 
835
- On the command line: `--slides --lang --tone --density` on `plan`, `--theme --speed` on
836
- `build`, `--voice --rate --pitch --no-subtitles` on `narrate`, `--images --image-provider
881
+ On the command line: `--slides --lang --tone --density` on `plan`, `--theme --design
882
+ --pack-seed --speed` on `build` (`--design --pack-seed` on `narrate` too), `--voice --rate --pitch --no-subtitles` on `narrate`, `--images --image-provider
837
883
  --image-model --image-style --image-max` on `plan` and `illustrate`, and all but the image
838
884
  flags on `pack`, which records the preferences the deck was made under — whether it was
839
885
  illustrated is read off the storyboard itself, the way `narration.enabled` is read off the
@@ -841,9 +887,255 @@ narration beside it.
841
887
 
842
888
  A preference sitting at its default says nothing, so a stored artifact wins over it and
843
889
  loses to anything you type. `plan` stamps `lang` and `theme` into the storyboard it
844
- writes; `build` then uses the storyboard's unless `--theme` or a config file restates one.
890
+ writes (and `design`, only when one was asked for); `build` then uses the storyboard's
891
+ unless `--theme`/`--design` or a config file restates one.
845
892
  Language is never overridden at build time — it describes copy that is already written.
846
893
 
894
+ `--design v2` (or `"design": "v2"` in the config file) lets the build vary each beat's
895
+ layout: bars as rows or columns, a pipeline as a row, a stair or a column, a claim beside,
896
+ mirrored against or above its figure, a comparison as columns or rows, and the headline on
897
+ top, in a left rail or under the body. The choice is made by code, deterministically, per
898
+ paper — never by the planner — and it never moves a stop, so narration stays aligned.
899
+ `build` writes the choices to `out/look.json`. Without the flag the deck is the classic one,
900
+ byte for byte. See `.planning/2026-10-07-v2-layout-director.md`.
901
+
902
+ ## v2 motion
903
+
904
+ `--design v2` replaces the 0.8 deck's single motion — every scene fading up the same
905
+ eyebrow and headline, every seam the same 0.4s dissolve, nothing moving once a slide has
906
+ built — with a grammar planned per deck (`src/emit/motion.ts`):
907
+
908
+ - **Entrances.** Six verbs — `rise`, `slide`, `snap`, `focus`, `wipe`, `mask` — one per
909
+ scene, chosen from the source id and beat id, never the same twice in a row, with the
910
+ stock `rise` capped at a quarter of the deck.
911
+ - **Seams.** `dissolve`, `push`, `lift`, `wipe`, `zoom`, picked from how two neighbouring
912
+ beats relate (same family → push, a role boundary → zoom, a title → lift, into the close
913
+ → dissolve), never repeated back to back, and at least three kinds in a deck of ten or
914
+ more beats. A beat `inside` the one before keeps the camera dive.
915
+ - **Emphasis while the narrator talks.** The part a sentence is about pulses, glows or is
916
+ underlined, starting on a cue boundary of that sentence inside the quiet stretch after
917
+ its stop, and is back at rest before the next reveal — so every frame a gate captures
918
+ at a stop is unchanged.
919
+
920
+ All of it is `fromTo` tweens with no callbacks, seeded rather than random, and keeps every
921
+ hold, every scene window and `timing.json` exactly as `classic` writes them. In `deck.html`
922
+ a v2 deck also glides through the seam into the next slide (0.8 cut to its first stop,
923
+ already built) and, while a stop's audio plays, seeks the scene through that stop's quiet
924
+ stretch on the audio clock so the emphasis lands on the same word as in the video. Both
925
+ are off under `prefers-reduced-motion`.
926
+
927
+ ## v2 bespoke scenes (`--bespoke`)
928
+
929
+ The archetypes finish building in about five seconds and then hold still while the voice
930
+ keeps talking. `build --design v2 --bespoke` hands four to six beats per deck to Codex
931
+ instead: for each, a GSAP + SVG scene written for that beat, keyed to its narration cues,
932
+ that keeps explaining for as long as the sentence runs. Everything else stays v2. It is off
933
+ unless asked for; v2 without it, and classic, emit the bytes they did.
934
+
935
+ ```bash
936
+ decksmith build storyboard.json --source source.json -o deck --design v2 --bespoke
937
+ # bespoke: b03-action-loop.draft in 121s …
938
+ # bespoke: b05-backbone — every gate passed and the rubric probe is clean; no critique call
939
+ # bespoke: 4 of 5 beats drawn bespoke, 7 Codex call(s), 190985 tokens, 343s
940
+ ```
941
+
942
+ **Which beats.** A deterministic rule (`src/bespoke/select.ts`): mechanism archetypes
943
+ (pipeline, equation-walk/-morph, line-chart, stack, grid, split-/bar-compare, and figures
944
+ last), mechanism words in en/ko/ja/zh, a cited equation, and the beat's weight; ties
945
+ broken by a hash. Never a title, callout or table, a beat with no narration, or a beat a
946
+ camera dives through. `plan --bespoke` lets the planner mark beats `bespoke: true|false`;
947
+ `false` is obeyed, `true` is a strong hint.
948
+
949
+ **Per beat:** illustration → draft → static contract → probe deck (a real build with every
950
+ `verify` gate plus the motion gates, photographed at every cue) → the rubric probe → at most
951
+ one critique-and-fix call with the contact sheet and the measures attached (`codex exec -i`)
952
+ → static contract → probe again → repair rounds. A draft that passed every gate and is clean
953
+ by the rubric probe is kept without the critique call; one that fails only on what the
954
+ repair fixes, and is otherwise clean, is repaired instead of critiqued. A beat that does not
955
+ pass keeps its archetype — the deck is never failed. If the fix round breaks a draft that
956
+ passed, the draft is kept.
957
+
958
+ **The illustration** (`src/bespoke/art.ts`, `src/bespoke/inspect.ts`). Every bespoke beat
959
+ but a data beat gets ONE picture from the image tool of the same Codex account
960
+ (`codex exec` with every tool off but `image_generation`; no API key — the account's own
961
+ login, as for the scenes), all of a deck's pictures asked for at once before the drafts
962
+ queue for their lanes. The illustrator is given the beat (fenced, untrusted), the pack's
963
+ colours and one fixed style paragraph — flat 2D vector, solid fills with hard edges; no
964
+ gradients, shading, shadows, gloss, 3D, clay or toy renders, by name — and asked for three
965
+ or four subjects doing the method, big, left to right, with empty ground between them and
966
+ NO text. Each picture is then inspected locally, for free: its subjects are the connected
967
+ regions that are not its flat ground (boxed, left to right); its flatness is how much of
968
+ it sits in its 16 commonest colours minus how much of it is soft shading (`FLAT_MIN`,
969
+ calibrated on round 3's 21 renders against 12 flat pictures); and its writing is read by
970
+ macOS Vision, once per script (en, ko, ja, zh), through a small Swift helper compiled once
971
+ into `~/.cache/decksmith/tools`. A picture with writing, a shaded render or one whose
972
+ subjects touch is redrawn ONCE with the reason; of two refused pictures the one without
973
+ writing and the flatter is kept, and one with writing never is. Off macOS the text check
974
+ reports `unchecked` in `bespoke.json` rather than passing. The deck gets a WebP with its
975
+ edges feathered into transparency for its box (~30-75 KB against 1.3 MB of PNG; a CSS mask
976
+ on the picture cost ~0.7s a probe frame); the draft call is shown the PNG and a copy with
977
+ the subjects boxed and numbered (`-i`). The shell places it: `<image data-art="1">` with no
978
+ href gets the href and a fixed placement covering the body box (`slice`), so the subjects'
979
+ boxes are known in box px before the scene is written. It is read only from
980
+ `$CODEX_HOME/generated_images/` (after symlinks), only as a PNG by its magic bytes, under
981
+ 12 MB. `--bespoke-art <n>` (default 6; `bespoke.art`) caps pictures per deck, counted apart
982
+ from `--bespoke-calls`; a beat whose picture cannot be had is drawn without one.
983
+
984
+ **Data beats** (line-chart, bar-compare, data-table) get no picture: their scene is asked
985
+ for the chart that builds with the voice — axes draw, bars grow with their counters, the
986
+ line traces, the named value lights — from the beat's own numbers, never a table painted
987
+ over an illustration (`data_over_picture`).
988
+
989
+ **The camera** of an illustrated scene is the shell's, staged in one grammar
990
+ (`src/bespoke/shots.ts`). The scene names its `shots` — on which cue, how far into it, which
991
+ subject — and the shell compiles them: an ESTABLISHING shot of the whole picture for at
992
+ least 1.6s, a PUSH IN (1.1s, power3.inOut) that frames the named subject and its label at
993
+ 1.6-2.2x, a move straight to the next subject, a 3% creep while a shot is held, and a
994
+ REVEAL back to the whole picture at the last cue; shots closer than 1.6s are dropped. Every
995
+ camera tween is a `fromTo` with explicit from-values, and no two touch. The scene's own
996
+ script may not move `#sN-cam` (`script_camera`). A scene without a picture still moves the
997
+ wrapper itself (`scale`/`x`/`y`, arithmetic in the prompt). A scene with a moving camera is
998
+ clipped to its box and marked `data-ds-clip`, so `verify` does not count what a push-in
999
+ carries past the canvas edge as off-canvas.
1000
+
1001
+ **The labels** of an illustrated scene are the shell's too (`src/bespoke/callouts.ts`). The
1002
+ scene says what each subject is called (`labels`); the shell gives each subject a zone just
1003
+ above it (or across its top when there is no room), as wide as half the gap to each
1004
+ neighbour, and sets the name there on a plate — 56px, shrinking to 44, then two lines —
1005
+ with a leader line to a dot on the subject, entering as the camera first arrives on it (or
1006
+ at the reveal). The zones are in the draft prompt, which tells the scene to keep its own
1007
+ drawing out of them; a name that does not fit even on two lines at 44px is refused
1008
+ statically (`labels`). Round 4's first run, with the scene placing its own labels on the
1009
+ subjects, put them there and then failed four scenes in five on their own plates and
1010
+ leader lines crossing their text.
1011
+
1012
+ **Repair** (`src/bespoke/repair.ts`), no model call. On `text_overlap`,
1013
+ `graphic_crosses_text` or `svg_text_overprint`, each colliding label's unit (the text, or
1014
+ the smallest `<g>` holding it and its plate) gets one constant offset — the smallest, in
1015
+ 8px rings over sixteen directions in a fixed order — that clears every other label by 16px,
1016
+ every visible stroke sample by 10px and every shape painted over it, inside the box, at
1017
+ every frame the probe measured, camera scale included; it is written as a wrapping
1018
+ `<g transform="translate()">`. A label never moves farther than 0.8 of its own height
1019
+ (at least 32px): a collision that needs more is left to the critique round. On
1020
+ `end_dimmed`, the elements dimming parts the scene had lit are tweened back to full
1021
+ strength just before the end; on `camera_end`, the camera is tweened home. On `seek_order`,
1022
+ two tweens on one target animating one property over overlapping time — which a timeline
1023
+ resolves one way seeking forward and the other seeking back — are untangled in favour of
1024
+ the later one (the earlier is shortened to end where it starts, or loses that target or
1025
+ property when both start together; `src/bespoke/untangle.ts`). A repaired scene goes
1026
+ through the static contract and is gated again (at most two repair rounds).
1027
+
1028
+ **What the model is shown** (`src/bespoke/prompt.ts`): the beat, its cues, the paper's
1029
+ excerpts (fenced, untrusted), the contract, motion-design rules with numbers (one focal
1030
+ element per cue at 88-120px, the rest dimmed but kept; fill the box; paint it with filled
1031
+ shapes; at least three kinds of motion, one of flow, camera, counter or morph; the end
1032
+ frame a summary), text widths measured in the pack's own font, and two of four hand-made
1033
+ reference scenes (`src/bespoke/references.ts`: a routing mechanism with particle flow and
1034
+ a counter, a camera zoom into one block, a traced curve whose gap morphs into the headline
1035
+ number, a scatter whose dots travel into a bell, and — first, for a beat with an
1036
+ illustration — a scene built on its picture), picked for the beat's archetype and painted
1037
+ in the pack's colours. `test/bespoke-references.test.ts` runs each reference
1038
+ through every gate and the rubric probe in a real deck, so a reference that stops passing
1039
+ stops being one. Parts a cue introduces are `<g data-cue="N">` groups (semantic grouping,
1040
+ after Vector Prism, arXiv 2512.14336). MorphSVG is registered for a deck only when one of
1041
+ its scenes morphs.
1042
+
1043
+ **The rubric probe** (`rubricProbe`, free: it reads what the probe measured) sends a
1044
+ passing draft to the critique round when any of these is off: the drawing paints under 12%
1045
+ of its box at the end (round 1's scenes painted 4-16%), more than half its parts are still
1046
+ dimmed at the end, no label reaches 88px (not asked of an illustrated scene, whose picture
1047
+ is its focus and whose names are the shell's), fewer than three kinds of motion or none of
1048
+ flow/camera/counter/morph, an illustrated scene that holds fewer than two push-ins, does not
1049
+ open wide, or names fewer than two subjects on them, a cue whose picture changes by under
1050
+ 0.5% of the frame, or a warning about the scene. The critique round scores the frames against a six-line rubric
1051
+ (fills the stage, one focus per cue, motion explains, legible hierarchy, the pack, sync),
1052
+ is told the measures and every finding with the colliding labels' coordinates, and returns
1053
+ the fixed scene.
1054
+
1055
+ **Caps**, checked before every call: `--bespoke-calls` (default 12; two per beat) and
1056
+ `--bespoke-seconds` (default 1800). A quota or rate-limit answer stops every further call.
1057
+ Codex runs only through the account's own CLI (`--bespoke-cli`, `--bespoke-model`), never
1058
+ an API key, five calls in flight (`bespoke.concurrency` in the config file), at
1059
+ `bespoke.effort` reasoning (default `medium`; `default` keeps the account's), from the
1060
+ scratch directory, and with its tools off: shell, apps, plugins, browser, image
1061
+ generation (the illustration call alone keeps it), web search and every MCP server the
1062
+ account configured. A bespoke call is a
1063
+ text transform; with tools on, the agent spent its first turns reading the account's own
1064
+ instruction files and skills (measured: 56,679 input tokens over several turns and
1065
+ 135-488s for a draft that tool-less took one turn of 18,247 and 166s). A paced deck (`--speed`/`--duration` other than 1×) and non-16:9 formats are
1066
+ skipped, with a line saying so.
1067
+
1068
+ **Cache.** Keyed by everything a scene is drawn from — the beat's words and params, the
1069
+ quoted excerpts, the cue windows, the stops, the box, the pack, the prompt and contract
1070
+ versions, the model, the illustration's key — under `~/.cache/decksmith/bespoke`
1071
+ (`--bespoke-cache`); pictures under its `art/`, keyed by the beat's words, the pack's
1072
+ colours, the art prompt's version and the model. A rebuild costs no calls. A beat the gates rejected after its critique round is cached as rejected;
1073
+ a fallback the beat did not earn (cap, quota, timeout) is not.
1074
+
1075
+ **What a generated scene may run.** Three layers. (1) A static walk before anything is
1076
+ built or opened (`src/bespoke/contract.ts`, acorn): GSAP timeline calls with literal vars
1077
+ at explicit seconds, `gsap.set`, scoped `root.querySelector`, `Math` minus `random`, local
1078
+ code; `window`, `document`, `fetch`, `eval`, timers, storage, navigation, callbacks and
1079
+ function-valued tween vars are refused by name; CSS must be scoped to the scene with no
1080
+ at-rules, `url()` or animation; markup is SVG and inline HTML with no handlers, SMIL or
1081
+ external references, and `<image>` only as the beat's illustration (`data-art="1"`, no
1082
+ href — the shell writes it); path data must be numbers and commands (a word in a `d`
1083
+ attribute is a console error that used to fail every scene probed with it). (2) A CSP `<meta>` in every composition that carries one —
1084
+ `connect-src 'none'`, no `unsafe-eval`, local files only — and a probe that fails a scene
1085
+ on any page error, request or navigation (an error that names no scene fails every scene
1086
+ probed with it, unless the value it quotes is in exactly one candidate, which is then the
1087
+ one failed). (3) The player's frame is sandboxed
1088
+ (`allow-scripts allow-same-origin allow-downloads`). The paper's text reaches the model
1089
+ fenced as untrusted data. The static walk is not a proof against obfuscation — a
1090
+ property name assembled from parts at run time gets past it — which is what (2) and (3)
1091
+ are for; and (3) isolates only when decks are served from another origin than the host.
1092
+
1093
+ **The motion gates** (`src/verify/scenes.ts`) run on every scene `bespoke.json` lists, as
1094
+ errors: `static_hold` (the picture must change during every cue), `graphic_crosses_text`
1095
+ (a visible stroke through a label — a curve whose bounding box contains a label is not
1096
+ its plate — or a shape painted over one), `seek_order` (the frame must not depend on what
1097
+ was seeked before it, beyond 1500px of rasterising noise — counted on changed AREAS: a pixel
1098
+ counts when it and its 8 neighbours changed, so a picture re-rasterised a hair differently
1099
+ on its first paint, an outline round every subject, 12,698px raw, counts 309), `stage_fill` (the settled
1100
+ drawing's bounding box covers 80% of its box), `type_hierarchy` (one label of 64px or
1101
+ more at the end; not asked of an illustrated scene), `stray_marker` (an SVG marker painted where its line is not drawn),
1102
+ `early_reveal` (a `data-cue="N"` group showing more than 0.5s before cue N),
1103
+ `shot_variety` (an illustrated scene opens on the whole picture and the camera — sampled
1104
+ every 0.5s, no screenshot — HOLDS push-ins at 1.5x or closer on at least two different
1105
+ subjects; round 3's 1.1-1.4x pans are not push-ins), `label_anchor` (a label that names a
1106
+ subject sits within 96px of its box and covers no other subject by more than a quarter of
1107
+ itself, and at the end at least two subjects are named that way), `data_over_picture`
1108
+ (five or more numbers on the illustration),
1109
+ `cue_groups` (no such groups, or one naming a cue the scene does not have), `end_dimmed`
1110
+ (at the settled frame, more than 10% of the parts the scene had shown lit are left under
1111
+ 0.6 opacity — a part drawn translucent from the start is not counted) and `camera_end` (the
1112
+ camera, or a viewBox, not home at the settled frame). Content
1113
+ outside a clipping `<svg>` — a camera zoomed in — is neither off the frame nor crossing
1114
+ anything. Overlap and crossing findings carry the labels' boxes in the body box's px.
1115
+ `seek_order` also runs on every other scene of a v2 deck, as a warning; it is what found
1116
+ the equation-walk bug fixed alongside this (22,037px before, 0 after). Below the tolerance
1117
+ and not traced: v2's equation-walk on the narrated demo differs by 529px on one KaTeX
1118
+ glyph when reached from past the scene's end.
1119
+
1120
+ `build` writes `bespoke.json` beside the deck: what was drawn and from where (cache,
1121
+ draft, critique), why each fallback, calls, tokens, seconds, the stages' wall clock, each
1122
+ picture's inspection (subjects, flatness, writing, draws) and each scene's staging
1123
+ measures. The full motion probe that `verify` would run again on a generated scene is
1124
+ skipped by `build` when the pass already ran it on the same bytes at the same scene id
1125
+ (`gatedAt`); `build` still seeks it with the deck's other scenes. `decksmith verify <dir>`
1126
+ always runs it. Set
1127
+ `DECKSMITH_BESPOKE_WORK=<dir>` to keep the prompts, replies and contact sheets.
1128
+
1129
+ **Costs** (measured on four HypePaper decks, twice each, see
1130
+ [`.planning/2026-10-09-v2-bespoke-round4.md`](.planning/2026-10-09-v2-bespoke-round4.md)):
1131
+ an illustration takes 60-120s, a draft 90-220s and a critique 70-120s; a deck's pass took
1132
+ 380-730s for 5-8 scene calls plus 3-7 pictures (redraws included) and 153-227k tokens, and
1133
+ the whole `build` 466-812s on a Mac with 9-23% memory free — three builds of eight under ten
1134
+ minutes. The pictures add 128-348 KB of WebP to a deck (round 3: 6-7 MB of PNG). Round 3
1135
+ took 593-742s and 214-325k tokens; round 1 13-31 minutes and 540-705k. A rebuild from the
1136
+ cache makes 0 calls. None of that fits a 300-second build timeout: a host running this has
1137
+ to give `build` that long, or run it off the request path.
1138
+
847
1139
  ## Themes
848
1140
 
849
1141
  Three, each a position rather than a hue.
@@ -862,6 +1154,41 @@ naming a family the deck does not declare falls back silently.
862
1154
  A theme is a name and a palette, and that is the whole extension point: a new one is a
863
1155
  file in `src/emit/themes/` plus a line in `THEMES`. No archetype learns it exists.
864
1156
 
1157
+ ### v2 style packs
1158
+
1159
+ Six more, each a whole look rather than a palette: its own typeface pairing and type
1160
+ scale, ground and accent, eyebrow treatment, figure framing and surface.
1161
+
1162
+ | Pack | Ground | Headline / body | Title | Lists | Bars | Leans to |
1163
+ |---|---|---|---|---|---|---|
1164
+ | `signal` | violet-black, Magma accent | Space Grotesk / Inter | glowing headline | ticks | pills | foot headlines |
1165
+ | `blueprint` | navy, cyan | IBM Plex Sans / IBM Plex Sans | drawing frame with corner marks | numbers | square, on dashed tracks | rail headlines |
1166
+ | `atlas` | espresso, amber | Source Serif 4 / Inter | centred, frontispiece rule | dots | slightly rounded | top headlines |
1167
+ | `folio` | cream, oxblood | Source Serif 4 / Source Serif 4 | masthead rules | numbers | square, no tracks | top headlines, versus |
1168
+ | `chalk` | cool white, ultramarine | Space Grotesk / IBM Plex Sans | highlighter under each word | cards | rounded, outlined | foot headlines, tables |
1169
+ | `journal` | sage, forest | IBM Plex Sans / Source Serif 4 | side bar down the title | ruled rows | slightly rounded, outlined tracks | rail headlines |
1170
+
1171
+ The forms are the pack's `forms` (list marks, Director affinity) and its `skin` (title,
1172
+ bars), which may set only paint: colours, shadows, radii, outlines and SVG stroke/fill
1173
+ opacity, never a box. In a Korean, Japanese or Chinese deck a pack's serif roles are set
1174
+ in Noto Serif KR/JP/SC, bundled beside the sans.
1175
+
1176
+ `--theme <pack>` forces one. `--design v2` picks one when nobody named a theme (a
1177
+ storyboard's default `ink` counts as nobody): a weighted, deterministic hash of the
1178
+ source id (or `--pack-seed`), leaning mildly toward packs that suit the deck's mix of
1179
+ archetypes. Over the 176 HypePaper storyboards on disk the busiest pack carries 20.5% of
1180
+ decks. With narration on disk, a pack that would stage a beat with a different stop count
1181
+ than the narration was recorded at, or that would leave out a beat the storyboard's own
1182
+ theme draws, is skipped for the next one; if none fits, the storyboard's own theme is kept.
1183
+ So `--design v2` never breaks a narrated rebuild and never costs a slide.
1184
+
1185
+ Each pack's faces ship beside a Latin deck, vendored from `@fontsource-variable/*` like
1186
+ Inter, and are measured by their own width tables (`src/emit/faces.ts`, written by
1187
+ `node scripts/measure-faces.mjs --write`). A CJK deck keeps its Noto bundle first in every
1188
+ stack — Noto Serif for a role the pack sets in a serif, Noto Sans otherwise — so the glyph
1189
+ shapes change with the pack in every language.
1190
+ The packs' interfaces are written up in `.planning/2026-10-07-v2-style-packs.md`.
1191
+
865
1192
  ## Narration
866
1193
 
867
1194
  ```sh
@@ -1278,6 +1605,8 @@ src/prefs.ts the three-layer preference resolver
1278
1605
  src/emit/kit.ts the seam between the deck shell and the archetype emitters
1279
1606
  src/emit/archetypes/ one emitter per archetype
1280
1607
  src/emit/themes/ one palette per file; the registry is the extension point
1608
+ src/emit/type.ts type specs: the v2 packs' faces and chrome scale, read by chromeHeight and chromeCss
1609
+ src/emit/faces.ts measured width tables for the packs' Latin faces (generated)
1281
1610
  src/images/ the three rungs a brief is drawn through, and the illustrate step
1282
1611
  src/narrate/ edge-tts, one segment per stop
1283
1612
  src/pack/ the .deck container and its bake/link/embed policy