diff --git a/main/manager-api/src/main/resources/db/changelog/202606231030.sql b/main/manager-api/src/main/resources/db/changelog/202606231030.sql new file mode 100644 index 00000000..99af238f --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202606231030.sql @@ -0,0 +1,15 @@ +-- 更新EdgeTTS供应器增加语速、音调、音量配置 +UPDATE `ai_model_provider` +SET fields = '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"rate","label":"语速(-100~100)","type":"number"},{"key":"volume","label":"音量(0~100)","type":"number"},{"key":"pitch","label":"音调(-100~100)","type":"number"}]' +WHERE id = 'SYSTEM_TTS_edge'; + +UPDATE `ai_model_config` SET +`remark` = 'EdgeTTS配置说明: +1. 使用微软Edge TTS服务 +2. 支持多种语言和音色 +3. 免费使用,无需注册 +4. 需要网络连接 +5. 输出文件保存在tmp/目录 +6. 语速:-100~100,0为正常速度 +7. 音量:0~100,50为正常音量 +8. 音调:-100~100,0为正常音调' WHERE `id` = 'TTS_EdgeTTS'; diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index aff608c6..3cefad28 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -676,3 +676,10 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202605251426.sql + - changeSet: + id: 202606231030 + author: RanChen + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202606231030.sql diff --git a/main/xiaozhi-server/core/providers/tts/alibl_stream.py b/main/xiaozhi-server/core/providers/tts/alibl_stream.py index d7626c55..bd0d2f83 100644 --- a/main/xiaozhi-server/core/providers/tts/alibl_stream.py +++ b/main/xiaozhi-server/core/providers/tts/alibl_stream.py @@ -80,6 +80,9 @@ class TTSProvider(TTSProviderBase): return self.ws logger.bind(tag=TAG).debug("开始建立新连接...") + # 建立新连接前取消旧监听任务 + await self._cancel_monitor_task() + self.ws = await websockets.connect( self.ws_url, additional_headers=self.header, @@ -306,16 +309,7 @@ class TTSProvider(TTSProviderBase): """清理资源""" await super().close() self.activate_session = False - # 取消监听任务 - if self._monitor_task: - try: - self._monitor_task.cancel() - await self._monitor_task - except asyncio.CancelledError: - pass - except Exception as e: - logger.bind(tag=TAG).warning(f"关闭时取消监听任务错误: {e}") - self._monitor_task = None + await self._cancel_monitor_task() # 关闭WebSocket连接 if self.ws: @@ -325,6 +319,18 @@ class TTSProvider(TTSProviderBase): pass self.ws = None self.last_active_time = None + + async def _cancel_monitor_task(self): + """取消监听任务""" + if self._monitor_task and not self._monitor_task.done(): + self._monitor_task.cancel() + try: + await self._monitor_task + except asyncio.CancelledError: + pass + except Exception as e: + logger.bind(tag=TAG).warning(f"取消监听任务错误: {e}") + self._monitor_task = None async def _start_monitor_tts_response(self): """监听TTS响应 - 长期运行""" diff --git a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py index 7ae64d1b..88f573b5 100644 --- a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py +++ b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py @@ -193,6 +193,9 @@ class TTSProvider(TTSProviderBase): return self.ws logger.bind(tag=TAG).debug("开始建立新连接...") + # 建立新连接前取消旧监听任务 + await self._cancel_monitor_task() + self.ws = await websockets.connect( self.ws_url, additional_headers={"X-NLS-Token": self.token}, @@ -401,15 +404,7 @@ class TTSProvider(TTSProviderBase): """资源清理""" await super().close() self.activate_session = False - if self._monitor_task: - try: - self._monitor_task.cancel() - await self._monitor_task - except asyncio.CancelledError: - pass - except Exception as e: - logger.bind(tag=TAG).warning(f"关闭时取消监听任务错误: {e}") - self._monitor_task = None + await self._cancel_monitor_task() if self.ws: try: @@ -504,6 +499,18 @@ class TTSProvider(TTSProviderBase): opus_encoder=None, ) + async def _cancel_monitor_task(self): + """取消监听任务""" + if self._monitor_task and not self._monitor_task.done(): + self._monitor_task.cancel() + try: + await self._monitor_task + except asyncio.CancelledError: + pass + except Exception as e: + logger.bind(tag=TAG).warning(f"取消监听任务错误: {e}") + self._monitor_task = None + def to_tts(self, text: str) -> list: """非流式TTS处理,用于测试及保存音频文件的场景""" try: diff --git a/main/xiaozhi-server/core/providers/tts/edge.py b/main/xiaozhi-server/core/providers/tts/edge.py index ffa1c3aa..bf82ff8d 100644 --- a/main/xiaozhi-server/core/providers/tts/edge.py +++ b/main/xiaozhi-server/core/providers/tts/edge.py @@ -6,6 +6,12 @@ from core.providers.tts.base import TTSProviderBase class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("ttsVolume", "volume", 0, 100, 50, int), + ("ttsRate", "speech_rate", -100, 100, 0, int), + ("ttsPitch", "pitch_rate", -100, 100, 0, int), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) if config.get("private_voice"): @@ -14,6 +20,22 @@ class TTSProvider(TTSProviderBase): self.voice = config.get("voice") self.audio_file_type = config.get("format", "mp3") + volume = config.get("volume", "50") + self.volume = int(volume) if volume else 50 + + speech_rate = config.get("rate", "0") + self.speech_rate = int(speech_rate) if speech_rate else 0 + + pitch_rate = config.get("pitch", "0") + self.pitch_rate = int(pitch_rate) if pitch_rate else 0 + + # 应用百分比调整 + self._apply_percentage_params(config) + + self.edge_rate = f"{self.speech_rate:+}%" + self.edge_volume = f"{self.volume:+}%" + self.edge_pitch = f"{self.pitch_rate:+}Hz" + def generate_filename(self, extension=".mp3"): return os.path.join( self.output_file, @@ -22,7 +44,13 @@ class TTSProvider(TTSProviderBase): async def text_to_speak(self, text, output_file): try: - communicate = edge_tts.Communicate(text, voice=self.voice) + communicate = edge_tts.Communicate( + text, + voice=self.voice, + rate=self.edge_rate, + volume=self.edge_volume, + pitch=self.edge_pitch, + ) if output_file: # 确保目录存在并创建空文件 os.makedirs(os.path.dirname(output_file), exist_ok=True) diff --git a/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py b/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py index 9cea8144..e1211109 100644 --- a/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py +++ b/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py @@ -229,6 +229,10 @@ class TTSProvider(TTSProviderBase): except: pass logger.bind(tag=TAG).debug("开始建立新连接...") + + # 建立新连接前取消旧监听任务 + await self._cancel_monitor_task() + ws_header = { "X-Api-App-Key": self.appId, "X-Api-Access-Key": self.access_token, @@ -472,16 +476,7 @@ class TTSProvider(TTSProviderBase): """资源清理方法""" await super().close() self.activate_session = False - # 取消监听任务 - if self._monitor_task: - try: - self._monitor_task.cancel() - await self._monitor_task - except asyncio.CancelledError: - pass - except Exception as e: - logger.bind(tag=TAG).warning(f"关闭时取消监听任务错误: {e}") - self._monitor_task = None + await self._cancel_monitor_task() if self.ws: try: @@ -731,6 +726,18 @@ class TTSProvider(TTSProviderBase): def wav_to_opus_data_audio_raw_stream(self, raw_data_var, is_end=False, callback: Callable[[Any], Any]=None): return self.opus_encoder.encode_pcm_to_opus_stream(raw_data_var, is_end, callback=callback) + async def _cancel_monitor_task(self): + """取消监听任务""" + if self._monitor_task and not self._monitor_task.done(): + self._monitor_task.cancel() + try: + await self._monitor_task + except asyncio.CancelledError: + pass + except Exception as e: + logger.bind(tag=TAG).warning(f"取消监听任务错误: {e}") + self._monitor_task = None + def to_tts(self, text: str) -> list: """非流式生成音频数据,用于生成音频及测试场景 Args: