Implement async B-roll scene rendering with media path resolution
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user