feat: parity — brightness, window switch, spelling, network, summarize (100 commands)

Mirrors the rust tier-3 batch. 90 → 100 commands (round number — feels
right for an "имба" milestone).

main.py:
+ do_brightness: WMI WmiMonitorBrightness read + WmiSetBrightness write,
  ±20% step or 100/10 absolute, "no_wmi" sentinel for desktops.
+ do_window_switch: same alias map as process_kill (xrom→chrome/...),
  PS finds process by name OR window-title substring, picks newest
  StartTime, AppActivate via WScript.Shell.
+ do_spell_out: split chars, join with ". ", _speak.
+ do_network_settings: action.section = wifi|bluetooth|network →
  matching ms-settings:* URI via cmd /c start "".
+ do_my_ip: Get-NetIPAddress filtered to non-loopback non-APIPA IPv4,
  WAN via Invoke-RestMethod ipify, speaks "Локальный X. Внешний Y".
+ do_summarize_selection: pyautogui.hotkey(ctrl, c) → 250ms wait →
  Get-Clipboard → Groq @ temp 0.3 ("3-5 sentence retell") → speak +
  clipboard.

commands.yaml +10 entries: brightness_up/down/max/min, switch_to_window,
spell_out, open_wifi_settings, open_bluetooth_settings, my_ip,
summarize_selection.

Backlog: closed all "Tier-3 still TODO" pure-Lua items. Remaining
TODOs (pomodoro, currency-math, stocks, translate-clipboard, daily
briefing, etc.) are even smaller LOC and can roll in the next batch.
This commit is contained in:
Bossiara13 2026-05-15 13:11:37 +03:00
parent b3a5bcabe5
commit ef8da9cba8
2 changed files with 249 additions and 0 deletions

View file

@ -740,6 +740,83 @@ random_choice:
- выбрать
action: {type: random_choice}
brightness_up:
phrases:
- ярче
- сделай ярче
- увеличь яркость
- поярче
action: {type: brightness, op: up}
brightness_down:
phrases:
- темнее
- сделай темнее
- уменьши яркость
- потемнее
action: {type: brightness, op: down}
brightness_max:
phrases:
- максимальная яркость
- яркость на максимум
action: {type: brightness, op: max}
brightness_min:
phrases:
- минимальная яркость
- яркость на минимум
action: {type: brightness, op: min}
switch_to_window:
phrases:
- переключись на
- переключи на
- активируй окно
- покажи окно
- перейди в
action: {type: window_switch}
spell_out:
phrases:
- произнеси по буквам
- продиктуй по буквам
- произнеси буквы
- по буквам
action: {type: spell_out}
open_wifi_settings:
phrases:
- настройки wifi
- настройки вайфай
- wifi настройки
- сеть
action: {type: network_settings, section: wifi}
open_bluetooth_settings:
phrases:
- настройки блютуз
- блютуз
action: {type: network_settings, section: bluetooth}
my_ip:
phrases:
- мой ай пи
- мой ip
- какой у меня айпи
- айпишник
action: {type: my_ip}
summarize_selection:
phrases:
- суммируй
- перескажи выделенное
- кратко перескажи
- о чём это
- что выделил
- перескажи это
action: {type: summarize_selection}
thanks:
phrases:
- спасибо

172
main.py
View file

@ -1193,6 +1193,166 @@ def do_random_choice(action, voice: str):
_speak(f"Я выбираю {random.choice(options)}.")
_BRIGHTNESS_PS = (
"$ErrorActionPreference='SilentlyContinue'; "
"try { $cur = (Get-WmiObject -Namespace root\\WMI -Class WmiMonitorBrightness -EA Stop).CurrentBrightness } "
"catch { Write-Output 'no_wmi'; exit 0 } "
"$target = switch ('__OP__') { 'up'{[Math]::Min(100,$cur+20)} 'down'{[Math]::Max(0,$cur-20)} 'max'{100} 'min'{10} default{$cur} } "
"(Get-WmiObject -Namespace root\\WMI -Class WmiMonitorBrightnessMethods).WmiSetBrightness(1,$target) | Out-Null; "
"Write-Output $target"
)
def do_brightness(action, voice: str):
op = action.get('op', 'up')
ps = _BRIGHTNESS_PS.replace('__OP__', op)
res = subprocess.run(
['powershell', '-NoProfile', '-Command', ps],
capture_output=True, text=True, encoding='utf-8', timeout=10,
)
out = (res.stdout or '').strip()
if out == 'no_wmi' or not out:
_speak("Не поддерживается на этом дисплее.")
return
_speak(f"Яркость {out} процентов.")
_WINDOW_ALIASES = {
'хром': 'chrome', 'хрома': 'chrome', 'едж': 'msedge', 'эдж': 'msedge',
'файрфокс': 'firefox', 'фаерфокс': 'firefox',
'вскод': 'code', 'vs code': 'code',
'дискорд': 'discord', 'телега': 'telegram', 'телеграм': 'telegram',
'стим': 'steam', 'обс': 'obs64',
'проводник': 'explorer', 'блокнот': 'notepad',
'терминал': 'windowsterminal', 'спотифай': 'spotify',
'яндекс': 'browser', 'браузер': 'browser',
}
_WINDOW_TRIGGERS = ('переключись на', 'переключи на', 'активируй окно',
'покажи окно', 'перейди в',
'switch to', 'activate window', 'focus window',
'перемкни на')
def do_window_switch(action, voice: str):
query = _strip_trigger(voice, _WINDOW_TRIGGERS).strip().lower()
if not query:
_speak("Какое окно?")
return
target = _WINDOW_ALIASES.get(query, query).replace("'", "")
ps = (
f"$procs = Get-Process | Where-Object {{ $_.MainWindowTitle -and "
f"($_.ProcessName -like '*{target}*' -or $_.MainWindowTitle -like '*{target}*') }} "
f"| Sort-Object -Property StartTime -Descending; "
f"if ($procs.Count -eq 0) {{ Write-Output 'NOT_FOUND'; exit 0 }}; "
f"$p = $procs[0]; "
f"$wsh = New-Object -ComObject WScript.Shell; "
f"$null = $wsh.AppActivate($p.Id); "
f"Write-Output $p.MainWindowTitle"
)
res = subprocess.run(['powershell', '-NoProfile', '-Command', ps],
capture_output=True, text=True, encoding='utf-8', timeout=10)
out = (res.stdout or '').strip()
if out == 'NOT_FOUND' or not out:
_speak(f"Не нашёл окно {query}.")
return
_speak(f"Переключаюсь на {out[:60]}.")
_SPELLING_TRIGGERS = ('продиктуй по буквам', 'произнеси по буквам',
'произнеси буквы', 'по буквам',
'spell out', 'spell it', 'вимов по буквах')
def do_spell_out(action, voice: str):
word = _strip_trigger(voice, _SPELLING_TRIGGERS).strip()
if not word:
_speak("Что произнести?")
return
letters = [ch for ch in word if not ch.isspace()]
if not letters:
return
_speak('. '.join(letters) + '.')
_NETWORK_URIS = {
'wifi': 'ms-settings:network-wifi',
'bluetooth': 'ms-settings:bluetooth',
'network': 'ms-settings:network',
}
def do_network_settings(action, voice: str):
section = action.get('section', 'network')
uri = _NETWORK_URIS.get(section, _NETWORK_URIS['network'])
subprocess.Popen(['cmd', '/c', 'start', '', uri], shell=False)
label = {'wifi': 'Wi-Fi', 'bluetooth': 'Bluetooth', 'network': 'сети'}.get(section, 'сети')
_speak(f"Открываю настройки {label}.")
def do_my_ip(action, voice: str):
ps = (
"$lan = (Get-NetIPAddress -AddressFamily IPv4 -EA SilentlyContinue | "
"Where-Object { $_.PrefixOrigin -in 'Dhcp','Manual' } | "
"Where-Object { $_.IPAddress -notmatch '^127\\.' -and $_.IPAddress -notmatch '^169\\.254\\.' } | "
"Select-Object -First 1).IPAddress; "
"if (-not $lan) { $lan = '(none)' }; "
"$wan = try { (Invoke-RestMethod -Uri 'https://api.ipify.org?format=json' -TimeoutSec 5).ip } catch { 'unavailable' }; "
"Write-Output ('LAN ' + $lan + ' WAN ' + $wan)"
)
res = subprocess.run(['powershell', '-NoProfile', '-Command', ps],
capture_output=True, text=True, encoding='utf-8', timeout=15)
out = (res.stdout or '').strip()
if not out:
_speak("Не получил IP.")
return
say = out.replace('LAN', 'Локальный').replace('WAN', '. Внешний')
_speak(say)
_SUMMARIZE_TRIGGERS = ('суммируй', 'перескажи выделенное', 'кратко перескажи',
'о чём это', 'что выделил', 'перескажи это',
'summarise this', 'summarize selection', 'tldr',
'перекажи це', 'стисло перекажи')
def do_summarize_selection(action, voice: str):
if not config.GROQ_TOKEN:
_speak("Groq токен не задан.")
return
try:
import pyautogui
pyautogui.hotkey('ctrl', 'c')
except Exception as e:
print(f"summarize copy failed: {e}")
_speak("Не удалось скопировать выделение.")
return
time.sleep(0.25)
res = subprocess.run(['powershell', '-NoProfile', '-Command', 'Get-Clipboard'],
capture_output=True, text=True, encoding='utf-8', timeout=10)
text = (res.stdout or '').strip()
if len(text) < 20:
_speak("Сначала выдели текст и повтори.")
return
if len(text) > 8000:
text = text[:8000] + " ..."
sys_prompt = ("Перескажи следующий текст одним абзацем (3-5 предложений). "
"Сохрани ключевые факты и имена, без 'вот суммарно'.")
try:
resp = client.chat.completions.create(
model=config.GROQ_MODEL,
messages=[{'role': 'system', 'content': sys_prompt},
{'role': 'user', 'content': text}],
max_tokens=400, temperature=0.3,
)
summary = resp.choices[0].message.content.strip()
except Exception as e:
print(f"summarize LLM failed: {e}")
_speak("LLM не ответила.")
return
_set_clipboard(summary)
_speak(summary)
def do_stopwatch(action, voice: str):
op = action.get('op', 'check')
state = _stopwatch_state_path()
@ -1365,6 +1525,18 @@ def run_action(action):
do_mouse(action, _current_voice or '')
elif t == 'random_choice':
do_random_choice(action, _current_voice or '')
elif t == 'brightness':
do_brightness(action, _current_voice or '')
elif t == 'window_switch':
do_window_switch(action, _current_voice or '')
elif t == 'spell_out':
do_spell_out(action, _current_voice or '')
elif t == 'network_settings':
do_network_settings(action, _current_voice or '')
elif t == 'my_ip':
do_my_ip(action, _current_voice or '')
elif t == 'summarize_selection':
do_summarize_selection(action, _current_voice or '')
_current_voice: str = ''