From 5790ca18affa64ef83189a9a9824abbf520d885c Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Fri, 15 May 2026 12:28:57 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20parity=20=E2=80=94=20reminders,=20today?= =?UTF-8?q?/tomorrow/yesterday,=20voice=20type,=20dice,=20stopwatch=20(67?= =?UTF-8?q?=20commands)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the rust feature/pc-tools work. yaml.safe_load + ast.parse pass. Commands count: 56 → 67 (+11). main.py: + do_set_reminder — strips trigger, parses "N (секунд|минут|часов)" with Russian-word numeral support (один/две/пять/десять/...). Spawns a daemon thread that time.sleep()s then runs tts.va_speak + PowerShell WScript.Shell.Popup. Defaults to 5 min when number/ unit are missing. + do_date_query — accepts {offset: 0/1/-1}, computes target date with datetime.timedelta, formats with hardcoded Russian weekday/month names (no locale dependency), speaks "Сегодня/Завтра/Вчера + ...". + do_type_text — clipboard + pyautogui.hotkey('ctrl', 'v') so it works in any focused window. + do_dice — coin/dice/random via random.choice / random.randint. + do_stopwatch — stores start ts in %LOCALAPPDATA%\com.priler.jarvis\stopwatch.txt; start/check/stop ops with humanised elapsed string. commands.yaml +11 entries: + set_reminder + today / tomorrow / yesterday (action: date_query, offset: 0/1/-1) + type_text + coin_flip / roll_dice / random_number (action: dice, kind: ...) + stopwatch_start / stopwatch_check / stopwatch_stop (action: stopwatch, op: start/check/stop) --- commands.yaml | 84 +++++++++++++++++++++++++ main.py | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+) diff --git a/commands.yaml b/commands.yaml index 1640b1f..4627a68 100644 --- a/commands.yaml +++ b/commands.yaml @@ -469,6 +469,90 @@ open_sound_panel: - управление звуком action: {type: open_sound_panel} +set_reminder: + phrases: + - напомни через + - напомни мне через + - поставь напоминание через + - установи таймер на + - таймер на + action: {type: set_reminder} + +today: + phrases: + - какой сегодня день + - какое сегодня число + - сегодня какое число + - что сегодня + action: {type: date_query, offset: 0} + +tomorrow: + phrases: + - какой день завтра + - что завтра + - завтра какое число + - какое число завтра + action: {type: date_query, offset: 1} + +yesterday: + phrases: + - какой был вчера день + - что было вчера + - вчера какое число было + action: {type: date_query, offset: -1} + +type_text: + phrases: + - напечатай + - набери текст + - введи текст + - впиши + action: {type: type_text} + +coin_flip: + phrases: + - подбрось монетку + - брось монетку + - орёл или решка + - орел или решка + action: {type: dice, kind: coin} + +roll_dice: + phrases: + - брось кубик + - подбрось кубик + - кинь кубик + action: {type: dice, kind: dice} + +random_number: + phrases: + - случайное число + - выбери случайное число + - рандомное число + action: {type: dice, kind: random} + +stopwatch_start: + phrases: + - засеки время + - запусти секундомер + - старт секундомер + - включи секундомер + action: {type: stopwatch, op: start} + +stopwatch_check: + phrases: + - сколько прошло + - сколько времени прошло + - проверь секундомер + action: {type: stopwatch, op: check} + +stopwatch_stop: + phrases: + - стоп секундомер + - останови секундомер + - выключи секундомер + action: {type: stopwatch, op: stop} + thanks: phrases: - спасибо diff --git a/main.py b/main.py index 8cf43ff..007bcf1 100644 --- a/main.py +++ b/main.py @@ -728,6 +728,166 @@ def do_open_sound_panel(action, voice: str): _speak("Открываю панель звука.") +import re as _re_reminders +import threading as _threading + +_REMINDER_TRIGGERS = ( + 'поставь напоминание через', 'установи таймер на', + 'напомни мне через', 'напомни через', 'напомни', + 'remind me in', 'set reminder in', 'set timer for', 'timer for', +) +_RUS_NUMERALS = { + 'один': 1, 'одну': 1, 'одна': 1, 'полтора': 1, 'полторы': 1, + 'два': 2, 'две': 2, 'три': 3, 'четыре': 4, 'пять': 5, + 'шесть': 6, 'семь': 7, 'восемь': 8, 'девять': 9, 'десять': 10, + 'пятнадцать': 15, 'двадцать': 20, 'тридцать': 30, 'сорок': 40, + 'пятьдесят': 50, +} +_UNIT_SECS = { + 'секунд': 1, 'секунду': 1, 'секунды': 1, 'second': 1, 'seconds': 1, 'sec': 1, + 'минут': 60, 'минуту': 60, 'минуты': 60, 'мин': 60, + 'minute': 60, 'minutes': 60, 'min': 60, + 'час': 3600, 'часа': 3600, 'часов': 3600, 'часик': 3600, + 'hour': 3600, 'hours': 3600, 'hr': 3600, +} + + +def _fmt_seconds(sec: int) -> str: + sec = int(sec) + if sec < 60: return f"{sec} сек" + if sec < 3600: return f"{sec // 60} мин" + return f"{sec // 3600} ч {(sec % 3600) // 60} мин" + + +def do_set_reminder(action, voice: str): + rest = _strip_trigger(voice, _REMINDER_TRIGGERS).strip().lower() + number, unit, body = None, None, None + m = _re_reminders.match(r'^(\d+)\s+(.+)$', rest) + if m: + number = int(m.group(1)) + rest = m.group(2) + else: + head, _, tail = rest.partition(' ') + if head in _RUS_NUMERALS: + number = _RUS_NUMERALS[head] + rest = tail + head, _, tail = rest.partition(' ') + if head: + for w, secs in _UNIT_SECS.items(): + if head.startswith(w): + unit = secs + body = tail + break + if number is None: + number, unit, body = 5, unit or 60, rest + elif unit is None: + unit, body = 60, rest + total = max(1, min(86400, number * unit)) + body = (body or '').strip() or "Напоминание!" + + def fire(): + time.sleep(total) + try: + recorder.stop() + tts.va_speak(f"Сэр, напоминаю: {body}") + except Exception: + pass + finally: + time.sleep(0.2) + try: recorder.start() + except Exception: pass + try: + subprocess.Popen( + ['powershell', '-NoProfile', '-Command', + "$wsh = New-Object -ComObject WScript.Shell; " + f"$wsh.Popup('{body.replace(chr(39), chr(39)+chr(39))}', 30, " + f"'Напоминание J.A.R.V.I.S.', 64) | Out-Null"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + except Exception: + pass + + _threading.Thread(target=fire, daemon=False).start() + _speak(f"Через {_fmt_seconds(total)} напомню: {body}.") + + +def do_date_query(action, voice: str): + offset = action.get('offset', 0) + target = datetime.datetime.now() + datetime.timedelta(days=offset) + weekdays = ['понедельник', 'вторник', 'среда', 'четверг', 'пятница', 'суббота', 'воскресенье'] + months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', + 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'] + text = f"{weekdays[target.weekday()]}, {target.day} {months[target.month - 1]}" + labels = {0: 'Сегодня', 1: 'Завтра', -1: 'Вчера'} + _speak(f"{labels.get(offset, '')} {text}.") + + +def do_type_text(action, voice: str): + triggers = ('набери текст', 'введи текст', 'напечатай', 'впиши', + 'type text', 'write text', 'type', 'надрукуй') + text = _strip_trigger(voice, triggers) + if not text: + _speak("Что напечатать?") + return + _set_clipboard(text) + time.sleep(0.15) + try: + import pyautogui + pyautogui.hotkey('ctrl', 'v') + except Exception as e: + print(f"voice type failed: {e}") + _speak("Не получилось напечатать.") + + +def do_dice(action, voice: str): + kind = action.get('kind', 'random') + if kind == 'coin': + _speak(random.choice(['Орёл.', 'Решка.'])) + elif kind == 'dice': + _speak(f"Выпало {random.randint(1, 6)}.") + else: + _speak(f"Число {random.randint(1, 100)}.") + + +def _stopwatch_state_path() -> str: + base = os.environ.get('LOCALAPPDATA', os.path.expanduser('~')) + p = os.path.join(base, 'com.priler.jarvis', 'stopwatch.txt') + os.makedirs(os.path.dirname(p), exist_ok=True) + return p + + +def do_stopwatch(action, voice: str): + op = action.get('op', 'check') + state = _stopwatch_state_path() + + def fmt(sec): + sec = int(sec) + if sec < 60: return f"{sec} секунд" + if sec < 3600: return f"{sec // 60} мин {sec % 60} сек" + return f"{sec // 3600} ч {(sec % 3600) // 60} мин {sec % 60} сек" + + if op == 'start': + with open(state, 'w', encoding='utf-8') as f: + f.write(str(time.time())) + _speak("Секундомер запущен.") + return + if not os.path.isfile(state): + _speak("Секундомер не запущен.") + return + try: + started = float(open(state, 'r', encoding='utf-8').read().strip()) + except (OSError, ValueError): + _speak("Секундомер не запущен.") + return + elapsed = time.time() - started + if op == 'stop': + try: os.unlink(state) + except OSError: pass + _speak(f"Прошло {fmt(elapsed)}.") + else: + _speak(f"Прошло {fmt(elapsed)}.") + + def do_sysinfo(action, voice: str): topic = action.get('topic', 'all') @@ -836,6 +996,16 @@ def run_action(action): do_list_projects(action, _current_voice or '') elif t == 'open_sound_panel': do_open_sound_panel(action, _current_voice or '') + elif t == 'set_reminder': + do_set_reminder(action, _current_voice or '') + elif t == 'date_query': + do_date_query(action, _current_voice or '') + elif t == 'type_text': + do_type_text(action, _current_voice or '') + elif t == 'dice': + do_dice(action, _current_voice or '') + elif t == 'stopwatch': + do_stopwatch(action, _current_voice or '') _current_voice: str = ''