Release v0.2.0 — VAD + YAML plugin commands

This commit is contained in:
Bossiara13 2026-04-22 19:43:57 +03:00
commit f26e2008b4
5 changed files with 374 additions and 244 deletions

View file

@ -49,10 +49,51 @@ python main.py
## Использование ## Использование
1. Скажите **«Джарвис»** (или **«jarvis»**) — ассистент даст звуковой сигнал готовности. 1. Скажите **«Джарвис»** (или **«jarvis»**) — ассистент даст звуковой сигнал готовности.
2. В течение 10 секунд произнесите команду: 2. Произнесите команду — окно записи закрывается само, как только вы замолчите (см. ниже про VAD):
- либо одну из фраз из `commands.yaml` (открыть браузер, выключить звук и т.д.); - либо одну из фраз из `commands.yaml` (открыть браузер, выключить звук и т.д.);
- либо начните со слова **«скажи …»** — остаток фразы уйдёт в Groq, ответ будет зачитан вслух. - либо начните со слова **«скажи …»** — остаток фразы уйдёт в 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. CC BY-NC-SA 4.0 — см. [LICENSE.txt](LICENSE.txt). Авторство оригинала — Abraham Tugalov / Priler. Изменения в этом форке — Bossiara13 (Dmitry Bykov), 2026.

View file

@ -1,157 +1,239 @@
gaming_mode_off:
- рабочий режим
- вернись в рабочий режим
- переключись на компьютер
- обратно на компьютер
- отключи игровой режим
- выйди из игрового режима
- вернись на компьютер
- выход с игрового режима
- рабочее пространство
- вернись в игровой режим
- выключи игровой режим
gaming_mode_on:
- включи игровой режим
- перейди в игровой режим
- я хочу поиграть
- переключись на телевизор
music:
- давай музыку
- музыка
- яндекс музыка
- включи музыку
- открой музыку
- послушаем музыку
- хочу музыку
- врубай музло
- включи что-то послушать
- давай что-то послушаем
music_next:
- следующий трек
- следующая мелодия
- следующая песня
- переключи трек
- переключи музыку
- переключи мелодию
- переключи песню
- следующая музыка
music_off:
- выключи музыку
- отруби музыку
- убери музыку
- отключи музыку
- закрой музыку
- останови музыку
- хватит музыки
music_prev:
- предыдущая музыка
- предыдущий трек
- предыдущая мелодия
- предыдущая песня
- верни трек
- верни мелодию
- верни песню
- верни прошлый трек
- верни прошлую мелодию
- верни прошлую песню
- верни прошлую музыку
- верни то что играло
- давай предыдущий трек
- переключи музыку обратно
- переключи на прошлый трек
- переключи на прошлую мелодию
- переключи на прошлую песню
music_save:
- сохрани трек
- мне нравится трек
- сохрани песню
- мне нравится песня
- добавь трек в избранное
- добавь песню в избранное
- запомни мелодию
- запомни трек
- запомни песню
- добавь мелодию в избранное
- сохрани то что сейчас играет
- добавь то что сейчас играет в избранное
- лайкни трек
- лайкни мелодию
- лайкни песню
'off':
- выключись
- вырубись
- завершить работу
- закройся
- отключись
- заверши свою работу
- на сегодня хватит
- выгрузи себя из памяти
- ты мне надоел
- пора спать
open_browser: open_browser:
- открой браузер phrases:
- запусти браузер - открой браузер
- открой гугл хром - запусти браузер
- гугл хром - открой гугл хром
open_google: - гугл хром
- открой гугл action: {type: exe, file: "Run browser.exe"}
- гугл
- запусти гугл
open_youtube: open_youtube:
- открой ютуб phrases:
- ютуб - открой ютуб
- запусти ютуб - ютуб
- запусти ютуб
action: {type: exe, file: "Run youtube.exe"}
open_google:
phrases:
- открой гугл
- гугл
- запусти гугл
action: {type: exe, file: "Run google.exe"}
music:
phrases:
- давай музыку
- музыка
- яндекс музыка
- включи музыку
- открой музыку
- послушаем музыку
- хочу музыку
- врубай музло
- включи что-то послушать
- давай что-то послушаем
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:
- следующий трек
- следующая мелодия
- следующая песня
- переключи трек
- переключи музыку
- переключи мелодию
- переключи песню
- следующая музыка
action: {type: exe, file: "Next music.exe", delay_ms: 200}
music_prev:
phrases:
- предыдущая музыка
- предыдущий трек
- предыдущая мелодия
- предыдущая песня
- верни трек
- верни мелодию
- верни песню
- верни прошлый трек
- верни прошлую мелодию
- верни прошлую песню
- верни прошлую музыку
- верни то что играло
- давай предыдущий трек
- переключи музыку обратно
- переключи на прошлый трек
- переключи на прошлую мелодию
- переключи на прошлую песню
action: {type: exe, file: "Prev music.exe", delay_ms: 200}
sound_off: sound_off:
- выключи звук phrases:
- беззвучный режим - выключи звук
- режим без звука - беззвучный режим
- отключи звук - режим без звука
- отключи звук
action:
type: multi
steps:
- {type: play_sound, name: ok}
- {type: system, op: volume_mute}
sound_on: sound_on:
- включи звук phrases:
- режим со звуком - включи звук
- верни звук - режим со звуком
stupid: - верни звук
- ты дурак action:
- ты дебил type: multi
- ты глупый steps:
- ты тупой - {type: system, op: volume_unmute}
switch_to_dynamics: - {type: play_sound, name: ok}
- переключи на динамики
- включи динамики
- перейди на динамики
- давай звук в динамики
- давай звук на динамики
- переключи звук в динамики
- переключи звук на динамики
- переключи на колонки
- включи колонки
- перейди на колонки
- давай звук в колонки
- давай звук на колонки
- переключи звук в колонки
- переключи звук на колонки
switch_to_headphones:
- переключи на наушники
- включи наушники
- перейди на наушники
- давай звук в наушники
- давай звук на наушники
- переключи звук в наушники
- переключи звук на наушники
- переключи на кракены
- включи кракены
- перейди на кракены
- давай звук в кракены
- давай звук на кракены
- переключи звук в кракены
- переключи звук на кракены
thanks: thanks:
- спасибо phrases:
- молодец - спасибо
- респект - молодец
- ты супер - респект
- отличная работа - ты супер
- ты крут - отличная работа
- ты большой молодец - ты крут
- ты реально крут - ты большой молодец
- ты афигенный - ты реально крут
- ты афигенный
action: {type: play_sound, name: thanks}
stupid:
phrases:
- ты дурак
- ты дебил
- ты глупый
- ты тупой
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:
- переключи на наушники
- включи наушники
- перейди на наушники
- давай звук в наушники
- давай звук на наушники
- переключи звук в наушники
- переключи звук на наушники
- переключи на кракены
- включи кракены
- перейди на кракены
- давай звук в кракены
- давай звук на кракены
- переключи звук в кракены
- переключи звук на кракены
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_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

178
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
@ -175,8 +180,7 @@ def filter_cmd(raw_voice: str):
def recognize_cmd(cmd: str): def recognize_cmd(cmd: str):
rc = {'cmd': '', 'percent': 0} rc = {'cmd': '', 'percent': 0}
for c, v in VA_CMD_LIST.items(): for c, v in VA_CMD_LIST.items():
for x in v['phrases']:
for x in v:
vrt = fuzz.ratio(cmd, x) vrt = fuzz.ratio(cmd, x)
if vrt > rc['percent']: if vrt > rc['percent']:
rc['cmd'] = c rc['cmd'] = c
@ -185,90 +189,59 @@ def recognize_cmd(cmd: str):
return rc 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): def execute_cmd(cmd: str, voice: str):
if cmd == 'open_browser': spec = VA_CMD_LIST.get(cmd)
subprocess.Popen([f'{CDIR}\\custom-commands\\Run browser.exe']) if not spec:
play("ok") return
action = spec['action']
elif cmd == 'open_youtube': run_action(action)
subprocess.Popen([f'{CDIR}\\custom-commands\\Run youtube.exe']) confirm = spec.get('confirm_sound')
play("ok") if confirm is None and action['type'] == 'exe':
confirm = 'ok'
elif cmd == 'open_google': if confirm:
subprocess.Popen([f'{CDIR}\\custom-commands\\Run google.exe']) play(confirm)
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)
recorder = PvRecorder(device_index=config.MICROPHONE_INDEX, frame_length=512) recorder = PvRecorder(device_index=config.MICROPHONE_INDEX, frame_length=512)
@ -301,20 +274,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