haixunMaster/lib/ai/analyze-viral.ts

119 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { z } from "zod";
import type { ProviderApiKeys } from "./keys";
import { withAgentSystem } from "./agent";
import { generateStructuredObject } from "./generate-structured";
import { getModel } from "./provider";
import { describePostImages } from "./vision";
import type { ViralAnalysis } from "@/lib/types/viral";
const viralSchema = z.object({
whyViral: z.array(z.string()).min(2).max(6),
hookPattern: z.string(),
structurePattern: z.string(),
emotionalTrigger: z.string(),
timingAngle: z.string(),
commentInsights: z.object({
themes: z.array(z.string()),
sentiment: z.string(),
topReactions: z.array(z.string()),
audienceQuestions: z.array(z.string()),
whyPeopleEngage: z.string(),
}),
visualAnalysis: z.object({
hasImages: z.boolean(),
layout: z.string(),
colorMood: z.string(),
textOnImage: z.boolean(),
typographyStyle: z.string(),
visualHook: z.string(),
replicationTips: z.array(z.string()),
imageGenPrompt: z.string(),
}),
replicationStrategy: z.string(),
keyTakeaways: z.array(z.string()),
});
export interface AnalyzeViralInput {
postText: string;
authorName?: string | null;
likeCount?: number | null;
replyCount?: number | null;
searchTag?: string | null;
mediaUrls?: string[];
mediaType?: string | null;
topicLabel: string;
topicBrief?: string | null;
persona?: string | null;
aiProvider: string;
aiModel: string;
apiKeys?: ProviderApiKeys;
replies: Array<{ text: string; authorName?: string | null; likeCount?: number | null }>;
}
export async function analyzeViralPost(input: AnalyzeViralInput): Promise<ViralAnalysis> {
const model = getModel(input.aiProvider, input.aiModel, input.apiKeys ?? {});
let visualDescription: string | null = null;
if (input.mediaUrls && input.mediaUrls.length > 0) {
visualDescription = await describePostImages(
input.mediaUrls,
input.postText,
input.apiKeys ?? {}
);
}
const repliesBlock = input.replies
.slice(0, 15)
.map(
(r, i) =>
`${i + 1}. @${r.authorName ?? "匿名"}${r.likeCount ?? 0}讚):${r.text.slice(0, 200)}`
)
.join("\n");
const visualBlock = visualDescription
? `\n\nAI 圖文視覺分析:\n${visualDescription}`
: input.mediaUrls && input.mediaUrls.length > 0
? `\n\n附圖 ${input.mediaUrls.length} 張,未能進行視覺辨識,請依貼文文字推測圖文策略)`
: "\n\n純文字貼文無附圖";
const object = await generateStructuredObject({
model,
provider: input.aiProvider,
modelId: input.aiModel,
schema: viralSchema,
system: withAgentSystem(`你是 Threads 爆款內容分析師。任務是拆解「為什麼這篇會紅」,並從留言中理解受眾為什麼買單。
分析重點:
- 開頭 hook 用了什麼手法(反差、提問、數字、故事、痛點)
- 內文結構幾段、節奏、轉折、CTA
- 留言反映的共鳴點(不是摘要留言,是找「為什麼大家願意互動」)
- 若有圖文,分析圖文如何配合文字放大傳播
- imageGenPrompt 要寫成可直接給 AI 繪圖的英文 prompt描述類似風格的圖
除 imageGenPrompt 外,全部繁體中文台灣用語。`),
prompt: `主題:${input.topicLabel}
${input.topicBrief ? `Brief${input.topicBrief}` : ""}
${input.persona ? `創作者人設:${input.persona}` : ""}
搜尋標籤:${input.searchTag ?? "—"}
媒體類型:${input.mediaType ?? "unknown"}
爆款貼文:
@${input.authorName ?? "匿名"} · ${input.likeCount ?? 0} 讚 · ${input.replyCount ?? 0} 留言
${input.postText}
${visualBlock}
熱門留言(理解受眾為什麼互動):
${repliesBlock || "(無留言資料)"}
請深度分析這篇為什麼會爆款,以及如何複製其成功模式。`,
});
return {
...object,
visualAnalysis: {
...object.visualAnalysis,
hasImages: (input.mediaUrls?.length ?? 0) > 0,
},
analyzedAt: new Date().toISOString(),
};
}