f891e579c5f1286aa11a5612c741dfb9db808865
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cc1bed92d8 |
perf: back the CPU similarity GEMM with OpenBLAS
The CPU path was a scalar triple loop. It is the correctness oracle for the GPU backends, but it is also what CI runs — there is no GPU on the N100 host — and since AR-003 removed the per-frame face cap, a crowded frame now scores many faces against a library-scale gallery. Scoring one face against 5000 embeddings is 2.6 MFLOP; in scalar that does not hold up (AR-027). S(g,f) viewed as row-major [n_faces x n_gallery] is exactly query * gallery^T, so the loop nest collapses into a single cblas_sgemm. OpenBLAS is optional in the build: found via pkg-config, and the scalar path remains when it is absent so no hard dependency is added and the two can be diffed when a similarity looks wrong. The configure step warns rather than failing, since a developer without it should still get a working tree. The test target links it too. Without that the suite compiles the scalar fallback while the builder image ships CBLAS, so CI would be verifying a kernel that is not the one running in production — the same class of mistake as testing a path the gate never executes. Recorded as required (not optional) in the DP-007 image, for the same reason. Suite: 92 cases, 6136 assertions, with CBLAS compiled in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: AR-026, AR-027, DP-007 | SR-001 |
||
|
|
61e487fbee |
test: replay the real tracker and registry from committed fixtures
Tier T2 — composition rather than units. The registry tests construct awkward states directly; these feed the pieces real 480x360 footage with the cuts, gaps and crowded frames that synthetic input does not produce. Six cases: - fixture integrity: exact frame and face counts, contiguous face_offset, and the embedder identity each dump carries (GR-004). The counts are asserted exactly rather than approximately, which was impossible before AR-004 — what a lossy run dropped depended on timing. - determinism: replaying a fixture twice gives identical track ids and windows. This is the property the whole fixture strategy rests on; without it every golden output derived from a fixture is unreliable and the CI replay tier is worthless. - every face is assigned a track, and flush leaves nothing open — a track still live at EOF is a window that never reaches the output. - windows are well-formed and inside the clip. A window ends at the last sighting, so it can never extend past the footage that produced it. - a longer extinction window yields fewer, longer tracks. On the sparse fixture (140 faces over 385 frames) that is the difference the constant actually makes: absorbing a gap versus splitting a window. - the cut-heavy fixture still contains cuts. This guards the corpus, not the code: a regeneration that produced cut-free fixtures would leave the association tests passing while silently testing nothing. Driving the functors directly rather than through a KPN network is deliberate — no threads, no channels, no scheduling, so the same input gives the same output. Suite: 86 cases, 6106 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: AR-004, AR-012, AR-013, VR-001, VR-002 | SR-002 |
||
|
|
f0c7126f80 |
feat: TrackRegistry — presence follows track extent
The spine of the redesign. Presence is now the extent of a track an actor owns, [first_seen, last_seen], rather than the subset of frames in which recognition happened to succeed. An actor recognised only at the end of a long track is present for all of it, which is what the scene-scoped ground truth actually records. AR-013 — `last_seen` as an optional carries the entire liveness state: unset means on screen, set means went off at that timestamp and still revivable, reaped means emitted and erased. No missing-frame counter, no expired flag. It subsumes the tracker's existing two-pool split, so there is no separate revival path — matching a dormant track is ordinary inter-frame association. The asymmetry is the point: interior gaps are claimed, the trailing cool-down is not. A face lost and re-associated within the timeout never closed its track, so the gap is presence — someone briefly occluded has not left the scene. But a track that dies ends at its last sighting, never at the death time. That is precisely the over-claim the retired extinction_sec keep-alive produced, where presence ran on into the closing credits. AR-014 — a belief swap A→B closes the track and opens a successor at the swap frame. Not a correction: two non-twins both clearing the threshold on one face is not realistic, whereas a track_id carried across a viewpoint change onto a different person is. Treating it as a swap-and-continue would emit one window blending two people; treating it as a boundary yields two that are each right. AR-015 — two live tracks owned by one actor means at least one is wrong, since a person cannot be in two places at once. A reverse index catches it on the update that causes it rather than by scanning. This makes identity a third cut detector, independent of the histogram and TransNetV2 and firing where those failed. AR-016 — flush() closes tracks still live at EOF. Without it a film ending mid-shot silently drops its closing cast, which presents as a recognition miss rather than a bookkeeping bug. Reaping hands the dead track to the aggregator and erases it, so the registry holds only live tracks and its size is bounded by concurrent on-screen faces rather than growing with the film. Locking: a frame's association pass is atomic as a unit via FrameScope, since per-call locking would let another thread observe a half-updated frame. owner() reads tally and verdict under one lock — separately, a track could be both unowned and owned within a single promotion decision. A vote for an already-reaped track is dropped and counted, because a nonzero count means the timeout is shorter than the matcher's lag. 11 unit tests, driven directly against the registry with no network and no fixture — the awkward cases are constructed rather than hunted for. Suite: 75 cases, 3236 assertions. Coverage 14/63 to 20/63. Not yet wired into FaceTrackerFunc; that is AR-007/AR-008. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: AR-012, AR-013, AR-014, AR-015, AR-016, AR-017 | SR-002 |
||
|
|
908d166173 |
feat: bind galleries to the embedder that built them
GR-004 — a gallery built with one embedding model is meaningless with another. Cosine similarities across models are garbage but look entirely plausible, so this fails silently and expensively; every measurement taken against a mismatched pair would have been quietly wrong. The stamp is the model basename plus a SHA-256 of its bytes, with embed_dim as a cheap extra guard. The hash decides and the name explains, because neither works alone: a name is a promise rather than a fact — models get re-exported in place under an unchanged filename, which is exactly the case where the weights differ and nothing else does — while a bare hash mismatch tells an operator nothing actionable. Mismatch is fatal in every mode with no bypass. Unstamped only warns, because unstamped is unknown rather than known-bad, and an error firing on every legacy gallery trains people to reach for the bypass reflexively. scripts/stamp_gallery.py binds an existing gallery in place with no re-embedding, so the warning is a migration step rather than a permanent state; --require-gallery-stamp promotes it to an error once a site has migrated. Two gaps found that would have defeated the requirement outright: - Embedding dumps carried no stamp, so a replay — which has no live embedder — had nothing to check the gallery against. Dumps now carry embedder_model and embedder_sha256 as root attributes. Additive; schema_version stays 1. This is the same gap the dump audit identified independently. - --merge produced one file holding two embedding spaces, which no later check can untangle. Merge paths now verify before writing. The stamp also survives identity_matcher's calibration write-back, which would otherwise have stripped it on the first analysis run — the check would have worked exactly once. Conflicts resolved additively: both branches appended a source to sae_gallery and to the test target, and both edited the GR-004 register row. Merged suite: 64 cases, 3199 assertions, passing on CPU with no GPU. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: GR-004, VR-001 | SR-001 |
||
|
|
7db40f430d |
GR-004: bind galleries to the embedder that built them
A gallery is only valid for the embedder that produced its vectors. Cosine
similarities across models are meaningless but *look* plausible, so the mistake
is silent and every measurement taken afterwards is suspect. Stamp the embedder
identity into the gallery at build; verify it at every load.
The stamp is the model file's basename plus the SHA-256 of its bytes (plus
embed_dim). The hash decides, the name explains. A name alone is a promise
rather than a fact — models get re-exported and overwritten in place under an
unchanged filename, which is exactly the case where the weights differ and
nothing else does. A hash alone is correct but unactionable in an error message.
SHA-256 is derived from the artefact, needs no registry kept current, and costs
~0.1s for a 250MB ONNX, memoised per process.
Mismatch is a hard error in every mode, with no bypass, naming both sides.
Unstamped legacy galleries warn loudly and proceed: unknown is not known-bad,
and hard-failing every pre-existing gallery would turn the check into something
people disable rather than trust. --require-gallery-stamp (or
SAE_REQUIRE_GALLERY_STAMP=1, which propagates to subprocesses) promotes that to
a hard error — the mode measurement work should run in. scripts/stamp_gallery.py
re-binds an existing gallery with no re-embedding, so "warn" is a cheap state to
leave rather than a permanent one.
Embedding dumps carry the same stamp: a replay has no live embedder, so the dump
is the embedder as far as the gallery is concerned. Derived galleries inherit
their source's stamp; --merge and the JSON gallery merge check before writing,
since one file holding two embedding spaces cannot be untangled afterwards.
Verified in: scene_analyze, scene_preview, the sae_kpn matcher binding,
replay.py, optimize.py (once per film at startup, before the first evaluation),
movienet_eval.py and both merge paths.
Stamp logic lives in src/gallery/embedder_stamp.{hpp,cpp} and its Python twin
scripts/sae_stamp.py, kept dependency-light so replay subprocesses do not pay
sae_gallery's requests/Pillow import to ask whether two models match.
Tests: 12 new cases in test_gallery_store.cpp covering the comparison logic,
both round trips, and the SHA-256 vectors that guarantee the C++ and hashlib
stamps agree. No ONNX or GPU required.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
45ef7c1916 |
Add the v1 audio signature to the pipeline (IR-004, IR-005, IR-007, IR-008)
Implements the content-derived spectral-peak signature from JRay-public-server/SPEC.md §3 so a truth file is self-identifying: 120 s window centred on the media midpoint, mono at 11025 Hz, 4096/1024 Hann STFT, 32 log-spaced bins over 300-3000 Hz, one byte per frame (5-bit peak band + 2-bit energy class), base64, `v1:` prefix. Audio decode is a second stream from the FFmpeg libraries the pipeline already links for video; libswresample is added to the existing ffmpeg_libs interface target. The FFT is written out rather than pulled from a library for the same reason the plugin vendors one: the output has to be bit-identical across two languages, so a dependency whose version could change the numerics is a liability. The server spec fixes the geometry but not enough to reproduce a byte stream — Hann periodicity, band aggregation, the energy-class definition, tie-breaking and the base64 alphabet are all unconstrained by it. Those are pinned in audio_signature.hpp and mirrored in the golden fixture, so the plugin can be implemented from the fixture alone. IR-005: tests/fixtures/audio/ carries a deterministic 120 s tone (FLAC — lossless, so identical PCM to the WAV make_fixture.py emits, and 3.5x smaller in git) plus the signature it must produce, the decoded-PCM checksum and the full parameter contract. That directory is the artefact shared with the plugin repo; the PCM checksum is separate from the signature so a codec-level difference is distinguishable from a DSP one. IR-007: media under 120 s emits no signature. Same for a file with no audio stream or one that will not open — UR-9 is an enhancement and must never be able to break a fetch. Verified against an independent Python reference implementation: same bytes. All 32 bands and all 4 energy classes appear in the golden vector, and the window-centring test wraps the fixture in 90 s of silence either side and requires the golden value back. Not wired into the truth-file output yet — that is the schema_version bump under IR-002/IR-003 and is deliberately out of scope here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
76df2f66aa |
test: add Catch2 unit test suite (gallery, calibration, tracking, similarity)
GPU-free, model-free tests for the pure logic: gallery HDF5 save/load round-trips (actors, embeddings, embedded calibration) and legacy JSON read back-compat; the calibration sigmoid fit, boundary inversion, and the in-memory hash-keyed cache reuse/staleness; TrackGallery's diversity-buffer eviction, novelty/spread safety gates, and promotion; FaceTracker's IoU/ embedding association and cross-cut track revival; and the GEMM similarity backend (forced to CPU so the suite runs without a GPU). Verified: all 39 test cases / 1640 assertions pass (cmake -DSAE_BUILD_TESTS=ON). |