@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.
@@ -17,11 +17,14 @@ interface EditControlsProps {
17
17
  currentTime?: number;
18
18
  /** Active scene name (sent with edit request for context) */
19
19
  activeScene?: string;
20
+ /** UI locale */
21
+ locale?: "en" | "zh";
20
22
  }
21
23
 
22
- export function EditControls({ onStatusChange, suppressReloadRef, currentTime, activeScene }: EditControlsProps) {
24
+ export function EditControls({ onStatusChange, suppressReloadRef, currentTime, activeScene, locale = "en" }: EditControlsProps) {
23
25
  const [busy, setBusy] = React.useState(false);
24
26
  const inputRef = React.useRef<HTMLInputElement>(null);
27
+ const isZh = locale === "zh";
25
28
 
26
29
  // ── Edit submit ──────────────────────────────────────────────────────
27
30
  const handleApplyEdit = React.useCallback(
@@ -75,12 +78,12 @@ export function EditControls({ onStatusChange, suppressReloadRef, currentTime, a
75
78
  <input
76
79
  ref={inputRef}
77
80
  id="edit-input"
78
- placeholder="What should change? e.g. make text bigger"
81
+ placeholder={isZh ? "想改什么?例如:把文字放大一些" : "What should change? e.g. make text bigger"}
79
82
  onKeyDown={handleKeyDown}
80
83
  />
81
84
  <button
82
85
  id="edit-btn"
83
- title="Apply edit"
86
+ title={isZh ? "应用修改" : "Apply edit"}
84
87
  disabled={busy}
85
88
  onClick={() => handleApplyEdit(inputRef.current?.value || "")}
86
89
  >
@@ -0,0 +1,96 @@
1
+ /**
2
+ * EditMessagePanel — displays edit session messages chronologically.
3
+ *
4
+ * Shows user edit requests and assistant responses in a scrollable panel
5
+ * that can be minimized to a compact bar. Replaces the old one-line
6
+ * edit-status in the header.
7
+ */
8
+ import * as React from "react";
9
+
10
+ export interface EditEntry {
11
+ id: number;
12
+ request: string;
13
+ /** Accumulated assistant response text */
14
+ progress: string;
15
+ status: "thinking" | "done" | "error";
16
+ error?: string;
17
+ }
18
+
19
+ interface EditMessagePanelProps {
20
+ entries: EditEntry[];
21
+ minimized: boolean;
22
+ locale?: "en" | "zh";
23
+ onToggleMinimize: () => void;
24
+ }
25
+
26
+ export function EditMessagePanel({ entries, minimized, locale = "en", onToggleMinimize }: EditMessagePanelProps) {
27
+ const listRef = React.useRef<HTMLDivElement>(null);
28
+ const isZh = locale === "zh";
29
+
30
+ // Auto-scroll to bottom when entries change
31
+ React.useEffect(() => {
32
+ if (listRef.current) {
33
+ listRef.current.scrollTop = listRef.current.scrollHeight;
34
+ }
35
+ }, [entries]);
36
+
37
+ // Minimized bar: show count + latest status
38
+ if (minimized) {
39
+ const latest = entries[entries.length - 1];
40
+ const label =
41
+ entries.length === 0
42
+ ? (isZh ? "✨ 编辑" : "✨ Edit")
43
+ : isZh
44
+ ? `✨ ${entries.length} 条编辑${latest?.status === "thinking" ? " ⏳" : ""}`
45
+ : `✨ ${entries.length} edit${entries.length > 1 ? "s" : ""}${latest?.status === "thinking" ? " ⏳" : ""}`;
46
+ return (
47
+ <button id="edit-message-bar" onClick={onToggleMinimize} title={isZh ? "显示编辑历史" : "Show edit history"}>
48
+ {label}
49
+ </button>
50
+ );
51
+ }
52
+
53
+ return (
54
+ <div id="edit-message-panel">
55
+ <div id="edit-message-header">
56
+ <span>{isZh ? "✨ 编辑历史" : "✨ Edit History"}</span>
57
+ <button
58
+ id="edit-message-minimize"
59
+ onClick={onToggleMinimize}
60
+ title={isZh ? "最小化" : "Minimize"}
61
+ aria-label={isZh ? "最小化编辑面板" : "Minimize edit panel"}
62
+ >
63
+ ─
64
+ </button>
65
+ </div>
66
+ <div id="edit-message-list" ref={listRef}>
67
+ {entries.length === 0 && (
68
+ <div className="edit-message-empty">{isZh ? "暂无编辑记录。请在下方输入修改请求。" : "No edits yet. Type a request below."}</div>
69
+ )}
70
+ {entries.map((entry) => (
71
+ <div key={entry.id} className={`edit-message-entry ${entry.status}`}>
72
+ <div className="edit-message-request">
73
+ <span className="edit-role">{isZh ? "你:" : "You:"}</span> {entry.request}
74
+ </div>
75
+ {entry.status === "thinking" && (
76
+ <div className="edit-message-thinking">
77
+ <span className="edit-role">{isZh ? "助手:" : "Assistant:"}</span> {isZh ? "思考中" : "Thinking"}
78
+ <span className="edit-dots"><span>.</span><span>.</span><span>.</span></span>
79
+ </div>
80
+ )}
81
+ {entry.status === "done" && entry.progress && (
82
+ <div className="edit-message-response">
83
+ <span className="edit-role">{isZh ? "助手:" : "Assistant:"}</span> {entry.progress}
84
+ </div>
85
+ )}
86
+ {entry.status === "error" && (
87
+ <div className="edit-message-error">
88
+ <span className="edit-role">{isZh ? "错误:" : "Error:"}</span> {entry.error || entry.progress || (isZh ? "编辑失败" : "Edit failed")}
89
+ </div>
90
+ )}
91
+ </div>
92
+ ))}
93
+ </div>
94
+ </div>
95
+ );
96
+ }
@@ -12,18 +12,19 @@ interface HeaderBarProps {
12
12
  mode: string;
13
13
  /** Label mode scene info text (e.g. "slide1 (1.2s)") */
14
14
  sceneInfo?: string;
15
- /** Edit mode status text (e.g. "✅ done", "⏳ editing...") */
16
- editStatus?: string;
17
- /** Edit mode: whether SSE is connected */
15
+ /** Whether SSE is connected */
18
16
  sseConnected?: boolean;
17
+ /** UI locale */
18
+ locale?: "en" | "zh";
19
19
  }
20
20
 
21
- export function HeaderBar({ mode, sceneInfo, editStatus, sseConnected }: HeaderBarProps) {
21
+ export function HeaderBar({ mode, sceneInfo, sseConnected, locale = "en" }: HeaderBarProps) {
22
+ const isZh = locale === "zh";
22
23
  const handleClose = React.useCallback(() => {
23
24
  navigator.sendBeacon("/api/shutdown", "{}");
24
25
  document.body.innerHTML =
25
- "<div style='display:flex;align-items:center;justify-content:center;height:100vh;background:#0a0a0a;color:#555;font-family:sans-serif;font-size:16px'>\u2B61 player closed \u2014 return to terminal</div>";
26
- }, []);
26
+ `<div style='display:flex;align-items:center;justify-content:center;height:100vh;background:#0a0a0a;color:#555;font-family:sans-serif;font-size:16px'>\u2B61 ${isZh ? "播放器已关闭,返回终端" : "player closed — return to terminal"}</div>`;
27
+ }, [isZh]);
27
28
 
28
29
  return (
29
30
  <div id="header">
@@ -32,13 +33,10 @@ export function HeaderBar({ mode, sceneInfo, editStatus, sseConnected }: HeaderB
32
33
  {mode === "label" && sceneInfo && (
33
34
  <span id="scene-info">{sceneInfo}</span>
34
35
  )}
35
- {mode === "edit" && editStatus && (
36
- <span id="edit-status">{editStatus}</span>
37
- )}
38
36
  {/* SSE indicator — shown in all modes */}
39
37
  <span
40
38
  id="sse-indicator"
41
- title={sseConnected ? "Connected — auto-reload ready" : "Disconnected"}
39
+ title={sseConnected ? (isZh ? "已连接,可自动刷新" : "Connected — auto-reload ready") : (isZh ? "连接断开" : "Disconnected")}
42
40
  style={{
43
41
  display: "inline-block",
44
42
  width: 8,
@@ -51,7 +49,7 @@ export function HeaderBar({ mode, sceneInfo, editStatus, sseConnected }: HeaderB
51
49
  </span>
52
50
  {/* Right: close button */}
53
51
  <div id="header-actions">
54
- <button id="close-btn" title="Close player and return to terminal" onClick={handleClose}>
52
+ <button id="close-btn" title={isZh ? "关闭播放器并返回终端" : "Close player and return to terminal"} onClick={handleClose}>
55
53
  ✕
56
54
  </button>
57
55
  </div>
@@ -1,5 +1,6 @@
1
1
  export { HeaderBar } from "./HeaderBar";
2
2
  export { EditControls } from "./EditControls";
3
+ export { EditMessagePanel } from "./EditMessagePanel";
3
4
  export { LabelControls } from "./LabelControls";
4
5
  export { SceneThumbnails } from "./SceneThumbnails";
5
6
  export { VariantBar } from "./VariantBar";
@@ -338,6 +338,8 @@ function compileLeaf(node2, ctx, parentKind) {
338
338
  zoom: node2.zoom ?? 10,
339
339
  center: node2.center,
340
340
  mapType: node2.mapType ?? "roadmap",
341
+ language: node2.language,
342
+ region: node2.region,
341
343
  travelMode: node2.travelMode ?? "DRIVING",
342
344
  routeMarker: node2.routeMarker ?? "\u{1F697}",
343
345
  googleMapsApiKey: ctx.googleMapsApiKey
@@ -1062,8 +1064,8 @@ var MAX_VIDEO_DURATION = Number(process.env.MARKCUT_MAX_VIDEO_DURATION) || 60;
1062
1064
  var MAX_VIDEO_DIMENSION = Number(process.env.MARKCUT_MAX_VIDEO_DIMENSION) || 360;
1063
1065
  var GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "";
1064
1066
  var DEFAULT_VTT_SAMPLE_INTERVAL = Number(process.env.MARKCUT_VTT_SAMPLE_INTERVAL) || 5;
1065
- var DEFAULT_STT_CLI = args.cliOverrides.stt || process.env.MARKCUT_STT_CLI || 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}"';
1066
- var DEFAULT_TTS_CLI = args.cliOverrides.tts || process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "en-US-GuyNeural" --text "{input}" --write-media "{output}"';
1067
+ var 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';
1068
+ var DEFAULT_TTS_CLI = args.cliOverrides.tts || process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "zh-CN-YunxiNeural" --text "{input}" --write-media "{output}"';
1067
1069
  var DEFAULT_AGENT_CLI = args.cliOverrides.agent || process.env.MARKCUT_AGENT_CLI || "npx pi -p {prompt}";
1068
1070
  var DEFAULT_EDIT_CLI = args.cliOverrides.editCli || process.env.MARKCUT_EDIT_CLI || "npx pi --session-id {sessionid} --system-prompt {systemprompt} -p {prompt}";
1069
1071
  var DEFAULT_TTI_CLI = args.cliOverrides.tti || process.env.MARKCUT_TTI_CLI || 'uvx --from mflux mflux-generate-flux2 --model flux2-klein-4b --steps 2 --prompt "{input}" --output "{output}" --seed {seed}';
@@ -10413,6 +10415,8 @@ function parseNodeLine(content3, lineNum) {
10413
10415
  zoom: attrs.zoom,
10414
10416
  center: attrs.center,
10415
10417
  mapType: attrs.mapType,
10418
+ language: attrs.language ?? attrs.lang,
10419
+ region: attrs.region,
10416
10420
  instruction: attrs.instruction,
10417
10421
  visible: attrs.visible,
10418
10422
  isBackground: attrs.isBackground,
@@ -128,7 +128,10 @@ export function serveFile(req, res, filePath) {
128
128
  const ext = extname(filePath).toLowerCase();
129
129
  const mime = MIME[ext] || "application/octet-stream";
130
130
  const fileSize = statSync(filePath).size;
131
- const cacheControl = (ext === ".js" || ext === ".html") ? "no-cache" : "public, max-age=3600";
131
+ // Dynamic authoring assets (vtt/json/html/js) must not be cached,
132
+ // otherwise subtitle edits can appear stale after refresh.
133
+ const noStoreExt = new Set([".js", ".html", ".json", ".vtt"]);
134
+ const cacheControl = noStoreExt.has(ext) ? "no-store" : "public, max-age=3600";
132
135
  const range = req.headers.range;
133
136
 
134
137
  if (range) {
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Modes:
6
6
  * --label – playback with label input overlay; labels map to media timestamps
7
- * --edit – auto-reload when the JSON file changes (agent edits file, player refreshes)
7
+ * --edit – edit input mode with AI-assisted changes
8
8
  *
9
9
  * Usage:
10
10
  * node src/player/server.mjs <video.json> [--label] [--edit] [--port 3001]
@@ -17,7 +17,7 @@ import { fileURLToPath } from "node:url";
17
17
  import { isDescriptiveRoot, resolveAndCompile, resolveAndCompileMarkdown, parseImportsBlock, extractDependencySpecs } from "./pipeline.mjs";
18
18
  import { bundleFromEntries } from "./bundler.mjs";
19
19
  import { extractScenes, MIME, serveFile, handleShutdown } from "./server-shared.mjs";
20
- import { DEFAULT_EDIT_CLI } from "../config.mjs";
20
+ import { DEFAULT_EDIT_CLI, DEFAULT_STT_CLI, GOOGLE_MAPS_API_KEY } from "../config.mjs";
21
21
 
22
22
 
23
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -79,6 +79,14 @@ const VARIANT_CONFIGS = parseVariantConfigs();
79
79
  const sseClients = new Set();
80
80
  let shutdownTimer = null;
81
81
 
82
+ /** Push a JSON event to all connected SSE clients. */
83
+ function ssePush(data) {
84
+ const text = `data: ${JSON.stringify(data)}\n\n`;
85
+ for (const client of sseClients) {
86
+ try { client.write(text); } catch { sseClients.delete(client); }
87
+ }
88
+ }
89
+
82
90
  // ─── Label store for label mode ───────────────────────────────────────────
83
91
  let labels = [];
84
92
 
@@ -128,16 +136,24 @@ function handleAgentLine(obj) {
128
136
  const t = obj.type;
129
137
  if (t === "message_end") {
130
138
  const txt = extractAssistantText(obj.message);
131
- if (txt) agentTurnText += (agentTurnText ? "\n" : "") + txt;
139
+ if (txt) {
140
+ agentTurnText += (agentTurnText ? "\n" : "") + txt;
141
+ // Push incremental assistant text to SSE clients
142
+ ssePush({ type: "edit:progress", text: agentTurnText });
143
+ }
132
144
  return;
133
145
  }
134
146
  if (t === "agent_settled" && pendingEditResolver) {
147
+ ssePush({ type: "edit:done", summary: agentTurnText.substring(0, 120) });
135
148
  finishAgentTurn({ ok: true });
136
149
  return;
137
150
  }
138
151
  if (t === "response" && obj.command === "prompt" && pendingEditResolver) {
139
152
  // Preflight result: success means "accepted, events incoming"; failure aborts.
140
- if (obj.success === false) finishAgentTurn({ ok: false, error: obj.error || "prompt rejected" });
153
+ if (obj.success === false) {
154
+ ssePush({ type: "edit:error", error: obj.error || "prompt rejected" });
155
+ finishAgentTurn({ ok: false, error: obj.error || "prompt rejected" });
156
+ }
141
157
  return;
142
158
  }
143
159
  if (t === "extension_error" && obj.error) {
@@ -447,11 +463,14 @@ async function compileVariant(config, parsed, raw) {
447
463
  scriptOutputDir: TTS_OUTPUT_DIR,
448
464
  mediaOutputDir: MEDIA_OUTPUT_DIR,
449
465
  includeOutputDir: INCLUDE_CACHE_DIR,
466
+ sttCli: DEFAULT_STT_CLI,
450
467
  subtitleOutputDir: subtitleDir,
451
468
  storyboard: MODE_STORYBOARD,
452
469
  variants: config.chain.length > 0 ? config.chain : undefined,
453
470
  });
454
- compiled = compileDescriptiveRoot(resolved);
471
+ compiled = compileDescriptiveRoot(resolved, {
472
+ googleMapsApiKey: GOOGLE_MAPS_API_KEY || "",
473
+ });
455
474
  } else {
456
475
  const parsedJson = JSON.parse(raw);
457
476
  const root = parsedJson.root || parsedJson;
@@ -471,6 +490,7 @@ async function compileVariant(config, parsed, raw) {
471
490
  scriptOutputDir: TTS_OUTPUT_DIR,
472
491
  mediaOutputDir: MEDIA_OUTPUT_DIR,
473
492
  includeOutputDir: INCLUDE_CACHE_DIR,
493
+ sttCli: DEFAULT_STT_CLI,
474
494
  subtitleOutputDir: subtitleDir,
475
495
  storyboard: MODE_STORYBOARD,
476
496
  variants: config.chain.length > 0 ? config.chain : undefined,
@@ -667,40 +687,36 @@ function getScenes(label) {
667
687
  // Will be awaited before announcing "Player ready"
668
688
  const initScenesPromise = compileAndExtractScenes();
669
689
 
670
- // ─── Watch file for changes (--edit mode) ───────────────────────────────
671
- if (MODE_EDIT) {
672
- let lastContent = readFileSync(VIDEO_JSON, "utf-8");
673
- watchFile(VIDEO_JSON, { interval: 1000 }, async (curr, prev) => {
674
- if (curr.mtimeMs === prev.mtimeMs) return;
675
- if (pipelineRunning) return;
676
- const newContent = readFileSync(VIDEO_JSON, "utf-8");
677
- if (newContent === lastContent) return;
678
- lastContent = newContent;
679
- pipelineRunning = true;
680
- console.log(` 📁 ${VIDEO_JSON} changed, re-running all variants...`);
690
+ // ─── Watch source file for changes (all modes) ──────────────────────────
691
+ let lastContent = readFileSync(VIDEO_JSON, "utf-8");
692
+ watchFile(VIDEO_JSON, { interval: 1000 }, async (curr, prev) => {
693
+ if (curr.mtimeMs === prev.mtimeMs) return;
694
+ if (pipelineRunning) return;
695
+ const newContent = readFileSync(VIDEO_JSON, "utf-8");
696
+ if (newContent === lastContent) return;
697
+ lastContent = newContent;
698
+ pipelineRunning = true;
699
+ console.log(` 📁 ${VIDEO_JSON} changed, re-running all variants...`);
681
700
 
682
- try {
683
- compiledRootCache.clear();
684
- scenesCache.clear();
685
- await compileAllVariants();
686
-
687
- for (const config of VARIANT_CONFIGS) {
688
- try {
689
- const root = await loadCompiledRoot(config.label);
690
- scenesCache.set(config.label, extractScenes(root));
691
- } catch {}
692
- }
701
+ try {
702
+ compiledRootCache.clear();
703
+ scenesCache.clear();
704
+ await compileAllVariants();
693
705
 
694
- for (const client of sseClients) {
695
- client.write("data: " + JSON.stringify({ type: "reload" }) + "\n\n");
696
- }
697
- } catch (e) {
698
- console.error(" ⚠️ Failed to re-process after change:", e.message);
699
- } finally {
700
- pipelineRunning = false;
706
+ for (const config of VARIANT_CONFIGS) {
707
+ try {
708
+ const root = await loadCompiledRoot(config.label);
709
+ scenesCache.set(config.label, extractScenes(root));
710
+ } catch {}
701
711
  }
702
- });
703
- }
712
+
713
+ ssePush({ type: "reload" });
714
+ } catch (e) {
715
+ console.error(" ⚠️ Failed to re-process after change:", e.message);
716
+ } finally {
717
+ pipelineRunning = false;
718
+ }
719
+ });
704
720
  // ─── MIME imported from ./server-shared.mjs ──────────────────────────────
705
721
 
706
722
  // ─── Variant detection from URL path ─────────────────────────────────────
@@ -762,7 +778,7 @@ function getHtml(variantLabel) {
762
778
  html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a0a; }
763
779
  body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; flex-direction: column; align-items: center; }
764
780
  #header { display: flex; align-items: center; justify-content: flex-end; width: 100%; max-width: 500px; padding: 8px 12px; flex-shrink: 0; gap: 8px; }
765
- #header-status, #edit-status { font-size: 11px; color: rgba(255,255,255,.4); flex: 1; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
781
+ #header-status { font-size: 11px; color: rgba(255,255,255,.4); flex: 1; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
766
782
  #scene-info { font-size: 11px; color: rgba(255,255,255,.4); flex: 1; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
767
783
  #header-actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
768
784
  #close-btn { width: 22px; height: 22px; border-radius: 50%; border: 1px solid rgba(255,255,255,.15); background: rgba(0,0,0,.3); color: rgba(255,255,255,.4); font-size: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all .15s; }
@@ -771,7 +787,7 @@ function getHtml(variantLabel) {
771
787
  .variant-link { font-size: 11px; padding: 3px 10px; border-radius: 12px; background: rgba(255,255,255,.06); color: rgba(255,255,255,.4); text-decoration: none; white-space: nowrap; transition: all .15s; }
772
788
  .variant-link:hover { background: rgba(255,255,255,.12); color: rgba(255,255,255,.7); }
773
789
  .variant-link.active { background: rgba(74,158,255,.2); color: #4a9eff; }
774
- #player-frame { flex: 1; width: 100%; max-width: 480px; min-height: 0; border-radius: 16px; overflow: hidden; border: 1px solid rgba(255,255,255,.08); background: #000; box-shadow: 0 4px 40px rgba(0,0,0,.6); margin: 0 12px; }
790
+ #player-frame { flex: 1; width: 100%; max-width: 480px; min-height: 0; border-radius: 16px; overflow: hidden; border: 1px solid rgba(255,255,255,.08); background: #000; box-shadow: 0 4px 40px rgba(0,0,0,.6); margin: 0 12px; position: relative; }
775
791
  #root { width: 100%; height: 100%; }
776
792
  #reload-toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(74,158,255,.9); color: #fff; padding: 12px 24px; border-radius: 10px; font-size: 14px; font-weight: 600; opacity: 0; transition: opacity .3s; pointer-events: none; z-index: 200; backdrop-filter: blur(8px); }
777
793
  #reload-toast.show { opacity: 1; }
@@ -805,6 +821,136 @@ function getHtml(variantLabel) {
805
821
  .scene-pill { flex-shrink: 0; padding: 4px 12px; border-radius: 12px; cursor: pointer; border: 1px solid transparent; transition: all .15s; background: rgba(255,255,255,.06); color: rgba(255,255,255,.45); font-size: 11px; white-space: nowrap; }
806
822
  .scene-pill:hover { border-color: rgba(74,158,255,.4); color: rgba(255,255,255,.7); }
807
823
  .scene-pill.active { background: rgba(74,158,255,.2); border-color: #4a9eff; color: #4a9eff; }
824
+
825
+ .subtitle-overlay>*{width:100%}
826
+
827
+ /* ── Edit message panel ───────────────────────────────────────────── */
828
+ #edit-message-overlay {
829
+ position: absolute;
830
+ left: 8px;
831
+ right: 8px;
832
+ top: 8px;
833
+ z-index: 30;
834
+ pointer-events: none;
835
+ }
836
+ #edit-message-panel {
837
+ width: 100%;
838
+ max-width: 100%;
839
+ flex-shrink: 0;
840
+ background: rgba(12,12,12,.78);
841
+ border: 1px solid rgba(255,255,255,.08);
842
+ border-radius: 10px;
843
+ margin: 0;
844
+ overflow: hidden;
845
+ backdrop-filter: blur(8px);
846
+ pointer-events: auto;
847
+ }
848
+ #edit-message-header {
849
+ display: flex;
850
+ align-items: center;
851
+ justify-content: space-between;
852
+ padding: 6px 10px;
853
+ font-size: 11px;
854
+ color: rgba(255,255,255,.5);
855
+ border-bottom: 1px solid rgba(255,255,255,.06);
856
+ }
857
+ #edit-message-minimize {
858
+ width: 20px;
859
+ height: 20px;
860
+ padding: 0;
861
+ background: rgba(255,255,255,.06);
862
+ color: rgba(255,255,255,.4);
863
+ border: 1px solid rgba(255,255,255,.1);
864
+ border-radius: 4px;
865
+ cursor: pointer;
866
+ font-size: 10px;
867
+ display: flex;
868
+ align-items: center;
869
+ justify-content: center;
870
+ transition: all .15s;
871
+ }
872
+ #edit-message-minimize:hover {
873
+ background: rgba(74,158,255,.2);
874
+ border-color: rgba(74,158,255,.4);
875
+ color: #4a9eff;
876
+ }
877
+ #edit-message-list {
878
+ max-height: 150px;
879
+ overflow-y: auto;
880
+ padding: 6px 10px;
881
+ display: flex;
882
+ flex-direction: column;
883
+ gap: 8px;
884
+ scrollbar-width: thin;
885
+ }
886
+ #edit-message-list::-webkit-scrollbar { width: 4px; }
887
+ #edit-message-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,.15); border-radius: 2px; }
888
+ .edit-message-empty {
889
+ font-size: 11px;
890
+ color: rgba(255,255,255,.25);
891
+ padding: 8px 0;
892
+ text-align: center;
893
+ }
894
+ .edit-message-entry {
895
+ font-size: 11px;
896
+ line-height: 1.5;
897
+ padding: 4px 6px;
898
+ border-radius: 6px;
899
+ background: rgba(255,255,255,.03);
900
+ }
901
+ .edit-message-entry.error {
902
+ background: rgba(255,60,60,.08);
903
+ }
904
+ .edit-role {
905
+ font-weight: 600;
906
+ color: rgba(255,255,255,.5);
907
+ margin-right: 4px;
908
+ }
909
+ .edit-message-request {
910
+ color: rgba(255,255,255,.6);
911
+ }
912
+ .edit-message-thinking {
913
+ color: rgba(74,158,255,.7);
914
+ }
915
+ .edit-dots span {
916
+ animation: editDotPulse 1.4s infinite;
917
+ opacity: 0;
918
+ }
919
+ .edit-dots span:nth-child(1) { animation-delay: 0s; }
920
+ .edit-dots span:nth-child(2) { animation-delay: 0.2s; }
921
+ .edit-dots span:nth-child(3) { animation-delay: 0.4s; }
922
+ @keyframes editDotPulse {
923
+ 0% { opacity: 0; }
924
+ 50% { opacity: 1; }
925
+ 100% { opacity: 0; }
926
+ }
927
+ .edit-message-response {
928
+ color: rgba(74,222,128,.8);
929
+ white-space: pre-wrap;
930
+ word-break: break-word;
931
+ }
932
+ .edit-message-error {
933
+ color: rgba(255,100,100,.8);
934
+ }
935
+ #edit-message-bar {
936
+ display: block;
937
+ width: auto;
938
+ padding: 4px 12px;
939
+ margin: 4px 12px;
940
+ border-radius: 14px;
941
+ border: 1px solid rgba(255,255,255,.1);
942
+ background: rgba(74,158,255,.12);
943
+ color: rgba(74,158,255,.8);
944
+ font-size: 11px;
945
+ cursor: pointer;
946
+ transition: all .15s;
947
+ flex-shrink: 0;
948
+ white-space: nowrap;
949
+ }
950
+ #edit-message-bar:hover {
951
+ background: rgba(74,158,255,.2);
952
+ border-color: rgba(74,158,255,.4);
953
+ }
808
954
  </style>
809
955
  </head>
810
956
  <body>
@@ -963,6 +1109,9 @@ Edit request: ${text}`;
963
1109
 
964
1110
  console.log(` 🤖 edit: ${text} (${currentTime !== undefined ? currentTime.toFixed(1) + "s" : ""} ${activeScene || ""})`);
965
1111
 
1112
+ // Push start event to SSE clients for real-time UI feedback
1113
+ ssePush({ type: "edit:start", request: text });
1114
+
966
1115
  // Send to the persistent rpc agent (one cold start; conversation kept in memory)
967
1116
  const result = await sendToAgent(userPrompt);
968
1117
 
@@ -988,10 +1137,12 @@ Edit request: ${text}`;
988
1137
  } else {
989
1138
  const msg = result.error || "failed";
990
1139
  console.error(` ❌ edit ${msg}: ${(result.output || "").trim().substring(0, 100)}`);
1140
+ ssePush({ type: "edit:error", error: msg });
991
1141
  res.writeHead(500, { "Content-Type": "application/json" });
992
1142
  res.end(JSON.stringify({ error: msg, output: (result.output || "").trim().substring(0, 200) }));
993
1143
  }
994
1144
  } catch (e) {
1145
+ ssePush({ type: "edit:error", error: e.message });
995
1146
  res.writeHead(500, { "Content-Type": "application/json" });
996
1147
  res.end(JSON.stringify({ error: e.message }));
997
1148
  }
@@ -1032,7 +1183,10 @@ Edit request: ${text}`;
1032
1183
  try {
1033
1184
  const root = await loadCompiledRoot(variantLabel);
1034
1185
  const rootOut = resolveAssetPaths(root);
1035
- res.writeHead(200, { "Content-Type": "application/json" });
1186
+ res.writeHead(200, {
1187
+ "Content-Type": "application/json",
1188
+ "Cache-Control": "no-store",
1189
+ });
1036
1190
  res.end(JSON.stringify(rootOut));
1037
1191
  } catch (e) {
1038
1192
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -1044,7 +1198,10 @@ Edit request: ${text}`;
1044
1198
  // API: Get scenes with media info for a specific variant
1045
1199
  if (path === "/api/scenes") {
1046
1200
  const { scenes, totalDuration } = getScenes(variantLabel);
1047
- res.writeHead(200, { "Content-Type": "application/json" });
1201
+ res.writeHead(200, {
1202
+ "Content-Type": "application/json",
1203
+ "Cache-Control": "no-store",
1204
+ });
1048
1205
  res.end(JSON.stringify(scenes));
1049
1206
  return;
1050
1207
  }
@@ -1052,7 +1209,10 @@ Edit request: ${text}`;
1052
1209
  // API: Get current video info for a specific variant
1053
1210
  if (path === "/api/video-info") {
1054
1211
  const { scenes, totalDuration } = getScenes(variantLabel);
1055
- res.writeHead(200, { "Content-Type": "application/json" });
1212
+ res.writeHead(200, {
1213
+ "Content-Type": "application/json",
1214
+ "Cache-Control": "no-store",
1215
+ });
1056
1216
  res.end(JSON.stringify({
1057
1217
  scenes,
1058
1218
  totalDuration,
@@ -1115,6 +1275,6 @@ server.listen(PORT, async () => {
1115
1275
  console.log(` ${config.label}: ${url}`);
1116
1276
  }
1117
1277
  }
1118
- if (MODE_EDIT) console.log(` Watching: ${VIDEO_JSON.split("/").pop()}`);
1278
+ console.log(` Watching: ${VIDEO_JSON.split("/").pop()}`);
1119
1279
  console.log("");
1120
1280
  });
@@ -496,7 +496,7 @@ function hasScript(root) {
496
496
 
497
497
  // Compile to validate
498
498
  const compiled = compileDescriptiveRoot(descriptive);
499
- emitSuccess(`Valid. Duration: ~${compiled.durationInSeconds ?? "?"}s, Scenes: ${compiled.children?.length ?? 0}`);
499
+ emitSuccess(`Valid`);
500
500
  process.exit(0);
501
501
 
502
502
  } catch (err) {
@@ -228,6 +228,8 @@ export const mapStream = base.extend({
228
228
  zoom: z.number().default(10),
229
229
  center: z.object({ lat: z.number(), lng: z.number() }).optional().describe("map view center (defaults to first waypoint)"),
230
230
  mapType: z.enum(["roadmap", "satellite", "hybrid", "terrain"]).default("roadmap").describe("Google Maps style"),
231
+ language: z.string().optional().describe("Google Maps UI/label language, e.g. zh-CN"),
232
+ region: z.string().optional().describe("Google Maps region code, e.g. CN"),
231
233
  travelMode: z.enum(["DRIVING", "WALKING", "BICYCLING", "TRANSIT"]).default("DRIVING").describe("Directions API travel mode"),
232
234
  routeMarker: z.string().default("🚗").describe("emoji/character for the animated traveling marker"),
233
235
  googleMapsApiKey: z.string().optional().describe("injected by compiler from GOOGLE_MAPS_API_KEY env var"),