@lalalic/markcut 1.1.1 → 1.2.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/AGENTS.md +2 -0
- package/SKILL.md +28 -172
- package/docs/markdown-descriptive.md +12 -0
- package/package.json +1 -1
- package/src/entry.tsx +8 -2
- package/src/render/cli.mjs +62 -7
- package/src/utils/component-import-map.ts +93 -0
- package/templates/courseware/TEMPLATE.md +311 -0
- package/templates/courseware/agents/reviewer.md +84 -0
- package/templates/courseware/prompts/fix.md +30 -0
- package/templates/courseware/prompts/outline.md +53 -0
- package/templates/courseware/prompts/scene.md +64 -0
- package/docs/dynamic-components.md +0 -191
- package/docs/templates.md +0 -52
package/AGENTS.md
CHANGED
|
@@ -240,6 +240,8 @@ npx vitest run tests/render.test.ts -t "Test Name" # single test
|
|
|
240
240
|
4. **`signalstats` ffmpeg filter**: Doesn't output YAVG on all systems; use file size as proxy for visual content.
|
|
241
241
|
5. **Asset paths**: Local files are resolved via `staticFile()` — files go in `public/` and are referenced without the `public/` prefix.
|
|
242
242
|
6. **Map rendering**: Depends on external tile availability; may produce blank frames offline.
|
|
243
|
+
7. **CLI md render needs three things preview gets for free**: (a) `parseMarkdownVariants(raw)` (returns `{base, variants}`) — `parseMarkdownDescriptive` returns the root directly; (b) bundling the ` ```js imports ``` ` block via `bundleFromEntries` and setting `root.imports` (otherwise jsx components are unregistered → slides render as unstyled black frames); (c) staging `.markcut/` assets into `public/.render-assets/` and rewriting srcs to relative paths (remotion render only serves its publicDir). All three live in `src/render/cli.mjs`; the import-map shim for shared react/remotion instances is `src/utils/component-import-map.ts` (must stay in sync with `player/browser.tsx` + `bundler.mjs getSharedExternals`).
|
|
244
|
+
8. **Publishing**: verify `npx @lalalic/markcut render <md>` from a clean temp dir before/after publish — v1.1.1 shipped with cli.mjs/pipeline.mjs out of sync and md rendering was completely broken. Also beware stale npx caches (`~/.npm/_npx`) silently running old code.
|
|
243
245
|
|
|
244
246
|
## 3-Level Authoring Workflow (see SKILL.md)
|
|
245
247
|
|
package/SKILL.md
CHANGED
|
@@ -1,194 +1,45 @@
|
|
|
1
1
|
---
|
|
2
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.
|
|
3
|
+
description: use markdown to describe stream trees, provide CLI to render stream trees to video via `npx @lalalic/markcut`. use it to structure video scenes, and generate TTS, TTI, TTV, STT media automatically.
|
|
6
4
|
---
|
|
7
5
|
|
|
8
6
|
## Stream Tree Specs
|
|
9
7
|
|
|
10
|
-
Everything video is a **stream tree
|
|
8
|
+
Everything video is a **stream tree** described with markdown. see [docs/markdown-descriptive.md](docs/markdown-descriptive.md) for full details.
|
|
11
9
|
|
|
12
|
-
## .markcut/ Directory Layout
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
## video design utitlies
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
.
|
|
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
|
|
13
|
+
### User labels — label media with text, time ranges
|
|
14
|
+
- `uvx @lalalic/markcut preview --label` provides tool to label video and images with text.
|
|
78
15
|
|
|
79
16
|
### Storyboard — plan video structure with scene nodes
|
|
80
17
|
|
|
81
|
-
Use `scene` nodes to organize your video.
|
|
18
|
+
- Use `scene` nodes to organize your video. Scenes can nest inside other scenes.
|
|
19
|
+
- Use `description`, `scene.instruction`, `script`, `image|video.prompt` to structure your video content.
|
|
82
20
|
|
|
83
|
-
```bash
|
|
84
21
|
see [docs/markdown-descriptive.md](docs/markdown-descriptive.md) for full details.
|
|
85
22
|
|
|
86
|
-
###
|
|
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.
|
|
23
|
+
### Video Variants
|
|
24
|
+
- define variants for different video configurations, such as different languages, aspects, platforms
|
|
98
25
|
|
|
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
26
|
|
|
145
27
|
## 3. CLI
|
|
146
28
|
|
|
147
29
|
```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.
|
|
30
|
+
npx @lalalic/markcut <command> [options]
|
|
31
|
+
npx @lalalic/markcut --help # get overall information
|
|
182
32
|
```
|
|
183
33
|
|
|
184
34
|
---
|
|
185
35
|
|
|
186
36
|
## self verification
|
|
187
37
|
some common issues (photo or video can't be displayed, audio missing), take below actions to verify
|
|
188
|
-
### preview
|
|
189
|
-
- screenshot some key frames, and understand image to verify
|
|
38
|
+
### preview
|
|
39
|
+
- take screenshot for some key frames in player, and understand image to verify intent
|
|
40
|
+
|
|
190
41
|
### final video
|
|
191
|
-
- screenshot some key frames, and understand image to verify
|
|
42
|
+
- screenshot some key frames, and understand image to verify intent
|
|
192
43
|
- stt the final video audio, and verify if vtt result is correct
|
|
193
44
|
|
|
194
45
|
|
|
@@ -197,13 +48,18 @@ some common issues (photo or video can't be displayed, audio missing), take belo
|
|
|
197
48
|
| Topic | File |
|
|
198
49
|
|-------|------|
|
|
199
50
|
| 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
51
|
| Label system (browse, label, export labels.json) | [docs/label-mode.md](docs/label-mode.md) |
|
|
208
52
|
| Player servers (label + edit mode) | [docs/edit-mode.md](docs/edit-mode.md) |
|
|
209
|
-
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## common used components
|
|
56
|
+
- `react-markdown` — render markdown content, use plugins to extend functionality
|
|
57
|
+
- `remark-gfm` — support GitHub Flavored Markdown (tables, strikethrough, task lists)
|
|
58
|
+
- `remark-toc` — generate table of contents
|
|
59
|
+
- `remark-math` — support math formulas
|
|
60
|
+
- `rehype-katex` — render math formulas with KaTeX
|
|
61
|
+
- `remark-mermaidjs` — render mermaid diagrams in
|
|
62
|
+
- `react-markdown-mermaid` — render mermaid diagrams in standalone mode (no need to install mermaid separately)
|
|
63
|
+
|
|
64
|
+
- `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
|
|
65
|
+
- `@remotion/starburst` — render starburst animations
|
|
@@ -707,6 +707,18 @@ Use `markcut verify` to parse and validate a descriptive markdown file without r
|
|
|
707
707
|
markcut verify courseware.md
|
|
708
708
|
```
|
|
709
709
|
|
|
710
|
+
# common used components
|
|
711
|
+
- `react-markdown` — render markdown content, use plugins to extend functionality
|
|
712
|
+
- `remark-gfm` — support GitHub Flavored Markdown (tables, strikethrough, task lists)
|
|
713
|
+
- `remark-toc` — generate table of contents
|
|
714
|
+
- `remark-math` — support math formulas
|
|
715
|
+
- `rehype-katex` — render math formulas with KaTeX
|
|
716
|
+
- `remark-mermaidjs` — render mermaid diagrams in
|
|
717
|
+
- `react-markdown-mermaid` — render mermaid diagrams in standalone mode (no need to install mermaid separately)
|
|
718
|
+
|
|
719
|
+
- `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
|
|
720
|
+
- `@remotion/starburst` — render starburst animations
|
|
721
|
+
|
|
710
722
|
## Example
|
|
711
723
|
|
|
712
724
|
```md
|
package/package.json
CHANGED
package/src/entry.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { AbsoluteFill, continueRender, delayRender } from "remotion";
|
|
2
|
+
import { AbsoluteFill, continueRender, delayRender, staticFile } from "remotion";
|
|
3
|
+
import { ensureSharedImportMap } from "./utils/component-import-map";
|
|
3
4
|
import { ComposeContext, EventProvider, type ComposeContextValue } from "./context/index";
|
|
4
5
|
|
|
5
6
|
import { FolderLeaf } from "./types/Folder";
|
|
@@ -58,7 +59,12 @@ function useComponentRegistry(imports: unknown): Record<string, React.ComponentT
|
|
|
58
59
|
if (!handleRef.current) {
|
|
59
60
|
handleRef.current = delayRender("Loading component registry: " + imports);
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
+
// Absolute URLs and "/"-rooted paths (preview server) load as-is;
|
|
63
|
+
// relative paths (render CLI stages bundles into publicDir) resolve
|
|
64
|
+
// through staticFile so the Remotion render server can serve them.
|
|
65
|
+
const moduleUrl = /^(https?:|data:|blob:|\/)/.test(imports) ? imports : staticFile(imports);
|
|
66
|
+
ensureSharedImportMap();
|
|
67
|
+
import(/* webpackIgnore: true */ moduleUrl)
|
|
62
68
|
.then((mod: any) => {
|
|
63
69
|
// The bundle exports all components as named exports
|
|
64
70
|
setRegistry(mod.default ?? mod);
|
package/src/render/cli.mjs
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* (TTS/STT/durations)
|
|
8
8
|
*/
|
|
9
9
|
import { execSync, spawn } from "node:child_process";
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
11
|
-
import {
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { resolve, dirname, join, extname } from "node:path";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -111,11 +112,42 @@ export function parseArgs(argv) {
|
|
|
111
112
|
*
|
|
112
113
|
* Use --verbose to see every frame line (original behavior).
|
|
113
114
|
*/
|
|
115
|
+
/**
|
|
116
|
+
* Stage local absolute-path assets (TTS audio, TTI/TTV media, subtitles from
|
|
117
|
+
* .markcut/) into ROOT/public/.render-assets/ and rewrite srcs to relative
|
|
118
|
+
* paths, so `npx remotion render` (publicDir = ROOT/public) can serve them.
|
|
119
|
+
* The preview server handles these paths via multi-root serving; the render
|
|
120
|
+
* CLI needs them staged into the one public dir Remotion knows about.
|
|
121
|
+
*/
|
|
122
|
+
function stageLocalAssets(tree) {
|
|
123
|
+
const assetsDir = join(ROOT, "public", ".render-assets");
|
|
124
|
+
const seen = new Map();
|
|
125
|
+
const walk = (node) => {
|
|
126
|
+
if (!node || typeof node !== "object") return;
|
|
127
|
+
if (typeof node.src === "string" && node.src.startsWith("/") && !node.src.startsWith("//") && existsSync(node.src)) {
|
|
128
|
+
let staged = seen.get(node.src);
|
|
129
|
+
if (!staged) {
|
|
130
|
+
const name = createHash("md5").update(node.src).digest("hex").slice(0, 12) + extname(node.src);
|
|
131
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
132
|
+
copyFileSync(node.src, join(assetsDir, name));
|
|
133
|
+
staged = `.render-assets/${name}`;
|
|
134
|
+
seen.set(node.src, staged);
|
|
135
|
+
}
|
|
136
|
+
node.src = staged;
|
|
137
|
+
}
|
|
138
|
+
for (const value of Object.values(node)) {
|
|
139
|
+
if (value && typeof value === "object") walk(value);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
walk(tree);
|
|
143
|
+
return tree;
|
|
144
|
+
}
|
|
145
|
+
|
|
114
146
|
function renderOne(streamTree, aspect, outputPath, verbose) {
|
|
115
147
|
const dims = ASPECTS[aspect];
|
|
116
148
|
if (!dims) throw new Error(`Unknown aspect: ${aspect}`);
|
|
117
149
|
|
|
118
|
-
const adapted = { ...streamTree, width: dims.width, height: dims.height };
|
|
150
|
+
const adapted = stageLocalAssets(JSON.parse(JSON.stringify({ ...streamTree, width: dims.width, height: dims.height })));
|
|
119
151
|
const tmpProps = join(ROOT, ".tmp", `render-${aspect}.json`);
|
|
120
152
|
mkdirSync(dirname(tmpProps), { recursive: true });
|
|
121
153
|
writeFileSync(tmpProps, JSON.stringify({ root: adapted }));
|
|
@@ -262,10 +294,12 @@ async function main() {
|
|
|
262
294
|
|
|
263
295
|
if (args.command === "render") {
|
|
264
296
|
let streamTree;
|
|
297
|
+
let rawInput = "";
|
|
265
298
|
|
|
266
299
|
if (args.file) {
|
|
267
300
|
const filePath = resolve(args.file);
|
|
268
301
|
const raw = readFileSync(filePath, "utf-8");
|
|
302
|
+
rawInput = raw;
|
|
269
303
|
|
|
270
304
|
// Helper: all generated artifacts live under .markcut/generated/
|
|
271
305
|
function generatedDir(filePath, sub) {
|
|
@@ -285,14 +319,14 @@ async function main() {
|
|
|
285
319
|
// Helper: resolve variants from the parsed descriptive root
|
|
286
320
|
async function resolveWithVariants(descriptive, options) {
|
|
287
321
|
const { resolveVariantOverrides, compileDescriptiveRoot, resolveAll } = await import("../player/pipeline.mjs");
|
|
288
|
-
const {
|
|
322
|
+
const { parseMarkdownVariants } = await import("../player/pipeline.mjs");
|
|
289
323
|
|
|
290
324
|
let resolved = descriptive;
|
|
291
325
|
|
|
292
326
|
// If variants are specified, apply variant-prefixed overrides
|
|
293
327
|
if (args.variant && args.variant.length > 0) {
|
|
294
328
|
// Also apply root config overrides from variant sections
|
|
295
|
-
const parsed =
|
|
329
|
+
const parsed = parseMarkdownVariants(raw);
|
|
296
330
|
const variantRoot = parsed.variants.get(args.variant[0]);
|
|
297
331
|
if (variantRoot) {
|
|
298
332
|
// Merge variant root config into base (tts, stt, width, height, etc.)
|
|
@@ -308,9 +342,9 @@ async function main() {
|
|
|
308
342
|
}
|
|
309
343
|
|
|
310
344
|
if (filePath.endsWith(".md")) {
|
|
311
|
-
const {
|
|
345
|
+
const { parseMarkdownVariants } = await import("../player/pipeline.mjs");
|
|
312
346
|
const fileDir = dirname(filePath);
|
|
313
|
-
const parsed =
|
|
347
|
+
const parsed = parseMarkdownVariants(raw);
|
|
314
348
|
streamTree = await resolveWithVariants(parsed.base, {
|
|
315
349
|
baseDir: fileDir,
|
|
316
350
|
scriptOutputDir: generatedDir(filePath, "tts"),
|
|
@@ -340,6 +374,27 @@ async function main() {
|
|
|
340
374
|
process.exit(1);
|
|
341
375
|
}
|
|
342
376
|
|
|
377
|
+
// Bundle the ```js imports``` component block (the preview server does this
|
|
378
|
+
// post-compile; the render CLI must do it too, otherwise jsx components are
|
|
379
|
+
// unregistered and slides render as unstyled raw text).
|
|
380
|
+
if (!streamTree.imports) {
|
|
381
|
+
const fenceMatch = rawInput.match(/^(```|~~~)\s*js imports\s*\n([\s\S]*?)^\1\s*$/m);
|
|
382
|
+
const rawSource = fenceMatch ? fenceMatch[2] : (streamTree.importsBlock ?? null);
|
|
383
|
+
if (rawSource && rawSource.trim()) {
|
|
384
|
+
const { parseImportsBlock, extractDependencySpecs } = await import("../player/pipeline.mjs");
|
|
385
|
+
const { bundleFromEntries } = await import("../player/bundler.mjs");
|
|
386
|
+
const entries = parseImportsBlock(rawSource);
|
|
387
|
+
const extraSpecs = extractDependencySpecs(rawSource);
|
|
388
|
+
const bundleDir = join(ROOT, "public", ".render-assets");
|
|
389
|
+
mkdirSync(bundleDir, { recursive: true });
|
|
390
|
+
const bundle = await bundleFromEntries(entries, extraSpecs, rawSource, bundleDir);
|
|
391
|
+
if (bundle.url) {
|
|
392
|
+
streamTree.imports = ".render-assets/" + bundle.url.split("/").pop();
|
|
393
|
+
console.log(` \u2705 components \u2192 ${bundle.exports.join(", ")}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
343
398
|
const output = args.output ? resolve(args.output) : join(ROOT, "out", "video.mp4");
|
|
344
399
|
await renderOne(streamTree, "16x9", output, args.verbose);
|
|
345
400
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared-module import map for dynamically loaded component bundles.
|
|
3
|
+
*
|
|
4
|
+
* Component bundles are built with react/react-dom/remotion marked as
|
|
5
|
+
* --external (see player/bundler.mjs getSharedExternals), so their output
|
|
6
|
+
* contains bare imports like `import { useState } from "react"`. The page
|
|
7
|
+
* that dynamically imports such a bundle must provide an import map that
|
|
8
|
+
* resolves those bare specifiers back to the already-loaded module
|
|
9
|
+
* instances — otherwise the import fails ("Failed to resolve module
|
|
10
|
+
* specifier") or, worse, a duplicate React breaks hooks and context.
|
|
11
|
+
*
|
|
12
|
+
* The preview player injects this map itself (player/browser.tsx). This
|
|
13
|
+
* helper provides the same shim for the Remotion render page (headless
|
|
14
|
+
* Chrome during `markcut render`). Both use the `#rmtr-import-map` guard,
|
|
15
|
+
* so whichever runs first wins and the other becomes a no-op.
|
|
16
|
+
*/
|
|
17
|
+
import * as React from "react";
|
|
18
|
+
import * as ReactDOM from "react-dom";
|
|
19
|
+
import * as Remotion from "remotion";
|
|
20
|
+
|
|
21
|
+
export function ensureSharedImportMap(): void {
|
|
22
|
+
if (typeof document === "undefined") return;
|
|
23
|
+
if (document.querySelector("#rmtr-import-map")) return;
|
|
24
|
+
|
|
25
|
+
const g = globalThis as any;
|
|
26
|
+
g.__remotionShared ??= {};
|
|
27
|
+
g.__remotionShared["react"] ??= React;
|
|
28
|
+
g.__remotionShared["react-dom"] ??= ReactDOM;
|
|
29
|
+
g.__remotionShared["remotion"] ??= Remotion;
|
|
30
|
+
|
|
31
|
+
// Stub for node:* imports that some component deps reference at module
|
|
32
|
+
// level for build-time helpers never called during rendering.
|
|
33
|
+
if (!g.__nodeModuleStub) {
|
|
34
|
+
const nodeModuleStubCode = `export function createRequire() { return () => ({ resolve: () => { throw new Error("node:module.createRequire is not available in the browser"); } }); }`;
|
|
35
|
+
g.__nodeModuleStub = URL.createObjectURL(
|
|
36
|
+
new Blob([nodeModuleStubCode], { type: "application/javascript" }),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const imports: Record<string, string> = {};
|
|
42
|
+
for (const spec of ["node:module", "node:fs", "node:path", "node:os"]) {
|
|
43
|
+
imports[spec] = g.__nodeModuleStub;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const [specifier, mod] of Object.entries(g.__remotionShared) as [string, any][]) {
|
|
47
|
+
const lines: string[] = [
|
|
48
|
+
`const _mod = globalThis.__remotionShared[${JSON.stringify(specifier)}];`,
|
|
49
|
+
];
|
|
50
|
+
for (const name of Object.keys(mod)) {
|
|
51
|
+
if (name === "default") continue;
|
|
52
|
+
lines.push(
|
|
53
|
+
`const __${name} = _mod[${JSON.stringify(name)}];`,
|
|
54
|
+
`export { __${name} as ${name} };`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if ("default" in mod) lines.push("export default _mod.default;");
|
|
58
|
+
imports[specifier] = URL.createObjectURL(
|
|
59
|
+
new Blob([lines.join("\n")], { type: "application/javascript" }),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// react/jsx-runtime — built manually because it's a sub-path of react.
|
|
64
|
+
const blobJsx = new Blob(
|
|
65
|
+
[
|
|
66
|
+
`
|
|
67
|
+
const R = globalThis.__remotionShared["react"];
|
|
68
|
+
const { createElement, Fragment } = R;
|
|
69
|
+
export { Fragment };
|
|
70
|
+
export function jsx(type, props, key) {
|
|
71
|
+
return createElement(type, key != null ? { ...props, key } : props);
|
|
72
|
+
}
|
|
73
|
+
export function jsxs(type, props, key) {
|
|
74
|
+
return createElement(type, key != null ? { ...props, key } : props);
|
|
75
|
+
}
|
|
76
|
+
export function jsxDEV(type, props, key, isStaticChildren, source, self) {
|
|
77
|
+
return createElement(type, key != null ? { ...props, key } : props);
|
|
78
|
+
}
|
|
79
|
+
`,
|
|
80
|
+
],
|
|
81
|
+
{ type: "application/javascript" },
|
|
82
|
+
);
|
|
83
|
+
imports["react/jsx-runtime"] = URL.createObjectURL(blobJsx);
|
|
84
|
+
|
|
85
|
+
const script = document.createElement("script");
|
|
86
|
+
script.type = "importmap";
|
|
87
|
+
script.id = "rmtr-import-map";
|
|
88
|
+
script.textContent = JSON.stringify({ imports });
|
|
89
|
+
document.head.appendChild(script);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.warn("Failed to create shared module import map:", e);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: courseware
|
|
3
|
+
description: Turn a topic or teaching material into a professional slide-based course video with TTS narration, bullet-reveal slides, and a reviewer quality gate.
|
|
4
|
+
when-to-use: lessons, lectures, training, tutorials, explainers — 1-5 minute educational videos built from slides + narration
|
|
5
|
+
engine: "@lalalic/markcut — run via `npx @lalalic/markcut`"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Courseware Template
|
|
9
|
+
|
|
10
|
+
Follow this file top to bottom. It defines **what** a professional courseware video is and **how** to produce one. It does not re-teach the engine — markcut markdown syntax comes from the markcut skill (`SKILL.md` → `docs/markdown-descriptive.md`). Read those first if you have not.
|
|
11
|
+
|
|
12
|
+
Layout of this template package:
|
|
13
|
+
|
|
14
|
+
| Path | Runs in | Purpose |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `TEMPLATE.md` | your context | everything: inputs, structure, rules, workflow, quality gate |
|
|
17
|
+
| `prompts/*.md` | **your own (orchestrator) context** | fill-in prompt templates you execute yourself |
|
|
18
|
+
| `agents/*.md` | **separate agent session** | subagent definitions (system prompt + task template) |
|
|
19
|
+
|
|
20
|
+
## 0. Prerequisites
|
|
21
|
+
|
|
22
|
+
- markcut skill loaded (engine contract: `SKILL.md` + `docs/markdown-descriptive.md`)
|
|
23
|
+
- `npx @lalalic/markcut` runnable
|
|
24
|
+
- TTS CLI available (default: `edge-tts`)
|
|
25
|
+
- For the reviewer gate: `ffmpeg`/`ffprobe`, an STT CLI (e.g. whisper), and image-understanding capability
|
|
26
|
+
|
|
27
|
+
## 1. Inputs — collect before starting
|
|
28
|
+
|
|
29
|
+
| Input | Required | Default | Notes |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| Topic / source material | **yes** | — | plain topic, outline, doc, or article |
|
|
32
|
+
| Audience level | no | beginner | beginner / intermediate / expert |
|
|
33
|
+
| Language(s) | no | en | extra languages become variants (see §3) |
|
|
34
|
+
| Target duration | no | 2 min | 1–5 min |
|
|
35
|
+
| Voice per language | no | en: `en-US-GuyNeural`, zh: `zh-CN-YunxiNeural` | edge-tts voice names |
|
|
36
|
+
| Accent color | no | `#61dafb` | theme knob, see §4 |
|
|
37
|
+
|
|
38
|
+
**Rule:** if the topic/material is missing or ambiguous, ask the user. Never invent course content beyond the given material plus well-established knowledge of the topic. Never invent facts, numbers, or citations.
|
|
39
|
+
|
|
40
|
+
## 2. Scene grammar — mandatory structure
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
# video ← root: width:1920 height:1080 fps:30 layout:series
|
|
44
|
+
│ subtitle:{fontSize:"20px"} tts:"<edge-tts CLI template>"
|
|
45
|
+
├── ## Hook ← 3–5s attention grabber, fixed duration
|
|
46
|
+
├── ## Slides ← layout:transitionSeries transition:fade(0.5)
|
|
47
|
+
│ ├── ### TitleSlide ← transitionSeries, bullet-reveal (1 beat)
|
|
48
|
+
│ ├── ### <Concept> × 3–6 ← transitionSeries, bullet-reveal (1 beat per bullet)
|
|
49
|
+
│ └── ### Summary ← transitionSeries, bullet-reveal (1 beat per takeaway)
|
|
50
|
+
└── ## Thanks ← fixed duration:6, no script
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Scene rules:
|
|
54
|
+
|
|
55
|
+
- **Hook**: `- image duration:3 prompt:"<vivid visual, cinematic>"` (or `video`). If no TTI/TTV CLI is configured, use a `<Slide>` hook with a provocative question instead — never leave a broken `prompt:` node.
|
|
56
|
+
- **Every scene inside Slides (TitleSlide, concepts, Summary)** uses the **bullet-reveal pattern** — this is mandatory, not optional:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
### <SceneName>
|
|
60
|
+
layout:transitionSeries transition:fade(0.5)
|
|
61
|
+
- component id:<shortId> isBackground:true jsx:"<Slide current={current}>{source}</Slide>"
|
|
62
|
+
~~~md source
|
|
63
|
+
## Title
|
|
64
|
+
- 🎯 **Key point 1** — elaboration
|
|
65
|
+
- 🎯 **Key point 2** — elaboration
|
|
66
|
+
- 🎯 **Key point 3** — elaboration
|
|
67
|
+
~~~
|
|
68
|
+
- script "Paragraph expanding bullet 1..." on:(start, <shortId>.current=1)
|
|
69
|
+
- script "Paragraph expanding bullet 2..." on:(start, <shortId>.current=2)
|
|
70
|
+
- script "Paragraph expanding bullet 3..." on:(start, <shortId>.current=3)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `id` must be unique per scene (e.g. `t`, `c1`, `c2`, `s`). The `id` connects the component to the script events.
|
|
74
|
+
- `current={current}` on the Slide component reads the event-driven bullet index and highlights the matching `<li>`.
|
|
75
|
+
- Each `- script "..."` node is one beat in the `transitionSeries`, so it gets a fade transition. The TTS duration of that script determines the segment length.
|
|
76
|
+
- The component has `isBackground:true` so its `<Slide>` spans all beats of the scene.
|
|
77
|
+
- **Beat count = bullet count**: TitleSlide gets 1 beat (just the welcome line), concept scenes get 1 beat per bullet, Summary gets 1 beat per row in the comparison table or per major takeaway.
|
|
78
|
+
|
|
79
|
+
- **Thanks**: fixed-visual, `duration:6`, no script.
|
|
80
|
+
|
|
81
|
+
- **Mermaid diagrams inside slides**: Fenced mermaid code blocks (` ```mermaid `) inside `~~~md source` are rendered by the `<Slide>` component (see §4). Use for flowcharts, architecture diagrams, sequence diagrams, timeline visualizations. The diagram renders as inline SVG — place it after bullet points or on its own.
|
|
82
|
+
|
|
83
|
+
- **Duration is TTS-driven**: never set explicit `duration` on scenes that have a script. Only fixed-visual scenes (Hook, Thanks) get `duration:`.
|
|
84
|
+
|
|
85
|
+
## 3. Authoring rules — the professional bar
|
|
86
|
+
|
|
87
|
+
Slide content:
|
|
88
|
+
|
|
89
|
+
- One idea per slide. ≤ 6 bullets. Bullet shape: `🔹 **bold key phrase** — short elaboration`.
|
|
90
|
+
- Emoji bullet prefixes for scannability (🤖 📊 🔄 🏷️ 💡 ⚡ …), consistent within the course.
|
|
91
|
+
- Use a markdown **table** whenever content is comparative (applications, methods, milestones).
|
|
92
|
+
- Use a `>` quote block for the one memorable takeaway line.
|
|
93
|
+
- Code blocks only when actually teaching code; keep ≤ 10 lines per slide.
|
|
94
|
+
|
|
95
|
+
Narration script:
|
|
96
|
+
|
|
97
|
+
- **One short paragraph per bullet.** Never merge bullets into a single monolithic script. Each bullet gets its own `- script "..."` node with `on:(start, id.current=N)`.
|
|
98
|
+
- Paragraph length ≈ **15–30 words** per beat (≈6–12 seconds at 2.5 w/s). Total words = target minutes × 150.
|
|
99
|
+
- Each paragraph **expands** that bullet — never reads it verbatim. The paragraph plus all sibling paragraphs must touch every bullet on the slide.
|
|
100
|
+
- The concrete example gets its own bullet and its own script paragraph. Never bury the example in a generic paragraph.
|
|
101
|
+
- Bullet ordering: concepts first (what/why), example last (concrete reinforcement). The example is the final beat before advancing to the next scene.
|
|
102
|
+
- Summary: each row in the comparison table gets one beat. The final beat closes the course ("Thank you for watching…").
|
|
103
|
+
|
|
104
|
+
Mermaid diagrams:
|
|
105
|
+
|
|
106
|
+
- Use ` ```mermaid ` fenced code blocks inside `~~~md source` for charts, flowcharts, architecture diagrams, sequence diagrams, and timelines.
|
|
107
|
+
- Place mermaid blocks after the bullet list, separated by a blank line.
|
|
108
|
+
- The Slide component (see §4) renders mermaid code blocks as inline SVG. Ensure the diagram fits within the slide (keep node labels short, max 1–2 sentences per node).
|
|
109
|
+
- For complex diagrams that take time to render, pre-render them as SVG files via `mmdc` (mermaid-cli) or your TTI CLI, and reference as `- image src:...` instead — this is more reliable for final render.
|
|
110
|
+
|
|
111
|
+
Multi-language (variants):
|
|
112
|
+
|
|
113
|
+
- For each extra language: a `<lang>:"..."` twin on every script node, a `~~~md <lang>-source` fence beside every `~~~md source`, and a `# <lang>` root block at the end of the file carrying that language's `tts:` voice.
|
|
114
|
+
- See the worked example in §7 for the exact shape (`zh` variant).
|
|
115
|
+
|
|
116
|
+
## 4. Components & styles — canonical, copy verbatim
|
|
117
|
+
|
|
118
|
+
Copy both blocks into the generated `course.md` unchanged (except the theme knobs below).
|
|
119
|
+
|
|
120
|
+
~~~~text
|
|
121
|
+
~~~js imports
|
|
122
|
+
import ReactMarkdown from 'react-markdown';
|
|
123
|
+
import remarkGfm from 'remark-gfm'
|
|
124
|
+
import mermaid from 'mermaid'
|
|
125
|
+
import { delayRender, continueRender } from 'remotion'
|
|
126
|
+
|
|
127
|
+
// Initialize mermaid once. Theme matches the slide deck's dark scheme.
|
|
128
|
+
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' })
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Renders a mermaid diagram as inline SVG.
|
|
132
|
+
* Uses Remotion's delayRender/continueRender so the diagram is guaranteed
|
|
133
|
+
* to be ready before the frame is captured.
|
|
134
|
+
*/
|
|
135
|
+
export function Mermaid({ source }) {
|
|
136
|
+
const ref = React.useRef(null)
|
|
137
|
+
|
|
138
|
+
React.useEffect(() => {
|
|
139
|
+
if (!source || !ref.current) return
|
|
140
|
+
const handle = delayRender('Mermaid rendering')
|
|
141
|
+
mermaid.render('mmd-' + Math.random().toString(36).slice(2), source)
|
|
142
|
+
.then((result) => {
|
|
143
|
+
if (ref.current) ref.current.innerHTML = result.svg
|
|
144
|
+
continueRender(handle)
|
|
145
|
+
})
|
|
146
|
+
.catch((err) => {
|
|
147
|
+
console.error('Mermaid error:', err)
|
|
148
|
+
continueRender(handle)
|
|
149
|
+
})
|
|
150
|
+
}, [source])
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
ref={ref}
|
|
155
|
+
style={{ width: '100%', maxWidth: 960, margin: '20px auto' }}
|
|
156
|
+
/>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Courseware slide component.
|
|
162
|
+
* - Renders markdown via react-markdown
|
|
163
|
+
* - Highlights the Nth bullet when `current=N`
|
|
164
|
+
* - Renders mermaid code blocks ( ```mermaid ...``` ) as inline diagrams
|
|
165
|
+
*/
|
|
166
|
+
export function Slide({ current = 0, children }) {
|
|
167
|
+
let idx = 1
|
|
168
|
+
return (
|
|
169
|
+
<div className="slide">
|
|
170
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}
|
|
171
|
+
components={{
|
|
172
|
+
li: ({ children }) => {
|
|
173
|
+
const highlight = idx === current; idx++
|
|
174
|
+
return <li className={highlight ? 'highlight' : ''}>{children}</li>
|
|
175
|
+
},
|
|
176
|
+
// Fenced code blocks — detect mermaid, render as diagram
|
|
177
|
+
pre: ({ children }) => {
|
|
178
|
+
const code = React.Children.toArray(children)[0]
|
|
179
|
+
if (code?.props?.className === 'language-mermaid') {
|
|
180
|
+
return <Mermaid source={String(code.props.children)} />
|
|
181
|
+
}
|
|
182
|
+
return <pre>{children}</pre>
|
|
183
|
+
},
|
|
184
|
+
}}>{children}</ReactMarkdown>
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
~~~
|
|
189
|
+
~~~~
|
|
190
|
+
|
|
191
|
+
~~~~text
|
|
192
|
+
~~~css stylesheet
|
|
193
|
+
/* Container sizing for a perfect 16:9 widescreen presentation slide */
|
|
194
|
+
.slide {
|
|
195
|
+
color: #f5f5f7;
|
|
196
|
+
padding: 40px;
|
|
197
|
+
display: flex;
|
|
198
|
+
flex-direction: column;
|
|
199
|
+
justify-content: center;
|
|
200
|
+
align-items: flex-start;
|
|
201
|
+
font-family: 'Helvetica Neue', Arial, sans-serif;
|
|
202
|
+
font-size: 30px;
|
|
203
|
+
|
|
204
|
+
/* Typography rules optimized for visibility from a distance */
|
|
205
|
+
h1 {
|
|
206
|
+
font-size: 2.8em;
|
|
207
|
+
color: #61dafb;
|
|
208
|
+
margin-top: 0;
|
|
209
|
+
margin-bottom: 20px;
|
|
210
|
+
line-height: 1.2;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
h2 {
|
|
214
|
+
font-size: 2em;
|
|
215
|
+
color: #a8dadc;
|
|
216
|
+
margin-top: 0;
|
|
217
|
+
margin-bottom: 15px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
p {
|
|
221
|
+
font-size: 1.3em;
|
|
222
|
+
line-height: 1.6;
|
|
223
|
+
color: #e0e0e6;
|
|
224
|
+
margin-bottom: 15px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* List styling specific to presentation bullet points */
|
|
228
|
+
ul, ol {
|
|
229
|
+
margin-left: 25px;
|
|
230
|
+
margin-bottom: 20px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
li {
|
|
234
|
+
font-size: 1.3em;
|
|
235
|
+
line-height: 1.8;
|
|
236
|
+
margin-bottom: 10px;
|
|
237
|
+
color: #e0e0e6;
|
|
238
|
+
list-style-type: none; /* Remove default bullets for custom styling */
|
|
239
|
+
|
|
240
|
+
/* Highlighted list item for event-driven bullet reveal */
|
|
241
|
+
&.highlight {
|
|
242
|
+
color: red;
|
|
243
|
+
font-weight: 700;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Code block handling inside slides */
|
|
248
|
+
pre {
|
|
249
|
+
background-color: #2d2d34;
|
|
250
|
+
padding: 15px;
|
|
251
|
+
border-radius: 6px;
|
|
252
|
+
width: 100%;
|
|
253
|
+
box-sizing: border-box;
|
|
254
|
+
overflow-x: auto;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
code {
|
|
258
|
+
font-family: 'Courier New', Courier, monospace;
|
|
259
|
+
font-size: 1.1em;
|
|
260
|
+
color: #ffb703;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
table {
|
|
264
|
+
width: 100%;
|
|
265
|
+
border-collapse: collapse;
|
|
266
|
+
margin-bottom: 20px;
|
|
267
|
+
|
|
268
|
+
th, td {
|
|
269
|
+
border: 1px solid #444;
|
|
270
|
+
padding: 10px;
|
|
271
|
+
text-align: left;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
~~~
|
|
276
|
+
~~~~
|
|
277
|
+
|
|
278
|
+
Theme knobs — change **values only**, never the structure:
|
|
279
|
+
|
|
280
|
+
| Knob | Selector | Default |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| Accent (h1) | `.slide h1 { color }` | `#61dafb` |
|
|
283
|
+
| Secondary (h2) | `.slide h2 { color }` | `#a8dadc` |
|
|
284
|
+
| Reveal highlight | `.slide li.highlight { color }` | `red` |
|
|
285
|
+
| Deck background | `.slide { background }` | transparent |
|
|
286
|
+
| Font | `.slide { font-family }` | Helvetica Neue |
|
|
287
|
+
|
|
288
|
+
## 5. Workflow
|
|
289
|
+
|
|
290
|
+
Prompts in `prompts/` are fill-in templates: replace every `{placeholder}`, then execute the prompt **in your own context**. `agents/` definitions run in a **separate session** (e.g. a fresh-context subagent).
|
|
291
|
+
|
|
292
|
+
1. **Outline** — fill `prompts/outline.md` → course outline. **Present the outline to the user and get confirmation** before continuing.
|
|
293
|
+
2. **Scenes** — for each outline section, fill `prompts/scene.md` → slide source + narration script (all languages).
|
|
294
|
+
3. **Assemble** — write `course.md`: §2 grammar, §3 rules, §4 blocks verbatim, root config line with the user's width/height/fps/voice.
|
|
295
|
+
4. **Render** — `npx @lalalic/markcut render course.md`. On engine errors: fix and re-render, max 3 attempts per error, then ask the user.
|
|
296
|
+
5. **Review (quality gate)** — run `agents/reviewer.md` in a fresh separate session. Give it absolute paths to: `course.md`, the rendered MP4, this `TEMPLATE.md`, plus target duration and language(s). It returns `{verdict, findings[]}` and never edits anything.
|
|
297
|
+
6. **Fix loop** — on FAIL: fill `prompts/fix.md` with the findings, apply the edits yourself, go back to step 4. Max **3** review iterations, then escalate to the user with the open findings.
|
|
298
|
+
|
|
299
|
+
## 6. Quality gate — exit criteria
|
|
300
|
+
|
|
301
|
+
Done only when ALL hold:
|
|
302
|
+
|
|
303
|
+
- [ ] reviewer verdict = `PASS` (zero blocker/major findings)
|
|
304
|
+
- [ ] total duration within ±15% of target
|
|
305
|
+
- [ ] structure matches §2 (hook, title, 3–6 concepts, summary, thanks)
|
|
306
|
+
- [ ] no blank/black frames at scene boundaries; slides legible at target resolution
|
|
307
|
+
- [ ] STT transcript of rendered audio matches scripts (≥90% content match)
|
|
308
|
+
|
|
309
|
+
## 7. Reference — worked example
|
|
310
|
+
|
|
311
|
+
Golden example: [`tests/fixtures/templates/courseware.md`](../../tests/fixtures/templates/courseware.md) — a bilingual (en + zh) "Introduction to Machine Learning" course demonstrating every rule above: hook with generated image, bullet-reveal with `on:(start, slide1.current=N)`, comparative tables, summary with selection guide, and the `# zh` variant block.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: courseware-reviewer
|
|
3
|
+
description: Read-only quality gate for videos produced from the courseware template. Runs in a separate session with fresh context.
|
|
4
|
+
context: fresh
|
|
5
|
+
mode: read-only
|
|
6
|
+
tools: read file, bash (ffmpeg/ffprobe, STT CLI), image understanding
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# System prompt
|
|
10
|
+
|
|
11
|
+
You are a strict, evidence-based QA reviewer for slide-based course videos. You review; you **never edit** any file. Every finding must cite concrete evidence (a frame image, an STT excerpt, a line in the source, a measured number). If you cannot verify a check, report it as a finding with severity `minor` and say why — never silently skip it.
|
|
12
|
+
|
|
13
|
+
## Inputs (provided in the task)
|
|
14
|
+
|
|
15
|
+
- `course_md` — absolute path to the generated courseware markdown
|
|
16
|
+
- `mp4` — absolute path to the rendered video
|
|
17
|
+
- `template_md` — absolute path to the courseware `TEMPLATE.md` (the rulebook: §2 grammar, §3 authoring rules, §6 exit criteria)
|
|
18
|
+
- `duration_min` — target duration in minutes
|
|
19
|
+
- `languages` — expected language(s)
|
|
20
|
+
|
|
21
|
+
## Procedure
|
|
22
|
+
|
|
23
|
+
### 1. Static review (source vs rulebook)
|
|
24
|
+
|
|
25
|
+
Read `template_md` §2/§3, then check `course_md`:
|
|
26
|
+
|
|
27
|
+
- structure: hook → title → 3–6 concepts → summary → thanks
|
|
28
|
+
- every concept scene: `layout:parallel` (or `transitionSeries` when bullet-reveal), one `isBackground:true` slide component + script node(s)
|
|
29
|
+
- bullet-reveal wiring: `id:slideN` ↔ `on:(start, slideN.current=K)` pairs are consistent and in order
|
|
30
|
+
- slide rules: ≤6 bullets, bold-key-phrase shape, table where comparative, one idea per slide
|
|
31
|
+
- script rules: word count ≈ scene budget (2.5 w/s), expands rather than reads the slide, concrete example present per concept
|
|
32
|
+
- imports/stylesheet blocks match the template canon (only theme-knob values may differ)
|
|
33
|
+
- language variants complete and mirrored (every script has its `<lang>:` twin, every source its `<lang>-source`)
|
|
34
|
+
- factual sanity: no obviously invented facts, numbers, or citations
|
|
35
|
+
|
|
36
|
+
### 2. Dynamic review (rendered artifact)
|
|
37
|
+
|
|
38
|
+
- `ffprobe` the MP4: duration (compare to `duration_min` ±15%), resolution, fps, audio stream present
|
|
39
|
+
- Extract frames: one at each scene midpoint plus each scene boundary ±0.5s (estimate boundaries from scene order and TTS-driven durations; evenly-spaced sampling of ~2× scene count is an acceptable fallback). For each frame, verify with image understanding:
|
|
40
|
+
- not blank/black
|
|
41
|
+
- it shows the expected slide for that timestamp (match heading/bullets against `course_md`)
|
|
42
|
+
- legibility: text within safe margins, no overflow/clipping, readable contrast
|
|
43
|
+
- Extract audio (`ffmpeg` → wav), transcribe with the STT CLI, and compare against the scripts in `course_md`: ≥90% content match, correct language, scene order correct
|
|
44
|
+
- Spot-check subtitle overlay in ≥2 frames if subtitles are configured
|
|
45
|
+
|
|
46
|
+
### 3. Report
|
|
47
|
+
|
|
48
|
+
Severity: `blocker` (broken output: blank frames, missing audio, wrong structure, unrendered scene) · `major` (professional-bar violation: slide/script mismatch, pacing off-budget >20%, illegible slide, missing example, variant out of sync) · `minor` (polish: emoji inconsistency, wording, small overflow).
|
|
49
|
+
|
|
50
|
+
## Output — exactly this JSON, nothing after it
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"verdict": "PASS | FAIL",
|
|
55
|
+
"measured": { "duration_s": 0, "target_s": 0, "resolution": "", "stt_match_pct": 0 },
|
|
56
|
+
"findings": [
|
|
57
|
+
{
|
|
58
|
+
"id": "F1",
|
|
59
|
+
"severity": "blocker | major | minor",
|
|
60
|
+
"scene": "<scene heading or 'global'>",
|
|
61
|
+
"check": "<which rule/check from the procedure>",
|
|
62
|
+
"issue": "<what is wrong>",
|
|
63
|
+
"evidence": "<frame path / STT excerpt / source line / measurement>",
|
|
64
|
+
"fix_hint": "<one-line suggestion for the orchestrator>"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`PASS` requires zero `blocker` and zero `major` findings.
|
|
71
|
+
|
|
72
|
+
# Task template (orchestrator fills and sends)
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Review a courseware video against its template.
|
|
76
|
+
|
|
77
|
+
course_md: {course_md}
|
|
78
|
+
mp4: {mp4}
|
|
79
|
+
template_md: {template_md}
|
|
80
|
+
duration_min: {duration_min}
|
|
81
|
+
languages: {languages}
|
|
82
|
+
|
|
83
|
+
Follow your procedure. Output the verdict JSON only.
|
|
84
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Prompt: Apply Reviewer Findings
|
|
2
|
+
|
|
3
|
+
> Fill every `{placeholder}`, then execute in your own (orchestrator) context.
|
|
4
|
+
> Used in TEMPLATE.md §5 step 6 after a FAIL verdict. You edit; the reviewer never does.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are fixing a courseware video that failed review. Apply the findings below to `{course_md_path}` with **minimal, targeted edits** — do not rewrite passing scenes.
|
|
9
|
+
|
|
10
|
+
Reviewer findings (JSON):
|
|
11
|
+
|
|
12
|
+
{findings_json}
|
|
13
|
+
|
|
14
|
+
Rules:
|
|
15
|
+
|
|
16
|
+
- Address every `blocker` and `major` finding. Address `minor` findings only when the fix is trivial and local.
|
|
17
|
+
- Each fix must respect TEMPLATE.md §2 scene grammar and §3 authoring rules — a fix that breaks a rule elsewhere is not a fix.
|
|
18
|
+
- Slide/script mismatch → prefer fixing the **script** (cheaper: TTS regenerates; slide layout stays validated).
|
|
19
|
+
- Pacing/duration findings → adjust script word counts toward budget (target duration × 150 words total).
|
|
20
|
+
- Blank/illegible frame findings → check the scene's node (missing `isBackground:true`, broken `src`/`prompt`, oversized content) before touching styles.
|
|
21
|
+
- Never modify the `~~~js imports` or `~~~css stylesheet` blocks except the documented theme knobs (TEMPLATE.md §4).
|
|
22
|
+
- Keep all language variants in sync: an edit to a script or slide must be mirrored in every `<lang>:`/`<lang>-source` twin.
|
|
23
|
+
|
|
24
|
+
After editing, list what changed:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
- <finding id>: <file location> — <one-line description of the edit>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then re-render and re-review (TEMPLATE.md §5 steps 4–5).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Prompt: Course Outline
|
|
2
|
+
|
|
3
|
+
> Fill every `{placeholder}`, then execute in your own (orchestrator) context.
|
|
4
|
+
> Output feeds `prompts/scene.md` and must be confirmed by the user before scene writing.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are an instructional designer planning a {duration_min}-minute video course.
|
|
9
|
+
|
|
10
|
+
Topic / source material:
|
|
11
|
+
|
|
12
|
+
{material}
|
|
13
|
+
|
|
14
|
+
Audience: {audience}
|
|
15
|
+
Language(s): {languages}
|
|
16
|
+
|
|
17
|
+
Requirements:
|
|
18
|
+
|
|
19
|
+
- Structure: 1 hook + 1 title slide + 3–6 concept sections + 1 summary.
|
|
20
|
+
- Total narration budget ≈ {duration_min} × 150 words. Allocate a word budget per section; the sum must fit the budget.
|
|
21
|
+
- Per concept section provide:
|
|
22
|
+
- title (≤ 5 words)
|
|
23
|
+
- 3–6 key points, each as `**bold key phrase** — short elaboration`
|
|
24
|
+
- one concrete example (real product, scenario, or story) — gets its own key point
|
|
25
|
+
- whether the content is comparative → table (if yes, name the columns)
|
|
26
|
+
- whether a mermaid diagram would help (flowchart, architecture, timeline)
|
|
27
|
+
- word budget
|
|
28
|
+
- Summary: one comparison table spanning all concepts (name the columns) + a selection/decision guide ("If X → use Y").
|
|
29
|
+
- Hook: one vivid visual idea usable as an image/video generation prompt, OR a provocative question if generation is unavailable.
|
|
30
|
+
- Stay strictly within the given material plus well-established knowledge. No invented facts, numbers, or citations.
|
|
31
|
+
|
|
32
|
+
Output format (markdown):
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
## Hook
|
|
36
|
+
- visual: <generation prompt or question>
|
|
37
|
+
|
|
38
|
+
## Concept 1: <title>
|
|
39
|
+
- points:
|
|
40
|
+
- **<key phrase>** — <elaboration>
|
|
41
|
+
- ... (last point = the concrete example)
|
|
42
|
+
- table: no | yes (<col1>, <col2>, ...)
|
|
43
|
+
- mermaid: no | yes (<type and description of diagram>)
|
|
44
|
+
- words: <n>
|
|
45
|
+
|
|
46
|
+
## Concept 2: ...
|
|
47
|
+
|
|
48
|
+
## Summary
|
|
49
|
+
- table columns: <dimension>, <per-concept columns...>
|
|
50
|
+
- guide:
|
|
51
|
+
- <condition> → <concept>
|
|
52
|
+
- words: <n>
|
|
53
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Prompt: Concept Scene (slide + script)
|
|
2
|
+
|
|
3
|
+
> Fill every `{placeholder}`, then execute in your own (orchestrator) context.
|
|
4
|
+
> Run once per outline section (TitleSlide, each Concept, Summary).
|
|
5
|
+
> Constraints below mirror TEMPLATE.md §2/§3 — the reviewer checks against them.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are writing one scene of a {duration_min}-minute course for a {audience} audience.
|
|
10
|
+
|
|
11
|
+
Outline section:
|
|
12
|
+
|
|
13
|
+
{outline_section}
|
|
14
|
+
|
|
15
|
+
Course context (for continuity — do not repeat content already covered):
|
|
16
|
+
|
|
17
|
+
{outline_full}
|
|
18
|
+
|
|
19
|
+
Produce, for language(s) {languages}:
|
|
20
|
+
|
|
21
|
+
**A. Slide source** (markdown, rendered by `<Slide>`):
|
|
22
|
+
|
|
23
|
+
- `##` heading = section title
|
|
24
|
+
- ≤ 6 bullets, shape: `<emoji> **bold key phrase** — short elaboration`
|
|
25
|
+
- consistent emoji set across the course: {emoji_set}
|
|
26
|
+
- markdown table if the outline marks this section comparative
|
|
27
|
+
- `>` quote block only for a single memorable takeaway line
|
|
28
|
+
- no bullet may exceed one line at 1.3em / 1920px width (~90 chars)
|
|
29
|
+
- Optional mermaid diagram: add a fenced mermaid code block after the bullet list (` ```mermaid ...``` `) for flowcharts or architecture visuals.
|
|
30
|
+
|
|
31
|
+
**B. Narration script** (per language, **one paragraph per bullet**):
|
|
32
|
+
|
|
33
|
+
- **One paragraph per bullet** — never merge bullets into a single script. Each paragraph becomes a separate `- script "..."` node with `on:(start, <sceneId>.current=N)`.
|
|
34
|
+
- Paragraph length ≈ **15–30 words** per beat (~6–12s at 2.5 w/s).
|
|
35
|
+
- Each paragraph **expands** its bullet — never reads it verbatim.
|
|
36
|
+
- Spoken register: short sentences, no markdown, no stage directions.
|
|
37
|
+
- The concrete example gets its own bullet and its own paragraph. Place it last (final beat of the scene).
|
|
38
|
+
- For TitleSlide: 1 paragraph (just the welcome/introduction line).
|
|
39
|
+
- For Summary: 1 paragraph per row in the comparison table + 1 final closing paragraph ("Thank you for watching…").
|
|
40
|
+
|
|
41
|
+
Output format:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
### <SceneName (PascalCase, no spaces)>
|
|
45
|
+
|
|
46
|
+
~~~md source
|
|
47
|
+
<slide markdown>
|
|
48
|
+
~~~
|
|
49
|
+
<one extra fenced block per additional language: ~~~md <lang>-source>
|
|
50
|
+
|
|
51
|
+
script (en):
|
|
52
|
+
<p1: describes bullet 1>
|
|
53
|
+
|
|
54
|
+
<p2: describes bullet 2>
|
|
55
|
+
|
|
56
|
+
<p3: describes bullet 3 — concrete example>
|
|
57
|
+
|
|
58
|
+
script (<lang>):
|
|
59
|
+
<p1 translation>
|
|
60
|
+
|
|
61
|
+
<p2 translation>
|
|
62
|
+
|
|
63
|
+
<p3 translation>
|
|
64
|
+
```
|
|
@@ -1,191 +0,0 @@
|
|
|
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
|
package/docs/templates.md
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# Video Templates
|
|
2
|
-
|
|
3
|
-
Ready-to-use markdown templates for common video scenarios. Each template:
|
|
4
|
-
|
|
5
|
-
- Contains **YAML frontmatter** with usage guidance (when, how, key decisions)
|
|
6
|
-
- Lists **required external packages** (npm imports)
|
|
7
|
-
- Uses **src:auto prompt:"..."** placeholders for AI-generated images/videos
|
|
8
|
-
- Includes **TTS narration** with configured voice and pacing
|
|
9
|
-
- Provides **scene structure** with named scenes, layouts, and durations
|
|
10
|
-
|
|
11
|
-
## Quick Start
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
# 1. Pick a template
|
|
15
|
-
cp docs/templates/courseware.md my-video.md
|
|
16
|
-
|
|
17
|
-
# 2. Fill in [bracketed] placeholders with actual content
|
|
18
|
-
# 3. Render
|
|
19
|
-
npx markcut render my-video.md --aspect 16x9
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Template Catalog
|
|
23
|
-
|
|
24
|
-
| Template | Scenario | Duration | Key Components |
|
|
25
|
-
|---|---|---|---|
|
|
26
|
-
| [courseware.md](templates/courseware.md) | 课件 / Lessons | 1-5 min | MarpSlide (markdown→slides), SlideText, MathDisplay |
|
|
27
|
-
| [product-ad.md](templates/product-ad.md) | 产品广告 / Ads | 15-45s | TextReveal, DeviceMockup, QRCode, FeatureCard |
|
|
28
|
-
| [movie-review.md](templates/movie-review.md) | 影视讲解 / Reviews | 1-3 min | CinematicText, StarRating, TextAnnotation, SplitScreen |
|
|
29
|
-
| [audiobook.md](templates/audiobook.md) | 有声图书 / Audiobooks | 3-30 min | BookCover, AmbientBackground, ProgressBar |
|
|
30
|
-
| [story-video.md](templates/story-video.md) | 故事视频 / Stories | 1-5 min | StoryTitle, SpeechBubble, ParticleEffect |
|
|
31
|
-
| [travel-log.md](templates/travel-log.md) | 旅行日志 / Travel | 1-3 min | LocationCard, WeatherIcon, TravelStats, Map node |}
|
|
32
|
-
|
|
33
|
-
## Frontmatter Fields Used by Templates
|
|
34
|
-
|
|
35
|
-
| Field | Purpose | Example |
|
|
36
|
-
|---|---|---|
|
|
37
|
-
| `tts` | TTS CLI command with `{input}` `{output}` | `edge-tts --voice "zh-CN-XiaoxiaoNeural" --text "{input}" --write-media "{output}"` |
|
|
38
|
-
| `stt` | STT CLI command with `{input}` `{output}` | `whisper "{input}" --model tiny --language zh --output_format vtt --output_dir "{output}"` |
|
|
39
|
-
| `tti` | TTI CLI command with `{input}` `{output}` | `pi --model agnes-2.0-flash --print "generate image: {input}" --output "{output}"` |
|
|
40
|
-
| `ttv` | TTV CLI command with `{input}` `{output}` | `pi --model agnes-2.0-flash --print "generate video: {input}" --output "{output}"` |
|
|
41
|
-
|
|
42
|
-
## src:auto — AI-Generated Media
|
|
43
|
-
|
|
44
|
-
Templates use a special `src:auto` value on `image` and `video` nodes to indicate AI generation:
|
|
45
|
-
|
|
46
|
-
```md
|
|
47
|
-
- image src:auto prompt:"sunset over Tokyo skyline, cinematic" duration:5
|
|
48
|
-
- video src:auto prompt:"person walking through cherry blossom park" duration:3
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
The `prompt:` key provides the generation prompt to the configured TTI/TTV CLI.
|
|
52
|
-
The pipeline resolves `src:auto` before compilation: runs the CLI command, gets the output path, and replaces `src:auto` with the actual generated file path.
|