saliacoel commited on
Commit
7a69086
·
verified ·
1 Parent(s): fc6c8da

Upload BAM_parser.py

Browse files
Files changed (1) hide show
  1. BAM_parser.py +399 -0
BAM_parser.py ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Dict, List, Tuple
5
+
6
+
7
+ # ============================================================================
8
+ # Output definitions (order matters!)
9
+ # ============================================================================
10
+ _OUTPUT_DEFS: List[Tuple[str, str]] = [
11
+ # Core identity
12
+ ("gender_str", "STRING"),
13
+ ("gender_int", "INT"),
14
+ ("age_str", "STRING"),
15
+ ("age_int", "INT"),
16
+ ("identity_str", "STRING"),
17
+ ("eyecolor_str", "STRING"),
18
+ ("hairstyle_str", "STRING"),
19
+
20
+ # Equipment (strings)
21
+ ("topwear_str", "STRING"),
22
+ ("bellywear_str", "STRING"),
23
+ ("breastwear_str", "STRING"),
24
+ ("handwear_left_str", "STRING"),
25
+ ("handwear_right_str", "STRING"),
26
+ ("wristwear_left_str", "STRING"),
27
+ ("wristwear_right_str", "STRING"),
28
+ ("forearm_left_str", "STRING"),
29
+ ("forearm_right_str", "STRING"),
30
+ ("elbow_left_str", "STRING"),
31
+ ("elbow_right_str", "STRING"),
32
+ ("upperarm_left_str", "STRING"),
33
+ ("upperarm_right_str", "STRING"),
34
+ ("shoulder_left_str", "STRING"),
35
+ ("shoulder_right_str", "STRING"),
36
+ ("shank_left_str", "STRING"),
37
+ ("shank_right_str", "STRING"),
38
+ ("knee_left_str", "STRING"),
39
+ ("knee_right_str", "STRING"),
40
+ ("foot_left_str", "STRING"),
41
+ ("foot_right_str", "STRING"),
42
+ ("necklace_str", "STRING"),
43
+ ("earring_left_str", "STRING"),
44
+ ("earring_right_str", "STRING"),
45
+ ("kneewear_str", "STRING"),
46
+ ("headwear_str", "STRING"),
47
+ ("facemask_str", "STRING"),
48
+ ("sunglasses_str", "STRING"),
49
+ ("glasses_str", "STRING"),
50
+ ("crotch_str", "STRING"),
51
+ ("one_piece_str", "STRING"),
52
+
53
+ # Tags (strings)
54
+ ("aesthetic_tag1", "STRING"),
55
+ ("aesthetic_tag2", "STRING"),
56
+ ("aesthetic_tag3", "STRING"),
57
+ ("aesthetic_tag4", "STRING"),
58
+ ("aesthetic_tag5", "STRING"),
59
+
60
+ ("skin_tag1", "STRING"),
61
+ ("skin_tag2", "STRING"),
62
+ ("skin_tag3", "STRING"),
63
+ ("skin_tag4", "STRING"),
64
+ ("skin_tag5", "STRING"),
65
+
66
+ ("expression_tag1", "STRING"),
67
+ ("expression_tag2", "STRING"),
68
+ ("expression_tag3", "STRING"),
69
+ ("expression_tag4", "STRING"),
70
+ ("expression_tag5", "STRING"),
71
+
72
+ # Unique headwear slot AFTER expression tags (your step 23)
73
+ ("headwear_str_2", "STRING"),
74
+
75
+ # Equipment section simplified (values only, comma-separated)
76
+ ("old_bam", "STRING"),
77
+ ]
78
+
79
+ RETURN_NAMES_TUPLE = tuple(name for name, _t in _OUTPUT_DEFS)
80
+ RETURN_TYPES_TUPLE = tuple(_t for _name, _t in _OUTPUT_DEFS)
81
+
82
+
83
+ # ============================================================================
84
+ # Equipment parsing helpers
85
+ # ============================================================================
86
+ def _extract_parenthesized_items(s: str) -> List[str]:
87
+ """
88
+ Extract top-level (...) items from a string, handling nested parentheses.
89
+ Returns inside text of each (...) (outer parens removed).
90
+ """
91
+ items: List[str] = []
92
+ depth = 0
93
+ buf: List[str] = []
94
+
95
+ for ch in s or "":
96
+ if ch == "(":
97
+ if depth == 0:
98
+ buf = []
99
+ else:
100
+ buf.append(ch)
101
+ depth += 1
102
+ elif ch == ")":
103
+ if depth > 0:
104
+ depth -= 1
105
+ if depth == 0:
106
+ item = "".join(buf).strip()
107
+ if item:
108
+ items.append(item)
109
+ else:
110
+ buf.append(ch)
111
+ else:
112
+ if depth > 0:
113
+ buf.append(ch)
114
+
115
+ return items
116
+
117
+
118
+ def _normalize_key(k: str) -> str:
119
+ k = (k or "").strip().lower()
120
+ k = k.replace(" ", "_").replace("-", "_")
121
+ k = re.sub(r"_+", "_", k)
122
+ return k
123
+
124
+
125
+ # Map key spellings to a canonical internal key.
126
+ _KEY_CANONICAL: Dict[str, str] = {
127
+ "belly": "bellywear",
128
+ "bellywear": "bellywear",
129
+
130
+ "topwear": "topwear",
131
+
132
+ "breast": "breastwear",
133
+ "breastwear": "breastwear",
134
+
135
+ "hand": "handwear",
136
+ "handwear": "handwear",
137
+
138
+ "wrist": "wristwear",
139
+ "wristwear": "wristwear",
140
+
141
+ "forearm": "forearm",
142
+ "elbow": "elbow",
143
+
144
+ "upperarm": "upperarm",
145
+ "upper_arm": "upperarm",
146
+
147
+ "shoulder": "shoulder",
148
+ "shank": "shank",
149
+
150
+ "knee": "knee",
151
+
152
+ "foot": "foot",
153
+ "footwear": "foot", # your example uses Footwear:
154
+ "shoe": "foot",
155
+ "shoes": "foot",
156
+
157
+ "necklace": "necklace",
158
+
159
+ "earring": "earring",
160
+ "earrings": "earring",
161
+
162
+ "kneewear": "kneewear",
163
+ "headwear": "headwear",
164
+
165
+ "facemask": "facemask",
166
+ "face_mask": "facemask",
167
+ "mask": "facemask",
168
+
169
+ "sunglasses": "sunglasses",
170
+ "glasses": "glasses",
171
+
172
+ "crotch": "crotch",
173
+
174
+ "onepiece": "one_piece",
175
+ "one_piece": "one_piece",
176
+ "one_piecewear": "one_piece",
177
+ }
178
+
179
+ # Canonical keys that are side-aware (Left/Right)
180
+ _SIDE_FIELDS: Dict[str, Tuple[str, str]] = {
181
+ "handwear": ("handwear_left_str", "handwear_right_str"),
182
+ "wristwear": ("wristwear_left_str", "wristwear_right_str"),
183
+ "forearm": ("forearm_left_str", "forearm_right_str"),
184
+ "elbow": ("elbow_left_str", "elbow_right_str"),
185
+ "upperarm": ("upperarm_left_str", "upperarm_right_str"),
186
+ "shoulder": ("shoulder_left_str", "shoulder_right_str"),
187
+ "shank": ("shank_left_str", "shank_right_str"),
188
+ "knee": ("knee_left_str", "knee_right_str"),
189
+ "foot": ("foot_left_str", "foot_right_str"),
190
+ "earring": ("earring_left_str", "earring_right_str"),
191
+ }
192
+
193
+ # Canonical keys that map to a single output
194
+ _SINGLE_FIELDS: Dict[str, str] = {
195
+ "topwear": "topwear_str",
196
+ "bellywear": "bellywear_str",
197
+ "breastwear": "breastwear_str",
198
+ "necklace": "necklace_str",
199
+ "kneewear": "kneewear_str",
200
+ "headwear": "headwear_str",
201
+ "facemask": "facemask_str",
202
+ "sunglasses": "sunglasses_str",
203
+ "glasses": "glasses_str",
204
+ "crotch": "crotch_str",
205
+ "one_piece": "one_piece_str",
206
+ }
207
+
208
+ _ALL_EQUIP_OUTPUTS = set(_SINGLE_FIELDS.values())
209
+ for lf, rf in _SIDE_FIELDS.values():
210
+ _ALL_EQUIP_OUTPUTS.add(lf)
211
+ _ALL_EQUIP_OUTPUTS.add(rf)
212
+
213
+
214
+ def _parse_equipment(equip: str) -> Tuple[Dict[str, str], str]:
215
+ """
216
+ Parse equipment section like:
217
+ (Knee_Left: Blue Skater Kneepad), (Knee:Green Skater Kneepads), (Belly:), ...
218
+
219
+ Rules implemented:
220
+ - Missing entries => output stays ""
221
+ - Empty "(Belly:)" => output ""
222
+ - Side-less "(Knee:...)" => assigns to BOTH left and right outputs
223
+ - old_bam => values-only, comma-separated, in original order
224
+ """
225
+ out: Dict[str, str] = {name: "" for name in _ALL_EQUIP_OUTPUTS}
226
+ old_values: List[str] = []
227
+
228
+ for item in _extract_parenthesized_items(equip or ""):
229
+ if ":" not in item:
230
+ continue
231
+
232
+ raw_key, raw_val = item.split(":", 1)
233
+ key = _normalize_key(raw_key)
234
+ val = (raw_val or "").strip()
235
+
236
+ # Trim a trailing comma, if any
237
+ if val.endswith(","):
238
+ val = val[:-1].rstrip()
239
+
240
+ # old_bam: collect only non-empty values
241
+ if val:
242
+ old_values.append(val)
243
+
244
+ # Detect side suffix
245
+ side = None
246
+ base_key = key
247
+ if base_key.endswith("_left"):
248
+ side = "left"
249
+ base_key = base_key[:-5]
250
+ elif base_key.endswith("_right"):
251
+ side = "right"
252
+ base_key = base_key[:-6]
253
+ base_key = base_key.rstrip("_")
254
+
255
+ canonical = _KEY_CANONICAL.get(base_key, base_key)
256
+
257
+ if canonical in _SIDE_FIELDS:
258
+ left_name, right_name = _SIDE_FIELDS[canonical]
259
+ if side == "left":
260
+ out[left_name] = val
261
+ elif side == "right":
262
+ out[right_name] = val
263
+ else:
264
+ # "(Knee:...)" style => both sides
265
+ out[left_name] = val
266
+ out[right_name] = val
267
+ elif canonical in _SINGLE_FIELDS:
268
+ out[_SINGLE_FIELDS[canonical]] = val
269
+ else:
270
+ # Unknown equipment key => ignored for structured outputs,
271
+ # but still included in old_bam via old_values.
272
+ pass
273
+
274
+ old_bam = ", ".join(v for v in old_values if v)
275
+ return out, old_bam
276
+
277
+
278
+ # ============================================================================
279
+ # BAM parsing
280
+ # ============================================================================
281
+ def _get_part(parts: List[str], idx: int) -> str:
282
+ return parts[idx] if idx < len(parts) else ""
283
+
284
+
285
+ def _safe_int(s: str, default: int = 0) -> int:
286
+ try:
287
+ return int((s or "").strip())
288
+ except Exception:
289
+ return default
290
+
291
+
292
+ def _parse_bam(bam: str) -> Dict[str, object]:
293
+ """
294
+ Expected structure after the first START### marker:
295
+
296
+ 0 gender_num
297
+ 1 age
298
+ 2 identity
299
+ 3 eyecolor
300
+ 4 hairstyle
301
+ 5 equipment
302
+ 6..10 aesthetic_tag1..5
303
+ 11..15 skin_tag1..5
304
+ 16..20 expression_tag1..5
305
+ 21 headwear_str_2
306
+ 22+ irrelevant
307
+ """
308
+ bam = bam or ""
309
+ marker = "START###"
310
+ idx = bam.find(marker)
311
+ payload = bam[idx + len(marker):] if idx != -1 else bam
312
+
313
+ parts = payload.split("###")
314
+
315
+ gender_token = _get_part(parts, 0).strip()
316
+ gender_int = 1 if gender_token == "1" else 2
317
+ gender_str = "boy" if gender_int == 1 else "girl"
318
+
319
+ age_str = _get_part(parts, 1).strip()
320
+ age_int = _safe_int(age_str, default=0)
321
+
322
+ identity_str = _get_part(parts, 2).strip()
323
+ eyecolor_str = _get_part(parts, 3).strip()
324
+ hairstyle_str = _get_part(parts, 4).strip()
325
+
326
+ equipment_raw = _get_part(parts, 5).strip()
327
+ equip_out, old_bam = _parse_equipment(equipment_raw)
328
+
329
+ out: Dict[str, object] = {
330
+ "gender_str": gender_str,
331
+ "gender_int": gender_int,
332
+ "age_str": age_str,
333
+ "age_int": age_int,
334
+ "identity_str": identity_str,
335
+ "eyecolor_str": eyecolor_str,
336
+ "hairstyle_str": hairstyle_str,
337
+ "old_bam": old_bam,
338
+ }
339
+
340
+ # Equipment
341
+ out.update(equip_out)
342
+
343
+ # Aesthetic tags
344
+ for i in range(5):
345
+ out[f"aesthetic_tag{i+1}"] = _get_part(parts, 6 + i).strip()
346
+
347
+ # Skin tags
348
+ for i in range(5):
349
+ out[f"skin_tag{i+1}"] = _get_part(parts, 11 + i).strip()
350
+
351
+ # Expression tags
352
+ for i in range(5):
353
+ out[f"expression_tag{i+1}"] = _get_part(parts, 16 + i).strip()
354
+
355
+ # Outer-layer headwear slot
356
+ out["headwear_str_2"] = _get_part(parts, 21).strip()
357
+
358
+ return out
359
+
360
+
361
+ # ============================================================================
362
+ # ComfyUI node
363
+ # ============================================================================
364
+ class BAMFormatParser:
365
+ """
366
+ ComfyUI custom node: parses your BAM string and exposes each field as outputs.
367
+ """
368
+
369
+ @classmethod
370
+ def INPUT_TYPES(cls):
371
+ return {
372
+ "required": {
373
+ "bam_string": ("STRING", {"multiline": True, "default": ""}),
374
+ }
375
+ }
376
+
377
+ RETURN_TYPES = RETURN_TYPES_TUPLE
378
+ RETURN_NAMES = RETURN_NAMES_TUPLE
379
+ FUNCTION = "parse"
380
+ CATEGORY = "BAM"
381
+
382
+ def parse(self, bam_string: str):
383
+ parsed = _parse_bam(bam_string)
384
+
385
+ # Guarantee every declared output exists (malformed BAM => defaults)
386
+ for name, t in _OUTPUT_DEFS:
387
+ if name not in parsed:
388
+ parsed[name] = 0 if t == "INT" else ""
389
+
390
+ return tuple(parsed[name] for name in RETURN_NAMES_TUPLE)
391
+
392
+
393
+ NODE_CLASS_MAPPINGS = {
394
+ "BAMFormatParser": BAMFormatParser,
395
+ }
396
+
397
+ NODE_DISPLAY_NAME_MAPPINGS = {
398
+ "BAMFormatParser": "BAM Format Parser",
399
+ }