캐릭터 정체성 유지 하나의 형태가 어떤 변형을 거쳐도 같은 형태로 읽히는 것, 캐릭터 파이프라인이 실제로 푸는 문제입니다.

마스코트나 캐릭터를 생성 파이프라인에 태워 보신 분이라면 익숙한 장면이 있습니다. 정면 컷 하나는 만족스럽게 나왔는데, 같은 캐릭터로 열두 가지 동작을 요청하면 열두 마리가 돌아옵니다. 색은 얼추 맞고 분위기도 비슷한데 같은 캐릭터라고 말하기는 어렵습니다.

이때 가장 먼저 떠오르는 처방은 레퍼런스를 더 넣는 것입니다. 저희도 그렇게 했고, 결과는 예상과 달랐습니다. 이 글은 캐릭터 여덟 종으로 측정한 숫자와, 그 숫자가 파이프라인 설계를 어떻게 바꿨는지에 대한 기록입니다. 결론부터 말씀드리면 정체성은 프롬프트로 지시하는 것이 아니라 가중치나 픽셀에 박아 넣는 것이었습니다.

베이스 모델은 레퍼런스를 줘도 캐릭터를 그리지 않습니다

측정 설계는 단순합니다. 캐릭터마다 레퍼런스 스틸 네 장을 조건으로 주고, 학습에 한 번도 쓰지 않은 동작 여섯 개를 생성하게 했습니다. 홀드아웃 동작이 학습셋과 겹치지 않는다는 사실은 사람 눈이 아니라 코드로 확인했습니다. 학습한 동작을 재생하는 것은 일반화가 아니라 암기이기 때문입니다.

두 팔은 프롬프트도 시드도 레퍼런스도 동일하고, 차이는 어댑터를 얹었는지 하나뿐입니다. 점수는 CLIP-I, 즉 생성 프레임의 이미지 임베딩과 레퍼런스 임베딩의 코사인 유사도입니다. 마스코트는 얼굴이 없는 경우가 많아 ArcFace 계열 얼굴 지표를 쓸 수 없고, 그 자리를 대신하는 것이 CLIP-I입니다.

A/B 결과 캐릭터 다섯 종, 학습에 없던 동작 여섯 개씩. 회색이 베이스, 파랑이 어댑터입니다.

숫자를 보시면 어댑터가 다섯 종 모두를 이겼습니다. 클립 단위로는 서른 번 중 스물여덟 번입니다. 그런데 이 표에서 정작 중요한 것은 파란 막대가 아니라 회색 막대입니다.

캐릭터 어댑터 베이스 차이
metis 0.762 0.427 +78.3%
velox 0.712 0.362 +96.8%
dodam 0.653 0.366 +78.4%
aegis 0.457 0.365 +25.4%
haram 0.419 0.353 +18.5%

베이스는 캐릭터가 무엇이든 0.353에서 0.427 사이에 모여 있습니다. 물방울 모양이든 삼각 날개든 궤도 위의 상자든 점수가 거의 같습니다. 레퍼런스 네 장을 성실히 조건으로 넣어 줘도 모델이 그 캐릭터를 그리지 않고 자기 평균으로 돌아온다는 뜻입니다. 레퍼런스는 분위기를 옮기지 정체성을 옮기지 않습니다.

이 사실이 왜 중요하냐면, 캐릭터가 흔들릴 때 대부분의 팀이 시도하는 처방이 정확히 그 방향이기 때문입니다. 레퍼런스를 여섯 장으로 늘리고, 프롬프트에 특징을 더 자세히 적고, 시드를 더 뽑아 고릅니다. 회색 막대가 평평하다는 것은 그 방향에 천장이 있다는 이야기입니다.

프롬프트를 조이는 것으로는 움직이지 않았습니다

베이스의 한계를 확인하기 전에, 저희도 프롬프트 쪽을 먼저 밀어 봤습니다. 자기일관성, 즉 같은 캐릭터를 여러 번 뽑았을 때 서로 얼마나 닮았는지를 재고 세 가지 개입을 차례로 넣었습니다. 값은 낮을수록 일관됩니다.

개입 사족보행 캐릭터 육각 별 캐릭터
기준 (시드 12개) 0.337 0.191
시드를 32개로 늘림 0.315 0.223
종과 팔다리 수를 못박음 0.305 0.244
자세를 정면으로 고정 0.154 0.210

시드를 세 배 가까이 늘려도 분산이 그대로였습니다. 표본을 늘렸는데 분산이 안 줄면 분산의 출처가 표본이 아니라는 뜻입니다. “네발 큰 고양이, 몸길이는 키의 두 배, 짧은 주둥이, 개도 도마뱀도 공룡도 아님”까지 적어도 0.315에서 0.305로 움직였을 뿐입니다.

의미 있게 움직인 것은 자세를 고정했을 때 하나뿐이었고, 이건 사실 캐릭터가 좋아진 게 아니라 저희 측정기가 자세와 정체성을 섞어서 재고 있었다는 발견이었습니다. 사족보행 캐릭터는 서 있는 방향만 바뀌어도 실루엣이 통째로 달라집니다. 이족 정면 캐릭터가 애초에 이 문제를 겪지 않았던 이유도 같습니다. 자세 자유도가 없었으니까요.

여기서 얻은 실무적 교훈이 두 개입니다. 첫째, 레퍼런스와 평가용 렌더는 자세를 고정해서 뽑아야 합니다. 그러지 않으면 자세 변동을 정체성 실패로 잘못 읽습니다. 둘째, 정체성은 프롬프트로 지시해서 얻는 것이 아닙니다. 이게 어댑터를 학습시킨 이유입니다.

편집은 정체성을 훨씬 잘 붙듭니다

그런데 학습만이 답은 아니었습니다. 레퍼런스 이미지를 조건으로 주고 동작만 바꾸는 편집을 태워 보니 숫자가 더 좋았습니다.

정면 컷 한 장을 앵커로 두고 프롬프트에는 정체성을 일절 쓰지 않았습니다. “레퍼런스 이미지의 캐릭터를 그대로 유지하고 자세만 바꿔라”만 지시하고, 손 흔들기부터 뒷모습, 측면, 앉기까지 열 가지 동작으로 160장을 만들었습니다. 결과는 CLIP-I 0.823입니다.

경로 CLIP-I 무엇을 만드나
레퍼런스 편집 0.823 스틸
학습된 어댑터 0.42 ~ 0.76 81프레임 영상
베이스 0.35 ~ 0.43 81프레임 영상
레퍼런스 자기유사도 0.96 지표의 상한

같은 척도에 올려 두긴 했지만 두 경로는 다른 과제입니다. 한쪽은 스틸 한 장을 편집하고 다른 쪽은 81프레임 영상을 생성합니다. 그대로 빼서 비교하면 안 됩니다. 다만 정체성 축만 놓고 보면 편집이 뚜렷하게 앞서고, 이 격차가 파이프라인 설계를 바꿨습니다.

정체성은 편집이 잡고, 영상은 어댑터가 잡습니다. 경쟁 관계가 아니라 분업입니다. 정면 캐릭터를 한 번 잘 뽑아 두고 표정과 포즈와 시점은 편집으로 파생한 다음, 편집이 못 하는 영역인 영상과 시간축 일관성에만 어댑터를 씁니다.

flowchart TB
    A["정면 캐릭터 1장<br/>텍스트→이미지"] --> B["편집으로 파생<br/>표정·포즈·시점"]
    B --> C["대표성 기준으로<br/>레퍼런스 4장 선별"]
    C --> D["동작 클립 12개<br/>레퍼런스→영상"]
    D --> E["어댑터 학습<br/>LoRA"]
    E --> F{"홀드아웃 A/B<br/>베이스를 이기나"}
    F -->|이김| G["카탈로그 등록"]
    F -->|짐| H["등록하지 않음"]
    B -.정체성 0.823.-> B
    E -.정체성 0.42~0.76.-> E

색은 hex로 지시되지 않습니다

정체성이 형태만의 문제는 아닙니다. 색이 새는 현상도 같이 쟀는데, 원인이 예상 밖이었습니다.

팔레트 준수율을 캐릭터별로 재 보니 두 캐릭터가 두 모델 모두에서 20%대로 떨어졌습니다. 처음에는 모델 문제로 보였지만, 실패한 두 캐릭터가 공교롭게도 팔레트가 어두운 두 캐릭터이기도 했습니다. 음영이 들어가면 어두운 색은 밝은 색보다 색공간에서 더 멀리 밀려납니다. 즉 스펙을 의심하기 전에 측정기를 의심할 이유가 있었습니다.

명도를 무시하고 색상만 비교하도록 지표를 바꾸자 둘이 갈렸습니다. 한쪽은 22%에서 99.7%로 뛰었고, 다른 한쪽은 23%에서 26.5%로 거의 움직이지 않았습니다. 앞은 측정기가 어두운 색을 벌준 것이었고, 뒤는 진짜 색상 이탈이었습니다.

진짜 이탈의 원인은 프롬프트가 색을 이렇게 넘기고 있었다는 데 있었습니다.

#24305E is the dominant colour covering the largest share of the body;
#0C1330 is secondary; #8FA3D6 only as small trim.

텍스트를 이미지로 만드는 모델에게 hex 코드는 색 지시가 아니라 가끔 색과 운이 맞는 토큰입니다. 통과한 캐릭터들은 팔레트가 흔한 색 이름 근처에 있어서 통과한 것이었고, 실패한 캐릭터는 초록과 빨강 변종까지 냈습니다.

hex를 말로 번역해서 넘기자 같은 캐릭터가 26.5%에서 43.5%로 올랐습니다. 1.6배지만 여전히 절반 미만인데, 남은 원인은 팔레트 자체에 있었습니다. 주색과 보조색의 색상각 차이가 0.7도, 즉 같은 색상의 명도 차이였습니다. 모델에게 구분할 근거가 없으면 자기 색을 씁니다.

여기서 얻은 규칙은 두 개입니다. 색은 hex가 아니라 이름으로 넘기고 hex는 뒤에 앵커로 붙입니다. 그리고 팔레트를 검사할 때 문자열이 아니라 색상각으로 충돌을 판정합니다. 처음에는 문자열을 비교했는데 “dark blue”와 “very dark blue”는 서로 다른 문자열이라 그대로 통과했습니다.

형태가 복잡하면 편집도 흔들립니다

편집이 만능은 아니었습니다. 캐릭터별로 보면 편차가 큽니다.

원뿔 볏을 가진 캐릭터는 0.878, 부유형과 사족보행은 0.82대인데, 팔다리 여섯 개가 방사형으로 뻗은 별 모양 캐릭터만 0.651로 떨어집니다. 점수가 낮은 동작 상위 여섯 개를 뽑아 보니 전부 이 캐릭터였습니다. 편집이 팔의 개수와 각도를 자주 바꿔 놓기 때문입니다.

같은 캐릭터가 텍스트 생성에서도 자기일관성이 가장 나빴다는 점을 함께 보면 방향이 분명합니다. 형태의 자유도가 많을수록 어느 경로로 만들든 흔들립니다. 팔다리 개수처럼 세기 쉬운 속성이 프롬프트에서 “여섯 개 이상”처럼 열려 있으면 매번 다른 수가 나옵니다.

캐릭터를 설계할 때 실루엣이 구분되는지만 보기 쉬운데, 그것 못지않게 형태가 몇 개의 자유도를 갖는지도 봐야 합니다. 자유도가 적은 형태는 어느 도구로도 잘 재현되고, 많은 형태는 어느 도구로도 흔들립니다.

캐스팅 전체로 넓히면 더 불편한 사실이 나옵니다. 형태 후보 여덟 개를 캐릭터 넷에 물려 각각 여덟 시드로 뽑고 색을 지운 채 비교했더니, 여덟 중 다섯만 서로 구분됐습니다. 나머지 셋은 전부 같은 휴머노이드로 수렴했습니다.

더 중요한 것은 제품 축이 작동하지 않았다는 점입니다. 서로 다른 제품에 같은 형태를 물리면 행별로 거의 같은 그림이 나왔습니다. 독수리 아키타입 캐릭터조차 같은 휴머노이드와 같은 덩어리를 냈습니다. 프롬프트의 제품 설명은 질감과 장식에는 닿지만 형태에는 닿지 않습니다.

그래서 형태 후보를 열두 개 더 만들어 다시 뽑았고 그중 열하나가 구분됐습니다. 새로 통과한 것들은 공통점이 있습니다. 별처럼 팔이 방사형으로 뻗거나, 밑변이 다리가 아니라 직선이거나, 다리 없이 그림자와 간격을 두거나, 몸이 마름모처럼 납작합니다. 전부 외곽선을 바꾸는 변화입니다.

반대로 배에 구멍을 뚫는 변화는 시트에서는 선명했지만 실루엣에서는 사라졌습니다. 구멍이 안쪽에 있어 외곽선을 바꾸지 않기 때문입니다. 저희가 그 시트를 실루엣 판정이라고 부르며 봤던 것이 착오였습니다.

한 가지 더 있습니다. 표면 질감은 정체성 축이 아닙니다. 손으로 그린 느낌인지 클레이인지 플랫 벡터인지는 색을 지우는 순간 사라집니다. 저희가 캐릭터 여덟 종에 서로 다른 골격을 배정한 이유가 이것입니다. 같은 골격에 다른 질감을 입힌 두 캐릭터는 실루엣만 놓고 보면 같은 캐릭터입니다.

임계값은 노이즈 위에서 정해야 합니다

캐릭터끼리 얼마나 구분되는지를 재면서 임계값을 0.18로 잡았습니다. 근거는 없었고, 그 값 아래면 같은 캐릭터라고 판정했습니다. 뒤늦게 같은 캐릭터의 시드끼리 거리를 재 보고서야 문제를 알았습니다.

같은 캐릭터의 시드 간 거리는 중앙값 0.102였고, 가장 심한 캐릭터는 0.337이었습니다. 서로 다른 캐릭터 사이의 최소 거리는 0.114였습니다. 두 분포가 겹칩니다. 사족보행 캐릭터는 자기 자신과의 거리가 서로 다른 어떤 캐릭터 쌍보다도 멀었습니다. 그러니 0.114를 “같은 실루엣”이라고 부른 판정들은 신호가 아니라 노이즈를 재고 있었던 셈입니다.

질문의 순서가 틀렸던 것입니다. “이 둘이 다른가”는 “이것이 두 번 같은가” 다음에 오는 질문입니다. 자기일관성이 확보되지 않은 캐릭터는 애초에 비교 대상이 아닙니다. 게이트를 그 순서로 바꾸고, 자기 분산이 큰 캐릭터는 쌍별 비교에서 아예 제외하도록 했습니다.

임계값을 쓰는 지표를 만드실 때 권하고 싶은 순서가 이것입니다. 먼저 같은 조건을 반복했을 때의 분산을 재고, 그 위에서 임계값을 정하십시오. 반대로 하면 상수 하나가 실험 전체의 결론을 조용히 정하게 됩니다.

조건이 안 붙어도 잡은 성공으로 끝납니다

이 결론에 도달하기까지 A/B를 두 번 버렸습니다. 원인은 모델도 데이터도 아니었습니다.

레퍼런스 조건 샘플링에서 레퍼런스가 붙지 않으면 잡이 실패하지 않습니다. 로그에 경고 한 줄이 남고 나머지는 정상적으로 끝납니다.

[refs] h6: 0 stills []
[refs] WARNING h6 has no reference stills in . — that clip will render unconditioned
[gen 1/6] h1 -> h1.mp4 (81f, refs=0, ..., 224.4s)
...
VERDICT: GO

여섯 클립이 전부 조건 없이 렌더됐는데 잡은 GO로 끝났고 산출물도 멀쩡해 보였습니다. 그대로 채점했다면 어댑터와 베이스가 둘 다 조건 없이 그린 결과를 비교한 셈이고, “학습이 효과 없다”는 정반대 결론이 나왔을 겁니다. 실제로 두 번 그렇게 읽힐 뻔했습니다.

원인은 두 겹이었습니다. 레퍼런스가 페르소나 단위로 해석되는데 슬러그 접두사가 페르소나로 잡혀 파일명과 어긋났고, 레퍼런스 디렉터리 경로도 틀렸습니다. 둘 다 기존 실행 스크립트에 명시돼 있던 값이었습니다. 읽고도 따르지 않았던 것이 문제였습니다.

그래서 이 확인을 사람의 기억이 아니라 파이프라인에 넣었습니다. 채점 직전에 로그에서 부착 증거를 읽고, 레퍼런스가 0장인 팔은 채점 자체를 거부합니다.

def refs_attached(run: str) -> bool | None:
    """로그가 말하는 부착 여부. None은 확인 불가이지 통과가 아니다."""
    log = pod_logs(run)
    for line in log.splitlines():
        if line.startswith("[refs]") and "stills" in line:
            n = line.split(":")[1].strip().split()[0]
            return n.isdigit() and int(n) > 0
    return None

일반화하면 이렇습니다. 조건부 입력은 안 붙어도 조용합니다. 레퍼런스 이미지, 마스크, 컨트롤넷 힌트, 시스템 프롬프트 조각 모두 마찬가지입니다. 이런 입력을 쓰는 파이프라인은 산출물이 아니라 로그에서 부착을 확인해야 하고, 그 확인은 자동화된 자리에 있어야 합니다.

파이프라인에 가져가실 것

가장 먼저 하실 일은 베이스의 바닥선을 재는 것입니다. 레퍼런스를 주고 캐릭터를 요청했을 때 지금 몇 점이 나오는지 모르면 얼마나 개선해야 하는지도 모릅니다. 저희는 0.35에서 0.43이었고, 이 숫자 하나가 학습에 GPU를 태울 근거가 됐습니다. 반대로 그 숫자가 이미 0.8이었다면 학습할 이유가 없었을 겁니다.

평가는 학습에 쓰지 않은 동작으로만 하십시오. 학습한 동작을 재생하는 것은 일반화가 아니라 암기이고, 그 차이는 점수에 그대로 드러납니다. 겹치지 않는다는 사실을 눈으로 확인하면 언젠가 놓칩니다. 저희는 홀드아웃을 만들 때 학습셋과의 교집합이 비어 있는지를 코드가 단언하게 했습니다.

지표를 고르실 때는 그 실험에서 무엇이 변하도록 설계했는지를 먼저 보십시오. 동작이 변하는 실험에서 실루엣 거리를 재면 실험 설계 자체를 실패로 읽습니다. 저희가 이 실수를 이틀 연속 했습니다. 자세와 정체성을 섞어서 잰다고 전날 문서에 적어 놓고 다음 날 또 그 지표로 편집본을 쟀습니다.

스틸과 영상에는 다른 도구를 쓰시는 편이 낫습니다. 정체성은 편집이 잘 붙들고 시간축은 어댑터가 담당합니다. 하나로 둘 다 해결하려고 하면 둘 다 어정쩡해집니다.

마지막으로 조건부 입력의 부착은 로그에서 확인하시고, 그 확인을 사람이 아니라 파이프라인에 두십시오. 기억에 맡기면 놓치고, 놓쳤을 때 나오는 결론은 단순히 틀린 게 아니라 정확히 반대 방향입니다.

ThakiCloud에서는

이 실험은 전부 사내 B200 클러스터에서 돌았습니다. 이미지 생성, 편집, 영상 생성, LoRA 학습, 평가까지 외부 API 없이 같은 환경 안에서 끝납니다. 캐릭터 여덟 종의 클립 생성과 어댑터 학습이 하루 안에 병렬로 돌아간 것도 GPU를 큐에 그대로 밀어 넣을 수 있었기 때문입니다.

모델 가중치는 사내 오브젝트 스토리지에서 내부망으로 당깁니다. 실측으로 파드에서 초당 1GB 정도가 나오는데, 같은 파일을 외부에서 받으면 초당 6MB 수준입니다. 53GB짜리 편집 모델을 매 잡마다 새로 받아야 하는 파이프라인에서는 이 차이가 실험을 할 수 있느냐 없느냐를 가릅니다.

학습이 끝난 어댑터는 모델 카탈로그에 등록해 다음 서빙에서 바로 쓸 수 있게 합니다. 다만 저희는 A/B에서 베이스를 이긴 어댑터만 등록합니다. 진 실험까지 등록하면 카탈로그가 “무엇을 서빙해야 하는가”에 답하지 못하게 되기 때문입니다. 이 판정은 사람의 판단이 아니라 등록 시점의 코드 게이트가 수행합니다.

Metis가 추론과 서빙 계층이고 Maxis가 학습과 평가 계층인데, 이번처럼 생성에서 학습을 거쳐 평가까지 한 흐름으로 이어지는 작업이 두 계층의 이음매를 가장 잘 드러냅니다. 실제로 이번에 걸린 문제들은 모델이 아니라 대부분 이음매에 있었습니다.

남은 질문

어댑터의 이득 폭이 캐릭터마다 크게 달랐습니다. 물방울 형태는 97% 올랐고 원뿔 볏은 19% 올랐습니다. 베이스가 이미 어느 정도 잡는 형태일수록 어댑터가 보탤 여지가 적다는 해석이 자연스럽지만, 다섯 종의 상관으로 순위까지 설명하기는 어렵습니다. 캐릭터를 늘려 다시 볼 생각입니다.

편집의 상한도 아직 모릅니다. 0.823은 레퍼런스 자기유사도 0.96과 거리가 있고, 그 간격이 편집의 한계인지 저희 프롬프트의 한계인지 갈라 보지 않았습니다.

같은 문제를 겪고 계시다면 베이스의 바닥선부터 재 보시길 권합니다. 저희도 그 숫자를 보고 나서야 무엇을 고쳐야 할지 알았습니다.

태그: character-consistency, clip-i, 평가, image-editing, lora, reference-conditioning

카테고리:

업데이트: