RealTimeSTT 0.3.5__tar.gz → 0.3.7__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.7
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.7
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.7
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.7
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.7
59
59
 
60
60
  See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
61
61
 
@@ -1,19 +1,37 @@
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
+ - `-c, --control, --control_url`: STT Control WebSocket URL; default `DEFAULT_CONTROL_URL`.
11
+ - `-d, --data, --data_url`: STT Data WebSocket URL; default `DEFAULT_DATA_URL`.
12
+ - `-D, --debug`: Enable debug mode.
13
+ - `-n, --norealtime`: Disable real-time output.
14
+ - `-W, --write`: Save recorded audio to a WAV file.
15
+ - `-s, --set`: Set a recorder parameter with format `PARAM VALUE`; can be used multiple times.
16
+ - `-m, --method`: Call a recorder method with optional arguments; can be used multiple times.
17
+ - `-g, --get`: Get a recorder parameter's value; can be used multiple times.
18
+ - `-l, --loop`: Continuously transcribe speech without exiting.
19
+ """
20
+
1
21
  from urllib.parse import urlparse
2
- from scipy import signal
3
22
  from queue import Queue
4
- import numpy as np
5
23
  import subprocess
6
24
  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,12 +53,12 @@ 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, continuous=False):
39
57
  self.control_url = control_url
40
58
  self.data_url = data_url
41
59
  self.control_ws = None
42
60
  self.data_ws_app = None
43
- self.data_ws_connected = None # WebSocket object that will be used for sending
61
+ self.data_ws_connected = None
44
62
  self.is_running = True
45
63
  self.debug = debug
46
64
  self.file_output = file_output
@@ -52,6 +70,18 @@ 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
76
+ self.continuous = continuous
77
+
78
+ self.debug_print("Initializing STT WebSocket Client")
79
+ self.debug_print(f"Control URL: {control_url}")
80
+ self.debug_print(f"Data URL: {data_url}")
81
+ self.debug_print(f"File Output: {file_output}")
82
+ self.debug_print(f"No Realtime: {norealtime}")
83
+ self.debug_print(f"Write Chunks: {writechunks}")
84
+ self.debug_print(f"Continuous Mode: {continuous}")
55
85
 
56
86
  # Audio attributes
57
87
  self.audio_interface = None
@@ -64,9 +94,12 @@ class STTWebSocketClient:
64
94
  self.data_ws_thread = None
65
95
  self.recording_thread = None
66
96
 
97
+
67
98
  def debug_print(self, message):
68
99
  if self.debug:
69
- print(message, file=sys.stderr)
100
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
101
+ thread_name = threading.current_thread().name
102
+ print(f"{Fore.CYAN}[DEBUG][{timestamp}][{thread_name}] {message}{Style.RESET_ALL}", file=sys.stderr)
70
103
 
71
104
  def connect(self):
72
105
  if not self.ensure_server_running():
@@ -74,7 +107,10 @@ class STTWebSocketClient:
74
107
  return False
75
108
 
76
109
  try:
110
+ self.debug_print("Attempting to establish WebSocket connections...")
111
+
77
112
  # Connect to control WebSocket
113
+ self.debug_print(f"Connecting to control WebSocket at {self.control_url}")
78
114
  self.control_ws = websocket.WebSocketApp(self.control_url,
79
115
  on_message=self.on_control_message,
80
116
  on_error=self.on_error,
@@ -82,10 +118,12 @@ class STTWebSocketClient:
82
118
  on_open=self.on_control_open)
83
119
 
84
120
  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
121
+ self.control_ws_thread.daemon = False
122
+ self.debug_print("Starting control WebSocket thread")
86
123
  self.control_ws_thread.start()
87
124
 
88
125
  # Connect to data WebSocket
126
+ self.debug_print(f"Connecting to data WebSocket at {self.data_url}")
89
127
  self.data_ws_app = websocket.WebSocketApp(self.data_url,
90
128
  on_message=self.on_data_message,
91
129
  on_error=self.on_error,
@@ -93,10 +131,11 @@ class STTWebSocketClient:
93
131
  on_open=self.on_data_open)
94
132
 
95
133
  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
134
+ self.data_ws_thread.daemon = False
135
+ self.debug_print("Starting data WebSocket thread")
97
136
  self.data_ws_thread.start()
98
137
 
99
- # Wait for the connections to be established
138
+ self.debug_print("Waiting for connections to be established...")
100
139
  if not self.connection_established.wait(timeout=10):
101
140
  self.debug_print("Timeout while connecting to the server.")
102
141
  return False
@@ -104,24 +143,31 @@ class STTWebSocketClient:
104
143
  self.debug_print("WebSocket connections established successfully.")
105
144
  return True
106
145
  except Exception as e:
107
- self.debug_print(f"Error while connecting to the server: {e}")
146
+ self.debug_print(f"Error while connecting to the server: {str(e)}")
108
147
  return False
109
148
 
110
149
  def on_control_open(self, ws):
111
- self.debug_print("Control WebSocket connection opened.")
150
+ self.debug_print("Control WebSocket connection opened successfully")
112
151
  self.connection_established.set()
113
152
  self.start_command_processor()
114
153
 
115
154
  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
155
+ self.debug_print("Data WebSocket connection opened successfully")
156
+ self.data_ws_connected = ws
118
157
  self.start_recording()
119
158
 
120
159
  def on_error(self, ws, error):
121
- self.debug_print(f"WebSocket error: {error}")
160
+ self.debug_print(f"WebSocket error occurred: {str(error)}")
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
+ if ws == self.data_ws_connected:
165
+ self.debug_print(f"Data connection closed (code {close_status_code}, msg: {close_msg})")
166
+ elif ws == self.control_ws:
167
+ self.debug_print(f"Control connection closed (code {close_status_code}, msg: {close_msg})")
168
+ else:
169
+ self.debug_print(f"Unknown connection closed (code {close_status_code}, msg: {close_msg})")
170
+
125
171
  self.is_running = False
126
172
  self.stop_event.set()
127
173
 
@@ -129,8 +175,11 @@ class STTWebSocketClient:
129
175
  parsed_url = urlparse(self.control_url)
130
176
  host = parsed_url.hostname
131
177
  port = parsed_url.port or 80
178
+ self.debug_print(f"Checking if server is running at {host}:{port}")
132
179
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
133
- return s.connect_ex((host, port)) == 0
180
+ result = s.connect_ex((host, port)) == 0
181
+ self.debug_print(f"Server status check result: {'running' if result else 'not running'}")
182
+ return result
134
183
 
135
184
  def ask_to_start_server(self):
136
185
  response = input("Would you like to start the STT server now? (y/n): ").strip().lower()
@@ -213,41 +262,56 @@ class STTWebSocketClient:
213
262
 
214
263
  def on_control_message(self, ws, message):
215
264
  try:
265
+ self.debug_print(f"Received control message: {message}")
216
266
  data = json.loads(message)
217
267
  if 'status' in data:
268
+ self.debug_print(f"Message status: {data['status']}")
218
269
  if data['status'] == 'success':
219
270
  if 'parameter' in data and 'value' in data:
271
+ self.debug_print(f"Parameter update: {data['parameter']} = {data['value']}")
220
272
  print(f"Parameter {data['parameter']} = {data['value']}")
221
273
  elif data['status'] == 'error':
274
+ self.debug_print(f"Server error received: {data.get('message', '')}")
222
275
  print(f"Server Error: {data.get('message', '')}")
223
276
  else:
224
277
  self.debug_print(f"Unknown control message format: {data}")
225
278
  except json.JSONDecodeError:
226
- self.debug_print(f"Received non-JSON control message: {message}")
279
+ self.debug_print(f"Failed to decode JSON control message: {message}")
227
280
  except Exception as e:
228
- self.debug_print(f"Error processing control message: {e}")
281
+ self.debug_print(f"Error processing control message: {str(e)}")
229
282
 
230
283
  def on_data_message(self, ws, message):
231
284
  try:
285
+ self.debug_print(f"Received data message: {message}")
232
286
  data = json.loads(message)
233
287
  message_type = data.get('type')
288
+ self.debug_print(f"Message type: {message_type}")
289
+
234
290
  if message_type == 'realtime':
235
291
  if data['text'] != self.last_text:
292
+ self.debug_print(f"New realtime text received: {data['text']}")
236
293
  self.last_text = data['text']
237
294
  if not self.norealtime:
238
295
  self.update_progress_bar(self.last_text)
296
+
239
297
  elif message_type == 'fullSentence':
298
+ self.debug_print(f"Full sentence received: {data['text']}")
240
299
  if self.file_output:
300
+ self.debug_print("Writing to file output")
241
301
  sys.stderr.write('\r\033[K')
242
302
  sys.stderr.write(data['text'])
243
303
  sys.stderr.write('\n')
244
304
  sys.stderr.flush()
245
305
  print(data['text'], file=self.file_output)
246
- self.file_output.flush() # Ensure it's written immediately
306
+ self.file_output.flush()
247
307
  else:
248
308
  self.finish_progress_bar()
249
309
  print(f"{data['text']}")
250
- self.stop()
310
+
311
+ if not self.continuous:
312
+ self.is_running = False
313
+ self.stop_event.set()
314
+
251
315
  elif message_type in {
252
316
  'vad_detect_start',
253
317
  'vad_detect_stop',
@@ -258,14 +322,13 @@ class STTWebSocketClient:
258
322
  'wakeword_detection_end',
259
323
  'transcription_start'}:
260
324
  pass # Known message types, no action needed
261
-
262
325
  else:
263
- self.debug_print(f"Unknown data message format: {data}")
326
+ self.debug_print(f"Other message type received: {message_type}")
264
327
 
265
328
  except json.JSONDecodeError:
266
- self.debug_print(f"Received non-JSON data message: {message}")
329
+ self.debug_print(f"Failed to decode JSON data message: {message}")
267
330
  except Exception as e:
268
- self.debug_print(f"Error processing data message: {e}")
331
+ self.debug_print(f"Error processing data message: {str(e)}")
269
332
 
270
333
  def show_initial_indicator(self):
271
334
  if self.norealtime:
@@ -309,11 +372,12 @@ class STTWebSocketClient:
309
372
  self.data_ws_connected.close()
310
373
 
311
374
  # Join threads to ensure they finish before exiting
312
- if self.control_ws_thread:
375
+ current_thread = threading.current_thread()
376
+ if self.control_ws_thread and self.control_ws_thread != current_thread:
313
377
  self.control_ws_thread.join()
314
- if self.data_ws_thread:
378
+ if self.data_ws_thread and self.data_ws_thread != current_thread:
315
379
  self.data_ws_thread.join()
316
- if self.recording_thread:
380
+ if self.recording_thread and self.recording_thread != current_thread:
317
381
  self.recording_thread.join()
318
382
 
319
383
  # Clean up audio resources
@@ -331,40 +395,69 @@ class STTWebSocketClient:
331
395
  def record_and_send_audio(self):
332
396
  try:
333
397
  if not self.setup_audio():
398
+ self.debug_print("Failed to set up audio recording")
334
399
  raise Exception("Failed to set up audio recording.")
335
400
 
336
- self.debug_print("Recording and sending audio...")
401
+ # Initialize WAV file writer if writechunks is provided
402
+ if self.writechunks:
403
+ self.wav_file = wave.open(self.writechunks, 'wb')
404
+ self.wav_file.setnchannels(CHANNELS)
405
+ self.wav_file.setsampwidth(pyaudio.get_sample_size(FORMAT))
406
+ self.wav_file.setframerate(self.device_sample_rate) # Use self.device_sample_rate
407
+
408
+ self.debug_print("Starting audio recording and transmission")
337
409
  self.show_initial_indicator()
338
410
 
339
- while self.is_running:
411
+ while self.is_running and not self.stop_event.is_set():
340
412
  try:
341
413
  audio_data = self.stream.read(CHUNK)
414
+ self.chunks_sent += 1
415
+ current_time = time.time()
416
+ elapsed = current_time - self.last_chunk_time
417
+
418
+ # Write to WAV file if enabled
419
+ if self.writechunks:
420
+ self.wav_file.writeframes(audio_data)
421
+
422
+ if self.chunks_sent % 100 == 0: # Log every 100 chunks
423
+ self.debug_print(f"Sent {self.chunks_sent} chunks. Last chunk took {elapsed:.3f}s")
424
+
342
425
  metadata = {"sampleRate": self.device_sample_rate}
343
426
  metadata_json = json.dumps(metadata)
344
427
  metadata_length = len(metadata_json)
345
428
  message = struct.pack('<I', metadata_length) + metadata_json.encode('utf-8') + audio_data
346
- self.data_ws_connected.send(message, opcode=websocket.ABNF.OPCODE_BINARY)
429
+
430
+ if self.is_running and not self.stop_event.is_set():
431
+ self.debug_print(f"Sending audio chunk {self.chunks_sent}: {len(audio_data)} bytes, metadata: {metadata_json}")
432
+ self.data_ws_connected.send(message, opcode=websocket.ABNF.OPCODE_BINARY)
433
+
434
+ self.last_chunk_time = current_time
435
+
347
436
  except Exception as e:
348
- self.debug_print(f"Error sending audio data: {e}")
349
- break # Exit the recording loop
437
+ self.debug_print(f"Error sending audio data: {str(e)}")
438
+ break
350
439
 
351
440
  except Exception as e:
352
- self.debug_print(f"Error in record_and_send_audio: {e}")
441
+ self.debug_print(f"Error in record_and_send_audio: {str(e)}")
353
442
  finally:
354
443
  self.cleanup_audio()
355
444
 
356
445
  def setup_audio(self):
357
446
  try:
447
+ self.debug_print("Initializing PyAudio interface")
358
448
  self.audio_interface = pyaudio.PyAudio()
359
449
  self.input_device_index = None
450
+
360
451
  try:
361
452
  default_device = self.audio_interface.get_default_input_device_info()
362
453
  self.input_device_index = default_device['index']
454
+ self.debug_print(f"Default input device found: {default_device}")
363
455
  except OSError as e:
364
- self.debug_print(f"No default input device found: {e}")
456
+ self.debug_print(f"No default input device found: {str(e)}")
365
457
  return False
366
458
 
367
- self.device_sample_rate = 16000 # Try 16000 Hz first
459
+ self.device_sample_rate = 16000
460
+ self.debug_print(f"Attempting to open audio stream with sample rate {self.device_sample_rate} Hz")
368
461
 
369
462
  try:
370
463
  self.stream = self.audio_interface.open(
@@ -375,29 +468,36 @@ class STTWebSocketClient:
375
468
  frames_per_buffer=CHUNK,
376
469
  input_device_index=self.input_device_index,
377
470
  )
378
- self.debug_print(f"Audio recording initialized successfully at {self.device_sample_rate} Hz, device index {self.input_device_index}")
471
+ self.debug_print(f"Audio stream initialized successfully")
472
+ self.debug_print(f"Audio parameters: rate={self.device_sample_rate}, channels={CHANNELS}, format={FORMAT}, chunk={CHUNK}")
379
473
  return True
380
474
  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}")
475
+ self.debug_print(f"Failed to initialize audio stream: {str(e)}")
382
476
  return False
383
477
 
384
478
  except Exception as e:
385
- self.debug_print(f"Error initializing audio recording: {e}")
479
+ self.debug_print(f"Error in setup_audio: {str(e)}")
386
480
  if self.audio_interface:
387
481
  self.audio_interface.terminate()
388
482
  return False
389
483
 
390
484
  def cleanup_audio(self):
485
+ self.debug_print("Cleaning up audio resources")
391
486
  try:
392
487
  if self.stream:
488
+ self.debug_print("Stopping and closing audio stream")
393
489
  self.stream.stop_stream()
394
490
  self.stream.close()
395
491
  self.stream = None
396
492
  if self.audio_interface:
493
+ self.debug_print("Terminating PyAudio interface")
397
494
  self.audio_interface.terminate()
398
495
  self.audio_interface = None
496
+ if self.writechunks and self.wav_file:
497
+ self.debug_print("Closing WAV file")
498
+ self.wav_file.close()
399
499
  except Exception as e:
400
- self.debug_print(f"Error cleaning up audio resources: {e}")
500
+ self.debug_print(f"Error during audio cleanup: {str(e)}")
401
501
 
402
502
  def set_parameter(self, parameter, value):
403
503
  command = {
@@ -428,39 +528,54 @@ class STTWebSocketClient:
428
528
  self.command_thread.daemon = False # Ensure it is not a daemon thread
429
529
  self.command_thread.start()
430
530
 
531
+
431
532
  def command_processor(self):
432
- self.debug_print(f"Starting command processor")
533
+ self.debug_print("Starting command processor thread")
433
534
  while not self.stop_event.is_set():
434
535
  try:
435
536
  command = self.commands.get(timeout=0.1)
537
+ self.debug_print(f"Processing command: {command}")
436
538
  if command['type'] == 'set_parameter':
539
+ self.debug_print(f"Setting parameter: {command['parameter']} = {command['value']}")
437
540
  self.set_parameter(command['parameter'], command['value'])
438
541
  elif command['type'] == 'get_parameter':
542
+ self.debug_print(f"Getting parameter: {command['parameter']}")
439
543
  self.get_parameter(command['parameter'])
440
544
  elif command['type'] == 'call_method':
545
+ self.debug_print(f"Calling method: {command['method']} with args: {command.get('args')} and kwargs: {command.get('kwargs')}")
441
546
  self.call_method(command['method'], command.get('args'), command.get('kwargs'))
442
547
  except queue.Empty:
443
548
  continue
444
549
  except Exception as e:
445
- self.debug_print(f"Error in command processor: {e}")
550
+ self.debug_print(f"Error in command processor: {str(e)}")
446
551
 
447
- self.debug_print(f"Leaving command processor")
552
+ self.debug_print("Command processor thread stopping")
448
553
 
449
554
  def add_command(self, command):
450
555
  self.commands.put(command)
451
556
 
452
557
  def main():
453
558
  parser = argparse.ArgumentParser(description="STT Client")
454
- parser.add_argument("--control-url", default=DEFAULT_CONTROL_URL, help="STT Control WebSocket URL")
455
- parser.add_argument("--data-url", default=DEFAULT_DATA_URL, help="STT Data WebSocket URL")
456
- parser.add_argument("--debug", action="store_true", help="Enable debug mode")
457
- parser.add_argument("-nort", "--norealtime", action="store_true", help="Disable real-time output")
458
- parser.add_argument("--set-param", nargs=2, metavar=('PARAM', 'VALUE'), action='append',
459
- help="Set a recorder parameter. Can be used multiple times.")
460
- parser.add_argument("--call-method", nargs='+', metavar='METHOD', action='append',
461
- help="Call a recorder method with optional arguments.")
462
- parser.add_argument("--get-param", nargs=1, metavar='PARAM', action='append',
463
- help="Get the value of a recorder parameter. Can be used multiple times.")
559
+
560
+ parser.add_argument("-c", "--control", "--control_url", default=DEFAULT_CONTROL_URL,
561
+ help="STT Control WebSocket URL")
562
+ parser.add_argument("-d", "--data", "--data_url", default=DEFAULT_DATA_URL,
563
+ help="STT Data WebSocket URL")
564
+ parser.add_argument("-D", "--debug", action="store_true",
565
+ help="Enable debug mode")
566
+ parser.add_argument("-n", "--norealtime", action="store_true",
567
+ help="Disable real-time output")
568
+ parser.add_argument("-W", "--write", metavar="FILE",
569
+ help="Save recorded audio to a WAV file")
570
+ parser.add_argument("-s", "--set", nargs=2, metavar=('PARAM', 'VALUE'), action='append',
571
+ help="Set a recorder parameter (can be used multiple times)")
572
+ parser.add_argument("-m", "--method", nargs='+', metavar='METHOD', action='append',
573
+ help="Call a recorder method with optional arguments")
574
+ parser.add_argument("-g", "--get", nargs=1, metavar='PARAM', action='append',
575
+ help="Get a recorder parameter's value (can be used multiple times)")
576
+ parser.add_argument("-l", "--loop", action="store_true",
577
+ help="Continuously transcribe speech without exiting")
578
+
464
579
  args = parser.parse_args()
465
580
 
466
581
  # Check if output is being redirected
@@ -469,7 +584,16 @@ def main():
469
584
  else:
470
585
  file_output = None
471
586
 
472
- client = STTWebSocketClient(args.control_url, args.data_url, args.debug, file_output, args.norealtime)
587
+
588
+ client = STTWebSocketClient(
589
+ args.control,
590
+ args.data,
591
+ args.debug,
592
+ file_output,
593
+ args.norealtime, # Adjusted logic for real-time output
594
+ args.write,
595
+ continuous=args.loop
596
+ )
473
597
 
474
598
  def signal_handler(sig, frame):
475
599
  client.stop()
@@ -481,8 +605,8 @@ def main():
481
605
  try:
482
606
  if client.connect():
483
607
  # Process command-line parameters
484
- if args.set_param:
485
- for param, value in args.set_param:
608
+ if args.set:
609
+ for param, value in args.set:
486
610
  try:
487
611
  if '.' in value:
488
612
  value = float(value)
@@ -497,16 +621,16 @@ def main():
497
621
  'value': value
498
622
  })
499
623
 
500
- if args.get_param:
501
- for param_list in args.get_param:
624
+ if args.get:
625
+ for param_list in args.get:
502
626
  param = param_list[0]
503
627
  client.add_command({
504
628
  'type': 'get_parameter',
505
629
  'parameter': param
506
630
  })
507
631
 
508
- if args.call_method:
509
- for method_call in args.call_method:
632
+ if args.method:
633
+ for method_call in args.method:
510
634
  method = method_call[0]
511
635
  args_list = method_call[1:] if len(method_call) > 1 else []
512
636
  client.add_command({
@@ -516,7 +640,7 @@ def main():
516
640
  })
517
641
 
518
642
  # If command-line parameters were used (like --get-param), wait for them to be processed
519
- if args.set_param or args.get_param or args.call_method:
643
+ if args.set or args.get or args.method:
520
644
  while not client.commands.empty():
521
645
  time.sleep(0.1)
522
646
 
@@ -533,4 +657,3 @@ def main():
533
657
 
534
658
  if __name__ == "__main__":
535
659
  main()
536
-
@@ -17,45 +17,40 @@ stt-server [OPTIONS]
17
17
  ```
18
18
 
19
19
  ### Available Parameters:
20
- - `--model` (str, default: 'medium.en'): Path to the STT model or model size. Options: tiny, tiny.en, base, base.en, small, small.en, medium, medium.en, large-v1, large-v2, or any huggingface CTranslate2 STT model like `deepdml/faster-whisper-large-v3-turbo-ct2`.
21
-
22
- - `--realtime_model_type` (str, default: 'tiny.en'): Model size for real-time transcription. Same options as `--model`.
23
-
24
- - `--language` (str, default: 'en'): Language code for the STT model. Leave empty for auto-detection.
25
-
26
- - `--input_device_index` (int, default: 1): Index of the audio input device to use.
27
-
28
- - `--silero_sensitivity` (float, default: 0.05): Sensitivity for Silero Voice Activity Detection (VAD). Lower values are less sensitive.
29
-
30
- - `--webrtc_sensitivity` (int, default: 3): Sensitivity for WebRTC VAD. Higher values are less sensitive.
31
-
32
- - `--min_length_of_recording` (float, default: 1.1): Minimum duration (in seconds) for a valid recording. Prevents short recordings.
33
-
34
- - `--min_gap_between_recordings` (float, default: 0): Minimum time (in seconds) between consecutive recordings.
35
-
36
- - `--enable_realtime_transcription` (flag, default: True): Enable real-time transcription of audio.
37
-
38
- - `--realtime_processing_pause` (float, default: 0.02): Time interval (in seconds) between processing audio chunks for real-time transcription. Lower values increase responsiveness.
39
-
40
- - `--silero_deactivity_detection` (flag, default: True): Use Silero model for end-of-speech detection.
41
-
42
- - `--early_transcription_on_silence` (float, default: 0.2): Start transcription after specified seconds of silence.
43
-
44
- - `--beam_size` (int, default: 5): Beam size for the main transcription model.
45
-
46
- - `--beam_size_realtime` (int, default: 3): Beam size for the real-time transcription model.
47
-
48
- - `--initial_prompt` (str, default: '...'): Initial prompt for the transcription model to guide its output format and style.
49
-
50
- - `--end_of_sentence_detection_pause` (float, default: 0.45): Duration of pause (in seconds) to consider as the end of a sentence.
51
-
52
- - `--unknown_sentence_detection_pause` (float, default: 0.7): Duration of pause (in seconds) to consider as an unknown or incomplete sentence.
53
-
54
- - `--mid_sentence_detection_pause` (float, default: 2.0): Duration of pause (in seconds) to consider as a mid-sentence break.
55
-
56
- - `--control_port` (int, default: 8011): Port for the control WebSocket connection.
57
-
58
- - `--data_port` (int, default: 8012): Port for the data WebSocket connection.
20
+ - `-m, --model`: Model path or size; default 'large-v2'.
21
+ - `-r, --rt-model, --realtime_model_type`: Real-time model size; default 'tiny.en'.
22
+ - `-l, --lang, --language`: Language code for transcription; default 'en'.
23
+ - `-i, --input-device, --input_device_index`: Audio input device index; default 1.
24
+ - `-c, --control, --control_port`: WebSocket control port; default 8011.
25
+ - `-d, --data, --data_port`: WebSocket data port; default 8012.
26
+ - `-w, --wake_words`: Wake word(s) to trigger listening; default "".
27
+ - `-D, --debug`: Enable debug logging.
28
+ - `-W, --write`: Save audio to WAV file.
29
+ - `--silero_sensitivity`: Silero VAD sensitivity (0-1); default 0.05.
30
+ - `--silero_use_onnx`: Use Silero ONNX model; default False.
31
+ - `--webrtc_sensitivity`: WebRTC VAD sensitivity (0-3); default 3.
32
+ - `--min_length_of_recording`: Minimum recording duration in seconds; default 1.1.
33
+ - `--min_gap_between_recordings`: Min time between recordings in seconds; default 0.
34
+ - `--enable_realtime_transcription`: Enable real-time transcription; default True.
35
+ - `--realtime_processing_pause`: Pause between audio chunk processing; default 0.02.
36
+ - `--silero_deactivity_detection`: Use Silero for end-of-speech detection; default True.
37
+ - `--early_transcription_on_silence`: Start transcription after silence in seconds; default 0.2.
38
+ - `--beam_size`: Beam size for main model; default 5.
39
+ - `--beam_size_realtime`: Beam size for real-time model; default 3.
40
+ - `--initial_prompt`: Initial transcription guidance prompt.
41
+ - `--end_of_sentence_detection_pause`: Silence duration for sentence end detection; default 0.45.
42
+ - `--unknown_sentence_detection_pause`: Pause duration for incomplete sentence detection; default 0.7.
43
+ - `--mid_sentence_detection_pause`: Pause for mid-sentence break; default 2.0.
44
+ - `--wake_words_sensitivity`: Wake word detection sensitivity (0-1); default 0.5.
45
+ - `--wake_word_timeout`: Wake word timeout in seconds; default 5.0.
46
+ - `--wake_word_activation_delay`: Delay before wake word activation; default 20.
47
+ - `--wakeword_backend`: Backend for wake word detection; default 'none'.
48
+ - `--openwakeword_model_paths`: Paths to OpenWakeWord models.
49
+ - `--openwakeword_inference_framework`: OpenWakeWord inference framework; default 'tensorflow'.
50
+ - `--wake_word_buffer_duration`: Wake word buffer duration in seconds; default 1.0.
51
+ - `--use_main_model_for_realtime`: Use main model for real-time transcription.
52
+ - `--use_extended_logging`: Enable extensive log messages.
53
+ - `--logchunks`: Log incoming audio chunks.
59
54
 
60
55
  ### WebSocket Interface:
61
56
  The server supports two WebSocket connections:
@@ -65,18 +60,26 @@ The server supports two WebSocket connections:
65
60
  The server will broadcast real-time transcription updates to all connected clients on the data WebSocket.
66
61
  """
67
62
 
63
+ from .install_packages import check_and_install_packages
64
+ from datetime import datetime
65
+ import logging
66
+ import asyncio
67
+ import pyaudio
68
+ import sys
68
69
 
69
- extended_logging = True
70
+
71
+ debug_logging = False
72
+ extended_logging = False
70
73
  send_recorded_chunk = False
71
- log_incoming_chunks = True
74
+ log_incoming_chunks = False
72
75
  stt_optimizations = False
76
+ writechunks = False#
77
+ wav_file = None
78
+ loglevel = logging.WARNING
73
79
 
80
+ FORMAT = pyaudio.paInt16
81
+ CHANNELS = 1
74
82
 
75
- from .install_packages import check_and_install_packages
76
- from datetime import datetime
77
- import asyncio
78
- import base64
79
- import sys
80
83
 
81
84
  if sys.platform == 'win32':
82
85
  asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
@@ -117,17 +120,25 @@ class bcolors:
117
120
 
118
121
  print(f"{bcolors.BOLD}{bcolors.OKCYAN}Starting server, please wait...{bcolors.ENDC}")
119
122
 
120
- import threading
121
- import json
122
- import websockets
123
+ # Initialize colorama
124
+ from colorama import init, Fore, Style
125
+ init()
126
+
123
127
  from RealtimeSTT import AudioToTextRecorder
124
- import numpy as np
125
128
  from scipy.signal import resample
129
+ import numpy as np
130
+ import websockets
131
+ import threading
132
+ import logging
133
+ import wave
134
+ import json
135
+ import time
126
136
 
127
137
  global_args = None
128
138
  recorder = None
129
139
  recorder_config = {}
130
140
  recorder_ready = threading.Event()
141
+ recorder_thread = None
131
142
  stop_recorder = False
132
143
  prev_text = ""
133
144
 
@@ -174,6 +185,12 @@ def preprocess_text(text):
174
185
 
175
186
  return text
176
187
 
188
+ def debug_print(message):
189
+ if debug_logging:
190
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
191
+ thread_name = threading.current_thread().name
192
+ print(f"{Fore.CYAN}[DEBUG][{timestamp}][{thread_name}] {message}{Style.RESET_ALL}", file=sys.stderr)
193
+
177
194
  def text_detected(text, loop):
178
195
  global prev_text
179
196
 
@@ -279,20 +296,36 @@ def on_transcription_start(loop):
279
296
 
280
297
  # Define the server's arguments
281
298
  def parse_arguments():
299
+ global debug_logging, extended_logging, loglevel, writechunks, log_incoming_chunks
300
+
282
301
  import argparse
283
302
  parser = argparse.ArgumentParser(description='Start the Speech-to-Text (STT) server with various configuration options.')
284
303
 
285
- parser.add_argument('--model', type=str, default='large-v2',
304
+ parser.add_argument('-m', '--model', type=str, default='large-v2',
286
305
  help='Path to the STT model or model size. Options include: tiny, tiny.en, base, base.en, small, small.en, medium, medium.en, large-v1, large-v2, or any huggingface CTranslate2 STT model such as deepdml/faster-whisper-large-v3-turbo-ct2. Default is large-v2.')
287
306
 
288
- parser.add_argument('--realtime_model_type', type=str, default='tiny.en',
289
- help='Model size for real-time transcription. The options are the same as --model. This is used only if real-time transcription is enabled. Default is tiny.en.')
307
+ parser.add_argument('-r', '--rt-model', '--realtime_model_type', type=str, default='tiny.en',
308
+ help='Model size for real-time transcription. Options same as --model. This is used only if real-time transcription is enabled (enable_realtime_transcription). Default is tiny.en.')
309
+
310
+ parser.add_argument('-l', '--lang', '--language', type=str, default='en',
311
+ 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')
290
312
 
291
- parser.add_argument('--language', type=str, default='en',
292
- 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.')
313
+ parser.add_argument('-i', '--input-device', '--input_device_index', type=int, default=1,
314
+ 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.')
293
315
 
294
- parser.add_argument('--input_device_index', type=int, default=1,
295
- 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.')
316
+ parser.add_argument('-c', '--control', '--control_port', type=int, default=8011,
317
+ help='The port number used for the control WebSocket connection. Control connections are used to send and receive commands to the server. Default is port 8011.')
318
+
319
+ parser.add_argument('-d', '--data', '--data_port', type=int, default=8012,
320
+ help='The port number used for the data WebSocket connection. Data connections are used to send audio data and receive transcription updates in real time. Default is port 8012.')
321
+
322
+ parser.add_argument('-w', '--wake_words', type=str, default="",
323
+ help='Specify the wake word(s) that will trigger the server to start listening. For example, setting this to "Jarvis" will make the system start transcribing when it detects the wake word "Jarvis". Default is "Jarvis".')
324
+
325
+ parser.add_argument('-D', '--debug', action='store_true', help='Enable debug logging for detailed server operations')
326
+
327
+ parser.add_argument("-W", "--write", metavar="FILE",
328
+ help="Save received audio to a WAV file")
296
329
 
297
330
  parser.add_argument('--silero_sensitivity', type=float, default=0.05,
298
331
  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.')
@@ -340,15 +373,6 @@ def parse_arguments():
340
373
  parser.add_argument('--mid_sentence_detection_pause', type=float, default=2.0,
341
374
  help='The duration of pause (in seconds) that the model should interpret as a mid-sentence break. Longer pauses can indicate a pause in speech but not necessarily the end of a sentence. Default is 2.0 seconds.')
342
375
 
343
- parser.add_argument('--control_port', type=int, default=8011,
344
- help='The port number used for the control WebSocket connection. Control connections are used to send and receive commands to the server. Default is port 8011.')
345
-
346
- parser.add_argument('--data_port', type=int, default=8012,
347
- help='The port number used for the data WebSocket connection. Data connections are used to send audio data and receive transcription updates in real time. Default is port 8012.')
348
-
349
- parser.add_argument('--wake_words', type=str, default="",
350
- help='Specify the wake word(s) that will trigger the server to start listening. For example, setting this to "Jarvis" will make the system start transcribing when it detects the wake word "Jarvis". Default is "Jarvis".')
351
-
352
376
  parser.add_argument('--wake_words_sensitivity', type=float, default=0.5,
353
377
  help='Sensitivity level for wake word detection, with a range from 0 (most sensitive) to 1 (least sensitive). Adjust this value based on your environment to ensure reliable wake word detection. Default is 0.5.')
354
378
 
@@ -376,9 +400,23 @@ 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('--logchunks', action='store_true', help='Enable logging of incoming audio chunks (periods)')
404
+
379
405
  # Parse arguments
380
406
  args = parser.parse_args()
381
407
 
408
+ debug_logging = args.debug
409
+ extended_logging = args.use_extended_logging
410
+ writechunks = args.write
411
+ log_incoming_chunks = args.logchunks
412
+
413
+ if debug_logging:
414
+ loglevel = logging.DEBUG
415
+ logging.basicConfig(level=loglevel, format='[%(asctime)s] %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
416
+ else:
417
+ loglevel = logging.WARNING
418
+
419
+
382
420
  # Replace escaped newlines with actual newlines in initial_prompt
383
421
  if args.initial_prompt:
384
422
  args.initial_prompt = args.initial_prompt.replace("\\n", "\n")
@@ -437,11 +475,13 @@ def decode_and_resample(
437
475
  return resampled_audio.astype(np.int16).tobytes()
438
476
 
439
477
  async def control_handler(websocket, path):
478
+ debug_print(f"New control connection from {websocket.remote_address}")
440
479
  print(f"{bcolors.OKGREEN}Control client connected{bcolors.ENDC}")
441
480
  global recorder
442
481
  control_connections.add(websocket)
443
482
  try:
444
483
  async for message in websocket:
484
+ debug_print(f"Received control message: {message[:200]}...")
445
485
  if not recorder_ready.is_set():
446
486
  print(f"{bcolors.WARNING}Recorder not ready{bcolors.ENDC}")
447
487
  continue
@@ -530,21 +570,38 @@ async def control_handler(websocket, path):
530
570
  control_connections.remove(websocket)
531
571
 
532
572
  async def data_handler(websocket, path):
573
+ global writechunks, wav_file
533
574
  print(f"{bcolors.OKGREEN}Data client connected{bcolors.ENDC}")
534
575
  data_connections.add(websocket)
535
576
  try:
536
577
  while True:
537
578
  message = await websocket.recv()
538
579
  if isinstance(message, bytes):
539
- if log_incoming_chunks:
580
+ if debug_logging:
581
+ debug_print(f"Received audio chunk (size: {len(message)} bytes)")
582
+ elif log_incoming_chunks:
540
583
  print(".", end='', flush=True)
541
584
  # Handle binary message (audio data)
542
585
  metadata_length = int.from_bytes(message[:4], byteorder='little')
543
586
  metadata_json = message[4:4+metadata_length].decode('utf-8')
544
587
  metadata = json.loads(metadata_json)
545
588
  sample_rate = metadata['sampleRate']
589
+
590
+ debug_print(f"Processing audio chunk with sample rate {sample_rate}")
546
591
  chunk = message[4+metadata_length:]
592
+
593
+ if writechunks:
594
+ if not wav_file:
595
+ wav_file = wave.open(writechunks, 'wb')
596
+ wav_file.setnchannels(CHANNELS)
597
+ wav_file.setsampwidth(pyaudio.get_sample_size(FORMAT))
598
+ wav_file.setframerate(sample_rate)
599
+
600
+ wav_file.writeframes(chunk)
601
+
547
602
  resampled_chunk = decode_and_resample(chunk, sample_rate, 16000)
603
+
604
+ debug_print(f"Resampled chunk size: {len(resampled_chunk)} bytes")
548
605
  recorder.feed_audio(resampled_chunk)
549
606
  else:
550
607
  print(f"{bcolors.WARNING}Received non-binary message on data connection{bcolors.ENDC}")
@@ -583,9 +640,9 @@ async def main_async():
583
640
 
584
641
  recorder_config = {
585
642
  'model': args.model,
586
- 'realtime_model_type': args.realtime_model_type,
587
- 'language': args.language,
588
- 'input_device_index': args.input_device_index,
643
+ 'realtime_model_type': args.rt_model,
644
+ 'language': args.lang,
645
+ 'input_device_index': args.input_device,
589
646
  'silero_sensitivity': args.silero_sensitivity,
590
647
  'silero_use_onnx': args.silero_use_onnx,
591
648
  'webrtc_sensitivity': args.webrtc_sensitivity,
@@ -622,47 +679,55 @@ async def main_async():
622
679
  # 'on_recorded_chunk': make_callback(loop, on_recorded_chunk),
623
680
  'no_log_file': True, # Disable logging to file
624
681
  'use_extended_logging': args.use_extended_logging,
682
+ 'level': loglevel,
625
683
  }
626
684
 
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}")
685
+ try:
686
+ # Attempt to start control and data servers
687
+ control_server = await websockets.serve(control_handler, "localhost", args.control)
688
+ data_server = await websockets.serve(data_handler, "localhost", args.data)
689
+ print(f"{bcolors.OKGREEN}Control server started on {bcolors.OKBLUE}ws://localhost:{args.control}{bcolors.ENDC}")
690
+ print(f"{bcolors.OKGREEN}Data server started on {bcolors.OKBLUE}ws://localhost:{args.data}{bcolors.ENDC}")
631
691
 
632
- # Task to broadcast audio messages
633
- broadcast_task = asyncio.create_task(broadcast_audio_messages())
692
+ # Start the broadcast and recorder threads
693
+ broadcast_task = asyncio.create_task(broadcast_audio_messages())
634
694
 
635
- recorder_thread = threading.Thread(target=_recorder_thread, args=(loop,))
636
- recorder_thread.start()
637
- recorder_ready.wait()
695
+ recorder_thread = threading.Thread(target=_recorder_thread, args=(loop,))
696
+ recorder_thread.start()
697
+ recorder_ready.wait()
638
698
 
639
- print(f"{bcolors.OKGREEN}Server started. Press Ctrl+C to stop the server.{bcolors.ENDC}")
699
+ print(f"{bcolors.OKGREEN}Server started. Press Ctrl+C to stop the server.{bcolors.ENDC}")
640
700
 
641
- try:
701
+ # Run server tasks
642
702
  await asyncio.gather(control_server.wait_closed(), data_server.wait_closed(), broadcast_task)
703
+ except OSError as e:
704
+ 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
705
  except KeyboardInterrupt:
644
- print(f"{bcolors.WARNING}{bcolors.BOLD}Shutting down gracefully...{bcolors.ENDC}")
706
+ print(f"{bcolors.WARNING}Server interrupted by user, shutting down...{bcolors.ENDC}")
645
707
  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
-
708
+ # Shutdown procedures for recorder and server threads
709
+ await shutdown_procedure()
710
+ print(f"{bcolors.OKGREEN}Server shutdown complete.{bcolors.ENDC}")
711
+
712
+ async def shutdown_procedure():
713
+ global stop_recorder, recorder_thread
714
+ if recorder:
715
+ stop_recorder = True
716
+ recorder.abort()
717
+ recorder.stop()
718
+ recorder.shutdown()
719
+ print(f"{bcolors.OKGREEN}Recorder shut down{bcolors.ENDC}")
720
+
721
+ if recorder_thread:
654
722
  recorder_thread.join()
655
723
  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}")
724
+
725
+ tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
726
+ for task in tasks:
727
+ task.cancel()
728
+ await asyncio.gather(*tasks, return_exceptions=True)
729
+
730
+ print(f"{bcolors.OKGREEN}All tasks cancelled, closing event loop now.{bcolors.ENDC}")
666
731
 
667
732
  def main():
668
733
  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.7",
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