267 lines
12 KiB
Python
267 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Generate introduction video for PPT slides 2 & 3
|
|
Final version - manually crafted detailed narration based on PDF/PPT content
|
|
Only requires OpenAI TTS API (no Gemini needed)
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment
|
|
load_dotenv()
|
|
|
|
|
|
class PPTIntroFinalGenerator:
|
|
def __init__(self):
|
|
self.project_root = Path(__file__).parent.parent
|
|
print("✓ Generator initialized")
|
|
|
|
def create_detailed_script(self) -> List[Dict]:
|
|
"""
|
|
Create detailed narration script based on PDF/PPT analysis
|
|
Manually crafted for maximum quality and relevance
|
|
"""
|
|
|
|
print("\n📝 Creating detailed narration script...")
|
|
|
|
# Detailed script based on actual PDF/PPT content
|
|
# These are high-quality narrations that cover all important information
|
|
segments = [
|
|
{
|
|
"id": "s2_welcome",
|
|
"text": "皆様、こんにちは。本日は、テスト太郎様とそのご家族のライフプランについてご紹介させていただきます。まずは、ご家族の基本情報からご覧ください。",
|
|
"estimatedDuration": 12,
|
|
"visualType": "ppt",
|
|
"visualAssetIndex": 1, # slide_002.png
|
|
"highlightArea": "full"
|
|
},
|
|
{
|
|
"id": "s2_representative",
|
|
"text": "ご家族の代表者、テスト太郎様は現在47歳。1980年1月1日生まれで、情報通信業界でご活躍されています。年収は703万円と、安定した収入を得ていらっしゃいます。業界の専門知識を活かし、キャリアを積み重ねてこられました。",
|
|
"estimatedDuration": 28,
|
|
"visualType": "ppt",
|
|
"visualAssetIndex": 1,
|
|
"highlightArea": "full"
|
|
},
|
|
{
|
|
"id": "s2_spouse",
|
|
"text": "続いて、奥様の花子様は37歳。1990年2月2日生まれで、卸売・小売業でお仕事をされており、年収は368万円です。ご家族を支えながら、ご自身のキャリアも大切にされている、素敵な方です。",
|
|
"estimatedDuration": 23,
|
|
"visualType": "ppt",
|
|
"visualAssetIndex": 1,
|
|
"highlightArea": "full"
|
|
},
|
|
{
|
|
"id": "s2_child",
|
|
"text": "お子様は現在7歳。2020年3月3日生まれで、すくすくと成長されています。教育プランとしては、保育園の認可園からスタートし、公立小学校へ進学。その後、私立中学校での教育を予定されています。お子様の可能性を最大限に伸ばす、しっかりとした教育方針をお持ちです。",
|
|
"estimatedDuration": 32,
|
|
"visualType": "ppt",
|
|
"visualAssetIndex": 1,
|
|
"highlightArea": "full"
|
|
},
|
|
{
|
|
"id": "s2_household",
|
|
"text": "テスト様ご家族の世帯年収は、合計で1071万円。現在の資産は852万円をお持ちです。年間の支出は1038万円となっており、計画的な家計管理をされています。ご夫婦で力を合わせ、安定した家計を築いていらっしゃいますね。",
|
|
"estimatedDuration": 28,
|
|
"visualType": "ppt",
|
|
"visualAssetIndex": 1,
|
|
"highlightArea": "full"
|
|
},
|
|
{
|
|
"id": "s3_transition",
|
|
"text": "それでは次に、ご家族の将来に予定されているライフイベントについて見ていきましょう。こちらのスライドをご覧ください。",
|
|
"estimatedDuration": 11,
|
|
"visualType": "ppt",
|
|
"visualAssetIndex": 2, # slide_003.png
|
|
"highlightArea": "full"
|
|
},
|
|
{
|
|
"id": "s3_life_events",
|
|
"text": "今後、さまざまなライフイベントが控えています。お子様の進学に伴う教育費、マイホームの購入や住宅ローンの返済、そして将来の退職後の生活設計まで。それぞれのイベントには、適切な資金準備が必要です。特に、私立中学校への進学費用や、退職後の生活資金については、長期的な視点での準備が重要となります。",
|
|
"estimatedDuration": 35,
|
|
"visualType": "ppt",
|
|
"visualAssetIndex": 2,
|
|
"highlightArea": "full"
|
|
},
|
|
{
|
|
"id": "s3_planning",
|
|
"text": "私たちファイナンシャルプランナーは、テスト太郎様ご家族の夢や目標を実現するために、最適なライフプランをご提案させていただきます。資産運用、保険の見直し、教育資金の準備、退職後の生活設計まで、トータルでサポートいたします。一緒に、安心で豊かな未来を築いていきましょう。",
|
|
"estimatedDuration": 32,
|
|
"visualType": "ppt",
|
|
"visualAssetIndex": 2,
|
|
"highlightArea": "full"
|
|
},
|
|
{
|
|
"id": "s3_closing",
|
|
"text": "ご清聴ありがとうございました。テスト様ご家族の明るい未来のために、これからも全力でサポートさせていただきます。",
|
|
"estimatedDuration": 10,
|
|
"visualType": "ppt",
|
|
"visualAssetIndex": 2,
|
|
"highlightArea": "full"
|
|
}
|
|
]
|
|
|
|
print(f" ✓ Created {len(segments)} detailed narration segments")
|
|
|
|
# Display preview
|
|
print("\n 📝 Script Preview:")
|
|
total_duration = 0
|
|
for i, seg in enumerate(segments, 1):
|
|
slide_num = seg['visualAssetIndex'] + 1
|
|
duration = seg['estimatedDuration']
|
|
total_duration += duration
|
|
print(f"\n {i}. [{seg['id']}] - Slide {slide_num} ({duration}s)")
|
|
print(f" {seg['text'][:70]}...")
|
|
|
|
print(f"\n 📊 Total estimated duration: {total_duration}s")
|
|
|
|
return segments
|
|
|
|
def generate_audio(self, segments: List[Dict]) -> List[Dict]:
|
|
"""Generate TTS audio using OpenAI"""
|
|
sys.path.insert(0, str(self.project_root))
|
|
|
|
try:
|
|
from backend.audio_generator import AudioGenerator
|
|
from backend.video_processor.utils import get_video_duration
|
|
except ImportError as e:
|
|
print(f"\n⚠️ Import error: {e}")
|
|
print(" Using estimated durations without audio generation")
|
|
for seg in segments:
|
|
seg["duration"] = seg.get("estimatedDuration", 10)
|
|
return segments
|
|
|
|
print("\n🎙️ Generating audio with OpenAI TTS...")
|
|
|
|
try:
|
|
audio_gen = AudioGenerator()
|
|
except ValueError as e:
|
|
print(f"\n⚠️ {e}")
|
|
print(" Using estimated durations without audio generation")
|
|
for seg in segments:
|
|
seg["duration"] = seg.get("estimatedDuration", 10)
|
|
return segments
|
|
|
|
output_dir = self.project_root / "public" / "ppt_intro_final_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_final_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 ✓ Audio generation complete ({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": "テスト太郎"
|
|
},
|
|
"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_final.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 (Final Version)")
|
|
print("=" * 70)
|
|
|
|
try:
|
|
# Create script
|
|
segments = self.create_detailed_script()
|
|
|
|
# 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_final_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_final.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_final.json")
|
|
|
|
print(f"\n📋 Content Coverage:")
|
|
print(f" ✓ 家族代表者(テスト太郎様)の詳細")
|
|
print(f" ✓ 配偶者(花子様)の情報")
|
|
print(f" ✓ お子様の教育プラン")
|
|
print(f" ✓ 世帯収入・資産・支出")
|
|
print(f" ✓ 将来のライフイベント")
|
|
print(f" ✓ ライフプランニングの提案")
|
|
|
|
return output_file
|
|
|
|
except Exception as e:
|
|
print(f"\n❌ Error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
raise
|
|
|
|
|
|
if __name__ == "__main__":
|
|
generator = PPTIntroFinalGenerator()
|
|
generator.run()
|