@lalalic/markcut 2.5.1 โ 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +15 -16
- package/README.md +89 -0
- package/package.json +1 -1
- package/skills/markcut/SKILL.md +1 -1
- package/skills/markcut/docs/markdown-descriptive.md +2 -2
- package/src/components/StoryboardCaption.tsx +74 -0
- package/src/components/StoryboardInfo.tsx +195 -0
- package/src/components/StoryboardSlot.tsx +96 -0
- package/src/config.mjs +2 -1
- package/src/descriptive/compiler.ts +52 -12
- package/src/descriptive/markdown.ts +5 -1
- package/src/descriptive/resolve.ts +160 -5
- package/src/entry.tsx +28 -5
- package/src/player/bundle/player.js +395 -65
- package/src/player/pipeline.mjs +149 -16
- package/src/player/pipeline.ts +4 -0
- package/src/player/server-shared.mjs +25 -9
- package/src/player/server.mjs +11 -3
- package/src/render/cli.mjs +12 -4
- package/src/schema/index.ts +1 -1
- package/src/types/Folder.tsx +1 -1
- package/src/types/Subtitle.tsx +4 -0
- package/src/vision/cli.mjs +43 -8
- package/tests/tmp/vision-1784830584961/images/.normalized/test-photo_384.jpg +0 -0
- package/tests/tmp/vision-1784830584961/images/metadata.json +8 -0
- package/tests/tmp/vision-1784830584961/images/test-photo.png +0 -0
- package/tests/vision.test.ts +207 -0
- package/tmp/frontmatter-test.ts +21 -0
|
@@ -490,19 +490,27 @@ function wrapWithEffects(
|
|
|
490
490
|
|
|
491
491
|
function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | DescriptiveScene | DescriptiveInclude>, ctx: CompileContext, parentKind: "series" | "parallel" | "transitionSeries"): CompileResult {
|
|
492
492
|
const id = node.id ?? uid();
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const
|
|
493
|
+
|
|
494
|
+
// Background nodes without explicit duration/endAt: let parent fill timing later
|
|
495
|
+
const hasOwnDuration = typeof node.duration === "number" || typeof node.endAt === "number";
|
|
496
|
+
const isBgNoOwnTiming = node.isBackground && !hasOwnDuration;
|
|
497
|
+
|
|
498
|
+
// For background without own timing, skip deriving duration โ will be filled by parent
|
|
499
|
+
const start = isBgNoOwnTiming
|
|
500
|
+
? (typeof node.start === "number" ? node.start : undefined)
|
|
501
|
+
: (parentKind === "parallel" ? Math.max(0, node.start ?? 0) : 0);
|
|
502
|
+
const duration = isBgNoOwnTiming ? undefined : deriveLeafDuration(node, ctx);
|
|
503
|
+
const end = duration != null ? start! + duration : undefined;
|
|
496
504
|
|
|
497
505
|
const base = {
|
|
498
506
|
id,
|
|
499
507
|
style: node.style,
|
|
500
508
|
visible: node.visible ?? true,
|
|
501
509
|
isBackground: node.isBackground,
|
|
502
|
-
start,
|
|
510
|
+
start: isBgNoOwnTiming ? (typeof node.start === "number" ? node.start : undefined) : start,
|
|
503
511
|
end,
|
|
504
|
-
startFrom: node.type === "video" || node.type === "audio" ? node.startFrom : undefined,
|
|
505
|
-
endAt: node.type === "video" || node.type === "audio" ? node.endAt : undefined,
|
|
512
|
+
startFrom: isBgNoOwnTiming ? undefined : (node.type === "video" || node.type === "audio" ? node.startFrom : undefined),
|
|
513
|
+
endAt: isBgNoOwnTiming ? undefined : (node.type === "video" || node.type === "audio" ? node.endAt : undefined),
|
|
506
514
|
durationInSeconds: end,
|
|
507
515
|
...pickOn(node),
|
|
508
516
|
};
|
|
@@ -518,7 +526,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
518
526
|
width: node.width ?? 1080,
|
|
519
527
|
height: node.height ?? 1920,
|
|
520
528
|
};
|
|
521
|
-
return { stream, duration: end };
|
|
529
|
+
return { stream, duration: end ?? 0 };
|
|
522
530
|
}
|
|
523
531
|
case "audio": {
|
|
524
532
|
const stream: Audio = {
|
|
@@ -529,7 +537,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
529
537
|
foreground: node.foreground,
|
|
530
538
|
speaker: node.speaker,
|
|
531
539
|
};
|
|
532
|
-
return { stream, duration: end };
|
|
540
|
+
return { stream, duration: end ?? 0 };
|
|
533
541
|
}
|
|
534
542
|
case "image": {
|
|
535
543
|
const stream: Image = {
|
|
@@ -538,7 +546,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
538
546
|
src: node.src,
|
|
539
547
|
fit: node.fit ?? "contain",
|
|
540
548
|
};
|
|
541
|
-
return { stream, duration: end };
|
|
549
|
+
return { stream, duration: end ?? 0 };
|
|
542
550
|
}
|
|
543
551
|
case "component": {
|
|
544
552
|
// Collect extra properties from descriptive node (e.g. `source` from ~~~md source fences)
|
|
@@ -560,7 +568,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
560
568
|
jsx: node.jsx,
|
|
561
569
|
data: Object.keys(bindings).length ? bindings : undefined,
|
|
562
570
|
};
|
|
563
|
-
return { stream, duration: end };
|
|
571
|
+
return { stream, duration: end ?? 0 };
|
|
564
572
|
}
|
|
565
573
|
case "rhythm": {
|
|
566
574
|
// rhythm without children compiles as a plain audio leaf
|
|
@@ -572,7 +580,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
572
580
|
spots: node.spots,
|
|
573
581
|
children: [],
|
|
574
582
|
};
|
|
575
|
-
return { stream, duration: end };
|
|
583
|
+
return { stream, duration: end ?? 0 };
|
|
576
584
|
}
|
|
577
585
|
case "map": {
|
|
578
586
|
const stream: MapStream = {
|
|
@@ -588,7 +596,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
588
596
|
routeMarker: node.routeMarker ?? "๐",
|
|
589
597
|
googleMapsApiKey: ctx.googleMapsApiKey,
|
|
590
598
|
};
|
|
591
|
-
return { stream, duration: end };
|
|
599
|
+
return { stream, duration: end ?? 0 };
|
|
592
600
|
}
|
|
593
601
|
}
|
|
594
602
|
}
|
|
@@ -676,6 +684,16 @@ function compileScene(
|
|
|
676
684
|
? aggregateDuration(compiledChildren, "parallel")
|
|
677
685
|
: aggregateDuration(compiledChildren, sceneKind, resolved.time);
|
|
678
686
|
const localDuration = Math.max(node.duration ?? 0, sceneContentDuration);
|
|
687
|
+
|
|
688
|
+
// Back-propagate parent duration to background children without own timing
|
|
689
|
+
for (const c of compiledChildren) {
|
|
690
|
+
if (c.stream.isBackground && c.stream.end == null) {
|
|
691
|
+
c.stream.end = localDuration;
|
|
692
|
+
c.stream.durationInSeconds = localDuration;
|
|
693
|
+
if (c.stream.start == null) c.stream.start = 0;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
679
697
|
const start = parentKind === "parallel" ? Math.max(0, node.start ?? 0) : 0;
|
|
680
698
|
const end = start + localDuration;
|
|
681
699
|
|
|
@@ -814,6 +832,15 @@ function compileContainer(node: DescriptiveContainer, ctx: CompileContext, paren
|
|
|
814
832
|
const children = compileChildren(node.children, ctx, node.type);
|
|
815
833
|
const duration = aggregateDuration(children, node.type, resolved.time);
|
|
816
834
|
|
|
835
|
+
// Back-propagate parent duration to background children without own timing
|
|
836
|
+
for (const c of children) {
|
|
837
|
+
if (c.stream.isBackground && c.stream.end == null) {
|
|
838
|
+
c.stream.end = duration;
|
|
839
|
+
c.stream.durationInSeconds = duration;
|
|
840
|
+
if (c.stream.start == null) c.stream.start = 0;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
817
844
|
const stream: Folder = {
|
|
818
845
|
id,
|
|
819
846
|
type: "folder",
|
|
@@ -1200,6 +1227,10 @@ export function compileDescriptiveRoot(input: DescriptiveRoot, options: CompileO
|
|
|
1200
1227
|
|
|
1201
1228
|
// Resolve frontmatter imports / inline component defs onto each component node.
|
|
1202
1229
|
const registry = resolveComponentSources(input);
|
|
1230
|
+
// Add built-in components so warnUnregisteredComponents doesn't flag them.
|
|
1231
|
+
registry.add("StoryboardSlot");
|
|
1232
|
+
registry.add("StoryboardCaption");
|
|
1233
|
+
registry.add("StoryboardInfo");
|
|
1203
1234
|
warnUnregisteredComponents(input, registry);
|
|
1204
1235
|
|
|
1205
1236
|
ensureUniqueIds(input.children, "root");
|
|
@@ -1207,6 +1238,15 @@ export function compileDescriptiveRoot(input: DescriptiveRoot, options: CompileO
|
|
|
1207
1238
|
const children = compileChildren(input.children, ctx, rootKind);
|
|
1208
1239
|
const duration = aggregateDuration(children, rootKind, resolved.time);
|
|
1209
1240
|
|
|
1241
|
+
// Back-propagate root duration to background children without own timing
|
|
1242
|
+
for (const c of children) {
|
|
1243
|
+
if (c.stream.isBackground && c.stream.end == null) {
|
|
1244
|
+
c.stream.end = duration;
|
|
1245
|
+
c.stream.durationInSeconds = duration;
|
|
1246
|
+
if (c.stream.start == null) c.stream.start = 0;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1210
1250
|
const compiled: Root = {
|
|
1211
1251
|
id: "root",
|
|
1212
1252
|
type: "root",
|
|
@@ -401,7 +401,11 @@ function internalParse(markdown: string): VariantParseResult {
|
|
|
401
401
|
for (const node of (mdast as any).children) {
|
|
402
402
|
switch (node.type) {
|
|
403
403
|
case "yaml": {
|
|
404
|
-
// Frontmatter is metadata only โ
|
|
404
|
+
// Frontmatter is metadata only โ store raw text for storyboard preview
|
|
405
|
+
const rawText = (node as any).value?.trim();
|
|
406
|
+
if (rawText) {
|
|
407
|
+
(targetRoot() as any).rawFrontmatter = rawText;
|
|
408
|
+
}
|
|
405
409
|
break;
|
|
406
410
|
}
|
|
407
411
|
case "heading": {
|
|
@@ -198,6 +198,8 @@ export async function resolveMediaDurations(
|
|
|
198
198
|
walkDown(clone as any, (node) => {
|
|
199
199
|
const n = node as DescriptiveNode;
|
|
200
200
|
if (n.type !== "video" && n.type !== "audio") return;
|
|
201
|
+
// Skip background nodes without explicit timing โ parent container fills duration
|
|
202
|
+
if (n.isBackground && typeof n.duration !== "number" && typeof n.endAt !== "number") return;
|
|
201
203
|
if (typeof n.duration === "number" && n.duration > 0) return;
|
|
202
204
|
if (typeof n.endAt === "number") return; // trim already specified
|
|
203
205
|
if (!n.src) return;
|
|
@@ -739,6 +741,148 @@ export async function resolveGeneratedMedia(
|
|
|
739
741
|
return clone;
|
|
740
742
|
}
|
|
741
743
|
|
|
744
|
+
// โโ Storyboard Mode โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Walk the descriptive tree and replace time-consuming nodes with built-in
|
|
748
|
+
* component placeholders so the user can preview the story structure fast:
|
|
749
|
+
*
|
|
750
|
+
* - `image`/`video` with `prompt` but no `src` โ `<StoryboardSlot>` (TTI/TTV placeholder)
|
|
751
|
+
* - `audio` with `script` but no `src` โ `<StoryboardCaption>` (caption-style text)
|
|
752
|
+
*
|
|
753
|
+
* Also prepends a `<StoryboardInfo>` card showing root metadata (title,
|
|
754
|
+
* dimensions, fps, variants) at the very first frame.
|
|
755
|
+
*
|
|
756
|
+
* Called early in `resolveAll` (after template-variable resolution). Since the
|
|
757
|
+
* nodes are now component type, subsequent steps (resolveGeneratedMedia,
|
|
758
|
+
* resolveMediaDurations, etc.) simply skip them โ no special-case logic needed.
|
|
759
|
+
*
|
|
760
|
+
* Only runs when `options.storyboard === true`.
|
|
761
|
+
*/
|
|
762
|
+
export function applyStoryboardOverrides(
|
|
763
|
+
root: DescriptiveRoot,
|
|
764
|
+
options?: { variants?: string[] },
|
|
765
|
+
): DescriptiveRoot {
|
|
766
|
+
const clone: DescriptiveRoot = JSON.parse(JSON.stringify(root));
|
|
767
|
+
let count = 0;
|
|
768
|
+
|
|
769
|
+
const walk = (nodes: any[]) => {
|
|
770
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
771
|
+
const n = nodes[i];
|
|
772
|
+
if (
|
|
773
|
+
(n.type === "image" || n.type === "video") &&
|
|
774
|
+
typeof n.prompt === "string" &&
|
|
775
|
+
(!n.src || n.src === "auto")
|
|
776
|
+
) {
|
|
777
|
+
// โโ Image/Video prompt โ StoryboardSlot โโโโโโโโโโโโโโโโโโโโโโโ
|
|
778
|
+
count++;
|
|
779
|
+
const kind = n.type === "video" ? "video" : "image";
|
|
780
|
+
nodes[i] = {
|
|
781
|
+
id: n.id,
|
|
782
|
+
type: "component",
|
|
783
|
+
jsx: `<StoryboardSlot kind="${kind}" prompt={prompt} />`,
|
|
784
|
+
prompt: n.prompt,
|
|
785
|
+
duration: n.duration,
|
|
786
|
+
start: n.start,
|
|
787
|
+
end: n.end,
|
|
788
|
+
style: n.style,
|
|
789
|
+
visible: n.visible,
|
|
790
|
+
isBackground: n.isBackground,
|
|
791
|
+
on: n.on,
|
|
792
|
+
instruction: n.instruction,
|
|
793
|
+
};
|
|
794
|
+
} else if (
|
|
795
|
+
n.type === "audio" &&
|
|
796
|
+
typeof n.script === "string" &&
|
|
797
|
+
(!n.src)
|
|
798
|
+
) {
|
|
799
|
+
// โโ Audio script โ StoryboardCaption โโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
800
|
+
count++;
|
|
801
|
+
// Default to 5s so the caption is readable; use node duration if set.
|
|
802
|
+
const dur = (typeof n.duration === "number" && n.duration > 0) ? n.duration : 5;
|
|
803
|
+
nodes[i] = {
|
|
804
|
+
id: n.id,
|
|
805
|
+
type: "component",
|
|
806
|
+
jsx: `<StoryboardCaption script={script}${n.speaker ? " speaker={speaker}" : ""} />`,
|
|
807
|
+
script: n.script,
|
|
808
|
+
speaker: n.speaker,
|
|
809
|
+
duration: dur,
|
|
810
|
+
start: n.start,
|
|
811
|
+
end: n.end,
|
|
812
|
+
style: n.style,
|
|
813
|
+
visible: n.visible,
|
|
814
|
+
isBackground: n.isBackground,
|
|
815
|
+
on: n.on,
|
|
816
|
+
instruction: n.instruction,
|
|
817
|
+
};
|
|
818
|
+
} else if (Array.isArray(n.children)) {
|
|
819
|
+
walk(n.children);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
walk(clone.children);
|
|
825
|
+
|
|
826
|
+
// Prepend a StoryboardInfo card with root metadata at the first frame.
|
|
827
|
+
// This lets the user see project context immediately.
|
|
828
|
+
const rootNode = clone as any;
|
|
829
|
+
// All binding values must be strings (the compiler's component case only
|
|
830
|
+
// collects string fields). Non-string values are stringified.
|
|
831
|
+
|
|
832
|
+
// Build a formatted config summary of all root-level frontmatter fields.
|
|
833
|
+
const configLines: string[] = [];
|
|
834
|
+
if (rootNode.name ?? rootNode.title) configLines.push(`title: ${rootNode.name ?? rootNode.title}`);
|
|
835
|
+
if (clone.width) configLines.push(`width: ${clone.width}`);
|
|
836
|
+
if (clone.height) configLines.push(`height: ${clone.height}`);
|
|
837
|
+
if (clone.fps) configLines.push(`fps: ${clone.fps}`);
|
|
838
|
+
if (clone.layout) configLines.push(`layout: ${clone.layout}`);
|
|
839
|
+
if (clone.transition) {
|
|
840
|
+
const t = clone.transitionTime ? `${clone.transition}(${clone.transitionTime})` : clone.transition;
|
|
841
|
+
configLines.push(`transition: ${t}`);
|
|
842
|
+
}
|
|
843
|
+
if (clone.tts) configLines.push(`tts: ${clone.tts}`);
|
|
844
|
+
if (clone.stt) configLines.push(`stt: ${clone.stt}`);
|
|
845
|
+
if (clone.tti) configLines.push(`tti: ${clone.tti}`);
|
|
846
|
+
if (clone.ttv) configLines.push(`ttv: ${clone.ttv}`);
|
|
847
|
+
if (clone.seed != null) configLines.push(`seed: ${clone.seed}`);
|
|
848
|
+
if (clone.stylesheet) configLines.push(`stylesheet: yes`);
|
|
849
|
+
if (clone.importsBlock) configLines.push(`imports: yes`);
|
|
850
|
+
if (clone.voices) configLines.push(`voices: ${Object.keys(clone.voices).join(", ")}`);
|
|
851
|
+
if (clone.metadata) configLines.push(`metadata: ${clone.metadata}`);
|
|
852
|
+
|
|
853
|
+
const configStr = configLines.join(" ยท ");
|
|
854
|
+
const variantStr = (options?.variants ?? []).join(", ");
|
|
855
|
+
// Frontmatter is the raw YAML block from the markdown source (if any).
|
|
856
|
+
const frontmatter = (rootNode as any).rawFrontmatter ?? "";
|
|
857
|
+
const infoComponent: any = {
|
|
858
|
+
type: "component",
|
|
859
|
+
jsx: `<StoryboardInfo config={config} variants={variants} frontmatter={frontmatter} />`,
|
|
860
|
+
config: configStr,
|
|
861
|
+
variants: variantStr,
|
|
862
|
+
frontmatter,
|
|
863
|
+
duration: 3,
|
|
864
|
+
start: 0,
|
|
865
|
+
end: 3,
|
|
866
|
+
visible: true,
|
|
867
|
+
};
|
|
868
|
+
const infoScene: any = {
|
|
869
|
+
type: "scene",
|
|
870
|
+
id: "storyboard-info",
|
|
871
|
+
name: "Overview",
|
|
872
|
+
title: "Storyboard Info",
|
|
873
|
+
instruction: "Project metadata and variant overview",
|
|
874
|
+
duration: 3,
|
|
875
|
+
children: [infoComponent],
|
|
876
|
+
};
|
|
877
|
+
clone.children = [infoScene, ...clone.children];
|
|
878
|
+
count++;
|
|
879
|
+
|
|
880
|
+
if (count > 0) {
|
|
881
|
+
console.log(` ๐ฌ Storyboard: ${count} placeholder${count > 1 ? "s" : ""} (TTI/TTV/TTS preview)`);
|
|
882
|
+
}
|
|
883
|
+
return clone;
|
|
884
|
+
}
|
|
885
|
+
|
|
742
886
|
// โโ Combined Pipeline โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
743
887
|
|
|
744
888
|
export interface ResolveAllOptions extends ResolveMediaOptions {
|
|
@@ -770,6 +914,9 @@ export interface ResolveAllOptions extends ResolveMediaOptions {
|
|
|
770
914
|
* a deterministic seed is auto-generated and written back to the source file,
|
|
771
915
|
* making it stable across restarts for cache consistency. */
|
|
772
916
|
sourcePath?: string;
|
|
917
|
+
/** Storyboard mode: skip TTI/TTV generation. The compiler will convert
|
|
918
|
+
* prompt image/video nodes to component placeholders instead. */
|
|
919
|
+
storyboard?: boolean;
|
|
773
920
|
}
|
|
774
921
|
|
|
775
922
|
/**
|
|
@@ -944,10 +1091,10 @@ export async function resolveAll(
|
|
|
944
1091
|
try {
|
|
945
1092
|
const raw = readFileSync(options.sourcePath, "utf-8");
|
|
946
1093
|
if (options.sourcePath.endsWith(".md")) {
|
|
947
|
-
// Insert seed: <N> after the
|
|
1094
|
+
// Insert seed: <N> right after the `# video` line, before any config.
|
|
948
1095
|
const lines = raw.split("\n");
|
|
949
|
-
const
|
|
950
|
-
const insertAt =
|
|
1096
|
+
const videoIdx = lines.findIndex((l) => l.trim().startsWith("# video"));
|
|
1097
|
+
const insertAt = videoIdx >= 0 ? videoIdx + 1 : 1;
|
|
951
1098
|
lines.splice(insertAt, 0, `seed:${autoSeed}`);
|
|
952
1099
|
writeFileSync(options.sourcePath, lines.join("\n"), "utf-8");
|
|
953
1100
|
} else if (options.sourcePath.endsWith(".json")) {
|
|
@@ -978,11 +1125,19 @@ export async function resolveAll(
|
|
|
978
1125
|
variant: options.variants?.[0] ?? "video",
|
|
979
1126
|
});
|
|
980
1127
|
|
|
981
|
-
// Step 2:
|
|
1128
|
+
// Step 2: Storyboard mode โ replace time-consuming nodes with built-in
|
|
1129
|
+
// component placeholders for fast structural preview. Converts image/video
|
|
1130
|
+
// prompts and audio scripts into StoryboardSlot / StoryboardCaption cards.
|
|
1131
|
+
// Also prepends a StoryboardInfo overlay with root metadata.
|
|
1132
|
+
if (options.storyboard) {
|
|
1133
|
+
result = applyStoryboardOverrides(result, { variants: options.variants });
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Step 3: Best-match media src resolution โ for pattern-based src values
|
|
982
1137
|
// (e.g. photo_${width}x${height}.jpg), find the closest existing file.
|
|
983
1138
|
result = await resolveMediaSrcs(result, { baseDir: options.baseDir });
|
|
984
1139
|
|
|
985
|
-
// Step
|
|
1140
|
+
// Step 4: Generate images/videos from prompts before probing durations.
|
|
986
1141
|
if (options.mediaOutputDir) {
|
|
987
1142
|
result = await resolveGeneratedMedia(result, {
|
|
988
1143
|
outputDir: options.mediaOutputDir,
|
package/src/entry.tsx
CHANGED
|
@@ -5,6 +5,9 @@ import { ComposeContext, EventProvider, type ComposeContextValue } from "./conte
|
|
|
5
5
|
|
|
6
6
|
import { FolderLeaf } from "./types/Folder";
|
|
7
7
|
import { SubtitleOverlay } from "./types/Subtitle";
|
|
8
|
+
import { StoryboardSlot } from "./components/StoryboardSlot";
|
|
9
|
+
import { StoryboardCaption } from "./components/StoryboardCaption";
|
|
10
|
+
import { StoryboardInfo } from "./components/StoryboardInfo";
|
|
8
11
|
import { getDurationInSeconds } from "./utils/index";
|
|
9
12
|
import { root as rootSchema, type Root } from "./schema/index";
|
|
10
13
|
import {
|
|
@@ -33,6 +36,20 @@ const DefaultContainer: ComposeContextValue["Container"] = ({ children, style, c
|
|
|
33
36
|
</div>
|
|
34
37
|
);
|
|
35
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Built-in components available to every video without registration.
|
|
41
|
+
* Currently includes the storyboard placeholder; add other universal
|
|
42
|
+
* components here as needed.
|
|
43
|
+
*
|
|
44
|
+
* User-provided components (via compose.components or root.imports) take
|
|
45
|
+
* precedence โ they can override any builtin by using the same name.
|
|
46
|
+
*/
|
|
47
|
+
const BUILTIN_COMPONENTS: Record<string, React.ComponentType<any>> = {
|
|
48
|
+
StoryboardSlot,
|
|
49
|
+
StoryboardCaption,
|
|
50
|
+
StoryboardInfo,
|
|
51
|
+
};
|
|
52
|
+
|
|
36
53
|
/**
|
|
37
54
|
* Load the component registry from root.imports.
|
|
38
55
|
* - If string: import it as an ESM module (pre-bundled by server)
|
|
@@ -115,11 +132,17 @@ export function MarkCut({ root, compose, background = "#000" }: MarkCutProps) {
|
|
|
115
132
|
React.useMemo(() => getDurationInSeconds(parsed as any, true), [parsed]);
|
|
116
133
|
|
|
117
134
|
const value = React.useMemo<ComposeContextValue>(
|
|
118
|
-
() =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
() => {
|
|
136
|
+
const userComponents = registry ?? compose?.components;
|
|
137
|
+
const components = userComponents
|
|
138
|
+
? { ...BUILTIN_COMPONENTS, ...userComponents }
|
|
139
|
+
: BUILTIN_COMPONENTS;
|
|
140
|
+
return {
|
|
141
|
+
Container: compose?.Container ?? DefaultContainer,
|
|
142
|
+
onError: compose?.onError,
|
|
143
|
+
components,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
123
146
|
[compose, registry],
|
|
124
147
|
);
|
|
125
148
|
|