RealTimeSTT 1.0.4__tar.gz → 1.1.0__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.
Files changed (84) hide show
  1. {realtimestt-1.0.4 → realtimestt-1.1.0}/MANIFEST.in +3 -0
  2. {realtimestt-1.0.4 → realtimestt-1.1.0}/PKG-INFO +13 -4
  3. {realtimestt-1.0.4 → realtimestt-1.1.0}/README.md +2 -1
  4. {realtimestt-1.0.4 → realtimestt-1.1.0}/RELEASE_NOTES.md +51 -2
  5. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/__init__.py +10 -0
  6. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/audio_recorder.py +106 -0
  7. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/initialization.py +295 -1
  8. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/lifecycle.py +50 -0
  9. realtimestt-1.1.0/RealtimeSTT/core/preview_transcription.py +322 -0
  10. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/realtime.py +450 -26
  11. realtimestt-1.1.0/RealtimeSTT/core/realtime_merge.py +821 -0
  12. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/recorder_config.py +44 -0
  13. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/recording.py +83 -2
  14. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/recording_buffers.py +48 -0
  15. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/runtime.py +1 -1
  16. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/shutdown.py +48 -0
  17. realtimestt-1.1.0/RealtimeSTT/core/tail_transcription.py +593 -0
  18. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/transcription.py +195 -11
  19. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/transcription_api.py +35 -14
  20. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/install_sherpa_models.py +1 -1
  21. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/factory.py +5 -0
  22. realtimestt-1.1.0/RealtimeSTT/transcription_engines/transcribe_cpp_engine.py +521 -0
  23. realtimestt-1.1.0/RealtimeSTT_server/PRODUCTION_SERVER.md +274 -0
  24. realtimestt-1.1.0/RealtimeSTT_server/production_server.py +5241 -0
  25. {realtimestt-1.0.4 → realtimestt-1.1.0}/example_fastapi_server/server.py +293 -15
  26. {realtimestt-1.0.4 → realtimestt-1.1.0}/realtimestt.egg-info/PKG-INFO +13 -4
  27. {realtimestt-1.0.4 → realtimestt-1.1.0}/realtimestt.egg-info/SOURCES.txt +7 -1
  28. {realtimestt-1.0.4 → realtimestt-1.1.0}/realtimestt.egg-info/requires.txt +10 -0
  29. {realtimestt-1.0.4 → realtimestt-1.1.0}/setup.py +9 -3
  30. realtimestt-1.1.0/tests/test_preview_transcription.py +339 -0
  31. realtimestt-1.1.0/tests/test_tail_transcription.py +456 -0
  32. realtimestt-1.0.4/RealtimeSTT_server/PRODUCTION_SERVER.md +0 -161
  33. realtimestt-1.0.4/RealtimeSTT_server/production_server.py +0 -2734
  34. {realtimestt-1.0.4 → realtimestt-1.1.0}/LICENSE +0 -0
  35. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/assets/__init__.py +0 -0
  36. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/assets/warmup_audio.wav +0 -0
  37. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/audio_input.py +0 -0
  38. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/audio_recorder_client.py +0 -0
  39. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/__init__.py +0 -0
  40. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/audio_input_worker.py +0 -0
  41. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/manual_audio_input.py +0 -0
  42. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/preroll.py +0 -0
  43. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/realtime_boundary_detector.py +0 -0
  44. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/realtime_callbacks.py +0 -0
  45. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/realtime_text_stabilizer.py +0 -0
  46. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/safepipe.py +0 -0
  47. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/silero_vad.py +0 -0
  48. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/state.py +0 -0
  49. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/text_formatting.py +0 -0
  50. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/voice_activity.py +0 -0
  51. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/core/wakeword.py +0 -0
  52. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/engines/__init__.py +0 -0
  53. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/engines/base_engine.py +0 -0
  54. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/install_kroko.py +0 -0
  55. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/model_manifests.py +0 -0
  56. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/__init__.py +0 -0
  57. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/_model_utils.py +0 -0
  58. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/base.py +0 -0
  59. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/cohere_transcribe_engine.py +0 -0
  60. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/faster_whisper_engine.py +0 -0
  61. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/funasr_engine.py +0 -0
  62. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/granite_speech_engine.py +0 -0
  63. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/hf_transformers_engines.py +0 -0
  64. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/kroko_onnx_engine.py +0 -0
  65. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/moonshine_engine.py +0 -0
  66. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/nemotron_engine.py +0 -0
  67. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/omnilingual_asr_engine.py +0 -0
  68. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/openai_api_engine.py +0 -0
  69. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/openai_whisper_engine.py +0 -0
  70. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/parakeet_engine.py +0 -0
  71. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/qwen3_asr_engine.py +0 -0
  72. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/sherpa_onnx_engine.py +0 -0
  73. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT/transcription_engines/whisper_cpp_engine.py +0 -0
  74. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT_server/__init__.py +0 -0
  75. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT_server/install_packages.py +0 -0
  76. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT_server/stt_cli_client.py +0 -0
  77. {realtimestt-1.0.4 → realtimestt-1.1.0}/RealtimeSTT_server/stt_server.py +0 -0
  78. {realtimestt-1.0.4 → realtimestt-1.1.0}/example_fastapi_server/__init__.py +0 -0
  79. {realtimestt-1.0.4 → realtimestt-1.1.0}/example_fastapi_server/protocol.py +0 -0
  80. {realtimestt-1.0.4 → realtimestt-1.1.0}/realtimestt.egg-info/dependency_links.txt +0 -0
  81. {realtimestt-1.0.4 → realtimestt-1.1.0}/realtimestt.egg-info/entry_points.txt +0 -0
  82. {realtimestt-1.0.4 → realtimestt-1.1.0}/realtimestt.egg-info/top_level.txt +0 -0
  83. {realtimestt-1.0.4 → realtimestt-1.1.0}/requirements.txt +0 -0
  84. {realtimestt-1.0.4 → realtimestt-1.1.0}/setup.cfg +0 -0
@@ -9,7 +9,10 @@ prune roadmap
9
9
  prune test-model-cache
10
10
  prune test-results
11
11
  prune docs_private
12
+ prune docs/handoffs
12
13
  prune tests_private
13
14
  global-exclude *Kopie*.py
14
15
  global-exclude *.bak
15
16
  global-exclude *.tmp
17
+ global-exclude *.log
18
+ global-exclude *.jsonl
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: realtimestt
3
- Version: 1.0.4
3
+ Version: 1.1.0
4
4
  Summary: A fast Voice Activity Detection and Transcription System
5
- Home-page: https://github.com/KoljaB/RealTimeSTT
5
+ Home-page: https://github.com/KoljaB/RealtimeSTT
6
6
  Author: Kolja Beigel
7
7
  Author-email: kolja.beigel@web.de
8
8
  License: MIT
9
9
  Keywords: real-time,audio,transcription,speech-to-text,voice-activity-detection,VAD,real-time-transcription,ambient-noise-detection,microphone-input,faster_whisper,speech-recognition,voice-assistants,audio-processing,buffered-transcription,pyaudio,ambient-noise-level,voice-deactivity
10
- Requires-Python: >=3.11
10
+ Requires-Python: >=3.11,<3.13
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  Requires-Dist: PyAudio==0.2.14
@@ -27,6 +27,12 @@ Provides-Extra: whisper-cpp
27
27
  Requires-Dist: pywhispercpp; extra == "whisper-cpp"
28
28
  Provides-Extra: whispercpp
29
29
  Requires-Dist: pywhispercpp; extra == "whispercpp"
30
+ Provides-Extra: transcribe-cpp
31
+ Requires-Dist: transcribe-cpp==0.2.1; extra == "transcribe-cpp"
32
+ Provides-Extra: transcribecpp
33
+ Requires-Dist: transcribe-cpp==0.2.1; extra == "transcribecpp"
34
+ Provides-Extra: parakeet-transcribe-cpp
35
+ Requires-Dist: transcribe-cpp==0.2.1; extra == "parakeet-transcribe-cpp"
30
36
  Provides-Extra: openai-whisper
31
37
  Requires-Dist: openai-whisper; extra == "openai-whisper"
32
38
  Provides-Extra: sherpa-onnx
@@ -120,6 +126,7 @@ Requires-Dist: silero-vad[onnx-cpu]>=6.2.1; python_version >= "3.8" and extra ==
120
126
  Provides-Extra: all
121
127
  Requires-Dist: faster-whisper==1.2.1; extra == "all"
122
128
  Requires-Dist: pywhispercpp; extra == "all"
129
+ Requires-Dist: transcribe-cpp==0.2.1; extra == "all"
123
130
  Requires-Dist: openai-whisper; extra == "all"
124
131
  Requires-Dist: sherpa-onnx==1.13.4; extra == "all"
125
132
  Requires-Dist: fastapi<1,>=0.115; extra == "all"
@@ -179,6 +186,7 @@ Available extras include:
179
186
 
180
187
  - faster-whisper: default CTranslate2 Whisper backend
181
188
  - whisper-cpp: whisper.cpp backend through pywhispercpp
189
+ - transcribe-cpp: first-party transcribe.cpp Python binding; add the matching CUDA provider separately
182
190
  - openai-whisper: original OpenAI Whisper Python backend
183
191
  - sherpa-onnx: sherpa-onnx CPU backends
184
192
  - server/production-server: versioned FastAPI HTTP and WebSocket ASR server
@@ -319,7 +327,8 @@ See the [Kroko-ONNX engine guide](docs/engines/kroko-onnx.md),
319
327
 
320
328
  ## Install
321
329
 
322
- Use Python 3.11 or newer for the current pinned dependency set.
330
+ The current CI matrix covers Python 3.11 and 3.12. Python 3.13 and newer are
331
+ not release targets until dependency and CI gates are available.
323
332
 
324
333
  ```bash
325
334
  pip install "RealtimeSTT[faster-whisper]"
@@ -72,7 +72,8 @@ See the [Kroko-ONNX engine guide](docs/engines/kroko-onnx.md),
72
72
 
73
73
  ## Install
74
74
 
75
- Use Python 3.11 or newer for the current pinned dependency set.
75
+ The current CI matrix covers Python 3.11 and 3.12. Python 3.13 and newer are
76
+ not release targets until dependency and CI gates are available.
76
77
 
77
78
  ```bash
78
79
  pip install "RealtimeSTT[faster-whisper]"
@@ -1,5 +1,54 @@
1
1
  # Release Notes
2
2
 
3
+ ## 1.1.0 - 2026-08-27
4
+
5
+ This stable release promotes the externally validated `1.1.0rc1` candidate.
6
+ See [the release-readiness checklist](docs/release-1.1.0-readiness.md) for the
7
+ required evidence gates and supported platform boundaries.
8
+
9
+ ### Added
10
+
11
+ - Added correlated Preview `resume` handling for the versioned production
12
+ WebSocket API, including an ordered acknowledgement barrier and explicit
13
+ candidate/audio provenance fields.
14
+ - Added an opt-in authenticated late full-turn correction operation for
15
+ Preview-only deployments, bounded by a configurable maximum audio duration.
16
+ - Added recorder Preview/tail transcription support with package-level result
17
+ types and the documented dual-realtime merge contracts.
18
+
19
+ ### Changed
20
+
21
+ - Preview-only production turns retain the complete logical-turn audio for model
22
+ input; live text remains diagnostic and is never substituted for Preview or
23
+ Final output.
24
+ - Empty Preview results use one bounded silence-padding retry, while Final
25
+ remains authoritative whenever Final ASR is enabled.
26
+ - Preview admission is now bounded and coalesced: one native dispatch lane
27
+ retains only the latest pending snapshot while an older inference is active.
28
+ - Unexpected live-lane exits now produce structured, observable degraded events
29
+ while preserving the authoritative-final fallback path.
30
+ - Raw-PCM responses propagate provider-reported language and finite language
31
+ probability metadata when the selected provider supplies it.
32
+ - Realtime punctuation splitting commits the frame boundary atomically, and the
33
+ local Preview worker rejects new speculative requests once its bounded queue
34
+ is full.
35
+ - Release packaging checks now validate the requested version, inspect archive
36
+ names/content for private material, and install both wheel and sdist from
37
+ outside the source checkout.
38
+
39
+ ### Release boundaries
40
+
41
+ - The documented Nemotron-live/Parakeet-final profile targets Linux x86-64 with
42
+ the pinned sherpa-onnx runtime. Native Windows remains a development target
43
+ for this pair; use another authoritative final engine there unless the
44
+ cumulative-recovery behavior is independently validated.
45
+ - Model weights remain external artifacts. Users must review and accept the
46
+ exact model/runtime licenses before downloading or redistributing them.
47
+ - The final source tree must pass supported CI, Linux real-model acceptance,
48
+ and isolated wheel and sdist installation before stable publication.
49
+ - `1.1.0` supersedes the `1.1.0rc1` prerelease; stable installations should pin
50
+ `RealtimeSTT==1.1.0` when an exact version is required.
51
+
3
52
  ## 1.0.4 - 2026-08-21
4
53
 
5
54
  ### Fixed
@@ -86,8 +135,8 @@
86
135
  streaming WebSocket APIs. Non-loopback binds require bearer authentication
87
136
  and TLS. The server includes bounded queues and sessions, backpressure,
88
137
  limits, structured errors, warmup, and graceful resource cleanup.
89
- - Added a reproducible same-host A/B benchmark harness for the AgentTalk/raw
90
- PCM16 contract and release CI for unit, distribution, and isolated wheel
138
+ - Added a reproducible same-host A/B benchmark harness for the raw PCM16 client
139
+ contract and release CI for unit, distribution, and isolated wheel
91
140
  installation checks.
92
141
 
93
142
  ### Changed
@@ -16,6 +16,8 @@ __all__ = [
16
16
  "TranscriptionEngineError",
17
17
  "TranscriptionInfo",
18
18
  "TranscriptionResult",
19
+ "PreviewTranscriptionResult",
20
+ "RealtimeTranscriptionMergeResult",
19
21
  "UnsupportedTranscriptionEngineError",
20
22
  ]
21
23
 
@@ -65,6 +67,14 @@ def __getattr__(name):
65
67
  from .engines import TranscriptionResult
66
68
 
67
69
  return TranscriptionResult
70
+ if name == "PreviewTranscriptionResult":
71
+ from .core.preview_transcription import PreviewTranscriptionResult
72
+
73
+ return PreviewTranscriptionResult
74
+ if name == "RealtimeTranscriptionMergeResult":
75
+ from .core.realtime_merge import RealtimeTranscriptionMergeResult
76
+
77
+ return RealtimeTranscriptionMergeResult
68
78
  if name == "UnsupportedTranscriptionEngineError":
69
79
  from .engines import UnsupportedTranscriptionEngineError
70
80
 
@@ -32,6 +32,10 @@ from .core.recording_buffers import (
32
32
  flush_buffered_audio as flush_recorder_buffered_audio,
33
33
  has_pending_recordings as recorder_has_pending_recordings,
34
34
  )
35
+ from .core.tail_transcription import (
36
+ MIN_LIVE_WORDS_FOR_FUZZY_REPAIR,
37
+ PREVIEW_TRANSCRIPTION_TAIL_SECONDS,
38
+ )
35
39
  from .core.shutdown import shutdown_recorder
36
40
  from .core.text_formatting import format_number
37
41
  from .core.transcription_api import (
@@ -206,6 +210,29 @@ class AudioToTextRecorder:
206
210
  deactivity_silence_confirmation_duration: float = (
207
211
  DEACTIVITY_SILENCE_CONFIRMATION_DURATION
208
212
  ),
213
+ # Speculative Preview transcription parameters
214
+ enable_preview_transcription: bool = False,
215
+ preview_model_type: Optional[str] = None,
216
+ preview_transcription_engine: Optional[str] = None,
217
+ preview_transcription_engine_options: Optional[dict] = None,
218
+ preview_transcription_tail_seconds: float = (
219
+ PREVIEW_TRANSCRIPTION_TAIL_SECONDS
220
+ ),
221
+ on_preview_transcription_finished=None,
222
+ preview_transcription_executor: Optional[Callable] = None,
223
+ preview_transcription_min_live_words_for_fuzzy_repair: int = (
224
+ MIN_LIVE_WORDS_FOR_FUZZY_REPAIR
225
+ ),
226
+ # Optional second, lower-latency realtime transcription lane
227
+ ultrafast_realtime_model_type: Optional[str] = None,
228
+ ultrafast_realtime_transcription_engine: Optional[str] = None,
229
+ ultrafast_realtime_transcription_engine_options: Optional[
230
+ dict
231
+ ] = None,
232
+ on_ultrafast_transcription_update=None,
233
+ on_merged_realtime_transcription_update=None,
234
+ on_realtime_transcription_merge_update=None,
235
+ ultrafast_realtime_max_tail_words: int = 5,
209
236
  ):
210
237
  """
211
238
  Initializes an audio recorder and transcription
@@ -315,6 +342,54 @@ class AudioToTextRecorder:
315
342
  recordings. Use "sentence" for the supported `. ? !` mode.
316
343
  - realtime_batch_size (int, default=16): Batch size for the real-time
317
344
  transcription model.
345
+ - ultrafast_realtime_model_type (str, default=None): Optional second
346
+ streaming realtime model. Its text may only append a bounded,
347
+ safely anchored suffix to the authoritative realtime model text.
348
+ - ultrafast_realtime_transcription_engine (str, default=None): Backend
349
+ for the ultrafast lane. If omitted, the realtime backend is used.
350
+ The backend must support streaming sessions.
351
+ - ultrafast_realtime_transcription_engine_options (dict,
352
+ default=None): Backend-specific ultrafast options. If omitted,
353
+ realtime engine options are reused.
354
+ - on_ultrafast_transcription_update (callable, default=None): Receives
355
+ the raw ultrafast text independently of the central callbacks.
356
+ - on_merged_realtime_transcription_update (callable, default=None):
357
+ Receives the complete sticky merged text only when it changes.
358
+ - on_realtime_transcription_merge_update (callable, default=None):
359
+ Receives every structured RealtimeTranscriptionMergeResult,
360
+ including held/no-anchor transitions.
361
+ - ultrafast_realtime_max_tail_words (int, default=5): Maximum number
362
+ of words the ultrafast model may append after a safe anchor. Lower
363
+ this value to reduce the speculative error radius.
364
+
365
+ Speculative Preview transcription:
366
+ - enable_preview_transcription (bool, default=False): Runs an
367
+ independent low-latency Preview transcription at confirmed VAD
368
+ silence. Preview is intended for speculative consumers such as an
369
+ early LLM response and is not authoritative Final ASR.
370
+ - preview_model_type (str, default=None): Model used for Preview. If
371
+ omitted while Preview is enabled, the loaded main model is reused
372
+ when the Preview engine and options also match; otherwise an
373
+ independent Preview model is initialized.
374
+ - preview_transcription_engine (str, default=None): Backend used for
375
+ Preview. If omitted, the main transcription backend is used.
376
+ - preview_transcription_engine_options (dict, default=None): Optional
377
+ backend-specific Preview options. If omitted, main-engine options
378
+ are reused and allow the loaded main model to be shared.
379
+ - preview_transcription_tail_seconds (float, default=3.0): Amount of
380
+ recent active speech sent to Preview ASR.
381
+ - preview_transcription_min_live_words_for_fuzzy_repair (int,
382
+ default=3): Minimum Live words required before constrained fuzzy
383
+ repair is allowed. Exact anchors remain available for shorter
384
+ Live text; values below the 3-word anchor length cannot lower the
385
+ matcher further.
386
+ - on_preview_transcription_finished (callable, default=None): Callback
387
+ receiving a PreviewTranscriptionResult. The result includes the
388
+ speculative text, Live boundary snapshot, tail text, alignment
389
+ status, and recording id.
390
+ - preview_transcription_executor (callable, default=None): Optional
391
+ external Preview ASR executor. It is called only with the bounded
392
+ Preview tail; it never receives or triggers the full Final audio.
318
393
 
319
394
  Voice activity and turn detection:
320
395
  - silero_sensitivity (float, default=SILERO_SENSITIVITY): Sensitivity
@@ -623,6 +698,37 @@ class AudioToTextRecorder:
623
698
  deactivity_silence_confirmation_duration=(
624
699
  deactivity_silence_confirmation_duration
625
700
  ),
701
+ enable_preview_transcription=enable_preview_transcription,
702
+ preview_model_type=preview_model_type,
703
+ preview_transcription_engine=preview_transcription_engine,
704
+ preview_transcription_engine_options=(
705
+ preview_transcription_engine_options
706
+ ),
707
+ preview_transcription_tail_seconds=preview_transcription_tail_seconds,
708
+ preview_transcription_min_live_words_for_fuzzy_repair=(
709
+ preview_transcription_min_live_words_for_fuzzy_repair
710
+ ),
711
+ on_preview_transcription_finished=on_preview_transcription_finished,
712
+ preview_transcription_executor=preview_transcription_executor,
713
+ ultrafast_realtime_model_type=ultrafast_realtime_model_type,
714
+ ultrafast_realtime_transcription_engine=(
715
+ ultrafast_realtime_transcription_engine
716
+ ),
717
+ ultrafast_realtime_transcription_engine_options=(
718
+ ultrafast_realtime_transcription_engine_options
719
+ ),
720
+ on_ultrafast_transcription_update=(
721
+ on_ultrafast_transcription_update
722
+ ),
723
+ on_merged_realtime_transcription_update=(
724
+ on_merged_realtime_transcription_update
725
+ ),
726
+ on_realtime_transcription_merge_update=(
727
+ on_realtime_transcription_merge_update
728
+ ),
729
+ ultrafast_realtime_max_tail_words=(
730
+ ultrafast_realtime_max_tail_words
731
+ ),
626
732
  )
627
733
 
628
734
  initialize_recorder(
@@ -19,6 +19,8 @@ from .realtime import (
19
19
  run_realtime_worker,
20
20
  )
21
21
  from .realtime_text_stabilizer import RealtimeTextStabilizer
22
+ from .realtime_merge import StickyRealtimeTranscriptionMerger
23
+ from .preview_transcription import start_preview_transcription_worker
22
24
  from .recording import run_recording_worker
23
25
  from ..transcription_engines import (
24
26
  TranscriptionEngineConfig,
@@ -29,7 +31,10 @@ from .safepipe import SafePipe
29
31
  from .wakeword import OPENWAKEWORD_BACKENDS, setup_wakeword_detection
30
32
  from .audio_input_worker import run_audio_data_worker
31
33
  from .runtime import read_stdout_pipe, start_recorder_worker
32
- from .transcription import run_transcription_worker
34
+ from .transcription import (
35
+ SharedFinalModelExecutor,
36
+ run_transcription_worker,
37
+ )
33
38
  from .voice_activity import warmup_voice_activity_detectors
34
39
 
35
40
 
@@ -63,6 +68,7 @@ def initialize_recorder(
63
68
  _initialize_transcription_runtime(recorder, recorder_cls)
64
69
  _start_audio_reader(recorder, recorder_cls)
65
70
  _initialize_realtime_transcription_model(recorder)
71
+ _initialize_preview_transcription_model(recorder)
66
72
  _initialize_wakeword_detection(
67
73
  recorder,
68
74
  normalized_wakeword_backend,
@@ -175,6 +181,69 @@ def _assign_initial_attributes(recorder, init_args, normalize_wakeword_backend):
175
181
  recorder.on_realtime_text_stabilization_update = (
176
182
  init_args["on_realtime_text_stabilization_update"]
177
183
  )
184
+ recorder.ultrafast_realtime_model_type = init_args[
185
+ "ultrafast_realtime_model_type"
186
+ ]
187
+ recorder.ultrafast_realtime_transcription_engine = (
188
+ init_args["ultrafast_realtime_transcription_engine"]
189
+ or recorder.realtime_transcription_engine
190
+ )
191
+ recorder.ultrafast_realtime_transcription_engine_options = (
192
+ init_args["ultrafast_realtime_transcription_engine_options"]
193
+ if init_args["ultrafast_realtime_transcription_engine_options"]
194
+ is not None
195
+ else recorder.realtime_transcription_engine_options
196
+ )
197
+ recorder.ultrafast_realtime_transcription_model = None
198
+ recorder._ultrafast_uses_realtime_model = False
199
+ recorder.on_ultrafast_transcription_update = init_args[
200
+ "on_ultrafast_transcription_update"
201
+ ]
202
+ recorder.on_merged_realtime_transcription_update = init_args[
203
+ "on_merged_realtime_transcription_update"
204
+ ]
205
+ recorder.on_realtime_transcription_merge_update = init_args[
206
+ "on_realtime_transcription_merge_update"
207
+ ]
208
+ recorder.ultrafast_realtime_max_tail_words = int(
209
+ init_args["ultrafast_realtime_max_tail_words"]
210
+ )
211
+
212
+ recorder.enable_preview_transcription = bool(
213
+ init_args["enable_preview_transcription"]
214
+ )
215
+ recorder.preview_model_type = (
216
+ init_args["preview_model_type"] or init_args["model"]
217
+ )
218
+ recorder.preview_transcription_engine = (
219
+ init_args["preview_transcription_engine"]
220
+ or init_args["transcription_engine"]
221
+ )
222
+ recorder.preview_transcription_engine_options = (
223
+ init_args["preview_transcription_engine_options"]
224
+ if init_args["preview_transcription_engine_options"] is not None
225
+ else recorder.transcription_engine_options
226
+ )
227
+ recorder.preview_transcription_tail_seconds = float(
228
+ init_args["preview_transcription_tail_seconds"]
229
+ )
230
+ recorder.preview_transcription_min_live_words_for_fuzzy_repair = int(
231
+ init_args["preview_transcription_min_live_words_for_fuzzy_repair"]
232
+ )
233
+ recorder.on_preview_transcription_finished = (
234
+ init_args["on_preview_transcription_finished"]
235
+ )
236
+ recorder.preview_transcription_executor = (
237
+ init_args["preview_transcription_executor"]
238
+ )
239
+ recorder._uses_external_preview_transcription_executor = (
240
+ recorder.preview_transcription_executor is not None
241
+ )
242
+ recorder.preview_transcription_uses_main_model = False
243
+ recorder._preview_uses_main_model = False
244
+ recorder._preview_uses_shared_final_worker = False
245
+ recorder.preview_transcription_model = None
246
+ recorder.preview_transcription_worker = None
178
247
  recorder.realtime_punctuation_split_marks = (
179
248
  _normalize_realtime_punctuation_split_marks(
180
249
  init_args["realtime_punctuation_split_marks"]
@@ -225,8 +294,20 @@ def _assign_initial_attributes(recorder, init_args, normalize_wakeword_backend):
225
294
  recorder.realtime_stabilized_text = ""
226
295
  recorder.realtime_stabilized_safetext = ""
227
296
  recorder.realtime_text_stabilizer = RealtimeTextStabilizer()
297
+ recorder.realtime_transcription_merger = StickyRealtimeTranscriptionMerger(
298
+ max_ultrafast_tail_words=(
299
+ recorder.ultrafast_realtime_max_tail_words
300
+ )
301
+ )
302
+ recorder.realtime_transcription_merger.reset(0)
228
303
  recorder.realtime_recording_id = 0
229
304
  recorder.realtime_observation_sequence = 0
305
+ recorder.ultrafast_realtime_observation_sequence = 0
306
+ recorder.ultrafast_realtime_transcription_text = ""
307
+ recorder.merged_realtime_transcription_text = ""
308
+ recorder.last_ultrafast_transcription = ""
309
+ recorder.last_merged_realtime_transcription = ""
310
+ recorder.last_realtime_transcription_merge_result = None
230
311
  recorder.realtime_text_stabilization_event = None
231
312
  recorder.realtime_stabilization_accepted_count = 0
232
313
  recorder.realtime_stabilization_outlier_count = 0
@@ -255,6 +336,16 @@ def _assign_initial_attributes(recorder, init_args, normalize_wakeword_backend):
255
336
  recorder.last_transcription_bytes = None
256
337
  recorder.last_transcription_bytes_b64 = None
257
338
  recorder.last_transcription_metadata = None
339
+ recorder.active_speech_tail_buffer = bytearray()
340
+ recorder._current_transcription_live_text = None
341
+ recorder._current_transcription_tail_audio = None
342
+ recorder._preview_transcription_submitted = False
343
+ recorder._pending_vad_live_text = None
344
+ recorder.preview_transcription_queue = None
345
+ recorder.preview_transcription_stop_event = None
346
+ recorder.preview_transcription_thread = None
347
+ recorder.last_preview_transcription = ""
348
+ recorder.last_preview_transcription_result = None
258
349
  recorder.last_preroll_selection = None
259
350
  recorder._pending_preroll_selection = None
260
351
  recorder.initial_prompt = init_args["initial_prompt"]
@@ -398,12 +489,16 @@ def _initialize_transcription_runtime(recorder, recorder_cls):
398
489
  recorder.stdout_thread = None
399
490
  recorder.parent_transcription_pipe = None
400
491
  recorder.parent_stdout_pipe = None
492
+ recorder.shared_preview_transcription_result_queue = None
401
493
  child_transcription_pipe = None
402
494
  child_stdout_pipe = None
403
495
 
404
496
  if not recorder._uses_external_transcription_executor:
405
497
  recorder.parent_transcription_pipe, child_transcription_pipe = SafePipe()
406
498
  recorder.parent_stdout_pipe, child_stdout_pipe = SafePipe()
499
+ if _should_share_preview_model(recorder):
500
+ recorder.shared_preview_transcription_result_queue = mp.Queue()
501
+ recorder._preview_uses_shared_final_worker = True
407
502
 
408
503
  recorder.device = "cuda" if recorder.device == "cuda" and torch.cuda.is_available() else "cpu"
409
504
 
@@ -432,10 +527,72 @@ def _initialize_transcription_runtime(recorder, recorder_cls):
432
527
  recorder.batch_size,
433
528
  recorder.faster_whisper_vad_filter,
434
529
  recorder.normalize_audio,
530
+ recorder.shared_preview_transcription_result_queue,
435
531
  )
436
532
  )
437
533
 
438
534
 
535
+ def _values_equal(left, right):
536
+ """Compares optional engine settings without assuming scalar values."""
537
+
538
+ if left is right:
539
+ return True
540
+ try:
541
+ result = left == right
542
+ except Exception:
543
+ return False
544
+ return result if isinstance(result, bool) else False
545
+
546
+
547
+ def _should_share_preview_model(recorder):
548
+ """Returns whether Preview can use the loaded Final model safely."""
549
+
550
+ return bool(
551
+ getattr(recorder, "enable_preview_transcription", False)
552
+ and not getattr(
553
+ recorder,
554
+ "_uses_external_preview_transcription_executor",
555
+ False,
556
+ )
557
+ and recorder.preview_model_type == recorder.main_model_type
558
+ and recorder.preview_transcription_engine
559
+ == recorder.transcription_engine
560
+ and _values_equal(
561
+ recorder.preview_transcription_engine_options,
562
+ recorder.transcription_engine_options,
563
+ )
564
+ )
565
+
566
+
567
+ def _should_share_ultrafast_realtime_model(recorder):
568
+ """Returns whether both realtime lanes can share one loaded model."""
569
+
570
+ return bool(
571
+ getattr(recorder, "ultrafast_realtime_model_type", None)
572
+ and getattr(recorder, "realtime_transcription_model", None) is not None
573
+ and recorder.ultrafast_realtime_model_type
574
+ == recorder.realtime_model_type
575
+ and recorder.ultrafast_realtime_transcription_engine
576
+ == recorder.realtime_transcription_engine
577
+ and _values_equal(
578
+ recorder.ultrafast_realtime_transcription_engine_options,
579
+ recorder.realtime_transcription_engine_options,
580
+ )
581
+ )
582
+
583
+
584
+ def _require_streaming_realtime_model(model, lane_name):
585
+ """Rejects an optional lane that cannot maintain streaming sessions."""
586
+
587
+ if not getattr(model, "supports_streaming", False) or not hasattr(
588
+ model,
589
+ "create_streaming_session",
590
+ ):
591
+ raise ValueError(
592
+ f"{lane_name} realtime model must support streaming sessions"
593
+ )
594
+
595
+
439
596
  def _start_audio_reader(recorder, recorder_cls):
440
597
  """
441
598
  Starts the background audio reader process.
@@ -515,6 +672,143 @@ def _initialize_realtime_transcription_model(recorder):
515
672
  f"{recorder.realtime_transcription_engine} realtime speech to text transcription model initialized successfully"
516
673
  )
517
674
 
675
+ if not (
676
+ recorder.enable_realtime_transcription
677
+ and recorder.ultrafast_realtime_model_type
678
+ ):
679
+ return
680
+
681
+ try:
682
+ if _should_share_ultrafast_realtime_model(recorder):
683
+ recorder.ultrafast_realtime_transcription_model = (
684
+ recorder.realtime_transcription_model
685
+ )
686
+ recorder._ultrafast_uses_realtime_model = True
687
+ else:
688
+ logger.info(
689
+ "Initializing %s ultrafast realtime transcription model %s",
690
+ recorder.ultrafast_realtime_transcription_engine,
691
+ recorder.ultrafast_realtime_model_type,
692
+ )
693
+ recorder.ultrafast_realtime_transcription_model = (
694
+ create_transcription_engine(
695
+ recorder.ultrafast_realtime_transcription_engine,
696
+ TranscriptionEngineConfig(
697
+ model=recorder.ultrafast_realtime_model_type,
698
+ download_root=recorder.download_root,
699
+ compute_type=recorder.compute_type,
700
+ gpu_device_index=recorder.gpu_device_index,
701
+ device=recorder.device,
702
+ beam_size=recorder.beam_size_realtime,
703
+ initial_prompt=recorder.initial_prompt_realtime,
704
+ suppress_tokens=recorder.suppress_tokens,
705
+ batch_size=recorder.realtime_batch_size,
706
+ vad_filter=recorder.faster_whisper_vad_filter,
707
+ normalize_audio=recorder.normalize_audio,
708
+ engine_options=(
709
+ recorder.ultrafast_realtime_transcription_engine_options
710
+ ),
711
+ ),
712
+ )
713
+ )
714
+
715
+ _require_streaming_realtime_model(
716
+ recorder.ultrafast_realtime_transcription_model,
717
+ "ultrafast",
718
+ )
719
+
720
+ if not recorder._ultrafast_uses_realtime_model:
721
+ current_dir = os.path.dirname(
722
+ os.path.dirname(os.path.realpath(__file__))
723
+ )
724
+ warmup_audio_path = os.path.join(
725
+ current_dir,
726
+ "assets",
727
+ "warmup_audio.wav",
728
+ )
729
+ warmup_audio_data, _ = sf.read(
730
+ warmup_audio_path,
731
+ dtype="float32",
732
+ )
733
+ recorder.ultrafast_realtime_transcription_model.warmup(
734
+ warmup_audio_data
735
+ )
736
+ except Exception as error:
737
+ logger.exception(
738
+ "Error initializing ultrafast realtime transcription model: %s",
739
+ error,
740
+ )
741
+ raise
742
+
743
+ logger.debug("Ultrafast realtime transcription model initialized")
744
+
745
+
746
+ def _initialize_preview_transcription_model(recorder):
747
+ """
748
+ Initializes the optional independent speculative Preview model.
749
+ """
750
+
751
+ if not recorder.enable_preview_transcription:
752
+ return
753
+
754
+ recorder._preview_uses_main_model = _should_share_preview_model(recorder)
755
+ if recorder._preview_uses_main_model:
756
+ recorder.preview_transcription_uses_main_model = True
757
+ if recorder._uses_external_transcription_executor:
758
+ recorder.preview_transcription_executor = (
759
+ recorder.transcription_executor
760
+ )
761
+ else:
762
+ recorder.preview_transcription_executor = SharedFinalModelExecutor(
763
+ recorder
764
+ )
765
+ recorder._uses_external_preview_transcription_executor = True
766
+ start_preview_transcription_worker(recorder)
767
+ return
768
+
769
+ if not recorder._uses_external_preview_transcription_executor:
770
+ try:
771
+ logger.info(
772
+ f"Initializing {recorder.preview_transcription_engine} preview "
773
+ f"transcription model {recorder.preview_model_type}, "
774
+ f"device: {recorder.device}, compute type: {recorder.compute_type}, "
775
+ f"download root: {recorder.download_root}"
776
+ )
777
+ recorder.preview_transcription_model = create_transcription_engine(
778
+ recorder.preview_transcription_engine,
779
+ TranscriptionEngineConfig(
780
+ model=recorder.preview_model_type,
781
+ download_root=recorder.download_root,
782
+ compute_type=recorder.compute_type,
783
+ gpu_device_index=recorder.gpu_device_index,
784
+ device=recorder.device,
785
+ beam_size=recorder.beam_size_realtime,
786
+ initial_prompt=recorder.initial_prompt,
787
+ suppress_tokens=recorder.suppress_tokens,
788
+ batch_size=0,
789
+ vad_filter=recorder.faster_whisper_vad_filter,
790
+ normalize_audio=recorder.normalize_audio,
791
+ engine_options=recorder.preview_transcription_engine_options,
792
+ ),
793
+ )
794
+
795
+ current_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
796
+ warmup_audio_path = os.path.join(
797
+ current_dir,
798
+ "assets",
799
+ "warmup_audio.wav",
800
+ )
801
+ warmup_audio_data, _ = sf.read(warmup_audio_path, dtype="float32")
802
+ recorder.preview_transcription_model.warmup(warmup_audio_data)
803
+ except Exception as error:
804
+ logger.exception(
805
+ "Error initializing preview transcription model: %s",
806
+ error,
807
+ )
808
+ raise
809
+
810
+ start_preview_transcription_worker(recorder)
811
+
518
812
 
519
813
  def _initialize_wakeword_detection(
520
814
  recorder,