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