Sfoglia il codice sorgente

根据首个场景自动生成封面

lkatzey 1 mese fa
parent
commit
8b7566a5c6
2 ha cambiato i file con 48 aggiunte e 22 eliminazioni
  1. 18 11
      packages/core/src/stages/parse.ts
  2. 30 11
      packages/templates/src/Root.tsx

+ 18 - 11
packages/core/src/stages/parse.ts

@@ -109,23 +109,30 @@ export async function parseText(
   const scenes: Scene[] = [];
   let sceneIndex = 0;
 
-  // 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.
+  // Cover scene — always present. When the user/LLM provided explicit cover
+  // content, use it. Otherwise derive the cover from the first content scene
+  // (its keyframes as a preview list, its first image as the background) so
+  // the cover reflects what the video is actually about. Default 1s so it
+  // flashes briefly without pushing back the real content.
   const coverInput = videoInput.cover;
+  const firstScene = videoInput.scenes[0];
+  const coverKeyframes =
+    coverInput?.keyframes ?? (firstScene?.keyframes ?? []).slice(0, 3);
+  const coverImages = coverInput
+    ? [
+        ...(coverInput.imagePath ? [{ path: coverInput.imagePath }] : []),
+        ...(coverInput.imageUrl ? [{ url: coverInput.imageUrl }] : []),
+        ...(coverInput.imageQuery ? [{ query: coverInput.imageQuery }] : []),
+      ]
+    : (firstScene?.images ?? []).slice(0, 1);
   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 }] : []),
-    ],
+    keyframes: coverKeyframes,
+    images: coverImages,
     backgroundQuery: coverInput?.imageQuery,
     duration: 1,
   });
@@ -161,7 +168,7 @@ export async function parseText(
 
   const result: ParsedContent = {
     title: videoInput.title,
-    subtitle: videoInput.subtitle,
+    subtitle: videoInput.subtitle ?? videoInput.scenes[0]?.title,
     summary: videoInput.summary || "",
     cover: videoInput.cover,
     scenes,

+ 30 - 11
packages/templates/src/Root.tsx

@@ -88,6 +88,7 @@ const TemplateComposition: React.FC<RemotionProps> = (props) => {
             >
               {sceneAudio}
               <CoverScene
+                template={props.template}
                 title={props.title}
                 subtitle={props.subtitle}
                 keyframes={scene.keyframes}
@@ -139,20 +140,30 @@ const TemplateComposition: React.FC<RemotionProps> = (props) => {
 // --- Cover Scene ---
 
 const CoverScene: React.FC<{
+  template: TemplateType;
   title: string;
   subtitle?: string;
   keyframes: Array<{ type: string; content: string }>;
   backgroundAsset?: { id: string; filename: string };
   totalFrames: number;
-}> = ({ title, subtitle, keyframes, backgroundAsset }) => {
+}> = ({ template, title, subtitle, keyframes, backgroundAsset }) => {
+  const accent = THEMES[template].primaryLight;
   return (
     <AbsoluteFill style={{ backgroundColor: "#0f172a" }}>
       {backgroundAsset && (
         <Img
           src={staticFile(backgroundAsset.filename)}
-          style={{ position: "absolute", width: "100%", height: "100%", objectFit: "cover", opacity: 0.3 }}
+          style={{ position: "absolute", width: "100%", height: "100%", objectFit: "cover" }}
         />
       )}
+      <div
+        style={{
+          position: "absolute",
+          inset: 0,
+          background:
+            "linear-gradient(to bottom, rgba(0,0,0,0.25), rgba(0,0,0,0.55))",
+        }}
+      />
       <div style={{
         position: "absolute", inset: 0, display: "flex",
         flexDirection: "column", justifyContent: "center", alignItems: "center",
@@ -162,27 +173,35 @@ const CoverScene: React.FC<{
           fontSize: 72, fontWeight: 800, color: "white",
           fontFamily: "Noto Sans SC", lineHeight: 1.2, marginBottom: 20,
           maxWidth: "80%",
+          textShadow: "0 2px 16px rgba(0,0,0,0.85), 0 0 4px rgba(0,0,0,0.85)",
         }}>
           {title}
         </div>
         {subtitle && (
           <div style={{
-            fontSize: 28, color: "rgba(255,255,255,0.7)",
+            fontSize: 28, color: "white",
             fontFamily: "Noto Sans SC", marginBottom: 40,
+            textShadow: "0 2px 12px rgba(0,0,0,0.85)",
           }}>
             {subtitle}
           </div>
         )}
         {keyframes.length > 0 && (
           <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
-            {keyframes.map((kf, i) => (
-              <div key={i} style={{
-                fontSize: 22, color: "rgba(255,255,255,0.5)",
-                fontFamily: "Noto Sans SC",
-              }}>
-                {kf.content}
-              </div>
-            ))}
+            {keyframes.map((kf, i) => {
+              const isHighlight = kf.type === "highlight";
+              return (
+                <div key={i} style={{
+                  fontSize: isHighlight ? 30 : 24,
+                  fontWeight: isHighlight ? 700 : 500,
+                  color: isHighlight ? accent : "white",
+                  fontFamily: "Noto Sans SC",
+                  textShadow: "0 1px 8px rgba(0,0,0,0.85)",
+                }}>
+                  {kf.content}
+                </div>
+              );
+            })}
           </div>
         )}
       </div>