saliacoel commited on
Commit
b7655bb
·
verified ·
1 Parent(s): f559035

Upload Bam_GPT_Parser.py

Browse files
Files changed (1) hide show
  1. Bam_GPT_Parser.py +429 -0
Bam_GPT_Parser.py ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Dict, List, Tuple
5
+
6
+
7
+ # =============================================================================
8
+ # Outputs (order matters)
9
+ # =============================================================================
10
+ _OUTPUT_DEFS: List[Tuple[str, str]] = [
11
+ # Core
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
21
+ ("topwear_str", "STRING"),
22
+ ("bellywear_str", "STRING"),
23
+ ("breastwear_str", "STRING"),
24
+
25
+ ("handwear_left_str", "STRING"),
26
+ ("handwear_right_str", "STRING"),
27
+ ("wristwear_left_str", "STRING"),
28
+ ("wristwear_right_str", "STRING"),
29
+ ("forearm_left_str", "STRING"),
30
+ ("forearm_right_str", "STRING"),
31
+ ("elbow_left_str", "STRING"),
32
+ ("elbow_right_str", "STRING"),
33
+ ("upperarm_left_str", "STRING"),
34
+ ("upperarm_right_str", "STRING"),
35
+ ("shoulder_left_str", "STRING"),
36
+ ("shoulder_right_str", "STRING"),
37
+
38
+ ("shank_left_str", "STRING"),
39
+ ("shank_right_str", "STRING"),
40
+
41
+ ("knee_left_str", "STRING"),
42
+ ("knee_right_str", "STRING"),
43
+
44
+ ("foot_left_str", "STRING"),
45
+ ("foot_right_str", "STRING"),
46
+
47
+ ("necklace_str", "STRING"),
48
+ ("earring_left_str", "STRING"),
49
+ ("earring_right_str", "STRING"),
50
+
51
+ ("kneewear_str", "STRING"),
52
+ ("headwear_str", "STRING"),
53
+ ("facemask_str", "STRING"),
54
+ ("sunglasses_str", "STRING"),
55
+ ("glasses_str", "STRING"),
56
+
57
+ ("crotch_str", "STRING"),
58
+ ("one_piece_str", "STRING"),
59
+
60
+ # Tags
61
+ ("aesthetic_tag1", "STRING"),
62
+ ("aesthetic_tag2", "STRING"),
63
+ ("aesthetic_tag3", "STRING"),
64
+ ("aesthetic_tag4", "STRING"),
65
+ ("aesthetic_tag5", "STRING"),
66
+
67
+ ("skin_tag1", "STRING"),
68
+ ("skin_tag2", "STRING"),
69
+ ("skin_tag3", "STRING"),
70
+ ("skin_tag4", "STRING"),
71
+ ("skin_tag5", "STRING"),
72
+
73
+ ("expression_tag1", "STRING"),
74
+ ("expression_tag2", "STRING"),
75
+ ("expression_tag3", "STRING"),
76
+ ("expression_tag4", "STRING"),
77
+ ("expression_tag5", "STRING"),
78
+
79
+ # Unique extra headwear slot
80
+ ("headwear_str_2", "STRING"),
81
+
82
+ # Flattened equipment values
83
+ ("old_bam", "STRING"),
84
+ ]
85
+
86
+ RETURN_NAMES_TUPLE = tuple(n for n, _t in _OUTPUT_DEFS)
87
+ RETURN_TYPES_TUPLE = tuple(_t for _n, _t in _OUTPUT_DEFS)
88
+
89
+
90
+ # =============================================================================
91
+ # Helpers
92
+ # =============================================================================
93
+ def _strip_quotes(v: str) -> str:
94
+ v = (v or "").strip()
95
+ if len(v) >= 2 and ((v[0] == v[-1] == '"') or (v[0] == v[-1] == "'")):
96
+ return v[1:-1].strip()
97
+ return v
98
+
99
+
100
+ def _norm_key(k: str) -> str:
101
+ k = (k or "").strip().lower()
102
+ k = k.replace(" ", "_").replace("-", "_")
103
+ k = re.sub(r"_+", "_", k)
104
+ return k
105
+
106
+
107
+ def _safe_int(s: str, default: int = 0) -> int:
108
+ try:
109
+ return int((s or "").strip())
110
+ except Exception:
111
+ return default
112
+
113
+
114
+ def _extract_gpt_bam_block(text: str) -> str:
115
+ """
116
+ Extract the first GPT_BAM block payload (between markers).
117
+ If markers are missing, returns the whole text (still attempts key=value parsing).
118
+ """
119
+ text = text or ""
120
+ m = re.search(r"GPT_BAM_START###(.*?)###GPT_BAM_END", text, flags=re.S | re.I)
121
+ return m.group(1) if m else text
122
+
123
+
124
+ # =============================================================================
125
+ # Equipment mapping
126
+ # =============================================================================
127
+ _KEY_CANONICAL: Dict[str, str] = {
128
+ "topwear": "topwear",
129
+ "belly": "bellywear",
130
+ "bellywear": "bellywear",
131
+ "breast": "breastwear",
132
+ "breastwear": "breastwear",
133
+
134
+ "hand": "handwear",
135
+ "handwear": "handwear",
136
+ "wrist": "wristwear",
137
+ "wristwear": "wristwear",
138
+
139
+ "forearm": "forearm",
140
+ "elbow": "elbow",
141
+ "upperarm": "upperarm",
142
+ "upper_arm": "upperarm",
143
+ "shoulder": "shoulder",
144
+
145
+ "shank": "shank",
146
+ "knee": "knee",
147
+
148
+ "foot": "foot",
149
+ "footwear": "foot",
150
+ "shoe": "foot",
151
+ "shoes": "foot",
152
+
153
+ "necklace": "necklace",
154
+
155
+ "earring": "earring",
156
+ "earrings": "earring",
157
+
158
+ "kneewear": "kneewear",
159
+ "headwear": "headwear",
160
+ "headwear2": "headwear2",
161
+
162
+ "facemask": "facemask",
163
+ "face_mask": "facemask",
164
+ "mask": "facemask",
165
+
166
+ "sunglasses": "sunglasses",
167
+ "glasses": "glasses",
168
+
169
+ "crotch": "crotch",
170
+
171
+ "onepiece": "one_piece",
172
+ "one_piece": "one_piece",
173
+ "one_piecewear": "one_piece",
174
+ }
175
+
176
+ _SIDE_FIELDS: Dict[str, Tuple[str, str]] = {
177
+ "handwear": ("handwear_left_str", "handwear_right_str"),
178
+ "wristwear": ("wristwear_left_str", "wristwear_right_str"),
179
+ "forearm": ("forearm_left_str", "forearm_right_str"),
180
+ "elbow": ("elbow_left_str", "elbow_right_str"),
181
+ "upperarm": ("upperarm_left_str", "upperarm_right_str"),
182
+ "shoulder": ("shoulder_left_str", "shoulder_right_str"),
183
+ "shank": ("shank_left_str", "shank_right_str"),
184
+ "knee": ("knee_left_str", "knee_right_str"),
185
+ "foot": ("foot_left_str", "foot_right_str"),
186
+ "earring": ("earring_left_str", "earring_right_str"),
187
+ }
188
+
189
+ _SINGLE_FIELDS: Dict[str, str] = {
190
+ "topwear": "topwear_str",
191
+ "bellywear": "bellywear_str",
192
+ "breastwear": "breastwear_str",
193
+ "necklace": "necklace_str",
194
+ "kneewear": "kneewear_str",
195
+ "headwear": "headwear_str",
196
+ "facemask": "facemask_str",
197
+ "sunglasses": "sunglasses_str",
198
+ "glasses": "glasses_str",
199
+ "crotch": "crotch_str",
200
+ "one_piece": "one_piece_str",
201
+ "headwear2": "headwear_str_2",
202
+ }
203
+
204
+ _ALL_EQUIP_OUTPUTS = set(_SINGLE_FIELDS.values())
205
+ for lf, rf in _SIDE_FIELDS.values():
206
+ _ALL_EQUIP_OUTPUTS.add(lf)
207
+ _ALL_EQUIP_OUTPUTS.add(rf)
208
+
209
+
210
+ def _assign_equip(
211
+ out: Dict[str, object],
212
+ old_vals: List[str],
213
+ raw_key: str,
214
+ val: str,
215
+ ) -> None:
216
+ """
217
+ Assign equipment into outputs.
218
+
219
+ Precedence rule:
220
+ - sided keys (.left/.right) ALWAYS overwrite that side
221
+ - unsided keys assign only to sides that are still empty
222
+ (so sided values "win" even if unsided appears later)
223
+ """
224
+ val = (val or "").strip()
225
+ k = _norm_key(raw_key)
226
+
227
+ # detect side
228
+ side = None
229
+ base = k
230
+
231
+ if base.endswith(".left"):
232
+ side = "left"
233
+ base = base[:-5]
234
+ elif base.endswith(".right"):
235
+ side = "right"
236
+ base = base[:-6]
237
+
238
+ if base.endswith("_left"):
239
+ side = "left"
240
+ base = base[:-5]
241
+ elif base.endswith("_right"):
242
+ side = "right"
243
+ base = base[:-6]
244
+
245
+ base = base.strip("._")
246
+ base_for_lookup = base.replace(".", "_")
247
+ canonical = _KEY_CANONICAL.get(base_for_lookup, base_for_lookup)
248
+
249
+ # always collect equip values (even unknown keys) for old_bam
250
+ if val:
251
+ old_vals.append(val)
252
+
253
+ if canonical in _SIDE_FIELDS:
254
+ left_name, right_name = _SIDE_FIELDS[canonical]
255
+ if side == "left":
256
+ out[left_name] = val
257
+ elif side == "right":
258
+ out[right_name] = val
259
+ else:
260
+ # unsided: fill only empties
261
+ if not out.get(left_name, ""):
262
+ out[left_name] = val
263
+ if not out.get(right_name, ""):
264
+ out[right_name] = val
265
+
266
+ elif canonical in _SINGLE_FIELDS:
267
+ out[_SINGLE_FIELDS[canonical]] = val
268
+
269
+ else:
270
+ # unknown equip key -> ignore for structured outputs
271
+ pass
272
+
273
+
274
+ # =============================================================================
275
+ # GPT_BAM parsing
276
+ # =============================================================================
277
+ def _parse_gpt_bam(text: str) -> Dict[str, object]:
278
+ payload = _extract_gpt_bam_block(text)
279
+ segments = [s.strip() for s in payload.split("###") if s.strip()]
280
+
281
+ # defaults
282
+ out: Dict[str, object] = {name: (0 if t == "INT" else "") for name, t in _OUTPUT_DEFS}
283
+ for k in _ALL_EQUIP_OUTPUTS:
284
+ out[k] = ""
285
+
286
+ # collect equip values for old_bam
287
+ old_vals: List[str] = []
288
+
289
+ # gender temp
290
+ g_int = None
291
+
292
+ for seg in segments:
293
+ if "=" in seg:
294
+ k, v = seg.split("=", 1)
295
+ elif ":" in seg:
296
+ k, v = seg.split(":", 1)
297
+ else:
298
+ continue
299
+
300
+ k = _norm_key(k)
301
+ v = _strip_quotes(v)
302
+
303
+ # core
304
+ if k in ("gender", "sex", "gender_int", "gender_num"):
305
+ vv = v.strip().lower()
306
+ if vv in ("1", "boy", "male", "m"):
307
+ g_int = 1
308
+ elif vv in ("2", "girl", "female", "f"):
309
+ g_int = 2
310
+
311
+ elif k in ("age", "age_str"):
312
+ out["age_str"] = v.strip()
313
+ out["age_int"] = _safe_int(out["age_str"], 0)
314
+
315
+ elif k in ("identity", "identity_str", "job", "role"):
316
+ out["identity_str"] = v.strip()
317
+
318
+ elif k in ("eyecolor", "eye_color", "eye", "eyecolor_str"):
319
+ out["eyecolor_str"] = v.strip()
320
+
321
+ elif k in ("hairstyle", "hair", "hairstyle_str"):
322
+ out["hairstyle_str"] = v.strip()
323
+
324
+ # equipment
325
+ elif k.startswith("equip.") or k.startswith("equipment."):
326
+ raw_equip_key = k.split(".", 1)[1]
327
+ _assign_equip(out, old_vals, raw_equip_key, v)
328
+
329
+ # tag slots (accept both dotted and *_tagN style)
330
+ elif k.startswith("aesthetic.") or k.startswith("aesthetic_tag"):
331
+ num = None
332
+ if k.startswith("aesthetic."):
333
+ suf = k.split(".", 1)[1]
334
+ if suf.isdigit():
335
+ num = int(suf)
336
+ else:
337
+ m = re.search(r"aesthetic_tag(\d+)", k)
338
+ if m:
339
+ num = int(m.group(1))
340
+ if num and 1 <= num <= 5:
341
+ out[f"aesthetic_tag{num}"] = v.strip()
342
+
343
+ elif k.startswith("skin.") or k.startswith("skin_tag"):
344
+ num = None
345
+ if k.startswith("skin."):
346
+ suf = k.split(".", 1)[1]
347
+ if suf.isdigit():
348
+ num = int(suf)
349
+ else:
350
+ m = re.search(r"skin_tag(\d+)", k)
351
+ if m:
352
+ num = int(m.group(1))
353
+ if num and 1 <= num <= 5:
354
+ out[f"skin_tag{num}"] = v.strip()
355
+
356
+ elif k.startswith("expression.") or k.startswith("expression_tag"):
357
+ num = None
358
+ if k.startswith("expression."):
359
+ suf = k.split(".", 1)[1]
360
+ if suf.isdigit():
361
+ num = int(suf)
362
+ else:
363
+ m = re.search(r"expression_tag(\d+)", k)
364
+ if m:
365
+ num = int(m.group(1))
366
+ if num and 1 <= num <= 5:
367
+ out[f"expression_tag{num}"] = v.strip()
368
+
369
+ # headwear2 aliases
370
+ elif k in ("headwear2", "headwear_tag2", "headwear_str_2", "equip_headwear2"):
371
+ out["headwear_str_2"] = v.strip()
372
+
373
+ # finalize gender
374
+ if g_int is None:
375
+ g_int = 2 # default to girl (matches your old rule: if not "1" then girl)
376
+ out["gender_int"] = int(g_int)
377
+ out["gender_str"] = "boy" if g_int == 1 else "girl"
378
+
379
+ # old_bam = unique equip values in order
380
+ seen = set()
381
+ uniq: List[str] = []
382
+ for v in old_vals:
383
+ if v and v not in seen:
384
+ uniq.append(v)
385
+ seen.add(v)
386
+ out["old_bam"] = ", ".join(uniq)
387
+
388
+ return out
389
+
390
+
391
+ # =============================================================================
392
+ # ComfyUI Node
393
+ # =============================================================================
394
+ class GPTBAMParser:
395
+ """
396
+ Parses GPT_BAM v1 (key=value fields separated by ###).
397
+ """
398
+
399
+ @classmethod
400
+ def INPUT_TYPES(cls):
401
+ return {
402
+ "required": {
403
+ "gpt_bam_string": ("STRING", {"multiline": True, "default": ""}),
404
+ }
405
+ }
406
+
407
+ RETURN_TYPES = RETURN_TYPES_TUPLE
408
+ RETURN_NAMES = RETURN_NAMES_TUPLE
409
+ FUNCTION = "parse"
410
+ CATEGORY = "BAM"
411
+
412
+ def parse(self, gpt_bam_string: str):
413
+ parsed = _parse_gpt_bam(gpt_bam_string)
414
+
415
+ # ensure all outputs exist
416
+ for name, t in _OUTPUT_DEFS:
417
+ if name not in parsed:
418
+ parsed[name] = 0 if t == "INT" else ""
419
+
420
+ return tuple(parsed[name] for name in RETURN_NAMES_TUPLE)
421
+
422
+
423
+ NODE_CLASS_MAPPINGS = {
424
+ "GPTBAMParser": GPTBAMParser,
425
+ }
426
+
427
+ NODE_DISPLAY_NAME_MAPPINGS = {
428
+ "GPTBAMParser": "GPT_BAM Parser (v1)",
429
+ }