Гибридные рилсы: talking-head на камеру + AI B-roll под TTS
Полностью сгенерированный talking-head плывёт от кадра к кадру и не проходит compliance под юридически чувствительный оффер. Гибридный рилс оставляет лицо живому эксперту на камере, а AI отдаёт только атмосферный B-roll без лица под синтезированную речь — в предсказуемом конвейере, где каждый шаг считается по деньгам и чинится за вечер.
Проблема
Полностью сгенерировать 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-секундную речь. Десять центов разницы окупаются тем, что вставки перестают выглядеть как залипшая гифка.
Где ломается
- Модель переписывает весь сценарий. «Заполни слоты» читается как «сделай хороший сценарий целиком», и абзацы под уже отснятое видео меняются. Единственное надёжное лекарство — детерминированный merge в скрипте плюс сохранённый сырой ответ LLM для повторного слияния без нового API-вызова. Промпт «не трогай абзацы» сам по себе не enforcement.
- Seedance отказывается от 3 секунд. Диапазон 4–12 с не очевиден из маркетинговых «коротких клипов». Без clamp в скрипте — HTTP 400 на ровном месте.
- Речь длиннее видео → гипнотический loop. Если T2V зовут с фиксированной короткой длиной, а LLM пишет длинную речь, 4-секундный клип крутится два-три раза под одну фразу. Лечится порядком «TTS первым, длина из ffprobe» плюс укорачиванием речи в brief; loop — safety net, не основной режим.
-
«Бесплатный» голос из библиотеки отвечает 402.
На free tier через API доступны только premade-голоса; shared/library требуют
платный план, даже если в каталоге стоит
free_users_allowed: true. Пометка в UI ≠ право синтезировать с free-ключом. Для «родного» русского тембра нужен paid или другой провайдер. -
ffprobe-static≠ffmpeg-static. Первый экспортирует объект{ path }, второй — строку пути.spawnпадает или путьundefined. Спасаетtypeof mod === "string" ? mod : mod?.pathво всех вызовах. -
Windows без ffmpeg и с фейковым Python.
pythonоткрывает Store,ffmpegне в PATH. Node-only пайплайн плюс статичные бинарники в skills снимают вопрос. -
Склейка телефона и нейросети.
Разные codec/size/fps/audio layout ломают
-c copy— чёрные кадры или рассинхрон. Для полного рилса сразу--mode reencodeс единым холстом рациональнее, чем надеяться на stream-copy. -
Video-стоимость доминирует.
Chat-LLM — доли цента, TTS на free — ноль, а Seedance — порядка $0.06–0.13 за ~5 с
на 480p. Поэтому cap 10 с и запрет
generate_audioв reels — продуктовые решения, а не микрооптимизация. Коллекция video-моделей на OpenRouter сортируется по популярности, а не по цене — легко выбрать дорогую по невнимательности.
Для кого и почему
Для тех, кто собирает мультимодальные конвейеры на внешних API — text-to-video, TTS, LLM — и хочет предсказуемую себестоимость секунды картинки вместо «магической кнопки». Практический интерес шире рилсов: любой пайплайн, где несколько платных моделей стыкуются через файлы и манифесты.
Три вывода переносятся почти дословно на другие мультимодальные сборки. Первый: разделяйте неизменяемый каркас и генерируемые слоты — в данных, в коде и в правах модели. Второй: длительность мультимодального клипа измеряется, а не угадывается — TTS → ffprobe → T2V duration единственный надёжный порядок. Третий: ограничения моделей (4–12 с, free vs library voices) — часть дизайна продукта, и закладывать их надо в схему, а не ловить в рантайме.
Гибрид talking-head + B-roll — прагматичный компромисс: лицо человека там, где нужно
доверие, генерация — там, где дорогая съёмка атмосферы. Пайплайн остаётся
экспериментальным: нет оркестратора-сервиса, очередей и UI. Зато каждый шаг —
читаемый .mjs
на десятки-сотни строк, который команда из одного разработчика и агента в IDE
прогоняет руками и чинит за вечер.
Ссылки
- Артефакт: клиентский экспериментальный пайплайн (публичного репозитория нет); агентские skills
concat-videosиextract-video-frameпод ffmpeg/ffprobe. - Внешний контекст: OpenRouter Videos API (Seedance 1.5 Pro), ElevenLabs TTS (
eleven_multilingual_v2),ffmpeg-static/ffprobe-static. - Блог: Как мы собрали пайплайн гибридных рилсов: живой эксперт плюс AI-видеовставки
Читать дальше
Об авторе
Григорий Добряков — AI-Driven Head of Engineering
Собираете мультимодальный пайплайн с предсказуемой себестоимостью?
Каркас и слоты, измеренная длительность, cost guard на video API — с рабочим конвейером, а не «магической кнопкой».
Написать мнеДругие разборы
Серия инженерных разборов: реальная задача → методология → рабочий артефакт → честный разбор того, где это ломается.
К серии →