0.2.64 update
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package credits
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Where the scan looks, and how it is narrowed.
|
||||
//
|
||||
// This is the file that decides how much of a media file is ever read, so it is where the
|
||||
// performance claim of the whole subsystem is either kept or given away. A generic tail
|
||||
// window is a fallback; the interesting case is the second episode of a season onwards,
|
||||
// where the first one has already told us where this show puts its credits.
|
||||
|
||||
// Provenance of a window, reported by the benchmark so "is demand-driven narrowing actually
|
||||
// saving work" is a question with a printed answer rather than an opinion.
|
||||
const (
|
||||
WindowGeneric = "generic-tail-window"
|
||||
WindowSeason = "season-history"
|
||||
WindowBehaviour = "tracearr-behaviour"
|
||||
)
|
||||
|
||||
const (
|
||||
// The generic tail, when nothing is known: a fifth of the runtime, floored at five
|
||||
// minutes so a short episode still has somewhere to look and capped at twelve so a
|
||||
// three-hour film does not turn into a thirty-six minute scan.
|
||||
genericTailFraction = 0.20
|
||||
genericTailMinimum = 5 * time.Minute
|
||||
genericTailMaximum = 12 * time.Minute
|
||||
|
||||
// The margin either side of an expected position. Wide enough to absorb a cold open
|
||||
// that ran long or a differently-cut episode, narrow enough that the scan is a couple of
|
||||
// minutes rather than ten.
|
||||
narrowMargin = 90 * time.Second
|
||||
|
||||
// seasonSpread is how much disagreement between known episodes is tolerated before the
|
||||
// season stops being evidence. A show that puts its credits at a consistent point is
|
||||
// usable; one whose known markers are three minutes apart is telling us the episodes are
|
||||
// not structurally alike, and narrowing on their average would look in the wrong place.
|
||||
seasonSpread = 2 * time.Minute
|
||||
|
||||
// seasonMinimumSamples is how many known episodes it takes. One is an anecdote — it may
|
||||
// itself be the mis-detection — and narrowing a scan onto a single unconfirmed reading is
|
||||
// how one wrong marker propagates through a whole season.
|
||||
seasonMinimumSamples = 2
|
||||
)
|
||||
|
||||
// ScanWindow is a span of the file, in milliseconds from its start.
|
||||
type ScanWindow struct {
|
||||
StartMs int64
|
||||
EndMs int64
|
||||
Source string
|
||||
}
|
||||
|
||||
func (w ScanWindow) DurationMs() int64 {
|
||||
if w.EndMs <= w.StartMs {
|
||||
return 0
|
||||
}
|
||||
return w.EndMs - w.StartMs
|
||||
}
|
||||
|
||||
func (w ScanWindow) Valid() bool { return w.DurationMs() > 0 }
|
||||
|
||||
// GenericTailWindow is where to look when nothing at all is known about the show.
|
||||
func GenericTailWindow(runtimeMs int64) ScanWindow {
|
||||
if runtimeMs <= 0 {
|
||||
return ScanWindow{}
|
||||
}
|
||||
tail := int64(float64(runtimeMs) * genericTailFraction)
|
||||
if minimum := genericTailMinimum.Milliseconds(); tail < minimum {
|
||||
tail = minimum
|
||||
}
|
||||
if maximum := genericTailMaximum.Milliseconds(); tail > maximum {
|
||||
tail = maximum
|
||||
}
|
||||
start := runtimeMs - tail
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
return ScanWindow{StartMs: start, EndMs: runtimeMs, Source: WindowGeneric}
|
||||
}
|
||||
|
||||
// aroundWindow is a narrow span centred on an expected position, clamped to the file.
|
||||
func aroundWindow(runtimeMs, expectedMs int64, source string) ScanWindow {
|
||||
margin := narrowMargin.Milliseconds()
|
||||
start := expectedMs - margin
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := expectedMs + margin
|
||||
if runtimeMs > 0 && end > runtimeMs {
|
||||
end = runtimeMs
|
||||
}
|
||||
if end <= start {
|
||||
return ScanWindow{}
|
||||
}
|
||||
return ScanWindow{StartMs: start, EndMs: end, Source: source}
|
||||
}
|
||||
|
||||
// seasonExpectation is where this show's credits are expected to start, learned from
|
||||
// episodes of the same season that have already been decided.
|
||||
//
|
||||
// Measured as a *tail offset* — how long the credits run for — rather than as an absolute
|
||||
// position, which is the one modelling decision in this file worth defending. Episodes of a
|
||||
// season vary in length by a minute or two; the credits sequence itself does not vary at all,
|
||||
// because it is the same sequence. Averaging absolute positions would smear that variation
|
||||
// into the estimate and force a wider margin to cover it.
|
||||
func seasonExpectation(runtimeMs int64, history []Marker, runtimeOf func(Marker) int64) (int64, bool) {
|
||||
if runtimeMs <= 0 || len(history) < seasonMinimumSamples {
|
||||
return 0, false
|
||||
}
|
||||
offsets := make([]int64, 0, len(history))
|
||||
for _, marker := range history {
|
||||
episodeRuntime := runtimeOf(marker)
|
||||
if episodeRuntime <= 0 || marker.CreditsStartMs <= 0 || marker.CreditsStartMs >= episodeRuntime {
|
||||
continue
|
||||
}
|
||||
// Only markers we would trust ourselves are allowed to steer a scan. A low-confidence
|
||||
// reading is exactly the one that should be re-examined, not the one that decides
|
||||
// where everything else looks.
|
||||
if marker.Confidence < ConfidenceThreshold {
|
||||
continue
|
||||
}
|
||||
offsets = append(offsets, episodeRuntime-marker.CreditsStartMs)
|
||||
}
|
||||
if len(offsets) < seasonMinimumSamples {
|
||||
return 0, false
|
||||
}
|
||||
sort.Slice(offsets, func(a, b int) bool { return offsets[a] < offsets[b] })
|
||||
if offsets[len(offsets)-1]-offsets[0] > seasonSpread.Milliseconds() {
|
||||
return 0, false
|
||||
}
|
||||
// The median, not the mean: one episode with a long "next time on" trailer after the
|
||||
// credits must not drag the estimate, and with two samples the median of the sorted pair
|
||||
// is the lower — the safer end, since looking slightly early costs nothing but looking
|
||||
// late misses the transition entirely.
|
||||
median := offsets[len(offsets)/2]
|
||||
expected := runtimeMs - median
|
||||
if expected <= 0 || expected >= runtimeMs {
|
||||
return 0, false
|
||||
}
|
||||
return expected, true
|
||||
}
|
||||
|
||||
// NarrowWindow decides where to look, preferring the strongest evidence available.
|
||||
//
|
||||
// Order matters and is deliberate. Behaviour first: where enough viewers stopped is a direct
|
||||
// observation of this exact episode, where season history is an inference from its
|
||||
// neighbours. Season history second. The generic tail last, and only because something has
|
||||
// to be.
|
||||
func NarrowWindow(
|
||||
runtimeMs int64,
|
||||
history []Marker,
|
||||
runtimeOf func(Marker) int64,
|
||||
behaviour BehaviourEvidence,
|
||||
) ScanWindow {
|
||||
if runtimeMs <= 0 {
|
||||
return ScanWindow{}
|
||||
}
|
||||
if behaviour.Usable() {
|
||||
if window := aroundWindow(runtimeMs, behaviour.StartMs, WindowBehaviour); window.Valid() {
|
||||
return window
|
||||
}
|
||||
}
|
||||
if runtimeOf != nil {
|
||||
if expected, ok := seasonExpectation(runtimeMs, history, runtimeOf); ok {
|
||||
if window := aroundWindow(runtimeMs, expected, WindowSeason); window.Valid() {
|
||||
return window
|
||||
}
|
||||
}
|
||||
}
|
||||
return GenericTailWindow(runtimeMs)
|
||||
}
|
||||
Reference in New Issue
Block a user