| """ |
| Common utilities and constants |
| Stable functions that rarely change |
| """ |
|
|
| import os |
| import time |
| from pathlib import Path |
| from typing import Dict, Any, List, Set |
|
|
| |
| IMG_EXTS: Set[str] = {'.jpg', '.jpeg', '.png', '.bmp', '.webp'} |
| VID_EXTS: Set[str] = {'.mp4', '.avi', '.mov', '.mkv'} |
|
|
| ASPECT: Dict[str, str] = { |
| 'youtube': '1920:1080', |
| 'reels': '1080:1920', |
| 'tiktok': '1080:1920', |
| 'square': '1080:1080', |
| 'widescreen': '1920:1080' |
| } |
|
|
| QUALITY: Dict[str, Dict[str, str]] = { |
| 'fast': {'preset': 'ultrafast', 'crf': '28'}, |
| 'balanced': {'preset': 'fast', 'crf': '23'}, |
| 'quality': {'preset': 'medium', 'crf': '20'}, |
| 'best': {'preset': 'slow', 'crf': '18'} |
| } |
|
|
| |
| def is_image(file_path: str) -> bool: |
| """Check if file is supported image format""" |
| return Path(file_path).suffix.lower() in IMG_EXTS |
|
|
| def is_video(file_path: str) -> bool: |
| """Check if file is supported video format""" |
| return Path(file_path).suffix.lower() in VID_EXTS |
|
|
| def is_media(file_path: str) -> bool: |
| """Check if file is supported media format""" |
| return is_image(file_path) or is_video(file_path) |
|
|
| def size_mb(file_path: str) -> float: |
| """Get file size in MB""" |
| if not os.path.exists(file_path): |
| return 0.0 |
| return round(os.path.getsize(file_path) / (1024 * 1024), 2) |
|
|
| def unique_name(prefix: str, extension: str = '.mp4') -> str: |
| """Generate unique filename with timestamp and PID""" |
| return f"{prefix}_{int(time.time())}_{os.getpid()}{extension}" |
|
|
| def safe_filename(name: str) -> str: |
| """Make filename safe for filesystem""" |
| invalid_chars = '<>:"/\\|?*' |
| for char in invalid_chars: |
| name = name.replace(char, '_') |
| return name[:100] |
|
|
| def format_duration(seconds: int) -> str: |
| """Format seconds to human readable duration""" |
| if seconds < 60: |
| return f"{seconds}s" |
| elif seconds < 3600: |
| return f"{seconds // 60}m {seconds % 60}s" |
| else: |
| h = seconds // 3600 |
| m = (seconds % 3600) // 60 |
| return f"{h}h {m}m" |
|
|
| def estimate_output(input_files: List[str], duration_per_file: int = 4) -> Dict[str, Any]: |
| """Estimate output video characteristics""" |
| total_files = len(input_files) |
| total_duration = total_files * duration_per_file |
| |
| |
| estimated_size_mb = total_duration * 2 |
| |
| return { |
| 'files': total_files, |
| 'duration': total_duration, |
| 'duration_fmt': format_duration(total_duration), |
| 'size_mb': round(estimated_size_mb, 1) |
| } |
|
|
| def valid_template(template: Dict[str, Any]) -> bool: |
| """Validate motion template structure""" |
| required_fields = ['name', 'scale', 'pan', 'rotate', 'duration'] |
| |
| if not all(field in template for field in required_fields): |
| return False |
| |
| |
| if not isinstance(template['duration'], (int, float)) or template['duration'] <= 0: |
| return False |
| |
| if not (isinstance(template['scale'], list) and len(template['scale']) == 2): |
| return False |
| |
| if not (isinstance(template['pan'], list) and len(template['pan']) == 4): |
| return False |
| |
| if not (isinstance(template['rotate'], list) and len(template['rotate']) == 2): |
| return False |
| |
| return True |
|
|
| def get_platform_info(aspect_key: str) -> Dict[str, str]: |
| """Get platform information and recommendations""" |
| platform_info = { |
| 'youtube': { |
| 'name': 'YouTube', |
| 'ratio': '16:9', |
| 'optimal_duration': '15-60s', |
| 'description': 'Horizontal format for YouTube Shorts and regular videos' |
| }, |
| 'reels': { |
| 'name': 'Instagram Reels', |
| 'ratio': '9:16', |
| 'optimal_duration': '15-30s', |
| 'description': 'Vertical format optimized for mobile viewing' |
| }, |
| 'tiktok': { |
| 'name': 'TikTok', |
| 'ratio': '9:16', |
| 'optimal_duration': '15-60s', |
| 'description': 'Vertical format for TikTok content' |
| }, |
| 'square': { |
| 'name': 'Instagram Post', |
| 'ratio': '1:1', |
| 'optimal_duration': '15-30s', |
| 'description': 'Square format for Instagram feed posts' |
| }, |
| 'widescreen': { |
| 'name': 'Widescreen', |
| 'ratio': '16:9', |
| 'optimal_duration': '30-120s', |
| 'description': 'Standard widescreen format for presentations' |
| } |
| } |
| |
| return platform_info.get(aspect_key, { |
| 'name': 'Custom', |
| 'ratio': 'Custom', |
| 'optimal_duration': 'Variable', |
| 'description': 'Custom aspect ratio' |
| }) |