From 068a8e675f07b0bd4bd788b4f1e5be382bb396f9 Mon Sep 17 00:00:00 2001 From: Holden Date: Wed, 17 Jun 2026 01:12:28 +0000 Subject: [PATCH] fix: 4 correctness bugs from full-codebase audit - executor: restore effective_count when replacement upload fails all retries (delete succeeded but slot was never filled, leaving cap undercount) - diversity: skip zero-norm embeddings before dedup/FPS selection (InsightFace zeros pass dedup with similarity 0 and score distance 1.0, getting selected first as maximally diverse) - cli: exclude None from skip_ids in _smaller_duplicate_ids (p.get('id') without None guard lets None into the set, silently dropping every other id-less person from the processed list) - embeddings: select face nearest crop centre instead of largest by area (25% margin can pull a bigger neighbouring face into the crop; largest-face selection then embeds the wrong person) --- winnow/cli.py | 1 + winnow/diversity.py | 3 +++ winnow/embeddings.py | 11 ++++++++--- winnow/executor.py | 6 ++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/winnow/cli.py b/winnow/cli.py index 7ac5447..d864b01 100644 --- a/winnow/cli.py +++ b/winnow/cli.py @@ -84,6 +84,7 @@ def _handle_duplicate_people(people: list[dict]) -> list[dict]: p.get("id") for ps in groups.values() for p in sorted(ps, key=lambda x: x.get("assetCount", 0), reverse=True)[1:] + if p.get("id") is not None } skip_ids = _smaller_duplicate_ids(duplicates) diff --git a/winnow/diversity.py b/winnow/diversity.py index 2b55c2e..154b4aa 100644 --- a/winnow/diversity.py +++ b/winnow/diversity.py @@ -303,6 +303,9 @@ def _select_by_embedding( emb = get_embedding(embed_img, asset_id=asset["id"]) if emb is not None: + if np.linalg.norm(emb) < 1e-6: + logger.debug("Zero-norm embedding for asset %s, skipping", asset["id"]) + continue embeddings.append(emb) valid_candidates.append(asset) confidence_scores.append(confidence) diff --git a/winnow/embeddings.py b/winnow/embeddings.py index a715162..7787da7 100644 --- a/winnow/embeddings.py +++ b/winnow/embeddings.py @@ -218,9 +218,14 @@ def get_face_embedding(img_pil: Image.Image) -> np.ndarray | None: if not faces: return None - # Return embedding of largest face - largest = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1])) - return largest.embedding + # Return embedding of the face nearest the crop centre; a large margin can pull + # a bigger neighbouring face into frame, and max-by-area would pick the wrong person. + cx, cy = img_pil.width / 2, img_pil.height / 2 + nearest = min( + faces, + key=lambda f: ((f.bbox[0] + f.bbox[2]) / 2 - cx) ** 2 + ((f.bbox[1] + f.bbox[3]) / 2 - cy) ** 2, + ) + return nearest.embedding except Exception as e: logger.error("Error getting face embedding: %s", e) return None diff --git a/winnow/executor.py b/winnow/executor.py index 52b7401..e263f4c 100644 --- a/winnow/executor.py +++ b/winnow/executor.py @@ -608,6 +608,12 @@ def upload_to_frigate(jobs: list[dict]) -> None: progress.console.print( f" [red]✗ {fname}: {type(e).__name__} - {e} (after {max_retries} attempts)[/red]" ) + else: + # All retries exhausted without a successful upload. + # Restore the slot freed by the preceding delete so the next + # candidate still sees at_cap=True and must beat the replacement gate. + if at_cap: + effective_count += 1 progress.advance(upload_task)