"""Configuration management using Pydantic Settings.""" import shutil from enum import Enum from pathlib import Path from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict def _find_binary(name: str) -> Path: """Return the absolute path to *name* on PATH, falling back to bare name.""" found = shutil.which(name) return Path(found) if found else Path(name) class CropMode(str, Enum): """Crop positioning strategy.""" SCENE_EQUILIBRIUM = "scene_equilibrium" # Per-scene equilibrium (resets at cuts) class DetectionModel(str, Enum): """Detection model selection.""" MEDIAPIPE = "mediapipe" class Settings(BaseSettings): """Application settings loaded from environment variables.""" # Gemini API Configuration # Default is empty string so the library can be imported without a .env file. # IntelligenceEngine / CutToMusicIntelligence will raise at call time if unset. gemini_api_key: str = "" gemini_model: str = "gemini-3-flash-preview" # Processing Configuration default_output_dir: Path = Path("./output") default_num_clips: int = 3 default_duration_min: float = 12.0 default_duration_max: float = 17.0 # Downsampling Configuration (for Gemini API submission only) # Gemini samples video at ~1fps internally, so 2fps + 480p is sufficient # and dramatically reduces preprocessing time and upload size. max_resolution_height: int = 480 max_fps: int = 2 # FFmpeg Configuration — auto-discovered via PATH; override with FFMPEG_PATH / FFPROBE_PATH ffmpeg_path: Path = Field(default_factory=lambda: _find_binary("ffmpeg")) ffprobe_path: Path = Field(default_factory=lambda: _find_binary("ffprobe")) # Reframing Configuration saliency_keyframe_interval: float = 1.0 # Sample every 1 second for better tracking smoothing_window: float = 2.0 # Increased for more aggressive smoothing min_transition_duration: float = 0.5 max_keyframes: int = ( 32 # Maximum keyframes in FFmpeg crop expression (higher = smoother tracking) ) # MediaPipe Face and Pose Detection Configuration max_faces: int = 3 # Maximum number of faces to detect min_face_confidence: float = 0.5 # Minimum confidence for face detection min_tracking_confidence: float = 0.5 # Minimum confidence for tracking enable_pose_fallback: bool = True # Enable pose detection as fallback when faces not detected # Scene Change Detection Configuration enable_scene_detection: bool = True scene_histogram_threshold: float = 0.45 scene_confidence_drop_threshold: float = 0.35 scene_pixel_diff_threshold: float = 0.25 scene_detection_min_interval: float = 0.5 # Debounce interval (seconds) scene_detection_debug: bool = False # Crop Positioning Mode Selection crop_mode: CropMode = CropMode.SCENE_EQUILIBRIUM # Equilibrium-Based Crop Positioning equilibrium_safety_margin: float = 0.2 # 20% margin on each side (60% safe zone) equilibrium_max_movement: float = 80.0 # Slower movement than subject-chasing (150px) equilibrium_sample_interval: float = 0.5 # Sample scene every 0.5s for analysis equilibrium_rubber_band_strength: float = 0.3 # Pull back toward equilibrium (0.0-1.0) # PySceneDetect Configuration (for SCENE_EQUILIBRIUM mode) pyscenedetect_detector: str = "adaptive" # "adaptive" or "content" pyscenedetect_adaptive_threshold: float = 3.0 # Lower = more sensitive (default: 3.0) pyscenedetect_content_threshold: float = 27.0 # For ContentDetector fallback pyscenedetect_min_scene_length: float = 1.0 # Minimum scene duration (seconds) pyscenedetect_luma_only: bool = False # true=brightness only, false=color too # Detection Model Configuration detection_model: DetectionModel = DetectionModel.MEDIAPIPE model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) # Global settings instance settings = Settings()