1414 lines
45 KiB
JavaScript
1414 lines
45 KiB
JavaScript
import { GoogleGenAI, Type, Modality } from "@google/genai";
|
||
import { createLogger, logApiRequest, logPerformance } from '../utils/logger.js';
|
||
import { withRetry, handleError, AppError, ERROR_TYPES } from '../utils/errorHandler.js';
|
||
/**
|
||
* AI服务模块
|
||
* 提供完整的AI功能,包括文本优化、角色生成等
|
||
* 与geminiService.ts保持接口一致性
|
||
*/
|
||
// ==================== 配置和常量 ====================
|
||
// Google GenAI 初始化
|
||
// const API_KEY = import.meta.env.VITE_GOOGLE_API_KEY || process.env.GOOGLE_API_KEY;
|
||
const API_KEY ='AIzaSyBmPgJKMnG7afAXR9JW14I5XSkOd_NwCVM';
|
||
if (!API_KEY) {
|
||
console.warn("Google API Key not found. AI services may not work properly.");
|
||
}
|
||
const ai = API_KEY ? new GoogleGenAI({ apiKey: API_KEY }) : null;
|
||
const AI_CONFIG = {
|
||
// Google GenAI 模型配置
|
||
model: 'gemini-2.5-flash',
|
||
|
||
// API配置
|
||
endpoints: {
|
||
optimize: '/ai/optimize-prompt',
|
||
character: '/ai/generate-character',
|
||
analyze: '/ai/analyze-image'
|
||
},
|
||
|
||
// 请求配置
|
||
timeout: 30000, // 30秒超时
|
||
retryAttempts: 3,
|
||
retryDelay: 1000, // 1秒重试延迟
|
||
|
||
// 缓存配置
|
||
cacheEnabled: true,
|
||
cacheExpiry: 5 * 60 * 1000, // 5分钟缓存
|
||
};
|
||
|
||
// ==================== 工具函数 ====================
|
||
|
||
/**
|
||
* 将DataURL转换为生成式AI部分格式
|
||
* 与geminiService.ts中的dataUrlToGenerativePart保持一致
|
||
*/
|
||
const dataUrlToGenerativePart = (dataUrl) => {
|
||
if (!dataUrl) return null;
|
||
|
||
const parts = dataUrl.split(',');
|
||
const mimeType = parts[0].match(/:(.*?);/)?.[1];
|
||
const base64Data = parts[1];
|
||
|
||
if (!mimeType || !base64Data) {
|
||
throw new Error("Invalid data URL format");
|
||
}
|
||
|
||
return {
|
||
inlineData: {
|
||
data: base64Data,
|
||
mimeType: mimeType,
|
||
},
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 日志记录器
|
||
*/
|
||
const logger = createLogger('AIService');
|
||
|
||
// ==================== 缓存机制 ====================
|
||
const cache = new Map();
|
||
|
||
const getCacheKey = (method, params) => {
|
||
return `${method}_${JSON.stringify(params)}`;
|
||
};
|
||
|
||
const getFromCache = (key) => {
|
||
if (!AI_CONFIG.cacheEnabled) return null;
|
||
|
||
const cached = cache.get(key);
|
||
if (cached && Date.now() - cached.timestamp < AI_CONFIG.cacheExpiry) {
|
||
logger.info('Cache hit', key);
|
||
return cached.data;
|
||
}
|
||
|
||
if (cached) {
|
||
cache.delete(key); // 清除过期缓存
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
const setCache = (key, data) => {
|
||
if (!AI_CONFIG.cacheEnabled) return;
|
||
|
||
cache.set(key, {
|
||
data,
|
||
timestamp: Date.now()
|
||
});
|
||
|
||
// 清理过期缓存
|
||
if (cache.size > 100) { // 限制缓存大小
|
||
const now = Date.now();
|
||
for (const [k, v] of cache.entries()) {
|
||
if (now - v.timestamp > AI_CONFIG.cacheExpiry) {
|
||
cache.delete(k);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// ==================== 错误处理 ====================
|
||
|
||
/**
|
||
* AI服务专用错误处理
|
||
* @param {Error} error - 原始错误
|
||
* @param {string} context - 错误上下文
|
||
* @returns {AppError} 处理后的错误
|
||
*/
|
||
const handleAIError = async (error, context = '') => {
|
||
// 如果已经是AppError,直接返回
|
||
if (error instanceof AppError) {
|
||
return error;
|
||
}
|
||
|
||
let errorType = ERROR_TYPES.AI_SERVICE;
|
||
let message = error.message || '未知AI服务错误';
|
||
|
||
// 根据错误信息进行分类
|
||
if (error.response) {
|
||
const status = error.response.status;
|
||
const responseData = error.response.data;
|
||
|
||
if (status === 401) {
|
||
errorType = ERROR_TYPES.AUTHENTICATION;
|
||
message = 'AI服务认证失败,请检查API密钥';
|
||
} else if (status === 403) {
|
||
errorType = ERROR_TYPES.AUTHORIZATION;
|
||
message = 'AI服务权限不足';
|
||
} else if (status === 429) {
|
||
errorType = ERROR_TYPES.RATE_LIMIT;
|
||
message = 'AI服务请求频率过高,请稍后重试';
|
||
} else if (status >= 500) {
|
||
errorType = ERROR_TYPES.SERVER;
|
||
message = 'AI服务器暂时不可用';
|
||
}
|
||
|
||
// 尝试从响应中获取更详细的错误信息
|
||
if (responseData && responseData.message) {
|
||
message = responseData.message;
|
||
}
|
||
} else if (error.code === 'ECONNABORTED' || message.includes('timeout')) {
|
||
errorType = ERROR_TYPES.TIMEOUT;
|
||
message = 'AI服务请求超时';
|
||
} else if (error.code === 'NETWORK_ERROR' || message.includes('Network')) {
|
||
errorType = ERROR_TYPES.NETWORK;
|
||
message = '网络连接失败,无法访问AI服务';
|
||
}
|
||
|
||
const aiError = new AppError(message, errorType, 'high', {
|
||
originalError: error.message,
|
||
context,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
logger.error(`AI Service Error in ${context}`, {
|
||
type: aiError.type,
|
||
message: aiError.message,
|
||
originalError: error.message,
|
||
stack: error.stack
|
||
});
|
||
|
||
return aiError;
|
||
};
|
||
|
||
// ==================== 核心AI服务函数 ====================
|
||
|
||
|
||
|
||
/**
|
||
* 生成3D模型并轮询状态直到完成
|
||
* @param {string} imageUrl - 图片URL
|
||
* @param {Object} options - 生成选项
|
||
* @param {Function} onProgress - 进度回调函数
|
||
* @returns {Promise<Object>} 生成结果
|
||
*/
|
||
export const generate3DModelWithPolling = async (imageUrl, options = {}, onProgress = null) => {
|
||
const startTime = Date.now();
|
||
logger.info('Starting 3D model generation with polling', { imageUrl, options });
|
||
|
||
try {
|
||
// 1. 创建生成任务
|
||
const createResult = await generate3DModel(imageUrl, options);
|
||
|
||
if (onProgress) {
|
||
onProgress({
|
||
status: 'created',
|
||
taskId: createResult.taskId,
|
||
message: '任务已创建,开始生成模型'
|
||
});
|
||
}
|
||
|
||
// 2. 轮询任务状态
|
||
let attempts = 0;
|
||
const maxAttempts = 60; // 最多尝试60次
|
||
const interval = 5000; // 每5秒查询一次
|
||
|
||
while (attempts < maxAttempts) {
|
||
// 获取任务状态
|
||
const statusResult = await get3DModelStatus(createResult.taskId);
|
||
|
||
// 调用进度回调
|
||
if (onProgress) {
|
||
onProgress({
|
||
status: statusResult.status,
|
||
taskId: statusResult.taskId,
|
||
message: `正在生成模型... (${attempts + 1}/${maxAttempts})`,
|
||
progress: Math.min((attempts + 1) / maxAttempts * 100, 90) // 最多显示90%进度
|
||
});
|
||
}
|
||
|
||
// 检查任务是否完成
|
||
if (statusResult.status === 'completed') {
|
||
if (onProgress) {
|
||
onProgress({
|
||
status: 'completed',
|
||
taskId: statusResult.taskId,
|
||
message: '模型生成完成',
|
||
progress: 100,
|
||
modelUrl: statusResult.modelUrl,
|
||
thumbnailUrl: statusResult.thumbnailUrl
|
||
});
|
||
}
|
||
|
||
// 记录性能
|
||
logPerformance('generate3DModelWithPolling', startTime);
|
||
|
||
return {
|
||
taskId: statusResult.taskId,
|
||
status: 'completed',
|
||
modelUrl: statusResult.modelUrl,
|
||
thumbnailUrl: statusResult.thumbnailUrl,
|
||
parts: statusResult.parts || []
|
||
};
|
||
}
|
||
|
||
// 检查任务是否失败
|
||
if (statusResult.status === 'failed') {
|
||
throw new AppError('模型生成失败', ERROR_TYPES.AI_SERVICE, 'high', {
|
||
taskId: statusResult.taskId
|
||
});
|
||
}
|
||
|
||
// 等待后再次检查
|
||
await new Promise(resolve => setTimeout(resolve, interval));
|
||
attempts++;
|
||
}
|
||
|
||
// 超时
|
||
throw new AppError('模型生成超时', ERROR_TYPES.TIMEOUT, 'high', {
|
||
taskId: createResult.taskId,
|
||
attempts
|
||
});
|
||
|
||
} catch (error) {
|
||
// 错误处理
|
||
throw await handleAIError(error, 'generate3DModelWithPolling');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 优化文本提示词
|
||
* 与geminiService.ts中的optimizePrompt保持接口一致
|
||
* @param {string} keywords - 用户输入的关键词/文本
|
||
* @param {string|null} inspirationImage - 可选的参考图片DataURL
|
||
* @returns {Promise<Object>} 优化后的角色档案或文本结果
|
||
*/
|
||
export const optimizePrompt = async (keywords, inspirationImage = null) => {
|
||
const startTime = Date.now();
|
||
logger.info('Starting prompt optimization', { keywords: keywords?.substring(0, 50), hasImage: !!inspirationImage });
|
||
try {
|
||
// 输入验证
|
||
if (!keywords || !keywords.trim()) {
|
||
throw new AppError('请输入要优化的文本内容', ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
|
||
// 检查AI服务是否可用
|
||
if (!ai) {
|
||
throw new AppError('AI服务未初始化,请检查API密钥配置', ERROR_TYPES.AI_SERVICE, 'high');
|
||
}
|
||
|
||
// 检查缓存
|
||
const cacheKey = getCacheKey('optimizePrompt', { keywords, hasImage: !!inspirationImage });
|
||
const cached = getFromCache(cacheKey);
|
||
if (cached) {
|
||
logger.info('Cache hit for prompt optimization');
|
||
return cached;
|
||
}
|
||
|
||
// 构建内容,与geminiService.ts保持一致
|
||
let contents = {
|
||
parts: [{
|
||
text: `Based on the following keywords, create a rich and detailed character profile. Keywords: "${keywords}"`
|
||
}]
|
||
};
|
||
|
||
if (inspirationImage) {
|
||
try {
|
||
const imagePart = dataUrlToGenerativePart(inspirationImage);
|
||
if (imagePart) {
|
||
contents.parts.push(imagePart);
|
||
contents.parts[0].text = `Analyze the provided image and use it as inspiration. Then, based on the following keywords, create a rich and detailed character profile. Keywords: "${keywords}"`;
|
||
}
|
||
} catch (imageError) {
|
||
logger.warn('Failed to process inspiration image', imageError);
|
||
// 继续处理,但不使用图片
|
||
}
|
||
}
|
||
|
||
// 执行AI请求(带重试机制和性能监控)
|
||
logger.time('AI Request');
|
||
const result = await withRetry(async () => {
|
||
const requestStart = Date.now();
|
||
const response = await ai.models.generateContent({
|
||
model: AI_CONFIG.model,
|
||
contents: contents,
|
||
config: {
|
||
responseMimeType: "application/json",
|
||
responseSchema: {
|
||
type: Type.OBJECT,
|
||
properties: {
|
||
name: { type: Type.STRING, description: "Character's unique name." },
|
||
gender: { type: Type.STRING, description: "Character's gender." },
|
||
appearance: { type: Type.STRING, description: "Detailed description of the character's physical appearance, inspired by the image if provided." },
|
||
personality: { type: Type.STRING, description: "Key personality traits." },
|
||
backstory: { type: Type.STRING, description: "A brief but compelling backstory." },
|
||
skills: { type: Type.ARRAY, items: { type: Type.STRING }, description: "List of unique skills or powers." },
|
||
birthday: { type: Type.STRING, description: "Character's birthday (e.g., 'October 31st')." },
|
||
likes: { type: Type.ARRAY, items: { type: Type.STRING }, description: "List of things the character likes." }
|
||
},
|
||
required: ["name", "gender", "appearance", "personality", "backstory", "skills", "birthday", "likes"]
|
||
},
|
||
},
|
||
});
|
||
|
||
const requestDuration = Date.now() - requestStart;
|
||
logApiRequest('GoogleGenAI', 'generateContent', 200, requestDuration, {
|
||
hasImage: !!inspirationImage,
|
||
keywordsLength: keywords.length
|
||
});
|
||
|
||
// 解析响应
|
||
const parsed = JSON.parse(response.text);
|
||
return parsed;
|
||
}, {
|
||
maxAttempts: AI_CONFIG.retryAttempts,
|
||
baseDelay: AI_CONFIG.retryDelay
|
||
});
|
||
|
||
logger.timeEnd('AI Request');
|
||
|
||
// 构建返回结果,严格按照React项目的CharacterProfile接口定义
|
||
const characterProfile = {
|
||
name: result.name || '未命名角色',
|
||
gender: result.gender || '未知',
|
||
appearance: result.appearance || '外观描述待生成',
|
||
personality: result.personality || '性格特征待定义',
|
||
backstory: result.backstory || '背景故事待完善',
|
||
skills: Array.isArray(result.skills) ? result.skills : ['待定义技能'],
|
||
birthday: result.birthday || '生日待设定',
|
||
likes: Array.isArray(result.likes) ? result.likes : ['兴趣爱好待添加']
|
||
};
|
||
|
||
const processingTime = Date.now() - startTime;
|
||
|
||
// 缓存结果(缓存纯净的CharacterProfile对象)
|
||
setCache(cacheKey, characterProfile);
|
||
|
||
// 记录性能指标
|
||
logPerformance('optimizePrompt', processingTime, {
|
||
characterName: characterProfile.name,
|
||
hasImage: !!inspirationImage,
|
||
keywordsLength: keywords.length,
|
||
cacheHit: false
|
||
});
|
||
|
||
logger.info('Prompt optimization completed successfully', {
|
||
processingTime,
|
||
characterName: characterProfile.name
|
||
});
|
||
|
||
// 直接返回CharacterProfile对象,与React项目保持一致
|
||
return characterProfile;
|
||
|
||
} catch (error) {
|
||
const aiError = await handleAIError(error, 'optimizePrompt');
|
||
|
||
// 记录失败的性能指标
|
||
logPerformance('optimizePrompt', Date.now() - startTime, {
|
||
success: false,
|
||
errorType: aiError.type,
|
||
hasImage: !!inspirationImage
|
||
});
|
||
|
||
// 抛出错误,让调用方处理,与React项目保持一致
|
||
throw new Error(aiError.getUserMessage());
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 生成创意建议
|
||
* @param {string} style - 风格类型
|
||
* @param {string} prompt - 提示词
|
||
* @returns {Promise<Object>} 创意建议结果
|
||
*/
|
||
export const generateCreativeSuggestions = async (style, prompt) => {
|
||
const startTime = Date.now();
|
||
logger.info('Generating creative suggestions', { style, prompt: prompt?.substring(0, 50) });
|
||
|
||
try {
|
||
// 输入验证
|
||
if (!style || !prompt) {
|
||
throw new AppError('风格和提示词不能为空', ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
|
||
// 检查AI服务是否可用
|
||
if (!ai) {
|
||
throw new AppError('AI服务未初始化,请检查API密钥配置', ERROR_TYPES.AI_SERVICE, 'high');
|
||
}
|
||
|
||
// 检查缓存
|
||
const cacheKey = getCacheKey('generateCreativeSuggestions', { style, prompt });
|
||
const cached = getFromCache(cacheKey);
|
||
if (cached) {
|
||
return cached;
|
||
}
|
||
|
||
// 构建内容
|
||
const contents = {
|
||
parts: [{
|
||
text: `Generate creative suggestions for the following prompt in ${style} style: "${prompt}". Provide 5-8 diverse and inspiring suggestions.`
|
||
}]
|
||
};
|
||
|
||
// 执行AI请求
|
||
const result = await withRetry(async () => {
|
||
const requestStart = Date.now();
|
||
|
||
const response = await ai.models.generateContent({
|
||
model: AI_CONFIG.model,
|
||
contents: contents,
|
||
config: {
|
||
responseMimeType: "application/json",
|
||
responseSchema: {
|
||
type: Type.OBJECT,
|
||
properties: {
|
||
suggestions: {
|
||
type: Type.ARRAY,
|
||
items: { type: Type.STRING },
|
||
description: "List of creative suggestions"
|
||
}
|
||
},
|
||
required: ["suggestions"]
|
||
},
|
||
},
|
||
});
|
||
|
||
const requestDuration = Date.now() - requestStart;
|
||
logApiRequest('GoogleGenAI', 'generateContent', 200, requestDuration, {
|
||
style,
|
||
promptLength: prompt.length
|
||
});
|
||
|
||
const parsed = JSON.parse(response.text);
|
||
return parsed;
|
||
}, {
|
||
maxAttempts: AI_CONFIG.retryAttempts,
|
||
baseDelay: AI_CONFIG.retryDelay
|
||
});
|
||
|
||
const finalResult = {
|
||
success: true,
|
||
suggestions: result.suggestions || [],
|
||
style,
|
||
originalPrompt: prompt,
|
||
processingTime: Date.now() - startTime,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
// 缓存结果
|
||
setCache(cacheKey, finalResult);
|
||
|
||
logger.info('Creative suggestions generated', {
|
||
processingTime: finalResult.processingTime,
|
||
suggestionsCount: finalResult.suggestions.length
|
||
});
|
||
|
||
return finalResult;
|
||
|
||
} catch (error) {
|
||
const aiError = await handleAIError(error, 'generateCreativeSuggestions');
|
||
|
||
// 记录失败的性能指标
|
||
logPerformance('generateCreativeSuggestions', Date.now() - startTime, {
|
||
success: false,
|
||
errorType: aiError.type,
|
||
style
|
||
});
|
||
|
||
// 返回错误结果,保持接口一致性
|
||
return {
|
||
success: false,
|
||
error: aiError.getUserMessage(),
|
||
errorType: aiError.type,
|
||
style,
|
||
originalPrompt: prompt,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 分析图片内容
|
||
* @param {string} imageDataUrl - 图片DataURL
|
||
* @returns {Promise<Object>} 图片分析结果
|
||
*/
|
||
export const analyzeImage = async (imageDataUrl) => {
|
||
const startTime = Date.now();
|
||
logger.info('Starting image analysis', { hasImage: !!imageDataUrl });
|
||
|
||
try {
|
||
// 输入验证
|
||
if (!imageDataUrl) {
|
||
throw new AppError('请提供要分析的图片', ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
|
||
// 检查AI服务是否可用
|
||
if (!ai) {
|
||
throw new AppError('AI服务未初始化,请检查API密钥配置', ERROR_TYPES.AI_SERVICE, 'high');
|
||
}
|
||
|
||
// 检查缓存
|
||
const imageHash = btoa(imageDataUrl.substring(0, 100)); // 简单的图片哈希
|
||
const cacheKey = getCacheKey('analyzeImage', { imageHash });
|
||
const cached = getFromCache(cacheKey);
|
||
if (cached) {
|
||
return cached;
|
||
}
|
||
|
||
// 构建内容
|
||
const imagePart = dataUrlToGenerativePart(imageDataUrl);
|
||
const contents = {
|
||
parts: [
|
||
{
|
||
text: 'Analyze this image and provide detailed information about its content, style, mood, and visual elements. Return the analysis in JSON format.'
|
||
},
|
||
imagePart
|
||
]
|
||
};
|
||
|
||
// 执行AI请求
|
||
const result = await withRetry(async () => {
|
||
const requestStart = Date.now();
|
||
|
||
const response = await ai.models.generateContent({
|
||
model: AI_CONFIG.model,
|
||
contents: contents,
|
||
config: {
|
||
responseMimeType: "application/json",
|
||
responseSchema: {
|
||
type: Type.OBJECT,
|
||
properties: {
|
||
description: { type: Type.STRING, description: "Detailed description of the image" },
|
||
tags: {
|
||
type: Type.ARRAY,
|
||
items: { type: Type.STRING },
|
||
description: "List of relevant tags"
|
||
},
|
||
style: { type: Type.STRING, description: "Artistic or visual style" },
|
||
mood: { type: Type.STRING, description: "Overall mood or atmosphere" },
|
||
colors: {
|
||
type: Type.ARRAY,
|
||
items: { type: Type.STRING },
|
||
description: "Dominant colors"
|
||
},
|
||
objects: {
|
||
type: Type.ARRAY,
|
||
items: { type: Type.STRING },
|
||
description: "Main objects or subjects"
|
||
}
|
||
},
|
||
required: ["description", "tags", "style", "mood"]
|
||
},
|
||
},
|
||
});
|
||
|
||
const requestDuration = Date.now() - requestStart;
|
||
logApiRequest('GoogleGenAI', 'generateContent', 200, requestDuration, {
|
||
hasImage: true,
|
||
imageSize: imageDataUrl.length
|
||
});
|
||
|
||
const parsed = JSON.parse(response.text);
|
||
return parsed;
|
||
}, {
|
||
maxAttempts: AI_CONFIG.retryAttempts,
|
||
baseDelay: AI_CONFIG.retryDelay
|
||
});
|
||
|
||
const finalResult = {
|
||
success: true,
|
||
description: result.description || '图片分析结果',
|
||
tags: Array.isArray(result.tags) ? result.tags : [],
|
||
style: result.style || '未识别风格',
|
||
mood: result.mood || '未识别情绪',
|
||
colors: Array.isArray(result.colors) ? result.colors : [],
|
||
objects: Array.isArray(result.objects) ? result.objects : [],
|
||
processingTime: Date.now() - startTime,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
// 缓存结果
|
||
setCache(cacheKey, finalResult);
|
||
|
||
logger.info('Image analysis completed', {
|
||
processingTime: finalResult.processingTime,
|
||
tagsCount: finalResult.tags.length
|
||
});
|
||
|
||
return finalResult;
|
||
|
||
} catch (error) {
|
||
const processingTime = Date.now() - startTime;
|
||
return handleAIError(error, 'analyzeImage', { hasImage: !!imageDataUrl, processingTime });
|
||
}
|
||
};
|
||
|
||
// ==================== 批处理和性能优化 ====================
|
||
|
||
/**
|
||
* 批量处理多个文本优化请求
|
||
* @param {Array} requests - 请求数组,每个包含 {keywords, inspirationImage}
|
||
* @returns {Promise<Array>} 批量处理结果
|
||
*/
|
||
export const batchOptimizePrompts = async (requests) => {
|
||
const startTime = Date.now();
|
||
logger.info('Starting batch prompt optimization', { requestCount: requests.length });
|
||
|
||
try {
|
||
if (!Array.isArray(requests) || requests.length === 0) {
|
||
throw new Error('请提供有效的批量请求数组');
|
||
}
|
||
|
||
// 限制批量大小
|
||
const maxBatchSize = 10;
|
||
if (requests.length > maxBatchSize) {
|
||
throw new Error(`批量请求数量不能超过 ${maxBatchSize} 个`);
|
||
}
|
||
|
||
// 并发处理,但限制并发数
|
||
const concurrencyLimit = 3;
|
||
const results = [];
|
||
|
||
for (let i = 0; i < requests.length; i += concurrencyLimit) {
|
||
const batch = requests.slice(i, i + concurrencyLimit);
|
||
const batchPromises = batch.map(req =>
|
||
optimizePrompt(req.keywords, req.inspirationImage)
|
||
);
|
||
|
||
const batchResults = await Promise.allSettled(batchPromises);
|
||
results.push(...batchResults.map(result =>
|
||
result.status === 'fulfilled' ? result.value : {
|
||
success: false,
|
||
error: result.reason?.message || '处理失败'
|
||
}
|
||
));
|
||
}
|
||
|
||
const successCount = results.filter(r => r.success).length;
|
||
const failureCount = results.length - successCount;
|
||
|
||
logger.info('Batch prompt optimization completed', {
|
||
totalRequests: requests.length,
|
||
successCount,
|
||
failureCount,
|
||
processingTime: Date.now() - startTime
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
results,
|
||
summary: {
|
||
total: requests.length,
|
||
successful: successCount,
|
||
failed: failureCount,
|
||
processingTime: Date.now() - startTime
|
||
}
|
||
};
|
||
|
||
} catch (error) {
|
||
logger.error('Batch prompt optimization failed', error);
|
||
return handleError(error, 'Batch prompt optimization');
|
||
}
|
||
};
|
||
|
||
// ==================== 缓存管理 ====================
|
||
|
||
/**
|
||
* 清除所有缓存
|
||
*/
|
||
export const clearCache = () => {
|
||
cache.clear();
|
||
logger.info('Cache cleared');
|
||
};
|
||
|
||
/**
|
||
* 获取缓存统计信息
|
||
*/
|
||
export const getCacheStats = () => {
|
||
const now = Date.now();
|
||
let validEntries = 0;
|
||
let expiredEntries = 0;
|
||
|
||
for (const [key, value] of cache.entries()) {
|
||
if (now - value.timestamp < AI_CONFIG.cacheExpiry) {
|
||
validEntries++;
|
||
} else {
|
||
expiredEntries++;
|
||
}
|
||
}
|
||
|
||
return {
|
||
totalEntries: cache.size,
|
||
validEntries,
|
||
expiredEntries,
|
||
cacheEnabled: AI_CONFIG.cacheEnabled,
|
||
cacheExpiry: AI_CONFIG.cacheExpiry
|
||
};
|
||
};
|
||
|
||
// ==================== 配置管理 ====================
|
||
|
||
/**
|
||
* 更新AI服务配置
|
||
* @param {Object} newConfig - 新的配置选项
|
||
*/
|
||
export const updateConfig = (newConfig) => {
|
||
Object.assign(AI_CONFIG, newConfig);
|
||
logger.info('AI service configuration updated', newConfig);
|
||
};
|
||
|
||
/**
|
||
* 获取当前配置
|
||
*/
|
||
export const getConfig = () => {
|
||
return { ...AI_CONFIG };
|
||
};
|
||
|
||
// ==================== 图片生成相关函数 ====================
|
||
|
||
/**
|
||
* 生成图片
|
||
* @param {string} prompt - 图片生成提示词
|
||
* @returns {Promise<string>} 生成的图片DataURL
|
||
*/
|
||
export const generateImage = async (prompt) => {
|
||
const startTime = Date.now();
|
||
logger.info('Starting image generation', { prompt: prompt?.substring(0, 100) });
|
||
|
||
try {
|
||
// 输入验证
|
||
if (!prompt || !prompt.trim()) {
|
||
throw new AppError('请提供图片生成提示词', ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
|
||
// 检查AI服务是否可用
|
||
if (!ai) {
|
||
throw new AppError('AI服务未初始化,请检查API密钥配置', ERROR_TYPES.AI_SERVICE, 'high');
|
||
}
|
||
|
||
// 检查缓存
|
||
const cacheKey = getCacheKey('generateImage', { prompt });
|
||
const cached = getFromCache(cacheKey);
|
||
if (cached) {
|
||
logger.info('Cache hit for image generation');
|
||
return cached;
|
||
}
|
||
// 执行AI请求
|
||
const result = await withRetry(async () => {
|
||
const requestStart = Date.now();
|
||
const response = await ai.models.generateContent({
|
||
model: 'gemini-2.5-flash-image',
|
||
contents: {
|
||
parts: [
|
||
{
|
||
text: prompt,
|
||
},
|
||
],
|
||
},
|
||
config: {
|
||
responseModalities: [Modality.IMAGE],
|
||
},
|
||
});
|
||
|
||
const requestDuration = Date.now() - requestStart;
|
||
logApiRequest('GoogleGenAI', 'generateContent', 200, requestDuration, {
|
||
promptLength: prompt.length
|
||
});
|
||
|
||
// 处理响应,提取图片数据
|
||
for (const part of response.candidates[0].content.parts) {
|
||
if (part.inlineData) {
|
||
const base64ImageBytes = part.inlineData.data;
|
||
const mimeType = part.inlineData.mimeType;
|
||
return `data:${mimeType};base64,${base64ImageBytes}`;
|
||
}
|
||
}
|
||
|
||
throw new Error("API did not return an image.");
|
||
}, {
|
||
maxAttempts: AI_CONFIG.retryAttempts,
|
||
baseDelay: AI_CONFIG.retryDelay
|
||
});
|
||
|
||
// 缓存结果
|
||
setCache(cacheKey, result);
|
||
|
||
const processingTime = Date.now() - startTime;
|
||
|
||
// 记录性能指标
|
||
logPerformance('generateImage', processingTime, {
|
||
promptLength: prompt.length,
|
||
cacheHit: false
|
||
});
|
||
|
||
logger.info('Image generation completed successfully', {
|
||
processingTime,
|
||
resultLength: result.length
|
||
});
|
||
|
||
return result;
|
||
|
||
} catch (error) {
|
||
const aiError = await handleAIError(error, 'generateImage');
|
||
|
||
// 记录失败的性能指标
|
||
logPerformance('generateImage', Date.now() - startTime, {
|
||
success: false,
|
||
errorType: aiError.type,
|
||
promptLength: prompt.length
|
||
});
|
||
|
||
// 抛出错误,让调用方处理
|
||
throw new Error(aiError.getUserMessage());
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 图片高清放大增强功能
|
||
* 使用Gemini 2.5 Flash Image的创意放大器功能
|
||
* @param {string} imageDataUrl - 需要增强的图片DataURL
|
||
* @param {Object} options - 增强选项
|
||
* @param {number} options.scaleFactor - 放大倍数 (默认2倍)
|
||
* @param {string} options.enhanceType - 增强类型: 'upscale' | 'enhance' | 'creative_zoom'
|
||
* @param {string} options.quality - 质量设置: 'high' | 'ultra' | 'creative'
|
||
* @returns {Promise<string>} 增强后的图片DataURL
|
||
*/
|
||
export const enhanceImageResolution = async (imageDataUrl, options = {}) => {
|
||
const startTime = Date.now();
|
||
const {
|
||
scaleFactor = 2,
|
||
enhanceType = 'upscale',
|
||
quality = 'high'
|
||
} = options;
|
||
|
||
logger.info('Starting image resolution enhancement', {
|
||
hasImage: !!imageDataUrl,
|
||
scaleFactor,
|
||
enhanceType,
|
||
quality
|
||
});
|
||
|
||
try {
|
||
// 输入验证
|
||
if (!imageDataUrl) {
|
||
throw new AppError('请提供需要增强的图片', ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
|
||
// 检查AI服务是否可用
|
||
if (!ai) {
|
||
throw new AppError('AI服务未初始化,请检查API密钥配置', ERROR_TYPES.AI_SERVICE, 'high');
|
||
}
|
||
|
||
// 检查缓存
|
||
const imageHash = btoa(imageDataUrl.substring(0, 100));
|
||
const cacheKey = getCacheKey('enhanceImageResolution', {
|
||
imageHash,
|
||
scaleFactor,
|
||
enhanceType,
|
||
quality
|
||
});
|
||
const cached = getFromCache(cacheKey);
|
||
if (cached) {
|
||
logger.info('Cache hit for image enhancement');
|
||
return cached;
|
||
}
|
||
|
||
// 处理输入图片
|
||
const imagePart = dataUrlToGenerativePart(imageDataUrl);
|
||
|
||
// 根据增强类型生成不同的提示词
|
||
let enhancePrompt = '';
|
||
switch (enhanceType) {
|
||
case 'upscale'://放大增强
|
||
enhancePrompt = `Enhance and upscale this image to ${scaleFactor}x higher resolution. Maintain all original details while improving clarity, sharpness, and overall image quality. Remove any noise or artifacts while preserving the original style and composition.`;
|
||
break;
|
||
case 'enhance': // 普通增强
|
||
enhancePrompt = `Enhance this image with superior quality improvements. Increase resolution, improve clarity, enhance details, reduce noise, and optimize colors while maintaining the original artistic style and composition. Focus on ${quality} quality enhancement.`;
|
||
break;
|
||
case 'creative_zoom'://创意放大
|
||
enhancePrompt = `Apply creative upscaling to this image with infinite zoom enhancement. Intelligently extrapolate and enhance details, improve resolution by ${scaleFactor}x, and add realistic details that maintain consistency with the original image style and content.`;
|
||
break;
|
||
default://默认增强
|
||
enhancePrompt = `Enhance and improve the resolution and quality of this image. Make it clearer, sharper, and more detailed while preserving the original content and style.`;
|
||
}
|
||
|
||
// 执行AI请求
|
||
const result = await withRetry(async () => {
|
||
const requestStart = Date.now();
|
||
|
||
const response = await ai.models.generateContent({
|
||
model: 'gemini-2.5-flash-image',
|
||
contents: {
|
||
parts: [
|
||
imagePart,
|
||
{ text: enhancePrompt },
|
||
],
|
||
},
|
||
config: {
|
||
responseModalities: [Modality.IMAGE],
|
||
// 如果支持,可以添加图片配置
|
||
imageConfig: {
|
||
aspectRatio: "1:1" // 保持原始比例,可根据需要调整
|
||
}
|
||
},
|
||
});
|
||
|
||
const requestDuration = Date.now() - requestStart;
|
||
logApiRequest('GoogleGenAI', 'enhanceImageResolution', 200, requestDuration, {
|
||
hasImage: true,
|
||
enhanceType,
|
||
scaleFactor,
|
||
quality
|
||
});
|
||
|
||
// 处理响应,提取图片数据
|
||
for (const part of response.candidates[0].content.parts) {
|
||
if (part.inlineData) {
|
||
const base64ImageBytes = part.inlineData.data;
|
||
const mimeType = part.inlineData.mimeType;
|
||
return `data:${mimeType};base64,${base64ImageBytes}`;
|
||
}
|
||
}
|
||
|
||
throw new Error("API did not return an enhanced image.");
|
||
}, {
|
||
maxAttempts: AI_CONFIG.retryAttempts,
|
||
baseDelay: AI_CONFIG.retryDelay
|
||
});
|
||
|
||
// 缓存结果
|
||
setCache(cacheKey, result);
|
||
|
||
const processingTime = Date.now() - startTime;
|
||
|
||
// 记录性能指标
|
||
logPerformance('enhanceImageResolution', processingTime, {
|
||
hasImage: true,
|
||
enhanceType,
|
||
scaleFactor,
|
||
quality,
|
||
cacheHit: false
|
||
});
|
||
|
||
logger.info('Image enhancement completed successfully', {
|
||
processingTime,
|
||
enhanceType,
|
||
scaleFactor,
|
||
resultLength: result.length
|
||
});
|
||
|
||
return result;
|
||
|
||
} catch (error) {
|
||
const aiError = await handleAIError(error, 'enhanceImageResolution');
|
||
|
||
// 记录失败的性能指标
|
||
logPerformance('enhanceImageResolution', Date.now() - startTime, {
|
||
success: false,
|
||
errorType: aiError.type,
|
||
hasImage: !!imageDataUrl,
|
||
enhanceType,
|
||
scaleFactor
|
||
});
|
||
|
||
// 抛出错误,让调用方处理
|
||
throw new Error(aiError.getUserMessage());
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 批量图片增强功能
|
||
* @param {Array<string>} imageDataUrls - 需要增强的图片DataURL数组
|
||
* @param {Object} options - 增强选项
|
||
* @returns {Promise<Array<string>>} 增强后的图片DataURL数组
|
||
*/
|
||
export const enhanceBatchImages = async (imageDataUrls, options = {}) => {
|
||
const startTime = Date.now();
|
||
logger.info('Starting batch image enhancement', {
|
||
imageCount: imageDataUrls.length,
|
||
options
|
||
});
|
||
|
||
try {
|
||
// 输入验证
|
||
if (!Array.isArray(imageDataUrls) || imageDataUrls.length === 0) {
|
||
throw new AppError('请提供需要增强的图片数组', ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
|
||
// 限制批量处理数量
|
||
const maxBatchSize = 10;
|
||
if (imageDataUrls.length > maxBatchSize) {
|
||
throw new AppError(`批量处理最多支持${maxBatchSize}张图片`, ERROR_TYPES.VALIDATION, 'medium');
|
||
}
|
||
|
||
// 并发处理图片增强
|
||
const enhancePromises = imageDataUrls.map((imageDataUrl, index) =>
|
||
enhanceImageResolution(imageDataUrl, {
|
||
...options,
|
||
// 为每个图片添加索引,便于调试
|
||
batchIndex: index
|
||
})
|
||
);
|
||
|
||
const results = await Promise.all(enhancePromises);
|
||
|
||
const processingTime = Date.now() - startTime;
|
||
|
||
// 记录性能指标
|
||
logPerformance('enhanceBatchImages', processingTime, {
|
||
imageCount: imageDataUrls.length,
|
||
successCount: results.length,
|
||
options
|
||
});
|
||
|
||
logger.info('Batch image enhancement completed successfully', {
|
||
processingTime,
|
||
imageCount: imageDataUrls.length,
|
||
successCount: results.length
|
||
});
|
||
|
||
return results;
|
||
|
||
} catch (error) {
|
||
const aiError = await handleAIError(error, 'enhanceBatchImages');
|
||
|
||
// 记录失败的性能指标
|
||
logPerformance('enhanceBatchImages', Date.now() - startTime, {
|
||
success: false,
|
||
errorType: aiError.type,
|
||
imageCount: imageDataUrls.length
|
||
});
|
||
|
||
// 抛出错误,让调用方处理
|
||
throw new Error(aiError.getUserMessage());
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 生成角色四视图
|
||
* 基于角色图片和描述,生成2x2网格布局的四视图(前、后、左、右)
|
||
* @param {string} characterImage - 角色图片DataURL
|
||
* @param {Object} profile - 角色描述信息
|
||
* @param {string} profile.name - 角色名称
|
||
* @param {string} profile.appearance - 角色外观描述
|
||
* @param {string} profile.personality - 角色性格描述
|
||
* @returns {Promise<string>} 生成的四视图图片DataURL
|
||
*/
|
||
export const generateFourViewSheet = async (characterImage) => {
|
||
try {
|
||
// 构建四视图生成提示词
|
||
const prompt = `Create a professional 4-view orthographic character sheet for 3D modeling, based on the provided reference image.
|
||
1. **[Texture Removal]** It is crucial to **completely remove** all plush textures, surface patterns, and soft shadow details from the original image. Imagine the character with all its fur **shaved off**, revealing its fundamental, solid geometric structure underneath.
|
||
2. **[Form Definition]** I need the output to be a **'Blank Model'** form with a smooth surface, similar to an unpainted **Vinyl Figure** or a resin model. The focus must be on its **core body silhouette and volume**.
|
||
The output MUST be a single image containing four views arranged in a 2x2 grid:
|
||
- Top-left: Front view (frontal view, with the character facing the camera)
|
||
- Top-right: Back view (back view, with the character facing away from the camera)
|
||
- Bottom-left: Left side view (side view, with the character facing to the left)
|
||
- Bottom-right: Right side view (side view, with the character facing to the right)
|
||
Each view must be perfectly orthographic, with no perspective distortion.
|
||
All four views must be precisely aligned horizontally and vertically.
|
||
The background for the entire sheet must be a neutral gray (#808080).
|
||
The character's proportions and art style must be consistent across all views and match the reference image.
|
||
Do not add any text labels or annotations. Just the four views on the gray background.
|
||
`;
|
||
// const prompt = `生成四视图,第一个格式展示正面,第二个格子展示背面,第三个格子展示左面,第四个格子展示右面`
|
||
console.log(prompt,'prompt');
|
||
// 处理角色图片
|
||
const imagePart = dataUrlToGenerativePart(characterImage);
|
||
// 构建请求的 parts 数组
|
||
const parts = [
|
||
imagePart,
|
||
{ text: prompt }
|
||
];
|
||
// 执行AI请求
|
||
const result = await withRetry(async () => {
|
||
const response = await ai.models.generateContent({
|
||
model: 'gemini-2.5-flash-image',
|
||
contents: {
|
||
parts: parts,
|
||
},
|
||
config: {
|
||
responseModalities: [Modality.IMAGE],
|
||
},
|
||
});
|
||
// 处理响应,提取图片数据
|
||
for (const part of response.candidates[0].content.parts) {
|
||
if (part.inlineData) {
|
||
const base64ImageBytes = part.inlineData.data;
|
||
const mimeType = part.inlineData.mimeType;
|
||
return `data:${mimeType};base64,${base64ImageBytes}`;
|
||
}
|
||
}
|
||
throw new Error("API did not return an image.");
|
||
}, {
|
||
maxAttempts: AI_CONFIG.retryAttempts,
|
||
baseDelay: AI_CONFIG.retryDelay
|
||
});
|
||
return result;
|
||
} catch (error) {
|
||
throw new Error(error);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 基于参考图片生成新图片
|
||
* @param {string} baseImage - 参考图片DataURL
|
||
* @param {string} prompt - 图片生成提示词
|
||
* @returns {Promise<string>} 生成的图片DataURL
|
||
*/
|
||
export const generateImageFromImage = async (baseImage, prompt) => {
|
||
const startTime = Date.now();
|
||
logger.info('Starting image generation from image', {
|
||
prompt: prompt?.substring(0, 100),
|
||
hasImage: !!baseImage
|
||
});
|
||
|
||
try {
|
||
// 输入验证
|
||
if (!baseImage) {
|
||
throw new AppError('请提供参考图片', ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
|
||
if (!prompt || !prompt.trim()) {
|
||
throw new AppError('请提供图片生成提示词', ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
|
||
// 检查AI服务是否可用
|
||
if (!ai) {
|
||
throw new AppError('AI服务未初始化,请检查API密钥配置', ERROR_TYPES.AI_SERVICE, 'high');
|
||
}
|
||
|
||
// 检查缓存
|
||
const imageHash = btoa(baseImage.substring(0, 100)); // 简单的图片哈希
|
||
const cacheKey = getCacheKey('generateImageFromImage', {
|
||
imageHash,
|
||
prompt
|
||
});
|
||
const cached = getFromCache(cacheKey);
|
||
if (cached) {
|
||
logger.info('Cache hit for image generation from image');
|
||
return cached;
|
||
}
|
||
|
||
// 处理参考图片
|
||
const imagePart = dataUrlToGenerativePart(baseImage);
|
||
|
||
// 执行AI请求
|
||
const result = await withRetry(async () => {
|
||
const requestStart = Date.now();
|
||
|
||
const response = await ai.models.generateContent({
|
||
model: 'gemini-2.5-flash-image',
|
||
contents: {
|
||
parts: [
|
||
imagePart,
|
||
{ text: prompt },
|
||
],
|
||
},
|
||
config: {
|
||
responseModalities: [Modality.IMAGE],
|
||
},
|
||
});
|
||
|
||
const requestDuration = Date.now() - requestStart;
|
||
logApiRequest('GoogleGenAI', 'generateContent', 200, requestDuration, {
|
||
hasImage: true,
|
||
promptLength: prompt.length
|
||
});
|
||
|
||
// 处理响应,提取图片数据
|
||
for (const part of response.candidates[0].content.parts) {
|
||
if (part.inlineData) {
|
||
const base64ImageBytes = part.inlineData.data;
|
||
const mimeType = part.inlineData.mimeType;
|
||
return `data:${mimeType};base64,${base64ImageBytes}`;
|
||
}
|
||
}
|
||
|
||
throw new Error("API did not return an image.");
|
||
}, {
|
||
maxAttempts: AI_CONFIG.retryAttempts,
|
||
baseDelay: AI_CONFIG.retryDelay
|
||
});
|
||
|
||
// 缓存结果
|
||
setCache(cacheKey, result);
|
||
|
||
const processingTime = Date.now() - startTime;
|
||
|
||
// 记录性能指标
|
||
logPerformance('generateImageFromImage', processingTime, {
|
||
promptLength: prompt.length,
|
||
hasImage: true,
|
||
cacheHit: false
|
||
});
|
||
|
||
logger.info('Image generation from image completed successfully', {
|
||
processingTime,
|
||
resultLength: result.length
|
||
});
|
||
|
||
return result;
|
||
|
||
} catch (error) {
|
||
const aiError = await handleAIError(error, 'generateImageFromImage');
|
||
|
||
// 记录失败的性能指标
|
||
logPerformance('generateImageFromImage', Date.now() - startTime, {
|
||
success: false,
|
||
errorType: aiError.type,
|
||
hasImage: !!baseImage,
|
||
promptLength: prompt.length
|
||
});
|
||
|
||
// 抛出错误,让调用方处理
|
||
throw new Error(aiError.getUserMessage());
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 生成角色图片
|
||
* 参照React项目中的generateCharacterImage实现
|
||
* @param {Object} profile - 角色档案对象
|
||
* @param {string} style - 图片风格
|
||
* @param {string|null} inspirationImage - 可选的参考图片DataURL
|
||
* @returns {Promise<string>} 生成的角色图片DataURL
|
||
*/
|
||
export const generateCharacterImage = async (profile, style, inspirationImage = null) => {
|
||
const startTime = Date.now();
|
||
logger.info('Starting character image generation', {
|
||
profileName: profile?.name,
|
||
style,
|
||
hasInspirationImage: !!inspirationImage
|
||
});
|
||
|
||
try {
|
||
// 输入验证
|
||
if (!profile || !profile.name) {
|
||
throw new AppError('请提供有效的角色档案', ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
|
||
if (!style || !style.trim()) {
|
||
throw new AppError('请提供图片风格', ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
|
||
// 构建提示词
|
||
const prompt = `A full-body character portrait of ${profile.name}.
|
||
Ensure the output image has a portrait aspect ratio of 9:16.
|
||
Appearance: ${profile.appearance}.
|
||
Personality: ${profile.personality}.
|
||
Style: ${style}, high detail, dynamic pose, cinematic lighting, epic fantasy art.
|
||
Cache ID: ${profile.cacheBuster || 'default'}.
|
||
Note: The image should not have white borders
|
||
`;
|
||
|
||
let result;
|
||
if (inspirationImage) {
|
||
// 基于参考图片生成
|
||
const fullPrompt = `Based on the provided image as inspiration, create a new character that fits this description: "${prompt}"`;
|
||
result = await generateImageFromImage(inspirationImage, fullPrompt);
|
||
} else {
|
||
// 纯文本生成
|
||
result = await generateImage(prompt);
|
||
}
|
||
return result;
|
||
} catch (error) {
|
||
const aiError = await handleAIError(error, 'generateCharacterImage');
|
||
|
||
// 记录失败的性能指标
|
||
logPerformance('generateCharacterImage', Date.now() - startTime, {
|
||
success: false,
|
||
errorType: aiError.type,
|
||
profileName: profile?.name || 'unknown',
|
||
style
|
||
});
|
||
|
||
// 抛出错误,让调用方处理
|
||
throw new Error(aiError.getUserMessage());
|
||
}
|
||
};
|
||
|
||
// ==================== 健康检查 ====================
|
||
|
||
/**
|
||
* 从多个参考图片生成新图片
|
||
* 支持多图像融合,可以将不同的图像组合成一个无缝的新视觉效果
|
||
* @param {string|string[]} baseImages - 参考图片DataURL,可以是单个图片或图片数组
|
||
* @param {string} prompt - 图片生成提示词
|
||
* @param {Object} options - 可选配置参数
|
||
* @param {number} options.maxImages - 最大图片数量限制,默认为5
|
||
* @returns {Promise<string>} 生成的图片DataURL
|
||
*/
|
||
export const generateImageFromMultipleImages = async (baseImages, prompt, options = {}) => {
|
||
const startTime = Date.now();
|
||
const { maxImages = 5 } = options;
|
||
|
||
// 标准化输入:确保 baseImages 是数组
|
||
const images = Array.isArray(baseImages) ? baseImages : [baseImages];
|
||
try {
|
||
// 输入验证
|
||
// if (!images || images.length === 0) {
|
||
// throw new AppError('请提供至少一张参考图片', ERROR_TYPES.VALIDATION, 'low');
|
||
// }
|
||
if (images.length > maxImages) {
|
||
throw new AppError(`参考图片数量不能超过 ${maxImages} 张`, ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
// 验证每张图片
|
||
// for (let i = 0; i < images.length; i++) {
|
||
// if (!images[i] || typeof images[i] !== 'string') {
|
||
// throw new AppError(`第 ${i + 1} 张参考图片无效`, ERROR_TYPES.VALIDATION, 'low');
|
||
// }
|
||
|
||
// if (!images[i].startsWith('data:image/')) {
|
||
// throw new AppError(`第 ${i + 1} 张参考图片格式无效,必须是有效的 DataURL`, ERROR_TYPES.VALIDATION, 'low');
|
||
// }
|
||
// }
|
||
if (!prompt || !prompt.trim()) {
|
||
throw new AppError('请提供图片生成提示词', ERROR_TYPES.VALIDATION, 'low');
|
||
}
|
||
// 检查AI服务是否可用
|
||
if (!ai) {
|
||
throw new AppError('AI服务未初始化,请检查API密钥配置', ERROR_TYPES.AI_SERVICE, 'high');
|
||
}
|
||
// 处理多个参考图片
|
||
const imageParts = images.map(image => dataUrlToGenerativePart(image));
|
||
// 构建请求的 parts 数组
|
||
const parts = [
|
||
...imageParts,
|
||
{ text: prompt }
|
||
];
|
||
// 执行AI请求
|
||
const result = await withRetry(async () => {
|
||
const response = await ai.models.generateContent({
|
||
model: 'gemini-2.5-flash-image',
|
||
contents: {
|
||
parts: parts,
|
||
},
|
||
config: {
|
||
responseModalities: [Modality.IMAGE],
|
||
},
|
||
});
|
||
// 处理响应,提取图片数据
|
||
for (const part of response.candidates[0].content.parts) {
|
||
if (part.inlineData) {
|
||
const base64ImageBytes = part.inlineData.data;
|
||
const mimeType = part.inlineData.mimeType;
|
||
return `data:${mimeType};base64,${base64ImageBytes}`;
|
||
}
|
||
}
|
||
|
||
throw new Error("API did not return an image.");
|
||
}, {
|
||
maxAttempts: AI_CONFIG.retryAttempts,
|
||
baseDelay: AI_CONFIG.retryDelay
|
||
});
|
||
return result;
|
||
|
||
} catch (error) {
|
||
console.log(error,'errorerrorerrorerrorerrorerror');
|
||
const aiError = await handleAIError(error, 'generateImageFromMultipleImages');
|
||
|
||
// 记录失败的性能指标
|
||
logPerformance('generateImageFromMultipleImages', Date.now() - startTime, {
|
||
success: false,
|
||
errorType: aiError.type,
|
||
imageCount: images.length,
|
||
promptLength: prompt.length
|
||
});
|
||
|
||
// 抛出错误,让调用方处理
|
||
throw new Error(aiError.getUserMessage());
|
||
}
|
||
};
|
||
|