Forráskód Böngészése

feat(templates): github-trending 封面改版——背景图+居中标题卡,去仓库列表

- 封面改为渲染器提供的背景图(default.png 兜底)+ 深色渐变蒙版,
  居中深色标题卡:刊头标题 + 日期 + 主要语言标签 + trendSummary
- 语言标签由 content 场景 repo.language 聚合去重(按仓库数取 top6,
  色点用 languageColor,缺失时回退主题色)
- 移除 top6 仓库卡片网格——封面不再罗列仓库,正片再看逐仓详解
- CoverDecor 新增 onDark:背景图模式用白色/更高透明度,浅色渐变兜底不变
- CLAUDE.md 同步封面描述

Co-Authored-By: Claude <noreply@anthropic.com>
lkatzey 1 hónapja
szülő
commit
62c02c3994
2 módosított fájl, 101 hozzáadás és 143 törlés
  1. 1 1
      CLAUDE.md
  2. 100 142
      packages/templates/src/Root.tsx

+ 1 - 1
CLAUDE.md

@@ -140,7 +140,7 @@ Provider 通过 `registerProvider()` 在 `packages/tts/src/providers/` 注册,
 
 5 个模板(news、knowledge、opinion、marketing、github-trending),每个在 `packages/templates/src/<name>/index.tsx`。`Root.tsx` 路由:`scene.kind === "cover"` → 内置 `CoverScene`;`"outro"` → 内置 `OutroScene`;其他 → `SCENE_MAP[template]`。
 
-- `github-trending` cover:深色标题卡(刊头 + 日期 + trendSummary)+ top6 仓库卡片网格(由 content 场景派生,按 todayStars 排序),进度条上方 `ChapterToc`(高亮当前章节)。是唯一带 `isPortrait` 分支的模板。
+- `github-trending` cover:背景图(default.png 兜底)+深色蒙版,居中深色标题卡,仅展示刊头标题 + 日期 + 主要语言标签(由 content 场景的 repo.language 聚合去重)+ trendSummary 概括,不再展示仓库列表;进度条上方 `ChapterToc`(高亮当前章节)。是唯一带 `isPortrait` 分支的模板。
 - `SubtitleBar`(`base/components/subtitle-bar.tsx`)现在**读预计算的 `caption[]`**(按累计时长找当前字幕),不再运行期分词。可选 `fontSize`/`style`(竖屏传 `{bottom:380}`/`fontSize:56`)。
 - 模板字段:读 `scene.title`(原 displayText)、`scene.captionOrigin`、`scene.cardList`(原 keyframes,`card.kind`/`card.desc`)、`scene.caption`、`scene.extension`(模板专属,如 github-trending 的 repo/highlights)、`scene.images[].filename`。
 

+ 100 - 142
packages/templates/src/Root.tsx

@@ -1,7 +1,6 @@
 import React, { useEffect, useState } from "react";
 import { Composition, Sequence, AbsoluteFill, Audio, staticFile, Img, useCurrentFrame, useVideoConfig, interpolate, continueRender, delayRender } from "remotion";
 import type { TemplateType, AspectRatio, RenderScene, RenderProps } from "@pipeline/shared";
-import { formatCount } from "@pipeline/shared";
 import NewsScene from "./news/index";
 import KnowledgeScene from "./knowledge/index";
 import OpinionScene from "./opinion/index";
@@ -60,8 +59,8 @@ const TEMPLATES: TemplateType[] = ["news", "knowledge", "opinion", "marketing",
 const TemplateComposition: React.FC<RenderProps> = (props) => {
   const { fps } = useVideoConfig();
   const SceneComponent = SCENE_MAP[props.template];
-  // github-trending: the cover lists every repo (name / language / today's
-  // star gain / one-line description). Content scenes carry repo data in extension.
+  // github-trending: the cover aggregates the repos' languages into tag
+  // chips. Content scenes carry repo data in extension.
   const coverRepos = props.scenes.filter(
     (s) => s.kind === "content" && ghExt(s)
   );
@@ -150,17 +149,20 @@ const TemplateComposition: React.FC<RenderProps> = (props) => {
 
 // Faint, sparse code/data motifs on the github-trending cover background.
 // Low-opacity and edge-anchored so the cover stays clean — the motifs read
-// only as subtle texture, never competing with the cards.
-const CoverDecor: React.FC<{ accent: string }> = ({ accent }) => {
+// only as subtle texture, never competing with the title card. Colors adapt
+// to the backdrop: near-black on the light gradient, white on the dark
+// scrim over the background image.
+const CoverDecor: React.FC<{ accent: string; onDark?: boolean }> = ({ accent, onDark }) => {
   const tok = (css: React.CSSProperties): React.CSSProperties => ({
     position: "absolute",
     fontFamily: "monospace",
     fontWeight: 700,
-    color: "#0f172a",
-    opacity: 0.06,
+    color: onDark ? "#ffffff" : "#0f172a",
+    opacity: onDark ? 0.08 : 0.06,
     letterSpacing: "0.02em",
     ...css,
   });
+  const chartOpacity = onDark ? 0.16 : 0.08;
   return (
     <div style={{ position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }}>
       <span style={tok({ top: 44, left: 52, fontSize: 56 })}>{"</>"}</span>
@@ -168,11 +170,11 @@ const CoverDecor: React.FC<{ accent: string }> = ({ accent }) => {
       <span style={tok({ bottom: 168, left: 54, fontSize: 28 })}>{"01 10 01"}</span>
       <span style={tok({ bottom: 150, right: 58, fontSize: 26 })}>{"git push"}</span>
       {/* Faint data line chart */}
-      <svg width="180" height="84" viewBox="0 0 180 84" style={{ position: "absolute", top: 64, right: 168, opacity: 0.08 }}>
+      <svg width="180" height="84" viewBox="0 0 180 84" style={{ position: "absolute", top: 64, right: 168, opacity: chartOpacity }}>
         <polyline points="0,66 34,50 68,56 102,28 136,34 180,8" fill="none" stroke={accent} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
       </svg>
       {/* Faint data bar cluster */}
-      <div style={{ position: "absolute", bottom: 206, left: 80, display: "flex", alignItems: "flex-end", gap: 7, opacity: 0.08 }}>
+      <div style={{ position: "absolute", bottom: 206, left: 80, display: "flex", alignItems: "flex-end", gap: 7, opacity: chartOpacity }}>
         <div style={{ width: 12, height: 32, background: accent, borderRadius: 3 }} />
         <div style={{ width: 12, height: 58, background: accent, borderRadius: 3 }} />
         <div style={{ width: 12, height: 24, background: accent, borderRadius: 3 }} />
@@ -198,21 +200,46 @@ const CoverScene: React.FC<{
   const { width, height } = useVideoConfig();
   const isPortrait = height > width;
 
-  // github-trending cover: a DARK title card (contrast over a light bg with
-  // faint code/data motifs) centered near the top, then the TOP 3 repos by
-  // today's star gain as large rounded "floating" cards — horizontal row in
-  // landscape, vertical stack in portrait. Doubles as the opening narration.
+  // github-trending cover: the renderer-provided background image (default.png
+  // fallback — no template-specific bg exists yet) under a dark scrim, with a
+  // DARK title card fully centered on top — no repo list. Shows only the
+  // masthead title, the date, the day's main language tags (aggregated across
+  // today's repos), and the trend summary line. Light gradient + CoverDecor is
+  // the no-image fallback (e.g. studio demo props).
   if (isGithubTrending) {
-    const topRepos = [...(coverRepos ?? [])]
-      .sort((a, b) => (ghExt(b)!.repo.todayStars ?? 0) - (ghExt(a)!.repo.todayStars ?? 0))
-      .slice(0, 6);
     const primary = THEMES[template].primary;
+    // Aggregate languages across today's repos → the cover's "main tags".
+    // Count-weighted, top repo's languageColor wins per language.
+    const langStats = new Map<string, { count: number; color?: string }>();
+    for (const s of coverRepos ?? []) {
+      const repo = ghExt(s)!.repo;
+      if (!repo.language) continue;
+      const stats = langStats.get(repo.language) ?? { count: 0, color: repo.languageColor };
+      stats.count += 1;
+      langStats.set(repo.language, stats);
+    }
+    const topLangs = [...langStats.entries()]
+      .sort((a, b) => b[1].count - a[1].count)
+      .slice(0, 6);
 
     return (
       <AbsoluteFill style={{
         background: "linear-gradient(135deg, #f8fafc 0%, #eef2f7 55%, #e2e8f0 100%)",
       }}>
-        <CoverDecor accent={accent} />
+        {backgroundAsset ? (
+          <>
+            <Img
+              src={staticFile(backgroundAsset.filename)}
+              style={{ position: "absolute", width: "100%", height: "100%", objectFit: "cover" }}
+            />
+            <div style={{
+              position: "absolute", inset: 0,
+              background: "linear-gradient(to bottom, rgba(2,6,23,0.40), rgba(2,6,23,0.70))",
+            }} />
+          </>
+        ) : undefined}
+        {/* Code/data motifs over both backdrops (light gradient or scrimmed image) */}
+        <CoverDecor accent={accent} onDark={!!backgroundAsset} />
         {/* Top accent line */}
         <div style={{
           position: "absolute", top: 0, left: 0, width: "100%", height: 6,
@@ -221,144 +248,75 @@ const CoverScene: React.FC<{
         <div style={{
           position: "absolute", inset: 0, display: "flex", flexDirection: "column",
           alignItems: "center",
-          justifyContent: isPortrait ? "center" : "flex-start",
-          padding: isPortrait ? "72px 64px 130px" : "58px 80px 112px",
+          justifyContent: "center",
+          padding: isPortrait ? "72px 64px 150px" : "58px 80px 130px",
         }}>
-          {/* DARK title card — centered, near the top (contrast over light bg) */}
+          {/* DARK title card — centered, title + date + tags + summary */}
           <div style={{
-            display: "flex", flexDirection: "column", alignItems: "center", gap: 14,
-            padding: isPortrait ? "38px 76px" : "32px 88px",
+            display: "flex", flexDirection: "column", alignItems: "center", gap: 20,
+            padding: isPortrait ? "64px 96px" : "56px 110px",
             borderRadius: 26,
             background: "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)",
             border: `1.5px solid ${primary}55`,
-            boxShadow: "0 18px 44px rgba(15,23,42,0.22)",
-            marginBottom: isPortrait ? 44 : 38,
-            maxWidth: isPortrait ? "84%" : "74%",
+            boxShadow: "0 24px 64px rgba(0,0,0,0.55), 0 4px 16px rgba(0,0,0,0.4)",
+            maxWidth: isPortrait ? "90%" : "80%",
             textAlign: "center",
           }}>
+            {/* Masthead title */}
             <div style={{
-              fontSize: isPortrait ? 72 : 66, fontWeight: 800, color: "#ffffff",
-              fontFamily: "Noto Sans SC", lineHeight: 1.15, letterSpacing: "-0.02em",
+              fontSize: isPortrait ? 108 : 96, fontWeight: 800, color: "#ffffff",
+              fontFamily: "Noto Sans SC", lineHeight: 1.12, letterSpacing: "-0.02em",
             }}>
               {title}
             </div>
-            <div style={{
-              display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap",
-              justifyContent: "center",
-            }}>
-              {subtitle && (
-                <span style={{
-                  fontSize: isPortrait ? 30 : 26, fontWeight: 600, color: accent,
-                  fontFamily: "Noto Sans SC",
-                }}>
-                  {subtitle}
-                </span>
-              )}
-              {trendSummary && (
-                <span style={{
-                  fontSize: isPortrait ? 28 : 24, fontWeight: 500, color: "#cbd5e1",
-                  fontFamily: "Noto Sans SC",
+            {/* Date */}
+            {subtitle && (
+              <div style={{
+                fontSize: isPortrait ? 36 : 32, fontWeight: 600, color: accent,
+                fontFamily: "Noto Sans SC",
+              }}>
+                {subtitle}
+              </div>
+            )}
+            {/* Main tags — languages aggregated across today's repos */}
+            {topLangs.length > 0 && (
+              <div style={{
+                display: "flex", flexWrap: "wrap",
+                alignItems: "center", justifyContent: "center",
+                gap: 14, marginTop: 6,
+              }}>
+                {topLangs.map(([lang, stats]) => (
+                  <span key={lang} style={{
+                    display: "inline-flex", alignItems: "center", gap: 9,
+                    fontSize: isPortrait ? 28 : 24, fontWeight: 500, color: "#ffffff",
+                    fontFamily: "Noto Sans SC",
+                    padding: "10px 24px", borderRadius: 999,
+                    background: "rgba(255,255,255,0.08)",
+                    border: `1px solid ${(stats.color || accent)}66`,
+                  }}>
+                    <ColorDot color={stats.color || accent} size={isPortrait ? 14 : 12} />
+                    {lang}
+                  </span>
+                ))}
+              </div>
+            )}
+            {/* Trend summary */}
+            {trendSummary && (
+              <>
+                <div style={{
+                  width: "56%", height: 1, marginTop: 10,
+                  background: "rgba(255,255,255,0.16)",
+                }} />
+                <div style={{
+                  fontSize: isPortrait ? 34 : 29, fontWeight: 500, color: "#cbd5e1",
+                  fontFamily: "Noto Sans SC", lineHeight: 1.5,
+                  maxWidth: "92%",
                 }}>
-                  · {trendSummary}
-                </span>
-              )}
-            </div>
+                  {trendSummary}
+                </div>
+              </>
+            )}
           </div>
-
-          {/* TOP 6 repos by today's star gain (3 per row → wraps to a grid) */}
-          {topRepos.length > 0 && (
-            <div style={{
-              display: "flex", flexWrap: "wrap",
-              gap: 24,
-              width: "100%",
-              maxWidth: isPortrait ? "90%" : "94%",
-            }}>
-              {topRepos.map((s, i) => {
-                const g = ghExt(s)!;
-                const repo = g.repo;
-                const language = repo.language || "";
-                const languageColor = repo.languageColor || accent;
-                const todayLabel = repo.todayStars != null ? `+${formatCount(repo.todayStars)}` : "";
-                return (
-                  <div key={s.id} style={{
-                    flex: isPortrait ? "0 0 calc((100% - 24px) / 2)" : "0 0 calc((100% - 48px) / 3)",
-                    minWidth: 0,
-                    display: "flex", flexDirection: "column",
-                    padding: isPortrait ? "28px 30px" : "30px",
-                    borderRadius: 22,
-                    background: "#ffffff",
-                    border: `1px solid ${primary}22`,
-                    boxShadow: "0 16px 38px rgba(15,23,42,0.12), 0 2px 8px rgba(15,23,42,0.06)",
-                  }}>
-                    {/* Rank badge + today's gain (the selection signal) */}
-                    <div style={{
-                      display: "flex", alignItems: "center", justifyContent: "space-between",
-                      marginBottom: 18,
-                    }}>
-                      <div style={{
-                        display: "inline-flex", alignItems: "center", justifyContent: "center",
-                        width: isPortrait ? 56 : 52, height: isPortrait ? 56 : 52,
-                        borderRadius: "50%",
-                        background: `linear-gradient(135deg, ${accent}, ${primary})`,
-                        color: "#fff", fontWeight: 800, fontFamily: "Noto Sans SC",
-                        fontSize: isPortrait ? 30 : 26,
-                        boxShadow: "0 4px 12px rgba(34,197,94,0.35)",
-                      }}>
-                        {i + 1}
-                      </div>
-                      {todayLabel && (
-                        <span style={{
-                          fontSize: isPortrait ? 56 : 50, fontWeight: 800, color: primary,
-                          fontFamily: "Noto Sans SC", lineHeight: 1,
-                        }}>{todayLabel}</span>
-                      )}
-                    </div>
-                    {/* Repo full name */}
-                    <div style={{
-                      fontSize: isPortrait ? 40 : 34, fontWeight: 800, color: "#0f172a",
-                      fontFamily: "Noto Sans SC", lineHeight: 1.2, marginBottom: 12,
-                      wordBreak: "break-word",
-                    }}>
-                      {repo.fullName}
-                    </div>
-                    {/* Language + total stars */}
-                    <div style={{
-                      display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap",
-                      marginBottom: 14, fontFamily: "Noto Sans SC",
-                    }}>
-                      {language && (
-                        <span style={{
-                          display: "inline-flex", alignItems: "center", gap: 8,
-                          fontSize: isPortrait ? 24 : 21, fontWeight: 600, color: "#334155",
-                          padding: "5px 14px", borderRadius: 999,
-                          background: `${languageColor}1a`, border: `1px solid ${languageColor}55`,
-                        }}>
-                          <ColorDot color={languageColor} size={isPortrait ? 14 : 12} />
-                          {language}
-                        </span>
-                      )}
-                      {repo.stars != null && (
-                        <span style={{
-                          fontSize: isPortrait ? 23 : 20, color: "#64748b", fontWeight: 500,
-                        }}>
-                          {formatCount(repo.stars)} stars
-                        </span>
-                      )}
-                    </div>
-                    {/* One-line description (highlights) */}
-                    <div style={{
-                      fontSize: isPortrait ? 27 : 23, lineHeight: 1.4, color: "#475569",
-                      fontFamily: "Noto Sans SC",
-                      display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical",
-                      overflow: "hidden",
-                    }}>
-                      {g.highlights}
-                    </div>
-                  </div>
-                );
-              })}
-            </div>
-          )}
         </div>
       </AbsoluteFill>
     );