haixunMaster/lib/ai/analyze-viral.ts

119 lines
3.9 KiB
TypeScript
Raw Permalink Normal View History

2026-06-21 12:50:31 +00:00
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(),
};
}