img2text.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import { OpenAI } from 'openai';
  2. import { createRequire } from 'module';
  3. import config from '../config.js';
  4. const require = createRequire(import.meta.url);
  5. // 预加载豆包模块(如果需要)
  6. // doubao 模块使用 CommonJS,需要通过 require 加载
  7. let doubaoImgReg = null;
  8. function getDoubaoImgReg() {
  9. if (!doubaoImgReg) {
  10. doubaoImgReg = require('./doubao/img-reg.js');
  11. }
  12. return doubaoImgReg;
  13. }
  14. // 注意:只有在使用非豆包模型时才需要 OpenAI 客户端
  15. // 豆包模型使用自己的 API URL,不会使用 BASE_URL_IMG2TEXT
  16. let client = null;
  17. function getOpenAIClient() {
  18. if (!client) {
  19. client = new OpenAI({
  20. apiKey: config.API_KEY,
  21. baseURL: config.BASE_URL_IMG2TEXT
  22. });
  23. }
  24. return client;
  25. }
  26. /**
  27. * 检查模型是否为豆包模型
  28. */
  29. function isDoubaoModel(modelName) {
  30. if (!modelName) return false;
  31. const modelLower = modelName.toLowerCase();
  32. return modelLower.includes('doubao') ||
  33. modelLower.includes('豆包') ||
  34. modelLower.includes('doubao-1.5') ||
  35. modelLower.includes('ep-'); // 豆包的 endpoint 格式
  36. }
  37. /**
  38. * 从 data URI 中提取 base64 数据
  39. */
  40. function extractBase64FromDataUri(dataUri) {
  41. // data:image/jpeg;base64,/9j/4AAQSkZJRg...
  42. const match = dataUri.match(/^data:([^;]+);base64,(.+)$/);
  43. if (match) {
  44. return {
  45. mimeType: match[1],
  46. base64: match[2]
  47. };
  48. }
  49. // 如果不是 data URI,直接返回
  50. return {
  51. mimeType: 'image/jpeg',
  52. base64: dataUri
  53. };
  54. }
  55. async function img2text(prompt, imageUrl) {
  56. try {
  57. if (!prompt) {
  58. throw new Error('Missing prompt');
  59. }
  60. if (!imageUrl) {
  61. throw new Error('Missing imageUrl');
  62. }
  63. // 检查是否为豆包模型
  64. if (isDoubaoModel(config.MODEL_IMG2TEXT)) {
  65. console.log(`📦 使用豆包模型: ${config.MODEL_IMG2TEXT}`);
  66. console.log(`⚠️ 注意:豆包模型使用自己的 API URL,不使用 BASE_URL_IMG2TEXT`);
  67. // 直接使用 require 调用豆包的 imgReg 函数
  68. // 豆包的 API URL 在 doubao/doubao.js 中定义:https://ark.cn-beijing.volces.com/api/v3/responses
  69. const imgReg = getDoubaoImgReg();
  70. // 从 imageUrl 中提取 base64 数据(img-reg.js 需要不含 data URI 前缀的 base64)
  71. const { base64, mimeType } = extractBase64FromDataUri(imageUrl);
  72. // 调用豆包的图片理解 API(使用豆包自己的 BASE_URL_DOUBAO,不会使用 BASE_URL_IMG2TEXT)
  73. const result = await imgReg(prompt, base64, null, mimeType, config.MODEL_IMG2TEXT);
  74. // 转换返回格式以匹配 OpenAI 格式
  75. // 豆包的返回格式可能是:
  76. // 1. { success: true, data: { output: { text: "..." } } }
  77. // 2. { success: true, data: { output: { choices: [{ message: { content: "..." } }] } } }
  78. // 3. { success: true, data: { text: "..." } }
  79. // OpenAI 格式: { success: true, data: { choices: [{ message: { content: ... } }] } }
  80. if (result && result.data) {
  81. // 调试:打印原始返回数据(前500字符)
  82. const resultStr = JSON.stringify(result.data);
  83. console.log(`📦 豆包原始返回数据(前500字符): ${resultStr.substring(0, 500)}`);
  84. // 尝试多种方式提取响应文本
  85. let content = '';
  86. // 方式1: result.data.output.text
  87. if (result.data.output && typeof result.data.output.text === 'string') {
  88. content = result.data.output.text;
  89. }
  90. // 方式2: result.data.output.choices[0].message.content
  91. else if (result.data.output && result.data.output.choices && Array.isArray(result.data.output.choices)) {
  92. content = result.data.output.choices[0]?.message?.content || '';
  93. }
  94. // 方式3: result.data.text
  95. else if (typeof result.data.text === 'string') {
  96. content = result.data.text;
  97. }
  98. // 方式4: result.data.response.text
  99. else if (result.data.response && typeof result.data.response.text === 'string') {
  100. content = result.data.response.text;
  101. }
  102. // 方式5: 递归查找包含文本的字段
  103. else {
  104. const findTextInObject = (obj) => {
  105. if (typeof obj === 'string' && obj.length > 50) {
  106. return obj; // 如果找到较长的字符串,可能是内容
  107. }
  108. if (typeof obj === 'object' && obj !== null) {
  109. for (const key in obj) {
  110. if (key === 'text' || key === 'content' || key === 'message') {
  111. const value = obj[key];
  112. if (typeof value === 'string') return value;
  113. if (typeof value === 'object') {
  114. const found = findTextInObject(value);
  115. if (found) return found;
  116. }
  117. }
  118. }
  119. // 递归查找所有子对象
  120. for (const key in obj) {
  121. if (typeof obj[key] === 'object') {
  122. const found = findTextInObject(obj[key]);
  123. if (found) return found;
  124. }
  125. }
  126. }
  127. return null;
  128. };
  129. content = findTextInObject(result.data) || '';
  130. }
  131. console.log(`📝 提取的文本内容长度: ${content.length} 字符`);
  132. // 转换为 OpenAI 兼容格式
  133. return {
  134. success: true,
  135. data: {
  136. choices: [{
  137. message: {
  138. content: content,
  139. role: 'assistant'
  140. }
  141. }]
  142. }
  143. };
  144. }
  145. // 如果格式不匹配,记录并返回原始结果
  146. console.warn('⚠️ 无法解析豆包返回格式,返回原始结果:', JSON.stringify(result).substring(0, 200));
  147. return result;
  148. }
  149. // 使用 OpenAI 兼容的 API(非豆包模型,使用 BASE_URL_IMG2TEXT)
  150. const response = await getOpenAIClient().chat.completions.create({
  151. model: config.MODEL_IMG2TEXT,
  152. messages: [
  153. {
  154. role: 'user',
  155. content: [
  156. { type: 'text', text: prompt },
  157. {
  158. type: 'image_url',
  159. image_url: {
  160. url: imageUrl,
  161. detail: 'high'
  162. }
  163. }
  164. ]
  165. }
  166. ],
  167. max_tokens: 8000, // 增加token限制以生成更长的小说
  168. stream: false
  169. });
  170. return { success: true, data: response };
  171. } catch (error) {
  172. throw error;
  173. }
  174. }
  175. export default img2text;