Guide

faster-whisper, openai-whisper, whisper.cpp and WhisperX all return word timestamps. Cut them to a clip and page them 5 at a time into karaoke captions, or let clipsdk do the whole job for $0.03 per source minute.

Whisper word timestamps from 4 engines, cut to one clip and paged 5 words at a time

Whisper can give every word its own start and end time, and a clip needs those times moved to its own zero before captions can light up one word at a time.

GuidesUpdated

Or one API call

$0.03per source minute

  • Every clip included, up to 1080p60 and 20 Mb/s
  • $1 free to start, 33 minutes of video
  • Prepaid credit, no subscription
See pricing

Word timestamps across Whisper setups

What each setup needs to return words, and what it gave back on the same 120 seconds of audio.

Word timestamps across Whisper setups
SetupHow you ask for wordsWhat came backLicense
faster-whisperword_timestamps=True209 words, the first ending at about 0.3 sMIT
openai-whisperword_timestamps=True234 words: the same 209, then 25 never saidMIT
whisper.cpp-ml 1 -sow -oj214 entries, 5 of them emptyMIT
WhisperXwav2vec2 alignment after Whisper209 words, 0 without a timeBSD-2-Clause
OpenAI API, whisper-1timestamp_granularities[]=word with verbose_jsonNot run herePaid API
clipsdkcaptions: "karaoke"43 words burned into a 22-second clip, plus SRT and VTTPaid API

Build word-timed captions for one clip

Three steps on a CPU: pull the audio, transcribe with word times, then cut and page the words for the clip.

  1. Pull the audio

    We pulled a mono WAV from the first 120 seconds of the NASA talk the API demo uses, so the clip we caption later sits inside it.

    terminal
    curl -O https://api.clipsdk.com/f/lib/demo/artemis-moon-speech.mp4
    ffmpeg -i artemis-moon-speech.mp4 -t 120 -vn -ac 1 -ar 16000 talk.wav
    pip install "faster-whisper==1.2.1" "av==15.1.0"  # av 19.0.2 breaks faster-whisper 1.2.1
    
  2. Transcribe with word timestamps

    With word_timestamps=True, faster-whisper puts a list of words inside every segment, each with a start and end in seconds. We flatten them into words.json for the next step.

    faster_whisper_words.py
    import json
    from faster_whisper import WhisperModel
    
    model = WhisperModel("large-v3-turbo", device="cpu", compute_type="int8")
    segments, info = model.transcribe("talk.wav", word_timestamps=True, language="en")
    
    words = [{"word": w.word.strip(), "start": w.start, "end": w.end} for s in segments for w in s.words]
    json.dump(words, open("words.json", "w"))
    
    for w in words[:6]:
        print(f'{w["start"]:6.2f} {w["end"]:6.2f} {w["word"]}')
    print(len(words), "words")
    
    # $ python faster_whisper_words.py
    #   0.00   0.34 Throughout
    #   0.34   1.28 America's
    #   1.28   1.86 story,
    #   2.50   2.62 there
    #   2.62   2.92 are
    #   2.92   3.62 defining
    # 209 words
    
  3. Cut the words to the clip and page them

    The API ranked the stretch from 50 s to 71 s as its second clip, so we caption that range. A word belongs to the clip its midpoint falls in, and its times move to the clip's own zero. Pages hold up to 5 words and break on a pause over 0.45 s or at a sentence end. The \k tags give each word its length in hundredths of a second, which is what makes a karaoke highlight move one word at a time.

    clip_captions.py
    import json
    
    CLIP_START, CLIP_END = 49.51, 71.24
    MAX_WORDS, PAUSE_S = 5, 0.45
    
    words = json.load(open("words.json"))
    
    # A word belongs to the clip its midpoint falls in, and its times become clip-relative
    clip = [
        {"word": w["word"], "start": max(w["start"], CLIP_START) - CLIP_START, "end": min(w["end"], CLIP_END) - CLIP_START}
        for w in words
        if CLIP_START <= (w["start"] + w["end"]) / 2 < CLIP_END
    ]
    
    # New caption page on a full page, a pause or a sentence end
    pages, page = [], []
    for w in clip:
        if page and (len(page) == MAX_WORDS or w["start"] - page[-1]["end"] > PAUSE_S or page[-1]["word"][-1] in ".?!"):
            pages.append(page)
            page = []
        page.append(w)
    pages.append(page)
    
    
    def stamp(t):
        ms = round(t * 1000)
        return f"{ms // 3600000:02}:{ms // 60000 % 60:02}:{ms // 1000 % 60:02},{ms % 1000:03}"
    
    
    def karaoke(page):
        parts = []
        for i, w in enumerate(page):
            until = page[i + 1]["start"] if i + 1 < len(page) else w["end"]
            parts.append(f'{{\\k{round((until - w["start"]) * 100)}}}{w["word"]}')
        return " ".join(parts)
    
    
    with open("clip.srt", "w") as srt:
        for i, p in enumerate(pages, 1):
            srt.write(f'{i}\n{stamp(p[0]["start"])} --> {stamp(p[-1]["end"])}\n{" ".join(w["word"] for w in p)}\n\n')
    
    print(len(clip), "words,", len(pages), "pages")
    print("\n\n".join(open("clip.srt").read().split("\n\n")[:3]))
    print(karaoke(pages[0]))
    
    # $ python clip_captions.py
    # 43 words, 9 pages
    # 1
    # 00:00:00,210 --> 00:00:02,930
    # President Kennedy knew that vision
    #
    # 2
    # 00:00:02,930 --> 00:00:05,010
    # would be hard, not easy.
    #
    # 3
    # 00:00:05,310 --> 00:00:06,610
    # He made that clear.
    # {\k82}President {\k54}Kennedy {\k44}knew {\k44}that {\k48}vision
    

Or send one request

Send the talk with captions: "karaoke" and the API transcribes it with @cf/openai/whisper-large-v3-turbo, picks the clips, gives each clip the words whose midpoint falls inside it and burns them in. Each clip also comes with SRT and VTT files. The presets are bold with 3 words a page, karaoke with 5 and minimal with 7. There are no custom styles.

The 8-minute talk returned 3 clips. Clip 2 holds 43 words, the range the code above captions by hand. At $0.03 per source minute, billed per second, the talk costs about $0.24.

POST/v1/clipsTry it
curl https://api.clipsdk.com/v1/clips \
  -H "Authorization: Bearer $CLIPSDK_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "video_url": "https://api.clipsdk.com/f/lib/demo/artemis-moon-speech.mp4",
    "clips": 3,
    "captions": "karaoke",
    "language": "en"
  }'
Clip 2, 22 seconds and 43 words, with karaoke captions burned in.clip-2.mp4clip-2.srtclip-2.vtt

The same audio in three other engines

faster-whisper, whisper.cpp and WhisperX all returned the same 209 words. openai-whisper returned those 209 plus 25 that nobody said, all crammed into the last few hundredths of a second. Timing differs too: the first word ends at about 0.3 s in faster-whisper and openai-whisper, 0.6 s in WhisperX and 0.8 s in whisper.cpp.

WhisperX leaves small gaps between words, a few hundredths of a second between the first two. faster-whisper and whisper.cpp run most words straight into the next, which hides pauses when you page captions.

In whisper.cpp, -ml 1 sets the maximum segment length to 1 character, so every token would start a new segment, and -sow splits on words instead of tokens. Each JSON entry then holds one word, with times in milliseconds.

openai_whisper_words.py
# pip install "openai-whisper==20250625"
import json

import whisper

model = whisper.load_model("turbo")
result = model.transcribe("talk.wav", word_timestamps=True, language="en")

words = [{"word": w["word"].strip(), "start": w["start"], "end": w["end"]} for s in result["segments"] for w in s["words"]]
json.dump(words, open("words-openai.json", "w"))

for w in words[:6]:
    print(f'{w["start"]:6.2f} {w["end"]:6.2f} {w["word"]}')
print(len(words), "words")

# $ OMP_NUM_THREADS=2 NUMBA_NUM_THREADS=2 python openai_whisper_words.py
#   0.00   0.34 Throughout
#   0.34   1.26 America's
#   1.26   1.86 story,
#   2.50   2.62 there
#   2.62   2.92 are
#   2.92   3.62 defining
# 234 words
whisper.cpp
git clone --depth 1 --branch v1.9.5 https://github.com/ggml-org/whisper.cpp
cd whisper.cpp && cmake -B build && cmake --build build -j --config Release
sh ./models/download-ggml-model.sh large-v3-turbo
./build/bin/whisper-cli -m models/ggml-large-v3-turbo.bin -f ../talk.wav -l en -ml 1 -sow -oj -of ../talk
jq -r '.transcription[:6][] | "\(.offsets.from / 1000) \(.offsets.to / 1000) \(.text)"' ../talk.json
jq '.transcription | length' ../talk.json

# output:
# 0 0.06
# 0.06 0.79  Throughout
# 0.79 1.5  America's
# 1.5 2.35  story,
# 2.35 2.64  there
# 2.64 2.96  are
# 214
whisperx_words.py
# pip install "whisperx==3.8.6"
import whisperx

audio = whisperx.load_audio("talk.wav")
model = whisperx.load_model("large-v3-turbo", "cpu", compute_type="int8", language="en")
result = model.transcribe(audio, batch_size=4)

align_model, metadata = whisperx.load_align_model(language_code="en", device="cpu")
aligned = whisperx.align(result["segments"], align_model, metadata, audio, "cpu")

words = aligned["word_segments"]
timed = [w for w in words if "start" in w]
for w in timed[:6]:
    print(f'{w["start"]:6.2f} {w["end"]:6.2f} {w["word"]}')
print(len(words), "words,", len(words) - len(timed), "without a time")

# $ python whisperx_words.py
#   0.03   0.59 Throughout
#   0.65   1.21 America's
#   1.35   1.99 story,
#   2.45   2.69 there
#   2.83   2.97 are
#   3.05   3.75 defining
# 209 words, 0 without a time

Mistakes that break clip captions

Most caption bugs on clips are timing bugs, not transcription bugs.

  • Times from the start of the file. Subtract the clip start, or every caption lands late by the clip's offset.
  • Words on the cut. Decide by midpoint, so a word is never in two clips or in none.
  • Pages that are too long. Cap the words per page and break on pauses, or the highlight crawls across a whole sentence.
  • Empty entries. whisper.cpp gave 5 empty entries here, the first at the very start; drop entries with no letters before you page.
  • Words with no time. WhisperX gives no time to words its aligner has no letters for, like numbers and currency, so borrow the neighbouring word's time.
  • Words never said. openai-whisper ended our file with 25 made-up words, each far shorter than any real word, so a 0.05 s floor drops them.
  • Thread caps that do not stick. openai-whisper's word timing runs numba, which raised torch from 2 threads to 24 on our 2-CPU sandbox and slowed encoding from 13 s to 76 s a window. Set NUMBA_NUM_THREADS too.

What it costs to run yourself

The models are free, so the cost is CPU time and your own. Word times are only the start: you still cut clips, frame them for 9:16, render captions and keep it all working when a library changes.

Hosted, Cloudflare Workers AI runs whisper-large-v3-turbo at $0.0005 per audio minute. That buys the words, not the clip. The clipsdk API charges $0.03 per source minute, billed per second, for the transcript, the ranked clips, the reframe and the burned-in captions. See pricing.

Questions, answered

How do I get word-level timestamps from Whisper?

Pass word_timestamps=True to transcribe() in openai-whisper or faster-whisper and read the words inside each segment. In whisper.cpp, run whisper-cli with -ml 1 -sow so each segment holds one word. The OpenAI API needs timestamp_granularities[]=word with response_format=verbose_json.

Which engine gives the most accurate word times?

We did not score accuracy against hand-labelled times. WhisperX re-aligns each word with a wav2vec2 model, which its README sells as accurate word-level timestamps. Our first word ended anywhere from about 0.3 to 0.8 s depending on the engine, so check a few by ear.

Why are my captions late on a clip?

The word times still count from the start of the source file. Subtract the clip start from every word, and assign words by midpoint so words at the edges are neither lost nor doubled.

Does the clipsdk API return word timestamps?

Not as a word list. Each clip comes with its transcript text, SRT and VTT files and burned-in captions, all built from the word times the engine keeps for that clip.

Start free

Your back catalog is full of shorts

Your first $1 is free: 33 minutes of video, every clip included up to 1080p60 and 20 Mb/s.

Two studio microphones on a desk in a dark room lit by a red glow