- rocm/intel/gpu extras are x86_64-only; aarch64 wheels don't exist so uv
failed to resolve them when required-environments includes aarch64
- test.yml: switch from --all-extras (broken by conflicts + missing wheels)
to --extra cpu which is cross-platform and sufficient for unit tests
- update-lockfile.yml: drop old file-swap loop; single pyproject means a
single uv lock run and a single uv.lock to commit
- Config: remove _ConfigAccessor and ConfigManager; use __getattr__ for lazy
loading on single _Config class; re-register self as _instance in __getattr__
so reset() always clears the correct object (item 1)
- upload_tracker: replace hand-rolled JSON store with sqlite3; auto-migrates
existing JSON on first run; remove dead record_frigate_file function;
connection re-opens when CACHE_DIR changes for test isolation (items 2, 8)
- diversity: move ThreadPoolExecutor import to module level; inject optional
fetch_fn parameter for testability (items 3, 6)
- pyproject: consolidate 4 variant files into extras (gpu/rocm/intel/cpu);
update Dockerfile to use --extra flag; delete variant pyproject/lock files;
uv.lock needs regen with `uv lock` after this change (item 4)
- jobs: extract _build_job helper to separate business logic from terminal I/O;
auto_configure delegates dedup/selection to _build_job (item 5)
- logging: convert f-string log calls to % interpolation throughout all winnow/
modules (item 7)
- reconcile: new module with reconcile_frigate_mappings and
enrich_asset_with_face_data extracted from executor.py (item 9)
- scheduler: print next scheduled run time after startup and after each run;
fix f-string logger.error call (item 10)
cache.py — model fingerprint auto-invalidation:
Replace hardcoded "buffalo_l_v1" version string with a fingerprint
derived from buffalo_l .onnx file sizes and mtimes. EmbeddingCache now
computes this at init time; stale embeddings from replaced or updated
model files are automatically invalidated. Falls back to the static
string before the model is downloaded.
Note: existing caches built against the old key will miss on the first
run after upgrade and recompute cleanly.
frigate_api.py — Frigate version check:
Add get_frigate_version() (GET /api/version). Called at the start of
upload_to_frigate(); warns if below v0.16 where the face training API
endpoints don't exist.
immich_api.py + cli.py — Immich version check:
Add get_immich_version() (GET /api/server/version). Called at startup
before get_people(); warns if below v1.106 where the face data and
merge APIs winnow depends on aren't guaranteed present.
Remaining TODO(frigate-api) annotations are left in place — they require
Frigate to expose per-file embeddings or a rebuild-complete signal before
they can be addressed.
Adds inline LIMITATION / TODO(frigate-api) comments at each specific
code site rather than a separate doc that would drift from the code.
frigate_api.py — recognize_face:
Mean-embedding limitation: score reflects the arithmetic mean of all
training embeddings. A bimodal set (frontals + profiles) has a mean
between clusters, making both ends look more novel than they are.
Fixable if Frigate exposes per-file embeddings for nearest-neighbour
comparison.
frigate_api.py — get_all_frigate_person_files:
"train" key exclusion is a hardcoded string. If Frigate adds other
special top-level keys in /api/faces they'll be silently treated as
person names. Needs a typed schema when Frigate documents the contract.
executor.py — recognize_face call site:
Async rebuild: each deletion triggers a background model rebuild in
Frigate. Subsequent recognize calls in the same run return None
(rebuild in progress), degrading quality replacement for later
candidates. Fixable with a rebuild-complete signal from Frigate.
executor.py — effective_count / manual file handling:
Manually-added files are invisible to diversity decisions. Winnow
observes their effect only indirectly via the Frigate score, not by
measuring their embedding distribution. Per-file embeddings from
Frigate would allow direct diversity measurement against the full set.
executor.py — Frigate version assumption:
All face training endpoints are v0.16+. No version check at startup;
failures on older versions are opaque 404s.
cache.py — MODEL_VERSIONS:
Version string is a hardcoded constant. Manual model file replacement
(custom weights, InsightFace update) won't invalidate cached embeddings.
Needs file-checksum-derived versioning or a CLEAR_EMBEDDING_CACHE flag.
diversity.py — thumbnail-resolution embeddings:
Diversity selection runs InsightFace on preview thumbnails; the actual
training crop comes from full-resolution originals. Negligible in
practice but degrades if Immich preview quality is low.
Tests cover the core ML pipeline algorithms in diversity.py — previously
untested. No network or model dependencies; all pure-function or
numpy-only paths:
- Face bbox and confidence extraction from Immich metadata, including
person_id filtering and missing-data edge cases
- Face crop scaling: verifies bbox coordinates are correctly scaled when
the thumbnail dimensions differ from the metadata image dimensions
- Near-duplicate dedup: removal below cosine threshold, quality-score
preference between duplicates, zero-quality-score treated as zero not
missing (falsy bug guard)
- K-Medoids: correct medoid count, distinctness, valid index range, and
full-N edge case
- Adaptive threshold: positive output, floor at 0.05 for identical
embeddings, single-point, scales with embedding spread
- Time-spread fallback: exact count, all-under-limit passthrough,
auto→30 default, first/last inclusion
- Cluster-aware selection: exact limit, subset invariant, auto-stop on
tight cluster, hard-example confidence weighting accepted
Remove torch/torchvision/transformers/ultralytics from all three variant
pyproject files (rocm/cpu/intel) — no longer needed since SigLIP and YOLO
were removed in v0.4.10. Update version to 0.4.10 and description.
NOTE: uv-rocm.lock, uv-cpu.lock, uv-intel.lock are now stale and must be
regenerated in their respective platform environments before the next Docker
build of those variants.
Also fix fetch_face_data() docstring to not mention embedding (removed field).
- immich_api.py: remove FaceData.embedding field and numpy import — Immich face
embeddings were never consumed after being fetched; bbox/confidence is all that's used
- embeddings.py: remove immich_embedding param from get_embedding() — never passed by
any caller; simplify docstring accordingly
- image_processing.py: wrap insightface_app.get() in warnings filter to suppress the
scikit-image FutureWarning about estimate being deprecated (already suppressed in
embeddings.py for the diversity path, was leaking from the crop-alignment path)
- README.md: remove two stale "object mode" / "object classification" fragments
After removing the object pipeline, several dead 'mode' artifacts remained:
- executor.py: unpack `config` from job even though it was no longer read
- jobs.py: set `"mode": "face"` in both configure paths (key never consumed)
- compose.yml: TRAINING_MODE=face env, OBJECT_CLASS comment, HF_HOME, stale MAX_AUTO_IMAGES default note
- Dockerfile: "and object classification" label, /models/huggingface mkdir, HF_HOME ENV
- cache.py: drop siglip MODEL_VERSIONS entry
- log_config.py: drop ultralytics/transformers/torch from noise silencers
- scheduler.py: drop HF_HOME and HuggingFace model check
- scripts/benchmark.py: drop bench_siglip and make_random_image
winnow is a face recognition training tool. Object mode required manual
file placement with no Frigate API, pulled in torch/torchvision/transformers/
ultralytics (~2 GB), and was architecturally misaligned with the project goal.
Removed:
- process_object_mode (YOLO inference), get_yolo_model
- SigLIP model stack (get_siglip_model, get_object_embedding, batch variant)
- entity_type branching throughout diversity, embeddings, jobs, executor
- TRAINING_MODE and OBJECT_CLASS env vars
- torch, torchvision, transformers, ultralytics dependencies
- pytorch index entries from pyproject.toml
- Object mode from README (Modes section, env var table, How It Works)
- How It Works step 9: "upload freely" → note novelty gate may skip below-cap candidates
- Persistence note: "Frigate rejections" → "rejected assets" (covers confidence skips too)
- RETRY_REJECTED description: explicitly covers all rejection types, not just Frigate
- Image Quality section: split into user-adjustable controls and calibrated image
processing defaults with a support disclaimer to deter blind tuning
Previously, assets that passed embedding-phase selection but failed the
faces API confidence check in execute_jobs were silently skipped with no
tracker entry. They appeared as valid candidates on every future run,
were re-selected, and re-skipped in an endless cycle. Now they are
marked rejected so they are excluded from future runs. RETRY_REJECTED=true
clears them if Immich later re-processes the image.
FRIGATE_SCORE_CEILING now defaults to dynamic mode (unset): below-cap
candidates are skipped if their pre-upload Frigate score exceeds the
most-redundant tracked file's score. This catches conditions already
covered by manually-added Frigate images that winnow cannot track —
the embedding-based diversity selection has no visibility into those.
Set FRIGATE_SCORE_CEILING=0 to disable; a positive value (e.g. 0.85)
still acts as a fixed hard ceiling. First-run safety is unchanged
(pre_run_count==0 prevents recognize_face from being called).
The two quality replacement branches (Frigate-score and blur-score)
shared identical structure and are merged into a single code path
parameterised by score source and comparison direction.
Also raises MIN_FACE_COUNT default from 0 to 3 and updates the
config test to match.
AUTO_MODE (batch/unattended) and STRATEGY=auto (diversity algorithm) shared
the same word for unrelated concepts. Renaming the strategy value to
'adaptive' eliminates the ambiguity. The old value is kept as a silent alias
so existing configs continue to work.
Interactive menu updated from "Auto (Objective Diversity)" to "Adaptive
Diversity". README AUTO_MODE description clarified to emphasise unattended
batch processing, not selection strategy.
- `:latest` image tag listed CUDA 13.3 — actual base is 12.8.1
- MERGE_DUPLICATE_PEOPLE existed in config but was absent from env var table
- TRACE_CROP_SIZE existed in CLI but was absent from env var table
- RESET_PERSON description now mentions `*` wildcard for bulk reset of all people
- Step 5 (near-duplicate removal) was added in v0.4.4 but never
documented; added between embedding and diversity selection
- Auto-stop was described as 'stops when similarity exceeds threshold'
which is backwards; it stops when the next candidate's distance to
already-selected images falls below the threshold (too similar, not
enough new information)
- Renumbered steps 5-8 to 6-9
- Tightened hard-example weighting description to match the code
All _load() calls after the first return the cached dict instead of
re-reading disk. _save() updates both disk and cache atomically.
Drops per-person tracker reads from ~90 to ~1 in the upload loop.
Keyed by resolved file path so test isolation (unique tmp_path dirs)
is preserved with no fixture changes needed.
- _dedup_embeddings: pre-allocated (Q,D) buffer replaces vstack-on-keep,
dropping O(K²×D) copy overhead down to O(K×D) fill work
- _kmedoids: swap cost sum replaced with numpy fancy-index reduction,
~20-50x faster per swap evaluation
- _reconcile_frigate_mappings: O(L) load/save pairs collapsed to one
batch write via record_frigate_files_batch
Fetching up to MAX_PAGES*page_size (1M) assets before the 3000-item
diversity pool cap was applied could exhaust memory on large Immich
libraries. Early-exit once 5000 items are collected — the pool cap
of 3000 makes anything beyond that wasteful. Also filter null/non-dict
items from page responses at fetch time.
- _dedup_embeddings: rebuild kept_stack only on keep (was every iteration → O(N²))
- _dedup_embeddings: fix quality_score sort key to use explicit None check (falsy-zero)
- _select_by_embedding: add post-dedup pool < limit guard with warning
- executor: use full resp.text for 'face' keyword check; only truncate display snippet
- _safe_person_dir: avoid false "//" prefix when output_dir resolves to filesystem root