speech_mlx
Provider-neutral MLX Audio batch recognition and incremental text-to-speech for
speech_core. MLX stays a batch inference engine: compose it with VAD and
utterance buffering instead of treating it as a streaming recognizer.
final provider = MlxSpeechProvider(
worker: MlxIsolateBatchWorker(
modelPath: 'mlx-community/parakeet-tdt-0.6b-v2',
),
modelId: 'parakeet-tdt-0.6b-v2',
ttsWorker: MlxIsolateTtsWorker(
modelPath: 'mlx-community/pocket-tts',
),
ttsModelId: 'pocket-tts',
voiceIds: const <String>['alba'],
);
try {
final result = await provider.transcribe(
BatchRecognitionRequest(audio: utteranceSource),
);
final source = provider.synthesize(
SpeechSynthesisRequest(
text: result.text,
voiceId: 'alba',
providerOptions: const MlxSynthesisOptions(seed: 7),
),
);
final session = await source.prepare();
final frames = session.frames.listen(routeFrame);
await session.start();
await frames.cancel();
await session.close();
} finally {
await provider.close();
}
One MlxSpeechProvider advertises every configured capability under the stable
mlx provider ID, so it can be registered once without colliding STT and TTS
adapters. Either worker may be omitted when an application needs only one
capability.
utteranceSource must be finite, continuous, and within
maximumInputSamples. Capture, VAD, utterance buffering, and routing remain
outside the provider. Source-native interleaved PCM crosses the worker boundary;
the long-lived model isolate performs the full concatenation, downmix, and
resampling work away from Flutter's UI isolate.
MlxIsolateBatchWorker and MlxIsolateTtsWorker load one model per long-lived
isolate and serialize inference. Their request queues are bounded. TTS delivers
real TtsModel.onAudioChunk PCM before generation completes and enforces a
finite maximumOutputSamples limit before sending chunks, so a stalled
consumer cannot create unbounded cross-isolate storage. The returned audio
source is intentionally non-pausable; attach its single frame stream to an
AudioRouter before calling start.
Cancelling active synchronous MLX inference terminates the worker isolate promptly. A later request transparently starts a fresh worker because native MLX kernels cannot process a Dart port message while a kernel is running.
Use SerializedMlxBatchWorker and SerializedMlxTtsWorker for deterministic
tests or embedding-specific workers. They serialize calls but do not move work
off the calling isolate. Their close timeout makes cleanup deterministic even
when an injected callback violates the cooperative-cancellation contract.
Turn completion
MlxSmartTurnScorer implements TurnCompletionScorer over the pinned Smart
Turn v3.2 classifier: given the audio leading up to a pause, it answers whether
the speaker was finished, which is the question a silence timeout cannot ask.
final scorer = MlxSmartTurnScorer(worker: MlxIsolateTurnWorker());
try {
final score = await scorer.scoreTurnCompletion(
// A ring-buffer slice ending at the pause. Trailing silence belongs in it.
TurnCompletionRequest.fromSamples(window),
);
if (score.isComplete) endTurn(score.probability);
} finally {
await scorer.close();
}
It is a separate provider from MlxSpeechProvider on purpose: the two own
different checkpoints with different lifecycles, and a detector usually wants
the turn model resident long before any transcription runs. It advertises only
SpeechCapability.turnCompletion, under the same stable mlx provider ID.
MlxIsolateTurnWorker takes no model path — Smart Turn is pinned by revision
and per-file SHA-256 inside mlx_audio, so the only knob is modelDirectory
for an already-materialized snapshot. Loading caps MLX's buffer cache and warms
the Metal kernels on silence, so the first real pause sees steady-state latency
(~5 ms per 8 s window after warmup, vs ~1.8 s cold).
Windows longer than the classifier's 8 s are cropped to their tail before
the isolate hop, and shorter ones are left-padded by the model: the decision is
about how the audio ended. Windows at another sample rate keep it — resampling
happens next to the model, with the same polyphase filter mlx_audio uses
elsewhere. Scoring failures raise SpeechFailure; the scorer never invents a
probability to keep a caller running, because a made-up 0.5 is
indistinguishable from a genuinely uncertain model.
Libraries
- speech_mlx
- MLX Audio adapters for provider-neutral speech contracts.