dikdimon commited on
Commit
ca9784f
·
verified ·
1 Parent(s): 9ce36f0

Upload z-sd-webui-neutral-prompt-workYEAH8 using SD-Hub

Browse files
Files changed (25) hide show
  1. z-sd-webui-neutral-prompt-workYEAH8/.gitignore +1 -0
  2. z-sd-webui-neutral-prompt-workYEAH8/LICENSE +21 -0
  3. z-sd-webui-neutral-prompt-workYEAH8/README.md +113 -0
  4. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/cfg_denoiser_hijack.cpython-310.pyc +0 -0
  5. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/global_state.cpython-310.pyc +0 -0
  6. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/hijacker.cpython-310.pyc +0 -0
  7. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/neutral_prompt_parser.cpython-310.pyc +0 -0
  8. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/prompt_parser_hijack.cpython-310.pyc +0 -0
  9. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/ui.cpython-310.pyc +0 -0
  10. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/xyz_grid.cpython-310.pyc +0 -0
  11. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/cfg_denoiser_hijack.py +388 -0
  12. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/external_code/__init__.py +23 -0
  13. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/external_code/api.py +5 -0
  14. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/external_code/ui (16).py +255 -0
  15. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/global_state.py +16 -0
  16. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/hijacker.py +34 -0
  17. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/neutral_prompt_parser.py +162 -0
  18. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/prompt_parser_hijack.py +88 -0
  19. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/ui.py +255 -0
  20. z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/xyz_grid.py +42 -0
  21. z-sd-webui-neutral-prompt-workYEAH8/scripts/__pycache__/neutral_prompt.cpython-310.pyc +0 -0
  22. z-sd-webui-neutral-prompt-workYEAH8/scripts/neutral_prompt.py +99 -0
  23. z-sd-webui-neutral-prompt-workYEAH8/test/perp_parser/__init__.py +0 -0
  24. z-sd-webui-neutral-prompt-workYEAH8/test/perp_parser/basic_test.py +122 -0
  25. z-sd-webui-neutral-prompt-workYEAH8/test/perp_parser/malicious_test.py +182 -0
z-sd-webui-neutral-prompt-workYEAH8/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ __pycache__/
z-sd-webui-neutral-prompt-workYEAH8/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 ljleb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
z-sd-webui-neutral-prompt-workYEAH8/README.md ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Neutral Prompt
2
+
3
+ Neutral prompt is an a1111 webui extension that adds alternative composable diffusion keywords to the prompt language. It enhances the original implementation using more recent research.
4
+
5
+ ## Features
6
+
7
+ - [Perp-Neg](https://perp-neg.github.io/) orthogonal prompts, invoked using the `AND_PERP` keyword
8
+ - saliency-aware noise blending, invoked using the `AND_SALT` keyword (credits to [Magic Fusion](https://magicfusion.github.io/) for the algorithm used to determine SNB maps from epsilons)
9
+ - semantic guidance top-k filtering, invoked using the `AND_TOPK` keyword (reference: https://arxiv.org/abs/2301.12247)
10
+ - standard deviation based CFG rescaling (Reference: https://arxiv.org/abs/2305.08891, section 3.4)
11
+
12
+ ## Usage
13
+
14
+ *Disclaimer: some sections of the readme have been generated by GPT-4. If anything is unclear, feel free to ask for clarifications in the [discussions](https://github.com/ljleb/sd-webui-neutral-prompt/discussions).*
15
+
16
+ ### Keyword `AND_PERP`
17
+
18
+ The `AND_PERP` keyword, standing for "PERPendicular `AND`", integrates the orthogonalization process described in the Perp-Neg paper. Essentially, `AND_PERP` allows for prompting concepts that highly overlap with regular prompts, by negating contradicting concepts.
19
+
20
+ You could visualize it as such: if `AND` prompts are "greedy" (taking as much space as possible in the output), `AND_PERP` prompts are opposite, relinquishing control as soon as there is a disagreement in the generated output.
21
+
22
+ ### Keyword `AND_SALT`
23
+
24
+ Saliency-aware blending is made possible using the `AND_SALT` keyword, shorthand for "SALienT `AND`". In essence, `AND_SALT` keeps the highest activation pixels at each denoising step.
25
+
26
+ Think of it as a territorial dispute: the image generated by the `AND` prompts is one country, and the images generated by `AND_SALT` prompts represent neighbouring nations. They're all vying for the same land - whoever strikes the strongest at a given time (denoising step) and location (latent pixel) claims it.
27
+
28
+ ### Keyword `AND_TOPK`
29
+
30
+ The `AND_TOPK` keyword refers to "TOP-K filtering". It keeps only the "k" highest activation latent pixels in the noise map and discards the rest. It works similarly to `AND_SALT`, except that the high-activation regions are simply added instead of replacing previous content.
31
+
32
+ Currently, k is constantly 5% of all latent pixels, meaning 95% of the weakest latent pixel values at each step are discarded.
33
+
34
+ Top-k filtering is useful when you want to have a more targeted effect on the generated image. It should work best with smaller objects and details.
35
+
36
+ ## Examples
37
+
38
+ ### Using the `AND_PERP` Keyword
39
+
40
+ Here is an example to illustrate one use case of the `AND_PREP` keyword. Prompt:
41
+
42
+ `beautiful castle landscape AND monster house castle :-1`
43
+
44
+ This is an XY grid with prompt S/R `AND, AND_PERP`:
45
+
46
+ ![image](https://github.com/ljleb/sd-webui-neutral-prompt/assets/32277961/29f3cf34-2ed4-45d2-b73a-b6fadec21d61)
47
+
48
+ Key observations:
49
+
50
+ - The `AND_PERP` images exhibit a higher dynamic range compared to the `AND` images.
51
+ - Since the prompts have a lot of overlap, the `AND` images sometimes struggle to depict a castle. This isn't a problem for the `AND_PERP` images.
52
+ - The `AND` images tend to lean towards a purple color, because this was the path of least resistance between the two opposing prompts during generation. In contrast, the `AND_PERP` images, free from this tug-of-war, present a clearer representation.
53
+
54
+ ### Using the `AND_SALT` Keyword
55
+
56
+ The `AND_SALT` keyword can be used to invoke saliency-aware blending. It spotlights and accentuates areas of high-activation in the output.
57
+
58
+ Consider this example prompt utilizing `AND_SALT`:
59
+
60
+ ```
61
+ a vibrant rainforest with lush green foliage
62
+ AND_SALT the glimmering rays of a golden sunset piercing through the trees
63
+ ```
64
+
65
+ In this case, the extension identifies and isolates the most salient regions in the sunset prompt. Then, the extension applies this marsked image to the rainforest prompt. Only the portions of the rainforest prompt that coincide with the salient areas of the sunset prompt are affected. These areas are replaced by pixels from the sunset prompt.
66
+
67
+ This is an XY grid with prompt S/R `AND_SALT, AND, AND_PERP`:
68
+
69
+ ![xyz_grid-0008-1564977627-a vibrant rainforest with lush green foliage_AND_SALT the glimmering rays of a golden sunset piercing through the trees](https://github.com/ljleb/sd-webui-neutral-prompt/assets/32277961/2404f20b-47f6-457f-b4c5-76b9fd919345)
70
+
71
+ Key observations:
72
+
73
+ - `AND_SALT` behaves more diplomatically, enhancing areas where its impact makes the most sense and aligning with high activity regions in the output
74
+ - `AND` gives equal weight to both prompts, creating a blended result
75
+ - `AND_PERP` will find its way through anything not blocked by the regular prompt
76
+
77
+ ## Advanced Features
78
+
79
+ ### Nesting prompts
80
+
81
+ The extension supports nesting of all prompt keywords including `AND`, allowing greater flexibility and control over the final output. Here's an example of how these keywords can be combined:
82
+
83
+ ```
84
+ magical tree forests, eternal city
85
+ AND_PERP [
86
+ electrical pole voyage
87
+ AND_SALT small nocturne companion
88
+ ]
89
+ AND_SALT [
90
+ electrical tornado
91
+ AND_SALT electric arcs, bzzz, sparks
92
+ ]
93
+ ```
94
+
95
+ To generate the final image from the diffusion model:
96
+
97
+ 1. The extension first processes the root `AND` prompts. In this case, it's just `magical tree forests, eternal city`
98
+ 2. It then processes the `AND_SALT` prompt `small nocturne companion` in the context of `electrical pole voyage`. This enhances salient features in the `electrical pole voyage` image
99
+ 3. This new image is orthogonalized with the image from `magical tree forests, eternal city`, blending the details of the 'electrical pole voyage' into the main scene without creating conflicts
100
+ 4. The extension then turns to the second `AND_SALT` group. It processes `electric arcs, bzzz, sparks` in the context of `electrical tornado`, amplifying salient features in the electrical tornado image
101
+ 5. The image from this `AND_SALT` group is then combined with the `magical tree forests, eternal city` image. The final output retains the strongest features from both the `electrical tornado` (enhanced by 'electric arcs, bzzz, sparks') and the earlier 'magical tree forests, eternal city' scene influenced by the 'electrical pole voyage'
102
+
103
+ Each keyword can define a distinct denoising space within its square brackets `[...]`. Prompts inside it merge into a single image before further processing down the prompt tree.
104
+
105
+ While there's no strict limit on the depth of nesting, experimental evidence suggests that going beyond a depth of 2 is generally unnecessary. We're still exploring the added precision from deeper nesting. If you discover innovative ways of controlling the generations using nested prompts, please share in the discussions!
106
+
107
+ ![image](https://github.com/ljleb/sd-webui-neutral-prompt/assets/32277961/f16587fe-2244-4832-a253-98f819a9e2e0)
108
+
109
+ ## Special Mentions
110
+
111
+ Special thanks to these people for helping make this extension possible:
112
+
113
+ - [Ai-Casanova](https://github.com/AI-Casanova) : for sharing mathematical knowledge, time, and conducting proof-testing to enhance the robustness of this extension
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/cfg_denoiser_hijack.cpython-310.pyc ADDED
Binary file (11 kB). View file
 
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/global_state.cpython-310.pyc ADDED
Binary file (701 Bytes). View file
 
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/hijacker.cpython-310.pyc ADDED
Binary file (1.81 kB). View file
 
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/neutral_prompt_parser.cpython-310.pyc ADDED
Binary file (5.86 kB). View file
 
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/prompt_parser_hijack.cpython-310.pyc ADDED
Binary file (3.26 kB). View file
 
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/ui.cpython-310.pyc ADDED
Binary file (5.33 kB). View file
 
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/__pycache__/xyz_grid.cpython-310.pyc ADDED
Binary file (1.65 kB). View file
 
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/cfg_denoiser_hijack.py ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # lib_neutral_prompt/cfg_denoiser_hijack.py
2
+
3
+ from lib_neutral_prompt import hijacker, global_state, neutral_prompt_parser
4
+ from lib_neutral_prompt.prompt_parser_hijack import WebuiPromptVisitor
5
+ from modules import script_callbacks, sd_samplers, shared, prompt_parser
6
+ from typing import Tuple, List
7
+ import dataclasses
8
+ import functools
9
+ import torch
10
+
11
+
12
+ # -----------------------------
13
+ # Quiet warnings (no-op)
14
+ # -----------------------------
15
+
16
+ def warn_unsupported_sampler():
17
+ # Тихо пропускаем (без вывода в консоль)
18
+ return
19
+
20
+
21
+ def warn_projection_not_found():
22
+ # Тихо пропускаем (без вывода в консоль)
23
+ return
24
+
25
+
26
+ # -----------------------------
27
+ # Math helpers used by visitors
28
+ # -----------------------------
29
+
30
+ def get_salience(vector: torch.Tensor) -> torch.Tensor:
31
+ # softmax по абсолютным значениям; возвращаем «карту заметности» формы vector
32
+ return torch.softmax(torch.abs(vector).flatten(), dim=0).reshape_as(vector)
33
+
34
+
35
+ def filter_abs_top_k(vector: torch.Tensor, k_ratio: float) -> torch.Tensor:
36
+ # пропускаем только самые большие по модулю компоненты (top-(1 - k_ratio))
37
+ k = int(torch.numel(vector) * (1 - k_ratio))
38
+ top_k, _ = torch.kthvalue(torch.abs(torch.flatten(vector)), max(k, 1))
39
+ return vector * (torch.abs(vector) >= top_k).to(vector.dtype)
40
+
41
+
42
+ def get_perpendicular_component(normal: torch.Tensor, vector: torch.Tensor, eps: float = 1e-8) -> torch.Tensor:
43
+ # проецируем vector на подпространство, перпендикулярное normal
44
+ denom = torch.clamp(torch.sum(normal * normal), min=eps)
45
+ if denom <= eps:
46
+ warn_projection_not_found()
47
+ return vector
48
+ return vector - normal * torch.sum(normal * vector) / denom
49
+
50
+
51
+ def salient_blend(normal: torch.Tensor, vectors: List[Tuple[torch.Tensor, float]]) -> torch.Tensor:
52
+ # пиксельно выбираем «самый заметный» вектор; складываем дельты относительно normal
53
+ if not vectors:
54
+ return torch.zeros_like(normal)
55
+ salience_maps = [get_salience(normal)] + [get_salience(v) for v, _ in vectors]
56
+ mask = torch.argmax(torch.stack(salience_maps, dim=0), dim=0)
57
+ result = torch.zeros_like(normal)
58
+ for mask_i, (vector, weight) in enumerate(vectors, start=1):
59
+ vector_mask = (mask == mask_i).float()
60
+ result += weight * vector_mask * (vector - normal)
61
+ return result
62
+
63
+
64
+ # -----------------------------
65
+ # Glue with webui
66
+ # -----------------------------
67
+
68
+ sd_samplers_hijacker = hijacker.ModuleHijacker.install_or_get(
69
+ module=sd_samplers,
70
+ hijacker_attribute="__neutral_prompt_hijacker",
71
+ on_uninstall=script_callbacks.on_script_unloaded,
72
+ )
73
+
74
+
75
+ @sd_samplers_hijacker.hijack("create_sampler")
76
+ def create_sampler_hijack(name: str, model, original_function):
77
+ sampler = original_function(name, model)
78
+ # только те, у кого есть model_wrap_cfg.combine_denoised
79
+ if not hasattr(sampler, "model_wrap_cfg") or not hasattr(sampler.model_wrap_cfg, "combine_denoised"):
80
+ if getattr(global_state, "is_enabled", False):
81
+ warn_unsupported_sampler()
82
+ return sampler
83
+
84
+ sampler.model_wrap_cfg.combine_denoised = functools.partial(
85
+ combine_denoised_hijack, original_function=sampler.model_wrap_cfg.combine_denoised
86
+ )
87
+ return sampler
88
+
89
+
90
+ # -----------------------------
91
+ # Scheduling helpers
92
+ # -----------------------------
93
+
94
+ def convert_to_prompt_expr_from_multicond(c, prompts):
95
+ # fallback: когда global_state.prompt_exprs ещё пуст, а webui уже дал индексы/веса
96
+ exprs = []
97
+ for batch_conds in c:
98
+ for idx, weight in batch_conds:
99
+ prompt_text = prompts[idx] if prompts and idx < len(prompts) else f"prompt_{idx}"
100
+ exprs.append(
101
+ neutral_prompt_parser.LeafPrompt(
102
+ weight=float(weight) if isinstance(weight, (int, float)) else 1.0,
103
+ conciliation=None,
104
+ prompt=prompt_text,
105
+ )
106
+ )
107
+ return exprs
108
+
109
+
110
+ def get_active_prompt(schedule, current_step):
111
+ # выбираем активный текст по текущему шагу
112
+ for end_step, text in schedule:
113
+ if current_step <= end_step:
114
+ return text
115
+ return schedule[-1][1] if schedule else "empty_prompt"
116
+
117
+
118
+ # -----------------------------
119
+ # Main entry
120
+ # -----------------------------
121
+
122
+ def combine_denoised_hijack(
123
+ x_out: torch.Tensor,
124
+ batch_cond_indices: List[List[Tuple[int, float]]],
125
+ text_uncond: torch.Tensor,
126
+ cond_scale: float,
127
+ original_function,
128
+ ) -> torch.Tensor:
129
+ """
130
+ Патчим combine_denoised так, чтобы он поддерживал Neutral Prompt-композиции.
131
+ """
132
+ if not getattr(global_state, "is_enabled", False):
133
+ return original_function(x_out, batch_cond_indices, text_uncond, cond_scale)
134
+
135
+ # если кто-то вызвал раньше, а exprs ещё нет — построим из webui conds
136
+ if not getattr(global_state, "prompt_exprs", []) and batch_cond_indices:
137
+ global_state.prompt_exprs = convert_to_prompt_expr_from_multicond(batch_cond_indices, [])
138
+
139
+ # поддержка расписаний webui (prompt travel / динамика по шагам)
140
+ prompt_schedules = prompt_parser.get_learned_conditioning_prompt_schedules(
141
+ getattr(global_state, "raw_prompts", [expr.accept(WebuiPromptVisitor()) for expr in global_state.prompt_exprs]),
142
+ shared.state.sampling_steps,
143
+ )
144
+ active_prompts = [get_active_prompt(s, shared.state.sampling_step) for s in prompt_schedules]
145
+ global_state.prompt_exprs = [neutral_prompt_parser.parse_root(p) for p in active_prompts]
146
+
147
+ # считаем «чистый» denoised от webui на переупакованном батче
148
+ denoised = get_webui_denoised(x_out, batch_cond_indices, text_uncond, cond_scale, original_function)
149
+
150
+ # затем добавим наши корректировки (aux_cond_delta) и CFG-rescale
151
+ uncond = x_out[-text_uncond.shape[0] :]
152
+
153
+ for batch_i, (prompt, cond_indices) in enumerate(zip(global_state.prompt_exprs, batch_cond_indices)):
154
+ args = CombineDenoiseArgs(x_out=x_out, uncond=uncond[batch_i], cond_indices=cond_indices)
155
+ cond_delta = prompt.accept(CondDeltaVisitor(), args, 0)
156
+ aux_cond_delta = prompt.accept(AuxCondDeltaVisitor(), args, cond_delta, 0)
157
+
158
+ # добавляем только aux часть в cfg_cond (основанный на webui результате)
159
+ cfg_cond = denoised[batch_i] + aux_cond_delta * cond_scale
160
+ # а rescale делаем относительно полной «цели»: uncond + cond_delta + aux_cond_delta
161
+ denoised[batch_i] = cfg_rescale(cfg_cond, uncond[batch_i] + cond_delta + aux_cond_delta)
162
+
163
+ return denoised
164
+
165
+
166
+ def get_webui_denoised(
167
+ x_out: torch.Tensor,
168
+ batch_cond_indices: List[List[Tuple[int, float]]],
169
+ text_uncond: torch.Tensor,
170
+ cond_scale: float,
171
+ original_function,
172
+ ) -> torch.Tensor:
173
+ """
174
+ Переупаковываем батч под оригинальный combine_denoised:
175
+ - строим список «условных срезов» (с учётом сложных композиций)
176
+ - отдаём webui оригинальные индексы/веса, но уже на наш переупакованный батч
177
+ - webui вернёт по одному denoised на картинку (а не на срез!)
178
+ """
179
+ uncond = x_out[-text_uncond.shape[0] :]
180
+ sliced_batch_x_out: List[torch.Tensor] = []
181
+ sliced_batch_cond_indices: List[List[Tuple[int, float]]] = []
182
+
183
+ for batch_i, (prompt, cond_indices) in enumerate(zip(global_state.prompt_exprs, batch_cond_indices)):
184
+ args = CombineDenoiseArgs(x_out=x_out, uncond=uncond[batch_i], cond_indices=cond_indices)
185
+
186
+ # sanity-check: число базовых листьев должно совпадать с количеством webui cond_indices
187
+ base_size = prompt.accept(BaseSizeVisitor())
188
+ # предупреждения отключены по требованию — просто не шумим
189
+
190
+ sliced_x_out, sliced_cond_indices = gather_webui_conds(prompt, args, 0, len(sliced_batch_x_out))
191
+ if sliced_cond_indices:
192
+ sliced_batch_cond_indices.append(sliced_cond_indices)
193
+ sliced_batch_x_out.extend(sliced_x_out)
194
+
195
+ # добавляем uncond-часть всего батча (webui ожидает её в хвосте)
196
+ sliced_batch_x_out += list(uncond)
197
+ sliced_batch_x_out = torch.stack(sliced_batch_x_out, dim=0)
198
+
199
+ # вызвать оригинальный combine_denoised на нашем переупакованном батче
200
+ return original_function(sliced_batch_x_out, sliced_batch_cond_indices, text_uncond, cond_scale)
201
+
202
+
203
+ def cfg_rescale(cfg_cond: torch.Tensor, cond: torch.Tensor) -> torch.Tensor:
204
+ """
205
+ CFG Rescale (как в SDXL/SD WebUI) — нормируем дисперсию cond к cond, но сохраняем среднее в смеси.
206
+ """
207
+ if getattr(global_state, "cfg_rescale", 0) == 0:
208
+ return cfg_cond
209
+
210
+ global_state.apply_and_clear_cfg_rescale_override()
211
+ cfg_cond_mean = cfg_cond.mean()
212
+ cfg_rescale_mean = (1 - global_state.cfg_rescale) * cfg_cond_mean + global_state.cfg_rescale * cond.mean()
213
+ cfg_rescale_factor = global_state.cfg_rescale * (cond.std() / (cfg_cond.std() + 1e-8) - 1) + 1
214
+ return cfg_rescale_mean + (cfg_cond - cfg_cond_mean) * cfg_rescale_factor
215
+
216
+
217
+ # -----------------------------
218
+ # Data passed around visitors
219
+ # -----------------------------
220
+
221
+ @dataclasses.dataclass
222
+ class CombineDenoiseArgs:
223
+ x_out: torch.Tensor
224
+ uncond: torch.Tensor
225
+ cond_indices: List[Tuple[int, float]] # (index_in_x_out, weight_from_webui)
226
+
227
+
228
+ # -----------------------------
229
+ # Size visitor for REAL webui indices
230
+ # -----------------------------
231
+
232
+ class BaseSizeVisitor:
233
+ """
234
+ Считает ТОЛЬКО базовые листья (conciliation is None), т.е. столько,
235
+ сколько webui реально выдаёт cond_indices.
236
+ """
237
+ def visit_leaf_prompt(self, that: neutral_prompt_parser.LeafPrompt) -> int:
238
+ return 1 if getattr(that, "conciliation", None) is None else 0
239
+
240
+ def visit_composite_prompt(self, that: neutral_prompt_parser.CompositePrompt) -> int:
241
+ if not getattr(that, "children", None):
242
+ return 0
243
+ return sum(child.accept(self) for child in that.children)
244
+
245
+
246
+ # -----------------------------
247
+ # Visitors producing deltas
248
+ # -----------------------------
249
+
250
+ class CondDeltaVisitor:
251
+ def visit_leaf_prompt(
252
+ self,
253
+ that: neutral_prompt_parser.LeafPrompt,
254
+ args: CombineDenoiseArgs,
255
+ index: int,
256
+ ) -> torch.Tensor:
257
+ # индекс тратится ТОЛЬКО на базовых листьях; подстрахуемся от выхода за границу
258
+ if index >= len(args.cond_indices):
259
+ return torch.zeros_like(args.x_out[0])
260
+
261
+ cond_info = args.cond_indices[index]
262
+ # предупреждения про несоответствие весов убраны по требованию
263
+
264
+ # разница между cond и uncond (базовая CFG-дельта)
265
+ return args.x_out[cond_info[0]] - args.uncond
266
+
267
+ def visit_composite_prompt(
268
+ self,
269
+ that: neutral_prompt_parser.CompositePrompt,
270
+ args: CombineDenoiseArgs,
271
+ index: int,
272
+ ) -> torch.Tensor:
273
+ cond_delta = torch.zeros_like(args.x_out[0])
274
+
275
+ for child in that.children:
276
+ child_cond_delta = child.accept(self, args, index)
277
+ # базовая логика — взвешенная сумма; SALIENCE применим на Aux-этапе
278
+ cond_delta += child.weight * child_cond_delta
279
+
280
+ # шаг по количеству БАЗОВЫХ листьев внутри ребёнка
281
+ index += child.accept(BaseSizeVisitor())
282
+
283
+ return cond_delta
284
+
285
+
286
+ class AuxCondDeltaVisitor:
287
+ def visit_leaf_prompt(
288
+ self,
289
+ that: neutral_prompt_parser.LeafPrompt,
290
+ args: CombineDenoiseArgs,
291
+ cond_delta: torch.Tensor,
292
+ index: int,
293
+ ) -> torch.Tensor:
294
+ # для одиночного листа нет доп. коррекции
295
+ return torch.zeros_like(args.x_out[0])
296
+
297
+ def visit_composite_prompt(
298
+ self,
299
+ that: neutral_prompt_parser.CompositePrompt,
300
+ args: CombineDenoiseArgs,
301
+ cond_delta: torch.Tensor,
302
+ index: int,
303
+ ) -> torch.Tensor:
304
+ aux_cond_delta = torch.zeros_like(args.x_out[0])
305
+ salient_cond_deltas: List[Tuple[torch.Tensor, float]] = []
306
+
307
+ for child in that.children:
308
+ if getattr(child, "conciliation", None) is not None:
309
+ # рекурсивно собираем child deltas
310
+ child_cond_delta = child.accept(CondDeltaVisitor(), args, index)
311
+ child_cond_delta += child.accept(self, args, child_cond_delta, index)
312
+
313
+ if child.conciliation == neutral_prompt_parser.ConciliationStrategy.PERPENDICULAR:
314
+ aux_cond_delta += child.weight * get_perpendicular_component(cond_delta, child_cond_delta)
315
+ elif child.conciliation == neutral_prompt_parser.ConciliationStrategy.SALIENCE_MASK:
316
+ salient_cond_deltas.append((child_cond_delta, child.weight))
317
+ elif child.conciliation == neutral_prompt_parser.ConciliationStrategy.SEMANTIC_GUIDANCE:
318
+ aux_cond_delta += child.weight * filter_abs_top_k(child_cond_delta, 0.05)
319
+
320
+ # шаг только по БАЗОВЫМ листьям
321
+ index += child.accept(BaseSizeVisitor())
322
+
323
+ # финальное «солёное» смешивание для узла, где встречались SALIENCE-дети
324
+ aux_cond_delta += salient_blend(cond_delta, salient_cond_deltas)
325
+ return aux_cond_delta
326
+
327
+
328
+ # -----------------------------
329
+ # Slice builder for webui call
330
+ # -----------------------------
331
+
332
+ def gather_webui_conds(
333
+ prompt: neutral_prompt_parser.PromptExpr,
334
+ args: CombineDenoiseArgs,
335
+ index_in: int,
336
+ index_out: int,
337
+ ) -> Tuple[List[torch.Tensor], List[Tuple[int, float]]]:
338
+ """
339
+ Формируем список «условных срезов» (x_out) и соответствующие пары (index, weight),
340
+ которые поймёт оригинальный webui combine_denoised.
341
+ - Для базовых листьев берём реальные webui cond_indices (и тратим индекс)
342
+ - Для «вторичных» листьев и композитов синтезируем канал: uncond + (cond_delta + aux_delta)
343
+ """
344
+ sliced_x_out: List[torch.Tensor] = []
345
+ sliced_cond_indices: List[Tuple[int, float]] = []
346
+
347
+ # Лист
348
+ if isinstance(prompt, neutral_prompt_parser.LeafPrompt):
349
+ if getattr(prompt, "conciliation", None) is None:
350
+ # базовый лист — берём реальный cond канал из x_out по webui индексу
351
+ if index_in >= len(args.cond_indices):
352
+ return sliced_x_out, sliced_cond_indices
353
+ real_idx, _w = args.cond_indices[index_in]
354
+ child_x_out = args.x_out[real_idx]
355
+ else:
356
+ # вторичный лист — синтезируем «условный канал»
357
+ child_cond = prompt.accept(CondDeltaVisitor(), args, index_in)
358
+ child_cond += prompt.accept(AuxCondDeltaVisitor(), args, child_cond, index_in)
359
+ child_x_out = args.uncond + child_cond
360
+
361
+ index_offset = index_out + len(sliced_x_out)
362
+ sliced_x_out.append(child_x_out)
363
+ sliced_cond_indices.append((index_offset, prompt.weight))
364
+ return sliced_x_out, sliced_cond_indices
365
+
366
+ # Композит
367
+ if isinstance(prompt, neutral_prompt_parser.CompositePrompt):
368
+ for child in prompt.children:
369
+ if isinstance(child, neutral_prompt_parser.LeafPrompt) and getattr(child, "conciliation", None) is None:
370
+ # базовый лист — реальный индекс webui
371
+ if index_in >= len(args.cond_indices):
372
+ break
373
+ real_idx, _w = args.cond_indices[index_in]
374
+ child_x_out = args.x_out[real_idx]
375
+ else:
376
+ # всё остальное — синтезируем канал
377
+ child_cond = child.accept(CondDeltaVisitor(), args, index_in)
378
+ child_cond += child.accept(AuxCondDeltaVisitor(), args, child_cond, index_in)
379
+ child_x_out = args.uncond + child_cond
380
+
381
+ index_offset = index_out + len(sliced_x_out)
382
+ sliced_x_out.append(child_x_out)
383
+ sliced_cond_indices.append((index_offset, child.weight))
384
+
385
+ # критично: шагать по ЧИСЛУ БАЗОВЫХ листьев внутри ребёнка
386
+ index_in += child.accept(BaseSizeVisitor())
387
+
388
+ return sliced_x_out, sliced_cond_indices
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/external_code/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import contextlib
2
+
3
+
4
+ @contextlib.contextmanager
5
+ def fix_path():
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ extension_path = str(Path(__file__).parent.parent.parent)
10
+ added = False
11
+ if extension_path not in sys.path:
12
+ sys.path.insert(0, extension_path)
13
+ added = True
14
+
15
+ yield
16
+
17
+ if added:
18
+ sys.path.remove(extension_path)
19
+
20
+
21
+ with fix_path():
22
+ del fix_path, contextlib
23
+ from .api import *
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/external_code/api.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from lib_neutral_prompt import global_state
2
+
3
+
4
+ def override_cfg_rescale(cfg_rescale: float):
5
+ global_state.cfg_rescale_override = cfg_rescale
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/external_code/ui (16).py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # lib_neutral_prompt/ui.py
2
+ from lib_neutral_prompt import global_state, neutral_prompt_parser
3
+ from modules import script_callbacks, shared
4
+ from typing import Dict, Tuple, List, Callable
5
+ import gradio as gr
6
+ import dataclasses
7
+
8
+ txt2img_prompt_textbox = None
9
+ img2img_prompt_textbox = None
10
+
11
+ # Только поддерживаемые типы
12
+ prompt_types = {
13
+ 'Perpendicular': neutral_prompt_parser.PromptKeyword.AND_PERP.value,
14
+ 'Saliency-aware': neutral_prompt_parser.PromptKeyword.AND_SALT.value,
15
+ 'Semantic guidance top-k': neutral_prompt_parser.PromptKeyword.AND_TOPK.value,
16
+ }
17
+
18
+ prompt_types_tooltip = '\n'.join([
19
+ 'AND — базовое сложение фич (встроено в WebUI, режим по умолчанию)',
20
+ 'Perpendicular (AND_PERP) — подавляет противоречащие фичи',
21
+ 'Saliency-aware (AND_SALT) — побеждают самые «заметные» фичи',
22
+ 'Semantic guidance top-k (AND_TOPK) — таргетные точечные изменения',
23
+ ])
24
+
25
+ GUIDE_MD = """
26
+ ### Neutral Prompt — Guide & Examples
27
+
28
+ **Как это работает**
29
+ - `AND` — обычная сумма фич. Пишется напрямую в промпте: `a AND (b:1.2)`
30
+ - Расширенные режимы оформляются ключевыми словами и **квадратными скобками**:
31
+ - `AND_PERP [ ... ] : w` — *перпендикулярная* компонента. Помогает «убрать конфликтующее».
32
+ - `AND_SALT [ ... ] : w` — *saliency-aware*. Смешивает в пользу «самых заметных» участков.
33
+ - `AND_TOPK [ ... ] : w` — *semantic top-k*. Оставляет только самые сильные (по модулю) изменения.
34
+
35
+ **Веса**
36
+ - Внутри скобок: `(text:0.8)` — классика WebUI.
37
+ - В плоском виде: `text:0.8` — тоже поддерживается.
38
+ - Вес группы: `AND_SALT [ a AND (b:0.7) ] : 0.5` — общий вес умножится на вклад детей.
39
+
40
+ **Быстрые примеры**
41
+ ```text
42
+ # Базовый AND
43
+ a cinematic portrait AND (dramatic lighting:1.2)
44
+
45
+ # Perpendicular: добавь деталь, не ломая основной стиль
46
+ AND_PERP [ cinematic lighting AND (sharp details:1.1) ] : 0.6
47
+
48
+ # Saliency-aware: усили неон и отблески
49
+ AND_SALT [ neon AND reflections:0.7 ] : 0.5
50
+
51
+ # Semantic top-k: точечные правки лица
52
+ AND_TOPK [ add freckles AND brighten eyes ] : 0.4
53
+ ```
54
+
55
+ **Комбо-примеры**
56
+ ```text
57
+ # Комбинация SALT + PERP
58
+ a rainy street at night
59
+ AND_SALT [ neon signs AND wet asphalt reflections:0.8 ] : 0.5
60
+ AND_PERP [ remove fog AND (crisp edges:1.1) ] : 0.4
61
+
62
+ # Комбинация TOPK + AND
63
+ a studio headshot
64
+ AND_TOPK [ enhance iris detail AND subtle skin texture ] : 0.35
65
+ AND (soft key light:1.15)
66
+ ```
67
+
68
+ **Расписания (prompt travel) — делает сам WebUI**
69
+ - Используй стандартный синтаксис WebUI, например:
70
+ ```text
71
+ a sunset over city [a sunrise over city: 18]
72
+ ```
73
+ - Наше расширение берёт **исходный текст** (raw prompt) для расписаний, поэтому просто пиши как обычно.
74
+ - Расширенные блоки тоже можно комбинировать с расписанием (WebUI решит активный текст на каждом шаге).
75
+
76
+ **Советы**
77
+ - Пиши «расширенные» блоки ближе к концу промпта и увеличивай вес постепенно (`0.3 → 0.6`).
78
+ - Если что-то «перетягивает одеяло», снизь вес блока или листьев внутри него.
79
+ - Не злоупотребляй количеством расширенных блоков: один-два аккуратных блока обычно лучше.
80
+ """
81
+
82
+ # Готовые примеры для кнопок вставки
83
+ EXAMPLE_BASE_AND = "a cinematic portrait AND (dramatic lighting:1.2)"
84
+ EXAMPLE_PERP = "AND_PERP [ cinematic lighting AND (sharp details:1.1) ] : 0.6"
85
+ EXAMPLE_SALT = "AND_SALT [ neon AND reflections:0.7 ] : 0.5"
86
+ EXAMPLE_TOPK = "AND_TOPK [ add freckles AND brighten eyes ] : 0.4"
87
+ EXAMPLE_COMBO = """a rainy street at night
88
+ AND_SALT [ neon signs AND wet asphalt reflections:0.8 ] : 0.5
89
+ AND_PERP [ remove fog AND (crisp edges:1.1) ] : 0.4"""
90
+ EXAMPLE_SCHEDULE = "a sunset over city [a sunrise over city: 18]"
91
+
92
+ @dataclasses.dataclass
93
+ class AccordionInterface:
94
+ get_elem_id: Callable
95
+
96
+ def __post_init__(self):
97
+ self.is_rendered = False
98
+
99
+ # Верхний слайдер
100
+ self.cfg_rescale = gr.Slider(
101
+ label='CFG rescale',
102
+ minimum=0, maximum=1, value=0
103
+ )
104
+
105
+ # Форматтер
106
+ self.neutral_prompt = gr.Textbox(
107
+ label='Neutral prompt',
108
+ show_label=False,
109
+ lines=3,
110
+ placeholder='Neutral prompt (нажмите Apply, чтобы добавить в позитивный промпт)'
111
+ )
112
+ self.neutral_cond_scale = gr.Slider(
113
+ label='Prompt weight',
114
+ minimum=-3, maximum=3, value=1
115
+ )
116
+ self.aux_prompt_type = gr.Dropdown(
117
+ label='Prompt type',
118
+ choices=list(prompt_types.keys()),
119
+ value=next(iter(prompt_types.keys())),
120
+ tooltip=prompt_types_tooltip,
121
+ elem_id=self.get_elem_id('formatter_prompt_type')
122
+ )
123
+ self.append_to_prompt_button = gr.Button(value='Apply to prompt')
124
+
125
+ # Гайд и кнопки вставки примеров
126
+ self.docs_md = gr.Markdown(value=GUIDE_MD)
127
+ self.btn_ins_base = gr.Button(value='Вставить пример: AND')
128
+ self.btn_ins_perp = gr.Button(value='Вставить пример: AND_PERP')
129
+ self.btn_ins_salt = gr.Button(value='Вставить пример: AND_SALT')
130
+ self.btn_ins_topk = gr.Button(value='Вставить пример: AND_TOPK')
131
+ self.btn_ins_combo = gr.Button(value='Вставить пример: Комбо')
132
+ self.btn_ins_schedule = gr.Button(value='Вставить пример: Расписание')
133
+
134
+ def arrange_components(self, is_img2img: bool):
135
+ if self.is_rendered:
136
+ return
137
+ with gr.Accordion(label='Neutral Prompt', open=False):
138
+ self.cfg_rescale.render()
139
+ with gr.Accordion(label='Prompt formatter', open=False):
140
+ self.neutral_prompt.render()
141
+ self.neutral_cond_scale.render()
142
+ self.aux_prompt_type.render()
143
+ self.append_to_prompt_button.render()
144
+ with gr.Accordion(label='Guide & Examples', open=False):
145
+ self.docs_md.render()
146
+ with gr.Row():
147
+ self.btn_ins_base.render()
148
+ self.btn_ins_perp.render()
149
+ with gr.Row():
150
+ self.btn_ins_salt.render()
151
+ self.btn_ins_topk.render()
152
+ with gr.Row():
153
+ self.btn_ins_combo.render()
154
+ self.btn_ins_schedule.render()
155
+
156
+ def connect_events(self, is_img2img: bool):
157
+ if self.is_rendered:
158
+ return
159
+ prompt_textbox = img2img_prompt_textbox if is_img2img else txt2img_prompt_textbox
160
+
161
+ def _append(init_prompt: str, prompt: str, scale: float, prompt_type: str):
162
+ base = (init_prompt or '').rstrip()
163
+ sep = '\n' if base else ''
164
+ return f"{base}{sep}{prompt_types[prompt_type]} {prompt} :{scale}", ""
165
+
166
+ def _append_example(init_prompt: str, snippet: str):
167
+ base = (init_prompt or '').rstrip()
168
+ sep = '\n' if base else ''
169
+ return f"{base}{sep}{snippet}"
170
+
171
+ # Кнопка «Apply to prompt» (форматтер)
172
+ self.append_to_prompt_button.click(
173
+ fn=_append,
174
+ inputs=[prompt_textbox, self.neutral_prompt, self.neutral_cond_scale, self.aux_prompt_type],
175
+ outputs=[prompt_textbox, self.neutral_prompt]
176
+ )
177
+
178
+ # Кнопки вставки примеров
179
+ self.btn_ins_base.click(
180
+ fn=_append_example,
181
+ inputs=[prompt_textbox, gr.State(EXAMPLE_BASE_AND)],
182
+ outputs=prompt_textbox
183
+ )
184
+ self.btn_ins_perp.click(
185
+ fn=_append_example,
186
+ inputs=[prompt_textbox, gr.State(EXAMPLE_PERP)],
187
+ outputs=prompt_textbox
188
+ )
189
+ self.btn_ins_salt.click(
190
+ fn=_append_example,
191
+ inputs=[prompt_textbox, gr.State(EXAMPLE_SALT)],
192
+ outputs=prompt_textbox
193
+ )
194
+ self.btn_ins_topk.click(
195
+ fn=_append_example,
196
+ inputs=[prompt_textbox, gr.State(EXAMPLE_TOPK)],
197
+ outputs=prompt_textbox
198
+ )
199
+ self.btn_ins_combo.click(
200
+ fn=_append_example,
201
+ inputs=[prompt_textbox, gr.State(EXAMPLE_COMBO)],
202
+ outputs=prompt_textbox
203
+ )
204
+ self.btn_ins_schedule.click(
205
+ fn=_append_example,
206
+ inputs=[prompt_textbox, gr.State(EXAMPLE_SCHEDULE)],
207
+ outputs=prompt_textbox
208
+ )
209
+
210
+ def set_rendered(self, value: bool = True):
211
+ self.is_rendered = value
212
+
213
+ def get_components(self) -> Tuple[gr.components.Component]:
214
+ return (self.cfg_rescale,)
215
+
216
+ def get_infotext_fields(self) -> Tuple[Tuple[gr.components.Component, str]]:
217
+ return tuple(zip(self.get_components(), ('CFG Rescale',)))
218
+
219
+ def get_paste_field_names(self) -> List[str]:
220
+ return ['CFG Rescale']
221
+
222
+ def get_extra_generation_params(self, args: Dict) -> Dict:
223
+ return {'CFG Rescale': args['cfg_rescale']}
224
+
225
+ def unpack_processing_args(self, cfg_rescale: float) -> Dict:
226
+ return {'cfg_rescale': cfg_rescale}
227
+
228
+ def on_ui_settings():
229
+ section = ('neutral_prompt', 'Neutral Prompt')
230
+ shared.opts.add_option(
231
+ 'neutral_prompt_enabled',
232
+ shared.OptionInfo(True, 'Enable neutral-prompt extension', section=section)
233
+ )
234
+ global_state.is_enabled = shared.opts.data.get('neutral_prompt_enabled', True)
235
+
236
+ shared.opts.add_option(
237
+ 'neutral_prompt_verbose',
238
+ shared.OptionInfo(False, 'Enable verbose debugging for neutral-prompt', section=section)
239
+ )
240
+ shared.opts.onchange('neutral_prompt_verbose', update_verbose)
241
+
242
+ script_callbacks.on_ui_settings(on_ui_settings)
243
+
244
+ def update_verbose():
245
+ global_state.verbose = shared.opts.data.get('neutral_prompt_verbose', False)
246
+
247
+ def on_after_component(component, **_kwargs):
248
+ if getattr(component, 'elem_id', None) == 'txt2img_prompt':
249
+ global txt2img_prompt_textbox
250
+ txt2img_prompt_textbox = component
251
+ if getattr(component, 'elem_id', None) == 'img2img_prompt':
252
+ global img2img_prompt_textbox
253
+ img2img_prompt_textbox = component
254
+
255
+ script_callbacks.on_after_component(on_after_component)
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/global_state.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+ from lib_neutral_prompt import neutral_prompt_parser
3
+
4
+
5
+ is_enabled: bool = False
6
+ prompt_exprs: List[neutral_prompt_parser.PromptExpr] = []
7
+ cfg_rescale: float = 0.0
8
+ verbose: bool = False
9
+ cfg_rescale_override: Optional[float] = None
10
+
11
+
12
+ def apply_and_clear_cfg_rescale_override():
13
+ global cfg_rescale, cfg_rescale_override
14
+ if cfg_rescale_override is not None:
15
+ cfg_rescale = cfg_rescale_override
16
+ cfg_rescale_override = None
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/hijacker.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+
3
+
4
+ class ModuleHijacker:
5
+ def __init__(self, module):
6
+ self.__module = module
7
+ self.__original_functions = dict()
8
+
9
+ def hijack(self, attribute):
10
+ if attribute not in self.__original_functions:
11
+ self.__original_functions[attribute] = getattr(self.__module, attribute)
12
+
13
+ def decorator(function):
14
+ setattr(self.__module, attribute, functools.partial(function, original_function=self.__original_functions[attribute]))
15
+ return function
16
+
17
+ return decorator
18
+
19
+ def reset_module(self):
20
+ for attribute, original_function in self.__original_functions.items():
21
+ setattr(self.__module, attribute, original_function)
22
+
23
+ self.__original_functions.clear()
24
+
25
+ @staticmethod
26
+ def install_or_get(module, hijacker_attribute, on_uninstall=lambda _callback: None):
27
+ if not hasattr(module, hijacker_attribute):
28
+ module_hijacker = ModuleHijacker(module)
29
+ setattr(module, hijacker_attribute, module_hijacker)
30
+ on_uninstall(lambda: delattr(module, hijacker_attribute))
31
+ on_uninstall(module_hijacker.reset_module)
32
+ return module_hijacker
33
+ else:
34
+ return getattr(module, hijacker_attribute)
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/neutral_prompt_parser.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # neutral_prompt_parser.py
2
+ import abc
3
+ import dataclasses
4
+ from enum import Enum
5
+ from typing import List, Tuple, Any, Optional
6
+ import re
7
+
8
+ class PromptKeyword(Enum):
9
+ AND = 'AND'
10
+ AND_PERP = 'AND_PERP'
11
+ AND_SALT = 'AND_SALT'
12
+ AND_TOPK = 'AND_TOPK'
13
+
14
+ prompt_keywords = [e.value for e in PromptKeyword]
15
+
16
+ class ConciliationStrategy(Enum):
17
+ PERPENDICULAR = PromptKeyword.AND_PERP.value
18
+ SALIENCE_MASK = PromptKeyword.AND_SALT.value
19
+ SEMANTIC_GUIDANCE = PromptKeyword.AND_TOPK.value
20
+
21
+ conciliation_strategies = [e.value for e in ConciliationStrategy]
22
+
23
+ @dataclasses.dataclass
24
+ class PromptExpr(abc.ABC):
25
+ weight: float
26
+ conciliation: Optional[ConciliationStrategy]
27
+
28
+ @abc.abstractmethod
29
+ def accept(self, visitor, *args, **kwargs) -> Any:
30
+ ...
31
+
32
+ @dataclasses.dataclass
33
+ class LeafPrompt(PromptExpr):
34
+ prompt: str
35
+ def accept(self, visitor, *args, **kwargs):
36
+ return visitor.visit_leaf_prompt(self, *args, **kwargs)
37
+
38
+ @dataclasses.dataclass
39
+ class CompositePrompt(PromptExpr):
40
+ children: List['PromptExpr']
41
+ def accept(self, visitor, *args, **kwargs):
42
+ return visitor.visit_composite_prompt(self, *args, **kwargs)
43
+
44
+ class FlatSizeVisitor:
45
+ def visit_leaf_prompt(self, that: LeafPrompt) -> int:
46
+ return 1
47
+ def visit_composite_prompt(self, that: CompositePrompt) -> int:
48
+ return sum(child.accept(self) for child in that.children) if that.children else 0
49
+
50
+ def parse_root(string: str) -> CompositePrompt:
51
+ tokens = tokenize(string)
52
+ skip_leading_ws(tokens)
53
+ prompts = parse_prompts(tokens)
54
+ return CompositePrompt(1.0, None, prompts)
55
+
56
+ def parse_prompts(tokens: List[str], *, nested: bool = False, closing_token: Optional[str] = None) -> List[PromptExpr]:
57
+ prompts: List[PromptExpr] = []
58
+ while tokens:
59
+ # если задали явный закрывающий токен (например, ']') — останавливаемся ровно на нём
60
+ if nested and closing_token and tokens[0] == closing_token:
61
+ break
62
+ # обратная совместимость: если closing_token не задан, но мы внутри — выходим на первой закрывающей
63
+ if nested and not closing_token and tokens[0] in [']', ')']:
64
+ break
65
+ prompts.append(parse_prompt(tokens, first=(len(prompts) == 0)))
66
+ skip_leading_ws(tokens)
67
+ return prompts
68
+
69
+ def skip_leading_ws(tokens: List[str]):
70
+ while tokens and tokens[0].strip() == '':
71
+ tokens.pop(0)
72
+
73
+ def parse_prompt(tokens: List[str], *, first: bool) -> PromptExpr:
74
+ skip_leading_ws(tokens)
75
+
76
+ # ключевое слово допускается и в начале
77
+ conc: Optional[ConciliationStrategy] = None
78
+ if tokens and tokens[0] in prompt_keywords:
79
+ op = tokens.pop(0)
80
+ conc = ConciliationStrategy(op) if op in conciliation_strategies else None
81
+ skip_leading_ws(tokens)
82
+
83
+ # [ ... ] : weight → CompositePrompt
84
+ if tokens and tokens[0] == '[':
85
+ tokens.pop(0)
86
+ children = parse_prompts(tokens, nested=True, closing_token=']')
87
+ if tokens and tokens[0] == ']':
88
+ tokens.pop(0) # <<< consume ']'
89
+ skip_leading_ws(tokens)
90
+ weight = 1.0
91
+ if tokens and tokens[0] == ':':
92
+ tokens.pop(0); skip_leading_ws(tokens)
93
+ if tokens and is_float(tokens[0]):
94
+ weight = float(tokens.pop(0))
95
+ return CompositePrompt(weight, conc, children)
96
+
97
+ # ( text : weight ) → LeafPrompt
98
+ if tokens and tokens[0] == '(':
99
+ tokens.pop(0)
100
+ text, weight = parse_prompt_text(tokens, nested=True)
101
+ if tokens and tokens[0] == ':':
102
+ tokens.pop(0); skip_leading_ws(tokens)
103
+ if tokens and is_float(tokens[0]):
104
+ weight = float(tokens.pop(0))
105
+ if tokens and tokens[0] == ')':
106
+ tokens.pop(0) # <<< consume ')'
107
+ return LeafPrompt(weight, conc, text.strip())
108
+
109
+ # plain text (может оканчиваться на ": float")
110
+ text, weight = parse_prompt_text(tokens, nested=False)
111
+ m = re.match(r'^(.*?)(?:\s*:\s*)([-+]?\d+(?:\.\d+)?\s*)$', text)
112
+ if m:
113
+ text = m.group(1).strip()
114
+ try:
115
+ weight = float(m.group(2))
116
+ except Exception:
117
+ weight = 1.0
118
+ return LeafPrompt(weight, conc, text.strip())
119
+
120
+ def parse_prompt_text(tokens: List[str], *, nested: bool) -> Tuple[str, float]:
121
+ # собираем текст, НЕ поедая закрывающие скобки верхнего уровня
122
+ parts: List[str] = []
123
+ weight = 1.0
124
+ depth = 0
125
+ while tokens:
126
+ t = tokens[0]
127
+ if nested and depth == 0:
128
+ # стоп на закрывающей верхнего уровня — не съедаем её (пусть родитель снимет)
129
+ if t in [')', ']']:
130
+ break
131
+ # стоп перед шаблоном ": <float> )|]" — вес заберёт родитель
132
+ if t == ':' and len(tokens) >= 3 and is_float(tokens[1]) and tokens[2].strip() in [')', ']']:
133
+ break
134
+ # стоп на ключевом слове на верхнем уровне
135
+ if t in prompt_keywords and depth == 0:
136
+ break
137
+ if t in ('(', '['):
138
+ depth += 1
139
+ elif t in (')', ']'):
140
+ if depth == 0:
141
+ break
142
+ depth -= 1
143
+ parts.append(tokens.pop(0))
144
+ text = ''.join(parts).strip()
145
+ # post: если сразу после текста идёт ": <float>" (для плоского случая)
146
+ if len(tokens) >= 2 and tokens[0] == ':' and is_float(tokens[1]):
147
+ tokens.pop(0)
148
+ weight = float(tokens.pop(0))
149
+ return text, weight
150
+
151
+ def tokenize(s: str) -> List[str]:
152
+ # режем по (), [], :, и ключевым словам; пробелы сохраняем отдельными токенами
153
+ kw_regex = '|'.join(r'\b' + re.escape(k) + r'\b' for k in prompt_keywords)
154
+ pattern = r'(\(|\)|\[|\]|:|' + kw_regex + r')'
155
+ return [x for x in re.split(pattern, s) if x != '']
156
+
157
+ def is_float(x: str) -> bool:
158
+ try:
159
+ float(x)
160
+ return True
161
+ except Exception:
162
+ return False
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/prompt_parser_hijack.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # lib_neutral_prompt/prompt_parser_hijack.py
2
+ import lark # оставим импорт, если webui его уже тянет
3
+ from modules import prompt_parser, script_callbacks
4
+ from . import neutral_prompt_parser, hijacker, global_state
5
+
6
+ class WebuiPromptVisitor:
7
+ """Транспилирует наше дерево в webui-совместимую строку:
8
+ - лист: (text:weight)
9
+ - составной: child1 AND child2 ...
10
+ """
11
+ def visit_leaf_prompt(self, that: neutral_prompt_parser.LeafPrompt) -> str:
12
+ text = str(getattr(that, "prompt", "")).strip()
13
+ w = float(getattr(that, "weight", 1.0) or 1.0)
14
+ return f"({text}:{w})" if text else ""
15
+
16
+ def visit_composite_prompt(self, that: neutral_prompt_parser.CompositePrompt) -> str:
17
+ parts = [child.accept(self) for child in getattr(that, "children", [])]
18
+ parts = [p for p in parts if p]
19
+ return " AND ".join(parts)
20
+
21
+ # ✅ создаём хижакер с обязательным hijacker_attribute (как в cfg_denoiser_hijack)
22
+ prompt_parser_hijacker = hijacker.ModuleHijacker.install_or_get(
23
+ module=prompt_parser,
24
+ hijacker_attribute="__neutral_prompt_hijacker",
25
+ on_uninstall=script_callbacks.on_script_unloaded,
26
+ )
27
+
28
+ def parse_prompts(prompts):
29
+ """
30
+ Принимает: строку, список/кортеж строк, или prompt_parser.SdConditioning.
31
+ Возвращает: List[str] — вебуишные строки вида "(text:weight) AND (...)".
32
+ Параллельно сохраняет raw_prompts и разобранные expr'ы в global_state.
33
+ """
34
+ # 1) Нормализуем вход к List[str]
35
+ SdConditioning = getattr(prompt_parser, "SdConditioning", None)
36
+
37
+ if SdConditioning is not None and isinstance(prompts, SdConditioning):
38
+ # SdConditioning может быть итерируемым набором саб-промптов
39
+ try:
40
+ webui_prompts = [str(p) for p in prompts]
41
+ except Exception:
42
+ webui_prompts = list(prompts)
43
+ elif isinstance(prompts, (list, tuple)):
44
+ webui_prompts = ["" if p is None else str(p) for p in prompts]
45
+ elif prompts is None:
46
+ webui_prompts = [""]
47
+ else:
48
+ webui_prompts = [str(prompts)]
49
+
50
+ # 2) Сохраняем «сырые» промпты для расписаний WebUI
51
+ global_state.raw_prompts = webui_prompts[:]
52
+
53
+ # 3) Парсим в expr-деревья и кладём в глобальное состояние
54
+ exprs = [neutral_prompt_parser.parse_root(p) for p in webui_prompts]
55
+ global_state.prompt_exprs = exprs
56
+
57
+ # 4) Транспилируем expr -> webui-строки совместимые с get_multicond_prompt_list
58
+ visitor = WebuiPromptVisitor()
59
+ return [expr.accept(visitor) for expr in exprs]
60
+
61
+ @prompt_parser_hijacker.hijack("get_multicond_prompt_list")
62
+ def get_multicond_prompt_list_hijack(prompts, original_function):
63
+ if not getattr(global_state, "is_enabled", False):
64
+ return original_function(prompts)
65
+
66
+ # 1) Нормализуем вход в список строк
67
+ SdCond = getattr(prompt_parser, "SdConditioning", None)
68
+ if SdCond and isinstance(prompts, SdCond):
69
+ try:
70
+ webui_prompts = [str(p) for p in prompts] # у вас SdConditioning — итерируемый список строк
71
+ except Exception:
72
+ webui_prompts = list(prompts)
73
+ elif isinstance(prompts, list):
74
+ webui_prompts = [str(p) for p in prompts]
75
+ else:
76
+ webui_prompts = [str(prompts)]
77
+
78
+ # 2) Сохраняем «сырые» промпты для webui-расписаний
79
+ global_state.raw_prompts = webui_prompts[:]
80
+
81
+ # 3) Строим expr'ы и держим их в глобальном состоянии
82
+ exprs = [neutral_prompt_parser.parse_root(p) for p in webui_prompts]
83
+ global_state.prompt_exprs = exprs
84
+
85
+ # 4) Возвращаем webui-совместимые строки (AND и (text:weight))
86
+ visitor = WebuiPromptVisitor()
87
+ transpiled = [expr.accept(visitor) for expr in exprs]
88
+ return original_function(transpiled)
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/ui.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # lib_neutral_prompt/ui.py
2
+ from lib_neutral_prompt import global_state, neutral_prompt_parser
3
+ from modules import script_callbacks, shared
4
+ from typing import Dict, Tuple, List, Callable
5
+ import gradio as gr
6
+ import dataclasses
7
+
8
+ txt2img_prompt_textbox = None
9
+ img2img_prompt_textbox = None
10
+
11
+ # Только поддерживаемые типы
12
+ prompt_types = {
13
+ 'Perpendicular': neutral_prompt_parser.PromptKeyword.AND_PERP.value,
14
+ 'Saliency-aware': neutral_prompt_parser.PromptKeyword.AND_SALT.value,
15
+ 'Semantic guidance top-k': neutral_prompt_parser.PromptKeyword.AND_TOPK.value,
16
+ }
17
+
18
+ prompt_types_tooltip = '\n'.join([
19
+ 'AND — базовое сложение фич (встроено в WebUI, режим по умолчанию)',
20
+ 'Perpendicular (AND_PERP) — подавляет противоречащие фичи',
21
+ 'Saliency-aware (AND_SALT) — побеждают самые «заметные» фичи',
22
+ 'Semantic guidance top-k (AND_TOPK) — таргетные точечные изменения',
23
+ ])
24
+
25
+ GUIDE_MD = """
26
+ ### Neutral z prompt — Guide & Examples
27
+
28
+ **Как это работает**
29
+ - `AND` — обычная сумма фич. Пишется напрямую в промпте: `a AND (b:1.2)`
30
+ - Расширенные режимы оформляются ключевыми словами и **квадратными скобками**:
31
+ - `AND_PERP [ ... ] : w` — *перпендикулярная* компонента. Помогает «убрать конфликтующее».
32
+ - `AND_SALT [ ... ] : w` — *saliency-aware*. Смешивает в пользу «самых заметных» участков.
33
+ - `AND_TOPK [ ... ] : w` — *semantic top-k*. Оставляет только самые сильные (по модулю) изменения.
34
+
35
+ **Веса**
36
+ - Внутри скобок: `(text:0.8)` — классика WebUI.
37
+ - В плоском виде: `text:0.8` — тоже поддерживается.
38
+ - Вес группы: `AND_SALT [ a AND (b:0.7) ] : 0.5` — общий вес умножится на вклад детей.
39
+
40
+ **Быстрые примеры**
41
+ ```text
42
+ # Базовый AND
43
+ a cinematic portrait AND (dramatic lighting:1.2)
44
+
45
+ # Perpendicular: добавь деталь, не ломая основной стиль
46
+ AND_PERP [ cinematic lighting AND (sharp details:1.1) ] : 0.6
47
+
48
+ # Saliency-aware: усили неон и отблески
49
+ AND_SALT [ neon AND reflections:0.7 ] : 0.5
50
+
51
+ # Semantic top-k: точечные правки лица
52
+ AND_TOPK [ add freckles AND brighten eyes ] : 0.4
53
+ ```
54
+
55
+ **Комбо-примеры**
56
+ ```text
57
+ # Комбинация SALT + PERP
58
+ a rainy street at night
59
+ AND_SALT [ neon signs AND wet asphalt reflections:0.8 ] : 0.5
60
+ AND_PERP [ remove fog AND (crisp edges:1.1) ] : 0.4
61
+
62
+ # Комбинация TOPK + AND
63
+ a studio headshot
64
+ AND_TOPK [ enhance iris detail AND subtle skin texture ] : 0.35
65
+ AND (soft key light:1.15)
66
+ ```
67
+
68
+ **Расписания (prompt travel) — делает сам WebUI**
69
+ - Используй стандартный синтаксис WebUI, например:
70
+ ```text
71
+ a sunset over city [a sunrise over city: 18]
72
+ ```
73
+ - Наше расширение берёт **исходный текст** (raw prompt) для расписаний, поэтому просто пиши как обычно.
74
+ - Расширенные блоки тоже можно комбинировать с расписанием (WebUI решит активный текст на каждом шаге).
75
+
76
+ **Советы**
77
+ - Пиши «расширенные» блоки ближе к концу промпта и увеличивай вес постепенно (`0.3 → 0.6`).
78
+ - Если что-то «перетягивает одеяло», снизь вес блока или листьев внутри него.
79
+ - Не злоупотребляй количеством расширенных блоков: один-два аккуратных блока обычно лучше.
80
+ """
81
+
82
+ # Готовые примеры для кнопок вставки
83
+ EXAMPLE_BASE_AND = "a cinematic portrait AND (dramatic lighting:1.2)"
84
+ EXAMPLE_PERP = "AND_PERP [ cinematic lighting AND (sharp details:1.1) ] : 0.6"
85
+ EXAMPLE_SALT = "AND_SALT [ neon AND reflections:0.7 ] : 0.5"
86
+ EXAMPLE_TOPK = "AND_TOPK [ add freckles AND brighten eyes ] : 0.4"
87
+ EXAMPLE_COMBO = """a rainy street at night
88
+ AND_SALT [ neon signs AND wet asphalt reflections:0.8 ] : 0.5
89
+ AND_PERP [ remove fog AND (crisp edges:1.1) ] : 0.4"""
90
+ EXAMPLE_SCHEDULE = "a sunset over city [a sunrise over city: 18]"
91
+
92
+ @dataclasses.dataclass
93
+ class AccordionInterface:
94
+ get_elem_id: Callable
95
+
96
+ def __post_init__(self):
97
+ self.is_rendered = False
98
+
99
+ # Верхний слайдер
100
+ self.cfg_rescale = gr.Slider(
101
+ label='CFG rescale',
102
+ minimum=0, maximum=1, value=0
103
+ )
104
+
105
+ # Форматтер
106
+ self.neutral_prompt = gr.Textbox(
107
+ label='Neutral z prompt',
108
+ show_label=False,
109
+ lines=3,
110
+ placeholder='Neutral z prompt (нажмите Apply, чтобы добавить в позитивный промпт)'
111
+ )
112
+ self.neutral_cond_scale = gr.Slider(
113
+ label='Prompt weight',
114
+ minimum=-3, maximum=3, value=1
115
+ )
116
+ self.aux_prompt_type = gr.Dropdown(
117
+ label='Prompt type',
118
+ choices=list(prompt_types.keys()),
119
+ value=next(iter(prompt_types.keys())),
120
+ tooltip=prompt_types_tooltip,
121
+ elem_id=self.get_elem_id('formatter_prompt_type')
122
+ )
123
+ self.append_to_prompt_button = gr.Button(value='Apply to prompt')
124
+
125
+ # Гайд и кнопки вставки примеров
126
+ self.docs_md = gr.Markdown(value=GUIDE_MD)
127
+ self.btn_ins_base = gr.Button(value='Вставить пример: AND')
128
+ self.btn_ins_perp = gr.Button(value='Вставить пример: AND_PERP')
129
+ self.btn_ins_salt = gr.Button(value='Вставить пример: AND_SALT')
130
+ self.btn_ins_topk = gr.Button(value='Вставить пример: AND_TOPK')
131
+ self.btn_ins_combo = gr.Button(value='Вставить пример: Комбо')
132
+ self.btn_ins_schedule = gr.Button(value='Вставить пример: Расписание')
133
+
134
+ def arrange_components(self, is_img2img: bool):
135
+ if self.is_rendered:
136
+ return
137
+ with gr.Accordion(label='Neutral z prompt', open=False):
138
+ self.cfg_rescale.render()
139
+ with gr.Accordion(label='Prompt formatter', open=False):
140
+ self.neutral_prompt.render()
141
+ self.neutral_cond_scale.render()
142
+ self.aux_prompt_type.render()
143
+ self.append_to_prompt_button.render()
144
+ with gr.Accordion(label='Guide & Examples', open=False):
145
+ self.docs_md.render()
146
+ with gr.Row():
147
+ self.btn_ins_base.render()
148
+ self.btn_ins_perp.render()
149
+ with gr.Row():
150
+ self.btn_ins_salt.render()
151
+ self.btn_ins_topk.render()
152
+ with gr.Row():
153
+ self.btn_ins_combo.render()
154
+ self.btn_ins_schedule.render()
155
+
156
+ def connect_events(self, is_img2img: bool):
157
+ if self.is_rendered:
158
+ return
159
+ prompt_textbox = img2img_prompt_textbox if is_img2img else txt2img_prompt_textbox
160
+
161
+ def _append(init_prompt: str, prompt: str, scale: float, prompt_type: str):
162
+ base = (init_prompt or '').rstrip()
163
+ sep = '\n' if base else ''
164
+ return f"{base}{sep}{prompt_types[prompt_type]} {prompt} :{scale}", ""
165
+
166
+ def _append_example(init_prompt: str, snippet: str):
167
+ base = (init_prompt or '').rstrip()
168
+ sep = '\n' if base else ''
169
+ return f"{base}{sep}{snippet}"
170
+
171
+ # Кнопка «Apply to prompt» (форматтер)
172
+ self.append_to_prompt_button.click(
173
+ fn=_append,
174
+ inputs=[prompt_textbox, self.neutral_prompt, self.neutral_cond_scale, self.aux_prompt_type],
175
+ outputs=[prompt_textbox, self.neutral_prompt]
176
+ )
177
+
178
+ # Кнопки вставки примеров
179
+ self.btn_ins_base.click(
180
+ fn=_append_example,
181
+ inputs=[prompt_textbox, gr.State(EXAMPLE_BASE_AND)],
182
+ outputs=prompt_textbox
183
+ )
184
+ self.btn_ins_perp.click(
185
+ fn=_append_example,
186
+ inputs=[prompt_textbox, gr.State(EXAMPLE_PERP)],
187
+ outputs=prompt_textbox
188
+ )
189
+ self.btn_ins_salt.click(
190
+ fn=_append_example,
191
+ inputs=[prompt_textbox, gr.State(EXAMPLE_SALT)],
192
+ outputs=prompt_textbox
193
+ )
194
+ self.btn_ins_topk.click(
195
+ fn=_append_example,
196
+ inputs=[prompt_textbox, gr.State(EXAMPLE_TOPK)],
197
+ outputs=prompt_textbox
198
+ )
199
+ self.btn_ins_combo.click(
200
+ fn=_append_example,
201
+ inputs=[prompt_textbox, gr.State(EXAMPLE_COMBO)],
202
+ outputs=prompt_textbox
203
+ )
204
+ self.btn_ins_schedule.click(
205
+ fn=_append_example,
206
+ inputs=[prompt_textbox, gr.State(EXAMPLE_SCHEDULE)],
207
+ outputs=prompt_textbox
208
+ )
209
+
210
+ def set_rendered(self, value: bool = True):
211
+ self.is_rendered = value
212
+
213
+ def get_components(self) -> Tuple[gr.components.Component]:
214
+ return (self.cfg_rescale,)
215
+
216
+ def get_infotext_fields(self) -> Tuple[Tuple[gr.components.Component, str]]:
217
+ return tuple(zip(self.get_components(), ('CFG Rescale',)))
218
+
219
+ def get_paste_field_names(self) -> List[str]:
220
+ return ['CFG Rescale']
221
+
222
+ def get_extra_generation_params(self, args: Dict) -> Dict:
223
+ return {'CFG Rescale': args['cfg_rescale']}
224
+
225
+ def unpack_processing_args(self, cfg_rescale: float) -> Dict:
226
+ return {'cfg_rescale': cfg_rescale}
227
+
228
+ def on_ui_settings():
229
+ section = ('neutral_prompt', 'Neutral z prompt')
230
+ shared.opts.add_option(
231
+ 'neutral_prompt_enabled',
232
+ shared.OptionInfo(True, 'Enable neutral-z-prompt extension', section=section)
233
+ )
234
+ global_state.is_enabled = shared.opts.data.get('neutral_prompt_enabled', True)
235
+
236
+ shared.opts.add_option(
237
+ 'neutral_prompt_verbose',
238
+ shared.OptionInfo(False, 'Enable verbose debugging for neutral-z-prompt', section=section)
239
+ )
240
+ shared.opts.onchange('neutral_prompt_verbose', update_verbose)
241
+
242
+ script_callbacks.on_ui_settings(on_ui_settings)
243
+
244
+ def update_verbose():
245
+ global_state.verbose = shared.opts.data.get('neutral_prompt_verbose', False)
246
+
247
+ def on_after_component(component, **_kwargs):
248
+ if getattr(component, 'elem_id', None) == 'txt2img_prompt':
249
+ global txt2img_prompt_textbox
250
+ txt2img_prompt_textbox = component
251
+ if getattr(component, 'elem_id', None) == 'img2img_prompt':
252
+ global img2img_prompt_textbox
253
+ img2img_prompt_textbox = component
254
+
255
+ script_callbacks.on_after_component(on_after_component)
z-sd-webui-neutral-prompt-workYEAH8/lib_neutral_prompt/xyz_grid.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from types import ModuleType
3
+ from typing import Optional
4
+ from modules import scripts
5
+ from lib_neutral_prompt import global_state
6
+
7
+
8
+ def patch():
9
+ xyz_module = find_xyz_module()
10
+ if xyz_module is None:
11
+ print("[sd-webui-neutral-prompt]", "xyz_grid.py not found.", file=sys.stderr)
12
+ return
13
+
14
+ xyz_module.axis_options.extend([
15
+ xyz_module.AxisOption("[Neutral Prompt] CFG Rescale", int_or_float, apply_cfg_rescale()),
16
+ ])
17
+
18
+
19
+ class XyzFloat(float):
20
+ is_xyz: bool = True
21
+
22
+
23
+ def apply_cfg_rescale():
24
+ def callback(_p, v, _vs):
25
+ global_state.cfg_rescale = XyzFloat(v)
26
+
27
+ return callback
28
+
29
+
30
+ def int_or_float(string):
31
+ try:
32
+ return int(string)
33
+ except ValueError:
34
+ return float(string)
35
+
36
+
37
+ def find_xyz_module() -> Optional[ModuleType]:
38
+ for data in scripts.scripts_data:
39
+ if data.script_class.__module__ in {"xyz_grid.py", "xy_grid.py"} and hasattr(data, "module"):
40
+ return data.module
41
+
42
+ return None
z-sd-webui-neutral-prompt-workYEAH8/scripts/__pycache__/neutral_prompt.cpython-310.pyc ADDED
Binary file (3.58 kB). View file
 
z-sd-webui-neutral-prompt-workYEAH8/scripts/neutral_prompt.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from lib_neutral_prompt import global_state, hijacker, neutral_prompt_parser, prompt_parser_hijack, cfg_denoiser_hijack, ui, xyz_grid
2
+ from modules import scripts, processing, shared
3
+ from typing import Dict
4
+ import functools
5
+
6
+
7
+ class NeutralPromptScript(scripts.Script):
8
+ def __init__(self):
9
+ self.accordion_interface = None
10
+ self._is_img2img = False
11
+
12
+ @property
13
+ def is_img2img(self):
14
+ return self._is_img2img
15
+
16
+ @is_img2img.setter
17
+ def is_img2img(self, is_img2img):
18
+ self._is_img2img = is_img2img
19
+ if self.accordion_interface is None:
20
+ self.accordion_interface = ui.AccordionInterface(self.elem_id)
21
+
22
+ def title(self) -> str:
23
+ return "Neutral Z Prompt"
24
+
25
+ def show(self, is_img2img: bool):
26
+ return scripts.AlwaysVisible
27
+
28
+ def ui(self, is_img2img: bool):
29
+ self.hijack_composable_lora(is_img2img)
30
+
31
+ self.accordion_interface.arrange_components(is_img2img)
32
+ self.accordion_interface.connect_events(is_img2img)
33
+ self.infotext_fields = self.accordion_interface.get_infotext_fields()
34
+ self.paste_field_names = self.accordion_interface.get_paste_field_names()
35
+ self.accordion_interface.set_rendered()
36
+ return self.accordion_interface.get_components()
37
+
38
+ def process(self, p: processing.StableDiffusionProcessing, *args):
39
+ args = self.accordion_interface.unpack_processing_args(*args)
40
+
41
+ self.update_global_state(args)
42
+ if global_state.is_enabled:
43
+ p.extra_generation_params.update(self.accordion_interface.get_extra_generation_params(args))
44
+
45
+ def update_global_state(self, args: Dict):
46
+ if shared.state.job_no == 0:
47
+ global_state.is_enabled = shared.opts.data.get('neutral_z_prompt_enabled', True)
48
+
49
+ for k, v in args.items():
50
+ try:
51
+ getattr(global_state, k)
52
+ except AttributeError:
53
+ continue
54
+
55
+ # Значение пришло из X/Y Grid и помечено подклассом float.
56
+ current = getattr(global_state, k)
57
+ if getattr(type(current), 'is_xyz', False):
58
+ # применяем только на этот прогон; в global_state не записываем
59
+ args[k] = float(current)
60
+ continue
61
+
62
+ if shared.state.job_no > 0:
63
+ continue
64
+
65
+ setattr(global_state, k, v)
66
+
67
+ def hijack_composable_lora(self, is_img2img):
68
+ if self.accordion_interface.is_rendered:
69
+ return
70
+
71
+ lora_script = None
72
+ script_runner = scripts.scripts_img2img if is_img2img else scripts.scripts_txt2img
73
+
74
+ for script in script_runner.alwayson_scripts:
75
+ if script.title().lower() == "composable lora":
76
+ lora_script = script
77
+ break
78
+
79
+ if lora_script is not None:
80
+ lora_script.process = functools.partial(composable_lora_process_hijack, original_function=lora_script.process)
81
+
82
+
83
+ def composable_lora_process_hijack(p: processing.StableDiffusionProcessing, *args, original_function, **kwargs):
84
+ if not global_state.is_enabled:
85
+ return original_function(p, *args, **kwargs)
86
+
87
+ # parse_prompts уже возвращает List[str]
88
+ transformed_prompts = prompt_parser_hijack.parse_prompts(p.all_prompts)
89
+ all_prompts, p.all_prompts = p.all_prompts, transformed_prompts
90
+ try:
91
+ res = original_function(p, *args, **kwargs)
92
+ finally:
93
+ # обязательно вернуть исходные промпты
94
+ p.all_prompts = all_prompts
95
+ return res
96
+
97
+
98
+
99
+ xyz_grid.patch()
z-sd-webui-neutral-prompt-workYEAH8/test/perp_parser/__init__.py ADDED
File without changes
z-sd-webui-neutral-prompt-workYEAH8/test/perp_parser/basic_test.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ import pathlib
3
+ import sys
4
+ sys.path.append(str(pathlib.Path(__file__).parent.parent.parent))
5
+ from lib_neutral_prompt import neutral_prompt_parser
6
+
7
+
8
+ class TestPromptParser(unittest.TestCase):
9
+ def setUp(self):
10
+ self.simple_prompt = neutral_prompt_parser.parse_root("hello :1.0")
11
+ self.and_prompt = neutral_prompt_parser.parse_root("hello AND goodbye :2.0")
12
+ self.and_perp_prompt = neutral_prompt_parser.parse_root("hello :1.0 AND_PERP goodbye :2.0")
13
+ self.and_salt_prompt = neutral_prompt_parser.parse_root("hello :1.0 AND_SALT goodbye :2.0")
14
+ self.nested_and_perp_prompt = neutral_prompt_parser.parse_root("hello :1.0 AND_PERP [goodbye :2.0 AND_PERP welcome :3.0]")
15
+ self.nested_and_salt_prompt = neutral_prompt_parser.parse_root("hello :1.0 AND_SALT [goodbye :2.0 AND_SALT welcome :3.0]")
16
+ self.invalid_weight = neutral_prompt_parser.parse_root("hello :not_a_float")
17
+
18
+ def test_simple_prompt_child_count(self):
19
+ self.assertEqual(len(self.simple_prompt.children), 1)
20
+
21
+ def test_simple_prompt_child_weight(self):
22
+ self.assertEqual(self.simple_prompt.children[0].weight, 1.0)
23
+
24
+ def test_simple_prompt_child_prompt(self):
25
+ self.assertEqual(self.simple_prompt.children[0].prompt, "hello ")
26
+
27
+ def test_square_weight_prompt(self):
28
+ prompt = "a [b c d e : f g h :1.5]"
29
+ parsed = neutral_prompt_parser.parse_root(prompt)
30
+ self.assertEqual(parsed.children[0].prompt, prompt)
31
+
32
+ composed_prompt = f"{prompt} AND_PERP other prompt"
33
+ parsed = neutral_prompt_parser.parse_root(composed_prompt)
34
+ self.assertEqual(parsed.children[0].prompt, prompt)
35
+
36
+ def test_and_prompt_child_count(self):
37
+ self.assertEqual(len(self.and_prompt.children), 2)
38
+
39
+ def test_and_prompt_child_weights_and_prompts(self):
40
+ self.assertEqual(self.and_prompt.children[0].weight, 1.0)
41
+ self.assertEqual(self.and_prompt.children[0].prompt, "hello ")
42
+ self.assertEqual(self.and_prompt.children[1].weight, 2.0)
43
+ self.assertEqual(self.and_prompt.children[1].prompt, " goodbye ")
44
+
45
+ def test_and_perp_prompt_child_count(self):
46
+ self.assertEqual(len(self.and_perp_prompt.children), 2)
47
+
48
+ def test_and_perp_prompt_child_types(self):
49
+ self.assertIsInstance(self.and_perp_prompt.children[0], neutral_prompt_parser.LeafPrompt)
50
+ self.assertIsInstance(self.and_perp_prompt.children[1], neutral_prompt_parser.LeafPrompt)
51
+
52
+ def test_and_perp_prompt_nested_child(self):
53
+ nested_child = self.and_perp_prompt.children[1]
54
+ self.assertEqual(nested_child.weight, 2.0)
55
+ self.assertEqual(nested_child.prompt.strip(), "goodbye")
56
+
57
+ def test_nested_and_perp_prompt_child_count(self):
58
+ self.assertEqual(len(self.nested_and_perp_prompt.children), 2)
59
+
60
+ def test_nested_and_perp_prompt_child_types(self):
61
+ self.assertIsInstance(self.nested_and_perp_prompt.children[0], neutral_prompt_parser.LeafPrompt)
62
+ self.assertIsInstance(self.nested_and_perp_prompt.children[1], neutral_prompt_parser.CompositePrompt)
63
+
64
+ def test_nested_and_perp_prompt_nested_child_types(self):
65
+ nested_child = self.nested_and_perp_prompt.children[1].children[0]
66
+ self.assertIsInstance(nested_child, neutral_prompt_parser.LeafPrompt)
67
+ nested_child = self.nested_and_perp_prompt.children[1].children[1]
68
+ self.assertIsInstance(nested_child, neutral_prompt_parser.LeafPrompt)
69
+
70
+ def test_nested_and_perp_prompt_nested_child(self):
71
+ nested_child = self.nested_and_perp_prompt.children[1].children[1]
72
+ self.assertEqual(nested_child.weight, 3.0)
73
+ self.assertEqual(nested_child.prompt.strip(), "welcome")
74
+
75
+ def test_invalid_weight_child_count(self):
76
+ self.assertEqual(len(self.invalid_weight.children), 1)
77
+
78
+ def test_invalid_weight_child_weight(self):
79
+ self.assertEqual(self.invalid_weight.children[0].weight, 1.0)
80
+
81
+ def test_invalid_weight_child_prompt(self):
82
+ self.assertEqual(self.invalid_weight.children[0].prompt, "hello :not_a_float")
83
+
84
+ def test_and_salt_prompt_child_count(self):
85
+ self.assertEqual(len(self.and_salt_prompt.children), 2)
86
+
87
+ def test_and_salt_prompt_child_types(self):
88
+ self.assertIsInstance(self.and_salt_prompt.children[0], neutral_prompt_parser.LeafPrompt)
89
+ self.assertIsInstance(self.and_salt_prompt.children[1], neutral_prompt_parser.LeafPrompt)
90
+
91
+ def test_and_salt_prompt_nested_child(self):
92
+ nested_child = self.and_salt_prompt.children[1]
93
+ self.assertEqual(nested_child.weight, 2.0)
94
+ self.assertEqual(nested_child.prompt.strip(), "goodbye")
95
+
96
+ def test_nested_and_salt_prompt_child_count(self):
97
+ self.assertEqual(len(self.nested_and_salt_prompt.children), 2)
98
+
99
+ def test_nested_and_salt_prompt_child_types(self):
100
+ self.assertIsInstance(self.nested_and_salt_prompt.children[0], neutral_prompt_parser.LeafPrompt)
101
+ self.assertIsInstance(self.nested_and_salt_prompt.children[1], neutral_prompt_parser.CompositePrompt)
102
+
103
+ def test_nested_and_salt_prompt_nested_child_types(self):
104
+ nested_child = self.nested_and_salt_prompt.children[1].children[0]
105
+ self.assertIsInstance(nested_child, neutral_prompt_parser.LeafPrompt)
106
+ nested_child = self.nested_and_salt_prompt.children[1].children[1]
107
+ self.assertIsInstance(nested_child, neutral_prompt_parser.LeafPrompt)
108
+
109
+ def test_nested_and_salt_prompt_nested_child(self):
110
+ nested_child = self.nested_and_salt_prompt.children[1].children[1]
111
+ self.assertEqual(nested_child.weight, 3.0)
112
+ self.assertEqual(nested_child.prompt.strip(), "welcome")
113
+
114
+ def test_start_with_prompt_editing(self):
115
+ prompt = "[(long shot:1.2):0.1] detail.."
116
+ res = neutral_prompt_parser.parse_root(prompt)
117
+ self.assertEqual(res.children[0].weight, 1.0)
118
+ self.assertEqual(res.children[0].prompt, prompt)
119
+
120
+
121
+ if __name__ == '__main__':
122
+ unittest.main()
z-sd-webui-neutral-prompt-workYEAH8/test/perp_parser/malicious_test.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ import pathlib
3
+ import sys
4
+ sys.path.append(str(pathlib.Path(__file__).parent.parent.parent))
5
+ from lib_neutral_prompt import neutral_prompt_parser
6
+
7
+
8
+ class TestMaliciousPromptParser(unittest.TestCase):
9
+ def setUp(self):
10
+ self.parser = neutral_prompt_parser
11
+
12
+ def test_empty(self):
13
+ result = self.parser.parse_root("")
14
+ self.assertEqual(result.children[0].prompt, "")
15
+ self.assertEqual(result.children[0].weight, 1.0)
16
+
17
+ def test_zero_weight(self):
18
+ result = self.parser.parse_root("hello :0.0")
19
+ self.assertEqual(result.children[0].weight, 0.0)
20
+
21
+ def test_mixed_positive_and_negative_weights(self):
22
+ result = self.parser.parse_root("hello :1.0 AND goodbye :-2.0")
23
+ self.assertEqual(result.children[0].weight, 1.0)
24
+ self.assertEqual(result.children[1].weight, -2.0)
25
+
26
+ def test_debalanced_square_brackets(self):
27
+ prompt = "a [ b " * 100
28
+ result = self.parser.parse_root(prompt)
29
+ self.assertEqual(result.children[0].prompt, prompt)
30
+
31
+ prompt = "a ] b " * 100
32
+ result = self.parser.parse_root(prompt)
33
+ self.assertEqual(result.children[0].prompt, prompt)
34
+
35
+ repeats = 10
36
+ prompt = "a [ [ b AND c ] " * repeats
37
+ result = self.parser.parse_root(prompt)
38
+ self.assertEqual([x.prompt for x in result.children], ["a [[ b ", *[" c ] a [[ b "] * (repeats - 1), " c ]"])
39
+
40
+ repeats = 10
41
+ prompt = "a [ b AND c ] ] " * repeats
42
+ result = self.parser.parse_root(prompt)
43
+ self.assertEqual([x.prompt for x in result.children], ["a [ b ", *[" c ]] a [ b "] * (repeats - 1), " c ]]"])
44
+
45
+ def test_erroneous_syntax(self):
46
+ result = self.parser.parse_root("hello :1.0 AND_PERP [goodbye :2.0")
47
+ self.assertEqual(result.children[0].weight, 1.0)
48
+ self.assertEqual(result.children[1].prompt, "[goodbye ")
49
+ self.assertEqual(result.children[1].weight, 2.0)
50
+
51
+ result = self.parser.parse_root("hello :1.0 AND_PERP goodbye :2.0]")
52
+ self.assertEqual(result.children[0].weight, 1.0)
53
+ self.assertEqual(result.children[1].prompt, " goodbye ")
54
+
55
+ result = self.parser.parse_root("hello :1.0 AND_PERP goodbye] :2.0")
56
+ self.assertEqual(result.children[1].prompt, " goodbye]")
57
+ self.assertEqual(result.children[1].weight, 2.0)
58
+
59
+ result = self.parser.parse_root("hello :1.0 AND_PERP a [ goodbye :2.0")
60
+ self.assertEqual(result.children[1].weight, 2.0)
61
+ self.assertEqual(result.children[1].prompt, " a [ goodbye ")
62
+
63
+ result = self.parser.parse_root("hello :1.0 AND_PERP AND goodbye :2.0")
64
+ self.assertEqual(result.children[0].weight, 1.0)
65
+ self.assertEqual(result.children[2].prompt, " goodbye ")
66
+
67
+ def test_huge_number_of_prompt_parts(self):
68
+ result = self.parser.parse_root(" AND ".join(f"hello{i} :{i}" for i in range(10**4)))
69
+ self.assertEqual(len(result.children), 10**4)
70
+
71
+ def test_prompt_ending_with_weight(self):
72
+ result = self.parser.parse_root("hello :1.0 AND :2.0")
73
+ self.assertEqual(result.children[0].weight, 1.0)
74
+ self.assertEqual(result.children[1].prompt, "")
75
+ self.assertEqual(result.children[1].weight, 2.0)
76
+
77
+ def test_huge_input_string(self):
78
+ big_string = "hello :1.0 AND " * 10**4
79
+ result = self.parser.parse_root(big_string)
80
+ self.assertEqual(len(result.children), 10**4 + 1)
81
+
82
+ def test_deeply_nested_prompt(self):
83
+ deeply_nested_prompt = "hello :1.0" + " AND_PERP [goodbye :2.0" * 100 + "]" * 100
84
+ result = self.parser.parse_root(deeply_nested_prompt)
85
+ self.assertIsInstance(result.children[1], neutral_prompt_parser.CompositePrompt)
86
+
87
+ def test_complex_nested_prompts(self):
88
+ complex_prompt = "hello :1.0 AND goodbye :2.0 AND_PERP [welcome :3.0 AND farewell :4.0 AND_PERP greetings:5.0]"
89
+ result = self.parser.parse_root(complex_prompt)
90
+ self.assertEqual(result.children[0].weight, 1.0)
91
+ self.assertEqual(result.children[1].weight, 2.0)
92
+ self.assertEqual(result.children[2].children[0].weight, 3.0)
93
+ self.assertEqual(result.children[2].children[1].weight, 4.0)
94
+ self.assertEqual(result.children[2].children[2].weight, 5.0)
95
+
96
+ def test_string_with_random_characters(self):
97
+ random_chars = "ASDFGHJKL:@#$/.,|}{><~`12[3]456AND_PERP7890"
98
+ try:
99
+ self.parser.parse_root(random_chars)
100
+ except Exception:
101
+ self.fail("parse_root couldn't handle a string with random characters.")
102
+
103
+ def test_string_with_unexpected_symbols(self):
104
+ unexpected_symbols = "hello :1.0 AND $%^&*()goodbye :2.0"
105
+ try:
106
+ self.parser.parse_root(unexpected_symbols)
107
+ except Exception:
108
+ self.fail("parse_root couldn't handle a string with unexpected symbols.")
109
+
110
+ def test_string_with_unconventional_structure(self):
111
+ unconventional_structure = "hello :1.0 AND_PERP :2.0 AND [goodbye]"
112
+ try:
113
+ self.parser.parse_root(unconventional_structure)
114
+ except Exception:
115
+ self.fail("parse_root couldn't handle a string with unconventional structure.")
116
+
117
+ def test_string_with_mixed_alphabets_and_numbers(self):
118
+ mixed_alphabets_and_numbers = "123hello :1.0 AND goodbye456 :2.0"
119
+ try:
120
+ self.parser.parse_root(mixed_alphabets_and_numbers)
121
+ except Exception:
122
+ self.fail("parse_root couldn't handle a string with mixed alphabets and numbers.")
123
+
124
+ def test_string_with_nested_brackets(self):
125
+ nested_brackets = "hello :1.0 AND [goodbye :2.0 AND [[welcome :3.0]]]"
126
+ try:
127
+ self.parser.parse_root(nested_brackets)
128
+ except Exception:
129
+ self.fail("parse_root couldn't handle a string with nested brackets.")
130
+
131
+ def test_unmatched_opening_braces(self):
132
+ unmatched_opening_braces = "hello [[[[[[[[[ :1.0 AND_PERP goodbye :2.0"
133
+ try:
134
+ self.parser.parse_root(unmatched_opening_braces)
135
+ except Exception:
136
+ self.fail("parse_root couldn't handle a string with unmatched opening braces.")
137
+
138
+ def test_unmatched_closing_braces(self):
139
+ unmatched_closing_braces = "hello :1.0 AND_PERP goodbye ]]]]]]]]] :2.0"
140
+ try:
141
+ self.parser.parse_root(unmatched_closing_braces)
142
+ except Exception:
143
+ self.fail("parse_root couldn't handle a string with unmatched closing braces.")
144
+
145
+ def test_repeating_colons(self):
146
+ repeating_colons = "hello ::::::: :1.0 AND_PERP goodbye :::: :2.0"
147
+ try:
148
+ self.parser.parse_root(repeating_colons)
149
+ except Exception:
150
+ self.fail("parse_root couldn't handle a string with repeating colons.")
151
+
152
+ def test_excessive_whitespace(self):
153
+ excessive_whitespace = "hello :1.0 AND_PERP goodbye :2.0"
154
+ try:
155
+ self.parser.parse_root(excessive_whitespace)
156
+ except Exception:
157
+ self.fail("parse_root couldn't handle a string with excessive whitespace.")
158
+
159
+ def test_repeating_AND_keyword(self):
160
+ repeating_AND_keyword = "hello :1.0 AND AND AND AND AND goodbye :2.0"
161
+ try:
162
+ self.parser.parse_root(repeating_AND_keyword)
163
+ except Exception:
164
+ self.fail("parse_root couldn't handle a string with repeating AND keyword.")
165
+
166
+ def test_repeating_AND_PERP_keyword(self):
167
+ repeating_AND_PERP_keyword = "hello :1.0 AND_PERP AND_PERP AND_PERP AND_PERP goodbye :2.0"
168
+ try:
169
+ self.parser.parse_root(repeating_AND_PERP_keyword)
170
+ except Exception:
171
+ self.fail("parse_root couldn't handle a string with repeating AND_PERP keyword.")
172
+
173
+ def test_square_weight_prompt(self):
174
+ prompt = "AND_PERP [weighted] you thought it was the end"
175
+ try:
176
+ self.parser.parse_root(prompt)
177
+ except Exception:
178
+ self.fail("parse_root couldn't handle a string starting with a square-weighted sub-prompt.")
179
+
180
+
181
+ if __name__ == '__main__':
182
+ unittest.main()