MyCustomNodes / BAM_parser.py
saliacoel's picture
Upload BAM_parser.py
7a69086 verified
from __future__ import annotations
import re
from typing import Dict, List, Tuple
# ============================================================================
# Output definitions (order matters!)
# ============================================================================
_OUTPUT_DEFS: List[Tuple[str, str]] = [
# Core identity
("gender_str", "STRING"),
("gender_int", "INT"),
("age_str", "STRING"),
("age_int", "INT"),
("identity_str", "STRING"),
("eyecolor_str", "STRING"),
("hairstyle_str", "STRING"),
# Equipment (strings)
("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"),
# Tags (strings)
("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"),
# Unique headwear slot AFTER expression tags (your step 23)
("headwear_str_2", "STRING"),
# Equipment section simplified (values only, comma-separated)
("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)
# ============================================================================
# Equipment parsing helpers
# ============================================================================
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
# Map key spellings to a canonical internal key.
_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", # your example uses Footwear:
"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",
}
# Canonical keys that are side-aware (Left/Right)
_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"),
}
# Canonical keys that map to a single output
_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()
# Trim a trailing comma, if any
if val.endswith(","):
val = val[:-1].rstrip()
# old_bam: collect only non-empty values
if val:
old_values.append(val)
# Detect side suffix
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:
# "(Knee:...)" style => both sides
out[left_name] = val
out[right_name] = val
elif canonical in _SINGLE_FIELDS:
out[_SINGLE_FIELDS[canonical]] = val
else:
# Unknown equipment key => ignored for structured outputs,
# but still included in old_bam via old_values.
pass
old_bam = ", ".join(v for v in old_values if v)
return out, old_bam
# ============================================================================
# BAM parsing
# ============================================================================
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,
}
# Equipment
out.update(equip_out)
# Aesthetic tags
for i in range(5):
out[f"aesthetic_tag{i+1}"] = _get_part(parts, 6 + i).strip()
# Skin tags
for i in range(5):
out[f"skin_tag{i+1}"] = _get_part(parts, 11 + i).strip()
# Expression tags
for i in range(5):
out[f"expression_tag{i+1}"] = _get_part(parts, 16 + i).strip()
# Outer-layer headwear slot
out["headwear_str_2"] = _get_part(parts, 21).strip()
return out
# ============================================================================
# ComfyUI node
# ============================================================================
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)
# Guarantee every declared output exists (malformed BAM => defaults)
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",
}