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.
- {realtimestt-0.3.100/realtimestt.egg-info → realtimestt-0.3.102}/PKG-INFO +3 -4
- {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT/audio_recorder.py +201 -98
- {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT/audio_recorder_client.py +88 -15
- realtimestt-0.3.102/RealtimeSTT/safepipe.py +245 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT_server/stt_server.py +79 -21
- {realtimestt-0.3.100 → realtimestt-0.3.102/realtimestt.egg-info}/PKG-INFO +3 -4
- {realtimestt-0.3.100 → realtimestt-0.3.102}/realtimestt.egg-info/SOURCES.txt +1 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/realtimestt.egg-info/requires.txt +2 -3
- {realtimestt-0.3.100 → realtimestt-0.3.102}/requirements.txt +2 -3
- {realtimestt-0.3.100 → realtimestt-0.3.102}/setup.py +1 -1
- {realtimestt-0.3.100 → realtimestt-0.3.102}/LICENSE +0 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/MANIFEST.in +0 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/README.md +0 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT/__init__.py +0 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT/audio_input.py +0 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT/warmup_audio.wav +0 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT_server/__init__.py +0 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT_server/install_packages.py +0 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/RealtimeSTT_server/stt_cli_client.py +0 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/realtimestt.egg-info/dependency_links.txt +0 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/realtimestt.egg-info/entry_points.txt +0 -0
- {realtimestt-0.3.100 → realtimestt-0.3.102}/realtimestt.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
20
|
+
Requires-Dist: scipy==1.15.2
|
|
21
21
|
Requires-Dist: openwakeword>=0.4.0
|
|
22
|
-
Requires-Dist: websockets==
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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=
|
|
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=
|
|
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 = "
|
|
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
|
-
|
|
690
|
-
self.
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2144
|
-
|
|
2145
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
2235
|
-
|
|
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
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
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
|
|
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
|
|
2801
|
+
self._run_callback(self.on_realtime_transcription_update, text)
|
|
2699
2802
|
|
|
2700
2803
|
def __enter__(self):
|
|
2701
2804
|
"""
|