diff --git a/README.md b/README.md index 5be765f..7e41728 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/commands.yaml b/commands.yaml index dc191dc..554fea0 100644 --- a/commands.yaml +++ b/commands.yaml @@ -1,157 +1,239 @@ -gaming_mode_off: -- рабочий режим -- вернись в рабочий режим -- переключись на компьютер -- обратно на компьютер -- отключи игровой режим -- выйди из игрового режима -- вернись на компьютер -- выход с игрового режима -- рабочее пространство -- вернись в игровой режим -- выключи игровой режим -gaming_mode_on: -- включи игровой режим -- перейди в игровой режим -- я хочу поиграть -- переключись на телевизор -music: -- давай музыку -- музыка -- яндекс музыка -- включи музыку -- открой музыку -- послушаем музыку -- хочу музыку -- врубай музло -- включи что-то послушать -- давай что-то послушаем -music_next: -- следующий трек -- следующая мелодия -- следующая песня -- переключи трек -- переключи музыку -- переключи мелодию -- переключи песню -- следующая музыка -music_off: -- выключи музыку -- отруби музыку -- убери музыку -- отключи музыку -- закрой музыку -- останови музыку -- хватит музыки -music_prev: -- предыдущая музыка -- предыдущий трек -- предыдущая мелодия -- предыдущая песня -- верни трек -- верни мелодию -- верни песню -- верни прошлый трек -- верни прошлую мелодию -- верни прошлую песню -- верни прошлую музыку -- верни то что играло -- давай предыдущий трек -- переключи музыку обратно -- переключи на прошлый трек -- переключи на прошлую мелодию -- переключи на прошлую песню -music_save: -- сохрани трек -- мне нравится трек -- сохрани песню -- мне нравится песня -- добавь трек в избранное -- добавь песню в избранное -- запомни мелодию -- запомни трек -- запомни песню -- добавь мелодию в избранное -- сохрани то что сейчас играет -- добавь то что сейчас играет в избранное -- лайкни трек -- лайкни мелодию -- лайкни песню -'off': -- выключись -- вырубись -- завершить работу -- закройся -- отключись -- заверши свою работу -- на сегодня хватит -- выгрузи себя из памяти -- ты мне надоел -- пора спать open_browser: -- открой браузер -- запусти браузер -- открой гугл хром -- гугл хром -open_google: -- открой гугл -- гугл -- запусти гугл + 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: + - давай музыку + - музыка + - яндекс музыка + - включи музыку + - открой музыку + - послушаем музыку + - хочу музыку + - врубай музло + - включи что-то послушать + - давай что-то послушаем + 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: -- выключи звук -- беззвучный режим -- режим без звука -- отключи звук + phrases: + - выключи звук + - беззвучный режим + - режим без звука + - отключи звук + action: + type: multi + steps: + - {type: play_sound, name: ok} + - {type: system, op: volume_mute} + sound_on: -- включи звук -- режим со звуком -- верни звук -stupid: -- ты дурак -- ты дебил -- ты глупый -- ты тупой -switch_to_dynamics: -- переключи на динамики -- включи динамики -- перейди на динамики -- давай звук в динамики -- давай звук на динамики -- переключи звук в динамики -- переключи звук на динамики -- переключи на колонки -- включи колонки -- перейди на колонки -- давай звук в колонки -- давай звук на колонки -- переключи звук в колонки -- переключи звук на колонки -switch_to_headphones: -- переключи на наушники -- включи наушники -- перейди на наушники -- давай звук в наушники -- давай звук на наушники -- переключи звук в наушники -- переключи звук на наушники -- переключи на кракены -- включи кракены -- перейди на кракены -- давай звук в кракены -- давай звук на кракены -- переключи звук в кракены -- переключи звук на кракены + phrases: + - включи звук + - режим со звуком + - верни звук + action: + type: multi + steps: + - {type: system, op: volume_unmute} + - {type: play_sound, name: ok} + thanks: -- спасибо -- молодец -- респект -- ты супер -- отличная работа -- ты крут -- ты большой молодец -- ты реально крут -- ты афигенный \ No newline at end of file + 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} diff --git a/config.py b/config.py index e39687a..825444d 100644 --- a/config.py +++ b/config.py @@ -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 diff --git a/main.py b/main.py index 5d30d3b..7ecc082 100644 --- a/main.py +++ b/main.py @@ -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: diff --git a/requirements.txt b/requirements.txt index 90e8ca8..22e2d95 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,9 @@ pvrecorder # Vosk vosk +# Voice activity detection (smart command-end) +webrtcvad-wheels + # Groq / OpenAI-compatible LLM openai>=1.0