RealTimeSTT 0.3.93__tar.gz → 0.3.95__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.93 → realtimestt-0.3.95}/PKG-INFO +7 -7
- {realtimestt-0.3.93 → realtimestt-0.3.95}/README.md +2 -3
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT/audio_input.py +81 -14
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT/audio_recorder.py +137 -36
- realtimestt-0.3.95/RealtimeSTT/warmup_audio.wav +0 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT.egg-info/PKG-INFO +7 -7
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT.egg-info/SOURCES.txt +1 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT.egg-info/requires.txt +4 -3
- {realtimestt-0.3.93 → realtimestt-0.3.95}/setup.py +3 -1
- {realtimestt-0.3.93 → realtimestt-0.3.95}/LICENSE +0 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT/__init__.py +0 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT/audio_recorder_client.py +0 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT.egg-info/dependency_links.txt +0 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT.egg-info/entry_points.txt +0 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT.egg-info/top_level.txt +0 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT_server/__init__.py +0 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT_server/install_packages.py +0 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT_server/stt_cli_client.py +0 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/RealtimeSTT_server/stt_server.py +0 -0
- {realtimestt-0.3.93 → realtimestt-0.3.95}/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.95
|
|
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
|
|
@@ -13,21 +13,22 @@ Requires-Python: >=3.6
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: PyAudio==0.2.14
|
|
16
|
-
Requires-Dist: faster-whisper==1.1.
|
|
16
|
+
Requires-Dist: faster-whisper==1.1.1
|
|
17
17
|
Requires-Dist: pvporcupine==1.9.5
|
|
18
18
|
Requires-Dist: webrtcvad-wheels==2.0.14
|
|
19
19
|
Requires-Dist: halo==0.0.31
|
|
20
20
|
Requires-Dist: torch
|
|
21
21
|
Requires-Dist: torchaudio
|
|
22
|
-
Requires-Dist: scipy==1.
|
|
22
|
+
Requires-Dist: scipy==1.15.1
|
|
23
23
|
Requires-Dist: openwakeword>=0.4.0
|
|
24
|
-
Requires-Dist: websockets==14.
|
|
24
|
+
Requires-Dist: websockets==14.2
|
|
25
25
|
Requires-Dist: websocket-client==1.8.0
|
|
26
26
|
Requires-Dist: numpy<2.0.0
|
|
27
|
+
Requires-Dist: soundfile==0.13.1
|
|
27
28
|
|
|
28
29
|
# RealtimeSTT
|
|
29
30
|
[](https://pypi.org/project/RealtimeSTT/)
|
|
30
|
-
[](https://pepy.tech/
|
|
31
|
+
[](https://www.pepy.tech/projects/realtimestt)
|
|
31
32
|
[](https://GitHub.com/KoljaB/RealtimeSTT/releases/)
|
|
32
33
|
[](https://GitHub.com/Naereen/KoljaB/RealtimeSTT/commit/)
|
|
33
34
|
[](https://GitHub.com/KoljaB/RealtimeSTT/network/)
|
|
@@ -55,7 +56,7 @@ https://github.com/user-attachments/assets/797e6552-27cd-41b1-a7f3-e5cbc72094f5
|
|
|
55
56
|
|
|
56
57
|
### Updates
|
|
57
58
|
|
|
58
|
-
Latest Version: v0.3.
|
|
59
|
+
Latest Version: v0.3.95
|
|
59
60
|
|
|
60
61
|
See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
|
|
61
62
|
|
|
@@ -67,7 +68,6 @@ See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
|
|
|
67
68
|
|
|
68
69
|
```python
|
|
69
70
|
from RealtimeSTT import AudioToTextRecorder
|
|
70
|
-
import pyautogui
|
|
71
71
|
|
|
72
72
|
def process_text(text):
|
|
73
73
|
print(text)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RealtimeSTT
|
|
2
2
|
[](https://pypi.org/project/RealtimeSTT/)
|
|
3
|
-
[](https://pepy.tech/
|
|
3
|
+
[](https://www.pepy.tech/projects/realtimestt)
|
|
4
4
|
[](https://GitHub.com/KoljaB/RealtimeSTT/releases/)
|
|
5
5
|
[](https://GitHub.com/Naereen/KoljaB/RealtimeSTT/commit/)
|
|
6
6
|
[](https://GitHub.com/KoljaB/RealtimeSTT/network/)
|
|
@@ -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.95
|
|
32
32
|
|
|
33
33
|
See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
|
|
34
34
|
|
|
@@ -40,7 +40,6 @@ See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
|
|
|
40
40
|
|
|
41
41
|
```python
|
|
42
42
|
from RealtimeSTT import AudioToTextRecorder
|
|
43
|
-
import pyautogui
|
|
44
43
|
|
|
45
44
|
def process_text(text):
|
|
46
45
|
print(text)
|
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
from colorama import init, Fore, Style
|
|
2
|
+
from scipy.signal import butter, filtfilt, resample_poly
|
|
2
3
|
import pyaudio
|
|
3
4
|
import logging
|
|
4
|
-
import time
|
|
5
5
|
|
|
6
|
+
DESIRED_RATE = 16000
|
|
7
|
+
CHUNK_SIZE = 1024
|
|
8
|
+
AUDIO_FORMAT = pyaudio.paInt16
|
|
9
|
+
CHANNELS = 1
|
|
6
10
|
|
|
7
11
|
class AudioInput:
|
|
8
|
-
def __init__(
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
input_device_index: int = None,
|
|
15
|
+
debug_mode: bool = False,
|
|
16
|
+
target_samplerate: int = DESIRED_RATE,
|
|
17
|
+
chunk_size: int = CHUNK_SIZE,
|
|
18
|
+
audio_format: int = AUDIO_FORMAT,
|
|
19
|
+
channels: int = CHANNELS,
|
|
20
|
+
resample_to_target: bool = True,
|
|
21
|
+
):
|
|
22
|
+
|
|
9
23
|
self.input_device_index = input_device_index
|
|
10
24
|
self.debug_mode = debug_mode
|
|
11
25
|
self.audio_interface = None
|
|
12
26
|
self.stream = None
|
|
13
27
|
self.device_sample_rate = None
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
self.
|
|
17
|
-
self.
|
|
18
|
-
self.
|
|
19
|
-
self.DESIRED_RATE = 16000
|
|
28
|
+
self.target_samplerate = target_samplerate
|
|
29
|
+
self.chunk_size = chunk_size
|
|
30
|
+
self.audio_format = audio_format
|
|
31
|
+
self.channels = channels
|
|
32
|
+
self.resample_to_target = resample_to_target
|
|
20
33
|
|
|
21
34
|
def get_supported_sample_rates(self, device_index):
|
|
22
35
|
"""Test which standard sample rates are supported by the specified device."""
|
|
@@ -31,8 +44,8 @@ class AudioInput:
|
|
|
31
44
|
if self.audio_interface.is_format_supported(
|
|
32
45
|
rate,
|
|
33
46
|
input_device=device_index, # Changed to input_device
|
|
34
|
-
input_channels=max_channels, # Changed to input_channels
|
|
35
|
-
input_format=self.
|
|
47
|
+
input_channels=max_channels, # Changed to input_channels
|
|
48
|
+
input_format=self.audio_format, # Changed to input_format
|
|
36
49
|
):
|
|
37
50
|
supported_rates.append(rate)
|
|
38
51
|
except:
|
|
@@ -108,18 +121,18 @@ class AudioInput:
|
|
|
108
121
|
if self.debug_mode:
|
|
109
122
|
print(f"Actual selected device index: {actual_device_index}")
|
|
110
123
|
self.input_device_index = actual_device_index
|
|
111
|
-
self.device_sample_rate = self._get_best_sample_rate(actual_device_index, self.
|
|
124
|
+
self.device_sample_rate = self._get_best_sample_rate(actual_device_index, self.target_samplerate)
|
|
112
125
|
|
|
113
126
|
if self.debug_mode:
|
|
114
127
|
print(f"Setting up audio on device {self.input_device_index} with sample rate {self.device_sample_rate}")
|
|
115
128
|
|
|
116
129
|
try:
|
|
117
130
|
self.stream = self.audio_interface.open(
|
|
118
|
-
format=self.
|
|
119
|
-
channels=self.
|
|
131
|
+
format=self.audio_format,
|
|
132
|
+
channels=self.channels,
|
|
120
133
|
rate=self.device_sample_rate,
|
|
121
134
|
input=True,
|
|
122
|
-
frames_per_buffer=self.
|
|
135
|
+
frames_per_buffer=self.chunk_size,
|
|
123
136
|
input_device_index=self.input_device_index,
|
|
124
137
|
)
|
|
125
138
|
if self.debug_mode:
|
|
@@ -135,6 +148,60 @@ class AudioInput:
|
|
|
135
148
|
self.audio_interface.terminate()
|
|
136
149
|
return False
|
|
137
150
|
|
|
151
|
+
def lowpass_filter(self, signal, cutoff_freq, sample_rate):
|
|
152
|
+
"""
|
|
153
|
+
Apply a low-pass Butterworth filter to prevent aliasing in the signal.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
signal (np.ndarray): Input audio signal to filter
|
|
157
|
+
cutoff_freq (float): Cutoff frequency in Hz
|
|
158
|
+
sample_rate (float): Sampling rate of the input signal in Hz
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
np.ndarray: Filtered audio signal
|
|
162
|
+
|
|
163
|
+
Notes:
|
|
164
|
+
- Uses a 5th order Butterworth filter
|
|
165
|
+
- Applies zero-phase filtering using filtfilt
|
|
166
|
+
"""
|
|
167
|
+
# Calculate the Nyquist frequency (half the sample rate)
|
|
168
|
+
nyquist_rate = sample_rate / 2.0
|
|
169
|
+
|
|
170
|
+
# Normalize cutoff frequency to Nyquist rate (required by butter())
|
|
171
|
+
normal_cutoff = cutoff_freq / nyquist_rate
|
|
172
|
+
|
|
173
|
+
# Design the Butterworth filter
|
|
174
|
+
b, a = butter(5, normal_cutoff, btype='low', analog=False)
|
|
175
|
+
|
|
176
|
+
# Apply zero-phase filtering (forward and backward)
|
|
177
|
+
filtered_signal = filtfilt(b, a, signal)
|
|
178
|
+
return filtered_signal
|
|
179
|
+
|
|
180
|
+
def resample_audio(self, pcm_data, target_sample_rate, original_sample_rate):
|
|
181
|
+
"""
|
|
182
|
+
Filter and resample audio data to a target sample rate.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
pcm_data (np.ndarray): Input audio data
|
|
186
|
+
target_sample_rate (int): Desired output sample rate in Hz
|
|
187
|
+
original_sample_rate (int): Original sample rate of input in Hz
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
np.ndarray: Resampled audio data
|
|
191
|
+
|
|
192
|
+
Notes:
|
|
193
|
+
- Applies anti-aliasing filter before resampling
|
|
194
|
+
- Uses polyphase filtering for high-quality resampling
|
|
195
|
+
"""
|
|
196
|
+
if target_sample_rate < original_sample_rate:
|
|
197
|
+
# Downsampling with low-pass filter
|
|
198
|
+
pcm_filtered = self.lowpass_filter(pcm_data, target_sample_rate / 2, original_sample_rate)
|
|
199
|
+
resampled = resample_poly(pcm_filtered, target_sample_rate, original_sample_rate)
|
|
200
|
+
else:
|
|
201
|
+
# Upsampling without low-pass filter
|
|
202
|
+
resampled = resample_poly(pcm_data, target_sample_rate, original_sample_rate)
|
|
203
|
+
return resampled
|
|
204
|
+
|
|
138
205
|
def read_chunk(self):
|
|
139
206
|
"""Read a chunk of audio data"""
|
|
140
207
|
return self.stream.read(self.CHUNK, exception_on_overflow=False)
|
|
@@ -26,15 +26,15 @@ Author: Kolja Beigel
|
|
|
26
26
|
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
+
from faster_whisper import WhisperModel, BatchedInferencePipeline
|
|
29
30
|
from typing import Iterable, List, Optional, Union
|
|
30
|
-
import torch.multiprocessing as mp
|
|
31
|
-
import torch
|
|
32
|
-
from ctypes import c_bool
|
|
33
31
|
from openwakeword.model import Model
|
|
32
|
+
import torch.multiprocessing as mp
|
|
34
33
|
from scipy.signal import resample
|
|
35
|
-
from scipy import signal
|
|
36
34
|
import signal as system_signal
|
|
37
|
-
from
|
|
35
|
+
from ctypes import c_bool
|
|
36
|
+
from scipy import signal
|
|
37
|
+
import soundfile as sf
|
|
38
38
|
import faster_whisper
|
|
39
39
|
import openwakeword
|
|
40
40
|
import collections
|
|
@@ -43,14 +43,13 @@ import pvporcupine
|
|
|
43
43
|
import traceback
|
|
44
44
|
import threading
|
|
45
45
|
import webrtcvad
|
|
46
|
-
import itertools
|
|
47
46
|
import datetime
|
|
48
47
|
import platform
|
|
49
|
-
import pyaudio
|
|
50
48
|
import logging
|
|
51
49
|
import struct
|
|
52
50
|
import base64
|
|
53
51
|
import queue
|
|
52
|
+
import torch
|
|
54
53
|
import halo
|
|
55
54
|
import time
|
|
56
55
|
import copy
|
|
@@ -120,7 +119,7 @@ class TranscriptionWorker:
|
|
|
120
119
|
data = self.conn.recv()
|
|
121
120
|
self.queue.put(data)
|
|
122
121
|
except Exception as e:
|
|
123
|
-
logging.error(f"Error receiving data from connection: {e}")
|
|
122
|
+
logging.error(f"Error receiving data from connection: {e}", exc_info=True)
|
|
124
123
|
else:
|
|
125
124
|
time.sleep(TIME_SLEEP)
|
|
126
125
|
|
|
@@ -144,8 +143,13 @@ class TranscriptionWorker:
|
|
|
144
143
|
model = BatchedInferencePipeline(model=model)
|
|
145
144
|
|
|
146
145
|
# Run a warm-up transcription
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
current_dir = os.path.dirname(os.path.realpath(__file__))
|
|
147
|
+
warmup_audio_path = os.path.join(
|
|
148
|
+
current_dir, "warmup_audio.wav"
|
|
149
|
+
)
|
|
150
|
+
warmup_audio_data, _ = sf.read(warmup_audio_path, dtype="float32")
|
|
151
|
+
segments, info = model.transcribe(warmup_audio_data, language="en", beam_size=1)
|
|
152
|
+
model_warmup_transcription = " ".join(segment.text for segment in segments)
|
|
149
153
|
except Exception as e:
|
|
150
154
|
logging.exception(f"Error initializing main faster_whisper transcription model: {e}")
|
|
151
155
|
raise
|
|
@@ -162,6 +166,7 @@ class TranscriptionWorker:
|
|
|
162
166
|
try:
|
|
163
167
|
audio, language = self.queue.get(timeout=0.1)
|
|
164
168
|
try:
|
|
169
|
+
logging.debug(f"Transcribing audio with language {language}")
|
|
165
170
|
if self.batch_size > 0:
|
|
166
171
|
segments, info = model.transcribe(
|
|
167
172
|
audio,
|
|
@@ -184,7 +189,7 @@ class TranscriptionWorker:
|
|
|
184
189
|
logging.debug(f"Final text detected with main model: {transcription}")
|
|
185
190
|
self.conn.send(('success', (transcription, info)))
|
|
186
191
|
except Exception as e:
|
|
187
|
-
logging.error(f"General error in transcription: {e}")
|
|
192
|
+
logging.error(f"General error in transcription: {e}", exc_info=True)
|
|
188
193
|
self.conn.send(('error', str(e)))
|
|
189
194
|
except queue.Empty:
|
|
190
195
|
continue
|
|
@@ -193,7 +198,7 @@ class TranscriptionWorker:
|
|
|
193
198
|
logging.debug("Transcription worker process finished due to KeyboardInterrupt")
|
|
194
199
|
break
|
|
195
200
|
except Exception as e:
|
|
196
|
-
logging.error(f"General error in processing queue item: {e}")
|
|
201
|
+
logging.error(f"General error in processing queue item: {e}", exc_info=True)
|
|
197
202
|
finally:
|
|
198
203
|
__builtins__['print'] = print # Restore the original print function
|
|
199
204
|
self.conn.close()
|
|
@@ -567,6 +572,8 @@ class AudioToTextRecorder:
|
|
|
567
572
|
self.sample_rate = sample_rate
|
|
568
573
|
self.recording_start_time = 0
|
|
569
574
|
self.recording_stop_time = 0
|
|
575
|
+
self.last_recording_start_time = 0
|
|
576
|
+
self.last_recording_stop_time = 0
|
|
570
577
|
self.wake_word_detect_time = 0
|
|
571
578
|
self.silero_check_time = 0
|
|
572
579
|
self.silero_working = False
|
|
@@ -590,6 +597,8 @@ class AudioToTextRecorder:
|
|
|
590
597
|
self.stream = None
|
|
591
598
|
self.start_recording_event = threading.Event()
|
|
592
599
|
self.stop_recording_event = threading.Event()
|
|
600
|
+
self.backdate_stop_seconds = 0.0
|
|
601
|
+
self.backdate_resume_seconds = 0.0
|
|
593
602
|
self.last_transcription_bytes = None
|
|
594
603
|
self.last_transcription_bytes_b64 = None
|
|
595
604
|
self.initial_prompt = initial_prompt
|
|
@@ -723,8 +732,15 @@ class AudioToTextRecorder:
|
|
|
723
732
|
)
|
|
724
733
|
if self.realtime_batch_size > 0:
|
|
725
734
|
self.realtime_model_type = BatchedInferencePipeline(model=self.realtime_model_type)
|
|
726
|
-
|
|
727
|
-
|
|
735
|
+
|
|
736
|
+
# Run a warm-up transcription
|
|
737
|
+
current_dir = os.path.dirname(os.path.realpath(__file__))
|
|
738
|
+
warmup_audio_path = os.path.join(
|
|
739
|
+
current_dir, "warmup_audio.wav"
|
|
740
|
+
)
|
|
741
|
+
warmup_audio_data, _ = sf.read(warmup_audio_path, dtype="float32")
|
|
742
|
+
segments, info = self.realtime_model_type.transcribe(warmup_audio_data, language="en", beam_size=1)
|
|
743
|
+
model_warmup_transcription = " ".join(segment.text for segment in segments)
|
|
728
744
|
except Exception as e:
|
|
729
745
|
logging.exception("Error initializing faster_whisper "
|
|
730
746
|
f"realtime transcription model: {e}"
|
|
@@ -860,6 +876,7 @@ class AudioToTextRecorder:
|
|
|
860
876
|
0.3)
|
|
861
877
|
)
|
|
862
878
|
self.frames = []
|
|
879
|
+
self.last_frames = []
|
|
863
880
|
|
|
864
881
|
# Recording control flags
|
|
865
882
|
self.is_recording = False
|
|
@@ -925,7 +942,7 @@ class AudioToTextRecorder:
|
|
|
925
942
|
logging.info("KeyboardInterrupt in read from stdout detected, exiting...")
|
|
926
943
|
break
|
|
927
944
|
except Exception as e:
|
|
928
|
-
logging.error(f"Unexpected error in read from stdout: {e}")
|
|
945
|
+
logging.error(f"Unexpected error in read from stdout: {e}", exc_info=True)
|
|
929
946
|
logging.error(traceback.format_exc()) # Log the full traceback here
|
|
930
947
|
break
|
|
931
948
|
time.sleep(0.1)
|
|
@@ -1074,7 +1091,7 @@ class AudioToTextRecorder:
|
|
|
1074
1091
|
return stream
|
|
1075
1092
|
|
|
1076
1093
|
except Exception as e:
|
|
1077
|
-
logging.error(f"Microphone connection failed: {e}. Retrying...")
|
|
1094
|
+
logging.error(f"Microphone connection failed: {e}. Retrying...", exc_info=True)
|
|
1078
1095
|
input_device_index = None
|
|
1079
1096
|
time.sleep(3) # Wait before retrying
|
|
1080
1097
|
continue
|
|
@@ -1190,7 +1207,7 @@ class AudioToTextRecorder:
|
|
|
1190
1207
|
if e.errno == pyaudio.paInputOverflowed:
|
|
1191
1208
|
logging.warning("Input overflowed. Frame dropped.")
|
|
1192
1209
|
else:
|
|
1193
|
-
logging.error(f"OSError during recording: {e}")
|
|
1210
|
+
logging.error(f"OSError during recording: {e}", exc_info=True)
|
|
1194
1211
|
# Attempt to reinitialize the stream
|
|
1195
1212
|
logging.error("Attempting to reinitialize the audio stream...")
|
|
1196
1213
|
|
|
@@ -1259,12 +1276,17 @@ class AudioToTextRecorder:
|
|
|
1259
1276
|
self.listen_start = time.time()
|
|
1260
1277
|
|
|
1261
1278
|
def abort(self):
|
|
1279
|
+
state = self.state
|
|
1262
1280
|
self.start_recording_on_voice_activity = False
|
|
1263
1281
|
self.stop_recording_on_voice_deactivity = False
|
|
1264
|
-
self._set_state("inactive")
|
|
1265
1282
|
self.interrupt_stop_event.set()
|
|
1266
|
-
self.was_interrupted
|
|
1283
|
+
if self.state != "inactive": # if inactive, was_interrupted will never be set
|
|
1284
|
+
self.was_interrupted.wait()
|
|
1285
|
+
self._set_state("transcribing")
|
|
1267
1286
|
self.was_interrupted.clear()
|
|
1287
|
+
if self.is_recording: # if recording, make sure to stop the recorder
|
|
1288
|
+
self.stop()
|
|
1289
|
+
|
|
1268
1290
|
|
|
1269
1291
|
def wait_audio(self):
|
|
1270
1292
|
"""
|
|
@@ -1308,13 +1330,60 @@ class AudioToTextRecorder:
|
|
|
1308
1330
|
if (self.stop_recording_event.wait(timeout=0.02)):
|
|
1309
1331
|
break
|
|
1310
1332
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1333
|
+
frames = self.frames
|
|
1334
|
+
if len(frames) == 0:
|
|
1335
|
+
frames = self.last_frames
|
|
1336
|
+
|
|
1337
|
+
# Calculate samples needed for backdating resume
|
|
1338
|
+
samples_to_keep = int(self.sample_rate * self.backdate_resume_seconds)
|
|
1339
|
+
|
|
1340
|
+
# First convert all current frames to audio array
|
|
1341
|
+
full_audio_array = np.frombuffer(b''.join(frames), dtype=np.int16)
|
|
1342
|
+
full_audio = full_audio_array.astype(np.float32) / INT16_MAX_ABS_VALUE
|
|
1343
|
+
|
|
1344
|
+
# Calculate how many samples we need to keep for backdating resume
|
|
1345
|
+
if samples_to_keep > 0:
|
|
1346
|
+
samples_to_keep = min(samples_to_keep, len(full_audio))
|
|
1347
|
+
# Keep the last N samples for backdating resume
|
|
1348
|
+
frames_to_read_audio = full_audio[-samples_to_keep:]
|
|
1349
|
+
|
|
1350
|
+
# Convert the audio back to int16 bytes for frames
|
|
1351
|
+
frames_to_read_int16 = (frames_to_read_audio * INT16_MAX_ABS_VALUE).astype(np.int16)
|
|
1352
|
+
frame_bytes = frames_to_read_int16.tobytes()
|
|
1353
|
+
|
|
1354
|
+
# Split into appropriate frame sizes (assuming standard frame size)
|
|
1355
|
+
FRAME_SIZE = 2048 # Typical frame size
|
|
1356
|
+
frames_to_read = []
|
|
1357
|
+
for i in range(0, len(frame_bytes), FRAME_SIZE):
|
|
1358
|
+
frame = frame_bytes[i:i + FRAME_SIZE]
|
|
1359
|
+
if frame: # Only add non-empty frames
|
|
1360
|
+
frames_to_read.append(frame)
|
|
1361
|
+
else:
|
|
1362
|
+
frames_to_read = []
|
|
1363
|
+
|
|
1364
|
+
# Process backdate stop seconds
|
|
1365
|
+
samples_to_remove = int(self.sample_rate * self.backdate_stop_seconds)
|
|
1366
|
+
|
|
1367
|
+
if samples_to_remove > 0:
|
|
1368
|
+
if samples_to_remove < len(full_audio):
|
|
1369
|
+
self.audio = full_audio[:-samples_to_remove]
|
|
1370
|
+
logging.debug(f"Removed {samples_to_remove} samples "
|
|
1371
|
+
f"({samples_to_remove/self.sample_rate:.3f}s) from end of audio")
|
|
1372
|
+
else:
|
|
1373
|
+
self.audio = np.array([], dtype=np.float32)
|
|
1374
|
+
logging.debug("Cleared audio (samples_to_remove >= audio length)")
|
|
1375
|
+
else:
|
|
1376
|
+
self.audio = full_audio
|
|
1377
|
+
logging.debug(f"No samples removed, final audio length: {len(self.audio)}")
|
|
1378
|
+
|
|
1314
1379
|
self.frames.clear()
|
|
1380
|
+
self.last_frames.clear()
|
|
1381
|
+
self.frames.extend(frames_to_read)
|
|
1382
|
+
|
|
1383
|
+
# Reset backdating parameters
|
|
1384
|
+
self.backdate_stop_seconds = 0.0
|
|
1385
|
+
self.backdate_resume_seconds = 0.0
|
|
1315
1386
|
|
|
1316
|
-
# Reset recording-related timestamps
|
|
1317
|
-
self.recording_stop_time = 0
|
|
1318
1387
|
self.listen_start = 0
|
|
1319
1388
|
|
|
1320
1389
|
self._set_state("inactive")
|
|
@@ -1363,6 +1432,12 @@ class AudioToTextRecorder:
|
|
|
1363
1432
|
|
|
1364
1433
|
while self.transcribe_count > 0:
|
|
1365
1434
|
logging.debug(F"Receive from parent_transcription_pipe after sendiung transcription request, transcribe_count: {self.transcribe_count}")
|
|
1435
|
+
if not self.parent_transcription_pipe.poll(0.1): # check if transcription done
|
|
1436
|
+
if self.interrupt_stop_event.is_set(): # check if interrupted
|
|
1437
|
+
self.was_interrupted.set()
|
|
1438
|
+
self._set_state("inactive")
|
|
1439
|
+
return "" # return empty string if interrupted
|
|
1440
|
+
continue
|
|
1366
1441
|
status, result = self.parent_transcription_pipe.recv()
|
|
1367
1442
|
self.transcribe_count -= 1
|
|
1368
1443
|
|
|
@@ -1383,12 +1458,12 @@ class AudioToTextRecorder:
|
|
|
1383
1458
|
print(f"Model {self.main_model_type} completed transcription in {transcription_time:.2f} seconds")
|
|
1384
1459
|
else:
|
|
1385
1460
|
logging.debug(f"Model {self.main_model_type} completed transcription in {transcription_time:.2f} seconds")
|
|
1386
|
-
return transcription
|
|
1461
|
+
return "" if self.interrupt_stop_event.is_set() else transcription # if interrupted return empty string
|
|
1387
1462
|
else:
|
|
1388
1463
|
logging.error(f"Transcription error: {result}")
|
|
1389
1464
|
raise Exception(result)
|
|
1390
1465
|
except Exception as e:
|
|
1391
|
-
logging.error(f"Error during transcription: {str(e)}")
|
|
1466
|
+
logging.error(f"Error during transcription: {str(e)}", exc_info=True)
|
|
1392
1467
|
raise e
|
|
1393
1468
|
|
|
1394
1469
|
def _process_wakeword(self, data):
|
|
@@ -1476,7 +1551,16 @@ class AudioToTextRecorder:
|
|
|
1476
1551
|
return self.transcribe()
|
|
1477
1552
|
|
|
1478
1553
|
|
|
1479
|
-
def
|
|
1554
|
+
def format_number(self, num):
|
|
1555
|
+
# Convert the number to a string
|
|
1556
|
+
num_str = f"{num:.10f}" # Ensure precision is sufficient
|
|
1557
|
+
# Split the number into integer and decimal parts
|
|
1558
|
+
integer_part, decimal_part = num_str.split('.')
|
|
1559
|
+
# Take the last two digits of the integer part and the first two digits of the decimal part
|
|
1560
|
+
result = f"{integer_part[-2:]}.{decimal_part[:2]}"
|
|
1561
|
+
return result
|
|
1562
|
+
|
|
1563
|
+
def start(self, frames = None):
|
|
1480
1564
|
"""
|
|
1481
1565
|
Starts recording audio directly without waiting for voice activity.
|
|
1482
1566
|
"""
|
|
@@ -1498,7 +1582,10 @@ class AudioToTextRecorder:
|
|
|
1498
1582
|
self.wakeword_detected = False
|
|
1499
1583
|
self.wake_word_detect_time = 0
|
|
1500
1584
|
self.frames = []
|
|
1585
|
+
if frames:
|
|
1586
|
+
self.frames = frames
|
|
1501
1587
|
self.is_recording = True
|
|
1588
|
+
|
|
1502
1589
|
self.recording_start_time = time.time()
|
|
1503
1590
|
self.is_silero_speech_active = False
|
|
1504
1591
|
self.is_webrtc_speech_active = False
|
|
@@ -1510,9 +1597,19 @@ class AudioToTextRecorder:
|
|
|
1510
1597
|
|
|
1511
1598
|
return self
|
|
1512
1599
|
|
|
1513
|
-
def stop(self
|
|
1600
|
+
def stop(self,
|
|
1601
|
+
backdate_stop_seconds: float = 0.0,
|
|
1602
|
+
backdate_resume_seconds: float = 0.0,
|
|
1603
|
+
):
|
|
1514
1604
|
"""
|
|
1515
1605
|
Stops recording audio.
|
|
1606
|
+
|
|
1607
|
+
Args:
|
|
1608
|
+
- backdate_stop_seconds (float, default="0.0"): Specifies the number of
|
|
1609
|
+
seconds to backdate the stop time. This is useful when the stop
|
|
1610
|
+
command is issued after the actual stop time.
|
|
1611
|
+
- backdate_resume_seconds (float, default="0.0"): Specifies the number
|
|
1612
|
+
of seconds to backdate the time relistening is initiated.
|
|
1516
1613
|
"""
|
|
1517
1614
|
|
|
1518
1615
|
# Ensure there's a minimum interval
|
|
@@ -1525,6 +1622,9 @@ class AudioToTextRecorder:
|
|
|
1525
1622
|
return self
|
|
1526
1623
|
|
|
1527
1624
|
logging.info("recording stopped")
|
|
1625
|
+
self.last_frames = copy.deepcopy(self.frames)
|
|
1626
|
+
self.backdate_stop_seconds = backdate_stop_seconds
|
|
1627
|
+
self.backdate_resume_seconds = backdate_resume_seconds
|
|
1528
1628
|
self.is_recording = False
|
|
1529
1629
|
self.recording_stop_time = time.time()
|
|
1530
1630
|
self.is_silero_speech_active = False
|
|
@@ -1533,6 +1633,9 @@ class AudioToTextRecorder:
|
|
|
1533
1633
|
self.start_recording_event.clear()
|
|
1534
1634
|
self.stop_recording_event.set()
|
|
1535
1635
|
|
|
1636
|
+
self.last_recording_start_time = self.recording_start_time
|
|
1637
|
+
self.last_recording_stop_time = self.recording_stop_time
|
|
1638
|
+
|
|
1536
1639
|
if self.on_recording_stop:
|
|
1537
1640
|
self.on_recording_stop()
|
|
1538
1641
|
|
|
@@ -1607,7 +1710,6 @@ class AudioToTextRecorder:
|
|
|
1607
1710
|
return
|
|
1608
1711
|
|
|
1609
1712
|
print("\033[91mRealtimeSTT shutting down\033[0m")
|
|
1610
|
-
# logging.debug("RealtimeSTT shutting down")
|
|
1611
1713
|
|
|
1612
1714
|
# Force wait_audio() and text() to exit
|
|
1613
1715
|
self.is_shut_down = True
|
|
@@ -1736,7 +1838,7 @@ class AudioToTextRecorder:
|
|
|
1736
1838
|
data = self.audio_queue.get()
|
|
1737
1839
|
|
|
1738
1840
|
except BrokenPipeError:
|
|
1739
|
-
logging.error("BrokenPipeError _recording_worker")
|
|
1841
|
+
logging.error("BrokenPipeError _recording_worker", exc_info=True)
|
|
1740
1842
|
self.is_running = False
|
|
1741
1843
|
break
|
|
1742
1844
|
|
|
@@ -1813,11 +1915,11 @@ class AudioToTextRecorder:
|
|
|
1813
1915
|
|
|
1814
1916
|
except struct.error:
|
|
1815
1917
|
logging.error("Error unpacking audio data "
|
|
1816
|
-
"for wake word processing.")
|
|
1918
|
+
"for wake word processing.", exc_info=True)
|
|
1817
1919
|
continue
|
|
1818
1920
|
|
|
1819
1921
|
except Exception as e:
|
|
1820
|
-
logging.error(f"Wake word processing error: {e}")
|
|
1922
|
+
logging.error(f"Wake word processing error: {e}", exc_info=True)
|
|
1821
1923
|
continue
|
|
1822
1924
|
|
|
1823
1925
|
if self.use_extended_logging:
|
|
@@ -2044,7 +2146,7 @@ class AudioToTextRecorder:
|
|
|
2044
2146
|
except Exception as e:
|
|
2045
2147
|
logging.debug('Debug: Caught exception in main try block')
|
|
2046
2148
|
if not self.interrupt_stop_event.is_set():
|
|
2047
|
-
logging.error(f"Unhandled exeption in _recording_worker: {e}")
|
|
2149
|
+
logging.error(f"Unhandled exeption in _recording_worker: {e}", exc_info=True)
|
|
2048
2150
|
raise
|
|
2049
2151
|
|
|
2050
2152
|
if self.use_extended_logging:
|
|
@@ -2111,7 +2213,7 @@ class AudioToTextRecorder:
|
|
|
2111
2213
|
logging.warning("Realtime transcription timed out")
|
|
2112
2214
|
continue
|
|
2113
2215
|
except Exception as e:
|
|
2114
|
-
logging.error(f"Error in realtime transcription: {str(e)}")
|
|
2216
|
+
logging.error(f"Error in realtime transcription: {str(e)}", exc_info=True)
|
|
2115
2217
|
continue
|
|
2116
2218
|
else:
|
|
2117
2219
|
# Perform transcription and assemble the text
|
|
@@ -2145,7 +2247,6 @@ class AudioToTextRecorder:
|
|
|
2145
2247
|
if self.is_recording and time.time() - \
|
|
2146
2248
|
self.recording_start_time > self.init_realtime_after_seconds:
|
|
2147
2249
|
|
|
2148
|
-
# logging.debug('Starting realtime transcription')
|
|
2149
2250
|
self.realtime_transcription_text = realtime_text
|
|
2150
2251
|
self.realtime_transcription_text = \
|
|
2151
2252
|
self.realtime_transcription_text.strip()
|
|
@@ -2225,7 +2326,7 @@ class AudioToTextRecorder:
|
|
|
2225
2326
|
time.sleep(TIME_SLEEP)
|
|
2226
2327
|
|
|
2227
2328
|
except Exception as e:
|
|
2228
|
-
logging.error(f"Unhandled exeption in _realtime_worker: {e}")
|
|
2329
|
+
logging.error(f"Unhandled exeption in _realtime_worker: {e}", exc_info=True)
|
|
2229
2330
|
raise
|
|
2230
2331
|
|
|
2231
2332
|
def _is_silero_speech(self, chunk):
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: RealtimeSTT
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.95
|
|
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
|
|
@@ -13,21 +13,22 @@ Requires-Python: >=3.6
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: PyAudio==0.2.14
|
|
16
|
-
Requires-Dist: faster-whisper==1.1.
|
|
16
|
+
Requires-Dist: faster-whisper==1.1.1
|
|
17
17
|
Requires-Dist: pvporcupine==1.9.5
|
|
18
18
|
Requires-Dist: webrtcvad-wheels==2.0.14
|
|
19
19
|
Requires-Dist: halo==0.0.31
|
|
20
20
|
Requires-Dist: torch
|
|
21
21
|
Requires-Dist: torchaudio
|
|
22
|
-
Requires-Dist: scipy==1.
|
|
22
|
+
Requires-Dist: scipy==1.15.1
|
|
23
23
|
Requires-Dist: openwakeword>=0.4.0
|
|
24
|
-
Requires-Dist: websockets==14.
|
|
24
|
+
Requires-Dist: websockets==14.2
|
|
25
25
|
Requires-Dist: websocket-client==1.8.0
|
|
26
26
|
Requires-Dist: numpy<2.0.0
|
|
27
|
+
Requires-Dist: soundfile==0.13.1
|
|
27
28
|
|
|
28
29
|
# RealtimeSTT
|
|
29
30
|
[](https://pypi.org/project/RealtimeSTT/)
|
|
30
|
-
[](https://pepy.tech/
|
|
31
|
+
[](https://www.pepy.tech/projects/realtimestt)
|
|
31
32
|
[](https://GitHub.com/KoljaB/RealtimeSTT/releases/)
|
|
32
33
|
[](https://GitHub.com/Naereen/KoljaB/RealtimeSTT/commit/)
|
|
33
34
|
[](https://GitHub.com/KoljaB/RealtimeSTT/network/)
|
|
@@ -55,7 +56,7 @@ https://github.com/user-attachments/assets/797e6552-27cd-41b1-a7f3-e5cbc72094f5
|
|
|
55
56
|
|
|
56
57
|
### Updates
|
|
57
58
|
|
|
58
|
-
Latest Version: v0.3.
|
|
59
|
+
Latest Version: v0.3.95
|
|
59
60
|
|
|
60
61
|
See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
|
|
61
62
|
|
|
@@ -67,7 +68,6 @@ See [release history](https://github.com/KoljaB/RealtimeSTT/releases).
|
|
|
67
68
|
|
|
68
69
|
```python
|
|
69
70
|
from RealtimeSTT import AudioToTextRecorder
|
|
70
|
-
import pyautogui
|
|
71
71
|
|
|
72
72
|
def process_text(text):
|
|
73
73
|
print(text)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
PyAudio==0.2.14
|
|
2
|
-
faster-whisper==1.1.
|
|
2
|
+
faster-whisper==1.1.1
|
|
3
3
|
pvporcupine==1.9.5
|
|
4
4
|
webrtcvad-wheels==2.0.14
|
|
5
5
|
halo==0.0.31
|
|
6
6
|
torch
|
|
7
7
|
torchaudio
|
|
8
|
-
scipy==1.
|
|
8
|
+
scipy==1.15.1
|
|
9
9
|
openwakeword>=0.4.0
|
|
10
|
-
websockets==14.
|
|
10
|
+
websockets==14.2
|
|
11
11
|
websocket-client==1.8.0
|
|
12
12
|
numpy<2.0.0
|
|
13
|
+
soundfile==0.13.1
|
|
@@ -9,7 +9,7 @@ with open('requirements.txt') as f:
|
|
|
9
9
|
|
|
10
10
|
setuptools.setup(
|
|
11
11
|
name="RealtimeSTT",
|
|
12
|
-
version="0.3.
|
|
12
|
+
version="0.3.95",
|
|
13
13
|
author="Kolja Beigel",
|
|
14
14
|
author_email="kolja.beigel@web.de",
|
|
15
15
|
description="A fast Voice Activity Detection and Transcription System",
|
|
@@ -25,6 +25,8 @@ setuptools.setup(
|
|
|
25
25
|
python_requires='>=3.6',
|
|
26
26
|
install_requires=requirements,
|
|
27
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
|
+
package_data={"RealtimeSTT": ["warmup_audio.wav"]},
|
|
29
|
+
include_package_data=True,
|
|
28
30
|
entry_points={
|
|
29
31
|
'console_scripts': [
|
|
30
32
|
'stt-server=RealtimeSTT_server.stt_server:main',
|
|
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
|