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>
85 lines
2.8 KiB
Python
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",
|
|
},
|
|
)
|