Мы небольшая студия, делаем экономическую стратегию про 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=1080window/size/viewport_height=1920window/size/window_width_override=540window/size/window_height_override=960window/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: , . Каждая фраза кэшируется в 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). Код конвейера могу показать подробнее, если будут вопросы в комментариях.
Источник: habr.com