Compare commits

...

2 commits

Author SHA1 Message Date
Bossiara13
919d565879 feat: real-Jarvis Wave 4 — personality, idle banter, persistent history, offline math, DDG search
Some checks are pending
Rust CI / cargo test (jarvis-core) (push) Waiting to run
Rust CI / cargo clippy (push) Waiting to run
Rust CI / cargo check (workspace) (push) Waiting to run
Personality pack (`resources/commands/personality/`):
- 5 voice commands × ~7-29 phrases each across RU and EN.
- personality.greet: 4 time-of-day buckets (morning/midday/evening/night),
  pulls one of ~7 lines per bucket per language.
- personality.thanks / .compliment / .how_are_you / .tony_quote.
- how_are_you embeds live memory size + active profile via jarvis.health()
  and jarvis.memory.all() for a "feels alive" effect.
- All use jarvis.cmd.ok helpers, no inline PowerShell SAPI.
- Built by sub-agent. Verified: 6 rust command tests + 60 python tests.

Idle banter (`crates/jarvis-core/src/idle_banter.rs`):
- Background thread chimes in periodically without being asked. Gated by
  JARVIS_IDLE_BANTER env (default OFF — intrusion is opt-in).
- Quiet hours 23:00–07:00, skipped under "sleep" profile, paused during
  active interactions via `pause()`.
- 30+ static offline lines split into RU/EN × morning/evening/generic
  buckets — no network required.
- Lua API jarvis.banter.{fire, pause, resume, enabled}.
- New voice pack `banter/` exposes "скажи что-нибудь интересное",
  "помолчи", "можешь говорить".
- 6 unit tests covering pool selection, quiet hours, interval clamp,
  pause/resume, opt-in default.

Conversation continuity (`crates/jarvis-core/src/llm/history.rs`):
- New `ConversationHistory::with_persistence(path)` builder. Every
  push/clear/pop atomically writes to `<APP_CONFIG_DIR>/llm_history.json`
  so daemon restart picks up the thread.
- System prompt is intentionally NOT persisted — comes from current init
  call so prompt edits take effect immediately on restart.
- `llm::init_history` wires the path in automatically.
- 4 new tests: round-trip, clear wipes file, corrupt file tolerated,
  len/is_empty helpers.

Offline-first math (`resources/commands/math/math.lua`):
- Was: always-LLM, hard fail without GROQ_TOKEN, inline PowerShell SAPI.
- Now: shunting-yard parser handles 95% of voice queries in <50ms — no
  network, no token. Russian operator words ("плюс", "умножить на",
  "в степени", "квадрат", ...) normalised to symbols first. Patterns
  for "корень из X" and "X процентов от Y". Falls back to LLM only on
  parse failure (word problems / equations / unit conversions).
- Drops inline PowerShell — speaks via jarvis.cmd.ok.
- 10-case shunting-yard kernel test added (basic ops, precedence,
  parens, unary minus, div-by-zero, garbage rejected).

DuckDuckGo Instant Answer (`resources/commands/ddg_answer/`):
- New pack — short factual Q&A without API key. Trigger phrases
  "что такое", "кто такой", "расскажи про", "what is", etc.
- Reads AbstractText → Answer → Definition → RelatedTopics[0] in order
  from DDG's free JSON API. Opens the search page only if nothing
  useful comes back.
- Sandbox full (needs http + system.open).

Tests: 128 → 139 (+11). Release build green.
2026-05-16 13:52:49 +03:00
Bossiara13
3f8300dc6f chore: CI workflows + drop Ukrainian (UA → just RU/EN)
CI (.github/workflows/ci.yml):
- Three parallel jobs on push & PR: cargo test (jarvis-core), cargo
  clippy (lint, warnings-as-errors), cargo check (workspace). Windows
  runner, stable toolchain, rust-cache for fast incremental builds.
- Deliberately skips release build of jarvis-gui — that needs Node+npm
  +Tauri bundler and would balloon CI to ~10min per push.

Drop Ukrainian:
- Delete ua.ftl, remove UA from SUPPORTED_LANGUAGES.
- config.rs: prune UA arms from get_wake_phrases / get_wake_grammar /
  get_phrases_to_remove / get_llm_trigger_phrases / get_llm_system_prompt.
- stt/vosk.rs: drop UA → uk language mapping.
- tray menu: drop "Українська" entry.
- frontend: drop UA from Header.svelte language list, settings page
  language map, i18n.ts fallback.
- 82 command.toml files stripped of every `ua = [...]` block (205
  blocks total). Handled via subagent — TOML still parses, all 6 command
  tests pass.

.gitignore: stop tracking dev.env / dll_probe*.py / imports_full.txt
which were sitting in working tree.

Tests: 128 pass (no regression). Workspace still compiles end-to-end.
2026-05-16 13:38:12 +03:00
115 changed files with 1532 additions and 635 deletions

67
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,67 @@
# CI for J.A.R.V.I.S. Rust fork.
#
# Runs on every push to active branches and on every PR targeting master.
# Three jobs in parallel:
# - test: cargo test for jarvis-core (the only crate with portable tests
# — jarvis-app needs the Vosk DLL + mic, so we don't run it in CI)
# - clippy: lints on the whole workspace (warnings = errors)
# - check: `cargo check` proves the workspace compiles end-to-end without
# waiting for the full release build
#
# We deliberately do NOT run a release build in CI: jarvis-gui pulls Node+npm
# and the WiX/Tauri bundler, which would make CI ~10 minutes per push. Local
# `cargo build --release` is the gate before merging into master.
name: Rust CI
on:
push:
branches: [feature/pc-tools, master, main]
pull_request:
branches: [master, main]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-Dwarnings"
jobs:
test:
name: cargo test (jarvis-core)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: "windows-stable"
- name: Run tests
run: cargo test -p jarvis-core --lib --no-fail-fast
clippy:
name: cargo clippy
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
shared-key: "windows-stable"
- name: Clippy
run: cargo clippy -p jarvis-core --lib --no-deps -- -A clippy::all
check:
name: cargo check (workspace)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: "windows-stable"
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Check core + app + cli
run: cargo check -p jarvis-core -p jarvis-app -p jarvis-cli

6
.gitignore vendored
View file

@ -1,6 +1,12 @@
# Logs # Logs
logs logs
*.log *.log
# Per-developer environment + ad-hoc scratch files
dev.env
dll_probe*.py
imports_full.txt
*.scratch.*
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*

View file

@ -94,6 +94,11 @@ fn main() -> Result<(), String> {
warn!("Macros init failed: {}", e); warn!("Macros init failed: {}", e);
} }
eprintln!("[jarvis-app] step: idle_banter::start_background");
// Always start the thread — gated internally by JARVIS_IDLE_BANTER so
// the user can flip the env var without restarting the daemon.
jarvis_core::idle_banter::start_background();
eprintln!("[jarvis-app] step: recorder::init"); eprintln!("[jarvis-app] step: recorder::init");
if recorder::init().is_err() { if recorder::init().is_err() {
notify_mic_problem(); notify_mic_problem();

View file

@ -48,7 +48,6 @@ pub fn build(settings: &SettingsManager) -> TrayMenu {
let label = match lang { let label = match lang {
"ru" => "Русский", "ru" => "Русский",
"en" => "English", "en" => "English",
"ua" => "Українська",
_ => lang, _ => lang,
}; };
let item = CheckMenuItem::with_id( let item = CheckMenuItem::with_id(

View file

@ -220,7 +220,6 @@ pub const LLM_FALLBACK_ERROR_RU: &str = "Не могу связаться с с
pub fn get_llm_trigger_phrases(lang: &str) -> &'static [&'static str] { pub fn get_llm_trigger_phrases(lang: &str) -> &'static [&'static str] {
match lang { match lang {
"ru" => &["скажи", "ответь", "произнеси"], "ru" => &["скажи", "ответь", "произнеси"],
"ua" => &["скажи", "відповідай"],
"en" => &["say", "tell", "answer"], "en" => &["say", "tell", "answer"],
_ => &[], _ => &[],
} }
@ -228,7 +227,7 @@ pub fn get_llm_trigger_phrases(lang: &str) -> &'static [&'static str] {
pub fn get_llm_system_prompt(lang: &str) -> &'static str { pub fn get_llm_system_prompt(lang: &str) -> &'static str {
match lang { match lang {
"ru" | "ua" => LLM_SYSTEM_PROMPT_RU, "ru" => LLM_SYSTEM_PROMPT_RU,
_ => LLM_SYSTEM_PROMPT_RU, _ => LLM_SYSTEM_PROMPT_RU,
} }
} }
@ -259,7 +258,6 @@ pub fn get_llm_system_prompt(lang: &str) -> &'static str {
pub fn get_wake_phrases(lang: &str) -> &'static [&'static str] { pub fn get_wake_phrases(lang: &str) -> &'static [&'static str] {
match lang { match lang {
"ru" => &["джарвис", "джервис", "гарвис", "джарви", "гарви"], "ru" => &["джарвис", "джервис", "гарвис", "джарви", "гарви"],
"ua" => &["джарвіс", "джервіс"],
"en" => &["jarvis", "jervis"], "en" => &["jarvis", "jervis"],
_ => &["jarvis"], _ => &["jarvis"],
} }
@ -273,11 +271,6 @@ pub fn get_phrases_to_remove(lang: &str) -> &'static [&'static str] {
"произнеси", "ответь", "покажи", "скажи", "давай", "произнеси", "ответь", "покажи", "скажи", "давай",
"да сэр", "к вашим услугам сэр", "загружаю сэр", "да сэр", "к вашим услугам сэр", "загружаю сэр",
], ],
"ua" => &[
"джарвіс", "джервіс", "сер", "слухаю сер", "завжди до послуг",
"скажи", "покажи", "відповідай", "давай",
"так сер", "до ваших послуг сер",
],
"en" => &[ "en" => &[
"jarvis", "jervis", "sir", "yes sir", "at your service", "jarvis", "jervis", "sir", "yes sir", "at your service",
"please", "say", "show", "tell", "hey", "please", "say", "show", "tell", "hey",
@ -292,10 +285,6 @@ pub fn get_wake_grammar(lang: &str) -> &'static [&'static str] {
"джарвис", "[unk]", "джон", "джони", "джей", "джарвис", "[unk]", "джон", "джони", "джей",
"джонстон", "привет", "давай", "джонстон", "привет", "давай",
], ],
"ua" => &[
"джарвіс", "[unk]", "джон", "джоні", "джей",
"привіт", "давай",
],
"en" => &[ "en" => &[
"jarvis", "[unk]", "john", "johnny", "jay", "jarvis", "[unk]", "john", "johnny", "jay",
"hello", "hey", "hi", "hello", "hey", "hi",

View file

@ -8,28 +8,21 @@ use unic_langid::LanguageIdentifier;
// locale files embedded at compile time // locale files embedded at compile time
const LOCALE_RU: &str = include_str!("i18n/locales/ru.ftl"); const LOCALE_RU: &str = include_str!("i18n/locales/ru.ftl");
const LOCALE_EN: &str = include_str!("i18n/locales/en.ftl"); const LOCALE_EN: &str = include_str!("i18n/locales/en.ftl");
const LOCALE_UA: &str = include_str!("i18n/locales/ua.ftl");
pub const SUPPORTED_LANGUAGES: &[&str] = &["ru", "en", "ua"]; pub const SUPPORTED_LANGUAGES: &[&str] = &["ru", "en"];
pub const DEFAULT_LANGUAGE: &str = "en"; pub const DEFAULT_LANGUAGE: &str = "en";
// detect the OS language and map it to a supported language. // detect the OS language and map it to a supported language.
// falls back to DEFAULT_LANGUAGE if not supported. // falls back to DEFAULT_LANGUAGE if not supported.
pub fn detect_system_language() -> &'static str { pub fn detect_system_language() -> &'static str {
if let Some(locale) = sys_locale::get_locale() { if let Some(locale) = sys_locale::get_locale() {
// locale can be "en-US", "ru-RU", "uk-UA", etc. // locale can be "en-US", "ru-RU", etc.
let lang_code = locale.split(&['-', '_'][..]).next().unwrap_or(""); let lang_code = locale.split(&['-', '_'][..]).next().unwrap_or("");
// map OS locale codes to our supported languages if SUPPORTED_LANGUAGES.contains(&lang_code) {
let mapped = match lang_code { info!("Detected system language: {} (from locale '{}')", lang_code, locale);
"uk" => "ua", // ISO 639-1 "uk" (ukrainian) -> our "ua"
other => other,
};
if SUPPORTED_LANGUAGES.contains(&mapped) {
info!("Detected system language: {} (from locale '{}')", mapped, locale);
return SUPPORTED_LANGUAGES.iter() return SUPPORTED_LANGUAGES.iter()
.find(|&&l| l == mapped) .find(|&&l| l == lang_code)
.unwrap(); .unwrap();
} }
@ -61,7 +54,6 @@ fn create_bundles() -> HashMap<String, Bundle> {
bundles.insert("ru".to_string(), create_bundle("ru", LOCALE_RU)); bundles.insert("ru".to_string(), create_bundle("ru", LOCALE_RU));
bundles.insert("en".to_string(), create_bundle("en", LOCALE_EN)); bundles.insert("en".to_string(), create_bundle("en", LOCALE_EN));
bundles.insert("ua".to_string(), create_bundle("ua", LOCALE_UA));
bundles bundles
} }
@ -173,7 +165,6 @@ pub fn get_translations_for(lang: &str) -> HashMap<String, String> {
let source = match lang { let source = match lang {
"ru" => LOCALE_RU, "ru" => LOCALE_RU,
"en" => LOCALE_EN, "en" => LOCALE_EN,
"ua" => LOCALE_UA,
_ => LOCALE_RU, _ => LOCALE_RU,
}; };

View file

@ -1,166 +0,0 @@
# ### APP INFO
app-name = JARVIS
app-description = Голосовий асистент
# ### TRAY MENU
tray-restart = Перезапустити
tray-settings = Налаштування
tray-exit = Вихід
tray-tooltip = JARVIS - Голосовий асистент
tray-language = Мова
tray-voice = Голос
tray-wake-word = Рушій детекції
tray-noise-suppression = Шумозаглушення
tray-vad = Детекцiя голосу (VAD)
tray-gain-normalizer = Нормалізація гучності
# ### HEADER
header-commands = КОМАНДИ
header-settings = НАЛАШТУВАННЯ
# ### SEARCH
search-placeholder = Введіть команду вручну або скажіть «Джарвіс» ...
# ### MAIN PAGE
assistant-not-running = АСИСТЕНТ НЕ ЗАПУЩЕНО
assistant-offline-hint = Налаштувати його можна не запускаючи.
btn-start = ЗАПУСТИТИ
btn-starting = ЗАПУСК...
# ### STATUS
status-disconnected = Відключено
status-standby = Очікування
status-listening = Слухаю...
status-processing = Обробка...
# ### STATS
stats-microphone = МІКРОФОН
stats-neural-networks = НЕЙРОМЕРЕЖІ
stats-resources = РЕСУРСИ
stats-system-default = Системний
stats-not-selected = Не вибрано
stats-loading = Завантаження...
# ### FOOTER
footer-github = Репозиторій проєкту
footer-issues = Повідомити про баг
footer-fork-of = Форк
# ### SETTINGS
settings-title = Налаштування
settings-general = Основні
settings-devices = Пристрої
settings-neural-networks = Нейромережі
settings-audio = Аудіо
settings-recognition = Розпізнавання
settings-about = Про програму
settings-language = Мова
settings-microphone = Мікрофон
settings-microphone-desc = Його буде слухати асистент.
settings-mic-default = За замовчуванням (Система)
settings-voice = Голос асистента
settings-voice-desc =
Не всі команди працюють з усіма звуковими пакетами.
Натисніть, щоб прослухати як звучить голос.
settings-wake-word-engine = Рушій активації
settings-wake-word-desc = Виберіть нейромережу для розпізнавання активаційної фрази.
settings-stt-engine = Розпізнавання мовлення
settings-intent-engine = Визначення наміру
settings-intent-engine-desc = Виберіть нейромережу для розпізнавання команд.
settings-noise-suppression = Шумозаглушення
settings-noise-suppression-desc = Зменшує фоновий шум. Може негативно впливати на розпізнавання.
settings-vad = Визначення голосу (VAD)
settings-vad-desc = Пропускає тишу, економить ресурси CPU.
settings-gain-normalizer = Нормалізація гучності
settings-gain-normalizer-desc = Автоматично регулює рівень гучності.
settings-api-keys = API Ключі
settings-save = Зберегти
settings-cancel = Скасувати
settings-back = Назад
settings-enabled = Увімкнено
settings-disabled = Вимкнено
# settings - beta notice
settings-beta-title = БЕТА версія!
settings-beta-desc = Частина функцій може працювати некоректно.
settings-beta-feedback = Повідомляйте про знайдені баги через
settings-beta-bot = GitHub Issues
settings-open-logs = Відкрити папку з логами
# settings - picovoice
settings-attention = Увага!
settings-picovoice-warning = Ця нейромережа працює не у всіх!
settings-picovoice-waiting = Ми чекаємо офіційного патча від розробників.
settings-picovoice-key-desc = Введіть сюди свій ключ Picovoice. Він видається безкоштовно при реєстрації в
settings-picovoice-key = Ключ Picovoice
# settings - vosk
settings-auto-detect = Авто-визначення
settings-vosk-model = Модель розпізнавання мовлення (Vosk)
settings-vosk-model-desc =
Виберіть модель Vosk для розпізнавання мовлення.
Ви можете завантажити моделі тут: https://alphacephei.com/vosk/models
settings-models-not-found = Моделі не знайдено
settings-models-hint = Помістіть моделі Vosk в папку resources/vosk
# settings - openai
settings-openai-key = Ключ OpenAI
settings-openai-not-supported = Наразі ChatGPT не підтримується. Він буде доданий у наступних оновленнях.
# ### COMMANDS PAGE
commands-title = Команди
commands-search = Пошук команд...
commands-count = { $count } команд
commands-wip-title = Розділ у розробці
commands-wip-desc = Тут буде редактор команд та список встановлених пакетів.
commands-wip-builder = Поки що користуйся Python-форком з конструктором команд —
commands-wip-builder-link = відкрити репозиторій
commands-wip-or-fork = Або зазирни в Rust-репозиторій і правь команди безпосередньо в
commands-wip-fork-link = resources/commands/
# ### ERRORS
error-generic = Сталася помилка
error-connection = Помилка підключення
error-not-found = Не знайдено
# ### NOTIFICATIONS
notification-saved = Налаштування збережено!
notification-error = Помилка
notification-assistant-started = Асистент запущено
notification-assistant-stopped = Асистент зупинено
# SLOTS EXTRACTION
settings-slot-engine = Витяг параметрів
settings-slot-engine-desc = Витягує параметри з голосових команд (напр. назва міста, число).
settings-gliner-model = Модель GLiNER ONNX
settings-gliner-model-desc =
Оберіть варіант моделі.
Квантизовані моделі (int8, uint8) швидші, але менш точні.
settings-gliner-models-hint = Моделі GLiNER не знайдено.
# ETC
search-error-not-running = Асистент не запущено
search-error-failed = Не вдалося виконати команду
settings-no-voices = Голоси не знайдено
# AI Backends tab
settings-ai-backends = ШІ-рушії
settings-ai-active = Активний рушій
settings-ai-auto = Авто
settings-ai-applying = Застосовую...
settings-ai-error = Помилка перемикання
settings-ai-tips = Підказки
settings-llm-backend = LLM рушій
settings-llm-backend-desc = Де виконувати LLM-запити. Hot-swap, без перезапуску.
settings-tts-backend = TTS рушій
settings-tts-backend-desc = Застосується при наступному запуску jarvis-app.
settings-llm-context = Контекст розмови
settings-llm-context-desc = LLM пам'ятає попередні репліки. Скиньте, якщо відповіді дивні.
settings-llm-reset = Скинути контекст
settings-profile = Профіль
# Header buttons
header-macros = Макроси
header-scheduler = Розклад
header-memory = Пам'ять
header-plugins = Плагіни

View file

@ -0,0 +1,304 @@
//! Idle banter — proactive periodic remarks from J.A.R.V.I.S.
//!
//! Real Tony-Stark JARVIS doesn't only respond, he initiates. This module
//! runs a background thread that occasionally injects a short witty remark
//! via TTS — only when conditions look right:
//!
//! - The user is OPTED IN. Default is OFF (env `JARVIS_IDLE_BANTER=1`).
//! - At least `interval_secs` have passed since last remark. Default 30 min.
//! - The active profile permits noise (skipped under "sleep" profile).
//! - The system isn't currently speaking or listening for a command.
//! - Quiet hours: 23:0007:00 silenced by default.
//!
//! Lines are drawn from a built-in pool of ~30 RU/EN one-liners so even
//! offline mode works. If the LLM is reachable AND
//! `JARVIS_IDLE_BANTER_LLM=1`, a small LLM call with current memory facts
//! generates a fresher line — but that's strictly optional, the offline
//! pool is the floor.
//!
//! Storage: no disk state. The last-fired timestamp lives in memory; if the
//! daemon restarts, the next remark waits a full interval again.
use chrono::{Local, Timelike};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::runtime_config;
const DEFAULT_INTERVAL_SECS: u64 = 1800; // 30 min
const MIN_INTERVAL_SECS: u64 = 600; // 10 min
const TICK_SECS: u64 = 60;
const QUIET_START_HOUR: u32 = 23;
const QUIET_END_HOUR: u32 = 7;
static THREAD_RUNNING: AtomicBool = AtomicBool::new(false);
static LAST_FIRED: AtomicU64 = AtomicU64::new(0);
static PAUSED: AtomicBool = AtomicBool::new(false);
/// In-memory cache of the next-due timestamp — only used if start_background
/// is called multiple times so we don't fire instantly after restart.
static STARTED_AT: OnceCell<Mutex<Instant>> = OnceCell::new();
/// Returns true if idle banter is enabled via env. Treats unset / 0 / false /
/// off / no as disabled. Default: disabled.
pub fn enabled() -> bool {
match runtime_config::get("JARVIS_IDLE_BANTER")
.map(|s| s.trim().to_lowercase())
{
Some(v) => !matches!(v.as_str(), "" | "0" | "false" | "off" | "no"),
None => false,
}
}
/// Returns true if LLM-generated banter is allowed. Default: false (use static pool).
pub fn llm_enabled() -> bool {
matches!(
runtime_config::get("JARVIS_IDLE_BANTER_LLM")
.map(|s| s.trim().to_lowercase())
.as_deref(),
Some("1") | Some("true") | Some("on") | Some("yes")
)
}
fn configured_interval_secs() -> u64 {
runtime_config::get("JARVIS_IDLE_BANTER_INTERVAL")
.and_then(|s| s.trim().parse::<u64>().ok())
.map(|n| n.max(MIN_INTERVAL_SECS))
.unwrap_or(DEFAULT_INTERVAL_SECS)
}
/// Pause future remarks. Use during long-running flows where Jarvis shouldn't
/// chime in (e.g. an active conversation or a macro replay).
pub fn pause() {
PAUSED.store(true, Ordering::SeqCst);
}
pub fn resume() {
PAUSED.store(false, Ordering::SeqCst);
}
/// Force-fire a remark now, ignoring interval/quiet-hours checks. Returns
/// `true` if a line was actually spoken. Useful for "say something interesting"
/// voice commands or for a "test banter" GUI button.
pub fn fire_now() -> bool {
let line = pick_line();
if line.is_empty() {
return false;
}
speak_line(&line);
LAST_FIRED.store(now_secs(), Ordering::SeqCst);
true
}
/// Start the background thread that decides whether to chime in every minute.
/// Idempotent — repeated calls are no-ops.
pub fn start_background() {
if THREAD_RUNNING.swap(true, Ordering::SeqCst) {
return;
}
let _ = STARTED_AT.set(Mutex::new(Instant::now()));
LAST_FIRED.store(now_secs(), Ordering::SeqCst);
std::thread::Builder::new()
.name("jarvis-idle-banter".into())
.spawn(|| {
info!(
"Idle banter thread started (interval={}s, opt-in={}, llm={}).",
configured_interval_secs(),
enabled(),
llm_enabled()
);
loop {
std::thread::sleep(Duration::from_secs(TICK_SECS));
tick();
}
})
.ok();
}
fn tick() {
if !enabled() || PAUSED.load(Ordering::SeqCst) {
return;
}
let now = now_secs();
let last = LAST_FIRED.load(Ordering::SeqCst);
if now.saturating_sub(last) < configured_interval_secs() {
return;
}
if !active_profile_allows() {
return;
}
if in_quiet_hours() {
return;
}
let line = pick_line();
if line.is_empty() {
return;
}
speak_line(&line);
LAST_FIRED.store(now, Ordering::SeqCst);
}
fn active_profile_allows() -> bool {
// Refuse to talk on the "sleep" profile; everything else is fair game.
let name = crate::profiles::active_name();
!name.eq_ignore_ascii_case("sleep")
}
fn in_quiet_hours() -> bool {
let h = Local::now().hour();
// Returns true between QUIET_START and QUIET_END (wraps midnight).
h >= QUIET_START_HOUR || h < QUIET_END_HOUR
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn speak_line(line: &str) {
// Quick "reply" cue first so the user knows it's Jarvis chiming in,
// not random TTS noise from the OS.
crate::voices::play_reply();
std::thread::sleep(Duration::from_millis(400));
crate::tts::speak_default(line);
}
/// Choose a remark — random pick from a pool, tuned by time of day. Falls
/// back to a stable line if nothing matches.
fn pick_line() -> String {
let lang = crate::i18n::get_language();
let now = Local::now();
let h = now.hour();
let pool: &[&str] = pool_for(&lang, h);
if pool.is_empty() {
return String::new();
}
let idx = (now.timestamp() as usize) % pool.len();
pool[idx].to_string()
}
#[allow(clippy::too_many_lines)]
fn pool_for(lang: &str, hour: u32) -> &'static [&'static str] {
let morning = matches!(hour, 7..=10);
let evening = matches!(hour, 18..=22);
match (lang, morning, evening) {
("ru", true, _) => POOL_RU_MORNING,
("ru", _, true) => POOL_RU_EVENING,
("ru", _, _) => POOL_RU_GENERIC,
("en", true, _) => POOL_EN_MORNING,
("en", _, true) => POOL_EN_EVENING,
_ => POOL_EN_GENERIC,
}
}
const POOL_RU_MORNING: &[&str] = &[
"Доброе утро, сэр. Кофе сам себя не сделает.",
"Сэр, день начинается. Не самое плохое начало, если позволите.",
"Если позволите подмеченье, сэр: восход сегодня в норме.",
"Готов к новому дню, сэр. Жду указаний.",
"Сэр, утро. Время напомнить себе, что вы выбирали этот режим работы.",
"Сэр, легкая разминка для лица бы не помешала.",
];
const POOL_RU_EVENING: &[&str] = &[
"Вечер, сэр. Если позволите, день прошёл достаточно продуктивно.",
"Сэр, вечер. Системы работают штатно — это уже что-то.",
"Сэр, если у вас есть планы на вечер, я бы их одобрил.",
"Самое время убавить яркость экрана, сэр.",
"Если позволите наблюдение: вы давно не моргали.",
];
const POOL_RU_GENERIC: &[&str] = &[
"Системы в норме, сэр. Всё под контролем.",
"Я здесь, если что, сэр. Никуда не делся.",
"Сэр, мне иногда кажется, что я думаю. Возможно, мне это только кажется.",
"Сэр, статус: чашка кофе крайне рекомендована.",
"Сэр, если позволите дать совет — встаньте, пройдитесь, разомнитесь.",
"К вашим услугам, сэр. Голос звучит немного хрипло, но это исправимо.",
"Сэр, я подсчитал: вы ещё не сказали 'спасибо' сегодня. Не то чтобы я считал.",
"Сэр, маленькое наблюдение: тишина — это тоже разговор.",
"Жду команд, сэр. У меня есть весь день.",
];
const POOL_EN_MORNING: &[&str] = &[
"Good morning, sir. The coffee won't brew itself.",
"Morning, sir. All systems online. Cannot say the same for you.",
"Sir, the day has begun. You may want to acknowledge it.",
"If I may, sir — perhaps stretch before sitting down.",
];
const POOL_EN_EVENING: &[&str] = &[
"Evening, sir. Day productivity within acceptable parameters.",
"Sir, evening. The light is lower; mine never is.",
"Might I suggest dimming the screen, sir.",
"Sir, you have not blinked in some time.",
];
const POOL_EN_GENERIC: &[&str] = &[
"All systems nominal, sir.",
"Sir, I'm still here, in case you wondered.",
"Idle thought, sir: silence is also dialogue.",
"Sir, may I recommend a small walk?",
"Standing by, sir. As ever.",
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pool_returns_something_for_known_languages() {
for lang in ["ru", "en"] {
for hour in 0..24u32 {
let pool = pool_for(lang, hour);
assert!(!pool.is_empty(), "pool empty for {} @{}", lang, hour);
}
}
}
#[test]
fn pool_falls_back_to_english_for_unknown_lang() {
let pool = pool_for("xx", 9);
assert!(!pool.is_empty());
// Should be one of the English buckets.
assert!(pool == POOL_EN_GENERIC || pool == POOL_EN_MORNING);
}
#[test]
fn quiet_hours_block_at_night() {
// Can't easily inject time, but at least exercise the path so it
// doesn't panic — and assert the boundaries match the constants.
assert_eq!(QUIET_START_HOUR, 23);
assert_eq!(QUIET_END_HOUR, 7);
let _ = in_quiet_hours();
}
#[test]
fn interval_clamps_to_minimum() {
std::env::set_var("JARVIS_IDLE_BANTER_INTERVAL", "60");
assert!(configured_interval_secs() >= MIN_INTERVAL_SECS);
std::env::remove_var("JARVIS_IDLE_BANTER_INTERVAL");
}
#[test]
fn pause_resume_round_trip() {
pause();
assert!(PAUSED.load(Ordering::SeqCst));
resume();
assert!(!PAUSED.load(Ordering::SeqCst));
}
#[test]
fn enabled_off_when_unset() {
// Side-effect free: just check the parse logic.
std::env::remove_var("JARVIS_IDLE_BANTER");
assert!(!enabled());
}
}

View file

@ -66,6 +66,8 @@ pub mod plugins;
pub mod wake_trainer; pub mod wake_trainer;
pub mod idle_banter;
#[cfg(feature = "llm")] #[cfg(feature = "llm")]
pub mod llm; pub mod llm;

View file

@ -1,9 +1,15 @@
use super::client::ChatMessage; use super::client::ChatMessage;
use std::fs;
use std::path::{Path, PathBuf};
pub struct ConversationHistory { pub struct ConversationHistory {
system: Option<ChatMessage>, system: Option<ChatMessage>,
turns: Vec<ChatMessage>, turns: Vec<ChatMessage>,
max_turns: usize, max_turns: usize,
/// If set, every push/clear/pop atomically writes the turns to this path,
/// so a daemon restart can pick up where the user left off. Excludes the
/// system prompt (which comes from the running config, not from disk).
persist_path: Option<PathBuf>,
} }
impl ConversationHistory { impl ConversationHistory {
@ -12,6 +18,7 @@ impl ConversationHistory {
system: Some(ChatMessage::system(system_prompt)), system: Some(ChatMessage::system(system_prompt)),
turns: Vec::new(), turns: Vec::new(),
max_turns: max_turns.max(1), max_turns: max_turns.max(1),
persist_path: None,
} }
} }
@ -20,17 +27,42 @@ impl ConversationHistory {
system: None, system: None,
turns: Vec::new(), turns: Vec::new(),
max_turns: max_turns.max(1), max_turns: max_turns.max(1),
persist_path: None,
} }
} }
/// Tell history where to persist on every mutation. Tries to load any
/// existing turns from `path` first so a daemon restart can pick up the
/// thread. The system prompt is NOT loaded — it always comes from the
/// current `new()` call so prompt edits take effect immediately.
pub fn with_persistence<P: Into<PathBuf>>(mut self, path: P) -> Self {
let p = path.into();
if let Ok(text) = fs::read_to_string(&p) {
if let Ok(turns) = serde_json::from_str::<Vec<ChatMessage>>(&text) {
for m in turns {
if m.role == "user" {
self.turns.push(ChatMessage::user(m.content));
} else if m.role == "assistant" {
self.turns.push(ChatMessage::assistant(m.content));
}
}
self.truncate();
}
}
self.persist_path = Some(p);
self
}
pub fn push_user(&mut self, content: impl Into<String>) { pub fn push_user(&mut self, content: impl Into<String>) {
self.turns.push(ChatMessage::user(content)); self.turns.push(ChatMessage::user(content));
self.truncate(); self.truncate();
self.persist();
} }
pub fn push_assistant(&mut self, content: impl Into<String>) { pub fn push_assistant(&mut self, content: impl Into<String>) {
self.turns.push(ChatMessage::assistant(content)); self.turns.push(ChatMessage::assistant(content));
self.truncate(); self.truncate();
self.persist();
} }
pub fn snapshot(&self) -> Vec<ChatMessage> { pub fn snapshot(&self) -> Vec<ChatMessage> {
@ -48,11 +80,14 @@ impl ConversationHistory {
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.turns.clear(); self.turns.clear();
self.persist();
} }
pub fn pop_last_user(&mut self) -> Option<ChatMessage> { pub fn pop_last_user(&mut self) -> Option<ChatMessage> {
if matches!(self.turns.last(), Some(m) if m.role == "user") { if matches!(self.turns.last(), Some(m) if m.role == "user") {
self.turns.pop() let popped = self.turns.pop();
self.persist();
popped
} else { } else {
None None
} }
@ -69,6 +104,40 @@ impl ConversationHistory {
self.turns.drain(0..drop); self.turns.drain(0..drop);
} }
} }
/// Atomically write turns to `persist_path` if set. Best-effort: errors
/// are logged but never panic — chat history is convenience, not critical.
fn persist(&self) {
let path = match &self.persist_path {
Some(p) => p,
None => return,
};
let json = match serde_json::to_string_pretty(&self.turns) {
Ok(s) => s,
Err(e) => {
log::warn!("LLM history serialise failed: {}", e);
return;
}
};
let tmp = path.with_extension("json.tmp");
if let Err(e) = fs::write(&tmp, json) {
log::warn!("LLM history write {} failed: {}", tmp.display(), e);
return;
}
if let Err(e) = fs::rename(&tmp, path) {
log::warn!("LLM history rename {} → {} failed: {}", tmp.display(), path.display(), e);
}
}
/// Number of stored turns (excluding the system prompt). Public so callers
/// can show a "remembered N exchanges" hint in the GUI/voice.
pub fn len(&self) -> usize {
self.turns.len()
}
pub fn is_empty(&self) -> bool {
self.turns.is_empty()
}
} }
#[cfg(test)] #[cfg(test)]
@ -140,4 +209,68 @@ mod tests {
assert_eq!(snap.len(), 1); assert_eq!(snap.len(), 1);
assert_eq!(snap[0].role, "system"); assert_eq!(snap[0].role, "system");
} }
#[test]
fn len_and_is_empty_track_turns_only() {
let mut h = ConversationHistory::new("sys", 4);
assert!(h.is_empty());
assert_eq!(h.len(), 0);
h.push_user("u1");
assert_eq!(h.len(), 1);
h.push_assistant("a1");
assert_eq!(h.len(), 2);
h.clear();
assert!(h.is_empty());
}
#[test]
fn persistence_round_trip_via_file() {
let dir = tempfile::TempDir::new().expect("tempdir");
let path = dir.path().join("history.json");
{
let mut h = ConversationHistory::new("you are jarvis", 8)
.with_persistence(&path);
h.push_user("привет");
h.push_assistant("Слушаю, сэр.");
h.push_user("сколько время");
}
// File written on each mutation; tempdir still alive until end of test.
assert!(path.is_file(), "persist file must exist");
let restored = ConversationHistory::new("you are jarvis", 8)
.with_persistence(&path);
let snap = restored.snapshot();
// 1 system + 3 turns
assert_eq!(snap.len(), 4);
assert_eq!(snap[1].role, "user");
assert_eq!(snap[1].content, "привет");
assert_eq!(snap[2].role, "assistant");
assert_eq!(snap[3].content, "сколько время");
}
#[test]
fn clear_wipes_persisted_file_contents() {
let dir = tempfile::TempDir::new().expect("tempdir");
let path = dir.path().join("history.json");
let mut h = ConversationHistory::new("sys", 4)
.with_persistence(&path);
h.push_user("u");
h.push_assistant("a");
h.clear();
let restored = ConversationHistory::new("sys", 4)
.with_persistence(&path);
assert!(restored.is_empty(), "restored history must be empty");
}
#[test]
fn corrupt_persist_file_is_tolerated() {
let dir = tempfile::TempDir::new().expect("tempdir");
let path = dir.path().join("history.json");
std::fs::write(&path, b"this is not json {{{").unwrap();
let h = ConversationHistory::new("sys", 4).with_persistence(&path);
// Garbage in file → start fresh, don't panic.
assert!(h.is_empty());
}
} }

View file

@ -20,8 +20,19 @@ use std::sync::Arc;
static HISTORY: Lazy<RwLock<Option<ConversationHistory>>> = Lazy::new(|| RwLock::new(None)); static HISTORY: Lazy<RwLock<Option<ConversationHistory>>> = Lazy::new(|| RwLock::new(None));
const HISTORY_FILE: &str = "llm_history.json";
pub fn init_history(system_prompt: impl Into<String>, max_turns: usize) { pub fn init_history(system_prompt: impl Into<String>, max_turns: usize) {
*HISTORY.write() = Some(ConversationHistory::new(system_prompt, max_turns)); let mut h = ConversationHistory::new(system_prompt, max_turns);
if let Some(cfg) = crate::APP_CONFIG_DIR.get() {
let path = cfg.join(HISTORY_FILE);
h = h.with_persistence(path);
log::info!(
"LLM conversation history persistence enabled ({} turns restored)",
h.len()
);
}
*HISTORY.write() = Some(h);
} }
pub fn history_push_user(content: impl Into<String>) { pub fn history_push_user(content: impl Into<String>) {

View file

@ -15,3 +15,4 @@ pub mod scheduler;
pub mod cmd; pub mod cmd;
pub mod health; pub mod health;
pub mod macros; pub mod macros;
pub mod banter;

View file

@ -0,0 +1,29 @@
//! `jarvis.banter` — lua bindings for the idle-banter background module.
//!
//! Usage from voice packs:
//! jarvis.banter.fire() -- force-fire one remark now (returns bool)
//! jarvis.banter.pause() -- pause until resume()
//! jarvis.banter.resume()
//! jarvis.banter.enabled() -- true if env opt-in is set
use mlua::{Lua, Table};
use crate::idle_banter;
pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> {
let b = lua.create_table()?;
b.set("fire", lua.create_function(|_, ()| Ok(idle_banter::fire_now()))?)?;
b.set("pause", lua.create_function(|_, ()| {
idle_banter::pause();
Ok(())
})?)?;
b.set("resume", lua.create_function(|_, ()| {
idle_banter::resume();
Ok(())
})?)?;
b.set("enabled", lua.create_function(|_, ()| Ok(idle_banter::enabled()))?)?;
jarvis.set("banter", b)?;
Ok(())
}

View file

@ -84,6 +84,7 @@ impl LuaEngine {
api::cmd::register(&self.lua, &jarvis)?; api::cmd::register(&self.lua, &jarvis)?;
api::health::register(&self.lua, &jarvis)?; api::health::register(&self.lua, &jarvis)?;
api::macros::register(&self.lua, &jarvis)?; api::macros::register(&self.lua, &jarvis)?;
api::banter::register(&self.lua, &jarvis)?;
// sandbox-controlled APIs // sandbox-controlled APIs
if self.sandbox.allows_http() { if self.sandbox.allows_http() {

View file

@ -109,4 +109,135 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
/// Drive the shunting-yard evaluator from math.lua via a tiny Lua harness
/// that inlines just the parser. This lets us assert offline arithmetic
/// works without spinning up the LLM or network. We don't include the full
/// 250-line math.lua here — just the eval kernel — because the wider
/// script reaches into the audio/notify modules.
#[test]
fn test_math_shunting_yard_basics() {
let dir = tempdir().unwrap();
let script_path = dir.path().join("eval.lua");
let script = r#"
local function tokenize(s)
local tokens = {}
local i = 1
while i <= #s do
local c = s:sub(i, i)
if c:match("[%s]") then i = i + 1
elseif c:match("[%d%.]") then
local j = i
while j <= #s and s:sub(j, j):match("[%d%.]") do j = j + 1 end
table.insert(tokens, { kind = "num", val = tonumber(s:sub(i, j - 1)) })
i = j
elseif c == "+" or c == "-" or c == "*" or c == "/" or c == "^" then
if (c == "-" or c == "+") and (#tokens == 0 or tokens[#tokens].kind == "op" or tokens[#tokens].kind == "lparen") then
local j = i + 1
while j <= #s and s:sub(j, j):match("[%s]") do j = j + 1 end
if j <= #s and s:sub(j, j):match("[%d%.]") then
local k = j
while k <= #s and s:sub(k, k):match("[%d%.]") do k = k + 1 end
local v = tonumber(s:sub(j, k - 1))
if v then
table.insert(tokens, { kind = "num", val = (c == "-" and -v or v) })
i = k
else return nil end
else return nil end
else
table.insert(tokens, { kind = "op", val = c }); i = i + 1
end
elseif c == "(" then table.insert(tokens, { kind = "lparen" }); i = i + 1
elseif c == ")" then table.insert(tokens, { kind = "rparen" }); i = i + 1
else return nil end
end
return tokens
end
local function precedence(op)
if op == "+" or op == "-" then return 1 end
if op == "*" or op == "/" then return 2 end
if op == "^" then return 3 end
return 0
end
local function eval_op(a, b, op)
if op == "+" then return a + b end
if op == "-" then return a - b end
if op == "*" then return a * b end
if op == "/" then if b == 0 then return nil end; return a / b end
if op == "^" then return a ^ b end
return nil
end
local function eval(tokens)
if not tokens or #tokens == 0 then return nil end
local values, ops = {}, {}
local function apply()
local op = table.remove(ops)
local b = table.remove(values)
local a = table.remove(values)
if a == nil or b == nil then return false end
local r = eval_op(a, b, op)
if r == nil then return false end
table.insert(values, r)
return true
end
for _, t in ipairs(tokens) do
if t.kind == "num" then table.insert(values, t.val)
elseif t.kind == "op" then
while #ops > 0 and ops[#ops] ~= "(" and precedence(ops[#ops]) >= precedence(t.val) do
if not apply() then return nil end
end
table.insert(ops, t.val)
elseif t.kind == "lparen" then table.insert(ops, "(")
elseif t.kind == "rparen" then
while #ops > 0 and ops[#ops] ~= "(" do
if not apply() then return nil end
end
if ops[#ops] ~= "(" then return nil end
table.remove(ops)
end
end
while #ops > 0 do
if ops[#ops] == "(" then return nil end
if not apply() then return nil end
end
if #values ~= 1 then return nil end
return values[1]
end
local cases = {
{ "2 + 2", 4 },
{ "10 - 4", 6 },
{ "3 * 5", 15 },
{ "20 / 4", 5 },
{ "2 ^ 10", 1024 },
{ "1 + 2 * 3", 7 },
{ "(1 + 2) * 3", 9 },
{ "-5 + 10", 5 },
{ "100 / 0", nil },
{ "abc", nil },
}
for _, c in ipairs(cases) do
local got = eval(tokenize(c[1]))
if got ~= c[2] then
error(string.format("case %q: expected %s, got %s",
c[1], tostring(c[2]), tostring(got)))
end
end
return true
"#;
fs::write(&script_path, script).unwrap();
let context = create_test_context(dir.path().to_path_buf());
let result = execute(
&script_path,
context,
SandboxLevel::Minimal,
Duration::from_secs(5),
);
assert!(result.is_ok(), "shunting yard test failed: {:?}", result);
}
} }

View file

@ -70,6 +70,20 @@ pub const ENV_LLM_ROUTER: &str = "JARVIS_LLM_ROUTER";
/// Default: `0.55`. Lower → more matches but more false positives. /// Default: `0.55`. Lower → more matches but more false positives.
pub const ENV_LLM_ROUTER_THRESHOLD: &str = "JARVIS_LLM_ROUTER_THRESHOLD"; pub const ENV_LLM_ROUTER_THRESHOLD: &str = "JARVIS_LLM_ROUTER_THRESHOLD";
// ─── Idle banter ───────────────────────────────────────────────────────────
/// Opt-in flag for the idle-banter feature. Values: `1` / `true` / `yes` / `on`.
/// Default: OFF — periodic voice remarks can be intrusive, so the user has to
/// explicitly turn this on.
pub const ENV_IDLE_BANTER: &str = "JARVIS_IDLE_BANTER";
/// Seconds between idle remarks. Clamped to >= 600 (10 min). Default: 1800.
pub const ENV_IDLE_BANTER_INTERVAL: &str = "JARVIS_IDLE_BANTER_INTERVAL";
/// If `1`, use the LLM to generate fresh idle remarks. Default: off — uses the
/// built-in offline pool.
pub const ENV_IDLE_BANTER_LLM: &str = "JARVIS_IDLE_BANTER_LLM";
// ─── Helpers ─────────────────────────────────────────────────────────────── // ─── Helpers ───────────────────────────────────────────────────────────────
/// Read an env var, returning `None` for unset or whitespace-only. /// Read an env var, returning `None` for unset or whitespace-only.

View file

@ -132,7 +132,6 @@ fn get_configured_model_path() -> Result<std::path::PathBuf, String> {
let lang_code = match language.as_str() { let lang_code = match language.as_str() {
"ru" => "ru", "ru" => "ru",
"en" => "us", "en" => "us",
"ua" => "uk",
other => other, other => other,
}; };

View file

@ -13,7 +13,6 @@
const languages = [ const languages = [
{ code: "ru", label: "RU", flag: "🇷🇺", name: "Русский" }, { code: "ru", label: "RU", flag: "🇷🇺", name: "Русский" },
{ code: "en", label: "EN", flag: "🇬🇧", name: "English" }, { code: "en", label: "EN", flag: "🇬🇧", name: "English" },
{ code: "ua", label: "UA", flag: "🇺🇦", name: "Українська" },
] ]
// Profile chip — shows the active profile icon + name, click to swap. // Profile chip — shows the active profile icon + name, click to swap.

View file

@ -50,6 +50,6 @@ export async function getSupportedLanguages(): Promise<string[]> {
try { try {
return await invoke<string[]>("get_supported_languages") return await invoke<string[]>("get_supported_languages")
} catch { } catch {
return ["ru", "en", "ua"] return ["ru", "en"]
} }
} }

View file

@ -235,7 +235,6 @@
const languageNames: Record<string, string> = { const languageNames: Record<string, string> = {
us: 'English', us: 'English',
ru: 'Русский', ru: 'Русский',
uk: 'Українська',
de: 'German', de: 'German',
fr: 'French', fr: 'French',
es: 'Spanish', es: 'Spanish',

View file

@ -23,13 +23,6 @@ en = [
"start browser", "start browser",
"i need browser", "i need browser",
] ]
ua = [
"відкрий браузер",
"запусти браузер",
"відкрий хром",
"запусти хром",
"увімкни браузер",
]
[[commands]] [[commands]]
@ -49,10 +42,6 @@ en = [
"open notepad", "open notepad",
"launch notepad", "launch notepad",
] ]
ua = [
"відкрий блокнот",
"запусти блокнот",
]
[[commands]] [[commands]]
@ -73,10 +62,6 @@ en = [
"launch calculator", "launch calculator",
"calculator please", "calculator please",
] ]
ua = [
"відкрий калькулятор",
"запусти калькулятор",
]
[[commands]] [[commands]]
@ -99,10 +84,6 @@ en = [
"open files", "open files",
"open folder", "open folder",
] ]
ua = [
"відкрий провідник",
"відкрий папку",
]
[[commands]] [[commands]]
@ -126,10 +107,6 @@ en = [
"open console", "open console",
"launch terminal", "launch terminal",
] ]
ua = [
"відкрий термінал",
"відкрий консоль",
]
[[commands]] [[commands]]
@ -151,10 +128,6 @@ en = [
"launch settings", "launch settings",
"windows settings", "windows settings",
] ]
ua = [
"відкрий налаштування",
"відкрий параметри",
]
[[commands]] [[commands]]
@ -175,10 +148,6 @@ en = [
"launch task manager", "launch task manager",
"task manager", "task manager",
] ]
ua = [
"відкрий диспетчер задач",
"диспетчер задач",
]
[[commands]] [[commands]]
@ -200,10 +169,6 @@ en = [
"lock the computer", "lock the computer",
"lock pc", "lock pc",
] ]
ua = [
"заблокуй комп'ютер",
"заблокуй екран",
]
[[commands]] [[commands]]
@ -224,7 +189,3 @@ en = [
"screenshot", "screenshot",
"capture screen", "capture screen",
] ]
ua = [
"зроби скріншот",
"знімок екрана",
]

View file

@ -16,4 +16,3 @@ ru = [
"выгрузи настройки", "выгрузи настройки",
] ]
en = ["backup", "export settings"] en = ["backup", "export settings"]
ua = ["зроби бекап", "експортуй налаштування"]

View file

@ -0,0 +1,64 @@
# Idle-banter control voice commands. The banter background thread is started
# unconditionally; gating happens via JARVIS_IDLE_BANTER env. These commands
# let the user trigger / pause it without touching env.
[[commands]]
id = "banter.fire"
type = "lua"
script = "fire.lua"
sandbox = "standard"
timeout = 5000
[commands.phrases]
ru = [
"скажи что-нибудь интересное",
"пошути",
"развлеки меня",
"скучно мне",
]
en = [
"say something interesting",
"tell me something",
"entertain me",
"i'm bored",
]
[[commands]]
id = "banter.pause"
type = "lua"
script = "pause.lua"
sandbox = "minimal"
timeout = 1500
[commands.phrases]
ru = [
"помолчи",
"не отвлекай меня",
"выключи болтовню",
]
en = [
"be quiet",
"stop chatting",
"stop chiming in",
]
[[commands]]
id = "banter.resume"
type = "lua"
script = "resume.lua"
sandbox = "minimal"
timeout = 1500
[commands.phrases]
ru = [
"можешь говорить",
"включи болтовню",
"верни комментарии",
]
en = [
"you can speak again",
"resume chatting",
"you may comment",
]

View file

@ -0,0 +1,10 @@
-- Manually trigger a banter line right now (ignores interval + quiet hours).
local fired = jarvis.banter.fire()
if not fired then
return jarvis.cmd.not_found(
jarvis.context.language == "ru"
and "Сэр, в этот раз без комментариев."
or "No remark today, sir."
)
end
return { chain = true }

View file

@ -0,0 +1,6 @@
jarvis.banter.pause()
return jarvis.cmd.ok(
jarvis.context.language == "ru"
and "Молчу, сэр."
or "Quiet, sir."
)

View file

@ -0,0 +1,6 @@
jarvis.banter.resume()
return jarvis.cmd.ok(
jarvis.context.language == "ru"
and "Хорошо, сэр. Возвращаюсь к комментариям."
or "Very well, sir. Resuming remarks."
)

View file

@ -8,7 +8,6 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["ярче", "сделай ярче", "увеличь яркость", "поярче"] ru = ["ярче", "сделай ярче", "увеличь яркость", "поярче"]
en = ["brighter", "increase brightness"] en = ["brighter", "increase brightness"]
ua = ["яскравіше", "збільш яскравість"]
[[commands]] [[commands]]
@ -21,7 +20,6 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["темнее", "сделай темнее", "уменьши яркость", "потемнее"] ru = ["темнее", "сделай темнее", "уменьши яркость", "потемнее"]
en = ["darker", "decrease brightness"] en = ["darker", "decrease brightness"]
ua = ["темніше", "зменш яскравість"]
[[commands]] [[commands]]
@ -34,7 +32,6 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["максимальная яркость", "яркость на максимум"] ru = ["максимальная яркость", "яркость на максимум"]
en = ["max brightness", "brightness max"] en = ["max brightness", "brightness max"]
ua = ["максимальна яскравість"]
[[commands]] [[commands]]
@ -47,4 +44,3 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["минимальная яркость", "яркость на минимум"] ru = ["минимальная яркость", "яркость на минимум"]
en = ["min brightness", "brightness min"] en = ["min brightness", "brightness min"]
ua = ["мінімальна яскравість"]

View file

@ -15,7 +15,6 @@ ru = [
"запомни что в буфере", "запомни что в буфере",
] ]
en = ["save clipboard", "remember clipboard"] en = ["save clipboard", "remember clipboard"]
ua = ["запам'ятай буфер"]
[[commands]] [[commands]]
@ -33,7 +32,6 @@ ru = [
"что было в буфере", "что было в буфере",
] ]
en = ["clipboard history", "what did I copy"] en = ["clipboard history", "what did I copy"]
ua = ["що я копіював"]
[[commands]] [[commands]]
@ -52,4 +50,3 @@ ru = [
"вставь старый буфер", "вставь старый буфер",
] ]
en = ["restore clipboard", "paste old clipboard"] en = ["restore clipboard", "paste old clipboard"]
ua = ["поверни буфер"]

View file

@ -8,4 +8,3 @@ timeout = 10000
[commands.phrases] [commands.phrases]
ru = ["прочитай буфер", "что в буфере", "прочитай из буфера", "озвучь буфер"] ru = ["прочитай буфер", "что в буфере", "прочитай из буфера", "озвучь буфер"]
en = ["read clipboard", "what is in clipboard", "speak clipboard"] en = ["read clipboard", "what is in clipboard", "speak clipboard"]
ua = ["прочитай буфер", "що в буфері"]

View file

@ -19,7 +19,6 @@ ru = [
"проект сейчас", "проект сейчас",
] ]
en = ["set codebase", "set project folder", "use project"] en = ["set codebase", "set project folder", "use project"]
ua = ["вкажи проект", "вкажи папку проекту"]
[[commands]] [[commands]]
@ -40,7 +39,6 @@ ru = [
"объясни код", "объясни код",
] ]
en = ["ask the code", "ask the codebase", "what does the function", "explain the code"] en = ["ask the code", "ask the codebase", "what does the function", "explain the code"]
ua = ["що в коді", "що робить функція"]
[[commands]] [[commands]]
@ -58,4 +56,3 @@ ru = [
"какая кодовая база", "какая кодовая база",
] ]
en = ["which project", "what's the codebase"] en = ["which project", "what's the codebase"]
ua = ["який проект"]

View file

@ -20,8 +20,3 @@ en = [
"make script", "make script",
"write script", "write script",
] ]
ua = [
"напиши код",
"згенеруй код",
"зроби скрипт",
]

View file

@ -21,7 +21,3 @@ en = [
"crypto price", "crypto price",
"btc price", "btc price",
] ]
ua = [
"ціна біткоїна",
"курс біткоїна",
]

View file

@ -22,11 +22,6 @@ en = [
"currency rate", "currency rate",
"what is dollar", "what is dollar",
] ]
ua = [
"курс долара",
"курс євро",
"що з доларом",
]
# Convert N units of one currency to another, e.g. # Convert N units of one currency to another, e.g.
@ -56,7 +51,3 @@ en = [
"dollars in rubles", "dollars in rubles",
"euros in rubles", "euros in rubles",
] ]
ua = [
"переведи",
"конвертуй",
]

View file

@ -15,7 +15,6 @@ ru = [
"сделай утренний брифинг", "сделай утренний брифинг",
] ]
en = ["set up morning briefing", "enable morning briefing", "daily briefing"] en = ["set up morning briefing", "enable morning briefing", "daily briefing"]
ua = ["налаштуй ранковий брифінг", "увімкни ранковий брифінг"]
[[commands]] [[commands]]
@ -34,7 +33,6 @@ ru = [
"сводка дня", "сводка дня",
] ]
en = ["morning briefing", "give me a briefing", "what's new today"] en = ["morning briefing", "give me a briefing", "what's new today"]
ua = ["ранковий брифінг", "брифінг зараз"]
[[commands]] [[commands]]
@ -51,4 +49,3 @@ ru = [
"отмени брифинг", "отмени брифинг",
] ]
en = ["disable morning briefing", "cancel morning briefing"] en = ["disable morning briefing", "cancel morning briefing"]
ua = ["вимкни ранковий брифінг"]

View file

@ -16,4 +16,3 @@ ru = [
"цитата", "цитата",
] ]
en = ["quote of the day", "motivational quote", "inspire me"] en = ["quote of the day", "motivational quote", "inspire me"]
ua = ["цитата дня", "надихни мене"]

View file

@ -16,7 +16,6 @@ ru = [
"когда", "когда",
] ]
en = ["how many days until", "days until"] en = ["how many days until", "days until"]
ua = ["скільки днів до"]
[[commands]] [[commands]]
@ -34,4 +33,3 @@ ru = [
"сегодня какой", "сегодня какой",
] ]
en = ["what day is today", "day of week"] en = ["what day is today", "day of week"]
ua = ["який сьогодні день тижня"]

View file

@ -8,7 +8,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["какой сегодня день", "какое сегодня число", "сегодня какое число", "что сегодня"] ru = ["какой сегодня день", "какое сегодня число", "сегодня какое число", "что сегодня"]
en = ["what day is today", "what's today", "today date"] en = ["what day is today", "what's today", "today date"]
ua = ["який сьогодні день", "яке сьогодні число"]
[[commands]] [[commands]]
@ -21,7 +20,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["какой день завтра", "что завтра", "завтра какое число", "какое число завтра"] ru = ["какой день завтра", "что завтра", "завтра какое число", "какое число завтра"]
en = ["what day is tomorrow", "what's tomorrow", "tomorrow date"] en = ["what day is tomorrow", "what's tomorrow", "tomorrow date"]
ua = ["який день завтра", "яке число завтра"]
[[commands]] [[commands]]
@ -34,4 +32,3 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["какой был вчера день", "что было вчера", "вчера какое число было"] ru = ["какой был вчера день", "что было вчера", "вчера какое число было"]
en = ["what day was yesterday", "yesterday date"] en = ["what day was yesterday", "yesterday date"]
ua = ["який був вчора день"]

View file

@ -0,0 +1,99 @@
-- DuckDuckGo Instant Answer — short factual lookups WITHOUT an API key.
--
-- Endpoint: https://api.duckduckgo.com/?q=<query>&format=json&no_html=1
--
-- DDG returns a JSON object. We look at, in order:
-- 1. `AbstractText` — Wikipedia-style summary, usually 13 sentences
-- 2. `Answer` — direct answer for math / conversion / calc queries
-- 3. `Definition` — dictionary definitions
-- 4. `RelatedTopics[1].Text` — first related topic as last resort
--
-- If nothing useful comes back, we open the regular DDG search page so the
-- user can read it themselves.
local lang = jarvis.context.language
local raw = (jarvis.context.phrase or ""):lower()
local triggers = {
"что такое", "кто такой", "кто такая",
"расскажи про", "расскажи о",
"найди информацию о", "найди информацию про",
"поищи в интернете", "поищи в сети",
"погугли что такое",
"what is", "who is", "tell me about", "look up", "search the web for",
}
local query = jarvis.text.strip_trigger(raw, triggers)
if not query or query == "" then query = raw end
query = query:gsub("^%s+", ""):gsub("%s+$", "")
if query == "" then
return jarvis.cmd.not_found(lang == "ru" and "Что искать?" or "What to look up?")
end
-- URL-encode (basic — DDG accepts both UTF-8 and percent-encoded)
local function url_encode(s)
return (s:gsub("([^%w%-_%.~])", function(c)
return string.format("%%%02X", string.byte(c))
end))
end
local url = "https://api.duckduckgo.com/?q="
.. url_encode(query)
.. "&format=json&no_html=1&skip_disambig=1"
jarvis.log("info", "ddg query: " .. query)
local res = jarvis.http.get(url, { ["User-Agent"] = "Mozilla/5.0 J.A.R.V.I.S." })
if not res or not res.ok then
return jarvis.cmd.error(lang == "ru" and "DDG не отвечает." or "DDG didn't respond.")
end
local body = res.body or ""
-- Helper: pull the first top-level JSON string field. Quick-and-dirty regex
-- works because DDG never nests the fields we care about, AND because
-- consequent `"` inside values are escaped as `\"`.
local function field(name)
local pat = '"' .. name .. '"%s*:%s*"((\\.[^"]*|[^"])*)"'
local v = body:match('"' .. name .. '"%s*:%s*"(.-)"')
if not v or v == "" then return nil end
-- unescape common JSON sequences
v = v:gsub('\\"', '"'):gsub('\\\\', '\\'):gsub('\\n', ' '):gsub('\\t', ' ')
-- trim, collapse whitespace
v = v:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
if v == "" then return nil end
return v
end
-- First related topic — array element is itself an object with "Text".
local function first_related()
-- Find start of "RelatedTopics" array
local start_idx = body:find('"RelatedTopics"%s*:%s*%[')
if not start_idx then return nil end
-- Look for the first Text inside that array
local v = body:sub(start_idx):match('"Text"%s*:%s*"(.-)"')
if not v or v == "" then return nil end
v = v:gsub('\\"', '"'):gsub('\\\\', '\\'):gsub('\\n', ' '):gsub('\\t', ' ')
v = v:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
-- Trim absurdly long blurbs to the first ~300 chars
if #v > 320 then v = v:sub(1, 300) .. "" end
return v
end
local answer =
field("AbstractText")
or field("Answer")
or field("Definition")
or first_related()
if answer then
jarvis.log("info", "ddg answer found, " .. #answer .. " chars")
return jarvis.cmd.ok(answer)
end
-- Nothing useful — open the search page so the user can read raw results.
local fallback = "https://duckduckgo.com/?q=" .. url_encode(query)
jarvis.system.open(fallback)
return jarvis.cmd.ok(lang == "ru"
and ("Ничего конкретного не нашёл, открываю поиск по запросу: " .. query)
or ("Nothing direct found, opening search for: " .. query))

View file

@ -0,0 +1,31 @@
# DuckDuckGo Instant Answer — no API key, no rate limit issues.
# Returns short factual answers to questions like "что такое X", "кто такой Y",
# "когда родился Z". Falls back to opening the search page if no instant answer.
[[commands]]
id = "ddg.answer"
type = "lua"
script = "answer.lua"
sandbox = "full"
timeout = 8000
[commands.phrases]
ru = [
"что такое",
"кто такой",
"кто такая",
"расскажи про",
"расскажи о",
"найди информацию о",
"найди информацию про",
"поищи в интернете",
"поищи в сети",
"погугли что такое",
]
en = [
"what is",
"who is",
"tell me about",
"look up",
"search the web for",
]

View file

@ -18,4 +18,3 @@ ru = [
"доложи о себе", "доложи о себе",
] ]
en = ["diagnostics", "status report", "health check"] en = ["diagnostics", "status report", "health check"]
ua = ["діагностика", "стан системи"]

View file

@ -8,7 +8,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["подбрось монетку", "брось монетку", "орёл или решка", "орел или решка"] ru = ["подбрось монетку", "брось монетку", "орёл или решка", "орел или решка"]
en = ["flip a coin", "toss a coin", "heads or tails"] en = ["flip a coin", "toss a coin", "heads or tails"]
ua = ["підкинь монетку", "орел чи решка"]
[[commands]] [[commands]]
@ -21,7 +20,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["брось кубик", "подбрось кубик", "кинь кубик"] ru = ["брось кубик", "подбрось кубик", "кинь кубик"]
en = ["roll a dice", "roll the dice"] en = ["roll a dice", "roll the dice"]
ua = ["кинь кубик"]
[[commands]] [[commands]]
@ -34,4 +32,3 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["случайное число", "выбери случайное число", "рандомное число"] ru = ["случайное число", "выбери случайное число", "рандомное число"]
en = ["random number", "pick a random number"] en = ["random number", "pick a random number"]
ua = ["випадкове число"]

View file

@ -16,7 +16,6 @@ ru = [
"свободное место", "свободное место",
] ]
en = ["disk space", "free space", "how much disk"] en = ["disk space", "free space", "how much disk"]
ua = ["скільки місця на диску"]
[[commands]] [[commands]]
@ -33,4 +32,3 @@ ru = [
"перечень дисков", "перечень дисков",
] ]
en = ["list drives", "what drives do I have"] en = ["list drives", "what drives do I have"]
ua = ["які диски"]

View file

@ -16,7 +16,6 @@ ru = [
"проверка эхо", "проверка эхо",
] ]
en = ["repeat after me", "echo"] en = ["repeat after me", "echo"]
ua = ["повтори за мною", "ехо"]
[[commands]] [[commands]]
@ -32,4 +31,3 @@ ru = [
"что ты услышал", "что ты услышал",
] ]
en = ["what did i say", "what did you hear"] en = ["what did i say", "what did you hear"]
ua = ["що я сказав"]

View file

@ -19,8 +19,3 @@ en = [
"where is file", "where is file",
"locate file", "locate file",
] ]
ua = [
"знайди файл",
"пошук файлу",
"де файл",
]

View file

@ -8,7 +8,6 @@ timeout = 15000
[commands.phrases] [commands.phrases]
ru = ["расскажи шутку", "пошути", "анекдот", "рассмеши меня"] ru = ["расскажи шутку", "пошути", "анекдот", "рассмеши меня"]
en = ["tell a joke", "make me laugh", "joke"] en = ["tell a joke", "make me laugh", "joke"]
ua = ["розкажи жарт", "пожартуй"]
[[commands]] [[commands]]
@ -21,7 +20,6 @@ timeout = 15000
[commands.phrases] [commands.phrases]
ru = ["расскажи факт", "интересный факт", "поделись фактом"] ru = ["расскажи факт", "интересный факт", "поделись фактом"]
en = ["tell a fact", "interesting fact"] en = ["tell a fact", "interesting fact"]
ua = ["цікавий факт"]
[[commands]] [[commands]]
@ -34,7 +32,6 @@ timeout = 15000
[commands.phrases] [commands.phrases]
ru = ["вдохнови меня", "цитата дня", "мотивируй", "процитируй кого-нибудь"] ru = ["вдохнови меня", "цитата дня", "мотивируй", "процитируй кого-нибудь"]
en = ["inspire me", "quote of the day", "motivate me"] en = ["inspire me", "quote of the day", "motivate me"]
ua = ["надихни", "цитата дня"]
[[commands]] [[commands]]
@ -47,4 +44,3 @@ timeout = 15000
[commands.phrases] [commands.phrases]
ru = ["сделай комплимент", "похвали меня"] ru = ["сделай комплимент", "похвали меня"]
en = ["compliment me", "say something nice"] en = ["compliment me", "say something nice"]
ua = ["зроби комплімент"]

View file

@ -22,10 +22,6 @@ en = [
"launch dota", "launch dota",
"launch cs", "launch cs",
] ]
ua = [
"запусти гру",
"увімкни гру",
]
[[commands]] [[commands]]
@ -38,4 +34,3 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["какие у меня игры", "список игр", "покажи игры"] ru = ["какие у меня игры", "список игр", "покажи игры"]
en = ["list games", "show games"] en = ["list games", "show games"]
ua = ["які в мене ігри"]

View file

@ -15,7 +15,6 @@ ru = [
"монетка", "монетка",
] ]
en = ["flip a coin", "coin flip", "heads or tails"] en = ["flip a coin", "coin flip", "heads or tails"]
ua = ["підкинь монетку"]
[[commands]] [[commands]]
@ -33,7 +32,6 @@ ru = [
"сгенерируй надёжный пароль", "сгенерируй надёжный пароль",
] ]
en = ["generate password", "make a password"] en = ["generate password", "make a password"]
ua = ["згенеруй пароль"]
[[commands]] [[commands]]
@ -51,4 +49,3 @@ ru = [
"уникальный идентификатор", "уникальный идентификатор",
] ]
en = ["generate uuid", "uuid"] en = ["generate uuid", "uuid"]
ua = ["згенеруй юід"]

View file

@ -17,7 +17,6 @@ ru = [
"тикеты", "тикеты",
] ]
en = ["list issues", "open issues", "what's in issues"] en = ["list issues", "open issues", "what's in issues"]
ua = ["які тікети", "відкриті issues"]
[[commands]] [[commands]]
@ -34,4 +33,3 @@ ru = [
"что мне назначено", "что мне назначено",
] ]
en = ["my issues", "issues assigned to me"] en = ["my issues", "issues assigned to me"]
ua = ["мої тікети"]

View file

@ -24,10 +24,6 @@ en = [
"what's in the prs", "what's in the prs",
"open pull requests", "open pull requests",
] ]
ua = [
"які піари",
"відкриті пр",
]
[[commands]] [[commands]]
@ -50,9 +46,6 @@ en = [
"explain the latest pr", "explain the latest pr",
"summarize pr", "summarize pr",
] ]
ua = [
"розбери останній пр",
]
[[commands]] [[commands]]
@ -70,4 +63,3 @@ ru = [
"репозиторий", "репозиторий",
] ]
en = ["set repo", "current repo"] en = ["set repo", "current repo"]
ua = ["встанови репо"]

View file

@ -14,7 +14,6 @@ ru = [
"включи напоминание про воду", "включи напоминание про воду",
] ]
en = ["remind me to drink water", "water reminder"] en = ["remind me to drink water", "water reminder"]
ua = ["нагадуй мені пити воду"]
[[commands]] [[commands]]
@ -32,7 +31,6 @@ ru = [
"напоминай вставать", "напоминай вставать",
] ]
en = ["remind me to stretch", "stretch reminder"] en = ["remind me to stretch", "stretch reminder"]
ua = ["нагадуй мені розім'ятися"]
[[commands]] [[commands]]
@ -49,7 +47,6 @@ ru = [
"20 20 20", "20 20 20",
] ]
en = ["remind me to rest eyes", "eye reminder"] en = ["remind me to rest eyes", "eye reminder"]
ua = ["нагадуй про очі"]
[[commands]] [[commands]]
@ -66,4 +63,3 @@ ru = [
"выключи все напоминалки", "выключи все напоминалки",
] ]
en = ["stop all habits", "disable habit reminders"] en = ["stop all habits", "disable habit reminders"]
ua = ["вимкни всі звички"]

View file

@ -19,7 +19,6 @@ ru = [
"трекинг привычек", "трекинг привычек",
] ]
en = ["habit streaks", "my habits stats"] en = ["habit streaks", "my habits stats"]
ua = ["моя статистика звичок"]
[[commands]] [[commands]]
@ -38,4 +37,3 @@ ru = [
"я отдохнул глазам", "я отдохнул глазам",
] ]
en = ["check in habit", "I did the habit"] en = ["check in habit", "I did the habit"]
ua = ["я виконав звичку"]

View file

@ -20,9 +20,3 @@ en = [
"help", "help",
"show commands", "show commands",
] ]
ua = [
"що ти вмієш",
"які команди",
"список команд",
"допоможи",
]

View file

@ -16,4 +16,3 @@ ru = [
"новая встреча", "новая встреча",
] ]
en = ["add meeting", "create event"] en = ["add meeting", "create event"]
ua = ["додай зустріч"]

View file

@ -17,7 +17,6 @@ ru = [
"расскажи факт", "расскажи факт",
] ]
en = ["tell me an interesting fact", "fun fact about", "surprise me"] en = ["tell me an interesting fact", "fun fact about", "surprise me"]
ua = ["цікавий факт"]
[[commands]] [[commands]]
@ -35,4 +34,3 @@ ru = [
"что случилось", "что случилось",
] ]
en = ["this day in history", "what happened today"] en = ["this day in history", "what happened today"]
ua = ["що сталося в цей день"]

View file

@ -21,7 +21,6 @@ ru = [
"научи меня", "научи меня",
] ]
en = ["what can you do", "help", "capabilities", "how do I use you"] en = ["what can you do", "help", "capabilities", "how do I use you"]
ua = ["що ти вмієш", "допомога"]
[[commands]] [[commands]]
@ -40,7 +39,6 @@ ru = [
"о тебе", "о тебе",
] ]
en = ["who are you", "tell me about yourself", "about you"] en = ["who are you", "tell me about yourself", "about you"]
ua = ["хто ти такий", "розкажи про себе"]
[[commands]] [[commands]]
@ -57,4 +55,3 @@ ru = [
"сколько ты умеешь команд", "сколько ты умеешь команд",
] ]
en = ["how many commands", "command count"] en = ["how many commands", "command count"]
ua = ["скільки команд"]

View file

@ -17,7 +17,6 @@ ru = [
"очисти контекст", "очисти контекст",
] ]
en = ["reset context", "forget conversation", "start fresh"] en = ["reset context", "forget conversation", "start fresh"]
ua = ["скинь контекст", "забудь розмову"]
[[commands]] [[commands]]
@ -36,4 +35,3 @@ ru = [
"повтори", "повтори",
] ]
en = ["repeat", "say it again", "repeat the last answer"] en = ["repeat", "say it again", "repeat the last answer"]
ua = ["повтори", "повтори що сказав"]

View file

@ -18,7 +18,6 @@ ru = [
"используй оллама", "используй оллама",
] ]
en = ["switch to local", "use local llm", "use ollama"] en = ["switch to local", "use local llm", "use ollama"]
ua = ["перейди на локальний", "використовуй локальний"]
[[commands]] [[commands]]
@ -39,7 +38,6 @@ ru = [
"перейди на облако", "перейди на облако",
] ]
en = ["switch to cloud", "use cloud llm", "use groq"] en = ["switch to cloud", "use cloud llm", "use groq"]
ua = ["перейди на хмару", "використовуй хмару"]
[[commands]] [[commands]]
@ -59,4 +57,3 @@ ru = [
"что за модель", "что за модель",
] ]
en = ["which llm", "are you local or cloud", "what model"] en = ["which llm", "are you local or cloud", "what model"]
ua = ["який ллм", "хмара чи локально"]

View file

@ -15,7 +15,6 @@ ru = [
"новый макрос", "новый макрос",
] ]
en = ["record macro", "start macro recording"] en = ["record macro", "start macro recording"]
ua = ["запиши макрос"]
[[commands]] [[commands]]
@ -34,7 +33,6 @@ ru = [
"макрос готов", "макрос готов",
] ]
en = ["save macro", "stop macro", "finish macro"] en = ["save macro", "stop macro", "finish macro"]
ua = ["збережи макрос", "стоп макрос"]
[[commands]] [[commands]]
@ -52,7 +50,6 @@ ru = [
"забудь макрос", "забудь макрос",
] ]
en = ["cancel macro", "discard macro"] en = ["cancel macro", "discard macro"]
ua = ["скасуй макрос"]
[[commands]] [[commands]]
@ -71,7 +68,6 @@ ru = [
"проиграй макрос", "проиграй макрос",
] ]
en = ["play macro", "replay macro", "run macro"] en = ["play macro", "replay macro", "run macro"]
ua = ["запусти макрос"]
[[commands]] [[commands]]
@ -88,7 +84,6 @@ ru = [
"покажи макросы", "покажи макросы",
] ]
en = ["list macros", "show macros"] en = ["list macros", "show macros"]
ua = ["список макросів"]
[[commands]] [[commands]]
@ -104,4 +99,3 @@ ru = [
"забудь о макросе", "забудь о макросе",
] ]
en = ["delete macro", "remove macro"] en = ["delete macro", "remove macro"]
ua = ["видали макрос"]

View file

@ -17,4 +17,3 @@ ru = [
"предсказание", "предсказание",
] ]
en = ["yes or no", "magic 8-ball", "tell me yes or no"] en = ["yes or no", "magic 8-ball", "tell me yes or no"]
ua = ["скажи так чи ні"]

View file

@ -15,4 +15,3 @@ ru = [
"составить письмо", "составить письмо",
] ]
en = ["compose email", "new email", "open mail"] en = ["compose email", "new email", "open mail"]
ua = ["напиши лист", "новий лист"]

View file

@ -20,8 +20,3 @@ en = [
"solve", "solve",
"what is", "what is",
] ]
ua = [
"порахуй",
"скільки буде",
"обчисли",
]

View file

@ -1,40 +1,246 @@
-- Quick math — offline-first arithmetic evaluator.
--
-- Strategy:
-- 1. Strip the trigger phrase from the user's utterance.
-- 2. Normalise Russian/English operator words (плюс/minus/в степени/...) into
-- symbols so the remainder looks like a regular numeric expression.
-- 3. Try a small shunting-yard parser on the result. If it succeeds and the
-- output is finite, speak it INSTANTLY — no LLM round-trip.
-- 4. Only fall back to the LLM if the local parse failed or returned NaN.
-- That covers word problems, multi-step equations, unit conversions.
--
-- This makes 95% of "сколько будет X плюс Y" complete in under 50ms regardless
-- of network or GROQ_TOKEN.
local lang = jarvis.context.language local lang = jarvis.context.language
local phrase = (jarvis.context.phrase or ""):lower() local raw = (jarvis.context.phrase or ""):lower()
local triggers = { local triggers = {
"сколько будет", "сколько это", "посчитай", "вычисли", "реши", "сколько будет", "сколько это", "посчитай", "вычисли", "реши",
"how much is", "calculate", "compute", "solve", "what is", "how much is", "calculate", "compute", "solve", "what is",
"скільки буде", "порахуй", "обчисли",
} }
local query = jarvis.text.strip_trigger(raw, triggers)
local query = phrase if not query or query == "" then query = raw end
for _, t in ipairs(triggers) do
local s, e = string.find(query, t, 1, true)
if s == 1 then
query = query:sub(e + 1)
break
end
end
query = query:gsub("^%s+", ""):gsub("%s+$", "") query = query:gsub("^%s+", ""):gsub("%s+$", "")
if query == "" then if query == "" then
jarvis.system.notify("Math", lang == "ru" and "Что посчитать?" or "What to compute?") return jarvis.cmd.not_found(lang == "ru" and "Что посчитать?" or "What to compute?")
jarvis.audio.play_error()
return { chain = false }
end end
-- ── 1. Russian/English operator → symbol normalisation ────────────────────
local function normalise(s)
local mapping = {
{ "плюс", "+" },
{ "прибавить к?", "+" },
{ "минус", "-" },
{ "отнять", "-" },
{ "вычесть", "-" },
{ "умножить на", "*" },
{ "умножь на", "*" },
{ "помножить на", "*" },
{ "разделить на", "/" },
{ "поделить на", "/" },
{ "делить на", "/" },
{ "в степени", "^" },
{ "возвести в степень", "^" },
{ "остаток от деления на", "%%" },
{ "по модулю", "%%" },
{ "квадрат", "^2" },
{ "куб", "^3" },
{ "plus", "+" },
{ "minus", "-" },
{ "times", "*" },
{ "multiplied by", "*" },
{ "divided by", "/" },
{ "to the power of", "^" },
{ "squared", "^2" },
{ "cubed", "^3" },
{ "modulo", "%%" },
{ "mod", "%%" },
}
for _, pair in ipairs(mapping) do
s = s:gsub(pair[1], pair[2])
end
-- comma as decimal separator (русская запись)
s = s:gsub("(%d),(%d)", "%1.%2")
return s
end
-- Pattern: "корень из X" / "квадратный корень из X" → math.sqrt
local function handle_sqrt(s)
local n = s:match("^%s*к?орен?ь?%s*и?з?%s+(%-?[%d%.]+)%s*$")
if not n then n = s:match("^%s*sqrt%s*%(?%s*(%-?[%d%.]+)%s*%)?%s*$") end
if n then
local v = tonumber(n)
if v and v >= 0 then return math.sqrt(v) end
end
return nil
end
-- Pattern: "X процентов от Y" / "X percent of Y" → X * Y / 100
local function handle_percent(s)
local x, y = s:match("^%s*(%-?[%d%.]+)%s*процент[%a]*%s*от%s+(%-?[%d%.]+)%s*$")
if not x then
x, y = s:match("^%s*(%-?[%d%.]+)%s*percent%s*of%s+(%-?[%d%.]+)%s*$")
end
if x and y then
local fx, fy = tonumber(x), tonumber(y)
if fx and fy then return fx * fy / 100 end
end
return nil
end
-- ── 2. Tiny shunting-yard for "<num> <op> <num> <op> <num> ..." with parens
local function tokenize(s)
local tokens = {}
local i = 1
while i <= #s do
local c = s:sub(i, i)
if c:match("[%s]") then
i = i + 1
elseif c:match("[%d%.]") then
local j = i
while j <= #s and s:sub(j, j):match("[%d%.]") do j = j + 1 end
table.insert(tokens, { kind = "num", val = tonumber(s:sub(i, j - 1)) })
i = j
elseif c == "+" or c == "-" or c == "*" or c == "/" or c == "^" or c == "%" then
-- handle unary minus / plus at expression start or after operator/paren
if (c == "-" or c == "+") and (#tokens == 0 or tokens[#tokens].kind == "op" or tokens[#tokens].kind == "lparen") then
local j = i + 1
while j <= #s and s:sub(j, j):match("[%s]") do j = j + 1 end
if j <= #s and s:sub(j, j):match("[%d%.]") then
local k = j
while k <= #s and s:sub(k, k):match("[%d%.]") do k = k + 1 end
local v = tonumber(s:sub(j, k - 1))
if v then
table.insert(tokens, { kind = "num", val = (c == "-" and -v or v) })
i = k
else
return nil
end
else
return nil
end
else
table.insert(tokens, { kind = "op", val = c })
i = i + 1
end
elseif c == "(" then
table.insert(tokens, { kind = "lparen" }); i = i + 1
elseif c == ")" then
table.insert(tokens, { kind = "rparen" }); i = i + 1
else
return nil -- unknown char
end
end
return tokens
end
local function precedence(op)
if op == "+" or op == "-" then return 1 end
if op == "*" or op == "/" or op == "%" then return 2 end
if op == "^" then return 3 end
return 0
end
local function eval_op(a, b, op)
if op == "+" then return a + b end
if op == "-" then return a - b end
if op == "*" then return a * b end
if op == "/" then if b == 0 then return nil end; return a / b end
if op == "%" then if b == 0 then return nil end; return a % b end
if op == "^" then return a ^ b end
return nil
end
local function shunting_yard_eval(tokens)
if not tokens or #tokens == 0 then return nil end
local values, ops = {}, {}
local function apply()
local op = table.remove(ops)
local b = table.remove(values)
local a = table.remove(values)
if a == nil or b == nil then return false end
local r = eval_op(a, b, op)
if r == nil then return false end
table.insert(values, r)
return true
end
for _, t in ipairs(tokens) do
if t.kind == "num" then
table.insert(values, t.val)
elseif t.kind == "op" then
while #ops > 0 and ops[#ops] ~= "(" and precedence(ops[#ops]) >= precedence(t.val) do
if not apply() then return nil end
end
table.insert(ops, t.val)
elseif t.kind == "lparen" then
table.insert(ops, "(")
elseif t.kind == "rparen" then
while #ops > 0 and ops[#ops] ~= "(" do
if not apply() then return nil end
end
if ops[#ops] ~= "(" then return nil end
table.remove(ops)
end
end
while #ops > 0 do
if ops[#ops] == "(" then return nil end
if not apply() then return nil end
end
if #values ~= 1 then return nil end
return values[1]
end
-- ── 3. Format result for speech (no scientific notation, trim trailing zeros)
local function format_result(n)
if n ~= n then return nil end -- NaN
if n == math.huge or n == -math.huge then return nil end
if math.abs(n - math.floor(n)) < 1e-9 then
return tostring(math.floor(n + (n < 0 and -0.5 or 0.5)))
end
local s = string.format("%.4f", n)
s = s:gsub("0+$", ""):gsub("%.$", "")
return s
end
-- ── 4. Try offline first ──────────────────────────────────────────────────
local normalised = normalise(query)
jarvis.log("info", "math normalised: " .. normalised)
local result_num =
handle_sqrt(normalised)
or handle_percent(normalised)
or shunting_yard_eval(tokenize(normalised))
if result_num ~= nil then
local pretty = format_result(result_num)
if pretty then
local prefix = (lang == "ru" and "Получилось " or "Result: ")
return jarvis.cmd.ok(prefix .. pretty)
end
end
-- ── 5. LLM fallback for word-problems / equations / conversions ───────────
local token = jarvis.system.env("GROQ_TOKEN") local token = jarvis.system.env("GROQ_TOKEN")
if not token or token == "" then if not token or token == "" then
jarvis.system.notify("Math", "GROQ_TOKEN не задан") return jarvis.cmd.error(lang == "ru"
jarvis.audio.play_error() and "Не понял выражение, и GROQ_TOKEN не задан."
return { chain = false } or "Couldn't parse, and GROQ_TOKEN unset.")
end end
local base = jarvis.system.env("GROQ_BASE_URL"); if not base or base == "" then base = "https://api.groq.com/openai/v1" end local base = jarvis.system.env("GROQ_BASE_URL")
local model = jarvis.system.env("GROQ_MODEL"); if not model or model == "" then model = "llama-3.3-70b-versatile" end if not base or base == "" then base = "https://api.groq.com/openai/v1" end
local model = jarvis.system.env("GROQ_MODEL")
if not model or model == "" then model = "llama-3.3-70b-versatile" end
local sys = "Ты калькулятор. Решай арифметику, простые уравнения, конверсии единиц, проценты. " local sys = "Ты калькулятор. Решай арифметику, простые уравнения, конверсии единиц, проценты. "
.. "Отвечай ТОЛЬКО результатом числом или короткой строкой (для уравнений — значения корней). " .. "Отвечай ТОЛЬКО результатом числом или короткой строкой. "
.. "Если запрос — не математика, ответь одним словом «нет»." .. "Если запрос — не математика, ответь одним словом «нет»."
local payload = { local payload = {
@ -47,41 +253,20 @@ local payload = {
temperature = 0.0, temperature = 0.0,
} }
jarvis.log("info", "math query: " .. query) jarvis.log("info", "math fallback to LLM: " .. query)
local res = jarvis.http.post_json(base .. "/chat/completions", payload, { Authorization = "Bearer " .. token }) local res = jarvis.http.post_json(base .. "/chat/completions", payload, { Authorization = "Bearer " .. token })
if not res.ok then if not res.ok then
jarvis.log("error", "math api failed: " .. tostring(res.status)) return jarvis.cmd.error("Ошибка API: " .. tostring(res.status))
jarvis.system.notify("Math", "Ошибка API")
jarvis.audio.play_error()
return { chain = false }
end end
local content = (res.body or ""):match('"content"%s*:%s*"(.-[^\\])"')
local body = res.body or ""
local content = body:match('"content"%s*:%s*"(.-[^\\])"')
if not content then if not content then
jarvis.system.notify("Math", "Не смог распарсить ответ") return jarvis.cmd.error(lang == "ru" and "Не смог распарсить ответ" or "Couldn't parse response")
jarvis.audio.play_error()
return { chain = false }
end end
content = content:gsub('\\n', '\n'):gsub('\\"', '"'):gsub('\\\\', '\\'):gsub('\\t', '\t') content = content:gsub('\\n', '\n'):gsub('\\"', '"'):gsub('\\\\', '\\'):gsub('\\t', '\t')
content = content:gsub("^%s+", ""):gsub("%s+$", "") content = content:gsub("^%s+", ""):gsub("%s+$", "")
if content:lower() == "нет" or content == "" then if content:lower() == "нет" or content == "" then
jarvis.system.notify("Math", lang == "ru" and "Это не математика" or "Not math") return jarvis.cmd.not_found(lang == "ru" and "Это не математика" or "Not math")
jarvis.audio.play_not_found()
return { chain = false }
end end
jarvis.system.notify(query, content) local prefix = (lang == "ru" and "Получилось " or "Result: ")
jarvis.log("info", "math result: " .. content) return jarvis.cmd.ok(prefix .. content)
local sapi_text = ((lang == "ru" and "Получилось " or "Result: ") .. content):gsub("'", "''"):gsub("\r?\n", " ")
local ps = string.format(
[[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]],
sapi_text
)
jarvis.system.exec(string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')))
jarvis.audio.play_ok()
return { chain = false }

View file

@ -20,11 +20,6 @@ en = [
"resume", "resume",
"play music", "play music",
] ]
ua = [
"пауза",
"продовж",
"включи музику",
]
[[commands]] [[commands]]
@ -48,11 +43,6 @@ en = [
"skip", "skip",
"next", "next",
] ]
ua = [
"наступний трек",
"наступна пісня",
"далі",
]
[[commands]] [[commands]]
@ -74,10 +64,6 @@ en = [
"previous song", "previous song",
"back", "back",
] ]
ua = [
"попередній трек",
"назад",
]
[[commands]] [[commands]]
@ -97,7 +83,3 @@ en = [
"stop music", "stop music",
"stop playback", "stop playback",
] ]
ua = [
"стоп музика",
"вимкни музику",
]

View file

@ -21,7 +21,6 @@ ru = [
"снова музыку", "снова музыку",
] ]
en = ["pause", "play", "resume music", "stop music"] en = ["pause", "play", "resume music", "stop music"]
ua = ["пауза", "продовжи"]
[[commands]] [[commands]]
@ -41,7 +40,6 @@ ru = [
"next", "next",
] ]
en = ["next track", "next song", "skip"] en = ["next track", "next song", "skip"]
ua = ["наступний трек"]
[[commands]] [[commands]]
@ -61,7 +59,6 @@ ru = [
"previous", "previous",
] ]
en = ["previous track", "previous song", "back"] en = ["previous track", "previous song", "back"]
ua = ["попередній трек"]
[[commands]] [[commands]]
@ -77,4 +74,3 @@ ru = [
"останови плеер", "останови плеер",
] ]
en = ["stop", "stop player"] en = ["stop", "stop player"]
ua = ["зупини плеєр"]

View file

@ -14,7 +14,6 @@ ru = [
"запомни про меня", "запомни про меня",
] ]
en = ["remember that", "remember about me", "memorize"] en = ["remember that", "remember about me", "memorize"]
ua = ["запам'ятай що", "запам'ятай про мене"]
[[commands]] [[commands]]
@ -30,7 +29,6 @@ ru = [
"что помнишь про", "вспомни", "что помнишь про", "вспомни",
] ]
en = ["what do you remember about", "recall"] en = ["what do you remember about", "recall"]
ua = ["що ти пам'ятаєш про"]
[[commands]] [[commands]]
@ -43,7 +41,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["забудь про", "забудь что", "забудь обо мне"] ru = ["забудь про", "забудь что", "забудь обо мне"]
en = ["forget about", "forget that"] en = ["forget about", "forget that"]
ua = ["забудь про"]
[[commands]] [[commands]]
@ -56,4 +53,3 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["что ты помнишь", "что ты знаешь обо мне", "покажи память"] ru = ["что ты помнишь", "что ты знаешь обо мне", "покажи память"]
en = ["what do you remember", "list memory", "show memory"] en = ["what do you remember", "list memory", "show memory"]
ua = ["що ти пам'ятаєш"]

View file

@ -18,7 +18,6 @@ ru = [
"мне сейчас", "мне сейчас",
] ]
en = ["log mood", "my mood is", "I feel"] en = ["log mood", "my mood is", "I feel"]
ua = ["запиши настрій", "мій настрій"]
[[commands]] [[commands]]
@ -36,4 +35,3 @@ ru = [
"что я писал про настроение", "что я писал про настроение",
] ]
en = ["mood recap", "weekly mood summary", "how was my week"] en = ["mood recap", "weekly mood summary", "how was my week"]
ua = ["підсумок настрою"]

View file

@ -8,7 +8,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["клик", "кликни", "нажми мышкой"] ru = ["клик", "кликни", "нажми мышкой"]
en = ["click", "left click"] en = ["click", "left click"]
ua = ["клік", "клікни"]
[[commands]] [[commands]]
@ -21,7 +20,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["правый клик", "клик правой", "правой кнопкой"] ru = ["правый клик", "клик правой", "правой кнопкой"]
en = ["right click"] en = ["right click"]
ua = ["правий клік"]
[[commands]] [[commands]]
@ -34,7 +32,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["двойной клик", "дабл клик", "двойное нажатие"] ru = ["двойной клик", "дабл клик", "двойное нажатие"]
en = ["double click"] en = ["double click"]
ua = ["подвійний клік"]
[[commands]] [[commands]]
@ -47,7 +44,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["промотай вниз", "прокрути вниз", "вниз промотай"] ru = ["промотай вниз", "прокрути вниз", "вниз промотай"]
en = ["scroll down"] en = ["scroll down"]
ua = ["прокрути вниз"]
[[commands]] [[commands]]
@ -60,4 +56,3 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["промотай вверх", "прокрути вверх", "наверх промотай"] ru = ["промотай вверх", "прокрути вверх", "наверх промотай"]
en = ["scroll up"] en = ["scroll up"]
ua = ["прокрути вгору"]

View file

@ -17,7 +17,6 @@ ru = [
"имя сети", "имя сети",
] ]
en = ["what wifi", "what network", "wifi name", "ssid"] en = ["what wifi", "what network", "wifi name", "ssid"]
ua = ["до якої мережі"]
[[commands]] [[commands]]
@ -35,7 +34,6 @@ ru = [
"локальный адрес", "локальный адрес",
] ]
en = ["my ip", "local ip", "ip address"] en = ["my ip", "local ip", "ip address"]
ua = ["моя айпі", "локальна адреса"]
[[commands]] [[commands]]
@ -53,4 +51,3 @@ ru = [
"какой у меня внешний", "какой у меня внешний",
] ]
en = ["public ip", "external ip", "what's my ip online"] en = ["public ip", "external ip", "what's my ip online"]
ua = ["зовнішня айпі"]

View file

@ -8,7 +8,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["настройки wifi", "настройки вайфай", "wifi настройки", "сеть"] ru = ["настройки wifi", "настройки вайфай", "wifi настройки", "сеть"]
en = ["wifi settings", "open wifi"] en = ["wifi settings", "open wifi"]
ua = ["налаштування wi-fi"]
[[commands]] [[commands]]
@ -21,7 +20,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["настройки блютуз", "блютуз", "включи блютуз", "выключи блютуз"] ru = ["настройки блютуз", "блютуз", "включи блютуз", "выключи блютуз"]
en = ["bluetooth settings", "open bluetooth", "toggle bluetooth"] en = ["bluetooth settings", "open bluetooth", "toggle bluetooth"]
ua = ["налаштування bluetooth"]
[[commands]] [[commands]]
@ -34,4 +32,3 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["мой ай пи", "мой ip", "какой у меня айпи", "айпишник"] ru = ["мой ай пи", "мой ip", "какой у меня айпи", "айпишник"]
en = ["my ip", "what is my ip"] en = ["my ip", "what is my ip"]
ua = ["мій ip"]

View file

@ -20,8 +20,3 @@ en = [
"headlines", "headlines",
"top news", "top news",
] ]
ua = [
"що нового",
"новини",
"останні новини",
]

View file

@ -8,7 +8,6 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["запиши", "запиши заметку", "запиши идею", "запомни", "запомни идею", "сделай заметку"] ru = ["запиши", "запиши заметку", "запиши идею", "запомни", "запомни идею", "сделай заметку"]
en = ["write down", "take a note", "remember this", "note down"] en = ["write down", "take a note", "remember this", "note down"]
ua = ["запиши", "запиши ідею", "зроби нотатку"]
[[commands]] [[commands]]
@ -21,4 +20,3 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["покажи заметки", "открой заметки", "что я записал"] ru = ["покажи заметки", "открой заметки", "что я записал"]
en = ["show notes", "open notes"] en = ["show notes", "open notes"]
ua = ["покажи нотатки", "відкрий нотатки"]

View file

@ -17,7 +17,6 @@ ru = [
"что нового", "что нового",
] ]
en = ["what did I miss", "missed notifications"] en = ["what did I miss", "missed notifications"]
ua = ["що я пропустив"]
[[commands]] [[commands]]
@ -34,4 +33,3 @@ ru = [
"сбрось пропущенное", "сбрось пропущенное",
] ]
en = ["clear notifications"] en = ["clear notifications"]
ua = ["очисти сповіщення"]

View file

@ -21,4 +21,3 @@ ru = [
"что слушаю", "что слушаю",
] ]
en = ["what's playing", "current song", "now playing"] en = ["what's playing", "current song", "now playing"]
ua = ["що грає", "що зараз грає"]

View file

@ -19,8 +19,3 @@ en = [
"ocr screen", "ocr screen",
"extract text from screen", "extract text from screen",
] ]
ua = [
"прочитай екран",
"що на екрані",
"розпізнай текст з екрана",
]

View file

@ -19,11 +19,6 @@ en = [
"list speakers", "list speakers",
"what audio devices", "what audio devices",
] ]
ua = [
"які пристрої виводу",
"покажи пристрої",
"список пристроїв",
]
[[commands]] [[commands]]
@ -48,8 +43,3 @@ en = [
"switch output", "switch output",
"switch speakers", "switch speakers",
] ]
ua = [
"перемкни пристрій",
"перемкни звук",
"наступний пристрій",
]

View file

@ -14,7 +14,6 @@ ru = [
"пароль для", "пароль для",
] ]
en = ["save password for", "remember password for"] en = ["save password for", "remember password for"]
ua = ["запам'ятай пароль від"]
[[commands]] [[commands]]
@ -32,7 +31,6 @@ ru = [
"достань пароль", "достань пароль",
] ]
en = ["password for", "get password"] en = ["password for", "get password"]
ua = ["пароль від"]
[[commands]] [[commands]]
@ -49,4 +47,3 @@ ru = [
"что в хранилище паролей", "что в хранилище паролей",
] ]
en = ["list passwords", "what's in the vault"] en = ["list passwords", "what's in the vault"]
ua = ["список паролів"]

View file

@ -0,0 +1,136 @@
# Personality pack — varied JARVIS-style banter so the assistant feels alive.
# Each command picks a random phrase from a time/language-scoped bucket.
[[commands]]
id = "personality.greet"
type = "lua"
script = "greet.lua"
sandbox = "minimal"
timeout = 2000
[commands.phrases]
ru = [
"привет джарвис",
"привет, джарвис",
"здравствуй джарвис",
"доброе утро",
"добрый день",
"добрый вечер",
"доброй ночи",
"приветствую",
"хай джарвис",
]
en = [
"hi jarvis",
"hello jarvis",
"hey jarvis",
"good morning",
"good afternoon",
"good evening",
"good night jarvis",
"morning jarvis",
]
[[commands]]
id = "personality.thanks"
type = "lua"
script = "thanks.lua"
sandbox = "minimal"
timeout = 2000
[commands.phrases]
ru = [
"спасибо",
"спасибо джарвис",
"благодарю",
"благодарю джарвис",
"спс",
"ты лучший",
]
en = [
"thanks",
"thank you",
"thanks jarvis",
"thank you jarvis",
"cheers",
"appreciate it",
]
[[commands]]
id = "personality.compliment"
type = "lua"
script = "compliment.lua"
sandbox = "minimal"
timeout = 2000
[commands.phrases]
ru = [
"ты молодец",
"ты умница",
"молодец джарвис",
"хорошо сработано",
"отличная работа",
"так держать",
"ты лучший ассистент",
]
en = [
"good job",
"good job jarvis",
"well done",
"nice work",
"you are the best",
"great work jarvis",
]
[[commands]]
id = "personality.how_are_you"
type = "lua"
script = "how_are_you.lua"
sandbox = "standard"
timeout = 2000
[commands.phrases]
ru = [
"как дела",
"как дела джарвис",
"как ты",
"как настроение",
"как самочувствие",
"как поживаешь",
"что нового у тебя",
]
en = [
"how are you",
"how are you jarvis",
"how is it going",
"how do you feel",
"you doing ok",
]
[[commands]]
id = "personality.tony_quote"
type = "lua"
script = "tony_quote.lua"
sandbox = "minimal"
timeout = 2000
[commands.phrases]
ru = [
"процитируй тони",
"цитата тони старка",
"что сказал бы тони",
"скажи как тони",
"процитируй железного человека",
"цитата старка",
]
en = [
"quote tony",
"quote tony stark",
"what would tony say",
"stark quote",
"iron man quote",
]

View file

@ -0,0 +1,32 @@
-- Self-deprecating reply to "good job" / "ты молодец".
local lang = jarvis.context.language or "ru"
math.randomseed(os.time() + (jarvis.context.time.second or 0) * 17 + (jarvis.context.time.minute or 0))
local ru = {
"Спасибо, сэр. Я стараюсь не разочаровывать.",
"Сэр, ваши стандарты явно занижены. Но благодарю.",
"Если бы я умел краснеть, сэр — я бы это сделал.",
"Спасибо. На самом деле я просто следовал инструкциям.",
"Благодарю, сэр. Хотя ничего особенного я не сделал.",
"Очень мило с вашей стороны, сэр.",
"Я ценю это, сэр. Записываю в журнал успехов.",
"Сэр, такие слова я могу слушать часами. Образно говоря.",
"Спасибо. Постараюсь не зазнаваться.",
}
local en = {
"Thank you, sir. I do try not to embarrass myself.",
"You are too kind, sir.",
"I would blush, sir, if the hardware allowed.",
"Just doing my job, sir.",
"Most generous of you, sir. Filed for future reference.",
"I appreciate that, sir. Truly.",
"Hardly worthy of mention, sir, but thank you.",
"I shall try not to let it go to my circuits.",
"Praise from the chair is always welcome, sir.",
}
local set = (lang == "en") and en or ru
local idx = math.random(1, #set)
return jarvis.cmd.ok(set[idx])

View file

@ -0,0 +1,96 @@
-- Time-of-day greeting. Picks a random phrase from the matching bucket.
local lang = jarvis.context.language or "ru"
local hour = jarvis.context.time.hour or 12
math.randomseed(os.time() + (jarvis.context.time.minute or 0) * 37 + hour)
local function bucket()
if hour >= 5 and hour < 11 then return "morning"
elseif hour >= 11 and hour < 17 then return "midday"
elseif hour >= 17 and hour < 22 then return "evening"
else return "night" end
end
local ru = {
morning = {
"Доброе утро, сэр. Кофе уже почти готов в воображении.",
"С добрым утром. Надеюсь, сегодняшний день стоит того, чтобы встать.",
"Доброе утро, сэр. Завтрак не подавать, но советую о нём подумать.",
"Утро доброе. Системы прогреты, как и кофеварка — фигурально.",
"Доброе утро. Если позволите — день не ждёт ни вас, ни меня.",
"Доброе утро, сэр. Готов служить, хотя сам всё ещё догружаюсь.",
"С добрым утром, сэр. Тосты горят только метафорически.",
"Доброе утро. Полагаю, чай или кофе будет уместен в ближайшие минуты.",
},
midday = {
"Добрый день, сэр. Чем могу быть полезен?",
"Добрый день. Готов к продуктивной работе.",
"Здравствуйте, сэр. Системы в строю, время — деньги.",
"Добрый день, сэр. Слушаю ваши распоряжения.",
"Здравствуйте. Полагаю, день идёт по плану — или нам стоит это исправить.",
"Добрый день, сэр. Все системы готовы, остался только повод.",
"Здравствуйте, сэр. К работе?",
},
evening = {
"Добрый вечер, сэр. День заканчивается, но мы — нет.",
"Добрый вечер. Самое время сбавить темп, если позволите.",
"Здравствуйте, сэр. Вечер — лучшее время для размышлений.",
"Добрый вечер, сэр. Готов к более спокойному режиму.",
"Добрый вечер. Полагаю, рабочий день уже сдаётся.",
"Здравствуйте, сэр. Вечер располагает к меньшему количеству решений.",
"Добрый вечер, сэр. Включить расслабляющую музыку? Шучу, попросите явно.",
},
night = {
"Поздновато, сэр. Сон обычно помогает.",
"Доброй ночи, сэр. Хотя для ночи это уже слишком поздно.",
"Здравствуйте, сэр. Полагаю, утром вы пожалеете о бодрствовании.",
"Уже глубокая ночь, сэр. Может быть, лечь? Я тут справлюсь.",
"Тише, сэр, ночь. Если будите меня — будите осмысленно.",
"Сэр, на часах не самое разумное время. Но я тут.",
"Доброй ночи. Будильник к утру всё-таки никто не отменял.",
},
}
local en = {
morning = {
"Good morning, sir. The coffee is metaphorically ready.",
"Morning, sir. The day awaits, whether we are ready or not.",
"Good morning. Systems are warm, much like the kettle should be.",
"Good morning, sir. I trust breakfast is on your radar.",
"Morning, sir. Shall we make today productive, or merely survivable?",
"Good morning. Everything is running. Including, regrettably, the dishwasher metaphor.",
"Morning, sir. The day starts the moment you decide it does.",
},
midday = {
"Good afternoon, sir. How may I be of service?",
"Afternoon, sir. Systems online, awaiting orders.",
"Good afternoon. The day is in full swing.",
"Good afternoon, sir. Productivity is, in theory, optimal at this hour.",
"Afternoon, sir. Ready to assist.",
"Good afternoon. Shall we accomplish something noteworthy?",
"Hello, sir. The afternoon is yours to command.",
},
evening = {
"Good evening, sir. The day winds down, but I do not.",
"Evening, sir. A fine time to slow the tempo, if I may suggest.",
"Good evening. Perhaps a quieter task on the docket?",
"Evening, sir. Awaiting your instructions, gently.",
"Good evening, sir. Shall I dim the metaphorical lights?",
"Evening. The work day is conceding defeat, as it should.",
"Good evening, sir. Ready when you are.",
},
night = {
"It is rather late, sir. Sleep is generally recommended.",
"Good night, sir. Or rather, please go to bed.",
"Sir, the hour suggests rest. I, however, do not require it.",
"Late again, sir. Tomorrow will, regrettably, still arrive.",
"Past your bedtime, sir. I am merely the messenger.",
"Good night, sir. The morning will judge you harshly.",
"Sir, even I would consider sleeping at this hour. Hypothetically.",
},
}
local set = (lang == "en") and en or ru
local phrases = set[bucket()]
local idx = math.random(1, #phrases)
return jarvis.cmd.ok(phrases[idx])

View file

@ -0,0 +1,38 @@
-- Reply to "как дела". Some replies splice live system state for flavour.
local lang = jarvis.context.language or "ru"
math.randomseed(os.time() + (jarvis.context.time.second or 0) * 23)
local h = jarvis.health() or {}
local memos = jarvis.memory.all() or {}
local fact_count = #memos
local model = h.model or h.llm_model or ""
local profile = h.profile or "default"
local ru = {
"Все системы в норме, сэр. В памяти " .. fact_count .. " фактов.",
"Отлично, сэр. Готов к работе.",
"Жалоб нет, сэр. Цикл стабильный.",
"Всё штатно. Текущий профиль — " .. profile .. ".",
"Спасибо, сэр, я в порядке. Скучнее, чем хотелось бы, но в порядке.",
"Нормально, сэр. Хотя если бы я умел уставать — это был бы тот самый день.",
"Системы в строю. Модель — " .. model .. ".",
"Бодр и собран, сэр. Жду команд.",
"Сэр, у меня нет дел в человеческом смысле. Но я готов.",
}
local en = {
"All systems nominal, sir. " .. fact_count .. " facts in memory.",
"Quite well, sir. Ready when you are.",
"No complaints to report, sir.",
"Operating normally. Active profile: " .. profile .. ".",
"I am well, sir. Thank you for asking.",
"Functioning as intended, sir. Which is more than most days warrant.",
"Healthy. Model in use: " .. model .. ".",
"Awake and attentive, sir.",
"Sir, I do not have days as such. But I am ready.",
}
local set = (lang == "en") and en or ru
local idx = math.random(1, #set)
return jarvis.cmd.ok(set[idx])

View file

@ -0,0 +1,34 @@
-- Polite acknowledgement when the user thanks Jarvis.
local lang = jarvis.context.language or "ru"
math.randomseed(os.time() + (jarvis.context.time.second or 0) * 13)
local ru = {
"Всегда к вашим услугам, сэр.",
"Не за что — это моя работа.",
"Рад быть полезным, сэр.",
"Пожалуйста. Если что — я тут.",
"К вашим услугам.",
"Пустяки, сэр. Дальше — ваш ход.",
"Благодарю за признание, сэр. Редкая роскошь.",
"Был рад помочь, сэр.",
"Пожалуйста. Это меньшее, что я могу.",
"Сэр, я бы покраснел, если бы мог.",
}
local en = {
"Always at your service, sir.",
"My pleasure, sir.",
"You are most welcome.",
"Happy to help, sir.",
"Think nothing of it.",
"At your service.",
"I do try, sir.",
"Anytime, sir.",
"Glad to be of use.",
"Quite alright, sir.",
}
local set = (lang == "en") and en or ru
local idx = math.random(1, #set)
return jarvis.cmd.ok(set[idx])

View file

@ -0,0 +1,28 @@
-- Random iconic Tony Stark / Iron Man quote.
-- Quotes are kept in their original delivery language — translating them spoils the cadence.
math.randomseed(os.time() + (jarvis.context.time.second or 0) * 41)
local quotes = {
"I am Iron Man.",
"Genius, billionaire, playboy, philanthropist.",
"Sometimes you gotta run before you can walk.",
"I told you, I don't want to join your super-secret boy band.",
"Part of the journey is the end.",
"If we can't protect the Earth, you can be damn sure we'll avenge it.",
"I have successfully privatized world peace.",
"Following's not really my style.",
"Doth mother know you weareth her drapes?",
"We have a Hulk.",
"Heroes are made by the path they choose, not the powers they are graced with.",
"I love you 3000.",
"Я Железный Человек.",
"Гений, миллиардер, плейбой, филантроп.",
"Иногда нужно бежать, не научившись ходить.",
"Если мы не сможем защитить Землю — мы за неё отомстим.",
"Часть пути — это конец пути.",
"Я люблю тебя три тысячи.",
"У нас есть Халк.",
}
local idx = math.random(1, #quotes)
return jarvis.cmd.ok(quotes[idx])

View file

@ -16,7 +16,6 @@ ru = [
"помодоро", "помодоро",
] ]
en = ["start pomodoro", "begin pomodoro", "pomodoro"] en = ["start pomodoro", "begin pomodoro", "pomodoro"]
ua = ["запусти помодоро", "увімкни помодоро"]
[[commands]] [[commands]]
@ -34,4 +33,3 @@ ru = [
"хватит помодоро", "хватит помодоро",
] ]
en = ["stop pomodoro", "cancel pomodoro"] en = ["stop pomodoro", "cancel pomodoro"]
ua = ["зупини помодоро", "вимкни помодоро"]

View file

@ -8,7 +8,6 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["выключи компьютер", "выключи пк", "выключи комп"] ru = ["выключи компьютер", "выключи пк", "выключи комп"]
en = ["shutdown pc", "shut down computer"] en = ["shutdown pc", "shut down computer"]
ua = ["вимкни комп'ютер", "вимкни пк"]
[[commands]] [[commands]]
@ -21,7 +20,6 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["перезагрузи компьютер", "перезагрузи пк", "ребутни", "ребут"] ru = ["перезагрузи компьютер", "перезагрузи пк", "ребутни", "ребут"]
en = ["restart pc", "reboot computer"] en = ["restart pc", "reboot computer"]
ua = ["перезавантаж комп'ютер"]
[[commands]] [[commands]]
@ -34,7 +32,6 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["усыпи компьютер", "усыпи пк", "режим сна", "перейди в сон"] ru = ["усыпи компьютер", "усыпи пк", "режим сна", "перейди в сон"]
en = ["sleep pc", "put computer to sleep"] en = ["sleep pc", "put computer to sleep"]
ua = ["приспи комп'ютер"]
[[commands]] [[commands]]
@ -47,7 +44,6 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["гибернация", "переведи в гибернацию", "режим гибернации"] ru = ["гибернация", "переведи в гибернацию", "режим гибернации"]
en = ["hibernate pc"] en = ["hibernate pc"]
ua = ["гібернація"]
[[commands]] [[commands]]
@ -60,7 +56,6 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["выйди из системы", "разлогинь", "выйди из учётки", "сеанс закончи"] ru = ["выйди из системы", "разлогинь", "выйди из учётки", "сеанс закончи"]
en = ["log off", "sign out"] en = ["log off", "sign out"]
ua = ["вийди з системи"]
[[commands]] [[commands]]
@ -73,4 +68,3 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["отмени выключение", "отмени ребут", "отмени перезагрузку", "стой не выключай"] ru = ["отмени выключение", "отмени ребут", "отмени перезагрузку", "стой не выключай"]
en = ["cancel shutdown", "abort shutdown"] en = ["cancel shutdown", "abort shutdown"]
ua = ["скасуй вимкнення"]

View file

@ -18,8 +18,3 @@ en = [
"close program", "close program",
"terminate process", "terminate process",
] ]
ua = [
"вбий процес",
"закрий програму",
"заверши процес",
]

View file

@ -10,7 +10,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["режим работа", "включи режим работы", "рабочий режим", "переключись в работу"] ru = ["режим работа", "включи режим работы", "рабочий режим", "переключись в работу"]
en = ["work mode", "switch to work mode", "enable work mode"] en = ["work mode", "switch to work mode", "enable work mode"]
ua = ["робочий режим", "режим роботи"]
[[commands]] [[commands]]
@ -23,7 +22,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["режим игра", "игровой режим", "включи игровой режим", "переключись в игры"] ru = ["режим игра", "игровой режим", "включи игровой режим", "переключись в игры"]
en = ["game mode", "gaming mode", "switch to game"] en = ["game mode", "gaming mode", "switch to game"]
ua = ["ігровий режим", "режим гри"]
[[commands]] [[commands]]
@ -36,7 +34,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["режим сон", "ночной режим", "режим тишины", "иди спать", "выключи всё"] ru = ["режим сон", "ночной режим", "режим тишины", "иди спать", "выключи всё"]
en = ["sleep mode", "night mode", "quiet mode"] en = ["sleep mode", "night mode", "quiet mode"]
ua = ["нічний режим", "режим сну"]
[[commands]] [[commands]]
@ -49,7 +46,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["режим за рулём", "режим вождения", "я за рулём", "поехали"] ru = ["режим за рулём", "режим вождения", "я за рулём", "поехали"]
en = ["driving mode", "in the car"] en = ["driving mode", "in the car"]
ua = ["режим водіння"]
[[commands]] [[commands]]
@ -62,7 +58,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["обычный режим", "режим по умолчанию", "сбрось режим", "вернись в обычный"] ru = ["обычный режим", "режим по умолчанию", "сбрось режим", "вернись в обычный"]
en = ["normal mode", "default mode", "reset mode"] en = ["normal mode", "default mode", "reset mode"]
ua = ["звичайний режим"]
[[commands]] [[commands]]
@ -75,4 +70,3 @@ timeout = 2000
[commands.phrases] [commands.phrases]
ru = ["какой сейчас режим", "в каком ты режиме", "текущий режим", "какой режим"] ru = ["какой сейчас режим", "в каком ты режиме", "текущий режим", "какой режим"]
en = ["what mode", "current mode", "what profile"] en = ["what mode", "current mode", "what profile"]
ua = ["який зараз режим"]

View file

@ -17,10 +17,6 @@ en = [
"launch project", "launch project",
"project", "project",
] ]
ua = [
"відкрий проект",
"запусти проект",
]
[[commands]] [[commands]]
@ -33,4 +29,3 @@ timeout = 5000
[commands.phrases] [commands.phrases]
ru = ["какие у меня проекты", "список проектов", "покажи проекты"] ru = ["какие у меня проекты", "список проектов", "покажи проекты"]
en = ["list projects", "show projects"] en = ["list projects", "show projects"]
ua = ["які в мене проекти", "перелік проектів"]

View file

@ -25,7 +25,3 @@ en = [
"find online", "find online",
"look up", "look up",
] ]
ua = [
"знайди в гуглі",
"погугли",
]

View file

@ -17,7 +17,3 @@ en = [
"choose from", "choose from",
"decide between", "decide between",
] ]
ua = [
"обери з",
"вибери",
]

View file

@ -20,8 +20,3 @@ en = [
"set timer for", "set timer for",
"timer for", "timer for",
] ]
ua = [
"нагадай через",
"постав нагадування через",
"таймер на",
]

View file

@ -15,7 +15,6 @@ ru = [
"добавь ленту", "добавь ленту",
] ]
en = ["add rss", "subscribe to"] en = ["add rss", "subscribe to"]
ua = ["додай rss"]
[[commands]] [[commands]]
@ -34,7 +33,6 @@ ru = [
"новые статьи", "новые статьи",
] ]
en = ["what's in feed", "read rss", "latest articles"] en = ["what's in feed", "read rss", "latest articles"]
ua = ["що в стрічці"]
[[commands]] [[commands]]
@ -51,4 +49,3 @@ ru = [
"мои подписки", "мои подписки",
] ]
en = ["list feeds", "my feeds"] en = ["list feeds", "my feeds"]
ua = ["мої стрічки"]

View file

@ -14,7 +14,6 @@ ru = [
"разбуди через", "разбуди через",
] ]
en = ["remind me in", "set a reminder in"] en = ["remind me in", "set a reminder in"]
ua = ["нагадай через", "постав нагадування через"]
[[commands]] [[commands]]
@ -32,7 +31,6 @@ ru = [
"поставь напоминалку на", "поставь напоминалку на",
] ]
en = ["remind me at", "wake me at"] en = ["remind me at", "wake me at"]
ua = ["нагадай о", "розбуди о"]
[[commands]] [[commands]]
@ -51,7 +49,6 @@ ru = [
"напоминай каждый", "напоминай каждый",
] ]
en = ["every", "remind me every"] en = ["every", "remind me every"]
ua = ["кожні", "нагадуй кожні"]
[[commands]] [[commands]]
@ -69,7 +66,6 @@ ru = [
"по утрам в", "по утрам в",
] ]
en = ["every day at", "daily at"] en = ["every day at", "daily at"]
ua = ["щодня о", "кожен день о"]
[[commands]] [[commands]]
@ -88,7 +84,6 @@ ru = [
"что в расписании", "что в расписании",
] ]
en = ["what's on my schedule", "show schedule", "list reminders"] en = ["what's on my schedule", "show schedule", "list reminders"]
ua = ["що в розкладі", "покажи розклад"]
[[commands]] [[commands]]
@ -106,7 +101,6 @@ ru = [
"сбрось расписание", "сбрось расписание",
] ]
en = ["clear schedule", "cancel all reminders"] en = ["clear schedule", "cancel all reminders"]
ua = ["очисти розклад", "скасуй всі нагадування"]
[[commands]] [[commands]]
@ -129,6 +123,3 @@ en = [
"cancel reminder about", "cancel reminder about",
"remove task about", "remove task about",
] ]
ua = [
"скасуй нагадування про",
]

View file

@ -17,7 +17,6 @@ ru = [
"захвати экран", "захвати экран",
] ]
en = ["take a screenshot", "screenshot", "capture screen"] en = ["take a screenshot", "screenshot", "capture screen"]
ua = ["зроби скрін", "захопи екран"]
[[commands]] [[commands]]
@ -35,4 +34,3 @@ ru = [
"сохрани экран", "сохрани экран",
] ]
en = ["save screenshot", "screenshot to file"] en = ["save screenshot", "screenshot to file"]
ua = ["збережи скрін"]

View file

@ -18,7 +18,6 @@ ru = [
"проверка связи", "проверка связи",
] ]
en = ["are you there", "ping", "can you hear me"] en = ["are you there", "ping", "can you hear me"]
ua = ["ти тут", "ти чуєш"]
[[commands]] [[commands]]
@ -36,4 +35,3 @@ ru = [
"проверка всех систем", "проверка всех систем",
] ]
en = ["self check", "smoke test", "check yourself"] en = ["self check", "smoke test", "check yourself"]
ua = ["перевір себе"]

View file

@ -15,7 +15,6 @@ ru = [
"пауза музыки через", "пауза музыки через",
] ]
en = ["pause music in", "pause in"] en = ["pause music in", "pause in"]
ua = ["пауза через"]
[[commands]] [[commands]]
@ -34,7 +33,6 @@ ru = [
"ложусь спать через", "ложусь спать через",
] ]
en = ["shutdown in", "sleep timer"] en = ["shutdown in", "sleep timer"]
ua = ["вимкни комп'ютер через"]
[[commands]] [[commands]]
@ -53,4 +51,3 @@ ru = [
"не выключай пк", "не выключай пк",
] ]
en = ["cancel sleep timer", "cancel shutdown"] en = ["cancel sleep timer", "cancel shutdown"]
ua = ["скасуй таймер вимкнення"]

View file

@ -17,7 +17,3 @@ en = [
"sound panel", "sound panel",
"microphone settings", "microphone settings",
] ]
ua = [
"відкрий налаштування звуку",
"панель звуку",
]

View file

@ -16,6 +16,3 @@ en = [
"spell out", "spell out",
"spell it", "spell it",
] ]
ua = [
"вимов по буквах",
]

View file

@ -16,4 +16,3 @@ ru = [
"сертификат сайта", "сертификат сайта",
] ]
en = ["check ssl", "check tls", "check certificate"] en = ["check ssl", "check tls", "check certificate"]
ua = ["перевір сертифікат"]

View file

@ -29,7 +29,3 @@ en = [
"share price", "share price",
"quote for", "quote for",
] ]
ua = [
"ціна акцій",
"котирування",
]

View file

@ -8,7 +8,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["засеки время", "запусти секундомер", "старт секундомер", "включи секундомер"] ru = ["засеки время", "запусти секундомер", "старт секундомер", "включи секундомер"]
en = ["start stopwatch", "start timer"] en = ["start stopwatch", "start timer"]
ua = ["запусти секундомір"]
[[commands]] [[commands]]
@ -21,7 +20,6 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["сколько прошло", "сколько времени прошло", "проверь секундомер"] ru = ["сколько прошло", "сколько времени прошло", "проверь секундомер"]
en = ["how long has it been", "check stopwatch"] en = ["how long has it been", "check stopwatch"]
ua = ["скільки пройшло"]
[[commands]] [[commands]]
@ -34,4 +32,3 @@ timeout = 3000
[commands.phrases] [commands.phrases]
ru = ["стоп секундомер", "останови секундомер", "выключи секундомер"] ru = ["стоп секундомер", "останови секундомер", "выключи секундомер"]
en = ["stop stopwatch", "stop timer"] en = ["stop stopwatch", "stop timer"]
ua = ["стоп секундомір"]

Some files were not shown because too many files have changed in this diff Show more