@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 +2 -2
- package/README.md +0 -3
- package/package.json +1 -1
- package/skills/markcut/SKILL.md +1 -0
- package/skills/markcut/docs/markdown-descriptive.md +262 -321
- package/src/render/cli.mjs +7 -61
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**:
|
|
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
|
|
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
package/skills/markcut/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
50
|
+
### 3. Leaf Nodes — Bullet Syntax
|
|
4
51
|
|
|
5
|
-
|
|
52
|
+
Every leaf (media, component, rhythm, map, effect, include) is a **markdown bullet** (`- `) containing space-separated tokens:
|
|
6
53
|
|
|
7
|
-
|
|
54
|
+
```
|
|
55
|
+
- <typeToken> [key:value ...] [bareFlag ...]
|
|
56
|
+
```
|
|
8
57
|
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
|
|
63
|
+
#### Type Token Reference
|
|
18
64
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
###
|
|
233
|
+
### 11. Frontmatter (Metadata Only)
|
|
51
234
|
|
|
52
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
273
|
-
|
|
274
|
-
Ray:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
package/src/render/cli.mjs
CHANGED
|
@@ -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
|
|
11
|
-
import {
|
|
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:
|
|
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, "
|
|
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 =
|
|
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
|
-
|
|
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);
|