119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
|
|
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(),
|
|||
|
|
};
|
|||
|
|
}
|