Files
Flaro/server/tests/Flaro.Core.Tests/AbspannkanteTests.cs
T
ScarriffleandClaude Opus 5 259996c5ad Zweimal vermerkt, einmal gespeichert, Serie verloren
„Markensuche für ‚A Knight of the Seven Kingdoms' abgebrochen — An error
occurred while saving the entity changes." Das war meiner, und er hat
mehr gekostet als eine Fehlermeldung: der Lauf brach beim Speichern ab,
und damit war alles weg, was er für diese Serie gefunden hatte.

`VermerkenAsync` sah in der Datenbank nach, ob die Staffel schon
vermerkt ist — und eine Abfrage sieht nicht, was derselbe Lauf eben erst
hinzugefügt und noch nicht gespeichert hat. Seit der Bildweg dazukam,
wird für dieselbe Staffel zweimal vermerkt: einmal nach dem Tonabgleich,
einmal vor dem Bild. Die zweite Abfrage fand nichts, legte eine zweite
Zeile an, und der eindeutige Schlüssel über (Serie, Staffel, Art) warf
sie beim Speichern wieder hinaus. Jetzt wird erst im Änderungsspeicher
nachgesehen, dann in der Datenbank.

**Und die Meldung taugte nichts.** Das Tagebuch hängt nur `Message` an,
und EF Core schreibt dort „See the inner exception for details." — eine
Zeile, die ausdrücklich sagt, dass sie die Auskunft nicht enthält. Alle
vier Abbruchmeldungen nennen jetzt den innersten Grund über
`Fehlertext.FuerMenschen`.

**Dazu der zweite Fund aus demselben Protokoll.** „(T)Raumschiff
Surprise": kein Abspann, 1318 Proben, Schwelle 51,1, letzte Probe 17 —
das Ende ist also dunkel, und trotzdem kein Fund. 1318 Proben auf zwanzig
Minuten heissen eine alle 0,9 Sekunden, und die Nachsicht gegenüber
hellen Stellen mitten im Abspann war in *Proben* angegeben: zwei Stück,
dort also 1,8 Sekunden. Bei einer Datei mit einem Schlüsselbild alle zehn
Sekunden waren dieselben zwei Proben zwanzig Sekunden. Dasselbe
Zugeständnis, zehnfach verschieden — genau der Fehler, den ich eine Zeile
höher bei der Schlusstafel schon behoben hatte. `Nachsicht` zählt jetzt
Sekunden, zehn davon.

**Und die Suche sagt, woran es lag.** „kein Abspann — das Ende ist hell"
gegen „die dunkle Strecke ist mit 20 s zu kurz" sind zwei verschiedene
Ursachen mit zwei verschiedenen Antworten; bisher stand dort beides Mal
dasselbe.

Vier neue Prüfungen, darunter dieselbe Szene bei 0,9 und bei 10 Sekunden
Abtastung. Mit der alten Probenzählung ist die dichte rot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 13:31:48 +02:00

411 lines
14 KiB
C#

using Flaro.Core.Media;
namespace Flaro.Core.Tests;
/// <summary>Wo der Abspann eines Films anfängt.</summary>
///
/// <remarks>
/// <b>Mit erfundenen Messreihen statt mit Filmen.</b> Geprüft wird das
/// Beurteilen, nicht ob ffmpeg rechnen kann. Die Zahlen stammen aus einer
/// Messung an einem erzeugten Film: Handlung 125, Abspann 18.
/// </remarks>
public class AbspannkanteTests
{
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
/// <summary>Eine Reihe von Messpunkten im Abstand von zehn Sekunden.</summary>
///
/// <param name="abspannAb">Ab wann dunkel. <c>null</c> heisst: nie.</param>
private static List<Bildprobe> Reihe(double von, double bis, double? abspannAb)
{
var proben = new List<Bildprobe>();
for (var t = von; t <= bis; t += 10)
proben.Add(new Bildprobe(t, abspannAb is { } ab && t >= ab ? 18 : 125));
return proben;
}
[Test]
public void Eine_dunkle_Strecke_bis_zum_Ende_ist_der_Abspann()
{
var proben = Reihe(0, 600, abspannAb: 420);
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(420));
}
/// <summary>Ein Film, der hell endet, hat keinen erkennbaren Abspann.</summary>
///
/// <remarks>
/// Namen über laufendem Bild — Marvel, Pixar, jede zweite Komödie. Kein
/// Fund ist hier die richtige Antwort: ein geratener Knopf schnitte
/// Handlung weg.
/// </remarks>
[Test]
public void Ohne_dunkles_Ende_kein_Fund()
{
Assert.That(Abspannkante.Suchen(Reihe(0, 600, abspannAb: null), Regeln), Is.Null);
}
/// <summary>Eine Schwarzblende mitten im Film zählt nicht.</summary>
[Test]
public void Eine_Schwarzblende_in_der_Mitte_zaehlt_nicht()
{
var proben = Reihe(0, 600, abspannAb: null);
for (var i = 20; i < 30; i++) proben[i] = proben[i] with { Helligkeit = 10 };
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null,
"sie hält nicht bis zum Ende durch");
}
/// <summary>Eine Szene nach dem Abspann nimmt den Fund weg.</summary>
///
/// <remarks>
/// Lieber keine Marke als eine, die über die Szene hinwegspringt.
/// </remarks>
[Test]
public void Eine_Szene_nach_dem_Abspann_nimmt_den_Fund_weg()
{
var proben = Reihe(0, 600, abspannAb: 420);
for (var i = proben.Count - 5; i < proben.Count; i++)
proben[i] = proben[i] with { Helligkeit = 120 };
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null);
}
/// <summary>Eine helle Schrifttafel mittendrin bricht die Strecke nicht.</summary>
///
/// <remarks>
/// <b>Eine Probe, also zehn Sekunden.</b> Hier standen zwei, und das
/// war unter der alten Zählweise gedacht: nachgesehen wurde, wie viele
/// <i>Proben</i> am Stück hell sind, nicht wie lange. Zwanzig Sekunden
/// helles Bild sind keine Schrifttafel mehr, sondern eine Szene — dass
/// die Suche dort abbricht, ist richtig und steht in
/// <c>AbspannNachsichtTests</c>.
/// </remarks>
[Test]
public void Eine_kurze_helle_Tafel_bricht_nichts()
{
var proben = Reihe(0, 600, abspannAb: 420);
proben[50] = proben[50] with { Helligkeit = 140 };
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(420));
}
/// <summary>Zwanzig Sekunden Dunkelheit sind kein Abspann.</summary>
[Test]
public void Zu_kurz_zaehlt_nicht()
{
Assert.That(Abspannkante.Suchen(Reihe(0, 600, abspannAb: 580), Regeln), Is.Null);
}
/// <summary>Ein halber Film im Dunkeln ist ein Fehlfund.</summary>
[Test]
public void Zu_lang_zaehlt_auch_nicht()
{
Assert.That(Abspannkante.Suchen(Reihe(0, 3600, abspannAb: 600), Regeln), Is.Null);
}
[Test]
public void Die_letzte_helle_Probe_begrenzt_die_Suche()
{
var proben = Reihe(0, 600, abspannAb: 420);
Assert.That(Abspannkante.HellDavor(proben, 420, Regeln), Is.EqualTo(410));
}
// ---------- Nachmessen ----------
/// <summary>Die binäre Suche findet die Kante auf vier Sekunden genau.</summary>
[Test]
public async Task Die_Kante_wird_eingegrenzt()
{
const double echteKante = 417.3;
var sonden = 0;
var kante = await Abspannkante.VerfeinernAsync(410, 420, Regeln.Dunkelschwelle, stelle =>
{
sonden++;
return Task.FromResult<double?>(stelle >= echteKante ? 18 : 125);
});
Assert.Multiple(() =>
{
Assert.That(kante, Is.EqualTo(echteKante).Within(4));
Assert.That(kante, Is.GreaterThanOrEqualTo(echteKante),
"lieber ein paar Sekunden Abspann zu viel als eine Sekunde Handlung");
Assert.That(sonden, Is.LessThanOrEqualTo(4), "zehn Sekunden auf vier: zwei Halbierungen");
});
}
/// <summary>Antwortet eine Sonde nicht, gilt der grobe Fund.</summary>
[Test]
public async Task Ohne_Antwort_bleibt_es_beim_groben_Fund()
{
var kante = await Abspannkante.VerfeinernAsync(
410, 420, Regeln.Dunkelschwelle, _ => Task.FromResult<double?>(null));
Assert.That(kante, Is.EqualTo(420));
}
/// <summary>Ein schon enger Bereich braucht keine Sonde.</summary>
[Test]
public async Task Ein_enger_Bereich_wird_nicht_angefasst()
{
var sonden = 0;
var kante = await Abspannkante.VerfeinernAsync(418, 420, Regeln.Dunkelschwelle, _ =>
{
sonden++;
return Task.FromResult<double?>(18);
});
Assert.Multiple(() =>
{
Assert.That(kante, Is.EqualTo(420));
Assert.That(sonden, Is.Zero);
});
}
}
/// <summary>Die Schwelle an gemessenen Abspannwerten, nicht an erfundenen.</summary>
///
/// <remarks>
/// <para>
/// <b>Der Fehler, für den diese Prüfungen stehen.</b> Die Erkennung lief
/// gegen eine feste Schwelle von 35, und die stammte aus einem Prüfbild
/// mit 0,8 Prozent weisser Fläche — Helligkeit 18. Ein echter Abspann hat
/// mehr Schrift.
/// </para>
/// <para>
/// <b>Nachgemessen</b> an erzeugtem 1080p-Material mit zwei Spalten Namen,
/// durch dieselbe ffmpeg-Kette wie im Betrieb
/// (<c>scale=160:-2,signalstats</c>):
/// </para>
/// <list type="table">
/// <item><description>Handlung: <b>126</b></description></item>
/// <item><description>Abspann, realistische Schriftdichte (1,3 % Weiss): <b>32</b></description></item>
/// <item><description>Abspann, dicht gesetzt (9,5 % Weiss): <b>47</b></description></item>
/// </list>
/// <para>
/// Die Fünfunddreissig lag also mitten zwischen den beiden Abspannfällen.
/// Seitdem ist sie nur noch der Boden; gemessen wird gegen den Grundton
/// des Films.
/// </para>
/// </remarks>
public class AbspannSchwellenTests
{
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
/// <summary>Ein Suchfenster, wie es im Betrieb entsteht.</summary>
///
/// <remarks>
/// Zwanzig Minuten vor dem Ende einer Folge von fünfundvierzig, an den
/// Schlüsselbildern abgetastet — also alle fünf Sekunden eine Probe.
/// </remarks>
private static List<Bildprobe> Fenster(double abspannAb, double imAbspann, double imInhalt = 126)
{
var proben = new List<Bildprobe>();
for (var t = 1500.0; t <= 2700; t += 5)
proben.Add(new Bildprobe(t, t < abspannAb ? imInhalt : imAbspann));
return proben;
}
/// <summary>Der Fall, an dem es scheiterte: ein schriftreicher Abspann.</summary>
///
/// <remarks>
/// Mit der alten festen Schwelle von 35 ist diese Prüfung rot: 47 gilt
/// dort als hell, und die Folge bekam keine Marke.
/// </remarks>
[Test]
public void Ein_dichter_Abspann_bei_siebenundvierzig_wird_gefunden()
{
var proben = Fenster(abspannAb: 2340, imAbspann: 47);
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2340).Within(5));
}
[Test]
public void Ein_schlanker_Abspann_bei_zweiunddreissig_wird_gefunden()
{
var proben = Fenster(abspannAb: 2340, imAbspann: 32);
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2340).Within(5));
}
/// <summary>Und die Schwelle liegt zwischen beidem, nicht daneben.</summary>
[Test]
public void Die_Schwelle_liegt_zwischen_Handlung_und_Abspann()
{
var schwelle = Abspannkante.Schwelle(Fenster(2340, 47), Regeln);
Assert.Multiple(() =>
{
Assert.That(schwelle, Is.GreaterThan(47), "ein dichter Abspann muss darunter liegen");
Assert.That(schwelle, Is.LessThan(126), "die Handlung muss darüber liegen");
});
}
/// <summary>**Ein dunkler Film wird dadurch nicht zum Dauerabspann.**</summary>
///
/// <remarks>
/// Eine relative Schwelle kann nach unten entgleisen: bei einem Film,
/// der durchweg dämmrig ist, wäre „etwas mehr als halb so hell" bald
/// jede Nachtszene. Deshalb bleibt <c>Dunkelschwelle</c> als Boden —
/// hier sorgt sie dafür, dass eine Schlussszene bei 40 nichts auslöst.
/// </remarks>
[Test]
public void Eine_duestere_Schlussszene_ist_kein_Abspann()
{
var proben = Fenster(abspannAb: 2340, imAbspann: 40, imInhalt: 55);
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null);
}
/// <summary>Helle Handlung bis zum Ende bleibt Handlung.</summary>
[Test]
public void Ohne_dunkles_Ende_kein_Fund()
{
var proben = Fenster(abspannAb: double.MaxValue, imAbspann: 0);
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null);
}
}
/// <summary>Was hinter dem Abspann noch kommt.</summary>
///
/// <remarks>
/// <b>Der Fehler, für den diese Prüfungen stehen.</b> Die Suche verlangte,
/// dass die allerletzte Probe dunkel ist. Hinter dem Abspann steht bei
/// vielen Produktionen aber noch eine Logotafel — Sender, Studio,
/// Förderer —, oft hell. Ein einziges weisses Schlussbild machte damit den
/// ganzen Abspann unsichtbar.
/// </remarks>
public class AbspannSchlusstafelTests
{
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
private static List<Bildprobe> Fenster(double abspannAb, double tafelAb = double.MaxValue)
{
var proben = new List<Bildprobe>();
for (var t = 1500.0; t <= 2700; t += 5)
{
var hell = t >= tafelAb ? 210 : t < abspannAb ? 126 : 32;
proben.Add(new Bildprobe(t, hell));
}
return proben;
}
/// <summary>Eine helle Tafel am Schluss verdeckt den Abspann nicht.</summary>
[Test]
public void Eine_Logotafel_am_Ende_zaehlt_nicht_dagegen()
{
// Fünfzehn Sekunden weiss hinter dem Abspann.
var proben = Fenster(abspannAb: 2340, tafelAb: 2690);
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2340).Within(5));
}
/// <summary>Eine lange helle Strecke am Ende schon.</summary>
///
/// <remarks>
/// Die Nachsicht hat eine Grenze, sonst fände die Suche hinter jeder
/// Schwarzblende einen Abspann und hinge die halbe Handlung daran.
/// </remarks>
[Test]
public void Eine_lange_helle_Strecke_am_Ende_bricht_ab()
{
var proben = Fenster(abspannAb: 2340, tafelAb: 2600);
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null);
}
}
/// <summary>Dieselbe Szene, zweierlei Schlüsselbilddichte.</summary>
///
/// <remarks>
/// <para>
/// <b>Der Fehler, für den diese Prüfungen stehen.</b> Die Nachsicht
/// gegenüber hellen Stellen mitten im Abspann war in <i>Proben</i>
/// angegeben, nicht in Sekunden. Was das bedeutet, hängt allein daran,
/// wie dicht eine Datei ihre Schlüsselbilder setzt — und das schwankt um
/// eine Zehnerpotenz.
/// </para>
/// <para>
/// Gemessen am Bestand: „(T)Raumschiff Surprise" liefert 1318 Proben auf
/// zwanzig Minuten, also eine alle 0,9 Sekunden. Zwei Proben Nachsicht
/// waren dort 1,8 Sekunden; bei einer Datei mit einem Schlüsselbild alle
/// zehn Sekunden waren dieselben zwei Proben zwanzig Sekunden. Ein
/// fünfsekündiges Logo mitten im Abspann zerriss die Strecke deshalb nur
/// bei der dicht abgetasteten Datei — und genau die fiel durch.
/// </para>
/// </remarks>
public class AbspannNachsichtTests
{
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
/// <summary>Abspann ab 2340 s, darin fünf Sekunden helles Logo.</summary>
private static List<Bildprobe> Fenster(double takt)
{
var proben = new List<Bildprobe>();
for (var t = 1500.0; t <= 2700; t += takt)
{
var imLogo = t >= 2500 && t < 2505;
proben.Add(new Bildprobe(t, t < 2340 ? 126 : imLogo ? 180 : 32));
}
return proben;
}
/// <summary>Dicht abgetastet — der Fall, der im Bestand durchfiel.</summary>
[Test]
public void Ein_Logo_zerreisst_den_Abspann_nicht_bei_dichten_Proben()
{
Assert.That(Abspannkante.Suchen(Fenster(takt: 0.9), Regeln),
Is.EqualTo(2340.0).Within(2));
}
/// <summary>Dünn abgetastet — derselbe Film, dieselbe Antwort.</summary>
[Test]
public void Und_auch_nicht_bei_duennen_Proben()
{
Assert.That(Abspannkante.Suchen(Fenster(takt: 10), Regeln),
Is.EqualTo(2340.0).Within(11));
}
/// <summary>Eine lange helle Strecke bricht weiterhin ab.</summary>
///
/// <remarks>
/// Sonst liefe die Suche rückwärts durch jede Nachtszene und meldete
/// den halben Film als Abspann.
/// </remarks>
[Test]
public void Eine_lange_helle_Strecke_bricht_die_Suche()
{
var proben = new List<Bildprobe>();
for (var t = 1500.0; t <= 2700; t += 1)
{
// Dunkel ab 2340, aber von 2400 bis 2460 eine Minute hell.
var hell = t < 2340 || (t >= 2400 && t < 2460);
proben.Add(new Bildprobe(t, hell ? 126 : 32));
}
// Gefunden wird nur das Stück hinter der hellen Minute.
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2460.0).Within(2));
}
/// <summary>Und der Grund steht für das Protokoll bereit.</summary>
[Test]
public void Der_Grund_wird_benannt()
{
var hell = new List<Bildprobe>();
for (var t = 1500.0; t <= 2700; t += 5) hell.Add(new Bildprobe(t, 126));
Abspannkante.Suchen(hell, Regeln, out var grund);
Assert.That(grund, Does.Contain("hell"));
var kurz = new List<Bildprobe>();
for (var t = 1500.0; t <= 2700; t += 5)
kurz.Add(new Bildprobe(t, t < 2680 ? 126 : 32));
Abspannkante.Suchen(kurz, Regeln, out var zuKurz);
Assert.That(zuKurz, Does.Contain("kurz"));
}
}