pahvalentines/backend/app/routers/webhook.py
michael 9c5b054dcc feat: Add Sonauto streaming audio on waiting page
Enable users to listen to their song while video generation continues
in the background. Backend proxies authenticated Sonauto stream API
since HTML audio elements cannot send auth headers.

- Add streaming_ready_at column to track when stream becomes available
- Enable streaming in Sonauto API request payload
- Handle GENERATING_STREAMING_READY webhook status
- Add /api/stream/{session_id} proxy endpoint using httpx
- Update StatusResponse with streaming_ready and task_id fields
- Add audio player UI with autoplay fallback for mobile
- Fade out audio gracefully before redirect to result page

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

82 lines
2.8 KiB
Python

"""API endpoint for Sonauto webhook callback."""
import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Submission
from app.schemas import WebhookPayload, WebhookResponse
logger = logging.getLogger(__name__)
router = APIRouter(tags=["webhook"])
def trigger_post_webhook_processing(session_id: str) -> None:
"""Trigger the chained Celery tasks after successful webhook."""
# Import here to avoid circular imports
from tasks.workers import download_audio, fetch_generation_details, create_video
from celery import chain
workflow = chain(
fetch_generation_details.s(session_id),
download_audio.s(),
create_video.s(),
)
workflow.apply_async()
logger.info(f"Triggered post-webhook processing chain for {session_id}")
@router.post("/api/webhook", response_model=WebhookResponse)
async def handle_webhook(
payload: WebhookPayload,
db: Session = Depends(get_db),
) -> WebhookResponse:
"""
Receive callback from Sonauto API when song generation completes.
- Validates task_id exists in database
- On SUCCESS: triggers processing chain (fetch details -> download audio -> create video)
- On FAILURE: marks submission as failed
"""
submission = db.query(Submission).filter_by(LLM_task_id=payload.task_id).first()
if not submission:
logger.warning(f"Webhook received for unknown task_id: {payload.task_id}")
raise HTTPException(status_code=404, detail="Unknown task_id")
submission.received_from_LLM = datetime.utcnow()
if payload.status == "SUCCESS":
db.commit()
# Trigger chained processing tasks
trigger_post_webhook_processing(submission.session_id)
logger.info(
f"Webhook SUCCESS for {submission.session_id}, triggered processing chain"
)
elif payload.status == "GENERATING_STREAMING_READY":
# Audio stream is ready, but generation is still in progress
# Set streaming_ready_at timestamp so frontend can start playing
submission.streaming_ready_at = datetime.utcnow()
db.commit()
logger.info(
f"Webhook GENERATING_STREAMING_READY for {submission.session_id}, streaming now available"
)
elif payload.status == "FAILURE":
submission.LLM_status = "fail"
submission.entry_status = "fail"
db.commit()
logger.warning(
f"Webhook FAILURE for {submission.session_id}: {payload.error_message}"
)
else:
logger.warning(
f"Webhook received unknown status '{payload.status}' for {submission.session_id}"
)
db.commit()
return WebhookResponse(success=True, message="Webhook processed")