| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- import axios from 'axios';
- import fs from 'fs';
- import path from 'path';
- import { config } from '../../config';
- import { VoiceParams } from '../../types';
- /**
- * 阿里云百炼 Qwen-TTS Provider(同步模式)
- *
- * 接口限制参考:
- * - Qwen-TTS (multimodal-generation): 单次 20,000 字符(同步/流式)
- * - CosyVoice (SpeechSynthesizer): 单次 20,000 字符(同步/流式)
- * - 传统长文本合成: 80,000 字符(建议 40,000 以内)
- * - 传统基础合成: 300 字符(已弃用)
- *
- * 调用方式:
- * - 同步非流式: POST → 直接返回音频 URL
- * - SSE 流式: 加 X-DashScope-SSE: enable header
- * - ⚠️ 不支持 X-DashScope-Async 异步模式
- *
- * 当前策略: 使用同步模式 + 1000 字符分段,每段快速返回音频 URL
- *
- * 文档: https://help.aliyun.com/zh/model-studio/qwen-tts
- */
- export class AliyunTtsProvider {
- private apiKey: string;
- private model: string;
- private voice: string;
- private baseUrl = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation';
- constructor() {
- this.apiKey = config.dashscope.apiKey;
- this.model = config.dashscope.model;
- this.voice = config.dashscope.voice;
- }
- /**
- * 语音合成(同步模式:直接返回音频 URL → 下载)
- */
- async synthesize(
- text: string,
- voiceId: string,
- params: VoiceParams,
- outputPath: string,
- retries: number = 3,
- modelOverride?: string,
- ): Promise<string> {
- const activeModel = modelOverride || this.model;
- let lastError: Error | null = null;
- for (let attempt = 1; attempt <= retries; attempt++) {
- try {
- // 构建请求体
- const requestBody: any = {
- model: activeModel,
- input: {
- text: text,
- voice: voiceId || this.voice,
- language_type: 'Chinese',
- },
- };
- // 使用 instruct 模型时支持指令控制
- if (activeModel.includes('instruct')) {
- const instructions: string[] = [];
- if (params.speed !== 1) {
- const speedDesc = params.speed > 1 ? '较快' : '较慢';
- instructions.push(`语速${speedDesc}`);
- }
- if (params.pitch !== 0) {
- const pitchDesc = params.pitch > 0 ? '较高' : '较低';
- instructions.push(`音调${pitchDesc}`);
- }
- if (instructions.length > 0) {
- requestBody.input.instructions = instructions.join(',');
- }
- }
- console.log(`📤 [Aliyun TTS] 尝试 ${attempt}/${retries}, model: ${activeModel}, voice: ${voiceId || this.voice}, text length: ${text.length}`);
- // 同步调用(不加 X-DashScope-Async,Qwen-TTS 不支持异步模式)
- const response = await axios.post(this.baseUrl, requestBody, {
- headers: {
- 'Authorization': `Bearer ${this.apiKey}`,
- 'Content-Type': 'application/json',
- },
- timeout: 60000,
- });
- // 检查响应
- if (response.status !== 200) {
- throw new Error(`Aliyun TTS 请求失败: HTTP ${response.status}`);
- }
- const data = response.data;
- if (data.code) {
- throw new Error(`Aliyun TTS 错误: ${data.message || JSON.stringify(data)}`);
- }
- // 获取音频 URL(同步模式直接在 output.audio.url 中返回)
- const audioUrl = data.output?.audio?.url;
- if (!audioUrl) {
- throw new Error(`Aliyun TTS 未返回音频 URL: ${JSON.stringify(data).substring(0, 200)}`);
- }
- console.log(`🔗 [Aliyun TTS] 获取音频 URL: ${audioUrl.substring(0, 80)}...`);
- // 下载音频文件
- try {
- return await this.downloadAudio(audioUrl, outputPath);
- } catch (downloadError: any) {
- console.warn(`⚠️ [Aliyun TTS] 音频下载失败: ${downloadError.message}`);
- // 下载失败,返回 cloud: URL 标记(后续会处理)
- return `cloud:${audioUrl}`;
- }
- } catch (error: any) {
- const errorDetails = error.response?.data || error.message;
- const isRateLimit = error.response?.status === 429 || errorDetails?.code === 'Throttling.RateQuota';
- const isServerError = error.response?.status >= 500;
- console.error(`❌ [Aliyun TTS] 失败 (尝试 ${attempt}/${retries}):`, error.message);
- if ((isRateLimit || isServerError) && attempt < retries) {
- const waitTime = Math.pow(2, attempt) * 1000;
- console.warn(`⏳ 等待 ${waitTime}ms 后重试...`);
- await new Promise(resolve => setTimeout(resolve, waitTime));
- lastError = new Error(`Aliyun TTS 临时错误: ${error.message}`);
- continue;
- }
- lastError = new Error(`Aliyun TTS 调用失败: ${error.message}`);
- }
- }
- throw lastError || new Error('Aliyun TTS 服务调用失败');
- }
- /**
- * 下载音频文件到本地
- */
- private async downloadAudio(audioUrl: string, outputPath: string): Promise<string> {
- console.log(`⬇️ [Aliyun TTS] 下载音频: ${audioUrl.substring(0, 80)}...`);
- const response = await axios.get(audioUrl, {
- responseType: 'arraybuffer',
- timeout: 120000,
- });
- const dir = path.dirname(outputPath);
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir, { recursive: true });
- }
- // 确保输出路径以 .wav 结尾(Qwen-TTS 返回 wav 格式)
- const finalPath = outputPath.endsWith('.wav') ? outputPath : outputPath.replace(/\.[^.]+$/, '.wav');
- fs.writeFileSync(finalPath, response.data);
- const stats = fs.statSync(finalPath);
- console.log(`✅ [Aliyun TTS] 下载完成: ${finalPath} (${stats.size} bytes)`);
- return finalPath;
- }
- }
|