Release v0.1.0 — first usable Jarvis fork (Groq + Vosk wake-word, no Picovoice)
This commit is contained in:
commit
1c1c63e5f0
7 changed files with 121 additions and 79 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
74
README.md
74
README.md
|
|
@ -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.
|
|
||||||
|
|
||||||

|
Голосовой ассистент для 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.
|
||||||
|
|
|
||||||
16
config.py
16
config.py
|
|
@ -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')
|
|
||||||
|
|
|
||||||
2
dev.env
2
dev.env
|
|
@ -1,2 +0,0 @@
|
||||||
PICOVOICE_TOKEN=<тут ключ-picovoice>
|
|
||||||
OPENAI_TOKEN=<тут ключ-openai>
|
|
||||||
1
dev.env.example
Normal file
1
dev.env.example
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
GROQ_TOKEN=<тут ключ-groq>
|
||||||
100
main.py
100
main.py
|
|
@ -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
|
||||||
|
|
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue