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 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
@@ -620,4 +627,4 @@ if __name__ == "__main__":
})
print("\nSample Insight JSON:\n", sample_json)
print("\nAll asset generation tests passed.")
print("To run full video composition, supply real background_img and avatar_video paths.")
print("To run full video composition, supply real background_img and avatar_video paths.")

View File

@@ -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)