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}
|
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:
|
thanks:
|
||||||
phrases:
|
phrases:
|
||||||
- спасибо
|
- спасибо
|
||||||
|
|
|
||||||
221
main.py
221
main.py
|
|
@ -511,6 +511,210 @@ def do_websearch(action, voice: str):
|
||||||
_speak(f"Открываю {svc}: {query[:100]}")
|
_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):
|
def do_audio_devices_panel(action, voice: str):
|
||||||
# Full programmatic switching needs IPolicyConfig + IMMDeviceEnumerator
|
# Full programmatic switching needs IPolicyConfig + IMMDeviceEnumerator
|
||||||
# via comtypes; out of scope for this pass. Opens Sound control panel so
|
# 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("Открываю настройки звука, сэр.")
|
_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):
|
def do_sysinfo(action, voice: str):
|
||||||
topic = action.get('topic', 'all')
|
topic = action.get('topic', 'all')
|
||||||
|
|
||||||
|
|
@ -615,6 +824,18 @@ def run_action(action):
|
||||||
do_process_kill(action, _current_voice or '')
|
do_process_kill(action, _current_voice or '')
|
||||||
elif t == 'websearch':
|
elif t == 'websearch':
|
||||||
do_websearch(action, _current_voice or '')
|
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 = ''
|
_current_voice: str = ''
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue