feat(audio): align a fetched manifest to the local file before storing

The signature had a producer and a reader but no consumer, so nothing
ever fingerprinted anything. `ManifestAligner` runs on the fetch path,
before the windows are stored.

A local alignment supersedes the server's offset. The server has never
seen this file — its offset is a runtime-difference inference at best,
while the local comparison is against the media the windows will actually
be drawn over. It also needs no round trip, so no signature leaves the
instance. This is what jRay's spec already meant by matching being a
consumer concern: the server never rewrites a manifest, so one stored
manifest serves every trim of the same cut.

The offset has two terms and only one is in the server's pseudocode. Both
windows are centred on their own file's midpoint, so unequal runtimes
start them at different absolute times; a release with 40 s of extra head
material recovers 20 s from the slide and 20 s from the anchor
difference. Using the slide alone is wrong by half the runtime difference
on every shifted release.

Degradation, never failure. Signatures off, no manifest signature, media
under the window, a `v2:` producer, a missing binary, a decode error —
each applies the server's offset rather than refusing, because a
signature is an enhancement to cut matching and must never break a fetch.

"Un-comparable" and "does not match" are kept distinct, which a test
caught: `Compare` returns null for both, and conflating them would report
a 90-second extra as content disagreeing with its own manifest. A genuine
disagreement is stored anyway — the audio may legitimately differ, a
different language track being the obvious case — and surfaced as a
caveat that outranks the tier's, since it is the stronger statement.

The applied offset, score, slide and the local file's own signature are
written beside the truth file: the offset is otherwise unrecoverable once
the windows are shifted, and the stored signature lets a later fetch
align without decoding again. Provenance is never injected into the truth
file, so the bytes served back stay the producer's (JR-004).

`docs/audio-alignment.md` documents the mechanism end to end.

TRACES: JR-047 | SR-003
This commit is contained in:
2026-07-31 16:52:21 +02:00
parent e8ce779ad3
commit 1e247c4c7d
12 changed files with 981 additions and 29 deletions
+51 -10
View File
@@ -3,7 +3,7 @@
<!-- GENERATED FILE - do not edit by hand. -->
<!-- Regenerate: scripts/traceability/traceability-gate.sh -->
**Generated:** 2026-07-31T14:19:46+00:00
**Generated:** 2026-07-31T14:50:36+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, static`).
@@ -11,13 +11,13 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev
| Metric | Value |
|---|---|
| Source files scanned | 74 |
| TRACES tags found | 45 |
| Source files scanned | 78 |
| TRACES tags found | 48 |
| EXCEPTION tags found | 0 |
| Requirements defined | 46 |
| Requirements covered | 37 |
| **Coverage** | **80.4%** (37/46) |
| Coverage of CI-executable scope | 84.1% (37/44) |
| Requirements defined | 47 |
| Requirements covered | 38 |
| **Coverage** | **80.9%** (38/47) |
| Coverage of CI-executable scope | 84.4% (38/45) |
| Tagged but unexecuted in CI | 1 |
| Orphan tags | 0 |
@@ -25,9 +25,9 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev
| Type | Covered | Tagged but unexecuted | Defined |
|---|---|---|---|
| JR | 37 | 1 | 46 |
| JR | 38 | 1 | 47 |
- **UT** tags present (separate taxonomy, not counted in coverage): UT-001, UT-002, UT-003, UT-004, UT-005, UT-007, UT-008, UT-009, UT-010, UT-011, UT-012, UT-013, UT-014, UT-015, UT-016, UT-017, UT-018, UT-019, UT-020, UT-021, UT-022, UT-023, UT-024, UT-025, UT-026, UT-027, UT-028, UT-029, UT-030, UT-031, UT-032, UT-033, UT-034, UT-035, UT-036, UT-037, UT-038, UT-039, UT-040, UT-041, UT-042, UT-043, UT-044, UT-045, UT-046, UT-047, UT-048, UT-049, UT-050, UT-051, UT-052
- **UT** tags present (separate taxonomy, not counted in coverage): UT-001, UT-002, UT-003, UT-004, UT-005, UT-007, UT-008, UT-009, UT-010, UT-011, UT-012, UT-013, UT-014, UT-015, UT-016, UT-017, UT-018, UT-019, UT-020, UT-021, UT-022, UT-023, UT-024, UT-025, UT-026, UT-027, UT-028, UT-029, UT-030, UT-031, UT-032, UT-033, UT-034, UT-035, UT-036, UT-037, UT-038, UT-039, UT-040, UT-041, UT-042, UT-043, UT-044, UT-045, UT-046, UT-047, UT-048, UT-049, UT-050, UT-051, UT-052, UT-053, UT-054, UT-055, UT-056, UT-057
- **PR** tags present (separate taxonomy, not counted in coverage): PR-001, PR-003, PR-004, PR-005, PR-006
- **SR** tags present (separate taxonomy, not counted in coverage): SR-001, SR-002, SR-003, SR-004, SR-005
@@ -110,6 +110,7 @@ _None._
| JR-044 | **Done** (UT-045, U… | T1 | SR-003 | covered | `Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs`, `Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs` | Media shorter than 120 s: emit no signature and apply no sync offset … |
| JR-045 | **Done** (UT-047…04… | T1 | SR-003 | covered | `Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs`, `Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs` | Emit and honour the signature's own `v1:` prefix, so a DSP change is … |
| JR-046 | **TBD** | T2, T4 | [system §4](../scripts/vendor/jray-proj… | untagged | - | Review UI for unidentified track clusters: show context crops, pick f… |
| JR-047 | **Done** (UT-053…05… | T1 | SR-003 | covered | `Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs`, `Jellyfin.Plugin.JRay/Models/TruthAlignment.cs`, `Jellyfin.Plugin.JRay/Services/ManifestAligner.cs` | **A fetched manifest is aligned against the local file before its win… |
## Detailed mapping
@@ -384,6 +385,14 @@ _None._
- [`Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs:24`](../Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs#L24) — `public static class AudioSignatureMatcher`
- [`Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs:26`](../Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs#L26) — `public class AudioSignatureMatcherTests`
### JR-047
**Locations:** 3
- [`Jellyfin.Plugin.JRay/Models/TruthAlignment.cs:17`](../Jellyfin.Plugin.JRay/Models/TruthAlignment.cs#L17) — `public class TruthAlignment`
- [`Jellyfin.Plugin.JRay/Services/ManifestAligner.cs:36`](../Jellyfin.Plugin.JRay/Services/ManifestAligner.cs#L36) — `public class ManifestAligner`
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`
### PR-001
**Locations:** 3
@@ -449,10 +458,11 @@ _None._
### SR-003
**Locations:** 11
**Locations:** 13
- [`Jellyfin.Plugin.JRay/Controllers/TruthController.cs:28`](../Jellyfin.Plugin.JRay/Controllers/TruthController.cs#L28) — `public class TruthController : ControllerBase`
- [`Jellyfin.Plugin.JRay/Models/Jmanifest.cs:21`](../Jellyfin.Plugin.JRay/Models/Jmanifest.cs#L21) — `public class Jmanifest`
- [`Jellyfin.Plugin.JRay/Models/TruthAlignment.cs:17`](../Jellyfin.Plugin.JRay/Models/TruthAlignment.cs#L17) — `public class TruthAlignment`
- [`Jellyfin.Plugin.JRay/Models/TruthCut.cs:15`](../Jellyfin.Plugin.JRay/Models/TruthCut.cs#L15) — `public class TruthCut`
- [`Jellyfin.Plugin.JRay/Models/TruthExtraction.cs:21`](../Jellyfin.Plugin.JRay/Models/TruthExtraction.cs#L21) — `public class TruthExtraction`
- [`Jellyfin.Plugin.JRay/Models/TruthFile.cs:26`](../Jellyfin.Plugin.JRay/Models/TruthFile.cs#L26) — `public class TruthFile`
@@ -460,6 +470,7 @@ _None._
- [`Jellyfin.Plugin.JRay/Services/AudioSignature.cs:49`](../Jellyfin.Plugin.JRay/Services/AudioSignature.cs#L49) — `public static class AudioSignature`
- [`Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs:24`](../Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs#L24) — `public static class AudioSignatureMatcher`
- [`Jellyfin.Plugin.JRay/Services/AudioSignatureService.cs:39`](../Jellyfin.Plugin.JRay/Services/AudioSignatureService.cs#L39) — `public class AudioSignatureService`
- [`Jellyfin.Plugin.JRay/Services/ManifestAligner.cs:36`](../Jellyfin.Plugin.JRay/Services/ManifestAligner.cs#L36) — `public class ManifestAligner`
- [`Jellyfin.Plugin.JRay/Services/ManifestConverter.cs:11`](../Jellyfin.Plugin.JRay/Services/ManifestConverter.cs#L11) — `public static class ManifestConverter`
- [`Jellyfin.Plugin.JRay/Services/TruthSchema.cs:35`](../Jellyfin.Plugin.JRay/Services/TruthSchema.cs#L35) — `public static class TruthSchema`
@@ -783,3 +794,33 @@ _None._
- [`Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs:26`](../Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs#L26) — `public class AudioSignatureMatcherTests`
### UT-053
**Locations:** 1
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`
### UT-054
**Locations:** 1
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`
### UT-055
**Locations:** 1
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`
### UT-056
**Locations:** 1
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`
### UT-057
**Locations:** 1
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`