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 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."""
|
||||||
|
with _speak_lock:
|
||||||
_stop_event.clear()
|
_stop_event.clear()
|
||||||
|
|
||||||
async def _run():
|
tmp_path = None
|
||||||
|
try:
|
||||||
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
||||||
tmp_path = f.name
|
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():
|
if _stop_event.is_set():
|
||||||
os.unlink(tmp_path)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
pygame.mixer.music.load(tmp_path)
|
pygame.mixer.music.load(tmp_path)
|
||||||
pygame.mixer.music.play()
|
pygame.mixer.music.play()
|
||||||
|
|
||||||
|
# Polling ohne pygame.time.wait (blockiert Event-Loop nicht)
|
||||||
while pygame.mixer.music.get_busy():
|
while pygame.mixer.music.get_busy():
|
||||||
if _stop_event.is_set():
|
if _stop_event.is_set():
|
||||||
pygame.mixer.music.stop()
|
pygame.mixer.music.stop()
|
||||||
break
|
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)
|
os.unlink(tmp_path)
|
||||||
|
except OSError:
|
||||||
asyncio.run(_run())
|
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)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user