Spaces:
Sleeping
Sleeping
Update sozo_gen.py
Browse files- sozo_gen.py +50 -31
sozo_gen.py
CHANGED
|
@@ -26,6 +26,7 @@ from google import genai
|
|
| 26 |
import requests
|
| 27 |
# In sozo_gen.py, near the other google imports
|
| 28 |
from google.genai import types as genai_types
|
|
|
|
| 29 |
|
| 30 |
# --- Configuration ---
|
| 31 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(funcName)s] - %(message)s')
|
|
@@ -359,9 +360,10 @@ def prepare_plot_data(spec: ChartSpecification, df: pd.DataFrame):
|
|
| 359 |
return df[spec.x_col]
|
| 360 |
|
| 361 |
# UPDATED: animate_chart now uses blit=False for accurate timing
|
|
|
|
| 362 |
def animate_chart(spec: ChartSpecification, df: pd.DataFrame, dur: float, out: Path, fps: int = FPS) -> str:
|
| 363 |
plot_data = prepare_plot_data(spec, df)
|
| 364 |
-
frames =
|
| 365 |
fig, ax = plt.subplots(figsize=(WIDTH / 100, HEIGHT / 100), dpi=100)
|
| 366 |
plt.tight_layout(pad=3.0)
|
| 367 |
ctype = spec.chart_type
|
|
@@ -664,6 +666,8 @@ def generate_video_from_project(df: pd.DataFrame, raw_md: str, data_context: Dic
|
|
| 664 |
logging.info(f"Generating video for project {project_id} with voice {voice_model}")
|
| 665 |
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", google_api_key=API_KEY, temperature=0.2)
|
| 666 |
|
|
|
|
|
|
|
| 667 |
story_prompt = f"""
|
| 668 |
Based on the following report, create a script for a {VIDEO_SCENES}-scene video.
|
| 669 |
1. The first scene MUST be an "Introduction". It must contain narration and a stock video tag like: <generate_stock_video: "search query">.
|
|
@@ -673,10 +677,11 @@ def generate_video_from_project(df: pd.DataFrame, raw_md: str, data_context: Dic
|
|
| 673 |
Report: {raw_md}
|
| 674 |
Only output the script, no extra text.
|
| 675 |
"""
|
| 676 |
-
script = llm.invoke(story_prompt).content
|
| 677 |
scenes = [s.strip() for s in script.split("[SCENE_BREAK]") if s.strip()]
|
| 678 |
video_parts, audio_parts, temps = [], [], []
|
| 679 |
total_audio_duration = 0.0
|
|
|
|
| 680 |
|
| 681 |
for i, sc in enumerate(scenes):
|
| 682 |
mp4 = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4"
|
|
@@ -701,40 +706,54 @@ def generate_video_from_project(df: pd.DataFrame, raw_md: str, data_context: Dic
|
|
| 701 |
video_dur = audio_dur + 1.5
|
| 702 |
|
| 703 |
try:
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
if
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
video_parts.append(video_path)
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
safe_chart(chart_descs[0], df, video_dur, mp4, data_context)
|
| 717 |
-
video_parts.append(str(mp4))
|
| 718 |
else:
|
| 719 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
except Exception as e:
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
try:
|
| 724 |
-
fallback_query = "abstract technology background"
|
| 725 |
-
video_path = search_and_download_pexels_video(fallback_query, video_dur, mp4)
|
| 726 |
-
if not video_path: raise ValueError("Fallback Pexels search failed.")
|
| 727 |
-
video_parts.append(video_path)
|
| 728 |
-
except Exception as fallback_e:
|
| 729 |
-
# --- Final Failsafe ---
|
| 730 |
-
logging.error(f"Scene {i+1}: Fallback visual also failed ({fallback_e}). Using placeholder.")
|
| 731 |
-
placeholder = placeholder_img()
|
| 732 |
-
placeholder.save(str(mp4).replace(".mp4", ".png"))
|
| 733 |
-
animate_image_fade(cv2.imread(str(mp4).replace(".mp4", ".png")), video_dur, mp4)
|
| 734 |
-
video_parts.append(str(mp4))
|
| 735 |
|
| 736 |
temps.append(mp4)
|
| 737 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_vid, \
|
| 739 |
tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as temp_aud, \
|
| 740 |
tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as final_vid:
|
|
@@ -743,7 +762,7 @@ def generate_video_from_project(df: pd.DataFrame, raw_md: str, data_context: Dic
|
|
| 743 |
audio_mix_path = Path(temp_aud.name)
|
| 744 |
final_vid_path = Path(final_vid.name)
|
| 745 |
|
| 746 |
-
concat_media(
|
| 747 |
concat_media(audio_parts, audio_mix_path)
|
| 748 |
|
| 749 |
cmd = [
|
|
|
|
| 26 |
import requests
|
| 27 |
# In sozo_gen.py, near the other google imports
|
| 28 |
from google.genai import types as genai_types
|
| 29 |
+
import math # Add this import at the top of your sozo_gen.py file
|
| 30 |
|
| 31 |
# --- Configuration ---
|
| 32 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(funcName)s] - %(message)s')
|
|
|
|
| 360 |
return df[spec.x_col]
|
| 361 |
|
| 362 |
# UPDATED: animate_chart now uses blit=False for accurate timing
|
| 363 |
+
|
| 364 |
def animate_chart(spec: ChartSpecification, df: pd.DataFrame, dur: float, out: Path, fps: int = FPS) -> str:
|
| 365 |
plot_data = prepare_plot_data(spec, df)
|
| 366 |
+
frames = math.ceil(dur * fps) # Use math.ceil to always round up frames
|
| 367 |
fig, ax = plt.subplots(figsize=(WIDTH / 100, HEIGHT / 100), dpi=100)
|
| 368 |
plt.tight_layout(pad=3.0)
|
| 369 |
ctype = spec.chart_type
|
|
|
|
| 666 |
logging.info(f"Generating video for project {project_id} with voice {voice_model}")
|
| 667 |
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", google_api_key=API_KEY, temperature=0.2)
|
| 668 |
|
| 669 |
+
domain = detect_dataset_domain(df)
|
| 670 |
+
|
| 671 |
story_prompt = f"""
|
| 672 |
Based on the following report, create a script for a {VIDEO_SCENES}-scene video.
|
| 673 |
1. The first scene MUST be an "Introduction". It must contain narration and a stock video tag like: <generate_stock_video: "search query">.
|
|
|
|
| 677 |
Report: {raw_md}
|
| 678 |
Only output the script, no extra text.
|
| 679 |
"""
|
| 680 |
+
script = llm.invoke(story_prompt).content.strip()
|
| 681 |
scenes = [s.strip() for s in script.split("[SCENE_BREAK]") if s.strip()]
|
| 682 |
video_parts, audio_parts, temps = [], [], []
|
| 683 |
total_audio_duration = 0.0
|
| 684 |
+
conclusion_video_path = None
|
| 685 |
|
| 686 |
for i, sc in enumerate(scenes):
|
| 687 |
mp4 = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4"
|
|
|
|
| 706 |
video_dur = audio_dur + 1.5
|
| 707 |
|
| 708 |
try:
|
| 709 |
+
primary_query = None
|
| 710 |
+
narration_lower = narrative.lower()
|
| 711 |
+
is_conclusion_scene = any(k in narration_lower for k in ["conclusion", "summary", "in closing", "final thoughts"])
|
| 712 |
+
|
| 713 |
+
if any(k in narration_lower for k in ["introduction", "welcome", "let's begin"]):
|
| 714 |
+
primary_query = f"abstract technology background {domain}"
|
| 715 |
+
elif is_conclusion_scene:
|
| 716 |
+
primary_query = f"future strategy business meeting {domain}"
|
| 717 |
+
|
| 718 |
+
if primary_query:
|
| 719 |
+
logging.info(f"Scene {i+1}: Pre-emptive guard triggered. Query: '{primary_query}'")
|
| 720 |
+
video_path = search_and_download_pexels_video(primary_query, video_dur, mp4)
|
| 721 |
+
if not video_path: raise ValueError("Pexels search failed for guarded query.")
|
| 722 |
video_parts.append(video_path)
|
| 723 |
+
if is_conclusion_scene:
|
| 724 |
+
conclusion_video_path = video_path
|
|
|
|
|
|
|
| 725 |
else:
|
| 726 |
+
chart_descs = extract_chart_tags(sc)
|
| 727 |
+
if chart_descs:
|
| 728 |
+
logging.info(f"Scene {i+1}: Primary attempt with animated chart.")
|
| 729 |
+
safe_chart(chart_descs[0], df, video_dur, mp4, data_context)
|
| 730 |
+
video_parts.append(str(mp4))
|
| 731 |
+
else:
|
| 732 |
+
raise ValueError("No chart tag found in a middle scene.")
|
| 733 |
except Exception as e:
|
| 734 |
+
logging.warning(f"Scene {i+1}: Primary visual failed ({e}). Marking for fallback.")
|
| 735 |
+
video_parts.append("FALLBACK_NEEDED")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
|
| 737 |
temps.append(mp4)
|
| 738 |
|
| 739 |
+
# Post-processing loop to apply the conclusion video as a fallback
|
| 740 |
+
if not conclusion_video_path: # Failsafe if conclusion scene itself failed
|
| 741 |
+
logging.warning("No conclusion video was generated; creating a generic one for fallbacks.")
|
| 742 |
+
fallback_mp4 = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4"
|
| 743 |
+
conclusion_video_path = search_and_download_pexels_video(f"data visualization abstract {domain}", 5.0, fallback_mp4)
|
| 744 |
+
if conclusion_video_path: temps.append(fallback_mp4)
|
| 745 |
+
|
| 746 |
+
final_video_parts = []
|
| 747 |
+
for part in video_parts:
|
| 748 |
+
if part == "FALLBACK_NEEDED":
|
| 749 |
+
if conclusion_video_path:
|
| 750 |
+
logging.info("Applying conclusion video as fallback for a failed scene.")
|
| 751 |
+
final_video_parts.append(conclusion_video_path)
|
| 752 |
+
else:
|
| 753 |
+
logging.error("Cannot apply fallback; no conclusion video available.")
|
| 754 |
+
else:
|
| 755 |
+
final_video_parts.append(part)
|
| 756 |
+
|
| 757 |
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_vid, \
|
| 758 |
tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as temp_aud, \
|
| 759 |
tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as final_vid:
|
|
|
|
| 762 |
audio_mix_path = Path(temp_aud.name)
|
| 763 |
final_vid_path = Path(final_vid.name)
|
| 764 |
|
| 765 |
+
concat_media(final_video_parts, silent_vid_path)
|
| 766 |
concat_media(audio_parts, audio_mix_path)
|
| 767 |
|
| 768 |
cmd = [
|