@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.
Files changed (98) hide show
  1. package/.env.example +27 -0
  2. package/.github/user-steer.md +9 -0
  3. package/.vscode/settings.json +3 -0
  4. package/AGENTS.md +271 -0
  5. package/README.md +219 -0
  6. package/SKILL.md +209 -0
  7. package/docs/dynamic-components.md +191 -0
  8. package/docs/edit-mode.md +220 -0
  9. package/docs/json-descriptive.md +539 -0
  10. package/docs/label-mode.md +110 -0
  11. package/docs/markdown-descriptive.md +751 -0
  12. package/docs/templates.md +52 -0
  13. package/package.json +64 -0
  14. package/remotion.config.ts +5 -0
  15. package/scripts/artlist-dl.mjs +190 -0
  16. package/scripts/build-pipeline.sh +19 -0
  17. package/scripts/build-player.sh +20 -0
  18. package/src/Root.tsx +55 -0
  19. package/src/config.mjs +88 -0
  20. package/src/context/EventContext.tsx +168 -0
  21. package/src/context/index.tsx +42 -0
  22. package/src/descriptive/compiler.test.ts +1135 -0
  23. package/src/descriptive/compiler.ts +1230 -0
  24. package/src/descriptive/dsl.ts +455 -0
  25. package/src/descriptive/markdown.test.ts +866 -0
  26. package/src/descriptive/markdown.ts +674 -0
  27. package/src/descriptive/resolve.test.ts +951 -0
  28. package/src/descriptive/resolve.ts +891 -0
  29. package/src/entry.tsx +163 -0
  30. package/src/index.ts +4 -0
  31. package/src/player/browser.tsx +356 -0
  32. package/src/player/bundle/player.js +60259 -0
  33. package/src/player/bundler.mjs +269 -0
  34. package/src/player/label-server.mjs +599 -0
  35. package/src/player/pipeline.mjs +11123 -0
  36. package/src/player/pipeline.ts +117 -0
  37. package/src/player/server-shared.mjs +144 -0
  38. package/src/player/server.mjs +1006 -0
  39. package/src/render/cli-tools.ts +177 -0
  40. package/src/render/cli.mjs +628 -0
  41. package/src/schema/index.ts +259 -0
  42. package/src/types/Audio.tsx +56 -0
  43. package/src/types/Component.tsx +135 -0
  44. package/src/types/Effect.tsx +88 -0
  45. package/src/types/Folder.tsx +180 -0
  46. package/src/types/FrameSyncStyle.tsx +51 -0
  47. package/src/types/Image.tsx +51 -0
  48. package/src/types/Include.tsx +394 -0
  49. package/src/types/Map.tsx +252 -0
  50. package/src/types/Rhythm.tsx +58 -0
  51. package/src/types/Scene.tsx +42 -0
  52. package/src/types/Subtitle.tsx +218 -0
  53. package/src/types/Video.tsx +70 -0
  54. package/src/types/keyframes.ts +454 -0
  55. package/src/utils/__tests__/vtt.test.ts +129 -0
  56. package/src/utils/index.ts +168 -0
  57. package/src/utils/tween.ts +118 -0
  58. package/src/vision/cli.mjs +1187 -0
  59. package/src/vision/vision_prompts.md +67 -0
  60. package/tests/dsl.test.ts +317 -0
  61. package/tests/fixtures/audio.json +25 -0
  62. package/tests/fixtures/basic.json +27 -0
  63. package/tests/fixtures/component-all.json +38 -0
  64. package/tests/fixtures/components.json +38 -0
  65. package/tests/fixtures/effects.json +64 -0
  66. package/tests/fixtures/full.json +51 -0
  67. package/tests/fixtures/map.json +23 -0
  68. package/tests/fixtures/md/all-nodes.md +28 -0
  69. package/tests/fixtures/md/basic.md +6 -0
  70. package/tests/fixtures/md/component-imports.md +20 -0
  71. package/tests/fixtures/md/edge-cases.md +33 -0
  72. package/tests/fixtures/md/effects.md +17 -0
  73. package/tests/fixtures/md/frontmatter.md +20 -0
  74. package/tests/fixtures/md/full-feature.md +58 -0
  75. package/tests/fixtures/md/imports-block.md +19 -0
  76. package/tests/fixtures/md/include-main.md +11 -0
  77. package/tests/fixtures/md/include-sub.md +25 -0
  78. package/tests/fixtures/md/jsx-code-fence.md +21 -0
  79. package/tests/fixtures/md/map.md +11 -0
  80. package/tests/fixtures/md/nested-scenes.md +25 -0
  81. package/tests/fixtures/md/rhythm.md +17 -0
  82. package/tests/fixtures/md/scenes.md +16 -0
  83. package/tests/fixtures/md/tween.md +11 -0
  84. package/tests/fixtures/md/vars-test.md +6 -0
  85. package/tests/fixtures/scenes.json +40 -0
  86. package/tests/fixtures/subtitle.json +59 -0
  87. package/tests/fixtures/subvideo.json +59 -0
  88. package/tests/fixtures/templates/courseware.md +351 -0
  89. package/tests/fixtures/tween-visual.json +28 -0
  90. package/tests/fixtures/video-series.json +54 -0
  91. package/tests/md-descriptive.test.ts +742 -0
  92. package/tests/render.test.ts +985 -0
  93. package/tests/schema.test.ts +68 -0
  94. package/tests/server.test.ts +308 -0
  95. package/tests/utils.ts +391 -0
  96. package/tests/vitest.config.ts +18 -0
  97. package/tests/vitest.integration.config.ts +16 -0
  98. 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) |