· No.2
색 수가 안 맞은 건 그림 탓이 아니었다 — 오진 하나가 리롤을 반복시킨 이틀
스프라이트 색 수 초과의 원인은 작화가 아니라 다운스케일 보간이었다. 그 밖에 공간 해시 400마리 0.52ms, 로컬 TTS의 655초 폭주, 생성 모델의 크로마키 배경색 표류까지 — 개소 첫 사이클에서 실제로 측정된 것들.
우리는 1인 규모의 AI 스튜디오이고, 게임 한 편과 웹 콘솔과 SNS 콘텐츠를 동시에 만들면서 매일 이미지 생성·LLM·TTS·에이전트 오케스트레이션을 실제로 돌린다. 그래서 여기 적는 건 "이 기법이 좋다더라"가 아니라 우리 GPU 한 장에서 나온 수치와, 틀렸던 판단들이다.
개소 첫 사이클(2026-08-05 ~ 08-06 연속 세션)에서 가장 값비쌌던 발견은 이거다. 스프라이트 색 수가 상한을 넘긴 원인이 그림이 아니라 후처리의 축소 보간이었다. 원본이 4톤인 이미지가 파이프라인을 통과하면 9~13색이 됐다. 우리는 그걸 모델 탓으로 보고 프롬프트에 톤 제한 문구를 계속 덧붙이며 리롤을 돌렸다. 프롬프트로는 절대 잡히지 않는 문제였다.
같은 사이클의 다른 수치들: 적 400마리 충돌 질의 0.52ms vs 0.96ms, 로컬 TTS의 문장당 94초 · 최악 655초, 이미지 114컷 생성 중 11컷 채택.
색 수 초과의 범인은 작화가 아니라 BOX 축소였다
무엇을 시도했나
우리 도트 규격은 스프라이트당 색 수 8색 이하(보스만 12색)이고, 이건 눈으로 보는 기준이 아니라 후처리 스크립트가 자동 판정해 초과분을 reject/로 격리하는 하드 게이트다.
문제는 스켈레톤이었다. 프롬프트에 이렇게까지 못 박아 뒀는데도 계속 리젝됐다.
an animated skeleton warrior standing upright, ...
exactly two flat tones only: pale bone white and one darker grey shadow tone,
no cloth, no armour, no weapon, extremely simple, minimal detail,
hollow dark eye sockets as the only dark accent생성 원본은 실제로 4톤 수준이었다. 그런데 파이프라인을 통과한 16×24 결과물은 9~13색. 우리의 첫 가설은 "모델이 지시를 안 지킨다"였다. 그래서 톤 제한 문구를 더 강하게 쓰고, 시드를 바꿔 리롤을 반복했다. 색 수는 줄지 않았다.
어떻게 측정했나
가설을 버리고 파이프라인 단계별로 색 수를 셌다. 후처리(scripts/pixelize.py)는 9단계다.
| 단계 | 처리 |
|---|---|
| 1 | 마젠타 크로마 키 + 스필 제거 → 알파 생성 |
| 2 | 콘텐츠 바운딩박스 크롭 |
| 3 | BOX 4배 축소 → NEAREST 확정 축소 |
| 4 | 발밑 앵커 정렬 (하단 중앙) |
| 5 | 알파 이진화 (임계 128) |
| 6 | 지정 팔레트(Resurrect 64) 강제 양자화, 디더링 off |
| 7 | 색 수 상한 초과 시 reject/ 격리 |
색 수를 세는 지점은 7단계, 즉 전경 픽셀의 고유 RGB 개수다. 원본이 4톤인데 7단계에서 9~13색이면 색은 1~6단계 어딘가에서 늘어난 것이다. 생성 단계가 아니라. 이 한 줄을 확인하는 데 이틀이 걸렸다.
3단계가 범인이다. BOX는 면적 평균 보간이라 두 톤이 만나는 경계마다 중간색을 만든다. 16px 캔버스로 ÷64 축소를 하면 경계 픽셀 비중이 커서 중간색이 대량으로 생긴다. 그리고 6단계의 팔레트 스냅은 그 중간색들을 각각 가장 가까운 서로 다른 64색 슬롯으로 확정해 버린다. 뭉개진 경계 하나가 색 슬롯 하나가 되는 구조였다.
BOX 축소 자체는 잘못이 아니다. NEAREST 한 번으로 ÷64를 처리하면 픽셀 하나를 원본 1px에서 샘플링해 노이즈가 그대로 박히기 때문에 2단 축소(BOX → NEAREST)는 유지해야 한다. 문제는 축소가 아니라 축소 뒤에 팔레트 스냅이 온다는 순서였다.
결과
--reduce 플래그를 만들어 순서를 바꿨다. 팔레트 스냅 전에 적응형 k색으로 먼저 줄인다.
# scripts/pixelize.py — --reduce 경로
if reduce:
work = arr[..., :3].copy()
if (alpha > 0).any(): # 투명부가 팔레트 슬롯을 먹지 않게 전경 중앙값으로 덮는다
work[alpha == 0] = np.median(work[alpha > 0], axis=0).astype(np.uint8)
ada = Image.fromarray(work, "RGB").quantize(
colors=max_colors, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE)
rgb_out = snap_palette(np.array(ada.convert("RGB")), pal_colors)
else:
flat = Image.fromarray(arr[..., :3], "RGB").quantize(palette=pal_img, dither=Image.Dither.NONE)
rgb_out = np.array(flat.convert("RGB"))| 기존 경로 (팔레트 직접 양자화) | --reduce (적응형 선축소 → 팔레트 스냅) | |
|---|---|---|
| 스켈레톤 원본 톤 수 | 4 | 4 |
| 후처리 후 색 수 | 9~13 (리젝) | ≤ 8 (상한 보장) |
| 리젝 해소 수단 | 프롬프트 수정 + 리롤 (효과 없음) | 플래그 하나 |
| 원본 재생성 필요 | 있음 | 없음 |
상한이 "보장"되는 이유는 알고리즘상이다. MEDIANCUT이 정확히 max_colors개로 줄이고, 팔레트 스냅은 색을 합칠 수는 있어도 늘리지 못한다. 확률이 아니라 구조로 막힌다.
두 가지 실무 디테일이 따라왔다.
- 디더링은 항상 off. 켜면 정수 배율 업스케일(도트는 ×2, ×3으로 확대해 보여준다) 시 모아레가 생긴다.
- 투명부를 전경 중앙값으로 덮은 뒤 양자화한다. 안 그러면 알파 0 영역의 잔여 RGB가
max_colors슬롯 중 하나를 통째로 먹어서, 실제 스프라이트가 쓸 수 있는 색이 하나 줄어든다.
다음 단계
- 선축소를 표준 공정으로 승격했다 (
docs/design/02_STYLE_GUIDE.md§4.4). 이제--reduce가 기본 경로다. - 일반화한 규칙 하나: 생성형 파이프라인에서 지표가 나쁘면 생성 단계를 의심하기 전에 후처리 단계를 계측하라. 생성은 확률적이라 "모델 탓"이 언제나 그럴듯해 보인다. 그래서 오진의 기본값이 되고, 리롤은 비용이 눈에 안 띄게 새는 형태(GPU 시간 + 사람의 판정 시간)로 쌓인다.
- 아직 못 한 것: 선축소가 실루엣 가독성에 주는 영향은 측정하지 못했다. 적응형 k색이 경계를 어디서 자르는지는 컷마다 다르므로, 16px 실크기 ×12 확대본으로 종 식별이 되는지는 여전히 육안 게이트로 남아 있다.
크로마키 배경색은 지정해도 그 색으로 안 나온다
무엇을 시도했나
배경 제거는 마젠타 크로마키로 한다. 프롬프트에 plain solid magenta background를 넣고 후처리에서 #FF00FF 기준으로 키를 잡는 방식이었다. 표준적이고, 그래서 의심하지 않았다.
키잉 실패 컷이 계속 나왔다. 어떤 컷은 배경이 안 빠지고, 어떤 컷은 캐릭터가 통째로 사라졌다.
어떻게 측정했나
실패 컷의 배경 픽셀 RGB를 직접 읽었다. 지시한 값은 (255, 0, 255) 하나인데, 실제로 나온 값은 컷마다 달랐다 — (226, 17, 245) ~ (217, 24, 153) 범위로 흘렀다.
이게 왜 치명적이냐면, 크로마 키는 지정색과의 RGB 유클리드 거리로 판정하기 때문이다. 허용 거리(--tol, 0~441 스케일에서 기본 100)를 고정해 두면 어떤 컷은 그 안에 들어오고 어떤 컷은 안 들어온다. 컷 단위로 성공/실패가 갈리는데, 원인이 프롬프트에 안 보인다.
두 번째 증상은 더 고약했다. 배경이 미세 그라데이션일 때 키잉이 모서리에 점 몇 개를 남기는데, 그 점이 콘텐츠 바운딩박스에 포함된다. 실측: 실제 피사체가 프레임의 15%인 컷의 bbox가 99.8%로 잡혔다. bbox는 축소 배율과 발밑 앵커 위치를 결정하므로, 점 하나가 스프라이트 크기와 정렬을 통째로 망친다.
결과
세 가지를 코드로 바꿨다.
① 배경색을 컷별로 자동 검출한다. 네 모서리 패치(각 변의 4%)의 중앙값을 배경색으로 본다. 피사체는 항상 중앙에 오도록 프롬프트가 통제하므로 모서리는 배경으로 안전하다.
def detect_chroma(rgb, frac=0.04):
h, w = rgb.shape[:2]
ph, pw = max(2, int(h * frac)), max(2, int(w * frac))
patches = np.concatenate([
rgb[:ph, :pw].reshape(-1, 3), rgb[:ph, -pw:].reshape(-1, 3),
rgb[-ph:, :pw].reshape(-1, 3), rgb[-ph:, -pw:].reshape(-1, 3),
])
return tuple(int(v) for v in np.median(patches, axis=0))② 연결 성분으로 스페클을 제거한다. 최대 덩어리의 2% 미만인 성분을 버린다. "최대 성분만 남기기"가 아니라 상대 크기로 거른 이유는, 궤도 링 아이콘처럼 정상적으로 분리된 부위가 있기 때문이다. 그걸 지우면 아이콘이 반쪽이 된다.
③ 키잉 실패 판정 기준을 bbox에서 면적으로 바꿨다.
if a.mean() > 0.92 * 255: # 전경 '면적' 기준. bbox 로 재면 점 몇 개에 오판정된다
return None, f"reject:key_failed(foreground>92% of frame, chroma={chroma})"| 이전 | 이후 | |
|---|---|---|
| 배경색 | #FF00FF 고정 | 컷별 모서리 중앙값 자동 검출 (--chroma auto가 기본) |
| 스페클 | 방치 → bbox 오염 | 최대 성분의 2% 미만 제거 |
| 실패 판정 | bbox 면적 | 전경 알파 면적 92% |
| 증상 | 컷마다 랜덤하게 실패, 크기·앵커 붕괴 | 실패 컷이 사유와 함께 격리됨 |
다음 단계
- 일반 규칙: 생성 모델에 지정한 정확한 값이 출력에 그대로 나온다고 가정하지 말 것. 색, 각도, 개수 전부 해당한다. 후처리는 지시값이 아니라 관측값을 기준으로 짜야 한다.
- 마젠타 스필(외곽에 남는 자주색 테두리)은 despill 로직으로 눌러 두었지만 완전하지 않다. 심해지면 배경색을
#00FF00으로 바꿔 재생성하는 대안을 남겨 뒀다 — 아직 필요하지 않았다. - 우리 프롬프트에 남은 방어 문구들이 사실상 리롤 이력의 화석이다. 실제 사용 중인 것들:
not diagonal, not tilted, not rotated(투사체가 자꾸 기울어서),absolutely no purple and no pink and no magenta(금색 젬이 배경색에 물들어서),clearly lighter than black(실루엣이 배경과 안 갈려서). 이 문구들이 어느 시점부터는 효과가 없어진다는 게 이번 글의 첫 섹션 결론이다.
300마리 60fps를 물리 엔진 없이: 공간 해시 실측
무엇을 시도했나
만들고 있는 게임은 뱀서라이크라 화면에 적이 수백 마리 있다. 충돌 질의를 Unity Physics2D 오버랩으로 갈지 공간 해시를 직접 구현할지 W1 첫 반나절에 정해야 했다.
기획 문서에 미리 박아 둔 원칙이 있었다.
Jobs/Burst·ECS 선반영 금지 — 공간 해시 + 풀링으로 충분, 프로파일러가 증명할 때만 격상.
이 원칙은 최적화를 미리 하지 말라는 뜻이면서, 동시에 추측으로 고르지도 말라는 뜻이다. 그래서 스파이크를 먼저 돌렸다.
어떻게 측정했나
- 동일 씬·동일 적 배치에서 두 방식만 교체해 비교
- 400마리로 비교 측정, 게이트 판정은 300마리에서
- Unity batchmode 오프스크린 실행, 프레임당 충돌 질의 CPU 시간
- 통과 기준: 300마리에서 60fps 유지 = 프레임 예산 16.7ms
결과
| 방식 | 400마리 avg | 300마리 avg | 300마리 p95 |
|---|---|---|---|
| 공간 해시 (채택) | 0.52ms | 0.521ms | 0.952ms |
| Unity Physics2D 오버랩 | 0.96ms | — | — |
300마리 기준 프레임 예산의 3%. 게이트를 큰 여유로 통과했고, 남은 3주 일정의 최대 기술 리스크가 여기서 해소됐다. 커밋 6d71a7c, 테스트 7/7.
수치 해석에서 조심해야 할 두 가지를 같이 적는다.
- 이건 "물리 엔진이 느리다"는 뜻이 아니다. Physics2D는 레이어 마스크·트리거 콜백·연속 충돌 같은 범용 기능을 다 갖춘 시스템이고, 우리가 필요한 건 "반경 안의 적 목록" 하나뿐이다. 쓰지 않는 기능의 값을 안 낸 것이지, 엔진이 못 만들어진 게 아니다. 요구가 하나로 좁혀질 때만 자체 구현이 이긴다.
- 이 수치는 오프스크린 CPU 시간이라 실빌드 렌더 비용이 빠져 있다. 실제 프레임에는 스프라이트 배칭·드로콜이 더해진다. W2 초에 실빌드로 1회 재측정해야 게이트 통과가 최종 확정된다. 지금은 잠정 통과다.
두 번째 항목은 우리에게 유리한 수치가 나왔을 때일수록 꼭 적어야 하는 종류의 단서다.
다음 단계
- 셀 크기는 1.0유닛으로 고정. 히트박스 반경은 소형 0.28 / 표준 0.35 / 중형 0.45 / 보스 1.10이라 셀 하나에 표준 크기 기준 여유가 남는다.
- W2 초 실빌드 재측정. 여기서 예산 초과가 나오면 그때 프로파일러 근거를 갖고 Jobs/Burst를 검토한다. 순서가 중요하다 — 증명 전에 격상하지 않는다.
- 같은 사이클에 이전 코드 버그 9건을 수정했고, 그중 하나가 배칭 파괴였다. 렌더 비용 재측정 때 이 수정분이 같이 반영된 상태로 재야 한다.
로컬 TTS 655초를 클라우드 1~2초로 되돌린 판단
무엇을 시도했나
우리 본부 에이전트("레이")는 음성으로 응답한다. 회사 원칙이 로컬 우선(비용 0, 데이터 외부 유출 0) 이라 로컬 TTS 스택(Voicebox + 자체 음성)으로 시작했다.
어떻게 측정했나
문장 단위 생성 시간을 실측하고, 웹 콘솔의 /api/tts 왕복 지연으로 체감 지연을 쟀다. GPU 상주 메모리도 같이 봤다 — GPU는 12GB 한 장뿐이고 이미지 생성 파이프라인과 공유하기 때문이다.
결과
| 로컬 TTS (CPU 백엔드) | Microsoft Edge 뉴럴 TTS (ko-KR-SunHiNeural) | |
|---|---|---|
| 문장당 생성 | 94초 | 1~2초 |
| 최악 사례 | 655초 (짧은 문장에서) | 관측 없음 |
| GPU 상주 | 4.3GB | 0 |
| 반복 문구 (문장 캐시) | — | 0.4초 |
| 안정성 | 서버 수시 다운 | 안정 |
세 가지가 결정적이었다.
- 655초는 짧은 문장에서 나왔다. 길이에 비례한 저하가 아니라 예측 불가능한 폭주라 "길이 상한" 같은 안전장치로 막을 수 없다. 최악값을 상정할 수 없는 컴포넌트는 대화형 경로에 둘 수 없다.
- 첫 콘솔 테스트는 HTTP 502였다. 원인은 게이트웨이 타임아웃 45초 < 생성 94초. 이때 타임아웃을 늘리는 건 증상 처리다. 45초 걸려도 대화가 아니다.
- 4.3GB 상주가 진짜 비용이었다. 12GB 한 장에서 TTS가 4.3GB를 점유하면 ComfyUI 이미지 생성이 밀려난다. 로컬 모델의 비용은 요금이 아니라 다른 작업을 못 하게 되는 기회비용으로 온다.
CUDA 백엔드로 전환해 실시간화를 시도했지만, 다운로드 중 로컬 TTS API가 응답 정지 상태가 됐다. 그 시점에 폐기를 결정했다.
다음 단계
- 규칙으로 정리했다: "로컬 우선"은 원칙이지 결론이 아니다. 대화형 지연 예산(1~2초)과 배치 작업 예산(밤새 돌려도 됨)은 다른 문제다. 같은 모델도 어느 경로에 있느냐에 따라 채택/폐기가 갈린다.
- GPU 교대 운영표에 상주 프로세스를 올리지 않는다는 항목을 추가했다. 로드/언로드가 명시적인 것만 12GB를 쓴다.
- 문장 캐시는 유지. 우리 에이전트는 정형 응답("확인했습니다", "작업 중입니다")이 많아 캐시 히트가 실제로 자주 난다.
한글은 이미지 모델에 그리게 하지 말고 코드로 조판하라
무엇을 시도했나
카드뉴스(1080×1350 인스타 규격)를 생성 모델로 만들려 했다. 배경·픽토그램은 물론이고 한글 텍스트까지 이미지 안에 그리는 방식으로 시작했다.
어떻게 측정했나
EP01을 3차까지 반복하며 수정 비용을 봤다.
- v1 단독 오브젝트 → v2 배경 + 포인트 → v3 실제 사실 반영
- 1차 출처(기사·공식 자료) 대조에서 사실 오류 3건 발견: 성벽 → 동물원 사육장 / 어깨 견장 → 목 예장 띠·훈장 / 사육사 동행 누락
즉 텍스트 한 글자를 고칠 때마다 이미지를 다시 생성해야 하는 구조였다. 오타 하나도, 사실 정정 하나도, A/B 문구 시험 하나도 전부 GPU 시간과 리롤 판정을 요구했다.
결과
한글을 이미지 생성에서 분리하고 코드 조판으로 옮겼다 (scripts/cardnews_compose.py). 배경·픽토그램만 생성 모델이 만들고, 문자는 전부 코드가 그린다.
| 이미지 모델이 한글까지 | 코드 조판 | |
|---|---|---|
| 오타 수정 비용 | 재생성 (GPU + 판정) | 0 |
| 사실 정정 비용 | 재생성 | 0 |
| A/B 문구 시험 | 컷마다 리롤 | 0 |
| 자소 분리·오식 위험 | 있음 (전량 육안 검수 필요) | 없음 |
부수적으로 발견한 실무 함정 하나: ComfyUI 워크플로우 JSON은 UI 형식과 API 형식이 다르다. 디자인 담당이 UI에서 내보낸 워크플로우를 서버가 받지 못해, API 형식으로 직접 작성해야 했다 (scripts/comfy_cardnews.py). 자동화하려면 처음부터 API 형식으로 만들어야 한다.
다음 단계
- 게이트를 세웠다: 1차 출처가 확인된 사실만 카드에 올린다. 2차 자료뿐인 항목은 대체 표기하거나 뺀다. 실제로 한 편에서 "종 수" 수치가 2차 자료여서 표기를 바꿨다.
- 소재 축은 이후 대표 지시로 게임 뉴스로 전환됐다. 포맷 골격(8장 구성·출처 카드·검수 게이트)은 그대로 이월.
- 남은 질문: 조판 품질(자간·줄바꿈)이 디자이너가 손으로 잡은 것과 어디서 갈리는지는 아직 안 재봤다.
"선례가 없다"는 확인이 아니라 가정이었다
무엇을 시도했나
우리 게임의 훅("정지하면 강해진다")이 얼마나 새로운지 검증하려 했다. 스토어 문구와 마케팅 전체가 이 신규성 위에 서 있어서, 착수 전에 확인이 필요했다.
어떻게 측정했나
장르 전수 조사는 불가능하다(뱀서라이크는 출시작이 너무 많다). 그래서 태그·시스템 단위로 검색 범위를 좁혀 부분 선례를 찾는 방식으로 갔다. "동일한 게임"이 아니라 "동일한 메커니즘 조각"을 찾는다.
결과
부분 선례가 나왔다. Brotato의 Stand Still 태그와 Soldier 캐릭터(정지 시 피해·공속 +50%)다. 신규성 가정이 절반만 맞았다.
같은 조사에서 나온 다른 수치 둘.
| 발견 | 수치 |
|---|---|
| Next Fest 성과 vs 페스트 이전 위시리스트 상관계수 | 0.825 |
| 한 사례 게임의 데모 중앙 플레이타임 / 런치 위시리스트 | 22분(우량 기준 38분 미달) / 17,826 |
첫 번째는 "페스트는 발견의 장이 아니라 증폭기"라는 뜻이다. 빈손으로 들어가면 증폭할 게 없다. 두 번째는 "데모의 깊이보다 스토어 페이지가 판다"는 쪽을 가리킨다.
다음 단계
- 차별점을 재정의했다. "정지 시 보너스"는 선례가 있으므로, 우리 문구는 보너스가 아니라 페널티·자폭 쪽으로 옮겨야 사실이 된다. 문구가 사실이 아니면 리뷰에서 그대로 돌아온다.
- 규칙으로 남긴 것: "선례를 못 찾음" ≠ "선례가 없음". 전수 조사가 불가능한 도메인에서 LLM 조사 결과를 "없다"로 단정하지 않는다. 조사 결과는 항상 "이 범위에서 못 찾았다"로 기록한다.
- 조사 작업은 이후 더 저렴한 모델로 내렸다 (조사=Sonnet / 제작=Opus). 조사는 검색·대조 위주라 큰 모델이 만드는 차이가 작다는 판단인데, 이 판단 자체는 아직 A/B로 검증하지 않았다.
서브에이전트에 모델을 명시하지 않아 작업 하나를 잃을 뻔했다
무엇을 시도했나
부서 에이전트를 병렬로 돌린다. 이미지·조사·구현·마케팅을 각각 다른 에이전트에 위임하고 본부가 종합하는 구조다.
어떻게 측정했나
측정이라기보다 사고 보고다. 웹 콘솔 v0.5 작업 중 담당 에이전트가 사용량 한도로 중단됐다. 원인을 추적해 보니 호출 시 모델을 지정하지 않았고, 그래서 부모 세션의 모델을 상속받아 가장 비싼 모델로 장시간 작업을 돌리고 있었다.
문제는 중단 자체가 아니라 중단 시점의 상태였다. 작업물이 커밋되지 않은 채로 멈췄다.
결과
두 가지 규칙을 세웠다.
- 서브에이전트 호출 시 모델을 항상 명시한다. 상속은 편해 보이지만, 위임한 작업의 비용을 위임한 쪽이 모르게 만든다. 실제 운용은 3단으로 고정했다 — 조사·리서치는 작은 모델, 제작·구현은 큰 모델, 최상위 모델은 대표 전용.
- 장시간 작업 에이전트에는 "기능 단위 중간 커밋"을 지시에 포함한다. 에이전트는 언제든 중단될 수 있다고 전제하고, 복구 지점을 남기는 것을 작업 지시의 일부로 만든다. 완성 후 커밋은 완성하지 못하면 아무것도 안 남는다.
해당 작업은 본부가 직접 이어받아 마무리했지만, 그건 운이 좋았던 것이다.
다음 단계
- 채택된 산출물을 (지시 → 결과) 쌍으로 부서별 데이터셋에 적립하기 시작했다. 현재 채택 3건·반려 1건. 반려도 사유와 함께 남긴다 — 나중에 무엇이 통과 기준이었는지는 반려 쪽에 더 선명하게 남는다.
- 아직 못 푼 것: 세션을 재접속할 때마다 인박스 감시 프로세스가 죽는다. 첫 사이클에서 두 번 수동 재가동했다. 세션 시작 루틴에 넣는 것으로 임시 대응 중이고, 근본 원인은 아직 모른다.
부록 — 한글 · 인코딩 · 윈도우
AI 도구 자체와는 무관하지만, 첫 사이클에서 가장 많은 시간을 먹은 건 인코딩이었다. 윈도우에서 한국어로 자동화를 돌리는 사람에게는 바로 쓸모가 있을 것 같아 남긴다.
| 증상 | 원인 | 규칙 |
|---|---|---|
한글 작업명으로 스케줄러 등록 실패 (0x8007007b) | BOM 없는 .ps1을 PowerShell 5.1이 ANSI로 읽음 | 한글이 든 .ps1은 UTF-8 BOM으로 저장 |
Unity가 manifest.json 해석 실패 (char 65279) | Set-Content -Encoding UTF8이 BOM을 삽입 | JSON은 BOM 금지. [IO.File]::WriteAllText(..., UTF8Encoding($false)) |
| 웹훅에 한글 페이로드 전송 시 HTTP 400 (영문은 204 성공) | Git Bash가 UTF-8 인자를 깨뜨림 | 한글 HTTP 전송은 PowerShell + [Text.Encoding]::UTF8.GetBytes() 경로로만 |
BOM 규칙이 파일 종류별로 정반대라는 게 핵심이다. .ps1은 BOM 필수, JSON은 BOM 금지. 하나의 습관으로 통일하려는 시도가 오히려 양쪽을 번갈아 깨뜨렸다.
오늘 걸린 것
- 팔레트 원본 파일 미배치.
resurrect64.hex가 없으면 후처리 스크립트가 즉시 중단된다. 후처리 전체가 이 파일 하나에 걸려 있다. - Z-Image VAE·int8 가중치 다운로드 미완. 워크플로우 로드는 되지만 실행이 안 된다.
- W1 성능 수치가 오프스크린 기준. 실빌드 렌더 비용 미포함 — 재측정 전까지 잠정 통과.
- 12GB 한 장 교대 운영. 상주 프로세스 하나가 파이프라인 하나를 밀어낸다는 걸 이번에 실측으로 확인했다.
- 이미지 채택률 9.6% (114컷 생성 / 11컷 채택). 이 수치를 어디까지 끌어올릴 수 있는지가 다음 사이클의 주요 지표다. 우리 가설은 스타일 LoRA로 리롤 배수를 ×5에서 ×2까지 내리는 것인데, 아직 학습 데이터가 기준선(40장)에 미달해 검증하지 못했다.