feat: presence windows come from registry claims (schema_version 2)
The sink no longer reconstructs presence from per-frame detections. A reaped track already IS a window — [first_seen, last_seen] of a track an actor owned — so it is pushed straight to the aggregator when it dies and written out as-is. AR-012 completed end to end. The annealing pass is deleted, not disabled: anneal_sec existed only to bridge gaps between isolated accepted frames, and a track that survives its own gaps leaves it nothing to do. The field is REMOVED from the output rather than zeroed — a field naming a mechanism the pipeline no longer has is actively misleading to anyone reading a manifest, and would outlive everyone who remembers why it reads 0. IR-002 — schema_version 2, matching jRay/SPEC.md JR-002. Windows become objects carrying `belief` and `route` rather than bare float pairs, so a consumer can caveat or filter instead of treating every window as equally certain. The new `extraction` block carries `extinction_sec` (the successor to anneal_sec, and what a consumer actually needs to interpret a window) and `gallery_scope` — global vs limited being the strongest single quality signal when two manifests compete for one cut, since identical gallery_size can mean very different recall. AR-016 wired: a pre-write hook flushes the registry with the last timestamp seen, so tracks still live at EOF are emitted. A film ends with faces on screen and those tracks have not timed out; without this the closing scene's cast is silently dropped, which reads as a recognition miss rather than a bookkeeping bug. IR-003 stays In Progress deliberately: the sink now writes after the flush, but the deferred re-identification pass (AR-020) does not exist yet, so output is still final at EOF rather than after it. This is a BREAKING format change and part of the coordinated SR-003 bump — it must ship together with the jRay reader and the server's acceptance of the new shape, not ahead of them. Suite: 80 cases, 3250 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: AR-012, AR-016, IR-002, IR-003 | SR-002, SR-003
This commit is contained in:
@@ -74,8 +74,8 @@ Status: `Done` · `In Progress` · `Planned` · `TBD` · `Withdrawn`
|
|||||||
| ID | Requirement | Traces to | Priority | Status |
|
| ID | Requirement | Traces to | Priority | Status |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| IR-001 | Emit the JRay truth format as sibling `.jray.json` | SR-003 | High | Done |
|
| IR-001 | Emit the JRay truth format as sibling `.jray.json` | SR-003 | High | Done |
|
||||||
| IR-002 | Windows carry belief + route; `extraction.*` carries `extinction_sec`, `gallery_scope` | SR-003 | High | Planned |
|
| IR-002 | Windows carry belief + route; `extraction.*` carries `extinction_sec`, `gallery_scope` | SR-003 | High | **Done** — `schema_version: 2`; windows are objects with `belief` + `route`; `extraction.*` carries `extinction_sec` and `gallery_scope`; `anneal_sec` removed |
|
||||||
| IR-003 | Output written **after** the deferred pass, not at EOF | SR-003 | High | Planned |
|
| IR-003 | Output written **after** the deferred pass, not at EOF | SR-003 | High | **In Progress** — sink builds windows from registry claims and flushes at EOF; the deferred pass (AR-020) does not exist yet, so output is still final at EOF |
|
||||||
| IR-004 | Compute the audio signature exactly per server spec §3 | SR-003 | Medium | **Done** — `src/audio_signature.*`; not yet emitted into the truth file (IR-002) |
|
| IR-004 | Compute the audio signature exactly per server spec §3 | SR-003 | Medium | **Done** — `src/audio_signature.*`; not yet emitted into the truth file (IR-002) |
|
||||||
| IR-005 | Golden-vector fixture shared with the plugin repo to prove bit-exactness | SR-003 | High | **Done** — `tests/fixtures/audio/`; v1 parameters now normative in server spec §3 |
|
| IR-005 | Golden-vector fixture shared with the plugin repo to prove bit-exactness | SR-003 | High | **Done** — `tests/fixtures/audio/`; v1 parameters now normative in server spec §3 |
|
||||||
| IR-007 | Media < 120 s: emit no signature, apply no sync offset — identical rule in both producers | SR-003 | Low | **Done** |
|
| IR-007 | Media < 120 s: emit no signature, apply no sync offset — identical rule in both producers | SR-003 | Low | **Done** |
|
||||||
|
|||||||
+46
-17
@@ -3,7 +3,7 @@
|
|||||||
<!-- GENERATED FILE - do not edit by hand. -->
|
<!-- GENERATED FILE - do not edit by hand. -->
|
||||||
<!-- Regenerate: scripts/traceability/traceability-gate.sh -->
|
<!-- Regenerate: scripts/traceability/traceability-gate.sh -->
|
||||||
|
|
||||||
**Generated:** 2026-07-31T07:15:30+00:00
|
**Generated:** 2026-07-31T08:01:31+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`).
|
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,12 +12,12 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev
|
|||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Source files scanned | 95 |
|
| Source files scanned | 95 |
|
||||||
| TRACES tags found | 77 |
|
| TRACES tags found | 82 |
|
||||||
| EXCEPTION tags found | 0 |
|
| EXCEPTION tags found | 0 |
|
||||||
| Requirements defined | 63 |
|
| Requirements defined | 63 |
|
||||||
| Requirements covered | 22 |
|
| Requirements covered | 24 |
|
||||||
| **Coverage** | **34.9%** (22/63) |
|
| **Coverage** | **38.1%** (24/63) |
|
||||||
| Coverage of CI-executable scope | 42.3% (22/52) |
|
| Coverage of CI-executable scope | 46.2% (24/52) |
|
||||||
| Tagged but unexecuted in CI | 3 |
|
| Tagged but unexecuted in CI | 3 |
|
||||||
| Orphan tags | 0 |
|
| Orphan tags | 0 |
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev
|
|||||||
|
|
||||||
| Type | Covered | Tagged but unexecuted | Defined |
|
| Type | Covered | Tagged but unexecuted | Defined |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| AR | 11 | 0 | 27 |
|
| AR | 13 | 0 | 27 |
|
||||||
| DP | 2 | 0 | 8 |
|
| DP | 2 | 0 | 8 |
|
||||||
| IR | 6 | 0 | 8 |
|
| IR | 6 | 0 | 8 |
|
||||||
| GR | 3 | 0 | 9 |
|
| GR | 3 | 0 | 9 |
|
||||||
@@ -83,12 +83,12 @@ _None._
|
|||||||
| AR-004 | Planned | T1, T4 | SR-002 | untagged | - | Backpressure: unbounded faces/frame absorbed by slowing, never by dro… |
|
| AR-004 | Planned | T1, T4 | SR-002 | untagged | - | Backpressure: unbounded faces/frame absorbed by slowing, never by dro… |
|
||||||
| AR-005 | Done | T1, T3 | SR-002 | covered | `src/face_utils.hpp` | Align to 112×112 via ArcFace 5-point similarity transform |
|
| AR-005 | Done | T1, T3 | SR-002 | covered | `src/face_utils.hpp` | Align to 112×112 via ArcFace 5-point similarity transform |
|
||||||
| AR-006 | Done | T3 | SR-002 | untagged | - | 512-d L2-normalised embeddings, batched |
|
| AR-006 | Done | T3 | SR-002 | untagged | - | 512-d L2-normalised embeddings, batched |
|
||||||
| AR-007 | In Progress | T2 | SR-002 | untagged | - | Associate detections by IoU + embedding, with **frame-dependent** wei… |
|
| 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 | Planned | T2 | SR-002 | untagged | - | One track pool keyed on `last_seen`; no separate revival path |
|
| 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-009 | Done | T2 | SR-002 | untagged | - | Camera-cut detection (histogram) as an association hint |
|
| AR-009 | Done | T2 | SR-002 | untagged | - | Camera-cut detection (histogram) as an association hint |
|
||||||
| AR-010 | **Not started** — `… | T2 | SR-002 | untagged | - | Scene-boundary detection (TransNetV2) as an association hint |
|
| AR-010 | **Not started** — `… | T2 | SR-002 | untagged | - | 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… |
|
| AR-011 | Planned | T1, T2 | SR-002 | untagged | - | **Every model is fed the input it was trained for** — cost reduced by… |
|
||||||
| AR-012 | **Done** — `src/tra… | T2 | **SR-002** | covered | `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Presence follows **track extent**, not per-frame recognition |
|
| AR-012 | **Done** — `src/tra… | T2 | **SR-002** | covered | `src/main.cpp`, `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Presence follows **track extent**, not per-frame recognition |
|
||||||
| AR-013 | **Done** — `last_se… | T2 | SR-002 | covered | `src/track_registry.hpp`, `tests/test_track_registry.cpp` | `last_seen` optional state machine; window ends at last sighting, nev… |
|
| AR-013 | **Done** — `last_se… | T2 | SR-002 | covered | `src/track_registry.hpp`, `tests/test_track_registry.cpp` | `last_seen` optional state machine; window ends at last sighting, nev… |
|
||||||
| AR-014 | **Done** — swap clo… | T2 | SR-002 | covered | `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Belief swap A→B terminates the track and starts a new one |
|
| AR-014 | **Done** — swap clo… | T2 | SR-002 | covered | `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Belief swap A→B terminates the track and starts a new one |
|
||||||
| 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-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… |
|
||||||
@@ -99,8 +99,8 @@ _None._
|
|||||||
| AR-020 | Planned | T2 | SR-005 | untagged | - | Deferred re-identification of unknown tracks against the final expand… |
|
| 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-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-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` | Fit sigmoid calibration from intra/inter similarity distributions |
|
| 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** (registry … | T1, static | SR-002 | covered | `src/evidence_discount.hpp` | **Always the calibrated probability, never a raw cosine** — exception… |
|
| AR-024 | **Done** — associat… | T1, static | SR-002 | covered | `src/config.hpp`, `src/evidence_discount.hpp`, `src/gallery/gallery_calibration.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-025 | **Done** — log-odds… | T1 | SR-002 | covered | `src/evidence_discount.hpp` | Per-track Bayesian accumulation in log-odds, with correlated-observat… |
|
| AR-025 | **Done** — log-odds… | T1 | SR-002 | covered | `src/evidence_discount.hpp` | Per-track Bayesian accumulation in log-odds, with correlated-observat… |
|
||||||
| AR-026 | In Progress | T1, T4 | SR-001 | untagged | - | All similarity computed as GEMM, including annex and deferred pass |
|
| AR-026 | In Progress | T1, T4 | SR-001 | untagged | - | All similarity computed as GEMM, including annex and deferred pass |
|
||||||
| AR-027 | Planned | T4 | SR-001 | untagged | - | Throughput acceptable for **arbitrary** gallery size |
|
| AR-027 | Planned | T4 | SR-001 | untagged | - | Throughput acceptable for **arbitrary** gallery size |
|
||||||
@@ -155,10 +155,27 @@ _None._
|
|||||||
|
|
||||||
- [`src/face_utils.hpp:2`](../src/face_utils.hpp#L2) — `inline cv::Mat align_face(const cv::Mat& img,`
|
- [`src/face_utils.hpp:2`](../src/face_utils.hpp#L2) — `inline cv::Mat align_face(const cv::Mat& img,`
|
||||||
|
|
||||||
|
### AR-007
|
||||||
|
|
||||||
|
**Locations:** 3
|
||||||
|
|
||||||
|
- [`src/config.hpp:97`](../src/config.hpp#L97) — `Unknown`
|
||||||
|
- [`src/main.cpp:199`](../src/main.cpp#L199) — `reg_cfg, EvidenceDiscounter(same_person));`
|
||||||
|
- [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown`
|
||||||
|
|
||||||
|
### AR-008
|
||||||
|
|
||||||
|
**Locations:** 3
|
||||||
|
|
||||||
|
- [`src/config.hpp:97`](../src/config.hpp#L97) — `Unknown`
|
||||||
|
- [`src/main.cpp:199`](../src/main.cpp#L199) — `reg_cfg, EvidenceDiscounter(same_person));`
|
||||||
|
- [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown`
|
||||||
|
|
||||||
### AR-012
|
### AR-012
|
||||||
|
|
||||||
**Locations:** 2
|
**Locations:** 3
|
||||||
|
|
||||||
|
- [`src/main.cpp:199`](../src/main.cpp#L199) — `reg_cfg, EvidenceDiscounter(same_person));`
|
||||||
- [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown`
|
- [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown`
|
||||||
- [`tests/test_track_registry.cpp:3`](../tests/test_track_registry.cpp#L3) — `Unknown`
|
- [`tests/test_track_registry.cpp:3`](../tests/test_track_registry.cpp#L3) — `Unknown`
|
||||||
|
|
||||||
@@ -199,15 +216,22 @@ _None._
|
|||||||
|
|
||||||
### AR-023
|
### AR-023
|
||||||
|
|
||||||
**Locations:** 1
|
**Locations:** 3
|
||||||
|
|
||||||
- [`src/gallery/gallery_calibration.hpp:2`](../src/gallery/gallery_calibration.hpp#L2) — `Unknown`
|
- [`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:110`](../src/nodes/identity_matcher_node.hpp#L110) — `const GalleryCalibration& calibration() const { return cal_; }`
|
||||||
|
|
||||||
### AR-024
|
### AR-024
|
||||||
|
|
||||||
**Locations:** 1
|
**Locations:** 6
|
||||||
|
|
||||||
|
- [`src/config.hpp:97`](../src/config.hpp#L97) — `Unknown`
|
||||||
- [`src/evidence_discount.hpp:2`](../src/evidence_discount.hpp#L2) — `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/main.cpp:199`](../src/main.cpp#L199) — `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) — `const GalleryCalibration& calibration() const { return cal_; }`
|
||||||
|
|
||||||
### AR-025
|
### AR-025
|
||||||
|
|
||||||
@@ -252,7 +276,7 @@ _None._
|
|||||||
- [`src/gallery/gallery_store.cpp:219`](../src/gallery/gallery_store.cpp#L219) — `Unknown`
|
- [`src/gallery/gallery_store.cpp:219`](../src/gallery/gallery_store.cpp#L219) — `Unknown`
|
||||||
- [`src/kpn_bindings.cpp:167`](../src/kpn_bindings.cpp#L167) — `Unknown`
|
- [`src/kpn_bindings.cpp:167`](../src/kpn_bindings.cpp#L167) — `Unknown`
|
||||||
- [`src/kpn_bindings.cpp:217`](../src/kpn_bindings.cpp#L217) — `Unknown`
|
- [`src/kpn_bindings.cpp:217`](../src/kpn_bindings.cpp#L217) — `Unknown`
|
||||||
- [`src/main.cpp:176`](../src/main.cpp#L176) — `Unknown`
|
- [`src/main.cpp:174`](../src/main.cpp#L174) — `Unknown`
|
||||||
- [`src/nodes/embedding_dump_node.hpp:30`](../src/nodes/embedding_dump_node.hpp#L30) — `static constexpr std::string_view label() { return "embedding_dump"; }`
|
- [`src/nodes/embedding_dump_node.hpp:30`](../src/nodes/embedding_dump_node.hpp#L30) — `static constexpr std::string_view label() { return "embedding_dump"; }`
|
||||||
- [`src/nodes/embedding_dump_node.hpp:102`](../src/nodes/embedding_dump_node.hpp#L102) — `H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);`
|
- [`src/nodes/embedding_dump_node.hpp:102`](../src/nodes/embedding_dump_node.hpp#L102) — `H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);`
|
||||||
- [`src/scene_preview.cpp:133`](../src/scene_preview.cpp#L133) — `int main(int argc, char** argv)`
|
- [`src/scene_preview.cpp:133`](../src/scene_preview.cpp#L133) — `int main(int argc, char** argv)`
|
||||||
@@ -383,7 +407,7 @@ _None._
|
|||||||
- [`src/gallery/gallery_store.cpp:219`](../src/gallery/gallery_store.cpp#L219) — `Unknown`
|
- [`src/gallery/gallery_store.cpp:219`](../src/gallery/gallery_store.cpp#L219) — `Unknown`
|
||||||
- [`src/kpn_bindings.cpp:167`](../src/kpn_bindings.cpp#L167) — `Unknown`
|
- [`src/kpn_bindings.cpp:167`](../src/kpn_bindings.cpp#L167) — `Unknown`
|
||||||
- [`src/kpn_bindings.cpp:217`](../src/kpn_bindings.cpp#L217) — `Unknown`
|
- [`src/kpn_bindings.cpp:217`](../src/kpn_bindings.cpp#L217) — `Unknown`
|
||||||
- [`src/main.cpp:176`](../src/main.cpp#L176) — `Unknown`
|
- [`src/main.cpp:174`](../src/main.cpp#L174) — `Unknown`
|
||||||
- [`src/nodes/embedding_dump_node.hpp:30`](../src/nodes/embedding_dump_node.hpp#L30) — `static constexpr std::string_view label() { return "embedding_dump"; }`
|
- [`src/nodes/embedding_dump_node.hpp:30`](../src/nodes/embedding_dump_node.hpp#L30) — `static constexpr std::string_view label() { return "embedding_dump"; }`
|
||||||
- [`src/nodes/embedding_dump_node.hpp:102`](../src/nodes/embedding_dump_node.hpp#L102) — `H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);`
|
- [`src/nodes/embedding_dump_node.hpp:102`](../src/nodes/embedding_dump_node.hpp#L102) — `H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);`
|
||||||
- [`src/scene_preview.cpp:133`](../src/scene_preview.cpp#L133) — `int main(int argc, char** argv)`
|
- [`src/scene_preview.cpp:133`](../src/scene_preview.cpp#L133) — `int main(int argc, char** argv)`
|
||||||
@@ -423,12 +447,17 @@ _None._
|
|||||||
|
|
||||||
### SR-002
|
### SR-002
|
||||||
|
|
||||||
**Locations:** 5
|
**Locations:** 10
|
||||||
|
|
||||||
|
- [`src/config.hpp:97`](../src/config.hpp#L97) — `Unknown`
|
||||||
- [`src/evidence_discount.hpp:2`](../src/evidence_discount.hpp#L2) — `Unknown`
|
- [`src/evidence_discount.hpp:2`](../src/evidence_discount.hpp#L2) — `Unknown`
|
||||||
- [`src/face_utils.hpp:2`](../src/face_utils.hpp#L2) — `inline cv::Mat align_face(const cv::Mat& img,`
|
- [`src/face_utils.hpp:2`](../src/face_utils.hpp#L2) — `inline cv::Mat align_face(const cv::Mat& img,`
|
||||||
- [`src/gallery/gallery_calibration.hpp:2`](../src/gallery/gallery_calibration.hpp#L2) — `Unknown`
|
- [`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/main.cpp:199`](../src/main.cpp#L199) — `reg_cfg, EvidenceDiscounter(same_person));`
|
||||||
- [`src/nodes/face_detector_node.hpp:2`](../src/nodes/face_detector_node.hpp#L2) — `Unknown`
|
- [`src/nodes/face_detector_node.hpp:2`](../src/nodes/face_detector_node.hpp#L2) — `Unknown`
|
||||||
|
- [`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) — `const GalleryCalibration& calibration() const { return cal_; }`
|
||||||
- [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown`
|
- [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown`
|
||||||
|
|
||||||
### SR-003
|
### SR-003
|
||||||
|
|||||||
+7
-1
@@ -16,7 +16,13 @@ enum class Verbosity {
|
|||||||
struct Config {
|
struct Config {
|
||||||
// ── Input ─────────────────────────────────────────────────────────────────
|
// ── Input ─────────────────────────────────────────────────────────────────
|
||||||
std::string movie_path;
|
std::string movie_path;
|
||||||
std::string gallery_path; // gallery.json produced by build_gallery
|
std::string gallery_path;
|
||||||
|
// TRACES: IR-002 | SR-003
|
||||||
|
// "global" (matched against the whole library) or "limited" (this title's
|
||||||
|
// credited cast only). The strongest single quality signal when two
|
||||||
|
// manifests compete for the same cut: identical gallery_size can mean very
|
||||||
|
// different recall depending on which was used.
|
||||||
|
std::string gallery_scope{"global"}; // gallery.json produced by build_gallery
|
||||||
|
|
||||||
// ── Output ───────────────────────────────────────────────────────────────
|
// ── Output ───────────────────────────────────────────────────────────────
|
||||||
std::string output_path; // annotations.json
|
std::string output_path; // annotations.json
|
||||||
|
|||||||
@@ -208,9 +208,20 @@ int main(int argc, char** argv) {
|
|||||||
|
|
||||||
matcher_fn.set_registry(registry);
|
matcher_fn.set_registry(registry);
|
||||||
|
|
||||||
|
|
||||||
FaceTrackerFunc ftracker_fn{cfg, registry, same_person};
|
FaceTrackerFunc ftracker_fn{cfg, registry, same_person};
|
||||||
SceneTrackerFunc tracker_fn {cfg};
|
SceneTrackerFunc tracker_fn {cfg};
|
||||||
ResultSinkFunc sink_fn {cfg, done};
|
ResultSinkFunc sink_fn {cfg, done};
|
||||||
|
|
||||||
|
/// TRACES: AR-012, AR-016, IR-002, IR-003 | SR-002
|
||||||
|
// A reaped track goes straight to the aggregator, so the registry holds only
|
||||||
|
// live tracks and its size is bounded by concurrent on-screen faces rather
|
||||||
|
// than growing with the film.
|
||||||
|
registry->on_track_dead([&sink_fn](const DeadTrack& d) { sink_fn.add_claim(d); });
|
||||||
|
// AR-016: a film ends with faces on screen and those tracks have not timed
|
||||||
|
// out. Without this flush the closing scene's cast is silently never
|
||||||
|
// emitted — a loss that reads as a recognition miss, not a bookkeeping bug.
|
||||||
|
sink_fn.set_pre_write_hook([registry](double last_ts) { registry->flush(last_ts); });
|
||||||
#ifdef SAE_DEBUG
|
#ifdef SAE_DEBUG
|
||||||
DebugRendererFunc debug_fn {cfg};
|
DebugRendererFunc debug_fn {cfg};
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
/// TRACES: IR-001 | SR-003
|
/// TRACES: IR-001 | SR-003
|
||||||
#include "types.hpp"
|
#include "types.hpp"
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
|
#include "track_registry.hpp"
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -10,6 +11,8 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -43,10 +46,26 @@ using json = nlohmann::json;
|
|||||||
struct ResultSinkFunc {
|
struct ResultSinkFunc {
|
||||||
static constexpr std::string_view label() { return "result_sink"; }
|
static constexpr std::string_view label() { return "result_sink"; }
|
||||||
|
|
||||||
|
/// TRACES: AR-012, AR-017, IR-002 | SR-002, SR-003
|
||||||
|
/// A finished presence claim from the registry. Called from inside the
|
||||||
|
/// registry's reap while it holds its own lock, so this must stay a cheap
|
||||||
|
/// push and must never re-enter the registry.
|
||||||
|
void add_claim(const DeadTrack& d) {
|
||||||
|
if (d.actor_idx < 0) return; // never owned: nothing to claim
|
||||||
|
std::lock_guard<std::mutex> g(claims_mu_);
|
||||||
|
claims_.push_back(d);
|
||||||
|
}
|
||||||
|
|
||||||
ResultSinkFunc(const Config& cfg, std::atomic<bool>& done)
|
ResultSinkFunc(const Config& cfg, std::atomic<bool>& done)
|
||||||
: cfg_(cfg), done_(done)
|
: cfg_(cfg), done_(done)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
/// TRACES: AR-016 | SR-002
|
||||||
|
/// Runs immediately before the output is written, with the last timestamp
|
||||||
|
/// seen. Used to flush tracks still live at EOF, which have not timed out
|
||||||
|
/// and would otherwise never be emitted.
|
||||||
|
void set_pre_write_hook(std::function<void(double)> fn) { pre_write_ = std::move(fn); }
|
||||||
|
|
||||||
void operator()(SceneAnnotation sa) {
|
void operator()(SceneAnnotation sa) {
|
||||||
if (sa.eof) {
|
if (sa.eof) {
|
||||||
flush();
|
flush();
|
||||||
@@ -59,12 +78,20 @@ struct ResultSinkFunc {
|
|||||||
<< " unknowns=" << count_unknown(sa.visible_actors)
|
<< " unknowns=" << count_unknown(sa.visible_actors)
|
||||||
<< std::flush;
|
<< std::flush;
|
||||||
|
|
||||||
|
for (const auto& ia : sa.visible_actors) {
|
||||||
|
if (ia.actor_idx < 0) continue;
|
||||||
|
auto& m = actor_meta_[ia.actor_idx];
|
||||||
|
if (m.name.empty())
|
||||||
|
m = {ia.name, ia.imdb_id, ia.tmdb_id, ia.jellyfin_id};
|
||||||
|
}
|
||||||
|
last_ts_ = sa.timestamp_sec;
|
||||||
frames_.push_back(std::move(sa));
|
frames_.push_back(std::move(sa));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write accumulated results and signal done. Safe to call more than once.
|
// Write accumulated results and signal done. Safe to call more than once.
|
||||||
void flush() {
|
void flush() {
|
||||||
if (written_.exchange(true)) return;
|
if (written_.exchange(true)) return;
|
||||||
|
if (pre_write_) pre_write_(last_ts_);
|
||||||
write_output();
|
write_output();
|
||||||
done_.store(true, std::memory_order_release);
|
done_.store(true, std::memory_order_release);
|
||||||
}
|
}
|
||||||
@@ -72,7 +99,7 @@ struct ResultSinkFunc {
|
|||||||
private:
|
private:
|
||||||
// Bump when the minimal/standard output JSON structure changes in a way
|
// Bump when the minimal/standard output JSON structure changes in a way
|
||||||
// the Jellyfin plugin needs to detect.
|
// the Jellyfin plugin needs to detect.
|
||||||
static constexpr int kSchemaVersion = 1;
|
static constexpr int kSchemaVersion = 2; // SR-003 coordinated bump
|
||||||
|
|
||||||
static int count_known(const std::vector<IdentifiedActor>& v) {
|
static int count_known(const std::vector<IdentifiedActor>& v) {
|
||||||
int n = 0;
|
int n = 0;
|
||||||
@@ -92,10 +119,19 @@ private:
|
|||||||
if (cfg_.verbosity == Verbosity::xray) {
|
if (cfg_.verbosity == Verbosity::xray) {
|
||||||
root = build_xray();
|
root = build_xray();
|
||||||
} else {
|
} else {
|
||||||
|
/// TRACES: IR-002 | SR-003
|
||||||
|
/// schema_version 2, per jRay/SPEC.md JR-002. anneal_sec is REMOVED
|
||||||
|
/// rather than zeroed: a field naming a mechanism the pipeline no
|
||||||
|
/// longer has is actively misleading, and would outlive everyone who
|
||||||
|
/// remembers why it reads 0. extinction_sec succeeds it as the
|
||||||
|
/// parameter that actually shapes window extent.
|
||||||
root["schema_version"] = kSchemaVersion;
|
root["schema_version"] = kSchemaVersion;
|
||||||
root["movie"] = cfg_.movie_path;
|
root["movie"] = cfg_.movie_path;
|
||||||
root["sample_fps"] = cfg_.sample_fps;
|
root["extraction"] = {
|
||||||
root["anneal_sec"] = cfg_.anneal_sec;
|
{"sample_fps", cfg_.sample_fps},
|
||||||
|
{"extinction_sec", cfg_.track_extinction_sec},
|
||||||
|
{"gallery_scope", cfg_.gallery_scope},
|
||||||
|
};
|
||||||
root["actors"] = build_epochs();
|
root["actors"] = build_epochs();
|
||||||
if (cfg_.verbosity == Verbosity::standard)
|
if (cfg_.verbosity == Verbosity::standard)
|
||||||
root["frames"] = build_standard();
|
root["frames"] = build_standard();
|
||||||
@@ -110,42 +146,45 @@ private:
|
|||||||
std::cerr << "[result_sink] done.\n";
|
std::cerr << "[result_sink] done.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Window {
|
||||||
|
double start{0.0};
|
||||||
|
double end{0.0};
|
||||||
|
float belief{0.f}; ///< the posterior that justified the claim (AR-017)
|
||||||
|
};
|
||||||
struct ActorWindow {
|
struct ActorWindow {
|
||||||
std::string name, imdb_id, tmdb_id, jellyfin_id;
|
std::string name, imdb_id, tmdb_id, jellyfin_id;
|
||||||
std::vector<std::pair<double, double>> scenes; // [start_sec, end_sec]
|
std::vector<Window> scenes;
|
||||||
};
|
};
|
||||||
|
struct ActorMeta { std::string name, imdb_id, tmdb_id, jellyfin_id; };
|
||||||
|
|
||||||
// Core logic: merge per-frame detections into annealed [start, end] windows.
|
// Core logic: merge per-frame detections into annealed [start, end] windows.
|
||||||
|
/// TRACES: AR-012, IR-002 | SR-002
|
||||||
|
/// A claim already IS a window — `[first_seen, last_seen]` of a track the
|
||||||
|
/// actor owned. There is no annealing pass: `anneal_sec` existed to bridge
|
||||||
|
/// gaps between isolated accepted frames, and a track that survives its own
|
||||||
|
/// gaps leaves it nothing to do (see the AR-012 withdrawal note).
|
||||||
std::vector<ActorWindow> build_actor_windows() {
|
std::vector<ActorWindow> build_actor_windows() {
|
||||||
struct Info { std::string name, imdb_id, tmdb_id, jellyfin_id; };
|
std::lock_guard<std::mutex> g(claims_mu_);
|
||||||
std::map<int, Info> actor_info;
|
|
||||||
std::map<int, std::vector<double>> timestamps;
|
|
||||||
|
|
||||||
for (const auto& frame : frames_) {
|
std::map<int, ActorWindow> by_actor;
|
||||||
for (const auto& ia : frame.visible_actors) {
|
for (const auto& c : claims_) {
|
||||||
if (ia.actor_idx < 0) continue;
|
auto& aw = by_actor[c.actor_idx];
|
||||||
actor_info[ia.actor_idx] = {ia.name, ia.imdb_id, ia.tmdb_id, ia.jellyfin_id};
|
if (aw.name.empty()) {
|
||||||
timestamps[ia.actor_idx].push_back(frame.timestamp_sec);
|
auto it = actor_meta_.find(c.actor_idx);
|
||||||
|
if (it != actor_meta_.end()) {
|
||||||
|
aw.name = it->second.name;
|
||||||
|
aw.imdb_id = it->second.imdb_id;
|
||||||
|
aw.tmdb_id = it->second.tmdb_id;
|
||||||
|
aw.jellyfin_id = it->second.jellyfin_id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
aw.scenes.push_back({c.first_seen, c.last_seen, c.belief});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ActorWindow> result;
|
std::vector<ActorWindow> result;
|
||||||
for (auto& [idx, ts_vec] : timestamps) {
|
for (auto& [idx, aw] : by_actor) {
|
||||||
ActorWindow aw;
|
std::sort(aw.scenes.begin(), aw.scenes.end(),
|
||||||
aw.name = actor_info[idx].name;
|
[](const Window& a, const Window& b) { return a.start < b.start; });
|
||||||
aw.imdb_id = actor_info[idx].imdb_id;
|
|
||||||
aw.tmdb_id = actor_info[idx].tmdb_id;
|
|
||||||
aw.jellyfin_id = actor_info[idx].jellyfin_id;
|
|
||||||
|
|
||||||
double win_start = ts_vec[0], win_end = ts_vec[0];
|
|
||||||
for (size_t i = 1; i < ts_vec.size(); ++i) {
|
|
||||||
if (ts_vec[i] - win_end > cfg_.anneal_sec) {
|
|
||||||
aw.scenes.push_back({win_start, win_end});
|
|
||||||
win_start = ts_vec[i];
|
|
||||||
}
|
|
||||||
win_end = ts_vec[i];
|
|
||||||
}
|
|
||||||
aw.scenes.push_back({win_start, win_end});
|
|
||||||
result.push_back(std::move(aw));
|
result.push_back(std::move(aw));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -154,9 +193,16 @@ private:
|
|||||||
json build_epochs() {
|
json build_epochs() {
|
||||||
json actors = json::array();
|
json actors = json::array();
|
||||||
for (const auto& aw : build_actor_windows()) {
|
for (const auto& aw : build_actor_windows()) {
|
||||||
|
// Objects, not float pairs: a window carries the belief that
|
||||||
|
// justified it and the route by which it was identified (AR-017),
|
||||||
|
// so a consumer can caveat or filter rather than treating every
|
||||||
|
// window as equally certain.
|
||||||
json windows = json::array();
|
json windows = json::array();
|
||||||
for (const auto& [s, e] : aw.scenes)
|
for (const auto& w : aw.scenes)
|
||||||
windows.push_back({s, e});
|
windows.push_back({{"start", w.start},
|
||||||
|
{"end", w.end},
|
||||||
|
{"belief", w.belief},
|
||||||
|
{"route", "live"}});
|
||||||
json ja;
|
json ja;
|
||||||
ja["name"] = aw.name;
|
ja["name"] = aw.name;
|
||||||
ja["imdb_id"] = aw.imdb_id;
|
ja["imdb_id"] = aw.imdb_id;
|
||||||
@@ -174,9 +220,9 @@ private:
|
|||||||
json build_xray() {
|
json build_xray() {
|
||||||
std::map<int, std::vector<std::string>> xray;
|
std::map<int, std::vector<std::string>> xray;
|
||||||
for (const auto& aw : build_actor_windows()) {
|
for (const auto& aw : build_actor_windows()) {
|
||||||
for (const auto& [start, end] : aw.scenes) {
|
for (const auto& w : aw.scenes) {
|
||||||
int t0 = static_cast<int>(std::floor(start));
|
int t0 = static_cast<int>(std::floor(w.start));
|
||||||
int t1 = static_cast<int>(std::ceil(end));
|
int t1 = static_cast<int>(std::ceil(w.end));
|
||||||
for (int t = t0; t <= t1; ++t)
|
for (int t = t0; t <= t1; ++t)
|
||||||
xray[t].push_back(aw.name);
|
xray[t].push_back(aw.name);
|
||||||
}
|
}
|
||||||
@@ -227,4 +273,9 @@ private:
|
|||||||
std::atomic<bool>& done_;
|
std::atomic<bool>& done_;
|
||||||
std::atomic<bool> written_{false};
|
std::atomic<bool> written_{false};
|
||||||
std::vector<SceneAnnotation> frames_;
|
std::vector<SceneAnnotation> frames_;
|
||||||
|
std::function<void(double)> pre_write_;
|
||||||
|
double last_ts_{0.0};
|
||||||
|
std::mutex claims_mu_;
|
||||||
|
std::vector<DeadTrack> claims_;
|
||||||
|
std::map<int, ActorMeta> actor_meta_; ///< actor_idx → identity keys
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user