| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- // 角色替换处理模块
- // 调用 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;
|