feat: add webrtcvad-based smart command-end detection

Replace the fixed 10s post-wake-word listen window with a VAD-driven
loop. Once at least 500ms of speech have been seen, 1.2s of silence
closes the window. Hard cap at 15s as a safety net.

Tunable in config.py via VAD_AGGRESSIVENESS, COMMAND_END_SILENCE_MS,
COMMAND_MIN_SPEECH_MS, COMMAND_MIN_LISTEN_MS, COMMAND_MAX_LISTEN_MS.
This commit is contained in:
Bossiara13 2026-04-22 19:32:30 +03:00
parent 277921ce14
commit 794d923bf9
3 changed files with 45 additions and 6 deletions

View file

@ -26,3 +26,11 @@ CHROME_PATH = 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe %s'
GROQ_TOKEN = os.getenv('GROQ_TOKEN') GROQ_TOKEN = os.getenv('GROQ_TOKEN')
GROQ_BASE_URL = "https://api.groq.com/openai/v1" GROQ_BASE_URL = "https://api.groq.com/openai/v1"
GROQ_MODEL = "llama-3.3-70b-versatile" GROQ_MODEL = "llama-3.3-70b-versatile"
# VAD (webrtcvad) — определяет конец команды по тишине вместо фиксированных 10 сек.
# 0..3, выше — агрессивнее режет шум (но и обрезает речь).
VAD_AGGRESSIVENESS = 2
COMMAND_END_SILENCE_MS = 1200
COMMAND_MIN_SPEECH_MS = 500
COMMAND_MIN_LISTEN_MS = 1000
COMMAND_MAX_LISTEN_MS = 15000

40
main.py
View file

@ -13,6 +13,7 @@ import openai
from openai import OpenAI from openai import OpenAI
import simpleaudio as sa import simpleaudio as sa
import vosk import vosk
import webrtcvad
import yaml import yaml
from comtypes import CLSCTX_ALL from comtypes import CLSCTX_ALL
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
@ -54,6 +55,10 @@ wake_rec = vosk.KaldiRecognizer(model, samplerate, wake_grammar)
kaldi_rec = vosk.KaldiRecognizer(model, samplerate) kaldi_rec = vosk.KaldiRecognizer(model, samplerate)
q = queue.Queue() q = queue.Queue()
VAD_FRAME_MS = 30
VAD_FRAME_BYTES = int(samplerate * VAD_FRAME_MS / 1000) * 2
vad = webrtcvad.Vad(config.VAD_AGGRESSIVENESS)
def gpt_answer(): def gpt_answer():
global message_log global message_log
@ -301,20 +306,43 @@ while True:
play("greet", True) play("greet", True)
print("Yes, sir.") print("Yes, sir.")
# reset the command recognizer so stale audio doesn't bleed into it
kaldi_rec.Reset() kaldi_rec.Reset()
ltc = time.time() listen_started = time.time()
speech_ms = 0
silence_ms = 0
vad_buf = b""
finalize = False
while True:
elapsed_ms = (time.time() - listen_started) * 1000
if elapsed_ms >= config.COMMAND_MAX_LISTEN_MS:
break
while time.time() - ltc <= 10:
pcm = recorder.read() pcm = recorder.read()
sp = struct.pack("h" * len(pcm), *pcm) sp = struct.pack("h" * len(pcm), *pcm)
kaldi_rec.AcceptWaveform(sp)
if kaldi_rec.AcceptWaveform(sp): vad_buf += sp
if va_respond(json.loads(kaldi_rec.Result())["text"]): while len(vad_buf) >= VAD_FRAME_BYTES:
ltc = time.time() frame = vad_buf[:VAD_FRAME_BYTES]
vad_buf = vad_buf[VAD_FRAME_BYTES:]
if vad.is_speech(frame, samplerate):
speech_ms += VAD_FRAME_MS
silence_ms = 0
else:
silence_ms += VAD_FRAME_MS
if elapsed_ms < config.COMMAND_MIN_LISTEN_MS:
continue
if speech_ms >= config.COMMAND_MIN_SPEECH_MS and silence_ms >= config.COMMAND_END_SILENCE_MS:
finalize = True
break break
if finalize or speech_ms > 0:
text = json.loads(kaldi_rec.FinalResult()).get("text", "")
if text:
va_respond(text)
wake_rec.Reset() wake_rec.Reset()
except Exception as err: except Exception as err:

View file

@ -4,6 +4,9 @@ pvrecorder
# Vosk # Vosk
vosk vosk
# Voice activity detection (smart command-end)
webrtcvad-wheels
# Groq / OpenAI-compatible LLM # Groq / OpenAI-compatible LLM
openai>=1.0 openai>=1.0