BurningTimesAi/공유/기획/FirstRising/아이템/comfy_lora_train.py

141 lines
7.5 KiB
Python

# FR 장비 아이콘 스타일 LoRA 학습 — ComfyUI 코어 TrainLoraNode (BT25 추가 지시 2026-09-25 "FR에서 사용한 아이템을 학습시킨 로라로 테스트")
# 1) 데이터셋: 기존 장비 아이콘 32장 중 금색 'S' 오버레이(ItemClass 1) 6장 제외 → 26장을 검정 배경 512² 로 합성, 원본·좌우반전·축소(0.85) 3벌 = 78장 + 캡션 .txt
# → C:\Comfyui\input\fr_icons_v1 (LoadImageTextDataSetFromFolder 가 input 하위 폴더만 읽음)
# 2) 학습: difconsistency(SD1.5) · LoRA rank 16 · lr 4e-4 · batch 2 · N step (인수, 기본 500) → output/loras → models/loras/FR_icons_v1.safetensors 복사
# 사용: python comfy_lora_train.py [steps]
import json, os, shutil, sys, time, uuid, glob, urllib.request, urllib.parse
from PIL import Image, ImageOps
HOST = "http://127.0.0.1:8000"
SRC = r"C:\BurningTimes\FirstRising\Assets\GameAssets\UI\EquipmentItemIcon"
DS_NAME = "fr_icons_v1"
DS = os.path.join(r"C:\Comfyui\input", DS_NAME)
LORA_OUT = r"C:\Comfyui\output\loras"
LORA_DST = os.path.join(r"C:\Comfyui\models\loras", "FR_icons_v1.safetensors")
CKPT = "difconsistencyCheckpoint_v10.safetensors"
TRIGGER = "frdicon"
RES = 512
STEPS = int(sys.argv[1]) if len(sys.argv) > 1 else 500
LR, RANK, BATCH, SEED = 4e-4, 16, 2, 7
# 캡션은 '내용(형태·색)'만 적고 화풍은 트리거 토큰에 묶는다. 실제 아이콘을 보고 작성(fr_icons_sheet32.png).
CAP = {
1015: "a war hammer with a dark wooden handle and a cracked iron head with orange sparks",
1029: "a simple curved wooden bow with bone colored limbs",
1043: "a dark steel dagger with an orange glowing gem in the hilt",
1057: "a dark iron flanged mace with orange glowing spikes",
1071: "a long thin dark rod dagger with an orange glowing tip",
1435: "a plain iron short sword with a brown leather grip",
1099: "a round dark amulet with a cyan glowing gem",
1113: "a silver shield shaped pendant with an icy blue gem",
1127: "a dark spiked diamond pendant with a gold and orange star gem",
1141: "a round red medallion with a skull",
1169: "a pair of dark leather gloves with cyan glowing rune lines",
1183: "a pair of brown leather gloves with engraved patterns",
1197: "a pair of dark leather gloves with orange glowing accents",
1211: "a pair of dark iron gauntlets with brown leather",
1239: "a dark plate armor with a fur collar and a cyan gem",
1253: "an ornate silver grey plate armor",
1267: "a grey steel plate armor with engraved patterns",
1281: "a chainmail armor with spiked shoulder pauldrons",
1309: "a brown leather belt with cyan gems and hanging chains",
1323: "a dark cloth waist wrap with a gold plate and a dagger",
1337: "a dark leather belt with a red rope and iron buckles",
1351: "a dark leather belt with a skull buckle and small cyan gems",
1379: "a pair of brown leather lace up boots with cyan gems",
1393: "a pair of brown leather boots with buckles",
1407: "a pair of blue and brown leather high boots with straps",
1421: "a pair of black plate high boots with straps",
}
EXCLUDED_S = [1001, 1085, 1155, 1225, 1295, 1365] # 금색 S 오버레이 — 화풍에 섞이지 않게 제외
def build_dataset():
if os.path.isdir(DS):
shutil.rmtree(DS)
os.makedirs(DS)
n = 0
for gid, desc in CAP.items():
im = Image.open(os.path.join(SRC, f"{gid}.png")).convert("RGBA").resize((RES, RES), Image.LANCZOS)
variants = {"a": im, "b": ImageOps.mirror(im)}
small = im.resize((int(RES * 0.85), int(RES * 0.85)), Image.LANCZOS)
c = Image.new("RGBA", (RES, RES), (0, 0, 0, 0)); c.paste(small, ((RES - small.width) // 2, (RES - small.height) // 2), small)
variants["c"] = c
for tag, v in variants.items():
bg = Image.new("RGB", (RES, RES), (0, 0, 0)); bg.paste(v, (0, 0), v)
bg.save(os.path.join(DS, f"{gid}_{tag}.png"))
with open(os.path.join(DS, f"{gid}_{tag}.txt"), "w", encoding="utf-8") as f:
f.write(f"{TRIGGER}, game equipment item icon, {desc}, black background")
n += 1
print(f"dataset {DS}: {n} images ({len(CAP)} icons x 3)", flush=True)
return n
def get(path):
return json.load(urllib.request.urlopen(HOST + path, timeout=120))
def vram():
try:
d = get("/system_stats")["devices"][0]
return f"VRAM free {d['vram_free'] / 2**30:.1f}/{d['vram_total'] / 2**30:.1f} GB"
except Exception as e:
return f"vram? {e}"
def train():
wf = {
"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": CKPT}},
"2": {"class_type": "LoadImageTextDataSetFromFolder", "inputs": {"folder": DS_NAME}},
"3": {"class_type": "MakeTrainingDataset", "inputs": {"images": ["2", 0], "vae": ["1", 2], "clip": ["1", 1], "texts": ["2", 1]}},
"4": {"class_type": "TrainLoraNode", "inputs": {
"model": ["1", 0], "latents": ["3", 0], "positive": ["3", 1],
"batch_size": BATCH, "grad_accumulation_steps": 1, "steps": STEPS, "learning_rate": LR, "rank": RANK,
"optimizer": "AdamW", "loss_function": "MSE", "seed": SEED, "training_dtype": "bf16", "lora_dtype": "bf16",
"quantized_backward": False, "algorithm": "LoRA", "gradient_checkpointing": True, "checkpoint_depth": 1,
"offloading": False, "existing_lora": "[None]", "bucket_mode": False, "bypass_mode": False}},
"5": {"class_type": "SaveLoRA", "inputs": {"lora": ["4", 0], "prefix": "loras/FR_icons_v1", "steps": ["4", 2]}},
"6": {"class_type": "LossGraphNode", "inputs": {"loss": ["4", 1], "filename_prefix": "FR_icons_v1_loss"}},
}
req = urllib.request.Request(HOST + "/prompt", data=json.dumps({"prompt": wf, "client_id": str(uuid.uuid4())}).encode(), headers={"Content-Type": "application/json"})
try:
r = json.load(urllib.request.urlopen(req, timeout=60))
except urllib.error.HTTPError as e:
print("PROMPT REJECTED:", e.read().decode(errors="replace")[:1500]); return None
if r.get("node_errors"):
print("NODE ERRORS:", json.dumps(r["node_errors"], ensure_ascii=False)[:1500]); return None
pid = r["prompt_id"]
print(f"queued {pid} · steps {STEPS} · batch {BATCH} · lr {LR} · rank {RANK} · {vram()}", flush=True)
t0 = time.time()
while time.time() - t0 < 5400:
time.sleep(15)
h = get(f"/history/{pid}")
if pid in h:
st = h[pid].get("status", {})
if st.get("status_str") == "error":
for m in st.get("messages", []):
if m[0] == "execution_error":
print("TRAIN ERROR:", m[1].get("node_type"), m[1].get("exception_message", "")[:1500])
print("\n".join(m[1].get("traceback", [])[-6:])[:2000])
return None
print(f"done in {time.time() - t0:.0f}s · {vram()}", flush=True)
return h[pid]["outputs"]
print(f" training… {time.time() - t0:.0f}s elapsed · {vram()}", flush=True)
print("TIMEOUT"); return None
if __name__ == "__main__":
build_dataset()
print(vram(), flush=True)
out = train()
if out is None:
sys.exit(1)
files = sorted(glob.glob(os.path.join(LORA_OUT, "FR_icons_v1*.safetensors")), key=os.path.getmtime)
if not files:
print("no lora file in", LORA_OUT); sys.exit(1)
shutil.copy2(files[-1], LORA_DST)
print("LORA:", files[-1], "->", LORA_DST, f"{os.path.getsize(LORA_DST) / 2**20:.1f} MB")
for nid, o in out.items():
for im in o.get("images", []):
print("output image:", im)