frameyard 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +1 -1
  3. package/dist/preview/fonts/LICENSE-figtree.txt +93 -0
  4. package/dist/preview/fonts/LICENSE-space-mono.txt +93 -0
  5. package/dist/preview/fonts/README.md +23 -0
  6. package/dist/preview/fonts/figtree-latin-wght-normal.woff2 +0 -0
  7. package/dist/preview/fonts/space-mono-latin-400-normal.woff2 +0 -0
  8. package/dist/preview/fonts/space-mono-latin-700-normal.woff2 +0 -0
  9. package/dist/preview/main.js +5 -2
  10. package/dist/preview/main.js.map +1 -1
  11. package/dist/preview/timeline-main.js +41 -23
  12. package/dist/preview/timeline-main.js.map +1 -1
  13. package/dist/renderer/audio.d.ts +6 -0
  14. package/dist/renderer/audio.d.ts.map +1 -1
  15. package/dist/renderer/audio.js +3 -1
  16. package/dist/renderer/audio.js.map +1 -1
  17. package/dist/renderer/browser.d.ts.map +1 -1
  18. package/dist/renderer/browser.js +13 -0
  19. package/dist/renderer/browser.js.map +1 -1
  20. package/dist/renderer/concurrency.d.ts +6 -0
  21. package/dist/renderer/concurrency.d.ts.map +1 -0
  22. package/dist/renderer/concurrency.js +27 -0
  23. package/dist/renderer/concurrency.js.map +1 -0
  24. package/dist/renderer/ffmpeg.d.ts +62 -3
  25. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  26. package/dist/renderer/ffmpeg.js +326 -45
  27. package/dist/renderer/ffmpeg.js.map +1 -1
  28. package/dist/renderer/session-pool.d.ts +28 -0
  29. package/dist/renderer/session-pool.d.ts.map +1 -0
  30. package/dist/renderer/session-pool.js +91 -0
  31. package/dist/renderer/session-pool.js.map +1 -0
  32. package/dist/renderer/timeline.d.ts.map +1 -1
  33. package/dist/renderer/timeline.js +518 -38
  34. package/dist/renderer/timeline.js.map +1 -1
  35. package/dist/renderer/timings.d.ts +13 -0
  36. package/dist/renderer/timings.d.ts.map +1 -0
  37. package/dist/renderer/timings.js +29 -0
  38. package/dist/renderer/timings.js.map +1 -0
  39. package/dist/server/preview-html.d.ts.map +1 -1
  40. package/dist/server/preview-html.js +74 -188
  41. package/dist/server/preview-html.js.map +1 -1
  42. package/dist/server/preview-style.d.ts +11 -0
  43. package/dist/server/preview-style.d.ts.map +1 -0
  44. package/dist/server/preview-style.js +298 -0
  45. package/dist/server/preview-style.js.map +1 -0
  46. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  47. package/dist/server/timeline-preview-html.js +140 -164
  48. package/dist/server/timeline-preview-html.js.map +1 -1
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.d.ts.map +1 -1
  51. package/dist/version.js +1 -1
  52. package/dist/version.js.map +1 -1
  53. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  54. package/docs/adr/0024-stream-composition-captures-and-skip-trivial-composites.md +86 -0
  55. package/docs/adr/0025-capture-timeline-layers-in-parallel-with-a-global-limit.md +67 -0
  56. package/docs/adr/0026-reuse-loaded-composition-sessions-across-segments.md +70 -0
  57. package/docs/adr/0027-encode-two-full-frame-layers-through-dual-pipes.md +76 -0
  58. package/docs/adr/0028-encode-full-frame-video-segments-directly.md +65 -0
  59. package/docs/preview-testing.md +8 -0
  60. package/docs/v0.x-design.md +3 -3
  61. package/package.json +2 -2
  62. package/skills/frameyard/SKILL.md +1 -1
@@ -0,0 +1,76 @@
1
+ # Encode a full-frame composition and a static overlay through dual pipes
2
+
3
+ **Status:** Accepted. Extends [ADR-0026](./0026-reuse-loaded-composition-sessions-across-segments.md).
4
+ **Date:** 2026-11-06
5
+
6
+ A segment with a full-frame composition base and one static composition overlay
7
+ now captures both layers concurrently and feeds them to one FFmpeg process
8
+ through two image pipes. The base has an identity transform; the overlay may use
9
+ a static fit, box, and position, but no keyframes, blend, transition, or
10
+ opacity. The process composites the background, base, and overlay and encodes
11
+ the delivery segment directly. The FFV1 intermediates and the separate
12
+ composite pass are skipped.
13
+
14
+ ## Context
15
+
16
+ The two-layer 60-second PIP gate takes 43.78 s; the overlay is a 640×360
17
+ composition on a second track. A two-full-frame-layer workload (1920×1080 base
18
+ plus a 1920×1080 transparent overlay) took 53.27 s with the normal FFV1 and
19
+ composite path. Both captures already ran concurrently after ADR 0025, but the
20
+ segment still paid for two FFV1 encodes, two decodes, and a separate composite
21
+ encode.
22
+
23
+ FFmpeg can read a second image stream from `pipe:3`, and Node can provide that
24
+ extra stdio pipe on Linux and macOS. The two captures write to independent
25
+ pipes, so neither blocks the other while FFmpeg overlays frames in order.
26
+
27
+ ## Decision
28
+
29
+ - `startDualPipeSegmentEncoder` spawns one FFmpeg process with:
30
+ - `pipe:0` for the base composition;
31
+ - `pipe:3` for the overlay composition;
32
+ - a `lavfi color` input for the timeline background;
33
+ - `[2:v][0:v]overlay`, then `[base][1:v]overlay`, then the delivery encoder.
34
+ - `captureTwoLayerDirectComposition` leases two capture sessions, writes each
35
+ layer to its pipe, and waits for both frame counts before finishing.
36
+ - The fast path requires exactly two composition layers: a base that matches the
37
+ stage with an identity transform, and an overlay with no keyframes, blend,
38
+ transition, opacity, scale, or rotation. The overlay may use a static fit,
39
+ explicit box, and position. An opaque timeline background is required.
40
+ - Windows falls back to the FFV1 and composite path because extra stdio pipe
41
+ descriptors are not portable there.
42
+ - `FRAMEYARD_DISABLE_DUAL_PIPE=1` forces the fallback for A/B measurements.
43
+ - One-layer direct segments keep `startDirectSegmentEncoder`; PIP, transformed,
44
+ blended, keyframed, nested, static, and media layers keep the segmented
45
+ lossless path.
46
+
47
+ ## Considered Options
48
+
49
+ - **General multi-pipe compositor for any layer count.** Rejected for now:
50
+ extra pipes multiply the coordination surface and make Windows support
51
+ harder. Two full-frame layers cover the measured multi-layer case.
52
+ - **Interleave two captures into one pipe.** Rejected: the filter graph would
53
+ need frame-splitting metadata and the capture loops would have to lockstep.
54
+ - **Buffer the base layer and stream only the overlay.** Rejected: buffering a
55
+ 300-frame 1080p layer costs hundreds of megabytes and adds a copy.
56
+ - **Keep FFV1 and tune the composite.** Measured filters and thread knobs gave
57
+ no meaningful gain; the intermediate round trip is the cost.
58
+
59
+ ## Consequences
60
+
61
+ - The two-full-frame-layer 60-second gate fell from 53.27 s to 34.62 s (−35.0%)
62
+ with byte-identical output. The full 60-minute two-layer gate completed in
63
+ 29.69 minutes with eight workers, 108,000 exact frames, and clean scratch,
64
+ just inside the 30-minute target.
65
+ - The two-layer PIP 60-second gate fell from 43.78 s to 33.90 s (−22.6%); sampled
66
+ decoded frames are pixel-identical to the FFV1-and-composite path. The full
67
+ PIP 60-minute gate completed in 25.75 minutes with eight workers, 108,000
68
+ exact frames, and clean scratch.
69
+ - The fast path writes no FFV1 files and no PNG sequences for qualifying
70
+ segments.
71
+ - A dual-pipe failure aborts both FFmpeg inputs and discards both sessions;
72
+ frame counts are checked per layer before publication.
73
+ - Windows and non-qualifying segments keep the existing deterministic path, so
74
+ the capture-pixel guarantee is unchanged.
75
+ - Tests cover two full-frame compositions with a transparent overlay over a
76
+ non-black background, plus the full timeline rendering suite.
@@ -0,0 +1,65 @@
1
+ # Encode full-frame video segments directly
2
+
3
+ **Status:** Accepted. Extends [ADR-0027](./0027-encode-two-full-frame-layers-through-dual-pipes.md).
4
+ **Date:** 2026-11-06
5
+
6
+ A segment with one full-frame video layer, an identity transform, and an opaque
7
+ timeline background now decodes and re-encodes the source range directly into
8
+ the delivery segment. It skips the background overlay, fit, presentation, and
9
+ composite filter graph, but keeps the same delivery encoder and container as
10
+ every other segment.
11
+
12
+ ## Context
13
+
14
+ The representative 60-minute 50/50 gate contains 24 seconds per minute of
15
+ full-frame video on a single track. The composite path still built a color
16
+ background, overlaid the video, normalized timestamps, fitted the frame, and
17
+ then encoded. The video source already matches the stage, so those filters are
18
+ identity work except for the fps/format normalization.
19
+
20
+ Stream-copy was considered and deferred: it requires keyframe alignment, matching
21
+ codec parameters, and a concat-compatible bitstream across composition and media
22
+ segments. A direct re-encode avoids those risks while removing the composite
23
+ overhead.
24
+
25
+ ## Decision
26
+
27
+ - `encodeVideoSegmentDirect` seeks to the clip's source start, maps the first
28
+ video stream, drops audio, applies
29
+ `fps=<project fps>,format=rgba,format=yuv420p`, and encodes with the normal
30
+ delivery encoder/container settings. The RGBA round trip preserves the
31
+ composite path's color conversion and keeps decoded pixels identical.
32
+ - The fast path requires one visual layer whose source is a video matching the
33
+ stage dimensions, an identity transform, and no keyframes, blend, transition,
34
+ or opacity. The timeline background must be opaque.
35
+ - `FRAMEYARD_DISABLE_VIDEO_DIRECT=1` forces the composite path for A/B
36
+ measurements.
37
+ - Video with transforms, opacity, overlays, keyframes, transitions, or a
38
+ non-matching source/stage size keeps the composite path. Composition and
39
+ nested-timeline segments are unaffected.
40
+
41
+ ## Considered Options
42
+
43
+ - **Stream-copy the video range.** Deferred: keyframe alignment and mixed-codec
44
+ concat compatibility need a larger probe and validation surface. A direct
45
+ re-encode reaches the target without that risk.
46
+ - **Keep the composite graph.** Rejected for this case: the background, fit, and
47
+ presentation filters are identity work for a full-frame video.
48
+ - **Use the dual-pipe path.** Not applicable: there is only one visual layer.
49
+
50
+ ## Consequences
51
+
52
+ - Full-frame video segments still re-encode, so every delivered segment shares
53
+ the same codec, profile, and container settings. The conformance risk is lower
54
+ than stream-copy.
55
+ - The representative 60-minute 50/50 gate drops the composite path for its
56
+ single-video seconds while retaining it for overlays, PIP, images, and
57
+ multi-layer segments.
58
+ - A 60-second full-frame video-only gate fell from 10.65 s to 7.61 s (−28.5%)
59
+ with pixel-identical decoded frames.
60
+ - The representative 60-minute 50/50 gate fell from 16.82 to 15.57 minutes
61
+ (−7.4%) with 180 direct video segments, exact 108,000 frames, clean scratch,
62
+ and a byte-identical output file.
63
+ - Tests cover a full-frame video timeline through the direct path and the
64
+ existing media/blend/keyframe/timeline suites.
65
+ - The fast path is internal; there is no CLI or JSON contract change.
@@ -39,6 +39,14 @@ expose the same seek/settle contract the renderer uses.
39
39
  `opacity` is `clipOpacity × eased progress` and it stacks above the outgoing
40
40
  layer on the same track. The strip draws a `.transition` band with a
41
41
  `from → to` tooltip; it is read-only.
42
+ - The strip opens with a `.ruler-scale` carrying five `.ruler-tick` labels at
43
+ 0/25/50/75/100% of the frame range. One `.playhead` line spans the ruler and
44
+ every track, driven by a `--playhead-factor` custom property on
45
+ `#timeline-inner`; `--progress` on `#scrub` fills the transport rail.
46
+ - Both preview pages share the chrome in `src/server/preview-style.ts` and its
47
+ design tokens. The chrome serves its own Figtree and Space Mono from
48
+ `/__frameyard/framework/preview/fonts/` and does not link the project font
49
+ sheet. The embedded host page has none of this chrome.
42
50
 
43
51
  ## Waiting for a frame
44
52
 
@@ -118,13 +118,13 @@ Production timeline rendering is segmented and parallel:
118
118
 
119
119
  1. Run static timeline validation, resolve the timeline, and project the scene and render plan.
120
120
  2. Split the timeline into segments at active-layer changes or a maximum segment duration. Each segment has a fixed set of visual layers and a fixed filter graph.
121
- 3. In parallel workers, capture the active composition and nested-timeline clip frames for each segment with Chromium and encode them to lossless FFV1 intermediates. Transparent clips are captured with an alpha-preserving background. A static composition is captured once per props variant as a transparent PNG and reused as an image layer across segments and workers.
121
+ 3. In parallel workers, capture the active composition and nested-timeline clip frames for each segment with Chromium and stream them into lossless FFV1 intermediates. Independent layers of one segment are captured concurrently under a global FIFO session limit, and loaded composition sessions are reused across segments when their entry, props, and transparency match. Transparent clips are captured with an alpha-preserving background. A static composition is captured once per props variant as a transparent PNG and reused as an image layer across segments and workers. A segment with one full-frame, untransformed composition and no keyframes, blend, transition, or opacity skips the FFV1 intermediate and the composite, encoding the capture directly into the delivery segment over the timeline background. A full-frame composition base plus a static composition overlay (including a static PIP box) uses two image pipes into one FFmpeg process, which composites the background, base, and scaled overlay before the delivery encode; Windows keeps the segmented fallback. A single full-frame video with an identity transform re-encodes its source range directly, skipping the composite filter graph while keeping the delivery encoder settings.
122
122
  4. Decode media clips, apply trims, fit, presentation keyframes, rotation into an expanded bound, and opacity, composite the segment with FFmpeg, and encode H.264, HEVC, or VP9.
123
123
  5. Join the ordered segments with a stream copy. Segment boundaries never change frame order or timing.
124
124
  6. Mix the timeline audio once with FFmpeg and mux it into the final output.
125
125
  7. Publish the output atomically; delete scratch intermediates on success and failure.
126
126
 
127
- The default timeline worker count is 8, configurable up to the machine's capacity. With `--encoder nvenc` the count is capped at eight concurrent encode sessions, the consumer driver limit; a larger `--workers` value produces an `ENCODER_WARNING` and the lower count. Scratch is a configurable directory with a bounded budget; it must not depend on `/tmp` alone. The reference long-form target is a representative 60-minute 1920×1080 30 fps timeline with a 50/50 composition/media split by layer-seconds, 60% one active layer, 30% two, and 10% three, rendered in 30 minutes or less on the reference machine. The 10-second benchmark measured 10.7 s for two composition captures, 35.2 MB of FFV1, and 6.1 s to composite and encode. Continuous three-layer stress runs measured 52.4 s and 58.5 s for 60 s of timeline, about 1× real time, and are documented as worst-case rather than the target workload. The representative gate measured 29.39 s for 60 s with four workers, exact 1800-frame output, and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes. The full 60-minute composition-only gate rendered 108,000 frames in 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 whose source keyframes (I-frames) fall on segment boundaries; media that needs scaling or transforms is re-encoded and adds cost not included in that projection. Timeline clip keyframes add per-frame filter expressions and, for animated opacity or scale, an alpha-mask pass per affected layer; the keyframe benchmark reports current timings and is not part of the 60-minute projection.
127
+ The default timeline worker count is 8, configurable up to the machine's capacity. With `--encoder nvenc` the count is capped at eight concurrent encode sessions, the consumer driver limit; a larger `--workers` value produces an `ENCODER_WARNING` and the lower count. Scratch is a configurable directory with a bounded budget; it must not depend on `/tmp` alone. The reference long-form target is a representative 60-minute 1920×1080 30 fps timeline with a 50/50 composition/media split by layer-seconds, 60% one active layer, 30% two, and 10% three, rendered in 30 minutes or less on the reference machine. The 10-second benchmark measured 10.7 s for two composition captures, 35.2 MB of FFV1, and 6.1 s to composite and encode. Continuous three-layer stress runs measured 52.4 s and 58.5 s for 60 s of timeline, about 1× real time, and are documented as worst-case rather than the target workload. The representative gate measured 29.39 s for 60 s with four workers, exact 1800-frame output, and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes. The full 60-minute composition-only gate rendered 108,000 frames in 37.28 minutes with four workers; it is stricter than the 50/50 target mix because every frame is an HTML composition capture. Capture was 48.9% of worker time, FFV1 23.2%, composite 24.7%, and session setup 2.8%. Composition captures now stream PNG buffers into FFV1, and a segment with one full-frame, untransformed composition skips the intermediate and composite. The 60-second one-layer gate fell from 43.27 s to 27.64 s, and the full 60-minute gate completed in 19.83 minutes with eight workers; the direct output was byte-identical to the FFV1-and-composite output. Reusing loaded sessions reduced the 60-minute gate further to 18.78 minutes with eight sessions instead of 360. The two-layer PIP 60-second gate improved from 62.79 s to 59.60 s with streaming and to 43.78 s with parallel layer capture under a global session limit. A full-frame composition base plus a static composition overlay uses dual image pipes; the full-frame 60-second workload fell from 53.27 s to 34.62 s, the PIP 60-second workload from 43.78 s to 33.90 s, and the full two-layer 60-minute gates completed in 29.69 minutes (full-frame) and 25.75 minutes (PIP) with eight workers. The production representative 60-minute gate — 50/50 layer-seconds, 60/30/10 occupancy, 360 clips, 540 segments, and video re-encoded rather than stream-copied — completed in 16.82 minutes with eight workers, exact 108,000 frames, and clean scratch. Direct full-frame video re-encoding later reduced it to 15.57 minutes with a byte-identical file; a 60-second video-only gate fell from 10.65 to 7.61 seconds. The 10-minute one-layer gate improved from 211.8 s to 192.6 s by reusing loaded composition sessions across segments. The fast path copies full-frame, untransformed video whose source matches the project format and whose source keyframes (I-frames) fall on segment boundaries; media that needs scaling or transforms is re-encoded and adds cost not included in that projection. Timeline clip keyframes add per-frame filter expressions and, for animated opacity or scale, an alpha-mask pass per affected layer; the keyframe benchmark reports current timings and is not part of the 60-minute projection.
128
128
 
129
129
  The renderer uses a bounded segment plan, a worker pool, and a scratch budget. It never publishes a partial output, and it cleans scratch on success and failure. Segment boundaries preserve timeline frame order and timing; the final audio mix is muxed once.
130
130
 
@@ -296,7 +296,7 @@ Tests prioritize public behavior and end-to-end evidence over implementation-onl
296
296
  - Media clips trim and fit deterministically; images require durations; source overruns fail.
297
297
  - Props overrides for timeline composition clips merge over clip props and are reflected in the render.
298
298
  - `check` exercises every animated frame and inspects a static composition once; it also evaluates every keyframe channel at every timeline frame. `still` follows the clip capture and timeline composite path. A static composition render captures one PNG; a timeline reuses one static image capture per props variant.
299
- - The 10-second and 60-second long-form benchmarks record capture, FFV1, composite, encode, disk, worker, and audio numbers. Continuous three-layer runs measured 52.4 s and 58.5 s for 60 s of timeline and are documented as worst-case (~1× real time). The representative 50/50, 60/30/10 occupancy gate measured 29.39 s for 60 s with four workers and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes with exact 1800-frame output. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact output and clean scratch, documenting composition-heavy performance above 30 minutes.
299
+ - The 10-second and 60-second long-form benchmarks record capture, FFV1, composite, encode, disk, worker, and audio numbers. Continuous three-layer runs measured 52.4 s and 58.5 s for 60 s of timeline and are documented as worst-case (~1× real time). The representative 50/50, 60/30/10 occupancy gate measured 29.39 s for 60 s with four workers and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes with exact 1800-frame output. The full 60-minute composition-only gate rendered 108,000 frames in 37.28 minutes with four workers before streaming and the direct-segment fast path; the same gate completed in 19.83 minutes with eight workers after ADR 0024 and 18.78 minutes after session reuse (ADR 0026), with exact output, clean scratch, and a byte-identical file. The ten-minute gate projects about 24 minutes at four workers. Independent layers are captured concurrently under a global session limit (ADR 0025); the two-layer PIP 60-second gate fell from 59.60 s to 43.78 s with parallel captures. A full-frame composition base plus a static composition overlay encodes through dual image pipes (ADR 0027); the full-frame 60-second workload fell from 53.27 s to 34.62 s, the PIP 60-second workload from 43.78 s to 33.90 s, and the full two-layer 60-minute gates completed in 29.69 minutes (full-frame) and 25.75 minutes (PIP) with eight workers. The representative 60-minute 50/50 production gate completed in 16.82 minutes with eight workers, exact 108,000 frames, clean scratch, and no video stream-copy; direct full-frame video re-encoding reduced it to 15.57 minutes with a byte-identical file. Loaded composition sessions are reused across segments (ADR 0026); the 10-minute one-layer gate fell from 211.8 s to 192.6 s, and the full gate opened eight sessions instead of 360.
300
300
  - Golden-frame tests cover representative composition and timeline visual behavior.
301
301
  - Normal CI uses fake `qwen-tts`, `sherpa-onnx-offline-tts`, and `whisper-cli` executables and small fixtures to verify arguments, artifacts, manifests, cancellation, and errors without model downloads or GPU hardware.
302
302
  - Real-model integration tests are explicitly opt-in and use documented upstream revisions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frameyard",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Local-first, deterministic video composition framework for HTML, CSS, and JavaScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,7 +64,7 @@
64
64
  "vitest": "5.0.5"
65
65
  },
66
66
  "scripts": {
67
- "build": "tsc -p tsconfig.json",
67
+ "build": "tsc -p tsconfig.json && node scripts/copy-preview-fonts.mjs",
68
68
  "typecheck": "tsc -p tsconfig.json --noEmit",
69
69
  "typecheck:test": "tsc -p tsconfig.test.json",
70
70
  "typecheck:browser": "tsc -p tsconfig.browser.json",
@@ -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, search or install project fonts, 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.9.0
4
+ frameyard-api-version: 0.10.0
5
5
  ---
6
6
 
7
7
  # Frameyard