feat(audio): store, serve and match the v1 audio signature

Completes UR-009. The register recorded the signature as stored; it was
not. `ingest` validated `cut.audio_signature` and wrote NULL, so every
served manifest came back without one — which also meant the plugin's own
alignment (jRay JR-047, Done) had nothing to align against and could never
run. That is the failure mode a status field is least able to catch: every
validation test passed and the feature delivered nothing.

Now stored, coarse-indexed and served back byte-identically, with a held
manifest adopting an incoming signature it lacked (§9a). `audio`-tier
matching runs on every read endpoint via an `audio_signature` parameter,
and `POST /manifests/search` answers the unknown-providence case with a
runtime prefilter, a bounded scan and honest truncation reporting.

Three rules §3 did not previously state, now normative:

- **±1 frame of slack in the score.** scene-actor-extraction VR-014
  measured the exact-frame rule demoting 27 of 40 correctly aligned
  releases to `loose`, because the two windows are cut on their own
  file's frame grid and those grids do not coincide. With ±1 frame all
  40 reach `audio` (worst 0.906) and the strongest false match is
  unmoved at 0.16.
- **The offset has two terms.** Both windows are anchored at their own
  file's runtime/2, so the slide alone is wrong by half the runtime
  difference on every shifted release. A signature without a runtime
  therefore cannot align, and is refused by name rather than answered
  at a lower tier.
- **A signature verdict is final**, including its refusals. Falling back
  to the runtime tier after the audio declined would let a coincidence
  overturn direct evidence, inverting the ordering the tier table exists
  to state.

The slide precomputes each frame's neighbourhood as a 32-bit bin set
rather than re-deriving it across 1201 slides — 3.3 ms to 1.1 ms per
candidate, with a test asserting exact equivalence to the rule written
the obvious way. The 1000-candidate search cap follows from that
measurement as a ~1.1 s ceiling per request, not a round number.

jRay's matcher still implements the pre-slack rule and will label some
alignments `loose` that this server calls `audio`. Nothing misaligns —
JR-047 makes the local answer win — but that register now carries the
follow-up.

TRACES: UR-008, UR-009 | SR-003
This commit is contained in:
2026-07-31 22:43:26 +02:00
parent 7eb5c175af
commit c41253ef5c
24 changed files with 2288 additions and 148 deletions
+25 -7
View File
@@ -35,7 +35,7 @@ requirement and no fixture-generation step, unlike `scene-actor-extraction`.
| UR-006 | Serve and accept a whole series in one operation | PR-006 | High | Done |
| UR-007 | Plugin queries an ordered, configurable list of servers | PR-005 | High | In Progress |
| UR-008 | Servers replicate manifests between each other | PR-006 | Medium | Done |
| UR-009 | Store an audio spectral-peak signature for content-based identification | SR-003 | Medium | In Progress |
| UR-009 | Store an audio spectral-peak signature for content-based identification | SR-003 | Medium | Done |
| UR-010 | Identity crossing the API boundary is TMDB/IMDB ids, never a name alone | SR-001 | High | Done |
| UR-011 | Reject any field capable of carrying binary or attacker-chosen content | SR-004 | High | Done |
| UR-012 | Never accept, store, or serve gallery data — reference faces or embeddings | SR-005 | High | Done |
@@ -68,11 +68,29 @@ a peer — is deliberately not an API: §9a requires that a peering exist only
because an operator typed a URL, so it is a database action, and
`there_is_no_endpoint_that_creates_a_peering` asserts the absence.
**UR-009 is `In Progress`.** The server accepts, validates and stores
`cut.audio_signature`, and `content_id` correctly excludes it (§9a). What is
absent is `audio`-tier matching and `POST /manifests/search`. This is the
sequencing §3 recommends — accumulate signatures first, enable matching once
coverage is useful — not an oversight.
**UR-009 is `Done`.** The server accepts, validates, stores and serves
`cut.audio_signature`; `content_id` correctly excludes it (§9a), and a held
manifest lacking one adopts an incoming signature rather than discarding it.
`audio`-tier matching runs on every read endpoint, and `POST /manifests/search`
answers the unknown-providence case.
> **The register previously recorded this row as storing the signature, and it
> did not.** `ingest` validated `cut.audio_signature` and then wrote `NULL`, so
> every served manifest came back without one — which also meant the plugin's
> own alignment (`jRay` JR-047, `Done`) had nothing to align against and could
> never run. Recorded here because it is the failure mode a status field is
> least able to catch: every validation test passed, and the feature delivered
> nothing.
**§3's scoring rule changed with this row.** A frame now agrees within ±1
frame rather than exactly, on `scene-actor-extraction` VR-014's measurement —
the exact rule demoted 27 of 40 correctly aligned releases to `loose` because
the two windows' frame grids do not coincide. **`jRay`'s `AudioSignatureMatcher`
still implements the pre-change rule**, so the plugin will label some alignments
`loose` that this server calls `audio`. Its local answer supersedes the server's
on the fetch path (JR-047), so nothing is misaligned by the divergence — but the
two are now out of step with §3, and the plugin register should carry the
follow-up.
**UR-012 is satisfied structurally, by absence.** There is no field in the
Jmanifest capable of carrying an embedding or a crop, and no endpoint that would
@@ -144,7 +162,7 @@ topology is the point, so this is a deliberate choice rather than an oversight.
| UR-006 | T2 | Bundle accepted per-episode, non-atomically | One bad episode rejected while its neighbours are accepted; envelope errors are whole-request `400` |
| UR-007 | **external** | *No server-side test, and cannot have one.* The obligation is the plugin's: `jRay` JR-025, which is `Done` and tagged in that register | Verified there, not here — counted as covered by cross-reference, never by a test in this repo. **See the status note: JR-025 being `Done` does not by itself close UR-007**, because the fetch path (`jRay` JR-031) is still `Planned` |
| UR-008 | T1 + T2 | Feed, fetch-by-hash, batch have, peer directory | **Cursor is strictly monotonic** — a ULID would sort out of write order within a millisecond and silently skip entries; a peer retraction flags rather than delists; only the opt-in abuse channel delists; `pending` is never replicated; **no endpoint can create a peering** |
| UR-009 | T1 | Signature structurally validated | Fixed length; reserved high bit; **media < 120 s must send no signature at all** |
| UR-009 | T1 + T2 | Signature validated, stored, served back, and matched on | Fixed length; reserved high bit; **media < 120 s must send no signature at all**; a `v2:` payload is refused rather than parsed; **the served signature is byte-identical to the contributed one** — a signature that is validated and then dropped passes every validation test and delivers nothing; a slide past the ±600-frame cap and unrelated content are both *declined*, never given a best-effort alignment; **the offset carries the window-anchor term**, not the slide alone; a signature disagreement is not overturned by a runtime coincidence; a search never returns a `pending` manifest |
| UR-010 | T1 + T2 | Actors persist as TMDB person ids | A name the upload invented does not round-trip |
| UR-011 | T2 | Every payload-shaped field rejected | base64, hex, markup, control characters, bidi overrides, compatibility homoglyphs |
| UR-012 | T2 | No endpoint accepts embeddings or image data | An `embedding` or `crop` field is an unknown-field `400` |