feat(truth): schema_version 2 read path, v2 only

Replaces the v1 shape rather than accepting both. `anneal_sec` and the
top-level `sample_fps` are deleted, not zeroed; `extraction` and `cut`
blocks arrive; `scenes` become objects carrying belief and route, so a
window records how far to trust it instead of being a bare float pair.

`TruthSchema.IsSupported` is the single gate and is applied on all four
read paths — sidecar, managed store load, managed PUT, and converted
manifest. Previously only the controller checked, so the version the
plugin claimed to require and the one it would actually parse were free
to drift. Rejections name the file and the version found, so an item that
looks empty is distinguishable from one that was refused.

`ManifestConverter` carries belief, route and both provenance blocks
through: dropping them would silently downgrade every fetched manifest
against a locally extracted one.

TRACES: JR-002, JR-003 | SR-003
This commit is contained in:
2026-07-31 16:23:58 +02:00
parent 596566813c
commit 19aecee646
15 changed files with 583 additions and 46 deletions
+24 -11
View File
@@ -5,7 +5,7 @@ namespace Jellyfin.Plugin.JRay.Models;
/// <summary>
/// Root object of a scene-actor-extraction "truth" file
/// (schema_version 1, minimal verbosity). See SPEC.md §1.
/// (<c>schema_version</c> 2). See SPEC.md §1.
/// </summary>
/// <remarks>
/// JRay <b>owns</b> this format; the extraction pipeline is its producer and the
@@ -13,15 +13,22 @@ namespace Jellyfin.Plugin.JRay.Models;
/// independently, breaking changes are batched into one coordinated
/// <c>schema_version</c> bump rather than made piecemeal.
///
/// This type is still the v1 shape. JR-002 replaces it: <c>anneal_sec</c> out,
/// an <c>extraction</c> provenance block and a <c>cut</c> block in, and
/// <c>scenes</c> becoming objects that carry belief and identification route.
/// <para>
/// <b>This is the v2 shape, and v1 is gone rather than deprecated.</b> The bump
/// removed <c>anneal_sec</c>, moved <c>sample_fps</c> into
/// <see cref="TruthExtraction"/>, added <see cref="TruthCut"/>, and turned
/// <c>scenes</c> from float pairs into <see cref="TruthScene"/> objects carrying
/// belief and route. Nothing here reads a v1 file: see
/// <see cref="Services.TruthSchema"/> for why that is a decision rather than an
/// omission.
/// </para>
/// </remarks>
// TRACES: JR-001, JR-002 | SR-003
public class TruthFile
{
/// <summary>
/// Gets or sets the schema version of this file.
/// Gets or sets the schema version of this file. Only
/// <see cref="Services.TruthSchema.SupportedVersion"/> is accepted.
/// </summary>
[JsonPropertyName("schema_version")]
public int SchemaVersion { get; set; }
@@ -29,20 +36,26 @@ public class TruthFile
/// <summary>
/// Gets or sets the source media path at extraction time (informational).
/// </summary>
/// <remarks>
/// Stripped on contribution (JR-034): it is a contributor's directory
/// layout, which is nobody else's business and identifies them.
/// </remarks>
[JsonPropertyName("movie")]
public string Movie { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the sampling rate (frames per second) used during extraction.
/// Gets or sets extraction provenance, or <c>null</c> when the producer
/// recorded none.
/// </summary>
[JsonPropertyName("sample_fps")]
public double SampleFps { get; set; }
[JsonPropertyName("extraction")]
public TruthExtraction? Extraction { get; set; }
/// <summary>
/// Gets or sets the gap (seconds) below which consecutive detections were merged into one scene.
/// Gets or sets which encode the timings apply to, or <c>null</c> when the
/// producer recorded none.
/// </summary>
[JsonPropertyName("anneal_sec")]
public double AnnealSec { get; set; }
[JsonPropertyName("cut")]
public TruthCut? Cut { get; set; }
/// <summary>
/// Gets the list of actors in the film, each with their scene-presence windows.