#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Generate video for specific PPT slides (slides 2 & 3) Extract information from PDF but only display PPT slides """ import os import json import sys from pathlib import Path from typing import Dict, List from dotenv import load_dotenv from google import genai from google.genai import types # Load environment load_dotenv() class PPTSlidesVideoGenerator: def __init__(self, slide_indices: List[int] = [1, 2]): """ Initialize generator for specific PPT slides Args: slide_indices: List of slide indices to generate video for (0-based) Default: [1, 2] means slide 2 and slide 3 (slide_002.png, slide_003.png) """ self.project_root = Path(__file__).parent.parent self.slide_indices = slide_indices # Initialize Gemini client api_key = os.getenv("GEMINI_API_KEY") if not api_key: raise ValueError("GEMINI_API_KEY not found in environment") self.client = genai.Client(api_key=api_key) def load_materials(self) -> Dict: """Load PPT slides and PDF data""" print("📦 Loading materials...") # Load existing render_props to get PPT slides info props_path = self.project_root / "assets/1_processed/render_props.json" with open(props_path, encoding="utf-8") as f: props_data = json.load(f) all_slides = props_data["assets"]["slides"] target_slides = [slide for slide in all_slides if slide["index"] in self.slide_indices] print(f"✓ Loaded {len(target_slides)} PPT slides (indices: {self.slide_indices})") # Extract PDF data using Gemini pdf_data = self.extract_pdf_data() return { "ppt_slides": target_slides, "pdf_data": pdf_data, "existing_props": props_data } def extract_pdf_data(self) -> Dict: """Extract relevant data from PDF using Gemini""" print("📄 Extracting data from PDF...") pdf_path = self.project_root / "assets/0_input/帳票(お客様向け).pdf" # Upload PDF to Gemini with open(pdf_path, 'rb') as f: upload_config = types.UploadFileConfig(mime_type='application/pdf') pdf_file = self.client.files.upload(file=f, config=upload_config) # Wait for processing import time while pdf_file.state == "PROCESSING": print(" ⏳ Processing PDF...") time.sleep(2) pdf_file = self.client.files.get(name=pdf_file.name) if pdf_file.state == "FAILED": raise RuntimeError(f"PDF processing failed: {pdf_file.error}") print(" ✓ PDF uploaded successfully") # Extraction prompt prompt = """ このPDFからライフプラン情報を詳細に抽出してください。 以下のJSON形式で出力してください: { "client_info": { "name": "クライアント名", "birth_date": "生年月日", "age": 年齢, "occupation": "業種", "annual_income": 年収(万円) }, "spouse_info": { "name": "配偶者名", "birth_date": "生年月日", "age": 年齢, "occupation": "業種", "annual_income": 年収(万円) }, "children": [ { "name": "子供の名前", "birth_date": "生年月日", "age": 年齢, "education_plan": "教育プラン(保育園→小学校→中学校など)" } ], "financial_summary": { "household_income": 世帯年収(万円), "annual_expenses": 年間支出(万円), "current_assets": 現在資産(万円), "savings": 貯蓄額(万円) }, "life_events": [ { "year": イベント発生年, "age": その時の年齢, "description": "イベント説明", "cost": 費用(万円) } ], "future_projections": { "retirement_age": 退職年齢, "life_expectancy": 想定寿命, "retirement_savings_needed": 必要な老後資金(万円) } } 可能な限り詳細な情報を抽出してください。数値は必ず含めてください。 """ response = self.client.models.generate_content( model="gemini-2.0-flash", contents=[pdf_file, prompt], config=types.GenerateContentConfig( temperature=0.1, response_mime_type="application/json" ) ) pdf_data = json.loads(response.text) print("✓ PDF data extracted successfully") return pdf_data def generate_script(self, materials: Dict) -> List[Dict]: """Generate narration script for PPT slides based on PDF data""" print("🤖 Generating narration script...") ppt_slides = materials["ppt_slides"] pdf_data = materials["pdf_data"] # Handle case where pdf_data might be a list or dict if isinstance(pdf_data, list): pdf_data = pdf_data[0] if pdf_data else {} # Build detailed context from PDF client_info = pdf_data.get("client_info", {}) spouse_info = pdf_data.get("spouse_info", {}) children = pdf_data.get("children", []) financial = pdf_data.get("financial_summary", {}) life_events = pdf_data.get("life_events", []) prompt = f""" あなたはプロのファイナンシャルプランナーであり、動画ナレーターです。 【重要】以下の2枚のPPTスライドについて、PDFから抽出した詳細情報を使って、 豊富で魅力的なナレーション原稿を作成してください。 【利用可能なPPTスライド】 スライド1 (slide_002.png): インデックス={ppt_slides[0]["index"]} スライド2 (slide_003.png): インデックス={ppt_slides[1]["index"] if len(ppt_slides) > 1 else "N/A"} 【PDFから抽出した詳細情報】 ■ クライアント情報: {json.dumps(client_info, ensure_ascii=False, indent=2)} ■ 配偶者情報: {json.dumps(spouse_info, ensure_ascii=False, indent=2)} ■ 子供情報: {json.dumps(children, ensure_ascii=False, indent=2)} ■ 財務サマリー: {json.dumps(financial, ensure_ascii=False, indent=2)} ■ ライフイベント (最初の5件): {json.dumps(life_events[:5], ensure_ascii=False, indent=2)} 【要件】 1. **2枚のPPTスライドのみを使用**してください - スライド1枚あたり2-4個のセグメントを作成 - 合計で4-6個のセグメントを生成 2. 各セグメントの構造: - id: セグメントID (例: "slide2_intro", "slide2_family", "slide3_events") - text: ナレーション文 (30-60秒分の自然な日本語、具体的な数値を多く含む) - duration: 推定時間(秒、実際の音声生成後に更新されます) - visualType: "ppt" (固定) - visualAssetIndex: PPTスライドのインデックス (0始まり) - highlightArea: "full" (固定) 3. **ナレーション作成のポイント**: - PDFから抽出した具体的な数値を必ず使用(名前、年齢、生年月日、年収、業種など) - 温かみのある親しみやすい語り口 - 視聴者が理解しやすいように、情報を整理して伝える - 各セグメント30-60秒程度の適切な長さ - 同じスライドを連続して使用しても良い(内容が異なれば) 4. **セグメント例**: - スライド2の前半: 家族構成の紹介 - スライド2の後半: 各メンバーの詳細(年齢、職業、年収など) - スライド3の前半: ライフイベントの概要 - スライド3の後半: 具体的なイベントと費用 **出力形式**: 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" ) ) segments = json.loads(response.text) print(f"✓ Generated {len(segments)} script segments") # Display preview print("\n📝 Script Preview:") for i, seg in enumerate(segments, 1): print(f"\n{i}. [{seg['id']}] - Slide {seg['visualAssetIndex'] + 1}") print(f" Text: {seg['text'][:100]}...") return segments def generate_audio(self, segments: List[Dict]) -> List[Dict]: """Generate TTS audio for each segment""" sys.path.insert(0, str(self.project_root)) from backend.audio_generator import AudioGenerator from backend.video_processor.utils import get_video_duration print("\n🎙️ Generating audio...") audio_gen = AudioGenerator() output_dir = self.project_root / "public" / "ppt_slides_audio" 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)}] {seg['id']}: Generating audio...") 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/ppt_slides_audio/{audio_filename}" print(f" ✓ Duration: {actual_duration:.2f}s") except Exception as e: print(f" ⚠️ Failed: {e}") seg["duration"] = 5.0 # Fallback duration continue total_duration = sum(s.get("duration", 0) for s in segments) print(f"\n✓ Generated {len(segments)} audio files (Total: {total_duration:.1f}s)") return segments def create_render_props(self, segments: List[Dict], materials: Dict) -> Dict: """Create render_props with only PPT slides""" existing_props = materials["existing_props"] new_props = { "meta": { "title": f"ライフプラン紹介 - スライド{self.slide_indices[0]+1}・{self.slide_indices[1]+1 if len(self.slide_indices) > 1 else ''}", "currencyUnit": "万円", "duration": sum(s.get("duration", 0) for s in segments), "clientName": materials["pdf_data"].get("client_info", {}).get("name", "お客様") }, "charts": existing_props.get("charts", {}), "script": segments, "assets": { "slides": existing_props["assets"]["slides"], # Keep all slides "videoClips": [], "broll": existing_props["assets"].get("broll", {}) } } output_path = self.project_root / "assets/1_processed/render_props_ppt_slides.json" with open(output_path, "w", encoding="utf-8") as f: json.dump(new_props, f, ensure_ascii=False, indent=2) print(f"\n✓ Saved render props to {output_path.name}") return new_props def run(self) -> str: """Execute full pipeline""" print(f"🚀 Generating video for PPT slides {[i+1 for i in self.slide_indices]}...\n") # Load materials materials = self.load_materials() print() # Generate script segments = self.generate_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/ppt_slides_video.mp4" print(f"✅ Pipeline complete!") print(f"\n📹 Render command:") print(f" npx remotion render MainVideo {output_file} \\") print(f" --props=\"assets/1_processed/render_props_ppt_slides.json\" --concurrency=1") print() print(f"📊 Video Summary:") print(f" - Target slides: {[f'slide_{i+1:03d}.png' for i in self.slide_indices]}") print(f" - Total segments: {len(segments)}") print(f" - Total duration: {props['meta']['duration']:.1f}s") print(f" - Client: {props['meta']['clientName']}") return output_file if __name__ == "__main__": # Generate video for slides 2 and 3 (indices 1 and 2) generator = PPTSlidesVideoGenerator(slide_indices=[1, 2]) generator.run()