@lalalic/markcut 2.8.0 → 2.9.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/README.md CHANGED
@@ -48,6 +48,7 @@ storyboard.md ──[parse]──▶ DescriptiveRoot ──[compile]──▶
48
48
  | **Built-in Mermaid** | `<Mermaid source="..." theme="dark" />` renders Mermaid diagrams as inline SVG. No imports needed |
49
49
  | **Styling** | Inline `style` strings on any node for CSS. JSX components use inline React styles |
50
50
  | **Live edit** | `--edit` watches the input file, re-runs pipeline, auto-reloads player |
51
+ | **Player language** | Player UI supports English/Chinese. Use `?lang=en` or `?lang=zh` |
51
52
  | **Storyboard** | `--storyboard` fast structure preview: replaces TTI/TTV prompts with placeholder components, skips slow generation. Implies `--edit` |
52
53
  | **Label mode** | `--label` interactive player with per-scene label input, saves to labels.json |
53
54
  | **CLI** | `render`, `preview` commands for MP4 export and Remotion Studio |
@@ -159,6 +160,34 @@ Compound variant labels (like `zh-tiktok`) are split on `-` to form a **variant
159
160
 
160
161
  The browser player reads `window.VARIANT` from the URL path and fetches the correct compiled data via `/api/video-data?variant=<name>`. A **variant switcher bar** at the top of the player lets you jump between variants instantly.
161
162
 
163
+ ### Player UI Language (EN / ZH)
164
+
165
+ The player UI (edit panel, placeholders, tooltips) supports English and Chinese.
166
+
167
+ - `?lang=en` for English
168
+ - `?lang=zh` for Chinese
169
+
170
+ Examples:
171
+
172
+ - `http://localhost:3001?lang=en`
173
+ - `http://localhost:3001?lang=zh`
174
+
175
+ If `lang` is not provided, the player falls back to browser language (Chinese browsers default to `zh`, otherwise `en`).
176
+
177
+ ### Map Language (EN / ZH)
178
+
179
+ `map` nodes support `language`/`lang` and `region` keys.
180
+
181
+ ```md
182
+ - map duration:3 lang:zh region:CN waypoints:[31.23,121.47,"上海";39.90,116.40,"北京"]
183
+ - map duration:3 lang:en region:US waypoints:[37.77,-122.41,"SF";34.05,-118.24,"LA"]
184
+ ```
185
+
186
+ Locale shorthand normalization:
187
+
188
+ - `lang:zh` / `language:zh` → `zh-CN` (default region `CN`)
189
+ - `lang:en` / `language:en` → `en` (default region `US`)
190
+
162
191
  ## `.markcut/` Directory Layout
163
192
 
164
193
  When you run `preview` or `render`, all generated artifacts live under `.markcut/` next to the source file. With multiple variants, each gets its own subdirectory:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalalic/markcut",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Markdown-to-video engine. Describe scenes in markdown, get a rendered video.",
5
5
  "bin": {
6
6
  "markcut": "bin/markcut"
@@ -80,52 +80,15 @@ some common issues (photo or video can't be displayed, audio missing), take belo
80
80
  | Topic | File |
81
81
  |-------|------|
82
82
  | Markdown descriptive format (primary authoring format) | [docs/markdown-descriptive.md](docs/markdown-descriptive.md) |
83
+ | Built-in components & common npm packages | [docs/components.md](docs/components.md) |
84
+ | Sound effects | [docs/sound-effects.md](docs/sound-effects.md) |
83
85
 
84
86
 
85
- ## Built-in Components (no imports needed)
86
87
 
87
- These components are available directly in `jsx:".."` fields or importable from the `~~~js imports` block via `@lalalic/markcut/components`:
88
+ ## Built-in Components
88
89
 
89
- ### `<Markdown />` — render markdown content
90
- ```md
91
- - component jsx:"<Markdown source='# Hello\n\n**bold** text.' />"
92
- ```
93
- - Uses `react-markdown` + `remark-gfm` (tables, strikethrough, task lists)
94
- - Supports `plugins` and `components` props: define custom renderers in imports block and pass them in
95
- - ` ```mermaid ` code fences inside markdown are automatically rendered as Mermaid diagrams
96
- - Importable: `import {Markdown} from "@lalalic/markcut/components"`
97
-
98
- ### `<Mermaid />` — render Mermaid diagrams as SVG
99
- ```md
100
- - component jsx:"<Mermaid source='graph TD; A-->B; A-->C; B-->D;' />"
101
- ```
102
- - Theme prop: `theme="default" | "dark" | "forest" | "neutral"` (default: `dark`)
103
- - Uses `delayRender`/`continueRender` for async rendering — diagram is ready before Remotion captures the frame
104
- - Errors shown inline in the output
105
- - Importable: `import {Mermaid} from "@lalalic/markcut/components"`
106
-
107
- ### Wrapping built-ins in custom components
108
- ```js
109
- import {Markdown, Mermaid} from "@lalalic/markcut/components"
110
-
111
- export function SuperMarkdown({ source }) {
112
- return (
113
- <Markdown
114
- source={source}
115
- components={{
116
- li: ({children}) => <li style={{color:'#ffd700'}}>{children}</li>,
117
- }}
118
- />
119
- )
120
- }
121
- ```
90
+ Built-in components available via `@lalalic/markcut/components`. See [docs/components.md](docs/components.md) for full reference.
122
91
 
123
- ## Common npm packages (used via imports block)
124
- - `react-markdown` + `remark-gfm` — already bundled as built-in `<Markdown />` above
125
- - `remark-toc` — generate table of contents
126
- - `remark-math` + `rehype-katex` — render math formulas with KaTeX
127
- - `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
128
- - `@remotion/starburst` — render starburst animations
129
92
 
130
93
  ## Golden rule
131
94
  - always check stream start and duration to avoid
@@ -133,8 +96,12 @@ export function SuperMarkdown({ source }) {
133
96
  - video cut off
134
97
  - subtitle mismatch
135
98
  - sync issues between audio, video, and subtitles
136
- - don't set duration for script or stream's duration depending on audio script
137
- - markcut resolver will automatically calculate the duration based on the audio script length
138
- - **don't** rm `.markcut` directory, which served as cache for all generated content. cache will auto update according to the content change. rm `.markcut` will cause all content to be regenerated, which is time consuming and wasteful.
99
+ according to the content change. rm `.markcut` will cause all content to be regenerated, which is time consuming and wasteful.
139
100
  - put all manual assets in `assets` folder, such as bgm, logo, watermark, etc. don't put them in `.markcut` folder, which is auto generated and will be deleted when `markcut clean` command is run.
140
- - `npx @lalalic/markcut preview` stuck until user close the preview window.
101
+
102
+ ### Don'ts
103
+ - **don't** set duration for script or stream's duration depending on audio script
104
+ - markcut resolver will automatically calculate the duration based on the audio script length
105
+ - **don't** rm `.markcut` directory, which served as cache for all generated content. cache will auto update
106
+ - **don't** set timeout for `preview`, `vision`, `render` markcut commands, which may take long time to generate medias.
107
+ - **don't** use skill to understand vision media. use `npx @lalalic/markcut vision <folder>`.
@@ -0,0 +1,46 @@
1
+ ## Built-in Components (no imports needed)
2
+
3
+ These components are available directly in `jsx:".."` fields or importable from the `~~~js imports` block via `@lalalic/markcut/components`:
4
+
5
+ ### `<Markdown />` — render markdown content
6
+ ```md
7
+ - component jsx:"<Markdown source='# Hello\n\n**bold** text.' />"
8
+ ```
9
+ - Uses `react-markdown` + `remark-gfm` (tables, strikethrough, task lists)
10
+ - Supports `plugins` and `components` props: define custom renderers in imports block and pass them in
11
+ - ` ```mermaid ` code fences inside markdown are automatically rendered as Mermaid diagrams
12
+ - Importable: `import {Markdown} from "@lalalic/markcut/components"`
13
+
14
+ ### `<Mermaid />` — render Mermaid diagrams as SVG
15
+ ```md
16
+ - component jsx:"<Mermaid source='graph TD; A-->B; A-->C; B-->D;' />"
17
+ ```
18
+ - Theme prop: `theme="default" | "dark" | "forest" | "neutral"` (default: `dark`)
19
+ - Uses `delayRender`/`continueRender` for async rendering — diagram is ready before Remotion captures the frame
20
+ - Errors shown inline in the output
21
+ - Importable: `import {Mermaid} from "@lalalic/markcut/components"`
22
+
23
+ ### Wrapping built-ins in custom components
24
+ ```js
25
+ import {Markdown, Mermaid} from "@lalalic/markcut/components"
26
+
27
+ export function SuperMarkdown({ source }) {
28
+ return (
29
+ <Markdown
30
+ source={source}
31
+ components={{
32
+ li: ({children}) => <li style={{color:'#ffd700'}}>{children}</li>,
33
+ }}
34
+ />
35
+ )
36
+ }
37
+ ```
38
+
39
+ ## Common npm packages (used via imports block)
40
+ - `react-markdown` + `remark-gfm` — already bundled as built-in `<Markdown />` above
41
+ - `remark-toc` — generate table of contents
42
+ - `remark-math` + `rehype-katex` — render math formulas with KaTeX
43
+ - `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
44
+ - `@remotion/starburst` — render starburst animations
45
+ - `react-webcam-pro` — render webcam video
46
+ - `react-chartjs-2` — render charts with Chart.js at https://react-chartjs-2.js.org/components
@@ -331,6 +331,9 @@ Subtitles are configured at the root level as a VTT overlay. Set via `subtitle:`
331
331
  | `zoom` | int (default 10) | map |
332
332
  | `center` | `{lat:n,lng:n}` JSON | map |
333
333
  | `mapType` | `roadmap\|satellite\|hybrid\|terrain` | map |
334
+ | `language` | map label/UI language. Supports `en`, `zh`, `zh-CN`, etc. | map |
335
+ | `lang` | alias of `language` (e.g. `lang:zh`) | map |
336
+ | `region` | region code hint (e.g. `CN`, `US`) | map |
334
337
  | `routeMarker` | emoji string e.g. `"🚗"` | map |
335
338
  | `title` | display title | scene |
336
339
  | `instruction` | visual intent / style / any prompt; NOT rendered | any |
@@ -433,6 +436,15 @@ When: animated route. Required: `duration`, `waypoints`.
433
436
 
434
437
  `- map duration:3 travelMode:DRIVING waypoints:[37.77,-122.41,"SF";34.05,-118.24,"LA"]`
435
438
 
439
+ Language and region are optional:
440
+
441
+ `- map duration:3 language:zh region:CN waypoints:[31.23,121.47,"上海";39.90,116.40,"北京"]`
442
+
443
+ Shorthand aliases:
444
+
445
+ - `language:zh` or `lang:zh` → normalized to `zh-CN` with `region:CN` default
446
+ - `language:en` or `lang:en` → keeps English labels with `region:US` default
447
+
436
448
  ### `include`
437
449
 
438
450
  When: embed an external markdown file as a sub-video. The sub-video is
@@ -0,0 +1,45 @@
1
+ # 🎧 Remotion Sound Effects
2
+
3
+ ## 1. URL Construction
4
+
5
+ All sound effect URLs follow this pattern:
6
+
7
+ `https://remotion.media/<name>.wav`
8
+
9
+ such as `https://remotion.media/sanctuary-guardian-what.wav`
10
+ ---
11
+
12
+ ## 2. Available Names
13
+
14
+ whip
15
+ whoosh
16
+ page-turn
17
+ switch
18
+ mouse-click
19
+ shutter-modern
20
+ shutter-old
21
+ ding
22
+ bruh
23
+ vine-boom
24
+ windows-xp-error
25
+ fah
26
+ spongebob-fail
27
+ omg-hell-nah
28
+ price-is-right-fail
29
+ romance-meme
30
+ bone-crack
31
+ anime-wow
32
+ yippee
33
+ loading-lag
34
+ wilhelm-scream
35
+ mac-quack
36
+ skedaddle
37
+ snapchat-notification
38
+ nelly-ahh
39
+ sanctuary-guardian-what
40
+ minecraft-hurt
41
+ oh-my-god-vine
42
+ illuminati-confirmed
43
+ dramatic-boomer
44
+ triggered
45
+ record-scratch
package/src/config.mjs CHANGED
@@ -85,9 +85,9 @@ export const DEFAULT_VTT_SAMPLE_INTERVAL = Number(process.env.MARKCUT_VTT_SAMPLE
85
85
 
86
86
 
87
87
  /** Speech-to-text CLI. Override via --stt flag or MARKCUT_STT_CLI env var. */
88
- export const DEFAULT_STT_CLI = args.cliOverrides.stt || process.env.MARKCUT_STT_CLI || 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}"';
88
+ export const DEFAULT_STT_CLI = args.cliOverrides.stt || process.env.MARKCUT_STT_CLI || 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}" --word_timestamps True --max_line_count 1 --max_line_width 14';
89
89
  /** Text-to-speech CLI. Override via --tts flag or MARKCUT_TTS_CLI env var. */
90
- export const DEFAULT_TTS_CLI = args.cliOverrides.tts || process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "en-US-GuyNeural" --text "{input}" --write-media "{output}"';
90
+ export const DEFAULT_TTS_CLI = args.cliOverrides.tts || process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "zh-CN-YunxiNeural" --text "{input}" --write-media "{output}"';
91
91
  /** Default agent CLI. Override via --agent flag or MARKCUT_AGENT_CLI env var. */
92
92
  export const DEFAULT_AGENT_CLI = args.cliOverrides.agent || process.env.MARKCUT_AGENT_CLI || 'npx pi -p {prompt}';
93
93
  /** Default edit agent CLI. Override via --edit-cli flag or MARKCUT_EDIT_CLI env var. */
@@ -147,6 +147,8 @@ export interface DescriptiveMap extends DescriptiveBaseNode {
147
147
  zoom?: number;
148
148
  center?: { lat: number; lng: number };
149
149
  mapType?: "roadmap" | "satellite" | "hybrid" | "terrain";
150
+ language?: string;
151
+ region?: string;
150
152
  travelMode?: "DRIVING" | "WALKING" | "BICYCLING" | "TRANSIT";
151
153
  routeMarker?: string;
152
154
  }
@@ -593,6 +595,8 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
593
595
  zoom: node.zoom ?? 10,
594
596
  center: node.center,
595
597
  mapType: node.mapType ?? "roadmap",
598
+ language: node.language,
599
+ region: node.region,
596
600
  travelMode: node.travelMode ?? "DRIVING",
597
601
  routeMarker: node.routeMarker ?? "🚗",
598
602
  googleMapsApiKey: ctx.googleMapsApiKey,
@@ -327,6 +327,8 @@ function parseNodeLine(content: string, lineNum?: number): DescriptiveNode {
327
327
  zoom: attrs.zoom as any,
328
328
  center: attrs.center as any,
329
329
  mapType: attrs.mapType as any,
330
+ language: (attrs.language as any) ?? (attrs.lang as any),
331
+ region: attrs.region as any,
330
332
  instruction: attrs.instruction as any,
331
333
  visible: attrs.visible as any,
332
334
  isBackground: attrs.isBackground as any,
@@ -14,7 +14,8 @@ import * as ReactDOM from "react-dom";
14
14
  import * as Remotion from "remotion";
15
15
  import { Player } from "@remotion/player";
16
16
  import { MarkCut, getDurationInSeconds } from "../entry";
17
- import { HeaderBar, EditControls, LabelControls, SceneThumbnails, VariantBar } from "./components/index";
17
+ import { HeaderBar, EditControls, EditMessagePanel, LabelControls, SceneThumbnails, VariantBar } from "./components/index";
18
+ import type { EditEntry } from "./components/EditMessagePanel";
18
19
  import * as MarkcutComponents from "../components/index";
19
20
 
20
21
  /**
@@ -161,6 +162,13 @@ function PlayerApp() {
161
162
  const urlParams = new URLSearchParams(typeof window !== "undefined" ? window.location.search : "");
162
163
  const autoPlay = urlParams.get("autoplay") === "true";
163
164
  const startAt = parseFloat(urlParams.get("start") || urlParams.get("t") || "0") || 0;
165
+ const locale: "en" | "zh" = (() => {
166
+ const raw = (urlParams.get("lang") || "").toLowerCase();
167
+ if (raw === "zh" || raw.startsWith("zh-")) return "zh";
168
+ if (raw === "en" || raw.startsWith("en-")) return "en";
169
+ if (typeof navigator !== "undefined" && navigator.language.toLowerCase().startsWith("zh")) return "zh";
170
+ return "en";
171
+ })();
164
172
 
165
173
  // Derive player config from data early so effects can reference them safely.
166
174
  const fps = data?.fps ?? 30;
@@ -389,10 +397,30 @@ function PlayerApp() {
389
397
  (typeof window !== "undefined" ? (window as any).MODE : null) || "preview";
390
398
 
391
399
  // Shared state for header info
392
- const [editStatus, setEditStatus] = React.useState("");
393
400
  const [sseConnected, setSseConnected] = React.useState(false);
394
401
  const [labelSceneInfo, setLabelSceneInfo] = React.useState("");
395
402
 
403
+ // ── Edit message panel state ─────────────────────────────────────────
404
+ const [editEntries, setEditEntries] = React.useState<EditEntry[]>([]);
405
+ const [showEditOverlay, setShowEditOverlay] = React.useState(false);
406
+ const nextEditIdRef = React.useRef(1);
407
+ const hideEditOverlayTimerRef = React.useRef<number | null>(null);
408
+
409
+ const clearHideOverlayTimer = React.useCallback(() => {
410
+ if (hideEditOverlayTimerRef.current !== null) {
411
+ window.clearTimeout(hideEditOverlayTimerRef.current);
412
+ hideEditOverlayTimerRef.current = null;
413
+ }
414
+ }, []);
415
+
416
+ const scheduleHideEditOverlay = React.useCallback(() => {
417
+ clearHideOverlayTimer();
418
+ hideEditOverlayTimerRef.current = window.setTimeout(() => {
419
+ setShowEditOverlay(false);
420
+ hideEditOverlayTimerRef.current = null;
421
+ }, 5000);
422
+ }, [clearHideOverlayTimer]);
423
+
396
424
  // SSE connection — shared across all modes as a server-liveness monitor
397
425
  // Edit mode also listens for "reload" messages (auto-refresh on file change)
398
426
  const suppressReloadRef = React.useRef(false);
@@ -404,8 +432,57 @@ function PlayerApp() {
404
432
  evtSource.onmessage = (e: MessageEvent) => {
405
433
  try {
406
434
  const msg = JSON.parse(e.data);
435
+
436
+ // Reload event (file changed on disk or agent finished editing)
407
437
  if (msg.type === "reload" && !suppressReloadRef.current) {
408
438
  window.dispatchEvent(new Event("refresh-player"));
439
+ return;
440
+ }
441
+
442
+ // Edit progress events (live from agent RPC)
443
+ if (msg.type === "edit:start") {
444
+ clearHideOverlayTimer();
445
+ setShowEditOverlay(true);
446
+ const id = nextEditIdRef.current++;
447
+ setEditEntries((prev) => [
448
+ ...prev,
449
+ { id, request: msg.request || "", progress: "", status: "thinking" },
450
+ ]);
451
+ return;
452
+ }
453
+ if (msg.type === "edit:progress") {
454
+ setEditEntries((prev) => {
455
+ const last = prev[prev.length - 1];
456
+ if (!last || last.status !== "thinking") return prev;
457
+ return prev.map((e) =>
458
+ e.id === last.id ? { ...e, progress: msg.text || "" } : e
459
+ );
460
+ });
461
+ return;
462
+ }
463
+ if (msg.type === "edit:done") {
464
+ setEditEntries((prev) => {
465
+ const last = prev[prev.length - 1];
466
+ if (!last) return prev;
467
+ return prev.map((e) =>
468
+ e.id === last.id
469
+ ? { ...e, status: "done", progress: e.progress || msg.summary || "" }
470
+ : e
471
+ );
472
+ });
473
+ scheduleHideEditOverlay();
474
+ return;
475
+ }
476
+ if (msg.type === "edit:error") {
477
+ setEditEntries((prev) => {
478
+ const last = prev[prev.length - 1];
479
+ if (!last) return prev;
480
+ return prev.map((e) =>
481
+ e.id === last.id ? { ...e, status: "error", error: msg.error } : e
482
+ );
483
+ });
484
+ scheduleHideEditOverlay();
485
+ return;
409
486
  }
410
487
  } catch {}
411
488
  };
@@ -414,10 +491,11 @@ function PlayerApp() {
414
491
  setSseConnected(false);
415
492
  }
416
493
  return () => {
494
+ clearHideOverlayTimer();
417
495
  evtSource?.close();
418
496
  setSseConnected(false);
419
497
  };
420
- }, []);
498
+ }, [clearHideOverlayTimer, scheduleHideEditOverlay]);
421
499
 
422
500
  if (error) {
423
501
  return <div style={{ color: "red", padding: 40, fontFamily: "sans-serif" }}>Error: {error}</div>;
@@ -440,9 +518,9 @@ function PlayerApp() {
440
518
  {/* ── Header (close button + mode info) ── */}
441
519
  <HeaderBar
442
520
  mode={mode}
443
- editStatus={editStatus}
444
521
  sseConnected={sseConnected}
445
522
  sceneInfo={mode === "label" ? labelSceneInfo : undefined}
523
+ locale={locale}
446
524
  />
447
525
 
448
526
  {/* ── Variant switcher ── */}
@@ -466,6 +544,18 @@ function PlayerApp() {
466
544
  doubleClickToFullscreen={true}
467
545
  autoPlay={autoPlay}
468
546
  />
547
+
548
+ {/* ── Edit message overlay (shown while waiting, then auto-hides) ── */}
549
+ {mode === "edit" && showEditOverlay && (
550
+ <div id="edit-message-overlay">
551
+ <EditMessagePanel
552
+ entries={editEntries}
553
+ minimized={false}
554
+ locale={locale}
555
+ onToggleMinimize={() => setShowEditOverlay(false)}
556
+ />
557
+ </div>
558
+ )}
469
559
  </div>
470
560
 
471
561
  {/* ── Scene thumbnails (shared across all modes) ── */}
@@ -483,10 +573,10 @@ function PlayerApp() {
483
573
  {/* ── Mode-specific controls ── */}
484
574
  {mode === "edit" && (
485
575
  <EditControls
486
- onStatusChange={setEditStatus}
487
576
  suppressReloadRef={suppressReloadRef}
488
577
  currentTime={currentTime}
489
578
  activeScene={activeScene}
579
+ locale={locale}
490
580
  />
491
581
  )}
492
582
  {mode === "label" && (