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.
Files changed (168) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/GLOSSARY.md +46 -2
  3. package/README.md +174 -106
  4. package/dist/bindings.d.ts +42 -0
  5. package/dist/bindings.d.ts.map +1 -0
  6. package/dist/bindings.js +144 -0
  7. package/dist/bindings.js.map +1 -0
  8. package/dist/cli.js +32 -21
  9. package/dist/cli.js.map +1 -1
  10. package/dist/composition/adapter.d.ts +30 -0
  11. package/dist/composition/adapter.d.ts.map +1 -0
  12. package/dist/composition/adapter.js +2 -0
  13. package/dist/composition/adapter.js.map +1 -0
  14. package/dist/composition/animation.d.ts +19 -0
  15. package/dist/composition/animation.d.ts.map +1 -0
  16. package/dist/composition/animation.js +55 -0
  17. package/dist/composition/animation.js.map +1 -0
  18. package/dist/composition/attributes.d.ts +7 -0
  19. package/dist/composition/attributes.d.ts.map +1 -0
  20. package/dist/composition/attributes.js +24 -0
  21. package/dist/composition/attributes.js.map +1 -0
  22. package/dist/composition/descriptor.d.ts +30 -0
  23. package/dist/composition/descriptor.d.ts.map +1 -0
  24. package/dist/composition/descriptor.js +188 -0
  25. package/dist/composition/descriptor.js.map +1 -0
  26. package/dist/composition/diagnostics.d.ts +12 -0
  27. package/dist/composition/diagnostics.d.ts.map +1 -0
  28. package/dist/composition/diagnostics.js +38 -0
  29. package/dist/composition/diagnostics.js.map +1 -0
  30. package/dist/composition/index.d.ts +12 -0
  31. package/dist/composition/index.d.ts.map +1 -0
  32. package/dist/composition/index.js +7 -0
  33. package/dist/composition/index.js.map +1 -0
  34. package/dist/composition/media.d.ts +27 -0
  35. package/dist/composition/media.d.ts.map +1 -0
  36. package/dist/composition/media.js +162 -0
  37. package/dist/composition/media.js.map +1 -0
  38. package/dist/composition/paths.d.ts +5 -0
  39. package/dist/composition/paths.d.ts.map +1 -0
  40. package/dist/composition/paths.js +16 -0
  41. package/dist/composition/paths.js.map +1 -0
  42. package/dist/composition/transcripts.d.ts +19 -0
  43. package/dist/composition/transcripts.d.ts.map +1 -0
  44. package/dist/composition/transcripts.js +57 -0
  45. package/dist/composition/transcripts.js.map +1 -0
  46. package/dist/composition/walk.d.ts +10 -0
  47. package/dist/composition/walk.d.ts.map +1 -0
  48. package/dist/composition/walk.js +32 -0
  49. package/dist/composition/walk.js.map +1 -0
  50. package/dist/contracts.d.ts +75 -47
  51. package/dist/contracts.d.ts.map +1 -1
  52. package/dist/contracts.js +14 -159
  53. package/dist/contracts.js.map +1 -1
  54. package/dist/errors.d.ts +15 -4
  55. package/dist/errors.d.ts.map +1 -1
  56. package/dist/errors.js +8 -0
  57. package/dist/errors.js.map +1 -1
  58. package/dist/html/document-adapter.d.ts +5 -0
  59. package/dist/html/document-adapter.d.ts.map +1 -0
  60. package/dist/html/document-adapter.js +17 -0
  61. package/dist/html/document-adapter.js.map +1 -0
  62. package/dist/html/index.d.ts +5 -0
  63. package/dist/html/index.d.ts.map +1 -0
  64. package/dist/html/index.js +3 -0
  65. package/dist/html/index.js.map +1 -0
  66. package/dist/html/parse.d.ts +26 -0
  67. package/dist/html/parse.d.ts.map +1 -0
  68. package/dist/html/parse.js +80 -0
  69. package/dist/html/parse.js.map +1 -0
  70. package/dist/html/validate.d.ts +14 -0
  71. package/dist/html/validate.d.ts.map +1 -0
  72. package/dist/html/validate.js +648 -0
  73. package/dist/html/validate.js.map +1 -0
  74. package/dist/index.d.ts +2 -2
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/preview/main.d.ts +10 -3
  79. package/dist/preview/main.d.ts.map +1 -1
  80. package/dist/preview/main.js +189 -89
  81. package/dist/preview/main.js.map +1 -1
  82. package/dist/project.d.ts +2 -0
  83. package/dist/project.d.ts.map +1 -1
  84. package/dist/project.js +4 -0
  85. package/dist/project.js.map +1 -1
  86. package/dist/renderer/browser.d.ts +11 -1
  87. package/dist/renderer/browser.d.ts.map +1 -1
  88. package/dist/renderer/browser.js +93 -14
  89. package/dist/renderer/browser.js.map +1 -1
  90. package/dist/renderer/index.d.ts +25 -5
  91. package/dist/renderer/index.d.ts.map +1 -1
  92. package/dist/renderer/index.js +82 -16
  93. package/dist/renderer/index.js.map +1 -1
  94. package/dist/runtime/bindings.d.ts +11 -0
  95. package/dist/runtime/bindings.d.ts.map +1 -0
  96. package/dist/runtime/bindings.js +164 -0
  97. package/dist/runtime/bindings.js.map +1 -0
  98. package/dist/runtime/diagnostics.d.ts +4 -0
  99. package/dist/runtime/diagnostics.d.ts.map +1 -0
  100. package/dist/runtime/diagnostics.js +107 -0
  101. package/dist/runtime/diagnostics.js.map +1 -0
  102. package/dist/runtime/document-adapter.d.ts +4 -0
  103. package/dist/runtime/document-adapter.d.ts.map +1 -0
  104. package/dist/runtime/document-adapter.js +19 -0
  105. package/dist/runtime/document-adapter.js.map +1 -0
  106. package/dist/runtime/host.d.ts +14 -0
  107. package/dist/runtime/host.d.ts.map +1 -0
  108. package/dist/runtime/host.js +596 -0
  109. package/dist/runtime/host.js.map +1 -0
  110. package/dist/runtime/index.d.ts +0 -29
  111. package/dist/runtime/index.d.ts.map +1 -1
  112. package/dist/runtime/index.js +0 -360
  113. package/dist/runtime/index.js.map +1 -1
  114. package/dist/runtime/media.d.ts.map +1 -1
  115. package/dist/runtime/media.js +20 -51
  116. package/dist/runtime/media.js.map +1 -1
  117. package/dist/scaffold.d.ts.map +1 -1
  118. package/dist/scaffold.js +177 -84
  119. package/dist/scaffold.js.map +1 -1
  120. package/dist/server/composition-html.d.ts +20 -0
  121. package/dist/server/composition-html.d.ts.map +1 -0
  122. package/dist/server/composition-html.js +45 -0
  123. package/dist/server/composition-html.js.map +1 -0
  124. package/dist/server/preview-html.d.ts.map +1 -1
  125. package/dist/server/preview-html.js +19 -14
  126. package/dist/server/preview-html.js.map +1 -1
  127. package/dist/server/server.d.ts.map +1 -1
  128. package/dist/server/server.js +94 -6
  129. package/dist/server/server.js.map +1 -1
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/docs/adr/0001-keep-composition-authoring-web-native.md +2 -0
  133. package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +7 -1
  134. package/docs/adr/0007-make-html-documents-the-composition-unit.md +31 -0
  135. package/docs/adr/0008-share-the-composition-document-grammar.md +18 -0
  136. package/docs/adr/0009-composite-production-frames-in-software.md +37 -0
  137. package/docs/migration-0.4.md +279 -0
  138. package/docs/v0.1-design.md +78 -56
  139. package/examples/demo/README.md +8 -4
  140. package/examples/demo/compositions/demo.controller.js +120 -0
  141. package/examples/demo/compositions/demo.html +253 -0
  142. package/examples/demo/video.config.js +1 -1
  143. package/examples/video/README.md +61 -0
  144. package/examples/video/assets/clip.mp4 +0 -0
  145. package/examples/video/assets/clip.webm +0 -0
  146. package/examples/video/compositions/video-mp4.html +362 -0
  147. package/examples/video/compositions/video.html +362 -0
  148. package/examples/video/props/video.json +8 -0
  149. package/examples/video/scripts/make-clip.sh +109 -0
  150. package/examples/video/video.config.js +6 -0
  151. package/examples/webgl/README.md +30 -0
  152. package/examples/webgl/compositions/webgl.controller.js +200 -0
  153. package/examples/webgl/compositions/webgl.html +215 -0
  154. package/examples/webgl/props/webgl.json +6 -0
  155. package/examples/webgl/video.config.js +5 -0
  156. package/package.json +7 -3
  157. package/skills/frameyard/README.md +4 -0
  158. package/skills/frameyard/SKILL.md +40 -12
  159. package/skills/frameyard/references/audio-assets.md +25 -14
  160. package/skills/frameyard/references/bindings.md +73 -0
  161. package/skills/frameyard/references/common-mistakes.md +59 -0
  162. package/skills/frameyard/references/controllers.md +126 -0
  163. package/skills/frameyard/references/deterministic-authoring.md +143 -52
  164. package/skills/frameyard/references/diagnostics.md +90 -0
  165. package/skills/frameyard/references/failure-recovery.md +37 -14
  166. package/skills/frameyard/references/json-results.md +43 -9
  167. package/skills/frameyard/references/subtitles-karaoke.md +10 -18
  168. package/examples/demo/compositions/demo.js +0 -206
@@ -1,10 +1,10 @@
1
- # Frameyard v0.1 Design Baseline
1
+ # Frameyard Design Baseline
2
2
 
3
3
  **Status:** Accepted
4
4
  **Initial acceptance:** 2026-10-27
5
- **Last design revision:** 2026-10-29
5
+ **Last design revision:** 2026-10-30
6
6
 
7
- This document records the decisions confirmed during the initial design sessions. It defines the v0.1 product and architecture baseline; later changes should revise this document explicitly rather than silently broaden the scope.
7
+ This document records the decisions confirmed during design sessions. It is the living product and architecture baseline; later changes revise this document explicitly rather than silently broaden the scope. The original v0.1 baseline has been revised through 0.4. Architecture decisions that changed the baseline are recorded in `docs/adr/`.
8
8
 
9
9
  ## 1. Product contract
10
10
 
@@ -12,10 +12,10 @@ Frameyard is a local-first, open-source framework for frontend developers and cr
12
12
 
13
13
  The initial use cases are product demos, social clips, animated diagrams, data visualizations, karaoke-style subtitles, and personalized videos. Optional local speech synthesis and transcription produce assets for these workflows. Professional long-form editing is outside the target audience.
14
14
 
15
- The core v0.1 success criterion is an end-to-end example in which a developer can:
15
+ The core success criterion is an end-to-end example in which a developer can:
16
16
 
17
- 1. Create a reusable 30-second, 1920×1080 composition.
18
- 2. Animate it with HTML, CSS, SVG, JavaScript, and seek-safe Canvas 2D.
17
+ 1. Create a reusable 30-second, 1920×1080 HTML composition.
18
+ 2. Animate it with HTML, CSS, SVG, and, where needed, a seek-safe controller for Canvas 2D and WebGL.
19
19
  3. Supply JSON props and synchronized audio.
20
20
  4. Preview and scrub it interactively.
21
21
  5. Render it reproducibly to MP4 without dropped frames or timing drift.
@@ -24,33 +24,37 @@ The agent-enabled success criterion additionally requires an agent using the off
24
24
 
25
25
  ## 2. Project and authoring model
26
26
 
27
- - Projects use ordinary HTML, CSS, static assets, and native JavaScript ES modules.
28
- - `video.config.js` explicitly lists composition module entry points. There is no directory-scanning discovery convention.
27
+ - Projects use ordinary HTML, CSS, static assets, and native JavaScript controllers, either inline in a composition document or in external modules.
28
+ - `video.config.js` explicitly lists `.html` composition entry points. There is no directory-scanning discovery convention.
29
29
  - A project may contain multiple compositions, each with a unique ID.
30
- - Each composition has fixed `width`, `height`, `fps`, and `durationInFrames` metadata. Props cannot change this metadata in v0.1.
31
- - A composition follows a `mount → update({ frame, props }) → dispose` lifecycle. Mounting creates the visual structure; updating makes that structure represent the requested frame.
32
- - `update()` must be seek-safe and synchronous. It cannot depend on previously visited frames.
33
- - Props are JSON-serializable, immutable, and fixed for one render. Composition defaults may be overridden by a JSON file or CLI option.
34
- - The core validates metadata and JSON-serializability. An optional composition-defined `validateProps()` hook handles domain-specific validation before loading.
35
- - User compositions are native JavaScript. The framework itself is authored in TypeScript and ships declarations, but v0.1 does not transpile user TypeScript.
30
+ - A composition is a full HTML document with one `<fy-composition>` root. The root declares fixed `width`, `height`, `fps`, and `duration-in-frames` metadata. Props cannot change this metadata.
31
+ - Default props are a JSON block, `<script type="application/json" data-fy="props">`. A render's JSON props merge shallowly over the defaults, are validated as JSON, deep-frozen, and fixed for the render.
32
+ - Props bind into the document through explicit `data-fy-*` attributes: `data-fy-text`, `data-fy-attr-*`, `data-fy-class-*`, `data-fy-if`, and `data-fy-repeat`. Primitive top-level props are also exposed as `--fy-prop-<name>` CSS custom properties. Bindings apply once per load; props are immutable.
33
+ - CSS and the Web Animations API are the animation language. `data-fy-anim-start`, `data-fy-anim-duration`, and `data-fy-anim-easing` schedule one animation per element in composition frames.
34
+ - An optional composition controller is declared with `<script type="text/fy-controller">`. It is either inline source or an external module referenced by `src`; a composition has exactly one controller. It may export `mount({ root, props })`, `update({ frame, props, root })`, `dispose()`, and `validateProps(props)`; at least one is required, and `mount` and `update` are optional for validation-only controllers. It has no metadata, default props, audio, or transcripts. Inline keeps small compositions in one file; external is for reuse, tests, and complex logic.
35
+ - Inline controllers are the default for small, single-use controllers. Extract to an external module when the controller grows, is reused, needs separate imports or tests, or drives Canvas 2D, WebGL, or karaoke.
36
+ - The controller is the only executable code in a composition document. Other executable `<script>` elements and inline `on*` handlers are rejected. Libraries are imported as ES modules from the controller.
37
+ - `fy` is the authoring namespace: `fy-composition`, `fy-transcript`, `data-fy-*`, `--fy-*`, and `text/fy-controller`. `frameyard` remains the product, package, CLI, config, manifest, server-route, import-specifier, and error-code namespace.
38
+ - User controllers are native JavaScript. The framework itself is authored in TypeScript and ships declarations, but 0.4 does not transpile user TypeScript.
36
39
 
37
40
  ## 3. Deterministic execution
38
41
 
39
42
  The only sources of animation state are the integer frame number and immutable props. Composition frame rate establishes the mapping between frames and time.
40
43
 
41
- - The runtime owns the frame clock and deterministic randomness.
42
- - CSS `@keyframes` and Web Animations API animations are paused and positioned from the shared frame clock.
43
- - JavaScript animation runs inside `update()`.
44
+ - The runtime owns the frame clock and deterministic randomness. The controller imports `random("stable:key")` from `frameyard/runtime`.
45
+ - CSS `@keyframes` and Web Animations API animations are paused and positioned from the shared frame clock. `data-fy-anim-*` attributes schedule one animation per element in composition frames; the runtime maps frames to milliseconds and sets the corresponding animation delay and duration.
46
+ - The runtime sets `--fy-frame`, `--fy-time`, `--fy-progress`, `--fy-fps`, `--fy-duration`, and `--fy-duration-frames` on the composition root every seek. Authors use them for simple computed CSS values.
47
+ - JavaScript animation runs in the controller's `update()`, which must be synchronous and seek-safe. It cannot depend on previously visited frames.
48
+ - Declarative bindings apply once per load, before the controller mounts. Controller-created DOM is the controller's responsibility.
44
49
  - Independent `requestAnimationFrame` loops, timers, CSS transitions, wall-clock reads, and unseeded randomness are not supported animation drivers.
45
- - Deterministic random values are stateless and key-based, such as `random("star:17")`, so they do not depend on call order or rendering history.
46
- - Development mode provides best-effort diagnostics for forbidden clocks and randomness; it does not claim to police every third-party library perfectly.
47
- - Assets, remote data, images, fonts, generated audio, and transcripts are resolved during an explicit loading phase before frame production.
48
- - Required network access is limited to the loading phase. Network requests are blocked during frame capture.
49
- - AI model inference is never part of `mount()`, `update()`, or frame production.
50
- - v0.1 supports DOM, CSS, SVG, Canvas 2D, WebGL, and frame-seeked embedded video.
51
- - Embedded video is loaded during the loading phase, paused, and seeked from the frame-derived source time before capture. Its source must be a local, seekable project file. An unmuted embedded video contributes its audio track to the offline mix unless the composition opts out.
52
- - WebGL 1 and WebGL 2 canvases are redrawn from the current frame and captured through the normal screenshot path. Production sessions pin software WebGL so pixel output does not depend on the host GPU.
53
- - Composition code is trusted local developer code. Frameyard does not claim to render untrusted compositions safely.
50
+ - Development mode provides best-effort diagnostics for forbidden clocks and randomness in the controller; it does not claim to police every third-party library perfectly.
51
+ - Assets, remote data, images, fonts, generated audio, and transcripts are resolved during an explicit loading phase before frame production. Network requests are blocked during frame capture.
52
+ - AI model inference is never part of the loading phase, controller lifecycle, or frame production.
53
+ - The baseline supports DOM, CSS, SVG, Canvas 2D, WebGL, and frame-seeked embedded video. Canvas 2D and WebGL are drawn from the current frame by the controller.
54
+ - Embedded video and audio are declared in the composition HTML with `data-fy-*` attributes. The runtime loads local files during the loading phase, pauses them, and seeks video from the frame-derived source time before capture. An unmuted video contributes its audio track to the offline mix unless the composition opts out.
55
+ - Full-scene opacity animations above an embedded video can make interactive preview reuse the load frame after a large seek; fade overlay layers instead. Capture sessions keep the composition hidden until the first seek, so production stills resolve to the requested frame.
56
+ - WebGL 1 and WebGL 2 canvases are captured through the normal screenshot path. Production sessions pin software WebGL so pixel output does not depend on the host GPU.
57
+ - Compositions are trusted local developer code. Frameyard does not claim to render untrusted compositions safely.
54
58
 
55
59
  ### Reproducibility guarantee
56
60
 
@@ -60,7 +64,7 @@ Generated assets are covered once frozen. Frameyard does not promise that repeat
60
64
 
61
65
  ## 4. Preview and production rendering
62
66
 
63
- Preview and production execute the same composition bundle and browser runtime. They differ only in which component drives the frame clock and whether frames are captured.
67
+ Preview and production execute the same composition document and browser host runtime. They differ only in which component drives the frame clock and whether frames are captured.
64
68
 
65
69
  The preview provides:
66
70
 
@@ -72,23 +76,30 @@ The preview provides:
72
76
 
73
77
  It is a developer inspection tool, not a visual composition editor.
74
78
 
79
+ The preview server serves each composition HTML document at its project path and injects the framework import map and a host module into composition entries. The host reads the composition descriptor—metadata, default props, and the controller declaration—from the same browser-safe composition document reader used by static validation, applies bindings, imports the controller from its authoring `src` or from the inline block's virtual module URL, mounts it, and exposes a small `window.__fy` API. Preview loads the document in a same-origin iframe and drives that API. The iframe is not a security sandbox; compositions are trusted local code. The server runs the static composition validation described in §6 before serving a composition to the preview.
80
+
75
81
  Production rendering:
76
82
 
77
83
  1. Uses a pinned, Playwright-managed Chromium through the Chrome DevTools Protocol.
78
- 2. Seeks explicitly to each requested frame.
79
- 3. Waits for composition readiness, embedded media decode, and browser paint.
80
- 4. Captures the frame.
81
- 5. Pipes frames in timeline order to FFmpeg for encoding and audio muxing.
84
+ 2. Navigates directly to the composition document, waits for the host, and loads the render's props.
85
+ 3. Seeks explicitly to each requested frame.
86
+ 4. Waits for composition readiness, embedded media decode, and browser paint.
87
+ 5. Captures the frame.
88
+ 6. Pipes frames in timeline order to FFmpeg for encoding and audio muxing.
89
+
90
+ `check` and `still` use the same host and capture path. `check` evaluates every frame without capturing or encoding. `still` captures one frame through the production path.
82
91
 
83
- The renderer is sequential by default and has no frame cache. Optional `--concurrency <n>` rendering, with `n` from 1 to 16, opens up to `n` independent browser sessions, each loading the same composition and props from the same preview server. Workers seek and capture assigned frames, while a single ordered writer pipes them to FFmpeg with bounded lookahead. Parallelism therefore changes scheduling only: every frame remains derivable from its integer frame number and immutable props, and output order is unchanged. The default stays at one session for predictable resource use, and the upper bound limits accidental browser-process and memory growth. Simple scenes target real-time preview, but there is no production rendering-speed guarantee; the CLI reports progress, current frame, and elapsed time.
92
+ The renderer is sequential by default and has no frame cache. Optional `--concurrency <n>` rendering, with `n` from 1 to 16, opens up to `n` independent browser sessions, each loading the same composition document and props from the same preview server. Workers seek and capture assigned frames, while a single ordered writer pipes them to FFmpeg with bounded lookahead. Parallelism therefore changes scheduling only: every frame remains derivable from its integer frame number and immutable props, and output order is unchanged. The default stays at one session for predictable resource use, and the upper bound limits accidental browser-process and memory growth. Simple scenes target real-time preview, but there is no production rendering-speed guarantee; the CLI reports progress, current frame, and elapsed time.
84
93
 
85
94
  ## 5. Audio and output
86
95
 
87
- Audio is a declarative timeline of clips. Each clip supports a source, start frame, source trim, and volume. Preview synchronizes this timeline in the browser; production mixes it offline with FFmpeg rather than treating live `<audio>` or Web Audio playback as the production source of truth. Unmuted embedded videos contribute their own audio tracks to the same mix, placed by the video's frame and trim attributes. Muting a video or setting `data-frameyard-audio="off"` keeps it visual-only.
96
+ Audio is a declarative timeline of `<audio>` elements. Each element uses `data-fy-start` (composition frame), `data-fy-trim` (seconds from the source start), optional `data-fy-duration` (seconds to use), and `data-fy-volume` (0-1). Preview synchronizes this timeline in the browser; production mixes it offline with FFmpeg rather than treating live `<audio>` or Web Audio playback as the production source of truth. Unmuted embedded videos contribute their own audio tracks to the same mix, placed by the video's frame and trim attributes. Muting a video or setting `data-fy-audio="off"` keeps it visual-only.
97
+
98
+ Audio, video, and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs (`img`, stylesheets) resolve relative to the composition document.
88
99
 
89
100
  Generated speech behaves like any other audio clip. Frameyard does not automatically time-stretch it, change composition duration, or regenerate it to fit. `check` reports clip durations and overrun warnings so users and agents can revise text, timing, or composition deliberately.
90
101
 
91
- v0.1 produces portable MP4 files with H.264 video and AAC audio. It exposes only a small set of validated encoding options, not arbitrary FFmpeg arguments. Single-frame PNG output through `still` is a debugging artifact, not general image-sequence support.
102
+ 0.4 produces portable MP4 files with H.264 video and AAC audio. It exposes only a small set of validated encoding options, not arbitrary FFmpeg arguments. Single-frame PNG output through `still` is a debugging artifact, not general image-sequence support.
92
103
 
93
104
  A render fails immediately on an uncaught exception, unhandled rejection, required-asset failure, or invalid composition output. Diagnostics identify the composition and, where applicable, the frame. The CLI exits nonzero, cleans temporary files, and never reports a partial video as successful. Existing output files are not overwritten unless explicitly requested.
94
105
 
@@ -96,36 +107,38 @@ A render fails immediately on an uncaught exception, unhandled rejection, requir
96
107
 
97
108
  Project and composition commands are:
98
109
 
99
- - `init`: scaffold a minimal project, including an agent guide (`AGENTS.md`) and a private `package.json` with pinned `dev`, `check`, `render`, and `still` scripts
110
+ - `init`: scaffold a minimal HTML composition project, including an agent guide (`AGENTS.md`), a `.gitignore` for `out/`, and a private `package.json` with pinned `dev`, `check`, `render`, and `still` scripts
100
111
  - `dev`: start the preview
101
- - `render <composition-id> --props props.json --out video.mp4`: render a composition, optionally with `--concurrency <n>` from 1 to 16
102
- - `check <composition-id>`: validate configuration, props, assets, audio, and transcript requirements, then mount the composition and evaluate every frame without capturing or encoding
112
+ - `render <composition-id> [--out video.mp4]`: render a composition, optionally with `--concurrency <n>` from 1 to 16; the default output is `out/<composition-id>.mp4` in the project root
113
+ - `check <composition-id>`: run static composition validation, then validate configuration, props, assets, audio, and transcript requirements, load the composition, and evaluate every frame without capturing or encoding
103
114
  - `still <composition-id> --frame <n>`: capture one requested frame as PNG through the production rendering path
104
115
  - `doctor`: inspect core requirements and optional integrations without repairing or downloading anything
105
116
 
117
+ `check`, `still`, and `render` run a Node-side static composition validation before launching Chromium. It validates metadata, the composition root, the controller declaration, default props, binding attributes and paths, animation attributes, scripts and event handlers, unsupported visual elements, and audio/transcript references. Static validation and the composition host read the composition grammar through one browser-safe composition document reader with parse5 and DOM adapters; the reader returns fixed facts and per-kind declarations, while the validator adds source locations, filesystem checks, and transcript parsing. Controller `src` is project-relative in composition source info and results; the authoring value is retained as `rawSrc` for the host import. Diagnostics carry stable codes and optional source locations; `check` exits nonzero on static errors without starting a browser. `dev` runs the same validation on load and surfaces its diagnostics in the preview.
118
+
106
119
  Local asset-production commands are `tts` and `transcribe`. Every non-interactive command supports `--json` with stable result and error schemas. Structured results go to stdout; human diagnostics and inference progress go to stderr.
107
120
 
108
- The built-in server serves native ES modules and static assets and injects an import map for the framework runtime. It performs no dependency pre-bundling. Third-party libraries must initially be browser-ready ES modules referenced explicitly. Running through the CLI server is required; direct `file://` execution is unsupported.
121
+ The built-in server serves native ES modules and static assets, injects an import map and composition host into composition entries, and serves an inline controller through a virtual module URL under the composition's directory. It performs no dependency pre-bundling. Third-party libraries must initially be browser-ready ES modules referenced explicitly. Running through the CLI server is required; direct `file://` execution is unsupported for deterministic preview or rendering. A composition document can still be opened directly as static HTML/CSS.
109
122
 
110
- v0.1 uses one repository and one publishable npm package with three internal layers:
123
+ Frameyard uses one repository and one publishable npm package with three internal layers:
111
124
 
112
- 1. Shared composition contracts
113
- 2. Browser runtime
125
+ 1. Shared composition contracts and HTML binding/metadata parsing
126
+ 2. Browser host runtime
114
127
  3. Node-based CLI and renderer
115
128
 
116
129
  These layers have clean internal boundaries but no public plugin API. FFmpeg is discovered on the user's PATH and checked for required capabilities, with actionable installation and version diagnostics. The project supports the current active Node.js LTS; Linux is the reference and CI platform, while macOS and Windows remain best-effort until covered by real rendering tests.
117
130
 
118
131
  ## 7. Official agent skill
119
132
 
120
- Frameyard ships an optional, host-neutral agent skill with a `SKILL.md`, supporting references, and thin scripts. The npm package includes skill files matching its API version, and documentation explains host-specific installation or referencing. v0.1 does not add a skill-installer command.
133
+ Frameyard ships an optional, host-neutral agent skill with a `SKILL.md`, supporting references, and thin scripts. The npm package includes skill files matching its API version, and documentation explains host-specific installation or referencing. 0.4 does not add a skill-installer command.
121
134
 
122
135
  The skill:
123
136
 
124
- - Uses the normal project format, composition API, and CLI available to humans.
137
+ - Uses the normal project format, HTML composition contract, optional controller, and CLI available to humans.
125
138
  - Does not introduce an agent-only runtime, MCP server, hosted control plane, or separate rendering API.
126
139
  - Triggers for Frameyard authoring and operation rather than claiming to be a general-purpose media-generation skill.
127
140
  - Uses Frameyard's audio wrappers instead of invoking `qwen-tts` or `whisper-cli` directly.
128
- - Includes references for deterministic authoring, audio assets, subtitle and karaoke recipes, JSON command results, and failure recovery.
141
+ - Includes references for deterministic HTML authoring, bindings, animation scheduling, controller rules, static validation, structured diagnostics, common authoring mistakes, audio assets, subtitle and karaoke recipes, JSON command results, and failure recovery.
129
142
  - Invokes speech synthesis or transcription only when requested or explicitly approved as part of the brief.
130
143
  - Does not overwrite or regenerate existing generated assets without permission.
131
144
  - Runs `check`, inspects representative `still` images when the host supports image viewing, and discloses when visual inspection was impossible.
@@ -137,7 +150,7 @@ A successful agent workflow preserves and reports the source project, props, gen
137
150
 
138
151
  The `tts` command integrates [`qwentts.cpp`](https://github.com/ServeurpersoCom/qwentts.cpp) through its external `qwen-tts` executable. It does not link the native library or require the upstream server.
139
152
 
140
- v0.1 supports CustomVoice mode only:
153
+ 0.4 supports CustomVoice mode only:
141
154
 
142
155
  - A named speaker is required.
143
156
  - An optional style instruction and language may be supplied.
@@ -163,19 +176,19 @@ The canonical artifact is a versioned Frameyard JSON document containing:
163
176
  - Timed segments
164
177
  - Optional word timings
165
178
 
166
- All timestamps are integer milliseconds relative to the source audio, not composition frame numbers. Composition code converts them using the audio clip's start frame, source trim, and composition frame rate.
179
+ All timestamps are integer milliseconds relative to the source audio, not composition frame numbers. The controller converts them using the audio clip's start frame, source trim, and composition frame rate.
167
180
 
168
- Compositions may declare required transcript assets with a `transcripts` field analogous to `audio`:
181
+ Compositions declare required transcript assets with `<fy-transcript>` elements:
169
182
 
170
- ```js
171
- transcripts: [{ src: "assets/voice.transcript.json", words: true }]
183
+ ```html
184
+ <fy-transcript src="assets/voice.transcript.json" words></fy-transcript>
172
185
  ```
173
186
 
174
187
  `check` and production validate declared transcript files before frame production. `words: true` requires measured word timings and fails when they are unavailable; it never triggers interpolation.
175
188
 
176
189
  SRT and VTT are derived exports, not the canonical contract. Word-level karaoke requires actual word timings and fails validation when they are unavailable. Frameyard never fabricates word timings by interpolating segment timestamps and never executes Whisper's generated bash/FFmpeg karaoke script.
177
190
 
178
- The official skill documents seek-safe subtitle and karaoke recipes. Compositions retain responsibility for visual presentation; v0.1 has no built-in subtitle renderer and does not automatically burn captions into videos.
191
+ The official skill documents seek-safe subtitle and karaoke recipes. Compositions retain responsibility for visual presentation; 0.4 has no built-in subtitle renderer and does not automatically burn captions into videos. Karaoke presentation uses the optional controller.
179
192
 
180
193
  ## 10. Generated assets and inference operation
181
194
 
@@ -200,12 +213,17 @@ Release documentation must identify the exact `qwentts.cpp` and `whisper.cpp` re
200
213
 
201
214
  Tests prioritize public behavior and end-to-end evidence over implementation-only unit coverage:
202
215
 
216
+ - HTML compositions validate metadata, default props, bindings, conditionals, repeats, and controller declarations, including inline/external parity and the exactly-one-controller rule.
217
+ - Static composition validation catches metadata, root/controller count, binding, animation, script, element, and asset-reference errors before Chromium.
218
+ - Diagnostics carry stable codes and source locations; JSON results expose the composition entry, controller declaration, and top-level diagnostics.
203
219
  - Sequential, individual, random-order, and parallel frame renders produce equivalent images.
204
220
  - Determinism checks compare captured pixels or stills; encoded MP4 bytes are not a reproducibility oracle.
205
- - Preview and renderer use the same runtime.
206
- - CSS, JavaScript, SVG, and Canvas animations seek correctly.
207
- - Embedded video frames seek deterministically, including under parallel capture, and their audio appears in the offline mix.
221
+ - Preview and renderer use the same composition document and host runtime.
222
+ - CSS, controller JavaScript, SVG, and Canvas animations seek correctly.
223
+ - Frame-scheduled `data-fy-anim-*` animations and frame-state CSS variables produce the expected frames.
224
+ - Embedded video and audio frames seek deterministically, including under parallel capture, and their audio appears in the offline mix.
208
225
  - WebGL canvases produce deterministic stills and rendered frames under the pinned software renderer.
226
+ - Executable scripts other than the inline or external controller, inline event handlers, `javascript:` URLs, and unsupported visual elements are rejected.
209
227
  - Asset and runtime failures produce nonzero CLI exits.
210
228
  - Rendered MP4 files have the expected dimensions, frame count, duration, and audio stream.
211
229
  - Golden-frame tests cover representative visual behavior.
@@ -214,7 +232,7 @@ Tests prioritize public behavior and end-to-end evidence over implementation-onl
214
232
  - Real-model integration tests are explicitly opt-in and use documented upstream revisions.
215
233
  - Agent-workflow evaluations verify source preservation, provenance, validation, still inspection behavior, failure reporting, and final renders.
216
234
 
217
- ## 12. Deferred from v0.1
235
+ ## 12. Deferred
218
236
 
219
237
  These are scope boundaries, not permanent rejections:
220
238
 
@@ -222,7 +240,10 @@ These are scope boundaries, not permanent rejections:
222
240
  - Cloud generative-AI services, text-to-video generation, and image generation
223
241
  - `qwentts.cpp` reference-audio voice cloning and VoiceDesign
224
242
  - Automatic long-text chunking, streaming speech synthesis, and alternative generated-audio formats
225
- - Built-in subtitle rendering and automatic caption burn-in
243
+ - Built-in subtitle rendering, automatic caption burn-in, and declarative karaoke elements
244
+ - Nested `data-fy-repeat` scopes and a general template language
245
+ - Declarative JSON Schema validation
246
+ - Arbitrary executable scripts in composition HTML
226
247
  - Automatic upstream builds, model downloads, weight redistribution, and environment repair
227
248
  - Agent-only runtimes, MCP servers, hosted agent control planes, and host-specific skill installers
228
249
  - React or other UI-framework adapters in the core
@@ -231,6 +252,7 @@ These are scope boundaries, not permanent rejections:
231
252
  - Live streams, remote media, and other visual sources that cannot be deterministically seeked
232
253
  - Dynamic per-frame asset loading or AI inference
233
254
  - Frame caches
255
+ - Batch still capture; agents can run `still` once per representative frame
234
256
  - Additional final-video formats, general image-sequence output, and arbitrary FFmpeg argument passthrough
235
257
  - Built-in TypeScript transpilation and npm dependency optimization
236
258
  - Rendering untrusted compositions
@@ -239,4 +261,4 @@ These are scope boundaries, not permanent rejections:
239
261
 
240
262
  ## 13. Release posture
241
263
 
242
- Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize.
264
+ Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize. 0.4.0 is a breaking authoring-model release: HTML documents replace JavaScript composition modules, and the `fy` namespace replaces `data-frameyard-*` authoring attributes. See [ADR 0007](./adr/0007-make-html-documents-the-composition-unit.md) and the [0.4 migration guide](./migration-0.4.md).
@@ -1,12 +1,16 @@
1
1
  # Frameyard demo
2
2
 
3
- A 30-second, 1920×1080 composition that exercises the v0.1 core:
3
+ A 30-second, 1920×1080 HTML composition that exercises the current core:
4
4
 
5
5
  - HTML and CSS layout, including seeked CSS keyframe animation
6
- - deterministic Canvas 2D starfield
6
+ - deterministic Canvas 2D starfield in an external controller
7
7
  - SVG path drawing and data bars updated from the frame
8
8
  - JSON props for the title, subtitle, and accent color
9
- - a declarative audio timeline with repeated local chime clips
9
+ - an `<audio>` timeline with repeated local chime clips
10
+ - frame-state CSS variables for the progress bar
11
+
12
+ The composition lives in `compositions/demo.html`; the optional controller is
13
+ `compositions/demo.controller.js`.
10
14
 
11
15
  From the repository root:
12
16
 
@@ -14,7 +18,7 @@ From the repository root:
14
18
  pnpm build
15
19
  node dist/cli.js check demo --project examples/demo
16
20
  node dist/cli.js still demo --frame 240 --project examples/demo --out demo-still.png
17
- node dist/cli.js render demo --project examples/demo --props examples/demo/props/demo.json --out demo.mp4
21
+ node dist/cli.js render demo --project examples/demo --props examples/demo/props/demo.json
18
22
  ```
19
23
 
20
24
  Start the interactive preview with:
@@ -0,0 +1,120 @@
1
+ import { random } from "frameyard/runtime";
2
+
3
+ const STAR_COUNT = 130;
4
+ const BAR_COUNT = 52;
5
+ const BASELINE = 858;
6
+
7
+ let context;
8
+ let signalPath;
9
+ let signalBars;
10
+ let frameLabel;
11
+ let stars;
12
+ let bars = [];
13
+ let signalLength = 0;
14
+
15
+ function clamp(value) {
16
+ return Math.max(0, Math.min(1, value));
17
+ }
18
+
19
+ function easeOut(value) {
20
+ const t = clamp(value);
21
+ return 1 - Math.pow(1 - t, 3);
22
+ }
23
+
24
+ function makeStars() {
25
+ stars = Array.from({ length: STAR_COUNT }, (_, index) => ({
26
+ x: random("star:" + index + ":x"),
27
+ y: random("star:" + index + ":y"),
28
+ radius: 0.6 + random("star:" + index + ":r") * 1.8,
29
+ speed: 6 + random("star:" + index + ":speed") * 34,
30
+ alpha: 0.12 + random("star:" + index + ":alpha") * 0.65,
31
+ }));
32
+ }
33
+
34
+ function buildBars(root) {
35
+ signalBars = root.querySelector("#signal-bars");
36
+ bars = [];
37
+
38
+ for (let index = 0; index < BAR_COUNT; index += 1) {
39
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
40
+ const height = 24 + random("bar:" + index + ":height") * 176;
41
+ const x = 168 + index * 31;
42
+ rect.setAttribute("x", String(x));
43
+ rect.setAttribute("width", "15");
44
+ rect.setAttribute("rx", "7.5");
45
+ rect.setAttribute("fill", "var(--fy-prop-accent)");
46
+ rect.setAttribute("height", "0");
47
+ rect.setAttribute("y", String(BASELINE));
48
+ rect.setAttribute("opacity", "0");
49
+ signalBars.append(rect);
50
+ bars.push({ element: rect, height });
51
+ }
52
+ }
53
+
54
+ function buildSignalPath(root) {
55
+ signalPath = root.querySelector("#signal-path");
56
+ const points = [];
57
+ for (let index = 0; index <= 120; index += 1) {
58
+ const x = 160 + (index / 120) * 1600;
59
+ const wave = Math.sin(index / 7) * 38 + Math.sin(index / 17 + 1.4) * 22 + (random("signal:" + index) - 0.5) * 12;
60
+ points.push([x, 535 + wave]);
61
+ }
62
+ const d = points.map(([x, y], index) => (index === 0 ? "M" : "L") + x + " " + y).join(" ");
63
+ signalPath.setAttribute("d", d);
64
+ signalLength = signalPath.getTotalLength();
65
+ signalPath.style.strokeDasharray = String(signalLength);
66
+ signalPath.style.strokeDashoffset = String(signalLength);
67
+ }
68
+
69
+ export default {
70
+ mount({ root }) {
71
+ context = root.querySelector("#field").getContext("2d");
72
+ frameLabel = root.querySelector("#frame");
73
+ makeStars();
74
+ buildBars(root);
75
+ buildSignalPath(root);
76
+ },
77
+
78
+ update({ frame }) {
79
+ const intro = easeOut(frame / 52);
80
+ const signalReveal = easeOut((frame - 96) / 260);
81
+ const width = context.canvas.width;
82
+ const height = context.canvas.height;
83
+
84
+ context.clearRect(0, 0, width, height);
85
+ for (const star of stars) {
86
+ const drift = (frame * star.speed) % width;
87
+ const x = (star.x * width + drift) % width;
88
+ const y = star.y * height;
89
+ context.beginPath();
90
+ context.fillStyle = "rgba(214, 236, 255, " + star.alpha * (0.45 + 0.55 * intro) + ")";
91
+ context.arc(x, y, star.radius, 0, Math.PI * 2);
92
+ context.fill();
93
+ }
94
+
95
+ signalPath.style.strokeDashoffset = String(signalLength * (1 - signalReveal));
96
+ signalPath.style.opacity = String(0.18 + signalReveal * 0.64);
97
+
98
+ const barReveal = clamp((frame - 120) / 240);
99
+ for (let index = 0; index < bars.length; index += 1) {
100
+ const bar = bars[index];
101
+ const local = clamp(barReveal * BAR_COUNT - index);
102
+ const height = bar.height * local;
103
+ bar.element.setAttribute("height", height.toFixed(2));
104
+ bar.element.setAttribute("y", String(BASELINE - height));
105
+ bar.element.setAttribute("opacity", (0.25 + local * 0.62).toFixed(3));
106
+ }
107
+
108
+ frameLabel.textContent = String(frame).padStart(3, "0");
109
+ },
110
+
111
+ dispose() {
112
+ context = undefined;
113
+ signalPath = undefined;
114
+ signalBars = undefined;
115
+ frameLabel = undefined;
116
+ stars = undefined;
117
+ bars = [];
118
+ signalLength = 0;
119
+ },
120
+ };