fix: address 4 code review findings (round 6)

- executor: snapshot has_frigate_model = effective_count > 0 before the
  upload loop; use it in the recognize_face gate instead of the live
  effective_count, which is incremented mid-loop and would otherwise
  trigger recognize_face calls against an empty Frigate model on first run
- jobs: restore if already_uploaded > 0 guard before limit = capacity so
  first-run auto-strategy jobs keep limit="auto" and the FPS adaptive
  early-stop can fire instead of always filling MAX_AUTO_IMAGES slots
- cli: retry get_people() once after a post-merge empty response before
  falling back to the pre-merge list; improve warning to name expired API
  key as a possible cause alongside transient network errors
- diversity: hoist hard_weight = np.where(...) above the FPS while loop
  since conf_array is constant; eliminates one O(n) numpy pass per
  selected image
This commit is contained in:
2026-06-16 22:22:51 +00:00
parent 2cb126a589
commit 692d77ee9f
4 changed files with 24 additions and 11 deletions
+7 -2
View File
@@ -131,10 +131,15 @@ def _handle_duplicate_people(people: list[dict]) -> list[dict]:
if merged_any:
rprint(" [dim]Re-fetching people after merge...[/dim]")
fresh = get_people()
if not fresh:
# Retry once: get_people() returns [] for both transient failures and
# auth errors (401); a second empty result strongly suggests a real failure.
fresh = get_people()
if not fresh:
logger.warning(
"Re-fetch after merge returned no people"
" — possible transient error; proceeding with pre-merge list"
"Re-fetch after merge returned no people (tried twice)"
" — possible transient error or expired API key;"
" proceeding with pre-merge list. Check IMMICH_API_KEY if this recurs."
)
return [p for p in people if p["id"] not in skip_ids]
# Filter out the smaller duplicate from any group whose merge failed — those
+4 -3
View File
@@ -556,10 +556,11 @@ def _cluster_aware_selection(
for idx in selected:
min_dists[idx] = -np.inf
# Hard example weighting: boost distance for low-confidence candidates.
# conf_array is constant after this point, so compute once outside the loop.
hard_weight = np.where(conf_array < 0.85, 1.0 + (0.85 - conf_array) * 2.0, 1.0)
while len(selected) < target:
# Hard example weighting: boost distance for low-confidence candidates
# Confidence < 0.85 gets up to 1.5× distance boost
hard_weight = np.where(conf_array < 0.85, 1.0 + (0.85 - conf_array) * 2.0, 1.0)
weighted_dists = min_dists * hard_weight
best_idx = int(np.argmax(weighted_dists))
+7 -2
View File
@@ -363,6 +363,11 @@ def upload_to_frigate(jobs: list[dict]) -> None:
progress.console.print(
f" [dim]{name}: first run — Frigate diversity scoring will apply from the next run[/dim]"
)
# Snapshot whether Frigate has a model before the upload loop starts.
# effective_count is incremented inside the loop on each successful upload,
# so using the live value would incorrectly trigger recognize_face calls
# mid-batch on the first run (after the first upload sets it to 1).
has_frigate_model = effective_count > 0
actually_uploaded: list[tuple[str, str | None]] = []
failed_deletes: set[str] = set()
min_quality_score_for_slot: float | None = None
@@ -392,7 +397,7 @@ def upload_to_frigate(jobs: list[dict]) -> None:
# Pre-upload Frigate score — clean measurement (image not yet in training set).
# Called for all below-cap uploads (seeds frigate_scores for future at-cap
# replacement) and for at-cap uploads when scores already exist. Skipped on
# skipped when effective_count == 0 since Frigate has no model yet.
# skipped when has_frigate_model is False (effective_count was 0 before the loop).
# recognize_face returns (face_name, score); we only use the score when the
# best match is for the correct person. Mismatches (or "unknown") are treated
# as None so a wrong-person score never drives a ceiling skip or replacement.
@@ -408,7 +413,7 @@ def upload_to_frigate(jobs: list[dict]) -> None:
# rebuild-complete signal, poll it between recognize calls during replacement
# sequences rather than accepting stale/None scores.
pre_fscore: float | None = None
if Config.ENABLE_FRIGATE_SCORES and effective_count > 0:
if Config.ENABLE_FRIGATE_SCORES and has_frigate_model:
if not at_cap or person_has_fscores:
_result = recognize_face(fpath)
if _result is not None and (_result[0] or "").casefold() == name.casefold():
+6 -4
View File
@@ -301,10 +301,12 @@ def auto_configure(people: list[dict]) -> list[dict]:
# decides per-image whether to swap; any candidate could be an improvement).
if not quality_replacement_only:
if limit == "auto":
# Switch from open-ended auto to a fixed budget at remaining capacity
# so the diversity selector itself stops at the right count instead of
# selecting more than MAX_AUTO_IMAGES and overflowing the cap.
limit = capacity
if already_uploaded > 0:
# Switch from open-ended auto to a fixed budget at remaining capacity
# so the diversity selector stops at the right count instead of
# selecting more than MAX_AUTO_IMAGES and overflowing the cap.
# First runs keep limit="auto" so FPS adaptive early-stop can fire.
limit = capacity
else:
limit = min(limit, capacity)