v0.11.0:0 - Speech model patches panel (lifecycle for v0.10.0 overlays)
Folds the image/parakeet_patches/apply.sh script into a one-click
dashboard action and adds drift detection so you can see at a glance
whether the parakeet-asr container has the latest Sortformer overlays
that spark-control ships.
Backend:
* image/app/speech_models.py - SpeechModelsManager: reads /health from
Parakeet, sha256s the local overlay files inside spark-control's
Docker image (/app/parakeet_patches), sha256s the same files inside
the parakeet-asr container via `docker exec ... sha256sum`, surfaces
in_sync / drift / missing status per file.
* GET /api/speech-models - status payload
* POST /api/speech-models/reapply - copies overlays into container,
verifies python syntax, restarts,
polls /health for ~120s, returns
step-by-step result
* POST /api/speech-models/restart - plain `docker restart parakeet-asr`
Dockerfile: now COPY parakeet_patches into the image at /app/parakeet_patches
so the runtime can read them. Future spark-control releases auto-carry
newer overlay versions; the panel surfaces drift after upgrade.
Frontend: new "Speech model patches" section on the dashboard with
* Status pill (in sync / drift / missing)
* Per-file SHA comparison (local vs container)
* Loaded-models pills (ASR + diarizer)
* Reapply + Restart buttons (both with confirmation modals)
* Live progress display during reapply with per-step ✓/✗
Verified post-install against the running cluster:
GET /api/speech-models shows both files in_sync (SHAs match) and both
models loaded ready on Spark 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ from .models import load_catalog
|
||||
from .nim import SUGGESTED_NIMS, CATALOG_URL, NimManager
|
||||
from .overrides import add_custom, delete_custom, extract_knobs_from_args, load_overrides, set_knobs
|
||||
from .services import docker_state, run_action, services_from_settings
|
||||
from .speech_models import SpeechModelsManager
|
||||
from .ssh import ssh_run
|
||||
from .swap import SwapManager
|
||||
from .updates import UpdateManager, get_update_status
|
||||
@@ -37,6 +38,7 @@ update_manager = UpdateManager(settings)
|
||||
hardware_probe = HardwareProbe(settings)
|
||||
nim_manager = NimManager(settings)
|
||||
deep_health = DeepHealth(settings)
|
||||
speech_models = SpeechModelsManager(settings)
|
||||
|
||||
app = FastAPI(title="spark-control", version="0.1.0")
|
||||
|
||||
@@ -495,6 +497,44 @@ async def service_action(name: str, action: str) -> dict:
|
||||
return {"name": name, "action": action, **result}
|
||||
|
||||
|
||||
# ---- Speech model patch management ----
|
||||
|
||||
@app.get("/api/speech-models")
|
||||
async def get_speech_models() -> dict:
|
||||
"""Status of the parakeet-asr container + the spark-control overlay patches
|
||||
(diarizer.py + main.py). Drift between local shipped patches and what's
|
||||
inside the container is surfaced so the UI can prompt for reapply."""
|
||||
return await speech_models.status()
|
||||
|
||||
|
||||
@app.post("/api/speech-models/reapply")
|
||||
async def post_speech_models_reapply() -> dict:
|
||||
"""Copy spark-control's shipped diarizer.py + patched main.py into the
|
||||
parakeet-asr container, verify Python syntax, restart the container, and
|
||||
wait for both models (Parakeet ASR + Sortformer) to reload. ~60–120 seconds."""
|
||||
try:
|
||||
result = await speech_models.reapply_patches()
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(409, str(e))
|
||||
if not result.get("ok"):
|
||||
# Bubble up which step failed for client-side error rendering.
|
||||
raise HTTPException(500, {"detail": "patch reapply failed", "result": result})
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/speech-models/restart")
|
||||
async def post_speech_models_restart() -> dict:
|
||||
"""`docker restart parakeet-asr` only — no file changes. Useful when the
|
||||
container's models look wedged but patches are already current."""
|
||||
try:
|
||||
result = await speech_models.restart_container()
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(409, str(e))
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(500, {"detail": "container restart failed", "result": result})
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/api/endpoints")
|
||||
async def get_endpoints() -> dict:
|
||||
"""Service-discovery summary. Stable shape; other apps on the LAN can poll this
|
||||
|
||||
Reference in New Issue
Block a user