|
|
@@ -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) {
|