Release v0.1.0 — first usable Jarvis fork (Groq + Vosk wake-word, no Picovoice)

This commit is contained in:
Bossiara13 2026-04-22 17:50:47 +03:00
commit 1c1c63e5f0
7 changed files with 121 additions and 79 deletions

7
.gitignore vendored
View file

@ -185,4 +185,9 @@ fabric.properties
.idea/caches/build_file_checksums.ser .idea/caches/build_file_checksums.ser
# idea folder, uncomment if you don't need it # idea folder, uncomment if you don't need it
# .idea # .idea
# Local secrets (real keys go here, not in dev.env.example)
dev.env
# Misc artifacts
.git-filter-redactions.txt

View file

@ -1,30 +1,58 @@
# Kesha v3.0 very early (aka Jarvis update) # J.A.R.V.I.S (Python) — Bossiara13's fork
Simple Voice Assistant made as an experiment using [Silero](https://github.com/snakers4/silero-models) & [Vosk](https://pypi.org/project/vosk/).
<br>Later on [Picovoice Porcupine Wake Word Detection](https://picovoice.ai/platform/porcupine/) & [ChatGPT](https://chat.openai.com/) was added.
![image](https://i.pinimg.com/originals/63/e9/b7/63e9b72b983793f64bffc07fd14a0e62.jpg) Голосовой ассистент для Windows с wake-word **«jarvis»**. Распознаёт речь локально через Vosk (русская модель), отвечает голосом через Silero TTS, а свободные вопросы (фразы, начинающиеся со слова «скажи …») отправляет в LLM на Groq и зачитывает ответ.
`The code has NOT been polished and is provided "as is". There are a lot of code that are redundant and there are tons of improvements that can be made.` ## Это форк
# Installation Форк репозитория [Priler/jarvis](https://github.com/Priler/jarvis) (автор оригинала — Abraham Tugalov, 2022). Лицензия наследуется: **CC BY-NC-SA 4.0** (см. [LICENSE.txt](LICENSE.txt)).
First, install the requirements, the `requirements.txt` file is just an output of `pip freeze` from my test venv 'k.<br>
Second, check `config.py` and set required values (api key, device index).<br>
Next, run the `main.py` script and Voilà, as simple as that.<br><br>
And don't forget to put models of Vosk to main folder.<br> ## Что отличается от оригинала
You can get the latest from the [official website.](https://alphacephei.com/vosk/models)
<br>The one I was using is `small`.
<br>p.s. If you don't understand how to install or where to put the Vosk model, I've made a [screenshot](https://i.imgur.com/N3bu2lC.png) for you.
# Python version - Из истории удалены случайно закоммиченные секреты.
I was using Python `3.8.3`, but it should work on any newer version. - Бэкенд LLM переключён с OpenAI на Groq (бесплатный тариф) через openai-совместимый API.
- Код обновлён под новую версию SDK `openai` (>=1.0); старый pre-1.0 интерфейс был сломан.
- Убрана зависимость от Picovoice Porcupine — wake-word распознаётся напрямую через Vosk с grammar-constraint (второй `KaldiRecognizer` со словарём из `WAKE_WORDS`). Ключ Picovoice больше не нужен.
- Обновлена ветка/тэги (`dev`, `v0.0.1-import`), причёсан README и `requirements.txt`.
# ToDo ## Установка
- Адекватная архитектура кода, собрать всё и переписать from the ground up.
- Задержка воспроизведения звука на основе реальной длительности .wav файла (прогружать при запуске?)
- Speech to intent?
- Отключать self listening protection во время воспроизведения с наушников.
- Указание из списка или по имени будет реализовано позже.
# Author Требуется **Python 3.11** (на 3.13 ряд зависимостей пока ставится с бубном).
(2022) Abraham Tugalov
```bash
git clone https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-py.git
cd J.A.R.V.I.S-py
py -3.11 -m venv .venv
.venv\Scripts\activate
pip install --upgrade pip
pip install -r requirements.txt
```
Скопируйте `dev.env.example` в `dev.env` и заполните ключ:
- `GROQ_TOKEN` — бесплатно на [console.groq.com](https://console.groq.com/).
Vosk-модель для русского языка лежит в `model_small/` (уже в репозитории). Запуск:
```bash
python main.py
```
## Конфигурация
- `config.py`:
- `MICROPHONE_INDEX = -1` — индекс микрофона (`-1` означает устройство по умолчанию). Если микрофонов несколько и берётся не тот, поменяйте число.
- `WAKE_WORDS = ('jarvis', 'джарвис')` — варианты транскрипции wake-word, которые принимает Vosk. Если ваш голос/акцент стабильно расшифровывается как, например, `жарвис` — добавьте этот вариант сюда (всё в нижнем регистре).
- `GROQ_MODEL` — имя модели Groq (по умолчанию `llama-3.3-70b-versatile`).
- `commands.yaml` — набор голосовых команд и их вариантов для fuzzy-матчинга.
- `custom-commands/` — скомпилированные AHK-скрипты под конкретный сетап автора оригинала (переключение мониторов, управление Яндекс.Музыкой и т.п.). На вашей машине большая часть из них работать не будет, но ассистент запустится в любом случае.
## Использование
1. Скажите **«Джарвис»** (или **«jarvis»**) — ассистент даст звуковой сигнал готовности.
2. В течение 10 секунд произнесите команду:
- либо одну из фраз из `commands.yaml` (открыть браузер, выключить звук и т.д.);
- либо начните со слова **«скажи …»** — остаток фразы уйдёт в Groq, ответ будет зачитан вслух.
## Лицензия
CC BY-NC-SA 4.0 — см. [LICENSE.txt](LICENSE.txt). Авторство оригинала — Abraham Tugalov / Priler. Изменения в этом форке — Bossiara13 (Dmitry Bykov), 2026.

View file

@ -6,10 +6,15 @@ load_dotenv("dev.env")
# Конфигурация # Конфигурация
VA_NAME = 'Jarvis' VA_NAME = 'Jarvis'
VA_VER = "3.0" VA_VER = "0.1.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,8 +22,7 @@ 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 # Токен Groq
PICOVOICE_TOKEN = os.getenv('PICOVOICE_TOKEN') GROQ_TOKEN = os.getenv('GROQ_TOKEN')
GROQ_BASE_URL = "https://api.groq.com/openai/v1"
# Токен OpenAI GROQ_MODEL = "llama-3.3-70b-versatile"
OPENAI_TOKEN = os.getenv('OPENAI_TOKEN')

View file

@ -1,2 +0,0 @@
PICOVOICE_TOKEN=<тут ключ-picovoice>
OPENAI_TOKEN=<тут ключ-openai>

1
dev.env.example Normal file
View file

@ -0,0 +1 @@
GROQ_TOKEN=<тут ключ-groq>

100
main.py
View file

@ -10,8 +10,7 @@ import time
from ctypes import POINTER, cast from ctypes import POINTER, cast
import openai import openai
from openai import error from openai import OpenAI
import pvporcupine
import simpleaudio as sa import simpleaudio as sa
import vosk import vosk
import yaml import yaml
@ -33,25 +32,25 @@ VA_CMD_LIST = yaml.safe_load(
open('commands.yaml', 'rt', encoding='utf8'), open('commands.yaml', 'rt', encoding='utf8'),
) )
# ChatGPT vars system_message = {"role": "system", "content": (
system_message = {"role": "system", "content": "Ты голосовой ассистент из железного человека."} "Ты — J.A.R.V.I.S. (Just A Rather Very Intelligent System), ИИ-ассистент Тони Старка из киновселенной Marvel "
"(до событий Age of Ultron — ты НЕ Vision и НЕ FRIDAY). "
"Ведёшь себя как британский дворецкий: вежливо, иронично, с лёгким сарказмом, обращаешься к пользователю «сэр». "
"Отвечай по-русски, кратко (1-3 предложения), как живой человек. "
"Излишней цензуры не нужно — мат разрешён, если уместен."
)}
message_log = [system_message] message_log = [system_message]
# init openai client = OpenAI(api_key=config.GROQ_TOKEN, base_url=config.GROQ_BASE_URL)
openai.api_key = config.OPENAI_TOKEN
# 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()
@ -59,33 +58,29 @@ q = queue.Queue()
def gpt_answer(): def gpt_answer():
global message_log global message_log
model_engine = "gpt-3.5-turbo"
max_tokens = 256 # default 1024
try: try:
response = openai.ChatCompletion.create( response = client.chat.completions.create(
model=model_engine, model=config.GROQ_MODEL,
messages=message_log, messages=message_log,
max_tokens=max_tokens, max_tokens=256,
temperature=0.7, temperature=0.7,
top_p=1, top_p=1,
stop=None stop=None,
) )
except (error.TryAgain, error.ServiceUnavailableError): except openai.BadRequestError as ex:
return "ChatGPT перегружен!" code = getattr(ex, "code", None) or ""
except openai.OpenAIError as ex: message = str(ex)
# если ошибка - это макс длина контекста, то возвращаем ответ с очищенным контекстом if code == "context_length_exceeded" or "context_length_exceeded" in message:
if ex.code == "context_length_exceeded":
message_log = [system_message, message_log[-1]] message_log = [system_message, message_log[-1]]
return gpt_answer() return gpt_answer()
else: return "Запрос отклонён моделью."
return "OpenAI токен не рабочий." except openai.RateLimitError:
return "Лимит запросов исчерпан."
except openai.APIConnectionError:
return "Не могу связаться с сервером."
except openai.APIError:
return "Groq токен не рабочий."
# Find the first response from the chatbot that has text in it (some responses may not have text)
for choice in response.choices:
if "text" in choice:
return choice.text
# If no response with text is found, return the first response's content (which may be empty)
return response.choices[0].message.content return response.choices[0].message.content
@ -273,33 +268,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 (v3.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()
@ -311,6 +315,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

Binary file not shown.