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>
82 lines
2.8 KiB
Python
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")
|