lkatzey il y a 1 mois
Parent
commit
84af2a0096

+ 19 - 4
packages/core/src/stages/assets.ts

@@ -103,10 +103,25 @@ export async function resolveAssets(
   const fallbackBg = join(assetsRoot, "backgrounds", "default.png");
   const bgPath = templateBg && existsSync(templateBg) ? templateBg : fallbackBg;
 
-  const backgrounds = parsed.scenes.map((_, i) => ({
-    id: `bg-${i}`,
-    localPath: bgPath,
-  }));
+  const backgrounds = parsed.scenes.map((scene, i) => {
+    // For the cover scene, prefer the user-provided cover image as the
+    // background when one was successfully resolved. Without this, the cover
+    // would always fall back to the template default background and silently
+    // ignore cover.imagePath / imageUrl / imageQuery.
+    if (scene.sceneType === "cover" && scene.images && scene.images.length > 0) {
+      const firstImg = scene.images[0];
+      const resolved = images.find(
+        (a) =>
+          (firstImg.path && a.path === firstImg.path) ||
+          (firstImg.url && a.url === firstImg.url) ||
+          (firstImg.query && a.query === firstImg.query)
+      );
+      if (resolved) {
+        return { id: `bg-${i}`, localPath: resolved.localPath };
+      }
+    }
+    return { id: `bg-${i}`, localPath: bgPath };
+  });
 
   return {
     images,

+ 20 - 23
packages/core/src/stages/parse.ts

@@ -109,29 +109,26 @@ export async function parseText(
   const scenes: Scene[] = [];
   let sceneIndex = 0;
 
-  // Cover scene (optional)
-  if (videoInput.cover) {
-    scenes.push({
-      id: "cover",
-      index: sceneIndex++,
-      sceneType: "cover",
-      narration: "",
-      title: videoInput.title,
-      keyframes: videoInput.cover.keyframes ?? [],
-      images: [
-        ...(videoInput.cover.imagePath
-          ? [{ path: videoInput.cover.imagePath }]
-          : []),
-        ...(videoInput.cover.imageUrl
-          ? [{ url: videoInput.cover.imageUrl }]
-          : []),
-        ...(videoInput.cover.imageQuery
-          ? [{ query: videoInput.cover.imageQuery }]
-          : []),
-      ],
-      backgroundQuery: videoInput.cover.imageQuery,
-    });
-  }
+  // Cover scene — always present. Use cover.keyframes/images if the user/LLM
+  // provided them, otherwise fall back to an empty cover driven by the
+  // top-level title/subtitle. Default 1s so it flashes briefly without
+  // pushing back the real content.
+  const coverInput = videoInput.cover;
+  scenes.push({
+    id: "cover",
+    index: sceneIndex++,
+    sceneType: "cover",
+    narration: "",
+    title: videoInput.title,
+    keyframes: coverInput?.keyframes ?? [],
+    images: [
+      ...(coverInput?.imagePath ? [{ path: coverInput.imagePath }] : []),
+      ...(coverInput?.imageUrl ? [{ url: coverInput.imageUrl }] : []),
+      ...(coverInput?.imageQuery ? [{ query: coverInput.imageQuery }] : []),
+    ],
+    backgroundQuery: coverInput?.imageQuery,
+    duration: 1,
+  });
 
   // Content scenes
   for (const s of videoInput.scenes) {

+ 1 - 4
packages/templates/src/Root.tsx

@@ -145,11 +145,8 @@ const CoverScene: React.FC<{
   backgroundAsset?: { id: string; filename: string };
   totalFrames: number;
 }> = ({ title, subtitle, keyframes, backgroundAsset }) => {
-  const frame = useCurrentFrame();
-  const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
-
   return (
-    <AbsoluteFill style={{ opacity, backgroundColor: "#0f172a" }}>
+    <AbsoluteFill style={{ backgroundColor: "#0f172a" }}>
       {backgroundAsset && (
         <Img
           src={staticFile(backgroundAsset.filename)}