frameyard 0.5.0 → 0.7.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 (128) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/GLOSSARY.md +152 -24
  3. package/README.md +99 -31
  4. package/dist/assets/process.js.map +1 -1
  5. package/dist/assets/provenance.js.map +1 -1
  6. package/dist/cli.js +326 -40
  7. package/dist/cli.js.map +1 -1
  8. package/dist/composition/attributes.d.ts.map +1 -1
  9. package/dist/composition/diagnostics.js.map +1 -1
  10. package/dist/contracts.d.ts +12 -0
  11. package/dist/contracts.d.ts.map +1 -1
  12. package/dist/contracts.js.map +1 -1
  13. package/dist/doctor.d.ts.map +1 -1
  14. package/dist/doctor.js +79 -0
  15. package/dist/doctor.js.map +1 -1
  16. package/dist/errors.d.ts +1 -1
  17. package/dist/errors.d.ts.map +1 -1
  18. package/dist/errors.js.map +1 -1
  19. package/dist/html/validate.js.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/preview/main.js +96 -11
  25. package/dist/preview/main.js.map +1 -1
  26. package/dist/preview/timeline-data.d.ts +85 -0
  27. package/dist/preview/timeline-data.d.ts.map +1 -0
  28. package/dist/preview/timeline-data.js +2 -0
  29. package/dist/preview/timeline-data.js.map +1 -0
  30. package/dist/preview/timeline-host.d.ts +21 -0
  31. package/dist/preview/timeline-host.d.ts.map +1 -0
  32. package/dist/preview/timeline-host.js +308 -0
  33. package/dist/preview/timeline-host.js.map +1 -0
  34. package/dist/preview/timeline-main.d.ts +17 -0
  35. package/dist/preview/timeline-main.d.ts.map +1 -0
  36. package/dist/preview/timeline-main.js +368 -0
  37. package/dist/preview/timeline-main.js.map +1 -0
  38. package/dist/project.d.ts +16 -1
  39. package/dist/project.d.ts.map +1 -1
  40. package/dist/project.js +81 -26
  41. package/dist/project.js.map +1 -1
  42. package/dist/renderer/browser.d.ts +2 -0
  43. package/dist/renderer/browser.d.ts.map +1 -1
  44. package/dist/renderer/browser.js +6 -1
  45. package/dist/renderer/browser.js.map +1 -1
  46. package/dist/renderer/ffmpeg.d.ts +126 -1
  47. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  48. package/dist/renderer/ffmpeg.js +450 -6
  49. package/dist/renderer/ffmpeg.js.map +1 -1
  50. package/dist/renderer/index.d.ts +28 -3
  51. package/dist/renderer/index.d.ts.map +1 -1
  52. package/dist/renderer/index.js +138 -38
  53. package/dist/renderer/index.js.map +1 -1
  54. package/dist/renderer/timeline.d.ts +146 -0
  55. package/dist/renderer/timeline.d.ts.map +1 -0
  56. package/dist/renderer/timeline.js +1098 -0
  57. package/dist/renderer/timeline.js.map +1 -0
  58. package/dist/runtime/host.js.map +1 -1
  59. package/dist/runtime/media.d.ts.map +1 -1
  60. package/dist/runtime/media.js +3 -6
  61. package/dist/runtime/media.js.map +1 -1
  62. package/dist/runtime/video.d.ts +13 -0
  63. package/dist/runtime/video.d.ts.map +1 -0
  64. package/dist/runtime/video.js +41 -0
  65. package/dist/runtime/video.js.map +1 -0
  66. package/dist/scaffold.d.ts.map +1 -1
  67. package/dist/scaffold.js +90 -24
  68. package/dist/scaffold.js.map +1 -1
  69. package/dist/server/composition-html.d.ts.map +1 -1
  70. package/dist/server/preview-html.d.ts.map +1 -1
  71. package/dist/server/preview-html.js +15 -1
  72. package/dist/server/preview-html.js.map +1 -1
  73. package/dist/server/server.d.ts.map +1 -1
  74. package/dist/server/server.js +39 -1
  75. package/dist/server/server.js.map +1 -1
  76. package/dist/server/timeline-preview-data.d.ts +4 -0
  77. package/dist/server/timeline-preview-data.d.ts.map +1 -0
  78. package/dist/server/timeline-preview-data.js +97 -0
  79. package/dist/server/timeline-preview-data.js.map +1 -0
  80. package/dist/server/timeline-preview-html.d.ts +4 -0
  81. package/dist/server/timeline-preview-html.d.ts.map +1 -0
  82. package/dist/server/timeline-preview-html.js +227 -0
  83. package/dist/server/timeline-preview-html.js.map +1 -0
  84. package/dist/timeline/plan.d.ts +32 -0
  85. package/dist/timeline/plan.d.ts.map +1 -0
  86. package/dist/timeline/plan.js +68 -0
  87. package/dist/timeline/plan.js.map +1 -0
  88. package/dist/timeline/resolve.d.ts +60 -0
  89. package/dist/timeline/resolve.d.ts.map +1 -0
  90. package/dist/timeline/resolve.js +523 -0
  91. package/dist/timeline/resolve.js.map +1 -0
  92. package/dist/timeline/schema.d.ts +71 -0
  93. package/dist/timeline/schema.d.ts.map +1 -0
  94. package/dist/timeline/schema.js +425 -0
  95. package/dist/timeline/schema.js.map +1 -0
  96. package/dist/transcript.js.map +1 -1
  97. package/dist/version.d.ts +1 -1
  98. package/dist/version.js +1 -1
  99. package/docs/adr/0002-make-frame-and-props-the-only-animation-state.md +2 -0
  100. package/docs/adr/0005-keep-parallel-rendering-opt-in-and-frame-ordered.md +2 -0
  101. package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
  102. package/docs/adr/0009-composite-production-frames-in-software.md +1 -1
  103. package/docs/adr/0010-make-timelines-the-render-unit.md +24 -0
  104. package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +22 -0
  105. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +31 -0
  106. package/docs/adr/0013-capture-bounded-multi-frame-stills-for-inspection.md +33 -0
  107. package/docs/preview-testing.md +53 -0
  108. package/docs/v0.1-design.md +138 -87
  109. package/examples/demo/README.md +10 -3
  110. package/examples/demo/timelines/demo.timeline.json +12 -0
  111. package/examples/demo/video.config.js +4 -0
  112. package/examples/video/README.md +50 -6
  113. package/examples/video/compositions/video-overlay.html +350 -0
  114. package/examples/video/props/video-timeline.json +10 -0
  115. package/examples/video/timelines/video-timeline-mp4.timeline.json +44 -0
  116. package/examples/video/timelines/video-timeline.timeline.json +44 -0
  117. package/examples/video/video.config.js +6 -0
  118. package/package.json +5 -3
  119. package/skills/frameyard/README.md +7 -5
  120. package/skills/frameyard/SKILL.md +19 -13
  121. package/skills/frameyard/references/common-mistakes.md +1 -0
  122. package/skills/frameyard/references/deterministic-authoring.md +2 -1
  123. package/skills/frameyard/references/diagnostics.md +1 -1
  124. package/skills/frameyard/references/failure-recovery.md +3 -2
  125. package/skills/frameyard/references/json-results.md +15 -6
  126. package/skills/frameyard/references/subtitles-karaoke.md +2 -2
  127. package/skills/frameyard/references/timelines.md +143 -0
  128. package/skills/frameyard/scripts/frameyard.mjs +20 -3
package/CHANGELOG.md CHANGED
@@ -8,6 +8,106 @@ 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.7.0] - 2026-11-02
12
+
13
+ ### Added
14
+
15
+ - `render --container <mp4|mov|mkv|webm>`,
16
+ `--codec <h264|hevc|vp9>`, and
17
+ `--encoder <auto|software|nvenc|qsv>` select the delivery container, codec,
18
+ and encoder for composition and timeline renders. `auto` is the default and
19
+ probes NVENC, then Quick Sync, then the codec's software encoder. MP4 with
20
+ H.264 stays the default and WebM defaults to VP9 with Opus; HEVC keeps the
21
+ `hvc1` sample entry in MP4/MOV, the output extension must match the
22
+ container, timeline workers are capped at the NVENC session limit, and render
23
+ results report the resolved `container`, `codec`, and `encoder`.
24
+ - `still --frame` accepts a frame number, an inclusive range, or a
25
+ comma-separated list and captures up to 32 unique frames in one invocation.
26
+ Multi-frame output writes
27
+ `still-<id>-<frame>.png` files to a directory; all frames are captured to
28
+ temporary files before any file is published.
29
+ - `doctor` adds optional `ffmpeg-opus-encoder` and `ffmpeg-video-encoders`
30
+ checks. The video check proves each supported encoder opens with a one-frame
31
+ encode and reports advertised encoders that the driver or device cannot
32
+ open; neither optional check affects `healthy`.
33
+
34
+ ### Changed
35
+
36
+ - `still` results always include `frames`; `frame` and `path` remain only when
37
+ exactly one frame was captured. The `--frame` contract, multi-frame `--out`
38
+ directory behavior, and the 32-frame inspection cap are documented in
39
+ ADR 0013. Single-frame output paths and pixels are unchanged.
40
+ - Typechecking uses TypeScript 7.0.2's native compiler. The three `tsconfig`
41
+ projects pass unchanged and emit the same JavaScript and declarations.
42
+
43
+ ### Fixed
44
+
45
+ - Embedded video settles on a presented frame in both composition capture and
46
+ timeline preview, using one shared paint barrier with animation-frame and
47
+ timeout fallbacks.
48
+ - Preview iframes pin a light color scheme to match the production capture
49
+ context, so transparent compositions composite over the same base color.
50
+ - Arrow and Shift+Arrow transport keys pressed inside composition and timeline
51
+ layer iframes reach the preview transport, while text entry does not step
52
+ frames across iframe boundaries.
53
+
54
+ [0.7.0]: https://github.com/vladiantio/frameyard/compare/v0.6.0...v0.7.0
55
+
56
+ ## [0.6.0] - 2026-11-01
57
+
58
+ ### Added
59
+
60
+ - JSON timelines with project-fixed format, video/audio tracks, composition,
61
+ media, and nested-timeline clips, source ranges, per-clip props, static
62
+ transforms, audio settings, and derived or overridden duration.
63
+ - Timeline validation: strict schema, references, composition static
64
+ validation, media probing, frame-rate checks, source bounds, overlaps,
65
+ nested cycles, and structured diagnostics.
66
+ - Segmented timeline rendering: Chromium captures composition and
67
+ nested-timeline clips to alpha-preserving FFV1 intermediates; FFmpeg
68
+ composites and encodes segments; segments are concatenated in order; audio
69
+ is mixed once and muxed.
70
+ - Transparent composition capture and nested timeline groups with recursive
71
+ visual, still, check, and audio handling.
72
+ - Timeline preview in `dev`: composition and timeline selectors, program
73
+ monitor, read-only tracks and clips, playhead, playback, frame stepping, and
74
+ one-second jumps with buttons or arrow keys, audio controls, and diagnostics.
75
+ - Timeline visual transforms accept `width`/`height` as an explicit clip box,
76
+ mutually exclusive with `scale`.
77
+ - `render`, `check`, and `still` accept timeline IDs alongside composition
78
+ IDs. Timeline renders add `--workers` and `--scratch`; timeline `--props`
79
+ is a map keyed by composition clip ID.
80
+ - `video.config.js` supports an optional project `format` and explicit
81
+ `timelines` map. `project.json` publishes `format` and `timelines`.
82
+ - Timeline-first `init` scaffold with a composition clip, timeline override
83
+ props, and `main` commands.
84
+ - The video example adds WebM and MP4 timelines that place video clips under a
85
+ transparent HTML overlay.
86
+ - Official skill reference for timelines, clips, tracks, nested timelines,
87
+ props overrides, commands, preview, and determinism.
88
+ - Timeline config, schema, resolution, planning, rendering, preview, and
89
+ runtime preview tests.
90
+
91
+ ### Changed
92
+
93
+ - `render` defaults to `out/<id>.mp4` for timelines and compositions.
94
+ - `check` and `still` use timeline clip capture and composite behavior for
95
+ timeline targets.
96
+ - Timeline audio mixing pads short inputs to the full timeline duration.
97
+ - Timed timeline layers are normalized to the project frame rate before
98
+ compositing.
99
+ - The design baseline, glossary, and architecture decision records describe
100
+ timelines as the render unit and compositions as clip sources.
101
+
102
+ ### Fixed
103
+
104
+ - Composition audio, embedded video audio, and nested timeline audio are
105
+ trimmed and shifted by clip source ranges and groups.
106
+ - Selecting a different composition in the preview picker reloads the iframe
107
+ instead of leaving the previous composition loaded.
108
+
109
+ [0.6.0]: https://github.com/vladiantio/frameyard/compare/v0.5.0...v0.6.0
110
+
11
111
  ## [0.5.0] - 2026-10-31
12
112
 
13
113
  ### Added
package/GLOSSARY.md CHANGED
@@ -1,21 +1,25 @@
1
1
  # Frameyard
2
2
 
3
- Frameyard is a local-first framework for creating short, parameterized videos with HTML, CSS, and JavaScript. Its domain separates reusable compositions from the fixed inputs and deterministic renders that turn them into videos.
3
+ Frameyard is a local-first framework for creating short, parameterized videos with HTML, CSS, and JavaScript. Its domain separates reusable compositions from the timelines that assemble them into a video, and from the fixed inputs and deterministic renders that turn those timelines into output.
4
4
 
5
5
  ## Language
6
6
 
7
7
  ### Authoring
8
8
 
9
9
  **Project**:
10
- A collection of related compositions and the assets and configuration they share.
10
+ A collection of related compositions, timelines, and the assets and configuration they share.
11
11
  _Avoid_: Video, render
12
12
 
13
+ **Project format**:
14
+ The fixed width, height, and frame rate shared by every timeline in a project.
15
+ _Avoid_: Composition metadata, render options
16
+
13
17
  **Composition**:
14
- A uniquely identified, reusable video definition with fixed dimensions, frame rate, duration in frames, and content parameterized by props.
18
+ A uniquely identified, reusable video source with fixed dimensions, frame rate, duration in frames, and content parameterized by props.
15
19
  _Avoid_: Scene, template, video
16
20
 
17
21
  **Composition metadata**:
18
- The fixed dimensions, frame rate, and duration in frames that define a composition's output contract.
22
+ The fixed dimensions, frame rate, and duration in frames that define a composition's source contract.
19
23
  _Avoid_: Render options, props
20
24
 
21
25
  **Composition descriptor**:
@@ -38,6 +42,10 @@ _Avoid_: Build, release
38
42
  A composition whose source is an HTML document.
39
43
  _Avoid_: HTML template, page
40
44
 
45
+ **Transparent composition**:
46
+ A composition that does not paint a background, so a timeline clip can reveal the layers below it.
47
+ _Avoid_: Alpha composition, overlay
48
+
41
49
  **Composition controller**:
42
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.
43
51
  _Avoid_: Composition module, plugin
@@ -55,25 +63,113 @@ An HTML attribute that maps a prop into text, an attribute, a class, or element
55
63
  _Avoid_: Template expression, interpolation
56
64
 
57
65
  **Props**:
58
- The immutable, JSON-serializable inputs supplied to a composition for one render.
66
+ The immutable, JSON-serializable inputs supplied to a composition for one load; a timeline clip can fix the props for its instance of the composition.
59
67
  _Avoid_: Options, settings, mutable state
60
68
 
61
69
  **Asset**:
62
- A local file required by a composition, whether static or generated.
70
+ A local file required by a composition or timeline, whether static or generated.
63
71
  _Avoid_: Resource, media file
64
72
 
73
+ **Timeline**:
74
+ A top-level edit that assembles tracks and clips into one video under the project format, with a duration derived from its clips or explicitly overridden.
75
+ _Avoid_: Sequence, composition, project
76
+
77
+ **Timeline document**:
78
+ The project-local JSON document that declares a timeline's tracks, clips, optional duration, and background.
79
+ _Avoid_: Composition, config
80
+
81
+ **Track**:
82
+ An ordered layer in a timeline. Video tracks composite; audio tracks mix.
83
+ _Avoid_: Channel, lane
84
+
85
+ **Video track**:
86
+ A track whose clips contribute visual layers, ordered from bottom to top.
87
+ _Avoid_: Visual lane
88
+
89
+ **Audio track**:
90
+ A track whose clips are audio-only and contribute additively to the timeline audio mix.
91
+ _Avoid_: Soundtrack
92
+
93
+ **Clip**:
94
+ A placement of a clip source on a track, with a timeline start, source in-point, duration, and presentation settings.
95
+ _Avoid_: Segment, item, block
96
+
97
+ **Clip source**:
98
+ The composition, nested timeline, or local media asset that a clip presents.
99
+ _Avoid_: Media, asset
100
+
101
+ **Composition clip**:
102
+ A clip whose source is a composition, with fixed props for that instance.
103
+ _Avoid_: Composition instance, nested composition
104
+
105
+ **Media clip**:
106
+ A clip whose source is a local video, image, or audio asset.
107
+ _Avoid_: Asset clip, raw clip
108
+
109
+ **Nested timeline clip**:
110
+ A clip whose source is another timeline.
111
+ _Avoid_: Nested sequence, compound clip
112
+
113
+ **Source in-point**:
114
+ The first source frame a clip uses.
115
+ _Avoid_: Trim start, offset
116
+
117
+ **Timeline frame**:
118
+ An integer-addressed position on a timeline's master frame grid.
119
+ _Avoid_: Composition frame, timestamp
120
+
121
+ **Composition frame**:
122
+ An integer-addressed position within a composition; the frame a controller receives.
123
+ _Avoid_: Timeline frame
124
+
125
+ **Timeline duration**:
126
+ The number of timeline frames a timeline produces, derived from its clips or explicitly overridden.
127
+ _Avoid_: Composition duration, length
128
+
129
+ **Timeline background**:
130
+ The optional color behind all layers of a timeline; it is transparent by default and the final video composites it over black.
131
+ _Avoid_: Canvas, backdrop
132
+
133
+ **Fit policy**:
134
+ How a clip source's natural frame is fitted into the project frame or an explicit clip box: contained, covering, or stretched.
135
+ _Avoid_: Resize mode, object-fit
136
+
137
+ **Clip transform**:
138
+ 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
+ _Avoid_: Effect, animation
140
+
141
+ **Nested timeline**:
142
+ A timeline used as a clip source inside another timeline.
143
+ _Avoid_: Compound composition
144
+
65
145
  ### Deterministic execution
66
146
 
67
147
  **Frame**:
68
- An integer-addressed position on a composition's timeline.
148
+ An integer-addressed position on a timeline or composition frame range; use timeline frame or composition frame when the distinction matters.
69
149
  _Avoid_: Timestamp, animation tick
70
150
 
151
+ **Active clip**:
152
+ A clip whose timeline range contains the current timeline frame.
153
+ _Avoid_: Visible clip
154
+
155
+ **Clip capture**:
156
+ The pixel frames a composition produces for a timeline clip before the timeline compositor combines layers.
157
+ _Avoid_: Screenshot, render
158
+
159
+ **Timeline composite**:
160
+ The deterministic combination of a timeline's layers into output frames.
161
+ _Avoid_: Browser composite, preview
162
+
163
+ **Render segment**:
164
+ A bounded span of a timeline that is rendered and encoded independently, then joined in timeline order.
165
+ _Avoid_: Chunk, part
166
+
71
167
  **Deterministic execution**:
72
- The rule that a composition's visual state at any frame depends only on that frame, its props, and pre-loaded data and assets.
168
+ The rule that a timeline frame's visual state depends only on that timeline frame, its clip sources and their props, and pre-loaded data and assets; a composition frame depends only on its composition frame, props, and pre-loaded data and assets.
73
169
  _Avoid_: Playback-driven animation
74
170
 
75
171
  **Frame clock**:
76
- The single source of animation time, derived from the current frame and the composition's frame rate.
172
+ The single source of animation time within a composition, derived from its composition frame and frame rate; the preview host or the production segment renderer drives it by seeking each active composition clip.
77
173
  _Avoid_: Wall clock, playback timer
78
174
 
79
175
  **Frame-state variable**:
@@ -81,7 +177,7 @@ A CSS custom property whose value is derived from the current composition frame.
81
177
  _Avoid_: Animation variable, clock variable
82
178
 
83
179
  **Seek safety**:
84
- The property that any frame can be produced correctly without depending on previously produced frames.
180
+ The property that any timeline frame can be produced correctly without depending on previously produced timeline frames, by seeking each active clip to its source frame.
85
181
  _Avoid_: History-dependent updates
86
182
 
87
183
  **Deterministic randomness**:
@@ -89,15 +185,15 @@ Random values derived statelessly from a key so they do not depend on call order
89
185
  _Avoid_: Unseeded randomness
90
186
 
91
187
  **Frame production**:
92
- The phase in which the runtime produces the visual state of a requested frame, without network access or model inference.
188
+ The phase in which the runtime produces the visual state of a requested timeline frame, without network access or model inference.
93
189
  _Avoid_: Real-time playback
94
190
 
95
191
  **Loading phase**:
96
- The phase before frame production when required data and assets become available.
192
+ The phase before frame production when all clip sources, compositions, media, data, and assets become available.
97
193
  _Avoid_: Per-frame fetching
98
194
 
99
195
  **Embedded media**:
100
- A local video file placed in a composition whose presentation time is derived from the composition frame, with optional frame offset and source trim.
196
+ A local video file placed inside a composition whose presentation time is derived from the composition frame, with optional frame offset and source trim.
101
197
  _Avoid_: Live stream, video track
102
198
 
103
199
  **Media settle**:
@@ -110,42 +206,74 @@ _Avoid_: GPU animation, live render
110
206
 
111
207
  ### Audio
112
208
 
113
- **Audio clip**:
114
- An audio source placed on a composition's timeline with a start frame, source trim, and volume.
115
- _Avoid_: Audio track
209
+ **Composition audio clip**:
210
+ An audio source declared inside a composition and placed by composition frames; it joins the timeline mix when the composition is used as a clip.
211
+ _Avoid_: Audio clip, audio track
212
+
213
+ **Timeline audio clip**:
214
+ An audio-only clip placed on a timeline audio track.
215
+ _Avoid_: Audio clip, soundtrack
116
216
 
117
217
  **Audio mix**:
118
- The single offline mix of a composition's audio clips for one render.
218
+ The single offline mix of a timeline's audible clips for one render.
119
219
  _Avoid_: Soundtrack, audio track, final cut
120
220
 
221
+ **Embedded audio**:
222
+ The audio track of an embedded video, contributed to the mix unless the video or clip is muted.
223
+ _Avoid_: Live audio
224
+
121
225
  ### Rendering
122
226
 
123
227
  **Composition host**:
124
228
  The injected runtime that loads an HTML composition, applies its bindings, and exposes its frame clock to preview and render.
125
229
  _Avoid_: Preview runtime, render runtime
126
230
 
231
+ **Timeline host**:
232
+ The preview runtime that loads a timeline, drives a bounded pool of active clip sources, and exposes the timeline frame clock to the preview.
233
+ _Avoid_: Timeline runtime, production renderer, editor
234
+
235
+ **Timeline renderer**:
236
+ The Node-side segmented renderer that captures composition clips, composites and encodes timeline segments, and joins them into one video.
237
+ _Avoid_: Timeline host, browser renderer
238
+
127
239
  **Preview**:
128
- An interactive, frame-accurate inspection of a composition using the same composition behavior as a render.
240
+ An interactive, frame-accurate inspection of a timeline or composition using the same behavior as a render, including a program monitor and a read-only timeline view.
129
241
  _Avoid_: Visual editor
130
242
 
243
+ **Playhead**:
244
+ The current timeline frame in preview.
245
+ _Avoid_: Cursor, scrubber
246
+
247
+ **Program monitor**:
248
+ The preview surface that shows the timeline at the playhead.
249
+ _Avoid_: Stage, canvas
250
+
131
251
  **Check**:
132
- A validation operation that first runs static composition validation, then loads a composition and evaluates every frame without capturing or encoding.
252
+ A validation operation that first runs static validation, then loads a timeline or composition and evaluates every frame without capturing or encoding.
133
253
  _Avoid_: Test render, dry run
134
254
 
135
255
  **Static composition validation**:
136
256
  The Node-side validation of a composition document's metadata, bindings, controller declaration, scripts, and asset references before browser frame production.
137
257
  _Avoid_: Lint, preflight
138
258
 
259
+ **Static timeline validation**:
260
+ The Node-side validation of a timeline document's tracks, clips, references, nested timelines, media, and referenced compositions before browser frame production.
261
+ _Avoid_: Lint, preflight
262
+
139
263
  **Composition diagnostic**:
140
264
  A structured error or warning about a composition's source, metadata, bindings, controller, assets, or frame production, with a stable code and optional source location.
141
265
  _Avoid_: Log message, error string
142
266
 
267
+ **Timeline diagnostic**:
268
+ A structured error or warning about a timeline's source, tracks, clips, references, media, or frame production, with a stable code and optional source location.
269
+ _Avoid_: Log message, error string
270
+
143
271
  **Render**:
144
- The production of one complete video from a composition and a fixed set of props.
272
+ The production of one complete video from a timeline or a standalone composition with fixed inputs.
145
273
  _Avoid_: Recording, export
146
274
 
147
275
  **Render concurrency**:
148
- The optional number of independent browser sessions used to seek and capture frames during a render. Frames are still encoded in timeline order, and concurrency must not change visual state.
276
+ 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.
149
277
  _Avoid_: Parallel composition, out-of-order output
150
278
 
151
279
  **Video**:
@@ -153,11 +281,11 @@ The finished audiovisual output of a successful render.
153
281
  _Avoid_: Composition, project
154
282
 
155
283
  **Still**:
156
- A single frame captured as a PNG through the production rendering path for debugging.
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.
157
285
  _Avoid_: Frame export, image sequence
158
286
 
159
287
  **Reproducibility**:
160
- The guarantee that, for the same composition revision, props, assets, and frame under a pinned runtime/browser environment, the visual state is identical. It does not imply identical encoded bytes, even on the same machine; encoded output depends on the encoder and its runtime.
288
+ The guarantee that, for the same timeline or composition revision, props, assets, and frame under a pinned runtime/browser environment, the visual state is identical. It does not imply identical encoded bytes, even on the same machine; encoded output depends on the encoder and its runtime.
161
289
  _Avoid_: Bit-for-bit encoding equality
162
290
 
163
291
  ### Agent operation
@@ -173,7 +301,7 @@ _Avoid_: Agent runtime, MCP server
173
301
  ### Local asset production
174
302
 
175
303
  **Generated asset**:
176
- A frozen local input produced by speech synthesis or transcription before a composition is loaded or rendered.
304
+ A frozen local input produced by speech synthesis or transcription before a timeline or composition is loaded or rendered.
177
305
  _Avoid_: Per-frame generation
178
306
 
179
307
  **Provenance manifest**:
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Frameyard
2
2
 
3
- Frameyard is a local-first framework for building short, parameterized videos with HTML, CSS, and an optional JavaScript controller, and rendering them deterministically.
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.4 design is implemented: HTML composition documents, declarative bindings, CSS animation scheduling, frame-state variables, inline and external controllers, the composition host, static validation with structured diagnostics, iframe preview, direct render/check/still sessions, audio and transcripts, deterministic Chromium capture, FFmpeg output, and the official agent skill.
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.
6
6
 
7
7
  ## Requirements
8
8
 
9
9
  - Node.js 22.12 or newer
10
- - FFmpeg and FFprobe on `PATH`, built with H.264 (`libx264`) and AAC support
10
+ - FFmpeg and FFprobe on `PATH`, built with H.264 (`libx264`), AAC, HEVC (`libx265`), VP9 (`libvpx-vp9`), and Opus (`libopus`) support. The software HEVC and VP9 encoders are only used for software renders; `--encoder nvenc` and `--encoder qsv` need no software encoder for that codec.
11
11
  - The Playwright-managed Chromium build
12
12
  - Optional: `qwen-tts` from [`qwentts.cpp`](https://github.com/ServeurpersoCom/qwentts.cpp) for local speech synthesis
13
13
  - Optional: `whisper-cli` from [`whisper.cpp`](https://github.com/ggml-org/whisper.cpp) for local transcription
@@ -26,7 +26,7 @@ cd my-video
26
26
  npx frameyard dev
27
27
  ```
28
28
 
29
- `dev` prints a local preview URL. Select a composition, edit its JSON props, scrub frames, and play the composition with its audio timeline.
29
+ `dev` prints a local preview URL. Preview a composition with JSON props editing, or open the timeline for a program monitor, read-only tracks and clips, scrubbing, playback, frame-by-frame stepping, one-second jumps, and diagnostics.
30
30
 
31
31
  The scaffold includes an `AGENTS.md` guide for coding agents working in the project, covering the HTML authoring rules and the `check`, `still`, and `render` workflow. It also includes a `package.json` with `dev`, `check`, `render`, and `still` scripts pinned to the scaffolding CLI version, so `pnpm dev` or `npm run dev` works without a global install, and a `.gitignore` that ignores `out/`.
32
32
 
@@ -36,24 +36,29 @@ Validate and render:
36
36
 
37
37
  ```sh
38
38
  npx frameyard doctor
39
- npx frameyard check hello
40
- npx frameyard render hello --props props/hello.json
41
- npx frameyard still hello --frame 42 --out hello-frame.png
39
+ npx frameyard check main
40
+ npx frameyard render main --props props/main.json --workers 8 --scratch /path/to/scratch
41
+ npx frameyard still main --frame 42 --out main-frame.png
42
+ npx frameyard still main --frame 0,118-126 --out out/stills
42
43
  ```
43
44
 
44
- `render` writes the video to `out/hello.mp4` in the project by default; pass `--out <file.mp4>` to choose another path. The scaffold's `.gitignore` ignores `out/`.
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/`.
45
46
 
46
- The 30-second reference composition lives in [`examples/demo`](./examples/demo), a focused WebGL shader example lives in [`examples/webgl`](./examples/webgl), and a two-pass embedded-video example with WebM and MP4 sources lives in [`examples/video`](./examples/video). 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), 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
48
 
48
49
  ## Project format
49
50
 
50
- A project is ordinary HTML, CSS, assets, and optional native JavaScript controller modules. `video.config.js` explicitly lists composition entry points; there is no directory scanning:
51
+ A project is ordinary HTML, CSS, JSON timelines, assets, and optional native JavaScript controller modules. `video.config.js` explicitly lists the project format, composition entry points, and timeline entry points; there is no directory scanning:
51
52
 
52
53
  ```js
53
54
  export default {
55
+ format: { width: 1920, height: 1080, fps: 30 },
54
56
  compositions: {
55
57
  hello: "./compositions/hello.html",
56
58
  },
59
+ timelines: {
60
+ main: "./timelines/main.timeline.json",
61
+ },
57
62
  };
58
63
  ```
59
64
 
@@ -89,6 +94,54 @@ A composition is a full HTML document with one `<fy-composition>` root. Metadata
89
94
 
90
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.
91
96
 
97
+ ### Timeline
98
+
99
+ A timeline is a JSON document that places composition and media clips on video and audio tracks. It inherits the project format and derives its duration from the latest clip end unless it declares `durationInFrames`:
100
+
101
+ ```json
102
+ {
103
+ "version": 1,
104
+ "tracks": [
105
+ { "id": "v1", "kind": "video" },
106
+ { "id": "v2", "kind": "video" },
107
+ { "id": "a1", "kind": "audio" }
108
+ ],
109
+ "clips": [
110
+ {
111
+ "id": "hello",
112
+ "track": "v1",
113
+ "source": { "kind": "composition", "id": "hello" },
114
+ "start": 0,
115
+ "duration": 150
116
+ },
117
+ {
118
+ "id": "logo",
119
+ "track": "v2",
120
+ "source": { "kind": "image", "src": "assets/logo.png" },
121
+ "start": 30,
122
+ "duration": 60,
123
+ "transform": { "fit": "contain", "width": 480, "height": 270, "x": 640, "y": -320, "rotation": 0, "opacity": 0.9 }
124
+ },
125
+ {
126
+ "id": "music",
127
+ "track": "a1",
128
+ "source": { "kind": "audio", "src": "assets/music.wav" },
129
+ "start": 0,
130
+ "duration": 150,
131
+ "audio": { "gain": 0.5 }
132
+ }
133
+ ]
134
+ }
135
+ ```
136
+
137
+ - 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.
141
+ - 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.
144
+
92
145
  ### Bindings
93
146
 
94
147
  - `data-fy-text="path"` sets text content.
@@ -216,16 +269,16 @@ Transcript paths resolve from the project root. `words` requires actual measured
216
269
 
217
270
  | Command | Purpose |
218
271
  | --- | --- |
219
- | `frameyard init [directory]` | Scaffold a minimal HTML project, including an `AGENTS.md` agent guide and pinned `package.json` scripts |
220
- | `frameyard dev [--port <port>]` | Start the preview server |
221
- | `frameyard check <id> [--props <file>]` | Run static validation, then evaluate every frame without capture |
222
- | `frameyard still <id> --frame <n>` | Capture one PNG through the production path |
223
- | `frameyard render <id> [--out <video.mp4>]` | Capture frames with Chromium and encode with FFmpeg, optionally in parallel; defaults to `out/<id>.mp4` in the project |
272
+ | `frameyard init [directory]` | Scaffold a timeline-first HTML/JSON project with an `AGENTS.md` guide and pinned scripts |
273
+ | `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` |
224
277
  | `frameyard tts ...` | Produce one frozen 24 kHz mono WAV with local qwentts.cpp |
225
278
  | `frameyard transcribe ...` | Produce canonical transcript JSON with optional word timings and SRT/VTT exports |
226
279
  | `frameyard doctor` | Inspect core requirements, optional integrations, and manifest integrity |
227
280
 
228
- Common options: `--project <dir>`, `--json`, `--help`, and `--version`. `still` and `render` accept `--overwrite`; `render` also accepts `--concurrency <n>` (1-16); `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; `render` defaults to `out/<id>.mp4` inside the project root.
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.
229
282
 
230
283
  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.
231
284
 
@@ -317,26 +370,41 @@ The optional host-neutral agent skill lives in [`skills/frameyard/`](./skills/fr
317
370
 
318
371
  ## Rendering and reproducibility
319
372
 
320
- Production rendering:
373
+ Production rendering has two targets.
374
+
375
+ A composition renders directly:
376
+
377
+ 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.
380
+ 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
+ A timeline renders through segments:
383
+
384
+ 1. Static timeline validation resolves references, media, nested timelines, durations, props, audio, and overlaps.
385
+ 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
+ 4. FFmpeg applies fit, static transforms, opacity, video/image decoding, and audio mixing, and encodes each segment as H.264, HEVC, or VP9.
388
+ 5. Segments are concatenated in timeline order without re-encoding and the global audio mix is muxed into the final output.
321
389
 
322
- 1. Runs static composition validation and resolves metadata before launching Chromium.
323
- 2. Navigates directly to the composition document with the composition host.
324
- 3. Launches one pinned Chromium session, or several independent sessions when `--concurrency` is greater than 1.
325
- 4. Seeks explicitly to each frame, calls the controller, seeks CSS/WAAPI animations, and waits for embedded media decode and browser paint.
326
- 5. Captures the viewport through the Chrome DevTools Protocol.
327
- 6. Pipes PNG frames in timeline order and the offline audio mix to FFmpeg.
328
- 7. Encodes H.264 video with `yuv420p` and AAC audio into an MP4.
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.
329
391
 
330
- Rendering is sequential by default. Pass `--concurrency <n>` (1-16) to seek and capture frames with up to `n` independent browser sessions; the effective value is capped by the composition's frame count. Captured frames are always encoded in timeline order, so parallel and sequential renders of the same inputs produce equivalent visual frames. Concurrency only changes scheduling: it does not change composition behavior or the determinism contract. Parallel rendering uses more CPU and memory, so choose a value appropriate for the machine.
392
+ 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.
331
393
 
332
- For the same composition revision, props, assets, frame, and pinned runtime/browser environment, the visual state is identical. Encoded bytes are not a reproducibility guarantee, even on the same machine; they depend on the FFmpeg build, encoder threading, and runtime environment.
394
+ 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.
333
395
 
334
- For a repeatable local measurement of seek, capture, encode, and full-render time, run `pnpm bench`. See `bench/README.md` for the measurement recipe and browser-flag experiments. The demo and the controlled fixtures under `bench/fixtures/` are the reference workloads.
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).
335
397
 
336
398
  ## Current limits
337
399
 
338
400
  Deferred from this implementation slice:
339
401
 
402
+ - Timeline transitions, effects, keyframes, blend modes, masks, and audio fades
403
+ - Full visual timeline editing, drag-and-drop, trim handles, and undo/redo
404
+ - Timeline-level variables and prop overrides inside nested timelines
405
+ - Per-timeline formats and frame-rate conversion/retiming
406
+ - Persistent clip caches; timeline scratch intermediates are cleaned after a render
407
+ - GPU compositing and codecs beyond H.264, HEVC, and VP9
340
408
  - Built-in subtitle rendering, automatic caption burn-in, and declarative karaoke elements
341
409
  - Nested `data-fy-repeat` scopes and a general template language
342
410
  - Declarative JSON Schema validation
@@ -344,11 +412,11 @@ Deferred from this implementation slice:
344
412
  - `qwentts.cpp` reference-audio voice cloning and VoiceDesign
345
413
  - Automatic long-text chunking, streaming synthesis, and alternative generated-audio formats
346
414
  - Automatic upstream builds, model downloads, environment repair, and hosted rendering
347
- - Frame caches, batch still capture, and general image-sequence output
415
+ - Unbounded batch still capture and general image-sequence output; `still` accepts at most 32 inspection frames per invocation
348
416
  - React or other UI-framework adapters, plugin APIs, and agent-only runtimes
349
417
  - TypeScript transpilation for project compositions and npm dependency optimization
350
418
 
351
- 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, and the [domain glossary](./GLOSSARY.md) for terminology.
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.
352
420
 
353
421
  ## Repository layout
354
422
 
@@ -368,7 +436,7 @@ See the [design baseline](./docs/v0.1-design.md) for the full product contract,
368
436
  - `skills/`: official host-neutral agent skill
369
437
  - `examples/demo/`: the 30-second reference composition
370
438
  - `examples/webgl/`: the focused WebGL shader example
371
- - `examples/video/`: the two-pass embedded-video example with WebM and MP4 sources
439
+ - `examples/video/`: embedded-video compositions and WebM/MP4 timelines with video clips under a transparent HTML overlay
372
440
  - `bench/fixtures/`: controlled HTML compositions for the render benchmark
373
441
  - `scripts/`: benchmark, release checks, versioning, and git-hook installation
374
442
  - `docs/adr/`: architecture decision records
@@ -387,7 +455,7 @@ node dist/cli.js doctor
387
455
 
388
456
  These commands use the local build; `npx frameyard` runs the published package.
389
457
 
390
- `pnpm check` runs the script syntax checks, lint, formatting checks, typechecks, and the full suite. `pnpm install` installs the repository's git hooks; pre-commit runs `pnpm check:static` and the unit tests, and pre-push runs the full suite. CI runs the same `pnpm check` on Linux with Node 22 and 24.
458
+ `pnpm check` runs the script syntax checks, lint, formatting checks, typechecks, and the full suite. `pnpm install` installs the repository's git hooks; pre-commit runs `pnpm check:static` and the unit tests, and pre-push runs the full suite. CI runs the same `pnpm check` on Linux with Node 22 and 24. For preview-only UI work, `pnpm check:preview` runs the static checks and the preview tests without the full suite; it does not replace `pnpm check` in CI or before pushing.
391
459
 
392
460
  CI uses fake `qwen-tts` and `whisper-cli` executables. Real-model tests are opt-in:
393
461