- "backend/pipeline/highlight_scorer.py" - "backend/pipeline/test_highlight_scorer.py" GSD-Task: S04/T02
244 lines
8.8 KiB
Python
244 lines
8.8 KiB
Python
"""Tests for the highlight scoring engine.
|
|
|
|
Verifies heuristic scoring produces sensible orderings and handles
|
|
edge cases gracefully.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from backend.pipeline.highlight_scorer import (
|
|
_content_type_weight,
|
|
_duration_fitness,
|
|
_plugin_richness,
|
|
_source_quality_weight,
|
|
_specificity_density,
|
|
_transcript_energy,
|
|
_video_type_weight,
|
|
score_moment,
|
|
)
|
|
|
|
|
|
# ── Fixture helpers ──────────────────────────────────────────────────────────
|
|
|
|
def _ideal_moment() -> dict:
|
|
"""45s technique moment, 3 plugins, specific summary, structured source."""
|
|
return dict(
|
|
start_time=10.0,
|
|
end_time=55.0, # 45s duration
|
|
content_type="technique",
|
|
summary=(
|
|
"Set the compressor threshold to -18 dB with a 4:1 ratio, "
|
|
"then boost the high shelf at 12 kHz by 3.5 dB using FabFilter Pro-Q 3."
|
|
),
|
|
plugins=["FabFilter Pro-Q 3", "SSL G-Bus Compressor", "Valhalla Room"],
|
|
raw_transcript=(
|
|
"The trick is to set the threshold low enough. Notice how "
|
|
"the compressor grabs the transients. Because we want to preserve "
|
|
"the dynamics, I always back off the ratio. The key is finding "
|
|
"that sweet spot where it's controlling but not squashing."
|
|
),
|
|
source_quality="structured",
|
|
video_content_type="tutorial",
|
|
)
|
|
|
|
|
|
def _mediocre_moment() -> dict:
|
|
"""90s settings moment, 1 plugin, decent summary, mixed source."""
|
|
return dict(
|
|
start_time=120.0,
|
|
end_time=210.0, # 90s duration
|
|
content_type="settings",
|
|
summary="Adjust the EQ settings for the vocal track to get a clearer sound.",
|
|
plugins=["FabFilter Pro-Q 3"],
|
|
raw_transcript=(
|
|
"So here we're just going to adjust this. I think it sounds "
|
|
"better when we cut some of the low end. Let me show you what "
|
|
"I mean. Yeah, that's better."
|
|
),
|
|
source_quality="mixed",
|
|
video_content_type="breakdown",
|
|
)
|
|
|
|
|
|
def _poor_moment() -> dict:
|
|
"""300s reasoning moment, 0 plugins, vague summary, unstructured source."""
|
|
return dict(
|
|
start_time=0.0,
|
|
end_time=300.0, # 300s duration → zero for duration_fitness
|
|
content_type="reasoning",
|
|
summary="General discussion about mixing philosophy and approach.",
|
|
plugins=[],
|
|
raw_transcript=(
|
|
"I think mixing is really about taste. Everyone has their own "
|
|
"approach. Some people like it loud, some people like it quiet. "
|
|
"There's no right or wrong way to do it really."
|
|
),
|
|
source_quality="unstructured",
|
|
video_content_type="livestream",
|
|
)
|
|
|
|
|
|
# ── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
class TestScoreMoment:
|
|
def test_ideal_moment_scores_high(self):
|
|
result = score_moment(**_ideal_moment())
|
|
assert result["score"] > 0.7, f"Ideal moment scored {result['score']}, expected > 0.7"
|
|
|
|
def test_poor_moment_scores_low(self):
|
|
result = score_moment(**_poor_moment())
|
|
assert result["score"] < 0.4, f"Poor moment scored {result['score']}, expected < 0.4"
|
|
|
|
def test_ordering_is_sensible(self):
|
|
ideal = score_moment(**_ideal_moment())
|
|
mediocre = score_moment(**_mediocre_moment())
|
|
poor = score_moment(**_poor_moment())
|
|
|
|
assert ideal["score"] > mediocre["score"] > poor["score"], (
|
|
f"Expected ideal ({ideal['score']:.3f}) > "
|
|
f"mediocre ({mediocre['score']:.3f}) > "
|
|
f"poor ({poor['score']:.3f})"
|
|
)
|
|
|
|
def test_score_bounds(self):
|
|
"""All scores in [0.0, 1.0] for edge cases."""
|
|
edge_cases = [
|
|
dict(start_time=0, end_time=0, summary="", plugins=None, raw_transcript=None),
|
|
dict(start_time=0, end_time=500, summary=None, plugins=[], raw_transcript=""),
|
|
dict(start_time=0, end_time=45, summary="x" * 10000, plugins=["a"] * 100),
|
|
dict(start_time=100, end_time=100), # zero duration
|
|
]
|
|
for kwargs in edge_cases:
|
|
result = score_moment(**kwargs)
|
|
assert 0.0 <= result["score"] <= 1.0, f"Score {result['score']} out of bounds for {kwargs}"
|
|
for dim, val in result["score_breakdown"].items():
|
|
assert 0.0 <= val <= 1.0, f"{dim}={val} out of bounds for {kwargs}"
|
|
|
|
def test_missing_optional_fields(self):
|
|
"""None raw_transcript and None plugins don't crash."""
|
|
result = score_moment(
|
|
start_time=10.0,
|
|
end_time=55.0,
|
|
content_type="technique",
|
|
summary="A summary.",
|
|
plugins=None,
|
|
raw_transcript=None,
|
|
source_quality=None,
|
|
video_content_type=None,
|
|
)
|
|
assert 0.0 <= result["score"] <= 1.0
|
|
assert result["duration_secs"] == 45.0
|
|
assert len(result["score_breakdown"]) == 7
|
|
|
|
def test_returns_duration_secs(self):
|
|
result = score_moment(start_time=10.0, end_time=55.0)
|
|
assert result["duration_secs"] == 45.0
|
|
|
|
def test_breakdown_has_seven_dimensions(self):
|
|
result = score_moment(**_ideal_moment())
|
|
assert len(result["score_breakdown"]) == 7
|
|
expected_keys = {
|
|
"duration_score", "content_density_score", "technique_relevance_score",
|
|
"plugin_diversity_score", "engagement_proxy_score", "position_score",
|
|
"uniqueness_score",
|
|
}
|
|
assert set(result["score_breakdown"].keys()) == expected_keys
|
|
|
|
|
|
class TestDurationFitness:
|
|
def test_bell_curve_peak(self):
|
|
"""45s scores higher than 10s, 10s scores higher than 400s."""
|
|
assert _duration_fitness(45) > _duration_fitness(10)
|
|
assert _duration_fitness(10) > _duration_fitness(400)
|
|
|
|
def test_sweet_spot(self):
|
|
assert _duration_fitness(30) == 1.0
|
|
assert _duration_fitness(45) == 1.0
|
|
assert _duration_fitness(60) == 1.0
|
|
|
|
def test_zero_at_extremes(self):
|
|
assert _duration_fitness(0) == 0.0
|
|
assert _duration_fitness(300) == 0.0
|
|
assert _duration_fitness(500) == 0.0
|
|
|
|
def test_negative_duration(self):
|
|
assert _duration_fitness(-10) == 0.0
|
|
|
|
|
|
class TestContentTypeWeight:
|
|
def test_technique_highest(self):
|
|
assert _content_type_weight("technique") == 1.0
|
|
|
|
def test_reasoning_lowest_known(self):
|
|
assert _content_type_weight("reasoning") == 0.4
|
|
|
|
def test_unknown_gets_default(self):
|
|
assert _content_type_weight("unknown") == 0.5
|
|
assert _content_type_weight(None) == 0.5
|
|
|
|
|
|
class TestSpecificityDensity:
|
|
def test_specific_summary_scores_high(self):
|
|
summary = "Set threshold to -18 dB with 4:1 ratio, boost 12 kHz by 3.5 dB"
|
|
score = _specificity_density(summary)
|
|
assert score > 0.5
|
|
|
|
def test_vague_summary_scores_low(self):
|
|
score = _specificity_density("General discussion about mixing philosophy.")
|
|
assert score < 0.3
|
|
|
|
def test_empty_returns_zero(self):
|
|
assert _specificity_density("") == 0.0
|
|
assert _specificity_density(None) == 0.0
|
|
|
|
|
|
class TestPluginRichness:
|
|
def test_three_plugins_maxes_out(self):
|
|
assert _plugin_richness(["a", "b", "c"]) == 1.0
|
|
|
|
def test_more_than_three_capped(self):
|
|
assert _plugin_richness(["a", "b", "c", "d"]) == 1.0
|
|
|
|
def test_empty(self):
|
|
assert _plugin_richness([]) == 0.0
|
|
assert _plugin_richness(None) == 0.0
|
|
|
|
|
|
class TestTranscriptEnergy:
|
|
def test_teaching_phrases_score_high(self):
|
|
transcript = (
|
|
"The trick is to notice how the compressor behaves. "
|
|
"Because we want dynamics, I always set it gently. The key is balance."
|
|
)
|
|
score = _transcript_energy(transcript)
|
|
assert score > 0.5
|
|
|
|
def test_bland_transcript_scores_low(self):
|
|
transcript = "And then we adjust this slider here. Okay that sounds fine."
|
|
score = _transcript_energy(transcript)
|
|
assert score < 0.3
|
|
|
|
def test_empty(self):
|
|
assert _transcript_energy("") == 0.0
|
|
assert _transcript_energy(None) == 0.0
|
|
|
|
|
|
class TestSourceQualityWeight:
|
|
def test_structured_highest(self):
|
|
assert _source_quality_weight("structured") == 1.0
|
|
|
|
def test_none_default(self):
|
|
assert _source_quality_weight(None) == 0.5
|
|
|
|
|
|
class TestVideoTypeWeight:
|
|
def test_tutorial_highest(self):
|
|
assert _video_type_weight("tutorial") == 1.0
|
|
|
|
def test_short_form_lowest(self):
|
|
assert _video_type_weight("short_form") == 0.3
|
|
|
|
def test_none_default(self):
|
|
assert _video_type_weight(None) == 0.5
|