Standalone Shonar Desktop: vendor portable sources + local engine; decouple from ~/Projects/Shonar
- shared/ = portable Android-origin sources vendored from deferred/desktop-server (app/build.gradle.kts srcDir repointed; PlaybackController.kt excluded as Android-only) - backend/ = bundled-lite engine (SQLite + inline queue); .venv symlinked from the old checkout, PYTHONPATH pins THIS backend's code over any editable install - repoRoot() resolves this project dir (env SHONAR_REPO still wins); desktop-dev.sh watches shared/ + backend/ - Verified: :app:compileKotlin + :app:test green (23 tests); engine boots on :8010, self-migrates, /healthz ok
This commit is contained in:
commit
76c867fca4
136 changed files with 21099 additions and 0 deletions
132
backend/shonar/api/v1/models.py
Normal file
132
backend/shonar/api/v1/models.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
"""Transcription model registry + global default (Stage 1).
|
||||
|
||||
- GET /models — supported models with display metadata, download and
|
||||
availability status, and which is the default.
|
||||
- GET /models/default — the current global default model name.
|
||||
- PUT /models/default — change the global default (affects future
|
||||
recordings only; saved per-recording models are never rewritten).
|
||||
- POST /models/{name}/download — fetch a model into the local cache
|
||||
(needs internet once; runs synchronously and may take minutes for
|
||||
large models).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from shonar.api.deps import CurrentUser, SessionDep
|
||||
from shonar.services.ai import ProviderConfigError
|
||||
from shonar.services.ai.model_registry import (
|
||||
SUPPORTED_TRANSCRIPTION_MODELS,
|
||||
get_global_default_model,
|
||||
is_faster_whisper_installed,
|
||||
is_model_downloaded,
|
||||
set_global_default_model,
|
||||
validate_model_name,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["models"])
|
||||
|
||||
|
||||
class TranscriptionModelOut(BaseModel):
|
||||
name: str
|
||||
display_name: str
|
||||
description: str
|
||||
params: str
|
||||
approx_memory: str
|
||||
relative_speed: str
|
||||
is_default: bool
|
||||
downloaded: bool
|
||||
available: bool
|
||||
|
||||
|
||||
class ModelsOut(BaseModel):
|
||||
default_model: str
|
||||
faster_whisper_installed: bool
|
||||
models: list[TranscriptionModelOut]
|
||||
|
||||
|
||||
class DefaultModelUpdate(BaseModel):
|
||||
model: str = Field(min_length=1, max_length=32)
|
||||
|
||||
|
||||
class DefaultModelOut(BaseModel):
|
||||
default_model: str
|
||||
|
||||
|
||||
async def _models_out(session: SessionDep) -> ModelsOut:
|
||||
default = await get_global_default_model(session)
|
||||
installed = is_faster_whisper_installed()
|
||||
return ModelsOut(
|
||||
default_model=default,
|
||||
faster_whisper_installed=installed,
|
||||
models=[
|
||||
TranscriptionModelOut(
|
||||
name=info.name,
|
||||
display_name=info.display_name,
|
||||
description=info.description,
|
||||
params=info.params,
|
||||
approx_memory=info.approx_memory,
|
||||
relative_speed=info.relative_speed,
|
||||
is_default=info.name == default,
|
||||
downloaded=is_model_downloaded(info.name),
|
||||
available=installed and is_model_downloaded(info.name),
|
||||
)
|
||||
for info in SUPPORTED_TRANSCRIPTION_MODELS.values()
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/models", response_model=ModelsOut)
|
||||
async def list_models(user: CurrentUser, session: SessionDep):
|
||||
return await _models_out(session)
|
||||
|
||||
|
||||
@router.get("/models/default", response_model=DefaultModelOut)
|
||||
async def get_default_model(user: CurrentUser, session: SessionDep):
|
||||
return DefaultModelOut(default_model=await get_global_default_model(session))
|
||||
|
||||
|
||||
@router.put("/models/default", response_model=DefaultModelOut)
|
||||
async def put_default_model(body: DefaultModelUpdate, user: CurrentUser, session: SessionDep):
|
||||
try:
|
||||
name = await set_global_default_model(session, body.model)
|
||||
except ProviderConfigError as e:
|
||||
raise HTTPException(422, str(e)) from None
|
||||
return DefaultModelOut(default_model=name)
|
||||
|
||||
|
||||
@router.post("/models/{name}/download", response_model=TranscriptionModelOut)
|
||||
async def download_model(name: str, user: CurrentUser, session: SessionDep):
|
||||
try:
|
||||
clean = validate_model_name(name)
|
||||
except ProviderConfigError as e:
|
||||
raise HTTPException(422, str(e)) from None
|
||||
if not is_faster_whisper_installed():
|
||||
raise HTTPException(
|
||||
501,
|
||||
"faster-whisper is not installed on this server "
|
||||
"(pip install shonar-backend[faster-whisper]).",
|
||||
)
|
||||
from shonar.services.ai.faster_whisper import _load_model
|
||||
|
||||
try:
|
||||
await asyncio.to_thread(_load_model, clean)
|
||||
except Exception as e: # noqa: BLE001 — surface download failures plainly
|
||||
raise HTTPException(500, f"Model download failed: {e}") from None
|
||||
default = await get_global_default_model(session)
|
||||
info = SUPPORTED_TRANSCRIPTION_MODELS[clean]
|
||||
return TranscriptionModelOut(
|
||||
name=info.name,
|
||||
display_name=info.display_name,
|
||||
description=info.description,
|
||||
params=info.params,
|
||||
approx_memory=info.approx_memory,
|
||||
relative_speed=info.relative_speed,
|
||||
is_default=info.name == default,
|
||||
downloaded=is_model_downloaded(clean),
|
||||
available=is_model_downloaded(clean),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue