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:
@@ -190,6 +190,11 @@ def my_tool(param: str) -> str:
|
|||||||
|
|
||||||
Добавь в список `ALL_TOOLS` в конце файла — агент автоматически получит доступ.
|
Добавь в список `ALL_TOOLS` в конце файла — агент автоматически получит доступ.
|
||||||
|
|
||||||
|
## TODO / Идеи
|
||||||
|
|
||||||
|
- [ ] **ChatterBox TTS вместо Silero** — более живой голос с эмоциями и клонированием голоса (10-30 сек сэмпла). Источник: [vndee/local-talking-llm](https://github.com/vndee/local-talking-llm). Параметр `exaggeration` (0.0–1.0) управляет экспрессивностью. Заменить `cosmo/tts.py`, интерфейс `say()`/`say_async()` не меняется. Проверить латентность на M1 CPU.
|
||||||
|
- [ ] **Ollama Modelfile** — запечь системный промпт и параметры (короткие ответы, русский язык) прямо в модель через Modelfile, вместо передачи в каждом запросе.
|
||||||
|
|
||||||
## Разработка
|
## Разработка
|
||||||
|
|
||||||
Логи пишутся в `logs/cosmo.log`. Уровень логирования меняется в конфиге (`logging.level: DEBUG`).
|
Логи пишутся в `logs/cosmo.log`. Уровень логирования меняется в конфиге (`logging.level: DEBUG`).
|
||||||
|
|||||||
@@ -7,18 +7,22 @@ audio:
|
|||||||
silence_duration: 1.0
|
silence_duration: 1.0
|
||||||
|
|
||||||
whisper:
|
whisper:
|
||||||
model_size: "small" # На Mac без GPU — small быстрее чем distil-large
|
model_size: "medium" # medium — лучшее качество русского на CPU (small слишком много ошибок)
|
||||||
device: "cpu" # Mac Intel/Apple Silicon — CPU (MPS пока не стабилен в faster-whisper)
|
device: "cpu" # Mac Intel/Apple Silicon — CPU (MPS пока не стабилен в faster-whisper)
|
||||||
compute_type: "int8" # int8 быстрее на CPU
|
compute_type: "auto" # auto вместо int8 — int8 слишком сильно режет качество русского
|
||||||
language: "ru"
|
language: "ru"
|
||||||
|
initial_prompt: "Cosmo, открой браузер, найди программу, запусти приложение." # подсказка для русского контекста
|
||||||
|
|
||||||
ollama:
|
ollama:
|
||||||
base_url: "http://localhost:11434"
|
base_url: "http://localhost:11434"
|
||||||
model: "qwen2.5:7b"
|
model: "qwen2.5:3b" # 3b быстрее на M1 (~2x), достаточно для голосовых команд
|
||||||
temperature: 0.2
|
temperature: 0.2
|
||||||
max_tokens: 1024
|
max_tokens: 1024
|
||||||
max_agent_steps: 10
|
max_agent_steps: 10
|
||||||
|
|
||||||
|
performance:
|
||||||
|
num_threads: 4 # CPU потоки для torch (TTS/Whisper)
|
||||||
|
|
||||||
tts:
|
tts:
|
||||||
enabled: true
|
enabled: true
|
||||||
silero_speaker: "eugene" # xenia (женский) baya aidar eugene kseniya
|
silero_speaker: "eugene" # xenia (женский) baya aidar eugene kseniya
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ else:
|
|||||||
from cosmo.tools import ALL_TOOLS, set_memory
|
from cosmo.tools import ALL_TOOLS, set_memory
|
||||||
_PLATFORM_NOTE = "Windows. Используй Git Bash, 'start' для запуска приложений."
|
_PLATFORM_NOTE = "Windows. Используй Git Bash, 'start' для запуска приложений."
|
||||||
|
|
||||||
SYSTEM_PROMPT = f"""Ты — Cosmo, умный голосовой ассистент. Платформа: {_PLATFORM_NOTE}
|
SYSTEM_PROMPT = """Ты — Cosmo, умный голосовой ассистент. Платформа: """ + _PLATFORM_NOTE + """
|
||||||
|
|
||||||
Правила:
|
Правила:
|
||||||
1. Используй инструменты для выполнения задач — не выдумывай результаты
|
1. Используй инструменты для выполнения задач — не выдумывай результаты
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ class Cosmo:
|
|||||||
self._command_event.set()
|
self._command_event.set()
|
||||||
|
|
||||||
def _process_command(self):
|
def _process_command(self):
|
||||||
self.tts.say_async("Слушаю")
|
# Синхронно — сначала говорим, потом слушаем (иначе TTS мешает записи)
|
||||||
|
self.tts.say("Слушаю")
|
||||||
|
|
||||||
# Partial results — печатаем в лог что слышим в реальном времени
|
# Partial results — печатаем в лог что слышим в реальном времени
|
||||||
def on_partial(text):
|
def on_partial(text):
|
||||||
|
|||||||
@@ -61,9 +61,10 @@ def find_program(name: str) -> str:
|
|||||||
"""
|
"""
|
||||||
Найти программу или приложение на macOS по имени.
|
Найти программу или приложение на macOS по имени.
|
||||||
Ищет в PATH, /Applications, ~/Applications и через Spotlight (mdfind).
|
Ищет в PATH, /Applications, ~/Applications и через Spotlight (mdfind).
|
||||||
|
Возвращает путь. Чтобы запустить — используй open_app.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: имя программы, например 'webstorm', 'chrome', 'cursor'
|
name: имя программы, например 'webstorm', 'chrome', 'cursor', 'safari'
|
||||||
"""
|
"""
|
||||||
stem = name.strip()
|
stem = name.strip()
|
||||||
logger.info(f"[find_program] {stem}")
|
logger.info(f"[find_program] {stem}")
|
||||||
@@ -92,6 +93,27 @@ def find_program(name: str) -> str:
|
|||||||
return f"Программа '{name}' не найдена."
|
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
|
@tool
|
||||||
def open_browser(url: str, search: bool = False) -> str:
|
def open_browser(url: str, search: bool = False) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -197,6 +219,7 @@ def memory_list(prefix: str = "") -> str:
|
|||||||
ALL_TOOLS = [
|
ALL_TOOLS = [
|
||||||
run_shell,
|
run_shell,
|
||||||
find_program,
|
find_program,
|
||||||
|
open_app,
|
||||||
open_browser,
|
open_browser,
|
||||||
read_file,
|
read_file,
|
||||||
write_file,
|
write_file,
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ class Transcriber:
|
|||||||
"language": whisper_cfg["language"],
|
"language": whisper_cfg["language"],
|
||||||
"device": whisper_cfg["device"],
|
"device": whisper_cfg["device"],
|
||||||
"compute_type": whisper_cfg["compute_type"],
|
"compute_type": whisper_cfg["compute_type"],
|
||||||
|
# Подсказка для Whisper — улучшает распознавание русского
|
||||||
|
"initial_prompt": whisper_cfg.get("initial_prompt", ""),
|
||||||
# Silero VAD параметры
|
# Silero VAD параметры
|
||||||
"silero_sensitivity": 0.4,
|
"silero_sensitivity": 0.4,
|
||||||
"webrtc_sensitivity": 3,
|
"webrtc_sensitivity": 3,
|
||||||
"post_speech_silence_duration": audio_cfg["silence_duration"],
|
"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,
|
"min_gap_between_recordings": 0.01,
|
||||||
# Отключаем wake word в RealtimeSTT — используем свой
|
# Отключаем wake word в RealtimeSTT — используем свой
|
||||||
"wakeword_backend": "none",
|
"wakeword_backend": "none",
|
||||||
|
|||||||
23
cosmo/tts.py
23
cosmo/tts.py
@@ -32,6 +32,10 @@ class TTS:
|
|||||||
self.enabled = False
|
self.enabled = False
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Оптимизация CPU-инференса на Apple Silicon
|
||||||
|
num_threads = config.get("performance", {}).get("num_threads", 4)
|
||||||
|
torch.set_num_threads(num_threads)
|
||||||
|
|
||||||
self._load_model()
|
self._load_model()
|
||||||
|
|
||||||
def _load_model(self):
|
def _load_model(self):
|
||||||
@@ -52,16 +56,33 @@ class TTS:
|
|||||||
logger.warning("TTS отключён")
|
logger.warning("TTS отключён")
|
||||||
self.enabled = False
|
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):
|
def say(self, text: str):
|
||||||
"""Произнести текст синхронно."""
|
"""Произнести текст синхронно."""
|
||||||
if not self.enabled or self._model is None:
|
if not self.enabled or self._model is None:
|
||||||
logger.info(f"[TTS]: {text}")
|
logger.info(f"[TTS]: {text}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
text = self._sanitize_text(text)
|
||||||
logger.debug(f"TTS: '{text}'")
|
logger.debug(f"TTS: '{text}'")
|
||||||
with self._lock:
|
with self._lock:
|
||||||
try:
|
try:
|
||||||
with torch.no_grad():
|
with torch.inference_mode():
|
||||||
audio = self._model.apply_tts(
|
audio = self._model.apply_tts(
|
||||||
text=text,
|
text=text,
|
||||||
speaker=self.speaker,
|
speaker=self.speaker,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Wake word detector для Cosmo.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
|
import time
|
||||||
import threading
|
import threading
|
||||||
import queue
|
import queue
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -31,7 +32,8 @@ class WakeWordDetector:
|
|||||||
self._thread = None
|
self._thread = None
|
||||||
|
|
||||||
# Порог уверенности для срабатывания (0.0 – 1.0)
|
# Порог уверенности для срабатывания (0.0 – 1.0)
|
||||||
self.threshold = 0.5
|
# 0.7 — баланс между надёжностью и защитой от ложных срабатываний/эха TTS
|
||||||
|
self.threshold = 0.7
|
||||||
|
|
||||||
logger.info("Загружаю wake word модель openwakeword...")
|
logger.info("Загружаю wake word модель openwakeword...")
|
||||||
|
|
||||||
@@ -88,14 +90,18 @@ class WakeWordDetector:
|
|||||||
"""Приостановить детект (пока идёт запись команды)."""
|
"""Приостановить детект (пока идёт запись команды)."""
|
||||||
self._paused = True
|
self._paused = True
|
||||||
|
|
||||||
def resume(self):
|
def resume(self, cooldown: float = 1.5):
|
||||||
"""Возобновить детект после записи команды."""
|
"""Возобновить детект после записи команды с защитой от эха."""
|
||||||
# Очищаем очередь, чтобы не срабатывать на эхо
|
# Ждём пока эхо от TTS затухнет
|
||||||
|
time.sleep(cooldown)
|
||||||
|
# Очищаем очередь — там буферизованный звук TTS
|
||||||
while not self._audio_queue.empty():
|
while not self._audio_queue.empty():
|
||||||
try:
|
try:
|
||||||
self._audio_queue.get_nowait()
|
self._audio_queue.get_nowait()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
break
|
break
|
||||||
|
# Сбрасываем внутреннее состояние модели (накопленные скоры)
|
||||||
|
self.model.reset()
|
||||||
self._paused = False
|
self._paused = False
|
||||||
logger.debug("Wake word детектор возобновлён")
|
logger.debug("Wake word детектор возобновлён")
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,23 @@ echo " Установка Cosmo на macOS"
|
|||||||
echo "============================================"
|
echo "============================================"
|
||||||
|
|
||||||
# --- Python ---
|
# --- Python ---
|
||||||
if ! command -v python3 &>/dev/null; then
|
# Предпочитаем Python 3.12 (лучшая совместимость с ML-пакетами)
|
||||||
|
PYTHON_BIN=""
|
||||||
|
for py in python3.12 python3.11 python3.13 python3; do
|
||||||
|
if command -v "$py" &>/dev/null; then
|
||||||
|
PYTHON_BIN="$py"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$PYTHON_BIN" ]; then
|
||||||
echo "ОШИБКА: Python3 не найден."
|
echo "ОШИБКА: Python3 не найден."
|
||||||
echo "Установи через Homebrew: brew install python@3.11"
|
echo "Установи через Homebrew: brew install python@3.12"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PYTHON_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
PYTHON_VERSION=$($PYTHON_BIN -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
||||||
echo "Python: $PYTHON_VERSION"
|
echo "Python: $PYTHON_VERSION ($PYTHON_BIN)"
|
||||||
|
|
||||||
# --- Homebrew зависимости ---
|
# --- Homebrew зависимости ---
|
||||||
if command -v brew &>/dev/null; then
|
if command -v brew &>/dev/null; then
|
||||||
@@ -24,19 +33,22 @@ else
|
|||||||
echo "Если будут ошибки с аудио — установи: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
|
echo "Если будут ошибки с аудио — установи: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[2/5] Обновляю pip..."
|
echo "[2/5] Создаю виртуальное окружение..."
|
||||||
python3 -m pip install --upgrade pip
|
rm -rf venv
|
||||||
|
$PYTHON_BIN -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
echo "[3/5] Устанавливаю зависимости..."
|
echo "[3/5] Обновляю pip и устанавливаю зависимости..."
|
||||||
python3 -m pip install -r requirements.txt
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
echo "[4/5] Устанавливаю faster-whisper..."
|
echo "[4/5] Устанавливаю faster-whisper..."
|
||||||
# На Mac (Apple Silicon) используем CPU compute type
|
# На Mac (Apple Silicon) используем CPU compute type
|
||||||
python3 -m pip install faster-whisper
|
pip install faster-whisper
|
||||||
|
|
||||||
echo "[5/5] Устанавливаю openwakeword..."
|
echo "[5/5] Устанавливаю openwakeword..."
|
||||||
python3 -m pip install openwakeword
|
pip install openwakeword
|
||||||
python3 -c "import openwakeword; openwakeword.utils.download_models()" 2>/dev/null || true
|
python -c "import openwakeword; openwakeword.utils.download_models()" 2>/dev/null || true
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
|
|||||||
BIN
models/hey_cosmo.onnx
Normal file
BIN
models/hey_cosmo.onnx
Normal file
Binary file not shown.
@@ -16,6 +16,11 @@ ollama==0.4.4 # официальный Python клиент Ollama
|
|||||||
pyyaml==6.0.2
|
pyyaml==6.0.2
|
||||||
loguru==0.7.2
|
loguru==0.7.2
|
||||||
|
|
||||||
|
# Аудио
|
||||||
|
sounddevice>=0.5.0
|
||||||
|
pydub>=0.25.1
|
||||||
|
imageio-ffmpeg>=0.6.0
|
||||||
|
|
||||||
# Инструменты агента
|
# Инструменты агента
|
||||||
psutil==6.0.0
|
psutil==6.0.0
|
||||||
pyautogui==0.9.54
|
pyautogui==0.9.54
|
||||||
|
|||||||
15
run_mac.sh
15
run_mac.sh
@@ -10,5 +10,18 @@ if ! curl -s http://localhost:11434 &>/dev/null; then
|
|||||||
sleep 2
|
sleep 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Оптимизация CPU-инференса на Apple Silicon
|
||||||
|
export OMP_NUM_THREADS=4
|
||||||
|
export MKL_NUM_THREADS=4
|
||||||
|
export PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0
|
||||||
|
|
||||||
|
# Активируем venv
|
||||||
|
if [ -d "venv" ]; then
|
||||||
|
source venv/bin/activate
|
||||||
|
else
|
||||||
|
echo "venv не найден. Сначала запусти: bash install_mac.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Запускаем с Mac-конфигом
|
# Запускаем с Mac-конфигом
|
||||||
COSMO_PLATFORM=mac python3 cosmo/main.py --config config/config_mac.yaml "$@"
|
COSMO_PLATFORM=mac python cosmo/main.py --config config/config_mac.yaml "$@"
|
||||||
|
|||||||
@@ -1,88 +1,86 @@
|
|||||||
# Dockerfile для обучения wake word модели openWakeWord
|
# Dockerfile для обуч<EFBFBD><EFBFBD>ния wake word модели openWakeWord
|
||||||
# Python 3.11 + torch 2.5 (последний совместимый с py3.11) + рабочие зависимости 2026
|
# Python 3.11 + torch (CPU) — без tensorflow (нам нужен только ONNX, не TFLite)
|
||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Системные зависимости (включая build-essential для webrtcvad)
|
# Системные зависимости
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
git wget curl ffmpeg libsndfile1 \
|
git wget curl ffmpeg libsndfile1 \
|
||||||
build-essential python3-dev \
|
build-essential python3-dev cmake \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Клонируем openWakeWord и piper-sample-generator
|
# --- Слой 1: PyTorch (самый тяжёлый, кэшируется) ---
|
||||||
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 \
|
RUN pip install --no-cache-dir \
|
||||||
torch==2.5.0 \
|
torch==2.5.0 \
|
||||||
torchaudio==2.5.0 \
|
torchaudio==2.5.0 \
|
||||||
--index-url https://download.pytorch.org/whl/cpu
|
--index-url https://download.pytorch.org/whl/cpu
|
||||||
|
|
||||||
# Зависимости обучения с совместимыми версиями
|
# --- Слой 2: ML-зависимости (без tensorflow!) ---
|
||||||
RUN pip install --no-cache-dir \
|
RUN pip install --no-cache-dir \
|
||||||
mutagen==1.47.0 \
|
mutagen==1.47.0 \
|
||||||
torchinfo==1.8.0 \
|
torchinfo==1.8.0 \
|
||||||
torchmetrics==1.2.0 \
|
torchmetrics==1.2.0 \
|
||||||
speechbrain==1.0.3 \
|
speechbrain==1.0.3 \
|
||||||
audiomentations==0.43.1 \
|
|
||||||
torch-audiomentations==0.12.0 \
|
|
||||||
pronouncing==0.2.0 \
|
pronouncing==0.2.0 \
|
||||||
"datasets==2.20.0" \
|
|
||||||
"pyarrow==14.0.2" \
|
|
||||||
"fsspec==2023.12.2" \
|
|
||||||
acoustics==0.2.6 \
|
acoustics==0.2.6 \
|
||||||
|
pyyaml "scipy<1.15" scikit-learn tqdm
|
||||||
|
|
||||||
|
# --- Слой 3: Аудио-аугментация ---
|
||||||
|
RUN pip install --no-cache-dir \
|
||||||
|
audiomentations==0.43.1 \
|
||||||
|
torch-audiomentations==0.12.0
|
||||||
|
|
||||||
|
# --- Слой 4: Датасеты и ONNX ---
|
||||||
|
RUN pip install --no-cache-dir \
|
||||||
|
"datasets>=2.20.0" \
|
||||||
|
"pyarrow>=15.0.0" \
|
||||||
webrtcvad \
|
webrtcvad \
|
||||||
onnx \
|
onnx \
|
||||||
onnxruntime \
|
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
|
|
||||||
|
|
||||||
|
# --- Сл<D0A1><D0BB>й 5: openWakeWord ---
|
||||||
|
RUN git clone https://github.com/dscripka/openWakeWord /openWakeWord
|
||||||
RUN pip install --no-cache-dir -e /openWakeWord
|
RUN pip install --no-cache-dir -e /openWakeWord
|
||||||
|
|
||||||
# Патч: заменяем onnx_tf на onnx2tf в train.py
|
# Ресурсные модели для feature extraction (melspectrogram + embedding)
|
||||||
RUN python - <<'EOF'
|
RUN mkdir -p /openWakeWord/openwakeword/resources/models && \
|
||||||
import re, pathlib
|
wget -q -O /openWakeWord/openwakeword/resources/models/melspectrogram.onnx \
|
||||||
|
"https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/melspectrogram.onnx" && \
|
||||||
|
wget -q -O /openWakeWord/openwakeword/resources/models/embedding_model.onnx \
|
||||||
|
"https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/embedding_model.onnx"
|
||||||
|
|
||||||
|
# Патч train.py: убираем зависимость от onnx_tf/tensorflow (нам нужен только ONNX)
|
||||||
|
RUN python - <<'PATCH'
|
||||||
|
import pathlib
|
||||||
|
|
||||||
train_py = pathlib.Path("/openWakeWord/openwakeword/train.py")
|
train_py = pathlib.Path("/openWakeWord/openwakeword/train.py")
|
||||||
text = train_py.read_text()
|
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
|
# Заменяем всю функцию convert_onnx_to_tflite на заглушку
|
||||||
|
old_func = text[text.find("def convert_onnx_to_tflite("):]
|
||||||
|
old_func = old_func[:old_func.find("\nif __name__")]
|
||||||
|
new_func = '''def convert_onnx_to_tflite(onnx_model_path, output_path):
|
||||||
|
"""Skipped — ONNX-only mode, TFLite not needed."""
|
||||||
|
return None
|
||||||
|
'''
|
||||||
|
text = text.replace(old_func, new_func)
|
||||||
|
|
||||||
|
train_py.write_text(text)
|
||||||
|
print("train.py patched: convert_onnx_to_tflite replaced with stub")
|
||||||
|
PATCH
|
||||||
|
|
||||||
|
# --- Слой 6: piper-sample-generator v2.0.0 (совместим с openWakeWord train.py) ---
|
||||||
|
RUN git clone --branch v2.0.0 https://github.com/rhasspy/piper-sample-generator /piper-sample-generator
|
||||||
|
RUN pip install --no-cache-dir piper-phonemize || true
|
||||||
RUN pip install --no-cache-dir -e /piper-sample-generator 2>/dev/null || \
|
RUN pip install --no-cache-dir -e /piper-sample-generator 2>/dev/null || \
|
||||||
pip install --no-cache-dir piper-tts
|
pip install --no-cache-dir piper-tts
|
||||||
|
|
||||||
# Скачиваем TTS модель LibriTTS-R medium (~66 MB) для генерации примеров
|
# TTS модель (.pt checkpoint) для генерации примеров
|
||||||
RUN mkdir -p /piper-sample-generator/models && \
|
RUN mkdir -p /piper-sample-generator/models && \
|
||||||
wget -q --show-progress \
|
wget -q --show-progress \
|
||||||
-O /piper-sample-generator/models/en_US-libritts_r-medium.onnx \
|
-O /piper-sample-generator/models/en_US-libritts_r-medium.pt \
|
||||||
"https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/libritts_r/medium/en_US-libritts_r-medium.onnx" && \
|
"https://github.com/rhasspy/piper-sample-generator/releases/download/v2.0.0/en_US-libritts_r-medium.pt"
|
||||||
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
|
RUN mkdir -p /data /output /samples
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ batch_n_per_class:
|
|||||||
"adversarial_negative": 50
|
"adversarial_negative": 50
|
||||||
"positive": 50
|
"positive": 50
|
||||||
|
|
||||||
|
# Пути для аугментации (пустые — аугментация без RIR и фонового шума)
|
||||||
|
rir_paths: []
|
||||||
|
background_paths: []
|
||||||
|
background_paths_duplication_rate: []
|
||||||
|
|
||||||
# Архитектура модели
|
# Архитектура модели
|
||||||
model_type: "dnn"
|
model_type: "dnn"
|
||||||
layer_size: 32
|
layer_size: 32
|
||||||
|
|||||||
@@ -51,24 +51,17 @@ NEGATIVE_FEATURES="$DATA_DIR/openwakeword_features_ACAV100M_2000_hrs_16bit.npy"
|
|||||||
VALIDATION_FEATURES="$DATA_DIR/validation_set_features.npy"
|
VALIDATION_FEATURES="$DATA_DIR/validation_set_features.npy"
|
||||||
|
|
||||||
if [ ! -f "$NEGATIVE_FEATURES" ]; then
|
if [ ! -f "$NEGATIVE_FEATURES" ]; then
|
||||||
echo "[2/4] Скачиваю негативный датасет (~20 GB, один раз)..."
|
echo "[2/4] Скачиваю негативный датасет (~17 GB + ~500 MB, один раз)..."
|
||||||
echo " Это займёт время в зависимости от скорости интернета."
|
echo " Это займёт время в зависимости от скорости интернета."
|
||||||
docker run --rm \
|
echo ""
|
||||||
-v "$DATA_DIR:/data" \
|
echo " Скачиваю ACAV100M features (~17 GB)..."
|
||||||
cosmo-wakeword-trainer \
|
wget -q --show-progress \
|
||||||
python -c "
|
-O "$NEGATIVE_FEATURES" \
|
||||||
from datasets import load_dataset
|
"https://huggingface.co/datasets/davidscripka/openwakeword_features/resolve/main/openwakeword_features_ACAV100M_2000_hrs_16bit.npy"
|
||||||
import numpy as np, os
|
echo " Скачиваю validation features (~500 MB)..."
|
||||||
print('Скачиваю ACAV100M features...')
|
wget -q --show-progress \
|
||||||
ds = load_dataset('davidscripka/openwakeword_features', 'ACAV100M_2000_hrs_16bit', split='train')
|
-O "$VALIDATION_FEATURES" \
|
||||||
arr = np.array(ds['features'])
|
"https://huggingface.co/datasets/davidscripka/openwakeword_features/resolve/main/validation_set_features.npy"
|
||||||
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 " Датасет готов."
|
echo " Датасет готов."
|
||||||
else
|
else
|
||||||
echo "[2/4] Негативный датасет уже скачан. Пропускаю."
|
echo "[2/4] Негативный датасет уже скачан. Пропускаю."
|
||||||
@@ -86,6 +79,7 @@ if [ -d "$POSITIVE_DIR" ] && [ -n "$(ls "$POSITIVE_DIR"/*.wav 2>/dev/null)" ]; t
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
|
--shm-size=2g \
|
||||||
-v "$SCRIPT_DIR/cosmo_config.yaml:/app/cosmo_config.yaml" \
|
-v "$SCRIPT_DIR/cosmo_config.yaml:/app/cosmo_config.yaml" \
|
||||||
-v "$DATA_DIR:/data" \
|
-v "$DATA_DIR:/data" \
|
||||||
-v "$MODELS_DIR:/output" \
|
-v "$MODELS_DIR:/output" \
|
||||||
|
|||||||
Reference in New Issue
Block a user