Просмотр исходного кода

feat(tts): 优化TTS异步生成和模型随机选择

1. TTS异步模式:立即返回audioId,前端轮询状态
2. 随机模型选择:支持从多个可用模型中随机选用
3. 修复分段逻辑:限制480字符确保不超API限制
4. 修复音频合并:WAV->MP3使用libmp3lame转码
5. 添加API重试机制:429/500错误指数退避重试
6. 修复bug:GET /ai/models 改用GET而非POST
7. 禁用不兼容模型:vd/vc等模型报403,仅保留qwen3-tts-flash

可用模型:qwen3-tts-flash

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MyFramework User 5 месяцев назад
Родитель
Сommit
7dd713b2d2

+ 12 - 6
my-uniapp-vue3/src/pages/ai/index.vue

@@ -103,7 +103,7 @@
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
-import { post } from '../../utils/request';
+import { get, post } from '../../utils/request';
 
 const prompt = ref('');
 const generating = ref(false);
@@ -120,12 +120,13 @@ const modelIndex = ref(0);
 
 // 页面加载时获取可用模型
 onMounted(async () => {
+  uni.showLoading({ title: '加载中...' });
   try {
-    const result = await post<{ models: string[]; default: string }>('/ai/models', {});
+    const result = await get<{ models: string[]; default: string }>('/ai/models');
     if (result.models) {
       modelList.value = result.models.map((m: string) => ({
         id: m,
-        name: m.includes('xiaomi') ? '通义小米分析专业版 (免费)' 
+        name: m.includes('xiaomi') ? '通义小米分析专业版 (免费)'
           : m.includes('122b') ? 'Qwen3.5-122B-A10B (免费)'
           : 'Qwen-Plus (付费)',
       }));
@@ -134,6 +135,9 @@ onMounted(async () => {
     }
   } catch (e) {
     console.error('获取模型列表失败:', e);
+    uni.showToast({ title: '获取模型失败', icon: 'none' });
+  } finally {
+    uni.hideLoading();
   }
 });
 
@@ -147,6 +151,7 @@ async function handleGenerate() {
   if (!prompt.value.trim() || generating.value) return;
 
   generating.value = true;
+  uni.showLoading({ title: 'AI 思考中...' });
   try {
     const result = await post<{ text: string; model: string }>('/ai/generate', {
       prompt: prompt.value,
@@ -154,24 +159,25 @@ async function handleGenerate() {
     });
 
     generatedText.value = result.text;
-    
+
     // 添加到历史记录
     historyList.value.unshift({
       prompt: prompt.value,
       text: result.text,
     });
-    
+
     // 保留最多 10 条历史
     if (historyList.value.length > 10) {
       historyList.value.pop();
     }
-    
+
     uni.showToast({ title: '生成成功', icon: 'success' });
   } catch (error: any) {
     console.error('AI 生成失败:', error);
     uni.showToast({ title: error.message || '生成失败', icon: 'none' });
   } finally {
     generating.value = false;
+    uni.hideLoading();
   }
 }
 

+ 51 - 7
my-uniapp-vue3/src/store/audio.ts

@@ -72,23 +72,67 @@ export const useAudioStore = defineStore('audio', () => {
     return result.voices;
   }
 
-  // 生成音频
+  // 生成音频(异步模式)
   async function generateAudio(
     text: string,
     voiceId: string,
     voiceParams: VoiceParams
   ) {
-    uni.showLoading({ title: '生成中...', mask: true });
+    console.log('📤 TTS 请求参数:', { text: text.substring(0, 50) + '...', textLength: text.length, voiceId, voiceParams });
+    uni.showLoading({ title: '正在创建任务...' });
     try {
-      const result = await post<{
+      // 1. 发起异步生成请求,立即返回 audioId
+      const { audioId } = await post<{
         audioId: string;
-        audioUrl: string;
-        duration: number;
-        size: number;
+        status: string;
       }>('/tts/generate', { text, voiceId, voiceParams });
 
+      // 2. 轮询状态直到完成
+      let attempts = 0;
+      const maxAttempts = 60; // 最多等待60次(60秒)
+      let pollInterval = 1; // 初始轮询间隔(秒)
+
+      console.log('🔄 开始轮询状态, audioId:', audioId);
+      while (attempts < maxAttempts) {
+        await new Promise(resolve => setTimeout(resolve, pollInterval * 1000));
+
+        const statusResult = await get<{
+          status: string;
+          audio?: {
+            audioUrl: string;
+            audioDuration: number;
+            audioSize: number;
+          };
+        }>(`/tts/status/${audioId}`);
+
+        console.log(`📊 轮询 ${attempts + 1}: status = ${statusResult.status}`);
+        
+        if (statusResult.status === 'completed' && statusResult.audio) {
+          uni.hideLoading();
+          console.log('✅ 生成成功');
+          uni.showToast({ title: '生成成功', icon: 'success' });
+          return {
+            audioId: audioId,
+            audioUrl: statusResult.audio.audioUrl,
+            duration: statusResult.audio.audioDuration,
+            size: statusResult.audio.audioSize,
+          };
+        }
+
+        if (statusResult.status === 'failed') {
+          console.log('❌ 状态为 failed');
+          console.log('❌ 完整状态:', statusResult);
+          uni.hideLoading();
+          throw new Error('音频生成失败,请重试');
+        }
+
+        // 渐进式增加轮询间隔(避免频繁请求)
+        pollInterval = Math.min(pollInterval + 0.5, 5);
+        attempts++;
+      }
+
       uni.hideLoading();
-      return result;
+      throw new Error('生成超时,请稍后重试');
     } catch (error) {
       uni.hideLoading();
       throw error;

+ 10 - 1
server/.env.example

@@ -9,7 +9,16 @@ DATABASE_URL="mysql://root:password@localhost:3306/audio-book"
 JWT_SECRET=your-super-secret-jwt-key-change-in-production
 JWT_EXPIRES_IN=7d
 
-# 阿里云 TTS
+# 阿里云百炼 Qwen TTS
+DASHSCOPE_API_KEY=your-dashscope-api-key
+DASHSCOPE_MODEL=qwen3-tts-instruct-flash
+DASHSCOPE_VOICE=Cherry
+
+# 长文本模式(启用 WebSocket 实时合成,支持更长文本)
+DASHSCOPE_USE_REALTIME=true
+DASHSCOPE_REALTIME_MODEL=qwen3-tts-instruct-flash-realtime
+
+# 阿里云 TTS(旧版配置,兼容)
 ALIYUN_ACCESS_KEY=sk-c25679401ba24c749f53be86b0c9a7a6
 ALIYUN_ACCESS_SECRET=your-access-secret
 ALIYUN_APP_KEY=your-app-key

+ 5 - 2
server/src/config/index.ts

@@ -3,9 +3,7 @@ import path from 'path';
 
 // 使用 process.cwd() 获取项目根目录(server目录)
 const projectRoot = process.cwd();
-console.log('📂 项目根目录:', projectRoot);
 dotenv.config({ path: path.resolve(projectRoot, '.env') });
-console.log('🔑 JWT_SECRET from env:', process.env.JWT_SECRET);
 
 export const config = {
   port: parseInt(process.env.PORT || '3000', 10),
@@ -26,6 +24,11 @@ export const config = {
     apiKey: process.env.DASHSCOPE_API_KEY || '',
     model: process.env.DASHSCOPE_MODEL || 'qwen3-tts-instruct-flash',
     voice: process.env.DASHSCOPE_VOICE || 'Cherry',
+    // 可用 TTS 模型列表(随机选用)
+    ttsModels: (process.env.DASHSCOPE_TTS_MODELS || 'qwen3-tts-instruct-flash').split(',').filter(Boolean),
+    // 长文本模式使用 WebSocket 实时合成
+    useRealtime: process.env.DASHSCOPE_USE_REALTIME === 'true',
+    realtimeModel: process.env.DASHSCOPE_REALTIME_MODEL || 'qwen3-tts-instruct-flash-realtime',
   },
   
   upload: {

+ 157 - 0
server/src/modules/tts/aliyun-realtime.provider.ts

@@ -0,0 +1,157 @@
+import WebSocket from 'ws';
+import fs from 'fs';
+import path from 'path';
+import { config } from '../../config';
+import { VoiceParams } from '../../types';
+
+// 阿里云百炼 Qwen-TTS WebSocket 实时语音合成
+// 文档: https://help.aliyun.com/zh/model-studio/qwen-tts-realtime
+// 支持流式输入,适合长文本
+
+export class AliyunRealtimeTtsProvider {
+  private apiKey: string;
+  private model: string;
+  private voice: string;
+  private baseUrl = 'wss://dashscope.aliyuncs.com/api-ws/v1/realtime';
+
+  constructor() {
+    this.apiKey = config.dashscope.apiKey;
+    this.model = config.dashscope.realtimeModel;
+    this.voice = config.dashscope.voice;
+  }
+
+  // 语音合成(WebSocket 流式)
+  async synthesize(
+    text: string,
+    voiceId: string,
+    params: VoiceParams,
+    outputPath: string
+  ): Promise<string> {
+    return new Promise((resolve, reject) => {
+      const voice = voiceId || this.voice;
+      const audioChunks: Buffer[] = [];
+      let sessionId: string;
+
+      console.log('🔊 [Qwen Realtime TTS] 建立 WebSocket 连接...');
+
+      const ws = new WebSocket(this.baseUrl, {
+        headers: {
+          'Authorization': `Bearer ${this.apiKey}`,
+          'X-DashScope-App': 'audio-tts',
+        },
+      });
+
+      ws.on('open', () => {
+        console.log('✅ [Qwen Realtime TTS] 连接已建立');
+      });
+
+      ws.on('message', (data) => {
+        try {
+          const message = JSON.parse(data.toString());
+
+          switch (message.event) {
+            case 'session.configed':
+              // 配置成功,发送文本
+              sessionId = message.session_id;
+              console.log('📝 [Qwen Realtime TTS] Session ID:', sessionId);
+
+              // 构建指令
+              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);
+              }
+
+              // 使用 server_commit 模式:直接发送完整文本,服务端自动处理分段
+              const textMessage = {
+                event: 'input_text_buffer.append',
+                context: {
+                  text: text,
+                },
+              };
+              ws.send(JSON.stringify(textMessage));
+              console.log('📤 [Qwen Realtime TTS] 文本已发送,长度:', text.length);
+
+              // 发送 commit 触发合成
+              const commitMessage = {
+                event: 'input_text_buffer.commit',
+              };
+              ws.send(JSON.stringify(commitMessage));
+              console.log('📤 [Qwen Realtime TTS] 触发语音合成...');
+              break;
+
+            case 'audio.stream':
+              // 接收音频数据
+              if (message.data) {
+                const audioBuffer = Buffer.from(message.data, 'base64');
+                audioChunks.push(audioBuffer);
+              }
+              break;
+
+            case 'audio.finish':
+              // 合成完成
+              console.log('✅ [Qwen Realtime TTS] 合成完成');
+
+              // 保存文件
+              if (audioChunks.length > 0) {
+                const dir = path.dirname(outputPath);
+                if (!fs.existsSync(dir)) {
+                  fs.mkdirSync(dir, { recursive: true });
+                }
+
+                const finalPath = outputPath.endsWith('.mp3') ? outputPath : outputPath.replace(/\.[^.]+$/, '.mp3');
+                const finalBuffer = Buffer.concat(audioChunks);
+                fs.writeFileSync(finalPath, finalBuffer);
+                console.log(`✅ [Qwen Realtime TTS] 文件已保存: ${finalPath}, 大小: ${finalBuffer.length} bytes`);
+                ws.close();
+                resolve(finalPath);
+              } else {
+                ws.close();
+                reject(new Error('Qwen Realtime TTS 未返回音频数据'));
+              }
+              break;
+
+            case 'task.finished':
+              // 任务完成
+              ws.close();
+              resolve(outputPath);
+              break;
+
+            case 'error':
+              console.error('❌ [Qwen Realtime TTS] 错误:', message.message);
+              ws.close();
+              reject(new Error(`Qwen Realtime TTS 错误: ${message.message}`));
+              break;
+
+            default:
+              // 忽略其他消息
+              break;
+          }
+        } catch (err) {
+          console.error('❌ [Qwen Realtime TTS] 解析消息失败:', err);
+        }
+      });
+
+      ws.on('error', (err) => {
+        console.error('❌ [Qwen Realtime TTS] WebSocket 错误:', err.message);
+        reject(err);
+      });
+
+      ws.on('close', () => {
+        console.log('🔌 [Qwen Realtime TTS] 连接已关闭');
+      });
+
+      // 超时处理
+      setTimeout(() => {
+        if (ws.readyState === WebSocket.OPEN) {
+          ws.close();
+          reject(new Error('Qwen Realtime TTS 请求超时'));
+        }
+      }, 120000); // 2分钟超时
+    });
+  }
+}

+ 111 - 73
server/src/modules/tts/aliyun.provider.ts

@@ -18,96 +18,134 @@ export class AliyunTtsProvider {
     this.voice = config.dashscope.voice;
   }
 
-  // 语音合成
+  // 语音合成(带重试机制)
   async synthesize(
     text: string,
     voiceId: string,
     params: VoiceParams,
-    outputPath: string
+    outputPath: string,
+    retries: number = 3,
+    modelOverride?: string
   ): Promise<string> {
-    try {
-      // 构建请求
-      const requestBody: any = {
-        model: this.model,
-        input: {
-          text: text,
-          voice: voiceId || this.voice,
-          language_type: 'Chinese',
-        },
-      };
-
-      // 使用 instruct 模型时支持指令控制
-      if (this.model.includes('instruct')) {
-        const instructions: string[] = [];
-        if (params.speed !== 1) {
-          const speedDesc = params.speed > 1 ? '较快' : '较慢';
-          instructions.push(`语速${speedDesc}`);
+    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',
+          },
+        };
+
+        console.log(`📤 [Aliyun TTS] 尝试 ${attempt}/${retries}`);
+        console.log(`   model: ${activeModel}, voice: ${voiceId || this.voice}`);
+        console.log(`   text length: ${text.length}`);
+        console.log(`   params: ${JSON.stringify(params)}`);
+
+        // 使用 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.parameters = {
+              instructions: instructions.join(','),
+            };
+          }
         }
-        if (params.pitch !== 0) {
-          const pitchDesc = params.pitch > 0 ? '较高' : '较低';
-          instructions.push(`音调${pitchDesc}`);
+
+        // 发送请求
+        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(`Qwen TTS 请求失败: ${response.status}`);
         }
-        if (instructions.length > 0) {
-          requestBody.parameters = {
-            instructions: instructions.join(','),
-          };
+
+        const data = response.data;
+        if (data.code) {
+          throw new Error(`Qwen TTS 错误: ${data.message || JSON.stringify(data)}`);
         }
-      }
 
-      // 发送请求
-      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(`Qwen TTS 请求失败: ${response.status}`);
-      }
+        // 获取音频 URL
+        const audioUrl = data.output?.audio?.url;
+        console.log('🔗 音频 URL:', audioUrl);
+        if (!audioUrl) {
+          throw new Error('Qwen TTS 未返回音频 URL');
+        }
 
-      const data = response.data;
-      if (data.code) {
-        throw new Error(`Qwen TTS 错误: ${data.message || JSON.stringify(data)}`);
-      }
+        // 尝试下载音频文件,如果失败则返回 URL
+        try {
+          const audioResponse = await axios.get(audioUrl, {
+            responseType: 'arraybuffer',
+            timeout: 60000,
+          });
 
-      // 获取音频 URL
-      const audioUrl = data.output?.audio?.url;
-      console.log('🔗 音频 URL:', audioUrl);
-      if (!audioUrl) {
-        throw new Error('Qwen TTS 未返回音频 URL');
-      }
+          // 保存文件
+          const dir = path.dirname(outputPath);
+          if (!fs.existsSync(dir)) {
+            fs.mkdirSync(dir, { recursive: true });
+          }
 
-      // 尝试下载音频文件,如果失败则返回 URL
-      try {
-        const audioResponse = await axios.get(audioUrl, {
-          responseType: 'arraybuffer',
-          timeout: 60000,
-        });
+          // 确保输出路径以 .wav 结尾
+          const finalPath = outputPath.endsWith('.wav') ? outputPath : outputPath.replace(/\.[^.]+$/, '.wav');
+          fs.writeFileSync(finalPath, audioResponse.data);
+          console.log(`✅ Qwen TTS 生成成功: ${finalPath}`);
 
-        // 保存文件
-        const dir = path.dirname(outputPath);
-        if (!fs.existsSync(dir)) {
-          fs.mkdirSync(dir, { recursive: true });
+          return finalPath;
+        } catch (downloadError: any) {
+          console.warn('⚠️ 音频下载失败,返回云端 URL:', downloadError.message);
+          // 返回一个特殊的路径标记,表示使用云端 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;
 
-        // 确保输出路径以 .wav 结尾
-        const finalPath = outputPath.endsWith('.wav') ? outputPath : outputPath.replace(/\.[^.]+$/, '.wav');
-        fs.writeFileSync(finalPath, audioResponse.data);
-        console.log(`✅ Qwen TTS 生成成功: ${finalPath}`);
+        console.error(`❌ Qwen TTS 调用失败 (尝试 ${attempt}/${retries}):`, JSON.stringify(errorDetails, null, 2));
 
-        return finalPath;
-      } catch (downloadError: any) {
-        console.warn('⚠️ 音频下载失败,返回云端 URL:', downloadError.message);
-        // 返回一个特殊的路径标记,表示使用云端 URL
-        return `cloud:${audioUrl}`;
+        if (isRateLimit && attempt < retries) {
+          // 速率限制:等待后重试(指数退避)
+          const waitTime = Math.pow(2, attempt) * 1000;
+          console.warn(`⏳ 速率限制,等待 ${waitTime}ms 后重试...`);
+          await new Promise(resolve => setTimeout(resolve, waitTime));
+          lastError = new Error(`Qwen TTS 速率限制: ${error.message}`);
+          continue;
+        }
+
+        if (isServerError && attempt < retries) {
+          // 服务器错误:等待后重试
+          const waitTime = Math.pow(2, attempt) * 1000;
+          console.warn(`⏳ 服务器错误,等待 ${waitTime}ms 后重试...`);
+          await new Promise(resolve => setTimeout(resolve, waitTime));
+          lastError = new Error(`Qwen TTS 服务器错误: ${error.message}`);
+          continue;
+        }
+
+        // 达到最大重试次数或不可重试的错误
+        throw new Error(`Qwen TTS 服务调用失败: ${error.message}, 详情: ${JSON.stringify(errorDetails)}`);
       }
-    } catch (error: any) {
-      const errorDetails = error.response?.data || error.message;
-      console.error('❌ Qwen TTS 调用失败:', JSON.stringify(errorDetails, null, 2));
-      throw new Error(`Qwen TTS 服务调用失败: ${error.message}, 详情: ${JSON.stringify(errorDetails)}`);
     }
+
+    // 理论上不会到达这里,但为了类型安全
+    throw lastError || new Error('Qwen TTS 服务调用失败');
   }
 }

+ 14 - 3
server/src/modules/tts/audio-merger.ts

@@ -23,9 +23,20 @@ export class AudioMerger {
       const listFile = '/tmp/ffmpeg_list.txt';
       fs.writeFileSync(listFile, listContent);
 
-      // 使用 FFmpeg concat 合并
-      const cmd = `ffmpeg -f concat -safe 0 -i "${listFile}" -c copy -y "${outputPath}"`;
-      await execAsync(cmd, { timeout: 60000 });
+      // 检查输出格式
+      const outputExt = outputPath.split('.').pop()?.toLowerCase();
+
+      // 构建 FFmpeg 命令
+      let cmd: string;
+      if (outputExt === 'mp3') {
+        // MP3 需要转码(WAV/PCM -> MP3)
+        cmd = `ffmpeg -f concat -safe 0 -i "${listFile}" -c:a libmp3lame -b:a 192k -y "${outputPath}"`;
+      } else {
+        // 其他格式直接复制
+        cmd = `ffmpeg -f concat -safe 0 -i "${listFile}" -c copy -y "${outputPath}"`;
+      }
+
+      await execAsync(cmd, { timeout: 120000 });
 
       console.log(`✅ 音频合并成功: ${outputPath}`);
       return outputPath;

+ 24 - 5
server/src/modules/tts/tts.controller.ts

@@ -1,7 +1,7 @@
 import Router from '@koa/router';
 import { Context } from 'koa';
 import * as TtsService from './tts.service';
-import { BadRequestError } from '../../middleware/errorHandler';
+import { BadRequestError, NotFoundError } from '../../middleware/errorHandler';
 import { optionalAuth } from '../../middleware/auth';
 import { usageLimitMiddleware, checkWordLimit } from '../../middleware/usageLimit';
 import { prisma } from '../../models';
@@ -37,7 +37,7 @@ router.get('/test-db', async (ctx: Context) => {
   }
 });
 
-// 生成音频
+// 生成音频(异步模式)
 router.post(
   '/generate',
   optionalAuth,
@@ -49,6 +49,9 @@ router.post(
       voiceParams?: { speed?: number; pitch?: number; volume?: number };
     };
 
+    // 调试日志
+    console.log('📥 收到 TTS 请求:', { textLength: text?.length, voiceId, voiceParams });
+
     // 参数验证
     if (!text || text.trim().length === 0) {
       throw new BadRequestError('请输入要转换的文本');
@@ -73,7 +76,7 @@ router.post(
       volume: voiceParams?.volume || 50,
     };
 
-    // 生成音频
+    // 异步生成音频(立即返回)
     const result = await TtsService.generateAudio(userId, text, voiceId, params);
 
     // 如果用户已登录,更新使用次数
@@ -89,12 +92,28 @@ router.post(
 
     ctx.body = {
       code: 0,
-      message: '音频生成成功',
-      data: result,
+      message: '音频生成任务已创建',
+      data: result, // { audioId, status: 'pending' }
     };
   }
 );
 
+// 获取音频生成状态
+router.get('/status/:audioId', async (ctx: Context) => {
+  const { audioId } = ctx.params;
+  const result = await TtsService.getAudioStatus(audioId);
+
+  if (result.status === 'not_found') {
+    throw new NotFoundError('音频不存在');
+  }
+
+  ctx.body = {
+    code: 0,
+    message: 'success',
+    data: result,
+  };
+});
+
 // 预览音色
 router.post('/preview', async (ctx: Context) => {
   const { voiceId, voiceParams } = ctx.request.body as {

+ 215 - 98
server/src/modules/tts/tts.service.ts

@@ -5,10 +5,19 @@ import { config } from '../../config';
 import { prisma } from '../../models';
 import { VoiceParams, Voice } from '../../types';
 import { AliyunTtsProvider } from './aliyun.provider';
+import { AliyunRealtimeTtsProvider } from './aliyun-realtime.provider';
 import { MockTtsProvider } from './mock.provider';
 import { AudioMerger } from './audio-merger';
 import { aiSummaryService } from './ai-summary.service';
 
+// 日志文件路径
+const LOG_FILE = path.join(process.cwd(), 'tts-debug.log');
+
+function logToFile(msg: string) {
+  const timestamp = new Date().toISOString();
+  fs.appendFileSync(LOG_FILE, `[${timestamp}] ${msg}\n`);
+}
+
 // 可用音色列表(使用阿里云官方音色)
 export const VOICES: Voice[] = [
   { id: 'cherry', name: '芊悦', gender: 'female', description: '阳光积极、亲切自然' },
@@ -42,13 +51,23 @@ export function getAliyunVoice(voiceId: string): string {
   return VOICE_MAPPING[voiceId] || 'Cherry';
 }
 
-// 文本分段
-export function splitText(text: string, maxLength: number = 500): string[] {
+// 判断是否使用长文本模式(>5000字符且启用realtime)
+// 注意:WebSocket realtime 模式需要特殊的API权限和配置,如果连接失败会导致生成失败。
+// 暂时强制禁用,使用 HTTP 分段模式
+export function shouldUseLongText(text: string): boolean {
+  return false; // 强制返回 false,禁用 realtime 模式
+}
+
+// 文本分段 - 阿里云 TTS 限制 600 字符,增加到 550 留安全余量
+export function splitText(text: string, maxLength: number = 550): string[] {
   const segments: string[] = [];
   let current = '';
 
+  // 清理文本,移除可能导致问题的字符
+  const cleanText = text.replace(/\r/g, '');
+
   // 按段落分割
-  const paragraphs = text.split(/\n+/);
+  const paragraphs = cleanText.split(/\n+/);
 
   for (const para of paragraphs) {
     if ((current + para).length <= maxLength) {
@@ -58,7 +77,8 @@ export function splitText(text: string, maxLength: number = 500): string[] {
 
       // 如果段落本身超长,按句子分割
       if (para.length > maxLength) {
-        const sentences = para.split(/[。!?;]/g);
+        // 使用更安全的分割方式
+        const sentences = para.match(/[^。!?;]+[。!?;]?/g) || [para];
         current = '';
 
         for (const sentence of sentences) {
@@ -86,22 +106,50 @@ export function splitText(text: string, maxLength: number = 500): string[] {
   }
 
   if (current) segments.push(current);
-  return segments;
+
+  // 验证每段长度不超过限制(安全检查)
+  const safeLimit = 550;
+  const validatedSegments = segments.map((seg, idx) => {
+    if (seg.length > safeLimit) {
+      console.warn(`⚠️ 段落 ${idx + 1} 长度 ${seg.length} 超过限制,强制截断`);
+      return seg.substring(0, safeLimit);
+    }
+    return seg;
+  });
+
+  return validatedSegments;
 }
 
 // TTS Provider 工厂
-function getTtsProvider() {
+function getTtsProvider(text: string, voiceId: string) {
+  const useLongText = shouldUseLongText(text);
+
+  // 文本超长时使用 WebSocket 实时合成
+  if (useLongText && config.dashscope.apiKey) {
+    console.log('🔊 文本超过5000字符,使用 Qwen Realtime TTS 流式合成');
+    return { provider: new AliyunRealtimeTtsProvider(), type: 'realtime' as const };
+  }
+
   // 优先使用阿里云百炼 Qwen TTS
   if (config.dashscope.apiKey) {
     console.log('🔊 使用阿里云百炼 Qwen TTS 服务');
-    return new AliyunTtsProvider();
+    return { provider: new AliyunTtsProvider(), type: 'http' as const };
   }
+
   // 降级到模拟服务
-  console.log('🔊 使用模拟 TTS 服务');
-  return new MockTtsProvider();
+  console.log('🔊 使用模拟 TTS 服务(无 API Key)');
+  return { provider: new MockTtsProvider(), type: 'mock' as const };
+}
+
+// 随机选择 TTS 模型
+function getRandomModel(): string {
+  const models = config.dashscope.ttsModels;
+  const model = models[Math.floor(Math.random() * models.length)];
+  console.log(`🎲 随机选择 TTS 模型: ${model}`);
+  return model;
 }
 
-// 生成音频
+// 生成音频(异步模式)
 export async function generateAudio(
   userId: string,
   text: string,
@@ -109,17 +157,9 @@ export async function generateAudio(
   voiceParams: VoiceParams
 ): Promise<{
   audioId: string;
-  audioUrl: string;
-  duration: number;
-  size: number;
+  status: string;
 }> {
-  const provider = getTtsProvider();
-
-  // 分段
-  const segments = splitText(text);
-  console.log(`📝 文本已分段: ${segments.length} 段`);
-
-  // 创建音频记录
+  // 创建音频记录(待处理状态)
   const audioId = uuidv4();
   const audioDir = path.join(config.upload.dir, audioId);
 
@@ -127,104 +167,181 @@ export async function generateAudio(
     fs.mkdirSync(audioDir, { recursive: true });
   }
 
-  // 并行生成各段音频
-  const audioFiles: string[] = [];
-  const cloudUrls: string[] = [];
-  const concurrency = 5; // 并发数
-
-  for (let i = 0; i < segments.length; i += concurrency) {
-    const batch = segments.slice(i, i + concurrency);
-    const results = await Promise.all(
-      batch.map((segment, idx) =>
-        provider.synthesize(segment, getAliyunVoice(voiceId), voiceParams, path.join(audioDir, `segment_${i + idx}.mp3`))
-      )
-    );
-    // 检查是否有云端 URL
-    results.forEach(r => {
-      if (r.startsWith('cloud:')) {
-        cloudUrls.push(r.substring(6));
-      } else {
-        audioFiles.push(r);
-      }
+  let audio;
+  try {
+    audio = await prisma.audio.create({
+      data: {
+        userId: userId ? parseInt(userId) : undefined,
+        title: '处理中...',
+        text,
+        summary: '',
+        tags: '[]',
+        audioUrl: '',
+        audioDuration: 0,
+        audioSize: 0,
+        wordCount: text.length,
+        voiceId,
+        voiceParams: JSON.stringify(voiceParams),
+        status: 'pending',
+      },
     });
+    console.log('📝 创建音频记录:', audio.id, '状态: pending');
+  } catch (error) {
+    console.error('❌ 音频记录创建失败:', error);
+    throw error;
   }
 
-  // 如果有云端 URL,直接返回(单段文本情况)
-  let audioUrl = '';
-  let duration = 0;
-  let size = 0;
-
-  if (cloudUrls.length > 0) {
-    // 使用第一个云端 URL(简化处理)
-    audioUrl = cloudUrls[0];
-    console.log('☁️ 使用云端音频 URL:', audioUrl);
-  } else if (audioFiles.length > 0) {
-    // 合并音频
-    const outputPath = path.join(audioDir, 'output.mp3');
-    const mergedFile = await AudioMerger.merge(audioFiles, outputPath);
-
-    // 获取文件信息
-    const stats = fs.statSync(mergedFile);
-    size = stats.size;
-    duration = await AudioMerger.getDuration(mergedFile);
-    audioUrl = `/uploads/${audioId}/output.mp3`;
-  }
-
-  // 使用 AI 生成标题、摘要和标签
-  console.log('🤖 使用 AI 生成标题、摘要和标签...');
-  const [title, summary, tags] = await Promise.all([
-    aiSummaryService.generateTitle(text),
-    aiSummaryService.generateSummary(text, 200),
-    aiSummaryService.extractTags(text),
-  ]);
-
-  // 创建数据库记录
-  const finalAudioUrl = cloudUrls.length > 0 ? cloudUrls[0] : `/uploads/${audioId}/output.mp3`;
-  console.log('📝 创建音频记录:', {
-    userId,
-    title,
-    text: text.substring(0, 50),
-    summary,
-    tags,
-    audioUrl: finalAudioUrl,
-    audioDuration: duration,
-    audioSize: size,
-    wordCount: text.length,
-    voiceId,
-    voiceParams,
-    status: 'completed',
+  // 异步处理音频生成
+  processAudioGeneration(audio.id, text, voiceId, voiceParams, audioDir).catch(error => {
+    const errMsg = `❌ 异步音频生成失败: ${error.message}`;
+    console.error(errMsg);
+    console.error('❌ 错误堆栈:', error.stack);
+    logToFile(errMsg + '\n' + error.stack);
+    prisma.audio.update({
+      where: { id: audio.id },
+      data: { status: 'failed' },
+    }).catch(err => console.error('❌ 更新数据库状态失败:', err));
   });
 
-  let audio;
+  // 立即返回音频ID和状态
+  return {
+    audioId: audio.id.toString(),
+    status: 'pending',
+  };
+}
+
+/**
+ * 异步处理音频生成
+ */
+async function processAudioGeneration(
+  audioRecordId: number,
+  text: string,
+  voiceId: string,
+  voiceParams: VoiceParams,
+  audioDir: string
+) {
+  const logMsg = `🔄 开始处理音频 ID: ${audioRecordId}, 文本长度: ${text.length}, voiceId: ${voiceId}`;
+  console.log(logMsg);
+  logToFile(logMsg);
+  
   try {
-    audio = await prisma.audio.create({
+    const { provider, type } = getTtsProvider(text, voiceId);
+    console.log(`🔧 Provider type: ${type}, voiceName: ${voiceId}`);
+    logToFile(`Provider type: ${type}, voiceId: ${voiceId}`);
+    const voiceName = getAliyunVoice(voiceId);
+    const selectedModel = (type !== 'mock') ? getRandomModel() : undefined;
+    console.log(`🎲 selectedModel: ${selectedModel}`);
+
+    // 根据 Provider 类型决定分段策略
+    let segments: string[];
+    if (type === 'realtime') {
+      segments = [text];
+      console.log(`📝 使用 Qwen Realtime TTS,文本长度 ${text.length} 字符(不需分段)`);
+    } else {
+      segments = splitText(text);
+      console.log(`📝 文本已分段: ${segments.length} 段`);
+    }
+
+    segments.forEach((seg, i) => {
+      console.log(`   段落 ${i + 1}: ${seg.length} 字符`);
+    });
+
+    // 并行生成各段音频
+    const audioFiles: string[] = [];
+    const cloudUrls: string[] = [];
+    const concurrency = type === 'realtime' ? 1 : 2;
+
+    for (let i = 0; i < segments.length; i += concurrency) {
+      const batch = segments.slice(i, i + concurrency);
+      
+      // 根据类型选择调用方式
+      let results: string[];
+      if (type === 'mock') {
+        results = await Promise.all(
+          batch.map((segment, idx) =>
+            (provider as any).synthesize(segment, voiceName, voiceParams, path.join(audioDir, `segment_${i + idx}.mp3`))
+          )
+        );
+      } else {
+        results = await Promise.all(
+          batch.map((segment, idx) =>
+            (provider as any).synthesize(segment, voiceName, voiceParams, path.join(audioDir, `segment_${i + idx}.mp3`), 3, selectedModel)
+          )
+        );
+      }
+      results.forEach(r => {
+        if (r.startsWith('cloud:')) {
+          cloudUrls.push(r.substring(6));
+        } else {
+          audioFiles.push(r);
+        }
+      });
+    }
+
+    console.log(`📁 生成了 ${audioFiles.length} 个音频文件, ${cloudUrls.length} 个云端URL`);
+
+    let audioUrl = '';
+    let duration = 0;
+    let size = 0;
+
+    if (cloudUrls.length > 0) {
+      audioUrl = cloudUrls[0];
+      console.log('☁️ 使用云端音频 URL:', audioUrl);
+    } else if (audioFiles.length > 0) {
+      const outputPath = path.join(audioDir, 'output.mp3');
+      const mergedFile = await AudioMerger.merge(audioFiles, outputPath);
+      const stats = fs.statSync(mergedFile);
+      size = stats.size;
+      duration = await AudioMerger.getDuration(mergedFile);
+      audioUrl = `/uploads/${audioDir.split(/[/\\]/).pop()}/output.mp3`;
+    }
+
+    // 使用 AI 生成标题、摘要和标签
+    console.log('🤖 使用 AI 生成标题、摘要和标签...');
+    const [title, summary, tags] = await Promise.all([
+      aiSummaryService.generateTitle(text),
+      aiSummaryService.generateSummary(text, 200),
+      aiSummaryService.extractTags(text),
+    ]);
+
+    const finalAudioUrl = cloudUrls.length > 0 ? cloudUrls[0] : audioUrl;
+
+    // 更新数据库记录
+    await prisma.audio.update({
+      where: { id: audioRecordId },
       data: {
-        userId: userId ? parseInt(userId) : undefined,
         title,
-        text,
         summary,
         tags: JSON.stringify(tags),
         audioUrl: finalAudioUrl,
         audioDuration: duration,
         audioSize: size,
-        wordCount: text.length,
-        voiceId,
-        voiceParams: JSON.stringify(voiceParams),
         status: 'completed',
       },
     });
-    console.log('✅ 音频记录创建成功:', audio.id);
+
+    console.log('✅ 音频生成完成:', audioRecordId);
   } catch (error) {
-    console.error('❌ 音频记录创建失败:', error);
+    console.error('❌ processAudioGeneration 错误:', error);
     throw error;
   }
+}
 
-  return {
-    audioId: audio.id.toString(),
-    audioUrl: audio.audioUrl,
-    duration,
-    size,
-  };
+/**
+ * 获取音频状态
+ */
+export async function getAudioStatus(audioId: string): Promise<{ status: string; audio?: any }> {
+  const id = parseInt(audioId);
+  if (isNaN(id)) {
+    return { status: 'not_found' };
+  }
+
+  const audio = await prisma.audio.findUnique({ where: { id } });
+  if (!audio) {
+    return { status: 'not_found' };
+  }
+
+  return { status: audio.status, audio };
 }
 
 // 获取可用音色