From f891e579c5f1286aa11a5612c741dfb9db808865 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Tue, 4 Aug 2026 14:04:21 +0200 Subject: [PATCH] chore(traces): put TRACES tags on their own line; regenerate the report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parser reads a tag up to end of line, so `# TRACES: GR-004 | SR-001 — prose` swallowed the prose into the tag and the row went unmatched. Splitting the comment leaves the tag greppable by the same pattern as the code tags and the commit trailers, which is the point of the house format. Mechanical throughout; no logic touched. The regenerated report reflects this session's new tags: 137 -> 148 found, and one more tagged-but-unexecuted, which is the SuperHero accuracy assertion that is documented but not yet a test. --- docs/traceability.md | 124 ++++++++++++++-------- experiments/xsource/landmark_voting.py | 72 +++++++++++-- experiments/xsource/resolution_sweep.py | 2 + experiments/xsource/verify_labels.py | 8 ++ scripts/filter_gallery.py | 3 +- scripts/make_gallery.py | 3 +- scripts/make_jellyfin_gallery.py | 3 +- scripts/movienet_eval.py | 3 +- scripts/optimizer/fetch_missing_actors.py | 6 +- scripts/optimizer/optimize.py | 3 +- scripts/optimizer/reembed_gallery.py | 3 +- scripts/optimizer/replay.py | 6 +- scripts/sae_embed_loader.py | 21 +++- scripts/sae_gallery.py | 6 +- scripts/validation/ground_truth.py | 5 + 15 files changed, 198 insertions(+), 70 deletions(-) diff --git a/docs/traceability.md b/docs/traceability.md index 33ce289..1497794 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -3,7 +3,7 @@ -**Generated:** 2026-07-31T15:01:49+00:00 +**Generated:** 2026-07-31T20:39:35+00:00 Denominators are read from [`requirements.md`](requirements.md) at run time, never hardcoded. Coverage counts a requirement only when it is tagged in source **and** has a verification tier this repo's CI host can execute (`T1, T2, T3, static`). @@ -12,13 +12,13 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev | Metric | Value | |---|---| | Source files scanned | 112 | -| TRACES tags found | 137 | +| TRACES tags found | 148 | | EXCEPTION tags found | 0 | | Requirements defined | 69 | | Requirements covered | 38 | | **Coverage** | **55.1%** (38/69) | | Coverage of CI-executable scope | 67.9% (38/56) | -| Tagged but unexecuted in CI | 7 | +| Tagged but unexecuted in CI | 8 | | Orphan tags | 0 | ### By type @@ -29,7 +29,7 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev | DP | 2 | 0 | 8 | | IR | 8 | 0 | 8 | | GR | 5 | 0 | 9 | -| VR | 1 | 6 | 14 | +| VR | 1 | 7 | 14 | - **UT** tags present (separate taxonomy, not counted in coverage): UT-001, UT-101, UT-102, UT-103, UT-104, UT-105, UT-106, UT-107, UT-108 - **IT** tags present (separate taxonomy, not counted in coverage): IT-001 @@ -54,9 +54,9 @@ These requirements have no verification tier this repo's CI host can run, so a t | VR-010 | out-of-ci | yes | Dump provenance attributes — embedder model, detector settings, `dens… | | VR-011 | out-of-ci | no | Rewrite the replay harness for the post-AR-012 output contract | | VR-012 | T4, out-of-ci | no | Quality-knee study — TPI/FPI vs sharpness and vs pose, as VR-005 did … | -| VR-013 | T4, out-of-ci | no | Cross-source identification probe — gallery from one recording, probe… | +| VR-013 | T4, out-of-ci | yes | Cross-source identification probe — gallery from one recording, probe… | -**Tagged but unexecuted:** AR-027, VR-001, VR-002, VR-003, VR-004, VR-005, VR-010 — a test exists and is tagged, but this CI host cannot run it. Report those runs separately. +**Tagged but unexecuted:** AR-027, VR-001, VR-002, VR-003, VR-004, VR-005, VR-010, VR-013 — a test exists and is tagged, but this CI host cannot run it. Report those runs separately. ## Orphan tags @@ -84,10 +84,10 @@ _None._ | AR-002 | Planned | T2 | SR-002 | untagged | - | Minimum face size **40×40 px** (VR-013 measured end to end; VR-005's … | | AR-003 | **Done** — `max_fac… | T1, T2, T4 | SR-002 | covered | `src/config.hpp`, `src/nodes/face_detector_node.hpp`, `src/nodes/identity_matcher_node.hpp` | No fixed per-frame face cap — crowd scenes must not lose background c… | | AR-004 | **Done** — KPN node… | T1, T4 | SR-002 | covered | `src/main.cpp`, `src/nodes/identity_matcher_node.hpp`, `tests/test_replay_fixtures.cpp` | Backpressure: unbounded faces/frame absorbed by slowing, never by dro… | -| AR-005 | **Done** — `umeyama… | T1, T3 | SR-002 | covered | `src/face_utils.hpp`, `tests/test_face_utils.cpp` | Align to 112×112 via ArcFace 5-point similarity transform, fitted by … | +| AR-005 | **Done** — `umeyama… | T1, T3 | SR-002 | covered | `src/face_utils.hpp`, `src/nodes/face_aligner_node.hpp`, `tests/test_face_utils.cpp` | Align to 112×112 via ArcFace 5-point similarity transform, fitted by … | | AR-006 | Done | T3 | SR-002 | covered | `src/nodes/embedder_node.hpp` | 512-d L2-normalised embeddings, batched | -| AR-007 | **Done** — `track_a… | T2 | SR-002 | covered | `src/config.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp` | Associate detections by IoU + embedding, with **frame-dependent** wei… | -| AR-008 | **Done** — one pool… | T2 | SR-002 | covered | `src/config.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp` | One track pool keyed on `last_seen`; no separate revival path | +| AR-007 | **Done** — `track_a… | T2 | SR-002 | covered | `src/config.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp`, `tests/test_face_tracker.cpp` | Associate detections by IoU + embedding, with **frame-dependent** wei… | +| AR-008 | **Done** — one pool… | T2 | SR-002 | covered | `src/config.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp`, `tests/test_face_tracker.cpp` | One track pool keyed on `last_seen`; no separate revival path | | AR-009 | Done | T2 | SR-002 | covered | `src/nodes/camera_position_change_detector_node.hpp` | Camera-cut detection (histogram) as an association hint | | AR-010 | **Done** — decode b… | T2 | SR-002 | covered | `src/main.cpp`, `src/nodes/scene_boundary_annotator_node.hpp`, `src/nodes/scene_detector_node.hpp`, `src/scene_boundaries.hpp` | Scene-boundary detection (TransNetV2) as an association hint | | AR-011 | Planned | T1, T2 | SR-002 | untagged | - | **Every model is fed the input it was trained for** — cost reduced by… | @@ -97,19 +97,19 @@ _None._ | AR-015 | **Done** — reverse … | T2 | SR-002 | covered | `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Two live tracks owned by one actor ⇒ treat as a detected cut, re-asso… | | AR-016 | **Done** — `flush()… | T2 | SR-002 | covered | `src/main.cpp`, `src/nodes/result_sink_node.hpp`, `src/track_registry.hpp`, `tests/test_track_registry.cpp` | All tracks closed at EOF — a film ends with faces on screen | | AR-017 | **Done** — `DeadTra… | T1, T2 | SR-002 | covered | `src/nodes/result_sink_node.hpp`, `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Every presence claim carries its belief and identification route | -| AR-018 | **Done** — banded a… | T1, T2 | SR-005 | covered | `src/config.hpp`, `src/gallery/track_gallery.hpp`, `src/nodes/identity_matcher_node.hpp` | Per-subject embedding store with banded admission (novel enough, safe… | -| AR-019 | **Done** — all thre… | T2 | SR-005 | covered | `src/gallery/track_gallery.hpp`, `src/nodes/identity_matcher_node.hpp` | Per-film gallery annex from owned tracks; acquires the non-frontal vi… | +| AR-018 | **Done** — banded a… | T1, T2 | SR-005 | covered | `src/config.hpp`, `src/gallery/track_gallery.hpp`, `src/nodes/identity_matcher_node.hpp`, `tests/test_track_gallery.cpp` | Per-subject embedding store with banded admission (novel enough, safe… | +| AR-019 | **Done** — all thre… | T2 | SR-005 | covered | `src/gallery/track_gallery.hpp`, `src/nodes/identity_matcher_node.hpp`, `tests/test_track_gallery.cpp` | Per-film gallery annex from owned tracks; acquires the non-frontal vi… | | AR-020 | Planned | T2 | SR-005 | untagged | - | Deferred re-identification of unknown tracks against the final expand… | | AR-021 | Planned | T2 | SR-005 | untagged | - | Cluster unknown tracks into one entity per person, under temporal can… | | AR-022 | Planned | T1, T2 | §4 | untagged | - | Capture still-unidentified tracks: embeddings, metadata, **context cr… | -| AR-023 | Done | T1 | SR-002 | covered | `src/gallery/gallery_calibration.hpp`, `src/nodes/identity_matcher_node.hpp` | Fit sigmoid calibration from intra/inter similarity distributions | -| AR-024 | **Done** — associat… | T1, static | SR-002 | covered | `src/config.hpp`, `src/evidence_discount.hpp`, `src/gallery/gallery_calibration.hpp`, `src/gallery/track_gallery.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp`, `src/nodes/identity_matcher_node.hpp` | **Always the calibrated probability, never a raw cosine** — exception… | +| AR-023 | Done | T1 | SR-002 | covered | `src/gallery/gallery_calibration.hpp`, `src/nodes/identity_matcher_node.hpp`, `tests/test_calibration.cpp` | Fit sigmoid calibration from intra/inter similarity distributions | +| AR-024 | **Done** — associat… | T1, static | SR-002 | covered | `src/config.hpp`, `src/evidence_discount.hpp`, `src/gallery/gallery_calibration.hpp`, `src/gallery/track_gallery.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp`, `src/nodes/identity_matcher_node.hpp`, `tests/test_track_gallery.cpp` | **Always the calibrated probability, never a raw cosine** — exception… | | AR-025 | **Done** — log-odds… | T1 | SR-002 | covered | `src/evidence_discount.hpp`, `src/nodes/identity_matcher_node.hpp` | Per-track Bayesian accumulation in log-odds, with correlated-observat… | -| AR-026 | In Progress | T1, T4 | SR-001 | covered | `src/backends/gemm_backend.cpp` | All similarity computed as GEMM, including annex and deferred pass | +| AR-026 | In Progress | T1, T4 | SR-001 | covered | `src/backends/gemm_backend.cpp`, `tests/test_similarity.cpp` | All similarity computed as GEMM, including annex and deferred pass | | AR-027 | Planned | T4 | SR-001 | tagged, unexecuted | `src/backends/gemm_backend.cpp` | Throughput acceptable for **arbitrary** gallery size | | AR-028 | Planned | T2 | SR-002 | untagged | - | **Embedding input quality assessed and carried** — every face scored … | | AR-029 | Planned | T1 | SR-002 | untagged | - | Sharpness measure on the **aligned crop** (scale-normalised, so it ca… | -| AR-030 | **In Progress** — m… | T1 | SR-002 | covered | `src/face_utils.hpp`, `tests/test_face_utils.cpp` | Visibility measure from the AR-001 5-point landmarks — extreme pose o… | +| AR-030 | **In Progress** — m… | T1 | SR-002 | covered | `src/face_utils.hpp`, `src/nodes/face_aligner_node.hpp`, `tests/test_face_utils.cpp` | Visibility measure from the AR-001 5-point landmarks — extreme pose o… | | DP-001 | Done | T1, manual | PR-004 | covered | `src/main.cpp` | One analysis core; modes are front-ends and must not fork pipeline lo… | | DP-002 | Done | T1, manual | PR-004 | covered | `src/main.cpp` | Batch CLI over one title | | DP-003 | Planned | T1, manual | PR-004 | untagged | - | On-demand resident service with bounded, observable queue | @@ -128,7 +128,7 @@ _None._ | IR-008 | **Done** | T1 | SR-003 | covered | `src/audio_signature.cpp`, `src/audio_signature.hpp`, `tests/test_audio_signature.cpp` | Emit and honour the signature's own `v1:` version prefix | | GR-001 | Done | T1, T3 | SR-001, SR-005 | covered | `scripts/make_jellyfin_gallery.py` | Build gallery from Jellyfin library cast, TMDB profile fallback | | GR-002 | Done | T1, T3 | PR-003 | covered | `scripts/make_jellyfin_gallery.py` | Incremental `--merge` refresh without re-embedding known actors | -| GR-003 | Planned | T1, T3 | SR-001 | covered | `src/build_gallery.cpp`, `src/gallery/gallery_calibration.hpp`, `src/gallery/gallery_report.hpp` | Report coverage: zero-image actors, under-referenced actors, dedup, c… | +| GR-003 | **Done** — `gallery… | T1, T3 | SR-001 | covered | `src/build_gallery.cpp`, `src/gallery/gallery_calibration.hpp`, `src/gallery/gallery_report.hpp`, `tests/test_calibration.cpp` | Report coverage: zero-image actors, under-referenced actors, dedup, c… | | GR-004 | **Done** — basename… | T1, T3 | SR-001 | covered | `scripts/filter_gallery.py`, `scripts/make_gallery.py`, `scripts/make_jellyfin_gallery.py`, `scripts/movienet_eval.py`, `scripts/optimizer/fetch_missing_actors.py`, `scripts/optimizer/optimize.py`, `scripts/optimizer/reembed_gallery.py`, `scripts/optimizer/replay.py`, `scripts/sae_embed_loader.py`, `scripts/sae_gallery.py`, `scripts/sae_stamp.py`, `scripts/stamp_gallery.py`, `src/config.hpp`, `src/gallery/embedder_stamp.cpp`, `src/gallery/embedder_stamp.hpp`, `src/gallery/gallery_builder.cpp`, `src/gallery/gallery_store.cpp`, `src/kpn_bindings.cpp`, `src/main.cpp`, `src/nodes/embedding_dump_node.hpp`, `src/scene_preview.cpp`, `src/types.hpp`, `tests/test_gallery_store.cpp` | Stamp embedder identity into the gallery; **hard startup error** on m… | | GR-005 | Done | T1, T3 | **SR-005** | covered | `src/gallery/gallery_store.hpp` | Gallery data never leaves the instance | | GR-006 | Planned | T1 | SR-005 | untagged | - | Provenance tiers: baked / harvested / confirmed, distinguishable per … | @@ -147,7 +147,7 @@ _None._ | VR-010 | Planned | out-of-ci | PR-002 | tagged, unexecuted | `src/nodes/embedding_dump_node.hpp` | Dump provenance attributes — embedder model, detector settings, `dens… | | VR-011 | Planned | out-of-ci | PR-002 | untagged | - | Rewrite the replay harness for the post-AR-012 output contract | | VR-012 | Planned | T4, out-of-ci | PR-002 | untagged | - | Quality-knee study — TPI/FPI vs sharpness and vs pose, as VR-005 did … | -| VR-013 | **In Progress** — h… | T4, out-of-ci | PR-002 | untagged | - | Cross-source identification probe — gallery from one recording, probe… | +| VR-013 | **In Progress** — h… | T4, out-of-ci | PR-002 | tagged, unexecuted | `experiments/xsource/resolution_sweep.py`, `experiments/xsource/verify_labels.py` | Cross-source identification probe — gallery from one recording, probe… | | VR-014 | **Done** — 40 rando… | T2, out-of-ci | PR-002 | covered | `scripts/validation/test_audio_offset.py` | Audio-signature **offset recovery on real content** — a known trim re… | ## Detailed mapping @@ -171,15 +171,16 @@ _None._ **Locations:** 4 - [`src/main.cpp:86`](../src/main.cpp#L86) — `static constexpr std::size_t kSceneJoinDepth = 256;` -- [`src/main.cpp:299`](../src/main.cpp#L299) — `Unknown` +- [`src/main.cpp:319`](../src/main.cpp#L319) — `Unknown` - [`src/nodes/identity_matcher_node.hpp:166`](../src/nodes/identity_matcher_node.hpp#L166) — `std::vector host_query(static_cast(kMaxFaces) * 512);` - [`tests/test_replay_fixtures.cpp:3`](../tests/test_replay_fixtures.cpp#L3) — `Unknown` ### AR-005 -**Locations:** 2 +**Locations:** 3 - [`src/face_utils.hpp:2`](../src/face_utils.hpp#L2) — `Unknown` +- [`src/nodes/face_aligner_node.hpp:7`](../src/nodes/face_aligner_node.hpp#L7) — `struct FaceAlignerFunc` - [`tests/test_face_utils.cpp:1`](../tests/test_face_utils.cpp#L1) — `Unknown` ### AR-006 @@ -190,19 +191,21 @@ _None._ ### AR-007 -**Locations:** 3 +**Locations:** 4 - [`src/config.hpp:108`](../src/config.hpp#L108) — `Unknown` - [`src/main.cpp:213`](../src/main.cpp#L213) — `reg_cfg, EvidenceDiscounter(same_person));` - [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown` +- [`tests/test_face_tracker.cpp:1`](../tests/test_face_tracker.cpp#L1) — `Unknown` ### AR-008 -**Locations:** 3 +**Locations:** 4 - [`src/config.hpp:108`](../src/config.hpp#L108) — `Unknown` - [`src/main.cpp:213`](../src/main.cpp#L213) — `reg_cfg, EvidenceDiscounter(same_person));` - [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown` +- [`tests/test_face_tracker.cpp:1`](../tests/test_face_tracker.cpp#L1) — `Unknown` ### AR-009 @@ -215,9 +218,9 @@ _None._ **Locations:** 9 - [`src/main.cpp:86`](../src/main.cpp#L86) — `static constexpr std::size_t kSceneJoinDepth = 256;` -- [`src/main.cpp:309`](../src/main.cpp#L309) — `Unknown` -- [`src/main.cpp:379`](../src/main.cpp#L379) — `return run_net(std::move(net));` -- [`src/main.cpp:411`](../src/main.cpp#L411) — `Unknown` +- [`src/main.cpp:329`](../src/main.cpp#L329) — `Unknown` +- [`src/main.cpp:399`](../src/main.cpp#L399) — `return run_net(std::move(net));` +- [`src/main.cpp:431`](../src/main.cpp#L431) — `Unknown` - [`src/nodes/scene_boundary_annotator_node.hpp:2`](../src/nodes/scene_boundary_annotator_node.hpp#L2) — `Unknown` - [`src/nodes/scene_detector_node.hpp:36`](../src/nodes/scene_detector_node.hpp#L36) — `static constexpr std::string_view label() { return "scene_detector"; }` - [`src/nodes/scene_detector_node.hpp:110`](../src/nodes/scene_detector_node.hpp#L110) — `void flush_remaining()` @@ -279,42 +282,48 @@ _None._ ### AR-018 -**Locations:** 3 +**Locations:** 5 - [`src/config.hpp:152`](../src/config.hpp#L152) — `Unknown` -- [`src/gallery/track_gallery.hpp:161`](../src/gallery/track_gallery.hpp#L161) — `struct TrackState` +- [`src/gallery/track_gallery.hpp:164`](../src/gallery/track_gallery.hpp#L164) — `struct TrackState` +- [`src/gallery/track_gallery.hpp:274`](../src/gallery/track_gallery.hpp#L274) — `static int plurality_actor(const TrackState& ts)` - [`src/nodes/identity_matcher_node.hpp:110`](../src/nodes/identity_matcher_node.hpp#L110) — `std::vector host_gallery(static_cast(n_gallery_) * 512);` +- [`tests/test_track_gallery.cpp:1`](../tests/test_track_gallery.cpp#L1) — `Unknown` ### AR-019 -**Locations:** 3 +**Locations:** 4 -- [`src/gallery/track_gallery.hpp:122`](../src/gallery/track_gallery.hpp#L122) — `void forget(int track_id) { tracks_.erase(track_id); }` +- [`src/gallery/track_gallery.hpp:123`](../src/gallery/track_gallery.hpp#L123) — `void forget(int track_id) { tracks_.erase(track_id); }` - [`src/nodes/identity_matcher_node.hpp:147`](../src/nodes/identity_matcher_node.hpp#L147) — `MatchedSceneFrame operator()(TrackedSceneFrame tf)` - [`src/nodes/identity_matcher_node.hpp:285`](../src/nodes/identity_matcher_node.hpp#L285) — `Unknown` +- [`tests/test_track_gallery.cpp:1`](../tests/test_track_gallery.cpp#L1) — `Unknown` ### AR-023 -**Locations:** 3 +**Locations:** 4 - [`src/gallery/gallery_calibration.hpp:2`](../src/gallery/gallery_calibration.hpp#L2) — `Unknown` - [`src/gallery/gallery_calibration.hpp:53`](../src/gallery/gallery_calibration.hpp#L53) — `float boundary_at(float p = 0.5f, float log_prior_odds = 0.f) const` - [`src/nodes/identity_matcher_node.hpp:117`](../src/nodes/identity_matcher_node.hpp#L117) — `const GalleryCalibration& calibration() const { return cal_; }` +- [`tests/test_calibration.cpp:1`](../tests/test_calibration.cpp#L1) — `Unknown` ### AR-024 -**Locations:** 10 +**Locations:** 12 - [`src/config.hpp:108`](../src/config.hpp#L108) — `Unknown` - [`src/config.hpp:152`](../src/config.hpp#L152) — `Unknown` - [`src/evidence_discount.hpp:2`](../src/evidence_discount.hpp#L2) — `Unknown` - [`src/gallery/gallery_calibration.hpp:53`](../src/gallery/gallery_calibration.hpp#L53) — `float boundary_at(float p = 0.5f, float log_prior_odds = 0.f) const` -- [`src/gallery/track_gallery.hpp:132`](../src/gallery/track_gallery.hpp#L132) — `void set_calibration(std::function c) { calibrate_ = std::move(c); }` -- [`src/gallery/track_gallery.hpp:161`](../src/gallery/track_gallery.hpp#L161) — `struct TrackState` +- [`src/gallery/track_gallery.hpp:133`](../src/gallery/track_gallery.hpp#L133) — `void set_calibration(std::function c) { calibrate_ = std::move(c); }` +- [`src/gallery/track_gallery.hpp:164`](../src/gallery/track_gallery.hpp#L164) — `struct TrackState` +- [`src/gallery/track_gallery.hpp:274`](../src/gallery/track_gallery.hpp#L274) — `static int plurality_actor(const TrackState& ts)` - [`src/main.cpp:213`](../src/main.cpp#L213) — `reg_cfg, EvidenceDiscounter(same_person));` - [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown` - [`src/nodes/identity_matcher_node.hpp:110`](../src/nodes/identity_matcher_node.hpp#L110) — `std::vector host_gallery(static_cast(n_gallery_) * 512);` - [`src/nodes/identity_matcher_node.hpp:117`](../src/nodes/identity_matcher_node.hpp#L117) — `const GalleryCalibration& calibration() const { return cal_; }` +- [`tests/test_track_gallery.cpp:1`](../tests/test_track_gallery.cpp#L1) — `Unknown` ### AR-025 @@ -326,9 +335,10 @@ _None._ ### AR-026 -**Locations:** 1 +**Locations:** 2 - [`src/backends/gemm_backend.cpp:44`](../src/backends/gemm_backend.cpp#L44) — `constexpr int kDim = 512;` +- [`tests/test_similarity.cpp:1`](../tests/test_similarity.cpp#L1) — `Unknown` ### AR-027 @@ -338,9 +348,10 @@ _None._ ### AR-030 -**Locations:** 2 +**Locations:** 3 - [`src/face_utils.hpp:2`](../src/face_utils.hpp#L2) — `Unknown` +- [`src/nodes/face_aligner_node.hpp:7`](../src/nodes/face_aligner_node.hpp#L7) — `struct FaceAlignerFunc` - [`tests/test_face_utils.cpp:1`](../tests/test_face_utils.cpp#L1) — `Unknown` ### DP-001 @@ -369,7 +380,7 @@ _None._ ### GR-003 -**Locations:** 13 +**Locations:** 16 - [`src/build_gallery.cpp:83`](../src/build_gallery.cpp#L83) — `Unknown` - [`src/gallery/gallery_calibration.hpp:80`](../src/gallery/gallery_calibration.hpp#L80) — `struct GalleryCalibrationStats` @@ -384,6 +395,9 @@ _None._ - [`src/gallery/gallery_report.hpp:446`](../src/gallery/gallery_report.hpp#L446) — `inline void save_gallery_report(const std::string& path, const GalleryReport& r)` - [`src/gallery/gallery_report.hpp:454`](../src/gallery/gallery_report.hpp#L454) — `inline GalleryReport load_gallery_report(const std::string& path)` - [`src/gallery/gallery_report.hpp:464`](../src/gallery/gallery_report.hpp#L464) — `return gallery_report_from_json(j);` +- [`tests/test_calibration.cpp:192`](../tests/test_calibration.cpp#L192) — `Embedding unit_axis(int slot)` +- [`tests/test_calibration.cpp:222`](../tests/test_calibration.cpp#L222) — `Unknown` +- [`tests/test_calibration.cpp:248`](../tests/test_calibration.cpp#L248) — `Unknown` ### GR-004 @@ -535,7 +549,7 @@ _None._ ### PR-002 -**Locations:** 8 +**Locations:** 10 - [`src/nodes/embedding_dump_node.hpp:2`](../src/nodes/embedding_dump_node.hpp#L2) — `Unknown` - [`src/nodes/embedding_dump_node.hpp:18`](../src/nodes/embedding_dump_node.hpp#L18) — `Unknown` @@ -545,6 +559,8 @@ _None._ - [`scripts/optimizer/replay.py:5`](../scripts/optimizer/replay.py#L5) — `Reads an embedding dump (scripts/optimizer/SCHEMA.md), feeds each frame as an` - [`scripts/optimizer/second_score.py:5`](../scripts/optimizer/second_score.py#L5) — `Unknown` - [`scripts/validation/ground_truth.py:24`](../scripts/validation/ground_truth.py#L24) — `Unknown` +- [`experiments/xsource/resolution_sweep.py:4`](../experiments/xsource/resolution_sweep.py#L4) — `Unknown` +- [`experiments/xsource/verify_labels.py:4`](../experiments/xsource/verify_labels.py#L4) — `Unknown` ### PR-004 @@ -554,7 +570,7 @@ _None._ ### SR-001 -**Locations:** 60 +**Locations:** 64 - [`src/backends/gemm_backend.cpp:44`](../src/backends/gemm_backend.cpp#L44) — `constexpr int kDim = 512;` - [`src/build_gallery.cpp:83`](../src/build_gallery.cpp#L83) — `Unknown` @@ -584,6 +600,9 @@ _None._ - [`src/nodes/embedding_dump_node.hpp:238`](../src/nodes/embedding_dump_node.hpp#L238) — `H5::H5File file(path_, H5F_ACC_TRUNC);` - [`src/scene_preview.cpp:133`](../src/scene_preview.cpp#L133) — `int main(int argc, char** argv)` - [`src/types.hpp:148`](../src/types.hpp#L148) — `struct Actor` +- [`tests/test_calibration.cpp:192`](../tests/test_calibration.cpp#L192) — `Embedding unit_axis(int slot)` +- [`tests/test_calibration.cpp:222`](../tests/test_calibration.cpp#L222) — `Unknown` +- [`tests/test_calibration.cpp:248`](../tests/test_calibration.cpp#L248) — `Unknown` - [`tests/test_gallery_store.cpp:182`](../tests/test_gallery_store.cpp#L182) — `TempFile tf("gallery_stamped.h5");` - [`tests/test_gallery_store.cpp:201`](../tests/test_gallery_store.cpp#L201) — `TempFile tf("gallery_stamped.h5");` - [`tests/test_gallery_store.cpp:219`](../tests/test_gallery_store.cpp#L219) — `TempFile tf("gallery_unstamped.h5");` @@ -596,6 +615,7 @@ _None._ - [`tests/test_gallery_store.cpp:348`](../tests/test_gallery_store.cpp#L348) — `Unknown` - [`tests/test_gallery_store.cpp:367`](../tests/test_gallery_store.cpp#L367) — `Unknown` - [`tests/test_gallery_store.cpp:384`](../tests/test_gallery_store.cpp#L384) — `TempFile tf("fake_model.onnx");` +- [`tests/test_similarity.cpp:1`](../tests/test_similarity.cpp#L1) — `Unknown` - [`scripts/filter_gallery.py:80`](../scripts/filter_gallery.py#L80) — `if actor_jellyfin_id(a) in cast_ids]` - [`scripts/make_gallery.py:181`](../scripts/make_gallery.py#L181) — `Unknown` - [`scripts/make_jellyfin_gallery.py:4`](../scripts/make_jellyfin_gallery.py#L4) — `Unknown` @@ -619,7 +639,7 @@ _None._ ### SR-002 -**Locations:** 32 +**Locations:** 35 - [`src/config.hpp:44`](../src/config.hpp#L44) — `Unknown` - [`src/config.hpp:108`](../src/config.hpp#L108) — `Unknown` @@ -630,12 +650,13 @@ _None._ - [`src/main.cpp:86`](../src/main.cpp#L86) — `static constexpr std::size_t kSceneJoinDepth = 256;` - [`src/main.cpp:213`](../src/main.cpp#L213) — `reg_cfg, EvidenceDiscounter(same_person));` - [`src/main.cpp:230`](../src/main.cpp#L230) — `reg_cfg, EvidenceDiscounter(same_person));` -- [`src/main.cpp:299`](../src/main.cpp#L299) — `Unknown` -- [`src/main.cpp:309`](../src/main.cpp#L309) — `Unknown` -- [`src/main.cpp:379`](../src/main.cpp#L379) — `return run_net(std::move(net));` -- [`src/main.cpp:411`](../src/main.cpp#L411) — `Unknown` +- [`src/main.cpp:319`](../src/main.cpp#L319) — `Unknown` +- [`src/main.cpp:329`](../src/main.cpp#L329) — `Unknown` +- [`src/main.cpp:399`](../src/main.cpp#L399) — `return run_net(std::move(net));` +- [`src/main.cpp:431`](../src/main.cpp#L431) — `Unknown` - [`src/nodes/camera_position_change_detector_node.hpp:30`](../src/nodes/camera_position_change_detector_node.hpp#L30) — `struct CameraPositionChangeDetectorFunc` - [`src/nodes/embedder_node.hpp:21`](../src/nodes/embedder_node.hpp#L21) — `struct EmbedderFunc` +- [`src/nodes/face_aligner_node.hpp:7`](../src/nodes/face_aligner_node.hpp#L7) — `struct FaceAlignerFunc` - [`src/nodes/face_detector_node.hpp:2`](../src/nodes/face_detector_node.hpp#L2) — `Unknown` - [`src/nodes/face_detector_node.hpp:47`](../src/nodes/face_detector_node.hpp#L47) — `private:` - [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown` @@ -652,6 +673,8 @@ _None._ - [`src/nodes/scene_detector_node.hpp:144`](../src/nodes/scene_detector_node.hpp#L144) — `void write_output()` - [`src/scene_boundaries.hpp:2`](../src/scene_boundaries.hpp#L2) — `Unknown` - [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown` +- [`tests/test_calibration.cpp:1`](../tests/test_calibration.cpp#L1) — `Unknown` +- [`tests/test_face_tracker.cpp:1`](../tests/test_face_tracker.cpp#L1) — `Unknown` - [`tests/test_face_utils.cpp:1`](../tests/test_face_utils.cpp#L1) — `Unknown` ### SR-003 @@ -668,16 +691,18 @@ _None._ ### SR-005 -**Locations:** 9 +**Locations:** 11 - [`src/config.hpp:152`](../src/config.hpp#L152) — `Unknown` - [`src/gallery/gallery_store.hpp:15`](../src/gallery/gallery_store.hpp#L15) — `Unknown` -- [`src/gallery/track_gallery.hpp:122`](../src/gallery/track_gallery.hpp#L122) — `void forget(int track_id) { tracks_.erase(track_id); }` -- [`src/gallery/track_gallery.hpp:132`](../src/gallery/track_gallery.hpp#L132) — `void set_calibration(std::function c) { calibrate_ = std::move(c); }` -- [`src/gallery/track_gallery.hpp:161`](../src/gallery/track_gallery.hpp#L161) — `struct TrackState` +- [`src/gallery/track_gallery.hpp:123`](../src/gallery/track_gallery.hpp#L123) — `void forget(int track_id) { tracks_.erase(track_id); }` +- [`src/gallery/track_gallery.hpp:133`](../src/gallery/track_gallery.hpp#L133) — `void set_calibration(std::function c) { calibrate_ = std::move(c); }` +- [`src/gallery/track_gallery.hpp:164`](../src/gallery/track_gallery.hpp#L164) — `struct TrackState` +- [`src/gallery/track_gallery.hpp:274`](../src/gallery/track_gallery.hpp#L274) — `static int plurality_actor(const TrackState& ts)` - [`src/nodes/identity_matcher_node.hpp:110`](../src/nodes/identity_matcher_node.hpp#L110) — `std::vector host_gallery(static_cast(n_gallery_) * 512);` - [`src/nodes/identity_matcher_node.hpp:147`](../src/nodes/identity_matcher_node.hpp#L147) — `MatchedSceneFrame operator()(TrackedSceneFrame tf)` - [`src/nodes/identity_matcher_node.hpp:285`](../src/nodes/identity_matcher_node.hpp#L285) — `Unknown` +- [`tests/test_track_gallery.cpp:1`](../tests/test_track_gallery.cpp#L1) — `Unknown` - [`scripts/make_jellyfin_gallery.py:4`](../scripts/make_jellyfin_gallery.py#L4) — `Unknown` ### UT-001 @@ -789,6 +814,13 @@ _None._ - [`src/nodes/embedding_dump_node.hpp:159`](../src/nodes/embedding_dump_node.hpp#L159) — `void operator()(EmbeddedSceneFrame ef)` - [`src/nodes/embedding_dump_node.hpp:242`](../src/nodes/embedding_dump_node.hpp#L242) — `H5::H5File file(path_, H5F_ACC_TRUNC);` +### VR-013 + +**Locations:** 2 + +- [`experiments/xsource/resolution_sweep.py:4`](../experiments/xsource/resolution_sweep.py#L4) — `Unknown` +- [`experiments/xsource/verify_labels.py:4`](../experiments/xsource/verify_labels.py#L4) — `Unknown` + ### VR-014 **Locations:** 1 diff --git a/experiments/xsource/landmark_voting.py b/experiments/xsource/landmark_voting.py index 1a1bd07..cd5f65d 100644 --- a/experiments/xsource/landmark_voting.py +++ b/experiments/xsource/landmark_voting.py @@ -30,6 +30,8 @@ import sae_embed # before cv2 — see alignment_compare.py import numpy as np import cv2 +import argparse + ROOT = "/home/dtourolle/Development/Jray-project/scene-actor-extraction/" M = ROOT + "models/" CLIPS = ["5157344", "5157339"] @@ -37,16 +39,31 @@ PROB_THRESHOLD = 0.754 GROUP_IOU = 0.55 # detections overlapping this much are the same face MATCH_IOU = 0.35 # tie a detection to the hand-labelled face -base_eng = sae_embed.FaceEmbedder(detector_model=M + "scrfd_500m_bnkps.onnx", +_ap = argparse.ArgumentParser() +_ap.add_argument("--detector", default="scrfd_500m_bnkps.onnx", + help="detector under models/. SCRFD sizes 500m / 2.5g / 10g come " + "from InsightFace's buffalo_sc / buffalo_m / buffalo_l packs") +_ap.add_argument("--vote-conf", type=float, default=None, + help="confidence floor for the voting pass. Omit to auto-tune " + "it to --target-votes") +_ap.add_argument("--target-votes", type=int, default=3, + help="votes per face to tune --vote-conf towards, so detectors " + "are compared at equal redundancy rather than equal settings") +_args = _ap.parse_args() + +base_eng = sae_embed.FaceEmbedder(detector_model=M + _args.detector, arcface_model=M + "LVFace-B_Glint360K.onnx", conf=0.5, nms=0.4, max_side=0) -# Same models, looser suppression: keep the duplicates NMS would have removed. -vote_eng = sae_embed.FaceEmbedder(detector_model=M + "scrfd_500m_bnkps.onnx", + + +def _make_vote_engine(conf): + # Same models, looser suppression: keep the duplicates NMS would have removed. + return sae_embed.FaceEmbedder(detector_model=M + _args.detector, arcface_model=M + "LVFace-B_Glint360K.onnx", - conf=0.3, nms=0.9, max_side=0) + conf=conf, nms=0.9, max_side=0) + + cal = sae_embed.gallery_calibration(ROOT + "gallery_lvface.h5") - - def iou(a, b): ax, ay, aw, ah = a; bx, by, bw, bh = b x0, y0 = max(ax, bx), max(ay, by) @@ -79,6 +96,46 @@ def vote(dets): return out +def tune_vote_conf(target, sample=6): + """Pick the confidence floor giving ~target detections per face to average. + + A larger SCRFD is more confident and suppresses harder, so at a fixed floor + it emits fewer overlapping anchors — median 2 against 500m's 3. Comparing + detectors at equal SETTINGS therefore also compares them at unequal + redundancy, and the voting arm is handicapped for the bigger models. Tuning + each to the same votes-per-face isolates landmark quality from how much + there was to average. + """ + frames = sorted(glob.glob(f"frames/d{CLIPS[0]}_*.png"))[:sample] + imgs = [cv2.imread(f) for f in frames] + best = (None, None, 1e9) + for conf in (0.30, 0.20, 0.12, 0.07, 0.04, 0.02, 0.01): + eng = _make_vote_engine(conf) + sizes = [n for img in imgs for _, _, _, n in vote(eng.detect(img))] + if not sizes: + continue + med = float(np.median(sizes)) + if abs(med - target) < best[2]: + best = (conf, eng, abs(med - target)) + print(f"[tune] conf={conf:.2f} -> median {med:.0f} votes/face", file=sys.stderr) + if med >= target: + break + if best[1] is None: + print(f"[tune] no confidence floor reached {target} votes/face; " + f"falling back to 0.30", file=sys.stderr) + return 0.30, _make_vote_engine(0.30) + print(f"[tune] chose conf={best[0]:.2f} for ~{target} votes/face", file=sys.stderr) + return best[0], best[1] + + +if _args.vote_conf is not None: + VOTE_CONF, vote_eng = _args.vote_conf, _make_vote_engine(_args.vote_conf) +else: + VOTE_CONF, vote_eng = tune_vote_conf(_args.target_votes) + +cal = sae_embed.gallery_calibration(ROOT + "gallery_lvface.h5") + + def collect(clip): lab = {os.path.basename(p): os.path.basename(os.path.dirname(p)) for p in glob.glob(f"labelling/{clip}/*/*.jpg") @@ -124,7 +181,8 @@ print(f"[voting] group size: median {np.median(allv):.0f}, " file=sys.stderr) GAL, PRB = "5157344", "5157339" -print(f"\ngallery {GAL} -> probe {PRB}, P>{PROB_THRESHOLD}\n") +print(f"\ndetector={_args.detector} vote_conf={VOTE_CONF:.2f} " + f"gallery {GAL} -> probe {PRB}, P>{PROB_THRESHOLD}\n") print(f"{'align':>8}{'person':>8}{'n_gal':>7}{'n_prb':>7}" f"{'within-clip':>13}{'cross-clip':>12}{'hit rate':>10}") summary = {} diff --git a/experiments/xsource/resolution_sweep.py b/experiments/xsource/resolution_sweep.py index 6603f75..7095475 100644 --- a/experiments/xsource/resolution_sweep.py +++ b/experiments/xsource/resolution_sweep.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """Impact of input resolution on cross-source identification. +TRACES: VR-013 | PR-002 + Gallery is built from one clip at NATIVE resolution. Probes come from the other clip with the WHOLE FRAME downscaled before it reaches the detector, so detection and landmark regression degrade together with the pixels. That is the diff --git a/experiments/xsource/verify_labels.py b/experiments/xsource/verify_labels.py index a556d51..c15a1fa 100644 --- a/experiments/xsource/verify_labels.py +++ b/experiments/xsource/verify_labels.py @@ -1,6 +1,14 @@ #!/usr/bin/env python3 """Integrity check on the labelled set, before it is used as ground truth. +TRACES: VR-013 | PR-002 + +VR-013's ground truth is hand-sorted rather than propagated by embedding +similarity, because propagation would keep only the faces the embedder already +gets right and silently drop the ones the sweep exists to find. This script is +what makes that claim checkable, so it is part of the requirement rather than a +helper of it. + Checks, loudest failure first: 1. INDEX INTEGRITY. Each crop's embedding is taken by re-detecting its source diff --git a/scripts/filter_gallery.py b/scripts/filter_gallery.py index c07deb0..cd83124 100644 --- a/scripts/filter_gallery.py +++ b/scripts/filter_gallery.py @@ -77,7 +77,8 @@ def main(): if missing > 0: print(f"[warn] {missing} cast member(s) not present in gallery (not yet embedded)", file=sys.stderr) - # TRACES: GR-004 | SR-001 — a filtered gallery holds the SAME vectors as its + # TRACES: GR-004 | SR-001 + # a filtered gallery holds the SAME vectors as its # source, so it inherits the source's binding. Dropping the stamp here would # silently launder a stamped gallery into an unstamped one. save_gallery_hdf5({"actors": actors}, Path(args.output), diff --git a/scripts/make_gallery.py b/scripts/make_gallery.py index 024b935..3b49f99 100755 --- a/scripts/make_gallery.py +++ b/scripts/make_gallery.py @@ -178,7 +178,8 @@ def main(): output = Path(args.output) image_root = Path(args.image_dir) if args.image_dir else output.parent / "images" - # TRACES: GR-004 | SR-001 — stamp with the model actually loaded, resolved + # TRACES: GR-004 | SR-001 + # stamp with the model actually loaded, resolved # through the same helper load_embedder uses so the two cannot diverge. arcface_path = resolve_arcface(args.models_dir, args.arcface) embedder = load_embedder(args.build_dir, args.models_dir, args.arcface) diff --git a/scripts/make_jellyfin_gallery.py b/scripts/make_jellyfin_gallery.py index 915982c..f7f9d0a 100644 --- a/scripts/make_jellyfin_gallery.py +++ b/scripts/make_jellyfin_gallery.py @@ -453,7 +453,8 @@ def main(): existing_actors = {} if args.merge and output.is_file(): existing = load_gallery_hdf5(output) - # TRACES: GR-004 | SR-001 — --merge keeps the existing actors' vectors and + # TRACES: GR-004 | SR-001 + # --merge keeps the existing actors' vectors and # embeds the new ones with THIS model. If they disagree, the result is one # gallery holding two incompatible embedding spaces, which is worse than a # mismatched gallery: no later check can separate them again. diff --git a/scripts/movienet_eval.py b/scripts/movienet_eval.py index 454c832..cf866f4 100644 --- a/scripts/movienet_eval.py +++ b/scripts/movienet_eval.py @@ -62,7 +62,8 @@ def main(): args = p.parse_args() embedder = load_embedder(args.build_dir, args.models_dir, args.arcface) - # TRACES: GR-004 | SR-001 — match() below is a bare dot product against the + # TRACES: GR-004 | SR-001 + # match() below is a bare dot product against the # gallery's vectors; if the gallery came from another model those numbers are # noise wearing a similarity's clothes. verify_gallery_stamp(args.gallery, diff --git a/scripts/optimizer/fetch_missing_actors.py b/scripts/optimizer/fetch_missing_actors.py index 773999b..ccad71b 100644 --- a/scripts/optimizer/fetch_missing_actors.py +++ b/scripts/optimizer/fetch_missing_actors.py @@ -106,7 +106,8 @@ def fetch(missing_path, out_path, token, build_dir, models_dir, arcface, f"(wiki={n_via_wikidata}) no_tmdb={n_no_tmdb} no_img={n_no_img} " f"no_face={n_no_face}", file=sys.stderr) - # TRACES: GR-004 | SR-001 — the legacy JSON gallery carries the same stamp as + # TRACES: GR-004 | SR-001 + # the legacy JSON gallery carries the same stamp as # the HDF5 one; src/gallery/gallery_store.cpp reads it from either. Path(out_path).write_text(json.dumps({"embedder": stamp, "actors": actors}, indent=2)) n_emb = sum(len(a["embeddings"]) for a in actors) @@ -120,7 +121,8 @@ def fetch(missing_path, out_path, token, build_dir, models_dir, arcface, def merge(base_path, add_path, out_path): base = json.loads(Path(base_path).read_text()) add = json.loads(Path(add_path).read_text()) - # TRACES: GR-004 | SR-001 — merging two galleries from different models makes + # TRACES: GR-004 | SR-001 + # merging two galleries from different models makes # ONE file containing two incompatible embedding spaces. Nothing downstream can # ever untangle that, so this is the one place the check must run before, not # after, the write. diff --git a/scripts/optimizer/optimize.py b/scripts/optimizer/optimize.py index 73e4091..16abd67 100644 --- a/scripts/optimizer/optimize.py +++ b/scripts/optimizer/optimize.py @@ -199,7 +199,8 @@ def main(): if not Path(f["dump"]).exists(): sys.exit(f"[opt] missing dump for {f['name']}: {f['dump']}") - # TRACES: GR-004 | SR-001 — every (dump, gallery) pair is checked ONCE here, + # TRACES: GR-004 | SR-001 + # every (dump, gallery) pair is checked ONCE here, # before the first evaluation. A DE sweep is thousands of replays; discovering # a cross-model pair at the end (or never) means every number it produced was # noise. Each replay subprocess re-checks its own pair anyway. diff --git a/scripts/optimizer/reembed_gallery.py b/scripts/optimizer/reembed_gallery.py index a672d59..cdd2282 100644 --- a/scripts/optimizer/reembed_gallery.py +++ b/scripts/optimizer/reembed_gallery.py @@ -59,7 +59,8 @@ def main(): ref = load_gallery_hdf5(Path(args.ref)) images_root = Path(args.images) embedder = load_embedder(args.build_dir, args.models_dir, args.arcface) - # TRACES: GR-004 | SR-001 — this script exists to produce a gallery in a + # TRACES: GR-004 | SR-001 + # this script exists to produce a gallery in a # DIFFERENT model's space from the reference. The output must therefore never # inherit the reference's stamp; it carries the stamp of --arcface, which is # the whole point of the bake-off being safe to run. diff --git a/scripts/optimizer/replay.py b/scripts/optimizer/replay.py index 8f93e17..57b7440 100644 --- a/scripts/optimizer/replay.py +++ b/scripts/optimizer/replay.py @@ -110,7 +110,8 @@ def replay(dump_path: str, gallery: str, cfg: dict, build_dir: str, stop: bool = sys.path.insert(0, build_dir) import sae_kpn - # TRACES: GR-004 | SR-001 — checked here, before any network is built, so a + # TRACES: GR-004 | SR-001 + # checked here, before any network is built, so a # cross-model replay dies with one readable error instead of producing a # plausible-looking score. add_identity_matcher re-checks it C++-side below; # that is the backstop for any other caller of the binding. @@ -249,7 +250,8 @@ def main(): # per-film gallery expansion: promotes pose-varied views of confidently-identified # actors into an in-memory annex, recovering ~+4 recall at no precision cost. p.add_argument("--expand-gallery", action="store_true") - # TRACES: GR-004 | SR-001 — promote an unprovable gallery/dump binding from a + # TRACES: GR-004 | SR-001 + # promote an unprovable gallery/dump binding from a # loud warning to a hard error. Measurement sweeps should set this (or # SAE_REQUIRE_GALLERY_STAMP=1) so no number comes from an unbound pair. p.add_argument("--require-gallery-stamp", action="store_true") diff --git a/scripts/sae_embed_loader.py b/scripts/sae_embed_loader.py index a60c13a..b28d979 100644 --- a/scripts/sae_embed_loader.py +++ b/scripts/sae_embed_loader.py @@ -11,6 +11,7 @@ argument, which is often None. """ import sys +import os from pathlib import Path DEFAULT_ARCFACE = "arcface_w600k_r50.onnx" @@ -19,8 +20,10 @@ DEFAULT_ARCFACE = "arcface_w600k_r50.onnx" def resolve_arcface(models_dir: str, arcface: str | None = None) -> str: """The ArcFace/LVFace ONNX path load_embedder would use for these arguments. - TRACES: GR-004 | SR-001 — single source of truth for "which model is this", - so the stamp written into a gallery can never drift from the model loaded.""" + TRACES: GR-004 | SR-001 + + Single source of truth for "which model is this", so the stamp written into + a gallery can never drift from the model loaded.""" return arcface if arcface else str(Path(models_dir) / DEFAULT_ARCFACE) @@ -49,13 +52,21 @@ def load_embedder(build_dir: str, models_dir: str, arcface: str | None = None, sys.exit(f"{name} model not found: {model}\nRun: bash scripts/download_models.sh") # A TRT-backend build cannot load .onnx; it needs pre-built engines from - # scripts/build_trt_engines.sh. Pass them when present (ignored by ORT). + # scripts/build_trt_engines.sh. + # + # These are passed only on request. The old comment here claimed they were + # "ignored by ORT" — they are not. The ORT backend treats an engine path as + # an instruction and raises, which is the right behaviour (silently ignoring + # a requested engine would be worse), but it meant that merely HAVING a + # populated trt_cache/ broke every ORT gallery build in the repo, with an + # error naming a flag the caller never set. + use_engines = os.environ.get("SAE_USE_TRT_ENGINES", "") not in ("", "0", "false") trt = Path(models_path).parent / "trt_cache" det_engine = trt / "scrfd.scrfd_500m_bnkps.640.fp16.engine" arc_engine = trt / f"arcface.{Path(arcface_path).stem}.b4.fp16.engine" return sae_embed.FaceEmbedder( detector_path, arcface_path, conf, nms, max_side, - str(det_engine) if det_engine.is_file() else "", - str(arc_engine) if arc_engine.is_file() else "", + str(det_engine) if (use_engines and det_engine.is_file()) else "", + str(arc_engine) if (use_engines and arc_engine.is_file()) else "", ) diff --git a/scripts/sae_gallery.py b/scripts/sae_gallery.py index 1addf29..b088a7d 100644 --- a/scripts/sae_gallery.py +++ b/scripts/sae_gallery.py @@ -168,7 +168,8 @@ def save_gallery_hdf5(gallery: dict, output: Path, embedder: dict | None = None) f.create_dataset("jellyfin_id", data=np.asarray(jf, dtype=object), dtype=str_t) f.create_dataset("name", data=np.asarray(name, dtype=object), dtype=str_t) f.create_dataset("source_images", data=np.asarray(src_images, dtype=object), dtype=str_t) - # TRACES: GR-004 | SR-001 — omitted entirely when unknown, so "unstamped" + # TRACES: GR-004 | SR-001 + # omitted entirely when unknown, so "unstamped" # round-trips as unstamped rather than as a stamp naming no model. if not _stamp_empty(embedder): g = f.create_group("embedder") @@ -196,7 +197,8 @@ def load_gallery_hdf5(path: Path) -> dict: if "source_images" in f: src_images = [s.decode() if isinstance(s, bytes) else s for s in f["source_images"][:]] - # TRACES: GR-004 | SR-001 — carried through so a derived gallery (filter, + # TRACES: GR-004 | SR-001 + # carried through so a derived gallery (filter, # merge, cast-restrict) keeps the binding of the gallery it came from. stamp = None if "embedder" in f: diff --git a/scripts/validation/ground_truth.py b/scripts/validation/ground_truth.py index 174f014..c6b6b1b 100644 --- a/scripts/validation/ground_truth.py +++ b/scripts/validation/ground_truth.py @@ -17,6 +17,11 @@ X-Ray/MovieNet key on IMDb nm-ids — see [[per-scene-presence-eval-design]]. Two sources implemented: * XRayGroundTruth — Zenodo scene-level Amazon X-Ray CSVs (cast-in-scene). * MovieNetGroundTruth — MovieNet-PS per-shot face annotations (on-screen faces). + +Both are published corpora addressed by title, so a scoring run is reproducible +from the identifiers alone — no annotation of ours travels with the code. + +TRACES: VR-004 | PR-002 """ from __future__ import annotations