영상 엔진을 LTX에서 MiniMax H3로 갈아타며 만난 함정들
로그는 초록불인데 결과물은 망가져 있었다. 화각이 흐르고, 오디오가 중국어로 바뀌고, 품질 게이트는 몇 달째 꺼져 있었다. 하루 뒤 서버를 정리하다 셋을 더 찾았다.
유튜브 채널용 영상 엔진을 LTX 2.3에서 MiniMax H3로 갈아탔다. 성능은 확실히 좋아졌는데, 그 과정에서 결과물이 조용히 망가지는 함정을 세 개 만났다. 셋 다 로그에는 "정상"이라고 찍힌다는 게 공통점이다.
기록해 둔다. 같은 걸 하려는 사람에게 시간을 아껴줄 것 같아서.
왜 갈아탔나
기존 파이프라인은 LTX 2.3의 audio-to-video로 캐릭터 립싱크 영상을 만든다. 베이스 이미지와 나레이션 오디오를 넣으면 입이 소리에 맞춰 움직인다.
문제는 확률적이라는 것이었다. 같은 입력이라도 어떤 클립은 입을 잘 움직이고 어떤 클립은 거의 다물고 있다. 그래서 생성된 클립의 입 벌림 정도(mediapipe의 jawOpen 블렌드셰이프)를 측정해 실패하면 재생성하는 게이트를 붙여 뒀다. 실측해 보니 LTX는 3개 중 1개가 게이트를 실패했다. 재생성 루프가 상시 돌아간다는 뜻이다.
MiniMax H3는 2026년 8월 3일 오픈웨이트로 공개됐다. 영상과 스테레오 오디오를 한 번에 생성하는 옴니모달 모델이다. 립싱크를 재보니 13개 중 1개 실패(약 8%). LTX의 33%와 비교하면 확실히 낫다.
여기까지는 좋았다.
함정 ① 프롬프트 포맷을 안 지키면 화각이 흐른다
H3로 만든 클립에서 카메라가 제멋대로 줌인하거나, 클립 중간에 장면이 통째로 다른 구도로 바뀌는 일이 생겼다. 시작 프레임과 후반 프레임의 SSIM을 재보면 0.386, 심하면 음수까지 떨어졌다. 완전히 다른 화면이 됐다는 뜻이다.
프롬프트에 이미 이렇게 써 놨는데도 그랬다.
"카메라는 완전히 고정되어 줌인이나 줌아웃 없이 일정한 화각을 유지한다."
영어로도 써 봤다. "one single continuous locked-off static medium shot... No zoom, no push-in, no pan, no dolly, no cuts." 그래도 흘렀다.
참조 이미지를 더 크게 넣는 옵션(ref_image_size: max)도 시도했다. 오히려 나빠졌다. 얼굴이 2.6배로 커지는 클로즈업 모핑이 났다.
그래서 공식 문서를 찾아봤다. MiniMax가 참조 기반 생성 전용 프롬프트 작성 가이드를 따로 배포하고 있었다. 읽어 보니 내 프롬프트는 형식 자체가 규격 밖이었다.
공식 포맷은 여섯 개 섹션을 정해진 순서로 쓴다.
subject_definitions ← 참조 대상 정의
summary ← 작업 유형 + 참조 관계 요약
retention_analysis ← 각 참조를 얼마나 보존할지 명시
detailed_description ← 샷 단위 본문
overall_soundscape
non_diegetic_music
핵심은 세 번째 retention_analysis였다. 화각 고정에 정식 장치가 따로 있었던 것이다.
<Picture 1> is the composition anchor for [Shot 1], fixing the camera angle,
subject size, and framing for the entire video.
...
retention_analysis:
<Picture 1> ([Shot 1] composition anchor): fully_preserved - the camera angle,
subject size, and framing are held constant from the first frame
to the last, with no zoom, push-in, pan, tilt, dolly, or reframing.
fully_preserved는 자유롭게 쓰는 문장이 아니라 정해진 키워드다. partially_preserved, attribute_transfer, weak_reference 중에서 고르게 돼 있다. 참조마다 "어디까지 지킬 것인가"를 이 값으로 선언한다.
그 외에 내가 어긴 규칙들.
- 본문은 영어로 써야 한다. 원어를 남기는 건
<d>[Korean] …</d>안의 대사뿐이다. 나는 전부 한국어로 썼다. - 정체성은
<Subject N>으로 정의한다. 가이드가 명시적으로 "이미지가 인물·장면·의상·스타일만 정의하면<Picture N>단독 항목을 만들지 마라"고 하는데, 내 시도는 정확히<Picture 1> = 인물…이었다. - 참조마다 역할(정체성·스타일·모션·카메라 거동·목소리)을 부여해야 한다. ComfyUI 문서의 표현이 인상적이었다 — "파일만 던져 놓고 되기를 바라지 말고(instead of dumping files and hoping)".
공식 포맷으로 다시 짜고 같은 조건에서 3테이크를 뽑았다.
| 태그 없는 프롬프트 | 공식 6섹션 포맷 | |
|---|---|---|
| 화각 드리프트 | 6중 4 | 3중 0 |
| 후반 SSIM | 0.386 | 0.982 |
| 줌 비율 | 1.1512 (줌인) | 1.0116 |
프롬프트 문구를 강하게 쓰는 문제가 아니라, 모델이 읽는 형식을 맞추는 문제였다.
함정 ② 클립이 나레이션보다 길면 오디오가 통째로 다시 만들어진다
이건 더 고약했다. 생성된 영상에서 첫마디만 한국어로 나오고 그 뒤부터 중국어로 넘어가는 현상이 있었다.
처음엔 "뒤에 남는 구간을 모델이 지어내나 보다" 하고 넘겼다. 나레이션은 6.7초인데 클립은 10.1초였으니, 남는 3.4초를 채우느라 그러는 줄 알았다.
틀렸다. 원본 나레이션과 모델이 뱉은 오디오의 파형을 정렬해 봤다.
| 조건 | 시간차 | 상관계수 |
|---|---|---|
| 나레이션 10.07초 / 클립 10.13초 (0.06초 차) | 8ms | 0.907 |
| 나레이션 6.71초 / 클립 10.13초 (3.42초 차) | 326ms | 0.348 |
상관계수 0.348은 원본과 딴판이라는 뜻이다. 타이밍도 326ms 밀렸다. 뒤쪽만 지어내는 게 아니라 나레이션 구간까지 통째로 재합성되고 있었다.
H3는 참조 오디오를 그대로 복사하지 않는다. 클립 길이를 채우도록 발화 전체를 다시 만든다. 길이가 안 맞으면 그 과정에서 원본을 벗어나고, 중국 회사가 만든 모델이니 중국어로 흘러간다.
대책은 두 가지를 같이 써야 했다.
- 클립 길이를 나레이션에 바짝 붙인다
- 참조 오디오를 클립 길이와 정확히 같게 무음 패딩해서 넣는다 — 빈 구간을 아예 없앤다
수정 후 상관계수 0.348 → 0.893, 시간차 326ms → 5ms.
덤으로 얻은 게 있다. 쓸데없이 긴 클립을 안 만들게 되니 프레임이 28~42% 줄었다(243프레임 → 175프레임). 생성 시간이 그만큼 빨라진다. 품질 버그를 고쳤더니 속도가 따라온 셈이다.
참고로 최종 결과물은 무사했다. 파이프라인이 모델 오디오를 버리고 원본 나레이션을 따로 입히는 구조였기 때문이다. 문제는 검토용으로 남겨 두는 개별 클립 파일에 그 소리가 그대로 남아 있었다는 것.
함정 ③ 품질 게이트가 몇 달간 꺼져 있었다
이게 제일 뼈아팠다.
립싱크·이물질·구도 드리프트를 검사하는 스크립트는 파이썬으로 돌아간다. OpenCV와 mediapipe가 필요하다. 그런데 이 PC에서 python을 치면 3.12가 잡히고, 거기엔 둘 다 없었다. 라이브러리는 3.11에만 깔려 있었다.
측정에 실패하면 어떻게 되도록 짜 놨느냐면 — "통과"로 처리하도록 해 놨다. 측정 도구 문제로 생성 자체를 막지 않으려는 배려였다.
결과적으로 게이트 세 개가 전부 무력화된 채, 로그에는 이렇게 찍히고 있었다.
lip ✓ (jawMean null) · darkRatio 0 ✓ · 구도 ✓유지
게이트 통과 ✓ (립·이물질·구도)
유일한 단서는 jawMean null과 darkRatio 0이었다. 둘 다 폴백 기본값이다. 모르고 보면 정상 로그다.
이것 때문에 실제로 화각이 무너진 클립이 그대로 채택돼 최종본에 들어갔다. LTX를 쓰던 시절에도 같은 상태였을 가능성이 높다.
고친 방식은 이렇다. 인터프리터를 자동 탐색하되, import cv2, mediapipe, numpy가 실제로 되는지 실행해 보고 되는 것만 채택한다. 못 찾으면 크게 경고를 띄운다. 로그도 폴백일 때는 ⚠️ 게이트 미측정으로 구분해 표기한다.
"측정 실패 = 통과" 폴백은 게이트를 조용히 없앤다. 폴백을 남기더라도 로그에서 반드시 구분돼야 한다.
그 밖에 알아 둘 것
H3는 계보가 둘이고 역할이 갈린다. ref2va는 참조 이미지 9장·영상 3개·오디오 3개를 받는데, 오디오 립싱크는 이 모드에서만 된다. fl2va는 첫/끝 프레임을 고정하는 대신 오디오 입력 슬롯이 아예 없다. 시작 프레임과 소스 이미지의 SSIM을 재보면 fl2va는 0.81, ref2va는 0.38이다. 그래서 말하는 장면은 ref2va, 자료화면은 fl2va로 나눠 붙였다.
16GB로는 1080p를 직접 못 뽑는다. 10초 기준 0.6메가픽셀이 상한이고 그 이상은 VRAM 부족으로 죽는다. 공식 문서를 보니 애초에 네이티브 캔버스가 단변 768px이고, 광고하는 2K는 호스팅 전용 모듈이라 오픈웨이트에 포함돼 있지 않았다. 저해상도로 생성하고 업스케일하는 게 정답이고, 참고한 해외 채널 세 곳도 예외 없이 그렇게 하고 있었다. 저해상도 생성 + 워크플로우 안에서 업스케일해서 1088×1920을 뽑는 데 10초 클립 기준 775초가 걸린다.
같은 시드면 서버가 달라도 결과가 같다. 서버 두 대에서 같은 조건으로 돌려 봤더니 비디오·오디오 스트림 해시가 완전히 일치했다. 생성 시간도 648초 대 644초. 부하 분산이나 폴백을 마음 놓고 쓸 수 있다는 뜻이라 실무적으로 의미가 크다.
정리하면
성능 좋다는 새 모델로 갈아타는 일이었는데, 정작 시간을 쓴 건 "잘 돌아가는 것처럼 보이는데 실은 망가진 상태"를 찾아내는 일이었다. 셋 다 에러를 내지 않았다. 로그는 초록불이었다.
특히 세 번째는 뼈아프다. 품질을 지키려고 만든 장치가 스스로 꺼져 있었고, 나는 그 초록불을 몇 달간 믿고 있었다.
그리고 하나 더. 이번에 결정적이었던 건 공식 문서 원문을 찾아본 것이었다. 검색 결과 요약이나 남의 튜토리얼로 짐작하며 프롬프트를 이리저리 바꿀 때는 진전이 없었는데, 모델 배포처가 직접 낸 가이드를 열어 보니 필요한 장치가 이미 문서에 다 적혀 있었다. 새 모델을 다룰 때 제일 먼저 할 일은 만든 쪽이 뭐라고 써 놨는지 읽는 것이다.
표본이 적을 때 성급히 결론 내린 것도 반성한다. 3개 뽑아 보고 "H3는 줌이 없다"고 판단해 보정 로직을 껐다가, 표본이 늘자 뒤집혀서 되돌렸다. 몇 번을 그랬다. 3개로는 아무것도 모른다.
덧붙임 — 하루 뒤에 셋이 더 나왔다
글을 올린 다음 날 서버 두 대를 정리하다가 같은 종류를 셋 더 찾았다. 앞의 셋과 성격이 똑같다. 아무것도 에러를 내지 않는다.
④ 폴백이 죽어 있었다
이 파이프라인은 H3가 말썽이면 LTX로 되돌아갈 수 있게 엔진을 옵션으로 뺐다. 그게 안전장치였다.
그런데 그 폴백이 살아 있는지 한 번도 재본 적이 없었다. 전제만 하고 있었다.
재봤더니 두 대 다 죽어 있었다. LTX용 커스텀 노드가 본체 업데이트와 어긋나 로딩에 실패한 상태였다. 본체가 두 달 전 함수 하나를 없애고 다른 것으로 통합했는데, 노드 쪽이 옛 이름을 계속 부르고 있었다. 모델 파일은 멀쩡히 다 있었으니 디스크만 봐서는 알 수 없다.
확인은 명령 한 줄이면 됐다. 노드 41개 중 1개가 없다고 바로 나왔다.
게이트 건과 같은 실수다. 그때는 초록불을 믿었고, 이번엔 아예 불을 켜 보지 않았다.
⑤ 검증한 건 하나인데, 다섯에 걸려 있었다
서버 두 대 중 한 대만 어텐션 최적화 라이브러리를 전역 플래그로 켠 채 떠 있었다. 기동 옵션 하나 차이다.
전역으로 켜면 그 서버에서 도는 모든 모델에 적용된다. 영상 모델만이 아니라 이미지·음악·음성 합성까지 전부. 그런데 내가 품질을 재본 건 H3 하나뿐이었다. 나머지 넷은 검증한 적 없는 어텐션으로 몇 주째 돌고 있었던 셈이다. 특히 음성과 음악은 품질이 조금 나빠져도 눈에 잘 안 띈다.
한쪽에 맞춰 다른 쪽도 켤까 잠깐 고민했는데, 그건 미검증 면적을 두 배로 늘리는 선택이었다. 양쪽을 똑같이 틀리게 맞추는 건 통일이 아니다.
그래서 반대로 갔다. 전역 플래그를 떼고, 워크플로우 안에서 노드로만 켠다. 그러면 모델별로 하나씩 재면서 켤 수 있고, 문제가 생겨도 어디서 났는지 좁혀진다. 전역은 다섯이 한꺼번에 바뀌어서 원인 분리가 안 된다.
⑥ "고쳤다"와 "반영됐다"는 다르다
플래그를 뗀다고 배치 파일을 고쳤는데, 정작 서버는 고치기 전에 띄운 프로세스가 그대로 돌고 있었다. 실행 중인 프로세스의 기동 인자는 나중에 파일을 고친다고 바뀌지 않는다. 파일은 깨끗한데 서버는 옛 설정으로 사는 상태.
여기서 내가 한 번 더 틀렸다. 처음에 나는 "그건 서버 PC의 배치 파일을 직접 봐야 알 수 있다"고 말했다. 아니었다. ComfyUI가 /system_stats로 자기 기동 인자를 그대로 알려준다. 찔러보지도 않고 안 된다고 단정한 것이다.
비슷한 걸 하나 더 만났다. 커스텀 노드 폴더에 .git이 없는 상태에서 버전을 확인하려고 git rev-parse HEAD를 치면, git이 상위 폴더로 올라가 본체 저장소의 해시를 조용히 반환한다. 에러는 없다. 그걸 롤백 기준점으로 적어 뒀으면 되돌릴 곳을 잃을 뻔했다.
그래서
여섯 개를 늘어놓고 보니 하나로 모인다. 확인하지 않은 것을 확인한 셈 쳤다.
게이트는 통과했다고 찍혔지만 재지 않았고, 폴백은 있다고 여겼지만 켜 보지 않았고, 설정은 고쳤지만 반영을 안 봤다. 그중 어느 것도 에러를 내지 않았다.
앞의 셋을 정리하며 "공식 문서 원문을 찾아 읽어라"라고 썼는데, 하루 지나고 하나 더 붙이게 됐다. 전제로 깔고 있는 것부터 한 번 재봐라. 대개 명령 한 줄이다.