#!/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()