Compare commits
No commits in common. "919d565879f738387dcf5f6c5d57ed9edc1031fe" and "4e21024509eda9e774917a731ed22227996e619a" have entirely different histories.
919d565879
...
4e21024509
115 changed files with 635 additions and 1532 deletions
67
.github/workflows/ci.yml
vendored
67
.github/workflows/ci.yml
vendored
|
|
@ -1,67 +0,0 @@
|
|||
# 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
6
.gitignore
vendored
|
|
@ -1,12 +1,6 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Per-developer environment + ad-hoc scratch files
|
||||
dev.env
|
||||
dll_probe*.py
|
||||
imports_full.txt
|
||||
*.scratch.*
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
|
|
|||
|
|
@ -94,11 +94,6 @@ fn main() -> Result<(), String> {
|
|||
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");
|
||||
if recorder::init().is_err() {
|
||||
notify_mic_problem();
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ pub fn build(settings: &SettingsManager) -> TrayMenu {
|
|||
let label = match lang {
|
||||
"ru" => "Русский",
|
||||
"en" => "English",
|
||||
"ua" => "Українська",
|
||||
_ => lang,
|
||||
};
|
||||
let item = CheckMenuItem::with_id(
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ pub const LLM_FALLBACK_ERROR_RU: &str = "Не могу связаться с с
|
|||
pub fn get_llm_trigger_phrases(lang: &str) -> &'static [&'static str] {
|
||||
match lang {
|
||||
"ru" => &["скажи", "ответь", "произнеси"],
|
||||
"ua" => &["скажи", "відповідай"],
|
||||
"en" => &["say", "tell", "answer"],
|
||||
_ => &[],
|
||||
}
|
||||
|
|
@ -227,7 +228,7 @@ pub fn get_llm_trigger_phrases(lang: &str) -> &'static [&'static str] {
|
|||
|
||||
pub fn get_llm_system_prompt(lang: &str) -> &'static str {
|
||||
match lang {
|
||||
"ru" => LLM_SYSTEM_PROMPT_RU,
|
||||
"ru" | "ua" => LLM_SYSTEM_PROMPT_RU,
|
||||
_ => LLM_SYSTEM_PROMPT_RU,
|
||||
}
|
||||
}
|
||||
|
|
@ -258,6 +259,7 @@ pub fn get_llm_system_prompt(lang: &str) -> &'static str {
|
|||
pub fn get_wake_phrases(lang: &str) -> &'static [&'static str] {
|
||||
match lang {
|
||||
"ru" => &["джарвис", "джервис", "гарвис", "джарви", "гарви"],
|
||||
"ua" => &["джарвіс", "джервіс"],
|
||||
"en" => &["jarvis", "jervis"],
|
||||
_ => &["jarvis"],
|
||||
}
|
||||
|
|
@ -271,6 +273,11 @@ pub fn get_phrases_to_remove(lang: &str) -> &'static [&'static str] {
|
|||
"произнеси", "ответь", "покажи", "скажи", "давай",
|
||||
"да сэр", "к вашим услугам сэр", "загружаю сэр",
|
||||
],
|
||||
"ua" => &[
|
||||
"джарвіс", "джервіс", "сер", "слухаю сер", "завжди до послуг",
|
||||
"скажи", "покажи", "відповідай", "давай",
|
||||
"так сер", "до ваших послуг сер",
|
||||
],
|
||||
"en" => &[
|
||||
"jarvis", "jervis", "sir", "yes sir", "at your service",
|
||||
"please", "say", "show", "tell", "hey",
|
||||
|
|
@ -285,6 +292,10 @@ pub fn get_wake_grammar(lang: &str) -> &'static [&'static str] {
|
|||
"джарвис", "[unk]", "джон", "джони", "джей",
|
||||
"джонстон", "привет", "давай",
|
||||
],
|
||||
"ua" => &[
|
||||
"джарвіс", "[unk]", "джон", "джоні", "джей",
|
||||
"привіт", "давай",
|
||||
],
|
||||
"en" => &[
|
||||
"jarvis", "[unk]", "john", "johnny", "jay",
|
||||
"hello", "hey", "hi",
|
||||
|
|
|
|||
|
|
@ -8,21 +8,28 @@ use unic_langid::LanguageIdentifier;
|
|||
// locale files embedded at compile time
|
||||
const LOCALE_RU: &str = include_str!("i18n/locales/ru.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"];
|
||||
pub const SUPPORTED_LANGUAGES: &[&str] = &["ru", "en", "ua"];
|
||||
pub const DEFAULT_LANGUAGE: &str = "en";
|
||||
|
||||
// detect the OS language and map it to a supported language.
|
||||
// falls back to DEFAULT_LANGUAGE if not supported.
|
||||
pub fn detect_system_language() -> &'static str {
|
||||
if let Some(locale) = sys_locale::get_locale() {
|
||||
// locale can be "en-US", "ru-RU", etc.
|
||||
// locale can be "en-US", "ru-RU", "uk-UA", etc.
|
||||
let lang_code = locale.split(&['-', '_'][..]).next().unwrap_or("");
|
||||
|
||||
if SUPPORTED_LANGUAGES.contains(&lang_code) {
|
||||
info!("Detected system language: {} (from locale '{}')", lang_code, locale);
|
||||
// map OS locale codes to our supported languages
|
||||
let mapped = match lang_code {
|
||||
"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()
|
||||
.find(|&&l| l == lang_code)
|
||||
.find(|&&l| l == mapped)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +61,7 @@ fn create_bundles() -> HashMap<String, Bundle> {
|
|||
|
||||
bundles.insert("ru".to_string(), create_bundle("ru", LOCALE_RU));
|
||||
bundles.insert("en".to_string(), create_bundle("en", LOCALE_EN));
|
||||
bundles.insert("ua".to_string(), create_bundle("ua", LOCALE_UA));
|
||||
|
||||
bundles
|
||||
}
|
||||
|
|
@ -165,6 +173,7 @@ pub fn get_translations_for(lang: &str) -> HashMap<String, String> {
|
|||
let source = match lang {
|
||||
"ru" => LOCALE_RU,
|
||||
"en" => LOCALE_EN,
|
||||
"ua" => LOCALE_UA,
|
||||
_ => LOCALE_RU,
|
||||
};
|
||||
|
||||
|
|
|
|||
166
crates/jarvis-core/src/i18n/locales/ua.ftl
Normal file
166
crates/jarvis-core/src/i18n/locales/ua.ftl
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# ### 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 = Плагіни
|
||||
|
|
@ -1,304 +0,0 @@
|
|||
//! 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:00–07: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());
|
||||
}
|
||||
}
|
||||
|
|
@ -66,8 +66,6 @@ pub mod plugins;
|
|||
|
||||
pub mod wake_trainer;
|
||||
|
||||
pub mod idle_banter;
|
||||
|
||||
#[cfg(feature = "llm")]
|
||||
pub mod llm;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
use super::client::ChatMessage;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub struct ConversationHistory {
|
||||
system: Option<ChatMessage>,
|
||||
turns: Vec<ChatMessage>,
|
||||
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 {
|
||||
|
|
@ -18,7 +12,6 @@ impl ConversationHistory {
|
|||
system: Some(ChatMessage::system(system_prompt)),
|
||||
turns: Vec::new(),
|
||||
max_turns: max_turns.max(1),
|
||||
persist_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -27,42 +20,17 @@ impl ConversationHistory {
|
|||
system: None,
|
||||
turns: Vec::new(),
|
||||
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>) {
|
||||
self.turns.push(ChatMessage::user(content));
|
||||
self.truncate();
|
||||
self.persist();
|
||||
}
|
||||
|
||||
pub fn push_assistant(&mut self, content: impl Into<String>) {
|
||||
self.turns.push(ChatMessage::assistant(content));
|
||||
self.truncate();
|
||||
self.persist();
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Vec<ChatMessage> {
|
||||
|
|
@ -80,14 +48,11 @@ impl ConversationHistory {
|
|||
|
||||
pub fn clear(&mut self) {
|
||||
self.turns.clear();
|
||||
self.persist();
|
||||
}
|
||||
|
||||
pub fn pop_last_user(&mut self) -> Option<ChatMessage> {
|
||||
if matches!(self.turns.last(), Some(m) if m.role == "user") {
|
||||
let popped = self.turns.pop();
|
||||
self.persist();
|
||||
popped
|
||||
self.turns.pop()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -104,40 +69,6 @@ impl ConversationHistory {
|
|||
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)]
|
||||
|
|
@ -209,68 +140,4 @@ mod tests {
|
|||
assert_eq!(snap.len(), 1);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,19 +20,8 @@ use std::sync::Arc;
|
|||
|
||||
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) {
|
||||
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);
|
||||
*HISTORY.write() = Some(ConversationHistory::new(system_prompt, max_turns));
|
||||
}
|
||||
|
||||
pub fn history_push_user(content: impl Into<String>) {
|
||||
|
|
|
|||
|
|
@ -15,4 +15,3 @@ pub mod scheduler;
|
|||
pub mod cmd;
|
||||
pub mod health;
|
||||
pub mod macros;
|
||||
pub mod banter;
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
//! `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(())
|
||||
}
|
||||
|
|
@ -84,7 +84,6 @@ impl LuaEngine {
|
|||
api::cmd::register(&self.lua, &jarvis)?;
|
||||
api::health::register(&self.lua, &jarvis)?;
|
||||
api::macros::register(&self.lua, &jarvis)?;
|
||||
api::banter::register(&self.lua, &jarvis)?;
|
||||
|
||||
// sandbox-controlled APIs
|
||||
if self.sandbox.allows_http() {
|
||||
|
|
|
|||
|
|
@ -109,135 +109,4 @@ mod tests {
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -70,20 +70,6 @@ pub const ENV_LLM_ROUTER: &str = "JARVIS_LLM_ROUTER";
|
|||
/// Default: `0.55`. Lower → more matches but more false positives.
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Read an env var, returning `None` for unset or whitespace-only.
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ fn get_configured_model_path() -> Result<std::path::PathBuf, String> {
|
|||
let lang_code = match language.as_str() {
|
||||
"ru" => "ru",
|
||||
"en" => "us",
|
||||
"ua" => "uk",
|
||||
other => other,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
const languages = [
|
||||
{ code: "ru", label: "RU", flag: "🇷🇺", name: "Русский" },
|
||||
{ code: "en", label: "EN", flag: "🇬🇧", name: "English" },
|
||||
{ code: "ua", label: "UA", flag: "🇺🇦", name: "Українська" },
|
||||
]
|
||||
|
||||
// Profile chip — shows the active profile icon + name, click to swap.
|
||||
|
|
|
|||
|
|
@ -50,6 +50,6 @@ export async function getSupportedLanguages(): Promise<string[]> {
|
|||
try {
|
||||
return await invoke<string[]>("get_supported_languages")
|
||||
} catch {
|
||||
return ["ru", "en"]
|
||||
return ["ru", "en", "ua"]
|
||||
}
|
||||
}
|
||||
|
|
@ -235,6 +235,7 @@
|
|||
const languageNames: Record<string, string> = {
|
||||
us: 'English',
|
||||
ru: 'Русский',
|
||||
uk: 'Українська',
|
||||
de: 'German',
|
||||
fr: 'French',
|
||||
es: 'Spanish',
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ en = [
|
|||
"start browser",
|
||||
"i need browser",
|
||||
]
|
||||
ua = [
|
||||
"відкрий браузер",
|
||||
"запусти браузер",
|
||||
"відкрий хром",
|
||||
"запусти хром",
|
||||
"увімкни браузер",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -42,6 +49,10 @@ en = [
|
|||
"open notepad",
|
||||
"launch notepad",
|
||||
]
|
||||
ua = [
|
||||
"відкрий блокнот",
|
||||
"запусти блокнот",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -62,6 +73,10 @@ en = [
|
|||
"launch calculator",
|
||||
"calculator please",
|
||||
]
|
||||
ua = [
|
||||
"відкрий калькулятор",
|
||||
"запусти калькулятор",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -84,6 +99,10 @@ en = [
|
|||
"open files",
|
||||
"open folder",
|
||||
]
|
||||
ua = [
|
||||
"відкрий провідник",
|
||||
"відкрий папку",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -107,6 +126,10 @@ en = [
|
|||
"open console",
|
||||
"launch terminal",
|
||||
]
|
||||
ua = [
|
||||
"відкрий термінал",
|
||||
"відкрий консоль",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -128,6 +151,10 @@ en = [
|
|||
"launch settings",
|
||||
"windows settings",
|
||||
]
|
||||
ua = [
|
||||
"відкрий налаштування",
|
||||
"відкрий параметри",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -148,6 +175,10 @@ en = [
|
|||
"launch task manager",
|
||||
"task manager",
|
||||
]
|
||||
ua = [
|
||||
"відкрий диспетчер задач",
|
||||
"диспетчер задач",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -169,6 +200,10 @@ en = [
|
|||
"lock the computer",
|
||||
"lock pc",
|
||||
]
|
||||
ua = [
|
||||
"заблокуй комп'ютер",
|
||||
"заблокуй екран",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -189,3 +224,7 @@ en = [
|
|||
"screenshot",
|
||||
"capture screen",
|
||||
]
|
||||
ua = [
|
||||
"зроби скріншот",
|
||||
"знімок екрана",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ ru = [
|
|||
"выгрузи настройки",
|
||||
]
|
||||
en = ["backup", "export settings"]
|
||||
ua = ["зроби бекап", "експортуй налаштування"]
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
# 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",
|
||||
]
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
-- 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 }
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
jarvis.banter.pause()
|
||||
return jarvis.cmd.ok(
|
||||
jarvis.context.language == "ru"
|
||||
and "Молчу, сэр."
|
||||
or "Quiet, sir."
|
||||
)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
jarvis.banter.resume()
|
||||
return jarvis.cmd.ok(
|
||||
jarvis.context.language == "ru"
|
||||
and "Хорошо, сэр. Возвращаюсь к комментариям."
|
||||
or "Very well, sir. Resuming remarks."
|
||||
)
|
||||
|
|
@ -8,6 +8,7 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["ярче", "сделай ярче", "увеличь яркость", "поярче"]
|
||||
en = ["brighter", "increase brightness"]
|
||||
ua = ["яскравіше", "збільш яскравість"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -20,6 +21,7 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["темнее", "сделай темнее", "уменьши яркость", "потемнее"]
|
||||
en = ["darker", "decrease brightness"]
|
||||
ua = ["темніше", "зменш яскравість"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -32,6 +34,7 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["максимальная яркость", "яркость на максимум"]
|
||||
en = ["max brightness", "brightness max"]
|
||||
ua = ["максимальна яскравість"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -44,3 +47,4 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["минимальная яркость", "яркость на минимум"]
|
||||
en = ["min brightness", "brightness min"]
|
||||
ua = ["мінімальна яскравість"]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ ru = [
|
|||
"запомни что в буфере",
|
||||
]
|
||||
en = ["save clipboard", "remember clipboard"]
|
||||
ua = ["запам'ятай буфер"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -32,6 +33,7 @@ ru = [
|
|||
"что было в буфере",
|
||||
]
|
||||
en = ["clipboard history", "what did I copy"]
|
||||
ua = ["що я копіював"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -50,3 +52,4 @@ ru = [
|
|||
"вставь старый буфер",
|
||||
]
|
||||
en = ["restore clipboard", "paste old clipboard"]
|
||||
ua = ["поверни буфер"]
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@ timeout = 10000
|
|||
[commands.phrases]
|
||||
ru = ["прочитай буфер", "что в буфере", "прочитай из буфера", "озвучь буфер"]
|
||||
en = ["read clipboard", "what is in clipboard", "speak clipboard"]
|
||||
ua = ["прочитай буфер", "що в буфері"]
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ ru = [
|
|||
"проект сейчас",
|
||||
]
|
||||
en = ["set codebase", "set project folder", "use project"]
|
||||
ua = ["вкажи проект", "вкажи папку проекту"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -39,6 +40,7 @@ ru = [
|
|||
"объясни код",
|
||||
]
|
||||
en = ["ask the code", "ask the codebase", "what does the function", "explain the code"]
|
||||
ua = ["що в коді", "що робить функція"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -56,3 +58,4 @@ ru = [
|
|||
"какая кодовая база",
|
||||
]
|
||||
en = ["which project", "what's the codebase"]
|
||||
ua = ["який проект"]
|
||||
|
|
|
|||
|
|
@ -20,3 +20,8 @@ en = [
|
|||
"make script",
|
||||
"write script",
|
||||
]
|
||||
ua = [
|
||||
"напиши код",
|
||||
"згенеруй код",
|
||||
"зроби скрипт",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -21,3 +21,7 @@ en = [
|
|||
"crypto price",
|
||||
"btc price",
|
||||
]
|
||||
ua = [
|
||||
"ціна біткоїна",
|
||||
"курс біткоїна",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ en = [
|
|||
"currency rate",
|
||||
"what is dollar",
|
||||
]
|
||||
ua = [
|
||||
"курс долара",
|
||||
"курс євро",
|
||||
"що з доларом",
|
||||
]
|
||||
|
||||
|
||||
# Convert N units of one currency to another, e.g.
|
||||
|
|
@ -51,3 +56,7 @@ en = [
|
|||
"dollars in rubles",
|
||||
"euros in rubles",
|
||||
]
|
||||
ua = [
|
||||
"переведи",
|
||||
"конвертуй",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ ru = [
|
|||
"сделай утренний брифинг",
|
||||
]
|
||||
en = ["set up morning briefing", "enable morning briefing", "daily briefing"]
|
||||
ua = ["налаштуй ранковий брифінг", "увімкни ранковий брифінг"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -33,6 +34,7 @@ ru = [
|
|||
"сводка дня",
|
||||
]
|
||||
en = ["morning briefing", "give me a briefing", "what's new today"]
|
||||
ua = ["ранковий брифінг", "брифінг зараз"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -49,3 +51,4 @@ ru = [
|
|||
"отмени брифинг",
|
||||
]
|
||||
en = ["disable morning briefing", "cancel morning briefing"]
|
||||
ua = ["вимкни ранковий брифінг"]
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ ru = [
|
|||
"цитата",
|
||||
]
|
||||
en = ["quote of the day", "motivational quote", "inspire me"]
|
||||
ua = ["цитата дня", "надихни мене"]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ ru = [
|
|||
"когда",
|
||||
]
|
||||
en = ["how many days until", "days until"]
|
||||
ua = ["скільки днів до"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -33,3 +34,4 @@ ru = [
|
|||
"сегодня какой",
|
||||
]
|
||||
en = ["what day is today", "day of week"]
|
||||
ua = ["який сьогодні день тижня"]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["какой сегодня день", "какое сегодня число", "сегодня какое число", "что сегодня"]
|
||||
en = ["what day is today", "what's today", "today date"]
|
||||
ua = ["який сьогодні день", "яке сьогодні число"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -20,6 +21,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["какой день завтра", "что завтра", "завтра какое число", "какое число завтра"]
|
||||
en = ["what day is tomorrow", "what's tomorrow", "tomorrow date"]
|
||||
ua = ["який день завтра", "яке число завтра"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -32,3 +34,4 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["какой был вчера день", "что было вчера", "вчера какое число было"]
|
||||
en = ["what day was yesterday", "yesterday date"]
|
||||
ua = ["який був вчора день"]
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
-- 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 1–3 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))
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# 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",
|
||||
]
|
||||
|
|
@ -18,3 +18,4 @@ ru = [
|
|||
"доложи о себе",
|
||||
]
|
||||
en = ["diagnostics", "status report", "health check"]
|
||||
ua = ["діагностика", "стан системи"]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["подбрось монетку", "брось монетку", "орёл или решка", "орел или решка"]
|
||||
en = ["flip a coin", "toss a coin", "heads or tails"]
|
||||
ua = ["підкинь монетку", "орел чи решка"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -20,6 +21,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["брось кубик", "подбрось кубик", "кинь кубик"]
|
||||
en = ["roll a dice", "roll the dice"]
|
||||
ua = ["кинь кубик"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -32,3 +34,4 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["случайное число", "выбери случайное число", "рандомное число"]
|
||||
en = ["random number", "pick a random number"]
|
||||
ua = ["випадкове число"]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ ru = [
|
|||
"свободное место",
|
||||
]
|
||||
en = ["disk space", "free space", "how much disk"]
|
||||
ua = ["скільки місця на диску"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -32,3 +33,4 @@ ru = [
|
|||
"перечень дисков",
|
||||
]
|
||||
en = ["list drives", "what drives do I have"]
|
||||
ua = ["які диски"]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ ru = [
|
|||
"проверка эхо",
|
||||
]
|
||||
en = ["repeat after me", "echo"]
|
||||
ua = ["повтори за мною", "ехо"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -31,3 +32,4 @@ ru = [
|
|||
"что ты услышал",
|
||||
]
|
||||
en = ["what did i say", "what did you hear"]
|
||||
ua = ["що я сказав"]
|
||||
|
|
|
|||
|
|
@ -19,3 +19,8 @@ en = [
|
|||
"where is file",
|
||||
"locate file",
|
||||
]
|
||||
ua = [
|
||||
"знайди файл",
|
||||
"пошук файлу",
|
||||
"де файл",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ timeout = 15000
|
|||
[commands.phrases]
|
||||
ru = ["расскажи шутку", "пошути", "анекдот", "рассмеши меня"]
|
||||
en = ["tell a joke", "make me laugh", "joke"]
|
||||
ua = ["розкажи жарт", "пожартуй"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -20,6 +21,7 @@ timeout = 15000
|
|||
[commands.phrases]
|
||||
ru = ["расскажи факт", "интересный факт", "поделись фактом"]
|
||||
en = ["tell a fact", "interesting fact"]
|
||||
ua = ["цікавий факт"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -32,6 +34,7 @@ timeout = 15000
|
|||
[commands.phrases]
|
||||
ru = ["вдохнови меня", "цитата дня", "мотивируй", "процитируй кого-нибудь"]
|
||||
en = ["inspire me", "quote of the day", "motivate me"]
|
||||
ua = ["надихни", "цитата дня"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -44,3 +47,4 @@ timeout = 15000
|
|||
[commands.phrases]
|
||||
ru = ["сделай комплимент", "похвали меня"]
|
||||
en = ["compliment me", "say something nice"]
|
||||
ua = ["зроби комплімент"]
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ en = [
|
|||
"launch dota",
|
||||
"launch cs",
|
||||
]
|
||||
ua = [
|
||||
"запусти гру",
|
||||
"увімкни гру",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -34,3 +38,4 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["какие у меня игры", "список игр", "покажи игры"]
|
||||
en = ["list games", "show games"]
|
||||
ua = ["які в мене ігри"]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ ru = [
|
|||
"монетка",
|
||||
]
|
||||
en = ["flip a coin", "coin flip", "heads or tails"]
|
||||
ua = ["підкинь монетку"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -32,6 +33,7 @@ ru = [
|
|||
"сгенерируй надёжный пароль",
|
||||
]
|
||||
en = ["generate password", "make a password"]
|
||||
ua = ["згенеруй пароль"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -49,3 +51,4 @@ ru = [
|
|||
"уникальный идентификатор",
|
||||
]
|
||||
en = ["generate uuid", "uuid"]
|
||||
ua = ["згенеруй юід"]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ ru = [
|
|||
"тикеты",
|
||||
]
|
||||
en = ["list issues", "open issues", "what's in issues"]
|
||||
ua = ["які тікети", "відкриті issues"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -33,3 +34,4 @@ ru = [
|
|||
"что мне назначено",
|
||||
]
|
||||
en = ["my issues", "issues assigned to me"]
|
||||
ua = ["мої тікети"]
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ en = [
|
|||
"what's in the prs",
|
||||
"open pull requests",
|
||||
]
|
||||
ua = [
|
||||
"які піари",
|
||||
"відкриті пр",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -46,6 +50,9 @@ en = [
|
|||
"explain the latest pr",
|
||||
"summarize pr",
|
||||
]
|
||||
ua = [
|
||||
"розбери останній пр",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -63,3 +70,4 @@ ru = [
|
|||
"репозиторий",
|
||||
]
|
||||
en = ["set repo", "current repo"]
|
||||
ua = ["встанови репо"]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ ru = [
|
|||
"включи напоминание про воду",
|
||||
]
|
||||
en = ["remind me to drink water", "water reminder"]
|
||||
ua = ["нагадуй мені пити воду"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -31,6 +32,7 @@ ru = [
|
|||
"напоминай вставать",
|
||||
]
|
||||
en = ["remind me to stretch", "stretch reminder"]
|
||||
ua = ["нагадуй мені розім'ятися"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -47,6 +49,7 @@ ru = [
|
|||
"20 20 20",
|
||||
]
|
||||
en = ["remind me to rest eyes", "eye reminder"]
|
||||
ua = ["нагадуй про очі"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -63,3 +66,4 @@ ru = [
|
|||
"выключи все напоминалки",
|
||||
]
|
||||
en = ["stop all habits", "disable habit reminders"]
|
||||
ua = ["вимкни всі звички"]
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ ru = [
|
|||
"трекинг привычек",
|
||||
]
|
||||
en = ["habit streaks", "my habits stats"]
|
||||
ua = ["моя статистика звичок"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -37,3 +38,4 @@ ru = [
|
|||
"я отдохнул глазам",
|
||||
]
|
||||
en = ["check in habit", "I did the habit"]
|
||||
ua = ["я виконав звичку"]
|
||||
|
|
|
|||
|
|
@ -20,3 +20,9 @@ en = [
|
|||
"help",
|
||||
"show commands",
|
||||
]
|
||||
ua = [
|
||||
"що ти вмієш",
|
||||
"які команди",
|
||||
"список команд",
|
||||
"допоможи",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ ru = [
|
|||
"новая встреча",
|
||||
]
|
||||
en = ["add meeting", "create event"]
|
||||
ua = ["додай зустріч"]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ ru = [
|
|||
"расскажи факт",
|
||||
]
|
||||
en = ["tell me an interesting fact", "fun fact about", "surprise me"]
|
||||
ua = ["цікавий факт"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -34,3 +35,4 @@ ru = [
|
|||
"что случилось",
|
||||
]
|
||||
en = ["this day in history", "what happened today"]
|
||||
ua = ["що сталося в цей день"]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ ru = [
|
|||
"научи меня",
|
||||
]
|
||||
en = ["what can you do", "help", "capabilities", "how do I use you"]
|
||||
ua = ["що ти вмієш", "допомога"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -39,6 +40,7 @@ ru = [
|
|||
"о тебе",
|
||||
]
|
||||
en = ["who are you", "tell me about yourself", "about you"]
|
||||
ua = ["хто ти такий", "розкажи про себе"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -55,3 +57,4 @@ ru = [
|
|||
"сколько ты умеешь команд",
|
||||
]
|
||||
en = ["how many commands", "command count"]
|
||||
ua = ["скільки команд"]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ ru = [
|
|||
"очисти контекст",
|
||||
]
|
||||
en = ["reset context", "forget conversation", "start fresh"]
|
||||
ua = ["скинь контекст", "забудь розмову"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -35,3 +36,4 @@ ru = [
|
|||
"повтори",
|
||||
]
|
||||
en = ["repeat", "say it again", "repeat the last answer"]
|
||||
ua = ["повтори", "повтори що сказав"]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ ru = [
|
|||
"используй оллама",
|
||||
]
|
||||
en = ["switch to local", "use local llm", "use ollama"]
|
||||
ua = ["перейди на локальний", "використовуй локальний"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -38,6 +39,7 @@ ru = [
|
|||
"перейди на облако",
|
||||
]
|
||||
en = ["switch to cloud", "use cloud llm", "use groq"]
|
||||
ua = ["перейди на хмару", "використовуй хмару"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -57,3 +59,4 @@ ru = [
|
|||
"что за модель",
|
||||
]
|
||||
en = ["which llm", "are you local or cloud", "what model"]
|
||||
ua = ["який ллм", "хмара чи локально"]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ ru = [
|
|||
"новый макрос",
|
||||
]
|
||||
en = ["record macro", "start macro recording"]
|
||||
ua = ["запиши макрос"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -33,6 +34,7 @@ ru = [
|
|||
"макрос готов",
|
||||
]
|
||||
en = ["save macro", "stop macro", "finish macro"]
|
||||
ua = ["збережи макрос", "стоп макрос"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -50,6 +52,7 @@ ru = [
|
|||
"забудь макрос",
|
||||
]
|
||||
en = ["cancel macro", "discard macro"]
|
||||
ua = ["скасуй макрос"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -68,6 +71,7 @@ ru = [
|
|||
"проиграй макрос",
|
||||
]
|
||||
en = ["play macro", "replay macro", "run macro"]
|
||||
ua = ["запусти макрос"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -84,6 +88,7 @@ ru = [
|
|||
"покажи макросы",
|
||||
]
|
||||
en = ["list macros", "show macros"]
|
||||
ua = ["список макросів"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -99,3 +104,4 @@ ru = [
|
|||
"забудь о макросе",
|
||||
]
|
||||
en = ["delete macro", "remove macro"]
|
||||
ua = ["видали макрос"]
|
||||
|
|
|
|||
|
|
@ -17,3 +17,4 @@ ru = [
|
|||
"предсказание",
|
||||
]
|
||||
en = ["yes or no", "magic 8-ball", "tell me yes or no"]
|
||||
ua = ["скажи так чи ні"]
|
||||
|
|
|
|||
|
|
@ -15,3 +15,4 @@ ru = [
|
|||
"составить письмо",
|
||||
]
|
||||
en = ["compose email", "new email", "open mail"]
|
||||
ua = ["напиши лист", "новий лист"]
|
||||
|
|
|
|||
|
|
@ -20,3 +20,8 @@ en = [
|
|||
"solve",
|
||||
"what is",
|
||||
]
|
||||
ua = [
|
||||
"порахуй",
|
||||
"скільки буде",
|
||||
"обчисли",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,246 +1,40 @@
|
|||
-- 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 raw = (jarvis.context.phrase or ""):lower()
|
||||
local phrase = (jarvis.context.phrase or ""):lower()
|
||||
|
||||
local triggers = {
|
||||
"сколько будет", "сколько это", "посчитай", "вычисли", "реши",
|
||||
"how much is", "calculate", "compute", "solve", "what is",
|
||||
"скільки буде", "порахуй", "обчисли",
|
||||
}
|
||||
local query = jarvis.text.strip_trigger(raw, triggers)
|
||||
if not query or query == "" then query = raw end
|
||||
|
||||
local query = phrase
|
||||
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+$", "")
|
||||
|
||||
if query == "" then
|
||||
return jarvis.cmd.not_found(lang == "ru" and "Что посчитать?" or "What to compute?")
|
||||
jarvis.system.notify("Math", lang == "ru" and "Что посчитать?" or "What to compute?")
|
||||
jarvis.audio.play_error()
|
||||
return { chain = false }
|
||||
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")
|
||||
if not token or token == "" then
|
||||
return jarvis.cmd.error(lang == "ru"
|
||||
and "Не понял выражение, и GROQ_TOKEN не задан."
|
||||
or "Couldn't parse, and GROQ_TOKEN unset.")
|
||||
jarvis.system.notify("Math", "GROQ_TOKEN не задан")
|
||||
jarvis.audio.play_error()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local base = jarvis.system.env("GROQ_BASE_URL")
|
||||
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 base = jarvis.system.env("GROQ_BASE_URL"); 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 payload = {
|
||||
|
|
@ -253,20 +47,41 @@ local payload = {
|
|||
temperature = 0.0,
|
||||
}
|
||||
|
||||
jarvis.log("info", "math fallback to LLM: " .. query)
|
||||
jarvis.log("info", "math query: " .. query)
|
||||
local res = jarvis.http.post_json(base .. "/chat/completions", payload, { Authorization = "Bearer " .. token })
|
||||
|
||||
if not res.ok then
|
||||
return jarvis.cmd.error("Ошибка API: " .. tostring(res.status))
|
||||
jarvis.log("error", "math api failed: " .. tostring(res.status))
|
||||
jarvis.system.notify("Math", "Ошибка API")
|
||||
jarvis.audio.play_error()
|
||||
return { chain = false }
|
||||
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
|
||||
return jarvis.cmd.error(lang == "ru" and "Не смог распарсить ответ" or "Couldn't parse response")
|
||||
jarvis.system.notify("Math", "Не смог распарсить ответ")
|
||||
jarvis.audio.play_error()
|
||||
return { chain = false }
|
||||
end
|
||||
content = content:gsub('\\n', '\n'):gsub('\\"', '"'):gsub('\\\\', '\\'):gsub('\\t', '\t')
|
||||
content = content:gsub("^%s+", ""):gsub("%s+$", "")
|
||||
|
||||
if content:lower() == "нет" or content == "" then
|
||||
return jarvis.cmd.not_found(lang == "ru" and "Это не математика" or "Not math")
|
||||
jarvis.system.notify("Math", lang == "ru" and "Это не математика" or "Not math")
|
||||
jarvis.audio.play_not_found()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local prefix = (lang == "ru" and "Получилось " or "Result: ")
|
||||
return jarvis.cmd.ok(prefix .. content)
|
||||
jarvis.system.notify(query, content)
|
||||
jarvis.log("info", "math result: " .. 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 }
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ en = [
|
|||
"resume",
|
||||
"play music",
|
||||
]
|
||||
ua = [
|
||||
"пауза",
|
||||
"продовж",
|
||||
"включи музику",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -43,6 +48,11 @@ en = [
|
|||
"skip",
|
||||
"next",
|
||||
]
|
||||
ua = [
|
||||
"наступний трек",
|
||||
"наступна пісня",
|
||||
"далі",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -64,6 +74,10 @@ en = [
|
|||
"previous song",
|
||||
"back",
|
||||
]
|
||||
ua = [
|
||||
"попередній трек",
|
||||
"назад",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -83,3 +97,7 @@ en = [
|
|||
"stop music",
|
||||
"stop playback",
|
||||
]
|
||||
ua = [
|
||||
"стоп музика",
|
||||
"вимкни музику",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ ru = [
|
|||
"снова музыку",
|
||||
]
|
||||
en = ["pause", "play", "resume music", "stop music"]
|
||||
ua = ["пауза", "продовжи"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -40,6 +41,7 @@ ru = [
|
|||
"next",
|
||||
]
|
||||
en = ["next track", "next song", "skip"]
|
||||
ua = ["наступний трек"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -59,6 +61,7 @@ ru = [
|
|||
"previous",
|
||||
]
|
||||
en = ["previous track", "previous song", "back"]
|
||||
ua = ["попередній трек"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -74,3 +77,4 @@ ru = [
|
|||
"останови плеер",
|
||||
]
|
||||
en = ["stop", "stop player"]
|
||||
ua = ["зупини плеєр"]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ ru = [
|
|||
"запомни про меня",
|
||||
]
|
||||
en = ["remember that", "remember about me", "memorize"]
|
||||
ua = ["запам'ятай що", "запам'ятай про мене"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -29,6 +30,7 @@ ru = [
|
|||
"что помнишь про", "вспомни",
|
||||
]
|
||||
en = ["what do you remember about", "recall"]
|
||||
ua = ["що ти пам'ятаєш про"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -41,6 +43,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["забудь про", "забудь что", "забудь обо мне"]
|
||||
en = ["forget about", "forget that"]
|
||||
ua = ["забудь про"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -53,3 +56,4 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["что ты помнишь", "что ты знаешь обо мне", "покажи память"]
|
||||
en = ["what do you remember", "list memory", "show memory"]
|
||||
ua = ["що ти пам'ятаєш"]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ ru = [
|
|||
"мне сейчас",
|
||||
]
|
||||
en = ["log mood", "my mood is", "I feel"]
|
||||
ua = ["запиши настрій", "мій настрій"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -35,3 +36,4 @@ ru = [
|
|||
"что я писал про настроение",
|
||||
]
|
||||
en = ["mood recap", "weekly mood summary", "how was my week"]
|
||||
ua = ["підсумок настрою"]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["клик", "кликни", "нажми мышкой"]
|
||||
en = ["click", "left click"]
|
||||
ua = ["клік", "клікни"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -20,6 +21,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["правый клик", "клик правой", "правой кнопкой"]
|
||||
en = ["right click"]
|
||||
ua = ["правий клік"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -32,6 +34,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["двойной клик", "дабл клик", "двойное нажатие"]
|
||||
en = ["double click"]
|
||||
ua = ["подвійний клік"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -44,6 +47,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["промотай вниз", "прокрути вниз", "вниз промотай"]
|
||||
en = ["scroll down"]
|
||||
ua = ["прокрути вниз"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -56,3 +60,4 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["промотай вверх", "прокрути вверх", "наверх промотай"]
|
||||
en = ["scroll up"]
|
||||
ua = ["прокрути вгору"]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ ru = [
|
|||
"имя сети",
|
||||
]
|
||||
en = ["what wifi", "what network", "wifi name", "ssid"]
|
||||
ua = ["до якої мережі"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -34,6 +35,7 @@ ru = [
|
|||
"локальный адрес",
|
||||
]
|
||||
en = ["my ip", "local ip", "ip address"]
|
||||
ua = ["моя айпі", "локальна адреса"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -51,3 +53,4 @@ ru = [
|
|||
"какой у меня внешний",
|
||||
]
|
||||
en = ["public ip", "external ip", "what's my ip online"]
|
||||
ua = ["зовнішня айпі"]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["настройки wifi", "настройки вайфай", "wifi настройки", "сеть"]
|
||||
en = ["wifi settings", "open wifi"]
|
||||
ua = ["налаштування wi-fi"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -20,6 +21,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["настройки блютуз", "блютуз", "включи блютуз", "выключи блютуз"]
|
||||
en = ["bluetooth settings", "open bluetooth", "toggle bluetooth"]
|
||||
ua = ["налаштування bluetooth"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -32,3 +34,4 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["мой ай пи", "мой ip", "какой у меня айпи", "айпишник"]
|
||||
en = ["my ip", "what is my ip"]
|
||||
ua = ["мій ip"]
|
||||
|
|
|
|||
|
|
@ -20,3 +20,8 @@ en = [
|
|||
"headlines",
|
||||
"top news",
|
||||
]
|
||||
ua = [
|
||||
"що нового",
|
||||
"новини",
|
||||
"останні новини",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["запиши", "запиши заметку", "запиши идею", "запомни", "запомни идею", "сделай заметку"]
|
||||
en = ["write down", "take a note", "remember this", "note down"]
|
||||
ua = ["запиши", "запиши ідею", "зроби нотатку"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -20,3 +21,4 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["покажи заметки", "открой заметки", "что я записал"]
|
||||
en = ["show notes", "open notes"]
|
||||
ua = ["покажи нотатки", "відкрий нотатки"]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ ru = [
|
|||
"что нового",
|
||||
]
|
||||
en = ["what did I miss", "missed notifications"]
|
||||
ua = ["що я пропустив"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -33,3 +34,4 @@ ru = [
|
|||
"сбрось пропущенное",
|
||||
]
|
||||
en = ["clear notifications"]
|
||||
ua = ["очисти сповіщення"]
|
||||
|
|
|
|||
|
|
@ -21,3 +21,4 @@ ru = [
|
|||
"что слушаю",
|
||||
]
|
||||
en = ["what's playing", "current song", "now playing"]
|
||||
ua = ["що грає", "що зараз грає"]
|
||||
|
|
|
|||
|
|
@ -19,3 +19,8 @@ en = [
|
|||
"ocr screen",
|
||||
"extract text from screen",
|
||||
]
|
||||
ua = [
|
||||
"прочитай екран",
|
||||
"що на екрані",
|
||||
"розпізнай текст з екрана",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ en = [
|
|||
"list speakers",
|
||||
"what audio devices",
|
||||
]
|
||||
ua = [
|
||||
"які пристрої виводу",
|
||||
"покажи пристрої",
|
||||
"список пристроїв",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -43,3 +48,8 @@ en = [
|
|||
"switch output",
|
||||
"switch speakers",
|
||||
]
|
||||
ua = [
|
||||
"перемкни пристрій",
|
||||
"перемкни звук",
|
||||
"наступний пристрій",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ ru = [
|
|||
"пароль для",
|
||||
]
|
||||
en = ["save password for", "remember password for"]
|
||||
ua = ["запам'ятай пароль від"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -31,6 +32,7 @@ ru = [
|
|||
"достань пароль",
|
||||
]
|
||||
en = ["password for", "get password"]
|
||||
ua = ["пароль від"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -47,3 +49,4 @@ ru = [
|
|||
"что в хранилище паролей",
|
||||
]
|
||||
en = ["list passwords", "what's in the vault"]
|
||||
ua = ["список паролів"]
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
# 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",
|
||||
]
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
-- 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])
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
-- 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])
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
-- 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])
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
-- 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])
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
-- 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])
|
||||
|
|
@ -16,6 +16,7 @@ ru = [
|
|||
"помодоро",
|
||||
]
|
||||
en = ["start pomodoro", "begin pomodoro", "pomodoro"]
|
||||
ua = ["запусти помодоро", "увімкни помодоро"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -33,3 +34,4 @@ ru = [
|
|||
"хватит помодоро",
|
||||
]
|
||||
en = ["stop pomodoro", "cancel pomodoro"]
|
||||
ua = ["зупини помодоро", "вимкни помодоро"]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["выключи компьютер", "выключи пк", "выключи комп"]
|
||||
en = ["shutdown pc", "shut down computer"]
|
||||
ua = ["вимкни комп'ютер", "вимкни пк"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -20,6 +21,7 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["перезагрузи компьютер", "перезагрузи пк", "ребутни", "ребут"]
|
||||
en = ["restart pc", "reboot computer"]
|
||||
ua = ["перезавантаж комп'ютер"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -32,6 +34,7 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["усыпи компьютер", "усыпи пк", "режим сна", "перейди в сон"]
|
||||
en = ["sleep pc", "put computer to sleep"]
|
||||
ua = ["приспи комп'ютер"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -44,6 +47,7 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["гибернация", "переведи в гибернацию", "режим гибернации"]
|
||||
en = ["hibernate pc"]
|
||||
ua = ["гібернація"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -56,6 +60,7 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["выйди из системы", "разлогинь", "выйди из учётки", "сеанс закончи"]
|
||||
en = ["log off", "sign out"]
|
||||
ua = ["вийди з системи"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -68,3 +73,4 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["отмени выключение", "отмени ребут", "отмени перезагрузку", "стой не выключай"]
|
||||
en = ["cancel shutdown", "abort shutdown"]
|
||||
ua = ["скасуй вимкнення"]
|
||||
|
|
|
|||
|
|
@ -18,3 +18,8 @@ en = [
|
|||
"close program",
|
||||
"terminate process",
|
||||
]
|
||||
ua = [
|
||||
"вбий процес",
|
||||
"закрий програму",
|
||||
"заверши процес",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["режим работа", "включи режим работы", "рабочий режим", "переключись в работу"]
|
||||
en = ["work mode", "switch to work mode", "enable work mode"]
|
||||
ua = ["робочий режим", "режим роботи"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -22,6 +23,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["режим игра", "игровой режим", "включи игровой режим", "переключись в игры"]
|
||||
en = ["game mode", "gaming mode", "switch to game"]
|
||||
ua = ["ігровий режим", "режим гри"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -34,6 +36,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["режим сон", "ночной режим", "режим тишины", "иди спать", "выключи всё"]
|
||||
en = ["sleep mode", "night mode", "quiet mode"]
|
||||
ua = ["нічний режим", "режим сну"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -46,6 +49,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["режим за рулём", "режим вождения", "я за рулём", "поехали"]
|
||||
en = ["driving mode", "in the car"]
|
||||
ua = ["режим водіння"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -58,6 +62,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["обычный режим", "режим по умолчанию", "сбрось режим", "вернись в обычный"]
|
||||
en = ["normal mode", "default mode", "reset mode"]
|
||||
ua = ["звичайний режим"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -70,3 +75,4 @@ timeout = 2000
|
|||
[commands.phrases]
|
||||
ru = ["какой сейчас режим", "в каком ты режиме", "текущий режим", "какой режим"]
|
||||
en = ["what mode", "current mode", "what profile"]
|
||||
ua = ["який зараз режим"]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ en = [
|
|||
"launch project",
|
||||
"project",
|
||||
]
|
||||
ua = [
|
||||
"відкрий проект",
|
||||
"запусти проект",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -29,3 +33,4 @@ timeout = 5000
|
|||
[commands.phrases]
|
||||
ru = ["какие у меня проекты", "список проектов", "покажи проекты"]
|
||||
en = ["list projects", "show projects"]
|
||||
ua = ["які в мене проекти", "перелік проектів"]
|
||||
|
|
|
|||
|
|
@ -25,3 +25,7 @@ en = [
|
|||
"find online",
|
||||
"look up",
|
||||
]
|
||||
ua = [
|
||||
"знайди в гуглі",
|
||||
"погугли",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -17,3 +17,7 @@ en = [
|
|||
"choose from",
|
||||
"decide between",
|
||||
]
|
||||
ua = [
|
||||
"обери з",
|
||||
"вибери",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -20,3 +20,8 @@ en = [
|
|||
"set timer for",
|
||||
"timer for",
|
||||
]
|
||||
ua = [
|
||||
"нагадай через",
|
||||
"постав нагадування через",
|
||||
"таймер на",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ ru = [
|
|||
"добавь ленту",
|
||||
]
|
||||
en = ["add rss", "subscribe to"]
|
||||
ua = ["додай rss"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -33,6 +34,7 @@ ru = [
|
|||
"новые статьи",
|
||||
]
|
||||
en = ["what's in feed", "read rss", "latest articles"]
|
||||
ua = ["що в стрічці"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -49,3 +51,4 @@ ru = [
|
|||
"мои подписки",
|
||||
]
|
||||
en = ["list feeds", "my feeds"]
|
||||
ua = ["мої стрічки"]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ ru = [
|
|||
"разбуди через",
|
||||
]
|
||||
en = ["remind me in", "set a reminder in"]
|
||||
ua = ["нагадай через", "постав нагадування через"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -31,6 +32,7 @@ ru = [
|
|||
"поставь напоминалку на",
|
||||
]
|
||||
en = ["remind me at", "wake me at"]
|
||||
ua = ["нагадай о", "розбуди о"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -49,6 +51,7 @@ ru = [
|
|||
"напоминай каждый",
|
||||
]
|
||||
en = ["every", "remind me every"]
|
||||
ua = ["кожні", "нагадуй кожні"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -66,6 +69,7 @@ ru = [
|
|||
"по утрам в",
|
||||
]
|
||||
en = ["every day at", "daily at"]
|
||||
ua = ["щодня о", "кожен день о"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -84,6 +88,7 @@ ru = [
|
|||
"что в расписании",
|
||||
]
|
||||
en = ["what's on my schedule", "show schedule", "list reminders"]
|
||||
ua = ["що в розкладі", "покажи розклад"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -101,6 +106,7 @@ ru = [
|
|||
"сбрось расписание",
|
||||
]
|
||||
en = ["clear schedule", "cancel all reminders"]
|
||||
ua = ["очисти розклад", "скасуй всі нагадування"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -123,3 +129,6 @@ en = [
|
|||
"cancel reminder about",
|
||||
"remove task about",
|
||||
]
|
||||
ua = [
|
||||
"скасуй нагадування про",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ ru = [
|
|||
"захвати экран",
|
||||
]
|
||||
en = ["take a screenshot", "screenshot", "capture screen"]
|
||||
ua = ["зроби скрін", "захопи екран"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -34,3 +35,4 @@ ru = [
|
|||
"сохрани экран",
|
||||
]
|
||||
en = ["save screenshot", "screenshot to file"]
|
||||
ua = ["збережи скрін"]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ ru = [
|
|||
"проверка связи",
|
||||
]
|
||||
en = ["are you there", "ping", "can you hear me"]
|
||||
ua = ["ти тут", "ти чуєш"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -35,3 +36,4 @@ ru = [
|
|||
"проверка всех систем",
|
||||
]
|
||||
en = ["self check", "smoke test", "check yourself"]
|
||||
ua = ["перевір себе"]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ ru = [
|
|||
"пауза музыки через",
|
||||
]
|
||||
en = ["pause music in", "pause in"]
|
||||
ua = ["пауза через"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -33,6 +34,7 @@ ru = [
|
|||
"ложусь спать через",
|
||||
]
|
||||
en = ["shutdown in", "sleep timer"]
|
||||
ua = ["вимкни комп'ютер через"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -51,3 +53,4 @@ ru = [
|
|||
"не выключай пк",
|
||||
]
|
||||
en = ["cancel sleep timer", "cancel shutdown"]
|
||||
ua = ["скасуй таймер вимкнення"]
|
||||
|
|
|
|||
|
|
@ -17,3 +17,7 @@ en = [
|
|||
"sound panel",
|
||||
"microphone settings",
|
||||
]
|
||||
ua = [
|
||||
"відкрий налаштування звуку",
|
||||
"панель звуку",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,3 +16,6 @@ en = [
|
|||
"spell out",
|
||||
"spell it",
|
||||
]
|
||||
ua = [
|
||||
"вимов по буквах",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ ru = [
|
|||
"сертификат сайта",
|
||||
]
|
||||
en = ["check ssl", "check tls", "check certificate"]
|
||||
ua = ["перевір сертифікат"]
|
||||
|
|
|
|||
|
|
@ -29,3 +29,7 @@ en = [
|
|||
"share price",
|
||||
"quote for",
|
||||
]
|
||||
ua = [
|
||||
"ціна акцій",
|
||||
"котирування",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["засеки время", "запусти секундомер", "старт секундомер", "включи секундомер"]
|
||||
en = ["start stopwatch", "start timer"]
|
||||
ua = ["запусти секундомір"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -20,6 +21,7 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["сколько прошло", "сколько времени прошло", "проверь секундомер"]
|
||||
en = ["how long has it been", "check stopwatch"]
|
||||
ua = ["скільки пройшло"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
|
|
@ -32,3 +34,4 @@ timeout = 3000
|
|||
[commands.phrases]
|
||||
ru = ["стоп секундомер", "останови секундомер", "выключи секундомер"]
|
||||
en = ["stop stopwatch", "stop timer"]
|
||||
ua = ["стоп секундомір"]
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue