listenflow
listenflow converts a stream of clean, 16 kHz mono PCM audio into asynchronous transcript events.
The core supplies engine contracts, shared utterance boundaries, and health snapshots. It has no
runtime dependencies and does not include a speech recognition model.
Engine modules are separate distributions and will be published individually as the needed modules
become ready. Supply an engine implementing STTEngineBase to perform recognition. The application
owns audio capture and cleanup, network transport, a persistent server, process supervision,
readiness, and recovery policy. Speech synthesis, dialogue control, and LLM calls also belong to
the application.
Install
Use Python 3.12 or 3.13. listenflow is licensed under MIT.
pip install listenflow
Use an engine
Use one engine instance on one event loop. An asynchronous context prepares the model on entry and closes the engine on exit, including when the body raises an exception:
from collections.abc import AsyncIterator
from listenflow import Final, STTEngineBase
async def recognize_stream(engine: STTEngineBase, pcm_chunks: AsyncIterator[bytes]) -> None:
async with engine:
async for event in engine.stream(pcm_chunks):
if isinstance(event, Final):
print(event.utterance_id, event.ts_offset_s, event.text)
snapshot = engine.health()
print(snapshot.processed_pcm_chunks, snapshot.emitted_events)
The application supplies both arguments: an engine instance and an asynchronous source of audio
chunks. Match the chunks to engine.capabilities.input_format. The shared PCM helpers accept
EngineInputFormat.PCM_S16LE (signed 16-bit little-endian) and EngineInputFormat.PCM_F32
(little-endian float32). Both represent 16 kHz mono audio; every chunk must contain whole samples.
Complete PCM input
Use transcribe(engine, pcm) when the complete PCM audio is already held as bytes:
from listenflow import Final, STTEngineBase, transcribe
async def recognize_pcm(engine: STTEngineBase, pcm: bytes) -> list[Final]:
async with engine:
return await transcribe(engine, pcm)
The function consumes engine.stream() and returns only its Final events in utterance order,
preserving their IDs, timestamps, text, and speaker labels. It returns [] when no final text is
emitted and propagates stream errors unchanged. The caller prepares and closes the engine and
supplies PCM matching its input format. Audio file reading and format conversion belong to the
application. See the complete PCM transcription API.
Synchronous callers
For synchronous code, call run_sync(engine.prepare()), read
iterate_sync(engine.stream(iterate_async(pcm_chunks))), and finish with
run_sync(engine.aclose()). These operations use the library's shared background event loop,
keeping the engine on one event loop. run_sync() waits for an awaitable; iterate_sync() turns
an asynchronous iterable into a synchronous iterator; iterate_async() reads a synchronous
iterable in worker threads and yields its values asynchronously.
The helpers can be called from multiple threads and inside a running event loop. If the caller
is interrupted while waiting, run_sync() and iterate_sync() cancel the background operation
and wait for cleanup before propagating the interruption. Use contextlib.closing around the
synchronous iterator to wait for cleanup when stopping early, and try/finally to close the
engine. See the synchronous helper API.
For complete PCM input, call run_sync(transcribe(engine, pcm)) after preparation and before
closure on that same background event loop.
Transcript events
Each event carries an utterance ID, an audio offset in seconds from the start of input, and a kind.
| Event | Kind | Content |
|---|---|---|
UtteranceStart |
utterance_start |
Start of an utterance. |
Partial |
partial |
Provisional text and an optional speaker label. |
Final |
final |
Committed text and an optional speaker label. |
UtteranceEnd |
utterance_end |
End of an utterance. |
TranscriptEvent is the union of these four types. Interpret provisional text according to
engine.capabilities.partial_mode. Append Final events to the committed transcript; committed
text is not revised. An utterance may produce more than one Final event.
See the transcript event API.
Capabilities and health
EngineCapabilities declares accepted languages as a frozenset[str], auto_language,
transport, input format, provisional output mode, finalization modes, timestamp detail, and speaker
labeling. Its values are immutable and readable before preparation and after closure.
HealthSnapshot reports state (EngineState.UNPREPARED, READY, or CLOSED), busy, and
last_progress_monotonic_s. Loading or a failed preparation remains UNPREPARED. Closing starts
the CLOSED state. Only a running model operation makes busy true. The timestamp advances after
PCM processing and utterance decode completion, including silence and empty recognition; reading
events does not change it. Recognition fields include processed PCM chunks, emitted events, active
decode, pending utterances, and an optional BiasApplicationSnapshot recording prompt application.
These synchronous reads use held values without I/O or waiting for a model operation.
See the engine and health API.
Operation lifetime and errors
prepare() loads and trials the model once after success and can be retried after failure.
Model operations wait in arrival order; callers can cancel a waiting operation. Recognition starts
on the first event read. Concurrent streams are rejected with EngineStateError, including while
the first stream is cleaning up.
aclose() cancels active and waiting work, waits for native cleanup, and releases resources once.
Overlapping close calls wait for release. Only the first close reports a release failure, as
InferenceError chained to the original error. Further closes finish normally. A closed engine
cannot be reused, but its capabilities and health remain readable. Use async with for preparation
and closure, and contextlib.aclosing or the iterator's aclose() when stopping a stream early.
Consumer cancellation stays asyncio.CancelledError; engine closure cancels operations with
EngineClosedError. Timeouts are supplied by the caller.
STTError is the base for ConfigurationError (ValueError), EngineStateError (RuntimeError),
EngineClosedError (an EngineStateError), InferenceError (RuntimeError),
EnvironmentUnavailableError (RuntimeError), and ModuleNotInstalledError (ImportError).
Environment problems are reported during preparation. ModuleNotInstalledError provides installation
guidance and chains the original import error when accessing a missing module listed in the core's
module table as listenflow.<name>. The table contains only engine modules published on PyPI.
Import an installed module outside that table explicitly with import listenflow.<name>.
Before that import, accessing an unlisted module as listenflow.<name> raises AttributeError.
If the module is missing, the explicit import raises Python's standard ModuleNotFoundError,
whose name identifies the missing module. Standalone helper validation keeps standard errors.
Recognition without text remains a normal utterance start/end pair.
Build an engine module
Subclass STTEngineBase in a separate distribution that depends on listenflow, and call
super().__init__() in the constructor. The parent owns the public operations and common health:
capabilitiesreturns the staticEngineCapabilitiesdeclaration.async _load()loads resources and runs trial inference; an unsuccessful load cleans up its own resources.async _release()returns resources from a successful load._recognize(pcm_chunks)returns an asynchronous iterator ofTranscriptEventvalues._health_details()returns a mapping of recognition fields; it must omit the three common fields and defaults to an empty mapping.
Hooks finish native work and close owned iterators before returning from cancellation or early
closure. They map native and upstream errors into the corresponding STTError subclass with the
original error as the cause. Call _mark_progress() after a PCM chunk is processed or an utterance
is decoded. When using a pipeline, pass on_progress=self._mark_progress in the engine constructor;
the pipeline forwards notifications and keeps no duplicate timestamp. Return its counters and
decode fields through _health_details().
The module owns model loading, recognition, runtime dependencies, configuration, and supported platforms. Use the shared utterance helpers when they match the engine's recognition interface.
Utterance boundaries
has_voice checks samples against a normalized amplitude threshold. Supply a finite float in
the range 0.0 to 1.0. The helper checks amplitude rather than a learned voice activity model.
UtteranceBoundarySession tracks audio offsets and converts ProvisionalObservation and
FinalObservation records into transcript events. Call observe_pcm() as audio arrives, feed the
recognizer's observations, and call finish_input() followed by finalize_events() at the end of
input. This path fits engines that already stream recognition observations.
UtteranceDecodePipeline fits engines that decode complete utterances. Configure silence, voice
threshold, and preroll with UtteranceBoundarySettings, and supply an async decoding callback:
from collections.abc import Awaitable, Callable, Iterable
from listenflow import (
ClosedUtterance,
EngineInputFormat,
FinalObservation,
ProvisionalObservation,
UtteranceBoundarySettings,
UtteranceDecodePipeline,
)
def make_pipeline(
decode_utterance: Callable[
[ClosedUtterance],
Awaitable[Iterable[ProvisionalObservation | FinalObservation]],
],
) -> UtteranceDecodePipeline:
return UtteranceDecodePipeline(
boundary_settings=UtteranceBoundarySettings(
silence_threshold_s=0.8,
voice_threshold=500 / 32768,
preroll_s=0.3,
),
decode_utterance=decode_utterance,
input_format=EngineInputFormat.PCM_S16LE,
)
The callback performs recognition on ClosedUtterance.pcm. A FinalObservation.source_offset_s
uses seconds from the beginning of the input audio, so convert any recognizer-relative offset
using the closed utterance's start_offset_s. The pipeline continues receiving PCM while decoding,
decodes utterances sequentially, and emits their events in utterance order. Iterate over
pipeline.stream(pcm_chunks) to consume the events. Normal end of input drains pending decoding;
decoder and input errors propagate to the consumer. Failure or early closure cancels an active
decode and waits for cleanup before reuse. When the optional on_progress callback is omitted,
the standalone pipeline records last_progress_monotonic_s after PCM processing and decode
completion. With the callback, the receiver owns the timestamp and the pipeline leaves it None.
Event reading does not advance progress.
See the utterance boundary API.
Offline models
Use OfflineModelDirectorySource to identify a model directory with an absolute path:
from pathlib import Path
from listenflow import OfflineModelDirectorySource
model_source = OfflineModelDirectorySource(str(Path("models/speech").resolve()))
An engine that accepts this value defines the directory layout and validates its contents. The application provisions the files; the core does not download models. See the offline model API.