목차 · 9개
이 글에는 제휴 링크가 없습니다. 영상·프롬프트·렌더 코드는 2026년 10월 16일 이 글을 위해 새로 만들었고, 길이·크기·렌더 시간은 Windows 11에서 직접 측정했습니다. 화면 속 숫자는 새벽 자동화의 실제 실행 기록(10월 15·16일)입니다. 로봇 그림은 AI 이미지 생성, 영상·도식은 직접 제작했습니다.
프로젝트 소개 모션그래픽, 프롬프트부터 렌더까지 직접 만들기

프로젝트 소개 모션그래픽 — 코드로 만드는 모션그래픽 2편 (캐릭터는 AI 이미지 생성)
1편에서는 SNS에 도는 모션그래픽 프롬프트 팩이 사실은 코드로 옮길 사양서라는 걸 풀었어요. 샘플 영상을 보고 나면 이런 생각이 들죠. “좋은데, 내 프로젝트도 이렇게 소개할 수 있을까?”
프로젝트 소개 영상은 영화 예고편과 비슷해요. 줄거리를 다 말하지 않고, 장면 몇 개와 제목 한 장으로 무엇인지 보여 줘요. 예고편에 나오는 장면만 진짜라면, 찍는 기술은 다른 영화에서 빌려 와도 돼요.
프롬프트 틀은 그대로 두고, 장면과 숫자만 내 프로젝트의 실제 기록으로 바꾸면 돼요.

작은 극장에서 깔때기·문서·체크 표시가 나오는 예고편을 보는 로봇 — 프로젝트 소개 영상은 예고편 (AI 이미지 생성)
① 실제 프로젝트를 소개한 12초 모션그래픽과 프롬프트 전문
② 프롬프트 → 코드 → 미리보기 → 렌더, 직접 해 본 과정과 시간
③ 내 프로젝트로 바꿔 만들 때 바꿀 것과 지킬 것
직접 해 보려면: 코딩 에이전트 · 파이썬(pycairo·numpy·scipy·fontTools) · ffmpeg
2026년 10월 16일 기준 · Windows 11에서 직접 렌더 · 연재 2편
1. 완성된 12초, 먼저 보세요
저희 새벽 콘텐츠 자동화를 소개하는 12초 인트로예요. 글자·도형·숫자·소리까지 모두 코드가 만들었어요. 찾고(FIND.), 쓰고(WRITE.), 내보낸 뒤(SHIP.), 이름(CONTENT FACTORY.)에서 멈춰요. 화면 속 숫자는 모두 실제 실행 기록이에요.
프로젝트 소개 모션그래픽 — 새벽 콘텐츠 자동화를 네 장면으로, 소리를 켜고 보세요 (직접 제작, 로봇은 AI 이미지 생성)
완성 파일을 직접 재 봤어요. 12.00초, 1920×1080, 30fps에 360장이고, 영상은 H.264, 소리는 AAC 스테레오예요. 파일 크기는 2.78MB(2,781,230바이트)예요. 같은 화질 기준(CRF 16)인 1편 샘플 영상(3.16MB)보다 조금 작아요.
2. 소개할 프로젝트, 새벽 콘텐츠 공장
저희 나들이 블로그의 축제·대회 안내 글은 사람이 자는 새벽 3시부터 7시 사이에 자동으로 관리돼요. 저희는 보통 아침 7시부터 새벽 3시까지 개발해요. 그래서 자동화는 그 뒤 네 시간에 몰아 돌려요.
일은 네 단계예요. 코드가 할 수 있는 일은 코드가 하고, AI는 일이 있을 때만 불러요.

달이 뜬 창가에서 깔때기로 받은 점을 문서로 바꾸는 로봇 — 새벽 콘텐츠 공장 (AI 이미지 생성)
① 지켜보기: 이미 쓴 글의 공식 출처를 코드가 읽고, 날짜·장소 같은 사실 줄만 비교해요. 10월 15일에는 하루에 1,613곳을 읽었어요.
② 찾기: 관광공사 축제 목록, 구글 뉴스 RSS, 마라톤 일정 모음에서 새 행사·대회를 찾아요. 첫 실행에서 1,827건을 받았고, 겹치는 것을 빼니 후보가 392건이었어요.
③ 쓰기: 후보 한 건마다 AI 세션을 새로 열어 글을 써요. 하루 상한은 12달러예요. 10월 16일 새벽에는 98분 동안 새 글 6편을 썼고, 비용은 약 9.8달러(어림)였어요.
④ 내보내기: 작업 중인 변경이 없을 때만 사이트에 배포해요. 네이버 글은 06:00 예약 큐로 나가요.

새벽 콘텐츠 자동화 네 단계 — 지켜보기·찾기는 코드, 쓰기는 AI가 일이 있을 때만, 내보내기는 조건부 (직접 제작)
AI를 화면 없이(헤드리스) 부를 때 딸려 가는 기본 맥락도 줄였어요. 쓰는 도구만 남기고 MCP를 빼는 식으로 줄였더니, 같은 ‘안녕’ 한마디가 0.26달러에서 0.06달러가 됐어요. 새벽 배포가 ‘작업 중인 변경이 없을 때만’인 것도 이유가 있어요. 낮에 고치던 코드가 새벽에 저절로 공개되면 안 되니까요.
3. 장면마다 진짜 숫자 하나씩
장면 하나에 프로젝트 단계 하나, 그리고 실제 숫자 하나를 넣었어요. 1편 샘플의 틀(왼쪽 큰 글씨 두 줄, 오른쪽 그래픽, 마지막 타이틀)은 그대로 두고, 오른쪽 그래픽과 둘째 줄만 프로젝트에 맞게 바꿨어요.
| 장면 | 화면에 보이는 것 |
|---|---|
01 FIND. | 출처 세 곳에서 떨어진 점이 깔때기로 모이고, 숫자가 1,827까지 오른 뒤 392로 내려가요 |
02 WRITE. | 문서 카드 6장이 한 장씩 떨어질 때마다 0 POSTS가 6 POSTS까지 올라가요 |
03 SHIP. | 시계가 03:00에서 07:00으로 돌고, 체크리스트 세 줄에 차례로 체크가 붙어요 |
04 CONTENT FACTORY. | 로봇이 얼굴 클로즈업에서 전신으로 빠지며 제목이 뜨고, 10.4초부터 멈춰요 |

01 FIND. — 관광공사 축제 목록·뉴스 RSS·마라톤 일정에서 받은 1,827건이 겹침을 빼고 392건으로 (직접 제작)

02 WRITE. — 후보 한 건마다 새 세션, 새벽 98분 동안 새 글 6편 (직접 제작)

03 SHIP. — 03:00에서 07:00까지, 출처 1,613곳 확인·조건부 배포·네이버 06:00 (직접 제작)

04 CONTENT FACTORY. — SECOND SYNTAX 로봇이 전신으로 빠지며 끝나는 타이틀 (직접 제작, 로봇은 AI 이미지 생성)
화면 아래 가로선 밑에는 그 숫자의 출처를 작은 글씨로 한 줄씩 적었어요. 이름하여 근거 줄이에요. 소개 영상에서 큰 숫자가 지나가면 보는 사람은 “이거 진짜야?” 하고 묻게 되거든요. 숫자를 크게 보여 줄수록 출처는 작게라도 꼭 남겨 두세요.
4. 프롬프트는 1편 틀 그대로
1편 프롬프트의 다섯 칸(출력 사양·디자인 시스템·캐릭터 처리·장면별·사운드)은 그대로 두고 내용만 바꿨어요. 색은 SECOND SYNTAX 브랜드 키트의 색 값, 캐릭터는 저희 로봇, 숫자는 실행 기록이에요. 프롬프트는 4,972자, 96줄이에요.
📋 프롬프트 원문 — 그대로 복사해 쓰세요
[첨부: SECOND SYNTAX 로봇 캐릭터 'tools' 포즈 (663×663, 투명 배경 스티커, 렌치·톱니를 든 전신)]
첨부한 이미지는 우리 브랜드 SECOND SYNTAX의 로봇 캐릭터야.
이 캐릭터를 넣어서, 내가 만든 '새벽 콘텐츠 자동화'를 소개하는 12초 모션그래픽 인트로를 MP4로 만들어줘.
화면 속 숫자는 아래에 적은 실제 값만 쓰고, 다른 숫자는 지어내지 마.
■ 출력 사양
- 12초, 1920×1080, 30fps(360프레임), H.264(yuv420p, CRF 16) + AAC 스테레오 192kbps
- 구현: Python + pycairo로 프레임을 렌더해 ffmpeg 파이프로 인코딩, 사운드는 numpy/scipy로 직접 합성
- 글꼴: Poppins Bold·SemiBold. google/fonts 저장소에서 OFL 파일을 받아 프로젝트 폴더에 두고, 시스템에는 설치하지 말 것.
cairo가 파일 글꼴을 직접 못 읽으면 fontTools로 글리프 윤곽을 꺼내 경로로 그릴 것(커닝 포함)
- 렌더 전에 주요 시점 프레임을 미리보기로 뽑아 검수할 것
■ 디자인 시스템 (SECOND SYNTAX 브랜드 키트 토큰)
- 색: 밤 남색 #241B45(어두운 장면 배경), 라벤더 #F6F3FD(밝은 장면 배경), 크림 #FEF9E4(어두운 장면 글자),
잉크 #1F1A33(밝은 장면 글자), 민트 #B3E1CE(강조 1), 민트 잉크 #1F6B55(밝은 배경 위 강조), 남색 #3D2F63(강조 2),
피치 #FEC4A7(로봇 볼 색, 강조 3), 연보라 #DDD3F7(보조 면), 카드 남색 #33285C(어두운 장면의 카드)
- 폰트: 대형 타이포 Poppins Bold 210px, 자간 -0.02em, 왼쪽 x=140 정렬. 1행 단어 베이스라인 y=430,
2행은 카운터 숫자 180px 베이스라인 y=665, 그 아래 라벨 26px(자간 0.18em) 베이스라인 y=735
- 문장 끝 마침표는 강조색(어두운 장면 민트, 밝은 장면 민트 잉크)
- 공통 요소: 좌상단 '0N / 04', 우상단 'SECOND SYNTAX'(22px, 자간 0.18em), y=970에 25% 불투명 가로선,
가로선 아래 y=1018에 '근거 줄'(18px, 자간 0.12em, 55% 불투명) — 그 장면 숫자의 출처를 적는다
- 글자 등장: 글자마다 베이스라인 아래 가려진 영역에서 솟아오르는 마스크 리빌(35ms 시차, 0.55초, ease-out-back 오버슈트)
- 카운터: 0부터 목표값까지 ease-out-expo로 오른다. 숫자 한 자리마다 고정폭 칸에 둬서 자릿수가 바뀌어도 흔들리지 않게, 천 단위 쉼표 포함.
목표값에 닿는 순간 6% 탄성 펄스
- 움직임은 ease-out-elastic, ease-out-back 위주의 탄력 있는 이징
- 장면 전환: 2.5/5.5/8.5초를 중심으로 ±0.32초 동안 사선 와이프(기울기 420px). 선두에 색 띠(민트→피치→민트)가 지나가고 새 장면이 띠 뒤를 따라 드러남
■ 로봇 캐릭터 처리
- 브랜드 키트 사본(투명 배경 + 흰 외곽선 스티커) 그대로 쓴다. 색·모양은 고치지 않는다
- 클로즈업용으로 4배 Lanczos 업스케일 + 언샤프 마스크 버전을 따로 만들고, 확대 배율이 1을 넘으면 4배 소스, 그 이하면 원본 소스를 사용
- 카메라 기준점(원본 663px 좌표): 얼굴 (332,255), 렌치 (95,345), 톱니 (540,400), 전체 (331,331)
- 카메라 배율은 '원본 1px당 화면 px'. 줌은 로그 공간에서 보간
■ 0–2.5초 'FIND.' (밤 남색 배경)
- 'FIND.' 크림(0.15초 시작), 마침표 민트
- 오른쪽 y=250에 출처 알약 3개 'TOURAPI' / 'NEWS RSS' / 'MARATHON'(중심 x=1260/1500/1740, 연보라 테두리, 크림 글자 22px)이 0.2초부터 0.1초 간격으로 탄성 팝
- 깔때기: 입구 y=370 폭 640(중심 x=1500) → 목 y=640 폭 70 → 출구 관 y=730까지. 크림 선폭 10, 둥근 끝.
0.25–0.85초에 위에서 아래로 선을 따라 그려짐
- 점 90개(민트·피치·연보라·크림, 반지름 7–12)가 알약에서 떨어져 깔때기로 빨려 들어가고, 안에서 모이며,
5개 중 1개만 목을 지나 아래로 빠져나와 민트 점이 되어 출구 아래에 한 줄로 쌓임. 0.3초부터 장면 끝까지 계속 흐름
- 카운터(실제 값): 0.55–1.15초 0 → 1,827(크림), 라벨 'ITEMS FETCHED'.
1.35–1.95초 1,827 → 392로 내려가며 색이 민트로 바뀌고, 라벨이 아래에서 위로 넘어가 'NEW AFTER DE-DUPLICATION'. 1.95초 탄성 펄스
- 근거 줄: 'FIRST RUN 10/15 · TOURAPI FESTIVALS + GOOGLE NEWS RSS + MARATHON CALENDAR'
■ 2.5–5.5초 'WRITE.' (라벤더 배경)
- 'WRITE.' 잉크, 마침표 민트 잉크
- 오른쪽 문서 카드 6장: 흰 카드 300×380, 모서리 22, 잉크 8% 그림자. 카드 안에 'POST 0N' 민트 알약, 남색 제목 막대,
연보라 본문 줄 4개, 피치 그림 칸
- 0.35초부터 0.3초 간격으로 카드가 위(-500px)에서 떨어져 ease-out-back으로 착지. 착지 자리 (1400+28i, 600-22i), 회전 -7/5/-4/6/-2/0도.
착지마다 카드 오른쪽 위에 민트 체크 배지가 탄성 팝
- 카드가 떨어지기 직전, 착지 자리 위에 작은 민트 고리(반지름 26)가 0.2초 돌고 사라짐 — '후보 하나에 새 세션 하나'
- 카드 뒤에 반지름 360 연보라 점선 고리가 천천히 회전, 남색 삼각형 포인트 회전
- 카운터(실제 값): 착지할 때마다 한 칸씩 0 → 6, '6 POSTS'(숫자 남색, POSTS 잉크). 라벨 'IN 98 MIN — ONLY WHEN THERE'S WORK'
- 근거 줄: 'NIGHT OF 10/16 · ONE CANDIDATE PER FRESH AI SESSION · ≈ $9.8 EST. · CAP $12/DAY'
■ 5.5–8.5초 'SHIP.' (밤 남색 → 새벽)
- 'SHIP.' 크림, 마침표 민트
- 배경: 작은 크림 별 40개가 반짝이다 2초 동안 사라지고, 오른쪽 아래에서 피치 빛(반지름 900 원형 그라데이션, 최대 35%)이 떠오름 = 새벽
- 카운터: 시계 '03:00' → '07:00'(0.35–2.1초, ease-in-out, 분 자리가 빠르게 돎), 민트. 라벨 'WHILE THE OWNER SLEEPS'
- 오른쪽 체크리스트 카드(카드 남색, 700×470, 모서리 28, 중심 1470,560): 머리 'NIGHTLY RUN' + 초승달 아이콘, 줄 3개
· '1,613 SOURCE PAGES CHECKED'
· 'SITE DEPLOY — ONLY IF CLEAN'
· 'NAVER QUEUE — 06:00'
0.7/1.15/1.6초에 줄마다 빈 원 → 체크 선이 그려지고 원이 민트로 채워짐(탄성). 줄 글자 크림 28px
- 근거 줄: '1,613 OFFICIAL PAGES READ ON 10/15 · FACT LINES ONLY · HEADLESS BASELINE $0.26 → $0.06'
■ 8.5–12초 'CONTENT FACTORY.' 타이틀 (라벤더 배경)
- 로봇 얼굴이 화면을 가득 채운 4.0배 클로즈업으로 시작해 0.35초간 8% 푸시인
- 0.35–1.05초 ease-out-back으로 전신까지 풀백(살짝 지나쳤다 돌아옴). 최종 배율 1.15, 중심 (1480,575)
- 잉크 16% 드롭섀도(16,8)
- 풀백이 끝날 무렵 뒤로 민트 원판(반지름 300, 중심 1480,545)이 탄성 등장하고, 광선 16개(남색/피치/남색/민트 반복, 길이 40/64)가 회전하며 펼쳐져 고정
- 'NIGHT SHIFT' 남색 알약 태그(크림 글자)가 (1770,240)에 -8° 기울기로 탄성 팝
- 왼쪽 공장 심볼(바닥 y=400): 몸통 사각형 + 톱니 지붕 3개 + 굴뚝 + 연기 점 3개. 순차적으로 탄성 성장, 연기 점은 위로 떠오르다 멈춤
- 'CONTENT' / 'FACTORY.'(긴 줄을 폭 940px에 맞춤, 베이스라인 600/770): 민트 바가 훑으며 덮었다 걷히는 동안 글자 마스크 리빌, 마침표 민트 잉크
- 부제 'WHILE YOU SLEEP — SECOND SYNTAX'(26px) + 남색 밑줄 120px
- 모든 움직임은 10.4초에 멈추고, 마지막 1.5초 이상 완전히 정지해 타이틀을 읽을 수 있게 유지
■ 사운드 (120 BPM, 2.5/5.5/8.5/10초가 모두 박자에 맞음)
- 리듬
· 킥: 0–9.5초 매 박자(42Hz로 떨어지는 피치 사인 + 클릭)
· 클랩: 2.5초부터 2·4박
· 하이햇: 1초부터 8분 오프비트, 5.5초부터 16분 + 오픈햇
- 베이스: 2초 마디마다 루트 A-F-C-G-A, 8분 오프비트, 쏘우파 + 420Hz 로우패스. 6초부터 옥타브 위 음 추가
- 플럭 아르페지오: A 마이너 펜타토닉 16분음, 2.5–10초, 좌우로 번갈아 배치
- 장면 연출 효과음
· 0.2/0.3/0.4초 알약 팝, 깔때기를 지나는 점마다 작은 물방울 틱
· 카운터가 오를 때 숫자 틱(처음엔 빽빽하고 점점 넓어짐), 목표값 도착 때 '딩'
· 카드 착지마다 낮은 '툭' + 배지 팝
· 체크 3번 '딩'(A5 → C#6 → E6로 한 음씩 올라감)
· 각 전환마다 0.5초 휘시 + 큰 킥 + 오픈햇
- 클라이맥스
· 8.5–10초 라이저, 9–10초 스네어 롤
· 9.55초 카메라가 멈출 때 '보잉', 9.78초 태그 팝
· 10초 임팩트(큰 킥 + 48Hz 서브 + 크래시) + A 마이너 패드(110/220/261.63/329.63/440Hz)가 끝까지 감쇠
- 마스터링: 마지막 0.3초 페이드아웃, tanh 소프트 클리핑, 피크 -1dB
원문 그대로예요(4,972자, 96줄). 대괄호 없이 실제 값을 넣었지만, 출력 사양·이징·전환·사운드 칸은 다른 프로젝트에 그대로 써도 돼요. 1편 샘플과 달라진 곳만 짝지어 보면 이래요.
| 프롬프트 칸 | 1편 샘플 → 이번 영상 |
|---|---|
| 색 | 샘플의 다섯 색 → 브랜드 키트 색 값 열 가지(밤 남색·라벤더·민트·피치 등) |
| 캐릭터 | 직접 그린 캐릭터 → 저희 로봇(브랜드 키트 사본, 색·모양 그대로) |
| 장면 그래픽 | 궤도·꽃 모양 렌즈 → 깔때기·문서 카드·체크리스트 |
| 새로 넣은 것 | 숫자 카운터, 화면 아래 근거 줄, 숫자를 지어내지 말라는 한 줄 |
5. 코드로 옮긴 것
프롬프트를 코딩 에이전트(클로드 코드)에게 그대로 주고 구현까지 맡겼어요. 나온 코드는 파이썬 1,230줄이에요. 크게 다섯 덩어리예요.
| 코드 | 하는 일 |
|---|---|
scene1~4(t) | 장면마다 함수 하나. t는 그 장면 안의 시간이에요 |
| 이징 함수 | ease-out-back·elastic·expo 등, 0~1 진행률을 탄력 있게 바꿔요 |
counter() | 숫자를 고정폭 칸에 두고 ease-out-expo로 올려요 |
draw_frame(T) | 전환 ±0.32초 동안 두 장면을 그리고 사선으로 잘라 이어요 |
build_audio() | 킥·베이스·플럭·휘시를 numpy로 합성해요 |
카운터의 핵심은 세 줄이에요. 진행률을 이징에 넣고, 반올림한 숫자에 천 단위 쉼표를 붙여요.
p = cl((t - 0.55) / 0.6)
n = round(1827 * e_expo(p))
s = f"{n:,}" # "1,827"
효과음 시각은 화면과 같은 상수에서 계산했어요. 카드가 떨어지는 시각을 바꾸면 ‘툭’ 소리도 같이 따라가요.
6. 미리보기 5번, 고친 곳 8군데
전체를 한 번에 뽑지 않고, 중요한 순간의 프레임만 먼저 뽑아 한 장짜리 시트로 봤어요. 미리보기는 5번(프레임 35장), 전체 렌더는 3번 했어요. 고친 곳은 모두 8군데예요.

프롬프트에서 MP4까지 — 프롬프트 4,972자, 코드 1,230줄, 미리보기 5번, 고친 곳 8군데, 마지막 렌더 21.4초 (직접 제작)
첫 미리보기에서 네 군데가 걸렸어요. 숫자 아래 라벨이 바뀌는 동안 두 줄이 겹쳤고, 새벽 빛이 남색 위에서 흙빛으로 탁해졌어요. 로봇 클로즈업 위로 아래쪽 글씨가 겹쳤고, 공장 굴뚝 연기가 맨 위 글줄에 닿았어요.
전체 렌더 뒤에는 완성 파일을 다시 열어 봤어요. 소리 피크가 기준을 넘었고, 글자가 올라오기 전에 바닥선 아래로 살짝 비쳤어요. 둘 다 아래 ‘막힐 때’에 적었어요.
마지막 렌더는 21.4초 걸렸어요. 프레임 360장을 그리는 데 12.3초(한 장에 약 34ms), 소리 합성과 피크 확인에 5.9초가 들었어요. 인텔 i5-14400 데스크톱이고, 그래픽 카드는 쓰지 않았어요. 끝 1.6초가 정말 멈췄는지도 쟀어요. 이웃 프레임끼리 차이가 10.4초 무렵부터 0이었어요.
7. 내 프로젝트로 바꿔 만들기
틀은 그대로 두고 단계·숫자·브랜드 세 가지만 바꾸면 내 프로젝트 소개 영상이 돼요.
- 프로젝트를 단계 세 개로 나누고, 단계마다 실제 숫자를 하나씩 고르세요.→ 장면 세 개와 타이틀 한 장의 뼈대가 생겨요.
- 1편 프롬프트의 색·글꼴·캐릭터 줄을 내 브랜드 값으로 바꾸세요.→ 이징·전환·사운드 줄은 그대로 둬도 돼요.
- 장면이 바뀌는 시각을 박자에 맞춰 두세요(120 BPM이면 0.5초 단위).→ 전환과 효과음이 음악과 함께 떨어져요.
- 에이전트에게 미리보기 시트부터 뽑아 달라고 하세요.→ 전체 렌더 전에 겹침·잘림을 잡을 수 있어요.
❌ 이렇게 말고
우리 서비스 소개 영상 멋지게 만들어줘
무엇을 보여 줄지, 어떤 숫자를 쓸지 정하지 않으면 AI가 그럴듯한 숫자를 지어낼 수 있어요.
⭕ 이렇게
장면 01은 [단계 이름]이야. 숫자는 [실제 값]만 쓰고, 화면 아래에 출처 한 줄을 적어줘. 다른 숫자는 지어내지 마
막힐 때
글꼴을 지정했는데 다른 글꼴로 그려져요
원인: pycairo의 기본 글꼴 기능(toy API)은 설치된 글꼴만 찾고, 없으면 오류 없이 다른 글꼴로 바꿔요. 이번에 Poppins를 줬더니 ‘FIND.’ 폭이 Arial과 똑같이 548.3px로 나왔어요.
해결: 글꼴 파일을 프로젝트 폴더에 두고, fontTools로 글자 윤곽을 꺼내 직접 그리게 하세요. 시스템에 설치하지 않아도 돼요.
화살표(→) 같은 기호가 빠져요
원인: 글꼴에 그 글자가 없어요. 이번에 받은 Poppins 파일에는 →가 없었어요.
해결: 기호를 도형으로 직접 그려 달라고 하거나, 그 글자가 있는 글꼴을 함께 쓰세요.
숫자가 올라갈 때 줄이 좌우로 흔들려요
원인: 숫자마다 폭이 달라요. Poppins Bold의 1은 4보다 폭이 절반 남짓이에요.
해결: 숫자를 가장 넓은 숫자 폭의 고정 칸에 두라고 프롬프트에 적으세요.
소리 피크를 -1dB로 맞췄는데 MP4에서는 0dB가 나와요
원인: AAC로 압축하면 파형이 조금 튀어요. 이번에 WAV는 -1.0dB였는데 MP4에서는 0.0dB, 잘린 샘플이 14개였어요.
해결: 압축했다 풀어 피크를 다시 재고 넘친 만큼 줄이세요. 1.72dB를 더 줄였더니 -1.08dB가 됐어요.
글자가 올라오기 전에 바닥선 아래로 살짝 비쳐요
원인: 가리는 영역을 바닥선보다 아래까지 열어 두면, 솟아오르는 글자 윗부분이 먼저 보여요.
해결: 글자마다 ‘바닥선 + 그 글자가 내려가는 깊이’까지만 보이게 하세요. 쉼표처럼 내려가는 글자만 조금 더 열면 돼요.
② 장면 하나에 실제 숫자 하나, 화면 아래에는 출처 한 줄
③ 미리보기 시트로 먼저 고치면 전체 렌더는 20초 남짓이에요
코드로 만드는 모션그래픽 연재
- 1편 프롬프트 팩과 그 뒤의 기술 (지난 글)
- 2편 내 프로젝트 일부를 소개하는 모션그래픽, 같은 방식으로 만들기 (이 글)
- 3편 직접 만든 영상과 프롬프트로 한 편 더 (준비 중)
자주 묻는 질문
Q. 같은 프롬프트를 주면 똑같은 영상이 나오나요?
색·위치·시간·숫자처럼 프롬프트에 숫자로 적은 부분은 같게 나와요. 숫자로 안 적은 곡선 모양이나 효과음 질감은 에이전트가 짜는 코드에 따라 조금씩 다를 수 있어요. 한 번 만든 코드를 다시 돌리면 그때부터는 같은 영상이 나와요.
Q. 좋은 컴퓨터가 있어야 하나요?
이번에는 일반 데스크톱(인텔 i5-14400)에서 12초 영상을 렌더하는 데 21.4초가 걸렸어요. 그래픽 카드는 쓰지 않았어요.
Q. 숫자가 바뀌면 영상을 다시 만들어야 하나요?
숫자는 코드의 장면 상수와 글자 몇 곳에만 있어서, 값만 바꾸고 다시 렌더하면 돼요. 이번 영상은 한 번 렌더에 20초 남짓이었어요.
Q. 캐릭터가 없으면 못 만드나요?
만들 수 있어요. 마지막 타이틀 장면의 캐릭터 자리에 로고나 제품 화면을 넣어도 같은 구조가 돼요.
1편 — 프롬프트 팩이 무엇이고 코드로 어떻게 옮기는지 → https://happyvarious.com/posts/20261016-ai-code-motion-prompt-pack/
참고 자료
- pycairo 문서 — Surfaces · pycairo.readthedocs.io/en/latest/reference/surfaces.html
- fontTools 문서 — BasePen · fonttools.readthedocs.io/en/latest/pens/basePen.html
- Poppins 글꼴(OFL) · github.com/google/fonts/tree/main/ofl/poppins
영상은 이 글의 프롬프트를 코딩 에이전트가 구현해 Windows 11에서 렌더했어요.
화면 속 숫자는 새벽 자동화의 실행 기록(10/15·16), 길이·크기·시간은 직접 쟀어요.
제휴 링크는 없어요. 로봇 그림은 AI 이미지 생성, 영상·도식은 직접 제작이에요.
렌더 코드 핵심
원본 렌더 코드(파이썬 1,230줄)에서 뽑은 부분이에요. 설명용으로 새로 짠 예시가 아니라 영상을 만든 코드 그대로예요(주석만 줄였어요).
글꼴 파일 → cairo 경로. pycairo의 기본 글꼴 기능은 파일 글꼴을 못 읽어서, fontTools 펜이 글자 윤곽을 cairo 경로 명령으로 옮겨요. TrueType의 2차 곡선은 BasePen이 3차 곡선으로 바꿔 넘겨 줘요.
class _Pen(BasePen):
def __init__(self, gs, ctx):
super().__init__(gs)
self.c = ctx
def _moveTo(self, p):
self.c.move_to(*p)
def _lineTo(self, p):
self.c.line_to(*p)
def _curveToOne(self, a, b, c):
self.c.curve_to(*a, *b, *c)
def _closePath(self):
self.c.close_path()
def glyph(self, ctx, ch, x, y, size): # Font 클래스 안
ctx.save()
ctx.translate(x, y)
ctx.scale(size / self.upem, -size / self.upem) # 글꼴 단위 → px, y축 뒤집기
ctx.append_path(self.path(ch))
ctx.restore()
글자마다 솟아오르는 마스크 리빌. 글자 i는 0.035초씩 늦게 시작하고, 가리는 영역은 ‘바닥선 + 그 글자가 내려가는 깊이’까지만 열어요(3차 수정).
for i, (ch, gx) in enumerate(glyphs):
p = cl((reveal - i * stagger) / dur)
if p <= 0:
continue
dy = (1 - e_back(p)) * size * 1.08
ctx.save()
ctx.rectangle(x + gx - size * 0.3, y - size * 1.3, size * 1.6,
size * 1.3 + font.below(ch) * size / font.upem + 1.5)
ctx.clip()
ctx.new_path()
font.glyph(ctx, ch, x + gx, y + dy, size)
ctx.set_source_rgba(*col, alpha)
ctx.fill()
ctx.restore()
사선 와이프. 전환 ±0.32초 동안 옛 장면을 그리고, 기울어진 다각형으로 자른 영역에 새 장면을 그린 뒤 띠 세 개(민트·피치·민트)를 얹어요.
def draw_frame(ctx, T):
k = max(i for i, (s, _) in enumerate(SCENES) if T >= s - (HW if i else 0))
for ci, c in enumerate(CUTS):
if c - HW <= T < c + HW:
old_s, old_f = SCENES[ci]
new_s, new_f = SCENES[ci + 1]
old_f(ctx, T - old_s)
X = -(SLANT + 3 * BAND) - 10 + (W + 10 + SLANT + 3 * BAND + 10) * e_inout((T - (c - HW)) / (2 * HW))
ctx.save()
slant(ctx, -4000, X)
ctx.clip()
new_f(ctx, T - new_s)
ctx.restore()
for b, col in enumerate((MINT, PEACH, MINT)):
slant(ctx, X + b * BAND, X + (b + 1) * BAND)
ctx.set_source_rgb(*col)
ctx.fill()
return
s, f = SCENES[k]
f(ctx, T - s)
AAC 압축 뒤 피크 맞추기(2차 수정). WAV를 AAC로 압축했다 풀어 피크를 재고, -1dB를 넘으면 그만큼 줄여요.
def aac_peak(wav):
enc = subprocess.run([ffmpeg_exe(), "-loglevel", "error", "-i", str(wav), "-c:a", "aac", "-b:a", "192k", "-f", "adts", "-"],
capture_output=True).stdout
dec = subprocess.run([ffmpeg_exe(), "-loglevel", "error", "-f", "aac", "-i", "-", "-f", "f32le", "-ac", "2", "-ar", str(SR), "-"],
input=enc, capture_output=True).stdout
return 20 * math.log10(np.max(np.abs(np.frombuffer(dec, np.float32))))
pk = aac_peak(path)
if pk > -1.0:
a = a * 10 ** ((-1.0 - pk - 0.05) / 20)
ffmpeg 파이프. cairo의 RGB24 그림은 흔한 PC(리틀 엔디언)에서 메모리에 B·G·R·빈칸 순서로 놓여서 bgr0로 알려 줘요. 소리 WAV는 두 번째 입력으로 같이 넣어 한 번에 MP4를 만들어요.
cmd = [ffmpeg_exe(), "-y", "-loglevel", "error",
"-f", "rawvideo", "-pix_fmt", "bgr0", "-s", f"{W}x{H}", "-framerate", str(FPS), "-i", "-",
"-i", str(wav), "-map", "0:v", "-map", "1:a",
"-c:v", "libx264", "-preset", "slow", "-crf", "16", "-pix_fmt", "yuv420p",
"-c:a", "aac", "-b:a", "192k", "-ar", str(SR), "-movflags", "+faststart", "-shortest", str(OUT_MP4)]
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
for i in range(NFRAMES):
draw_frame(ctx, i / FPS)
surf.flush()
proc.stdin.write(bytes(surf.get_data()))
proc.stdin.close()
proc.wait() 