| 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"),
|
| ("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"),
|
|
|
|
|
| ("old_bam", "STRING"),
|
| ]
|
|
|
| RETURN_NAMES_TUPLE = tuple(name for name, _t in _OUTPUT_DEFS)
|
| RETURN_TYPES_TUPLE = tuple(_t for _name, _t in _OUTPUT_DEFS)
|
|
|
|
|
|
|
|
|
|
|
| def _extract_parenthesized_items(s: str) -> List[str]:
|
| """
|
| Extract top-level (...) items from a string, handling nested parentheses.
|
| Returns inside text of each (...) (outer parens removed).
|
| """
|
| items: List[str] = []
|
| depth = 0
|
| buf: List[str] = []
|
|
|
| for ch in s or "":
|
| if ch == "(":
|
| if depth == 0:
|
| buf = []
|
| else:
|
| buf.append(ch)
|
| depth += 1
|
| elif ch == ")":
|
| if depth > 0:
|
| depth -= 1
|
| if depth == 0:
|
| item = "".join(buf).strip()
|
| if item:
|
| items.append(item)
|
| else:
|
| buf.append(ch)
|
| else:
|
| if depth > 0:
|
| buf.append(ch)
|
|
|
| return items
|
|
|
|
|
| def _normalize_key(k: str) -> str:
|
| k = (k or "").strip().lower()
|
| k = k.replace(" ", "_").replace("-", "_")
|
| k = re.sub(r"_+", "_", k)
|
| return k
|
|
|
|
|
|
|
| _KEY_CANONICAL: Dict[str, str] = {
|
| "belly": "bellywear",
|
| "bellywear": "bellywear",
|
|
|
| "topwear": "topwear",
|
|
|
| "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",
|
|
|
| "facemask": "facemask",
|
| "face_mask": "facemask",
|
| "mask": "facemask",
|
|
|
| "sunglasses": "sunglasses",
|
| "glasses": "glasses",
|
|
|
| "crotch": "crotch",
|
|
|
| "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",
|
| "one_piece": "one_piece_str",
|
| }
|
|
|
| _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 _parse_equipment(equip: str) -> Tuple[Dict[str, str], str]:
|
| """
|
| Parse equipment section like:
|
| (Knee_Left: Blue Skater Kneepad), (Knee:Green Skater Kneepads), (Belly:), ...
|
|
|
| Rules implemented:
|
| - Missing entries => output stays ""
|
| - Empty "(Belly:)" => output ""
|
| - Side-less "(Knee:...)" => assigns to BOTH left and right outputs
|
| - old_bam => values-only, comma-separated, in original order
|
| """
|
| out: Dict[str, str] = {name: "" for name in _ALL_EQUIP_OUTPUTS}
|
| old_values: List[str] = []
|
|
|
| for item in _extract_parenthesized_items(equip or ""):
|
| if ":" not in item:
|
| continue
|
|
|
| raw_key, raw_val = item.split(":", 1)
|
| key = _normalize_key(raw_key)
|
| val = (raw_val or "").strip()
|
|
|
|
|
| if val.endswith(","):
|
| val = val[:-1].rstrip()
|
|
|
|
|
| if val:
|
| old_values.append(val)
|
|
|
|
|
| side = None
|
| base_key = key
|
| if base_key.endswith("_left"):
|
| side = "left"
|
| base_key = base_key[:-5]
|
| elif base_key.endswith("_right"):
|
| side = "right"
|
| base_key = base_key[:-6]
|
| base_key = base_key.rstrip("_")
|
|
|
| canonical = _KEY_CANONICAL.get(base_key, base_key)
|
|
|
| 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:
|
|
|
| out[left_name] = val
|
| out[right_name] = val
|
| elif canonical in _SINGLE_FIELDS:
|
| out[_SINGLE_FIELDS[canonical]] = val
|
| else:
|
|
|
|
|
| pass
|
|
|
| old_bam = ", ".join(v for v in old_values if v)
|
| return out, old_bam
|
|
|
|
|
|
|
|
|
|
|
| def _get_part(parts: List[str], idx: int) -> str:
|
| return parts[idx] if idx < len(parts) else ""
|
|
|
|
|
| def _safe_int(s: str, default: int = 0) -> int:
|
| try:
|
| return int((s or "").strip())
|
| except Exception:
|
| return default
|
|
|
|
|
| def _parse_bam(bam: str) -> Dict[str, object]:
|
| """
|
| Expected structure after the first START### marker:
|
|
|
| 0 gender_num
|
| 1 age
|
| 2 identity
|
| 3 eyecolor
|
| 4 hairstyle
|
| 5 equipment
|
| 6..10 aesthetic_tag1..5
|
| 11..15 skin_tag1..5
|
| 16..20 expression_tag1..5
|
| 21 headwear_str_2
|
| 22+ irrelevant
|
| """
|
| bam = bam or ""
|
| marker = "START###"
|
| idx = bam.find(marker)
|
| payload = bam[idx + len(marker):] if idx != -1 else bam
|
|
|
| parts = payload.split("###")
|
|
|
| gender_token = _get_part(parts, 0).strip()
|
| gender_int = 1 if gender_token == "1" else 2
|
| gender_str = "boy" if gender_int == 1 else "girl"
|
|
|
| age_str = _get_part(parts, 1).strip()
|
| age_int = _safe_int(age_str, default=0)
|
|
|
| identity_str = _get_part(parts, 2).strip()
|
| eyecolor_str = _get_part(parts, 3).strip()
|
| hairstyle_str = _get_part(parts, 4).strip()
|
|
|
| equipment_raw = _get_part(parts, 5).strip()
|
| equip_out, old_bam = _parse_equipment(equipment_raw)
|
|
|
| out: Dict[str, object] = {
|
| "gender_str": gender_str,
|
| "gender_int": gender_int,
|
| "age_str": age_str,
|
| "age_int": age_int,
|
| "identity_str": identity_str,
|
| "eyecolor_str": eyecolor_str,
|
| "hairstyle_str": hairstyle_str,
|
| "old_bam": old_bam,
|
| }
|
|
|
|
|
| out.update(equip_out)
|
|
|
|
|
| for i in range(5):
|
| out[f"aesthetic_tag{i+1}"] = _get_part(parts, 6 + i).strip()
|
|
|
|
|
| for i in range(5):
|
| out[f"skin_tag{i+1}"] = _get_part(parts, 11 + i).strip()
|
|
|
|
|
| for i in range(5):
|
| out[f"expression_tag{i+1}"] = _get_part(parts, 16 + i).strip()
|
|
|
|
|
| out["headwear_str_2"] = _get_part(parts, 21).strip()
|
|
|
| return out
|
|
|
|
|
|
|
|
|
|
|
| class BAMFormatParser:
|
| """
|
| ComfyUI custom node: parses your BAM string and exposes each field as outputs.
|
| """
|
|
|
| @classmethod
|
| def INPUT_TYPES(cls):
|
| return {
|
| "required": {
|
| "bam_string": ("STRING", {"multiline": True, "default": ""}),
|
| }
|
| }
|
|
|
| RETURN_TYPES = RETURN_TYPES_TUPLE
|
| RETURN_NAMES = RETURN_NAMES_TUPLE
|
| FUNCTION = "parse"
|
| CATEGORY = "BAM"
|
|
|
| def parse(self, bam_string: str):
|
| parsed = _parse_bam(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 = {
|
| "BAMFormatParser": BAMFormatParser,
|
| }
|
|
|
| NODE_DISPLAY_NAME_MAPPINGS = {
|
| "BAMFormatParser": "BAM Format Parser",
|
| } |