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

362 lines
13 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 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()