Merge branch 'pr-483'
# Conflicts: # backend/services/podcast/broll_composer.py # backend/services/podcast/broll_service.py
This commit is contained in:
@@ -4,6 +4,8 @@ Layered composition pipeline: Background + Chart + Avatar Circle + Text Overlays
|
||||
"""
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import uuid
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
@@ -40,7 +42,7 @@ def crossfade_concat(scenes: list, fade_dur: float = 0.5):
|
||||
if i > 0:
|
||||
c = c.fx(vfx.CrossFadeIn, fade_dur)
|
||||
faded.append(c)
|
||||
return concatenate_videoclips(faded, padding=-int(fade_dur), method="compose")
|
||||
return concatenate_videoclips(faded, padding=-fade_dur, method="compose")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -305,8 +307,6 @@ def make_line_trend(data: dict, out_path: str, title: str = "") -> str:
|
||||
fig.savefig(out_path, dpi=150, transparent=True, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
return out_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Text / Bullet overlay (Pillow → PNG)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -403,7 +403,7 @@ def ken_burns(clip: ImageClip, zoom_ratio: float = 0.08) -> ImageClip:
|
||||
# Scene builders (one per visual_cue type)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_data_scene(assets: SceneAssets, insight: Insight) -> CompositeVideoClip:
|
||||
def build_data_scene(assets: SceneAssets, insight: Insight, temp_dir: Path) -> CompositeVideoClip:
|
||||
"""
|
||||
Layout: Background (Ken Burns) + Chart (fade-in) + Avatar circle (corner) + Insight card
|
||||
"""
|
||||
@@ -427,7 +427,7 @@ def build_data_scene(assets: SceneAssets, insight: Insight) -> CompositeVideoCli
|
||||
.fx(vfx.fadeout, 0.4))
|
||||
layers.append(chart)
|
||||
|
||||
card_path = "/tmp/insight_card.png"
|
||||
card_path = str(temp_dir / f"insight_card_{uuid.uuid4().hex}.png")
|
||||
make_insight_card(insight.key_insight, insight.supporting_stat, card_path)
|
||||
card = (ImageClip(card_path)
|
||||
.set_duration(d - 1)
|
||||
@@ -446,7 +446,7 @@ def build_data_scene(assets: SceneAssets, insight: Insight) -> CompositeVideoCli
|
||||
|
||||
|
||||
def build_bullet_scene(assets: SceneAssets, insight: Insight,
|
||||
bullets: list[str]) -> CompositeVideoClip:
|
||||
bullets: list[str], temp_dir: Path) -> CompositeVideoClip:
|
||||
"""
|
||||
Layout: AI image (Ken Burns) + Bullet overlay + Avatar circle
|
||||
"""
|
||||
@@ -460,7 +460,7 @@ def build_bullet_scene(assets: SceneAssets, insight: Insight,
|
||||
bg = bg.fx(vfx.lum_contrast, 0, -50)
|
||||
layers.append(bg)
|
||||
|
||||
bullet_path = "/tmp/bullets.png"
|
||||
bullet_path = str(temp_dir / f"bullets_{uuid.uuid4().hex}.png")
|
||||
make_bullet_overlay(bullets, bullet_path, width=860)
|
||||
bullets_clip = (ImageClip(bullet_path)
|
||||
.set_duration(d - 1)
|
||||
@@ -490,15 +490,20 @@ def build_full_avatar_scene(assets: SceneAssets, insight: Insight) -> VideoFileC
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def dispatch_scene(insight: Insight, assets: SceneAssets,
|
||||
bullet_lines: Optional[list[str]] = None):
|
||||
bullet_lines: Optional[list[str]] = None,
|
||||
temp_dir: Optional[str | Path] = None):
|
||||
"""Dispatch scene based on visual_cue type."""
|
||||
cue = insight.visual_cue
|
||||
scene_temp_dir = Path(temp_dir) if temp_dir else Path(
|
||||
tempfile.mkdtemp(prefix=f"broll_{cue}_")
|
||||
)
|
||||
scene_temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if cue == "full_avatar":
|
||||
return build_full_avatar_scene(assets, insight)
|
||||
|
||||
elif cue in ("bar_comparison", "bar_chart_comparison", "bar_horizontal", "line_trend", "pie", "stacked_bar"):
|
||||
chart_path = "/tmp/chart.png"
|
||||
chart_path = str(scene_temp_dir / f"chart_{uuid.uuid4().hex}.png")
|
||||
chart_data = insight.chart_data or {}
|
||||
if cue in ("bar_comparison", "bar_chart_comparison"):
|
||||
# Normalize {labels, values} -> {labels, before, after} for make_bar_chart
|
||||
@@ -523,14 +528,14 @@ def dispatch_scene(insight: Insight, assets: SceneAssets,
|
||||
make_stacked_bar(chart_data, chart_path,
|
||||
title=insight.key_insight)
|
||||
assets.chart_img = chart_path
|
||||
return build_data_scene(assets, insight)
|
||||
return build_data_scene(assets, insight, scene_temp_dir)
|
||||
|
||||
elif cue == "bullet_points":
|
||||
lines = bullet_lines or [insight.key_insight, insight.supporting_stat]
|
||||
return build_bullet_scene(assets, insight, lines)
|
||||
return build_bullet_scene(assets, insight, lines, scene_temp_dir)
|
||||
|
||||
else:
|
||||
return build_data_scene(assets, insight)
|
||||
return build_data_scene(assets, insight, scene_temp_dir)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -571,8 +576,10 @@ def pipeline_from_json(insight_json: str,
|
||||
data = json.loads(insight_json)
|
||||
insight = Insight(**{k: data[k] for k in Insight.__dataclass_fields__ if k in data})
|
||||
assets = SceneAssets(background_img=background_img, avatar_video=avatar_video)
|
||||
scene_temp_dir = Path(tempfile.mkdtemp(prefix=f"scene_{insight.visual_cue}_"))
|
||||
scene = dispatch_scene(insight, assets,
|
||||
bullet_lines=data.get("bullet_lines"))
|
||||
bullet_lines=data.get("bullet_lines"),
|
||||
temp_dir=scene_temp_dir)
|
||||
out = f"/tmp/scene_{insight.visual_cue}.mp4"
|
||||
compose_video([scene], output_path=out)
|
||||
return out
|
||||
|
||||
@@ -139,9 +139,13 @@ class BrollService:
|
||||
background_img=background_img_path,
|
||||
avatar_video=avatar_video_path,
|
||||
)
|
||||
scene_temp_dir = self.get_output_path(
|
||||
f"scene_assets_{scene_id_safe}_{uuid.uuid4().hex[:8]}"
|
||||
)
|
||||
scene_temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate the scene
|
||||
scene = dispatch_scene(insight, assets)
|
||||
scene = dispatch_scene(insight, assets, temp_dir=scene_temp_dir)
|
||||
|
||||
# Write video
|
||||
compose_video([scene], output_path=out_path)
|
||||
|
||||
Reference in New Issue
Block a user