d901208032
- 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
96 lines
2.4 KiB
Python
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")
|