Dynamic floor now always active once Frigate scores exist — new images must
score at least as well as the weakest image already in the set, with no
config required. FRIGATE_SCORE_THRESHOLD adds an explicit absolute floor on
top. Gate active state is surfaced in normal output for both cases.
Quality replacement now pre-checks the gate threshold before deleting the
worst image. If the candidate would fail the gate, replacement is skipped
entirely rather than creating a net slot loss.
Gate-failed assets are reclassified as rejected (moved from uploaded_asset_ids
to rejected_asset_ids) so they are excluded from future runs without wasting
API calls on re-upload. RESET_PERSON still clears rejected records for a true
full reset. RETRY_REJECTED can recover them if the threshold is later lowered.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the dynamic threshold (min stored Frigate score) raises the effective
gate floor above the configured FRIGATE_SCORE_THRESHOLD, print it as a dim
info line rather than only logging at DEBUG/VERBOSE level.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- At upload start, diff tracker vs live Frigate file list and remove any
mappings for files no longer present; corrects effective_count so manually
deleted files don't permanently consume quota slots
- Add ENABLE_FRIGATE_SCORES config (default true); when false, skips all
recognize_face calls and falls back to blur scores for quality replacement
- Print a dim notice when FRIGATE_SCORE_THRESHOLD is set but pre_run_count
is zero, so users know the gate is deferred to the next run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
At the start of each person's upload phase, compute the minimum stored
Frigate recognition score across all currently mapped files. Use
max(config_threshold, dynamic_min) as the effective gate threshold so
new uploads must score at least as well as the weakest image already
in the training set.
Prevents overtraining well-recognised people: if all 80 images score
≥0.85, the dynamic threshold becomes ~0.85 and new additions that
score below that are removed rather than diluting a good training set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When FRIGATE_SCORE_THRESHOLD > 0, images that score below the threshold
after upload are deleted from Frigate and removed from the tracker.
Skipped when pre_run_count == 0 (cold start — no class mean to compare
against yet). Deletion happens after reconciliation so the Frigate
filename is known. Disabled by default (0.0).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Frigate classifies images with Laplacian variance < 120 as "very blurry"
and its own docs recommend avoiding blurry training data. Winnow was
accepting images in the 100-120 range that Frigate considers too blurry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
50px crops produce ~2,500–4,225 total pixels — well below Frigate's own
camera capture range of 16k–50k px. 90px guarantees ≥8,100 total pixels
even when face margins are fully clipped by image edges.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Store (width, height) of each face crop in the tracker at upload time
alongside the existing blur score. Expose TRACE_CROP_SIZE=<px> to look
up which Immich asset produced a crop with that pixel dimension, making
it straightforward to trace unexpected or low-quality images visible in
Frigate back to their source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was >=4.57.6; installed version is 5.12.0. Prevents users from
accidentally resolving the old 4.x series.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
setup-qemu-action v3 → v4, build-push-action v6 → v7
Required before June 16 when Node.js 20 actions are forced to Node.js 24
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- quality.py:122: split long tuple line to satisfy E501 (146 → ≤120)
- update-lockfile.yml: add branches filter so tag pushes don't trigger
the workflow (tag checkout is detached HEAD; git push has no target)
- release.yml: split four Docker build steps into parallel jobs (build-gpu,
build-cpu, build-rocm, build-intel), each with its own runner; previously
all four ran in one job and exhausted disk after GPU+CPU builds, leaving
ROCm and Intel cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
C1/C4: Cap blur-score computation at 1440 px before calling assess_quality
so scores are always on the same Laplacian scale as the embedding path
(which operates on Immich preview thumbnails). Also converts the image to
RGB before scoring and stores 0.0 on assess_quality failure so files
uploaded without a score remain eligible for future quality replacement
instead of occupying a slot permanently.
C2: Fall back to the tracker's mapped-filename set as the pre-upload
baseline when the Frigate GET /api/faces endpoint is unreachable at upload
start. Previously, uploads that succeeded during a partial API outage were
never mapped in frigate_files, leaving get_tracked_frigate_file_count
permanently under-counting those files and allowing Frigate to exceed
MAX_AUTO_IMAGES over time.
C3: Track min_quality_score_for_slot when a quality-replacement delete
succeeds but the subsequent upload fails. This ensures the freed slot can
only be filled by a candidate that beats the deleted file's score, not just
the next file in iteration order (which could be lower quality than what
was deleted).
Add get_tracked_frigate_filenames() to upload_tracker and expand tracker
tests to cover the new function and exclude-parameter behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Pipeline rewritten as 8 steps: separates thumbnail pass (quality filter +
embeddings) from full-res download and crop; adds quality replacement
logic at the upload step
- Persistence note updated to cover rejected IDs and RETRY_REJECTED
- Auto mode stopping threshold documented (20%/10% of median pairwise distance)
- Add OUTPUT_DIR env var (was undocumented)
- Local Install section notes interactive vs auto mode behaviour
- QUALITY_REPLACEMENT description tightened
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- update-lockfile.yml: rename workflow to "Update lockfiles"
- release.yml: add explicit uv python install 3.13 for consistency with other workflows
- docker-publish.yml: rename cpu cache scope from linux/amd64-cpu to cpu (now multi-arch)
- README: add GitHub release version badge and License badge
- README: shorten QUALITY_REPLACEMENT table cell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- update-lockfile.yml: regenerate all four lockfiles (main + cpu/rocm/intel
variants) on any pyproject change; add variant pyproject files to trigger
- docker-publish.yml: add lockfiles to paths-ignore so the bot commit does
not trigger a second Docker build; remove redundant CONTRIBUTING/SECURITY
entries already covered by **.md; add QEMU to build-cpu; set CPU image to
linux/amd64,linux/arm64 to match release
- release.yml: inline lockfile generation now covers all variants; add
workflow_dispatch guard that fails if not dispatched from main
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dockerfile: ARG VERSION + OCI labels (title, description, source, licenses, version)
- release.yml: pass VERSION build-arg to all four image builds
- docker-publish.yml: extend paths-ignore to cover community files
- .github/ISSUE_TEMPLATE/config.yml: disable blank issues, link to Discussions and wiki
- README.md: add Getting Help section
- pyproject.toml: expand description; add System Administrators audience classifier
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
C1/C4: Cap blur-score computation at 1440 px before calling assess_quality
so scores are always on the same Laplacian scale as the embedding path
(which operates on Immich preview thumbnails). Also converts the image to
RGB before scoring and stores 0.0 on assess_quality failure so files
uploaded without a score remain eligible for future quality replacement
instead of occupying a slot permanently.
C2: Fall back to the tracker's mapped-filename set as the pre-upload
baseline when the Frigate GET /api/faces endpoint is unreachable at upload
start. Previously, uploads that succeeded during a partial API outage were
never mapped in frigate_files, leaving get_tracked_frigate_file_count
permanently under-counting those files and allowing Frigate to exceed
MAX_AUTO_IMAGES over time.
C3: Track min_quality_score_for_slot when a quality-replacement delete
succeeds but the subsequent upload fails. This ensures the freed slot can
only be filled by a candidate that beats the deleted file's score, not just
the next file in iteration order (which could be lower quality than what
was deleted).
Add get_tracked_frigate_filenames() to upload_tracker and expand tracker
tests to cover the new function and exclude-parameter behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Pipeline rewritten as 8 steps: separates thumbnail pass (quality filter +
embeddings) from full-res download and crop; adds quality replacement
logic at the upload step
- Persistence note updated to cover rejected IDs and RETRY_REJECTED
- Auto mode stopping threshold documented (20%/10% of median pairwise distance)
- Add OUTPUT_DIR env var (was undocumented)
- Local Install section notes interactive vs auto mode behaviour
- QUALITY_REPLACEMENT description tightened
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- update-lockfile.yml: rename workflow to "Update lockfiles"
- release.yml: add explicit uv python install 3.13 for consistency with other workflows
- docker-publish.yml: rename cpu cache scope from linux/amd64-cpu to cpu (now multi-arch)
- README: add GitHub release version badge and License badge
- README: shorten QUALITY_REPLACEMENT table cell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- update-lockfile.yml: regenerate all four lockfiles (main + cpu/rocm/intel
variants) on any pyproject change; add variant pyproject files to trigger
- docker-publish.yml: add lockfiles to paths-ignore so the bot commit does
not trigger a second Docker build; remove redundant CONTRIBUTING/SECURITY
entries already covered by **.md; add QEMU to build-cpu; set CPU image to
linux/amd64,linux/arm64 to match release
- release.yml: inline lockfile generation now covers all variants; add
workflow_dispatch guard that fails if not dispatched from main
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dockerfile: ARG VERSION + OCI labels (title, description, source, licenses, version)
- release.yml: pass VERSION build-arg to all four image builds
- docker-publish.yml: extend paths-ignore to cover community files
- .github/ISSUE_TEMPLATE/config.yml: disable blank issues, link to Discussions and wiki
- README.md: add Getting Help section
- pyproject.toml: expand description; add System Administrators audience classifier
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, effective_count and the jobs.py cap check used the total
Frigate file count (including manually-added files), so any file a user
curated by hand ate into winnow's managed quota. Now:
- get_tracked_frigate_file_count() returns len(frigate_files) from the
tracker — only files winnow uploaded and reconciled
- effective_count in the upload loop uses this tracker count so
manually-added files are invisible to the cap
- jobs.py capacity check uses len(frigate_files) instead of the live
Frigate API count or cached frigate_count
- Frigate API call for known_frigate_files_at_start is now only used
for the post-upload reconciliation diff, not for cap enforcement
Side-effect: fixes audit bug #1 — an unreachable Frigate GET no longer
zeroes effective_count and bypasses the cap, because the cap is now
read from the always-available local tracker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>