Все видео, которыми мы рекламируем AI Budżet, сделаны без единого стокового кадра. В них нет After Effects, Remotion, Lottie и модели генерации изображений. Каждый пиксель — либо настоящий скриншот приложения, либо нарисован кодом: на Python или на JavaScript в Canvas 2D. Самые интересные из них — тридцатисекундные анимационные фильмы с собственной, полностью синтезированной музыкой — собирают агенты Claude Code: один субагент на файл сцены, затем волны критиков, которые измеряют пиксели. Ниже — вся кухня с технической стороны: три конвейера, контракт сцены, гейт детерминизма, 6 миллисекунд, которые надо вернуть компрессору, и аргументы ffmpeg, с которыми файл проходит через Instagram и TikTok.
AI Budżet (ai-budget.pl) — приложение для личного и семейного бюджета с ИИ: голосовой ввод расходов (Whisper), OCR (Optical Character Recognition) превращает скан или фото документа в текст, который компьютер может прочитать, — первый шаг к тому, чтобы счёт из письма попадал в систему сам. Полное определение → чеков, импорт из банка, общие семейные счета, кошелёк поездки с разделением счетов и сумма «можно потратить безопасно». Оно работает offline-first со сквозным шифрованием, на девяти языках. Стек — Turborepo с NestJS и Prisma, Expo/React Native для телефона и веба и админка на Next.js. Об ИИ-архитектуре мы писали отдельно. Важная для рекламы деталь: приложение есть на Android и в браузере, версии для iOS нет — поэтому правила креативов прямо запрещают показывать App Store и iPhone.
Почему код, а не монтажка и сток
Первая причина — воспроизводимость. Интерфейс приложения меняется, и реклама, смонтированная в редакторе, стареет вместе с ним. Скрипт, который собирает ролик из скриншотов, достаточно перезапустить на свежих скриншотах. Фильм, нарисованный кодом, рендерится с нуля одной командой, кадр в кадр одинаково, — за этим следит отдельный гейт, о нём ниже.
Вторая причина — локализация. Приложение говорит на девяти языках, а кампании мы ведём как минимум на польском и английском. В видеоредакторе каждая языковая версия — ручная работа. У нас текст — это данные: словарь подписей на каждый язык в Python-скрипте или заменённые строки и валюта в копии проекта фильма.
Третья причина — правдивость. Реклама финансового приложения не может обещать функцию, которой нет, или показывать счёт, который не сходится. Когда креатив — это код в том же репозитории, что и продукт, его можно проверить так же, как код: по вики продукта, по цифрам, по диффу.
Четвёртая — контроль версий. Каждое изменение рекламы — коммит, который можно просмотреть, откатить и сравнить. Сами готовые видеофайлы агенты при этом в коммиты не кладут — об этом в разделе о потоке кампаний.
Три конвейера и когда какой
В репозитории ai-budget-assistant живут три независимых способа делать видео. Они различаются входом, инструментами и тем, сколько стоит одна штука.
| A: скриншоты | B: тур по функциям | C: процедурный фильм | |
|---|---|---|---|
| Вход | Настоящие скриншоты телефона или браузера плюс списки PHASES и PILLS | Сырая запись анимированного тура по функциям внутри самого приложения | Бриф, вики продукта, файл темы приложения |
| Инструменты | Python, Pillow 12, imageio, numpy, imageio-ffmpeg | Python, numpy, ffmpeg через пайп | JavaScript (Canvas 2D), Web Audio, Playwright с headless Chromium, ffmpeg, агенты Claude Code |
| Результат | MP4 9:16 или 4:5 в 30 fps и GIF шириной 600 px, без звука | MP4 9:16 длиной 19 с с тихой стереодорожкой AAC | 30 с, 1080×1920, 24 fps, синтезированная музыка, версии PL и EN |
| Когда используем | Кампания одной фичи, где нужно показать настоящие экраны; около 77 скриптов на около 37 кампаний | Один ролик, который проходит по нескольким функциям в настоящем интерфейсе | Реклама-история с одной идеей, где сообщение несут движение и музыка |
Конвейер A — фабрика: дёшев в использовании, хорош для кампании одной фичи. Конвейер B — один ролик из записи настоящего интерфейса. Конвейер C — ремесло: тридцать секунд анимации, за которыми стоит больше десяти тысяч строк кода сцен. Описываем их в этом порядке, но больше всего места отдаём третьему.
Конвейер A: ролики из скриншотов на Python
В каталоге docs/marketing/scripts/ лежит около 77 скриптов вида build_<фича>_reel.py, которые покрывают примерно 37 кампаний: бюджеты, цели, разделение счетов, поездки, карты, инфляцию, ИИ-чат, подписки и другие. Зависимости скромные: Pillow 12, imageio, numpy и imageio-ffmpeg.
Сцена — это данные, а не код. Скрипт получает список настоящих скриншотов телефона или браузера и два списка: PHASES — надзаголовок и заголовок для каждой сцены — и PILLS, то есть теги фич. Новая кампания на практике — это новые скриншоты и новые подписи, а не новая анимация.
Фон считается один раз, до цикла кадров: вертикальный градиент от (44, 28, 15) к (11, 11, 14), размытые по Гауссу оранжевые свечения, рамка телефона или окно браузера, иконка приложения, значок бренда и адрес. На каждый кадр остаётся только то, что движется: скриншот сдвигается внутри маски скруглённого прямоугольника, а движением управляет кривая smoothstep, t · t · (3 − 2t). У неё нулевая скорость на обоих концах, поэтому сдвиг не дёргается.
- Фрагмент кода (Python, конвейер A)
- Что делает
def ease(t): return t*t*(3 - 2*t)- Smoothstep: нулевая скорость в начале и в конце сдвига, без рывка линейного движения
content = Image.new("RGBA", (VW, VH), (22, 22, 27, 255))- Тёмный «экран» размером с дисплей телефона
content.alpha_composite(shots[scene], (0, int(-e*VH)))- Текущий скриншот уезжает вверх на долю e высоты экрана
if off > 0: content.alpha_composite(shots[nxt], (0, int((1-e)*VH)))- Во время перехода следующий скриншот въезжает снизу
content.putalpha(mask)- Маска скруглённого прямоугольника обрезает оба скриншота по форме экрана
frame.alpha_composite(content, (VX, VY))- Наложение на фон, посчитанный один раз до цикла
paste_alpha(frame, tops[pc], max(0.0, 1 - 2*e))- Старый заголовок гаснет в первой половине перехода
paste_alpha(frame, tops[pn], max(0.0, 2*e - 1))- Новый заголовок проявляется во второй половине, поэтому тексты никогда не накладываются
Самая неочевидная деталь — заголовки. При классическом кроссфейде старая подпись гаснет, а новая проявляется одновременно, так что в середине перехода обе полупрозрачны и сливаются в нечитаемую дымку. Поэтому переход последовательный: старый заголовок исчезает в первой половине перехода, новый появляется во второй. В ролике для сайта адрес в адресной строке браузера переключается ровно в середине перехода, чтобы совпадать с тем, что показывает окно.
Ритм фиксированный: 30 кадров в секунду, 3,8–4,0 с на каждый экран и переходы по 0,5–0,8 с. Скрипт вызывается так: python build_<имя>_reel.py [gif|mp4|both] [9:16|4:5]. Формат 9:16 — это 1080×1920. Формат 4:5 — 1080×1344, и это число стоит пояснить. Обе стороны кратны 8, поэтому кодеку не нужно ничего масштабировать, а пропорция остаётся внутри диапазона от 4:5 до 16:9, который принимает Instagram.
Кодирование идёт в два этапа. imageio пишет файл через libx264, затем ffmpeg перекодирует его с параметрами -profile:v main -level 4.0 -pix_fmt yuv420p -movflags +faststart -crf 18. Профиль Main уровня 4.0 с yuv420p — самый широко поддерживаемый вариант H.264, а faststart переносит индекс файла в начало, чтобы воспроизведение стартовало до полной загрузки. Рядом с MP4 пишется GIF шириной 600 px. Звука в роликах нет: музыку выбирают уже в приложении Instagram или TikTok.
Языковые версии — это словари COPY на каждый язык и отдельная папка рендеров на каждый язык. Одно замечание о приватности, выученное на ошибках: персональные данные на скриншотах мы обрезаем, а не размываем. Размытие пробовали, и оно не сработало, поэтому теперь этих фрагментов просто нет в кадре.
Конвейер B: ролик из настоящего тура по функциям
У приложения есть собственный анимированный тур по функциям с промо-режимом: интерфейс без навигационных элементов, 6 с на слайд. Вход конвейера B — сырая запись экрана этого тура, а скрипт build_feature_tour_reel.py превращает её в готовый ролик. Он добавляет вступительную карточку (1,2 с), кроссфейд с замедлением (0,4 с) в запись и в конце 0,7 с перехода в карточку с призывом к действию, которая держится 3,5 с. Оверлей с брендом и адресом он смешивает по альфа-каналу в numpy, а пустые кадры в начале записи пропускает, проверяя цвет одного пикселя.
Два инженерных решения здесь важнее всего остального. Во-первых, кадры стримятся, а не держатся в памяти: около 830 кадров Full HD — это около 5 ГБ RAM. Скрипт отдаёт сырой RGB прямо в ffmpeg через пайп. Во-вторых, файл получает тихую стереодорожку AAC из фильтра anullsrc. Многие плееры на телефонах и встроенные просмотрщики в приложениях отказываются от MP4 без аудиопотока или показывают вместо него чёрный кадр. Ролик длится 19 с, соблюдает безопасные зоны (ничего в верхних 190 px и ниже y = 1540, где Instagram и TikTok кладут свой интерфейс) и существует в польской версии для Instagram и английской для TikTok.
Конвейер C: процедурные фильмы — обзор
Третий конвейер — тридцатисекундные рекламные фильмы, целиком нарисованные на JavaScript: Canvas 2D, без фреймворка и без единого графического или звукового файла. Музыка синтезируется в Web Audio. У инструментов на Node одна зависимость — playwright, то есть headless Chromium, — плюс системный ffmpeg.
Фильмы собирают агенты Claude Code через скилл проекта .claude/skills/ad-film. Этот скилл добавляет бриф бренда AI Budżet поверх открытого скилла procedural-film под лицензией MIT, который даёт сам метод и инструменты. Мы внесли в него исправление для Windows: адреса file:// строятся через pathToFileURL, чтобы Playwright загружал локальные страницы и на Windows.
Пока готово пять фильмов, каждый на польском и английском: receipt-journey («Paragon, który liczy się sam» — чек, который сам раскладывается по категориям), kto-placi (кто платит, то есть разделение счёта), oddam-pietnastego (долги), real-salary и shared-family. Параметры общие: 12–14 планов, 30 с, 120 bpm, 24 fps, 1080×1920. Код сцен — 8–15 тысяч строк на фильм. В receipt-journey 13 623 строки в 14 файлах сцен по 700–1 400 строк каждый.
Процесс состоит из десяти шагов. Бриф: фича, идея, около 30 с, 9:16. Исследование: каждый факт привязан к вики продукта и пользовательской документации, чтобы фильм никогда не обещал функцию, которой нет. Арт-библия: цвета бренда прямо из файла темы приложения — brand #E37F2B, brandDeep #B85F16, brandPale #FBE3CC и brandGlow #FFBA60. Раскадровка на сетке битов, с таблицами общей геометрии для монтажных склеек «по совпадению» и безопасной зоной x 60–940, y 220–1540. Дальше таймлайн как данные, заглушки с гейтом и черновик в половинном масштабе, сцены, музыка, волны критиков и выдача.
Таймлайн как данные и контракт сцены
Сердце фильма — файл timeline.js. В нём темп, длина и формат, список планов и плоский список звуковых событий — в receipt-journey их 46. Каждый план начинается и заканчивается на бите, имеет режим (иллюстрированный «бумажный» лист или схематичный тёмно-синий чертёж) и бриф — описание в одно-два предложения того, что должно произойти. Для плана today-coin бриф звучит примерно так: подпись «Bezpieczne wydatki / na dziś» («можно потратить / сегодня»), сегодняшний столбец превращается в монету с суммой 77 zł, которая приземляется на втором бите и медленно поворачивается.
Сцена — объект с функцией draw(ctx, t, info). Параметр t — секунды от начала плана, а info несёт его длительность, глобальное время T, номер кадра, размеры кадра и вспомогательную библиотеку. Внутри сцены биты — именованные константы с комментарием, который связывает их с глобальным таймлайном, так что любой, кто читает файл, видит, к какому удару музыки привязано движение.
- Фрагмент кода (JavaScript, конвейер C)
- Смысл
FILM.TIMELINE = { title: 'Paragon, który liczy się sam', bpm: 120, duration: 30, fps: 24, width: 1080, height: 1920, shots: […], cues: […] }- Весь фильм как данные: темп, длина, формат, список планов и список звуковых событий
{ id: 'today-coin', file: '11-today-coin.js', start: 20, end: 23, mode: 'illustrated', title: 'Safe to spend today', brief: '…' }- Один план: начинается и заканчивается на бите, имеет режим (иллюстрация или схема) и бриф для автора сцены
{ t: 20.5, kind: 'hit', note: 'The coin: bright FM bell, tock and noise burst ka-ching, sub hit' }- Одно из 46 звуковых событий в receipt-journey; музыка и картинка читают одни и те же часы
FILM.scene({ id: 'egg-blueprint', draw(ctx, t, info) { … } })- Контракт сцены; id обязан совпадать с планом из FILM.TIMELINE.shots
t, info: { dur, p, T, frame, W, H, lib, shot }- t — секунды от начала плана (0 … info.dur); в info длительность, глобальное время, номер кадра, размеры и вспомогательная библиотека
const B_LAND = 0.5; // T 20.5 beat 2: the coin lands, yellow ring bursts- Биты как именованные константы: в комментарии глобальное время и номер бита
const GLINTS = [1.0, 1.5, 2.0, 2.5]; // a glint every beat- При 120 bpm бит длится 0,5 с, так что блик на каждый бит — это раз в полсекунды
const tw = L.onTwos(t);- Время, квантованное до 1/12 с, — анимация «на двойках»
const LAT = 0.006;- Компенсация 6 мс задержки компрессора в звуковой цепочке
Этот контракт намеренно узкий. Сцена ничего не знает о других сценах, не управляет временем и не хранит состояние между кадрами. Она получает время и рисует. Благодаря этому кадры можно рендерить в любом порядке, параллельно в нескольких браузерах, и воспроизвести любой кадр через неделю.
Гейт детерминизма
Узкий контракт ничего не стоит, если его никто не проверяет. Это делает скрипт check.cjs, который выполняет шесть проверок: скан медиа, детерминизм, скан исходников, таймлайн, отрисовка и стоимость кадра. С инженерной точки зрения интереснее всего первые две.
Детерминизм проверяется грубо. Первый, средний и последний кадр каждого плана рендерятся пятью способами, и их хеши сравниваются: на прогретой странице вперёд, на прогретой странице в обратном порядке, на свежей странице в перемешанном порядке вперемешку с кадрами-приманками, «на холодную» и последовательно. Если сцена держит скрытое состояние — счётчик, Кэш — это повторное использование неизменной части запроса: провайдеры моделей берут за неё меньше, поэтому продуманный промпт может стоить в несколько раз дешевле. Полное определение →, зависящий от порядка вызовов, анимацию, запущенную «при первом вызове», — один из пяти хешей разойдётся.
Скан исходников отклоняет всё, что вносит в кадр недетерминированный вход: Math.random, Date, performance.now и случайность из crypto. Вместо этого библиотека даёт генератор с сидом lib.rng(seed) и функцию lib.hash. Функция draw(ctx, t, info) не хранит состояния между кадрами, а кэш разрешён, только если он — чистая функция своих входов.
Рисованный характер из кода
Векторная графика, которая заново считается в каждом кадре, обычно выглядит слишком гладкой и слишком «цифровой». Два простых механизма приближают её к рисованной вручную анимации. Первый — часы «boil»: lib.boil(T, 12) меняет сид дрожания линий 12 раз в секунду, и контуры слегка «кипят», будто их перерисовывают кадр за кадром. Второй — onTwos(t), который квантует движение до 1/12 с, как в классической анимации «на двойках», где один рисунок держится два кадра при 24 fps.
К этому добавляется бумажное зерно на иллюстрированных планах и мелкий шум на схематичных, плюс небольшой набор кривых движения: inOutCubic, outBack, outExpo и inOutSine. Все эти эффекты — функции времени и сида, поэтому гейт детерминизма пропускает их без исключений.
Синтезированная музыка и 6 миллисекунд
music.js не загружает ни одного сэмпла. Вся дорожка строится из осцилляторов, периодических волн (PeriodicWave), буферов шума, сгенерированных из сида, свёрточных ревербераторов на сгенерированных импульсных откликах, пинг-понг-дилея, склеивающего компрессора и мягкого лимитера. Монета из плана today-coin, например, — это яркий FM-колокольчик, стук, шумовое «ка-чинг» и удар саба, описанные одной записью в списке событий.
Лучшая история в этом файле — про компрессор. DynamicsCompressorNode в Web Audio задерживает выход на фиксированные 6 мс look-ahead — мы намерили 288 сэмплов при 48 кГц. Без поправки каждый удар выходил из микса на 6 мс позже своего бита. Решение простое: всё, что идёт до компрессора, планируется на константу LAT = 0.006 раньше, поэтому покидает мастер ровно на своём событии. Опоздать может только событие в первых 6 мс окна рендера. Инструмент анализа проверяет, что начала событий попадают в записанное время с точностью до 10 мс.
Одна и та же функция рендера обслуживает два режима. Офлайн мы рендерим через OfflineAudioContext, который приостанавливается за 0,25 с до каждого такта, чтобы его запланировать. Вживую, в браузере, планированием управляет таймер с упреждением 1,5 с. Поскольку код общий, то, что слышно в превью, и попадает в файл.
Агенты: один файл сцены — один субагент
Код сцен пишут агенты Claude Code. После шага с заглушками — каждый план существует как пустая сцена, которая проходит гейт, а черновик в половинном масштабе показывает ритм целого — сцены получают субагенты, по одному на файл, работающие параллельно. Правило одно, и оно абсолютное: владение файлом — закон. Субагент редактирует только свой файл сцены. Поэтому один план можно отрендерить и посмотреть, пока соседние файлы ещё написаны наполовину.
Настоящий интерфейс приложения в этом конвейере не записывается. Он перерисован кодом: в cast.js лежат uiChat, uiHome, uiDebt, uiSplit и другие компоненты, которые используют настоящие польские строки интерфейса приложения. Экран в фильме выглядит как приложение, но является частью иллюстрации и может анимироваться вместе с ней.
Волны критиков и что они на самом деле нашли
Когда сцены готовы, приходят критики — снова субагенты. Они оценивают контактные листы кадров и измеряют пиксели в numpy. Проверяют границы подписей и безопасную зону, контраст по WCAG, обе стороны каждой склейки по совпадению, польские диакритические знаки и арифметику на экране. Находки получают приоритет P1, P2 или P3 и исправляются волнами, а после каждой волны критики смотрят снова.
Три реальные находки из receipt-journey хорошо показывают, что они ищут. Первая: подписи вылезали за x = 940, потому что арт-библия давала подписям ширину 860 px, что противоречило безопасной зоне. Предел снизили до 800 px. Вторая: блик на монете примерно 35% времени её показа выбеливал надпись «77 zł» до контраста 2,1:1. Исправление: обрезаем блик вокруг цифр и добавляем обводку тушью в 3 px, что поднимает контраст выше 7:1. Третья — проверка арифметики на экране: 240 = 180 + 35 + 25 и 912 − 65 = 847 = 11 × 77. Сумма на сегодня обязана следовать из чисел, показанных раньше.
Кодирование и выдача под платформы
Рендер устроен так, чтобы ошибка всплывала как можно раньше. Сначала рендерится звук, поэтому сломанная дорожка упадёт до того, как уйдут минуты на рендер кадров. Параллельные воркеры работают каждый со своим браузером, PNG-кадры идут в ffmpeg через image2pipe, а предыдущий экспорт заменяется только тогда, когда новое кодирование завершилось целиком. Прерванный рендер не оставляет битый файл на месте хорошего.
Самая важная часть аргументов ffmpeg — преобразование цвета. Canvas рисует в полном диапазоне RGB, а видео должно жить в ограниченном диапазоне BT.709. Поэтому фильтр масштабирования явно говорит scale=in_range=full:out_range=tv:out_color_matrix=bt709 с флагами lanczos+accurate_rnd+full_chroma_int, затем format=yuv420p, а в файл пишутся метаданные -colorspace bt709, -color_primaries bt709, -color_trc bt709 и -color_range tv. Без этого один и тот же оранжевый #E37F2B выглядел бы по-разному в каждом плеере. Звук — -c:a aac -b:a 192k при 48 кГц в стерео, а -movflags +faststart снова переносит индекс в начало файла.
| Версия | Формат | Видео | Звук |
|---|---|---|---|
| A — ролик 9:16 | 1080×1920, 30 fps | H.264 Main@4.0, yuv420p, CRF 18, faststart | Нет — музыку выбирают в приложении Instagram или TikTok |
| A — ролик 4:5 | 1080×1344, 30 fps | Как выше | Нет |
| A — GIF | Ширина 600 px | GIF | — |
| B — ролик из тура по функциям | 1080×1920, fps как в записи | H.264 Main@4.0, yuv420p, CRF 18, preset slow, faststart | Тихая стереодорожка AAC (anullsrc, 44,1 кГц) |
| C — мастер фильма | 1080×1920, 24 fps | H.264, CRF 16, yuv420p, BT.709; 34–43 Мбит/с, 120–155 МБ на 30 с | AAC 192 кбит/с, 48 кГц, стерео; сырая дорожка около −16,2 LUFS |
| C — версия для телефона | 720×1280, 24 fps | H.264, CRF 23 | AAC |
| C — версия для соцсетей | 1080×1920, 24 fps | H.264, около 19 Мбит/с | AAC после loudnorm I=−14, TP=−1,5, LRA=11; от −13,1 до −14 LUFS |
Из одного фильма получается комплект: мастер (libx264, CRF 16, yuv420p BT.709, 24 fps, AAC 192k при 48 кГц), версия для телефона 720×1280 с CRF 23 и версия для соцсетей 1080×1920 с нормализацией громкости loudnorm=I=-14:TP=-1.5:LRA=11. Сырая дорожка — около −16,2 LUFS, выдаваемые файлы — от −13,1 до −14 LUFS. Мастеры весят 120–155 МБ на 30 с (34–43 Мбит/с), версия для соцсетей — около 19 Мбит/с. Сверху — самодостаточный HTML-плеер, список планов, статичный постер для Stories (Pillow, со стикером-ссылкой на Google Play) и рекламные тексты.
Локализация, не трогая анимацию
В конвейере A локализация — это словарь COPY на каждый язык и отдельная папка рендеров. В конвейере C английский фильм — копия проекта с нетронутыми таймингом, геометрией, музыкой и анимацией. Меняются только строки и валюта — злотые на евро, с десятичной точкой вместо запятой, — плюс рисунок монет. В английской версии фильма о разделении счёта появляется биметаллическая монета в 1 €.
Копия вместо переключателя языка в одном проекте даёт простое преимущество: перевод не может случайно сдвинуть событие на бит, а каждая версия отдельно проходит тот же гейт и тех же критиков. Более длинная английская подпись, которая не влезает в 800 px, — просто новая находка.
От идеи до кампании
Фильмы не начинаются с чистого листа. Агенты сканируют репозиторий и предлагают от 3 до 7 идей кампаний, каждую с проверяемым доказательством — коммитом, файлом или числом. Идеи попадают на внутреннюю доску идей, где агенты предлагают кампании с доказательствами, а человек их утверждает. После утверждения агент собирает креатив, запускает команду проверки и отчитывается о созданных файлах, названных по шаблону <имя>-<формат>-<локаль>.<расширение>. Сами креативы агенты никогда не коммитят.
Честно о стоимости: один фильм съедает большую долю плана использования. Поэтому скилл ведёт файл HANDOFF.md, который позволяет остановленной сессии продолжить с того места, где она встала, а не начинать заново. Результатов рекламы мы здесь не приводим — этот текст о мастерской, а не об эффективности кампаний.
Уроки и подводные камни
Тихая аудиодорожка. MP4 без аудиопотока плееры на телефонах и встроенные просмотрщики иногда отклоняют или показывают чёрный кадр. Дорожка из anullsrc ничего не стоит и снимает проблему целиком.
Размеры, кратные 8. 1080×1344 вместо «красивых» 1080×1350: кодек не масштабирует, а пропорция остаётся в диапазоне Instagram от 4:5 до 16:9.
Обрезать, а не размывать. Персональные данные на скриншотах мы не размываем, а вырезаем из кадра. Размытие пробовали, и оно не сработало.
Безопасные зоны в пикселях. Верхние 190 px и всё ниже y = 1540 принадлежат интерфейсу Instagram и TikTok. В фильмах безопасная зона — x 60–940 и y 220–1540, и критики измеряют её на отрендеренных кадрах, а не верят раскадровке.
Сначала звук. Дорожка рендерится раньше кадров, поэтому сломанная музыка стоит секунды, а не минуты. Готовый экспорт заменяет предыдущий только после полного кодирования.
Стримить кадры. 830 кадров Full HD в памяти — это около 5 ГБ. Пайп в ffmpeg превращает это в небольшое постоянное потребление RAM.
Подписи по очереди, а не одновременно. Последовательная смена заголовков дешевле любого эффекта и убирает нечитаемую дымку двух наложенных текстов.
Реклама, которая является кодом, обладает теми же достоинствами, что и код: её можно просмотреть, воспроизвести, перевести, не трогая анимацию, и проверить, говорит ли она правду. Попробовать AI Budżet можно на ai-budget.pl или в Google Play, а код приложения лежит на GitHub. Если вам нужен похожий видеоконвейер для вашего продукта — от роликов из скриншотов до процедурных фильмов, которые собирают агенты, — напишите нам: development@mi-code.pl.
Частые вопросы
- Можно ли сделать видеорекламу приложения без After Effects и стоковых материалов?
- Да. В рекламе AI Budżet каждый пиксель — либо настоящий скриншот, либо нарисован кодом. Ролики из скриншотов собираются на Python (Pillow, imageio, numpy, ffmpeg), а 30-секундные фильмы рисуются на JavaScript в Canvas 2D с музыкой, синтезированной в Web Audio, и рендерятся покадрово через Playwright и ffmpeg.
- Как добиться, чтобы анимация на Canvas рендерилась детерминированно, кадр в кадр?
- Сцена получает только время и рисует: draw(ctx, t, info) не хранит состояние между кадрами, а кэш допускается только как чистая функция входов. Случайность берётся из генератора с сидом. Гейт check.cjs отклоняет в коде Math.random, Date, performance.now и случайность из crypto, а первый, средний и последний кадр каждого плана рендерит пятью способами и сравнивает хеши.
- Зачем добавлять тихую аудиодорожку в MP4 без музыки?
- Многие плееры на телефонах и встроенные просмотрщики отклоняют MP4 без аудиопотока или показывают вместо него чёрный кадр. Тихую стереодорожку AAC мы добавляем в ffmpeg фильтром anullsrc с опцией -shortest. Исключение — ролики, музыку к которым выбирают в Instagram или TikTok.
- Какое разрешение выбрать для видео 4:5 в Instagram?
- Мы используем 1080×1344 вместо 1080×1350. Обе стороны кратны 8, поэтому кодек H.264 ничего не масштабирует, а пропорция остаётся в диапазоне от 4:5 до 16:9, который принимает Instagram. Кодируем в профиле Main 4.0, yuv420p, CRF 18 с +faststart.
- Как синхронизировать музыку из Web Audio с кадрами видео?
- Музыка и картинка читают один и тот же таймлайн со списком событий. Офлайн мы рендерим через OfflineAudioContext, который приостанавливается за 0,25 с до каждого такта, чтобы его запланировать. DynamicsCompressorNode задерживает сигнал на 6 мс (288 сэмплов при 48 кГц), поэтому всё, что идёт до компрессора, планируется на 6 мс раньше. Инструмент анализа проверяет, что события попадают в своё время с точностью до 10 мс.