Files
jeremy d901208032 feat: VoiceCoach v2.1 - GPU local, Ollama LLM, settings panel, token server separation
- pipeline.py: switched LLM from OpenClaw/Claude to local Ollama, added settings loading, sentence aggregator, async SpeechBrain, tuned VAD
- main.py: simplified (no more token server thread, just pipeline)
- token_server.py: NEW - standalone token + settings API server
- docker-compose.yml: all services on GPU machine, added token-server service, CUDA speaches, Ollama LLM
- web-client/index.html: added settings panel with sliders
- web-client/app.js: settings management, localStorage persistence, push to server
- web-client/style.css: settings panel styles
- web-client/nginx.conf: proxy to token-server instead of pipecat-agent
- patch_livekit.py: updated with proper indentation-safe patching
2026-04-04 12:27:06 -05:00

96 lines
2.4 KiB
Python

"""
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")