Гибридные рилсы: talking-head на камеру + AI B-roll под TTS | Григорий Добряков

Григорий Добряков

Howto · разбор

Разбор 19 Text-to-video TTS · ffmpeg Cost guard

Гибридные рилсы: talking-head на камеру + AI B-roll под TTS

Полностью сгенерированный talking-head плывёт от кадра к кадру и не проходит compliance под юридически чувствительный оффер. Гибридный рилс оставляет лицо живому эксперту на камере, а AI отдаёт только атмосферный B-roll без лица под синтезированную речь — в предсказуемом конвейере, где каждый шаг считается по деньгам и чинится за вечер.

Tech Lead Architect Founder Head of AI

Проблема

Полностью сгенерировать talking-head сегодня можно, но выйдет дорого, лицо будет плыть от кадра к кадру, а под юридически чувствительный оффер (недвижимость) это не проходит вовсе: клиенту нужен узнаваемый человек, которому верят, а не аватар, которого через месяц засудят за «обещание доходности из уст несуществующего лица».

С другой стороны, снимать каждую перебивку — стройку, транспорт, документ крупным планом, телефон в руке — тоже дорого: это выездная съёмка ради трёх секунд атмосферы.

Отсюда гибрид. Каркас речи эксперт говорит на камеру сам — это доверие и лицо. Между его абзацами вставляются AI-мосты: генеративное видео без лица плюс синтезированная речь. Задача — собрать это в предсказуемый конвейер, где каждый шаг можно прогнать руками, посчитать по деньгам и починить за вечер, а не в «магическую кнопку», которая иногда выдаёт шедевр, а иногда — 13-секундную речь поверх 4-секундного клипа, крутящегося три раза подряд.

Сразу оговорка про что это не. Это не «оживить фото» и не липсинк. Лицо в T2V запрещено на уровне system-промпта и brief. AI отвечает только за атмосферный B-roll — там, где съёмка дорогая, а точность лица не нужна, потому что лица в кадре нет.

Методика

Пять стадий, жёсткий порядок, детерминированные стыки. Стек намеренно скучный: чистый Node ESM без Python, ручной парсер .env, fetch к HTTP API, локальный ffmpeg из npm-пакетов.

Принцип 1 — неизменяемый каркас и генерируемые слоты

Сценарий делится на две несмешиваемые части. Каркас (source-scenario.md — фиксированные абзацы, которые эксперт уже произнёс на камеру; их нельзя трогать, иначе baseline-клипы на диске перестанут совпадать с текстом. Между абзацами — плейсхолдеры <CUT 1>, <CUT 2>, …

Модель заполняет только слоты. Не переписывает каркас, не «улучшает» сценарий целиком. Ранние прогоны показали, что «заполни слоты» LLM охотно читает как «сделай хороший сценарий с нуля» — и переписывает абзацы, под которые уже отснято видео. Лечится не уговорами в промпте, а контрактом: модель возвращает только CUT-блоки, слияние с каркасом делает скрипт. Детерминированный merge даёт идемпотентный re-run, побитовое совпадение baseline-текста и чистый diff — меняются только вставки.

Каждый слот описан в brief cuts-brief.md: роль в воронке (боль / метод / доверие / усиление CTA), смысл речи (без готовой формулировки — её пишет LLM), тип кадра для T2V и стык с соседними абзацами. Глобальные ограничения — вертикаль 9:16, устно 3–5 секунд на вставку, без цен, конкретных ЖК/застройщиков, обещаний доходности и гарантий ипотеки. Это одновременно compliance и защита от галлюцинаций цифр.

Порядок финальной склейки живёт в timeline.json — простое чередование baseline-файлов и номеров CUT. Речь и клипы связаны порядком, а не таймкодами внутри одного файла: любой кусок можно переснять, не пересобирая тайминги.

Принцип 2 — длительность измеряется, а не угадывается

Порядок стадий жёсткий: сначала речь, потом видео. Из каждого CUT-блока берётся текст до разделителя (после него — video-промпт на английском). Этот текст уходит в TTS (ElevenLabs, eleven_multilingual_v2, premade-голос). Файл cut-N.mp3 сохраняется — и его длительность берётся не из ответа API, а из локального ffprobe. Источник длины прямо пишется в манифест: "duration_source": "ffprobe".

Дальше эта измеренная длина, а не оценка «примерно пять секунд» из brief, задаёт длительность T2V-клипа. Ключ ко всему — не доверять оценке там, где можно измерить. В result-2 четыре реплики дали 6.0 / 4.6 / 6.3 / 5.1 секунды — уже не «все по пять».

Принцип 3 — ограничения модели как параметр продукта, а не баг интеграции

Text-to-video — Seedance 1.5 Pro через OpenRouter Videos API: aspect_ratio 9:16, resolution 480p, generate_audio: false (звук уже есть из TTS, нативная генерация аудио удорожает секунду впустую). Жизненный цикл — POST job → polling каждые 5 с → download → запись в манифест.

Seedance принимает длительность только в диапазоне 4–12 секунд. Запрос на 3 секунды возвращает HTTP 400. Отсюда формула:

requested = max(4, min(10, ceil(speech_duration_sec)))

Нижняя граница 4 — требование API. Верхняя 10 — жёсткий cost guard (API формально даёт до 12, но каждая лишняя секунда видео — деньги). ceil — чтобы видео не оказалось короче речи. Если речь всё же длиннее 10 секунд, видео заказывается короче, а на стадии mux картинка зацикливается до конца аудио (-stream_loop -1). Loop оставлен как страховка, не как штатный режим.

Принцип 4 — mux и финальная склейка

Для каждой пары cut-N.mp4 + cut-N.mp3: ffprobe длительностей, при videoDur + 0.05 < audioDur — loop видео (запас 50 мс гасит ложные loop от округления probe), затем ffmpeg кладёт речь поверх видео, обрезает по длине речи, кодирует H.264 + AAC. Результат — cut-N-av.mp4.

Финальный рилс собирается отдельно по timeline.json: baseline-клипы с телефона чередуются с AI-вставками. iPhone .MOV и Seedance .mp4 почти всегда несовместимы по кодеку/разрешению/fps, поэтому склейка уходит в re-encode с единым холстом (scale + pad, 30 fps, yuv420p, AAC); клипам без звука подставляется anullsrc, чтобы дорожка не прыгала.

Принцип 5 — манифесты как бухгалтерская книга

Каждая платная или измеримая стадия пишет JSON рядом с артефактами: cut-tts.json (длительности, символы, quota, голос), cut-videos.json (jobId, cost на клип, фактическая длительность заказа), cut-av.json (зациклено или нет, итоговые длины). Это позволяет объяснить счёт OpenRouter/ElevenLabs по клипам, пересобрать только часть (--only 3), отладить «почему видео зациклилось» и не гадать, откуда взялась длина Seedance. Медиа — в .gitignore; в git остаются сценарии, brief, timeline и манифесты. Воспроизводимость без гигабайтов.

Артефакт

Экспериментальный пайплайн, вырос из разового скрипта T2V-генерации через OpenRouter и оброс стадиями. Публичного репозитория нет — контур клиентский. Костяк:

reels/
├── fill-cuts.mjs             # LLM заполняет слоты <CUT N>, скрипт мержит с каркасом
├── generate-cut-tts.mjs      # ElevenLabs → cut-N.mp3 + cut-tts.json (длина из ffprobe)
├── generate-cut-videos.mjs   # Seedance → cut-N.mp4 + cut-videos.json
├── mux-cut-av.mjs            # речь поверх видео → cut-N-av.mp4
├── baseline/variant-*/       # source-scenario.md, cuts-brief.md, timeline.json, *.MOV
└── generations/result-*/     # scenario.md, cut-N.*, манифесты, cuts-av.mp4

ffmpeg и ffprobe — статичные бинарники из npm-пакетов, обёрнуты в агентские skills concat-videos и extract-video-frame. На Windows это не прихоть: системный python из WindowsApps — заглушка Store, а ffmpeg в PATH обычно отсутствует.

Два реальных прогона как proof:

result-1 (ранний) result-2 (после ужесточения)
Речь на слот длинные абзацы (до ~13 с) 1–2 коротких предложения (~5–6 с)
Длительность T2V фиксированные 4 с из TTS: 6 / 5 / 7 / 6 с
Mux loop все 4 клипа зациклены ни одного loop
Video cost ~$0.18 ~$0.28

Парадокс, который стоит проговорить: result-2 дороже по видео, но честнее визуально — нет троекратного прокручивания 4-секундного B-roll под 13-секундную речь. Десять центов разницы окупаются тем, что вставки перестают выглядеть как залипшая гифка.

Где ломается

Для кого и почему

Для тех, кто собирает мультимодальные конвейеры на внешних API — text-to-video, TTS, LLM — и хочет предсказуемую себестоимость секунды картинки вместо «магической кнопки». Практический интерес шире рилсов: любой пайплайн, где несколько платных моделей стыкуются через файлы и манифесты.

Три вывода переносятся почти дословно на другие мультимодальные сборки. Первый: разделяйте неизменяемый каркас и генерируемые слоты — в данных, в коде и в правах модели. Второй: длительность мультимодального клипа измеряется, а не угадывается — TTS → ffprobe → T2V duration единственный надёжный порядок. Третий: ограничения моделей (4–12 с, free vs library voices) — часть дизайна продукта, и закладывать их надо в схему, а не ловить в рантайме.

Гибрид talking-head + B-roll — прагматичный компромисс: лицо человека там, где нужно доверие, генерация — там, где дорогая съёмка атмосферы. Пайплайн остаётся экспериментальным: нет оркестратора-сервиса, очередей и UI. Зато каждый шаг — читаемый .mjs на десятки-сотни строк, который команда из одного разработчика и агента в IDE прогоняет руками и чинит за вечер.

Ссылки

Об авторе

Григорий Добряков — AI-Driven Head of Engineering

Собираете мультимодальный пайплайн с предсказуемой себестоимостью?

Каркас и слоты, измеренная длительность, cost guard на video API — с рабочим конвейером, а не «магической кнопкой».

Написать мне

Другие разборы

Серия инженерных разборов: реальная задача → методология → рабочий артефакт → честный разбор того, где это ломается.

К серии →