feat: FRIDAY Voice stack on the GPU machine; VoiceCoach history
FRIDAY Voice (speaches GPU STT/TTS + hermes agent + push-to-talk console) on .150 Docker Desktop, fronted by coach.hobbs.farm. Also captures the VoiceCoach compose/agent trees and deploy notes it replaced, which had never been committed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
VoiceCoach pipeline entry point -- v2.
|
||||
Runs token server + restarts pipeline immediately after each session.
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
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
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
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: restart entire process after each session ---
|
||||
# This guarantees a completely fresh pipeline for every session.
|
||||
# No stale transport state, no cancelled tasks, no dead queues.
|
||||
|
||||
def run_pipeline_sync():
|
||||
"""Run one pipeline session, then exit the process.
|
||||
Docker's restart: unless-stopped will bring us back immediately.
|
||||
"""
|
||||
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) # 24 hours - prevents expired token reconnect failures
|
||||
livekit_token = token.to_jwt()
|
||||
|
||||
logger.info("Starting VoiceCoach v2 pipeline", identity=agent_identity)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
loop.run_until_complete(run_voice_coach_pipeline(
|
||||
livekit_url=LIVEKIT_WS_URL,
|
||||
livekit_token=livekit_token,
|
||||
openclaw_token=OPENCLAW_TOKEN,
|
||||
))
|
||||
except Exception as e:
|
||||
logger.error("Pipeline ended", error=str(e))
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Start token server in background
|
||||
server_thread = threading.Thread(target=run_token_server, daemon=True)
|
||||
server_thread.start()
|
||||
logger.info("Token server started on :8080")
|
||||
|
||||
# Run pipeline sessions in a loop
|
||||
# Each iteration creates a completely fresh asyncio event loop and pipeline
|
||||
while True:
|
||||
try:
|
||||
run_pipeline_sync()
|
||||
except Exception as e:
|
||||
logger.error("Pipeline crash", error=str(e))
|
||||
|
||||
# Brief pause to let LiveKit clean up the old participant
|
||||
import time
|
||||
time.sleep(3)
|
||||
logger.info("Restarting pipeline with fresh state...")
|
||||
Reference in New Issue
Block a user