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

351 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 video for specific PPT slides (slides 2 & 3)
Extract information from PDF but only display PPT slides
"""
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 PPTSlidesVideoGenerator:
def __init__(self, slide_indices: List[int] = [1, 2]):
"""
Initialize generator for specific PPT slides
Args:
slide_indices: List of slide indices to generate video for (0-based)
Default: [1, 2] means slide 2 and slide 3 (slide_002.png, slide_003.png)
"""
self.project_root = Path(__file__).parent.parent
self.slide_indices = slide_indices
# 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 slides and PDF data"""
print("📦 Loading materials...")
# Load existing render_props to get PPT slides info
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)
all_slides = props_data["assets"]["slides"]
target_slides = [slide for slide in all_slides if slide["index"] in self.slide_indices]
print(f"✓ Loaded {len(target_slides)} PPT slides (indices: {self.slide_indices})")
# Extract PDF data using Gemini
pdf_data = self.extract_pdf_data()
return {
"ppt_slides": target_slides,
"pdf_data": pdf_data,
"existing_props": props_data
}
def extract_pdf_data(self) -> Dict:
"""Extract relevant data from PDF using Gemini"""
print("📄 Extracting data from PDF...")
pdf_path = self.project_root / "assets/0_input/帳票(お客様向け).pdf"
# Upload PDF to Gemini
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
import time
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
prompt = """
このPDFからライフプラン情報を詳細に抽出してください。
以下のJSON形式で出力してください:
{
"client_info": {
"name": "クライアント名",
"birth_date": "生年月日",
"age": 年齢,
"occupation": "業種",
"annual_income": 年収(万円)
},
"spouse_info": {
"name": "配偶者名",
"birth_date": "生年月日",
"age": 年齢,
"occupation": "業種",
"annual_income": 年収(万円)
},
"children": [
{
"name": "子供の名前",
"birth_date": "生年月日",
"age": 年齢,
"education_plan": "教育プラン(保育園→小学校→中学校など)"
}
],
"financial_summary": {
"household_income": 世帯年収(万円),
"annual_expenses": 年間支出(万円),
"current_assets": 現在資産(万円),
"savings": 貯蓄額(万円)
},
"life_events": [
{
"year": イベント発生年,
"age": その時の年齢,
"description": "イベント説明",
"cost": 費用(万円)
}
],
"future_projections": {
"retirement_age": 退職年齢,
"life_expectancy": 想定寿命,
"retirement_savings_needed": 必要な老後資金(万円)
}
}
可能な限り詳細な情報を抽出してください。数値は必ず含めてください。
"""
response = self.client.models.generate_content(
model="gemini-2.0-flash",
contents=[pdf_file, prompt],
config=types.GenerateContentConfig(
temperature=0.1,
response_mime_type="application/json"
)
)
pdf_data = json.loads(response.text)
print("✓ PDF data extracted successfully")
return pdf_data
def generate_script(self, materials: Dict) -> List[Dict]:
"""Generate narration script for PPT slides based on PDF data"""
print("🤖 Generating narration script...")
ppt_slides = materials["ppt_slides"]
pdf_data = materials["pdf_data"]
# Handle case where pdf_data might be a list or dict
if isinstance(pdf_data, list):
pdf_data = pdf_data[0] if pdf_data else {}
# Build detailed context from PDF
client_info = pdf_data.get("client_info", {})
spouse_info = pdf_data.get("spouse_info", {})
children = pdf_data.get("children", [])
financial = pdf_data.get("financial_summary", {})
life_events = pdf_data.get("life_events", [])
prompt = f"""
あなたはプロのファイナンシャルプランナーであり、動画ナレーターです。
【重要】以下の2枚のPPTスライドについて、PDFから抽出した詳細情報を使って、
豊富で魅力的なナレーション原稿を作成してください。
【利用可能なPPTスライド】
スライド1 (slide_002.png): インデックス={ppt_slides[0]["index"]}
スライド2 (slide_003.png): インデックス={ppt_slides[1]["index"] if len(ppt_slides) > 1 else "N/A"}
【PDFから抽出した詳細情報】
■ クライアント情報:
{json.dumps(client_info, ensure_ascii=False, indent=2)}
■ 配偶者情報:
{json.dumps(spouse_info, ensure_ascii=False, indent=2)}
■ 子供情報:
{json.dumps(children, ensure_ascii=False, indent=2)}
■ 財務サマリー:
{json.dumps(financial, ensure_ascii=False, indent=2)}
■ ライフイベント (最初の5件):
{json.dumps(life_events[:5], ensure_ascii=False, indent=2)}
【要件】
1. **2枚のPPTスライドのみを使用**してください
- スライド1枚あたり2-4個のセグメントを作成
- 合計で4-6個のセグメントを生成
2. 各セグメントの構造:
- id: セグメントID (例: "slide2_intro", "slide2_family", "slide3_events")
- text: ナレーション文 (30-60秒分の自然な日本語、具体的な数値を多く含む)
- duration: 推定時間(秒、実際の音声生成後に更新されます)
- visualType: "ppt" (固定)
- visualAssetIndex: PPTスライドのインデックス (0始まり)
- highlightArea: "full" (固定)
3. **ナレーション作成のポイント**:
- PDFから抽出した具体的な数値を必ず使用(名前、年齢、生年月日、年収、業種など)
- 温かみのある親しみやすい語り口
- 視聴者が理解しやすいように、情報を整理して伝える
- 各セグメント30-60秒程度の適切な長さ
- 同じスライドを連続して使用しても良い(内容が異なれば)
4. **セグメント例**:
- スライド2の前半: 家族構成の紹介
- スライド2の後半: 各メンバーの詳細(年齢、職業、年収など)
- スライド3の前半: ライフイベントの概要
- スライド3の後半: 具体的なイベントと費用
**出力形式**: 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"
)
)
segments = json.loads(response.text)
print(f"✓ Generated {len(segments)} script segments")
# Display preview
print("\n📝 Script Preview:")
for i, seg in enumerate(segments, 1):
print(f"\n{i}. [{seg['id']}] - Slide {seg['visualAssetIndex'] + 1}")
print(f" Text: {seg['text'][:100]}...")
return segments
def generate_audio(self, segments: List[Dict]) -> List[Dict]:
"""Generate TTS audio for each segment"""
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...")
audio_gen = AudioGenerator()
output_dir = self.project_root / "public" / "ppt_slides_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']}: Generating audio...")
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_slides_audio/{audio_filename}"
print(f" ✓ Duration: {actual_duration:.2f}s")
except Exception as e:
print(f" ⚠️ Failed: {e}")
seg["duration"] = 5.0 # Fallback duration
continue
total_duration = sum(s.get("duration", 0) for s in segments)
print(f"\n✓ Generated {len(segments)} audio files (Total: {total_duration:.1f}s)")
return segments
def create_render_props(self, segments: List[Dict], materials: Dict) -> Dict:
"""Create render_props with only PPT slides"""
existing_props = materials["existing_props"]
new_props = {
"meta": {
"title": f"ライフプラン紹介 - スライド{self.slide_indices[0]+1}・{self.slide_indices[1]+1 if len(self.slide_indices) > 1 else ''}",
"currencyUnit": "万円",
"duration": sum(s.get("duration", 0) for s in segments),
"clientName": materials["pdf_data"].get("client_info", {}).get("name", "お客様")
},
"charts": existing_props.get("charts", {}),
"script": segments,
"assets": {
"slides": existing_props["assets"]["slides"], # Keep all slides
"videoClips": [],
"broll": existing_props["assets"].get("broll", {})
}
}
output_path = self.project_root / "assets/1_processed/render_props_ppt_slides.json"
with open(output_path, "w", encoding="utf-8") as f:
json.dump(new_props, f, ensure_ascii=False, indent=2)
print(f"\n✓ Saved render props to {output_path.name}")
return new_props
def run(self) -> str:
"""Execute full pipeline"""
print(f"🚀 Generating video for PPT slides {[i+1 for i in self.slide_indices]}...\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/ppt_slides_video.mp4"
print(f"✅ Pipeline complete!")
print(f"\n📹 Render command:")
print(f" npx remotion render MainVideo {output_file} \\")
print(f" --props=\"assets/1_processed/render_props_ppt_slides.json\" --concurrency=1")
print()
print(f"📊 Video Summary:")
print(f" - Target slides: {[f'slide_{i+1:03d}.png' for i in self.slide_indices]}")
print(f" - Total segments: {len(segments)}")
print(f" - Total duration: {props['meta']['duration']:.1f}s")
print(f" - Client: {props['meta']['clientName']}")
return output_file
if __name__ == "__main__":
# Generate video for slides 2 and 3 (indices 1 and 2)
generator = PPTSlidesVideoGenerator(slide_indices=[1, 2])
generator.run()