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:
@@ -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."""
|
||||
with _speak_lock:
|
||||
_stop_event.clear()
|
||||
|
||||
async def _run():
|
||||
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()
|
||||
|
||||
# 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
|
||||
pygame.time.wait(50)
|
||||
time.sleep(0.05)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if tmp_path and os.path.exists(tmp_path):
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
|
||||
asyncio.run(_run())
|
||||
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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user