@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 +29 -0
- package/package.json +1 -1
- package/skills/markcut/SKILL.md +12 -45
- package/skills/markcut/docs/components.md +46 -0
- package/skills/markcut/docs/markdown-descriptive.md +12 -0
- package/skills/markcut/docs/sound-effects.md +45 -0
- package/src/config.mjs +2 -2
- package/src/descriptive/compiler.ts +4 -0
- package/src/descriptive/markdown.ts +2 -0
- package/src/player/browser.tsx +95 -5
- package/src/player/bundle/player.js +783 -593
- package/src/player/components/EditControls.tsx +6 -3
- package/src/player/components/EditMessagePanel.tsx +96 -0
- package/src/player/components/HeaderBar.tsx +9 -11
- package/src/player/components/index.ts +1 -0
- package/src/player/pipeline.mjs +6 -2
- package/src/player/server-shared.mjs +4 -1
- package/src/player/server.mjs +202 -42
- package/src/render/cli.mjs +1 -1
- package/src/schema/index.ts +2 -0
- package/src/types/Map.tsx +51 -1
- package/tests/tmp/vision-1784830584961/images/.normalized/test-photo_384.jpg +0 -0
- package/tests/tmp/vision-1784830584961/images/metadata.json +0 -8
- package/tests/tmp/vision-1784830584961/images/test-photo.png +0 -0
- package/tmp/frontmatter-test.ts +0 -21
|
@@ -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
|
-
/**
|
|
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,
|
|
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
|
-
|
|
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";
|
package/src/player/pipeline.mjs
CHANGED
|
@@ -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 "
|
|
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
|
-
|
|
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) {
|
package/src/player/server.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Modes:
|
|
6
6
|
* --label – playback with label input overlay; labels map to media timestamps
|
|
7
|
-
* --edit –
|
|
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)
|
|
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)
|
|
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 (
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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
|
-
|
|
1278
|
+
console.log(` Watching: ${VIDEO_JSON.split("/").pop()}`);
|
|
1119
1279
|
console.log("");
|
|
1120
1280
|
});
|
package/src/render/cli.mjs
CHANGED
|
@@ -496,7 +496,7 @@ function hasScript(root) {
|
|
|
496
496
|
|
|
497
497
|
// Compile to validate
|
|
498
498
|
const compiled = compileDescriptiveRoot(descriptive);
|
|
499
|
-
emitSuccess(`Valid
|
|
499
|
+
emitSuccess(`Valid`);
|
|
500
500
|
process.exit(0);
|
|
501
501
|
|
|
502
502
|
} catch (err) {
|
package/src/schema/index.ts
CHANGED
|
@@ -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"),
|