diff --git a/.env.example b/.env.example index 81e3aaf..016e580 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,7 @@ STRATEGY=auto # BLUR_THRESHOLD=120.0 # Laplacian blur threshold; lower = accept more blur (default: 120.0) # MAX_AUTO_IMAGES=80 # Hard cap on auto-diversity selection (default: 80) # FRIGATE_SCORE_THRESHOLD=0.0 # Quality gate: remove images scoring below this after upload (0 = disabled; requires at least one prior run) +# ENABLE_FRIGATE_SCORES=true # Call Frigate's recognize endpoint after each upload to store quality scores (default: true; adds ~200ms per upload) # ── Caching & Models ────────────────────────────────────────────────────────── # FORCE_CPU=true # Disable GPU, fall back to CPU diff --git a/winnow/config.py b/winnow/config.py index 6bd4dff..784b853 100644 --- a/winnow/config.py +++ b/winnow/config.py @@ -32,6 +32,7 @@ class _Config: MAX_AUTO_IMAGES: int = 80 QUALITY_REPLACEMENT: bool = True FRIGATE_SCORE_THRESHOLD: float = 0.0 + ENABLE_FRIGATE_SCORES: bool = True # People filtering MIN_FACE_COUNT: int = 0 @@ -64,6 +65,7 @@ class _Config: self.MAX_AUTO_IMAGES = int(os.getenv("MAX_AUTO_IMAGES", "80")) self.QUALITY_REPLACEMENT = os.getenv("QUALITY_REPLACEMENT", "true").lower() in ("true", "1", "yes") self.FRIGATE_SCORE_THRESHOLD = float(os.getenv("FRIGATE_SCORE_THRESHOLD", "0.0")) + self.ENABLE_FRIGATE_SCORES = os.getenv("ENABLE_FRIGATE_SCORES", "true").lower() in ("true", "1", "yes") 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") diff --git a/winnow/executor.py b/winnow/executor.py index eeab846..1a84183 100644 --- a/winnow/executor.py +++ b/winnow/executor.py @@ -357,6 +357,17 @@ def upload_to_frigate(jobs: list[dict]) -> None: known_frigate_files_at_start: set[str] = get_tracked_frigate_filenames(name) else: known_frigate_files_at_start: set[str] = set(_snapshot) + # Remove tracker mappings for files that no longer exist in Frigate + # (manually deleted, or cleaned up outside winnow). This corrects the + # effective_count so those slots are available for new uploads. + stale = get_tracked_frigate_filenames(name) - known_frigate_files_at_start + for stale_fn in stale: + remove_frigate_file(name, stale_fn) + if stale: + progress.console.print( + f" [dim]{name}: cleared {len(stale)} stale mapping(s)" + " (file(s) no longer in Frigate)[/dim]" + ) effective_count = get_tracked_frigate_file_count(name) pre_run_count = effective_count quality_replacement = job.get("config", {}).get("quality_replacement", False) @@ -367,6 +378,10 @@ def upload_to_frigate(jobs: list[dict]) -> None: effective_threshold = max(Config.FRIGATE_SCORE_THRESHOLD, _dynamic or 0.0) if _dynamic is not None and _dynamic > Config.FRIGATE_SCORE_THRESHOLD: logger.debug(f"{name}: dynamic Frigate score threshold {_dynamic:.3f}") + if pre_run_count == 0: + progress.console.print( + f" [dim]{name}: first run — quality gate will apply from the next run[/dim]" + ) else: effective_threshold = 0.0 actually_uploaded: list[tuple[str, str | None]] = [] @@ -397,7 +412,7 @@ def upload_to_frigate(jobs: list[dict]) -> None: progress.console.print(f" [dim]⏭ {fname}: at cap, quality replacement disabled[/dim]") progress.advance(upload_task) continue - using_fscore = has_frigate_scores(name) + using_fscore = has_frigate_scores(name) and Config.ENABLE_FRIGATE_SCORES if using_fscore: candidate_score = recognize_face(fpath) if candidate_score is None: @@ -458,7 +473,7 @@ def upload_to_frigate(jobs: list[dict]) -> None: asset_id = asset_map.get(fname) if asset_id: - post_fscore = recognize_face(fpath) + post_fscore = recognize_face(fpath) if Config.ENABLE_FRIGATE_SCORES else None mark_uploaded( asset_id, person_name=name,