""" Generate comprehensive video using PDF pages as visual anchor (NOT PPT) CRITICAL: This script uses PDF page content as the structural backbone - PDF pages are converted to images and used as visual materials - PPT slides are NOT used as the main visual sequence - Content organization follows PDF page structure Usage: python backend/generate_combined_video.py """ import argparse import json import os from pathlib import Path from typing import Dict, Any, List from dotenv import load_dotenv from google import genai from google.genai import types class CombinedVideoGenerator: """Generate video with PDF pages as structural anchor""" def __init__(self): load_dotenv() self.project_root = Path(__file__).parent.parent self.api_key = os.getenv("GEMINI_API_KEY") if not self.api_key: raise ValueError("GEMINI_API_KEY not found in .env") self.client = genai.Client(api_key=self.api_key) def convert_pdf_to_images(self) -> List[str]: """Convert PDF pages to images for video use""" print("📄 Converting PDF pages to images...") try: import fitz # PyMuPDF except ImportError: print("⚠️ PyMuPDF not installed, using placeholder images") return [] pdf_path = self.project_root / "assets/0_input/帳票(お客様向け).pdf" output_dir = self.project_root / "public" / "pdf_pages" output_dir.mkdir(parents=True, exist_ok=True) doc = fitz.open(str(pdf_path)) image_paths = [] for page_num in range(len(doc)): page = doc[page_num] pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) # 2x resolution image_filename = f"pdf_page_{page_num + 1:03d}.png" image_path = output_dir / image_filename pix.save(str(image_path)) image_paths.append(f"pdf_pages/{image_filename}") print(f" ✓ Page {page_num + 1}/{len(doc)}") doc.close() print(f"✓ Converted {len(image_paths)} PDF pages") return image_paths def load_all_materials(self) -> Dict[str, Any]: """Load materials with PDF as primary visual source""" import sys sys.path.insert(0, str(self.project_root)) print("📦 Loading all materials...") # Convert PDF to images (PRIMARY VISUAL SOURCE) pdf_images = self.convert_pdf_to_images() # Load existing render_props for charts ONLY props_path = self.project_root / "assets/1_processed/render_props.json" with open(props_path, "r", encoding="utf-8") as f: props_data = json.load(f) print(f"✓ Loaded chart data") # Prepare PDF summary pdf_summary = { "client_name": props_data["meta"].get("clientName", "お客様"), "current_age": 47, "spouse_age": 37, "child_age": 7, "annual_income": 703, "current_assets": 836, "annual_expenditure": 1038, "charts": props_data["charts"], "pdf_images": pdf_images # PRIMARY VISUAL ASSETS } print(f"✓ Prepared PDF data with {len(pdf_images)} page images") return { "pdf": pdf_summary, "existing_props": props_data } def generate_comprehensive_script(self, materials: Dict) -> List[Dict]: """ Generate script using PDF page structure (NOT PPT) Structure based on PDF pages: 1. Opening (intro) 2. Family basic info (PDF page 1) 3. Life events (PDF page 2) 4. Asset projection intro (PDF page 3) 5. Chart analysis (use chart view) 6. Education plan (PDF page 4) 7. Summary (PDF page 5) """ print("🤖 Generating script based on PDF structure...") pdf_data = materials["pdf"] pdf_images = pdf_data["pdf_images"] prompt = f""" あなたはプロのファイナンシャルプランナーであり、動画解説のナレーターです。 以下の情報に基づいて、**PDFページを視覚素材とする**解説動画のナレーション原稿を作成してください。 【重要】視覚素材はPDFページ画像を使用します(PPTスライドではありません) 【家族情報】 - 家族代表: {pdf_data["client_name"]} ({pdf_data["current_age"]}歳) - 配偶者: {pdf_data["spouse_age"]}歳 - お子様: {pdf_data["child_age"]}歳 - 現在の年収: {pdf_data["annual_income"]}万円 - 現在の資産: {pdf_data["current_assets"]}万円 - 年間支出: {pdf_data["annual_expenditure"]}万円 【利用可能なPDFページ画像】 {len(pdf_images)}ページ分の画像が利用可能 【要件】 1. 以下の構成で原稿を作成(**家族成員紹介のみ**): - intro: オープニング(10秒、家族紹介の導入) - rep_intro: 家族代表紹介(20秒、PDFページ1、代表者の詳細) - spouse_intro: 配偶者紹介(15秒、PDFページ1、配偶者の詳細) - child_intro: お子様紹介(15秒、PDFページ1、お子様と教育プラン) - family_summary: 家族紹介まとめ(10秒、家族全体のまとめ) 2. 各セグメントの構造: - id: セグメントID - text: ナレーション文(具体的な数値・年齢・職業を含む) - duration: 推定時間(秒) - visualType: "pdf_page" - pdfPageIndex: PDFページ番号(0始まり) - highlightArea: "full" 3. **重要**: 家族成員の年齢、職業、年収など具体的な情報を丁寧に紹介 4. 温かみのある親しみやすい語り口で、家族の魅力を伝える **出力形式**: JSON配列 """ response = self.client.models.generate_content( model="gemini-2.0-flash", contents=prompt, config=types.GenerateContentConfig( temperature=0.7, response_mime_type="application/json" ) ) result = json.loads(response.text) print(f"✓ Generated {len(result)} script segments") return result def generate_audio(self, segments: List[Dict]) -> List[Dict]: """Generate TTS audio for each segment""" import sys sys.path.insert(0, str(self.project_root)) from backend.audio_generator import AudioGenerator from backend.video_processor.utils import get_video_duration print("🎙️ Generating audio...") audio_gen = AudioGenerator() output_dir = self.project_root / "public" / "pdf_narration" output_dir.mkdir(parents=True, exist_ok=True) for i, seg in enumerate(segments): text = seg["text"] audio_filename = f"narration_{seg['id']}.mp3" audio_path = output_dir / audio_filename print(f" [{i+1}/{len(segments)}] {text[:40]}...") try: response = audio_gen.client.audio.speech.create( model="tts-1", voice="nova", input=text ) response.stream_to_file(str(audio_path)) actual_duration = get_video_duration(str(audio_path)) seg["duration"] = actual_duration seg["audioPath"] = f"public/pdf_narration/{audio_filename}" except Exception as e: print(f" ⚠️ Failed: {e}") continue print(f"✓ Generated {len(segments)} audio files") return segments def create_render_props(self, segments: List[Dict], materials: Dict) -> Dict: """Create render_props using PDF pages as visuals""" existing_props = materials["existing_props"] pdf_images = materials["pdf"]["pdf_images"] # Build PDF page assets pdf_page_assets = [ { "index": i, "image_path": f"public/{img}", "description": f"PDF Page {i+1}" } for i, img in enumerate(pdf_images) ] new_props = { "meta": { "title": f"【PDF解説】{materials['pdf']['client_name']}様のライフプラン", "currencyUnit": "万円", "duration": sum(s.get("duration", 0) for s in segments), "clientName": materials["pdf"]["client_name"] }, "charts": existing_props["charts"], "script": segments, "assets": { "pdfPages": pdf_page_assets, # PDF pages as PRIMARY visuals "slides": [], # Empty - not using PPT "videoClips": existing_props["assets"].get("videoClips", []), "broll": existing_props["assets"].get("broll", {}) } } output_path = self.project_root / "assets/1_processed/render_props_pdf_based.json" with open(output_path, "w", encoding="utf-8") as f: json.dump(new_props, f, ensure_ascii=False, indent=2) print(f"✓ Saved PDF-based render props to {output_path.name}") return new_props def run(self) -> str: """Execute full pipeline""" print("🚀 Generating PDF-based comprehensive video...\n") # Load materials (PDF as primary) materials = self.load_all_materials() print() # Generate script segments = self.generate_comprehensive_script(materials) print() # Generate audio segments = self.generate_audio(segments) print() # Create render props props = self.create_render_props(segments, materials) print() output_file = "assets/2_output/pdf_based_video.mp4" print(f"✅ Pipeline complete! Render command:") print(f" npx remotion render MainVideo {output_file} \\") print(f" --props=\"assets/1_processed/render_props_pdf_based.json\"") print() print("📋 改动说明:") print(" 1. 替换视觉素材: PPT slides → PDF page images") print(" 2. 结构锚点: 按PDF页面组织内容,而非PPT顺序") print(" 3. 新增assets.pdfPages字段存储PDF页面图像") print(" 4. visualType: 'pdf_page'(替代'ppt')") print(" 5. pdfPageIndex: 指向PDF页码(替代slideIndex)") return output_file def main(): try: generator = CombinedVideoGenerator() generator.run() except Exception as e: print(f"\n❌ Generation failed: {e}") import traceback traceback.print_exc() return 1 return 0 if __name__ == "__main__": exit(main())