리노컷 공통 파이프라인 캐스트 확장 — 정렬·정규화·얼굴 해칭·원화 눈썹·눈 실측

- register_edit(편집본 ECC 정렬·챔퍼 지표), normalize_base(P1 머리 크기 정규화, 위쪽 종이색 채움)
- faceDetail region=face(얼굴 해칭을 원화 픽셀로 보존)·matchFacelessColor
- brow_sprite 단계: 원화 눈썹 잉크 스프라이트(hairFront 아래 제외), 윗눈꺼풀 두께·홍채 실측
- 리그 계약 RigBrow.halfWidth/sprite·RigEye.lineScale, 렌더러 눈썹 16띠·바깥쪽 눈꺼풀 굵기
- 실행기 UTF-8 하위 실행, brow_centerline 패딩본 자체 생성, QA 시트 촬영 도구(깜빡임 회피·고해상도 대조)
- P1은 새 필드를 모두 끄고 게시 자산·리그 바이트 동일
This commit is contained in:
Yun Chan 2026-10-01 22:38:27 +09:00
parent 9a22d1647b
commit 98577cf4ae
18 changed files with 2524 additions and 85 deletions

View file

@ -37,6 +37,7 @@ DEFAULT_BACKDROP = {
"energy": "#E2E0D0",
}
DEFAULT_BUST_Y_OFFSET = 40.0
DEFAULT_FACE_INSET_PX = 8.0
@dataclass
@ -70,6 +71,32 @@ class PersonaConfig:
# 단위 회귀가 성립한다. {"browLeft": {...}, "browRight": {...}} 형태(각
# inner/peak/outer), manifest.landmarks.eyebrowLeft/Right와 같은 스키마.
brow_landmarks_override: dict | None
# false면 export_rig.py가 RigBrow.halfWidth를 쓰지 않는다(렌더러가 linocutGeometry.ts
# BROW_STOPS 고정값을 쓴다). 기본 true — 원화에서 잰 눈썹 반두께(manifest.browCenterline.
# halfWidth{Left,Right})를 쓴다. P1은 이미 고정 굵기로 빌드·검수됐으므로 false로 둔다.
brow_half_width_from_art: bool
# false면 brow_sprite.py가 눈썹 스프라이트(layers/v2/brow-{left,right}.png)를
# 만들지 않는다(결정문 §8.4.1). 기본 true. P1은 이미 고정 벡터 눈썹으로
# 빌드·검수됐으므로 false로 둔다.
brow_sprite_from_art: bool
# false면 brow_sprite.py가 눈꺼풀 선 굵기 배율(RigEye.lineScale)을 재지 않는다
# (결정문 §8.4.1). 기본 true. P1은 lineScale의 기준값 자체(P1_UPPER_LID_THICKNESS_PX)라
# false로 둔다.
eye_line_scale_from_art: bool
# false면 export_rig.py가 RigEye.iris에 manifest.landmarks(mediapipe)의 값을 그대로
# 쓴다. 기본 true — brow_sprite.py가 원화에서 실측한 홍채(manifest.iris)를 쓴다
# (2차 보정 T3: mediapipe iris 랜드마크는 실제 눈 비례로 학습돼 판화 잉크 홍채보다
# 크게 잡는 경향이 있다). P1은 이미 고정 벡터 렌더로 빌드·검수됐으므로 false로 둔다.
iris_from_art: bool
# faceDetail.region: "features"(기본, 눈·입 주변 타원만) | "face"(얼굴 오벌 전체 —
# 캐스트 확장, 얼굴 피부 해칭 보존). face_detail.py만 쓴다.
face_detail_region: str
# region="face"일 때 얼굴 오벌 경계에서 안쪽으로 줄이는 폭(px) — 원화 윤곽 잉크선이
# 이 영역에 들어오지 않게 하는 여유. region="features"면 쓰이지 않는다.
face_inset_px: float
# true면 face_detail.py가 제외 영역(눈·눈썹·입) 안을 채우는 faceless(F) 픽셀의
# 색을 주변 원화 피부에 맞춰 평행이동한다(보간/블러 없음). 기본 false(P1 그대로).
match_faceless_color: bool
def require_pivots(self) -> dict:
if self.pivots is None:
@ -183,6 +210,18 @@ def load_persona_config(persona_dir: Path) -> PersonaConfig:
face_detail = data.get("faceDetail", {})
moles = face_detail.get("moles", [])
brow_landmarks_override = face_detail.get("browLandmarksOverride")
brow_half_width_from_art = data.get("browHalfWidthFromArt", True)
brow_sprite_from_art = data.get("browSpriteFromArt", True)
eye_line_scale_from_art = data.get("eyeLineScaleFromArt", True)
iris_from_art = data.get("irisFromArt", True)
face_detail_region = face_detail.get("region", "features")
if face_detail_region not in ("features", "face"):
raise SystemExit(
f"[중단] {persona_dir.name}/persona.json의 faceDetail.region={face_detail_region!r}는 "
"지원하지 않는다 — \"features\" 또는 \"face\"만 쓴다."
)
face_inset_px = float(face_detail.get("faceInsetPx", DEFAULT_FACE_INSET_PX))
match_faceless_color = bool(face_detail.get("matchFacelessColor", False))
pivots = data.get("pivots")
crops = data.get("crops", {})
@ -211,4 +250,11 @@ def load_persona_config(persona_dir: Path) -> PersonaConfig:
palette_fixed=palette_fixed,
backdrop=backdrop,
brow_landmarks_override=brow_landmarks_override,
brow_half_width_from_art=bool(brow_half_width_from_art),
brow_sprite_from_art=bool(brow_sprite_from_art),
eye_line_scale_from_art=bool(eye_line_scale_from_art),
iris_from_art=bool(iris_from_art),
face_detail_region=face_detail_region,
face_inset_px=face_inset_px,
match_faceless_color=match_faceless_color,
)