@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 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**. see [docs/markdown-descriptive.md](docs/markdown-descriptive.md) for full details.
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
- When you run `preview` or `render`, all generated artifacts live under `.markcut/` next to the source file:
11
+ ## video design utitlies
15
12
 
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
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. Each scene has `title`, `instruction`, `script` (TTS narration), `children`. Scenes can nest inside other scenes.
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
- ### 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.
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 or render mode
189
- - screenshot some key frames, and understand image to verify if the image is correct
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 if the image is correct
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
- | Template overview and TTI/TTV config | [docs/templates.md](docs/templates.md) |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalalic/markcut",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Markdown-to-video engine. Describe scenes in markdown, get a rendered video.",
5
5
  "bin": {
6
6
  "markcut": "bin/markcut"
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
- import(/* webpackIgnore: true */ imports)
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);
@@ -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 { resolve, dirname, join } from "node:path";
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 { parseMarkdownDescriptive } = await import("../player/pipeline.mjs");
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 = parseMarkdownDescriptive(raw);
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 { parseMarkdownDescriptive } = await import("../player/pipeline.mjs");
345
+ const { parseMarkdownVariants } = await import("../player/pipeline.mjs");
312
346
  const fileDir = dirname(filePath);
313
- const parsed = parseMarkdownDescriptive(raw);
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.