RealTimeSTT 0.3.92__tar.gz → 0.3.94__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 (19) hide show
  1. {realtimestt-0.3.92 → realtimestt-0.3.94}/PKG-INFO +4 -4
  2. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT/audio_input.py +81 -14
  3. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT/audio_recorder.py +128 -33
  4. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT/audio_recorder_client.py +48 -0
  5. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT.egg-info/PKG-INFO +4 -4
  6. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT.egg-info/requires.txt +3 -3
  7. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT_server/stt_server.py +61 -6
  8. {realtimestt-0.3.92 → realtimestt-0.3.94}/setup.py +1 -1
  9. {realtimestt-0.3.92 → realtimestt-0.3.94}/LICENSE +0 -0
  10. {realtimestt-0.3.92 → realtimestt-0.3.94}/README.md +0 -0
  11. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT/__init__.py +0 -0
  12. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT.egg-info/SOURCES.txt +0 -0
  13. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT.egg-info/dependency_links.txt +0 -0
  14. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT.egg-info/entry_points.txt +0 -0
  15. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT.egg-info/top_level.txt +0 -0
  16. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT_server/__init__.py +0 -0
  17. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT_server/install_packages.py +0 -0
  18. {realtimestt-0.3.92 → realtimestt-0.3.94}/RealtimeSTT_server/stt_cli_client.py +0 -0
  19. {realtimestt-0.3.92 → realtimestt-0.3.94}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: RealtimeSTT
3
- Version: 0.3.92
3
+ Version: 0.3.94
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
@@ -13,15 +13,15 @@ Requires-Python: >=3.6
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: PyAudio==0.2.14
16
- Requires-Dist: faster-whisper==1.1.0
16
+ Requires-Dist: faster-whisper==1.1.1
17
17
  Requires-Dist: pvporcupine==1.9.5
18
18
  Requires-Dist: webrtcvad-wheels==2.0.14
19
19
  Requires-Dist: halo==0.0.31
20
20
  Requires-Dist: torch
21
21
  Requires-Dist: torchaudio
22
- Requires-Dist: scipy==1.14.1
22
+ Requires-Dist: scipy==1.15.1
23
23
  Requires-Dist: openwakeword>=0.4.0
24
- Requires-Dist: websockets==14.1
24
+ Requires-Dist: websockets==14.2
25
25
  Requires-Dist: websocket-client==1.8.0
26
26
  Requires-Dist: numpy<2.0.0
27
27
 
@@ -1,22 +1,35 @@
1
1
  from colorama import init, Fore, Style
2
+ from scipy.signal import butter, filtfilt, resample_poly
2
3
  import pyaudio
3
4
  import logging
4
- import time
5
5
 
6
+ DESIRED_RATE = 16000
7
+ CHUNK_SIZE = 1024
8
+ AUDIO_FORMAT = pyaudio.paInt16
9
+ CHANNELS = 1
6
10
 
7
11
  class AudioInput:
8
- def __init__(self, input_device_index=None, debug_mode=False):
12
+ def __init__(
13
+ self,
14
+ input_device_index: int = None,
15
+ debug_mode: bool = False,
16
+ target_samplerate: int = DESIRED_RATE,
17
+ chunk_size: int = CHUNK_SIZE,
18
+ audio_format: int = AUDIO_FORMAT,
19
+ channels: int = CHANNELS,
20
+ resample_to_target: bool = True,
21
+ ):
22
+
9
23
  self.input_device_index = input_device_index
10
24
  self.debug_mode = debug_mode
11
25
  self.audio_interface = None
12
26
  self.stream = None
13
27
  self.device_sample_rate = None
14
-
15
- # PyAudio constants
16
- self.CHUNK = 1024
17
- self.FORMAT = pyaudio.paInt16
18
- self.CHANNELS = 1
19
- self.DESIRED_RATE = 16000
28
+ self.target_samplerate = target_samplerate
29
+ self.chunk_size = chunk_size
30
+ self.audio_format = audio_format
31
+ self.channels = channels
32
+ self.resample_to_target = resample_to_target
20
33
 
21
34
  def get_supported_sample_rates(self, device_index):
22
35
  """Test which standard sample rates are supported by the specified device."""
@@ -31,8 +44,8 @@ class AudioInput:
31
44
  if self.audio_interface.is_format_supported(
32
45
  rate,
33
46
  input_device=device_index, # Changed to input_device
34
- input_channels=max_channels, # Changed to input_channels
35
- input_format=self.FORMAT, # Changed to input_format
47
+ input_channels=max_channels, # Changed to input_channels
48
+ input_format=self.audio_format, # Changed to input_format
36
49
  ):
37
50
  supported_rates.append(rate)
38
51
  except:
@@ -108,18 +121,18 @@ class AudioInput:
108
121
  if self.debug_mode:
109
122
  print(f"Actual selected device index: {actual_device_index}")
110
123
  self.input_device_index = actual_device_index
111
- self.device_sample_rate = self._get_best_sample_rate(actual_device_index, self.DESIRED_RATE)
124
+ self.device_sample_rate = self._get_best_sample_rate(actual_device_index, self.target_samplerate)
112
125
 
113
126
  if self.debug_mode:
114
127
  print(f"Setting up audio on device {self.input_device_index} with sample rate {self.device_sample_rate}")
115
128
 
116
129
  try:
117
130
  self.stream = self.audio_interface.open(
118
- format=self.FORMAT,
119
- channels=self.CHANNELS,
131
+ format=self.audio_format,
132
+ channels=self.channels,
120
133
  rate=self.device_sample_rate,
121
134
  input=True,
122
- frames_per_buffer=self.CHUNK,
135
+ frames_per_buffer=self.chunk_size,
123
136
  input_device_index=self.input_device_index,
124
137
  )
125
138
  if self.debug_mode:
@@ -135,6 +148,60 @@ class AudioInput:
135
148
  self.audio_interface.terminate()
136
149
  return False
137
150
 
151
+ def lowpass_filter(self, signal, cutoff_freq, sample_rate):
152
+ """
153
+ Apply a low-pass Butterworth filter to prevent aliasing in the signal.
154
+
155
+ Args:
156
+ signal (np.ndarray): Input audio signal to filter
157
+ cutoff_freq (float): Cutoff frequency in Hz
158
+ sample_rate (float): Sampling rate of the input signal in Hz
159
+
160
+ Returns:
161
+ np.ndarray: Filtered audio signal
162
+
163
+ Notes:
164
+ - Uses a 5th order Butterworth filter
165
+ - Applies zero-phase filtering using filtfilt
166
+ """
167
+ # Calculate the Nyquist frequency (half the sample rate)
168
+ nyquist_rate = sample_rate / 2.0
169
+
170
+ # Normalize cutoff frequency to Nyquist rate (required by butter())
171
+ normal_cutoff = cutoff_freq / nyquist_rate
172
+
173
+ # Design the Butterworth filter
174
+ b, a = butter(5, normal_cutoff, btype='low', analog=False)
175
+
176
+ # Apply zero-phase filtering (forward and backward)
177
+ filtered_signal = filtfilt(b, a, signal)
178
+ return filtered_signal
179
+
180
+ def resample_audio(self, pcm_data, target_sample_rate, original_sample_rate):
181
+ """
182
+ Filter and resample audio data to a target sample rate.
183
+
184
+ Args:
185
+ pcm_data (np.ndarray): Input audio data
186
+ target_sample_rate (int): Desired output sample rate in Hz
187
+ original_sample_rate (int): Original sample rate of input in Hz
188
+
189
+ Returns:
190
+ np.ndarray: Resampled audio data
191
+
192
+ Notes:
193
+ - Applies anti-aliasing filter before resampling
194
+ - Uses polyphase filtering for high-quality resampling
195
+ """
196
+ if target_sample_rate < original_sample_rate:
197
+ # Downsampling with low-pass filter
198
+ pcm_filtered = self.lowpass_filter(pcm_data, target_sample_rate / 2, original_sample_rate)
199
+ resampled = resample_poly(pcm_filtered, target_sample_rate, original_sample_rate)
200
+ else:
201
+ # Upsampling without low-pass filter
202
+ resampled = resample_poly(pcm_data, target_sample_rate, original_sample_rate)
203
+ return resampled
204
+
138
205
  def read_chunk(self):
139
206
  """Read a chunk of audio data"""
140
207
  return self.stream.read(self.CHUNK, exception_on_overflow=False)
@@ -120,7 +120,7 @@ class TranscriptionWorker:
120
120
  data = self.conn.recv()
121
121
  self.queue.put(data)
122
122
  except Exception as e:
123
- logging.error(f"Error receiving data from connection: {e}")
123
+ logging.error(f"Error receiving data from connection: {e}", exc_info=True)
124
124
  else:
125
125
  time.sleep(TIME_SLEEP)
126
126
 
@@ -139,8 +139,13 @@ class TranscriptionWorker:
139
139
  device_index=self.gpu_device_index,
140
140
  download_root=self.download_root,
141
141
  )
142
+ # Create a short dummy audio array, for example 1 second of silence at 16 kHz
142
143
  if self.batch_size > 0:
143
144
  model = BatchedInferencePipeline(model=model)
145
+
146
+ # Run a warm-up transcription
147
+ dummy_audio = np.zeros(16000, dtype=np.float32)
148
+ model.transcribe(dummy_audio, language="en", beam_size=1)
144
149
  except Exception as e:
145
150
  logging.exception(f"Error initializing main faster_whisper transcription model: {e}")
146
151
  raise
@@ -157,6 +162,7 @@ class TranscriptionWorker:
157
162
  try:
158
163
  audio, language = self.queue.get(timeout=0.1)
159
164
  try:
165
+ logging.debug(f"Transcribing audio with language {language}")
160
166
  if self.batch_size > 0:
161
167
  segments, info = model.transcribe(
162
168
  audio,
@@ -179,7 +185,7 @@ class TranscriptionWorker:
179
185
  logging.debug(f"Final text detected with main model: {transcription}")
180
186
  self.conn.send(('success', (transcription, info)))
181
187
  except Exception as e:
182
- logging.error(f"General error in transcription: {e}")
188
+ logging.error(f"General error in transcription: {e}", exc_info=True)
183
189
  self.conn.send(('error', str(e)))
184
190
  except queue.Empty:
185
191
  continue
@@ -188,7 +194,7 @@ class TranscriptionWorker:
188
194
  logging.debug("Transcription worker process finished due to KeyboardInterrupt")
189
195
  break
190
196
  except Exception as e:
191
- logging.error(f"General error in processing queue item: {e}")
197
+ logging.error(f"General error in processing queue item: {e}", exc_info=True)
192
198
  finally:
193
199
  __builtins__['print'] = print # Restore the original print function
194
200
  self.conn.close()
@@ -281,6 +287,7 @@ class AudioToTextRecorder:
281
287
  buffer_size: int = BUFFER_SIZE,
282
288
  sample_rate: int = SAMPLE_RATE,
283
289
  initial_prompt: Optional[Union[str, Iterable[int]]] = None,
290
+ initial_prompt_realtime: Optional[Union[str, Iterable[int]]] = None,
284
291
  suppress_tokens: Optional[List[int]] = [-1],
285
292
  print_transcription_time: bool = False,
286
293
  early_transcription_on_silence: int = 0,
@@ -294,12 +301,12 @@ class AudioToTextRecorder:
294
301
 
295
302
  Args:
296
303
  - model (str, default="tiny"): Specifies the size of the transcription
297
- model to use or the path to a converted model directory.
298
- Valid options are 'tiny', 'tiny.en', 'base', 'base.en',
299
- 'small', 'small.en', 'medium', 'medium.en', 'large-v1',
300
- 'large-v2'.
301
- If a specific size is provided, the model is downloaded
302
- from the Hugging Face Hub.
304
+ model to use or the path to a converted model directory.
305
+ Valid options are 'tiny', 'tiny.en', 'base', 'base.en',
306
+ 'small', 'small.en', 'medium', 'medium.en', 'large-v1',
307
+ 'large-v2'.
308
+ If a specific size is provided, the model is downloaded
309
+ from the Hugging Face Hub.
303
310
  - download_root (str, default=None): Specifies the root path were the Whisper models
304
311
  are downloaded to. When empty, the default is used.
305
312
  - language (str, default=""): Language code for speech-to-text engine.
@@ -472,7 +479,9 @@ class AudioToTextRecorder:
472
479
  recording. Changing this will very probably functionality (as the
473
480
  WebRTC VAD model is very sensitive towards the sample rate).
474
481
  - initial_prompt (str or iterable of int, default=None): Initial
475
- prompt to be fed to the transcription models.
482
+ prompt to be fed to the main transcription model.
483
+ - initial_prompt_realtime (str or iterable of int, default=None):
484
+ Initial prompt to be fed to the real-time transcription model.
476
485
  - suppress_tokens (list of int, default=[-1]): Tokens to be suppressed
477
486
  from the transcription output.
478
487
  - print_transcription_time (bool, default=False): Logs processing time
@@ -533,6 +542,8 @@ class AudioToTextRecorder:
533
542
  self.enable_realtime_transcription = enable_realtime_transcription
534
543
  self.use_main_model_for_realtime = use_main_model_for_realtime
535
544
  self.main_model_type = model
545
+ if not download_root:
546
+ download_root = None
536
547
  self.download_root = download_root
537
548
  self.realtime_model_type = realtime_model_type
538
549
  self.realtime_processing_pause = realtime_processing_pause
@@ -557,6 +568,8 @@ class AudioToTextRecorder:
557
568
  self.sample_rate = sample_rate
558
569
  self.recording_start_time = 0
559
570
  self.recording_stop_time = 0
571
+ self.last_recording_start_time = 0
572
+ self.last_recording_stop_time = 0
560
573
  self.wake_word_detect_time = 0
561
574
  self.silero_check_time = 0
562
575
  self.silero_working = False
@@ -580,9 +593,12 @@ class AudioToTextRecorder:
580
593
  self.stream = None
581
594
  self.start_recording_event = threading.Event()
582
595
  self.stop_recording_event = threading.Event()
596
+ self.backdate_stop_seconds = 0.0
597
+ self.backdate_resume_seconds = 0.0
583
598
  self.last_transcription_bytes = None
584
599
  self.last_transcription_bytes_b64 = None
585
600
  self.initial_prompt = initial_prompt
601
+ self.initial_prompt_realtime = initial_prompt_realtime
586
602
  self.suppress_tokens = suppress_tokens
587
603
  self.use_wake_words = wake_words or wakeword_backend in {'oww', 'openwakeword', 'openwakewords'}
588
604
  self.detected_language = None
@@ -697,7 +713,11 @@ class AudioToTextRecorder:
697
713
  if self.enable_realtime_transcription and not self.use_main_model_for_realtime:
698
714
  try:
699
715
  logging.info("Initializing faster_whisper realtime "
700
- f"transcription model {self.realtime_model_type}"
716
+ f"transcription model {self.realtime_model_type}, "
717
+ f"default device: {self.device}, "
718
+ f"compute type: {self.compute_type}, "
719
+ f"device index: {self.gpu_device_index}, "
720
+ f"download root: {self.download_root}"
701
721
  )
702
722
  self.realtime_model_type = faster_whisper.WhisperModel(
703
723
  model_size_or_path=self.realtime_model_type,
@@ -708,7 +728,8 @@ class AudioToTextRecorder:
708
728
  )
709
729
  if self.realtime_batch_size > 0:
710
730
  self.realtime_model_type = BatchedInferencePipeline(model=self.realtime_model_type)
711
-
731
+ dummy_audio = np.zeros(16000, dtype=np.float32)
732
+ self.realtime_model_type.transcribe(dummy_audio, language="en", beam_size=1)
712
733
  except Exception as e:
713
734
  logging.exception("Error initializing faster_whisper "
714
735
  f"realtime transcription model: {e}"
@@ -844,6 +865,7 @@ class AudioToTextRecorder:
844
865
  0.3)
845
866
  )
846
867
  self.frames = []
868
+ self.last_frames = []
847
869
 
848
870
  # Recording control flags
849
871
  self.is_recording = False
@@ -909,7 +931,7 @@ class AudioToTextRecorder:
909
931
  logging.info("KeyboardInterrupt in read from stdout detected, exiting...")
910
932
  break
911
933
  except Exception as e:
912
- logging.error(f"Unexpected error in read from stdout: {e}")
934
+ logging.error(f"Unexpected error in read from stdout: {e}", exc_info=True)
913
935
  logging.error(traceback.format_exc()) # Log the full traceback here
914
936
  break
915
937
  time.sleep(0.1)
@@ -1058,7 +1080,7 @@ class AudioToTextRecorder:
1058
1080
  return stream
1059
1081
 
1060
1082
  except Exception as e:
1061
- logging.error(f"Microphone connection failed: {e}. Retrying...")
1083
+ logging.error(f"Microphone connection failed: {e}. Retrying...", exc_info=True)
1062
1084
  input_device_index = None
1063
1085
  time.sleep(3) # Wait before retrying
1064
1086
  continue
@@ -1174,7 +1196,7 @@ class AudioToTextRecorder:
1174
1196
  if e.errno == pyaudio.paInputOverflowed:
1175
1197
  logging.warning("Input overflowed. Frame dropped.")
1176
1198
  else:
1177
- logging.error(f"OSError during recording: {e}")
1199
+ logging.error(f"OSError during recording: {e}", exc_info=True)
1178
1200
  # Attempt to reinitialize the stream
1179
1201
  logging.error("Attempting to reinitialize the audio stream...")
1180
1202
 
@@ -1292,13 +1314,60 @@ class AudioToTextRecorder:
1292
1314
  if (self.stop_recording_event.wait(timeout=0.02)):
1293
1315
  break
1294
1316
 
1295
- # Convert recorded frames to the appropriate audio format.
1296
- audio_array = np.frombuffer(b''.join(self.frames), dtype=np.int16)
1297
- self.audio = audio_array.astype(np.float32) / INT16_MAX_ABS_VALUE
1317
+ frames = self.frames
1318
+ if len(frames) == 0:
1319
+ frames = self.last_frames
1320
+
1321
+ # Calculate samples needed for backdating resume
1322
+ samples_to_keep = int(self.sample_rate * self.backdate_resume_seconds)
1323
+
1324
+ # First convert all current frames to audio array
1325
+ full_audio_array = np.frombuffer(b''.join(frames), dtype=np.int16)
1326
+ full_audio = full_audio_array.astype(np.float32) / INT16_MAX_ABS_VALUE
1327
+
1328
+ # Calculate how many samples we need to keep for backdating resume
1329
+ if samples_to_keep > 0:
1330
+ samples_to_keep = min(samples_to_keep, len(full_audio))
1331
+ # Keep the last N samples for backdating resume
1332
+ frames_to_read_audio = full_audio[-samples_to_keep:]
1333
+
1334
+ # Convert the audio back to int16 bytes for frames
1335
+ frames_to_read_int16 = (frames_to_read_audio * INT16_MAX_ABS_VALUE).astype(np.int16)
1336
+ frame_bytes = frames_to_read_int16.tobytes()
1337
+
1338
+ # Split into appropriate frame sizes (assuming standard frame size)
1339
+ FRAME_SIZE = 2048 # Typical frame size
1340
+ frames_to_read = []
1341
+ for i in range(0, len(frame_bytes), FRAME_SIZE):
1342
+ frame = frame_bytes[i:i + FRAME_SIZE]
1343
+ if frame: # Only add non-empty frames
1344
+ frames_to_read.append(frame)
1345
+ else:
1346
+ frames_to_read = []
1347
+
1348
+ # Process backdate stop seconds
1349
+ samples_to_remove = int(self.sample_rate * self.backdate_stop_seconds)
1350
+
1351
+ if samples_to_remove > 0:
1352
+ if samples_to_remove < len(full_audio):
1353
+ self.audio = full_audio[:-samples_to_remove]
1354
+ logging.debug(f"Removed {samples_to_remove} samples "
1355
+ f"({samples_to_remove/self.sample_rate:.3f}s) from end of audio")
1356
+ else:
1357
+ self.audio = np.array([], dtype=np.float32)
1358
+ logging.debug("Cleared audio (samples_to_remove >= audio length)")
1359
+ else:
1360
+ self.audio = full_audio
1361
+ logging.debug(f"No samples removed, final audio length: {len(self.audio)}")
1362
+
1298
1363
  self.frames.clear()
1364
+ self.last_frames.clear()
1365
+ self.frames.extend(frames_to_read)
1366
+
1367
+ # Reset backdating parameters
1368
+ self.backdate_stop_seconds = 0.0
1369
+ self.backdate_resume_seconds = 0.0
1299
1370
 
1300
- # Reset recording-related timestamps
1301
- self.recording_stop_time = 0
1302
1371
  self.listen_start = 0
1303
1372
 
1304
1373
  self._set_state("inactive")
@@ -1372,7 +1441,7 @@ class AudioToTextRecorder:
1372
1441
  logging.error(f"Transcription error: {result}")
1373
1442
  raise Exception(result)
1374
1443
  except Exception as e:
1375
- logging.error(f"Error during transcription: {str(e)}")
1444
+ logging.error(f"Error during transcription: {str(e)}", exc_info=True)
1376
1445
  raise e
1377
1446
 
1378
1447
  def _process_wakeword(self, data):
@@ -1460,7 +1529,16 @@ class AudioToTextRecorder:
1460
1529
  return self.transcribe()
1461
1530
 
1462
1531
 
1463
- def start(self):
1532
+ def format_number(self, num):
1533
+ # Convert the number to a string
1534
+ num_str = f"{num:.10f}" # Ensure precision is sufficient
1535
+ # Split the number into integer and decimal parts
1536
+ integer_part, decimal_part = num_str.split('.')
1537
+ # Take the last two digits of the integer part and the first two digits of the decimal part
1538
+ result = f"{integer_part[-2:]}.{decimal_part[:2]}"
1539
+ return result
1540
+
1541
+ def start(self, frames = None):
1464
1542
  """
1465
1543
  Starts recording audio directly without waiting for voice activity.
1466
1544
  """
@@ -1482,7 +1560,10 @@ class AudioToTextRecorder:
1482
1560
  self.wakeword_detected = False
1483
1561
  self.wake_word_detect_time = 0
1484
1562
  self.frames = []
1563
+ if frames:
1564
+ self.frames = frames
1485
1565
  self.is_recording = True
1566
+
1486
1567
  self.recording_start_time = time.time()
1487
1568
  self.is_silero_speech_active = False
1488
1569
  self.is_webrtc_speech_active = False
@@ -1494,9 +1575,19 @@ class AudioToTextRecorder:
1494
1575
 
1495
1576
  return self
1496
1577
 
1497
- def stop(self):
1578
+ def stop(self,
1579
+ backdate_stop_seconds: float = 0.0,
1580
+ backdate_resume_seconds: float = 0.0,
1581
+ ):
1498
1582
  """
1499
1583
  Stops recording audio.
1584
+
1585
+ Args:
1586
+ - backdate_stop_seconds (float, default="0.0"): Specifies the number of
1587
+ seconds to backdate the stop time. This is useful when the stop
1588
+ command is issued after the actual stop time.
1589
+ - backdate_resume_seconds (float, default="0.0"): Specifies the number
1590
+ of seconds to backdate the time relistening is initiated.
1500
1591
  """
1501
1592
 
1502
1593
  # Ensure there's a minimum interval
@@ -1509,6 +1600,9 @@ class AudioToTextRecorder:
1509
1600
  return self
1510
1601
 
1511
1602
  logging.info("recording stopped")
1603
+ self.last_frames = copy.deepcopy(self.frames)
1604
+ self.backdate_stop_seconds = backdate_stop_seconds
1605
+ self.backdate_resume_seconds = backdate_resume_seconds
1512
1606
  self.is_recording = False
1513
1607
  self.recording_stop_time = time.time()
1514
1608
  self.is_silero_speech_active = False
@@ -1517,6 +1611,9 @@ class AudioToTextRecorder:
1517
1611
  self.start_recording_event.clear()
1518
1612
  self.stop_recording_event.set()
1519
1613
 
1614
+ self.last_recording_start_time = self.recording_start_time
1615
+ self.last_recording_stop_time = self.recording_stop_time
1616
+
1520
1617
  if self.on_recording_stop:
1521
1618
  self.on_recording_stop()
1522
1619
 
@@ -1591,7 +1688,6 @@ class AudioToTextRecorder:
1591
1688
  return
1592
1689
 
1593
1690
  print("\033[91mRealtimeSTT shutting down\033[0m")
1594
- # logging.debug("RealtimeSTT shutting down")
1595
1691
 
1596
1692
  # Force wait_audio() and text() to exit
1597
1693
  self.is_shut_down = True
@@ -1720,7 +1816,7 @@ class AudioToTextRecorder:
1720
1816
  data = self.audio_queue.get()
1721
1817
 
1722
1818
  except BrokenPipeError:
1723
- logging.error("BrokenPipeError _recording_worker")
1819
+ logging.error("BrokenPipeError _recording_worker", exc_info=True)
1724
1820
  self.is_running = False
1725
1821
  break
1726
1822
 
@@ -1797,11 +1893,11 @@ class AudioToTextRecorder:
1797
1893
 
1798
1894
  except struct.error:
1799
1895
  logging.error("Error unpacking audio data "
1800
- "for wake word processing.")
1896
+ "for wake word processing.", exc_info=True)
1801
1897
  continue
1802
1898
 
1803
1899
  except Exception as e:
1804
- logging.error(f"Wake word processing error: {e}")
1900
+ logging.error(f"Wake word processing error: {e}", exc_info=True)
1805
1901
  continue
1806
1902
 
1807
1903
  if self.use_extended_logging:
@@ -2028,7 +2124,7 @@ class AudioToTextRecorder:
2028
2124
  except Exception as e:
2029
2125
  logging.debug('Debug: Caught exception in main try block')
2030
2126
  if not self.interrupt_stop_event.is_set():
2031
- logging.error(f"Unhandled exeption in _recording_worker: {e}")
2127
+ logging.error(f"Unhandled exeption in _recording_worker: {e}", exc_info=True)
2032
2128
  raise
2033
2129
 
2034
2130
  if self.use_extended_logging:
@@ -2095,7 +2191,7 @@ class AudioToTextRecorder:
2095
2191
  logging.warning("Realtime transcription timed out")
2096
2192
  continue
2097
2193
  except Exception as e:
2098
- logging.error(f"Error in realtime transcription: {str(e)}")
2194
+ logging.error(f"Error in realtime transcription: {str(e)}", exc_info=True)
2099
2195
  continue
2100
2196
  else:
2101
2197
  # Perform transcription and assemble the text
@@ -2104,7 +2200,7 @@ class AudioToTextRecorder:
2104
2200
  audio_array,
2105
2201
  language=self.language if self.language else None,
2106
2202
  beam_size=self.beam_size_realtime,
2107
- initial_prompt=self.initial_prompt,
2203
+ initial_prompt=self.initial_prompt_realtime,
2108
2204
  suppress_tokens=self.suppress_tokens,
2109
2205
  batch_size=self.realtime_batch_size
2110
2206
  )
@@ -2113,7 +2209,7 @@ class AudioToTextRecorder:
2113
2209
  audio_array,
2114
2210
  language=self.language if self.language else None,
2115
2211
  beam_size=self.beam_size_realtime,
2116
- initial_prompt=self.initial_prompt,
2212
+ initial_prompt=self.initial_prompt_realtime,
2117
2213
  suppress_tokens=self.suppress_tokens
2118
2214
  )
2119
2215
 
@@ -2129,7 +2225,6 @@ class AudioToTextRecorder:
2129
2225
  if self.is_recording and time.time() - \
2130
2226
  self.recording_start_time > self.init_realtime_after_seconds:
2131
2227
 
2132
- # logging.debug('Starting realtime transcription')
2133
2228
  self.realtime_transcription_text = realtime_text
2134
2229
  self.realtime_transcription_text = \
2135
2230
  self.realtime_transcription_text.strip()
@@ -2209,7 +2304,7 @@ class AudioToTextRecorder:
2209
2304
  time.sleep(TIME_SLEEP)
2210
2305
 
2211
2306
  except Exception as e:
2212
- logging.error(f"Unhandled exeption in _realtime_worker: {e}")
2307
+ logging.error(f"Unhandled exeption in _realtime_worker: {e}", exc_info=True)
2213
2308
  raise
2214
2309
 
2215
2310
  def _is_silero_speech(self, chunk):
@@ -28,6 +28,7 @@ DEFAULT_DATA_URL = "ws://127.0.0.1:8012"
28
28
  INIT_MODEL_TRANSCRIPTION = "tiny"
29
29
  INIT_MODEL_TRANSCRIPTION_REALTIME = "tiny"
30
30
  INIT_REALTIME_PROCESSING_PAUSE = 0.2
31
+ INIT_REALTIME_INITIAL_PAUSE = 0.2
31
32
  INIT_SILERO_SENSITIVITY = 0.4
32
33
  INIT_WEBRTC_SENSITIVITY = 3
33
34
  INIT_POST_SPEECH_SILENCE_DURATION = 0.6
@@ -68,6 +69,7 @@ class AudioToTextRecorderClient:
68
69
 
69
70
  def __init__(self,
70
71
  model: str = INIT_MODEL_TRANSCRIPTION,
72
+ download_root: str = None,
71
73
  language: str = "",
72
74
  compute_type: str = "default",
73
75
  input_device_index: int = None,
@@ -81,14 +83,17 @@ class AudioToTextRecorderClient:
81
83
  use_microphone=True,
82
84
  spinner=True,
83
85
  level=logging.WARNING,
86
+ batch_size: int = 16,
84
87
 
85
88
  # Realtime transcription parameters
86
89
  enable_realtime_transcription=False,
87
90
  use_main_model_for_realtime=False,
88
91
  realtime_model_type=INIT_MODEL_TRANSCRIPTION_REALTIME,
89
92
  realtime_processing_pause=INIT_REALTIME_PROCESSING_PAUSE,
93
+ init_realtime_after_seconds=INIT_REALTIME_INITIAL_PAUSE,
90
94
  on_realtime_transcription_update=None,
91
95
  on_realtime_transcription_stabilized=None,
96
+ realtime_batch_size: int = 16,
92
97
 
93
98
  # Voice activation parameters
94
99
  silero_sensitivity: float = INIT_SILERO_SENSITIVITY,
@@ -133,6 +138,7 @@ class AudioToTextRecorderClient:
133
138
  buffer_size: int = BUFFER_SIZE,
134
139
  sample_rate: int = SAMPLE_RATE,
135
140
  initial_prompt: Optional[Union[str, Iterable[int]]] = None,
141
+ initial_prompt_realtime: Optional[Union[str, Iterable[int]]] = None,
136
142
  suppress_tokens: Optional[List[int]] = [-1],
137
143
  print_transcription_time: bool = False,
138
144
  early_transcription_on_silence: int = 0,
@@ -162,10 +168,14 @@ class AudioToTextRecorderClient:
162
168
  self.use_microphone = use_microphone
163
169
  self.spinner = spinner
164
170
  self.level = level
171
+ self.batch_size = batch_size
172
+ self.init_realtime_after_seconds = init_realtime_after_seconds
173
+ self.realtime_batch_size = realtime_batch_size
165
174
 
166
175
  # Real-time transcription parameters
167
176
  self.enable_realtime_transcription = enable_realtime_transcription
168
177
  self.use_main_model_for_realtime = use_main_model_for_realtime
178
+ self.download_root = download_root
169
179
  self.realtime_model_type = realtime_model_type
170
180
  self.realtime_processing_pause = realtime_processing_pause
171
181
  self.on_realtime_transcription_update = on_realtime_transcription_update
@@ -204,6 +214,7 @@ class AudioToTextRecorderClient:
204
214
  self.buffer_size = buffer_size
205
215
  self.sample_rate = sample_rate
206
216
  self.initial_prompt = initial_prompt
217
+ self.initial_prompt_realtime = initial_prompt_realtime
207
218
  self.suppress_tokens = suppress_tokens
208
219
  self.print_transcription_time = print_transcription_time
209
220
  self.early_transcription_on_silence = early_transcription_on_silence
@@ -376,6 +387,43 @@ class AudioToTextRecorderClient:
376
387
  args += ['--model', self.model]
377
388
  if self.realtime_model_type:
378
389
  args += ['--realtime_model_type', self.realtime_model_type]
390
+ if self.download_root:
391
+ args += ['--root', self.download_root]
392
+ if self.batch_size is not None:
393
+ args += ['--batch', str(self.batch_size)]
394
+ if self.realtime_batch_size is not None:
395
+ args += ['--realtime_batch_size', str(self.realtime_batch_size)]
396
+ if self.init_realtime_after_seconds is not None:
397
+ args += ['--init_realtime_after_seconds', str(self.init_realtime_after_seconds)]
398
+ if self.initial_prompt_realtime:
399
+ sanitized_prompt = self.initial_prompt_realtime.replace("\n", "\\n")
400
+ args += ['--initial_prompt_realtime', sanitized_prompt]
401
+
402
+ # if self.compute_type:
403
+ # args += ['--compute_type', self.compute_type]
404
+ # if self.input_device_index is not None:
405
+ # args += ['--input_device_index', str(self.input_device_index)]
406
+ # if self.gpu_device_index is not None:
407
+ # args += ['--gpu_device_index', str(self.gpu_device_index)]
408
+ # if self.device:
409
+ # args += ['--device', self.device]
410
+ # if self.spinner:
411
+ # args.append('--spinner') # flag, no need for True/False
412
+ # if self.enable_realtime_transcription:
413
+ # args.append('--enable_realtime_transcription') # flag, no need for True/False
414
+ # if self.handle_buffer_overflow:
415
+ # args.append('--handle_buffer_overflow') # flag, no need for True/False
416
+ # if self.suppress_tokens:
417
+ # args += ['--suppress_tokens', str(self.suppress_tokens)]
418
+ # if self.print_transcription_time:
419
+ # args.append('--print_transcription_time') # flag, no need for True/False
420
+ # if self.allowed_latency_limit is not None:
421
+ # args += ['--allowed_latency_limit', str(self.allowed_latency_limit)]
422
+ # if self.no_log_file:
423
+ # args.append('--no_log_file') # flag, no need for True
424
+ # if self.debug_mode:
425
+ # args.append('--debug') # flag, no need for True/False
426
+
379
427
  if self.language:
380
428
  args += ['--language', self.language]
381
429
  if self.silero_sensitivity is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: RealtimeSTT
3
- Version: 0.3.92
3
+ Version: 0.3.94
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
@@ -13,15 +13,15 @@ Requires-Python: >=3.6
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: PyAudio==0.2.14
16
- Requires-Dist: faster-whisper==1.1.0
16
+ Requires-Dist: faster-whisper==1.1.1
17
17
  Requires-Dist: pvporcupine==1.9.5
18
18
  Requires-Dist: webrtcvad-wheels==2.0.14
19
19
  Requires-Dist: halo==0.0.31
20
20
  Requires-Dist: torch
21
21
  Requires-Dist: torchaudio
22
- Requires-Dist: scipy==1.14.1
22
+ Requires-Dist: scipy==1.15.1
23
23
  Requires-Dist: openwakeword>=0.4.0
24
- Requires-Dist: websockets==14.1
24
+ Requires-Dist: websockets==14.2
25
25
  Requires-Dist: websocket-client==1.8.0
26
26
  Requires-Dist: numpy<2.0.0
27
27
 
@@ -1,12 +1,12 @@
1
1
  PyAudio==0.2.14
2
- faster-whisper==1.1.0
2
+ faster-whisper==1.1.1
3
3
  pvporcupine==1.9.5
4
4
  webrtcvad-wheels==2.0.14
5
5
  halo==0.0.31
6
6
  torch
7
7
  torchaudio
8
- scipy==1.14.1
8
+ scipy==1.15.1
9
9
  openwakeword>=0.4.0
10
- websockets==14.1
10
+ websockets==14.2
11
11
  websocket-client==1.8.0
12
12
  numpy<2.0.0
@@ -27,6 +27,8 @@ stt-server [OPTIONS]
27
27
  - `-D, --debug`: Enable debug logging.
28
28
  - `-W, --write`: Save audio to WAV file.
29
29
  - `-s, --silence_timing`: Enable dynamic silence duration for sentence detection; default True.
30
+ - `-b, --batch, --batch_size`: Batch size for inference; default 16.
31
+ - `--root, --download_root`: Specifies the root path were the Whisper models are downloaded to.
30
32
  - `--silero_sensitivity`: Silero VAD sensitivity (0-1); default 0.05.
31
33
  - `--silero_use_onnx`: Use Silero ONNX model; default False.
32
34
  - `--webrtc_sensitivity`: WebRTC VAD sensitivity (0-3); default 3.
@@ -38,7 +40,10 @@ stt-server [OPTIONS]
38
40
  - `--early_transcription_on_silence`: Start transcription after silence in seconds; default 0.2.
39
41
  - `--beam_size`: Beam size for main model; default 5.
40
42
  - `--beam_size_realtime`: Beam size for real-time model; default 3.
41
- - `--initial_prompt`: Initial transcription guidance prompt.
43
+ - `--init_realtime_after_seconds`: Initial waiting time for realtime transcription; default 0.2.
44
+ - `--realtime_batch_size`: Batch size for the real-time transcription model; default 16.
45
+ - `--initial_prompt`: Initial main transcription guidance prompt.
46
+ - `--initial_prompt_realtime`: Initial realtime transcription guidance prompt.
42
47
  - `--end_of_sentence_detection_pause`: Silence duration for sentence end detection; default 0.45.
43
48
  - `--unknown_sentence_detection_pause`: Pause duration for incomplete sentence detection; default 0.7.
44
49
  - `--mid_sentence_detection_pause`: Pause for mid-sentence break; default 2.0.
@@ -52,6 +57,14 @@ stt-server [OPTIONS]
52
57
  - `--use_main_model_for_realtime`: Use main model for real-time transcription.
53
58
  - `--use_extended_logging`: Enable extensive log messages.
54
59
  - `--logchunks`: Log incoming audio chunks.
60
+ - `--compute_type`: Type of computation to use.
61
+ - `--input_device_index`: Index of the audio input device.
62
+ - `--gpu_device_index`: Index of the GPU device.
63
+ - `--device`: Device to use for computation.
64
+ - `--handle_buffer_overflow`: Handle buffer overflow during transcription.
65
+ - `--suppress_tokens`: Suppress tokens during transcription.
66
+ - `--allowed_latency_limit`: Allowed latency limit for real-time transcription.
67
+
55
68
 
56
69
  ### WebSocket Interface:
57
70
  The server supports two WebSocket connections:
@@ -364,7 +377,7 @@ def parse_arguments():
364
377
  parser.add_argument('-l', '--lang', '--language', type=str, default='en',
365
378
  help='Language code for the STT model to transcribe in a specific language. Leave this empty for auto-detection based on input audio. Default is en. List of supported language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L11-L110')
366
379
 
367
- parser.add_argument('-i', '--input-device', '--input_device_index', type=int, default=1,
380
+ parser.add_argument('-i', '--input-device', '--input-device-index', type=int, default=1,
368
381
  help='Index of the audio input device to use. Use this option to specify a particular microphone or audio input device based on your system. Default is 1.')
369
382
 
370
383
  parser.add_argument('-c', '--control', '--control_port', type=int, default=8011,
@@ -378,12 +391,23 @@ def parse_arguments():
378
391
 
379
392
  parser.add_argument('-D', '--debug', action='store_true', help='Enable debug logging for detailed server operations')
380
393
 
381
- parser.add_argument("-W", "--write", metavar="FILE",
382
- help="Save received audio to a WAV file")
394
+ parser.add_argument('-W', '--write', metavar='FILE', help='Save received audio to a WAV file')
395
+
396
+ parser.add_argument('-b', '--batch', '--batch_size', type=int, default=16, help='Batch size for inference. This parameter controls the number of audio chunks processed in parallel during transcription. Default is 16.')
397
+
398
+ parser.add_argument('--root', '--download_root', type=str,default=None, help='Specifies the root path where the Whisper models are downloaded to. Default is None.')
383
399
 
384
400
  parser.add_argument('-s', '--silence_timing', action='store_true', default=True,
385
401
  help='Enable dynamic adjustment of silence duration for sentence detection. Adjusts post-speech silence duration based on detected sentence structure and punctuation. Default is False.')
386
402
 
403
+ parser.add_argument('--init_realtime_after_seconds', type=float, default=0.2,
404
+ help='The initial waiting time in seconds before real-time transcription starts. This delay helps prevent false positives at the beginning of a session. Default is 0.2 seconds.')
405
+
406
+ parser.add_argument('--realtime_batch_size', type=int, default=16,
407
+ help='Batch size for the real-time transcription model. This parameter controls the number of audio chunks processed in parallel during real-time transcription. Default is 16.')
408
+
409
+ parser.add_argument('--initial_prompt_realtime', type=str, default="", help='Initial prompt that guides the real-time transcription model to produce transcriptions in a particular style or format.')
410
+
387
411
  parser.add_argument('--silero_sensitivity', type=float, default=0.05,
388
412
  help='Sensitivity level for Silero Voice Activity Detection (VAD), with a range from 0 to 1. Lower values make the model less sensitive, useful for noisy environments. Default is 0.05.')
389
413
 
@@ -457,6 +481,23 @@ def parse_arguments():
457
481
  parser.add_argument('--use_extended_logging', action='store_true',
458
482
  help='Writes extensive log messages for the recording worker, that processes the audio chunks.')
459
483
 
484
+ parser.add_argument('--compute_type', type=str, default='default',
485
+ help='Type of computation to use. See https://opennmt.net/CTranslate2/quantization.html')
486
+
487
+ parser.add_argument('--gpu_device_index', type=int, default=0,
488
+ help='Index of the GPU device to use. Default is None.')
489
+
490
+ parser.add_argument('--device', type=str, default='cuda',
491
+ help='Device for model to use. Can either be "cuda" or "cpu". Default is cuda.')
492
+
493
+ parser.add_argument('--handle_buffer_overflow', action='store_true',
494
+ help='Handle buffer overflow during transcription. Default is False.')
495
+
496
+ parser.add_argument('--suppress_tokens', type=int, default=[-1], nargs='*', help='Suppress tokens during transcription. Default is [-1].')
497
+
498
+ parser.add_argument('--allowed_latency_limit', type=int, default=100,
499
+ help='Maximal amount of chunks that can be unprocessed in queue before discarding chunks.. Default is 100.')
500
+
460
501
  parser.add_argument('--logchunks', action='store_true', help='Enable logging of incoming audio chunks (periods)')
461
502
 
462
503
  # Parse arguments
@@ -479,6 +520,9 @@ def parse_arguments():
479
520
  if args.initial_prompt:
480
521
  args.initial_prompt = args.initial_prompt.replace("\\n", "\n")
481
522
 
523
+ if args.initial_prompt_realtime:
524
+ args.initial_prompt_realtime = args.initial_prompt_realtime.replace("\\n", "\n")
525
+
482
526
  return args
483
527
 
484
528
  def _recorder_thread(loop):
@@ -534,7 +578,7 @@ def decode_and_resample(
534
578
 
535
579
  return resampled_audio.astype(np.int16).tobytes()
536
580
 
537
- async def control_handler(websocket, path):
581
+ async def control_handler(websocket):
538
582
  debug_print(f"New control connection from {websocket.remote_address}")
539
583
  print(f"{bcolors.OKGREEN}Control client connected{bcolors.ENDC}")
540
584
  global recorder
@@ -629,7 +673,7 @@ async def control_handler(websocket, path):
629
673
  finally:
630
674
  control_connections.remove(websocket)
631
675
 
632
- async def data_handler(websocket, path):
676
+ async def data_handler(websocket):
633
677
  global writechunks, wav_file
634
678
  print(f"{bcolors.OKGREEN}Data client connected{bcolors.ENDC}")
635
679
  data_connections.add(websocket)
@@ -700,8 +744,13 @@ async def main_async():
700
744
 
701
745
  recorder_config = {
702
746
  'model': args.model,
747
+ 'download_root': args.root,
703
748
  'realtime_model_type': args.rt_model,
704
749
  'language': args.lang,
750
+ 'batch_size': args.batch,
751
+ 'init_realtime_after_seconds': args.init_realtime_after_seconds,
752
+ 'realtime_batch_size': args.realtime_batch_size,
753
+ 'initial_prompt_realtime': args.initial_prompt_realtime,
705
754
  'input_device_index': args.input_device,
706
755
  'silero_sensitivity': args.silero_sensitivity,
707
756
  'silero_use_onnx': args.silero_use_onnx,
@@ -740,6 +789,12 @@ async def main_async():
740
789
  'no_log_file': True, # Disable logging to file
741
790
  'use_extended_logging': args.use_extended_logging,
742
791
  'level': loglevel,
792
+ 'compute_type': args.compute_type,
793
+ 'gpu_device_index': args.gpu_device_index,
794
+ 'device': args.device,
795
+ 'handle_buffer_overflow': args.handle_buffer_overflow,
796
+ 'suppress_tokens': args.suppress_tokens,
797
+ 'allowed_latency_limit': args.allowed_latency_limit,
743
798
  }
744
799
 
745
800
  try:
@@ -9,7 +9,7 @@ with open('requirements.txt') as f:
9
9
 
10
10
  setuptools.setup(
11
11
  name="RealtimeSTT",
12
- version="0.3.92",
12
+ version="0.3.94",
13
13
  author="Kolja Beigel",
14
14
  author_email="kolja.beigel@web.de",
15
15
  description="A fast Voice Activity Detection and Transcription System",
File without changes
File without changes
File without changes