aliyun-realtime.provider.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  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.AliyunRealtimeTtsProvider = void 0;
  7. const ws_1 = __importDefault(require("ws"));
  8. const fs_1 = __importDefault(require("fs"));
  9. const path_1 = __importDefault(require("path"));
  10. const config_1 = require("../../config");
  11. // 阿里云百炼 Qwen-TTS WebSocket 实时语音合成
  12. // 文档: https://help.aliyun.com/zh/model-studio/qwen-tts-realtime
  13. // 支持流式输入,适合长文本
  14. class AliyunRealtimeTtsProvider {
  15. apiKey;
  16. model;
  17. voice;
  18. baseUrl = 'wss://dashscope.aliyuncs.com/api-ws/v1/realtime';
  19. constructor() {
  20. this.apiKey = config_1.config.dashscope.apiKey;
  21. this.model = config_1.config.dashscope.realtimeModel;
  22. this.voice = config_1.config.dashscope.voice;
  23. }
  24. // 语音合成(WebSocket 流式)
  25. async synthesize(text, voiceId, params, outputPath) {
  26. return new Promise((resolve, reject) => {
  27. const voice = voiceId || this.voice;
  28. const audioChunks = [];
  29. let sessionId;
  30. console.log('🔊 [Qwen Realtime TTS] 建立 WebSocket 连接...');
  31. const ws = new ws_1.default(this.baseUrl, {
  32. headers: {
  33. 'Authorization': `Bearer ${this.apiKey}`,
  34. 'X-DashScope-App': 'audio-tts',
  35. },
  36. });
  37. ws.on('open', () => {
  38. console.log('✅ [Qwen Realtime TTS] 连接已建立');
  39. });
  40. ws.on('message', (data) => {
  41. try {
  42. const message = JSON.parse(data.toString());
  43. switch (message.event) {
  44. case 'session.configed':
  45. // 配置成功,发送文本
  46. sessionId = message.session_id;
  47. console.log('📝 [Qwen Realtime TTS] Session ID:', sessionId);
  48. // 构建指令
  49. const instructions = [];
  50. if (params.speed !== 1) {
  51. const speedDesc = params.speed > 1 ? '语速较快' : '语速较慢';
  52. instructions.push(speedDesc);
  53. }
  54. if (params.pitch !== 0) {
  55. const pitchDesc = params.pitch > 0 ? '音调较高' : '音调较低';
  56. instructions.push(pitchDesc);
  57. }
  58. // 使用 server_commit 模式:直接发送完整文本,服务端自动处理分段
  59. const textMessage = {
  60. event: 'input_text_buffer.append',
  61. context: {
  62. text: text,
  63. },
  64. };
  65. ws.send(JSON.stringify(textMessage));
  66. console.log('📤 [Qwen Realtime TTS] 文本已发送,长度:', text.length);
  67. // 发送 commit 触发合成
  68. const commitMessage = {
  69. event: 'input_text_buffer.commit',
  70. };
  71. ws.send(JSON.stringify(commitMessage));
  72. console.log('📤 [Qwen Realtime TTS] 触发语音合成...');
  73. break;
  74. case 'audio.stream':
  75. // 接收音频数据
  76. if (message.data) {
  77. const audioBuffer = Buffer.from(message.data, 'base64');
  78. audioChunks.push(audioBuffer);
  79. }
  80. break;
  81. case 'audio.finish':
  82. // 合成完成
  83. console.log('✅ [Qwen Realtime TTS] 合成完成');
  84. // 保存文件
  85. if (audioChunks.length > 0) {
  86. const dir = path_1.default.dirname(outputPath);
  87. if (!fs_1.default.existsSync(dir)) {
  88. fs_1.default.mkdirSync(dir, { recursive: true });
  89. }
  90. const finalPath = outputPath.endsWith('.mp3') ? outputPath : outputPath.replace(/\.[^.]+$/, '.mp3');
  91. const finalBuffer = Buffer.concat(audioChunks);
  92. fs_1.default.writeFileSync(finalPath, finalBuffer);
  93. console.log(`✅ [Qwen Realtime TTS] 文件已保存: ${finalPath}, 大小: ${finalBuffer.length} bytes`);
  94. ws.close();
  95. resolve(finalPath);
  96. }
  97. else {
  98. ws.close();
  99. reject(new Error('Qwen Realtime TTS 未返回音频数据'));
  100. }
  101. break;
  102. case 'task.finished':
  103. // 任务完成
  104. ws.close();
  105. resolve(outputPath);
  106. break;
  107. case 'error':
  108. console.error('❌ [Qwen Realtime TTS] 错误:', message.message);
  109. ws.close();
  110. reject(new Error(`Qwen Realtime TTS 错误: ${message.message}`));
  111. break;
  112. default:
  113. // 忽略其他消息
  114. break;
  115. }
  116. }
  117. catch (err) {
  118. console.error('❌ [Qwen Realtime TTS] 解析消息失败:', err);
  119. }
  120. });
  121. ws.on('error', (err) => {
  122. console.error('❌ [Qwen Realtime TTS] WebSocket 错误:', err.message);
  123. reject(err);
  124. });
  125. ws.on('close', () => {
  126. console.log('🔌 [Qwen Realtime TTS] 连接已关闭');
  127. });
  128. // 超时处理
  129. setTimeout(() => {
  130. if (ws.readyState === ws_1.default.OPEN) {
  131. ws.close();
  132. reject(new Error('Qwen Realtime TTS 请求超时'));
  133. }
  134. }, 120000); // 2分钟超时
  135. });
  136. }
  137. }
  138. exports.AliyunRealtimeTtsProvider = AliyunRealtimeTtsProvider;