@lalalic/markcut 1.0.0

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