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
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
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")
|
||||
Reference in New Issue
Block a user