Wszystkie wideo, którymi reklamujemy AI Budżet, powstają bez ani jednej klatki ze stocku. Nie ma w nich After Effects, Remotion, Lottie ani modelu generującego obrazy. Każdy piksel jest albo prawdziwym zrzutem ekranu aplikacji, albo został narysowany przez kod — w Pythonie lub w JavaScripcie na Canvas 2D. Najciekawsze z tych filmów, trzydziestosekundowe animacje z własną, w całości syntetyzowaną muzyką, budują agenci Claude Code: jeden subagent na plik sceny, potem fale krytyków, którzy mierzą piksele. Poniżej opisujemy od strony technicznej całą kuchnię: trzy potoki, kontrakt sceny, bramkę determinizmu, 6 milisekund, które trzeba oddać kompresorowi, i parametry ffmpeg, z którymi plik przechodzi przez Instagram i TikTok.

AI Budżet (ai-budget.pl) to aplikacja do budżetu osobistego i rodzinnego z AI: wydatki wpisywane głosem (Whisper), OCR (Optical Character Recognition) zamienia skan albo zdjęcie dokumentu w tekst, który komputer potrafi odczytać — pierwszy krok, żeby faktura z e-maila trafiała do systemu sama. Pełna definicja → paragonów, import z banku, wspólne konta rodzinne, portfel podróży z dzieleniem rachunków i kwota „bezpieczne do wydania”. Działa offline-first z szyfrowaniem end-to-end, w dziewięciu językach. Stos to Turborepo z NestJS i Prismą, Expo/React Native na telefon i web oraz panel administracyjny w Next.js. O architekturze AI pisaliśmy osobno. Ważny dla reklam szczegół: aplikacja jest na Androida i w przeglądarce, nie ma wersji na iOS — więc reguły kreacji wprost zabraniają pokazywania App Store czy iPhone’a.

Dlaczego kod, a nie montażownia i stock

Pierwszy powód to powtarzalność. Interfejs aplikacji się zmienia, a reklama nagrana w edytorze starzeje się razem z nim. Skrypt, który składa rolkę ze zrzutów ekranu, wystarczy uruchomić ponownie na nowych zrzutach. Film narysowany w kodzie renderuje się od zera jednym poleceniem, klatka w klatkę tak samo — dbamy o to osobną bramką, o której niżej.

Drugi powód to lokalizacja. Aplikacja mówi w dziewięciu językach, a kampanie prowadzimy co najmniej po polsku i angielsku. W edytorze wideo każda wersja językowa to ręczna praca. U nas tekst jest daną: słownik z napisami dla każdego języka w skrypcie Pythona albo podmienione ciągi i waluta w kopii projektu filmu.

Trzeci powód to prawdziwość. Reklama aplikacji finansowej nie może obiecać funkcji, której nie ma, ani pokazać rachunku, który się nie sumuje. Kiedy kreacja jest kodem w tym samym repozytorium co produkt, można ją sprawdzić tak jak kod: z wiki produktu, z liczbami, z diffem.

Czwarty to kontrola wersji. Każda zmiana w reklamie jest commitem, który da się przejrzeć, cofnąć i porównać. Same gotowe pliki wideo agenci zostawiają jednak poza commitami — o tym w części o przepływie kampanii.

Trzy potoki i kiedy który

W repozytorium ai-budget-assistant żyją trzy niezależne sposoby robienia wideo. Różnią się wejściem, narzędziami i tym, ile wysiłku kosztuje jedna sztuka.

A: zrzuty ekranuB: prezentacja funkcjiC: film proceduralny
WejściePrawdziwe zrzuty z telefonu lub przeglądarki plus listy PHASES i PILLSSurowe nagranie animowanej prezentacji funkcji w samej aplikacjiBrief, wiki produktu, plik motywu aplikacji
NarzędziaPython, Pillow 12, imageio, numpy, imageio-ffmpegPython, numpy, ffmpeg przez potokJavaScript (Canvas 2D), Web Audio, Playwright z Chromium bez okna, ffmpeg, agenci Claude Code
WynikMP4 9:16 lub 4:5 w 30 fps i GIF o szerokości 600 px, bez dźwiękuMP4 9:16 o długości 19 s z cichą ścieżką stereo AAC30 s, 1080×1920, 24 fps, syntetyzowana muzyka, wersje PL i EN
Kiedy go używamyKampania jednej funkcji, która ma pokazać prawdziwe ekrany; ok. 77 skryptów na ok. 37 kampaniiJedna rolka, która przechodzi przez kilka funkcji w prawdziwym interfejsieReklama-opowieść z jedną puentą, w której ruch i muzyka niosą przekaz

Potok A to fabryka: tani w użyciu, dobry na kampanię jednej funkcji. Potok B to jedna rolka z nagrania prawdziwego interfejsu. Potok C to rzemiosło — trzydzieści sekund animacji, za którymi stoi kilkanaście tysięcy linii kodu scen. Opisujemy je w tej kolejności, ale najwięcej miejsca poświęcamy trzeciemu.

Potok A: rolki ze zrzutów ekranu w Pythonie

Katalog docs/marketing/scripts/ zawiera około 77 skryptów o nazwach build_<funkcja>_reel.py, które obsługują mniej więcej 37 kampanii: budżety, cele, dzielenie rachunków, podróże, mapy, inflację, czat z AI, subskrypcje i inne. Zależności są skromne: Pillow 12, imageio, numpy i imageio-ffmpeg.

Scena jest daną, nie kodem. Skrypt dostaje listę prawdziwych zrzutów z telefonu lub przeglądarki oraz dwie listy: PHASES — nadtytuł i nagłówek dla każdej sceny — oraz PILLS, czyli etykiety funkcji. Nowa kampania to w praktyce nowe zrzuty i nowe napisy, a nie nowa animacja.

Tło liczymy raz, przed pętlą klatek: pionowy gradient od (44, 28, 15) do (11, 11, 14), rozmyte filtrem Gaussa pomarańczowe poświaty, ramka telefonu albo okno przeglądarki, ikona aplikacji, znaczek marki i adres. Na każdą klatkę zostaje tylko to, co się rusza: zrzut przesuwa się wewnątrz maski prostokąta z zaokrąglonymi rogami, a ruch prowadzi krzywa smoothstep, czyli t · t · (3 − 2t). Ma zerową prędkość na obu końcach, więc przejazd nie szarpie.

Fragment kodu (Python, potok A)
Co robi
def ease(t): return t*t*(3 - 2*t)
Smoothstep: zerowa prędkość na początku i końcu przejazdu, bez szarpnięcia ruchu liniowego
content = Image.new("RGBA", (VW, VH), (22, 22, 27, 255))
Ciemny „ekran” o rozmiarze wyświetlacza telefonu
content.alpha_composite(shots[scene], (0, int(-e*VH)))
Bieżący zrzut wyjeżdża w górę o ułamek e wysokości ekranu
if off > 0: content.alpha_composite(shots[nxt], (0, int((1-e)*VH)))
W trakcie przejścia następny zrzut wjeżdża od dołu
content.putalpha(mask)
Maska prostokąta z zaokrąglonymi rogami przycina oba zrzuty do kształtu ekranu
frame.alpha_composite(content, (VX, VY))
Złożenie na tło policzone raz przed pętlą
paste_alpha(frame, tops[pc], max(0.0, 1 - 2*e))
Stary nagłówek gaśnie w pierwszej połowie przejścia
paste_alpha(frame, tops[pn], max(0.0, 2*e - 1))
Nowy nagłówek pojawia się w drugiej połowie, więc teksty nigdy się nie nakładają

Najmniej oczywisty szczegół to nagłówki. Przy klasycznym przenikaniu stary napis gaśnie, a nowy pojawia się w tym samym czasie, więc w połowie przejścia oba są półprzezroczyste i nakładają się w nieczytelną mgłę. Dlatego przenikanie jest sekwencyjne: stary nagłówek znika w pierwszej połowie przejścia, nowy pojawia się w drugiej. W rolce strony internetowej adres w pasku przeglądarki przełącza się dokładnie w połowie przejścia, więc zgadza się z tym, co widać w oknie.

Rytm jest stały: 30 klatek na sekundę, 3,8–4,0 s na każdy ekran i przejścia trwające 0,5–0,8 s. Skrypt wywołuje się tak: python build_<nazwa>_reel.py [gif|mp4|both] [9:16|4:5]. Format 9:16 to 1080×1920. Format 4:5 to 1080×1344 — liczba, którą warto wyjaśnić. Oba wymiary są wielokrotnością 8, więc koder nie musi niczego skalować, a proporcja mieści się w zakresie od 4:5 do 16:9, który przyjmuje Instagram.

Kodowanie odbywa się dwuetapowo. imageio zapisuje plik przez libx264, a potem ffmpeg koduje go ponownie z parametrami -profile:v main -level 4.0 -pix_fmt yuv420p -movflags +faststart -crf 18. Profil Main na poziomie 4.0 z yuv420p to najszerzej obsługiwany wariant H.264, a faststart przenosi indeks pliku na początek, żeby odtwarzanie ruszało przed pobraniem całości. Obok MP4 powstaje GIF o szerokości 600 px. Rolki nie mają dźwięku: muzykę wybiera się już w aplikacji Instagrama lub TikToka.

Wersje językowe to słowniki COPY dla każdego języka i osobny katalog renderów na każdy język. Jedna uwaga o prywatności, której nauczyliśmy się na błędach: dane osobowe na zrzutach przycinamy, a nie rozmywamy. Rozmycie próbowaliśmy i się nie sprawdziło, więc teraz tych fragmentów po prostu nie ma w kadrze.

Potok B: rolka z prawdziwej prezentacji funkcji

Aplikacja ma własną animowaną prezentację funkcji z trybem promocyjnym: interfejs bez elementów nawigacji, 6 s na slajd. Wejściem potoku B jest surowe nagranie ekranu tej prezentacji, a skrypt build_feature_tour_reel.py zamienia je w gotową rolkę. Dodaje kartę otwierającą (1,2 s), przenikanie z wygaszeniem (0,4 s) do nagrania, a na końcu 0,7 s przejścia do planszy z wezwaniem do działania, która trwa 3,5 s. Nakładkę z marką i adresem miesza kanałem alfa w numpy, a puste klatki na początku nagrania pomija, sprawdzając kolor jednego piksela.

Dwie decyzje inżynierskie są tu ważniejsze niż cała reszta. Po pierwsze, klatki są strumieniowane, a nie trzymane w pamięci: około 830 klatek Full HD to około 5 GB RAM. Skrypt przepuszcza surowe RGB prosto do ffmpeg przez potok. Po drugie, plik dostaje cichą ścieżkę stereo AAC z filtra anullsrc. Wiele odtwarzaczy na telefonach i podglądów w aplikacjach odrzuca MP4 bez strumienia audio albo pokazuje w jego miejscu czarną klatkę. Rolka ma 19 s, uwzględnia strefy bezpieczne (nic w górnych 190 px ani poniżej y = 1540, gdzie Instagram i TikTok kładą swój interfejs) i istnieje w wersji polskiej na Instagram i angielskiej na TikTok.

Potok C: filmy proceduralne — przegląd

Trzeci potok to trzydziestosekundowe filmy reklamowe narysowane w całości w JavaScripcie: Canvas 2D, bez frameworka i bez jednego pliku graficznego czy dźwiękowego. Muzyka jest syntetyzowana w Web Audio. Narzędzia w Node mają jedną zależność — playwright, czyli Chromium bez okna — plus systemowy ffmpeg.

Filmy budują agenci Claude Code przez skill projektu .claude/skills/ad-film. Ten skill dokłada brief marki AI Budżet do otwartego skilla procedural-film na licencji MIT, który dostarcza samą metodę i narzędzia. Od siebie dołożyliśmy do niego poprawkę dla Windows: adresy file:// budowane przez pathToFileURL, dzięki której Playwright ładuje lokalne strony także na Windows.

Do tej pory powstało pięć filmów, każdy po polsku i angielsku: receipt-journey („Paragon, który liczy się sam” — paragon sam rozkłada się na kategorie), kto-placi (kto płaci, czyli dzielenie rachunku), oddam-pietnastego (długi), real-salary oraz shared-family. Parametry są wspólne: 12–14 ujęć, 30 s, 120 bpm, 24 fps, 1080×1920. Kod scen to 8–15 tysięcy linii na film. Receipt-journey ma 13 623 linie w 14 plikach scen po 700–1 400 linii każdy.

Dziesięć kroków od briefu do dostawy; fale krytyków wracają do scen, dopóki zostają poprawki

Proces ma dziesięć kroków. Brief: funkcja, puenta, ok. 30 s, 9:16. Research: każdy fakt przypięty do wiki produktu i dokumentacji użytkownika, żeby film nigdy nie obiecał funkcji, której nie ma. Biblia stylu: kolory marki wprost z pliku motywu aplikacji — brand #E37F2B, brandDeep #B85F16, brandPale #FBE3CC i brandGlow #FFBA60. Storyboard na siatce beatów, z tablicami wspólnej geometrii dla cięć montażowych „na dopasowanie” i strefą bezpieczną x 60–940, y 220–1540. Dalej oś czasu jako dane, zaślepki z bramką i szkic w połowie skali, sceny, muzyka, fale krytyków i dostawa.

Oś czasu jako dane i kontrakt sceny

Sercem filmu jest plik timeline.js. Opisuje tempo, długość i format, listę ujęć oraz płaską listę zdarzeń dźwiękowych — w receipt-journey jest ich 46. Każde ujęcie zaczyna się i kończy na beacie, ma tryb (ilustrowany „papierowy” albo schematyczny granatowy blueprint) i brief, czyli zdaniowy opis tego, co ma się wydarzyć. Dla ujęcia today-coin brief brzmi mniej więcej tak: napis „Bezpieczne wydatki / na dziś”, dzisiejsza kolumna zamienia się w monetę z kwotą 77 zł, która ląduje na drugim beacie i powoli się obraca.

Scena to obiekt z funkcją draw(ctx, t, info). Parametr t to sekundy od początku ujęcia, a info niesie jego długość, czas globalny T, numer klatki, wymiary kadru i bibliotekę pomocniczą. W samej scenie beaty są nazwanymi stałymi z komentarzem, który wiąże je z globalną osią czasu, więc każdy, kto czyta plik, widzi, z którym uderzeniem muzyki zgrany jest ruch.

Fragment kodu (JavaScript, potok C)
Znaczenie
FILM.TIMELINE = { title: 'Paragon, który liczy się sam', bpm: 120, duration: 30, fps: 24, width: 1080, height: 1920, shots: […], cues: […] }
Cały film jako dane: tempo, długość, format, lista ujęć i lista zdarzeń dźwiękowych
{ id: 'today-coin', file: '11-today-coin.js', start: 20, end: 23, mode: 'illustrated', title: 'Safe to spend today', brief: '…' }
Jedno ujęcie: zaczyna się i kończy na beacie, ma tryb (ilustracja lub schemat) i brief dla autora sceny
{ t: 20.5, kind: 'hit', note: 'The coin: bright FM bell, tock and noise burst ka-ching, sub hit' }
Jedno z 46 zdarzeń dźwiękowych w receipt-journey; muzyka i obraz czytają ten sam czas
FILM.scene({ id: 'egg-blueprint', draw(ctx, t, info) { … } })
Kontrakt sceny; id musi odpowiadać ujęciu z FILM.TIMELINE.shots
t, info: { dur, p, T, frame, W, H, lib, shot }
t to sekundy od początku ujęcia (0 … info.dur); info niesie długość, czas globalny, numer klatki, wymiary i bibliotekę pomocniczą
const B_LAND = 0.5; // T 20.5 beat 2: the coin lands, yellow ring bursts
Beaty jako nazwane stałe: komentarz podaje czas globalny i numer beatu
const GLINTS = [1.0, 1.5, 2.0, 2.5]; // a glint every beat
Przy 120 bpm beat trwa 0,5 s, więc błysk co beat to co pół sekundy
const tw = L.onTwos(t);
Czas skwantowany do 1/12 s — animacja „na dwójkach”
const LAT = 0.006;
Kompensacja 6 ms opóźnienia kompresora w ścieżce dźwiękowej

Ta umowa jest celowo wąska. Scena nie wie nic o innych scenach, nie zarządza czasem i nie trzyma stanu między klatkami. Dostaje czas i rysuje. Dzięki temu da się renderować klatki w dowolnej kolejności, równolegle w kilku przeglądarkach i powtórzyć dowolną klatkę po tygodniu.

Bramka determinizmu

Wąski kontrakt nic nie daje, jeśli nikt go nie egzekwuje. Robi to skrypt check.cjs, który wykonuje sześć kontroli: skan mediów, determinizm, skan źródeł, oś czasu, rysowanie i koszt klatki. Dwie pierwsze z punktu widzenia inżyniera są najciekawsze.

Determinizm sprawdzamy brutalnie. Pierwszą, środkową i ostatnią klatkę każdego ujęcia renderujemy pięcioma sposobami i porównujemy skróty: na rozgrzanej stronie w przód, na rozgrzanej stronie wstecz, na świeżej stronie w losowej kolejności wymieszanej z klatkami-wabikami, na zimno i sekwencyjnie. Jeśli scena trzyma ukryty stan — licznik, Cache to ponowne użycie tej samej, powtarzalnej części zapytania — dostawcy modeli liczą za nią mniej, więc dobrze ułożony prompt bywa kilka razy tańszy. Pełna definicja → zależny od kolejności wywołań, animację zaczętą „przy pierwszym wywołaniu” — któryś z pięciu skrótów się rozjedzie.

Skan źródeł odrzuca wszystko, co wprowadza do klatki niedeterministyczne wejście: Math.random, Date, performance.now i losowość z crypto. Zamiast tego biblioteka daje ziarnisty generator lib.rng(seed) i funkcję lib.hash. Funkcja draw(ctx, t, info) jest bezstanowa dla każdej klatki, a cache jest dozwolony tylko wtedy, gdy jest czystą funkcją swoich wejść.

Ręcznie rysowany charakter z kodu

Grafika wektorowa liczona w każdej klatce wygląda zwykle zbyt gładko i zbyt „cyfrowo”. Dwa proste mechanizmy przybliżają ją do animacji rysowanej ręcznie. Pierwszy to zegar „boil”: lib.boil(T, 12) zmienia ziarno drgania linii 12 razy na sekundę, więc kontury lekko falują jak przerysowywane klatka po klatce. Drugi to onTwos(t), który kwantuje ruch do 1/12 s — tak jak w klasycznej animacji rysowanej „na dwójkach”, gdzie jeden rysunek trzyma się przez dwie klatki przy 24 fps.

Do tego ziarno papieru na ujęciach ilustrowanych i drobny szum na schematycznych, plus mały zestaw krzywych ruchu: inOutCubic, outBack, outExpo i inOutSine. Wszystkie te efekty są funkcjami czasu i ziarna, więc bramka determinizmu przepuszcza je bez wyjątków.

Muzyka syntetyzowana i 6 milisekund

Plik music.js nie ładuje ani jednej próbki. Cała ścieżka powstaje z oscylatorów, fal okresowych (PeriodicWave), buforów szumu generowanych z ziarna, pogłosów na splocie z wygenerowanymi odpowiedziami impulsowymi, opóźnienia ping-pong, kompresora sklejającego miks i miękkiego limitera. Moneta z ujęcia today-coin to na przykład jasny dzwonek FM, stuknięcie, szumowe „ka-ching” i uderzenie subbasu, opisane jednym wpisem na liście zdarzeń.

Najlepsza historia z tego pliku dotyczy kompresora. DynamicsCompressorNode w Web Audio opóźnia sygnał o stałe 6 ms look-ahead — zmierzyliśmy 288 próbek przy 48 kHz. Bez korekty każde uderzenie wychodziło z miksu 6 ms po swoim beacie. Rozwiązanie jest proste: wszystko, co idzie przed kompresorem, planujemy o stałą LAT = 0.006 wcześniej, więc opuszcza master dokładnie na swoim zdarzeniu. Spóźnić się może tylko zdarzenie z pierwszych 6 ms okna renderowania. Narzędzie analityczne weryfikuje, że początki zdarzeń trafiają w zapisane czasy z dokładnością do 10 ms.

Ta sama funkcja renderująca obsługuje dwa tryby. Offline renderujemy przez OfflineAudioContext, który zatrzymuje się 0,25 s przed każdym taktem, żeby go zaplanować. Na żywo, w przeglądarce, planowaniem steruje timer z wyprzedzeniem 1,5 s. Ponieważ kod jest wspólny, to, co słychać w podglądzie, jest tym, co trafi do pliku.

Agenci: jeden plik sceny, jeden subagent

Kod scen piszą agenci Claude Code. Po kroku z zaślepkami — każde ujęcie istnieje jako pusta scena, która przechodzi bramkę, a szkic w połowie skali pokazuje rytm całości — sceny dostają subagenci, po jednym na plik, pracujący równolegle. Reguła jest jedna i bezwzględna: własność pliku to prawo. Subagent edytuje tylko swój plik sceny. Dzięki temu jedno ujęcie można wyrenderować i obejrzeć, podczas gdy pliki sąsiednie są jeszcze w połowie napisane.

Prawdziwego interfejsu aplikacji nie nagrywamy w tym potoku. Jest odrysowany w kodzie: plik cast.js zawiera komponenty uiChat, uiHome, uiDebt, uiSplit i inne, używające prawdziwych polskich napisów z interfejsu aplikacji. Ekran w filmie wygląda jak aplikacja, ale jest częścią ilustracji i może się animować razem z nią.

Fale krytyków i co naprawdę znaleźli

Kiedy sceny są gotowe, wchodzą krytycy — znów subagenci. Oceniają arkusze kontaktowe z klatek i mierzą piksele w numpy. Sprawdzają zasięg napisów i strefę bezpieczną, kontrast WCAG, obie strony każdego cięcia na dopasowanie, polskie znaki diakrytyczne i arytmetykę widoczną na ekranie. Znaleziska dostają priorytet P1, P2 lub P3 i są naprawiane falami, a po każdej fali krytycy patrzą jeszcze raz.

Trzy prawdziwe znaleziska z receipt-journey dobrze pokazują, czego szukają. Pierwsze: napisy wychodziły poza x = 940, bo biblia stylu dawała napisom 860 px szerokości, co przeczyło strefie bezpiecznej. Limit zmniejszyliśmy do 800 px. Drugie: błysk na monecie przez około 35% czasu jej ekspozycji wypłukiwał napis „77 zł” do kontrastu 2,1:1. Poprawka: przycinamy błysk wokół cyfr i dodajemy 3-pikselowy obrys tuszem, co podnosi kontrast powyżej 7:1. Trzecie to weryfikacja arytmetyki na ekranie: 240 = 180 + 35 + 25 oraz 912 − 65 = 847 = 11 × 77. Kwota na dziś musi wynikać z liczb pokazanych wcześniej.

Kodowanie i dostarczenie na platformy

Renderowanie jest ułożone tak, żeby błąd wychodził jak najwcześniej. Najpierw renderuje się dźwięk, więc zepsuta ścieżka wyłoży się, zanim pójdą minuty renderowania klatek. Równoległe procesy robocze mają każdy własną przeglądarkę, klatki PNG płyną do ffmpeg przez image2pipe, a poprzedni eksport jest zastępowany dopiero wtedy, gdy nowe kodowanie zakończy się w całości. Przerwany render nie zostawia uszkodzonego pliku w miejscu dobrego.

Najważniejszy fragment argumentów ffmpeg to konwersja koloru. Canvas rysuje w pełnym zakresie RGB, a wideo ma żyć w ograniczonym zakresie BT.709. Dlatego filtr skalowania jawnie mówi scale=in_range=full:out_range=tv:out_color_matrix=bt709 z flagami lanczos+accurate_rnd+full_chroma_int, potem format=yuv420p, a do pliku zapisujemy metadane -colorspace bt709, -color_primaries bt709, -color_trc bt709 i -color_range tv. Bez tego ta sama pomarańczowa #E37F2B wyglądałaby inaczej w każdym odtwarzaczu. Dźwięk to -c:a aac -b:a 192k przy 48 kHz w stereo, a -movflags +faststart znów przenosi indeks na początek pliku.

WersjaFormatObrazDźwięk
A — rolka 9:161080×1920, 30 fpsH.264 Main@4.0, yuv420p, CRF 18, faststartBrak — muzykę wybiera się w aplikacji Instagram lub TikTok
A — rolka 4:51080×1344, 30 fpsJak wyżejBrak
A — GIFSzerokość 600 pxGIF—
B — rolka z prezentacji1080×1920, fps z nagraniaH.264 Main@4.0, yuv420p, CRF 18, preset slow, faststartCicha ścieżka stereo AAC (anullsrc, 44,1 kHz)
C — master filmu1080×1920, 24 fpsH.264, CRF 16, yuv420p, BT.709; 34–43 Mb/s, 120–155 MB na 30 sAAC 192 kb/s, 48 kHz, stereo; surowa ścieżka ok. −16,2 LUFS
C — wersja na telefon720×1280, 24 fpsH.264, CRF 23AAC
C — wersja do social mediów1080×1920, 24 fpsH.264, ok. 19 Mb/sAAC po loudnorm I=−14, TP=−1,5, LRA=11; od −13,1 do −14 LUFS

Z jednego filmu powstaje komplet: master (libx264, CRF 16, yuv420p BT.709, 24 fps, AAC 192k przy 48 kHz), wersja na telefon 720×1280 z CRF 23 i wersja do social mediów 1080×1920 z normalizacją głośności loudnorm=I=-14:TP=-1.5:LRA=11. Surowa ścieżka ma około −16,2 LUFS, a dostarczane pliki od −13,1 do −14 LUFS. Mastery mają 120–155 MB na 30 s (34–43 Mb/s), wersja social około 19 Mb/s. Do tego samodzielny odtwarzacz HTML, lista ujęć, statyczny plakat do Stories (Pillow, z naklejką z linkiem do Google Play) i teksty reklamy.

Lokalizacja bez ruszania animacji

W potoku A lokalizacja to słownik COPY dla każdego języka i osobny katalog renderów. W potoku C angielski film to kopia projektu z nietkniętym timingiem, geometrią, muzyką i animacją. Zmieniają się tylko ciągi znaków i waluta — złote na euro, z kropką dziesiętną zamiast przecinka — oraz grafika monet. W angielskiej wersji filmu o dzieleniu rachunku pojawia się bimetaliczna moneta 1 €.

Kopia zamiast przełącznika języka w jednym projekcie ma prostą zaletę: tłumaczenie nie może przypadkiem przesunąć zdarzenia o beat, a każda wersja przechodzi przez tę samą bramkę i tych samych krytyków osobno. Dłuższy angielski napis, który nie mieści się w 800 px, jest po prostu nowym znaleziskiem.

Od pomysłu do kampanii

Filmy nie zaczynają się od pustej kartki. Agenci skanują repozytorium i proponują od 3 do 7 pomysłów na kampanię, każdy ze sprawdzalnym dowodem — commitem, plikiem albo liczbą. Pomysły trafiają na wewnętrzną tablicę pomysłów, na której agenci zgłaszają kampanie z dowodami, a człowiek je zatwierdza. Po zatwierdzeniu agent buduje kreację, uruchamia polecenie weryfikujące i raportuje wyprodukowane pliki, nazwane według wzoru <nazwa>-<format>-<język>.<rozszerzenie>. Agenci nigdy sami nie commitują kreacji.

Uczciwie o koszcie: jeden film zjada dużą część planu użycia. Dlatego skill prowadzi plik HANDOFF.md, który pozwala przerwanej sesji podjąć pracę w miejscu, w którym stanęła, zamiast zaczynać od nowa. Nie podajemy tu wyników reklam — ten tekst opisuje warsztat, a nie skuteczność kampanii.

Lekcje i pułapki

Cicha ścieżka audio. MP4 bez strumienia dźwięku bywa odrzucany przez odtwarzacze w telefonach i podglądy w aplikacjach albo wyświetla czarną klatkę. Ścieżka z anullsrc nic nie kosztuje i usuwa cały problem.

Wymiary podzielne przez 8. 1080×1344 zamiast „ładnego” 1080×1350: koder nie skaluje, a proporcja zostaje w zakresie Instagrama od 4:5 do 16:9.

Przycinać, nie rozmywać. Danych osobowych na zrzutach nie rozmywamy, tylko je wycinamy z kadru. Rozmycie próbowaliśmy i się nie sprawdziło.

Strefy bezpieczne liczone w pikselach. Górne 190 px i wszystko poniżej y = 1540 należy do interfejsu Instagrama i TikToka. W filmach strefa to x 60–940 i y 220–1540, a krytycy mierzą ją na klatkach, zamiast wierzyć storyboardowi.

Najpierw dźwięk. Ścieżka renderuje się przed klatkami, więc zepsuta muzyka kosztuje sekundy, a nie minuty. Gotowy eksport zastępuje poprzedni dopiero po pełnym kodowaniu.

Strumieniować klatki. 830 klatek Full HD w pamięci to około 5 GB. Potok do ffmpeg zamienia ten problem w stałe, małe zużycie RAM.

Napisy po kolei, nie jednocześnie. Sekwencyjne przenikanie nagłówków jest tańsze niż jakikolwiek efekt i usuwa nieczytelną mgłę dwóch nakładających się tekstów.

Reklama, która jest kodem, ma te same zalety co kod: da się ją przejrzeć, powtórzyć, przetłumaczyć bez ruszania animacji i sprawdzić, czy mówi prawdę. AI Budżet możesz wypróbować na ai-budget.pl albo w Google Play, a kod aplikacji jest na GitHubie. Jeśli chcesz podobny potok wideo dla swojego produktu — od zrzutów ekranu po filmy proceduralne budowane przez agentów — napisz do nas: development@mi-code.pl.

Najczęstsze pytania

Czy da się zrobić reklamę wideo aplikacji bez After Effects i materiałów stockowych?
Tak. W reklamach AI Budżet każdy piksel jest prawdziwym zrzutem ekranu albo rysunkiem z kodu. Rolki ze zrzutów składamy w Pythonie (Pillow, imageio, numpy, ffmpeg), a 30-sekundowe filmy rysujemy w JavaScripcie na Canvas 2D z muzyką syntetyzowaną w Web Audio i renderujemy klatka po klatce przez Playwright i ffmpeg.
Jak zapewnić, że animacja na Canvas renderuje się deterministycznie, klatka w klatkę?
Scena dostaje tylko czas i rysuje: funkcja draw(ctx, t, info) nie trzyma stanu między klatkami, a cache jest dozwolony wyłącznie jako czysta funkcja wejść. Losowość pochodzi z generatora z ziarnem. Bramka check.cjs odrzuca w kodzie Math.random, Date, performance.now i losowość z crypto, a pierwszą, środkową i ostatnią klatkę każdego ujęcia renderuje pięcioma sposobami i porównuje skróty.
Dlaczego dodawać cichą ścieżkę dźwiękową do MP4 bez muzyki?
Wiele odtwarzaczy w telefonach i podglądów w aplikacjach odrzuca MP4 bez strumienia audio albo pokazuje w jego miejscu czarną klatkę. Cichą ścieżkę stereo AAC dodajemy w ffmpeg filtrem anullsrc z opcją -shortest. Wyjątkiem są rolki, do których muzykę wybiera się w Instagramie lub TikToku.
Jaką rozdzielczość wybrać dla wideo 4:5 na Instagram?
Używamy 1080×1344 zamiast 1080×1350. Oba wymiary są wielokrotnością 8, więc koder H.264 niczego nie skaluje, a proporcja mieści się w zakresie od 4:5 do 16:9, który przyjmuje Instagram. Kodujemy w profilu Main 4.0, yuv420p, CRF 18 z +faststart.
Jak zsynchronizować muzykę z Web Audio z klatkami wideo?
Muzyka i obraz czytają tę samą oś czasu z listą zdarzeń. Offline renderujemy przez OfflineAudioContext, który zatrzymuje się 0,25 s przed każdym taktem, żeby go zaplanować. DynamicsCompressorNode opóźnia sygnał o 6 ms (288 próbek przy 48 kHz), więc wszystko przed kompresorem planujemy o 6 ms wcześniej. Narzędzie analityczne sprawdza, że zdarzenia trafiają w swój czas z dokładnością do 10 ms.