@lalalic/markcut 2.5.1 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +15 -16
- package/README.md +89 -0
- package/package.json +1 -1
- package/skills/markcut/SKILL.md +1 -1
- package/skills/markcut/docs/markdown-descriptive.md +2 -2
- package/src/components/StoryboardCaption.tsx +74 -0
- package/src/components/StoryboardInfo.tsx +195 -0
- package/src/components/StoryboardSlot.tsx +96 -0
- package/src/config.mjs +2 -1
- package/src/descriptive/compiler.ts +52 -12
- package/src/descriptive/markdown.ts +5 -1
- package/src/descriptive/resolve.ts +160 -5
- package/src/entry.tsx +28 -5
- package/src/player/bundle/player.js +395 -65
- package/src/player/pipeline.mjs +149 -16
- package/src/player/pipeline.ts +4 -0
- package/src/player/server-shared.mjs +25 -9
- package/src/player/server.mjs +11 -3
- package/src/render/cli.mjs +12 -4
- package/src/schema/index.ts +1 -1
- package/src/types/Folder.tsx +1 -1
- package/src/types/Subtitle.tsx +4 -0
- package/src/vision/cli.mjs +43 -8
- package/tests/tmp/vision-1784830584961/images/.normalized/test-photo_384.jpg +0 -0
- package/tests/tmp/vision-1784830584961/images/metadata.json +8 -0
- package/tests/tmp/vision-1784830584961/images/test-photo.png +0 -0
- package/tests/vision.test.ts +207 -0
- package/tmp/frontmatter-test.ts +21 -0
package/src/player/pipeline.mjs
CHANGED
|
@@ -224,18 +224,20 @@ function wrapWithEffects(node2, result, parentKind) {
|
|
|
224
224
|
}
|
|
225
225
|
function compileLeaf(node2, ctx, parentKind) {
|
|
226
226
|
const id = node2.id ?? uid();
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
const
|
|
227
|
+
const hasOwnDuration = typeof node2.duration === "number" || typeof node2.endAt === "number";
|
|
228
|
+
const isBgNoOwnTiming = node2.isBackground && !hasOwnDuration;
|
|
229
|
+
const start = isBgNoOwnTiming ? typeof node2.start === "number" ? node2.start : void 0 : parentKind === "parallel" ? Math.max(0, node2.start ?? 0) : 0;
|
|
230
|
+
const duration = isBgNoOwnTiming ? void 0 : deriveLeafDuration(node2, ctx);
|
|
231
|
+
const end = duration != null ? start + duration : void 0;
|
|
230
232
|
const base = {
|
|
231
233
|
id,
|
|
232
234
|
style: node2.style,
|
|
233
235
|
visible: node2.visible ?? true,
|
|
234
236
|
isBackground: node2.isBackground,
|
|
235
|
-
start,
|
|
237
|
+
start: isBgNoOwnTiming ? typeof node2.start === "number" ? node2.start : void 0 : start,
|
|
236
238
|
end,
|
|
237
|
-
startFrom: node2.type === "video" || node2.type === "audio" ? node2.startFrom : void 0,
|
|
238
|
-
endAt: node2.type === "video" || node2.type === "audio" ? node2.endAt : void 0,
|
|
239
|
+
startFrom: isBgNoOwnTiming ? void 0 : node2.type === "video" || node2.type === "audio" ? node2.startFrom : void 0,
|
|
240
|
+
endAt: isBgNoOwnTiming ? void 0 : node2.type === "video" || node2.type === "audio" ? node2.endAt : void 0,
|
|
239
241
|
durationInSeconds: end,
|
|
240
242
|
...pickOn(node2)
|
|
241
243
|
};
|
|
@@ -250,7 +252,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
250
252
|
width: node2.width ?? 1080,
|
|
251
253
|
height: node2.height ?? 1920
|
|
252
254
|
};
|
|
253
|
-
return { stream, duration: end };
|
|
255
|
+
return { stream, duration: end ?? 0 };
|
|
254
256
|
}
|
|
255
257
|
case "audio": {
|
|
256
258
|
const stream = {
|
|
@@ -261,7 +263,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
261
263
|
foreground: node2.foreground,
|
|
262
264
|
speaker: node2.speaker
|
|
263
265
|
};
|
|
264
|
-
return { stream, duration: end };
|
|
266
|
+
return { stream, duration: end ?? 0 };
|
|
265
267
|
}
|
|
266
268
|
case "image": {
|
|
267
269
|
const stream = {
|
|
@@ -270,7 +272,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
270
272
|
src: node2.src,
|
|
271
273
|
fit: node2.fit ?? "contain"
|
|
272
274
|
};
|
|
273
|
-
return { stream, duration: end };
|
|
275
|
+
return { stream, duration: end ?? 0 };
|
|
274
276
|
}
|
|
275
277
|
case "component": {
|
|
276
278
|
const KNOWN_COMPONENT_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -298,7 +300,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
298
300
|
jsx: node2.jsx,
|
|
299
301
|
data: Object.keys(bindings).length ? bindings : void 0
|
|
300
302
|
};
|
|
301
|
-
return { stream, duration: end };
|
|
303
|
+
return { stream, duration: end ?? 0 };
|
|
302
304
|
}
|
|
303
305
|
case "rhythm": {
|
|
304
306
|
const stream = {
|
|
@@ -309,7 +311,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
309
311
|
spots: node2.spots,
|
|
310
312
|
children: []
|
|
311
313
|
};
|
|
312
|
-
return { stream, duration: end };
|
|
314
|
+
return { stream, duration: end ?? 0 };
|
|
313
315
|
}
|
|
314
316
|
case "map": {
|
|
315
317
|
const stream = {
|
|
@@ -325,7 +327,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
325
327
|
routeMarker: node2.routeMarker ?? "\u{1F697}",
|
|
326
328
|
googleMapsApiKey: ctx.googleMapsApiKey
|
|
327
329
|
};
|
|
328
|
-
return { stream, duration: end };
|
|
330
|
+
return { stream, duration: end ?? 0 };
|
|
329
331
|
}
|
|
330
332
|
}
|
|
331
333
|
}
|
|
@@ -388,6 +390,13 @@ function compileScene(node2, ctx, parentKind) {
|
|
|
388
390
|
];
|
|
389
391
|
const sceneContentDuration = sceneKind === "parallel" ? aggregateDuration(compiledChildren, "parallel") : aggregateDuration(compiledChildren, sceneKind, resolved.time);
|
|
390
392
|
const localDuration = Math.max(node2.duration ?? 0, sceneContentDuration);
|
|
393
|
+
for (const c of compiledChildren) {
|
|
394
|
+
if (c.stream.isBackground && c.stream.end == null) {
|
|
395
|
+
c.stream.end = localDuration;
|
|
396
|
+
c.stream.durationInSeconds = localDuration;
|
|
397
|
+
if (c.stream.start == null) c.stream.start = 0;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
391
400
|
const start = parentKind === "parallel" ? Math.max(0, node2.start ?? 0) : 0;
|
|
392
401
|
const end = start + localDuration;
|
|
393
402
|
const stream = {
|
|
@@ -481,6 +490,13 @@ function compileContainer(node2, ctx, parentKind) {
|
|
|
481
490
|
const resolved = node2.type === "transitionSeries" ? resolveTransition(node2.transition, node2.transitionTime) : { name: "fade", time: 0.5 };
|
|
482
491
|
const children = compileChildren(node2.children, ctx, node2.type);
|
|
483
492
|
const duration = aggregateDuration(children, node2.type, resolved.time);
|
|
493
|
+
for (const c of children) {
|
|
494
|
+
if (c.stream.isBackground && c.stream.end == null) {
|
|
495
|
+
c.stream.end = duration;
|
|
496
|
+
c.stream.durationInSeconds = duration;
|
|
497
|
+
if (c.stream.start == null) c.stream.start = 0;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
484
500
|
const stream = {
|
|
485
501
|
id,
|
|
486
502
|
type: "folder",
|
|
@@ -722,10 +738,20 @@ function compileDescriptiveRoot(input, options = {}) {
|
|
|
722
738
|
googleMapsApiKey
|
|
723
739
|
};
|
|
724
740
|
const registry = resolveComponentSources(input);
|
|
741
|
+
registry.add("StoryboardSlot");
|
|
742
|
+
registry.add("StoryboardCaption");
|
|
743
|
+
registry.add("StoryboardInfo");
|
|
725
744
|
warnUnregisteredComponents(input, registry);
|
|
726
745
|
ensureUniqueIds(input.children, "root");
|
|
727
746
|
const children = compileChildren(input.children, ctx, rootKind);
|
|
728
747
|
const duration = aggregateDuration(children, rootKind, resolved.time);
|
|
748
|
+
for (const c of children) {
|
|
749
|
+
if (c.stream.isBackground && c.stream.end == null) {
|
|
750
|
+
c.stream.end = duration;
|
|
751
|
+
c.stream.durationInSeconds = duration;
|
|
752
|
+
if (c.stream.start == null) c.stream.start = 0;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
729
755
|
const compiled = {
|
|
730
756
|
id: "root",
|
|
731
757
|
type: "root",
|
|
@@ -985,7 +1011,7 @@ var args = (function parseArgs(argv) {
|
|
|
985
1011
|
"--tti": "tti",
|
|
986
1012
|
"--ttv": "ttv"
|
|
987
1013
|
};
|
|
988
|
-
const args2 = { command: "", file: "", output: "", forceNew: false, verbose: false, dev: false, label: false, edit: false, noBrowser: false, chat: false, port: 3001, compile: false, cli: false, showClis: false, scriptOutputDir: "", mediaOutputDir: "", variant: [], cliOverrides: {} };
|
|
1014
|
+
const args2 = { command: "", file: "", output: "", forceNew: false, verbose: false, dev: false, label: false, edit: false, storyboard: false, noBrowser: false, chat: false, port: 3001, compile: false, cli: false, showClis: false, scriptOutputDir: "", mediaOutputDir: "", variant: [], cliOverrides: {} };
|
|
989
1015
|
let i = 2;
|
|
990
1016
|
if (argv[i]) args2.command = argv[i++];
|
|
991
1017
|
if (argv[i] && !argv[i].startsWith("--")) args2.file = argv[i++];
|
|
@@ -1002,6 +1028,7 @@ var args = (function parseArgs(argv) {
|
|
|
1002
1028
|
else if (flag === "--dev") args2.dev = true;
|
|
1003
1029
|
else if (flag === "--label") args2.label = true;
|
|
1004
1030
|
else if (flag === "--edit") args2.edit = true;
|
|
1031
|
+
else if (flag === "--storyboard") args2.storyboard = true;
|
|
1005
1032
|
else if (flag === "--no-browser") args2.noBrowser = true;
|
|
1006
1033
|
else if (flag === "--port" && argv[i]) args2.port = parseInt(argv[i], 10);
|
|
1007
1034
|
else if (flag.startsWith("--port=")) args2.port = parseInt(flag.split("=")[1], 10);
|
|
@@ -10410,6 +10437,10 @@ function internalParse(markdown) {
|
|
|
10410
10437
|
for (const node2 of mdast.children) {
|
|
10411
10438
|
switch (node2.type) {
|
|
10412
10439
|
case "yaml": {
|
|
10440
|
+
const rawText = node2.value?.trim();
|
|
10441
|
+
if (rawText) {
|
|
10442
|
+
targetRoot().rawFrontmatter = rawText;
|
|
10443
|
+
}
|
|
10413
10444
|
break;
|
|
10414
10445
|
}
|
|
10415
10446
|
case "heading": {
|
|
@@ -10738,6 +10769,7 @@ async function resolveMediaDurations(root, options = {}) {
|
|
|
10738
10769
|
walkDown(clone, (node2) => {
|
|
10739
10770
|
const n = node2;
|
|
10740
10771
|
if (n.type !== "video" && n.type !== "audio") return;
|
|
10772
|
+
if (n.isBackground && typeof n.duration !== "number" && typeof n.endAt !== "number") return;
|
|
10741
10773
|
if (typeof n.duration === "number" && n.duration > 0) return;
|
|
10742
10774
|
if (typeof n.endAt === "number") return;
|
|
10743
10775
|
if (!n.src) return;
|
|
@@ -11046,6 +11078,103 @@ async function resolveGeneratedMedia(root, options) {
|
|
|
11046
11078
|
}
|
|
11047
11079
|
return clone;
|
|
11048
11080
|
}
|
|
11081
|
+
function applyStoryboardOverrides(root, options) {
|
|
11082
|
+
const clone = JSON.parse(JSON.stringify(root));
|
|
11083
|
+
let count = 0;
|
|
11084
|
+
const walk = (nodes) => {
|
|
11085
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
11086
|
+
const n = nodes[i];
|
|
11087
|
+
if ((n.type === "image" || n.type === "video") && typeof n.prompt === "string" && (!n.src || n.src === "auto")) {
|
|
11088
|
+
count++;
|
|
11089
|
+
const kind = n.type === "video" ? "video" : "image";
|
|
11090
|
+
nodes[i] = {
|
|
11091
|
+
id: n.id,
|
|
11092
|
+
type: "component",
|
|
11093
|
+
jsx: `<StoryboardSlot kind="${kind}" prompt={prompt} />`,
|
|
11094
|
+
prompt: n.prompt,
|
|
11095
|
+
duration: n.duration,
|
|
11096
|
+
start: n.start,
|
|
11097
|
+
end: n.end,
|
|
11098
|
+
style: n.style,
|
|
11099
|
+
visible: n.visible,
|
|
11100
|
+
isBackground: n.isBackground,
|
|
11101
|
+
on: n.on,
|
|
11102
|
+
instruction: n.instruction
|
|
11103
|
+
};
|
|
11104
|
+
} else if (n.type === "audio" && typeof n.script === "string" && !n.src) {
|
|
11105
|
+
count++;
|
|
11106
|
+
const dur = typeof n.duration === "number" && n.duration > 0 ? n.duration : 5;
|
|
11107
|
+
nodes[i] = {
|
|
11108
|
+
id: n.id,
|
|
11109
|
+
type: "component",
|
|
11110
|
+
jsx: `<StoryboardCaption script={script}${n.speaker ? " speaker={speaker}" : ""} />`,
|
|
11111
|
+
script: n.script,
|
|
11112
|
+
speaker: n.speaker,
|
|
11113
|
+
duration: dur,
|
|
11114
|
+
start: n.start,
|
|
11115
|
+
end: n.end,
|
|
11116
|
+
style: n.style,
|
|
11117
|
+
visible: n.visible,
|
|
11118
|
+
isBackground: n.isBackground,
|
|
11119
|
+
on: n.on,
|
|
11120
|
+
instruction: n.instruction
|
|
11121
|
+
};
|
|
11122
|
+
} else if (Array.isArray(n.children)) {
|
|
11123
|
+
walk(n.children);
|
|
11124
|
+
}
|
|
11125
|
+
}
|
|
11126
|
+
};
|
|
11127
|
+
walk(clone.children);
|
|
11128
|
+
const rootNode = clone;
|
|
11129
|
+
const configLines = [];
|
|
11130
|
+
if (rootNode.name ?? rootNode.title) configLines.push(`title: ${rootNode.name ?? rootNode.title}`);
|
|
11131
|
+
if (clone.width) configLines.push(`width: ${clone.width}`);
|
|
11132
|
+
if (clone.height) configLines.push(`height: ${clone.height}`);
|
|
11133
|
+
if (clone.fps) configLines.push(`fps: ${clone.fps}`);
|
|
11134
|
+
if (clone.layout) configLines.push(`layout: ${clone.layout}`);
|
|
11135
|
+
if (clone.transition) {
|
|
11136
|
+
const t = clone.transitionTime ? `${clone.transition}(${clone.transitionTime})` : clone.transition;
|
|
11137
|
+
configLines.push(`transition: ${t}`);
|
|
11138
|
+
}
|
|
11139
|
+
if (clone.tts) configLines.push(`tts: ${clone.tts}`);
|
|
11140
|
+
if (clone.stt) configLines.push(`stt: ${clone.stt}`);
|
|
11141
|
+
if (clone.tti) configLines.push(`tti: ${clone.tti}`);
|
|
11142
|
+
if (clone.ttv) configLines.push(`ttv: ${clone.ttv}`);
|
|
11143
|
+
if (clone.seed != null) configLines.push(`seed: ${clone.seed}`);
|
|
11144
|
+
if (clone.stylesheet) configLines.push(`stylesheet: yes`);
|
|
11145
|
+
if (clone.importsBlock) configLines.push(`imports: yes`);
|
|
11146
|
+
if (clone.voices) configLines.push(`voices: ${Object.keys(clone.voices).join(", ")}`);
|
|
11147
|
+
if (clone.metadata) configLines.push(`metadata: ${clone.metadata}`);
|
|
11148
|
+
const configStr = configLines.join(" \xB7 ");
|
|
11149
|
+
const variantStr = (options?.variants ?? []).join(", ");
|
|
11150
|
+
const frontmatter2 = rootNode.rawFrontmatter ?? "";
|
|
11151
|
+
const infoComponent = {
|
|
11152
|
+
type: "component",
|
|
11153
|
+
jsx: `<StoryboardInfo config={config} variants={variants} frontmatter={frontmatter} />`,
|
|
11154
|
+
config: configStr,
|
|
11155
|
+
variants: variantStr,
|
|
11156
|
+
frontmatter: frontmatter2,
|
|
11157
|
+
duration: 3,
|
|
11158
|
+
start: 0,
|
|
11159
|
+
end: 3,
|
|
11160
|
+
visible: true
|
|
11161
|
+
};
|
|
11162
|
+
const infoScene = {
|
|
11163
|
+
type: "scene",
|
|
11164
|
+
id: "storyboard-info",
|
|
11165
|
+
name: "Overview",
|
|
11166
|
+
title: "Storyboard Info",
|
|
11167
|
+
instruction: "Project metadata and variant overview",
|
|
11168
|
+
duration: 3,
|
|
11169
|
+
children: [infoComponent]
|
|
11170
|
+
};
|
|
11171
|
+
clone.children = [infoScene, ...clone.children];
|
|
11172
|
+
count++;
|
|
11173
|
+
if (count > 0) {
|
|
11174
|
+
console.log(` \u{1F3AC} Storyboard: ${count} placeholder${count > 1 ? "s" : ""} (TTI/TTV/TTS preview)`);
|
|
11175
|
+
}
|
|
11176
|
+
return clone;
|
|
11177
|
+
}
|
|
11049
11178
|
async function resolveIncludes(root, options = {}) {
|
|
11050
11179
|
const clone = JSON.parse(JSON.stringify(root));
|
|
11051
11180
|
const baseDir = options.baseDir ?? process.cwd();
|
|
@@ -11136,8 +11265,8 @@ async function resolveAll2(root, options = {}) {
|
|
|
11136
11265
|
const raw = readFileSync(options.sourcePath, "utf-8");
|
|
11137
11266
|
if (options.sourcePath.endsWith(".md")) {
|
|
11138
11267
|
const lines = raw.split("\n");
|
|
11139
|
-
const
|
|
11140
|
-
const insertAt =
|
|
11268
|
+
const videoIdx = lines.findIndex((l) => l.trim().startsWith("# video"));
|
|
11269
|
+
const insertAt = videoIdx >= 0 ? videoIdx + 1 : 1;
|
|
11141
11270
|
lines.splice(insertAt, 0, `seed:${autoSeed}`);
|
|
11142
11271
|
writeFileSync(options.sourcePath, lines.join("\n"), "utf-8");
|
|
11143
11272
|
} else if (options.sourcePath.endsWith(".json")) {
|
|
@@ -11161,6 +11290,9 @@ async function resolveAll2(root, options = {}) {
|
|
|
11161
11290
|
fps: result.fps ?? 30,
|
|
11162
11291
|
variant: options.variants?.[0] ?? "video"
|
|
11163
11292
|
});
|
|
11293
|
+
if (options.storyboard) {
|
|
11294
|
+
result = applyStoryboardOverrides(result, { variants: options.variants });
|
|
11295
|
+
}
|
|
11164
11296
|
result = await resolveMediaSrcs(result, { baseDir: options.baseDir });
|
|
11165
11297
|
if (options.mediaOutputDir) {
|
|
11166
11298
|
result = await resolveGeneratedMedia(result, {
|
|
@@ -11220,7 +11352,8 @@ async function resolveAndCompile(data, options = {}) {
|
|
|
11220
11352
|
sttCli: options.sttCli,
|
|
11221
11353
|
subtitleOutputDir: options.subtitleOutputDir,
|
|
11222
11354
|
seed: options.seed,
|
|
11223
|
-
variants: options.variants
|
|
11355
|
+
variants: options.variants,
|
|
11356
|
+
storyboard: options.storyboard
|
|
11224
11357
|
});
|
|
11225
11358
|
const compiled = compileDescriptiveRoot(resolved, {
|
|
11226
11359
|
googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY
|
package/src/player/pipeline.ts
CHANGED
|
@@ -45,6 +45,9 @@ export interface ResolveAndCompileOptions {
|
|
|
45
45
|
/** Path to the source file (.md or .json). When set and root.seed is missing,
|
|
46
46
|
* a deterministic seed is auto-generated and written back to the source file. */
|
|
47
47
|
sourcePath?: string;
|
|
48
|
+
/** Storyboard mode: skip TTI/TTV generation, convert prompt image/video nodes
|
|
49
|
+
* to component placeholders for fast structural preview. */
|
|
50
|
+
storyboard?: boolean;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/**
|
|
@@ -99,6 +102,7 @@ export async function resolveAndCompile(
|
|
|
99
102
|
subtitleOutputDir: options.subtitleOutputDir,
|
|
100
103
|
seed: options.seed,
|
|
101
104
|
variants: options.variants,
|
|
105
|
+
storyboard: options.storyboard,
|
|
102
106
|
});
|
|
103
107
|
|
|
104
108
|
// 2. Sync compile: descriptive → stream tree
|
|
@@ -40,28 +40,44 @@ export function extractScenes(root) {
|
|
|
40
40
|
for (const s of root.children) {
|
|
41
41
|
if (!(s.type === "folder" || s.type === "scene" || s.children?.length)) continue;
|
|
42
42
|
if (s.isBackground) continue;
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
43
|
+
// Compute scene duration. Priority:
|
|
44
|
+
// 1. s.durationInSeconds (set by engine's getDurationInSeconds)
|
|
45
|
+
// 2. Sum of children's durationInSeconds (for series) or max (for parallel)
|
|
46
|
+
// 3. Leaf node's end-start (backward compat with old compiled JSON)
|
|
47
|
+
// 4. Default 5s
|
|
46
48
|
let dur = s.durationInSeconds;
|
|
47
49
|
if (!dur || dur <= 0) {
|
|
50
|
+
const kids = s.children || [];
|
|
51
|
+
const childDurs = kids
|
|
52
|
+
.filter(c => !c.isBackground)
|
|
53
|
+
.map(c => c.durationInSeconds ?? (c.end != null ? c.end - (c.start ?? 0) : 0))
|
|
54
|
+
.filter(d => d > 0);
|
|
55
|
+
if (childDurs.length > 0) {
|
|
56
|
+
// Series → sum; Parallel → max. We don't know which here, so sum is
|
|
57
|
+
// the safe upper bound (parallel scenes are rare as direct root children).
|
|
58
|
+
dur = childDurs.reduce((a, b) => a + b, 0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!dur || dur <= 0) {
|
|
62
|
+
const leaf = (s.children || []).find(c => c.src && (c.type === "image" || c.type === "video"));
|
|
48
63
|
const src2 = leaf || s;
|
|
49
64
|
dur = (src2.end ?? 5) - (src2.start ?? 0);
|
|
50
65
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
const leaf = (s.children || []).find(c => c.src && (c.type === "image" || c.type === "video"));
|
|
67
|
+
// Use the raw cumulative offset as the scene start time. Transition overlap
|
|
68
|
+
// is a rendering detail — scene thumbnails seek to the raw boundary so the
|
|
69
|
+
// user lands on the first frame of the intended scene, not inside a transition.
|
|
54
70
|
scenes.push({
|
|
55
71
|
name: s.name || s.id || "scene",
|
|
56
|
-
start,
|
|
57
|
-
end:
|
|
72
|
+
start: offset,
|
|
73
|
+
end: offset + dur,
|
|
58
74
|
duration: dur,
|
|
59
75
|
src: leaf?.src || "",
|
|
60
76
|
mediaType: leaf?.type || "unknown",
|
|
61
77
|
});
|
|
62
78
|
offset += dur;
|
|
63
79
|
}
|
|
64
|
-
totalDuration = offset
|
|
80
|
+
totalDuration = offset;
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
// Format 2: scenes wrapped in a "scenes" folder
|
package/src/player/server.mjs
CHANGED
|
@@ -30,6 +30,10 @@ const VIDEO_JSON = inputArg ? resolve(inputArg) : join(ROOT, "video.json");
|
|
|
30
30
|
const IS_MARKDOWN = VIDEO_JSON.endsWith(".md");
|
|
31
31
|
const MODE_LABEL = process.argv.includes("--label");
|
|
32
32
|
const MODE_EDIT = process.argv.includes("--edit");
|
|
33
|
+
// Storyboard mode: skip slow TTI/TTV generation. The compiler converts prompt
|
|
34
|
+
// image/video nodes into component placeholders so the user can review the
|
|
35
|
+
// story structure fast and chat to reshape it.
|
|
36
|
+
const MODE_STORYBOARD = process.argv.includes("--storyboard");
|
|
33
37
|
|
|
34
38
|
// ─── Variant configuration ─────────────────────────────────────────────────
|
|
35
39
|
// Each --variant flag defines a separate compilation target.
|
|
@@ -326,9 +330,11 @@ function resolveAssetPaths(root) {
|
|
|
326
330
|
if (typeof node.src === "string" && !node.src.startsWith("http://") && !node.src.startsWith("https://") && !node.src.startsWith("data:")) {
|
|
327
331
|
// Already a relative path under MARKCUT_BASE (shouldn't be absolute)
|
|
328
332
|
if (!node.src.startsWith("/") && !node.src.startsWith(".")) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
333
|
+
// Plain relative path — prefix with / (server serves from project root)
|
|
334
|
+
node.src = "/" + node.src;
|
|
335
|
+
} else if (node.src.startsWith("./")) {
|
|
336
|
+
// Dot-prefixed relative path — strip ./ and prefix with /
|
|
337
|
+
node.src = "/" + node.src.slice(2);
|
|
332
338
|
}
|
|
333
339
|
// Convert absolute paths under MARKCUT_BASE
|
|
334
340
|
if (typeof node.src === "string" && node.src.startsWith(MARKCUT_BASE)) {
|
|
@@ -442,6 +448,7 @@ async function compileVariant(config, parsed, raw) {
|
|
|
442
448
|
mediaOutputDir: MEDIA_OUTPUT_DIR,
|
|
443
449
|
includeOutputDir: INCLUDE_CACHE_DIR,
|
|
444
450
|
subtitleOutputDir: subtitleDir,
|
|
451
|
+
storyboard: MODE_STORYBOARD,
|
|
445
452
|
variants: config.chain.length > 0 ? config.chain : undefined,
|
|
446
453
|
});
|
|
447
454
|
compiled = compileDescriptiveRoot(resolved);
|
|
@@ -465,6 +472,7 @@ async function compileVariant(config, parsed, raw) {
|
|
|
465
472
|
mediaOutputDir: MEDIA_OUTPUT_DIR,
|
|
466
473
|
includeOutputDir: INCLUDE_CACHE_DIR,
|
|
467
474
|
subtitleOutputDir: subtitleDir,
|
|
475
|
+
storyboard: MODE_STORYBOARD,
|
|
468
476
|
variants: config.chain.length > 0 ? config.chain : undefined,
|
|
469
477
|
});
|
|
470
478
|
} else {
|
package/src/render/cli.mjs
CHANGED
|
@@ -65,6 +65,9 @@ Commands:
|
|
|
65
65
|
|
|
66
66
|
preview <file.json|.md> Open player with live preview
|
|
67
67
|
--label Open label input overlay
|
|
68
|
+
--storyboard Fast structure preview: show TTI/TTV prompts as
|
|
69
|
+
placeholder components (skips slow generation).
|
|
70
|
+
Implies --edit so you can reshape the story.
|
|
68
71
|
--no-browser Skip opening browser automatically
|
|
69
72
|
--port <num> Port for the player server (default: 3001)
|
|
70
73
|
|
|
@@ -72,11 +75,11 @@ Commands:
|
|
|
72
75
|
--output <path> Output path (default: out/video.mp4)
|
|
73
76
|
--verbose Show full per-frame progress (default: compact)
|
|
74
77
|
|
|
75
|
-
vision <folder>
|
|
76
|
-
--label
|
|
78
|
+
vision <folder> Full pipeline: extract → normalize → percept → segments
|
|
79
|
+
--label Add interactive labeling step before AI pipeline
|
|
80
|
+
--instruct "text" Background context about people/places (injected into prompts)
|
|
77
81
|
--prompts-file <path> Path to prompts markdown file (default: vision_prompts.md)
|
|
78
82
|
--vtt-sample-interval <n> Sample one video frame every N seconds (default: 5)
|
|
79
|
-
--context "text" Background context about people/places (injected into prompts)
|
|
80
83
|
--skip-stt Skip speech-to-text for videos
|
|
81
84
|
--dry-run Show what would be processed without running AI
|
|
82
85
|
--show-prompts Print the prompts file and exit
|
|
@@ -209,6 +212,10 @@ edit=${DEFAULT_EDIT_CLI}`);
|
|
|
209
212
|
}
|
|
210
213
|
|
|
211
214
|
if (args.command === "preview") {
|
|
215
|
+
// --storyboard implies --edit: show story structure fast (prompts as
|
|
216
|
+
// placeholder components) and let user chat to reshape before generation.
|
|
217
|
+
if (args.storyboard) args.edit = true;
|
|
218
|
+
|
|
212
219
|
// Auto-detect: markdown files must use the player server (not Remotion Studio)
|
|
213
220
|
const isMarkdown = args.file?.endsWith(".md");
|
|
214
221
|
if (isMarkdown && !args.edit && !args.label) {
|
|
@@ -223,12 +230,13 @@ edit=${DEFAULT_EDIT_CLI}`);
|
|
|
223
230
|
}
|
|
224
231
|
const labelFlag = args.label ? "--label" : "";
|
|
225
232
|
const editFlag = args.edit ? "--edit" : "";
|
|
233
|
+
const storyboardFlag = args.storyboard ? "--storyboard" : "";
|
|
226
234
|
const portFlag = `--port=${args.port || 3001}`;
|
|
227
235
|
const fileFlag = args.file || join(ROOT, "video.json");
|
|
228
236
|
const port = args.port || 3001;
|
|
229
237
|
// Pass variant flags to the server
|
|
230
238
|
const variantFlags = args.variant.map(v => `--variant=${v}`);
|
|
231
|
-
const serverArgs = [playerServer, resolve(fileFlag), labelFlag, editFlag, portFlag, ...variantFlags].filter(Boolean);
|
|
239
|
+
const serverArgs = [playerServer, resolve(fileFlag), labelFlag, editFlag, storyboardFlag, portFlag, ...variantFlags].filter(Boolean);
|
|
232
240
|
const child = spawn("node", serverArgs, { cwd: ROOT, stdio: ["ignore", "pipe", "inherit"] });
|
|
233
241
|
let serverReady = false;
|
|
234
242
|
let stdoutBuffer = "";
|
package/src/schema/index.ts
CHANGED
|
@@ -57,7 +57,7 @@ export const folder = base.extend({
|
|
|
57
57
|
export type Folder = z.infer<typeof folder>;
|
|
58
58
|
|
|
59
59
|
export const subtitleOverlay = z.object({
|
|
60
|
-
src: z.string().describe("path or URL to VTT file covering the full video duration"),
|
|
60
|
+
src: z.string().optional().describe("path or URL to VTT file covering the full video duration; omitted when no subtitles are available"),
|
|
61
61
|
style: z.string().optional().describe("inline css for the overlay container"),
|
|
62
62
|
fontSize: z.union([z.number(), z.string()]).optional(),
|
|
63
63
|
type: z.string().optional().describe("caption animation type: bounce, fade, typewriter, colorful, glowing, neon, etc."),
|
package/src/types/Folder.tsx
CHANGED
|
@@ -172,8 +172,8 @@ export function FolderLeaf({ stream }: { stream: FolderStream }) {
|
|
|
172
172
|
style={containerStyle}
|
|
173
173
|
className={`${orientation} ${stream.type}`.trim()}
|
|
174
174
|
>
|
|
175
|
-
{isSeries ? <TypedSeries>{sequences}</TypedSeries> : sequences}
|
|
176
175
|
{bgContent}
|
|
176
|
+
{isSeries ? <TypedSeries>{sequences}</TypedSeries> : sequences}
|
|
177
177
|
</Container>
|
|
178
178
|
</AudioContext.Provider>
|
|
179
179
|
);
|
package/src/types/Subtitle.tsx
CHANGED
|
@@ -237,6 +237,9 @@ export function SubtitleOverlay({ subtitle }: { subtitle: SubtitleOverlayConfig
|
|
|
237
237
|
const { fps } = useVideoConfig();
|
|
238
238
|
const [cues, setCues] = React.useState<Cue[] | null>(null);
|
|
239
239
|
|
|
240
|
+
// No subtitle source — nothing to render
|
|
241
|
+
if (!subtitle.src) return null;
|
|
242
|
+
|
|
240
243
|
const CaptionComponent = React.useMemo(
|
|
241
244
|
() => resolveCaption(subtitle.type),
|
|
242
245
|
[subtitle.type],
|
|
@@ -244,6 +247,7 @@ export function SubtitleOverlay({ subtitle }: { subtitle: SubtitleOverlayConfig
|
|
|
244
247
|
|
|
245
248
|
React.useEffect(() => {
|
|
246
249
|
const { src } = subtitle;
|
|
250
|
+
if (!src) return;
|
|
247
251
|
|
|
248
252
|
// Inline VTT body
|
|
249
253
|
if (src.includes("-->")) {
|
package/src/vision/cli.mjs
CHANGED
|
@@ -648,9 +648,12 @@ function buildPreviewTree(folder, metadata) {
|
|
|
648
648
|
}
|
|
649
649
|
entries.sort((a, b) => a.created.localeCompare(b.created));
|
|
650
650
|
|
|
651
|
+
// Random seed for TTI/TTV generation.
|
|
652
|
+
const seed = Math.floor(Math.random() * 0xFFFFFFFF);
|
|
653
|
+
|
|
651
654
|
return {
|
|
652
655
|
id: "root", type: "root", width: 1080, height: 1920, fps: 30,
|
|
653
|
-
isSeries: true, transition: "fade", transitionTime: 0.5,
|
|
656
|
+
seed, isSeries: true, transition: "fade", transitionTime: 0.5,
|
|
654
657
|
children: entries.map((e) => ({
|
|
655
658
|
id: e.name, type: "folder", isSeries: false,
|
|
656
659
|
children: [{
|
|
@@ -1093,14 +1096,14 @@ function printUsage() {
|
|
|
1093
1096
|
markcut vision — Analyze images and videos in a folder for video generation
|
|
1094
1097
|
|
|
1095
1098
|
Usage:
|
|
1096
|
-
markcut vision <folder>
|
|
1097
|
-
markcut vision <folder> --label
|
|
1099
|
+
markcut vision <folder> Full pipeline: extract → normalize → percept → segments
|
|
1100
|
+
markcut vision <folder> --label Same as default, plus interactive labeling step before AI
|
|
1098
1101
|
|
|
1099
1102
|
Options:
|
|
1100
|
-
--label Open
|
|
1103
|
+
--label Open interactive labeling preview before running AI pipeline
|
|
1104
|
+
--instruct "text" Background context about people/places (injected into prompts)
|
|
1101
1105
|
--prompts-file <path> Path to prompts markdown file (default: vision_prompts.md)
|
|
1102
1106
|
--vtt-sample-interval <n> Sample one video frame every N seconds (default: 5)
|
|
1103
|
-
--context "text" Background context about people/places (injected into prompts)
|
|
1104
1107
|
--pick <files> Comma-separated filenames to process
|
|
1105
1108
|
--skip-stt Skip speech-to-text for videos
|
|
1106
1109
|
--dry-run Show what would be processed without running AI
|
|
@@ -1134,9 +1137,8 @@ export async function main(args) {
|
|
|
1134
1137
|
|
|
1135
1138
|
else if (flag === "--prompts-file" && args[i]) { promptsFile = resolve(args[i++]); }
|
|
1136
1139
|
else if (flag === "--vtt-sample-interval" && args[i]) { vttSampleInterval = parseInt(args[i++], 10) || DEFAULT_VTT_SAMPLE_INTERVAL; }
|
|
1137
|
-
else if (flag === "--
|
|
1140
|
+
else if (flag === "--instruct" && args[i]) { context = args[i++]; }
|
|
1138
1141
|
else if (flag === "--show-prompts") { console.log(readFileSync(promptsFile, "utf-8")); return; }
|
|
1139
|
-
|
|
1140
1142
|
else if (flag === "--skip-stt") { skipSTT = true; }
|
|
1141
1143
|
else if (flag === "--pick" && args[i]) { for (const f of args[i++].split(",")) pickSet.add(f.trim()); }
|
|
1142
1144
|
else if (flag === "--dry-run") { dryRun = true; }
|
|
@@ -1151,12 +1153,45 @@ export async function main(args) {
|
|
|
1151
1153
|
for (const [name, value] of promptOverrides) prompts.set(name, value);
|
|
1152
1154
|
|
|
1153
1155
|
if (doLabel) {
|
|
1156
|
+
// Full pipeline with interactive labeling
|
|
1154
1157
|
await runFullPipeline(folder, prompts, context, pickSet, vttSampleInterval, skipSTT, dryRun);
|
|
1155
1158
|
} else {
|
|
1156
|
-
|
|
1159
|
+
// Default: full pipeline without labeling (extract → normalize → percept → segments)
|
|
1160
|
+
const metadataPath = join(folder, "metadata.json");
|
|
1161
|
+
if (!existsSync(metadataPath)) {
|
|
1162
|
+
emitInfo(`\n📋 Extracting metadata...`);
|
|
1163
|
+
await runMetadataMode(folder, pickSet, false);
|
|
1164
|
+
}
|
|
1165
|
+
if (!existsSync(metadataPath) || Object.keys(JSON.parse(readFileSync(metadataPath, "utf-8"))).length === 0) {
|
|
1166
|
+
emitError("No media files found — nothing to process.");
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
emitInfo(`\n🔧 Running full pipeline (normalize → perceive)...`);
|
|
1170
|
+
await runNormalizeAndPercept(folder, metadataPath, prompts, context, pickSet, vttSampleInterval, skipSTT);
|
|
1157
1171
|
}
|
|
1158
1172
|
}
|
|
1159
1173
|
|
|
1174
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
1175
|
+
// Exported for testing
|
|
1176
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
1177
|
+
|
|
1178
|
+
export {
|
|
1179
|
+
scanMedia,
|
|
1180
|
+
buildPreviewTree,
|
|
1181
|
+
mergeLabelsIntoMetadata,
|
|
1182
|
+
buildMergedCues,
|
|
1183
|
+
parseVTT,
|
|
1184
|
+
loadPrompts,
|
|
1185
|
+
substituteTemplate,
|
|
1186
|
+
shQuote,
|
|
1187
|
+
normalizePerception,
|
|
1188
|
+
extractRawText,
|
|
1189
|
+
stripThinkBlocks,
|
|
1190
|
+
looseJSONParse,
|
|
1191
|
+
formatTime,
|
|
1192
|
+
timeToSeconds,
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1160
1195
|
// Direct execution
|
|
1161
1196
|
if (process.argv[1] && process.argv[1].includes("src/vision/cli.mjs")) {
|
|
1162
1197
|
main(process.argv).catch((e) => { emitError(e.message); process.exit(1); });
|
|
Binary file
|