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.
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ src/listenflow/_version.py
10
+ .nf/tmp/
11
+ .nf/logs/
12
+ /datasets/
13
+ /site/
@@ -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
+ ]