feat: onset-aware section scoring so slow swells rank at the bottom
A section's score is now its peak dB above the noise floor capped by the sharpest rise within ONSET_SECONDS (0.5 s). Real events (voices, impacts, barks) rise fast and keep their full prominence; a gradual swell that outruns the 30 s floor blocks (gusts, distant approaching cars) still flags but scores near zero, so score-ranked review (chips, U/I highlights, "Highlights only" mode) surfaces events first. A section starting in a file's first 0.5 s is scored against the floor instead, so events cut off by a file split are not punished as swells. Old cached analyses carry now-wrong scores, so the cache gains a leading "detector" version key (DETECTOR_VERSION = 2) checked by both _cached_analysis_params() and the /api/analyze cache hit path; v1 caches never match and are recomputed on the next analyse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,37 @@ def test_min_duration_applies_after_gap_merging():
|
||||
assert sections[0]['end'] >= 61.0
|
||||
|
||||
|
||||
def test_slow_swell_scores_near_zero_sharp_burst_scores_high():
|
||||
# A ~10 s swell rises faster than the 30 s floor blocks can track, so it
|
||||
# still flags — but its score must collapse (onset cap) so it ranks at the
|
||||
# bottom, while a sharp burst with the same peak keeps its full score.
|
||||
rms = [0.002] * 1200
|
||||
for k in range(80): # rise −54 → −28 dB over 8 s
|
||||
rms[400 + k] = 0.002 * 10 ** (26 * (k / 80) / 20)
|
||||
rms[480:500] = [0.04] * 20 # hold 2 s at −28 dB
|
||||
for k in range(80): # fall back over 8 s
|
||||
rms[500 + k] = 0.04 * 10 ** (-26 * (k / 80) / 20)
|
||||
rms[900:910] = [0.04] * 10 # sharp 1 s burst, same peak
|
||||
sections = _run(rms)
|
||||
assert len(sections) == 2
|
||||
swell, burst = sections
|
||||
assert swell['start'] < 50.0 < 90.0 <= burst['start']
|
||||
assert burst['score'] >= 24.0 # ≈ full 26 dB prominence
|
||||
assert swell['score'] <= 5.0 # capped by its slow onset
|
||||
assert swell['score'] < burst['score']
|
||||
|
||||
|
||||
def test_burst_at_file_start_keeps_full_score():
|
||||
# No pre-event history to measure a rise against: the onset cap falls back
|
||||
# to prominence above the floor, so events cut off by a file split are not
|
||||
# punished as swells.
|
||||
rms = [0.05] * 10 + [0.002] * 1190
|
||||
sections = _run(rms)
|
||||
assert len(sections) == 1
|
||||
assert sections[0]['start'] == 0.0
|
||||
assert sections[0]['score'] >= 24.0
|
||||
|
||||
|
||||
def test_noise_floor_tracks_blocks_and_ignores_short_events():
|
||||
quiet_db = 20 * math.log10(0.002)
|
||||
db = [quiet_db] * 1200
|
||||
|
||||
Reference in New Issue
Block a user