feat: store Frigate recognition scores and use them for quality replacement

After each successful upload, call POST /api/faces/recognize to get
Frigate's own confidence score (0-1) for the uploaded crop. Store it
in the tracker as frigate_scores alongside the existing blur score.

When quality replacement activates and frigate_scores are present,
use them for the replacement comparison instead of blur scores — an
image Frigate recognizes poorly is a worse training image than one it
recognizes well, regardless of sharpness. Falls back to blur scores
on first run before any frigate_scores are populated.

Also surfaces frigate_score in TRACE_CROP_SIZE output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 16:21:58 +00:00
co-authored by Claude Sonnet 4.6
parent ab641847b2
commit 03be6ce2cb
4 changed files with 115 additions and 23 deletions
+29 -11
View File
@@ -13,7 +13,7 @@ from rich import print as rprint
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
from .config import Config, get_headers
from .frigate_api import delete_frigate_person_files, get_frigate_person_files
from .frigate_api import delete_frigate_person_files, get_frigate_person_files, recognize_face
from .image_processing import process_face_mode, process_full_mode, process_object_mode
from .immich_api import fetch_face_data, fetch_full_image
from .log_config import console
@@ -22,6 +22,7 @@ from .upload_tracker import (
get_lowest_quality_mapped_file,
get_tracked_frigate_file_count,
get_tracked_frigate_filenames,
has_frigate_scores,
mark_rejected,
mark_uploaded,
record_frigate_file,
@@ -383,30 +384,45 @@ 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
new_score = score_map.get(fname)
if new_score is None:
progress.console.print(f" [dim]⏭ {fname}: no confidence score, skipping replacement[/dim]")
progress.advance(upload_task)
continue
using_fscore = has_frigate_scores(name)
if using_fscore:
candidate_score = recognize_face(fpath)
if candidate_score is None:
progress.console.print(
f" [dim]⏭ {fname}: Frigate recognize unavailable, skipping replacement[/dim]"
)
progress.advance(upload_task)
continue
score_label = "frigate"
else:
candidate_score = score_map.get(fname)
if candidate_score is None:
progress.console.print(
f" [dim]⏭ {fname}: no quality score, skipping replacement[/dim]"
)
progress.advance(upload_task)
continue
score_label = "blur"
worst = get_lowest_quality_mapped_file(name, exclude=failed_deletes)
if worst is None or new_score <= worst[2]:
if worst is None or candidate_score <= worst[2]:
worst_score_str = f"{worst[2]:.3f}" if worst is not None else "N/A"
progress.console.print(
f" [dim]⏭ {fname}: score {new_score:.3f} ≤ worst mapped"
f" [dim]⏭ {fname}: {score_label} {candidate_score:.3f} ≤ worst"
f" {worst_score_str}, skipping[/dim]"
)
progress.advance(upload_task)
continue
# Delete the worst mapped file to make room for the better one
worst_frigate_file, _worst_asset_id, worst_score = worst
progress.console.print(
f" 🔄 {fname}: score {new_score:.3f} > {worst_score:.3f},"
f" 🔄 {fname}: {score_label} {candidate_score:.3f} > {worst_score:.3f},"
f" replacing {worst_frigate_file}"
)
if delete_frigate_person_files(name, [worst_frigate_file]):
remove_frigate_file(name, worst_frigate_file)
effective_count -= 1
min_quality_score_for_slot = worst_score
# Slot floor guard uses blur scores only — frigate_score mode
# will re-evaluate the next candidate via recognize_face anyway.
min_quality_score_for_slot = score_map.get(fname) if not using_fscore else None
else:
logger.warning(f"Failed to delete {worst_frigate_file} for {name}, skipping replacement")
failed_deletes.add(worst_frigate_file)
@@ -429,11 +445,13 @@ def upload_to_frigate(jobs: list[dict]) -> None:
asset_id = asset_map.get(fname)
if asset_id:
post_fscore = recognize_face(fpath)
mark_uploaded(
asset_id,
person_name=name,
score=score_map.get(fname),
crop_dims=dims_map.get(fname),
frigate_score=post_fscore,
)
actually_uploaded.append((fname, asset_id))