- 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은 새 필드를 모두 끄고 게시 자산·리그 바이트 동일
150 lines
7 KiB
Python
150 lines
7 KiB
Python
"""공통 리노컷 리그 파이프라인 — 단일 진입점.
|
|
|
|
페르소나 폴더 하나(예: docs/avatar-art/p1-linocut/, persona.json이 있어야 한다)에
|
|
대해 1~11단계를 정해진 의존 순서대로 실행한다. 각 단계는 독립 프로세스로 돌린다
|
|
(mediapipe FaceLandmarker+ImageSegmenter를 같은 프로세스에서 같이 쓰면 세그폴트가
|
|
나는 것과 같은 이유로, 모든 단계를 서브프로세스로 분리해 두면 한 단계의 문제가
|
|
다른 단계·전체 파이프라인을 오염시키지 않는다).
|
|
|
|
단계 의존 순서(실제 코드 의존성 기준 — 전달받은 작업 설명의 번호는 주제별
|
|
묶음이라 실행 순서와 다르다. 특히 final_previews는 export_rig가 쓴
|
|
export-rig-report.json을 읽으므로 반드시 export_rig *다음*이다):
|
|
|
|
1. landmarks — base-front.png 랜드마크 검출, manifest.landmarks 작성
|
|
2. brow_centerline — manifest.landmarks.eyebrowLeft/Right를 중심선으로 보정
|
|
3. segmentation — layers/{body,head,hairFront}.png(v1) + manifest 기준 섹션
|
|
4. layers_v2 — layers/v2/{body,head,hairFront}.png(결함 보정)
|
|
5. face_detail — layers/v2/face-detail.png
|
|
6. brow_sprite — layers/v2/brow-{left,right}.png + manifest.eyeLineScale(결정문 §8.4.1)
|
|
7. paper_grain — layers/v2/paper-grain.png(다른 단계와 독립, 순서 유연)
|
|
8. lip_texture — layers/v2/lip-{upper,lower,shadow}.png
|
|
9. jaw_pieces — layers/v2/jaw-{head,detail}.png
|
|
10. export_rig — WebP 게시 + rigs/<code>Rig.ts 생성
|
|
11. final_previews — 게시된 WebP로 모션·눈/입 확대 미리보기
|
|
|
|
주의(재실행 순서): landmarks를 혼자 다시 돌리면 manifest.landmarks를 통째로
|
|
새로 쓰므로, brow_centerline이 보정한 eyebrowLeft/Right가 사라진다. landmarks만
|
|
다시 돌릴 때는 brow_centerline도 같이 다시 돌려야 한다(이 스크립트가 --only로
|
|
한 단계만 돌릴 때는 호출하는 쪽이 책임진다 — 자동으로 뒤따라 돌리지 않는다).
|
|
|
|
실행:
|
|
<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 # 단계 이름 목록
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
from persona_config import load_persona_config # noqa: E402
|
|
|
|
# (단계 이름, 스크립트 파일명) — 실행 순서 그대로.
|
|
STAGES: list[tuple[str, str]] = [
|
|
("landmarks", "landmarks.py"),
|
|
("brow_centerline", "brow_centerline.py"),
|
|
("segmentation", "segmentation.py"),
|
|
("layers_v2", "layers_v2.py"),
|
|
("face_detail", "face_detail.py"),
|
|
("brow_sprite", "brow_sprite.py"),
|
|
("paper_grain", "paper_grain.py"),
|
|
("lip_texture", "lip_texture.py"),
|
|
("jaw_pieces", "jaw_pieces.py"),
|
|
("export_rig", "export_rig.py"),
|
|
("final_previews", "final_previews.py"),
|
|
]
|
|
STAGE_NAMES = [name for name, _ in STAGES]
|
|
|
|
|
|
def run_stage(name: str, script: str, persona_dir: Path) -> tuple[bool, str, float]:
|
|
script_path = SCRIPTS_DIR / script
|
|
t0 = time.monotonic()
|
|
# 단계 스크립트 출력은 UTF-8로 주고받는다. Windows 콘솔 기본(cp949)은 em dash 같은 문자를
|
|
# 못 그려, 모티프 없는 리그 분기(export_rig) 같은 데서 단계가 UnicodeEncodeError로 죽었다
|
|
# (2026-10-01 P7). 부모가 콘솔에 옮겨 쓸 때 못 그리는 문자는 main()에서 대체 문자로 바꾼다.
|
|
proc = subprocess.run(
|
|
[sys.executable, "-u", str(script_path), str(persona_dir)],
|
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
env={**os.environ, "PYTHONIOENCODING": "utf-8"},
|
|
)
|
|
elapsed = time.monotonic() - t0
|
|
output = proc.stdout + proc.stderr
|
|
ok = proc.returncode == 0
|
|
return ok, output, elapsed
|
|
|
|
|
|
def main() -> int:
|
|
sys.stdout.reconfigure(errors="replace")
|
|
sys.stderr.reconfigure(errors="replace")
|
|
parser = argparse.ArgumentParser(description="공통 리노컷 파이프라인 실행")
|
|
parser.add_argument("persona_dir", nargs="?", help="페르소나 폴더(예: docs/avatar-art/p1-linocut)")
|
|
parser.add_argument("--only", choices=STAGE_NAMES, help="이 단계만 실행한다")
|
|
parser.add_argument("--from", dest="from_stage", choices=STAGE_NAMES, help="이 단계부터 끝까지 실행한다")
|
|
parser.add_argument("--list", action="store_true", help="단계 이름 목록을 출력하고 끝낸다")
|
|
args = parser.parse_args()
|
|
|
|
if args.list:
|
|
for name in STAGE_NAMES:
|
|
print(name)
|
|
return 0
|
|
|
|
if not args.persona_dir:
|
|
parser.error("persona_dir가 필요하다(--list가 아니면).")
|
|
|
|
persona_dir = Path(args.persona_dir).resolve()
|
|
cfg = load_persona_config(persona_dir) # persona.json 검증 겸 조기 실패
|
|
|
|
if args.only:
|
|
selected = [s for s in STAGES if s[0] == args.only]
|
|
elif args.from_stage:
|
|
start = STAGE_NAMES.index(args.from_stage)
|
|
selected = STAGES[start:]
|
|
else:
|
|
selected = STAGES
|
|
|
|
print(f"=== 공통 리노컷 파이프라인: persona={cfg.code} dir={persona_dir} ===")
|
|
print(f"실행할 단계: {[s[0] for s in selected]}")
|
|
|
|
run_log: list[dict] = []
|
|
for name, script in selected:
|
|
print(f"\n--- [{name}] {script} ---")
|
|
ok, output, elapsed = run_stage(name, script, persona_dir)
|
|
print(output.rstrip())
|
|
print(f"--- [{name}] {'OK' if ok else '실패'} ({elapsed:.1f}s) ---")
|
|
run_log.append({"stage": name, "ok": ok, "elapsedSec": round(elapsed, 2)})
|
|
if not ok:
|
|
print(f"\n[중단] 단계 {name}에서 실패했다. 이후 단계는 실행하지 않는다.")
|
|
_record_run_log(cfg.manifest_path, cfg.code, run_log, completed=False)
|
|
return 1
|
|
|
|
_record_run_log(cfg.manifest_path, cfg.code, run_log, completed=True)
|
|
print(f"\n=== 전체 완료: {len(selected)}개 단계 ===")
|
|
return 0
|
|
|
|
|
|
def _record_run_log(manifest_path: Path, persona_code: str, run_log: list[dict], completed: bool) -> None:
|
|
"""단계별 실행 기록(성공 여부·소요 시간)을 manifest에 남긴다. 각 단계 자체의
|
|
검사 수치(halo%, 평균절대차 등)는 이미 각 단계 스크립트가 manifest의 해당
|
|
섹션(layersV2, lipTexture, jaw, ...)에 기록하므로 여기서 중복하지 않는다."""
|
|
if not manifest_path.exists():
|
|
return
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
manifest["pipelineRun"] = {
|
|
"persona": persona_code,
|
|
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
"completed": completed,
|
|
"stages": run_log,
|
|
}
|
|
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|