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)
This commit is contained in:
@@ -84,6 +84,7 @@ def _handle_duplicate_people(people: list[dict]) -> list[dict]:
|
|||||||
p.get("id")
|
p.get("id")
|
||||||
for ps in groups.values()
|
for ps in groups.values()
|
||||||
for p in sorted(ps, key=lambda x: x.get("assetCount", 0), reverse=True)[1:]
|
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)
|
skip_ids = _smaller_duplicate_ids(duplicates)
|
||||||
|
|||||||
@@ -303,6 +303,9 @@ def _select_by_embedding(
|
|||||||
|
|
||||||
emb = get_embedding(embed_img, asset_id=asset["id"])
|
emb = get_embedding(embed_img, asset_id=asset["id"])
|
||||||
if emb is not None:
|
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)
|
embeddings.append(emb)
|
||||||
valid_candidates.append(asset)
|
valid_candidates.append(asset)
|
||||||
confidence_scores.append(confidence)
|
confidence_scores.append(confidence)
|
||||||
|
|||||||
@@ -218,9 +218,14 @@ def get_face_embedding(img_pil: Image.Image) -> np.ndarray | None:
|
|||||||
if not faces:
|
if not faces:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Return embedding of largest face
|
# Return embedding of the face nearest the crop centre; a large margin can pull
|
||||||
largest = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
|
# a bigger neighbouring face into frame, and max-by-area would pick the wrong person.
|
||||||
return largest.embedding
|
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:
|
except Exception as e:
|
||||||
logger.error("Error getting face embedding: %s", e)
|
logger.error("Error getting face embedding: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -608,6 +608,12 @@ def upload_to_frigate(jobs: list[dict]) -> None:
|
|||||||
progress.console.print(
|
progress.console.print(
|
||||||
f" [red]✗ {fname}: {type(e).__name__} - {e} (after {max_retries} attempts)[/red]"
|
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)
|
progress.advance(upload_task)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user