0.2.66 - End Credits improvements / Gateway: 0.1.47 - End credits redesign

This commit is contained in:
ponzischeme89
2026-08-15 22:26:17 +12:00
parent e528d04b43
commit 4bc4b075ec
28 changed files with 643 additions and 211 deletions
+61 -1
View File
@@ -38,6 +38,27 @@ const (
// primary guard against answering on noise, and it is set high because the cost of a
// wrong marker is somebody losing the end of an episode.
minSeparation = 0.18
// onsetFraction is how credit-like a frame has to be, relative to the established roll,
// to count as already part of it.
//
// The split itself lands where the credits are *established*, because the sustain window
// after it has to clear creditLikeFloor on average — and credits usually fade in, so the
// first second or two of the fade scores below that floor and pushes the split forward.
// Measured against real media the gap is two to three seconds, which is visible: the roll
// has plainly begun before the picture moves.
//
// So the split is walked backwards through the fade to the first frame that is already
// mostly credit-like. Two fifths is deliberately generous — this is looking for the start
// of a ramp, not for more credits — and it is a fraction of the established level rather
// than an absolute, because a roll over a bright background never reaches the same score
// as one over black and would otherwise never be walked back at all.
onsetFraction = 0.4
// onsetBackoffMs bounds that walk. A fade is a second or two; anything walking further is
// no longer following one, and the bound is what stops a gradual dimming at the end of a
// scene dragging the marker back into the programme.
onsetBackoffMs = 4000
)
// VisualDetector implements Detector over the ffmpeg sampler.
@@ -195,7 +216,46 @@ func findTransition(frames []frameStats, interval time.Duration) (int, float64,
}
bestIndex, bestSeparation, found = split, separation, true
}
return bestIndex, bestSeparation, found
if !found {
return 0, 0, false
}
// The separation is reported for the split that earned it, never for the walked-back
// frame: confidence is a statement about how clearly the transition was found, and
// recomputing it over a fade would report the answer as weaker for having been improved.
return backOffToOnset(scores, bestIndex, segmentMean(bestIndex, min(bestIndex+sustain, len(scores))), interval),
bestSeparation, true
}
// backOffToOnset walks a split backwards through the credits' fade-in.
//
// Pure, bounded, and it can only ever move the marker earlier — which is the direction that
// needs the care, so both bounds matter: it stops at the first frame that is not already
// mostly credit-like, and it never travels further than onsetBackoffMs. On a hard cut from
// programme to credits the preceding frame scores near zero and the walk stops immediately,
// which is correct: there is no fade to find and the split was already right.
func backOffToOnset(scores []float64, split int, established float64, interval time.Duration) int {
if split <= 0 || established <= 0 || interval <= 0 {
return split
}
limit := int(float64(onsetBackoffMs) / float64(interval.Milliseconds()))
if limit < 1 {
// A sampling interval coarser than the whole allowance cannot resolve a fade at all.
// The coarse pass is this case, and it is the one whose answer the fine pass replaces.
return split
}
floor := onsetFraction * established
earliest := split - limit
if earliest < 0 {
earliest = 0
}
onset := split
for index := split - 1; index >= earliest; index-- {
if scores[index] < floor {
break
}
onset = index
}
return onset
}
// visualConfidence maps separation onto a score.
+84
View File
@@ -160,3 +160,87 @@ func TestVisualConfidenceIsCapped(t *testing.T) {
t.Fatalf("a detection at the separation threshold scored %.2f, below the bar", score)
}
}
// The onset walk-back, tested on scores directly.
//
// It is pure and it is the only thing in the detector that can move a marker *earlier*, so
// its two bounds matter more than the movement itself: a hard cut must not move at all, and
// a long ramp must not drag the marker back into the programme.
// establishedCredits is the score a fully-established roll produces, near enough — the
// literal matters only as the thing onsetFraction is measured against.
const establishedCredits = 0.90
func TestBackOffWalksIntoTheFade(t *testing.T) {
// Forty frames of programme, three of fade, then the roll. The split arrives at the first
// fully-established frame; the fade before it is already the credits.
scores := make([]float64, 0, 60)
for range 40 {
scores = append(scores, 0.02)
}
scores = append(scores, 0.40, 0.55, 0.75)
for range 17 {
scores = append(scores, establishedCredits)
}
onset := backOffToOnset(scores, 43, establishedCredits, fineInterval)
if onset != 40 {
t.Fatalf("onset at frame %d, want 40 (the first frame of the fade)", onset)
}
}
// A cut straight from programme to credits has no fade to find, and the split was already
// right. Moving it would be inventing a ramp that is not there.
func TestBackOffStopsAtAHardCut(t *testing.T) {
scores := make([]float64, 0, 60)
for range 40 {
scores = append(scores, 0.02)
}
for range 20 {
scores = append(scores, establishedCredits)
}
if onset := backOffToOnset(scores, 40, establishedCredits, fineInterval); onset != 40 {
t.Fatalf("a hard cut moved from 40 to %d", onset)
}
}
// The bound is what stops a scene dimming gradually towards the credits pulling the marker
// back through the last minute of the programme.
func TestBackOffIsBounded(t *testing.T) {
// Everything qualifies, so only the bound can stop the walk.
scores := make([]float64, 80)
for index := range scores {
scores[index] = establishedCredits
}
onset := backOffToOnset(scores, 60, establishedCredits, fineInterval)
limit := int(float64(onsetBackoffMs) / float64(fineInterval.Milliseconds()))
if onset != 60-limit {
t.Fatalf("walked back to %d, want %d (%d frames)", onset, 60-limit, limit)
}
if travelled := (60 - onset) * int(fineInterval.Milliseconds()); travelled > onsetBackoffMs {
t.Fatalf("walked back %dms, past the %dms bound", travelled, onsetBackoffMs)
}
}
// A frame that is dark but not yet mostly credit-like is still the programme.
func TestBackOffRefusesAFrameBelowTheOnsetFraction(t *testing.T) {
scores := []float64{0.02, 0.02, 0.10, establishedCredits, establishedCredits}
// 0.10 is well under onsetFraction of the established level.
if onset := backOffToOnset(scores, 3, establishedCredits, fineInterval); onset != 3 {
t.Fatalf("a below-threshold frame was walked into: onset %d, want 3", onset)
}
}
// Guarding the degenerate inputs rather than trusting callers, since this runs on whatever
// the sampler produced.
func TestBackOffHandlesDegenerateInput(t *testing.T) {
scores := []float64{establishedCredits, establishedCredits}
if onset := backOffToOnset(scores, 0, establishedCredits, fineInterval); onset != 0 {
t.Fatalf("a split at zero moved to %d", onset)
}
if onset := backOffToOnset(scores, 1, 0, fineInterval); onset != 1 {
t.Fatalf("an established level of zero moved the split to %d", onset)
}
}