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:
parent
7d83bf8299
commit
3795a0b4ec
2 changed files with 283 additions and 0 deletions
|
|
@ -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
221
main.py
|
|
@ -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 = ''
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue