Lightweight Deepgram-only sidecar that excludes PyTorch, faster-whisper, RealtimeSTT, and CUDA. Only includes audio capture + WebSocket streaming to Deepgram. Requires a Deepgram API key (BYOK or managed mode). Changes: - client/models.py: Extracted TranscriptionResult into standalone module so deepgram_transcription.py doesn't transitively import torch - backend/app_controller.py: Made RealtimeTranscriptionEngine and DeviceManager imports lazy (only loaded when remote.mode == "local") - local-transcription-cloud.spec: PyInstaller spec excluding all ML deps - SidecarSetup.svelte: Added "Cloud Only (Deepgram)" variant option - build-sidecar-cloud.yml: CI workflow building cloud sidecar for all 3 OS - sidecar-release.yml: Dispatches cloud build alongside CPU/CUDA builds Sidecar download options are now: - Standard (CPU): ~500 MB - local Whisper on any computer - GPU Accelerated (CUDA): ~2 GB - local Whisper with NVIDIA GPU - Cloud Only (Deepgram): ~50 MB - requires API key, no local models Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
151 lines
5.4 KiB
Python
151 lines
5.4 KiB
Python
"""Tests for backend.api_server.APIServer REST endpoints."""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
|
|
# Ensure project root is on path
|
|
project_root = Path(__file__).resolve().parent.parent.parent
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
|
|
# ── GET /api/status ─────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_status(api_client):
|
|
resp = await api_client.get("/api/status")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "state" in data
|
|
assert "is_transcribing" in data
|
|
assert "version" in data
|
|
assert "web_server" in data
|
|
|
|
|
|
# ── GET /api/config ─────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_config(api_client):
|
|
resp = await api_client.get("/api/config")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
# The config should be a dict (the raw config mapping)
|
|
assert isinstance(data, dict)
|
|
|
|
|
|
# ── PUT /api/config ─────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_put_config(api_client, controller):
|
|
"""Updating config via PUT should persist and return success."""
|
|
# Patch reload_engine to avoid heavy lifting
|
|
controller.reload_engine = MagicMock(return_value=(True, "ok"))
|
|
controller.current_model_size = "base.en"
|
|
controller.current_device_config = "auto"
|
|
controller.config.set("transcription.model", "base.en")
|
|
controller.config.set("transcription.device", "auto")
|
|
|
|
resp = await api_client.put(
|
|
"/api/config",
|
|
json={"settings": {"display.font_size": 24}},
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["status"] == "ok"
|
|
|
|
# Verify the value was actually saved
|
|
assert controller.config.get("display.font_size") == 24
|
|
|
|
|
|
# ── POST /api/start (engine not ready) ─────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_when_not_ready(api_client, controller):
|
|
"""Starting transcription without an engine should return 400."""
|
|
controller.transcription_engine = None
|
|
resp = await api_client.post("/api/start")
|
|
assert resp.status_code == 400
|
|
|
|
|
|
# ── POST /api/clear ─────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clear(api_client, controller):
|
|
from client.models import TranscriptionResult
|
|
from datetime import datetime
|
|
|
|
controller.transcriptions = [
|
|
TranscriptionResult(text="One", is_final=True, timestamp=datetime.now(), user_name="U"),
|
|
]
|
|
|
|
resp = await api_client.post("/api/clear")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["cleared"] == 1
|
|
assert len(controller.transcriptions) == 0
|
|
|
|
|
|
# ── GET /api/audio-devices ──────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_audio_devices(api_client, controller):
|
|
"""Audio devices endpoint should return a list, even when mocked."""
|
|
# Mock sounddevice so the test works without audio hardware
|
|
with patch("backend.app_controller.AppController.get_audio_devices",
|
|
return_value=[{"index": 0, "name": "Mock Mic"}]):
|
|
resp = await api_client.get("/api/audio-devices")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "devices" in data
|
|
assert len(data["devices"]) >= 1
|
|
|
|
|
|
# ── GET /api/compute-devices ────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_compute_devices(api_client, controller):
|
|
resp = await api_client.get("/api/compute-devices")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "devices" in data
|
|
# At minimum we get the "Auto-detect" entry
|
|
assert any(d["id"] == "auto" for d in data["devices"])
|
|
|
|
|
|
# ── GET /api/check-update ──────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_update(api_client, controller):
|
|
"""check-update should return a dict with an 'available' key."""
|
|
with patch.object(controller, "check_for_updates",
|
|
return_value={"available": False, "current_version": "1.0.0"}):
|
|
resp = await api_client.get("/api/check-update")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "available" in data
|
|
|
|
|
|
# ── GET /api/version ────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_version(api_client):
|
|
resp = await api_client.get("/api/version")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "version" in data
|
|
# Should be a non-empty string
|
|
assert isinstance(data["version"], str)
|
|
assert len(data["version"]) > 0
|