Add quality replacement for Frigate face training images
When a person is at MAX_AUTO_IMAGES, winnow now replaces the lowest-quality mapped training image in Frigate if a higher-confidence candidate is available, keeping the training set always optimised. Only files winnow uploaded (tracked via frigate_files mapping) are ever replaced — manually added Frigate training images are never touched. A concurrent-upload race condition is detected per-file: if N>1 new files appear after one upload, the mapping is skipped rather than guessed, logging at INFO level. The per-file snapshot approach is retained over a batch approach because wrong mappings (which a batch approach risks on race) are worse than no mapping. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,3 +69,73 @@ def test_duplicate_marks_are_idempotent():
|
||||
mark_uploaded("dup", person_name="Alice")
|
||||
mark_uploaded("dup", person_name="Alice")
|
||||
assert filter_already_uploaded(["dup", "new"]) == ["new"]
|
||||
|
||||
|
||||
# ── frigate_files mapping ─────────────────────────────────────────────────────
|
||||
|
||||
def test_record_and_remove_frigate_file():
|
||||
from winnow.upload_tracker import get_person_summary, record_frigate_file, remove_frigate_file
|
||||
record_frigate_file("Alice", "Alice-1000.webp", "asset-a1")
|
||||
assert "Alice-1000.webp" in get_person_summary()["Alice"]["frigate_files"]
|
||||
remove_frigate_file("Alice", "Alice-1000.webp")
|
||||
assert "Alice-1000.webp" not in get_person_summary()["Alice"]["frigate_files"]
|
||||
|
||||
|
||||
def test_remove_nonexistent_frigate_file_is_safe():
|
||||
from winnow.upload_tracker import remove_frigate_file
|
||||
# Should not raise even if the file was never recorded
|
||||
remove_frigate_file("Alice", "Alice-ghost.webp")
|
||||
|
||||
|
||||
def test_remove_frigate_file_does_not_unmark_asset():
|
||||
"""Deleting a Frigate file should not re-expose the source asset for upload."""
|
||||
from winnow.upload_tracker import (
|
||||
filter_already_uploaded,
|
||||
mark_uploaded,
|
||||
record_frigate_file,
|
||||
remove_frigate_file,
|
||||
)
|
||||
mark_uploaded("asset-a1", person_name="Alice")
|
||||
record_frigate_file("Alice", "Alice-1000.webp", "asset-a1")
|
||||
remove_frigate_file("Alice", "Alice-1000.webp")
|
||||
# Asset must still be excluded — it was deliberately replaced, not lost
|
||||
assert filter_already_uploaded(["asset-a1"]) == []
|
||||
|
||||
|
||||
def test_get_lowest_quality_mapped_file_none_when_empty():
|
||||
from winnow.upload_tracker import get_lowest_quality_mapped_file
|
||||
assert get_lowest_quality_mapped_file("Alice") is None
|
||||
|
||||
|
||||
def test_get_lowest_quality_mapped_file_returns_lowest():
|
||||
from winnow.upload_tracker import (
|
||||
get_lowest_quality_mapped_file,
|
||||
mark_uploaded,
|
||||
record_frigate_file,
|
||||
)
|
||||
mark_uploaded("asset-hi", person_name="Alice", score=0.95)
|
||||
mark_uploaded("asset-lo", person_name="Alice", score=0.71)
|
||||
record_frigate_file("Alice", "Alice-1000.webp", "asset-hi")
|
||||
record_frigate_file("Alice", "Alice-1001.webp", "asset-lo")
|
||||
result = get_lowest_quality_mapped_file("Alice")
|
||||
assert result is not None
|
||||
frigate_filename, asset_id, score = result
|
||||
assert frigate_filename == "Alice-1001.webp"
|
||||
assert asset_id == "asset-lo"
|
||||
assert score == pytest.approx(0.71, abs=0.001)
|
||||
|
||||
|
||||
def test_get_lowest_quality_mapped_file_skips_unscored():
|
||||
"""Files mapped without a score should not be returned as candidates."""
|
||||
from winnow.upload_tracker import (
|
||||
get_lowest_quality_mapped_file,
|
||||
mark_uploaded,
|
||||
record_frigate_file,
|
||||
)
|
||||
mark_uploaded("asset-scored", person_name="Alice", score=0.85)
|
||||
mark_uploaded("asset-noscr", person_name="Alice")
|
||||
record_frigate_file("Alice", "Alice-1000.webp", "asset-scored")
|
||||
record_frigate_file("Alice", "Alice-1001.webp", "asset-noscr")
|
||||
result = get_lowest_quality_mapped_file("Alice")
|
||||
assert result is not None
|
||||
assert result[1] == "asset-scored" # only scored file is a candidate
|
||||
|
||||
Reference in New Issue
Block a user