vignette/docs/avatar-art/linocut-pipeline/scripts/run_pipeline.py
Yun Chan ecb36d123f 리노컷 자산 파이프라인 공통화와 캐스트 외형 설계
- P1 전용 스크립트를 docs/avatar-art/linocut-pipeline 으로 옮겨 persona.json 설정으로 일반화(P1 재실행 리그 바이트 동일)
- 7명 외형·상징 설계(linocut-cast.md)와 P2~P7 정면 원화 생성 프롬프트, 얼굴 없는 화풍 참조
- P1 원화 생성 프롬프트 보존
2026-10-01 16:11:28 +09:00

141 lines
6.4 KiB
Python

"""공통 리노컷 리그 파이프라인 — 단일 진입점.
페르소나 폴더 하나(예: docs/avatar-art/p1-linocut/, persona.json이 있어야 한다)에
대해 1~8단계를 정해진 의존 순서대로 실행한다. 각 단계는 독립 프로세스로 돌린다
(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. paper_grain — layers/v2/paper-grain.png(다른 단계와 독립, 순서 유연)
7. lip_texture — layers/v2/lip-{upper,lower,shadow}.png
8. jaw_pieces — layers/v2/jaw-{head,detail}.png
9. export_rig — WebP 게시 + rigs/<code>Rig.ts 생성
10. 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 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"),
("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()
proc = subprocess.run(
[sys.executable, "-u", str(script_path), str(persona_dir)],
capture_output=True, text=True,
)
elapsed = time.monotonic() - t0
output = proc.stdout + proc.stderr
ok = proc.returncode == 0
return ok, output, elapsed
def main() -> int:
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())