replace-character.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. // 角色替换处理模块
  2. // 调用 Gemini API 进行图生图处理
  3. const https = require('https');
  4. const PROMPT_TEXT = `Analyze the layout and poses in image 1 (the template).
  5. Apply the character design from image 2 to that template.
  6. Create a high-quality animated sprite sheet.
  7. Ensure the weapon and character size are consistent across all frames.
  8. CRITICAL INSTRUCTIONS:
  9. 1. The layout, grid structure, and poses MUST EXACTLY match the first image. Use a pure white background.
  10. 2. DO NOT STRETCH the sprites. Maintain the original internal aspect ratio of the characters from image 2.
  11. 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.
  12. 4. Apply the character's appearance (colors, clothing, features) to the poses in the template.`;
  13. class ReplaceCharacterHandler {
  14. /**
  15. * 处理角色替换请求
  16. * @param {http.IncomingMessage} req - HTTP 请求对象
  17. * @param {http.ServerResponse} res - HTTP 响应对象
  18. */
  19. static handleReplaceRequest(req, res) {
  20. if (req.method !== 'POST') {
  21. res.writeHead(405, { 'Content-Type': 'application/json' });
  22. res.end(JSON.stringify({ error: 'Method not allowed' }));
  23. return;
  24. }
  25. let body = '';
  26. req.on('data', (chunk) => {
  27. body += chunk.toString();
  28. });
  29. req.on('end', () => {
  30. try {
  31. const data = JSON.parse(body);
  32. const { image1, image2, image1Width, image1Height, additionalPrompt } = data;
  33. if (!image1 || !image2) {
  34. res.writeHead(400, { 'Content-Type': 'application/json' });
  35. res.end(JSON.stringify({ error: 'Missing required fields: image1, image2' }));
  36. return;
  37. }
  38. if (!image1Width || !image1Height) {
  39. res.writeHead(400, { 'Content-Type': 'application/json' });
  40. res.end(JSON.stringify({ error: 'Missing required fields: image1Width, image1Height' }));
  41. return;
  42. }
  43. // 调用 Gemini API
  44. this.callGeminiAPI(image1, image2, image1Width, image1Height, additionalPrompt || '', res);
  45. } catch (error) {
  46. console.error('[ReplaceCharacterHandler] 解析请求数据失败:', error);
  47. res.writeHead(400, { 'Content-Type': 'application/json' });
  48. res.end(JSON.stringify({ error: 'Invalid JSON data', details: error.message }));
  49. }
  50. });
  51. req.on('error', (error) => {
  52. console.error('[ReplaceCharacterHandler] 请求错误:', error);
  53. res.writeHead(500, { 'Content-Type': 'application/json' });
  54. res.end(JSON.stringify({ error: 'Request error', details: error.message }));
  55. });
  56. }
  57. /**
  58. * 调用 Gemini API
  59. * @param {string} image1Base64 - image1 的 base64 数据(spritesheet)
  60. * @param {string} image2Base64 - image2 的 base64 数据(参考图)
  61. * @param {number} image1Width - image1 的宽度
  62. * @param {number} image1Height - image1 的高度
  63. * @param {string} additionalPrompt - 额外提示词
  64. * @param {http.ServerResponse} res - HTTP 响应对象
  65. */
  66. static callGeminiAPI(image1Base64, image2Base64, image1Width, image1Height, additionalPrompt, res) {
  67. // 移除 data:image/png;base64, 前缀(如果有)
  68. const cleanImage1 = image1Base64.replace(/^data:image\/\w+;base64,/, '');
  69. const cleanImage2 = image2Base64.replace(/^data:image\/\w+;base64,/, '');
  70. // 构建请求内容
  71. const content = [
  72. {
  73. type: "image_url",
  74. image_url: {
  75. url: `data:image/png;base64,${cleanImage1}`
  76. }
  77. },
  78. {
  79. type: "image_url",
  80. image_url: {
  81. url: `data:image/png;base64,${cleanImage2}`
  82. }
  83. },
  84. {
  85. type: "text",
  86. text: this.buildPromptText(image1Width, image1Height, additionalPrompt)
  87. }
  88. ];
  89. const requestData = JSON.stringify({
  90. model: "gemini-3-pro-image-preview",
  91. messages: [{ role: "user", content: content }]
  92. });
  93. const options = {
  94. hostname: 'api.chatanywhere.tech',
  95. port: 443,
  96. path: '/v1/chat/completions',
  97. method: 'POST',
  98. headers: {
  99. 'Authorization': 'Bearer sk-j32LgDixK6pfESYGfJtgc2Tzlmszx5NZhSH0sOzpLQkYuKek',
  100. 'Content-Type': 'application/json',
  101. 'Content-Length': Buffer.byteLength(requestData)
  102. },
  103. timeout: 300000 // 5分钟超时
  104. };
  105. console.log('[ReplaceCharacterHandler] 正在调用 Gemini API...');
  106. const geminiReq = https.request(options, (geminiRes) => {
  107. let responseData = '';
  108. geminiRes.on('data', (chunk) => {
  109. responseData += chunk;
  110. });
  111. geminiRes.on('end', () => {
  112. try {
  113. if (geminiRes.statusCode !== 200) {
  114. console.error('[ReplaceCharacterHandler] Gemini API 返回错误:', geminiRes.statusCode, responseData);
  115. res.writeHead(500, { 'Content-Type': 'application/json' });
  116. res.end(JSON.stringify({ error: 'Gemini API error', status: geminiRes.statusCode, details: responseData }));
  117. return;
  118. }
  119. const response = JSON.parse(responseData);
  120. // 解析响应,提取图片
  121. const imageData = this.extractImageFromResponse(response);
  122. if (!imageData) {
  123. console.error('[ReplaceCharacterHandler] 无法从响应中提取图片');
  124. res.writeHead(500, { 'Content-Type': 'application/json' });
  125. res.end(JSON.stringify({ error: 'Failed to extract image from response' }));
  126. return;
  127. }
  128. // 返回图片数据
  129. res.writeHead(200, {
  130. 'Content-Type': 'application/json',
  131. 'Access-Control-Allow-Origin': '*'
  132. });
  133. res.end(JSON.stringify({
  134. success: true,
  135. imageData: imageData
  136. }));
  137. console.log('[ReplaceCharacterHandler] ✓ 成功处理请求');
  138. } catch (error) {
  139. console.error('[ReplaceCharacterHandler] 解析响应失败:', error);
  140. res.writeHead(500, { 'Content-Type': 'application/json' });
  141. res.end(JSON.stringify({ error: 'Failed to parse response', details: error.message }));
  142. }
  143. });
  144. });
  145. geminiReq.on('error', (error) => {
  146. console.error('[ReplaceCharacterHandler] 请求错误:', error);
  147. res.writeHead(500, { 'Content-Type': 'application/json' });
  148. res.end(JSON.stringify({ error: 'Request failed', details: error.message }));
  149. });
  150. geminiReq.on('timeout', () => {
  151. console.error('[ReplaceCharacterHandler] 请求超时');
  152. geminiReq.destroy();
  153. res.writeHead(500, { 'Content-Type': 'application/json' });
  154. res.end(JSON.stringify({ error: 'Request timeout' }));
  155. });
  156. geminiReq.write(requestData);
  157. geminiReq.end();
  158. }
  159. /**
  160. * 从 Gemini API 响应中提取图片数据
  161. * @param {Object} response - Gemini API 响应对象
  162. * @returns {string|null} base64 图片数据
  163. */
  164. static extractImageFromResponse(response) {
  165. try {
  166. console.log('[ReplaceCharacterHandler] 解析响应:', JSON.stringify(response).substring(0, 500));
  167. // Gemini API 响应格式:response.choices[0].message.content 可能包含图片
  168. // 检查是否有 choices 数组
  169. if (response.choices && response.choices.length > 0) {
  170. const message = response.choices[0].message;
  171. // 检查 content 字段
  172. if (message.content) {
  173. // 如果 content 是数组,查找图片
  174. if (Array.isArray(message.content)) {
  175. for (const item of message.content) {
  176. if (item.type === 'image_url' && item.image_url && item.image_url.url) {
  177. const imageUrl = item.image_url.url;
  178. // 提取 base64 数据
  179. if (imageUrl.startsWith('data:image/')) {
  180. const base64Match = imageUrl.match(/base64,(.+)$/);
  181. if (base64Match) {
  182. console.log('[ReplaceCharacterHandler] ✓ 从 content 数组中找到图片');
  183. return base64Match[1];
  184. }
  185. }
  186. }
  187. }
  188. }
  189. // 如果 content 是字符串,尝试多种解析方式
  190. else if (typeof message.content === 'string') {
  191. // 尝试直接匹配 base64 图片数据
  192. const base64Match = message.content.match(/data:image\/[^;]+;base64,([A-Za-z0-9+/=]+)/);
  193. if (base64Match) {
  194. console.log('[ReplaceCharacterHandler] ✓ 从 content 字符串中直接提取到 base64');
  195. return base64Match[1];
  196. }
  197. // 尝试解析 JSON
  198. try {
  199. const parsed = JSON.parse(message.content);
  200. if (parsed.image_url && parsed.image_url.url) {
  201. const imageUrl = parsed.image_url.url;
  202. if (imageUrl.startsWith('data:image/')) {
  203. const base64Match = imageUrl.match(/base64,(.+)$/);
  204. if (base64Match) {
  205. console.log('[ReplaceCharacterHandler] ✓ 从解析的 JSON 中找到图片');
  206. return base64Match[1];
  207. }
  208. }
  209. }
  210. } catch (e) {
  211. // 不是 JSON,继续其他方式
  212. }
  213. }
  214. }
  215. }
  216. // 如果直接响应中包含图片数据
  217. if (response.image_data) {
  218. console.log('[ReplaceCharacterHandler] ✓ 从响应根对象中找到图片');
  219. return response.image_data;
  220. }
  221. // 尝试在整个响应中搜索 base64 图片数据
  222. const responseStr = JSON.stringify(response);
  223. const base64Match = responseStr.match(/data:image\/[^;]+;base64,([A-Za-z0-9+/=]{100,})/);
  224. if (base64Match) {
  225. console.log('[ReplaceCharacterHandler] ✓ 在整个响应中搜索到 base64');
  226. return base64Match[1];
  227. }
  228. console.error('[ReplaceCharacterHandler] 无法从响应中提取图片');
  229. return null;
  230. } catch (error) {
  231. console.error('[ReplaceCharacterHandler] 提取图片失败:', error);
  232. return null;
  233. }
  234. }
  235. /**
  236. * 构建提示词文本
  237. * @param {number} image1Width - image1 的宽度
  238. * @param {number} image1Height - image1 的高度
  239. * @param {string} additionalPrompt - 额外提示词
  240. * @returns {string} 完整的提示词文本
  241. */
  242. static buildPromptText(image1Width, image1Height, additionalPrompt) {
  243. let prompt = `Analyze the layout and poses in image 1 (the template).
  244. Apply the character design from image 2 to that template.
  245. Create a high-quality animated sprite sheet.
  246. Ensure the weapon and character size are consistent across all frames.
  247. CRITICAL INSTRUCTIONS:
  248. 1. The layout, grid structure, and poses MUST EXACTLY match the first image. Use a pure white background.
  249. 2. DO NOT STRETCH the sprites. Maintain the original internal aspect ratio of the characters from image 2.
  250. 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.
  251. 4. Apply the character's appearance (colors, clothing, features) to the poses in the template.`;
  252. // 如果有额外提示词,添加到末尾
  253. if (additionalPrompt && additionalPrompt.trim()) {
  254. prompt += `\n\nAdditional instructions: ${additionalPrompt.trim()}`;
  255. }
  256. return prompt;
  257. }
  258. }
  259. module.exports = ReplaceCharacterHandler;