From 8881d65c8d4ec48fde0c62bd90f1ad5e716e3a50 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 26 Jun 2026 22:25:53 +0200 Subject: [PATCH] Fix: audio_output Hang + Stimme auf Seraphina MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - speak() hing weil pygame.time.wait() die asyncio Event-Loop blockierte. Fix: time.sleep(0.05) statt pygame.time.wait() im Playback-Loop. - asyncio.new_event_loop() statt asyncio.run() — thread-sicher, kein "event loop already running" in Worker-Threads. - _speak_lock verhindert parallele TTS-Aufrufe. - Stimme: de-DE-SeraphinaMultilingualNeural als Default (modern, weiblich, multilingual neural). Dropdown in Settings mit allen DE/EN Optionen. - speak()-Signatur: voice + rate als Parameter (statt config-Import). Co-Authored-By: Claude Sonnet 4.6 --- client/hermes-voice/audio_output.py | 58 ++++++++++++++++----------- client/hermes-voice/config_manager.py | 2 +- client/hermes-voice/main.py | 9 ++++- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/client/hermes-voice/audio_output.py b/client/hermes-voice/audio_output.py index 3e2904f..1b4f0c1 100644 --- a/client/hermes-voice/audio_output.py +++ b/client/hermes-voice/audio_output.py @@ -2,47 +2,59 @@ import asyncio import os import tempfile import threading +import time import pygame import edge_tts -from config import TTS_VOICE, TTS_RATE, TTS_PITCH pygame.mixer.init() _stop_event = threading.Event() +_speak_lock = threading.Lock() def stop_speaking(): - """Unterbricht laufende Sprachausgabe (z.B. wenn User spricht).""" _stop_event.set() pygame.mixer.music.stop() -def speak(text: str): +def speak(text: str, voice: str = "de-DE-SeraphinaMultilingualNeural", rate: str = "+0%"): """Text → Edge TTS → Lautsprecher. Blockiert bis fertig oder unterbrochen.""" - _stop_event.clear() + with _speak_lock: + _stop_event.clear() - async def _run(): - with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: - tmp_path = f.name + tmp_path = None + try: + with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: + tmp_path = f.name - communicate = edge_tts.Communicate(text, TTS_VOICE, rate=TTS_RATE, pitch=TTS_PITCH) - await communicate.save(tmp_path) + # Edge TTS in eigenem Event-Loop (Thread-sicher) + loop = asyncio.new_event_loop() + try: + communicate = edge_tts.Communicate(text, voice, rate=rate) + loop.run_until_complete(communicate.save(tmp_path)) + finally: + loop.close() - if _stop_event.is_set(): - os.unlink(tmp_path) - return - - pygame.mixer.music.load(tmp_path) - pygame.mixer.music.play() - - while pygame.mixer.music.get_busy(): if _stop_event.is_set(): - pygame.mixer.music.stop() - break - pygame.time.wait(50) + return - os.unlink(tmp_path) + pygame.mixer.music.load(tmp_path) + pygame.mixer.music.play() - asyncio.run(_run()) + # Polling ohne pygame.time.wait (blockiert Event-Loop nicht) + while pygame.mixer.music.get_busy(): + if _stop_event.is_set(): + pygame.mixer.music.stop() + break + time.sleep(0.05) + + except Exception: + pass + finally: + if tmp_path and os.path.exists(tmp_path): + try: + os.unlink(tmp_path) + except OSError: + pass def play_ding(): @@ -55,4 +67,4 @@ def play_ding(): stereo = np.column_stack([wave, wave]) sound = pygame.sndarray.make_sound(stereo) sound.play() - pygame.time.wait(int(duration * 1000) + 20) + time.sleep(duration + 0.02) diff --git a/client/hermes-voice/config_manager.py b/client/hermes-voice/config_manager.py index b908eaf..c2d725c 100644 --- a/client/hermes-voice/config_manager.py +++ b/client/hermes-voice/config_manager.py @@ -9,7 +9,7 @@ DEFAULTS = { "chat_model": "Hermes-4-14B", "vision_model": "Qwen3-VL-2B-Instruct", "hotkey": "ctrl_r", - "tts_voice": "de-DE-KillianNeural", + "tts_voice": "de-DE-SeraphinaMultilingualNeural", "tts_rate": "+0%", "whisper_model": "small", "language": "de", diff --git a/client/hermes-voice/main.py b/client/hermes-voice/main.py index 275b8ed..8b9dc65 100644 --- a/client/hermes-voice/main.py +++ b/client/hermes-voice/main.py @@ -259,8 +259,13 @@ class SettingsWindow(ctk.CTkToplevel): label("TTS Stimme") self._voice = dropdown("tts_voice", [ - "de-DE-KillianNeural", "de-DE-SeraphinaMultilingualNeural", - "de-DE-ConradNeural", "en-US-AndrewNeural", "en-US-AriaNeural", + "de-DE-SeraphinaMultilingualNeural", + "de-DE-AmalaNeural", + "de-DE-KatjaNeural", + "de-DE-KillianNeural", + "de-DE-ConradNeural", + "en-US-AriaNeural", + "en-US-JennyNeural", ]) label("Whisper Modell")