diff --git a/.gitignore b/.gitignore index 669d75e..cd887af 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,5 @@ node_modules/ *.njsproj *.sln *.sw? + +*.mp3 \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/tts.py b/backend/app/api/v1/endpoints/tts.py index 6caafeb..106a283 100644 --- a/backend/app/api/v1/endpoints/tts.py +++ b/backend/app/api/v1/endpoints/tts.py @@ -1,10 +1,12 @@ -# tts.py import uuid import websockets import time import fastrand import json import asyncio +import os +import aiofiles +from datetime import datetime from typing import Dict, Any, Optional as OptionalType from app.constants.tts import APP_ID, TOKEN, SPEAKER @@ -34,8 +36,26 @@ EVENT_TaskRequest = 200 EVENT_TTSSentenceEnd = 351 EVENT_TTSResponse = 352 +# 音频文件保存目录 +TEMP_AUDIO_DIR = "./temp_audio" + + +# 确保音频目录存在 +async def ensure_audio_dir(): + """异步创建音频目录""" + if not os.path.exists(TEMP_AUDIO_DIR): + os.makedirs(TEMP_AUDIO_DIR, exist_ok=True) + + +# 生成时间戳文件名 +def generate_audio_filename() -> str: + """生成基于时间戳的音频文件名""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] # 精确到毫秒 + return f"{timestamp}.mp3" + + +# ... 保留所有原有的类定义和工具函数 ... -# 所有类定义 class Header: def __init__(self, protocol_version=PROTOCOL_VERSION, @@ -199,6 +219,8 @@ class TTSState: self.session_id: OptionalType[str] = None self.task: OptionalType[asyncio.Task] = None # 用于追踪异步任务 self.is_processing = False + self.audio_data = bytearray() # 用于收集音频数据 + self.audio_filename = None # 保存的文件名 # 全局状态管理 @@ -305,6 +327,18 @@ async def create_tts_connection() -> websockets.WebSocketServerProtocol: return volc_ws +# 异步保存音频文件 +async def save_audio_file(audio_data: bytes, filename: str) -> str: + """异步保存音频文件""" + await ensure_audio_dir() + file_path = os.path.join(TEMP_AUDIO_DIR, filename) + + async with aiofiles.open(file_path, 'wb') as f: + await f.write(audio_data) + + return file_path + + # 处理单个TTS任务 async def process_tts_task(websocket, message_id: str, text: str): """处理单个TTS任务(独立协程)""" @@ -318,6 +352,8 @@ async def process_tts_task(websocket, message_id: str, text: str): raise Exception(f"找不到TTS状态: {message_id}") tts_state.is_processing = True + # 生成音频文件名 + tts_state.audio_filename = generate_audio_filename() # 创建独立的TTS连接 tts_state.volc_ws = await create_tts_connection() @@ -373,8 +409,12 @@ async def process_tts_task(websocket, message_id: str, text: str): elif res.optional.event == EVENT_TTSResponse: audio_count += 1 - print(f"发送音频数据 [{message_id}] #{audio_count},大小: {len(res.payload)}") - # 发送音频数据 + print(f"收到音频数据 [{message_id}] #{audio_count},大小: {len(res.payload)}") + + # 收集音频数据 + tts_state.audio_data.extend(res.payload) + + # 发送音频数据到前端 await websocket.send_json({ "id": audio_count, "type": "tts_audio_data", @@ -387,10 +427,20 @@ async def process_tts_task(websocket, message_id: str, text: str): except asyncio.TimeoutError: print(f"TTS响应超时 [{message_id}],强制结束") - # 发送完成消息 + # 异步保存音频文件 + if tts_state.audio_data: + file_path = await save_audio_file( + bytes(tts_state.audio_data), + tts_state.audio_filename + ) + print(f"音频文件已保存 [{message_id}]: {file_path}") + + # 发送完成消息,包含文件路径 await websocket.send_json({ "type": "tts_audio_complete", - "messageId": message_id + "messageId": message_id, + "audioFile": tts_state.audio_filename, + "audioPath": os.path.join(TEMP_AUDIO_DIR, tts_state.audio_filename) if tts_state.audio_data else None }) print(f"TTS处理完成 [{message_id}],共发送 {audio_count} 个音频包") diff --git a/web/components.d.ts b/web/components.d.ts index 3400cc8..dea5305 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -9,6 +9,8 @@ declare module 'vue' { export interface GlobalComponents { Avatar: typeof import('./src/components/avatar.vue')['default'] Markdown: typeof import('./src/components/markdown.vue')['default'] + Message_tools: typeof import('./src/components/MessageTools.vue')['default'] + MessageTools: typeof import('./src/components/MessageTools.vue')['default'] NButton: typeof import('naive-ui')['NButton'] NCollapse: typeof import('naive-ui')['NCollapse'] NCollapseItem: typeof import('naive-ui')['NCollapseItem'] diff --git a/web/src/components/MessageTools.vue b/web/src/components/MessageTools.vue new file mode 100644 index 0000000..e8aaa1f --- /dev/null +++ b/web/src/components/MessageTools.vue @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/web/src/views/ChatLLMView.vue b/web/src/views/ChatLLMView.vue index 177e3c6..a70260b 100644 --- a/web/src/views/ChatLLMView.vue +++ b/web/src/views/ChatLLMView.vue @@ -4,7 +4,6 @@ import type { Message } from "@/interfaces"; import { throttle } from "lodash-es"; import AIAvatar from "@/assets/ai_avatar.png"; import { - DocumentDuplicateIcon, ExclamationTriangleIcon, microphone, PaperAirplaneIcon, @@ -13,7 +12,6 @@ import { import UserAvatar from "@/assets/user_avatar.jpg"; import markdown from "@/components/markdown.vue"; import { useAsrStore, useChatStore, useLayoutStore } from "@/stores"; -import { copy } from "@/utils"; const chatStore = useChatStore(); const { historyMessages, completing, modelList, modelInfo, thinking } = @@ -206,19 +204,7 @@ onMounted(() => { -
-
- -
- - - 复制内容 - -
+ diff --git a/web/src/views/VoiceView.vue b/web/src/views/VoiceView.vue index 2a880e4..60efce8 100644 --- a/web/src/views/VoiceView.vue +++ b/web/src/views/VoiceView.vue @@ -191,9 +191,7 @@ onMounted(() => { -
- -
+