Merge branch 'pr-483'

# Conflicts:
#	backend/services/podcast/broll_composer.py
#	backend/services/podcast/broll_service.py
This commit is contained in:
ajaysi
2026-05-23 13:37:44 +05:30
2 changed files with 26 additions and 15 deletions

View File

@@ -4,6 +4,8 @@ Layered composition pipeline: Background + Chart + Avatar Circle + Text Overlays
""" """
import json import json
import tempfile
import uuid
import numpy as np import numpy as np
from pathlib import Path from pathlib import Path
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -40,7 +42,7 @@ def crossfade_concat(scenes: list, fade_dur: float = 0.5):
if i > 0: if i > 0:
c = c.fx(vfx.CrossFadeIn, fade_dur) c = c.fx(vfx.CrossFadeIn, fade_dur)
faded.append(c) 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") fig.savefig(out_path, dpi=150, transparent=True, bbox_inches="tight")
plt.close(fig) plt.close(fig)
return out_path return out_path
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Text / Bullet overlay (Pillow → PNG) # 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) # 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 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)) .fx(vfx.fadeout, 0.4))
layers.append(chart) 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) make_insight_card(insight.key_insight, insight.supporting_stat, card_path)
card = (ImageClip(card_path) card = (ImageClip(card_path)
.set_duration(d - 1) .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, 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 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) bg = bg.fx(vfx.lum_contrast, 0, -50)
layers.append(bg) 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) make_bullet_overlay(bullets, bullet_path, width=860)
bullets_clip = (ImageClip(bullet_path) bullets_clip = (ImageClip(bullet_path)
.set_duration(d - 1) .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, 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.""" """Dispatch scene based on visual_cue type."""
cue = insight.visual_cue 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": if cue == "full_avatar":
return build_full_avatar_scene(assets, insight) return build_full_avatar_scene(assets, insight)
elif cue in ("bar_comparison", "bar_chart_comparison", "bar_horizontal", "line_trend", "pie", "stacked_bar"): 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 {} chart_data = insight.chart_data or {}
if cue in ("bar_comparison", "bar_chart_comparison"): if cue in ("bar_comparison", "bar_chart_comparison"):
# Normalize {labels, values} -> {labels, before, after} for make_bar_chart # 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, make_stacked_bar(chart_data, chart_path,
title=insight.key_insight) title=insight.key_insight)
assets.chart_img = chart_path assets.chart_img = chart_path
return build_data_scene(assets, insight) return build_data_scene(assets, insight, scene_temp_dir)
elif cue == "bullet_points": elif cue == "bullet_points":
lines = bullet_lines or [insight.key_insight, insight.supporting_stat] 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: 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) data = json.loads(insight_json)
insight = Insight(**{k: data[k] for k in Insight.__dataclass_fields__ if k in data}) 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) 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, 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" out = f"/tmp/scene_{insight.visual_cue}.mp4"
compose_video([scene], output_path=out) compose_video([scene], output_path=out)
return out return out

View File

@@ -139,9 +139,13 @@ class BrollService:
background_img=background_img_path, background_img=background_img_path,
avatar_video=avatar_video_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 # Generate the scene
scene = dispatch_scene(insight, assets) scene = dispatch_scene(insight, assets, temp_dir=scene_temp_dir)
# Write video # Write video
compose_video([scene], output_path=out_path) compose_video([scene], output_path=out_path)