@lalalic/markcut 1.2.0 → 2.1.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.
Files changed (61) hide show
  1. package/AGENTS.md +40 -17
  2. package/README.md +7 -3
  3. package/SKILL.md +0 -2
  4. package/docs/edit-mode.md +1 -1
  5. package/docs/label-mode.md +6 -4
  6. package/docs/markdown-descriptive.md +34 -1
  7. package/docs/system-prompt-edit.md +19 -0
  8. package/package.json +1 -1
  9. package/src/config.mjs +8 -3
  10. package/src/descriptive/compiler.test.ts +42 -42
  11. package/src/descriptive/compiler.ts +41 -52
  12. package/src/descriptive/markdown.ts +8 -4
  13. package/src/descriptive/resolve.test.ts +14 -7
  14. package/src/descriptive/resolve.ts +148 -20
  15. package/src/player/browser.tsx +178 -54
  16. package/src/player/bundle/player.js +1149 -566
  17. package/src/player/components/EditControls.tsx +92 -0
  18. package/src/player/components/HeaderBar.tsx +60 -0
  19. package/src/player/components/LabelControls.tsx +367 -0
  20. package/src/player/components/SceneThumbnails.tsx +60 -0
  21. package/src/player/components/VariantBar.tsx +39 -0
  22. package/src/player/components/index.ts +5 -0
  23. package/src/player/pipeline.mjs +130 -66
  24. package/src/player/pipeline.ts +3 -1
  25. package/src/player/server-shared.mjs +5 -7
  26. package/src/player/server.mjs +336 -239
  27. package/src/render/cli.mjs +4 -6
  28. package/src/schema/index.ts +20 -23
  29. package/src/types/Audio.tsx +25 -33
  30. package/src/types/Component.tsx +18 -24
  31. package/src/types/Effect.tsx +31 -39
  32. package/src/types/Image.tsx +23 -30
  33. package/src/types/Include.tsx +70 -76
  34. package/src/types/Map.tsx +48 -44
  35. package/src/types/Rhythm.tsx +19 -27
  36. package/src/types/Video.tsx +40 -47
  37. package/src/utils/index.ts +23 -10
  38. package/src/vision/cli.mjs +6 -6
  39. package/templates/courseware/TEMPLATE.md +16 -10
  40. package/tests/fixtures/audio.json +4 -2
  41. package/tests/fixtures/basic.json +2 -1
  42. package/tests/fixtures/component-all.json +4 -2
  43. package/tests/fixtures/components.json +4 -2
  44. package/tests/fixtures/effects.json +12 -6
  45. package/tests/fixtures/full.json +8 -4
  46. package/tests/fixtures/map.json +2 -1
  47. package/tests/fixtures/md/dialogue.md +12 -0
  48. package/tests/fixtures/md/edge-cases.md +9 -0
  49. package/tests/fixtures/scenes.json +4 -2
  50. package/tests/fixtures/subtitle.json +6 -3
  51. package/tests/fixtures/subvideo.json +11 -6
  52. package/tests/fixtures/templates/courseware.md +61 -4
  53. package/tests/fixtures/tween-visual.json +2 -6
  54. package/tests/fixtures/video-series.json +6 -14
  55. package/tests/md-descriptive.test.ts +170 -0
  56. package/tests/render.test.ts +32 -16
  57. package/tests/schema.test.ts +6 -3
  58. package/tests/server.test.ts +9 -6
  59. package/tests/utils.ts +4 -4
  60. package/scripts/artlist-dl.mjs +0 -190
  61. package/src/player/label-server.mjs +0 -599
@@ -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 innerActions = innerStream.actions ?? [];
193
- const firstAction = innerActions[0] ?? {};
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
- actions: [{
200
- ...firstAction,
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
- actions: [{
224
- id: uid(),
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
- loop: node2.type === "audio" ? node2.loop : void 0,
254
- volume: node2.type === "video" || node2.type === "audio" || node2.type === "rhythm" ? node2.volume : void 0
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
- actions: [action]
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
- actions: [action]
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
- actions: [
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
- actions: [
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
- var DEFAULT_TTS_CLI = process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "en-US-GuyNeural" --text "{input}" --write-media "{output}"';
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 DEFAULT_TTI_CLI = 'uvx --from mflux mflux-generate-flux2 --model flux2-klein-4b --steps 5 --prompt "{input}" --output "{output}"';
998
- var DEFAULT_TTV_CLI = "";
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
- if (!raw) throw new DslError("script requires text content", ctx);
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, parent) => {
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, parent });
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, parent } of allScriptNodes) {
10755
- const ttsCli = parent?.tts ?? clone.tts ?? options.ttsCli ?? DEFAULT_TTS_CLI;
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 label = firstWords(node2.script, 8);
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.actions?.[0]?.start ?? 0;
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
- const text3 = lines.slice(lines.indexOf(tline) + 1).join("\n").trim();
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 = {}) {
@@ -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 servers.
3
+ * Shared utilities for the markcut player server.
4
4
  *
5
- * Extracts common code between server.mjs (edit/preview) and
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 action = leaf?.actions?.[0] || s.actions?.[0] || {};
45
- const dur = (action.end || 5) - (action.start || 0);
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 action = child?.actions?.[0];
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,