@lalalic/markcut 2.4.2 → 2.4.5

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/AGENTS.md CHANGED
@@ -242,9 +242,9 @@ npx vitest run tests/render.test.ts -t "Test Name" # single test
242
242
  1. **TransitionSeries + isBackground**: `TransitionSeries` from `@remotion/transitions` rejects non-Sequence/Transition children. Don't mix `isBackground:true` children with `isSeries:true, transition:"..."` at the same level.
243
243
  2. **Video endAt formula**: Must be `startFrom_frames + ((endAt_source - startFrom_source) * fps / playbackRate)` — changing to `endAt_source * fps` breaks slow-motion playback.
244
244
  3. **`signalstats` ffmpeg filter**: Doesn't output YAVG on all systems; use file size as proxy for visual content.
245
- 4. **Asset paths**: Local files are resolved via `staticFile()` files go in `public/` and are referenced without the `public/` prefix.
245
+ 4. **Asset paths**: All `src` paths in markdown are resolved relative to the `.md` file's directory. Preview mode serves files via a multi-directory HTTP server; render mode uses absolute paths directly (bypassing `staticFile()`). Never use a `public/` prefix in markdown `src`.
246
246
  5. **Map rendering**: Depends on external tile availability; may produce blank frames offline.
247
- 6. **CLI md render needs three things preview gets for free**: (a) `parseMarkdownVariants(raw)` (returns `{base, variants}`) — `parseMarkdownDescriptive` returns the root directly; (b) bundling the ` ```js imports ``` ` block via `bundleFromEntries` and setting `root.imports` (otherwise jsx components are unregistered → slides render as unstyled black frames); (c) staging `.markcut/` assets into `public/.render-assets/` and rewriting srcs to relative paths (remotion render only serves its publicDir). All three live in `src/render/cli.mjs`; the import-map shim for shared react/remotion instances is `src/utils/component-import-map.ts` (must stay in sync with `player/browser.tsx` + `bundler.mjs getSharedExternals`).
247
+ 6. **CLI md render needs two things preview gets for free**: (a) `parseMarkdownVariants(raw)` (returns `{base, variants}`) — `parseMarkdownDescriptive` returns the root directly; (b) bundling the ` ```js imports ``` ` block via `bundleFromEntries` and setting `root.imports` (otherwise jsx components are unregistered → slides render as unstyled black frames). Asset staging (`stageLocalAssets`) was removed render now keeps absolute paths from `resolve.ts`, bypassing `staticFile()` entirely. All live in `src/render/cli.mjs`; the import-map shim for shared react/remotion instances is `src/utils/component-import-map.ts` (must stay in sync with `player/browser.tsx` + `bundler.mjs getSharedExternals`).
248
248
  7. **Publishing**: verify `npx @lalalic/markcut render <md>` from a clean temp dir before/after publish — v1.1.1 shipped with cli.mjs/pipeline.mjs out of sync and md rendering was completely broken. Also beware stale npx caches (`~/.npm/_npx`) silently running old code.
249
249
  8. **Clean break from `actions[]`**: leaf timing is now flat base fields (`start`/`end`/`duration`/`startFrom`/`endAt`). Old compiled JSON carrying `actions:[{...}]` no longer parses into a duration — re-compile from markdown/descriptive JSON, or migrate `actions[0]` → flat fields (drop `id`/`volume`/`style`/`effectId`; keep `start`/`end`/`startFrom`/`endAt`/`loop`).
250
250
 
package/README.md CHANGED
@@ -91,10 +91,7 @@ Inline components can be defined entirely in the imports block using `export fun
91
91
 
92
92
  | Document | What it covers |
93
93
  |---|---|
94
- | [docs/json-descriptive.md](docs/json-descriptive.md) | Full JSON descriptive schema reference |
95
94
  | [docs/markdown-strict-descriptive.md](docs/markdown-strict-descriptive.md) | Markdown descriptive syntax reference |
96
- | [docs/label-mode.md](docs/label-mode.md) | Label mode player and workflow |
97
- | [docs/edit-mode.md](docs/edit-mode.md) | Live edit mode with SSE reload |
98
95
 
99
96
 
100
97
  ## Variants
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalalic/markcut",
3
- "version": "2.4.2",
3
+ "version": "2.4.5",
4
4
  "description": "Markdown-to-video engine. Describe scenes in markdown, get a rendered video.",
5
5
  "bin": {
6
6
  "markcut": "bin/markcut"
@@ -88,3 +88,4 @@ some common issues (photo or video can't be displayed, audio missing), take belo
88
88
  - markcut resolver will automatically calculate the duration based on the audio script length
89
89
  - **don't** rm `.markcut` directory, which served as cache for all generated content. cache will auto update according to the content change. rm `.markcut` will cause all content to be regenerated, which is time consuming and wasteful.
90
90
  - put all manual assets in `assets` folder, such as bgm, logo, watermark, etc. don't put them in `.markcut` folder, which is auto generated and will be deleted when `markcut clean` command is run.
91
+ - `npx @lalalic/markcut preview` stuck until user close the preview window.
@@ -1,62 +1,271 @@
1
- # Markdown Descriptive (Agent Contract)
1
+ # markcut Markdown Descriptive Spec
2
+ markcut Markdown Descriptive is a markdown-based authoring format for markcut. It is designed to be human-readable and writable, while also being machine-parsable. It is the primary authoring format for markcut, and is used for both interactive and non-interactive workflows.
3
+
4
+
5
+ ## DSL — Complete Grammar Reference
6
+
7
+ This section defines the markcut Markdown Descriptive grammar. An agent should read this to understand the exact syntax rules for constructing or parsing a `.md` video document.
8
+
9
+ ### 1. Document Structure (top to bottom)
10
+
11
+ ```
12
+ ┌─ YAML Frontmatter (optional)
13
+ ├─ ───...───
14
+ │ hook: My Video
15
+ │ emotion: ...
16
+ │ ───...───
17
+ ├─ # video ← Root node (REQUIRED, heading level 1)
18
+ │ width:1080 height:1920 fps:30 layout:series
19
+ │ subtitle:{src:"captions.vtt",type:"Typewriter"}
20
+ │ ~~~css stylesheet ← root.stylesheet
21
+ │ .bg { background: #000; }
22
+ │ ~~~
23
+ ├─ ~~~js imports ← Imports block
24
+ │ export { PieChart } from "recharts"
25
+ │ ~~~
26
+ ├─ ## scene1 ← Scene nodes (heading level 2+)
27
+ │ layout:parallel
28
+ │ - image src:bg.jpg duration:3
29
+ │ - script "Hello world"
30
+ │ ### sub-scene ← Nested scene (heading level 3)
31
+ │ - component duration:2 jsx:"<Title />"
32
+ ├─ ## scene2
33
+ │ - video src:clip.mp4 effects:[fadeIn]
34
+ ├─ # zh ← Variant sections (h1 headings OTHER than `video`)
35
+ │ tts:"edge-tts --voice zh-CN-YunxiNeural..." ← override root.tts for zh variant
36
+ ├─ # youtube ← Variant sections
37
+ width:1920 height:1080
38
+ ```
39
+
40
+ ### 2. Heading Levels & Their Roles
41
+
42
+ | Level | Pattern | Role | Rules |
43
+ |---|---|---|---|
44
+ | H1 | `# video` | **Root** — video canvas definition | Exactly one `# video` required. Everything else is nested inside this section. |
45
+ | H1 | `# zh`, `# youtube`, `# portrait` etc. | **Variant** — override section for alternate versions | Any `# <name>` other than `video`. Only contains override configs + variant-prefixed overrides on base nodes. |
46
+ | H2 | `## <name>` | **Scene** — top-level scene container | `name` = single word (no spaces).|
47
+ | H3 | `### <name>` | **Nested scene** — sub-scene inside parent | Same rules as H2. Creates nesting: parent scene's children array includes this as a sub-container. |
48
+ | H4+ | `#### <name>` | **Deeply nested** — further nesting | Works identically; depth reflects nesting level. |
2
49
 
3
- Complete reference for LLM-driven video generation. The parser uses **remark** (`unified` + `remark-parse` + `remark-frontmatter`) for structural parsing (headings, lists, code fences) and extracts raw text from source positions, so JSX values with `<`/`>` are preserved correctly.
50
+ ### 3. Leaf Nodes Bullet Syntax
4
51
 
5
- ## Output Contract
52
+ Every leaf (media, component, rhythm, map, effect, include) is a **markdown bullet** (`- `) containing space-separated tokens:
6
53
 
7
- A markdown document compiled into a renderable scene tree.
54
+ ```
55
+ - <typeToken> [key:value ...] [bareFlag ...]
56
+ ```
8
57
 
9
- - Top heading `# video` marks the document root. (Only `# video` is treated as the main section — `# anything_else` creates a **variant** section for alternate configs, see [Variants](#variants-language--platform--any-override).)
10
- - Optional YAML frontmatter block `---\n...\n---\n` at the very top
11
- - Root config line: `width:<n> height:<n> fps:<n> layout:<mode>` (key:value pairs on the line after `# video`)
12
- - Scenes via `##`/`###`/`####` headings
13
- - Leaf nodes via `- typeToken ...` bullets
14
- - Component registrations via `` ~~~js imports `` code fence (or inline JSX definitions)
15
- - Properties via indented code fences (`~~~<lang> <propName>`); `~~~script` only valid on audio nodes
58
+ Where:
59
+ - **`typeToken`** the node type (see table below). Required as first token.
60
+ - **`key:value`** property assignments. Space-separated. Values with spaces must be quoted: `title:"Long Title"`, `instruction:"make it cinematic"`.
61
+ - **`bareFlag`** boolean property that defaults to `true` when present (no `:value` needed). Examples: `isBackground`, `foreground`, `visible`.
16
62
 
17
- ## Frontmatter (metadata only)
63
+ #### Type Token Reference
18
64
 
19
- A YAML block at the top of the document, delimited by `---`. Frontmatter is
20
- **metadata only** — it does NOT affect video configuration. All video config
21
- (width, height, fps, layout, tts, stt, stylesheet, etc.) comes from the
22
- **root config line** (key:value pairs on the line after `# video`).
65
+ | Token | Stream Type | When | Required Keys |
66
+ |---|---|---|---|
67
+ | `image` | `image` | Still photos, title cards | `src`, `duration` |
68
+ | `video` | `video` | Video clips | `src` + (`duration` or `endAt`) |
69
+ | `audio` | `audio` | Voiceover, BGM, SFX | `src` + (`duration` or `endAt`) |
70
+ | `script "..."` | `audio` | Narration/TTS (shorthand for audio with script) | `script` text (the `"..."` is the primary content) |
71
+ | `component` | `component` | JSX React component | `jsx` (inline or code fence) + `duration` (unless `isBackground`) |
72
+ | `rhythm` | `rhythm` | Beat-synced audio with timed children | `src`, `spots`, `children` |
73
+ | `map` | `map` | Animated route visualization | `duration`, `waypoints` |
74
+ | `include` | `include` | Embed external `.md` sub-video | `src` (path to another `.md` file) |
75
+ | `parallel` | *container* | Parallel layout container | `children` (indented bullets below) |
76
+ | `series` | *container* | Sequential layout container | `children` (indented bullets below) |
77
+ | `transitionSeries` | *container* | Sequential with transitions | `children`, `transition` (e.g. `transition:fade`) |
78
+
79
+ ### 4. Property Value Formats
80
+
81
+ | Format | Example | Description |
82
+ |---|---|---|
83
+ | `key:number` | `duration:3`, `width:1080`, `volume:0.8` | Numeric values (int or float). Seconds for time values. |
84
+ | `key:string` | `src:cover.jpg`, `fit:cover`, `animation:fadeIn` | Unquoted single-word string. |
85
+ | `key:"quoted string"` | `title:"Long Title"`, `jsx:"<StatCounter value={42} />"` | Quoted string for values containing spaces or special chars. Supports double `"` and single `'` quotes. |
86
+ | `bareFlag` | `isBackground`, `foreground` | Boolean flag — sets the key to `true` when present. |
87
+ | `key:JSON` | `spots:[0.5,1.2,1.9]`, `on:(start, slide.current=0)`, `effects:[fadeIn, bounceIn]` | Structured values using brackets/parens. See subsections below. |
88
+ | `key:<mode>` | `layout:series` | Enum-like mode selection. |
89
+ | `~~~<lang> [propName=lang]` | `~~~jsx jsx`, `~~~script script` | Indented code fence for long-form property values (JSX, script, prompt, etc.). See section 6. |
23
90
 
24
- ```yaml
25
- ---
26
- title: My Campaign
27
- description: Q4 product launch
28
- ---
91
+ ### 5. Structured Property Patterns
92
+
93
+ #### Effects — `effects:[name, name(params...)]`
94
+
95
+ Apply animations on any leaf or container. No wrapper node needed.
96
+ Built-in `animation` values:
97
+ `fadeIn fadeOut fadeInDown fadeInUp fadeInLeft fadeInRight fadeOutDown fadeOutUp fadeOutLeft fadeOutRight slideInDown slideInUp slideInLeft slideInRight slideOutDown slideOutUp slideOutLeft slideOutRight zoomIn zoomOut zoomInDown zoomInUp zoomInLeft zoomInRight bounce bounceIn rotateIn rotateOut rotateInDownLeft rotateInDownRight rotateInUpLeft rotateInUpRight flipInX flipInY pulse flash heartBeat rubberBand shakeX shakeY swing tada wobble jello rollIn rollOut jackInTheBox lightSpeedIn lightSpeedOut`
98
+
99
+ `- effect animation:fadeIn duration:2` then indented children.
100
+
101
+ ```
102
+ - image src:hero.jpg duration:3 effects:[fadeIn(1.5, ease-out, 2)]
103
+ - image src:hero.jpg duration:3 effects:[fadeIn, bounceIn(1, ease-out)]
104
+ ```
105
+
106
+ Params: `(duration=1s, timingFunction=linear, iterationCount=1)` — all optional, comma-separated.
107
+
108
+ #### Events — `on:(when, state)`
109
+
110
+ Fire JS expression at a specific frame to mutate component state.
111
+
112
+ ```
113
+ - script "Narration" on:(start, slide.current=0)
114
+ - script "Beat" on:(50%, slide.current++)
115
+ ```
116
+
117
+ `when`: `start`/`end`/`50%` (percent) / `2.5s` (seconds value). `state`: any JS expression.
118
+
119
+ #### Lists — `spots:[n,n,n]`, `start:n`
120
+
121
+ ```
122
+ - rhythm src:beat.mp3 spots:[0.5,1.2,1.9]
123
+ ```
124
+
125
+ #### Waypoints — `waypoints:[lat,lng,"label"; lat,lng,"label"]`
126
+
127
+ Semicolons separate waypoints, commas separate fields within one waypoint.
128
+
129
+ ```
130
+ - map duration:3 waypoints:[37.77,-122.41,"SF";34.05,-118.24,"LA"]
131
+ ```
132
+
133
+ #### Duration shorthand
134
+
135
+ `duration:n` is syntactic sugar for `end:start+duration` (both `start` and `end` default to 0 when absent). If `end` is set explicitly, `duration` is ignored.
136
+
137
+ ### 6. Long-Form Properties — Indented Code Fences
138
+
139
+ When a property value is too long for a single line (JSX, prompts, scripts, markdown source), use an **indented code fence** under the bullet:
140
+
141
+ ```md
142
+ - component duration:4
143
+ ~~~jsx jsx
144
+ <StatCounter value={42} />
145
+ ~~~
146
+ - video start:5 volume:0
147
+ ~~~prompt prompt
148
+ animation of a robot learning to walk, cinematic lighting
149
+ ~~~
150
+ - audio src:bg.mp3 duration:10
151
+ ~~~script
152
+ This is a longer narration that spans multiple lines.
153
+ ~~~
154
+ ```
155
+
156
+ **Syntax**: `~~~<lang> [propertyName=lang]`. If `propertyName` is omitted, it defaults to `lang`.
157
+
158
+ | Fence | Sets property | Use case |
159
+ |---|---|---|
160
+ | `~~~jsx jsx` or `~~~jsx` | `jsx` | Component JSX expression |
161
+ | `~~~prompt prompt` | `prompt` | TTI/TTV generation prompt |
162
+ | `~~~script script` or `~~~script` | `script` | Narration text on audio nodes |
163
+ | `~~~css stylesheet` | `stylesheet` | Global CSS (only valid at root level) |
164
+ | `~~~md <key>` | arbitrary | Markdown content for a specific key (e.g., `source` for `react-markdown`) |
165
+
166
+ ### 7. Scene Metadata Block
167
+
168
+ Scene properties go on the **line(s) immediately below the heading**, before any child bullets:
169
+
170
+ ```md
171
+ ## MyScene
172
+ layout:parallel transition:fade transitionTime:1.2
173
+ title:"My Scene Title" instruction:"Cinematic intro"
174
+ - image src:bg.jpg duration:3
175
+ - script "Narration here"
176
+ ```
177
+
178
+ Multiple metadata lines are allowed. They're parsed as space-separated key:value pairs per line.
179
+
180
+ ### 8. Imports Block — Component Registry
181
+
182
+ A `` ~~~js imports `` code fence (typically at the end of the document) registers external React components for use in `jsx:` expressions:
183
+
184
+ ```md
185
+ ~~~js imports
186
+ export { PieChart } from "recharts"
187
+ export { StatCounter as Counter } from "stat-counter"
188
+ export function Hello({ name }) {
189
+ return <div style={{color: '#fff'}}>Hello {name}</div>;
190
+ }
191
+ ~~~
192
+ ```
193
+
194
+ Patterns: `export { Name } from "spec"`, `export { Name as Alias } from "spec"`, `export function Name(...) { ... }`, `import { Name } from "spec"` (also works).
195
+
196
+ ### 9. Variant Overrides Syntax
197
+
198
+ Variant sections (`# zh`, `# portrait`, etc.) provide root-level overrides. Leaf nodes use **variant-prefixed keys** or **bare variant keys** to override content per variant:
199
+
200
+ ```md
201
+ # video
202
+ - script "English text" ← base value
203
+ zh:"中文文本" ← bare variant key: replaces "script" (primary key for audio)
204
+ - component jsx:"<Slide>{source}</Slide>"
205
+ ~~~md source ← base value for key "source"
206
+ ## English title
207
+ ~~~
208
+ ~~~md zh-source ← variant-prefixed: replaces "source" when variant=zh
209
+ ## 中文标题
210
+ ~~~
29
211
  ```
30
212
 
31
- ## Root Config Line
213
+ | Override type | Syntax | What it replaces |
214
+ |---|---|---|
215
+ | **Bare variant key** | `zh:"value"` | Node's primary content key (`script` for audio, `jsx` for component, `src` for image/video) |
216
+ | **Variant-prefixed key** | `zh-src:path` | Specific key matching the suffix (`zh-src` → replaces `src`) |
217
+ | **Variant-prefixed code fence** | `~~~md zh-source` | Same as above, for code-fence-backed properties |
218
+
219
+ ### 10. Root Config Line
32
220
 
33
- The line after `# video` contains all video configuration as space-separated
34
- `key:value` pairs:
221
+ The line after `# video` holds canvas-level config as space-separated `key:value` pairs:
35
222
 
36
223
  ```markdown
37
224
  # video
38
- width:1920 height:1080 fps:30 layout:series
39
- ~~~css stylesheet
40
- .bg { background: #000; }
41
- ~~~
42
- subtitle:{src:"captions.vtt",type:"Bounce"}
225
+ width:1920 height:1080 fps:30 layout:series transition:fade transitionTime:1.2
226
+ subtitle:{src:"captions.vtt",type:"Typewriter",fontSize:48}
43
227
  ```
44
228
 
45
- Supported keys: `width`, `height`, `fps`, `layout`, `tts`, `stt`, `tti`, `ttv`,
46
- `transition`, `transitionTime`, `instruction`, `metadata`, `stylesheet`, `subtitle`.
229
+ Supported root keys: `width`, `height`, `fps`, `layout`, `tts`, `stt`, `tti`, `ttv`, `transition`, `transitionTime`, `instruction`, `metadata`, `stylesheet`, `subtitle`, `voices`.
47
230
 
48
- Values containing spaces must be quoted with double or single quotes.
231
+ Values containing spaces (like JSON for `subtitle:` or `voices:`) must be quoted with double or single quotes, or wrapped in `{...}`/`[...]` brackets which the parser handles natively.
49
232
 
50
- ### Subtitle on the config line
233
+ ### 11. Frontmatter (Metadata Only)
51
234
 
52
- Set subtitle via `subtitle:` on the root config line:
235
+ Optional YAML block at the very top, delimited by `---`. **Does NOT affect video config** — only for metadata:
236
+
237
+ ```yaml
238
+ ---
239
+ title: My Campaign
240
+ description: Q4 product launch
241
+ hook: ...
242
+ conflict: ...
243
+ emotion: ...
244
+ ending: ...
245
+ ---
246
+ ```
247
+
248
+ All video configuration comes from the root config line (section 10), NOT from frontmatter.
249
+
250
+ ### 12. Minimal Valid Document
53
251
 
54
252
  ```markdown
55
253
  # video
56
- width:640 height:480 subtitle:{src:"captions.vtt",type:"Bounce",fontSize:48}
254
+ width:1080 height:1920 fps:30 layout:series
255
+
256
+ ## Scene1
257
+ - image src:bg.jpg duration:3
258
+ - script "Hello world"
57
259
  ```
58
260
 
59
- See the [Subtitle](#subtitle) section below for all supported fields.
261
+ ### 13. `src` Path Resolution Context Root
262
+ all src paths are resolved relative to the markdown file's location. For example, if your markdown file is at `./videos/course.md`, then `src:./assets/bg.jpg` resolves to `./videos/assets/bg.jpg`. Absolute paths (starting with `/`) are resolved relative to the project root.
263
+
264
+
265
+ ### 14. verify
266
+ - all assets path are resolved relative to the markdown file's location.
267
+ - `npx @lalalic/markcut verify book.md` to verify the markdown document is valid and can be rendered. It will check for missing required fields, invalid values, and other common issues.
268
+
60
269
 
61
270
  ## Template Variables
62
271
 
@@ -98,53 +307,6 @@ Subtitles are configured at the root level as a VTT overlay. Set via `subtitle:`
98
307
 
99
308
  If `src` is plain text (no `-->` markers), it renders as a single caption for the full video duration. The `type` field maps to a `remotion-subtitle` animation component — omit for a plain static caption.
100
309
 
101
-
102
- ### Imports block (recommended)
103
-
104
- Use a `` ~~~js imports `` code fence at end of the document (or anywhere in the body). The block acts as a **component registry** — it re-exports components from external packages or defines them inline, making them available to JSX expressions throughout the video.
105
-
106
- ```
107
- ~~~js imports
108
- export { PieChart } from "recharts"
109
- export { BarChart, LineChart } from "recharts"
110
- export { StatCounter as Counter } from "stat-counter"
111
-
112
- export function Hello({ name }) {
113
- return <div style={{color: '#fff'}}>Hello {name}</div>;
114
- }
115
- ~~~
116
- ```
117
-
118
- Think of this block as the **index file** for the video's component scope. `export { Name } from "spec"` re-exports an external component (conceptually correct — the block is the public API of available components). `export function Name()` defines an inline component directly.
119
-
120
- The imports block is the **primary** way to register components. The legacy YAML `imports:` array in frontmatter is still supported as a fallback, but the code block is preferred.
121
-
122
- > **How it works**: The server extracts the imports block from the raw source, parses it with `parseImportsBlock`, then runs `bundleFromEntries` which creates a temp npm project, installs packages, and bundles everything into a single ESM file with esbuild. The resulting bundle URL is set on `root.imports` (e.g. `/.component-cache/be710e5c.js`). At render time, `MarkCut.useComponentRegistry` dynamically imports this URL and feeds the named exports to react-jsx-parser.
123
-
124
- Supported patterns inside the block:
125
-
126
- | Pattern | Effect |
127
- |---|---|
128
- | `export { Name } from "spec"` | Re-exports `Name` from the resolved source (recommended) |
129
- | `export { Name as Alias } from "spec"` | Re-exports under `Alias` instead |
130
- | `export { N1, N2 } from "spec"` | Re-exports multiple from the same source |
131
- | `export default Name from "spec"` | Re-exports default export |
132
- | `export function Name(...) { ... }` | Inline component definition |
133
- | `export default function Name(...) { ... }` | Inline component definition (default) |
134
-
135
- For compatibility, `import { Name } from "spec"` also works and produces the same result — both syntaxes register the name identically.
136
-
137
- `from:` spec forms:
138
-
139
- | Prefix | Resolved by bundler as |
140
- |---|---|
141
- | `pkg` or `npm:pkg` | npm package — `npm install pkg`, then `esbuild` re-exports it |
142
- | `pkg@1.2.3` or `npm:pkg@1.2.3` | npm package with pinned version |
143
- | `@scope/pkg` or `npm:@scope/pkg` | npm scoped package |
144
- | `git:user/repo/path` | Raw specifier passed to esbuild; requires resolvable module |
145
- | `github:user/repo/path` | Same as `git:` |
146
- | `https://...`, `http://...`, path | Used as-is by esbuild |
147
-
148
310
  ## Key Reference (use these names)
149
311
 
150
312
  | Key | Means | Applies to | Note
@@ -195,42 +357,6 @@ For compatibility, `import { Name } from "spec"` also works and produces the sam
195
357
 
196
358
  ## Type Catalog
197
359
 
198
- Each type below shows: when to use, required keys, markdown syntax.
199
-
200
- ### `scene` (heading) — organizer (preferred)
201
-
202
- When: always. `scene` is a container — scenes can nest inside other scenes via deeper headings (`##` → `###` → `####`).
203
-
204
- Syntax:
205
-
206
- ```md
207
- ## <title>
208
- layout:<x> [transition:<t> transitionTime:<n>] [instruction:..]
209
- - <children>
210
- ```
211
-
212
- Scene metadata (layout, instruction, transition) goes on the line(s) immediately below the heading, before any child bullets. This keeps the heading clean. `name` comes from the heading text (must be a single token — no spaces). For multi-word titles, use key-value `title:"Long Title"` on the metadata line. `title` optionally follows ` - ` in the heading (e.g. `## Chapter1 - The Beginning` splits to name=`Chapter1`, title=`The Beginning`).
213
-
214
- For narration, use the `- script "..."` audio alias as a child of the scene (see [Narration / TTS](#narration--tts) below).
215
-
216
- ### `image`
217
-
218
- When: photos, stills, title cards. Required: `src`, `duration`.
219
-
220
- `- image src:cover.jpg duration:2 fit:cover`
221
-
222
- ### `video`
223
-
224
- When: moving footage. Required: `src` + (`duration` or `endAt`).
225
-
226
- `- video src:clip.mp4 startFrom:1 endAt:4 volume:0.8`
227
-
228
- ### `audio`
229
-
230
- When: voiceover, BGM, SFX, TTS narration. Required: `src` + (`duration` or `endAt`).
231
-
232
- `- audio src:bgm.mp3 duration:6 volume:0.4 isBackground`
233
-
234
360
  ### Narration / TTS
235
361
 
236
362
  Narration text is set via the `script` field on audio nodes. There are three ways to provide it:
@@ -247,7 +373,7 @@ This is a shorthand that creates an audio node with the `script` field. Supports
247
373
 
248
374
  ```md
249
375
  - script "Welcome to the course"
250
- - script "Voiceover" volume:0.8 start:2 duration:5 foreground:true
376
+ - script "Voiceover" volume:0.8 start:2 foreground:true
251
377
  ```
252
378
 
253
379
  **3. Code fence `~~~script` on an audio node**
@@ -269,9 +395,12 @@ All three patterns produce an `audio` node with a `script` field. The pipeline's
269
395
  When a `script` contains multiple lines matching `SpeakerName: text` format, the pipeline automatically expands it into a multi-turn dialogue where each line becomes a separate audio node with its own TTS generation. This allows different speakers to have different voices.
270
396
 
271
397
  ```md
272
- - script "Ray: Hello everyone and welcome
273
- Alice: Good day to you all
274
- Ray: Let's get started"
398
+ - script
399
+ ~~~script
400
+ Ray: Hello everyone and welcome
401
+ Alice: Good day to you all
402
+ Ray: Let's get started
403
+ ~~~
275
404
  ```
276
405
 
277
406
  The dialogue lines play sequentially. Each line is transcribed separately, and subtitles include the speaker prefix (e.g., `Ray: Hello everyone and welcome`).
@@ -295,112 +424,6 @@ voices:{"Ray":"--voice en-US-GuyNeural --rate +20%","Clone":"--voice clone-xxx -
295
424
 
296
425
  The final TTS command for a speaker: `<root tts cli> <speaker voice flags>`.
297
426
 
298
- ### `subtitle` (root-level overlay)
299
-
300
- Subtitles are configured at the root level as a VTT overlay, not as tree nodes. Set via YAML frontmatter `subtitle:` or `root.subtitle` in JSON.
301
-
302
- The `type` field selects an animated caption component from `remotion-subtitle`:
303
-
304
- | Value | Component |
305
- |---|---|
306
- | *(omit)* | `Caption` — plain static text |
307
- | `Bounce` | `BounceCaption` — bouncing entrance |
308
- | `Fade` | `FadeCaption` — fade in |
309
- | `Typewriter` | `TypewriterCaption` — typewriter reveal |
310
- | `Colorful` | `ColorfulCaption` — rainbow text |
311
- | `Glowing` | `GlowingCaption` — glow effect |
312
- | `Neon` | `NeonCaption` — neon sign |
313
- | `Zoom` | `ZoomCaption` — zoom in |
314
-
315
- > **HTML in cue text**: Each cue's text is rendered via `dangerouslySetInnerHTML`, so you can use HTML tags with inline CSS to style individual words:
316
- > ```vtt
317
- > 00:00:01.000 --> 00:00:03.000
318
- > The <span style="color:#ff6b6b;font-weight:bold">quick</span> brown <span style="font-style:italic">fox</span> jumps over the lazy dog
319
- > ```
320
- > The `Typewriter` caption animation correctly respects HTML tag boundaries (character reveal skips over tags, only animates visible text).
321
-
322
- Each cue is rendered as a separate `<Sequence>` for optimal performance — inactive cues consume zero CPU.
323
-
324
- See [JSON Descriptive](json-descriptive.md#subtitle-root-level-overlay) for the full field reference.
325
-
326
- ## Effects on Any Node
327
-
328
- Apply animations directly via the `effects` key on **any** node (leaf or container). No wrapper node needed.
329
-
330
- ```md
331
- - image src:hero.jpg duration:3 effects:[fadeIn]
332
- - component duration:2 jsx:"<Title />" effects:[bounceIn]
333
- - video src:clip.mp4 duration:4 effects:[fadeIn, slideInLeft]
334
- ```
335
-
336
- ### Parameterized syntax
337
-
338
- Add positional parameters in parentheses — comma-separated.
339
-
340
- ```md
341
- - image src:card.jpg duration:3 effects:[fadeIn(1.5)]
342
- - image src:card.jpg duration:3 effects:[fadeIn(1.5, ease-out)]
343
- - image src:card.jpg duration:3 effects:[fadeIn(1.5, ease-out, 2)]
344
- ```
345
-
346
- Order: `(duration, timingFunction, iterationCount)`
347
-
348
- | Position | Parameter | Example |
349
- |---|---|---|
350
- | 1st | `duration` (seconds) | `fadeIn(1.5)` |
351
- | 2nd | `timingFunction` | `fadeIn(1.5, ease-out)` |
352
- | 3rd | `iterationCount` | `fadeIn(1.5, ease-out, 2)` |
353
-
354
- All parameters are optional — omit trailing ones.
355
-
356
- ### Multiple effects
357
-
358
- Effects are applied outermost-first (first in the array is the outermost wrapper):
359
-
360
- ```md
361
- - image src:hero.jpg duration:3 effects:[fadeIn, bounceIn]
362
- ```
363
-
364
- ### `component`
365
-
366
- When: JSX expression rendered at runtime with registered imports in scope. Required for non-background components: `duration` . The `jsx` value can come from an inline attribute or an indented code fence.
367
-
368
- Components must be registered via a `` ~~~js imports `` code block. Usage is via `jsx:"<TagName ... />"` on the component node.
369
-
370
- ```md
371
- ~~~js imports
372
- import { StatCounter } from "stat-counter"
373
- import { Logo } from "github:myorg/design#Logo"
374
- ~~~
375
-
376
- # JSX usage (references registered components as tags)
377
- - component duration:1 jsx:"<StatCounter value={42} />"
378
- ```
379
-
380
- ### Code fence properties
381
-
382
- Properties that are too long for a single line can be provided via an indented code fence under the bullet item. The fence language (`~~~<lang> <propName>`) specifies which property to set:
383
-
384
- ```md
385
- - component duration:4 isBackground:true
386
- ~~~jsx jsx
387
- <div style={{color:'#fff'}}>Hello</div>
388
- ~~~
389
-
390
- - video start:5 volume:0
391
- ~~~prompt prompt
392
- animation of a robot learning to walk, cinematic lighting
393
- ~~~
394
- ```
395
-
396
- The fence syntax is `~~~<lang> <propName>`. If `propName` is omitted, it defaults to `lang`. Common patterns:
397
-
398
- | Fence | Sets property | Use case |
399
- |---|---|---|
400
- | `~~~jsx jsx` or `~~~jsx` | `jsx` | Component JSX expression |
401
- | `~~~prompt prompt` | `prompt` | TTI/TTV generation prompt |
402
- | `~~~script script` or `~~~script` | `script` | Narration text on audio nodes |
403
-
404
427
  ### `rhythm`
405
428
 
406
429
  When: beat-synced audio (music drops, music-reactive reveals). Required: `src`, `spots`, `children`.
@@ -414,15 +437,6 @@ Each child is assigned to a beat slot: child[i] starts at `spots[i]`, ends at `s
414
437
  - image src:flash3.jpg
415
438
  ```
416
439
 
417
- ### `effect`
418
-
419
- When: CSS keyframe animation wrapper. Required: `children`. `duration` falls back to children max.
420
-
421
- `- effect animation:fadeIn duration:2` then indented children.
422
-
423
- Built-in `animation` values:
424
- `fadeIn fadeOut fadeInDown fadeInUp fadeInLeft fadeInRight fadeOutDown fadeOutUp fadeOutLeft fadeOutRight slideInDown slideInUp slideInLeft slideInRight slideOutDown slideOutUp slideOutLeft slideOutRight zoomIn zoomOut zoomInDown zoomInUp zoomInLeft zoomInRight bounce bounceIn rotateIn rotateOut rotateInDownLeft rotateInDownRight rotateInUpLeft rotateInUpRight flipInX flipInY pulse flash heartBeat rubberBand shakeX shakeY swing tada wobble jello rollIn rollOut jackInTheBox lightSpeedIn lightSpeedOut`
425
-
426
440
  ### `map`
427
441
 
428
442
  When: animated route. Required: `duration`, `waypoints`.
@@ -495,19 +509,6 @@ Any valid JavaScript expression — assign values, increment counters, toggle bo
495
509
 
496
510
  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.
497
511
 
498
- ### JSON form
499
-
500
- In the compiled stream tree, events are represented as an `on` field on any node:
501
-
502
- ```json
503
- {
504
- "type": "audio",
505
- "src": "narration.mp3",
506
- "duration": 3,
507
- "on": { "when": "start", "state": "slide1.current=0" }
508
- }
509
- ```
510
-
511
512
  ## Variants (Language / Platform / Any Override)
512
513
 
513
514
  Produce different versions of the same video from a single markdown file
@@ -621,12 +622,10 @@ npx markcut render courseware.md --variant zh --output zh-video.mp4
621
622
 
622
623
  ### Combined variants
623
624
 
624
- Multiple `--variant` flags are applied in order. Each subsequent variant
625
- can override values set by a previous one. The directory is named with
626
- all variants joined by `-`:
625
+ `--variant` value support multiple variants joined by `-` to merge multiple override sections. For example, `--variant zh-tiktok` merges the `# zh` and `# tiktok` sections into the base config.
627
626
 
628
627
  ```bash
629
- npx markcut preview courseware.md --variant zh --variant tiktok
628
+ npx markcut preview courseware.md --variant zh-tiktok
630
629
  # Uses .markcut/courseware/zh-tiktok/ for variant artifacts
631
630
  ```
632
631
 
@@ -636,37 +635,6 @@ When a variant section provides a root-level key (e.g., `tts`), it merges
636
635
  into the base config. Scene-level `tts` overrides root-level `tts`,
637
636
  and the `ttsCli` option (if provided) overrides both.
638
637
 
639
- ### How overrides are resolved
640
-
641
- 1. The parser extracts `# video` as the base root and each `# <name>` as a
642
- variant config map.
643
- 2. When `--variant zh` is used, the `# zh` section's root keys are merged
644
- into the base root (e.g., `tts` is replaced).
645
- 3. `resolveVariantOverrides` walks the entire tree: for each node,
646
- variant-prefixed keys (like `zh-src`) replace their unprefixed
647
- counterparts. Bare variant keys (like `zh`) replace the node's
648
- primary content key (see table above).
649
- 4. All `<variant>-*` keys and bare variant keys are stripped from the
650
- output — the compiled tree contains only the resolved values.
651
- 5. The pipeline then runs TTS/STT/media generation as usual with the
652
- overridden config and content.
653
-
654
- ### Directory layout for variant artifacts
655
-
656
- ```
657
- .markcut/
658
- ├── generated/ ← shared, content-addressed
659
- │ ├── tts/ ← TTS audio (same script → same file)
660
- │ ├── media/ ← TTI/TTV images/videos
661
- │ └── includes/ ← compiled sub-video JSON
662
- ├── courseware/ ← per-source-file
663
- │ ├── components/ ← component bundles (shared)
664
- │ ├── compiled.json ← default (en) compiled output
665
- │ └── zh/
666
- │ ├── compiled.json ← zh variant compiled output
667
- │ └── subtitles.vtt ← zh variant subtitles
668
- ```
669
-
670
638
  TTS audio files are content-addressed (hash of `script + CLI`), so
671
639
  identical scripts across variants produce the same file. The merged
672
640
  `subtitles.vtt` is per-variant because script content and timing offsets
@@ -680,16 +648,6 @@ differ.
680
648
  4. `transitionTime` subtracted between `transitionSeries` items.
681
649
  5. Containers derive duration from children (parallel=max, series=sum, ts=sum−overlap).
682
650
 
683
- ## Generation Workflow
684
-
685
- 1. Root: `# video` + `width:<n> height:<n> fps:<n> layout:<mode>` on the next line.
686
- 2. Frontmatter (optional): `---` block for root attrs + tts/stt pipeline config + `stylesheet`.
687
- 3. Component registrations: `` ~~~js imports `` block for external components.
688
- 4. Scenes via `##` with `layout:` metadata on the line below.
689
- 5. Leaves as `- type key:value ...` bullets indented under scenes.
690
- 6. Long values (JSX, prompts, scripts) use indented `~~~<lang> <propName>` code fences.
691
- 7. Verify each leaf has resolvable `duration`.
692
-
693
651
  ## Tween Animation
694
652
 
695
653
  Animate numeric props over time by using `tween(from?, to, easing?)` expressions inside JSX usage expressions. Tweens resolve at render time using Remotion's `interpolate()`.
@@ -725,30 +683,7 @@ tween(#000, #FFF) — color interpolation
725
683
  - `tween()` uses Remotion's `interpolate()` with the component's action duration.
726
684
  - At frame 0, `tween(from, to)` returns `from`.
727
685
 
728
- ## Self-Check
729
-
730
- - [ ] Root has `width`, `height`, `fps`, `layout`.
731
- - [ ] Every scene has ≥1 child or `script`.
732
- - [ ] All values use explicit `key:value` syntax (no bare tokens).
733
- - [ ] `start` only used inside `parallel` containers.
734
- - [ ] No `src` on component nodes (use `jsx:` instead).
735
- - [ ] Component registrations use `` ~~~js imports `` block — the ONLY supported method.
736
- - [ ] Every component `jsx:` references a name registered in `` ~~~js imports ``.
737
- - [ ] Every `jsx:` on a component node is a usage expression (JSX tag), not a definition.
738
- - [ ] Inline component definitions go inside `` ~~~js imports `` as `export function Name(...) { ... }`.
739
- - [ ] Scene names are single tokens (no spaces) — use `title:"..."` for multi-word titles.
740
- - [ ] Code fence properties are indented under their parent bullet.
741
- - [ ] Event targets (`id:name` on component) match the `id` used in `on:(when, id.prop=value)` expressions.
742
-
743
- ## Validation with CLI
744
-
745
- Use `markcut verify` to parse and validate a descriptive markdown file without rendering:
746
-
747
- ```bash
748
- markcut verify courseware.md
749
- ```
750
-
751
- # common used components
686
+ # Common used components
752
687
  - `react-markdown` — render markdown content, use plugins to extend functionality
753
688
  - `remark-gfm` — support GitHub Flavored Markdown (tables, strikethrough, task lists)
754
689
  - `remark-toc` — generate table of contents
@@ -760,7 +695,13 @@ markcut verify courseware.md
760
695
  - `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
761
696
  - `@remotion/starburst` — render starburst animations
762
697
 
763
- # best practices
698
+ # Best practices
699
+ - Markcut engine will automatically determine the duration of background streams, video, script audio. Don't set duration for them if no special requirements (e.g., speed up/down on purpose).
700
+ - global streams (BGM, Logo, Lip-sync video,...) should be set on root level, and set `isBackground:true` to let it loop to fill the whole video duration.
701
+ - Set `isBackground:true` for static vision content, such as image, when audio is playing
702
+ - adjust audio start time with `start` property to avoid audio cut when transition effect is applied on vision scene
703
+
704
+
764
705
  ~~~ example - avoid audio cut
765
706
  - parallel
766
707
  - image|video isBackground:true
@@ -7,9 +7,8 @@
7
7
  * (TTS/STT/durations)
8
8
  */
9
9
  import { execSync, spawn } from "node:child_process";
10
- import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
11
- import { createHash } from "node:crypto";
12
- import { resolve, dirname, join, extname } from "node:path";
10
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
11
+ import { resolve, dirname, join } from "node:path";
13
12
  import { fileURLToPath } from "node:url";
14
13
  import {
15
14
  DEFAULT_TTS_CLI, DEFAULT_STT_CLI, DEFAULT_TTI_CLI, DEFAULT_TTV_CLI,
@@ -96,62 +95,10 @@ Commands:
96
95
  * Use --verbose to see every frame line (original behavior).
97
96
  */
98
97
 
99
- /**
100
- * Resolve a potentially relative source path against baseDir, skipping
101
- * absolute paths, URLs, and data URIs.
102
- */
103
- function resolveAssetPath(src, baseDir) {
104
- if (!src || typeof src !== "string") return null;
105
- // Already absolute on disk
106
- if (src.startsWith("/")) return src;
107
- // URL or data URI — can't stage locally
108
- if (/^(https?:|data:|blob:)/.test(src)) return null;
109
- // Relative path: resolve against the input file's directory
110
- const abs = resolve(baseDir, src);
111
- if (existsSync(abs)) return abs;
112
- return null;
113
- }
114
-
115
- /**
116
- * Stage all local src files (absolute and relative) into
117
- * ROOT/public/.render-assets/ so `npx remotion render` (publicDir = ROOT/public)
118
- * can serve them. Relative paths are resolved against `baseDir` (the input
119
- * file's directory). The preview server handles these paths via multi-root
120
- * serving; the render CLI needs them staged into the one public dir Remotion
121
- * knows about.
122
- */
123
- function stageLocalAssets(tree, baseDir) {
124
- const assetsDir = join(ROOT, "public", ".render-assets");
125
- const seen = new Map();
126
- const walk = (node) => {
127
- if (!node || typeof node !== "object") return;
128
- if (typeof node.src === "string") {
129
- const absPath = resolveAssetPath(node.src, baseDir);
130
- if (absPath) {
131
- let staged = seen.get(absPath);
132
- if (!staged) {
133
- const name = createHash("md5").update(absPath).digest("hex").slice(0, 12) + extname(absPath);
134
- mkdirSync(assetsDir, { recursive: true });
135
- copyFileSync(absPath, join(assetsDir, name));
136
- staged = `.render-assets/${name}`;
137
- seen.set(absPath, staged);
138
- }
139
- node.src = staged;
140
- }
141
- }
142
- for (const value of Object.values(node)) {
143
- if (value && typeof value === "object") walk(value);
144
- }
145
- };
146
- walk(tree);
147
- return tree;
148
- }
149
-
150
- function renderOne(streamTree, outputPath, verbose, baseDir) {
151
- const adapted = stageLocalAssets(JSON.parse(JSON.stringify(streamTree)), baseDir);
98
+ function renderOne(streamTree, outputPath, verbose) {
152
99
  const tmpProps = join(ROOT, ".tmp", "render-stream.json");
153
100
  mkdirSync(dirname(tmpProps), { recursive: true });
154
- writeFileSync(tmpProps, JSON.stringify({ root: adapted }));
101
+ writeFileSync(tmpProps, JSON.stringify({ root: streamTree }));
155
102
 
156
103
  mkdirSync(dirname(outputPath), { recursive: true });
157
104
 
@@ -409,19 +356,18 @@ edit=${DEFAULT_EDIT_CLI}`);
409
356
  const { bundleFromEntries } = await import("../player/bundler.mjs");
410
357
  const entries = parseImportsBlock(rawSource);
411
358
  const extraSpecs = extractDependencySpecs(rawSource);
412
- const bundleDir = join(ROOT, "public", ".render-assets");
359
+ const bundleDir = join(ROOT, ".tmp", "component-bundle");
413
360
  mkdirSync(bundleDir, { recursive: true });
414
361
  const bundle = await bundleFromEntries(entries, extraSpecs, rawSource, bundleDir);
415
362
  if (bundle.url) {
416
- streamTree.imports = ".render-assets/" + bundle.url.split("/").pop();
363
+ streamTree.imports = join(bundleDir, bundle.url.split("/").pop());
417
364
  console.log(` \u2705 components \u2192 ${bundle.exports.join(", ")}`);
418
365
  }
419
366
  }
420
367
  }
421
368
 
422
369
  const output = args.output ? resolve(args.output) : join(ROOT, "out", "video.mp4");
423
- const fileDir = dirname(resolve(args.file));
424
- await renderOne(streamTree, output, args.verbose, fileDir);
370
+ await renderOne(streamTree, output, args.verbose);
425
371
 
426
372
  console.log("\n✅ Render complete.");
427
373
  process.exit(0);