#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Generate introduction video for PPT slides 2 & 3 with API-based content generation - Extract detailed info from PDF using Gemini API - Generate rich narration based on PDF data - Only display PPT slides (no PDF page switching) - Generate audio using OpenAI TTS """ import os import json import sys import time 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 PPTIntroVideoGenerator: def __init__(self): self.project_root = Path(__file__).parent.parent # 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) print("✓ Gemini API client initialized") def extract_pdf_info(self) -> Dict: """Extract detailed information from PDF using Gemini""" print("\n📄 Extracting information from PDF...") pdf_path = self.project_root / "assets/0_input/帳票(お客様向け).pdf" # Upload PDF to Gemini print(" Uploading PDF...") 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 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 - focus on family and life events prompt = """ このPDFから、家族紹介とライフイベントに関する詳細情報を抽出してください。 以下のJSON形式で出力してください: { "family": { "representative": { "name": "代表者名", "age": 年齢, "birth_date": "生年月日", "occupation": "業種", "annual_income": 年収(万円), "description": "簡単な説明" }, "spouse": { "name": "配偶者名", "age": 年齢, "birth_date": "生年月日", "occupation": "業種", "annual_income": 年収(万円), "description": "簡単な説明" }, "children": [ { "name": "子供の呼称", "age": 年齢, "birth_date": "生年月日", "education_plan": "進学プラン(保育園→小学校→中学校など)", "description": "簡単な説明" } ], "household": { "total_income": 世帯年収(万円), "annual_expenses": 年間支出(万円), "current_assets": 現在資産(万円), "savings_per_year": 年間貯蓄額(万円) } }, "life_events": [ { "year": イベント発生年, "age_representative": その時の代表者年齢, "event_name": "イベント名", "description": "詳細説明", "cost": 費用(万円), "category": "カテゴリー(教育/住宅/退職など)" } ], "financial_outlook": { "retirement_age": 退職年齢, "life_expectancy": 想定寿命, "key_concerns": ["懸念事項1", "懸念事項2"], "opportunities": ["機会1", "機会2"] } } 可能な限り具体的で詳細な情報を抽出してください。 """ print(" Analyzing PDF content...") response = self.client.models.generate_content( model="gemini-2.0-flash-exp", contents=[pdf_file, prompt], config=types.GenerateContentConfig( temperature=0.1, response_mime_type="application/json" ) ) pdf_info = json.loads(response.text) print(" ✓ PDF information extracted") # Print summary family = pdf_info.get("family", {}) rep = family.get("representative", {}) print(f"\n 📊 Extracted Info:") print(f" - Client: {rep.get('name')}") print(f" - Household income: {family.get('household', {}).get('total_income')}万円") print(f" - Life events: {len(pdf_info.get('life_events', []))} events") return pdf_info def generate_narration_script(self, pdf_info: Dict) -> List[Dict]: """Generate narration script using Gemini based on PDF info""" print("\n🤖 Generating narration script...") family = pdf_info.get("family", {}) events = pdf_info.get("life_events", []) outlook = pdf_info.get("financial_outlook", {}) prompt = f""" あなたはプロのファイナンシャルプランナーであり、温かみのある動画ナレーターです。 【重要な制約】 - PPTスライド2枚(slide_002.png と slide_003.png)のみを使用 - スライド2: 家族基本情報(家族構成、年齢、職業、年収) - スライド3: ライフイベント(将来の予定) - 各スライドにつき2-4個のセグメントを作成 - 合計5-7個のセグメントを生成 【抽出したPDF情報】 ■ 家族情報: {json.dumps(family, ensure_ascii=False, indent=2)} ■ ライフイベント(最初の5件): {json.dumps(events[:5] if events else [], ensure_ascii=False, indent=2)} ■ 将来展望: {json.dumps(outlook, ensure_ascii=False, indent=2)} 【要件】 1. セグメント構造: - id: ユニークなID(例: "s2_intro", "s2_family", "s3_events") - text: ナレーション文(30-50秒分、温かく親しみやすい語り口) - estimatedDuration: 推定時間(秒、実際の音声生成後に更新されます) - visualType: "ppt" (固定) - visualAssetIndex: 1(スライド2)または 2(スライド3) - highlightArea: "full" (固定) 2. ナレーションのポイント: - PDFの具体的な数値を必ず含める(名前、年齢、年収、費用など) - 親しみやすく温かい語り口 - 各セグメント30-50秒程度 - 自然な流れで情報を伝える - 聞き手が理解しやすいように整理 3. セグメント配分例: 【スライド2(家族情報)】 - セグメント1: 家族紹介の導入(10-15秒) - セグメント2: 代表者の詳細(25-35秒) - セグメント3: 配偶者と子供の詳細(30-40秒) - セグメント4: 家計サマリー(20-30秒) 【スライド3(ライフイベント)】 - セグメント5: イベント概要の導入(10-15秒) - セグメント6: 具体的なイベント紹介(35-45秒) - セグメント7: 将来への展望(20-30秒) 4. 推定時間の設定: - 日本語の読み上げ速度: 約300-350文字/分(5-6文字/秒) - テキストの長さから適切な時間を推定 【出力】 JSON配列のみを返してください。説明文は不要です。 例: [ {{ "id": "s2_intro", "text": "それでは、テスト太郎様のご家族についてご紹介いたします...", "estimatedDuration": 12, "visualType": "ppt", "visualAssetIndex": 1, "highlightArea": "full" }}, ... ] """ response = self.client.models.generate_content( model="gemini-2.0-flash-exp", contents=prompt, config=types.GenerateContentConfig( temperature=0.8, response_mime_type="application/json" ) ) segments = json.loads(response.text) print(f" ✓ Generated {len(segments)} narration segments") # Display preview print("\n 📝 Script Preview:") for i, seg in enumerate(segments, 1): slide_num = seg['visualAssetIndex'] + 1 print(f"\n {i}. [{seg['id']}] - Slide {slide_num} ({seg.get('estimatedDuration', 0)}s)") print(f" {seg['text'][:80]}...") return segments def generate_audio(self, segments: List[Dict]) -> List[Dict]: """Generate TTS audio using OpenAI""" 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 with OpenAI TTS...") audio_gen = AudioGenerator() output_dir = self.project_root / "public" / "ppt_intro_audio" output_dir.mkdir(parents=True, exist_ok=True) for i, seg in enumerate(segments, 1): text = seg["text"] audio_filename = f"narration_{seg['id']}.mp3" audio_path = output_dir / audio_filename print(f" [{i}/{len(segments)}] {seg['id']}: Generating...") 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_intro_audio/{audio_filename}" print(f" ✓ {actual_duration:.2f}s") except Exception as e: print(f" ⚠️ Failed: {e}") seg["duration"] = seg.get("estimatedDuration", 10) continue total_duration = sum(s.get("duration", 0) for s in segments) print(f"\n ✓ Generated {len(segments)} audio files ({total_duration:.1f}s total)") return segments def create_render_props(self, segments: List[Dict]) -> Dict: """Create render_props configuration""" print("\n📦 Creating render props...") # Load existing render_props for charts and assets props_path = self.project_root / "assets/1_processed/render_props.json" with open(props_path, encoding="utf-8") as f: existing_props = json.load(f) new_props = { "meta": { "title": "ライフプラン紹介 - 家族とイベント", "currencyUnit": "万円", "duration": sum(s.get("duration", 0) for s in segments), "clientName": existing_props["meta"].get("clientName", "お客様") }, "charts": existing_props.get("charts", {}), "script": segments, "assets": { "slides": existing_props["assets"]["slides"], "videoClips": [], "broll": existing_props["assets"].get("broll", {}) } } output_path = self.project_root / "assets/1_processed/render_props_ppt_intro.json" with open(output_path, "w", encoding="utf-8") as f: json.dump(new_props, f, ensure_ascii=False, indent=2) print(f" ✓ Saved to {output_path.name}") return new_props def run(self) -> str: """Execute full pipeline""" print("=" * 70) print("🚀 PPT Introduction Video Generator") print("=" * 70) try: # Extract PDF info pdf_info = self.extract_pdf_info() # Generate script segments = self.generate_narration_script(pdf_info) # Generate audio segments = self.generate_audio(segments) # Create render props props = self.create_render_props(segments) # Summary print("\n" + "=" * 70) print("✅ Generation Complete!") print("=" * 70) output_file = "assets/2_output/ppt_intro_video.mp4" print(f"\n📹 Render command:") print(f" npx remotion render MainVideo {output_file} \\") print(f" --props=\"assets/1_processed/render_props_ppt_intro.json\" --concurrency=1") print(f"\n📊 Video Summary:") print(f" - Target slides: slide_002.png, slide_003.png") print(f" - Total segments: {len(segments)}") print(f" - Total duration: {props['meta']['duration']:.1f}s") print(f" - Client: {props['meta']['clientName']}") print(f" - Config file: render_props_ppt_intro.json") return output_file except Exception as e: print(f"\n❌ Error: {e}") import traceback traceback.print_exc() raise if __name__ == "__main__": generator = PPTIntroVideoGenerator() generator.run()