Backend: - tiers.py: real license validation via Heimdall (self-hosted key or cloud user_id), 30-min cache, BYOK exception for translation - session_store.py: create_session() accepts tier/user_id/window_ms/transcribe_lang/ num_speakers; tier-aware _should_reap() (Free: reap on subscriber leave, Paid: activity TTL); auto-snapshot paid sessions with history on reap; voice-ready probe on session start - services/translator.py: DeepL-backed label translation, CF-managed and BYOK key paths, per-session label cache - models/session.py, scene_event.py, service_event.py: new event types and session fields - api/snapshots.py: GET /sessions/history + POST /session/resume (Paid tier only) - api/dev.py: dev-only /dev/voice/stop|start|restart proxy to cf-orch agent - migrations/002_session_snapshots.sql: session_snapshots table Frontend: - ComposeBar.vue: pre-session settings panel (window, language, speakers, Elcor toggle) with slide transition; passes options to startSession() - stores/settings.ts: persistent user preferences (localStorage), survives page reload - stores/session.ts: voiceReady/voiceError/voiceWarning states; SceneEvent/AccentEvent/ ServiceEvent types; TTL timers for environ/queue badges; startSession() accepts opts - composables/useToneStream.ts: handlers for scene-event, accent-event, service-event SSE - NowPanel.vue: diarization speaker identity badge, acoustic scene badge, privacy risk indicator - HistoryStrip.vue: per-speaker colour chips in history strip - components/DevPanel.vue: dev-only cf-voice stop/restart controls + thread view toggle - App.vue: DevPanel in footer (DEV only), threadView flag switches v0.1.x/v0.2.x preview Tests: - tests/test_pinning.py: 20 tests covering tier-aware reaping, snapshot creation, and translator behaviour - tests/test_tiers.py: 17 tests covering key validation, cloud resolution, caching, require_paid() gate, BYOK exception - All 72 tests passing
79 lines
2.8 KiB
Python
79 lines
2.8 KiB
Python
# app/api/dev.py — dev-only endpoints for local service management
|
|
#
|
|
# Proxies stop/start calls to the cf-orch agent so the frontend dev panel
|
|
# doesn't need to know the agent URL directly.
|
|
# Not registered in cloud or demo mode.
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
from app.config import settings
|
|
|
|
router = APIRouter(prefix="/dev", tags=["dev"])
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _agent_url() -> str:
|
|
url = settings.cf_orch_agent_url
|
|
if not url:
|
|
raise HTTPException(503, detail="CF_ORCH_AGENT_URL not configured")
|
|
return url.rstrip("/")
|
|
|
|
|
|
@router.post("/voice/stop")
|
|
async def voice_stop() -> dict:
|
|
"""Stop the cf-voice sidecar via the cf-orch agent."""
|
|
agent = _agent_url()
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
try:
|
|
resp = await client.post(f"{agent}/services/cf-voice/stop", json={})
|
|
except httpx.RequestError as exc:
|
|
raise HTTPException(502, detail=f"Agent unreachable: {exc}") from exc
|
|
if resp.status_code >= 400:
|
|
raise HTTPException(resp.status_code, detail=resp.text)
|
|
logger.info("dev: cf-voice stop → %s", resp.status_code)
|
|
return {"ok": True, "action": "stop"}
|
|
|
|
|
|
@router.post("/voice/start")
|
|
async def voice_start() -> dict:
|
|
"""Start the cf-voice sidecar via the cf-orch agent."""
|
|
agent = _agent_url()
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
try:
|
|
resp = await client.post(f"{agent}/services/cf-voice/start", json={})
|
|
except httpx.RequestError as exc:
|
|
raise HTTPException(502, detail=f"Agent unreachable: {exc}") from exc
|
|
if resp.status_code >= 400:
|
|
raise HTTPException(resp.status_code, detail=resp.text)
|
|
logger.info("dev: cf-voice start → %s", resp.status_code)
|
|
return {"ok": True, "action": "start"}
|
|
|
|
|
|
@router.post("/voice/restart")
|
|
async def voice_restart() -> dict:
|
|
"""Stop then start cf-voice via the cf-orch agent."""
|
|
import asyncio
|
|
|
|
agent = _agent_url()
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
try:
|
|
await client.post(f"{agent}/services/cf-voice/stop", json={})
|
|
except httpx.RequestError as exc:
|
|
raise HTTPException(502, detail=f"Agent unreachable on stop: {exc}") from exc
|
|
|
|
# Give the process time to fully exit before start fires
|
|
await asyncio.sleep(2.0)
|
|
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
try:
|
|
resp = await client.post(f"{agent}/services/cf-voice/start", json={})
|
|
except httpx.RequestError as exc:
|
|
raise HTTPException(502, detail=f"Agent unreachable on start: {exc}") from exc
|
|
if resp.status_code >= 400:
|
|
raise HTTPException(resp.status_code, detail=resp.text)
|
|
logger.info("dev: cf-voice restart complete")
|
|
return {"ok": True, "action": "restart"}
|