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

284 lines
10 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 detailed narration video for specific PPT slide with PDF context
This script creates a comprehensive narration video combining:
1. PPT slide content (text + visual)
2. PDF financial data context
3. AI-generated detailed explanation
4. Smooth transitions between content sections
Usage:
python backend/generate_detailed_narration.py --slide 2
"""
import argparse
import json
import os
from pathlib import Path
from typing import Dict, Any, List
from dotenv import load_dotenv
# Import Gemini API
from google import genai
from google.genai import types
class DetailedNarrationGenerator:
"""Generate detailed narration video for specific slide"""
def __init__(self, slide_index: int = 2):
load_dotenv()
self.project_root = Path(__file__).parent.parent
self.slide_index = slide_index
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 load_ppt_slide(self) -> Dict[str, Any]:
"""Load specific PPT slide data"""
import sys
sys.path.insert(0, str(self.project_root))
from backend.ppt_extractor import extract_ppt_data
ppt_path = self.project_root / "assets/0_input/ライフプラン情報.pptx"
ppt_data = extract_ppt_data(str(ppt_path))
if self.slide_index >= len(ppt_data["slides"]):
raise ValueError(f"Slide {self.slide_index} not found")
return ppt_data["slides"][self.slide_index]
def load_pdf_context(self) -> Dict[str, Any]:
"""Load PDF financial data as context"""
import sys
sys.path.insert(0, str(self.project_root))
from backend.extractor import extract_pdf_data
pdf_path = self.project_root / "assets/0_input/帳票(お客様向け).pdf"
return extract_pdf_data(str(pdf_path))
def generate_narration_script(self, slide: Dict, pdf_data: Dict) -> List[Dict]:
"""
Generate detailed narration script using Gemini
Returns:
List of narration segments with text, duration, visual cues
"""
print(f"🤖 Generating detailed narration for slide {self.slide_index}...")
prompt = f"""
あなたはプロのファイナンシャルプランナーであり、親しみやすい動画解説者です。
以下の情報に基づいて、詳細な解説動画用のナレーション原稿を作成してください。
【PPTスライド内容】
タイトル: {slide['title']}
テキスト:
{slide['text']}
【PDFから抽出した追加情報】
- 家族代表: {pdf_data.get('client_name', 'テスト太郎')} ({pdf_data.get('current_age', 47)}歳)
- 年収: {pdf_data.get('annual_income', 703)}万円
- 現在の資産: {pdf_data.get('current_assets', 836)}万円
- 年間支出: {pdf_data.get('annual_expenditure', 1038)}万円
【要件】
1. スライドの内容を**段落ごとに分解**し、各段落を詳細に解説してください
2. 各セグメントは以下の構造で出力してください:
- intro: スライド全体の導入(15秒程度)
- section_1: 家族代表の基本情報(20秒程度)
- section_2: 配偶者情報(15秒程度)
- section_3: お子様情報と教育プラン(20秒程度)
- summary: まとめ(10秒程度)
3. 各セグメントには以下を含めてください:
- text: ナレーション文(自然な口語体、具体的な数値を含む)
- duration: 推定時間(秒)
- visualCue: 画面表示の指示("ppt" / "zoom_in" / "highlight_section")
- highlightArea: 強調する領域("top" / "middle" / "bottom" / "full")
4. 数値やデータは必ず具体的に言及し、抽象的な表現は避けてください
5. 親しみやすく、わかりやすい言葉遣いを心がけてください
**出力形式**: JSON配列として返してください
[
{{
"id": "intro",
"text": "それでは、ご家族の基本情報について詳しく見ていきましょう。...",
"duration": 15,
"visualCue": "ppt",
"highlightArea": "full"
}},
...
]
"""
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)} narration segments")
return result
def create_smooth_transitions(self, segments: List[Dict]) -> List[Dict]:
"""Add transition effects between segments"""
enhanced_segments = []
for i, seg in enumerate(segments):
# Enhance segment with visual metadata
enhanced_seg = {
**seg,
"type": "narration",
"visualType": "ppt",
"visualAssetIndex": self.slide_index,
"brollKeyword": f"Japanese family planning {seg.get('highlightArea', 'full')}",
}
enhanced_segments.append(enhanced_seg)
return enhanced_segments
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 for narration segments...")
audio_gen = AudioGenerator()
output_dir = self.project_root / "public" / f"slide_{self.slide_index}_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}] Generating: {text[:50]}...")
# Use OpenAI TTS API directly
try:
response = audio_gen.client.audio.speech.create(
model="tts-1",
voice="nova",
input=text
)
response.stream_to_file(str(audio_path))
# Get actual duration
actual_duration = get_video_duration(str(audio_path))
seg["duration"] = actual_duration
seg["audioPath"] = f"public/slide_{self.slide_index}_narration/{audio_filename}"
except Exception as e:
print(f" ⚠️ Failed: {e}")
continue
print(f"✓ Generated audio for {len(segments)} segments")
return segments
def create_render_props(self, segments: List[Dict], slide: Dict, pdf_data: Dict) -> Dict:
"""Create render_props.json for the detailed narration video"""
# Load original props for charts
original_props_path = self.project_root / "assets/1_processed/render_props.json"
if original_props_path.exists():
with open(original_props_path, "r", encoding="utf-8") as f:
original_props = json.load(f)
else:
original_props = {"charts": {}, "assets": {"slides": []}}
# Construct new props
new_props = {
"meta": {
"title": f"【詳細解説】{slide['title']} - {pdf_data.get('client_name', 'お客様')}様",
"currencyUnit": "万円",
"duration": sum(s.get("duration", 0) for s in segments),
"clientName": pdf_data.get("client_name", "お客様")
},
"charts": original_props.get("charts", {}),
"script": segments,
"assets": {
**original_props.get("assets", {}),
"targetSlide": slide
}
}
# Save to file
output_path = self.project_root / f"assets/1_processed/render_props_slide_{self.slide_index}_detailed.json"
with open(output_path, "w", encoding="utf-8") as f:
json.dump(new_props, f, ensure_ascii=False, indent=2)
print(f"✓ Saved render props to {output_path.name}")
return new_props
def run(self) -> str:
"""Execute full pipeline"""
print(f"🚀 Generating detailed narration video for slide {self.slide_index}...")
print()
# Load data
slide = self.load_ppt_slide()
pdf_data = self.load_pdf_context()
print(f"✓ Loaded slide: {slide['title']}")
print(f"✓ Loaded PDF context for: {pdf_data.get('client_name')}")
print()
# Generate narration
segments = self.generate_narration_script(slide, pdf_data)
print()
# Add transitions
segments = self.create_smooth_transitions(segments)
print(f"✓ Added smooth transitions")
print()
# Generate audio
segments = self.generate_audio(segments)
print()
# Create render props
props = self.create_render_props(segments, slide, pdf_data)
print()
output_file = f"assets/2_output/slide_{self.slide_index}_detailed.mp4"
print(f"✅ Pipeline complete! Use the following command to render:")
print(f" npx remotion render MainVideo {output_file} \\")
print(f" --props=\"assets/1_processed/render_props_slide_{self.slide_index}_detailed.json\"")
return output_file
def main():
parser = argparse.ArgumentParser(
description="Generate detailed narration video for specific PPT slide"
)
parser.add_argument("--slide", type=int, default=2, help="Slide index (default: 2)")
args = parser.parse_args()
try:
generator = DetailedNarrationGenerator(slide_index=args.slide)
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())