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

92
train_wakeword/Dockerfile Normal file
View File

@@ -0,0 +1,92 @@
# Dockerfile для обучения wake word модели openWakeWord
# Python 3.11 + torch 2.5 (последний совместимый с py3.11) + рабочие зависимости 2026
FROM python:3.11-slim
WORKDIR /app
# Системные зависимости (включая build-essential для webrtcvad)
RUN apt-get update && apt-get install -y \
git wget curl ffmpeg libsndfile1 \
build-essential python3-dev \
&& rm -rf /var/lib/apt/lists/*
# Клонируем openWakeWord и piper-sample-generator
RUN git clone https://github.com/dscripka/openWakeWord /openWakeWord
RUN git clone https://github.com/rhasspy/piper-sample-generator /piper-sample-generator
# Torch 2.5.0 — последний для Python 3.11, CPU версия (обучение не требует GPU)
RUN pip install --no-cache-dir \
torch==2.5.0 \
torchaudio==2.5.0 \
--index-url https://download.pytorch.org/whl/cpu
# Зависимости обучения с совместимыми версиями
RUN pip install --no-cache-dir \
mutagen==1.47.0 \
torchinfo==1.8.0 \
torchmetrics==1.2.0 \
speechbrain==1.0.3 \
audiomentations==0.43.1 \
torch-audiomentations==0.12.0 \
pronouncing==0.2.0 \
"datasets==2.20.0" \
"pyarrow==14.0.2" \
"fsspec==2023.12.2" \
acoustics==0.2.6 \
webrtcvad \
onnx \
onnxruntime \
onnx2tf \
pyyaml scipy scikit-learn tqdm
# TFLite конвертация через onnx2tf (замена мёртвого onnx_tf)
# Патчим train.py чтобы использовал onnx2tf вместо onnx_tf
RUN pip install --no-cache-dir \
tensorflow-cpu==2.21.0 \
tensorflow_probability==0.24.0
RUN pip install --no-cache-dir -e /openWakeWord
# Патч: заменяем onnx_tf на onnx2tf в train.py
RUN python - <<'EOF'
import re, pathlib
train_py = pathlib.Path("/openWakeWord/openwakeword/train.py")
text = train_py.read_text()
# Заменяем импорт onnx_tf
text = text.replace(
"import onnx_tf",
"import onnx2tf as onnx_tf_compat"
)
text = text.replace(
"from onnx_tf.backend import prepare",
"# onnx_tf replaced by onnx2tf"
)
# Заменяем вызов convert_onnx_to_tflite если он есть
text = re.sub(
r"onnx_tf\.backend\.prepare\(.*?\)",
"None # onnx2tf handles tflite conversion differently",
text, flags=re.DOTALL
)
train_py.write_text(text)
print("train.py patched OK")
EOF
# Устанавливаем piper-sample-generator
RUN pip install --no-cache-dir -e /piper-sample-generator 2>/dev/null || \
pip install --no-cache-dir piper-tts
# Скачиваем TTS модель LibriTTS-R medium (~66 MB) для генерации примеров
RUN mkdir -p /piper-sample-generator/models && \
wget -q --show-progress \
-O /piper-sample-generator/models/en_US-libritts_r-medium.onnx \
"https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/libritts_r/medium/en_US-libritts_r-medium.onnx" && \
wget -q \
-O /piper-sample-generator/models/en_US-libritts_r-medium.onnx.json \
"https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/libritts_r/medium/en_US-libritts_r-medium.onnx.json"
RUN mkdir -p /data /output /samples
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,53 @@
# Конфиг для обучения wake word модели "Hey Cosmo"
# Документация: https://github.com/dscripka/openWakeWord
model_name: "hey_cosmo"
output_dir: "/output"
# Целевая фраза — "hey cosmo" работает лучше чем просто "cosmo"
target_phrase:
- "hey cosmo"
- "cosmo"
# Похожие слова для улучшения устойчивости к ложным срабатываниям
custom_negative_phrases:
- "hey cosmos"
- "hey cosmic"
- "hey cosplay"
- "hey presto"
- "hey como"
- "hey koz"
- "cozmo"
# Количество синтетических примеров
n_samples: 10000
n_samples_val: 1000
tts_batch_size: 25
# Аугментация
augmentation_batch_size: 16
augmentation_rounds: 2
# Пути внутри Docker контейнера
piper_sample_generator_path: "/piper-sample-generator"
false_positive_validation_data_path: "/data/validation_set_features.npy"
feature_data_files:
"ACAV100M_sample": "/data/openwakeword_features_ACAV100M_2000_hrs_16bit.npy"
batch_n_per_class:
"ACAV100M_sample": 1024
"adversarial_negative": 50
"positive": 50
# Архитектура модели
model_type: "dnn"
layer_size: 32
steps: 50000
# Цели качества
max_negative_weight: 1500
target_false_positives_per_hour: 0.5
target_accuracy: 0.7
target_recall: 0.5
lr: 0.0001

View File

@@ -0,0 +1,42 @@
#!/bin/bash
set -e
echo "============================================"
echo " Обучение wake word модели 'Hey Cosmo'"
echo "============================================"
CONFIG="/app/cosmo_config.yaml"
OWW="/openWakeWord/openwakeword/train.py"
# Шаг 1: Генерация синтетических аудио примеров через TTS
echo ""
echo "[1/3] Генерирую синтетические примеры через TTS..."
python "$OWW" --training_config "$CONFIG" --generate_clips
echo " Готово."
# Шаг 2: Аугментация (шумы, реверберация, разные условия)
echo ""
echo "[2/3] Аугментирую примеры (шумы, реверберация)..."
python "$OWW" --training_config "$CONFIG" --augment_clips
echo " Готово."
# Шаг 3: Обучение модели
echo ""
echo "[3/3] Обучаю модель..."
python "$OWW" --training_config "$CONFIG" --train_model
echo " Готово."
# Копируем результат
echo ""
echo "============================================"
if ls /output/hey_cosmo*.onnx 1>/dev/null 2>&1; then
echo " Модель обучена успешно!"
echo " Файлы в папке models/:"
ls -lh /output/*.onnx 2>/dev/null || true
ls -lh /output/*.tflite 2>/dev/null || true
else
echo " ОШИБКА: .onnx файл не найден в /output"
echo " Проверь логи выше."
exit 1
fi
echo "============================================"

View File

@@ -0,0 +1,114 @@
"""
Запись голосовых примеров для обучения wake word модели.
Запускай: python train_wakeword/record_samples.py
Скрипт записывает N примеров слова "Hey Cosmo" с паузами между ними.
Файлы сохраняются в train_wakeword/samples/positive/
"""
import os
import sys
import time
import wave
import struct
import threading
try:
import pyaudio
except ImportError:
print("Установи pyaudio: pip install pyaudio")
sys.exit(1)
# --- Настройки ---
WAKE_WORD = "Hey Cosmo"
N_SAMPLES = 30 # сколько примеров записать
RECORD_SECS = 2.0 # длина одной записи (сек)
PAUSE_SECS = 2.0 # пауза между записями (сек)
SAMPLE_RATE = 16000
CHANNELS = 1
CHUNK = 512
OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "samples", "positive")
os.makedirs(OUTPUT_DIR, exist_ok=True)
def record_clip(pa: pyaudio.PyAudio, filename: str, duration: float):
stream = pa.open(
format=pyaudio.paInt16,
channels=CHANNELS,
rate=SAMPLE_RATE,
input=True,
frames_per_buffer=CHUNK,
)
frames = []
n_chunks = int(SAMPLE_RATE / CHUNK * duration)
for _ in range(n_chunks):
frames.append(stream.read(CHUNK, exception_on_overflow=False))
stream.stop_stream()
stream.close()
with wave.open(filename, "wb") as wf:
wf.setnchannels(CHANNELS)
wf.setsampwidth(pa.get_sample_size(pyaudio.paInt16))
wf.setframerate(SAMPLE_RATE)
wf.writeframes(b"".join(frames))
def countdown(seconds: int):
for i in range(seconds, 0, -1):
print(f"\r {i}...", end="", flush=True)
time.sleep(1)
print("\r Говори! ", end="", flush=True)
def main():
# Считаем уже записанные файлы
existing = [f for f in os.listdir(OUTPUT_DIR) if f.endswith(".wav")]
start_idx = len(existing)
if start_idx >= N_SAMPLES:
print(f"Уже записано {start_idx} примеров. Для перезаписи удали папку {OUTPUT_DIR}")
return
pa = pyaudio.PyAudio()
print("=" * 50)
print(f" Запись примеров wake word: \"{WAKE_WORD}\"")
print(f" Нужно записать: {N_SAMPLES} примеров")
print(f" Уже есть: {start_idx}")
print(f" Длина каждой записи: {RECORD_SECS} сек")
print("=" * 50)
print()
print("Инструкция:")
print(" - Говори чётко и естественно")
print(" - Меняй интонацию, темп, громкость")
print(" - Можно говорить чуть тише / громче / быстрее")
print(" - Представь что реально обращаешься к ассистенту")
print()
input(" Нажми Enter когда готов начать...")
print()
for i in range(start_idx, N_SAMPLES):
num = i + 1
filename = os.path.join(OUTPUT_DIR, f"hey_cosmo_{num:03d}.wav")
print(f"[{num:2d}/{N_SAMPLES}] Приготовься... ", end="", flush=True)
countdown(2)
record_clip(pa, filename, RECORD_SECS)
print(f" ✓ записано")
if num < N_SAMPLES:
time.sleep(PAUSE_SECS)
pa.terminate()
print()
print("=" * 50)
print(f" Готово! Записано {N_SAMPLES} примеров.")
print(f" Папка: {OUTPUT_DIR}")
print()
print(" Следующий шаг:")
print(" bash train_wakeword/train.sh")
print("=" * 50)
if __name__ == "__main__":
main()

119
train_wakeword/train.sh Normal file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
MODELS_DIR="$PROJECT_DIR/models"
SAMPLES_DIR="$SCRIPT_DIR/samples"
DATA_DIR="$SCRIPT_DIR/docker_data"
echo "============================================"
echo " Cosmo Wake Word — обучение модели"
echo "============================================"
echo ""
# Проверяем Docker
if ! command -v docker &>/dev/null; then
echo "ОШИБКА: Docker не найден."
echo "Установи Docker Desktop: https://www.docker.com/products/docker-desktop/"
exit 1
fi
if ! docker info &>/dev/null; then
echo "ОШИБКА: Docker не запущен. Запусти Docker Desktop и попробуй снова."
exit 1
fi
# Проверяем что есть записанные примеры
POSITIVE_DIR="$SAMPLES_DIR/positive"
if [ ! -d "$POSITIVE_DIR" ] || [ -z "$(ls "$POSITIVE_DIR"/*.wav 2>/dev/null)" ]; then
echo "Записанные примеры не найдены."
echo "Сначала запусти запись голоса:"
echo " python train_wakeword/record_samples.py"
echo ""
read -p "Продолжить без записанных примеров? (используются только TTS) [y/N]: " yn
case "$yn" in
[Yy]*) echo "Продолжаем с только TTS примерами..." ;;
*) exit 0 ;;
esac
fi
mkdir -p "$MODELS_DIR" "$DATA_DIR"
# Собираем Docker образ
echo "[1/4] Собираю Docker образ (первый раз ~5-10 мин)..."
docker build -t cosmo-wakeword-trainer "$SCRIPT_DIR" --quiet
echo " Образ готов."
echo ""
# Скачиваем датасет негативных примеров если нет
NEGATIVE_FEATURES="$DATA_DIR/openwakeword_features_ACAV100M_2000_hrs_16bit.npy"
VALIDATION_FEATURES="$DATA_DIR/validation_set_features.npy"
if [ ! -f "$NEGATIVE_FEATURES" ]; then
echo "[2/4] Скачиваю негативный датасет (~20 GB, один раз)..."
echo " Это займёт время в зависимости от скорости интернета."
docker run --rm \
-v "$DATA_DIR:/data" \
cosmo-wakeword-trainer \
python -c "
from datasets import load_dataset
import numpy as np, os
print('Скачиваю ACAV100M features...')
ds = load_dataset('davidscripka/openwakeword_features', 'ACAV100M_2000_hrs_16bit', split='train')
arr = np.array(ds['features'])
np.save('/data/openwakeword_features_ACAV100M_2000_hrs_16bit.npy', arr)
print('Скачиваю validation features...')
ds_val = load_dataset('davidscripka/openwakeword_features', 'validation_set', split='train')
arr_val = np.array(ds_val['features'])
np.save('/data/validation_set_features.npy', arr_val)
print('Датасет скачан.')
"
echo " Датасет готов."
else
echo "[2/4] Негативный датасет уже скачан. Пропускаю."
fi
echo ""
# Запускаем обучение
echo "[3/4] Запускаю обучение в Docker..."
echo " Это займёт ~30-60 минут."
echo ""
SAMPLES_MOUNT=""
if [ -d "$POSITIVE_DIR" ] && [ -n "$(ls "$POSITIVE_DIR"/*.wav 2>/dev/null)" ]; then
SAMPLES_MOUNT="-v $POSITIVE_DIR:/samples/positive"
fi
docker run --rm \
-v "$SCRIPT_DIR/cosmo_config.yaml:/app/cosmo_config.yaml" \
-v "$DATA_DIR:/data" \
-v "$MODELS_DIR:/output" \
$SAMPLES_MOUNT \
cosmo-wakeword-trainer
echo ""
echo "[4/4] Копирую модель в проект..."
# Ищем готовую модель
ONNX_FILE=$(ls "$MODELS_DIR"/*.onnx 2>/dev/null | head -1)
if [ -n "$ONNX_FILE" ]; then
echo ""
echo "============================================"
echo " Готово! Модель сохранена:"
echo " $ONNX_FILE"
echo ""
echo " Обновляю wake_word детектор..."
# Обновляем путь в конфиге
MODEL_FILENAME=$(basename "$ONNX_FILE")
sed -i "s|wakeword_models=\[\"hey_jarvis\"\]|wakeword_models=[\"models/$MODEL_FILENAME\"]|g" \
"$PROJECT_DIR/cosmo/wake_word.py" 2>/dev/null || true
echo " Теперь запускай: bash run.sh"
echo " и говори 'Hey Cosmo' для активации!"
echo "============================================"
else
echo "ОШИБКА: .onnx файл не найден в $MODELS_DIR"
echo "Проверь логи Docker выше."
exit 1
fi