- P1 전용 스크립트를 docs/avatar-art/linocut-pipeline 으로 옮겨 persona.json 설정으로 일반화(P1 재실행 리그 바이트 동일) - 7명 외형·상징 설계(linocut-cast.md)와 P2~P7 정면 원화 생성 프롬프트, 얼굴 없는 화풍 참조 - P1 원화 생성 프롬프트 보존
8.5 KiB
공통 리노컷 아바타 파이프라인
아바타 v3 리노컷 리그(결정문 avatar-expression-engine-v3.md
§8.2 리그 원칙)의 자산 파이프라인이다. P1~P7 등 모든 페르소나가 이 공통 스크립트를 공유하고,
페르소나별 차이는 각 페르소나 폴더(예: ../p1-linocut/)의 persona.json에서만 읽는다.
결과물(레이어 픽셀, 알파, 게시된 WebP, 리그 TS)은 페르소나별 폴더 구조나 상수 위치가 바뀌었다는 이유로 달라지지 않는다 — 알고리즘은 건드리지 않았다(P1 회귀 검증: 아래 참고).
단계와 의존 순서
run_pipeline.py가 아래 순서로 각 단계를 독립 프로세스로 실행한다(괄호는 산출물):
landmarks.py— base-front.png 랜드마크 검출 (manifest.landmarks)brow_centerline.py—manifest.landmarks.eyebrowLeft/Right를 잉크 띠 중심선으로 보정segmentation.py—layers/{body,head,hairFront}.png(v1) + manifest 기준 섹션layers_v2.py—layers/v2/{body,head,hairFront}.png(턱 밑 띠·잔머리 halo 보정)face_detail.py—layers/v2/face-detail.pngpaper_grain.py—layers/v2/paper-grain.png(다른 단계와 독립, 순서 유연)lip_texture.py—layers/v2/lip-{upper,lower,shadow}.pngjaw_pieces.py—layers/v2/jaw-{head,detail}.pngexport_rig.py— WebP 게시(apps/web/public/avatar/v3/<publicSlug>/) +rigs/<rigFileName>생성final_previews.py— 게시된 WebP로 모션·눈/입 확대 미리보기
이 순서는 전달받은 작업 설명의 번호(주제별 묶음)와 다르다 — 특히 final_previews는
export_rig가 쓴 export-rig-report.json을 읽으므로 반드시 export_rig 다음이어야
한다. face_detail·jaw_pieces는 manifest.landmarks(12단계가 채움)와
4단계)가 먼저 있어야 한다.layers/v2/{head,hairFront,body}.png(3
실행
# 전체 실행
<venv>/python.exe run_pipeline.py <persona-dir>
# 한 단계만 다시 실행
<venv>/python.exe run_pipeline.py <persona-dir> --only face_detail
# 중간부터 끝까지
<venv>/python.exe run_pipeline.py <persona-dir> --from lip_texture
# 단계 이름 목록
<venv>/python.exe run_pipeline.py --list
각 단계 스크립트는 python <script>.py <persona-dir>로 단독 실행도 된다(디버깅용).
주의: landmarks.py만 혼자 다시 돌리면 manifest.landmarks를 통째로 새로 써서
brow_centerline.py가 보정한 눈썹 중심선이 사라진다. --only landmarks를 쓴 뒤에는
--only brow_centerline도 반드시 같이 돌려야 한다(자동으로 뒤따라 돌지 않는다).
persona.json 스키마
페르소나 폴더(예: ../p1-linocut/persona.json)에 둔다. 랜드마크·분할로 계산 가능한
값은 각 단계 스크립트가 직접 계산하므로 여기 없다 — 원화마다 달라지고 유도할 수 없는
값(참조 이미지를 보고 사람이 고른 점·상자)만 이 파일에 둔다.
{
"code": "P1", // 필수. manifest.persona, rig.persona
"publicSlug": "p1", // 생략 시 code.lower(). apps/web/public/avatar/v3/<publicSlug>/
"rigFileName": "p1Rig.ts", // 생략 시 "<publicSlug>Rig.ts"
"rigExportName": "P1_LINOCUT_RIG", // 생략 시 "<CODE>_LINOCUT_RIG"
// 모티프 팔레트(motifPetal/motifLeaf) 표본을 뽑을 스타일 참조 이미지(persona.json 기준 상대경로).
// export_rig.py의 팔레트 계산에서만 쓴다 — 모티프 스프라이트 자체는 이번 파이프라인
// 범위 밖이다(오케스트레이터가 따로 설계).
"styleFrame": "../art-direction-v3/p1/r2-b-linocut.png",
"paletteSamples": {
// 머리카락 어두운 덩어리 표본(ink 팔레트색). base-front.png 픽셀 기준 상자.
"ink": { "box": [280, 100, 720, 350], "lumThreshold": 55 },
// 눈 흰자/홍채/홍채테 고정 설계값(결정문 §8.2 "고정값" 원칙) — 생략하면 공통 기본값 사용.
"eyeOverride": { "sclera": "#D8CEBD", "iris": "#4F3B2C", "irisRing": "#1E1F1F" },
// styleFrame에서 꽃잎(ochre)·잎/구름(blue) 색을 뽑을 상자들. kind는 "ochre" 또는 "blue".
"motifPetalBoxes": [{ "label": "sun", "kind": "ochre", "box": [1230, 10, 1536, 210] }],
"motifLeafBoxes": [{ "label": "cloudLeft", "kind": "blue", "box": [20, 20, 380, 190] }]
},
"faceDetail": {
// 점(기미) 등 랜드마크로 안 나오는 얼굴 반점. 없으면 빈 배열(점 없는 캐릭터도 된다).
"moles": [{ "center": [661.3, 627.9], "radius": 20.0 }]
},
// 렌더러 회전/스케일 중심점(결정문 §8.4). 원화를 보고 목·몸통·얼굴 중심을 정한다.
"pivots": { "neck": [500, 990], "body": [502, 1566], "face": [490, 660] },
// bust 크롭은 정사각형(변 = 캔버스 폭)이고 위쪽 오프셋만 여기서 정한다.
"crops": { "bustYOffset": 40 }
}
eyeOverride·paletteFixed(mouthInner/teeth/blush/tear/pallor/paper)·backdrop(겉표정
그룹별 배경색)은 모든 페르소나가 공유하는 기본값이 있다(persona_config.py의
DEFAULT_*) — 캐릭터마다 다르게 할 필요가 있을 때만 persona.json에 적어 덮어쓴다.
styleFrame·paletteSamples.ink·paletteSamples.motifPetalBoxes/motifLeafBoxes·
pivots는 필수다(export_rig.py가 해당 값을 쓰는 시점에 없으면 어떤 필드를
채워야 하는지 알려주며 멈춘다).
faceDetail.browLandmarksOverride — P1 전용 호환 장치, 새 페르소나는 쓰지 않는다
"faceDetail": {
"browLandmarksOverride": {
"browLeft": { "inner": [..], "peak": [..], "outer": [..] },
"browRight": { "inner": [..], "peak": [..], "outer": [..] }
}
}
있으면 face_detail.py가 눈썹 제외 영역·눈 영역 y0 계산에 manifest.landmarks의
현재(중심선 보정) 눈썹 좌표 대신 이 값을 쓴다. 다른 랜드마크(눈·입·코·턱)는 그대로
현재 값을 쓴다 — 눈썹만 바꾼다.
P1의 기존 face-detail.png·jaw-detail.png·해당 webp·p1Rig.ts는 눈썹 중심선 보정
(manifest.browCenterline) 이전 좌표(browCenterline.oldPoints)로 빌드된 뒤
"다시 빌드하지 않는다"는 오케스트레이터 지시로 고정됐다(소유자도 그 결과를 검수했다).
그래서 P1 persona.json에는 browCenterline.oldPoints와 같은 값을 넣어 재실행 결과가
그 고정본과 바이트 단위로 같아지게 한다. 새 페르소나는 이 필드를 넣지 않는다 —
처음부터 중심선 보정 좌표로 빌드되므로 과거 좌표를 따로 고정할 이유가 없다.
모델 파일(저장소에 없음)
scripts/_models/에 MediaPipe 모델을 받아 둔다(모든 페르소나가 공유, .gitignore의
docs/avatar-art/*/scripts/_models/ 패턴에 그대로 맞는다).
face_landmarker.task: https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.taskselfie_multiclass_256x256.tflite: https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_multiclass_256x256/float32/latest/selfie_multiclass_256x256.tflite
python 환경은 numpy·Pillow·scipy·opencv·mediapipe가 필요하다.
새 페르소나를 추가할 때 사람이 해야 하는 일
이 파이프라인은 자동으로 원화를 만들지 않는다. 사람(또는 다른 워커)이 먼저 준비해야 하는 것:
- 원화 2장:
<persona-dir>/base/base-front.png(정면 기본형)과base-faceless.png(같은 그림에서 눈·눈썹·입만 지운 것). 결정문 §8.2 생성 규칙을 따른다. <persona-dir>/raw/body.png: body 레이어가 head_mask로 가려지는 목 상단 영역을 메울 재생성 참조 편집본(크로마키 초록 배경, base-front와 같은 정렬).<persona-dir>/persona.json: 위 스키마대로 작성한다. 특히styleFrame·paletteSamples·pivots는 원화를 눈으로 보고 정해야 한다(자동 유도 불가).- (선택)
<persona-dir>/motif/: 모티프 스프라이트는 이 라운드 범위 밖이다 — 오케스트레이터가 따로 설계한다. 없으면export_rig.py가 모티프 없는 리그를 만든다(rig.motif 생략). scripts/_models/에 모델 파일이 없으면 받아 둔다(위 링크, 한 번만).
그 다음 run_pipeline.py <persona-dir>를 실행하고, 출력된 검사 수치(halo%, 평균절대차
등 — 각 단계 스크립트가 콘솔에 찍고 manifest.json에도 남긴다)를 기준치와 비교해 판정한다.