
Zero to Autopilot, 파트 3: 정지 이미지에 $0.00로 실제 움직임 부여하기
요약
AI 비디오 생성 비용을 절감하기 위해 ffmpeg를 활용하여 정지 이미지에 움직임을 부여하는 엔지니어링 기법을 소개합니다. 고가의 Image-to-Video 모델 대신 필터그래프를 사용하여 비용 효율적인 미디어 파이프라인을 구축하는 방법을 다룹니다.
핵심 포인트
- 고가의 AI 비디오 생성 비용을 ffmpeg 필터로 대체하여 $0로 구현
- 정지 이미지에 grain, vignette, glitch 등 다양한 효과 적용
- 수학적 기법을 활용한 저비용 고효율 미디어 파이프라인 구축
- 오픈 소스 기반의 실시간 적용 가능한 ffmpeg 필터그래프 활용
시리즈: Zero to Autopilot — 자기 개선형 AI 미디어 채널 구축하기. 7부작 중 3부입니다. 파트 1은 전체적인 전망과 저의 10달러짜리 깨달음이었고, 파트 2는 7단계 파이프라인(pipeline)에 관한 것이었습니다. 이번 파트는 엔지니어링의 핵심입니다. 유료 AI 비디오를 무료 움직임으로 대체하는 것입니다.
데이터 상태: 현재 실시간 적용 중 — 리포지토리(repo)에서 가져온 실제 ffmpeg 필터그래프(filtergraphs)입니다. 여기에 소개된 모든 효과는 라이브 갤러리(dasein108.github.io/slope-studio)에서 실제로 작동하고 있으며, 코드는 오픈 소스로 공개되어 있습니다.
시청자에게 필요한 것은 생성된 비디오가 아닙니다. 그들에게 필요한 것은 움직임입니다.
파트 1의 요약은 한 줄의 산수로 표현됩니다. 호스팅된 AI 이미지-투-비디오(image-to-video) 비용은 초당 계산됩니다. Kling의 경우 초당 0.07달러이므로, 150초 길이의 쇼츠(Short)를 만드는 데 약 10.50달러가 듭니다. 단 하나의 주인공 장면(hero shot)을 위해서는 괜찮을지 모르지만, 수백 개의 저렴한 실험을 만드는 것에 전체 전략이 달려 있는 상황에서 모든 장면에 이를 기본값으로 사용하는 것은 터무니없습니다.
하지만 시청자들은 결코 생성된(generated) 비디오를 요구한 적이 없습니다. 그들은 움직임의 느낌을 원합니다. 서서히 흐르고, 숨 쉬며, 비트에 맞춰 컷이 전환되는 정지 이미지만으로도 주의를 끄는 데는 충분합니다. 저는 수년 전 인디 게임을 출시하며 이 점을 체득했습니다. 게임 제작의 핵심은 저렴한 수학을 이용해 값비싼 것들을 흉내 내는 것입니다. 파티클 아티스트(particle artist)를 고용할 예산이 없으면 파티클 시스템(particle system)을 직접 작성하고, 애니메이션 예산이 없으면 몇 개의 레이어를 패럴랙스 스크롤(parallax-scroll)하여 분위기를 조성합니다. 이와 동일한 본능이 AI 미디어에도 그대로 적용됩니다. 아래의 모든 내용은 단 하나의 정지 이미지, ffmpeg, 그리고 0달러로 이루어집니다.
ffmpeg가 핵심 비결입니다: 효과는 곧 하나의 문자열입니다
이곳의 숨은 영웅은 바로 ffmpeg입니다. ffmpeg에는 약 400개의 내장 필터 (built-in filters)가 포함되어 있으며, "효과 (effect)"란 그중 몇 개를 쉼표로 연결한 것에 불과합니다. 렌더링 엔진도, GPU 셰이더 (GPU shaders)도, SDK도, 호출당 비용도 필요 없습니다. 이미 당신이 가지고 있는 하나의 바이너리 (binary)면 충분합니다. 이 시리즈의 모든 움직임은 ffmpeg 필터그래프 (filtergraph)이며, 이는 _효과를 추가하는 것이 곧 문자열을 추가하는 것_임을 의미합니다.
빈티지한 느낌을 주는 oldfilm의 전체 구현 코드는 다음과 같습니다:
"[0:v]colorchannelmixer=.393:.769:.189:0:.349:.686:.168:0:.272:.534:.131," # → 세피아 (sepia)
"eq=contrast=1.12:saturation=0.82:brightness='0.035*sin(27*t)+0.025*sin(11*t)'," # 깜빡임 (flicker)
"noise=alls=22:allf=t," # 필름 그레인 (film grain), 매 프레임마다 새로 생성
...
Unix 파이프 (Unix pipe)처럼 읽으세요. 각 쉼표는 "그 다음에"를 의미합니다:
colorchannelmixer— 이미지를 세피아 톤으로 매핑하는 3×3 RGB 행렬 (matrix)입니다.eq=…brightness='…sin(t)…'—t는 프레임의 타임스탬프 (timestamp)이므로, 밝기가 시간에 따라 흔들립니다 (wobbles). 즉, 영사기 게이트의 깜빡임 (flicker) 효과입니다. 시간 표현식 (Time expressions)이야말로 효과를 애니메이션화하는 핵심입니다. 여기서는sin(t)가 쓰였고, 다음에는 Ken-Burns 효과의 서서히 다가가는zoom이 쓰일 것입니다.noise=allf=t—f=t는 매 프레임마다 그레인 (grain)을 다시 무작위화하여, 정지해 있지 않고 반짝이게 만듭니다.vignette=PI/4— 모서리를 어둡게 만듭니다.
네 개의 기본 필터와 하나의 문자열만으로 움직임이 만들어집니다. 글리치 (glitch) 효과는 rgbashift + noise이며, 색수차 (chromatic aberration)는 단순히 rgbashift입니다. 비 효과는 overlay로 합성된 파티클 레이어 (particle layer)입니다. 이 채널이 수백 개의 영상을 제작할 수 있는 이유는 더 저렴한 모델을 써서가 아닙니다. 효과를 만드는 예산이 텍스트 에디터와 ffmpeg -filter_complex이기 때문입니다.
효과의 계열 (The effect families)
그 단 하나의 바이너리가 방대한 어휘를 제공합니다. 카탈로그는 몇 가지 계열로 분류되며, 각 계열은 _이 장면에 무엇이 필요한가?_라는 서로 다른 질문에 답합니다.
- 카메라 모션 (Camera motion) —
kenburns,motion-drift{left,right,up,down},motion-zoom{in,out},pulse. 가장 저렴한 생명력: 정지 화면이 팬(pans)하거나 드리프트(drifts)하고 숨 쉬는 듯한 느낌. 대부분의 장면에 기본으로 사용됩니다. - 깊이 (Depth) —
parallax,blurred-parallax. 실제 2.5D 효과: 전경 피사체는 고정된 채 배경만 그 뒤로 움직입니다. 명확한 피사체가 있는 풍경에 적합합니다. - 키네틱 유형 (Kinetic type) —
kinetic. 강조점: 헤드라인이 화면 위로 슬라이드되어 들어옵니다. 모든 장면에 필요한 것은 아니며, 후크(hook)나 핵심 통계 수치에 사용됩니다. - 분위기 (Atmosphere) —
rain,snow,fog,embers,blood,petals,leaves,wind. 분위기와 장소의 느낌—감정적인 날씨를 무료로 합성합니다. - 색상 및 룩 그레이드 (Colour & look grades) —
grain,vignette,oldfilm,sunrise,sunset,godrays,chroma. 톤과 시대적 배경. 이 계열은 '의도적인' 것과 '엉성한' 것을 분리하는 데 가장 큰 역할을 합니다: 그레인(grain)과 비네트(vignette)만으로도
studio/animate.py
a = (animator or "kenburns").strip()
if a == "kenburns" or a == "": ffmpeg.ken_burns(image, dst, seconds)
...
핵심 동력인 Ken-Burns는 단일 zoompan 표현식입니다. 크롭(crop)이 절대 가장자리에 닿지 않도록 먼저 소스를 2배로 확대(over-scale)합니다:
# studio/ffmpeg.py — ken_burns()
vf = (f"crop={w*2}:{h*2},"
f"zoompan=z='min(zoom+0.0012,1.12)':d={frames}:s={w}x{h}:fps={fps}:"
...
z='min(zoom+0.0012,1.12)'는 줌(zoom)을 프레임당 아주 미세하게 증가시키며, 1.12배에서 제한됩니다. motion-* 프리셋들은 서로 다른 z/x/y 표현식을 사용하는 동일한 메커니즘이며, 하나의 필터그래프(filtergraph)로부터 파생된 움직임의 전체 제품군입니다.
패럴랙스(Parallax), ffmpeg가 단독으로는 할 수 없는 유일한 효과
패럴랙스(Parallax) — 피사체는 고정하고 배경을 뒤로 밀어내어 깊이감을 주는 것 — 는 "효과는 문자열이다"라는 규칙의 예외입니다. ffmpeg는 레이어를 합성(composite)할 수는 있지만 피사체를 직접 찾을 수는 없습니다. 따라서 이 작업에는 먼저 작고 매우 인디 개발자스러운 해킹(hack)이 필요합니다. rembg가 피사체(정적인 전경)를 잘라내고, Python이 깨끗한 배경 평면을 구축하면, 그제서야 ffmpeg가 배경을 이동시키고 전경을 overlay 합니다.
문제는 바로 "깨끗한 배경"입니다. 단순한 방식은 컷아웃(cutout) 뒤로 원본 정지 영상을 밀어내는 것인데, 이 정지 영상에는 이미 피사체가 포함되어 있으므로 배경에 기괴한 **유령 쌍둥이(ghost twin)**가 번지는 듯한 현상이 발생합니다. 해결책은 ffmpeg에 피사체 _뒤쪽_이 완전히 채워진 배경을 제공하는 것이며, 두 가지 방법이 있습니다:
- 동일한 이미지에서 인페인팅(Inpaint) 하기 (기본값) — 무료 블러-확산(blur-diffusion) 채우기 방식입니다. 반복적으로 블러 처리를 한 다음 알려진 픽셀을 다시 찍어(re-stamp), 피사체가 있던 구멍이 주변 환경과 어우러져 치유되도록 합니다.
- 별도의 플레이트(plate) 생성하기 — 피사체 없이 장면을 다시 프롬프트합니다 (
--parallax-plates, +1 정지 이미지). 더 깔끔하며, 인페인팅의 추측성 결과가 없습니다.
# studio/animate.py — _inpaint_subject() (피사체의 구멍을 치유함)
for _ in range(iters):
blurred = bg.filter(ImageFilter.GaussianBlur(radius))
...
두 가지 요소를 모두 수용하는 더 저렴한 세 번째 옵션도 있습니다. 표류하는 평면(drifting plane)을 강하게 흐리게 처리하여 복제본이 부드러운 보케 (bokeh)로 녹아들게 만드는 방식입니다 (blurred-parallax). 복잡한 배경에서는 이것이 부자연스러운 컷아웃 (cutout)이 아니라 몽환적인 피사체 심도 (depth-of-field)로 읽힙니다. 버그가 두 번째의 정당한 스타일로 변모한 셈입니다.
텍스트, 그리고 존재하지 않았던 폰트 라이브러리
키네틱 타이포그래피 (Kinetic type)는 부드럽게 맥동하는 정지 이미지 위로 헤드라인을 슬라이드하며 등장합니다. 텍스트는 Pillow를 통해 투명한 PNG로 렌더링되며, 애니메이션이 적용된 y 좌표와 함께 overlay 되어 제자리로 떠오릅니다:
# 헤드라인이 처음 0.6초 동안 떠오르며 자리를 잡음
over = "[bg][t]overlay=x=(W-w)/2:y='H*0.18 - 50*min(t/0.6,1)':format=auto[v]"
왜 ffmpeg의 drawtext가 아닌 Pillow를 사용했을까요? 이 렌더링이 수행되는 박스는 libfreetype과 libass 없이 빌드된 ffmpeg를 사용하기 때문입니다. 따라서 drawtext와 subtitles= 모두 단순히 실패합니다. 빌드 환경과 싸우는 대신, 저는 헤드라인과 인코딩된 자막 스트립을 포함한 모든 텍스트를 Pillow PNG로 렌더링하여 오버레이합니다. 이러한 제약 사항은 오히려 픽셀 단위의 완벽한 타이포그래피 제어를 제공하는, 더 이식성 높은 설계를 강제했습니다.
효과 선택: 모델이 제안하고, 코드가 제약한다
모든 장면이 켄 번스 (Ken-Burns) 효과를 기본값으로 사용한다면 이 정도 규모의 라이브러리는 가치가 없습니다. 그리고 그것이 바로 이 프로젝트가 시작된 지점이었습니다. 따라서 작은 아트 디렉션 레이어 (studio/artdirect.py)가 의도적으로 하이브리드 정책을 사용하여 결정합니다:
- 스크립트 모델은 제안(proposes)합니다: 프롬프트 내에 문서화된 메뉴 중에서 장면별
animator(애니메이터) /atmosphere(분위기) /fx(특수 효과) /transition(전환)을 선택합니다. 이를 통해 선택 사항이 장면의 분위기와 일치하도록 합니다. 예를 들어, 결투 장면에는embers(불꽃)와 붉은flash(섬광)가 들어가고, 회상 장면에는oldfilm(오래된 필름)이, 풍경 장면에는parallax(시차)가 적용됩니다. - 그 후 결정론적 패스(deterministic pass)가 이를 제약(constrains)합니다: 이름이 유효한지 검증하고, 모델이 누락한 부분은 위치 및 키워드 휴리스틱(heuristics)을 사용하여 채웁니다 (예: hook(훅) →
kinetic(역동적), scenery(풍경) →parallax(시차)). 또한 취향 캡(taste caps)을 적용합니다. 예를 들어,flash(섬광)는 충격 효과이므로 최대 한 장면에서만 유지되며, 단일 분위기(atmosphere)가 영상 전체를 뒤덮을 수 없도록 제한합니다.
"모델이 제안하고, 코드가 제약한다(Model proposes, code constrains)"는 이 프로젝트 전반에 걸쳐 반복되는 원칙입니다. 이는 모델의 판단력은 활용하되 모델 특유의 일관성 없는 결과(inconsistency)는 피하고 싶을 때 사용할 수 있는 훌륭한 기본 설정입니다. 또한 동일한 패스가 키(key)가 없는 stub (스텁) 경로에서도 실행되기 때문에, 모든 영상은 동일한 팬(pan) 효과가 나열되는 대신 실제적인 아트 디렉션을 부여받게 됩니다.
한 가지 구체적인 이점은 고어(gore) 없이도 폭력성을 저렴하게 표현할 수 있다는 점입니다(이는 이미지 모델의 콘텐츠 필터도 통과하게 해줍니다). 컷 부분에 붉은 flash (섬광)를 넣고 blood (혈흔) 오버레이를 몇 프레임 정도 추가하는 것만으로도, 시청자의 마음이 나머지를 채우고 내레이션이 의미를 전달하며, 비용은 전혀 들지 않습니다.
아직 해결하지 못한 것: manim
3Blue1Brown의 기반이 되는 엔진인 Manim은 여기서 가장 유망하지만 가장 해결되지 않은 도구입니다. 진정한 벡터 애니메이션(vector animation) — 원이 사각형으로 변하거나, 그래프가 스스로 그려지거나, 방정식이 항별로 변환되는 것 — 은 교육용 채널에게는 거의 치트키와 같으며, $0의 비용으로 선명하게 렌더링할 수 있습니다. 장면은 모델이 작성한 manim_code 필드를 가질 수 있으며, 파이프라인이 이를 렌더링하게 됩니다.
[

AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
