리노컷 자산 파이프라인 공통화와 캐스트 외형 설계
- P1 전용 스크립트를 docs/avatar-art/linocut-pipeline 으로 옮겨 persona.json 설정으로 일반화(P1 재실행 리그 바이트 동일) - 7명 외형·상징 설계(linocut-cast.md)와 P2~P7 정면 원화 생성 프롬프트, 얼굴 없는 화풍 참조 - P1 원화 생성 프롬프트 보존
This commit is contained in:
parent
f11e76ff18
commit
ecb36d123f
38 changed files with 2393 additions and 990 deletions
141
docs/avatar-art/linocut-pipeline/scripts/run_pipeline.py
Normal file
141
docs/avatar-art/linocut-pipeline/scripts/run_pipeline.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
"""공통 리노컷 리그 파이프라인 — 단일 진입점.
|
||||
|
||||
페르소나 폴더 하나(예: 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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue