pahvalentines/backend/app/routers/stream.py
michael a570ac657f fix: Improve audio streaming reliability and playback
Stream endpoint:
- Increase read timeout to 600s for progressive audio generation
- Use larger 16KB chunks for smoother playback
- Remove misleading Accept-Ranges header (we don't support range requests)
- Add logging for stream start/completion
- Properly close httpx client after streaming

Frontend:
- Wait for 'canplay' event before attempting autoplay
- Add event listeners for play, error, stalled, waiting
- Native audio controls' play button now properly hides tap message
- Add 5s fallback timeout if canplay doesn't fire

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 08:40:02 -06:00

85 lines
2.8 KiB
Python

"""API endpoint for proxying Sonauto audio stream."""
import logging
import httpx
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.config import settings
from app.database import get_db
from app.models import Submission
logger = logging.getLogger(__name__)
router = APIRouter(tags=["stream"])
SONAUTO_STREAM_URL = "https://api-stream.sonauto.ai/stream"
@router.get("/api/stream/{session_id}")
async def stream_audio(
session_id: str,
db: Session = Depends(get_db),
):
"""
Proxy audio stream from Sonauto API.
This endpoint exists because HTML <audio> elements cannot send custom
Authorization headers, but Sonauto's stream endpoint requires Bearer token auth.
- 404: Session not found
- 400: Streaming not ready yet
- 502: Sonauto stream failed
"""
submission = db.get(Submission, session_id)
if not submission:
raise HTTPException(status_code=404, detail="Session not found")
if not submission.streaming_ready_at:
raise HTTPException(status_code=400, detail="Streaming not ready")
if not submission.LLM_task_id:
raise HTTPException(status_code=400, detail="No task ID available")
stream_url = f"{SONAUTO_STREAM_URL}/{submission.LLM_task_id}"
# Create a persistent client for the stream
# Use a very long timeout since Sonauto streams progressively as it generates
client = httpx.AsyncClient(timeout=httpx.Timeout(30.0, read=600.0))
async def stream_generator():
"""Stream audio chunks from Sonauto."""
try:
async with client.stream(
"GET",
stream_url,
headers={"Authorization": f"Bearer {settings.SONAUTO_API_KEY}"},
) as response:
if response.status_code != 200:
logger.error(
f"Sonauto stream error for {session_id}: {response.status_code}"
)
return
logger.info(f"Streaming started for {session_id}")
async for chunk in response.aiter_bytes(chunk_size=16384):
yield chunk
logger.info(f"Streaming completed for {session_id}")
except httpx.ReadTimeout:
logger.warning(f"Stream read timeout for {session_id} - this may be normal for long generations")
except httpx.RequestError as e:
logger.error(f"Stream request error for {session_id}: {e}")
finally:
await client.aclose()
return StreamingResponse(
stream_generator(),
media_type="audio/mpeg",
headers={
"Cache-Control": "no-cache, no-store, must-revalidate",
"X-Content-Type-Options": "nosniff",
},
)