AI-video-lifeplan-demo/backend/investigate_subtitle_extrac...

413 lines
14 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.

"""
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()