@lalalic/markcut 2.8.0 → 3.0.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/B] +2 -0
- package/README.md +29 -0
- package/package.json +1 -1
- package/skills/markcut/SKILL.md +12 -45
- package/skills/markcut/docs/components.md +89 -0
- package/skills/markcut/docs/markdown-descriptive.md +17 -2
- package/skills/markcut/docs/sound-effects.md +45 -0
- package/src/components/Markdown.tsx +138 -24
- package/src/components/Mermaid.tsx +223 -22
- package/src/config.mjs +2 -2
- package/src/context/EventContext.tsx +3 -0
- package/src/descriptive/compiler.ts +68 -29
- package/src/descriptive/markdown.ts +20 -0
- package/src/player/browser.tsx +95 -5
- package/src/player/bundle/player.js +1078 -629
- 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 +72 -21
- 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 +4 -1
- package/src/types/Component.tsx +27 -1
- package/src/types/Effect.tsx +13 -6
- package/src/types/Folder.tsx +1 -1
- package/src/types/Map.tsx +51 -1
- package/src/utils/index.ts +14 -2
- package/tests/fixtures/md/animate-diagrams.md +40 -0
- package/tests/fixtures/md/electricity-grow.md +130 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/segments/test-clip_0to3_seg_1100to3000.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/test-clip_0to3.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/test-clip_audio.mp3 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/metadata.json +9 -0
- package/tests/tmp/vision-1785081637127-video/videos/test-clip.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/test-clip.vtt +5 -0
- 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
|
@@ -33,7 +33,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
33
33
|
|
|
34
34
|
// src/utils/index.ts
|
|
35
35
|
function uid() {
|
|
36
|
-
|
|
36
|
+
const raw = Math.random().toString(36).slice(2, 10);
|
|
37
|
+
const first = raw[0];
|
|
38
|
+
if (/^[0-9]/.test(first)) {
|
|
39
|
+
const letter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
|
|
40
|
+
return letter + raw;
|
|
41
|
+
}
|
|
42
|
+
return raw;
|
|
37
43
|
}
|
|
38
44
|
function walkDown(node2, visit, parent = null, depth = 0) {
|
|
39
45
|
const keep = visit(node2, parent, depth);
|
|
@@ -207,7 +213,8 @@ function wrapWithEffects(node2, result, parentKind) {
|
|
|
207
213
|
const absStart = innerStream.start ?? 0;
|
|
208
214
|
const absEnd = innerStream.end ?? result.duration;
|
|
209
215
|
const duration = absEnd - absStart;
|
|
210
|
-
const
|
|
216
|
+
const isBgNoEnd = innerStream.isBackground && innerStream.end == null;
|
|
217
|
+
const resetStream = isBgNoEnd ? { ...innerStream } : {
|
|
211
218
|
...innerStream,
|
|
212
219
|
start: 0,
|
|
213
220
|
end: duration,
|
|
@@ -224,28 +231,33 @@ function wrapWithEffects(node2, result, parentKind) {
|
|
|
224
231
|
id: uid(),
|
|
225
232
|
type: "effect",
|
|
226
233
|
animation: spec.animation,
|
|
234
|
+
animationDurationSeconds: spec.duration,
|
|
227
235
|
durationInSeconds: spec.duration,
|
|
228
236
|
animationTimingFunction: spec.animationTimingFunction,
|
|
229
237
|
animationIterationCount: spec.animationIterationCount ?? 1,
|
|
230
238
|
customKeyframes: spec.customKeyframes,
|
|
231
239
|
children: [currentStream],
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
// For background inner nodes: propagate start/end as-is so parent
|
|
241
|
+
// back-propagation fills the correct scene duration. The effect's
|
|
242
|
+
// durationInSeconds (animation spec) controls animation timing.
|
|
243
|
+
start: isOutermost && isBgNoEnd ? innerStream.start : effStart,
|
|
244
|
+
end: isOutermost && isBgNoEnd ? void 0 : effEnd,
|
|
245
|
+
visible: innerStream.visible ?? true,
|
|
235
246
|
...pickOn(node2)
|
|
236
247
|
};
|
|
237
248
|
}
|
|
238
249
|
return { stream: currentStream, duration: result.duration };
|
|
239
250
|
}
|
|
240
251
|
function compileLeaf(node2, ctx, parentKind) {
|
|
241
|
-
const id = node2.id
|
|
252
|
+
const id = node2.id;
|
|
253
|
+
const hasExplicitId = node2.id != null;
|
|
242
254
|
const hasOwnDuration = typeof node2.duration === "number" || typeof node2.endAt === "number";
|
|
243
255
|
const isBgNoOwnTiming = node2.isBackground && !hasOwnDuration;
|
|
244
256
|
const start = isBgNoOwnTiming ? typeof node2.start === "number" ? node2.start : void 0 : parentKind === "parallel" ? Math.max(0, node2.start ?? 0) : 0;
|
|
245
257
|
const duration = isBgNoOwnTiming ? void 0 : deriveLeafDuration(node2, ctx);
|
|
246
258
|
const end = duration != null ? start + duration : void 0;
|
|
247
259
|
const base = {
|
|
248
|
-
id,
|
|
260
|
+
...id ? { id } : {},
|
|
249
261
|
style: node2.style,
|
|
250
262
|
visible: node2.visible ?? true,
|
|
251
263
|
isBackground: node2.isBackground,
|
|
@@ -305,8 +317,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
305
317
|
const bindings = {};
|
|
306
318
|
for (const key of Object.keys(node2)) {
|
|
307
319
|
if (!KNOWN_COMPONENT_KEYS.has(key)) {
|
|
308
|
-
|
|
309
|
-
if (typeof val === "string") bindings[key] = val;
|
|
320
|
+
bindings[key] = node2[key];
|
|
310
321
|
}
|
|
311
322
|
}
|
|
312
323
|
const stream = {
|
|
@@ -338,6 +349,8 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
338
349
|
zoom: node2.zoom ?? 10,
|
|
339
350
|
center: node2.center,
|
|
340
351
|
mapType: node2.mapType ?? "roadmap",
|
|
352
|
+
language: node2.language,
|
|
353
|
+
region: node2.region,
|
|
341
354
|
travelMode: node2.travelMode ?? "DRIVING",
|
|
342
355
|
routeMarker: node2.routeMarker ?? "\u{1F697}",
|
|
343
356
|
googleMapsApiKey: ctx.googleMapsApiKey
|
|
@@ -405,13 +418,23 @@ function compileScene(node2, ctx, parentKind) {
|
|
|
405
418
|
];
|
|
406
419
|
const sceneContentDuration = sceneKind === "parallel" ? aggregateDuration(compiledChildren, "parallel") : aggregateDuration(compiledChildren, sceneKind, resolved.time);
|
|
407
420
|
const localDuration = Math.max(node2.duration ?? 0, sceneContentDuration);
|
|
408
|
-
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
421
|
+
function backpropagate(stream2, dur) {
|
|
422
|
+
if (stream2.end == null) {
|
|
423
|
+
stream2.end = dur;
|
|
424
|
+
if (stream2.durationInSeconds == null) {
|
|
425
|
+
stream2.durationInSeconds = dur;
|
|
426
|
+
}
|
|
427
|
+
if (stream2.start == null) stream2.start = 0;
|
|
428
|
+
}
|
|
429
|
+
if (stream2.type === "effect" && Array.isArray(stream2.children)) {
|
|
430
|
+
for (const child of stream2.children) {
|
|
431
|
+
backpropagate(child, dur);
|
|
432
|
+
}
|
|
413
433
|
}
|
|
414
434
|
}
|
|
435
|
+
for (const c of compiledChildren) {
|
|
436
|
+
backpropagate(c.stream, localDuration);
|
|
437
|
+
}
|
|
415
438
|
const start = parentKind === "parallel" ? Math.max(0, node2.start ?? 0) : 0;
|
|
416
439
|
const end = start + localDuration;
|
|
417
440
|
const stream = {
|
|
@@ -505,13 +528,23 @@ function compileContainer(node2, ctx, parentKind) {
|
|
|
505
528
|
const resolved = node2.type === "transitionSeries" ? resolveTransition(node2.transition, node2.transitionTime) : { name: "fade", time: 0.5 };
|
|
506
529
|
const children = compileChildren(node2.children, ctx, node2.type);
|
|
507
530
|
const duration = aggregateDuration(children, node2.type, resolved.time);
|
|
508
|
-
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
531
|
+
function backpropagate(stream2, dur) {
|
|
532
|
+
if (stream2.end == null) {
|
|
533
|
+
stream2.end = dur;
|
|
534
|
+
if (stream2.durationInSeconds == null) {
|
|
535
|
+
stream2.durationInSeconds = dur;
|
|
536
|
+
}
|
|
537
|
+
if (stream2.start == null) stream2.start = 0;
|
|
538
|
+
}
|
|
539
|
+
if (stream2.type === "effect" && Array.isArray(stream2.children)) {
|
|
540
|
+
for (const child of stream2.children) {
|
|
541
|
+
backpropagate(child, dur);
|
|
542
|
+
}
|
|
513
543
|
}
|
|
514
544
|
}
|
|
545
|
+
for (const c of children) {
|
|
546
|
+
backpropagate(c.stream, duration);
|
|
547
|
+
}
|
|
515
548
|
const stream = {
|
|
516
549
|
id,
|
|
517
550
|
type: "folder",
|
|
@@ -1062,8 +1095,8 @@ var MAX_VIDEO_DURATION = Number(process.env.MARKCUT_MAX_VIDEO_DURATION) || 60;
|
|
|
1062
1095
|
var MAX_VIDEO_DIMENSION = Number(process.env.MARKCUT_MAX_VIDEO_DIMENSION) || 360;
|
|
1063
1096
|
var GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "";
|
|
1064
1097
|
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 "
|
|
1098
|
+
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';
|
|
1099
|
+
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
1100
|
var DEFAULT_AGENT_CLI = args.cliOverrides.agent || process.env.MARKCUT_AGENT_CLI || "npx pi -p {prompt}";
|
|
1068
1101
|
var DEFAULT_EDIT_CLI = args.cliOverrides.editCli || process.env.MARKCUT_EDIT_CLI || "npx pi --session-id {sessionid} --system-prompt {systemprompt} -p {prompt}";
|
|
1069
1102
|
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}';
|
|
@@ -10120,6 +10153,7 @@ var TYPE_TOKENS = {
|
|
|
10120
10153
|
video: "video",
|
|
10121
10154
|
audio: "audio",
|
|
10122
10155
|
component: "component",
|
|
10156
|
+
event: "event",
|
|
10123
10157
|
rhythm: "rhythm",
|
|
10124
10158
|
include: "include",
|
|
10125
10159
|
map: "map",
|
|
@@ -10339,6 +10373,21 @@ function parseNodeLine(content3, lineNum) {
|
|
|
10339
10373
|
preserveVariantAttrs(node2, attrs);
|
|
10340
10374
|
return node2;
|
|
10341
10375
|
}
|
|
10376
|
+
case "event": {
|
|
10377
|
+
const node2 = {
|
|
10378
|
+
type: "component",
|
|
10379
|
+
id: attrs.id,
|
|
10380
|
+
jsx: "",
|
|
10381
|
+
duration: attrs.duration,
|
|
10382
|
+
start: attrs.start,
|
|
10383
|
+
instruction: attrs.instruction,
|
|
10384
|
+
style: attrs.style,
|
|
10385
|
+
effects: attrs.effects,
|
|
10386
|
+
on: attrs.on
|
|
10387
|
+
};
|
|
10388
|
+
preserveVariantAttrs(node2, attrs);
|
|
10389
|
+
return node2;
|
|
10390
|
+
}
|
|
10342
10391
|
case "rhythm": {
|
|
10343
10392
|
const src = firstPositional ?? attrs.src;
|
|
10344
10393
|
if (!src) throw new DslError("rhythm requires src", ctx);
|
|
@@ -10413,6 +10462,8 @@ function parseNodeLine(content3, lineNum) {
|
|
|
10413
10462
|
zoom: attrs.zoom,
|
|
10414
10463
|
center: attrs.center,
|
|
10415
10464
|
mapType: attrs.mapType,
|
|
10465
|
+
language: attrs.language ?? attrs.lang,
|
|
10466
|
+
region: attrs.region,
|
|
10416
10467
|
instruction: attrs.instruction,
|
|
10417
10468
|
visible: attrs.visible,
|
|
10418
10469
|
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) {
|