Seguir la letra mientras suena la canción - #40
Merged
Merged
Conversation
`!lyrics` answered with the whole song at once, which for a long track meant several messages in a row — `Rap God` is 7,936 characters and arrived as three. It was also static: it said nothing about where the song actually was. It is now one message that follows the music. The line being sung is shown with two lines of context either side, and the same message keeps editing itself as the song moves and as the queue advances. A Stop button ends it, and so does `!stop`, an empty queue, or the player being destroyed. Genius has no timestamps, so it cannot do this. LRCLIB can: free, no API key, LRC bodies with `[mm:ss.xx]` marks. It was checked against the music this bot actually plays before anything was built — KAROL G, Bad Bunny, Queen and Eminem all came back synced. Lookups send the duration too, which is what keeps a query from matching a live version whose timings are wrong for the audio playing. Genius stays as the fallback for what LRCLIB does not have. Two decisions were made for the sake of the small host it runs on: - **It does not poll.** The follower works out when the next line begins and sleeps until then, capped at two seconds so a pause or a skip is noticed promptly. Measured on the server: 0.018s of CPU over twelve seconds of following, which is 0.15% of one core. - **There is no cache.** One was drafted and dropped: the follower reloads only when `player.current` changes identity, and `loop track` replays the same dict, so it would never have been read. Edits are throttled — Discord allows about five per five seconds per channel and a fast song changes lines more often than that. The first threshold tried was 2.5s, which a test caught as wrong: above the two-second wakeup cap, it delayed ordinary line changes by a whole wakeup, plainly visible on lyrics. It is 1.5s, and a test now asserts the two constants stay in that order. The pitch effects would have broken the sync outright. `nightcore` plays at 1.25x while `elapsed` counts wall-clock, so the song runs a quarter ahead of the clock and the lyrics would drift further out every minute. `Effect` gains a `rate`, the player exposes a `position` that accounts for it, and — because a declared rate that disagrees with its filter is worse than none — a test measures each rate against real FFmpeg rather than trusting the number. The static path is fixed too, since it is what songs without timings fall back to. It is one message with page buttons instead of several, and pages now break between lines: the old version sliced at a fixed character count and cut words in half. Two bugs turned up while testing against the real thing: - `lyrics_api.fetch` took a `loop` argument, and the cog was passing `self.bot.loop` — which raises if the gateway is not up yet. The parameter was never needed; inside a coroutine `get_running_loop()` is the answer. Removed. - A payload can carry timings and no plain copy, which would have rendered an empty page for `!lyrics <search>`. The words are now derived from the timed lines when that happens. The parser and the renderer are pure functions and carry the awkward cases: several timestamps on one line, `[ar:]` metadata, out-of-order marks, an instrumental gap that must stay a gap rather than leave the previous line up through the whole break. The follower is driven by a scripted clock, so its tests run in microseconds and none of them touch Discord or the network. Verified end to end on the server against the real LRCLIB: 69 lines for BbY WOW, the right window at every position, the Genius fallback working with no gateway, the follower stopping on destroy, and no aiohttp session or pending task left behind. Tests: 427, up from 411.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
!lyricsrespondía con la canción entera de golpe —Rap Godson 7.936caracteres y llegaban en tres mensajes. Y era estática: no decía nada de por
dónde iba la canción.
Ahora es un solo mensaje que sigue la música, estilo karaoke.
427 tests (antes 411).
Cómo se ve
El mismo mensaje se va editando solo mientras suena, y al pasar a la siguiente
canción carga su letra y sigue. Se detiene con el botón ⏹, con
!stop, alvaciarse la cola o si el player muere.
De dónde salen los tiempos
Genius no tiene marcas de tiempo, así que no puede hacer esto.
LRCLIB sí: gratis, sin API key, cuerpos LRC con marcas
[mm:ss.xx]. Lo comprobé contra la música que este bot reproduce de verdadantes de construir nada — KAROL G, Bad Bunny, Queen y Eminem, todas
sincronizadas.
Las consultas mandan también la duración, que es lo que evita emparejar con
una versión en directo cuyos tiempos no cuadran con el audio que suena. Genius se
queda como respaldo para lo que LRCLIB no tenga.
Rendimiento
Dos decisiones tomadas por la caja pequeña donde corre:
hasta ahí, con tope de 2 s para notar pausas y saltos. Medido en el servidor:
0,018 s de CPU en 12 segundos siguiendo la canción = 0,15% de un núcleo.
player.currentcambia de identidad, yloop trackreproduce el mismoobjeto. Nunca se habría leído.
Las ediciones van limitadas — Discord permite unas 5 por 5 s por canal y una
canción rápida cambia de línea más a menudo. El primer umbral que probé, 2,5 s,
lo cazó un test como incorrecto: al estar por encima del tope de despertar de
2 s, retrasaba cada cambio de línea normal un despertar entero, cosa que en una
letra se ve a simple vista. Quedó en 1,5 s, y hay un test que afirma que las dos
constantes mantienen ese orden.
Los efectos de velocidad habrían roto la sincronía
nightcoreva a 1,25x mientraselapsedcuenta reloj de pared: la canciónavanza un cuarto más rápido que el reloj y la letra se iría desviando más cada
minuto. Así que
Effectgana un camporate, el player expone unapositionque lo tiene en cuenta, y —porque un
ratedeclarado que no case con su filtroes peor que ninguno— un test lo mide contra FFmpeg real en vez de fiarse del
número:
El camino estático también se arregla
Es a donde caen las canciones sin tiempos, así que importa: un mensaje con
botones ◀ ▶ en lugar de varios. Y las páginas ahora cortan entre líneas —
la versión anterior troceaba cada 4000 caracteres exactos y partía palabras por
la mitad.
Dos bugs que salieron al probar contra lo real
lyrics_api.fetchrecibía un argumentoloop, y el cog le pasabaself.bot.loop— que lanza excepción si el gateway aún no está levantado.El parámetro no hacía falta: dentro de una corutina
get_running_loop()ya esla respuesta. Eliminado.
habría pintado una página vacía en
!lyrics <busqueda>. Ahora las palabras sederivan de las líneas con marca.
Tests
El parser y el renderizador son funciones puras y cargan con los casos
incómodos: varias marcas en una línea (
[00:10][01:20] estribillo), metadatos[ar:], marcas desordenadas, y un hueco instrumental que debe seguir siendo unhueco en vez de dejar la línea anterior puesta durante todo el break.
El seguidor va con un reloj guionizado, así que sus tests corren en microsegundos
y ninguno toca Discord ni la red.
Cómo probarlo
pytest # 427Verificado de extremo a extremo en el servidor contra el LRCLIB real: 69 líneas
para BbY WOW, la ventana correcta en cada posición, el respaldo de Genius
funcionando sin gateway, el seguidor parándose solo al destruir el player, y
ninguna sesión de aiohttp ni tarea pendiente al cerrar.
En Discord, tras desplegar:
!play <algo con letra>→!lyrics— un mensaje que se mueve con la canción.!nightcoremientras sigue — la letra debe seguir cuadrando, no adelantarse.!lyrics Rap God— un mensaje con ◀ ▶, no tres seguidos.!lyrics zxqwvasd— "Couldn't find lyrics", sin traza.!stopcon el seguidor activo — debe pararse solo en menos de 2 s.El diseño está en
docs/superpowers/specs/2026-09-12-synced-lyrics-design.md.