- P1 전용 스크립트를 docs/avatar-art/linocut-pipeline 으로 옮겨 persona.json 설정으로 일반화(P1 재실행 리그 바이트 동일) - 7명 외형·상징 설계(linocut-cast.md)와 P2~P7 정면 원화 생성 프롬프트, 얼굴 없는 화풍 참조 - P1 원화 생성 프롬프트 보존
149 lines
8.5 KiB
Markdown
149 lines
8.5 KiB
Markdown
# 공통 리노컷 아바타 파이프라인
|
|
|
|
아바타 v3 리노컷 리그(결정문 [`avatar-expression-engine-v3.md`](../../decisions/avatar-expression-engine-v3.md)
|
|
§8.2 리그 원칙)의 자산 파이프라인이다. P1~P7 등 모든 페르소나가 이 공통 스크립트를 공유하고,
|
|
페르소나별 차이는 각 페르소나 폴더(예: `../p1-linocut/`)의 `persona.json`에서만 읽는다.
|
|
|
|
결과물(레이어 픽셀, 알파, 게시된 WebP, 리그 TS)은 페르소나별 폴더 구조나 상수 위치가
|
|
바뀌었다는 이유로 달라지지 않는다 — 알고리즘은 건드리지 않았다(P1 회귀 검증: 아래 참고).
|
|
|
|
## 단계와 의존 순서
|
|
|
|
`run_pipeline.py`가 아래 순서로 각 단계를 독립 프로세스로 실행한다(괄호는 산출물):
|
|
|
|
1. `landmarks.py` — base-front.png 랜드마크 검출 (`manifest.landmarks`)
|
|
2. `brow_centerline.py` — `manifest.landmarks.eyebrowLeft/Right`를 잉크 띠 중심선으로 보정
|
|
3. `segmentation.py` — `layers/{body,head,hairFront}.png`(v1) + manifest 기준 섹션
|
|
4. `layers_v2.py` — `layers/v2/{body,head,hairFront}.png`(턱 밑 띠·잔머리 halo 보정)
|
|
5. `face_detail.py` — `layers/v2/face-detail.png`
|
|
6. `paper_grain.py` — `layers/v2/paper-grain.png`(다른 단계와 독립, 순서 유연)
|
|
7. `lip_texture.py` — `layers/v2/lip-{upper,lower,shadow}.png`
|
|
8. `jaw_pieces.py` — `layers/v2/jaw-{head,detail}.png`
|
|
9. `export_rig.py` — WebP 게시(`apps/web/public/avatar/v3/<publicSlug>/`) + `rigs/<rigFileName>` 생성
|
|
10. `final_previews.py` — 게시된 WebP로 모션·눈/입 확대 미리보기
|
|
|
|
이 순서는 전달받은 작업 설명의 번호(주제별 묶음)와 다르다 — 특히 `final_previews`는
|
|
`export_rig`가 쓴 `export-rig-report.json`을 읽으므로 반드시 `export_rig` *다음*이어야
|
|
한다. `face_detail`·`jaw_pieces`는 `manifest.landmarks`(1~2단계가 채움)와
|
|
`layers/v2/{head,hairFront,body}.png`(3~4단계)가 먼저 있어야 한다.
|
|
|
|
## 실행
|
|
|
|
```bash
|
|
# 전체 실행
|
|
<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`)에 둔다. 랜드마크·분할로 계산 가능한
|
|
값은 각 단계 스크립트가 직접 계산하므로 여기 없다 — 원화마다 달라지고 유도할 수 없는
|
|
값(참조 이미지를 보고 사람이 고른 점·상자)만 이 파일에 둔다.
|
|
|
|
```jsonc
|
|
{
|
|
"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 전용 호환 장치, 새 페르소나는 쓰지 않는다
|
|
|
|
```jsonc
|
|
"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.task
|
|
- `selfie_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가 필요하다.
|
|
|
|
## 새 페르소나를 추가할 때 사람이 해야 하는 일
|
|
|
|
이 파이프라인은 자동으로 원화를 만들지 않는다. 사람(또는 다른 워커)이 먼저 준비해야
|
|
하는 것:
|
|
|
|
1. 원화 2장: `<persona-dir>/base/base-front.png`(정면 기본형)과
|
|
`base-faceless.png`(같은 그림에서 눈·눈썹·입만 지운 것). 결정문 §8.2 생성 규칙을 따른다.
|
|
2. `<persona-dir>/raw/body.png`: body 레이어가 head_mask로 가려지는 목 상단 영역을
|
|
메울 재생성 참조 편집본(크로마키 초록 배경, base-front와 같은 정렬).
|
|
3. `<persona-dir>/persona.json`: 위 스키마대로 작성한다. 특히 `styleFrame`·
|
|
`paletteSamples`·`pivots`는 원화를 눈으로 보고 정해야 한다(자동 유도 불가).
|
|
4. (선택) `<persona-dir>/motif/`: 모티프 스프라이트는 이 라운드 범위 밖이다 — 오케스트레이터가
|
|
따로 설계한다. 없으면 `export_rig.py`가 모티프 없는 리그를 만든다(rig.motif 생략).
|
|
5. `scripts/_models/`에 모델 파일이 없으면 받아 둔다(위 링크, 한 번만).
|
|
|
|
그 다음 `run_pipeline.py <persona-dir>`를 실행하고, 출력된 검사 수치(halo%, 평균절대차
|
|
등 — 각 단계 스크립트가 콘솔에 찍고 manifest.json에도 남긴다)를 기준치와 비교해 판정한다.
|