Blog · 25 August 2026 · 8 min read

Whisper hallucinates
on silence.

Speech recognition does not go quiet when nobody is talking — it invents, confidently. Gating on voice activity is the fix, and the standard way to set that gate silently throws away thirty seconds of somebody presenting.

Ampersand can record a meeting — in a room, or a call where it captures the far end too — and turn it into a note with a summary, the decisions, the action items and the full transcript. Speech recognition happens on your own machine, with a Whisper model running through whisper.cpp on the GPU via Metal. No bot joins the call, and no audio is uploaded anywhere.

Cloud transcription was an explicit non-goal rather than something we ran out of time for: an hour of a private conversation, including whoever else was in the room, is the most sensitive thing this app would ever transmit. Running the model locally is the easy part. Deciding what to hand it is where the work was.

the speech model, on your Mac
~570 MBthe speech model, on your Maclarge-v3-turbo, q5-quantized — fetched once, on first use
the faster, rougher option
~140 MBthe faster, rougher optionbase — for when the big one is more than you want to store
languages you can pin
43languages you can pinplus Automatic; a set language is what makes a wrong guess impossible

A meeting is mostly silence

Speech recognition does not return an empty string when nobody is speaking. Whisper was trained on a large corpus of transcribed video, so its idea of “audio with no speech in it” is shaped by intros, outros, background music and subtitle credits. Feed it room tone and it will confidently produce something.

The classic failure
# the shape of it. the model was handed every second
# of the meeting, including the ones nobody spoke in.

00:14:02   Thank you.
00:14:09   Thank you.
00:14:16   Thank you.
00:14:23   Subtitles by the Amara.org community
00:14:31   Thank you.

# nothing failed. nothing warned. the model was asked
# what was said, and it answered.
A mishearing is a wrong answer to a real question. This is an answer to no question at all — there was nothing said in those seconds to get wrong — and it arrives with the same timestamp, the same formatting and the same confidence as every line somebody really spoke.

The repeated “Thank you.” and the fabricated subtitle credit are the two that everyone who has pointed Whisper at a long recording has seen. On a podcast they are a curiosity; in a transcript somebody will read as a record of what was said they are a defect, and nothing in the output distinguishes them from the real lines.

Now the ratio. A meeting has pauses between sentences, silence while somebody finds a document, the four minutes at the start where people are still joining, and a stretch after the goodbyes. Measured in seconds, a meeting is mostly silence — so gating on voice activity is not a performance optimisation that saves GPU time. It is a correctness requirement.

The textbook threshold throws away speech

The standard energy-based gate is two lines. Split the audio into short frames, take the energy of each, and estimate the noise floor as a low percentile of those energies — the quietest tenth of a recording is a fair guess at the room with nobody in it. Then call anything a fixed multiple above the floor speech, with an absolute minimum so a digitally silent source does not produce a threshold of zero.

Two terms, and the third one that was missing
# the textbook gate: a noise floor taken from a low
# percentile of frame energy, times a multiplier.

floor      = percentile(frame_energy, low)
threshold  = max(absolute_floor, floor * voice_multiplier)

# thirty unbroken seconds of one person presenting:
# the low percentile lands INSIDE the speech, the
# multiplier lifts the threshold above every frame in
# the segment, and the whole stretch is classified as
# silence and dropped. no error. no output.

# the fix is a third term -- a ceiling on the threshold,
# taken from a high percentile of the same signal.

speech_peak = percentile(frame_energy, high)
threshold   = min(threshold, speech_peak * peak_fraction)
The first two terms describe the room. The third describes the speech — and without a term anchored to the loud end of the signal, the estimator has no way to notice that its “quietest tenth” was somebody talking quietly.
One horizontal line, two answers · schematic
The textbook gate
frame energy · no scale
The textbook gate on a segment made entirely of speechFrame energy across a segment with no silence in it. The noise-floor estimate, taken from the tenth percentile of frame energy, lands inside the speech; the threshold derived from it sits above every frame, so the whole segment is classified as silence and discarded.time →10th percentilethe floor estimatethreshold= floor × multiplier

The floor estimate has landed inside the speech, so the threshold derived from it clears the loudest frame in the segment. Nothing is above the line: thirty seconds of somebody presenting is classified as silence and dropped.

With the third term
frame energy · no scale
The same segment once the threshold is cappedThe same frame energies. A cap taken from the ninety-fifth percentile holds the threshold below the speech however high the floor estimate is, so every frame is classified as speech and the segment is kept.time →95th percentilethe peakthreshold= fraction × peak

The same frames, the same floor estimate. A cap taken from the peak holds the threshold under speech that is plainly speech, whatever the floor says. Every frame is kept.

Frames the gate keepsFrames it discards
Both panels are drawn from identical frame values, and the only thing that differs between them is where the solid line sits. On a segment that does contain silence, the floor term is the one doing the work and the cap never binds — it exists for the segment that has none. No energy series from a real meeting was ever recorded, so the envelope here is a shape, not a measurement, and it is drawn without units for that reason.

The estimator assumes the segment it is looking at contains some silence. Take one that does not — thirty unbroken seconds of a person presenting — and the low percentile no longer lands in the room tone. It lands in the quieter part of the speech. Multiply that by the voice factor and the threshold sits above every frame in the segment: the gate looks at thirty seconds of continuous talking and reports that nobody said anything.

The repair is a third term — cap the threshold at a fraction of a high percentile of the same signal. If the loudest part of this segment is plainly speech, the threshold may not exceed a set share of it, whatever the floor estimate says. One extra line, and the all-speech case stops being a hole.

It was found by a test, not by review. The gate had been read by a human and looked correct, because it is the correct formula for the case it was written for. What caught it was feeding it audio that was entirely speech and asserting that the boundaries came back non-empty.

And it is the more dangerous of the two directions. A hallucinated sentence is visible: it is in the transcript, it reads oddly, somebody notices. Audio that was silently dropped looks exactly like a pause. Nobody reads a transcript and thinks “there should be thirty more seconds here”. Of the two ways this gate can be wrong, one reports itself and the other does not.

Where the cuts go, and why overlap is not free

Speech is chopped into segments at silences, but a long uninterrupted stretch has to be cut somewhere regardless, so there are two kinds of boundary. One found in silence has no word spanning it and needs no stitching. A forced cut lands in the middle of a word, so its segments are padded to overlap slightly. Granting that overlap everywhere rather than only at forced cuts is not harmless: it makes the model transcribe the same speech twice. The padding and the minimum silence length are therefore pinned against each other rather than tuned independently.

Who was speaking: a fact on a call, a guess in a room

On a call we capture two audio tracks: your microphone, and the audio your Mac is playing. Those are separate signals from separate sources, so labelling one you and the other the far end is not inference and cannot be wrong. The two-track split is the diarization — no model, no embeddings, no chance of a confident mistake.

In a room there is one microphone and four people in front of it. Real multi-speaker diarization means computing voice embeddings and clustering them, and it is wrong often enough that a transcript would sometimes attribute a sentence to the person who did not say it. Putting the wrong quote in a colleague’s mouth is worse than attributing nothing, and shipping that in a first version is how a feature earns a reputation it never loses.

So an in-person transcript carries no speaker labels at all, and the note says so in a quoted line closing the transcript. Stating a limit inside the artifact costs one sentence and stops somebody discovering it three months later while relying on it.

Where a speaker label comes from · schematic
On a call · two tracks
Your microphoneMe
Your Mac’s audioOthers

Two signals from two sources. No model is involved, so there is no confident mistake available to make.

In a room · one track
One microphoneunlabelled
Everyone in front of itunlabelled

One signal, several people. Telling them apart needs voice embeddings and clustering — a guess, and not one we are willing to put in somebody’s mouth.

On a call the label is a property of which track the audio arrived on — a fact about the recording rather than a judgement about a voice, which is why it cannot be wrong. In a room there is one track, so there is nothing for it to be a fact about, and the transcript says so instead of guessing.

A wrong language does not fail. It translates.

The bug that took longest to see had nothing to do with audio. Whisper can detect the spoken language, and if you do not tell it one it guesses per decoding window. A wrong guess does not error and does not warn — it translates. A Serbian conversation came back as fluent, plausible English.

That is the same failure class as the silence hallucination: confident, well-formed output, wrong in a way the output itself cannot express. We had detection. It never ran on the path most people use.

Detection under a continue
# what the loop used to do

for segment in session:
    if already_transcribed(segment):
        continue                    # <-- detection lived below this

    if language is unknown:
        language = detect(segment)

    transcribe(segment, language)

# a session that is COMPLETE and has no language never
# reaches the detector. that is every recording started
# from inside a note, because those transcribe while you
# are still talking.
One early exit, three lines above the code that had to run. Every path through this loop worked except the one where a session arrives already complete with no language recorded — which is what recording from inside a note produces every single time.

Detection sat inside the per-segment loop, below the early exit for segments already done, so it could only ever fire on a segment that also needed transcribing. Every combination worked except one: a session that is complete and has no language recorded, which is precisely what a recording started from inside a note produces, because those transcribe while you are still speaking. By the final pass there is nothing left to transcribe, so nothing to hang detection off, and decoding fell back to the per-window guess. The path that worked in testing was the one nobody uses.

The repair had three parts, and only one of them is the bug fix.

  • The three-way decision — skip, detect only, or transcribe — became a small named function with its own tests, precisely because the conditional was the thing that was wrong. A mutation of it now fails a test instead of a meeting.
  • Detection runs once per session, on the longest available window. Whisper reads only the opening of what it is given, so a one-second window is close to a coin flip and a half-minute one is not. The old code sampled the first segment — the worst possible one: “can you hear me?”, room tone, somebody still joining.
  • The verdict is pinned to the session and persisted immediately, so the repair case — which reaches the loop with nothing left to transcribe — does not recompute it forever.

We also log which of three routes decided the language — set explicitly, carried over, or detected — with the confidence and the window length, because a run that never detected anything used to be indistinguishable from one that never tried. And the override lists 43 languages: if you work in a language other than English, setting it is what makes a wrong guess impossible rather than merely recoverable.

Where the audio goes, and the one thing that can leave

A two-hour recording cannot sit in memory, and a crash ninety minutes in must not cost you the meeting, so audio is buffered to a folder outside your vault — never into iCloud or whatever else syncs it. On a Mac that can store a key for it, that buffer is encrypted at rest with a key belonging to that machine alone, not with your vault passphrase. Recording never asks whether your vault is unlocked, a decision we got wrong the first time and wrote up separately.

It becomes eligible for deletion only once its content exists somewhere that is not the buffer, so a crash between stopping and writing cannot cost you the recording.

Transcription itself is entirely local. The one thing that can leave the machine is the finished transcript, if you have pointed AI summarization at an off-device provider — a reasonable thing to do, which the app discloses permanently in its settings, naming the provider it would go to. That line appears only when the provider really is off-device: a local model or Apple Intelligence sends nothing anywhere and gets no warning, because a warning on the route that carries no risk is how people learn to ignore the one that does. It replaced a per-recording confirmation dialog, which was right about the substance and wrong about the placement: the provider is chosen once, so re-asking every time was friction with no decision in it.

What we took from it

  • A model that cannot say “nothing” will say something. Wherever you hand a generative model an input that might be empty, decide what empty means before it does.
  • Estimators carry assumptions about their input. A noise floor from a low percentile assumes there is noise to find. Test the distribution you will really see, degenerate ends included.
  • Prefer the failure that announces itself. Between output that is visibly wrong and output that is invisibly missing, test the second — nobody reports a gap they cannot see.
  • Do not promise inference you cannot stand behind. Two microphones are a fact; one microphone and a clustering model is a guess, and an unlabelled transcript beats a confidently mislabelled one.

The meetings overview covers what the feature does day to day, the documentation every setting behind it, and the technical specifications the rest of the app.

Get started

Recorded on your Mac.
Transcribed on your Mac.

Ampersand records a meeting, transcribes it on-device, and writes the note — without a bot joining the call or a byte of audio leaving the machine.

Free to start · Mac, Windows & Linux · No account required