RealTimeSTT 0.3.100__tar.gz → 0.3.102__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 (23) hide show
  1. {realtimestt-0.3.100/realtimestt.egg-info → realtimestt-0.3.102}/PKG-INFO +3 -4
  2. {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT/audio_recorder.py +201 -98
  3. {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT/audio_recorder_client.py +88 -15
  4. realtimestt-0.3.102/RealtimeSTT/safepipe.py +245 -0
  5. {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT_server/stt_server.py +79 -21
  6. {realtimestt-0.3.100 → realtimestt-0.3.102/realtimestt.egg-info}/PKG-INFO +3 -4
  7. {realtimestt-0.3.100 → realtimestt-0.3.102}/realtimestt.egg-info/SOURCES.txt +1 -0
  8. {realtimestt-0.3.100 → realtimestt-0.3.102}/realtimestt.egg-info/requires.txt +2 -3
  9. {realtimestt-0.3.100 → realtimestt-0.3.102}/requirements.txt +2 -3
  10. {realtimestt-0.3.100 → realtimestt-0.3.102}/setup.py +1 -1
  11. {realtimestt-0.3.100 → realtimestt-0.3.102}/LICENSE +0 -0
  12. {realtimestt-0.3.100 → realtimestt-0.3.102}/MANIFEST.in +0 -0
  13. {realtimestt-0.3.100 → realtimestt-0.3.102}/README.md +0 -0
  14. {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT/__init__.py +0 -0
  15. {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT/audio_input.py +0 -0
  16. {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT/warmup_audio.wav +0 -0
  17. {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT_server/__init__.py +0 -0
  18. {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT_server/install_packages.py +0 -0
  19. {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT_server/stt_cli_client.py +0 -0
  20. {realtimestt-0.3.100 → realtimestt-0.3.102}/realtimestt.egg-info/dependency_links.txt +0 -0
  21. {realtimestt-0.3.100 → realtimestt-0.3.102}/realtimestt.egg-info/entry_points.txt +0 -0
  22. {realtimestt-0.3.100 → realtimestt-0.3.102}/realtimestt.egg-info/top_level.txt +0 -0
  23. {realtimestt-0.3.100 → realtimestt-0.3.102}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: realtimestt
3
- Version: 0.3.100
3
+ Version: 0.3.102
4
4
  Summary: A fast Voice Activity Detection and Transcription System
5
5
  Home-page: https://github.com/KoljaB/RealTimeSTT
6
6
  Author: Kolja Beigel
@@ -17,11 +17,10 @@ Requires-Dist: webrtcvad-wheels==2.0.14
17
17
  Requires-Dist: halo==0.0.31
18
18
  Requires-Dist: torch
19
19
  Requires-Dist: torchaudio
20
- Requires-Dist: scipy==1.15.1
20
+ Requires-Dist: scipy==1.15.2
21
21
  Requires-Dist: openwakeword>=0.4.0
22
- Requires-Dist: websockets==14.2
22
+ Requires-Dist: websockets==15.0.1
23
23
  Requires-Dist: websocket-client==1.8.0
24
- Requires-Dist: numpy<2.0.0
25
24
  Requires-Dist: soundfile==0.13.1
26
25
  Dynamic: author
27
26
  Dynamic: author-email
@@ -34,6 +34,7 @@ from scipy.signal import resample
34
34
  import signal as system_signal
35
35
  from ctypes import c_bool
36
36
  from scipy import signal
37
+ from .safepipe import SafePipe
37
38
  import soundfile as sf
38
39
  import faster_whisper
39
40
  import openwakeword
@@ -93,7 +94,7 @@ if platform.system() != 'Darwin':
93
94
  class TranscriptionWorker:
94
95
  def __init__(self, conn, stdout_pipe, model_path, download_root, compute_type, gpu_device_index, device,
95
96
  ready_event, shutdown_event, interrupt_stop_event, beam_size, initial_prompt, suppress_tokens,
96
- batch_size, faster_whisper_vad_filter):
97
+ batch_size, faster_whisper_vad_filter, normalize_audio):
97
98
  self.conn = conn
98
99
  self.stdout_pipe = stdout_pipe
99
100
  self.model_path = model_path
@@ -109,6 +110,7 @@ class TranscriptionWorker:
109
110
  self.suppress_tokens = suppress_tokens
110
111
  self.batch_size = batch_size
111
112
  self.faster_whisper_vad_filter = faster_whisper_vad_filter
113
+ self.normalize_audio = normalize_audio
112
114
  self.queue = queue.Queue()
113
115
 
114
116
  def custom_print(self, *args, **kwargs):
@@ -120,13 +122,16 @@ class TranscriptionWorker:
120
122
 
121
123
  def poll_connection(self):
122
124
  while not self.shutdown_event.is_set():
123
- if self.conn.poll(0.01):
124
- try:
125
+ try:
126
+ # Use a longer timeout to reduce polling frequency
127
+ if self.conn.poll(0.01): # Increased from 0.01 to 0.5 seconds
125
128
  data = self.conn.recv()
126
129
  self.queue.put(data)
127
- except Exception as e:
128
- logging.error(f"Error receiving data from connection: {e}", exc_info=True)
129
- else:
130
+ else:
131
+ # Sleep only if no data, but use a shorter sleep
132
+ time.sleep(TIME_SLEEP)
133
+ except Exception as e:
134
+ logging.error(f"Error receiving data from connection: {e}", exc_info=True)
130
135
  time.sleep(TIME_SLEEP)
131
136
 
132
137
  def run(self):
@@ -170,15 +175,32 @@ class TranscriptionWorker:
170
175
  try:
171
176
  while not self.shutdown_event.is_set():
172
177
  try:
173
- audio, language = self.queue.get(timeout=0.1)
178
+ audio, language, use_prompt = self.queue.get(timeout=0.1)
174
179
  try:
175
180
  logging.debug(f"Transcribing audio with language {language}")
181
+ start_t = time.time()
182
+
183
+ # normalize audio to -0.95 dBFS
184
+ if audio is not None and audio .size > 0:
185
+ if self.normalize_audio:
186
+ peak = np.max(np.abs(audio))
187
+ if peak > 0:
188
+ audio = (audio / peak) * 0.95
189
+ else:
190
+ logging.error("Received None audio for transcription")
191
+ self.conn.send(('error', "Received None audio for transcription"))
192
+ continue
193
+
194
+ prompt = None
195
+ if use_prompt:
196
+ prompt = self.initial_prompt if self.initial_prompt else None
197
+
176
198
  if self.batch_size > 0:
177
199
  segments, info = model.transcribe(
178
200
  audio,
179
201
  language=language if language else None,
180
202
  beam_size=self.beam_size,
181
- initial_prompt=self.initial_prompt,
203
+ initial_prompt=prompt,
182
204
  suppress_tokens=self.suppress_tokens,
183
205
  batch_size=self.batch_size,
184
206
  vad_filter=self.faster_whisper_vad_filter
@@ -188,13 +210,13 @@ class TranscriptionWorker:
188
210
  audio,
189
211
  language=language if language else None,
190
212
  beam_size=self.beam_size,
191
- initial_prompt=self.initial_prompt,
213
+ initial_prompt=prompt,
192
214
  suppress_tokens=self.suppress_tokens,
193
215
  vad_filter=self.faster_whisper_vad_filter
194
216
  )
195
-
217
+ elapsed = time.time() - start_t
196
218
  transcription = " ".join(seg.text for seg in segments).strip()
197
- logging.debug(f"Final text detected with main model: {transcription}")
219
+ logging.debug(f"Final text detected with main model: {transcription} in {elapsed:.4f}s")
198
220
  self.conn.send(('success', (transcription, info)))
199
221
  except Exception as e:
200
222
  logging.error(f"General error in transcription: {e}", exc_info=True)
@@ -277,9 +299,11 @@ class AudioToTextRecorder:
277
299
  on_vad_stop=None,
278
300
  on_vad_detect_start=None,
279
301
  on_vad_detect_stop=None,
302
+ on_turn_detection_start=None,
303
+ on_turn_detection_stop=None,
280
304
 
281
305
  # Wake word parameters
282
- wakeword_backend: str = "pvporcupine",
306
+ wakeword_backend: str = "",
283
307
  openwakeword_model_paths: str = None,
284
308
  openwakeword_inference_framework: str = "onnx",
285
309
  wake_words: str = "",
@@ -309,6 +333,7 @@ class AudioToTextRecorder:
309
333
  no_log_file: bool = False,
310
334
  use_extended_logging: bool = False,
311
335
  faster_whisper_vad_filter: bool = True,
336
+ normalize_audio: bool = False,
312
337
  ):
313
338
  """
314
339
  Initializes an audio recorder and transcription
@@ -436,6 +461,14 @@ class AudioToTextRecorder:
436
461
  called when the system stops listening for voice activity. This is
437
462
  not called when VAD actually stops (use on_vad_stop for this), but
438
463
  when the system stops listening for it.
464
+ - on_turn_detection_start (callable, default=None): Callback function
465
+ to be called when the system starts to listen for a turn of speech.
466
+ - on_turn_detection_stop (callable, default=None): Callback function to
467
+ be called when the system stops listening for a turn of speech.
468
+ - wakeword_backend (str, default=""): Specifies the backend library to
469
+ use for wake word detection. Supported options include 'pvporcupine'
470
+ for using the Porcupine wake word engine or 'oww' for using the
471
+ OpenWakeWord engine.
439
472
  - wakeword_backend (str, default="pvporcupine"): Specifies the backend
440
473
  library to use for wake word detection. Supported options include
441
474
  'pvporcupine' for using the Porcupine wake word engine or 'oww' for
@@ -530,6 +563,9 @@ class AudioToTextRecorder:
530
563
  the system will additionally use the VAD filter from the faster_whisper library
531
564
  for voice activity detection. This filter is more robust against
532
565
  background noise but requires additional GPU resources.
566
+ - normalize_audio (bool, default=False): If set to True, the system will
567
+ normalize the audio to a specific range before processing. This can
568
+ help improve the quality of the transcription.
533
569
 
534
570
  Raises:
535
571
  Exception: Errors related to initializing transcription
@@ -564,6 +600,8 @@ class AudioToTextRecorder:
564
600
  self.on_vad_stop = on_vad_stop
565
601
  self.on_vad_detect_start = on_vad_detect_start
566
602
  self.on_vad_detect_stop = on_vad_detect_stop
603
+ self.on_turn_detection_start = on_turn_detection_start
604
+ self.on_turn_detection_stop = on_turn_detection_stop
567
605
  self.on_wakeword_detection_start = on_wakeword_detection_start
568
606
  self.on_wakeword_detection_end = on_wakeword_detection_end
569
607
  self.on_recorded_chunk = on_recorded_chunk
@@ -641,6 +679,8 @@ class AudioToTextRecorder:
641
679
  self.early_transcription_on_silence = early_transcription_on_silence
642
680
  self.use_extended_logging = use_extended_logging
643
681
  self.faster_whisper_vad_filter = faster_whisper_vad_filter
682
+ self.normalize_audio = normalize_audio
683
+ self.awaiting_speech_end = False
644
684
 
645
685
  # ----------------------------------------------------------------------------
646
686
  # Named logger configuration
@@ -686,8 +726,9 @@ class AudioToTextRecorder:
686
726
  self.interrupt_stop_event = mp.Event()
687
727
  self.was_interrupted = mp.Event()
688
728
  self.main_transcription_ready_event = mp.Event()
689
- self.parent_transcription_pipe, child_transcription_pipe = mp.Pipe()
690
- self.parent_stdout_pipe, child_stdout_pipe = mp.Pipe()
729
+
730
+ self.parent_transcription_pipe, child_transcription_pipe = SafePipe()
731
+ self.parent_stdout_pipe, child_stdout_pipe = SafePipe()
691
732
 
692
733
  # Set device for model
693
734
  self.device = "cuda" if self.device == "cuda" and torch.cuda.is_available() else "cpu"
@@ -771,7 +812,7 @@ class AudioToTextRecorder:
771
812
  "transcription model initialized successfully")
772
813
 
773
814
  # Setup wake word detection
774
- if wake_words or wakeword_backend in {'oww', 'openwakeword', 'openwakewords'}:
815
+ if wake_words or wakeword_backend in {'oww', 'openwakeword', 'openwakewords', 'pvp', 'pvporcupine'}:
775
816
  self.wakeword_backend = wakeword_backend
776
817
 
777
818
  self.wake_words_list = [
@@ -783,7 +824,7 @@ class AudioToTextRecorder:
783
824
  for _ in range(len(self.wake_words_list))
784
825
  ]
785
826
 
786
- if self.wakeword_backend in {'pvp', 'pvporcupine'}:
827
+ if wake_words and self.wakeword_backend in {'pvp', 'pvporcupine'}:
787
828
 
788
829
  try:
789
830
  self.porcupine = pvporcupine.create(
@@ -796,7 +837,8 @@ class AudioToTextRecorder:
796
837
  except Exception as e:
797
838
  logger.exception(
798
839
  "Error initializing porcupine "
799
- f"wake word detection engine: {e}"
840
+ f"wake word detection engine: {e}. "
841
+ f"Wakewords: {self.wake_words_list}."
800
842
  )
801
843
  raise
802
844
 
@@ -804,7 +846,7 @@ class AudioToTextRecorder:
804
846
  "Porcupine wake word detection engine initialized successfully"
805
847
  )
806
848
 
807
- elif self.wakeword_backend in {'oww', 'openwakeword', 'openwakewords'}:
849
+ elif wake_words and self.wakeword_backend in {'oww', 'openwakeword', 'openwakewords'}:
808
850
 
809
851
  openwakeword.utils.download_models()
810
852
 
@@ -847,7 +889,7 @@ class AudioToTextRecorder:
847
889
  )
848
890
 
849
891
  else:
850
- logger.exception(f"Wakeword engine {self.wakeword_backend} unknown/unsupported. Please specify one of: pvporcupine, openwakeword.")
892
+ logger.exception(f"Wakeword engine {self.wakeword_backend} unknown/unsupported or wake_words not specified. Please specify one of: pvporcupine, openwakeword.")
851
893
 
852
894
 
853
895
  # Setup voice activity detection model WebRTC
@@ -971,6 +1013,10 @@ class AudioToTextRecorder:
971
1013
  worker = TranscriptionWorker(*args, **kwargs)
972
1014
  worker.run()
973
1015
 
1016
+ def _run_callback(self, cb, *args, **kwargs):
1017
+ if cb:
1018
+ threading.Thread(target=cb, args=args, kwargs=kwargs, daemon=True).start()
1019
+
974
1020
  @staticmethod
975
1021
  def _audio_data_worker(
976
1022
  audio_queue,
@@ -1439,41 +1485,23 @@ class AudioToTextRecorder:
1439
1485
  self.shutdown()
1440
1486
  raise # Re-raise the exception after cleanup
1441
1487
 
1442
- def transcribe(self):
1443
- """
1444
- Transcribes audio captured by this class instance using the
1445
- `faster_whisper` model.
1446
1488
 
1447
- Automatically starts recording upon voice activity if not manually
1448
- started using `recorder.start()`.
1449
- Automatically stops recording upon voice deactivity if not manually
1450
- stopped with `recorder.stop()`.
1451
- Processes the recorded audio to generate transcription.
1452
-
1453
- Args:
1454
- on_transcription_finished (callable, optional): Callback function
1455
- to be executed when transcription is ready.
1456
- If provided, transcription will be performed asynchronously,
1457
- and the callback will receive the transcription as its argument.
1458
- If omitted, the transcription will be performed synchronously,
1459
- and the result will be returned.
1460
-
1461
- Returns (if no callback is set):
1462
- str: The transcription of the recorded audio.
1463
-
1464
- Raises:
1465
- Exception: If there is an error during the transcription process.
1466
- """
1467
- self._set_state("transcribing")
1468
- audio_copy = copy.deepcopy(self.audio)
1489
+ def perform_final_transcription(self, audio_bytes=None, use_prompt=True):
1469
1490
  start_time = 0
1470
1491
  with self.transcription_lock:
1471
-
1492
+ if audio_bytes is None:
1493
+ audio_bytes = copy.deepcopy(self.audio)
1494
+
1495
+ if audio_bytes is None or len(audio_bytes) == 0:
1496
+ print("No audio data available for transcription")
1497
+ #logger.info("No audio data available for transcription")
1498
+ return ""
1499
+
1472
1500
  try:
1473
1501
  if self.transcribe_count == 0:
1474
1502
  logger.debug("Adding transcription request, no early transcription started")
1475
1503
  start_time = time.time() # Start timing
1476
- self.parent_transcription_pipe.send((audio_copy, self.language))
1504
+ self.parent_transcription_pipe.send((audio_bytes, self.language, use_prompt))
1477
1505
  self.transcribe_count += 1
1478
1506
 
1479
1507
  while self.transcribe_count > 0:
@@ -1493,7 +1521,7 @@ class AudioToTextRecorder:
1493
1521
  segments, info = result
1494
1522
  self.detected_language = info.language if info.language_probability > 0 else None
1495
1523
  self.detected_language_probability = info.language_probability
1496
- self.last_transcription_bytes = copy.deepcopy(audio_copy)
1524
+ self.last_transcription_bytes = copy.deepcopy(audio_bytes)
1497
1525
  self.last_transcription_bytes_b64 = base64.b64encode(self.last_transcription_bytes.tobytes()).decode('utf-8')
1498
1526
  transcription = self._preprocess_output(segments)
1499
1527
  end_time = time.time() # End timing
@@ -1512,6 +1540,43 @@ class AudioToTextRecorder:
1512
1540
  logger.error(f"Error during transcription: {str(e)}", exc_info=True)
1513
1541
  raise e
1514
1542
 
1543
+
1544
+ def transcribe(self):
1545
+ """
1546
+ Transcribes audio captured by this class instance using the
1547
+ `faster_whisper` model.
1548
+
1549
+ Automatically starts recording upon voice activity if not manually
1550
+ started using `recorder.start()`.
1551
+ Automatically stops recording upon voice deactivity if not manually
1552
+ stopped with `recorder.stop()`.
1553
+ Processes the recorded audio to generate transcription.
1554
+
1555
+ Args:
1556
+ on_transcription_finished (callable, optional): Callback function
1557
+ to be executed when transcription is ready.
1558
+ If provided, transcription will be performed asynchronously,
1559
+ and the callback will receive the transcription as its argument.
1560
+ If omitted, the transcription will be performed synchronously,
1561
+ and the result will be returned.
1562
+
1563
+ Returns (if no callback is set):
1564
+ str: The transcription of the recorded audio.
1565
+
1566
+ Raises:
1567
+ Exception: If there is an error during the transcription process.
1568
+ """
1569
+ audio_copy = copy.deepcopy(self.audio)
1570
+ self._set_state("transcribing")
1571
+ if self.on_transcription_start:
1572
+ abort_value = self.on_transcription_start(audio_copy)
1573
+ if not abort_value:
1574
+ return self.perform_final_transcription(audio_copy)
1575
+ return None
1576
+ else:
1577
+ return self.perform_final_transcription(audio_copy)
1578
+
1579
+
1515
1580
  def _process_wakeword(self, data):
1516
1581
  """
1517
1582
  Processes audio data to detect wake words.
@@ -1639,7 +1704,7 @@ class AudioToTextRecorder:
1639
1704
  self.start_recording_event.set()
1640
1705
 
1641
1706
  if self.on_recording_start:
1642
- self.on_recording_start()
1707
+ self._run_callback(self.on_recording_start)
1643
1708
 
1644
1709
  return self
1645
1710
 
@@ -1683,7 +1748,7 @@ class AudioToTextRecorder:
1683
1748
  self.last_recording_stop_time = self.recording_stop_time
1684
1749
 
1685
1750
  if self.on_recording_stop:
1686
- self.on_recording_stop()
1751
+ self._run_callback(self.on_recording_stop)
1687
1752
 
1688
1753
  return self
1689
1754
 
@@ -1858,7 +1923,7 @@ class AudioToTextRecorder:
1858
1923
  if self.on_recorded_chunk:
1859
1924
  if self.use_extended_logging:
1860
1925
  logger.debug('Debug: Calling on_recorded_chunk')
1861
- self.on_recorded_chunk(data)
1926
+ self._run_callback(self.on_recorded_chunk, data)
1862
1927
 
1863
1928
  if self.use_extended_logging:
1864
1929
  logger.debug('Debug: Checking if handle_buffer_overflow is True')
@@ -1928,7 +1993,7 @@ class AudioToTextRecorder:
1928
1993
  if self.on_wakeword_timeout:
1929
1994
  if self.use_extended_logging:
1930
1995
  logger.debug('Debug: Calling on_wakeword_timeout')
1931
- self.on_wakeword_timeout()
1996
+ self._run_callback(self.on_wakeword_timeout)
1932
1997
  delay_was_passed = wake_word_activation_delay_passed
1933
1998
 
1934
1999
  if self.use_extended_logging:
@@ -1981,7 +2046,7 @@ class AudioToTextRecorder:
1981
2046
  if self.on_wakeword_detected:
1982
2047
  if self.use_extended_logging:
1983
2048
  logger.debug('Debug: Calling on_wakeword_detected')
1984
- self.on_wakeword_detected()
2049
+ self._run_callback(self.on_wakeword_detected)
1985
2050
 
1986
2051
  if self.use_extended_logging:
1987
2052
  logger.debug('Debug: Checking voice activity conditions')
@@ -1998,7 +2063,7 @@ class AudioToTextRecorder:
1998
2063
  if self._is_voice_active():
1999
2064
 
2000
2065
  if self.on_vad_start:
2001
- self.on_vad_start()
2066
+ self._run_callback(self.on_vad_start)
2002
2067
 
2003
2068
  if self.use_extended_logging:
2004
2069
  logger.debug('Debug: Voice activity detected')
@@ -2028,7 +2093,13 @@ class AudioToTextRecorder:
2028
2093
 
2029
2094
  if self.use_extended_logging:
2030
2095
  logger.debug('Debug: Resetting speech_end_silence_start')
2031
- self.speech_end_silence_start = 0
2096
+
2097
+ if self.speech_end_silence_start != 0:
2098
+ self.speech_end_silence_start = 0
2099
+ if self.on_turn_detection_stop:
2100
+ if self.use_extended_logging:
2101
+ logger.debug('Debug: Calling on_turn_detection_stop')
2102
+ self._run_callback(self.on_turn_detection_stop)
2032
2103
 
2033
2104
  else:
2034
2105
  if self.use_extended_logging:
@@ -2084,6 +2155,12 @@ class AudioToTextRecorder:
2084
2155
  (time.time() - self.recording_start_time > self.min_length_of_recording):
2085
2156
 
2086
2157
  self.speech_end_silence_start = time.time()
2158
+ self.awaiting_speech_end = True
2159
+ if self.on_turn_detection_start:
2160
+ if self.use_extended_logging:
2161
+ logger.debug('Debug: Calling on_turn_detection_start')
2162
+
2163
+ self._run_callback(self.on_turn_detection_start)
2087
2164
 
2088
2165
  if self.use_extended_logging:
2089
2166
  logger.debug('Debug: Checking early transcription conditions')
@@ -2098,18 +2175,26 @@ class AudioToTextRecorder:
2098
2175
 
2099
2176
  if self.use_extended_logging:
2100
2177
  logger.debug("Debug: early transcription request pipe send")
2101
- self.parent_transcription_pipe.send((audio, self.language))
2178
+ self.parent_transcription_pipe.send((audio, self.language, True))
2102
2179
  if self.use_extended_logging:
2103
2180
  logger.debug("Debug: early transcription request pipe send return")
2104
2181
  self.allowed_to_early_transcribe = False
2105
2182
 
2106
2183
  else:
2184
+ self.awaiting_speech_end = False
2107
2185
  if self.use_extended_logging:
2108
2186
  logger.debug('Debug: Handling speech detection')
2109
2187
  if self.speech_end_silence_start:
2110
2188
  if self.use_extended_logging:
2111
2189
  logger.info("Resetting self.speech_end_silence_start")
2112
- self.speech_end_silence_start = 0
2190
+
2191
+ if self.speech_end_silence_start != 0:
2192
+ self.speech_end_silence_start = 0
2193
+ if self.on_turn_detection_stop:
2194
+ if self.use_extended_logging:
2195
+ logger.debug('Debug: Calling on_turn_detection_stop')
2196
+ self._run_callback(self.on_turn_detection_stop)
2197
+
2113
2198
  self.allowed_to_early_transcribe = True
2114
2199
 
2115
2200
  if self.use_extended_logging:
@@ -2118,8 +2203,9 @@ class AudioToTextRecorder:
2118
2203
  if self.speech_end_silence_start and time.time() - \
2119
2204
  self.speech_end_silence_start >= \
2120
2205
  self.post_speech_silence_duration:
2206
+
2121
2207
  if self.on_vad_stop:
2122
- self.on_vad_stop()
2208
+ self._run_callback(self.on_vad_stop)
2123
2209
 
2124
2210
  if self.use_extended_logging:
2125
2211
  logger.debug('Debug: Formatting silence start time')
@@ -2140,9 +2226,12 @@ class AudioToTextRecorder:
2140
2226
  self.frames.append(data)
2141
2227
  self.stop()
2142
2228
  if not self.is_recording:
2143
- if self.use_extended_logging:
2144
- logger.debug('Debug: Resetting speech_end_silence_start')
2145
- self.speech_end_silence_start = 0
2229
+ if self.speech_end_silence_start != 0:
2230
+ self.speech_end_silence_start = 0
2231
+ if self.on_turn_detection_stop:
2232
+ if self.use_extended_logging:
2233
+ logger.debug('Debug: Calling on_turn_detection_stop')
2234
+ self._run_callback(self.on_turn_detection_stop)
2146
2235
 
2147
2236
  if self.use_extended_logging:
2148
2237
  logger.debug('Debug: Handling non-wake word scenario')
@@ -2151,6 +2240,8 @@ class AudioToTextRecorder:
2151
2240
  logger.debug('Debug: Setting failed_stop_attempt to True')
2152
2241
  failed_stop_attempt = True
2153
2242
 
2243
+ self.awaiting_speech_end = False
2244
+
2154
2245
  if self.use_extended_logging:
2155
2246
  logger.debug('Debug: Checking if recording stopped')
2156
2247
  if not self.is_recording and was_recording:
@@ -2175,7 +2266,7 @@ class AudioToTextRecorder:
2175
2266
  if self.wakeword_detected and self.on_wakeword_timeout:
2176
2267
  if self.use_extended_logging:
2177
2268
  logger.debug('Debug: Calling on_wakeword_timeout')
2178
- self.on_wakeword_timeout()
2269
+ self._run_callback(self.on_wakeword_timeout)
2179
2270
  self.wakeword_detected = False
2180
2271
 
2181
2272
  if self.use_extended_logging:
@@ -2206,6 +2297,8 @@ class AudioToTextRecorder:
2206
2297
  logger.debug('Debug: Exiting _recording_worker method')
2207
2298
 
2208
2299
 
2300
+
2301
+
2209
2302
  def _realtime_worker(self):
2210
2303
  """
2211
2304
  Performs real-time transcription if the feature is enabled.
@@ -2225,14 +2318,29 @@ class AudioToTextRecorder:
2225
2318
  if not self.enable_realtime_transcription:
2226
2319
  return
2227
2320
 
2228
- # Continue running as long as the main process is active
2321
+ # Track time of last transcription
2322
+ last_transcription_time = time.time()
2323
+
2229
2324
  while self.is_running:
2230
2325
 
2231
- # Check if the recording is active
2232
2326
  if self.is_recording:
2233
2327
 
2234
- # Sleep for the duration of the transcription resolution
2235
- time.sleep(self.realtime_processing_pause)
2328
+ # MODIFIED SLEEP LOGIC:
2329
+ # Wait until realtime_processing_pause has elapsed,
2330
+ # but check often so we can respond to changes quickly.
2331
+ while (
2332
+ time.time() - last_transcription_time
2333
+ ) < self.realtime_processing_pause:
2334
+ time.sleep(0.001)
2335
+ if not self.is_running or not self.is_recording:
2336
+ break
2337
+
2338
+ if self.awaiting_speech_end:
2339
+ time.sleep(0.001)
2340
+ continue
2341
+
2342
+ # Update transcription time
2343
+ last_transcription_time = time.time()
2236
2344
 
2237
2345
  # Convert the buffer frames to a NumPy array
2238
2346
  audio_array = np.frombuffer(
@@ -2249,7 +2357,7 @@ class AudioToTextRecorder:
2249
2357
  if self.use_main_model_for_realtime:
2250
2358
  with self.transcription_lock:
2251
2359
  try:
2252
- self.parent_transcription_pipe.send((audio_array, self.language))
2360
+ self.parent_transcription_pipe.send((audio_array, self.language, True))
2253
2361
  if self.parent_transcription_pipe.poll(timeout=5): # Wait for 5 seconds
2254
2362
  logger.debug("Receive from realtime worker after transcription request to main model")
2255
2363
  status, result = self.parent_transcription_pipe.recv()
@@ -2270,6 +2378,13 @@ class AudioToTextRecorder:
2270
2378
  continue
2271
2379
  else:
2272
2380
  # Perform transcription and assemble the text
2381
+ if self.normalize_audio:
2382
+ # normalize audio to -0.95 dBFS
2383
+ if audio_array is not None and audio_array.size > 0:
2384
+ peak = np.max(np.abs(audio_array))
2385
+ if peak > 0:
2386
+ audio_array = (audio_array / peak) * 0.95
2387
+
2273
2388
  if self.realtime_batch_size > 0:
2274
2389
  segments, info = self.realtime_model_type.transcribe(
2275
2390
  audio_array,
@@ -2338,20 +2453,17 @@ class AudioToTextRecorder:
2338
2453
  )
2339
2454
 
2340
2455
  if matching_pos < 0:
2341
- if self.realtime_stabilized_safetext:
2342
- self._on_realtime_transcription_stabilized(
2343
- self._preprocess_output(
2344
- self.realtime_stabilized_safetext,
2345
- True
2346
- )
2347
- )
2348
- else:
2349
- self._on_realtime_transcription_stabilized(
2350
- self._preprocess_output(
2351
- self.realtime_transcription_text,
2352
- True
2353
- )
2354
- )
2456
+ # pick which text to send
2457
+ text_to_send = (
2458
+ self.realtime_stabilized_safetext
2459
+ if self.realtime_stabilized_safetext
2460
+ else self.realtime_transcription_text
2461
+ )
2462
+ # preprocess once
2463
+ processed = self._preprocess_output(text_to_send, True)
2464
+ # invoke on its own thread
2465
+ self._run_callback(self._on_realtime_transcription_stabilized, processed)
2466
+
2355
2467
  else:
2356
2468
  # We found parts of the stabilized text
2357
2469
  # in the transcripted text
@@ -2364,17 +2476,10 @@ class AudioToTextRecorder:
2364
2476
  # AND at the same time delivers fresh detected
2365
2477
  # parts on the first run without the need for
2366
2478
  # two transcriptions
2367
- self._on_realtime_transcription_stabilized(
2368
- self._preprocess_output(output_text, True)
2369
- )
2479
+ self._run_callback(self._on_realtime_transcription_stabilized, self._preprocess_output(output_text, True))
2370
2480
 
2371
2481
  # Invoke the callback with the transcribed text
2372
- self._on_realtime_transcription_update(
2373
- self._preprocess_output(
2374
- self.realtime_transcription_text,
2375
- True
2376
- )
2377
- )
2482
+ self._run_callback(self._on_realtime_transcription_update, self._preprocess_output(self.realtime_transcription_text,True))
2378
2483
 
2379
2484
  # If not recording, sleep briefly before checking again
2380
2485
  else:
@@ -2538,27 +2643,25 @@ class AudioToTextRecorder:
2538
2643
  # Execute callbacks based on transitioning FROM a particular state
2539
2644
  if old_state == "listening":
2540
2645
  if self.on_vad_detect_stop:
2541
- self.on_vad_detect_stop()
2646
+ self._run_callback(self.on_vad_detect_stop)
2542
2647
  elif old_state == "wakeword":
2543
2648
  if self.on_wakeword_detection_end:
2544
- self.on_wakeword_detection_end()
2649
+ self._run_callback(self.on_wakeword_detection_end)
2545
2650
 
2546
2651
  # Execute callbacks based on transitioning TO a particular state
2547
2652
  if new_state == "listening":
2548
2653
  if self.on_vad_detect_start:
2549
- self.on_vad_detect_start()
2654
+ self._run_callback(self.on_vad_detect_start)
2550
2655
  self._set_spinner("speak now")
2551
2656
  if self.spinner and self.halo:
2552
2657
  self.halo._interval = 250
2553
2658
  elif new_state == "wakeword":
2554
2659
  if self.on_wakeword_detection_start:
2555
- self.on_wakeword_detection_start()
2660
+ self._run_callback(self.on_wakeword_detection_start)
2556
2661
  self._set_spinner(f"say {self.wake_words}")
2557
2662
  if self.spinner and self.halo:
2558
2663
  self.halo._interval = 500
2559
2664
  elif new_state == "transcribing":
2560
- if self.on_transcription_start:
2561
- self.on_transcription_start()
2562
2665
  self._set_spinner("transcribing")
2563
2666
  if self.spinner and self.halo:
2564
2667
  self.halo._interval = 50
@@ -2677,7 +2780,7 @@ class AudioToTextRecorder:
2677
2780
  """
2678
2781
  if self.on_realtime_transcription_stabilized:
2679
2782
  if self.is_recording:
2680
- self.on_realtime_transcription_stabilized(text)
2783
+ self._run_callback(self.on_realtime_transcription_stabilized, text)
2681
2784
 
2682
2785
  def _on_realtime_transcription_update(self, text):
2683
2786
  """
@@ -2695,7 +2798,7 @@ class AudioToTextRecorder:
2695
2798
  """
2696
2799
  if self.on_realtime_transcription_update:
2697
2800
  if self.is_recording:
2698
- self.on_realtime_transcription_update(text)
2801
+ self._run_callback(self.on_realtime_transcription_update, text)
2699
2802
 
2700
2803
  def __enter__(self):
2701
2804
  """