Jamulix: Optimierungen übernehmen, lokale Voice-Modelle ignorieren
This commit is contained in:
parent
e99508a727
commit
91b9ef3374
12 changed files with 555 additions and 94 deletions
|
|
@ -13,6 +13,7 @@ from app.dependencies import (
|
|||
resolve_output_endpoint,
|
||||
get_store,
|
||||
piper_voice_for_language,
|
||||
voice_for_route,
|
||||
)
|
||||
from app.core.memory_extractor import maybe_schedule_extraction
|
||||
from app.quota import enforce_quota, record_usage, QuotaExceededError
|
||||
|
|
@ -96,7 +97,7 @@ async def chat(
|
|||
raise HTTPException(status_code=429, detail=str(exc))
|
||||
|
||||
# Explizit angeforderte Stimme gewinnt; sonst folgt sie der Routensprache.
|
||||
voice = payload.voice or piper_voice_for_language(route.tts_provider, route.language)
|
||||
voice = payload.voice or voice_for_route(route.tts_provider, route.language, route.voice_gender)
|
||||
|
||||
try:
|
||||
trace, audio = await orchestrator.chat_text(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import httpx
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.config import settings, active_profile
|
||||
|
|
@ -12,14 +13,60 @@ from app.dependencies import (
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
async def _chatterbox_reachable() -> bool:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=1.5) as client:
|
||||
r = await client.get(f"{settings.chatterbox_base_url.rstrip('/')}/status")
|
||||
return r.status_code < 500
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def _cartesia_available() -> bool:
|
||||
key = settings.cartesia_api_key.strip()
|
||||
voice_id = settings.cartesia_voice_id.strip()
|
||||
if not key or not voice_id:
|
||||
return False
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
r = await client.get(
|
||||
"https://api.cartesia.ai/voices",
|
||||
headers={"X-API-Key": key, "Cartesia-Version": "2024-06-10"},
|
||||
)
|
||||
return r.status_code < 400
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def _openrouter_tts_available() -> bool:
|
||||
"""Prüft ob das konfigurierte TTS-Modell auf OpenRouter via /audio/speech erreichbar ist."""
|
||||
key = settings.openrouter_api_key.strip()
|
||||
model = settings.openrouter_tts_model.strip()
|
||||
if not key or not model:
|
||||
return False
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
r = await client.post(
|
||||
"https://openrouter.ai/api/v1/audio/speech",
|
||||
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
||||
json={"model": model, "input": "Test", "voice": settings.openrouter_tts_voice or "alloy",
|
||||
"response_format": "mp3"},
|
||||
)
|
||||
# 200 = Audio OK, 4xx mit "does not exist" = Modell fehlt
|
||||
if r.status_code == 200:
|
||||
return True
|
||||
body = r.text
|
||||
return "does not exist" not in body and r.status_code < 500
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
async def get_config():
|
||||
"""Zeigt aktives Profil, aufgeloeste Default-Route und verfuegbare Bausteine.
|
||||
|
||||
Bewusst OHNE Secrets - API-Keys werden nur als 'gesetzt/nicht gesetzt' gemeldet.
|
||||
"""
|
||||
"""Zeigt aktives Profil, aufgeloeste Default-Route und verfuegbare Bausteine."""
|
||||
route = resolve_route()
|
||||
audio_router = get_audio_router()
|
||||
chatterbox_ok, openrouter_tts_ok, cartesia_ok = await _chatterbox_reachable(), await _openrouter_tts_available(), await _cartesia_available()
|
||||
return {
|
||||
"profile": active_profile(),
|
||||
"app_env": settings.app_env,
|
||||
|
|
@ -30,6 +77,9 @@ async def get_config():
|
|||
"tts_providers": sorted(TTS_REGISTRY),
|
||||
"input_endpoints": [c.model_dump() for c in await audio_router.list_inputs()],
|
||||
"output_endpoints": [c.model_dump() for c in await audio_router.list_outputs()],
|
||||
"chatterbox_reachable": chatterbox_ok,
|
||||
"openrouter_tts_available": openrouter_tts_ok,
|
||||
"cartesia_available": cartesia_ok,
|
||||
},
|
||||
"secrets": {
|
||||
"openrouter_api_key_set": bool(settings.openrouter_api_key.strip()),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from app.dependencies import (
|
|||
resolve_output_endpoint,
|
||||
get_store,
|
||||
piper_voice_for_language,
|
||||
voice_for_route,
|
||||
)
|
||||
from app.quota import enforce_quota, record_usage, QuotaExceededError
|
||||
from app.schemas import SpeakRequest
|
||||
|
|
@ -36,7 +37,7 @@ async def speak(
|
|||
try:
|
||||
route = resolve_route(user, session_id, overrides)
|
||||
# Explizit angefragte Stimme gewinnt; sonst folgt sie der Sprache (Piper-Parität zu /chat).
|
||||
voice = payload.voice or piper_voice_for_language(route.tts_provider, route.language)
|
||||
voice = payload.voice or voice_for_route(route.tts_provider, route.language, route.voice_gender)
|
||||
orchestrator = build_orchestrator(route)
|
||||
output = await resolve_output_endpoint(route)
|
||||
except SessionOwnershipError as exc:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from app.dependencies import (
|
|||
build_orchestrator,
|
||||
resolve_output_endpoint,
|
||||
piper_voice_for_language,
|
||||
voice_for_route,
|
||||
)
|
||||
from app.store import ANONYMOUS_USER_ID, SessionOwnershipError
|
||||
from app.audio.vad import EnergyVAD
|
||||
|
|
@ -122,7 +123,7 @@ async def _run_turn(
|
|||
elif effective_voice is not None:
|
||||
voice = effective_voice
|
||||
else:
|
||||
voice = piper_voice_for_language(route.tts_provider, route.language)
|
||||
voice = voice_for_route(route.tts_provider, route.language, route.voice_gender)
|
||||
stream = bool(options.get("stream"))
|
||||
# text_only: Geräte-TTS (Web Speech API) spricht selbst -> kein Server-Audio erzeugen/senden.
|
||||
text_only = bool(options.get("text_only"))
|
||||
|
|
@ -233,15 +234,15 @@ async def _voice_turn(websocket, store, user, session_id, audio, fmt, options):
|
|||
audio, fmt=fmt, language=None
|
||||
)
|
||||
# Stimme folgt der erkannten Sprache (Fallback: Routensprache).
|
||||
effective_voice = piper_voice_for_language(
|
||||
route.tts_provider, detected_language or route.language
|
||||
effective_voice = voice_for_route(
|
||||
route.tts_provider, detected_language or route.language, route.voice_gender
|
||||
)
|
||||
else:
|
||||
# Fix: konfigurierte Sprache an Whisper → Whisper übersetzt automatisch.
|
||||
transcript = await orchestrator.stt.transcribe(audio, fmt=fmt, language=route.language)
|
||||
detected_language = None
|
||||
# Stimme folgt der konfigurierten Sprache.
|
||||
effective_voice = piper_voice_for_language(route.tts_provider, route.language)
|
||||
effective_voice = voice_for_route(route.tts_provider, route.language, route.voice_gender)
|
||||
except Exception as exc:
|
||||
await websocket.send_json({"type": "error", "status": 502, "detail": str(exc)})
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue