fix: quality gate disabled at threshold 0; batch deletions; summary shows net count
- FRIGATE_SCORE_THRESHOLD=0.0 now fully disables the quality gate including the dynamic floor; a positive value is required to activate either - Post-reconcile gate deletions are batched into one API call per person instead of one call per file - Per-person summary reports gate removals and net uploaded count when the gate fires; grand summary includes total removed across all people - .env.example comment updated to match the corrected opt-in behaviour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -30,7 +30,7 @@ STRATEGY=auto
|
|||||||
# MIN_CONFIDENCE=0.7 # Minimum face detection confidence (default: 0.7)
|
# MIN_CONFIDENCE=0.7 # Minimum face detection confidence (default: 0.7)
|
||||||
# BLUR_THRESHOLD=120.0 # Laplacian blur threshold; lower = accept more blur (default: 120.0)
|
# 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)
|
# MAX_AUTO_IMAGES=80 # Hard cap on auto-diversity selection (default: 80)
|
||||||
# FRIGATE_SCORE_THRESHOLD=0.0 # Remove uploaded images scoring below this after upload (0 = disabled; skipped on cold start)
|
# FRIGATE_SCORE_THRESHOLD=0.0 # Quality gate: remove images scoring below this after upload (0 = disabled; requires at least one prior run)
|
||||||
|
|
||||||
# ── Caching & Models ──────────────────────────────────────────────────────────
|
# ── Caching & Models ──────────────────────────────────────────────────────────
|
||||||
# FORCE_CPU=true # Disable GPU, fall back to CPU
|
# FORCE_CPU=true # Disable GPU, fall back to CPU
|
||||||
|
|||||||
+34
-19
@@ -305,7 +305,7 @@ def upload_to_frigate(jobs: list[dict]) -> None:
|
|||||||
|
|
||||||
rprint(f" People: [bold]{len(face_jobs)}[/bold], Total images: [bold]{total_files}[/bold]")
|
rprint(f" People: [bold]{len(face_jobs)}[/bold], Total images: [bold]{total_files}[/bold]")
|
||||||
|
|
||||||
uploaded, failed = 0, 0
|
uploaded, failed, gate_total = 0, 0, 0
|
||||||
max_retries = 2
|
max_retries = 2
|
||||||
|
|
||||||
with Progress(
|
with Progress(
|
||||||
@@ -360,15 +360,15 @@ def upload_to_frigate(jobs: list[dict]) -> None:
|
|||||||
effective_count = get_tracked_frigate_file_count(name)
|
effective_count = get_tracked_frigate_file_count(name)
|
||||||
pre_run_count = effective_count
|
pre_run_count = effective_count
|
||||||
quality_replacement = job.get("config", {}).get("quality_replacement", False)
|
quality_replacement = job.get("config", {}).get("quality_replacement", False)
|
||||||
# Dynamic threshold: at least as strict as the weakest image already stored.
|
# Dynamic threshold: only active when FRIGATE_SCORE_THRESHOLD > 0.
|
||||||
# Takes whichever is higher — the configured floor or the current set minimum.
|
# Zero means the gate is disabled — the dynamic floor does not activate.
|
||||||
|
if Config.FRIGATE_SCORE_THRESHOLD > 0:
|
||||||
_dynamic = get_min_frigate_score(name)
|
_dynamic = get_min_frigate_score(name)
|
||||||
effective_threshold = max(
|
effective_threshold = max(Config.FRIGATE_SCORE_THRESHOLD, _dynamic or 0.0)
|
||||||
Config.FRIGATE_SCORE_THRESHOLD,
|
|
||||||
_dynamic if _dynamic is not None else 0.0,
|
|
||||||
)
|
|
||||||
if _dynamic is not None and _dynamic > Config.FRIGATE_SCORE_THRESHOLD:
|
if _dynamic is not None and _dynamic > Config.FRIGATE_SCORE_THRESHOLD:
|
||||||
logger.debug(f"{name}: dynamic Frigate score threshold {_dynamic:.3f}")
|
logger.debug(f"{name}: dynamic Frigate score threshold {_dynamic:.3f}")
|
||||||
|
else:
|
||||||
|
effective_threshold = 0.0
|
||||||
actually_uploaded: list[tuple[str, str | None]] = []
|
actually_uploaded: list[tuple[str, str | None]] = []
|
||||||
failed_deletes: set[str] = set()
|
failed_deletes: set[str] = set()
|
||||||
quality_gate_failed: set[str] = set()
|
quality_gate_failed: set[str] = set()
|
||||||
@@ -549,33 +549,46 @@ def upload_to_frigate(jobs: list[dict]) -> None:
|
|||||||
_reconcile_frigate_mappings(name, known_frigate_files_at_start, actually_uploaded)
|
_reconcile_frigate_mappings(name, known_frigate_files_at_start, actually_uploaded)
|
||||||
|
|
||||||
# Post-reconcile quality gate: filenames are now mapped, so we can delete.
|
# Post-reconcile quality gate: filenames are now mapped, so we can delete.
|
||||||
|
gate_removed = 0
|
||||||
if quality_gate_failed:
|
if quality_gate_failed:
|
||||||
removed = 0
|
to_delete: list[tuple[str, str]] = [] # (frigate_fn, asset_id)
|
||||||
for asset_id in quality_gate_failed:
|
for asset_id in quality_gate_failed:
|
||||||
frigate_fn = get_frigate_filename_for_asset(name, asset_id)
|
frigate_fn = get_frigate_filename_for_asset(name, asset_id)
|
||||||
if frigate_fn and delete_frigate_person_files(name, [frigate_fn]):
|
if frigate_fn:
|
||||||
remove_frigate_file(name, frigate_fn)
|
to_delete.append((frigate_fn, asset_id))
|
||||||
effective_count -= 1
|
|
||||||
removed += 1
|
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"{name}: could not remove low-score file for {asset_id}"
|
f"{name}: could not remove low-score file for {asset_id}"
|
||||||
" — no Frigate filename mapped (reconciliation race?)"
|
" — no Frigate filename mapped (reconciliation race?)"
|
||||||
)
|
)
|
||||||
if removed:
|
if to_delete:
|
||||||
progress.console.print(
|
if delete_frigate_person_files(name, [fn for fn, _ in to_delete]):
|
||||||
f" [yellow]🗑 {name}: removed {removed} image(s) below"
|
for frigate_fn, _aid in to_delete:
|
||||||
f" Frigate score threshold ({effective_threshold:.2f})[/yellow]"
|
remove_frigate_file(name, frigate_fn)
|
||||||
|
gate_removed = len(to_delete)
|
||||||
|
effective_count -= gate_removed
|
||||||
|
gate_total += gate_removed
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"{name}: batch delete of {len(to_delete)} low-score file(s) failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Per-person summary
|
# Per-person summary
|
||||||
if person_failed == 0:
|
if person_failed == 0 and gate_removed == 0:
|
||||||
progress.console.print(
|
progress.console.print(
|
||||||
f" ✅ {name}: {person_uploaded}/{person_uploaded} uploaded"
|
f" ✅ {name}: {person_uploaded}/{person_uploaded} uploaded"
|
||||||
)
|
)
|
||||||
else:
|
elif person_failed == 0:
|
||||||
|
net = person_uploaded - gate_removed
|
||||||
progress.console.print(
|
progress.console.print(
|
||||||
f" ⚠️ {name}: {person_uploaded} succeeded, {person_failed} failed"
|
f" [yellow]✅ {name}: {person_uploaded} uploaded,"
|
||||||
|
f" {gate_removed} removed by quality gate (score < {effective_threshold:.2f})"
|
||||||
|
f" → {net} net[/yellow]"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
gate_note = f", {gate_removed} removed by quality gate" if gate_removed else ""
|
||||||
|
progress.console.print(
|
||||||
|
f" ⚠️ {name}: {person_uploaded} succeeded, {person_failed} failed{gate_note}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Grand summary
|
# Grand summary
|
||||||
@@ -585,6 +598,8 @@ def upload_to_frigate(jobs: list[dict]) -> None:
|
|||||||
rprint(f" ❌ Failed: [red]{failed}[/red]")
|
rprint(f" ❌ Failed: [red]{failed}[/red]")
|
||||||
else:
|
else:
|
||||||
rprint(" ❌ Failed: 0")
|
rprint(" ❌ Failed: 0")
|
||||||
|
if gate_total:
|
||||||
|
rprint(f" 🗑 Removed (quality gate): [yellow]{gate_total}[/yellow]")
|
||||||
|
|
||||||
if failed > 0:
|
if failed > 0:
|
||||||
rprint(" [yellow]Check logs above for per-file error details.[/yellow]")
|
rprint(" [yellow]Check logs above for per-file error details.[/yellow]")
|
||||||
|
|||||||
Reference in New Issue
Block a user