Repository: AUTOMATIC1111/stable-diffusion-webui

Issue:
[Bug]: alternating words with an empty option throws an exception
### Is there an existing issue for this?

- [X] I have searched the existing issues and checked the recent builds/commits

### What happened?

Using an empty option in an alternation, like "[red|] apple", throws an exception. Easy workaround is to put a space in the empty option, like "[red| ] apple", and then it doesn't error out.

### Steps to reproduce the problem

1. type "[red|] apple" as the prompt
2. Press generate


### What should have happened?

does not throw an exception

### Commit where the problem happens

d5ce044b

### What platforms do you use to access the UI ?

Windows

### What browsers do you use to access the UI ?

Mozilla Firefox

### Command Line Arguments

```Shell
xformers
```


### List of extensions

wildcards, controlnet

### Console logs

```Shell
Error completing request
Arguments: ('task(wz2r40k8hiugpjz)', '[|red] thing', '\n', [], 70, 0, True, False, 1, 1, 7, -1.0, -1.0, 0, 0, 0, False, 400, 700, True, 0.15, 2, '4x_foolhardy_Remacri', 20, 0, 0, 0, False, 'canny', 'control_sd15_canny(fef5e48e)', 0.1, None, False, 'Scale to Fit (Inner Fit)', False, False, False, False, False, False, '', '', '', 1, '', 0, '', 0, '', True, False, False, False) {}
Traceback (most recent call last):
  File "D:\ai\sdweb\modules\prompt_parser.py", line 76, in alternate
    yield next(args[(step - 1)%len(args)])
StopIteration

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "D:\ai\sdweb\venv\lib\site-packages\lark\visitors.py", line 116, in _call_userfunc
    return f(children)
  File "D:\ai\sdweb\modules\prompt_parser.py", line 84, in start
    return ''.join(flatten(args))
  File "D:\ai\sdweb\modules\prompt_parser.py", line 83, in flatten
    yield from flatten(gen)
  File "D:\ai\sdweb\modules\prompt_parser.py", line 83, in flatten
    yield from flatten(gen)
  File "D:\ai\sdweb\modules\prompt_parser.py", line 82, in flatten
    for gen in x:
RuntimeError: generator raised StopIteration

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:\ai\sdweb\modules\call_queue.py", line 56, in f
    res = list(func(*args, **kwargs))
  File "D:\ai\sdweb\modules\call_queue.py", line 37, in f
    res = func(*args, **kwargs)
  File "D:\ai\sdweb\modules\txt2img.py", line 52, in txt2img
    processed = process_images(p)
  File "D:\ai\sdweb\modules\processing.py", line 476, in process_images
    res = process_images_inner(p)
  File "D:\ai\sdweb\modules\processing.py", line 604, in process_images_inner
    c = get_conds_with_caching(prompt_parser.get_multicond_learned_conditioning, prompts, p.steps, cached_c)
  File "D:\ai\sdweb\modules\processing.py", line 562, in get_conds_with_caching
    cache[1] = function(shared.sd_model, required_prompts, steps)
  File "D:\ai\sdweb\modules\prompt_parser.py", line 205, in get_multicond_learned_conditioning
    learned_conditioning = get_learned_conditioning(model, prompt_flat_list, steps)
  File "D:\ai\sdweb\scripts\prompt_blending.py", line 40, in hijacked_get_learned_conditioning
    return real_get_learned_conditioning(model, switched_prompts, steps)
  File "D:\ai\sdweb\modules\prompt_parser.py", line 129, in get_learned_conditioning
    prompt_schedules = get_learned_conditioning_prompt_schedules(prompts, steps)
  File "D:\ai\sdweb\modules\prompt_parser.py", line 102, in get_learned_conditioning_prompt_schedules
    promptdict = {prompt: get_schedule(prompt) for prompt in set(prompts)}
  File "D:\ai\sdweb\modules\prompt_parser.py", line 102, in <dictcomp>
    promptdict = {prompt: get_schedule(prompt) for prompt in set(prompts)}
  File "D:\ai\sdweb\modules\prompt_parser.py", line 100, in get_schedule
    return [[t, at_step(t, tree)] for t in collect_steps(steps, tree)]
  File "D:\ai\sdweb\modules\prompt_parser.py", line 100, in <listcomp>
    return [[t, at_step(t, tree)] for t in collect_steps(steps, tree)]
  File "D:\ai\sdweb\modules\prompt_parser.py", line 90, in at_step
    return AtStep().transform(tree)
  File "D:\ai\sdweb\venv\lib\site-packages\lark\visitors.py", line 153, in transform
    return self._transform_tree(tree)
  File "D:\ai\sdweb\venv\lib\site-packages\lark\visitors.py", line 149, in _transform_tree
    return self._call_userfunc(tree, children)
  File "D:\ai\sdweb\venv\lib\site-packages\lark\visitors.py", line 120, in _call_userfunc
    raise VisitError(tree.data, tree, e)
lark.exceptions.VisitError: Error trying to process rule "start":

generator raised StopIteration
```


### Additional information

_No response_


Discussion:
can you double-check your prompt? I had the same issue, and then noticed that there was an extra divider |  after the last altnernating word. As soon as I removed it, the error stopped.
The point is that the | at the end is there intentionally, it's trying to choose the empty string intentionally. It is a bug that you can't do that.
As a trivial example of why you might want this, consider `[fe|]male`. Yes, obviously you can write `[female|male]`, but it gets at the reason for wanting it.

I looked into the code, but since the grammar uses a CFG with an LALR parser it looks pretty complicated to support; you'd have to add an empty production and then adjust rules to include that as an option, and I'm not familiar enough with Lark to make those sorts of changes myself. (It would be trivial to handle in some alternative parsing engines, but it doesn't make sense to switch parsers just for this.)

Resolving patch:
diff --git a/modules/prompt_parser.py b/modules/prompt_parser.py
--- a/modules/prompt_parser.py
+++ b/modules/prompt_parser.py
@@ -20,7 +20,7 @@
         | "(" prompt ":" prompt ")"
         | "[" prompt "]"
 scheduled: "[" [prompt ":"] prompt ":" [WHITESPACE] NUMBER [WHITESPACE] "]"
-alternate: "[" prompt ("|" prompt)+ "]"
+alternate: "[" prompt ("|" [prompt])+ "]"
 WHITESPACE: /\s+/
 plain: /([^\\\[\]():|]|\\.)+/
 %import common.SIGNED_NUMBER -> NUMBER
@@ -53,6 +53,10 @@ def get_learned_conditioning_prompt_schedules(prompts, steps):
     [[3, '((a][:b:c '], [10, '((a][:b:c d']]
     >>> g("[a|(b:1.1)]")
     [[1, 'a'], [2, '(b:1.1)'], [3, 'a'], [4, '(b:1.1)'], [5, 'a'], [6, '(b:1.1)'], [7, 'a'], [8, '(b:1.1)'], [9, 'a'], [10, '(b:1.1)']]
+    >>> g("[fe|]male")
+    [[1, 'female'], [2, 'male'], [3, 'female'], [4, 'male'], [5, 'female'], [6, 'male'], [7, 'female'], [8, 'male'], [9, 'female'], [10, 'male']]
+    >>> g("[fe|||]male")
+    [[1, 'female'], [2, 'male'], [3, 'male'], [4, 'male'], [5, 'female'], [6, 'male'], [7, 'male'], [8, 'male'], [9, 'female'], [10, 'male']]
     """
 
     def collect_steps(steps, tree):
@@ -78,7 +82,8 @@ def scheduled(self, args):
                 before, after, _, when, _ = args
                 yield before or () if step <= when else after
             def alternate(self, args):
-                yield next(args[(step - 1)%len(args)])
+                args = ["" if not arg else arg for arg in args]
+                yield args[(step - 1) % len(args)]
             def start(self, args):
                 def flatten(x):
                     if type(x) == str:


Test patch: