"""
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="{time:HH:mm:ss} | {level: <8} | {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):
self.tts.say_async("Слушаю")
# 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()