Files
Flaro/server/tests/Flaro.Core.Tests/FingerprintabgleichTests.cs
T
ScarriffleandClaude Opus 5 ed13d5fa43 Kratzplatz, Markenerkennung, Tonabdruck — erfundene Wörter für Fachbegriffe
„Scratch disk" wörtlich zu übersetzen ist derselbe Fehler wie „Wachhund"
für Watchdog und „Keks" für Cookie. Und „Marke" ist kein Fachbegriff für
das, was gemeint ist: Jellyfin nennt diese Abschnitte Media Segments, und
das ist auch das Wort, in dem sich darüber reden lässt.

    Kratzplatz*       → Transcode*          (was es ist, steht in FLARO_TRANSCODE)
    Marke, Marken     → Segment, Segmente
    Markenerkennung   → Segmenterkennung
    Markenart/-suche  → Segmentart/-suche
    Tonabdruck        → Fingerprint         (Chromaprint nennt es so)
    Tonabgleich       → Fingerprintabgleich
    Schlüsselbild     → Keyframe
    Bildabtaster      → Frameabtaster

Gewöhnliches Deutsch bleibt gewöhnliches Deutsch: Wartung, Freigabe,
Meldung, Abspann, Vorspann. Das war nie das Problem.

**Was sich nicht ändert, und zwar mit Absicht.** Die drei
Einstellungsschlüssel `wartung.kratzplatz.paket.*` bleiben, wie sie sind —
sie stehen in der Datenbank, und ein neuer Name hiesse, dass die Werte
des Nutzers stillschweigend auf die Vorgabe zurückfallen. Dasselbe für
die Endpunktpfade `/marken`: sie sind nicht sichtbar, und ein Umbenennen
wäre Diff ohne Gewinn.

**Die Migration ist von Hand geschrieben.** `dotnet ef` hat ein Gerüst
erzeugt, das beide Tabellen löscht und neu anlegt — es sieht eine
verschwundene und eine neue Tabelle, nicht eine umbenannte. Damit wäre
jedes erkannte Segment und jeder Vermerk „hier wurde schon gesucht" weg
gewesen. `RenameTable` und `RenameIndex` tun dasselbe, ohne eine Zeile
anzufassen; nachgemessen an einer echten SQLite-Datei: Zeilen vorher 1
und 1, nachher 1 und 1, Inhalt unverändert.

Nachgezogen: in `bilder.ts` und `main.ts` heisst „marke" ein Häkchen und
das Logo — die blieben unberührt. Und „Segment" ist sächlich, was eine
Blindersetzung nicht weiss: „eine Segment" stand danach an vierzehn
Stellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-16 16:26:57 +02:00

166 lines
6.4 KiB
C#

using Flaro.Core.Media;
namespace Flaro.Core.Tests;
/// <summary>Was zwei Folgen gemeinsam haben.</summary>
///
/// <remarks>
/// <b>Mit erfundenen Abdrücken statt mit Mediendateien.</b> Was hier geprüft
/// wird, ist die Suche nach dem gemeinsamen Stück — nicht, ob Chromaprint
/// rechnen kann. Eine Prüfung, die eine Serie mit echten Folgen braucht,
/// liefe nirgends ausser auf dem Rechner dessen, der sie geschrieben hat.
/// </remarks>
public class FingerprintabgleichTests
{
/// <summary>Chromaprint mit Standardeinstellungen.</summary>
private const double JeFingerprint = 0.1238;
/// <summary>Eine Folge von Abdrücken, die sich nicht wiederholt.</summary>
///
/// <remarks>
/// Aus einem festen Startwert gerechnet, nicht gewürfelt: dieselbe
/// Prüfung soll morgen dasselbe Ergebnis haben.
/// </remarks>
private static uint[] Rauschen(int anzahl, int saat)
{
var werte = new uint[anzahl];
var stand = (uint)saat * 2654435761u + 1u;
for (var i = 0; i < anzahl; i++)
{
stand ^= stand << 13;
stand ^= stand >> 17;
stand ^= stand << 5;
werte[i] = stand;
}
return werte;
}
/// <summary>Hängt Abschnitte aneinander.</summary>
private static uint[] Aneinander(params uint[][] teile) =>
teile.SelectMany(t => t).ToArray();
/// <summary>Wie viele Zeitscheiben so viele Sekunden sind.</summary>
private static int Scheiben(double sekunden) => (int)Math.Round(sekunden / JeFingerprint);
[Test]
public void Ein_gemeinsamer_Vorspann_wird_gefunden()
{
var vorspann = Rauschen(Scheiben(30), saat: 1);
// Folge 1: zehn Sekunden Handlung, dann der Vorspann, dann weiter.
var a = Aneinander(Rauschen(Scheiben(10), 2), vorspann, Rauschen(Scheiben(60), 3));
// Folge 2: der Vorspann liegt viel später.
var b = Aneinander(Rauschen(Scheiben(70), 4), vorspann, Rauschen(Scheiben(20), 5));
var treffer = Fingerprintabgleich.Finden(a, b, JeFingerprint);
Assert.That(treffer, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(treffer!.AVon, Is.EqualTo(10).Within(1.0));
Assert.That(treffer.ABis, Is.EqualTo(40).Within(1.0));
Assert.That(treffer.BVon, Is.EqualTo(70).Within(1.0));
Assert.That(treffer.BBis, Is.EqualTo(100).Within(1.0));
});
}
/// <summary>Zwei Folgen ohne Gemeinsamkeit ergeben keinen Fund.</summary>
///
/// <remarks>
/// <b>Der wichtigere der beiden Fälle.</b> Ein Vorspann, der nicht
/// gefunden wird, kostet einen Knopf. Ein Vorspann, der erfunden wird,
/// schneidet dem Zuschauer eine halbe Minute Handlung weg.
/// </remarks>
[Test]
public void Ohne_Gemeinsamkeit_kommt_nichts()
{
var a = Rauschen(Scheiben(120), 11);
var b = Rauschen(Scheiben(120), 12);
Assert.That(Fingerprintabgleich.Finden(a, b, JeFingerprint), Is.Null);
}
/// <summary>Ein zu kurzes gemeinsames Stück zählt nicht.</summary>
[Test]
public void Drei_Sekunden_sind_kein_Vorspann()
{
var kurz = Rauschen(Scheiben(3), 21);
var a = Aneinander(Rauschen(Scheiben(30), 22), kurz, Rauschen(Scheiben(30), 23));
var b = Aneinander(Rauschen(Scheiben(50), 24), kurz, Rauschen(Scheiben(10), 25));
Assert.That(Fingerprintabgleich.Finden(a, b, JeFingerprint), Is.Null);
}
/// <summary>Ein paar abweichende Bits machen den Fund nicht kaputt.</summary>
///
/// <remarks>
/// Derselbe Vorspann aus zwei Dateien ist nie bitgleich: andere Bitrate,
/// anderer Codec, andere Lautstärke. Wäre Gleichheit verlangt, fände der
/// Abgleich an echten Folgen nie etwas.
/// </remarks>
[Test]
public void Kleine_Abweichungen_stoeren_nicht()
{
var vorspann = Rauschen(Scheiben(30), 31);
var leichtAnders = vorspann.Select((w, i) => i % 3 == 0 ? w ^ 0b101u : w).ToArray();
var a = Aneinander(Rauschen(Scheiben(5), 32), vorspann, Rauschen(Scheiben(40), 33));
var b = Aneinander(Rauschen(Scheiben(20), 34), leichtAnders, Rauschen(Scheiben(40), 35));
var treffer = Fingerprintabgleich.Finden(a, b, JeFingerprint);
Assert.That(treffer, Is.Not.Null);
Assert.That(treffer!.Laenge, Is.EqualTo(30).Within(2.0));
}
/// <summary>Eine eingeblendete Ansage mitten im Vorspann reisst ihn nicht entzwei.</summary>
[Test]
public void Eine_kurze_Luecke_haelt_den_Lauf_zusammen()
{
var vorspann = Rauschen(Scheiben(30), 41);
// Eine Sekunde mittendrin ist in der einen Folge anders.
var mitAnsage = (uint[])vorspann.Clone();
var mitte = mitAnsage.Length / 2;
for (var i = mitte; i < mitte + Scheiben(1); i++) mitAnsage[i] = 0xDEADBEEF;
var a = Aneinander(Rauschen(Scheiben(5), 42), vorspann, Rauschen(Scheiben(30), 43));
var b = Aneinander(Rauschen(Scheiben(15), 44), mitAnsage, Rauschen(Scheiben(30), 45));
var treffer = Fingerprintabgleich.Finden(a, b, JeFingerprint);
Assert.That(treffer, Is.Not.Null);
Assert.That(treffer!.Laenge, Is.EqualTo(30).Within(2.0),
"Die Lücke gehoert zum Vorspann, nicht dahinter");
}
/// <summary>Eine ganze Staffel auf einmal.</summary>
[Test]
public void Jede_Folge_der_Staffel_bekommt_ihre_Stelle()
{
var vorspann = Rauschen(Scheiben(25), 51);
var folgen = new List<(int, IReadOnlyList<uint>)>();
for (var i = 0; i < 5; i++)
{
folgen.Add((i, Aneinander(
Rauschen(Scheiben(5 + i * 10), 60 + i), vorspann, Rauschen(Scheiben(30), 70 + i))));
}
var gefunden = Fingerprintabgleich.ReiheAbgleichen(folgen, JeFingerprint);
Assert.That(gefunden, Has.Count.EqualTo(5), "auch die letzte Folge");
for (var i = 0; i < 5; i++)
{
Assert.That(gefunden[i].Von, Is.EqualTo(5 + i * 10).Within(1.5), $"Folge {i}");
Assert.That(gefunden[i].Bis - gefunden[i].Von, Is.EqualTo(25).Within(2.0), $"Folge {i}");
}
}
/// <summary>Eine einzelne Folge hat niemanden zum Vergleichen.</summary>
[Test]
public void Eine_Folge_allein_ergibt_nichts()
{
var eine = new List<(int, IReadOnlyList<uint>)> { (0, Rauschen(Scheiben(60), 81)) };
Assert.That(Fingerprintabgleich.ReiheAbgleichen(eine, JeFingerprint), Is.Empty);
}
}