notes: keep matched words that fall inside a missing-lyrics span
A missing span covers whole Whisper segments, so it often also holds lines that were heard and matched. Skipping every aligned word starting inside one dropped those lines' notes and left empty line breaks in the chart. Words from lines never heard are already excluded via dropped_lines, so the span now only drives the warning and the review marker.
This commit is contained in:
+2
-10
@@ -84,19 +84,11 @@ def build_notes(rows: list[dict], times, f0, conf, language: str | None,
|
||||
) -> tuple[list[TimedNote], list[float], list[str]]:
|
||||
notes: list[TimedNote] = []
|
||||
breaks: list[float] = []
|
||||
prev_line, prev_end, last_seen_line = None, 0.0, None
|
||||
prev_line, prev_end = None, 0.0
|
||||
for row in rows:
|
||||
# last_seen_line tracks every row (skipped or not) so a run of
|
||||
# skipped rows can't trigger a duplicate break on the next row of
|
||||
# the same still-unseen line; prev_line/prev_end only advance on
|
||||
# rows that actually emit notes, so a skipped row can't corrupt
|
||||
# line_initial or break timing for its own line's later words.
|
||||
if last_seen_line is not None and row["line_index"] > last_seen_line:
|
||||
if prev_line is not None and row["line_index"] > prev_line:
|
||||
breaks.append((prev_end + row["start"]) / 2)
|
||||
last_seen_line = row["line_index"]
|
||||
line_initial = row["line_index"] != prev_line
|
||||
if any(a <= row["start"] < b for a, b in missing_spans):
|
||||
continue
|
||||
prev_line, prev_end = row["line_index"], row["end"]
|
||||
segs = segment_span(times, f0, conf, row["start"], row["end"])
|
||||
if not segs:
|
||||
|
||||
@@ -47,9 +47,8 @@ def classify_lyrics_match(segments, ref: RefLyrics, pad: float = 1.0
|
||||
dropped_lines: reference line indices covered only by 'insert' opcodes
|
||||
(present in the lyrics, never heard). missing_spans: the padded, merged
|
||||
bounds of the heard segments behind 'delete' opcodes (heard, but matching
|
||||
no reference word) — scoped tightly to the unmatched audio itself, not
|
||||
the whole vocal section it falls in, so matched lines sharing a section
|
||||
with an ad-lib keep their notes.
|
||||
no reference word). A segment can also hold matched lines, so a span
|
||||
marks where to look, not words to discard.
|
||||
"""
|
||||
matcher, seg_of, line_of = match_lyrics(segments, ref)
|
||||
covered = [False] * len(line_of)
|
||||
|
||||
+9
-22
@@ -297,15 +297,19 @@ def test_notes_stage_registered():
|
||||
assert "notes" in [s.name for s in all_stages()]
|
||||
|
||||
|
||||
def test_build_notes_skips_notes_within_missing_span():
|
||||
def test_build_notes_keeps_matched_words_within_missing_span():
|
||||
# A missing span covers whole Whisper segments, so it can hold matched
|
||||
# lines alongside the unmatched singing; those lines must keep their notes.
|
||||
times, f0, conf = flat_pitch()
|
||||
rows = [
|
||||
{"word": "keep", "start": 1.0, "end": 1.4, "score": 0.9, "line_index": 0},
|
||||
{"word": "skip", "start": 5.0, "end": 5.4, "score": 0.9, "line_index": 1},
|
||||
{"word": "held", "start": 5.0, "end": 5.4, "score": 0.9, "line_index": 1},
|
||||
]
|
||||
notes, _, _ = build_notes(rows, times, f0, conf, "English",
|
||||
missing_spans=[(4.5, 6.0)])
|
||||
assert [n.text for n in notes] == ["keep"]
|
||||
notes, breaks, warnings = build_notes(rows, times, f0, conf, "English",
|
||||
missing_spans=[(4.5, 6.0)])
|
||||
assert [n.text for n in notes] == ["keep", "held"]
|
||||
assert breaks == [(1.4 + 5.0) / 2]
|
||||
assert warnings == ["missing lyrics near 0:04–0:06"]
|
||||
|
||||
|
||||
def test_warnings_report_dropped_lines():
|
||||
@@ -325,23 +329,6 @@ def test_warnings_report_missing_spans():
|
||||
assert warnings == ["missing lyrics near 1:21–1:29"]
|
||||
|
||||
|
||||
def test_build_notes_skipped_row_mid_line_does_not_corrupt_spacing_or_breaks():
|
||||
# "gone" falls inside a missing span and is skipped; "away" is the same
|
||||
# line's next word and must still get its leading space, and the break
|
||||
# before the next line must use the last *emitted* word's end (3.4, from
|
||||
# "away"), not the skipped row's end (2.4).
|
||||
times, f0, conf = flat_pitch()
|
||||
rows = [
|
||||
{"word": "keep", "start": 1.0, "end": 1.4, "score": 0.9, "line_index": 0},
|
||||
{"word": "gone", "start": 2.0, "end": 2.4, "score": 0.9, "line_index": 0},
|
||||
{"word": "away", "start": 3.0, "end": 3.4, "score": 0.9, "line_index": 0},
|
||||
{"word": "next", "start": 5.0, "end": 5.4, "score": 0.9, "line_index": 1},
|
||||
]
|
||||
notes, breaks, _ = build_notes(rows, times, f0, conf, "English",
|
||||
missing_spans=[(1.9, 2.5)])
|
||||
assert [n.text for n in notes] == ["keep", " away", "next"]
|
||||
assert breaks == [(3.4 + 5.0) / 2]
|
||||
|
||||
def test_notes_carry_line_index():
|
||||
times, f0, conf = flat_pitch()
|
||||
rows = [{"word": "hey", "start": 1.0, "end": 1.4, "score": 0.9, "line_index": 3}]
|
||||
|
||||
@@ -53,7 +53,7 @@ def test_classify_lyrics_match_keeps_partially_matched_lines():
|
||||
def test_classify_lyrics_match_scopes_missing_span_to_adlib_not_whole_section():
|
||||
# matched line and an unmatched ad-lib close enough together that
|
||||
# vocal_sections would merge them into one section; missing_spans must
|
||||
# stay tight around the ad-lib so the matched line keeps its notes.
|
||||
# stay tight around the ad-lib so the review marks the right place.
|
||||
ref = normalize_lyrics("she's got a smile\nthat it seems to me")
|
||||
segs = [seg(1, 4, "she's got a smile that it seems to me"),
|
||||
seg(6, 8, "completely unrelated ad lib")]
|
||||
|
||||
Reference in New Issue
Block a user