@lalalic/markcut 1.0.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/.env.example +27 -0
- package/.github/user-steer.md +9 -0
- package/.vscode/settings.json +3 -0
- package/AGENTS.md +271 -0
- package/README.md +219 -0
- package/SKILL.md +209 -0
- package/docs/dynamic-components.md +191 -0
- package/docs/edit-mode.md +220 -0
- package/docs/json-descriptive.md +539 -0
- package/docs/label-mode.md +110 -0
- package/docs/markdown-descriptive.md +751 -0
- package/docs/templates.md +52 -0
- package/package.json +64 -0
- package/remotion.config.ts +5 -0
- package/scripts/artlist-dl.mjs +190 -0
- package/scripts/build-pipeline.sh +19 -0
- package/scripts/build-player.sh +20 -0
- package/src/Root.tsx +55 -0
- package/src/config.mjs +88 -0
- package/src/context/EventContext.tsx +168 -0
- package/src/context/index.tsx +42 -0
- package/src/descriptive/compiler.test.ts +1135 -0
- package/src/descriptive/compiler.ts +1230 -0
- package/src/descriptive/dsl.ts +455 -0
- package/src/descriptive/markdown.test.ts +866 -0
- package/src/descriptive/markdown.ts +674 -0
- package/src/descriptive/resolve.test.ts +951 -0
- package/src/descriptive/resolve.ts +891 -0
- package/src/entry.tsx +163 -0
- package/src/index.ts +4 -0
- package/src/player/browser.tsx +356 -0
- package/src/player/bundle/player.js +60259 -0
- package/src/player/bundler.mjs +269 -0
- package/src/player/label-server.mjs +599 -0
- package/src/player/pipeline.mjs +11123 -0
- package/src/player/pipeline.ts +117 -0
- package/src/player/server-shared.mjs +144 -0
- package/src/player/server.mjs +1006 -0
- package/src/render/cli-tools.ts +177 -0
- package/src/render/cli.mjs +628 -0
- package/src/schema/index.ts +259 -0
- package/src/types/Audio.tsx +56 -0
- package/src/types/Component.tsx +135 -0
- package/src/types/Effect.tsx +88 -0
- package/src/types/Folder.tsx +180 -0
- package/src/types/FrameSyncStyle.tsx +51 -0
- package/src/types/Image.tsx +51 -0
- package/src/types/Include.tsx +394 -0
- package/src/types/Map.tsx +252 -0
- package/src/types/Rhythm.tsx +58 -0
- package/src/types/Scene.tsx +42 -0
- package/src/types/Subtitle.tsx +218 -0
- package/src/types/Video.tsx +70 -0
- package/src/types/keyframes.ts +454 -0
- package/src/utils/__tests__/vtt.test.ts +129 -0
- package/src/utils/index.ts +168 -0
- package/src/utils/tween.ts +118 -0
- package/src/vision/cli.mjs +1187 -0
- package/src/vision/vision_prompts.md +67 -0
- package/tests/dsl.test.ts +317 -0
- package/tests/fixtures/audio.json +25 -0
- package/tests/fixtures/basic.json +27 -0
- package/tests/fixtures/component-all.json +38 -0
- package/tests/fixtures/components.json +38 -0
- package/tests/fixtures/effects.json +64 -0
- package/tests/fixtures/full.json +51 -0
- package/tests/fixtures/map.json +23 -0
- package/tests/fixtures/md/all-nodes.md +28 -0
- package/tests/fixtures/md/basic.md +6 -0
- package/tests/fixtures/md/component-imports.md +20 -0
- package/tests/fixtures/md/edge-cases.md +33 -0
- package/tests/fixtures/md/effects.md +17 -0
- package/tests/fixtures/md/frontmatter.md +20 -0
- package/tests/fixtures/md/full-feature.md +58 -0
- package/tests/fixtures/md/imports-block.md +19 -0
- package/tests/fixtures/md/include-main.md +11 -0
- package/tests/fixtures/md/include-sub.md +25 -0
- package/tests/fixtures/md/jsx-code-fence.md +21 -0
- package/tests/fixtures/md/map.md +11 -0
- package/tests/fixtures/md/nested-scenes.md +25 -0
- package/tests/fixtures/md/rhythm.md +17 -0
- package/tests/fixtures/md/scenes.md +16 -0
- package/tests/fixtures/md/tween.md +11 -0
- package/tests/fixtures/md/vars-test.md +6 -0
- package/tests/fixtures/scenes.json +40 -0
- package/tests/fixtures/subtitle.json +59 -0
- package/tests/fixtures/subvideo.json +59 -0
- package/tests/fixtures/templates/courseware.md +351 -0
- package/tests/fixtures/tween-visual.json +28 -0
- package/tests/fixtures/video-series.json +54 -0
- package/tests/md-descriptive.test.ts +742 -0
- package/tests/render.test.ts +985 -0
- package/tests/schema.test.ts +68 -0
- package/tests/server.test.ts +308 -0
- package/tests/utils.ts +391 -0
- package/tests/vitest.config.ts +18 -0
- package/tests/vitest.integration.config.ts +16 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
# JSON Descriptive (Canonical IR)
|
|
2
|
+
|
|
3
|
+
Complete reference for LLM-driven video generation. Emit valid JSON only.
|
|
4
|
+
|
|
5
|
+
## Output Contract
|
|
6
|
+
|
|
7
|
+
A single root object describing a renderable video:
|
|
8
|
+
|
|
9
|
+
- Required at root: `width`, `height`, `fps`, `layout`, `children`
|
|
10
|
+
- `children` is an array of nodes (see Type Catalog)
|
|
11
|
+
- Every `scene` / container must have non-empty `children`
|
|
12
|
+
- Timing is resolved bottom-up; containers derive duration from children
|
|
13
|
+
|
|
14
|
+
## Type Catalog
|
|
15
|
+
|
|
16
|
+
### `scene` — storyboard organizer (preferred top-level node)
|
|
17
|
+
|
|
18
|
+
When to use: always. Scenes are the default organizational unit. They render as folders but carry narrative metadata.
|
|
19
|
+
|
|
20
|
+
| Field | Required | Type | Notes |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `type` | yes | `"scene"` | |
|
|
23
|
+
| `name` | yes | string | unique scene identifier |
|
|
24
|
+
| `layout` | yes | `"series"\|"parallel"\|"transitionSeries"` | |
|
|
25
|
+
| `children` | yes | node[] | must be non-empty |
|
|
26
|
+
| `transition` | opt | `"fade"\|"slide"\|"wipe"\|"flip"\|"clockWipe"` | only for transitionSeries |
|
|
27
|
+
| `transitionTime` | opt | number | seconds (default 0.5) |
|
|
28
|
+
| `instruction`,`script`,`style` | opt | | metadata |
|
|
29
|
+
| `on` | opt | `{ when, state }` | Event spec that fires at a specific frame |
|
|
30
|
+
|
|
31
|
+
> **`scene` is a container.** It supports nesting: a scene can hold other scenes, containers, or leaves. Use nested scenes for chapters, acts, or grouped beats.
|
|
32
|
+
|
|
33
|
+
Example of nested scenes:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"type": "scene",
|
|
38
|
+
"name": "Chapter2",
|
|
39
|
+
"layout": "series",
|
|
40
|
+
"children": [
|
|
41
|
+
{ "type": "scene", "name": "Feature1", "layout": "parallel", "children": [ ... ] },
|
|
42
|
+
{ "type": "scene", "name": "Feature2", "layout": "parallel", "children": [ ... ] }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `video`
|
|
48
|
+
|
|
49
|
+
When to use: any moving footage (.mp4, .mov, etc.).
|
|
50
|
+
|
|
51
|
+
| Field | Required | Type | Notes |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| `type` | yes | `"video"` | |
|
|
54
|
+
| `src` | yes | string | path/URL |
|
|
55
|
+
| `duration` | cond | number | required if no `endAt` |
|
|
56
|
+
| `startFrom` | opt | number | trim from source start (sec) |
|
|
57
|
+
| `endAt` | opt | number | trim at source position; effective length = `endAt − startFrom` |
|
|
58
|
+
| `start` | opt | number | parallel only |
|
|
59
|
+
| `volume` | opt | number 0–1 | default 1 |
|
|
60
|
+
| `playbackRate` | opt | number | |
|
|
61
|
+
| `width`,`height` | opt | number | default 1080×1920 |
|
|
62
|
+
| `instruction`,`script`,`style` | opt | | metadata |
|
|
63
|
+
| `on` | opt | `{ when, state }` | Event spec that fires at a specific frame |
|
|
64
|
+
| `effects` | opt | `string[]` or `object[]` | **New** — apply animations directly: `["fadeIn"]` or `[{animation:"bounceIn",animationTimingFunction:"ease-out"}]` |
|
|
65
|
+
|
|
66
|
+
### `audio`
|
|
67
|
+
|
|
68
|
+
When to use: voiceover, BGM, SFX.
|
|
69
|
+
|
|
70
|
+
| Field | Required | Type | Notes |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| `type` | yes | `"audio"` | |
|
|
73
|
+
| `src` | yes | string | |
|
|
74
|
+
| `duration` | cond | number | required if no `endAt` |
|
|
75
|
+
| `startFrom`,`endAt` | opt | number | trim |
|
|
76
|
+
| `volume` | opt | number 0–1 | |
|
|
77
|
+
| `loop` | opt | int | loop count >1 |
|
|
78
|
+
| `foreground` | opt | bool | ducks parent video audio |
|
|
79
|
+
| `start` | opt | number | parallel only |
|
|
80
|
+
| `on` | opt | `{ when, state }` | Event spec that fires at a specific frame |
|
|
81
|
+
| `effects` | opt | `string[]` or `object[]` | **New** — apply animations directly |
|
|
82
|
+
|
|
83
|
+
### `image`
|
|
84
|
+
|
|
85
|
+
When to use: photos, stills, title cards.
|
|
86
|
+
|
|
87
|
+
| Field | Required | Type | Notes |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
| `type` | yes | `"image"` | |
|
|
90
|
+
| `src` | yes | string | |
|
|
91
|
+
| `duration` | yes | number | no intrinsic duration |
|
|
92
|
+
| `fit` | opt | `"contain"\|"cover"\|"fill"` | default `contain` |
|
|
93
|
+
| `start` | opt | number | parallel only |
|
|
94
|
+
| `on` | opt | `{ when, state }` | Event spec that fires at a specific frame |
|
|
95
|
+
|
|
96
|
+
### `subtitle` (root-level overlay)
|
|
97
|
+
|
|
98
|
+
When to use: captions, on-screen text, karaoke. Set as `root.subtitle` — not a tree node.
|
|
99
|
+
|
|
100
|
+
| Field | Required | Type | Notes |
|
|
101
|
+
|---|---|---|---|
|
|
102
|
+
| `src` | yes | string | VTT file path/URL, inline VTT body, or plain text |
|
|
103
|
+
| `type` | opt | string | caption animation component: `Bounce`, `Fade`, `Typewriter`, `Colorful`, `Glowing`, `Neon`, etc. Default: plain `Caption` |
|
|
104
|
+
| `style` | opt | string | inline CSS for the overlay container |
|
|
105
|
+
| `fontSize` | opt | number\|string | default 56 |
|
|
106
|
+
| `fontFamily` | opt | string | font family for subtitle text |
|
|
107
|
+
| `fontStyle` | opt | string | `normal`, `italic`, `bold`, `bold italic`, etc. |
|
|
108
|
+
|
|
109
|
+
> **HTML in cue text**: VTT cue text supports HTML tags with inline CSS for per-word styling:
|
|
110
|
+
> ```vtt
|
|
111
|
+
> 00:00:01.000 --> 00:00:03.000
|
|
112
|
+
> The <span style="color:#ff6b6b;font-weight:bold">quick</span> brown <span style="font-style:italic">fox</span>
|
|
113
|
+
> ```
|
|
114
|
+
> Tags like `<span>`, `<b>`, `<i>`, `<br>`, and inline `style` attributes all work. The `Typewriter` caption type respects HTML tag boundaries during character reveal.
|
|
115
|
+
|
|
116
|
+
If `src` is plain text (no `-->`), renders as a single static caption for the entire video duration. The `type` field selects an animated caption component from `remotion-subtitle` — omit for a plain static caption.
|
|
117
|
+
|
|
118
|
+
### `component`
|
|
119
|
+
|
|
120
|
+
When to use: JSX expression rendered at runtime with frontmatter imports in scope.
|
|
121
|
+
|
|
122
|
+
| Field | Required | Type | Notes |
|
|
123
|
+
|---|---|---|---|
|
|
124
|
+
| `type` | yes | `"component"` | |
|
|
125
|
+
| `jsx` | yes | string | JSX usage expression, e.g. `"<BarChart data={...} />"` |
|
|
126
|
+
| `duration` | yes | number |
|
|
127
|
+
| `id` | opt | string | Required for event targeting — registers component in event context |
|
|
128
|
+
| `on` | opt | `{ when, state }` | Event spec that fires at a specific frame |
|
|
129
|
+
| `effects` | opt | `string[]` or `object[]` | **New** — apply animations directly |
|
|
130
|
+
|
|
131
|
+
### `rhythm`
|
|
132
|
+
|
|
133
|
+
When to use: beat-synced audio (music drops, music-reactive reveals). Duration is derived from `spots` — no explicit `duration` needed.
|
|
134
|
+
|
|
135
|
+
| Field | Required | Type | Notes |
|
|
136
|
+
|---|---|---|---|
|
|
137
|
+
| `type` | yes | `"rhythm"` | |
|
|
138
|
+
| `src` | yes | string | audio file |
|
|
139
|
+
| `spots` | yes | number[] | beat timestamps (sec); must have ≥2 entries |
|
|
140
|
+
| `children` | yes | node[] | one child per beat slot; child[i] starts at spots[i], ends at spots[i+1] |
|
|
141
|
+
| `volume` | opt | number 0–1 | default 1 |
|
|
142
|
+
|
|
143
|
+
Duration = `spots[last] + average_gap`. Equivalent to how long the last child plays.
|
|
144
|
+
|
|
145
|
+
### `effects` (on any node)
|
|
146
|
+
|
|
147
|
+
Apply CSS keyframe animations directly via the `effects` field — no wrapper node needed.
|
|
148
|
+
|
|
149
|
+
Each entry is an animation name (string) with optional comma-separated positional parameters inside parentheses:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"type": "image",
|
|
154
|
+
"src": "hero.jpg",
|
|
155
|
+
"duration": 3,
|
|
156
|
+
"effects": ["fadeIn", "bounceIn(1.5, ease-out, 2)"]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Parameter order: `(duration, timingFunction, iterationCount)` — all optional.
|
|
161
|
+
|
|
162
|
+
Built-in animation names:
|
|
163
|
+
|
|
164
|
+
- **Fades**: `fadeIn`, `fadeOut`, `fadeInDown`, `fadeInUp`, `fadeInLeft`, `fadeInRight`, `fadeOutDown`, `fadeOutUp`, `fadeOutLeft`, `fadeOutRight`
|
|
165
|
+
- **Slides in**: `slideInDown`, `slideInUp`, `slideInLeft`, `slideInRight`
|
|
166
|
+
- **Slides out**: `slideOutDown`, `slideOutUp`, `slideOutLeft`, `slideOutRight`
|
|
167
|
+
- **Zooms**: `zoomIn`, `zoomOut`, `zoomInDown`, `zoomInUp`, `zoomInLeft`, `zoomInRight`
|
|
168
|
+
- **Bounces**: `bounce`, `bounceIn`
|
|
169
|
+
- **Rotations**: `rotateIn`, `rotateOut`, `rotateInDownLeft`, `rotateInDownRight`, `rotateInUpLeft`, `rotateInUpRight`
|
|
170
|
+
- **Flips**: `flipInX`, `flipInY`
|
|
171
|
+
- **Attention**: `pulse`, `flash`, `heartBeat`, `rubberBand`, `shakeX`, `shakeY`, `swing`, `tada`, `wobble`, `jello`
|
|
172
|
+
- **Specials**: `rollIn`, `rollOut`, `jackInTheBox`, `lightSpeedIn`, `lightSpeedOut`
|
|
173
|
+
|
|
174
|
+
See [Markdown Descriptive](markdown-descriptive.md#effects-on-any-node) for the full syntax reference.
|
|
175
|
+
|
|
176
|
+
### `map`
|
|
177
|
+
|
|
178
|
+
When to use: animated route visualization (Google Maps).
|
|
179
|
+
|
|
180
|
+
| Field | Required | Type | Notes |
|
|
181
|
+
|---|---|---|---|
|
|
182
|
+
| `type` | yes | `"map"` | |
|
|
183
|
+
| `waypoints` | yes | `{lat,lng,label?,media?}[]` | min 2 for routing |
|
|
184
|
+
| `duration` | yes | number | |
|
|
185
|
+
| `travelMode` | opt | `"DRIVING"\|"WALKING"\|"BICYCLING"\|"TRANSIT"` | |
|
|
186
|
+
| `mapType` | opt | `"roadmap"\|"satellite"\|"hybrid"\|"terrain"` | |
|
|
187
|
+
| `routeMarker`,`routeColor`,`routeWeight`,`zoom`,`center` | opt | | styling |
|
|
188
|
+
|
|
189
|
+
### `include`
|
|
190
|
+
|
|
191
|
+
When to use: embed an external markdown file as a sub-video.
|
|
192
|
+
|
|
193
|
+
In the **descriptive form**, `src` points to a `.md` file. The pipeline
|
|
194
|
+
resolves it recursively: parses the markdown, runs TTS/media/includes,
|
|
195
|
+
compiles to a stream tree JSON (cached at `.markcut/generated/includes/`),
|
|
196
|
+
and bundles any component imports independently.
|
|
197
|
+
|
|
198
|
+
| Field | Required | Type | Notes |
|
|
199
|
+
|---|---|---|---|
|
|
200
|
+
| `type` | yes | `"include"` | |
|
|
201
|
+
| `src` | cond | string | path to `.md` file (descriptive) or compiled `.json` |
|
|
202
|
+
| `children` | opt | node[] | inline fallback if no `src` |
|
|
203
|
+
| `duration` | cond | number | optional — auto-detected from compiled sub-video |
|
|
204
|
+
| `volume` | opt | number | |
|
|
205
|
+
|
|
206
|
+
> **Component isolation:** The sub-video's `imports` are bundled to
|
|
207
|
+
> `.markcut/<sub-basename>/components/<hash>.js` and loaded at render
|
|
208
|
+
> time in a nested `ComposeContext`, so sub-video components don't
|
|
209
|
+
> conflict with parent components.
|
|
210
|
+
|
|
211
|
+
## Imports
|
|
212
|
+
|
|
213
|
+
External React components are registered via `root.imports`. In the **descriptive** form (input to the pipeline), it's an array of import entries. In the **compiled** form (output of the server pipeline, consumed by the player), it's a string URL pointing to a pre-bundled ESM module.
|
|
214
|
+
|
|
215
|
+
### Descriptive form (input)
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"imports": [
|
|
220
|
+
{ "name": "BarChart", "from": "@nivo/bar" },
|
|
221
|
+
{ "name": "Logo", "from": "git:myorg/assets/src/Logo.tsx" },
|
|
222
|
+
{ "name": "Greeting", "jsx": "export default ({name}) => <h1 style={{color:'#fff'}}>Hello {name}</h1>" }
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Import entry fields
|
|
228
|
+
|
|
229
|
+
| Field | Required | Type | Notes |
|
|
230
|
+
|---|---|---|---|
|
|
231
|
+
| `name` | yes | string | component name — used as JSX tag in usage expressions |
|
|
232
|
+
| `from` | cond | string | source spec (see below); alternative to `jsx` |
|
|
233
|
+
| `jsx` | cond | string | inline component definition source (e.g. `"export default ({text}) => <span>{text}</span>"`) |
|
|
234
|
+
| `exports` | opt | string | named export to pick from the module (default: `"default"`) |
|
|
235
|
+
|
|
236
|
+
### `from:` spec forms (descriptive form)
|
|
237
|
+
|
|
238
|
+
| Pattern | Resolved by bundler as |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `pkg` or `npm:pkg` | npm package — `npm install pkg`, then `esbuild` re-exports it |
|
|
241
|
+
| `pkg@1.2.3` or `npm:pkg@1.2.3` | npm package with pinned version |
|
|
242
|
+
| `@scope/pkg` or `npm:@scope/pkg` | npm scoped package |
|
|
243
|
+
| `git:user/repo/path` | Raw specifier passed to esbuild; requires the module to be resolvable |
|
|
244
|
+
| `github:user/repo/path` | Same as `git:` |
|
|
245
|
+
| `https://...`, `http://...` | Raw URL passed directly to esbuild as an external |
|
|
246
|
+
| local path | Filesystem path relative to the bundle project |
|
|
247
|
+
|
|
248
|
+
### Imports block (markdown only)
|
|
249
|
+
|
|
250
|
+
The `` ```js imports `` or `~~~js imports` code block is a markdown-only feature. See [Markdown Descriptive](markdown-descriptive.md) for the full reference.
|
|
251
|
+
|
|
252
|
+
### Using imports in component nodes
|
|
253
|
+
|
|
254
|
+
Components reference imports by name as JSX tags in the usage expression. The compiler passes the resolved `imports` map through to the stream node for runtime resolution:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"type": "component",
|
|
259
|
+
"jsx": "<BarChart data={[{name:'A',value:80},{name:'B',value:60},{name:'C',value:40}]} keys={['value']} indexBy='name' />",
|
|
260
|
+
"duration": 4
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Inline `jsx:` definitions in the imports array are also available as JSX tags in usage expressions:
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
{
|
|
268
|
+
"imports": [
|
|
269
|
+
{ "name": "Greeting", "jsx": "export default ({name}) => <h1 style={{color:'#fff'}}>Hello {name}</h1>" }
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
// ...
|
|
273
|
+
{ "type": "component", "jsx": "<Greeting name='World' />", "duration": 2 }
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
> Components defined via `jsx:` inline definitions are bundled by the player server at startup and loaded as a single ESM module. They have access to `useCurrentFrame()`, `interpolate()`, and other Remotion hooks via the bundled module's imports.
|
|
277
|
+
|
|
278
|
+
### Imports in markdown
|
|
279
|
+
|
|
280
|
+
The markdown descriptive format supports the same `imports:` array in YAML frontmatter, plus ` ```jsx Name ` code fence blocks for inline definitions. See [Markdown Strict Descriptive](markdown-strict-descriptive.md#component) for details.
|
|
281
|
+
|
|
282
|
+
## Events
|
|
283
|
+
|
|
284
|
+
Fire a JavaScript expression at a specific frame to mutate a registered component's state. Useful for syncing UI state with narration beats.
|
|
285
|
+
|
|
286
|
+
### Registering a component
|
|
287
|
+
|
|
288
|
+
Add an `id` to a component node to register it in the global event context:
|
|
289
|
+
|
|
290
|
+
```json
|
|
291
|
+
{
|
|
292
|
+
"type": "component",
|
|
293
|
+
"jsx": "<Slide current={current}>{source}</Slide>",
|
|
294
|
+
"duration": 4,
|
|
295
|
+
"id": "slide1"
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The `id` becomes the variable name available in event expressions.
|
|
300
|
+
|
|
301
|
+
### Event spec
|
|
302
|
+
|
|
303
|
+
Any node can carry an `on` field with the following shape:
|
|
304
|
+
|
|
305
|
+
| Field | Required | Type | Notes |
|
|
306
|
+
|---|---|---|---|
|
|
307
|
+
| `when` | yes | string | Frame selector: `"start"`, `"end"`, `"50%"`, `"2.5s"`, etc. |
|
|
308
|
+
| `state` | yes | string | JavaScript expression evaluated with registered component proxies in scope |
|
|
309
|
+
|
|
310
|
+
### Examples
|
|
311
|
+
|
|
312
|
+
```json
|
|
313
|
+
{
|
|
314
|
+
"type": "scene",
|
|
315
|
+
"children": [
|
|
316
|
+
{
|
|
317
|
+
"type": "audio",
|
|
318
|
+
"script": "Narration 1",
|
|
319
|
+
"duration": 3,
|
|
320
|
+
"on": { "when": "start", "state": "slide1.current=0" }
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"type": "audio",
|
|
324
|
+
"script": "Narration 2",
|
|
325
|
+
"duration": 2,
|
|
326
|
+
"on": { "when": "start", "state": "slide1.current++" }
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The expression is evaluated with all registered component IDs as scope variables. Each component variable is a Proxy whose property setter triggers a React re-render on the corresponding component node.
|
|
333
|
+
|
|
334
|
+
## Common Metadata Fields
|
|
335
|
+
|
|
336
|
+
Available on every node:
|
|
337
|
+
|
|
338
|
+
| Field | Type | Purpose |
|
|
339
|
+
|---|---|---|
|
|
340
|
+
| `id` | string | unique within parent scope; required for referencing |
|
|
341
|
+
| `instruction` | string | visual intent or style guide; not rendered |
|
|
342
|
+
| `script` | string | narration/dialogue text — TTS source; only meaningful on `scene` nodes |
|
|
343
|
+
| `style` | string | inline CSS (semicolon-separated); applied to the node's container div, e.g. `"border-radius:16px; opacity:0.9"` |
|
|
344
|
+
| `visible` | bool | default `true`; set `false` to hide without removing |
|
|
345
|
+
| `isBackground` | bool | when `true`, node loops to fill parent duration and does **not** contribute to container duration calculation — use for BGM, looping background imagery |
|
|
346
|
+
| `on` | object `{ when, state }` | Event spec that fires a JS expression at a specific frame, mutating registered component state — see [Events](#events-1) section |
|
|
347
|
+
|
|
348
|
+
> **`style` tip:** applies to the wrapping container, not the inner media element. Use for positioning, sizing, and opacity overrides.
|
|
349
|
+
> **`isBackground` tip:** use for audio tracks, looping video overlays, or any element that should play across the full parent duration.
|
|
350
|
+
|
|
351
|
+
## Timing Rules
|
|
352
|
+
|
|
353
|
+
1. `duration` is authoritative for non-trimmed leaves.
|
|
354
|
+
2. For `video`/`audio`: if `startFrom`+`endAt` present, clip length = `endAt − startFrom`.
|
|
355
|
+
3. `start` allowed only inside `parallel`.
|
|
356
|
+
4. `transitionTime` subtracted between items in `transitionSeries`.
|
|
357
|
+
5. `isBackground` children do not contribute to parent duration.
|
|
358
|
+
|
|
359
|
+
## Style
|
|
360
|
+
|
|
361
|
+
Each node accepts a `style` string for inline CSS on its container div. Available on every node:
|
|
362
|
+
|
|
363
|
+
| Field | Type | Purpose |
|
|
364
|
+
|
|
365
|
+
TTS, STT, TTI, and TTV are each configured via a **single CLI template string**. The LLM embeds every tool-specific parameter (voice, model, rate, size, style, etc.) directly in the string — only the built-in variables below are substituted by the engine.
|
|
366
|
+
|
|
367
|
+
**Root level** — sets defaults for all scenes:
|
|
368
|
+
```json
|
|
369
|
+
{
|
|
370
|
+
"tts": "edge-tts --voice \"en-US-GuyNeural\" --text \"{input}\" --write-media \"{output}\"",
|
|
371
|
+
"stt": "whisper \"{input}\" --output_format vtt --output_dir \"{outputDir}\""
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Per scene** — narration via audio child with `script`:
|
|
376
|
+
```json
|
|
377
|
+
{
|
|
378
|
+
"type": "scene",
|
|
379
|
+
"children": [
|
|
380
|
+
{ "type": "audio", "script": "Hello world", "volume": 1 },
|
|
381
|
+
...
|
|
382
|
+
]
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Precedence:** scene-level `tts` → root-level `tts` → pipeline option → hardcoded default.
|
|
387
|
+
|
|
388
|
+
### Built-in Variables
|
|
389
|
+
|
|
390
|
+
| Variable | Applies To | Description |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| `{input}` | TTS, TTI, TTV | Input content: narration text (TTS), generation prompt (TTI, TTV), audio file path (STT) |
|
|
393
|
+
| `{output}` | All | Output location: file path for TTS/TTI/TTV, directory for STT VTT files |
|
|
394
|
+
|
|
395
|
+
### Defaults & Prerequisites
|
|
396
|
+
|
|
397
|
+
| Pipeline | Field | Default CLI | Prerequisite |
|
|
398
|
+
|---|---|---|---|
|
|
399
|
+
| **Text-to-Speech** | `tts` | `edge-tts --voice "en-US-GuyNeural" --text "{input}" --write-media "{output}"` | `edge-tts` (`pip install edge-tts`) |
|
|
400
|
+
| **Speech-to-Text** | `stt` | `whisper "{input}" --output_format vtt --output_dir "{output}"` | `openai-whisper` (`pip install openai-whisper`) |
|
|
401
|
+
| **Text-to-Image** | `tti` | `pi --model agnes-2.0-flash --print "generate image: {input}" --output "{output}"` | `pi` CLI (`pip install pi-sdk`) |
|
|
402
|
+
| **Text-to-Video** | `ttv` | `pi --model agnes-2.0-flash --print "generate video: {input}" --output "{output}"` | `pi` CLI (`pip install pi-sdk`) |
|
|
403
|
+
|
|
404
|
+
### Notes
|
|
405
|
+
|
|
406
|
+
- Only the 2 variables listed above (`{input}` and `{output}`) are substituted. All other parameters (voice, model, rate, size, style, language, etc.) must be written verbatim into the CLI string.
|
|
407
|
+
- To use a different TTS engine (e.g. mlx-audio, piper), simply pass its full command as `tts`:
|
|
408
|
+
```json
|
|
409
|
+
{ "tts": "mlx-audio tts --model speecht5 --text "{input}" --ref-audio ./voice.wav --output "{output}"" }
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Tween Animation
|
|
413
|
+
|
|
414
|
+
Animate numeric props over time using `tween(from?, to, easing?)` expressions in component props. Tweens resolve at render time using Remotion's `interpolate()`, producing smooth frame-by-frame animation.
|
|
415
|
+
|
|
416
|
+
### Syntax
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
tween(to) — 0 → to, linear
|
|
420
|
+
tween(from, to) — from → to, linear
|
|
421
|
+
tween(from, to, easeOut) — from → to, with easing
|
|
422
|
+
tween(from, to, spring) — from → to, spring animation
|
|
423
|
+
tween(from, to, spring(damping:12)) — spring with custom params
|
|
424
|
+
tween(#000, #FFF) — color hex → hex
|
|
425
|
+
tween(#000, #FFF, easeInOut) — color hex with easing
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Usage in JSX expressions
|
|
429
|
+
|
|
430
|
+
Place `tween(...)` expressions inside JSX usage expressions. The engine compiles the JSX at runtime and resolves each `tween()` call to an animated number at each frame:
|
|
431
|
+
|
|
432
|
+
```json
|
|
433
|
+
{
|
|
434
|
+
"type": "component",
|
|
435
|
+
"jsx": "<BarChart data={[{name:'A',value:tween(0,80)},{name:'B',value:tween(0,60)},{name:'C',value:tween(0,40)}]} keys={['value']} indexBy='name' />",
|
|
436
|
+
"duration": 4
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
This animates the bars from 0 to their target heights over 4 seconds. At frame 0 all values are 0; they interpolate linearly to 80, 60, 40 by the end.
|
|
441
|
+
|
|
442
|
+
You can also use `tween()` in inline SVG expressions:
|
|
443
|
+
|
|
444
|
+
```json
|
|
445
|
+
{
|
|
446
|
+
"type": "component",
|
|
447
|
+
"jsx": "<svg viewBox='0 0 400 300'><rect y={260 - tween(0, 200)} width={80} height={tween(0, 200)} fill='#E38627' /></svg>",
|
|
448
|
+
"duration": 4
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Supported easings
|
|
453
|
+
|
|
454
|
+
| Name | Remotion mapping |
|
|
455
|
+
|---|---|
|
|
456
|
+
| `linear` (default) | identity |
|
|
457
|
+
| `ease`, `easeIn` | `Easing.in(Easing.ease)` |
|
|
458
|
+
| `easeOut` | `Easing.out(Easing.ease)` |
|
|
459
|
+
| `easeInOut` | `Easing.inOut(Easing.ease)` |
|
|
460
|
+
| `spring` | `spring()` from remotion |
|
|
461
|
+
| `spring(damping:N, mass:N)` | spring with custom config |
|
|
462
|
+
|
|
463
|
+
### Color tween
|
|
464
|
+
|
|
465
|
+
Hexadecimal colors can be interpolated:
|
|
466
|
+
|
|
467
|
+
```json
|
|
468
|
+
{ "fill": "tween(#000000, #ff0000)" }
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Important notes
|
|
472
|
+
|
|
473
|
+
- Tween values only work inside `component` nodes (not on `image`, `video`, `audio`, etc.).
|
|
474
|
+
- The frame range is derived from the **action duration** (the `duration` field for the component minus its `start` offset).
|
|
475
|
+
- The `tween()` function uses Remotion's `interpolate()` under the hood, with the frame range set to the component's action duration.
|
|
476
|
+
- At frame 0, `tween(from, to)` returns `from`. The component receives the initial value immediately.
|
|
477
|
+
|
|
478
|
+
## Generation Workflow
|
|
479
|
+
|
|
480
|
+
1. Choose `width`/`height`/`fps`/`layout`/`theme`.
|
|
481
|
+
2. Break the video into 3–7 `scene` nodes.
|
|
482
|
+
3. For each scene: write `script` (narration, TTS source) and `instruction` (visual intent). Only `scene` nodes carry `script` — leaf nodes ignore it. When scenes nest, the **innermost** scene's `script` wins; parent scenes with nested `script` children are skipped to prevent overlapping narration.
|
|
483
|
+
4. Add leaf nodes inside scenes.
|
|
484
|
+
5. Add transitions only between scene-grouped sequences.
|
|
485
|
+
6. Verify every leaf has resolvable duration.
|
|
486
|
+
|
|
487
|
+
## Validation Checklist
|
|
488
|
+
|
|
489
|
+
- [ ] Root has width, height, fps, layout, children.
|
|
490
|
+
- [ ] Every scene/container has non-empty children.
|
|
491
|
+
- [ ] Every video/audio/image/map/component has duration or trim.
|
|
492
|
+
- [ ] No `start` outside parallel.
|
|
493
|
+
- [ ] No duplicate `id` within the same parent.
|
|
494
|
+
- [ ] Event targets (`"id": "name"` on component) match the `"id"` used in `on.when` / `on.state` expressions.
|
|
495
|
+
- [ ] All JSON is valid (no comments, no trailing commas).
|
|
496
|
+
|
|
497
|
+
## Example
|
|
498
|
+
|
|
499
|
+
```json
|
|
500
|
+
{
|
|
501
|
+
"width": 1080,
|
|
502
|
+
"height": 1920,
|
|
503
|
+
"fps": 30,
|
|
504
|
+
"layout": "series",
|
|
505
|
+
"theme": "neon",
|
|
506
|
+
"children": [
|
|
507
|
+
{
|
|
508
|
+
"type": "scene",
|
|
509
|
+
"title": "Hook",
|
|
510
|
+
"layout": "parallel",
|
|
511
|
+
"children": [
|
|
512
|
+
{ "type": "audio", "script": "A short memory film", "volume": 1 },
|
|
513
|
+
{ "type": "image", "src": "cover.jpg", "duration": 2 },
|
|
514
|
+
{ "type": "image", "src": "intro.jpg", "duration": 1.6, "start": 0.2 }
|
|
515
|
+
]
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
"type": "scene",
|
|
519
|
+
"title": "Journey",
|
|
520
|
+
"layout": "transitionSeries",
|
|
521
|
+
"transition": "fade",
|
|
522
|
+
"transitionTime": 0.4,
|
|
523
|
+
"children": [
|
|
524
|
+
{ "type": "audio", "script": "Set location and emotional tone", "volume": 1 },
|
|
525
|
+
{ "type": "video", "src": "clips/arrival.mp4", "startFrom": 0, "endAt": 3.5 },
|
|
526
|
+
{ "type": "video", "src": "clips/fire.mp4", "startFrom": 1, "endAt": 4 }
|
|
527
|
+
]
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
"type": "scene",
|
|
531
|
+
"title": "Stat",
|
|
532
|
+
"layout": "parallel",
|
|
533
|
+
"children": [
|
|
534
|
+
{ "type": "component", "jsx": "<StatCounter value={42} label='S-mores' />", "duration": 2 }
|
|
535
|
+
]
|
|
536
|
+
}
|
|
537
|
+
]
|
|
538
|
+
}
|
|
539
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Label System
|
|
2
|
+
|
|
3
|
+
Interactive media labeling before understanding clips in json stream tree.
|
|
4
|
+
The stream tree follow rules, see [docs/json-descriptive.md](docs/json-descriptive.md) for full details .
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
The label player lets you load a stream tree, browse its clips, and attach descriptive labels to each clip's media.
|
|
9
|
+
The output is an updated `labels.json` file with `description` fields populated.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Label from a stream tree JSON or labels.json
|
|
15
|
+
npx markcut preview labels.json --label
|
|
16
|
+
|
|
17
|
+
# Specify port
|
|
18
|
+
npx markcut preview labels.json --label --port 3031
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Labels Persistence
|
|
22
|
+
The stream tree follow rules, see [docs/json-descriptive.md](docs/json-descriptive.md) for full details .
|
|
23
|
+
Labels are saved to the source file.
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"root": {
|
|
28
|
+
"id": "root",
|
|
29
|
+
"type": "root",
|
|
30
|
+
"children": [
|
|
31
|
+
{
|
|
32
|
+
"id": "scene-1",
|
|
33
|
+
"type": "folder",
|
|
34
|
+
"children": [
|
|
35
|
+
{
|
|
36
|
+
"id": "scene-1-media",
|
|
37
|
+
"type": "image",
|
|
38
|
+
"src": "/media/photo.jpg",
|
|
39
|
+
"actions": [{ "start": 0, "end": 5 }],
|
|
40
|
+
"description": "Best group shot from the campfire"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "scene-2",
|
|
46
|
+
"type": "folder",
|
|
47
|
+
"children": [
|
|
48
|
+
{
|
|
49
|
+
"id": "scene-2-media",
|
|
50
|
+
"type": "video",
|
|
51
|
+
"src": "/media/clip.mp4",
|
|
52
|
+
"actions": [{ "start": 0, "end": 5 }],
|
|
53
|
+
"description": "Sunset timelapse — use for intro"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The `description` field on each media leaf node holds the label text.
|
|
63
|
+
|
|
64
|
+
## Workflow
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Stream tree JSON (labels.json)
|
|
68
|
+
│
|
|
69
|
+
▼
|
|
70
|
+
─── preview --label ───→ Browse & label clips
|
|
71
|
+
│ │
|
|
72
|
+
│ ▼
|
|
73
|
+
│ Updated labels.json
|
|
74
|
+
│ │
|
|
75
|
+
▼ ▼
|
|
76
|
+
Use in Storyboard ──────→ Assemble → Render
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
1. **Prepare** a stream tree JSON with scenes
|
|
80
|
+
2. **Label** with `preview --label` to add descriptions to each scene
|
|
81
|
+
3. **Export** labels are saved alongside the source file
|
|
82
|
+
4. **Use** the labeled scenes in your storyboard or assembly workflow
|
|
83
|
+
|
|
84
|
+
## API Endpoints
|
|
85
|
+
|
|
86
|
+
| Endpoint | Method | Description |
|
|
87
|
+
|----------|--------|-------------|
|
|
88
|
+
| `/api/video-data` | GET | Returns the raw stream tree JSON |
|
|
89
|
+
| `/api/video-info` | GET | Returns scenes array with timing + totalDuration |
|
|
90
|
+
| `/api/labels` | GET | Returns the current stream tree with descriptions |
|
|
91
|
+
| `/api/labels` | POST | Saves updated descriptions ({descriptions: {index: "text"}}) |
|
|
92
|
+
| `/api/shutdown` | POST | Kills the server |
|
|
93
|
+
| `/api/events` | GET | SSE events (reload on file change) |
|
|
94
|
+
|
|
95
|
+
## Implementation
|
|
96
|
+
|
|
97
|
+
Source: `src/player/label-server.mjs`
|
|
98
|
+
|
|
99
|
+
- Built on Node.js `http` module (no Express dependency)
|
|
100
|
+
- Serves bundled `player.js` (esbuild output from `src/player/browser.tsx`)
|
|
101
|
+
- Labels saved atomically to disk on each POST
|
|
102
|
+
- Accepts stream tree JSON only (no media folder mode)
|
|
103
|
+
|
|
104
|
+
## Reference
|
|
105
|
+
|
|
106
|
+
| Topic | Link |
|
|
107
|
+
|-------|------|
|
|
108
|
+
| Storyboard (video planning) | [storyboard.md](storyboard.md) |
|
|
109
|
+
| Stream tree types | `SKILL.md` |
|
|
110
|
+
| Dynamic components | [dynamic-components.md](dynamic-components.md) |
|