RealTimeSTT 0.3.4__tar.gz → 0.3.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {realtimestt-0.3.4 → realtimestt-0.3.6}/PKG-INFO +2 -2
- {realtimestt-0.3.4 → realtimestt-0.3.6}/README.md +1 -1
- {realtimestt-0.3.4 → realtimestt-0.3.6}/RealtimeSTT/audio_recorder.py +42 -12
- {realtimestt-0.3.4 → realtimestt-0.3.6}/RealtimeSTT.egg-info/PKG-INFO +2 -2
- {realtimestt-0.3.4 → realtimestt-0.3.6}/RealtimeSTT.egg-info/SOURCES.txt +4 -4
- realtimestt-0.3.6/RealtimeSTT.egg-info/entry_points.txt +3 -0
- realtimestt-0.3.6/RealtimeSTT.egg-info/top_level.txt +2 -0
- {realtimestt-0.3.4/server → realtimestt-0.3.6/RealtimeSTT_server}/stt_cli_client.py +217 -140
- {realtimestt-0.3.4/server → realtimestt-0.3.6/RealtimeSTT_server}/stt_server.py +130 -61
- {realtimestt-0.3.4 → realtimestt-0.3.6}/setup.py +34 -34
- realtimestt-0.3.4/RealtimeSTT.egg-info/entry_points.txt +0 -3
- realtimestt-0.3.4/RealtimeSTT.egg-info/top_level.txt +0 -2
- {realtimestt-0.3.4 → realtimestt-0.3.6}/LICENSE +0 -0
- {realtimestt-0.3.4 → realtimestt-0.3.6}/RealtimeSTT/__init__.py +0 -0
- {realtimestt-0.3.4 → realtimestt-0.3.6}/RealtimeSTT/audio_recorder_client.py +0 -0
- {realtimestt-0.3.4 → realtimestt-0.3.6}/RealtimeSTT.egg-info/dependency_links.txt +0 -0
- {realtimestt-0.3.4 → realtimestt-0.3.6}/RealtimeSTT.egg-info/requires.txt +0 -0
- {realtimestt-0.3.4/server → realtimestt-0.3.6/RealtimeSTT_server}/__init__.py +0 -0
- {realtimestt-0.3.4/server → realtimestt-0.3.6/RealtimeSTT_server}/install_packages.py +0 -0
- {realtimestt-0.3.4 → realtimestt-0.3.6}/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.6
|
|
4
4
|
Summary: A fast Voice Activity Detection and Transcription System
|
|
5
5
|
Home-page: https://github.com/KoljaB/RealTimeSTT
|
|
6
6
|
Author: Kolja Beigel
|
|
@@ -55,7 +55,7 @@ https://github.com/user-attachments/assets/797e6552-27cd-41b1-a7f3-e5cbc72094f5
|
|
|
55
55
|
|
|
56
56
|
### Updates
|
|
57
57
|
|
|
58
|
-
Latest Version: v0.3.
|
|
58
|
+
Latest Version: v0.3.6
|
|
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.6
|
|
4
4
|
Summary: A fast Voice Activity Detection and Transcription System
|
|
5
5
|
Home-page: https://github.com/KoljaB/RealTimeSTT
|
|
6
6
|
Author: Kolja Beigel
|
|
@@ -55,7 +55,7 @@ https://github.com/user-attachments/assets/797e6552-27cd-41b1-a7f3-e5cbc72094f5
|
|
|
55
55
|
|
|
56
56
|
### Updates
|
|
57
57
|
|
|
58
|
-
Latest Version: v0.3.
|
|
58
|
+
Latest Version: v0.3.6
|
|
59
59
|
|
|
60
60
|
See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
|
|
61
61
|
|
|
@@ -10,7 +10,7 @@ RealtimeSTT.egg-info/dependency_links.txt
|
|
|
10
10
|
RealtimeSTT.egg-info/entry_points.txt
|
|
11
11
|
RealtimeSTT.egg-info/requires.txt
|
|
12
12
|
RealtimeSTT.egg-info/top_level.txt
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
RealtimeSTT_server/__init__.py
|
|
14
|
+
RealtimeSTT_server/install_packages.py
|
|
15
|
+
RealtimeSTT_server/stt_cli_client.py
|
|
16
|
+
RealtimeSTT_server/stt_server.py
|
|
@@ -1,90 +1,39 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Speech-to-Text (STT)
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
stt [OPTIONS]
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### Available Parameters:
|
|
23
|
-
- `--control-url` (default: "ws://localhost:8011"): The WebSocket URL for server control commands.
|
|
24
|
-
|
|
25
|
-
- `--data-url` (default: "ws://localhost:8012"): The WebSocket URL for sending audio data and receiving transcription updates.
|
|
26
|
-
|
|
27
|
-
- `--debug`: Enable debug mode, which prints detailed logs to stderr.
|
|
28
|
-
|
|
29
|
-
- `--nort` or `--norealtime`: Disable real-time output of transcription results.
|
|
30
|
-
|
|
31
|
-
- `--set-param PARAM VALUE`: Set a recorder parameter (e.g., silero_sensitivity, beam_size, etc.). This option can be used multiple times to set different parameters.
|
|
32
|
-
|
|
33
|
-
- `--get-param PARAM`: Retrieve the value of a specific recorder parameter. This option can be used multiple times to retrieve different parameters.
|
|
34
|
-
|
|
35
|
-
- `--call-method METHOD [ARGS]`: Call a method on the recorder with optional arguments. This option can be used multiple times for different methods.
|
|
36
|
-
|
|
37
|
-
### Example Usage:
|
|
38
|
-
1. **Start the client with default settings:**
|
|
39
|
-
```bash
|
|
40
|
-
stt
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
2. **Set a recorder parameter (e.g., set Silero sensitivity to 0.1):**
|
|
44
|
-
```bash
|
|
45
|
-
stt --set-param silero_sensitivity 0.1
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
3. **Retrieve the value of a recorder parameter (e.g., get the current Silero sensitivity):**
|
|
49
|
-
```bash
|
|
50
|
-
stt --get-param silero_sensitivity
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
4. **Call a method on the recorder (e.g., start the microphone input):**
|
|
54
|
-
```bash
|
|
55
|
-
stt --call-method set_microphone
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
5. **Run in debug mode:**
|
|
59
|
-
```bash
|
|
60
|
-
stt --debug
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### WebSocket Interface:
|
|
64
|
-
- **Control WebSocket**: Used for sending control commands like setting parameters or invoking methods.
|
|
65
|
-
- **Data WebSocket**: Used for sending audio data for real-time transcription and receiving transcription results.
|
|
66
|
-
|
|
67
|
-
The client can be used to send audio data to the server for transcription and to control the behavior of the server remotely.
|
|
2
|
+
This is a command-line client for the Speech-to-Text (STT) server.
|
|
3
|
+
It records audio from the default input device and sends it to the server for speech recognition.
|
|
4
|
+
It can also process commands to set parameters, get parameter values, or call methods on the server.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
stt [--control-url CONTROL_URL] [--data-url DATA_URL] [--debug] [--norealtime] [--set-param PARAM VALUE] [--call-method METHOD [ARGS ...]] [--get-param PARAM]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--control-url CONTROL_URL STT Control WebSocket URL
|
|
11
|
+
--data-url DATA_URL STT Data WebSocket URL
|
|
12
|
+
--debug Enable debug mode
|
|
13
|
+
--norealtime Disable real-time output
|
|
14
|
+
--set-param PARAM VALUE Set a recorder parameter. Can be used multiple times.
|
|
15
|
+
--call-method METHOD [ARGS ...] Call a recorder method with optional arguments.
|
|
16
|
+
--get-param PARAM Get the value of a recorder parameter. Can be used multiple times.
|
|
68
17
|
"""
|
|
69
18
|
|
|
70
|
-
import
|
|
71
|
-
import sys
|
|
72
|
-
import pyaudio
|
|
73
|
-
import numpy as np
|
|
19
|
+
from urllib.parse import urlparse
|
|
74
20
|
from scipy import signal
|
|
75
|
-
import
|
|
21
|
+
from queue import Queue
|
|
22
|
+
import numpy as np
|
|
23
|
+
import subprocess
|
|
24
|
+
import threading
|
|
76
25
|
import websocket
|
|
77
26
|
import argparse
|
|
78
|
-
import
|
|
79
|
-
import threading
|
|
80
|
-
import time
|
|
27
|
+
import pyaudio
|
|
81
28
|
import struct
|
|
82
29
|
import socket
|
|
83
|
-
import subprocess
|
|
84
30
|
import shutil
|
|
85
|
-
from urllib.parse import urlparse
|
|
86
31
|
import queue
|
|
87
|
-
|
|
32
|
+
import json
|
|
33
|
+
import time
|
|
34
|
+
import wave
|
|
35
|
+
import sys
|
|
36
|
+
import os
|
|
88
37
|
|
|
89
38
|
os.environ['ALSA_LOG_LEVEL'] = 'none'
|
|
90
39
|
|
|
@@ -104,12 +53,13 @@ init()
|
|
|
104
53
|
websocket.enableTrace(False)
|
|
105
54
|
|
|
106
55
|
class STTWebSocketClient:
|
|
107
|
-
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):
|
|
108
57
|
self.control_url = control_url
|
|
109
58
|
self.data_url = data_url
|
|
110
59
|
self.control_ws = None
|
|
111
|
-
self.
|
|
112
|
-
self.
|
|
60
|
+
self.data_ws_app = None
|
|
61
|
+
self.data_ws_connected = None # WebSocket object that will be used for sending
|
|
62
|
+
self.is_running = True
|
|
113
63
|
self.debug = debug
|
|
114
64
|
self.file_output = file_output
|
|
115
65
|
self.last_text = ""
|
|
@@ -120,6 +70,16 @@ class STTWebSocketClient:
|
|
|
120
70
|
self.message_queue = Queue()
|
|
121
71
|
self.commands = Queue()
|
|
122
72
|
self.stop_event = threading.Event()
|
|
73
|
+
self.chunks_sent = 0
|
|
74
|
+
self.last_chunk_time = time.time()
|
|
75
|
+
self.writechunks = writechunks # Add this to store the file name for writing audio chunks
|
|
76
|
+
|
|
77
|
+
self.debug_print("Initializing STT WebSocket Client")
|
|
78
|
+
self.debug_print(f"Control URL: {control_url}")
|
|
79
|
+
self.debug_print(f"Data URL: {data_url}")
|
|
80
|
+
self.debug_print(f"File Output: {file_output}")
|
|
81
|
+
self.debug_print(f"No Realtime: {norealtime}")
|
|
82
|
+
self.debug_print(f"Write Chunks: {writechunks}")
|
|
123
83
|
|
|
124
84
|
# Audio attributes
|
|
125
85
|
self.audio_interface = None
|
|
@@ -132,9 +92,12 @@ class STTWebSocketClient:
|
|
|
132
92
|
self.data_ws_thread = None
|
|
133
93
|
self.recording_thread = None
|
|
134
94
|
|
|
95
|
+
|
|
135
96
|
def debug_print(self, message):
|
|
136
97
|
if self.debug:
|
|
137
|
-
|
|
98
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
|
99
|
+
thread_name = threading.current_thread().name
|
|
100
|
+
print(f"{Fore.CYAN}[DEBUG][{timestamp}][{thread_name}] {message}{Style.RESET_ALL}", file=sys.stderr)
|
|
138
101
|
|
|
139
102
|
def connect(self):
|
|
140
103
|
if not self.ensure_server_running():
|
|
@@ -142,7 +105,10 @@ class STTWebSocketClient:
|
|
|
142
105
|
return False
|
|
143
106
|
|
|
144
107
|
try:
|
|
108
|
+
self.debug_print("Attempting to establish WebSocket connections...")
|
|
109
|
+
|
|
145
110
|
# Connect to control WebSocket
|
|
111
|
+
self.debug_print(f"Connecting to control WebSocket at {self.control_url}")
|
|
146
112
|
self.control_ws = websocket.WebSocketApp(self.control_url,
|
|
147
113
|
on_message=self.on_control_message,
|
|
148
114
|
on_error=self.on_error,
|
|
@@ -150,21 +116,24 @@ class STTWebSocketClient:
|
|
|
150
116
|
on_open=self.on_control_open)
|
|
151
117
|
|
|
152
118
|
self.control_ws_thread = threading.Thread(target=self.control_ws.run_forever)
|
|
153
|
-
self.control_ws_thread.daemon = False
|
|
119
|
+
self.control_ws_thread.daemon = False
|
|
120
|
+
self.debug_print("Starting control WebSocket thread")
|
|
154
121
|
self.control_ws_thread.start()
|
|
155
122
|
|
|
156
123
|
# Connect to data WebSocket
|
|
157
|
-
self.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
self.data_ws_thread
|
|
124
|
+
self.debug_print(f"Connecting to data WebSocket at {self.data_url}")
|
|
125
|
+
self.data_ws_app = websocket.WebSocketApp(self.data_url,
|
|
126
|
+
on_message=self.on_data_message,
|
|
127
|
+
on_error=self.on_error,
|
|
128
|
+
on_close=self.on_close,
|
|
129
|
+
on_open=self.on_data_open)
|
|
130
|
+
|
|
131
|
+
self.data_ws_thread = threading.Thread(target=self.data_ws_app.run_forever)
|
|
132
|
+
self.data_ws_thread.daemon = False
|
|
133
|
+
self.debug_print("Starting data WebSocket thread")
|
|
165
134
|
self.data_ws_thread.start()
|
|
166
135
|
|
|
167
|
-
|
|
136
|
+
self.debug_print("Waiting for connections to be established...")
|
|
168
137
|
if not self.connection_established.wait(timeout=10):
|
|
169
138
|
self.debug_print("Timeout while connecting to the server.")
|
|
170
139
|
return False
|
|
@@ -172,24 +141,30 @@ class STTWebSocketClient:
|
|
|
172
141
|
self.debug_print("WebSocket connections established successfully.")
|
|
173
142
|
return True
|
|
174
143
|
except Exception as e:
|
|
175
|
-
self.debug_print(f"Error while connecting to the server: {e}")
|
|
144
|
+
self.debug_print(f"Error while connecting to the server: {str(e)}")
|
|
176
145
|
return False
|
|
177
146
|
|
|
147
|
+
|
|
178
148
|
def on_control_open(self, ws):
|
|
179
|
-
self.debug_print("Control WebSocket connection opened
|
|
149
|
+
self.debug_print("Control WebSocket connection opened successfully")
|
|
180
150
|
self.connection_established.set()
|
|
181
151
|
self.start_command_processor()
|
|
182
152
|
|
|
183
153
|
def on_data_open(self, ws):
|
|
184
|
-
self.debug_print("Data WebSocket connection opened
|
|
185
|
-
self.
|
|
154
|
+
self.debug_print("Data WebSocket connection opened successfully")
|
|
155
|
+
self.data_ws_connected = ws
|
|
186
156
|
self.start_recording()
|
|
187
157
|
|
|
188
158
|
def on_error(self, ws, error):
|
|
189
|
-
self.debug_print(f"WebSocket error: {error}")
|
|
159
|
+
self.debug_print(f"WebSocket error occurred: {str(error)}")
|
|
160
|
+
self.debug_print(f"WebSocket object: {ws}")
|
|
161
|
+
self.debug_print(f"Error type: {type(error)}")
|
|
190
162
|
|
|
191
163
|
def on_close(self, ws, close_status_code, close_msg):
|
|
192
|
-
self.debug_print(f"WebSocket connection closed
|
|
164
|
+
self.debug_print(f"WebSocket connection closed")
|
|
165
|
+
self.debug_print(f"Close status code: {close_status_code}")
|
|
166
|
+
self.debug_print(f"Close message: {close_msg}")
|
|
167
|
+
self.debug_print(f"WebSocket object: {ws}")
|
|
193
168
|
self.is_running = False
|
|
194
169
|
self.stop_event.set()
|
|
195
170
|
|
|
@@ -197,8 +172,11 @@ class STTWebSocketClient:
|
|
|
197
172
|
parsed_url = urlparse(self.control_url)
|
|
198
173
|
host = parsed_url.hostname
|
|
199
174
|
port = parsed_url.port or 80
|
|
175
|
+
self.debug_print(f"Checking if server is running at {host}:{port}")
|
|
200
176
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
201
|
-
|
|
177
|
+
result = s.connect_ex((host, port)) == 0
|
|
178
|
+
self.debug_print(f"Server status check result: {'running' if result else 'not running'}")
|
|
179
|
+
return result
|
|
202
180
|
|
|
203
181
|
def ask_to_start_server(self):
|
|
204
182
|
response = input("Would you like to start the STT server now? (y/n): ").strip().lower()
|
|
@@ -208,8 +186,57 @@ class STTWebSocketClient:
|
|
|
208
186
|
if os.name == 'nt': # Windows
|
|
209
187
|
subprocess.Popen('start /min cmd /c stt-server', shell=True)
|
|
210
188
|
else: # Unix-like systems
|
|
211
|
-
|
|
212
|
-
|
|
189
|
+
terminal_emulators = [
|
|
190
|
+
'gnome-terminal',
|
|
191
|
+
'x-terminal-emulator',
|
|
192
|
+
'konsole',
|
|
193
|
+
'xfce4-terminal',
|
|
194
|
+
'lxterminal',
|
|
195
|
+
'xterm',
|
|
196
|
+
'mate-terminal',
|
|
197
|
+
'terminator',
|
|
198
|
+
'tilix',
|
|
199
|
+
'alacritty',
|
|
200
|
+
'urxvt',
|
|
201
|
+
'eterm',
|
|
202
|
+
'rxvt',
|
|
203
|
+
'kitty',
|
|
204
|
+
'hyper'
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
terminal = None
|
|
208
|
+
for term in terminal_emulators:
|
|
209
|
+
if shutil.which(term):
|
|
210
|
+
terminal = term
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
if terminal:
|
|
214
|
+
terminal_exec_options = {
|
|
215
|
+
'x-terminal-emulator': ['--'],
|
|
216
|
+
'gnome-terminal': ['--'],
|
|
217
|
+
'mate-terminal': ['--'],
|
|
218
|
+
'terminator': ['--'],
|
|
219
|
+
'tilix': ['--'],
|
|
220
|
+
'konsole': ['-e'],
|
|
221
|
+
'xfce4-terminal': ['-e'],
|
|
222
|
+
'lxterminal': ['-e'],
|
|
223
|
+
'alacritty': ['-e'],
|
|
224
|
+
'xterm': ['-e'],
|
|
225
|
+
'rxvt': ['-e'],
|
|
226
|
+
'urxvt': ['-e'],
|
|
227
|
+
'eterm': ['-e'],
|
|
228
|
+
'kitty': [],
|
|
229
|
+
'hyper': ['--command']
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
exec_option = terminal_exec_options.get(terminal, None)
|
|
233
|
+
if exec_option is not None:
|
|
234
|
+
subprocess.Popen([terminal] + exec_option + ['stt-server'], start_new_session=True)
|
|
235
|
+
print(f"STT server started in a new terminal window using {terminal}.", file=sys.stderr)
|
|
236
|
+
else:
|
|
237
|
+
print(f"Unsupported terminal emulator '{terminal}'. Please start the STT server manually.", file=sys.stderr)
|
|
238
|
+
else:
|
|
239
|
+
print("No supported terminal emulator found. Please start the STT server manually.", file=sys.stderr)
|
|
213
240
|
|
|
214
241
|
def ensure_server_running(self):
|
|
215
242
|
if not self.is_server_running():
|
|
@@ -230,57 +257,70 @@ class STTWebSocketClient:
|
|
|
230
257
|
return False
|
|
231
258
|
return True
|
|
232
259
|
|
|
233
|
-
# Handle control messages like set_parameter, get_parameter, etc.
|
|
234
260
|
def on_control_message(self, ws, message):
|
|
235
261
|
try:
|
|
262
|
+
self.debug_print(f"Received control message: {message}")
|
|
236
263
|
data = json.loads(message)
|
|
237
|
-
# Handle server response with status
|
|
238
264
|
if 'status' in data:
|
|
265
|
+
self.debug_print(f"Message status: {data['status']}")
|
|
239
266
|
if data['status'] == 'success':
|
|
240
|
-
# print(f"Server Response: {data.get('message', '')}")
|
|
241
267
|
if 'parameter' in data and 'value' in data:
|
|
268
|
+
self.debug_print(f"Parameter update: {data['parameter']} = {data['value']}")
|
|
242
269
|
print(f"Parameter {data['parameter']} = {data['value']}")
|
|
243
270
|
elif data['status'] == 'error':
|
|
271
|
+
self.debug_print(f"Server error received: {data.get('message', '')}")
|
|
244
272
|
print(f"Server Error: {data.get('message', '')}")
|
|
245
273
|
else:
|
|
246
274
|
self.debug_print(f"Unknown control message format: {data}")
|
|
247
275
|
except json.JSONDecodeError:
|
|
248
|
-
self.debug_print(f"
|
|
276
|
+
self.debug_print(f"Failed to decode JSON control message: {message}")
|
|
249
277
|
except Exception as e:
|
|
250
|
-
self.debug_print(f"Error processing control message: {e}")
|
|
278
|
+
self.debug_print(f"Error processing control message: {str(e)}")
|
|
251
279
|
|
|
252
|
-
# Handle real-time transcription and full sentence updates
|
|
253
280
|
def on_data_message(self, ws, message):
|
|
254
281
|
try:
|
|
282
|
+
self.debug_print(f"Received data message: {message}")
|
|
255
283
|
data = json.loads(message)
|
|
256
|
-
|
|
257
|
-
|
|
284
|
+
message_type = data.get('type')
|
|
285
|
+
self.debug_print(f"Message type: {message_type}")
|
|
286
|
+
|
|
287
|
+
if message_type == 'realtime':
|
|
258
288
|
if data['text'] != self.last_text:
|
|
289
|
+
self.debug_print(f"New realtime text received: {data['text']}")
|
|
259
290
|
self.last_text = data['text']
|
|
260
291
|
if not self.norealtime:
|
|
261
292
|
self.update_progress_bar(self.last_text)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
elif data.get('type') == 'fullSentence':
|
|
293
|
+
elif message_type == 'fullSentence':
|
|
294
|
+
self.debug_print(f"Full sentence received: {data['text']}")
|
|
265
295
|
if self.file_output:
|
|
296
|
+
self.debug_print("Writing to file output")
|
|
266
297
|
sys.stderr.write('\r\033[K')
|
|
267
298
|
sys.stderr.write(data['text'])
|
|
268
299
|
sys.stderr.write('\n')
|
|
269
300
|
sys.stderr.flush()
|
|
270
301
|
print(data['text'], file=self.file_output)
|
|
271
|
-
self.file_output.flush()
|
|
302
|
+
self.file_output.flush()
|
|
272
303
|
else:
|
|
273
304
|
self.finish_progress_bar()
|
|
274
305
|
print(f"{data['text']}")
|
|
275
306
|
self.stop()
|
|
276
|
-
|
|
307
|
+
elif message_type in {
|
|
308
|
+
'vad_detect_start',
|
|
309
|
+
'vad_detect_stop',
|
|
310
|
+
'recording_start',
|
|
311
|
+
'recording_stop',
|
|
312
|
+
'wakeword_detected',
|
|
313
|
+
'wakeword_detection_start',
|
|
314
|
+
'wakeword_detection_end',
|
|
315
|
+
'transcription_start'}:
|
|
316
|
+
pass # Known message types, no action needed
|
|
277
317
|
else:
|
|
278
|
-
self.debug_print(f"
|
|
318
|
+
self.debug_print(f"Other message type received: {message_type}")
|
|
279
319
|
|
|
280
320
|
except json.JSONDecodeError:
|
|
281
|
-
self.debug_print(f"
|
|
321
|
+
self.debug_print(f"Failed to decode JSON data message: {message}")
|
|
282
322
|
except Exception as e:
|
|
283
|
-
self.debug_print(f"Error processing data message: {e}")
|
|
323
|
+
self.debug_print(f"Error processing data message: {str(e)}")
|
|
284
324
|
|
|
285
325
|
def show_initial_indicator(self):
|
|
286
326
|
if self.norealtime:
|
|
@@ -317,10 +357,11 @@ class STTWebSocketClient:
|
|
|
317
357
|
self.finish_progress_bar()
|
|
318
358
|
self.is_running = False
|
|
319
359
|
self.stop_event.set()
|
|
360
|
+
self.debug_print("Stopping client and cleaning up resources.")
|
|
320
361
|
if self.control_ws:
|
|
321
362
|
self.control_ws.close()
|
|
322
|
-
if self.
|
|
323
|
-
self.
|
|
363
|
+
if self.data_ws_connected:
|
|
364
|
+
self.data_ws_connected.close()
|
|
324
365
|
|
|
325
366
|
# Join threads to ensure they finish before exiting
|
|
326
367
|
if self.control_ws_thread:
|
|
@@ -345,40 +386,67 @@ class STTWebSocketClient:
|
|
|
345
386
|
def record_and_send_audio(self):
|
|
346
387
|
try:
|
|
347
388
|
if not self.setup_audio():
|
|
389
|
+
self.debug_print("Failed to set up audio recording")
|
|
348
390
|
raise Exception("Failed to set up audio recording.")
|
|
349
391
|
|
|
350
|
-
|
|
392
|
+
# Initialize WAV file writer if writechunks is provided
|
|
393
|
+
if self.writechunks:
|
|
394
|
+
self.wav_file = wave.open(self.writechunks, 'wb')
|
|
395
|
+
self.wav_file.setnchannels(CHANNELS)
|
|
396
|
+
self.wav_file.setsampwidth(pyaudio.get_sample_size(FORMAT))
|
|
397
|
+
self.wav_file.setframerate(self.device_sample_rate) # Use self.device_sample_rate
|
|
398
|
+
|
|
399
|
+
self.debug_print("Starting audio recording and transmission")
|
|
351
400
|
self.show_initial_indicator()
|
|
352
401
|
|
|
353
402
|
while self.is_running:
|
|
354
403
|
try:
|
|
355
404
|
audio_data = self.stream.read(CHUNK)
|
|
405
|
+
self.chunks_sent += 1
|
|
406
|
+
current_time = time.time()
|
|
407
|
+
elapsed = current_time - self.last_chunk_time
|
|
408
|
+
|
|
409
|
+
# Write to WAV file if enabled
|
|
410
|
+
if self.writechunks:
|
|
411
|
+
self.wav_file.writeframes(audio_data)
|
|
412
|
+
|
|
413
|
+
if self.chunks_sent % 100 == 0: # Log every 100 chunks
|
|
414
|
+
self.debug_print(f"Sent {self.chunks_sent} chunks. Last chunk took {elapsed:.3f}s")
|
|
415
|
+
|
|
356
416
|
metadata = {"sampleRate": self.device_sample_rate}
|
|
357
417
|
metadata_json = json.dumps(metadata)
|
|
358
418
|
metadata_length = len(metadata_json)
|
|
359
419
|
message = struct.pack('<I', metadata_length) + metadata_json.encode('utf-8') + audio_data
|
|
360
|
-
|
|
420
|
+
|
|
421
|
+
self.debug_print(f"Sending audio chunk {self.chunks_sent}: {len(audio_data)} bytes, metadata: {metadata_json}")
|
|
422
|
+
self.data_ws_connected.send(message, opcode=websocket.ABNF.OPCODE_BINARY)
|
|
423
|
+
self.last_chunk_time = current_time
|
|
424
|
+
|
|
361
425
|
except Exception as e:
|
|
362
|
-
self.debug_print(f"Error sending audio data: {e}")
|
|
363
|
-
break
|
|
426
|
+
self.debug_print(f"Error sending audio data: {str(e)}")
|
|
427
|
+
break
|
|
364
428
|
|
|
365
429
|
except Exception as e:
|
|
366
|
-
self.debug_print(f"Error in record_and_send_audio: {e}")
|
|
430
|
+
self.debug_print(f"Error in record_and_send_audio: {str(e)}")
|
|
367
431
|
finally:
|
|
368
432
|
self.cleanup_audio()
|
|
369
433
|
|
|
370
434
|
def setup_audio(self):
|
|
371
435
|
try:
|
|
436
|
+
self.debug_print("Initializing PyAudio interface")
|
|
372
437
|
self.audio_interface = pyaudio.PyAudio()
|
|
373
438
|
self.input_device_index = None
|
|
439
|
+
|
|
374
440
|
try:
|
|
375
441
|
default_device = self.audio_interface.get_default_input_device_info()
|
|
376
442
|
self.input_device_index = default_device['index']
|
|
443
|
+
self.debug_print(f"Default input device found: {default_device}")
|
|
377
444
|
except OSError as e:
|
|
378
|
-
self.debug_print(f"No default input device found: {e}")
|
|
445
|
+
self.debug_print(f"No default input device found: {str(e)}")
|
|
379
446
|
return False
|
|
380
447
|
|
|
381
|
-
self.device_sample_rate = 16000
|
|
448
|
+
self.device_sample_rate = 16000
|
|
449
|
+
self.debug_print(f"Attempting to open audio stream with sample rate {self.device_sample_rate} Hz")
|
|
382
450
|
|
|
383
451
|
try:
|
|
384
452
|
self.stream = self.audio_interface.open(
|
|
@@ -389,29 +457,36 @@ class STTWebSocketClient:
|
|
|
389
457
|
frames_per_buffer=CHUNK,
|
|
390
458
|
input_device_index=self.input_device_index,
|
|
391
459
|
)
|
|
392
|
-
self.debug_print(f"Audio
|
|
460
|
+
self.debug_print(f"Audio stream initialized successfully")
|
|
461
|
+
self.debug_print(f"Audio parameters: rate={self.device_sample_rate}, channels={CHANNELS}, format={FORMAT}, chunk={CHUNK}")
|
|
393
462
|
return True
|
|
394
463
|
except Exception as e:
|
|
395
|
-
self.debug_print(f"Failed to initialize audio stream
|
|
464
|
+
self.debug_print(f"Failed to initialize audio stream: {str(e)}")
|
|
396
465
|
return False
|
|
397
466
|
|
|
398
467
|
except Exception as e:
|
|
399
|
-
self.debug_print(f"Error
|
|
468
|
+
self.debug_print(f"Error in setup_audio: {str(e)}")
|
|
400
469
|
if self.audio_interface:
|
|
401
470
|
self.audio_interface.terminate()
|
|
402
471
|
return False
|
|
403
472
|
|
|
404
473
|
def cleanup_audio(self):
|
|
474
|
+
self.debug_print("Cleaning up audio resources")
|
|
405
475
|
try:
|
|
406
476
|
if self.stream:
|
|
477
|
+
self.debug_print("Stopping and closing audio stream")
|
|
407
478
|
self.stream.stop_stream()
|
|
408
479
|
self.stream.close()
|
|
409
480
|
self.stream = None
|
|
410
481
|
if self.audio_interface:
|
|
482
|
+
self.debug_print("Terminating PyAudio interface")
|
|
411
483
|
self.audio_interface.terminate()
|
|
412
484
|
self.audio_interface = None
|
|
485
|
+
if self.writechunks and self.wav_file:
|
|
486
|
+
self.debug_print("Closing WAV file")
|
|
487
|
+
self.wav_file.close()
|
|
413
488
|
except Exception as e:
|
|
414
|
-
self.debug_print(f"Error
|
|
489
|
+
self.debug_print(f"Error during audio cleanup: {str(e)}")
|
|
415
490
|
|
|
416
491
|
def set_parameter(self, parameter, value):
|
|
417
492
|
command = {
|
|
@@ -442,27 +517,28 @@ class STTWebSocketClient:
|
|
|
442
517
|
self.command_thread.daemon = False # Ensure it is not a daemon thread
|
|
443
518
|
self.command_thread.start()
|
|
444
519
|
|
|
520
|
+
|
|
445
521
|
def command_processor(self):
|
|
446
|
-
|
|
447
|
-
self.debug_print(f"Starting command processor")
|
|
448
|
-
#while self.is_running and not self.stop_event.is_set():
|
|
522
|
+
self.debug_print("Starting command processor thread")
|
|
449
523
|
while not self.stop_event.is_set():
|
|
450
524
|
try:
|
|
451
525
|
command = self.commands.get(timeout=0.1)
|
|
526
|
+
self.debug_print(f"Processing command: {command}")
|
|
452
527
|
if command['type'] == 'set_parameter':
|
|
528
|
+
self.debug_print(f"Setting parameter: {command['parameter']} = {command['value']}")
|
|
453
529
|
self.set_parameter(command['parameter'], command['value'])
|
|
454
530
|
elif command['type'] == 'get_parameter':
|
|
531
|
+
self.debug_print(f"Getting parameter: {command['parameter']}")
|
|
455
532
|
self.get_parameter(command['parameter'])
|
|
456
533
|
elif command['type'] == 'call_method':
|
|
534
|
+
self.debug_print(f"Calling method: {command['method']} with args: {command.get('args')} and kwargs: {command.get('kwargs')}")
|
|
457
535
|
self.call_method(command['method'], command.get('args'), command.get('kwargs'))
|
|
458
|
-
except queue.Empty:
|
|
459
|
-
continue
|
|
536
|
+
except queue.Empty:
|
|
537
|
+
continue
|
|
460
538
|
except Exception as e:
|
|
461
|
-
self.debug_print(f"Error in command processor: {e}")
|
|
462
|
-
# finally:
|
|
463
|
-
#print(f"Leaving command processor")
|
|
464
|
-
self.debug_print(f"Leaving command processor")
|
|
539
|
+
self.debug_print(f"Error in command processor: {str(e)}")
|
|
465
540
|
|
|
541
|
+
self.debug_print("Command processor thread stopping")
|
|
466
542
|
|
|
467
543
|
def add_command(self, command):
|
|
468
544
|
self.commands.put(command)
|
|
@@ -473,6 +549,7 @@ def main():
|
|
|
473
549
|
parser.add_argument("--data-url", default=DEFAULT_DATA_URL, help="STT Data WebSocket URL")
|
|
474
550
|
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
|
|
475
551
|
parser.add_argument("-nort", "--norealtime", action="store_true", help="Disable real-time output")
|
|
552
|
+
parser.add_argument("--writechunks", metavar="FILE", help="Save recorded audio chunks to a WAV file")
|
|
476
553
|
parser.add_argument("--set-param", nargs=2, metavar=('PARAM', 'VALUE'), action='append',
|
|
477
554
|
help="Set a recorder parameter. Can be used multiple times.")
|
|
478
555
|
parser.add_argument("--call-method", nargs='+', metavar='METHOD', action='append',
|
|
@@ -487,7 +564,7 @@ def main():
|
|
|
487
564
|
else:
|
|
488
565
|
file_output = None
|
|
489
566
|
|
|
490
|
-
client = STTWebSocketClient(args.control_url, args.data_url, args.debug, file_output, args.norealtime)
|
|
567
|
+
client = STTWebSocketClient(args.control_url, args.data_url, args.debug, file_output, args.norealtime, args.writechunks)
|
|
491
568
|
|
|
492
569
|
def signal_handler(sig, frame):
|
|
493
570
|
client.stop()
|
|
@@ -502,7 +579,6 @@ def main():
|
|
|
502
579
|
if args.set_param:
|
|
503
580
|
for param, value in args.set_param:
|
|
504
581
|
try:
|
|
505
|
-
# Attempt to parse the value to the appropriate type
|
|
506
582
|
if '.' in value:
|
|
507
583
|
value = float(value)
|
|
508
584
|
else:
|
|
@@ -552,3 +628,4 @@ def main():
|
|
|
552
628
|
|
|
553
629
|
if __name__ == "__main__":
|
|
554
630
|
main()
|
|
631
|
+
|
|
@@ -65,18 +65,26 @@ The server supports two WebSocket connections:
|
|
|
65
65
|
The server will broadcast real-time transcription updates to all connected clients on the data WebSocket.
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
|
+
from .install_packages import check_and_install_packages
|
|
69
|
+
from datetime import datetime
|
|
70
|
+
import logging
|
|
71
|
+
import asyncio
|
|
72
|
+
import pyaudio
|
|
73
|
+
import sys
|
|
74
|
+
|
|
68
75
|
|
|
69
|
-
|
|
76
|
+
debug_logging = False
|
|
77
|
+
extended_logging = False
|
|
70
78
|
send_recorded_chunk = False
|
|
71
79
|
log_incoming_chunks = False
|
|
72
80
|
stt_optimizations = False
|
|
81
|
+
writechunks = False#
|
|
82
|
+
wav_file = None
|
|
83
|
+
loglevel = logging.WARNING
|
|
73
84
|
|
|
85
|
+
FORMAT = pyaudio.paInt16
|
|
86
|
+
CHANNELS = 1
|
|
74
87
|
|
|
75
|
-
from .install_packages import check_and_install_packages
|
|
76
|
-
from datetime import datetime
|
|
77
|
-
import asyncio
|
|
78
|
-
import base64
|
|
79
|
-
import sys
|
|
80
88
|
|
|
81
89
|
if sys.platform == 'win32':
|
|
82
90
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
@@ -117,17 +125,25 @@ class bcolors:
|
|
|
117
125
|
|
|
118
126
|
print(f"{bcolors.BOLD}{bcolors.OKCYAN}Starting server, please wait...{bcolors.ENDC}")
|
|
119
127
|
|
|
120
|
-
|
|
121
|
-
import
|
|
122
|
-
|
|
128
|
+
# Initialize colorama
|
|
129
|
+
from colorama import init, Fore, Style
|
|
130
|
+
init()
|
|
131
|
+
|
|
123
132
|
from RealtimeSTT import AudioToTextRecorder
|
|
124
|
-
import numpy as np
|
|
125
133
|
from scipy.signal import resample
|
|
134
|
+
import numpy as np
|
|
135
|
+
import websockets
|
|
136
|
+
import threading
|
|
137
|
+
import logging
|
|
138
|
+
import wave
|
|
139
|
+
import json
|
|
140
|
+
import time
|
|
126
141
|
|
|
127
142
|
global_args = None
|
|
128
143
|
recorder = None
|
|
129
144
|
recorder_config = {}
|
|
130
145
|
recorder_ready = threading.Event()
|
|
146
|
+
recorder_thread = None
|
|
131
147
|
stop_recorder = False
|
|
132
148
|
prev_text = ""
|
|
133
149
|
|
|
@@ -174,6 +190,12 @@ def preprocess_text(text):
|
|
|
174
190
|
|
|
175
191
|
return text
|
|
176
192
|
|
|
193
|
+
def debug_print(message):
|
|
194
|
+
if debug_logging:
|
|
195
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
|
196
|
+
thread_name = threading.current_thread().name
|
|
197
|
+
print(f"{Fore.CYAN}[DEBUG][{timestamp}][{thread_name}] {message}{Style.RESET_ALL}", file=sys.stderr)
|
|
198
|
+
|
|
177
199
|
def text_detected(text, loop):
|
|
178
200
|
global prev_text
|
|
179
201
|
|
|
@@ -259,26 +281,28 @@ def on_transcription_start(loop):
|
|
|
259
281
|
})
|
|
260
282
|
asyncio.run_coroutine_threadsafe(audio_queue.put(message), loop)
|
|
261
283
|
|
|
262
|
-
def on_realtime_transcription_update(text, loop):
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def on_recorded_chunk(chunk, loop):
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
284
|
+
# def on_realtime_transcription_update(text, loop):
|
|
285
|
+
# # Send real-time transcription updates to the client
|
|
286
|
+
# text = preprocess_text(text)
|
|
287
|
+
# message = json.dumps({
|
|
288
|
+
# 'type': 'realtime_update',
|
|
289
|
+
# 'text': text
|
|
290
|
+
# })
|
|
291
|
+
# asyncio.run_coroutine_threadsafe(audio_queue.put(message), loop)
|
|
292
|
+
|
|
293
|
+
# def on_recorded_chunk(chunk, loop):
|
|
294
|
+
# if send_recorded_chunk:
|
|
295
|
+
# bytes_b64 = base64.b64encode(chunk.tobytes()).decode('utf-8')
|
|
296
|
+
# message = json.dumps({
|
|
297
|
+
# 'type': 'recorded_chunk',
|
|
298
|
+
# 'bytes': bytes_b64
|
|
299
|
+
# })
|
|
300
|
+
# asyncio.run_coroutine_threadsafe(audio_queue.put(message), loop)
|
|
279
301
|
|
|
280
302
|
# Define the server's arguments
|
|
281
303
|
def parse_arguments():
|
|
304
|
+
global debug_logging, extended_logging, loglevel, writechunks, log_incoming_chunks
|
|
305
|
+
|
|
282
306
|
import argparse
|
|
283
307
|
parser = argparse.ArgumentParser(description='Start the Speech-to-Text (STT) server with various configuration options.')
|
|
284
308
|
|
|
@@ -346,7 +370,7 @@ def parse_arguments():
|
|
|
346
370
|
parser.add_argument('--data_port', type=int, default=8012,
|
|
347
371
|
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
372
|
|
|
349
|
-
parser.add_argument('--wake_words', type=str, default="
|
|
373
|
+
parser.add_argument('--wake_words', type=str, default="",
|
|
350
374
|
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
375
|
|
|
352
376
|
parser.add_argument('--wake_words_sensitivity', type=float, default=0.5,
|
|
@@ -358,7 +382,7 @@ def parse_arguments():
|
|
|
358
382
|
parser.add_argument('--wake_word_activation_delay', type=float, default=20,
|
|
359
383
|
help='The delay in seconds before the wake word detection is activated after the system starts listening. This prevents false positives during the start of a session. Default is 0.5 seconds.')
|
|
360
384
|
|
|
361
|
-
parser.add_argument('--wakeword_backend', type=str, default='
|
|
385
|
+
parser.add_argument('--wakeword_backend', type=str, default='none',
|
|
362
386
|
help='The backend used for wake word detection. You can specify different backends such as "default" or any custom implementations depending on your setup. Default is "pvporcupine".')
|
|
363
387
|
|
|
364
388
|
parser.add_argument('--openwakeword_model_paths', type=str, nargs='*',
|
|
@@ -376,9 +400,27 @@ def parse_arguments():
|
|
|
376
400
|
parser.add_argument('--use_extended_logging', action='store_true',
|
|
377
401
|
help='Writes extensive log messages for the recording worker, that processes the audio chunks.')
|
|
378
402
|
|
|
403
|
+
parser.add_argument('--debug', action='store_true', help='Enable debug logging for detailed server operations')
|
|
404
|
+
|
|
405
|
+
parser.add_argument('--logchunks', action='store_true', help='Enable logging of incoming audio chunks (periods)')
|
|
406
|
+
|
|
407
|
+
parser.add_argument("--writechunks", metavar="FILE", help="Save received audio chunks to a WAV file")
|
|
408
|
+
|
|
379
409
|
# Parse arguments
|
|
380
410
|
args = parser.parse_args()
|
|
381
411
|
|
|
412
|
+
debug_logging = args.debug
|
|
413
|
+
extended_logging = args.use_extended_logging
|
|
414
|
+
writechunks = args.writechunks
|
|
415
|
+
log_incoming_chunks = args.logchunks
|
|
416
|
+
|
|
417
|
+
if debug_logging:
|
|
418
|
+
loglevel = logging.DEBUG
|
|
419
|
+
logging.basicConfig(level=loglevel, format='[%(asctime)s] %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
|
420
|
+
else:
|
|
421
|
+
loglevel = logging.WARNING
|
|
422
|
+
|
|
423
|
+
|
|
382
424
|
# Replace escaped newlines with actual newlines in initial_prompt
|
|
383
425
|
if args.initial_prompt:
|
|
384
426
|
args.initial_prompt = args.initial_prompt.replace("\\n", "\n")
|
|
@@ -437,11 +479,13 @@ def decode_and_resample(
|
|
|
437
479
|
return resampled_audio.astype(np.int16).tobytes()
|
|
438
480
|
|
|
439
481
|
async def control_handler(websocket, path):
|
|
482
|
+
debug_print(f"New control connection from {websocket.remote_address}")
|
|
440
483
|
print(f"{bcolors.OKGREEN}Control client connected{bcolors.ENDC}")
|
|
441
484
|
global recorder
|
|
442
485
|
control_connections.add(websocket)
|
|
443
486
|
try:
|
|
444
487
|
async for message in websocket:
|
|
488
|
+
debug_print(f"Received control message: {message[:200]}...")
|
|
445
489
|
if not recorder_ready.is_set():
|
|
446
490
|
print(f"{bcolors.WARNING}Recorder not ready{bcolors.ENDC}")
|
|
447
491
|
continue
|
|
@@ -530,21 +574,38 @@ async def control_handler(websocket, path):
|
|
|
530
574
|
control_connections.remove(websocket)
|
|
531
575
|
|
|
532
576
|
async def data_handler(websocket, path):
|
|
577
|
+
global writechunks, wav_file
|
|
533
578
|
print(f"{bcolors.OKGREEN}Data client connected{bcolors.ENDC}")
|
|
534
579
|
data_connections.add(websocket)
|
|
535
580
|
try:
|
|
536
581
|
while True:
|
|
537
582
|
message = await websocket.recv()
|
|
538
583
|
if isinstance(message, bytes):
|
|
539
|
-
if
|
|
584
|
+
if debug_logging:
|
|
585
|
+
debug_print(f"Received audio chunk (size: {len(message)} bytes)")
|
|
586
|
+
elif log_incoming_chunks:
|
|
540
587
|
print(".", end='', flush=True)
|
|
541
588
|
# Handle binary message (audio data)
|
|
542
589
|
metadata_length = int.from_bytes(message[:4], byteorder='little')
|
|
543
590
|
metadata_json = message[4:4+metadata_length].decode('utf-8')
|
|
544
591
|
metadata = json.loads(metadata_json)
|
|
545
592
|
sample_rate = metadata['sampleRate']
|
|
593
|
+
|
|
594
|
+
debug_print(f"Processing audio chunk with sample rate {sample_rate}")
|
|
546
595
|
chunk = message[4+metadata_length:]
|
|
596
|
+
|
|
597
|
+
if writechunks:
|
|
598
|
+
if not wav_file:
|
|
599
|
+
wav_file = wave.open(writechunks, 'wb')
|
|
600
|
+
wav_file.setnchannels(CHANNELS)
|
|
601
|
+
wav_file.setsampwidth(pyaudio.get_sample_size(FORMAT))
|
|
602
|
+
wav_file.setframerate(sample_rate)
|
|
603
|
+
|
|
604
|
+
wav_file.writeframes(chunk)
|
|
605
|
+
|
|
547
606
|
resampled_chunk = decode_and_resample(chunk, sample_rate, 16000)
|
|
607
|
+
|
|
608
|
+
debug_print(f"Resampled chunk size: {len(resampled_chunk)} bytes")
|
|
548
609
|
recorder.feed_audio(resampled_chunk)
|
|
549
610
|
else:
|
|
550
611
|
print(f"{bcolors.WARNING}Received non-binary message on data connection{bcolors.ENDC}")
|
|
@@ -619,50 +680,58 @@ async def main_async():
|
|
|
619
680
|
'on_wakeword_detection_start': make_callback(loop, on_wakeword_detection_start),
|
|
620
681
|
'on_wakeword_detection_end': make_callback(loop, on_wakeword_detection_end),
|
|
621
682
|
'on_transcription_start': make_callback(loop, on_transcription_start),
|
|
622
|
-
'on_recorded_chunk': make_callback(loop, on_recorded_chunk),
|
|
683
|
+
# 'on_recorded_chunk': make_callback(loop, on_recorded_chunk),
|
|
623
684
|
'no_log_file': True, # Disable logging to file
|
|
624
685
|
'use_extended_logging': args.use_extended_logging,
|
|
686
|
+
'level': loglevel,
|
|
625
687
|
}
|
|
626
688
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
689
|
+
try:
|
|
690
|
+
# Attempt to start control and data servers
|
|
691
|
+
control_server = await websockets.serve(control_handler, "localhost", args.control_port)
|
|
692
|
+
data_server = await websockets.serve(data_handler, "localhost", args.data_port)
|
|
693
|
+
print(f"{bcolors.OKGREEN}Control server started on {bcolors.OKBLUE}ws://localhost:{args.control_port}{bcolors.ENDC}")
|
|
694
|
+
print(f"{bcolors.OKGREEN}Data server started on {bcolors.OKBLUE}ws://localhost:{args.data_port}{bcolors.ENDC}")
|
|
631
695
|
|
|
632
|
-
|
|
633
|
-
|
|
696
|
+
# Start the broadcast and recorder threads
|
|
697
|
+
broadcast_task = asyncio.create_task(broadcast_audio_messages())
|
|
634
698
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
699
|
+
recorder_thread = threading.Thread(target=_recorder_thread, args=(loop,))
|
|
700
|
+
recorder_thread.start()
|
|
701
|
+
recorder_ready.wait()
|
|
638
702
|
|
|
639
|
-
|
|
703
|
+
print(f"{bcolors.OKGREEN}Server started. Press Ctrl+C to stop the server.{bcolors.ENDC}")
|
|
640
704
|
|
|
641
|
-
|
|
705
|
+
# Run server tasks
|
|
642
706
|
await asyncio.gather(control_server.wait_closed(), data_server.wait_closed(), broadcast_task)
|
|
707
|
+
except OSError as e:
|
|
708
|
+
print(f"{bcolors.FAIL}Error: Could not start server on specified ports. It’s possible another instance of the server is already running, or the ports are being used by another application.{bcolors.ENDC}")
|
|
643
709
|
except KeyboardInterrupt:
|
|
644
|
-
print(f"{bcolors.WARNING}
|
|
710
|
+
print(f"{bcolors.WARNING}Server interrupted by user, shutting down...{bcolors.ENDC}")
|
|
645
711
|
finally:
|
|
646
|
-
#
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
712
|
+
# Shutdown procedures for recorder and server threads
|
|
713
|
+
await shutdown_procedure()
|
|
714
|
+
print(f"{bcolors.OKGREEN}Server shutdown complete.{bcolors.ENDC}")
|
|
715
|
+
|
|
716
|
+
async def shutdown_procedure():
|
|
717
|
+
global stop_recorder, recorder_thread
|
|
718
|
+
if recorder:
|
|
719
|
+
stop_recorder = True
|
|
720
|
+
recorder.abort()
|
|
721
|
+
recorder.stop()
|
|
722
|
+
recorder.shutdown()
|
|
723
|
+
print(f"{bcolors.OKGREEN}Recorder shut down{bcolors.ENDC}")
|
|
724
|
+
|
|
725
|
+
if recorder_thread:
|
|
654
726
|
recorder_thread.join()
|
|
655
727
|
print(f"{bcolors.OKGREEN}Recorder thread finished{bcolors.ENDC}")
|
|
656
|
-
|
|
657
|
-
|
|
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}")
|
|
728
|
+
|
|
729
|
+
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
|
730
|
+
for task in tasks:
|
|
731
|
+
task.cancel()
|
|
732
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
733
|
+
|
|
734
|
+
print(f"{bcolors.OKGREEN}All tasks cancelled, closing event loop now.{bcolors.ENDC}")
|
|
666
735
|
|
|
667
736
|
def main():
|
|
668
737
|
try:
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import setuptools
|
|
2
|
-
|
|
3
|
-
with open("README.md", "r") as fh:
|
|
4
|
-
long_description = fh.read()
|
|
5
|
-
|
|
6
|
-
# Read requirements.txt
|
|
7
|
-
with open('requirements.txt') as f:
|
|
8
|
-
requirements = f.read().splitlines()
|
|
9
|
-
|
|
10
|
-
setuptools.setup(
|
|
11
|
-
name="RealtimeSTT",
|
|
12
|
-
version="0.3.
|
|
13
|
-
author="Kolja Beigel",
|
|
14
|
-
author_email="kolja.beigel@web.de",
|
|
15
|
-
description="A fast Voice Activity Detection and Transcription System",
|
|
16
|
-
long_description=long_description,
|
|
17
|
-
long_description_content_type="text/markdown",
|
|
18
|
-
url="https://github.com/KoljaB/RealTimeSTT",
|
|
19
|
-
packages=setuptools.find_packages(),
|
|
20
|
-
classifiers=[
|
|
21
|
-
"Programming Language :: Python :: 3",
|
|
22
|
-
"License :: OSI Approved :: MIT License",
|
|
23
|
-
"Operating System :: OS Independent",
|
|
24
|
-
],
|
|
25
|
-
python_requires='>=3.6',
|
|
26
|
-
install_requires=requirements,
|
|
27
|
-
keywords="real-time, audio, transcription, speech-to-text, voice-activity-detection, VAD, real-time-transcription, ambient-noise-detection, microphone-input, faster_whisper, speech-recognition, voice-assistants, audio-processing, buffered-transcription, pyaudio, ambient-noise-level, voice-deactivity",
|
|
28
|
-
entry_points={
|
|
29
|
-
'console_scripts': [
|
|
30
|
-
'stt-server=
|
|
31
|
-
'stt=
|
|
32
|
-
],
|
|
33
|
-
},
|
|
34
|
-
)
|
|
1
|
+
import setuptools
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
# Read requirements.txt
|
|
7
|
+
with open('requirements.txt') as f:
|
|
8
|
+
requirements = f.read().splitlines()
|
|
9
|
+
|
|
10
|
+
setuptools.setup(
|
|
11
|
+
name="RealtimeSTT",
|
|
12
|
+
version="0.3.6",
|
|
13
|
+
author="Kolja Beigel",
|
|
14
|
+
author_email="kolja.beigel@web.de",
|
|
15
|
+
description="A fast Voice Activity Detection and Transcription System",
|
|
16
|
+
long_description=long_description,
|
|
17
|
+
long_description_content_type="text/markdown",
|
|
18
|
+
url="https://github.com/KoljaB/RealTimeSTT",
|
|
19
|
+
packages=setuptools.find_packages(include=["RealtimeSTT", "RealtimeSTT_server"]),
|
|
20
|
+
classifiers=[
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
],
|
|
25
|
+
python_requires='>=3.6',
|
|
26
|
+
install_requires=requirements,
|
|
27
|
+
keywords="real-time, audio, transcription, speech-to-text, voice-activity-detection, VAD, real-time-transcription, ambient-noise-detection, microphone-input, faster_whisper, speech-recognition, voice-assistants, audio-processing, buffered-transcription, pyaudio, ambient-noise-level, voice-deactivity",
|
|
28
|
+
entry_points={
|
|
29
|
+
'console_scripts': [
|
|
30
|
+
'stt-server=RealtimeSTT_server.stt_server:main',
|
|
31
|
+
'stt=RealtimeSTT_server.stt_cli_client:main',
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|