Presence was decided by a LINQ predicate inline in the controller, so the semantics SR-002 sets were nowhere stated in code -- the read path complied by accident rather than by requirement. PresenceLookup is now the unit that decides, tagged, with the reasoning next to it. JR-004: windows are served exactly as given. UT-021 pins that [0,10] and [10,20] are not merged despite looking mergeable -- two windows mean a genuine departure and return, and collapsing them answers a different question from the one the truth file asked. UT-022 pins a byte-identical round trip. JR-005: bounds inclusive at both ends, zero-length windows are real sightings rather than degenerate ones to discard, overlaps resolve. The wording was the larger half of JR-005. The overlay rendered a bare list: it asserted nothing, but told the viewer nothing either, and the default reading of a paused frame is "these people are on screen" -- exactly what SR-002 forbids. It now carries an "In this scene" heading. ActorAtTime became ActorInScene, and README no longer contains "on screen" anywhere; it stated the forbidden reading outright in seven places, including the opening sentence. JR-006: measured rather than assumed. UT-023 builds 50 actors x 1000 windows and asserts the response is bounded by actor count, never window count. The lookup is a full scan on purpose -- an early exit on `start > t` would exploit the sortedness the format requires, but would silently under-report the moment one producer emitted windows out of order. UT-020 pins that unsorted input still resolves; WindowsAreSorted is a diagnostic, not a correctness dependency. Third mutation check: making the end bound exclusive fails UT-016 and UT-018 and nothing else. One character turns an inclusive window into a half-open one, dropping an actor at exactly the moment a scene ends. TRACES: UT-016, UT-017, UT-018, UT-019, UT-020, UT-021, UT-022, UT-023 TRACES: JR-004, JR-005, JR-006 | SR-002 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
120 lines
4.0 KiB
C#
120 lines
4.0 KiB
C#
using System.Collections.Generic;
|
|
using Jellyfin.Plugin.JRay.Models;
|
|
|
|
namespace Jellyfin.Plugin.JRay.Services;
|
|
|
|
/// <summary>
|
|
/// Answers "which actors are in the scene at time <c>t</c>" from a truth file.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is the unit that decides presence, so the scene-scoped semantics live
|
|
/// here rather than being spread through the controller.
|
|
///
|
|
/// <para>
|
|
/// <b>A window is a claim about scene membership, not a recognition event.</b>
|
|
/// An actor who turns away, is occluded, or is off-camera while the shot cuts to
|
|
/// whoever they are speaking to is still present. Two windows mean a genuine
|
|
/// departure and return, not a break in detection — so this code reads windows
|
|
/// exactly as given and never merges, splits, trims, or reorders them.
|
|
/// </para>
|
|
/// <para>
|
|
/// Bounds are inclusive at both ends, matching the format's definition. That
|
|
/// makes adjacent windows such as <c>[0,10]</c> and <c>[10,20]</c> both contain
|
|
/// <c>t = 10</c>; reporting the actor present once is correct, and is not a
|
|
/// reason to merge the windows.
|
|
/// </para>
|
|
/// </remarks>
|
|
// TRACES: JR-004, JR-005, JR-006 | SR-002
|
|
public static class PresenceLookup
|
|
{
|
|
/// <summary>
|
|
/// Determines whether an actor is present in the scene at <paramref name="t"/>.
|
|
/// </summary>
|
|
/// <param name="actor">The actor entry from a truth file.</param>
|
|
/// <param name="t">The timestamp, in seconds.</param>
|
|
/// <returns><c>true</c> when any window contains <paramref name="t"/>.</returns>
|
|
public static bool IsPresentAt(TruthActor actor, double t)
|
|
{
|
|
if (actor is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// A full scan, deliberately: windows may be numerous, but correctness
|
|
// must not depend on the producer having honoured the sortedness
|
|
// guarantee. An early exit on `start > t` would be faster and would
|
|
// silently under-report the moment one file arrived out of order —
|
|
// trading a correctness risk for a saving that does not matter at this
|
|
// scale (see JR-006).
|
|
foreach (var window in actor.Scenes)
|
|
{
|
|
if (window.Length == 2 && window[0] <= t && t <= window[1])
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lists the actors present in the scene at <paramref name="t"/>, in the
|
|
/// order the truth file lists them.
|
|
/// </summary>
|
|
/// <param name="truth">The truth file.</param>
|
|
/// <param name="t">The timestamp, in seconds.</param>
|
|
/// <returns>The actors whose windows contain <paramref name="t"/>.</returns>
|
|
public static IEnumerable<TruthActor> ActorsPresentAt(TruthFile truth, double t)
|
|
{
|
|
if (truth is null)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
foreach (var actor in truth.Actors)
|
|
{
|
|
if (IsPresentAt(actor, t))
|
|
{
|
|
yield return actor;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether an actor's windows are sorted by start time, as the
|
|
/// truth-file format requires of producers.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Presence lookup does not depend on this — it is a diagnostic. A file that
|
|
/// fails it is still read correctly, but it signals a producer bug worth
|
|
/// surfacing rather than absorbing silently.
|
|
/// </remarks>
|
|
/// <param name="actor">The actor entry from a truth file.</param>
|
|
/// <returns><c>true</c> when every window starts at or after its predecessor.</returns>
|
|
public static bool WindowsAreSorted(TruthActor actor)
|
|
{
|
|
if (actor is null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
double previousStart = double.NegativeInfinity;
|
|
foreach (var window in actor.Scenes)
|
|
{
|
|
if (window.Length != 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (window[0] < previousStart)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
previousStart = window[0];
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|