Skill #9 · Animated subtitles

video-captions

Накладывает анимированные субтитры на видео локально через Whisper + ASS + FFmpeg или Remotion. Karaoke-стиль с подсветкой по словам, без внешних API, без расходов. Финальный шаг основного видео-пайплайна.

Путь
~/.claude/skills/video-captions/
Триггеры
субтитры, captions, добавь субтитры, прогорь субтитры, karaoke subtitles, анимированные субтитры
Зависит от
FFmpeg (с libass), openai-whisper, Node.js (для Remotion-режима), Python 3.9+
Пэйрится с

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

Это «титровальщик». На вход: видео с озвучкой + текст копирайтера. На выход: то же видео, но с анимированными субтитрами, где активное слово подсвечивается синхронно с речью. Алгоритм:

  1. Прогоняет аудио через Whisper → получает word-level timestamps (start/end на каждое слово)
  2. Берёт оригинальный текст копирайтера (не транскрипт Whisper!) и подставляет в найденные тайминги
  3. Группирует слова в фразы (2-4 на строку для karaoke / 1 для pop)
  4. Генерирует ASS-файл (Advanced SubStation Alpha) с karaoke-тегами \kf для подсветки
  5. FFmpeg либо Remotion прожигает субтитры в кадры → готовое .mp4
ВСЕГДА передавать --text с оригинальным копирайтерским текстом. Whisper делает ошибки распознавания (например, «Баздент» вместо «Bas Dent», «с травман» вместо «Straumann»). Текст для отображения берётся из --text, Whisper нужен ТОЛЬКО для тайминга. Без --text — в финальном видео будут косяки транскрипции.

Два рендерера

СкриптДвижокСкоростьГибкостьКогда
remotion_captions.py
default
Remotion (React) 2-5 минут Полная — spring physics, фоны, любые анимации, sentence-aware группировка (не разрывает предложения между caption-страницами) Финальная доставка, соцсети, когда визуал важен
local_captions.py FFmpeg + ASS burn-in Секунды Ограничена ASS-форматом — цвет, обводка, тень, karaoke-подсветка. Никаких физических анимаций. Fallback / быстрый драфт, bulk-производство
Дефолт = remotion_captions.py. Это user-preference. Внутри captions-скилла не использовать local_captions.py (ASS burn-in), если только пользователь явно не попросит fallback. Также не использовать встроенные субтитры из assemble.py в director — это отдельный шаг, делается этим скиллом.

Стили субтитров

StyleОписаниеЛучше для
karaokeПодсветка по словам жёлтым на белом тексте + чёрная обводка, 60px, снизу. Универсал.Дефолт — работает для всего
boldТо же что karaoke, но 70px и обводка толще. Бьёт по глазам.Короткие ролики, hook-фокус
minimalМаленький белый текст 40px, тонкая обводка. Деликатно.Корпоратив, B2B
popПо одному слову по центру, 80px, scale-in анимация.TikTok / Reels стиль
tiktokЗолотая подсветка на активном слове, 2-3 слова в группе, pop-in scale, полупрозрачный бэкграунд-бокс, центр, 72px.Соцсети, виральный контент
Monochrome scale-pop вариант — для talking-head и personal brand. Берём karaoke с --highlight-color "#FFFFFF": активное слово окрашивается в тот же цвет что и неактивный текст, остаётся только пульсация размера (scale-pop). Без жёлтой/cyan вспышки. Использует video-screencast по умолчанию.

Команды

Основной вызов (Remotion-режим, дефолт)

python ~/.claude/skills/video-captions/scripts/remotion_captions.py \
  --video assembled_video.mp4 \
  --text "$(cat copywriter_original.txt)" \
  --style karaoke \
  --language ru \
  --model small \
  --project-dir ~/video-projects/bas_dent \
  --output captioned.mp4

FFmpeg fallback (быстрый, ASS burn-in)

python ~/.claude/skills/video-captions/scripts/local_captions.py \
  --video assembled_video.mp4 \
  --text "$(cat copywriter_original.txt)" \
  --style karaoke \
  --language ru \
  --model small \
  --project-dir ~/video-projects/bas_dent

Monochrome scale-pop (для talking-head)

python ~/.claude/skills/video-captions/scripts/remotion_captions.py \
  --video cleaned_talking_head.mp4 \
  --text "$(cat canonical.txt)" \
  --style karaoke \
  --highlight-color "#FFFFFF" \
  --font-size 64 \
  --project-dir ~/video-projects/bas_dent \
  --output captioned.mp4

Hook overlay (анимированный заголовок на первой сцене)

python ~/.claude/skills/video-captions/scripts/hook_overlay.py \
  --video assembled_video.mp4 \
  --text "$(cat voiceover_full.txt)" \
  --voiceover voiceover.mp3 \
  --hook-style neon \
  --sfx-file ~/.claude/skills/video-soundfx/sfx/text/pop_bright.mp3 \
  --project-dir ~/video-projects/bas_dent \
  --language ru

Пропустить captions во время hook-сцены

python ~/.claude/skills/video-captions/scripts/remotion_captions.py \
  --video video_with_hook.mp4 \
  --text "..." \
  --style karaoke \
  --skip-until 4.9 \
  --project-dir ~/video-projects/bas_dent

Полные опции

--style karaoke|bold|minimal|pop|tiktok  # анимация (default: karaoke)
--font "Arial Black"                     # имя шрифта
--font-size 60                           # размер в пикселях
--highlight-color "&H0000FFFF"            # ASS-цвет активного слова (жёлтый)
--primary-color "&H00FFFFFF"              # ASS-цвет обычного текста (белый)
--model tiny|base|small|medium|large     # Whisper-модель (default: small)
--language ru|en|kk|uk                   # язык для Whisper hint
--text "..."                             # ОРИГИНАЛЬНЫЙ текст копирайтера (обязательно!)
--skip-until 4.9                         # пропустить субтитры до X сек (для hook overlay)

Hook templates (15 пресетов)

Hook overlay — анимированный заголовок на первой сцене ролика. Слова влетают по одному со spring-анимацией + SFX. Применяется только к первому предложению из voiceover-текста.

StyleШрифтыАнимацияХарактер
neonBebas+Oswald+Bangerszoom-inНеоновое свечение, розовый/cyan/gold
markerPermanentMarker+LilitaOnescale-slamМаркерный, жёлтый/коралл
cyberChakraPetch+PaytoneOneslide-rightКибер, cyan/фиолет
gradientOswald+Bangers+BebasdropКонтраст, фиолет/белый/коралл
stencilBangersslide-leftТрафарет, белый/красный/жёлтый

Hook overlay автоматически:

  1. Достаёт первое предложение из voiceover-текста
  2. Берёт word-level timestamps через Whisper
  3. Классифицирует слова на «key» (большие, цветные) и «filler» (мелкие, белые)
  4. Рендерит с per-word spring-анимациями + опциональный SFX pop
  5. Возвращает hook_end_sec для --skip-until в шаге captions
Hook = первое предложение целиком. Длительность hook-сцены и первой сцены = длительность всего первого предложения, измеряется через Whisper до сборки. Второй клип идёт только после первого предложения. Это зашитое правило пайплайна.

Скрипты

ФайлЧто делает
scripts/remotion_captions.py defaultГлавный воркхорс. Whisper тайминги → JSON props → npx remotion render. Качественные React-анимации, sentence-aware группировка.
scripts/local_captions.pyFallback FFmpeg-режим. Whisper → ASS-файл с karaoke-тегами → ffmpeg -vf "ass=..." burn-in.
scripts/transcribe_words.pyОбёртка над Whisper для получения чистого word-level транскрипта (используется hook_overlay и captions_api).
scripts/captions_api.pyAPI-уровень: парсит --text копирайтера, мэтчит на тайминги Whisper, отдаёт нормализованные phrases для рендера.
scripts/hook_overlay.pyРендерит анимированный заголовок на первой сцене. 5 hook-стилей (neon/marker/cyber/gradient/stencil), per-word spring, SFX pops.
scripts/render_final.pyФинальный композ для multi-stage пайплайна (hook + captions + музыка в одном проходе).

Цвета в ASS-формате

ASS использует &HAABBGGRR (alpha, blue, green, red — обратный порядок к RGB):

ЦветASSHex (для Remotion-режима)
Белый&H00FFFFFF#FFFFFF
Жёлтый&H0000FFFF#FFFF00
Cyan&H00FFFF00#00FFFF
Красный&H000000FF#FF0000
Зелёный&H0000FF00#00FF00

Whisper-модели

МодельРазмерСкоростьТочностьКогда
tiny39MBсамая быстраябазоваябыстрый тест
base74MBбыстраяхорошаядрафт
small461MBсредняяотличнаяdefault — лучший баланс
medium1.5GBмедленнаяпочти идеальнаякогда точность критична
large2.9GBсамая медленнаялучшаякритический контент

Первый запуск качает модель (~461MB для small) в ~/.cache/whisper/ — потом из кэша.

Конфиг из project.yaml

Captions читают из ~/video-projects/<project>/project.yaml:

language: ru                      # язык для Whisper hint
captions:
  style: karaoke                  # karaoke|bold|minimal|pop|tiktok
  font: "Arial Black"             # имя шрифта (опционально)
  font_size: 60                   # размер в пикселях
  highlight_color: "&H0000FFFF"   # жёлтый (default для karaoke)
  primary_color: "&H00FFFFFF"     # белый
  model: small                    # Whisper-модель

Gotchas и tips

Без --text ничего не делать. Whisper-транскрипт ≠ копирайтерский текст. Бренды/имена/термины распознаются с ошибками. Всегда копировать текст из voiceover/copywriter и передавать через --text. Это user-preference, зашитое в memory.
FFmpeg должен быть с libass. Стандартный ffmpeg из Homebrew часто без libass. Нужен ffmpeg-full: brew install ffmpeg-full. Проверка: ffmpeg -filters | grep ass должна показывать subtitle-фильтр.
Видео должно иметь audio track. Whisper падает на видео без звука. Если на входе silent-видео (например голый montage) — captions невозможны, нужен сначала director с озвучкой.
ASS-файл сохраняется рядом с output. Можно подправить руками (изменить тайминг, цвет на конкретное слово, добавить sub-эффект) и затем прожечь повторно через FFmpeg напрямую: ffmpeg -i input.mp4 -vf "ass=subtitles.ass" -c:v libx264 -crf 23 output.mp4.
Remotion-режим переживает sentence-разрывы. ASS-режим может разорвать предложение в неудобном месте (между двумя caption-страницами). Remotion группирует пословно с учётом границ предложений — выглядит ровно.

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

Открой Showcase → final-voiceover — там 2 финальных voiceover-видео для bas-dent с karaoke-субтитрами (рендер через Remotion). Все примеры — реальные production-runs, embedded локально (работают офлайн).
А также Showcase → 🎯 Hook overlay presets — 15 анимированных hook-стилей (stencil / neon / glitch / cyber / 3d-pop / scale-pop / marker / typewriter / chip-bg / gradient / bold-statement / editorial / minimal / karaoke-row / highlight-marker). Один и тот же текст «Запусти за 30 дней» через 15 разных стилей.

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

copywriter voiceover director captions reviewer

Captions = финальный шаг основного видео-пайплайна. Вход: assembled.mp4 + copywriter_text.txt. Выход: captioned.mp4 с прожжёнными анимированными субтитрами. Дальше — только reviewer для контроля качества.

Также используется в screencast-пайплайне для talking-head контента — обычно с --style karaoke --highlight-color "#FFFFFF" (monochrome scale-pop):

screencast captions director/overlay_music.py

В этом маршруте captions работают по чистому видео с реальным микрофоном (после voice cleanup).