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