import os import subprocess import logging from uuid import uuid4 from datetime import datetime from app.database import SessionLocal from app.models.asset import Asset from app.config import settings logger = logging.getLogger(__name__) def extract_frame(asset_id: str, timestamp: float): """ Extract a frame from a video asset at a specific timestamp. """ print(f"DEBUG: Extracting frame for asset {asset_id} at {timestamp}") db = SessionLocal() try: # Get input asset from uuid import UUID try: uuid_id = UUID(str(asset_id)) except ValueError: print(f"DEBUG: Invalid UUID string: {asset_id}") raise ValueError("Invalid asset ID format") asset = db.query(Asset).filter(Asset.id == uuid_id).first() if not asset: print(f"DEBUG: Asset {asset_id} not found in DB") raise ValueError("Asset not found") if not asset.file_path or not os.path.exists(asset.file_path): print(f"DEBUG: File path not found: {asset.file_path}") raise ValueError(f"Video file not found on disk: {asset.file_path}") # Generate output filename # Format: {original_name}_frame_{timestamp}.png base_name = os.path.splitext(asset.original_filename)[0] # Clean timestamp format to be safe (replace . with -) time_str = f"{timestamp:.3f}".replace('.', '-') filename = f"{base_name}_frame_{time_str}_{uuid4().hex[:6]}.png" storage_path = os.path.join(settings.storage_path, "images") os.makedirs(storage_path, exist_ok=True) output_path = os.path.join(storage_path, filename) print(f"DEBUG: Output path: {output_path}") # Build ffmpeg command # -ss before -i for faster seeking # -vframes 1 to get one frame # -q:v 2 for high quality jpg, but we want png so usually just default or compression level # PNG is lossless by default in ffmpeg usually. cmd = [ 'ffmpeg', '-y', # Overwrite '-ss', str(timestamp), '-i', asset.file_path, '-vframes', '1', output_path ] logger.info(f"Extracting frame with command: {' '.join(cmd)}") print(f"DEBUG: Running command: {cmd}") result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: logger.error(f"FFmpeg failed: {result.stderr}") print(f"DEBUG: FFmpeg failed stderr: {result.stderr}") print(f"DEBUG: FFmpeg failed stdout: {result.stdout}") raise ValueError(f"Frame extraction failed: {result.stderr}") if not os.path.exists(output_path): print(f"DEBUG: Output file missing after success return code") raise ValueError("Output file was not created") # Get file size file_size = os.path.getsize(output_path) # Get dimensions if possible (assume same as video or read it) # We can use Pillow if installed, or just use input video dims width = asset.width height = asset.height # Determine mime type mime_type = "image/png" # Create new asset new_asset = Asset( user_id=asset.user_id, project_id=asset.project_id, original_filename=filename, stored_filename=filename, file_path=output_path, file_type="image", mime_type=mime_type, file_size_bytes=file_size, width=width, height=height, source_module="frame_extractor", parent_asset_id=asset.id, asset_metadata={ "source_video_id": str(asset.id), "timestamp": timestamp } ) db.add(new_asset) db.commit() db.refresh(new_asset) return new_asset finally: db.close()