aliyun.provider.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.AliyunTtsProvider = void 0;
  7. const axios_1 = __importDefault(require("axios"));
  8. const fs_1 = __importDefault(require("fs"));
  9. const path_1 = __importDefault(require("path"));
  10. const config_1 = require("../../config");
  11. // 阿里云百炼 Qwen TTS Provider
  12. // 文档: https://help.aliyun.com/zh/model-studio/qwen-tts
  13. class AliyunTtsProvider {
  14. apiKey;
  15. model;
  16. voice;
  17. baseUrl = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation';
  18. constructor() {
  19. this.apiKey = config_1.config.dashscope.apiKey;
  20. this.model = config_1.config.dashscope.model;
  21. this.voice = config_1.config.dashscope.voice;
  22. }
  23. // 语音合成(带重试机制)
  24. async synthesize(text, voiceId, params, outputPath, retries = 3, modelOverride) {
  25. const activeModel = modelOverride || this.model;
  26. let lastError = null;
  27. for (let attempt = 1; attempt <= retries; attempt++) {
  28. try {
  29. // 构建请求
  30. const requestBody = {
  31. model: activeModel,
  32. input: {
  33. text: text,
  34. voice: voiceId || this.voice,
  35. language_type: 'Chinese',
  36. },
  37. };
  38. console.log(`📤 [Aliyun TTS] 尝试 ${attempt}/${retries}`);
  39. console.log(` model: ${activeModel}, voice: ${voiceId || this.voice}`);
  40. console.log(` text length: ${text.length}`);
  41. console.log(` params: ${JSON.stringify(params)}`);
  42. // 使用 instruct 模型时支持指令控制
  43. if (activeModel.includes('instruct')) {
  44. const instructions = [];
  45. if (params.speed !== 1) {
  46. const speedDesc = params.speed > 1 ? '较快' : '较慢';
  47. instructions.push(`语速${speedDesc}`);
  48. }
  49. if (params.pitch !== 0) {
  50. const pitchDesc = params.pitch > 0 ? '较高' : '较低';
  51. instructions.push(`音调${pitchDesc}`);
  52. }
  53. if (instructions.length > 0) {
  54. requestBody.parameters = {
  55. instructions: instructions.join(','),
  56. };
  57. }
  58. }
  59. // 发送请求
  60. const response = await axios_1.default.post(this.baseUrl, requestBody, {
  61. headers: {
  62. 'Authorization': `Bearer ${this.apiKey}`,
  63. 'Content-Type': 'application/json',
  64. },
  65. timeout: 60000,
  66. });
  67. // 检查响应
  68. if (response.status !== 200) {
  69. throw new Error(`Qwen TTS 请求失败: ${response.status}`);
  70. }
  71. const data = response.data;
  72. if (data.code) {
  73. throw new Error(`Qwen TTS 错误: ${data.message || JSON.stringify(data)}`);
  74. }
  75. // 获取音频 URL
  76. const audioUrl = data.output?.audio?.url;
  77. console.log('🔗 音频 URL:', audioUrl);
  78. if (!audioUrl) {
  79. throw new Error('Qwen TTS 未返回音频 URL');
  80. }
  81. // 尝试下载音频文件,如果失败则返回 URL
  82. try {
  83. const audioResponse = await axios_1.default.get(audioUrl, {
  84. responseType: 'arraybuffer',
  85. timeout: 60000,
  86. });
  87. // 保存文件
  88. const dir = path_1.default.dirname(outputPath);
  89. if (!fs_1.default.existsSync(dir)) {
  90. fs_1.default.mkdirSync(dir, { recursive: true });
  91. }
  92. // 确保输出路径以 .wav 结尾
  93. const finalPath = outputPath.endsWith('.wav') ? outputPath : outputPath.replace(/\.[^.]+$/, '.wav');
  94. fs_1.default.writeFileSync(finalPath, audioResponse.data);
  95. console.log(`✅ Qwen TTS 生成成功: ${finalPath}`);
  96. return finalPath;
  97. }
  98. catch (downloadError) {
  99. console.warn('⚠️ 音频下载失败,返回云端 URL:', downloadError.message);
  100. // 返回一个特殊的路径标记,表示使用云端 URL
  101. return `cloud:${audioUrl}`;
  102. }
  103. }
  104. catch (error) {
  105. const errorDetails = error.response?.data || error.message;
  106. const isRateLimit = error.response?.status === 429 || errorDetails?.code === 'Throttling.RateQuota';
  107. const isServerError = error.response?.status >= 500;
  108. console.error(`❌ Qwen TTS 调用失败 (尝试 ${attempt}/${retries}):`, JSON.stringify(errorDetails, null, 2));
  109. if (isRateLimit && attempt < retries) {
  110. // 速率限制:等待后重试(指数退避)
  111. const waitTime = Math.pow(2, attempt) * 1000;
  112. console.warn(`⏳ 速率限制,等待 ${waitTime}ms 后重试...`);
  113. await new Promise(resolve => setTimeout(resolve, waitTime));
  114. lastError = new Error(`Qwen TTS 速率限制: ${error.message}`);
  115. continue;
  116. }
  117. if (isServerError && attempt < retries) {
  118. // 服务器错误:等待后重试
  119. const waitTime = Math.pow(2, attempt) * 1000;
  120. console.warn(`⏳ 服务器错误,等待 ${waitTime}ms 后重试...`);
  121. await new Promise(resolve => setTimeout(resolve, waitTime));
  122. lastError = new Error(`Qwen TTS 服务器错误: ${error.message}`);
  123. continue;
  124. }
  125. // 达到最大重试次数或不可重试的错误
  126. throw new Error(`Qwen TTS 服务调用失败: ${error.message}, 详情: ${JSON.stringify(errorDetails)}`);
  127. }
  128. }
  129. // 理论上不会到达这里,但为了类型安全
  130. throw lastError || new Error('Qwen TTS 服务调用失败');
  131. }
  132. }
  133. exports.AliyunTtsProvider = AliyunTtsProvider;