Новости по регионам

  1. Главная
  2. IT
  3. Как мы собираем рекламные шортсы прямо из движка игры: Godot, Python и ffmpeg

Как мы собираем рекламные шортсы прямо из движка игры: Godot, Python и ffmpeg

Мы небольшая студия, делаем экономическую стратегию про IT-компанию на Godot 4. Для соцсетей нужно много коротких вертикальных роликов, а монтировать каждый руками долго и скучно. В итоге собрали конвейер: игра сама разыгрывает сцену и пишет видео, а скрипт на Python монтирует из этого шортс с подписями, голосом, музыкой и обложкой. Один ролик собирается примерно за минуту, сейчас их 64. Расскажу, как это устроено и на какие грабли наступили.

Запись: Movie Maker в Godot

В Godot 4 есть режим Movie Maker. Если запустить проект с ключом --write-movie, движок пишет каждый кадр в файл с фиксированным шагом времени, а не в реальном времени. Запись не зависит от того, успевает ли компьютер: ролик в 60 кадров в секунду получается ровным, даже если тяжёлый кадр рендерится 100 мс.

godot --path game -s res://tools/short_story.gd --fixed-fps 60 --write-movie story/friday.avi

Вертикальное окно получили через override.cfg. Если рядом с проектом лежит этот файл, Godot при старте берёт из него настройки поверх project.godot. Скрипт кладёт файл, запускает запись и удаляет его:

[display]

window/size/viewport_width=1080
window/size/viewport_height=1920
window/size/window_width_override=540
window/size/window_height_override=960
window/stretch/mode="viewport"

Окно на экране 540×960, а кадр пишется 1080×1920. Удалять файл нужно обязательно в finally: однажды он остался в папке после упавшей записи, и следующая сборка игры сломалась на копировании проекта. Теперь сборщик демо этот файл пропускает, а скрипт записи отказывается стартовать, если override.cfg уже лежит рядом и он его не создавал.

Movie Maker пишет AVI с MJPEG, файлы большие: 30 секунд записи весят около 250 МБ. Это только исходник, дальше всё пережимается.

Постановка сцены

short_story.gd запускается как скрипт SceneTree (ключ -s). Он начинает партию с заранее подготовленной компанией, расставляет людей, переводит игровое время и ставит камеру. История описана списком кадров: id, длительность и время на подготовку. Подготовка нужна, чтобы сцена успокоилась: подгрузились модели, люди сели за столы, свет пересчитался. Потом это время вырезается.

Рядом с видео скрипт пишет JSON с чистыми отрезками:

[[2.5, 6.9, "day"], [7.7, 11.5, "cat_walk"], [11.9, 21.3, "cat_kb"], [22.1, 25.9, "team"], [26.7, 30.5, "founder"]]

Монтаж дальше работает только с этими кусками и ничего не знает о подготовке.

Чтобы истории можно было ставить, игре пришлось научиться кое-чему по команде: усадить кота на клавиатуру основателя, включить красный свет при сбое, задержать команду в офисе до ночи. Сохранения и настройки игрока при записи не трогаются: партия пишется в отдельную тестовую папку, оформление меняется только в памяти.

Сценарий как данные

Ролик описывается «битами». Бит это один кадр и то, что в нём происходит: фразы закадрового голоса, подпись, уведомления на экране телефона, звуки. Примерно так:

'meeting': {
    'folder': '115_sozvon_na_pyat_minut',
    'hook': 'Созвонnна 5 минут',
    'beats': [
        B('story/friday:calm', 'Созвон на пять минут, сказал менеджер.'),
        B('story/deadline:eve', 'Прошёл час.', cap=('16:00', 'mid'), pad=1.0),
        B('story/deadline:late', 'Обсудили, когда обсудим.', cap=('19:00', 'mid')),
        B('story/deadline:cat_sleep', 'Байт ушёл спать ещё на первой минуте.', grade='dusk'),
    ],
    'music': ('lofi_19.ogg', 3.0, 0.32),
}

Длительность кадра не задаётся руками, а считается от длины голоса: вступление, фразы с паузой 0,7 с между ними и 0,75 с после. Перед финальной карточкой пауза дольше, 1,3 с, иначе переход получался резким. Если записи кадра не хватает под голос, монтаж сначала сдвигает начало куска, а если и этого мало, замедляет кадр, но не больше чем до 0,7×.

Голос

Голос синтезируется локально: Silero TTS v5 для русского языка, PyTorch на процессоре. Паузы и интонацию задаём SSML: <break time="100ms"/>, <prosody pitch="high">. Каждая фраза кэшируется в WAV, имя файла это md5 от текста, голоса и скорости. Повторная сборка ролика с теми же фразами ничего не синтезирует заново, а правка одной фразы пересчитывает только её. На фразу уходит около секунды.

После синтеза голос проходит через фильтры ffmpeg: шумодав, деэссер, лёгкий эквалайзер и короткое эхо, чтобы он звучал не так стерильно.

Монтаж

Каждый кусок вырезается отдельной командой ffmpeg с наездом камеры. Наезд задаётся выражением, которое считается на каждом кадре:

fps=30,scale=1080:1920,
scale=w='trunc(iw*(1+(1.08-1)*t/2.4)/2)*2':h='trunc(ih*(1+(1.08-1)*t/2.4)/2)*2':eval=frame,
crop=1080:1920:x='(iw-1080)/2':y='(ih-1920)/2'

На ударных моментах вместо плавного наезда стоит резкий за 0,12 с и пара толчков камеры, которые тоже собираются из выражений с min и max от времени. Цветовые настроения («вечер», «тревога») это обычные eq, colorbalance и vignette.

Подписи рисует PIL: белый жирный текст с обводкой, он читается на любом фоне. Каждая подпись это отдельный PNG, который накладывается overlay с enable='between(t,a,b)'. Сообщения рабочего чата и уведомления на телефоне рисуются так же. Всё это собирается в один filter_complex_script: в командную строку он не помещается.

Есть безопасная зона. Сверху около 250 пикселей закрывает строка площадки, снизу около 420 пикселей занимают подпись, кнопки и описание. Всё важное держим между ними.

Музыка режется кусками под настроение ролика: спокойно, тревога, снова спокойно. Звуки игры взяты из бесплатных наборов Kenney (CC0).

Обложка и тексты

В конце скрипт берёт кадр из ролика, кладёт на него крупный заголовок и сохраняет обложку. Рядом пишет название, описание, закреплённый комментарий и хештеги в отдельные файлы. В ссылке на сайт стоят метки с номером ролика, чтобы потом было видно, какой именно ролик привёл людей.

Что получилось

  • 64 сюжетных ролика. Запись одной истории занимает пару минут, монтаж ролика около минуты.

  • Ролик можно пересобрать после любого изменения в игре. Когда мы поменяли маскота и финальную карточку, все ролики пересобрали одной командой.

  • Первая сотня роликов была собрана генератором по шаблону: одни и те же планы, одинаковые подписи. Смотрелось скучно. Сейчас сценарий каждой истории пишет человек, а машина делает только монтаж.

  • Площадки не дают загрузить через наш инструмент файл больше 10 МБ, поэтому для выкладки ролики дополнительно пережимаются двухпроходным x264 примерно до 9 МБ.

Если интересно посмотреть, что получается в итоге: игра называется Котопорация, у неё есть бесплатное демо (granikogame.ru). Код конвейера могу показать подробнее, если будут вопросы в комментариях.

source

Мировые новости

Меню