Files
cosmo-voice-assistant/cosmo/tools_mac.py
Daniil Klimov 110d9cde29 Mac M1 optimizations, fix train pipeline, add Hey Cosmo wake word model
- Fix install_mac.sh: use venv + Python 3.12 (3.14 incompatible with ML libs)
- Fix run_mac.sh: activate venv, add CPU thread optimization env vars
- Fix agent.py: remove f-string from SYSTEM_PROMPT template (NameError on import)
- Add missing deps: sounddevice, pydub, imageio-ffmpeg, omegaconf
- Optimize for M1: torch.inference_mode, set_num_threads, OMP/MKL tuning
- Switch to qwen2.5:3b for faster LLM responses on Mac
- Switch Whisper to medium model with auto compute (small+int8 had poor Russian)
- Add initial_prompt for better Russian transcription
- Add open_app tool for native macOS app launching
- Fix TTS: sanitize Latin text to Cyrillic for Silero compatibility
- Fix wake word echo: add cooldown after TTS, reset model state, raise threshold
- Make "Слушаю" TTS synchronous to avoid mic interference
- Fix train Dockerfile: remove tensorflow/onnx2tf (only ONNX needed), fix deps
- Fix train.sh: use wget for dataset download, add --shm-size=2g
- Add trained hey_cosmo.onnx wake word model
- Add TODO section to CLAUDE.md (ChatterBox TTS, Ollama Modelfile ideas)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:19:53 +03:00

230 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Инструменты агента для smolagents — macOS версия.
Отличия от Windows: нативный bash, поиск через mdfind/Spotlight,
запуск приложений через 'open -a', нет реестра.
"""
import os
import subprocess
import webbrowser
import urllib.parse
from loguru import logger
from smolagents import tool
from cosmo.memory import Memory
_memory: Memory | None = None
def set_memory(mem: Memory):
global _memory
_memory = mem
# ------------------------------------------------------------------
# Инструменты
# ------------------------------------------------------------------
@tool
def run_shell(command: str) -> str:
"""
Выполнить команду в bash и получить вывод.
На macOS доступны все стандартные unix-команды.
Для запуска приложений используй: open -a "AppName" или open /path/to/app.
Для поиска файлов: mdfind, find, which.
Args:
command: bash команда для выполнения
"""
logger.info(f"[run_shell] {command}")
try:
result = subprocess.run(
["bash", "-c", command],
capture_output=True,
text=True,
timeout=20,
encoding="utf-8",
errors="replace",
)
output = result.stdout.strip()
stderr = result.stderr.strip()
if result.returncode != 0 and stderr:
return f"[returncode={result.returncode}]\nstdout: {output}\nstderr: {stderr}"
return output if output else f"[выполнено, returncode={result.returncode}]"
except subprocess.TimeoutExpired:
return "[таймаут 20с]"
except Exception as e:
return f"[ошибка: {e}]"
@tool
def find_program(name: str) -> str:
"""
Найти программу или приложение на macOS по имени.
Ищет в PATH, /Applications, ~/Applications и через Spotlight (mdfind).
Возвращает путь. Чтобы запустить — используй open_app.
Args:
name: имя программы, например 'webstorm', 'chrome', 'cursor', 'safari'
"""
stem = name.strip()
logger.info(f"[find_program] {stem}")
steps = [
# which — ищет в PATH
f"which {stem} 2>/dev/null",
# Spotlight — самый надёжный способ найти .app
f"mdfind 'kMDItemKind == \"Application\"' | grep -i '{stem}' | head -3",
# /Applications напрямую
f"find /Applications -maxdepth 2 -iname '*{stem}*.app' 2>/dev/null | head -3",
# ~/Applications
f"find ~/Applications -maxdepth 2 -iname '*{stem}*.app' 2>/dev/null | head -3",
# Homebrew bin
f"find /opt/homebrew/bin /usr/local/bin -iname '*{stem}*' 2>/dev/null | head -3",
]
results = []
for cmd in steps:
out = run_shell(cmd)
if out and not out.startswith("[") and len(out) > 3:
results.append(out.strip())
if results:
return "Найдено:\n" + "\n".join(results)
return f"Программа '{name}' не найдена."
@tool
def open_app(name: str) -> str:
"""
Запустить приложение на macOS по имени или полному пути.
Примеры: open_app("Safari"), open_app("/Applications/Safari.app"), open_app("Telegram").
Args:
name: имя приложения или полный путь к .app
"""
logger.info(f"[open_app] {name}")
# Если передан полный путь к .app
if name.endswith(".app") and os.path.exists(name):
result = run_shell(f'open "{name}"')
else:
# Пробуем по имени через open -a
result = run_shell(f'open -a "{name}"')
if result.startswith("[ошибка") or "returncode=1" in result:
return f"Не удалось открыть '{name}'. Попробуй find_program чтобы найти точное имя."
return f"Приложение '{name}' запущено."
@tool
def open_browser(url: str, search: bool = False) -> str:
"""
Открыть URL в браузере или выполнить поиск в Google.
Args:
url: полный URL (https://...) или поисковый запрос если search=True
search: если True — выполнить поиск Google по тексту в url
"""
if search:
url = "https://www.google.com/search?q=" + urllib.parse.quote(url)
elif not url.startswith(("http://", "https://")):
url = "https://" + url
webbrowser.open(url)
logger.info(f"[open_browser] {url}")
return f"Открыт браузер: {url}"
@tool
def read_file(path: str) -> str:
"""
Прочитать содержимое текстового файла.
Args:
path: абсолютный путь к файлу
"""
try:
with open(path, "r", encoding="utf-8", errors="replace") as f:
return f.read(8000)
except FileNotFoundError:
return f"Файл не найден: {path}"
except Exception as e:
return f"Ошибка чтения: {e}"
@tool
def write_file(path: str, content: str) -> str:
"""
Записать текст в файл (создаёт или перезаписывает).
Args:
path: абсолютный путь к файлу
content: содержимое файла
"""
try:
dir_path = os.path.dirname(path)
if dir_path:
os.makedirs(dir_path, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(content)
return f"Файл записан: {path}"
except Exception as e:
return f"Ошибка записи: {e}"
@tool
def memory_set(key: str, value: str) -> str:
"""
Сохранить факт в долгосрочную память ассистента.
Используй для запоминания путей программ, предпочтений пользователя.
Ключи: 'app.название', 'user.имя', 'pref.что-то'.
Args:
key: ключ факта, например 'app.webstorm'
value: значение для сохранения
"""
if _memory is None:
return "Память не инициализирована"
_memory.set(key, value)
return f"Запомнено: {key} = {value!r}"
@tool
def memory_get(key: str) -> str:
"""
Получить сохранённый факт из долгосрочной памяти по ключу.
Args:
key: ключ факта, например 'app.webstorm'
"""
if _memory is None:
return "Память не инициализирована"
value = _memory.get(key)
return f"{key} = {value!r}" if value else f"Факт '{key}' не найден"
@tool
def memory_list(prefix: str = "") -> str:
"""
Показать все факты из памяти, опционально по префиксу.
Args:
prefix: префикс для фильтрации, например 'app.'
"""
if _memory is None:
return "Память не инициализирована"
facts = _memory.list_facts(prefix)
if not facts:
return "Память пуста" if not prefix else f"Нет фактов с префиксом '{prefix}'"
return "\n".join(f"{k}: {v}" for k, v in sorted(facts.items()))
ALL_TOOLS = [
run_shell,
find_program,
open_app,
open_browser,
read_file,
write_file,
memory_set,
memory_get,
memory_list,
]