@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.
- package/AGENTS.md +15 -16
- package/package.json +1 -1
- 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 +157 -2
- 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 +9 -1
- package/src/schema/index.ts +1 -1
- package/src/types/Folder.tsx +1 -1
- package/src/types/Subtitle.tsx +4 -0
- 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/tmp/frontmatter-test.ts +21 -0
|
@@ -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:
|
|
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
|
|