"""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, _pause_density, _plugin_richness, _source_quality_weight, _speaking_pace_fitness, _specificity_density, _speech_rate_variance, _transcript_energy, _video_type_weight, extract_word_timings, 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", ) def _make_word_timings( start: float = 0.0, count: int = 40, wps: float = 4.0, pause_every: int | None = None, pause_duration: float = 0.8, ) -> list[dict]: """Generate synthetic word-timing dicts for testing. Parameters ---------- start : float Start time in seconds. count : int Number of words to generate. wps : float Words per second (base rate). pause_every : int | None Insert a pause every N words. None = no pauses. pause_duration : float Duration of each pause in seconds. """ timings = [] t = start word_dur = 1.0 / wps * 0.7 # 70% speaking, 30% normal gap gap = 1.0 / wps * 0.3 for i in range(count): timings.append({"word": f"word{i}", "start": t, "end": t + word_dur}) t += word_dur + gap if pause_every and (i + 1) % pause_every == 0: t += pause_duration return timings def _make_transcript_segments(word_timings: list[dict], words_per_segment: int = 10) -> list[dict]: """Group word timings into transcript segments for extract_word_timings tests.""" segments = [] for i in range(0, len(word_timings), words_per_segment): chunk = word_timings[i : i + words_per_segment] segments.append({"words": chunk}) return segments # ── 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"]) == 10 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_ten_dimensions(self): result = score_moment(**_ideal_moment()) assert len(result["score_breakdown"]) == 10 expected_keys = { "duration_score", "content_density_score", "technique_relevance_score", "plugin_diversity_score", "engagement_proxy_score", "position_score", "uniqueness_score", "speech_rate_variance_score", "pause_density_score", "speaking_pace_score", } assert set(result["score_breakdown"].keys()) == expected_keys def test_without_word_timings_audio_dims_are_neutral(self): """When word_timings is None, audio proxy dimensions score 0.5.""" result = score_moment(start_time=10.0, end_time=55.0) bd = result["score_breakdown"] assert bd["speech_rate_variance_score"] == 0.5 assert bd["pause_density_score"] == 0.5 assert bd["speaking_pace_score"] == 0.5 def test_with_word_timings_changes_score(self): """Providing word_timings should shift the composite score vs without.""" base = _ideal_moment() without = score_moment(**base) # Add word timings at a good teaching pace (~4 WPS) with some pauses timings = _make_word_timings(start=10.0, count=120, wps=4.0, pause_every=15) with_timings = score_moment(**base, word_timings=timings) # Scores should differ since audio dims are no longer neutral assert with_timings["score"] != without["score"] 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 # ── Audio proxy function tests ─────────────────────────────────────────────── class TestExtractWordTimings: def test_filters_by_time_window(self): words = _make_word_timings(start=0.0, count=40, wps=4.0) segments = _make_transcript_segments(words) # Extract window 2.0–5.0s result = extract_word_timings(segments, start_time=2.0, end_time=5.0) for w in result: assert 2.0 <= w["start"] <= 5.0 def test_returns_all_when_window_covers_entire_range(self): words = _make_word_timings(start=0.0, count=20, wps=4.0) segments = _make_transcript_segments(words) result = extract_word_timings(segments, start_time=0.0, end_time=100.0) assert len(result) == 20 def test_empty_transcript_data(self): assert extract_word_timings([], start_time=0.0, end_time=10.0) == [] def test_no_words_in_window(self): words = _make_word_timings(start=0.0, count=10, wps=4.0) segments = _make_transcript_segments(words) # Window far beyond the word timings result = extract_word_timings(segments, start_time=100.0, end_time=200.0) assert result == [] def test_segments_without_words_key(self): """Segments missing 'words' are skipped gracefully.""" segments = [{"text": "hello"}, {"words": [{"start": 1.0, "end": 1.2, "word": "a"}]}] result = extract_word_timings(segments, start_time=0.0, end_time=10.0) assert len(result) == 1 def test_words_without_start_are_skipped(self): segments = [{"words": [{"end": 1.2, "word": "a"}, {"start": 2.0, "end": 2.2, "word": "b"}]}] result = extract_word_timings(segments, start_time=0.0, end_time=10.0) assert len(result) == 1 assert result[0]["word"] == "b" class TestSpeechRateVariance: def test_none_returns_neutral(self): assert _speech_rate_variance(None) == 0.5 def test_too_few_words_returns_neutral(self): timings = _make_word_timings(count=3, wps=4.0) assert _speech_rate_variance(timings) == 0.5 def test_short_span_returns_neutral(self): """Words spanning <5s should return neutral.""" timings = _make_word_timings(count=10, wps=4.0, start=0.0) # 10 words at 4 WPS = 2.5s span → too short assert _speech_rate_variance(timings) == 0.5 def test_uniform_pace_scores_low(self): """Steady 4 WPS for 30s → low variance.""" timings = _make_word_timings(start=0.0, count=120, wps=4.0) score = _speech_rate_variance(timings) assert score < 0.4, f"Uniform pace scored {score}, expected < 0.4" def test_varied_pace_scores_higher(self): """Alternating fast/slow sections → higher variance.""" timings = [] t = 0.0 # Fast section: 6 WPS for 10s for i in range(60): dur = 0.12 timings.append({"word": f"w{i}", "start": t, "end": t + dur}) t += 1.0 / 6.0 # Slow section: 2 WPS for 10s for i in range(20): dur = 0.3 timings.append({"word": f"w{60+i}", "start": t, "end": t + dur}) t += 0.5 score = _speech_rate_variance(timings) uniform_score = _speech_rate_variance( _make_word_timings(start=0.0, count=80, wps=4.0) ) assert score > uniform_score, ( f"Varied pace ({score:.3f}) should be > uniform ({uniform_score:.3f})" ) def test_score_bounded(self): timings = _make_word_timings(start=0.0, count=200, wps=4.0) score = _speech_rate_variance(timings) assert 0.0 <= score <= 1.0 class TestPauseDensity: def test_none_returns_neutral(self): assert _pause_density(None) == 0.5 def test_single_word_returns_neutral(self): assert _pause_density([{"start": 0.0, "end": 0.2}]) == 0.5 def test_no_pauses_scores_zero(self): """Continuous speech with no gaps >0.5s → 0.""" timings = _make_word_timings(start=0.0, count=60, wps=4.0) score = _pause_density(timings) assert score == 0.0 def test_frequent_pauses_scores_high(self): """Pauses every 5 words → high density.""" timings = _make_word_timings(start=0.0, count=60, wps=4.0, pause_every=5, pause_duration=0.8) score = _pause_density(timings) assert score > 0.5, f"Frequent pauses scored {score}, expected > 0.5" def test_long_pauses_weighted_more(self): """One 1.5s pause should score higher than one 0.6s pause in a longer segment.""" # Build timings with one long pause at midpoint — 60 words for longer duration long_pause = [] t = 0.0 for i in range(60): long_pause.append({"word": f"w{i}", "start": t, "end": t + 0.15}) t += 0.25 if i == 29: t += 1.5 # long pause >1.0s # Build timings with one short pause — same word count short_pause = [] t = 0.0 for i in range(60): short_pause.append({"word": f"w{i}", "start": t, "end": t + 0.15}) t += 0.25 if i == 29: t += 0.6 # short pause >0.5s but <1.0s assert _pause_density(long_pause) > _pause_density(short_pause) def test_score_bounded(self): timings = _make_word_timings(start=0.0, count=60, wps=4.0, pause_every=3, pause_duration=1.5) score = _pause_density(timings) assert 0.0 <= score <= 1.0 class TestSpeakingPaceFitness: def test_none_returns_neutral(self): assert _speaking_pace_fitness(None) == 0.5 def test_single_word_returns_neutral(self): assert _speaking_pace_fitness([{"start": 0.0, "end": 0.2}]) == 0.5 def test_optimal_pace_scores_high(self): """4 WPS (optimal teaching pace) → 1.0.""" timings = _make_word_timings(start=0.0, count=40, wps=4.0) score = _speaking_pace_fitness(timings) assert score == 1.0, f"4 WPS scored {score}, expected 1.0" def test_three_wps_is_sweet_spot_edge(self): timings = _make_word_timings(start=0.0, count=30, wps=3.0) score = _speaking_pace_fitness(timings) assert score == 1.0 def test_five_wps_is_sweet_spot_edge(self): timings = _make_word_timings(start=0.0, count=50, wps=5.0) score = _speaking_pace_fitness(timings) assert score > 0.95, f"5 WPS scored {score}, expected near 1.0" def test_too_slow_scores_lower(self): """1.5 WPS → below sweet spot.""" timings = _make_word_timings(start=0.0, count=15, wps=1.5) score = _speaking_pace_fitness(timings) assert 0.4 < score < 0.6, f"1.5 WPS scored {score}, expected ~0.5" def test_too_fast_scores_lower(self): """8 WPS → above sweet spot.""" timings = _make_word_timings(start=0.0, count=80, wps=8.0) score = _speaking_pace_fitness(timings) assert 0.0 < score < 1.0 def test_very_fast_scores_zero(self): """10+ WPS → 0.""" timings = _make_word_timings(start=0.0, count=110, wps=11.0) score = _speaking_pace_fitness(timings) assert score == 0.0 def test_zero_wps_scores_zero(self): """Very short duration → neutral.""" timings = [{"start": 0.0, "end": 0.01}, {"start": 0.005, "end": 0.015}] score = _speaking_pace_fitness(timings) # Duration ~0.015s → too short → 0.5 (neutral) assert score == 0.5 def test_score_bounded(self): for wps in [0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0]: timings = _make_word_timings(start=0.0, count=max(10, int(wps * 10)), wps=wps) score = _speaking_pace_fitness(timings) assert 0.0 <= score <= 1.0, f"WPS {wps} scored {score} out of bounds" class TestBackwardCompatibility: """Ensure the weight rebalancing doesn't break existing relative orderings.""" def test_ideal_still_beats_poor(self): ideal = score_moment(**_ideal_moment()) poor = score_moment(**_poor_moment()) assert ideal["score"] > poor["score"] def test_ideal_still_above_threshold(self): result = score_moment(**_ideal_moment()) assert result["score"] > 0.6, f"Ideal scored {result['score']}, expected > 0.6" def test_poor_still_below_threshold(self): result = score_moment(**_poor_moment()) assert result["score"] < 0.45, f"Poor scored {result['score']}, expected < 0.45" def test_weights_sum_to_one(self): from backend.pipeline.highlight_scorer import _WEIGHTS assert abs(sum(_WEIGHTS.values()) - 1.0) < 1e-9