Compare commits
No commits in common. "main" and "v0.3.0" have entirely different histories.
16 changed files with 25 additions and 61597 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
||||||
*.onnx filter=lfs diff=lfs merge=lfs -text
|
|
||||||
24
README.md
24
README.md
|
|
@ -43,9 +43,7 @@ python main.py
|
||||||
- `MICROPHONE_INDEX = -1` — индекс микрофона (`-1` означает устройство по умолчанию). Если микрофонов несколько и берётся не тот, поменяйте число.
|
- `MICROPHONE_INDEX = -1` — индекс микрофона (`-1` означает устройство по умолчанию). Если микрофонов несколько и берётся не тот, поменяйте число.
|
||||||
- `WAKE_WORDS = ('jarvis', 'джарвис')` — варианты транскрипции wake-word, которые принимает Vosk. Если ваш голос/акцент стабильно расшифровывается как, например, `жарвис` — добавьте этот вариант сюда (всё в нижнем регистре).
|
- `WAKE_WORDS = ('jarvis', 'джарвис')` — варианты транскрипции wake-word, которые принимает Vosk. Если ваш голос/акцент стабильно расшифровывается как, например, `жарвис` — добавьте этот вариант сюда (всё в нижнем регистре).
|
||||||
- `GROQ_MODEL` — имя модели Groq (по умолчанию `llama-3.3-70b-versatile`).
|
- `GROQ_MODEL` — имя модели Groq (по умолчанию `llama-3.3-70b-versatile`).
|
||||||
- `INTENT_SIMILARITY_THRESHOLD = 0.45` — минимальная косинусная близость для срабатывания команды (см. ниже).
|
- `commands.yaml` — набор голосовых команд и их вариантов для fuzzy-матчинга.
|
||||||
- `DENOISE_ENABLED = False` — шумоподавление перед Vosk (см. ниже).
|
|
||||||
- `commands.yaml` — набор голосовых команд и их вариантов; матчится семантически по эмбеддингам.
|
|
||||||
- `custom-commands/` — скомпилированные AHK-скрипты под конкретный сетап автора оригинала (переключение мониторов, управление Яндекс.Музыкой и т.п.). На вашей машине большая часть из них работать не будет, но ассистент запустится в любом случае.
|
- `custom-commands/` — скомпилированные AHK-скрипты под конкретный сетап автора оригинала (переключение мониторов, управление Яндекс.Музыкой и т.п.). На вашей машине большая часть из них работать не будет, но ассистент запустится в любом случае.
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
@ -65,26 +63,6 @@ python main.py
|
||||||
- `COMMAND_MIN_LISTEN_MS` — минимальное время записи, чтобы не закрыться мгновенно (1000 мс).
|
- `COMMAND_MIN_LISTEN_MS` — минимальное время записи, чтобы не закрыться мгновенно (1000 мс).
|
||||||
- `COMMAND_MAX_LISTEN_MS` — жёсткий потолок (15000 мс).
|
- `COMMAND_MAX_LISTEN_MS` — жёсткий потолок (15000 мс).
|
||||||
|
|
||||||
## Семантический матчинг команд
|
|
||||||
|
|
||||||
Раньше распознанная фраза сравнивалась с фразами из `commands.yaml` через `fuzzywuzzy.fuzz.ratio` — это срабатывало только на формальное совпадение букв. Теперь используется sentence-transformer `all-MiniLM-L6-v2` (ONNX, CPU) — фраза кодируется в 384-мерный вектор, считается косинусная близость с фразами всех команд, побеждает самая близкая.
|
|
||||||
|
|
||||||
Это означает, что фраза `«запусти браузер»` сматчится с командой `open_browser`, даже если в YAML записано только `«открой браузер»` — у них близкий смысл.
|
|
||||||
|
|
||||||
- Модель лежит в `models/all-MiniLM-L6-v2/` (87 МБ, хранится через Git LFS — `git lfs pull` после клонирования).
|
|
||||||
- Порог `INTENT_SIMILARITY_THRESHOLD = 0.45` (косинус, диапазон [-1; 1], для близких смыслов обычно 0.7+). Ниже порога — «команда не распознана», сработает `not_found.wav` (или ветка `«скажи …»` для LLM).
|
|
||||||
- Все фразы из `commands.yaml` эмбеддятся один раз при старте; на каждой команде эмбеддится только распознанная пользователем фраза (~1-5 мс на CPU).
|
|
||||||
|
|
||||||
## Шумоподавление перед STT
|
|
||||||
|
|
||||||
Опциональный пре-процессинг аудио командного буфера через `noisereduce` (спектральное гейтирование). Помогает в шумных помещениях (кулер, кондиционер, фоновая музыка). К wake-word фазе не применяется — там важна отзывчивость.
|
|
||||||
|
|
||||||
В `config.py`:
|
|
||||||
|
|
||||||
- `DENOISE_ENABLED = False` — главный тумблер.
|
|
||||||
- `DENOISE_PROP = 0.85` — доля подавления (0..1).
|
|
||||||
- `DENOISE_STATIONARY = False` — `False` для меняющегося шума (рекомендуется), `True` для стабильного фона.
|
|
||||||
|
|
||||||
## Эффекты под AI-ассистента
|
## Эффекты под AI-ассистента
|
||||||
|
|
||||||
Silero звучит естественно, но иногда хочется лёгкой «AI-окраски» в духе J.A.R.V.I.S. — узкую полосу, еле заметную реверберацию, при желании небольшой сдвиг высоты. Это тонкий постпроцессинг, голос и sample rate не меняются.
|
Silero звучит естественно, но иногда хочется лёгкой «AI-окраски» в духе J.A.R.V.I.S. — узкую полосу, еле заметную реверберацию, при желании небольшой сдвиг высоты. Это тонкий постпроцессинг, голос и sample rate не меняются.
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,23 @@ open_browser:
|
||||||
phrases:
|
phrases:
|
||||||
- открой браузер
|
- открой браузер
|
||||||
- запусти браузер
|
- запусти браузер
|
||||||
- открой яндекс
|
- открой гугл хром
|
||||||
- яндекс браузер
|
- гугл хром
|
||||||
action: {type: url, href: "about:blank", browser: "yandex"}
|
action: {type: exe, file: "Run browser.exe"}
|
||||||
|
|
||||||
open_youtube:
|
open_youtube:
|
||||||
phrases:
|
phrases:
|
||||||
- открой ютуб
|
- открой ютуб
|
||||||
- ютуб
|
- ютуб
|
||||||
- запусти ютуб
|
- запусти ютуб
|
||||||
action: {type: url, href: "https://www.youtube.com", browser: "yandex"}
|
action: {type: exe, file: "Run youtube.exe"}
|
||||||
|
|
||||||
open_google:
|
open_google:
|
||||||
phrases:
|
phrases:
|
||||||
- открой гугл
|
- открой гугл
|
||||||
- гугл
|
- гугл
|
||||||
- запусти гугл
|
- запусти гугл
|
||||||
action: {type: url, href: "https://www.google.com", browser: "yandex"}
|
action: {type: exe, file: "Run google.exe"}
|
||||||
|
|
||||||
music:
|
music:
|
||||||
phrases:
|
phrases:
|
||||||
|
|
|
||||||
23
config.py
23
config.py
|
|
@ -6,7 +6,7 @@ load_dotenv("dev.env")
|
||||||
|
|
||||||
# Конфигурация
|
# Конфигурация
|
||||||
VA_NAME = 'Jarvis'
|
VA_NAME = 'Jarvis'
|
||||||
VA_VER = "0.4.1"
|
VA_VER = "0.3.0"
|
||||||
VA_ALIAS = ('джарвис',)
|
VA_ALIAS = ('джарвис',)
|
||||||
VA_TBR = ('скажи', 'покажи', 'ответь', 'произнеси', 'расскажи', 'сколько', 'слушай')
|
VA_TBR = ('скажи', 'покажи', 'ответь', 'произнеси', 'расскажи', 'сколько', 'слушай')
|
||||||
|
|
||||||
|
|
@ -19,12 +19,8 @@ WAKE_WORDS = ('jarvis', 'джарвис')
|
||||||
# -1 это стандартное записывающее устройство
|
# -1 это стандартное записывающее устройство
|
||||||
MICROPHONE_INDEX = -1
|
MICROPHONE_INDEX = -1
|
||||||
|
|
||||||
BROWSER_PATHS = {
|
# Путь к браузеру Google Chrome
|
||||||
'yandex': 'C:/Program Files/Yandex/YandexBrowser/Application/browser.exe',
|
CHROME_PATH = 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe %s'
|
||||||
'chrome': 'C:/Program Files/Google/Chrome/Application/chrome.exe',
|
|
||||||
'firefox': 'C:/Program Files/Mozilla Firefox/firefox.exe',
|
|
||||||
'edge': 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Токен Groq
|
# Токен Groq
|
||||||
GROQ_TOKEN = os.getenv('GROQ_TOKEN')
|
GROQ_TOKEN = os.getenv('GROQ_TOKEN')
|
||||||
|
|
@ -45,16 +41,3 @@ TTS_BANDPASS_HIGH_HZ = 7000
|
||||||
TTS_REVERB_WET = 0.20
|
TTS_REVERB_WET = 0.20
|
||||||
TTS_REVERB_DECAY_MS = 100
|
TTS_REVERB_DECAY_MS = 100
|
||||||
TTS_PITCH_SEMITONES = 0
|
TTS_PITCH_SEMITONES = 0
|
||||||
|
|
||||||
# Семантический матчинг команд через эмбеддинги MiniLM-L6-v2 (ONNX, CPU).
|
|
||||||
# Порог — косинусная близость [0..1]. На вход поступает уже отфильтрованная
|
|
||||||
# фраза (без алиасов и vа_tbr); 0.45 эмпирически отделяет валидные команды
|
|
||||||
# от шума, оставляя запас на разговорные перефразировки.
|
|
||||||
INTENT_SIMILARITY_THRESHOLD = 0.45
|
|
||||||
|
|
||||||
# Шумоподавление аудио перед Vosk на этапе захвата команды (после wake-word).
|
|
||||||
# Спектральное гейтирование через noisereduce; даёт небольшую латентность,
|
|
||||||
# поэтому выключено по умолчанию и применяется только к буферу команды.
|
|
||||||
DENOISE_ENABLED = False
|
|
||||||
DENOISE_PROP = 0.85
|
|
||||||
DENOISE_STATIONARY = False
|
|
||||||
|
|
|
||||||
79
intent.py
79
intent.py
|
|
@ -1,79 +0,0 @@
|
||||||
import os
|
|
||||||
import numpy as np
|
|
||||||
import onnxruntime as ort
|
|
||||||
from tokenizers import Tokenizer
|
|
||||||
|
|
||||||
import config
|
|
||||||
|
|
||||||
|
|
||||||
MODEL_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models", "all-MiniLM-L6-v2")
|
|
||||||
MAX_TOKENS = 128
|
|
||||||
|
|
||||||
|
|
||||||
def _mean_pool(last_hidden: np.ndarray, attention_mask: np.ndarray) -> np.ndarray:
|
|
||||||
mask = attention_mask.astype(np.float32)[..., None]
|
|
||||||
summed = (last_hidden * mask).sum(axis=1)
|
|
||||||
counts = np.clip(mask.sum(axis=1), a_min=1e-9, a_max=None)
|
|
||||||
return summed / counts
|
|
||||||
|
|
||||||
|
|
||||||
def _l2_normalize(x: np.ndarray) -> np.ndarray:
|
|
||||||
norm = np.linalg.norm(x, axis=-1, keepdims=True)
|
|
||||||
return x / np.clip(norm, a_min=1e-12, a_max=None)
|
|
||||||
|
|
||||||
|
|
||||||
class IntentClassifier:
|
|
||||||
def __init__(self, model_dir: str = MODEL_DIR):
|
|
||||||
tok_path = os.path.join(model_dir, "tokenizer.json")
|
|
||||||
onnx_path = os.path.join(model_dir, "model.onnx")
|
|
||||||
self._tokenizer = Tokenizer.from_file(tok_path)
|
|
||||||
self._tokenizer.enable_truncation(max_length=MAX_TOKENS)
|
|
||||||
self._tokenizer.enable_padding(pad_id=0, pad_token="[PAD]")
|
|
||||||
so = ort.SessionOptions()
|
|
||||||
so.intra_op_num_threads = max(1, (os.cpu_count() or 2) // 2)
|
|
||||||
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
|
||||||
self._session = ort.InferenceSession(onnx_path, sess_options=so, providers=["CPUExecutionProvider"])
|
|
||||||
self._input_names = {inp.name for inp in self._session.get_inputs()}
|
|
||||||
self._cache: dict[str, np.ndarray] = {}
|
|
||||||
|
|
||||||
def embed(self, texts: list[str]) -> np.ndarray:
|
|
||||||
if not texts:
|
|
||||||
return np.zeros((0, 384), dtype=np.float32)
|
|
||||||
encs = self._tokenizer.encode_batch(texts)
|
|
||||||
ids = np.array([e.ids for e in encs], dtype=np.int64)
|
|
||||||
mask = np.array([e.attention_mask for e in encs], dtype=np.int64)
|
|
||||||
feeds = {"input_ids": ids, "attention_mask": mask}
|
|
||||||
if "token_type_ids" in self._input_names:
|
|
||||||
feeds["token_type_ids"] = np.zeros_like(ids)
|
|
||||||
outputs = self._session.run(None, feeds)
|
|
||||||
last_hidden = outputs[0]
|
|
||||||
pooled = _mean_pool(last_hidden, mask)
|
|
||||||
return _l2_normalize(pooled).astype(np.float32)
|
|
||||||
|
|
||||||
def prime(self, candidates: dict[str, list[str]]) -> None:
|
|
||||||
self._cache.clear()
|
|
||||||
for cmd_id, phrases in candidates.items():
|
|
||||||
if not phrases:
|
|
||||||
continue
|
|
||||||
self._cache[cmd_id] = self.embed(list(phrases))
|
|
||||||
|
|
||||||
def match(self, utterance: str, candidates: dict[str, list[str]]) -> dict:
|
|
||||||
utterance = (utterance or "").strip()
|
|
||||||
if not utterance:
|
|
||||||
return {"cmd": "", "score": 0.0}
|
|
||||||
for cmd_id, phrases in candidates.items():
|
|
||||||
if cmd_id not in self._cache and phrases:
|
|
||||||
self._cache[cmd_id] = self.embed(list(phrases))
|
|
||||||
query = self.embed([utterance])[0]
|
|
||||||
best_cmd = ""
|
|
||||||
best_score = -1.0
|
|
||||||
for cmd_id, mat in self._cache.items():
|
|
||||||
sims = mat @ query
|
|
||||||
top = float(sims.max()) if sims.size else -1.0
|
|
||||||
if top > best_score:
|
|
||||||
best_score = top
|
|
||||||
best_cmd = cmd_id
|
|
||||||
threshold = getattr(config, "INTENT_SIMILARITY_THRESHOLD", 0.45)
|
|
||||||
if best_score < threshold:
|
|
||||||
return {"cmd": "", "score": max(0.0, best_score)}
|
|
||||||
return {"cmd": best_cmd, "score": max(0.0, best_score)}
|
|
||||||
51
main.py
51
main.py
|
|
@ -10,7 +10,6 @@ import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from ctypes import POINTER, cast
|
from ctypes import POINTER, cast
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import openai
|
import openai
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
import simpleaudio as sa
|
import simpleaudio as sa
|
||||||
|
|
@ -28,7 +27,6 @@ from rich import print
|
||||||
|
|
||||||
import config
|
import config
|
||||||
import tts
|
import tts
|
||||||
from intent import IntentClassifier
|
|
||||||
|
|
||||||
# some consts
|
# some consts
|
||||||
CDIR = os.getcwd()
|
CDIR = os.getcwd()
|
||||||
|
|
@ -62,28 +60,6 @@ VAD_FRAME_MS = 30
|
||||||
VAD_FRAME_BYTES = int(samplerate * VAD_FRAME_MS / 1000) * 2
|
VAD_FRAME_BYTES = int(samplerate * VAD_FRAME_MS / 1000) * 2
|
||||||
vad = webrtcvad.Vad(config.VAD_AGGRESSIVENESS)
|
vad = webrtcvad.Vad(config.VAD_AGGRESSIVENESS)
|
||||||
|
|
||||||
intent_classifier = IntentClassifier()
|
|
||||||
intent_classifier.prime({c: v['phrases'] for c, v in VA_CMD_LIST.items()})
|
|
||||||
|
|
||||||
if config.DENOISE_ENABLED:
|
|
||||||
import noisereduce as nr
|
|
||||||
else:
|
|
||||||
nr = None
|
|
||||||
|
|
||||||
|
|
||||||
def denoise_pcm(raw: bytes) -> bytes:
|
|
||||||
if not raw or nr is None:
|
|
||||||
return raw
|
|
||||||
samples = np.frombuffer(raw, dtype=np.int16).astype(np.float32) / 32768.0
|
|
||||||
reduced = nr.reduce_noise(
|
|
||||||
y=samples,
|
|
||||||
sr=samplerate,
|
|
||||||
prop_decrease=config.DENOISE_PROP,
|
|
||||||
stationary=config.DENOISE_STATIONARY,
|
|
||||||
)
|
|
||||||
clipped = np.clip(reduced * 32768.0, -32768, 32767).astype(np.int16)
|
|
||||||
return clipped.tobytes()
|
|
||||||
|
|
||||||
|
|
||||||
def gpt_answer():
|
def gpt_answer():
|
||||||
global message_log
|
global message_log
|
||||||
|
|
@ -164,10 +140,9 @@ def va_respond(voice: str):
|
||||||
|
|
||||||
print(cmd)
|
print(cmd)
|
||||||
|
|
||||||
min_percent = int(round(config.INTENT_SIMILARITY_THRESHOLD * 100))
|
|
||||||
if len(cmd['cmd'].strip()) <= 0:
|
if len(cmd['cmd'].strip()) <= 0:
|
||||||
return False
|
return False
|
||||||
elif cmd['percent'] < min_percent or cmd['cmd'] not in VA_CMD_LIST.keys():
|
elif cmd['percent'] < 70 or cmd['cmd'] not in VA_CMD_LIST.keys():
|
||||||
# play("not_found")
|
# play("not_found")
|
||||||
# tts.va_speak("Что?")
|
# tts.va_speak("Что?")
|
||||||
if fuzz.ratio(voice.join(voice.split()[:1]).strip(), "скажи") > 75:
|
if fuzz.ratio(voice.join(voice.split()[:1]).strip(), "скажи") > 75:
|
||||||
|
|
@ -204,9 +179,15 @@ def filter_cmd(raw_voice: str):
|
||||||
|
|
||||||
|
|
||||||
def recognize_cmd(cmd: str):
|
def recognize_cmd(cmd: str):
|
||||||
candidates = {c: v['phrases'] for c, v in VA_CMD_LIST.items()}
|
rc = {'cmd': '', 'percent': 0}
|
||||||
res = intent_classifier.match(cmd, candidates)
|
for c, v in VA_CMD_LIST.items():
|
||||||
return {'cmd': res['cmd'], 'percent': int(round(res['score'] * 100))}
|
for x in v['phrases']:
|
||||||
|
vrt = fuzz.ratio(cmd, x)
|
||||||
|
if vrt > rc['percent']:
|
||||||
|
rc['cmd'] = c
|
||||||
|
rc['percent'] = vrt
|
||||||
|
|
||||||
|
return rc
|
||||||
|
|
||||||
|
|
||||||
def _set_mute(state: int):
|
def _set_mute(state: int):
|
||||||
|
|
@ -252,12 +233,6 @@ def run_action(action):
|
||||||
else:
|
else:
|
||||||
subprocess.Popen(action['cmd'], shell=True)
|
subprocess.Popen(action['cmd'], shell=True)
|
||||||
elif t == 'url':
|
elif t == 'url':
|
||||||
browser = action.get('browser')
|
|
||||||
if browser:
|
|
||||||
exe = config.BROWSER_PATHS.get(browser)
|
|
||||||
if exe and os.path.isfile(exe):
|
|
||||||
subprocess.Popen([exe, action['href']])
|
|
||||||
return
|
|
||||||
webbrowser.open(action['href'])
|
webbrowser.open(action['href'])
|
||||||
elif t == 'keys':
|
elif t == 'keys':
|
||||||
import pyautogui
|
import pyautogui
|
||||||
|
|
@ -320,7 +295,6 @@ while True:
|
||||||
silence_ms = 0
|
silence_ms = 0
|
||||||
vad_buf = b""
|
vad_buf = b""
|
||||||
finalize = False
|
finalize = False
|
||||||
cmd_buf = b"" if config.DENOISE_ENABLED else None
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
elapsed_ms = (time.time() - listen_started) * 1000
|
elapsed_ms = (time.time() - listen_started) * 1000
|
||||||
|
|
@ -329,10 +303,7 @@ while True:
|
||||||
|
|
||||||
pcm = recorder.read()
|
pcm = recorder.read()
|
||||||
sp = struct.pack("h" * len(pcm), *pcm)
|
sp = struct.pack("h" * len(pcm), *pcm)
|
||||||
if cmd_buf is None:
|
|
||||||
kaldi_rec.AcceptWaveform(sp)
|
kaldi_rec.AcceptWaveform(sp)
|
||||||
else:
|
|
||||||
cmd_buf += sp
|
|
||||||
|
|
||||||
vad_buf += sp
|
vad_buf += sp
|
||||||
while len(vad_buf) >= VAD_FRAME_BYTES:
|
while len(vad_buf) >= VAD_FRAME_BYTES:
|
||||||
|
|
@ -351,8 +322,6 @@ while True:
|
||||||
break
|
break
|
||||||
|
|
||||||
if finalize or speech_ms > 0:
|
if finalize or speech_ms > 0:
|
||||||
if cmd_buf is not None:
|
|
||||||
kaldi_rec.AcceptWaveform(denoise_pcm(cmd_buf))
|
|
||||||
text = json.loads(kaldi_rec.FinalResult()).get("text", "")
|
text = json.loads(kaldi_rec.FinalResult()).get("text", "")
|
||||||
if text:
|
if text:
|
||||||
va_respond(text)
|
va_respond(text)
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
---
|
|
||||||
license: apache-2.0
|
|
||||||
pipeline_tag: sentence-similarity
|
|
||||||
---
|
|
||||||
|
|
||||||
ONNX port of [sentence-transformers/all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) for text classification and similarity searches.
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
Here's an example of performing inference using the model with [FastEmbed](https://github.com/qdrant/fastembed).
|
|
||||||
|
|
||||||
```py
|
|
||||||
from fastembed import TextEmbedding
|
|
||||||
|
|
||||||
documents = [
|
|
||||||
"You should stay, study and sprint.",
|
|
||||||
"History can only prepare us to be surprised yet again.",
|
|
||||||
]
|
|
||||||
|
|
||||||
model = TextEmbedding(model_name="sentence-transformers/all-MiniLM-L6-v2")
|
|
||||||
embeddings = list(model.embed(documents))
|
|
||||||
|
|
||||||
# [
|
|
||||||
# array([
|
|
||||||
# 0.00611658, 0.00068912, -0.0203846, ..., -0.01751488, -0.01174267,
|
|
||||||
# 0.01463472
|
|
||||||
# ],
|
|
||||||
# dtype=float32),
|
|
||||||
# array([
|
|
||||||
# 0.00173448, -0.00329958, 0.01557874, ..., -0.01473586, 0.0281806,
|
|
||||||
# -0.00448205
|
|
||||||
# ],
|
|
||||||
# dtype=float32)
|
|
||||||
# ]
|
|
||||||
```
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"_name_or_path": "sentence-transformers/all-MiniLM-L6-v2",
|
|
||||||
"architectures": [
|
|
||||||
"BertModel"
|
|
||||||
],
|
|
||||||
"attention_probs_dropout_prob": 0.1,
|
|
||||||
"classifier_dropout": null,
|
|
||||||
"gradient_checkpointing": false,
|
|
||||||
"hidden_act": "gelu",
|
|
||||||
"hidden_dropout_prob": 0.1,
|
|
||||||
"hidden_size": 384,
|
|
||||||
"initializer_range": 0.02,
|
|
||||||
"intermediate_size": 1536,
|
|
||||||
"layer_norm_eps": 1e-12,
|
|
||||||
"max_position_embeddings": 512,
|
|
||||||
"model_type": "bert",
|
|
||||||
"num_attention_heads": 12,
|
|
||||||
"num_hidden_layers": 6,
|
|
||||||
"pad_token_id": 0,
|
|
||||||
"position_embedding_type": "absolute",
|
|
||||||
"transformers_version": "4.36.2",
|
|
||||||
"type_vocab_size": 2,
|
|
||||||
"use_cache": true,
|
|
||||||
"vocab_size": 30522
|
|
||||||
}
|
|
||||||
BIN
models/all-MiniLM-L6-v2/model.onnx
(Stored with Git LFS)
BIN
models/all-MiniLM-L6-v2/model.onnx
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,37 +0,0 @@
|
||||||
{
|
|
||||||
"cls_token": {
|
|
||||||
"content": "[CLS]",
|
|
||||||
"lstrip": false,
|
|
||||||
"normalized": false,
|
|
||||||
"rstrip": false,
|
|
||||||
"single_word": false
|
|
||||||
},
|
|
||||||
"mask_token": {
|
|
||||||
"content": "[MASK]",
|
|
||||||
"lstrip": false,
|
|
||||||
"normalized": false,
|
|
||||||
"rstrip": false,
|
|
||||||
"single_word": false
|
|
||||||
},
|
|
||||||
"pad_token": {
|
|
||||||
"content": "[PAD]",
|
|
||||||
"lstrip": false,
|
|
||||||
"normalized": false,
|
|
||||||
"rstrip": false,
|
|
||||||
"single_word": false
|
|
||||||
},
|
|
||||||
"sep_token": {
|
|
||||||
"content": "[SEP]",
|
|
||||||
"lstrip": false,
|
|
||||||
"normalized": false,
|
|
||||||
"rstrip": false,
|
|
||||||
"single_word": false
|
|
||||||
},
|
|
||||||
"unk_token": {
|
|
||||||
"content": "[UNK]",
|
|
||||||
"lstrip": false,
|
|
||||||
"normalized": false,
|
|
||||||
"rstrip": false,
|
|
||||||
"single_word": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,64 +0,0 @@
|
||||||
{
|
|
||||||
"added_tokens_decoder": {
|
|
||||||
"0": {
|
|
||||||
"content": "[PAD]",
|
|
||||||
"lstrip": false,
|
|
||||||
"normalized": false,
|
|
||||||
"rstrip": false,
|
|
||||||
"single_word": false,
|
|
||||||
"special": true
|
|
||||||
},
|
|
||||||
"100": {
|
|
||||||
"content": "[UNK]",
|
|
||||||
"lstrip": false,
|
|
||||||
"normalized": false,
|
|
||||||
"rstrip": false,
|
|
||||||
"single_word": false,
|
|
||||||
"special": true
|
|
||||||
},
|
|
||||||
"101": {
|
|
||||||
"content": "[CLS]",
|
|
||||||
"lstrip": false,
|
|
||||||
"normalized": false,
|
|
||||||
"rstrip": false,
|
|
||||||
"single_word": false,
|
|
||||||
"special": true
|
|
||||||
},
|
|
||||||
"102": {
|
|
||||||
"content": "[SEP]",
|
|
||||||
"lstrip": false,
|
|
||||||
"normalized": false,
|
|
||||||
"rstrip": false,
|
|
||||||
"single_word": false,
|
|
||||||
"special": true
|
|
||||||
},
|
|
||||||
"103": {
|
|
||||||
"content": "[MASK]",
|
|
||||||
"lstrip": false,
|
|
||||||
"normalized": false,
|
|
||||||
"rstrip": false,
|
|
||||||
"single_word": false,
|
|
||||||
"special": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"clean_up_tokenization_spaces": true,
|
|
||||||
"cls_token": "[CLS]",
|
|
||||||
"do_basic_tokenize": true,
|
|
||||||
"do_lower_case": true,
|
|
||||||
"mask_token": "[MASK]",
|
|
||||||
"max_length": 128,
|
|
||||||
"model_max_length": 512,
|
|
||||||
"never_split": null,
|
|
||||||
"pad_to_multiple_of": null,
|
|
||||||
"pad_token": "[PAD]",
|
|
||||||
"pad_token_type_id": 0,
|
|
||||||
"padding_side": "right",
|
|
||||||
"sep_token": "[SEP]",
|
|
||||||
"stride": 0,
|
|
||||||
"strip_accents": null,
|
|
||||||
"tokenize_chinese_chars": true,
|
|
||||||
"tokenizer_class": "BertTokenizer",
|
|
||||||
"truncation_side": "right",
|
|
||||||
"truncation_strategy": "longest_first",
|
|
||||||
"unk_token": "[UNK]"
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -27,13 +27,6 @@ sounddevice
|
||||||
PyYAML
|
PyYAML
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
|
||||||
# Semantic intent classifier (MiniLM-L6-v2 ONNX)
|
|
||||||
onnxruntime>=1.17
|
|
||||||
tokenizers>=0.15
|
|
||||||
|
|
||||||
# Noise suppression before STT (optional, off by default)
|
|
||||||
noisereduce>=3.0
|
|
||||||
|
|
||||||
# GUI command constructor (tools/command_builder)
|
# GUI command constructor (tools/command_builder)
|
||||||
ruamel.yaml
|
ruamel.yaml
|
||||||
pywebview
|
pywebview
|
||||||
|
|
|
||||||
|
|
@ -65,20 +65,13 @@ class ShellAction:
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
KNOWN_BROWSERS = ("yandex", "chrome", "firefox", "edge")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UrlAction:
|
class UrlAction:
|
||||||
href: str
|
href: str
|
||||||
browser: Optional[str] = None
|
|
||||||
type: str = "url"
|
type: str = "url"
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
out: dict = {"type": "url", "href": self.href}
|
return {"type": "url", "href": self.href}
|
||||||
if self.browser:
|
|
||||||
out["browser"] = self.browser
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -164,10 +157,7 @@ def action_from_dict(data: Any) -> Action:
|
||||||
href = data.get("href")
|
href = data.get("href")
|
||||||
if not href or not isinstance(href, str):
|
if not href or not isinstance(href, str):
|
||||||
raise SchemaError("url action requires non-empty 'href' string")
|
raise SchemaError("url action requires non-empty 'href' string")
|
||||||
browser = data.get("browser")
|
return UrlAction(href=href)
|
||||||
if browser is not None and browser not in KNOWN_BROWSERS:
|
|
||||||
raise SchemaError(f"url.browser must be one of {KNOWN_BROWSERS} or omitted")
|
|
||||||
return UrlAction(href=href, browser=browser)
|
|
||||||
if t == "keys":
|
if t == "keys":
|
||||||
sequence = data.get("sequence")
|
sequence = data.get("sequence")
|
||||||
text = data.get("text")
|
text = data.get("text")
|
||||||
|
|
|
||||||
|
|
@ -191,25 +191,10 @@
|
||||||
<label>URL</label>
|
<label>URL</label>
|
||||||
<input type="text" id="url-href" placeholder="https://example.com">
|
<input type="text" id="url-href" placeholder="https://example.com">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label>Браузер</label>
|
|
||||||
<select id="url-browser">
|
|
||||||
<option value="">по умолчанию (системный)</option>
|
|
||||||
<option value="yandex">Yandex Browser</option>
|
|
||||||
<option value="chrome">Google Chrome</option>
|
|
||||||
<option value="firefox">Mozilla Firefox</option>
|
|
||||||
<option value="edge">Microsoft Edge</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
const inp = f.querySelector("#url-href");
|
const inp = f.querySelector("#url-href");
|
||||||
const sel = f.querySelector("#url-browser");
|
if (state.action) inp.value = state.action.href || "";
|
||||||
if (state.action) {
|
|
||||||
inp.value = state.action.href || "";
|
|
||||||
sel.value = state.action.browser || "";
|
|
||||||
}
|
|
||||||
inp.addEventListener("input", () => updateAction());
|
inp.addEventListener("input", () => updateAction());
|
||||||
sel.addEventListener("change", () => updateAction());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -496,23 +481,8 @@
|
||||||
inp.type = "text";
|
inp.type = "text";
|
||||||
inp.value = step.href || "";
|
inp.value = step.href || "";
|
||||||
inp.placeholder = "https://...";
|
inp.placeholder = "https://...";
|
||||||
const sel = document.createElement("select");
|
inp.addEventListener("input", () => onChange({ type: "url", href: inp.value }));
|
||||||
sel.innerHTML = `
|
|
||||||
<option value="">по умолчанию (системный)</option>
|
|
||||||
<option value="yandex">Yandex Browser</option>
|
|
||||||
<option value="chrome">Google Chrome</option>
|
|
||||||
<option value="firefox">Mozilla Firefox</option>
|
|
||||||
<option value="edge">Microsoft Edge</option>`;
|
|
||||||
sel.value = step.browser || "";
|
|
||||||
const apply = () => {
|
|
||||||
const next = { type: "url", href: inp.value };
|
|
||||||
if (sel.value) next.browser = sel.value;
|
|
||||||
onChange(next);
|
|
||||||
};
|
|
||||||
inp.addEventListener("input", apply);
|
|
||||||
sel.addEventListener("change", apply);
|
|
||||||
body.appendChild(wrapField("URL", inp));
|
body.appendChild(wrapField("URL", inp));
|
||||||
body.appendChild(wrapField("Браузер", sel));
|
|
||||||
} else if (t === "keys") {
|
} else if (t === "keys") {
|
||||||
const inp = document.createElement("input");
|
const inp = document.createElement("input");
|
||||||
inp.type = "text";
|
inp.type = "text";
|
||||||
|
|
@ -599,10 +569,7 @@
|
||||||
if (delay && delay.value) a.delay_ms = parseInt(delay.value, 10);
|
if (delay && delay.value) a.delay_ms = parseInt(delay.value, 10);
|
||||||
state.action = a;
|
state.action = a;
|
||||||
} else if (t === "url") {
|
} else if (t === "url") {
|
||||||
const a = { type: "url", href: (document.getElementById("url-href")||{}).value || "" };
|
state.action = { type: "url", href: (document.getElementById("url-href")||{}).value || "" };
|
||||||
const b = (document.getElementById("url-browser")||{}).value;
|
|
||||||
if (b) a.browser = b;
|
|
||||||
state.action = a;
|
|
||||||
} else if (t === "keys") {
|
} else if (t === "keys") {
|
||||||
const mode = (document.getElementById("keys-mode")||{}).value;
|
const mode = (document.getElementById("keys-mode")||{}).value;
|
||||||
if (mode === "text") {
|
if (mode === "text") {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue