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:
2026-04-04 12:27:06 -05:00
parent f62772663d
commit d901208032
12 changed files with 923 additions and 229 deletions
+17 -65
View File
@@ -1,18 +1,15 @@
"""
VoiceCoach pipeline entry point -- v2.
Runs token server + pipeline with unique identity per session.
VoiceCoach pipeline entry point.
Token server runs as a separate container — this file only runs the pipeline.
Pipeline exits on session end; Docker restarts the container for a fresh state.
"""
import asyncio
import logging
import os
import threading
import uuid
from pathlib import Path
from datetime import timedelta
import structlog
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
from livekit import api as livekit_api
from pipeline import run_voice_coach_pipeline
@@ -23,68 +20,23 @@ logger = structlog.get_logger()
LIVEKIT_API_KEY = os.environ["LIVEKIT_API_KEY"]
LIVEKIT_API_SECRET = os.environ["LIVEKIT_API_SECRET"]
LIVEKIT_WS_URL = os.getenv("LIVEKIT_URL", "ws://livekit-server:7880")
LIVEKIT_PUBLIC_URL = os.getenv("LIVEKIT_PUBLIC_URL", "wss://voice.hobbs.farm")
OPENCLAW_TOKEN = os.getenv("OPENCLAW_TOKEN", "")
# --- Token HTTP server ---
token_app = FastAPI(title="VoiceCoach Token Service")
class TokenRequest(BaseModel):
identity: str = "jeremy"
class TokenResponse(BaseModel):
token: str
url: str
@token_app.get("/health")
async def health():
return {"status": "ok", "version": "2.0"}
@token_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)
def run_token_server():
uvicorn.run(token_app, host="0.0.0.0", port=8080, log_level="info")
# --- Main ---
async def main():
server_thread = threading.Thread(target=run_token_server, daemon=True)
server_thread.start()
logger.info("Token server started on :8080")
agent_identity = f"voicecoach-agent-{uuid.uuid4().hex[:8]}"
token = livekit_api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET)
token.with_grants(livekit_api.VideoGrants(room_join=True, room="voicecoach"))
token.identity = agent_identity
token.ttl = timedelta(hours=24)
livekit_token = token.to_jwt()
while True:
agent_identity = f"voicecoach-agent-{uuid.uuid4().hex[:8]}"
token = livekit_api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET)
token.with_grants(livekit_api.VideoGrants(room_join=True, room="voicecoach"))
token.identity = agent_identity
livekit_token = token.to_jwt()
logger.info("Starting VoiceCoach v2 pipeline", identity=agent_identity)
try:
await run_voice_coach_pipeline(
livekit_url=LIVEKIT_WS_URL,
livekit_token=livekit_token,
openclaw_token=OPENCLAW_TOKEN,
)
except Exception as e:
logger.error("Pipeline error, restarting in 35s", error=str(e))
await asyncio.sleep(35)
logger.info("Starting VoiceCoach pipeline", identity=agent_identity)
await run_voice_coach_pipeline(
livekit_url=LIVEKIT_WS_URL,
livekit_token=livekit_token,
openclaw_token="",
)
logger.info("Pipeline exited")
if __name__ == "__main__":