#!/usr/bin/env python3 """ Auto Life Plan Video Generator - Main Entry Point This script orchestrates the entire video generation pipeline: 1. Parse PDF/Video inputs (Gemini Pro / Whisper) 2. Generate script (Gemini Pro) 3. Fetch B-Roll assets (Pexels API) 4. Build chart data (Simulation engine) 5. Generate render_props.json 6. Trigger Remotion render Usage: python main.py --pdf path/to/report.pdf python main.py --pdf report.pdf --video interview.mp4 python main.py --config config.json """ import argparse import json import subprocess import sys from pathlib import Path from typing import Dict, Any # Import backend modules (to be implemented) # from backend.extractor import extract_pdf_data, extract_video_data # from backend.scripter import generate_script # from backend.media_fetcher import fetch_broll # from backend.data_builder import build_chart_data class VideoGeneratorPipeline: """Main orchestrator for video generation""" def __init__(self, config: Dict[str, Any]): self.config = config self.project_root = Path(__file__).parent self.assets_dir = self.project_root / "assets" self.input_dir = self.assets_dir / "0_input" self.processed_dir = self.assets_dir / "1_processed" self.output_dir = self.assets_dir / "2_output" # Ensure directories exist for dir_path in [self.input_dir, self.processed_dir, self.output_dir]: dir_path.mkdir(parents=True, exist_ok=True) # Auto-discover input files if not specified self._auto_discover_inputs() def _auto_discover_inputs(self): """ Auto-discover input files from assets/0_input/ directory Priority: PDF > PPTX > Video """ print("๐Ÿ” Scanning input directory for files...") # Find PDF files if not self.config.get("pdf_path"): pdf_files = list(self.input_dir.glob("*.pdf")) if pdf_files: self.config["pdf_path"] = str(pdf_files[0]) print(f" โœ“ Found PDF: {pdf_files[0].name}") # Find PPTX files if not self.config.get("ppt_path"): ppt_files = list(self.input_dir.glob("*.pptx")) # Filter out temporary files (starting with ~$) ppt_files = [f for f in ppt_files if not f.name.startswith("~$")] if ppt_files: self.config["ppt_path"] = str(ppt_files[0]) print(f" โœ“ Found PPT: {ppt_files[0].name}") # Find video files if not self.config.get("video_path"): video_extensions = ["*.mp4", "*.mov", "*.avi", "*.mkv"] for ext in video_extensions: video_files = list(self.input_dir.glob(ext)) if video_files: self.config["video_path"] = str(video_files[0]) print(f" โœ“ Found Video: {video_files[0].name}") break # Summary if not any([self.config.get("pdf_path"), self.config.get("ppt_path")]): print(" โš ๏ธ No PDF or PPT files found in input directory") print() def run(self) -> str: """Execute the full pipeline and return output video path""" print("๐Ÿš€ Starting Auto Life Plan Video Generation Pipeline...") # Step 1: Extract data from PDF or PPT print("\n๐Ÿ“„ Step 1/6: Extracting data from documents...") pdf_data = self.extract_pdf() ppt_data = self.extract_ppt() # Step 2: Extract data from video (if provided) print("\n๐ŸŽฅ Step 2/6: Processing video footage...") video_data = self.extract_video() # Step 3: Generate narration script print("\nโœ๏ธ Step 3/6: Generating narration script...") script = self.generate_script(pdf_data, ppt_data, video_data) # Step 3.5: Generate audio from script (NEW) print("\n๐ŸŽ™๏ธ Step 3.5/6: Generating audio narration...") script = self.generate_audio(script) # Step 4: Fetch B-Roll assets (TEMPORARILY DISABLED) print("\n๐ŸŽฌ Step 4/6: Fetching B-Roll assets (SKIPPED)...") broll_assets = {} # self.fetch_broll(script, video_data) # Step 5: Build render props print("\n๐Ÿ“Š Step 5/6: Building render props...") render_props = self.build_render_props(pdf_data, ppt_data, script, broll_assets, video_data) # Save render_props.json props_file = self.processed_dir / "render_props.json" with open(props_file, "w", encoding="utf-8") as f: json.dump(render_props, f, ensure_ascii=False, indent=2) print(f" โœ“ Saved render props to {props_file}") # Step 6: Render video with Remotion print("\n๐ŸŽž๏ธ Step 6/6: Rendering video with Remotion...") output_path = self.render_video(props_file) print(f"\nโœ… Video generation complete! Output: {output_path}") return str(output_path) def extract_pdf(self) -> Dict[str, Any]: """Extract data from PDF using Gemini Pro""" pdf_path = self.config.get("pdf_path") if not pdf_path or not Path(pdf_path).exists(): print(" [SKIP] No PDF file found") return {} print(f" ๐Ÿ“„ Processing PDF: {Path(pdf_path).name}") try: from backend.extractor import extract_pdf_data pdf_data = extract_pdf_data(pdf_path) print(f" โœ“ Successfully extracted data for: {pdf_data.get('client_name', 'Unknown')}") return pdf_data except Exception as e: print(f" โš ๏ธ PDF extraction failed: {e}") import traceback traceback.print_exc() return {} def extract_ppt(self) -> Dict[str, Any]: """Extract slides from PowerPoint presentation""" ppt_path = self.config.get("ppt_path") if not ppt_path or not Path(ppt_path).exists(): print(" [SKIP] No PPT file found") return {} print(f" ๐Ÿ“Š Processing PPT: {Path(ppt_path).name}") try: from backend.ppt_extractor import extract_ppt_data ppt_data = extract_ppt_data(ppt_path) print(f" โœ“ Successfully extracted {ppt_data.get('total_slides', 0)} slides") return ppt_data except Exception as e: print(f" โš ๏ธ PPT extraction failed: {e}") import traceback traceback.print_exc() return {} def extract_video(self) -> Dict[str, Any]: """Extract data from video using Whisper (if video provided)""" video_path = self.config.get("video_path") if not video_path or not Path(video_path).exists(): print(" [SKIP] No video file provided or file not found") return {} try: from backend.extractor import extract_video_data video_data = extract_video_data(video_path) # Optionally extract video clips if self.config.get("extract_clips", False): print(" โœ‚๏ธ Extracting key video clips...") from backend.video_slicer import extract_video_clips clips = extract_video_clips(video_path, video_data, max_clips=3) video_data["clips"] = clips return video_data except Exception as e: print(f" โš ๏ธ Video extraction failed: {e}") import traceback traceback.print_exc() return {} def generate_script(self, pdf_data: Dict, ppt_data: Dict, video_data: Dict) -> Dict[str, Any]: """Generate narration script using Gemini Pro""" print(" โœ๏ธ Generating script with Gemini Pro...") from backend.scripter import generate_script # Merge PPT and PDF data combined_data = {**pdf_data} if ppt_data: combined_data["presentation"] = ppt_data try: script = generate_script(combined_data, video_data) print(f" โœ“ Generated {len(script.get('segments', []))} script segments") return script except Exception as e: print(f" โŒ Script generation failed: {e}") import traceback traceback.print_exc() raise def generate_audio(self, script_data: Dict) -> Dict[str, Any]: """Generate audio narration from script using OpenAI TTS""" from backend.audio_generator import generate_audio_for_script from backend.video_processor.utils import get_video_duration try: # 1. Generate TTS audio for non-video segments script_data = generate_audio_for_script(script_data, voice="alloy") # 2. Sync duration for video segments with actual clip duration print(" ๐ŸŽฌ Syncing durations for interview video segments...") video_data = self.extract_video() # Get cached video data with clips clips = video_data.get("clips", []) for segment in script_data.get("segments", []): if segment.get("visualType") == "video": idx = segment.get("visualAssetIndex", -1) if 0 <= idx < len(clips): clip = clips[idx] clip_path = clip.get("outputPath") or clip.get("filepath") if clip_path and Path(clip_path).exists(): actual_duration = get_video_duration(clip_path) print(f" โœ“ Video Segment {segment['id']}: Updated duration to {actual_duration:.2f}s (Clip {idx})") segment["duration"] = actual_duration return script_data except Exception as e: print(f" โš ๏ธ Audio generation failed: {e}") print(" Continuing without audio...") return script_data def fetch_broll(self, script: Dict, video_data: Dict = None) -> Dict[str, Any]: """Fetch B-Roll footage from Pexels API based on script context""" try: from backend.media_fetcher import MediaFetcher # 1. Extract keywords explicitly requested in script segments keywords = set() for segment in script.get("segments", []): if segment.get("visualType") == "broll" and segment.get("brollKeyword"): keywords.add(segment.get("brollKeyword")) # 2. Add keywords from video data as fallback/supplement if video_data and "keywords" in video_data: for kw in video_data["keywords"][:3]: keywords.add(kw) # 3. Use default keywords if still empty if not keywords: keywords.update(["ๅฎถๆ—", "่€ๅพŒ", "่ฒฏ่“„"]) keyword_list = list(keywords) print(f" ๐ŸŽฌ Fetching B-Roll for context: {', '.join(keyword_list)}") fetcher = MediaFetcher() broll_results = fetcher.fetch_broll(keyword_list, max_per_keyword=1) # Convert to simple filepath mapping broll_assets = {} for keyword, videos in broll_results.items(): if videos: # Store only the filename to be used with staticFile() in Remotion broll_assets[keyword] = Path(videos[0]["filepath"]).name return broll_assets except Exception as e: print(f" โš ๏ธ B-Roll fetching failed: {e}") import traceback traceback.print_exc() return {} def build_render_props( self, pdf_data: Dict, ppt_data: Dict, script: Dict, broll: Dict, video_data: Dict = None ) -> Dict[str, Any]: """Build the complete render_props.json structure""" print(" Building chart data and render props...") # Use the financial simulator to build chart data that matches the # TypeScript RenderProps contract (frontend/src/render-props-types.ts) from backend.data_builder import build_chart_data charts = build_chart_data(pdf_data) if pdf_data else {} # Add video clips if available video_clips = [] if video_data and "clips" in video_data: video_clips = video_data["clips"] # Add PPT slides if available ppt_slides = [] if ppt_data and "slides" in ppt_data: ppt_slides = [ { "index": slide["index"], "title": slide["title"], "image_path": slide["image_path"].replace("\\", "/"), } for slide in ppt_data["slides"] ] return { "meta": { "title": f"{pdf_data.get('client_name', 'ใŠๅฎขๆง˜')}ๆง˜ใฎไบบ็”Ÿ่จญ่จˆ", "currencyUnit": "ไธ‡ๅ††", "duration": 180, # 3 minutes "clientName": pdf_data.get("client_name"), }, "charts": charts, "script": script["segments"], "assets": { "broll": broll, "videoClips": video_clips, "slides": ppt_slides, }, } def render_video(self, props_file: Path) -> Path: """Render video using Remotion""" output_file = self.output_dir / "life_plan_video.mp4" # If dry-run flag is set, skip rendering step if self.config.get("dry_run"): print(" [SKIP] Dry run enabled - not rendering video with Remotion") return output_file # Remotion render command # Use "npx.cmd" on Windows to ensure subprocess can find it npx_executable = "npx.cmd" if sys.platform == "win32" else "npx" cmd = [ npx_executable, "remotion", "render", "src/index.ts", "MainVideo", str(output_file), f"--props={props_file}", "--concurrency=1", ] print(f" Running: {' '.join(cmd)}") try: subprocess.run(cmd, check=True, cwd=self.project_root, shell=True) except subprocess.CalledProcessError as e: print(f" โŒ Remotion render failed: {e}") sys.exit(1) return output_file def main(): """CLI entry point""" parser = argparse.ArgumentParser( description="Auto Life Plan Video Generator", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python main.py --pdf assets/0_input/report.pdf python main.py --pdf report.pdf --video interview.mp4 python main.py --config config.json --output custom_output.mp4 """, ) parser.add_argument( "--pdf", type=str, help="Path to the Life Plan PDF report (optional if files exist in assets/0_input/)", ) parser.add_argument( "--ppt", type=str, help="Path to the PowerPoint presentation (optional if files exist in assets/0_input/)", ) parser.add_argument( "--video", type=str, help="Path to the interview video (optional)", ) parser.add_argument( "--config", type=str, help="Path to configuration JSON file", ) parser.add_argument( "--output", type=str, help="Custom output filename (default: life_plan_video.mp4)", ) parser.add_argument( "--dry-run", action="store_true", help="Generate render_props.json without rendering", ) parser.add_argument( "--extract-clips", action="store_true", help="Extract video clips from source video (optional)", ) args = parser.parse_args() # Build configuration config = {} if args.config: with open(args.config, "r", encoding="utf-8") as f: config = json.load(f) else: # Only require --pdf if no files found in input directory config["pdf_path"] = args.pdf config["ppt_path"] = args.ppt config["video_path"] = args.video if args.output: config["output_filename"] = args.output config["dry_run"] = args.dry_run config["extract_clips"] = args.extract_clips # Run pipeline pipeline = VideoGeneratorPipeline(config) try: output_path = pipeline.run() print(f"\n๐ŸŽ‰ Success! Video saved to: {output_path}") except Exception as e: print(f"\nโŒ Pipeline failed: {e}") import traceback traceback.print_exc() sys.exit(1) if __name__ == "__main__": main()