""" Standalone LiveKit token server + settings API. Runs independently from the pipeline so it never goes down when the pipeline crashes. Settings are stored in a JSON file so the pipeline can read them on startup. """ import json import os from pathlib import Path import uvicorn from fastapi import FastAPI from pydantic import BaseModel from typing import Optional from livekit import api as livekit_api LIVEKIT_API_KEY = os.environ["LIVEKIT_API_KEY"] LIVEKIT_API_SECRET = os.environ["LIVEKIT_API_SECRET"] LIVEKIT_PUBLIC_URL = os.getenv("LIVEKIT_PUBLIC_URL", "wss://voice.hobbs.farm") SETTINGS_PATH = Path(os.getenv("SETTINGS_PATH", "/data/settings.json")) app = FastAPI(title="VoiceCoach Token + Settings Service") DEFAULTS = { "confidence": 0.7, "min_volume": 0.3, "start_secs": 0.4, "stop_secs": 0.5, "max_tokens": 150, "tts_voice": "af_nova", } def load_settings() -> dict: try: if SETTINGS_PATH.exists(): return {**DEFAULTS, **json.loads(SETTINGS_PATH.read_text())} except Exception: pass return {**DEFAULTS} def save_settings(settings: dict): SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True) SETTINGS_PATH.write_text(json.dumps(settings, indent=2)) class TokenRequest(BaseModel): identity: str = "jeremy" class TokenResponse(BaseModel): token: str url: str class Settings(BaseModel): confidence: Optional[float] = None min_volume: Optional[float] = None start_secs: Optional[float] = None stop_secs: Optional[float] = None max_tokens: Optional[int] = None tts_voice: Optional[str] = None @app.get("/health") async def health(): return {"status": "ok", "version": "2.1"} @app.post("/token") async def get_token(req: TokenRequest): token = livekit_api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET) token.with_grants(livekit_api.VideoGrants( room_join=True, room="voicecoach", can_publish=True, can_subscribe=True, )) token.identity = req.identity return TokenResponse(token=token.to_jwt(), url=LIVEKIT_PUBLIC_URL) @app.get("/settings") async def get_settings(): return load_settings() @app.post("/settings") async def update_settings(s: Settings): current = load_settings() update = {k: v for k, v in s.dict().items() if v is not None} current.update(update) save_settings(current) return current if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8080, log_level="info")