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

452 lines
39 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
# 2026-10-02 PD 지시 — "내가 원하는 아트풍은 첨부한 아트풍(Game Icon Research Institute LoRA-ANFG_V3 예시)인데, 우리 프로젝트의 원본 에셋이 너무 실사풍이라…"
# → 원본 에셋을 따라 칠하는 방식(comfy_moba_repaint*.py)은 실사풍을 그대로 끌고 오므로 방향이 틀렸다.
# 여기서는 원본 에셋을 쓰지 않고, 양식화된 게임 아이콘 기반 모델(mobaGameEquipment_v10) 위에 ANFG v3 LoRA 를 얹어 글자만으로 뽑는다.
# (10-01 시도는 기반이 실사풍 difconsistency 였다 — 얇고 사실적으로 나와 PD 가 반려. 이 조합은 처음이다.)
# python comfy_moba_anfg.py <출력폴더> <round 이름> [LoRA 강도들 "0.7,1.0"] [시드 수=2] [크기=512] [다듬기 0|1]
import io, json, os, sys, time, uuid, urllib.parse
from PIL import Image, ImageDraw
import comfy_moba_repaint as base # 통신 · 바둑판 바탕은 앞선 스크립트의 것을 쓴다
CKPT = "mobaGameEquipment_v10.safetensors"
LORA = "giI_anfg_v3_000018.safetensors"
TRIGGER = "game icon institute, game icon, ahfg" # PD 가 알려 준 트리거
NEG = ("(worst quality, low quality:1.3), lowres, text, watermark, signature, blurry, photo, realistic, photorealistic, 3d render, thin, "
"1girl, 1boy, person, human, face, body, multiple objects, duplicate, frame, border, circle background")
UPSCALER = "4x-UltraSharp.pth"
# 2026-10-02 PD 지적 — a2 선별본(final_sheet)이 "너무 캐주얼한 느낌". 예시는 어두운 금속 · 정교한 장식 · 깊은 명암의 MMORPG 풍인데
# a2 는 묘사가 단순하고(금 반지 + 붉은 보석) 다듬기(0.35)가 면을 매끈하게 밀어 만화풍이 됐다.
# 분위기 사전 설정(환경변수 ANFG_TONE): 프롬프트 뒤에 붙는 낱말 · 부정어에 더하는 낱말. 다듬기 세기는 ANFG_REFINE(기본 0.35).
TONES = {
"": ("", ""),
"epic": (", epic fantasy mmorpg equipment, ornate, intricate engraved details, dark metal, painterly shading, dramatic lighting, deep shadows, glowing accents",
", cartoon, casual, cute, chibi, toy, flat color, simple design, cel shading, thick outline"),
# a4(epic) 검토: 명암 · 질감은 목표에 가까워졌으나 형태가 녹고(반지 → 가면, 벨트 → 칼날) 빛 번짐 · 잘림이 늘었다. LoRA 1.0 은 형태가 더 녹는다.
# refined: PD 가 고른 것의 공통점(실제 장비 비례 · 얇은 외곽선 · 면 안의 명암 단계 · 금 + 한 색)을 낱말로 옮기고, 발광 · 원색 · 굵은 외곽선을 부정어로 누른다.
"refined": (", realistic proportions, clean sharp edges, crisp silhouette, polished metal, subtle painterly shading, thin outline, soft studio light",
", cartoon, casual, cute, chibi, toy, flat color, thick black outline, neon colors, oversized gem, glow, bloom, smoke, flame, motion blur, cropped, cut off"),
# a9 검토: 형태는 길잡이로 풀렸고 남은 걸림돌은 "단순함"(도형에 색만 입힌 수준 · 명암 단계가 적어 플라스틱 느낌 · 정면 평면 구도).
# rich: 장식 밀도와 명암을 올린다. 길잡이 ControlNet 은 일찍 풀어(끝 0.35) 모델이 장식을 덧붙일 여지를 준다.
# a10 검토: 단검 · 방패는 장식 · 명암이 PD 합격본 수준에 근접. 다만 ControlNet 끝 0.35 는 24장 중 약 10장이 길잡이를 벗어났고(활 → 덩어리, 반지 → 메달),
# "embedded gemstones" 는 망치 머리 · 반지 안을 통보석으로 만들었고, "three-quarter view" 는 효과가 약했다 → 두 낱말을 빼고 끝을 0.5 로(가는 종류는 0.6). ANFG 는 0.7 고정(0.9 는 형태가 녹는다).
"rich": (", ornate fantasy design, engraved filigree, layered metal plates, crisp silhouette, clean sharp edges, "
"detailed painterly shading, painterly brush strokes, strong highlights and deep shadows, dark ambient occlusion",
", cartoon, casual, cute, chibi, toy, plastic, glossy plastic, plain, simple design, flat color, flat front view, thick black outline, neon colors, glow, bloom, smoke, flame, motion blur, cropped, cut off, "
"round medallion, coin, disc, close-up"),
# a14 검토: 배색 · 외곽선 · 밝기는 PD 합격본과 한 세트. 남은 차이 = 손그림 붓결과 깊은 그림자(깨끗한 그라데이션 + 흰 하이라이트 띠가 지배적).
# paint: 붓결 · 무광 · 어두운 바탕색 쪽 낱말로 바꾸고, 매끈함 · 광택을 부정어로 누른다(다듬기 방식 비교와 함께 a16 에서 시험).
"paint": (", ornate fantasy design, engraved filigree, layered metal plates, crisp silhouette, hand painted texture, visible painterly brush strokes, "
"matte dark metal, deep shadows, dark ambient occlusion, rich muted colors, strong rim light, specular highlights, high contrast",
", cartoon, casual, cute, chibi, toy, plastic, glossy plastic, plain, simple design, flat color, flat front view, thick black outline, neon colors, glow, bloom, smoke, flame, motion blur, cropped, cut off, "
"round medallion, coin, disc, close-up, smooth gradient, airbrushed, shiny, white highlight band, 3d render, purple, magenta, pink"),
}
# 구도 낱말(맨 앞) — 분위기 설정이 있을 때만 붙인다(a1 · a2 재현을 깨지 않게)
COMPOSE = "single item, centered, full object in frame, wide margin, "
TONE = os.environ.get("ANFG_TONE", "")
REFINE = float(os.environ.get("ANFG_REFINE", "0.35"))
# 자체 학습 LoRA(comfy_anfg_train.py 가 만든 FR_anfg_<버전>)를 ANFG LoRA 위에 함께 건다 — 환경변수 ANFG_LORA2 = "파일이름:강도". 걸면 트리거 fricon 을 앞에 붙인다.
LORA2 = os.environ.get("ANFG_LORA2", "")
# 1024 로 키우는 방식: model = 4x-UltraSharp(선명하지만 면을 매끈하게 민다) · lanczos = 단순 확대 뒤 다듬기에서 모델이 다시 칠하게 둔다(붓결을 살리려는 시험)
UPSCALE = os.environ.get("ANFG_UPSCALE", "model")
# PD 가 보여 준 예시와 같은 종류(반지 · 투구 · 장갑 · 갑옷 · 목걸이) + FR 부위(검 · 부츠 · 벨트)
SUBJECTS = [
("ring", "a blue dragon head ring, silver scales, glowing cyan"),
("helmet", "a red and gold horned helmet"),
("gloves", "a single armored gauntlet, dark steel with gold trim and purple gem"),
("armor", "a red and gold chest armor with shoulder plates"),
("necklace", "a red gem pendant necklace with gold wings"),
("sword", "a broad sword, blue glowing blade, gold hilt"),
("boots", "a pair of armored boots, blue and gold"),
("belt", "a leather belt with large gold buckle and red gem"),
]
# FR 장비 전 부위(무기 6 · 방어구 4 · 장신구 2 · 보조 장비 8) — a1 에서 형태가 약했던 벨트 · 검 · 장갑은 낱말을 구체적으로 바꿨다
SUBJECTS_FR = [
("sword", "a single longsword, steel blade, gold crossguard, blue gem, full weapon in frame"),
("blunt", "a single war hammer, heavy steel head with gold trim, full weapon in frame"),
("bow", "a single recurve bow, wooden with gold tips and green gem, full weapon in frame"),
("dagger", "a single dagger, curved steel blade, red gem pommel, full weapon in frame"),
("staff", "a single magic staff, gold top holding a purple crystal, full weapon in frame"),
("magic_dagger", "a single magic dagger, glowing green crystal blade, gold hilt, full weapon in frame"),
("armor", "a red and gold chest armor with shoulder plates"),
("belt", "a waist belt, wide brown leather strap loop, large round gold buckle with red gem"),
("gloves", "a pair of armored gloves, steel fingers, gold trim, purple gem"),
("boots", "a pair of armored boots, blue and gold"),
("ring", "a gold ring with a large red gemstone"),
("necklace", "a blue gem pendant necklace with gold chain"),
("shield", "a kite shield, blue steel with gold rim and lion emblem"),
("quiver", "a leather quiver full of arrows, gold trim"),
("spellbook", "a closed thick spellbook, purple cover, gold corners, glowing gem"),
("throwing", "a four pointed steel shuriken with gold center"),
("orb", "a glowing cyan crystal orb held in a gold claw stand"),
("offhand_sword", "a single short sword, wide blade, gold hilt, full weapon in frame"),
("totem", "a carved wooden totem with glowing green runes and feathers"),
("horn", "a gold trimmed war horn"),
]
# a2 검토(직접 봄): 풍은 일정하나 긴 무기(둔기 · 활 · 지팡이 · 화살통)가 전부 검 모양으로 쏠리고, 장식 테두리 · 화면 밖 잘림이 섞였다.
# a3: FR 원본 아이콘에서 "윤곽선만" 빌려 형태를 잡는다(색 · 질감은 가져오지 않으므로 실사풍이 따라오지 않는다). (이름, 묘사, 윤곽을 빌릴 원본 번호)
SUBJECTS_SHAPE = [
("blunt", "a war hammer, heavy steel head with gold trim and blue gem, brown handle", 1015),
("mace", "a flanged mace, gold and steel head with red gem, dark handle", 1057),
("bow", "a recurve bow, green and gold limbs, thin bowstring", 1029),
("dagger", "a dagger, steel blade, gold guard, red gem", 1043),
("sword", "a short sword, blue steel blade, gold crossguard", 1435),
("belt1", "a leather belt, brown strap, gold buckle with red gem", 1337),
("belt2", "a leather belt, dark strap, gold skull buckle, cyan gems", 1351),
("belt3", "a leather belt with gold plates and hanging chains, cyan gems", 1309),
("gloves1", "a pair of armored gloves, purple leather, gold trim, glowing runes", 1169),
("gloves2", "a pair of steel gauntlets, gold trim, red gems", 1211),
("boots1", "a pair of armored boots, blue and gold", 1393),
("boots2", "a pair of high plate boots, red and gold", 1421),
("armor1", "a chest armor, red and gold plates, shoulder pauldrons", 1253),
("armor2", "a chest armor, blue steel and gold, fur collar, cyan gem", 1239),
("necklace1", "a round amulet, gold rim, glowing cyan gem", 1099),
("necklace2", "a shield shaped pendant, silver and gold, blue gem", 1113),
]
# a5 검토(에이전트가 직접 봄): 글자만으로는 검 · 단검 · 갑옷 · 장화만 안정. 둔기 · 활 · 지팡이 · 화살통 · 뿔피리 등은 칼날 모양으로 쏠리고, 벨트 · 투척 · 방패는 원형 바탕이 붙는다.
# SUBJECTS_ALL: (이름, 묘사, 윤곽 원본 번호 또는 None, 종류별 부정어). 묘사 · 부정어는 a5 검토의 수정안을 반영했다.
SUBJECTS_ALL = [
("sword", "a single longsword, steel blade, gold crossguard, blue gem, full weapon in frame", None, "two blades"),
("blunt", "a one-handed flanged mace, heavy spiked steel head with gold trim on a short handle", 1057, "sword, blade"),
("hammer", "a war hammer, heavy block steel head with gold trim and blue gem, brown wooden handle", 1015, "sword, blade"),
("bow", "a recurve bow, curved wooden limbs with gold tips, thin taut bowstring, side view", 1029, "sword, blade, shield, arrow, harp, panel, banner, sail"),
("dagger", "a single dagger, curved steel blade, red gem pommel, full weapon in frame", None, "two blades"),
("staff", "a long wooden wizard staff, full length visible, purple crystal on top, diagonal", None, "sword, blade, scepter"),
("magic_dagger", "a single magic dagger, green crystal blade, gold hilt, full weapon in frame", None, "two blades, floating orbs"),
("armor", "a red and gold chest armor with shoulder plates", None, ""),
("belt", "a leather waist belt laid flat as a horizontal strap, large gold buckle in the center, red leather", 1337, "ring, circle, pants, torso, round frame"),
("gloves", "a single armored gauntlet, five fingers, steel plate glove with gold trim, wrist cuff", None, "boot, bare hand"),
("boots", "a pair of armored boots, blue and gold", None, ""),
("ring", "a gold ring with a large red gemstone, closed circular finger ring band", None, "ribbon, broken ring"),
("necklace", "a pendant necklace, short closed chain loop fully visible, blue gem pendant with gold setting", None, "bottle, wings, cropped chain"),
("shield", "a kite shield, plain blue field with gold rim and central boss, three-quarter view, small in frame", None, "lion, animal, face, crest, round frame"),
("quiver", "a leather quiver full of arrows, feathered arrow shafts sticking out of the top, shoulder strap", None, "sword, blade"),
("spellbook", "a closed thick spellbook, purple cover, gold corner clasps, gem on the front cover, three-quarter view", None, "open book, scroll"),
("throwing", "a single steel throwing knife, kunai, small leaf shaped blade with ring pommel", None, "shuriken, star, disc, wheel, round frame"),
("orb", "a crystal ball, deep blue glass sphere on a small gold stand", None, "cyan, teal, horns"),
("offhand_sword", "a short parrying dagger, main-gauche, wide curved gold handguard, whole weapon in frame", None, "two blades"),
("totem", "a small carved wooden idol statue, tribal face, gold bands, red gem eyes", None, "green glow, neon, pillar, column, round frame"),
("horn", "a curved ivory war horn, blowing horn, gold rim bands, leather strap", None, "axe, blade, trumpet, all gold"),
]
# 초안 방식(draft): 물건을 정확히 아는 실사풍 모델로 먼저 "형태 초안"을 그리고, 그 초안의 윤곽선만 ControlNet 으로 넘겨 ANFG 풍으로 새로 그린다.
# 색 · 질감은 넘어가지 않으므로 실사풍이 따라오지 않고, 글자만으로는 못 잡던 형태(활 · 화살통 · 뿔피리 등)를 잡는다 — 어떤 장비든 글자로 뽑기 위한 방법.
DRAFT_CKPT = "difconsistencyCheckpoint_v10.safetensors"
DRAFT_NEG = "person, human, hand, character, multiple objects, text, watermark, frame, border, cropped, cut off, blurry, lowres"
def add_draft(wf, desc, seed, cn_strength, cn_end):
"""형태 초안(d*) → 굵은 선화 → ControlNet 을 글자 조건(4 · 5)에 걸어 샘플러(7)에 물린다."""
wf["d1"] = {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": DRAFT_CKPT}}
wf["d4"] = {"class_type": "CLIPTextEncode", "inputs": {"clip": ["d1", 1], "text": "%s, single object, centered, whole object visible, wide margin, game item concept art, plain dark background" % desc}}
wf["d5"] = {"class_type": "CLIPTextEncode", "inputs": {"clip": ["d1", 1], "text": DRAFT_NEG}}
wf["d6"] = {"class_type": "EmptyLatentImage", "inputs": {"width": 512, "height": 512, "batch_size": 1}}
wf["d7"] = {"class_type": "KSampler", "inputs": {"model": ["d1", 0], "positive": ["d4", 0], "negative": ["d5", 0], "latent_image": ["d6", 0], "seed": seed + 7,
"steps": 24, "cfg": 7.0, "sampler_name": "dpmpp_2m", "scheduler": "karras", "denoise": 1.0}}
wf["d8"] = {"class_type": "VAEDecode", "inputs": {"samples": ["d7", 0], "vae": ["d1", 2]}}
wf["d9"] = {"class_type": "SaveImage", "inputs": {"images": ["d8", 0], "filename_prefix": "moba_anfg_draft"}}
wf["30"] = {"class_type": "ControlNetLoader", "inputs": {"control_net_name": CN_LINEART}}
wf["31"] = {"class_type": "LineArtPreprocessor", "inputs": {"image": ["d8", 0], "coarse": "enable", "resolution": 512}}
wf["32"] = {"class_type": "ControlNetApplyAdvanced", "inputs": {"positive": ["4", 0], "negative": ["5", 0], "control_net": ["30", 0], "image": ["31", 0],
"strength": cn_strength, "start_percent": 0.0, "end_percent": cn_end, "vae": ["1", 2]}}
wf["7"]["inputs"]["positive"] = ["32", 0]; wf["7"]["inputs"]["negative"] = ["32", 1]
return wf
# 길잡이 모음 방식(lib): icon_shapes.py 가 만든 moba_anfg/shapes/<종류>__<출처>.png 의 윤곽선으로 형태를 잡는다(a6 검토에서 가장 정확했던 방식).
# 종류별 묘사의 {pal} 자리에 색 조합을 돌려 넣어, 같은 형태라도 다른 디자인이 나오게 한다(학습 데이터가 한 색으로 쏠리지 않게).
TYPE_DESC = {
"sword": "a longsword, ornate engraved steel blade with a fuller, dark steel shading, ornate crossguard, {pal}",
"dagger": "a dagger, engraved steel blade with a fuller, dark steel shading, ornate hilt and crossguard, {pal}",
"blunt": "a flanged mace, heavy spiked metal head on a short handle, {pal}",
"hammer": "a war hammer, heavy block metal head with ornate banded ends and an engraved emblem, wrapped handle, {pal}",
"axe": "a single-bladed battle axe, solid broad axe head attached to the shaft, engraved steel blade, dark steel shading, wooden shaft with metal fittings, {pal}",
"bow": "an ornate recurve bow, carved limbs with gold filigree fittings, gem at the wrapped grip, thin bowstring, {pal}",
"staff": "a wizard staff, long shaft, glowing crystal head ornament, {pal}",
"armor": "a chest armor, layered plates, shoulder pauldrons, {pal}",
"helmet": "a knight helmet, visor, crest, {pal}",
"belt": "a waist belt, leather strap, large ornate buckle, {pal}",
"gloves": "a pair of armored gauntlets, plated fingers, wrist cuffs, {pal}",
"boots": "a pair of armored boots, plated shins, {pal}",
"ring": "a finger ring, large faceted gemstone, engraved band, {pal}",
"necklace": "a pendant amulet, faceted gem in an ornate setting, {pal}",
"shield": "a shield, metal rim, central emblem boss, {pal}",
"quiver": "a leather quiver full of arrows, feathered arrows sticking out, shoulder strap, {pal}",
"spellbook": "a closed spellbook, thick tome, metal corner clasps, gem on the cover, {pal}",
"throwing": "a throwing blade, kunai, sharp steel, {pal}",
"orb": "a crystal orb, glass sphere on an ornate stand, {pal}",
"totem": "a carved wooden totem, stacked tribal faces, small wings, {pal}",
"horn": "a curved war horn, ivory body, metal rim bands, {pal}",
}
# PD 가 고른 것 = 금 + 적 또는 금 + 청 + 은빛 금속. 그 둘을 앞에 두고 나머지로 폭을 넓힌다.
# a7 검토: 여섯 색을 돌리니 청록 · 보라 · 초록 단색 칠이 40% 쯤 나와 불합격이 늘었다 → 금을 늘 넣고 한 색만 더한다. 은 단색은 세트에서 차갑게 떠서 뺐다.
PALETTES = ["gold and crimson red with silver steel", "gold and royal blue with silver steel", "gold trim on dark steel with red gems", "gold trim on silver steel with blue gems"]
# a21 뒤 최상급 점검(검토): 세트로는 묶이지만 목표 풍과 다른 점 = 원색 파랑이 전면(30장 중 약 20장) · 목표는 어둡고 채도 낮은 바탕에 포인트만 빛난다.
# ANFG_PAL=dark 이면 어두운 바탕색 + 작은 강조색 조합을 쓴다(PD 에게 방향 비교로 보여 줄 시험용 — PD 가 고른 합격본은 금 + 적/청의 밝은 쪽이다).
if os.environ.get("ANFG_PAL", "") == "dark":
PALETTES = ["dark steel with antique gold trim and a small glowing blue gem accent", "dark iron and antique gold with deep crimson accents",
"blackened steel with bronze trim and a small glowing red gem accent", "dark silver and antique gold with deep blue enamel accents"]
# 2026-10-03 PD 지적 — "붉은색 · 푸른색 같은 원색 계열만 쓰고 있어서 색감이 풍부하지 않고 저급해 보인다" + 레퍼런스(핀터레스트 → ArtStation "RPG Weapon Icons 02", Olekzandr Zahorulko).
# 레퍼런스에서 본 것: 색이 "칠"이 아니라 "재질"에서 나온다(강철 회색 · 나무 갈색 · 가죽 · 뼈 · 청동 · 녹슨 쇠). 원색은 물건 전체가 아니라 작은 강조(감은 끈 · 보석 · 마법 빛)에만,
# 그 강조색이 물건마다 다르다(청록 · 보라 · 초록 · 주황 · 분홍 · 하늘). 어두운 바탕 위에서 명암 폭이 넓다.
# 우리 문제: 색 조합이 "금 + 적 / 금 + 청" 넷뿐이고 물건 전체를 그 색 에나멜로 칠한다 · 자체 LoRA v4 가 그 배색으로 학습돼 더 끌어당긴다.
# ANFG_PAL=material: 재질 조합(몸체) × 강조색(작은 부분)을 따로 돌린다 → 조합 수가 많아 물건마다 색이 달라진다.
MATERIALS = ["weathered steel and dark walnut wood", "dark iron and worn brown leather", "aged bronze and bone ivory", "blackened steel and antique brass",
"pale silver and deep burgundy leather", "tarnished copper and dark oak wood", "gunmetal grey steel and tan leather wraps", "antique gold and dark slate grey steel"]
ACCENTS = ["a small emerald green gem accent", "a small amethyst purple gem accent", "a small teal cyan glowing accent", "a small amber orange gem accent",
"a small ruby red gem accent", "a small sapphire blue gem accent", "a small jade green inlay accent", "a small rose pink crystal accent",
"a small turquoise inlay accent", "a small pale moonstone white gem accent"]
if os.environ.get("ANFG_PAL", "") == "material":
# a25(PM 이 비교 시트를 직접 봄): 반지 · 벨트 · 방패 · 목걸이 · 망치 · 검은 재질색 + 작은 강조로 잘 나왔으나, 갑옷 · 장화 · 투구는 강조색이 몸체 전체를 덮었다(보라 갑옷 · 초록 투구).
# → 재질에 가중치를 주고 "몸체는 재질색, 강조는 작은 보석 하나"임을 낱말로 못박는다.
PALETTES = ["(body made of %s:1.25), natural material colors, %s only" % (m, ACCENTS[(k * 3 + j) % len(ACCENTS)]) for j in range(5) for k, m in enumerate(MATERIALS)]
# a26 검토(에이전트 + PM 이 최상급 24장 직접 확인): 원색 문제는 약 70% 에서 풀리고 세트가 확실히 풍부해졌다. 남은 문제 —
# ① 재질 낱말이 엉뚱한 부위에 붙는다(나무 칼날 · 나무 투구 · 뼈 벨트) ② 장식 밀도가 줄어 민무늬 면이 늘었다 ③ 갈색 쪽으로 쏠려 차가운 재질이 적다 ④ 일부는 여전히 한 색이 몸체를 덮는다.
# ANFG_PAL=parts: 재질을 "부위"에 묶는다. TYPE_PARTS 의 묘사에 {metal}(주 금속) · {metal2}(장식 금속) · {soft}(가죽/나무/천) · {accent}(작은 강조색) 자리를 두고 따로 돌린다.
METALS = ["weathered steel", "blued steel", "dark iron", "aged bronze", "verdigris bronze", "pale silver", "blackened steel", "antique gold", "tarnished copper", "gunmetal grey steel"]
METALS2 = ["antique brass trim", "silver trim", "antique gold trim", "dark iron rivets and trim", "bronze trim"]
SOFTS = ["dark walnut wood", "worn brown leather", "deep burgundy leather", "tan leather wraps", "dark oak wood", "teal leather wrap", "black leather", "bone ivory"]
ACCENTS2 = ["a small emerald green gem", "a small amethyst purple gem", "a small turquoise inlay", "a small amber orange gem", "a small ruby red gem",
"a small sapphire blue gem", "a small jade green inlay", "a small moonstone white gem", "a small topaz yellow gem", "a small garnet dark red gem"]
ORNATE = "engraved filigree, etched runes, rivets, worn edges, fine scratches"
TYPE_PARTS = {
"sword": "a longsword, {metal} blade with a fuller, {metal2} crossguard, {soft} wrapped grip, {accent} in the pommel",
"dagger": "a dagger, {metal} blade with a fuller, {metal2} crossguard, {soft} wrapped grip, {accent} in the pommel",
"axe": "a single-bladed battle axe, solid broad {metal} axe head attached to the shaft, {soft} shaft with {metal2}, {accent} on the axe head",
"blunt": "a flanged mace, heavy spiked {metal} head, {soft} wrapped handle with {metal2}, {accent} on the head",
"hammer": "a war hammer, heavy block {metal} head with banded ends, {soft} wrapped handle with {metal2}, {accent} on the head",
"bow": "an ornate recurve bow, carved {soft} limbs with {metal} fittings, wrapped grip, thin bowstring, {accent} at the grip",
"staff": "a wizard staff, long {soft} shaft with {metal} fittings, crystal head ornament, {accent} glowing softly",
"armor": "a chest armor, layered {metal} plates with {metal2}, {soft} straps, shoulder pauldrons, {accent} on the chest",
"helmet": "a knight helmet, {metal} plates with {metal2}, visor, crest, {accent} on the brow",
"belt": "a waist belt, {soft} strap, large ornate {metal} buckle with {metal2}, {accent} in the buckle",
"gloves": "a pair of armored gauntlets, {metal} plated fingers with {metal2}, {soft} cuffs, {accent} on the back of the hand",
"boots": "a pair of armored boots, {metal} plated shins with {metal2}, {soft} boot, {accent} on the shin",
"ring": "a finger ring, engraved {metal} band with {metal2}, {accent} in an ornate setting",
"necklace": "a pendant amulet, {metal} ornate setting with {metal2}, {accent} in the center",
"shield": "a shield, {metal} rim with {metal2}, {soft} covered face, central emblem boss, {accent} on the boss",
"quiver": "a quiver full of arrows, {soft} body with {metal} fittings, feathered arrows sticking out, shoulder strap, {accent} on the fitting",
"spellbook": "a closed spellbook, thick tome, {soft} cover, {metal} corner clasps, {accent} on the cover",
"throwing": "a throwing blade, kunai, sharp {metal} blade, {soft} wrapped grip, {accent} on the ring pommel",
"orb": "a crystal orb, glass sphere on an ornate {metal} stand with {metal2}, {accent} glow inside the sphere",
"totem": "a carved totem, {soft} body with stacked tribal faces, {metal} bands, small wings, {accent} eyes",
"horn": "a curved war horn, bone ivory body, {metal} rim bands with {metal2}, {soft} strap, {accent} on the band",
}
PARTS = os.environ.get("ANFG_PAL", "") == "parts"
def parts_desc(kind, n):
"""종류 kind 의 n 번째 색 조합 묘사 — 금속 · 장식 금속 · 부드러운 재질 · 강조색을 서로 다른 걸음으로 돌려 겹치지 않게 한다"""
return TYPE_PARTS[kind].format(metal=METALS[n % len(METALS)], metal2=METALS2[(n // 2) % len(METALS2)],
soft=SOFTS[(n * 3 + 1) % len(SOFTS)], accent=ACCENTS2[(n * 7 + 2) % len(ACCENTS2)]) + ", " + ORNATE
# 색을 누르는 부정어(재질 조합일 때만 붙인다)
MATERIAL_NEG = ", saturated primary colors, blue enamel, red enamel, fully blue, fully red, all gold, monochrome gold, two-tone color scheme, candy colors, fully purple, fully green, fully teal, colored metal plates, painted armor, colored enamel body, red armor, magenta, wooden blade, circular background, glow outline, halo"
SHAPES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "moba_anfg", "shapes")
# icon_make.py 가 적는 종류별 묘사(새 종류 · 묘사 고침)를 표에 합친다
_DESC_JSON = os.path.join(os.path.dirname(os.path.abspath(__file__)), "moba_anfg", "type_desc.json")
if os.path.exists(_DESC_JSON):
TYPE_DESC.update(json.load(io.open(_DESC_JSON, encoding="utf-8")))
def lib_subjects():
"""길잡이 모음의 그림마다 대상 한 줄: (이름 = 종류-출처, 묘사, 길잡이 경로, 부정어, 종류)"""
import glob
neg = dict((x[0], x[3]) for x in SUBJECTS_ALL)
subs = []
act = os.path.join(SHAPES_DIR, "active.txt") # icon_shapes.py 가 적는 "지금 쓰는 길잡이" 목록
names = io.open(act, encoding="utf-8").read().split() if os.path.exists(act) else [os.path.basename(f) for f in sorted(glob.glob(os.path.join(SHAPES_DIR, "*__*.png")))]
for k, path in enumerate(os.path.join(SHAPES_DIR, n) for n in names):
kind, label = os.path.basename(path)[:-4].split("__", 1)
if kind in TYPE_DESC:
subs.append((kind + "-" + label.replace("_", ""), TYPE_DESC[kind], path, neg.get(kind, ""), kind))
return subs
SRC = base.SRC
CN_LINEART = "control_v11p_sd15_lineart_fp16.safetensors"
FRAME_NEG = ", ornate frame, picture frame, card, corner ornaments, vignette, ring shaped background, out of frame"
def shape_image(gid, fit=0.72):
"""원본 아이콘을 1024 캔버스 가운데에 여백을 넉넉히 두고 놓는다(윤곽 추출용 · 여백이 곧 완성본의 여백)"""
im = Image.open(os.path.join(SRC, "%d.png" % gid)).convert("RGBA")
box = im.getchannel("A").getbbox()
if box:
im = im.crop(box)
scale = 1024 * fit / float(max(im.size))
im = im.resize((max(1, int(im.width * scale)), max(1, int(im.height * scale))), Image.LANCZOS)
rgb = Image.new("RGB", (1024, 1024), base.BG)
rgb.paste(im, ((1024 - im.width) // 2, (1024 - im.height) // 2), im)
return rgb
def add_shape(wf, image_name, cn_strength, cn_end):
"""글자 조건(4 · 5)에 윤곽 ControlNet 을 걸어 샘플러(7 · 44)에 물린다. 굵은 선(coarse)만 써서 세부는 모델이 새로 그리게 둔다."""
wf["22"] = {"class_type": "LoadImage", "inputs": {"image": image_name}}
wf["30"] = {"class_type": "ControlNetLoader", "inputs": {"control_net_name": CN_LINEART}}
wf["31"] = {"class_type": "LineArtPreprocessor", "inputs": {"image": ["22", 0], "coarse": "enable", "resolution": 768}}
wf["32"] = {"class_type": "ControlNetApplyAdvanced", "inputs": {"positive": ["4", 0], "negative": ["5", 0], "control_net": ["30", 0], "image": ["31", 0],
"strength": cn_strength, "start_percent": 0.0, "end_percent": cn_end, "vae": ["1", 2]}}
wf["7"]["inputs"]["positive"] = ["32", 0]; wf["7"]["inputs"]["negative"] = ["32", 1]
return wf
def workflow(prompt, seed, strength, size, refine, extra_neg=""):
wf = {
"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": CKPT}},
"2": {"class_type": "LoraLoader", "inputs": {"model": ["1", 0], "clip": ["1", 1], "lora_name": LORA, "strength_model": strength, "strength_clip": strength}},
"3": {"class_type": "CLIPSetLastLayer", "inputs": {"clip": ["2", 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 + FRAME_NEG + (TONES[TONE][1].replace(", purple, magenta, pink", "") if os.environ.get("ANFG_PAL", "") in ("material", "parts") else TONES[TONE][1]) + (MATERIAL_NEG if os.environ.get("ANFG_PAL", "") in ("material", "parts") else "") + ((", " + extra_neg) if extra_neg else "")}},
"6": {"class_type": "EmptyLatentImage", "inputs": {"width": size, "height": size, "batch_size": 1}},
"7": {"class_type": "KSampler", "inputs": {"model": ["2", 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]}},
}
model = ["2", 0]
if LORA2:
name2, strength2 = LORA2.rsplit(":", 1)
wf["2b"] = {"class_type": "LoraLoaderModelOnly", "inputs": {"model": ["2", 0], "lora_name": name2, "strength_model": float(strength2)}}
model = ["2b", 0]
wf["7"]["inputs"]["model"] = model
last = "8"
if refine:
# 4배 확대 → 1024 → 약하게 다듬기(세부 선명도)
wf["40"] = {"class_type": "UpscaleModelLoader", "inputs": {"model_name": UPSCALER}}
wf["41"] = {"class_type": "ImageUpscaleWithModel", "inputs": {"upscale_model": ["40", 0], "image": ["8", 0]}}
wf["42"] = {"class_type": "ImageScale", "inputs": {"image": ["41", 0] if UPSCALE == "model" else ["8", 0], "upscale_method": "lanczos", "width": 1024, "height": 1024, "crop": "disabled"}}
if UPSCALE != "model":
del wf["40"]; del wf["41"]
wf["43"] = {"class_type": "VAEEncode", "inputs": {"pixels": ["42", 0], "vae": ["1", 2]}}
wf["44"] = {"class_type": "KSampler", "inputs": {"model": model, "positive": ["4", 0], "negative": ["5", 0], "latent_image": ["43", 0], "seed": seed + 1,
"steps": 24, "cfg": 7.0, "sampler_name": "dpmpp_2m", "scheduler": "karras", "denoise": REFINE}}
wf["45"] = {"class_type": "VAEDecode", "inputs": {"samples": ["44", 0], "vae": ["1", 2]}}
last = "45"
wf["12"] = {"class_type": "InspyrenetRembg", "inputs": {"image": [last, 0], "torchscript_jit": "default"}}
wf["9"] = {"class_type": "SaveImage", "inputs": {"images": ["12", 0], "filename_prefix": "moba_anfg"}}
return wf
def run(wf, want="9"):
pid = base.post("/prompt", {"prompt": wf, "client_id": str(uuid.uuid4())})["prompt_id"]
t0 = time.time()
while time.time() - t0 < 900:
hist = json.loads(base.get("/history/" + pid))
if pid in hist:
st = hist[pid].get("status", {})
if st.get("status_str") == "error":
raise RuntimeError(json.dumps(st)[:1500])
for nid, node in hist[pid].get("outputs", {}).items():
if nid != want:
continue
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(base.get("/view?" + q))).convert("RGBA")
time.sleep(1.0)
raise TimeoutError("comfy timeout")
# a12 검토: 가는 무기(검 · 단검 · 도끼 · 활 · 지팡이 …)는 512 에서 날이 흰 납작면으로 뭉개진다 → 768 로 그리고(모델 권장 크기), 길잡이를 끝까지 더 오래 잡는다(끝 0.7).
THIN = ("sword", "dagger", "axe", "bow", "staff", "throwing", "blunt", "hammer", "magic_dagger", "offhand_sword")
THIN_SIZE = int(os.environ.get("ANFG_THIN_SIZE", "768"))
SEED_BASE = int(os.environ.get("ANFG_SEED", "0")) # 회차마다 다른 시드를 쓰려면 바꾼다
def hash_name(name):
"""종류 이름으로 시드를 흩는다(표의 순서가 바뀌어도 같은 종류는 같은 시드)"""
return sum(ord(c) * (k + 3) for k, c in enumerate(name)) % 9973
def main():
out, tag = sys.argv[1], sys.argv[2]
strengths = [float(v) for v in (sys.argv[3] if len(sys.argv) > 3 else "0.7,1.0").split(",")]
seeds = int(sys.argv[4]) if len(sys.argv) > 4 else 2
size = int(sys.argv[5]) if len(sys.argv) > 5 else 512
refine = int(sys.argv[6]) if len(sys.argv) > 6 else 0
global SUBJECTS
mode = sys.argv[7] if len(sys.argv) > 7 else ""
cn_strength = float(sys.argv[8]) if len(sys.argv) > 8 else 0.6
cn_end = float(sys.argv[9]) if len(sys.argv) > 9 else 0.6
if mode == "fr":
SUBJECTS = SUBJECTS_FR
elif mode == "shape":
SUBJECTS = SUBJECTS_SHAPE
elif mode in ("all", "draft", "allshape"):
SUBJECTS = SUBJECTS_ALL
elif mode == "lib":
SUBJECTS = lib_subjects()
only = os.environ.get("ANFG_ONLY", "") # 쉼표로 적은 종류만 돌린다
if only:
SUBJECTS = [x for x in SUBJECTS if x[0].split("-")[0] in only.split(",")]
skip = os.environ.get("ANFG_SKIP", "") # 행 이름에 이 글자가 들어 있으면 뺀다(예: genpd — PD 합격본 윤곽 길잡이는 복제를 만든다)
if skip:
SUBJECTS = [x for x in SUBJECTS if not any(k in x[0] for k in skip.split(","))]
rows = os.environ.get("ANFG_ROWS", "") # 쉼표로 적은 행 이름(종류-길잡이)만 돌린다
if rows:
SUBJECTS = [x for x in SUBJECTS if x[0] in rows.split(",")]
os.makedirs(out, exist_ok=True)
cols = len(strengths) * seeds
cell, pad, left = 220, 4, 80
sheet = Image.new("RGB", (left + cols * (cell + pad) + pad, len(SUBJECTS) * (cell + pad) + pad), (24, 24, 28))
draw = ImageDraw.Draw(sheet)
for i, sub in enumerate(SUBJECTS):
name, desc = sub[0], sub[1]
extra_neg = sub[3] if len(sub) > 3 else ""
use_shape = len(sub) > 2 and sub[2] and mode in ("shape", "allshape")
shape = base.upload(shape_image(sub[2]), "bt_anfg_shape_%d.png" % sub[2]) if use_shape else None
if mode == "lib":
guide = Image.open(sub[2]).convert("RGB")
if "-genpd" in name:
# a11 검토: PD 합격본의 윤곽을 그대로 넘기면 색만 바뀐 복제가 나온다 → 좌우를 뒤집어 넘긴다
guide = guide.transpose(Image.FLIP_LEFT_RIGHT)
shape = base.upload(guide, "bt_anfg_lib_%s.png" % name)
y = pad + i * (cell + pad)
draw.text((pad, y + 4), name, fill=(240, 220, 160))
col = 0
for s in strengths:
for j in range(seeds):
fn = "%s_%s_s%02d_%d.png" % (tag, name, int(s * 10), j)
# 길잡이 모음 방식은 그림마다 색 조합을 돌린다(종류 안 순번 + 시드 순번 + 회차 시드)
d1 = desc.format(pal=PALETTES[(i * 7 + j * 3 + SEED_BASE) % len(PALETTES)]) if "{pal}" in desc else desc
if PARTS and name.split("-")[0] in TYPE_PARTS:
d1 = parts_desc(name.split("-")[0], i * 7 + j * 3 + SEED_BASE)
prompt = "%s, %s%s, simple background, black background, no humans" % (("fricon, " if LORA2 else "") + TRIGGER, (COMPOSE if TONE else "") + d1, TONES[TONE][0])
# 이미 만든 그림은 다시 만들지 않는다(끊겨도 이어서 돌린다)
if os.path.exists(os.path.join(out, fn)):
im = Image.open(os.path.join(out, fn)).convert("RGBA")
else:
# 종류 표가 정해진 예전 회차(a1 · a2)는 순번 시드를 그대로 쓴다(재현). 새 표는 이름으로 흩는다.
seed = (50000 + i * 37 + j * 733) if mode in ("", "fr", "shape") else (50000 + SEED_BASE + hash_name(name) + j * 733)
kind = name.split("-")[0]
wf = workflow(prompt, seed, s, THIN_SIZE if kind in THIN else size, refine, extra_neg)
if shape:
# Layer Lab 길잡이는 캐주얼한 세부가 새므로 약하게 걸어 큰 윤곽만 잡는다
weak = mode == "lib" and "-ll" in name
# 가는 형태(반지 · 활 · 지팡이 · 투척)는 ControlNet 을 일찍 풀면 다른 물건이 된다 — 끝을 0.6 아래로 내리지 않는다
end = max(cn_end, 0.7) if kind in THIN else (max(cn_end, 0.6) if kind == "ring" else cn_end)
add_shape(wf, shape, 0.4 if weak else cn_strength, 0.4 if weak else end)
elif mode == "draft":
add_draft(wf, d1, seed, cn_strength, cn_end)
im = run(wf)
im.save(os.path.join(out, fn))
# 학습 캡션으로 쓸 묘사를 곁파일로 남긴다(icon_dataset.py 가 읽는다)
with io.open(os.path.join(out, fn[:-4] + ".txt"), "w", encoding="utf-8") as f:
f.write(d1)
# 예시와 견주기 쉽게 어두운 바탕에 올린다
bg = Image.new("RGB", (cell, cell), (12, 12, 14)); small = im.resize((cell, cell), Image.LANCZOS); bg.paste(small, (0, 0), small)
sheet.paste(bg, (left + col * (cell + pad), y))
draw.text((left + col * (cell + pad) + 3, y + 2), "LoRA %.1f #%d" % (s, j), fill=(200, 200, 200))
col += 1
print("%s %s" % (tag, name)); sys.stdout.flush()
sheet.save(os.path.join(out, "%s_sheet.png" % tag))
print("sheet", os.path.join(out, "%s_sheet.png" % tag))
if __name__ == "__main__":
main()