Fix: audio_output Hang + Stimme auf Seraphina

- 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 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-26 22:25:53 +02:00
parent 037c4b11df
commit 8881d65c8d
3 changed files with 43 additions and 26 deletions
+35 -23
View File
@@ -2,47 +2,59 @@ import asyncio
import os import os
import tempfile import tempfile
import threading import threading
import time
import pygame import pygame
import edge_tts import edge_tts
from config import TTS_VOICE, TTS_RATE, TTS_PITCH
pygame.mixer.init() pygame.mixer.init()
_stop_event = threading.Event() _stop_event = threading.Event()
_speak_lock = threading.Lock()
def stop_speaking(): def stop_speaking():
"""Unterbricht laufende Sprachausgabe (z.B. wenn User spricht)."""
_stop_event.set() _stop_event.set()
pygame.mixer.music.stop() 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.""" """Text → Edge TTS → Lautsprecher. Blockiert bis fertig oder unterbrochen."""
_stop_event.clear() with _speak_lock:
_stop_event.clear()
async def _run(): tmp_path = None
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: try:
tmp_path = f.name 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) # Edge TTS in eigenem Event-Loop (Thread-sicher)
await communicate.save(tmp_path) 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(): if _stop_event.is_set():
pygame.mixer.music.stop() return
break
pygame.time.wait(50)
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(): def play_ding():
@@ -55,4 +67,4 @@ def play_ding():
stereo = np.column_stack([wave, wave]) stereo = np.column_stack([wave, wave])
sound = pygame.sndarray.make_sound(stereo) sound = pygame.sndarray.make_sound(stereo)
sound.play() sound.play()
pygame.time.wait(int(duration * 1000) + 20) time.sleep(duration + 0.02)
+1 -1
View File
@@ -9,7 +9,7 @@ DEFAULTS = {
"chat_model": "Hermes-4-14B", "chat_model": "Hermes-4-14B",
"vision_model": "Qwen3-VL-2B-Instruct", "vision_model": "Qwen3-VL-2B-Instruct",
"hotkey": "ctrl_r", "hotkey": "ctrl_r",
"tts_voice": "de-DE-KillianNeural", "tts_voice": "de-DE-SeraphinaMultilingualNeural",
"tts_rate": "+0%", "tts_rate": "+0%",
"whisper_model": "small", "whisper_model": "small",
"language": "de", "language": "de",
+7 -2
View File
@@ -259,8 +259,13 @@ class SettingsWindow(ctk.CTkToplevel):
label("TTS Stimme") label("TTS Stimme")
self._voice = dropdown("tts_voice", [ self._voice = dropdown("tts_voice", [
"de-DE-KillianNeural", "de-DE-SeraphinaMultilingualNeural", "de-DE-SeraphinaMultilingualNeural",
"de-DE-ConradNeural", "en-US-AndrewNeural", "en-US-AriaNeural", "de-DE-AmalaNeural",
"de-DE-KatjaNeural",
"de-DE-KillianNeural",
"de-DE-ConradNeural",
"en-US-AriaNeural",
"en-US-JennyNeural",
]) ])
label("Whisper Modell") label("Whisper Modell")