0.2.64 update
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package credits
|
||||
|
||||
import "time"
|
||||
|
||||
// How sure we are, and when that is sure enough to act.
|
||||
//
|
||||
// The governing rule is the brief's: prefer no marker to a wrong marker. A missing Skip
|
||||
// Credits button is an absence nobody notices; a button that appears during the final scene
|
||||
// and throws somebody past the ending is a fault they will remember, and one they cannot
|
||||
// undo without seeking back and finding their place. Every threshold here is set on that
|
||||
// asymmetry rather than on getting the most coverage.
|
||||
|
||||
const (
|
||||
// ConfidenceThreshold is the bar for storing a marker at all. Anything under it is
|
||||
// discarded — not stored with a low score for something later to filter, because a
|
||||
// stored marker is one a future season estimate will narrow a scan onto.
|
||||
ConfidenceThreshold = 0.70
|
||||
|
||||
// agreementTolerance is how far apart two independent findings can be and still be
|
||||
// describing the same transition. A visual detector reads the first frame of the roll;
|
||||
// a viewer presses stop a few seconds into it.
|
||||
agreementTolerance = 20 * time.Second
|
||||
|
||||
// disagreementPenalty is what a contradiction costs. Two signals pointing at different
|
||||
// places are not one strong finding and a weak one — they are evidence that at least one
|
||||
// detector is wrong about this file, and usually the right answer is to store nothing.
|
||||
disagreementPenalty = 0.3
|
||||
|
||||
// combinedCeiling caps agreement. Nothing here observes the credits directly, so a
|
||||
// certainty of 1 would be a claim the method cannot support.
|
||||
combinedCeiling = 0.98
|
||||
|
||||
// behaviourCeiling caps behaviour on its own. A household can agree precisely and still
|
||||
// be agreeing about the moment the last line of dialogue lands rather than the cut.
|
||||
behaviourCeiling = 0.90
|
||||
|
||||
// RewriteTolerance is how much a new reading may differ from a stored one before it is
|
||||
// worth a write. Readings of the same episode wobble by a few seconds; rewriting the row
|
||||
// each time would turn a subsystem whose whole claim is "one write per episode, ever"
|
||||
// into one that writes on every playback.
|
||||
RewriteTolerance = 12 * time.Second
|
||||
|
||||
// rewriteImprovement is how much better the evidence has to be for a rewrite *within*
|
||||
// tolerance to be worth doing. Inside the tolerance the position is not meaningfully
|
||||
// different, so the only reason to write is that the confidence changed enough to matter
|
||||
// to a later season estimate.
|
||||
rewriteImprovement = 0.15
|
||||
)
|
||||
|
||||
// BehaviourConfidence scores a stop cluster.
|
||||
//
|
||||
// Three things move it and they are independent: how many people agreed, how tightly they
|
||||
// agreed, and whether they pressed next rather than simply stopping. A next-episode
|
||||
// transition is the only one of the three that is unambiguous about *why* they left.
|
||||
func BehaviourConfidence(evidence BehaviourEvidence) float64 {
|
||||
if !evidence.Found || evidence.UserCount < behaviourNarrowMinUsers {
|
||||
return 0
|
||||
}
|
||||
score := 0.45
|
||||
// Each viewer past the second is worth less than the one before it: the step from two to
|
||||
// three is the one that rules out a coincidence, and the step from six to seven adds
|
||||
// almost nothing.
|
||||
for extra := 0; extra < evidence.UserCount-behaviourNarrowMinUsers; extra++ {
|
||||
score += 0.12 / float64(extra+1)
|
||||
}
|
||||
if evidence.NextEpisodeCount > 0 {
|
||||
score += 0.10 * float64(evidence.NextEpisodeCount) / float64(evidence.UserCount)
|
||||
}
|
||||
// Tightness, measured against the cluster tolerance: agreeing to within three seconds
|
||||
// earns nearly all of this, agreeing to within the full half-minute earns none of it.
|
||||
if tolerance := behaviourCluster.Milliseconds(); tolerance > 0 {
|
||||
tightness := 1 - float64(evidence.SpreadMs)/float64(tolerance)
|
||||
if tightness > 0 {
|
||||
score += 0.15 * tightness
|
||||
}
|
||||
}
|
||||
if score > behaviourCeiling {
|
||||
score = behaviourCeiling
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// Combine reconciles a visual detection with behavioural evidence into the marker that will
|
||||
// be stored, or into nothing.
|
||||
//
|
||||
// The interesting case is agreement, and the reason to want it is that the two signals fail
|
||||
// in unrelated ways: a visual detector is fooled by a dark, static final scene, and a stop
|
||||
// cluster is fooled by an episode everybody happened to abandon at the same point. Neither
|
||||
// failure makes the other more likely, so agreement between them is worth far more than
|
||||
// either on its own — which is why the combination is a probabilistic union rather than an
|
||||
// average, and why an average would have been the wrong shape entirely.
|
||||
func Combine(visual Detection, evidence BehaviourEvidence) (Detection, bool) {
|
||||
behaviourScore := BehaviourConfidence(evidence)
|
||||
hasBehaviour := evidence.Found && behaviourScore > 0
|
||||
|
||||
switch {
|
||||
case visual.Found && hasBehaviour:
|
||||
gap := visual.StartMs - evidence.StartMs
|
||||
if gap < 0 {
|
||||
gap = -gap
|
||||
}
|
||||
if gap <= agreementTolerance.Milliseconds() {
|
||||
combined := visual
|
||||
combined.Method = MethodCombined
|
||||
// The earlier of the two. A skip that begins at the first frame of the roll is
|
||||
// correct; one that begins a few seconds in has already shown the viewer the
|
||||
// thing they asked to skip.
|
||||
if evidence.StartMs < combined.StartMs {
|
||||
combined.StartMs = evidence.StartMs
|
||||
}
|
||||
combined.Confidence = union(visual.Confidence, behaviourScore)
|
||||
if combined.Confidence > combinedCeiling {
|
||||
combined.Confidence = combinedCeiling
|
||||
}
|
||||
return combined, combined.Confidence >= ConfidenceThreshold
|
||||
}
|
||||
// They disagree. Take whichever is stronger, pay the penalty, and let the threshold
|
||||
// decide — which, with a penalty this size, usually means storing nothing.
|
||||
stronger := visual
|
||||
stronger.Method = MethodVisual
|
||||
if behaviourScore > visual.Confidence {
|
||||
stronger = Detection{
|
||||
Found: true,
|
||||
StartMs: evidence.StartMs,
|
||||
Confidence: behaviourScore,
|
||||
Method: MethodBehaviour,
|
||||
}
|
||||
}
|
||||
stronger.Confidence -= disagreementPenalty
|
||||
return stronger, stronger.Confidence >= ConfidenceThreshold
|
||||
|
||||
case visual.Found:
|
||||
visual.Method = MethodVisual
|
||||
return visual, visual.Confidence >= ConfidenceThreshold
|
||||
|
||||
case hasBehaviour && evidence.StandaloneMarker():
|
||||
detection := Detection{
|
||||
Found: true,
|
||||
StartMs: evidence.StartMs,
|
||||
Confidence: behaviourScore,
|
||||
Method: MethodBehaviour,
|
||||
}
|
||||
return detection, detection.Confidence >= ConfidenceThreshold
|
||||
|
||||
default:
|
||||
return Detection{}, false
|
||||
}
|
||||
}
|
||||
|
||||
// union combines two independent probabilities: the chance that at least one of them is
|
||||
// right, which is what independence buys.
|
||||
func union(a, b float64) float64 {
|
||||
if a < 0 {
|
||||
a = 0
|
||||
}
|
||||
if b < 0 {
|
||||
b = 0
|
||||
}
|
||||
return 1 - (1-a)*(1-b)
|
||||
}
|
||||
|
||||
// ShouldRewrite decides whether new evidence justifies touching an existing row.
|
||||
//
|
||||
// The default answer is no. Once an episode has a good marker it should never be written
|
||||
// again, and this function is the only thing standing between that promise and a row that is
|
||||
// updated every time somebody watches the episode.
|
||||
func ShouldRewrite(existing Marker, candidate Detection) bool {
|
||||
// An operator's correction is final. Nothing automatic may overwrite a position somebody
|
||||
// set by hand, whatever it thinks it has found.
|
||||
if existing.DetectionMethod == MethodManual {
|
||||
return false
|
||||
}
|
||||
if !candidate.Found || candidate.Confidence < ConfidenceThreshold {
|
||||
return false
|
||||
}
|
||||
gap := candidate.StartMs - existing.CreditsStartMs
|
||||
if gap < 0 {
|
||||
gap = -gap
|
||||
}
|
||||
if gap <= RewriteTolerance.Milliseconds() {
|
||||
// Same position, within the wobble. Only a materially better score is worth a write,
|
||||
// and only because a later season estimate will weigh this row by its confidence.
|
||||
return candidate.Confidence >= existing.Confidence+rewriteImprovement
|
||||
}
|
||||
// A genuinely different position. It has to be better evidence than what is already
|
||||
// there, not merely different — otherwise two detectors that disagree would rewrite the
|
||||
// row past each other for ever.
|
||||
return candidate.Confidence > existing.Confidence
|
||||
}
|
||||
Reference in New Issue
Block a user