ComfyUI/comfy/parse_choice.py
Barry Downes d91544b83c Reimplement dynamic prompts in the CLIPTextEncode node.
To achieve repeatability, a random seed input is added.

By this method, the original prompt text remains intact when
serializing and deserializing from a PNG or history.

But the random seed ensures the same choices will be made
when re-translating the "choices" in the original prompt
text, thus ensuring repeatability.

Also, choices can now be nested.
2023-08-01 03:15:44 +10:00

115 lines
3.7 KiB
Python

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