Surfaces a routing verdict for the CURRENTLY-SELECTED TTS engine in the two system-health surfaces, so a CPU fallback / unavailable-GPU is heard about before a slow or failed synth — the no-silent-fallback contract, read-only. - `tts_backend.active_routing()` + `gpu_routing_verdict()`: the active engine's routing derived from list_backends() (byte-identical to the matrix) plus the host compute summary (family + VRAM from the canonical probe). Never raise. - `/system/diagnose` gains a `gpu_routing` check: accelerated→ok, accelerated-with-caveat / cpu_fallback→warn (+ actionable hint), cpu_only→ok (no-GPU host is the expected normal state — never noise-warns), unavailable→ fail, no-engine→warn. ASCII-safe detail strings (the text dump enforces ASCII). - `/setup/preflight` gains an "Active engine routing" check + an explicit `gpu_routing` object on PreflightResponse (a real field — the response has no extra="allow", so it would otherwise be dropped). `device` gains `gpu_family` (ROCm-vs-CUDA aware) + `vram_gb`. New `GpuRouting` schema. Tests: gpu_routing_verdict (host + active-engine + degraded), diagnose status mapping across all 6 states + never-raises, preflight gpu_routing object + check + device.gpu_family. Existing diagnose/preflight tests stay green (checks are additive; the report's top-level key set is unchanged). Deferred (documented): synth-time routing headers/WS-frames at the 3 synth entry points. Selection is already hard-gated (PR 3 select_engine), and the matrix (PR 5) + this preflight/diagnose verdict surface the situation — the synth-time signal is incremental belt-and-suspenders for the env-var-pinned edge and is best validated interactively. Tracked as a #21 follow-up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
213 lines
6.8 KiB
Python
213 lines
6.8 KiB
Python
"""Pydantic v2 schemas for request/response validation.
|
||
|
||
Shared across routers — import from here rather than defining inline.
|
||
Using ``model_config = ConfigDict(...)`` for Pydantic v2 compat.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
|
||
|
||
# ── System ────────────────────────────────────────────────────────────────
|
||
|
||
class SysinfoResponse(BaseModel):
|
||
"""GET /sysinfo"""
|
||
model_config = ConfigDict(extra="allow")
|
||
|
||
cpu: float = Field(description="CPU usage percentage (0–100)")
|
||
ram: float = Field(description="Used RAM in GiB")
|
||
total_ram: float = Field(description="Total RAM in GiB")
|
||
vram: float = Field(0.0, description="Used VRAM in GiB")
|
||
gpu_active: bool = Field(False, description="Whether a GPU is actively used")
|
||
|
||
|
||
class SystemInfoResponse(BaseModel):
|
||
"""GET /system/info"""
|
||
model_config = ConfigDict(extra="allow")
|
||
|
||
app_version: str = ""
|
||
data_dir: str
|
||
outputs_dir: str
|
||
crash_log_path: str
|
||
idle_timeout_seconds: int
|
||
model_checkpoint: str = "unknown"
|
||
asr_model: str = "unknown"
|
||
translate_provider: str = "unknown"
|
||
has_hf_token: bool = False
|
||
# Xet fast-download backend state (FDL-03): {xet_enabled, xet_version, high_performance}
|
||
fast_download: dict | None = None
|
||
device: str = "cpu"
|
||
python: str = ""
|
||
platform: str = ""
|
||
arch: str = ""
|
||
os_version: str = ""
|
||
cpu_model: str = ""
|
||
cpu_count: int = 0
|
||
ram_total_gb: float = 0.0
|
||
gpu_name: str = ""
|
||
vram_total_gb: float = 0.0
|
||
disk_free_gb: float = 0.0
|
||
error: str | None = None
|
||
ffmpeg_ok: bool = False
|
||
ffmpeg_path: str = ""
|
||
proxy_url: str = ""
|
||
share_enabled: bool = False
|
||
share_port: int | None = None
|
||
lan_addresses: list[str] = []
|
||
pin_required: bool = False
|
||
backend_port: int = 3900
|
||
share_port_base: int = 3901
|
||
ui_port: int = 3901
|
||
|
||
|
||
class ModelStatusResponse(BaseModel):
|
||
"""GET /model/status"""
|
||
model_config = ConfigDict(extra="allow")
|
||
|
||
status: str = Field(description="idle | loading | ready")
|
||
checkpoint: str | None = None
|
||
loaded_at: str | None = None
|
||
sub_stage: str | None = Field(None, description="Current loading sub-stage: importing | loading_weights | loading_asr | compiling | ready | error")
|
||
detail: str | None = Field(None, description="Human-readable detail of current loading phase")
|
||
error: str | None = Field(None, description="Error message if loading failed")
|
||
|
||
|
||
class LogsResponse(BaseModel):
|
||
"""GET /system/logs"""
|
||
lines: list[str] = Field(default_factory=list)
|
||
path: str = ""
|
||
exists: bool = False
|
||
total_lines: int = 0
|
||
error: str | None = None
|
||
candidates: list[str] | None = None
|
||
|
||
|
||
class FlushMemoryResponse(BaseModel):
|
||
"""POST /system/flush-memory"""
|
||
flushed: bool = True
|
||
unloaded_model: bool = False
|
||
ram_after: float = 0.0
|
||
vram_after: float = 0.0
|
||
|
||
|
||
# ── Setup ─────────────────────────────────────────────────────────────────
|
||
|
||
class MissingModel(BaseModel):
|
||
repo_id: str
|
||
label: str
|
||
|
||
|
||
class SetupStatusResponse(BaseModel):
|
||
"""GET /setup/status"""
|
||
models_ready: bool
|
||
missing: list[MissingModel] = Field(default_factory=list)
|
||
hf_cache_dir: str
|
||
disk_free_gb: float
|
||
min_free_gb: int = 10
|
||
enough_disk: bool = True
|
||
|
||
|
||
class PreflightCheck(BaseModel):
|
||
"""One check in the preflight report."""
|
||
model_config = ConfigDict(extra="allow")
|
||
|
||
id: str
|
||
label: str
|
||
status: str = Field(description="pass | warn | fail")
|
||
detail: str = ""
|
||
fix: str | None = None
|
||
|
||
|
||
class DeviceInfo(BaseModel):
|
||
"""GPU/system device info from preflight."""
|
||
model_config = ConfigDict(extra="allow")
|
||
|
||
os: str
|
||
arch: str
|
||
gpu_vendor: str = "none"
|
||
gpu_backend: str = "cpu"
|
||
gpu_available: bool = False
|
||
gpu_driver: str | None = None
|
||
gpu_device_name: str | None = None
|
||
# From the canonical device probe (core.device_caps) — distinguishes ROCm
|
||
# from CUDA, unlike the legacy nvidia-smi-based gpu_vendor/gpu_backend.
|
||
gpu_family: str = "cpu"
|
||
vram_gb: float = 0.0
|
||
ram_gb: float = 0.0
|
||
disk_free_gb: float = 0.0
|
||
|
||
|
||
class GpuRouting(BaseModel):
|
||
"""Routing verdict for the active TTS engine on THIS host (#21).
|
||
|
||
Distinct from the per-engine `routing_*` keys in `/engines`: this is the
|
||
single verdict for the *currently-selected* engine, surfaced in preflight +
|
||
diagnose so the user hears about a CPU fallback / unavailable GPU before a
|
||
slow or failed synth — no silent CPU fallback.
|
||
"""
|
||
model_config = ConfigDict(extra="allow")
|
||
|
||
engine: str | None = None # active TTS engine id
|
||
effective_device: str | None = None # device it will actually use here
|
||
routing_status: str | None = None # accelerated|cpu_fallback|cpu_only|unavailable|none
|
||
routing_reason: str | None = None # scrubbed; null when none
|
||
host_family: str = "cpu" # detect_host_caps().family
|
||
vram_gb: float = 0.0
|
||
|
||
|
||
class PreflightResponse(BaseModel):
|
||
"""GET /setup/preflight"""
|
||
ok: bool
|
||
has_warnings: bool = False
|
||
checks: list[PreflightCheck] = Field(default_factory=list)
|
||
device: DeviceInfo
|
||
# Explicit field (PreflightResponse has no extra="allow") so the verdict
|
||
# survives serialization instead of being silently dropped.
|
||
gpu_routing: GpuRouting | None = None
|
||
|
||
|
||
class InstallModelRequest(BaseModel):
|
||
"""POST /models/install"""
|
||
repo_id: str
|
||
|
||
|
||
class DeleteModelResponse(BaseModel):
|
||
"""DELETE /models/{repo_id}"""
|
||
deleted: bool = True
|
||
repo_id: str
|
||
freed_bytes: int = 0
|
||
|
||
|
||
# ── Models list ───────────────────────────────────────────────────────────
|
||
|
||
class ModelEntry(BaseModel):
|
||
"""One model in the GET /models response."""
|
||
model_config = ConfigDict(extra="allow")
|
||
|
||
repo_id: str
|
||
label: str
|
||
role: str
|
||
size: str = ""
|
||
required: bool = False
|
||
installed: bool = False
|
||
supported: bool = True
|
||
size_on_disk: int | None = None
|
||
nb_files: int | None = None
|
||
|
||
|
||
# ── Effect presets ─────────────────────────────────────────────────────
|
||
|
||
class EffectPresetEntry(BaseModel):
|
||
"""One DSP effect preset."""
|
||
model_config = ConfigDict(extra="allow")
|
||
|
||
id: str
|
||
label: str
|
||
icon: str
|
||
description: str
|
||
|
||
|
||
class EffectPresetsResponse(BaseModel):
|
||
"""GET /engines/effects/presets"""
|
||
presets: list[EffectPresetEntry] = Field(default_factory=list)
|