listenflow 0.17.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- listenflow-0.17.6/.gitignore +13 -0
- listenflow-0.17.6/LICENSE +21 -0
- listenflow-0.17.6/PKG-INFO +275 -0
- listenflow-0.17.6/README.md +254 -0
- listenflow-0.17.6/pyproject.toml +102 -0
- listenflow-0.17.6/src/listenflow/__init__.py +101 -0
- listenflow-0.17.6/src/listenflow/_async.py +23 -0
- listenflow-0.17.6/src/listenflow/_version.py +24 -0
- listenflow-0.17.6/src/listenflow/base.py +317 -0
- listenflow-0.17.6/src/listenflow/engine.py +926 -0
- listenflow-0.17.6/src/listenflow/errors.py +29 -0
- listenflow-0.17.6/src/listenflow/events.py +37 -0
- listenflow-0.17.6/src/listenflow/health.py +36 -0
- listenflow-0.17.6/src/listenflow/sync.py +246 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 minamik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: listenflow
|
|
3
|
+
Version: 0.17.6
|
|
4
|
+
Summary: Engine-neutral streaming speech-to-text contracts and runtime
|
|
5
|
+
Project-URL: Repository, https://github.com/sync-dev-org/listenflow
|
|
6
|
+
Project-URL: Documentation, https://sync-dev-org.github.io/listenflow/
|
|
7
|
+
Author-email: minamik <mia@sync.dev>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: speech-recognition,speech-to-text,stt
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: <3.14,>=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# listenflow
|
|
23
|
+
|
|
24
|
+
Convert a stream of clean, 16 kHz mono PCM audio into asynchronous transcript events. `listenflow`
|
|
25
|
+
provides engine-neutral contracts, shared utterance boundaries, and health snapshots for speech
|
|
26
|
+
recognition engines.
|
|
27
|
+
|
|
28
|
+
The core package has no runtime dependencies and does not include a speech recognition model.
|
|
29
|
+
Engine modules are separate distributions and will be published individually as the needed modules
|
|
30
|
+
become ready. To transcribe audio, supply an engine that implements `STTEngineBase`.
|
|
31
|
+
|
|
32
|
+
The application supplies audio capture, audio cleanup, network transport, a persistent server, and
|
|
33
|
+
process supervision. It also decides readiness and recovery policy, and owns speech synthesis,
|
|
34
|
+
dialogue control, and LLM calls.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
Requires Python 3.12 or 3.13. Licensed under
|
|
39
|
+
[MIT](https://github.com/sync-dev-org/listenflow/blob/main/LICENSE).
|
|
40
|
+
|
|
41
|
+
```console
|
|
42
|
+
pip install listenflow
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Use an engine
|
|
46
|
+
|
|
47
|
+
An engine exposes `capabilities`, loads and trials its model with `prepare()`, accepts an
|
|
48
|
+
`AsyncIterator[bytes]` through `stream()`, and reports progress through `health()`. Use `async with`
|
|
49
|
+
to prepare it on entry and release its resources on exit. Use one engine instance on one event loop.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from collections.abc import AsyncIterator
|
|
53
|
+
|
|
54
|
+
from listenflow import Final, STTEngineBase
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def transcribe(engine: STTEngineBase, pcm_chunks: AsyncIterator[bytes]) -> None:
|
|
58
|
+
async with engine:
|
|
59
|
+
async for event in engine.stream(pcm_chunks):
|
|
60
|
+
if isinstance(event, Final):
|
|
61
|
+
print(event.utterance_id, event.ts_offset_s, event.text)
|
|
62
|
+
|
|
63
|
+
snapshot = engine.health()
|
|
64
|
+
print(snapshot.processed_pcm_chunks, snapshot.emitted_events)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Pass an engine instance and an audio source to `transcribe`. The core installation supplies the
|
|
68
|
+
contract; the engine supplies recognition. For the shared PCM helpers, select
|
|
69
|
+
`EngineInputFormat.PCM_S16LE` for signed 16-bit little-endian samples or `EngineInputFormat.PCM_F32`
|
|
70
|
+
for little-endian float32 samples. Both use 16 kHz mono audio, and each chunk must contain whole
|
|
71
|
+
samples. Match the audio representation to the engine's declared input format.
|
|
72
|
+
|
|
73
|
+
### Synchronous callers
|
|
74
|
+
|
|
75
|
+
Use the core's three helpers to call an engine from synchronous code:
|
|
76
|
+
|
|
77
|
+
- `run_sync()` runs an awaitable on the library's shared background event loop and waits for its result.
|
|
78
|
+
- `iterate_sync()` converts an asynchronous iterable into a synchronous iterator.
|
|
79
|
+
- `iterate_async()` converts a synchronous iterable into an asynchronous iterator, reading the input in worker threads.
|
|
80
|
+
|
|
81
|
+
Operations passed through `run_sync()` and `iterate_sync()` use the same background event loop,
|
|
82
|
+
so the following preparation, recognition, and closure keep the engine on one event loop:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from collections.abc import Iterable
|
|
86
|
+
from contextlib import closing
|
|
87
|
+
|
|
88
|
+
from listenflow import Final, STTEngineBase, iterate_async, iterate_sync, run_sync
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def transcribe_sync(engine: STTEngineBase, pcm_chunks: Iterable[bytes]) -> None:
|
|
92
|
+
try:
|
|
93
|
+
run_sync(engine.prepare())
|
|
94
|
+
with closing(iterate_sync(engine.stream(iterate_async(pcm_chunks)))) as events:
|
|
95
|
+
for event in events:
|
|
96
|
+
if isinstance(event, Final):
|
|
97
|
+
print(event.utterance_id, event.ts_offset_s, event.text)
|
|
98
|
+
finally:
|
|
99
|
+
run_sync(engine.aclose())
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The helpers support callers in multiple threads and inside an already running event loop.
|
|
103
|
+
Calling `run_sync()` or `iterate_sync()` from the shared background event loop thread raises
|
|
104
|
+
`EngineStateError`; use `await` or `async for` there instead.
|
|
105
|
+
Keep all asynchronous operations on a given engine on the same event loop. If a synchronous
|
|
106
|
+
caller is interrupted while waiting in `run_sync()` or `iterate_sync()`, the helper cancels the
|
|
107
|
+
background operation and waits for its cleanup before propagating the interruption. Closing an
|
|
108
|
+
`iterate_sync()` iterator also waits for asynchronous cleanup; `contextlib.closing` ensures this
|
|
109
|
+
happens when the loop stops early or its body raises an exception.
|
|
110
|
+
|
|
111
|
+
See the [synchronous helper API](https://sync-dev-org.github.io/listenflow/api/sync/).
|
|
112
|
+
|
|
113
|
+
## Transcript events
|
|
114
|
+
|
|
115
|
+
Every event has an `utterance_id`, a `ts_offset_s` measured in seconds from the start of the input
|
|
116
|
+
audio, and a `kind`.
|
|
117
|
+
|
|
118
|
+
| Event | Kind | Meaning |
|
|
119
|
+
| --- | --- | --- |
|
|
120
|
+
| `UtteranceStart` | `utterance_start` | An utterance has started. |
|
|
121
|
+
| `Partial` | `partial` | Provisional text, with an optional speaker label. |
|
|
122
|
+
| `Final` | `final` | Committed text, with an optional speaker label. |
|
|
123
|
+
| `UtteranceEnd` | `utterance_end` | The utterance has ended. |
|
|
124
|
+
|
|
125
|
+
Use `Partial` for a provisional display according to the engine's `partial_mode`. Append `Final`
|
|
126
|
+
events to the committed transcript; committed text is not revised. An utterance can contain more
|
|
127
|
+
than one `Final` event. `TranscriptEvent` is the union of the four event types.
|
|
128
|
+
|
|
129
|
+
## Capabilities and health
|
|
130
|
+
|
|
131
|
+
`EngineCapabilities` is an immutable, static declaration that can be read before preparation
|
|
132
|
+
and after closure:
|
|
133
|
+
|
|
134
|
+
| Field | Describes |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `languages` | Accepted language codes, as a `frozenset[str]`. An empty set means the engine has no language argument and declares no languages. |
|
|
137
|
+
| `auto_language` | Whether a language of `None` selects automatic detection. |
|
|
138
|
+
| `transport` | In-process, WebSocket, gRPC, or HTTP per utterance. |
|
|
139
|
+
| `input_format` | The audio representation accepted by the engine. |
|
|
140
|
+
| `partial_mode` | Replace, append, commit-only, or no provisional output. |
|
|
141
|
+
| `finalization` | One or more ways the engine commits text or closes an utterance. |
|
|
142
|
+
| `timestamp_detail` | No timestamps, or segment, word, token, or sample offsets. |
|
|
143
|
+
| `diarization` | No speaker labels, segment labels, word labels, or mutable spans. |
|
|
144
|
+
|
|
145
|
+
`HealthSnapshot` reports held values immediately without I/O or waiting for model operations:
|
|
146
|
+
|
|
147
|
+
| Field | Meaning |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `state` | `EngineState.UNPREPARED`, `READY`, or `CLOSED`. Loading and failed preparation remain `UNPREPARED`. Closing starts the `CLOSED` state. |
|
|
150
|
+
| `busy` | A model operation is in progress, including recognition cleanup. Waiting calls alone do not make it true. |
|
|
151
|
+
| `last_progress_monotonic_s` | Time in `time.monotonic()` seconds of the last processed PCM chunk or completed utterance decode; `None` before any progress. Event delivery does not update it. |
|
|
152
|
+
| `processed_pcm_chunks`, `emitted_events` | Recognition counters. |
|
|
153
|
+
| `decoding_utterance_id`, `pending_decode_utterances` | Current utterance decode and number of waiting utterances. |
|
|
154
|
+
| `bias_application` | Optional `BiasApplicationSnapshot` describing recognition prompt application. |
|
|
155
|
+
|
|
156
|
+
The application interprets these values and decides when an engine is ready or stalled.
|
|
157
|
+
|
|
158
|
+
## Operation lifetime and errors
|
|
159
|
+
|
|
160
|
+
`prepare()` loads the model and performs trial inference. A second successful preparation does
|
|
161
|
+
nothing; a failed preparation can be retried. Environment failures are reported by `prepare()`.
|
|
162
|
+
Model operations wait in arrival order, and a waiting caller can cancel its operation. Recognition
|
|
163
|
+
starts on the first event read. A second stream started during recognition or its cleanup raises
|
|
164
|
+
`EngineStateError` without disturbing the first stream.
|
|
165
|
+
|
|
166
|
+
`aclose()` cancels active and waiting operations, waits for native work to finish, then releases
|
|
167
|
+
resources once. The closed engine cannot be prepared or used for recognition again. Repeated
|
|
168
|
+
`aclose()` calls finish normally; overlapping calls wait for resource release. If release fails,
|
|
169
|
+
only the first call raises `InferenceError` with the original failure as its cause. Capabilities
|
|
170
|
+
and health remain readable after closure.
|
|
171
|
+
|
|
172
|
+
When stopping before the end of a stream, explicitly await the iterator's `aclose()` or use
|
|
173
|
+
`contextlib.aclosing`. Consumer cancellation and early closure deliver cancellation to inference
|
|
174
|
+
and wait for its cleanup before the engine can be reused. Native work may need to run to completion.
|
|
175
|
+
Caller cancellation remains `asyncio.CancelledError`; cancellation by the engine's `aclose()` and
|
|
176
|
+
operations after closure raise `EngineClosedError`. The caller supplies timeouts when needed.
|
|
177
|
+
|
|
178
|
+
All engine errors inherit from `STTError` and also support the corresponding standard handler:
|
|
179
|
+
|
|
180
|
+
| Error | Standard base | Meaning |
|
|
181
|
+
| --- | --- | --- |
|
|
182
|
+
| `ConfigurationError` | `ValueError` | Invalid engine configuration or input, including incomplete PCM samples. |
|
|
183
|
+
| `EngineStateError` | `RuntimeError` | Recognition before preparation or a concurrent stream. |
|
|
184
|
+
| `EngineClosedError` | `EngineStateError` | An operation on a closed engine or cancelled by engine closure. |
|
|
185
|
+
| `InferenceError` | `RuntimeError` | Runtime failure. |
|
|
186
|
+
| `EnvironmentUnavailableError` | `RuntimeError` | Missing native components, libraries, or devices. |
|
|
187
|
+
| `ModuleNotInstalledError` | `ImportError` | Attribute access to a missing module listed in the core's module table; includes installation guidance. |
|
|
188
|
+
|
|
189
|
+
The core's module table contains only engine modules published on PyPI and is currently empty.
|
|
190
|
+
Import an installed module outside that table explicitly with `import listenflow.<name>`.
|
|
191
|
+
Before that import, accessing an unlisted module as `listenflow.<name>` raises `AttributeError`.
|
|
192
|
+
If the module is missing, the explicit import raises Python's standard `ModuleNotFoundError`,
|
|
193
|
+
whose `name` identifies the missing module.
|
|
194
|
+
|
|
195
|
+
Standalone PCM, utterance, model-source, and capability helpers retain their standard validation
|
|
196
|
+
errors. An utterance without recognized text remains a normal start/end event pair.
|
|
197
|
+
|
|
198
|
+
## Build an engine module
|
|
199
|
+
|
|
200
|
+
Subclass `STTEngineBase` in a separate package and call `super().__init__()` in your constructor.
|
|
201
|
+
The parent implements `prepare()`, `stream()`, `aclose()`, `async with`, and common health fields.
|
|
202
|
+
The module owns its recognition runtime, configuration, and supported platforms and implements:
|
|
203
|
+
|
|
204
|
+
| Hook | Responsibility |
|
|
205
|
+
| --- | --- |
|
|
206
|
+
| `capabilities` | Return the static `EngineCapabilities` property. |
|
|
207
|
+
| `async _load()` | Load resources and run trial inference. Clean up resources from an unsuccessful load. |
|
|
208
|
+
| `async _release()` | Return resources from a successful load. |
|
|
209
|
+
| `_recognize(pcm_chunks)` | Return an asynchronous transcript iterator. Finish native work and close owned iterators before returning from cancellation or early closure. |
|
|
210
|
+
| `_health_details()` | Return a mapping of recognition fields only; the default is empty. |
|
|
211
|
+
|
|
212
|
+
Call the parent's `_mark_progress()` after processing a PCM chunk or completing an utterance decode.
|
|
213
|
+
Map native and upstream errors into the appropriate `STTError` subclass and chain the original
|
|
214
|
+
exception as the cause. A hook that cannot interrupt native inference must wait for it to finish.
|
|
215
|
+
|
|
216
|
+
For an engine that decodes complete utterances, use `UtteranceDecodePipeline` to share audio
|
|
217
|
+
segmentation and transcript event handling. Pass `on_progress=self._mark_progress` from the engine's
|
|
218
|
+
constructor to keep the progress timestamp in the parent, and return the pipeline's recognition
|
|
219
|
+
counters and decode fields from `_health_details()`. For an engine with its own streaming
|
|
220
|
+
recognition, use `UtteranceBoundarySession` to translate PCM and recognition observations into events.
|
|
221
|
+
|
|
222
|
+
## Utterance boundaries
|
|
223
|
+
|
|
224
|
+
- `has_voice` checks whether any sample reaches a normalized full-scale amplitude threshold.
|
|
225
|
+
The threshold must be a finite `float` between `0.0` and `1.0`; the helper is amplitude-based.
|
|
226
|
+
- `UtteranceBoundarySettings` configures the silence threshold, voice threshold, and preroll.
|
|
227
|
+
- `UtteranceBoundarySession` tracks audio offsets and utterance IDs. Feed PCM through
|
|
228
|
+
`observe_pcm()` and recognition results through `observe_provisional()` or `observe_final()`.
|
|
229
|
+
Call `finish_input()` when input ends, then drain `finalize_events()`.
|
|
230
|
+
- `UtteranceDecodePipeline` receives PCM while decoding closed utterances in order. Its async
|
|
231
|
+
`decode_utterance` callback receives a `ClosedUtterance` and returns an iterable of
|
|
232
|
+
`ProvisionalObservation` or `FinalObservation` records. `stream()` yields transcript events
|
|
233
|
+
in utterance order and completes outstanding decoding when input ends normally. Failure or early
|
|
234
|
+
closure cancels an active decode and waits for its cleanup before allowing reuse.
|
|
235
|
+
- `on_progress` optionally forwards PCM-processing and decode-completion notifications to an engine.
|
|
236
|
+
With this callback, the pipeline stores no timestamp. Used on its own, the pipeline records
|
|
237
|
+
`last_progress_monotonic_s`. Reading events does not advance progress.
|
|
238
|
+
|
|
239
|
+
See the [utterance API](https://sync-dev-org.github.io/listenflow/api/utterances/) for the session and
|
|
240
|
+
pipeline interfaces.
|
|
241
|
+
|
|
242
|
+
## Offline models
|
|
243
|
+
|
|
244
|
+
`OfflineModelDirectorySource` identifies a model directory with an absolute path:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from pathlib import Path
|
|
248
|
+
|
|
249
|
+
from listenflow import OfflineModelDirectorySource
|
|
250
|
+
|
|
251
|
+
model_source = OfflineModelDirectorySource(str(Path("models/speech").resolve()))
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Pass this value to an engine that accepts an offline model source. The engine defines the directory
|
|
255
|
+
layout and validates its contents. The application provisions the files; the core does not download
|
|
256
|
+
models.
|
|
257
|
+
|
|
258
|
+
## Documentation
|
|
259
|
+
|
|
260
|
+
The [documentation guide](https://sync-dev-org.github.io/listenflow/) links to the API reference for
|
|
261
|
+
engines, synchronous helpers, transcript events, utterance boundaries, and offline models.
|
|
262
|
+
|
|
263
|
+
## Development
|
|
264
|
+
|
|
265
|
+
Run from the repository root:
|
|
266
|
+
|
|
267
|
+
```console
|
|
268
|
+
uv sync
|
|
269
|
+
uv lock --check
|
|
270
|
+
uv run pytest
|
|
271
|
+
uv run ruff check
|
|
272
|
+
uv run ruff format --check
|
|
273
|
+
uv build
|
|
274
|
+
uv run mkdocs build --strict
|
|
275
|
+
```
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# listenflow
|
|
2
|
+
|
|
3
|
+
Convert a stream of clean, 16 kHz mono PCM audio into asynchronous transcript events. `listenflow`
|
|
4
|
+
provides engine-neutral contracts, shared utterance boundaries, and health snapshots for speech
|
|
5
|
+
recognition engines.
|
|
6
|
+
|
|
7
|
+
The core package has no runtime dependencies and does not include a speech recognition model.
|
|
8
|
+
Engine modules are separate distributions and will be published individually as the needed modules
|
|
9
|
+
become ready. To transcribe audio, supply an engine that implements `STTEngineBase`.
|
|
10
|
+
|
|
11
|
+
The application supplies audio capture, audio cleanup, network transport, a persistent server, and
|
|
12
|
+
process supervision. It also decides readiness and recovery policy, and owns speech synthesis,
|
|
13
|
+
dialogue control, and LLM calls.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Requires Python 3.12 or 3.13. Licensed under
|
|
18
|
+
[MIT](https://github.com/sync-dev-org/listenflow/blob/main/LICENSE).
|
|
19
|
+
|
|
20
|
+
```console
|
|
21
|
+
pip install listenflow
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Use an engine
|
|
25
|
+
|
|
26
|
+
An engine exposes `capabilities`, loads and trials its model with `prepare()`, accepts an
|
|
27
|
+
`AsyncIterator[bytes]` through `stream()`, and reports progress through `health()`. Use `async with`
|
|
28
|
+
to prepare it on entry and release its resources on exit. Use one engine instance on one event loop.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from collections.abc import AsyncIterator
|
|
32
|
+
|
|
33
|
+
from listenflow import Final, STTEngineBase
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def transcribe(engine: STTEngineBase, pcm_chunks: AsyncIterator[bytes]) -> None:
|
|
37
|
+
async with engine:
|
|
38
|
+
async for event in engine.stream(pcm_chunks):
|
|
39
|
+
if isinstance(event, Final):
|
|
40
|
+
print(event.utterance_id, event.ts_offset_s, event.text)
|
|
41
|
+
|
|
42
|
+
snapshot = engine.health()
|
|
43
|
+
print(snapshot.processed_pcm_chunks, snapshot.emitted_events)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Pass an engine instance and an audio source to `transcribe`. The core installation supplies the
|
|
47
|
+
contract; the engine supplies recognition. For the shared PCM helpers, select
|
|
48
|
+
`EngineInputFormat.PCM_S16LE` for signed 16-bit little-endian samples or `EngineInputFormat.PCM_F32`
|
|
49
|
+
for little-endian float32 samples. Both use 16 kHz mono audio, and each chunk must contain whole
|
|
50
|
+
samples. Match the audio representation to the engine's declared input format.
|
|
51
|
+
|
|
52
|
+
### Synchronous callers
|
|
53
|
+
|
|
54
|
+
Use the core's three helpers to call an engine from synchronous code:
|
|
55
|
+
|
|
56
|
+
- `run_sync()` runs an awaitable on the library's shared background event loop and waits for its result.
|
|
57
|
+
- `iterate_sync()` converts an asynchronous iterable into a synchronous iterator.
|
|
58
|
+
- `iterate_async()` converts a synchronous iterable into an asynchronous iterator, reading the input in worker threads.
|
|
59
|
+
|
|
60
|
+
Operations passed through `run_sync()` and `iterate_sync()` use the same background event loop,
|
|
61
|
+
so the following preparation, recognition, and closure keep the engine on one event loop:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from collections.abc import Iterable
|
|
65
|
+
from contextlib import closing
|
|
66
|
+
|
|
67
|
+
from listenflow import Final, STTEngineBase, iterate_async, iterate_sync, run_sync
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def transcribe_sync(engine: STTEngineBase, pcm_chunks: Iterable[bytes]) -> None:
|
|
71
|
+
try:
|
|
72
|
+
run_sync(engine.prepare())
|
|
73
|
+
with closing(iterate_sync(engine.stream(iterate_async(pcm_chunks)))) as events:
|
|
74
|
+
for event in events:
|
|
75
|
+
if isinstance(event, Final):
|
|
76
|
+
print(event.utterance_id, event.ts_offset_s, event.text)
|
|
77
|
+
finally:
|
|
78
|
+
run_sync(engine.aclose())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The helpers support callers in multiple threads and inside an already running event loop.
|
|
82
|
+
Calling `run_sync()` or `iterate_sync()` from the shared background event loop thread raises
|
|
83
|
+
`EngineStateError`; use `await` or `async for` there instead.
|
|
84
|
+
Keep all asynchronous operations on a given engine on the same event loop. If a synchronous
|
|
85
|
+
caller is interrupted while waiting in `run_sync()` or `iterate_sync()`, the helper cancels the
|
|
86
|
+
background operation and waits for its cleanup before propagating the interruption. Closing an
|
|
87
|
+
`iterate_sync()` iterator also waits for asynchronous cleanup; `contextlib.closing` ensures this
|
|
88
|
+
happens when the loop stops early or its body raises an exception.
|
|
89
|
+
|
|
90
|
+
See the [synchronous helper API](https://sync-dev-org.github.io/listenflow/api/sync/).
|
|
91
|
+
|
|
92
|
+
## Transcript events
|
|
93
|
+
|
|
94
|
+
Every event has an `utterance_id`, a `ts_offset_s` measured in seconds from the start of the input
|
|
95
|
+
audio, and a `kind`.
|
|
96
|
+
|
|
97
|
+
| Event | Kind | Meaning |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `UtteranceStart` | `utterance_start` | An utterance has started. |
|
|
100
|
+
| `Partial` | `partial` | Provisional text, with an optional speaker label. |
|
|
101
|
+
| `Final` | `final` | Committed text, with an optional speaker label. |
|
|
102
|
+
| `UtteranceEnd` | `utterance_end` | The utterance has ended. |
|
|
103
|
+
|
|
104
|
+
Use `Partial` for a provisional display according to the engine's `partial_mode`. Append `Final`
|
|
105
|
+
events to the committed transcript; committed text is not revised. An utterance can contain more
|
|
106
|
+
than one `Final` event. `TranscriptEvent` is the union of the four event types.
|
|
107
|
+
|
|
108
|
+
## Capabilities and health
|
|
109
|
+
|
|
110
|
+
`EngineCapabilities` is an immutable, static declaration that can be read before preparation
|
|
111
|
+
and after closure:
|
|
112
|
+
|
|
113
|
+
| Field | Describes |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `languages` | Accepted language codes, as a `frozenset[str]`. An empty set means the engine has no language argument and declares no languages. |
|
|
116
|
+
| `auto_language` | Whether a language of `None` selects automatic detection. |
|
|
117
|
+
| `transport` | In-process, WebSocket, gRPC, or HTTP per utterance. |
|
|
118
|
+
| `input_format` | The audio representation accepted by the engine. |
|
|
119
|
+
| `partial_mode` | Replace, append, commit-only, or no provisional output. |
|
|
120
|
+
| `finalization` | One or more ways the engine commits text or closes an utterance. |
|
|
121
|
+
| `timestamp_detail` | No timestamps, or segment, word, token, or sample offsets. |
|
|
122
|
+
| `diarization` | No speaker labels, segment labels, word labels, or mutable spans. |
|
|
123
|
+
|
|
124
|
+
`HealthSnapshot` reports held values immediately without I/O or waiting for model operations:
|
|
125
|
+
|
|
126
|
+
| Field | Meaning |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `state` | `EngineState.UNPREPARED`, `READY`, or `CLOSED`. Loading and failed preparation remain `UNPREPARED`. Closing starts the `CLOSED` state. |
|
|
129
|
+
| `busy` | A model operation is in progress, including recognition cleanup. Waiting calls alone do not make it true. |
|
|
130
|
+
| `last_progress_monotonic_s` | Time in `time.monotonic()` seconds of the last processed PCM chunk or completed utterance decode; `None` before any progress. Event delivery does not update it. |
|
|
131
|
+
| `processed_pcm_chunks`, `emitted_events` | Recognition counters. |
|
|
132
|
+
| `decoding_utterance_id`, `pending_decode_utterances` | Current utterance decode and number of waiting utterances. |
|
|
133
|
+
| `bias_application` | Optional `BiasApplicationSnapshot` describing recognition prompt application. |
|
|
134
|
+
|
|
135
|
+
The application interprets these values and decides when an engine is ready or stalled.
|
|
136
|
+
|
|
137
|
+
## Operation lifetime and errors
|
|
138
|
+
|
|
139
|
+
`prepare()` loads the model and performs trial inference. A second successful preparation does
|
|
140
|
+
nothing; a failed preparation can be retried. Environment failures are reported by `prepare()`.
|
|
141
|
+
Model operations wait in arrival order, and a waiting caller can cancel its operation. Recognition
|
|
142
|
+
starts on the first event read. A second stream started during recognition or its cleanup raises
|
|
143
|
+
`EngineStateError` without disturbing the first stream.
|
|
144
|
+
|
|
145
|
+
`aclose()` cancels active and waiting operations, waits for native work to finish, then releases
|
|
146
|
+
resources once. The closed engine cannot be prepared or used for recognition again. Repeated
|
|
147
|
+
`aclose()` calls finish normally; overlapping calls wait for resource release. If release fails,
|
|
148
|
+
only the first call raises `InferenceError` with the original failure as its cause. Capabilities
|
|
149
|
+
and health remain readable after closure.
|
|
150
|
+
|
|
151
|
+
When stopping before the end of a stream, explicitly await the iterator's `aclose()` or use
|
|
152
|
+
`contextlib.aclosing`. Consumer cancellation and early closure deliver cancellation to inference
|
|
153
|
+
and wait for its cleanup before the engine can be reused. Native work may need to run to completion.
|
|
154
|
+
Caller cancellation remains `asyncio.CancelledError`; cancellation by the engine's `aclose()` and
|
|
155
|
+
operations after closure raise `EngineClosedError`. The caller supplies timeouts when needed.
|
|
156
|
+
|
|
157
|
+
All engine errors inherit from `STTError` and also support the corresponding standard handler:
|
|
158
|
+
|
|
159
|
+
| Error | Standard base | Meaning |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| `ConfigurationError` | `ValueError` | Invalid engine configuration or input, including incomplete PCM samples. |
|
|
162
|
+
| `EngineStateError` | `RuntimeError` | Recognition before preparation or a concurrent stream. |
|
|
163
|
+
| `EngineClosedError` | `EngineStateError` | An operation on a closed engine or cancelled by engine closure. |
|
|
164
|
+
| `InferenceError` | `RuntimeError` | Runtime failure. |
|
|
165
|
+
| `EnvironmentUnavailableError` | `RuntimeError` | Missing native components, libraries, or devices. |
|
|
166
|
+
| `ModuleNotInstalledError` | `ImportError` | Attribute access to a missing module listed in the core's module table; includes installation guidance. |
|
|
167
|
+
|
|
168
|
+
The core's module table contains only engine modules published on PyPI and is currently empty.
|
|
169
|
+
Import an installed module outside that table explicitly with `import listenflow.<name>`.
|
|
170
|
+
Before that import, accessing an unlisted module as `listenflow.<name>` raises `AttributeError`.
|
|
171
|
+
If the module is missing, the explicit import raises Python's standard `ModuleNotFoundError`,
|
|
172
|
+
whose `name` identifies the missing module.
|
|
173
|
+
|
|
174
|
+
Standalone PCM, utterance, model-source, and capability helpers retain their standard validation
|
|
175
|
+
errors. An utterance without recognized text remains a normal start/end event pair.
|
|
176
|
+
|
|
177
|
+
## Build an engine module
|
|
178
|
+
|
|
179
|
+
Subclass `STTEngineBase` in a separate package and call `super().__init__()` in your constructor.
|
|
180
|
+
The parent implements `prepare()`, `stream()`, `aclose()`, `async with`, and common health fields.
|
|
181
|
+
The module owns its recognition runtime, configuration, and supported platforms and implements:
|
|
182
|
+
|
|
183
|
+
| Hook | Responsibility |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| `capabilities` | Return the static `EngineCapabilities` property. |
|
|
186
|
+
| `async _load()` | Load resources and run trial inference. Clean up resources from an unsuccessful load. |
|
|
187
|
+
| `async _release()` | Return resources from a successful load. |
|
|
188
|
+
| `_recognize(pcm_chunks)` | Return an asynchronous transcript iterator. Finish native work and close owned iterators before returning from cancellation or early closure. |
|
|
189
|
+
| `_health_details()` | Return a mapping of recognition fields only; the default is empty. |
|
|
190
|
+
|
|
191
|
+
Call the parent's `_mark_progress()` after processing a PCM chunk or completing an utterance decode.
|
|
192
|
+
Map native and upstream errors into the appropriate `STTError` subclass and chain the original
|
|
193
|
+
exception as the cause. A hook that cannot interrupt native inference must wait for it to finish.
|
|
194
|
+
|
|
195
|
+
For an engine that decodes complete utterances, use `UtteranceDecodePipeline` to share audio
|
|
196
|
+
segmentation and transcript event handling. Pass `on_progress=self._mark_progress` from the engine's
|
|
197
|
+
constructor to keep the progress timestamp in the parent, and return the pipeline's recognition
|
|
198
|
+
counters and decode fields from `_health_details()`. For an engine with its own streaming
|
|
199
|
+
recognition, use `UtteranceBoundarySession` to translate PCM and recognition observations into events.
|
|
200
|
+
|
|
201
|
+
## Utterance boundaries
|
|
202
|
+
|
|
203
|
+
- `has_voice` checks whether any sample reaches a normalized full-scale amplitude threshold.
|
|
204
|
+
The threshold must be a finite `float` between `0.0` and `1.0`; the helper is amplitude-based.
|
|
205
|
+
- `UtteranceBoundarySettings` configures the silence threshold, voice threshold, and preroll.
|
|
206
|
+
- `UtteranceBoundarySession` tracks audio offsets and utterance IDs. Feed PCM through
|
|
207
|
+
`observe_pcm()` and recognition results through `observe_provisional()` or `observe_final()`.
|
|
208
|
+
Call `finish_input()` when input ends, then drain `finalize_events()`.
|
|
209
|
+
- `UtteranceDecodePipeline` receives PCM while decoding closed utterances in order. Its async
|
|
210
|
+
`decode_utterance` callback receives a `ClosedUtterance` and returns an iterable of
|
|
211
|
+
`ProvisionalObservation` or `FinalObservation` records. `stream()` yields transcript events
|
|
212
|
+
in utterance order and completes outstanding decoding when input ends normally. Failure or early
|
|
213
|
+
closure cancels an active decode and waits for its cleanup before allowing reuse.
|
|
214
|
+
- `on_progress` optionally forwards PCM-processing and decode-completion notifications to an engine.
|
|
215
|
+
With this callback, the pipeline stores no timestamp. Used on its own, the pipeline records
|
|
216
|
+
`last_progress_monotonic_s`. Reading events does not advance progress.
|
|
217
|
+
|
|
218
|
+
See the [utterance API](https://sync-dev-org.github.io/listenflow/api/utterances/) for the session and
|
|
219
|
+
pipeline interfaces.
|
|
220
|
+
|
|
221
|
+
## Offline models
|
|
222
|
+
|
|
223
|
+
`OfflineModelDirectorySource` identifies a model directory with an absolute path:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from pathlib import Path
|
|
227
|
+
|
|
228
|
+
from listenflow import OfflineModelDirectorySource
|
|
229
|
+
|
|
230
|
+
model_source = OfflineModelDirectorySource(str(Path("models/speech").resolve()))
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Pass this value to an engine that accepts an offline model source. The engine defines the directory
|
|
234
|
+
layout and validates its contents. The application provisions the files; the core does not download
|
|
235
|
+
models.
|
|
236
|
+
|
|
237
|
+
## Documentation
|
|
238
|
+
|
|
239
|
+
The [documentation guide](https://sync-dev-org.github.io/listenflow/) links to the API reference for
|
|
240
|
+
engines, synchronous helpers, transcript events, utterance boundaries, and offline models.
|
|
241
|
+
|
|
242
|
+
## Development
|
|
243
|
+
|
|
244
|
+
Run from the repository root:
|
|
245
|
+
|
|
246
|
+
```console
|
|
247
|
+
uv sync
|
|
248
|
+
uv lock --check
|
|
249
|
+
uv run pytest
|
|
250
|
+
uv run ruff check
|
|
251
|
+
uv run ruff format --check
|
|
252
|
+
uv build
|
|
253
|
+
uv run mkdocs build --strict
|
|
254
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "listenflow"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Engine-neutral streaming speech-to-text contracts and runtime"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "minamik", email = "mia@sync.dev" },
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.12,<3.14"
|
|
11
|
+
keywords = ["stt", "speech-to-text", "speech-recognition"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://github.com/sync-dev-org/listenflow"
|
|
26
|
+
Documentation = "https://sync-dev-org.github.io/listenflow/"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"mkdocs-material>=9.7.7",
|
|
35
|
+
"mkdocstrings[python]>=1.0.6",
|
|
36
|
+
"noren==0.2.0",
|
|
37
|
+
"pytest>=8.0",
|
|
38
|
+
"pytest-asyncio>=0.24",
|
|
39
|
+
"ruff>=0.8",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.version]
|
|
43
|
+
source = "vcs"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.version.raw-options]
|
|
46
|
+
root = "."
|
|
47
|
+
relative_to = "pyproject.toml"
|
|
48
|
+
tag_regex = "^v(?P<version>[^/]+)$"
|
|
49
|
+
git_describe_command = [
|
|
50
|
+
"git",
|
|
51
|
+
"describe",
|
|
52
|
+
"--dirty",
|
|
53
|
+
"--tags",
|
|
54
|
+
"--long",
|
|
55
|
+
"--match",
|
|
56
|
+
"v*",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.hatch.build.hooks.vcs]
|
|
60
|
+
version-file = "src/listenflow/_version.py"
|
|
61
|
+
|
|
62
|
+
[tool.hatch.build]
|
|
63
|
+
dev-mode-dirs = ["src"]
|
|
64
|
+
|
|
65
|
+
[tool.hatch.build.targets.wheel]
|
|
66
|
+
packages = ["src/listenflow"]
|
|
67
|
+
|
|
68
|
+
[tool.hatch.build.targets.sdist]
|
|
69
|
+
only-include = ["src/listenflow", "README.md", "LICENSE", "pyproject.toml"]
|
|
70
|
+
|
|
71
|
+
[tool.pytest.ini_options]
|
|
72
|
+
asyncio_mode = "auto"
|
|
73
|
+
testpaths = ["tests"]
|
|
74
|
+
addopts = [
|
|
75
|
+
"--strict-markers",
|
|
76
|
+
"-m",
|
|
77
|
+
"not performance",
|
|
78
|
+
]
|
|
79
|
+
markers = [
|
|
80
|
+
"cpu: qualified CPU payload and model tests",
|
|
81
|
+
"gpu: requires a real GPU and explicitly provisioned runtime assets",
|
|
82
|
+
"performance: opt-in performance measurement excluded from the default test run",
|
|
83
|
+
"req(id): parent requirement ID for an externally visible behavior test",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
[tool.ruff]
|
|
87
|
+
line-length = 100
|
|
88
|
+
src = ["src", "tests"]
|
|
89
|
+
exclude = [
|
|
90
|
+
".git",
|
|
91
|
+
".nf",
|
|
92
|
+
".pytest_cache",
|
|
93
|
+
".ruff_cache",
|
|
94
|
+
".venv",
|
|
95
|
+
"__pycache__",
|
|
96
|
+
"*.egg-info",
|
|
97
|
+
"build",
|
|
98
|
+
"build-*",
|
|
99
|
+
"datasets",
|
|
100
|
+
"dist",
|
|
101
|
+
"**/_version.py",
|
|
102
|
+
]
|