frameyard 0.6.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 (70) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/GLOSSARY.md +1 -1
  3. package/README.md +14 -11
  4. package/dist/assets/process.js.map +1 -1
  5. package/dist/assets/provenance.js.map +1 -1
  6. package/dist/cli.js +243 -54
  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/doctor.d.ts.map +1 -1
  11. package/dist/doctor.js +79 -0
  12. package/dist/doctor.js.map +1 -1
  13. package/dist/errors.d.ts +1 -1
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js.map +1 -1
  16. package/dist/html/validate.js.map +1 -1
  17. package/dist/preview/main.js +12 -5
  18. package/dist/preview/main.js.map +1 -1
  19. package/dist/preview/timeline-host.d.ts +2 -0
  20. package/dist/preview/timeline-host.d.ts.map +1 -1
  21. package/dist/preview/timeline-host.js +21 -10
  22. package/dist/preview/timeline-host.js.map +1 -1
  23. package/dist/preview/timeline-main.js +9 -5
  24. package/dist/preview/timeline-main.js.map +1 -1
  25. package/dist/renderer/ffmpeg.d.ts +66 -2
  26. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  27. package/dist/renderer/ffmpeg.js +259 -13
  28. package/dist/renderer/ffmpeg.js.map +1 -1
  29. package/dist/renderer/index.d.ts +28 -3
  30. package/dist/renderer/index.d.ts.map +1 -1
  31. package/dist/renderer/index.js +138 -38
  32. package/dist/renderer/index.js.map +1 -1
  33. package/dist/renderer/timeline.d.ts +41 -4
  34. package/dist/renderer/timeline.d.ts.map +1 -1
  35. package/dist/renderer/timeline.js +153 -39
  36. package/dist/renderer/timeline.js.map +1 -1
  37. package/dist/runtime/host.js.map +1 -1
  38. package/dist/runtime/media.d.ts.map +1 -1
  39. package/dist/runtime/media.js +3 -6
  40. package/dist/runtime/media.js.map +1 -1
  41. package/dist/runtime/video.d.ts +13 -0
  42. package/dist/runtime/video.d.ts.map +1 -0
  43. package/dist/runtime/video.js +41 -0
  44. package/dist/runtime/video.js.map +1 -0
  45. package/dist/scaffold.d.ts.map +1 -1
  46. package/dist/scaffold.js +4 -3
  47. package/dist/scaffold.js.map +1 -1
  48. package/dist/server/composition-html.d.ts.map +1 -1
  49. package/dist/server/preview-html.js +1 -1
  50. package/dist/server/server.js.map +1 -1
  51. package/dist/server/timeline-preview-html.js.map +1 -1
  52. package/dist/timeline/plan.js.map +1 -1
  53. package/dist/timeline/schema.js.map +1 -1
  54. package/dist/transcript.js.map +1 -1
  55. package/dist/version.d.ts +1 -1
  56. package/dist/version.js +1 -1
  57. package/docs/adr/0013-capture-bounded-multi-frame-stills-for-inspection.md +33 -0
  58. package/docs/preview-testing.md +53 -0
  59. package/docs/v0.1-design.md +16 -14
  60. package/package.json +5 -3
  61. package/skills/frameyard/README.md +7 -5
  62. package/skills/frameyard/SKILL.md +5 -3
  63. package/skills/frameyard/references/common-mistakes.md +1 -0
  64. package/skills/frameyard/references/deterministic-authoring.md +2 -1
  65. package/skills/frameyard/references/diagnostics.md +1 -1
  66. package/skills/frameyard/references/failure-recovery.md +3 -2
  67. package/skills/frameyard/references/json-results.md +15 -6
  68. package/skills/frameyard/references/subtitles-karaoke.md +2 -2
  69. package/skills/frameyard/references/timelines.md +6 -4
  70. package/skills/frameyard/scripts/frameyard.mjs +20 -3
@@ -0,0 +1,33 @@
1
+ # Capture bounded multi-frame stills for inspection
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-11-01
5
+
6
+ `still` will accept a bounded set of frames in one invocation. `--frame` becomes a spec: a frame number (`42`), an inclusive range (`2-12`), or a comma-separated list (`0,118-126,240`). Ranges are only shorthand for lists. Duplicates are removed preserving the first occurrence, order is preserved, and a single invocation captures at most 32 unique frames. There is no step, start/end flag, fps flag, output pattern, or general image-sequence mode. With one frame, `--out` remains a `.png` file. With multiple frames, `--out` is a directory and the files are named `still-<id>-<frame>.png`; when `--out` is omitted the current working directory is used.
7
+
8
+ The batch uses one preview server and one composition session with sequential seeks. Timeline stills resolve the timeline once, start one server, reuse one scratch directory, and call the existing frame composite path for each requested frame. All frames are validated against the target duration and all output paths are checked for conflicts before Chromium starts. Capture is all-or-nothing at the capture boundary: every frame is captured to a temporary file first, and only after all captures succeed are the files published. Publication is atomic per file. A disk or I/O failure during publication can leave a prefix of already-published files; strict batch atomicity is not promised.
9
+
10
+ JSON results always add `frames: [{ frame, path }]`, including for a single frame. `frame` and `path` remain only when exactly one frame was captured. `diagnostics`, `width`, `height`, and `elapsedMs` stay shared at the top level; diagnostics are merged and deduplicated, and timing is for the whole invocation. Grammar and cap failures are `USAGE`; out-of-range frames are `FRAME_OUT_OF_RANGE` with all invalid frames and the valid range; output conflicts are `OUTPUT_EXISTS` with every conflicting path; capture failures keep their underlying code and add the attempted frames and the failing frame. The preflight order is parse/expand, cap, bounds, output conflicts, then Chromium.
11
+
12
+ ## Context
13
+
14
+ Agents and humans inspect several representative frames when validating motion, easing, transitions, video seeking, and visual regressions. The previous `still` contract captured exactly one frame per invocation, so inspecting a short animation required one command per frame or an external loop. That multiplied tool calls, repeated static validation, preview-server startup, and Chromium startup, and fragmented the structured diagnostics across several JSON envelopes. The agent skill already asked for several representative frames, so the CLI fought the documented workflow.
15
+
16
+ At the same time, general image-sequence output is a documented non-goal. The boundary is scale and intent, not syntax: a list or short range for inspection is useful, while exporting an arbitrary sequence should use a video render or a separate downstream tool.
17
+
18
+ ## Considered Options
19
+
20
+ - **Keep one frame per invocation.** Rejected: simple, but every inspection of motion pays one process, server, and browser per frame and forces the agent to loop or make several calls.
21
+ - **Allow unbounded batch capture or a `--step`/`--fps` sequence.** Rejected: this turns `still` into an image-sequence exporter and makes it easy to generate hundreds of files and unreadable inspection workloads. A hard cap keeps the feature in the debugging-artifact category.
22
+ - **Capture a contact sheet (one PNG grid).** Rejected for now: one image is cheaper to inspect, but it loses per-frame resolution and adds layout and compositing decisions that are not part of the rendering contract.
23
+ - **Open a fresh session per frame.** Rejected: slower, and it hides state or media-seek bugs that production sequential seeking would expose. One session also makes batch pixels comparable to single-frame captures, which the tests assert.
24
+ - **Add a separate `--frames` flag or a `stills` command.** Rejected: it duplicates the concept and forces callers to choose a tool based on frame count. Extending `--frame` keeps one grammar and one command.
25
+ - **Strict batch atomicity with a staging directory and a directory rename.** Rejected: it requires a new output-directory protocol and cannot restore files already replaced under `--overwrite` without a swap. Capture-level all-or-nothing plus per-file atomic publication covers the realistic failure mode for an inspection tool.
26
+
27
+ ## Consequences
28
+
29
+ - `still` remains a debugging artifact. The cap is per invocation; repeated invocations can still produce more files, but the CLI does not advertise or support image-sequence export.
30
+ - Single-frame output paths, PNG bytes, human output, and the existing JSON fields are unchanged; `frames` is additive. Multi-frame results omit the singular `frame` and `path`.
31
+ - A failed batch publishes no files and removes temporary files. If the CLI created the output directory, it removes the empty directory on failure. Existing directories are left in place.
32
+ - Output naming is one rule everywhere: `still-<id>-<frame>.png` without zero-padding. Composition and timeline IDs are already valid filename components.
33
+ - The cap, range grammar, output rules, and publication caveat are documented in the design baseline, README, CLI help, glossary, and official skill. Tests cover grammar, dedupe, order, cap boundaries, bounds, conflicts, output modes, all-or-nothing failure, batch-versus-single pixels for compositions and timelines, embedded video, and WebGL.
@@ -0,0 +1,53 @@
1
+ # Preview testing
2
+
3
+ The preview runs the production composition and timeline hosts in a browser. Use
4
+ the embedded hosts for deterministic Playwright probes; they are same-origin and
5
+ expose the same seek/settle contract the renderer uses.
6
+
7
+ ## Composition preview
8
+
9
+ - Page: `/__frameyard/?composition=<id>`.
10
+ - Stage: the `#stage-frame` iframe.
11
+ - API: `window.__frameyardPreview` exposes `load(entry, props?)`, `seek(frame)`,
12
+ `settle()`, and `dispose()`.
13
+ - `seek()` is synchronous. Call `await settle()` before reading pixels; `load()`
14
+ resets the host, so apply props through it.
15
+
16
+ ## Timeline preview
17
+
18
+ - Page: `/__frameyard/timeline/<id>`; add `?__fy-embed=1` for the embedded host.
19
+ - API: `window.__fyTimeline` exposes `ready`, `format`, `durationInFrames`,
20
+ `load()`, `seek(frame)`, `settle()`, and `dispose()`.
21
+ - Order: `await ready`, `await load()`, `await seek(frame)`, `await settle()`.
22
+ - The non-embedded page owns transport and audio; only the embedded host exposes
23
+ `window.__fyTimeline`.
24
+
25
+ ## Waiting for a frame
26
+
27
+ - `settle()` waits for video presentation through `requestVideoFrameCallback`
28
+ and then for two animation frames for composition and iframe paint. Do not add
29
+ `waitForTimeout` to assertions.
30
+ - For a composition layer, wait for a controller-updated marker. The shared
31
+ helper `test/helpers/preview.ts` exports `waitForFrame(page, frame)`.
32
+ - For a video layer, sample the rendered stage after `settle()`; do not trust
33
+ `video.currentTime` alone. `test/timeline-preview-runtime.test.ts` covers this.
34
+ - The same helper exports `openEmbeddedTimeline`, `loadTimeline`, and
35
+ `seekTimeline`.
36
+
37
+ ## Verification
38
+
39
+ For preview-only UI changes, run `pnpm check:preview`. It runs the static checks
40
+ and then builds before running `test/preview-runtime.test.ts`,
41
+ `test/timeline-preview.test.ts`, and `test/timeline-preview-runtime.test.ts`.
42
+ Use the full `pnpm check` when the change reaches shared contracts, the runtime,
43
+ the renderer, or the CLI, and before pushing or publishing.
44
+
45
+ ## Notes
46
+
47
+ - Preview iframes and the composition stage pin `color-scheme: light` to match
48
+ the production capture context. A dark inherited scheme makes Chromium paint a
49
+ white base behind transparent compositions.
50
+ - A running `dev` server keeps the `dist/` modules it loaded. Restart it after
51
+ `pnpm build`.
52
+ - The timeline preview host is not the production compositor; compare pixels at
53
+ the capture boundary, not encoded output.
@@ -90,7 +90,7 @@ The timeline frame is the master clock. A composition's frame is derived from th
90
90
 
91
91
  ### Reproducibility guarantee
92
92
 
93
- For the same timeline or composition revision, props, assets, segment plan, and frame under a pinned runtime/browser/FFmpeg environment, the captured composition frames and the deterministic timeline composite are identical. The final MP4 is not promised to be bit-identical: encoded bytes depend on the encoder and its runtime.
93
+ For the same timeline or composition revision, props, assets, segment plan, and frame under a pinned runtime/browser/FFmpeg environment, the captured composition frames and the deterministic timeline composite are identical. The final output is not promised to be bit-identical: encoded bytes depend on the encoder and its runtime.
94
94
 
95
95
  Preview and production share composition behavior and capture composition frames the same way, but the timeline preview composites layers in Chromium while production composites them with FFmpeg. Small differences in scaling, alpha, and video color conversion are expected and do not violate clip-capture parity.
96
96
 
@@ -113,16 +113,16 @@ Production timeline rendering is segmented and parallel:
113
113
  1. Run static timeline validation and resolve the render plan.
114
114
  2. Split the timeline into segments at active-layer changes or a maximum segment duration. Each segment has a fixed set of visual layers and a fixed filter graph.
115
115
  3. In parallel workers, capture the active composition and nested-timeline clip frames for each segment with Chromium and encode them to lossless FFV1 intermediates. Transparent clips are captured with an alpha-preserving background.
116
- 4. Decode media clips, apply trims, fit, static transforms, and opacity, composite the segment with FFmpeg, and encode H.264.
116
+ 4. Decode media clips, apply trims, fit, static transforms, and opacity, composite the segment with FFmpeg, and encode H.264, HEVC, or VP9.
117
117
  5. Join the ordered segments with a stream copy. Segment boundaries never change frame order or timing.
118
118
  6. Mix the timeline audio once with FFmpeg and mux it into the final output.
119
119
  7. Publish the output atomically; delete scratch intermediates on success and failure.
120
120
 
121
- The default timeline worker count is 8, configurable up to the machine's capacity. Scratch is a configurable directory with a bounded budget; it must not depend on `/tmp` alone. The reference long-form target is a representative 60-minute 1920×1080 30 fps timeline with a 50/50 composition/media split by layer-seconds, 60% one active layer, 30% two, and 10% three, rendered in 30 minutes or less on the reference machine. The 10-second benchmark measured 10.7 s for two composition captures, 35.2 MB of FFV1, and 6.1 s to composite and encode. Continuous three-layer stress runs measured 52.4 s and 58.5 s for 60 s of timeline, about 1× real time, and are documented as worst-case rather than the target workload. The representative gate measured 29.39 s for 60 s with four workers, exact 1800-frame output, and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact 108,000-frame / 3,600 s output; it is stricter than the 50/50 target mix because every frame is an HTML composition capture. The fast path copies full-frame, untransformed video whose source matches the project format and has keyframes at segment boundaries; media that needs scaling or transforms is re-encoded and adds cost not included in that projection.
121
+ The default timeline worker count is 8, configurable up to the machine's capacity. With `--encoder nvenc` the count is capped at eight concurrent encode sessions, the consumer driver limit; a larger `--workers` value produces an `ENCODER_WARNING` and the lower count. Scratch is a configurable directory with a bounded budget; it must not depend on `/tmp` alone. The reference long-form target is a representative 60-minute 1920×1080 30 fps timeline with a 50/50 composition/media split by layer-seconds, 60% one active layer, 30% two, and 10% three, rendered in 30 minutes or less on the reference machine. The 10-second benchmark measured 10.7 s for two composition captures, 35.2 MB of FFV1, and 6.1 s to composite and encode. Continuous three-layer stress runs measured 52.4 s and 58.5 s for 60 s of timeline, about 1× real time, and are documented as worst-case rather than the target workload. The representative gate measured 29.39 s for 60 s with four workers, exact 1800-frame output, and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact 108,000-frame / 3,600 s output; it is stricter than the 50/50 target mix because every frame is an HTML composition capture. The fast path copies full-frame, untransformed video whose source matches the project format and has keyframes at segment boundaries; media that needs scaling or transforms is re-encoded and adds cost not included in that projection.
122
122
 
123
- The renderer uses a bounded segment plan, a worker pool, and a scratch budget. It never publishes a partial MP4, and it cleans scratch on success and failure. Segment boundaries preserve timeline frame order and timing; the final audio mix is muxed once.
123
+ The renderer uses a bounded segment plan, a worker pool, and a scratch budget. It never publishes a partial output, and it cleans scratch on success and failure. Segment boundaries preserve timeline frame order and timing; the final audio mix is muxed once.
124
124
 
125
- `check` and `still` use the same clip capture and timeline composite path for a requested frame. `check` evaluates every timeline frame; on long timelines this is a validation cost, not a render. `still` captures one frame as PNG without encoding a video.
125
+ `check` and `still` use the same clip capture and timeline composite path for requested frames. `check` evaluates every timeline frame; on long timelines this is a validation cost, not a render. `still` captures one or more requested frames as PNGs without encoding a video.
126
126
 
127
127
  ## 5. Audio and output
128
128
 
@@ -133,13 +133,15 @@ A timeline has one audio mix. It combines:
133
133
  - timeline audio clips on audio tracks;
134
134
  - audio from nested timeline clips, recursively.
135
135
 
136
- Per-clip gain and mute, per-audio-track gain and mute, and composition-level volume apply. Audio fades are deferred. The mix is produced once with FFmpeg and muxed into the final MP4. Preview mixes the same resolved clips in the browser for monitoring; it is not the production source of truth.
136
+ Per-clip gain and mute, per-audio-track gain and mute, and composition-level volume apply. Audio fades are deferred. The mix is produced once with FFmpeg and muxed into the final output. Preview mixes the same resolved clips in the browser for monitoring; it is not the production source of truth.
137
137
 
138
138
  Audio, video, and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition document.
139
139
 
140
140
  Generated speech behaves like any other audio clip. Frameyard does not automatically time-stretch it, change timeline duration, or regenerate it to fit. `check` reports clip durations and overrun warnings so users and agents can revise text, timing, or the timeline deliberately.
141
141
 
142
- 0.6 produces portable MP4 files with H.264 video and AAC audio. It exposes only a small set of validated encoding options, not arbitrary FFmpeg arguments. Single-frame PNG output through `still` is a debugging artifact, not general image-sequence support.
142
+ 0.6 produces MP4, MOV, MKV, or WebM files. The default is MP4 with H.264 video and AAC audio; HEVC and VP9 are opt-in, and WebM is VP9 with Opus. HEVC in MP4 and MOV keeps the `hvc1` sample entry and is not playable in browsers without platform HEVC decoding. It exposes only a small set of validated encoding options, not arbitrary FFmpeg arguments.
143
+
144
+ PNG output through `still`, including a bounded set of inspection frames, is a debugging artifact, not general image-sequence support.
143
145
 
144
146
  A render fails immediately on an uncaught exception, unhandled rejection, required-asset failure, invalid composition output, segment encoding failure, or scratch failure. Diagnostics identify the timeline, clip, segment, and, where applicable, the frame. The CLI exits nonzero, cleans temporary files, and never reports a partial video as successful. Existing output files are not overwritten unless explicitly requested.
145
147
 
@@ -149,9 +151,9 @@ Project and composition commands are:
149
151
 
150
152
  - `init`: scaffold a timeline-first project with a composition, a one-clip timeline, a project format, an agent guide, a `.gitignore` for `out/`, and a private `package.json` with pinned `dev`, `check`, `render`, and `still` scripts.
151
153
  - `dev`: start the preview with composition and timeline modes.
152
- - `render <id> [--out video.mp4]`: render a timeline or a composition. Timeline renders accept `--workers <n>` and `--scratch <dir>`; composition renders accept `--concurrency <n>` from 1 to 16. The default output is `out/<id>.mp4`.
154
+ - `render <id> [--out video]`: render a timeline or a composition. Timeline renders accept `--workers <n>` and `--scratch <dir>`; composition renders accept `--concurrency <n>` from 1 to 16. `--container <mp4|mov|mkv|webm>` and `--codec <h264|hevc|vp9>` select the delivery pair (MP4/H.264 by default; WebM defaults to VP9), and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded; `auto` is the default and probes NVENC then Quick Sync before falling back to software. The output extension must match the container. The default output is `out/<id>.<ext>`.
153
155
  - `check <id>`: run static validation, then validate configuration, props, assets, media, audio, and transcript requirements, load the target, and evaluate every frame without capturing or encoding.
154
- - `still <id> --frame <n>`: capture one requested frame as PNG through the clip capture and timeline composite path.
156
+ - `still <id> --frame <spec>`: capture one or more requested frames as PNGs through the clip capture and timeline composite path. `<spec>` is a frame number (`42`), an inclusive range (`2-12`), or a comma-separated list (`0,118-126,240`); duplicates are removed preserving the first occurrence, input order is preserved, and one invocation captures at most 32 unique frames. There is no step or sequence mode. A single frame keeps the existing `--out <file.png>` behavior; multiple frames use `--out <directory>` and write `still-<id>-<frame>.png` files, defaulting to the current working directory. Frames and output conflicts are validated before Chromium, all frames are captured to temporary files before publication, and a failed capture publishes no files. JSON results add `frames` and keep the singular `frame` and `path` only when one frame was captured. See [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md).
155
157
  - `doctor`: inspect core requirements and optional integrations without repairing or downloading anything.
156
158
 
157
159
  `check`, `still`, and `render` run a Node-side static validation before launching Chromium. For a timeline it validates the JSON document, tracks, clips, references, nested timelines, cycles, frame-rate compatibility, overlaps, source bounds, media, transforms, audio settings, props, and every referenced composition. Static validation and the composition host read the composition grammar through one browser-safe document reader. Timeline diagnostics carry stable codes and optional source locations or JSON paths, and composition diagnostics from inside a timeline carry the clip and track context.
@@ -259,12 +261,12 @@ Tests prioritize public behavior and end-to-end evidence over implementation-onl
259
261
  - Timeline JSON validation catches schema, track, clip, reference, cycle, frame-rate, overlap, bounds, media, transform, audio, and props errors before Chromium.
260
262
  - Diagnostics carry stable codes and source locations or JSON paths; JSON results expose the composition or timeline entry, clips, tracks, and top-level diagnostics.
261
263
  - Sequential, individual, random-order, and parallel composition renders produce equivalent images; timeline segment workers produce ordered output and identical captured composition frames.
262
- - Determinism checks compare captured composition pixels or stills. The final H.264 bytes and the FFmpeg composite versus Chromium preview are not reproducibility oracles.
264
+ - Determinism checks compare captured composition pixels or stills. The final encoded bytes and the FFmpeg composite versus Chromium preview are not reproducibility oracles.
263
265
  - Preview and renderer use the same composition document and host behavior; timeline preview uses the same active clip hosts.
264
266
  - CSS, controller JavaScript, SVG, Canvas, WebGL, embedded video, and audio frames seek correctly.
265
267
  - Frame-scheduled `data-fy-anim-*` animations and frame-state CSS variables produce the expected frames.
266
268
  - Timeline capture preserves alpha for transparent compositions; FFV1 intermediates decode pixel-identically to the captured frames.
267
- - Timeline segment encoding, ordered concatenation, global audio mix, and scratch cleanup produce the expected MP4 dimensions, frame count, duration, and audio stream.
269
+ - Timeline segment encoding, ordered concatenation, global audio mix, and scratch cleanup produce the expected output dimensions, frame count, duration, and audio stream.
268
270
  - Nested timelines preserve group opacity and transform semantics; cycles fail static validation.
269
271
  - Media clips trim and fit deterministically; images require durations; source overruns fail.
270
272
  - Props overrides for timeline composition clips merge over clip props and are reflected in the render.
@@ -285,7 +287,7 @@ These are scope boundaries, not permanent rejections:
285
287
  - Track solo and video-track visibility
286
288
  - Per-timeline formats and frame-rate conversion/retiming
287
289
  - Persistent clip caches; the segmented renderer cleans its intermediates
288
- - Hardware encoders, GPU compositing, and non-H.264 output
290
+ - GPU compositing and codecs beyond H.264, HEVC, and VP9
289
291
  - Hosted rendering, collaboration, and asset marketplaces
290
292
  - Cloud generative-AI services, text-to-video generation, and image generation
291
293
  - `qwentts.cpp` reference-audio voice cloning and VoiceDesign
@@ -301,7 +303,7 @@ These are scope boundaries, not permanent rejections:
301
303
  - Dynamic composition or timeline metadata derived from props
302
304
  - Live streams, remote media, and other visual sources that cannot be deterministically seeked
303
305
  - Dynamic per-frame asset loading or AI inference
304
- - Batch still capture; agents can run `still` once per representative frame
306
+ - Unbounded batch still capture; `still` accepts at most 32 inspection frames per invocation and is not an image-sequence exporter
305
307
  - Additional final-video formats, general image-sequence output, and arbitrary FFmpeg argument passthrough
306
308
  - Built-in TypeScript transpilation and npm dependency optimization
307
309
  - Rendering untrusted compositions
@@ -310,4 +312,4 @@ These are scope boundaries, not permanent rejections:
310
312
 
311
313
  ## 13. Release posture
312
314
 
313
- Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, timeline, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize. 0.6.0 adds timelines, project formats, segmented timeline rendering, and a timeline-first scaffold without breaking the existing HTML composition authoring contract. See [ADR 0010](./adr/0010-make-timelines-the-render-unit.md), [ADR 0011](./adr/0011-compose-timeline-frames-in-a-live-browser-host.md), and [ADR 0012](./adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md).
315
+ Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, timeline, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize. 0.6.0 adds timelines, project formats, segmented timeline rendering, and a timeline-first scaffold without breaking the existing HTML composition authoring contract. See [ADR 0010](./adr/0010-make-timelines-the-render-unit.md), [ADR 0011](./adr/0011-compose-timeline-frames-in-a-live-browser-host.md), and [ADR 0012](./adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md). 0.7.0 adds delivery container, codec, and encoder selection, bounded multi-frame still inspection, and optional encoder probes in `doctor` without breaking the timeline or composition contracts; bounded still inspection is recorded in [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frameyard",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Local-first, deterministic video composition framework for HTML, CSS, and JavaScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -59,7 +59,7 @@
59
59
  "@types/node": "26.6.5",
60
60
  "oxfmt": "0.75.0",
61
61
  "oxlint": "1.90.0",
62
- "typescript": "5.9.3",
62
+ "typescript": "7.0.2",
63
63
  "vitest": "5.0.5"
64
64
  },
65
65
  "scripts": {
@@ -74,8 +74,10 @@
74
74
  "check:static": "pnpm check:scripts && pnpm lint && pnpm format:check && pnpm typecheck && pnpm typecheck:test && pnpm typecheck:browser",
75
75
  "compare-stills": "node scripts/compare-stills.mjs",
76
76
  "check": "pnpm check:static && pnpm test",
77
+ "check:preview": "pnpm check:static && pnpm test:preview",
77
78
  "test": "pnpm build && vitest run",
78
- "test:unit": "vitest run test/contracts.test.ts test/docs.test.ts test/random.test.ts test/release.test.ts test/scaffold.test.ts test/transcript.test.ts",
79
+ "test:preview": "pnpm build && vitest run test/preview-runtime.test.ts test/timeline-preview.test.ts test/timeline-preview-runtime.test.ts",
80
+ "test:unit": "vitest run test/contracts.test.ts test/docs.test.ts test/encoders.test.ts test/random.test.ts test/release.test.ts test/scaffold.test.ts test/transcript.test.ts",
79
81
  "bench": "pnpm build && node scripts/bench-render.mjs",
80
82
  "set-version": "node scripts/set-version.mjs",
81
83
  "verify-release": "node scripts/verify-release.mjs",
@@ -10,16 +10,18 @@ Install from the repository with the open agent-skills CLI:
10
10
  npx skills add vladiantio/frameyard
11
11
  ```
12
12
 
13
- The skill lives at `skills/frameyard` in the repository and npm package. Alternatively, copy or symlink the whole `skills/frameyard/` directory into the skill location your agent host uses. Examples:
13
+ The skill lives at `skills/frameyard` in the repository and npm package. Alternatively, copy or symlink the whole `skills/frameyard/` directory into the skill location your agent host uses:
14
14
 
15
- - Claude Code project skills: `.claude/skills/frameyard`
16
- - Agent hosts that read `.agents/skills`: `.agents/skills/frameyard`
17
- - Global installations: place it under your host's documented personal skills directory
15
+ - Project-scoped: `.claude/skills/frameyard` for Claude Code, or `.agents/skills/frameyard` for hosts that read the shared `.agents` container.
16
+ - Personal or global: `~/.claude/skills/frameyard`, `~/.agents/skills/frameyard`, or another host's personal directory; for example, OpenCode reads `~/.config/opencode/skills/frameyard`.
17
+ - Keep one canonical copy and symlink it into the host directories you use, so updates do not drift.
18
18
 
19
- The skill is host-neutral: it uses only the normal `frameyard` project format and CLI. There is no agent-only runtime.
19
+ The skill is host-neutral: it uses only the normal `frameyard` project format and CLI. There is no agent-only runtime. All `references/`, `scripts/`, and `SKILL.md` paths inside the skill are relative, so the directory works wherever it is installed.
20
20
 
21
21
  Run the CLI with `npx frameyard`. It resolves the latest published version; pin it with `npx frameyard@<version>` or install the package locally when the CLI version must match the skill. When the skill is copied outside the npm package, `scripts/frameyard.mjs` falls back to the `frameyard` command on `PATH`; if that command is unavailable, set `FRAMEYARD_CLI` to an installed CLI path.
22
22
 
23
+ When you work inside the Frameyard repository itself, `npx frameyard` resolves the published package, not your working tree. Run the built CLI (`node dist/cli.js`, after `pnpm build`) or `skills/frameyard/scripts/frameyard.mjs`, which prefers the repository build. The repository `AGENTS.md` is the authority for development commands.
24
+
23
25
  ## Contents
24
26
 
25
27
  - `SKILL.md`: trigger description and operating workflow
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: frameyard
3
3
  description: Create, validate, inspect, and render deterministic Frameyard videos authored with HTML compositions and JSON timelines. Use when a user asks to build or operate a Frameyard project, produce parameterized video, assemble clips into a timeline, add a local TTS or transcription asset through Frameyard's wrappers, or diagnose a Frameyard render. Do not use for general-purpose media generation outside a Frameyard project.
4
- frameyard-api-version: 0.6.0
4
+ frameyard-api-version: 0.7.0
5
5
  ---
6
6
 
7
7
  # Frameyard
@@ -22,8 +22,8 @@ Use Frameyard's public project format and CLI. Do not introduce an agent-only ru
22
22
  3. **Provide props** as JSON. Composition props merge over defaults; timeline render props are a map keyed by composition clip ID. Validate domain-specific requirements with a controller's `validateProps()`.
23
23
  4. **Produce optional assets** with `npx frameyard tts` and `npx frameyard transcribe`, never with the upstream executables. Read [audio assets](references/audio-assets.md). Do not overwrite or regenerate existing generated assets without permission.
24
24
  5. **Validate** with `npx frameyard check <id> --json`, where `<id>` is a timeline or composition. Static validation runs before Chromium; then every frame is evaluated. Read the structured `diagnostics` array. Read [diagnostics](references/diagnostics.md) and [common mistakes](references/common-mistakes.md).
25
- 6. **Inspect** representative frames with `npx frameyard still <id> --frame <n> --out <file.png> --json`. View the PNGs when the host supports image viewing. If visual inspection is impossible, say so explicitly.
26
- 7. **Render** with `npx frameyard render <id> [--out <file.mp4>] [--props <file>] --json`. Composition renders accept `--concurrency <n>`; timeline renders accept `--workers <n>` and `--scratch <dir>`. The default output is `out/<id>.mp4` in the project root.
25
+ 6. **Inspect** representative frames with `npx frameyard still <id> --frame <spec> --out <path> --json`, where `<spec>` is a frame number, an inclusive range like `2-12`, or a comma-separated list with at most 32 unique frames. With one frame, `--out` is a `.png` file; with multiple frames, it is a directory and the files are `still-<id>-<frame>.png`. View the PNGs when the host supports image viewing. If visual inspection is impossible, say so explicitly.
26
+ 7. **Render** with `npx frameyard render <id> [--out <file>] [--props <file>] --json`. Composition renders accept `--concurrency <n>`; timeline renders accept `--workers <n>` and `--scratch <dir>`. `--container <mp4|mov|mkv|webm>` and `--codec <h264|hevc|vp9>` select the delivery pair (MP4/H.264 by default, WebM defaults to VP9), and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded, defaulting to `auto`. The output extension must match the container. The default output is `out/<id>.<ext>` in the project root.
27
27
  8. **Report** the project path, timeline and composition entries, controller, props, generated assets, provenance manifest, validation results, diagnostics, still paths and inspection status, final video path, and any failures.
28
28
 
29
29
  Never claim success when `check` fails, a still is missing, a render exits nonzero, or visual inspection was skipped. A partial or unpublished render is not a successful result.
@@ -35,6 +35,8 @@ Never claim success when `check` fails, a still is missing, a render exits nonze
35
35
  - Use `--project <dir>` when not running from the project root.
36
36
  - `render` writes to `out/<id>.mp4` in the project root unless `--out` is passed. Explicit `--out` paths are relative to the current working directory.
37
37
  - Use `--concurrency <n>` only for composition renders; the default is `1` and the maximum is `16`. Use `--workers <n>` for timeline renders; the default is `8`. Timeline renders also accept `--scratch <dir>` for FFV1 intermediates.
38
+ - `still --frame <spec>` accepts `n`, `n-m`, or a comma-separated list; ranges are inclusive, duplicates are removed preserving the first occurrence, order is preserved, and one call captures at most 32 unique frames. Do not use `still` as an image-sequence exporter. With multiple frames, `--out` is a directory and files are named `still-<id>-<frame>.png`; with one frame, `--out` is a `.png` file.
39
+ - `--encoder` selects the encoder: `auto` (default) probes NVENC, then Quick Sync, then the codec's software encoder; `software` forces `libx264`, `libx265`, or `libvpx-vp9`; `nvenc` and `qsv` force a vendor. `--codec <h264|hevc|vp9>` and `--container <mp4|mov|mkv|webm>` select the delivery pair: MP4 and MOV hold H.264/HEVC with AAC, MKV holds H.264/HEVC/VP9, and WebM holds VP9 with Opus and defaults to VP9. `--out` must use the container's extension. The probe is a real one-frame encode. Encoders, codecs, and containers change encoded bytes and file size, not captured frames; do not switch them to fix a visual difference. With `nvenc`, timeline `--workers` is capped at eight, the consumer driver session limit, and a larger value reports an `ENCODER_WARNING`. HEVC MP4/MOV files use the `hvc1` tag and do not play in browsers without platform HEVC decoding.
38
40
  - For a timeline, `--props` is a JSON object keyed by top-level composition clip ID. For a composition, it is the props object.
39
41
  - Use `npx frameyard doctor --json` before reporting an environment problem. It inspects without repairing or downloading.
40
42
  - Read [JSON results](references/json-results.md) for result and error shapes, and [failure recovery](references/failure-recovery.md) for actionable error codes.
@@ -54,6 +54,7 @@
54
54
  ## Validation
55
55
 
56
56
  - Replacing `check` with a single `still`.
57
+ - Using `still` as an image-sequence exporter, adding `--step`-style workflows, or requesting more than 32 unique frames in one call.
57
58
  - Ignoring warnings or `diagnostics` in `--json` output.
58
59
  - Claiming success after a nonzero exit, a missing still, or skipped visual inspection.
59
60
  - Using `--overwrite` or `--force` without permission.
@@ -217,9 +217,10 @@ Images declared in the document are awaited by the host. Static fonts should be
217
217
  ```sh
218
218
  npx frameyard check hello --json
219
219
  npx frameyard still hello --frame 42 --out hello-42.png --json
220
+ npx frameyard still hello --frame 0,118-126 --out out/stills --json
220
221
  ```
221
222
 
222
- `check` runs static validation before Chromium, then evaluates every frame and validates audio and declared transcripts without capturing or encoding. Read the structured `diagnostics` array. Do not replace `check` with a single still.
223
+ `check` runs static validation before Chromium, then evaluates every frame and validates audio and declared transcripts without capturing or encoding. Read the structured `diagnostics` array. Do not replace `check` with a single still. Inspect several representative frames in one `still` call with a range or list; the batch seeks sequentially in one session and is limited to 32 unique frames.
223
224
 
224
225
  ## Metadata is fixed
225
226
 
@@ -75,7 +75,7 @@ Static validation covers:
75
75
  - `controller`: `{ kind: "inline" }`, `{ kind: "external", src }`, or `null`
76
76
  - `diagnostics`: all static and runtime diagnostics, deduplicated
77
77
 
78
- `check` also exposes `metadata`, `framesChecked`, `audio.details`, and `transcripts.details`. `still` exposes `frame`, `path`, `width`, and `height`. `render` exposes the output path, dimensions, frame count, concurrency, bytes, and audio/transcript clip counts.
78
+ `check` also exposes `metadata`, `framesChecked`, `audio.details`, and `transcripts.details`. `still` exposes `frames`, `width`, and `height`, plus `frame` and `path` when exactly one frame was captured. `render` exposes the output path, dimensions, frame count, concurrency, bytes, and audio/transcript clip counts.
79
79
 
80
80
  Failures use the same envelope with `error.code`, `error.message`, optional `error.details`, and top-level `diagnostics`.
81
81
 
@@ -32,7 +32,7 @@ Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and onl
32
32
  | `ELEMENT_UNKNOWN` | Unknown `fy-*` element | Use `fy-composition`/`fy-transcript` or a normal element. |
33
33
  | `CONTENT_OUTSIDE_ROOT` | Visual content outside `<fy-composition>` | Move it inside the root. |
34
34
  | `PROPS_INVALID` | Invalid props JSON or `validateProps()` failure | Fix the props file or validator. |
35
- | `FRAME_OUT_OF_RANGE` | Frame outside `0..durationInFrames - 1` | Choose a valid frame. |
35
+ | `FRAME_OUT_OF_RANGE` | Frame outside `0..durationInFrames - 1` | Choose valid frames; a batch error lists every invalid frame and the valid range. |
36
36
  | `AUDIO_INVALID` | Bad clip or unreadable/unsupported audio | Use a local project-root-relative file with a decodable audio stream. |
37
37
  | `AUDIO_WARNING` | Audio timeline overrun or silent embedded media | Adjust timing/trim or remove the silent source. |
38
38
  | `MEDIA_INVALID` | Invalid media source or attributes | Use local, seekable files and valid `data-fy-*` values. |
@@ -57,7 +57,8 @@ Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and onl
57
57
 
58
58
  ## Behavior to rely on
59
59
 
60
- - A failed render writes to a temporary file and does not publish a partial MP4.
60
+ - A failed render writes to a temporary file and does not publish a partial output.
61
+ - A multi-frame still captures every frame to a temporary file first; a failed capture publishes no stills. `OUTPUT_EXISTS` lists every conflicting output path before capture starts.
61
62
  - Failed TTS/transcription operations leave no artifact and no manifest entry.
62
63
  - Existing outputs are replaced only with `--overwrite` (renders/stills) or `--force` (generated assets).
63
64
  - Cancellation terminates the child inference process and cleans temporary work.
@@ -11,7 +11,7 @@ Every non-interactive command supports `--json`. Stdout contains one JSON docume
11
11
  }
12
12
  ```
13
13
 
14
- Additional fields are command-specific and stable within the 0.4 contract.
14
+ Additional fields are command-specific and stable within the 0.4 contract. `check`, `still`, and `render` report `elapsedMs`, the wall time of the operation from static validation and preflight through the final frame or publication; CLI argument parsing is excluded.
15
15
 
16
16
  ## Error envelope
17
17
 
@@ -100,15 +100,19 @@ Static validation diagnostics include source locations. Runtime diagnostics may
100
100
  "composition": "hello",
101
101
  "entry": "compositions/hello.html",
102
102
  "controller": { "kind": "external", "src": "compositions/hello.controller.js" },
103
- "frame": 42,
104
- "path": "/absolute/path/hello-42.png",
103
+ "frames": [
104
+ { "frame": 0, "path": "/absolute/path/out/stills/still-hello-0.png" },
105
+ { "frame": 42, "path": "/absolute/path/out/stills/still-hello-42.png" }
106
+ ],
105
107
  "width": 1920,
106
108
  "height": 1080,
107
109
  "diagnostics": [],
108
- "elapsedMs": 700
110
+ "elapsedMs": 1400
109
111
  }
110
112
  ```
111
113
 
114
+ `frames` is always present and follows the expanded `--frame` order. `frame` and `path` are also present only when exactly one frame was captured. `--frame` accepts a number, an inclusive range (`2-12`), or a comma-separated list; at most 32 unique frames are captured per invocation. With multiple frames, `--out` is a directory and the files are named `still-<id>-<frame>.png`.
115
+
112
116
  ## render
113
117
 
114
118
  ```json
@@ -126,6 +130,9 @@ Static validation diagnostics include source locations. Runtime diagnostics may
126
130
  "durationSeconds": 30,
127
131
  "framesRendered": 900,
128
132
  "concurrency": 1,
133
+ "codec": "h264",
134
+ "container": "mp4",
135
+ "encoder": "libx264",
129
136
  "bytes": 1234567,
130
137
  "audio": { "clips": 1 },
131
138
  "transcripts": { "clips": 1 },
@@ -134,7 +141,7 @@ Static validation diagnostics include source locations. Runtime diagnostics may
134
141
  }
135
142
  ```
136
143
 
137
- A successful render means the final path exists and was published atomically. A nonzero exit means no new partial video was published. Without `--out`, the final path is `out/<composition-id>.mp4` inside the project root. `concurrency` is the number of browser sessions actually used; it is capped by `durationInFrames` and by the CLI maximum of 16.
144
+ A successful render means the final path exists and was published atomically. A nonzero exit means no new partial video was published. Without `--out`, the final path is `out/<composition-id>.<ext>` inside the project root, using the container extension. `concurrency` is the number of browser sessions actually used; it is capped by `durationInFrames` and by the CLI maximum of 16. `container` is `mp4`, `mov`, `mkv`, or `webm`; `codec` is `h264`, `hevc`, or `vp9`; and `encoder` is the resolved encoder (`libx264`, `libx265`, `libvpx-vp9`, `h264_nvenc`, `hevc_nvenc`, `h264_qsv`, or `hevc_qsv`). Timeline results also report the effective `workers` count after the encoder session cap, with an `ENCODER_WARNING` diagnostic when it was reduced.
138
145
 
139
146
  ## tts
140
147
 
@@ -190,7 +197,7 @@ A successful render means the final path exists and was published atomically. A
190
197
 
191
198
  ## doctor
192
199
 
193
- Doctor reports findings rather than throwing for environment problems. `ok: true` means the inspection completed; `healthy` says whether required checks, manifest integrity, explicitly supplied models, and integration compatibility passed.
200
+ Doctor reports findings rather than throwing for environment problems. `ok: true` means the inspection completed; `healthy` says whether required checks, manifest integrity, explicitly supplied models, and integration compatibility passed. `ffmpeg-opus-encoder` and `ffmpeg-video-encoders` are optional: the video check probes each supported encoder with a one-frame encode and lists any that FFmpeg advertises but cannot open (an old driver, for example), and neither check affects `healthy`.
194
201
 
195
202
  ```json
196
203
  {
@@ -201,6 +208,8 @@ Doctor reports findings rather than throwing for environment problems. `ok: true
201
208
  { "name": "node", "required": true, "status": "ok", "detail": "v22.12.0" },
202
209
  { "name": "ffmpeg", "required": true, "status": "ok", "detail": "ffmpeg version ..." },
203
210
  { "name": "ffmpeg-encoders", "required": true, "status": "ok", "detail": "libx264 and aac are available." },
211
+ { "name": "ffmpeg-opus-encoder", "required": false, "status": "ok", "detail": "libopus is available for VP9/WebM audio." },
212
+ { "name": "ffmpeg-video-encoders", "required": false, "status": "error", "detail": "Unavailable: h264_nvenc (Driver does not support the required nvenc API version); hevc_nvenc (Driver does not support the required nvenc API version). Available: libx264, libx265, libvpx-vp9, h264_qsv, hevc_qsv." },
204
213
  { "name": "ffprobe", "required": true, "status": "ok", "detail": "ffprobe version ..." },
205
214
  { "name": "chromium", "required": true, "status": "ok", "detail": "/path/to/chrome" },
206
215
  { "name": "manifest", "required": false, "status": "ok", "detail": "2 artifacts verified." }
@@ -72,10 +72,10 @@ Apply that value to text styling inside the controller's `update({ frame })`. Do
72
72
 
73
73
  ```sh
74
74
  npx frameyard check karaoke --json
75
- npx frameyard still karaoke --frame 120 --out karaoke-120.png --json
75
+ npx frameyard still karaoke --frame 118-126 --out karaoke-frames --json
76
76
  ```
77
77
 
78
- Inspect several frames across a word boundary. A single still cannot prove that highlighting is synchronized.
78
+ Inspect several frames across a word boundary in one batch. A single still cannot prove that highlighting is synchronized.
79
79
 
80
80
  ## Derived exports
81
81
 
@@ -109,7 +109,7 @@ No keyframes or animated transforms in 0.6.
109
109
  - Timeline audio clips live on audio tracks. Audio tracks support `gain` and `muted`.
110
110
  - `audio.gain` on a clip is 0–1 and multiplies the source audio.
111
111
  - There are no linked A/V pairs. For a J/L cut, mute the visual clip and add an audio clip with a different `sourceIn`.
112
- - Audio is mixed once for the whole timeline and muxed into the final MP4.
112
+ - Audio is mixed once for the whole timeline and muxed into the final output.
113
113
 
114
114
  ## Nested timelines
115
115
 
@@ -120,15 +120,17 @@ A nested timeline clip behaves as one layer: its internal tracks are composited
120
120
  ```sh
121
121
  npx frameyard check main --json
122
122
  npx frameyard still main --frame 42 --out main-42.png --json
123
+ npx frameyard still main --frame 0,118-126 --out out/stills --json
123
124
  npx frameyard render main --props props/main.json --workers 8 --scratch /path/to/scratch --json
124
125
  ```
125
126
 
126
127
  - `--props` on a timeline is a JSON object keyed by top-level composition clip ID; values merge over the clip's `props`.
127
- - `--workers` is 1–16. The default is 8.
128
+ - `--workers` is 1–16. The default is 8. With `--encoder nvenc` it is capped at 8 concurrent encode sessions and a larger value reports an `ENCODER_WARNING`.
128
129
  - `--scratch` selects the FFV1 intermediate directory. Use a disk with enough space; intermediates are cleaned after the render.
130
+ - `--container <mp4|mov|mkv|webm>` selects the delivery container (default `mp4`); `--codec <h264|hevc|vp9>` selects the codec, and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded. MP4 and MOV hold H.264/HEVC with AAC; MKV holds all three; WebM holds VP9 with Opus and defaults to VP9. `auto` is the default and probes NVENC, then Quick Sync, then software; `software` forces `libx264`, `libx265`, or `libvpx-vp9`. FFV1 intermediates stay lossless and software-encoded, HEVC keeps the `hvc1` tag in MP4/MOV, and the output extension must match the container.
129
131
  - `--concurrency` is composition-only.
130
132
  - `render` defaults to `out/<timeline-id>.mp4`.
131
- - `check` evaluates every timeline frame and composition frame. `still` captures one frame through the same clip capture and composite path.
133
+ - `check` evaluates every timeline frame and composition frame. `still` captures one or more requested frames through the same clip capture and composite path; a multi-frame still resolves the timeline, starts one server, and reuses one scratch directory. The `--frame` spec and 32-frame cap are the same as for compositions.
132
134
  - `npx frameyard dev` lists timelines in the preview; the timeline view has a program monitor, read-only tracks and clips, playhead, playback, audio controls, and diagnostics.
133
135
 
134
136
  ## Determinism and performance
@@ -137,5 +139,5 @@ npx frameyard render main --props props/main.json --workers 8 --scratch /path/to
137
139
  - Composition frames are captured with Chromium; transparent compositions preserve alpha.
138
140
  - Timeline production captures active composition and nested-timeline clips to lossless FFV1, composites segments with FFmpeg, concatenates the ordered segments, and mixes audio once.
139
141
  - Preview composites in Chromium; production composites with FFmpeg. Small scaling, alpha, and video color differences are expected. Parity is guaranteed at clip capture, not at final encoded pixels.
140
- - Final MP4 bytes are not a reproducibility guarantee.
142
+ - Final output bytes are not a reproducibility guarantee.
141
143
  - Long timelines are segmented and bounded. The reference target and measured worst cases are documented in `docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md`.
@@ -1,13 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync } from "node:fs";
3
+ import { existsSync, readFileSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
8
- const localCli = path.resolve(scriptDir, "../../../dist/cli.js");
8
+ const packageRoot = path.resolve(scriptDir, "../../..");
9
9
  const configured = process.env.FRAMEYARD_CLI;
10
10
 
11
+ // Only prefer a sibling build inside the frameyard package itself. A copied
12
+ // skill sits three levels below its host directory, which may hold an
13
+ // unrelated dist/cli.js.
14
+ function packageCli() {
15
+ const manifestPath = path.join(packageRoot, "package.json");
16
+ const cliPath = path.join(packageRoot, "dist", "cli.js");
17
+ if (!existsSync(manifestPath) || !existsSync(cliPath)) return undefined;
18
+ try {
19
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
20
+ return manifest.name === "frameyard" ? cliPath : undefined;
21
+ } catch {
22
+ return undefined;
23
+ }
24
+ }
25
+
26
+ const localCli = packageCli();
27
+
11
28
  let command;
12
29
  let args;
13
30
  if (configured) {
@@ -22,7 +39,7 @@ if (configured) {
22
39
  command = configured;
23
40
  args = process.argv.slice(2);
24
41
  }
25
- } else if (existsSync(localCli)) {
42
+ } else if (localCli) {
26
43
  command = process.execPath;
27
44
  args = [localCli, ...process.argv.slice(2)];
28
45
  } else {