refactor: replace porcupine wake-word with vosk grammar-constrained recognizer

uses a second KaldiRecognizer with a JSON grammar of WAKE_WORDS + [unk] for
the wake phase, then hands off to the full-vocab recognizer for the 10s
command window. drops the pvporcupine dependency entirely.
This commit is contained in:
Bossiara13 2026-04-22 17:46:44 +03:00
parent 8019ee0baa
commit a4b919c11e
2 changed files with 35 additions and 27 deletions

View file

@ -6,10 +6,15 @@ load_dotenv("dev.env")
# Конфигурация # Конфигурация
VA_NAME = 'Jarvis' VA_NAME = 'Jarvis'
VA_VER = "0.1.0" VA_VER = "0.2.0"
VA_ALIAS = ('джарвис',) VA_ALIAS = ('джарвис',)
VA_TBR = ('скажи', 'покажи', 'ответь', 'произнеси', 'расскажи', 'сколько', 'слушай') VA_TBR = ('скажи', 'покажи', 'ответь', 'произнеси', 'расскажи', 'сколько', 'слушай')
# Wake-words распознаются Vosk'ом с grammar-constraint. Добавляйте сюда
# варианты написания — Vosk с русской моделью может расшифровать "jarvis"
# как "джарвис", "жарвис" и т.п.
WAKE_WORDS = ('jarvis', 'джарвис')
# ID микрофона (можете просто менять ID пока при запуске не отобразится нужный) # ID микрофона (можете просто менять ID пока при запуске не отобразится нужный)
# -1 это стандартное записывающее устройство # -1 это стандартное записывающее устройство
MICROPHONE_INDEX = -1 MICROPHONE_INDEX = -1
@ -17,9 +22,6 @@ MICROPHONE_INDEX = -1
# Путь к браузеру Google Chrome # Путь к браузеру Google Chrome
CHROME_PATH = 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe %s' CHROME_PATH = 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe %s'
# Токен Picovoice
PICOVOICE_TOKEN = os.getenv('PICOVOICE_TOKEN')
# Токен Groq # Токен Groq
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"

52
main.py
View file

@ -11,7 +11,6 @@ from ctypes import POINTER, cast
import openai import openai
from openai import OpenAI from openai import OpenAI
import pvporcupine
import simpleaudio as sa import simpleaudio as sa
import vosk import vosk
import yaml import yaml
@ -39,18 +38,14 @@ message_log = [system_message]
client = OpenAI(api_key=config.GROQ_TOKEN, base_url=config.GROQ_BASE_URL) client = OpenAI(api_key=config.GROQ_TOKEN, base_url=config.GROQ_BASE_URL)
# PORCUPINE
porcupine = pvporcupine.create(
access_key=config.PICOVOICE_TOKEN,
keywords=['jarvis'],
sensitivities=[1]
)
# print(pvporcupine.KEYWORDS)
# VOSK
model = vosk.Model("model_small") model = vosk.Model("model_small")
samplerate = 16000 samplerate = 16000
device = config.MICROPHONE_INDEX device = config.MICROPHONE_INDEX
# wake-word phase uses a grammar-constrained recognizer so only WAKE_WORDS
# (plus [unk] filler) can match — command phase needs full vocab, hence two.
wake_grammar = json.dumps(list(config.WAKE_WORDS) + ["[unk]"], ensure_ascii=False)
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()
@ -268,33 +263,42 @@ def execute_cmd(cmd: str, voice: str):
elif cmd == 'off': elif cmd == 'off':
play("off", True) play("off", True)
porcupine.delete()
exit(0) exit(0)
# `-1` is the default input audio device. recorder = PvRecorder(device_index=config.MICROPHONE_INDEX, frame_length=512)
recorder = PvRecorder(device_index=config.MICROPHONE_INDEX, frame_length=porcupine.frame_length)
recorder.start() recorder.start()
print('Using device: %s' % recorder.selected_device) print('Using device: %s' % recorder.selected_device)
print(f"Jarvis (v0.1.0) начал свою работу ...") print(f"Jarvis (v{config.VA_VER}) начал свою работу ...")
play("run") play("run")
time.sleep(0.5) time.sleep(0.5)
ltc = time.time() - 1000
def heard_wake_word(text: str) -> bool:
if not text:
return False
lowered = text.lower()
return any(w in lowered for w in config.WAKE_WORDS)
while True: while True:
try: try:
pcm = recorder.read() pcm = recorder.read()
keyword_index = porcupine.process(pcm) sp = struct.pack("h" * len(pcm), *pcm)
if keyword_index >= 0: if not wake_rec.AcceptWaveform(sp):
recorder.stop() continue
play("greet", True)
print("Yes, sir.") result_text = json.loads(wake_rec.Result()).get("text", "")
recorder.start() # prevent self recording if not heard_wake_word(result_text):
ltc = time.time() continue
play("greet", True)
print("Yes, sir.")
# reset the command recognizer so stale audio doesn't bleed into it
kaldi_rec.Reset()
ltc = time.time()
while time.time() - ltc <= 10: while time.time() - ltc <= 10:
pcm = recorder.read() pcm = recorder.read()
@ -306,6 +310,8 @@ while True:
break break
wake_rec.Reset()
except Exception as err: except Exception as err:
print(f"Unexpected {err=}, {type(err)=}") print(f"Unexpected {err=}, {type(err)=}")
raise raise