AI-video-lifeplan-demo/backend/video_slicer.py

214 lines
7.2 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

"""
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 []