Merge feature/vad-and-yaml-plugins into dev

Adds webrtcvad-based smart command-end detection (fixed 10s window
replaced by VAD with configurable silence/min/max thresholds) and
migrates execute_cmd to a YAML-driven plugin system with typed
actions (exe, play_sound, system, multi, sleep). 17 commands
migrated; all original behavior preserved.
This commit is contained in:
Bossiara13 2026-04-22 19:43:52 +03:00
commit 4f3b827a47
5 changed files with 374 additions and 244 deletions

View file

@ -49,10 +49,51 @@ python main.py
## Использование
1. Скажите **«Джарвис»** (или **«jarvis»**) — ассистент даст звуковой сигнал готовности.
2. В течение 10 секунд произнесите команду:
2. Произнесите команду — окно записи закрывается само, как только вы замолчите (см. ниже про VAD):
- либо одну из фраз из `commands.yaml` (открыть браузер, выключить звук и т.д.);
- либо начните со слова **«скажи …»** — остаток фразы уйдёт в Groq, ответ будет зачитан вслух.
## VAD (определение конца команды)
Окно записи команды закрывается по голосовой активности (webrtcvad), а не по таймеру. Параметры в `config.py`:
- `VAD_AGGRESSIVENESS` (0..3) — насколько агрессивно VAD режет шум; по умолчанию 2.
- `COMMAND_END_SILENCE_MS` — длительность тишины, после которой окно закрывается (по умолчанию 1200 мс).
- `COMMAND_MIN_SPEECH_MS` — минимальная длительность речи, иначе тишина не считается концом (500 мс).
- `COMMAND_MIN_LISTEN_MS` — минимальное время записи, чтобы не закрыться мгновенно (1000 мс).
- `COMMAND_MAX_LISTEN_MS` — жёсткий потолок (15000 мс).
## Схема commands.yaml
Каждая команда — это пара `phrases` + `action`:
```yaml
open_browser:
phrases:
- открой браузер
- запусти браузер
action: {type: exe, file: "Run browser.exe"}
```
Типы действий:
- `exe` — запустить `.exe` из `custom-commands/`. Поля: `file` (имя файла), необязательные `wait: true` (ждать завершения), `delay_ms` (пауза после запуска). По умолчанию после `exe` проигрывается звук подтверждения `ok` — отключается через `confirm_sound: null`.
- `play_sound` — проиграть WAV из `sound/`. Поле `name` (без расширения). Имена `greet` и `ok` проигрываются с рандомным выбором из вариантов `greet1/2/3.wav`, `ok1/2/3.wav`.
- `system` — встроенные операции. `op: volume_mute`, `volume_unmute`, `exit`.
- `multi` — последовательность шагов: `steps: [...]`.
Шаги внутри `multi` — тех же типов. Пример:
```yaml
sound_off:
phrases: [выключи звук, отключи звук]
action:
type: multi
steps:
- {type: play_sound, name: ok}
- {type: system, op: volume_mute}
```
## Лицензия
CC BY-NC-SA 4.0 — см. [LICENSE.txt](LICENSE.txt). Авторство оригинала — Abraham Tugalov / Priler. Изменения в этом форке — Bossiara13 (Dmitry Bykov), 2026.

View file

@ -1,21 +1,27 @@
gaming_mode_off:
- рабочий режим
- вернись в рабочий режим
- переключись на компьютер
- обратно на компьютер
- отключи игровой режим
- выйди из игрового режима
- вернись на компьютер
- выход с игрового режима
- рабочее пространство
- вернись в игровой режим
- выключи игровой режим
gaming_mode_on:
- включи игровой режим
- перейди в игровой режим
- я хочу поиграть
- переключись на телевизор
open_browser:
phrases:
- открой браузер
- запусти браузер
- открой гугл хром
- гугл хром
action: {type: exe, file: "Run browser.exe"}
open_youtube:
phrases:
- открой ютуб
- ютуб
- запусти ютуб
action: {type: exe, file: "Run youtube.exe"}
open_google:
phrases:
- открой гугл
- гугл
- запусти гугл
action: {type: exe, file: "Run google.exe"}
music:
phrases:
- давай музыку
- музыка
- яндекс музыка
@ -26,7 +32,40 @@ music:
- врубай музло
- включи что-то послушать
- давай что-то послушаем
action: {type: exe, file: "Run music.exe"}
music_off:
phrases:
- выключи музыку
- отруби музыку
- убери музыку
- отключи музыку
- закрой музыку
- останови музыку
- хватит музыки
action: {type: exe, file: "Stop music.exe", delay_ms: 200}
music_save:
phrases:
- сохрани трек
- мне нравится трек
- сохрани песню
- мне нравится песня
- добавь трек в избранное
- добавь песню в избранное
- запомни мелодию
- запомни трек
- запомни песню
- добавь мелодию в избранное
- сохрани то что сейчас играет
- добавь то что сейчас играет в избранное
- лайкни трек
- лайкни мелодию
- лайкни песню
action: {type: exe, file: "Save music.exe", delay_ms: 200}
music_next:
phrases:
- следующий трек
- следующая мелодия
- следующая песня
@ -35,15 +74,10 @@ music_next:
- переключи мелодию
- переключи песню
- следующая музыка
music_off:
- выключи музыку
- отруби музыку
- убери музыку
- отключи музыку
- закрой музыку
- останови музыку
- хватит музыки
action: {type: exe, file: "Next music.exe", delay_ms: 200}
music_prev:
phrases:
- предыдущая музыка
- предыдущий трек
- предыдущая мелодия
@ -61,76 +95,87 @@ music_prev:
- переключи на прошлый трек
- переключи на прошлую мелодию
- переключи на прошлую песню
music_save:
- сохрани трек
- мне нравится трек
- сохрани песню
- мне нравится песня
- добавь трек в избранное
- добавь песню в избранное
- запомни мелодию
- запомни трек
- запомни песню
- добавь мелодию в избранное
- сохрани то что сейчас играет
- добавь то что сейчас играет в избранное
- лайкни трек
- лайкни мелодию
- лайкни песню
'off':
- выключись
- вырубись
- завершить работу
- закройся
- отключись
- заверши свою работу
- на сегодня хватит
- выгрузи себя из памяти
- ты мне надоел
- пора спать
open_browser:
- открой браузер
- запусти браузер
- открой гугл хром
- гугл хром
open_google:
- открой гугл
- гугл
- запусти гугл
open_youtube:
- открой ютуб
- ютуб
- запусти ютуб
action: {type: exe, file: "Prev music.exe", delay_ms: 200}
sound_off:
phrases:
- выключи звук
- беззвучный режим
- режим без звука
- отключи звук
action:
type: multi
steps:
- {type: play_sound, name: ok}
- {type: system, op: volume_mute}
sound_on:
phrases:
- включи звук
- режим со звуком
- верни звук
action:
type: multi
steps:
- {type: system, op: volume_unmute}
- {type: play_sound, name: ok}
thanks:
phrases:
- спасибо
- молодец
- респект
- ты супер
- отличная работа
- ты крут
- ты большой молодец
- ты реально крут
- ты афигенный
action: {type: play_sound, name: thanks}
stupid:
phrases:
- ты дурак
- ты дебил
- ты глупый
- ты тупой
switch_to_dynamics:
- переключи на динамики
- включи динамики
- перейди на динамики
- давай звук в динамики
- давай звук на динамики
- переключи звук в динамики
- переключи звук на динамики
- переключи на колонки
- включи колонки
- перейди на колонки
- давай звук в колонки
- давай звук на колонки
- переключи звук в колонки
- переключи звук на колонки
action: {type: play_sound, name: stupid}
gaming_mode_on:
phrases:
- включи игровой режим
- перейди в игровой режим
- я хочу поиграть
- переключись на телевизор
action:
type: multi
steps:
- {type: play_sound, name: ok}
- {type: exe, file: "Switch to gaming mode.exe", wait: true}
- {type: play_sound, name: ready}
gaming_mode_off:
phrases:
- рабочий режим
- вернись в рабочий режим
- переключись на компьютер
- обратно на компьютер
- отключи игровой режим
- выйди из игрового режима
- вернись на компьютер
- выход с игрового режима
- рабочее пространство
- вернись в игровой режим
- выключи игровой режим
action:
type: multi
steps:
- {type: play_sound, name: ok}
- {type: exe, file: "Switch back to workspace.exe", wait: true}
- {type: play_sound, name: ready}
switch_to_headphones:
phrases:
- переключи на наушники
- включи наушники
- перейди на наушники
@ -145,13 +190,50 @@ switch_to_headphones:
- давай звук на кракены
- переключи звук в кракены
- переключи звук на кракены
thanks:
- спасибо
- молодец
- респект
- ты супер
- отличная работа
- ты крут
- ты большой молодец
- ты реально крут
- ты афигенный
action:
type: multi
steps:
- {type: play_sound, name: ok}
- {type: exe, file: "Switch to headphones.exe", wait: true, delay_ms: 500}
- {type: play_sound, name: ready}
switch_to_dynamics:
phrases:
- переключи на динамики
- включи динамики
- перейди на динамики
- давай звук в динамики
- давай звук на динамики
- переключи звук в динамики
- переключи звук на динамики
- переключи на колонки
- включи колонки
- перейди на колонки
- давай звук в колонки
- давай звук на колонки
- переключи звук в колонки
- переключи звук на колонки
action:
type: multi
steps:
- {type: play_sound, name: ok}
- {type: exe, file: "Switch to dynamics.exe", wait: true, delay_ms: 500}
- {type: play_sound, name: ready}
'off':
phrases:
- выключись
- вырубись
- завершить работу
- закройся
- отключись
- заверши свою работу
- на сегодня хватит
- выгрузи себя из памяти
- ты мне надоел
- пора спать
action:
type: multi
steps:
- {type: play_sound, name: "off"}
- {type: system, op: exit}

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_BASE_URL = "https://api.groq.com/openai/v1"
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

178
main.py
View file

@ -13,6 +13,7 @@ import openai
from openai import OpenAI
import simpleaudio as sa
import vosk
import webrtcvad
import yaml
from comtypes import CLSCTX_ALL
from fuzzywuzzy import fuzz
@ -54,6 +55,10 @@ wake_rec = vosk.KaldiRecognizer(model, samplerate, wake_grammar)
kaldi_rec = vosk.KaldiRecognizer(model, samplerate)
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():
global message_log
@ -175,8 +180,7 @@ def filter_cmd(raw_voice: str):
def recognize_cmd(cmd: str):
rc = {'cmd': '', 'percent': 0}
for c, v in VA_CMD_LIST.items():
for x in v:
for x in v['phrases']:
vrt = fuzz.ratio(cmd, x)
if vrt > rc['percent']:
rc['cmd'] = c
@ -185,90 +189,59 @@ def recognize_cmd(cmd: str):
return rc
def _set_mute(state: int):
devices = AudioUtilities.GetSpeakers()
interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
volume = cast(interface, POINTER(IAudioEndpointVolume))
volume.SetMute(state, None)
def _shutdown():
try:
recorder.stop()
recorder.delete()
finally:
sys.exit(0)
def run_action(action):
t = action['type']
if t == 'exe':
path = f"{CDIR}\\custom-commands\\{action['file']}"
if action.get('wait'):
subprocess.check_call([path])
else:
subprocess.Popen([path])
if 'delay_ms' in action:
time.sleep(action['delay_ms'] / 1000)
elif t == 'play_sound':
play(action['name'])
elif t == 'system':
op = action['op']
if op == 'volume_mute':
_set_mute(1)
elif op == 'volume_unmute':
_set_mute(0)
elif op == 'exit':
_shutdown()
elif t == 'sleep':
time.sleep(action['ms'] / 1000)
elif t == 'multi':
for step in action['steps']:
run_action(step)
def execute_cmd(cmd: str, voice: str):
if cmd == 'open_browser':
subprocess.Popen([f'{CDIR}\\custom-commands\\Run browser.exe'])
play("ok")
elif cmd == 'open_youtube':
subprocess.Popen([f'{CDIR}\\custom-commands\\Run youtube.exe'])
play("ok")
elif cmd == 'open_google':
subprocess.Popen([f'{CDIR}\\custom-commands\\Run google.exe'])
play("ok")
elif cmd == 'music':
subprocess.Popen([f'{CDIR}\\custom-commands\\Run music.exe'])
play("ok")
elif cmd == 'music_off':
subprocess.Popen([f'{CDIR}\\custom-commands\\Stop music.exe'])
time.sleep(0.2)
play("ok")
elif cmd == 'music_save':
subprocess.Popen([f'{CDIR}\\custom-commands\\Save music.exe'])
time.sleep(0.2)
play("ok")
elif cmd == 'music_next':
subprocess.Popen([f'{CDIR}\\custom-commands\\Next music.exe'])
time.sleep(0.2)
play("ok")
elif cmd == 'music_prev':
subprocess.Popen([f'{CDIR}\\custom-commands\\Prev music.exe'])
time.sleep(0.2)
play("ok")
elif cmd == 'sound_off':
play("ok", True)
devices = AudioUtilities.GetSpeakers()
interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
volume = cast(interface, POINTER(IAudioEndpointVolume))
volume.SetMute(1, None)
elif cmd == 'sound_on':
devices = AudioUtilities.GetSpeakers()
interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
volume = cast(interface, POINTER(IAudioEndpointVolume))
volume.SetMute(0, None)
play("ok")
elif cmd == 'thanks':
play("thanks")
elif cmd == 'stupid':
play("stupid")
elif cmd == 'gaming_mode_on':
play("ok")
subprocess.check_call([f'{CDIR}\\custom-commands\\Switch to gaming mode.exe'])
play("ready")
elif cmd == 'gaming_mode_off':
play("ok")
subprocess.check_call([f'{CDIR}\\custom-commands\\Switch back to workspace.exe'])
play("ready")
elif cmd == 'switch_to_headphones':
play("ok")
subprocess.check_call([f'{CDIR}\\custom-commands\\Switch to headphones.exe'])
time.sleep(0.5)
play("ready")
elif cmd == 'switch_to_dynamics':
play("ok")
subprocess.check_call([f'{CDIR}\\custom-commands\\Switch to dynamics.exe'])
time.sleep(0.5)
play("ready")
elif cmd == 'off':
play("off", True)
exit(0)
spec = VA_CMD_LIST.get(cmd)
if not spec:
return
action = spec['action']
run_action(action)
confirm = spec.get('confirm_sound')
if confirm is None and action['type'] == 'exe':
confirm = 'ok'
if confirm:
play(confirm)
recorder = PvRecorder(device_index=config.MICROPHONE_INDEX, frame_length=512)
@ -301,20 +274,43 @@ while True:
play("greet", True)
print("Yes, sir.")
# reset the command recognizer so stale audio doesn't bleed into it
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()
sp = struct.pack("h" * len(pcm), *pcm)
kaldi_rec.AcceptWaveform(sp)
if kaldi_rec.AcceptWaveform(sp):
if va_respond(json.loads(kaldi_rec.Result())["text"]):
ltc = time.time()
vad_buf += sp
while len(vad_buf) >= VAD_FRAME_BYTES:
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
if finalize or speech_ms > 0:
text = json.loads(kaldi_rec.FinalResult()).get("text", "")
if text:
va_respond(text)
wake_rec.Reset()
except Exception as err:

View file

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