// 角色替换处理模块 // 调用 Gemini API 进行图生图处理 const https = require('https'); const PROMPT_TEXT = `Analyze the layout and poses in image 1 (the template). Apply the character design from image 2 to that template. Create a high-quality animated sprite sheet. Ensure the weapon and character size are consistent across all frames. CRITICAL INSTRUCTIONS: 1. The layout, grid structure, and poses MUST EXACTLY match the first image. Use a pure white background. 2. DO NOT STRETCH the sprites. Maintain the original internal aspect ratio of the characters from image 2. 3. If the output target aspect ratio (e.g., [这里填写你的目标比例,如 1:1 或 16:9]) differs from the template, add padding (empty space) rather than stretching the content. 4. Apply the character's appearance (colors, clothing, features) to the poses in the template.`; class ReplaceCharacterHandler { /** * 处理角色替换请求 * @param {http.IncomingMessage} req - HTTP 请求对象 * @param {http.ServerResponse} res - HTTP 响应对象 */ static handleReplaceRequest(req, res) { if (req.method !== 'POST') { res.writeHead(405, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Method not allowed' })); return; } let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', () => { try { const data = JSON.parse(body); const { image1, image2, image1Width, image1Height, additionalPrompt } = data; if (!image1 || !image2) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Missing required fields: image1, image2' })); return; } if (!image1Width || !image1Height) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Missing required fields: image1Width, image1Height' })); return; } // 调用 Gemini API this.callGeminiAPI(image1, image2, image1Width, image1Height, additionalPrompt || '', res); } catch (error) { console.error('[ReplaceCharacterHandler] 解析请求数据失败:', error); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid JSON data', details: error.message })); } }); req.on('error', (error) => { console.error('[ReplaceCharacterHandler] 请求错误:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Request error', details: error.message })); }); } /** * 调用 Gemini API * @param {string} image1Base64 - image1 的 base64 数据(spritesheet) * @param {string} image2Base64 - image2 的 base64 数据(参考图) * @param {number} image1Width - image1 的宽度 * @param {number} image1Height - image1 的高度 * @param {string} additionalPrompt - 额外提示词 * @param {http.ServerResponse} res - HTTP 响应对象 */ static callGeminiAPI(image1Base64, image2Base64, image1Width, image1Height, additionalPrompt, res) { // 移除 data:image/png;base64, 前缀(如果有) const cleanImage1 = image1Base64.replace(/^data:image\/\w+;base64,/, ''); const cleanImage2 = image2Base64.replace(/^data:image\/\w+;base64,/, ''); // 构建请求内容 const content = [ { type: "image_url", image_url: { url: `data:image/png;base64,${cleanImage1}` } }, { type: "image_url", image_url: { url: `data:image/png;base64,${cleanImage2}` } }, { type: "text", text: this.buildPromptText(image1Width, image1Height, additionalPrompt) } ]; const requestData = JSON.stringify({ model: "gemini-3-pro-image-preview", messages: [{ role: "user", content: content }] }); const options = { hostname: 'api.chatanywhere.tech', port: 443, path: '/v1/chat/completions', method: 'POST', headers: { 'Authorization': 'Bearer sk-j32LgDixK6pfESYGfJtgc2Tzlmszx5NZhSH0sOzpLQkYuKek', 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(requestData) }, timeout: 300000 // 5分钟超时 }; console.log('[ReplaceCharacterHandler] 正在调用 Gemini API...'); const geminiReq = https.request(options, (geminiRes) => { let responseData = ''; geminiRes.on('data', (chunk) => { responseData += chunk; }); geminiRes.on('end', () => { try { if (geminiRes.statusCode !== 200) { console.error('[ReplaceCharacterHandler] Gemini API 返回错误:', geminiRes.statusCode, responseData); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Gemini API error', status: geminiRes.statusCode, details: responseData })); return; } const response = JSON.parse(responseData); // 解析响应,提取图片 const imageData = this.extractImageFromResponse(response); if (!imageData) { console.error('[ReplaceCharacterHandler] 无法从响应中提取图片'); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to extract image from response' })); return; } // 返回图片数据 res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: true, imageData: imageData })); console.log('[ReplaceCharacterHandler] ✓ 成功处理请求'); } catch (error) { console.error('[ReplaceCharacterHandler] 解析响应失败:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to parse response', details: error.message })); } }); }); geminiReq.on('error', (error) => { console.error('[ReplaceCharacterHandler] 请求错误:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Request failed', details: error.message })); }); geminiReq.on('timeout', () => { console.error('[ReplaceCharacterHandler] 请求超时'); geminiReq.destroy(); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Request timeout' })); }); geminiReq.write(requestData); geminiReq.end(); } /** * 从 Gemini API 响应中提取图片数据 * @param {Object} response - Gemini API 响应对象 * @returns {string|null} base64 图片数据 */ static extractImageFromResponse(response) { try { console.log('[ReplaceCharacterHandler] 解析响应:', JSON.stringify(response).substring(0, 500)); // Gemini API 响应格式:response.choices[0].message.content 可能包含图片 // 检查是否有 choices 数组 if (response.choices && response.choices.length > 0) { const message = response.choices[0].message; // 检查 content 字段 if (message.content) { // 如果 content 是数组,查找图片 if (Array.isArray(message.content)) { for (const item of message.content) { if (item.type === 'image_url' && item.image_url && item.image_url.url) { const imageUrl = item.image_url.url; // 提取 base64 数据 if (imageUrl.startsWith('data:image/')) { const base64Match = imageUrl.match(/base64,(.+)$/); if (base64Match) { console.log('[ReplaceCharacterHandler] ✓ 从 content 数组中找到图片'); return base64Match[1]; } } } } } // 如果 content 是字符串,尝试多种解析方式 else if (typeof message.content === 'string') { // 尝试直接匹配 base64 图片数据 const base64Match = message.content.match(/data:image\/[^;]+;base64,([A-Za-z0-9+/=]+)/); if (base64Match) { console.log('[ReplaceCharacterHandler] ✓ 从 content 字符串中直接提取到 base64'); return base64Match[1]; } // 尝试解析 JSON try { const parsed = JSON.parse(message.content); if (parsed.image_url && parsed.image_url.url) { const imageUrl = parsed.image_url.url; if (imageUrl.startsWith('data:image/')) { const base64Match = imageUrl.match(/base64,(.+)$/); if (base64Match) { console.log('[ReplaceCharacterHandler] ✓ 从解析的 JSON 中找到图片'); return base64Match[1]; } } } } catch (e) { // 不是 JSON,继续其他方式 } } } } // 如果直接响应中包含图片数据 if (response.image_data) { console.log('[ReplaceCharacterHandler] ✓ 从响应根对象中找到图片'); return response.image_data; } // 尝试在整个响应中搜索 base64 图片数据 const responseStr = JSON.stringify(response); const base64Match = responseStr.match(/data:image\/[^;]+;base64,([A-Za-z0-9+/=]{100,})/); if (base64Match) { console.log('[ReplaceCharacterHandler] ✓ 在整个响应中搜索到 base64'); return base64Match[1]; } console.error('[ReplaceCharacterHandler] 无法从响应中提取图片'); return null; } catch (error) { console.error('[ReplaceCharacterHandler] 提取图片失败:', error); return null; } } /** * 构建提示词文本 * @param {number} image1Width - image1 的宽度 * @param {number} image1Height - image1 的高度 * @param {string} additionalPrompt - 额外提示词 * @returns {string} 完整的提示词文本 */ static buildPromptText(image1Width, image1Height, additionalPrompt) { let prompt = `Analyze the layout and poses in image 1 (the template). Apply the character design from image 2 to that template. Create a high-quality animated sprite sheet. Ensure the weapon and character size are consistent across all frames. CRITICAL INSTRUCTIONS: 1. The layout, grid structure, and poses MUST EXACTLY match the first image. Use a pure white background. 2. DO NOT STRETCH the sprites. Maintain the original internal aspect ratio of the characters from image 2. 3. The output image MUST EXACTLY match image 1's dimensions: width ${image1Width}px, height ${image1Height}px. The output image should have the exact same size and aspect ratio as image 1. Do not change the overall canvas size or aspect ratio. 4. Apply the character's appearance (colors, clothing, features) to the poses in the template.`; // 如果有额外提示词,添加到末尾 if (additionalPrompt && additionalPrompt.trim()) { prompt += `\n\nAdditional instructions: ${additionalPrompt.trim()}`; } return prompt; } } module.exports = ReplaceCharacterHandler;