0.2.64 update
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package credits
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The visual changepoint, tested on synthesised frame statistics rather than on media.
|
||||
//
|
||||
// findTransition is the whole of the visual decision — everything above it is ffmpeg
|
||||
// plumbing — and it is pure, so the cases that matter can be built as literals: a clean
|
||||
// transition, a dark final scene that must not be mistaken for one, and a file with no
|
||||
// credits at all.
|
||||
|
||||
// programmeFrame is an ordinary scene: mid-bright, varied, detailed.
|
||||
func programmeFrame(position time.Duration) frameStats {
|
||||
return frameStats{
|
||||
PositionMs: position.Milliseconds(),
|
||||
Mean: 0.42,
|
||||
Variance: 0.055,
|
||||
DarkFraction: 0.10,
|
||||
EdgeDensity: 0.075,
|
||||
Diff: 0.08,
|
||||
}
|
||||
}
|
||||
|
||||
// creditsFrame is text on black: dark, flat, and textured in the narrow band text produces.
|
||||
func creditsFrame(position time.Duration) frameStats {
|
||||
return frameStats{
|
||||
PositionMs: position.Milliseconds(),
|
||||
Mean: 0.05,
|
||||
Variance: 0.006,
|
||||
DarkFraction: 0.94,
|
||||
EdgeDensity: 0.030,
|
||||
Diff: 0.02,
|
||||
}
|
||||
}
|
||||
|
||||
// darkSceneFrame is the trap: a night exterior is dark and flat but carries none of the fine
|
||||
// detail text does.
|
||||
func darkSceneFrame(position time.Duration) frameStats {
|
||||
return frameStats{
|
||||
PositionMs: position.Milliseconds(),
|
||||
Mean: 0.08,
|
||||
Variance: 0.010,
|
||||
DarkFraction: 0.88,
|
||||
EdgeDensity: 0.004,
|
||||
Diff: 0.05,
|
||||
}
|
||||
}
|
||||
|
||||
func window(start time.Duration, kinds ...func(time.Duration) frameStats) []frameStats {
|
||||
frames := make([]frameStats, 0, len(kinds))
|
||||
for index, build := range kinds {
|
||||
frames = append(frames, build(start+time.Duration(index)*coarseInterval))
|
||||
}
|
||||
return frames
|
||||
}
|
||||
|
||||
func repeatFrames(count int, build func(time.Duration) frameStats) []func(time.Duration) frameStats {
|
||||
out := make([]func(time.Duration) frameStats, 0, count)
|
||||
for index := 0; index < count; index++ {
|
||||
out = append(out, build)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestFindsACleanTransition(t *testing.T) {
|
||||
// Two minutes of programme, then two minutes of credits, at one frame every four seconds.
|
||||
kinds := append(repeatFrames(30, programmeFrame), repeatFrames(30, creditsFrame)...)
|
||||
frames := window(39*time.Minute, kinds...)
|
||||
|
||||
index, separation, found := findTransition(frames, coarseInterval)
|
||||
if !found {
|
||||
t.Fatal("a clean transition was not found")
|
||||
}
|
||||
if index != 30 {
|
||||
t.Fatalf("transition at frame %d, want 30", index)
|
||||
}
|
||||
if separation < minSeparation {
|
||||
t.Fatalf("separation %.3f below the threshold %.3f", separation, minSeparation)
|
||||
}
|
||||
if score := visualConfidence(separation); score < ConfidenceThreshold {
|
||||
t.Fatalf("confidence %.2f below the threshold", score)
|
||||
}
|
||||
}
|
||||
|
||||
// The whole point of not answering on darkness alone.
|
||||
func TestDarkSceneIsNotCredits(t *testing.T) {
|
||||
kinds := append(repeatFrames(30, programmeFrame), repeatFrames(30, darkSceneFrame)...)
|
||||
frames := window(39*time.Minute, kinds...)
|
||||
|
||||
if _, _, found := findTransition(frames, coarseInterval); found {
|
||||
t.Fatal("a dark night scene was reported as credits")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoTransitionInOrdinaryProgramme(t *testing.T) {
|
||||
frames := window(39*time.Minute, repeatFrames(60, programmeFrame)...)
|
||||
if _, _, found := findTransition(frames, coarseInterval); found {
|
||||
t.Fatal("found a transition in a window with no transition in it")
|
||||
}
|
||||
}
|
||||
|
||||
// A window that started too late shows credits from its first frame, which is evidence about
|
||||
// the window rather than about the file.
|
||||
func TestCreditsFromTheFirstFrameAreNotATransition(t *testing.T) {
|
||||
frames := window(41*time.Minute, repeatFrames(50, creditsFrame)...)
|
||||
if _, _, found := findTransition(frames, coarseInterval); found {
|
||||
t.Fatal("an all-credits window was read as a transition")
|
||||
}
|
||||
}
|
||||
|
||||
// A dark beat at the end of an act is not sustained; a credits roll is.
|
||||
func TestBriefDarkBeatIsIgnored(t *testing.T) {
|
||||
kinds := append(repeatFrames(20, programmeFrame), repeatFrames(3, creditsFrame)...)
|
||||
kinds = append(kinds, repeatFrames(30, programmeFrame)...)
|
||||
frames := window(39*time.Minute, kinds...)
|
||||
|
||||
if _, _, found := findTransition(frames, coarseInterval); found {
|
||||
t.Fatal("a three-frame dark beat was reported as credits")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTooFewFramesAnswerNothing(t *testing.T) {
|
||||
frames := window(41*time.Minute, repeatFrames(4, creditsFrame)...)
|
||||
if _, _, found := findTransition(frames, coarseInterval); found {
|
||||
t.Fatal("four frames were enough to claim a transition")
|
||||
}
|
||||
if _, _, found := findTransition(nil, coarseInterval); found {
|
||||
t.Fatal("no frames produced a transition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreditScoreSeparatesTheThreeCases(t *testing.T) {
|
||||
credits := creditScore(creditsFrame(0))
|
||||
dark := creditScore(darkSceneFrame(0))
|
||||
programme := creditScore(programmeFrame(0))
|
||||
|
||||
if credits <= dark || dark <= programme {
|
||||
t.Fatalf("scores do not separate: credits %.2f, dark scene %.2f, programme %.2f",
|
||||
credits, dark, programme)
|
||||
}
|
||||
if credits < creditLikeFloor {
|
||||
t.Fatalf("a textbook credits frame scored %.2f, below the floor %.2f",
|
||||
credits, creditLikeFloor)
|
||||
}
|
||||
if dark >= creditLikeFloor {
|
||||
t.Fatalf("a dark scene scored %.2f, at or above the credits floor %.2f",
|
||||
dark, creditLikeFloor)
|
||||
}
|
||||
}
|
||||
|
||||
// Confidence from one detector agreeing with itself is not corroboration.
|
||||
func TestVisualConfidenceIsCapped(t *testing.T) {
|
||||
if score := visualConfidence(10); score >= 1 {
|
||||
t.Fatalf("visual confidence reached %.2f", score)
|
||||
}
|
||||
if score := visualConfidence(minSeparation); score < ConfidenceThreshold {
|
||||
t.Fatalf("a detection at the separation threshold scored %.2f, below the bar", score)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user