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>
This commit is contained in:
parent
6bf281fc30
commit
5e128a74ce
7 changed files with 333 additions and 114 deletions
|
|
@ -1,7 +1,11 @@
|
|||
import asyncio
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.config import settings, active_profile
|
||||
from app.capabilities import resolve_gpus
|
||||
from app.providers.tts.cartesia import _UNSUPPORTED_LANGS as _CARTESIA_UNSUPPORTED
|
||||
from app.dependencies import (
|
||||
resolve_route,
|
||||
|
|
@ -13,6 +17,13 @@ from app.dependencies import (
|
|||
|
||||
router = APIRouter()
|
||||
|
||||
# /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()
|
||||
|
||||
|
||||
async def _chatterbox_reachable() -> bool:
|
||||
try:
|
||||
|
|
@ -62,16 +73,31 @@ async def _openrouter_tts_available() -> bool:
|
|||
return False
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
async def get_config():
|
||||
"""Zeigt aktives Profil, aufgeloeste Default-Route und verfuegbare Bausteine."""
|
||||
async def _build_config() -> dict:
|
||||
"""Baut die /api/config-Antwort (eine Quelle der Wahrheit für die Menü-Sichtbarkeit)."""
|
||||
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()
|
||||
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},
|
||||
]
|
||||
return {
|
||||
"profile": active_profile(),
|
||||
"app_env": settings.app_env,
|
||||
"default_route": route.as_dict(),
|
||||
"tts_menu": tts_menu,
|
||||
"available": {
|
||||
"stt_providers": sorted(STT_REGISTRY),
|
||||
"llm_providers": sorted(LLM_REGISTRY),
|
||||
|
|
@ -82,8 +108,23 @@ async def get_config():
|
|||
"openrouter_tts_available": openrouter_tts_ok,
|
||||
"cartesia_available": cartesia_ok,
|
||||
"cartesia_unsupported_languages": sorted(_CARTESIA_UNSUPPORTED),
|
||||
"gpus": gpus,
|
||||
},
|
||||
"secrets": {
|
||||
"openrouter_api_key_set": bool(settings.openrouter_api_key.strip()),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue