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(),
|
||
};
|
||
} |