frameyard 0.7.0 → 0.8.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 (138) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/GLOSSARY.md +40 -16
  3. package/README.md +54 -48
  4. package/dist/cli.js +23 -16
  5. package/dist/cli.js.map +1 -1
  6. package/dist/composition/descriptor.d.ts +17 -3
  7. package/dist/composition/descriptor.d.ts.map +1 -1
  8. package/dist/composition/descriptor.js +51 -28
  9. package/dist/composition/descriptor.js.map +1 -1
  10. package/dist/composition/format.d.ts +12 -0
  11. package/dist/composition/format.d.ts.map +1 -0
  12. package/dist/composition/format.js +28 -0
  13. package/dist/composition/format.js.map +1 -0
  14. package/dist/composition/index.d.ts +2 -1
  15. package/dist/composition/index.d.ts.map +1 -1
  16. package/dist/composition/index.js +1 -0
  17. package/dist/composition/index.js.map +1 -1
  18. package/dist/contracts.d.ts +55 -6
  19. package/dist/contracts.d.ts.map +1 -1
  20. package/dist/contracts.js +3 -0
  21. package/dist/contracts.js.map +1 -1
  22. package/dist/errors.d.ts +1 -1
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js.map +1 -1
  25. package/dist/html/document-adapter.d.ts +3 -3
  26. package/dist/html/document-adapter.d.ts.map +1 -1
  27. package/dist/html/document-adapter.js +1 -1
  28. package/dist/html/document-adapter.js.map +1 -1
  29. package/dist/html/index.d.ts +1 -1
  30. package/dist/html/index.d.ts.map +1 -1
  31. package/dist/html/parse.d.ts +10 -2
  32. package/dist/html/parse.d.ts.map +1 -1
  33. package/dist/html/parse.js +14 -5
  34. package/dist/html/parse.js.map +1 -1
  35. package/dist/html/validate.d.ts +12 -1
  36. package/dist/html/validate.d.ts.map +1 -1
  37. package/dist/html/validate.js +233 -61
  38. package/dist/html/validate.js.map +1 -1
  39. package/dist/index.d.ts +4 -4
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/preview/main.d.ts +2 -2
  44. package/dist/preview/main.d.ts.map +1 -1
  45. package/dist/preview/main.js +35 -14
  46. package/dist/preview/main.js.map +1 -1
  47. package/dist/preview/timeline-data.d.ts +5 -1
  48. package/dist/preview/timeline-data.d.ts.map +1 -1
  49. package/dist/preview/timeline-host.d.ts.map +1 -1
  50. package/dist/preview/timeline-host.js +10 -0
  51. package/dist/preview/timeline-host.js.map +1 -1
  52. package/dist/preview/timeline-main.js +3 -2
  53. package/dist/preview/timeline-main.js.map +1 -1
  54. package/dist/project.d.ts +3 -5
  55. package/dist/project.d.ts.map +1 -1
  56. package/dist/project.js.map +1 -1
  57. package/dist/renderer/browser.d.ts +2 -2
  58. package/dist/renderer/browser.d.ts.map +1 -1
  59. package/dist/renderer/browser.js +10 -4
  60. package/dist/renderer/browser.js.map +1 -1
  61. package/dist/renderer/ffmpeg.d.ts +3 -0
  62. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  63. package/dist/renderer/ffmpeg.js +122 -56
  64. package/dist/renderer/ffmpeg.js.map +1 -1
  65. package/dist/renderer/index.d.ts +40 -4
  66. package/dist/renderer/index.d.ts.map +1 -1
  67. package/dist/renderer/index.js +99 -24
  68. package/dist/renderer/index.js.map +1 -1
  69. package/dist/renderer/timeline.d.ts.map +1 -1
  70. package/dist/renderer/timeline.js +89 -0
  71. package/dist/renderer/timeline.js.map +1 -1
  72. package/dist/runtime/host.d.ts.map +1 -1
  73. package/dist/runtime/host.js +155 -31
  74. package/dist/runtime/host.js.map +1 -1
  75. package/dist/scaffold.d.ts.map +1 -1
  76. package/dist/scaffold.js +104 -101
  77. package/dist/scaffold.js.map +1 -1
  78. package/dist/server/composition-html.d.ts +13 -7
  79. package/dist/server/composition-html.d.ts.map +1 -1
  80. package/dist/server/composition-html.js +65 -16
  81. package/dist/server/composition-html.js.map +1 -1
  82. package/dist/server/server.d.ts.map +1 -1
  83. package/dist/server/server.js +26 -6
  84. package/dist/server/server.js.map +1 -1
  85. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  86. package/dist/server/timeline-preview-data.js +11 -0
  87. package/dist/server/timeline-preview-data.js.map +1 -1
  88. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  89. package/dist/server/timeline-preview-html.js +2 -1
  90. package/dist/server/timeline-preview-html.js.map +1 -1
  91. package/dist/timeline/resolve.d.ts +4 -3
  92. package/dist/timeline/resolve.d.ts.map +1 -1
  93. package/dist/timeline/resolve.js +14 -4
  94. package/dist/timeline/resolve.js.map +1 -1
  95. package/dist/timeline/schema.d.ts +7 -0
  96. package/dist/timeline/schema.d.ts.map +1 -1
  97. package/dist/timeline/schema.js +50 -0
  98. package/dist/timeline/schema.js.map +1 -1
  99. package/dist/version.d.ts +1 -1
  100. package/dist/version.js +1 -1
  101. package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
  102. package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
  103. package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
  104. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  105. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +37 -0
  106. package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
  107. package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
  108. package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
  109. package/docs/migration-0.4.md +3 -1
  110. package/docs/migration-0.8.md +163 -0
  111. package/docs/preview-testing.md +7 -0
  112. package/docs/v0.1-design.md +42 -34
  113. package/examples/blend/README.md +106 -0
  114. package/examples/blend/assets/ink.mp4 +0 -0
  115. package/examples/blend/compositions/aurora.html +55 -0
  116. package/examples/blend/compositions/backdrop.html +74 -0
  117. package/examples/blend/compositions/labels.html +149 -0
  118. package/examples/blend/scripts/make-ink.sh +26 -0
  119. package/examples/blend/timelines/blend.timeline.json +155 -0
  120. package/examples/blend/video.config.js +11 -0
  121. package/examples/demo/compositions/demo.html +243 -251
  122. package/examples/static/README.md +17 -0
  123. package/examples/static/compositions/still.html +37 -0
  124. package/examples/static/timelines/still.timeline.json +13 -0
  125. package/examples/static/video.config.js +9 -0
  126. package/examples/video/compositions/video-mp4.html +349 -357
  127. package/examples/video/compositions/video-overlay.html +337 -345
  128. package/examples/video/compositions/video.html +349 -357
  129. package/examples/webgl/compositions/webgl.html +205 -213
  130. package/package.json +2 -1
  131. package/skills/frameyard/SKILL.md +15 -13
  132. package/skills/frameyard/references/common-mistakes.md +29 -6
  133. package/skills/frameyard/references/controllers.md +5 -4
  134. package/skills/frameyard/references/deterministic-authoring.md +41 -36
  135. package/skills/frameyard/references/diagnostics.md +11 -3
  136. package/skills/frameyard/references/failure-recovery.md +25 -5
  137. package/skills/frameyard/references/json-results.md +46 -4
  138. package/skills/frameyard/references/timelines.md +16 -12
package/CHANGELOG.md CHANGED
@@ -8,6 +8,66 @@ Frameyard uses `0.x` versions until the composition, configuration, CLI,
8
8
  JSON-result, transcript, and skill contracts stabilize. Before `1.0.0`, a
9
9
  contract change may ship in a minor release and is called out explicitly below.
10
10
 
11
+ ## [0.8.0] - 2026-11-03
12
+
13
+ ### Changed
14
+
15
+ - Compositions are partial HTML fragments instead of full HTML documents.
16
+ Authors keep one top-level `<fy-composition>` root plus top-level props,
17
+ controller, import maps, and styles; Frameyard generates the document shell,
18
+ import map, sizing style, format block, and host runtime. A legacy full
19
+ document fails with `COMPOSITION_DOCUMENT_SHELL`. See ADR 0016 and
20
+ [migration-0.8](./docs/migration-0.8.md).
21
+ - Top-level author import maps are validated and hoisted after the framework
22
+ map; `frameyard` and `frameyard/` specifiers are reserved and invalid maps
23
+ report `IMPORT_MAP_INVALID`.
24
+ - `<fy-composition>` makes `width`, `height`, and `fps` optional and keeps
25
+ `duration-in-frames` required only for an animated composition. An omitted
26
+ value is inherited from the project `format`; a declared value wins, and
27
+ `width`/`height` must be declared together. `CompositionSourceInfo` gains
28
+ `declaredFormat`, and `project.json`/render results keep exposing resolved
29
+ metadata.
30
+ - A composition that declares neither `duration-in-frames` nor `fps` is static.
31
+ It has no frame clock, is captured once, and `render` writes `out/<id>.png`.
32
+ `fps` without `duration-in-frames` is an error. See ADR 0017 and
33
+ [migration-0.8](./docs/migration-0.8.md).
34
+ - Static compositions are visual-only: `data-fy-anim-*`, `<video>`, `<audio>`,
35
+ and `<fy-transcript>` are errors. The host runs `mount()` and prop validation,
36
+ skips `update()`, leaves frame-state variables unset, pauses CSS animations at
37
+ time 0, and reports `STATIC_UPDATE_IGNORED` and `STATIC_ANIMATION_PAUSED`.
38
+ - A static composition clip in a timeline behaves like an image clip: it
39
+ requires `duration`, rejects nonzero `sourceIn`, contributes no audio or
40
+ transcripts, and is captured once per composition/props pair and reused
41
+ across segments.
42
+ - `render` results are a discriminated union with `kind: "video"` or
43
+ `kind: "image"`. Static composition renders reject `--container`, `--codec`,
44
+ `--encoder`, and `--concurrency`. `check` adds `kind: "animated"` or
45
+ `kind: "static"`; a static check inspects one state and reports
46
+ `framesChecked: 1`.
47
+ - `still` on a static composition exposes exactly frame 0; other frames fail
48
+ with `FRAME_OUT_OF_RANGE`.
49
+ - Preview loads a static composition once, disables its transport, and keeps a
50
+ static timeline layer loaded without seeking it.
51
+ - Rendering a composition that omits dimensions in a project without `format`
52
+ fails with `COMPOSITION_FORMAT_UNRESOLVED` instead of a document diagnostic;
53
+ `check`, `still`, `render`, and the preview report the same actionable error.
54
+ See ADR 0015.
55
+
56
+ ### Added
57
+
58
+ - Visual timeline clips accept a static `blend` mode with the 12 separable CSS
59
+ modes (`normal` through `exclusion`); `hue`, `saturation`, `color`, and
60
+ `luminosity` are rejected because they need cross-channel math. Preview
61
+ applies `mix-blend-mode` and stacks layers by track; production uses an
62
+ alpha-aware FFmpeg composite. Nested timelines blend as flattened groups.
63
+ See ADR 0014 and `examples/blend/`.
64
+ - `examples/static/` demonstrates a timeless composition rendered to PNG and
65
+ reused as a timeline image.
66
+ - `examples/blend/compositions/backdrop.html` demonstrates an inherited
67
+ composition format.
68
+ - Static composition validation, host, CLI, timeline, preview, and example
69
+ coverage.
70
+
11
71
  ## [0.7.0] - 2026-11-02
12
72
 
13
73
  ### Added
@@ -51,6 +111,7 @@ contract change may ship in a minor release and is called out explicitly below.
51
111
  layer iframes reach the preview transport, while text entry does not step
52
112
  frames across iframe boundaries.
53
113
 
114
+ [0.8.0]: https://github.com/vladiantio/frameyard/compare/v0.7.0...v0.8.0
54
115
  [0.7.0]: https://github.com/vladiantio/frameyard/compare/v0.6.0...v0.7.0
55
116
 
56
117
  ## [0.6.0] - 2026-11-01
package/GLOSSARY.md CHANGED
@@ -11,27 +11,39 @@ A collection of related compositions, timelines, and the assets and configuratio
11
11
  _Avoid_: Video, render
12
12
 
13
13
  **Project format**:
14
- The fixed width, height, and frame rate shared by every timeline in a project.
14
+ The fixed width, height, and frame rate shared by every timeline in a project and inherited by compositions that omit those values.
15
15
  _Avoid_: Composition metadata, render options
16
16
 
17
17
  **Composition**:
18
- A uniquely identified, reusable video source with fixed dimensions, frame rate, duration in frames, and content parameterized by props.
18
+ A uniquely identified, reusable video or still source, content parameterized by props, and a format that is either declared or inherited from the project. An animated composition has a duration in frames; a static composition has no time base.
19
19
  _Avoid_: Scene, template, video
20
20
 
21
+ **Static composition**:
22
+ A composition that declares neither `duration-in-frames` nor `fps`. It has no frame clock, audio, or transcripts, is captured once in its initial state, and renders to a PNG. A timeline treats it as an image source whose clip declares the duration.
23
+ _Avoid_: Still video, one-frame composition
24
+
25
+ **Declared format**:
26
+ The width, height, and frame rate a composition fragment declares on its root. A value may be absent, in which case it is inherited; a declared value always wins over the project format.
27
+ _Avoid_: Composition metadata, defaults
28
+
29
+ **Resolved format**:
30
+ The width, height, and frame rate an animated composition actually renders with, after declared values and the project format are combined. A static composition resolves width and height only. It is part of the immutable frame environment.
31
+ _Avoid_: Output options, render settings
32
+
21
33
  **Composition metadata**:
22
- The fixed dimensions, frame rate, and duration in frames that define a composition's source contract.
34
+ The resolved dimensions plus the frame rate and duration that define an animated composition's source contract, or the dimensions and `static: true` of a static composition.
23
35
  _Avoid_: Render options, props
24
36
 
25
37
  **Composition descriptor**:
26
- The fixed facts read from an HTML composition before frame production: composition metadata, default props, and the controller declaration. It excludes per-load declarations such as audio clips, transcripts, and animation schedules.
38
+ The fixed facts read from a composition fragment before frame production: declared composition metadata, default props, and the controller declaration. It excludes per-load declarations such as audio clips, transcripts, and animation schedules.
27
39
  _Avoid_: Composition config, parsed document
28
40
 
29
41
  **Composition lifecycle**:
30
- The host-driven sequence that loads an HTML composition, seeks it to frames, and releases it.
42
+ The host-driven sequence that loads a composition fragment, optionally seeks it to frames, and releases it. A static composition is loaded, captured once, and released without seeking.
31
43
  _Avoid_: Render loop, playback lifecycle
32
44
 
33
45
  **Controller lifecycle**:
34
- The `mount → update → dispose` sequence a composition controller uses for per-frame computation.
46
+ The `mount → update → dispose` sequence an animated composition controller uses for per-frame computation. A static composition runs `mount` and `dispose` only.
35
47
  _Avoid_: Composition lifecycle
36
48
 
37
49
  **Composition revision**:
@@ -39,23 +51,27 @@ A specific version of a composition's source and metadata.
39
51
  _Avoid_: Build, release
40
52
 
41
53
  **HTML composition**:
42
- A composition whose source is an HTML document.
43
- _Avoid_: HTML template, page
54
+ A composition whose source is a partial HTML fragment.
55
+ _Avoid_: HTML document, page
56
+
57
+ **Composition fragment**:
58
+ The authoring source of an HTML composition: body content with one top-level `<fy-composition>` root, plus optional top-level props, controller, import maps, and styles. Frameyard generates the document shell.
59
+ _Avoid_: HTML document, page
44
60
 
45
61
  **Transparent composition**:
46
62
  A composition that does not paint a background, so a timeline clip can reveal the layers below it.
47
63
  _Avoid_: Alpha composition, overlay
48
64
 
49
65
  **Composition controller**:
50
- An optional JavaScript controller, either inline in the composition document or in an external module, that performs per-frame computation an HTML composition's declarative HTML and CSS cannot express.
66
+ An optional JavaScript controller, either inline in the composition fragment or in an external module, that performs per-frame computation an HTML composition's declarative HTML and CSS cannot express.
51
67
  _Avoid_: Composition module, plugin
52
68
 
53
69
  **Inline controller**:
54
- A composition controller whose source is embedded in the composition document.
70
+ A composition controller whose source is embedded in the composition fragment.
55
71
  _Avoid_: Embedded script
56
72
 
57
73
  **External controller**:
58
- A composition controller whose source is a separate JavaScript module referenced by the composition document.
74
+ A composition controller whose source is a separate JavaScript module referenced by the composition fragment.
59
75
  _Avoid_: Controller file
60
76
 
61
77
  **Declarative binding**:
@@ -138,6 +154,10 @@ _Avoid_: Resize mode, object-fit
138
154
  The static size, position, rotation, and opacity applied to a visual clip. Size is either an explicit `width`/`height` box or a `scale` multiplier.
139
155
  _Avoid_: Effect, animation
140
156
 
157
+ **Blend mode**:
158
+ The static CSS `mix-blend-mode` a visual clip applies against the accumulated backdrop of all lower layers and the timeline background. It respects the clip's source alpha and is not animated.
159
+ _Avoid_: Filter, effect, composite mode
160
+
141
161
  **Nested timeline**:
142
162
  A timeline used as a clip source inside another timeline.
143
163
  _Avoid_: Compound composition
@@ -225,7 +245,7 @@ _Avoid_: Live audio
225
245
  ### Rendering
226
246
 
227
247
  **Composition host**:
228
- The injected runtime that loads an HTML composition, applies its bindings, and exposes its frame clock to preview and render.
248
+ The injected runtime that loads a composition fragment, applies its bindings, and exposes its frame clock to preview and render.
229
249
  _Avoid_: Preview runtime, render runtime
230
250
 
231
251
  **Timeline host**:
@@ -249,11 +269,11 @@ The preview surface that shows the timeline at the playhead.
249
269
  _Avoid_: Stage, canvas
250
270
 
251
271
  **Check**:
252
- A validation operation that first runs static validation, then loads a timeline or composition and evaluates every frame without capturing or encoding.
272
+ A validation operation that first runs static validation, then loads a timeline or composition and evaluates every frame without capturing or encoding. A static composition has one state, so it is loaded and inspected once.
253
273
  _Avoid_: Test render, dry run
254
274
 
255
275
  **Static composition validation**:
256
- The Node-side validation of a composition document's metadata, bindings, controller declaration, scripts, and asset references before browser frame production.
276
+ The Node-side validation of a composition fragment's metadata, bindings, controller declaration, scripts, and asset references before browser frame production.
257
277
  _Avoid_: Lint, preflight
258
278
 
259
279
  **Static timeline validation**:
@@ -269,9 +289,13 @@ A structured error or warning about a timeline's source, tracks, clips, referenc
269
289
  _Avoid_: Log message, error string
270
290
 
271
291
  **Render**:
272
- The production of one complete video from a timeline or a standalone composition with fixed inputs.
292
+ The production of one complete video from a timeline or an animated standalone composition, or one PNG from a static standalone composition, with fixed inputs.
273
293
  _Avoid_: Recording, export
274
294
 
295
+ **Static composition render**:
296
+ The PNG produced by `render` for a static composition. It has no frame rate, duration, audio, or encoder.
297
+ _Avoid_: Still, frame export
298
+
275
299
  **Render concurrency**:
276
300
  The optional number of independent browser sessions used to seek and capture frames during a composition render; timeline rendering is sequential in 0.6. Frames are still encoded in timeline order, and concurrency must not change visual state.
277
301
  _Avoid_: Parallel composition, out-of-order output
@@ -281,7 +305,7 @@ The finished audiovisual output of a successful render.
281
305
  _Avoid_: Composition, project
282
306
 
283
307
  **Still**:
284
- One or more requested frames captured as PNGs through the production rendering path for a timeline or composition. A single invocation captures at most 32 inspection frames; this is not image-sequence output.
308
+ One or more requested frames captured as PNGs through the production rendering path for a timeline or composition. A static composition exposes exactly frame 0. A single invocation captures at most 32 inspection frames; this is not image-sequence output.
285
309
  _Avoid_: Frame export, image sequence
286
310
 
287
311
  **Reproducibility**:
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Frameyard is a local-first framework for building parameterized videos with HTML, CSS, and an optional JavaScript controller, assembling them into timelines, and rendering them deterministically.
4
4
 
5
- The 0.6 design is implemented: HTML composition documents, JSON timelines with tracks and clips, nested timelines, declarative bindings, CSS animation scheduling, frame-state variables, inline and external controllers, composition and timeline hosts, static validation with structured diagnostics, an interactive preview with a read-only timeline, direct composition rendering, segmented lossless timeline rendering, audio and transcripts, deterministic Chromium capture, FFmpeg output, and the official agent skill.
5
+ The current design is implemented: HTML composition fragments, JSON timelines with tracks and clips, nested timelines, static clip blend modes, declarative bindings, CSS animation scheduling, frame-state variables, inline and external controllers, composition and timeline hosts, static validation with structured diagnostics, an interactive preview with a read-only timeline, direct composition rendering, segmented lossless timeline rendering, audio and transcripts, deterministic Chromium capture, FFmpeg output, and the official agent skill.
6
6
 
7
7
  ## Requirements
8
8
 
@@ -44,7 +44,7 @@ npx frameyard still main --frame 0,118-126 --out out/stills
44
44
 
45
45
  The scaffold wraps its composition in `timelines/main.timeline.json`. `render` writes the video to `out/main.mp4` in the project by default; pass `--out <file.mp4>` to choose another path. The composition remains renderable on its own with `npx frameyard render hello --props props/hello.json`. The scaffold's `.gitignore` ignores `out/`.
46
46
 
47
- The 30-second reference composition lives in [`examples/demo`](./examples/demo), a focused WebGL shader example lives in [`examples/webgl`](./examples/webgl), and the video example in [`examples/video`](./examples/video) includes two embedded-video compositions plus WebM and MP4 timelines that place the clips on tracks under a transparent HTML overlay. Each README has the commands for running it from a repository checkout.
47
+ The 30-second reference composition lives in [`examples/demo`](./examples/demo), a focused WebGL shader example lives in [`examples/webgl`](./examples/webgl), the video example in [`examples/video`](./examples/video) includes two embedded-video compositions plus WebM and MP4 timelines that place the clips on tracks under a transparent HTML overlay, [`examples/blend`](./examples/blend) demonstrates static CSS blend modes on composition and video clips, and [`examples/static`](./examples/static) shows a timeless composition rendered as a PNG and reused as a timeline image. Each README has the commands for running it from a repository checkout.
48
48
 
49
49
  ## Project format
50
50
 
@@ -62,37 +62,37 @@ export default {
62
62
  };
63
63
  ```
64
64
 
65
- A composition is a full HTML document with one `<fy-composition>` root. Metadata, default props, markup, CSS, bindings, audio, transcripts, and an optional controller all live in the document:
65
+ A composition is a partial HTML fragment with one top-level `<fy-composition>` root. Metadata, default props, markup, CSS, bindings, audio, transcripts, and an optional controller all live in the fragment; Frameyard generates the document shell at render time:
66
66
 
67
67
  ```html
68
- <!doctype html>
69
- <html>
70
- <head>
71
- <meta charset="utf-8">
72
- <script type="application/json" data-fy="props">
73
- { "title": "Hello", "accent": "#b8f36b" }
74
- </script>
75
- <style>
76
- @keyframes fade-up {
77
- from { opacity: 0; transform: translateY(24px); }
78
- to { opacity: 1; transform: translateY(0); }
79
- }
80
- #title { animation: fade-up 1s ease-out both; }
81
- </style>
82
- </head>
83
- <body>
84
- <fy-composition width="1920" height="1080" fps="30" duration-in-frames="900">
85
- <h1 id="title"
86
- data-fy-text="title"
87
- data-fy-anim-start="0"
88
- data-fy-anim-duration="30"></h1>
89
- <div id="progress" style="width: calc(var(--fy-progress) * 100%)"></div>
90
- </fy-composition>
91
- </body>
92
- </html>
68
+ <script type="application/json" data-fy="props">
69
+ { "title": "Hello", "accent": "#b8f36b" }
70
+ </script>
71
+ <style>
72
+ @keyframes fade-up {
73
+ from { opacity: 0; transform: translateY(24px); }
74
+ to { opacity: 1; transform: translateY(0); }
75
+ }
76
+ #title { animation: fade-up 1s ease-out both; }
77
+ </style>
78
+ <fy-composition width="1920" height="1080" fps="30" duration-in-frames="900">
79
+ <h1 id="title"
80
+ data-fy-text="title"
81
+ data-fy-anim-start="0"
82
+ data-fy-anim-duration="30"></h1>
83
+ <div id="progress" style="width: calc(var(--fy-progress) * 100%)"></div>
84
+ </fy-composition>
93
85
  ```
94
86
 
95
- `width`, `height`, `fps`, and `duration-in-frames` are fixed. Props are JSON-serializable, deep-frozen, and merged shallowly over the `data-fy="props"` defaults. They cannot change metadata.
87
+ Do not add `<!doctype>`, `<html>`, `<head>`, `<body>`, `<title>`, or `<meta>`; the framework owns the document shell, import map, and host runtime. Props, the controller, and import maps are top-level siblings of the root. Top-level `<style>` and `<link rel="stylesheet">` elements are hoisted into the generated head; styles inside the root stay in the body.
88
+
89
+ An optional top-level `<script type="importmap">` can add bare specifiers for controllers. Frameyard hoists it before the host module; `frameyard` and `frameyard/` specifiers are reserved.
90
+
91
+ `width`, `height`, and `fps` are optional: a missing value is inherited from the project `format`, and a declared value always wins. Declare `width` and `height` together or not at all. The same fragment above can therefore be written as `<fy-composition duration-in-frames="900">`, rendering at 1920×1080 and 30 fps because the project fixes that format. Props are JSON-serializable, deep-frozen, and merged shallowly over the `data-fy="props"` defaults. They cannot change metadata.
92
+
93
+ A composition that declares **neither** `duration-in-frames` nor `fps` is static. It has no frame clock, uses width/height only, is captured once, and renders to a PNG; `render hello` writes `out/hello.png`. Its controller may `mount()` and validate props, but `update()` is not called and frame-state variables are not set. CSS animations are paused at time 0 with a warning; `data-fy-anim-*`, `<video>`, `<audio>`, and `<fy-transcript>` are errors. Declaring `fps` without `duration-in-frames` is an error. A static composition used in a timeline behaves as an image clip: declare the clip `duration` and keep `sourceIn: 0`. See [`examples/static`](./examples/static).
94
+
95
+ A composition that declares no dimensions in a project without `format` fails with `COMPOSITION_FORMAT_UNRESOLVED`: declare `width`/`height` or add `format { width, height, fps }` to `video.config.js`. A composition-only project stays valid without `format` as long as its compositions declare their own.
96
96
 
97
97
  ### Timeline
98
98
 
@@ -120,6 +120,7 @@ A timeline is a JSON document that places composition and media clips on video a
120
120
  "source": { "kind": "image", "src": "assets/logo.png" },
121
121
  "start": 30,
122
122
  "duration": 60,
123
+ "blend": "multiply",
123
124
  "transform": { "fit": "contain", "width": 480, "height": 270, "x": 640, "y": -320, "rotation": 0, "opacity": 0.9 }
124
125
  },
125
126
  {
@@ -135,12 +136,12 @@ A timeline is a JSON document that places composition and media clips on video a
135
136
  ```
136
137
 
137
138
  - Sources are `composition` + `id`, `timeline` + `id`, or `video`/`image`/`audio` + project-relative `src`.
138
- - `start`, `sourceIn`, and `duration` use timeline frames. Omitted durations use the rest of the source; image clips require a duration.
139
- - Composition clips can fix `props`, trim the source range, and repeat the same composition with different settings.
140
- - Visual clips support static `fit`, `width`/`height` (an explicit box), `x`, `y`, `scale`, `rotation`, and `opacity`. `fit` applies inside the box; `width` and `height` go together and are mutually exclusive with `scale`. No keyframes, transitions, or effects in 0.6.
139
+ - `start`, `sourceIn`, and `duration` use timeline frames. Omitted durations use the rest of the source; image clips and static composition clips require a duration.
140
+ - Composition clips can fix `props`, trim the source range, and repeat the same composition with different settings. A static composition clip must use `sourceIn: 0`, has no intrinsic duration, and contributes no audio or transcripts.
141
+ - Visual clips support static `fit`, `width`/`height` (an explicit box), `x`, `y`, `scale`, `rotation`, `opacity`, and `blend`. `fit` applies inside the box; `width` and `height` go together and are mutually exclusive with `scale`. `blend` is a CSS `mix-blend-mode` value with source-alpha semantics, from `normal` (the default) through `multiply`, `screen`, `overlay`, the light/dodge/burn family, `difference`, and `exclusion`; `hue`, `saturation`, `color`, and `luminosity` are rejected. No keyframes, transitions, or other effects.
141
142
  - Video, composition, and nested-timeline clips contribute audio automatically unless muted. Timeline audio clips live on audio tracks.
142
- - A composition used in a timeline must match the project fps; different sizes are fitted by the clip. Clips on one track cannot overlap.
143
- - Nested timelines are rendered as groups, so the parent clip's opacity and transform apply to the whole nested result.
143
+ - A composition that declares an `fps` must match the project fps; an undeclared `fps` inherits it. Different sizes are fitted by the clip. Clips on one track cannot overlap.
144
+ - Nested timelines are rendered as groups, so the parent clip's opacity, transform, and blend mode apply to the whole nested result. A blend inside a nested timeline sees only that timeline's own backdrop; a transparent nested timeline does not see the parent's layers.
144
145
 
145
146
  ### Bindings
146
147
 
@@ -208,7 +209,7 @@ The controller lifecycle is `mount → update → dispose`. `update({ frame, pro
208
209
 
209
210
  ## Embedded video and WebGL
210
211
 
211
- Embedded videos are declared in the composition document and frame-seeked by the runtime:
212
+ Embedded videos are declared in the composition fragment and frame-seeked by the runtime:
212
213
 
213
214
  ```html
214
215
  <video
@@ -227,7 +228,7 @@ Embedded videos are declared in the composition document and frame-seeked by the
227
228
  | `data-fy-duration` | Optional seconds of source to use | rest of source |
228
229
  | `data-fy-volume` | Embedded audio volume from `0` to `1` | `1` |
229
230
 
230
- The runtime pauses every video and seeks it to the frame-derived source time. Do not call `play()`, `pause()`, or set `currentTime` in a controller. Videos must be in the composition document's light DOM; shadow roots are not scanned. Video sources are local project-relative files; live streams, remote URLs, and sources that cannot be seeked are not supported. The runtime loads the complete file during the loading phase, so frame production performs no media network requests.
231
+ The runtime pauses every video and seeks it to the frame-derived source time. Do not call `play()`, `pause()`, or set `currentTime` in a controller. Videos must be in the composition fragment's light DOM; shadow roots are not scanned. Video sources are local project-relative files; live streams, remote URLs, and sources that cannot be seeked are not supported. The runtime loads the complete file during the loading phase, so frame production performs no media network requests.
231
232
 
232
233
  An unmuted `<video>` also contributes its soundtrack to the same timeline as declarative audio clips. Frameyard extracts that track with FFmpeg for production and mirrors it in preview, while keeping the video element itself silent. Set `muted` or `data-fy-audio="off"` to use a video as a visual-only asset. See [`examples/video`](./examples/video) for the same composition driven by a VP9/Opus WebM clip and an H.264/AAC MP4 clip.
233
234
 
@@ -271,14 +272,14 @@ Transcript paths resolve from the project root. `words` requires actual measured
271
272
  | --- | --- |
272
273
  | `frameyard init [directory]` | Scaffold a timeline-first HTML/JSON project with an `AGENTS.md` guide and pinned scripts |
273
274
  | `frameyard dev [--port <port>]` | Start the preview server; preview compositions or open a timeline |
274
- | `frameyard check <id> [--props <file>]` | Run static validation, then evaluate every frame of a composition or timeline without capture |
275
- | `frameyard still <id> --frame <spec>` | Capture one or more PNGs through the production path for a composition or timeline |
276
- | `frameyard render <id> [--out <video.mp4>]` | Render a composition directly, or a timeline through the segmented lossless pipeline; defaults to `out/<id>.mp4` |
275
+ | `frameyard check <id> [--props <file>]` | Run static validation, then evaluate every animated frame or the single state of a static composition without capture |
276
+ | `frameyard still <id> --frame <spec>` | Capture one or more PNGs through the production path for a composition or timeline; a static composition exposes frame 0 |
277
+ | `frameyard render <id> [--out <video.mp4>]` | Render an animated composition directly, a static composition to PNG, or a timeline through the segmented lossless pipeline; defaults to `out/<id>.<ext>` |
277
278
  | `frameyard tts ...` | Produce one frozen 24 kHz mono WAV with local qwentts.cpp |
278
279
  | `frameyard transcribe ...` | Produce canonical transcript JSON with optional word timings and SRT/VTT exports |
279
280
  | `frameyard doctor` | Inspect core requirements, optional integrations, and manifest integrity |
280
281
 
281
- Common options: `--project <dir>`, `--json`, `--help`, and `--version`. `still` and `render` accept `--overwrite`. `still` accepts `--frame <spec>`, where `<spec>` is a frame number, an inclusive range such as `2-12`, or a comma-separated list; duplicates are removed preserving the first occurrence, order is preserved, and one invocation captures at most 32 unique frames. With one frame, `--out` is a `.png` file; with multiple frames, it is a directory and the files are named `still-<id>-<frame>.png`, defaulting to the current working directory. Composition renders accept `--concurrency <n>` (1-16); timeline renders accept `--workers <n>` (1-16) and `--scratch <dir>`. `render` also accepts `--container <mp4|mov|mkv|webm>`, `--codec <h264|hevc|vp9>`, and `--encoder <auto|software|nvenc|qsv>`. MP4 and MOV hold H.264 and HEVC with AAC; MKV holds H.264, HEVC, and VP9; WebM holds VP9 with Opus and defaults to VP9. `--out` must use the container's extension, and `render` defaults to `out/<id>.<ext>` accordingly. `--encoder` defaults to `auto`, which probes NVENC, then Quick Sync with a one-frame encode, and falls back to the codec's software encoder; `software` forces `libx264`, `libx265`, or `libvpx-vp9`. For a timeline, `--props` is a JSON object keyed by composition clip id; for a composition, it is the props object. `tts` and `transcribe` accept `--force`. `init` takes the target directory as a positional argument. Explicit output paths passed with `--out` are relative to the current working directory.
282
+ Common options: `--project <dir>`, `--json`, `--help`, and `--version`. `still` and `render` accept `--overwrite`. `still` accepts `--frame <spec>`, where `<spec>` is a frame number, an inclusive range such as `2-12`, or a comma-separated list; duplicates are removed preserving the first occurrence, order is preserved, and one invocation captures at most 32 unique frames. With one frame, `--out` is a `.png` file; with multiple frames, it is a directory and the files are named `still-<id>-<frame>.png`, defaulting to the current working directory. A static composition exposes only frame 0. Animated composition renders accept `--concurrency <n>` (1-16); timeline renders accept `--workers <n>` (1-16) and `--scratch <dir>`. `render` also accepts `--container <mp4|mov|mkv|webm>`, `--codec <h264|hevc|vp9>`, and `--encoder <auto|software|nvenc|qsv>`. MP4 and MOV hold H.264 and HEVC with AAC; MKV holds H.264, HEVC, and VP9; WebM holds VP9 with Opus and defaults to VP9. `--out` must use the container's extension, and `render` defaults to `out/<id>.<ext>` accordingly. A static composition always writes `out/<id>.png` and rejects `--container`, `--codec`, `--encoder`, and `--concurrency`; use a timeline image clip when the deliverable must be video. `--encoder` defaults to `auto`, which probes NVENC, then Quick Sync with a one-frame encode, and falls back to the codec's software encoder; `software` forces `libx264`, `libx265`, or `libvpx-vp9`. For a timeline, `--props` is a JSON object keyed by composition clip id; for a composition, it is the props object. `tts` and `transcribe` accept `--force`. `init` takes the target directory as a positional argument. Explicit output paths passed with `--out` are relative to the current working directory.
282
283
 
283
284
  A render fails immediately on an uncaught error, unhandled rejection, missing required asset, or invalid composition output. It writes to a temporary file and publishes only after FFmpeg succeeds. Existing outputs are not replaced unless `--overwrite` is supplied.
284
285
 
@@ -375,31 +376,31 @@ Production rendering has two targets.
375
376
  A composition renders directly:
376
377
 
377
378
  1. Static composition validation resolves metadata before launching Chromium.
378
- 2. Chromium navigates to the composition document with the composition host.
379
- 3. One session, or several when `--concurrency` is greater than 1, seeks every frame, calls the controller, seeks CSS/WAAPI animations, and waits for embedded media decode and paint.
379
+ 2. Chromium navigates to the served composition document with the composition host.
380
+ 3. For an animated composition, one session, or several when `--concurrency` is greater than 1, seeks every frame, calls the controller, seeks CSS/WAAPI animations, and waits for embedded media decode and paint. A static composition loads once and captures one PNG without seeking frames.
380
381
  4. The viewport is captured through the Chrome DevTools Protocol, piped to FFmpeg, and encoded as H.264, HEVC, or VP9 with AAC or Opus audio.
381
382
 
382
383
  A timeline renders through segments:
383
384
 
384
385
  1. Static timeline validation resolves references, media, nested timelines, durations, props, audio, and overlaps.
385
386
  2. The timeline is split at active-layer changes and a maximum segment duration.
386
- 3. In parallel workers, Chromium captures active composition and nested-timeline clips to alpha-preserving FFV1 intermediates.
387
+ 3. In parallel workers, Chromium captures active composition and nested-timeline clips to alpha-preserving FFV1 intermediates. A static composition is captured once per props variant as a transparent PNG and reused as an image layer across segments.
387
388
  4. FFmpeg applies fit, static transforms, opacity, video/image decoding, and audio mixing, and encodes each segment as H.264, HEVC, or VP9.
388
389
  5. Segments are concatenated in timeline order without re-encoding and the global audio mix is muxed into the final output.
389
390
 
390
- Composition concurrency changes scheduling only. Timeline workers process independent segments; output order and captured composition frames remain deterministic. `check` and `still` use the same clip capture and composite path as their target. A multi-frame `still` captures sequentially, reuses one preview server, and its frames are pixel-identical to capturing each frame separately.
391
+ Composition concurrency changes scheduling only. Timeline workers process independent segments; output order and captured composition frames remain deterministic. `check` and `still` use the same clip capture and composite path as their target. A multi-frame `still` captures sequentially, reuses one preview server, and its frames are pixel-identical to capturing each frame separately. A static composition render performs one capture and does not seek frames.
391
392
 
392
393
  Both targets encode H.264 in MP4 by default. `--container` selects `mp4`, `mov`, `mkv`, or `webm`, `--codec` selects `h264`, `hevc`, or `vp9`, and `--encoder` selects how that codec is encoded: `software` forces `libx264`, `libx265`, or `libvpx-vp9`; `nvenc` and `qsv` drive NVIDIA NVENC and Intel Quick Sync; the default `auto` probes NVENC, then Quick Sync, then falls back to the codec's software encoder. WebM defaults to VP9 and rejects H.264 and HEVC; MP4 and MOV reject VP9. VP9 renders use Opus audio everywhere; H.264 and HEVC use AAC. The probe is a real one-frame encode, so a driver that lists an encoder but cannot open it is skipped instead of failing mid-render. Hardware encoders are faster but produce different encoded bytes and file sizes at the calibrated quality. HEVC in MP4 and MOV uses the `hvc1` sample entry so Apple players accept it, but it does not play in browsers without platform HEVC decoding, so H.264 remains the portable default. Hardware encoders never capture frames, and the segmented timeline pipeline still writes lossless alpha-preserving FFV1 intermediates in software. NVENC allows eight concurrent encode sessions on consumer drivers, so timeline `--workers` is capped at eight when NVENC resolves, with an `ENCODER_WARNING` diagnostic when a larger value is requested. Use `--encoder software` when the encoder choice must not depend on the host GPU, such as CI or a reproducible A/B measurement; encoded bytes still depend on the FFmpeg build and its threading.
393
394
 
394
395
  For the same composition or timeline revision, props, assets, segment plan, frame, and pinned runtime/browser/FFmpeg environment, the captured composition frames and deterministic composite are identical. Encoded bytes are not a reproducibility guarantee, even on the same machine; they depend on the FFmpeg build, encoder threading, and runtime environment. Preview composites in Chromium while timeline production composites with FFmpeg, so small differences in scaling, alpha, and video color conversion are expected; parity is guaranteed at clip capture.
395
396
 
396
- For a repeatable local measurement of seek, capture, encode, and full-render time, run `pnpm bench`. See `bench/README.md` for the measurement recipe. Timeline performance is tracked separately with the long-form benchmarks; the representative target and worst-case measurements are in [ADR 0012](./docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md).
397
+ For a repeatable local measurement of seek, capture, encode, and full-render time, run `pnpm bench`; `pnpm bench:static` compares an animated composition with the same visual rendered as a static PNG. See `bench/README.md` for the measurement recipe. Timeline performance is tracked separately with the long-form benchmarks; the representative target and worst-case measurements are in [ADR 0012](./docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md).
397
398
 
398
399
  ## Current limits
399
400
 
400
401
  Deferred from this implementation slice:
401
402
 
402
- - Timeline transitions, effects, keyframes, blend modes, masks, and audio fades
403
+ - Timeline transitions, filters, keyframes, masks, and audio fades
403
404
  - Full visual timeline editing, drag-and-drop, trim handles, and undo/redo
404
405
  - Timeline-level variables and prop overrides inside nested timelines
405
406
  - Per-timeline formats and frame-rate conversion/retiming
@@ -413,16 +414,19 @@ Deferred from this implementation slice:
413
414
  - Automatic long-text chunking, streaming synthesis, and alternative generated-audio formats
414
415
  - Automatic upstream builds, model downloads, environment repair, and hosted rendering
415
416
  - Unbounded batch still capture and general image-sequence output; `still` accepts at most 32 inspection frames per invocation
417
+ - Audio, transcripts, or embedded video in a static composition; use a timeline
418
+ - A `--video` escape hatch for static composition renders; use a timeline image clip
419
+ - Exporting a fully static timeline as a PNG or recursively optimizing static nested timelines
416
420
  - React or other UI-framework adapters, plugin APIs, and agent-only runtimes
417
421
  - TypeScript transpilation for project compositions and npm dependency optimization
418
422
 
419
- See the [design baseline](./docs/v0.1-design.md) for the full product contract, [ADR 0007](./docs/adr/0007-make-html-documents-the-composition-unit.md) for the HTML authoring decision, [ADR 0010](./docs/adr/0010-make-timelines-the-render-unit.md) for timelines, [ADR 0012](./docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md) for segmented rendering, [ADR 0013](./docs/adr/0013-capture-bounded-multi-frame-stills-for-inspection.md) for bounded multi-frame stills, and the [domain glossary](./GLOSSARY.md) for terminology.
423
+ See the [design baseline](./docs/v0.1-design.md) for the full product contract, [ADR 0007](./docs/adr/0007-make-html-documents-the-composition-unit.md) for the HTML authoring decision, [ADR 0010](./docs/adr/0010-make-timelines-the-render-unit.md) for timelines, [ADR 0012](./docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md) for segmented rendering, [ADR 0013](./docs/adr/0013-capture-bounded-multi-frame-stills-for-inspection.md) for bounded multi-frame stills, [ADR 0016](./docs/adr/0016-author-compositions-as-html-fragments.md) for fragment composition authoring, [ADR 0017](./docs/adr/0017-render-timeless-compositions-as-stills.md) for static composition rendering, the [0.8 migration guide](./docs/migration-0.8.md), and the [domain glossary](./GLOSSARY.md) for terminology.
420
424
 
421
425
  ## Repository layout
422
426
 
423
427
  - `src/contracts.ts`: shared composition, props, audio, transcript, and diagnostic contracts
424
428
  - `src/bindings.ts`: browser-safe binding path and value semantics
425
- - `src/composition/`: browser-safe composition document reader, descriptor, per-kind declarations, and adapter contract
429
+ - `src/composition/`: browser-safe composition grammar reader (document and fragment adapters), descriptor, per-kind declarations, and adapter contract
426
430
  - `src/html/`: Node-side parse5 adapter and static composition validation
427
431
  - `src/transcript.ts`: canonical transcript validation and SRT/VTT formatting
428
432
  - `src/runtime/`: browser host, frame clock, deterministic randomness, bindings, media, diagnostics
@@ -437,6 +441,8 @@ See the [design baseline](./docs/v0.1-design.md) for the full product contract,
437
441
  - `examples/demo/`: the 30-second reference composition
438
442
  - `examples/webgl/`: the focused WebGL shader example
439
443
  - `examples/video/`: embedded-video compositions and WebM/MP4 timelines with video clips under a transparent HTML overlay
444
+ - `examples/blend/`: composition and video clips composited with static CSS blend modes
445
+ - `examples/static/`: a timeless composition rendered as a PNG and reused as a timeline image
440
446
  - `bench/fixtures/`: controlled HTML compositions for the render benchmark
441
447
  - `scripts/`: benchmark, release checks, versioning, and git-hook installation
442
448
  - `docs/adr/`: architecture decision records
package/dist/cli.js CHANGED
@@ -238,7 +238,7 @@ Usage:
238
238
  frameyard dev [--port <port>] [--project <dir>]
239
239
  frameyard check <composition-id> [--props <file>] [--project <dir>] [--json]
240
240
  frameyard still <composition-or-timeline-id> --frame <spec> [--out <file-or-dir>] [--props <file>] [--project <dir>] [--json]
241
- frameyard render <composition-or-timeline-id> [--out <video>] [--props <file>] [--project <dir>] [--concurrency <n>] [--workers <n>] [--encoder <name>] [--codec <name>] [--container <name>] [--scratch <dir>] [--overwrite] [--json]
241
+ frameyard render <composition-or-timeline-id> [--out <file>] [--props <file>] [--project <dir>] [--concurrency <n>] [--workers <n>] [--encoder <name>] [--codec <name>] [--container <name>] [--scratch <dir>] [--overwrite] [--json]
242
242
  frameyard tts --speaker <name> --talker-model <gguf> --codec-model <gguf> --out <file.wav> [--text-file <file>] [--style <text>] [--language <lang>] [--device <name>] [--executable <path>] [--manifest <file>] [--force] [--json]
243
243
  frameyard transcribe <source> --model <gguf> --out <file.json> [--words] [--export srt,vtt] [--language <lang>] [--executable <path>] [--manifest <file>] [--force] [--json]
244
244
  frameyard doctor [--qwen-tts <path>] [--whisper-cli <path>] [--talker-model <gguf>] [--codec-model <gguf>] [--model <gguf>] [--manifest <file>] [--project <dir>] [--json]
@@ -246,9 +246,9 @@ Usage:
246
246
  Options:
247
247
  --project <dir> Use a project other than the current directory.
248
248
  --props <file> Composition props, or a { "<clip-id>": {...} } override map for a timeline.
249
- --out <path> Output path for still, render, tts, or transcribe. For still, one frame uses a .png file and multiple frames use a directory. Render defaults to out/<id>.mp4 in the project.
249
+ --out <path> Output path for still, render, tts, or transcribe. For still, one frame uses a .png file and multiple frames use a directory. Render defaults to out/<id>.mp4 for video and out/<id>.png for a static composition.
250
250
  --frame <spec> Still frames: n, n-m, or a comma-separated list (maximum 32 unique frames).
251
- --concurrency <n> Parallel browser sessions for composition render, 1-${MAX_RENDER_CONCURRENCY} (default: 1).
251
+ --concurrency <n> Parallel browser sessions for animated composition render, 1-${MAX_RENDER_CONCURRENCY} (default: 1).
252
252
  --workers <n> Parallel segment workers for timeline render, 1-${MAX_TIMELINE_WORKERS} (default: 8).
253
253
  --encoder <name> Encoder: auto, software, nvenc, or qsv (default: auto).
254
254
  --codec <name> Delivery codec for render: h264, hevc, or vp9 (default: h264; vp9 for webm).
@@ -287,10 +287,10 @@ function printCommandHelp(command) {
287
287
  process.stdout.write("Usage: frameyard check <composition-id> [--props <file>] [--project <dir>] [--json]\nValidate props, assets, audio, and every frame without encoding.\n");
288
288
  return;
289
289
  case "still":
290
- process.stdout.write("Usage: frameyard still <composition-or-timeline-id> --frame <spec> [--out <path>] [--props <file>] [--project <dir>] [--overwrite] [--json]\nCapture one or more frames through the production rendering path. <spec> is a frame number, an inclusive range like 2-12, or a comma-separated list, with at most 32 unique frames per invocation. With one frame, --out is a .png file; with multiple frames, --out is a directory.\n");
290
+ process.stdout.write("Usage: frameyard still <composition-or-timeline-id> --frame <spec> [--out <path>] [--props <file>] [--project <dir>] [--overwrite] [--json]\nCapture one or more frames through the production rendering path. <spec> is a frame number, an inclusive range like 2-12, or a comma-separated list, with at most 32 unique frames per invocation. With one frame, --out is a .png file; with multiple frames, --out is a directory. A static composition exposes only frame 0.\n");
291
291
  return;
292
292
  case "render":
293
- process.stdout.write(`Usage: frameyard render <composition-or-timeline-id> [--out <video>] [--props <file>] [--project <dir>] [--concurrency <n>] [--workers <n>] [--encoder <name>] [--codec <name>] [--container <name>] [--scratch <dir>] [--overwrite] [--json]\nRender an MP4, MOV, MKV, or WebM with H.264, HEVC, or VP9 video and AAC or Opus audio. The default output is out/<id>.<extension> in the project. Composition concurrency is 1-${MAX_RENDER_CONCURRENCY}; timeline workers are 1-${MAX_TIMELINE_WORKERS}. Encoder choices are auto, software, nvenc, and qsv; the default auto probes NVENC, then Quick Sync, then the codec's software encoder. WebM and VP9 use Opus audio, and HEVC in MP4/MOV keeps the hvc1 sample entry.\n`);
293
+ process.stdout.write(`Usage: frameyard render <composition-or-timeline-id> [--out <file>] [--props <file>] [--project <dir>] [--concurrency <n>] [--workers <n>] [--encoder <name>] [--codec <name>] [--container <name>] [--scratch <dir>] [--overwrite] [--json]\nRender an MP4, MOV, MKV, or WebM with H.264, HEVC, or VP9 video and AAC or Opus audio, or a PNG for a static composition. The default output is out/<id>.<extension> for video and out/<id>.png for a static composition. Animated composition concurrency is 1-${MAX_RENDER_CONCURRENCY}; timeline workers are 1-${MAX_TIMELINE_WORKERS}. Encoder choices are auto, software, nvenc, and qsv; the default auto probes NVENC, then Quick Sync, then the codec's software encoder. WebM and VP9 use Opus audio, and HEVC in MP4/MOV keeps the hvc1 sample entry. A static composition rejects --concurrency, --container, --codec, and --encoder.\n`);
294
294
  return;
295
295
  case "tts":
296
296
  process.stdout.write("Usage: frameyard tts --speaker <name> --talker-model <gguf> --codec-model <gguf> --out <file.wav> [--text-file <file>] [--style <text>] [--language <lang>] [--device <name>] [--executable <path>] [--manifest <file>] [--force] [--json]\nProduce one frozen 24 kHz mono WAV with local qwentts.cpp CustomVoice synthesis.\n");
@@ -559,6 +559,9 @@ async function runCheck(parsed, signal) {
559
559
  if (json) {
560
560
  printJson({ ok: true, command: "check", ...result });
561
561
  }
562
+ else if (result.kind === "static") {
563
+ process.stdout.write(`✓ ${compositionId}: static ${result.metadata.width}×${result.metadata.height}\n`);
564
+ }
562
565
  else {
563
566
  const meta = result.metadata;
564
567
  process.stdout.write(`✓ ${compositionId}: ${result.framesChecked} frames, ${meta.width}×${meta.height} @ ${meta.fps} fps (${formatDuration(meta.durationInFrames / meta.fps)})\n`);
@@ -752,9 +755,11 @@ async function runRender(parsed, signal) {
752
755
  const project = await projectFromFlag(parsed.flags);
753
756
  const props = await readPropsFile(parsed.flags);
754
757
  const encoder = encoderChoice(parsed.flags);
755
- const container = containerChoice(parsed.flags) ?? "mp4";
758
+ const containerValue = containerChoice(parsed.flags);
759
+ const container = containerValue ?? "mp4";
756
760
  // WebM is a VP9/Opus container; every other container defaults to H.264.
757
- const codec = codecChoice(parsed.flags) ?? defaultCodecForContainer(container);
761
+ const codecValue = codecChoice(parsed.flags);
762
+ const codec = codecValue ?? defaultCodecForContainer(container);
758
763
  assertContainerCodec(container, codec);
759
764
  if (encoder !== undefined && encoder !== "auto") {
760
765
  // Rejects codec/vendor mismatches such as --codec hevc --encoder libx264.
@@ -762,9 +767,8 @@ async function runRender(parsed, signal) {
762
767
  }
763
768
  const targetId = parsed.positionals[0];
764
769
  const outValue = flagString(parsed.flags, "out");
765
- const outPath = outValue === undefined
766
- ? path.join(project.root, "out", `${targetId}.${CONTAINER_EXTENSION[container]}`)
767
- : path.resolve(process.cwd(), outValue);
770
+ const explicitOutPath = outValue === undefined ? undefined : path.resolve(process.cwd(), outValue);
771
+ const timelineOutPath = explicitOutPath ?? path.join(project.root, "out", `${targetId}.${CONTAINER_EXTENSION[container]}`);
768
772
  const json = parsed.flags.has("json") === true;
769
773
  if (project.timelines.has(targetId)) {
770
774
  if (parsed.flags.has("concurrency")) {
@@ -786,7 +790,7 @@ async function runRender(parsed, signal) {
786
790
  const result = await renderTimeline({
787
791
  project,
788
792
  timelineId: targetId,
789
- outPath,
793
+ outPath: timelineOutPath,
790
794
  ...(workers !== undefined ? { workers } : {}),
791
795
  ...(scratchDir !== undefined ? { scratchDir } : {}),
792
796
  ...(encoder !== undefined ? { encoder } : {}),
@@ -811,16 +815,16 @@ async function runRender(parsed, signal) {
811
815
  }
812
816
  const compositionId = targetId;
813
817
  const concurrencyValue = parsed.flags.get("concurrency");
814
- const concurrency = validateRenderConcurrency(typeof concurrencyValue === "string" ? Number(concurrencyValue) : undefined);
818
+ const concurrency = typeof concurrencyValue === "string" ? validateRenderConcurrency(Number(concurrencyValue)) : undefined;
815
819
  const result = await renderComposition({
816
820
  project,
817
821
  compositionId,
818
822
  props,
819
- outPath,
820
- concurrency,
823
+ ...(explicitOutPath !== undefined ? { outPath: explicitOutPath } : {}),
824
+ ...(concurrency !== undefined ? { concurrency } : {}),
821
825
  ...(encoder !== undefined ? { encoder } : {}),
822
- codec,
823
- container,
826
+ ...(codecValue !== undefined ? { codec: codecValue } : {}),
827
+ ...(containerValue !== undefined ? { container: containerValue } : {}),
824
828
  overwrite: parsed.flags.has("overwrite") === true,
825
829
  signal,
826
830
  onProgress: progressPrinter("Rendering"),
@@ -829,6 +833,9 @@ async function runRender(parsed, signal) {
829
833
  if (json) {
830
834
  printJson({ ok: true, command: "render", ...result });
831
835
  }
836
+ else if (result.kind === "image") {
837
+ process.stdout.write(`Rendered ${compositionId} to ${result.path} (${result.width}×${result.height}, ${result.bytes} bytes)\n`);
838
+ }
832
839
  else {
833
840
  process.stdout.write(`Rendered ${compositionId} to ${result.path} (${result.framesRendered} frames, ${formatDuration(result.durationSeconds)}, ${result.audio.clips} audio clip${result.audio.clips === 1 ? "" : "s"}, ${result.encoder} encoder)\n`);
834
841
  }