Explorar o código

优化提示词

lkatzey hai 1 mes
pai
achega
a3f5f384d2

+ 56 - 4
packages/core/src/stages/parse.ts

@@ -30,8 +30,14 @@ function formatTodayChineseDate(): string {
  * 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.
+ *
+ * When `template === "github-trending"`, also strips leading greetings from
+ * every scene's narration — the cover scene already opens with "大家好,今天
+ * 是..." so any per-scene greeting would duplicate it. LLM adherence to the
+ * prompt rule is unreliable, so this is the source of truth.
  */
-function applyLengthLimits(input: unknown): unknown {  if (!input || typeof input !== "object") return input;
+function applyLengthLimits(input: unknown, template: string): unknown {
+  if (!input || typeof input !== "object") return input;
   const root = (input as any).scenes && Array.isArray((input as any).scenes)
     ? { ...(input as any) }
     : input;
@@ -40,12 +46,15 @@ function applyLengthLimits(input: unknown): unknown {  if (!input || typeof inpu
   (root as any).scenes = (root as any).scenes.map((scene: any) => {
     if (!scene || typeof scene !== "object") return scene;
     const next: any = { ...scene };
-    next.narration = clipToLength(scene.narration, 200);
+    const narration = template === "github-trending"
+      ? stripLeadingGreeting(scene.narration)
+      : scene.narration;
+    next.narration = clipToLength(narration, 200);
     if (scene.github && typeof scene.github === "object") {
       next.github = {
         ...scene.github,
         highlights: clipToLength(scene.github.highlights, 30),
-        intro: clipToLength(scene.github.intro, 300),
+        intro: clipToLength(scene.github.intro, 200),
         review: clipToLength(scene.github.review, 30),
       };
     }
@@ -54,6 +63,49 @@ function applyLengthLimits(input: unknown): unknown {  if (!input || typeof inpu
   return root;
 }
 
+/** Common Chinese greeting prefixes that open a narration. Used to dedup
+ *  greetings when the cover scene already provides one. */
+const GREETING_PATTERNS = [
+  /^[大各]位(?:好|大大|朋友们)?[,,!!\s]+/,
+  /^大家(?:好|朋友们)?[,,!!\s]+/,
+  /^哈喽[,,!!\s]+/,
+  /^嗨[,,!!\s]+/,
+  /^早(?:上)?好[,,!!\s]+/,
+  /^下(?:午)?好[,,!!\s]+/,
+  /^晚(?:上)?好[,,!!\s]+/,
+];
+
+/** Show-opener clauses that reference the day / show / format instead of the
+ *  project. e.g. "今天是GitHub热榜速览,..." or "今天为大家带来..." — these
+ *  duplicate the cover scene's framing and must be stripped from content
+ *  narrations. Each pattern removes one comma-delimited leading clause. */
+const META_OPENER_PATTERNS = [
+  // 今天 / 本期 / 本周 / 本次 + a show-context noun + clause boundary
+  /^(?:今天|本(?:周|期|次))[^,,。!!.]*?(?:热榜|速览|榜单|节目|频道|播报|速递|盘点|精选|专栏|特辑)[^,,。!!.]*?[,,。!!.]\s*/,
+  // "今天[为给]大家[带介推带聊]..." — host-style lead-in to a list of items
+  /^今天[为给][^,,。!!.]*?[,,。!!.]\s*/,
+];
+
+function stripLeadingGreeting(s: string | undefined): string | undefined {
+  if (typeof s !== "string" || s.length === 0) return s;
+  let out = s;
+  // Strip simple greetings first (may reveal a meta-opener that follows).
+  for (const re of GREETING_PATTERNS) {
+    out = out.replace(re, "");
+  }
+  // Then strip show-opener clauses. Run a few passes so back-to-back
+  // meta clauses ("今天为大家带来几个项目,首先要聊的是 React") collapse fully.
+  for (let i = 0; i < 3; i++) {
+    let next = out;
+    for (const re of META_OPENER_PATTERNS) {
+      next = next.replace(re, "");
+    }
+    if (next === out) break;
+    out = next;
+  }
+  return out;
+}
+
 /** Strip emoji/decorative glyphs from every visible-text field of a VideoInput. */
 function sanitizeVideoInput(input: VideoInput): VideoInput {
   const cleanKeyframes = (kfs: typeof input.scenes[number]["keyframes"]) =>
@@ -135,7 +187,7 @@ export async function parseText(
     // LLMs routinely overshoot the documented character budgets (narration
     // ≤200, github highlights/intro/review ≤30/60/30). Clip in place before
     // schema validation so the pipeline tolerates drift instead of hard-failing.
-    aiParsed = applyLengthLimits(aiParsed);
+    aiParsed = applyLengthLimits(aiParsed, template);
 
     const validationResult = VideoInputSchema.safeParse(aiParsed);
     if (!validationResult.success) {

+ 6 - 1
packages/shared/src/llm/prompts/parse-text.ts

@@ -106,12 +106,17 @@ Order matters: social preview first, star history second.
 
 github.highlights (项目亮点): one short phrase, ≤ 30 Chinese characters, naming the single most distinctive technical capability or signal. Example: "声明式 UI + 虚拟 DOM,生态最大". No filler ("这是一个强大的..."), no marketing adjectives, no language mention unless language IS the value proposition.
 
-github.intro (项目介绍): a DETAILED, multi-sentence project description, up to 300 Chinese characters. Aim for the upper end of the budget — the goal is comprehensive coverage, not brevity. Cover: what the project actually does (concrete capabilities, not category labels), the problem it solves and for whom, 1-2 distinguishing mechanisms or technical choices (e.g. "用 Rust 实现", "采用 CRDT 算法", "基于 MCP 协议"), notable ecosystem signals (contributors, recent activity, adoption) if space allows, and the typical workflow or integration pattern. Pull specifics from the README — names of features, numbers, comparisons — rather than restating the headline. Stay factual; do NOT include a language bullet unless language IS the value proposition.
+github.intro (项目介绍): a DETAILED, multi-sentence project description, up to 200 Chinese characters. Aim for the upper end of the budget — the goal is comprehensive coverage, not brevity. Cover: what the project actually does (concrete capabilities, not category labels), the problem it solves and for whom, 1-2 distinguishing mechanisms or technical choices (e.g. "用 Rust 实现", "采用 CRDT 算法", "基于 MCP 协议"), notable ecosystem signals (contributors, recent activity, adoption) if space allows, and the typical workflow or integration pattern. Pull specifics from the README — names of features, numbers, comparisons — rather than restating the headline. Stay factual; do NOT include a language bullet unless language IS the value proposition.
 
 github.review (点评推荐): one short recommendation, ≤ 30 Chinese characters, naming the target user / scenario or a one-line verdict. Example: "适合追求极致启动速度的 CLI 与服务端场景". Stay neutral — avoid superlatives like "最强" or "神器".
 
 narration (口播): ≤ 200 characters. Cover what the project does (deeper than intro), why it is notable today (timing / trend / adoption), and one distinctive characteristic a developer cares about. Pack specifics — numbers, comparisons, concrete use cases. Do NOT include a language bullet unless language is core to the value; do NOT read out the GitHub URL.
 
+GREETING RULE (overrides the base prompt's first-scene greeting rule): the cover scene for this template already opens with "大家好,今天是...". Therefore EVERY content scene's narration — including the first repo scene — MUST NOT begin with any of the following:
+- A greeting: "大家好", "各位", "哈喽", "嗨", time-of-day greetings.
+- A show-opener that references the day, show, or format: "今天是GitHub热榜速览", "今天为大家带来...", "今天给大家介绍...", "本期节目...", "本周热榜...", "欢迎收看...".
+- Any meta lead-in. The narration must start DIRECTLY with project content (e.g. "首先要聊的是 React...", "接下来这个项目...", "React 是 Facebook 出品的声明式 UI 库...").
+
 CHARACTER LIMITS ARE STRICT — exceeding them breaks the layout. If content does not fit, drop the least important detail rather than compressing into a longer sentence.
 
 Number formatting: stars/forks/etc. always render with lowercase k suffix when ≥ 1000 (e.g. 1234 → "1.2k", 12345 → "12.3k", 123456 → "123k"). Values under 1000 stay as plain integers. The collector already formats them in markdown text, but when you write counts into github.highlights / intro / review / narration yourself, apply the same rule.

+ 1 - 1
packages/shared/src/types/scene.ts

@@ -49,7 +49,7 @@ export type RepoMeta = z.infer<typeof RepoMetaSchema>;
 export const GithubSceneDataSchema = z.object({
   repo: RepoMetaSchema,
   highlights: z.string().max(30),
-  intro: z.string().max(300),
+  intro: z.string().max(200),
   review: z.string().max(30),
 });
 

+ 17 - 17
packages/templates/src/github-trending/index.tsx

@@ -49,7 +49,7 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
 
   // -- Layout geometry (px) --
   const pad = isPortrait ? 60 : 80;
-  const headerHeight = isPortrait ? 220 : 180;
+  const headerHeight = isPortrait ? 260 : 220;
   const dividerY = pad + headerHeight;
   const footerReserved = isPortrait ? 220 : 140;
   const contentTop = dividerY + 24;
@@ -123,7 +123,7 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
         right: pad,
       }}>
         <div style={{
-          fontSize: isPortrait ? 56 : 60,
+          fontSize: isPortrait ? 72 : 80,
           fontWeight: 800,
           color: "white",
           fontFamily: "Noto Sans SC",
@@ -132,7 +132,7 @@ const GithubTrendingScene: React.FC<GithubTrendingSceneProps> = ({
         }}>
           {fullName}
         </div>
-        <div style={{ display: "flex", flexWrap: "wrap", gap: 12, marginTop: 18 }}>
+        <div style={{ display: "flex", flexWrap: "wrap", gap: 14, marginTop: 22 }}>
           {language && <Tag accent={palette.accent}><ColorDot color={languageColor} />{language}</Tag>}
           {starsLabel && <Tag accent={palette.accent}>{starsLabel}</Tag>}
           {license && <Tag accent={palette.accent}>{license}</Tag>}
@@ -191,14 +191,14 @@ const Tag: React.FC<{ accent: string; children: React.ReactNode }> = ({ accent,
   <div style={{
     display: "inline-flex",
     alignItems: "center",
-    gap: 8,
-    padding: "8px 18px",
-    borderRadius: 8,
+    gap: 10,
+    padding: "12px 24px",
+    borderRadius: 10,
     background: "rgba(255,255,255,0.08)",
     border: `1px solid ${accent}55`,
     color: "white",
     fontFamily: "Noto Sans SC",
-    fontSize: 22,
+    fontSize: 28,
     fontWeight: 500,
     backdropFilter: "blur(4px)",
   }}>
@@ -209,8 +209,8 @@ const Tag: React.FC<{ accent: string; children: React.ReactNode }> = ({ accent,
 const ColorDot: React.FC<{ color: string }> = ({ color }) => (
   <span style={{
     display: "inline-block",
-    width: 10,
-    height: 10,
+    width: 14,
+    height: 14,
     borderRadius: "50%",
     background: color || "#ccc",
   }} />
@@ -245,25 +245,25 @@ const Section: React.FC<{
     background: "rgba(255,255,255,0.05)",
     border: "1px solid rgba(255,255,255,0.10)",
     borderRadius: 12,
-    padding: "20px 24px",
+    padding: "22px 28px",
     display: "flex",
     flexDirection: "column",
-    gap: 10,
+    gap: 12,
   }}>
     <div style={{
       display: "flex",
       alignItems: "center",
-      gap: 10,
+      gap: 12,
     }}>
       <span style={{
         display: "inline-block",
-        width: 4,
-        height: 20,
+        width: 5,
+        height: 26,
         background: accent,
         borderRadius: 2,
       }} />
       <span style={{
-        fontSize: 22,
+        fontSize: 28,
         fontWeight: 700,
         color: accent,
         fontFamily: "Noto Sans SC",
@@ -273,8 +273,8 @@ const Section: React.FC<{
       </span>
     </div>
     <div style={{
-      fontSize: 26,
-      lineHeight: 1.5,
+      fontSize: 32,
+      lineHeight: 1.4,
       color: "white",
       fontFamily: "Noto Sans SC",
       fontWeight: 400,