| | from __future__ import annotations
|
| |
|
| | import re
|
| | from typing import Dict, List, Tuple
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | _OUTPUT_DEFS: List[Tuple[str, str]] = [
|
| |
|
| | ("gender_str", "STRING"),
|
| | ("gender_int", "INT"),
|
| | ("age_str", "STRING"),
|
| | ("age_int", "INT"),
|
| | ("identity_str", "STRING"),
|
| | ("eyecolor_str", "STRING"),
|
| | ("hairstyle_str", "STRING"),
|
| |
|
| |
|
| | ("topwear_str", "STRING"),
|
| | ("bellywear_str", "STRING"),
|
| | ("breastwear_str", "STRING"),
|
| |
|
| | ("handwear_left_str", "STRING"),
|
| | ("handwear_right_str", "STRING"),
|
| | ("wristwear_left_str", "STRING"),
|
| | ("wristwear_right_str", "STRING"),
|
| | ("forearm_left_str", "STRING"),
|
| | ("forearm_right_str", "STRING"),
|
| | ("elbow_left_str", "STRING"),
|
| | ("elbow_right_str", "STRING"),
|
| | ("upperarm_left_str", "STRING"),
|
| | ("upperarm_right_str", "STRING"),
|
| | ("shoulder_left_str", "STRING"),
|
| | ("shoulder_right_str", "STRING"),
|
| |
|
| | ("shank_left_str", "STRING"),
|
| | ("shank_right_str", "STRING"),
|
| |
|
| | ("knee_left_str", "STRING"),
|
| | ("knee_right_str", "STRING"),
|
| |
|
| | ("foot_left_str", "STRING"),
|
| | ("foot_right_str", "STRING"),
|
| |
|
| | ("necklace_str", "STRING"),
|
| | ("earring_left_str", "STRING"),
|
| | ("earring_right_str", "STRING"),
|
| |
|
| | ("kneewear_str", "STRING"),
|
| | ("headwear_str", "STRING"),
|
| | ("facemask_str", "STRING"),
|
| | ("sunglasses_str", "STRING"),
|
| | ("glasses_str", "STRING"),
|
| |
|
| | ("crotch_str", "STRING"),
|
| | ("belt_str", "STRING"),
|
| | ("skirt_str", "STRING"),
|
| | ("one_piece_str", "STRING"),
|
| |
|
| |
|
| | ("aesthetic_tag1", "STRING"),
|
| | ("aesthetic_tag2", "STRING"),
|
| | ("aesthetic_tag3", "STRING"),
|
| | ("aesthetic_tag4", "STRING"),
|
| | ("aesthetic_tag5", "STRING"),
|
| |
|
| | ("skin_tag1", "STRING"),
|
| | ("skin_tag2", "STRING"),
|
| | ("skin_tag3", "STRING"),
|
| | ("skin_tag4", "STRING"),
|
| | ("skin_tag5", "STRING"),
|
| |
|
| | ("expression_tag1", "STRING"),
|
| | ("expression_tag2", "STRING"),
|
| | ("expression_tag3", "STRING"),
|
| | ("expression_tag4", "STRING"),
|
| | ("expression_tag5", "STRING"),
|
| |
|
| |
|
| | ("headwear_str_2", "STRING"),
|
| |
|
| |
|
| | ("all_equip", "STRING"),
|
| |
|
| |
|
| | ("bam_ancient", "STRING"),
|
| | ]
|
| |
|
| | RETURN_NAMES_TUPLE = tuple(n for n, _t in _OUTPUT_DEFS)
|
| | RETURN_TYPES_TUPLE = tuple(_t for _n, _t in _OUTPUT_DEFS)
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | _NEGATIVE_PROMPT_1 = (
|
| | "monochrome, sketch, colorless, (asymmetrical face:1.5), "
|
| | "(asymmetrical tail-arched eyebrows:1.0), (terribly drawn eyes:1.2), "
|
| | "(heterochromia:1.5), watermark, text, visible background objects, visible floor, "
|
| | "(floor-effects:1.5), (background-effects:1.5), non-character, character-shadow, floor-shadow"
|
| | )
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def _strip_quotes(v: str) -> str:
|
| | v = (v or "").strip()
|
| | if len(v) >= 2 and ((v[0] == v[-1] == '"') or (v[0] == v[-1] == "'")):
|
| | return v[1:-1].strip()
|
| | return v
|
| |
|
| |
|
| | def _norm_key(k: str) -> str:
|
| | k = (k or "").strip().lower()
|
| | k = k.replace(" ", "_").replace("-", "_")
|
| | k = re.sub(r"_+", "_", k)
|
| | return k
|
| |
|
| |
|
| | def _safe_int(s: str, default: int = 0) -> int:
|
| | try:
|
| | return int((s or "").strip())
|
| | except Exception:
|
| | return default
|
| |
|
| |
|
| | def _norm_spaces(s: str) -> str:
|
| | s = (s or "").replace("\r", " ").replace("\n", " ")
|
| | s = re.sub(r"\s+", " ", s).strip()
|
| | return s
|
| |
|
| |
|
| | def _extract_gpt_bam_block(text: str) -> str:
|
| | """
|
| | Extract first GPT_BAM block payload (between markers).
|
| | If markers are missing, returns the whole text (still attempts key=value parsing).
|
| | """
|
| | text = text or ""
|
| | m = re.search(r"GPT_BAM_START###(.*?)###GPT_BAM_END", text, flags=re.S | re.I)
|
| | return m.group(1) if m else text
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def _clean_identity_like(s: str) -> str:
|
| | s = (s or "").strip().replace("_", " ")
|
| | return _norm_spaces(s)
|
| |
|
| |
|
| | def _clean_eyes(s: str) -> str:
|
| | s = (s or "").strip()
|
| | s = s.replace("_eyes", "")
|
| | s = s.replace("_", " ")
|
| | return _norm_spaces(s)
|
| |
|
| |
|
| | def _clean_hair(s: str) -> str:
|
| | s = (s or "").strip()
|
| | parts = [p.strip() for p in s.split(",") if p.strip()]
|
| | cleaned: List[str] = []
|
| | for p in parts:
|
| |
|
| | for suf in ("_hairstyle", "_hairsyle", "_hair"):
|
| | if p.endswith(suf):
|
| | p = p[: -len(suf)]
|
| |
|
| | p = p.replace("_hairstyle", "").replace("_hairsyle", "").replace("_hair", "")
|
| | p = p.replace("_", " ")
|
| | p = _norm_spaces(p)
|
| | if p:
|
| | cleaned.append(p)
|
| | return ", ".join(cleaned)
|
| |
|
| |
|
| | def _clean_tag(s: str, kind: str) -> str:
|
| | s = (s or "").strip()
|
| | if kind == "aesthetic":
|
| | s = s.replace("_aesthetic", "").replace("aesthetic_", "").replace("aesthetic", "")
|
| | elif kind == "skin":
|
| | s = s.replace("_skin", "").replace("skin_", "").replace("skin", "")
|
| | elif kind == "expression":
|
| | s = s.replace("_expression", "").replace("expression_", "").replace("expression", "")
|
| | s = s.replace("_", " ")
|
| | return _norm_spaces(s)
|
| |
|
| |
|
| | def _zero_if_empty(s: str) -> str:
|
| | s = _norm_spaces(s)
|
| | return s if s else "0"
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | _KEY_CANONICAL: Dict[str, str] = {
|
| | "topwear": "topwear",
|
| | "belly": "bellywear",
|
| | "bellywear": "bellywear",
|
| | "breast": "breastwear",
|
| | "breastwear": "breastwear",
|
| |
|
| | "hand": "handwear",
|
| | "handwear": "handwear",
|
| | "wrist": "wristwear",
|
| | "wristwear": "wristwear",
|
| |
|
| | "forearm": "forearm",
|
| | "elbow": "elbow",
|
| | "upperarm": "upperarm",
|
| | "upper_arm": "upperarm",
|
| | "shoulder": "shoulder",
|
| |
|
| | "shank": "shank",
|
| | "knee": "knee",
|
| |
|
| | "foot": "foot",
|
| | "footwear": "foot",
|
| | "shoe": "foot",
|
| | "shoes": "foot",
|
| |
|
| | "necklace": "necklace",
|
| |
|
| | "earring": "earring",
|
| | "earrings": "earring",
|
| |
|
| | "kneewear": "kneewear",
|
| | "headwear": "headwear",
|
| | "headwear2": "headwear2",
|
| |
|
| | "facemask": "facemask",
|
| | "face_mask": "facemask",
|
| | "mask": "facemask",
|
| |
|
| | "sunglasses": "sunglasses",
|
| | "glasses": "glasses",
|
| |
|
| | "crotch": "crotch",
|
| | "belt": "belt",
|
| | "skirt": "skirt",
|
| |
|
| | "onepiece": "one_piece",
|
| | "one_piece": "one_piece",
|
| | "one_piecewear": "one_piece",
|
| | }
|
| |
|
| | _SIDE_FIELDS: Dict[str, Tuple[str, str]] = {
|
| | "handwear": ("handwear_left_str", "handwear_right_str"),
|
| | "wristwear": ("wristwear_left_str", "wristwear_right_str"),
|
| | "forearm": ("forearm_left_str", "forearm_right_str"),
|
| | "elbow": ("elbow_left_str", "elbow_right_str"),
|
| | "upperarm": ("upperarm_left_str", "upperarm_right_str"),
|
| | "shoulder": ("shoulder_left_str", "shoulder_right_str"),
|
| | "shank": ("shank_left_str", "shank_right_str"),
|
| | "knee": ("knee_left_str", "knee_right_str"),
|
| | "foot": ("foot_left_str", "foot_right_str"),
|
| | "earring": ("earring_left_str", "earring_right_str"),
|
| | }
|
| |
|
| | _SINGLE_FIELDS: Dict[str, str] = {
|
| | "topwear": "topwear_str",
|
| | "bellywear": "bellywear_str",
|
| | "breastwear": "breastwear_str",
|
| | "necklace": "necklace_str",
|
| | "kneewear": "kneewear_str",
|
| | "headwear": "headwear_str",
|
| | "facemask": "facemask_str",
|
| | "sunglasses": "sunglasses_str",
|
| | "glasses": "glasses_str",
|
| | "crotch": "crotch_str",
|
| | "belt": "belt_str",
|
| | "skirt": "skirt_str",
|
| | "one_piece": "one_piece_str",
|
| | "headwear2": "headwear_str_2",
|
| | }
|
| |
|
| | _ALL_EQUIP_OUTPUTS = set(_SINGLE_FIELDS.values())
|
| | for lf, rf in _SIDE_FIELDS.values():
|
| | _ALL_EQUIP_OUTPUTS.add(lf)
|
| | _ALL_EQUIP_OUTPUTS.add(rf)
|
| |
|
| |
|
| | def _assign_equip(
|
| | out: Dict[str, object],
|
| | equip_values_in_order: List[str],
|
| | raw_key: str,
|
| | val: str,
|
| | ) -> None:
|
| | """
|
| | Assign equipment into structured outputs.
|
| |
|
| | Precedence rule:
|
| | - sided keys (.left/.right or _left/_right) overwrite that side
|
| | - unsided keys fill only empty sides (so sided values win even if unsided appears later)
|
| |
|
| | Also collects ALL equip values (even unknown keys) into equip_values_in_order.
|
| | """
|
| | val = (val or "").strip()
|
| | k = _norm_key(raw_key)
|
| |
|
| |
|
| | side = None
|
| | base = k
|
| |
|
| | if base.endswith(".left"):
|
| | side = "left"
|
| | base = base[:-5]
|
| | elif base.endswith(".right"):
|
| | side = "right"
|
| | base = base[:-6]
|
| |
|
| | if base.endswith("_left"):
|
| | side = "left"
|
| | base = base[:-5]
|
| | elif base.endswith("_right"):
|
| | side = "right"
|
| | base = base[:-6]
|
| |
|
| | base = base.strip("._")
|
| | base_for_lookup = base.replace(".", "_")
|
| | canonical = _KEY_CANONICAL.get(base_for_lookup, base_for_lookup)
|
| |
|
| |
|
| | if val:
|
| | equip_values_in_order.append(val)
|
| |
|
| | if canonical in _SIDE_FIELDS:
|
| | left_name, right_name = _SIDE_FIELDS[canonical]
|
| | if side == "left":
|
| | out[left_name] = val
|
| | elif side == "right":
|
| | out[right_name] = val
|
| | else:
|
| |
|
| | if not out.get(left_name, ""):
|
| | out[left_name] = val
|
| | if not out.get(right_name, ""):
|
| | out[right_name] = val
|
| |
|
| | elif canonical in _SINGLE_FIELDS:
|
| | out[_SINGLE_FIELDS[canonical]] = val
|
| |
|
| | else:
|
| |
|
| | pass
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def _parse_gpt_bam(text: str) -> Dict[str, object]:
|
| | payload = _extract_gpt_bam_block(text)
|
| | segments = [s.strip() for s in payload.split("###") if s.strip()]
|
| |
|
| |
|
| | out: Dict[str, object] = {name: (0 if t == "INT" else "") for name, t in _OUTPUT_DEFS}
|
| | for k in _ALL_EQUIP_OUTPUTS:
|
| | out[k] = ""
|
| |
|
| | equip_values_in_order: List[str] = []
|
| |
|
| | g_int = None
|
| |
|
| | for seg in segments:
|
| | if "=" in seg:
|
| | k, v = seg.split("=", 1)
|
| | elif ":" in seg:
|
| | k, v = seg.split(":", 1)
|
| | else:
|
| | continue
|
| |
|
| | k = _norm_key(k)
|
| | v = _strip_quotes(v)
|
| |
|
| |
|
| | if k in ("gender", "sex", "gender_int", "gender_num"):
|
| | vv = v.strip().lower()
|
| | if vv in ("1", "boy", "male", "m"):
|
| | g_int = 1
|
| | elif vv in ("2", "girl", "female", "f"):
|
| | g_int = 2
|
| |
|
| | elif k in ("age", "age_str"):
|
| | out["age_str"] = v.strip()
|
| | out["age_int"] = _safe_int(out["age_str"], 0)
|
| |
|
| | elif k in ("identity", "identity_str", "job", "role"):
|
| | out["identity_str"] = v.strip()
|
| |
|
| | elif k in ("eyecolor", "eye_color", "eye", "eyecolor_str"):
|
| | out["eyecolor_str"] = v.strip()
|
| |
|
| | elif k in ("hairstyle", "hair", "hairstyle_str"):
|
| | out["hairstyle_str"] = v.strip()
|
| |
|
| |
|
| | elif k.startswith("equip.") or k.startswith("equipment."):
|
| | raw_equip_key = k.split(".", 1)[1]
|
| | _assign_equip(out, equip_values_in_order, raw_equip_key, v)
|
| |
|
| |
|
| | elif k.startswith("aesthetic.") or k.startswith("aesthetic_tag"):
|
| | num = None
|
| | if k.startswith("aesthetic."):
|
| | suf = k.split(".", 1)[1]
|
| | if suf.isdigit():
|
| | num = int(suf)
|
| | else:
|
| | m = re.search(r"aesthetic_tag(\d+)", k)
|
| | if m:
|
| | num = int(m.group(1))
|
| | if num and 1 <= num <= 5:
|
| | out[f"aesthetic_tag{num}"] = v.strip()
|
| |
|
| | elif k.startswith("skin.") or k.startswith("skin_tag"):
|
| | num = None
|
| | if k.startswith("skin."):
|
| | suf = k.split(".", 1)[1]
|
| | if suf.isdigit():
|
| | num = int(suf)
|
| | else:
|
| | m = re.search(r"skin_tag(\d+)", k)
|
| | if m:
|
| | num = int(m.group(1))
|
| | if num and 1 <= num <= 5:
|
| | out[f"skin_tag{num}"] = v.strip()
|
| |
|
| | elif k.startswith("expression.") or k.startswith("expression_tag"):
|
| | num = None
|
| | if k.startswith("expression."):
|
| | suf = k.split(".", 1)[1]
|
| | if suf.isdigit():
|
| | num = int(suf)
|
| | else:
|
| | m = re.search(r"expression_tag(\d+)", k)
|
| | if m:
|
| | num = int(m.group(1))
|
| | if num and 1 <= num <= 5:
|
| | out[f"expression_tag{num}"] = v.strip()
|
| |
|
| |
|
| | elif k in ("headwear2", "headwear_tag2", "headwear_str_2", "equip_headwear2"):
|
| | out["headwear_str_2"] = v.strip()
|
| |
|
| | else:
|
| |
|
| |
|
| | pass
|
| |
|
| |
|
| | if g_int is None:
|
| | g_int = 2
|
| | out["gender_int"] = int(g_int)
|
| | out["gender_str"] = "boy" if g_int == 1 else "girl"
|
| |
|
| |
|
| | seen = set()
|
| | equip_unique: List[str] = []
|
| | for v in equip_values_in_order:
|
| | v = (v or "").strip()
|
| | if v and v not in seen:
|
| | equip_unique.append(v)
|
| | seen.add(v)
|
| | out["all_equip"] = ", ".join(equip_unique)
|
| |
|
| |
|
| | out["bam_ancient"] = _convert_to_ancient(out, equip_unique)
|
| |
|
| | return out
|
| |
|
| |
|
| | def _convert_to_ancient(parsed: Dict[str, object], equip_unique: List[str]) -> str:
|
| | gender_int = int(parsed.get("gender_int", 2) or 2)
|
| |
|
| | age_str = str(parsed.get("age_str", "") or "").strip()
|
| | if not age_str:
|
| | age_str = str(parsed.get("age_int", 0) or 0)
|
| |
|
| | identity = _clean_identity_like(str(parsed.get("identity_str", "") or ""))
|
| | eyes = _clean_eyes(str(parsed.get("eyecolor_str", "") or ""))
|
| | hair = _clean_hair(str(parsed.get("hairstyle_str", "") or ""))
|
| |
|
| |
|
| | equip_list: List[str] = list(equip_unique)
|
| |
|
| | def add_unique(val: str) -> None:
|
| | val = (val or "").strip()
|
| | if not val:
|
| | return
|
| | if val not in equip_list:
|
| | equip_list.append(val)
|
| |
|
| |
|
| | foot_l = str(parsed.get("foot_left_str", "") or "").strip()
|
| | foot_r = str(parsed.get("foot_right_str", "") or "").strip()
|
| | if not foot_l and not foot_r:
|
| | add_unique("bare foot")
|
| |
|
| |
|
| | hand_l = str(parsed.get("handwear_left_str", "") or "").strip()
|
| | hand_r = str(parsed.get("handwear_right_str", "") or "").strip()
|
| | if not hand_l and not hand_r:
|
| | add_unique("bare hands")
|
| |
|
| |
|
| | top = str(parsed.get("topwear_str", "") or "").strip()
|
| | breast = str(parsed.get("breastwear_str", "") or "").strip()
|
| | one_piece = str(parsed.get("one_piece_str", "") or "").strip()
|
| | if not top and not breast and not one_piece:
|
| | add_unique("naked breasts")
|
| |
|
| |
|
| | crotch = str(parsed.get("crotch_str", "") or "").strip()
|
| | skirt = str(parsed.get("skirt_str", "") or "").strip()
|
| | if not top and not one_piece and not crotch and not skirt:
|
| | if gender_int == 1:
|
| | add_unique("naked crotch exposed penis")
|
| | else:
|
| | add_unique("naked crotch exposed vagina")
|
| |
|
| | equip_str = ", ".join([e for e in equip_list if (e or "").strip()])
|
| |
|
| |
|
| | aest = [_clean_tag(str(parsed.get(f"aesthetic_tag{i}", "") or ""), "aesthetic") for i in range(1, 6)]
|
| | skin = [_clean_tag(str(parsed.get(f"skin_tag{i}", "") or ""), "skin") for i in range(1, 6)]
|
| | expr = [_clean_tag(str(parsed.get(f"expression_tag{i}", "") or ""), "expression") for i in range(1, 6)]
|
| |
|
| | hw_extra = _clean_identity_like(str(parsed.get("headwear_str_2", "") or ""))
|
| |
|
| |
|
| | fields = [
|
| | "START",
|
| | str(gender_int),
|
| | _zero_if_empty(age_str),
|
| | _zero_if_empty(identity),
|
| | _zero_if_empty(eyes),
|
| | _zero_if_empty(hair),
|
| | _zero_if_empty(equip_str),
|
| | *(_zero_if_empty(a) for a in aest),
|
| | *(_zero_if_empty(s) for s in skin),
|
| | *(_zero_if_empty(e) for e in expr),
|
| | _zero_if_empty(hw_extra),
|
| | "0",
|
| | "0",
|
| | "0",
|
| | _NEGATIVE_PROMPT_1,
|
| | "0",
|
| | "END",
|
| | ]
|
| |
|
| |
|
| | out = "###".join(fields[:-1]) + "###" + fields[-1] + "###"
|
| | out = _norm_spaces(out)
|
| | return out
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | class BAMParser_Ancestral:
|
| | """
|
| | Parses GPT_BAM v1 (key=value fields separated by ###) and also outputs bam_ancient.
|
| | """
|
| |
|
| | @classmethod
|
| | def INPUT_TYPES(cls):
|
| | return {
|
| | "required": {
|
| | "gpt_bam_string": ("STRING", {"multiline": True, "default": ""}),
|
| | }
|
| | }
|
| |
|
| | RETURN_TYPES = RETURN_TYPES_TUPLE
|
| | RETURN_NAMES = RETURN_NAMES_TUPLE
|
| | FUNCTION = "parse"
|
| | CATEGORY = "BAM"
|
| |
|
| | def parse(self, gpt_bam_string: str):
|
| | parsed = _parse_gpt_bam(gpt_bam_string)
|
| |
|
| |
|
| | for name, t in _OUTPUT_DEFS:
|
| | if name not in parsed:
|
| | parsed[name] = 0 if t == "INT" else ""
|
| |
|
| | return tuple(parsed[name] for name in RETURN_NAMES_TUPLE)
|
| |
|
| |
|
| | NODE_CLASS_MAPPINGS = {
|
| | "BAMParser_Ancestral": BAMParser_Ancestral,
|
| | }
|
| |
|
| | NODE_DISPLAY_NAME_MAPPINGS = {
|
| | "BAMParser_Ancestral": "BAMParser_Ancestral",
|
| | } |