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
Word timestamps across Whisper setups
What each setup needs to return words, and what it gave back on the same 120 seconds of audio.
| Setup | How you ask for words | What came back | License |
|---|---|---|---|
| faster-whisper | word_timestamps=True | 209 words, the first ending at about 0.3 s | MIT |
| openai-whisper | word_timestamps=True | 234 words: the same 209, then 25 never said | MIT |
| whisper.cpp | -ml 1 -sow -oj | 214 entries, 5 of them empty | MIT |
| WhisperX | wav2vec2 alignment after Whisper | 209 words, 0 without a time | BSD-2-Clause |
| OpenAI API, whisper-1 | timestamp_granularities[]=word with verbose_json | Not run here | Paid API |
| clipsdk | captions: "karaoke" | 43 words burned into a 22-second clip, plus SRT and VTT | Paid 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.
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.1Transcribe 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 intowords.jsonfor 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 wordsCut 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
\ktags 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.
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"
}'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.
# 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
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
# 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_THREADStoo.
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.
Keep reading
- Animated captions, burned in word by wordBurn word-by-word captions into every clip in one of 3 presets: bold, karaoke or minimal. Or set captions to false for clean clips with SRT and VTT files. $0.03 per source minute.
- Clip every conference session the same dayUpload each conference session as it ends and get up to 20 ranked vertical clips per talk. A 40 minute keynote costs ~$1.20, with 3 jobs at once and 200 jobs a day.
- POST /v1/clips referencePOST /v1/clips takes a video URL and returns up to 20 clips of 10 to 90 s, scored 0 to 100. Defaults: 5 clips of 20 to 60 s, 9:16 at 1080x1920, bold captions, auto reframe.
- Scene cut detection for clipsFind hard cuts with PySceneDetect and ffmpeg scdet, split a clip into shots, and see why a vertical reframe must restart at each one.
- Active speaker detection in PythonFind who is talking in a two-person shot on a CPU with YuNet landmarks and mouth motion, then check it against the transcript.
- AutoFlip alternativeGoogle ended AutoFlip support in 2023. Rebuild its shot-by-shot vertical crop in Python with MediaPipe Tasks and PySceneDetect, or send one request.
Sources
- faster-whisper on GitHub, checked 7 November 2026
- openai-whisper on GitHub, checked 7 November 2026
- whisper.cpp v1.9.5 release, checked 7 November 2026
- WhisperX on GitHub, checked 7 November 2026
- OpenAI API spec, transcriptions, checked 7 November 2026
- Workers AI pricing, checked 7 November 2026
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.
