frameyard 0.8.0 → 0.10.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 (327) hide show
  1. package/CHANGELOG.md +177 -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/fonts/LICENSE-figtree.txt +93 -0
  108. package/dist/preview/fonts/LICENSE-space-mono.txt +93 -0
  109. package/dist/preview/fonts/README.md +23 -0
  110. package/dist/preview/fonts/figtree-latin-wght-normal.woff2 +0 -0
  111. package/dist/preview/fonts/space-mono-latin-400-normal.woff2 +0 -0
  112. package/dist/preview/fonts/space-mono-latin-700-normal.woff2 +0 -0
  113. package/dist/preview/main.js +5 -2
  114. package/dist/preview/main.js.map +1 -1
  115. package/dist/preview/timeline-data.d.ts +17 -69
  116. package/dist/preview/timeline-data.d.ts.map +1 -1
  117. package/dist/preview/timeline-host.d.ts.map +1 -1
  118. package/dist/preview/timeline-host.js +70 -45
  119. package/dist/preview/timeline-host.js.map +1 -1
  120. package/dist/preview/timeline-main.d.ts.map +1 -1
  121. package/dist/preview/timeline-main.js +88 -27
  122. package/dist/preview/timeline-main.js.map +1 -1
  123. package/dist/project.d.ts +10 -3
  124. package/dist/project.d.ts.map +1 -1
  125. package/dist/project.js +20 -17
  126. package/dist/project.js.map +1 -1
  127. package/dist/renderer/audio.d.ts +6 -0
  128. package/dist/renderer/audio.d.ts.map +1 -1
  129. package/dist/renderer/audio.js +3 -1
  130. package/dist/renderer/audio.js.map +1 -1
  131. package/dist/renderer/browser.d.ts.map +1 -1
  132. package/dist/renderer/browser.js +13 -0
  133. package/dist/renderer/browser.js.map +1 -1
  134. package/dist/renderer/concurrency.d.ts +6 -0
  135. package/dist/renderer/concurrency.d.ts.map +1 -0
  136. package/dist/renderer/concurrency.js +27 -0
  137. package/dist/renderer/concurrency.js.map +1 -0
  138. package/dist/renderer/ffmpeg.d.ts +104 -5
  139. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  140. package/dist/renderer/ffmpeg.js +580 -67
  141. package/dist/renderer/ffmpeg.js.map +1 -1
  142. package/dist/renderer/index.js +1 -1
  143. package/dist/renderer/index.js.map +1 -1
  144. package/dist/renderer/keyframe-expressions.d.ts +39 -0
  145. package/dist/renderer/keyframe-expressions.d.ts.map +1 -0
  146. package/dist/renderer/keyframe-expressions.js +180 -0
  147. package/dist/renderer/keyframe-expressions.js.map +1 -0
  148. package/dist/renderer/session-pool.d.ts +28 -0
  149. package/dist/renderer/session-pool.d.ts.map +1 -0
  150. package/dist/renderer/session-pool.js +91 -0
  151. package/dist/renderer/session-pool.js.map +1 -0
  152. package/dist/renderer/timeline.d.ts +2 -0
  153. package/dist/renderer/timeline.d.ts.map +1 -1
  154. package/dist/renderer/timeline.js +600 -147
  155. package/dist/renderer/timeline.js.map +1 -1
  156. package/dist/renderer/timings.d.ts +13 -0
  157. package/dist/renderer/timings.d.ts.map +1 -0
  158. package/dist/renderer/timings.js +29 -0
  159. package/dist/renderer/timings.js.map +1 -0
  160. package/dist/runtime/host.d.ts.map +1 -1
  161. package/dist/runtime/host.js +4 -0
  162. package/dist/runtime/host.js.map +1 -1
  163. package/dist/scaffold.d.ts +3 -0
  164. package/dist/scaffold.d.ts.map +1 -1
  165. package/dist/scaffold.js +48 -320
  166. package/dist/scaffold.js.map +1 -1
  167. package/dist/server/composition-html.d.ts +2 -0
  168. package/dist/server/composition-html.d.ts.map +1 -1
  169. package/dist/server/composition-html.js +7 -0
  170. package/dist/server/composition-html.js.map +1 -1
  171. package/dist/server/preview-html.d.ts.map +1 -1
  172. package/dist/server/preview-html.js +74 -188
  173. package/dist/server/preview-html.js.map +1 -1
  174. package/dist/server/preview-style.d.ts +11 -0
  175. package/dist/server/preview-style.d.ts.map +1 -0
  176. package/dist/server/preview-style.js +298 -0
  177. package/dist/server/preview-style.js.map +1 -0
  178. package/dist/server/server.d.ts.map +1 -1
  179. package/dist/server/server.js +31 -1
  180. package/dist/server/server.js.map +1 -1
  181. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  182. package/dist/server/timeline-preview-data.js +12 -34
  183. package/dist/server/timeline-preview-data.js.map +1 -1
  184. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  185. package/dist/server/timeline-preview-html.js +140 -148
  186. package/dist/server/timeline-preview-html.js.map +1 -1
  187. package/dist/templates.d.ts +16 -0
  188. package/dist/templates.d.ts.map +1 -0
  189. package/dist/templates.js +186 -0
  190. package/dist/templates.js.map +1 -0
  191. package/dist/timeline/easing.d.ts +36 -0
  192. package/dist/timeline/easing.d.ts.map +1 -0
  193. package/dist/timeline/easing.js +225 -0
  194. package/dist/timeline/easing.js.map +1 -0
  195. package/dist/timeline/keyframes.d.ts +23 -0
  196. package/dist/timeline/keyframes.d.ts.map +1 -0
  197. package/dist/timeline/keyframes.js +95 -0
  198. package/dist/timeline/keyframes.js.map +1 -0
  199. package/dist/timeline/plan.d.ts +6 -12
  200. package/dist/timeline/plan.d.ts.map +1 -1
  201. package/dist/timeline/plan.js +17 -30
  202. package/dist/timeline/plan.js.map +1 -1
  203. package/dist/timeline/resolve.d.ts +12 -0
  204. package/dist/timeline/resolve.d.ts.map +1 -1
  205. package/dist/timeline/resolve.js +105 -9
  206. package/dist/timeline/resolve.js.map +1 -1
  207. package/dist/timeline/scene.d.ts +126 -0
  208. package/dist/timeline/scene.d.ts.map +1 -0
  209. package/dist/timeline/scene.js +109 -0
  210. package/dist/timeline/scene.js.map +1 -0
  211. package/dist/timeline/schema.d.ts +31 -0
  212. package/dist/timeline/schema.d.ts.map +1 -1
  213. package/dist/timeline/schema.js +240 -9
  214. package/dist/timeline/schema.js.map +1 -1
  215. package/dist/timeline/transitions.d.ts +21 -0
  216. package/dist/timeline/transitions.d.ts.map +1 -0
  217. package/dist/timeline/transitions.js +23 -0
  218. package/dist/timeline/transitions.js.map +1 -0
  219. package/dist/version.d.ts +1 -1
  220. package/dist/version.d.ts.map +1 -1
  221. package/dist/version.js +1 -1
  222. package/dist/version.js.map +1 -1
  223. package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
  224. package/docs/adr/0010-make-timelines-the-render-unit.md +3 -2
  225. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  226. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +2 -1
  227. package/docs/adr/0015-inherit-composition-format-from-the-project.md +2 -2
  228. package/docs/adr/0016-author-compositions-as-html-fragments.md +1 -1
  229. package/docs/adr/0018-animate-timeline-clips-with-keyframes.md +35 -0
  230. package/docs/adr/0019-make-same-track-overlap-the-transition-window.md +36 -0
  231. package/docs/adr/0020-crossfade-audio-clips-on-audio-tracks.md +27 -0
  232. package/docs/adr/0021-project-timeline-scenes-once.md +32 -0
  233. package/docs/adr/0022-gate-heavy-operations-with-a-workspace-resource-policy.md +35 -0
  234. package/docs/adr/0023-pin-fonts-as-project-assets.md +98 -0
  235. package/docs/adr/0024-stream-composition-captures-and-skip-trivial-composites.md +86 -0
  236. package/docs/adr/0025-capture-timeline-layers-in-parallel-with-a-global-limit.md +67 -0
  237. package/docs/adr/0026-reuse-loaded-composition-sessions-across-segments.md +70 -0
  238. package/docs/adr/0027-encode-two-full-frame-layers-through-dual-pipes.md +76 -0
  239. package/docs/adr/0028-encode-full-frame-video-segments-directly.md +65 -0
  240. package/docs/migration-0.4.md +2 -2
  241. package/docs/preview-testing.md +27 -6
  242. package/docs/{v0.1-design.md → v0.x-design.md} +57 -32
  243. package/examples/blend/README.md +3 -3
  244. package/examples/demo/README.md +2 -2
  245. package/examples/intro/README.md +71 -0
  246. package/examples/intro/assets/fonts/figtree/LICENSE.txt +93 -0
  247. package/examples/intro/assets/fonts/figtree/figtree-latin-wght-normal.woff2 +0 -0
  248. package/examples/intro/assets/fonts/instrument-serif/LICENSE.txt +93 -0
  249. package/examples/intro/assets/fonts/instrument-serif/instrument-serif-latin-400-italic.woff2 +0 -0
  250. package/examples/intro/assets/fonts/space-mono/LICENSE.txt +93 -0
  251. package/examples/intro/assets/fonts/space-mono/space-mono-latin-400-normal.woff2 +0 -0
  252. package/examples/intro/assets/fonts/space-mono/space-mono-latin-700-normal.woff2 +0 -0
  253. package/examples/intro/assets/grain.png +0 -0
  254. package/examples/intro/assets/score.ogg +0 -0
  255. package/examples/intro/compositions/brand.html +412 -0
  256. package/examples/intro/compositions/chrome.controller.js +63 -0
  257. package/examples/intro/compositions/chrome.html +256 -0
  258. package/examples/intro/compositions/lattice.controller.js +148 -0
  259. package/examples/intro/compositions/lattice.html +308 -0
  260. package/examples/intro/compositions/opening.html +504 -0
  261. package/examples/intro/compositions/studio.html +930 -0
  262. package/examples/intro/compositions/theme.css +218 -0
  263. package/examples/intro/fy.config.js +13 -0
  264. package/examples/intro/fy.fonts.json +100 -0
  265. package/examples/intro/scripts/make-grain.mjs +79 -0
  266. package/examples/intro/scripts/make-score.mjs +702 -0
  267. package/examples/intro/timelines/intro.timeline.json +60 -0
  268. package/examples/keyframes/README.md +76 -0
  269. package/examples/keyframes/compositions/backdrop.html +60 -0
  270. package/examples/keyframes/compositions/card.html +99 -0
  271. package/examples/keyframes/fy.config.js +10 -0
  272. package/examples/keyframes/timelines/keyframes.timeline.json +60 -0
  273. package/examples/nested/README.md +112 -0
  274. package/examples/nested/assets/pulse.wav +0 -0
  275. package/examples/nested/compositions/backdrop.html +84 -0
  276. package/examples/nested/compositions/card.html +275 -0
  277. package/examples/nested/compositions/overlay.html +307 -0
  278. package/examples/nested/fy.config.js +13 -0
  279. package/examples/nested/scripts/make-pulse.sh +20 -0
  280. package/examples/nested/timelines/chapter-a.timeline.json +39 -0
  281. package/examples/nested/timelines/chapter-b.timeline.json +39 -0
  282. package/examples/nested/timelines/nested-reel.timeline.json +52 -0
  283. package/examples/static/README.md +2 -2
  284. package/examples/transitions/README.md +128 -0
  285. package/examples/transitions/assets/alpha.wav +0 -0
  286. package/examples/transitions/assets/beta.wav +0 -0
  287. package/examples/transitions/assets/gamma.wav +0 -0
  288. package/examples/transitions/compositions/alpha.html +56 -0
  289. package/examples/transitions/compositions/beta.html +56 -0
  290. package/examples/transitions/compositions/gamma.html +56 -0
  291. package/examples/transitions/fy.config.js +12 -0
  292. package/examples/transitions/timelines/audio-crossfade.timeline.json +44 -0
  293. package/examples/transitions/timelines/transitions.timeline.json +31 -0
  294. package/examples/video/README.md +4 -4
  295. package/examples/webgl/README.md +1 -1
  296. package/package.json +13 -6
  297. package/scaffolds/init/AGENTS.md +131 -0
  298. package/scaffolds/init/README.md +56 -0
  299. package/scaffolds/init/compositions/hello.html +79 -0
  300. package/scaffolds/init/fy.config.js +9 -0
  301. package/scaffolds/init/gitignore.template +1 -0
  302. package/scaffolds/init/package.json +11 -0
  303. package/scaffolds/init/props/hello.json +4 -0
  304. package/scaffolds/init/props/main.json +6 -0
  305. package/scaffolds/init/timelines/main.timeline.json +20 -0
  306. package/scaffolds/local-models/LOCAL-MODELS.md +77 -0
  307. package/scaffolds/local-models/fy.policy.json +5 -0
  308. package/scaffolds/local-models/opencode/opencode.jsonc +17 -0
  309. package/scaffolds/local-models/pi/APPEND_SYSTEM.md +21 -0
  310. package/scaffolds/local-models/pi/sandbox.json +15 -0
  311. package/scaffolds/local-models/pi/settings.json +15 -0
  312. package/skills/frameyard/README.md +1 -0
  313. package/skills/frameyard/SKILL.md +59 -61
  314. package/skills/frameyard/references/audio-assets.md +32 -3
  315. package/skills/frameyard/references/common-mistakes.md +53 -2
  316. package/skills/frameyard/references/deterministic-authoring.md +10 -3
  317. package/skills/frameyard/references/diagnostics.md +31 -1
  318. package/skills/frameyard/references/failure-recovery.md +32 -6
  319. package/skills/frameyard/references/fonts.md +132 -0
  320. package/skills/frameyard/references/json-results.md +119 -6
  321. package/skills/frameyard/references/quick-reference.md +222 -0
  322. package/skills/frameyard/references/timelines.md +97 -7
  323. /package/examples/blend/{video.config.js → fy.config.js} +0 -0
  324. /package/examples/demo/{video.config.js → fy.config.js} +0 -0
  325. /package/examples/static/{video.config.js → fy.config.js} +0 -0
  326. /package/examples/video/{video.config.js → fy.config.js} +0 -0
  327. /package/examples/webgl/{video.config.js → fy.config.js} +0 -0
@@ -0,0 +1,70 @@
1
+ # Reuse loaded composition sessions across segments
2
+
3
+ **Status:** Accepted. Extends [ADR-0025](./0025-capture-timeline-layers-in-parallel-with-a-global-limit.md). Extended by [ADR-0027](./0027-encode-two-full-frame-layers-through-dual-pipes.md): two full-frame layers encode through dual image pipes.
4
+ **Date:** 2026-11-06
5
+
6
+ The timeline renderer keeps a bounded pool of loaded composition sessions and
7
+ leases them to segment captures. A session is keyed by composition entry, props,
8
+ and transparent-background mode, and is reused across segments and clips that
9
+ share those inputs. Sessions are discarded on a capture error and all sessions
10
+ are closed when the render ends.
11
+
12
+ ## Context
13
+
14
+ Every composition layer of every segment opened a new Chromium browser, loaded
15
+ the composition, waited for `networkidle` (roughly 450 ms), and closed it. The
16
+ one-layer 60-minute gate opened 360 sessions; a two-layer workload opens 720.
17
+ The 10-minute one-layer gate without a pool took 211.8 s, and its session stage
18
+ was a repeated fixed cost rather than frame work.
19
+
20
+ The host already has the right reuse semantics: `load` restores the pristine DOM
21
+ and mounts the controller once, while `seek` is required to be stateless. A
22
+ loaded session with the same props can seek to any frame of any segment.
23
+
24
+ ## Decision
25
+
26
+ - `CompositionSessionPool` stores idle sessions in a map keyed by
27
+ `entry + props + transparentBackground`.
28
+ - `maxIdle` is `max(2, workers)`. Active sessions are already bounded by the
29
+ capture semaphore; the idle cap bounds memory for workloads with many unique
30
+ compositions.
31
+ - `captureCompositionFrames` leases a session instead of opening one directly.
32
+ A successful capture releases the lease; a capture, diagnostics, or encoder
33
+ error discards it.
34
+ - Nested timelines share the render's pool. Static compositions keep their
35
+ one-shot capture and cache because they are captured once per props variant.
36
+ - The render closes the pool before the preview server in its cleanup path.
37
+ - The session timing event records `reused: true` when the lease came from the
38
+ pool.
39
+
40
+ ## Considered Options
41
+
42
+ - **Remove `networkidle` only.** Rejected as the primary change: it removes a
43
+ fixed wait but does not avoid browser launch, navigation, and host load. It
44
+ also weakens the asset-loading boundary for compositions with stylesheet
45
+ background images. Session reuse amortizes the whole startup cost instead.
46
+ - **Share one browser process without reusing pages.** Rejected: it saves the
47
+ browser launch but still pays navigation, host load, and `networkidle` per
48
+ segment.
49
+ - **Per-worker pools.** Rejected: a shared pool lets any worker reuse an idle
50
+ session and keeps the idle budget independent of worker count.
51
+ - **Unbounded idle caching.** Rejected: compositions with many unique props or
52
+ entries could retain an unbounded number of pages and media elements.
53
+
54
+ ## Consequences
55
+
56
+ - The 10-minute one-layer gate fell from 211.8 s to 192.6 s (−9.0%) with the
57
+ same 8 workers and byte-identical output. The full 60-minute gate fell from
58
+ 19.83 to 18.78 minutes (−5.3%) and opened eight sessions for 360 segments
59
+ instead of 360 sessions. Its output is byte-identical to the baseline.
60
+ - The two-layer 60-second gate did not show a clear gain (43.78 s to 45.43 s):
61
+ six segments are too few for reuse to pay back the pool's initial opens. The
62
+ long-form workload is the target.
63
+ - Reused sessions return accumulated diagnostics; the final merge continues to
64
+ deduplicate them. A page error discards the session so a broken render cannot
65
+ poison later segments.
66
+ - The pool is internal to a render. There is no public CLI or JSON contract
67
+ change.
68
+ - Tests cover same-key reuse, discard, idle-budget eviction, close, and the
69
+ existing timeline suite with embedded media, controllers, blends, keyframes,
70
+ transitions, and nested timelines.
@@ -0,0 +1,76 @@
1
+ # Encode a full-frame composition and a static overlay through dual pipes
2
+
3
+ **Status:** Accepted. Extends [ADR-0026](./0026-reuse-loaded-composition-sessions-across-segments.md).
4
+ **Date:** 2026-11-06
5
+
6
+ A segment with a full-frame composition base and one static composition overlay
7
+ now captures both layers concurrently and feeds them to one FFmpeg process
8
+ through two image pipes. The base has an identity transform; the overlay may use
9
+ a static fit, box, and position, but no keyframes, blend, transition, or
10
+ opacity. The process composites the background, base, and overlay and encodes
11
+ the delivery segment directly. The FFV1 intermediates and the separate
12
+ composite pass are skipped.
13
+
14
+ ## Context
15
+
16
+ The two-layer 60-second PIP gate takes 43.78 s; the overlay is a 640×360
17
+ composition on a second track. A two-full-frame-layer workload (1920×1080 base
18
+ plus a 1920×1080 transparent overlay) took 53.27 s with the normal FFV1 and
19
+ composite path. Both captures already ran concurrently after ADR 0025, but the
20
+ segment still paid for two FFV1 encodes, two decodes, and a separate composite
21
+ encode.
22
+
23
+ FFmpeg can read a second image stream from `pipe:3`, and Node can provide that
24
+ extra stdio pipe on Linux and macOS. The two captures write to independent
25
+ pipes, so neither blocks the other while FFmpeg overlays frames in order.
26
+
27
+ ## Decision
28
+
29
+ - `startDualPipeSegmentEncoder` spawns one FFmpeg process with:
30
+ - `pipe:0` for the base composition;
31
+ - `pipe:3` for the overlay composition;
32
+ - a `lavfi color` input for the timeline background;
33
+ - `[2:v][0:v]overlay`, then `[base][1:v]overlay`, then the delivery encoder.
34
+ - `captureTwoLayerDirectComposition` leases two capture sessions, writes each
35
+ layer to its pipe, and waits for both frame counts before finishing.
36
+ - The fast path requires exactly two composition layers: a base that matches the
37
+ stage with an identity transform, and an overlay with no keyframes, blend,
38
+ transition, opacity, scale, or rotation. The overlay may use a static fit,
39
+ explicit box, and position. An opaque timeline background is required.
40
+ - Windows falls back to the FFV1 and composite path because extra stdio pipe
41
+ descriptors are not portable there.
42
+ - `FRAMEYARD_DISABLE_DUAL_PIPE=1` forces the fallback for A/B measurements.
43
+ - One-layer direct segments keep `startDirectSegmentEncoder`; PIP, transformed,
44
+ blended, keyframed, nested, static, and media layers keep the segmented
45
+ lossless path.
46
+
47
+ ## Considered Options
48
+
49
+ - **General multi-pipe compositor for any layer count.** Rejected for now:
50
+ extra pipes multiply the coordination surface and make Windows support
51
+ harder. Two full-frame layers cover the measured multi-layer case.
52
+ - **Interleave two captures into one pipe.** Rejected: the filter graph would
53
+ need frame-splitting metadata and the capture loops would have to lockstep.
54
+ - **Buffer the base layer and stream only the overlay.** Rejected: buffering a
55
+ 300-frame 1080p layer costs hundreds of megabytes and adds a copy.
56
+ - **Keep FFV1 and tune the composite.** Measured filters and thread knobs gave
57
+ no meaningful gain; the intermediate round trip is the cost.
58
+
59
+ ## Consequences
60
+
61
+ - The two-full-frame-layer 60-second gate fell from 53.27 s to 34.62 s (−35.0%)
62
+ with byte-identical output. The full 60-minute two-layer gate completed in
63
+ 29.69 minutes with eight workers, 108,000 exact frames, and clean scratch,
64
+ just inside the 30-minute target.
65
+ - The two-layer PIP 60-second gate fell from 43.78 s to 33.90 s (−22.6%); sampled
66
+ decoded frames are pixel-identical to the FFV1-and-composite path. The full
67
+ PIP 60-minute gate completed in 25.75 minutes with eight workers, 108,000
68
+ exact frames, and clean scratch.
69
+ - The fast path writes no FFV1 files and no PNG sequences for qualifying
70
+ segments.
71
+ - A dual-pipe failure aborts both FFmpeg inputs and discards both sessions;
72
+ frame counts are checked per layer before publication.
73
+ - Windows and non-qualifying segments keep the existing deterministic path, so
74
+ the capture-pixel guarantee is unchanged.
75
+ - Tests cover two full-frame compositions with a transparent overlay over a
76
+ non-black background, plus the full timeline rendering suite.
@@ -0,0 +1,65 @@
1
+ # Encode full-frame video segments directly
2
+
3
+ **Status:** Accepted. Extends [ADR-0027](./0027-encode-two-full-frame-layers-through-dual-pipes.md).
4
+ **Date:** 2026-11-06
5
+
6
+ A segment with one full-frame video layer, an identity transform, and an opaque
7
+ timeline background now decodes and re-encodes the source range directly into
8
+ the delivery segment. It skips the background overlay, fit, presentation, and
9
+ composite filter graph, but keeps the same delivery encoder and container as
10
+ every other segment.
11
+
12
+ ## Context
13
+
14
+ The representative 60-minute 50/50 gate contains 24 seconds per minute of
15
+ full-frame video on a single track. The composite path still built a color
16
+ background, overlaid the video, normalized timestamps, fitted the frame, and
17
+ then encoded. The video source already matches the stage, so those filters are
18
+ identity work except for the fps/format normalization.
19
+
20
+ Stream-copy was considered and deferred: it requires keyframe alignment, matching
21
+ codec parameters, and a concat-compatible bitstream across composition and media
22
+ segments. A direct re-encode avoids those risks while removing the composite
23
+ overhead.
24
+
25
+ ## Decision
26
+
27
+ - `encodeVideoSegmentDirect` seeks to the clip's source start, maps the first
28
+ video stream, drops audio, applies
29
+ `fps=<project fps>,format=rgba,format=yuv420p`, and encodes with the normal
30
+ delivery encoder/container settings. The RGBA round trip preserves the
31
+ composite path's color conversion and keeps decoded pixels identical.
32
+ - The fast path requires one visual layer whose source is a video matching the
33
+ stage dimensions, an identity transform, and no keyframes, blend, transition,
34
+ or opacity. The timeline background must be opaque.
35
+ - `FRAMEYARD_DISABLE_VIDEO_DIRECT=1` forces the composite path for A/B
36
+ measurements.
37
+ - Video with transforms, opacity, overlays, keyframes, transitions, or a
38
+ non-matching source/stage size keeps the composite path. Composition and
39
+ nested-timeline segments are unaffected.
40
+
41
+ ## Considered Options
42
+
43
+ - **Stream-copy the video range.** Deferred: keyframe alignment and mixed-codec
44
+ concat compatibility need a larger probe and validation surface. A direct
45
+ re-encode reaches the target without that risk.
46
+ - **Keep the composite graph.** Rejected for this case: the background, fit, and
47
+ presentation filters are identity work for a full-frame video.
48
+ - **Use the dual-pipe path.** Not applicable: there is only one visual layer.
49
+
50
+ ## Consequences
51
+
52
+ - Full-frame video segments still re-encode, so every delivered segment shares
53
+ the same codec, profile, and container settings. The conformance risk is lower
54
+ than stream-copy.
55
+ - The representative 60-minute 50/50 gate drops the composite path for its
56
+ single-video seconds while retaining it for overlays, PIP, images, and
57
+ multi-layer segments.
58
+ - A 60-second full-frame video-only gate fell from 10.65 s to 7.61 s (−28.5%)
59
+ with pixel-identical decoded frames.
60
+ - The representative 60-minute 50/50 gate fell from 16.82 to 15.57 minutes
61
+ (−7.4%) with 180 direct video segments, exact 108,000 frames, clean scratch,
62
+ and a byte-identical output file.
63
+ - Tests cover a full-frame video timeline through the direct path and the
64
+ existing media/blend/keyframe/timeline suites.
65
+ - The fast path is internal; there is no CLI or JSON contract change.
@@ -259,11 +259,11 @@ export default {
259
259
 
260
260
  ## 11. Update preview and render expectations
261
261
 
262
- No command changes. Preview loads the composition in a same-origin iframe; `check`, `still`, and `render` navigate directly to the same document. The composition host is injected by the server, so keep composition entries listed in `video.config.js` and run through `frameyard dev` or `frameyard render`, not `file://`.
262
+ No command changes. Preview loads the composition in a same-origin iframe; `check`, `still`, and `render` navigate directly to the same document. The composition host is injected by the server, so keep composition entries listed in `fy.config.js` and run through `frameyard dev` or `frameyard render`, not `file://`.
263
263
 
264
264
  ## Migration checklist
265
265
 
266
- - [ ] Change `video.config.js` entries to `.html`.
266
+ - [ ] Change `fy.config.js` entries to `.html`.
267
267
  - [ ] Move metadata to `<fy-composition>`.
268
268
  - [ ] Move `defaultProps` to the `data-fy="props"` JSON block.
269
269
  - [ ] Move `mount()` markup into the document.
@@ -25,9 +25,28 @@ expose the same seek/settle contract the renderer uses.
25
25
  - Order: `await ready`, `await load()`, `await seek(frame)`, `await settle()`.
26
26
  - The non-embedded page owns transport and audio; only the embedded host exposes
27
27
  `window.__fyTimeline`.
28
- - A static composition source carries `static: true` and no
29
- `durationInFrames`. The host loads the layer once and skips per-frame seek;
30
- the layer remains visible while the timeline playhead moves.
28
+ - A static composition source carries `metadata.static: true` and no
29
+ `metadata.durationInFrames`. The host loads the layer once and skips per-frame
30
+ seek; the layer remains visible while the timeline playhead moves.
31
+ - The embedded host projects the serialized resolved timeline with the shared
32
+ scene module on every seek. Do not add a preview-only clip or transition
33
+ model; give the scene module what it needs instead.
34
+ - A keyframed clip updates the layer wrapper's inline `opacity` and `transform`
35
+ on every seek. Assert those styles or the rendered stage after `settle()`.
36
+ Markers are `.keyframe-marker` elements with a `data-frame` absolute timeline
37
+ frame and a channel/value tooltip; clicking one seeks the playhead.
38
+ - Inside a transition window both clips are active. The incoming layer's inline
39
+ `opacity` is `clipOpacity × eased progress` and it stacks above the outgoing
40
+ layer on the same track. The strip draws a `.transition` band with a
41
+ `from → to` tooltip; it is read-only.
42
+ - The strip opens with a `.ruler-scale` carrying five `.ruler-tick` labels at
43
+ 0/25/50/75/100% of the frame range. One `.playhead` line spans the ruler and
44
+ every track, driven by a `--playhead-factor` custom property on
45
+ `#timeline-inner`; `--progress` on `#scrub` fills the transport rail.
46
+ - Both preview pages share the chrome in `src/server/preview-style.ts` and its
47
+ design tokens. The chrome serves its own Figtree and Space Mono from
48
+ `/__frameyard/framework/preview/fonts/` and does not link the project font
49
+ sheet. The embedded host page has none of this chrome.
31
50
 
32
51
  ## Waiting for a frame
33
52
 
@@ -51,9 +70,11 @@ the renderer, or the CLI, and before pushing or publishing.
51
70
 
52
71
  ## Notes
53
72
 
54
- - Preview iframes and the composition stage pin `color-scheme: light` to match
55
- the production capture context. A dark inherited scheme makes Chromium paint a
56
- white base behind transparent compositions.
73
+ - Timeline layer iframes pin `color-scheme: light` on both the iframe element
74
+ and the embedded document root to match the production capture context.
75
+ The iframe element's `color-scheme` does not reach the embedded document, so a
76
+ composition that declares a dark scheme would otherwise paint an opaque canvas
77
+ base and hide every layer beneath it.
57
78
  - A running `dev` server keeps the `dist/` modules it loaded. Restart it after
58
79
  `pnpm build`.
59
80
  - The timeline preview host is not the production compositor; compare pixels at
@@ -2,9 +2,9 @@
2
2
 
3
3
  **Status:** Accepted
4
4
  **Initial acceptance:** 2026-10-27
5
- **Last design revision:** 2026-11-03
5
+ **Last design revision:** 2026-11-04
6
6
 
7
- This document records the decisions confirmed during design sessions. It is the living product and architecture baseline; later changes revise this document explicitly rather than silently broaden the scope. The original v0.1 baseline has been revised through 0.8 and extended with static clip blend modes. Architecture decisions that changed the baseline are recorded in `docs/adr/`.
7
+ This document records the decisions confirmed during design sessions. It is the living product and architecture baseline; later changes revise this document explicitly rather than silently broaden the scope. Architecture decisions that changed the baseline are recorded in `docs/adr/`.
8
8
 
9
9
  ## 1. Product contract
10
10
 
@@ -31,7 +31,7 @@ The agent-enabled success criterion additionally requires an agent using the off
31
31
  ### Project
32
32
 
33
33
  - Projects use ordinary HTML, CSS, static assets, and native JavaScript controllers, either inline in a composition fragment or in external modules.
34
- - `video.config.js` explicitly maps project IDs to source files. There is no directory scanning. It has three optional maps:
34
+ - `fy.config.js` explicitly maps project IDs to source files. There is no directory scanning. It has three optional maps:
35
35
  - `format`: the project's fixed `width`, `height`, and `fps`.
36
36
  - `compositions`: IDs mapped to `.html` composition fragments.
37
37
  - `timelines`: IDs mapped to JSON timeline documents.
@@ -55,7 +55,7 @@ The agent-enabled success criterion additionally requires an agent using the off
55
55
 
56
56
  - A timeline is a versioned JSON document. It references compositions and nested timelines by project ID and media clips by project-relative path.
57
57
  - A timeline has a project-fixed format, an ordered list of tracks, and a list of clips. It may declare a `background` color and an optional `durationInFrames`.
58
- - Tracks are `video` or `audio`. Video tracks composite from bottom to top; audio tracks mix. Each clip belongs to exactly one track. Clips on the same track cannot overlap; clips on different tracks can. Touching endpoints are allowed. Transitions are deferred, so overlap within a track would have ambiguous precedence.
58
+ - Tracks are `video` or `audio`. Video tracks composite from bottom to top; audio tracks mix. Each clip belongs to exactly one track. Clips on the same track cannot overlap unless a timeline-level `transitions` entry explicitly pairs them; clips on different tracks can. Touching endpoints are allowed but are not a transition. A transition's window is the overlap, and its declared geometry is `from.start < to.start < from.end < to.end`. See [ADR 0019](./adr/0019-make-same-track-overlap-the-transition-window.md).
59
59
  - A clip has an ID, a track, a source, a timeline start, a source in-point, a duration, and clip-specific presentation settings. Source in-point, duration, and timeline start are integer project frames.
60
60
  - Clip sources are:
61
61
  - a composition, identified by its project ID;
@@ -64,12 +64,14 @@ The agent-enabled success criterion additionally requires an agent using the off
64
64
  - A composition clip fixes props for that instance and may trim the composition's source range. The same composition may appear multiple times with different props, source ranges, transforms, and audio settings. A clip whose composition source is static behaves like an image: it must declare `duration`, must use `sourceIn: 0`, has no intrinsic source duration, and contributes no audio or transcripts. The renderer captures one transparent PNG per composition/props pair and reuses it across segments.
65
65
  - A nested timeline clip is a visual and audio source. It uses the project format, trims with the same source in-point and duration, contributes its audio, and is rendered as a group so parent opacity, transform, and blend mode apply to the whole nested result. A blend mode inside a nested timeline sees only that timeline's own backdrop; a transparent nested timeline does not see the parent's layers, and a declared nested background is part of the group. Cycles are rejected statically. Timeline clips do not override props of compositions inside the nested timeline.
66
66
  - Media clips use local project files only. Video and audio durations, dimensions, and audio presence are probed before frame production. Images and static composition clips require an explicit duration. A video or audio clip that exceeds its source range is an error; an omitted duration uses the remaining source.
67
- - Visual clips support a static transform: fit policy (`contain`, `cover`, `stretch`), an optional explicit `width`/`height` box (both fields together, mutually exclusive with `scale`) or a `scale` multiplier, pixel offset, rotation, and opacity. `fit` applies inside the box; without one, the project frame is the box.
68
- - Visual clips may declare a static `blend` mode. It uses CSS `mix-blend-mode` semantics against the accumulated backdrop of all lower layers and the timeline background, respects the clip's source alpha, and defaults to `normal`. The supported modes are `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, and `exclusion`; `hue`, `saturation`, `color`, and `luminosity` are rejected. There are no keyframes, masks, filters, or other effects.
67
+ - Visual clips support a static transform: fit policy (`contain`, `cover`, `stretch`), an optional explicit `width`/`height` box (both fields together, mutually exclusive with `scale`) or a `scale` multiplier, pixel offset, rotation, and opacity. `fit` applies inside the box; without one, the project frame is the box. Rotation expands to the full rotated bounds rather than clipping corners.
68
+ - Visual clips may animate `x`, `y`, `scale`, `rotation`, and `opacity` with a per-channel `keyframes` object. Keys use clip-local integer frames; `0` is the clip start boundary and `duration` the end boundary. Keys hold before the first and after the last key, never extend the clip, and use a CSS timing function on the interval after each key. A keyed channel cannot also be an explicit scalar in `transform`. `opacity` and `scale` are clamped after easing; position and rotation may overshoot. `keyframes.scale` is mutually exclusive with an explicit `width`/`height` box. Timeline document version stays 1.
69
+ - A timeline may declare `transitions`: `{ from, to, kind, easing? }`, where `kind` is `dissolve` on a video track or `crossfade` on an audio track, and `easing` uses the keyframe CSS timing-function vocabulary. Both clips stay on one track and overlap; the window is `to.start .. from.end`. A dissolve composites the incoming clip above the outgoing clip with alpha `clipOpacity × eased progress`; both kinds apply equal-power audio gain (`sqrt(1 - t)` and `sqrt(t)`) to contributed audio or to the two audio clips. An overlap without a declared transition, a non-overlapping pair, reversed or contained geometry, cross-track pairs, a kind that does not match the track, duplicate participation, and unknown kinds are errors. Timeline document version stays 1.
70
+ - Visual clips may declare a static `blend` mode. It uses CSS `mix-blend-mode` semantics against the accumulated backdrop of all lower layers and the timeline background, respects the clip's source alpha, and defaults to `normal`. The supported modes are `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, and `exclusion`; `hue`, `saturation`, `color`, and `luminosity` are rejected. There are no animated blend modes, masks, filters, or other effects.
69
71
  - Visual clips contribute their audio to the timeline mix unless muted: composition audio, embedded video audio, and nested timeline audio. Static compositions contribute no audio; clip audio settings on a static composition are ignored with a warning. Audio-only clips live on audio tracks and may source audio or video files. Per-clip and per-audio-track gain and mute are supported. There are no linked A/V pairs; a J/L cut is expressed by muting a video clip and placing a separate audio clip with a different source in-point.
70
72
  - A timeline's duration is derived from the latest clip end. An explicit `durationInFrames` may extend it but not cut clips. A timeline with no clips is invalid; an audio-only timeline is valid and renders black video with audio.
71
73
  - A timeline background is transparent by default. Blend modes see the accumulated backdrop including this background. The final video composites it over black; a nested timeline remains transparent so lower layers can show through. A composition that paints no background is transparent and may be used as an overlay.
72
- - The preview provides a read-only timeline view: tracks, clips, playhead, program monitor, and diagnostics. It is not a visual editor.
74
+ - The preview provides a read-only timeline view: tracks, clips, keyframe markers, playhead, program monitor, and diagnostics. It is not a visual editor.
73
75
 
74
76
  ## 3. Deterministic execution
75
77
 
@@ -81,10 +83,10 @@ The timeline frame is the master clock. A composition's frame is derived from th
81
83
  - JavaScript animation runs in the controller's `update()`, which must be synchronous and seek-safe. It cannot depend on previously visited frames.
82
84
  - Declarative bindings apply once per load, before the controller mounts. Controller-created DOM is the controller's responsibility.
83
85
  - Independent `requestAnimationFrame` loops, timers, CSS transitions, wall-clock reads, and unseeded randomness are not supported animation drivers.
84
- - Timeline clips use static transforms and static blend modes. Timeline-level animation, transitions, and other effects are not supported.
86
+ - Timeline clip keyframes animate `x`, `y`, `scale`, `rotation`, and `opacity` from the integer clip-local frame. They are evaluated against absolute timeline frames, so segment boundaries do not change values. Preview evaluates the exact CSS timing function; production generates FFmpeg expressions and approximates the curve. Blend modes remain static. Transition progress is likewise evaluated from the absolute timeline frame; only the incoming clip carries transition alpha during a dissolve, and both kinds use the same eased progress for equal-power audio gain.
85
87
  - Preview and production seek composition clips explicitly. The timeline compositor is deterministic given the captured clip frames, media frames, clip settings, and segment boundaries.
86
88
  - The resolved format—declared width, height, and fps combined with the project format—is part of the immutable frame environment, alongside props and loaded assets. Layouts that use viewport units or percentages therefore depend on the resolved format by design; preview and production always resolve the same values.
87
- - Assets, remote data, images, fonts, generated audio, transcripts, and media are resolved before frame production. For a segmented timeline render, each segment's composition and nested-timeline clips are loaded and settled before that segment's frames are captured; no frame production depends on a new network request. Network is blocked during capture.
89
+ - Assets, remote data, images, fonts, generated audio, transcripts, and media are resolved before frame production. Project fonts are vendored locally, recorded in `fy.fonts.json`, and injected as `@font-face` rules; the host waits for `document.fonts.ready` and fails a used face that reports an error. For a segmented timeline render, each segment's composition and nested-timeline clips are loaded and settled before that segment's frames are captured; no frame production depends on a new network request. Network is blocked during capture.
88
90
  - AI model inference is never part of the loading phase, controller lifecycle, or frame production.
89
91
  - The baseline supports DOM, CSS, SVG, Canvas 2D, WebGL, and frame-seeked embedded video. Canvas 2D and WebGL are drawn from the current frame by the controller.
90
92
  - Embedded video and audio inside compositions are declared with `data-fy-*` attributes. The runtime loads local files, pauses them, and seeks video from the frame-derived source time before capture. An unmuted video contributes its audio track to the timeline mix unless the composition opts out or the clip mutes it.
@@ -100,12 +102,12 @@ Preview and production share composition behavior and capture composition frames
100
102
 
101
103
  ## 4. Preview and production rendering
102
104
 
103
- Preview and production execute the same composition sources and host runtime. They differ in which component drives the frame clock, which compositor combines timeline layers, and whether frames are captured.
105
+ Preview and production execute the same composition sources and host runtime. They differ in which component drives the frame clock, which compositor combines timeline layers, and whether frames are captured. Both also project the same timeline scene from the resolved timeline: the ordered active clips with their transition roles, composite order, frame-to-source mapping, capture modes, and presentation defaults. The scene projection is browser-safe; capture sessions, audio mixing, and both compositors stay separate.
104
106
 
105
107
  The preview provides:
106
108
 
107
109
  - Composition preview: composition selection, JSON props editing, play/pause, frame-accurate scrubbing, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, current frame and time, audio controls, and diagnostics. A static composition loads once and disables the transport.
108
- - Timeline preview: timeline selection, a read-only track/clip view, playhead, program monitor, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, audio controls, and diagnostics. A static composition layer stays loaded and is not seeked per frame.
110
+ - Timeline preview: timeline selection, a read-only track/clip view with keyframe markers, playhead, program monitor, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, audio controls, and diagnostics. A static composition layer stays loaded and is not seeked per frame.
109
111
  - A bounded pool of active composition and nested-timeline clips. It loads clips around the playhead instead of preloading a whole long timeline.
110
112
 
111
113
  The preview server serves each composition fragment at its project path, wraps it in the fixed document shell, and injects the framework import map and host runtime. It serves timeline documents and a timeline preview host. The timeline preview host drives active composition/nested-timeline clips, seeks them, and exposes a timeline API to the preview. Preview audio is mixed by the preview page from the resolved timeline audio clips.
@@ -114,15 +116,15 @@ Production composition rendering uses one Chromium session per composition. An a
114
116
 
115
117
  Production timeline rendering is segmented and parallel:
116
118
 
117
- 1. Run static timeline validation and resolve the render plan.
119
+ 1. Run static timeline validation, resolve the timeline, and project the scene and render plan.
118
120
  2. Split the timeline into segments at active-layer changes or a maximum segment duration. Each segment has a fixed set of visual layers and a fixed filter graph.
119
- 3. In parallel workers, capture the active composition and nested-timeline clip frames for each segment with Chromium and encode them to lossless FFV1 intermediates. Transparent clips are captured with an alpha-preserving background. A static composition is captured once per props variant as a transparent PNG and reused as an image layer across segments and workers.
120
- 4. Decode media clips, apply trims, fit, static transforms, and opacity, composite the segment with FFmpeg, and encode H.264, HEVC, or VP9.
121
+ 3. In parallel workers, capture the active composition and nested-timeline clip frames for each segment with Chromium and stream them into lossless FFV1 intermediates. Independent layers of one segment are captured concurrently under a global FIFO session limit, and loaded composition sessions are reused across segments when their entry, props, and transparency match. Transparent clips are captured with an alpha-preserving background. A static composition is captured once per props variant as a transparent PNG and reused as an image layer across segments and workers. A segment with one full-frame, untransformed composition and no keyframes, blend, transition, or opacity skips the FFV1 intermediate and the composite, encoding the capture directly into the delivery segment over the timeline background. A full-frame composition base plus a static composition overlay (including a static PIP box) uses two image pipes into one FFmpeg process, which composites the background, base, and scaled overlay before the delivery encode; Windows keeps the segmented fallback. A single full-frame video with an identity transform re-encodes its source range directly, skipping the composite filter graph while keeping the delivery encoder settings.
122
+ 4. Decode media clips, apply trims, fit, presentation keyframes, rotation into an expanded bound, and opacity, composite the segment with FFmpeg, and encode H.264, HEVC, or VP9.
121
123
  5. Join the ordered segments with a stream copy. Segment boundaries never change frame order or timing.
122
124
  6. Mix the timeline audio once with FFmpeg and mux it into the final output.
123
125
  7. Publish the output atomically; delete scratch intermediates on success and failure.
124
126
 
125
- The default timeline worker count is 8, configurable up to the machine's capacity. With `--encoder nvenc` the count is capped at eight concurrent encode sessions, the consumer driver limit; a larger `--workers` value produces an `ENCODER_WARNING` and the lower count. Scratch is a configurable directory with a bounded budget; it must not depend on `/tmp` alone. The reference long-form target is a representative 60-minute 1920×1080 30 fps timeline with a 50/50 composition/media split by layer-seconds, 60% one active layer, 30% two, and 10% three, rendered in 30 minutes or less on the reference machine. The 10-second benchmark measured 10.7 s for two composition captures, 35.2 MB of FFV1, and 6.1 s to composite and encode. Continuous three-layer stress runs measured 52.4 s and 58.5 s for 60 s of timeline, about 1× real time, and are documented as worst-case rather than the target workload. The representative gate measured 29.39 s for 60 s with four workers, exact 1800-frame output, and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact 108,000-frame / 3,600 s output; it is stricter than the 50/50 target mix because every frame is an HTML composition capture. The fast path copies full-frame, untransformed video whose source matches the project format and has keyframes at segment boundaries; media that needs scaling or transforms is re-encoded and adds cost not included in that projection.
127
+ The default timeline worker count is 8, configurable up to the machine's capacity. With `--encoder nvenc` the count is capped at eight concurrent encode sessions, the consumer driver limit; a larger `--workers` value produces an `ENCODER_WARNING` and the lower count. Scratch is a configurable directory with a bounded budget; it must not depend on `/tmp` alone. The reference long-form target is a representative 60-minute 1920×1080 30 fps timeline with a 50/50 composition/media split by layer-seconds, 60% one active layer, 30% two, and 10% three, rendered in 30 minutes or less on the reference machine. The 10-second benchmark measured 10.7 s for two composition captures, 35.2 MB of FFV1, and 6.1 s to composite and encode. Continuous three-layer stress runs measured 52.4 s and 58.5 s for 60 s of timeline, about 1× real time, and are documented as worst-case rather than the target workload. The representative gate measured 29.39 s for 60 s with four workers, exact 1800-frame output, and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes. The full 60-minute composition-only gate rendered 108,000 frames in 37.28 minutes with four workers; it is stricter than the 50/50 target mix because every frame is an HTML composition capture. Capture was 48.9% of worker time, FFV1 23.2%, composite 24.7%, and session setup 2.8%. Composition captures now stream PNG buffers into FFV1, and a segment with one full-frame, untransformed composition skips the intermediate and composite. The 60-second one-layer gate fell from 43.27 s to 27.64 s, and the full 60-minute gate completed in 19.83 minutes with eight workers; the direct output was byte-identical to the FFV1-and-composite output. Reusing loaded sessions reduced the 60-minute gate further to 18.78 minutes with eight sessions instead of 360. The two-layer PIP 60-second gate improved from 62.79 s to 59.60 s with streaming and to 43.78 s with parallel layer capture under a global session limit. A full-frame composition base plus a static composition overlay uses dual image pipes; the full-frame 60-second workload fell from 53.27 s to 34.62 s, the PIP 60-second workload from 43.78 s to 33.90 s, and the full two-layer 60-minute gates completed in 29.69 minutes (full-frame) and 25.75 minutes (PIP) with eight workers. The production representative 60-minute gate — 50/50 layer-seconds, 60/30/10 occupancy, 360 clips, 540 segments, and video re-encoded rather than stream-copied — completed in 16.82 minutes with eight workers, exact 108,000 frames, and clean scratch. Direct full-frame video re-encoding later reduced it to 15.57 minutes with a byte-identical file; a 60-second video-only gate fell from 10.65 to 7.61 seconds. The 10-minute one-layer gate improved from 211.8 s to 192.6 s by reusing loaded composition sessions across segments. The fast path copies full-frame, untransformed video whose source matches the project format and whose source keyframes (I-frames) fall on segment boundaries; media that needs scaling or transforms is re-encoded and adds cost not included in that projection. Timeline clip keyframes add per-frame filter expressions and, for animated opacity or scale, an alpha-mask pass per affected layer; the keyframe benchmark reports current timings and is not part of the 60-minute projection.
126
128
 
127
129
  The renderer uses a bounded segment plan, a worker pool, and a scratch budget. It never publishes a partial output, and it cleans scratch on success and failure. Segment boundaries preserve timeline frame order and timing; the final audio mix is muxed once.
128
130
 
@@ -153,16 +155,23 @@ A render fails immediately on an uncaught exception, unhandled rejection, requir
153
155
 
154
156
  Project and composition commands are:
155
157
 
156
- - `init`: scaffold a timeline-first project with a composition, a one-clip timeline, a project format, an agent guide, a `.gitignore` for `out/`, and a private `package.json` with pinned `dev`, `check`, `render`, and `still` scripts.
158
+ - `init [directory] [--local-models]`: scaffold a timeline-first project with a composition, a one-clip timeline, a project format, an agent guide, a `.gitignore` for `out/`, and a private `package.json` with pinned `dev`, `check`, `render`, and `still` scripts. Without a directory, an interactive terminal prompts for a project name (default `my-video`) and creates that directory; non-interactive and `--json` runs use `my-video`. `--local-models` also installs the optional workspace policy, OpenCode permissions, and Pi settings.
157
159
  - `dev`: start the preview with composition and timeline modes.
158
160
  - `render <id> [--out video]`: render a timeline, an animated composition, or a static composition. Timeline renders accept `--workers <n>` and `--scratch <dir>`; composition renders accept `--concurrency <n>` from 1 to 16. `--container <mp4|mov|mkv|webm>` and `--codec <h264|hevc|vp9>` select the delivery pair (MP4/H.264 by default; WebM defaults to VP9), and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded; `auto` is the default and probes NVENC then Quick Sync before falling back to software. The output extension must match the container. The default output is `out/<id>.<ext>`. A static composition writes `out/<id>.png` and rejects `--container`, `--codec`, `--encoder`, and `--concurrency`. The JSON result is a discriminated union with `kind: "video"` or `kind: "image"`.
161
+ - `context`: print a read-only project digest with the format, composition metadata, prop keys and types, controller declarations, timeline clip summaries, unique assets, installed fonts, diagnostics, active policy, and next commands. It never launches Chromium.
162
+ - `lint <id>`: run static validation, timeline resolution, scene and keyframe projection for every timeline frame, a controller determinism scan, and media metadata probing without Chromium. It does not run `validateProps` or composition runtime; `check` remains the full gate.
163
+ - `inspect <image>` and `compare <first> <second>`: decode images through FFmpeg and report blank/alpha/color/content facts or a pixel and 3×3 region diff. These are text-only inspection aids, not a substitute for viewing a still.
164
+ - `add --list`, `add local-models`, and `add <composition|timeline> <id> --template <name>`: list templates and scaffolds, install the optional local-model host scaffold, or write a valid template entry and print the exact `fy.config.js` registration snippet. `add` never edits the config and never overwrites an existing file or registered id.
165
+ - `font search`, `font add <id>`, `font add --file`, `font add --url`, `font list`, `font verify`, and `font remove <id>`: search Fontsource, vendor a Fontsource/local/URL font into `assets/fonts/<id>/`, record it in `fy.fonts.json`, inspect its license/hash status, or remove it. The server serves the generated `@font-face` sheet at `/__frameyard/fonts.css` and injects it into composition documents. Fontsource installs pin `npmVersion` and vendor the package license; URL imports require HTTPS outside loopback tests and record an unknown license only in the `full` tier.
159
166
  - `check <id>`: run static validation, then validate configuration, props, assets, media, audio, and transcript requirements, load the target, and evaluate every frame without capturing or encoding. A static composition is loaded and inspected once and returns `kind: "static"` with `framesChecked: 1`.
160
167
  - `still <id> --frame <spec>`: capture one or more requested frames as PNGs through the clip capture and timeline composite path. A static composition exposes exactly frame 0. `<spec>` is a frame number (`42`), an inclusive range (`2-12`), or a comma-separated list (`0,118-126,240`); duplicates are removed preserving the first occurrence, input order is preserved, and one invocation captures at most 32 unique frames. There is no step or sequence mode. A single frame keeps the existing `--out <file.png>` behavior; multiple frames use `--out <directory>` and write `still-<id>-<frame>.png` files, defaulting to the current working directory. Frames and output conflicts are validated before Chromium, all frames are captured to temporary files before publication, and a failed capture publishes no files. JSON results add `frames` and keep the singular `frame` and `path` only when one frame was captured. See [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md).
161
- - `doctor`: inspect core requirements and optional integrations without repairing or downloading anything.
168
+ - `doctor`: inspect core requirements, optional integrations, the active resource policy, installed font manifests, and basic CPU/load/memory resources without repairing or downloading anything.
162
169
 
163
- `check`, `still`, and `render` run a Node-side static validation before launching Chromium. For a timeline it validates the JSON document, tracks, clips, references, nested timelines, cycles, frame-rate compatibility, overlaps, source bounds, media, transforms, audio settings, props, and every referenced composition. Static validation and the composition host read the composition grammar through one browser-safe reader with document and fragment scopes. Timeline diagnostics carry stable codes and optional source locations or JSON paths, and composition diagnostics from inside a timeline carry the clip and track context.
170
+ `lint`, `check`, `still`, and `render` run a Node-side static validation; `check`, `still`, and `render` then launch Chromium. For a timeline it validates the JSON document, tracks, clips, references, nested timelines, cycles, frame-rate compatibility, overlaps and transitions, source bounds, media, transforms, keyframes, audio settings, props, and every referenced composition. Static validation and the composition host read the composition grammar through one browser-safe reader with document and fragment scopes. Timeline diagnostics carry stable codes and optional source locations or JSON paths, and composition diagnostics from inside a timeline carry the clip and track context.
164
171
 
165
- Local asset-production commands are `tts` and `transcribe`. Every non-interactive command supports `--json` with stable result and error schemas. Structured results go to stdout; human diagnostics and inference progress go to stderr.
172
+ Local asset-production commands are `tts` and `transcribe`. Every non-interactive command supports `--json` with stable result and error schemas. Structured results go to stdout; human diagnostics and inference progress go to stderr. Project font acquisition is a separate authoring operation: `fy.fonts.json` records vendored files, licenses, and SHA-256 values; there is no shared font cache and no runtime font CDN. Fontsource search and download use the public Fontsource API and pinned CDN versions; local files and explicit HTTPS URLs cover other sources. The generated `@font-face` sheet is served at `/__frameyard/fonts.css` and injected before author styles.
173
+
174
+ A workspace `fy.policy.json` gates expensive operations. The nearest file from the working directory upward applies, independent of `fy.config.js`; `FRAMEYARD_POLICY` replaces it. `light` allows one still frame and blocks `check`, `tts`, `transcribe`, and `render`; `authoring` allows four still frames and `check` at low priority and blocks the three heavy operations; `full` allows everything up to the 32-frame still cap. Font list/verify/search are available in every tier; `font add` requires authoring or full; `font remove`, `font add --force`, and an unlicensed `font add --url` require full. A file can only tighten its tier. `--profile shared-cpu` caps timeline workers at two and composition concurrency at one and lowers process priority. `tts`, `transcribe`, and `render` accept `--dry-run`, which validates inputs and returns `plan`, `commandLine`, `prerequisites`, and `blockers` without writing outputs, loading a model, launching Chromium, or encoding. The policy is a guardrail, not a security boundary; see [ADR 0022](./adr/0022-gate-heavy-operations-with-a-workspace-resource-policy.md).
166
175
 
167
176
  The built-in server serves native ES modules, static assets, composition entries, timeline documents, and the timeline preview host. It wraps each configured composition fragment in the fixed document shell, injects the framework import map and composition host, and serves an inline controller through a virtual module URL. It performs no dependency pre-bundling. Third-party libraries must initially be browser-ready ES modules referenced explicitly. Running through the CLI server is required; direct `file://` execution is unsupported for deterministic preview or rendering.
168
177
 
@@ -184,20 +193,24 @@ The skill:
184
193
  - Uses the normal project format, HTML composition contract, JSON timeline contract, optional controller, and CLI available to humans.
185
194
  - Does not introduce an agent-only runtime, MCP server, hosted control plane, or separate rendering API.
186
195
  - Triggers for Frameyard authoring and operation rather than claiming to be a general-purpose media-generation skill.
187
- - Uses Frameyard's audio wrappers instead of invoking `qwen-tts` or `whisper-cli` directly.
188
- - Includes references for deterministic HTML authoring, timeline JSON authoring, tracks, clips, source ranges, props, media, nested timelines, static transforms, audio, validation, diagnostics, common mistakes, subtitle and karaoke recipes, JSON command results, long-form worker/scratch settings, and failure recovery.
196
+ - Uses Frameyard's audio wrappers instead of invoking `qwen-tts`, `sherpa-onnx-offline-tts`, or `whisper-cli` directly.
197
+ - Includes references for deterministic HTML authoring, timeline JSON authoring, tracks, clips, source ranges, props, media, nested timelines, static transforms, clip keyframes, clip transitions, audio, validation, diagnostics, common mistakes, subtitle and karaoke recipes, JSON command results, long-form worker/scratch settings, and failure recovery, plus a compact quick reference for limited-context hosts.
189
198
  - Invokes speech synthesis or transcription only when requested or explicitly approved as part of the brief.
190
199
  - Does not overwrite or regenerate existing generated assets without permission.
200
+ - Uses `lint` for the edit loop, `check` for the full gate, and `--dry-run` to hand off heavy operations when the workspace resource policy blocks them.
201
+ - Respects `fy.policy.json` and does not edit the policy file, host configuration, or skill source.
191
202
  - Runs `check`, inspects representative `still` images when the host supports image viewing, and discloses when visual inspection was impossible.
192
203
  - Does not claim success when checks fail or rendering is partial.
193
204
 
194
205
  A successful agent workflow preserves and reports the source project, timeline document, props, generated assets, provenance manifest, validation results, and final video path. The user must be able to inspect, modify, and reproduce the result rather than receiving only an opaque MP4.
195
206
 
207
+ Local-model host setup is optional and separate from a Frameyard project: `scaffolds/local-models/` contains an `authoring` policy, OpenCode permissions, and Pi settings that a workspace can copy when a local model and rendering share the machine. Without the scaffold, the CLI defaults to `full` and no host configuration applies.
208
+
196
209
  ## 8. Local speech synthesis
197
210
 
198
211
  The `tts` command integrates [`qwentts.cpp`](https://github.com/ServeurpersoCom/qwentts.cpp) through its external `qwen-tts` executable. It does not link the native library or require the upstream server.
199
212
 
200
- 0.6 supports CustomVoice mode only:
213
+ The qwen engine supports CustomVoice mode only:
201
214
 
202
215
  - A named speaker is required.
203
216
  - An optional style instruction and language may be supplied.
@@ -207,7 +220,15 @@ The `tts` command integrates [`qwentts.cpp`](https://github.com/ServeurpersoCom/
207
220
  - One invocation produces one frozen 24 kHz mono WAV from one complete text input.
208
221
  - Automatic long-text chunking, streaming, and alternative audio formats are deferred.
209
222
 
210
- Users acquire and provide models explicitly. Frameyard does not build `qwentts.cpp`, download or redistribute models, or imply that upstream code licenses settle model-weight licensing. Executable resolution uses an explicit option, then environment configuration, then PATH.
223
+ `tts --engine supertonic` integrates [`sherpa-onnx`](https://github.com/k2-fsa/sherpa-onnx) through its external `sherpa-onnx-offline-tts` executable and the Supertonic 3 model archive:
224
+
225
+ - The engine covers 31 languages and requires one of their language codes, a numeric speaker id, and a directory containing the seven model files.
226
+ - The engine writes its native WAV; Frameyard normalizes it to the same frozen 24 kHz mono WAV contract with FFmpeg and records `sourceSampleRate` and `sourceChannels` in the manifest.
227
+ - The manifest records the seven model identities, the pinned sherpa-onnx revision, and the model archive name.
228
+ - Supertonic expression tags belong in the text; the qwen-only style and device options are rejected for this engine.
229
+ - The upstream Supertonic repository is archived and its weights use the OpenRAIL-M license. Users acquire the executable and weights explicitly; Frameyard does not redistribute them.
230
+
231
+ Users acquire and provide models explicitly. Frameyard does not build `qwentts.cpp` or `sherpa-onnx`, download or redistribute models, or imply that upstream code licenses settle model-weight licensing. Executable resolution uses an explicit option, then environment configuration, then PATH.
211
232
 
212
233
  ## 9. Local transcription and timed text
213
234
 
@@ -247,14 +268,14 @@ Generated artifacts follow a shared contract:
247
268
 
248
269
  - Outputs are written atomically to explicit paths.
249
270
  - Existing outputs are not overwritten unless `--force` is supplied.
250
- - A versioned project-local provenance manifest records artifact content hashes, generator details, model identities, and parameters. The default manifest is `frameyard.assets.json` in the project root; `--manifest` selects another path.
271
+ - A versioned project-local provenance manifest records artifact content hashes, generator details, model identities, and parameters. The default manifest is `fy.assets.json` in the project root; `--manifest` selects another path.
251
272
  - Transcription provenance also identifies the source asset by content hash.
252
273
  - Partial operations leave neither apparently valid artifacts nor manifest entries.
253
- - `doctor` validates executables, model readability, capabilities, and supported integration versions without modifying the environment.
274
+ - `doctor` validates executables, model readability, capabilities, and supported integration versions when the upstream CLI exposes a version, without modifying the environment.
254
275
 
255
276
  Each operation runs one inference subprocess, and the official agent workflow executes inference sequentially to avoid unexpected CPU/GPU contention. There is no arbitrary fixed timeout. Cancellation terminates the child process and removes partial outputs.
256
277
 
257
- Release documentation must identify the exact `qwentts.cpp` and `whisper.cpp` revisions against which the wrappers are tested. Frameyard does not claim compatibility with arbitrary upstream revisions.
278
+ Release documentation must identify the exact `qwentts.cpp`, `sherpa-onnx`, and `whisper.cpp` revisions against which the wrappers are tested. Frameyard does not claim compatibility with arbitrary upstream revisions.
258
279
 
259
280
  ## 11. Verification strategy
260
281
 
@@ -262,7 +283,7 @@ Tests prioritize public behavior and end-to-end evidence over implementation-onl
262
283
 
263
284
  - HTML compositions validate metadata, default props, bindings, conditionals, repeats, and controller declarations, including inline/external parity and the exactly-one-controller rule.
264
285
  - Static composition validation catches metadata, root/controller count, binding, animation, script, element, and asset-reference errors before Chromium. It resolves declared format values against the project format and reports an unresolved format to the command rather than the document. A composition with neither time attribute is static; `fps` without `duration-in-frames`, `data-fy-anim-*`, media, and transcripts on a static composition are errors.
265
- - Timeline JSON validation catches schema, track, clip, reference, cycle, frame-rate, overlap, bounds, media, transform, audio, and props errors before Chromium.
286
+ - Timeline JSON validation catches schema, track, clip, reference, cycle, frame-rate, overlap, transition, bounds, media, transform, audio, and props errors before Chromium.
266
287
  - Diagnostics carry stable codes and source locations or JSON paths; JSON results expose the composition or timeline entry, clips, tracks, and top-level diagnostics.
267
288
  - Sequential, individual, random-order, and parallel composition renders produce equivalent images; timeline segment workers produce ordered output and identical captured composition frames.
268
289
  - Determinism checks compare captured composition pixels or stills. The final encoded bytes and the FFmpeg composite versus Chromium preview are not reproducibility oracles.
@@ -274,18 +295,19 @@ Tests prioritize public behavior and end-to-end evidence over implementation-onl
274
295
  - Nested timelines preserve group opacity and transform semantics; cycles fail static validation.
275
296
  - Media clips trim and fit deterministically; images require durations; source overruns fail.
276
297
  - Props overrides for timeline composition clips merge over clip props and are reflected in the render.
277
- - `check` exercises every animated frame and inspects a static composition once; `still` follows the clip capture and timeline composite path. A static composition render captures one PNG; a timeline reuses one static image capture per props variant.
278
- - The 10-second and 60-second long-form benchmarks record capture, FFV1, composite, encode, disk, worker, and audio numbers. Continuous three-layer runs measured 52.4 s and 58.5 s for 60 s of timeline and are documented as worst-case (~1× real time). The representative 50/50, 60/30/10 occupancy gate measured 29.39 s for 60 s with four workers and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes with exact 1800-frame output. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact output and clean scratch, documenting composition-heavy performance above 30 minutes.
298
+ - `check` exercises every animated frame and inspects a static composition once; it also evaluates every keyframe channel at every timeline frame. `still` follows the clip capture and timeline composite path. A static composition render captures one PNG; a timeline reuses one static image capture per props variant.
299
+ - The 10-second and 60-second long-form benchmarks record capture, FFV1, composite, encode, disk, worker, and audio numbers. Continuous three-layer runs measured 52.4 s and 58.5 s for 60 s of timeline and are documented as worst-case (~1× real time). The representative 50/50, 60/30/10 occupancy gate measured 29.39 s for 60 s with four workers and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes with exact 1800-frame output. The full 60-minute composition-only gate rendered 108,000 frames in 37.28 minutes with four workers before streaming and the direct-segment fast path; the same gate completed in 19.83 minutes with eight workers after ADR 0024 and 18.78 minutes after session reuse (ADR 0026), with exact output, clean scratch, and a byte-identical file. The ten-minute gate projects about 24 minutes at four workers. Independent layers are captured concurrently under a global session limit (ADR 0025); the two-layer PIP 60-second gate fell from 59.60 s to 43.78 s with parallel captures. A full-frame composition base plus a static composition overlay encodes through dual image pipes (ADR 0027); the full-frame 60-second workload fell from 53.27 s to 34.62 s, the PIP 60-second workload from 43.78 s to 33.90 s, and the full two-layer 60-minute gates completed in 29.69 minutes (full-frame) and 25.75 minutes (PIP) with eight workers. The representative 60-minute 50/50 production gate completed in 16.82 minutes with eight workers, exact 108,000 frames, clean scratch, and no video stream-copy; direct full-frame video re-encoding reduced it to 15.57 minutes with a byte-identical file. Loaded composition sessions are reused across segments (ADR 0026); the 10-minute one-layer gate fell from 211.8 s to 192.6 s, and the full gate opened eight sessions instead of 360.
279
300
  - Golden-frame tests cover representative composition and timeline visual behavior.
280
- - Normal CI uses fake `qwen-tts` and `whisper-cli` executables and small fixtures to verify arguments, artifacts, manifests, cancellation, and errors without model downloads or GPU hardware.
301
+ - Normal CI uses fake `qwen-tts`, `sherpa-onnx-offline-tts`, and `whisper-cli` executables and small fixtures to verify arguments, artifacts, manifests, cancellation, and errors without model downloads or GPU hardware.
281
302
  - Real-model integration tests are explicitly opt-in and use documented upstream revisions.
282
303
  - Agent-workflow evaluations verify source preservation, provenance, validation, still inspection behavior, failure reporting, and final renders.
304
+ - Local-model evals are opt-in and run against a llama.cpp router: eight authoring tasks, first-pass lint, repair turns, token and wall-time counts, and sampled server CPU/RSS/GPU. They exercise the same quick reference and CLI as an agent, never render, and stay outside `pnpm check`.
283
305
 
284
306
  ## 12. Deferred
285
307
 
286
308
  These are scope boundaries, not permanent rejections:
287
309
 
288
- - Transitions, filters, keyframes, masks, and audio fades
310
+ - Animated blend modes, filters, masks, and audio fades
289
311
  - Full visual timeline editing, drag-and-drop, clip handles, and undo/redo
290
312
  - Timeline-level variables and prop overrides for compositions inside nested timelines
291
313
  - Track solo and video-track visibility
@@ -299,9 +321,12 @@ These are scope boundaries, not permanent rejections:
299
321
  - Built-in subtitle rendering, automatic caption burn-in, and declarative karaoke elements
300
322
  - Nested `data-fy-repeat` scopes and a general template language
301
323
  - Declarative JSON Schema validation
324
+ - A shared font cache, hosted font catalog, or first-class Google Fonts/Fontshare/Bunny providers; Fontsource plus local/HTTPS imports cover acquisition
325
+ - Typography roles, font-pair presets, a preview font picker, and variable axes other than `wght`
302
326
  - Arbitrary executable scripts in composition HTML
303
327
  - Automatic upstream builds, model downloads, weight redistribution, and environment repair
304
328
  - Agent-only runtimes, MCP servers, hosted agent control planes, and host-specific skill installers
329
+ - Automatic idle scheduling, job queues, and OS-level resource enforcement; the workspace policy is a guardrail, not a sandbox
305
330
  - React or other UI-framework adapters in the core
306
331
  - Public plugin APIs and a broad integration ecosystem
307
332
  - Dynamic composition or timeline metadata derived from props
@@ -320,4 +345,4 @@ These are scope boundaries, not permanent rejections:
320
345
 
321
346
  ## 13. Release posture
322
347
 
323
- Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, timeline, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize. 0.6.0 adds timelines, project formats, segmented timeline rendering, and a timeline-first scaffold without breaking the existing HTML composition authoring contract. See [ADR 0010](./adr/0010-make-timelines-the-render-unit.md), [ADR 0011](./adr/0011-compose-timeline-frames-in-a-live-browser-host.md), and [ADR 0012](./adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md). 0.7.0 adds delivery container, codec, and encoder selection, bounded multi-frame still inspection, and optional encoder probes in `doctor` without breaking the timeline or composition contracts; bounded still inspection is recorded in [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md). Static clip blend modes are recorded in [ADR 0014](./adr/0014-compose-timeline-clips-with-css-blend-modes.md). Optional composition `width`, `height`, and `fps` with project-format inheritance are recorded in [ADR 0015](./adr/0015-inherit-composition-format-from-the-project.md). 0.8.0 changes compositions to HTML fragments, inherits their format from the project, and renders timeless compositions as still images; the contracts are recorded in [ADR 0015](./adr/0015-inherit-composition-format-from-the-project.md), [ADR 0016](./adr/0016-author-compositions-as-html-fragments.md), and [ADR 0017](./adr/0017-render-timeless-compositions-as-stills.md).
348
+ Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and features and contract changes are documented explicitly until those contracts stabilize.
@@ -62,11 +62,11 @@ examples/blend/
62
62
  timelines/blend.timeline.json
63
63
  assets/ink.mp4 procedural 12-second source clip
64
64
  scripts/make-ink.sh regenerate assets/ink.mp4
65
- video.config.js
65
+ fy.config.js
66
66
  ```
67
67
 
68
68
  The timeline lives in `timelines/blend.timeline.json`; the project format and
69
- entries are fixed in `video.config.js`. `backdrop.html` is a static composition
69
+ entries are fixed in `fy.config.js`. `backdrop.html` is a static composition
70
70
  that omits all dimensions and time metadata, so it inherits the project's
71
71
  1920×1080 size. `aurora.html` is a static composition that declares its
72
72
  dimensions but no time metadata. `labels.html` is an animated composition and
@@ -81,7 +81,7 @@ pnpm build
81
81
  node dist/cli.js check blend-timeline --project examples/blend
82
82
  node dist/cli.js still blend-timeline --frame 15,45,75,105,135,165,195,225,255,285,315,345 --project examples/blend --out blend-stills
83
83
  node dist/cli.js still blend-timeline --frame 285 --project examples/blend --out blend-soft-light.png
84
- node dist/cli.js render blend-timeline --project examples/blend
84
+ FRAMEYARD_POLICY=full node dist/cli.js render blend-timeline --project examples/blend
85
85
  node dist/cli.js dev --project examples/blend
86
86
  ```
87
87
 
@@ -19,13 +19,13 @@ From the repository root:
19
19
  pnpm build
20
20
  node dist/cli.js check demo-timeline --project examples/demo
21
21
  node dist/cli.js still demo-timeline --frame 240 --project examples/demo --out demo-still.png
22
- node dist/cli.js render demo-timeline --project examples/demo --props examples/demo/props/demo.json
22
+ FRAMEYARD_POLICY=full node dist/cli.js render demo-timeline --project examples/demo --props examples/demo/props/demo.json
23
23
  ```
24
24
 
25
25
  The composition remains renderable on its own:
26
26
 
27
27
  ```sh
28
- node dist/cli.js render demo --project examples/demo --props examples/demo/props/demo.json
28
+ FRAMEYARD_POLICY=full node dist/cli.js render demo --project examples/demo --props examples/demo/props/demo.json
29
29
  ```
30
30
 
31
31
  Start the interactive preview with: