feat(menu): nicht nutzbare TTS-Provider ausblenden + GPU-Erkennung
Menue zeigt nur tatsaechlich nutzbare Vorlese-Optionen, um Verwirrung durch
folgenlose Eintraege zu vermeiden:
- Backend ist alleinige Quelle: /api/config liefert fertiges tts_menu
(Chatterbox nur wenn erreichbar, Cloud nur mit OpenRouter-TTS, Cartesia nur
mit Key). piper bleibt garantierter, immer sichtbarer Default.
- Frontend blendet aus statt auszugrauen; 'Im Geraet' nur auf Mobilgeraeten
(isMobile). Aktive/gespeicherte Auswahl auf einen sichtbaren Provider
geklemmt -> kein Server-422.
- 4x refresh*Preset + 3x /api/config-Fetch -> 1x refreshTtsMenu; /api/config
serverseitig 30s gecacht (spart die bisher kostenpflichtige OpenRouter-Probe
pro Seitenladen).
GPU-Erkennung zentralisiert (keine Dopplung):
- app/capabilities.py: detect_physical_gpus/resolve_gpus (nvidia-smi).
- Neues Setting GPUS (auto|none|0,1,2), beim Start an CUDA_VISIBLE_DEVICES
gekoppelt -> angezeigt = real nutzbar. admin_llm nutzt dieselbe Erkennung.
Tests: tests/test_capabilities.py (Maske, Kopplung, Menue-Sichtbarkeit, GPUs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:16:21 +02:00
|
|
|
import asyncio
|
|
|
|
|
import time
|
|
|
|
|
|
2026-06-24 22:01:38 +02:00
|
|
|
import httpx
|
2026-06-17 01:48:56 +02:00
|
|
|
from fastapi import APIRouter
|
|
|
|
|
|
|
|
|
|
from app.config import settings, active_profile
|
feat(menu): nicht nutzbare TTS-Provider ausblenden + GPU-Erkennung
Menue zeigt nur tatsaechlich nutzbare Vorlese-Optionen, um Verwirrung durch
folgenlose Eintraege zu vermeiden:
- Backend ist alleinige Quelle: /api/config liefert fertiges tts_menu
(Chatterbox nur wenn erreichbar, Cloud nur mit OpenRouter-TTS, Cartesia nur
mit Key). piper bleibt garantierter, immer sichtbarer Default.
- Frontend blendet aus statt auszugrauen; 'Im Geraet' nur auf Mobilgeraeten
(isMobile). Aktive/gespeicherte Auswahl auf einen sichtbaren Provider
geklemmt -> kein Server-422.
- 4x refresh*Preset + 3x /api/config-Fetch -> 1x refreshTtsMenu; /api/config
serverseitig 30s gecacht (spart die bisher kostenpflichtige OpenRouter-Probe
pro Seitenladen).
GPU-Erkennung zentralisiert (keine Dopplung):
- app/capabilities.py: detect_physical_gpus/resolve_gpus (nvidia-smi).
- Neues Setting GPUS (auto|none|0,1,2), beim Start an CUDA_VISIBLE_DEVICES
gekoppelt -> angezeigt = real nutzbar. admin_llm nutzt dieselbe Erkennung.
Tests: tests/test_capabilities.py (Maske, Kopplung, Menue-Sichtbarkeit, GPUs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:16:21 +02:00
|
|
|
from app.capabilities import resolve_gpus
|
2026-06-25 00:49:28 +02:00
|
|
|
from app.providers.tts.cartesia import _UNSUPPORTED_LANGS as _CARTESIA_UNSUPPORTED
|
2026-06-17 01:48:56 +02:00
|
|
|
from app.dependencies import (
|
|
|
|
|
resolve_route,
|
|
|
|
|
get_audio_router,
|
|
|
|
|
STT_REGISTRY,
|
|
|
|
|
LLM_REGISTRY,
|
|
|
|
|
TTS_REGISTRY,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
feat(menu): nicht nutzbare TTS-Provider ausblenden + GPU-Erkennung
Menue zeigt nur tatsaechlich nutzbare Vorlese-Optionen, um Verwirrung durch
folgenlose Eintraege zu vermeiden:
- Backend ist alleinige Quelle: /api/config liefert fertiges tts_menu
(Chatterbox nur wenn erreichbar, Cloud nur mit OpenRouter-TTS, Cartesia nur
mit Key). piper bleibt garantierter, immer sichtbarer Default.
- Frontend blendet aus statt auszugrauen; 'Im Geraet' nur auf Mobilgeraeten
(isMobile). Aktive/gespeicherte Auswahl auf einen sichtbaren Provider
geklemmt -> kein Server-422.
- 4x refresh*Preset + 3x /api/config-Fetch -> 1x refreshTtsMenu; /api/config
serverseitig 30s gecacht (spart die bisher kostenpflichtige OpenRouter-Probe
pro Seitenladen).
GPU-Erkennung zentralisiert (keine Dopplung):
- app/capabilities.py: detect_physical_gpus/resolve_gpus (nvidia-smi).
- Neues Setting GPUS (auto|none|0,1,2), beim Start an CUDA_VISIBLE_DEVICES
gekoppelt -> angezeigt = real nutzbar. admin_llm nutzt dieselbe Erkennung.
Tests: tests/test_capabilities.py (Maske, Kopplung, Menue-Sichtbarkeit, GPUs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:16:21 +02:00
|
|
|
# /api/config probt externe Dienste (u. a. einen KOSTENPFLICHTIGEN OpenRouter-TTS-Aufruf).
|
|
|
|
|
# Daher serverseitig kurz cachen: das Frontend ruft den Endpunkt mehrfach (Laden + Menü-Öffnen).
|
|
|
|
|
_CACHE: dict | None = None
|
|
|
|
|
_CACHE_TS: float = 0.0
|
|
|
|
|
_CACHE_TTL = 30.0
|
|
|
|
|
_CACHE_LOCK = asyncio.Lock()
|
|
|
|
|
|
2026-06-17 01:48:56 +02:00
|
|
|
|
2026-06-24 22:01:38 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
feat(menu): nicht nutzbare TTS-Provider ausblenden + GPU-Erkennung
Menue zeigt nur tatsaechlich nutzbare Vorlese-Optionen, um Verwirrung durch
folgenlose Eintraege zu vermeiden:
- Backend ist alleinige Quelle: /api/config liefert fertiges tts_menu
(Chatterbox nur wenn erreichbar, Cloud nur mit OpenRouter-TTS, Cartesia nur
mit Key). piper bleibt garantierter, immer sichtbarer Default.
- Frontend blendet aus statt auszugrauen; 'Im Geraet' nur auf Mobilgeraeten
(isMobile). Aktive/gespeicherte Auswahl auf einen sichtbaren Provider
geklemmt -> kein Server-422.
- 4x refresh*Preset + 3x /api/config-Fetch -> 1x refreshTtsMenu; /api/config
serverseitig 30s gecacht (spart die bisher kostenpflichtige OpenRouter-Probe
pro Seitenladen).
GPU-Erkennung zentralisiert (keine Dopplung):
- app/capabilities.py: detect_physical_gpus/resolve_gpus (nvidia-smi).
- Neues Setting GPUS (auto|none|0,1,2), beim Start an CUDA_VISIBLE_DEVICES
gekoppelt -> angezeigt = real nutzbar. admin_llm nutzt dieselbe Erkennung.
Tests: tests/test_capabilities.py (Maske, Kopplung, Menue-Sichtbarkeit, GPUs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:16:21 +02:00
|
|
|
async def _build_config() -> dict:
|
|
|
|
|
"""Baut die /api/config-Antwort (eine Quelle der Wahrheit für die Menü-Sichtbarkeit)."""
|
2026-06-17 01:48:56 +02:00
|
|
|
route = resolve_route()
|
|
|
|
|
audio_router = get_audio_router()
|
feat(menu): nicht nutzbare TTS-Provider ausblenden + GPU-Erkennung
Menue zeigt nur tatsaechlich nutzbare Vorlese-Optionen, um Verwirrung durch
folgenlose Eintraege zu vermeiden:
- Backend ist alleinige Quelle: /api/config liefert fertiges tts_menu
(Chatterbox nur wenn erreichbar, Cloud nur mit OpenRouter-TTS, Cartesia nur
mit Key). piper bleibt garantierter, immer sichtbarer Default.
- Frontend blendet aus statt auszugrauen; 'Im Geraet' nur auf Mobilgeraeten
(isMobile). Aktive/gespeicherte Auswahl auf einen sichtbaren Provider
geklemmt -> kein Server-422.
- 4x refresh*Preset + 3x /api/config-Fetch -> 1x refreshTtsMenu; /api/config
serverseitig 30s gecacht (spart die bisher kostenpflichtige OpenRouter-Probe
pro Seitenladen).
GPU-Erkennung zentralisiert (keine Dopplung):
- app/capabilities.py: detect_physical_gpus/resolve_gpus (nvidia-smi).
- Neues Setting GPUS (auto|none|0,1,2), beim Start an CUDA_VISIBLE_DEVICES
gekoppelt -> angezeigt = real nutzbar. admin_llm nutzt dieselbe Erkennung.
Tests: tests/test_capabilities.py (Maske, Kopplung, Menue-Sichtbarkeit, GPUs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:16:21 +02:00
|
|
|
chatterbox_ok, openrouter_tts_ok, cartesia_ok, gpus = (
|
|
|
|
|
await _chatterbox_reachable(),
|
|
|
|
|
await _openrouter_tts_available(),
|
|
|
|
|
await _cartesia_available(),
|
|
|
|
|
await resolve_gpus(),
|
|
|
|
|
)
|
|
|
|
|
# Fertige Sichtbarkeits-Entscheidung je TTS-Provider -> Frontend dupliziert keine Regeln.
|
|
|
|
|
# "device" ist client_only: nur das Frontend kann "Mobilgerät?" beantworten (isMobile()).
|
|
|
|
|
# "piper" bleibt der garantierte, immer sichtbare Default (Menü nie leer).
|
|
|
|
|
tts_menu = [
|
|
|
|
|
{"provider": "piper", "visible": True, "client_only": False},
|
|
|
|
|
{"provider": "chatterbox", "visible": chatterbox_ok, "client_only": False},
|
|
|
|
|
{"provider": "cartesia", "visible": cartesia_ok, "client_only": False},
|
|
|
|
|
{"provider": "openrouter", "visible": openrouter_tts_ok, "client_only": False},
|
|
|
|
|
{"provider": "device", "visible": True, "client_only": True},
|
|
|
|
|
]
|
2026-06-17 01:48:56 +02:00
|
|
|
return {
|
|
|
|
|
"profile": active_profile(),
|
|
|
|
|
"app_env": settings.app_env,
|
|
|
|
|
"default_route": route.as_dict(),
|
feat(menu): nicht nutzbare TTS-Provider ausblenden + GPU-Erkennung
Menue zeigt nur tatsaechlich nutzbare Vorlese-Optionen, um Verwirrung durch
folgenlose Eintraege zu vermeiden:
- Backend ist alleinige Quelle: /api/config liefert fertiges tts_menu
(Chatterbox nur wenn erreichbar, Cloud nur mit OpenRouter-TTS, Cartesia nur
mit Key). piper bleibt garantierter, immer sichtbarer Default.
- Frontend blendet aus statt auszugrauen; 'Im Geraet' nur auf Mobilgeraeten
(isMobile). Aktive/gespeicherte Auswahl auf einen sichtbaren Provider
geklemmt -> kein Server-422.
- 4x refresh*Preset + 3x /api/config-Fetch -> 1x refreshTtsMenu; /api/config
serverseitig 30s gecacht (spart die bisher kostenpflichtige OpenRouter-Probe
pro Seitenladen).
GPU-Erkennung zentralisiert (keine Dopplung):
- app/capabilities.py: detect_physical_gpus/resolve_gpus (nvidia-smi).
- Neues Setting GPUS (auto|none|0,1,2), beim Start an CUDA_VISIBLE_DEVICES
gekoppelt -> angezeigt = real nutzbar. admin_llm nutzt dieselbe Erkennung.
Tests: tests/test_capabilities.py (Maske, Kopplung, Menue-Sichtbarkeit, GPUs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:16:21 +02:00
|
|
|
"tts_menu": tts_menu,
|
2026-06-17 01:48:56 +02:00
|
|
|
"available": {
|
|
|
|
|
"stt_providers": sorted(STT_REGISTRY),
|
|
|
|
|
"llm_providers": sorted(LLM_REGISTRY),
|
|
|
|
|
"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()],
|
2026-06-24 22:01:38 +02:00
|
|
|
"chatterbox_reachable": chatterbox_ok,
|
|
|
|
|
"openrouter_tts_available": openrouter_tts_ok,
|
|
|
|
|
"cartesia_available": cartesia_ok,
|
2026-06-25 00:49:28 +02:00
|
|
|
"cartesia_unsupported_languages": sorted(_CARTESIA_UNSUPPORTED),
|
feat(menu): nicht nutzbare TTS-Provider ausblenden + GPU-Erkennung
Menue zeigt nur tatsaechlich nutzbare Vorlese-Optionen, um Verwirrung durch
folgenlose Eintraege zu vermeiden:
- Backend ist alleinige Quelle: /api/config liefert fertiges tts_menu
(Chatterbox nur wenn erreichbar, Cloud nur mit OpenRouter-TTS, Cartesia nur
mit Key). piper bleibt garantierter, immer sichtbarer Default.
- Frontend blendet aus statt auszugrauen; 'Im Geraet' nur auf Mobilgeraeten
(isMobile). Aktive/gespeicherte Auswahl auf einen sichtbaren Provider
geklemmt -> kein Server-422.
- 4x refresh*Preset + 3x /api/config-Fetch -> 1x refreshTtsMenu; /api/config
serverseitig 30s gecacht (spart die bisher kostenpflichtige OpenRouter-Probe
pro Seitenladen).
GPU-Erkennung zentralisiert (keine Dopplung):
- app/capabilities.py: detect_physical_gpus/resolve_gpus (nvidia-smi).
- Neues Setting GPUS (auto|none|0,1,2), beim Start an CUDA_VISIBLE_DEVICES
gekoppelt -> angezeigt = real nutzbar. admin_llm nutzt dieselbe Erkennung.
Tests: tests/test_capabilities.py (Maske, Kopplung, Menue-Sichtbarkeit, GPUs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:16:21 +02:00
|
|
|
"gpus": gpus,
|
2026-06-17 01:48:56 +02:00
|
|
|
},
|
|
|
|
|
"secrets": {
|
|
|
|
|
"openrouter_api_key_set": bool(settings.openrouter_api_key.strip()),
|
|
|
|
|
},
|
|
|
|
|
}
|
feat(menu): nicht nutzbare TTS-Provider ausblenden + GPU-Erkennung
Menue zeigt nur tatsaechlich nutzbare Vorlese-Optionen, um Verwirrung durch
folgenlose Eintraege zu vermeiden:
- Backend ist alleinige Quelle: /api/config liefert fertiges tts_menu
(Chatterbox nur wenn erreichbar, Cloud nur mit OpenRouter-TTS, Cartesia nur
mit Key). piper bleibt garantierter, immer sichtbarer Default.
- Frontend blendet aus statt auszugrauen; 'Im Geraet' nur auf Mobilgeraeten
(isMobile). Aktive/gespeicherte Auswahl auf einen sichtbaren Provider
geklemmt -> kein Server-422.
- 4x refresh*Preset + 3x /api/config-Fetch -> 1x refreshTtsMenu; /api/config
serverseitig 30s gecacht (spart die bisher kostenpflichtige OpenRouter-Probe
pro Seitenladen).
GPU-Erkennung zentralisiert (keine Dopplung):
- app/capabilities.py: detect_physical_gpus/resolve_gpus (nvidia-smi).
- Neues Setting GPUS (auto|none|0,1,2), beim Start an CUDA_VISIBLE_DEVICES
gekoppelt -> angezeigt = real nutzbar. admin_llm nutzt dieselbe Erkennung.
Tests: tests/test_capabilities.py (Maske, Kopplung, Menue-Sichtbarkeit, GPUs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:16:21 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/config")
|
|
|
|
|
async def get_config():
|
|
|
|
|
"""Zeigt aktives Profil, aufgeloeste Default-Route und verfuegbare Bausteine (gecacht)."""
|
|
|
|
|
global _CACHE, _CACHE_TS
|
|
|
|
|
if _CACHE is not None and (time.monotonic() - _CACHE_TS) < _CACHE_TTL:
|
|
|
|
|
return _CACHE
|
|
|
|
|
async with _CACHE_LOCK:
|
|
|
|
|
if _CACHE is not None and (time.monotonic() - _CACHE_TS) < _CACHE_TTL:
|
|
|
|
|
return _CACHE
|
|
|
|
|
_CACHE = await _build_config()
|
|
|
|
|
_CACHE_TS = time.monotonic()
|
|
|
|
|
return _CACHE
|