Beim Staffelfinale klingt der Abspann anders
Und genau daran fiel er durch. Der Tonabgleich lebt davon, dass der Abspann in jeder Folge derselbe ist; beim Finale ist er es oft nicht — anderer Schluss, andere Musik, kein Treffer. In jeder Staffel fehlte deshalb genau eine Marke, immer die letzte, und wer dort weitersah, sah keinen Knopf. Das war der ganze Fehler. Der Spieler war nie schuld, und meine Schranken-Vermutung von eben war falsch. Was der Ton nicht findet, sucht jetzt das Bild — derselbe Weg wie bei einem Film: dunkel werden und bis zum Ende durchhalten. Das Verfahren lag fertig in `FilmAsync` und war nur dort eingebaut; es steht jetzt für sich und wird von beiden benutzt. Nur für die Lücken: bei einer Staffel ohne fehlende Marke läuft der Abtaster gar nicht. **Drei Punkte heissen „mehr davon", nicht „tu etwas".** Bei einer Folge taten sie etwas — wegräumen aus „Fortsetzen", ohne Rückfrage. Jeder, der sie sah, hat ein Menü erwartet, draufgetippt, und dann baute die Seite sich neu auf und die Punkte waren weg. Jetzt ein Kreuz, und eine Zeile, die sagt, was geschehen ist. Und die Markenanzeige nennt die Zeiten, nicht nur die Namen: dass eine Marke da ist, sagt noch nicht, dass sie an der richtigen Stelle steht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
74643b8242
commit
1340176bf0
@@ -227,25 +227,12 @@ public sealed class Markenerkennung(
|
||||
.AnyAsync(x => x.MovieId == filmId && x.Art == Markenart.Abspann, ct))
|
||||
return false;
|
||||
|
||||
var ab = Math.Max(0, laufzeit.TotalSeconds - Suchbereich);
|
||||
var proben = await abtaster.UeberblickAsync(
|
||||
film.Path, ab, laufzeit.TotalSeconds - ab, ct);
|
||||
|
||||
await VermerkenAsync(db, filmId, Markenart.Abspann, ct);
|
||||
|
||||
var regeln = Abspannregeln.Vorgabe;
|
||||
var grob = Abspannkante.Suchen(proben, regeln);
|
||||
var gefunden = await BildabspannAsync(
|
||||
db, abtaster, filmId, istFilm: true, film.Path, laufzeit.TotalSeconds, ct);
|
||||
|
||||
if (grob is { } stelle)
|
||||
{
|
||||
var hell = Abspannkante.HellDavor(proben, stelle, regeln) ?? ab;
|
||||
var kante = await Abspannkante.VerfeinernAsync(
|
||||
hell, stelle, regeln, wo => abtaster.HelligkeitAsync(film.Path, wo, ct));
|
||||
|
||||
await SchreibenAsync(db, filmId, istFilm: true, Markenart.Abspann,
|
||||
kante, laufzeit.TotalSeconds, ct);
|
||||
}
|
||||
else
|
||||
if (!gefunden)
|
||||
{
|
||||
// Nichts gefunden ist ein Ergebnis. Eine alte Marke, die jetzt
|
||||
// nicht mehr bestätigt wird, gehört weg — sonst bliebe ein
|
||||
@@ -256,7 +243,43 @@ public sealed class Markenerkennung(
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return grob is not null;
|
||||
return gefunden;
|
||||
}
|
||||
|
||||
/// <summary>Sucht den Abspann im Bild — bei einem Film wie bei einer Folge.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Dunkel und bis zum Ende durchhalten</b> — mehr braucht es nicht,
|
||||
/// und mehr hat ein Abspann auch nicht gemeinsam. Erst ein grober
|
||||
/// Überblick über die letzten zwanzig Minuten aus Schlüsselbildern
|
||||
/// (billig, aber auf fünf bis zehn Sekunden ungenau), dann eine Handvoll
|
||||
/// kurzer Sonden für die Kante.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Warum das auch bei Folgen gebraucht wird.</b> Der Tonabgleich lebt
|
||||
/// davon, dass der Abspann in jeder Folge derselbe ist — und genau das
|
||||
/// stimmt beim Staffelfinale oft nicht: anderer Schluss, andere Musik,
|
||||
/// kein Treffer. Das Bild weiss davon nichts; dort sieht das Finale aus
|
||||
/// wie jede andere Folge.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private async Task<bool> BildabspannAsync(
|
||||
FlaroContext db, Bildabtaster abtaster, Guid titelId, bool istFilm,
|
||||
string pfad, double laufzeit, CancellationToken ct)
|
||||
{
|
||||
var ab = Math.Max(0, laufzeit - Suchbereich);
|
||||
var proben = await abtaster.UeberblickAsync(pfad, ab, laufzeit - ab, ct);
|
||||
|
||||
var regeln = Abspannregeln.Vorgabe;
|
||||
if (Abspannkante.Suchen(proben, regeln) is not { } stelle) return false;
|
||||
|
||||
var hell = Abspannkante.HellDavor(proben, stelle, regeln) ?? ab;
|
||||
var kante = await Abspannkante.VerfeinernAsync(
|
||||
hell, stelle, regeln, wo => abtaster.HelligkeitAsync(pfad, wo, ct));
|
||||
|
||||
return await SchreibenAsync(db, titelId, istFilm, Markenart.Abspann,
|
||||
kante, laufzeit, ct);
|
||||
}
|
||||
|
||||
private async Task LaufenAsync(Guid serieId, bool erneut, CancellationToken ct)
|
||||
@@ -264,6 +287,7 @@ public sealed class Markenerkennung(
|
||||
using var raum = scopes.CreateScope();
|
||||
var db = raum.ServiceProvider.GetRequiredService<FlaroContext>();
|
||||
var leser = raum.ServiceProvider.GetRequiredService<Tonabdruckleser>();
|
||||
var abtaster = raum.ServiceProvider.GetRequiredService<Bildabtaster>();
|
||||
|
||||
if (!await leser.VorhandenAsync(ct))
|
||||
{
|
||||
@@ -356,6 +380,42 @@ public sealed class Markenerkennung(
|
||||
gefunden++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fehlt.Contains(Markenart.Abspann)) continue;
|
||||
|
||||
// **Was der Ton nicht fand, sucht das Bild.**
|
||||
//
|
||||
// Der Abgleich lebt davon, dass der Abspann in jeder Folge
|
||||
// derselbe ist — und beim Staffelfinale stimmt das oft nicht:
|
||||
// anderer Schluss, andere Musik, kein Treffer. Genau dort fiel
|
||||
// die letzte Folge jeder Staffel durch, und nur dort. Das Bild
|
||||
// weiss davon nichts; dort sieht das Finale aus wie jede
|
||||
// andere Folge.
|
||||
//
|
||||
// **Nur für die Lücken.** Der Überblick dekodiert zwanzig
|
||||
// Minuten Schlüsselbilder; bei einer Staffel ohne Lücke läuft
|
||||
// er gar nicht, mit Lücke ein- bis zweimal. Erst speichern,
|
||||
// sonst sieht die Abfrage die eben geschriebenen Marken nicht.
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
var nummern = geordnet.Select(f => f.Id).ToList();
|
||||
var mitAbspann = await db.Marken.AsNoTracking()
|
||||
.Where(m => m.Art == Markenart.Abspann
|
||||
&& m.EpisodeId != null && nummern.Contains(m.EpisodeId.Value))
|
||||
.Select(m => m.EpisodeId!.Value)
|
||||
.ToListAsync(ct);
|
||||
|
||||
foreach (var folge in geordnet)
|
||||
{
|
||||
if (mitAbspann.Contains(folge.Id)) continue;
|
||||
if (folge.Duration is not { TotalSeconds: > 0 } dauer) continue;
|
||||
|
||||
if (await BildabspannAsync(db, abtaster, folge.Id, istFilm: false,
|
||||
folge.Path, dauer.TotalSeconds, ct))
|
||||
{
|
||||
gefunden++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Immer speichern: auch ein Lauf ohne Fund hinterlässt Vermerke,
|
||||
|
||||
@@ -455,9 +455,20 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
stand?.favourite ? 'Aus Favoriten nehmen' : 'Zu Favoriten',
|
||||
async () => { await favorit(eintrag.id, !stand?.favourite); await laden(); }));
|
||||
|
||||
// **Drei Punkte heissen „mehr davon", nicht „tu etwas".**
|
||||
//
|
||||
// Hier taten sie etwas: wegräumen aus „Fortsetzen", ohne Rückfrage.
|
||||
// Jeder, der sie sah, hat ein Menü erwartet, draufgetippt — und dann
|
||||
// baute die Seite sich neu auf und die Punkte waren weg. Von aussen
|
||||
// sah das aus wie ein Fehler, und ohne Rückmeldung war nicht einmal
|
||||
// zu erkennen, was passiert war.
|
||||
if ((stand?.positionSeconds ?? 0) > 0 && !stand?.dismissed) {
|
||||
reihe.appendChild(rundKnopf('mehr', false, 'Aus „Fortsetzen" entfernen',
|
||||
async () => { await weglegen(eintrag.id); await laden(); }));
|
||||
reihe.appendChild(rundKnopf('schliessen', false, 'Aus „Fortsetzen" entfernen',
|
||||
async () => {
|
||||
await weglegen(eintrag.id);
|
||||
hinweisZeigen('Aus „Fortsetzen" entfernt.');
|
||||
await laden();
|
||||
}));
|
||||
}
|
||||
|
||||
// **Diesen einen Titel neu holen, nicht den ganzen Bestand.**
|
||||
@@ -1094,9 +1105,14 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
function markenwort(marken: Folge['markers']): string {
|
||||
if (!istAdmin()) return '';
|
||||
if (!marken || marken.length === 0) return 'keine Marken';
|
||||
// **Mit Zeiten, nicht bloss mit Namen.** Dass eine Marke da ist, sagt
|
||||
// noch nicht, dass sie an der richtigen Stelle steht — und genau das
|
||||
// ist der Unterschied zwischen „die Erkennung hat nichts gefunden"
|
||||
// und „der Knopf kommt zur falschen Minute".
|
||||
return marken
|
||||
.map(m => m.kind === 'intro' ? 'Vorspann' : 'Abspann')
|
||||
.join(' + ');
|
||||
.map(m => `${m.kind === 'intro' ? 'Vorspann' : 'Abspann'} `
|
||||
+ `${dauer(m.startSeconds)}–${dauer(m.endSeconds)}`)
|
||||
.join(' · ');
|
||||
}
|
||||
|
||||
function folgenblock(folge: Folge): HTMLElement {
|
||||
|
||||
Reference in New Issue
Block a user