frameyard 0.3.0 → 0.4.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 (104) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/GLOSSARY.md +42 -2
  3. package/README.md +162 -101
  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 +25 -10
  9. package/dist/cli.js.map +1 -1
  10. package/dist/contracts.d.ts +73 -47
  11. package/dist/contracts.d.ts.map +1 -1
  12. package/dist/contracts.js +14 -159
  13. package/dist/contracts.js.map +1 -1
  14. package/dist/errors.d.ts +15 -4
  15. package/dist/errors.d.ts.map +1 -1
  16. package/dist/errors.js +8 -0
  17. package/dist/errors.js.map +1 -1
  18. package/dist/html/index.d.ts +5 -0
  19. package/dist/html/index.d.ts.map +1 -0
  20. package/dist/html/index.js +3 -0
  21. package/dist/html/index.js.map +1 -0
  22. package/dist/html/parse.d.ts +30 -0
  23. package/dist/html/parse.d.ts.map +1 -0
  24. package/dist/html/parse.js +80 -0
  25. package/dist/html/parse.js.map +1 -0
  26. package/dist/html/validate.d.ts +14 -0
  27. package/dist/html/validate.d.ts.map +1 -0
  28. package/dist/html/validate.js +931 -0
  29. package/dist/html/validate.js.map +1 -0
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/preview/main.d.ts +10 -3
  35. package/dist/preview/main.d.ts.map +1 -1
  36. package/dist/preview/main.js +186 -88
  37. package/dist/preview/main.js.map +1 -1
  38. package/dist/project.d.ts +2 -0
  39. package/dist/project.d.ts.map +1 -1
  40. package/dist/project.js +4 -0
  41. package/dist/project.js.map +1 -1
  42. package/dist/renderer/browser.d.ts +11 -1
  43. package/dist/renderer/browser.d.ts.map +1 -1
  44. package/dist/renderer/browser.js +62 -10
  45. package/dist/renderer/browser.js.map +1 -1
  46. package/dist/renderer/index.d.ts +25 -5
  47. package/dist/renderer/index.d.ts.map +1 -1
  48. package/dist/renderer/index.js +64 -9
  49. package/dist/renderer/index.js.map +1 -1
  50. package/dist/runtime/bindings.d.ts +11 -0
  51. package/dist/runtime/bindings.d.ts.map +1 -0
  52. package/dist/runtime/bindings.js +164 -0
  53. package/dist/runtime/bindings.js.map +1 -0
  54. package/dist/runtime/diagnostics.d.ts +4 -0
  55. package/dist/runtime/diagnostics.d.ts.map +1 -0
  56. package/dist/runtime/diagnostics.js +107 -0
  57. package/dist/runtime/diagnostics.js.map +1 -0
  58. package/dist/runtime/host.d.ts +12 -0
  59. package/dist/runtime/host.d.ts.map +1 -0
  60. package/dist/runtime/host.js +701 -0
  61. package/dist/runtime/host.js.map +1 -0
  62. package/dist/runtime/index.d.ts +0 -29
  63. package/dist/runtime/index.d.ts.map +1 -1
  64. package/dist/runtime/index.js +0 -360
  65. package/dist/runtime/index.js.map +1 -1
  66. package/dist/runtime/media.js +7 -7
  67. package/dist/runtime/media.js.map +1 -1
  68. package/dist/scaffold.d.ts.map +1 -1
  69. package/dist/scaffold.js +167 -79
  70. package/dist/scaffold.js.map +1 -1
  71. package/dist/server/composition-html.d.ts +20 -0
  72. package/dist/server/composition-html.d.ts.map +1 -0
  73. package/dist/server/composition-html.js +45 -0
  74. package/dist/server/composition-html.js.map +1 -0
  75. package/dist/server/preview-html.d.ts.map +1 -1
  76. package/dist/server/preview-html.js +19 -14
  77. package/dist/server/preview-html.js.map +1 -1
  78. package/dist/server/server.d.ts.map +1 -1
  79. package/dist/server/server.js +94 -6
  80. package/dist/server/server.js.map +1 -1
  81. package/dist/version.d.ts +1 -1
  82. package/dist/version.js +1 -1
  83. package/docs/adr/0001-keep-composition-authoring-web-native.md +2 -0
  84. package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +3 -1
  85. package/docs/adr/0007-make-html-documents-the-composition-unit.md +31 -0
  86. package/docs/migration-0.4.md +279 -0
  87. package/docs/v0.1-design.md +76 -55
  88. package/examples/demo/README.md +7 -3
  89. package/examples/demo/compositions/demo.controller.js +120 -0
  90. package/examples/demo/compositions/demo.html +253 -0
  91. package/examples/demo/video.config.js +1 -1
  92. package/package.json +4 -2
  93. package/skills/frameyard/README.md +4 -0
  94. package/skills/frameyard/SKILL.md +38 -11
  95. package/skills/frameyard/references/audio-assets.md +25 -14
  96. package/skills/frameyard/references/bindings.md +73 -0
  97. package/skills/frameyard/references/common-mistakes.md +59 -0
  98. package/skills/frameyard/references/controllers.md +126 -0
  99. package/skills/frameyard/references/deterministic-authoring.md +143 -52
  100. package/skills/frameyard/references/diagnostics.md +90 -0
  101. package/skills/frameyard/references/failure-recovery.md +37 -14
  102. package/skills/frameyard/references/json-results.md +41 -7
  103. package/skills/frameyard/references/subtitles-karaoke.md +10 -18
  104. package/examples/demo/compositions/demo.js +0 -206
package/CHANGELOG.md CHANGED
@@ -8,6 +8,77 @@ 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.4.0] - 2026-10-30
12
+
13
+ ### Breaking
14
+
15
+ - **Composition authoring model:** compositions are HTML documents, not
16
+ JavaScript modules. `video.config.js` entries point to `.html` files, and the
17
+ public `defineComposition` authoring path is removed. The `mount → update →
18
+ dispose` lifecycle remains only as the optional controller contract.
19
+ - **Authoring namespace:** `fy-composition`, `fy-transcript`, `data-fy-*`,
20
+ `--fy-*`, and `text/fy-controller` replace the corresponding
21
+ `frameyard`/`data-frameyard-*` names. Existing embedded-media attributes move
22
+ from `data-frameyard-*` to `data-fy-*`.
23
+ - **Executable code:** only the optional controller, inline or external, may
24
+ execute. Other executable `<script>` elements and inline `on*` handlers are
25
+ rejected.
26
+ - **Validation and karaoke:** domain validation and subtitle/karaoke rendering
27
+ move to the optional controller; no built-in subtitle renderer is added.
28
+ - **Diagnostics:** warnings are structured diagnostic objects with `severity`,
29
+ `code`, `message`, and optional `details`, not plain strings. Error envelopes
30
+ gain optional `error.details` and top-level `diagnostics`. Unsupported visual
31
+ elements inside a composition (`iframe`, `object`, `embed`) and `javascript:`
32
+ URLs are rejected.
33
+
34
+ ### Added
35
+
36
+ - Declarative HTML composition contract: `<fy-composition>` metadata, a
37
+ `data-fy="props"` default-props block, `data-fy-text`, `data-fy-attr-*`,
38
+ `data-fy-class-*`, `data-fy-if`, and `data-fy-repeat` bindings.
39
+ - Frame-scheduled CSS animation with `data-fy-anim-start`,
40
+ `data-fy-anim-duration`, and `data-fy-anim-easing`, plus `--fy-frame`,
41
+ `--fy-time`, `--fy-progress`, `--fy-fps`, and `--fy-duration` frame-state
42
+ variables.
43
+ - Optional composition controller, inline or external, with `mount`, `update`,
44
+ `dispose`, and `validateProps`. Inline controllers are extracted and served
45
+ through a virtual module URL so relative imports and `import.meta.url` work.
46
+ - Node-side static composition validation before Chromium in `check`, `still`,
47
+ and `render`, and on load in `dev`, with source-located diagnostics.
48
+ - Binding, animation, script, event-handler, unsupported-element, and
49
+ asset-reference validation.
50
+ - `entry`, `controller`, and top-level `diagnostics` in `check`, `still`, and
51
+ `render` JSON results.
52
+ - Agent-facing minimal skeleton, binding table, controller examples, and
53
+ common-mistakes guidance in the skill and scaffold.
54
+ - Injected composition host exposing `window.__fy` with metadata, resolved
55
+ audio/transcript clips, `load`, `seek`, `settle`, and `dispose`.
56
+ - HTML `<audio>` and `<fy-transcript>` declarations for the audio timeline and
57
+ transcript requirements.
58
+ - Same-origin iframe preview and a direct render/check/still path that share
59
+ the host API.
60
+
61
+ ### Changed
62
+
63
+ - Preview loads each composition document in a same-origin iframe; `check`,
64
+ `still`, and `render` navigate directly to the composition URL. The separate
65
+ preview-page render path is removed.
66
+ - `check` fails fast on static errors without launching Chromium; warnings
67
+ still allow frame evaluation. Nested `data-fy-repeat` is rejected with a
68
+ clear diagnostic; nested data uses the controller.
69
+ - `init` scaffolds an HTML composition and no longer generates a JavaScript
70
+ composition module.
71
+ - The agent skill, README, design baseline, and project guides document the
72
+ HTML model. See [the 0.4 migration guide](./docs/migration-0.4.md).
73
+
74
+ ### Removed
75
+
76
+ - JavaScript composition modules, `defineComposition`, and the
77
+ `data-frameyard-*` authoring attributes.
78
+ - The preview-page composition loading path used by the renderer.
79
+
80
+ [0.4.0]: https://github.com/vladiantio/frameyard/compare/v0.3.0...v0.4.0
81
+
11
82
  ## [0.3.0] - 2026-10-30
12
83
 
13
84
  ### Added
package/GLOSSARY.md CHANGED
@@ -19,13 +19,37 @@ The fixed dimensions, frame rate, and duration in frames that define a compositi
19
19
  _Avoid_: Render options, props
20
20
 
21
21
  **Composition lifecycle**:
22
- The `mount → update → dispose` sequence that governs a composition's frame production.
22
+ The host-driven sequence that loads an HTML composition, seeks it to frames, and releases it.
23
23
  _Avoid_: Render loop, playback lifecycle
24
24
 
25
+ **Controller lifecycle**:
26
+ The `mount → update → dispose` sequence a composition controller uses for per-frame computation.
27
+ _Avoid_: Composition lifecycle
28
+
25
29
  **Composition revision**:
26
30
  A specific version of a composition's source and metadata.
27
31
  _Avoid_: Build, release
28
32
 
33
+ **HTML composition**:
34
+ A composition whose source is an HTML document.
35
+ _Avoid_: HTML template, page
36
+
37
+ **Composition controller**:
38
+ An optional JavaScript controller, either inline in the composition document or in an external module, that performs per-frame computation an HTML composition's declarative HTML and CSS cannot express.
39
+ _Avoid_: Composition module, plugin
40
+
41
+ **Inline controller**:
42
+ A composition controller whose source is embedded in the composition document.
43
+ _Avoid_: Embedded script
44
+
45
+ **External controller**:
46
+ A composition controller whose source is a separate JavaScript module referenced by the composition document.
47
+ _Avoid_: Controller file
48
+
49
+ **Declarative binding**:
50
+ An HTML attribute that maps a prop into text, an attribute, a class, or element presence.
51
+ _Avoid_: Template expression, interpolation
52
+
29
53
  **Props**:
30
54
  The immutable, JSON-serializable inputs supplied to a composition for one render.
31
55
  _Avoid_: Options, settings, mutable state
@@ -48,6 +72,10 @@ _Avoid_: Playback-driven animation
48
72
  The single source of animation time, derived from the current frame and the composition's frame rate.
49
73
  _Avoid_: Wall clock, playback timer
50
74
 
75
+ **Frame-state variable**:
76
+ A CSS custom property whose value is derived from the current composition frame.
77
+ _Avoid_: Animation variable, clock variable
78
+
51
79
  **Seek safety**:
52
80
  The property that any frame can be produced correctly without depending on previously produced frames.
53
81
  _Avoid_: History-dependent updates
@@ -88,14 +116,26 @@ _Avoid_: Soundtrack, audio track, final cut
88
116
 
89
117
  ### Rendering
90
118
 
119
+ **Composition host**:
120
+ The injected runtime that loads an HTML composition, applies its bindings, and exposes its frame clock to preview and render.
121
+ _Avoid_: Preview runtime, render runtime
122
+
91
123
  **Preview**:
92
124
  An interactive, frame-accurate inspection of a composition using the same composition behavior as a render.
93
125
  _Avoid_: Visual editor
94
126
 
95
127
  **Check**:
96
- A validation operation that checks configuration, props, assets, audio, and transcript requirements, then mounts a composition and evaluates every frame without capturing or encoding.
128
+ A validation operation that first runs static composition validation, then loads a composition and evaluates every frame without capturing or encoding.
97
129
  _Avoid_: Test render, dry run
98
130
 
131
+ **Static composition validation**:
132
+ The Node-side validation of a composition document's metadata, bindings, controller declaration, scripts, and asset references before browser frame production.
133
+ _Avoid_: Lint, preflight
134
+
135
+ **Composition diagnostic**:
136
+ A structured error or warning about a composition's source, metadata, bindings, controller, assets, or frame production, with a stable code and optional source location.
137
+ _Avoid_: Log message, error string
138
+
99
139
  **Render**:
100
140
  The production of one complete video from a composition and a fixed set of props.
101
141
  _Avoid_: Recording, export
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Frameyard
2
2
 
3
- Frameyard is a local-first framework for building short, parameterized videos with HTML, CSS, and JavaScript and rendering them deterministically.
3
+ Frameyard is a local-first framework for building short, parameterized videos with HTML, CSS, and an optional JavaScript controller, and rendering them deterministically.
4
4
 
5
- The v0.1 design is implemented: composition contracts, browser runtime and preview, `init`, `dev`, `check`, `still`, `render`, `tts`, `transcribe`, `doctor`, deterministic Chromium capture, embedded video and WebGL, FFmpeg output, provenance manifests, transcript validation, and the official agent skill.
5
+ The 0.4 design is implemented: HTML composition documents, declarative bindings, CSS animation scheduling, frame-state variables, inline and external controllers, the composition host, static validation with structured diagnostics, iframe preview, direct render/check/still sessions, audio and transcripts, deterministic Chromium capture, FFmpeg output, and the official agent skill.
6
6
 
7
7
  ## Requirements
8
8
 
@@ -28,7 +28,7 @@ npx frameyard dev
28
28
 
29
29
  `dev` prints a local preview URL. Select a composition, edit its JSON props, scrub frames, and play the composition with its audio timeline.
30
30
 
31
- The scaffold includes an `AGENTS.md` guide for coding agents working in the project, covering the determinism rules and the `check`, `still`, and `render` workflow. It also includes a `package.json` with `dev`, `check`, `render`, and `still` scripts pinned to the scaffolding CLI version, so `pnpm dev` or `npm run dev` works without a global install.
31
+ The scaffold includes an `AGENTS.md` guide for coding agents working in the project, covering the HTML authoring rules and the `check`, `still`, and `render` workflow. It also includes a `package.json` with `dev`, `check`, `render`, and `still` scripts pinned to the scaffolding CLI version, so `pnpm dev` or `npm run dev` works without a global install.
32
32
 
33
33
  `npx frameyard` resolves the latest published version. Pin it with `npx frameyard@<version>`, or install the package locally, when a project needs a fixed CLI version.
34
34
 
@@ -45,127 +45,176 @@ The 30-second reference composition lives in [`examples/demo`](./examples/demo).
45
45
 
46
46
  ## Project format
47
47
 
48
- A project is ordinary HTML, CSS, assets, and native JavaScript ES modules. `video.config.js` explicitly lists composition entry points; there is no directory scanning:
48
+ A project is ordinary HTML, CSS, assets, and optional native JavaScript controller modules. `video.config.js` explicitly lists composition entry points; there is no directory scanning:
49
49
 
50
50
  ```js
51
51
  export default {
52
52
  compositions: {
53
- hello: "./compositions/hello.js",
53
+ hello: "./compositions/hello.html",
54
54
  },
55
55
  };
56
56
  ```
57
57
 
58
- A composition default-exports a definition. `defineComposition` is optional but gives early metadata validation and editor types:
58
+ A composition is a full HTML document with one `<fy-composition>` root. Metadata, default props, markup, CSS, bindings, audio, transcripts, and an optional controller all live in the document:
59
59
 
60
- ```js
61
- import { defineComposition } from "frameyard";
62
- import { random } from "frameyard/runtime";
63
-
64
- let title;
65
-
66
- export default defineComposition({
67
- width: 1920,
68
- height: 1080,
69
- fps: 30,
70
- durationInFrames: 900,
71
- defaultProps: { title: "Hello" },
72
- validateProps(props) {
73
- return typeof props.title === "string" ? [] : ["title must be a string"];
74
- },
75
- audio: [{ src: "assets/hit.wav", startFrame: 15, volume: 0.8 }],
60
+ ```html
61
+ <!doctype html>
62
+ <html>
63
+ <head>
64
+ <meta charset="utf-8">
65
+ <script type="application/json" data-fy="props">
66
+ { "title": "Hello", "accent": "#b8f36b" }
67
+ </script>
68
+ <style>
69
+ @keyframes fade-up {
70
+ from { opacity: 0; transform: translateY(24px); }
71
+ to { opacity: 1; transform: translateY(0); }
72
+ }
73
+ #title { animation: fade-up 1s ease-out both; }
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <fy-composition width="1920" height="1080" fps="30" duration-in-frames="900">
78
+ <h1 id="title"
79
+ data-fy-text="title"
80
+ data-fy-anim-start="0"
81
+ data-fy-anim-duration="30"></h1>
82
+ <div id="progress" style="width: calc(var(--fy-progress) * 100%)"></div>
83
+ </fy-composition>
84
+ </body>
85
+ </html>
86
+ ```
76
87
 
77
- mount({ container }) {
78
- container.innerHTML = "<h1 id='title'></h1>";
79
- title = container.querySelector("#title");
80
- },
88
+ `width`, `height`, `fps`, and `duration-in-frames` are fixed. Props are JSON-serializable, deep-frozen, and merged shallowly over the `data-fy="props"` defaults. They cannot change metadata.
81
89
 
82
- update({ frame, props }) {
83
- title.textContent = props.title;
84
- title.style.transform = `translateX(${random("title:" + frame) * 40}px)`;
85
- },
90
+ ### Bindings
86
91
 
87
- dispose() {
88
- title = undefined;
89
- },
90
- });
92
+ - `data-fy-text="path"` sets text content.
93
+ - `data-fy-attr-src="path"` (and other `data-fy-attr-*` names) sets an attribute.
94
+ - `data-fy-class-active="path"` toggles a class.
95
+ - `data-fy-if="path"` removes the element when the value is false.
96
+ - `<template data-fy-repeat="items">` clones its content for each item, with `item` and `index` in scope and `--fy-index` on each clone.
97
+ - Paths are dot-separated (`user.name`, `items.0.label`). Missing paths warn and resolve to an empty value.
98
+ - Primitive top-level props are also exposed as `--fy-prop-<name>` CSS custom properties.
99
+
100
+ Bindings apply once when a composition loads, before the controller mounts. Controller-created DOM is the controller's responsibility.
101
+
102
+ ### Animation
103
+
104
+ CSS `@keyframes` and the Web Animations API are the animation language. The runtime pauses every animation and seeks it from the frame clock.
105
+
106
+ `data-fy-anim-start`, `data-fy-anim-duration`, and `data-fy-anim-easing` schedule one animation per element in composition frames. The CSS `animation` shorthand supplies keyframes and fill mode; the attributes override its delay and duration.
107
+
108
+ The runtime sets frame-state variables on `:root`:
109
+
110
+ | Variable | Meaning |
111
+ | --- | --- |
112
+ | `--fy-frame` | Current frame |
113
+ | `--fy-time` | Current time in seconds |
114
+ | `--fy-progress` | Progress from `0` to `1` |
115
+ | `--fy-fps` | Composition frame rate |
116
+ | `--fy-duration` | Duration in seconds |
117
+ | `--fy-duration-frames` | Duration in frames |
118
+
119
+ Use `--fy-progress` for progress bars and `--fy-frame` or `--fy-time` in `calc()` for simple computed motion. CSS transitions are not deterministic and are diagnosed.
120
+
121
+ ### Controller
122
+
123
+ An optional controller handles what HTML and CSS cannot express. It is declared exactly once, either inline or as an external module:
124
+
125
+ ```html
126
+ <script type="text/fy-controller">
127
+ export default {
128
+ mount({ root, props }) { /* ... */ },
129
+ update({ frame, props, root }) { /* ... */ },
130
+ dispose() { /* ... */ },
131
+ validateProps(props) { /* ... */ },
132
+ };
133
+ </script>
91
134
  ```
92
135
 
93
- The runtime supplies an import map for `frameyard` and `frameyard/runtime`, so project code needs no bundling step and no `node_modules` import.
136
+ ```html
137
+ <script type="text/fy-controller" src="./hello.controller.js"></script>
138
+ ```
139
+
140
+ - `mount()` may be asynchronous; `update()` and `validateProps()` are synchronous and seek-safe; `dispose()` releases resources.
141
+ - Inline controllers are the default for small, single-use controllers. Extract to an external module for Canvas 2D, WebGL, karaoke, reuse, tests, or a large controller.
142
+ - A validation-only controller may export just `validateProps(props)`.
143
+ - The controller is the only executable code in a composition. Other executable scripts and `on*` handlers are rejected.
144
+ - Inline source is served through a virtual module URL, so relative imports and `import.meta.url` behave like an external module. A literal `</script>` inside inline source must be escaped.
94
145
 
95
146
  ### Composition lifecycle
96
147
 
97
- - `mount({ container, props })` creates the visual structure once and may finish asynchronous work.
98
- - `update({ frame, props })` makes that structure represent the frame. It must be synchronous and seek-safe.
99
- - `dispose()` releases resources after the last frame.
148
+ The composition host loads the document, applies bindings, imports the controller, calls `mount()`, prepares media, and seeks to frame 0. `check`, `still`, and `render` then seek explicitly to requested frames and wait for embedded media and browser paint before evaluating or capturing. `dispose()` runs before the host releases the document.
100
149
 
101
- The only animation inputs are the integer frame and immutable props. Metadata (`width`, `height`, `fps`, `durationInFrames`) is fixed and validated before mounting. Props are JSON-serializable, deep-frozen, and merged shallowly over `defaultProps`.
150
+ The controller lifecycle is `mount → update → dispose`. `update({ frame, props })` must be synchronous and seek-safe. Use `random("stable:key")` from `frameyard/runtime` instead of `Math.random()`. Independent `requestAnimationFrame` loops, timers, wall-clock reads, and CSS transitions are not deterministic animation drivers.
102
151
 
103
- Use `random("stable:key")` instead of `Math.random()`. The runtime pauses CSS animations and the Web Animations API inside the composition container and seeks them to the current frame. Independent `requestAnimationFrame` loops, timers, transitions, and wall-clock reads are not deterministic animation drivers.
152
+ `check` runs static composition validation before launching Chromium. It validates metadata, the root and controller declarations, default props, bindings, animation attributes, scripts and handlers, unsupported visual elements, and asset references, then reports stable diagnostic codes with source locations.
104
153
 
105
154
  ## Embedded video and WebGL
106
155
 
107
- Embedded videos are frame-seeked by the runtime. Place a `<video>` element in `mount()` and describe its place on the composition timeline with data attributes:
156
+ Embedded videos are declared in the composition document and frame-seeked by the runtime:
108
157
 
109
158
  ```html
110
159
  <video
111
160
  src="assets/clip.webm"
112
- data-frameyard-start="30"
113
- data-frameyard-trim="0.5"
114
- data-frameyard-duration="4"
115
- data-frameyard-volume="0.8"
161
+ data-fy-start="30"
162
+ data-fy-trim="0.5"
163
+ data-fy-duration="4"
164
+ data-fy-volume="0.8"
116
165
  ></video>
117
166
  ```
118
167
 
119
168
  | Attribute | Meaning | Default |
120
169
  | --- | --- | --- |
121
- | `data-frameyard-start` | Composition frame where the source starts | `0` |
122
- | `data-frameyard-trim` | Seconds trimmed from the source start | `0` |
123
- | `data-frameyard-duration` | Optional seconds of source to use | rest of source |
124
- | `data-frameyard-volume` | Embedded audio volume from `0` to `1` | `1` |
170
+ | `data-fy-start` | Composition frame where the source starts | `0` |
171
+ | `data-fy-trim` | Seconds trimmed from the source start | `0` |
172
+ | `data-fy-duration` | Optional seconds of source to use | rest of source |
173
+ | `data-fy-volume` | Embedded audio volume from `0` to `1` | `1` |
125
174
 
126
- The runtime pauses every video and seeks it to the frame-derived source time. Do not call `play()`, `pause()`, or set `currentTime` in `update()`. Create video elements and set these attributes in `mount()`: a video added or changed during `update()` cannot be prepared without network access and is rejected. Videos must be in the composition container's light DOM; shadow roots are not scanned. Video sources are project-relative local 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.
175
+ 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 document'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.
127
176
 
128
- 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-frameyard-audio="off"` to use a video as a visual-only asset. The attributes above also control where its audio enters the mix.
177
+ 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.
129
178
 
130
- WebGL 1 and WebGL 2 canvases are captured through the normal frame path. Draw the complete frame from `update()` and do not depend on the previous frame's drawing buffer; clear or redraw the scene each time. Production sessions pin software WebGL (SwiftShader) so rendered pixels do not vary with the host GPU.
179
+ WebGL 1 and WebGL 2 canvases are captured through the normal frame path. Draw the complete frame from the controller's `update()` and do not depend on the previous frame's drawing buffer; clear or redraw the scene each time. Production sessions pin software WebGL (SwiftShader) so rendered pixels do not vary with the host GPU.
131
180
 
132
181
  ## Audio timeline
133
182
 
134
- Audio clips are declarative:
183
+ Audio clips are `<audio>` elements:
135
184
 
136
- ```js
137
- audio: [
138
- {
139
- src: "assets/voice.wav", // project-relative local file
140
- startFrame: 30, // composition timeline position
141
- trimStart: 0.25, // seconds trimmed from the source
142
- trimDuration: 4.2, // optional seconds to use
143
- volume: 0.8, // 0..1
144
- },
145
- ];
185
+ ```html
186
+ <audio src="assets/voice.wav"
187
+ data-fy-start="30"
188
+ data-fy-trim="0.25"
189
+ data-fy-duration="4.2"
190
+ data-fy-volume="0.8"></audio>
146
191
  ```
147
192
 
148
- Preview synchronizes the timeline in the browser. Production mixes the same clips offline with FFmpeg. Unmuted embedded videos contribute their audio tracks to this mix unless they opt out. `check` reports clip durations and warnings when a clip extends beyond the composition. Frameyard never time-stretches audio or changes composition duration to fit it.
193
+ - Placement is in composition frames.
194
+ - Trimming is in seconds from the source.
195
+ - Audio, video, and transcript `src` values resolve from the project root.
196
+ - Frameyard never time-stretches audio or changes composition duration to fit it.
197
+ - `check` reports clip durations and overrun diagnostics.
198
+
199
+ Preview synchronizes the timeline in the browser. Production mixes the same clips offline with FFmpeg. Unmuted embedded videos contribute their audio tracks to this mix unless they opt out.
149
200
 
150
201
  ## Transcripts
151
202
 
152
- A composition can declare canonical transcript assets that `check`, `still`, and `render` must validate:
203
+ A composition declares canonical transcript assets with `<fy-transcript>`:
153
204
 
154
- ```js
155
- transcripts: [
156
- { src: "assets/voice.transcript.json", words: true },
157
- ],
205
+ ```html
206
+ <fy-transcript src="assets/voice.transcript.json" words></fy-transcript>
158
207
  ```
159
208
 
160
- Transcript paths are project-relative. `words: true` requires actual measured word timings for karaoke-style presentation; Frameyard fails with `TRANSCRIPT_WORDS_UNAVAILABLE` rather than interpolating them. See [subtitles and karaoke](./skills/frameyard/references/subtitles-karaoke.md) for the canonical JSON shape and the seek-safe frame conversion recipe.
209
+ Transcript paths resolve from the project root. `words` requires actual measured word timings for karaoke-style presentation; Frameyard fails with `TRANSCRIPT_WORDS_UNAVAILABLE` rather than interpolating them. `check`, `still`, and `render` validate declared transcripts. Visual presentation is the composition's responsibility; a controller can read the transcript JSON and render timed text or karaoke. See [subtitles and karaoke](./skills/frameyard/references/subtitles-karaoke.md) for the canonical JSON shape and the seek-safe frame conversion recipe.
161
210
 
162
211
  ## Commands
163
212
 
164
213
  | Command | Purpose |
165
214
  | --- | --- |
166
- | `frameyard init [directory]` | Scaffold a minimal project, including an `AGENTS.md` agent guide and pinned `package.json` scripts |
215
+ | `frameyard init [directory]` | Scaffold a minimal HTML project, including an `AGENTS.md` agent guide and pinned `package.json` scripts |
167
216
  | `frameyard dev [--port <port>]` | Start the preview server |
168
- | `frameyard check <id> [--props <file>]` | Validate props, assets, audio, transcripts, and every frame without capture |
217
+ | `frameyard check <id> [--props <file>]` | Run static validation, then evaluate every frame without capture |
169
218
  | `frameyard still <id> --frame <n>` | Capture one PNG through the production path |
170
219
  | `frameyard render <id> --out <video.mp4>` | Capture frames with Chromium and encode with FFmpeg, optionally in parallel |
171
220
  | `frameyard tts ...` | Produce one frozen 24 kHz mono WAV with local qwentts.cpp |
@@ -183,27 +232,33 @@ Non-interactive commands support `--json`. Results go to stdout and human progre
183
232
  ```json
184
233
  {
185
234
  "ok": true,
186
- "command": "render",
235
+ "command": "check",
187
236
  "composition": "hello",
188
- "path": "/absolute/path/hello.mp4",
189
- "width": 1920,
190
- "height": 1080,
191
- "fps": 30,
192
- "durationInFrames": 900,
193
- "framesRendered": 900,
194
- "concurrency": 1,
195
- "audio": { "clips": 1, "warnings": [] },
196
- "transcripts": { "clips": 1, "warnings": [] }
237
+ "entry": "compositions/hello.html",
238
+ "controller": { "kind": "inline" },
239
+ "metadata": { "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 900 },
240
+ "framesChecked": 900,
241
+ "audio": { "clips": 1 },
242
+ "transcripts": { "clips": 0 },
243
+ "diagnostics": []
197
244
  }
198
245
  ```
199
246
 
200
- Errors use the same envelope:
247
+ Diagnostics carry `severity`, `code`, `message`, and optional `details` with source locations, element or attribute names, values, and suggestions. Errors use the same envelope and may include `error.details` and top-level `diagnostics`:
201
248
 
202
249
  ```json
203
250
  {
204
251
  "ok": false,
205
- "command": "render",
206
- "error": { "code": "OUTPUT_EXISTS", "message": "Output already exists: ..." }
252
+ "command": "check",
253
+ "error": { "code": "BINDING_INVALID", "message": "Unknown Frameyard attribute ..." },
254
+ "diagnostics": [
255
+ {
256
+ "severity": "error",
257
+ "code": "BINDING_INVALID",
258
+ "message": "Unknown Frameyard attribute \"data-fy-txt\".",
259
+ "details": { "path": "compositions/hello.html", "line": 12, "column": 5, "attribute": "data-fy-txt" }
260
+ }
261
+ ]
207
262
  }
208
263
  ```
209
264
 
@@ -254,18 +309,19 @@ npx frameyard doctor --talker-model models/talker.gguf --codec-model models/code
254
309
 
255
310
  ## Agent skill
256
311
 
257
- The optional host-neutral agent skill lives in [`skills/frameyard/`](./skills/frameyard). It uses the same project format and CLI as humans, documents deterministic authoring, audio assets, subtitle/karaoke recipes, JSON results, and failure recovery, and ships a thin CLI wrapper. The npm package includes the skill files matching its API version. Install it with `npx skills add vladiantio/frameyard`, or see [`skills/frameyard/README.md`](./skills/frameyard/README.md) for host-specific installation or referencing.
312
+ The optional host-neutral agent skill lives in [`skills/frameyard/`](./skills/frameyard). It uses the same project format and CLI as humans, documents HTML authoring, bindings, controllers, static diagnostics, audio assets, subtitle/karaoke recipes, JSON results, and failure recovery, and ships a thin CLI wrapper. The npm package includes the skill files matching its API version. Install it with `npx skills add vladiantio/frameyard`, or see [`skills/frameyard/README.md`](./skills/frameyard/README.md) for host-specific installation or referencing.
258
313
 
259
314
  ## Rendering and reproducibility
260
315
 
261
316
  Production rendering:
262
317
 
263
- 1. Starts the same preview server and browser runtime used by `dev`.
264
- 2. Launches one pinned Chromium session, or several independent sessions when `--concurrency` is greater than 1.
265
- 3. Seeks explicitly to each frame and waits for composition updates.
266
- 4. Captures the viewport through the Chrome DevTools Protocol.
267
- 5. Pipes PNG frames in timeline order and the offline audio mix to FFmpeg.
268
- 6. Encodes H.264 video with `yuv420p` and AAC audio into an MP4.
318
+ 1. Runs static composition validation and resolves metadata before launching Chromium.
319
+ 2. Navigates directly to the composition document with the composition host.
320
+ 3. Launches one pinned Chromium session, or several independent sessions when `--concurrency` is greater than 1.
321
+ 4. Seeks explicitly to each frame, calls the controller, seeks CSS/WAAPI animations, and waits for embedded media decode and browser paint.
322
+ 5. Captures the viewport through the Chrome DevTools Protocol.
323
+ 6. Pipes PNG frames in timeline order and the offline audio mix to FFmpeg.
324
+ 7. Encodes H.264 video with `yuv420p` and AAC audio into an MP4.
269
325
 
270
326
  Rendering is sequential by default. Pass `--concurrency <n>` (1-16) to seek and capture frames with up to `n` independent browser sessions; the effective value is capped by the composition's frame count. Captured frames are always encoded in timeline order, so parallel and sequential renders of the same inputs produce equivalent visual frames. Concurrency only changes scheduling: it does not change composition behavior or the determinism contract. Parallel rendering uses more CPU and memory, so choose a value appropriate for the machine.
271
327
 
@@ -277,31 +333,36 @@ For a repeatable local measurement of seek, capture, encode, and full-render tim
277
333
 
278
334
  Deferred from this implementation slice:
279
335
 
280
- - Built-in subtitle rendering, automatic caption burn-in, and karaoke rendering
336
+ - Built-in subtitle rendering, automatic caption burn-in, and declarative karaoke elements
337
+ - Nested `data-fy-repeat` scopes and a general template language
338
+ - Declarative JSON Schema validation
339
+ - Arbitrary executable scripts in composition HTML
281
340
  - `qwentts.cpp` reference-audio voice cloning and VoiceDesign
282
341
  - Automatic long-text chunking, streaming synthesis, and alternative generated-audio formats
283
342
  - Automatic upstream builds, model downloads, environment repair, and hosted rendering
284
- - Frame caches and general image-sequence output
343
+ - Frame caches, batch still capture, and general image-sequence output
285
344
  - React or other UI-framework adapters, plugin APIs, and agent-only runtimes
286
345
  - TypeScript transpilation for project compositions and npm dependency optimization
287
346
 
288
- See the [v0.1 design baseline](./docs/v0.1-design.md) for the full product contract and the [domain glossary](./GLOSSARY.md) for terminology.
347
+ 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, and the [domain glossary](./GLOSSARY.md) for terminology.
289
348
 
290
349
  ## Repository layout
291
350
 
292
- - `src/contracts.ts`: shared composition, props, audio, and transcript requirement contracts
351
+ - `src/contracts.ts`: shared composition, props, audio, transcript, and diagnostic contracts
352
+ - `src/bindings.ts`: browser-safe binding path and value semantics
353
+ - `src/html/`: Node-side parsing, static composition validation, and host injection
293
354
  - `src/transcript.ts`: canonical transcript validation and SRT/VTT formatting
294
- - `src/runtime/`: browser lifecycle, frame clock, deterministic randomness
295
- - `src/preview/`: interactive preview client
296
- - `src/server/`: native ES-module server and import map
297
- - `src/renderer/`: Chromium capture, audio mixing, transcript inspection, FFmpeg encoding
355
+ - `src/runtime/`: browser host, frame clock, deterministic randomness, bindings, media, diagnostics
356
+ - `src/preview/`: interactive iframe preview client
357
+ - `src/server/`: native ES-module server, host injection, and inline-controller route
358
+ - `src/renderer/`: Chromium capture, static validation integration, audio mixing, transcript inspection, FFmpeg encoding
298
359
  - `src/assets/`: local TTS/transcription wrappers, executable resolution, atomic publishing, provenance
299
360
  - `src/doctor.ts`: environment, integration, and manifest inspection
300
361
  - `src/scaffold.ts`: `init` scaffold templates, including the generated package manifest, project README, and agent guide
301
362
  - `src/cli.ts`: command-line entry point
302
363
  - `skills/`: official host-neutral agent skill
303
364
  - `examples/demo/`: the 30-second reference composition
304
- - `bench/fixtures/`: controlled compositions for the render benchmark
365
+ - `bench/fixtures/`: controlled HTML compositions for the render benchmark
305
366
  - `scripts/`: benchmark, release checks, versioning, and git-hook installation
306
367
  - `docs/adr/`: architecture decision records
307
368
 
@@ -0,0 +1,42 @@
1
+ import type { Props } from "./contracts.js";
2
+ export interface RepeatScope {
3
+ item: unknown;
4
+ index: number;
5
+ }
6
+ export interface BindingScope {
7
+ props: Readonly<Props>;
8
+ repeat?: RepeatScope;
9
+ }
10
+ export interface BindingPathValidation {
11
+ ok: boolean;
12
+ reason?: string;
13
+ }
14
+ export interface ResolvedBindingPath {
15
+ found: boolean;
16
+ value: unknown;
17
+ }
18
+ export interface CoercedBindingValue<T> {
19
+ value: T;
20
+ invalid?: boolean;
21
+ }
22
+ /** Validates a dot-separated binding path without resolving it. */
23
+ export declare function validateBindingPath(path: string): BindingPathValidation;
24
+ /**
25
+ * Resolves a dot-separated path against props, or against the repeat scope when
26
+ * the first segment is `item` or `index`. Only own properties are read.
27
+ */
28
+ export declare function resolveBindingPath(scope: BindingScope, path: string): ResolvedBindingPath;
29
+ /** JavaScript truthiness, used by `data-fy-if` and class bindings. */
30
+ export declare function isTruthy(value: unknown): boolean;
31
+ /** Coerces a value for `textContent`. Objects and arrays are invalid. */
32
+ export declare function coerceText(value: unknown): CoercedBindingValue<string>;
33
+ /**
34
+ * Coerces a value for an attribute. `true` writes an empty attribute, and
35
+ * `false`, `null`, and `undefined` remove it.
36
+ */
37
+ export declare function coerceAttribute(value: unknown): CoercedBindingValue<string | null>;
38
+ /** Coerces a value for a class binding. Objects and arrays are invalid. */
39
+ export declare function coerceClass(value: unknown): CoercedBindingValue<boolean>;
40
+ /** Suggests the closest top-level prop name for a mistyped binding path. */
41
+ export declare function suggestBindingPath(path: string, props: Readonly<Props>): string | undefined;
42
+ //# sourceMappingURL=bindings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,KAAK,EAAE,CAAC,CAAC;IACT,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAKD,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,qBAAqB,CAqBvE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,mBAAmB,CA6BzF;AAED,sEAAsE;AACtE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEhD;AAED,yEAAyE;AACzE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAWtE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAAC,MAAM,GAAG,IAAI,CAAC,CAWlF;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAWxE;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,MAAM,GAAG,SAAS,CAqB3F"}