ソースを参照

github-trending模板:图片增加圆角展示,封面优化

lkatzey 1 ヶ月 前
コミット
1ecb522ade

+ 27 - 12
packages/core/src/stages/parse.ts

@@ -19,14 +19,19 @@ export interface ParseStageConfig {
   source?: string;
 }
 
+/** Today's date formatted as e.g. "2026年6月19日" (zh-CN, no leading zeros). */
+function formatTodayChineseDate(): string {
+  const d = new Date();
+  return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`;
+}
+
 /**
  * Clip LLM-generated text fields to their schema-enforced maximums before
  * validation. Clips at a sentence/clause boundary when possible (see
  * clipToLength) so the result still reads naturally. Mutates a copy and
  * returns it; the input is left untouched.
  */
-function applyLengthLimits(input: unknown): unknown {
-  if (!input || typeof input !== "object") return input;
+function applyLengthLimits(input: unknown): unknown {  if (!input || typeof input !== "object") return input;
   const root = (input as any).scenes && Array.isArray((input as any).scenes)
     ? { ...(input as any) }
     : input;
@@ -145,6 +150,17 @@ export async function parseText(
   const scenes: Scene[] = [];
   let sceneIndex = 0;
 
+  // For github-trending the cover is a fixed masthead — title is the channel
+  // name, subtitle is today's date in zh-CN. Override whatever the LLM produced
+  // so the cover stays deterministic regardless of input.
+  if (template === "github-trending") {
+    videoInput = {
+      ...videoInput,
+      title: "GitHub 每日热榜",
+      subtitle: formatTodayChineseDate(),
+    };
+  }
+
   // 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
@@ -152,16 +168,15 @@ export async function parseText(
   // 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);
-  // For the auto-derived cover, only inherit a "real" image (path / url / query)
-  // from the first content scene — and only for templates that actually have
-  // usable hero imagery. For github-trending the first scene's images are
-  // repo-social-preview / star-history chart refs that are meaningless as a
-  // full-screen cover background, so we skip inheritance entirely and let the
-  // cover fall back to the template's default background.
-  const inheritCoverImage = template !== "github-trending";
-  const firstSceneCoverImage = inheritCoverImage
+  // For github-trending the cover is a clean title screen — it should not
+  // preview or hint at later repo scenes (no inherited keyframes, no inherited
+  // image). Other templates keep the original "inherit from first scene"
+  // behaviour so the cover reflects the video's actual topic.
+  const inheritFromFirstScene = template !== "github-trending";
+  const coverKeyframes = inheritFromFirstScene
+    ? (coverInput?.keyframes ?? (firstScene?.keyframes ?? []).slice(0, 3))
+    : (coverInput?.keyframes ?? []);
+  const firstSceneCoverImage = inheritFromFirstScene
     ? (firstScene?.images ?? []).find((img) => img.path || img.url || img.query)
     : undefined;
   const coverImages = coverInput

+ 30 - 16
packages/templates/src/github-trending/index.tsx

@@ -82,7 +82,16 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
   }
 
   const colGap = isPortrait ? 0 : 40;
-  const leftColWidth = isPortrait ? 0 : Math.round(contentWidth * 0.4);
+  // Landscape left column: cap width so both images stacked (each at its
+  // natural aspect ratio) fit within contentHeight. Assumes typical aspects
+  // (social preview 2:1, star history ~1.5:1 → combined height factor 7/6).
+  // Falls back to 40% of contentWidth when that is smaller.
+  const leftColWidth = isPortrait
+    ? 0
+    : Math.min(
+        Math.round(contentWidth * 0.4),
+        Math.round((contentHeight - 20) / (7 / 6)),
+      );
   const rightColWidth = isPortrait ? contentWidth : contentWidth - leftColWidth - colGap;
 
   return (
@@ -207,18 +216,19 @@ const ColorDot: React.FC<{ color: string }> = ({ color }) => (
   }} />
 );
 
-const ImageFrame: React.FC<{ children: React.ReactNode; minHeight?: number; flex?: number }> = ({ children, minHeight = 0, flex }) => (
+const ImageFrame: React.FC<{ children: React.ReactNode; flex?: string | number }> = ({ children, flex }) => (
   <div style={{
-    flex: flex ?? 1,
-    minHeight,
-    background: "rgba(255,255,255,0.04)",
+    flex,
+    width: flex ? undefined : "100%",
+    height: "auto",
+    background: "#0b1220",
     border: "1px solid rgba(255,255,255,0.10)",
-    borderRadius: 12,
+    borderRadius: 16,
     overflow: "hidden",
     display: "flex",
     alignItems: "center",
     justifyContent: "center",
-    padding: 12,
+    boxShadow: "0 12px 32px rgba(0,0,0,0.45), 0 2px 8px rgba(0,0,0,0.35)",
   }}>
     {children}
   </div>
@@ -304,7 +314,9 @@ const LandscapeContent: React.FC<ContentProps> = ({
       display: "flex",
       gap: colGap,
     }}>
-      {/* Left column: images stacked (hidden entirely when neither is present) */}
+      {/* Left column: images stacked with natural heights (same width,
+          heights determined by each image's aspect ratio). Hidden entirely
+          when neither is present. */}
       {hasAnyImage && (
         <div style={{
           width: leftColWidth,
@@ -312,15 +324,16 @@ const LandscapeContent: React.FC<ContentProps> = ({
           display: "flex",
           flexDirection: "column",
           gap: 20,
+          overflow: "hidden",
         }}>
           {socialPreview && (
-            <ImageFrame flex={1}>
-              <Img src={staticFile(socialPreview.filename)} style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain" }} />
+            <ImageFrame>
+              <Img src={staticFile(socialPreview.filename)} style={{ display: "block", width: "100%", height: "auto" }} />
             </ImageFrame>
           )}
           {starHistory && (
-            <ImageFrame flex={1}>
-              <Img src={staticFile(starHistory.filename)} style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain" }} />
+            <ImageFrame>
+              <Img src={staticFile(starHistory.filename)} style={{ display: "block", width: "100%", height: "auto" }} />
             </ImageFrame>
           )}
         </div>
@@ -358,21 +371,22 @@ const PortraitContent: React.FC<ContentProps> = ({
       flexDirection: "column",
       gap: 20,
     }}>
-      {/* Top row: images side-by-side (or single full-width; hidden when neither) */}
+      {/* Top row: images side-by-side, each flex:1 (same width) with natural
+          height. Row sizes to tallest child; shorter child top-aligned. */}
       {hasAnyImage && (
         <div style={{
-          height: Math.round(contentHeight * 0.42),
           display: "flex",
           gap: 20,
+          alignItems: "flex-start",
         }}>
           {socialPreview && (
             <ImageFrame flex={1}>
-              <Img src={staticFile(socialPreview.filename)} style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain" }} />
+              <Img src={staticFile(socialPreview.filename)} style={{ display: "block", width: "100%", height: "auto" }} />
             </ImageFrame>
           )}
           {starHistory && (
             <ImageFrame flex={1}>
-              <Img src={staticFile(starHistory.filename)} style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain" }} />
+              <Img src={staticFile(starHistory.filename)} style={{ display: "block", width: "100%", height: "auto" }} />
             </ImageFrame>
           )}
         </div>