feat: parity — news, currency, crypto, fun, wiki, power, help (82 commands)
Mirrors the rust analog-inspired tier-2 packs. yaml + ast smoke pass. 56 → 82 commands (+26 yaml entries; 14 of them are power/fun/wiki/ news/etc. permutations). main.py — 7 new action handlers, all built on stdlib urllib.request (no new dep). Helper _http_get() with User-Agent header so RSS feeds that 403 default UAs go through. + do_news: pulls RSS (default Lenta.ru), extracts <title> via regex, skips channel title (idx 0), takes 5, sends to Groq for a 2-3 sentence summary at temp 0.4. Falls back to reading first 3 headlines verbatim if no token. + do_currency_rate: cbr-xml-daily.ru daily_json.js. Recognises USD/EUR/CNY/GBP/JPY/KZT in the phrase, computes value/nominal, diffs against Previous, speaks Russian with direction. + do_crypto_price: CoinGecko simple/price for BTC/ETH/SOL/DOGE/TON with 24h change, picked by phrase keywords. + do_fun_ask: joke / fact / quote / compliment via Groq at temp 1.0 with per-kind system prompts, random seed in user message. + do_wiki_lookup: ru.wikipedia opensearch → page summary, optional Groq retell when extract > 350 chars. + do_power: dispatch table for shutdown / restart / sleep / hibernate / logoff / cancel — runs shutdown.exe /s|/r|/h|/l|/a or rundll32 powrprof,SetSuspendState. 30-second grace on shutdown and restart with /c message so "отмени выключение" works. + do_help_commands: sorts VA_CMD_LIST.keys(), speaks first 12 + total, full list to clipboard. commands.yaml +26 entries: news_headlines, currency_rate, crypto_price, joke / fact / quote / compliment (action:fun_ask, kind: ...), wiki_lookup, shutdown_pc / restart_pc / sleep_pc / hibernate_pc / logoff_user / cancel_shutdown (action:power, op: ...), help. Inspired by what mainstream voice assistants (Siri/Alexa/Алиса/ Cortana) ship by default + power-user utilities (shutdown, window/process control).
This commit is contained in:
parent
5790ca18af
commit
40a46567b8
2 changed files with 360 additions and 0 deletions
128
commands.yaml
128
commands.yaml
|
|
@ -553,6 +553,134 @@ stopwatch_stop:
|
||||||
- выключи секундомер
|
- выключи секундомер
|
||||||
action: {type: stopwatch, op: stop}
|
action: {type: stopwatch, op: stop}
|
||||||
|
|
||||||
|
news_headlines:
|
||||||
|
phrases:
|
||||||
|
- что нового
|
||||||
|
- новости
|
||||||
|
- что в мире
|
||||||
|
- последние новости
|
||||||
|
- расскажи новости
|
||||||
|
- топ новостей
|
||||||
|
action: {type: news}
|
||||||
|
|
||||||
|
currency_rate:
|
||||||
|
phrases:
|
||||||
|
- курс доллара
|
||||||
|
- курс евро
|
||||||
|
- сколько стоит доллар
|
||||||
|
- сколько стоит евро
|
||||||
|
- курс юаня
|
||||||
|
- курс валют
|
||||||
|
- что с долларом
|
||||||
|
- что с евро
|
||||||
|
action: {type: currency_rate}
|
||||||
|
|
||||||
|
crypto_price:
|
||||||
|
phrases:
|
||||||
|
- сколько биткоин
|
||||||
|
- цена биткоина
|
||||||
|
- курс биткоина
|
||||||
|
- цена эфира
|
||||||
|
- сколько эфир
|
||||||
|
- цена крипты
|
||||||
|
- цена соланы
|
||||||
|
action: {type: crypto_price}
|
||||||
|
|
||||||
|
joke:
|
||||||
|
phrases:
|
||||||
|
- расскажи шутку
|
||||||
|
- пошути
|
||||||
|
- анекдот
|
||||||
|
- рассмеши меня
|
||||||
|
action: {type: fun_ask, kind: joke}
|
||||||
|
|
||||||
|
fact:
|
||||||
|
phrases:
|
||||||
|
- расскажи факт
|
||||||
|
- интересный факт
|
||||||
|
- поделись фактом
|
||||||
|
action: {type: fun_ask, kind: fact}
|
||||||
|
|
||||||
|
quote:
|
||||||
|
phrases:
|
||||||
|
- вдохнови меня
|
||||||
|
- цитата дня
|
||||||
|
- мотивируй
|
||||||
|
- процитируй кого-нибудь
|
||||||
|
action: {type: fun_ask, kind: quote}
|
||||||
|
|
||||||
|
compliment:
|
||||||
|
phrases:
|
||||||
|
- сделай комплимент
|
||||||
|
- похвали меня
|
||||||
|
action: {type: fun_ask, kind: compliment}
|
||||||
|
|
||||||
|
wiki_lookup:
|
||||||
|
phrases:
|
||||||
|
- что такое
|
||||||
|
- кто такой
|
||||||
|
- кто такая
|
||||||
|
- расскажи про
|
||||||
|
- википедия про
|
||||||
|
- найди в википедии про
|
||||||
|
action: {type: wiki_lookup}
|
||||||
|
|
||||||
|
shutdown_pc:
|
||||||
|
phrases:
|
||||||
|
- выключи компьютер
|
||||||
|
- выключи пк
|
||||||
|
- выключи комп
|
||||||
|
action: {type: power, op: shutdown}
|
||||||
|
|
||||||
|
restart_pc:
|
||||||
|
phrases:
|
||||||
|
- перезагрузи компьютер
|
||||||
|
- перезагрузи пк
|
||||||
|
- ребутни
|
||||||
|
- ребут
|
||||||
|
action: {type: power, op: restart}
|
||||||
|
|
||||||
|
sleep_pc:
|
||||||
|
phrases:
|
||||||
|
- усыпи компьютер
|
||||||
|
- усыпи пк
|
||||||
|
- режим сна
|
||||||
|
- перейди в сон
|
||||||
|
action: {type: power, op: sleep}
|
||||||
|
|
||||||
|
hibernate_pc:
|
||||||
|
phrases:
|
||||||
|
- гибернация
|
||||||
|
- переведи в гибернацию
|
||||||
|
- режим гибернации
|
||||||
|
action: {type: power, op: hibernate}
|
||||||
|
|
||||||
|
logoff_user:
|
||||||
|
phrases:
|
||||||
|
- выйди из системы
|
||||||
|
- разлогинь
|
||||||
|
- выйди из учётки
|
||||||
|
- сеанс закончи
|
||||||
|
action: {type: power, op: logoff}
|
||||||
|
|
||||||
|
cancel_shutdown:
|
||||||
|
phrases:
|
||||||
|
- отмени выключение
|
||||||
|
- отмени ребут
|
||||||
|
- отмени перезагрузку
|
||||||
|
- стой не выключай
|
||||||
|
action: {type: power, op: cancel}
|
||||||
|
|
||||||
|
help:
|
||||||
|
phrases:
|
||||||
|
- что ты умеешь
|
||||||
|
- какие команды
|
||||||
|
- список команд
|
||||||
|
- помощь
|
||||||
|
- помоги
|
||||||
|
- что можешь
|
||||||
|
action: {type: help_commands}
|
||||||
|
|
||||||
thanks:
|
thanks:
|
||||||
phrases:
|
phrases:
|
||||||
- спасибо
|
- спасибо
|
||||||
|
|
|
||||||
232
main.py
232
main.py
|
|
@ -856,6 +856,224 @@ def _stopwatch_state_path() -> str:
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
import urllib.request as _urlreq
|
||||||
|
import urllib.parse as _urlparse
|
||||||
|
|
||||||
|
|
||||||
|
def _http_get(url: str, timeout: float = 15.0) -> str:
|
||||||
|
req = _urlreq.Request(url, headers={'User-Agent': 'Jarvis/1.0'})
|
||||||
|
with _urlreq.urlopen(req, timeout=timeout) as r:
|
||||||
|
return r.read().decode('utf-8', errors='replace')
|
||||||
|
|
||||||
|
|
||||||
|
def do_news(action, voice: str):
|
||||||
|
feed = action.get('feed', 'https://lenta.ru/rss/news')
|
||||||
|
try:
|
||||||
|
body = _http_get(feed, timeout=15)
|
||||||
|
except Exception as e:
|
||||||
|
_speak("Не получил RSS.")
|
||||||
|
print(f"news fetch failed: {e}")
|
||||||
|
return
|
||||||
|
titles = _re_reminders.findall(r'<title>\s*<!\[CDATA\[(.+?)\]\]>\s*</title>', body)
|
||||||
|
if not titles:
|
||||||
|
titles = _re_reminders.findall(r'<title>\s*(.+?)\s*</title>', body)
|
||||||
|
titles = [t.strip() for t in titles if t.strip()]
|
||||||
|
if len(titles) <= 1:
|
||||||
|
_speak("Не получилось распарсить новости.")
|
||||||
|
return
|
||||||
|
top = titles[1:6]
|
||||||
|
joined = ' | '.join(top)
|
||||||
|
_set_clipboard(joined)
|
||||||
|
say = None
|
||||||
|
if config.GROQ_TOKEN:
|
||||||
|
try:
|
||||||
|
resp = client.chat.completions.create(
|
||||||
|
model=config.GROQ_MODEL,
|
||||||
|
messages=[
|
||||||
|
{'role': 'system', 'content': 'Ты диктор. Сжато перескажи 3 главные темы из списка заголовков, 2-3 предложения.'},
|
||||||
|
{'role': 'user', 'content': joined},
|
||||||
|
],
|
||||||
|
max_tokens=220, temperature=0.4,
|
||||||
|
)
|
||||||
|
say = resp.choices[0].message.content.strip()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"news LLM summary failed: {e}")
|
||||||
|
if not say:
|
||||||
|
say = "Главные новости: " + ". ".join(top[:3])
|
||||||
|
_speak(say)
|
||||||
|
|
||||||
|
|
||||||
|
_CURRENCY_LABELS = {'USD': 'Доллар', 'EUR': 'Евро', 'CNY': 'Юань',
|
||||||
|
'GBP': 'Фунт', 'JPY': 'Йена', 'KZT': 'Тенге'}
|
||||||
|
|
||||||
|
|
||||||
|
def do_currency_rate(action, voice: str):
|
||||||
|
lower = voice.lower()
|
||||||
|
code = 'USD'
|
||||||
|
if 'евро' in lower or 'eur' in lower: code = 'EUR'
|
||||||
|
elif 'юан' in lower or 'cny' in lower: code = 'CNY'
|
||||||
|
elif 'фунт' in lower: code = 'GBP'
|
||||||
|
elif 'йен' in lower: code = 'JPY'
|
||||||
|
elif 'тенге' in lower: code = 'KZT'
|
||||||
|
try:
|
||||||
|
data = json.loads(_http_get('https://www.cbr-xml-daily.ru/daily_json.js', timeout=10))
|
||||||
|
except Exception as e:
|
||||||
|
_speak("Не получил данные ЦБ.")
|
||||||
|
print(f"cbr fetch failed: {e}")
|
||||||
|
return
|
||||||
|
v = data.get('Valute', {}).get(code)
|
||||||
|
if not v:
|
||||||
|
_speak(f"Нет данных по {code}.")
|
||||||
|
return
|
||||||
|
val = v['Value'] / max(1, v.get('Nominal', 1))
|
||||||
|
prev = v.get('Previous', v['Value']) / max(1, v.get('Nominal', 1))
|
||||||
|
diff = val - prev
|
||||||
|
label = _CURRENCY_LABELS.get(code, code)
|
||||||
|
if diff > 0.01:
|
||||||
|
_speak(f"{label} стоит {val:.2f} рубля, поднялся на {diff:.2f}.")
|
||||||
|
elif diff < -0.01:
|
||||||
|
_speak(f"{label} стоит {val:.2f} рубля, опустился на {abs(diff):.2f}.")
|
||||||
|
else:
|
||||||
|
_speak(f"{label} стоит {val:.2f} рубля, без изменений.")
|
||||||
|
|
||||||
|
|
||||||
|
def do_crypto_price(action, voice: str):
|
||||||
|
lower = voice.lower()
|
||||||
|
if 'эфир' in lower or 'eth' in lower or 'ethereum' in lower:
|
||||||
|
coin, label = 'ethereum', 'Эфир'
|
||||||
|
elif 'солан' in lower: coin, label = 'solana', 'Солана'
|
||||||
|
elif 'doge' in lower or 'дог' in lower: coin, label = 'dogecoin', 'Догикоин'
|
||||||
|
elif 'ton' in lower or 'тон' in lower: coin, label = 'the-open-network', 'TON'
|
||||||
|
else: coin, label = 'bitcoin', 'Биткоин'
|
||||||
|
try:
|
||||||
|
url = (f"https://api.coingecko.com/api/v3/simple/price?ids={coin}"
|
||||||
|
"&vs_currencies=usd,rub&include_24hr_change=true")
|
||||||
|
data = json.loads(_http_get(url, timeout=10)).get(coin, {})
|
||||||
|
except Exception as e:
|
||||||
|
_speak("CoinGecko не ответил.")
|
||||||
|
print(f"coingecko fetch failed: {e}")
|
||||||
|
return
|
||||||
|
if not data:
|
||||||
|
_speak(f"Нет данных по {label}.")
|
||||||
|
return
|
||||||
|
usd = data.get('usd', 0)
|
||||||
|
ch = data.get('usd_24h_change', 0)
|
||||||
|
if ch > 1:
|
||||||
|
_speak(f"{label} стоит {int(usd)} долларов, за сутки вырос на {ch:.1f} процентов.")
|
||||||
|
elif ch < -1:
|
||||||
|
_speak(f"{label} стоит {int(usd)} долларов, за сутки упал на {abs(ch):.1f} процентов.")
|
||||||
|
else:
|
||||||
|
_speak(f"{label} стоит {int(usd)} долларов, без существенных изменений.")
|
||||||
|
|
||||||
|
|
||||||
|
_FUN_PROMPTS = {
|
||||||
|
'joke': "Расскажи одну короткую шутку на русском, 1-3 предложения. Без вступления.",
|
||||||
|
'fact': "Расскажи один интересный неочевидный факт. 1-3 предложения. Без 'вот факт'.",
|
||||||
|
'quote': "Дай вдохновляющую цитату известного автора. Формат: <цитата> — <автор>.",
|
||||||
|
'compliment': "Сделай искренний короткий комплимент пользователю, как Джарвис-дворецкий. 1-2 предложения, обращение «сэр».",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def do_fun_ask(action, voice: str):
|
||||||
|
kind = action.get('kind', 'joke')
|
||||||
|
sys_prompt = _FUN_PROMPTS.get(kind, _FUN_PROMPTS['joke'])
|
||||||
|
if not config.GROQ_TOKEN:
|
||||||
|
_speak("Groq токен не задан.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
resp = client.chat.completions.create(
|
||||||
|
model=config.GROQ_MODEL,
|
||||||
|
messages=[
|
||||||
|
{'role': 'system', 'content': sys_prompt},
|
||||||
|
{'role': 'user', 'content': f'seed {random.randint(1, 9999)}'},
|
||||||
|
],
|
||||||
|
max_tokens=220, temperature=1.0,
|
||||||
|
)
|
||||||
|
text = resp.choices[0].message.content.strip()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"fun api failed: {e}")
|
||||||
|
_speak("Ошибка.")
|
||||||
|
return
|
||||||
|
_speak(text)
|
||||||
|
|
||||||
|
|
||||||
|
_WIKI_TRIGGERS = ('найди в википедии про', 'википедия про', 'расскажи про',
|
||||||
|
'кто такая', 'кто такой', 'что такое',
|
||||||
|
'tell me about', 'wikipedia about', 'who is', 'what is',
|
||||||
|
'розкажи про', 'хто такий', 'що таке')
|
||||||
|
|
||||||
|
|
||||||
|
def do_wiki_lookup(action, voice: str):
|
||||||
|
query = _strip_trigger(voice, _WIKI_TRIGGERS).strip()
|
||||||
|
if not query:
|
||||||
|
_speak("О чём рассказать?")
|
||||||
|
return
|
||||||
|
host = 'ru.wikipedia.org'
|
||||||
|
try:
|
||||||
|
search_url = (f"https://{host}/w/api.php?action=opensearch"
|
||||||
|
f"&search={_urlparse.quote(query)}&limit=1&format=json")
|
||||||
|
search_data = json.loads(_http_get(search_url, timeout=10))
|
||||||
|
if len(search_data) < 2 or not search_data[1]:
|
||||||
|
_speak(f"Не нашёл {query}.")
|
||||||
|
return
|
||||||
|
title = search_data[1][0]
|
||||||
|
summary_url = f"https://{host}/api/rest_v1/page/summary/{_urlparse.quote(title)}"
|
||||||
|
summary = json.loads(_http_get(summary_url, timeout=10))
|
||||||
|
except Exception as e:
|
||||||
|
_speak("Википедия не ответила.")
|
||||||
|
print(f"wiki fetch failed: {e}")
|
||||||
|
return
|
||||||
|
extract = summary.get('extract', '')
|
||||||
|
if not extract:
|
||||||
|
_speak("Не получил конспект.")
|
||||||
|
return
|
||||||
|
say = extract[:400]
|
||||||
|
if config.GROQ_TOKEN and len(extract) > 350:
|
||||||
|
try:
|
||||||
|
resp = client.chat.completions.create(
|
||||||
|
model=config.GROQ_MODEL,
|
||||||
|
messages=[
|
||||||
|
{'role': 'system', 'content': "Перескажи текст из Википедии простым языком в 2-3 предложения. Без 'википедия гласит'."},
|
||||||
|
{'role': 'user', 'content': extract[:2500]},
|
||||||
|
],
|
||||||
|
max_tokens=220, temperature=0.3,
|
||||||
|
)
|
||||||
|
say = resp.choices[0].message.content.strip()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"wiki LLM failed: {e}")
|
||||||
|
_set_clipboard(f"{summary.get('title', title)}\n\n{extract}")
|
||||||
|
_speak(say)
|
||||||
|
|
||||||
|
|
||||||
|
_POWER_ACTIONS = {
|
||||||
|
'shutdown': (['shutdown.exe', '/s', '/t', '30', '/c', 'J.A.R.V.I.S.: shutdown in 30s'], "Выключение через 30 секунд."),
|
||||||
|
'restart': (['shutdown.exe', '/r', '/t', '30', '/c', 'J.A.R.V.I.S.: restart in 30s'], "Перезагрузка через 30 секунд."),
|
||||||
|
'logoff': (['shutdown.exe', '/l'], "Выход из системы."),
|
||||||
|
'sleep': (['rundll32.exe', 'powrprof.dll,SetSuspendState', '0,1,0'], "Перехожу в сон."),
|
||||||
|
'hibernate': (['shutdown.exe', '/h'], "Гибернация."),
|
||||||
|
'cancel': (['shutdown.exe', '/a'], "Выключение отменено."),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def do_power(action, voice: str):
|
||||||
|
op = action.get('op', 'cancel')
|
||||||
|
cmd, label = _POWER_ACTIONS.get(op, _POWER_ACTIONS['cancel'])
|
||||||
|
res = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
|
if res.returncode == 0 or op == 'cancel':
|
||||||
|
tail = " Скажи отмени если передумал." if op in ('shutdown', 'restart') else ""
|
||||||
|
_speak(label + tail)
|
||||||
|
else:
|
||||||
|
_speak(f"Не получилось: {op}.")
|
||||||
|
print(f"power {op} failed: {res.stderr[:200]}")
|
||||||
|
|
||||||
|
|
||||||
|
def do_help_commands(action, voice: str):
|
||||||
|
names = sorted(VA_CMD_LIST.keys())
|
||||||
|
preview = ', '.join(names[:12])
|
||||||
|
_set_clipboard('\n'.join(names))
|
||||||
|
_speak(f"Загружено {len(names)} команд. Например: {preview}. Полный список в буфере.")
|
||||||
|
|
||||||
|
|
||||||
def do_stopwatch(action, voice: str):
|
def do_stopwatch(action, voice: str):
|
||||||
op = action.get('op', 'check')
|
op = action.get('op', 'check')
|
||||||
state = _stopwatch_state_path()
|
state = _stopwatch_state_path()
|
||||||
|
|
@ -1006,6 +1224,20 @@ def run_action(action):
|
||||||
do_dice(action, _current_voice or '')
|
do_dice(action, _current_voice or '')
|
||||||
elif t == 'stopwatch':
|
elif t == 'stopwatch':
|
||||||
do_stopwatch(action, _current_voice or '')
|
do_stopwatch(action, _current_voice or '')
|
||||||
|
elif t == 'news':
|
||||||
|
do_news(action, _current_voice or '')
|
||||||
|
elif t == 'currency_rate':
|
||||||
|
do_currency_rate(action, _current_voice or '')
|
||||||
|
elif t == 'crypto_price':
|
||||||
|
do_crypto_price(action, _current_voice or '')
|
||||||
|
elif t == 'fun_ask':
|
||||||
|
do_fun_ask(action, _current_voice or '')
|
||||||
|
elif t == 'wiki_lookup':
|
||||||
|
do_wiki_lookup(action, _current_voice or '')
|
||||||
|
elif t == 'power':
|
||||||
|
do_power(action, _current_voice or '')
|
||||||
|
elif t == 'help_commands':
|
||||||
|
do_help_commands(action, _current_voice or '')
|
||||||
|
|
||||||
|
|
||||||
_current_voice: str = ''
|
_current_voice: str = ''
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue