영상 하나는 천 장의 사진만큼의 가치가 있다
요약
16GB VRAM의 로컬 환경에서 FLUX, Wan2.2, MuseTalk 등을 조합하여 소설을 모션 비디오로 변환하는 파이프라인 구축 사례를 소개합니다. 각 AI 모델의 특성을 파악하고 이를 연결하여 일관된 캐릭터와 립싱크가 포함된 영상을 생성하는 워크플로우를 다룹니다.
핵심 포인트
- 16GB VRAM(RTX 4060Ti) 환경에서 클라우드 없이 로컬 파이프라인 구현
- FLUX(이미지), Wan2.2(움직임), MuseTalk(립싱크)의 역할 분담 및 조합
- PuLID를 활용하여 여러 장면에서 캐릭터 얼굴의 일관성 유지
- AI 모델을 마법 상자가 아닌, 특정 기능에 특화된 악기로 취급하는 관점 제시
FLUX, Wan2.2, PuLID, MuseTalk, 그리고 ComfyUI를 사용하여, 단 하나의 16GB GPU 로컬 환경에서 소설 전체를 내레이션과 입 모양 동기화(lip-synced)가 포함된 모션 비디오로 변환합니다.
'사진 한 장은 천 마디 말의 가치가 있다'라는 오래된 격언이 있습니다. 저는 이를 확장하여 '영상 하나는 천 장의 사진만큼의 가치가 있다'라고 말하고 싶습니다. 그래서 저는 지난 몇 달 동안 이 아이디어를 고된 방식으로 테스트했습니다. 소설 전체를 내레이션과 입 모양 동기화(lip-synced)가 포함된 모션 비디오로 변환하는 파이프라인을 구축했습니다. _오만과 편견(Pride and Prejudice)_이나 400년 된 _요재지이(Strange Tales from a Chinese Studio, 聊斋志异)_의 이야기를 입력하면, 100개 이상의 장면 전체에서 일관된 캐릭터 얼굴을 유지하는 완성된 영화가 나옵니다.
단 한 사람. 16GB VRAM을 가진 RTX 4060Ti 한 대. 모든 것은 클라우드 추론(cloud inference) 없이 로컬에서 이루어집니다.
저는 이것을 iTube라고 불렀습니다. 그리고 솔직히 말해서, 영상은 제 기대보다 더 잘 나왔습니다. 알고 있는 소설이 움직이고, 목소리가 입혀지며, 호흡이 있는 무언가로 변하는 것을 보는 것은 요약본을 읽는 것과는 진정으로 다른 경험입니다. 저의 가설은 이렇습니다. 영상은 텍스트보다 이야기를 '전달하는' 데 더 나은 매체라는 것이며, 완성된 클립들은 그 가설이 옳았음을 대부분 증명해 주었습니다.
작동 방식에 대해 다른 글을 읽기 전에 직접 확인해 보세요:
- 🎬 — 서구 공공 저작물 클래식
- 🎬 聊斋志异 / Strange Tales from a Chinese Studio — 중국 고전 설화 플레이리스트
하지만 이 포스트는 데모 영상 모음집이 아닙니다. 아무도 경고해주지 않는 부분, 즉 모델들 사이의 과정에 관한 이야기입니다. 왜냐하면 진짜 작업은 바로 그곳에서 이루어졌기 때문입니다.
모델은 마법 상자가 아니라 악기입니다
제가 배운 가장 중요한 한 가지이자, 이와 같은 파이프라인을 다루기 전에 협업자가 반드시 이해했으면 하는 점은 다음과 같습니다:
각 AI 패키지는 정확히 한 가지 일을 잘할 수 있으며, 여러분의 전체 업무는 각 모델의 능력이 정확히 어디에서 끝나는지 파악한 다음, 그 공백을 우회하도록 스크립트 로직을 작성하는 것입니다.
스택(stack)은 생소하지 않습니다. 생성형 미디어(generative media) 분야의 대부분의 사람들은 이 구성 요소들을 알고 있습니다:
- FLUX는 장면을 위한 정지 이미지를 생성합니다. 그것이 하는 일의 전부입니다 — 단 하나의 프레임이죠. 아름답고 적절한 프롬프트(prompt)로 제어 가능하지만, 정지되어 있습니다.
- Wan2.2는 그 정지 이미지를 가져와 움직임을 추가합니다 — 이미지-투-비디오 (image-to-video). 결정적으로, 이것은 '말(speech)'이 아니라 '움직임(movement)'을 애니메이션화합니다. 바람, 물, 고개를 돌리는 동작, 걷기 같은 것들 말이죠. 립싱크(lip-sync)는 수행하지 않으며, 방치할 경우 요청하지 않은 움직임을 만들어내기도 합니다.
- MuseTalk는 립싱크(lip-sync) 전문가입니다. 얼굴과 오디오 클립을 제공하면, 음성에 맞춰 입 모양을 구동합니다. 단 한 가지 작업만 수행하며, 스택(stack) 내에서 그 일을 해내는 유일한 요소입니다.
- edge-tts는 나레이션과 대화 오디오를 합성(synthesize)합니다.
- PuLID는 캐릭터의 얼굴을 고정하여, 모든 장면에서 동일한 인물이 동일 인물로 보이게 합니다.
여기서 얻는 통찰은 단순히 이 목록이 아닙니다. 오디오가 먼저 오고, 그 외의 모든 것이 오디오를 중심으로 휘어진다는 점입니다. MuseTalk는 얼굴과 정해진 길이의 오디오 클립이 필요합니다. 따라서 말하는 대사의 길이가 장면의 길이를 결정합니다. 장면의 지속 시간을 먼저 정하고 그 안에 음성을 맞추는 것이 아닙니다. 음성은 고정되어 있고, 결정적이며, 움직일 수 없는 반면, 시각 요소는 유연하여 음성에 맞춰 늘리거나 자를 수 있습니다. 오디오는 골격이고 비디오는 살이라는 이 역전된 개념을 내면화하고 나면, 전체 스크립팅(scripting) 로직이 제자리를 찾게 됩니다. 이를 내면화하기 전에는 아무것도 맞아떨어지지 않으며, 왜 그런지 이유조차 알 수 없습니다.
따라서 이 파이프라인(pipeline)을 구동하는 "시나리오"를 쓰는 것은 창의적인 글쓰기가 아닙니다. 다섯 가지 모델의 정확한 능력과 실패 모드(failure modes)를 머릿속에 동시에 담아두고 쓰는 작업입니다. 이것이 실제 작업에서 무엇을 의미하는지 몇 가지 예를 들자면 — 이는 제가 이 도구를 이해하기 전에 직접 겪었던 버그들입니다:
Wan은 소리가 없는 장면에서도 입을 움직이려 할 것입니다. 대사가 없는 달빛 비치는 마당 — 이때 Wan은 친절하게도 지나가는 행인이 아무 말 없이 입을 벙긋거리게 만드는데, 이는 매우 불안한 느낌을 줍니다. 해결책: 얼굴은 있지만 대사가 없는 장면은 Wan으로 보내지 않습니다; 대신 정지 프레임으로 라우팅(route)합니다. 스크립트가 이 차이를 알고 표시해야 합니다.
Wan은 텅 빈 풍경 속에 사람을 불러내어 애니메이션을 만듭니다. 이미지-투-비디오 (image-to-video) 모델은 진공 상태를 채우려 하기 때문입니다. 즉, 사용자가 원하든 원하지 않든 그럴듯한 움직임을 찾아냅니다. 해결책: 얼굴이 없는 환경 장면에는 명시적인 부정 프롬프트 (person, people, human, face, figure)를 부여하고, 환경적 움직임(나뭇잎, 물, 빛)을 향한 긍정적 리다이렉트 (positive redirect)를 설정합니다. 무엇이 움직여야 하는지 정확히 알려주지 않으면, 모델이 스스로 결정해 버립니다.
FLUX는 중국 괴담 속 요괴를 서구적인 얼굴 구조로 렌더링합니다. 모델이 가장 많이 접한 미학을 기본값으로 사용하기 때문입니다. 해결책: 모든 생명체 장면에는 명시적인 동아시아적 골격 구조와 중국 설화 디자인 단서들을 고정(anchor)해야 합니다. 그렇지 않으면 전체적인 결과물이 문화적으로 잘못된 불쾌한 골짜기 (uncanniness)로 흘러갑니다. (기괴한 형상들의 경우, 저는 실사 공포보다는 중국 오페라 또는 목판화 스타일을 선택했습니다. 이는 부분적으로는 취향의 문제였고, 부분적으로는 캐리커처처럼 읽힐 수 있는 결과물을 피하기 위함이었습니다.) 그 결과물은 聊斋志异 playlist에서 확인할 수 있습니다.
이 중 그 어떤 것도 모델의 버그가 아닙니다. 모델은 그저 자신이 하는 일을 정확히 수행하고 있을 뿐입니다. 매번 발생한 버그는 바로 저의 실수였습니다. 악기에게 자신의 음역대를 벗어난 음을 연주하라고 요구한 것이죠. 결국 제품은 패키지들의 조합과 스크립팅 로직(scripting logic)의 결합입니다. 어느 하나만으로는 제품이 될 수 없습니다.
가장 어려운 부분: 그래프를 연결하는 법을 배우는 것
위의 모든 내용은 ComfyUI 워크플로우가 작동한다는 것을 전제로 합니다. 그 단계에 도달하는 것 자체가 하나의 교육 과정이었으며, 저는 지나고 나서 회상하는 방식이 아니라 실제로 그것이 어떻게 느껴졌는지에 대해 솔직해지고 싶습니다.
ComfyUI는 브라우저 기반의 노드 그래프 (node graph)입니다. 캔버스를 더블 클릭하여 노드를 검색하고, 이를 배치한 뒤, 한 노드의 출력 소켓 (output socket)에서 다음 노드의 입력 소켓 (input socket)으로 선을 드래그합니다. 때로는 필요한 노드가 아예 없을 수도 있으며, 그럴 때는 해당 노드를 제공하는 패키지를 먼저 설치해야 합니다. 이것이 인터페이스의 전부입니다. 글로 써놓으면 단순하게 들립니다.
실제로, 처음 한동안은 제가 무엇을 하고 있는지 전혀 몰랐습니다. 저는 AI가 생성한 지침을 단계별로 따르고 있었고, 모든 단계가 '이것을 하고, 저것을 클릭하고, 여기에 연결하라'는 식의 임의적인 과정처럼 느껴졌으며, 그 '이유'에 대한 감각이 전혀 없었습니다. 나중에야 그 당혹스러운 단계들을 한참 거친 것이 그저 패키지들을 설치하는 과정이었다는 것을 깨달았습니다. 이것이 새로운 도구를 배울 때 아무도 말해주지 않는 부분입니다. 즉, 해당 절차가 '무엇인지'에 대한 정신적 모델 (Mental Model)이 전혀 없는 상태에서도 한 시간 동안 절차를 완벽하게 수행할 수 있다는 점입니다. 단계 자체가 어려운 것이 아닙니다. 자신이 지금 어떤 범주의 작업을 하고 있는지 모르는 것이 어려운 것입니다.
결국 모델이 나타났습니다. 와이어(Wires)는 데이터를 연결합니다. 모델 로더 (Model Loader)가 인코더 (Encoder)에 데이터를 공급하고, 인코더는 샘플러 (Sampler)에, 샘플러는 다시 디코더 (Decoder)에 공급합니다. 이 구조가 이해되는 순간, 그래프는 더 이상 의례적인 행위가 아니라 제가 추론하고 디버깅 (Debug)할 수 있는 무언가가 되었습니다.
그리고 보상이 찾아왔습니다. 실행을 하자, 정지된 이미지가 움직였습니다. 그 모든 서툰 시도 끝에, 제가 생성한 프레임이 실제로 살아 움직이는 것을 보는 것은 진정으로 짜릿한 경험이었습니다. 이전의 혼란이 가치 있게 느껴지게 만드는 그런 순간 중 하나였습니다. 저는 이해도 못한 채 우연히, 그리고 서툴게 그것을 만들어냈지만, 그것은 '작동'했습니다.
그 작동하는 그래프는 이제 wan_i2v_480.json이라는 이름으로 리포지토리 (Repo)에 고정되어 있습니다. 덕분에 제가 매번 브라우저에서 수동으로 다시 구축할 필요 없이, 파이프라인 (Pipeline)이 프로그래밍 방식으로 이를 재현할 수 있습니다. 수동으로 연결한 그래프를 재사용 가능한 아티팩트 (Artifact)로 만드는 것은, 어떤 의미에서는
- 시스템 Python 환경의
127.0.0.1:8188에 있는 "cons" 스택과, - fp8 Wan 모델을 로드할 수 있는 전용 가상 환경 (venv) 내의
127.0.0.1:8189에 있는 "move" 스택입니다.
각 파이프라인 단계는 실제 종속성 (dependencies)이 위치한 백엔드로 라우팅됩니다. 16GB 그래픽 카드는 두 서버의 모델을 동시에 상주 시킬 수 없기 때문에, 오케스트레이션 (orchestration) 과정에서 다른 백엔드를 올리기 전에 반드시 하나의 백엔드를 종료하여 — 포트 (port) 단위로 프로세스를 죽여 VRAM을 확보해야 — 합니다. 두 개의 서버가 하나의 GPU를 번갈아 가며 사용하는 방식입니다.
이러한 분리는 설계상의 선택이 아니라 임시 방편 (workaround)임을 분명히 하고 싶습니다. 공유 시스템 Python을 fp8 빌드로 업그레이드할 경우 이미 작동 중인 MuseTalk 스택이 깨질 위험이 있었기에, 두 개의 격리된 환경을 구축하는 것이 '필요한' 방식이 아닌 '안전한' 경로였기 때문입니다. 한 환경이 충돌 없이 fp8 Wan 빌드와 mmpose를 모두 수용할 수 있게 된다면, 하나의 서버로 병합될 가능성이 매우 높으며 저는 이미 그 과정의 중간 단계에 와 있습니다. 하지만 서버를 병합하더라도 더 근본적인 제약 사항은 변하지 않습니다. 16GB 환경에서는 모델들을 여전히 한꺼번에 메모리에 올릴 수 없습니다. 현재는 서버를 종료함으로써 VRAM을 확보하지만, 병합 후에는 하나의 서버 내에서 모델을 스와핑 (swapping) 함으로써 확보하게 될 것입니다. 메커니즘은 더 깔끔해지겠지만, 메모리 한계치 (memory ceiling) 자체는 움직이지 않습니다.
하지만 이 라우팅을 제대로 설정하는 데는 꽤 오랜 시간이 걸렸습니다. 왜냐하면 오랫동안 최악의 방식으로, 즉 조용히 (silently) 실패했기 때문입니다. 모션 (motion) 단계가 fp8 모델을 로드할 수 없는 '잘못된' 백엔드로 명령을 전달했고, 로드에 실패하면 파이프라인은 정지 영상 (static frame)으로 폴백 (fallback) 되었습니다. 충돌 (crash)도, 트레이스백 (traceback)도 없이, 그저... 움직임이 없었습니다. 모든 장면이 성공했다고 보고하면서도 조용히 정지 화면으로 렌더링되었습니다. 차라리 충돌이 발생했다면 자비로웠을 것입니다. 충돌은 줄 번호를 가리키니까요. 하지만 이 상황은 아무것도 가리키지 않았습니다. 제가 폴백 (fallback) 메커니즘을 너무나 우아하게 구축해 놓은 나머지, 폴백이 발생하게 만든 바로 그 원인을 숨겨버렸기 때문입니다.
그 교훈은 그 어떤 것보다 저에게 강렬하게 각인되었습니다. 우아한 폴백 (fallback)은 버그가 숨어들기 딱 좋은 장소입니다. 시스템이 조용히 성능을 저하시킨다면(degrade), 성능이 저하되었다는 사실을 반드시 명확하게(loud) 알려야 합니다. 그렇지 않으면 로그상에서 "성공"과 "완전한 실패"가 동일하게 보이기 때문입니다.
라벨이 아닌 바이트 (bytes)를 믿으세요
이 프로젝트를 훨씬 넘어서는 일반적인 원칙이기에, 전달할 가치가 있는 또 하나의 디버깅 교훈을 공유하고자 합니다.
제 오디오 단계에서는 가끔 모든 검사를 통과하는 파일들이 생성되곤 했습니다. 메타데이터(metadata)상으로는 유효한 MP3이고, 재생 시간과 비트레이트(bitrate)도 정확했지만, 실제로 디코딩 (decoding)을 시도하는 순간 파일이 손상되어 있었습니다. 그러면 후속 단계인 MuseTalk에서 오류가 발생했고, 저는 평소 의심하던 범인인 메모리 부족 (out-of-memory) 문제를 탓하며 재시도 사이클을 낭비하곤 했습니다.
해결책은 메타데이터를 믿지 말고 _실제 디코딩 (real decoding)_을 통해 검증하는 것이었습니다. 파일을 ffmpeg로 실행하여 헤더(header)가 주장하는 내용이 아니라, 실제로 샘플 (samples)이 생성되는지 확인하는 방식입니다. 메타데이터 검사는 주장일 뿐이지만, 디코딩은 사실입니다. 어떤 파이프라인 (pipeline)에서든, 각 단계의 인계 시점에 물건에 붙은 라벨이 아니라 물건 그 자체를 검증하십시오. 이 원칙 하나가 그 어떤 것보다 저의 수많은 시간을 아껴주었습니다.
아직 할 수 없는 것 — 그리고 한계가 대부분 메모리 때문인 이유
성공만큼이나 실패의 간극에서도 많은 정보를 얻을 수 있기에, 한계점에 대해 솔직하게 말씀드리고자 합니다.
비언어적 소리 (non-verbal sound)의 부재. 현재 파이프라인은 음성은 처리하지만, 장면을 생생하게 만드는 소리들 — 외침, 말의 울음소리, 말발굽 소리, 바람과 눈보라의 울부짖음 등 — 은 처리하지 못합니다. 음향 효과 (sound-effects) 레이어를 추가하는 것이 다음 단계입니다.
배경 음악 (BGM)은 여전히 진행 중입니다. 장면별 BGM 계획 체계는 갖추고 있지만, 나레이션 아래에 음악을 적절히 배치하는 것 — 더킹 (ducking), 분위기 맞춤, 전환 등 — 은 아직 미완성 상태입니다.
모든 장면은 다른 장면과 독립적으로 생성됩니다. 이것이 제가 가장 흥미롭다고 느끼는 한계점인데, 저는 이것이 이론적인 문제가 아니라 메모리 (memory) 문제라고 생각하기 때문입니다. 원칙적으로는 파이프라인이 장면 간에 컨텍스트 (context)를 전달하여 캐릭터의 코트나 방의 구조가 샷 (shot)마다 진정으로 일관되게 유지될 수 있습니다. 하지만 실제로 백 개 이상의 장면에 걸쳐 캐릭터 일관성 (character consistency), 객체 일관성 (object consistency)과 같은 방대한 비디오 컨텍스트를 메모리에 유지하는 것은 엄청나게 비용이 많이 드는 일이며, 저는 이미 16GB의 한계에 부딪힌 상태입니다. 그래서 각 장면은 고립된 상태로 렌더링되며, 저는 진정한 공유 컨텍스트 (shared context) 대신 PuLID 페이스 락킹 (face-locking)과 절제된 프롬프팅 (prompting)을 통해 일관성을 강제합니다. VRAM (비디오 램)이 더 많아진다면 상황은 달라질 것입니다.
해상도 또한 VRAM에 의해 제한됩니다. 저의 기본 렌더링은 매우 작습니다. 품질의 상한선이 그래픽 카드에 얼마나 많은 데이터를 담을 수 있는지에 의해 눈에 띄게 결정될 정도로 작습니다. 저는 메모리가 더 많아지고 기본 해상도가 높아지면 출력 품질이 상당히 도약할 것이라고 확신합니다. 현재로서는 메모리가 거의 모든 작업이 부딪히는 벽입니다.
만약 이러한 한계점들에 하나의 주제가 있다면, 그것은 바로 이것입니다: 소비자용 하드웨어에서 메모리는 일관성, 해상도, 풍부함 등 중요한 거의 모든 요소에 있어 결합 제약 (binding constraint)입니다. 모델들은 제 그래픽 카드가 담을 수 있는 것보다 더 많은 것을 할 수 있는 능력을 갖추고 있습니다.
왜
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기