Commit Graph
453 Commits
Author SHA1 Message Date
dependabot[bot] 6ec49976c7 chore(deps): bump astral-sh/setup-uv from 8.2.0 to 8.3.2
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.2.0 to 8.3.2.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/fac544c07dec837d0ccb6301d7b5580bf5edae39...11f9893b081a58869d3b5fccaea48c9e9e46f990)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-09 18:54:17 +00:00
flan eba50f8a4a Merge pull request #48 from sudolulo/dependabot/docker/nvidia/cuda-12.9.2-cudnn-runtime-ubuntu24.04
chore(deps): bump nvidia/cuda from 12.8.1-cudnn-runtime-ubuntu24.04 to 12.9.2-cudnn-runtime-ubuntu24.04
2026-06-26 18:14:53 -04:00
flan 95d5e91a67 Merge pull request #49 from sudolulo/dependabot/uv/python-deps-c7af4feaef
chore(deps): bump the python-deps group with 3 updates
2026-06-26 18:14:22 -04:00
dependabot[bot] 9e904d9f34 chore(deps): bump the python-deps group with 3 updates
Bumps the python-deps group with 3 updates: [numpy](https://github.com/numpy/numpy), [pytest](https://github.com/pytest-dev/pytest) and [ruff](https://github.com/astral-sh/ruff).


Updates `numpy` from 2.4.6 to 2.5.0
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.4.6...v2.5.0)

Updates `pytest` from 9.1.0 to 9.1.1
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/9.1.0...9.1.1)

Updates `ruff` from 0.15.18 to 0.15.20
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.18...0.15.20)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-deps
- dependency-name: pytest
  dependency-version: 9.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: python-deps
- dependency-name: ruff
  dependency-version: 0.15.20
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: python-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-25 18:55:02 +00:00
dependabot[bot] 18305b5b06 chore(deps): bump nvidia/cuda
Bumps nvidia/cuda from 12.8.1-cudnn-runtime-ubuntu24.04 to 12.9.2-cudnn-runtime-ubuntu24.04.

---
updated-dependencies:
- dependency-name: nvidia/cuda
  dependency-version: 12.9.2-cudnn-runtime-ubuntu24.04
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-25 18:53:34 +00:00
flan 9b68f62f69 Merge pull request #44 from sudolulo/dependabot/github_actions/actions/checkout-7.0.0
chore(deps): bump actions/checkout from 6.0.3 to 7.0.0
2026-06-20 15:06:54 -04:00
flan b5d03a2695 Merge branch 'main' into dependabot/github_actions/actions/checkout-7.0.0 2026-06-20 15:05:37 -04:00
flan fce46d409e Merge pull request #45 from sudolulo/dependabot/uv/python-deps-ae41630b22
chore(deps): bump the python-deps group across 1 directory with 5 updates
2026-06-20 15:05:13 -04:00
dependabot[bot] 5044343a99 chore(deps): bump the python-deps group across 1 directory with 5 updates
Bumps the python-deps group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [onnxruntime-gpu](https://github.com/microsoft/onnxruntime) | `1.26.0` | `1.27.0` |
| [nvidia-cudnn-cu12](https://developer.nvidia.com/cuda-zone) | `9.23.1.3` | `9.23.2.1` |
| [onnxruntime](https://github.com/microsoft/onnxruntime) | `1.26.0` | `1.27.0` |
| [pytest](https://github.com/pytest-dev/pytest) | `9.0.3` | `9.1.0` |
| [ruff](https://github.com/astral-sh/ruff) | `0.15.17` | `0.15.18` |



Updates `onnxruntime-gpu` from 1.26.0 to 1.27.0
- [Release notes](https://github.com/microsoft/onnxruntime/releases)
- [Changelog](https://github.com/microsoft/onnxruntime/blob/main/docs/ReleaseManagement.md)
- [Commits](https://github.com/microsoft/onnxruntime/commits)

Updates `nvidia-cudnn-cu12` from 9.23.1.3 to 9.23.2.1

Updates `onnxruntime` from 1.26.0 to 1.27.0
- [Release notes](https://github.com/microsoft/onnxruntime/releases)
- [Changelog](https://github.com/microsoft/onnxruntime/blob/main/docs/ReleaseManagement.md)
- [Commits](https://github.com/microsoft/onnxruntime/commits)

Updates `pytest` from 9.0.3 to 9.1.0
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.3...9.1.0)

Updates `ruff` from 0.15.17 to 0.15.18
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.17...0.15.18)

---
updated-dependencies:
- dependency-name: nvidia-cudnn-cu12
  dependency-version: 9.23.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-deps
- dependency-name: onnxruntime
  dependency-version: 1.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-deps
- dependency-name: onnxruntime-gpu
  dependency-version: 1.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-deps
- dependency-name: pytest
  dependency-version: 9.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: python-deps
- dependency-name: ruff
  dependency-version: 0.15.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: python-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-18 20:42:03 +00:00
dependabot[bot] 2a673fcd9d chore(deps): bump actions/checkout from 6.0.3 to 7.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-18 20:40:32 +00:00
flan 04151057c9 Merge pull request #46 from sudolulo/dev
release: v0.6.6
v0.6.6
2026-06-18 16:38:28 -04:00
flan 3ea6b9d566 Merge pull request #47 from sudolulo/chore/simplify-lockfile-ci
chore: replace lockfile auto-update with uv lock --check
2026-06-18 16:36:10 -04:00
flan e281ac01e1 chore: add pre-commit hook to auto-update lockfile on pyproject.toml changes 2026-06-18 20:33:58 +00:00
flan 0ac02c3108 chore: update lockfile for v0.6.6 2026-06-18 20:31:36 +00:00
flan db30449b09 chore: replace lockfile auto-update with uv lock --check 2026-06-18 20:29:43 +00:00
flan 1e4425bfd7 release: v0.6.6 2026-06-18 20:23:58 +00:00
flan 2be9f400dd Merge pull request #42 from sudolulo/fix/frigate-500-permanent-rejection
fix: Frigate 500 permanent rejection + 13 correctness bugs
2026-06-17 15:17:20 -04:00
flan f3b5bd9334 fix: update MAX_AUTO_IMAGES default assertion to 5 2026-06-17 19:16:43 +00:00
flan 0906342edf fix: 10 correctness bugs from full codebase audit
- immich_api: .get("items") or [] handles {"items": null} without crashing len()
- immich_api: catch TypeError alongside ValueError in filter_recent_assets for
  timezone-naive fileCreatedAt comparisons
- scheduler: catch SystemExit in addition to KeyboardInterrupt so cli.main()
  cannot kill the long-running scheduler process
- scheduler: reseed croniter from wall-clock time after each run so overrunning
  jobs don't schedule an immediate back-to-back rerun
- config: reject negative YEARS_FILTER values with a warning, reset to default 10
- frigate_api: return True (not False) for empty filenames list — callers cannot
  distinguish no-op from network failure on False
- jobs: warn on unrecognised STRATEGY value instead of silently falling back
- jobs: casefold ONLY_PEOPLE / SKIP_PEOPLE matching so "john doe" matches "John Doe"
- executor: <= → < so a same-score candidate can fill a freed replacement slot
- embeddings: set _insightface_loaded=True on GPU+CPU double-failure to prevent
  N re-init attempts (one per asset) when InsightFace is broken for a whole run
2026-06-17 19:12:58 +00:00
flan 5e0a871314 fix: 2 low findings from audit — consistent 500 match source, accurate return type
Use full_body for the 500 'could not process' permanent-rejection check,
consistent with the 400 'face' check on the line above. error_detail is
truncated to 100 chars via the fallback path, which could silently miss
the phrase in a long response body.

Remove | None from process_face_mode return type — every code path returns
tuple[int,int] or str; None is unreachable. Update docstring to match.
2026-06-17 17:47:31 +00:00
flan 86caffc8d7 fix: 3 audit findings — truthy skip-reason bug, 500 match consistency, CHANGELOG note
1. if saved: → if isinstance(saved, tuple): so string skip-reasons from
   process_face_mode no longer register as successes and create phantom
   asset_map entries with no JPEG on disk. Dead reason/fallback code in
   the else branch now correctly handles str and None returns.

2. "could not process" permanent-rejection check now uses error_detail
   (json message field, falling back to body[:100]) instead of full_body,
   keeping the match consistent with what is displayed to the user.

3. CHANGELOG [Unreleased] breaking-change note for MAX_AUTO_IMAGES 20→5
   so upgrading users know to set the env var if they want the old cap.
2026-06-17 17:31:20 +00:00
flan f6e494071e fix: lower MAX_AUTO_IMAGES default from 20 to 5
Smaller default cap is more conservative for new installs and better
reflects the minimum viable training set for Frigate face recognition.
Users who need more can set MAX_AUTO_IMAGES explicitly.
2026-06-17 17:19:09 +00:00
flan b69f776378 fix: surface skip reasons and suppress norm_crop FutureWarning
process_face_mode now returns a descriptive string instead of None for
filtered-out faces ("face too small 45x38px, min 90px", "no face
metadata"), so the executor can print a useful reason rather than the
generic "no usable face data".

Also suppresses the InsightFace norm_crop FutureWarning about deprecated
estimate usage, which was noisy at INFO level on every aligned crop.
2026-06-17 17:18:43 +00:00
flan c4910d82ef fix: treat Frigate 500 'Could not process' as permanent rejection
Frigate returns HTTP 500 with 'Could not process' when its face detector
cannot find or embed a face in the uploaded crop — this will never succeed
on retry. Previously these were silently logged at DEBUG and retried on
every future run.

- Surface 500 error details inline (same display path as 400)
- Mark 500 + 'could not process' as a permanent rejection so the asset
  is skipped on future runs instead of retried indefinitely
2026-06-17 17:16:04 +00:00
flan 5b8b3ab736 Merge pull request #41 from sudolulo/dev
fix: lockfile workflow opens PR on dev instead of direct push
2026-06-16 23:23:44 -04:00
flan dc0c431e6b chore: update lockfile 2026-06-17 03:12:20 +00:00
flan 3296940806 fix: lockfile workflow opens PR on dev instead of direct push
dev is a protected branch requiring PRs. The previous direct push caused
the lockfile update CI job to fail with 'protected branch hook declined'.
When the triggering branch is dev, the workflow now creates a side branch
and opens a PR; all other branches continue to push directly.
2026-06-17 03:12:00 +00:00
flan fdcb4efac3 Merge pull request #40 from sudolulo/dev
release: v0.6.5
v0.6.5
2026-06-16 23:08:08 -04:00
flan c1f04be15b release: v0.6.5 2026-06-17 03:06:27 +00:00
flan 001dd2c575 fix: reinstall onnxruntime-gpu after uv sync to guarantee GPU binary wins
insightface depends on onnxruntime (CPU) as a direct dependency. During
uv sync --extra gpu, both onnxruntime (CPU, 24.6 MB binary) and
onnxruntime-gpu (GPU, 24.7 MB binary) are installed in parallel — both
claim onnxruntime/capi/onnxruntime_pybind11_state.so. The last writer wins,
which is non-deterministic in uv's parallel installer.

On GitHub Actions (no GPU, different scheduler ordering), the CPU binary
consistently wins, leaving onnxruntime-gpu's pybind11_state.so as the CPU
version. CUDAExecutionProvider then silently disappears because the CPU
binary's provider registration code has no CUDA EP.

Fix: after uv sync, reinstall onnxruntime-gpu explicitly using the already-
cached wheel. Since uv pip install is synchronous and runs after the parallel
sync completes, the GPU binary is guaranteed to be on disk when the build
layer commits.
2026-06-17 03:05:33 +00:00
flan edf576bc93 Merge pull request #39 from sudolulo/fix/codebase-audit-r2
fix: codebase audit rounds 2-5 (correctness, cli, jobs)
2026-06-16 23:02:42 -04:00
flan 561a1a3d72 fix: 5 findings from codebase audit round 5
cli.py:
- _smaller_duplicate_ids: walrus operator eliminates double p.get("id")
  per element; truthiness check replaces dead "is not None" guard (all
  persons in by_name are guaranteed to have a truthy id after the
  line-75 gate)
- Extract _excl() helper inside _handle_duplicate_people — replaces 4
  identical [p for p in lst if p.get("id") not in skip_ids] expressions
  across all return paths

jobs.py:
- Extract _valid_people() — shared filter for interactive_configure and
  auto_configure; uses (p.get("name") or "").strip() to match cli.py's
  whitespace-strip gate, preventing whitespace-only Immich names from
  reaching _build_job and creating blank Frigate person labels
- Hoist queued_ids set before the display loop in interactive_configure:
  O(N) set lookup per render instead of O(N×|jobs|) linear scan
2026-06-17 02:28:58 +00:00
flan 614542decd fix: 4 findings from codebase audit round 4
jobs.py:
- Add p.get("id") guard to valid_people filter in both
  interactive_configure and auto_configure — id-less named persons
  passed through by _handle_duplicate_people are now excluded before
  any bare-subscript access in the configure paths
- Fix bare p["id"] → p.get("id") in the queued-marker check at line 214
  (runs unconditionally on all valid_people during menu display, before
  any user selection or fetch_all_assets guard)

cli.py:
- Remove dead-code survivor_id and merge_ids guards: after the by_name
  fix (line 75 requires p.get("id")), all persons in any ordered list
  have ids, so neither guard can ever fire; removing them prevents
  misleading readers about what states are reachable
2026-06-17 02:20:38 +00:00
flan 3fccf9c8f9 fix: 6 findings from codebase audit round 3
cli.py:
- Filter id-less persons from by_name at construction (root fix for all
  bare-subscript crashes downstream — persons with a name but no id are
  excluded from duplicate detection entirely)
- Belt-and-suspenders on warning-path display: p['id'] → p.get('id')
- Extract survivor_id with .get(); skip group if survivor has no id
- Guard merge_ids: skip API call when list is empty after id filtering
- Walrus operator in merge_ids comprehension: p.get("id") called once
  per item instead of twice

executor.py:
- Add cross-reference comment at success-path reset so the for/else
  rollback pairing is explicit for future maintainers
2026-06-17 02:09:16 +00:00
flan eab3d9fe64 fix: 3 correctness bugs from codebase audit round 2
- executor.py: clear min_quality_score_for_slot alongside effective_count
  restore in for/else block; leaving the stale floor from the deleted
  file's score blocked the next candidate from filling the restored slot
- cli.py: guard merge_ids with p.get('id') is not None, consistent with
  the _smaller_duplicate_ids fix; bare p['id'] raised KeyError on any
  person dict missing the id field in the auto-merge path
- immich_api.py: replace bare data['major'/'minor'/'patch'] subscripts
  with .get() in get_immich_version; KeyError was silently swallowed by
  except Exception, causing version-gated flags to disable without warning
2026-06-17 01:54:13 +00:00
flan 5509be150e Merge pull request #38 from sudolulo/fix/codebase-audit-r1
fix: codebase audit r1 — correctness fixes, version banner, GPU dep
2026-06-16 21:51:09 -04:00
flan 0bd2eaf9fb fix: add missing nvidia CUDA pip packages for onnxruntime-gpu 1.26.0
ORT 1.26.0 changed provider loading to gate on the presence of required
nvidia pip packages before attempting to load libonnxruntime_providers_cuda.so.
Without nvidia-cuda-runtime-cu12, nvidia-cufft-cu12, and nvidia-curand-cu12
installed as Python packages, ORT silently skips the CUDA EP plugin entirely
(confirmed via /proc/maps: the .so was never dlopen'd despite existing on disk
and all system CUDA libs being present in ldconfig).

nvidia-nvjitlink-cu12 pulled in as a transitive dependency.
2026-06-17 01:47:49 +00:00
flan b80d26b36b feat: display version in startup banner 2026-06-17 01:18:45 +00:00
flan 068a8e675f fix: 4 correctness bugs from full-codebase audit
- executor: restore effective_count when replacement upload fails all retries
  (delete succeeded but slot was never filled, leaving cap undercount)
- diversity: skip zero-norm embeddings before dedup/FPS selection
  (InsightFace zeros pass dedup with similarity 0 and score distance 1.0,
  getting selected first as maximally diverse)
- cli: exclude None from skip_ids in _smaller_duplicate_ids
  (p.get('id') without None guard lets None into the set, silently
  dropping every other id-less person from the processed list)
- embeddings: select face nearest crop centre instead of largest by area
  (25% margin can pull a bigger neighbouring face into the crop;
  largest-face selection then embeds the wrong person)
2026-06-17 01:12:28 +00:00
flan c36e7bf28e Merge pull request #37 from sudolulo/dev
fix: exclude main from lockfile update workflow trigger
2026-06-16 21:03:35 -04:00
flan 0ffe08bc6f Merge pull request #36 from sudolulo/fix/lockfile-workflow-main-exclusion
fix: exclude main from lockfile update workflow trigger
2026-06-16 21:01:25 -04:00
flan 3423d41535 fix: exclude main from lockfile update trigger
main is protected and only receives merges from dev; pushing directly
to it from CI is blocked by branch protection rules.
2026-06-17 00:56:52 +00:00
flan 6e29407231 Merge pull request #35 from sudolulo/dev
release: v0.6.4
v0.6.4
2026-06-16 20:21:19 -04:00
flan 5dcfde7c36 release: v0.6.4 2026-06-17 00:17:32 +00:00
flan 0914608bc8 fix: address 2 missed p[\"id\"] bare subscripts in cli.py (round 12)
Round 11's replace_all missed two occurrences:
- _smaller_duplicate_ids inner comprehension (line 84): p["id"] →
  p.get("id") so a named person with a missing "id" field does not
  crash skip_ids computation before any return path is reached
- all-merges-failed fallback return (line 157): same fix; the outer
  indentation prevented replace_all from matching this occurrence

The intentional p["id"] in merge_ids (line 119) is kept: that ID is
passed directly to merge_people() where None would be a caller bug,
not a silent data corruption.
2026-06-17 00:10:01 +00:00
flan 2182c87c40 fix: address 2 code review findings (round 11)
- upload_tracker: revert data[flat_key] = [] from round 8; clearing the
  entire shared legacy flat list on a corrupt value wipes all persons'
  IDs, not just the one being reset; since a corrupt non-list value is
  already unreadable by load_uploaded_ids, leaving it in place is safer
  than a mass-wipe; update warning message to note the field is unaffected
  but unreadable so the corruption is still observable
- cli: use p.get("id") instead of p["id"] in both people-list fallback
  returns (_handle_duplicate_people lines 144 and 157) for consistency
  with the success path at line 149; bare subscript crashes on malformed
  unnamed persons that bypass _smaller_duplicate_ids
2026-06-17 00:02:49 +00:00
flan 0236ed2d6b fix: address 3 code review findings (round 10)
- immich_api: use 'or []' instead of .get("people", []) in get_people
  so {"people": null} responses (some Immich versions with zero people
  enrolled) return [] rather than None; .get() default only fires when
  the key is absent, not when its value is null
- embeddings: log OSError from os.dup2 restore at DEBUG rather than
  silently swallowing it; if a C extension (CUDA/onnxruntime) invalidates
  the saved fd, the restore fails silently and stdout stays wired to
  /dev/null — logging makes the event observable without changing the
  swallow-and-continue semantics
- cache: remove MemoryError re-raise from EmbeddingCache.get(); a cache
  read OOM aborted the entire diversity-selection batch for the person
  rather than falling back to a fresh embedding computation, which is
  the more appropriate OOM gate; broadening back to except Exception
  restores the pre-round-5 fallback behavior
2026-06-16 23:51:21 +00:00
flan 34f7985357 docs: document BaseException limitation in _suppress_output finally block
A KeyboardInterrupt raised inside the saved_out cleanup block would
propagate past the saved_err and devnull_fd blocks, leaking those fds.
In CPython this race is not realistically triggerable — KI is delivered
between bytecodes and os.dup2 is a single atomic C syscall — so we
accept the theoretical risk rather than silencing BaseException in a
finally block.
2026-06-16 23:41:38 +00:00
flan 25880ded91 fix: address 2 code review findings (round 9)
- executor: revert person_has_fscores=True back into try/except else
  branch; moving it outside in round 8 was a regression — when the
  tracker write fails on the first-ever upload (no prior frigate_scores
  in tracker), setting the flag True prematurely switches at-cap
  replacement into fscore mode, get_most_redundant_mapped_file returns
  None (no entries), and all replacements are silently skipped;
  the flag must only be set when the score is actually written
- diversity: remove dead face_crop-None guard; any face that passes
  assess_quality (≥90 px MIN_FACE_WIDTH) produces a crop ≥135 px
  (face + 25% margin), which is always above the 30 px crop minimum,
  making the guard unreachable; _crop_face_from_thumbnail also calls
  _get_face_bbox internally, so face_bbox is not None guarantees the
  inner bbox check also passes
2026-06-16 23:40:05 +00:00
flan 461ceb7af4 fix: address 5 code review findings (round 8)
- diversity: fix hard_count regression from round 7 — revert to
  'is not None and < 0.85' so only images that actually receive a
  FPS boost (confirmed low confidence) are counted as hard examples;
  None-confidence images use conf_array=1.0 (no boost) and should
  not appear in the hard-example log count
- diversity: fix _scale_bbox_to_thumbnail to use explicit zero-guard
  for imageWidth/imageHeight (meta_w or 0; scale = img_w/meta_w if
  meta_w else 1.0) — mirrors image_processing.py pattern; prevents
  `or img_w` from silently treating imageWidth=0 as missing and
  returning scale=1.0 without surfacing the zero-metadata case
- embeddings: wrap all three os.close calls in _suppress_output
  finally block with try/except OSError: pass so a failed close
  in one branch cannot abort the outer finally and leak devnull_fd
  or the saved_err/saved_out fds
- upload_tracker: clear corrupt flat-list key (data[flat_key] = [])
  after the isinstance warning instead of leaving the corrupt value
  in place — prevents stale IDs persisting across reset_person calls
  and future load_uploaded_ids() from seeing a non-list value
- executor: move 'if pre_fscore is not None: person_has_fscores = True'
  out of the try/except else branch so it fires even when mark_uploaded
  raises; Frigate scores exist once measured regardless of tracker
  write success, and replacement strategy should reflect that
2026-06-16 23:24:11 +00:00