""" Generate detailed narration video for specific PPT slide with PDF context This script creates a comprehensive narration video combining: 1. PPT slide content (text + visual) 2. PDF financial data context 3. AI-generated detailed explanation 4. Smooth transitions between content sections Usage: python backend/generate_detailed_narration.py --slide 2 """ import argparse import json import os from pathlib import Path from typing import Dict, Any, List from dotenv import load_dotenv # Import Gemini API from google import genai from google.genai import types class DetailedNarrationGenerator: """Generate detailed narration video for specific slide""" def __init__(self, slide_index: int = 2): load_dotenv() self.project_root = Path(__file__).parent.parent self.slide_index = slide_index 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 load_ppt_slide(self) -> Dict[str, Any]: """Load specific PPT slide data""" import sys sys.path.insert(0, str(self.project_root)) from backend.ppt_extractor import extract_ppt_data ppt_path = self.project_root / "assets/0_input/ライフプラン情報.pptx" ppt_data = extract_ppt_data(str(ppt_path)) if self.slide_index >= len(ppt_data["slides"]): raise ValueError(f"Slide {self.slide_index} not found") return ppt_data["slides"][self.slide_index] def load_pdf_context(self) -> Dict[str, Any]: """Load PDF financial data as context""" import sys sys.path.insert(0, str(self.project_root)) from backend.extractor import extract_pdf_data pdf_path = self.project_root / "assets/0_input/帳票(お客様向け).pdf" return extract_pdf_data(str(pdf_path)) def generate_narration_script(self, slide: Dict, pdf_data: Dict) -> List[Dict]: """ Generate detailed narration script using Gemini Returns: List of narration segments with text, duration, visual cues """ print(f"🤖 Generating detailed narration for slide {self.slide_index}...") prompt = f""" あなたはプロのファイナンシャルプランナーであり、親しみやすい動画解説者です。 以下の情報に基づいて、詳細な解説動画用のナレーション原稿を作成してください。 【PPTスライド内容】 タイトル: {slide['title']} テキスト: {slide['text']} 【PDFから抽出した追加情報】 - 家族代表: {pdf_data.get('client_name', 'テスト太郎')} ({pdf_data.get('current_age', 47)}歳) - 年収: {pdf_data.get('annual_income', 703)}万円 - 現在の資産: {pdf_data.get('current_assets', 836)}万円 - 年間支出: {pdf_data.get('annual_expenditure', 1038)}万円 【要件】 1. スライドの内容を**段落ごとに分解**し、各段落を詳細に解説してください 2. 各セグメントは以下の構造で出力してください: - intro: スライド全体の導入(15秒程度) - section_1: 家族代表の基本情報(20秒程度) - section_2: 配偶者情報(15秒程度) - section_3: お子様情報と教育プラン(20秒程度) - summary: まとめ(10秒程度) 3. 各セグメントには以下を含めてください: - text: ナレーション文(自然な口語体、具体的な数値を含む) - duration: 推定時間(秒) - visualCue: 画面表示の指示("ppt" / "zoom_in" / "highlight_section") - highlightArea: 強調する領域("top" / "middle" / "bottom" / "full") 4. 数値やデータは必ず具体的に言及し、抽象的な表現は避けてください 5. 親しみやすく、わかりやすい言葉遣いを心がけてください **出力形式**: JSON配列として返してください [ {{ "id": "intro", "text": "それでは、ご家族の基本情報について詳しく見ていきましょう。...", "duration": 15, "visualCue": "ppt", "highlightArea": "full" }}, ... ] """ 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)} narration segments") return result def create_smooth_transitions(self, segments: List[Dict]) -> List[Dict]: """Add transition effects between segments""" enhanced_segments = [] for i, seg in enumerate(segments): # Enhance segment with visual metadata enhanced_seg = { **seg, "type": "narration", "visualType": "ppt", "visualAssetIndex": self.slide_index, "brollKeyword": f"Japanese family planning {seg.get('highlightArea', 'full')}", } enhanced_segments.append(enhanced_seg) return enhanced_segments 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 for narration segments...") audio_gen = AudioGenerator() output_dir = self.project_root / "public" / f"slide_{self.slide_index}_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}] Generating: {text[:50]}...") # Use OpenAI TTS API directly try: response = audio_gen.client.audio.speech.create( model="tts-1", voice="nova", input=text ) response.stream_to_file(str(audio_path)) # Get actual duration actual_duration = get_video_duration(str(audio_path)) seg["duration"] = actual_duration seg["audioPath"] = f"public/slide_{self.slide_index}_narration/{audio_filename}" except Exception as e: print(f" ⚠️ Failed: {e}") continue print(f"✓ Generated audio for {len(segments)} segments") return segments def create_render_props(self, segments: List[Dict], slide: Dict, pdf_data: Dict) -> Dict: """Create render_props.json for the detailed narration video""" # Load original props for charts original_props_path = self.project_root / "assets/1_processed/render_props.json" if original_props_path.exists(): with open(original_props_path, "r", encoding="utf-8") as f: original_props = json.load(f) else: original_props = {"charts": {}, "assets": {"slides": []}} # Construct new props new_props = { "meta": { "title": f"【詳細解説】{slide['title']} - {pdf_data.get('client_name', 'お客様')}様", "currencyUnit": "万円", "duration": sum(s.get("duration", 0) for s in segments), "clientName": pdf_data.get("client_name", "お客様") }, "charts": original_props.get("charts", {}), "script": segments, "assets": { **original_props.get("assets", {}), "targetSlide": slide } } # Save to file output_path = self.project_root / f"assets/1_processed/render_props_slide_{self.slide_index}_detailed.json" with open(output_path, "w", encoding="utf-8") as f: json.dump(new_props, f, ensure_ascii=False, indent=2) print(f"✓ Saved render props to {output_path.name}") return new_props def run(self) -> str: """Execute full pipeline""" print(f"🚀 Generating detailed narration video for slide {self.slide_index}...") print() # Load data slide = self.load_ppt_slide() pdf_data = self.load_pdf_context() print(f"✓ Loaded slide: {slide['title']}") print(f"✓ Loaded PDF context for: {pdf_data.get('client_name')}") print() # Generate narration segments = self.generate_narration_script(slide, pdf_data) print() # Add transitions segments = self.create_smooth_transitions(segments) print(f"✓ Added smooth transitions") print() # Generate audio segments = self.generate_audio(segments) print() # Create render props props = self.create_render_props(segments, slide, pdf_data) print() output_file = f"assets/2_output/slide_{self.slide_index}_detailed.mp4" print(f"✅ Pipeline complete! Use the following command to render:") print(f" npx remotion render MainVideo {output_file} \\") print(f" --props=\"assets/1_processed/render_props_slide_{self.slide_index}_detailed.json\"") return output_file def main(): parser = argparse.ArgumentParser( description="Generate detailed narration video for specific PPT slide" ) parser.add_argument("--slide", type=int, default=2, help="Slide index (default: 2)") args = parser.parse_args() try: generator = DetailedNarrationGenerator(slide_index=args.slide) 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())