@lalalic/markcut 1.2.0 → 2.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/AGENTS.md +39 -17
- package/README.md +7 -3
- package/SKILL.md +0 -2
- package/docs/edit-mode.md +1 -1
- package/docs/label-mode.md +6 -4
- package/docs/markdown-descriptive.md +34 -1
- package/docs/system-prompt-edit.md +16 -0
- package/package.json +1 -1
- package/src/config.mjs +8 -3
- package/src/descriptive/compiler.test.ts +42 -42
- package/src/descriptive/compiler.ts +41 -52
- package/src/descriptive/markdown.ts +8 -4
- package/src/descriptive/resolve.test.ts +14 -7
- package/src/descriptive/resolve.ts +148 -20
- package/src/player/browser.tsx +178 -54
- package/src/player/bundle/player.js +1168 -566
- package/src/player/components/EditControls.tsx +92 -0
- package/src/player/components/HeaderBar.tsx +60 -0
- package/src/player/components/LabelControls.tsx +367 -0
- package/src/player/components/SceneThumbnails.tsx +87 -0
- package/src/player/components/VariantBar.tsx +39 -0
- package/src/player/components/index.ts +5 -0
- package/src/player/pipeline.mjs +130 -66
- package/src/player/pipeline.ts +3 -1
- package/src/player/server-shared.mjs +5 -7
- package/src/player/server.mjs +194 -187
- package/src/render/cli.mjs +4 -6
- package/src/schema/index.ts +20 -23
- package/src/types/Audio.tsx +25 -33
- package/src/types/Component.tsx +18 -24
- package/src/types/Effect.tsx +31 -39
- package/src/types/Image.tsx +23 -30
- package/src/types/Include.tsx +70 -76
- package/src/types/Map.tsx +48 -44
- package/src/types/Rhythm.tsx +19 -27
- package/src/types/Video.tsx +40 -47
- package/src/utils/index.ts +23 -10
- package/src/vision/cli.mjs +6 -6
- package/templates/courseware/TEMPLATE.md +16 -10
- package/tests/fixtures/audio.json +4 -2
- package/tests/fixtures/basic.json +2 -1
- package/tests/fixtures/component-all.json +4 -2
- package/tests/fixtures/components.json +4 -2
- package/tests/fixtures/effects.json +12 -6
- package/tests/fixtures/full.json +8 -4
- package/tests/fixtures/map.json +2 -1
- package/tests/fixtures/md/dialogue.md +12 -0
- package/tests/fixtures/md/edge-cases.md +9 -0
- package/tests/fixtures/scenes.json +4 -2
- package/tests/fixtures/subtitle.json +6 -3
- package/tests/fixtures/subvideo.json +11 -6
- package/tests/fixtures/templates/courseware.md +61 -4
- package/tests/fixtures/tween-visual.json +2 -6
- package/tests/fixtures/video-series.json +6 -14
- package/tests/md-descriptive.test.ts +170 -0
- package/tests/render.test.ts +32 -16
- package/tests/schema.test.ts +6 -3
- package/tests/server.test.ts +9 -6
- package/tests/utils.ts +4 -4
- package/scripts/artlist-dl.mjs +0 -190
- package/src/player/label-server.mjs +0 -599
package/src/player/pipeline.mjs
CHANGED
|
@@ -189,19 +189,13 @@ function wrapWithEffects(node2, result, parentKind) {
|
|
|
189
189
|
if (!rawEffects || rawEffects.length === 0) return result;
|
|
190
190
|
const effects = rawEffects.map(normalizeEffectSpec);
|
|
191
191
|
const innerStream = result.stream;
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const absStart = firstAction.start ?? 0;
|
|
195
|
-
const absEnd = firstAction.end ?? result.duration;
|
|
192
|
+
const absStart = innerStream.start ?? 0;
|
|
193
|
+
const absEnd = innerStream.end ?? result.duration;
|
|
196
194
|
const duration = absEnd - absStart;
|
|
197
195
|
const resetStream = {
|
|
198
196
|
...innerStream,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
id: uid(),
|
|
202
|
-
start: 0,
|
|
203
|
-
end: duration
|
|
204
|
-
}],
|
|
197
|
+
start: 0,
|
|
198
|
+
end: duration,
|
|
205
199
|
durationInSeconds: duration
|
|
206
200
|
};
|
|
207
201
|
let currentStream = resetStream;
|
|
@@ -220,11 +214,8 @@ function wrapWithEffects(node2, result, parentKind) {
|
|
|
220
214
|
animationIterationCount: spec.animationIterationCount ?? 1,
|
|
221
215
|
customKeyframes: spec.customKeyframes,
|
|
222
216
|
children: [currentStream],
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
start: effStart,
|
|
226
|
-
end: effEnd
|
|
227
|
-
}],
|
|
217
|
+
start: effStart,
|
|
218
|
+
end: effEnd,
|
|
228
219
|
visible: true,
|
|
229
220
|
...pickOn(node2)
|
|
230
221
|
};
|
|
@@ -241,17 +232,12 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
241
232
|
style: node2.style,
|
|
242
233
|
visible: node2.visible ?? true,
|
|
243
234
|
isBackground: node2.isBackground,
|
|
244
|
-
durationInSeconds: end,
|
|
245
|
-
...pickOn(node2)
|
|
246
|
-
};
|
|
247
|
-
const action = {
|
|
248
|
-
id: uid(),
|
|
249
235
|
start,
|
|
250
236
|
end,
|
|
251
237
|
startFrom: node2.type === "video" || node2.type === "audio" ? node2.startFrom : void 0,
|
|
252
238
|
endAt: node2.type === "video" || node2.type === "audio" ? node2.endAt : void 0,
|
|
253
|
-
|
|
254
|
-
|
|
239
|
+
durationInSeconds: end,
|
|
240
|
+
...pickOn(node2)
|
|
255
241
|
};
|
|
256
242
|
switch (node2.type) {
|
|
257
243
|
case "video": {
|
|
@@ -262,8 +248,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
262
248
|
volume: node2.volume ?? 1,
|
|
263
249
|
playbackRate: node2.playbackRate,
|
|
264
250
|
width: node2.width ?? 1080,
|
|
265
|
-
height: node2.height ?? 1920
|
|
266
|
-
actions: [action]
|
|
251
|
+
height: node2.height ?? 1920
|
|
267
252
|
};
|
|
268
253
|
return { stream, duration: end };
|
|
269
254
|
}
|
|
@@ -274,7 +259,8 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
274
259
|
src: node2.src,
|
|
275
260
|
volume: node2.volume ?? 1,
|
|
276
261
|
foreground: node2.foreground,
|
|
277
|
-
|
|
262
|
+
loop: node2.loop,
|
|
263
|
+
speaker: node2.speaker
|
|
278
264
|
};
|
|
279
265
|
return { stream, duration: end };
|
|
280
266
|
}
|
|
@@ -283,8 +269,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
283
269
|
...base,
|
|
284
270
|
type: "image",
|
|
285
271
|
src: node2.src,
|
|
286
|
-
fit: node2.fit ?? "contain"
|
|
287
|
-
actions: [action]
|
|
272
|
+
fit: node2.fit ?? "contain"
|
|
288
273
|
};
|
|
289
274
|
return { stream, duration: end };
|
|
290
275
|
}
|
|
@@ -312,8 +297,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
312
297
|
...base,
|
|
313
298
|
type: "component",
|
|
314
299
|
jsx: node2.jsx,
|
|
315
|
-
data: Object.keys(bindings).length ? bindings : void 0
|
|
316
|
-
actions: [action]
|
|
300
|
+
data: Object.keys(bindings).length ? bindings : void 0
|
|
317
301
|
};
|
|
318
302
|
return { stream, duration: end };
|
|
319
303
|
}
|
|
@@ -324,8 +308,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
324
308
|
src: node2.src,
|
|
325
309
|
volume: node2.volume ?? 1,
|
|
326
310
|
spots: node2.spots,
|
|
327
|
-
children: []
|
|
328
|
-
actions: [action]
|
|
311
|
+
children: []
|
|
329
312
|
};
|
|
330
313
|
return { stream, duration: end };
|
|
331
314
|
}
|
|
@@ -341,7 +324,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
341
324
|
mapType: node2.mapType ?? "roadmap",
|
|
342
325
|
travelMode: node2.travelMode ?? "DRIVING",
|
|
343
326
|
routeMarker: node2.routeMarker ?? "\u{1F697}",
|
|
344
|
-
|
|
327
|
+
googleMapsApiKey: ctx.googleMapsApiKey
|
|
345
328
|
};
|
|
346
329
|
return { stream, duration: end };
|
|
347
330
|
}
|
|
@@ -397,6 +380,7 @@ function compileScene(node2, ctx, parentKind) {
|
|
|
397
380
|
type: "folder",
|
|
398
381
|
visible: true,
|
|
399
382
|
isSeries: true,
|
|
383
|
+
start: 0,
|
|
400
384
|
transition: sceneKind === "transitionSeries" ? resolved.name : void 0,
|
|
401
385
|
transitionTime: sceneKind === "transitionSeries" ? resolved.time : 0.5,
|
|
402
386
|
children: compiledChildren.map((c) => c.stream),
|
|
@@ -416,6 +400,7 @@ function compileScene(node2, ctx, parentKind) {
|
|
|
416
400
|
style: node2.style,
|
|
417
401
|
visible: node2.visible ?? true,
|
|
418
402
|
isBackground: node2.isBackground,
|
|
403
|
+
start,
|
|
419
404
|
children: sceneChildren,
|
|
420
405
|
durationInSeconds: end,
|
|
421
406
|
...pickOn(node2)
|
|
@@ -447,13 +432,8 @@ function compileInclude(node2, ctx, parentKind) {
|
|
|
447
432
|
src: node2.src,
|
|
448
433
|
volume: node2.volume ?? 1,
|
|
449
434
|
children: compiledChildren.map((c) => c.stream),
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
id: uid(),
|
|
453
|
-
start,
|
|
454
|
-
end
|
|
455
|
-
}
|
|
456
|
-
],
|
|
435
|
+
start,
|
|
436
|
+
end,
|
|
457
437
|
durationInSeconds: end,
|
|
458
438
|
...pickOn(node2)
|
|
459
439
|
};
|
|
@@ -489,14 +469,8 @@ function compileRhythm(node2, ctx, parentKind) {
|
|
|
489
469
|
volume: node2.volume ?? 1,
|
|
490
470
|
spots: node2.spots,
|
|
491
471
|
children: compiledChildren,
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
id: uid(),
|
|
495
|
-
start,
|
|
496
|
-
end,
|
|
497
|
-
volume: node2.volume
|
|
498
|
-
}
|
|
499
|
-
],
|
|
472
|
+
start,
|
|
473
|
+
end,
|
|
500
474
|
durationInSeconds: end,
|
|
501
475
|
...pickOn(node2)
|
|
502
476
|
};
|
|
@@ -514,6 +488,7 @@ function compileContainer(node2, ctx, parentKind) {
|
|
|
514
488
|
style: node2.style,
|
|
515
489
|
visible: node2.visible ?? true,
|
|
516
490
|
isBackground: node2.isBackground,
|
|
491
|
+
start: 0,
|
|
517
492
|
isSeries: node2.type !== "parallel",
|
|
518
493
|
transition: node2.type === "transitionSeries" ? resolved.name : void 0,
|
|
519
494
|
transitionTime: node2.type === "transitionSeries" ? resolved.time : 0.5,
|
|
@@ -733,17 +708,20 @@ function resolveVariantOverrides(root, variantChain) {
|
|
|
733
708
|
return clone;
|
|
734
709
|
}
|
|
735
710
|
function compileDescriptiveRoot(input, options = {}) {
|
|
711
|
+
const root = typeof input === "string" ? JSON.parse(input) : input;
|
|
712
|
+
const resolved = resolveTransition(root.layout ? root.transition ?? root.layout : root.transition, root.transitionTime);
|
|
713
|
+
const rootKind = root.layout === "parallel" ? "parallel" : "series";
|
|
714
|
+
const googleMapsApiKey = options.googleMapsApiKey ?? "";
|
|
736
715
|
const ctx = {
|
|
737
716
|
defaults: {
|
|
738
717
|
...DEFAULTS,
|
|
739
718
|
...options.defaults ?? {}
|
|
740
|
-
}
|
|
719
|
+
},
|
|
720
|
+
googleMapsApiKey
|
|
741
721
|
};
|
|
742
722
|
const registry = resolveComponentSources(input);
|
|
743
723
|
warnUnregisteredComponents(input, registry);
|
|
744
724
|
ensureUniqueIds(input.children, "root");
|
|
745
|
-
const rootKind = input.layout ?? "series";
|
|
746
|
-
const resolved = resolveTransition(input.transition, input.transitionTime);
|
|
747
725
|
const children = compileChildren(input.children, ctx, rootKind);
|
|
748
726
|
const duration = aggregateDuration(children, rootKind, resolved.time);
|
|
749
727
|
const compiled = {
|
|
@@ -992,10 +970,22 @@ import { join, dirname as dirname2, resolve as resolvePath, relative } from "nod
|
|
|
992
970
|
import { execSync } from "node:child_process";
|
|
993
971
|
import { existsSync, mkdirSync } from "node:fs";
|
|
994
972
|
import { dirname } from "node:path";
|
|
995
|
-
|
|
973
|
+
|
|
974
|
+
// src/config.mjs
|
|
975
|
+
var MAX_IMAGE_DIMENSION = Number(process.env.MARKCUT_MAX_IMAGE_DIMENSION) || 384;
|
|
976
|
+
var MAX_VIDEO_DURATION = Number(process.env.MARKCUT_MAX_VIDEO_DURATION) || 60;
|
|
977
|
+
var MAX_VIDEO_DIMENSION = Number(process.env.MARKCUT_MAX_VIDEO_DIMENSION) || 360;
|
|
978
|
+
var DEFAULT_ITT_CLI = process.env.MARKCUT_ITT_CLI || 'uvx --from mlx-vlm mlx_vlm.generate --model mlx-community/MiniCPM-V-4.6-bf16 --max-tokens 2048 --prompt "{prompt}" --image {input} --temperature 0.0 --thinking-mode disabled';
|
|
979
|
+
var GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "";
|
|
980
|
+
var DEFAULT_VTT_SAMPLE_INTERVAL = Number(process.env.MARKCUT_VTT_SAMPLE_INTERVAL) || 5;
|
|
981
|
+
var DEFAULT_VTT_CLI = process.env.MARKCUT_VTT_CLI || `uvx --from mlx-vlm mlx_vlm.generate --model mlx-community/MiniCPM-V-4.6-bf16 --max-tokens 2048 --prompt "{prompt}" --video {input} --temperature 0.0 --processor-kwargs '{"max_num_frames": 32, "stack_frames": 1, "max_slice_nums": 1, "use_image_id": false}'`;
|
|
996
982
|
var DEFAULT_STT_CLI = process.env.MARKCUT_STT_CLI || 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}"';
|
|
997
|
-
var
|
|
998
|
-
var
|
|
983
|
+
var DEFAULT_TTS_CLI = process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "en-US-GuyNeural" --text "{input}" --write-media "{output}"';
|
|
984
|
+
var DEFAULT_AGENT_CLI = process.env.MARKCUT_AGENT_CLI || "npx pi -p {prompt}";
|
|
985
|
+
var DEFAULT_TTI_CLI = process.env.MARKCUT_TTI_CLI || 'uvx --from mflux mflux-generate-flux2 --model flux2-klein-4b --steps 5 --prompt "{input}" --output "{output}"';
|
|
986
|
+
var DEFAULT_TTV_CLI = process.env.MARKCUT_TTV_CLI || "";
|
|
987
|
+
|
|
988
|
+
// src/render/cli-tools.ts
|
|
999
989
|
function substituteCli(template, input, output) {
|
|
1000
990
|
const safeOutput = output;
|
|
1001
991
|
let safeInput;
|
|
@@ -10128,7 +10118,8 @@ function preserveVariantAttrs(node2, attrs) {
|
|
|
10128
10118
|
"transitionTime",
|
|
10129
10119
|
"layout",
|
|
10130
10120
|
"componentName",
|
|
10131
|
-
"props"
|
|
10121
|
+
"props",
|
|
10122
|
+
"speaker"
|
|
10132
10123
|
]);
|
|
10133
10124
|
for (const [k, v] of Object.entries(attrs)) {
|
|
10134
10125
|
if (!STANDARD.has(k)) {
|
|
@@ -10225,6 +10216,7 @@ function parseNodeLine(content3, lineNum) {
|
|
|
10225
10216
|
type: "audio",
|
|
10226
10217
|
id: attrs.id,
|
|
10227
10218
|
src,
|
|
10219
|
+
speaker: attrs.speaker,
|
|
10228
10220
|
duration: attrs.duration,
|
|
10229
10221
|
start: attrs.start,
|
|
10230
10222
|
startFrom: attrs.startFrom,
|
|
@@ -10302,12 +10294,12 @@ function parseNodeLine(content3, lineNum) {
|
|
|
10302
10294
|
}
|
|
10303
10295
|
case "script": {
|
|
10304
10296
|
const raw = firstPositional ?? (attrs.script ? String(attrs.script) : void 0);
|
|
10305
|
-
|
|
10306
|
-
const text3 = isQuoted(raw) ? unquote(raw) : raw;
|
|
10297
|
+
const text3 = raw ? isQuoted(raw) ? unquote(raw) : raw : void 0;
|
|
10307
10298
|
const scriptNode = {
|
|
10308
10299
|
type: "audio",
|
|
10309
10300
|
id: attrs.id,
|
|
10310
10301
|
script: text3,
|
|
10302
|
+
speaker: attrs.speaker,
|
|
10311
10303
|
volume: attrs.volume ?? 1,
|
|
10312
10304
|
start: attrs.start,
|
|
10313
10305
|
duration: attrs.duration,
|
|
@@ -10595,6 +10587,9 @@ function applyRootAttrs(root, attrs) {
|
|
|
10595
10587
|
}
|
|
10596
10588
|
break;
|
|
10597
10589
|
}
|
|
10590
|
+
case "voices":
|
|
10591
|
+
root.voices = v;
|
|
10592
|
+
break;
|
|
10598
10593
|
default:
|
|
10599
10594
|
throw new Error(`unknown root key: ${k}`);
|
|
10600
10595
|
}
|
|
@@ -10735,29 +10730,92 @@ async function resolveMediaDurations(root, options = {}) {
|
|
|
10735
10730
|
});
|
|
10736
10731
|
return clone;
|
|
10737
10732
|
}
|
|
10733
|
+
function parseDialogueLines(script) {
|
|
10734
|
+
const lines = script.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
10735
|
+
const dialoguePattern = /^([\w\u4e00-\u9fff][\w\s\u4e00-\u9fff]*?):\s*(.+)$/;
|
|
10736
|
+
const result = [];
|
|
10737
|
+
for (const line of lines) {
|
|
10738
|
+
const match = line.match(dialoguePattern);
|
|
10739
|
+
if (!match) return [];
|
|
10740
|
+
result.push({ speaker: match[1].trim(), text: match[2].trim() });
|
|
10741
|
+
}
|
|
10742
|
+
return result.length >= 2 ? result : [];
|
|
10743
|
+
}
|
|
10744
|
+
function resolveDialogue(root) {
|
|
10745
|
+
const clone = JSON.parse(JSON.stringify(root));
|
|
10746
|
+
const expansions = [];
|
|
10747
|
+
walkDown(clone, (node2, parent) => {
|
|
10748
|
+
if (node2.type !== "audio") return;
|
|
10749
|
+
if (!node2.script || typeof node2.script !== "string") return;
|
|
10750
|
+
if (node2.src) return;
|
|
10751
|
+
const lines = parseDialogueLines(node2.script);
|
|
10752
|
+
if (lines.length < 2) return;
|
|
10753
|
+
if (!parent || !Array.isArray(parent.children)) return;
|
|
10754
|
+
const idx = parent.children.indexOf(node2);
|
|
10755
|
+
if (idx === -1) return;
|
|
10756
|
+
expansions.push({ parent, index: idx, originalNode: node2, lines });
|
|
10757
|
+
});
|
|
10758
|
+
if (expansions.length === 0) return root;
|
|
10759
|
+
expansions.sort((a, b) => b.index - a.index);
|
|
10760
|
+
for (const { parent, index: index2, originalNode, lines } of expansions) {
|
|
10761
|
+
const dialogueNodes = [];
|
|
10762
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10763
|
+
const line = lines[i];
|
|
10764
|
+
const { speaker, text: text3 } = line;
|
|
10765
|
+
const lineNode = {
|
|
10766
|
+
...originalNode,
|
|
10767
|
+
id: originalNode.id ? `${originalNode.id}-line-${i}` : void 0,
|
|
10768
|
+
script: text3,
|
|
10769
|
+
speaker,
|
|
10770
|
+
start: 0,
|
|
10771
|
+
duration: void 0
|
|
10772
|
+
// Will be set by TTS duration probing
|
|
10773
|
+
};
|
|
10774
|
+
delete lineNode.src;
|
|
10775
|
+
dialogueNodes.push(lineNode);
|
|
10776
|
+
}
|
|
10777
|
+
const seriesContainer = {
|
|
10778
|
+
type: "series",
|
|
10779
|
+
id: `${originalNode.id ?? "dialogue"}-series`,
|
|
10780
|
+
children: dialogueNodes
|
|
10781
|
+
};
|
|
10782
|
+
parent.children.splice(index2, 1, seriesContainer);
|
|
10783
|
+
}
|
|
10784
|
+
const expanded = expansions.length > 0 ? clone : root;
|
|
10785
|
+
const count = expansions.reduce((sum, e) => sum + e.lines.length, 0);
|
|
10786
|
+
console.log(` \u{1F4AC} dialogue: expanded ${expansions.length} script${expansions.length > 1 ? "s" : ""} into ${count} lines`);
|
|
10787
|
+
return expanded;
|
|
10788
|
+
}
|
|
10738
10789
|
async function resolveScripts(root, options) {
|
|
10739
10790
|
const clone = JSON.parse(JSON.stringify(root));
|
|
10740
10791
|
mkdirSync2(options.outputDir, { recursive: true });
|
|
10741
10792
|
const cache = readCacheManifest(options.outputDir);
|
|
10742
10793
|
let cacheDirty = false;
|
|
10743
10794
|
const allScriptNodes = [];
|
|
10744
|
-
walkDown(clone, (node2
|
|
10795
|
+
walkDown(clone, (node2) => {
|
|
10745
10796
|
if (node2.type !== "audio") return;
|
|
10746
10797
|
if (!node2.script || typeof node2.script !== "string") return;
|
|
10747
10798
|
if (node2.src) return;
|
|
10748
10799
|
const id = node2.id ?? `audio-${allScriptNodes.length}`;
|
|
10749
|
-
allScriptNodes.push({ node: node2, id
|
|
10800
|
+
allScriptNodes.push({ node: node2, id });
|
|
10750
10801
|
});
|
|
10751
10802
|
if (allScriptNodes.length > 0) {
|
|
10752
10803
|
console.log(` \u{1F50A} TTS: generating ${allScriptNodes.length} script${allScriptNodes.length > 1 ? "s" : ""}...`);
|
|
10753
10804
|
}
|
|
10754
|
-
for (const { node: node2, id
|
|
10755
|
-
|
|
10805
|
+
for (const { node: node2, id } of allScriptNodes) {
|
|
10806
|
+
let ttsCli = clone.tts ?? options.ttsCli ?? DEFAULT_TTS_CLI;
|
|
10807
|
+
if (node2.speaker && clone.voices) {
|
|
10808
|
+
const speakerVoice = clone.voices[node2.speaker];
|
|
10809
|
+
if (speakerVoice) {
|
|
10810
|
+
ttsCli = `${ttsCli} ${speakerVoice}`;
|
|
10811
|
+
}
|
|
10812
|
+
}
|
|
10756
10813
|
const cacheKey = computeCacheKey({ script: node2.script, cli: ttsCli });
|
|
10757
10814
|
const audioPath = join(options.outputDir, `${cacheKey}.mp3`);
|
|
10758
10815
|
const cached = checkCache(cache, `tts:${cacheKey}`, cacheKey);
|
|
10759
10816
|
let generated;
|
|
10760
|
-
const
|
|
10817
|
+
const speakerLabel = node2.speaker ? `${node2.speaker}: ` : "";
|
|
10818
|
+
const label = `${speakerLabel}${firstWords(node2.script, 8)}`;
|
|
10761
10819
|
if (cached) {
|
|
10762
10820
|
generated = cached;
|
|
10763
10821
|
console.log(` \u2713 TTS: ${label} (cached)`);
|
|
@@ -10799,10 +10857,10 @@ async function resolveSubtitles(root, options) {
|
|
|
10799
10857
|
for (const node2 of nodes) {
|
|
10800
10858
|
if (node2.isBackground) continue;
|
|
10801
10859
|
const nodeStart = parentIsSeries ? seriesOffset : parentOffset;
|
|
10802
|
-
const actionStart = node2.
|
|
10860
|
+
const actionStart = node2.start ?? 0;
|
|
10803
10861
|
const effectiveOffset = nodeStart + actionStart;
|
|
10804
10862
|
if (node2.type === "audio" && node2.src) {
|
|
10805
|
-
clips.push({ audioSrc: node2.src, offset: effectiveOffset });
|
|
10863
|
+
clips.push({ audioSrc: node2.src, offset: effectiveOffset, speaker: node2.speaker });
|
|
10806
10864
|
}
|
|
10807
10865
|
if (node2.children && node2.children.length > 0) {
|
|
10808
10866
|
let childIsSeries = false;
|
|
@@ -10838,7 +10896,7 @@ async function resolveSubtitles(root, options) {
|
|
|
10838
10896
|
const mergedLines = ["WEBVTT", ""];
|
|
10839
10897
|
let cueIndex = 1;
|
|
10840
10898
|
let sttFailedCount = 0;
|
|
10841
|
-
for (const { audioSrc, offset } of clips) {
|
|
10899
|
+
for (const { audioSrc, offset, speaker } of clips) {
|
|
10842
10900
|
const audioHash = existsSync2(audioSrc) ? createHash("sha1").update(readFileSync(audioSrc)).digest("hex").slice(0, 12) : audioSrc;
|
|
10843
10901
|
const sttCacheKey = computeCacheKey({ audioHash, cli: sttCli });
|
|
10844
10902
|
const sttKey = `stt:${audioSrc.split("/").pop()}`;
|
|
@@ -10883,7 +10941,10 @@ async function resolveSubtitles(root, options) {
|
|
|
10883
10941
|
const sec = (s % 60).toFixed(3).padStart(6, "0");
|
|
10884
10942
|
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${sec}`;
|
|
10885
10943
|
};
|
|
10886
|
-
|
|
10944
|
+
let text3 = lines.slice(lines.indexOf(tline) + 1).join("\n").trim();
|
|
10945
|
+
if (speaker) {
|
|
10946
|
+
text3 = `${speaker}: ${text3}`;
|
|
10947
|
+
}
|
|
10887
10948
|
mergedLines.push(String(cueIndex++));
|
|
10888
10949
|
mergedLines.push(`${formatSec(toSec(a) + offset)} --> ${formatSec(toSec(z) + offset)}`);
|
|
10889
10950
|
mergedLines.push(text3, "");
|
|
@@ -11056,6 +11117,7 @@ async function resolveAll2(root, options = {}) {
|
|
|
11056
11117
|
baseDir: options.baseDir,
|
|
11057
11118
|
skip: options.skip
|
|
11058
11119
|
});
|
|
11120
|
+
result = resolveDialogue(result);
|
|
11059
11121
|
if (options.scriptOutputDir) {
|
|
11060
11122
|
result = await resolveScripts(result, {
|
|
11061
11123
|
outputDir: options.scriptOutputDir,
|
|
@@ -11101,7 +11163,9 @@ async function resolveAndCompile(data, options = {}) {
|
|
|
11101
11163
|
subtitleOutputDir: options.subtitleOutputDir,
|
|
11102
11164
|
variants: options.variants
|
|
11103
11165
|
});
|
|
11104
|
-
const compiled = compileDescriptiveRoot(resolved
|
|
11166
|
+
const compiled = compileDescriptiveRoot(resolved, {
|
|
11167
|
+
googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY
|
|
11168
|
+
});
|
|
11105
11169
|
return compiled;
|
|
11106
11170
|
}
|
|
11107
11171
|
async function resolveAndCompileMarkdown(markdown, options = {}) {
|
package/src/player/pipeline.ts
CHANGED
|
@@ -94,7 +94,9 @@ export async function resolveAndCompile(
|
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
// 2. Sync compile: descriptive → stream tree
|
|
97
|
-
const compiled = compileDescriptiveRoot(resolved
|
|
97
|
+
const compiled = compileDescriptiveRoot(resolved, {
|
|
98
|
+
googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY,
|
|
99
|
+
});
|
|
98
100
|
|
|
99
101
|
return compiled;
|
|
100
102
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Shared utilities for markcut player
|
|
3
|
+
* Shared utilities for the markcut player server.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* label-server.mjs (label annotation) into one place.
|
|
5
|
+
* Common code used by server.mjs across all modes (preview, edit, label).
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { statSync, createReadStream, readFileSync, existsSync } from "node:fs";
|
|
@@ -41,8 +40,8 @@ export function extractScenes(root) {
|
|
|
41
40
|
if (!(s.type === "folder" || s.type === "scene" || s.children?.length)) continue;
|
|
42
41
|
if (s.isBackground) continue;
|
|
43
42
|
const leaf = (s.children || []).find(c => c.src && (c.type === "image" || c.type === "video"));
|
|
44
|
-
const
|
|
45
|
-
const dur = (
|
|
43
|
+
const src2 = leaf || s;
|
|
44
|
+
const dur = (src2.end ?? 5) - (src2.start ?? 0);
|
|
46
45
|
scenes.push({
|
|
47
46
|
name: s.name || s.id || "scene",
|
|
48
47
|
start: offset,
|
|
@@ -62,8 +61,7 @@ export function extractScenes(root) {
|
|
|
62
61
|
let offset = 0;
|
|
63
62
|
for (const s of scenesFolder.children) {
|
|
64
63
|
const child = s.children?.[0] || {};
|
|
65
|
-
const
|
|
66
|
-
const dur = action ? (action.end - action.start) : 5;
|
|
64
|
+
const dur = (child.end ?? ((child.start ?? 0) + 5)) - (child.start ?? 0);
|
|
67
65
|
scenes.push({
|
|
68
66
|
name: s.name,
|
|
69
67
|
start: offset,
|