feat: default whisper-server builder fields

parent ee45a78c
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
"""Admin dashboard routes.""" """Admin dashboard routes."""
from pathlib import Path from pathlib import Path
import re
import shutil
from typing import Optional from typing import Optional
from fastapi import APIRouter, Request, Response, Form, HTTPException, Depends from fastapi import APIRouter, Request, Response, Form, HTTPException, Depends
...@@ -54,6 +56,25 @@ def set_config_manager(mgr): ...@@ -54,6 +56,25 @@ def set_config_manager(mgr):
config_manager = mgr config_manager = mgr
def _next_whisper_server_model_id(audio_models) -> str:
used_suffixes = set()
for model in audio_models or []:
if not isinstance(model, dict) or model.get("backend") != "whisper-server":
continue
match = re.fullmatch(r"whisper(\d+)", str(model.get("id") or "").strip())
if match:
used_suffixes.add(int(match.group(1)))
suffix = 0
while suffix in used_suffixes:
suffix += 1
return f"whisper{suffix}"
def _default_whisper_server_path() -> str:
return shutil.which("whisper-server") or "/usr/local/bin/whisper-server"
def get_current_user(request: Request) -> Optional[str]: def get_current_user(request: Request) -> Optional[str]:
"""Get the current logged-in user from session cookie.""" """Get the current logged-in user from session cookie."""
if session_manager is None: if session_manager is None:
...@@ -1257,10 +1278,10 @@ async def api_model_configure(request: Request, username: str = Depends(require_ ...@@ -1257,10 +1278,10 @@ async def api_model_configure(request: Request, username: str = Depends(require_
if data.get("backend") == "whisper-server": if data.get("backend") == "whisper-server":
model_id = (data.get("model_id") or "").strip() model_id = (data.get("model_id") or "").strip()
if not model_id: if not model_id:
raise HTTPException(status_code=400, detail="model_id is required") model_id = _next_whisper_server_model_id(config_manager.models_data.get("audio_models", []))
server_path = (data.get("server_path") or "").strip() server_path = (data.get("server_path") or "").strip()
if not server_path: if not server_path:
raise HTTPException(status_code=400, detail="server_path is required") server_path = _default_whisper_server_path()
model_source = (data.get("model_source") or "cached-gguf").strip() or "cached-gguf" model_source = (data.get("model_source") or "cached-gguf").strip() or "cached-gguf"
if model_source not in {"cached-gguf", "manual-path"}: if model_source not in {"cached-gguf", "manual-path"}:
raise HTTPException(status_code=400, detail="model_source must be one of: cached-gguf, manual-path") raise HTTPException(status_code=400, detail="model_source must be one of: cached-gguf, manual-path")
......
...@@ -2,7 +2,7 @@ from pathlib import Path ...@@ -2,7 +2,7 @@ from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
import pytest import pytest
from fastapi import HTTPException from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
...@@ -49,15 +49,19 @@ def _build_config(tmp_path): ...@@ -49,15 +49,19 @@ def _build_config(tmp_path):
return cfg return cfg
def _build_admin_test_client(routes):
app = FastAPI()
app.include_router(routes.router)
app.dependency_overrides[routes.require_admin] = lambda: "admin"
return app, TestClient(app)
def test_model_configure_persists_whisper_server_audio_model(monkeypatch, tmp_path): def test_model_configure_persists_whisper_server_audio_model(monkeypatch, tmp_path):
from codai.admin import routes from codai.admin import routes
from codai.api.app import app
cfg = _build_config(tmp_path) cfg = _build_config(tmp_path)
monkeypatch.setattr(routes, "config_manager", cfg, raising=False) monkeypatch.setattr(routes, "config_manager", cfg, raising=False)
app.dependency_overrides[routes.require_admin] = lambda: "admin" app, client = _build_admin_test_client(routes)
client = TestClient(app)
response = client.post( response = client.post(
"/admin/api/model-configure", "/admin/api/model-configure",
json={ json={
...@@ -94,7 +98,6 @@ def test_model_configure_persists_whisper_server_audio_model(monkeypatch, tmp_pa ...@@ -94,7 +98,6 @@ def test_model_configure_persists_whisper_server_audio_model(monkeypatch, tmp_pa
def test_model_configure_rejects_duplicate_whisper_server_model_id(monkeypatch, tmp_path): def test_model_configure_rejects_duplicate_whisper_server_model_id(monkeypatch, tmp_path):
from codai.admin import routes from codai.admin import routes
from codai.api.app import app
cfg = _build_config(tmp_path) cfg = _build_config(tmp_path)
cfg.models_data["audio_models"] = [ cfg.models_data["audio_models"] = [
...@@ -109,9 +112,7 @@ def test_model_configure_rejects_duplicate_whisper_server_model_id(monkeypatch, ...@@ -109,9 +112,7 @@ def test_model_configure_rejects_duplicate_whisper_server_model_id(monkeypatch,
} }
] ]
monkeypatch.setattr(routes, "config_manager", cfg, raising=False) monkeypatch.setattr(routes, "config_manager", cfg, raising=False)
app.dependency_overrides[routes.require_admin] = lambda: "admin" app, client = _build_admin_test_client(routes)
client = TestClient(app)
response = client.post( response = client.post(
"/admin/api/model-configure", "/admin/api/model-configure",
json={ json={
...@@ -132,15 +133,95 @@ def test_model_configure_rejects_duplicate_whisper_server_model_id(monkeypatch, ...@@ -132,15 +133,95 @@ def test_model_configure_rejects_duplicate_whisper_server_model_id(monkeypatch,
app.dependency_overrides.clear() app.dependency_overrides.clear()
def test_model_configure_accepts_cached_gguf_whisper_server_model(monkeypatch, tmp_path): def test_model_configure_defaults_missing_whisper_server_model_id_to_whisper0(monkeypatch, tmp_path):
from codai.admin import routes from codai.admin import routes
from codai.api.app import app
cfg = _build_config(tmp_path) cfg = _build_config(tmp_path)
monkeypatch.setattr(routes, "config_manager", cfg, raising=False) monkeypatch.setattr(routes, "config_manager", cfg, raising=False)
app.dependency_overrides[routes.require_admin] = lambda: "admin" app, client = _build_admin_test_client(routes)
response = client.post(
"/admin/api/model-configure",
json={
"model_id": "",
"model_type": "audio_models",
"backend": "whisper-server",
"server_path": "/usr/local/bin/whisper-server",
"model_path": "/models/ggml-base.bin",
"port": 8744,
"gpu_device": 0,
"load_mode": "on-request",
},
)
client = TestClient(app) assert response.status_code == 200
assert cfg.models_data["audio_models"][0]["id"] == "whisper0"
app.dependency_overrides.clear()
def test_model_configure_defaults_missing_whisper_server_model_id_to_smallest_free_suffix(monkeypatch, tmp_path):
from codai.admin import routes
cfg = _build_config(tmp_path)
cfg.models_data["audio_models"] = [
{"id": "whisper0", "backend": "whisper-server"},
{"id": "whisper1", "backend": "whisper-server"},
{"id": "whisper3", "backend": "whisper-server"},
]
monkeypatch.setattr(routes, "config_manager", cfg, raising=False)
app, client = _build_admin_test_client(routes)
response = client.post(
"/admin/api/model-configure",
json={
"model_type": "audio_models",
"backend": "whisper-server",
"server_path": "/usr/local/bin/whisper-server",
"model_path": "/models/ggml-small.bin",
"port": 8745,
"gpu_device": 1,
"load_mode": "load",
},
)
assert response.status_code == 200
assert cfg.models_data["audio_models"][-1]["id"] == "whisper2"
app.dependency_overrides.clear()
def test_model_configure_defaults_missing_whisper_server_path_to_usr_local_bin(monkeypatch, tmp_path):
from codai.admin import routes
cfg = _build_config(tmp_path)
monkeypatch.setattr(routes, "config_manager", cfg, raising=False)
monkeypatch.setattr(routes.shutil, "which", lambda _: None)
app, client = _build_admin_test_client(routes)
response = client.post(
"/admin/api/model-configure",
json={
"model_id": "whisper-vulkan-base",
"model_type": "audio_models",
"backend": "whisper-server",
"server_path": "",
"model_path": "/models/ggml-base.bin",
"port": 8744,
"gpu_device": 0,
"load_mode": "on-request",
},
)
assert response.status_code == 200
assert cfg.models_data["audio_models"][0]["server_path"] == "/usr/local/bin/whisper-server"
app.dependency_overrides.clear()
def test_model_configure_accepts_cached_gguf_whisper_server_model(monkeypatch, tmp_path):
from codai.admin import routes
cfg = _build_config(tmp_path)
monkeypatch.setattr(routes, "config_manager", cfg, raising=False)
app, client = _build_admin_test_client(routes)
response = client.post( response = client.post(
"/admin/api/model-configure", "/admin/api/model-configure",
json={ json={
...@@ -176,13 +257,10 @@ def test_model_configure_accepts_cached_gguf_whisper_server_model(monkeypatch, t ...@@ -176,13 +257,10 @@ def test_model_configure_accepts_cached_gguf_whisper_server_model(monkeypatch, t
def test_model_configure_defaults_missing_whisper_server_model_source_to_cached_gguf(monkeypatch, tmp_path): def test_model_configure_defaults_missing_whisper_server_model_source_to_cached_gguf(monkeypatch, tmp_path):
from codai.admin import routes from codai.admin import routes
from codai.api.app import app
cfg = _build_config(tmp_path) cfg = _build_config(tmp_path)
monkeypatch.setattr(routes, "config_manager", cfg, raising=False) monkeypatch.setattr(routes, "config_manager", cfg, raising=False)
app.dependency_overrides[routes.require_admin] = lambda: "admin" app, client = _build_admin_test_client(routes)
client = TestClient(app)
response = client.post( response = client.post(
"/admin/api/model-configure", "/admin/api/model-configure",
json={ json={
...@@ -217,13 +295,10 @@ def test_model_configure_defaults_missing_whisper_server_model_source_to_cached_ ...@@ -217,13 +295,10 @@ def test_model_configure_defaults_missing_whisper_server_model_source_to_cached_
def test_model_configure_rejects_cached_gguf_whisper_server_without_model_path(monkeypatch, tmp_path): def test_model_configure_rejects_cached_gguf_whisper_server_without_model_path(monkeypatch, tmp_path):
from codai.admin import routes from codai.admin import routes
from codai.api.app import app
cfg = _build_config(tmp_path) cfg = _build_config(tmp_path)
monkeypatch.setattr(routes, "config_manager", cfg, raising=False) monkeypatch.setattr(routes, "config_manager", cfg, raising=False)
app.dependency_overrides[routes.require_admin] = lambda: "admin" app, client = _build_admin_test_client(routes)
client = TestClient(app)
response = client.post( response = client.post(
"/admin/api/model-configure", "/admin/api/model-configure",
json={ json={
...@@ -247,13 +322,10 @@ def test_model_configure_rejects_cached_gguf_whisper_server_without_model_path(m ...@@ -247,13 +322,10 @@ def test_model_configure_rejects_cached_gguf_whisper_server_without_model_path(m
def test_model_configure_rejects_manual_path_whisper_server_without_model_path(monkeypatch, tmp_path): def test_model_configure_rejects_manual_path_whisper_server_without_model_path(monkeypatch, tmp_path):
from codai.admin import routes from codai.admin import routes
from codai.api.app import app
cfg = _build_config(tmp_path) cfg = _build_config(tmp_path)
monkeypatch.setattr(routes, "config_manager", cfg, raising=False) monkeypatch.setattr(routes, "config_manager", cfg, raising=False)
app.dependency_overrides[routes.require_admin] = lambda: "admin" app, client = _build_admin_test_client(routes)
client = TestClient(app)
response = client.post( response = client.post(
"/admin/api/model-configure", "/admin/api/model-configure",
json={ json={
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment