ShadowTEM commited on
Commit
ca8512a
·
verified ·
1 Parent(s): 94d9c2b

remode sd and pydub

Browse files
Files changed (1) hide show
  1. app/stt.py +244 -244
app/stt.py CHANGED
@@ -1,244 +1,244 @@
1
- import os
2
- import time
3
- import yaml
4
- import json
5
- from decouple import config as decouple_config # if still using .env for other values
6
- from groq import Groq
7
- import sounddevice as sd
8
- import soundfile as sf
9
- import numpy as np
10
- import noisereduce as nr
11
- import keyboard
12
- import queue
13
- import sys
14
- import datetime
15
- from pydub import AudioSegment
16
- import os
17
- import tempfile
18
-
19
- class AudioProcessor:
20
- def __init__(self):
21
- """
22
- Initialize the AudioProcessor by loading configuration and setting up the Groq client.
23
- """
24
- # Compute the absolute path to the config file located at <project_root>/config/config.yml.
25
- base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
26
- config_path = os.path.join(base_dir, "config", "config.yml")
27
-
28
- # Load the configuration from the YAML file.
29
- with open(config_path, "r") as file:
30
- self.config_data = yaml.safe_load(file)
31
-
32
- # Access the Groq settings from the configuration.
33
- self.groq_api_key = self.config_data["groq"]["api_key"]
34
- self.groq_model = self.config_data["groq"]["model"]
35
-
36
- # Initialize the Groq client.
37
- self.groq_client = Groq(api_key=self.groq_api_key)
38
-
39
- # Store default audio settings from the config.
40
- self.sample_rate = self.config_data.get("audio", {}).get("sample_rate", 44100)
41
- self.channels = self.config_data.get("audio", {}).get("channels", 1)
42
- self.default_duration = self.config_data.get("audio", {}).get("duration", 5)
43
- self.default_output_file = self.config_data.get("audio", {}).get("output_file", "denoised_output.wav")
44
-
45
- # Setup logging folder (assuming logs folder is at project_root/logs)
46
- base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
47
- self.logs_folder = os.path.join(base_dir, "logs")
48
- os.makedirs(self.logs_folder, exist_ok=True)
49
- self.log_file = os.path.join(self.logs_folder, "conversation_logs.json")
50
-
51
- def wait_for_key(self, target_key):
52
- """
53
- Wait for the target key to be pressed. If 's' is pressed, exit the program.
54
- """
55
- while True:
56
- if keyboard.is_pressed('s'):
57
- print("Stop key 's' pressed. Exiting program.")
58
- sys.exit(0)
59
- if keyboard.is_pressed(target_key):
60
- # Debounce: wait until key is released.
61
- while keyboard.is_pressed(target_key):
62
- time.sleep(0.1)
63
- break
64
- time.sleep(0.1)
65
-
66
- def record_and_denoise(self, fs=None, channels=None, filename=None):
67
- """
68
- Waits for the user to press 'q' to start recording and again 'q' to stop.
69
- Also checks if 's' is pressed to exit immediately.
70
- Then it performs noise reduction and saves the cleaned audio.
71
-
72
- Parameters:
73
- fs (int): Sampling rate (e.g., 44100 Hz). Defaults to configuration value.
74
- channels (int): Number of audio channels (1 for mono, 2 for stereo). Defaults to configuration value.
75
- filename (str): Output filename for the denoised audio. Defaults to configuration value.
76
-
77
- Returns:
78
- np.ndarray: The denoised audio signal.
79
- int: The sampling rate.
80
- """
81
- fs = fs if fs is not None else self.sample_rate
82
- channels = channels if channels is not None else self.channels
83
- filename = filename if filename is not None else self.default_output_file
84
-
85
- print("Press 'q' to start recording (or 's' to exit).")
86
- self.wait_for_key('q')
87
- print("Recording started. Press 'q' again to stop (or 's' to exit).")
88
-
89
- # Set up a queue to collect recorded audio data.
90
- audio_queue = queue.Queue()
91
-
92
- def callback(indata, frames, time_info, status):
93
- if status:
94
- print(status)
95
- audio_queue.put(indata.copy())
96
-
97
- # Start the input stream.
98
- stream = sd.InputStream(samplerate=fs, channels=channels, callback=callback)
99
- stream.start()
100
-
101
- # Instead of a single blocking wait, check in a loop.
102
- while True:
103
- if keyboard.is_pressed('s'):
104
- print("Stop key 's' pressed during recording. Exiting program.")
105
- stream.stop()
106
- sys.exit(0)
107
- if keyboard.is_pressed('q'):
108
- # Debounce the key press.
109
- while keyboard.is_pressed('q'):
110
- time.sleep(0.1)
111
- break
112
- time.sleep(0.1)
113
- stream.stop()
114
-
115
- # Combine all audio chunks from the queue.
116
- audio_chunks = []
117
- while not audio_queue.empty():
118
- audio_chunks.append(audio_queue.get())
119
- if audio_chunks:
120
- audio = np.concatenate(audio_chunks, axis=0)
121
- else:
122
- print("No audio was recorded.")
123
- return None, fs
124
-
125
- # If mono, remove the extra dimension.
126
- if channels == 1 and audio.ndim > 1:
127
- audio = np.squeeze(audio)
128
-
129
- print("Recording stopped. Reducing background noise...")
130
- noise_duration = 0.5
131
- noise_samples = int(noise_duration * fs)
132
- noise_sample = audio if audio.shape[0] < noise_samples else audio[:noise_samples]
133
-
134
- # Perform noise reduction.
135
- denoised_audio = nr.reduce_noise(y=audio, sr=fs, y_noise=noise_sample, prop_decrease=1.0)
136
-
137
- # Save the denoised audio to a file.
138
- sf.write(filename, denoised_audio, fs)
139
- print(f"Denoised audio saved as '{filename}'")
140
-
141
- return denoised_audio, fs
142
-
143
- def prepare_audio(self, file_path):
144
- """
145
- Check if the provided audio file is in WAV format.
146
- If not, convert it to WAV and return the path to the converted file.
147
- """
148
- ext = os.path.splitext(file_path)[1].lower()
149
- if ext != ".wav":
150
- try:
151
- audio = AudioSegment.from_file(file_path)
152
- # Create a temporary file to hold the WAV data.
153
- temp_wav_fd, temp_wav_path = tempfile.mkstemp(suffix=".wav")
154
- os.close(temp_wav_fd) # Close the file descriptor.
155
- audio.export(temp_wav_path, format="wav")
156
- print(f"Converted {file_path} to WAV format at {temp_wav_path}")
157
- return temp_wav_path
158
- except Exception as e:
159
- print(f"Error converting audio file: {e}")
160
- return file_path
161
- return file_path
162
-
163
- def transcribe_audio(self, file_path, language="en", prompt=None, temperature=0.0):
164
- """
165
- Transcribe an audio file using the Groq API with the specified Whisper model.
166
- This function now accepts any voice input format by converting non-WAV files to WAV.
167
-
168
- Parameters:
169
- file_path (str): Path to the audio file.
170
- language (str): Language code (default "en").
171
- prompt (str): Optional transcription prompt.
172
- temperature (float): Temperature parameter for transcription (default 0.0).
173
-
174
- Returns:
175
- str: The transcribed text.
176
- """
177
- # Ensure the audio is in WAV format.
178
- prepared_file = self.prepare_audio(file_path)
179
- with open(prepared_file, "rb") as f:
180
- audio_bytes = f.read()
181
-
182
- transcription = self.groq_client.audio.transcriptions.create(
183
- file=(os.path.basename(prepared_file), audio_bytes),
184
- model=self.groq_model,
185
- prompt=prompt,
186
- response_format="json",
187
- language=language,
188
- temperature=temperature
189
- )
190
-
191
- # If a temporary file was created, remove it.
192
- if prepared_file != file_path:
193
- try:
194
- os.remove(prepared_file)
195
- except Exception as e:
196
- print(f"Error removing temporary file: {e}")
197
-
198
- return transcription.text
199
-
200
- def log_conversation(self, user_text, bot_text=""):
201
- """
202
- Append a conversation entry to the JSON log file.
203
- Each entry has the following format:
204
- {
205
- "timestamp": "YYYY-MM-DD HH:MM:SS",
206
- "User": { "text": "User's transcribed text" },
207
- "bot": { "text": "Bot's reply text" }
208
- }
209
-
210
- Parameters:
211
- user_text (str): The text from the user (transcribed).
212
- bot_text (str): The bot's reply text (empty until LLM integration).
213
- """
214
- entry = {
215
- "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
216
- "User": {"text": user_text},
217
- "bot": {"text": bot_text}
218
- }
219
-
220
- # Load existing logs if the file exists, else start with an empty list.
221
- if os.path.exists(self.log_file):
222
- try:
223
- with open(self.log_file, "r", encoding="utf-8") as f:
224
- logs = json.load(f)
225
- except json.JSONDecodeError:
226
- logs = []
227
- else:
228
- logs = []
229
-
230
- logs.append(entry)
231
-
232
- # Save the updated logs.
233
- with open(self.log_file, "w", encoding="utf-8") as f:
234
- json.dump(logs, f, indent=4)
235
- print("Conversation logged.")
236
-
237
- # Example usage:
238
- if __name__ == "__main__":
239
- processor = AudioProcessor()
240
- denoised_audio, fs = processor.record_and_denoise()
241
- if denoised_audio:
242
- transcription = processor.transcribe_audio(processor.default_output_file, language="en")
243
- print("Transcribed Text:\n", transcription)
244
- processor.log_conversation(transcription, bot_text="")
 
1
+ import os
2
+ import time
3
+ import yaml
4
+ import json
5
+ from decouple import config as decouple_config # if still using .env for other values
6
+ from groq import Groq
7
+ # import sounddevice as sd
8
+ import soundfile as sf
9
+ import numpy as np
10
+ import noisereduce as nr
11
+ import keyboard
12
+ import queue
13
+ import sys
14
+ import datetime
15
+ # from pydub import AudioSegment
16
+ import os
17
+ import tempfile
18
+
19
+ class AudioProcessor:
20
+ def __init__(self):
21
+ """
22
+ Initialize the AudioProcessor by loading configuration and setting up the Groq client.
23
+ """
24
+ # Compute the absolute path to the config file located at <project_root>/config/config.yml.
25
+ base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
26
+ config_path = os.path.join(base_dir, "config", "config.yml")
27
+
28
+ # Load the configuration from the YAML file.
29
+ with open(config_path, "r") as file:
30
+ self.config_data = yaml.safe_load(file)
31
+
32
+ # Access the Groq settings from the configuration.
33
+ self.groq_api_key = self.config_data["groq"]["api_key"]
34
+ self.groq_model = self.config_data["groq"]["model"]
35
+
36
+ # Initialize the Groq client.
37
+ self.groq_client = Groq(api_key=self.groq_api_key)
38
+
39
+ # Store default audio settings from the config.
40
+ self.sample_rate = self.config_data.get("audio", {}).get("sample_rate", 44100)
41
+ self.channels = self.config_data.get("audio", {}).get("channels", 1)
42
+ self.default_duration = self.config_data.get("audio", {}).get("duration", 5)
43
+ self.default_output_file = self.config_data.get("audio", {}).get("output_file", "denoised_output.wav")
44
+
45
+ # Setup logging folder (assuming logs folder is at project_root/logs)
46
+ base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
47
+ self.logs_folder = os.path.join(base_dir, "logs")
48
+ os.makedirs(self.logs_folder, exist_ok=True)
49
+ self.log_file = os.path.join(self.logs_folder, "conversation_logs.json")
50
+
51
+ def wait_for_key(self, target_key):
52
+ """
53
+ Wait for the target key to be pressed. If 's' is pressed, exit the program.
54
+ """
55
+ while True:
56
+ if keyboard.is_pressed('s'):
57
+ print("Stop key 's' pressed. Exiting program.")
58
+ sys.exit(0)
59
+ if keyboard.is_pressed(target_key):
60
+ # Debounce: wait until key is released.
61
+ while keyboard.is_pressed(target_key):
62
+ time.sleep(0.1)
63
+ break
64
+ time.sleep(0.1)
65
+
66
+ def record_and_denoise(self, fs=None, channels=None, filename=None):
67
+ """
68
+ Waits for the user to press 'q' to start recording and again 'q' to stop.
69
+ Also checks if 's' is pressed to exit immediately.
70
+ Then it performs noise reduction and saves the cleaned audio.
71
+
72
+ Parameters:
73
+ fs (int): Sampling rate (e.g., 44100 Hz). Defaults to configuration value.
74
+ channels (int): Number of audio channels (1 for mono, 2 for stereo). Defaults to configuration value.
75
+ filename (str): Output filename for the denoised audio. Defaults to configuration value.
76
+
77
+ Returns:
78
+ np.ndarray: The denoised audio signal.
79
+ int: The sampling rate.
80
+ """
81
+ fs = fs if fs is not None else self.sample_rate
82
+ channels = channels if channels is not None else self.channels
83
+ filename = filename if filename is not None else self.default_output_file
84
+
85
+ print("Press 'q' to start recording (or 's' to exit).")
86
+ self.wait_for_key('q')
87
+ print("Recording started. Press 'q' again to stop (or 's' to exit).")
88
+
89
+ # Set up a queue to collect recorded audio data.
90
+ audio_queue = queue.Queue()
91
+
92
+ def callback(indata, frames, time_info, status):
93
+ if status:
94
+ print(status)
95
+ audio_queue.put(indata.copy())
96
+
97
+ # Start the input stream.
98
+ stream = sd.InputStream(samplerate=fs, channels=channels, callback=callback)
99
+ stream.start()
100
+
101
+ # Instead of a single blocking wait, check in a loop.
102
+ while True:
103
+ if keyboard.is_pressed('s'):
104
+ print("Stop key 's' pressed during recording. Exiting program.")
105
+ stream.stop()
106
+ sys.exit(0)
107
+ if keyboard.is_pressed('q'):
108
+ # Debounce the key press.
109
+ while keyboard.is_pressed('q'):
110
+ time.sleep(0.1)
111
+ break
112
+ time.sleep(0.1)
113
+ stream.stop()
114
+
115
+ # Combine all audio chunks from the queue.
116
+ audio_chunks = []
117
+ while not audio_queue.empty():
118
+ audio_chunks.append(audio_queue.get())
119
+ if audio_chunks:
120
+ audio = np.concatenate(audio_chunks, axis=0)
121
+ else:
122
+ print("No audio was recorded.")
123
+ return None, fs
124
+
125
+ # If mono, remove the extra dimension.
126
+ if channels == 1 and audio.ndim > 1:
127
+ audio = np.squeeze(audio)
128
+
129
+ print("Recording stopped. Reducing background noise...")
130
+ noise_duration = 0.5
131
+ noise_samples = int(noise_duration * fs)
132
+ noise_sample = audio if audio.shape[0] < noise_samples else audio[:noise_samples]
133
+
134
+ # Perform noise reduction.
135
+ denoised_audio = nr.reduce_noise(y=audio, sr=fs, y_noise=noise_sample, prop_decrease=1.0)
136
+
137
+ # Save the denoised audio to a file.
138
+ sf.write(filename, denoised_audio, fs)
139
+ print(f"Denoised audio saved as '{filename}'")
140
+
141
+ return denoised_audio, fs
142
+
143
+ def prepare_audio(self, file_path):
144
+ """
145
+ Check if the provided audio file is in WAV format.
146
+ If not, convert it to WAV and return the path to the converted file.
147
+ """
148
+ ext = os.path.splitext(file_path)[1].lower()
149
+ if ext != ".wav":
150
+ try:
151
+ audio = AudioSegment.from_file(file_path)
152
+ # Create a temporary file to hold the WAV data.
153
+ temp_wav_fd, temp_wav_path = tempfile.mkstemp(suffix=".wav")
154
+ os.close(temp_wav_fd) # Close the file descriptor.
155
+ audio.export(temp_wav_path, format="wav")
156
+ print(f"Converted {file_path} to WAV format at {temp_wav_path}")
157
+ return temp_wav_path
158
+ except Exception as e:
159
+ print(f"Error converting audio file: {e}")
160
+ return file_path
161
+ return file_path
162
+
163
+ def transcribe_audio(self, file_path, language="en", prompt=None, temperature=0.0):
164
+ """
165
+ Transcribe an audio file using the Groq API with the specified Whisper model.
166
+ This function now accepts any voice input format by converting non-WAV files to WAV.
167
+
168
+ Parameters:
169
+ file_path (str): Path to the audio file.
170
+ language (str): Language code (default "en").
171
+ prompt (str): Optional transcription prompt.
172
+ temperature (float): Temperature parameter for transcription (default 0.0).
173
+
174
+ Returns:
175
+ str: The transcribed text.
176
+ """
177
+ # Ensure the audio is in WAV format.
178
+ prepared_file = self.prepare_audio(file_path)
179
+ with open(prepared_file, "rb") as f:
180
+ audio_bytes = f.read()
181
+
182
+ transcription = self.groq_client.audio.transcriptions.create(
183
+ file=(os.path.basename(prepared_file), audio_bytes),
184
+ model=self.groq_model,
185
+ prompt=prompt,
186
+ response_format="json",
187
+ language=language,
188
+ temperature=temperature
189
+ )
190
+
191
+ # If a temporary file was created, remove it.
192
+ if prepared_file != file_path:
193
+ try:
194
+ os.remove(prepared_file)
195
+ except Exception as e:
196
+ print(f"Error removing temporary file: {e}")
197
+
198
+ return transcription.text
199
+
200
+ def log_conversation(self, user_text, bot_text=""):
201
+ """
202
+ Append a conversation entry to the JSON log file.
203
+ Each entry has the following format:
204
+ {
205
+ "timestamp": "YYYY-MM-DD HH:MM:SS",
206
+ "User": { "text": "User's transcribed text" },
207
+ "bot": { "text": "Bot's reply text" }
208
+ }
209
+
210
+ Parameters:
211
+ user_text (str): The text from the user (transcribed).
212
+ bot_text (str): The bot's reply text (empty until LLM integration).
213
+ """
214
+ entry = {
215
+ "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
216
+ "User": {"text": user_text},
217
+ "bot": {"text": bot_text}
218
+ }
219
+
220
+ # Load existing logs if the file exists, else start with an empty list.
221
+ if os.path.exists(self.log_file):
222
+ try:
223
+ with open(self.log_file, "r", encoding="utf-8") as f:
224
+ logs = json.load(f)
225
+ except json.JSONDecodeError:
226
+ logs = []
227
+ else:
228
+ logs = []
229
+
230
+ logs.append(entry)
231
+
232
+ # Save the updated logs.
233
+ with open(self.log_file, "w", encoding="utf-8") as f:
234
+ json.dump(logs, f, indent=4)
235
+ print("Conversation logged.")
236
+
237
+ # Example usage:
238
+ if __name__ == "__main__":
239
+ processor = AudioProcessor()
240
+ denoised_audio, fs = processor.record_and_denoise()
241
+ if denoised_audio:
242
+ transcription = processor.transcribe_audio(processor.default_output_file, language="en")
243
+ print("Transcribed Text:\n", transcription)
244
+ processor.log_conversation(transcription, bot_text="")