납품 인박스 반영 도구 Tools/apply_delivery.py — 제작 세션이 묶음 완료 시 deliveries/<batch>.json 1장만 쓰고, 반영 세션은 list/apply --all 로 배경 복사·md5 검증·poseset 교체(PD 2026-11-05 「완전히 완료된 경우 리소스만 전달」)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-05 01:39:51 +09:00
parent b9e9f22914
commit 46f65e942f
1 changed files with 148 additions and 0 deletions

148
Tools/apply_delivery.py Normal file
View File

@ -0,0 +1,148 @@
"""납품 인박스 → 게임 반영 (파일 작업만 · Unity 쪽은 끝에 출력하는 명령으로).
PD 2026-11-05: 「매번 수신하지 않아도, 요청한 작업이 완전히 완료된 경우 해당 리소스만 전달받는 체계」.
제작 세션은 묶음이 **완전히** 끝났을 때만 아래 json 한 장을 인박스에 쓴다(중간 보고 없음 · 채팅 메시지 없음).
인박스 = C:\\ArtRnD\\samguk_game\\deliveries\\<YYYYMMDD_HHMM>_<batch>.json
{
"batch": "bg" | "hgj" | "heroes_back" | "dz" | "cc" | "wei" | …,
"kind": "bg" | "poses",
"qa": "PASS",
"session": "<만든 세션 이름>",
# kind == "bg"
"file": "C:/ArtRnD/samguk_game/bg/battlefield_01.png", "md5": "<파일 md5>",
# kind == "poses"
"root": "C:/ArtRnD/samguk_game/poses", # <root>/<art>/<clip>.png
"arts": ["dz_infantry", "dz_spear", …],
"clips": ["front_idle", "front_ready", "front_action"], # back_* 가 있으면 함께
"md5_list": "C:/ArtRnD/samguk_game/poses/md5.txt" # 선택 · <root> 기준 상대 경로 · md5sum 형식
}
실행: <ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py list
<ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py apply <json 경로> (반영 뒤 .applied.json 으로 이름 바꿈)
<ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py apply --all (미반영 전부)
반영 뒤 Unity: 메뉴 재임포트 → 비동기 테스트 → 커밋 (끝에 출력).
"""
import argparse
import hashlib
import json
import os
import shutil
import subprocess
import sys
PROJECT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
INBOX = r"C:\ArtRnD\samguk_game\deliveries"
EXPORTER = r"C:\Ai\NerdNavisAi\.claude\skills\skeletal-anim-pipeline\files\scripts\rig_frames_export.py"
UNITS = os.path.join(PROJECT, "Assets", "Resources", "Units")
BOARD = os.path.join(PROJECT, "Assets", "Resources", "Board", "battlefield_01.png")
def md5_of(path):
h = hashlib.md5()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def pending():
os.makedirs(INBOX, exist_ok=True)
return sorted(f for f in os.listdir(INBOX) if f.endswith(".json") and not f.endswith(".applied.json"))
def check_md5_list(root, md5_list):
"""md5sum 형식(<md5> <상대경로>) 목록 검증 → (ok, bad)."""
ok, bad = 0, []
for line in open(md5_list, encoding="utf-8"):
line = line.strip()
if not line:
continue
digest, rel = line.split(None, 1)
rel = rel.lstrip("*").replace("/", os.sep)
p = os.path.join(root, rel)
if not os.path.exists(p) or md5_of(p) != digest.lower():
bad.append(rel)
else:
ok += 1
return ok, bad
def apply_bg(d):
src = d["file"]
if d.get("md5") and md5_of(src) != d["md5"].lower():
sys.exit("배경 md5 불일치: " + src)
os.makedirs(os.path.dirname(BOARD), exist_ok=True)
shutil.copyfile(src, BOARD)
print("bg ->", BOARD, "md5", md5_of(BOARD))
def apply_poses(d):
root = d["root"]
if d.get("md5_list"):
ok, bad = check_md5_list(root, d["md5_list"])
if bad:
sys.exit("md5 불일치 %d: %s" % (len(bad), ", ".join(bad[:5])))
print("md5 OK", ok)
clips = d.get("clips") or ["front_idle", "front_ready", "front_action"]
for art in d["arts"]:
src = os.path.join(root, art)
have = [c for c in clips if os.path.exists(os.path.join(src, c + ".png"))]
if not have:
sys.exit("포즈 없음: " + src)
out = os.path.join(UNITS, art)
if os.path.isdir(out):
shutil.rmtree(out) # 구판(리그 프레임·정지 그림) 제거 — git 에 있음
r = subprocess.run([sys.executable, "-I", "-B", "-X", "utf8", EXPORTER, "poseset", src, "--out", out, "--clips", ",".join(have)],
capture_output=True, text=True, encoding="utf-8")
if r.returncode != 0:
sys.exit("poseset 실패 " + art + "\n" + r.stdout + r.stderr)
print(r.stdout.strip().splitlines()[-1])
def cmd_list(a):
files = pending()
print("미반영 납품:", len(files))
for f in files:
d = json.load(open(os.path.join(INBOX, f), encoding="utf-8"))
print(" ", f, "|", d.get("kind"), d.get("batch"), "|", d.get("qa"), "|", d.get("session", ""))
def cmd_apply(a):
targets = pending() if a.all else [os.path.basename(a.json)]
if not targets:
print("반영할 납품 없음")
return
for f in targets:
p = os.path.join(INBOX, f)
d = json.load(open(p, encoding="utf-8"))
if d.get("qa", "PASS") != "PASS":
print("건너뜀(QA 미통과):", f)
continue
print("==", f, d.get("kind"), d.get("batch"))
if d["kind"] == "bg":
apply_bg(d)
elif d["kind"] == "poses":
apply_poses(d)
else:
sys.exit("모르는 kind: " + str(d.get("kind")))
os.replace(p, p[:-5] + ".applied.json")
print("\n다음(Unity · PD 에디터 Play 중이면 먼저 editor.playmode stop):")
print(" unity command --project-path . editor invoke_menu --path 'TKS/Reimport Unit Art (유닛 아트 재임포트)' --result-only")
print(" unity command --project-path . test run --mode playmode --async-tests --timeout 600 --result-only ; unity command --project-path . test status --result-only")
print(" git add -A && git commit (끝줄 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>)")
if __name__ == "__main__":
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sp = p.add_subparsers(dest="cmd", required=True)
l = sp.add_parser("list")
l.set_defaults(fn=cmd_list)
ap = sp.add_parser("apply")
ap.add_argument("json", nargs="?", default="")
ap.add_argument("--all", action="store_true")
ap.set_defaults(fn=cmd_apply)
a = p.parse_args()
if a.cmd == "apply" and not a.all and not a.json:
sys.exit("json 경로 또는 --all")
a.fn(a)