@lalalic/markcut 1.1.1 → 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.
Files changed (69) hide show
  1. package/AGENTS.md +39 -15
  2. package/README.md +7 -3
  3. package/SKILL.md +26 -172
  4. package/docs/edit-mode.md +1 -1
  5. package/docs/label-mode.md +6 -4
  6. package/docs/markdown-descriptive.md +46 -1
  7. package/docs/system-prompt-edit.md +16 -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/entry.tsx +8 -2
  16. package/src/player/browser.tsx +178 -54
  17. package/src/player/bundle/player.js +1168 -566
  18. package/src/player/components/EditControls.tsx +92 -0
  19. package/src/player/components/HeaderBar.tsx +60 -0
  20. package/src/player/components/LabelControls.tsx +367 -0
  21. package/src/player/components/SceneThumbnails.tsx +87 -0
  22. package/src/player/components/VariantBar.tsx +39 -0
  23. package/src/player/components/index.ts +5 -0
  24. package/src/player/pipeline.mjs +130 -66
  25. package/src/player/pipeline.ts +3 -1
  26. package/src/player/server-shared.mjs +5 -7
  27. package/src/player/server.mjs +194 -187
  28. package/src/render/cli.mjs +66 -13
  29. package/src/schema/index.ts +20 -23
  30. package/src/types/Audio.tsx +25 -33
  31. package/src/types/Component.tsx +18 -24
  32. package/src/types/Effect.tsx +31 -39
  33. package/src/types/Image.tsx +23 -30
  34. package/src/types/Include.tsx +70 -76
  35. package/src/types/Map.tsx +48 -44
  36. package/src/types/Rhythm.tsx +19 -27
  37. package/src/types/Video.tsx +40 -47
  38. package/src/utils/component-import-map.ts +93 -0
  39. package/src/utils/index.ts +23 -10
  40. package/src/vision/cli.mjs +6 -6
  41. package/templates/courseware/TEMPLATE.md +317 -0
  42. package/templates/courseware/agents/reviewer.md +84 -0
  43. package/templates/courseware/prompts/fix.md +30 -0
  44. package/templates/courseware/prompts/outline.md +53 -0
  45. package/templates/courseware/prompts/scene.md +64 -0
  46. package/tests/fixtures/audio.json +4 -2
  47. package/tests/fixtures/basic.json +2 -1
  48. package/tests/fixtures/component-all.json +4 -2
  49. package/tests/fixtures/components.json +4 -2
  50. package/tests/fixtures/effects.json +12 -6
  51. package/tests/fixtures/full.json +8 -4
  52. package/tests/fixtures/map.json +2 -1
  53. package/tests/fixtures/md/dialogue.md +12 -0
  54. package/tests/fixtures/md/edge-cases.md +9 -0
  55. package/tests/fixtures/scenes.json +4 -2
  56. package/tests/fixtures/subtitle.json +6 -3
  57. package/tests/fixtures/subvideo.json +11 -6
  58. package/tests/fixtures/templates/courseware.md +61 -4
  59. package/tests/fixtures/tween-visual.json +2 -6
  60. package/tests/fixtures/video-series.json +6 -14
  61. package/tests/md-descriptive.test.ts +170 -0
  62. package/tests/render.test.ts +32 -16
  63. package/tests/schema.test.ts +6 -3
  64. package/tests/server.test.ts +9 -6
  65. package/tests/utils.ts +4 -4
  66. package/docs/dynamic-components.md +0 -191
  67. package/docs/templates.md +0 -52
  68. package/scripts/artlist-dl.mjs +0 -190
  69. package/src/player/label-server.mjs +0 -599
@@ -132,7 +132,7 @@ export { BarChart } from "npm:recharts"`,
132
132
  });
133
133
 
134
134
  describe("compileDescriptiveRoot", () => {
135
- it("compiles simple series into legacy root+actions", () => {
135
+ it("compiles simple series into flat start/end timing", () => {
136
136
  const compiled = compileDescriptiveRoot({
137
137
  width: 640,
138
138
  height: 480,
@@ -149,13 +149,13 @@ describe("compileDescriptiveRoot", () => {
149
149
  expect(compiled.children).toHaveLength(2);
150
150
 
151
151
  const first = compiled.children[0] as any;
152
- expect(first.actions[0].start).toBe(0);
153
- expect(first.actions[0].end).toBe(2);
152
+ expect(first.start).toBe(0);
153
+ expect(first.end).toBe(2);
154
154
 
155
155
  const second = compiled.children[1] as any;
156
- expect(second.actions[0].startFrom).toBe(1);
157
- expect(second.actions[0].endAt).toBe(4);
158
- expect(second.actions[0].end).toBe(3);
156
+ expect(second.startFrom).toBe(1);
157
+ expect(second.endAt).toBe(4);
158
+ expect(second.end).toBe(3);
159
159
  });
160
160
 
161
161
  it("allows explicit start in parallel root", () => {
@@ -170,8 +170,8 @@ describe("compileDescriptiveRoot", () => {
170
170
  expect(compiled.isSeries).toBe(false);
171
171
  expect(compiled.durationInSeconds).toBe(3);
172
172
  const first = compiled.children[0] as any;
173
- expect(first.actions[0].start).toBe(1);
174
- expect(first.actions[0].end).toBe(3);
173
+ expect(first.start).toBe(1);
174
+ expect(first.end).toBe(3);
175
175
  });
176
176
 
177
177
  it("warns on duplicate ids", () => {
@@ -253,8 +253,8 @@ describe("compileDescriptiveRoot", () => {
253
253
 
254
254
  const includeWithSrc = withSrc.children[0] as any;
255
255
  expect(includeWithSrc.type).toBe("include");
256
- expect(includeWithSrc.actions[0].start).toBe(0);
257
- expect(includeWithSrc.actions[0].end).toBe(4);
256
+ expect(includeWithSrc.start).toBe(0);
257
+ expect(includeWithSrc.end).toBe(4);
258
258
 
259
259
  const withChildren = compileDescriptiveRoot({
260
260
  layout: "series",
@@ -273,7 +273,7 @@ describe("compileDescriptiveRoot", () => {
273
273
  const includeInline = withChildren.children[0] as any;
274
274
  expect(includeInline.type).toBe("include");
275
275
  expect(includeInline.children).toHaveLength(2);
276
- expect(includeInline.actions[0].end).toBe(3);
276
+ expect(includeInline.end).toBe(3);
277
277
  });
278
278
 
279
279
  it("covers remaining types: effect (via effects:[]) and map", () => {
@@ -304,13 +304,13 @@ describe("compileDescriptiveRoot", () => {
304
304
  // effects:[] on image compiles into Effect wrapper stream
305
305
  const effect = compiled.children[0] as any;
306
306
  expect(effect.type).toBe("effect");
307
- expect(effect.actions[0].start).toBe(0);
308
- expect(effect.actions[0].end).toBe(2);
307
+ expect(effect.start).toBe(0);
308
+ expect(effect.end).toBe(2);
309
309
  expect(effect.children).toHaveLength(1);
310
310
 
311
311
  const map = compiled.children.find((c: any) => c.id === "map-1") as any;
312
312
  expect(map.type).toBe("map");
313
- expect(map.actions[0].end).toBe(4);
313
+ expect(map.end).toBe(4);
314
314
  expect(map.waypoints).toHaveLength(2);
315
315
  expect(map.routeMarker).toBe("🚲");
316
316
  expect(map.travelMode).toBe("BICYCLING");
@@ -339,11 +339,11 @@ describe("compileDescriptiveRoot", () => {
339
339
  expect(fx.children[0].type).toBe("image");
340
340
  expect(fx.children[0].src).toBe("hero.jpg");
341
341
  // Effect action spans leaf duration
342
- expect(fx.actions[0].start).toBe(0);
343
- expect(fx.actions[0].end).toBe(3);
342
+ expect(fx.start).toBe(0);
343
+ expect(fx.end).toBe(3);
344
344
  // Inner leaf action is relative (start=0)
345
- expect(fx.children[0].actions[0].start).toBe(0);
346
- expect(fx.children[0].actions[0].end).toBe(3);
345
+ expect(fx.children[0].start).toBe(0);
346
+ expect(fx.children[0].end).toBe(3);
347
347
  });
348
348
 
349
349
  it("compiles leaf node with effects as object specs", () => {
@@ -371,11 +371,11 @@ describe("compileDescriptiveRoot", () => {
371
371
  expect(fx.animationTimingFunction).toBe("ease-out");
372
372
  expect(fx.animationIterationCount).toBe(2);
373
373
  // Outermost effect carries the absolute timing from parallel layout
374
- expect(fx.actions[0].start).toBe(1);
375
- expect(fx.actions[0].end).toBe(3);
374
+ expect(fx.start).toBe(1);
375
+ expect(fx.end).toBe(3);
376
376
  // Inner leaf timing is relative
377
- expect(fx.children[0].actions[0].start).toBe(0);
378
- expect(fx.children[0].actions[0].end).toBe(2);
377
+ expect(fx.children[0].start).toBe(0);
378
+ expect(fx.children[0].end).toBe(2);
379
379
  });
380
380
 
381
381
  it("compiles effects on container (parallel) nodes", () => {
@@ -471,9 +471,9 @@ describe("compileDescriptiveRoot", () => {
471
471
  expect(fx.animationTimingFunction).toBe("ease-out");
472
472
  expect(fx.animationIterationCount).toBe(3);
473
473
  // Effect duration from param overrides to 2s
474
- expect(fx.actions[0].end - fx.actions[0].start).toBe(2);
474
+ expect(fx.end - fx.start).toBe(2);
475
475
  // Inner leaf still plays full 5s
476
- expect(fx.children[0].actions[0].end - fx.children[0].actions[0].start).toBe(5);
476
+ expect(fx.children[0].end - fx.children[0].start).toBe(5);
477
477
  });
478
478
 
479
479
  it("compiles effect with comma-separated positional params (duration, timingFunction, iterationCount)", () => {
@@ -495,7 +495,7 @@ describe("compileDescriptiveRoot", () => {
495
495
  expect(fx.animation).toBe("fadeIn");
496
496
  expect(fx.animationTimingFunction).toBe("ease-in");
497
497
  expect(fx.animationIterationCount).toBe(3);
498
- expect(fx.actions[0].end - fx.actions[0].start).toBe(2);
498
+ expect(fx.end - fx.start).toBe(2);
499
499
  });
500
500
 
501
501
  it("compiles effect with positional params — duration only", () => {
@@ -515,7 +515,7 @@ describe("compileDescriptiveRoot", () => {
515
515
  const fx = compiled.children[0] as any;
516
516
  expect(fx.type).toBe("effect");
517
517
  expect(fx.animation).toBe("fadeIn");
518
- expect(fx.actions[0].end - fx.actions[0].start).toBe(1.5);
518
+ expect(fx.end - fx.start).toBe(1.5);
519
519
  });
520
520
 
521
521
  it("compiles effect with partial positional params — duration + timingFunction", () => {
@@ -536,7 +536,7 @@ describe("compileDescriptiveRoot", () => {
536
536
  expect(fx.animation).toBe("fadeIn");
537
537
  expect(fx.animationTimingFunction).toBe("ease-out");
538
538
  expect(fx.animationIterationCount).toBe(1);
539
- expect(fx.actions[0].end - fx.actions[0].start).toBe(2.5);
539
+ expect(fx.end - fx.start).toBe(2.5);
540
540
  });
541
541
 
542
542
  it("supports deep nested layout containers across all modes", () => {
@@ -726,15 +726,15 @@ describe("compileDescriptiveRoot", () => {
726
726
  expect(rhythm.children).toHaveLength(4);
727
727
 
728
728
  // child[0] starts at beat 0.5, ends at beat 1.5 => duration 1
729
- expect(rhythm.children[0].actions[0].start).toBe(0.5);
730
- expect(rhythm.children[0].actions[0].end).toBe(1.5);
729
+ expect(rhythm.children[0].start).toBe(0.5);
730
+ expect(rhythm.children[0].end).toBe(1.5);
731
731
 
732
732
  // child[1] starts at beat 1.5, ends at beat 2.5 => duration 1
733
- expect(rhythm.children[1].actions[0].start).toBe(1.5);
734
- expect(rhythm.children[1].actions[0].end).toBe(2.5);
733
+ expect(rhythm.children[1].start).toBe(1.5);
734
+ expect(rhythm.children[1].end).toBe(2.5);
735
735
 
736
736
  // last child starts at last beat (3.5), extends by avg gap (1.0) => ends at 4.5
737
- expect(rhythm.children[3].actions[0].end).toBe(4.5);
737
+ expect(rhythm.children[3].end).toBe(4.5);
738
738
  // rhythm duration = last spot + avg gap
739
739
  expect(rhythm.durationInSeconds).toBe(4.5);
740
740
  });
@@ -778,11 +778,11 @@ describe("compileDescriptiveRoot", () => {
778
778
  expect(bgm.type).toBe("audio");
779
779
  expect(bgm.volume).toBe(0.3);
780
780
  expect(bgm.foreground).toBe(false);
781
- expect(bgm.actions[0].end).toBe(5);
781
+ expect(bgm.end).toBe(5);
782
782
 
783
783
  const sfx = compiled.children.find((c: any) => c.id === "sfx") as any;
784
- expect(sfx.actions[0].start).toBe(2);
785
- expect(sfx.actions[0].end).toBe(3);
784
+ expect(sfx.start).toBe(2);
785
+ expect(sfx.end).toBe(3);
786
786
  });
787
787
 
788
788
  it("compiles component node with props", () => {
@@ -801,7 +801,7 @@ describe("compileDescriptiveRoot", () => {
801
801
  const c = compiled.children[0] as any;
802
802
  expect(c.type).toBe("component");
803
803
  expect(c.jsx).toContain("AnimatedHeadline");
804
- expect(c.actions[0].end).toBe(3);
804
+ expect(c.end).toBe(3);
805
805
  });
806
806
 
807
807
  it("compiles rhythm without children as audio leaf", () => {
@@ -817,7 +817,7 @@ describe("compileDescriptiveRoot", () => {
817
817
  expect(r.src).toBe("beat.mp3");
818
818
  expect(r.children).toEqual([]);
819
819
  // rhythm duration = last spot + avg gap = 2.5 + 2.0 = 4.5
820
- expect(r.actions[0].end).toBeCloseTo(4.5);
820
+ expect(r.end).toBeCloseTo(4.5);
821
821
  });
822
822
 
823
823
  it("compiles include with src as leaf", () => {
@@ -831,7 +831,7 @@ describe("compileDescriptiveRoot", () => {
831
831
  const inc = compiled.children[0] as any;
832
832
  expect(inc.type).toBe("include");
833
833
  expect(inc.src).toBe("./child.json");
834
- expect(inc.actions[0].end).toBe(3);
834
+ expect(inc.end).toBe(3);
835
835
  });
836
836
 
837
837
  it("compiles effects:[] with named params (duration, timingFunction, iterationCount)", () => {
@@ -865,10 +865,10 @@ describe("compileDescriptiveRoot", () => {
865
865
  });
866
866
 
867
867
  const img = compiled.children[0] as any;
868
- expect(img.actions[0].end).toBe(3); // image default is 3s
868
+ expect(img.end).toBe(3); // image default is 3s
869
869
 
870
870
  const vid = compiled.children[1] as any;
871
- expect(vid.actions[0].end).toBe(3); // video default is 3s
871
+ expect(vid.end).toBe(3); // video default is 3s
872
872
  });
873
873
 
874
874
  it("uses default duration when duration is missing", () => {
@@ -879,7 +879,7 @@ describe("compileDescriptiveRoot", () => {
879
879
  ],
880
880
  });
881
881
  const img = compiled.children[0] as any;
882
- expect(img.actions[0].end).toBe(3); // image default is 3s
882
+ expect(img.end).toBe(3); // image default is 3s
883
883
  });
884
884
 
885
885
  it("preserves root-level instruction and stylesheet", () => {
@@ -916,7 +916,7 @@ describe("compileDescriptiveRoot", () => {
916
916
  expect(m.type).toBe("map");
917
917
  expect(m.waypoints).toHaveLength(2);
918
918
  expect(m.travelMode).toBe("DRIVING");
919
- expect(m.actions[0].end).toBe(5);
919
+ expect(m.end).toBe(5);
920
920
  });
921
921
 
922
922
  it("filters invisible children", () => {
@@ -18,6 +18,8 @@ import { uid, walkDown } from "../utils/index";
18
18
 
19
19
  export interface CompileOptions {
20
20
  defaults?: Partial<Record<"image" | "video" | "audio" | "component" | "rhythm" | "include" | "map", number>>;
21
+ /** Google Maps API key, injected onto map nodes during compilation. */
22
+ googleMapsApiKey?: string;
21
23
  }
22
24
 
23
25
  /** A single effect spec — either a bare animation name or an object with options. */
@@ -65,6 +67,10 @@ export interface DescriptiveAudio extends DescriptiveBaseNode {
65
67
  src?: string;
66
68
  /** Narration text for TTS generation. When set without src, triggers TTS pipeline. */
67
69
  script?: string;
70
+ /** Speaker name for multi-turn dialogue. Set by resolveDialogue when expanding
71
+ * `SpeakerName: text` format scripts. Used for per-speaker voice selection and
72
+ * subtitle prefix. */
73
+ speaker?: string;
68
74
  volume?: number;
69
75
  foreground?: boolean;
70
76
  startFrom?: number;
@@ -183,6 +189,12 @@ export interface DescriptiveRoot {
183
189
  imports?: never;
184
190
  /** Raw imports block source (from \`\`\`imports code fence). Parsed by parseImportsBlock. */
185
191
  importsBlock?: string;
192
+ /** Per-speaker voice mapping for multi-turn dialogue.
193
+ * Keys are speaker names (e.g. "Ray", "Alice"), values are TTS voice names
194
+ * (e.g. "en-US-GuyNeural", "en-US-JennyNeural").
195
+ * When a dialogue node has a matching `speaker` field, its TTS voice is
196
+ * substituted from this map. */
197
+ voices?: Record<string, string>;
186
198
  children: DescriptiveNode[];
187
199
  }
188
200
 
@@ -250,6 +262,7 @@ export function resolveAllTemplateVars(
250
262
 
251
263
  interface CompileContext {
252
264
  defaults: Record<"image" | "video" | "audio" | "component" | "rhythm" | "include" | "map" | "effect", number>;
265
+ googleMapsApiKey: string;
253
266
  }
254
267
 
255
268
  interface CompileResult {
@@ -422,24 +435,18 @@ function wrapWithEffects(
422
435
  if (!rawEffects || rawEffects.length === 0) return result;
423
436
 
424
437
  const effects = rawEffects.map(normalizeEffectSpec);
425
- const innerStream = result.stream;
426
- const innerActions = (innerStream as any).actions ?? [];
427
- const firstAction = innerActions[0] ?? {};
428
- const absStart = firstAction.start ?? 0;
429
- const absEnd = firstAction.end ?? result.duration;
438
+ const innerStream = result.stream as any;
439
+ const absStart = innerStream.start ?? 0;
440
+ const absEnd = innerStream.end ?? result.duration;
430
441
  const duration = absEnd - absStart;
431
442
 
432
- // Reset inner stream's actions to be relative (start=0) so the effect
443
+ // Reset inner stream's timing to be relative (start=0) so the effect
433
444
  // wrapper owns the absolute timing. The EffectWrapper renders children
434
- // with their relative actions inside its own Sequence.
445
+ // with their relative timing inside its own Sequence.
435
446
  const resetStream = {
436
447
  ...innerStream,
437
- actions: [{
438
- ...firstAction,
439
- id: uid(),
440
- start: 0,
441
- end: duration,
442
- }],
448
+ start: 0,
449
+ end: duration,
443
450
  durationInSeconds: duration,
444
451
  } as any;
445
452
 
@@ -466,11 +473,8 @@ function wrapWithEffects(
466
473
  animationIterationCount: spec.animationIterationCount ?? 1,
467
474
  customKeyframes: spec.customKeyframes,
468
475
  children: [currentStream],
469
- actions: [{
470
- id: uid(),
471
- start: effStart,
472
- end: effEnd,
473
- }],
476
+ start: effStart,
477
+ end: effEnd,
474
478
  visible: true,
475
479
  ...pickOn(node),
476
480
  } as Effect;
@@ -490,21 +494,12 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
490
494
  style: node.style,
491
495
  visible: node.visible ?? true,
492
496
  isBackground: node.isBackground,
493
- durationInSeconds: end,
494
- ...pickOn(node),
495
- };
496
-
497
- const action = {
498
- id: uid(),
499
497
  start,
500
498
  end,
501
499
  startFrom: node.type === "video" || node.type === "audio" ? node.startFrom : undefined,
502
500
  endAt: node.type === "video" || node.type === "audio" ? node.endAt : undefined,
503
- loop: node.type === "audio" ? node.loop : undefined,
504
- volume:
505
- node.type === "video" || node.type === "audio" || node.type === "rhythm"
506
- ? node.volume
507
- : undefined,
501
+ durationInSeconds: end,
502
+ ...pickOn(node),
508
503
  };
509
504
 
510
505
  switch (node.type) {
@@ -517,7 +512,6 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
517
512
  playbackRate: node.playbackRate,
518
513
  width: node.width ?? 1080,
519
514
  height: node.height ?? 1920,
520
- actions: [action],
521
515
  };
522
516
  return { stream, duration: end };
523
517
  }
@@ -528,7 +522,8 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
528
522
  src: node.src,
529
523
  volume: node.volume ?? 1,
530
524
  foreground: node.foreground,
531
- actions: [action],
525
+ loop: node.loop,
526
+ speaker: node.speaker,
532
527
  };
533
528
  return { stream, duration: end };
534
529
  }
@@ -538,7 +533,6 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
538
533
  type: "image",
539
534
  src: node.src,
540
535
  fit: node.fit ?? "contain",
541
- actions: [action],
542
536
  };
543
537
  return { stream, duration: end };
544
538
  }
@@ -561,7 +555,6 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
561
555
  type: "component",
562
556
  jsx: node.jsx,
563
557
  data: Object.keys(bindings).length ? bindings : undefined,
564
- actions: [action],
565
558
  };
566
559
  return { stream, duration: end };
567
560
  }
@@ -574,7 +567,6 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
574
567
  volume: node.volume ?? 1,
575
568
  spots: node.spots,
576
569
  children: [],
577
- actions: [action],
578
570
  };
579
571
  return { stream, duration: end };
580
572
  }
@@ -590,7 +582,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
590
582
  mapType: node.mapType ?? "roadmap",
591
583
  travelMode: node.travelMode ?? "DRIVING",
592
584
  routeMarker: node.routeMarker ?? "🚗",
593
- actions: [action],
585
+ googleMapsApiKey: ctx.googleMapsApiKey,
594
586
  };
595
587
  return { stream, duration: end };
596
588
  }
@@ -668,6 +660,7 @@ function compileScene(
668
660
  type: "folder",
669
661
  visible: true,
670
662
  isSeries: true,
663
+ start: 0,
671
664
  transition: sceneKind === "transitionSeries" ? resolved.name : undefined,
672
665
  transitionTime: sceneKind === "transitionSeries" ? resolved.time : 0.5,
673
666
  children: compiledChildren.map((c) => c.stream),
@@ -691,6 +684,7 @@ function compileScene(
691
684
  style: node.style,
692
685
  visible: node.visible ?? true,
693
686
  isBackground: node.isBackground,
687
+ start,
694
688
  children: sceneChildren,
695
689
  durationInSeconds: end,
696
690
  ...pickOn(node),
@@ -732,13 +726,8 @@ function compileInclude(
732
726
  src: node.src,
733
727
  volume: node.volume ?? 1,
734
728
  children: compiledChildren.map((c) => c.stream),
735
- actions: [
736
- {
737
- id: uid(),
738
- start,
739
- end,
740
- },
741
- ],
729
+ start,
730
+ end,
742
731
  durationInSeconds: end,
743
732
  ...pickOn(node),
744
733
  };
@@ -802,14 +791,8 @@ function compileRhythm(
802
791
  volume: node.volume ?? 1,
803
792
  spots: node.spots,
804
793
  children: compiledChildren,
805
- actions: [
806
- {
807
- id: uid(),
808
- start,
809
- end,
810
- volume: node.volume,
811
- },
812
- ],
794
+ start,
795
+ end,
813
796
  durationInSeconds: end,
814
797
  ...pickOn(node),
815
798
  };
@@ -833,6 +816,7 @@ function compileContainer(node: DescriptiveContainer, ctx: CompileContext, paren
833
816
  style: node.style,
834
817
  visible: node.visible ?? true,
835
818
  isBackground: node.isBackground,
819
+ start: 0,
836
820
  isSeries: node.type !== "parallel",
837
821
  transition: node.type === "transitionSeries" ? resolved.name : undefined,
838
822
  transitionTime: node.type === "transitionSeries" ? resolved.time : 0.5,
@@ -1191,11 +1175,18 @@ export function resolveVariantOverrides(
1191
1175
  }
1192
1176
 
1193
1177
  export function compileDescriptiveRoot(input: DescriptiveRoot, options: CompileOptions = {}): Root {
1178
+ const root: DescriptiveRoot = typeof input === "string" ? JSON.parse(input) : input;
1179
+
1180
+ const resolved = resolveTransition(root.layout ? root.transition ?? root.layout : root.transition, root.transitionTime);
1181
+ const rootKind: "series" | "parallel" | "transitionSeries" = root.layout === "parallel" ? "parallel" : "series";
1182
+
1183
+ const googleMapsApiKey = options.googleMapsApiKey ?? "";
1194
1184
  const ctx: CompileContext = {
1195
1185
  defaults: {
1196
1186
  ...DEFAULTS,
1197
1187
  ...(options.defaults ?? {}),
1198
1188
  },
1189
+ googleMapsApiKey,
1199
1190
  };
1200
1191
 
1201
1192
  // Resolve frontmatter imports / inline component defs onto each component node.
@@ -1204,8 +1195,6 @@ export function compileDescriptiveRoot(input: DescriptiveRoot, options: CompileO
1204
1195
 
1205
1196
  ensureUniqueIds(input.children, "root");
1206
1197
 
1207
- const rootKind = input.layout ?? "series";
1208
- const resolved = resolveTransition(input.transition, input.transitionTime);
1209
1198
  const children = compileChildren(input.children, ctx, rootKind);
1210
1199
  const duration = aggregateDuration(children, rootKind, resolved.time);
1211
1200
 
@@ -101,7 +101,7 @@ function preserveVariantAttrs(node: Record<string, unknown>, attrs: Record<strin
101
101
  "spots", "waypoints", "routeColor", "routeWeight", "routeMarker",
102
102
  "travelMode", "zoom", "center", "mapType", "data", "prompt",
103
103
  "name", "title", "transition", "transitionTime", "layout",
104
- "componentName", "props",
104
+ "componentName", "props", "speaker",
105
105
  ]);
106
106
  for (const [k, v] of Object.entries(attrs)) {
107
107
  if (!STANDARD.has(k)) {
@@ -214,6 +214,7 @@ function parseNodeLine(content: string, lineNum?: number): DescriptiveNode {
214
214
  type: "audio",
215
215
  id: attrs.id as any,
216
216
  src,
217
+ speaker: attrs.speaker as string | undefined,
217
218
  duration: attrs.duration as any,
218
219
  start: attrs.start as any,
219
220
  startFrom: attrs.startFrom as any,
@@ -292,14 +293,14 @@ function parseNodeLine(content: string, lineNum?: number): DescriptiveNode {
292
293
  }
293
294
  case "script": {
294
295
  const raw = firstPositional ?? (attrs.script ? String(attrs.script) : undefined);
295
- if (!raw) throw new DslError("script requires text content", ctx);
296
- // Unquote if needed (standalone `- script "..."` preserves quotes in the token)
297
- const text = isQuoted(raw) ? unquote(raw) : raw;
296
+ // Text may be empty — ~~~script code fence will provide it later
297
+ const text = raw ? (isQuoted(raw) ? unquote(raw) : raw) : undefined;
298
298
  // Script is an alias for audio — creates an audio node with TTS-needed marker.
299
299
  const scriptNode: any = {
300
300
  type: "audio",
301
301
  id: attrs.id as any,
302
302
  script: text,
303
+ speaker: attrs.speaker as string | undefined,
303
304
  volume: (attrs.volume as number | undefined) ?? 1,
304
305
  start: attrs.start as any,
305
306
  duration: attrs.duration as any,
@@ -667,6 +668,9 @@ function applyRootAttrs(root: DescriptiveRoot, attrs: Record<string, unknown>):
667
668
  }
668
669
  break;
669
670
  }
671
+ case "voices":
672
+ root.voices = v as Record<string, string>;
673
+ break;
670
674
  default:
671
675
  throw new Error(`unknown root key: ${k}`);
672
676
  }
@@ -119,7 +119,8 @@ describe("resolveSubtitles", () => {
119
119
  type: "scene", name: "S", layout: "parallel",
120
120
  children: [{
121
121
  id: "a1", type: "audio", src: audioPath,
122
- actions: [{ id: "act1", start: 5, end: 8 }],
122
+ start: 5,
123
+ end: 8,
123
124
  } as any],
124
125
  durationInSeconds: 8,
125
126
  } as any],
@@ -490,7 +491,8 @@ describe("resolveSubtitles — additional", () => {
490
491
  stt: "custom-stt {input} --out {output}",
491
492
  children: [{
492
493
  type: "scene", name: "S", layout: "parallel",
493
- children: [{ id: "a1", type: "audio", src: audioPath, actions: [{ id: "act1", start: 0, end: 2 }] } as any],
494
+ children: [{ id: "a1", type: "audio", src: audioPath, start: 0,
495
+ end: 2 } as any],
494
496
  durationInSeconds: 2,
495
497
  } as any],
496
498
  };
@@ -511,7 +513,8 @@ describe("resolveSubtitles — additional", () => {
511
513
  const root: DescriptiveRoot = {
512
514
  children: [{
513
515
  type: "scene", name: "S", layout: "parallel",
514
- children: [{ id: "a1", type: "audio", src: audioPath, actions: [{ id: "act1", start: 0, end: 2 }] } as any],
516
+ children: [{ id: "a1", type: "audio", src: audioPath, start: 0,
517
+ end: 2 } as any],
515
518
  durationInSeconds: 2,
516
519
  } as any],
517
520
  };
@@ -531,7 +534,8 @@ describe("resolveSubtitles — additional", () => {
531
534
  const root: DescriptiveRoot = {
532
535
  children: [{
533
536
  type: "scene", name: "S", layout: "parallel",
534
- children: [{ id: "a1", type: "audio", src: audioPath, actions: [{ id: "act1", start: 0, end: 2 }] } as any],
537
+ children: [{ id: "a1", type: "audio", src: audioPath, start: 0,
538
+ end: 2 } as any],
535
539
  durationInSeconds: 2,
536
540
  } as any],
537
541
  };
@@ -558,8 +562,10 @@ describe("resolveSubtitles — additional", () => {
558
562
  children: [{
559
563
  type: "scene", name: "S", layout: "parallel",
560
564
  children: [
561
- { id: "a1", type: "audio", src: a1, actions: [{ id: "act1", start: 0, end: 2 }] },
562
- { id: "a2", type: "audio", src: a2, actions: [{ id: "act2", start: 3, end: 5 }] },
565
+ { id: "a1", type: "audio", src: a1, start: 0,
566
+ end: 2 },
567
+ { id: "a2", type: "audio", src: a2, start: 3,
568
+ end: 5 },
563
569
  ],
564
570
  durationInSeconds: 5,
565
571
  }],
@@ -586,7 +592,8 @@ describe("resolveSubtitles — additional", () => {
586
592
  const root: any = {
587
593
  children: [{
588
594
  type: "scene", name: "S", layout: "parallel",
589
- children: [{ id: "a1", type: "audio", src: audioPath, actions: [{ id: "act1", start: 0, end: 2 }] }],
595
+ children: [{ id: "a1", type: "audio", src: audioPath, start: 0,
596
+ end: 2 }],
590
597
  durationInSeconds: 2,
591
598
  }],
592
599
  };