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>
This commit is contained in:
@@ -19,7 +19,7 @@ else:
|
||||
from cosmo.tools import ALL_TOOLS, set_memory
|
||||
_PLATFORM_NOTE = "Windows. Используй Git Bash, 'start' для запуска приложений."
|
||||
|
||||
SYSTEM_PROMPT = f"""Ты — Cosmo, умный голосовой ассистент. Платформа: {_PLATFORM_NOTE}
|
||||
SYSTEM_PROMPT = """Ты — Cosmo, умный голосовой ассистент. Платформа: """ + _PLATFORM_NOTE + """
|
||||
|
||||
Правила:
|
||||
1. Используй инструменты для выполнения задач — не выдумывай результаты
|
||||
|
||||
@@ -71,7 +71,8 @@ class Cosmo:
|
||||
self._command_event.set()
|
||||
|
||||
def _process_command(self):
|
||||
self.tts.say_async("Слушаю")
|
||||
# Синхронно — сначала говорим, потом слушаем (иначе TTS мешает записи)
|
||||
self.tts.say("Слушаю")
|
||||
|
||||
# Partial results — печатаем в лог что слышим в реальном времени
|
||||
def on_partial(text):
|
||||
|
||||
@@ -61,9 +61,10 @@ def find_program(name: str) -> str:
|
||||
"""
|
||||
Найти программу или приложение на macOS по имени.
|
||||
Ищет в PATH, /Applications, ~/Applications и через Spotlight (mdfind).
|
||||
Возвращает путь. Чтобы запустить — используй open_app.
|
||||
|
||||
Args:
|
||||
name: имя программы, например 'webstorm', 'chrome', 'cursor'
|
||||
name: имя программы, например 'webstorm', 'chrome', 'cursor', 'safari'
|
||||
"""
|
||||
stem = name.strip()
|
||||
logger.info(f"[find_program] {stem}")
|
||||
@@ -92,6 +93,27 @@ def find_program(name: str) -> str:
|
||||
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:
|
||||
"""
|
||||
@@ -197,6 +219,7 @@ def memory_list(prefix: str = "") -> str:
|
||||
ALL_TOOLS = [
|
||||
run_shell,
|
||||
find_program,
|
||||
open_app,
|
||||
open_browser,
|
||||
read_file,
|
||||
write_file,
|
||||
|
||||
@@ -20,11 +20,13 @@ class Transcriber:
|
||||
"language": whisper_cfg["language"],
|
||||
"device": whisper_cfg["device"],
|
||||
"compute_type": whisper_cfg["compute_type"],
|
||||
# Подсказка для Whisper — улучшает распознавание русского
|
||||
"initial_prompt": whisper_cfg.get("initial_prompt", ""),
|
||||
# Silero VAD параметры
|
||||
"silero_sensitivity": 0.4,
|
||||
"webrtc_sensitivity": 3,
|
||||
"post_speech_silence_duration": audio_cfg["silence_duration"],
|
||||
"min_length_of_recording": 0.3,
|
||||
"min_length_of_recording": 0.5,
|
||||
"min_gap_between_recordings": 0.01,
|
||||
# Отключаем wake word в RealtimeSTT — используем свой
|
||||
"wakeword_backend": "none",
|
||||
|
||||
23
cosmo/tts.py
23
cosmo/tts.py
@@ -32,6 +32,10 @@ class TTS:
|
||||
self.enabled = False
|
||||
return
|
||||
|
||||
# Оптимизация CPU-инференса на Apple Silicon
|
||||
num_threads = config.get("performance", {}).get("num_threads", 4)
|
||||
torch.set_num_threads(num_threads)
|
||||
|
||||
self._load_model()
|
||||
|
||||
def _load_model(self):
|
||||
@@ -52,16 +56,33 @@ class TTS:
|
||||
logger.warning("TTS отключён")
|
||||
self.enabled = False
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_text(text: str) -> str:
|
||||
"""Заменяет латиницу на читаемый русский для TTS."""
|
||||
import re
|
||||
# Транслитерация частых англ. слов которые Silero не прочитает
|
||||
text = re.sub(r'[Ss]afari', 'Сафари', text)
|
||||
text = re.sub(r'[Cc]hrome', 'Хром', text)
|
||||
text = re.sub(r'[Tt]elegram', 'Телеграм', text)
|
||||
text = re.sub(r'[Ww]eb[Ss]torm', 'ВебШторм', text)
|
||||
text = re.sub(r'[Vv][Ss]\s?[Cc]ode', 'ВиЭс Код', text)
|
||||
# Оставшиеся латинские слова — убираем, чтобы Silero не зависал
|
||||
text = re.sub(r'[A-Za-z]+', '', text)
|
||||
# Убираем лишние пробелы
|
||||
text = re.sub(r'\s+', ' ', text).strip()
|
||||
return text if text else "Готово"
|
||||
|
||||
def say(self, text: str):
|
||||
"""Произнести текст синхронно."""
|
||||
if not self.enabled or self._model is None:
|
||||
logger.info(f"[TTS]: {text}")
|
||||
return
|
||||
|
||||
text = self._sanitize_text(text)
|
||||
logger.debug(f"TTS: '{text}'")
|
||||
with self._lock:
|
||||
try:
|
||||
with torch.no_grad():
|
||||
with torch.inference_mode():
|
||||
audio = self._model.apply_tts(
|
||||
text=text,
|
||||
speaker=self.speaker,
|
||||
|
||||
@@ -5,6 +5,7 @@ Wake word detector для Cosmo.
|
||||
|
||||
import os
|
||||
import glob
|
||||
import time
|
||||
import threading
|
||||
import queue
|
||||
import numpy as np
|
||||
@@ -31,7 +32,8 @@ class WakeWordDetector:
|
||||
self._thread = None
|
||||
|
||||
# Порог уверенности для срабатывания (0.0 – 1.0)
|
||||
self.threshold = 0.5
|
||||
# 0.7 — баланс между надёжностью и защитой от ложных срабатываний/эха TTS
|
||||
self.threshold = 0.7
|
||||
|
||||
logger.info("Загружаю wake word модель openwakeword...")
|
||||
|
||||
@@ -88,14 +90,18 @@ class WakeWordDetector:
|
||||
"""Приостановить детект (пока идёт запись команды)."""
|
||||
self._paused = True
|
||||
|
||||
def resume(self):
|
||||
"""Возобновить детект после записи команды."""
|
||||
# Очищаем очередь, чтобы не срабатывать на эхо
|
||||
def resume(self, cooldown: float = 1.5):
|
||||
"""Возобновить детект после записи команды с защитой от эха."""
|
||||
# Ждём пока эхо от TTS затухнет
|
||||
time.sleep(cooldown)
|
||||
# Очищаем очередь — там буферизованный звук TTS
|
||||
while not self._audio_queue.empty():
|
||||
try:
|
||||
self._audio_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
# Сбрасываем внутреннее состояние модели (накопленные скоры)
|
||||
self.model.reset()
|
||||
self._paused = False
|
||||
logger.debug("Wake word детектор возобновлён")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user