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
117
tests/test_capabilities.py
Normal file
117
tests/test_capabilities.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
"""GPU-Erkennung/-Maske und das daraus abgeleitete TTS-Menü in /api/config (offline)."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import app.capabilities as cap
|
||||
import app.api.config as cfgmod
|
||||
from app.config import settings, _parse_gpu_indices, _apply_gpu_visibility
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def _gpus(*indices):
|
||||
async def fake():
|
||||
return [
|
||||
{"index": i, "name": f"GPU{i}", "used_mib": 0, "total_mib": 1, "percent": 0}
|
||||
for i in indices
|
||||
]
|
||||
return fake
|
||||
|
||||
|
||||
# ── Maske/Parsing ────────────────────────────────────────────────────────────
|
||||
|
||||
def test_parse_gpu_indices():
|
||||
assert _parse_gpu_indices("0, 2 ,1") == [0, 1, 2]
|
||||
assert _parse_gpu_indices("") == []
|
||||
assert _parse_gpu_indices("auto") == []
|
||||
|
||||
|
||||
def test_apply_gpu_visibility_none(monkeypatch):
|
||||
monkeypatch.setattr(settings, "gpus", "none")
|
||||
monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False)
|
||||
_apply_gpu_visibility()
|
||||
assert os.environ["CUDA_VISIBLE_DEVICES"] == ""
|
||||
|
||||
|
||||
def test_apply_gpu_visibility_explicit_sets_cuda(monkeypatch):
|
||||
monkeypatch.setattr(settings, "gpus", "2,0")
|
||||
monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False)
|
||||
_apply_gpu_visibility()
|
||||
assert os.environ["CUDA_VISIBLE_DEVICES"] == "0,2"
|
||||
|
||||
|
||||
def test_apply_gpu_visibility_auto_keeps_existing(monkeypatch):
|
||||
monkeypatch.setattr(settings, "gpus", "auto")
|
||||
monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "1")
|
||||
_apply_gpu_visibility()
|
||||
assert os.environ["CUDA_VISIBLE_DEVICES"] == "1" # unveraendert
|
||||
|
||||
|
||||
# ── resolve_gpus ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_resolve_gpus_none(monkeypatch):
|
||||
monkeypatch.setattr(settings, "gpus", "none")
|
||||
assert asyncio.run(cap.resolve_gpus()) == []
|
||||
|
||||
|
||||
def test_resolve_gpus_auto_all(monkeypatch):
|
||||
monkeypatch.setattr(settings, "gpus", "auto")
|
||||
monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False)
|
||||
monkeypatch.setattr(cap, "detect_physical_gpus", _gpus(0, 1))
|
||||
assert [g["index"] for g in asyncio.run(cap.resolve_gpus())] == [0, 1]
|
||||
|
||||
|
||||
def test_resolve_gpus_explicit_subset(monkeypatch):
|
||||
monkeypatch.setattr(settings, "gpus", "0,2")
|
||||
monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False)
|
||||
monkeypatch.setattr(cap, "detect_physical_gpus", _gpus(0, 1, 2))
|
||||
assert [g["index"] for g in asyncio.run(cap.resolve_gpus())] == [0, 2]
|
||||
|
||||
|
||||
def test_resolve_gpus_intersects_cuda_mask(monkeypatch):
|
||||
monkeypatch.setattr(settings, "gpus", "auto")
|
||||
monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "1")
|
||||
monkeypatch.setattr(cap, "detect_physical_gpus", _gpus(0, 1))
|
||||
assert [g["index"] for g in asyncio.run(cap.resolve_gpus())] == [1]
|
||||
|
||||
|
||||
# ── /api/config: Menü-Sichtbarkeit + gpus ────────────────────────────────────
|
||||
|
||||
def _patch_probes(monkeypatch, *, chatterbox, openrouter, cartesia, gpus):
|
||||
async def _t():
|
||||
return True
|
||||
async def _f():
|
||||
return False
|
||||
async def _gpu():
|
||||
return [{"index": i, "name": f"GPU{i}"} for i in gpus]
|
||||
monkeypatch.setattr(cfgmod, "_chatterbox_reachable", _t if chatterbox else _f)
|
||||
monkeypatch.setattr(cfgmod, "_openrouter_tts_available", _t if openrouter else _f)
|
||||
monkeypatch.setattr(cfgmod, "_cartesia_available", _t if cartesia else _f)
|
||||
monkeypatch.setattr(cfgmod, "resolve_gpus", _gpu)
|
||||
monkeypatch.setattr(cfgmod, "_CACHE", None) # Cache je Test zuruecksetzen
|
||||
monkeypatch.setattr(cfgmod, "_CACHE_TS", 0.0)
|
||||
|
||||
|
||||
def test_config_tts_menu_hides_unavailable(monkeypatch):
|
||||
_patch_probes(monkeypatch, chatterbox=False, openrouter=False, cartesia=True, gpus=[])
|
||||
data = client.get("/api/config").json()
|
||||
menu = {m["provider"]: m for m in data["tts_menu"]}
|
||||
assert menu["piper"]["visible"] is True # garantierter Default
|
||||
assert menu["chatterbox"]["visible"] is False # nicht erreichbar -> aus
|
||||
assert menu["openrouter"]["visible"] is False # kein TTS-Modell -> aus
|
||||
assert menu["cartesia"]["visible"] is True
|
||||
assert menu["device"]["client_only"] is True # Mobil-Regel macht das Frontend
|
||||
assert data["available"]["gpus"] == []
|
||||
|
||||
|
||||
def test_config_reports_gpus(monkeypatch):
|
||||
_patch_probes(monkeypatch, chatterbox=True, openrouter=True, cartesia=True, gpus=[0, 1])
|
||||
data = client.get("/api/config").json()
|
||||
assert [g["index"] for g in data["available"]["gpus"]] == [0, 1]
|
||||
assert {m["provider"] for m in data["tts_menu"]} == {
|
||||
"piper", "chatterbox", "cartesia", "openrouter", "device"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue