diff --git a/config.py b/config.py index 1904666..d9e9e29 100644 --- a/config.py +++ b/config.py @@ -47,3 +47,10 @@ TTS_PITCH_SEMITONES = 0 # фраза (без алиасов и vа_tbr); 0.45 эмпирически отделяет валидные команды # от шума, оставляя запас на разговорные перефразировки. INTENT_SIMILARITY_THRESHOLD = 0.45 + +# Шумоподавление аудио перед Vosk на этапе захвата команды (после wake-word). +# Спектральное гейтирование через noisereduce; даёт небольшую латентность, +# поэтому выключено по умолчанию и применяется только к буферу команды. +DENOISE_ENABLED = False +DENOISE_PROP = 0.85 +DENOISE_STATIONARY = False diff --git a/main.py b/main.py index e5be02a..56909f4 100644 --- a/main.py +++ b/main.py @@ -10,6 +10,7 @@ import time import webbrowser from ctypes import POINTER, cast +import numpy as np import openai from openai import OpenAI import simpleaudio as sa @@ -64,6 +65,25 @@ vad = webrtcvad.Vad(config.VAD_AGGRESSIVENESS) intent_classifier = IntentClassifier() intent_classifier.prime({c: v['phrases'] for c, v in VA_CMD_LIST.items()}) +if config.DENOISE_ENABLED: + import noisereduce as nr +else: + nr = None + + +def denoise_pcm(raw: bytes) -> bytes: + if not raw or nr is None: + return raw + samples = np.frombuffer(raw, dtype=np.int16).astype(np.float32) / 32768.0 + reduced = nr.reduce_noise( + y=samples, + sr=samplerate, + prop_decrease=config.DENOISE_PROP, + stationary=config.DENOISE_STATIONARY, + ) + clipped = np.clip(reduced * 32768.0, -32768, 32767).astype(np.int16) + return clipped.tobytes() + def gpt_answer(): global message_log @@ -294,6 +314,7 @@ while True: silence_ms = 0 vad_buf = b"" finalize = False + cmd_buf = b"" if config.DENOISE_ENABLED else None while True: elapsed_ms = (time.time() - listen_started) * 1000 @@ -302,7 +323,10 @@ while True: pcm = recorder.read() sp = struct.pack("h" * len(pcm), *pcm) - kaldi_rec.AcceptWaveform(sp) + if cmd_buf is None: + kaldi_rec.AcceptWaveform(sp) + else: + cmd_buf += sp vad_buf += sp while len(vad_buf) >= VAD_FRAME_BYTES: @@ -321,6 +345,8 @@ while True: break if finalize or speech_ms > 0: + if cmd_buf is not None: + kaldi_rec.AcceptWaveform(denoise_pcm(cmd_buf)) text = json.loads(kaldi_rec.FinalResult()).get("text", "") if text: va_respond(text)