Files
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

146 lines
4.6 KiB
Python
Raw Permalink 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.
"""
Cosmo — локальный голосовой ассистент.
Стек: openWakeWord → RealtimeSTT (Whisper + Silero VAD) → smolagents + Ollama → RealtimeTTS (Silero)
Запуск:
bash run.sh
python cosmo/main.py
"""
import sys
import os
import argparse
import threading
import yaml
# Указываем pydub где искать ffmpeg (установлен через imageio-ffmpeg)
try:
import imageio_ffmpeg
from pydub import AudioSegment
AudioSegment.converter = imageio_ffmpeg.get_ffmpeg_exe()
except Exception:
pass
from loguru import logger
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from cosmo.wake_word import WakeWordDetector
from cosmo.transcriber import Transcriber
from cosmo.memory import Memory
from cosmo.agent import Agent
from cosmo.tts import TTS
def load_config(path: str) -> dict:
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def setup_logging(config: dict):
log_cfg = config.get("logging", {})
level = log_cfg.get("level", "INFO")
log_file = log_cfg.get("file", "logs/cosmo.log")
os.makedirs(os.path.dirname(log_file), exist_ok=True)
logger.remove()
logger.add(
sys.stderr, level=level, colorize=True,
format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | {message}",
)
logger.add(log_file, level="DEBUG", rotation="10 MB", retention="7 days", encoding="utf-8")
class Cosmo:
def __init__(self, config: dict):
self.config = config
self.name = config["assistant"]["name"]
self._running = False
self._command_event = threading.Event()
logger.info("Инициализирую модули...")
self.tts = TTS(config)
self.transcriber = Transcriber(config)
self.memory = Memory()
self.agent = Agent(config, self.memory)
self.wake_word = WakeWordDetector(config, on_detected_callback=self._on_wake_word)
def _on_wake_word(self):
logger.info(f"=== {self.name} активирован! ===")
self._command_event.set()
def _process_command(self):
# Синхронно — сначала говорим, потом слушаем (иначе TTS мешает записи)
self.tts.say("Слушаю")
# Partial results — печатаем в лог что слышим в реальном времени
def on_partial(text):
logger.debug(f"[partial] {text}")
text = self.transcriber.record_and_transcribe(on_partial=on_partial)
if not text.strip():
self.tts.say_async("Не расслышал, попробуй ещё раз")
return
# Агент обрабатывает команду и возвращает ответ для TTS
response = self.agent.run(text)
self.tts.say(response)
def run(self):
self._running = True
self.wake_word.start()
logger.info("=" * 50)
logger.info(f" {self.name} запущен!")
logger.info(f" Скажи '{self.name}' чтобы активировать.")
logger.info(f" Ctrl+C для выхода.")
logger.info("=" * 50)
self.tts.say(f"{self.name} запущен")
try:
while self._running:
triggered = self._command_event.wait(timeout=0.5)
if triggered and self._running:
self._command_event.clear()
try:
self._process_command()
finally:
self.wake_word.resume()
except KeyboardInterrupt:
logger.info("Ctrl+C — завершаю...")
finally:
self.shutdown()
def shutdown(self):
logger.info("Завершаю работу...")
self._running = False
self.wake_word.stop()
self.transcriber.shutdown()
logger.info(f"{self.name} остановлен")
def main():
parser = argparse.ArgumentParser(description="Cosmo — голосовой ассистент")
parser.add_argument("--config", default="config/config.yaml")
args = parser.parse_args()
config_path = args.config
if not os.path.isabs(config_path):
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
config_path = os.path.join(project_root, config_path)
if not os.path.exists(config_path):
print(f"Конфиг не найден: {config_path}")
sys.exit(1)
config = load_config(config_path)
setup_logging(config)
Cosmo(config).run()
if __name__ == "__main__":
main()