RealTimeSTT 0.3.7__tar.gz → 0.3.8__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.7 → RealtimeSTT-0.3.8}/PKG-INFO +4 -14
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/README.md +3 -1
- RealtimeSTT-0.3.8/RealtimeSTT/__init__.py +3 -0
- RealtimeSTT-0.3.8/RealtimeSTT/audio_input.py +153 -0
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/RealtimeSTT/audio_recorder_client.py +73 -72
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/RealtimeSTT.egg-info/PKG-INFO +4 -14
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/RealtimeSTT.egg-info/SOURCES.txt +1 -0
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/RealtimeSTT.egg-info/requires.txt +1 -0
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/RealtimeSTT_server/install_packages.py +0 -50
- RealtimeSTT-0.3.8/RealtimeSTT_server/stt_cli_client.py +283 -0
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/RealtimeSTT_server/stt_server.py +70 -10
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/setup.py +1 -1
- realtimestt-0.3.7/RealtimeSTT/__init__.py +0 -2
- realtimestt-0.3.7/RealtimeSTT_server/stt_cli_client.py +0 -659
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/LICENSE +0 -0
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/RealtimeSTT/audio_recorder.py +0 -0
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/RealtimeSTT.egg-info/dependency_links.txt +0 -0
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/RealtimeSTT.egg-info/entry_points.txt +0 -0
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/RealtimeSTT.egg-info/top_level.txt +0 -0
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/RealtimeSTT_server/__init__.py +0 -0
- {realtimestt-0.3.7 → RealtimeSTT-0.3.8}/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.8
|
|
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
|
|
@@ -12,18 +12,6 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Requires-Python: >=3.6
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: PyAudio==0.2.14
|
|
16
|
-
Requires-Dist: faster-whisper==1.0.3
|
|
17
|
-
Requires-Dist: pvporcupine==1.9.5
|
|
18
|
-
Requires-Dist: webrtcvad-wheels==2.0.14
|
|
19
|
-
Requires-Dist: halo==0.0.31
|
|
20
|
-
Requires-Dist: torch
|
|
21
|
-
Requires-Dist: torchaudio
|
|
22
|
-
Requires-Dist: scipy==1.14.1
|
|
23
|
-
Requires-Dist: websockets==v12.0
|
|
24
|
-
Requires-Dist: websocket-client==1.8.0
|
|
25
|
-
Requires-Dist: openwakeword>=0.4.0
|
|
26
|
-
Requires-Dist: numpy<2.0.0
|
|
27
15
|
|
|
28
16
|
# RealtimeSTT
|
|
29
17
|
[](https://pypi.org/project/RealtimeSTT/)
|
|
@@ -55,7 +43,7 @@ https://github.com/user-attachments/assets/797e6552-27cd-41b1-a7f3-e5cbc72094f5
|
|
|
55
43
|
|
|
56
44
|
### Updates
|
|
57
45
|
|
|
58
|
-
Latest Version: v0.3.
|
|
46
|
+
Latest Version: v0.3.8
|
|
59
47
|
|
|
60
48
|
See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
|
|
61
49
|
|
|
@@ -462,6 +450,8 @@ When you initialize the `AudioToTextRecorder` class, you have various options to
|
|
|
462
450
|
|
|
463
451
|
- **level** (int, default=logging.WARNING): Logging level.
|
|
464
452
|
|
|
453
|
+
- **init_logging** (bool, default=True): Whether to initialize the logging framework. Set to False to manage this yourself.
|
|
454
|
+
|
|
465
455
|
- **handle_buffer_overflow** (bool, default=True): If set, the system will log a warning when an input overflow occurs during recording and remove the data from the buffer.
|
|
466
456
|
|
|
467
457
|
- **beam_size** (int, default=5): The beam size to use for beam search decoding.
|
|
@@ -28,7 +28,7 @@ https://github.com/user-attachments/assets/797e6552-27cd-41b1-a7f3-e5cbc72094f5
|
|
|
28
28
|
|
|
29
29
|
### Updates
|
|
30
30
|
|
|
31
|
-
Latest Version: v0.3.
|
|
31
|
+
Latest Version: v0.3.8
|
|
32
32
|
|
|
33
33
|
See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
|
|
34
34
|
|
|
@@ -435,6 +435,8 @@ When you initialize the `AudioToTextRecorder` class, you have various options to
|
|
|
435
435
|
|
|
436
436
|
- **level** (int, default=logging.WARNING): Logging level.
|
|
437
437
|
|
|
438
|
+
- **init_logging** (bool, default=True): Whether to initialize the logging framework. Set to False to manage this yourself.
|
|
439
|
+
|
|
438
440
|
- **handle_buffer_overflow** (bool, default=True): If set, the system will log a warning when an input overflow occurs during recording and remove the data from the buffer.
|
|
439
441
|
|
|
440
442
|
- **beam_size** (int, default=5): The beam size to use for beam search decoding.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from colorama import init, Fore, Style
|
|
2
|
+
import pyaudio
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AudioInput:
|
|
8
|
+
def __init__(self, input_device_index=None, debug_mode=False):
|
|
9
|
+
self.input_device_index = input_device_index
|
|
10
|
+
self.debug_mode = debug_mode
|
|
11
|
+
self.audio_interface = None
|
|
12
|
+
self.stream = None
|
|
13
|
+
self.device_sample_rate = None
|
|
14
|
+
|
|
15
|
+
# PyAudio constants
|
|
16
|
+
self.CHUNK = 1024
|
|
17
|
+
self.FORMAT = pyaudio.paInt16
|
|
18
|
+
self.CHANNELS = 1
|
|
19
|
+
self.DESIRED_RATE = 16000
|
|
20
|
+
|
|
21
|
+
def get_supported_sample_rates(self, device_index):
|
|
22
|
+
"""Test which standard sample rates are supported by the specified device."""
|
|
23
|
+
standard_rates = [8000, 9600, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000]
|
|
24
|
+
supported_rates = []
|
|
25
|
+
|
|
26
|
+
device_info = self.audio_interface.get_device_info_by_index(device_index)
|
|
27
|
+
max_channels = device_info.get('maxInputChannels') # Changed from maxOutputChannels
|
|
28
|
+
|
|
29
|
+
for rate in standard_rates:
|
|
30
|
+
try:
|
|
31
|
+
if self.audio_interface.is_format_supported(
|
|
32
|
+
rate,
|
|
33
|
+
input_device=device_index, # Changed to input_device
|
|
34
|
+
input_channels=max_channels, # Changed to input_channels
|
|
35
|
+
input_format=self.FORMAT, # Changed to input_format
|
|
36
|
+
):
|
|
37
|
+
supported_rates.append(rate)
|
|
38
|
+
except:
|
|
39
|
+
continue
|
|
40
|
+
return supported_rates
|
|
41
|
+
|
|
42
|
+
def _get_best_sample_rate(self, actual_device_index, desired_rate):
|
|
43
|
+
"""Determines the best available sample rate for the device."""
|
|
44
|
+
try:
|
|
45
|
+
device_info = self.audio_interface.get_device_info_by_index(actual_device_index)
|
|
46
|
+
supported_rates = self.get_supported_sample_rates(actual_device_index)
|
|
47
|
+
|
|
48
|
+
if desired_rate in supported_rates:
|
|
49
|
+
return desired_rate
|
|
50
|
+
|
|
51
|
+
return max(supported_rates)
|
|
52
|
+
|
|
53
|
+
# lower_rates = [r for r in supported_rates if r <= desired_rate]
|
|
54
|
+
# if lower_rates:
|
|
55
|
+
# return max(lower_rates)
|
|
56
|
+
|
|
57
|
+
# higher_rates = [r for r in supported_rates if r > desired_rate]
|
|
58
|
+
# if higher_rates:
|
|
59
|
+
# return min(higher_rates)
|
|
60
|
+
|
|
61
|
+
return int(device_info.get('defaultSampleRate', 44100))
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logging.warning(f"Error determining sample rate: {e}")
|
|
65
|
+
return 44100 # Safe fallback
|
|
66
|
+
|
|
67
|
+
def list_devices(self):
|
|
68
|
+
"""List all available audio input devices with supported sample rates."""
|
|
69
|
+
try:
|
|
70
|
+
init() # Initialize colorama
|
|
71
|
+
self.audio_interface = pyaudio.PyAudio()
|
|
72
|
+
device_count = self.audio_interface.get_device_count()
|
|
73
|
+
|
|
74
|
+
print(f"Available audio input devices:")
|
|
75
|
+
#print(f"{Fore.LIGHTBLUE_EX}Available audio input devices:{Style.RESET_ALL}")
|
|
76
|
+
for i in range(device_count):
|
|
77
|
+
device_info = self.audio_interface.get_device_info_by_index(i)
|
|
78
|
+
device_name = device_info.get('name')
|
|
79
|
+
max_input_channels = device_info.get('maxInputChannels', 0)
|
|
80
|
+
|
|
81
|
+
if max_input_channels > 0: # Only consider devices with input capabilities
|
|
82
|
+
supported_rates = self.get_supported_sample_rates(i)
|
|
83
|
+
print(f"{Fore.LIGHTGREEN_EX}Device {Style.RESET_ALL}{i}{Fore.LIGHTGREEN_EX}: {device_name}{Style.RESET_ALL}")
|
|
84
|
+
|
|
85
|
+
# Format each rate in cyan
|
|
86
|
+
if supported_rates:
|
|
87
|
+
rates_formatted = ", ".join([f"{Fore.CYAN}{rate}{Style.RESET_ALL}" for rate in supported_rates])
|
|
88
|
+
print(f" {Fore.YELLOW}Supported sample rates: {rates_formatted}{Style.RESET_ALL}")
|
|
89
|
+
else:
|
|
90
|
+
print(f" {Fore.YELLOW}Supported sample rates: None{Style.RESET_ALL}")
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"Error listing devices: {e}")
|
|
94
|
+
finally:
|
|
95
|
+
if self.audio_interface:
|
|
96
|
+
self.audio_interface.terminate()
|
|
97
|
+
|
|
98
|
+
def setup(self):
|
|
99
|
+
"""Initialize audio interface and open stream"""
|
|
100
|
+
try:
|
|
101
|
+
self.audio_interface = pyaudio.PyAudio()
|
|
102
|
+
|
|
103
|
+
if self.debug_mode:
|
|
104
|
+
print(f"Input device index: {self.input_device_index}")
|
|
105
|
+
actual_device_index = (self.input_device_index if self.input_device_index is not None
|
|
106
|
+
else self.audio_interface.get_default_input_device_info()['index'])
|
|
107
|
+
|
|
108
|
+
if self.debug_mode:
|
|
109
|
+
print(f"Actual selected device index: {actual_device_index}")
|
|
110
|
+
self.input_device_index = actual_device_index
|
|
111
|
+
self.device_sample_rate = self._get_best_sample_rate(actual_device_index, self.DESIRED_RATE)
|
|
112
|
+
|
|
113
|
+
if self.debug_mode:
|
|
114
|
+
print(f"Setting up audio on device {self.input_device_index} with sample rate {self.device_sample_rate}")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
self.stream = self.audio_interface.open(
|
|
118
|
+
format=self.FORMAT,
|
|
119
|
+
channels=self.CHANNELS,
|
|
120
|
+
rate=self.device_sample_rate,
|
|
121
|
+
input=True,
|
|
122
|
+
frames_per_buffer=self.CHUNK,
|
|
123
|
+
input_device_index=self.input_device_index,
|
|
124
|
+
)
|
|
125
|
+
if self.debug_mode:
|
|
126
|
+
print(f"Audio recording initialized successfully at {self.device_sample_rate} Hz")
|
|
127
|
+
return True
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"Failed to initialize audio stream at {self.device_sample_rate} Hz: {e}")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f"Error initializing audio recording: {e}")
|
|
134
|
+
if self.audio_interface:
|
|
135
|
+
self.audio_interface.terminate()
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def read_chunk(self):
|
|
139
|
+
"""Read a chunk of audio data"""
|
|
140
|
+
return self.stream.read(self.CHUNK, exception_on_overflow=False)
|
|
141
|
+
|
|
142
|
+
def cleanup(self):
|
|
143
|
+
"""Clean up audio resources"""
|
|
144
|
+
try:
|
|
145
|
+
if self.stream:
|
|
146
|
+
self.stream.stop_stream()
|
|
147
|
+
self.stream.close()
|
|
148
|
+
self.stream = None
|
|
149
|
+
if self.audio_interface:
|
|
150
|
+
self.audio_interface.terminate()
|
|
151
|
+
self.audio_interface = None
|
|
152
|
+
except Exception as e:
|
|
153
|
+
print(f"Error cleaning up audio resources: {e}")
|
|
@@ -9,15 +9,18 @@ import websocket
|
|
|
9
9
|
import threading
|
|
10
10
|
import platform
|
|
11
11
|
import logging
|
|
12
|
-
import pyaudio
|
|
13
12
|
import socket
|
|
14
13
|
import struct
|
|
15
14
|
import signal
|
|
15
|
+
import wave
|
|
16
16
|
import json
|
|
17
17
|
import time
|
|
18
18
|
import sys
|
|
19
19
|
import os
|
|
20
20
|
|
|
21
|
+
# Import the AudioInput class
|
|
22
|
+
from .audio_input import AudioInput
|
|
23
|
+
|
|
21
24
|
DEFAULT_CONTROL_URL = "ws://127.0.0.1:8011"
|
|
22
25
|
DEFAULT_DATA_URL = "ws://127.0.0.1:8012"
|
|
23
26
|
|
|
@@ -36,11 +39,8 @@ INIT_WAKE_WORD_TIMEOUT = 5.0
|
|
|
36
39
|
INIT_WAKE_WORD_BUFFER_DURATION = 0.1
|
|
37
40
|
ALLOWED_LATENCY_LIMIT = 100
|
|
38
41
|
|
|
39
|
-
CHUNK = 1024
|
|
40
|
-
FORMAT = pyaudio.paInt16
|
|
41
|
-
CHANNELS = 1
|
|
42
|
-
SAMPLE_RATE = 16000
|
|
43
42
|
BUFFER_SIZE = 512
|
|
43
|
+
SAMPLE_RATE = 16000
|
|
44
44
|
|
|
45
45
|
INIT_HANDLE_BUFFER_OVERFLOW = False
|
|
46
46
|
if platform.system() != 'Darwin':
|
|
@@ -143,6 +143,7 @@ class AudioToTextRecorderClient:
|
|
|
143
143
|
control_url: str = DEFAULT_CONTROL_URL,
|
|
144
144
|
data_url: str = DEFAULT_DATA_URL,
|
|
145
145
|
autostart_server: bool = True,
|
|
146
|
+
output_wav_file: str = None,
|
|
146
147
|
):
|
|
147
148
|
|
|
148
149
|
# Set instance variables from constructor parameters
|
|
@@ -213,6 +214,7 @@ class AudioToTextRecorderClient:
|
|
|
213
214
|
self.control_url = control_url
|
|
214
215
|
self.data_url = data_url
|
|
215
216
|
self.autostart_server = autostart_server
|
|
217
|
+
self.output_wav_file = output_wav_file
|
|
216
218
|
|
|
217
219
|
# Instance variables
|
|
218
220
|
self.muted = False
|
|
@@ -223,6 +225,9 @@ class AudioToTextRecorderClient:
|
|
|
223
225
|
self.final_text_ready = threading.Event()
|
|
224
226
|
self.realtime_text = ""
|
|
225
227
|
self.final_text = ""
|
|
228
|
+
self._recording = False
|
|
229
|
+
self.server_already_running = False
|
|
230
|
+
self.wav_file = None
|
|
226
231
|
|
|
227
232
|
self.request_counter = 0
|
|
228
233
|
self.pending_requests = {} # Map from request_id to threading.Event and value
|
|
@@ -238,6 +243,13 @@ class AudioToTextRecorderClient:
|
|
|
238
243
|
if self.use_microphone:
|
|
239
244
|
self.start_recording()
|
|
240
245
|
|
|
246
|
+
|
|
247
|
+
if self.server_already_running:
|
|
248
|
+
self.set_parameter("language", self.language)
|
|
249
|
+
# self.set_parameter("model", self.model)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
241
253
|
def text(self, on_transcription_finished=None):
|
|
242
254
|
self.realtime_text = ""
|
|
243
255
|
self.submitted_realtime_text = ""
|
|
@@ -251,17 +263,15 @@ class AudioToTextRecorderClient:
|
|
|
251
263
|
wait_interval = 0.02 # Wait in small intervals, e.g., 100ms
|
|
252
264
|
max_wait_time = 60 # Timeout after 60 seconds
|
|
253
265
|
|
|
254
|
-
while total_wait_time < max_wait_time:
|
|
266
|
+
while total_wait_time < max_wait_time and self.is_running and self._recording:
|
|
255
267
|
if self.final_text_ready.wait(timeout=wait_interval):
|
|
256
268
|
break # Break if transcription is ready
|
|
257
|
-
|
|
258
|
-
# if not self.realtime_text == self.submitted_realtime_text:
|
|
259
|
-
# if self.on_realtime_transcription_update:
|
|
260
|
-
# self.on_realtime_transcription_update(self.realtime_text)
|
|
261
|
-
# self.submitted_realtime_text = self.realtime_text
|
|
262
269
|
|
|
263
|
-
|
|
270
|
+
if not self.is_running or not self._recording:
|
|
271
|
+
break
|
|
264
272
|
|
|
273
|
+
total_wait_time += wait_interval
|
|
274
|
+
|
|
265
275
|
# Check if a manual interrupt has occurred
|
|
266
276
|
if total_wait_time >= max_wait_time:
|
|
267
277
|
if self.debug_mode:
|
|
@@ -273,6 +283,9 @@ class AudioToTextRecorderClient:
|
|
|
273
283
|
|
|
274
284
|
self.recording_start.clear()
|
|
275
285
|
|
|
286
|
+
if not self.is_running or not self._recording:
|
|
287
|
+
return ""
|
|
288
|
+
|
|
276
289
|
if on_transcription_finished:
|
|
277
290
|
threading.Thread(target=on_transcription_finished, args=(self.final_text,)).start()
|
|
278
291
|
|
|
@@ -280,7 +293,7 @@ class AudioToTextRecorderClient:
|
|
|
280
293
|
|
|
281
294
|
except KeyboardInterrupt:
|
|
282
295
|
if self.debug_mode:
|
|
283
|
-
print("KeyboardInterrupt in
|
|
296
|
+
print("KeyboardInterrupt in text(), exiting...")
|
|
284
297
|
raise KeyboardInterrupt
|
|
285
298
|
|
|
286
299
|
except Exception as e:
|
|
@@ -318,7 +331,7 @@ class AudioToTextRecorderClient:
|
|
|
318
331
|
if not self.ensure_server_running():
|
|
319
332
|
print("Cannot start STT server. Exiting.")
|
|
320
333
|
return False
|
|
321
|
-
|
|
334
|
+
|
|
322
335
|
try:
|
|
323
336
|
# Connect to control WebSocket
|
|
324
337
|
self.control_ws = websocket.WebSocketApp(self.control_url,
|
|
@@ -438,7 +451,7 @@ class AudioToTextRecorderClient:
|
|
|
438
451
|
if not self.is_server_running():
|
|
439
452
|
if self.debug_mode:
|
|
440
453
|
print("STT server is not running.", file=sys.stderr)
|
|
441
|
-
if self.autostart_server
|
|
454
|
+
if self.autostart_server:
|
|
442
455
|
self.start_server()
|
|
443
456
|
if self.debug_mode:
|
|
444
457
|
print("Waiting for STT server to start...", file=sys.stderr)
|
|
@@ -454,7 +467,16 @@ class AudioToTextRecorderClient:
|
|
|
454
467
|
else:
|
|
455
468
|
print("STT server is required. Please start it manually.", file=sys.stderr)
|
|
456
469
|
return False
|
|
470
|
+
|
|
471
|
+
else:
|
|
472
|
+
self.server_already_running = True
|
|
473
|
+
|
|
457
474
|
return True
|
|
475
|
+
|
|
476
|
+
def list_devices(self):
|
|
477
|
+
"""List all available audio input devices."""
|
|
478
|
+
audio = AudioInput(debug_mode=self.debug_mode)
|
|
479
|
+
audio.list_devices()
|
|
458
480
|
|
|
459
481
|
def start_recording(self):
|
|
460
482
|
self.recording_thread = threading.Thread(target=self.record_and_send_audio)
|
|
@@ -462,45 +484,29 @@ class AudioToTextRecorderClient:
|
|
|
462
484
|
self.recording_thread.start()
|
|
463
485
|
|
|
464
486
|
def setup_audio(self):
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
self.input_device_index
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
except OSError as e:
|
|
472
|
-
print(f"No default input device found: {e}")
|
|
473
|
-
return False
|
|
474
|
-
|
|
475
|
-
self.device_sample_rate = 16000 # Try 16000 Hz first
|
|
476
|
-
|
|
477
|
-
try:
|
|
478
|
-
self.stream = self.audio_interface.open(
|
|
479
|
-
format=FORMAT,
|
|
480
|
-
channels=CHANNELS,
|
|
481
|
-
rate=self.device_sample_rate,
|
|
482
|
-
input=True,
|
|
483
|
-
frames_per_buffer=CHUNK,
|
|
484
|
-
input_device_index=self.input_device_index,
|
|
485
|
-
)
|
|
486
|
-
if self.debug_mode:
|
|
487
|
-
print(f"Audio recording initialized successfully at {self.device_sample_rate} Hz")
|
|
488
|
-
return True
|
|
489
|
-
except Exception as e:
|
|
490
|
-
print(f"Failed to initialize audio stream at {self.device_sample_rate} Hz: {e}")
|
|
491
|
-
return False
|
|
492
|
-
|
|
493
|
-
except Exception as e:
|
|
494
|
-
print(f"Error initializing audio recording: {e}")
|
|
495
|
-
if self.audio_interface:
|
|
496
|
-
self.audio_interface.terminate()
|
|
497
|
-
return False
|
|
487
|
+
"""Initialize audio input"""
|
|
488
|
+
self.audio_input = AudioInput(
|
|
489
|
+
input_device_index=self.input_device_index,
|
|
490
|
+
debug_mode=self.debug_mode
|
|
491
|
+
)
|
|
492
|
+
return self.audio_input.setup()
|
|
498
493
|
|
|
499
494
|
def record_and_send_audio(self):
|
|
495
|
+
"""Record and stream audio data"""
|
|
496
|
+
self._recording = True
|
|
497
|
+
|
|
500
498
|
try:
|
|
501
499
|
if not self.setup_audio():
|
|
502
500
|
raise Exception("Failed to set up audio recording.")
|
|
503
501
|
|
|
502
|
+
# Initialize WAV file writer if output_wav_file is provided
|
|
503
|
+
if self.output_wav_file:
|
|
504
|
+
self.wav_file = wave.open(self.output_wav_file, 'wb')
|
|
505
|
+
self.wav_file.setnchannels(1)
|
|
506
|
+
self.wav_file.setsampwidth(2)
|
|
507
|
+
self.wav_file.setframerate(self.audio_input.device_sample_rate) # Use self.device_sample_rate
|
|
508
|
+
|
|
509
|
+
|
|
504
510
|
if self.debug_mode:
|
|
505
511
|
print("Recording and sending audio...")
|
|
506
512
|
|
|
@@ -510,7 +516,10 @@ class AudioToTextRecorderClient:
|
|
|
510
516
|
continue
|
|
511
517
|
|
|
512
518
|
try:
|
|
513
|
-
audio_data = self.
|
|
519
|
+
audio_data = self.audio_input.read_chunk()
|
|
520
|
+
|
|
521
|
+
if self.wav_file:
|
|
522
|
+
self.wav_file.writeframes(audio_data)
|
|
514
523
|
|
|
515
524
|
if self.on_recorded_chunk:
|
|
516
525
|
self.on_recorded_chunk(audio_data)
|
|
@@ -519,7 +528,7 @@ class AudioToTextRecorderClient:
|
|
|
519
528
|
continue
|
|
520
529
|
|
|
521
530
|
if self.recording_start.is_set():
|
|
522
|
-
metadata = {"sampleRate": self.device_sample_rate}
|
|
531
|
+
metadata = {"sampleRate": self.audio_input.device_sample_rate}
|
|
523
532
|
metadata_json = json.dumps(metadata)
|
|
524
533
|
metadata_length = len(metadata_json)
|
|
525
534
|
message = struct.pack('<I', metadata_length) + metadata_json.encode('utf-8') + audio_data
|
|
@@ -528,30 +537,26 @@ class AudioToTextRecorderClient:
|
|
|
528
537
|
if log_outgoing_chunks:
|
|
529
538
|
print(".", flush=True, end='')
|
|
530
539
|
self.data_ws.send(message, opcode=websocket.ABNF.OPCODE_BINARY)
|
|
531
|
-
except KeyboardInterrupt:
|
|
540
|
+
except KeyboardInterrupt:
|
|
532
541
|
if self.debug_mode:
|
|
533
542
|
print("KeyboardInterrupt in record_and_send_audio, exiting...")
|
|
534
543
|
break
|
|
535
544
|
except Exception as e:
|
|
536
545
|
print(f"Error sending audio data: {e}")
|
|
537
|
-
break
|
|
546
|
+
break
|
|
538
547
|
|
|
539
548
|
except Exception as e:
|
|
540
|
-
print(f"Error in record_and_send_audio: {e}")
|
|
549
|
+
print(f"Error in record_and_send_audio: {e}", file=sys.stderr)
|
|
541
550
|
finally:
|
|
542
551
|
self.cleanup_audio()
|
|
552
|
+
self.final_text_ready.set() # fake final text to stop the text() method
|
|
553
|
+
self.is_running = False
|
|
554
|
+
self._recording = False
|
|
543
555
|
|
|
544
556
|
def cleanup_audio(self):
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
self.stream.close()
|
|
549
|
-
self.stream = None
|
|
550
|
-
if self.audio_interface:
|
|
551
|
-
self.audio_interface.terminate()
|
|
552
|
-
self.audio_interface = None
|
|
553
|
-
except Exception as e:
|
|
554
|
-
print(f"Error cleaning up audio resources: {e}")
|
|
557
|
+
"""Clean up audio resources"""
|
|
558
|
+
if hasattr(self, 'audio_input'):
|
|
559
|
+
self.audio_input.cleanup()
|
|
555
560
|
|
|
556
561
|
def on_control_message(self, ws, message):
|
|
557
562
|
try:
|
|
@@ -585,7 +590,7 @@ class AudioToTextRecorderClient:
|
|
|
585
590
|
self.realtime_text = data['text']
|
|
586
591
|
|
|
587
592
|
timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3]
|
|
588
|
-
print(f"Realtime text [{timestamp}]: {bcolors.OKCYAN}{self.realtime_text}{bcolors.ENDC}")
|
|
593
|
+
# print(f"Realtime text [{timestamp}]: {bcolors.OKCYAN}{self.realtime_text}{bcolors.ENDC}")
|
|
589
594
|
|
|
590
595
|
if self.on_realtime_transcription_update:
|
|
591
596
|
# Call the callback in a new thread to avoid blocking
|
|
@@ -704,14 +709,14 @@ class AudioToTextRecorderClient:
|
|
|
704
709
|
self.control_ws.send(json.dumps(command))
|
|
705
710
|
|
|
706
711
|
def shutdown(self):
|
|
712
|
+
"""Shutdown all resources"""
|
|
707
713
|
self.is_running = False
|
|
708
|
-
#self.stop_event.set()
|
|
709
714
|
if self.control_ws:
|
|
710
715
|
self.control_ws.close()
|
|
711
716
|
if self.data_ws:
|
|
712
717
|
self.data_ws.close()
|
|
713
718
|
|
|
714
|
-
# Join threads
|
|
719
|
+
# Join threads
|
|
715
720
|
if self.control_ws_thread:
|
|
716
721
|
self.control_ws_thread.join()
|
|
717
722
|
if self.data_ws_thread:
|
|
@@ -719,12 +724,8 @@ class AudioToTextRecorderClient:
|
|
|
719
724
|
if self.recording_thread:
|
|
720
725
|
self.recording_thread.join()
|
|
721
726
|
|
|
722
|
-
# Clean up audio
|
|
723
|
-
|
|
724
|
-
self.stream.stop_stream()
|
|
725
|
-
self.stream.close()
|
|
726
|
-
if self.audio_interface:
|
|
727
|
-
self.audio_interface.terminate()
|
|
727
|
+
# Clean up audio
|
|
728
|
+
self.cleanup_audio()
|
|
728
729
|
|
|
729
730
|
def __enter__(self):
|
|
730
731
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: RealtimeSTT
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
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
|
|
@@ -12,18 +12,6 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Requires-Python: >=3.6
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: PyAudio==0.2.14
|
|
16
|
-
Requires-Dist: faster-whisper==1.0.3
|
|
17
|
-
Requires-Dist: pvporcupine==1.9.5
|
|
18
|
-
Requires-Dist: webrtcvad-wheels==2.0.14
|
|
19
|
-
Requires-Dist: halo==0.0.31
|
|
20
|
-
Requires-Dist: torch
|
|
21
|
-
Requires-Dist: torchaudio
|
|
22
|
-
Requires-Dist: scipy==1.14.1
|
|
23
|
-
Requires-Dist: websockets==v12.0
|
|
24
|
-
Requires-Dist: websocket-client==1.8.0
|
|
25
|
-
Requires-Dist: openwakeword>=0.4.0
|
|
26
|
-
Requires-Dist: numpy<2.0.0
|
|
27
15
|
|
|
28
16
|
# RealtimeSTT
|
|
29
17
|
[](https://pypi.org/project/RealtimeSTT/)
|
|
@@ -55,7 +43,7 @@ https://github.com/user-attachments/assets/797e6552-27cd-41b1-a7f3-e5cbc72094f5
|
|
|
55
43
|
|
|
56
44
|
### Updates
|
|
57
45
|
|
|
58
|
-
Latest Version: v0.3.
|
|
46
|
+
Latest Version: v0.3.8
|
|
59
47
|
|
|
60
48
|
See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
|
|
61
49
|
|
|
@@ -462,6 +450,8 @@ When you initialize the `AudioToTextRecorder` class, you have various options to
|
|
|
462
450
|
|
|
463
451
|
- **level** (int, default=logging.WARNING): Logging level.
|
|
464
452
|
|
|
453
|
+
- **init_logging** (bool, default=True): Whether to initialize the logging framework. Set to False to manage this yourself.
|
|
454
|
+
|
|
465
455
|
- **handle_buffer_overflow** (bool, default=True): If set, the system will log a warning when an input overflow occurs during recording and remove the data from the buffer.
|
|
466
456
|
|
|
467
457
|
- **beam_size** (int, default=5): The beam size to use for beam search decoding.
|
|
@@ -53,53 +53,3 @@ def check_and_install_packages(packages):
|
|
|
53
53
|
else:
|
|
54
54
|
print(f"The program requires '{install_name}' to run. Exiting...")
|
|
55
55
|
sys.exit(1)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# import subprocess
|
|
59
|
-
# import sys
|
|
60
|
-
|
|
61
|
-
# def check_and_install_packages(packages):
|
|
62
|
-
# """
|
|
63
|
-
# Checks if the specified packages are installed, and if not, prompts the user
|
|
64
|
-
# to install them.
|
|
65
|
-
|
|
66
|
-
# Parameters:
|
|
67
|
-
# - packages: A list of dictionaries, each containing:
|
|
68
|
-
# - 'import_name': The name used in the import statement.
|
|
69
|
-
# - 'install_name': (Optional) The name used in the pip install command.
|
|
70
|
-
# Defaults to 'import_name' if not provided.
|
|
71
|
-
# - 'version': (Optional) Version constraint for the package.
|
|
72
|
-
# """
|
|
73
|
-
# for package in packages:
|
|
74
|
-
# import_name = package['import_name']
|
|
75
|
-
# install_name = package.get('install_name', import_name)
|
|
76
|
-
# version = package.get('version', '')
|
|
77
|
-
|
|
78
|
-
# try:
|
|
79
|
-
# print(f"import {import_name}")
|
|
80
|
-
# __import__(import_name)
|
|
81
|
-
# print(f"imported {import_name}")
|
|
82
|
-
# except ImportError:
|
|
83
|
-
# user_input = input(
|
|
84
|
-
# f"This program requires the '{import_name}' library, which is not installed.\n"
|
|
85
|
-
# f"Do you want to install it now? (y/n): "
|
|
86
|
-
# )
|
|
87
|
-
# if user_input.strip().lower() == 'y':
|
|
88
|
-
# try:
|
|
89
|
-
# # Build the pip install command
|
|
90
|
-
# install_command = [sys.executable, "-m", "pip", "install"]
|
|
91
|
-
# if version:
|
|
92
|
-
# install_command.append(f"{install_name}{version}")
|
|
93
|
-
# else:
|
|
94
|
-
# install_command.append(install_name)
|
|
95
|
-
|
|
96
|
-
# subprocess.check_call(install_command)
|
|
97
|
-
# __import__(import_name)
|
|
98
|
-
# print(f"Successfully installed '{install_name}'.")
|
|
99
|
-
# except Exception as e:
|
|
100
|
-
# print(f"An error occurred while installing '{install_name}': {e}")
|
|
101
|
-
# sys.exit(1)
|
|
102
|
-
# else:
|
|
103
|
-
# print(f"The program requires the '{import_name}' library to run. Exiting...")
|
|
104
|
-
# sys.exit(1)
|
|
105
|
-
|