Initial commit: Cosmo voice assistant

Полностью локальный голосовой ассистент на Python.

Стек:
- Wake word: openWakeWord (onnxruntime)
- STT: RealtimeSTT + faster-whisper + Silero VAD (CUDA)
- LLM-агент: smolagents ToolCallingAgent + Ollama qwen2.5:7b
- TTS: Silero V4 (torch.hub) + sounddevice
- Shell: Git Bash (Windows) / bash (macOS)

Поддерживает Windows и macOS. Агент с памятью и tool calling —
находит программы самостоятельно, запоминает пути, выполняет
произвольные shell-команды.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
d.klimov
2026-04-10 15:58:12 +03:00
commit 6010816f1d
23 changed files with 1969 additions and 0 deletions

87
cosmo/transcriber.py Normal file
View File

@@ -0,0 +1,87 @@
"""
STT модуль на базе RealtimeSTT.
Использует faster-whisper + Silero VAD под капотом.
Поддерживает стриминг — partial transcriptions во время речи.
"""
import threading
from RealtimeSTT import AudioToTextRecorder
from loguru import logger
class Transcriber:
def __init__(self, config: dict):
whisper_cfg = config["whisper"]
audio_cfg = config["audio"]
self._recorder: AudioToTextRecorder | None = None
self._config = {
"model": whisper_cfg["model_size"],
"language": whisper_cfg["language"],
"device": whisper_cfg["device"],
"compute_type": whisper_cfg["compute_type"],
# Silero VAD параметры
"silero_sensitivity": 0.4,
"webrtc_sensitivity": 3,
"post_speech_silence_duration": audio_cfg["silence_duration"],
"min_length_of_recording": 0.3,
"min_gap_between_recordings": 0.01,
# Отключаем wake word в RealtimeSTT — используем свой
"wakeword_backend": "none",
# Не запускать в режиме непрерывного прослушивания
"use_microphone": True,
"spinner": False,
"level": 0, # минимальный лог уровень внутри RealtimeSTT
}
logger.info(
f"Инициализирую RealtimeSTT: модель={whisper_cfg['model_size']}, "
f"device={whisper_cfg['device']}, compute={whisper_cfg['compute_type']}"
)
self._init_recorder()
def _init_recorder(self):
try:
self._recorder = AudioToTextRecorder(**self._config)
logger.info("RealtimeSTT готов")
except Exception as e:
logger.error(f"Ошибка инициализации RealtimeSTT: {e}")
raise
def record_and_transcribe(self, on_partial: callable = None) -> str:
"""
Записывает команду и транскрибирует.
on_partial(text) — опциональный колбэк для частичных результатов.
Возвращает финальный текст.
"""
if self._recorder is None:
self._init_recorder()
result_holder = []
done_event = threading.Event()
def on_text(text: str):
result_holder.append(text)
done_event.set()
# Partial results — показываем что слышим в реальном времени
if on_partial:
self._recorder.on_realtime_transcription_update = on_partial
logger.info("Слушаю команду...")
self._recorder.text(on_text)
done_event.wait(timeout=12.0)
text = result_holder[0].strip() if result_holder else ""
if text:
logger.info(f"Транскрипция: '{text}'")
else:
logger.info("Команда не распознана (тишина или таймаут)")
return text
def shutdown(self):
if self._recorder:
try:
self._recorder.shutdown()
except Exception:
pass