frameyard 0.8.0 → 0.9.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.
Files changed (285) hide show
  1. package/CHANGELOG.md +110 -14
  2. package/GLOSSARY.md +112 -2
  3. package/README.md +302 -265
  4. package/dist/assets/integrations.d.ts +8 -4
  5. package/dist/assets/integrations.d.ts.map +1 -1
  6. package/dist/assets/integrations.js +8 -4
  7. package/dist/assets/integrations.js.map +1 -1
  8. package/dist/assets/provenance.d.ts +2 -2
  9. package/dist/assets/provenance.d.ts.map +1 -1
  10. package/dist/assets/provenance.js +5 -3
  11. package/dist/assets/provenance.js.map +1 -1
  12. package/dist/assets/tts-supertonic.d.ts +28 -0
  13. package/dist/assets/tts-supertonic.d.ts.map +1 -0
  14. package/dist/assets/tts-supertonic.js +156 -0
  15. package/dist/assets/tts-supertonic.js.map +1 -0
  16. package/dist/assets/tts.d.ts +18 -6
  17. package/dist/assets/tts.d.ts.map +1 -1
  18. package/dist/assets/tts.js +219 -97
  19. package/dist/assets/tts.js.map +1 -1
  20. package/dist/cli.js +1685 -93
  21. package/dist/cli.js.map +1 -1
  22. package/dist/composition/format.d.ts +1 -1
  23. package/dist/composition/format.d.ts.map +1 -1
  24. package/dist/composition/format.js +1 -1
  25. package/dist/composition/format.js.map +1 -1
  26. package/dist/context.d.ts +82 -0
  27. package/dist/context.d.ts.map +1 -0
  28. package/dist/context.js +277 -0
  29. package/dist/context.js.map +1 -0
  30. package/dist/contracts.d.ts +25 -0
  31. package/dist/contracts.d.ts.map +1 -1
  32. package/dist/contracts.js.map +1 -1
  33. package/dist/doctor.d.ts +14 -1
  34. package/dist/doctor.d.ts.map +1 -1
  35. package/dist/doctor.js +166 -7
  36. package/dist/doctor.js.map +1 -1
  37. package/dist/errors.d.ts +1 -1
  38. package/dist/errors.d.ts.map +1 -1
  39. package/dist/errors.js.map +1 -1
  40. package/dist/fonts/add.d.ts +41 -0
  41. package/dist/fonts/add.d.ts.map +1 -0
  42. package/dist/fonts/add.js +259 -0
  43. package/dist/fonts/add.js.map +1 -0
  44. package/dist/fonts/binary.d.ts +4 -0
  45. package/dist/fonts/binary.d.ts.map +1 -0
  46. package/dist/fonts/binary.js +21 -0
  47. package/dist/fonts/binary.js.map +1 -0
  48. package/dist/fonts/css.d.ts +4 -0
  49. package/dist/fonts/css.d.ts.map +1 -0
  50. package/dist/fonts/css.js +50 -0
  51. package/dist/fonts/css.js.map +1 -0
  52. package/dist/fonts/fontsource.d.ts +107 -0
  53. package/dist/fonts/fontsource.d.ts.map +1 -0
  54. package/dist/fonts/fontsource.js +565 -0
  55. package/dist/fonts/fontsource.js.map +1 -0
  56. package/dist/fonts/index.d.ts +11 -0
  57. package/dist/fonts/index.d.ts.map +1 -0
  58. package/dist/fonts/index.js +11 -0
  59. package/dist/fonts/index.js.map +1 -0
  60. package/dist/fonts/install.d.ts +24 -0
  61. package/dist/fonts/install.d.ts.map +1 -0
  62. package/dist/fonts/install.js +145 -0
  63. package/dist/fonts/install.js.map +1 -0
  64. package/dist/fonts/manifest.d.ts +21 -0
  65. package/dist/fonts/manifest.d.ts.map +1 -0
  66. package/dist/fonts/manifest.js +96 -0
  67. package/dist/fonts/manifest.js.map +1 -0
  68. package/dist/fonts/options.d.ts +9 -0
  69. package/dist/fonts/options.d.ts.map +1 -0
  70. package/dist/fonts/options.js +66 -0
  71. package/dist/fonts/options.js.map +1 -0
  72. package/dist/fonts/schema.d.ts +4 -0
  73. package/dist/fonts/schema.d.ts.map +1 -0
  74. package/dist/fonts/schema.js +316 -0
  75. package/dist/fonts/schema.js.map +1 -0
  76. package/dist/fonts/summary.d.ts +16 -0
  77. package/dist/fonts/summary.d.ts.map +1 -0
  78. package/dist/fonts/summary.js +22 -0
  79. package/dist/fonts/summary.js.map +1 -0
  80. package/dist/fonts/types.d.ts +56 -0
  81. package/dist/fonts/types.d.ts.map +1 -0
  82. package/dist/fonts/types.js +8 -0
  83. package/dist/fonts/types.js.map +1 -0
  84. package/dist/fonts/verify.d.ts +33 -0
  85. package/dist/fonts/verify.d.ts.map +1 -0
  86. package/dist/fonts/verify.js +200 -0
  87. package/dist/fonts/verify.js.map +1 -0
  88. package/dist/html/validate.d.ts.map +1 -1
  89. package/dist/html/validate.js +59 -0
  90. package/dist/html/validate.js.map +1 -1
  91. package/dist/index.d.ts +3 -3
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +1 -1
  94. package/dist/index.js.map +1 -1
  95. package/dist/inspect.d.ts +61 -0
  96. package/dist/inspect.d.ts.map +1 -0
  97. package/dist/inspect.js +325 -0
  98. package/dist/inspect.js.map +1 -0
  99. package/dist/lint.d.ts +63 -0
  100. package/dist/lint.d.ts.map +1 -0
  101. package/dist/lint.js +265 -0
  102. package/dist/lint.js.map +1 -0
  103. package/dist/policy.d.ts +69 -0
  104. package/dist/policy.d.ts.map +1 -0
  105. package/dist/policy.js +361 -0
  106. package/dist/policy.js.map +1 -0
  107. package/dist/preview/timeline-data.d.ts +17 -69
  108. package/dist/preview/timeline-data.d.ts.map +1 -1
  109. package/dist/preview/timeline-host.d.ts.map +1 -1
  110. package/dist/preview/timeline-host.js +70 -45
  111. package/dist/preview/timeline-host.js.map +1 -1
  112. package/dist/preview/timeline-main.d.ts.map +1 -1
  113. package/dist/preview/timeline-main.js +47 -4
  114. package/dist/preview/timeline-main.js.map +1 -1
  115. package/dist/project.d.ts +10 -3
  116. package/dist/project.d.ts.map +1 -1
  117. package/dist/project.js +20 -17
  118. package/dist/project.js.map +1 -1
  119. package/dist/renderer/ffmpeg.d.ts +42 -2
  120. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  121. package/dist/renderer/ffmpeg.js +254 -22
  122. package/dist/renderer/ffmpeg.js.map +1 -1
  123. package/dist/renderer/index.js +1 -1
  124. package/dist/renderer/index.js.map +1 -1
  125. package/dist/renderer/keyframe-expressions.d.ts +39 -0
  126. package/dist/renderer/keyframe-expressions.d.ts.map +1 -0
  127. package/dist/renderer/keyframe-expressions.js +180 -0
  128. package/dist/renderer/keyframe-expressions.js.map +1 -0
  129. package/dist/renderer/timeline.d.ts +2 -0
  130. package/dist/renderer/timeline.d.ts.map +1 -1
  131. package/dist/renderer/timeline.js +89 -116
  132. package/dist/renderer/timeline.js.map +1 -1
  133. package/dist/runtime/host.d.ts.map +1 -1
  134. package/dist/runtime/host.js +4 -0
  135. package/dist/runtime/host.js.map +1 -1
  136. package/dist/scaffold.d.ts +3 -0
  137. package/dist/scaffold.d.ts.map +1 -1
  138. package/dist/scaffold.js +48 -320
  139. package/dist/scaffold.js.map +1 -1
  140. package/dist/server/composition-html.d.ts +2 -0
  141. package/dist/server/composition-html.d.ts.map +1 -1
  142. package/dist/server/composition-html.js +7 -0
  143. package/dist/server/composition-html.js.map +1 -1
  144. package/dist/server/server.d.ts.map +1 -1
  145. package/dist/server/server.js +31 -1
  146. package/dist/server/server.js.map +1 -1
  147. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  148. package/dist/server/timeline-preview-data.js +12 -34
  149. package/dist/server/timeline-preview-data.js.map +1 -1
  150. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  151. package/dist/server/timeline-preview-html.js +18 -2
  152. package/dist/server/timeline-preview-html.js.map +1 -1
  153. package/dist/templates.d.ts +16 -0
  154. package/dist/templates.d.ts.map +1 -0
  155. package/dist/templates.js +186 -0
  156. package/dist/templates.js.map +1 -0
  157. package/dist/timeline/easing.d.ts +36 -0
  158. package/dist/timeline/easing.d.ts.map +1 -0
  159. package/dist/timeline/easing.js +225 -0
  160. package/dist/timeline/easing.js.map +1 -0
  161. package/dist/timeline/keyframes.d.ts +23 -0
  162. package/dist/timeline/keyframes.d.ts.map +1 -0
  163. package/dist/timeline/keyframes.js +95 -0
  164. package/dist/timeline/keyframes.js.map +1 -0
  165. package/dist/timeline/plan.d.ts +6 -12
  166. package/dist/timeline/plan.d.ts.map +1 -1
  167. package/dist/timeline/plan.js +17 -30
  168. package/dist/timeline/plan.js.map +1 -1
  169. package/dist/timeline/resolve.d.ts +12 -0
  170. package/dist/timeline/resolve.d.ts.map +1 -1
  171. package/dist/timeline/resolve.js +105 -9
  172. package/dist/timeline/resolve.js.map +1 -1
  173. package/dist/timeline/scene.d.ts +126 -0
  174. package/dist/timeline/scene.d.ts.map +1 -0
  175. package/dist/timeline/scene.js +109 -0
  176. package/dist/timeline/scene.js.map +1 -0
  177. package/dist/timeline/schema.d.ts +31 -0
  178. package/dist/timeline/schema.d.ts.map +1 -1
  179. package/dist/timeline/schema.js +240 -9
  180. package/dist/timeline/schema.js.map +1 -1
  181. package/dist/timeline/transitions.d.ts +21 -0
  182. package/dist/timeline/transitions.d.ts.map +1 -0
  183. package/dist/timeline/transitions.js +23 -0
  184. package/dist/timeline/transitions.js.map +1 -0
  185. package/dist/version.d.ts +1 -1
  186. package/dist/version.js +1 -1
  187. package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
  188. package/docs/adr/0010-make-timelines-the-render-unit.md +3 -2
  189. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +2 -1
  190. package/docs/adr/0015-inherit-composition-format-from-the-project.md +2 -2
  191. package/docs/adr/0016-author-compositions-as-html-fragments.md +1 -1
  192. package/docs/adr/0018-animate-timeline-clips-with-keyframes.md +35 -0
  193. package/docs/adr/0019-make-same-track-overlap-the-transition-window.md +36 -0
  194. package/docs/adr/0020-crossfade-audio-clips-on-audio-tracks.md +27 -0
  195. package/docs/adr/0021-project-timeline-scenes-once.md +32 -0
  196. package/docs/adr/0022-gate-heavy-operations-with-a-workspace-resource-policy.md +35 -0
  197. package/docs/adr/0023-pin-fonts-as-project-assets.md +98 -0
  198. package/docs/migration-0.4.md +2 -2
  199. package/docs/preview-testing.md +19 -6
  200. package/docs/{v0.1-design.md → v0.x-design.md} +55 -30
  201. package/examples/blend/README.md +3 -3
  202. package/examples/demo/README.md +2 -2
  203. package/examples/intro/README.md +71 -0
  204. package/examples/intro/assets/fonts/figtree/LICENSE.txt +93 -0
  205. package/examples/intro/assets/fonts/figtree/figtree-latin-wght-normal.woff2 +0 -0
  206. package/examples/intro/assets/fonts/instrument-serif/LICENSE.txt +93 -0
  207. package/examples/intro/assets/fonts/instrument-serif/instrument-serif-latin-400-italic.woff2 +0 -0
  208. package/examples/intro/assets/fonts/space-mono/LICENSE.txt +93 -0
  209. package/examples/intro/assets/fonts/space-mono/space-mono-latin-400-normal.woff2 +0 -0
  210. package/examples/intro/assets/fonts/space-mono/space-mono-latin-700-normal.woff2 +0 -0
  211. package/examples/intro/assets/grain.png +0 -0
  212. package/examples/intro/assets/score.ogg +0 -0
  213. package/examples/intro/compositions/brand.html +412 -0
  214. package/examples/intro/compositions/chrome.controller.js +63 -0
  215. package/examples/intro/compositions/chrome.html +256 -0
  216. package/examples/intro/compositions/lattice.controller.js +148 -0
  217. package/examples/intro/compositions/lattice.html +308 -0
  218. package/examples/intro/compositions/opening.html +504 -0
  219. package/examples/intro/compositions/studio.html +930 -0
  220. package/examples/intro/compositions/theme.css +218 -0
  221. package/examples/intro/fy.config.js +13 -0
  222. package/examples/intro/fy.fonts.json +100 -0
  223. package/examples/intro/scripts/make-grain.mjs +79 -0
  224. package/examples/intro/scripts/make-score.mjs +702 -0
  225. package/examples/intro/timelines/intro.timeline.json +60 -0
  226. package/examples/keyframes/README.md +76 -0
  227. package/examples/keyframes/compositions/backdrop.html +60 -0
  228. package/examples/keyframes/compositions/card.html +99 -0
  229. package/examples/keyframes/fy.config.js +10 -0
  230. package/examples/keyframes/timelines/keyframes.timeline.json +60 -0
  231. package/examples/nested/README.md +112 -0
  232. package/examples/nested/assets/pulse.wav +0 -0
  233. package/examples/nested/compositions/backdrop.html +84 -0
  234. package/examples/nested/compositions/card.html +275 -0
  235. package/examples/nested/compositions/overlay.html +307 -0
  236. package/examples/nested/fy.config.js +13 -0
  237. package/examples/nested/scripts/make-pulse.sh +20 -0
  238. package/examples/nested/timelines/chapter-a.timeline.json +39 -0
  239. package/examples/nested/timelines/chapter-b.timeline.json +39 -0
  240. package/examples/nested/timelines/nested-reel.timeline.json +52 -0
  241. package/examples/static/README.md +2 -2
  242. package/examples/transitions/README.md +128 -0
  243. package/examples/transitions/assets/alpha.wav +0 -0
  244. package/examples/transitions/assets/beta.wav +0 -0
  245. package/examples/transitions/assets/gamma.wav +0 -0
  246. package/examples/transitions/compositions/alpha.html +56 -0
  247. package/examples/transitions/compositions/beta.html +56 -0
  248. package/examples/transitions/compositions/gamma.html +56 -0
  249. package/examples/transitions/fy.config.js +12 -0
  250. package/examples/transitions/timelines/audio-crossfade.timeline.json +44 -0
  251. package/examples/transitions/timelines/transitions.timeline.json +31 -0
  252. package/examples/video/README.md +4 -4
  253. package/examples/webgl/README.md +1 -1
  254. package/package.json +12 -5
  255. package/scaffolds/init/AGENTS.md +131 -0
  256. package/scaffolds/init/README.md +56 -0
  257. package/scaffolds/init/compositions/hello.html +79 -0
  258. package/scaffolds/init/fy.config.js +9 -0
  259. package/scaffolds/init/gitignore.template +1 -0
  260. package/scaffolds/init/package.json +11 -0
  261. package/scaffolds/init/props/hello.json +4 -0
  262. package/scaffolds/init/props/main.json +6 -0
  263. package/scaffolds/init/timelines/main.timeline.json +20 -0
  264. package/scaffolds/local-models/LOCAL-MODELS.md +77 -0
  265. package/scaffolds/local-models/fy.policy.json +5 -0
  266. package/scaffolds/local-models/opencode/opencode.jsonc +17 -0
  267. package/scaffolds/local-models/pi/APPEND_SYSTEM.md +21 -0
  268. package/scaffolds/local-models/pi/sandbox.json +15 -0
  269. package/scaffolds/local-models/pi/settings.json +15 -0
  270. package/skills/frameyard/README.md +1 -0
  271. package/skills/frameyard/SKILL.md +59 -61
  272. package/skills/frameyard/references/audio-assets.md +32 -3
  273. package/skills/frameyard/references/common-mistakes.md +53 -2
  274. package/skills/frameyard/references/deterministic-authoring.md +10 -3
  275. package/skills/frameyard/references/diagnostics.md +31 -1
  276. package/skills/frameyard/references/failure-recovery.md +32 -6
  277. package/skills/frameyard/references/fonts.md +132 -0
  278. package/skills/frameyard/references/json-results.md +119 -6
  279. package/skills/frameyard/references/quick-reference.md +222 -0
  280. package/skills/frameyard/references/timelines.md +97 -7
  281. /package/examples/blend/{video.config.js → fy.config.js} +0 -0
  282. /package/examples/demo/{video.config.js → fy.config.js} +0 -0
  283. /package/examples/static/{video.config.js → fy.config.js} +0 -0
  284. /package/examples/video/{video.config.js → fy.config.js} +0 -0
  285. /package/examples/webgl/{video.config.js → fy.config.js} +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Using the old authoring model
4
4
 
5
- - **JavaScript composition modules / `defineComposition`** were removed in 0.4. A composition is a `.html` fragment listed in `video.config.js`.
5
+ - **JavaScript composition modules / `defineComposition`** were removed in 0.4. A composition is a `.html` fragment listed in `fy.config.js`.
6
6
  - **`data-frameyard-*`** media attributes were removed in 0.4. Use `data-fy-*`.
7
7
 
8
8
  ## Document structure
@@ -47,8 +47,18 @@
47
47
  - Importing libraries from executable `<script>` tags; import them from the controller instead.
48
48
  - Using `Math.random()`, `Date.now()`, `performance.now()`, timers, independent `requestAnimationFrame` loops, or CSS transitions.
49
49
 
50
- ## Media and transcripts
50
+ ## Fonts
51
+
52
+ - Using a remote Google Fonts `<link>` in a composition that must render reproducibly. Search Fontsource with `font search` and vendor the file; remote fonts produce `FONT_REMOTE`.
53
+ - Installing a Fontsource variable family and then expecting static weights from the same record. Use `--static --weight 400,700` for static files, or `--force` to replace the family.
54
+ - Passing `--license` with a Fontsource id; the provider supplies the license and the flag is rejected. Use `--license` only with `--file`/`--url`.
55
+ - Importing a font by URL without a license. It records `unknown`, warns, and requires `full`; add `--license` and `--license-file` when the license allows redistribution.
56
+ - Adding a `.woff2` file but forgetting the license or `--family`; `font add` needs the family name and warns when the license is unknown.
57
+ - Hand-editing a path or hash in `fy.fonts.json`. Reinstall the face; `check`, `still`, and `render` verify hashes before Chromium.
58
+ - Installing only weight 400 and using `font-weight: 700`, which asks the browser to synthesize a bold face. Add the weight or a variable range.
59
+ - Moving or renaming a vendored font file without updating the manifest; `font verify` reports `FONT_FILE_MISSING`.
51
60
 
61
+ ## Media and transcripts
52
62
  - Using remote, live, or unseekable media sources.
53
63
  - Using `<source>` or `<track>` children; use a local `src` and `<fy-transcript>`.
54
64
  - Adding or changing media in `update()`; prepare it in the fragment or in `mount()`.
@@ -56,6 +66,15 @@
56
66
  - Assuming media/transcript `src` is document-relative. It is project-root-relative.
57
67
  - Assuming the controller `src` is project-root-relative. It is relative to the composition URL.
58
68
 
69
+ ## Generated audio
70
+
71
+ - Passing `--talker-model` or `--codec-model` with `--engine supertonic`, or `--model-dir` with `--engine qwen`. Flags are engine-specific.
72
+ - Using a speaker name with `--engine supertonic`; `--speaker` is the numeric `--sid`.
73
+ - Omitting `--language` with Supertonic or using a code outside its 31-language list.
74
+ - Expecting `--style` or `--device` to work with Supertonic; both are qwen-only. Put expression tags in the text.
75
+ - Pointing `--model-dir` at individual files instead of the extracted seven-file archive.
76
+ - Regenerating an existing speech asset without `--force` and user approval (`OUTPUT_EXISTS`).
77
+
59
78
  ## Timelines
60
79
 
61
80
  - Putting `blend` on an audio clip. It is only valid on visual clips.
@@ -66,6 +85,18 @@
66
85
  - Omitting `duration` on a static composition clip, or setting `sourceIn` above 0. A static source has no intrinsic duration or frames.
67
86
  - Expecting a static composition clip to contribute audio or transcripts. It is visual-only.
68
87
 
88
+ ## Timeline keyframes
89
+
90
+ - Treating `frame` as a timeline frame or a source frame. Keyframe frames are clip-local; `0` is the clip start boundary and `duration` is the end boundary.
91
+ - Expecting keyframes to extend a clip. A key past `duration` is a `TIMELINE_CLIP_OUT_OF_RANGE` error; declare a longer clip instead.
92
+ - Declaring the same channel as a scalar in `transform` and as a keyframe track. Keyed channels and static channels are separate; defaults do not conflict.
93
+ - Combining `keyframes.scale` with a `transform.width`/`height` box. Use a keyed scale without a box, or keep the box and animate `x`, `y`, `rotation`, and `opacity`.
94
+ - Putting keyframes on an audio clip. Only visual clips have presentation channels.
95
+ - Using seconds instead of integer clip-local frames, or putting keys out of order.
96
+ - Expecting the final key’s `easing` to do anything. Easing belongs to the interval after a key; the last key has no following interval.
97
+ - Expecting preview and encoded keyframe motion to match exactly. Preview uses the exact CSS curve; production approximates it in FFmpeg and may differ by a subpixel.
98
+ - Expecting an animated `blend` mode. Blend remains static; only `opacity` can animate the layer’s contribution to a blend.
99
+
69
100
  ## Static compositions
70
101
 
71
102
  - Expecting `update()` or `--fy-*` frame-state variables to run. A static composition only runs `mount()` and prop validation; use CSS, markup, or mount-time canvas drawing.
@@ -74,6 +105,26 @@
74
105
  - Passing `--container`, `--codec`, `--encoder`, or `--concurrency` to `render` for a static composition. Use a timeline image clip if the deliverable must be video.
75
106
  - Requesting a still frame other than 0 from a static composition.
76
107
 
108
+ ## Context, inspection, and templates
109
+
110
+ - Reading the whole repository before a small edit. Run `npx frameyard context --json` first; it returns the format, props, timelines, assets, policy, and next commands.
111
+ - Treating `inspect`/`compare` as a vision model. They return facts such as blank, alpha coverage, bbox, and changed pixels; they do not judge whether a design looks right.
112
+ - Expecting `compare` to exit 0 when the images differ. It exits 1 and still returns `ok: true` with `identical: false`.
113
+ - Forgetting that `add` does not edit `fy.config.js`. Merge the returned `register` object yourself, then run `lint`.
114
+ - Adding a composition template to a project without `format`. Composition templates inherit the project format; add `format { width, height, fps }` first.
115
+ - Adding the slideshow timeline before `assets/slide-1.png` through `assets/slide-3.png` exist. `check` will report the missing assets.
116
+
117
+ ## Resource policy and lint
118
+
119
+ - Using `check` as the edit loop. Use `lint`; it resolves the timeline, projects every scene and keyframe channel, and scans the controller without Chromium.
120
+ - Treating a clean `lint` as a successful `check`. Lint does not run `validateProps` or composition runtime.
121
+ - Running `tts`, `transcribe`, or `render` while the workspace policy is `authoring` or `light`. Use `--dry-run` and hand off the command.
122
+ - Editing `fy.policy.json`, `.opencode/`, `.pi/`, or `skills/frameyard/` to bypass the policy. Those files are protected by host permissions and are not part of the authoring task.
123
+ - Passing `--force` or `--overwrite` outside `full`. Ask the user before replacing an output; the CLI will fail with `RESOURCE_POLICY_BLOCKED`.
124
+ - Requesting more still frames than the tier allows. The caps are 1 for `light`, 4 for `authoring`, and 32 for `full`; split larger ranges.
125
+ - Passing `--workers 8` with `--profile shared-cpu`. The profile caps workers at 2 and fails with `RESOURCE_PROFILE_CONFLICT` instead of clamping.
126
+ - Forgetting that the policy is discovered from the working directory upward, independent of `fy.config.js`. Running from a nested directory still finds the workspace file.
127
+
77
128
  ## Validation
78
129
 
79
130
  - Replacing `check` with a single `still`.
@@ -4,7 +4,7 @@ Frameyard compositions are partial HTML fragments. The preview server wraps them
4
4
 
5
5
  ## Project configuration
6
6
 
7
- `video.config.js` lists every composition explicitly:
7
+ `fy.config.js` lists every composition explicitly:
8
8
 
9
9
  ```js
10
10
  export default {
@@ -99,6 +99,12 @@ CSS `@keyframes` and the Web Animations API are paused and seeked by the runtime
99
99
  - The root exposes `--fy-frame`, `--fy-time`, `--fy-progress`, `--fy-fps`, `--fy-duration`, and `--fy-duration-frames`. Use them in `calc()` for simple computed motion and progress bars.
100
100
  - Do not use CSS transitions. They are diagnosed as non-deterministic.
101
101
 
102
+ ## Timeline clip keyframes
103
+
104
+ CSS `@keyframes` animate elements *inside* a composition. A timeline clip's `keyframes` animate the clip's placement in the edit: `x`, `y`, `scale`, `rotation`, and `opacity`. The two mechanisms compose: a composition may animate internally while its timeline clip also fades or moves.
105
+
106
+ Timeline keyframes use clip-local integer frames, not composition frames or seconds. Keyframe values are derived only from the integer frame and the declared easing, so they are seek-safe. Preview evaluates the exact CSS timing function; production generates per-frame FFmpeg expressions and approximates the curve. See [timelines](timelines.md) for the contract.
107
+
102
108
  ## Controllers
103
109
 
104
110
  An optional controller handles Canvas 2D, WebGL, karaoke, and per-frame computation CSS cannot express.
@@ -215,17 +221,18 @@ Declare canonical transcript requirements with:
215
221
 
216
222
  Remote data, images, fonts, and other assets must resolve during the loading phase before frame production. The production browser blocks network requests after loading begins. A fetch from `update()` is a bug.
217
223
 
218
- Images declared in the fragment are awaited by the host. Static fonts should be loaded with `document.fonts.ready` or an explicit `FontFace` before frame production.
224
+ Images declared in the fragment are awaited by the host. Fonts should be vendored into the project with `npx frameyard font search`/`font add <id>` or `font add --file`; the server injects the generated `@font-face` sheet and the host awaits `document.fonts.ready`. A used face that fails to load fails the capture with `FONT_LOAD_FAILED`. A remote font URL produces a `FONT_REMOTE` warning and is not reproducible. See [fonts](fonts.md).
219
225
 
220
226
  ## Validation and inspection
221
227
 
222
228
  ```sh
229
+ npx frameyard lint hello --json
223
230
  npx frameyard check hello --json
224
231
  npx frameyard still hello --frame 42 --out hello-42.png --json
225
232
  npx frameyard still hello --frame 0,118-126 --out out/stills --json
226
233
  ```
227
234
 
228
- `check` runs static validation before Chromium, then evaluates every animated frame and validates audio and declared transcripts without capturing or encoding. A static composition is inspected once. Read the structured `diagnostics` array. Do not replace `check` with a single still. Inspect several representative frames in one `still` call with a range or list; the batch seeks sequentially in one session and is limited to 32 unique frames. A static composition exposes only frame 0.
235
+ `lint` runs static validation, timeline resolution, scene and keyframe projection for every timeline frame, and a controller determinism scan without Chromium. Use it while editing; it does not run `validateProps` or composition runtime. `check` runs static validation before Chromium, then evaluates every animated frame and validates audio and declared transcripts without capturing or encoding. A static composition is inspected once. Read the structured `diagnostics` array. Do not replace `check` with a single still. Inspect several representative frames in one `still` call with a range or list; the batch seeks sequentially in one session and is limited to 32 unique frames in `full` (4 in `authoring`, 1 in `light`). A static composition exposes only frame 0.
229
236
 
230
237
  ## Metadata is fixed
231
238
 
@@ -35,7 +35,7 @@ Static validation covers:
35
35
  }
36
36
  ```
37
37
 
38
- `details` may include `path`, `line`, `column`, `endLine`, `endColumn`, `element`, `attribute`, `value`, `suggestion`, `frame`, and `controller`.
38
+ `details` may include `path`, `line`, `column`, `endLine`, `endColumn`, `element`, `attribute`, `value`, `suggestion`, `frame`, and `controller`. Timeline diagnostics may also include `timeline`, `clip`, `track`, `channel`, `keyframe`, `jsonPath`, `timelineFrame`, `sourceFrame`, `from`, `to`, and `nesting`; keyframe errors use `channel`, the zero-based `keyframe` index, and the clip-local `frame`, and transition errors use `from`/`to`.
39
39
 
40
40
  ## Common codes
41
41
 
@@ -66,14 +66,38 @@ Static validation covers:
66
66
  | `ELEMENT_NOT_SUPPORTED` | error | `iframe`, `object`, `embed`, `base`, refresh/CSP meta, `source`, `track`, or declarative shadow DOM |
67
67
  | `ELEMENT_UNKNOWN` | warning | Unknown `fy-*` element |
68
68
  | `CONTENT_OUTSIDE_ROOT` | warning | Visual content outside `<fy-composition>` |
69
+ | `TIMELINE_TRANSITION_INVALID` | error | Unknown transition field, invalid clip id, or unsupported easing |
70
+ | `TIMELINE_TRANSITION_UNKNOWN_CLIP` | error | Transition names a missing or self clip |
71
+ | `TIMELINE_TRANSITION_WRONG_TRACK` | error | Transition clips are on different tracks |
72
+ | `TIMELINE_TRANSITION_AUDIO_TRACK` | error | A `dissolve` is declared on an audio track; use `crossfade` |
73
+ | `TIMELINE_TRANSITION_VIDEO_TRACK` | error | A `crossfade` is declared on a video track; use `dissolve` |
74
+ | `TIMELINE_TRANSITION_NOT_OVERLAPPING` | error | Declared transition has no overlap |
75
+ | `TIMELINE_TRANSITION_GEOMETRY` | error | Reversed order or the incoming clip ends before the outgoing clip |
76
+ | `TIMELINE_TRANSITION_DUPLICATE` | error | Repeated pair or a clip used twice on one side |
77
+ | `TIMELINE_TRANSITION_KIND` | error | Unsupported or missing transition `kind` |
69
78
  | `MEDIA_INVALID` | error | Invalid or missing audio/video source or attributes |
70
79
  | `TRANSCRIPT_INVALID` | error | Invalid or missing transcript source |
71
80
  | `ASSET_NOT_FOUND` | error | Missing image, stylesheet, media, or transcript file |
81
+ | `FONT_MANIFEST_INVALID` | error | Invalid font manifest schema, path, face, weight, or duplicate family |
82
+ | `FONT_FILE_MISSING` | error | A manifest font file is missing |
83
+ | `FONT_FILE_INVALID` | error | Wrong extension, signature, or non-file font path |
84
+ | `FONT_HASH_MISMATCH` | error | A vendored font file changed size or SHA-256 |
85
+ | `FONT_LICENSE_MISSING` | warning | Font license type or file is unknown or absent |
86
+ | `FONT_LOAD_FAILED` | error | Chromium failed to load a used `@font-face` |
87
+ | `FONT_PROVIDER_UNAVAILABLE` | error | Fontsource API/CDN unreachable or invalid response |
88
+ | `FONT_DOWNLOAD_FAILED` | error | A Fontsource or URL response was not a valid font file |
89
+ | `FONT_REMOTE` | warning | CSS references a remote font or stylesheet |
90
+ | `FONT_NOT_FOUND` | error | Fontsource id or `font remove` id is not installed |
72
91
  | `AUDIO_WARNING` | warning | Audio timeline overrun or silent embedded media |
73
92
  | `TRANSCRIPT_WARNING` | warning | Empty transcript text or no timed segments |
74
93
  | `DETERMINISM_WARNING` | warning | Forbidden clock/randomness, CSS transition, or runtime diagnostic |
75
94
  | `COMPOSITION_NOT_READY` | error | `seek`/`settle` before a successful load |
76
95
  | `COMPOSITION_DISPOSED` | error | Operation after the host was disposed |
96
+ | `IMAGE_INVALID` | error | An inspect/compare input is missing, unreadable, not an image, or has different dimensions in a comparison |
97
+ | `POLICY_INVALID` | error | `fy.policy.json` or `FRAMEYARD_POLICY` is missing a required field, has an unknown field, or uses an invalid value |
98
+ | `RESOURCE_POLICY_BLOCKED` | error | The active tier blocks the operation, `--force`, or `--overwrite`; use `--dry-run` or ask the user for a `full` run |
99
+ | `RESOURCE_POLICY_LIMIT` | error | A still request exceeds the active tier's frame cap |
100
+ | `RESOURCE_PROFILE_CONFLICT` | error | `--workers` or `--concurrency` exceeds the `shared-cpu` profile cap |
77
101
 
78
102
  ## Results
79
103
 
@@ -87,6 +111,12 @@ Static validation covers:
87
111
 
88
112
  Failures use the same envelope with `error.code`, `error.message`, optional `error.details`, and top-level `diagnostics`.
89
113
 
114
+ ## Lint and resource policy
115
+
116
+ `lint` returns the same diagnostic shape as `check` but does not launch Chromium. It reports static composition and timeline errors, scene and keyframe projection errors for every timeline frame, media metadata errors, and `DETERMINISM_WARNING` for controller source that calls wall-clock or randomness APIs. It does not run `validateProps` or composition runtime.
117
+
118
+ Policy failures use the normal error envelope. `RESOURCE_POLICY_BLOCKED` details include `policy`, `operation`, `allowed`, and a `suggestion`; `RESOURCE_POLICY_LIMIT` includes `limit` and `requested`; `RESOURCE_PROFILE_CONFLICT` includes `profile`, `option`, `limit`, and `requested`. Run the blocked `tts`, `transcribe`, or `render` command with `--dry-run` to get the handoff plan instead of treating the block as a render failure. `font add` requires `authoring` or `full`; `font remove` and `font add --force` require `full`.
119
+
90
120
  ## Working with diagnostics
91
121
 
92
122
  1. Run `npx frameyard check <id> --json`.
@@ -7,13 +7,13 @@ Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and onl
7
7
  | Code | Meaning | Action |
8
8
  | --- | --- | --- |
9
9
  | `USAGE` | Invalid flags or missing required arguments | Read `npx frameyard <command> --help`. |
10
- | `CONFIG_NOT_FOUND` | No `video.config.js` found | Run from the project root or pass `--project`. |
11
- | `CONFIG_INVALID` | Invalid config or composition entry | Fix `video.config.js`; entries must be explicit, local, `.html`, and exist. |
12
- | `COMPOSITION_NOT_FOUND` | ID is not listed | Add the entry to `video.config.js`. |
10
+ | `CONFIG_NOT_FOUND` | No `fy.config.js` found | Run from the project root or pass `--project`. |
11
+ | `CONFIG_INVALID` | Invalid config or composition entry | Fix `fy.config.js`; entries must be explicit, local, `.html`, and exist. |
12
+ | `COMPOSITION_NOT_FOUND` | ID is not listed | Add the entry to `fy.config.js`. |
13
13
  | `COMPOSITION_INVALID` | Invalid root, metadata, fragment structure, or reserved attribute | Fix the composition fragment; check `details.line` and `details.attribute`. |
14
14
  | `COMPOSITION_DOCUMENT_SHELL` | A full document shell is present | Remove `<!doctype>`, `<html>`, `<head>`, and `<body>`; keep the fragment body and top-level metadata. See the 0.8 migration guide. |
15
15
  | `IMPORT_MAP_INVALID` | Invalid import map placement, JSON, attributes, or reserved specifier | Keep one or more top-level import maps; remove `frameyard`/`frameyard/` keys and forbidden attributes. |
16
- | `COMPOSITION_FORMAT_UNRESOLVED` | The composition omits `width`/`height` and the project has no `format` | Declare `width`/`height` on `<fy-composition>`, add `format { width, height, fps }` to `video.config.js`, or render through a timeline. |
16
+ | `COMPOSITION_FORMAT_UNRESOLVED` | The composition omits `width`/`height` and the project has no `format` | Declare `width`/`height` on `<fy-composition>`, add `format { width, height, fps }` to `fy.config.js`, or render through a timeline. |
17
17
  | `COMPOSITION_ERROR` | Mount, update, or browser page error | Read the frame location; remove forbidden clocks/randomness or fix the controller. |
18
18
  | `COMPOSITION_UPDATE_ASYNC` | `update()` returned a promise | Move async work to `mount()`. |
19
19
  | `COMPOSITION_NOT_READY` | `seek`/`settle` before a successful load | Await `load`/`ready`. |
@@ -42,17 +42,41 @@ Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and onl
42
42
  | `FRAME_OUT_OF_RANGE` | Frame outside `0..durationInFrames - 1` | Choose valid frames; a batch error lists every invalid frame and the valid range. |
43
43
  | `AUDIO_INVALID` | Bad clip or unreadable/unsupported audio | Use a local project-root-relative file with a decodable audio stream. |
44
44
  | `AUDIO_WARNING` | Audio timeline overrun or silent embedded media | Adjust timing/trim or remove the silent source. |
45
+ | `TIMELINE_TRANSITION_UNKNOWN_CLIP` | A transition names a missing or self clip | Use two distinct resolved clip IDs in this timeline. |
46
+ | `TIMELINE_TRANSITION_WRONG_TRACK` | The pair is on different tracks | Keep both clips on one video track. |
47
+ | `TIMELINE_TRANSITION_AUDIO_TRACK` | A `dissolve` is on an audio track | Use `"kind": "crossfade"`. |
48
+ | `TIMELINE_TRANSITION_VIDEO_TRACK` | A `crossfade` is on a video track | Use `"kind": "dissolve"`. |
49
+ | `TIMELINE_TRANSITION_NOT_OVERLAPPING` | The incoming clip starts at or after the outgoing clip ends | Move `to.start` earlier; touching endpoints are not a transition. |
50
+ | `TIMELINE_TRANSITION_GEOMETRY` | Reversed order or the incoming clip ends before the outgoing clip | Use `from.start < to.start < from.end < to.end`. |
51
+ | `TIMELINE_TRANSITION_DUPLICATE` | Repeated pair or a clip used twice on one side | Declare each pair once; a clip may chain `from → to → from`. |
52
+ | `TIMELINE_TRANSITION_KIND` | Unsupported or missing `kind` | Use `"kind": "dissolve"` on a video track or `"kind": "crossfade"` on an audio track. |
53
+ | `TIMELINE_TRANSITION_INVALID` | Unknown transition field, invalid id, or unsupported easing | Fix the entry; easing uses the keyframe timing-function vocabulary. |
45
54
  | `MEDIA_INVALID` | Invalid media source or attributes | Use local, seekable files and valid `data-fy-*` values. |
46
55
  | `MEDIA_SEEK_FAILED` | Embedded video did not reach the requested time | Re-encode with a regular keyframe interval or replace the source. |
47
56
  | `TRANSCRIPT_INVALID` | Invalid/missing transcript declaration or JSON | Regenerate with `npx frameyard transcribe`; do not hand-edit timestamps. |
48
57
  | `TRANSCRIPT_WORDS_UNAVAILABLE` | Karaoke requested without measured word timings | Re-run transcription with `--words` or remove `words`. |
49
58
  | `TRANSCRIPT_WARNING` | Empty transcript text or no timed segments | Regenerate the transcript. |
50
59
  | `ASSET_NOT_FOUND` | Required file is missing or unreadable | Restore the asset, model, or text file. |
60
+ | `FONT_MANIFEST_INVALID` | Invalid or unknown manifest field, version, id, family, path, face, or weight | Fix `fy.fonts.json`; reinstall with `npx frameyard font add --file` instead of hand-editing paths. |
61
+ | `FONT_FILE_MISSING` | A font file in the manifest does not exist | Restore the file or remove the face/family. |
62
+ | `FONT_FILE_INVALID` | Wrong extension, signature, or non-file path | Reinstall the font from a valid WOFF2/WOFF/TTF/OTF source. |
63
+ | `FONT_HASH_MISMATCH` | A vendored font file changed size or hash | Restore the file, or reinstall with `npx frameyard font add --force` after approval. |
64
+ | `FONT_LICENSE_MISSING` | License type/file is unknown or absent | Add `--license`/`--license-file`; do not redistribute the project until resolved. |
65
+ | `FONT_LOAD_FAILED` | Chromium could not load a used `@font-face` | Run `npx frameyard font verify`; fix the manifest or file. |
66
+ | `FONT_PROVIDER_UNAVAILABLE` | Fontsource API/CDN is unreachable or returned an invalid response | Retry, check the network, or install the file with `font add --file`/`--url`. |
67
+ | `FONT_DOWNLOAD_FAILED` | A Fontsource or URL response was not a valid font file | Retry the install; verify the URL and family metadata. |
68
+ | `FONT_REMOTE` | CSS still references a remote font/stylesheet | Vendor the font with `font add`; remote fonts are not reproducible. |
69
+ | `FONT_NOT_FOUND` | Fontsource id or `font remove` id is not installed | Search with `npx frameyard font search --json`, or list installed ids. |
51
70
  | `DETERMINISM_WARNING` | Forbidden clock/randomness, CSS transition, or runtime warning | Derive state from `frame`/`props`; use `random("stable:key")`. |
52
71
  | `OUTPUT_EXISTS` | Output exists and replacement was not approved | Reuse it, choose another path, or pass `--overwrite`/`--force` with permission. |
72
+ | `IMAGE_INVALID` | An inspected or compared image is missing, unreadable, or has different dimensions | Check the paths; for `compare`, use images with the same width and height. |
73
+ | `POLICY_INVALID` | The workspace policy file or `FRAMEYARD_POLICY` is malformed | Read the `details.path`/`jsonPath`, fix the field, or ask the user to remove the override. |
74
+ | `RESOURCE_POLICY_BLOCKED` | The active tier blocks the operation, `--force`, or `--overwrite` | Use `--dry-run` for a handoff plan, or ask the user to run the heavy command with `FRAMEYARD_POLICY=full`. |
75
+ | `RESOURCE_POLICY_LIMIT` | The still request exceeds the tier frame cap | Split the frames into smaller calls; do not assume the 32-frame library cap is available. |
76
+ | `RESOURCE_PROFILE_CONFLICT` | `--workers` or `--concurrency` exceeds the `shared-cpu` cap | Lower the value or omit `--profile`; the CLI does not clamp it. |
53
77
  | `OUTPUT_FAILED` | An output or manifest could not be written atomically | Check directory permissions and free space; rerun. |
54
- | `EXECUTABLE_NOT_FOUND` | `qwen-tts` or `whisper-cli` could not be resolved | Pass `--executable`, set the environment variable, or fix `PATH`. |
55
- | `TTS_FAILED` | Speech synthesis failed or produced an invalid WAV | Read stderr; verify the talker/codec models and CLI compatibility. |
78
+ | `EXECUTABLE_NOT_FOUND` | `qwen-tts`, `sherpa-onnx-offline-tts`, or `whisper-cli` could not be resolved | Pass `--executable`, set `FRAMEYARD_QWEN_TTS`, `FRAMEYARD_SHERPA_ONNX_TTS`, or `FRAMEYARD_WHISPER_CLI`, or fix `PATH`. |
79
+ | `TTS_FAILED` | Speech synthesis failed or produced an invalid WAV | Read stderr; verify the engine-specific models (`--talker-model`/`--codec-model` for qwen, `--model-dir` for Supertonic) and CLI compatibility. |
56
80
  | `TRANSCRIBE_FAILED` | Normalization or `whisper-cli` failed | Verify FFmpeg, the source media, and the Whisper model. |
57
81
  | `MANIFEST_INVALID` | Provenance manifest is corrupt or unsupported | Restore or regenerate the manifest; do not invent hashes. |
58
82
  | `FFMPEG_FAILED` | FFmpeg is missing, lacks encoders, or failed | Run `npx frameyard doctor --json`; install FFmpeg with libx264 and AAC. |
@@ -62,6 +86,8 @@ Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and onl
62
86
  | `CANCELLED` | The operation was interrupted | Rerun when ready. Partial outputs are removed. |
63
87
  | `INTERNAL` | Unexpected internal failure | Rerun with `FRAMEYARD_DEBUG=1` and report the stack. |
64
88
 
89
+ When a heavy command is blocked, run it once with `--dry-run`. The result contains `commandLine`, `prerequisites`, and `blockers`; report `deferred: true` and the command instead of claiming the render or asset exists. The command line already includes `FRAMEYARD_POLICY=full` when the active tier is not `full`.
90
+
65
91
  ## Behavior to rely on
66
92
 
67
93
  - A failed render writes to a temporary file and does not publish a partial output.
@@ -0,0 +1,132 @@
1
+ # Fonts
2
+
3
+ Frameyard vendors font files into the project. A composition uses a font by its
4
+ CSS family name; the preview server injects the generated `@font-face` sheet
5
+ into every composition before author styles. There is no runtime font CDN
6
+ dependency.
7
+
8
+ ## Search Fontsource
9
+
10
+ ```sh
11
+ npx frameyard font search "space grotesk" --subset latin --json
12
+ npx frameyard font search --category sans-serif --variable --limit 10 --json
13
+ ```
14
+
15
+ Filters are `--category`, `--subset`, `--style`, `--variable`, and `--limit`
16
+ (1–100). Search returns `id`, `family`, `category`, `variable`, `weights`,
17
+ `styles`, `subsets`, `license`, and `type`. Fontsource is the default catalog;
18
+ Google Fonts is covered through Fontsource and does not need a separate API key.
19
+
20
+ ## Install a Fontsource family
21
+
22
+ ```sh
23
+ npx frameyard font add space-grotesk --subset latin
24
+ ```
25
+
26
+ The command fetches the family metadata, pins the Fontsource `npmVersion`,
27
+ downloads only the requested WOFF2 files, downloads the package `LICENSE`, and
28
+ records the exact CDN URL and SHA-256 in `fy.fonts.json`.
29
+
30
+ - A variable family installs the `wght` axis by default. Use multiple
31
+ `--subset` and `--style` values, for example
32
+ `--subset latin,latin-ext --style normal,italic`.
33
+ - `--static` installs static files. `--weight` accepts a comma-separated list,
34
+ for example `--static --weight 400,700`.
35
+ - `--family` overrides the CSS family name when a project needs an alias.
36
+ - Fontsource supplies the license; do not pass `--license` for a Fontsource id.
37
+ - A variable family and a static install of the same family cannot be mixed
38
+ without `--force`, which replaces the family record.
39
+
40
+ `FONT_PROVIDER_UNAVAILABLE` means the Fontsource API or CDN was unreachable.
41
+ `FONT_DOWNLOAD_FAILED` means a response was not a valid WOFF2 file.
42
+
43
+ ## Install a local or URL font
44
+
45
+ ```sh
46
+ npx frameyard font add --file ./fonts/SpaceGrotesk.woff2 \
47
+ --family "Space Grotesk" --weight 300-700 --subset latin \
48
+ --license OFL-1.1 --license-file ./fonts/OFL.txt
49
+ ```
50
+
51
+ `--weight` is a number (`400`) or a variable range (`300-700`). `--style` is
52
+ `normal` or `italic`. `--subset`, `--unicode-range`, `--license`, and
53
+ `--license-file` are optional; a missing license is a warning. The command
54
+ copies the font and license under `assets/fonts/<id>/`, records the SHA-256 and
55
+ byte size, and prints the CSS family to use.
56
+
57
+ ```sh
58
+ npx frameyard font add --url https://example.com/Inter.woff2 \
59
+ --family "Inter" --license OFL-1.1 --license-file ./fonts/OFL.txt \
60
+ --sha256 <hash>
61
+ ```
62
+
63
+ `--url` requires HTTPS; loopback HTTP is allowed for local tests. `--sha256` is
64
+ optional but recommended: the computed hash is always recorded. A URL import
65
+ without `--license`/`--license-file` records `license: { "type": "unknown" }`,
66
+ warns in
67
+ `list`/`verify`/`doctor`, requires the `full` policy, and does not use a shared
68
+ cache. Do not point `--url` at a font whose license you cannot redistribute.
69
+
70
+ Fontsource is the primary catalog. Google Fonts is covered by Fontsource.
71
+ Bunny Fonts is not a vendoring source: its catalog is undocumented, it exposes
72
+ no license metadata, and its files are not frozen. Fontshare is not a provider:
73
+ its Free Font License prohibits redistribution, subsetting, and format
74
+ conversion for most of its catalog. Only OFL-licensed Fontshare files may be
75
+ vendored, through `font add --url`, with their license text and an explicit
76
+ decision by the user.
77
+
78
+ ## Manage the manifest
79
+
80
+ ```sh
81
+ npx frameyard font list --json
82
+ npx frameyard font verify --json
83
+ npx frameyard font remove space-grotesk --json
84
+ ```
85
+
86
+ `font list` reports installed families, subset/style labels, license, file
87
+ count, bytes, and status. `font verify` checks paths, formats, byte sizes,
88
+ SHA-256 values, license files, and orphaned font directories. `font remove`
89
+ deletes the manifest entry and `assets/fonts/<id>/`; add `--keep-files` to leave
90
+ the binaries in place. `font remove`, `font add --force`, and an unlicensed
91
+ `--url` import require the `full` policy.
92
+
93
+ Adding another subset, style, or weight to the same family merges into the
94
+ existing entry. Adding the same face again fails with `OUTPUT_EXISTS`; pass
95
+ `--force` to replace it. `fy.fonts.json` has `version: 1` and one entry
96
+ per family. Commit it with `assets/fonts/` and the license files.
97
+
98
+ ## Use the font
99
+
100
+ ```css
101
+ .title {
102
+ font-family: "Space Grotesk", system-ui, sans-serif;
103
+ font-weight: 700;
104
+ }
105
+ ```
106
+
107
+ The generated sheet uses `font-display: block`. The host waits for
108
+ `document.fonts.ready` after `mount()`, then fails with `FONT_LOAD_FAILED` if a
109
+ used face reports an error instead of silently capturing fallback text.
110
+ `check`, `still`, and `render` verify sizes and hashes before launching
111
+ Chromium; `lint` verifies existence and size. A changed file fails with
112
+ `FONT_HASH_MISMATCH`.
113
+
114
+ ## Legacy fonts and remote URLs
115
+
116
+ Existing local `@font-face` rules and `<link rel="stylesheet">` elements keep
117
+ working. A remote font URL in inline CSS, linked local CSS, or a remote
118
+ stylesheet link produces a `FONT_REMOTE` warning. It may work during the loading
119
+ phase, but it is not reproducible and is not covered by the manifest.
120
+
121
+ ## Agent workflow
122
+
123
+ 1. Search Fontsource with `font search` or choose a local/URL source and verify
124
+ its license.
125
+ 2. Install with `font add`, preferring the variable face or the exact static
126
+ weights the composition uses.
127
+ 3. Use the returned family name in the composition CSS.
128
+ 4. Run `font verify`, `context --json`, `lint`, and `still`.
129
+ 5. Inspect the still when typography or line breaks change.
130
+
131
+ Do not download fonts from `update()` or `mount()`. Font acquisition is an
132
+ authoring operation, not frame production.
@@ -51,7 +51,107 @@ Additional fields are command-specific and stable within the 0.8 contract. `chec
51
51
  }
52
52
  ```
53
53
 
54
- Static validation diagnostics include source locations. Runtime diagnostics may include a `frame`. Audio and transcript warnings use `AUDIO_WARNING` and `TRANSCRIPT_WARNING`; controller clock/randomness and CSS transition warnings use `DETERMINISM_WARNING`. See [diagnostics](diagnostics.md) for the code table.
54
+ Static validation diagnostics include source locations. Runtime diagnostics may include a `frame`. Timeline diagnostics add `timeline`, `clip`, `track`, `jsonPath`, and, for keyframe errors, `channel` and the zero-based `keyframe` index; transition diagnostics add `from` and `to`. Audio and transcript warnings use `AUDIO_WARNING` and `TRANSCRIPT_WARNING`; controller clock/randomness and CSS transition warnings use `DETERMINISM_WARNING`. See [diagnostics](diagnostics.md) for the code table.
55
+
56
+ ## lint
57
+
58
+ `lint` reports `kind: "composition"` or `kind: "timeline"` and the same diagnostic shape. A timeline result includes `framesProjected`, `segments`, `tracks`, `clips`, and `transitions`; a composition result includes `metadata` and reports `framesProjected: 0` because lint does not seek frames. The result also includes the active `policy` details.
59
+
60
+ ```json
61
+ {
62
+ "ok": true,
63
+ "command": "lint",
64
+ "policy": { "policy": "authoring", "maxStillFrames": 4, "allowOverwrite": false, "source": "env" },
65
+ "kind": "timeline",
66
+ "timeline": "main",
67
+ "entry": "timelines/main.timeline.json",
68
+ "format": { "width": 1920, "height": 1080, "fps": 30 },
69
+ "durationInFrames": 900,
70
+ "framesProjected": 900,
71
+ "segments": 3,
72
+ "diagnostics": [],
73
+ "elapsedMs": 2
74
+ }
75
+ ```
76
+
77
+ `lint` does not run `validateProps` or composition runtime. A clean lint is not a successful `check`.
78
+
79
+ ## context
80
+
81
+ `context` returns a read-only project digest. It includes `root`, `format`, `policy`, `compositions`, `timelines`, `assets`, `fonts`, `next`, and `truncated`. Each composition carries its resolved `metadata`, prop `path`/`type` pairs, controller, and diagnostic counts; each timeline carries duration, tracks, clip summaries, transitions, audio/transcript counts, and diagnostic counts. Each font carries `id`, `family`, `provider`, optional `version`, `variable`, `subsets`, `styles`, `license`, `files`, `bytes`, and `status`. `truncated.fonts` reports a clipped font list.
82
+
83
+ ```json
84
+ {
85
+ "ok": true,
86
+ "command": "context",
87
+ "root": "/path/to/project",
88
+ "format": { "width": 1920, "height": 1080, "fps": 30 },
89
+ "policy": { "policy": "authoring", "profile": "shared-cpu", "maxStillFrames": 4, "allowOverwrite": false, "source": "file" },
90
+ "compositions": [
91
+ { "id": "main", "entry": "compositions/main.html", "kind": "animated", "metadata": { "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 900 }, "controller": { "kind": "external", "src": "compositions/main.controller.js" }, "props": [{ "path": "title", "type": "string" }], "diagnostics": { "errors": 0, "warnings": 0, "codes": [] } }
92
+ ],
93
+ "timelines": [],
94
+ "assets": [],
95
+ "next": ["npx frameyard lint main --json"],
96
+ "truncated": { "clips": false, "assets": false, "props": false }
97
+ }
98
+ ```
99
+
100
+ ## inspect
101
+
102
+ `inspect` decodes the first frame and reports `width`, `height`, `bytes`, `alpha`, `blank`, `background`, `content`, `colors`, `luminanceGrid`, and `edgeDensity`. `alpha.coverage` is the opaque fraction; `content.bbox` is `null` when no pixels differ from the background. It never launches Chromium.
103
+
104
+ ## compare
105
+
106
+ `compare` reports `identical`, `differingPixels`, `totalPixels`, `percent`, `maxChannelDelta`, `meanChannelDelta`, `bbox`, and 3×3 `regions`. It exits 1 when the images differ; the JSON body still has `ok: true` and `identical: false`. Different dimensions fail with `IMAGE_INVALID`.
107
+
108
+ ## add
109
+
110
+ `add --list` returns `templates` with `name`, `kind`, `description`, and optional `prerequisites`. Adding an entry returns `kind`, `id`, `template`, `entry`, `path`, `register`, `prerequisites`, and `next`. `register` is the exact object to merge into `fy.config.js`; `add` does not edit the config itself.
111
+
112
+ ## font
113
+
114
+ `font search` returns `provider: "fontsource"`, the `query`, applied `filters`, `results`, and `truncated`. Each result has `id`, `family`, `category`, `variable`, `weights`, `styles`, `subsets`, `license`, optional `version`, and `type`.
115
+
116
+ `font add` (Fontsource id, `--file`, or `--url`) returns `action: "add"`, a `font` summary (`id`, `family`, `provider`, optional `version`, `variable`, `subsets`, `styles`, `license`, `files`, `bytes`, `status`), the `manifest` path, installed `files`, `warnings`, and `next` commands. Fontsource results identify `provider: "fontsource"` and the pinned `version`; URL results identify `provider: "url"`. `font list` returns `manifest` and a `fonts` array of the same summaries. `font verify` returns `manifest`, `fonts`, `files`, `bytes`, `valid`, and `diagnostics`; errors exit 1, warnings exit 0. `font remove` returns `id`, `family`, `manifest`, `files`, and `keptFiles`.
117
+
118
+ ```json
119
+ {
120
+ "ok": true,
121
+ "command": "font",
122
+ "action": "verify",
123
+ "manifest": "/project/fy.fonts.json",
124
+ "fonts": 1,
125
+ "files": 1,
126
+ "bytes": 22288,
127
+ "valid": true,
128
+ "diagnostics": []
129
+ }
130
+ ```
131
+
132
+ A present-but-invalid manifest fails with `FONT_MANIFEST_INVALID`; missing files use `FONT_FILE_MISSING`, changed content uses `FONT_HASH_MISMATCH`, and a failed browser load uses `FONT_LOAD_FAILED`. Remote font CSS is a `FONT_REMOTE` warning. Fontsource network or API failures use `FONT_PROVIDER_UNAVAILABLE`; invalid downloaded WOFF2 content uses `FONT_DOWNLOAD_FAILED`; a missing Fontsource family or `font remove` id uses `FONT_NOT_FOUND`.
133
+
134
+ ## dry run
135
+
136
+ `tts`, `transcribe`, and `render` accept `--dry-run`. The result has `dryRun: true`, the active `policy`, a command-specific `plan`, a shell-ready `commandLine`, `prerequisites`, `blockers`, and the output path. It does not create the output directory, write the manifest, load a model, launch Chromium, or encode.
137
+
138
+ ```json
139
+ {
140
+ "ok": true,
141
+ "command": "render",
142
+ "dryRun": true,
143
+ "deferred": true,
144
+ "policy": { "policy": "authoring", "maxStillFrames": 4, "allowOverwrite": false, "source": "env" },
145
+ "plan": { "kind": "video", "frames": 900, "segments": 3, "workers": 2, "encoder": "auto", "codec": "h264", "container": "mp4", "probed": false },
146
+ "commandLine": "FRAMEYARD_POLICY=full npx frameyard render main --workers 2 --codec h264 --container mp4",
147
+ "prerequisites": ["assets/voice.wav"],
148
+ "blockers": [],
149
+ "diagnostics": [],
150
+ "output": { "path": "out/main.mp4", "exists": false }
151
+ }
152
+ ```
153
+
154
+ An existing output appears in `blockers` with `OUTPUT_EXISTS`; the dry run still exits 0. The handoff command includes `FRAMEYARD_POLICY=full` when the active tier is not `full`. A dry run is not a render; `deferred: true` means the heavy operation was not executed and the `commandLine` is the handoff.
55
155
 
56
156
  ## check
57
157
 
@@ -111,6 +211,10 @@ A static composition result reports `kind: "static"`, dimensions only, `framesCh
111
211
  }
112
212
  ```
113
213
 
214
+ ### Timeline check
215
+
216
+ A timeline check reports `timeline`, `entry`, `format`, `durationInFrames`, `framesChecked`, `tracks`, `clips`, `transitions`, `audio`, `transcripts`, `diagnostics`, and `elapsedMs`. `framesChecked` equals the timeline duration; `transitions` is the resolved count, and transition diagnostics carry `from`/`to` and `jsonPath` in `details`.
217
+
114
218
  ## still
115
219
 
116
220
  ```json
@@ -191,6 +295,7 @@ A successful render means the final path exists and was published atomically. A
191
295
  {
192
296
  "ok": true,
193
297
  "command": "tts",
298
+ "engine": "qwen",
194
299
  "path": "/absolute/path/assets/voice.wav",
195
300
  "bytes": 480000,
196
301
  "sha256": "64-character hex digest",
@@ -198,12 +303,12 @@ A successful render means the final path exists and was published atomically. A
198
303
  "channels": 1,
199
304
  "speaker": "vivian",
200
305
  "language": "English",
201
- "manifest": "/absolute/path/frameyard.assets.json",
306
+ "manifest": "/absolute/path/fy.assets.json",
202
307
  "elapsedMs": 12000
203
308
  }
204
309
  ```
205
310
 
206
- `language` is omitted when it was not supplied.
311
+ `engine` is `qwen` or `supertonic`. `language` is omitted when it was not supplied; Supertonic always supplies it. `device` is present only for qwen.
207
312
 
208
313
  ## transcribe
209
314
 
@@ -230,7 +335,7 @@ A successful render means the final path exists and was published atomically. A
230
335
  "sha256": "64-character hex digest"
231
336
  }
232
337
  ],
233
- "manifest": "/absolute/path/frameyard.assets.json",
338
+ "manifest": "/absolute/path/fy.assets.json",
234
339
  "elapsedMs": 30000
235
340
  }
236
341
  ```
@@ -239,6 +344,8 @@ A successful render means the final path exists and was published atomically. A
239
344
 
240
345
  ## doctor
241
346
 
347
+ The doctor result adds `policy` and `resources` fields. `policy.status` is `ok` or `error`; an invalid policy is reported as a finding without preventing the environment checks. `resources` reports platform, architecture, CPU count, load average, and total/free memory. `fonts` reports the project font manifest path, whether it exists, `valid`, installed `fonts`/`files`/`bytes`, and an optional `detail` for the first integrity problem.
348
+
242
349
  Doctor reports findings rather than throwing for environment problems. `ok: true` means the inspection completed; `healthy` says whether required checks, manifest integrity, explicitly supplied models, and integration compatibility passed. `ffmpeg-opus-encoder` and `ffmpeg-video-encoders` are optional: the video check probes each supported encoder with a one-frame encode and lists any that FFmpeg advertises but cannot open (an old driver, for example), and neither check affects `healthy`.
243
350
 
244
351
  ```json
@@ -271,6 +378,12 @@ Doctor reports findings rather than throwing for environment problems. `ok: true
271
378
  "status": "missing",
272
379
  "requested": false,
273
380
  "referenceRevision": "..."
381
+ },
382
+ "sherpaOnnxTts": {
383
+ "name": "sherpa-onnx-offline-tts",
384
+ "status": "missing",
385
+ "requested": false,
386
+ "referenceRevision": "..."
274
387
  }
275
388
  },
276
389
  "models": [
@@ -282,8 +395,8 @@ Doctor reports findings rather than throwing for environment problems. `ok: true
282
395
  "sha256": "64-character hex digest"
283
396
  }
284
397
  ],
285
- "manifest": { "path": "/absolute/path/frameyard.assets.json", "exists": true, "valid": true, "artifacts": 2 }
398
+ "manifest": { "path": "/absolute/path/fy.assets.json", "exists": true, "valid": true, "artifacts": 2 }
286
399
  }
287
400
  ```
288
401
 
289
- `models` is populated only for model paths passed explicitly with `--talker-model`, `--codec-model`, or `--model`. `healthy: false` exits `1`. Missing optional integrations do not fail, but a found executable with incompatible capabilities does.
402
+ `models` is populated only for model paths passed explicitly with `--talker-model`, `--codec-model`, `--model-dir`, or `--model`. A Supertonic `--model-dir` reports one entry with role `supertonic`; `tts` provenance records the seven individual model files. `healthy: false` exits `1`. Missing optional integrations do not fail, but a found executable with incompatible capabilities does.