diff --git a/commands.yaml b/commands.yaml index 73bba1b..1640b1f 100644 --- a/commands.yaml +++ b/commands.yaml @@ -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: - спасибо diff --git a/main.py b/main.py index 865c359..53839b9 100644 --- a/main.py +++ b/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 = ''