@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.
- package/AGENTS.md +39 -15
- package/README.md +7 -3
- package/SKILL.md +26 -172
- package/docs/edit-mode.md +1 -1
- package/docs/label-mode.md +6 -4
- package/docs/markdown-descriptive.md +46 -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/entry.tsx +8 -2
- 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 +66 -13
- 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/component-import-map.ts +93 -0
- package/src/utils/index.ts +23 -10
- package/src/vision/cli.mjs +6 -6
- package/templates/courseware/TEMPLATE.md +317 -0
- package/templates/courseware/agents/reviewer.md +84 -0
- package/templates/courseware/prompts/fix.md +30 -0
- package/templates/courseware/prompts/outline.md +53 -0
- package/templates/courseware/prompts/scene.md +64 -0
- 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/docs/dynamic-components.md +0 -191
- package/docs/templates.md +0 -52
- package/scripts/artlist-dl.mjs +0 -190
- 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
|
|
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.
|
|
153
|
-
expect(first.
|
|
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.
|
|
157
|
-
expect(second.
|
|
158
|
-
expect(second.
|
|
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.
|
|
174
|
-
expect(first.
|
|
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.
|
|
257
|
-
expect(includeWithSrc.
|
|
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.
|
|
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.
|
|
308
|
-
expect(effect.
|
|
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.
|
|
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.
|
|
343
|
-
expect(fx.
|
|
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].
|
|
346
|
-
expect(fx.children[0].
|
|
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.
|
|
375
|
-
expect(fx.
|
|
374
|
+
expect(fx.start).toBe(1);
|
|
375
|
+
expect(fx.end).toBe(3);
|
|
376
376
|
// Inner leaf timing is relative
|
|
377
|
-
expect(fx.children[0].
|
|
378
|
-
expect(fx.children[0].
|
|
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.
|
|
474
|
+
expect(fx.end - fx.start).toBe(2);
|
|
475
475
|
// Inner leaf still plays full 5s
|
|
476
|
-
expect(fx.children[0].
|
|
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.
|
|
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.
|
|
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.
|
|
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].
|
|
730
|
-
expect(rhythm.children[0].
|
|
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].
|
|
734
|
-
expect(rhythm.children[1].
|
|
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].
|
|
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.
|
|
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.
|
|
785
|
-
expect(sfx.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
868
|
+
expect(img.end).toBe(3); // image default is 3s
|
|
869
869
|
|
|
870
870
|
const vid = compiled.children[1] as any;
|
|
871
|
-
expect(vid.
|
|
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.
|
|
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.
|
|
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
|
|
427
|
-
const
|
|
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
|
|
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
|
|
445
|
+
// with their relative timing inside its own Sequence.
|
|
435
446
|
const resetStream = {
|
|
436
447
|
...innerStream,
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
562
|
-
|
|
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,
|
|
595
|
+
children: [{ id: "a1", type: "audio", src: audioPath, start: 0,
|
|
596
|
+
end: 2 }],
|
|
590
597
|
durationInSeconds: 2,
|
|
591
598
|
}],
|
|
592
599
|
};
|