From e00b0218a0b89e6e16d16695300854b76961b833 Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Wed, 22 Apr 2026 10:41:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0TTS=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E5=90=88=E6=88=90=E5=8A=9F=E8=83=BD=20-=20Edge=20TTS=E6=96=B9?= =?UTF-8?q?=E6=A1=88=E5=AE=8C=E6=95=B4=E5=AE=9E=E7=8E=B0=EF=BC=8CChatTTS?= =?UTF-8?q?=E9=A2=84=E7=95=99=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/server.cpython-310.pyc | Bin 5489 -> 7307 bytes __pycache__/tts_service.cpython-310.pyc | Bin 0 -> 7660 bytes logs/server.log | Bin 2686 -> 266 bytes main.py | 8 +- requirements.txt | 3 +- server.py | 69 ++ static/tts.html | 873 ++++++++++++++++++++++++ tts_service.py | 223 ++++++ 8 files changed, 1174 insertions(+), 2 deletions(-) create mode 100644 __pycache__/tts_service.cpython-310.pyc create mode 100644 static/tts.html create mode 100644 tts_service.py diff --git a/__pycache__/server.cpython-310.pyc b/__pycache__/server.cpython-310.pyc index bed4e99a16bf3547c98a451f43c6857cc2d22b25..06ec19b664952706a3b61dbbe55954569dfd9b9d 100644 GIT binary patch delta 3464 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 delta 1708 zcmZuxO>7%g5Z<@DPS%d&=EtdH$4PAG*C8csLMciA)3l+K1gfL~thQ$3d1==;-e|lI zZ6)MN4JVXJ)jPlik=y_Qt>hCo4%|3!UkNxN;8<}*h&RuWrV+OG^Y>=njAy=i`#zd} zIu>zTTSJ=s++X?4c^Z2ZN$TIRN8`mBXFKg+L2y^kb?{EL3PH=R{($A8TrYOX=P-n~ z8PmAF6XV^xEEne%v_b?9LEE;GOYlUohex3O7K09j#5%VFOq$dnL=#Fi%ivR@|agC5=s$!#5ap-)*;(wc^To+K*|K?>54kt_^4 z3=mt>VUP!5h?_hU*VarJhLNp+%r~GSNB@ls%gF5Wk=JGD7|-kreF4T*=%B1_2#(0& zhImHqdJ!hJOqp_+cf-;5bo?0@=%ta|2p=hq@&p{?QJCB|mN9FS;O(vw7%#z>gQop&vanmXAgHd~-zKO2uy)5@uObPn^kb&d% z@#YU0%Ze|pOy`)iE@us-bT~{fB7U*1q*^5PTQ8IxUiOxqb?#O@wZ}J$6}OCo;3;4kg;8uWCv7_3^jAKmPRcoFA~O;J6sCCqqYd zHY*mwFHOefhI`Qod&8O7bY)8uPI-+ZFBkY9cccrc|1WerOMH@GO4P%Xv2LOX@)@W& z<-vsbD}4Li<3y=<-@4@D`kd|Ae$WQUuQ|K~c!At= za6}ienS9d(G-f;@zHc3d8KN}AY!GLOQvrU9>M*~0*W(pDO%`)amVjr7hE%$|%CGu4 zC$0NN*bq~xkT2Y}y(O_2xxsFVuObULpJXcIMRKXra*5a!!92mM z1bcen6Z)>^VjMnNl^*q?Yjw@?MpY?ZW>Qt!cPK=4FCw1ka0ysy@;V z-y%4u9N7$lw+Y@M$Pqjf(`uxk7L|T@ofyf7H#BWnYaE1gv_5bOgQSAe?L%R7TxI$C zwT=zeq!c+Oe&|fss2Qb3wq>s)&2N(LGxzwuPNr#}D(OHcYTTyMzfqj3>;H=BY$VMW z-0~*JitRaWnSCQl(Ie)n%q}h_GDa5rL@_bIj*Ewhxs0`O7wod<6p*eEze&DRyu7KZ z@(ri#VOX5*$<)+Uq7J8FZ*2GxI@aqBK#9L^BX1l~x;F3x!8pNr0yQkEA8!)m32Xu? z$8WC6+r-7PZ(y~&Pgo_fI+oU{br-6oUVetjfJ#wKblW$s9#9~oq G{{0W2-+cH0 diff --git a/__pycache__/tts_service.cpython-310.pyc b/__pycache__/tts_service.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9b665b87fb1301b2db2a5a77edaf9eea23aabfb GIT binary patch literal 7660 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 literal 0 HcmV?d00001 diff --git a/logs/server.log b/logs/server.log index 8e45bc0b3035d7069f3c8821613ae516c8d7fac9..b2a61820d0fcc4398dd116c3aec8112e049560f0 100644 GIT binary patch delta 53 zcmew-(#14EiQUB5$iUJvcJg0Nm5IK6G8R_G7A6)Z3c3nP?yeyU`XwdB`UOS#Wtl0d JMH4>?0|2nc5c&WB literal 2686 zcmeH}&riZI6o5VRSG;h-7`qm6Zo3&GN;E=b#)ILol}%4? z`m6vM5&Igk1{@#%eB{gIC7#oL((^)1S(hN}^Qn@TM(P#x#;wD76CT zC$xqHrPqgP;QGTf0!3K&yp~*B1-4bnkM5?`h{hF3LdoV8ET=4o!QA?*#^uw)*%(m6 zXDjhE*vRrBVQEIXpB5?2E-&C5`nNYFB;9@3I=Cy#dBO + + + + + 语音对话(TTS版) + + + +
+
+

🎤 语音对话(TTS版)

+
+ + 检测连接... +
+
+ + +
+

🔊 TTS语音合成设置

+
+
+
❌ 无 TTS
+
只显示文字
+
+
+
🌐 Edge TTS
+
检测中...
+
+
+
🤖 ChatTTS
+
暂未部署
+
+
+ +
+ + +
+ +
点击按钮开始录音
+ +
+ + +
+
+ + +
+
+ + +
+
开始对话吧!
+
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/tts_service.py b/tts_service.py new file mode 100644 index 0000000..f2fd857 --- /dev/null +++ b/tts_service.py @@ -0,0 +1,223 @@ +""" +TTS 语音合成模块 +支持多种 TTS 方案 +""" + +import os +import uuid +import logging +import asyncio +from abc import ABC, abstractmethod +from typing import Optional, Tuple +from datetime import datetime + +# 配置 +AUDIO_DIR = os.getenv("AUDIO_DIR", "audio_cache") +os.makedirs(AUDIO_DIR, exist_ok=True) + +logger = logging.getLogger(__name__) + + +class TTSProvider(ABC): + """TTS 提供者抽象类""" + + @abstractmethod + async def synthesize(self, text: str) -> Tuple[str, str]: + """ + 合成语音 + 返回: (音频文件路径, 音频URL路径) + """ + pass + + @abstractmethod + def get_name(self) -> str: + """获取提供者名称""" + pass + + @abstractmethod + def is_available(self) -> bool: + """检查是否可用""" + pass + + +class EdgeTTSProvider(TTSProvider): + """Edge TTS 提供者(微软免费TTS)""" + + # 可用音色 + VOICES = { + "zh-CN-XiaoxiaoNeural": "晓晓(女)", + "zh-CN-YunxiNeural": "云希(男)", + "zh-CN-YunyangNeural": "云扬(男)", + "zh-CN-XiaochenNeural": "晓晨(女)", + "zh-CN-XiaohanNeural": "晓涵(女)", + "zh-CN-XiaomengNeural": "晓梦(女)", + "zh-CN-XiaomoNeural": "晓墨(女)", + "zh-CN-XiaoruiNeural": "晓睿(女)", + "zh-CN-XiaoshuangNeural": "晓双(女)", + "zh-CN-XiaoxuanNeural": "晓萱(女)", + "zh-CN-XiaoyanNeural": "晓颜(女)", + "zh-CN-XiaoyouNeural": "晓悠(女)", + } + + DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" + + def __init__(self, voice: Optional[str] = None): + self.voice = voice or self.DEFAULT_VOICE + self._available = None + + async def synthesize(self, text: str) -> Tuple[str, str]: + """使用 Edge TTS 合成语音""" + import edge_tts + + # 生成唯一文件名 + filename = f"{uuid.uuid4().hex}.mp3" + filepath = os.path.join(AUDIO_DIR, filename) + + # 合成语音 + communicate = edge_tts.Communicate(text, self.voice) + await communicate.save(filepath) + + # 返回路径 + audio_url = f"/audio/{filename}" + return filepath, audio_url + + def get_name(self) -> str: + return "Edge TTS" + + def get_voice_name(self) -> str: + """获取当前音色名称""" + return self.VOICES.get(self.voice, self.voice) + + def is_available(self) -> bool: + """检查 Edge TTS 是否可用""" + if self._available is None: + try: + import edge_tts + self._available = True + except ImportError: + logger.warning("edge-tts not installed") + self._available = False + return self._available + + def set_voice(self, voice: str): + """设置音色""" + if voice in self.VOICES: + self.voice = voice + else: + logger.warning(f"Unknown voice: {voice}, using default") + + +class ChatTTSProvider(TTSProvider): + """ChatTTS 提供者(本地部署,预留接口)""" + + # 预留配置 + CHATTTS_URL = os.getenv("CHATTTS_URL", "http://localhost:19020") + + def __init__(self): + self._available = False # 暂不可用 + + async def synthesize(self, text: str) -> Tuple[str, str]: + """ + 使用 ChatTTS 合成语音 + TODO: 后续实现 + """ + raise NotImplementedError("ChatTTS 尚未实现,请先部署 ChatTTS 服务") + + def get_name(self) -> str: + return "ChatTTS" + + def is_available(self) -> bool: + """检查 ChatTTS 是否可用""" + # TODO: 后续实现检测逻辑 + return self._available + + def set_available(self, available: bool): + """设置可用状态(部署后调用)""" + self._available = available + + +class NoTTSProvider(TTSProvider): + """不使用 TTS""" + + async def synthesize(self, text: str) -> Tuple[str, str]: + return None, None + + def get_name(self) -> str: + return "无 TTS" + + def is_available(self) -> bool: + return True + + +# TTS 管理器 +class TTSManager: + """TTS 方案管理""" + + PROVIDERS = { + "edge": EdgeTTSProvider, + "chattts": ChatTTSProvider, + "none": NoTTSProvider, + } + + def __init__(self, default_provider: str = "none"): + self.current_provider = default_provider + self._providers = {} + + # 初始化 Edge TTS(如果可用) + edge_provider = EdgeTTSProvider() + if edge_provider.is_available(): + self._providers["edge"] = edge_provider + + # 初始化 ChatTTS(预留) + self._providers["chattts"] = ChatTTSProvider() + + # 无 TTS + self._providers["none"] = NoTTSProvider() + + def get_provider(self, provider_name: Optional[str] = None) -> TTSProvider: + """获取 TTS 提供者""" + name = provider_name or self.current_provider + return self._providers.get(name, self._providers["none"]) + + def set_provider(self, provider_name: str): + """设置当前 TTS 方案""" + if provider_name in self._providers: + self.current_provider = provider_name + else: + logger.warning(f"Unknown provider: {provider_name}") + + def list_providers(self) -> list: + """列出所有可用方案""" + return [ + { + "name": name, + "display_name": provider.get_name(), + "available": provider.is_available() + } + for name, provider in self._providers.items() + ] + + def get_edge_voices(self) -> dict: + """获取 Edge TTS 可用音色""" + return EdgeTTSProvider.VOICES + + async def synthesize(self, text: str, provider_name: Optional[str] = None) -> Optional[str]: + """ + 合成语音 + 返回音频URL + """ + provider = self.get_provider(provider_name) + if not provider.is_available(): + logger.warning(f"Provider {provider.get_name()} not available") + return None + + try: + _, audio_url = await provider.synthesize(text) + return audio_url + except Exception as e: + logger.error(f"TTS synthesis failed: {e}") + return None + + +# 全局 TTS 管理器 +tts_manager = TTSManager() \ No newline at end of file