feat: parity — reminders, today/tomorrow/yesterday, voice type, dice, stopwatch (67 commands)
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)
This commit is contained in:
parent
69359ed5c0
commit
5790ca18af
2 changed files with 254 additions and 0 deletions
170
main.py
170
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 = ''
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue