@lalalic/markcut 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +27 -0
- package/.github/user-steer.md +9 -0
- package/.vscode/settings.json +3 -0
- package/AGENTS.md +271 -0
- package/README.md +219 -0
- package/SKILL.md +209 -0
- package/docs/dynamic-components.md +191 -0
- package/docs/edit-mode.md +220 -0
- package/docs/json-descriptive.md +539 -0
- package/docs/label-mode.md +110 -0
- package/docs/markdown-descriptive.md +751 -0
- package/docs/templates.md +52 -0
- package/package.json +64 -0
- package/remotion.config.ts +5 -0
- package/scripts/artlist-dl.mjs +190 -0
- package/scripts/build-pipeline.sh +19 -0
- package/scripts/build-player.sh +20 -0
- package/src/Root.tsx +55 -0
- package/src/config.mjs +88 -0
- package/src/context/EventContext.tsx +168 -0
- package/src/context/index.tsx +42 -0
- package/src/descriptive/compiler.test.ts +1135 -0
- package/src/descriptive/compiler.ts +1230 -0
- package/src/descriptive/dsl.ts +455 -0
- package/src/descriptive/markdown.test.ts +866 -0
- package/src/descriptive/markdown.ts +674 -0
- package/src/descriptive/resolve.test.ts +951 -0
- package/src/descriptive/resolve.ts +891 -0
- package/src/entry.tsx +163 -0
- package/src/index.ts +4 -0
- package/src/player/browser.tsx +356 -0
- package/src/player/bundle/player.js +60259 -0
- package/src/player/bundler.mjs +269 -0
- package/src/player/label-server.mjs +599 -0
- package/src/player/pipeline.mjs +11123 -0
- package/src/player/pipeline.ts +117 -0
- package/src/player/server-shared.mjs +144 -0
- package/src/player/server.mjs +1006 -0
- package/src/render/cli-tools.ts +177 -0
- package/src/render/cli.mjs +628 -0
- package/src/schema/index.ts +259 -0
- package/src/types/Audio.tsx +56 -0
- package/src/types/Component.tsx +135 -0
- package/src/types/Effect.tsx +88 -0
- package/src/types/Folder.tsx +180 -0
- package/src/types/FrameSyncStyle.tsx +51 -0
- package/src/types/Image.tsx +51 -0
- package/src/types/Include.tsx +394 -0
- package/src/types/Map.tsx +252 -0
- package/src/types/Rhythm.tsx +58 -0
- package/src/types/Scene.tsx +42 -0
- package/src/types/Subtitle.tsx +218 -0
- package/src/types/Video.tsx +70 -0
- package/src/types/keyframes.ts +454 -0
- package/src/utils/__tests__/vtt.test.ts +129 -0
- package/src/utils/index.ts +168 -0
- package/src/utils/tween.ts +118 -0
- package/src/vision/cli.mjs +1187 -0
- package/src/vision/vision_prompts.md +67 -0
- package/tests/dsl.test.ts +317 -0
- package/tests/fixtures/audio.json +25 -0
- package/tests/fixtures/basic.json +27 -0
- package/tests/fixtures/component-all.json +38 -0
- package/tests/fixtures/components.json +38 -0
- package/tests/fixtures/effects.json +64 -0
- package/tests/fixtures/full.json +51 -0
- package/tests/fixtures/map.json +23 -0
- package/tests/fixtures/md/all-nodes.md +28 -0
- package/tests/fixtures/md/basic.md +6 -0
- package/tests/fixtures/md/component-imports.md +20 -0
- package/tests/fixtures/md/edge-cases.md +33 -0
- package/tests/fixtures/md/effects.md +17 -0
- package/tests/fixtures/md/frontmatter.md +20 -0
- package/tests/fixtures/md/full-feature.md +58 -0
- package/tests/fixtures/md/imports-block.md +19 -0
- package/tests/fixtures/md/include-main.md +11 -0
- package/tests/fixtures/md/include-sub.md +25 -0
- package/tests/fixtures/md/jsx-code-fence.md +21 -0
- package/tests/fixtures/md/map.md +11 -0
- package/tests/fixtures/md/nested-scenes.md +25 -0
- package/tests/fixtures/md/rhythm.md +17 -0
- package/tests/fixtures/md/scenes.md +16 -0
- package/tests/fixtures/md/tween.md +11 -0
- package/tests/fixtures/md/vars-test.md +6 -0
- package/tests/fixtures/scenes.json +40 -0
- package/tests/fixtures/subtitle.json +59 -0
- package/tests/fixtures/subvideo.json +59 -0
- package/tests/fixtures/templates/courseware.md +351 -0
- package/tests/fixtures/tween-visual.json +28 -0
- package/tests/fixtures/video-series.json +54 -0
- package/tests/md-descriptive.test.ts +742 -0
- package/tests/render.test.ts +985 -0
- package/tests/schema.test.ts +68 -0
- package/tests/server.test.ts +308 -0
- package/tests/utils.ts +391 -0
- package/tests/vitest.config.ts +18 -0
- package/tests/vitest.integration.config.ts +16 -0
- package/tsconfig.json +20 -0
package/SKILL.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: markcut
|
|
3
|
+
description: >-
|
|
4
|
+
Compose and render videos from streamline trees. CLI renders stream tree
|
|
5
|
+
to MP4. Run via `npx markcut` — no install, no code.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Stream Tree Specs
|
|
9
|
+
|
|
10
|
+
Everything video is a **stream tree**. see [docs/markdown-descriptive.md](docs/markdown-descriptive.md) for full details.
|
|
11
|
+
|
|
12
|
+
## .markcut/ Directory Layout
|
|
13
|
+
|
|
14
|
+
When you run `preview` or `render`, all generated artifacts live under `.markcut/` next to the source file:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
.markcut/
|
|
18
|
+
generated/ ← shared, content-addressed
|
|
19
|
+
tts/ ← TTS audio (content-hash filenames)
|
|
20
|
+
media/ ← TTI/TTV media (content-hash filenames)
|
|
21
|
+
includes/ ← compiled sub-video JSON (content-hash)
|
|
22
|
+
<basename>/ ← per-source-file artifacts
|
|
23
|
+
components/ ← component bundles (per-file: imports differ)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `generated/` — shared artifacts using content-hash filenames, so identical scripts/prompts across different source files reuse the same cached file
|
|
27
|
+
- `<basename>/components/` — per-file component bundles; each source file has its own `imports` block, so bundles live under its basename
|
|
28
|
+
- The server serves `.markcut/` as a document root, so components at `.markcut/slides/components/abc.js` are served as `/slides/components/abc.js`
|
|
29
|
+
|
|
30
|
+
## Include (Sub-Video Embedding)
|
|
31
|
+
|
|
32
|
+
The `include` node embeds an external `.md` file as a sub-video. The pipeline fully resolves it recursively:
|
|
33
|
+
|
|
34
|
+
1. **`resolveIncludes()`** (step 0 of `resolveAll`): reads the referenced `.md`, parses it, resolves its own includes/TTS/media, compiles to a stream tree, writes the compiled JSON to `.markcut/generated/includes/<hash>.json`
|
|
35
|
+
2. **`resolveIncludeImports()`** (server post-compile): if the sub-video has a ` ```js imports ``` ` block, bundles its components independently and writes the bundle URL into the compiled JSON
|
|
36
|
+
3. **`IncludeLeaf`** (render): loads the compiled JSON, dynamically imports the sub-video's component bundle, creates a nested `ComposeContext` with the sub-video's registry merged, and renders via `FolderLeaf`
|
|
37
|
+
|
|
38
|
+
The sub-video's own components are isolated from the parent's — naming conflicts are resolved by "sub-video wins" priority.
|
|
39
|
+
|
|
40
|
+
```markdown
|
|
41
|
+
# video
|
|
42
|
+
## Main
|
|
43
|
+
- component duration:3 jsx:"<MainTitle />"
|
|
44
|
+
## Embedded
|
|
45
|
+
- include src:./sub-video.md
|
|
46
|
+
## End
|
|
47
|
+
- component duration:2 jsx:"<Outro />"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```markdown
|
|
51
|
+
# sub-video
|
|
52
|
+
- component duration:3 jsx:"<SubSlide scene='1' />"
|
|
53
|
+
|
|
54
|
+
```js imports
|
|
55
|
+
export function SubSlide({ scene }) {
|
|
56
|
+
return <div style={{background:'#667eea', ...}}>Scene {scene}</div>;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
```
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Video Design Best Practice (How-To)
|
|
63
|
+
|
|
64
|
+
The engine supports four authoring phases. Each has a dedicated doc with full detail.
|
|
65
|
+
|
|
66
|
+
### Label — annotate clips in a stream tree
|
|
67
|
+
|
|
68
|
+
Load a stream tree, preview each clip, and attach descriptive labels.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx markcut preview <flat stream tree file> --label
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Labels save to `labels.json`. See [docs/label-mode.md](docs/label-mode.md) for:
|
|
75
|
+
- Label player UI (thumbnails, player, label input)
|
|
76
|
+
- Labels persistence format
|
|
77
|
+
- Full workflow diagram
|
|
78
|
+
|
|
79
|
+
### Storyboard — plan video structure with scene nodes
|
|
80
|
+
|
|
81
|
+
Use `scene` nodes to organize your video. Each scene has `title`, `instruction`, `script` (TTS narration), `children`. Scenes can nest inside other scenes.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
see [docs/markdown-descriptive.md](docs/markdown-descriptive.md) for full details.
|
|
85
|
+
|
|
86
|
+
### 3. Assemble — render stream tree to MP4
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx markcut render <stream tree file> --aspect all # MP4 output
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Template Variables
|
|
95
|
+
|
|
96
|
+
`${width}`, `${height}`, `${fps}`, `${variant}` can be used in `src`,
|
|
97
|
+
`prompt`, and `stylesheet` values. They resolve to the root config values.
|
|
98
|
+
|
|
99
|
+
```markdown
|
|
100
|
+
# video
|
|
101
|
+
width:1920 height:1080
|
|
102
|
+
## Scene
|
|
103
|
+
- image src:photo_${width}x${height}.jpg
|
|
104
|
+
- component jsx:"<h1>${width}</h1>" ← NOT resolved (jsx excluded)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> Only `src`, `prompt`, and `stylesheet` support template variables.
|
|
108
|
+
> Root config keys, jsx, script, style, etc. do not.
|
|
109
|
+
|
|
110
|
+
## Frontmatter
|
|
111
|
+
|
|
112
|
+
The YAML `---` block is **metadata only**. It does NOT affect video config.
|
|
113
|
+
All configuration comes from the root config line after `# video`:
|
|
114
|
+
|
|
115
|
+
```markdown
|
|
116
|
+
---
|
|
117
|
+
title: My Campaign
|
|
118
|
+
---
|
|
119
|
+
# video
|
|
120
|
+
width:640 height:480 fps:30 layout:series tts:"edge-tts --voice '...' --text '{input}' --write-media '{output}'"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## AI Media Generation (TTS / STT / TTI(text-to-image) / TTV(text-to-video))
|
|
124
|
+
|
|
125
|
+
All four pipelines are configured via a **single CLI string** on the root config line. The user/LLM embeds every tool-specific parameter directly in the string — only `{input}` and `{output}` are substituted by the engine.
|
|
126
|
+
|
|
127
|
+
Default CLI templates (used when not overridden on the root config line):
|
|
128
|
+
|
|
129
|
+
| Config | Default |
|
|
130
|
+
|--------|---------|
|
|
131
|
+
| `tts` | `uvx edge-tts --voice "en-US-GuyNeural" --text "{input}" --write-media "{output}"` |
|
|
132
|
+
| `stt` | `uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}"` |
|
|
133
|
+
| `tti` | `uvx --from mflux mflux-generate-flux2 --model flux2-klein-4b --steps 5 --prompt "{input}" --output "{output}"` |
|
|
134
|
+
| `ttv` | (uses TTI to create an image, then ffmpeg to loop it for 5s) |
|
|
135
|
+
|
|
136
|
+
Override on the root config line:
|
|
137
|
+
|
|
138
|
+
```markdown
|
|
139
|
+
# video
|
|
140
|
+
width:1920 height:1080 tts:"uvx edge-tts --voice 'zh-CN-YunxiNeural' --text '{input}' --write-media '{output}'"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 3. CLI
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx markcut <command> [options]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Commands
|
|
152
|
+
|
|
153
|
+
| Command | Description |
|
|
154
|
+
|---------|-------------|
|
|
155
|
+
| `render <file>` | Render stream tree to MP4 |
|
|
156
|
+
| `preview <file> --label` | Label clips in a simplified stream tree |
|
|
157
|
+
| `preview <file> --edit` | Live editing loop (auto-reload on file change) |
|
|
158
|
+
| `verify <file>` | Validate descriptive file + check imports |
|
|
159
|
+
### Options
|
|
160
|
+
|
|
161
|
+
| Flag | Values | Default |
|
|
162
|
+
|------|--------|---------|
|
|
163
|
+
| `--aspect` | `16x9` / `9x16` / `1x1` / `all` | `16x9` |
|
|
164
|
+
| `--output` | path | `out/video-{aspect}.mp4` |
|
|
165
|
+
| `--port` | number | `3001` |
|
|
166
|
+
| `--verbose` | flag | `false` (compact progress) |
|
|
167
|
+
| `--compile` | flag | `false` (resolve only) |
|
|
168
|
+
| `--script-output-dir` | dir | `.markcut/generated/tts/` |
|
|
169
|
+
| `--media-output-dir` | dir | `.markcut/generated/media/` |
|
|
170
|
+
|
|
171
|
+
> Default output dirs auto-resolve to `.markcut/<basename>/<type>/` relative to the source file. You only need `--script-output-dir` / `--media-output-dir` to override.
|
|
172
|
+
|
|
173
|
+
### Edit Mode for Agents
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Start player in background
|
|
177
|
+
npx markcut preview <stream tree file> --edit --port 3001 &
|
|
178
|
+
|
|
179
|
+
# Player auto-opens browser. Agent edits JSON → player auto-reloads.
|
|
180
|
+
# User clicks ✕ → server exits → agent regains control.
|
|
181
|
+
# Browser feedback input writes to feedback.txt.
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## self verification
|
|
187
|
+
some common issues (photo or video can't be displayed, audio missing), take below actions to verify
|
|
188
|
+
### preview or render mode
|
|
189
|
+
- screenshot some key frames, and understand image to verify if the image is correct
|
|
190
|
+
### final video
|
|
191
|
+
- screenshot some key frames, and understand image to verify if the image is correct
|
|
192
|
+
- stt the final video audio, and verify if vtt result is correct
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
## Reference
|
|
196
|
+
|
|
197
|
+
| Topic | File |
|
|
198
|
+
|-------|------|
|
|
199
|
+
| Markdown descriptive format (primary authoring format) | [docs/markdown-descriptive.md](docs/markdown-descriptive.md) |
|
|
200
|
+
| **Video Templates** — ready-to-use markdown for common scenarios | **[docs/templates/](docs/templates/)** |
|
|
201
|
+
| ┣ Courseware / 课件 | [docs/templates/courseware.md](docs/templates/courseware.md) |
|
|
202
|
+
| ┣ Product Ad / 产品广告 | [docs/templates/product-ad.md](docs/templates/product-ad.md) |
|
|
203
|
+
| ┣ Movie Review / 影视讲解 | [docs/templates/movie-review.md](docs/templates/movie-review.md) |
|
|
204
|
+
| ┣ Audiobook / 有声图书 | [docs/templates/audiobook.md](docs/templates/audiobook.md) |
|
|
205
|
+
| ┣ Story Video / 故事视频 | [docs/templates/story-video.md](docs/templates/story-video.md) |
|
|
206
|
+
| ┣ Travel Log / 旅行日志 | [docs/templates/travel-log.md](docs/templates/travel-log.md) |
|
|
207
|
+
| Label system (browse, label, export labels.json) | [docs/label-mode.md](docs/label-mode.md) |
|
|
208
|
+
| Player servers (label + edit mode) | [docs/edit-mode.md](docs/edit-mode.md) |
|
|
209
|
+
| Template overview and TTI/TTV config | [docs/templates.md](docs/templates.md) |
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Dynamic Components
|
|
2
|
+
|
|
3
|
+
Three ways to add custom visual content without rebuilding the engine.
|
|
4
|
+
|
|
5
|
+
## 1. Effects on Any Node (CSS Keyframes)
|
|
6
|
+
|
|
7
|
+
Add the `effects` key to any node to apply CSS keyframe animations. No wrapper node needed.
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"id": "animated-scene",
|
|
12
|
+
"type": "image",
|
|
13
|
+
"src": "photo.jpg",
|
|
14
|
+
"duration": 3,
|
|
15
|
+
"effects": ["bounceIn(1, ease-out, 2)"]
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 25+ Built-in Animation Names
|
|
20
|
+
|
|
21
|
+
| Fades | Slides | Zooms | Attention | Bounce | Rotations |
|
|
22
|
+
|-------|--------|-------|-----------|--------|-----------|
|
|
23
|
+
| `fadeIn` | `slideInDown` | `zoomIn` | `pulse` | `bounceIn` | `rotateIn` |
|
|
24
|
+
| `fadeOut` | `slideInUp` | `zoomOut` | `flash` | | `rotateOut` |
|
|
25
|
+
| `fadeInDown` | `slideInLeft` | | `bounce` | | |
|
|
26
|
+
| `fadeInUp` | `slideInRight` | | `heartBeat` | | |
|
|
27
|
+
| `fadeInLeft` | | | `rubberBand` | | |
|
|
28
|
+
| `fadeInRight` | | | `shakeX` | | |
|
|
29
|
+
| (8 total) | | | | | |
|
|
30
|
+
|
|
31
|
+
### Custom Keyframes
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"type": "effect",
|
|
36
|
+
"animation": "custom",
|
|
37
|
+
"customKeyframes": {
|
|
38
|
+
"0": { "opacity": "0", "transform": "scale3d(0,0,0) rotate(0deg)" },
|
|
39
|
+
"50": { "opacity": "0.5", "transform": "scale3d(1.2,1.2,1.2) rotate(180deg)" },
|
|
40
|
+
"100": { "opacity": "1", "transform": "scale3d(1,1,1) rotate(360deg)" }
|
|
41
|
+
},
|
|
42
|
+
"children": [...],
|
|
43
|
+
"actions": [{ "start": 0, "end": 2 }]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Percentages `"0"`–`"100"` map to action duration. Any numeric CSS property works.
|
|
48
|
+
|
|
49
|
+
## 2. Remote Components (bundler-based)
|
|
50
|
+
|
|
51
|
+
Register remote React components via the `~~~js imports` code fence (markdown) or `root.imports` array (JSON descriptive). The player server bundles all registered components into a single ESM module at startup.
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
~~~js imports
|
|
55
|
+
export { BarChart } from "recharts"
|
|
56
|
+
export { Hello } from "git:user/repo/path/to/Hello.tsx"
|
|
57
|
+
export { Badge } from "https://cdn.example.com/components/badge.js"
|
|
58
|
+
|
|
59
|
+
export function Slide(props) {
|
|
60
|
+
return <div style={{color: '#fff'}}>{props.children}</div>;
|
|
61
|
+
}
|
|
62
|
+
~~~
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then use them in component JSX:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"type": "component",
|
|
70
|
+
"jsx": "<Badge text='LIVE' color='#ff0000' />",
|
|
71
|
+
"actions": [{ "start": 0, "end": 3 }]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### How it works
|
|
76
|
+
|
|
77
|
+
1. The server extracts the imports block from the source file
|
|
78
|
+
2. `parseImportsBlock` resolves `export { X } from "spec"` and `export function X() {}` patterns
|
|
79
|
+
3. Inline functions' source code is scanned for `import X from "pkg"` statements — those packages are added to dependencies
|
|
80
|
+
4. `bundleFromEntries` creates a temp npm project, installs all packages, and runs `esbuild` to produce a single ESM file
|
|
81
|
+
5. The bundled file URL is set on `root.imports` and served from `/.component-cache/<hash>.js`
|
|
82
|
+
6. At runtime, `MarkCut.useComponentRegistry` does a dynamic `import(url)` to load all named exports
|
|
83
|
+
7. react-jsx-parser resolves component tags from the loaded registry
|
|
84
|
+
|
|
85
|
+
### Import spec forms
|
|
86
|
+
|
|
87
|
+
| Pattern | Resolves to | Example |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `pkg` or `npm:pkg` | npm package (installed + bundled) | `export { BarChart } from "recharts"` |
|
|
90
|
+
| `git:user/repo` | GitHub repo source | `export { Comp } from "git:user/repo/src/Comp.tsx"` |
|
|
91
|
+
| `https://...` | Raw URL (used as-is) | `export { X } from "https://cdn.example.com/x.js"` |
|
|
92
|
+
| local path | Filesystem path | `export { X } from "./local/Component.tsx"` |
|
|
93
|
+
|
|
94
|
+
### Inline functions
|
|
95
|
+
|
|
96
|
+
Define components directly in the imports block. Dependencies used inside the function body are automatically detected and installed:
|
|
97
|
+
|
|
98
|
+
```markdown
|
|
99
|
+
~~~js imports
|
|
100
|
+
import ReactMarkdown from "react-markdown"
|
|
101
|
+
|
|
102
|
+
export function Slide({ children }) {
|
|
103
|
+
return <div className="slide"><ReactMarkdown>{children}</ReactMarkdown></div>;
|
|
104
|
+
}
|
|
105
|
+
~~~
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The `import` statements at the top serve dual purpose: they bring packages into scope inside inline functions AND tell the bundler what to install. The packages are added to `package.json` during bundling.
|
|
109
|
+
|
|
110
|
+
### Component Contract
|
|
111
|
+
|
|
112
|
+
Props injected automatically:
|
|
113
|
+
- Nothing special required — just standard React props. The engine passes any `data` bindings from the component node as JSX variables.
|
|
114
|
+
|
|
115
|
+
For frame-accurate animation, use standard Remotion hooks inside inline functions:
|
|
116
|
+
|
|
117
|
+
```markdown
|
|
118
|
+
~~~js imports
|
|
119
|
+
import { useCurrentFrame } from "remotion"
|
|
120
|
+
|
|
121
|
+
export function FadeIn({ children }) {
|
|
122
|
+
const frame = useCurrentFrame();
|
|
123
|
+
const opacity = Math.min(1, frame / 15);
|
|
124
|
+
return <div style={{ opacity }}>{children}</div>;
|
|
125
|
+
}
|
|
126
|
+
~~~
|
|
127
|
+
|
|
128
|
+
Note: `remotion` and `react` are automatically available in the bundle's external scope — they do NOT need to be imported. However, adding `import { useCurrentFrame } from "remotion"` in the imports block is harmless and makes the dependency explicit.
|
|
129
|
+
|
|
130
|
+
## 3. Custom Components
|
|
131
|
+
|
|
132
|
+
Create a React component file in `src/components/`, register in `builtinComponents`.
|
|
133
|
+
|
|
134
|
+
### Component Contract
|
|
135
|
+
|
|
136
|
+
Props the engine injects automatically:
|
|
137
|
+
- `action: { start: number; end: number }` — action timing from the stream node
|
|
138
|
+
|
|
139
|
+
Use `useCurrentFrame()` for frame-accurate animation, `useVideoConfig()` for canvas size.
|
|
140
|
+
|
|
141
|
+
### Example
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
// src/components/my/Badge.tsx
|
|
145
|
+
import React from "react";
|
|
146
|
+
import { useCurrentFrame, interpolate } from "remotion";
|
|
147
|
+
|
|
148
|
+
export const Badge: React.FC<{text: string; color?: string; action: any}> =
|
|
149
|
+
({ text, color, action }) => {
|
|
150
|
+
const frame = useCurrentFrame();
|
|
151
|
+
const local = frame - action.start * 30;
|
|
152
|
+
const opacity = interpolate(local, [0, 15], [0, 1]);
|
|
153
|
+
return (
|
|
154
|
+
<div style={{ opacity, padding: 12, background: color || theme.colors.primary,
|
|
155
|
+
color: "white", borderRadius: 8, fontSize: 48 }}>
|
|
156
|
+
{text}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Register
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
// src/components/index.ts
|
|
166
|
+
import { Badge } from "./my/Badge";
|
|
167
|
+
export const builtinComponents = { ..., Badge };
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Reference in stream tree
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"type": "component",
|
|
175
|
+
"jsx": "<Badge text='NEW' color='#ff6b35' />",
|
|
176
|
+
"actions": [{ "start": 1, "end": 4 }]
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# common used components
|
|
182
|
+
- `react-markdown` — render markdown content, use plugins to extend functionality
|
|
183
|
+
- `remark-gfm` — support GitHub Flavored Markdown (tables, strikethrough, task lists)
|
|
184
|
+
- `remark-toc` — generate table of contents
|
|
185
|
+
- `remark-math` — support math formulas
|
|
186
|
+
- `rehype-katex` — render math formulas with KaTeX
|
|
187
|
+
- `remark-mermaidjs` — render mermaid diagrams in
|
|
188
|
+
- `react-markdown-mermaid` — render mermaid diagrams in standalone mode (no need to install mermaid separately)
|
|
189
|
+
|
|
190
|
+
- `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
|
|
191
|
+
- `@remotion/starburst` — render starburst animations
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Watch Mode
|
|
2
|
+
|
|
3
|
+
> `preview <file.md> --edit` — live editing loop with AI-assisted JSON editing.
|
|
4
|
+
|
|
5
|
+
Edit mode gives you a browser player that **automatically reloads** when the source file changes, plus an **edit input** that calls an AI coding agent (pi) to make changes on your behalf.
|
|
6
|
+
|
|
7
|
+
Generated artifacts (TTS audio, compiled includes, component bundles) are
|
|
8
|
+
stored under `.markcut/` next to the source file — see [SKILL.md](../SKILL.md) for the full layout.
|
|
9
|
+
|
|
10
|
+
## Quickstart
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cd remotion-engine
|
|
14
|
+
npm run preview -- sample-visual.json --edit
|
|
15
|
+
# → Browser opens, terminal blocks until you close it
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
┌─────────────┐ HTTP ┌──────────────────┐ spawn ┌──────────┐
|
|
22
|
+
│ Browser │◄──────────────►│ player/server │◄────────────►│ pi CLI │
|
|
23
|
+
│ (Chrome) │ SSE │ .mjs (Node) │ one-shot │ (agent) │
|
|
24
|
+
└──────┬──────┘ └────────┬─────────┘ └──────────┘
|
|
25
|
+
│ │
|
|
26
|
+
│ POST /api/edit │ fs.watchFile
|
|
27
|
+
│ {text: "make text bigger"} │ (poll every 500ms)
|
|
28
|
+
│ │
|
|
29
|
+
│ GET /api/video-data ▼
|
|
30
|
+
│ ← stream tree JSON ┌──────────────┐
|
|
31
|
+
│ │ stream tree │
|
|
32
|
+
│ EventSource │ .json (file) │
|
|
33
|
+
│ /api/events └──────────────┘
|
|
34
|
+
│ ← {type:"reload"} ▲
|
|
35
|
+
│ │
|
|
36
|
+
│ window.dispatchEvent │ pi edits
|
|
37
|
+
│ ("refresh-player") │ this file
|
|
38
|
+
│ │
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## File: `src/player/server.mjs`
|
|
42
|
+
|
|
43
|
+
### Server startup (`cli.mjs` → `server.mjs`)
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
node src/render/cli.mjs preview file.json --edit --port 3031
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
1. `cli.mjs` spawns `server.mjs` with `--edit` flag
|
|
50
|
+
2. `server.mjs` parses `file.json` to build a recursive tree description
|
|
51
|
+
3. Starts `fs.watchFile(file.json, {interval:500})` — polls every 500ms for changes
|
|
52
|
+
4. Starts HTTP server on specified port
|
|
53
|
+
5. Auto-opens browser via `open http://localhost:PORT`
|
|
54
|
+
|
|
55
|
+
### Page served
|
|
56
|
+
|
|
57
|
+
The HTML page loads two scripts:
|
|
58
|
+
|
|
59
|
+
| Script | Source | Role |
|
|
60
|
+
|--------|--------|------|
|
|
61
|
+
| `player.js` | Bundled esbuild output from `src/player/browser.tsx` | Renders the stream tree using `@remotion/player` with `MarkCut` |
|
|
62
|
+
| Inline script | Generated by `getHtml()` in `server.mjs` | Watch-mode UI controls (close button, edit input, SSE) |
|
|
63
|
+
|
|
64
|
+
### Edit flow
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Browser Server pi CLI
|
|
68
|
+
│ │ │
|
|
69
|
+
│── POST /api/edit ────────────► │ │
|
|
70
|
+
│ {text: "make text bigger"} │ │
|
|
71
|
+
│ │── spawn("pi", ["-p", prompt])│
|
|
72
|
+
│ │ prompt includes: │
|
|
73
|
+
│ │ • full tree description │
|
|
74
|
+
│ │ (recursive, any depth) │
|
|
75
|
+
│ │ • edit history │
|
|
76
|
+
│ │ • knowledge (all stream │
|
|
77
|
+
│ │ types, components, │
|
|
78
|
+
│ │ schema, styling guide) │
|
|
79
|
+
│ │ • edit request │
|
|
80
|
+
│ │ │
|
|
81
|
+
│ │ pi can edit ANY field on │
|
|
82
|
+
│ │ ANY node in the tree │
|
|
83
|
+
│ │ ├── edits file.json
|
|
84
|
+
│ │ └── exits
|
|
85
|
+
│ │◄── exit code 0 │
|
|
86
|
+
│◄── {done:true, output:"..."} ──┤ │
|
|
87
|
+
│ │ │
|
|
88
|
+
│ suppressReload = true │ │
|
|
89
|
+
│ header shows "Changed text..." │ │
|
|
90
|
+
│ │ │
|
|
91
|
+
│ (fs.watchFile fires SSE │ │
|
|
92
|
+
│ but suppressReload=true │ │
|
|
93
|
+
│ so it's ignored) │ │
|
|
94
|
+
│ │ │
|
|
95
|
+
│ after 4s: │ │
|
|
96
|
+
│ dispatchEvent("refresh-player")│ │
|
|
97
|
+
│ → player re-fetches /api/data │ │
|
|
98
|
+
│ → re-renders with new JSON │ │
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Key design decisions
|
|
102
|
+
|
|
103
|
+
**Why in-place refresh instead of `location.reload()`?**
|
|
104
|
+
- Timeline playback continues uninterrupted
|
|
105
|
+
- Status text persists until next edit
|
|
106
|
+
- No page flash
|
|
107
|
+
|
|
108
|
+
**Why `suppressReload`?**
|
|
109
|
+
- SSE fires ~500ms after pi edits the file
|
|
110
|
+
- API response arrives at the same time
|
|
111
|
+
- `suppressReload` flag ignores the SSE event during edit
|
|
112
|
+
- The client reloads itself after showing the summary
|
|
113
|
+
|
|
114
|
+
## File: `src/player/browser.tsx`
|
|
115
|
+
|
|
116
|
+
The bundled player app:
|
|
117
|
+
|
|
118
|
+
1. Fetches `/api/video-data` on mount
|
|
119
|
+
2. Renders the stream tree using `@remotion/player` with `MarkCut`
|
|
120
|
+
3. Listens for `"refresh-player"` custom DOM event
|
|
121
|
+
4. On refresh event: re-fetches data, re-renders in-place
|
|
122
|
+
|
|
123
|
+
## File: `src/render/cli.mjs`
|
|
124
|
+
|
|
125
|
+
The CLI routes `--edit` flag to the player server:
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
if (args.edit) {
|
|
129
|
+
const serverArgs = ["src/player/server.mjs", file, "--edit", `--port=${port}`];
|
|
130
|
+
const child = spawn("node", serverArgs, { cwd: ROOT, stdio: "inherit" });
|
|
131
|
+
// ...blocks until server exits...
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## API Endpoints
|
|
136
|
+
|
|
137
|
+
| Endpoint | Method | Description |
|
|
138
|
+
|----------|--------|-------------|
|
|
139
|
+
| `/api/video-data` | GET | Returns the raw stream tree JSON file |
|
|
140
|
+
| `/api/scenes` | GET | Returns parsed scenes array with timing |
|
|
141
|
+
| `/api/video-info` | GET | Returns scenes + totalDuration + mode flags |
|
|
142
|
+
| `/api/edit` | POST | Accepts `{text}` → spawns pi to edit the JSON → file watcher triggers → SSE reload |
|
|
143
|
+
| `/api/shutdown` | POST | Kills the server process, unblocking the agent |
|
|
144
|
+
| `/api/events` | GET | SSE stream — sends `{type:"reload"}` when file changes |
|
|
145
|
+
| `/api/labels` | GET/POST | (label mode only) Save/load scene labels |
|
|
146
|
+
|
|
147
|
+
## The pi prompt
|
|
148
|
+
|
|
149
|
+
Each edit call sends a structured prompt with a **recursive tree description** instead of a flat scene list:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
You are editing sample-visual.json, a Remotion stream tree JSON.
|
|
153
|
+
|
|
154
|
+
The stream tree (indentation shows nesting; timing in seconds):
|
|
155
|
+
root "root" (1080×1920, 30fps, series, transition:fade)
|
|
156
|
+
folder "scene-1" (parallel)
|
|
157
|
+
component GradientBackground {type:"radial",animated:true,noise:true} [05s, bg]
|
|
158
|
+
component AnimatedHeadline {"text":"Hello Remotion",...} [0.54.5s]
|
|
159
|
+
folder "scene-2" (parallel)
|
|
160
|
+
component GradientBackground {type:"linear",animated:true} [05s, bg]
|
|
161
|
+
subtitle "This text is styled with CSS" fontSize:52 [0.54.5s]
|
|
162
|
+
...
|
|
163
|
+
|
|
164
|
+
Previous edits:
|
|
165
|
+
1. make scene 1 headline text orange
|
|
166
|
+
|
|
167
|
+
Edit request: make scene 1 headline text bigger
|
|
168
|
+
|
|
169
|
+
--- Knowledge ---
|
|
170
|
+
You can edit ANY field on ANY node in the JSON...
|
|
171
|
+
Stream types: root, folder, video, audio, image, subtitle, component, effect, rhythm, map
|
|
172
|
+
...
|
|
173
|
+
IMPORTANT: Read the full existing JSON file before editing. Only edit the JSON file. You can change, add, or remove any field on any node. Output ONLY a one-line summary of what specific change you made.
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The tree description walks the JSON recursively, showing:
|
|
177
|
+
- **Nesting depth** via indentation
|
|
178
|
+
- **Type** and **id/name** of each node
|
|
179
|
+
- **Key fields** (dimensions, fps, theme, series/parallel, transitions)
|
|
180
|
+
- **Timing** from actions (`[startend]`)
|
|
181
|
+
- **isBackground** flag (`[bg]`)
|
|
182
|
+
- **Component props** and node-specific fields (fontSize, fit, style, volume, etc.)
|
|
183
|
+
|
|
184
|
+
This means pi can edit **any aspect of the JSON** ÔÇö not just scene props. Nested folders, effects, rhythms, maps, actions, styles, themes, global stylesheets, audio config ÔÇö everything is visible and editable.
|
|
185
|
+
|
|
186
|
+
## Session context
|
|
187
|
+
|
|
188
|
+
The server maintains `editHistory[]` — every edit is appended. On each call, previous edits are included so pi knows what was done before:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
Previous edits:
|
|
192
|
+
1. make scene 1 headline text orange
|
|
193
|
+
2. change background from radial to linear gradient
|
|
194
|
+
3. add 3D angle to device mockup in scene 4
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
This gives pi enough context to make coherent changes across multiple edit rounds.
|
|
198
|
+
|
|
199
|
+
## Close / tab-close
|
|
200
|
+
|
|
201
|
+
| Action | What happens |
|
|
202
|
+
|--------|-------------|
|
|
203
|
+
| Click **✕** | `sendBeacon("/api/shutdown")` → server exits → agent unblocks |
|
|
204
|
+
| Close **tab** | SSE connection drops → 3s grace → server exits |
|
|
205
|
+
| Close **browser** | Same as tab close |
|
|
206
|
+
|
|
207
|
+
## Label mode (`--label`)
|
|
208
|
+
|
|
209
|
+
`preview <file.json> --label` starts the same player but with a **label input** instead of the edit input. Labels are saved to `labels.json` next to the source file. No AI editing, no file watching.
|
|
210
|
+
|
|
211
|
+
## Build
|
|
212
|
+
|
|
213
|
+
The browser bundle is built with esbuild:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
bash scripts/build-player.sh
|
|
217
|
+
# → src/player/bundle/player.js
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The bundle includes React, @remotion/player, MarkCut, all 13 built-in components, themes, templates, and the duration utility. It is served as a static file by the player server.
|