@lalalic/markcut 2.2.4 → 2.2.6
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/package.json +1 -1
- package/skills/markcut/SKILL.md +8 -0
- package/skills/markcut/docs/markdown-descriptive.md +40 -36
- package/skills/markcut/docs/templates/courseware/TEMPLATE.md +2 -3
- package/skills/markcut/docs/templates/deep-dive/TEMPLATE.md +3 -4
- package/skills/markcut/docs/templates/illustrated-book/TEMPLATE.md +2 -6
- package/skills/markcut/docs/templates/short-film/TEMPLATE.md +3 -5
- package/skills/markcut/docs/templates/short-video/TEMPLATE.md +2 -3
- package/skills/markcut/docs/templates/vlog/TEMPLATE.md +25 -81
- package/src/config.mjs +3 -1
- package/src/descriptive/compiler.ts +5 -0
- package/src/descriptive/resolve.test.ts +6 -0
- package/src/descriptive/resolve.ts +19 -6
- package/src/render/cli-tools.ts +20 -6
- package/src/schema/index.ts +1 -0
- package/skills/markcut/docs/json-descriptive.md +0 -539
package/package.json
CHANGED
package/skills/markcut/SKILL.md
CHANGED
|
@@ -28,6 +28,14 @@ see [docs/markdown-descriptive.md](docs/markdown-descriptive.md) for full detail
|
|
|
28
28
|
- Templates in ``docs/templates/`` provide ready-to-use video structures for `orchestrator` agents, use them as starting point to create your own videos.
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
### Viral Story requires
|
|
32
|
+
- **Hooks** : why should the viewer watch this?
|
|
33
|
+
- **Conflict** : what challenges or obstacles do the characters face?
|
|
34
|
+
- **Resolution** : how are the conflicts resolved?
|
|
35
|
+
- **Emotion** : what feelings are evoked in the viewer?
|
|
36
|
+
- **Call to action** : what should the viewer do next?
|
|
37
|
+
- **Open ending** : does the story leave room for interpretation or continuation?
|
|
38
|
+
|
|
31
39
|
## 3. CLI
|
|
32
40
|
|
|
33
41
|
```bash
|
|
@@ -284,7 +284,7 @@ voices:{"Ray":"--voice en-US-GuyNeural","Alice":"--voice en-US-JennyNeural"}
|
|
|
284
284
|
Alice: I'm working on something exciting"
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
Since values are raw CLI flags, they naturally support voice cloning, rate, pitch
|
|
287
|
+
Since values are raw CLI flags, they naturally support voice cloning, rate, pitch:
|
|
288
288
|
|
|
289
289
|
```md
|
|
290
290
|
voices:{"Ray":"--voice en-US-GuyNeural --rate +20%","Clone":"--voice clone-xxx --pitch +5Hz"}
|
|
@@ -757,45 +757,49 @@ markcut verify courseware.md
|
|
|
757
757
|
- `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
|
|
758
758
|
- `@remotion/starburst` — render starburst animations
|
|
759
759
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
height: 1920
|
|
766
|
-
fps: 30
|
|
767
|
-
---
|
|
768
|
-
# video
|
|
769
|
-
layout:series
|
|
770
|
-
~~~js imports
|
|
771
|
-
export { StatCounter } from "stat-counter"
|
|
772
|
-
export { Logo } from "github:myorg/design-system#Logo"
|
|
773
|
-
|
|
774
|
-
export function Greeting({ name }) {
|
|
775
|
-
return <div style={{color: '#fff', fontSize: 28, textAlign: 'center'}}>Hello {name}!</div>
|
|
776
|
-
}
|
|
760
|
+
# best practices
|
|
761
|
+
~~~ example - avoid audio cut
|
|
762
|
+
- parallel
|
|
763
|
+
- image|video background:true
|
|
764
|
+
- audio|script
|
|
777
765
|
~~~
|
|
778
766
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
-
|
|
782
|
-
-
|
|
767
|
+
~~~ example - global streams
|
|
768
|
+
#video
|
|
769
|
+
- audio background:true loop:true src:bgm.mp3
|
|
770
|
+
- component background:true loop:true jsx:"<Logo />" style:"position:fixed;top:10px;left:10px;width:100px;height:100px;"
|
|
771
|
+
- parallel title:"lip sync" style:"position:fixed;bottom:100px;right:100px;width:100px;height:100px;"
|
|
772
|
+
- video src:background.mp4 loop:true background:true
|
|
773
|
+
- video src:lip_sync.mp4
|
|
774
|
+
~~~
|
|
783
775
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
- video src:clips/fire.mp4 startFrom:1 endAt:4
|
|
776
|
+
~~~ example - styling VTT captions
|
|
777
|
+
```vtt
|
|
778
|
+
000:00:00.000 --> 00:00:05.000
|
|
779
|
+
It's a <span style="color:#ff6b6b;font-weight:bold">Bear</span>.
|
|
789
780
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
- component duration:2 jsx:"<StatCounter value={42} label='S-mores' />"
|
|
781
|
+
000:00:05.000 --> 00:00:10.000
|
|
782
|
+
And now it's <span class="emoji-dead">dead</span>.
|
|
793
783
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
784
|
+
000:00:10.000 --> 00:00:15.000
|
|
785
|
+
<span class="bong">Bong!</span>
|
|
786
|
+
```
|
|
797
787
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
-
|
|
788
|
+
#root
|
|
789
|
+
```css stylesheet
|
|
790
|
+
.emoji-dead {
|
|
791
|
+
font-family: "Funny Emoji", sans-serif;
|
|
792
|
+
font-size: 48px;
|
|
793
|
+
}
|
|
794
|
+
.bong {
|
|
795
|
+
color: #ff6b6b;
|
|
796
|
+
font-weight: bold;
|
|
797
|
+
position: absolute;
|
|
798
|
+
top: 50%;
|
|
799
|
+
left: 50%;
|
|
800
|
+
transform: translate(-50%, -50%);
|
|
801
|
+
animation: bong 1s ease-in-out infinite;
|
|
802
|
+
}
|
|
801
803
|
```
|
|
804
|
+
~~~
|
|
805
|
+
|
|
@@ -39,7 +39,7 @@ Layout of this template package:
|
|
|
39
39
|
|
|
40
40
|
```
|
|
41
41
|
# video ← root: width:1920 height:1080 fps:30 layout:series
|
|
42
|
-
│ subtitle:{fontSize:"20px"}
|
|
42
|
+
│ subtitle:{fontSize:"20px"}
|
|
43
43
|
├── ## Hook ← 3–5s attention grabber, fixed duration
|
|
44
44
|
├── ## Slides ← layout:transitionSeries transition:fade(0.5)
|
|
45
45
|
│ ├── ### TitleSlide ← transitionSeries, bullet-reveal (1 beat)
|
|
@@ -199,10 +199,9 @@ See markcut docs for available effect types: `fadeIn`, `zoomIn`, `bounceIn`, `sl
|
|
|
199
199
|
|
|
200
200
|
Guide TTS pronunciation for more natural speech using SSML-style cues in the script text:
|
|
201
201
|
- **Pause**: use `—` (em dash) or `...` for natural breath pauses
|
|
202
|
-
- **Emphasis**: capitalize key terms or use `*asterisks*` (edge-tts interprets these as emphasis)
|
|
203
202
|
- **Rate**: set `tts.rate:+10%` on the root config line for faster pacing, or `tts:{"--rate +10%"}` per scene
|
|
204
203
|
- **Phonetic spelling**: for jargon ("Snyk" = "snik", "Grafana" = "gruh-fah-nuh"), add parenthetical pronunciation on first use: `Snyk (pronounced "snik")`
|
|
205
|
-
- **Language mix**: for bilingual content, switch
|
|
204
|
+
- **Language mix**: for bilingual content, switch tts voice per language using the `# <lang>` variant block
|
|
206
205
|
|
|
207
206
|
## 3. Authoring rules — the professional bar
|
|
208
207
|
|
|
@@ -34,8 +34,7 @@ flowchart LR
|
|
|
34
34
|
## 0. Prerequisites
|
|
35
35
|
|
|
36
36
|
- `npx @lalalic/markcut` runnable
|
|
37
|
-
-
|
|
38
|
-
- `ffmpeg`/`ffprobe` on PATH
|
|
37
|
+
- `ffmpeg`/`ffprobe/exiftool` on PATH
|
|
39
38
|
- Web search capability (for research phase)
|
|
40
39
|
- ITT/TTV CLI (for `src:auto` visuals) — optional but recommended
|
|
41
40
|
- For reviewer: image-understanding capability, STT CLI
|
|
@@ -50,7 +49,7 @@ flowchart LR
|
|
|
50
49
|
| Tone | no | `analytical` | `analytical` (neutral, evidence-driven), `opinionated` (strong thesis, persuasive), `philosophical` (open-ended, reflective), `investigative` (detective-like, reveals) |
|
|
51
50
|
| Target duration | no | 8 min | 3–20 min. Longer formats need deeper scene breakdown |
|
|
52
51
|
| Depth | no | `moderate` | `overview` (surface-level, broad), `moderate` (some depth, 3-5 angles), `deep` (thorough, 5-10 angles, academic-level) |
|
|
53
|
-
| Voice | no |
|
|
52
|
+
| Voice | no | | mlx-audio voice per language |
|
|
54
53
|
| BGM style | no | `ambient` | background music mood. Optional — deep dives can work without BGM |
|
|
55
54
|
|
|
56
55
|
**Core rule: Every factual claim must be supported by a source.** If you cannot find a source for a claim, either qualify it ("some argue that...", "it's believed that...") or remove it. Cite sources on screen (see §3).
|
|
@@ -63,7 +62,7 @@ A deep dive is not a lecture and not a highlight reel — it's a **narrative jou
|
|
|
63
62
|
|
|
64
63
|
```
|
|
65
64
|
# video ← root: width:1920 height:1080 fps:30 layout:series
|
|
66
|
-
│ subtitle:{fontSize:"24px"}
|
|
65
|
+
│ subtitle:{fontSize:"24px"}
|
|
67
66
|
│ transition:fade(0.5)
|
|
68
67
|
├── ## Hook ← 15–30s. The question, the contradiction, the mystery
|
|
69
68
|
├── ## Context ← 30–60s. Background the audience needs
|
|
@@ -34,10 +34,7 @@ flowchart LR
|
|
|
34
34
|
## 0. Prerequisites
|
|
35
35
|
|
|
36
36
|
- `npx @lalalic/markcut` runnable
|
|
37
|
-
-
|
|
38
|
-
- ITT CLI (for `src:auto` illustration generation) — **strongly recommended**, illustrations are the core of this format
|
|
39
|
-
- `ffmpeg`/`ffprobe` on PATH
|
|
40
|
-
- For reviewer: image-understanding capability, STT CLI
|
|
37
|
+
- `ffmpeg`/`ffprobe`/`exiftool` on PATH
|
|
41
38
|
|
|
42
39
|
## 1. Inputs — collect before starting
|
|
43
40
|
|
|
@@ -49,7 +46,7 @@ flowchart LR
|
|
|
49
46
|
| Art style | **yes** | — | the illustration style for ALL pages. Must be consistent. Examples: "watercolor, soft pastels, children's book illustration style", "ink wash painting, traditional Chinese style", "digital painting, studio Ghibli inspired, warm lighting", "woodcut print style, high contrast black and white", "vintage botanical illustration, sepia tones". This single style string is injected into every TTI prompt. |
|
|
50
47
|
| Language | no | en | narration and on-screen text language |
|
|
51
48
|
| Reading pace | no | `moderate` | `slow` (3-4s per line, ~10s per spread), `moderate` (2-3s per line, ~7s per spread), `fast` (1.5-2s per line, ~5s per spread) |
|
|
52
|
-
| Voice | no |
|
|
49
|
+
| Voice | no | | mlx-audio voice. Prefer warm, expressive voices for storytelling |
|
|
53
50
|
| BGM mood | no | `ambient` | optional background music. "soft piano", "ambient nature", "lullaby", or "none" |
|
|
54
51
|
| Pages | no | auto | target number of spreads/pages. Auto-calculated from text if not set |
|
|
55
52
|
|
|
@@ -62,7 +59,6 @@ flowchart LR
|
|
|
62
59
|
```
|
|
63
60
|
# video ← root: width:1920 height:1080 fps:30 layout:series
|
|
64
61
|
│ subtitle:{fontSize:"40px",fontFamily:"Georgia,serif"}
|
|
65
|
-
│ tts:"<edge-tts CLI>"
|
|
66
62
|
│ transition:fade(0.5)
|
|
67
63
|
├── - audio isBackground:true src:bgm.mp3 volume:0.08 (optional)
|
|
68
64
|
│
|
|
@@ -33,9 +33,7 @@ flowchart LR
|
|
|
33
33
|
## 0. Prerequisites
|
|
34
34
|
|
|
35
35
|
- `npx @lalalic/markcut` runnable
|
|
36
|
-
-
|
|
37
|
-
- ITT CLI (for `src:auto` scene generation) — **critical**, every scene is AI-generated
|
|
38
|
-
- `ffmpeg`/`ffprobe` on PATH
|
|
36
|
+
- `ffmpeg`/`ffprobe`/`exiftool` on PATH
|
|
39
37
|
- For reviewer: image-understanding capability, STT CLI
|
|
40
38
|
|
|
41
39
|
## 1. Inputs — collect before starting
|
|
@@ -48,7 +46,7 @@ flowchart LR
|
|
|
48
46
|
| Tone | no | `serious` | `serious` (somber, weighty), `noir` (dark, mysterious), `melancholic` (sad, reflective), `hopeful` (bittersweet, uplifting) |
|
|
49
47
|
| Language | no | en | narration and on-screen text language |
|
|
50
48
|
| Target duration | no | 7 min | 5–10 min |
|
|
51
|
-
| Voice(s) | no |
|
|
49
|
+
| Voice(s) | no | | mlx-audio voice(s). For multiple characters, provide a list: {narrator, character1, character2} |
|
|
52
50
|
| Music mood | no | `cinematic` | score style: cinematic, ambient, noir jazz, minimal piano, tense. BGM is **recommended but not mandatory** — silence is a valid cinematic choice |
|
|
53
51
|
| Aspect ratio | no | `16:9` | `16:9` (standard), `2.35:1` (cinemascope — add black bars in stylesheet), `4:3` (vintage) |
|
|
54
52
|
|
|
@@ -60,7 +58,7 @@ flowchart LR
|
|
|
60
58
|
|
|
61
59
|
```
|
|
62
60
|
# video ← root: width:1920 height:1080 fps:30 layout:series
|
|
63
|
-
│
|
|
61
|
+
│
|
|
64
62
|
│ transition:fade(0.5)
|
|
65
63
|
├── - audio isBackground:true foreground:true src:score.mp3 volume:0.12
|
|
66
64
|
├── - audio isBackground:true src:ambient.mp3 volume:0.05 (optional, diegetic ambience)
|
|
@@ -34,7 +34,6 @@ flowchart LR
|
|
|
34
34
|
## 0. Prerequisites
|
|
35
35
|
|
|
36
36
|
- `npx @lalalic/markcut` runnable
|
|
37
|
-
- TTS CLI (default: `edge-tts`)
|
|
38
37
|
- `ffmpeg`/`ffprobe` on PATH
|
|
39
38
|
- ITT/TTV CLI (for `src:auto` background generation) — optional but recommended
|
|
40
39
|
- For reviewer: image-understanding capability, STT CLI
|
|
@@ -49,7 +48,7 @@ flowchart LR
|
|
|
49
48
|
| Language | no | en | narration language |
|
|
50
49
|
| Style | no | `cinematic` | `cinematic` (dramatic), `energetic` (fast cuts, bright), `minimal` (clean, slow), `trendy` (meme-aware, casual) |
|
|
51
50
|
| Target duration | no | 30s | 15–60s |
|
|
52
|
-
| Voice | no |
|
|
51
|
+
| Voice | no | | mlx-audio voice |
|
|
53
52
|
| BGM mood | no | `upbeat` | mood for background music. BGM is **mandatory**. |
|
|
54
53
|
|
|
55
54
|
**Rule:** For explainer/quote/storytime formats, generate all visuals via `src:auto prompt:"..."` — describe vivid, cinematic scenes. For showcase/highlight formats, use the provided source media and let the narration describe it.
|
|
@@ -61,7 +60,7 @@ flowchart LR
|
|
|
61
60
|
```
|
|
62
61
|
# video ← root: width:1080 height:1920 fps:30 layout:series
|
|
63
62
|
│ subtitle:{fontSize:"56px",type:"Typewriter",fontFamily:"Arial Black"}
|
|
64
|
-
│
|
|
63
|
+
│
|
|
65
64
|
│ transition:fade(0.3)
|
|
66
65
|
├── - audio isBackground:true foreground:true src:bgm.mp3 volume:0.1
|
|
67
66
|
│
|
|
@@ -31,7 +31,7 @@ Follow this file top to bottom. Read the markcut skill (`SKILL.md` → `docs/mar
|
|
|
31
31
|
| Style | no | `daily` | `daily` (casual), `lyrical` (cinematic), `humorous` (funny) |
|
|
32
32
|
| Target duration | no | 60s | 15–120s for short vlogs |
|
|
33
33
|
| Profile | **parse from** | context | user's personality, region, family members, voice ref, life context. Read this file at the start — it's the authoritative source for who the creator is. The creator's name, region, family, and regular activities feed into narration naturally ("my daughter", "drove up from Ottawa", "my third camping trip this year"). |
|
|
34
|
-
| Voice | no |
|
|
34
|
+
| Voice | no | | mlx-audio voice. If profile has a voice reference URL, use TTS voice cloning instead |
|
|
35
35
|
| BGM style | no | `ambient` | mood/genre for background music. BGM is **mandatory** — see §2 for root-level audio node |
|
|
36
36
|
| Vlog history | **read from** | `vlog_history.json` or folder | JSON array of past vlogs in the project. Used for story continuity ("last time I was here...", "my third camping trip this year"). If the file doesn't exist, start fresh. See §3 for history rules. |
|
|
37
37
|
|
|
@@ -42,8 +42,15 @@ Follow this file top to bottom. Read the markcut skill (`SKILL.md` → `docs/mar
|
|
|
42
42
|
### Overview
|
|
43
43
|
|
|
44
44
|
```
|
|
45
|
+
---
|
|
46
|
+
storyoutline: # it's your logic to design story.
|
|
47
|
+
hook:
|
|
48
|
+
confliction:
|
|
49
|
+
emotion:
|
|
50
|
+
open-ending:
|
|
51
|
+
---
|
|
45
52
|
# video ← root: width:1920 height:1080 fps:30 layout:series
|
|
46
|
-
│
|
|
53
|
+
│
|
|
47
54
|
├── ## Route ← (only if GPS data available)
|
|
48
55
|
│ layout:parallel
|
|
49
56
|
│ - map waypoints:[...]
|
|
@@ -77,9 +84,11 @@ width:1920 height:1080 fps:30 layout:series
|
|
|
77
84
|
- Volume: 0.10–0.20 for ambient BGM; 0.05–0.10 for lyrical music.
|
|
78
85
|
- Source BGM from royalty-free libraries (see `prompts/bgm-select.md`).
|
|
79
86
|
|
|
80
|
-
### Core design: one scene per story beat
|
|
87
|
+
### Core design: one scene per story beat
|
|
88
|
+
- determine **Hook**, **Confliction**, **Emotion**, **Open-ending**
|
|
89
|
+
- the vlog's story arc: hook → chronological story beats → close.
|
|
81
90
|
|
|
82
|
-
Each scene is a **story beat** — a single moment or idea that stands on its own. Inside a scene, `layout:parallel` means all media and the subtitle narration play simultaneously.
|
|
91
|
+
- Each scene is a **story beat** — a single moment or idea that stands on its own. Inside a scene, `layout:parallel` means all media and the subtitle narration play simultaneously.
|
|
83
92
|
|
|
84
93
|
```
|
|
85
94
|
## Into the Woods
|
|
@@ -90,7 +99,7 @@ layout:parallel
|
|
|
90
99
|
```
|
|
91
100
|
|
|
92
101
|
**Rules:**
|
|
93
|
-
|
|
102
|
+
- determine story arc first, then group clips into scenes. Each scene is a single story beat.
|
|
94
103
|
- **Scene count**: 5–12 scenes. Each 3–20 seconds. Total duration matches target ±15%.
|
|
95
104
|
- **Hook → Core → Vibe → Close arc**: The first scene hooks the viewer (best visual + intriguing line). Middle scenes are chronological story beats. The final scene provides emotional/humorous closure.
|
|
96
105
|
- **Transition between scenes**: The root `layout:series` plays scenes sequentially. For smooth transitions, add `transition:fade(0.5)` to the root config line.
|
|
@@ -100,19 +109,6 @@ layout:parallel
|
|
|
100
109
|
- **BGM is mandatory**: Always add an `- audio isBackground:true foreground:true` at root level (see BGM section above).
|
|
101
110
|
- **No bullet-reveal** — vlogs are linear media, not slides. No `current`/`on:` events needed.
|
|
102
111
|
|
|
103
|
-
### Scene media node style
|
|
104
|
-
|
|
105
|
-
For images:
|
|
106
|
-
```markdown
|
|
107
|
-
- image src:<relative-path> duration:<s> start:<s>
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
For video clips (trimmed):
|
|
111
|
-
```markdown
|
|
112
|
-
- video src:<relative-path> startFrom:<s> endAt:<s> start:<s>
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
The `src` path is relative to the media folder. During assembly, media is symlinked/copied into the markcut project so `npx markcut render` can serve it.
|
|
116
112
|
|
|
117
113
|
## 2.b Production polish — route sync, overlays, effects
|
|
118
114
|
|
|
@@ -154,14 +150,14 @@ Use per-scene map overlays when GPS data is available for that specific clip. Us
|
|
|
154
150
|
|
|
155
151
|
## 3. Authoring rules — the professional bar
|
|
156
152
|
|
|
157
|
-
### Filter rules (
|
|
153
|
+
### Filter rules (use `prompts/group-clips.md`)
|
|
158
154
|
|
|
159
|
-
- Keep all clips unless:
|
|
155
|
+
- Keep all clips unless: near-duplicate of a better clip, or off-topic.
|
|
160
156
|
- Always keep clips with a positive user label from the interactive labeling step.
|
|
161
157
|
- Max ~20% drops. Never drop below 5 clips total.
|
|
162
158
|
- List every dropped clip with a reason.
|
|
163
159
|
|
|
164
|
-
### Grouping rules (
|
|
160
|
+
### Grouping rules (use `prompts/group-clips.md`)
|
|
165
161
|
|
|
166
162
|
- **Visual continuity rule**: clips sharing the same background/outfit/subject-state form one scene. Exhaust one scene before moving to the next. No jumping A→B→A.
|
|
167
163
|
- 1–4 clips per scene (a single long take >10s may be its own scene).
|
|
@@ -187,7 +183,7 @@ Before writing narration, search for local news, events, or weather for the vlog
|
|
|
187
183
|
- **Local event**: "Turns out the Gatineau Park was running the fall colours festival that weekend."
|
|
188
184
|
- **Seasonal context**: "Full moon that night, so the trail was brighter than usual."
|
|
189
185
|
|
|
190
|
-
Rules
|
|
186
|
+
**Rules:**
|
|
191
187
|
- Only use facts confirmed by at least one search result. Never invent weather/events.
|
|
192
188
|
- Keep it to 1–2 local-context references per vlog. Don't overwhelm the narration.
|
|
193
189
|
- The context should **amplify** the story, not replace it. "Drove up during the fall colours festival so the park was packed" tells us more than just the festival fact.
|
|
@@ -207,7 +203,7 @@ Rules:
|
|
|
207
203
|
- Keep references brief (one line per vlog max). The story should stand alone.
|
|
208
204
|
- Never fabricate a past event. Only what exists in the history file.
|
|
209
205
|
|
|
210
|
-
###
|
|
206
|
+
### User profile
|
|
211
207
|
|
|
212
208
|
Read `~/.pi/agent/user.md` at the start and weave the creator's identity into narration naturally:
|
|
213
209
|
|
|
@@ -215,7 +211,7 @@ Read `~/.pi/agent/user.md` at the start and weave the creator's identity into na
|
|
|
215
211
|
- **Region**: name-drop the city/area ("drove up from Ottawa", "my favourite spot in the Outaouais").
|
|
216
212
|
- **Family**: use real names/roles ("my daughter", "my wife") as shown in the profile and visible in clips.
|
|
217
213
|
- **Regular activities**: if the user hikes regularly, frame this as one of many hikes ("another trail checked off the list").
|
|
218
|
-
- **Voice reference**: if the profile has a voice URL for TTS cloning
|
|
214
|
+
- **Voice reference**: if the profile has a voice URL for TTS cloning.
|
|
219
215
|
|
|
220
216
|
### Title
|
|
221
217
|
|
|
@@ -226,44 +222,6 @@ One title, 3–15 words, poetic/suspenseful/summarizing, social-media friendly.
|
|
|
226
222
|
- Same pattern as courseware: `# <lang>` variant block at file end with per-language TTS voice.
|
|
227
223
|
- Every `script`/`subtitle` node gets a `<lang>:"..."` twin.
|
|
228
224
|
|
|
229
|
-
## 4. Components & styles
|
|
230
|
-
|
|
231
|
-
Vlogs don't need custom JSX components — they use markcut's built-in `image`, `video`, `audio`, `subtitle`, and `map` nodes. However, you may add these optional enhancements:
|
|
232
|
-
|
|
233
|
-
### Optional: NarratorBox component
|
|
234
|
-
|
|
235
|
-
If adding a narrator overlay with name label, include this in the `~~~js imports` block:
|
|
236
|
-
|
|
237
|
-
```jsx
|
|
238
|
-
import { delayRender, continueRender } from 'remotion'
|
|
239
|
-
|
|
240
|
-
export function NarratorBox({ src, name = '', size = 100 }) {
|
|
241
|
-
return (
|
|
242
|
-
<div style={{
|
|
243
|
-
position: 'absolute', bottom: 40, left: 40,
|
|
244
|
-
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6,
|
|
245
|
-
}}>
|
|
246
|
-
<div style={{
|
|
247
|
-
width: size, height: size, borderRadius: '50%', overflow: 'hidden',
|
|
248
|
-
border: '3px solid rgba(255,255,255,.6)',
|
|
249
|
-
boxShadow: '0 0 20px rgba(0,0,0,.4)',
|
|
250
|
-
}}>
|
|
251
|
-
<img src={src} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
252
|
-
</div>
|
|
253
|
-
{name && (
|
|
254
|
-
<span style={{
|
|
255
|
-
fontSize: 14, color: '#fff', textShadow: '0 2px 8px rgba(0,0,0,.6)',
|
|
256
|
-
whiteSpace: 'nowrap', fontWeight: 500,
|
|
257
|
-
}}>{name}</span>
|
|
258
|
-
)}
|
|
259
|
-
</div>
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Stylesheet
|
|
265
|
-
|
|
266
|
-
No stylesheet needed for standard vlogs. The built-in markcut rendering handles video/image filling, subtitle positioning, and audio mixing. Add one only if you need custom overlays.
|
|
267
225
|
|
|
268
226
|
## 5. Workflow
|
|
269
227
|
|
|
@@ -284,7 +242,7 @@ No stylesheet needed for standard vlogs. The built-in markcut rendering handles
|
|
|
284
242
|
}
|
|
285
243
|
]
|
|
286
244
|
```
|
|
287
|
-
If the file doesn't exist,
|
|
245
|
+
If the file doesn't exist, try to extract for project folder.
|
|
288
246
|
|
|
289
247
|
### Phase 1: Prepare media
|
|
290
248
|
|
|
@@ -303,7 +261,7 @@ No stylesheet needed for standard vlogs. The built-in markcut rendering handles
|
|
|
303
261
|
5. **Search local news/events** — Fill `prompts/local-context.md` with the vlog's location, date range, and theme. This searches for relevant weather, events, news, or seasonal information. The result feeds into narration with 1–2 natural references.
|
|
304
262
|
|
|
305
263
|
6. **Group and filter clips** — Fill `prompts/group-clips.md` with clip data from `metadata.json`. This produces:
|
|
306
|
-
-
|
|
264
|
+
- Speed up less-interesting clips vs Dropped clips
|
|
307
265
|
- Visual-continuity scene groups
|
|
308
266
|
- Trim boundaries for video clips
|
|
309
267
|
- GPS waypoints for per-scene map overlays
|
|
@@ -325,16 +283,7 @@ No stylesheet needed for standard vlogs. The built-in markcut rendering handles
|
|
|
325
283
|
|
|
326
284
|
8. **Select BGM** — Choose background music that matches the vlog's mood. Fill `prompts/bgm-select.md` or search using the `audio-sourcing` skill. Download the track and reference it as `src:bgm.mp3` in the root-level audio node.
|
|
327
285
|
|
|
328
|
-
9. **Assemble
|
|
329
|
-
|
|
330
|
-
**Media staging**: Make media accessible to the renderer:
|
|
331
|
-
```
|
|
332
|
-
mkdir -p .markcut/generated/media/
|
|
333
|
-
cp <media-folder>/clips_normalized/*.mp4 .markcut/generated/media/
|
|
334
|
-
cp <media-folder>/*.jpg .markcut/generated/media/
|
|
335
|
-
cp /path/to/bgm.mp3 .markcut/generated/media/bgm.mp3
|
|
336
|
-
```
|
|
337
|
-
Reference as `src:.markcut/generated/media/<file>` in the markdown.
|
|
286
|
+
9. **Assemble .md** — Write the markcut markdown file using §2 grammar. For scenes with GPS data, add per-scene map overlays (see §2.b).
|
|
338
287
|
|
|
339
288
|
10. **Render** — `npx @lalalic/markcut render course.md`. On engine errors: fix and re-render, max 3 attempts per error, then ask the user.
|
|
340
289
|
|
|
@@ -371,12 +320,7 @@ Done only when ALL hold:
|
|
|
371
320
|
- [ ] BGM is **present** (ffprobe confirms audio stream or mixed track) and audible (not silent, not clipping)
|
|
372
321
|
- [ ] BGM ducked under voice — TTS audible above music in ≥90% of spoken segments
|
|
373
322
|
- [ ] local context references (if any) verified against search results — no invented weather/events
|
|
374
|
-
- [ ] history references
|
|
323
|
+
- [ ] history references — no fabricated past events
|
|
375
324
|
- [ ] media `src` paths resolve correctly (no broken links in rendered output)
|
|
376
325
|
- [ ] profile used: narration tone matches creator personality, family names from profile appear correctly
|
|
377
|
-
- [ ] per-scene map overlays (if GPS available) positioned without obstructing main
|
|
378
|
-
|
|
379
|
-
## 7. Reference — worked examples
|
|
380
|
-
|
|
381
|
-
- Golden example: See `tests/fixtures/templates/courseware.md` for the template format.
|
|
382
|
-
- Reference vlog storyboard: The bullx vlog project at `free2/bullx/packages/vlog/storyboard.md` shows a real vlog (birthday camping at Lac Philippe) with 13 chronological scenes, route map, clip trimming, and first-person narration. The project's `.pi/agents/story-writer.md` and `.pi/prompts/vlog-storyboard.md` contain the source of truth for the story-writing methodology documented here.
|
|
326
|
+
- [ ] per-scene map overlays (if GPS available) positioned without obstructing main conten
|
package/src/config.mjs
CHANGED
|
@@ -94,8 +94,10 @@ export const DEFAULT_EDIT_CLI = process.env.MARKCUT_EDIT_CLI || 'npx pi --sessio
|
|
|
94
94
|
|
|
95
95
|
// ── Render-only pipeline CLI templates ─────────────────────────────────────
|
|
96
96
|
// These are specific to the render pipeline (no vision pipeline equivalent).
|
|
97
|
+
// All templates support {input}, {output}, and {seed} placeholders.
|
|
98
|
+
// {seed} is substituted with root.seed (or empty string if not set).
|
|
97
99
|
|
|
98
100
|
export const DEFAULT_TTI_CLI =
|
|
99
101
|
process.env.MARKCUT_TTI_CLI ||
|
|
100
|
-
'uvx --from mflux mflux-generate-flux2 --model flux2-klein-4b --steps 5 --prompt "{input}" --output "{output}"';
|
|
102
|
+
'uvx --from mflux mflux-generate-flux2 --model flux2-klein-4b --steps 5 --prompt "{input}" --output "{output}" --seed {seed}';
|
|
101
103
|
export const DEFAULT_TTV_CLI = process.env.MARKCUT_TTV_CLI || '';
|
|
@@ -180,6 +180,11 @@ export interface DescriptiveRoot {
|
|
|
180
180
|
stt?: string;
|
|
181
181
|
tti?: string;
|
|
182
182
|
ttv?: string;
|
|
183
|
+
/** Global seed for TTI/TTV generation. When the CLI template contains {seed},
|
|
184
|
+
* this value is substituted in to make generation reproducible.
|
|
185
|
+
* All generated media in this video share the same seed, ensuring visual
|
|
186
|
+
* consistency across scenes. Default: no seed (random generation). */
|
|
187
|
+
seed?: number;
|
|
183
188
|
layout?: "series" | "parallel" | "transitionSeries";
|
|
184
189
|
transition?: string;
|
|
185
190
|
transitionTime?: number;
|
|
@@ -164,6 +164,7 @@ describe("resolveGeneratedMedia", () => {
|
|
|
164
164
|
"sunset",
|
|
165
165
|
expect.stringMatching(/\.png$/),
|
|
166
166
|
"gen-img --prompt {input} --out {output}",
|
|
167
|
+
undefined,
|
|
167
168
|
);
|
|
168
169
|
});
|
|
169
170
|
|
|
@@ -186,6 +187,7 @@ describe("resolveGeneratedMedia", () => {
|
|
|
186
187
|
expect.stringMatching(/\.mp4$/),
|
|
187
188
|
"gen-vid --prompt {input} --out {output}",
|
|
188
189
|
expect.any(String), // ttiCmd defaults to DEFAULT_TTI_CLI
|
|
190
|
+
undefined,
|
|
189
191
|
);
|
|
190
192
|
});
|
|
191
193
|
|
|
@@ -235,6 +237,7 @@ describe("resolveGeneratedMedia", () => {
|
|
|
235
237
|
"override test",
|
|
236
238
|
expect.stringMatching(/\.png$/),
|
|
237
239
|
"root-tti {input} --out {output}",
|
|
240
|
+
undefined,
|
|
238
241
|
);
|
|
239
242
|
});
|
|
240
243
|
|
|
@@ -253,6 +256,7 @@ describe("resolveGeneratedMedia", () => {
|
|
|
253
256
|
"test",
|
|
254
257
|
expect.stringMatching(/\.png$/),
|
|
255
258
|
"custom-tti --prompt {input} --out {output}",
|
|
259
|
+
undefined,
|
|
256
260
|
);
|
|
257
261
|
});
|
|
258
262
|
|
|
@@ -642,6 +646,7 @@ describe("resolveGeneratedMedia — TTV additional", () => {
|
|
|
642
646
|
expect.stringMatching(/\.mp4$/),
|
|
643
647
|
"root-ttv {input} --out {output}",
|
|
644
648
|
expect.any(String),
|
|
649
|
+
undefined,
|
|
645
650
|
);
|
|
646
651
|
});
|
|
647
652
|
|
|
@@ -661,6 +666,7 @@ describe("resolveGeneratedMedia — TTV additional", () => {
|
|
|
661
666
|
expect.stringMatching(/\.mp4$/),
|
|
662
667
|
"custom-ttv {input} --out {output}",
|
|
663
668
|
expect.any(String),
|
|
669
|
+
undefined,
|
|
664
670
|
);
|
|
665
671
|
});
|
|
666
672
|
|
|
@@ -678,6 +678,10 @@ export interface ResolveGeneratedMediaOptions {
|
|
|
678
678
|
ttiCli?: string;
|
|
679
679
|
/** Default TTV CLI template (overrides root.ttv) */
|
|
680
680
|
ttvCli?: string;
|
|
681
|
+
/** Global seed for reproducible TTI/TTV generation. Applied when the CLI
|
|
682
|
+
* template contains {seed}. All generated media share this seed for
|
|
683
|
+
* visual consistency across scenes. */
|
|
684
|
+
seed?: number;
|
|
681
685
|
}
|
|
682
686
|
|
|
683
687
|
/**
|
|
@@ -723,9 +727,13 @@ export async function resolveGeneratedMedia(
|
|
|
723
727
|
? (clone.tti ?? options.ttiCli ?? DEFAULT_TTI_CLI)
|
|
724
728
|
: (clone.ttv ?? options.ttvCli ?? DEFAULT_TTV_CLI);
|
|
725
729
|
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
const
|
|
730
|
+
// Use root.seed for reproducible generation (applied when CLI template has {seed}).
|
|
731
|
+
// seed is part of the cache key so different seeds produce different cached outputs.
|
|
732
|
+
const seed = options.seed;
|
|
733
|
+
|
|
734
|
+
// Content-addressed filename: hash of prompt + CLI + type + seed, so identical
|
|
735
|
+
// prompts with different seeds produce distinct cached media.
|
|
736
|
+
const cacheKey = computeCacheKey({ prompt, cli, type, seed });
|
|
729
737
|
const outputPath = join(options.outputDir, `${cacheKey}.${ext}`);
|
|
730
738
|
|
|
731
739
|
const cached = checkCache(cache, `gen:${cacheKey}`, cacheKey);
|
|
@@ -742,8 +750,8 @@ export async function resolveGeneratedMedia(
|
|
|
742
750
|
console.log(` 🔊 ${label}: ${labelText}...`);
|
|
743
751
|
const ttiCmd = clone.tti ?? options.ttiCli ?? DEFAULT_TTI_CLI;
|
|
744
752
|
const result = type === "image"
|
|
745
|
-
? generateTTI(prompt, outputPath, cli)
|
|
746
|
-
: generateTTV(prompt, outputPath, cli, ttiCmd);
|
|
753
|
+
? generateTTI(prompt, outputPath, cli, seed)
|
|
754
|
+
: generateTTV(prompt, outputPath, cli, ttiCmd, seed);
|
|
747
755
|
if (result) {
|
|
748
756
|
node.src = outputPath;
|
|
749
757
|
updateCache(cache, `gen:${cacheKey}`, cacheKey, outputPath);
|
|
@@ -782,6 +790,8 @@ export interface ResolveAllOptions extends ResolveMediaOptions {
|
|
|
782
790
|
ttiCli?: string;
|
|
783
791
|
/** TTV CLI template override (default: pi agent). Overrides root.ttv. */
|
|
784
792
|
ttvCli?: string;
|
|
793
|
+
/** Global seed for reproducible TTI/TTV generation. Inherited by includes. */
|
|
794
|
+
seed?: number;
|
|
785
795
|
/** If set, resolve includes. Directory where pre-compiled include JSON files are stored. */
|
|
786
796
|
includeOutputDir?: string;
|
|
787
797
|
/** Separate output dir for the merged subtitles.vtt.
|
|
@@ -973,12 +983,15 @@ export async function resolveAll(
|
|
|
973
983
|
// (e.g. photo_${width}x${height}.jpg), find the closest existing file.
|
|
974
984
|
result = await resolveMediaSrcs(result, { baseDir: options.baseDir });
|
|
975
985
|
|
|
976
|
-
// Step 3: Generate images/videos from prompts before probing durations
|
|
986
|
+
// Step 3: Generate images/videos from prompts before probing durations.
|
|
987
|
+
// Use root.seed if set; otherwise inherit from options (for includes).
|
|
988
|
+
const generationSeed = result.seed ?? options.seed;
|
|
977
989
|
if (options.mediaOutputDir) {
|
|
978
990
|
result = await resolveGeneratedMedia(result, {
|
|
979
991
|
outputDir: options.mediaOutputDir,
|
|
980
992
|
ttiCli: options.ttiCli,
|
|
981
993
|
ttvCli: options.ttvCli,
|
|
994
|
+
seed: generationSeed,
|
|
982
995
|
});
|
|
983
996
|
}
|
|
984
997
|
|
package/src/render/cli-tools.ts
CHANGED
|
@@ -40,8 +40,13 @@ export { DEFAULT_TTS_CLI, DEFAULT_STT_CLI, DEFAULT_TTI_CLI, DEFAULT_TTV_CLI };
|
|
|
40
40
|
*
|
|
41
41
|
* template: --text '{input}' input: it's "great"
|
|
42
42
|
* → --text 'it'\''s "great"'
|
|
43
|
+
*
|
|
44
|
+
* Placeholders:
|
|
45
|
+
* {input} — user text/path (quote-aware escaped)
|
|
46
|
+
* {output} — output file path (simple escaped)
|
|
47
|
+
* {seed} — integer seed for reproducible generation (or empty string)
|
|
43
48
|
*/
|
|
44
|
-
function substituteCli(template: string, input: string, output: string): string {
|
|
49
|
+
function substituteCli(template: string, input: string, output: string, seed?: number): string {
|
|
45
50
|
// Escape the output value (file paths rarely have quotes, but be safe)
|
|
46
51
|
const safeOutput = output;
|
|
47
52
|
|
|
@@ -63,9 +68,15 @@ function substituteCli(template: string, input: string, output: string): string
|
|
|
63
68
|
safeInput = input.replace(/"/g, '\\"').replace(/'/g, "'\\''");
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
// {seed} is always a simple integer — no escaping needed beyond providing
|
|
72
|
+
// a sensible default. If not set, emit empty string so CLI flags like
|
|
73
|
+
// "--seed {seed}" become "--seed " (harmless for tools that ignore empty flags).
|
|
74
|
+
const safeSeed = seed !== undefined ? String(seed) : "";
|
|
75
|
+
|
|
66
76
|
return template
|
|
67
77
|
.replace(/\{input\}/g, safeInput)
|
|
68
|
-
.replace(/\{output\}/g, safeOutput)
|
|
78
|
+
.replace(/\{output\}/g, safeOutput)
|
|
79
|
+
.replace(/\{seed\}/g, safeSeed);
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
// ── Exported functions ────────────────────────────────────────────────────
|
|
@@ -101,11 +112,12 @@ export async function generateSTT(audioPath: string, outputDir: string, cli?: st
|
|
|
101
112
|
|
|
102
113
|
/**
|
|
103
114
|
* Generate an image from a text prompt.
|
|
115
|
+
* @param seed Optional seed for reproducible generation. Substituted into {seed} in the CLI template.
|
|
104
116
|
* @returns output path on success, empty string on failure.
|
|
105
117
|
*/
|
|
106
|
-
export function generateTTI(prompt: string, outputPath: string, cli?: string): string {
|
|
118
|
+
export function generateTTI(prompt: string, outputPath: string, cli?: string, seed?: number): string {
|
|
107
119
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
108
|
-
const cmd = substituteCli(cli ?? DEFAULT_TTI_CLI, prompt, outputPath);
|
|
120
|
+
const cmd = substituteCli(cli ?? DEFAULT_TTI_CLI, prompt, outputPath, seed);
|
|
109
121
|
try {
|
|
110
122
|
execSync(cmd, {
|
|
111
123
|
encoding: "utf-8",
|
|
@@ -128,6 +140,7 @@ export function generateTTI(prompt: string, outputPath: string, cli?: string): s
|
|
|
128
140
|
* to create an image from the prompt, then uses ffmpeg to produce a 3-second MP4.
|
|
129
141
|
*
|
|
130
142
|
* @param ttiCmd Optional TTI CLI template for the image step (defaults to DEFAULT_TTI_CLI).
|
|
143
|
+
* @param seed Optional seed for reproducible generation. Substituted into {seed} in the CLI template.
|
|
131
144
|
* @returns output path on success, empty string on failure.
|
|
132
145
|
*/
|
|
133
146
|
export function generateTTV(
|
|
@@ -135,13 +148,14 @@ export function generateTTV(
|
|
|
135
148
|
outputPath: string,
|
|
136
149
|
cli?: string,
|
|
137
150
|
ttiCmd?: string,
|
|
151
|
+
seed?: number,
|
|
138
152
|
): string {
|
|
139
153
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
140
154
|
|
|
141
155
|
if (!cli) {
|
|
142
156
|
// Default: use generateTTI to create an image, then ffmpeg to make a 3s MP4
|
|
143
157
|
const pngPath = outputPath.replace(/\.mp4$/, ".png");
|
|
144
|
-
const imageResult = generateTTI(prompt, pngPath, ttiCmd);
|
|
158
|
+
const imageResult = generateTTI(prompt, pngPath, ttiCmd, seed);
|
|
145
159
|
if (!imageResult || !existsSync(imageResult)) {
|
|
146
160
|
console.error(` ✗ TTV: TTI step produced no image for "${prompt.slice(0, 50)}..."`);
|
|
147
161
|
return "";
|
|
@@ -161,7 +175,7 @@ export function generateTTV(
|
|
|
161
175
|
}
|
|
162
176
|
|
|
163
177
|
// Custom CLI mode: substitute template and run
|
|
164
|
-
const cmd = substituteCli(cli, prompt, outputPath);
|
|
178
|
+
const cmd = substituteCli(cli, prompt, outputPath, seed);
|
|
165
179
|
try {
|
|
166
180
|
execSync(cmd, {
|
|
167
181
|
encoding: "utf-8",
|
package/src/schema/index.ts
CHANGED
|
@@ -74,6 +74,7 @@ export const root = folder.extend({
|
|
|
74
74
|
fps: z.number().int().positive().default(30),
|
|
75
75
|
instruction: z.string().optional(),
|
|
76
76
|
metadata: z.string().optional(),
|
|
77
|
+
seed: z.number().optional().describe("global seed for TTI/TTV generation; substituted into CLI {seed} placeholder"),
|
|
77
78
|
stylesheet: z.string().optional().describe("global css; selectors use .type and .name"),
|
|
78
79
|
subtitle: subtitleOverlay.optional().describe("global subtitle overlay; src is a VTT file with absolute timestamps"),
|
|
79
80
|
/**
|
|
@@ -1,539 +0,0 @@
|
|
|
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
|
-
```
|