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

165 lines
8.3 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
# 2026-10-01 PD 지시 — mobaGameEquipment_v10 결과 36장 중 "쓸 만한 것"은 3장(초록 마법 단검 · 은색 갑옷 · 갈색 부츠)뿐.
# 합격 기준(PD 가 고른 3장의 공통점): 물건 하나가 따로 떨어져 있다 · 원형 바탕/후광/딸린 조각이 없다 · 전체가 여백 안에 들어온다.
# 방법: 대상마다 여러 장을 뽑고(장당 약 7초), 투명도(알파)로 자동 채점해 기준에 맞는 것만 추린다 → 추린 것을 사람이 본다.
# python comfy_moba_batch.py <출력폴더> <round 이름> <대상당 장수> [시작] [끝]
# 자동 채점(배경 제거 뒤 알파 기준):
# · 불투명 비율 6~42% (원형 바탕이 붙으면 50% 를 넘는다) · 가장자리 닿음 1.5% 이하(잘림) · 가장 큰 덩어리가 불투명의 88% 이상(딸린 조각 없음)
import io, json, os, sys, time, uuid, urllib.request, urllib.parse
from collections import deque
from PIL import Image, ImageDraw
HOST = "http://127.0.0.1:8000"
CKPT = "mobaGameEquipment_v10.safetensors"
PREFIX = "(masterpiece:1.2), (best quality:1.2), moba game icon"
NEG = ("lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, "
"1girl, 1boy, person, human, character, face, hair, breasts, body, wearing, mannequin, bust, "
"circle, round frame, border, badge, emblem, aura, halo, light rays, glowing background, explosion, fire background, "
"multiple objects, two weapons, crossed weapons, duplicate, out of frame")
# 1 · 2차에서 형태가 약했던 대상은 낱말을 구체적으로 바꿨다(활 · 벨트 · 목걸이 · 화살통 · 반지 · 둔기 · 투척)
SUBJECTS = [
("sword", "longsword"),
("blunt", "one-handed war hammer"),
("bow", "longbow, wooden bow with bowstring"),
("dagger", "dagger"),
("staff", "magic staff with crystal"),
("magic_dagger", "magic dagger, glowing blade"),
("armor", "plate armor, breastplate"),
("belt", "leather belt with metal buckle"),
("gloves", "gauntlets"),
("boots", "leather boots"),
("ring", "gold ring with gemstone"),
("necklace", "amulet, pendant with gem on chain"),
("shield", "shield"),
("quiver", "arrow quiver, leather tube holding arrows"),
("spellbook", "closed spellbook, thick book"),
("throwing", "throwing knife"),
("orb", "crystal orb"),
("offhand_sword", "short sword"),
]
def post(path, data):
req = urllib.request.Request(HOST + path, data=json.dumps(data).encode("utf-8"), headers={"Content-Type": "application/json"})
return json.loads(urllib.request.urlopen(req, timeout=60).read())
def get(path):
return urllib.request.urlopen(HOST + path, timeout=300).read()
def workflow(prompt, seed):
return {
"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": CKPT}},
"3": {"class_type": "CLIPSetLastLayer", "inputs": {"clip": ["1", 1], "stop_at_clip_layer": -2}},
"4": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["3", 0], "text": prompt}},
"5": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["3", 0], "text": NEG}},
"6": {"class_type": "EmptyLatentImage", "inputs": {"width": 512, "height": 512, "batch_size": 1}},
"7": {"class_type": "KSampler", "inputs": {"model": ["1", 0], "positive": ["4", 0], "negative": ["5", 0], "latent_image": ["6", 0], "seed": seed, "steps": 30, "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": "moba_batch"}},
}
def run(prompt, seed):
pid = post("/prompt", {"prompt": workflow(prompt, seed), "client_id": str(uuid.uuid4())})["prompt_id"]
t0 = time.time()
while time.time() - t0 < 600:
hist = json.loads(get("/history/" + pid))
if pid in hist:
st = hist[pid].get("status", {})
if st.get("status_str") == "error":
raise RuntimeError(json.dumps(st)[:900])
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(get("/view?" + q))).convert("RGBA")
time.sleep(1.0)
raise TimeoutError("comfy timeout")
def score(im):
"""알파로 채점 — (합격 여부, 불투명 비율, 가장자리 닿음 비율, 가장 큰 덩어리 비율)"""
n = 96
a = im.getchannel("A").resize((n, n), Image.BILINEAR)
px = a.load()
solid = [[px[x, y] > 40 for x in range(n)] for y in range(n)]
total = sum(sum(1 for v in row if v) for row in solid)
if total == 0:
return False, 0.0, 0.0, 0.0
opaque = total / float(n * n)
edge = sum(1 for i in range(n) if solid[0][i]) + sum(1 for i in range(n) if solid[n - 1][i]) + sum(1 for i in range(n) if solid[i][0]) + sum(1 for i in range(n) if solid[i][n - 1])
edge_ratio = edge / float(4 * n)
seen = [[False] * n for _ in range(n)]
best = 0
for y in range(n):
for x in range(n):
if solid[y][x] and not seen[y][x]:
q = deque([(x, y)]); seen[y][x] = True; size = 0
while q:
cx, cy = q.popleft(); size += 1
for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nx, ny = cx + dx, cy + dy
if 0 <= nx < n and 0 <= ny < n and solid[ny][nx] and not seen[ny][nx]:
seen[ny][nx] = True; q.append((nx, ny))
best = max(best, size)
main = best / float(total)
ok = 0.06 <= opaque <= 0.42 and edge_ratio <= 0.015 and main >= 0.88
return ok, opaque, edge_ratio, main
def checker(size, n=16):
im = Image.new("RGB", (size, size), (58, 58, 58))
d = ImageDraw.Draw(im)
for y in range(0, size, n):
for x in range(0, size, n):
if (x // n + y // n) % 2:
d.rectangle([x, y, x + n - 1, y + n - 1], fill=(74, 74, 74))
return im
def main():
out, tag, count = sys.argv[1], sys.argv[2], int(sys.argv[3])
lo = int(sys.argv[4]) if len(sys.argv) > 4 else 0
hi = int(sys.argv[5]) if len(sys.argv) > 5 else len(SUBJECTS)
os.makedirs(out, exist_ok=True)
subs = SUBJECTS[lo:hi]
show = 6
cell, pad, label = 170, 4, 16
sheet = Image.new("RGB", (pad + 110 + show * (cell + pad), len(subs) * (cell + pad) + pad), (40, 40, 40))
draw = ImageDraw.Draw(sheet)
report = []
for i, (name, body) in enumerate(subs):
prompt = "%s, (%s:1.35), no humans, single item, isolated, centered, full object in frame, simple background, still life" % (PREFIX, body)
passed = []
t0 = time.time()
for j in range(count):
im = run(prompt, 12000 + (lo + i) * 29 + j * 503)
ok, opaque, edge, mainc = score(im)
fn = "%s_%s_%02d%s.png" % (tag, name, j, "" if ok else "_x")
im.save(os.path.join(out, fn))
if ok:
passed.append((mainc - abs(opaque - 0.24), fn, im))
passed.sort(key=lambda t: -t[0])
report.append((name, len(passed), count))
print("%s %s 합격 %d/%d (%.0f초)" % (tag, name, len(passed), count, time.time() - t0)); sys.stdout.flush()
y = pad + i * (cell + pad)
draw.text((pad, y + 4), name, fill=(240, 220, 160))
draw.text((pad, y + 22), "%d/%d" % (len(passed), count), fill=(200, 200, 200))
for k, (_, fn, im) in enumerate(passed[:show]):
bg = checker(cell)
small = im.resize((cell, cell), Image.LANCZOS)
bg.paste(small, (0, 0), small)
sheet.paste(bg, (pad + 110 + k * (cell + pad), y))
draw.text((pad + 112 + k * (cell + pad), y + 2), fn[len(tag) + len(name) + 2:len(tag) + len(name) + 4], fill=(255, 255, 255))
path = os.path.join(out, "%s_pass_sheet_%d_%d.png" % (tag, lo, hi))
sheet.save(path)
print("sheet", path)
print("요약", ", ".join("%s %d/%d" % r for r in report))
if __name__ == "__main__":
main()