Skill #3 · TTS audio

video-voiceover

Генерирует voiceover-аудио для роликов через ElevenLabs TTS. Берёт текст, выбирает следующий голос из rotation-пула проекта (least-recently-used), возвращает готовый mp3 + точную длительность. Плюс audio_process.py — постпродакшн (убирает паузы, EQ, нормализация до -16 LUFS).

Путь
~/.claude/skills/video-voiceover/
Триггеры
озвучка, voiceover, озвучь текст, сгенерируй озвучку, TTS, text-to-speech, голос для ролика, начитка, ElevenLabs
Зависит от
ELEVENLABS_API_KEY в env, httpx, ffmpeg/ffprobe, минимум 1 голос в project_voices
Пэйрится с
copywriter (даёт текст), director (получает mp3 + duration), orchestrator

Что делает простыми словами

Это TTS-машинка с правильной ротацией. У каждого проекта свой пул голосов (например 2-3 ElevenLabs voice IDs). Когда нужно озвучить новый текст:

  1. Смотрит, какие голоса есть у проекта (через project_voices junction)
  2. Выбирает least-recently-used → next-voice
  3. Читает settings из project.yaml.voiceover (model, speed)
  4. Вызывает ElevenLabs API через tts.py
  5. Сохраняет mp3 в project/voiceovers/
  6. Замеряет точную длительность через ffprobe
  7. Записывает usage в БД (для следующей ротации)
  8. Постпроцесс через audio_process.py — короче паузы, нормализация громкости, EQ
  9. Возвращает: путь, длительность, voice, текст

Дальше director берёт эти 4 параметра и собирает под них видео.

Зачем rotation: 5 роликов подряд одним голосом → юзер ощущает реклама-конвейер и пролистывает. Чередование Rachel / Bella / Sarah ощущается как разные креативы, даже если визуал похож.

Voice rotation: как работает next-voice

В project_voices junction-таблице хранится не только linkage, но и usage stats: times_used, last_used_at. Когда вызываешь next-voice:

SELECT v.id, v.voice_id, v.name
FROM voices v
JOIN project_voices pv ON pv.voice_id = v.id
WHERE pv.project_id = ?
ORDER BY pv.last_used_at ASC NULLS FIRST, pv.times_used ASC
LIMIT 1

Возвращается голос, который дольше всего не использовался в этом проекте. Если все равны (первый запуск) — берётся первый по порядку.

После генерации:

python ~/.claude/skills/video-voiceover/scripts/db.py record-usage \
  <project-dir> <voice-db-id> <audio-path> "<text>" --duration 28.4

Это обновляет last_used_at и инкрементит times_used, плюс линкует запись в voiceover_history с проектом через project_voiceovers.

Один и тот же голос может быть в N проектах. ElevenLabs voice_id — глобальный, привязка к проекту через junction. project-setup при онбординге спрашивает «хочешь привязать голоса из других проектов?» — именно про эту фишку.

Workflow по шагам

1. Проверить голоса проекта

python ~/.claude/skills/video-voiceover/scripts/db.py list-voices <project-dir>

Если пусто — попросить юзера добавить:

python ~/.claude/skills/video-voiceover/scripts/db.py add-voice <project-dir> \
  --voice-id "21m00Tcm4TlvDq8ikWAM" \
  --name "Rachel" \
  --language "en" \
  --description "Calm, professional female voice"

voice_id юзер берёт с elevenlabs.io/voice-library или через list-available:

python ~/.claude/skills/video-voiceover/scripts/db.py list-available \
  --api-key "$ELEVENLABS_API_KEY" \
  --language ru

2. Выбрать следующий голос

python ~/.claude/skills/video-voiceover/scripts/db.py next-voice <project-dir>
# → {"id": 3, "voice_id": "21m00Tcm4TlvDq8ikWAM", "name": "Rachel", "language": "ru"}

3. Сгенерировать аудио

python ~/.claude/skills/video-voiceover/scripts/tts.py \
  --project-dir <project-dir> \
  --text "Полный текст озвучки" \
  --voice-id 21m00Tcm4TlvDq8ikWAM

Возвращает путь к mp3 и точную длительность.

4. Postprocess (ОБЯЗАТЕЛЬНО)

python ~/.claude/skills/video-voiceover/scripts/audio_process.py \
  --input voiceover.mp3 \
  --output voiceover_processed.mp3

Использовать processed-файл для всех следующих шагов (его длительность короче из-за вырезанных пауз).

5. Отрапортовать

Возвращаешь caller'у (или юзеру):

Эти 4 параметра нужны director'у для сборки.

audio_process.py — что делает

Сырое ElevenLabs аудио имеет неприятные особенности: долгие паузы между предложениями, неровную громкость, бубнящие низы и шипящие верха. Postprocess это всё лечит за один проход:

ЭтапЧто делаетЭффект
1. Silence removalУкорачивает паузы между фразами~5 сек экономии на 30-сек ролике
2. Voice EQHighpass 180Hz + lowpass 9kHz + 3kHz presence boostУбирает rumble снизу + hiss сверху, добавляет «телевизионную» чёткость
3. Adaptive levelerДинамическая нормализация громкости (как Auphonic)Тихие места подтягиваются вверх, громкие — вниз
4. Loudness normalizationEBU R128 до -16 LUFSBroadcast standard. Соответствует тому, что воспроизводят соцсети
Без postprocess не работать. Если отдать сырое аудио в director — у тебя будет (а) видео длиннее, чем нужно, потому что Whisper-замер первого предложения попадёт в паузу; (б) рассинхрон при сборке монтажных сегментов; (в) музыка перебивает голос неравномерно. Всегда audio_process.py → processed file → director.

Project settings

Настройки голоса хранятся per-project — либо в project.yaml.voiceover, либо в таблице voiceover_settings:

python ~/.claude/skills/video-voiceover/scripts/db.py get-settings <project-dir>
# → {"speed": 1.2, "stability": 0.5, "similarity_boost": 0.75, "model": "eleven_multilingual_v2"}

python ~/.claude/skills/video-voiceover/scripts/db.py set-settings <project-dir> \
  --speed 1.2 \
  --stability 0.5 \
  --similarity-boost 0.75 \
  --model "eleven_multilingual_v2"

Defaults

ПараметрDefaultЧто это
speed1.2Скорость произнесения. 1.0 — обычная, 1.2 — рекламная (динамичнее)
stability0.5Стабильность голоса. <0.5 — экспрессия (драматичнее), >0.5 — монотоннее
similarity_boost0.75Насколько точно следовать оригинальному голосу
modeleleven_multilingual_v2Лучшее качество, 29 языков, emotional range

ElevenLabs модели

МодельBest for
eleven_multilingual_v2Default. Лучшее качество, 29 языков, emotional range
eleven_turbo_v2_5Хорошее качество, ниже latency
eleven_flash_v2_5Самая быстрая, real-time

Для рекламных роликов почти всегда multilingual_v2 — на 15-60-секундный текст latency не критичен, качество важнее.

Voice management команды

# Добавить голос в пул проекта
python ~/.claude/skills/video-voiceover/scripts/db.py add-voice <project-dir> \
  --voice-id "ID" --name "Name" --language "ru" --description "Description"

# Список голосов с usage-статистикой
python ~/.claude/skills/video-voiceover/scripts/db.py list-voices <project-dir>

# Удалить голос из пула
python ~/.claude/skills/video-voiceover/scripts/db.py remove-voice <project-dir> <voice-db-id>

# Просмотреть ElevenLabs voice library
python ~/.claude/skills/video-voiceover/scripts/db.py list-available \
  --api-key "$ELEVENLABS_API_KEY" --language ru

Внутренности

Скрипты

ФайлЧто делает
scripts/tts.pyГлавный воркхорс: вызывает ElevenLabs API, сохраняет mp3, замеряет длительность через ffprobe
scripts/audio_process.pyПостпродакшн: silence-removal + EQ + leveler + loudnorm
scripts/db.pyCRUD по голосам (project-scoped), rotation (next-voice), settings, usage history
scripts/api_keys.pyРезолвит ELEVENLABS_API_KEY из env или project/api_keys.db
scripts/schema.pySchema source-of-truth (v8) — копия одинакова во всех скиллах

Таблицы БД, которые трогает

ТаблицаДействиеЗачем
voicesRead / INSERTКаталог всех голосов (ID, имя, язык, описание)
project_voicesRead / INSERT / UPDATEJunction + usage stats (times_used, last_used_at)
voiceover_historyINSERTЗапись каждой генерации (text, voice, path, duration)
project_voiceoversINSERTJunction: scope audio к проекту
voiceover_settingsRead / UPDATEPer-project settings (speed/stability/model)

Конфиг из project.yaml

language: "ru"                        # язык TTS

voiceover:
  provider: "elevenlabs"              # пока единственный
  model: "eleven_multilingual_v2"     # → ElevenLabs API
  speed: 1.2                          # рекламный темп

Если в yaml нет блока voiceover — используются дефолты, либо значения из таблицы voiceover_settings (последняя имеет приоритет, если есть).

Output / Pipeline

ElevenLabs TTS → voiceover.mp3 (raw, длинный, неровный)
       ↓
audio_process.py → voiceover_processed.mp3 (короче, нормализованный, EQ-ed)
       ↓
Director (assemble.py) — использует processed-файл

Файлы лежат в:

~/video-projects/<project>/voiceovers/<timestamp>_<voice-name>.mp3
~/video-projects/<project>/voiceovers/<timestamp>_<voice-name>_processed.mp3

Gotchas и tips

Всегда processed-файл для downstream. Если по ошибке передать raw-файл в director — длительность будет неправильной (длиннее на 5+ сек из-за пауз), и весь монтажный план поедет. audio_process.py = обязательный шаг, не опциональный.
Без ELEVENLABS_API_KEY ничего не работает. Скрипт api_keys.py сначала смотрит env, потом project/api_keys.db. Если нет нигде — кидает осмысленную ошибку «set ELEVENLABS_API_KEY».
Голос должен поддерживать язык. ElevenLabs voice настроенный под английский плохо звучит на русском (акцент, неестественная интонация). При add-voice указывай --language правильно, и фильтруй list-voices / list-available по нему.
Rotation для свежести. Если у проекта 3 голоса и каждый ролик использует один — за 9 роликов каждый голос прозвучит ровно 3 раза, без перевеса. Идеально для batch'а креативов.
speed 1.2 — sweet spot для рекламы. 1.0 ощущается медленно (зритель скроллит), 1.4+ начинает звучать как робот. 1.2 — динамично, но всё ещё естественно. Для length-guidelines в copywriter уже учтено.
stability ниже = драматичнее. Для storytelling-роликов опусти до 0.4 — голос будет более экспрессивный. Для direct_offer оставь 0.5 — нужна чёткость, не эмоция.
Связь с rotation в Director. У director аналогичная rotation для клипов (через video_clips). Оба пула обновляются независимо — голос Rachel может крутиться 3-й раз подряд, а клипы все разные (или наоборот).

🎬 Примеры работы

Открой Showcase → voiceover — там 2 mp3 от ElevenLabs (raw + post-processed через audio_process.py). Все примеры — реальные production-runs, embedded локально (работают офлайн).

Место в пайплайне

copywriter voiceover audio_process director captions

Voiceover = мост между текстом и видео. Принимает: text + voice_id. Отдаёт: mp3 + duration + voice + text. Director без этих 4 параметров не знает, под что монтировать.

Сверху над voiceover обычно стоит orchestrator, который сам вызывает next-voicetts.pyaudio_process.pyrecord-usage в одном проходе. Снизуdirector и captions (последний использует тот же processed-файл для Whisper-замера слов в караоке-субтитрах).