AI-video-lifeplan-demo/backend/generate_family_intro.py

292 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()