@lalalic/markcut 2.6.0 → 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.
@@ -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
  /**
@@ -978,11 +1125,19 @@ export async function resolveAll(
978
1125
  variant: options.variants?.[0] ?? "video",
979
1126
  });
980
1127
 
981
- // Step 2: Best-match media src resolution — for pattern-based src values
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 3: Generate images/videos from prompts before probing durations.
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
- Container: compose?.Container ?? DefaultContainer,
120
- onError: compose?.onError,
121
- components: registry ?? compose?.components,
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