mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-06-22 15:59:45 +08:00
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.
115 lines
3.7 KiB
Python
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
|
|
|