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:
2026-07-11 11:19:04 -05:00
parent dddc6734d0
commit 266ea11447
29 changed files with 4494 additions and 0 deletions
@@ -0,0 +1,66 @@
"""
FastAPI service wrapping VocalAnalyzer.
Internal-only -- not exposed via Traefik.
"""
import logging
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse
from dataclasses import asdict
from analyzer import VocalAnalyzer
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
MODEL_DIR = os.getenv("MODEL_DIR", "/app/models")
analyzer: VocalAnalyzer = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global analyzer
logger.info("Starting VocalAnalyzer...")
analyzer = VocalAnalyzer(model_dir=MODEL_DIR)
logger.info("VocalAnalyzer ready.")
yield
logger.info("Shutting down.")
app = FastAPI(title="SpeechBrain Vocal Analysis Service", lifespan=lifespan)
@app.get("/health")
async def health():
if analyzer is None:
raise HTTPException(status_code=503, detail="Model not loaded")
return {"status": "ok", "model": "emotion-recognition-wav2vec2-IEMOCAP"}
@app.post("/analyze")
async def analyze(audio: UploadFile = File(...)):
if analyzer is None:
raise HTTPException(status_code=503, detail="Model not loaded")
content = await audio.read()
if len(content) == 0:
raise HTTPException(status_code=400, detail="Empty audio file")
result = analyzer.analyze(content, content_type=audio.content_type or "audio/wav")
return JSONResponse(content=asdict(result))
@app.post("/calibrate")
async def calibrate(audio: UploadFile = File(...)):
if analyzer is None:
raise HTTPException(status_code=503, detail="Model not loaded")
content = await audio.read()
if len(content) == 0:
raise HTTPException(status_code=400, detail="Empty audio file")
result = analyzer.analyze(content, content_type=audio.content_type or "audio/wav")
result_dict = asdict(result)
result_dict["is_baseline"] = True
return JSONResponse(content=result_dict)