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

99 lines
6.6 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
# 2026-10-02 PD 지시(자율 반복) — 길잡이가 없는(또는 직접 그린 도형으로는 안 되는) 종류의 길잡이를 자동으로 마련한다.
# a15 검토: 도끼는 도형을 네 번 고쳐도 "속 빈 초승달"(선화가 면을 고리로 읽는다). 투구 · 반지 · 뿔피리 · 구슬도 단순 도형에서는 매끈하게만 나온다.
# → 기반 모델(mobaGameEquipment_v10 · LoRA 없음)이 글자로 그린 그림 가운데 "물건 하나 · 여백 안 · 한 덩어리"인 것을 골라 그 윤곽을 길잡이로 쓴다.
# 안쪽 선이 풍부해 모델이 채울 근거가 생긴다. 색 · 질감은 넘어가지 않는다(윤곽선만 쓴다).
# python icon_guide_bootstrap.py <종류> "<묘사>" [장수=10] [고를 수=2] ["부정어"]
# 결과: moba_anfg/shapes/<종류>__genmoba<k>.png (active.txt 에 등록) · 후보 전체는 moba_anfg/bootstrap/ 에 남긴다.
import io, json, os, sys, time, uuid, urllib.parse
from PIL import Image
import comfy_moba_batch as mb
import icon_shapes
# a17 · a18 검토: 기반 모델(SD1.5)의 초벌은 단검이 두 자루로, 반지 · 뿔피리가 다른 물건으로 나와 길잡이로 쓸 수 없었다.
# → 환경변수 BOOT_MODEL=xl 이면 글자를 더 잘 알아듣는 SDXL 아이콘 모델(gameIconInstitute_v4XL)로 초벌을 그린다. 풍은 상관없다(윤곽만 쓴다). 등록 이름은 genxl<k>.
XL = os.environ.get("BOOT_MODEL", "") == "xl"
XL_CKPT = "gameIconInstitute_v4XL.safetensors"
XL_NEG = "worst quality, low quality, lowres, blurry, cropped, cut off, text, watermark, signature, person, hands, multiple objects, duplicate, frame, border, round background"
def run_xl(desc, seed):
"""SDXL 초벌 한 장(1024 · 배경 제거)"""
wf = {
"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": XL_CKPT}},
"3": {"class_type": "CLIPSetLastLayer", "inputs": {"clip": ["1", 1], "stop_at_clip_layer": -2}},
"4": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["3", 0], "text": "gmic_\\(3dicon\\), game icon, %s, single object, centered, whole object visible, wide margin, simple dark background" % desc}},
"5": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["3", 0], "text": XL_NEG}},
"6": {"class_type": "EmptyLatentImage", "inputs": {"width": 1024, "height": 1024, "batch_size": 1}},
"7": {"class_type": "KSampler", "inputs": {"model": ["1", 0], "positive": ["4", 0], "negative": ["5", 0], "latent_image": ["6", 0], "seed": seed, "steps": 20, "cfg": 7.0, "sampler_name": "dpmpp_2m", "scheduler": "karras", "denoise": 1.0}},
"8": {"class_type": "VAEDecode", "inputs": {"samples": ["7", 0], "vae": ["1", 2]}},
"12": {"class_type": "InspyrenetRembg", "inputs": {"image": ["8", 0], "torchscript_jit": "default"}},
"9": {"class_type": "SaveImage", "inputs": {"images": ["12", 0], "filename_prefix": "boot_xl"}},
}
pid = mb.post("/prompt", {"prompt": wf, "client_id": str(uuid.uuid4())})["prompt_id"]
t0 = time.time()
while time.time() - t0 < 900:
hist = json.loads(mb.get("/history/" + pid))
if pid in hist:
st = hist[pid].get("status", {})
if st.get("status_str") == "error":
raise RuntimeError(json.dumps(st)[:1200])
for node in hist[pid].get("outputs", {}).values():
for im in node.get("images", []):
q = urllib.parse.urlencode({"filename": im["filename"], "subfolder": im.get("subfolder", ""), "type": im.get("type", "output")})
return Image.open(io.BytesIO(mb.get("/view?" + q))).convert("RGBA")
time.sleep(1.5)
raise TimeoutError("comfy timeout")
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "moba_anfg", "bootstrap")
def main():
kind, desc = sys.argv[1], sys.argv[2]
count = int(sys.argv[3]) if len(sys.argv) > 3 else 10
keep = int(sys.argv[4]) if len(sys.argv) > 4 else 2
extra_neg = sys.argv[5] if len(sys.argv) > 5 else ""
os.makedirs(OUT, exist_ok=True)
global XL_NEG
if extra_neg:
mb.NEG = mb.NEG + ", " + extra_neg
XL_NEG = XL_NEG + ", " + extra_neg
prompt = "%s, (%s:1.35), no humans, single item, isolated, centered, full object in frame, simple background, still life" % (mb.PREFIX, desc)
passed = []
for j in range(count):
seed = 77000 + sum(ord(c) for c in kind) * 13 + j * 503
im = run_xl(desc, seed) if XL else mb.run(prompt, seed)
ok, opaque, edge, mainc = mb.score(im)
fn = "%s%s_%02d%s.png" % ("xl_" if XL else "", kind, j, "" if ok else "_x")
im.save(os.path.join(OUT, fn))
if ok:
# 한 덩어리이고 적당한 크기일수록 앞에 둔다
passed.append((mainc - abs(opaque - 0.24), fn))
passed.sort(reverse=True)
# a19 검토: 자동 채점은 "두 자루 · 칼집 딸림 · 다른 물건(뿔피리 → 확성기)"을 못 거른다 → 초벌 전체를 한 장으로 묶어 두고,
# 고를 수(keep)를 0 으로 주면 등록하지 않는다. 그때는 초벌 시트를 보고(사람 · 검토 에이전트) 고른 것을 `python icon_shapes.py add <종류> <파일> <이름>` 으로 등록한다.
prefix = ("xl_" if XL else "") + kind + "_"
names = sorted(f for f in os.listdir(OUT) if f.startswith(prefix) and f.endswith(".png") and "sheet" not in f)
cell, pad, cols = 256, 4, 6
rows = (len(names) + cols - 1) // cols
from PIL import ImageDraw
sheet = Image.new("RGB", (cols * (cell + pad) + pad, rows * (cell + pad) + pad), (24, 24, 28))
d = ImageDraw.Draw(sheet)
for i, fn in enumerate(names):
im = Image.open(os.path.join(OUT, fn)).convert("RGBA")
bg = Image.new("RGB", (cell, cell), (52, 52, 58)); small = im.resize((cell, cell), Image.LANCZOS); bg.paste(small, (0, 0), small)
x, y = pad + (i % cols) * (cell + pad), pad + (i // cols) * (cell + pad)
sheet.paste(bg, (x, y)); d.text((x + 3, y + 2), fn[:-4], fill=(255, 120, 120) if fn.endswith("_x.png") else (120, 255, 120))
sheet.save(os.path.join(OUT, prefix + "sheet.png"))
# 이미 등록된 genmoba 번호 뒤에 잇는다
label = "genxl" if XL else "genmoba"
have = len([f for f in os.listdir(icon_shapes.OUT) if f.startswith(kind + "__" + label)])
for k, (_, fn) in enumerate(passed[:keep]):
icon_shapes.add_from(kind, os.path.join(OUT, fn), "%s%d" % (label, have + k))
print("%s: 통과 %d/%d · 길잡이 등록 %d (%s)" % (kind, len(passed), count, min(keep, len(passed)), ", ".join(fn for _, fn in passed[:keep])))
if __name__ == "__main__":
main()