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.
- {realtimestt-0.3.5 → realtimestt-0.3.7}/PKG-INFO +2 -2
- {realtimestt-0.3.5 → realtimestt-0.3.7}/README.md +1 -1
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT/audio_recorder.py +42 -12
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT.egg-info/PKG-INFO +2 -2
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT_server/stt_cli_client.py +184 -61
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT_server/stt_server.py +165 -100
- {realtimestt-0.3.5 → realtimestt-0.3.7}/setup.py +1 -1
- {realtimestt-0.3.5 → realtimestt-0.3.7}/LICENSE +0 -0
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT/__init__.py +0 -0
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT/audio_recorder_client.py +0 -0
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT.egg-info/SOURCES.txt +0 -0
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT.egg-info/dependency_links.txt +0 -0
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT.egg-info/entry_points.txt +0 -0
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT.egg-info/requires.txt +0 -0
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT.egg-info/top_level.txt +0 -0
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT_server/__init__.py +0 -0
- {realtimestt-0.3.5 → realtimestt-0.3.7}/RealtimeSTT_server/install_packages.py +0 -0
- {realtimestt-0.3.5 → realtimestt-0.3.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: RealtimeSTT
|
|
3
|
-
Version: 0.3.
|
|
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.
|
|
58
|
+
Latest Version: v0.3.7
|
|
59
59
|
|
|
60
60
|
See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
|
|
61
61
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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"
|
|
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()
|
|
306
|
+
self.file_output.flush()
|
|
247
307
|
else:
|
|
248
308
|
self.finish_progress_bar()
|
|
249
309
|
print(f"{data['text']}")
|
|
250
|
-
|
|
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"
|
|
326
|
+
self.debug_print(f"Other message type received: {message_type}")
|
|
264
327
|
|
|
265
328
|
except json.JSONDecodeError:
|
|
266
|
-
self.debug_print(f"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
455
|
-
parser.add_argument("
|
|
456
|
-
|
|
457
|
-
parser.add_argument("-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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.
|
|
485
|
-
for param, value in args.
|
|
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.
|
|
501
|
-
for param_list in args.
|
|
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.
|
|
509
|
-
for method_call in args.
|
|
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.
|
|
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
|
-
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
- `--
|
|
31
|
-
|
|
32
|
-
- `--min_length_of_recording
|
|
33
|
-
|
|
34
|
-
- `--
|
|
35
|
-
|
|
36
|
-
- `--
|
|
37
|
-
|
|
38
|
-
- `--
|
|
39
|
-
|
|
40
|
-
- `--
|
|
41
|
-
|
|
42
|
-
- `--
|
|
43
|
-
|
|
44
|
-
- `--
|
|
45
|
-
|
|
46
|
-
- `--
|
|
47
|
-
|
|
48
|
-
- `--
|
|
49
|
-
|
|
50
|
-
- `--
|
|
51
|
-
|
|
52
|
-
- `--
|
|
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
|
-
|
|
70
|
+
|
|
71
|
+
debug_logging = False
|
|
72
|
+
extended_logging = False
|
|
70
73
|
send_recorded_chunk = False
|
|
71
|
-
log_incoming_chunks =
|
|
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
|
-
|
|
121
|
-
import
|
|
122
|
-
|
|
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.
|
|
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('--
|
|
292
|
-
|
|
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('--
|
|
295
|
-
help='
|
|
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
|
|
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.
|
|
587
|
-
'language': args.
|
|
588
|
-
'input_device_index': args.
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
633
|
-
|
|
692
|
+
# Start the broadcast and recorder threads
|
|
693
|
+
broadcast_task = asyncio.create_task(broadcast_audio_messages())
|
|
634
694
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
695
|
+
recorder_thread = threading.Thread(target=_recorder_thread, args=(loop,))
|
|
696
|
+
recorder_thread.start()
|
|
697
|
+
recorder_ready.wait()
|
|
638
698
|
|
|
639
|
-
|
|
699
|
+
print(f"{bcolors.OKGREEN}Server started. Press Ctrl+C to stop the server.{bcolors.ENDC}")
|
|
640
700
|
|
|
641
|
-
|
|
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}
|
|
706
|
+
print(f"{bcolors.WARNING}Server interrupted by user, shutting down...{bcolors.ENDC}")
|
|
645
707
|
finally:
|
|
646
|
-
#
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|