Audial's analysis now returns a song's lyrics with a start and end time for every word, on the track's own timeline. That is the data behind a karaoke-style highlight, subtitles, an LRC file, a lyric video, or a search for the bar where the hook first lands.
This post is the developer's view: the calls, the JSON, the failure cases, and a few recipes. If you only want to see the lyrics, upload a track on the Audio Analysis tab and press Get lyrics; the current word lights up as the song plays.
How it fits together
Lyrics are an add-on on an analysis. There are three steps:
- Analyse the mix.
POST /api/functions/run/segmentationreturns the analysis execution. Keep itsexeId. While the analysis runs, Audial also prepares the track's parts in the background, the vocal among them. - Ask for the lyrics.
POST /api/functions/run/lyricswith thatexeIdasanalysisExeId. The words are transcribed from the prepared vocal, not from the full mix. - Read the result. It lands on the analysis execution under
addons.lyrics, and in the merged analysis view.
An analysis counts as one run. Everything you add to it (features, tags, lyrics, a sample pack) is included, so the lyrics cost nothing extra.
Every request carries your X-API-Key and X-User-ID headers. Base URL: https://api.audialmusic.ai/api.
Step 1: analyse
Send the file inline as multipart form data. No stems, BPM or key are needed:
curl -X POST "https://api.audialmusic.ai/api/functions/run/segmentation" \
-H "X-API-Key: your_api_key" -H "X-User-ID: your_user_id" \
-F "userId=your_user_id" \
-F "original=@/path/to/song.mp3"
The answer is the execution. Its state moves from processing to completed, usually in about half a minute, and its stems field shows the background preparation:
{
"exeId": "exe_12345abcde",
"exeType": "segmentation",
"state": "processing",
"stems": { "state": "running", "exeId": "exe_23456bcdef" }
}
Step 2: ask for the lyrics
The add-on takes two fields, as JSON or as form parts:
curl -X POST "https://api.audialmusic.ai/api/functions/run/lyrics" \
-H "X-API-Key: your_api_key" -H "X-User-ID: your_user_id" \
-H "Content-Type: application/json" \
-d '{ "userId": "your_user_id", "analysisExeId": "exe_12345abcde" }'
It answers at once:
{ "exeId": "exe_56789efghi", "analysisExeId": "exe_12345abcde", "addon": "lyrics", "state": "queued" }
Three answers to handle:
- 409
STEMS_NOT_READY: the vocal is still being prepared (the body'sstatesays where it is). Nothing was started; send the same request again a minute later. If itsstateisfailed, preparing the track failed:POST /api/functions/run/stem-splitterwith{ userId, analysisExeId }tries again. - 409
ADDON_UNAVAILABLE: the analysis has not finished yet, or it is an older analysis whose inputs cannot be found. - 404
EXECUTION_NOT_FOUND: no analysis of yours has that id.
Calling again while a run is queued, running or completed answers that same run, so a retry loop never starts duplicates. "rerun": true starts a new one.
Step 3: read the result
Poll GET /api/db/{userId}/execution/{exeId} with the analysis's id until addons.lyrics.state is completed. The result is on addons.lyrics.result:
{
"lyrics": "la la la la\nhold me close tonight",
"is_instrumental": false,
"lines": [
{ "start": 36.02, "end": 38.41, "words": [["la", 36.02, 36.31], ["la", 36.62, 36.9], ["la", 37.21, 37.5], ["la", 37.81, 38.41]] },
{ "start": 39.12, "end": 41.3, "words": [["hold", 39.12, 39.45], ["me", 39.5, 39.71], ["close", 39.8, 40.33], ["tonight", 40.41, 41.3]] }
],
"segments": [
{ "text": "la la la la", "start": 36.02, "end": 38.41 },
{ "text": "hold me close tonight", "start": 39.12, "end": 41.3 }
],
"section_lyrics": ["", "la la la la hold me close tonight", "", "", "", "", ""],
"first_vocal_beat_time": 36.02,
"warnings": []
}
(The words and their times are illustrative. The seven entries of section_lyrics match the demo track's seven sections.)
lines: one per lyric line, withstartandend, andwords, each[word, start, end]in seconds on the track's timeline.segments: the same lines as plain text with their times.section_lyrics: one string per section of the analysis, in order.first_vocal_beat_time: where the vocals start.is_instrumental:true, with no lines, when there is nothing to transcribe.
The merged view, GET /api/db/{userId}/execution/{exeId}/analysis, lays the same data into the analysis itself: song_analysis.lyrics, song_analysis.lyrics_lines, segment_analysis[i].lyrics and first_vocal_beat_time, next to the sections and the tempo map. That is the one to read if you want the lyrics and the structure together.
The same thing in Python
The SDK runs the three steps for you, including the wait for the vocal:
import audial # audial-sdk 1.7 or later
audial.config.set_api_key("your_api_key")
audial.config.set_user_id("your_user_id")
analysis = audial.segment("song.mp3")
lyrics = audial.lyrics(analysis["analysis_id"])
for line in lyrics["lines"]:
print(line["start"], " ".join(word for word, start, end in line["words"]))
audial.lyrics keeps retrying while the API answers STEMS_NOT_READY, for up to wait_s seconds (600 by default), restarts the preparation once if it failed, and saves the result as <analysis_id>_analysis/lyrics.json in your results folder. Pass on_progress to see the execution while a machine starts after a quiet spell.
Recipe: the word at a given time
Flatten the words once, then look the current time up with a binary search. This is the whole of a karaoke highlight:
from bisect import bisect_right
words = [w for line in lyrics["lines"] for w in line["words"]]
starts = [start for _, start, _ in words]
def word_at(t):
i = bisect_right(starts, t) - 1
if i >= 0 and t < words[i][2]:
return words[i][0]
return None # between words, or before the vocals start
Recipe: an LRC file
LRC wants [mm:ss.xx] before each line, and the enhanced form puts <mm:ss.xx> before each word:
def stamp(t): # seconds -> mm:ss.xx
m, cs = divmod(round(t * 100), 6000)
return f"{m:02d}:{cs / 100:05.2f}"
def to_lrc(lines, words=False):
out = []
for line in lines:
if words:
text = " ".join(f"<{stamp(start)}>{w}" for w, start, end in line["words"])
else:
text = " ".join(w for w, start, end in line["words"])
out.append(f"[{stamp(line['start'])}]{text}")
return "\n".join(out)
open("song.lrc", "w").write(to_lrc(lyrics["lines"], words=True))
On the example above that writes [00:36.02]<00:36.02>la <00:36.62>la ….
Recipe: lyrics by section
section_lyrics lines up with the analysis's sections, so the labels come from the analysis:
sections = analysis["audio_segmentation"]["sections"]
for section, text in zip(sections, lyrics["section_lyrics"]):
if text:
print(f'{section["label"]} at {section["start"]:.1f}s: {text}')
That is how you find which chorus the hook first appears in, or skip the instrumental sections of a lyric video.
From an assistant
The Audial MCP server (0.6.0 or later) exposes the add-on as get_lyrics. It takes an analysis id, or a file, which it analyses first, and returns the timed lines. "Get the lyrics of ~/Music/song.mp3 with their word timings and save them as an LRC file" works as one instruction in Claude Code.
Things to know
- Language. It is detected automatically, and the words come back in the language they are sung in. Nothing is translated.
- Accuracy. It is a transcription, so expect mistakes where a careful listener would struggle too: mumbled or heavily processed vocals, stacked backing vocals over the lead, words lost under the mix. Read the result before you print it.
- Timing. Times are on the track's own timeline, the same clock as the sections and the tempo map, so a player's current position picks the word directly. No offset is needed.
- No lyric lookup. Audial transcribes what is sung. It does not fetch published lyrics.
- Older analyses made before November 2026 have lyrics as text without word timings. Analyse the track again for timed lines.
Where to go next
The page for this is lyrics with word timestamps. The full field list is under Lyrics (add-on) and Analysis Result on the API reference, and the analysis itself is explained in song sections that follow the real tempo.
