214 lines
7.2 KiB
Python
214 lines
7.2 KiB
Python
"""
|
||
Video Slicing Module
|
||
|
||
Extract important segments from source video based on transcription analysis.
|
||
This is an optional feature for including interview clips in the final video.
|
||
"""
|
||
|
||
import os
|
||
from pathlib import Path
|
||
from typing import Dict, Any, List, Optional
|
||
|
||
try:
|
||
from google import genai
|
||
from google.genai import types
|
||
except ImportError:
|
||
genai = None
|
||
|
||
from backend.video_processor.utils import slice_video, get_video_duration
|
||
|
||
|
||
class VideoSlicer:
|
||
"""Extract important segments from source video"""
|
||
|
||
def __init__(self, api_key: Optional[str] = None):
|
||
self.api_key = api_key or os.getenv("GEMINI_API_KEY")
|
||
# CRITICAL: Output directly to public/ for Remotion to find them via staticFile
|
||
self.output_dir = Path("public")
|
||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
if self.api_key and genai:
|
||
self.client = genai.Client(api_key=self.api_key)
|
||
else:
|
||
self.client = None
|
||
|
||
def extract_key_moments(
|
||
self,
|
||
video_path: str,
|
||
transcription_data: Dict[str, Any],
|
||
max_clips: int = 3,
|
||
target_duration: float = 5.0
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
Analyze transcription and extract key video moments
|
||
|
||
Args:
|
||
video_path: Path to source video
|
||
transcription_data: Transcription with segments and timestamps
|
||
max_clips: Maximum number of clips to extract
|
||
target_duration: Target duration for each clip in seconds
|
||
|
||
Returns:
|
||
List of clip info dictionaries with start, end, filepath, description
|
||
"""
|
||
print(f" ✂️ Analyzing video for key moments...")
|
||
|
||
# Get video duration
|
||
try:
|
||
duration = get_video_duration(video_path)
|
||
print(f" ℹ️ Video duration: {duration:.1f}s")
|
||
except Exception as e:
|
||
print(f" ⚠️ Could not get video duration: {e}")
|
||
return []
|
||
|
||
# Use Gemini to identify key moments from transcription
|
||
if self.client and "segments" in transcription_data:
|
||
key_segments = self._identify_key_segments(
|
||
transcription_data["segments"],
|
||
max_clips=max_clips
|
||
)
|
||
else:
|
||
# Fallback: use simple heuristics
|
||
key_segments = self._simple_segment_selection(
|
||
transcription_data.get("segments", []),
|
||
max_clips=max_clips
|
||
)
|
||
|
||
# Extract video clips
|
||
clips = []
|
||
for i, segment_info in enumerate(key_segments[:max_clips]):
|
||
start = segment_info["start"]
|
||
end = min(segment_info["end"], start + target_duration)
|
||
|
||
# Ensure within video bounds
|
||
if end > duration:
|
||
end = duration
|
||
if start >= duration:
|
||
continue
|
||
|
||
# Extract clip
|
||
output_path = self.output_dir / f"clip_{i+1}_{start:.1f}-{end:.1f}.mp4"
|
||
|
||
try:
|
||
if not output_path.exists():
|
||
print(f" ✂️ Extracting clip {i+1}: {start:.1f}s - {end:.1f}s")
|
||
clip_path = slice_video(video_path, start, end, str(output_path))
|
||
else:
|
||
clip_path = str(output_path)
|
||
print(f" ✓ Clip {i+1} already cached")
|
||
|
||
clips.append({
|
||
"id": f"clip_{i+1}",
|
||
"outputPath": clip_path,
|
||
"filepath": clip_path, # Keep for backward compatibility if any
|
||
"start": start,
|
||
"end": end,
|
||
"duration": end - start,
|
||
"description": segment_info.get("text", "")[:100]
|
||
})
|
||
|
||
except Exception as e:
|
||
print(f" ⚠️ Failed to extract clip {i+1}: {e}")
|
||
|
||
print(f" ✓ Extracted {len(clips)} video clips")
|
||
return clips
|
||
|
||
def _identify_key_segments(
|
||
self,
|
||
segments: List[Dict[str, Any]],
|
||
max_clips: int
|
||
) -> List[Dict[str, Any]]:
|
||
"""Use Gemini to identify important segments"""
|
||
if not self.client:
|
||
return self._simple_segment_selection(segments, max_clips)
|
||
|
||
# Prepare transcript for analysis
|
||
transcript_text = "\n".join([
|
||
f"[{seg['start']:.1f}s - {seg['end']:.1f}s] {seg['text']}"
|
||
for seg in segments
|
||
])
|
||
|
||
prompt = f"""
|
||
以下は動画のトランスクリプトです。ライフプランニングの説明動画に最も適した重要なシーンを{max_clips}つ選んでください。
|
||
|
||
トランスクリプト:
|
||
{transcript_text}
|
||
|
||
以下のJSON形式で、重要なシーンの開始時刻、終了時刻、理由を返してください:
|
||
|
||
```json
|
||
[
|
||
{{
|
||
"start": 開始時刻(秒),
|
||
"end": 終了時刻(秒),
|
||
"text": "そのシーンのテキスト",
|
||
"reason": "選んだ理由"
|
||
}}
|
||
]
|
||
```
|
||
"""
|
||
|
||
try:
|
||
response = self.client.models.generate_content(
|
||
model='gemini-2.0-flash-exp',
|
||
contents=prompt
|
||
)
|
||
|
||
response_text = response.text.strip()
|
||
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()
|
||
|
||
import json
|
||
key_segments = json.loads(response_text)
|
||
print(f" 🤖 Gemini identified {len(key_segments)} key moments")
|
||
return key_segments
|
||
|
||
except Exception as e:
|
||
print(f" ⚠️ Gemini analysis failed: {e}, using fallback...")
|
||
return self._simple_segment_selection(segments, max_clips)
|
||
|
||
def _simple_segment_selection(
|
||
self,
|
||
segments: List[Dict[str, Any]],
|
||
max_clips: int
|
||
) -> List[Dict[str, Any]]:
|
||
"""Simple heuristic: select evenly spaced segments"""
|
||
if not segments:
|
||
return []
|
||
|
||
# Select evenly distributed segments
|
||
step = len(segments) // max_clips if len(segments) >= max_clips else 1
|
||
selected = [segments[i * step] for i in range(min(max_clips, len(segments)))]
|
||
|
||
return selected
|
||
|
||
|
||
def extract_video_clips(
|
||
video_path: str,
|
||
transcription_data: Dict[str, Any],
|
||
max_clips: int = 3
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
Convenience function to extract video clips
|
||
|
||
Args:
|
||
video_path: Path to source video
|
||
transcription_data: Transcription with segments
|
||
max_clips: Maximum number of clips to extract
|
||
|
||
Returns:
|
||
List of clip information dictionaries
|
||
"""
|
||
try:
|
||
slicer = VideoSlicer()
|
||
return slicer.extract_key_moments(video_path, transcription_data, max_clips=max_clips)
|
||
except Exception as e:
|
||
print(f" ⚠️ Video slicing failed: {e}")
|
||
return []
|