ComfyUI-SongLRC
Timed .lrc lyrics for generated songs, saved beside the audio, with a player that highlights each line as it is sung.
ComfyUI-SongLRC

Turn generated song lyrics into a timed .lrc file that is saved beside the audio,
with a matching name and counter.
output/audio/songs/Hold The Line Tonight_00007.mp3
output/audio/songs/Hold The Line Tonight_00007.lrc
Built and tested against ComfyUI-FL-YuE2.
It also works with MiniMax Music 3 and, in principle, any node chain that produces
lyrics plus an AUDIO output.
This pack was previously called ComfyUI-MiniMaxLRC. See Migrating below.
What it gives you
- Real per-line timestamps from forced alignment when WhisperX is installed
- Score-aware timing from a YuE2 ABC score when it is not
- A song title taken from a
Title:line, or derived from the repeated hook - Audio and LRC written with the identical name and five digit counter
- A standalone LRC saver too, so the pack needs no other node packs
- A player that scrolls the lyrics against the audio, so timing is checked by ear
- ComfyUI's own Save Audio (Advanced) keeps working, so FLAC, MP3 and Opus quality controls are untouched
Install
Through ComfyUI-Manager, search for ComfyUI-SongLRC.
Or clone it:
cd ComfyUI/custom_nodes
git clone https://github.com/TheAwaken1/ComfyUI-SongLRC
pip install -r ComfyUI-SongLRC/requirements.txt
Restart ComfyUI.
The nodes
| Node | Purpose |
|------|---------|
| Lyrics Clean | Strips titles, markdown and stray tags. Outputs clean lyrics and a title. |
| Song Filename | Turns a title into <folder>/<title> for Save Audio (Advanced). |
| Lyrics to LRC | Times the lyrics against the audio and writes the LRC text. |
| Save Matching LRC | Writes the LRC next to the audio file that was just saved. |
| Save LRC (auto) | Writes the LRC on its own, with its own counter. |
| Music Player (SongLRC) | Plays the song with its lyrics highlighted in time. |
Both savers show the finished lyrics on the node after a run: the song title, then the timed lines. The length and byline tags stay in the file, where players read them, rather than cluttering the node. The saved path goes to the console.
The player
Connect the final AUDIO and the LRC to Music Player (SongLRC) and run the
graph.
Lyrics ride a wheel rather than sitting in a list. The current line holds the centre, larger and lit; the lines before and after tilt away on an arc and fade toward the edges of the frame, which is masked top and bottom so nothing ends abruptly. Click any line to jump to it. A blank cue is an instrumental gap and shows as three dots rather than leaving a lyric stuck on screen.
Transport is a play button and a progress bar, with no clock. A running timer pulls the eye away from the words, and the wheel already shows where you are in the song. Drag the bar to scrub.
It is the fastest way to judge timing, because a drift of half a second is obvious
by ear and invisible in a text file. If lines run consistently early or late, adjust
timing_scale on Lyrics to LRC, or shift everything with offset_seconds.
The two savers do different jobs, and you normally want one of them, not both. Save Matching LRC waits for Save Audio (Advanced) and reuses that file's exact name and number, so the pair always match. That is the one to use whenever you are saving the song. Save LRC (auto) waits for nothing, which is what you want when you are only after the lyrics, or when the audio is saved somewhere else. Running both writes the same lyrics twice under different names.
Every node in the pack shares one colour, a burnt-orange title over a deep crimson body, so a SongLRC chain is recognisable at a glance among other node packs.
Using it

Two examples ship with the pack.
example_workflows/any_audio_to_lrc.json uses only this pack plus ComfyUI's own
audio nodes. Point it at any song file, paste that song's lyrics, and it times them,
saves the LRC and plays it back. Pasting a finished .lrc works too: Lyrics Clean
strips the old timestamps and header tags first, and takes the title from the file,
so nothing gets timed twice. Nothing else needs to be installed, which makes it
the quickest way to see how the six nodes connect.

example_workflows/yue2_song_to_lrc.json is the full pipeline using
ComfyUI-FL-YuE2.

example_workflows/yue2_song_to_lrc_qwen3.json adds a local Qwen3-VL model that
writes the lyrics, so a song idea becomes a finished track with timed lyrics in one
run. The generated text goes into Lyrics Clean, which feeds both the song model and
the timing, so the sung words and the timed words cannot drift apart.

example_workflows/comfy_yue2_song_to_lrc.json is the same pipeline on ComfyUI's
own YuE2 nodes, so it needs no song node pack at all. The generated ABC score feeds
the LRC timing here too, so sections land where the music puts them. It is a straight line from lyrics to
a finished song with a matching LRC, using only this pack, FL-YuE2 and ComfyUI's own
audio nodes.
Note that FL-YuE2 names the render length max_duration, which is what the example
uses. If you run a fork that renamed it, set the length on the Render node once after
loading.
You do not need a text generator. Type or paste your lyrics into Lyrics Clean. Its lyrics output feeds both the Compose node and Lyrics to LRC, so the words that get sung and the words that get timed can never drift apart. Its title output feeds Song Filename.
If you do use a generator, such as a Qwen or Spark Studio node, connect it to the Lyrics Clean text input instead of typing. Everything downstream is identical.
Leave the title field on Song Filename blank and it derives one from the repeated chorus line. Fill it in to name the song yourself.
Timing quality
Three methods, best first. The node picks the best one available automatically.
- Forced alignment. Connect the final
AUDIOto Lyrics to LRC and install WhisperX. This gives real timestamps. - Score timing. Connect the Compose node's
score_abcoutput to thestructureinput. Sections are placed using the score's tempo and bar counts rather than being spread evenly. The example workflow wires this for you. - Even timing. The fallback. Lyrics are spread across the song with an allowance for the intro and outro.
If timings drift, adjust timing_scale above 1 when lyrics run early and below 1
when they run late. Use offset_seconds to shift everything.
Installing WhisperX safely
A plain pip install whisperx often replaces your CUDA PyTorch with a CPU-only
build and breaks ComfyUI. Install it without touching torch:
pip install whisperx --no-deps
pip install faster-whisper ctranslate2 nltk pandas pyannote.audio omegaconf
Then confirm CUDA still works:
python -c "import torch; print(torch.cuda.is_available())"
If that prints False, reinstall the CUDA torch build you had before. Skipping
WhisperX is fine; the nodes fall back to score or even timing.
Migrating from ComfyUI-MiniMaxLRC
Old workflows keep loading. The four previous class names are registered as hidden aliases pointing at the renamed nodes, so nothing turns into a red missing node.
Two changes to know about:
- The default output folder is now
audio/songsrather thanaudio/YuE2. Song Filename has afolderwidget, so set it back if you want the old path. - The LRC
ar:tag is now blank by default and is left out when empty, instead of claiming every song was made by MiniMax Music 3.
Remove the old ComfyUI-MiniMaxLRC folder after installing this one, or the two
packs will fight over the same class names.
Running it from code
The nodes run inside ComfyUI, so drive them through ComfyUI's API. Export your graph with Workflow, Export (API) first, then post it.
curl -X POST http://127.0.0.1:8188/prompt -H "Content-Type: application/json" -d @workflow_api.json
import json, urllib.request
graph = json.load(open("workflow_api.json", encoding="utf-8"))
# Node 8 is Lyrics Clean in the bundled example.
graph["8"]["inputs"]["text"] = "[verse]\nNew words here\n[chorus]\nAnd a hook"
body = json.dumps({"prompt": graph}).encode()
req = urllib.request.Request("http://127.0.0.1:8188/prompt", data=body,
headers={"Content-Type": "application/json"})
print(json.load(urllib.request.urlopen(req))["prompt_id"])
const graph = await (await fetch("./workflow_api.json")).json();
graph["8"].inputs.text = "[verse]\nNew words here\n[chorus]\nAnd a hook";
const r = await fetch("http://127.0.0.1:8188/prompt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: graph }),
});
console.log((await r.json()).prompt_id);
Poll GET /history/<prompt_id> for completion. The LRC lands in your output folder
next to the audio; Save Matching LRC also reports the path it wrote.
Tests
python -m unittest discover -s tests
License
MIT