Технические заметки
Почему base64 PCM вместо WAV/Opus
- Низкая задержка: не нужно ждать формирования заголовка WAV или энкодинга Opus.
- Простота клиента: Python-агент может напрямую скормить PCM в
pyaudio / sounddevice / alsaaudio.
- Компромисс: трафик больше, чем с Opus, но в локальной сети это некритично.
- Opus можно добавить позже как опциональный формат.
Почему один TTS worker
- CUDA context не любит параллельные вызовы из разных потоков.
- Один worker с
asyncio.Queue гарантирует последовательный доступ к GPU и предсказуемое потребление VRAM.
- Если в будущем понадобится масштабирование — можно запустить несколько независимых инстансов.
Управление остановкой
stop_event — центральный механизм:
SessionState.stop() устанавливает флаг.
- Все длительные операции (TTS, отправка аудио) могут его проверять.
- После
stop новое сообщение text автоматически сбрасывает флаг и начинает новую фразу.
Сегментация
Цель — найти баланс между:
- задержкой (короткие сегменты синтезируются быстрее),
- качеством (TTS лучше звучит на целых предложениях),
- реалтаймом (не ждём слишком долго).
Параметры по умолчанию:
min_segment_length = 30
max_segment_length = 200
max_buffer_wait_ms = 500
Для русского языка предложения обычно короче, чем на английском, поэтому max_length выбран консервативно.
F5-TTS: особенности интеграции
- Используется готовая модель
F5TTS_v1_Base через официальный пакет f5-tts.
- Модель автоматически загружается из Hugging Face при первом вызове
load(); можно скачать заранее скриптом scripts/download_f5_tts.py.
- Референсное аудио транскрибируется автоматически, если
ref_text не задан.
- Результат транскрипции и путь к обработанному аудио кэшируются по паре
(путь, эмоция), чтобы не повторять работу при смене сегментов.
- Скорость регулируется параметром
speed инференса F5-TTS.
- Текущая целевая частота дискретизации — 24 kHz; модель сама передискретирует референс при необходимости.
- Инференс выполняется в отдельном потоке (
asyncio.to_thread) с asyncio.Lock, чтобы не блокировать event loop и не допускать параллельных CUDA-вызовов. Все send_text сериализуются через отдельный _send_lock, чтобы избежать deadlock внутри websockets.
Замеры задержки (RTX 3090, Python 3.11, CUDA 12.6)
- Первый запуск без
DEFAULT_REF_TEXT: ~5–6 с, большая часть уходит на Whisper-транскрипцию референса.
- С
DEFAULT_REF_TEXT и WARMUP=true: warm-up занимает ~2 с (загрузка модели + один инференс).
- После warm-up с кэшированным референсом: первый audio-chunk ~1.1 с на коротком сегменте.
- 4 сегмента подряд: первый finished ~1.1 с, последний ~4.4 с.
stop + возобновление работает без переподключения WebSocket.
Потенциальные проблемы
- F5-TTS может не идеально произносить украинский / европейские языки из коробки — возможно потребуется fine-tuning или fallback. Сейчас протокол и сегментатор не ограничивают язык; качество зависит от самой модели.
- RTX 3060 (12 GB) подойдёт для базовой модели, но batch-size и длина референса придётся ограничивать.
- Быстрый
stop во время CUDA kernel не прервёт уже запущенный kernel, но предотвратит отправку результата.
main.py создаёт engine до старта uvicorn; при TTS_BACKEND=f5_tts первый запуск может занять десяток секунд из-за загрузки модели и vocos.