frameyard 0.7.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 (334) hide show
  1. package/CHANGELOG.md +164 -7
  2. package/GLOSSARY.md +152 -18
  3. package/README.md +328 -285
  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 +1705 -106
  21. package/dist/cli.js.map +1 -1
  22. package/dist/composition/descriptor.d.ts +17 -3
  23. package/dist/composition/descriptor.d.ts.map +1 -1
  24. package/dist/composition/descriptor.js +51 -28
  25. package/dist/composition/descriptor.js.map +1 -1
  26. package/dist/composition/format.d.ts +12 -0
  27. package/dist/composition/format.d.ts.map +1 -0
  28. package/dist/composition/format.js +28 -0
  29. package/dist/composition/format.js.map +1 -0
  30. package/dist/composition/index.d.ts +2 -1
  31. package/dist/composition/index.d.ts.map +1 -1
  32. package/dist/composition/index.js +1 -0
  33. package/dist/composition/index.js.map +1 -1
  34. package/dist/context.d.ts +82 -0
  35. package/dist/context.d.ts.map +1 -0
  36. package/dist/context.js +277 -0
  37. package/dist/context.js.map +1 -0
  38. package/dist/contracts.d.ts +80 -6
  39. package/dist/contracts.d.ts.map +1 -1
  40. package/dist/contracts.js +3 -0
  41. package/dist/contracts.js.map +1 -1
  42. package/dist/doctor.d.ts +14 -1
  43. package/dist/doctor.d.ts.map +1 -1
  44. package/dist/doctor.js +166 -7
  45. package/dist/doctor.js.map +1 -1
  46. package/dist/errors.d.ts +1 -1
  47. package/dist/errors.d.ts.map +1 -1
  48. package/dist/errors.js.map +1 -1
  49. package/dist/fonts/add.d.ts +41 -0
  50. package/dist/fonts/add.d.ts.map +1 -0
  51. package/dist/fonts/add.js +259 -0
  52. package/dist/fonts/add.js.map +1 -0
  53. package/dist/fonts/binary.d.ts +4 -0
  54. package/dist/fonts/binary.d.ts.map +1 -0
  55. package/dist/fonts/binary.js +21 -0
  56. package/dist/fonts/binary.js.map +1 -0
  57. package/dist/fonts/css.d.ts +4 -0
  58. package/dist/fonts/css.d.ts.map +1 -0
  59. package/dist/fonts/css.js +50 -0
  60. package/dist/fonts/css.js.map +1 -0
  61. package/dist/fonts/fontsource.d.ts +107 -0
  62. package/dist/fonts/fontsource.d.ts.map +1 -0
  63. package/dist/fonts/fontsource.js +565 -0
  64. package/dist/fonts/fontsource.js.map +1 -0
  65. package/dist/fonts/index.d.ts +11 -0
  66. package/dist/fonts/index.d.ts.map +1 -0
  67. package/dist/fonts/index.js +11 -0
  68. package/dist/fonts/index.js.map +1 -0
  69. package/dist/fonts/install.d.ts +24 -0
  70. package/dist/fonts/install.d.ts.map +1 -0
  71. package/dist/fonts/install.js +145 -0
  72. package/dist/fonts/install.js.map +1 -0
  73. package/dist/fonts/manifest.d.ts +21 -0
  74. package/dist/fonts/manifest.d.ts.map +1 -0
  75. package/dist/fonts/manifest.js +96 -0
  76. package/dist/fonts/manifest.js.map +1 -0
  77. package/dist/fonts/options.d.ts +9 -0
  78. package/dist/fonts/options.d.ts.map +1 -0
  79. package/dist/fonts/options.js +66 -0
  80. package/dist/fonts/options.js.map +1 -0
  81. package/dist/fonts/schema.d.ts +4 -0
  82. package/dist/fonts/schema.d.ts.map +1 -0
  83. package/dist/fonts/schema.js +316 -0
  84. package/dist/fonts/schema.js.map +1 -0
  85. package/dist/fonts/summary.d.ts +16 -0
  86. package/dist/fonts/summary.d.ts.map +1 -0
  87. package/dist/fonts/summary.js +22 -0
  88. package/dist/fonts/summary.js.map +1 -0
  89. package/dist/fonts/types.d.ts +56 -0
  90. package/dist/fonts/types.d.ts.map +1 -0
  91. package/dist/fonts/types.js +8 -0
  92. package/dist/fonts/types.js.map +1 -0
  93. package/dist/fonts/verify.d.ts +33 -0
  94. package/dist/fonts/verify.d.ts.map +1 -0
  95. package/dist/fonts/verify.js +200 -0
  96. package/dist/fonts/verify.js.map +1 -0
  97. package/dist/html/document-adapter.d.ts +3 -3
  98. package/dist/html/document-adapter.d.ts.map +1 -1
  99. package/dist/html/document-adapter.js +1 -1
  100. package/dist/html/document-adapter.js.map +1 -1
  101. package/dist/html/index.d.ts +1 -1
  102. package/dist/html/index.d.ts.map +1 -1
  103. package/dist/html/parse.d.ts +10 -2
  104. package/dist/html/parse.d.ts.map +1 -1
  105. package/dist/html/parse.js +14 -5
  106. package/dist/html/parse.js.map +1 -1
  107. package/dist/html/validate.d.ts +12 -1
  108. package/dist/html/validate.d.ts.map +1 -1
  109. package/dist/html/validate.js +292 -61
  110. package/dist/html/validate.js.map +1 -1
  111. package/dist/index.d.ts +4 -4
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +2 -2
  114. package/dist/index.js.map +1 -1
  115. package/dist/inspect.d.ts +61 -0
  116. package/dist/inspect.d.ts.map +1 -0
  117. package/dist/inspect.js +325 -0
  118. package/dist/inspect.js.map +1 -0
  119. package/dist/lint.d.ts +63 -0
  120. package/dist/lint.d.ts.map +1 -0
  121. package/dist/lint.js +265 -0
  122. package/dist/lint.js.map +1 -0
  123. package/dist/policy.d.ts +69 -0
  124. package/dist/policy.d.ts.map +1 -0
  125. package/dist/policy.js +361 -0
  126. package/dist/policy.js.map +1 -0
  127. package/dist/preview/main.d.ts +2 -2
  128. package/dist/preview/main.d.ts.map +1 -1
  129. package/dist/preview/main.js +35 -14
  130. package/dist/preview/main.js.map +1 -1
  131. package/dist/preview/timeline-data.d.ts +17 -65
  132. package/dist/preview/timeline-data.d.ts.map +1 -1
  133. package/dist/preview/timeline-host.d.ts.map +1 -1
  134. package/dist/preview/timeline-host.js +73 -38
  135. package/dist/preview/timeline-host.js.map +1 -1
  136. package/dist/preview/timeline-main.d.ts.map +1 -1
  137. package/dist/preview/timeline-main.js +47 -3
  138. package/dist/preview/timeline-main.js.map +1 -1
  139. package/dist/project.d.ts +13 -8
  140. package/dist/project.d.ts.map +1 -1
  141. package/dist/project.js +20 -17
  142. package/dist/project.js.map +1 -1
  143. package/dist/renderer/browser.d.ts +2 -2
  144. package/dist/renderer/browser.d.ts.map +1 -1
  145. package/dist/renderer/browser.js +10 -4
  146. package/dist/renderer/browser.js.map +1 -1
  147. package/dist/renderer/ffmpeg.d.ts +44 -1
  148. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  149. package/dist/renderer/ffmpeg.js +371 -73
  150. package/dist/renderer/ffmpeg.js.map +1 -1
  151. package/dist/renderer/index.d.ts +40 -4
  152. package/dist/renderer/index.d.ts.map +1 -1
  153. package/dist/renderer/index.js +100 -25
  154. package/dist/renderer/index.js.map +1 -1
  155. package/dist/renderer/keyframe-expressions.d.ts +39 -0
  156. package/dist/renderer/keyframe-expressions.d.ts.map +1 -0
  157. package/dist/renderer/keyframe-expressions.js +180 -0
  158. package/dist/renderer/keyframe-expressions.js.map +1 -0
  159. package/dist/renderer/timeline.d.ts +2 -0
  160. package/dist/renderer/timeline.d.ts.map +1 -1
  161. package/dist/renderer/timeline.js +168 -106
  162. package/dist/renderer/timeline.js.map +1 -1
  163. package/dist/runtime/host.d.ts.map +1 -1
  164. package/dist/runtime/host.js +159 -31
  165. package/dist/runtime/host.js.map +1 -1
  166. package/dist/scaffold.d.ts +3 -0
  167. package/dist/scaffold.d.ts.map +1 -1
  168. package/dist/scaffold.js +48 -317
  169. package/dist/scaffold.js.map +1 -1
  170. package/dist/server/composition-html.d.ts +15 -7
  171. package/dist/server/composition-html.d.ts.map +1 -1
  172. package/dist/server/composition-html.js +72 -16
  173. package/dist/server/composition-html.js.map +1 -1
  174. package/dist/server/server.d.ts.map +1 -1
  175. package/dist/server/server.js +56 -6
  176. package/dist/server/server.js.map +1 -1
  177. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  178. package/dist/server/timeline-preview-data.js +12 -23
  179. package/dist/server/timeline-preview-data.js.map +1 -1
  180. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  181. package/dist/server/timeline-preview-html.js +20 -3
  182. package/dist/server/timeline-preview-html.js.map +1 -1
  183. package/dist/templates.d.ts +16 -0
  184. package/dist/templates.d.ts.map +1 -0
  185. package/dist/templates.js +186 -0
  186. package/dist/templates.js.map +1 -0
  187. package/dist/timeline/easing.d.ts +36 -0
  188. package/dist/timeline/easing.d.ts.map +1 -0
  189. package/dist/timeline/easing.js +225 -0
  190. package/dist/timeline/easing.js.map +1 -0
  191. package/dist/timeline/keyframes.d.ts +23 -0
  192. package/dist/timeline/keyframes.d.ts.map +1 -0
  193. package/dist/timeline/keyframes.js +95 -0
  194. package/dist/timeline/keyframes.js.map +1 -0
  195. package/dist/timeline/plan.d.ts +6 -12
  196. package/dist/timeline/plan.d.ts.map +1 -1
  197. package/dist/timeline/plan.js +17 -30
  198. package/dist/timeline/plan.js.map +1 -1
  199. package/dist/timeline/resolve.d.ts +16 -3
  200. package/dist/timeline/resolve.d.ts.map +1 -1
  201. package/dist/timeline/resolve.js +119 -13
  202. package/dist/timeline/resolve.js.map +1 -1
  203. package/dist/timeline/scene.d.ts +126 -0
  204. package/dist/timeline/scene.d.ts.map +1 -0
  205. package/dist/timeline/scene.js +109 -0
  206. package/dist/timeline/scene.js.map +1 -0
  207. package/dist/timeline/schema.d.ts +38 -0
  208. package/dist/timeline/schema.d.ts.map +1 -1
  209. package/dist/timeline/schema.js +290 -9
  210. package/dist/timeline/schema.js.map +1 -1
  211. package/dist/timeline/transitions.d.ts +21 -0
  212. package/dist/timeline/transitions.d.ts.map +1 -0
  213. package/dist/timeline/transitions.js +23 -0
  214. package/dist/timeline/transitions.js.map +1 -0
  215. package/dist/version.d.ts +1 -1
  216. package/dist/version.js +1 -1
  217. package/docs/adr/0007-make-html-documents-the-composition-unit.md +2 -2
  218. package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
  219. package/docs/adr/0010-make-timelines-the-render-unit.md +3 -2
  220. package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
  221. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  222. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +38 -0
  223. package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
  224. package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
  225. package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
  226. package/docs/adr/0018-animate-timeline-clips-with-keyframes.md +35 -0
  227. package/docs/adr/0019-make-same-track-overlap-the-transition-window.md +36 -0
  228. package/docs/adr/0020-crossfade-audio-clips-on-audio-tracks.md +27 -0
  229. package/docs/adr/0021-project-timeline-scenes-once.md +32 -0
  230. package/docs/adr/0022-gate-heavy-operations-with-a-workspace-resource-policy.md +35 -0
  231. package/docs/adr/0023-pin-fonts-as-project-assets.md +98 -0
  232. package/docs/migration-0.4.md +5 -3
  233. package/docs/migration-0.8.md +163 -0
  234. package/docs/preview-testing.md +23 -3
  235. package/docs/{v0.1-design.md → v0.x-design.md} +86 -53
  236. package/examples/blend/README.md +106 -0
  237. package/examples/blend/assets/ink.mp4 +0 -0
  238. package/examples/blend/compositions/aurora.html +55 -0
  239. package/examples/blend/compositions/backdrop.html +74 -0
  240. package/examples/blend/compositions/labels.html +149 -0
  241. package/examples/blend/fy.config.js +11 -0
  242. package/examples/blend/scripts/make-ink.sh +26 -0
  243. package/examples/blend/timelines/blend.timeline.json +155 -0
  244. package/examples/demo/README.md +2 -2
  245. package/examples/demo/compositions/demo.html +243 -251
  246. package/examples/intro/README.md +71 -0
  247. package/examples/intro/assets/fonts/figtree/LICENSE.txt +93 -0
  248. package/examples/intro/assets/fonts/figtree/figtree-latin-wght-normal.woff2 +0 -0
  249. package/examples/intro/assets/fonts/instrument-serif/LICENSE.txt +93 -0
  250. package/examples/intro/assets/fonts/instrument-serif/instrument-serif-latin-400-italic.woff2 +0 -0
  251. package/examples/intro/assets/fonts/space-mono/LICENSE.txt +93 -0
  252. package/examples/intro/assets/fonts/space-mono/space-mono-latin-400-normal.woff2 +0 -0
  253. package/examples/intro/assets/fonts/space-mono/space-mono-latin-700-normal.woff2 +0 -0
  254. package/examples/intro/assets/grain.png +0 -0
  255. package/examples/intro/assets/score.ogg +0 -0
  256. package/examples/intro/compositions/brand.html +412 -0
  257. package/examples/intro/compositions/chrome.controller.js +63 -0
  258. package/examples/intro/compositions/chrome.html +256 -0
  259. package/examples/intro/compositions/lattice.controller.js +148 -0
  260. package/examples/intro/compositions/lattice.html +308 -0
  261. package/examples/intro/compositions/opening.html +504 -0
  262. package/examples/intro/compositions/studio.html +930 -0
  263. package/examples/intro/compositions/theme.css +218 -0
  264. package/examples/intro/fy.config.js +13 -0
  265. package/examples/intro/fy.fonts.json +100 -0
  266. package/examples/intro/scripts/make-grain.mjs +79 -0
  267. package/examples/intro/scripts/make-score.mjs +702 -0
  268. package/examples/intro/timelines/intro.timeline.json +60 -0
  269. package/examples/keyframes/README.md +76 -0
  270. package/examples/keyframes/compositions/backdrop.html +60 -0
  271. package/examples/keyframes/compositions/card.html +99 -0
  272. package/examples/keyframes/fy.config.js +10 -0
  273. package/examples/keyframes/timelines/keyframes.timeline.json +60 -0
  274. package/examples/nested/README.md +112 -0
  275. package/examples/nested/assets/pulse.wav +0 -0
  276. package/examples/nested/compositions/backdrop.html +84 -0
  277. package/examples/nested/compositions/card.html +275 -0
  278. package/examples/nested/compositions/overlay.html +307 -0
  279. package/examples/nested/fy.config.js +13 -0
  280. package/examples/nested/scripts/make-pulse.sh +20 -0
  281. package/examples/nested/timelines/chapter-a.timeline.json +39 -0
  282. package/examples/nested/timelines/chapter-b.timeline.json +39 -0
  283. package/examples/nested/timelines/nested-reel.timeline.json +52 -0
  284. package/examples/static/README.md +17 -0
  285. package/examples/static/compositions/still.html +37 -0
  286. package/examples/static/fy.config.js +9 -0
  287. package/examples/static/timelines/still.timeline.json +13 -0
  288. package/examples/transitions/README.md +128 -0
  289. package/examples/transitions/assets/alpha.wav +0 -0
  290. package/examples/transitions/assets/beta.wav +0 -0
  291. package/examples/transitions/assets/gamma.wav +0 -0
  292. package/examples/transitions/compositions/alpha.html +56 -0
  293. package/examples/transitions/compositions/beta.html +56 -0
  294. package/examples/transitions/compositions/gamma.html +56 -0
  295. package/examples/transitions/fy.config.js +12 -0
  296. package/examples/transitions/timelines/audio-crossfade.timeline.json +44 -0
  297. package/examples/transitions/timelines/transitions.timeline.json +31 -0
  298. package/examples/video/README.md +4 -4
  299. package/examples/video/compositions/video-mp4.html +349 -357
  300. package/examples/video/compositions/video-overlay.html +337 -345
  301. package/examples/video/compositions/video.html +349 -357
  302. package/examples/webgl/README.md +1 -1
  303. package/examples/webgl/compositions/webgl.html +205 -213
  304. package/package.json +13 -5
  305. package/scaffolds/init/AGENTS.md +131 -0
  306. package/scaffolds/init/README.md +56 -0
  307. package/scaffolds/init/compositions/hello.html +79 -0
  308. package/scaffolds/init/fy.config.js +9 -0
  309. package/scaffolds/init/gitignore.template +1 -0
  310. package/scaffolds/init/package.json +11 -0
  311. package/scaffolds/init/props/hello.json +4 -0
  312. package/scaffolds/init/props/main.json +6 -0
  313. package/scaffolds/init/timelines/main.timeline.json +20 -0
  314. package/scaffolds/local-models/LOCAL-MODELS.md +77 -0
  315. package/scaffolds/local-models/fy.policy.json +5 -0
  316. package/scaffolds/local-models/opencode/opencode.jsonc +17 -0
  317. package/scaffolds/local-models/pi/APPEND_SYSTEM.md +21 -0
  318. package/scaffolds/local-models/pi/sandbox.json +15 -0
  319. package/scaffolds/local-models/pi/settings.json +15 -0
  320. package/skills/frameyard/README.md +1 -0
  321. package/skills/frameyard/SKILL.md +59 -59
  322. package/skills/frameyard/references/audio-assets.md +32 -3
  323. package/skills/frameyard/references/common-mistakes.md +81 -7
  324. package/skills/frameyard/references/controllers.md +5 -4
  325. package/skills/frameyard/references/deterministic-authoring.md +49 -37
  326. package/skills/frameyard/references/diagnostics.md +42 -4
  327. package/skills/frameyard/references/failure-recovery.md +56 -10
  328. package/skills/frameyard/references/fonts.md +132 -0
  329. package/skills/frameyard/references/json-results.md +165 -10
  330. package/skills/frameyard/references/quick-reference.md +222 -0
  331. package/skills/frameyard/references/timelines.md +109 -15
  332. /package/examples/demo/{video.config.js → fy.config.js} +0 -0
  333. /package/examples/video/{video.config.js → fy.config.js} +0 -0
  334. /package/examples/webgl/{video.config.js → fy.config.js} +0 -0
@@ -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.
@@ -11,7 +11,7 @@ Every non-interactive command supports `--json`. Stdout contains one JSON docume
11
11
  }
12
12
  ```
13
13
 
14
- Additional fields are command-specific and stable within the 0.4 contract. `check`, `still`, and `render` report `elapsedMs`, the wall time of the operation from static validation and preflight through the final frame or publication; CLI argument parsing is excluded.
14
+ Additional fields are command-specific and stable within the 0.8 contract. `check`, `still`, and `render` report `elapsedMs`, the wall time of the operation from static validation and preflight through the final frame or publication; CLI argument parsing is excluded.
15
15
 
16
16
  ## Error envelope
17
17
 
@@ -51,14 +51,117 @@ Additional fields are command-specific and stable within the 0.4 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
 
158
+ Animated composition results report `kind: "animated"`. `metadata` reports the resolved format, combining declared values with the project `format`.
159
+
58
160
  ```json
59
161
  {
60
162
  "ok": true,
61
163
  "command": "check",
164
+ "kind": "animated",
62
165
  "composition": "hello",
63
166
  "entry": "compositions/hello.html",
64
167
  "controller": { "kind": "inline" },
@@ -89,7 +192,28 @@ Static validation diagnostics include source locations. Runtime diagnostics may
89
192
  }
90
193
  ```
91
194
 
92
- `entry` is the project-relative HTML path. `controller` is `{ kind: "inline" }`, `{ kind: "external", src }`, or `null`.
195
+ `entry` is the project-relative composition fragment path. `controller` is `{ kind: "inline" }`, `{ kind: "external", src }`, or `null`.
196
+
197
+ A static composition result reports `kind: "static"`, dimensions only, `framesChecked: 1`, and no audio/transcript fields:
198
+
199
+ ```json
200
+ {
201
+ "ok": true,
202
+ "command": "check",
203
+ "kind": "static",
204
+ "composition": "still",
205
+ "entry": "compositions/still.html",
206
+ "controller": null,
207
+ "metadata": { "width": 1920, "height": 1080, "static": true },
208
+ "framesChecked": 1,
209
+ "diagnostics": [],
210
+ "elapsedMs": 600
211
+ }
212
+ ```
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`.
93
217
 
94
218
  ## still
95
219
 
@@ -111,14 +235,17 @@ Static validation diagnostics include source locations. Runtime diagnostics may
111
235
  }
112
236
  ```
113
237
 
114
- `frames` is always present and follows the expanded `--frame` order. `frame` and `path` are also present only when exactly one frame was captured. `--frame` accepts a number, an inclusive range (`2-12`), or a comma-separated list; at most 32 unique frames are captured per invocation. With multiple frames, `--out` is a directory and the files are named `still-<id>-<frame>.png`.
238
+ `frames` is always present and follows the expanded `--frame` order. `frame` and `path` are also present only when exactly one frame was captured. `--frame` accepts a number, an inclusive range (`2-12`), or a comma-separated list; at most 32 unique frames are captured per invocation. With multiple frames, `--out` is a directory and the files are named `still-<id>-<frame>.png`. A static composition exposes only frame `0`; other frames fail with `FRAME_OUT_OF_RANGE`.
115
239
 
116
240
  ## render
117
241
 
242
+ Animated composition and timeline results report `kind: "video"`:
243
+
118
244
  ```json
119
245
  {
120
246
  "ok": true,
121
247
  "command": "render",
248
+ "kind": "video",
122
249
  "composition": "hello",
123
250
  "entry": "compositions/hello.html",
124
251
  "controller": { "kind": "inline" },
@@ -141,7 +268,26 @@ Static validation diagnostics include source locations. Runtime diagnostics may
141
268
  }
142
269
  ```
143
270
 
144
- A successful render means the final path exists and was published atomically. A nonzero exit means no new partial video was published. Without `--out`, the final path is `out/<composition-id>.<ext>` inside the project root, using the container extension. `concurrency` is the number of browser sessions actually used; it is capped by `durationInFrames` and by the CLI maximum of 16. `container` is `mp4`, `mov`, `mkv`, or `webm`; `codec` is `h264`, `hevc`, or `vp9`; and `encoder` is the resolved encoder (`libx264`, `libx265`, `libvpx-vp9`, `h264_nvenc`, `hevc_nvenc`, `h264_qsv`, or `hevc_qsv`). Timeline results also report the effective `workers` count after the encoder session cap, with an `ENCODER_WARNING` diagnostic when it was reduced.
271
+ A static composition result reports `kind: "image"` and has no fps, duration, frame count, codec, container, encoder, audio, or transcript fields:
272
+
273
+ ```json
274
+ {
275
+ "ok": true,
276
+ "command": "render",
277
+ "kind": "image",
278
+ "composition": "still",
279
+ "entry": "compositions/still.html",
280
+ "controller": null,
281
+ "path": "/absolute/path/out/still.png",
282
+ "width": 1920,
283
+ "height": 1080,
284
+ "bytes": 48210,
285
+ "diagnostics": [],
286
+ "elapsedMs": 700
287
+ }
288
+ ```
289
+
290
+ A successful render means the final path exists and was published atomically. A nonzero exit means no new partial output was published. Without `--out`, the final path is `out/<composition-id>.<ext>` for video and `out/<composition-id>.png` for a static composition inside the project root. `concurrency` is the number of browser sessions actually used; it is capped by `durationInFrames` and by the CLI maximum of 16, and a static composition rejects it. `container` is `mp4`, `mov`, `mkv`, or `webm`; `codec` is `h264`, `hevc`, or `vp9`; and `encoder` is the resolved encoder (`libx264`, `libx265`, `libvpx-vp9`, `h264_nvenc`, `hevc_nvenc`, `h264_qsv`, or `hevc_qsv`). Timeline results also report the effective `workers` count after the encoder session cap, with an `ENCODER_WARNING` diagnostic when it was reduced.
145
291
 
146
292
  ## tts
147
293
 
@@ -149,6 +295,7 @@ A successful render means the final path exists and was published atomically. A
149
295
  {
150
296
  "ok": true,
151
297
  "command": "tts",
298
+ "engine": "qwen",
152
299
  "path": "/absolute/path/assets/voice.wav",
153
300
  "bytes": 480000,
154
301
  "sha256": "64-character hex digest",
@@ -156,12 +303,12 @@ A successful render means the final path exists and was published atomically. A
156
303
  "channels": 1,
157
304
  "speaker": "vivian",
158
305
  "language": "English",
159
- "manifest": "/absolute/path/frameyard.assets.json",
306
+ "manifest": "/absolute/path/fy.assets.json",
160
307
  "elapsedMs": 12000
161
308
  }
162
309
  ```
163
310
 
164
- `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.
165
312
 
166
313
  ## transcribe
167
314
 
@@ -188,7 +335,7 @@ A successful render means the final path exists and was published atomically. A
188
335
  "sha256": "64-character hex digest"
189
336
  }
190
337
  ],
191
- "manifest": "/absolute/path/frameyard.assets.json",
338
+ "manifest": "/absolute/path/fy.assets.json",
192
339
  "elapsedMs": 30000
193
340
  }
194
341
  ```
@@ -197,6 +344,8 @@ A successful render means the final path exists and was published atomically. A
197
344
 
198
345
  ## doctor
199
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
+
200
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`.
201
350
 
202
351
  ```json
@@ -229,6 +378,12 @@ Doctor reports findings rather than throwing for environment problems. `ok: true
229
378
  "status": "missing",
230
379
  "requested": false,
231
380
  "referenceRevision": "..."
381
+ },
382
+ "sherpaOnnxTts": {
383
+ "name": "sherpa-onnx-offline-tts",
384
+ "status": "missing",
385
+ "requested": false,
386
+ "referenceRevision": "..."
232
387
  }
233
388
  },
234
389
  "models": [
@@ -240,8 +395,8 @@ Doctor reports findings rather than throwing for environment problems. `ok: true
240
395
  "sha256": "64-character hex digest"
241
396
  }
242
397
  ],
243
- "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 }
244
399
  }
245
400
  ```
246
401
 
247
- `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.
@@ -0,0 +1,222 @@
1
+ # Quick reference
2
+
3
+ Use this page when context is limited or when you need one canonical skeleton.
4
+ Read a topic reference only when the task needs it; the full contract is in
5
+ `SKILL.md` and the linked references.
6
+
7
+ ## Choose the smallest authoring level
8
+
9
+ 1. **Static composition** for a poster, card, or thumbnail: no `duration-in-frames`, no `fps`, renders to PNG.
10
+ 2. **CSS animation** for timed reveals, wipes, and simple motion: `@keyframes` plus `data-fy-anim-*`.
11
+ 3. **Bindings** for props-driven text, attributes, classes, conditionals, and repeats.
12
+ 4. **Controller** for Canvas 2D, WebGL, karaoke, or per-frame math CSS cannot express.
13
+ 5. **Timeline** when several compositions, media clips, or audio tracks must share one deliverable.
14
+
15
+ Do not use a controller when CSS or bindings are enough.
16
+
17
+ ## Animated composition
18
+
19
+ A composition is a partial HTML fragment, never a full document. Props, the
20
+ controller, import maps, and styles are top-level siblings of the root.
21
+
22
+ ```html
23
+ <script type="application/json" data-fy="props">{ "title": "Hello" }</script>
24
+ <style>
25
+ html, body { margin: 0; width: 100%; height: 100%; }
26
+ @keyframes rise { from { opacity: 0; } to { opacity: 1; } }
27
+ h1 { animation: rise 1s linear both; font: 8vmin system-ui; color: white; }
28
+ body { background: #101820; display: grid; place-items: center; }
29
+ </style>
30
+ <fy-composition duration-in-frames="150">
31
+ <h1 data-fy-text="title" data-fy-anim-start="0" data-fy-anim-duration="30"></h1>
32
+ </fy-composition>
33
+ ```
34
+
35
+ `width`, `height`, and `fps` are optional and inherit the project `format`.
36
+ Declare both `width` and `height` together. Never derive metadata from props.
37
+
38
+ ## Static composition
39
+
40
+ ```html
41
+ <script type="application/json" data-fy="props">{ "title": "Poster" }</script>
42
+ <style>
43
+ body { margin: 0; background: #0c1220; color: #ff7a45; display: grid; place-items: center; }
44
+ h1 { font: 8vmin system-ui; }
45
+ </style>
46
+ <fy-composition>
47
+ <h1 data-fy-text="title"></h1>
48
+ </fy-composition>
49
+ ```
50
+
51
+ A static composition has no frame clock: no `data-fy-anim-*`, no `<video>`,
52
+ `<audio>`, or `<fy-transcript>`. Its controller runs `mount()` only.
53
+
54
+ ## Fonts
55
+
56
+ Fontsource is the default catalog. Search, then install a pinned family:
57
+
58
+ ```sh
59
+ npx frameyard font search "space grotesk" --subset latin --json
60
+ npx frameyard font add space-grotesk --subset latin
61
+ ```
62
+
63
+ Variable families install the `wght` face by default. Use `--static --weight
64
+ 400,700` for static files. A local or brand font uses `font add --file`; an
65
+ explicit HTTPS source uses `font add --url` and may pass `--sha256`. A URL
66
+ import without `--license` or `--license-file` is recorded as unknown and
67
+ requires `full`.
68
+
69
+ Then use the family name normally:
70
+
71
+ ```css
72
+ h1 { font-family: "Space Grotesk", system-ui, sans-serif; font-weight: 700; }
73
+ ```
74
+
75
+ The server injects `@font-face` before author styles, uses `font-display: block`,
76
+ and the host waits for `document.fonts.ready`. Run `font verify` after moving
77
+ files. Remote font URLs produce `FONT_REMOTE`; a used face that fails to load
78
+ produces `FONT_LOAD_FAILED`. See [fonts](fonts.md).
79
+
80
+ ## Timeline
81
+
82
+ ```json
83
+ {
84
+ "version": 1,
85
+ "tracks": [
86
+ { "id": "v1", "kind": "video" },
87
+ { "id": "a1", "kind": "audio" }
88
+ ],
89
+ "clips": [
90
+ {
91
+ "id": "intro",
92
+ "track": "v1",
93
+ "source": { "kind": "composition", "id": "intro" },
94
+ "start": 0
95
+ },
96
+ {
97
+ "id": "music",
98
+ "track": "a1",
99
+ "source": { "kind": "audio", "src": "assets/music.wav" },
100
+ "start": 0,
101
+ "duration": 150,
102
+ "audio": { "gain": 0.6 }
103
+ }
104
+ ]
105
+ }
106
+ ```
107
+
108
+ `start`, `sourceIn`, and `duration` are timeline frames. Image clips and static
109
+ composition clips require an explicit `duration`; a static composition clip
110
+ needs `sourceIn: 0` and contributes no audio. Source kinds are `composition`,
111
+ `timeline`, `video`, `image`, and `audio`.
112
+
113
+ ## Bindings
114
+
115
+ - `data-fy-text="path"` sets text.
116
+ - `data-fy-attr-src="path"` sets an attribute.
117
+ - `data-fy-class-active="path"` toggles a class.
118
+ - `data-fy-if="path"` removes the element when false.
119
+ - `<template data-fy-repeat="items">` clones content; use `item` and `index`.
120
+ - Paths are dot-separated; missing paths warn and resolve empty.
121
+
122
+ Bindings apply once per load, before `mount()`. Controller-created DOM is not
123
+ re-bound.
124
+
125
+ ## Controller
126
+
127
+ ```html
128
+ <script type="text/fy-controller">
129
+ export default {
130
+ mount({ root, props }) {},
131
+ update({ frame, props }) {},
132
+ dispose() {},
133
+ };
134
+ </script>
135
+ ```
136
+
137
+ Exactly one controller is allowed, inline or external. `mount` may be async;
138
+ `update` is synchronous and seek-safe. The controller is the only executable
139
+ code. Use `random("stable:key")` from `frameyard/runtime`, never `Math.random()`.
140
+
141
+ ## Commands
142
+
143
+ | Command | Use |
144
+ | --- | --- |
145
+ | `npx frameyard context --json` | One compact project digest: format, props, timelines, assets, policy, next commands |
146
+ | `npx frameyard lint <id> --json` | Fast edit loop: static validation, scene/keyframe projection, controller determinism |
147
+ | `npx frameyard check <id> --json` | Full gate: props hooks and every animated frame through Chromium |
148
+ | `npx frameyard still <id> --frame <spec> --json` | Inspect representative frames; policy caps the count |
149
+ | `npx frameyard inspect <image> --json` | Blank, alpha, colors, content bbox, luminance grid, edge density |
150
+ | `npx frameyard compare <a> <b> --json` | Pixel and 3×3 region diff; exits 1 when different |
151
+ | `npx frameyard render <id> --dry-run --json` | Handoff plan without Chromium, FFmpeg, or a model |
152
+ | `npx frameyard render <id> --json` | Produce the video when the policy allows it |
153
+ | `npx frameyard add --list --json` | List project templates |
154
+ | `npx frameyard add composition <id> --template <name> --json` | Write a valid fragment and print the config snippet |
155
+ | `npx frameyard font search <query> --json` | Search Fontsource by name, category, subset, style, or variable |
156
+ | `npx frameyard font add <fontsource-id> --json` | Pin and vendor a Fontsource family; variable `wght` by default |
157
+ | `npx frameyard font add <fontsource-id> --static --weight 400,700 --json` | Install explicit static weights |
158
+ | `npx frameyard font add --file <path> --family <name> --json` | Vendor a local font into `assets/fonts/` and update `fy.fonts.json` |
159
+ | `npx frameyard font add --url <https-url> --family <name> --json` | Download and vendor an HTTPS font, optionally with `--sha256` |
160
+ | `npx frameyard font list --json` | Installed families, subsets, licenses, files, and status |
161
+ | `npx frameyard font verify --json` | Manifest paths, hashes, licenses, and orphaned directories |
162
+ | `npx frameyard font remove <id> --json` | Remove a font and its directory; `--keep-files` keeps the binaries |
163
+ | `npx frameyard doctor --json` | Environment, integrations, policy, fonts, and resources |
164
+
165
+ Every non-interactive command accepts `--json`. Parse stdout; read stderr for
166
+ progress and diagnostics.
167
+
168
+ ## Resource policy
169
+
170
+ `fy.policy.json` is discovered from the working directory upward,
171
+ independent of `fy.config.js`.
172
+
173
+ | Tier | `still` | `check` | `tts`/`transcribe`/`render` | overwrite |
174
+ | --- | --- | --- | --- | --- |
175
+ | `light` | 1 frame | blocked | blocked | denied |
176
+ | `authoring` | 4 frames | low priority | blocked | denied |
177
+ | `full` | 32 frames | allowed | allowed | allowed |
178
+
179
+ `lint`, `context`, `doctor`, `add`, `inspect`, `compare`, and `--dry-run` are
180
+ available in every tier. `font search`, `font list`, and `font verify` are
181
+ available in every tier; `font add` requires `authoring` or `full`; `font
182
+ remove`, `font add --force`, and `font add --url` without
183
+ `--license`/`--license-file` require `full`. When a heavy command is blocked, run it with `--dry-run`, report `deferred: true`, and give the user
184
+ `commandLine`, `prerequisites`, and `blockers`.
185
+
186
+ ## Ten diagnostics that matter
187
+
188
+ | Code | Fix |
189
+ | --- | --- |
190
+ | `COMPOSITION_DOCUMENT_SHELL` | Remove `<!doctype>`, `<html>`, `<head>`, and `<body>`; compositions are fragments |
191
+ | `COMPOSITION_FORMAT_UNRESOLVED` | Declare `width`/`height` or add project `format` |
192
+ | `COMPOSITION_INVALID` | Check the single `<fy-composition>` root and metadata |
193
+ | `CONTROLLER_INVALID` | Exactly one controller; inline XOR `src`; at least one export |
194
+ | `SCRIPT_NOT_ALLOWED` | Move executable code into the controller |
195
+ | `BINDING_PATH_MISSING` | Fix the props path; use the diagnostic suggestion |
196
+ | `BINDING_INVALID` | Unknown `data-fy-*` attribute; use a documented binding |
197
+ | `TIMELINE_CLIP_OUT_OF_RANGE` | Keyframes cannot extend a clip; lengthen the clip or move the key |
198
+ | `TIMELINE_CLIP_OVERLAP` | Declare a transition or move the clip |
199
+ | `RESOURCE_POLICY_BLOCKED` | Use `--dry-run`, or ask the user for `FRAMEYARD_POLICY=full` |
200
+
201
+ Font diagnostics: `FONT_HASH_MISMATCH` means the vendored file changed; restore
202
+ it or reinstall with `font add --force`. `FONT_LOAD_FAILED` means a used face
203
+ did not load; check the manifest and `font verify`. `FONT_REMOTE` is a warning
204
+ that the composition still depends on a remote font URL.
205
+
206
+ ## Do not
207
+
208
+ - Use `Date.now()`, `performance.now()`, timers, `requestAnimationFrame` loops, or CSS transitions as animation inputs.
209
+ - Call `play()`, `pause()`, or set `currentTime` on embedded media.
210
+ - Fetch remote data from `update()`; load during `mount()`.
211
+ - Put visual content outside `<fy-composition>`.
212
+ - Edit `fy.policy.json`, host configuration, or the skill source.
213
+ - Claim success when `check` fails, a still is missing, or a render did not run.
214
+
215
+ ## Next
216
+
217
+ - Full authoring rules: [deterministic authoring](deterministic-authoring.md)
218
+ - Fonts: [fonts](fonts.md)
219
+ - Timelines and keyframes: [timelines](timelines.md)
220
+ - Error codes: [diagnostics](diagnostics.md)
221
+ - Result shapes: [JSON results](json-results.md)
222
+ - Recovery loops: [failure recovery](failure-recovery.md)