Implement async B-roll scene rendering with media path resolution

This commit is contained in:
ي
2026-04-20 08:32:42 +05:30
parent ba9ddbf368
commit cf70261658

View File

@@ -4,6 +4,9 @@ B-Roll Handlers
API endpoints for B-roll chart preview and video generation. API endpoints for B-roll chart preview and video generation.
""" """
from pathlib import Path
from urllib.parse import urlparse
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
@@ -12,13 +15,115 @@ import uuid
from middleware.auth_middleware import get_current_user from middleware.auth_middleware import get_current_user
from api.story_writer.utils.auth import require_authenticated_user from api.story_writer.utils.auth import require_authenticated_user
from api.story_writer.task_manager import task_manager
from api.podcast.utils import _resolve_podcast_media_file
from services.podcast.broll_service import get_broll_service from services.podcast.broll_service import get_broll_service
from utils.media_utils import resolve_media_path
from loguru import logger from loguru import logger
router = APIRouter() router = APIRouter()
def _resolve_broll_background_image_path(background_image_url: str) -> str:
"""Resolve background image URL/path to a local file path."""
resolved = resolve_media_path(background_image_url)
if not resolved:
raise HTTPException(status_code=404, detail=f"Background image not found: {background_image_url}")
return str(resolved)
def _resolve_broll_avatar_video_path(avatar_video_url: Optional[str], user_id: str) -> Optional[str]:
"""Resolve optional avatar video URL/path to a local file path."""
if not avatar_video_url:
return None
parsed = urlparse(avatar_video_url)
path = parsed.path if parsed.scheme else avatar_video_url
if "/api/podcast/videos/" in path:
filename = path.split("/api/podcast/videos/", 1)[1].split("?", 1)[0].strip()
if not filename:
raise HTTPException(status_code=400, detail="Invalid avatar video URL")
return str(_resolve_podcast_media_file(filename, "video", user_id))
local_path = Path(path).expanduser().resolve()
if local_path.exists() and local_path.is_file():
return str(local_path)
raise HTTPException(
status_code=400,
detail=(
"Unsupported avatar video URL format. "
"Use /api/podcast/videos/{filename} or a valid local file path."
),
)
def _execute_broll_scene_task(
task_id: str,
*,
scene_id: str,
key_insight: str,
supporting_stat: str,
chart_data: Optional[Dict[str, Any]],
visual_cue: str,
duration: float,
background_img_path: str,
avatar_video_path: Optional[str],
):
"""Background task for rendering a B-roll scene."""
try:
task_manager.update_task_status(
task_id,
"processing",
progress=10.0,
message="Starting B-roll scene render...",
)
broll_service = get_broll_service()
task_manager.update_task_status(
task_id,
"processing",
progress=35.0,
message="Composing scene layers and overlays...",
)
video_path = broll_service.generate_scene_broll(
scene_id=scene_id,
key_insight=key_insight,
supporting_stat=supporting_stat,
chart_data=chart_data,
visual_cue=visual_cue,
duration=duration,
background_img_path=background_img_path,
avatar_video_path=avatar_video_path,
)
filename = Path(video_path).name
video_url = f"/api/podcast/broll/final/{filename}"
task_manager.update_task_status(
task_id,
"completed",
progress=100.0,
message="B-roll scene render completed.",
result={
"scene_id": scene_id,
"broll_video_path": video_path,
"broll_video_url": video_url,
},
)
except Exception as exc:
logger.error(f"[Broll] Task {task_id} failed: {exc}")
task_manager.update_task_status(
task_id,
"failed",
error=f"B-roll scene render failed: {str(exc)}",
error_status=500,
)
class ChartPreviewRequest(BaseModel): class ChartPreviewRequest(BaseModel):
"""Request model for chart preview generation.""" """Request model for chart preview generation."""
chart_data: Dict[str, Any] = Field(..., description="Chart data (labels, before/after, etc.)") chart_data: Dict[str, Any] = Field(..., description="Chart data (labels, before/after, etc.)")
@@ -51,8 +156,11 @@ class BrollSceneRequest(BaseModel):
class BrollSceneResponse(BaseModel): class BrollSceneResponse(BaseModel):
"""Response for B-roll scene generation.""" """Response for B-roll scene generation."""
scene_id: str scene_id: str
broll_video_url: str broll_video_url: str = ""
broll_video_path: str broll_video_path: str = ""
task_id: Optional[str] = None
status: str = "completed"
message: Optional[str] = None
class BrollComposeRequest(BaseModel): class BrollComposeRequest(BaseModel):
@@ -136,16 +244,35 @@ async def generate_broll_scene(
detail=f"Invalid visual_cue. Must be one of: {valid_cues}" detail=f"Invalid visual_cue. Must be one of: {valid_cues}"
) )
# For now, return a placeholder - full video generation requires background_img_path = _resolve_broll_background_image_path(request.background_image_url)
# resolving image/video URLs to actual file paths avatar_video_path = _resolve_broll_avatar_video_path(request.avatar_video_url, user_id)
# In V2, this will integrate with the actual video generation
logger.info(f"[Broll] B-roll scene request for scene: {request.scene_id}") logger.info(f"[Broll] B-roll scene request for scene: {request.scene_id}")
# Scene rendering can be expensive, so use task manager/background execution.
task_id = task_manager.create_task(
"podcast_broll_scene_generation",
metadata={"owner_user_id": user_id, "scene_id": request.scene_id},
)
background_tasks.add_task(
_execute_broll_scene_task,
task_id=task_id,
scene_id=request.scene_id,
key_insight=request.key_insight,
supporting_stat=request.supporting_stat,
chart_data=request.chart_data,
visual_cue=request.visual_cue,
duration=request.duration,
background_img_path=background_img_path,
avatar_video_path=avatar_video_path,
)
return BrollSceneResponse( return BrollSceneResponse(
scene_id=request.scene_id, scene_id=request.scene_id,
broll_video_url="", task_id=task_id,
broll_video_path="", status="pending",
message="B-roll scene render started. Poll /api/podcast/task/{task_id}/status for progress.",
) )
except HTTPException: except HTTPException: