RealTimeSTT 0.3.5__tar.gz → 0.3.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: RealtimeSTT
3
- Version: 0.3.5
3
+ Version: 0.3.6
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
@@ -55,7 +55,7 @@ https://github.com/user-attachments/assets/797e6552-27cd-41b1-a7f3-e5cbc72094f5
55
55
 
56
56
  ### Updates
57
57
 
58
- Latest Version: v0.3.5
58
+ Latest Version: v0.3.6
59
59
 
60
60
  See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
61
61
 
@@ -28,7 +28,7 @@ https://github.com/user-attachments/assets/797e6552-27cd-41b1-a7f3-e5cbc72094f5
28
28
 
29
29
  ### Updates
30
30
 
31
- Latest Version: v0.3.5
31
+ Latest Version: v0.3.6
32
32
 
33
33
  See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
34
34
 
@@ -63,6 +63,7 @@ os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
63
63
  INIT_MODEL_TRANSCRIPTION = "tiny"
64
64
  INIT_MODEL_TRANSCRIPTION_REALTIME = "tiny"
65
65
  INIT_REALTIME_PROCESSING_PAUSE = 0.2
66
+ INIT_REALTIME_INITIAL_PAUSE = 0.2
66
67
  INIT_SILERO_SENSITIVITY = 0.4
67
68
  INIT_WEBRTC_SENSITIVITY = 3
68
69
  INIT_POST_SPEECH_SILENCE_DURATION = 0.6
@@ -179,6 +180,12 @@ class TranscriptionWorker:
179
180
  polling_thread.join() # Wait for the polling thread to finish
180
181
 
181
182
 
183
+ class bcolors:
184
+ OKGREEN = '\033[92m' # Green for active speech detection
185
+ WARNING = '\033[93m' # Yellow for silence detection
186
+ ENDC = '\033[0m' # Reset to default color
187
+
188
+
182
189
  class AudioToTextRecorder:
183
190
  """
184
191
  A class responsible for capturing audio from the microphone, detecting
@@ -207,6 +214,7 @@ class AudioToTextRecorder:
207
214
  use_main_model_for_realtime=False,
208
215
  realtime_model_type=INIT_MODEL_TRANSCRIPTION_REALTIME,
209
216
  realtime_processing_pause=INIT_REALTIME_PROCESSING_PAUSE,
217
+ init_realtime_after_seconds=INIT_REALTIME_INITIAL_PAUSE,
210
218
  on_realtime_transcription_update=None,
211
219
  on_realtime_transcription_stabilized=None,
212
220
 
@@ -326,6 +334,9 @@ class AudioToTextRecorder:
326
334
  interval in seconds after a chunk of audio gets transcribed. Lower
327
335
  values will result in more "real-time" (frequent) transcription
328
336
  updates but may increase computational load.
337
+ - init_realtime_after_seconds (float, default=0.2): Specifies the
338
+ initial waiting time after the recording was initiated before
339
+ yielding the first realtime transcription
329
340
  - on_realtime_transcription_update = A callback function that is
330
341
  triggered whenever there's an update in the real-time
331
342
  transcription. The function is called with the newly transcribed
@@ -499,6 +510,7 @@ class AudioToTextRecorder:
499
510
  self.main_model_type = model
500
511
  self.realtime_model_type = realtime_model_type
501
512
  self.realtime_processing_pause = realtime_processing_pause
513
+ self.init_realtime_after_seconds = init_realtime_after_seconds
502
514
  self.on_realtime_transcription_update = (
503
515
  on_realtime_transcription_update
504
516
  )
@@ -1619,8 +1631,8 @@ class AudioToTextRecorder:
1619
1631
  # Continuously monitor audio for voice activity
1620
1632
  while self.is_running:
1621
1633
 
1622
- if self.use_extended_logging:
1623
- logging.debug('Debug: Entering inner try block')
1634
+ # if self.use_extended_logging:
1635
+ # logging.debug('Debug: Entering inner try block')
1624
1636
  if last_inner_try_time:
1625
1637
  last_processing_time = time.time() - last_inner_try_time
1626
1638
  if last_processing_time > 0.1:
@@ -1628,20 +1640,20 @@ class AudioToTextRecorder:
1628
1640
  logging.warning('### WARNING: PROCESSING TOOK TOO LONG')
1629
1641
  last_inner_try_time = time.time()
1630
1642
  try:
1631
- if self.use_extended_logging:
1632
- logging.debug('Debug: Trying to get data from audio queue')
1643
+ # if self.use_extended_logging:
1644
+ # logging.debug('Debug: Trying to get data from audio queue')
1633
1645
  try:
1634
1646
  data = self.audio_queue.get(timeout=0.01)
1635
1647
  self.last_words_buffer.append(data)
1636
1648
  except queue.Empty:
1637
- if self.use_extended_logging:
1638
- logging.debug('Debug: Queue is empty, checking if still running')
1649
+ # if self.use_extended_logging:
1650
+ # logging.debug('Debug: Queue is empty, checking if still running')
1639
1651
  if not self.is_running:
1640
1652
  if self.use_extended_logging:
1641
1653
  logging.debug('Debug: Not running, breaking loop')
1642
1654
  break
1643
- if self.use_extended_logging:
1644
- logging.debug('Debug: Continuing to next iteration')
1655
+ # if self.use_extended_logging:
1656
+ # logging.debug('Debug: Continuing to next iteration')
1645
1657
  continue
1646
1658
 
1647
1659
  if self.use_extended_logging:
@@ -2072,7 +2084,7 @@ class AudioToTextRecorder:
2072
2084
  # double check recording state
2073
2085
  # because it could have changed mid-transcription
2074
2086
  if self.is_recording and time.time() - \
2075
- self.recording_start_time > 0.5:
2087
+ self.recording_start_time > self.init_realtime_after_seconds:
2076
2088
 
2077
2089
  # logging.debug('Starting realtime transcription')
2078
2090
  self.realtime_transcription_text = realtime_text
@@ -2179,7 +2191,11 @@ class AudioToTextRecorder:
2179
2191
  SAMPLE_RATE).item()
2180
2192
  is_silero_speech_active = vad_prob > (1 - self.silero_sensitivity)
2181
2193
  if is_silero_speech_active:
2182
- self.is_silero_speech_active = True
2194
+ if not self.is_silero_speech_active and self.use_extended_logging:
2195
+ logging.info(f"{bcolors.OKGREEN}Silero VAD detected speech{bcolors.ENDC}")
2196
+ elif self.is_silero_speech_active and self.use_extended_logging:
2197
+ logging.info(f"{bcolors.WARNING}Silero VAD detected silence{bcolors.ENDC}")
2198
+ self.is_silero_speech_active = is_silero_speech_active
2183
2199
  self.silero_working = False
2184
2200
  return is_silero_speech_active
2185
2201
 
@@ -2191,6 +2207,8 @@ class AudioToTextRecorder:
2191
2207
  data (bytes): raw bytes of audio data (1024 raw bytes with
2192
2208
  16000 sample rate and 16 bits per sample)
2193
2209
  """
2210
+ speech_str = f"{bcolors.OKGREEN}WebRTC VAD detected speech{bcolors.ENDC}"
2211
+ silence_str = f"{bcolors.WARNING}WebRTC VAD detected silence{bcolors.ENDC}"
2194
2212
  if self.sample_rate != 16000:
2195
2213
  pcm_data = np.frombuffer(chunk, dtype=np.int16)
2196
2214
  data_16000 = signal.resample_poly(
@@ -2212,6 +2230,9 @@ class AudioToTextRecorder:
2212
2230
  if self.debug_mode:
2213
2231
  logging.info(f"Speech detected in frame {i + 1}"
2214
2232
  f" of {num_frames}")
2233
+ if not self.is_webrtc_speech_active and self.use_extended_logging:
2234
+ logging.info(speech_str)
2235
+ self.is_webrtc_speech_active = True
2215
2236
  return True
2216
2237
  if all_frames_must_be_true:
2217
2238
  if self.debug_mode and speech_frames == num_frames:
@@ -2219,10 +2240,19 @@ class AudioToTextRecorder:
2219
2240
  f"{num_frames} frames")
2220
2241
  elif self.debug_mode:
2221
2242
  logging.info(f"Speech not detected in all {num_frames} frames")
2222
- return speech_frames == num_frames
2243
+ speech_detected = speech_frames == num_frames
2244
+ if speech_detected and not self.is_webrtc_speech_active and self.use_extended_logging:
2245
+ logging.info(speech_str)
2246
+ elif not speech_detected and self.is_webrtc_speech_active and self.use_extended_logging:
2247
+ logging.info(silence_str)
2248
+ self.is_webrtc_speech_active = speech_detected
2249
+ return speech_detected
2223
2250
  else:
2224
2251
  if self.debug_mode:
2225
2252
  logging.info(f"Speech not detected in any of {num_frames} frames")
2253
+ if self.is_webrtc_speech_active and self.use_extended_logging:
2254
+ logging.info(silence_str)
2255
+ self.is_webrtc_speech_active = False
2226
2256
  return False
2227
2257
 
2228
2258
  def _check_voice_activity(self, data):
@@ -2232,7 +2262,7 @@ class AudioToTextRecorder:
2232
2262
  Args:
2233
2263
  data: The audio data to be checked for voice activity.
2234
2264
  """
2235
- self.is_webrtc_speech_active = self._is_webrtc_speech(data)
2265
+ self._is_webrtc_speech(data)
2236
2266
 
2237
2267
  # First quick performing check for voice activity using WebRTC
2238
2268
  if self.is_webrtc_speech_active:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: RealtimeSTT
3
- Version: 0.3.5
3
+ Version: 0.3.6
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
@@ -55,7 +55,7 @@ https://github.com/user-attachments/assets/797e6552-27cd-41b1-a7f3-e5cbc72094f5
55
55
 
56
56
  ### Updates
57
57
 
58
- Latest Version: v0.3.5
58
+ Latest Version: v0.3.6
59
59
 
60
60
  See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
61
61
 
@@ -1,3 +1,21 @@
1
+ """
2
+ This is a command-line client for the Speech-to-Text (STT) server.
3
+ It records audio from the default input device and sends it to the server for speech recognition.
4
+ It can also process commands to set parameters, get parameter values, or call methods on the server.
5
+
6
+ Usage:
7
+ stt [--control-url CONTROL_URL] [--data-url DATA_URL] [--debug] [--norealtime] [--set-param PARAM VALUE] [--call-method METHOD [ARGS ...]] [--get-param PARAM]
8
+
9
+ Options:
10
+ --control-url CONTROL_URL STT Control WebSocket URL
11
+ --data-url DATA_URL STT Data WebSocket URL
12
+ --debug Enable debug mode
13
+ --norealtime Disable real-time output
14
+ --set-param PARAM VALUE Set a recorder parameter. Can be used multiple times.
15
+ --call-method METHOD [ARGS ...] Call a recorder method with optional arguments.
16
+ --get-param PARAM Get the value of a recorder parameter. Can be used multiple times.
17
+ """
18
+
1
19
  from urllib.parse import urlparse
2
20
  from scipy import signal
3
21
  from queue import Queue
@@ -7,13 +25,13 @@ import threading
7
25
  import websocket
8
26
  import argparse
9
27
  import pyaudio
10
- import logging
11
28
  import struct
12
29
  import socket
13
30
  import shutil
14
31
  import queue
15
32
  import json
16
33
  import time
34
+ import wave
17
35
  import sys
18
36
  import os
19
37
 
@@ -35,7 +53,7 @@ init()
35
53
  websocket.enableTrace(False)
36
54
 
37
55
  class STTWebSocketClient:
38
- def __init__(self, control_url, data_url, debug=False, file_output=None, norealtime=False):
56
+ def __init__(self, control_url, data_url, debug=False, file_output=None, norealtime=False, writechunks=None):
39
57
  self.control_url = control_url
40
58
  self.data_url = data_url
41
59
  self.control_ws = None
@@ -52,6 +70,16 @@ class STTWebSocketClient:
52
70
  self.message_queue = Queue()
53
71
  self.commands = Queue()
54
72
  self.stop_event = threading.Event()
73
+ self.chunks_sent = 0
74
+ self.last_chunk_time = time.time()
75
+ self.writechunks = writechunks # Add this to store the file name for writing audio chunks
76
+
77
+ self.debug_print("Initializing STT WebSocket Client")
78
+ self.debug_print(f"Control URL: {control_url}")
79
+ self.debug_print(f"Data URL: {data_url}")
80
+ self.debug_print(f"File Output: {file_output}")
81
+ self.debug_print(f"No Realtime: {norealtime}")
82
+ self.debug_print(f"Write Chunks: {writechunks}")
55
83
 
56
84
  # Audio attributes
57
85
  self.audio_interface = None
@@ -64,9 +92,12 @@ class STTWebSocketClient:
64
92
  self.data_ws_thread = None
65
93
  self.recording_thread = None
66
94
 
95
+
67
96
  def debug_print(self, message):
68
97
  if self.debug:
69
- print(message, file=sys.stderr)
98
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
99
+ thread_name = threading.current_thread().name
100
+ print(f"{Fore.CYAN}[DEBUG][{timestamp}][{thread_name}] {message}{Style.RESET_ALL}", file=sys.stderr)
70
101
 
71
102
  def connect(self):
72
103
  if not self.ensure_server_running():
@@ -74,7 +105,10 @@ class STTWebSocketClient:
74
105
  return False
75
106
 
76
107
  try:
108
+ self.debug_print("Attempting to establish WebSocket connections...")
109
+
77
110
  # Connect to control WebSocket
111
+ self.debug_print(f"Connecting to control WebSocket at {self.control_url}")
78
112
  self.control_ws = websocket.WebSocketApp(self.control_url,
79
113
  on_message=self.on_control_message,
80
114
  on_error=self.on_error,
@@ -82,10 +116,12 @@ class STTWebSocketClient:
82
116
  on_open=self.on_control_open)
83
117
 
84
118
  self.control_ws_thread = threading.Thread(target=self.control_ws.run_forever)
85
- self.control_ws_thread.daemon = False # Set to False to ensure proper shutdown
119
+ self.control_ws_thread.daemon = False
120
+ self.debug_print("Starting control WebSocket thread")
86
121
  self.control_ws_thread.start()
87
122
 
88
123
  # Connect to data WebSocket
124
+ self.debug_print(f"Connecting to data WebSocket at {self.data_url}")
89
125
  self.data_ws_app = websocket.WebSocketApp(self.data_url,
90
126
  on_message=self.on_data_message,
91
127
  on_error=self.on_error,
@@ -93,10 +129,11 @@ class STTWebSocketClient:
93
129
  on_open=self.on_data_open)
94
130
 
95
131
  self.data_ws_thread = threading.Thread(target=self.data_ws_app.run_forever)
96
- self.data_ws_thread.daemon = False # Set to False to ensure proper shutdown
132
+ self.data_ws_thread.daemon = False
133
+ self.debug_print("Starting data WebSocket thread")
97
134
  self.data_ws_thread.start()
98
135
 
99
- # Wait for the connections to be established
136
+ self.debug_print("Waiting for connections to be established...")
100
137
  if not self.connection_established.wait(timeout=10):
101
138
  self.debug_print("Timeout while connecting to the server.")
102
139
  return False
@@ -104,24 +141,30 @@ class STTWebSocketClient:
104
141
  self.debug_print("WebSocket connections established successfully.")
105
142
  return True
106
143
  except Exception as e:
107
- self.debug_print(f"Error while connecting to the server: {e}")
144
+ self.debug_print(f"Error while connecting to the server: {str(e)}")
108
145
  return False
109
146
 
147
+
110
148
  def on_control_open(self, ws):
111
- self.debug_print("Control WebSocket connection opened.")
149
+ self.debug_print("Control WebSocket connection opened successfully")
112
150
  self.connection_established.set()
113
151
  self.start_command_processor()
114
152
 
115
153
  def on_data_open(self, ws):
116
- self.debug_print("Data WebSocket connection opened.")
117
- self.data_ws_connected = ws # Store the connected websocket object for sending data
154
+ self.debug_print("Data WebSocket connection opened successfully")
155
+ self.data_ws_connected = ws
118
156
  self.start_recording()
119
157
 
120
158
  def on_error(self, ws, error):
121
- self.debug_print(f"WebSocket error: {error}")
159
+ self.debug_print(f"WebSocket error occurred: {str(error)}")
160
+ self.debug_print(f"WebSocket object: {ws}")
161
+ self.debug_print(f"Error type: {type(error)}")
122
162
 
123
163
  def on_close(self, ws, close_status_code, close_msg):
124
- self.debug_print(f"WebSocket connection closed: {close_status_code} - {close_msg}")
164
+ self.debug_print(f"WebSocket connection closed")
165
+ self.debug_print(f"Close status code: {close_status_code}")
166
+ self.debug_print(f"Close message: {close_msg}")
167
+ self.debug_print(f"WebSocket object: {ws}")
125
168
  self.is_running = False
126
169
  self.stop_event.set()
127
170
 
@@ -129,8 +172,11 @@ class STTWebSocketClient:
129
172
  parsed_url = urlparse(self.control_url)
130
173
  host = parsed_url.hostname
131
174
  port = parsed_url.port or 80
175
+ self.debug_print(f"Checking if server is running at {host}:{port}")
132
176
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
133
- return s.connect_ex((host, port)) == 0
177
+ result = s.connect_ex((host, port)) == 0
178
+ self.debug_print(f"Server status check result: {'running' if result else 'not running'}")
179
+ return result
134
180
 
135
181
  def ask_to_start_server(self):
136
182
  response = input("Would you like to start the STT server now? (y/n): ").strip().lower()
@@ -213,37 +259,47 @@ class STTWebSocketClient:
213
259
 
214
260
  def on_control_message(self, ws, message):
215
261
  try:
262
+ self.debug_print(f"Received control message: {message}")
216
263
  data = json.loads(message)
217
264
  if 'status' in data:
265
+ self.debug_print(f"Message status: {data['status']}")
218
266
  if data['status'] == 'success':
219
267
  if 'parameter' in data and 'value' in data:
268
+ self.debug_print(f"Parameter update: {data['parameter']} = {data['value']}")
220
269
  print(f"Parameter {data['parameter']} = {data['value']}")
221
270
  elif data['status'] == 'error':
271
+ self.debug_print(f"Server error received: {data.get('message', '')}")
222
272
  print(f"Server Error: {data.get('message', '')}")
223
273
  else:
224
274
  self.debug_print(f"Unknown control message format: {data}")
225
275
  except json.JSONDecodeError:
226
- self.debug_print(f"Received non-JSON control message: {message}")
276
+ self.debug_print(f"Failed to decode JSON control message: {message}")
227
277
  except Exception as e:
228
- self.debug_print(f"Error processing control message: {e}")
278
+ self.debug_print(f"Error processing control message: {str(e)}")
229
279
 
230
280
  def on_data_message(self, ws, message):
231
281
  try:
282
+ self.debug_print(f"Received data message: {message}")
232
283
  data = json.loads(message)
233
284
  message_type = data.get('type')
285
+ self.debug_print(f"Message type: {message_type}")
286
+
234
287
  if message_type == 'realtime':
235
288
  if data['text'] != self.last_text:
289
+ self.debug_print(f"New realtime text received: {data['text']}")
236
290
  self.last_text = data['text']
237
291
  if not self.norealtime:
238
292
  self.update_progress_bar(self.last_text)
239
293
  elif message_type == 'fullSentence':
294
+ self.debug_print(f"Full sentence received: {data['text']}")
240
295
  if self.file_output:
296
+ self.debug_print("Writing to file output")
241
297
  sys.stderr.write('\r\033[K')
242
298
  sys.stderr.write(data['text'])
243
299
  sys.stderr.write('\n')
244
300
  sys.stderr.flush()
245
301
  print(data['text'], file=self.file_output)
246
- self.file_output.flush() # Ensure it's written immediately
302
+ self.file_output.flush()
247
303
  else:
248
304
  self.finish_progress_bar()
249
305
  print(f"{data['text']}")
@@ -258,14 +314,13 @@ class STTWebSocketClient:
258
314
  'wakeword_detection_end',
259
315
  'transcription_start'}:
260
316
  pass # Known message types, no action needed
261
-
262
317
  else:
263
- self.debug_print(f"Unknown data message format: {data}")
318
+ self.debug_print(f"Other message type received: {message_type}")
264
319
 
265
320
  except json.JSONDecodeError:
266
- self.debug_print(f"Received non-JSON data message: {message}")
321
+ self.debug_print(f"Failed to decode JSON data message: {message}")
267
322
  except Exception as e:
268
- self.debug_print(f"Error processing data message: {e}")
323
+ self.debug_print(f"Error processing data message: {str(e)}")
269
324
 
270
325
  def show_initial_indicator(self):
271
326
  if self.norealtime:
@@ -331,40 +386,67 @@ class STTWebSocketClient:
331
386
  def record_and_send_audio(self):
332
387
  try:
333
388
  if not self.setup_audio():
389
+ self.debug_print("Failed to set up audio recording")
334
390
  raise Exception("Failed to set up audio recording.")
335
391
 
336
- self.debug_print("Recording and sending audio...")
392
+ # Initialize WAV file writer if writechunks is provided
393
+ if self.writechunks:
394
+ self.wav_file = wave.open(self.writechunks, 'wb')
395
+ self.wav_file.setnchannels(CHANNELS)
396
+ self.wav_file.setsampwidth(pyaudio.get_sample_size(FORMAT))
397
+ self.wav_file.setframerate(self.device_sample_rate) # Use self.device_sample_rate
398
+
399
+ self.debug_print("Starting audio recording and transmission")
337
400
  self.show_initial_indicator()
338
401
 
339
402
  while self.is_running:
340
403
  try:
341
404
  audio_data = self.stream.read(CHUNK)
405
+ self.chunks_sent += 1
406
+ current_time = time.time()
407
+ elapsed = current_time - self.last_chunk_time
408
+
409
+ # Write to WAV file if enabled
410
+ if self.writechunks:
411
+ self.wav_file.writeframes(audio_data)
412
+
413
+ if self.chunks_sent % 100 == 0: # Log every 100 chunks
414
+ self.debug_print(f"Sent {self.chunks_sent} chunks. Last chunk took {elapsed:.3f}s")
415
+
342
416
  metadata = {"sampleRate": self.device_sample_rate}
343
417
  metadata_json = json.dumps(metadata)
344
418
  metadata_length = len(metadata_json)
345
419
  message = struct.pack('<I', metadata_length) + metadata_json.encode('utf-8') + audio_data
420
+
421
+ self.debug_print(f"Sending audio chunk {self.chunks_sent}: {len(audio_data)} bytes, metadata: {metadata_json}")
346
422
  self.data_ws_connected.send(message, opcode=websocket.ABNF.OPCODE_BINARY)
423
+ self.last_chunk_time = current_time
424
+
347
425
  except Exception as e:
348
- self.debug_print(f"Error sending audio data: {e}")
349
- break # Exit the recording loop
426
+ self.debug_print(f"Error sending audio data: {str(e)}")
427
+ break
350
428
 
351
429
  except Exception as e:
352
- self.debug_print(f"Error in record_and_send_audio: {e}")
430
+ self.debug_print(f"Error in record_and_send_audio: {str(e)}")
353
431
  finally:
354
432
  self.cleanup_audio()
355
433
 
356
434
  def setup_audio(self):
357
435
  try:
436
+ self.debug_print("Initializing PyAudio interface")
358
437
  self.audio_interface = pyaudio.PyAudio()
359
438
  self.input_device_index = None
439
+
360
440
  try:
361
441
  default_device = self.audio_interface.get_default_input_device_info()
362
442
  self.input_device_index = default_device['index']
443
+ self.debug_print(f"Default input device found: {default_device}")
363
444
  except OSError as e:
364
- self.debug_print(f"No default input device found: {e}")
445
+ self.debug_print(f"No default input device found: {str(e)}")
365
446
  return False
366
447
 
367
- self.device_sample_rate = 16000 # Try 16000 Hz first
448
+ self.device_sample_rate = 16000
449
+ self.debug_print(f"Attempting to open audio stream with sample rate {self.device_sample_rate} Hz")
368
450
 
369
451
  try:
370
452
  self.stream = self.audio_interface.open(
@@ -375,29 +457,36 @@ class STTWebSocketClient:
375
457
  frames_per_buffer=CHUNK,
376
458
  input_device_index=self.input_device_index,
377
459
  )
378
- self.debug_print(f"Audio recording initialized successfully at {self.device_sample_rate} Hz, device index {self.input_device_index}")
460
+ self.debug_print(f"Audio stream initialized successfully")
461
+ self.debug_print(f"Audio parameters: rate={self.device_sample_rate}, channels={CHANNELS}, format={FORMAT}, chunk={CHUNK}")
379
462
  return True
380
463
  except Exception as e:
381
- self.debug_print(f"Failed to initialize audio stream at {self.device_sample_rate} Hz, device index {self.input_device_index}: {e}")
464
+ self.debug_print(f"Failed to initialize audio stream: {str(e)}")
382
465
  return False
383
466
 
384
467
  except Exception as e:
385
- self.debug_print(f"Error initializing audio recording: {e}")
468
+ self.debug_print(f"Error in setup_audio: {str(e)}")
386
469
  if self.audio_interface:
387
470
  self.audio_interface.terminate()
388
471
  return False
389
472
 
390
473
  def cleanup_audio(self):
474
+ self.debug_print("Cleaning up audio resources")
391
475
  try:
392
476
  if self.stream:
477
+ self.debug_print("Stopping and closing audio stream")
393
478
  self.stream.stop_stream()
394
479
  self.stream.close()
395
480
  self.stream = None
396
481
  if self.audio_interface:
482
+ self.debug_print("Terminating PyAudio interface")
397
483
  self.audio_interface.terminate()
398
484
  self.audio_interface = None
485
+ if self.writechunks and self.wav_file:
486
+ self.debug_print("Closing WAV file")
487
+ self.wav_file.close()
399
488
  except Exception as e:
400
- self.debug_print(f"Error cleaning up audio resources: {e}")
489
+ self.debug_print(f"Error during audio cleanup: {str(e)}")
401
490
 
402
491
  def set_parameter(self, parameter, value):
403
492
  command = {
@@ -428,23 +517,28 @@ class STTWebSocketClient:
428
517
  self.command_thread.daemon = False # Ensure it is not a daemon thread
429
518
  self.command_thread.start()
430
519
 
520
+
431
521
  def command_processor(self):
432
- self.debug_print(f"Starting command processor")
522
+ self.debug_print("Starting command processor thread")
433
523
  while not self.stop_event.is_set():
434
524
  try:
435
525
  command = self.commands.get(timeout=0.1)
526
+ self.debug_print(f"Processing command: {command}")
436
527
  if command['type'] == 'set_parameter':
528
+ self.debug_print(f"Setting parameter: {command['parameter']} = {command['value']}")
437
529
  self.set_parameter(command['parameter'], command['value'])
438
530
  elif command['type'] == 'get_parameter':
531
+ self.debug_print(f"Getting parameter: {command['parameter']}")
439
532
  self.get_parameter(command['parameter'])
440
533
  elif command['type'] == 'call_method':
534
+ self.debug_print(f"Calling method: {command['method']} with args: {command.get('args')} and kwargs: {command.get('kwargs')}")
441
535
  self.call_method(command['method'], command.get('args'), command.get('kwargs'))
442
536
  except queue.Empty:
443
537
  continue
444
538
  except Exception as e:
445
- self.debug_print(f"Error in command processor: {e}")
539
+ self.debug_print(f"Error in command processor: {str(e)}")
446
540
 
447
- self.debug_print(f"Leaving command processor")
541
+ self.debug_print("Command processor thread stopping")
448
542
 
449
543
  def add_command(self, command):
450
544
  self.commands.put(command)
@@ -455,6 +549,7 @@ def main():
455
549
  parser.add_argument("--data-url", default=DEFAULT_DATA_URL, help="STT Data WebSocket URL")
456
550
  parser.add_argument("--debug", action="store_true", help="Enable debug mode")
457
551
  parser.add_argument("-nort", "--norealtime", action="store_true", help="Disable real-time output")
552
+ parser.add_argument("--writechunks", metavar="FILE", help="Save recorded audio chunks to a WAV file")
458
553
  parser.add_argument("--set-param", nargs=2, metavar=('PARAM', 'VALUE'), action='append',
459
554
  help="Set a recorder parameter. Can be used multiple times.")
460
555
  parser.add_argument("--call-method", nargs='+', metavar='METHOD', action='append',
@@ -469,7 +564,7 @@ def main():
469
564
  else:
470
565
  file_output = None
471
566
 
472
- client = STTWebSocketClient(args.control_url, args.data_url, args.debug, file_output, args.norealtime)
567
+ client = STTWebSocketClient(args.control_url, args.data_url, args.debug, file_output, args.norealtime, args.writechunks)
473
568
 
474
569
  def signal_handler(sig, frame):
475
570
  client.stop()
@@ -65,18 +65,26 @@ The server supports two WebSocket connections:
65
65
  The server will broadcast real-time transcription updates to all connected clients on the data WebSocket.
66
66
  """
67
67
 
68
+ from .install_packages import check_and_install_packages
69
+ from datetime import datetime
70
+ import logging
71
+ import asyncio
72
+ import pyaudio
73
+ import sys
74
+
68
75
 
69
- extended_logging = True
76
+ debug_logging = False
77
+ extended_logging = False
70
78
  send_recorded_chunk = False
71
- log_incoming_chunks = True
79
+ log_incoming_chunks = False
72
80
  stt_optimizations = False
81
+ writechunks = False#
82
+ wav_file = None
83
+ loglevel = logging.WARNING
73
84
 
85
+ FORMAT = pyaudio.paInt16
86
+ CHANNELS = 1
74
87
 
75
- from .install_packages import check_and_install_packages
76
- from datetime import datetime
77
- import asyncio
78
- import base64
79
- import sys
80
88
 
81
89
  if sys.platform == 'win32':
82
90
  asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
@@ -117,17 +125,25 @@ class bcolors:
117
125
 
118
126
  print(f"{bcolors.BOLD}{bcolors.OKCYAN}Starting server, please wait...{bcolors.ENDC}")
119
127
 
120
- import threading
121
- import json
122
- import websockets
128
+ # Initialize colorama
129
+ from colorama import init, Fore, Style
130
+ init()
131
+
123
132
  from RealtimeSTT import AudioToTextRecorder
124
- import numpy as np
125
133
  from scipy.signal import resample
134
+ import numpy as np
135
+ import websockets
136
+ import threading
137
+ import logging
138
+ import wave
139
+ import json
140
+ import time
126
141
 
127
142
  global_args = None
128
143
  recorder = None
129
144
  recorder_config = {}
130
145
  recorder_ready = threading.Event()
146
+ recorder_thread = None
131
147
  stop_recorder = False
132
148
  prev_text = ""
133
149
 
@@ -174,6 +190,12 @@ def preprocess_text(text):
174
190
 
175
191
  return text
176
192
 
193
+ def debug_print(message):
194
+ if debug_logging:
195
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
196
+ thread_name = threading.current_thread().name
197
+ print(f"{Fore.CYAN}[DEBUG][{timestamp}][{thread_name}] {message}{Style.RESET_ALL}", file=sys.stderr)
198
+
177
199
  def text_detected(text, loop):
178
200
  global prev_text
179
201
 
@@ -279,6 +301,8 @@ def on_transcription_start(loop):
279
301
 
280
302
  # Define the server's arguments
281
303
  def parse_arguments():
304
+ global debug_logging, extended_logging, loglevel, writechunks, log_incoming_chunks
305
+
282
306
  import argparse
283
307
  parser = argparse.ArgumentParser(description='Start the Speech-to-Text (STT) server with various configuration options.')
284
308
 
@@ -376,9 +400,27 @@ def parse_arguments():
376
400
  parser.add_argument('--use_extended_logging', action='store_true',
377
401
  help='Writes extensive log messages for the recording worker, that processes the audio chunks.')
378
402
 
403
+ parser.add_argument('--debug', action='store_true', help='Enable debug logging for detailed server operations')
404
+
405
+ parser.add_argument('--logchunks', action='store_true', help='Enable logging of incoming audio chunks (periods)')
406
+
407
+ parser.add_argument("--writechunks", metavar="FILE", help="Save received audio chunks to a WAV file")
408
+
379
409
  # Parse arguments
380
410
  args = parser.parse_args()
381
411
 
412
+ debug_logging = args.debug
413
+ extended_logging = args.use_extended_logging
414
+ writechunks = args.writechunks
415
+ log_incoming_chunks = args.logchunks
416
+
417
+ if debug_logging:
418
+ loglevel = logging.DEBUG
419
+ logging.basicConfig(level=loglevel, format='[%(asctime)s] %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
420
+ else:
421
+ loglevel = logging.WARNING
422
+
423
+
382
424
  # Replace escaped newlines with actual newlines in initial_prompt
383
425
  if args.initial_prompt:
384
426
  args.initial_prompt = args.initial_prompt.replace("\\n", "\n")
@@ -437,11 +479,13 @@ def decode_and_resample(
437
479
  return resampled_audio.astype(np.int16).tobytes()
438
480
 
439
481
  async def control_handler(websocket, path):
482
+ debug_print(f"New control connection from {websocket.remote_address}")
440
483
  print(f"{bcolors.OKGREEN}Control client connected{bcolors.ENDC}")
441
484
  global recorder
442
485
  control_connections.add(websocket)
443
486
  try:
444
487
  async for message in websocket:
488
+ debug_print(f"Received control message: {message[:200]}...")
445
489
  if not recorder_ready.is_set():
446
490
  print(f"{bcolors.WARNING}Recorder not ready{bcolors.ENDC}")
447
491
  continue
@@ -530,21 +574,38 @@ async def control_handler(websocket, path):
530
574
  control_connections.remove(websocket)
531
575
 
532
576
  async def data_handler(websocket, path):
577
+ global writechunks, wav_file
533
578
  print(f"{bcolors.OKGREEN}Data client connected{bcolors.ENDC}")
534
579
  data_connections.add(websocket)
535
580
  try:
536
581
  while True:
537
582
  message = await websocket.recv()
538
583
  if isinstance(message, bytes):
539
- if log_incoming_chunks:
584
+ if debug_logging:
585
+ debug_print(f"Received audio chunk (size: {len(message)} bytes)")
586
+ elif log_incoming_chunks:
540
587
  print(".", end='', flush=True)
541
588
  # Handle binary message (audio data)
542
589
  metadata_length = int.from_bytes(message[:4], byteorder='little')
543
590
  metadata_json = message[4:4+metadata_length].decode('utf-8')
544
591
  metadata = json.loads(metadata_json)
545
592
  sample_rate = metadata['sampleRate']
593
+
594
+ debug_print(f"Processing audio chunk with sample rate {sample_rate}")
546
595
  chunk = message[4+metadata_length:]
596
+
597
+ if writechunks:
598
+ if not wav_file:
599
+ wav_file = wave.open(writechunks, 'wb')
600
+ wav_file.setnchannels(CHANNELS)
601
+ wav_file.setsampwidth(pyaudio.get_sample_size(FORMAT))
602
+ wav_file.setframerate(sample_rate)
603
+
604
+ wav_file.writeframes(chunk)
605
+
547
606
  resampled_chunk = decode_and_resample(chunk, sample_rate, 16000)
607
+
608
+ debug_print(f"Resampled chunk size: {len(resampled_chunk)} bytes")
548
609
  recorder.feed_audio(resampled_chunk)
549
610
  else:
550
611
  print(f"{bcolors.WARNING}Received non-binary message on data connection{bcolors.ENDC}")
@@ -622,47 +683,55 @@ async def main_async():
622
683
  # 'on_recorded_chunk': make_callback(loop, on_recorded_chunk),
623
684
  'no_log_file': True, # Disable logging to file
624
685
  'use_extended_logging': args.use_extended_logging,
686
+ 'level': loglevel,
625
687
  }
626
688
 
627
- control_server = await websockets.serve(control_handler, "localhost", args.control_port)
628
- data_server = await websockets.serve(data_handler, "localhost", args.data_port)
629
- print(f"{bcolors.OKGREEN}Control server started on {bcolors.OKBLUE}ws://localhost:{args.control_port}{bcolors.ENDC}")
630
- print(f"{bcolors.OKGREEN}Data server started on {bcolors.OKBLUE}ws://localhost:{args.data_port}{bcolors.ENDC}")
689
+ try:
690
+ # Attempt to start control and data servers
691
+ control_server = await websockets.serve(control_handler, "localhost", args.control_port)
692
+ data_server = await websockets.serve(data_handler, "localhost", args.data_port)
693
+ print(f"{bcolors.OKGREEN}Control server started on {bcolors.OKBLUE}ws://localhost:{args.control_port}{bcolors.ENDC}")
694
+ print(f"{bcolors.OKGREEN}Data server started on {bcolors.OKBLUE}ws://localhost:{args.data_port}{bcolors.ENDC}")
631
695
 
632
- # Task to broadcast audio messages
633
- broadcast_task = asyncio.create_task(broadcast_audio_messages())
696
+ # Start the broadcast and recorder threads
697
+ broadcast_task = asyncio.create_task(broadcast_audio_messages())
634
698
 
635
- recorder_thread = threading.Thread(target=_recorder_thread, args=(loop,))
636
- recorder_thread.start()
637
- recorder_ready.wait()
699
+ recorder_thread = threading.Thread(target=_recorder_thread, args=(loop,))
700
+ recorder_thread.start()
701
+ recorder_ready.wait()
638
702
 
639
- print(f"{bcolors.OKGREEN}Server started. Press Ctrl+C to stop the server.{bcolors.ENDC}")
703
+ print(f"{bcolors.OKGREEN}Server started. Press Ctrl+C to stop the server.{bcolors.ENDC}")
640
704
 
641
- try:
705
+ # Run server tasks
642
706
  await asyncio.gather(control_server.wait_closed(), data_server.wait_closed(), broadcast_task)
707
+ except OSError as e:
708
+ print(f"{bcolors.FAIL}Error: Could not start server on specified ports. It’s possible another instance of the server is already running, or the ports are being used by another application.{bcolors.ENDC}")
643
709
  except KeyboardInterrupt:
644
- print(f"{bcolors.WARNING}{bcolors.BOLD}Shutting down gracefully...{bcolors.ENDC}")
710
+ print(f"{bcolors.WARNING}Server interrupted by user, shutting down...{bcolors.ENDC}")
645
711
  finally:
646
- # Shut down the recorder
647
- if recorder:
648
- stop_recorder = True
649
- recorder.abort()
650
- recorder.stop()
651
- recorder.shutdown()
652
- print(f"{bcolors.OKGREEN}Recorder shut down{bcolors.ENDC}")
653
-
712
+ # Shutdown procedures for recorder and server threads
713
+ await shutdown_procedure()
714
+ print(f"{bcolors.OKGREEN}Server shutdown complete.{bcolors.ENDC}")
715
+
716
+ async def shutdown_procedure():
717
+ global stop_recorder, recorder_thread
718
+ if recorder:
719
+ stop_recorder = True
720
+ recorder.abort()
721
+ recorder.stop()
722
+ recorder.shutdown()
723
+ print(f"{bcolors.OKGREEN}Recorder shut down{bcolors.ENDC}")
724
+
725
+ if recorder_thread:
654
726
  recorder_thread.join()
655
727
  print(f"{bcolors.OKGREEN}Recorder thread finished{bcolors.ENDC}")
656
-
657
- # Cancel all active tasks in the event loop
658
- tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
659
- for task in tasks:
660
- task.cancel()
661
-
662
- # Run pending tasks and handle cancellation
663
- await asyncio.gather(*tasks, return_exceptions=True)
664
-
665
- print(f"{bcolors.OKGREEN}All tasks cancelled, closing event loop now.{bcolors.ENDC}")
728
+
729
+ tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
730
+ for task in tasks:
731
+ task.cancel()
732
+ await asyncio.gather(*tasks, return_exceptions=True)
733
+
734
+ print(f"{bcolors.OKGREEN}All tasks cancelled, closing event loop now.{bcolors.ENDC}")
666
735
 
667
736
  def main():
668
737
  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.5",
12
+ version="0.3.6",
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