Align transcript to SRT timestamps
Turn a clean, plain-text transcript and an existing timing map into ready-to-use SRT subtitles.
Run — free
Each timestamp becomes a numbered cue, while the transcript supplies the exact words viewers will read. Optional reference text tells the aligner how many transcript words belong to each timed segment, so corrections to spelling and punctuation do not destroy the original timing structure. The result uses standard SRT timecodes, stable cue numbering, blank lines between blocks, and a configurable maximum characters-per-line rule.
Prepare the transcript and timing map
Start with the final transcript exactly as it should appear in the subtitles. Whitespace between words is normalized, but spelling, capitalization, and punctuation remain part of the supplied words. Then provide an ordered timestamps array. Every item needs a start and end value in seconds and becomes one SRT cue. A timestamp may also include reference text taken from the original transcription. The aligner does not copy that reference wording into the output; it counts its words and consumes the same number of words from the corrected transcript. This is useful when an automatic transcription already supplied segment times but an editor later fixed names, punctuation, or wording without changing the overall word allocation. When reference text is omitted, the timestamp consumes one transcript word, which suits word-level timing data. Make sure the combined word counts cover the complete transcript. Extra transcript words or timestamp counts that run past the transcript produce an explicit input error instead of silently dropping content. Times use seconds and may include decimals, such as 1.25 or 8.9.
Control subtitle line length
Set max_chars_per_line to the limit required by your delivery workflow, player, broadcaster, or accessibility guidelines. The default is 42 characters, and accepted values range from 8 through 120. Wrapping happens only at word boundaries. The algorithm adds words to a line until the next word would exceed the selected maximum, then starts a new line. It never changes a word, removes punctuation, or inserts a hyphen. Because preserving transcript text is the priority, a single word longer than the requested limit is rejected with a clear error. You can then raise the limit or edit that word intentionally rather than receiving a subtitle that looks valid but contains altered text. Each timestamp remains one cue even if wrapping produces several lines; timing is never invented or divided during formatting. This makes the output predictable when the timing map has already been reviewed. For conventional two-line subtitle targets, prepare timestamp segments with enough words to fit within two wrapped lines at your chosen limit. The capability enforces characters per line, not a hidden cue-length policy.
Validate timing and use the SRT output
Before producing any subtitle blocks, the aligner validates every timing entry. Start and end must be finite, non-negative second values, and the end must be later than the start. Entries must also remain chronological: the next cue cannot begin before the previous cue ends. Adjacent cues may touch exactly, which is common in word-level and tightly edited segment timing. If an overlap or backwards range appears, processing stops and identifies the timestamp item where ordering failed. This fail-fast behavior prevents an apparently polished SRT file from hiding timing defects that players may render inconsistently. Valid seconds are rounded to the nearest millisecond and formatted as the SRT convention HH:MM:SS,mmm. The result includes a complete srt string for copying or saving and a blocks array for applications that want structured cue data. Cue numbers begin at one, arrows and blank separators are inserted automatically, and output is deterministic: identical input always returns identical subtitle text. Run it in the browser for interactive preparation or call the API for $0.002 when subtitle generation belongs in an automated media pipeline.
What you can do with it
Apply editorial corrections
Keep segment timing from automatic transcription while replacing its wording with an approved, corrected transcript.
Convert word timing to SRT
Turn one timestamp per word into standards-shaped SRT cues without manually formatting timecodes or numbering.
Enforce delivery line limits
Wrap subtitle wording at a known characters-per-line threshold before importing it into a player or editing system.
FAQ
What does an API request cost?
Each API request costs $0.002. The browser version can be used interactively without sending the work to a remote model.
Does reference text appear in the subtitles?
No. Reference text determines how many words a timestamp receives. The visible cue wording always comes from the transcript.
Can timestamps overlap?
No. Every cue must end after it starts, and each next cue must start at or after the previous cue ends.
What happens when words do not match the timestamp counts?
The request fails if words remain unassigned or if the timestamp word counts require more words than the transcript contains.
Will a long word be split to satisfy the limit?
No. A word longer than max_chars_per_line causes an input error, preserving the transcript rather than altering it silently.
How are fractional seconds formatted?
Seconds are rounded to the nearest millisecond and emitted with the standard SRT comma separator.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/audio/audio-to-text-srt-align \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"transcript":"Welcome to the demonstration. This subtitle wraps cleanly.","timestamps":[{"start":0,"end":2.4,"text":"Welcome to the demonstration."},{"start":2.4,"end":5.2,"text":"This subtitle wraps cleanly."}]}'const res = await fetch("https://api.kit.forhosting.com/audio/audio-to-text-srt-align", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"transcript": "Welcome to the demonstration. This subtitle wraps cleanly.",
"timestamps": [
{
"start": 0,
"end": 2.4,
"text": "Welcome to the demonstration."
},
{
"start": 2.4,
"end": 5.2,
"text": "This subtitle wraps cleanly."
}
]
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/audio/audio-to-text-srt-align",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"transcript": "Welcome to the demonstration. This subtitle wraps cleanly.",
"timestamps": [
{
"start": 0,
"end": 2.4,
"text": "Welcome to the demonstration."
},
{
"start": 2.4,
"end": 5.2,
"text": "This subtitle wraps cleanly."
}
]
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/audio/audio-to-text-srt-align", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"transcript":"Welcome to the demonstration. This subtitle wraps cleanly.","timestamps":[{"start":0,"end":2.4,"text":"Welcome to the demonstration."},{"start":2.4,"end":5.2,"text":"This subtitle wraps cleanly."}]}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"transcript":"Welcome to the demonstration. This subtitle wraps cleanly.","timestamps":[{"start":0,"end":2.4,"text":"Welcome to the demonstration."},{"start":2.4,"end":5.2,"text":"This subtitle wraps cleanly."}]}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/audio/audio-to-text-srt-align", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"transcript": "Welcome to the demonstration. This subtitle wraps cleanly.",
"timestamps": [
{
"start": 0,
"end": 2.4,
"text": "Welcome to the demonstration."
},
{
"start": 2.4,
"end": 5.2,
"text": "This subtitle wraps cleanly."
}
]
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "audio.audio_to_text_srt_align",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_items | 10000 |
max_chars | 500000 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |