diff --git a/docker-compose.yml b/docker-compose.yml index 483d45f..96f5f28 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,6 +84,7 @@ services: - SPEECHBRAIN_SERVICE_URL=http://speechbrain-service:8001 - PIPER_SERVICE_URL=http://piper-tts:8003 - WHISPER_MODEL=medium + - LIVEKIT_PUBLIC_URL=wss://voice.hobbs.farm depends_on: - livekit-server - speechbrain-service diff --git a/pipecat-agent/main.py b/pipecat-agent/main.py index c543a39..f861856 100755 --- a/pipecat-agent/main.py +++ b/pipecat-agent/main.py @@ -1,12 +1,16 @@ """ Entry point for the Pipecat VoiceCoach agent. -Reads config from environment variables. +Runs both the token HTTP server (for web clients) and the voice pipeline. """ import asyncio import logging import os +import threading 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 @@ -14,15 +18,59 @@ 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") + +# --- Token HTTP server (serves /token for web clients) --- + +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"} + + +@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 pipeline --- async def main(): anthropic_key = os.environ["ANTHROPIC_API_KEY"] - livekit_api_key = os.environ["LIVEKIT_API_KEY"] - livekit_api_secret = os.environ["LIVEKIT_API_SECRET"] - livekit_url = os.getenv("LIVEKIT_URL", "ws://livekit-server:7880") - # Generate LiveKit access token for this agent - token = livekit_api.AccessToken(livekit_api_key, livekit_api_secret) + # Start token server in a background thread + server_thread = threading.Thread(target=run_token_server, daemon=True) + server_thread.start() + logger.info("Token server started on :8080") + + # Generate agent's own LiveKit token + token = livekit_api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET) token.with_grants(livekit_api.VideoGrants(room_join=True, room="voicecoach")) token.identity = "voicecoach-agent" livekit_token = token.to_jwt() @@ -33,9 +81,9 @@ async def main(): "current_mode": "coaching mode", } - logger.info("Starting VoiceCoach pipeline", livekit_url=livekit_url) + logger.info("Starting VoiceCoach pipeline", livekit_url=LIVEKIT_WS_URL) await run_voice_coach_pipeline( - livekit_url=livekit_url, + livekit_url=LIVEKIT_WS_URL, livekit_token=livekit_token, anthropic_key=anthropic_key, session_context=session_context, diff --git a/pipecat-agent/requirements.txt b/pipecat-agent/requirements.txt index 37cbc33..01d6c69 100755 --- a/pipecat-agent/requirements.txt +++ b/pipecat-agent/requirements.txt @@ -1,4 +1,6 @@ pipecat-ai[livekit,silero,openai]==0.0.48 +fastapi==0.115.0 +uvicorn[standard]==0.30.0 anthropic==0.34.0 faster-whisper==1.0.3 requests==2.32.3