diff --git a/compose.yml b/compose.yml index 2db4fa2..8f6690f 100644 --- a/compose.yml +++ b/compose.yml @@ -3,27 +3,49 @@ services: image: ghcr.io/sudolulo/if-curator-headless:latest container_name: if-curator environment: + # ── Required ────────────────────────────────────────────────────────── - IMMICH_URL=${IMMICH_URL} - API_KEY=${API_KEY} - - AUTO_MODE=true - FRIGATE_URL=${FRIGATE_URL} + + # ── Mode & Strategy ─────────────────────────────────────────────────── + - AUTO_MODE=true + # TRAINING_MODE: face = upload to Frigate face recognition API + # object = save crops to output dir for manual Frigate placement - TRAINING_MODE=face + # STRATEGY: auto = objective diversity (recommended), standard = 30 imgs, broad = 100 imgs - STRATEGY=auto + # - LIMIT=50 # Custom image count; overrides STRATEGY preset + # - OBJECT_CLASS=dog # Object label for object mode (e.g. dog, cat, car) + + # ── People Filtering ────────────────────────────────────────────────── + # - ONLY_PEOPLE=John,Jane # Comma-separated; process only these people + # - SKIP_PEOPLE=Unknown # Comma-separated; skip these people + # - MIN_FACE_COUNT=5 # Skip people with fewer than N assets in Immich + # - YEARS_FILTER=10 # Only include images from the last N years (default: 10) + + # ── Image Quality ───────────────────────────────────────────────────── + # - MIN_FACE_WIDTH=50 # Minimum face width in pixels (default: 50) + # - FACE_MARGIN=0.15 # Padding around face crop as fraction (default: 0.15) + # - ENABLE_FACE_ALIGNMENT=true # Align face before cropping (default: true) + # - USE_FULL_RESOLUTION=true # Use full-res images vs thumbnails (default: true) + # - MIN_CONFIDENCE=0.7 # Minimum face detection confidence (default: 0.7) + # - BLUR_THRESHOLD=100.0 # Laplacian blur threshold; lower = accept more blur (default: 100.0) + # - MAX_AUTO_IMAGES=80 # Hard cap on auto-diversity selection (default: 80) + + # ── Caching & Models ────────────────────────────────────────────────── - FORCE_CPU=false - ENABLE_CACHE=true - CACHE_DIR=/app/.if_cache - HF_HOME=/models/huggingface - INSIGHTFACE_HOME=/models - # - ONLY_PEOPLE=John,Jane - # - SKIP_PEOPLE=Unknown - # - MIN_FACE_COUNT=5 - # ── Tracker overrides (one-shot, remove after use) ── - # - DRY_RUN=true # Preview selection without downloading/uploading - # - RETRY_REJECTED=true # Re-attempt previously rejected images - # - RESET_PERSON=John # Clear uploaded+rejected history for one person + # ── Tracker overrides (one-shot, remove after use) ──────────────────── + # - DRY_RUN=true # Preview selection without downloading/uploading + # - RETRY_REJECTED=true # Re-attempt previously rejected images + # - RESET_PERSON=John # Clear uploaded+rejected history for one person - # ── Scheduling ── + # ── Scheduling ──────────────────────────────────────────────────────── # Cron expression (unset = run once and exit) # Every Sunday at 3 AM: - CRON_SCHEDULE=0 3 * * 0 @@ -44,4 +66,3 @@ services: - driver: nvidia count: all capabilities: [gpu] - diff --git a/if_curator/config.py b/if_curator/config.py index 7397a98..de56df5 100644 --- a/if_curator/config.py +++ b/if_curator/config.py @@ -58,6 +58,12 @@ class _Config: self.YEARS_FILTER = int(os.getenv("YEARS_FILTER", "10")) self.MIN_FACE_WIDTH = int(os.getenv("MIN_FACE_WIDTH", "50")) self.MIN_FACE_COUNT = int(os.getenv("MIN_FACE_COUNT", "0")) + self.BLUR_THRESHOLD = float(os.getenv("BLUR_THRESHOLD", "100.0")) + self.MIN_CONFIDENCE = float(os.getenv("MIN_CONFIDENCE", "0.7")) + self.MAX_AUTO_IMAGES = int(os.getenv("MAX_AUTO_IMAGES", "80")) + self.FACE_MARGIN = float(os.getenv("FACE_MARGIN", "0.15")) + self.USE_FULL_RESOLUTION = os.getenv("USE_FULL_RESOLUTION", "true").lower() in ("true", "1", "yes") + self.ENABLE_FACE_ALIGNMENT = os.getenv("ENABLE_FACE_ALIGNMENT", "true").lower() in ("true", "1", "yes") self.ENABLE_CACHE = os.getenv("ENABLE_CACHE", "false").lower() in ("true", "1", "yes") self.CACHE_DIR = os.getenv("CACHE_DIR", ".if_cache") diff --git a/if_curator/jobs.py b/if_curator/jobs.py index 311505f..88f28db 100644 --- a/if_curator/jobs.py +++ b/if_curator/jobs.py @@ -63,8 +63,14 @@ def _get_strategy_choice(has_embedding: bool, entity_type: str) -> tuple[int | s def _resolve_strategy(strategy: str, has_embedding: bool) -> tuple[int | str, str]: """Resolve env var strategy to (limit, selection_mode) without prompts.""" + custom_limit = os.environ.get("LIMIT", "").strip() + if not has_embedding: - return 30, "time" + limit = int(custom_limit) if custom_limit else 30 + return limit, "time" + + if custom_limit: + return int(custom_limit), "smart" strategy_map = { "auto": ("auto", "smart"),