import random import comfy.parse from comfy.parse import ParseError class LogicError(Exception): # something that shouldn't be possible occurred in the code # not the user's fault pass def translate_choices(text, seed=0): ''' Parses the text, translating "{A|B|C}" choices into a single choice. An option is chosen randomly from the available options. For example: "a {green|red|blue} ball on a {wooden|metal} bench" might expand to "a red ball on a wooden bench". Nesting choices is supported, so "a woman wearing a {{lavish|garish|expensive|stylish|} {red|brown|blue|} dress|{sexy|realistic|} {police|nurse|maid} uniform|{black leather|wooly|thick} coat}" could expand to "a woman wearing a stylish brown dress". All random choices are governed by the supplied random seed value, ensuring repeatability. You can use a single PrimitiveNode with an INT value, and connect that to the CLIPTextEncode node and the KSampler node in a typical Stable Diffusion 1.5 workflow, for example. Notes: * this function must correctly support valid inputs * for invalid inputs: * raise an error if that's supported cleanly enough in the system * otherwise, return the original input as the output, and issue a warning to stdout or stderr if that's acceptable in the system * must preserve escaped metacharacters for the prompt weight parsing ''' # the user will be escaping for both this processing (using curly braces) and the weight processing (using round parentheses) # from their perspective, they will need to escape literal data like this to cover both sets of processing: # { -> \{ # } -> \} # | -> \| # \ -> \\ # ( -> \( # ) -> \) def parse_choice(input): options = [] while True: options.append(parse_text_with_choices(input)) if 0: pass elif m := input.match(r'\|'): # loop around for another choice pass elif m := input.match(r'\}'): break else: raise ParseError(input, f"Expected '|' or '}}' after choice text") # choose one of the options text = rng.choice(options) return text def parse_text_with_choices(input): out = [] while True: if 0: pass elif m := input.match(r'[\\\{]'): # a single metacharacter, \ or { ch = m.group(0) if 0: pass elif ch == '\\': if not (m := input.match(r'.')): raise ParseError(input, f'Unexpected end of input after backslash') ch = m.group(0) if 0: pass elif ch in {'\\', '(', ')'}: # these are metacharacters in the weight parsing phase, so we have to handle them specially # maintain the escaping for the upcoming weight parsing phase out.append(f'\\{ch}') elif ch in {'{', '}', '|'}: # escaping a metacharacter to make it literal out.append(ch) # output literal character else: # other characters shouldn't require escaping # treat it as a normal escape regardless # policy subject to change out.append(ch) elif ch == '{': # choice chosen_text = parse_choice(input) out.append(chosen_text) else: raise LogicError(input, f"Expected metacharacter '\\' or '{{' ") elif m := input.match(r'[^\\\{\}\|]+'): # 1 or more non-metacharacters out.append(m.group(0)) else: # didn't match \, { or non-metacharacters # must be either |, } or end of input break return ''.join(out) # init our local random number generator rng = random.Random(seed) try: input = comfy.parse.Cursor(text) out = parse_text_with_choices(input) if not input.match(r'$'): raise ParseError(input, f'Failed to parse up to the end of the prompt text') return out except (ParseError, LogicError) as e: # alternative: re-throw the error stdout.write(f'Error parsing prompt: {e}'); return text