feat: verse-based lyric grouping and active-verse tracking
group_into_verses() splits LRC lyrics on empty lines or timestamp gaps >3.5s; find_active_verse() returns the current verse lines and active index with INTER_VERSE_CAP (8s) cap on the last line of each verse. ControlState.set_lyrics() now pre-computes verses; current() exposes verse_lines and verse_active instead of the old 5-line lyric_context. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ad35be37c7
commit
2560a4fce6
1 changed files with 53 additions and 32 deletions
|
|
@ -96,6 +96,7 @@ class ControlState:
|
|||
def set_lyrics(self, lyrics: list[tuple[float, str]]) -> None:
|
||||
with self._lock:
|
||||
self._lyrics = lyrics
|
||||
self._verses = group_into_verses(lyrics)
|
||||
|
||||
def set_position(self, position: float) -> None:
|
||||
with self._lock:
|
||||
|
|
@ -103,40 +104,17 @@ class ControlState:
|
|||
|
||||
def current(self) -> dict[str, object]:
|
||||
with self._lock:
|
||||
lyrics = getattr(self, "_lyrics", [])
|
||||
verses = getattr(self, "_verses", [])
|
||||
position = getattr(self, "_position", 0.0)
|
||||
MAX_DISPLAY = 5.0
|
||||
|
||||
active_idx = -1
|
||||
line = ""
|
||||
for i, (ts, text) in enumerate(lyrics):
|
||||
if ts > position:
|
||||
break
|
||||
next_ts = lyrics[i + 1][0] if i + 1 < len(lyrics) else ts + MAX_DISPLAY
|
||||
if position <= min(next_ts, ts + MAX_DISPLAY):
|
||||
active_idx = i
|
||||
line = text
|
||||
else:
|
||||
active_idx = -1
|
||||
line = ""
|
||||
|
||||
# Build 5-line context from non-empty lyrics, centered on active
|
||||
context: list[dict[str, object]] = []
|
||||
if active_idx >= 0 and line:
|
||||
display_idxs = [i for i, (_, t) in enumerate(lyrics) if t.strip()]
|
||||
if active_idx in display_idxs:
|
||||
pos = display_idxs.index(active_idx)
|
||||
for offset in range(-2, 3):
|
||||
di = pos + offset
|
||||
t = lyrics[display_idxs[di]][1] if 0 <= di < len(display_idxs) else ""
|
||||
context.append({"text": t, "offset": offset})
|
||||
|
||||
verse_lines, verse_active = find_active_verse(verses, position)
|
||||
line = verse_lines[verse_active] if verse_active >= 0 and verse_lines else ""
|
||||
return {
|
||||
"art_url": getattr(self, "_art_url", None),
|
||||
"rgb": list(getattr(self, "_rgb", [0, 200, 255])),
|
||||
"rgb2": list(getattr(self, "_rgb2", [255, 0, 150])),
|
||||
"lyric_line": line,
|
||||
"lyric_context": context,
|
||||
"art_url": getattr(self, "_art_url", None),
|
||||
"rgb": list(getattr(self, "_rgb", [0, 200, 255])),
|
||||
"rgb2": list(getattr(self, "_rgb2", [255, 0, 150])),
|
||||
"lyric_line": line,
|
||||
"verse_lines": verse_lines,
|
||||
"verse_active": verse_active,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -221,6 +199,9 @@ def start_control_server() -> ThreadingHTTPServer:
|
|||
return server
|
||||
|
||||
|
||||
VERSE_GAP_SECS = 3.5 # gap between lines that splits a verse
|
||||
INTER_VERSE_CAP = 8.0 # how long to keep last line of a verse visible
|
||||
|
||||
_LRC_TS = re.compile(r'\[(\d{1,2}):(\d{2}(?:\.\d+)?)\]')
|
||||
|
||||
|
||||
|
|
@ -255,6 +236,46 @@ def clean_title(title: str) -> str:
|
|||
return t
|
||||
|
||||
|
||||
def group_into_verses(
|
||||
lyrics: list[tuple[float, str]],
|
||||
) -> list[list[tuple[float, str]]]:
|
||||
"""Split non-empty lyrics into verses on empty LRC lines or large timestamp gaps."""
|
||||
verses: list[list[tuple[float, str]]] = []
|
||||
current: list[tuple[float, str]] = []
|
||||
for ts, text in lyrics:
|
||||
if not text.strip():
|
||||
if current:
|
||||
verses.append(current)
|
||||
current = []
|
||||
continue
|
||||
if current and (ts - current[-1][0]) > VERSE_GAP_SECS:
|
||||
verses.append(current)
|
||||
current = []
|
||||
current.append((ts, text))
|
||||
if current:
|
||||
verses.append(current)
|
||||
return verses
|
||||
|
||||
|
||||
def find_active_verse(
|
||||
verses: list[list[tuple[float, str]]],
|
||||
position: float,
|
||||
) -> tuple[list[str], int]:
|
||||
"""Return (verse_text_lines, active_index) or ([], -1) when in a gap."""
|
||||
for v_idx, verse in enumerate(verses):
|
||||
for l_idx, (ts, _) in enumerate(verse):
|
||||
if ts > position:
|
||||
break
|
||||
if l_idx + 1 < len(verse):
|
||||
deadline = verse[l_idx + 1][0]
|
||||
else:
|
||||
nxt = verses[v_idx + 1][0][0] if v_idx + 1 < len(verses) else ts + INTER_VERSE_CAP
|
||||
deadline = min(nxt, ts + INTER_VERSE_CAP)
|
||||
if position <= deadline:
|
||||
return [t for _, t in verse], l_idx
|
||||
return [], -1
|
||||
|
||||
|
||||
def fetch_lyrics(artist: str, title: str) -> list[tuple[float, str]]:
|
||||
if not artist or not title:
|
||||
return []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue