whisper_ggml 2.6.0
whisper_ggml: ^2.6.0 copied to clipboard
OpenAI Whisper ASR (Automatic Speech Recognition) for Flutter
Whisper GGML #
On-device speech-to-text for Flutter, powered by whisper.cpp v1.9.1.
Transcribe audio files or transcribe live while the user speaks — fully on-device, no server, no API keys.
Highlights #
- 🎙 Live transcription — partial transcripts stream in while recording, refined as more audio arrives. The model loads once per session and an adaptive energy gate keeps silence from producing hallucinated text.
- 📄 File transcription — one call to transcribe a recording.
- 📦 Offline-first — models download once and are cached, or ship them in your app's assets for fully offline use (see the example app).
- 🌍 99 languages — pick one (
'en','fr','de', …) or use'auto'to detect. - 🎛 Decoding controls — vocabulary biasing, context conditioning, and non-speech token suppression exposed from whisper.cpp.
- ⚡ Fast — whisper.cpp v1.9.1 with Accelerate on Apple platforms;
an 11-second clip transcribes in ~0.4 s with the
basemodel on an Apple Silicon Mac.
Supported platforms #
| Platform | Minimum version |
|---|---|
| Android | API 21 |
| iOS | 15.6 |
| macOS | 10.15 |
| Windows | 10 (x64) |
| Linux | x64 |
Installation #
dependencies:
whisper_ggml: ^2.6.0
Requires Dart 3.7+ (Flutter 3.29+).
Quick start #
import 'package:whisper_ggml/whisper_ggml.dart';
final controller = WhisperController();
final result = await controller.transcribe(
model: WhisperModel.tiny,
audioPath: '/path/to/audio.wav',
lang: 'en',
);
print(result?.transcription.text);
Pass onProgress: (percent) => ... to receive transcription progress
as a 0–100 percentage (coarse steps) while inference runs.
Pass withSegments: true to also get per-segment timestamps in
result.transcription.segments (each segment has fromTs/toTs
Durations and its text); add splitOnWord: true for one segment
per word instead of per phrase.
Speaker-turn detection (diarization) #
With the tinydiarize model (WhisperModel.smallEnTdrz, English only),
diarize: true marks the segments after which the speaker changes:
final result = await controller.transcribe(
model: WhisperModel.smallEnTdrz,
audioPath: '/path/to/audio.wav',
lang: 'en',
diarize: true,
withSegments: true,
);
for (final segment in result?.transcription.segments ?? []) {
print('${segment.text}${segment.speakerTurnNext ? ' [speaker turn]' : ''}');
}
This detects turn boundaries; it does not label or count speakers. With
regular models diarize has no effect.
The model is downloaded automatically on first use. Non-WAV input is
converted with the bundled FFmpeg — except on Windows and Linux, where
FFmpeg is not bundled: an ffmpeg executable on PATH is used when
present (on Linux, apt install ffmpeg or equivalent), otherwise the
input must already be a 16 kHz mono WAV (the format the record package
produces with AudioEncoder.wav, sampleRate: 16000, numChannels: 1).
Live (streaming) transcription #
transcribeLive accepts any stream of 16 kHz mono little-endian PCM16
audio and emits progressively refined transcripts while the audio flows.
With the record package:
final pcmStream = await recorder.startStream(const RecordConfig(
encoder: AudioEncoder.pcm16bits,
sampleRate: 16000,
numChannels: 1,
));
final session = await controller.transcribeLive(
model: WhisperModel.base,
pcm16Stream: pcmStream,
lang: 'en',
);
session.partials.listen((text) {
print(text); // full transcript so far, not a delta
});
// Later — stop recording, finalize, and free the model:
await recorder.stop();
final finalText = await session.stop();
Good to know:
- The model stays loaded for the whole session; inference runs on a background isolate and never blocks the UI.
- An adaptive energy gate keeps silence away from the decoder, which
otherwise hallucinates on silent audio. For unusually loud rooms or quiet
speakers, tune
gateNoiseFloorCap,gateVoiceRatio, andgateRmsMin. - Only one live session can run at a time.
- Real non-speech sounds (knocks, clicks) may transcribe as bracketed
annotations like
[door slams]. - Bring your own model: pass
modelPath:instead ofmodel:to run the session against any local ggml model file. For full manual control,startWhisperLiveSession(modelPath: ...)is exported too — no audio stream wiring, you callsession.feed(pcm16Bytes)yourself. - Session-per-utterance? Pass
keepModelLoaded: trueand the model is parked in native memory when the session stops, so the next session (or one-shottranscribe) with the same model starts without the multi-second load. See "Keeping the model loaded between transcriptions" below — the same cache, memory trade-off, andreleaseModel()apply.
Models #
| Model | Multilingual | English-only |
|---|---|---|
| tiny | WhisperModel.tiny |
WhisperModel.tinyEn |
| base | WhisperModel.base |
WhisperModel.baseEn |
| small | WhisperModel.small |
WhisperModel.smallEn |
| medium | WhisperModel.medium |
WhisperModel.mediumEn |
| large-v3 | WhisperModel.large |
— |
Smaller models are faster; larger models are more accurate. tiny and
base are good defaults for live transcription; small is a strong
accuracy/speed balance for file transcription on modern phones.
Decoding options #
Available on both transcribe and transcribeLive:
| Option | Default | What it does |
|---|---|---|
initialPrompt |
null |
Biases decoding toward the vocabulary, names, and punctuation it contains — useful for domain-specific terms that otherwise get misrecognised. Decoding also mimics the prompt's style: an unpunctuated prompt tends to produce unpunctuated output. |
noContext |
false |
Stops whisper from conditioning on prior-segment transcripts (like Python whisper's condition_on_previous_text=False). Helps against hallucinated repetition on short, independent utterances. |
suppressNonSpeechTokens |
false |
Suppresses bracketed annotations such as [BLANK_AUDIO] or [music]. Side effect: real sounds may decode as plausible-looking words instead, which is why the example keeps it off. |
Keeping the model loaded between transcriptions #
By default every transcribe call loads the model from disk (seconds for
the small models and up) and frees it when the request completes. For
push-to-talk dictation, voice notes, and other short repeated requests
against the same model, that load dominates the per-utterance latency.
Pass keepModelLoaded: true to park the loaded model in native memory
instead — the next transcription with the same model skips the load:
final result = await controller.transcribe(
model: WhisperModel.base,
audioPath: audioPath,
keepModelLoaded: true, // model stays resident for the next request
);
// When dictation is over, free the parked model:
await controller.releaseModel();
Good to know:
-
A reused model transcribes exactly like a freshly loaded one — no text or decoder state carries over between requests.
-
The parked model keeps its weights in RAM (from ~100 MB for
tinyup to several GB for the large models) until you release it. On phones, release it when dictation ends rather than keeping it forever. -
One model stays resident per process; parking a different model replaces and frees the previous one. A request for a different model with
keepModelLoaded: falseleaves the parked one alone. -
Requests stay fully concurrent: a second transcription that arrives while the parked model is in use simply loads its own copy, exactly as before.
-
The native engine is compiled with
-O3on all platforms, including debug builds on iOS/macOS, Windows (/O2), and Linux — transcription speed there is close to release. -
Windows and Linux x64 builds target AVX2 by default, like upstream whisper.cpp's standard x64 binaries (supported by virtually every x64 CPU since ~2013). For very old CPUs, build with
-DWHISPER_GGML_AVX2=OFF. -
Android debug builds run the Dart layer in JIT mode; use
--releasefor representative performance. -
The bundled whisper.cpp v1.9.1 is roughly 15× faster than the engine in versions before 2.0.0.