frameyard 0.3.0 → 0.5.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 +117 -0
- package/GLOSSARY.md +46 -2
- package/README.md +174 -106
- package/dist/bindings.d.ts +42 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +144 -0
- package/dist/bindings.js.map +1 -0
- package/dist/cli.js +32 -21
- package/dist/cli.js.map +1 -1
- package/dist/composition/adapter.d.ts +30 -0
- package/dist/composition/adapter.d.ts.map +1 -0
- package/dist/composition/adapter.js +2 -0
- package/dist/composition/adapter.js.map +1 -0
- package/dist/composition/animation.d.ts +19 -0
- package/dist/composition/animation.d.ts.map +1 -0
- package/dist/composition/animation.js +55 -0
- package/dist/composition/animation.js.map +1 -0
- package/dist/composition/attributes.d.ts +7 -0
- package/dist/composition/attributes.d.ts.map +1 -0
- package/dist/composition/attributes.js +24 -0
- package/dist/composition/attributes.js.map +1 -0
- package/dist/composition/descriptor.d.ts +30 -0
- package/dist/composition/descriptor.d.ts.map +1 -0
- package/dist/composition/descriptor.js +188 -0
- package/dist/composition/descriptor.js.map +1 -0
- package/dist/composition/diagnostics.d.ts +12 -0
- package/dist/composition/diagnostics.d.ts.map +1 -0
- package/dist/composition/diagnostics.js +38 -0
- package/dist/composition/diagnostics.js.map +1 -0
- package/dist/composition/index.d.ts +12 -0
- package/dist/composition/index.d.ts.map +1 -0
- package/dist/composition/index.js +7 -0
- package/dist/composition/index.js.map +1 -0
- package/dist/composition/media.d.ts +27 -0
- package/dist/composition/media.d.ts.map +1 -0
- package/dist/composition/media.js +162 -0
- package/dist/composition/media.js.map +1 -0
- package/dist/composition/paths.d.ts +5 -0
- package/dist/composition/paths.d.ts.map +1 -0
- package/dist/composition/paths.js +16 -0
- package/dist/composition/paths.js.map +1 -0
- package/dist/composition/transcripts.d.ts +19 -0
- package/dist/composition/transcripts.d.ts.map +1 -0
- package/dist/composition/transcripts.js +57 -0
- package/dist/composition/transcripts.js.map +1 -0
- package/dist/composition/walk.d.ts +10 -0
- package/dist/composition/walk.d.ts.map +1 -0
- package/dist/composition/walk.js +32 -0
- package/dist/composition/walk.js.map +1 -0
- package/dist/contracts.d.ts +75 -47
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +14 -159
- package/dist/contracts.js.map +1 -1
- package/dist/errors.d.ts +15 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -1
- package/dist/html/document-adapter.d.ts +5 -0
- package/dist/html/document-adapter.d.ts.map +1 -0
- package/dist/html/document-adapter.js +17 -0
- package/dist/html/document-adapter.js.map +1 -0
- package/dist/html/index.d.ts +5 -0
- package/dist/html/index.d.ts.map +1 -0
- package/dist/html/index.js +3 -0
- package/dist/html/index.js.map +1 -0
- package/dist/html/parse.d.ts +26 -0
- package/dist/html/parse.d.ts.map +1 -0
- package/dist/html/parse.js +80 -0
- package/dist/html/parse.js.map +1 -0
- package/dist/html/validate.d.ts +14 -0
- package/dist/html/validate.d.ts.map +1 -0
- package/dist/html/validate.js +648 -0
- package/dist/html/validate.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/preview/main.d.ts +10 -3
- package/dist/preview/main.d.ts.map +1 -1
- package/dist/preview/main.js +189 -89
- package/dist/preview/main.js.map +1 -1
- package/dist/project.d.ts +2 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +4 -0
- package/dist/project.js.map +1 -1
- package/dist/renderer/browser.d.ts +11 -1
- package/dist/renderer/browser.d.ts.map +1 -1
- package/dist/renderer/browser.js +93 -14
- package/dist/renderer/browser.js.map +1 -1
- package/dist/renderer/index.d.ts +25 -5
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +82 -16
- package/dist/renderer/index.js.map +1 -1
- package/dist/runtime/bindings.d.ts +11 -0
- package/dist/runtime/bindings.d.ts.map +1 -0
- package/dist/runtime/bindings.js +164 -0
- package/dist/runtime/bindings.js.map +1 -0
- package/dist/runtime/diagnostics.d.ts +4 -0
- package/dist/runtime/diagnostics.d.ts.map +1 -0
- package/dist/runtime/diagnostics.js +107 -0
- package/dist/runtime/diagnostics.js.map +1 -0
- package/dist/runtime/document-adapter.d.ts +4 -0
- package/dist/runtime/document-adapter.d.ts.map +1 -0
- package/dist/runtime/document-adapter.js +19 -0
- package/dist/runtime/document-adapter.js.map +1 -0
- package/dist/runtime/host.d.ts +14 -0
- package/dist/runtime/host.d.ts.map +1 -0
- package/dist/runtime/host.js +596 -0
- package/dist/runtime/host.js.map +1 -0
- package/dist/runtime/index.d.ts +0 -29
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +0 -360
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/media.d.ts.map +1 -1
- package/dist/runtime/media.js +20 -51
- package/dist/runtime/media.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +177 -84
- package/dist/scaffold.js.map +1 -1
- package/dist/server/composition-html.d.ts +20 -0
- package/dist/server/composition-html.d.ts.map +1 -0
- package/dist/server/composition-html.js +45 -0
- package/dist/server/composition-html.js.map +1 -0
- package/dist/server/preview-html.d.ts.map +1 -1
- package/dist/server/preview-html.js +19 -14
- package/dist/server/preview-html.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +94 -6
- package/dist/server/server.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/adr/0001-keep-composition-authoring-web-native.md +2 -0
- package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +7 -1
- package/docs/adr/0007-make-html-documents-the-composition-unit.md +31 -0
- package/docs/adr/0008-share-the-composition-document-grammar.md +18 -0
- package/docs/adr/0009-composite-production-frames-in-software.md +37 -0
- package/docs/migration-0.4.md +279 -0
- package/docs/v0.1-design.md +78 -56
- package/examples/demo/README.md +8 -4
- package/examples/demo/compositions/demo.controller.js +120 -0
- package/examples/demo/compositions/demo.html +253 -0
- package/examples/demo/video.config.js +1 -1
- package/examples/video/README.md +61 -0
- package/examples/video/assets/clip.mp4 +0 -0
- package/examples/video/assets/clip.webm +0 -0
- package/examples/video/compositions/video-mp4.html +362 -0
- package/examples/video/compositions/video.html +362 -0
- package/examples/video/props/video.json +8 -0
- package/examples/video/scripts/make-clip.sh +109 -0
- package/examples/video/video.config.js +6 -0
- package/examples/webgl/README.md +30 -0
- package/examples/webgl/compositions/webgl.controller.js +200 -0
- package/examples/webgl/compositions/webgl.html +215 -0
- package/examples/webgl/props/webgl.json +6 -0
- package/examples/webgl/video.config.js +5 -0
- package/package.json +7 -3
- package/skills/frameyard/README.md +4 -0
- package/skills/frameyard/SKILL.md +40 -12
- package/skills/frameyard/references/audio-assets.md +25 -14
- package/skills/frameyard/references/bindings.md +73 -0
- package/skills/frameyard/references/common-mistakes.md +59 -0
- package/skills/frameyard/references/controllers.md +126 -0
- package/skills/frameyard/references/deterministic-authoring.md +143 -52
- package/skills/frameyard/references/diagnostics.md +90 -0
- package/skills/frameyard/references/failure-recovery.md +37 -14
- package/skills/frameyard/references/json-results.md +43 -9
- package/skills/frameyard/references/subtitles-karaoke.md +10 -18
- package/examples/demo/compositions/demo.js +0 -206
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Failure recovery
|
|
2
2
|
|
|
3
|
-
Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and only then render.
|
|
3
|
+
Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and only then render. `check`, `still`, and `render` run static composition validation before Chromium; read the structured `diagnostics` array for source locations.
|
|
4
4
|
|
|
5
5
|
## Common codes
|
|
6
6
|
|
|
@@ -8,30 +8,52 @@ Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and onl
|
|
|
8
8
|
| --- | --- | --- |
|
|
9
9
|
| `USAGE` | Invalid flags or missing required arguments | Read `npx frameyard <command> --help`. |
|
|
10
10
|
| `CONFIG_NOT_FOUND` | No `video.config.js` found | Run from the project root or pass `--project`. |
|
|
11
|
-
| `CONFIG_INVALID` | Invalid config or composition entry | Fix `video.config.js`; entries must be explicit, local, and exist. |
|
|
11
|
+
| `CONFIG_INVALID` | Invalid config or composition entry | Fix `video.config.js`; entries must be explicit, local, `.html`, and exist. |
|
|
12
12
|
| `COMPOSITION_NOT_FOUND` | ID is not listed | Add the entry to `video.config.js`. |
|
|
13
|
-
| `COMPOSITION_INVALID` | Invalid metadata or
|
|
14
|
-
| `COMPOSITION_ERROR` | Mount, update, or browser page error | Read the frame location
|
|
13
|
+
| `COMPOSITION_INVALID` | Invalid doctype, root, metadata, or document structure | Fix the composition document; check `details.line` and `details.attribute`. |
|
|
14
|
+
| `COMPOSITION_ERROR` | Mount, update, or browser page error | Read the frame location; remove forbidden clocks/randomness or fix the controller. |
|
|
15
15
|
| `COMPOSITION_UPDATE_ASYNC` | `update()` returned a promise | Move async work to `mount()`. |
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
16
|
+
| `COMPOSITION_NOT_READY` | `seek`/`settle` before a successful load | Await `load`/`ready`. |
|
|
17
|
+
| `COMPOSITION_DISPOSED` | Operation after host disposal | Reload the composition. |
|
|
18
|
+
| `CONTROLLER_INVALID` | Missing, duplicate, or invalid controller declaration/file/exports | Keep exactly one inline or external controller; verify its exports and path. |
|
|
19
|
+
| `BINDING_INVALID` | Unknown `data-fy-*` attribute or malformed path | Fix the attribute name or path. |
|
|
20
|
+
| `BINDING_PATH_MISSING` | Binding path absent from resolved props | Fix the prop or path; use `details.suggestion`. |
|
|
21
|
+
| `BINDING_VALUE_INVALID` | Object/array used where a primitive is required | Use a string/number/boolean or move the logic to a controller. |
|
|
22
|
+
| `BINDING_REPEAT_INVALID` | Repeat not on `<template>`, or path missing/not an array | Use `<template data-fy-repeat="items">` with an array. |
|
|
23
|
+
| `BINDING_REPEAT_NESTED` | Nested repeat | Use a controller for nested data. |
|
|
24
|
+
| `BINDING_TARGET_INVALID` | `data-fy-text` on a void element or the root | Move the binding to a text container. |
|
|
25
|
+
| `ANIMATION_INVALID` | Invalid `data-fy-anim-*` value | Use non-negative integer frames and a positive duration. |
|
|
26
|
+
| `ANIMATION_NOT_FOUND` | `data-fy-anim-*` without a CSS animation | Add an `animation` shorthand or remove the attributes. |
|
|
27
|
+
| `ANIMATION_MULTIPLE` | Multiple animations on one scheduled element | Use one animation or move scheduling to CSS/the controller. |
|
|
28
|
+
| `SCRIPT_NOT_ALLOWED` | Executable script other than the controller | Move code into the controller. |
|
|
29
|
+
| `EVENT_HANDLER_NOT_ALLOWED` | Inline `on*` handler or `data-fy-attr-on*` | Remove the handler; use a controller. |
|
|
30
|
+
| `URL_NOT_ALLOWED` | `javascript:` URL | Use a safe URL. |
|
|
31
|
+
| `ELEMENT_NOT_SUPPORTED` | `iframe`, `object`, `embed`, `base`, refresh/CSP meta, `source`, `track`, or declarative shadow DOM | Use supported DOM/SVG/canvas/video sources. |
|
|
32
|
+
| `ELEMENT_UNKNOWN` | Unknown `fy-*` element | Use `fy-composition`/`fy-transcript` or a normal element. |
|
|
33
|
+
| `CONTENT_OUTSIDE_ROOT` | Visual content outside `<fy-composition>` | Move it inside the root. |
|
|
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. |
|
|
36
|
+
| `AUDIO_INVALID` | Bad clip or unreadable/unsupported audio | Use a local project-root-relative file with a decodable audio stream. |
|
|
37
|
+
| `AUDIO_WARNING` | Audio timeline overrun or silent embedded media | Adjust timing/trim or remove the silent source. |
|
|
38
|
+
| `MEDIA_INVALID` | Invalid media source or attributes | Use local, seekable files and valid `data-fy-*` values. |
|
|
39
|
+
| `MEDIA_SEEK_FAILED` | Embedded video did not reach the requested time | Re-encode with a regular keyframe interval or replace the source. |
|
|
40
|
+
| `TRANSCRIPT_INVALID` | Invalid/missing transcript declaration or JSON | Regenerate with `npx frameyard transcribe`; do not hand-edit timestamps. |
|
|
41
|
+
| `TRANSCRIPT_WORDS_UNAVAILABLE` | Karaoke requested without measured word timings | Re-run transcription with `--words` or remove `words`. |
|
|
42
|
+
| `TRANSCRIPT_WARNING` | Empty transcript text or no timed segments | Regenerate the transcript. |
|
|
21
43
|
| `ASSET_NOT_FOUND` | Required file is missing or unreadable | Restore the asset, model, or text file. |
|
|
44
|
+
| `DETERMINISM_WARNING` | Forbidden clock/randomness, CSS transition, or runtime warning | Derive state from `frame`/`props`; use `random("stable:key")`. |
|
|
22
45
|
| `OUTPUT_EXISTS` | Output exists and replacement was not approved | Reuse it, choose another path, or pass `--overwrite`/`--force` with permission. |
|
|
23
46
|
| `OUTPUT_FAILED` | An output or manifest could not be written atomically | Check directory permissions and free space; rerun. |
|
|
24
47
|
| `EXECUTABLE_NOT_FOUND` | `qwen-tts` or `whisper-cli` could not be resolved | Pass `--executable`, set the environment variable, or fix `PATH`. |
|
|
25
48
|
| `TTS_FAILED` | Speech synthesis failed or produced an invalid WAV | Read stderr; verify the talker/codec models and CLI compatibility. |
|
|
26
49
|
| `TRANSCRIBE_FAILED` | Normalization or `whisper-cli` failed | Verify FFmpeg, the source media, and the Whisper model. |
|
|
27
|
-
| `TRANSCRIPT_INVALID` | Canonical transcript JSON is malformed | Regenerate it with `npx frameyard transcribe`; do not hand-edit timestamps. |
|
|
28
|
-
| `TRANSCRIPT_WORDS_UNAVAILABLE` | Karaoke requested without measured word timings | Re-run transcription with `--words` or remove `words: true`. |
|
|
29
50
|
| `MANIFEST_INVALID` | Provenance manifest is corrupt or unsupported | Restore or regenerate the manifest; do not invent hashes. |
|
|
30
51
|
| `FFMPEG_FAILED` | FFmpeg is missing, lacks encoders, or failed | Run `npx frameyard doctor --json`; install FFmpeg with libx264 and AAC. |
|
|
31
52
|
| `FFPROBE_FAILED` | FFprobe is missing or failed | Install FFmpeg/FFprobe together. |
|
|
32
53
|
| `BROWSER_FAILED` | Chromium could not start | Install the Playwright Chromium build. |
|
|
33
54
|
| `RENDER_FAILED` | Capture or encoding failed | Read stderr; rerun `check` and a representative `still`. |
|
|
34
55
|
| `CANCELLED` | The operation was interrupted | Rerun when ready. Partial outputs are removed. |
|
|
56
|
+
| `INTERNAL` | Unexpected internal failure | Rerun with `FRAMEYARD_DEBUG=1` and report the stack. |
|
|
35
57
|
|
|
36
58
|
## Behavior to rely on
|
|
37
59
|
|
|
@@ -40,6 +62,7 @@ Do not claim success after a nonzero exit. Fix the cause, rerun `check`, and onl
|
|
|
40
62
|
- Existing outputs are replaced only with `--overwrite` (renders/stills) or `--force` (generated assets).
|
|
41
63
|
- Cancellation terminates the child inference process and cleans temporary work.
|
|
42
64
|
- `check` validates every frame and declared transcripts without encoding.
|
|
65
|
+
- Static validation failures exit before Chromium starts.
|
|
43
66
|
- `check`, `still`, and `render` seek embedded videos and wait for decode before evaluating or capturing a frame.
|
|
44
67
|
- `doctor` inspects but never repairs, downloads, or modifies the environment.
|
|
45
68
|
|
|
@@ -51,10 +74,10 @@ If the host cannot display images, say explicitly that representative stills cou
|
|
|
51
74
|
|
|
52
75
|
Report all of:
|
|
53
76
|
|
|
54
|
-
- source project path
|
|
77
|
+
- source project path, composition entry, and controller;
|
|
55
78
|
- props file or inline props used;
|
|
56
79
|
- generated audio/transcript paths and provenance manifest path;
|
|
57
|
-
- `check` result, including warnings;
|
|
80
|
+
- `check` result, including diagnostics and warnings;
|
|
58
81
|
- still paths and whether they were viewed;
|
|
59
82
|
- final video path, frame count, and audio/transcript summary;
|
|
60
|
-
- any skipped step or unresolved
|
|
83
|
+
- any skipped step or unresolved diagnostic.
|
|
@@ -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.
|
|
14
|
+
Additional fields are command-specific and stable within the 0.4 contract.
|
|
15
15
|
|
|
16
16
|
## Error envelope
|
|
17
17
|
|
|
@@ -21,12 +21,37 @@ Additional fields are command-specific and stable within the 0.1 contract.
|
|
|
21
21
|
"command": "render",
|
|
22
22
|
"error": {
|
|
23
23
|
"code": "OUTPUT_EXISTS",
|
|
24
|
-
"message": "Output already exists: ..."
|
|
24
|
+
"message": "Output already exists: ...",
|
|
25
|
+
"details": {}
|
|
26
|
+
},
|
|
27
|
+
"diagnostics": []
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`error.details` and top-level `diagnostics` are optional. Use the error `code` for decisions, not message text. `command` is `null` only for usage failures that occur before a command is known. Exit code `2` means usage; `130` means cancellation; other failures exit `1`.
|
|
32
|
+
|
|
33
|
+
## Diagnostics
|
|
34
|
+
|
|
35
|
+
`check`, `still`, and `render` results contain a `diagnostics` array:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"severity": "warning",
|
|
40
|
+
"code": "BINDING_PATH_MISSING",
|
|
41
|
+
"message": "data-fy-text references prop \"titel\", which is not in the resolved props.",
|
|
42
|
+
"details": {
|
|
43
|
+
"path": "compositions/hello.html",
|
|
44
|
+
"line": 12,
|
|
45
|
+
"column": 5,
|
|
46
|
+
"element": "h1",
|
|
47
|
+
"attribute": "data-fy-text",
|
|
48
|
+
"value": "titel",
|
|
49
|
+
"suggestion": "title"
|
|
25
50
|
}
|
|
26
51
|
}
|
|
27
52
|
```
|
|
28
53
|
|
|
29
|
-
|
|
54
|
+
Static validation diagnostics include source locations. Runtime diagnostics may include a `frame`. Audio and transcript warnings use `AUDIO_WARNING` and `TRANSCRIPT_WARNING`; controller clock/randomness and CSS transition warnings use `DETERMINISM_WARNING`. See [diagnostics](diagnostics.md) for the code table.
|
|
30
55
|
|
|
31
56
|
## check
|
|
32
57
|
|
|
@@ -35,11 +60,12 @@ Use the error `code` for decisions, not message text. `command` is `null` only f
|
|
|
35
60
|
"ok": true,
|
|
36
61
|
"command": "check",
|
|
37
62
|
"composition": "hello",
|
|
63
|
+
"entry": "compositions/hello.html",
|
|
64
|
+
"controller": { "kind": "inline" },
|
|
38
65
|
"metadata": { "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 900 },
|
|
39
66
|
"framesChecked": 900,
|
|
40
67
|
"audio": {
|
|
41
68
|
"clips": 1,
|
|
42
|
-
"warnings": [],
|
|
43
69
|
"details": [
|
|
44
70
|
{
|
|
45
71
|
"src": "assets/voice.wav",
|
|
@@ -54,15 +80,17 @@ Use the error `code` for decisions, not message text. `command` is `null` only f
|
|
|
54
80
|
},
|
|
55
81
|
"transcripts": {
|
|
56
82
|
"clips": 1,
|
|
57
|
-
"warnings": [],
|
|
58
83
|
"details": [
|
|
59
84
|
{ "src": "assets/voice.transcript.json", "language": "en", "segments": 3, "words": 17, "durationMs": 5200, "bytes": 2048 }
|
|
60
85
|
]
|
|
61
86
|
},
|
|
87
|
+
"diagnostics": [],
|
|
62
88
|
"elapsedMs": 1200
|
|
63
89
|
}
|
|
64
90
|
```
|
|
65
91
|
|
|
92
|
+
`entry` is the project-relative HTML path. `controller` is `{ kind: "inline" }`, `{ kind: "external", src }`, or `null`.
|
|
93
|
+
|
|
66
94
|
## still
|
|
67
95
|
|
|
68
96
|
```json
|
|
@@ -70,10 +98,13 @@ Use the error `code` for decisions, not message text. `command` is `null` only f
|
|
|
70
98
|
"ok": true,
|
|
71
99
|
"command": "still",
|
|
72
100
|
"composition": "hello",
|
|
101
|
+
"entry": "compositions/hello.html",
|
|
102
|
+
"controller": { "kind": "external", "src": "compositions/hello.controller.js" },
|
|
73
103
|
"frame": 42,
|
|
74
104
|
"path": "/absolute/path/hello-42.png",
|
|
75
105
|
"width": 1920,
|
|
76
106
|
"height": 1080,
|
|
107
|
+
"diagnostics": [],
|
|
77
108
|
"elapsedMs": 700
|
|
78
109
|
}
|
|
79
110
|
```
|
|
@@ -85,7 +116,9 @@ Use the error `code` for decisions, not message text. `command` is `null` only f
|
|
|
85
116
|
"ok": true,
|
|
86
117
|
"command": "render",
|
|
87
118
|
"composition": "hello",
|
|
88
|
-
"
|
|
119
|
+
"entry": "compositions/hello.html",
|
|
120
|
+
"controller": { "kind": "inline" },
|
|
121
|
+
"path": "/absolute/path/out/hello.mp4",
|
|
89
122
|
"width": 1920,
|
|
90
123
|
"height": 1080,
|
|
91
124
|
"fps": 30,
|
|
@@ -94,13 +127,14 @@ Use the error `code` for decisions, not message text. `command` is `null` only f
|
|
|
94
127
|
"framesRendered": 900,
|
|
95
128
|
"concurrency": 1,
|
|
96
129
|
"bytes": 1234567,
|
|
97
|
-
"audio": { "clips": 1
|
|
98
|
-
"transcripts": { "clips": 1
|
|
130
|
+
"audio": { "clips": 1 },
|
|
131
|
+
"transcripts": { "clips": 1 },
|
|
132
|
+
"diagnostics": [],
|
|
99
133
|
"elapsedMs": 99000
|
|
100
134
|
}
|
|
101
135
|
```
|
|
102
136
|
|
|
103
|
-
A successful render means the final path exists and was published atomically. A nonzero exit means no new partial video was published. `concurrency` is the number of browser sessions actually used; it is capped by `durationInFrames` and by the CLI maximum of 16.
|
|
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.
|
|
104
138
|
|
|
105
139
|
## tts
|
|
106
140
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Subtitles and karaoke
|
|
2
2
|
|
|
3
|
-
Frameyard does not burn captions into videos and has no built-in subtitle renderer.
|
|
3
|
+
Frameyard does not burn captions into videos and has no built-in subtitle renderer. A composition controller renders timed text. Transcription produces a canonical transcript; SRT and VTT are derived exports.
|
|
4
4
|
|
|
5
5
|
## Canonical transcript
|
|
6
6
|
|
|
@@ -33,29 +33,21 @@ All timestamps are integer milliseconds relative to the source audio:
|
|
|
33
33
|
|
|
34
34
|
## Declaring transcript requirements
|
|
35
35
|
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
{ src: "assets/voice.transcript.json", words: true },
|
|
39
|
-
],
|
|
36
|
+
```html
|
|
37
|
+
<fy-transcript src="assets/voice.transcript.json" words></fy-transcript>
|
|
40
38
|
```
|
|
41
39
|
|
|
42
|
-
Transcript `src` paths
|
|
40
|
+
Transcript `src` paths resolve from the project root. `check`, `still`, and `render` validate declared transcript files. Preview loads the declarations but leaves visual presentation to the composition controller.
|
|
43
41
|
|
|
44
42
|
## Converting source time to frames
|
|
45
43
|
|
|
46
|
-
Given an audio
|
|
44
|
+
Given an `<audio>` element:
|
|
47
45
|
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
src: "assets/voice.wav",
|
|
51
|
-
startFrame: 30,
|
|
52
|
-
trimStart: 0.25,
|
|
53
|
-
trimDuration: 4.2,
|
|
54
|
-
volume: 0.8,
|
|
55
|
-
};
|
|
46
|
+
```html
|
|
47
|
+
<audio src="assets/voice.wav" data-fy-start="30" data-fy-trim="0.25" data-fy-duration="4.2" data-fy-volume="0.8"></audio>
|
|
56
48
|
```
|
|
57
49
|
|
|
58
|
-
Convert a word or segment time with:
|
|
50
|
+
the controller can read the attributes or keep matching constants. Convert a word or segment time with:
|
|
59
51
|
|
|
60
52
|
```js
|
|
61
53
|
const FPS = 30;
|
|
@@ -64,7 +56,7 @@ const frame = clip.startFrame + Math.round((word.startMs / 1000 - clip.trimStart
|
|
|
64
56
|
|
|
65
57
|
Skip words before the trimmed source range or after `trimStart + trimDuration`. Do not divide by `durationInFrames` or assume a 30 fps composition.
|
|
66
58
|
|
|
67
|
-
A seek-safe karaoke update only reads the current frame:
|
|
59
|
+
A seek-safe karaoke update in the controller only reads the current frame:
|
|
68
60
|
|
|
69
61
|
```js
|
|
70
62
|
function wordProgress(word, frame) {
|
|
@@ -74,7 +66,7 @@ function wordProgress(word, frame) {
|
|
|
74
66
|
}
|
|
75
67
|
```
|
|
76
68
|
|
|
77
|
-
Apply that value to text styling inside `update({ frame })`. Do not use timers, `requestAnimationFrame`, or transitions.
|
|
69
|
+
Apply that value to text styling inside the controller's `update({ frame })`. Do not use timers, `requestAnimationFrame`, or transitions. Declare the transcript with `<fy-transcript src="..." words>` and fetch the canonical JSON in `mount()`.
|
|
78
70
|
|
|
79
71
|
## Check and inspect
|
|
80
72
|
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { defineComposition } from "frameyard";
|
|
2
|
-
import { random } from "frameyard/runtime";
|
|
3
|
-
|
|
4
|
-
let scene;
|
|
5
|
-
let title;
|
|
6
|
-
let subtitle;
|
|
7
|
-
let kaption;
|
|
8
|
-
let progress;
|
|
9
|
-
let signalPath;
|
|
10
|
-
let bars = [];
|
|
11
|
-
let starfield;
|
|
12
|
-
let stars;
|
|
13
|
-
let context;
|
|
14
|
-
let signalLength = 0;
|
|
15
|
-
|
|
16
|
-
const STAR_COUNT = 130;
|
|
17
|
-
const BAR_COUNT = 52;
|
|
18
|
-
const BASELINE = 858;
|
|
19
|
-
|
|
20
|
-
function clamp(value) {
|
|
21
|
-
return Math.max(0, Math.min(1, value));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function easeOut(value) {
|
|
25
|
-
const t = clamp(value);
|
|
26
|
-
return 1 - Math.pow(1 - t, 3);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function makeStars() {
|
|
30
|
-
stars = Array.from({ length: STAR_COUNT }, (_, index) => ({
|
|
31
|
-
x: random("star:" + index + ":x"),
|
|
32
|
-
y: random("star:" + index + ":y"),
|
|
33
|
-
radius: 0.6 + random("star:" + index + ":r") * 1.8,
|
|
34
|
-
speed: 6 + random("star:" + index + ":speed") * 34,
|
|
35
|
-
alpha: 0.12 + random("star:" + index + ":alpha") * 0.65,
|
|
36
|
-
}));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function buildBars() {
|
|
40
|
-
const svg = document.querySelector("#signal");
|
|
41
|
-
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
42
|
-
bars = [];
|
|
43
|
-
|
|
44
|
-
for (let index = 0; index < BAR_COUNT; index += 1) {
|
|
45
|
-
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
46
|
-
const height = 24 + random("bar:" + index + ":height") * 176;
|
|
47
|
-
const x = 168 + index * 31;
|
|
48
|
-
rect.setAttribute("x", String(x));
|
|
49
|
-
rect.setAttribute("width", "15");
|
|
50
|
-
rect.setAttribute("rx", "7.5");
|
|
51
|
-
rect.setAttribute("fill", "var(--accent)");
|
|
52
|
-
rect.setAttribute("height", "0");
|
|
53
|
-
rect.setAttribute("y", String(BASELINE));
|
|
54
|
-
rect.setAttribute("opacity", "0");
|
|
55
|
-
group.append(rect);
|
|
56
|
-
bars.push({ element: rect, height });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
svg.append(group);
|
|
60
|
-
return group;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function buildSignalPath() {
|
|
64
|
-
const points = [];
|
|
65
|
-
for (let index = 0; index <= 120; index += 1) {
|
|
66
|
-
const x = 160 + (index / 120) * 1600;
|
|
67
|
-
const wave = Math.sin(index / 7) * 38 + Math.sin(index / 17 + 1.4) * 22 + (random("signal:" + index) - 0.5) * 12;
|
|
68
|
-
points.push([x, 535 + wave]);
|
|
69
|
-
}
|
|
70
|
-
const d = points.map(([x, y], index) => (index === 0 ? "M" : "L") + x + " " + y).join(" ");
|
|
71
|
-
signalPath.setAttribute("d", d);
|
|
72
|
-
signalLength = signalPath.getTotalLength();
|
|
73
|
-
signalPath.style.strokeDasharray = String(signalLength);
|
|
74
|
-
signalPath.style.strokeDashoffset = String(signalLength);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export default defineComposition({
|
|
78
|
-
width: 1920,
|
|
79
|
-
height: 1080,
|
|
80
|
-
fps: 30,
|
|
81
|
-
durationInFrames: 900,
|
|
82
|
-
defaultProps: {
|
|
83
|
-
title: "Frameyard",
|
|
84
|
-
subtitle: "Deterministic video, authored with the web platform.",
|
|
85
|
-
accent: "#b8f36b",
|
|
86
|
-
},
|
|
87
|
-
validateProps(props) {
|
|
88
|
-
const errors = [];
|
|
89
|
-
if (typeof props.title !== "string" || props.title.length === 0) errors.push("title must be a non-empty string");
|
|
90
|
-
if (typeof props.subtitle !== "string") errors.push("subtitle must be a string");
|
|
91
|
-
if (typeof props.accent !== "string") errors.push("accent must be a string");
|
|
92
|
-
return errors;
|
|
93
|
-
},
|
|
94
|
-
audio: [
|
|
95
|
-
{ src: "assets/chime.wav", startFrame: 18, volume: 0.5 },
|
|
96
|
-
{ src: "assets/chime.wav", startFrame: 300, volume: 0.38 },
|
|
97
|
-
{ src: "assets/chime.wav", startFrame: 600, volume: 0.42 },
|
|
98
|
-
{ src: "assets/chime.wav", startFrame: 840, volume: 0.55 },
|
|
99
|
-
],
|
|
100
|
-
mount({ container, props }) {
|
|
101
|
-
container.style.setProperty("--accent", props.accent);
|
|
102
|
-
container.innerHTML =
|
|
103
|
-
"<style>" +
|
|
104
|
-
"@keyframes orbit { from { transform: rotate(0deg) translateX(210px) rotate(0deg); } to { transform: rotate(360deg) translateX(210px) rotate(-360deg); } }" +
|
|
105
|
-
"@keyframes breathe { 0%, 100% { opacity: .35; transform: scale(1); } 50% { opacity: .7; transform: scale(1.12); } }" +
|
|
106
|
-
"#scene { position: relative; width: 100%; height: 100%; overflow: hidden; background: #080a0d; color: #eef2f7; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }" +
|
|
107
|
-
"#field { position: absolute; inset: 0; width: 100%; height: 100%; }" +
|
|
108
|
-
"#orb { position: absolute; top: 46%; left: 38%; width: 460px; height: 460px; margin: -230px 0 0 -230px; border-radius: 50%; background: var(--accent); filter: blur(110px); opacity: .28; animation: orbit 30s linear infinite;" +
|
|
109
|
-
" }" +
|
|
110
|
-
"#glow { position: absolute; top: 8%; right: 6%; width: 280px; height: 280px; border-radius: 50%; background: var(--accent); filter: blur(90px); animation: breathe 9s ease-in-out infinite; }" +
|
|
111
|
-
"#rail { position: absolute; top: 92px; left: 160px; right: 160px; height: 1px; background: rgba(255,255,255,.14); }" +
|
|
112
|
-
"#eyebrow { position: absolute; top: 64px; left: 160px; font: 600 14px/1 ui-monospace, monospace; letter-spacing: .28em; text-transform: uppercase; color: var(--accent); }" +
|
|
113
|
-
"#meta { position: absolute; top: 64px; right: 160px; font: 500 14px/1 ui-monospace, monospace; letter-spacing: .12em; color: #788595; }" +
|
|
114
|
-
"#copy { position: absolute; left: 160px; top: 236px; width: 1320px; }" +
|
|
115
|
-
"#title { margin: 0; font-size: 156px; line-height: .92; letter-spacing: -.055em; font-weight: 680; }" +
|
|
116
|
-
"#subtitle { margin: 34px 0 0; max-width: 820px; font-size: 31px; line-height: 1.45; color: #96a2b2; font-weight: 420; }" +
|
|
117
|
-
"#signal { position: absolute; left: 0; bottom: 0; width: 1920px; height: 1080px; overflow: visible; }" +
|
|
118
|
-
"#caption { position: absolute; left: 160px; bottom: 92px; font: 500 13px/1 ui-monospace, monospace; letter-spacing: .16em; color: #7e8b9b; }" +
|
|
119
|
-
"#caption strong { color: var(--accent); font-weight: 600; }" +
|
|
120
|
-
"#progress-track { position: absolute; left: 160px; right: 160px; bottom: 72px; height: 2px; background: rgba(255,255,255,.1); }" +
|
|
121
|
-
"#progress { position: absolute; inset: 0 auto 0 0; width: 0%; background: var(--accent); box-shadow: 0 0 18px var(--accent); }" +
|
|
122
|
-
"#vignette { position: absolute; inset: 0; background: radial-gradient(ellipse at center, transparent 48%, rgba(0,0,0,.42) 100%); pointer-events: none; }" +
|
|
123
|
-
"</style>" +
|
|
124
|
-
'<div id="scene">' +
|
|
125
|
-
'<canvas id="field" width="1920" height="1080"></canvas>' +
|
|
126
|
-
'<div id="orb"></div><div id="glow"></div><div id="vignette"></div>' +
|
|
127
|
-
'<div id="rail"></div><div id="eyebrow">Frameyard / Field Notes</div><div id="meta">1920×1080 · 30 FPS · 30.00 S</div>' +
|
|
128
|
-
'<div id="copy"><h1 id="title"></h1><p id="subtitle"></p></div>' +
|
|
129
|
-
'<svg id="signal" viewBox="0 0 1920 1080" fill="none" aria-hidden="true">' +
|
|
130
|
-
'<path id="signal-path" stroke="var(--accent)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity=".82"/>' +
|
|
131
|
-
"</svg>" +
|
|
132
|
-
'<div id="caption">FRAME <strong id="frame">000</strong> / 899 · SEEK-SAFE CANVAS + SVG + CSS</div>' +
|
|
133
|
-
'<div id="progress-track"><div id="progress"></div></div>' +
|
|
134
|
-
"</div>";
|
|
135
|
-
|
|
136
|
-
scene = container.querySelector("#scene");
|
|
137
|
-
title = container.querySelector("#title");
|
|
138
|
-
subtitle = container.querySelector("#subtitle");
|
|
139
|
-
kaption = container.querySelector("#frame");
|
|
140
|
-
progress = container.querySelector("#progress");
|
|
141
|
-
signalPath = container.querySelector("#signal-path");
|
|
142
|
-
starfield = container.querySelector("#field");
|
|
143
|
-
context = starfield.getContext("2d");
|
|
144
|
-
makeStars();
|
|
145
|
-
buildBars();
|
|
146
|
-
buildSignalPath();
|
|
147
|
-
title.textContent = props.title;
|
|
148
|
-
subtitle.textContent = props.subtitle;
|
|
149
|
-
},
|
|
150
|
-
update({ frame, props }) {
|
|
151
|
-
const intro = easeOut(frame / 52);
|
|
152
|
-
const copyIntro = easeOut((frame - 26) / 62);
|
|
153
|
-
const signalReveal = easeOut((frame - 96) / 260);
|
|
154
|
-
const outro = 1 - clamp((frame - 824) / 70);
|
|
155
|
-
const total = 899;
|
|
156
|
-
|
|
157
|
-
scene.style.opacity = String(outro);
|
|
158
|
-
title.textContent = props.title;
|
|
159
|
-
subtitle.textContent = props.subtitle;
|
|
160
|
-
title.style.opacity = String(intro);
|
|
161
|
-
title.style.transform = "translateY(" + (1 - intro) * 46 + "px)";
|
|
162
|
-
subtitle.style.opacity = String(copyIntro);
|
|
163
|
-
subtitle.style.transform = "translateY(" + (1 - copyIntro) * 26 + "px)";
|
|
164
|
-
kaption.textContent = String(frame).padStart(3, "0");
|
|
165
|
-
progress.style.width = (frame / total) * 100 + "%";
|
|
166
|
-
|
|
167
|
-
const width = starfield.width;
|
|
168
|
-
const height = starfield.height;
|
|
169
|
-
context.clearRect(0, 0, width, height);
|
|
170
|
-
for (const star of stars) {
|
|
171
|
-
const drift = (frame * star.speed) % width;
|
|
172
|
-
const x = (star.x * width + drift) % width;
|
|
173
|
-
const y = star.y * height;
|
|
174
|
-
context.beginPath();
|
|
175
|
-
context.fillStyle = "rgba(214, 236, 255, " + star.alpha * (0.45 + 0.55 * intro) + ")";
|
|
176
|
-
context.arc(x, y, star.radius, 0, Math.PI * 2);
|
|
177
|
-
context.fill();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
signalPath.style.strokeDashoffset = String(signalLength * (1 - signalReveal));
|
|
181
|
-
signalPath.style.opacity = String(0.18 + signalReveal * 0.64);
|
|
182
|
-
|
|
183
|
-
const barReveal = clamp((frame - 120) / 240);
|
|
184
|
-
for (let index = 0; index < bars.length; index += 1) {
|
|
185
|
-
const bar = bars[index];
|
|
186
|
-
const local = clamp(barReveal * BAR_COUNT - index);
|
|
187
|
-
const height = bar.height * local;
|
|
188
|
-
bar.element.setAttribute("height", height.toFixed(2));
|
|
189
|
-
bar.element.setAttribute("y", String(BASELINE - height));
|
|
190
|
-
bar.element.setAttribute("opacity", (0.25 + local * 0.62).toFixed(3));
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
dispose() {
|
|
194
|
-
scene = undefined;
|
|
195
|
-
title = undefined;
|
|
196
|
-
subtitle = undefined;
|
|
197
|
-
kaption = undefined;
|
|
198
|
-
progress = undefined;
|
|
199
|
-
signalPath = undefined;
|
|
200
|
-
starfield = undefined;
|
|
201
|
-
context = undefined;
|
|
202
|
-
bars = [];
|
|
203
|
-
stars = undefined;
|
|
204
|
-
signalLength = 0;
|
|
205
|
-
},
|
|
206
|
-
});
|