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
84 lines
2.8 KiB
Python
84 lines
2.8 KiB
Python
# app/main.py — Linnet FastAPI application factory
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
from app.api import audio, corrections, dev, events, export, history, samples, sessions, snapshots
|
|
from app.config import settings
|
|
from app.services import session_store
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)s %(name)s — %(message)s",
|
|
)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# Startup: begin idle session reaper
|
|
session_store.start_reaper()
|
|
yield
|
|
# Shutdown: cancel the reaper cleanly
|
|
await session_store.stop_reaper()
|
|
|
|
|
|
app = FastAPI(
|
|
title="Linnet",
|
|
description="Real-time tone annotation — tonal subtext labels for ND/autistic users",
|
|
version="0.1.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
# ── Mode middleware (applied before CORS so headers are always present) ──────
|
|
if settings.demo_mode:
|
|
from app.middleware.demo import DemoModeMiddleware
|
|
app.add_middleware(DemoModeMiddleware)
|
|
logging.getLogger(__name__).info("DEMO_MODE active")
|
|
|
|
if settings.cloud_mode:
|
|
from app.middleware.cloud import CloudAuthMiddleware
|
|
app.add_middleware(CloudAuthMiddleware)
|
|
logging.getLogger(__name__).info("CLOUD_MODE active")
|
|
|
|
# ── CORS ─────────────────────────────────────────────────────────────────────
|
|
_frontend_port = str(settings.linnet_frontend_port)
|
|
_origins = [
|
|
f"http://localhost:{_frontend_port}",
|
|
f"http://127.0.0.1:{_frontend_port}",
|
|
]
|
|
if settings.cloud_mode:
|
|
_origins += [
|
|
"https://menagerie.circuitforge.tech",
|
|
"https://circuitforge.tech",
|
|
]
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# ── Routers ───────────────────────────────────────────────────────────────────
|
|
app.include_router(sessions.router)
|
|
app.include_router(events.router)
|
|
if not settings.demo_mode and not settings.cloud_mode:
|
|
app.include_router(dev.router)
|
|
app.include_router(history.router)
|
|
app.include_router(audio.router)
|
|
app.include_router(export.router)
|
|
app.include_router(samples.router)
|
|
app.include_router(corrections.router, prefix="/corrections", tags=["corrections"])
|
|
app.include_router(snapshots.router)
|
|
|
|
|
|
@app.get("/health")
|
|
def health() -> dict:
|
|
mode = "demo" if settings.demo_mode else ("cloud" if settings.cloud_mode else "dev")
|
|
return {"status": "ok", "service": "linnet", "mode": mode}
|