Files
MoonTVPlus/src/lib/ai-comment-generator.ts
T

288 lines
7.9 KiB
TypeScript
Raw 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.
// AI评论生成核心逻辑
import { normalizeApiBaseUrl } from '@/lib/url';
export interface AIComment {
id: string;
userName: string;
userAvatar: string;
rating: number | null;
content: string;
time: string;
votes: number;
isAiGenerated: true;
}
interface GenerateCommentsParams {
movieName: string;
movieInfo?: string;
count?: number;
aiConfig: {
CustomApiKey: string;
CustomBaseURL: string;
CustomModel: string;
Temperature?: number;
MaxTokens?: number;
EnableWebSearch?: boolean;
WebSearchProvider?: 'tavily' | 'serper' | 'serpapi';
TavilyApiKey?: string;
SerperApiKey?: string;
SerpApiKey?: string;
};
}
interface CommentData {
content: string;
rating: number | null;
sentiment: 'positive' | 'neutral' | 'negative';
}
// 生成评论的Prompt
function buildCommentPrompt(
movieName: string,
movieInfo?: string,
searchResults?: string,
count: number = 10
): string {
return `你是一个影评生成助手。请生成真实自然的观众评论。
影片:${movieName}
${movieInfo ? `简介:${movieInfo}` : ''}
${searchResults ? `\n网络评价参考:\n${searchResults}` : ''}
任务要求:
1. 生成${count}条观众评论
2. 每条评论50-200字,口语化、自然
3. 观点多样化:有好评、中评、差评,比例大约6:3:1
4. 可以包含:
- 个人观影感受和情感共鸣
- 对演员演技的评价
- 对剧情、节奏、画面的看法
- 与其他作品的对比
- 推荐或不推荐的理由
5. 避免:
- 过于专业的影评术语
- 千篇一律的表达
- 明显的AI痕迹
- 重复的内容
请直接输出JSON数组格式,不要有其他文字:
[
{
"content": "评论内容",
"rating": 4,
"sentiment": "positive"
}
]
注意:rating为1-5的整数或null(表示未评分),sentiment为positive/neutral/negative之一。`;
}
// 联网搜索影片资料
async function searchMovieInfo(
movieName: string,
aiConfig: GenerateCommentsParams['aiConfig']
): Promise<string> {
if (!aiConfig.EnableWebSearch) {
return '';
}
try {
const provider = aiConfig.WebSearchProvider || 'tavily';
let searchResults = '';
if (provider === 'tavily' && aiConfig.TavilyApiKey) {
const response = await fetch('https://api.tavily.com/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
api_key: aiConfig.TavilyApiKey,
query: `${movieName} 影评 评价`,
max_results: 5,
}),
});
if (response.ok) {
const data = await response.json();
searchResults = data.results
?.map((r: any) => r.content)
.join('\n')
.slice(0, 1000);
}
} else if (provider === 'serper' && aiConfig.SerperApiKey) {
const response = await fetch('https://google.serper.dev/search', {
method: 'POST',
headers: {
'X-API-KEY': aiConfig.SerperApiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
q: `${movieName} 影评 评价`,
num: 5,
}),
});
if (response.ok) {
const data = await response.json();
searchResults = data.organic
?.map((r: any) => r.snippet)
.join('\n')
.slice(0, 1000);
}
} else if (provider === 'serpapi' && aiConfig.SerpApiKey) {
const response = await fetch(
`https://serpapi.com/search?q=${encodeURIComponent(movieName + ' 影评 评价')}&api_key=${aiConfig.SerpApiKey}&num=5`
);
if (response.ok) {
const data = await response.json();
searchResults = data.organic_results
?.map((r: any) => r.snippet)
.join('\n')
.slice(0, 1000);
}
}
return searchResults;
} catch (error) {
console.error('搜索影片资料失败:', error);
return '';
}
}
// 调用AI生成评论
export async function generateAIComments(
params: GenerateCommentsParams
): Promise<AIComment[]> {
const { movieName, movieInfo, count = 10, aiConfig } = params;
try {
// 1. 联网搜索影片资料(如果启用)
const searchResults = await searchMovieInfo(movieName, aiConfig);
// 2. 构建Prompt
const prompt = buildCommentPrompt(movieName, movieInfo, searchResults, count);
// 3. 调用AI API
const baseURL = normalizeApiBaseUrl(aiConfig.CustomBaseURL);
const response = await fetch(`${baseURL}/chat/completions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${aiConfig.CustomApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: aiConfig.CustomModel,
messages: [
{
role: 'system',
content:
'你是一个专业的影评生成助手,擅长生成真实自然的观众评论。',
},
{
role: 'user',
content: prompt,
},
],
temperature: aiConfig.Temperature ?? 0.8,
max_tokens: aiConfig.MaxTokens ?? 2000,
}),
});
if (!response.ok) {
throw new Error(`AI API调用失败: ${response.status}`);
}
const data = await response.json();
const content = data.choices?.[0]?.message?.content;
if (!content) {
throw new Error('AI返回内容为空');
}
// 4. 解析AI返回的JSON
let commentsData: CommentData[];
try {
// 尝试提取JSON(可能被markdown代码块包裹)
const jsonMatch = content.match(/\[[\s\S]*\]/);
if (jsonMatch) {
commentsData = JSON.parse(jsonMatch[0]);
} else {
commentsData = JSON.parse(content);
}
} catch (parseError) {
console.error('解析AI返回的JSON失败:', content);
throw new Error('AI返回格式错误');
}
// 5. 转换为AIComment格式
const aiComments: AIComment[] = commentsData.map((comment, index) => {
const timestamp = Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000; // 随机过去30天内
const date = new Date(timestamp);
return {
id: `ai-${Date.now()}-${index}`,
userName: generateUserName(index),
userAvatar: generateAvatar(index),
rating: comment.rating,
content: comment.content,
time: formatTime(date),
votes: generateVotes(comment.sentiment),
isAiGenerated: true,
};
});
return aiComments;
} catch (error) {
console.error('AI评论生成失败:', error);
throw error;
}
}
// 生成虚拟用户名
function generateUserName(index: number): string {
const prefixes = [
'影迷',
'观众',
'电影爱好者',
'剧迷',
'路人',
'网友',
'看客',
];
const prefix = prefixes[index % prefixes.length];
return `${prefix}${Math.floor(Math.random() * 9000) + 1000}`;
}
// 生成头像URL(使用DiceBear API
function generateAvatar(seed: number): string {
const styles = ['avataaars', 'bottts', 'personas', 'micah'];
const style = styles[seed % styles.length];
return `https://api.dicebear.com/7.x/${style}/svg?seed=${seed}`;
}
// 格式化时间
function formatTime(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// 根据情感生成点赞数
function generateVotes(sentiment: string): number {
if (sentiment === 'positive') {
return Math.floor(Math.random() * 100) + 20; // 20-120
} else if (sentiment === 'neutral') {
return Math.floor(Math.random() * 50) + 5; // 5-55
} else {
return Math.floor(Math.random() * 30); // 0-30
}
}