Video Montage — Full Production Pipeline
Вертикальные ролики (9:16, 1080×1920, 30fps) от исходников до готового файла.
Windows-адаптация:
pythonвместоpython3; headless Chrome ="C:/Program Files/Google/Chrome/Application/chrome.exe"(те же флаги); временные файлы → scratchpad, не/tmp; правило кодека то же: толькоlibx264 + yuv420p. Смежное: монтаж готового футажа →video-editor; AI-генерация сцен →video-generation; музыка →ace-step/elevenlabs; публикация в Telegram-канал →tg-bot-publish, в остальные соцсети — своим публикатором (например, Postiz self-hosted), готовой обёртки в паке нет.
Требуется: ffmpeg (с libass и drawtext) · whisper (pip install openai-whisper) ·
Python с Pillow · yt-dlp · ключ ElevenLabs для озвучки.
Пословный караоке-тайминг требует WhisperX отдельно (pip install whisperx) — он в
пак не входит; без него работают обычные субтитры, см. video-editor.
Ключи и голос — свои
Скрипты берут ключи из переменных окружения, а если их нет — из
~/.claude/.credentials.master.env (шаблон: ~/.claude/templates/.credentials.master.env.example,
скопируй и заполни). Нужны ELEVENLABS_API_KEY для озвучки и GOOGLE_API_KEY для
контроля дорожки на слух (scripts/review_cut.py) и поиска рта у персонажа
(scripts/mouth_map.py).
$VOICE_ID / ELEVENLABS_VOICE_ID_RU — идентификатор твоего голоса, дефолта у
него нет намеренно: чужой голос в твоём ролике — это не «удобная настройка».
Где взять: кабинет ElevenLabs → Voices → нужный голос → ID; подробнее — навык
elevenlabs. Не задал — voice_timed.py остановится с явным сообщением, а не
озвучит чужим тембром.
0. Конвейер целиком: одна задача — весь ролик
Когда просьба звучит как «сделай ролик про X», а не как отдельная операция монтажа, не выбирай инструменты вручную — запускай конвейер. Он сам разберёт задачу на роли и проведёт её от брифа до готового файла.
Workflow({
scriptPath: '~/.claude/skills/video-montage/workflows/video-factory.js',
args: { brief: 'как за день собрали лендинг вайбкодингом',
seconds: 35, platform: 'reels', goal: 'watch',
workdir: '~/video-factory/lending-35s' }
})
workdir по умолчанию — ~/video-factory/run. В нём намеренно нет ~/Videos: на
локализованном Linux системная папка называется «Видео», а mkdir -p ~/Videos не
падает — он молча создаёт второй каталог, и готовый ролик оказывается не там, где его
ищут. Класть в системную «Видео» — передавай уже развёрнутый путь:
workdir: "$(xdg-user-dir VIDEOS 2>/dev/null || echo $HOME/Videos)/video-factory/lending-35s".
Итоговый абсолютный путь называй в отчёте: «готово» без пути — это ненайденный файл.
Обрыв не страшен: Workflow({scriptPath, resumeFromRunId}) продолжит с места, а готовые
кадры помечены в конверте и второй раз не оплачиваются.
Роли и порядок (полностью — references/pipeline-architecture.md):
бриф ─┬─ сценарий ─┐
└─ музыка ───┴─ раскадровка ─ кадры (волнами по 4) ─ монтаж ─ контроль
Сценарий и музыка идут одновременно; кадры независимы друг от друга и обрабатываются волнами — больше шести агентов разом упирается в ограничение сервера. Барьеры стоят только там, где они честно нужны: раскадровке нужны и структура, и сетка долей.
Единый документ между ролями — schemas/production.schema.json. Каждая роль пишет
свой раздел и не трогает чужие; это и делает параллельность возможной, и позволяет
перезапускать этапы поодиночке.
Ремесленная часть — в каталоге приёмов, 79 записей с источником и статусом доказательности у каждой:
python scripts/techniques.py stats
python scripts/techniques.py find "не держится внимание"
python scripts/techniques.py for-block крючок
Структура ролика строится не по AIDA: та описывает убеждение покупателя (печатная реклама, 1898) и оптимизирует намерение купить — величину, которую лента не измеряет. Площадки официально называют главным сигналом досмотр, поэтому план строится вокруг удержания:
python scripts/reel_structure.py --message "…" --seconds 35 --goal shares --music track.mp3
python scripts/reel_structure.py --why # откуда взято каждое число
Приёмка: посмотреть и послушать готовый файл
Последний шаг любого ролика — не «сборка прошла без ошибок», а собственный просмотр и прослушивание. Сборка молчит о том, что видно и слышно с первой секунды: у персонажа белый прямоугольник вместо фона, титры серые в момент появления, речь не совпадает с картинкой, последние семь секунд немые. Всё это даёт нулевой код возврата.
python scripts/review_cut.py reel.mp4 -o review/ --listen
Выдаёт контактный лист (весь ролик одной сеткой — открыть и посмотреть), замеры звука (громкость, пики, тишина) и разбор дорожки на слух. Три источника не заменяют друг друга: замеры не слышат кашу в речи, слух не видит призрака в кадре, глаз не измеряет перегруз. Возраст файла печатается первой строкой — если рендер упал, на диске лежит прошлая сборка, и приёмка бодро отчитается по ней.
Липсинк без сторонних платформ
Пока рот на рисунке неподвижен, персонаж читается как мёртвая картинка — это первое, за что цепляется глаз. Рот открывается по громкости голоса; фонемы для рисованного персонажа не нужны, так делают в рисованной анимации с начала звукового кино.
python scripts/mouth_map.py ./poses -o mouth.json --draw check/ # где рот на каждой позе
python scripts/voice_envelope.py voice.mp3 --fps 30 --duration 20 -o env.json
python scripts/cutout.py ./poses -o ./poses_cut # убрать фон
python scripts/cutout.py ./stickers -o ./stickers_cut --outline 14 # наклейка с каймой
Разметку рта обязательно смотреть глазами через --draw: модель отвечает уверенно и
в тех случаях, когда промахнулась.
Озвучка, попадающая в титры
Монолитная озвучка расходится с картинкой: диктор говорит со своей скоростью, титры идут со своей. Каждая фраза синтезируется отдельно и ставится в момент своего титра, а хронометраж подгоняется под реальную речь.
python scripts/voice_timed.py script.json -o voice.mp3 --duration 20 --retime script_fit.json
--retime возвращает сценарий, ужатый под то, как реально звучит речь: без него
половина ролика — паузы, и на слух он затянут, хотя каждый кусок нормальный.
1. Субтитры
1.1 Расшифровка
Whisper кормить только 16 kHz моно WAV:
ffmpeg -y -i video.MOV -ar 16000 -ac 1 -c:a pcm_s16le work/audio.wav
| Модель | Когда | Скорость |
|---|---|---|
small |
быстрая проверка, короткие клипы | быстро |
medium |
один говорящий в камеру | средне |
large-v3 |
несколько голосов, реплики ИИ-ассистентов, тихий/далёкий звук | ~10× медленнее medium |
Всегда --initial_prompt со словами из предметной области — он резко снижает
выдумывание на терминах и именах, которых модель не ждёт:
whisper work/audio.wav --model medium --language ru --output_format srt --output_dir work/ \
--initial_prompt "имена, названия продуктов, сленг, технические термины"
1.2 Правка ошибок
Сырой вывод Whisper использовать нельзя — ошибки выглядят правдоподобно и
проходят мимо взгляда. Прочитать каждую строку; типовые поломки: бренды в кашу
(«код-код-экси» → «Claude Code, Codex»), сленг мимо («позадрочишь» →
«позадротишь»), числа словами («сторилл соус» → «сто рилсов»), неверные границы
слов («вайп-код и шпродук» → «вайб-кодить продукт»), пропущенная тихая реплика
ИИ (перезапустить на large-v3).
Исправленный файл сохраняется как audio_corrected.srt — дальше по конвейеру
источник истины только он.
1.3 SRT → ASS
python scripts/gen_subs.py input.srt output.ass [--font helvetica] [--offset 5.0] [--max-words 2]
По умолчанию 3 слова на титр (темп TikTok), --offset сдвигает всё на длину
интро, --max-words 2 замедляет.
| Стиль | Шрифт | Кегль | Обводка | Для чего |
|---|---|---|---|---|
impact (по умолчанию) |
Impact Bold | 90pt | 8px чёрная | мем/TikTok, ролики с закадром |
helvetica |
Helvetica Neue Bold | 80pt | 3px чёрная | говорящая голова, диалоги, чистый вид |
1.4 Вжечь в видео
ffmpeg -y -i source_video.MOV -vf "format=yuv420p,ass=subs.ass" \
-c:v libx264 -preset medium -crf 18 -c:a aac -b:a 192k \
-movflags +faststart output_with_subs.mp4
2. Озвучка (ElevenLabs)
2.1 Никогда не монолитом
Не синтезируй одним вызовом речь длиннее 15 секунд: фразы слипаются, темп не управляется. Режь на сегменты и размечай паузы:
[seg] Первое предложение. [pause:short]
[seg] Второе, та же мысль. [pause:medium]
[seg] Новая тема. [pause:long]
[seg] Вывод или призыв.
| Пауза | Длина | Когда |
|---|---|---|
| short | 0.16s | между фразами внутри одной мысли |
| medium | 0.34s | между темами |
| long | 0.55s | перед призывом, ключевой цифрой, выводом |
ffmpeg -y -f lavfi -i anullsrc=r=44100:cl=mono -t 0.16 -q:a 9 silence_short.mp3
2.2 Синтез сегмента
curl -s -X POST "https://api.elevenlabs.io/v1/text-to-speech/$VOICE_ID" \
-H "xi-api-key: $ELEVENLABS_API_KEY" -H "Content-Type: application/json" \
-d '{"text":"Текст сегмента.","model_id":"eleven_multilingual_v2",
"voice_settings":{"stability":0.5,"similarity_boost":0.75,"style":0.0,"use_speaker_boost":true}}' \
--output segment_01.mp3
style 0.2-0.4 — энергичнее, stability 0.6-0.7 — спокойнее и ровнее. Выше
style: 0.8 начинается искажение.
2.3 Склейка и темп
Собери concat-лист, чередуя сегменты и файлы тишины, склей и ускорь:
ffmpeg -y -f concat -safe 0 -i concat.txt -c:a libmp3lame -q:a 2 vo_raw.mp3
ffmpeg -y -i vo_raw.mp3 -af "atempo=1.20" vo_final.mp3
1.15-1.25× звучит естественно. Выше 1.40× — роботный голос.
Знаки препинания темпом не управляют: ElevenLabs игнорирует большинство пауз, поэтому файлы тишины обязательны.
3. Сборка видео
3.1 Подготовка клипов
Все клипы приводятся к постоянным 30fps ДО любой склейки — смесь VFR и CFR даёт подвисшие и продублированные кадры:
ffmpeg -y -i clip.MOV \
-vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,fps=30" \
-c:v libx264 -crf 18 -preset fast \
-color_range 2 -colorspace bt709 -color_trc bt709 -color_primaries bt709 \
-c:a aac -b:a 192k clip_prepped.mp4
Вертикальные клипы с телефона: ffprobe покажет 1920×1080 + rotation=-90.
Метаданным поворота не верить — вытащи кадр и посмотри:
ffmpeg -ss 1 -i clip.MOV -vframes 1 work/check.jpg.
3.2 Ken Burns для статики
Статичный кадр держит внимание 5-7 секунд, дальше нужен медленный зум:
ffmpeg -y -loop 1 -i photo.jpg -t 6 \
-vf "scale=1120:1992,zoompan=z='min(zoom+0.0008,1.05)':d=180:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1080x1920,fps=30" \
-c:v libx264 -crf 18 -pix_fmt yuv420p ken_burns.mp4
Исходник масштабируется с запасом (1120×1992 при кадре 1080×1920), иначе на зуме вылезут края.
3.3 Картинка под текст закадра
~80 % клипов должны прямо иллюстрировать то, о чём говорит диктор сейчас, ~20 % —
перебивки (зал, ходьба, печатание) для разнообразия между темами. Порядок работы:
расписать таймлайн (0:00-0:15 тема A, 0:15-0:40 тема B, 0:40-1:00 призыв),
разложить клипы по слотам, чередовать ракурсы (с рук / со штатива).
3.4 Склейка
# все клипы уже приведены к одному кодеку → без перекодировки
ffmpeg -y -f concat -safe 0 -i concat.txt -c copy body.mp4
Если кодеки разные — -filter_complex "[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[v][a]"
с перекодировкой.
3.5 Оверлеи интро/аутро
ffmpeg -y -i bg_video.mp4 -i title.png -i logo.png -filter_complex "
[0:v][1:v]overlay=x=(W-w)/2:y=300:enable='between(t,0,5)'[tmp];
[tmp][2:v]overlay=x=(W-w)/2:y=800:enable='between(t,1,5)'" \
-c:v libx264 -crf 18 -c:a copy intro.mp4
4. Текстовые ролики
B-roll + стилизованный текст + музыка, без закадра. Для цитат, советов, цифр, списков.
python scripts/gen_text_overlay.py --text "Цитата" --output overlay.png \
[--font Impact] [--font-size 72] [--position lower_third] [--max-chars 25]
11 пресетов шрифтов, автоуменьшение кегля при переполнении (стоп на 36pt), перенос по предложениям, обводка + тень, три позиции (center, lower_third, upper_third).
Фон под текст затемнить, иначе текст не читается на светлых кадрах:
ffmpeg -y -i clip.mp4 -i text_overlay.png -filter_complex "
[0:v]drawbox=c=black@0.27:t=fill[dimmed];
[dimmed][1:v]overlay=0:0" -c:v libx264 -crf 18 -c:a copy text_reel.mp4
0.27 — рабочее значение по умолчанию для текстовых роликов.
Забрать звук у трендового ролика: yt-dlp -f bestaudio -o "source_audio.%(ext)s" <url>
(или ffmpeg -i reel.mp4 -vn -c:a copy source_audio.m4a), подмешать через
[1:a]volume=0.15[bgm];[0:a][bgm]amix=inputs=2:duration=first[a].
5. Звук
5.1 Уровень музыки
| Тип контента | Громкость BGM |
|---|---|
| Ролик с закадром | 5-8 % |
| Текстовый ролик (без голоса) | 15-20 % |
| Драматический момент | 3-5 % (приседает под ключевой фразой) |
ffmpeg -y -i body.mp4 -i bgm.mp3 -filter_complex "
[1:a]aloop=loop=-1:size=2e+09,atrim=duration=60,volume=0.08[bgm];
[0:a][bgm]amix=inputs=2:duration=first:dropout_transition=2[a]" \
-map 0:v -map "[a]" -c:v copy -c:a aac -b:a 192k final.mp4
5.2 Звук в конкретный момент
ffmpeg -y -i body.mp4 -i sfx.mp3 -filter_complex "
[1:a]volume=0.25,adelay=2000|2000[sfx];
[0:a][sfx]amix=inputs=2:duration=first[a]" \
-map 0:v -map "[a]" -c:v copy -c:a aac body_with_sfx.mp4
adelay=2000|2000 — 2 секунды, в миллисекундах, значение на каждый канал
отдельно (один канал = звук поедет только слева).
5.3 Чистка закадра
# паузы длиннее 0.3 с
ffmpeg -i vo.mp3 -af "silencedetect=noise=-30dB:d=0.3" -f null - 2>&1 | grep "silence_end"
# запинки — по пословным таймкодам
whisper vo.mp3 --model small --language ru --word_timestamps True --output_format json --output_dir work/
# срезать тишину в начале (в записях с телефона её всегда 0.5-1.5 с)
ffmpeg -y -i vo.mp3 -ss 0.8 -c:a copy vo_trimmed.mp3
6. Цвет и формат
Телефон пишет HEVC в HLG/bt2020 10 бит — большинство плееров и браузеров показывают это неверно (выцветшая или перенасыщенная картинка). Проверка:
ffprobe -v error -select_streams v:0 \
-show_entries stream=codec_name,pix_fmt,color_space,color_transfer,color_primaries video.MOV
bt2020 / arib-std-b67 / yuv420p10le → это HDR, нужен перегон в SDR:
-vf "format=yuv420p" -color_primaries bt709 -color_trc bt709 -colorspace bt709.
Эти же три тега ставить на КАЖДОМ кодировании libx264 — без них картинка теряет насыщенность понемногу на каждом проходе, и к финалу видно.
| Назначение | Кодек | CRF | Примечание |
|---|---|---|---|
| Мастер | libx264 | 18 | всегда |
| Telegram / сжатое | libx264 | 28-30 | при нужде до <50 МБ — scale=720:1280, аудио 96k |
| Превью | libx264 | 30 | быстрая проверка |
| Никогда | hevc_videotoolbox | — | файл может не открыться в iCloud/QuickTime |
-movflags +faststart — на каждом экспорте, иначе видео не стримится в вебе и на
мобильных.
7. Приёмка и выдача
# кадры на ключевых секундах — смотреть глазами
for t in 1 5 10 20 30; do ffmpeg -y -ss $t -i final.mp4 -vframes 1 -q:v 2 work/check_t${t}s.jpg; done
# речь в финале совпадает со сценарием?
whisper final.mp4 --model small --language ru --output_format txt --output_dir work/qa/
# концовка не срезана? (-shortest молча режет последнюю фразу)
ffprobe -v error -show_entries format=duration -of csv=p=0 final.mp4
ffmpeg -y -sseof -10 -i final.mp4 -vn -c:a pcm_s16le work/ending.wav
whisper work/ending.wav --model small --output_format txt --output_dir work/qa/
На кадрах проверяй: титры видны и на месте, шрифт и обводка те, нет двойных титров от прошлых рендеров, оверлеи не съехали.
8. Пословная синхронизация, карточки, липсинк под трек
Когда титр или карточка должны попадать в конкретное слово (музыка, рэп, быстрый
сленг), всплывающие карточки-определения, откуда брать настоящие фото и логотипы,
сборка музыкального клипа из нескольких дублей → references/word-sync-cards.md.
Оттуда три вещи, о которых легко забыть в основном потоке: посегментные тайминги
Whisper плывут на ±1-2 с и на музыке выглядят сломанными; трек почти никогда не
начинается с t=0; однокадровый PNG нельзя гасить fade=alpha — альфа замерзает
на нуле и карточка не появляется вовсе.
9. Безопасные зоны (1080×1920)
Интерфейс площадки (подпись, имя, полоса прогресса, кнопки реакций) перекрывает края кадра. Титры, карточки и логотипы держать внутри:
| Поле | Instagram/FB Reels | TikTok | Что там |
|---|---|---|---|
| Сверху | 220px (мин. 108) | 108px | профиль, поиск |
| Снизу | 420px (мин. 320) | 320px | подпись, имя, прогресс |
| Слева | 60px | 60px | — |
| Справа | 120px | 120px | колонка лайк/коммент/шер |
Кросс-платформенный безопасный прямоугольник: 900×1400 по центру. Карточки
центровать с симметричными боковыми полями; вертикаль субтитров задавать через
MarginV в стиле ASS, чтобы текст сидел выше нижней зоны.
⚠️ Уникализация (§10) зумит кадр примерно в 1,075× и съедает эти поля — закладывай запас заранее и перемеряй на финальном файле, а не на исходном.
10. Уникализация под репост
Заливаешь заново уже опубликованный ролик и площадка помечает дубликатом →
references/uniquify.md: три слоя детекта (визуальный CNN, аудиоотпечаток,
хэш+метаданные) и две готовые команды под них.
Главное оттуда: кроп/яркость/перекодировка по отдельности НЕ работают; на роликах с вшитыми карточками нельзя применять бочку и виньетку (выгнут прямые края); никогда не отражать по горизонтали и не менять скорость/питч звука.
11. Правила, оплаченные ошибками
CFR до склейки — все сегменты приводятся к постоянным 30fps перед любой конкатенацией. VFR + CFR = подвисшие кадры, дубли, расход звука.
Не накладывать поверх вшитого текста — если в видео уже вжжены субтитры, вторые сверху дают призрачный текст. Пересобирать из исходных слоёв.
Максимум 3 версии — если после v3 не работает, остановиться и разобраться системно, а не латать симптомы.
Диагностика до пересборки — ffprobe, кадры, логи ДО новой попытки сборки.
Текст субтитров проверять руками — даже large-v3 делает правдоподобные ошибки.
Тишина в начале закадра — в записях с телефона всегда 0.5-1.5 с, детектировать и срезать.
Картинка под слова — говорит «письмо» → показываем письмо. 80 % по теме, 20 % перебивки.
Проверять ориентацию клипа — метаданные поворота врут, вытащить кадр и посмотреть.
Никогда
hevc_videotoolboxв финал — QuickTime и iCloud могут отказаться открыть. Толькоlibx264 + yuv420p.Всегда
-movflags +faststart— иначе видео не стримится в вебе и на мобильных.Пословная синхронизация требует пословных таймкодов — для музыки, рэпа и быстрого сленга посегментные тайминги плывут ±1-2 с.
--word_timestamps Trueи свой текст поверх потока слов. Не считать, что трек начинается с t=0.Не гасить однокадровый PNG альфа-фейдом — альфа замерзает на нуле и оверлей не появляется. Только
overlay=…:enable='between(t,a,b)'.Ключ картинки — имя, а не индекс — после пересортировки списка индекс укажет на чужой элемент.
Настоящие фото для настоящих людей и брендов — сгенерированные лица и логотипы выглядят плохо; AI оставить абстракциям и предметам.
Соблюдать безопасные зоны и закладывать запас под уникализацию — верх 220 / низ 420 / слева 60 / справа 120 (§9); центральный кроп из §10 съедает поля, поэтому мерить на финальном файле.
Уникализировать, не искажая вшитую графику — при вшитых карточках и плашках никакой бочки и виньетки; зерно + оттенок + кроп + срез метаданных. Не отражать и не менять скорость звука.