Forced-alignment library and CLI that timestamps lyrics against an audio file, exporting LRC
Project description
syncalong
A Python library and CLI tool that aligns plain-text lyrics to an audio file and outputs timestamped lyrics in LRC format.
It uses OpenAI's Whisper to transcribe the audio with word-level timestamps, then runs a dynamic-programming sequence alignment to map those words back onto your lyrics.
📖 Full documentation: https://syncalong.readthedocs.io/
Prerequisites
- Python 3.9+
- ffmpeg — required by Whisper for audio decoding.
Install via your package manager (
apt install ffmpeg,brew install ffmpeg, etc.)
Installation
pip install syncalong
Or from a checkout, in editable / development mode:
pip install -e ".[dev,docs]"
Optional: vocal separation
For better results on studio recordings (where background music may
confuse the speech model), install with the vocal-separation extra:
pip install "syncalong[vocal-separation]"
This adds Demucs, which isolates the vocal track before transcription.
Usage
syncalong LYRICS_FILE AUDIO_FILE [options]
Examples
Basic usage — outputs LRC to stdout:
syncalong song.txt song.mp3
Save to a file:
syncalong song.txt song.mp3 > song.lrc
Use a larger Whisper model for better accuracy:
syncalong song.txt song.wav -m medium
Isolate vocals first (recommended for studio tracks):
syncalong song.txt song.flac --separate-vocals
Specify language (skips auto-detection):
syncalong lied.txt lied.mp3 -l de
Options
| Flag | Description | Default |
|---|---|---|
-m, --model |
Whisper model: tiny, base, small, medium, large, turbo, or variants like small.en (English-only) and large-v3. |
base |
-l, --language |
Language code (e.g. en, de, ja). Auto-detected if omitted. |
auto |
--separate-vocals |
Run Demucs to isolate vocals before transcription. | off |
--no-lyrics-prompt |
Don't feed the lyrics to Whisper as a decoding prompt. | off |
--threshold |
Minimum fuzzy-match score (0–100) to accept a word alignment. | 55 |
Lyrics file format
One lyric line per text line. Blank lines are preserved as instrumental
breaks. Section headers in brackets (like [Chorus] or (Bridge)) are
detected and kept as structural markers.
[Verse 1]
I walk a lonely road
The only one that I have ever known
[Chorus]
My shadow's the only one that walks beside me
Output format
Standard LRC with [mm:ss.xx] timestamps:
[00:12.34] I walk a lonely road
[00:15.67] The only one that I have ever known
Use as a library
syncalong is also an importable package, so you can embed alignment in your
own code or batch-process a whole album while loading the Whisper model only
once. import syncalong is cheap — the heavy Whisper import is deferred until a
model is actually loaded.
Align a single song
import syncalong
from pathlib import Path
tx = syncalong.Transcriber(model_name="base") # load the model once
res = syncalong.align(Path("lyrics.txt"), "song.mp3", transcriber=tx)
print(res.lrc) # the LRC document, as a string
res.timed_lines # [(LyricLine, 12.3), (LyricLine, None), ...]
res.matched, res.total # e.g. (28, 30) — lines that got a timestamp / all lines
align() accepts lyrics as a pathlib.Path (read from a file), a str of
lyrics text, or a pre-parsed list[LyricLine]; audio is a path (str or
pathlib.Path). Other keyword arguments: transcriber (pass a pre-loaded
Transcriber to reuse across songs), plus the CLI-mirroring model_name,
language, use_lyrics_prompt, threshold, and separate_vocals. If you omit
transcriber, align() loads a model itself from model_name.
Batch-process an album (reuse the model)
Loading a Whisper model takes seconds, so a long-running job should create one
Transcriber and reuse it. align_to_lrc() is a convenience wrapper returning
just the LRC string (equivalent to align(...).lrc):
import syncalong
from pathlib import Path
tx = syncalong.Transcriber(model_name="small") # loaded once for the whole run
for audio in Path("album/").glob("*.flac"):
lyrics = audio.with_suffix(".txt")
lrc = syncalong.align_to_lrc(lyrics, audio, transcriber=tx)
audio.with_suffix(".lrc").write_text(lrc, encoding="utf-8")
Lyrics already in memory? Pass them as a plain string instead of a path:
text = "I walk a lonely road\nThe only one that I have ever known\n"
res = syncalong.align(text, "song.mp3", transcriber=tx)
Note:
separate_vocals=Truerequires the optionalvocal-separationextra (pip install syncalong[vocal-separation]). Passing it without demucs installed raisesModuleNotFoundError.
How it works
-
Transcribe — Whisper runs on the audio file with
word_timestamps=True, producing a list of(word, start_time, end_time)tuples. The beginning of your lyrics is passed to the decoder as aninitial_promptto bias it toward the correct words (disable with--no-lyrics-prompt), and conditioning on previously decoded text is turned off to avoid the repetition loops Whisper is prone to on music. -
Normalize — Both the lyrics and the transcript are lowercased and stripped of punctuation so that matching is case/punctuation-insensitive.
-
Align — A Needleman–Wunsch-style dynamic programming algorithm finds the optimal monotonic mapping between lyric words and transcript words. Word similarity is scored with rapidfuzz (Levenshtein ratio), so minor mishearings by Whisper are tolerated.
-
Map to lines — Each lyric line receives the timestamp of its earliest matched word. Small gaps between matched lines are filled with linear interpolation, and unmatched lines before the first match or after the last one are extrapolated from the transcript's start and end times, so every sung line gets a tag.
-
Format — The timed lines are printed in LRC format to stdout.
Tips for best results
- Use
--separate-vocalson any track with significant instrumentation. - Bump up the model size (
-m smallor-m medium) if alignment is poor. For English songs the.envariants (-m small.en) are often more accurate at the same speed. Thelargemodel is the most accurate but is slow without a GPU;turbois a good speed/quality compromise. - Check your lyrics — extra or missing words relative to the actual performance will reduce alignment quality. The closer the lyrics match what's actually sung, the better.
- GPU acceleration — if you have a CUDA-capable GPU and PyTorch is installed with CUDA support, Whisper will use it automatically.
Documentation
Full guides and the auto-generated API reference live at https://syncalong.readthedocs.io/:
Contributing
Contributions are welcome. Install the dev tooling with
pip install -e ".[dev,docs]" and make sure the quality gate passes before
opening a PR:
pytest
ruff check .
black --check .
pyright src
See the contributing guide for the full workflow, including how releases are cut from git tags.
License
Released under the MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file syncalong-1.0.0.tar.gz.
File metadata
- Download URL: syncalong-1.0.0.tar.gz
- Upload date:
- Size: 44.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e4412172504ad01e80306f8cb0007a9e0351e3f5c2f9d58e3105500b38c1173
|
|
| MD5 |
e3e251e9d977ee9ee9a4b915140a9dfe
|
|
| BLAKE2b-256 |
c96e2dbb239702890d25cc0c43ec7e9875ea910b11a047dd8d8dd1131dc35ce6
|
Provenance
The following attestation bundles were made for syncalong-1.0.0.tar.gz:
Publisher:
release.yml on wickeddoc/syncalong
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
syncalong-1.0.0.tar.gz -
Subject digest:
6e4412172504ad01e80306f8cb0007a9e0351e3f5c2f9d58e3105500b38c1173 - Sigstore transparency entry: 2213520041
- Sigstore integration time:
-
Permalink:
wickeddoc/syncalong@0d736364dd3466dd1b2c77bb2f975bfd863877f0 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/wickeddoc
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0d736364dd3466dd1b2c77bb2f975bfd863877f0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file syncalong-1.0.0-py3-none-any.whl.
File metadata
- Download URL: syncalong-1.0.0-py3-none-any.whl
- Upload date:
- Size: 20.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
35f316442186b4137a5ce9c24f54efec9cceaedbc1e11ddbf4e8e0bdddf6ac63
|
|
| MD5 |
b24a4d53cea9cd71539969b55de6e57f
|
|
| BLAKE2b-256 |
c7b933190b92dabc9b3b36a3a8bd642f964cdc5127cbd8ce726b6554132e4e06
|
Provenance
The following attestation bundles were made for syncalong-1.0.0-py3-none-any.whl:
Publisher:
release.yml on wickeddoc/syncalong
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
syncalong-1.0.0-py3-none-any.whl -
Subject digest:
35f316442186b4137a5ce9c24f54efec9cceaedbc1e11ddbf4e8e0bdddf6ac63 - Sigstore transparency entry: 2213520159
- Sigstore integration time:
-
Permalink:
wickeddoc/syncalong@0d736364dd3466dd1b2c77bb2f975bfd863877f0 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/wickeddoc
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0d736364dd3466dd1b2c77bb2f975bfd863877f0 -
Trigger Event:
push
-
Statement type: