clip-forge/backend/app/services/clip_extraction.py

118 lines
2.8 KiB
Python

"""Clip extraction service using FFmpeg."""
import asyncio
import logging
import os
from app.config import settings
logger = logging.getLogger(__name__)
async def extract_clip(
video_path: str,
start_time: float,
end_time: float,
output_path: str,
) -> str:
"""Extract a clip from video using FFmpeg stream copy (instant, no re-encode).
Args:
video_path: path to source video
start_time: clip start in seconds
end_time: clip end in seconds
output_path: where to write the clip
Returns:
output_path
"""
os.makedirs(os.path.dirname(output_path), exist_ok=True)
duration = end_time - start_time
# Use stream copy for speed - seek before input for accuracy
cmd = [
"ffmpeg",
"-ss", str(start_time),
"-i", video_path,
"-t", str(duration),
"-c", "copy",
"-avoid_negative_ts", "make_zero",
"-y",
output_path,
]
logger.info(
f"Extracting clip: {start_time:.1f}s - {end_time:.1f}s -> {output_path}"
)
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
_, stderr = await proc.communicate()
if proc.returncode != 0:
raise RuntimeError(f"FFmpeg clip extraction failed: {stderr.decode()}")
size_mb = os.path.getsize(output_path) / (1024 * 1024)
logger.info(f"Extracted clip: {output_path} ({size_mb:.1f} MB)")
return output_path
async def extract_thumbnail(
video_path: str,
timestamp: float,
output_path: str,
) -> str:
"""Extract a single frame as thumbnail."""
os.makedirs(os.path.dirname(output_path), exist_ok=True)
cmd = [
"ffmpeg",
"-ss", str(timestamp),
"-i", video_path,
"-vframes", "1",
"-q:v", "2",
"-y",
output_path,
]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
_, stderr = await proc.communicate()
if proc.returncode != 0:
raise RuntimeError(f"FFmpeg thumbnail extraction failed: {stderr.decode()}")
return output_path
async def get_video_duration(video_path: str) -> float:
"""Get video duration in seconds using ffprobe."""
cmd = [
"ffprobe",
"-v", "quiet",
"-print_format", "json",
"-show_format",
video_path,
]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await proc.communicate()
if proc.returncode != 0:
return 0.0
import json
data = json.loads(stdout.decode())
return float(data.get("format", {}).get("duration", 0))