Feat: Edge-TTS-Engine (native dt. Azure-Stimmen, kein Akzent) + Probe-hören-Knopf
Edge-TTS als 4. Engine (gratis, kein Key, KEIN Cloning → natives Deutsch ohne Akzent — die einzige Lösung gegen das Akzent-Problem aller Cloning-Engines). /voices listet dt. Edge-Stimmen (weiblich zuerst, inkl. Gisela). Picker: Engine 'Edge (natürlich · gratis)' + Probe-hören-Knopf (festen Satz je Engine/Stimme abspielen, ohne reinsprechen). Default bleibt Piper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+60
-2
@@ -60,6 +60,8 @@ CHATTERBOX_REF = os.environ.get("VOICE_CHATTERBOX_REF", "")
|
||||
ELEVEN_MODEL = os.environ.get("VOICE_ELEVENLABS_MODEL", "eleven_flash_v2_5")
|
||||
ELEVEN_DEFAULT_VOICE = os.environ.get("VOICE_ELEVENLABS_VOICE", "21m00Tcm4TlvDq8ikWAM")
|
||||
HERMES_ENV = Path.home() / ".hermes" / ".env"
|
||||
# Edge-TTS: native deutsche Azure-Neural-Stimmen, gratis & ohne Key (kein Cloning → kein Akzent).
|
||||
EDGE_DEFAULT = os.environ.get("VOICE_EDGE_VOICE", "de-DE-KatjaNeural")
|
||||
|
||||
|
||||
def _eleven_key() -> str:
|
||||
@@ -230,6 +232,60 @@ def elevenlabs_list() -> list[dict]:
|
||||
return []
|
||||
|
||||
|
||||
# =================================================================================
|
||||
# TTS — Edge-TTS (native dt. Azure-Neural-Stimmen, gratis, kein Key, KEIN Cloning → kein Akzent)
|
||||
# =================================================================================
|
||||
def edge_tts_synth(text: str, voice: str) -> bytes:
|
||||
import asyncio
|
||||
import edge_tts
|
||||
|
||||
v = voice or EDGE_DEFAULT
|
||||
|
||||
async def _run() -> bytes:
|
||||
buf = bytearray()
|
||||
communicate = edge_tts.Communicate(text, v)
|
||||
async for chunk in communicate.stream():
|
||||
if chunk["type"] == "audio":
|
||||
buf.extend(chunk["data"])
|
||||
return bytes(buf)
|
||||
|
||||
try:
|
||||
return asyncio.run(_run())
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise HTTPException(502, f"Edge-TTS-Fehler: {exc}")
|
||||
|
||||
|
||||
_edge_cache: list | None = None
|
||||
|
||||
|
||||
def edge_list() -> list[dict]:
|
||||
"""Deutsche Edge/Azure-Stimmen (einmal vom Dienst geholt + gecacht). Weibliche zuerst."""
|
||||
global _edge_cache
|
||||
if _edge_cache is not None:
|
||||
return _edge_cache
|
||||
try:
|
||||
import asyncio
|
||||
import edge_tts
|
||||
|
||||
voices = asyncio.run(edge_tts.list_voices())
|
||||
out = []
|
||||
for v in voices:
|
||||
if not v.get("Locale", "").startswith("de-"):
|
||||
continue
|
||||
short = v["ShortName"] # z.B. de-DE-KatjaNeural
|
||||
gender = "weiblich" if v.get("Gender") == "Female" else "männlich"
|
||||
name = short.split("-")[-1].replace("Neural", "").replace("Multilingual", " (multiling.)")
|
||||
out.append({"engine": "edge", "id": short,
|
||||
"label": f"{name} · {v['Locale']} · {gender}",
|
||||
"clonable": False, "_female": v.get("Gender") == "Female"})
|
||||
out.sort(key=lambda i: (not i.pop("_female"), i["id"]))
|
||||
_edge_cache = out
|
||||
except Exception: # noqa: BLE001
|
||||
log.warning("Edge-Stimmenliste fehlgeschlagen", exc_info=True)
|
||||
_edge_cache = []
|
||||
return _edge_cache
|
||||
|
||||
|
||||
# =================================================================================
|
||||
# FastAPI
|
||||
# =================================================================================
|
||||
@@ -258,14 +314,14 @@ class TTSIn(BaseModel):
|
||||
@app.get("/health")
|
||||
def health() -> dict:
|
||||
return {"ok": True, "stt_model": STT_MODEL,
|
||||
"engines": ["piper", "chatterbox", "elevenlabs"],
|
||||
"engines": ["edge", "piper", "chatterbox", "elevenlabs"],
|
||||
"elevenlabs_key": bool(_eleven_key()),
|
||||
"piper_voices": [v["id"] for v in piper_list()]}
|
||||
|
||||
|
||||
@app.get("/voices")
|
||||
def voices() -> dict:
|
||||
return {"voices": piper_list() + chatterbox_list() + elevenlabs_list(),
|
||||
return {"voices": edge_list() + piper_list() + chatterbox_list() + elevenlabs_list(),
|
||||
"default": {"engine": "piper", "voice": PIPER_DEFAULT}}
|
||||
|
||||
|
||||
@@ -284,6 +340,8 @@ def tts(body: TTSIn) -> Response:
|
||||
text = (body.text or "").strip()
|
||||
if not text:
|
||||
raise HTTPException(400, "Leerer Text.")
|
||||
if body.engine == "edge":
|
||||
return Response(content=edge_tts_synth(text, body.voice), media_type="audio/mpeg")
|
||||
if body.engine == "elevenlabs":
|
||||
return Response(content=elevenlabs_tts(text, body.voice), media_type="audio/mpeg")
|
||||
if body.engine == "chatterbox":
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Kern (immer nötig — STT + Piper-TTS + Server). Chatterbox + torch zieht install.sh
|
||||
# separat best-effort nach (CPU-Wheel), damit der Voice-Loop auch ohne Premium-Engine läuft.
|
||||
# Kern (immer nötig — STT + Server). Piper-TTS läuft über das offizielle Binary (install.sh lädt
|
||||
# es), nicht über das PyPI-Paket (piper-phonemize hat auf neueren Linux keine Wheels). Chatterbox
|
||||
# + torch zieht install.sh best-effort nach (CPU-Wheel), damit der Loop auch ohne Premium läuft.
|
||||
fastapi
|
||||
uvicorn
|
||||
python-multipart
|
||||
soundfile
|
||||
numpy
|
||||
faster-whisper
|
||||
piper-tts==1.2.0
|
||||
edge-tts
|
||||
|
||||
Reference in New Issue
Block a user