Fix broll temp asset handling and crossfade precision

This commit is contained in:
ي
2026-04-20 08:37:20 +05:30
parent ba9ddbf368
commit 7e4cc51086
2 changed files with 31 additions and 36 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")
# ---------------------------------------------------------------------------
@@ -302,28 +304,6 @@ def make_stacked_bar(data: dict, out_path: str, title: str = "",
return out_path
def make_line_trend(data: dict, out_path: str, title: str = "") -> str:
"""Render a trend line chart. Returns output path."""
x_vals = data.get("x", [])
y_vals = data.get("y", [])
fig, ax = plt.subplots(figsize=(8, 4.5), facecolor="none")
ax.set_facecolor("none")
ax.plot(x_vals, y_vals, color=CHART_STYLE["accent"],
linewidth=2.5, marker="o", markersize=7, zorder=3)
ax.fill_between(x_vals, y_vals, alpha=0.12, color=CHART_STYLE["accent"])
ax.spines[:].set_visible(False)
ax.tick_params(colors=CHART_STYLE["text"])
ax.yaxis.grid(True, color=CHART_STYLE["grid"], linewidth=0.6, zorder=0)
if title:
ax.set_title(title, color=CHART_STYLE["text"], fontsize=13,
fontweight="bold", pad=12)
fig.tight_layout(pad=0.5)
fig.savefig(out_path, dpi=150, transparent=True, bbox_inches="tight")
plt.close(fig)
return out_path
# ---------------------------------------------------------------------------
# Text / Bullet overlay (Pillow → PNG)
# ---------------------------------------------------------------------------
@@ -420,7 +400,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
"""
@@ -444,7 +424,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)
@@ -463,7 +443,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
"""
@@ -477,7 +457,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)
@@ -507,15 +487,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_chart_comparison", "line_trend"):
chart_path = "/tmp/chart.png"
chart_path = str(scene_temp_dir / f"chart_{uuid.uuid4().hex}.png")
if cue == "bar_chart_comparison":
make_bar_chart(insight.chart_data, chart_path,
title=insight.key_insight)
@@ -523,14 +508,14 @@ def dispatch_scene(insight: Insight, assets: SceneAssets,
make_line_trend(insight.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 +556,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 +607,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

@@ -17,6 +17,10 @@ from loguru import logger
# Import chart generators directly
from services.podcast.broll_composer import (
Insight,
SceneAssets,
dispatch_scene,
compose_video,
make_bar_chart,
make_horizontal_bar,
make_line_trend,
@@ -146,9 +150,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)
@@ -250,4 +258,4 @@ def get_broll_service(output_dir: Optional[str] = None) -> BrollService:
global _broll_service_instance
if _broll_service_instance is None:
_broll_service_instance = BrollService(output_dir=output_dir)
return _broll_service_instance
return _broll_service_instance