File size: 10,126 Bytes
ca8512a
 
 
 
 
 
 
 
 
 
36436fd
ca8512a
 
 
 
 
 
 
 
 
 
 
 
0f3e652
ca8512a
 
 
 
0f3e652
ca8512a
 
 
0f3e652
 
 
 
 
 
 
 
 
 
 
ca8512a
0f3e652
ca8512a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
import os
import time
import yaml
import json
from decouple import config as decouple_config  # if still using .env for other values
from groq import Groq
# import sounddevice as sd
import soundfile as sf
import numpy as np
import noisereduce as nr
# import keyboard  
import queue
import sys
import datetime
# from pydub import AudioSegment
import os
import tempfile

class AudioProcessor:
    def __init__(self):
        """
        Initialize the AudioProcessor by loading configuration and setting up the Groq client.
        """
        
        # Compute the absolute path to the config file located at <project_root>/config/config.yml.
        base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
        config_path = os.path.join(base_dir, "config", "config.yml")
        
        # Load the configuration from the YAML file
        with open(config_path, "r") as file:
            self.config_data = yaml.safe_load(file)
        
        # Access Groq settings - Get API key from Hugging Face secrets
        self.groq_api_key = os.getenv("GROQ_API_KEY")  # From environment variables
        self.groq_model = self.config_data["groq"]["model"]  # Keep model name in config
        
        # Validate configuration
        if not self.groq_api_key:
            raise ValueError("Groq API key not found in environment variables. "
                             "Set GROQ_API_KEY in Hugging Face secrets.")
        
        if not self.groq_model:
            raise ValueError("Groq model not configured in config.yml")
        
        # Initialize the Groq client
        self.groq_client = Groq(api_key=self.groq_api_key)
        
        # Store default audio settings from the config.
        self.sample_rate = self.config_data.get("audio", {}).get("sample_rate", 44100)
        self.channels = self.config_data.get("audio", {}).get("channels", 1)
        self.default_duration = self.config_data.get("audio", {}).get("duration", 5)
        self.default_output_file = self.config_data.get("audio", {}).get("output_file", "denoised_output.wav")
        
        # Setup logging folder (assuming logs folder is at project_root/logs)
        base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
        self.logs_folder = os.path.join(base_dir, "logs")
        os.makedirs(self.logs_folder, exist_ok=True)
        self.log_file = os.path.join(self.logs_folder, "conversation_logs.json")
    
    def wait_for_key(self, target_key):
        """
        Wait for the target key to be pressed. If 's' is pressed, exit the program.
        """
        while True:
            if keyboard.is_pressed('s'):
                print("Stop key 's' pressed. Exiting program.")
                sys.exit(0)
            if keyboard.is_pressed(target_key):
                # Debounce: wait until key is released.
                while keyboard.is_pressed(target_key):
                    time.sleep(0.1)
                break
            time.sleep(0.1)

    def record_and_denoise(self, fs=None, channels=None, filename=None):
        """
        Waits for the user to press 'q' to start recording and again 'q' to stop.
        Also checks if 's' is pressed to exit immediately.
        Then it performs noise reduction and saves the cleaned audio.
        
        Parameters:
            fs (int): Sampling rate (e.g., 44100 Hz). Defaults to configuration value.
            channels (int): Number of audio channels (1 for mono, 2 for stereo). Defaults to configuration value.
            filename (str): Output filename for the denoised audio. Defaults to configuration value.
        
        Returns:
            np.ndarray: The denoised audio signal.
            int: The sampling rate.
        """
        fs = fs if fs is not None else self.sample_rate
        channels = channels if channels is not None else self.channels
        filename = filename if filename is not None else self.default_output_file

        print("Press 'q' to start recording (or 's' to exit).")
        self.wait_for_key('q')
        print("Recording started. Press 'q' again to stop (or 's' to exit).")
        
        # Set up a queue to collect recorded audio data.
        audio_queue = queue.Queue()
        
        def callback(indata, frames, time_info, status):
            if status:
                print(status)
            audio_queue.put(indata.copy())
        
        # Start the input stream.
        stream = sd.InputStream(samplerate=fs, channels=channels, callback=callback)
        stream.start()
        
        # Instead of a single blocking wait, check in a loop.
        while True:
            if keyboard.is_pressed('s'):
                print("Stop key 's' pressed during recording. Exiting program.")
                stream.stop()
                sys.exit(0)
            if keyboard.is_pressed('q'):
                # Debounce the key press.
                while keyboard.is_pressed('q'):
                    time.sleep(0.1)
                break
            time.sleep(0.1)
        stream.stop()
        
        # Combine all audio chunks from the queue.
        audio_chunks = []
        while not audio_queue.empty():
            audio_chunks.append(audio_queue.get())
        if audio_chunks:
            audio = np.concatenate(audio_chunks, axis=0)
        else:
            print("No audio was recorded.")
            return None, fs
        
        # If mono, remove the extra dimension.
        if channels == 1 and audio.ndim > 1:
            audio = np.squeeze(audio)
        
        print("Recording stopped. Reducing background noise...")
        noise_duration = 0.5
        noise_samples = int(noise_duration * fs)
        noise_sample = audio if audio.shape[0] < noise_samples else audio[:noise_samples]
        
        # Perform noise reduction.
        denoised_audio = nr.reduce_noise(y=audio, sr=fs, y_noise=noise_sample, prop_decrease=1.0)
        
        # Save the denoised audio to a file.
        sf.write(filename, denoised_audio, fs)
        print(f"Denoised audio saved as '{filename}'")
        
        return denoised_audio, fs

    def prepare_audio(self, file_path):
        """
        Check if the provided audio file is in WAV format.
        If not, convert it to WAV and return the path to the converted file.
        """
        ext = os.path.splitext(file_path)[1].lower()
        if ext != ".wav":
            try:
                audio = AudioSegment.from_file(file_path)
                # Create a temporary file to hold the WAV data.
                temp_wav_fd, temp_wav_path = tempfile.mkstemp(suffix=".wav")
                os.close(temp_wav_fd)  # Close the file descriptor.
                audio.export(temp_wav_path, format="wav")
                print(f"Converted {file_path} to WAV format at {temp_wav_path}")
                return temp_wav_path
            except Exception as e:
                print(f"Error converting audio file: {e}")
                return file_path
        return file_path

    def transcribe_audio(self, file_path, language="en", prompt=None, temperature=0.0):
        """
        Transcribe an audio file using the Groq API with the specified Whisper model.
        This function now accepts any voice input format by converting non-WAV files to WAV.
        
        Parameters:
            file_path (str): Path to the audio file.
            language (str): Language code (default "en").
            prompt (str): Optional transcription prompt.
            temperature (float): Temperature parameter for transcription (default 0.0).
            
        Returns:
            str: The transcribed text.
        """
        # Ensure the audio is in WAV format.
        prepared_file = self.prepare_audio(file_path)
        with open(prepared_file, "rb") as f:
            audio_bytes = f.read()
        
        transcription = self.groq_client.audio.transcriptions.create(
            file=(os.path.basename(prepared_file), audio_bytes),
            model=self.groq_model,
            prompt=prompt,
            response_format="json",
            language=language,
            temperature=temperature
        )
        
        # If a temporary file was created, remove it.
        if prepared_file != file_path:
            try:
                os.remove(prepared_file)
            except Exception as e:
                print(f"Error removing temporary file: {e}")
        
        return transcription.text

    def log_conversation(self, user_text, bot_text=""):
        """
        Append a conversation entry to the JSON log file.
        Each entry has the following format:
        {
            "timestamp": "YYYY-MM-DD HH:MM:SS",
            "User": { "text": "User's transcribed text" },
            "bot": { "text": "Bot's reply text" }
        }
        
        Parameters:
            user_text (str): The text from the user (transcribed).
            bot_text (str): The bot's reply text (empty until LLM integration).
        """
        entry = {
            "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "User": {"text": user_text},
            "bot": {"text": bot_text}
        }
        
        # Load existing logs if the file exists, else start with an empty list.
        if os.path.exists(self.log_file):
            try:
                with open(self.log_file, "r", encoding="utf-8") as f:
                    logs = json.load(f)
            except json.JSONDecodeError:
                logs = []
        else:
            logs = []
        
        logs.append(entry)
        
        # Save the updated logs.
        with open(self.log_file, "w", encoding="utf-8") as f:
            json.dump(logs, f, indent=4)
        print("Conversation logged.")

# Example usage:
if __name__ == "__main__":
    processor = AudioProcessor()
    denoised_audio, fs = processor.record_and_denoise()
    if denoised_audio:
        transcription = processor.transcribe_audio(processor.default_output_file, language="en")
        print("Transcribed Text:\n", transcription)
        processor.log_conversation(transcription, bot_text="")