#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Generate family introduction video combining PPT and PDF pages Focus: Family member introduction ONLY """ 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 FamilyIntroGenerator: 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) def load_materials(self) -> Dict: """Load PPT, PDF and existing render_props data""" print("📦 Loading materials...") # Load existing render_props 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) # Convert PDF pages to images pdf_images = self.convert_pdf_to_images() # Extract family info from existing data family_info = { "rep_name": props_data["meta"].get("clientName", "テスト太郎"), "rep_age": 47, "rep_birth": "1980年1月1日", "rep_income": 703, "rep_industry": "情報通信業", "spouse_name": "花子", "spouse_age": 37, "spouse_birth": "1990年2月2日", "spouse_income": 368, "spouse_industry": "卸売・小売業", "child_name": "息子", "child_age": 7, "child_birth": "2020年3月3日", "child_education": "保育園認可→小学校公立→中学校私立", "total_income": 1071, "total_assets": 852, "annual_expense": 1038 } print(f"✓ Loaded PPT slides: {len(props_data['assets']['slides'])} pages") print(f"✓ Loaded PDF images: {len(pdf_images)} pages") return { "family": family_info, "ppt_slides": props_data["assets"]["slides"], "pdf_images": pdf_images, "broll": props_data["assets"].get("broll", {}), "video_clips": props_data["assets"].get("videoClips", []), "existing_props": props_data } def convert_pdf_to_images(self) -> List[str]: """Convert PDF pages to images""" import fitz # PyMuPDF print("📄 Converting PDF pages to images...") 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)) 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}") if (page_num + 1) % 10 == 0: print(f" ✓ Converted {page_num + 1}/{len(doc)} pages") print(f"✓ Converted {len(doc)} PDF pages") doc.close() return image_paths def generate_script(self, materials: Dict) -> List[Dict]: """Generate family introduction script with PPT and PDF switching""" print("🤖 Generating family introduction script...") family = materials["family"] prompt = f""" あなたはプロのファイナンシャルプランナーであり、動画ナレーターです。 【重要】家族成員紹介のみに焦点を当てた動画原稿を作成してください。 PPTページとPDFページを適切に切り替えながら、家族の魅力を伝えます。 【家族情報】 - 家族代表: {family["rep_name"]} ({family["rep_age"]}歳、生年月日{family["rep_birth"]}) 業種: {family["rep_industry"]} 年収: {family["rep_income"]}万円 - 配偶者: {family["spouse_name"]} ({family["spouse_age"]}歳、生年月日{family["spouse_birth"]}) 業種: {family["spouse_industry"]} 年収: {family["spouse_income"]}万円 - お子様: {family["child_name"]} ({family["child_age"]}歳、生年月日{family["child_birth"]}) 教育: {family["child_education"]} - 世帯年収: {family["total_income"]}万円 - 現在資産: {family["total_assets"]}万円 - 年間支出: {family["annual_expense"]}万円 【利用可能な視覚素材】 - PPTスライド: {len(materials["ppt_slides"])}ページ スライド0=タイトルページ「ライフプラン」 スライド1=家族基本情報ページ(テスト太郎47歳/花子37歳/息子7歳、業種・年収・進学プラン詳細) - PDFページ: {len(materials["pdf_images"])}ページ ページ0=表紙 ページ1=目次 ページ2=家族基本情報詳細(テスト太郎/花子/息子の生年月日・年齢・年収・業種) ページ3=ライフイベント一覧 【要件】 1. 以下の構成で原稿を作成(**家族成員紹介のみ**): - intro: オープニング(10秒、PPTスライド0でタイトル表示) - family_overview: 家族全体の概要(20秒、PPTスライド1、3人家族構成・業種・年収紹介) - family_details: 家族詳細情報(30秒、PDFページ2のまま継続、代表者→配偶者→子供の詳細) - financial_summary: 家計まとめ(10秒、PPTスライド1で締めくくり) 2. 各セグメントの構造: - id: セグメントID(英数字) - text: ナレーション文(温かみのある語り口、具体的な数値・業種含む) - duration: 推定時間(秒) - visualType: "ppt" or "pdf_page" - visualAssetIndex: PPTスライド番号(visualType="ppt"の場合、0始まり) - pdfPageIndex: PDFページ番号(visualType="pdf_page"の場合、0始まり) - highlightArea: "full" 3. **重要ポイント**: - PPT素材を最大活用:スライド1には家族全員の詳細情報が記載されている - 同じページは連続して使用し、途中で別ページに切り替えない - PPTスライド1で家族概要を紹介後、PDFページ2で各成員の詳細を深掘り - 各家族成員の個性や魅力を丁寧に紹介(年齢・生年月日・業種・年収すべて言及) **出力形式**: 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" / "family_intro_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']}: {text[:30]}...") 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/family_intro_audio/{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 with PPT and PDF pages""" existing_props = materials["existing_props"] pdf_images = materials["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"【家族紹介】{materials['family']['rep_name']}様ご家族", "currencyUnit": "万円", "duration": sum(s.get("duration", 0) for s in segments), "clientName": materials["family"]["rep_name"] }, "charts": existing_props.get("charts", {}), "script": segments, "assets": { "pdfPages": pdf_page_assets, "slides": existing_props["assets"]["slides"], # Keep PPT slides "videoClips": existing_props["assets"].get("videoClips", []), "broll": existing_props["assets"].get("broll", {}) } } output_path = self.project_root / "assets/1_processed/render_props_family_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 family intro render props to {output_path.name}") return new_props def run(self) -> str: """Execute full pipeline""" print("🚀 Generating family introduction video...\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/family_intro_video.mp4" print(f"✅ Pipeline complete! Render command:") print(f" npx remotion render MainVideo {output_file} \\") print(f" --props=\"assets/1_processed/render_props_family_intro.json\" --concurrency=1") print() print("📋 改动说明:") print(" 1. 内容范围: 仅家族成员介绍(代表者+配偶者+お子様)") print(" 2. 视觉素材: PPT与PDF页面自然切换") print(" 3. 无主干依赖: 两种素材平衡使用,不依赖单一文件作为主线") print(" 4. 语调: 温暖亲切的家庭介绍风格") print(f" 5. 总时长: {props['meta']['duration']:.1f}秒") return output_file if __name__ == "__main__": generator = FamilyIntroGenerator() generator.run()