feat: tier-1 parity — translate, math, theme toggle, projects, sound panel (56 commands total)

Mirrors the rust packs from the same iteration. yaml.safe_load + ast.parse
both pass.

main.py — six new action handlers + a _speak_in_lang helper:
+ do_translate: picks target language from "переведи на X" / "translate to X"
  triggers, defaults to English, sends to Groq with strict "translation only"
  prompt, drops to clipboard, speaks via SAPI in matching ISO voice
+ do_math_eval: strips trigger, Groq at temp=0.0 max_tokens=64,
  short-circuits on "нет" sentinel for non-math
+ do_theme_set: writes AppsUseLightTheme + SystemUsesLightTheme via
  Set-ItemProperty under HKCU\...\Themes\Personalize, action param
  mode: dark|light
+ do_open_project / do_list_projects: reads/creates
  %USERPROFILE%\Documents\jarvis-projects.json (a flat list of
  {name, path}), exact-match → substring-match, opens in VS Code if
  code.cmd on PATH else Explorer. First call writes a sample config
  pointing under USERPROFILE so it works on any host.
+ do_open_sound_panel: rundll32 mmsys.cpl,,1 — Recording tab

commands.yaml — 8 new entries:
+ translate, math_eval
+ theme_dark, theme_light (action params mode:dark/light)
+ open_project, list_projects
+ open_sound_panel

SAPI voice selection iterates GetInstalledVoices() and picks first
matching TwoLetterISOLanguageName — Russian phrases sound Russian,
English/German/etc. sound right too, falls back to default if no
matching voice is installed.

Total commands: 56 (was 49).
This commit is contained in:
Bossiara13 2026-05-15 12:06:44 +03:00
parent 7d83bf8299
commit 3795a0b4ec
2 changed files with 283 additions and 0 deletions

View file

@ -407,6 +407,68 @@ search_yandex:
- пробей в яндекс
action: {type: websearch, service: yandex}
translate:
phrases:
- переведи на английский
- переведи на русский
- переведи на украинский
- переведи на немецкий
- переведи на французский
- переведи на испанский
- переведи на китайский
- переведи на японский
- переведи
- как по английски
- как по русски
action: {type: translate}
math_eval:
phrases:
- посчитай
- сколько будет
- вычисли
- реши
- сколько это
action: {type: math_eval}
theme_dark:
phrases:
- тёмная тема
- темная тема
- включи тёмную тему
- тёмный режим
- темный режим
action: {type: theme_set, mode: dark}
theme_light:
phrases:
- светлая тема
- включи светлую тему
- светлый режим
action: {type: theme_set, mode: light}
open_project:
phrases:
- открой проект
- запусти проект
- открой папку проекта
action: {type: open_project}
list_projects:
phrases:
- какие у меня проекты
- список проектов
- покажи проекты
action: {type: list_projects}
open_sound_panel:
phrases:
- открой настройки звука
- панель звука
- настройки микрофона
- управление звуком
action: {type: open_sound_panel}
thanks:
phrases:
- спасибо

221
main.py
View file

@ -511,6 +511,210 @@ def do_websearch(action, voice: str):
_speak(f"Открываю {svc}: {query[:100]}")
_TRANSLATE_TRIGGERS_WITH_LANG = (
'переведи на ', 'translate to ', 'переклади на ', 'how to say in ',
)
_TRANSLATE_GENERIC = ('переведи', 'translate', 'переклади', 'как по ', 'how to say')
_TARGET_LANGS = {
'английский': 'English', 'english': 'English',
'русский': 'Russian', 'russian': 'Russian',
'украинский': 'Ukrainian','ukrainian': 'Ukrainian',
'немецкий': 'German', 'german': 'German',
'французский':'French', 'french': 'French',
'испанский': 'Spanish', 'spanish': 'Spanish',
'итальянский':'Italian',
'китайский': 'Chinese', 'chinese': 'Chinese',
'японский': 'Japanese', 'japanese': 'Japanese',
'польский': 'Polish',
'турецкий': 'Turkish',
}
_SAPI_ISO = {'Russian': 'ru', 'Ukrainian': 'uk', 'English': 'en',
'German': 'de', 'French': 'fr', 'Spanish': 'es',
'Italian': 'it', 'Chinese': 'zh', 'Japanese': 'ja',
'Polish': 'pl', 'Turkish': 'tr'}
def _speak_in_lang(text: str, iso: str = 'ru'):
escaped = text.replace("'", "''").replace('\r', ' ').replace('\n', ' ')
ps = (
"Add-Type -AssemblyName System.Speech; "
"$s = New-Object System.Speech.Synthesis.SpeechSynthesizer; "
f"$iso='{iso}'; "
"foreach ($v in $s.GetInstalledVoices()) { "
" if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq $iso) { "
" try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} "
" } "
"} "
f"$s.Speak('{escaped}')"
)
recorder.stop()
subprocess.run(['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ps],
capture_output=True, timeout=20)
time.sleep(0.2)
recorder.start()
def do_translate(action, voice: str):
if not config.GROQ_TOKEN:
_speak("Groq токен не задан.")
return
lower = voice.lower()
target = None
body = lower
for t in _TRANSLATE_TRIGGERS_WITH_LANG:
if lower.startswith(t):
rest = lower[len(t):].strip()
first = rest.split(' ', 1)[0] if rest else ''
if first in _TARGET_LANGS:
target = _TARGET_LANGS[first]
body = rest[len(first):].strip()
break
if target is None:
for t in _TRANSLATE_GENERIC:
if lower.startswith(t):
body = lower[len(t):].strip()
break
if not body:
_speak("Что переводить?")
return
target = target or 'English'
sys_prompt = (
f"You are a professional translator. Translate the user message into {target}. "
"Output ONLY the translation, no explanations, no quotes, no prefix. "
"Preserve idioms and tone."
)
try:
resp = client.chat.completions.create(
model=config.GROQ_MODEL,
messages=[{'role': 'system', 'content': sys_prompt},
{'role': 'user', 'content': body}],
max_tokens=512, temperature=0.3,
)
translation = resp.choices[0].message.content.strip()
except Exception as e:
print(f"translate api failed: {e}")
_speak("Ошибка перевода.")
return
_set_clipboard(translation)
_speak_in_lang(translation, _SAPI_ISO.get(target, 'en'))
def do_math_eval(action, voice: str):
if not config.GROQ_TOKEN:
_speak("Groq токен не задан.")
return
triggers = ('сколько будет', 'сколько это', 'посчитай', 'вычисли', 'реши',
'how much is', 'calculate', 'compute', 'solve', 'what is',
'скільки буде', 'порахуй', 'обчисли')
query = _strip_trigger(voice, triggers)
if not query:
_speak("Что посчитать?")
return
sys_prompt = (
"Ты калькулятор. Решай арифметику, простые уравнения, конверсии единиц, проценты. "
"Отвечай ТОЛЬКО результатом числом или короткой строкой (для уравнений — значения корней). "
"Если запрос — не математика, ответь одним словом «нет»."
)
try:
resp = client.chat.completions.create(
model=config.GROQ_MODEL,
messages=[{'role': 'system', 'content': sys_prompt},
{'role': 'user', 'content': query}],
max_tokens=64, temperature=0.0,
)
answer = resp.choices[0].message.content.strip()
except Exception as e:
print(f"math api failed: {e}")
_speak("Ошибка.")
return
if answer.lower() == 'нет' or not answer:
_speak("Это не математика.")
return
_speak(f"Получилось {answer}.")
def do_theme_set(action, voice: str):
is_light = action.get('mode') == 'light'
val = 1 if is_light else 0
key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
ps = (
f"Set-ItemProperty -Path 'Registry::{key}' -Name AppsUseLightTheme -Value {val} -Type DWord; "
f"Set-ItemProperty -Path 'Registry::{key}' -Name SystemUsesLightTheme -Value {val} -Type DWord"
)
res = subprocess.run(['powershell', '-NoProfile', '-Command', ps],
capture_output=True, text=True, timeout=10)
if res.returncode == 0:
_speak("Светлая тема включена." if is_light else "Тёмная тема включена.")
else:
_speak("Не получилось переключить тему.")
def _load_projects_config():
user_profile = os.environ.get('USERPROFILE', os.path.expanduser('~'))
cfg_path = os.path.join(user_profile, 'Documents', 'jarvis-projects.json')
if not os.path.isfile(cfg_path):
sample = [
{"name": "jarvis-rust", "path": os.path.join(user_profile, "Jarvis", "rust")},
{"name": "jarvis-python", "path": os.path.join(user_profile, "Jarvis", "python")},
{"name": "dietpi", "path": os.path.join(user_profile, "Desktop", "Dietpi")},
]
os.makedirs(os.path.dirname(cfg_path), exist_ok=True)
with open(cfg_path, 'w', encoding='utf-8') as f:
json.dump(sample, f, ensure_ascii=False, indent=2)
return cfg_path, sample, True
try:
with open(cfg_path, 'r', encoding='utf-8') as f:
return cfg_path, json.load(f), False
except (OSError, json.JSONDecodeError) as e:
print(f"projects config read failed: {e}")
return cfg_path, [], False
def do_open_project(action, voice: str):
cfg_path, entries, created = _load_projects_config()
if created:
_speak("Создал шаблон в Documents, отредактируй и повтори.")
subprocess.Popen(['cmd', '/c', 'start', '', cfg_path], shell=False)
return
if not entries:
_speak("Проектов в конфиге нет.")
return
triggers = ('открой папку проекта', 'запусти проект', 'открой проект', 'проект',
'open project', 'launch project', 'project',
'відкрий проект')
query = _strip_trigger(voice, triggers).lower().strip()
if not query:
_speak("Какой проект?")
return
match = (next((e for e in entries if e['name'].lower() == query), None)
or next((e for e in entries if query in e['name'].lower()), None)
or next((e for e in entries if e['name'].lower() in query), None))
if not match:
_speak(f"Не нашёл {query}.")
return
path = match['path']
if not os.path.exists(path):
_speak(f"Путь не существует: {path}")
return
code_check = subprocess.run(['where', 'code.cmd'], capture_output=True, text=True)
if code_check.returncode == 0 and code_check.stdout.strip():
subprocess.Popen(['cmd', '/c', 'code', path], shell=False)
else:
subprocess.Popen(['cmd', '/c', 'start', '', path], shell=False)
_speak(f"Открываю {match['name']}.")
def do_list_projects(action, voice: str):
cfg_path, entries, created = _load_projects_config()
if created or not entries:
_speak("Список пуст.")
return
names = [e['name'] for e in entries]
_speak(f"Проектов {len(names)}: {', '.join(names)}.")
def do_audio_devices_panel(action, voice: str):
# Full programmatic switching needs IPolicyConfig + IMMDeviceEnumerator
# via comtypes; out of scope for this pass. Opens Sound control panel so
@ -519,6 +723,11 @@ def do_audio_devices_panel(action, voice: str):
_speak("Открываю настройки звука, сэр.")
def do_open_sound_panel(action, voice: str):
subprocess.Popen(['rundll32.exe', 'shell32.dll,Control_RunDLL', 'mmsys.cpl,,1'])
_speak("Открываю панель звука.")
def do_sysinfo(action, voice: str):
topic = action.get('topic', 'all')
@ -615,6 +824,18 @@ def run_action(action):
do_process_kill(action, _current_voice or '')
elif t == 'websearch':
do_websearch(action, _current_voice or '')
elif t == 'translate':
do_translate(action, _current_voice or '')
elif t == 'math_eval':
do_math_eval(action, _current_voice or '')
elif t == 'theme_set':
do_theme_set(action, _current_voice or '')
elif t == 'open_project':
do_open_project(action, _current_voice or '')
elif t == 'list_projects':
do_list_projects(action, _current_voice or '')
elif t == 'open_sound_panel':
do_open_sound_panel(action, _current_voice or '')
_current_voice: str = ''