ai评论生成

This commit is contained in:
mtvpls
2026-03-14 11:30:17 +08:00
parent ea169593a6
commit 11317abc98
8 changed files with 731 additions and 0 deletions
+284
View File
@@ -0,0 +1,284 @@
// AI评论生成核心逻辑
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 response = await fetch(`${aiConfig.CustomBaseURL}/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
}
}