""" Investigation Script 3: Subtitle/Transcript Extraction - Granularity Comparison This script tests different approaches for subtitle extraction: 1. Movie-level (entire video summary) 2. Second-level (time-stamped subtitles per second/phrase) 3. Word-level (individual word timestamps) Compares two methods: - Method A: Upload video directly to Gemini - Method B: Extract audio with ffmpeg, upload audio only Tests which is faster, more accurate, and uses fewer tokens. """ import os import json import time import subprocess from pathlib import Path from typing import Dict, Any, List, Optional, Literal from dotenv import load_dotenv from google import genai from google.genai import types from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn from rich.panel import Panel from rich.table import Table from rich.json import JSON # Load environment variables load_dotenv() # Initialize Rich console console = Console() # Configure Gemini API GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") if not GEMINI_API_KEY: console.print("[red]❌ GEMINI_API_KEY not found in .env file[/red]") exit(1) # Initialize Gemini client client = genai.Client(api_key=GEMINI_API_KEY) class AudioExtractor: """Extract audio from video using ffmpeg""" @staticmethod def extract_audio(video_path: str, output_path: str) -> Optional[str]: """ Extract audio from video file using ffmpeg Args: video_path: Path to video file output_path: Path for output audio file Returns: Path to extracted audio file, or None if failed """ try: console.print(f"[cyan]Extracting audio from video...[/cyan]") # Use ffmpeg to extract audio as MP3 cmd = [ 'ffmpeg', '-i', video_path, '-vn', # No video '-acodec', 'libmp3lame', # MP3 codec '-q:a', '2', # High quality '-y', # Overwrite output output_path ] result = subprocess.run( cmd, capture_output=True, text=True, check=True ) console.print(f"[green]✓[/green] Audio extracted: {Path(output_path).name}") return output_path except subprocess.CalledProcessError as e: console.print(f"[red]❌ FFmpeg error:[/red] {e.stderr}") return None except FileNotFoundError: console.print("[red]❌ FFmpeg not found. Please install ffmpeg.[/red]") return None class SubtitleExtractor: """Extract subtitles at different granularity levels""" def __init__(self): self.model = 'gemini-3-pro-preview' console.print("[green]✓[/green] Initialized Subtitle Extractor") def upload_media(self, file_path: str, mime_type: str) -> Any: """Upload video or audio file to Gemini""" console.print(f"[cyan]Uploading {mime_type.split('/')[0]}:[/cyan] {Path(file_path).name}") with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console ) as progress: task = progress.add_task("Uploading to Gemini API...", total=None) with open(file_path, 'rb') as f: upload_config = types.UploadFileConfig(mime_type=mime_type) media_file = client.files.upload(file=f, config=upload_config) # Wait for processing while media_file.state == "PROCESSING": time.sleep(2) media_file = client.files.get(name=media_file.name) progress.update(task, completed=True) if media_file.state == "FAILED": raise ValueError(f"Media processing failed: {media_file.state}") console.print(f"[green]✓[/green] Media uploaded successfully") return media_file def extract_movie_level(self, media_file: Any) -> Dict[str, Any]: """ Extract movie-level subtitle (entire video summary) Returns: Dictionary with summary and full transcript """ prompt = """ このビデオ/音声の内容を全体として書き起こしてください。 以下の情報を抽出してください: 1. **全体の要約**: ビデオ全体の内容を2-3文で要約 2. **完全な書き起こし**: すべての発言を時系列で書き起こし 3. **話者**: 誰が話しているか識別 **出力形式**: 必ずJSON形式で返してください: { "summary": "全体要約", "full_transcript": "完全な書き起こしテキスト", "speakers": ["話者1", "話者2"], "duration": "MM:SS" } """ return self._generate_content(media_file, prompt, "Movie-level") def extract_second_level(self, media_file: Any) -> Dict[str, Any]: """ Extract second-level subtitles (time-stamped by phrase/sentence) Returns: Dictionary with time-stamped subtitle segments """ prompt = """ この音声/ビデオの内容を時間軸に沿って正確に書き起こし、字幕形式で出力してください。 **要件**: - 各発言を開始時刻と終了時刻付きでセグメント化 - 話者を識別 - 時刻はMM:SS.mmm形式(秒単位で正確に) **出力形式**: 必ずJSON形式で返してください: { "subtitles": [ { "index": 1, "start_time": "00:00.000", "end_time": "00:03.500", "speaker": "話者名", "text": "発言内容" } ], "total_segments": 0 } """ return self._generate_content(media_file, prompt, "Second-level") def extract_word_level(self, media_file: Any) -> Dict[str, Any]: """ Extract word-level subtitles (timestamp for each word) Returns: Dictionary with word-by-word timestamps """ prompt = """ この音声/ビデオの内容を単語レベルで書き起こし、各単語のタイムスタンプを付けてください。 **要件**: - 各単語に開始時刻を付与 - 話者を識別 - 時刻はMM:SS.mmm形式 **出力形式**: 必ずJSON形式で返してください: { "word_timestamps": [ { "word": "単語", "start_time": "00:00.000", "speaker": "話者名" } ], "total_words": 0 } """ return self._generate_content(media_file, prompt, "Word-level") def _generate_content(self, media_file: Any, prompt: str, level: str) -> Dict[str, Any]: """Generate content with token tracking""" console.print(f"[cyan]Extracting {level} subtitles...[/cyan]") start_time = time.time() with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console ) as progress: task = progress.add_task(f"Generating {level} content...", total=None) response = client.models.generate_content( model=self.model, contents=[media_file, prompt] ) progress.update(task, completed=True) elapsed_time = time.time() - start_time # Extract token usage token_info = { "input_tokens": response.usage_metadata.prompt_token_count if hasattr(response, 'usage_metadata') else 0, "output_tokens": response.usage_metadata.candidates_token_count if hasattr(response, 'usage_metadata') else 0, "total_tokens": response.usage_metadata.total_token_count if hasattr(response, 'usage_metadata') else 0 } console.print(f"[cyan]📊 Tokens:[/cyan] {token_info['total_tokens']:,} | [cyan]⏱ Time:[/cyan] {elapsed_time:.2f}s") # Parse response try: response_text = response.text if "```json" in response_text: json_start = response_text.find("```json") + 7 json_end = response_text.find("```", json_start) response_text = response_text[json_start:json_end].strip() elif "```" in response_text: json_start = response_text.find("```") + 3 json_end = response_text.find("```", json_start) response_text = response_text[json_start:json_end].strip() result = json.loads(response_text) result["_metadata"] = { "level": level, "token_usage": token_info, "processing_time_seconds": elapsed_time, "model": self.model } console.print(f"[green]✓[/green] {level} extraction complete") return result except json.JSONDecodeError as e: console.print(f"[yellow]⚠[/yellow] JSON parsing failed, returning raw response") return { "raw_response": response.text, "error": str(e), "_metadata": { "level": level, "token_usage": token_info, "processing_time_seconds": elapsed_time } } def compare_methods(video_path: str, output_dir: Path, granularity: Literal["movie", "second", "word"]): """ Compare video vs audio extraction for a specific granularity level Args: video_path: Path to video file output_dir: Output directory for results granularity: Level of granularity to test """ extractor = SubtitleExtractor() audio_extractor = AudioExtractor() results = {} console.print(f"\n[bold cyan]Testing {granularity.upper()}-level extraction[/bold cyan]") console.print("="*60) # Method A: Direct video upload console.print("\n[bold]Method A: Video Upload[/bold]") try: video_file = extractor.upload_media(video_path, 'video/mp4') if granularity == "movie": result_a = extractor.extract_movie_level(video_file) elif granularity == "second": result_a = extractor.extract_second_level(video_file) else: # word result_a = extractor.extract_word_level(video_file) results["method_a_video"] = result_a except Exception as e: console.print(f"[red]❌ Method A failed:[/red] {e}") results["method_a_video"] = {"error": str(e)} # Method B: Audio extraction + upload console.print("\n[bold]Method B: Audio Extraction + Upload[/bold]") try: # Extract audio audio_path = str(output_dir / f"{Path(video_path).stem}_audio.mp3") extracted_audio = audio_extractor.extract_audio(video_path, audio_path) if extracted_audio: audio_file = extractor.upload_media(extracted_audio, 'audio/mpeg') if granularity == "movie": result_b = extractor.extract_movie_level(audio_file) elif granularity == "second": result_b = extractor.extract_second_level(audio_file) else: # word result_b = extractor.extract_word_level(audio_file) results["method_b_audio"] = result_b else: results["method_b_audio"] = {"error": "Audio extraction failed"} except Exception as e: console.print(f"[red]❌ Method B failed:[/red] {e}") results["method_b_audio"] = {"error": str(e)} return results def main(): """Main investigation workflow""" console.print(Panel.fit( "[bold cyan]Subtitle Extraction Investigation[/bold cyan]\n" "Comparing Video vs Audio | Movie/Second/Word Level Granularity", border_style="cyan" )) # Setup paths input_dir = Path("assets/0_input") output_dir = Path("assets/1_processed/subtitle_investigation") output_dir.mkdir(parents=True, exist_ok=True) # Get first video for testing video_files = list(input_dir.glob("*.mp4")) if not video_files: console.print("[red]❌ No video files found[/red]") return test_video = video_files[0] # Use first video for comparison console.print(f"\n[cyan]Test Video:[/cyan] {test_video.name}") # Test all granularity levels all_results = {} for granularity in ["movie", "second", "word"]: results = compare_methods(str(test_video), output_dir, granularity) all_results[granularity] = results # Save individual results result_file = output_dir / f"{test_video.stem}_{granularity}_comparison.json" with open(result_file, 'w', encoding='utf-8') as f: json.dump(results, f, ensure_ascii=False, indent=2) console.print(f"[green]✓[/green] Results saved: {result_file.name}") # Generate comparison summary console.print("\n" + "="*60) console.print("[bold green]Comparison Summary[/bold green]\n") table = Table(title="Performance Comparison", show_header=True) table.add_column("Granularity", style="cyan") table.add_column("Method", style="yellow") table.add_column("Tokens", justify="right", style="magenta") table.add_column("Time (s)", justify="right", style="blue") for granularity, results in all_results.items(): for method_name, data in results.items(): if "_metadata" in data: meta = data["_metadata"] table.add_row( granularity.upper(), method_name.replace("_", " ").title(), f"{meta['token_usage']['total_tokens']:,}", f"{meta['processing_time_seconds']:.2f}" ) console.print(table) # Save complete comparison summary_file = output_dir / "complete_comparison.json" with open(summary_file, 'w', encoding='utf-8') as f: json.dump(all_results, f, ensure_ascii=False, indent=2) console.print(f"\n[green]✓[/green] Complete comparison saved: {summary_file}") if __name__ == "__main__": main()