Record a spoken answer. Get back what you actually said — hesitations, false starts, pauses and all.
Speech-to-text is trained on tidy subtitles, so it tidies you up. Say
"I think... uh... I think that people, they should... should have more time"
and most transcribers hand you back
"I think that people should have more time."
That is a better answer than the one you gave. For captions that is the right call. For exam practice it destroys the signal: examiners score hesitation, repair, and pausing, and a cleaned transcript hides exactly those things — from you, from your teacher, and from any AI you ask for feedback.
verbatim keeps them, and measures them.
## TIMING
speech span : 39.9s
first word at : 0.4s
clip length : 45.0s
time allowed : 45s
## RATE
words : 66 total, 41 content
speech rate : 62 wpm
articulation rate : 73 wpm
phonation ratio : 84%
mean run : 8.2 words between pauses
## PAUSES (>=0.3s)
count : 7 (short 4, medium 0, long 3)
silent total : 6.2s
1.2s at 5.4s before "I"
1.0s at 11.1s before "ate"
2.4s at 22.7s before "because"
## DISFLUENCY
fillers : 4 (6.0/min)
uh x4
hesitation marks : 21 [*] = probable cut-off
immediate repeats : 0
## VERBATIM ... 0.3-0.6s .... 0.6-1s ----- >1s
[*] Hi, ...[0.3s] uh, I was, ...[0.5s] uh, -----[1.2s] [*] I [*] ate, ...
Plus a cleaned transcript for grammar and vocabulary review.
This tool measures. It does not score. No bands, no thresholds, no "you should". Interpretation belongs to you, your teacher, or whatever you paste the output into.
| Metric | What it tells you |
|---|---|
| speech rate | words per minute including your silences |
| articulation rate | words per minute while actually speaking |
| phonation ratio | share of your time that was speech, not silence |
| mean run | words between pauses — a standard fluency measure |
| hesitation marks | [*] where the model detected a probable cut-off |
Speech rate and articulation rate together separate two very different problems. If speech rate is low but articulation rate is normal, you are stopping a lot. If both are low, you are speaking slowly. Those need opposite fixes, and a single "words per minute" number cannot tell them apart.
Two sources, merged, and the longer reading wins.
The obvious source is the gap between one word's end timestamp and the next word's start. On its own it is not enough. Whisper's word timings come from aligning the model's attention to the audio, and that alignment routinely absorbs trailing silence into the word before it — a real 1.4s pause can leave a gap of exactly zero. Word timings alone therefore miss the long pauses, which are the ones that matter.
So the audio is also read directly: ffmpeg's silencedetect, with a threshold set relative to the recording's own peak level rather than a fixed dB assumption. Each silent stretch is attributed to the word that follows it. On a test clip with a 1.4s pause, word gaps reported no pause at all and a phonation ratio of 86%; adding the waveform pass reported the pause and a phonation ratio of 68%.
Requires Python 3.9+ and ffmpeg.
git clone https://github.com/Rez-Shok/verbatim.git
cd verbatim
./install.shThe installer creates a virtualenv, installs
whisper-timestamped,
and tells you how to put verbatim on your PATH.
Manual install
python3 -m venv venv
./venv/bin/python -m pip install -U pip whisper-timestamped
chmod +x verbatim verbatim_asr.py
ln -s "$PWD/verbatim" ~/.local/bin/verbatimffmpeg: brew install ffmpeg (macOS), sudo apt install ffmpeg (Debian/Ubuntu).
Platforms: macOS (avfoundation), Linux (PulseAudio or ALSA), WSL, Windows
(dshow, less tested). The first run downloads a Whisper model (~500 MB for
small.en); after that it is fully offline. Nothing is uploaded anywhere.
verbatim mic # pick your microphone, once
verbatim # asks how long, records, transcribes
verbatim get # print the report, copy to clipboardIt asks for a duration and remembers your last answer, so on later runs you
just press enter. Give it a number for a timed take, or type f to record
freely and stop with enter when you are done.
Recording returns the moment the clip ends. A single background worker holds the model in memory and drains the queue, so your next take never waits for the previous transcription. Record four answers back to back, then collect them all:
verbatim ; verbatim ; verbatim ; verbatim
verbatim get allverbatim |
ask for a duration, then record |
verbatim 90 |
record for 90 seconds |
verbatim free |
record until you stop it |
verbatim -m "Describe a book" |
attach the prompt to the report |
verbatim status |
queue and worker state |
verbatim get [all] |
newest report, or all of today's |
verbatim add FILE |
queue an existing recording |
verbatim mic |
choose input device |
verbatim stop |
stop the background worker |
Reports and clips are kept per day under ~/verbatim-sessions/YYYY-MM-DD/.
Set the duration to whatever your exam allows — every speaking test uses different limits, and they change between versions, so the number comes from you rather than from a table baked into this tool. Pressing enter during a timed take stops it early and keeps what you have.
Free recording has a safety ceiling of 30 minutes (VERBATIM_MAX_FREE).
| Variable | Default |
|---|---|
VERBATIM_MODEL |
small.en |
VERBATIM_LANG |
en |
VERBATIM_FORMAT |
text (or json) |
VERBATIM_DATA |
~/verbatim-sessions |
VERBATIM_HOME |
~/.config/verbatim |
VERBATIM_MAX_FREE |
1800 (free-recording ceiling, seconds) |
VERBATIM_IDLE |
120 (seconds the worker stays warm when idle) |
VERBATIM_KEEP_WAV |
0 — set to 1 to keep uncompressed WAV |
VERBATIM_BITRATE |
128k (AAC bitrate for stored clips) |
VERBATIM_INPUT |
ffmpeg input arguments, replacing device selection |
VERBATIM_INPUT is the escape hatch for anything this script cannot enumerate
— a virtual cable, a named ALSA device, a container:
VERBATIM_INPUT="-f pulse -i alsa_input.usb-Blue_Microphones" verbatim 45verbatim_asr.py is a standalone tool; the recorder is optional.
./venv/bin/python verbatim_asr.py interview.m4a
./venv/bin/python verbatim_asr.py interview.m4a --format json
./venv/bin/python verbatim_asr.py --json saved.asr.json # re-measure, no ASR--json re-measures saved model output without touching the audio, so its
pauses come from word gaps alone and will read tidier than the originals.
Or hand it to the queue and let the warm worker take it: verbatim add interview.m4a.
The report is designed to paste directly into an LLM. Because it carries pause lengths and disfluency positions rather than a cleaned sentence, the model can comment on delivery instead of guessing from prose. A prompt that works well:
Here is a verbatim transcript of my spoken answer with pause timings. The task was a two-minute long turn on a familiar topic. Analyse my grammatical range, lexical resource, and coherence. Point out where the long pauses suggest I lost the thread, and rewrite two sentences to show a stronger response.
Use VERBATIM_FORMAT=json if you are building tooling on top.
| Model | Speed | Use when |
|---|---|---|
tiny.en / base.en |
fastest | clean audio, good mic, you want quick turnaround |
small.en |
default | reasonable balance |
medium.en |
slow | strong accent or noisy room, accuracy matters most |
export VERBATIM_MODEL=base.en # for the session
VERBATIM_MODEL=base.en verbatim # for one runNote the export: a bare VAR=value line sets a shell variable that child
processes never see. Changing the model restarts the background worker
automatically, since it holds the previous model in memory.
There is a real tension: bigger models transcribe more accurately but also normalise more aggressively, so they may quietly repair the very disfluencies you want to see. Smaller models stay rawer but mis-hear more words. Try both on one recording and pick what matches what you remember saying.
- Pronunciation is not measured. Whisper reports what you said, not how clearly you said it. For pronunciation scoring use a dedicated tool.
- Disfluency detection is approximate. The
[*]marks come fromwhisper-timestampedand are a signal, not ground truth. Listen to your recording before concluding anything from a high count. - Timestamps carry alignment error. Pauses are cross-checked against the waveform, which fixes the large ones, but durations near the 0.3s threshold should still be read as approximate.
- Silence detection assumes a reasonably quiet room. The threshold follows the recording's peak level; constant loud background noise will hide short pauses from the waveform pass, leaving only the word-gap estimate.
- English-first. Other languages work via
VERBATIM_LANG, but the filler and hedge lists inverbatim_asr.pyare English. Extend them for yours. - Noise wrecks everything. A quiet room and a decent mic matter more than model size.
Audio is captured with no resampling, downmixing or encoding applied to the
live stream — those are done afterwards — because realtime processing on a
capture stream is a common source of dropouts and artefacts. If you still hear
problems, record once with VERBATIM_KEEP_WAV=1 and listen to the WAV. If the
WAV is clean the issue is in encoding; if it is not, the capture itself is
dropping samples and a larger -thread_queue_size or a different device may
help.
The measurement layer is pure standard library, so the suite runs without PyTorch or a downloaded model. ffmpeg-dependent tests skip themselves if ffmpeg is missing.
python3 -m unittest discover -s tests -vUseful directions: filler lists for other languages, Windows testing, a whisper.cpp backend for Apple Silicon acceleration, richer fluency measures. Open an issue first for anything large.
MIT