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

303 lines
11 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.

"""
Generate comprehensive video using PDF pages as visual anchor (NOT PPT)
CRITICAL: This script uses PDF page content as the structural backbone
- PDF pages are converted to images and used as visual materials
- PPT slides are NOT used as the main visual sequence
- Content organization follows PDF page structure
Usage:
python backend/generate_combined_video.py
"""
import argparse
import json
import os
from pathlib import Path
from typing import Dict, Any, List
from dotenv import load_dotenv
from google import genai
from google.genai import types
class CombinedVideoGenerator:
"""Generate video with PDF pages as structural anchor"""
def __init__(self):
load_dotenv()
self.project_root = Path(__file__).parent.parent
self.api_key = os.getenv("GEMINI_API_KEY")
if not self.api_key:
raise ValueError("GEMINI_API_KEY not found in .env")
self.client = genai.Client(api_key=self.api_key)
def convert_pdf_to_images(self) -> List[str]:
"""Convert PDF pages to images for video use"""
print("📄 Converting PDF pages to images...")
try:
import fitz # PyMuPDF
except ImportError:
print("⚠️ PyMuPDF not installed, using placeholder images")
return []
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)) # 2x resolution
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}")
print(f" ✓ Page {page_num + 1}/{len(doc)}")
doc.close()
print(f"✓ Converted {len(image_paths)} PDF pages")
return image_paths
def load_all_materials(self) -> Dict[str, Any]:
"""Load materials with PDF as primary visual source"""
import sys
sys.path.insert(0, str(self.project_root))
print("📦 Loading all materials...")
# Convert PDF to images (PRIMARY VISUAL SOURCE)
pdf_images = self.convert_pdf_to_images()
# Load existing render_props for charts ONLY
props_path = self.project_root / "assets/1_processed/render_props.json"
with open(props_path, "r", encoding="utf-8") as f:
props_data = json.load(f)
print(f"✓ Loaded chart data")
# Prepare PDF summary
pdf_summary = {
"client_name": props_data["meta"].get("clientName", "お客様"),
"current_age": 47,
"spouse_age": 37,
"child_age": 7,
"annual_income": 703,
"current_assets": 836,
"annual_expenditure": 1038,
"charts": props_data["charts"],
"pdf_images": pdf_images # PRIMARY VISUAL ASSETS
}
print(f"✓ Prepared PDF data with {len(pdf_images)} page images")
return {
"pdf": pdf_summary,
"existing_props": props_data
}
def generate_comprehensive_script(self, materials: Dict) -> List[Dict]:
"""
Generate script using PDF page structure (NOT PPT)
Structure based on PDF pages:
1. Opening (intro)
2. Family basic info (PDF page 1)
3. Life events (PDF page 2)
4. Asset projection intro (PDF page 3)
5. Chart analysis (use chart view)
6. Education plan (PDF page 4)
7. Summary (PDF page 5)
"""
print("🤖 Generating script based on PDF structure...")
pdf_data = materials["pdf"]
pdf_images = pdf_data["pdf_images"]
prompt = f"""
あなたはプロのファイナンシャルプランナーであり、動画解説のナレーターです。
以下の情報に基づいて、**PDFページを視覚素材とする**解説動画のナレーション原稿を作成してください。
【重要】視覚素材はPDFページ画像を使用します(PPTスライドではありません)
【家族情報】
- 家族代表: {pdf_data["client_name"]} ({pdf_data["current_age"]}歳)
- 配偶者: {pdf_data["spouse_age"]}歳
- お子様: {pdf_data["child_age"]}歳
- 現在の年収: {pdf_data["annual_income"]}万円
- 現在の資産: {pdf_data["current_assets"]}万円
- 年間支出: {pdf_data["annual_expenditure"]}万円
【利用可能なPDFページ画像】
{len(pdf_images)}ページ分の画像が利用可能
【要件】
1. 以下の構成で原稿を作成(**家族成員紹介のみ**):
- intro: オープニング(10秒、家族紹介の導入)
- rep_intro: 家族代表紹介(20秒、PDFページ1、代表者の詳細)
- spouse_intro: 配偶者紹介(15秒、PDFページ1、配偶者の詳細)
- child_intro: お子様紹介(15秒、PDFページ1、お子様と教育プラン)
- family_summary: 家族紹介まとめ(10秒、家族全体のまとめ)
2. 各セグメントの構造:
- id: セグメントID
- text: ナレーション文(具体的な数値・年齢・職業を含む)
- duration: 推定時間(秒)
- visualType: "pdf_page"
- pdfPageIndex: PDFページ番号(0始まり)
- highlightArea: "full"
3. **重要**: 家族成員の年齢、職業、年収など具体的な情報を丁寧に紹介
4. 温かみのある親しみやすい語り口で、家族の魅力を伝える
**出力形式**: 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" / "pdf_narration"
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)}] {text[:40]}...")
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/pdf_narration/{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 using PDF pages as visuals"""
existing_props = materials["existing_props"]
pdf_images = materials["pdf"]["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"【PDF解説】{materials['pdf']['client_name']}様のライフプラン",
"currencyUnit": "万円",
"duration": sum(s.get("duration", 0) for s in segments),
"clientName": materials["pdf"]["client_name"]
},
"charts": existing_props["charts"],
"script": segments,
"assets": {
"pdfPages": pdf_page_assets, # PDF pages as PRIMARY visuals
"slides": [], # Empty - not using PPT
"videoClips": existing_props["assets"].get("videoClips", []),
"broll": existing_props["assets"].get("broll", {})
}
}
output_path = self.project_root / "assets/1_processed/render_props_pdf_based.json"
with open(output_path, "w", encoding="utf-8") as f:
json.dump(new_props, f, ensure_ascii=False, indent=2)
print(f"✓ Saved PDF-based render props to {output_path.name}")
return new_props
def run(self) -> str:
"""Execute full pipeline"""
print("🚀 Generating PDF-based comprehensive video...\n")
# Load materials (PDF as primary)
materials = self.load_all_materials()
print()
# Generate script
segments = self.generate_comprehensive_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/pdf_based_video.mp4"
print(f"✅ Pipeline complete! Render command:")
print(f" npx remotion render MainVideo {output_file} \\")
print(f" --props=\"assets/1_processed/render_props_pdf_based.json\"")
print()
print("📋 改动说明:")
print(" 1. 替换视觉素材: PPT slides → PDF page images")
print(" 2. 结构锚点: 按PDF页面组织内容,而非PPT顺序")
print(" 3. 新增assets.pdfPages字段存储PDF页面图像")
print(" 4. visualType: 'pdf_page'(替代'ppt')")
print(" 5. pdfPageIndex: 指向PDF页码(替代slideIndex)")
return output_file
def main():
try:
generator = CombinedVideoGenerator()
generator.run()
except Exception as e:
print(f"\n❌ Generation failed: {e}")
import traceback
traceback.print_exc()
return 1
return 0
if __name__ == "__main__":
exit(main())