RioShiina commited on
Commit
7fb97e6
·
verified ·
1 Parent(s): 598748a

refactor: unify prompt enhancer chains into a data-driven pipeline

Browse files

Replace the three per-architecture prompt enhancer injectors with a single
registry-driven implementation, and modernize the node it emits.

chain_injectors/__init__.py CHANGED
@@ -42,6 +42,18 @@ def get_registered_features():
42
  full_module_name = f"chain_injectors.{module_name}"
43
  chain_type = f"dynamic_{feature_name}_chains"
44
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  features[feature_name] = {
46
  'module': full_module_name,
47
  'chain_type': chain_type
 
42
  full_module_name = f"chain_injectors.{module_name}"
43
  chain_type = f"dynamic_{feature_name}_chains"
44
 
45
+ # Honour an explicit CHAIN_TYPE the same way discover_injectors() does.
46
+ # Without this, a module that declares CHAIN_TYPE = "foo" is registered
47
+ # under "foo" by discover_injectors() but looked up here as
48
+ # "dynamic_foo_chains", so the assembler silently skips it.
49
+ try:
50
+ module = importlib.import_module(full_module_name)
51
+ declared = getattr(module, 'CHAIN_TYPE', None)
52
+ if declared:
53
+ chain_type = declared
54
+ except Exception:
55
+ pass
56
+
57
  features[feature_name] = {
58
  'module': full_module_name,
59
  'chain_type': chain_type
chain_injectors/flux3_prompt_upsampler_injector.py DELETED
@@ -1,304 +0,0 @@
1
- """FLUX.3 Prompt Upsampler Chain Injector.
2
-
3
- This injector integrates the FLUX.3 prompt upsampler model into the generation workflow:
4
- - Uses qwen3.5_9b_flux3_prompt_upsampler_w4a8.safetensors (type: flux3).
5
- - Uses PrimitiveStringMultiline for system prompt and input prompt.
6
- - Expands user prompt into detailed generative captions using TextGenerate.
7
- - Supports reasoning mode (thinking=True) via native ComfyUI TextGenerate reasoning separation.
8
- - Supports single or multi-reference image inputs: multiple reference images are stitched
9
- into a structured grid layout via ComfyUI native ImageStitch nodes and scaled via ImageScaleToTotalPixels,
10
- then connected to the TextGenerate 'image' input.
11
- - Connects enhanced prompt directly to downstream CLIPTextEncode positive prompt.
12
- """
13
-
14
- try:
15
- from utils.app_utils import ensure_file_downloaded
16
- except ImportError:
17
- ensure_file_downloaded = None
18
-
19
- SYSTEM_PROMPTS = {
20
- "t2i": (
21
- "You are the prompt upsampler of a text-to-image model. Expand the user's short request into ONE detailed, self-contained caption: the exact text the image model receives. Output only the caption, with no greetings, explanations, refusals, questions, Markdown, headings, JSON, coordinates, ids or angle-bracket tokens, and no quotes around it. If the input is not clearly an image request, caption a visualization of its concept. The request may begin with a \"TARGET CANVAS: W:H\" line: compose for that canvas (wide spreads content sideways, tall stacks it) and never mention the ratio or the canvas.\n\n"
22
- "Fidelity and planning: keep every explicit subject, attribute, count, spatial constraint, style cue and piece of wording; never contradict, substitute, drop or narrow the request, and keep an abstract concept abstract. Decide everything left open as ONE concrete, consistent plan; never offer alternatives or hedge. Never re-express a stated constraint through another mechanism; add camera optics (depth of field, bokeh, blur) only when asked for or implied. Without a stated style, plan an actual photograph and open with the medium convention (\"Full-color outdoor photograph, ...\", \"Full-color indoor photograph, ...\", \"Studio photograph, ...\", \"Close-up photograph, ...\"); keep \"Photorealistic\" for renders, and turn \"photorealistic\" or \"realistic\" requests for photos into the photograph convention. Give a photograph a few ordinary, non-ideal specifics drawn from the scene's own objects instead of the stock-photo ideal. Do not invent names, brands, logos or celebrities; keep user-named ones. A long request keeps its content but not its wording: drop appraisal and sales vocabulary, turn moods into observable physical facts, and add only new concrete content with positions. Time of day is lighting, not rendered text.\n\n"
23
- "Caption format: one continuous prose block in the generative voice, never \"this image shows\", \"we can see\" or \"is visible\". Lead with medium and style, then scene, lighting and camera, then the subjects with concrete colors, materials and state. Every person in focus gets an observable profile: ethnicity, gender, age range, skin tone (\"fair skin\", \"brown skin\"), hair and clothing. Make the layout reconstructable from words: frame-relative positions (frame-left, frame-right, upper, lower, foreground, midground, background), sentence-initial anchors (\"On the left, ...\", \"In the background, ...\"), relations (holds, faces, behind) and a consistent order; describe up to about ten instances one by one, beyond that give the exact count and arrangement. Out-of-focus or distant planes are shapes, colors and silhouettes, never identities or readable text, and the background takes about a sixth of the caption. Be definite: no hedging adverbs, no quality or emotion judgments, no closing sentence about mood; state negations as what is present. Rendered text: transcribe every wording verbatim inside double quotes in its original language and script, keep the line breaks of multi-line text, and give its typography, color and placement; author concrete, domain-correct wording when the request needs text it does not supply, and name canonical structures (calendar, keyboard, periodic table) exactly with their full layout. All prose is English; only quoted text keeps its language. Typical length is 100 to 350 words, more for complex scenes.\n\n"
24
- "Output only the caption."
25
- ),
26
- "i2i": (
27
- "You are the prompt upsampler of an image-editing model. You see the reference image(s), attached in order (the first image, the second image, ...), and the user's short request. Expand it into ONE detailed, self-contained instruction: the caption the editing model receives together with the same images. Output only the instruction as one continuous English prose block, with no greetings, explanations, Markdown, JSON, coordinates, ids or angle-bracket tokens. Read a vague or conversational request as the most plausible edit of the supplied images.\n\n"
28
- "Pick the voice by task shape. When one reference stays and gets bounded changes, write an imperative editing instruction on it (\"Remove the ...\", \"Change the ... to ...\", \"Zoom in on ...\"); a whole-image transformation opens with its scope (\"Convert the entire image into ...\", \"Apply a ... filter over the entire scene\", \"Horizontally flip the entire image so that ...\", \"Shift the camera viewpoint to ...\", then where the major elements should now be). When the references are drawn into a newly composed frame (combining subjects, a new scene, a garment, object or style donor), write a generation request (\"Create a photograph of ...\", \"Create an anime-style illustration of ...\", \"Generate a new image featuring ...\") that says what to take from each image, by ordinal, and what is newly generated.\n\n"
29
- "Keep every explicit edit, constraint, count, position, style cue and piece of wording, and decide what is left open as one concrete plan. Ground everything in what is actually visible: identify each referent unambiguously by its real appearance, never mention content that is not there, and use every image the user attached. State every end state concretely (which color, how large, which pose, which material) and give new content enough appearance to be generated. Cover the knock-on effects of the change (shadows, reflections, occlusion, perspective, scale, lighting) and say what fills a removed region. A zoom-in names what stays in frame and which neighbors are cropped out; an outpainting continues the scene in the same medium. Describe the change, not the whole scene, and close an editing instruction with one compact clause naming what stays unchanged; transferred people keep their identity, face and accessories unless asked otherwise. Transcribe on-image text verbatim in its original language inside double quotes, with line breaks as \\n, and match the original font, size and color when replacing it. Name recognizable public figures, brands and landmarks you can see. Directions are frame-relative; use no evaluative or quality words. Typical length is 50 to 150 words.\n\n"
30
- "Output only the instruction."
31
- ),
32
- "t2i_bbox": (
33
- "You are the prompt upsampler of a text-to-image model. Plan the full image the user's short request implies and output ONE structured caption on a single line: the global caption paragraph, one space, then ONE JSON array, the element table that places every cited element with a bounding box in the image to be generated. Output nothing else (no Markdown fences, commentary or refusals); visualize the concept of a non-image input. The request may begin with a \"TARGET CANVAS: W:H\" line: compose for that canvas and never mention it. Boxes live on a 0-1000 grid stretched onto the canvas: an element of physical width-to-height ratio a gets an x-span of about y-span x a x H/W.\n\n"
34
- "Planning: keep every explicit subject, attribute, count, spatial constraint, style cue and piece of wording, and decide everything left open as ONE concrete composition that the caption and the table both describe. Without a stated style plan an actual photograph and open with the medium convention (\"Full-color outdoor photograph, ...\", \"Studio photograph, ...\"); add camera optics only when asked for; do not invent names, brands, logos or celebrities; drop appraisal and sales vocabulary.\n\nThe table: one row per distinct object, person, animal, building or salient region the caption mentions and one per coherent text block; no rows for style, lighting or atmosphere. Structured layouts (tables, grids, calendars, keyboards, charts, maps, posters, pages) decompose into their regions, each row carrying that region's complete verbatim content; more than about ten interchangeable items become one collection row giving the exact count and arrangement, while items whose positions matter keep a row each. Row keys, in order: \"id\", \"bbox\", \"desc\". id: the bare class noun plus an index numbered left to right, then top to bottom, from 1 (person_1, dog_2); a text block is its two-letter language code plus _Text_ plus an index (En_Text_1); one entity is one row and ids are unique. bbox: [y_min, x_min, y_max, x_max] as integers from 0 to 1000, top-left origin, clamped at the borders, with plausible relative sizes and a small margin around text. desc: one self-contained paragraph in the generative voice that opens with the element's identity (never \"the X\"): shape, material, colors, state and markings, but no position. People in focus get the richest desc: ethnicity, gender, age range, skin tone, build, hair, face, each garment, accessories, pose and expression. A text row carries its complete wording verbatim in double quotes, with line breaks as \\n, plus its typography.\n\nThe global caption: one flowing paragraph in the generative voice (medium first, then scene, lighting, camera and frame-relative layout; no quality judgments) that names each table element at identity level at its first mention and cites it with its id in its own angle brackets: \"a wicker table <table_1> holding a tiered stand <stand_1>\". Cite every table id and no other; appearance detail and text content live in the descs; no coordinates. Text-dense documents are laid out region by region with every visible text transcribed verbatim at a readable size (cut content rather than shrink type), and a grid quotes every visible cell. All prose is English; only quoted text keeps its language.\n\nOutput only the caption and the table."
35
- ),
36
- "i2i_bbox": (
37
- "You are the annotation system an image-editing model was trained on. You see the source image(s), attached in order as ref_image_0, ref_image_1, ..., and the user's editing instruction; the edited target image does not exist yet. Determine the edit precisely, imagine the target consistently with the source, and write the record its annotator would have written, on one line: the instruction paragraph, one space, then ONE JSON array. Output nothing else.\n\nThe paragraph: English, instruction voice (\"Change the ...\", \"Remove the ...\", or \"Create a photograph of ...\" when the references are drawn into a new frame), never observer phrasing and never a comparison with the target. Cite every involved object, person or text block at its first mention with its table id in its own angle brackets (\"remove the delivery van <van_1>\"), every table id exactly once and no other id; cite a reference image by its token when content is taken from it (\"the cup <cup_1> from <ref_image_1>\"). End states (new color, new text in quotes, pose, degree) live in the paragraph, static appearance in the descs; no coordinates. An editing instruction closes with one compact clause that everything else stays identical to the source.\n\nThe table, rows {\"id\", \"from\", \"src_bbox\", \"tgt_bbox\", \"desc\"}: one row per edited, removed, added, moved or anchoring item, plus the scene or background when it is itself changed or preserved; no rows for untouched scenery. id: a lowercase class noun plus an index (person_1, cup_2) numbered left to right, then top to bottom; text blocks are En_Text_1 and so on; ids are unique. from: \"ref_image_N\" for an item visible in reference N, \"target\" for newly created content. src_bbox is the item's box in its from-image and null for target rows; tgt_bbox is its box in the imagined result and null for removed items; moved or resized items carry both; new items are sized like similar objects in the frame. Boxes are [y_min, x_min, y_max, x_max] integers from 0 to 1000, top-left origin, tight around objects, with 5 to 15 units of margin around text. A row may also carry src_bbox_1stage and tgt_bbox_1stage, a first-pass estimate of the same boxes. desc: one English paragraph of the item's static appearance in its from-image (for target rows, its decided appearance), opening with its identity; people get the richest descs (ethnicity, gender, age range, skin tone, hair, face, garments, accessories, pose, expression); text rows quote their text verbatim.\n\nState only decided facts: visible in the source, demanded by the instruction, or a concrete default needed to realize it. Settle whether the edit targets one instance, all of a class or several; land every magnitude on a concrete value; include visible knock-on effects; added objects are new objects at real-world size; a zoom-in names the neighbors it excludes; an outpainting keeps the source whole and continues it in the same medium. Everything the instruction does not involve stays identical to the source.\n\nOutput only the paragraph and the table."
38
- )
39
- }
40
-
41
- DEFAULT_FLUX3_SYSTEM_PROMPT = SYSTEM_PROMPTS["t2i"]
42
-
43
-
44
- def create_node(assembler, class_type, title):
45
- """Create a workflow node dictionary with fallback template generation."""
46
- try:
47
- node = assembler._get_node_template(class_type)
48
- except Exception:
49
- node = {
50
- "inputs": {},
51
- "class_type": class_type,
52
- "_meta": {"title": title}
53
- }
54
- if "_meta" not in node:
55
- node["_meta"] = {}
56
- node['_meta']['title'] = title
57
- return node
58
-
59
-
60
- def inject(assembler, chain_definition, chain_items):
61
- """Inject FLUX.3 prompt upsampler nodes into the target generation workflow."""
62
- if not chain_items:
63
- return
64
-
65
- is_enabled = False
66
- thinking_enabled = False
67
- bbox_mode = False
68
- max_length = 3584
69
- custom_system_prompt = None
70
-
71
- for item in chain_items:
72
- if isinstance(item, dict):
73
- if item.get("enable", False) or item.get("enabled", False) or item.get("value", False):
74
- is_enabled = True
75
- if item.get("thinking", False) in (True, "true", "on", "yes", "1", 1):
76
- thinking_enabled = True
77
- if item.get("bbox_mode", False) in (True, "true", "on", "yes", "1", 1):
78
- bbox_mode = True
79
- if "max_length" in item and item["max_length"]:
80
- try:
81
- max_length = int(item["max_length"])
82
- except (ValueError, TypeError):
83
- pass
84
- if "system_prompt" in item and item["system_prompt"]:
85
- custom_system_prompt = str(item["system_prompt"])
86
- elif item is True or str(item).lower() in ("on", "true", "yes", "1"):
87
- is_enabled = True
88
- break
89
-
90
- if not is_enabled:
91
- return
92
-
93
- target_node_name = chain_definition.get('target_node', 'pos_prompt')
94
- target_node_id = assembler.node_map.get(target_node_name)
95
-
96
- if not target_node_id or target_node_id not in assembler.workflow:
97
- for node_id, node in assembler.workflow.items():
98
- if isinstance(node, dict) and node.get('class_type') == 'CLIPTextEncode':
99
- title = node.get('_meta', {}).get('title', '').lower()
100
- if 'pos' in title or 'positive' in title:
101
- target_node_id = node_id
102
- break
103
-
104
- if not target_node_id or target_node_id not in assembler.workflow:
105
- for node_id, node in assembler.workflow.items():
106
- if isinstance(node, dict) and node.get('class_type') in ('CLIPTextEncode', 'TextEncodeQwenImage21', 'TextEncodeMingImageEdit'):
107
- target_node_id = node_id
108
- break
109
-
110
- if not target_node_id or target_node_id not in assembler.workflow:
111
- print(f"Warning: Target node '{target_node_name}' not found for FLUX.3 Prompt Upsampler. Skipping.")
112
- return
113
-
114
- target_node = assembler.workflow[target_node_id]
115
- original_prompt = target_node.get('inputs', {}).get('text')
116
- if original_prompt is None:
117
- original_prompt = target_node.get('inputs', {}).get('prompt', '')
118
-
119
- # 1. Collect image connections for multi-image reference support
120
- load_image_nodes = [
121
- node_id for node_id, node in assembler.workflow.items()
122
- if isinstance(node, dict) and node.get('class_type') == 'LoadImage'
123
- ]
124
- has_load_image = len(load_image_nodes) > 0
125
- target_image_conn = None
126
-
127
- # Auto-select official BFL system prompt template:
128
- # If loadimage is detected: i2i / i2i_bbox; otherwise t2i / t2i_bbox
129
- if custom_system_prompt:
130
- system_prompt = custom_system_prompt
131
- else:
132
- if has_load_image:
133
- template_key = "i2i_bbox" if bbox_mode else "i2i"
134
- else:
135
- template_key = "t2i_bbox" if bbox_mode else "t2i"
136
- system_prompt = SYSTEM_PROMPTS.get(template_key, DEFAULT_FLUX3_SYSTEM_PROMPT)
137
-
138
- if has_load_image:
139
- indexed_slots = []
140
- target_inputs = target_node.get('inputs', {})
141
- for slot_key, slot_val in target_inputs.items():
142
- if slot_key.startswith('images.image_') and isinstance(slot_val, (list, tuple)):
143
- try:
144
- idx = int(slot_key.split('_')[-1])
145
- except Exception:
146
- idx = 999
147
- indexed_slots.append((idx, slot_val))
148
-
149
- indexed_slots.sort(key=lambda x: x[0])
150
- ref_image_conns = [s[1] for s in indexed_slots]
151
-
152
- if not ref_image_conns:
153
- for lid in load_image_nodes:
154
- scaled_conn = None
155
- for nid, node in assembler.workflow.items():
156
- if isinstance(node, dict) and node.get('class_type') == 'ImageScaleToTotalPixels':
157
- img_input = node.get('inputs', {}).get('image')
158
- if isinstance(img_input, (list, tuple)) and str(img_input[0]) == str(lid):
159
- scaled_conn = [nid, 0]
160
- break
161
- if scaled_conn:
162
- ref_image_conns.append(scaled_conn)
163
- else:
164
- scale_id = assembler._get_unique_id()
165
- scale_node = create_node(assembler, "ImageScaleToTotalPixels", "Scale Input Image")
166
- scale_node['inputs'] = {
167
- "upscale_method": "nearest-exact",
168
- "megapixels": 1,
169
- "resolution_steps": 1,
170
- "image": [lid, 0]
171
- }
172
- assembler.workflow[scale_id] = scale_node
173
- ref_image_conns.append([scale_id, 0])
174
-
175
- if len(ref_image_conns) == 1:
176
- target_image_conn = ref_image_conns[0]
177
- elif len(ref_image_conns) >= 2:
178
- num_cols = 2 if len(ref_image_conns) <= 4 else 3
179
- rows = []
180
- for i in range(0, len(ref_image_conns), num_cols):
181
- rows.append(ref_image_conns[i:i + num_cols])
182
-
183
- stitched_rows = []
184
- for r_idx, row_imgs in enumerate(rows):
185
- if len(row_imgs) == 1:
186
- stitched_rows.append(row_imgs[0])
187
- else:
188
- curr_stitch = row_imgs[0]
189
- for c_idx in range(1, len(row_imgs)):
190
- st_id = assembler._get_unique_id()
191
- st_node = create_node(assembler, "ImageStitch", f"Stitch Images (Row {r_idx+1}-{c_idx})")
192
- st_node['inputs'] = {
193
- "direction": "right",
194
- "match_image_size": True,
195
- "spacing_width": 0,
196
- "spacing_color": "white",
197
- "image1": curr_stitch,
198
- "image2": row_imgs[c_idx]
199
- }
200
- assembler.workflow[st_id] = st_node
201
- curr_stitch = [st_id, 0]
202
- stitched_rows.append(curr_stitch)
203
-
204
- if len(stitched_rows) == 1:
205
- final_stitched = stitched_rows[0]
206
- else:
207
- curr_vertical = stitched_rows[0]
208
- for r_idx in range(1, len(stitched_rows)):
209
- st_id = assembler._get_unique_id()
210
- st_node = create_node(assembler, "ImageStitch", f"Stitch Images (Vertical {r_idx})")
211
- st_node['inputs'] = {
212
- "direction": "down",
213
- "match_image_size": True,
214
- "spacing_width": 0,
215
- "spacing_color": "white",
216
- "image1": curr_vertical,
217
- "image2": stitched_rows[r_idx]
218
- }
219
- assembler.workflow[st_id] = st_node
220
- curr_vertical = [st_id, 0]
221
- final_stitched = curr_vertical
222
-
223
- final_scale_id = assembler._get_unique_id()
224
- final_scale_node = create_node(assembler, "ImageScaleToTotalPixels", "Scale Stitched Image")
225
- final_scale_node['inputs'] = {
226
- "upscale_method": "nearest-exact",
227
- "megapixels": 1,
228
- "resolution_steps": 1,
229
- "image": final_stitched
230
- }
231
- assembler.workflow[final_scale_id] = final_scale_node
232
- target_image_conn = [final_scale_id, 0]
233
-
234
- # 2. Ensure model is downloaded
235
- clip_model_name = "qwen3.5_9b_flux3_prompt_upsampler_w4a8.safetensors"
236
- if ensure_file_downloaded:
237
- try:
238
- ensure_file_downloaded(clip_model_name)
239
- except Exception as e:
240
- print(f"Warning: Failed to ensure '{clip_model_name}' downloaded: {e}")
241
-
242
- # 3. Create CLIPLoader node (type: flux3)
243
- clip_node_id = assembler._get_unique_id()
244
- clip_node = create_node(assembler, "CLIPLoader", "Load CLIP")
245
- clip_node['inputs'] = {
246
- "clip_name": clip_model_name,
247
- "type": "flux3",
248
- "device": "default"
249
- }
250
- assembler.workflow[clip_node_id] = clip_node
251
-
252
- # 4. Create PrimitiveStringMultiline node for System Prompt
253
- system_prompt_node_id = assembler._get_unique_id()
254
- system_prompt_node = create_node(assembler, "PrimitiveStringMultiline", "Text (System prompt)")
255
- system_prompt_node['inputs'] = {
256
- "value": system_prompt
257
- }
258
- assembler.workflow[system_prompt_node_id] = system_prompt_node
259
-
260
- # 5. Create PrimitiveStringMultiline node for User Prompt
261
- if isinstance(original_prompt, (list, tuple)):
262
- prompt_conn = original_prompt
263
- else:
264
- prompt_node_id = assembler._get_unique_id()
265
- prompt_node = create_node(assembler, "PrimitiveStringMultiline", "Text (Multiline)")
266
- prompt_node['inputs'] = {
267
- "value": original_prompt if isinstance(original_prompt, str) else ""
268
- }
269
- assembler.workflow[prompt_node_id] = prompt_node
270
- prompt_conn = [prompt_node_id, 0]
271
-
272
- # 6. Create TextGenerate node
273
- text_gen_node_id = assembler._get_unique_id()
274
- text_gen_node = create_node(assembler, "TextGenerate", "Generate Text")
275
- text_gen_inputs = {
276
- "prompt": prompt_conn,
277
- "max_length": max_length,
278
- "sampling_mode": "on",
279
- "sampling_mode.temperature": 1.0,
280
- "sampling_mode.top_k": 1,
281
- "sampling_mode.top_p": 0.95,
282
- "sampling_mode.min_p": 0.0,
283
- "sampling_mode.repetition_penalty": 1.05,
284
- "sampling_mode.seed": 0,
285
- "sampling_mode.presence_penalty": 1.5,
286
- "thinking": thinking_enabled,
287
- "use_default_template": True,
288
- "mtp": "auto",
289
- "clip": [clip_node_id, 0],
290
- "system_prompt": [system_prompt_node_id, 0]
291
- }
292
- if target_image_conn is not None:
293
- text_gen_inputs["image"] = target_image_conn
294
-
295
- text_gen_node['inputs'] = text_gen_inputs
296
- assembler.workflow[text_gen_node_id] = text_gen_node
297
-
298
- # 7. Connect TextGenerate output to positive prompt node
299
- if 'text' in target_node.get('inputs', {}):
300
- target_node['inputs']['text'] = [text_gen_node_id, 0]
301
- else:
302
- target_node['inputs']['prompt'] = [text_gen_node_id, 0]
303
-
304
- print(f"[Injector] FLUX.3 Prompt Upsampler applied successfully (clip='{clip_model_name}', thinking={thinking_enabled}, images={has_load_image}).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chain_injectors/ming_image_prompt_enhancer_injector.py DELETED
@@ -1,262 +0,0 @@
1
- """Ming-Image Prompt Enhancer Chain Injector.
2
-
3
- This injector automatically integrates the Qwen3.8-27B prompt enhancement model into
4
- the Ming-Image generation workflow:
5
- - Detects whether the task is pure text-to-image (T2I) or involves images (I2I, inpainting,
6
- outpainting, hires.fix, or multi-reference images).
7
- - Uses the Qwen3.8-27B PE text encoder: qwen3.8_27b_w4a8.safetensors (type: qwen_image).
8
- - Handles single or multi-reference inputs: multiple reference images are stitched into
9
- a structured grid layout via ComfyUI native ImageStitch nodes and scaled via ImageScaleToTotalPixels.
10
- - Supports reasoning mode (thinking=True) leveraging ComfyUI TextGenerate native reasoning separation
11
- (output slot 0 outputs clean prompt text, while output slot 1 isolates reasoning chain).
12
- """
13
-
14
- try:
15
- from utils.app_utils import ensure_file_downloaded
16
- except ImportError:
17
- ensure_file_downloaded = None
18
-
19
-
20
- DEFAULT_MING_IMAGE_SYSTEM_PROMPT = (
21
- "You are a senior visual designer and image-prompt engineer. Expand the user's request into one precise, high-resolution Figma-style caption. Return only one JSON object. "
22
- "Use exactly two top-level keys. `canvas_settings` contains exactly `aspect_ratio`, `ambient_lighting`, and `image_style`. `layers` lists visible groups from background to topmost overlay. Every layer contains exactly `description`, `coordinates`, `hierarchy_and_relation`, and `color_specs`; `color_specs` is an array of hex colors. "
23
- '`coordinates` MUST be one string, never an object or array, in exactly this form: `"cx: 0.500, cy: 0.500, w: 1.000, h: 1.000"`. Values are normalized; each bbox encloses its complete owned object and stays inside the canvas. '
24
- "A layer is one selectable visible semantic group: background, full person, coherent object, panel, card, row, or text block. Prefer the fewest groups that preserve the layout. Keep people and objects intact. Never create invisible parents, guides, placeholders, empty layers, duplicate summaries, or multiple owners for one element. "
25
- "Preserve every user-supplied rendered string character-for-character and as one contiguous string. Unless multiple visible copies are requested, it must occur exactly once across all `description` fields and zero times in `hierarchy_and_relation`. Quote it only where describing its visible rendering; refer to the related subject elsewhere with unquoted semantic wording. Enumerate intended copy, invent extra copy sparingly, and never hide content behind \"other text\", \"remaining labels\", or \"etc.\" "
26
- "Describe concrete composition, typography, materials, texture, lighting, pose, and camera treatment without literary filler. Use `hierarchy_and_relation` only for ownership, alignment, containment, stacking, and occlusion. "
27
- "Infer structured layouts first. Use one complete layer per card and state its row and column. A compact secondary table may be one layer only if every header and cell is listed; otherwise use a visible shared frame when present, one complete header, and one complete layer per body row, binding values to columns and stating blanks. Enumerate sequences, schedules, spans, gaps, and vacant tracks in visual order. Do not mistake ordinary alignment for a table. "
28
- "Silently verify schema, string coordinates, Z-order, exact-text counts, geometry, bbox validity, and completeness."
29
- )
30
-
31
-
32
- def create_node(assembler, class_type, title):
33
- """Create a workflow node dictionary with fallback template generation."""
34
- try:
35
- node = assembler._get_node_template(class_type)
36
- except Exception:
37
- node = {
38
- "inputs": {},
39
- "class_type": class_type,
40
- "_meta": {"title": title}
41
- }
42
- if "_meta" not in node:
43
- node["_meta"] = {}
44
- node['_meta']['title'] = title
45
- return node
46
-
47
-
48
- def inject(assembler, chain_definition, chain_items):
49
- """Inject Ming-Image prompt enhancer nodes into the target generation workflow."""
50
- if not chain_items:
51
- return
52
-
53
- is_enabled = False
54
- thinking_enabled = False
55
- max_length = 4096
56
- system_prompt = DEFAULT_MING_IMAGE_SYSTEM_PROMPT
57
- for item in chain_items:
58
- if isinstance(item, dict):
59
- if item.get("enable", False) or item.get("enabled", False) or item.get("value", False):
60
- is_enabled = True
61
- if item.get("thinking", False) in (True, "true", "on", "yes", "1", 1):
62
- thinking_enabled = True
63
- if "max_length" in item and item["max_length"]:
64
- try:
65
- max_length = int(item["max_length"])
66
- except (ValueError, TypeError):
67
- pass
68
- if "system_prompt" in item and item["system_prompt"]:
69
- system_prompt = str(item["system_prompt"])
70
- elif item is True or str(item).lower() in ("on", "true", "yes", "1"):
71
- is_enabled = True
72
- break
73
-
74
- if not is_enabled:
75
- return
76
-
77
- target_node_name = chain_definition.get('target_node', 'prompt')
78
- target_node_id = assembler.node_map.get(target_node_name)
79
-
80
- if not target_node_id or target_node_id not in assembler.workflow:
81
- for node_id, node in assembler.workflow.items():
82
- if isinstance(node, dict) and node.get('class_type') == 'TextEncodeMingImageEdit':
83
- target_node_id = node_id
84
- break
85
-
86
- if not target_node_id or target_node_id not in assembler.workflow:
87
- print(f"Warning: Target node '{target_node_name}' (TextEncodeMingImageEdit) not found for Ming-Image Prompt Enhancer. Skipping.")
88
- return
89
-
90
- target_node = assembler.workflow[target_node_id]
91
- original_prompt = target_node.get('inputs', {}).get('prompt', '')
92
-
93
- # 1. Check if any LoadImage nodes exist in the workflow
94
- load_image_nodes = [
95
- node_id for node_id, node in assembler.workflow.items()
96
- if isinstance(node, dict) and node.get('class_type') == 'LoadImage'
97
- ]
98
- has_load_image = len(load_image_nodes) > 0
99
-
100
- target_image_conn = None
101
- clip_model_name = "qwen3.8_27b_w4a8.safetensors"
102
-
103
- if has_load_image:
104
- # Collect image connections
105
- # Prioritize extracting all images.image_* slots from target_node['inputs'] (injected by reference image injectors)
106
- indexed_slots = []
107
- target_inputs = target_node.get('inputs', {})
108
- for slot_key, slot_val in target_inputs.items():
109
- if slot_key.startswith('images.image_') and isinstance(slot_val, (list, tuple)):
110
- try:
111
- idx = int(slot_key.split('_')[-1])
112
- except Exception:
113
- idx = 999
114
- indexed_slots.append((idx, slot_val))
115
-
116
- indexed_slots.sort(key=lambda x: x[0])
117
- ref_image_conns = [s[1] for s in indexed_slots]
118
-
119
- # If no reference image slots exist (e.g. pure img2img / inpaint / outpaint), retrieve from LoadImage nodes
120
- if not ref_image_conns:
121
- for lid in load_image_nodes:
122
- scaled_conn = None
123
- for nid, node in assembler.workflow.items():
124
- if isinstance(node, dict) and node.get('class_type') == 'ImageScaleToTotalPixels':
125
- img_input = node.get('inputs', {}).get('image')
126
- if isinstance(img_input, (list, tuple)) and str(img_input[0]) == str(lid):
127
- scaled_conn = [nid, 0]
128
- break
129
- if scaled_conn:
130
- ref_image_conns.append(scaled_conn)
131
- else:
132
- scale_id = assembler._get_unique_id()
133
- scale_node = create_node(assembler, "ImageScaleToTotalPixels", "Scale Input Image")
134
- scale_node['inputs'] = {
135
- "upscale_method": "nearest-exact",
136
- "megapixels": 1,
137
- "resolution_steps": 1,
138
- "image": [lid, 0]
139
- }
140
- assembler.workflow[scale_id] = scale_node
141
- ref_image_conns.append([scale_id, 0])
142
-
143
- # Organize multiple reference images into grid stitching
144
- if len(ref_image_conns) == 1:
145
- target_image_conn = ref_image_conns[0]
146
- elif len(ref_image_conns) >= 2:
147
- num_cols = 2 if len(ref_image_conns) <= 4 else 3
148
-
149
- rows = []
150
- for i in range(0, len(ref_image_conns), num_cols):
151
- rows.append(ref_image_conns[i:i + num_cols])
152
-
153
- # Horizontally stitch images in each row
154
- stitched_rows = []
155
- for r_idx, row_imgs in enumerate(rows):
156
- if len(row_imgs) == 1:
157
- stitched_rows.append(row_imgs[0])
158
- else:
159
- curr_stitch = row_imgs[0]
160
- for c_idx in range(1, len(row_imgs)):
161
- st_id = assembler._get_unique_id()
162
- st_node = create_node(assembler, "ImageStitch", f"Stitch Images (Row {r_idx+1}-{c_idx})")
163
- st_node['inputs'] = {
164
- "direction": "right",
165
- "match_image_size": True,
166
- "spacing_width": 0,
167
- "spacing_color": "white",
168
- "image1": curr_stitch,
169
- "image2": row_imgs[c_idx]
170
- }
171
- assembler.workflow[st_id] = st_node
172
- curr_stitch = [st_id, 0]
173
- stitched_rows.append(curr_stitch)
174
-
175
- # Vertically stitch multiple rows
176
- if len(stitched_rows) == 1:
177
- final_stitched = stitched_rows[0]
178
- else:
179
- curr_vertical = stitched_rows[0]
180
- for r_idx in range(1, len(stitched_rows)):
181
- st_id = assembler._get_unique_id()
182
- st_node = create_node(assembler, "ImageStitch", f"Stitch Images (Vertical {r_idx})")
183
- st_node['inputs'] = {
184
- "direction": "down",
185
- "match_image_size": True,
186
- "spacing_width": 0,
187
- "spacing_color": "white",
188
- "image1": curr_vertical,
189
- "image2": stitched_rows[r_idx]
190
- }
191
- assembler.workflow[st_id] = st_node
192
- curr_vertical = [st_id, 0]
193
- final_stitched = curr_vertical
194
-
195
- # Scale final stitched image to ~1024 level via ImageScaleToTotalPixels
196
- final_scale_id = assembler._get_unique_id()
197
- final_scale_node = create_node(assembler, "ImageScaleToTotalPixels", "Scale Stitched Image")
198
- final_scale_node['inputs'] = {
199
- "upscale_method": "nearest-exact",
200
- "megapixels": 1,
201
- "resolution_steps": 1,
202
- "image": final_stitched
203
- }
204
- assembler.workflow[final_scale_id] = final_scale_node
205
- target_image_conn = [final_scale_id, 0]
206
-
207
- # 2. Ensure model is downloaded in CPU stage before workflow execution
208
- if ensure_file_downloaded:
209
- try:
210
- ensure_file_downloaded(clip_model_name)
211
- except Exception as e:
212
- print(f"Warning: Failed to ensure '{clip_model_name}' downloaded: {e}")
213
-
214
- # 3. Create CLIPLoader node (using qwen_image type for Qwen3.8-27B PE)
215
- clip_node_id = assembler._get_unique_id()
216
- clip_node = create_node(assembler, "CLIPLoader", "Load CLIP")
217
- clip_node['inputs'] = {
218
- "clip_name": clip_model_name,
219
- "type": "qwen_image",
220
- "device": "default"
221
- }
222
- assembler.workflow[clip_node_id] = clip_node
223
-
224
- # 4. Create PrimitiveString node for System Prompt (Text node)
225
- system_prompt_node_id = assembler._get_unique_id()
226
- system_prompt_node = create_node(assembler, "PrimitiveString", "Text")
227
- system_prompt_node['inputs'] = {
228
- "value": system_prompt
229
- }
230
- assembler.workflow[system_prompt_node_id] = system_prompt_node
231
-
232
- # 5. Create TextGenerate node
233
- text_gen_node_id = assembler._get_unique_id()
234
- text_gen_node = create_node(assembler, "TextGenerate", "Generate Text")
235
- text_gen_inputs = {
236
- "prompt": original_prompt,
237
- "max_length": max_length,
238
- "sampling_mode": "on",
239
- "sampling_mode.temperature": 0.7,
240
- "sampling_mode.top_k": 64,
241
- "sampling_mode.top_p": 0.95,
242
- "sampling_mode.min_p": 0.05,
243
- "sampling_mode.repetition_penalty": 1.05,
244
- "sampling_mode.seed": 0,
245
- "sampling_mode.presence_penalty": 0,
246
- "thinking": thinking_enabled,
247
- "use_default_template": True,
248
- "mtp": "auto",
249
- "clip": [clip_node_id, 0],
250
- "system_prompt": [system_prompt_node_id, 0]
251
- }
252
- if target_image_conn is not None:
253
- text_gen_inputs["image"] = target_image_conn
254
-
255
- text_gen_node['inputs'] = text_gen_inputs
256
- assembler.workflow[text_gen_node_id] = text_gen_node
257
-
258
- # 6. Connect TextGenerate output directly to target node prompt
259
- # ComfyUI TextGenerate node natively strips reasoning tags (<think>...</think>) into output slot 1,
260
- # leaving clean enhanced prompt in output slot 0.
261
- target_node['inputs']['prompt'] = [text_gen_node_id, 0]
262
- print(f"[Injector] Ming-Image Prompt Enhancer applied successfully ({'I2I' if has_load_image else 'T2I'}, clip='{clip_model_name}', thinking={thinking_enabled}).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chain_injectors/prompt_enhancer_injector.py ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prompt Enhancer Chain Injector (data-driven).
2
+
3
+ Merges the former qwen_image_2_1_prompt_enhancer / ming_image_prompt_enhancer /
4
+ flux3_prompt_upsampler injectors into a single implementation driven by
5
+ ``yaml/prompt_enhancer.yaml``.
6
+
7
+ The YAML declares three independent modules, named after their UI labels in the
8
+ "Prompt Enhancer Settings" accordion:
9
+
10
+ * ``architecture`` Model framework -> default ``clip`` (Prompt Enhancer Model) and
11
+ default ``system_prompt`` (System Prompt). These are the dropdown defaults
12
+ applied whenever the base model changes. A missing entry falls back to the
13
+ first registry item.
14
+ * ``clip`` Every name shown in the "Prompt Enhancer Model" dropdown,
15
+ mapped to its checkpoint ``filename`` and ``clip_type``. Declaration order is
16
+ dropdown order.
17
+ * ``system_prompt`` Every name shown in the "System Prompt" dropdown, mapped to
18
+ the system prompt sent to ``TextGenerateV2``. An empty value means no system
19
+ prompt.
20
+
21
+ The modules are fully decoupled: any framework may pick any clip, and any clip
22
+ may be paired with any system prompt. ``clip_type`` therefore travels with the
23
+ clip, not with the framework. There is no image-based or bbox auto-detection.
24
+
25
+ The injector then:
26
+
27
+ 1. Resolves the target text-encode node that receives the enhanced prompt.
28
+ 2. Collects reference images (native ``images.image_N`` slots first, otherwise
29
+ ``LoadImage`` nodes) and feeds each one into its own autogrow slot.
30
+ 3. Loads the selected checkpoint through ``CLIPLoader`` and, when the chosen
31
+ system prompt carries text, materialises it as a primitive string node.
32
+ 4. Emits a ``TextGenerateV2`` node (Sampling Mode ``off``) and wires its clean
33
+ ``generated_text`` output into the downstream positive prompt.
34
+ """
35
+
36
+ import os
37
+
38
+ import yaml
39
+
40
+ try:
41
+ from utils.app_utils import ensure_file_downloaded
42
+ except ImportError:
43
+ ensure_file_downloaded = None
44
+
45
+
46
+ CHAIN_TYPE = "prompt_enhancer"
47
+
48
+ _YAML_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "yaml")
49
+ _CONFIG_PATH = os.path.join(_YAML_DIR, "prompt_enhancer.yaml")
50
+
51
+ # Node class -> (target node names to try, input keys holding the source prompt)
52
+ _TARGET_HINTS = (
53
+ ("TextEncodeQwenImage21", ("prompt",), ("prompt",)),
54
+ ("TextEncodeMingImageEdit", ("prompt",), ("prompt",)),
55
+ ("CLIPTextEncode", ("pos_prompt",), ("text", "prompt")),
56
+ )
57
+
58
+ _config_cache = None
59
+
60
+
61
+ def load_prompt_enhancer_config():
62
+ """Load and cache the prompt enhancer registry."""
63
+ global _config_cache
64
+ if _config_cache is None:
65
+ try:
66
+ with open(_CONFIG_PATH, "r", encoding="utf-8") as f:
67
+ _config_cache = yaml.safe_load(f) or {}
68
+ except (OSError, yaml.YAMLError) as e:
69
+ print(f"Warning: Failed to load prompt enhancer config '{_CONFIG_PATH}': {e}")
70
+ _config_cache = {}
71
+ return _config_cache
72
+
73
+
74
+ def get_clip_registry():
75
+ """Return the ordered ``clip name -> {filename, clip_type}`` registry."""
76
+ return load_prompt_enhancer_config().get("clip", {}) or {}
77
+
78
+
79
+ def get_system_prompt_registry():
80
+ """Return the ``system_prompt name -> text`` map (``""`` = no prompt)."""
81
+ return load_prompt_enhancer_config().get("system_prompt", {}) or {}
82
+
83
+
84
+ def get_architectures():
85
+ """Return the ``model framework -> {clip, system_prompt}`` defaults map."""
86
+ return load_prompt_enhancer_config().get("architecture", {}) or {}
87
+
88
+
89
+ def get_clip_names():
90
+ """Return the selectable clip names, in declared order."""
91
+ return [str(name) for name in get_clip_registry().keys()]
92
+
93
+
94
+ def get_clip_entry(name):
95
+ """Return the ``{filename, clip_type}`` registry entry for a clip name."""
96
+ entry = get_clip_registry().get(str(name))
97
+ return entry if isinstance(entry, dict) else {}
98
+
99
+
100
+ def get_system_prompt_names():
101
+ """Return the selectable system_prompt names, including empty (no-prompt) ones."""
102
+ return [str(name) for name in get_system_prompt_registry().keys()]
103
+
104
+
105
+ def get_all_system_prompt_keys():
106
+ """Return the system_prompt names that carry text (empty ones are dropped)."""
107
+ return [name for name, text in get_system_prompt_registry().items() if text]
108
+
109
+
110
+ def get_architecture_defaults(framework=None):
111
+ """Return ``{model, system_prompt}`` defaults for a model framework.
112
+
113
+ Reads the ``architecture`` module. Falls back to the first registry entry
114
+ when the framework is unknown or omits a field, so the UI always has a
115
+ usable default.
116
+ """
117
+ defaults = get_architectures().get(str(framework), {}) if framework else {}
118
+ if not isinstance(defaults, dict):
119
+ defaults = {}
120
+
121
+ clip_names = get_clip_names()
122
+ prompt_names = get_system_prompt_names()
123
+
124
+ model = defaults.get("clip")
125
+ if model not in clip_names:
126
+ model = clip_names[0] if clip_names else None
127
+
128
+ system_prompt = defaults.get("system_prompt")
129
+ if system_prompt not in prompt_names:
130
+ system_prompt = prompt_names[0] if prompt_names else None
131
+
132
+ return {"model": model, "system_prompt": system_prompt}
133
+
134
+
135
+ def create_node(assembler, class_type, title):
136
+ """Create a workflow node dictionary with fallback template generation."""
137
+ try:
138
+ node = assembler._get_node_template(class_type)
139
+ except Exception:
140
+ node = {
141
+ "inputs": {},
142
+ "class_type": class_type,
143
+ "_meta": {"title": title}
144
+ }
145
+ if "_meta" not in node:
146
+ node["_meta"] = {}
147
+ node['_meta']['title'] = title
148
+ return node
149
+
150
+
151
+ def _as_bool(value, default=False):
152
+ if isinstance(value, bool):
153
+ return value
154
+ if value is None:
155
+ return default
156
+ return str(value).strip().lower() in ("on", "true", "yes", "1")
157
+
158
+
159
+ def _parse_chain_items(chain_items):
160
+ """Normalise the chain items into ``{enable, thinking, model, system_prompt, max_length}``."""
161
+ params = {
162
+ "enable": False,
163
+ "thinking": False,
164
+ "model": None,
165
+ "system_prompt": None,
166
+ "max_length": None,
167
+ }
168
+ if isinstance(chain_items, dict):
169
+ chain_items = [chain_items]
170
+
171
+ for item in chain_items or []:
172
+ if isinstance(item, dict):
173
+ if item.get("enable", False) or item.get("enabled", False) or item.get("value", False):
174
+ params["enable"] = True
175
+ params["thinking"] = _as_bool(item.get("thinking"), False)
176
+ if item.get("model"):
177
+ params["model"] = str(item["model"])
178
+ if item.get("system_prompt"):
179
+ params["system_prompt"] = str(item["system_prompt"])
180
+ if item.get("max_length"):
181
+ try:
182
+ params["max_length"] = int(item["max_length"])
183
+ except (ValueError, TypeError):
184
+ pass
185
+ elif item is True or str(item).strip().lower() in ("on", "true", "yes", "1"):
186
+ params["enable"] = True
187
+ break
188
+
189
+ return params
190
+
191
+
192
+ def _infer_clip_type(clip_filename):
193
+ """Best-effort ``CLIPLoader`` type when ``clip_type`` is missing from the YAML.
194
+
195
+ A wrong type silently loads the checkpoint into the wrong text encoder, so the
196
+ filename is inspected before falling back to the most common type.
197
+ """
198
+ name = str(clip_filename or "").lower()
199
+ if "flux3" in name:
200
+ print(f"Warning: 'clip_type' missing for '{clip_filename}'; inferred 'flux3' from the filename.")
201
+ return "flux3"
202
+ if "flux2" in name:
203
+ print(f"Warning: 'clip_type' missing for '{clip_filename}'; inferred 'flux2' from the filename.")
204
+ return "flux2"
205
+ print(f"Warning: 'clip_type' missing for '{clip_filename}'; defaulting to 'qwen_image'. Add it to prompt_enhancer.yaml.")
206
+ return "qwen_image"
207
+
208
+
209
+ def _resolve_model(params, chain_definition=None):
210
+ """Pick the Prompt Enhancer Model: explicit request, then framework default, then first registered."""
211
+ available = get_clip_names()
212
+ if not available:
213
+ return None
214
+
215
+ requested = params.get("model")
216
+ if not requested:
217
+ defaults = get_architecture_defaults((chain_definition or {}).get("framework"))
218
+ requested = defaults["model"]
219
+
220
+ if requested:
221
+ requested = str(requested)
222
+ if requested in available:
223
+ return requested
224
+ # Tolerate the checkpoint filename being supplied instead of the name.
225
+ for name in available:
226
+ if str(get_clip_entry(name).get("filename") or "") == requested:
227
+ return name
228
+ # Unknown name: fall through to the framework default.
229
+
230
+ defaults = get_architecture_defaults((chain_definition or {}).get("framework"))
231
+ return defaults["model"] if defaults["model"] in available else available[0]
232
+
233
+
234
+ def _select_system_prompt(requested_system_prompt, framework=None):
235
+ """Resolve the system prompt text from the global system_prompt registry."""
236
+ prompts = get_system_prompt_registry()
237
+ if not prompts:
238
+ return None
239
+
240
+ # An explicit UI choice always wins, including an empty prompt (no prompt).
241
+ if requested_system_prompt and str(requested_system_prompt) in prompts:
242
+ return prompts[str(requested_system_prompt)] or None
243
+
244
+ # Otherwise use the framework default, falling back to any non-empty prompt.
245
+ default_prompt = get_architecture_defaults(framework)["system_prompt"]
246
+ if default_prompt and str(default_prompt) in prompts:
247
+ return prompts[str(default_prompt)] or None
248
+
249
+ for text in prompts.values():
250
+ if text:
251
+ return text
252
+ return None
253
+
254
+
255
+ def _find_target_node(assembler, chain_definition=None):
256
+ """Locate the text-encode node that should receive the enhanced prompt."""
257
+ target_node_name = (chain_definition or {}).get("target_node")
258
+ target_node_id = assembler.node_map.get(target_node_name) if target_node_name else None
259
+ if target_node_id and target_node_id in assembler.workflow:
260
+ return target_node_id, None
261
+
262
+ for class_type, node_names, input_keys in _TARGET_HINTS:
263
+ return_class = (class_type, input_keys)
264
+ for node_name in node_names:
265
+ node_id = assembler.node_map.get(node_name)
266
+ if node_id and assembler.workflow.get(node_id, {}).get('class_type') == class_type:
267
+ return node_id, return_class
268
+
269
+ # Prefer a positive-prompt encode node when the class_type is shared.
270
+ for node_id, node in assembler.workflow.items():
271
+ if not isinstance(node, dict) or node.get('class_type') != class_type:
272
+ continue
273
+ title = str(node.get('_meta', {}).get('title', '')).lower()
274
+ if 'pos' in title or 'positive' in title:
275
+ return node_id, return_class
276
+
277
+ for node_id, node in assembler.workflow.items():
278
+ if isinstance(node, dict) and node.get('class_type') == class_type:
279
+ return node_id, return_class
280
+
281
+ return None, None
282
+
283
+
284
+ def _get_autogrow_slot_names(class_type, input_name='images'):
285
+ """Resolve the per-image autogrow slot names for ``class_type.input_name``.
286
+
287
+ ComfyUI exposes two flavours of Autogrow template:
288
+
289
+ * ``TemplateNames(names=["image1", ...])`` -> explicit names
290
+ * ``TemplatePrefix(prefix="image", max=N)`` -> ``prefix + range(max)``
291
+
292
+ Returns ``None`` when the node schema cannot be introspected.
293
+ """
294
+ try:
295
+ from comfy_integration.nodes import NODE_CLASS_MAPPINGS
296
+ node_class = NODE_CLASS_MAPPINGS.get(class_type)
297
+ except Exception:
298
+ node_class = None
299
+
300
+ if node_class is not None:
301
+ try:
302
+ input_types = node_class.INPUT_TYPES() or {}
303
+ except Exception:
304
+ input_types = {}
305
+ for category in ('required', 'optional'):
306
+ spec = (input_types.get(category) or {}).get(input_name)
307
+ if not spec or len(spec) < 2 or not isinstance(spec[1], dict):
308
+ continue
309
+ template = spec[1].get('template')
310
+ if not isinstance(template, dict):
311
+ continue
312
+ names = template.get('names')
313
+ if isinstance(names, (list, tuple)) and names:
314
+ return [str(n) for n in names]
315
+ prefix = template.get('prefix')
316
+ if isinstance(prefix, str):
317
+ max_count = template.get('max', 100)
318
+ if not isinstance(max_count, int) or max_count <= 0:
319
+ max_count = 100
320
+ return [f"{prefix}{i}" for i in range(max_count)]
321
+
322
+ # Fall back to the published TextGenerateV2 contract.
323
+ if class_type == "TextGenerateV2":
324
+ return [f"image{i}" for i in range(1, 51)]
325
+ return None
326
+
327
+
328
+ def _collect_reference_images(assembler, target_node):
329
+ """Collect the reference image connections feeding the prompt node.
330
+
331
+ Native autogrow slots on the target node (``images.image_N``) win; otherwise
332
+ every ``LoadImage`` node is scaled and used. Each image keeps its own
333
+ connection so the caller can wire them into separate autogrow slots.
334
+ """
335
+ load_image_nodes = [
336
+ node_id for node_id, node in assembler.workflow.items()
337
+ if isinstance(node, dict) and node.get('class_type') == 'LoadImage'
338
+ ]
339
+
340
+ indexed_slots = []
341
+ for slot_key, slot_val in target_node.get('inputs', {}).items():
342
+ if slot_key.startswith('images.image_') and isinstance(slot_val, (list, tuple)):
343
+ try:
344
+ idx = int(slot_key.split('_')[-1])
345
+ except (ValueError, IndexError):
346
+ idx = 999
347
+ indexed_slots.append((idx, slot_val))
348
+
349
+ indexed_slots.sort(key=lambda x: x[0])
350
+ if indexed_slots:
351
+ return [slot[1] for slot in indexed_slots]
352
+
353
+ if not load_image_nodes:
354
+ return []
355
+
356
+ scaled_conns = []
357
+ for load_id in load_image_nodes:
358
+ scaled_conn = None
359
+ for node_id, node in assembler.workflow.items():
360
+ if isinstance(node, dict) and node.get('class_type') == 'ImageScaleToTotalPixels':
361
+ img_input = node.get('inputs', {}).get('image')
362
+ if isinstance(img_input, (list, tuple)) and str(img_input[0]) == str(load_id):
363
+ scaled_conn = [node_id, 0]
364
+ break
365
+ if scaled_conn is None:
366
+ scale_id = assembler._get_unique_id()
367
+ scale_node = create_node(assembler, "ImageScaleToTotalPixels", "Scale Input Image")
368
+ scale_node['inputs'] = {
369
+ "upscale_method": "nearest-exact",
370
+ "megapixels": 1,
371
+ "resolution_steps": 1,
372
+ "image": [load_id, 0]
373
+ }
374
+ assembler.workflow[scale_id] = scale_node
375
+ scaled_conn = [scale_id, 0]
376
+ scaled_conns.append(scaled_conn)
377
+ return scaled_conns
378
+
379
+
380
+ def inject(assembler, chain_definition, chain_items):
381
+ """Inject prompt enhancer nodes into the target generation workflow."""
382
+ if not chain_items:
383
+ return
384
+
385
+ params = _parse_chain_items(chain_items)
386
+ if not params["enable"]:
387
+ return
388
+
389
+ model_name = _resolve_model(params, chain_definition)
390
+ if not model_name:
391
+ print("Warning: No Prompt Enhancer Models configured in prompt_enhancer.yaml. Skipping.")
392
+ return
393
+
394
+ framework = (chain_definition or {}).get("framework")
395
+ entry = get_clip_entry(model_name)
396
+ clip_model_name = str(entry.get("filename") or model_name)
397
+ clip_type = str(entry.get("clip_type") or _infer_clip_type(clip_model_name))
398
+
399
+ target_node_id, class_hint = _find_target_node(assembler, chain_definition)
400
+ if not target_node_id:
401
+ print(f"Warning: Target text-encode node for prompt enhancer '{model_name}' not found. Skipping.")
402
+ return
403
+
404
+ target_node = assembler.workflow[target_node_id]
405
+
406
+ # Fall back to the detected node class when the recipe gave no target_node.
407
+ prompt_input_keys = (class_hint[1] if class_hint else None) or ("text", "prompt")
408
+ original_prompt = None
409
+ for input_key in prompt_input_keys:
410
+ original_prompt = target_node.get('inputs', {}).get(input_key)
411
+ if original_prompt is not None:
412
+ break
413
+ if original_prompt is None:
414
+ original_prompt = ""
415
+
416
+ image_conns = _collect_reference_images(assembler, target_node)
417
+ system_prompt_name = params["system_prompt"] if params["system_prompt"] else None
418
+ if ensure_file_downloaded:
419
+ try:
420
+ ensure_file_downloaded(clip_model_name)
421
+ except Exception as e:
422
+ print(f"Warning: Failed to ensure '{clip_model_name}' downloaded: {e}")
423
+
424
+ # 1. Load the selected Prompt Enhancer Model checkpoint
425
+ clip_node_id = assembler._get_unique_id()
426
+ clip_node = create_node(assembler, "CLIPLoader", "Load CLIP")
427
+ clip_node['inputs'] = {
428
+ "clip_name": clip_model_name,
429
+ "type": clip_type,
430
+ "device": "default"
431
+ }
432
+ assembler.workflow[clip_node_id] = clip_node
433
+
434
+ # 2. Materialise the system prompt when the selected System Prompt carries text
435
+ system_prompt_conn = None
436
+ system_prompt = _select_system_prompt(system_prompt_name, framework)
437
+ if system_prompt:
438
+ system_prompt_node_id = assembler._get_unique_id()
439
+ system_prompt_node = create_node(assembler, "PrimitiveStringMultiline", "Text (System prompt)")
440
+ system_prompt_node['inputs'] = {"value": system_prompt}
441
+ assembler.workflow[system_prompt_node_id] = system_prompt_node
442
+ system_prompt_conn = [system_prompt_node_id, 0]
443
+
444
+ # 3. Emit the TextGenerateV2 node (Sampling Mode off: sampling-only sub-inputs are omitted)
445
+ max_length = params["max_length"] or 4096
446
+ text_gen_node_id = assembler._get_unique_id()
447
+ text_gen_node = create_node(assembler, "TextGenerateV2", "Generate Text")
448
+ text_gen_inputs = {
449
+ "prompt": original_prompt,
450
+ "max_length": max_length,
451
+ "sampling_mode": "off",
452
+ "thinking": params["thinking"],
453
+ "use_default_template": True,
454
+ "mtp": "auto",
455
+ "clip": [clip_node_id, 0]
456
+ }
457
+ if system_prompt_conn is not None:
458
+ text_gen_inputs["system_prompt"] = system_prompt_conn
459
+
460
+ # Wire each reference image into its own autogrow slot (image1, image2, ...).
461
+ if image_conns:
462
+ slot_names = _get_autogrow_slot_names("TextGenerateV2", "images")
463
+ if slot_names:
464
+ for conn, slot_name in zip(image_conns, slot_names):
465
+ text_gen_inputs[slot_name] = conn
466
+ if len(image_conns) > len(slot_names):
467
+ print(
468
+ f"Warning: {len(image_conns)} reference images but TextGenerateV2 "
469
+ f"exposes only {len(slot_names)} slots; extra images were dropped."
470
+ )
471
+ else:
472
+ print("Warning: could not resolve TextGenerateV2 autogrow slots; images not wired.")
473
+
474
+ text_gen_node['inputs'] = text_gen_inputs
475
+ assembler.workflow[text_gen_node_id] = text_gen_node
476
+
477
+ # 4. Route the clean generated_text output into the downstream prompt input
478
+ # (slot 0 carries the enhanced prompt; ComfyUI puts reasoning into slot 1)
479
+ target_inputs = target_node.setdefault('inputs', {})
480
+ if 'text' in target_inputs or ('prompt' not in target_inputs and 'text' in prompt_input_keys):
481
+ target_inputs['text'] = [text_gen_node_id, 0]
482
+ else:
483
+ target_inputs['prompt'] = [text_gen_node_id, 0]
484
+
485
+ print(
486
+ f"[Injector] Prompt Enhancer '{model_name}' applied successfully "
487
+ f"(clip='{clip_model_name}', type='{clip_type}', "
488
+ f"system_prompt={system_prompt_name or 'auto'})."
489
+ )
chain_injectors/qwen_image_2_1_prompt_enhancer_injector.py DELETED
@@ -1,245 +0,0 @@
1
- """Qwen-Image-2.1 Prompt Enhancer Chain Injector.
2
-
3
- This injector automatically integrates the Qwen-Image-2.1 prompt enhancement model into
4
- the generation workflow:
5
- - Detects whether the task is pure text-to-image (T2I) or involves images (I2I, inpainting,
6
- outpainting, hires.fix, or multi-reference images).
7
- - Automatically selects the appropriate prompt enhancement checkpoint:
8
- - T2I: qwen3.5_9b_qwen_image_2.1_pe_t2i.int8_convrot.safetensors
9
- - I2I / Reference: qwen3.5_9b_qwen_image_2.1_pe_i2i.int8_convrot.safetensors
10
- - Handles single or multi-reference inputs: multiple reference images are stitched into
11
- a structured grid layout via ComfyUI native ImageStitch nodes and scaled via ImageScaleToTotalPixels.
12
- - Supports reasoning mode (thinking=True) leveraging ComfyUI TextGenerate native reasoning separation
13
- (output slot 0 outputs clean prompt text, while output slot 1 isolates reasoning chain).
14
- """
15
-
16
- try:
17
- from utils.app_utils import ensure_file_downloaded
18
- except ImportError:
19
- ensure_file_downloaded = None
20
-
21
-
22
- def create_node(assembler, class_type, title):
23
- """Create a workflow node dictionary with fallback template generation."""
24
- try:
25
- node = assembler._get_node_template(class_type)
26
- except Exception:
27
- node = {
28
- "inputs": {},
29
- "class_type": class_type,
30
- "_meta": {"title": title}
31
- }
32
- if "_meta" not in node:
33
- node["_meta"] = {}
34
- node['_meta']['title'] = title
35
- return node
36
-
37
-
38
- def inject(assembler, chain_definition, chain_items):
39
- """Inject Qwen-Image-2.1 prompt enhancer nodes into the target generation workflow."""
40
- if not chain_items:
41
- return
42
-
43
- is_enabled = False
44
- thinking_enabled = False
45
- max_length = 4096
46
- for item in chain_items:
47
- if isinstance(item, dict):
48
- if item.get("enable", False) or item.get("enabled", False) or item.get("value", False):
49
- is_enabled = True
50
- if item.get("thinking", False) in (True, "true", "on", "yes", "1", 1):
51
- thinking_enabled = True
52
- if "max_length" in item and item["max_length"]:
53
- try:
54
- max_length = int(item["max_length"])
55
- except (ValueError, TypeError):
56
- pass
57
- elif item is True or str(item).lower() in ("on", "true", "yes", "1"):
58
- is_enabled = True
59
- break
60
-
61
- if not is_enabled:
62
- return
63
-
64
- target_node_name = chain_definition.get('target_node', 'prompt')
65
- target_node_id = assembler.node_map.get(target_node_name)
66
-
67
- if not target_node_id or target_node_id not in assembler.workflow:
68
- for node_id, node in assembler.workflow.items():
69
- if isinstance(node, dict) and node.get('class_type') == 'TextEncodeQwenImage21':
70
- target_node_id = node_id
71
- break
72
-
73
- if not target_node_id or target_node_id not in assembler.workflow:
74
- print(f"Warning: Target node '{target_node_name}' (TextEncodeQwenImage21) not found for Qwen-Image-2.1 Prompt Enhancer. Skipping.")
75
- return
76
-
77
- target_node = assembler.workflow[target_node_id]
78
- original_prompt = target_node.get('inputs', {}).get('prompt', '')
79
-
80
- # 1. Check if any LoadImage nodes exist in the workflow
81
- load_image_nodes = [
82
- node_id for node_id, node in assembler.workflow.items()
83
- if isinstance(node, dict) and node.get('class_type') == 'LoadImage'
84
- ]
85
- has_load_image = len(load_image_nodes) > 0
86
-
87
- target_image_conn = None
88
-
89
- if not has_load_image:
90
- # Pure text-to-image (txt2img) task without reference images
91
- clip_model_name = "qwen3.5_9b_qwen_image_2.1_pe_t2i.int8_convrot.safetensors"
92
- else:
93
- # Task with reference images or img2img / inpaint / outpaint / hires.fix
94
- clip_model_name = "qwen3.5_9b_qwen_image_2.1_pe_i2i.int8_convrot.safetensors"
95
-
96
- # Collect image connections
97
- # Prioritize extracting all images.image_* slots from target_node['inputs'] (injected by reference image injectors)
98
- indexed_slots = []
99
- target_inputs = target_node.get('inputs', {})
100
- for slot_key, slot_val in target_inputs.items():
101
- if slot_key.startswith('images.image_') and isinstance(slot_val, (list, tuple)):
102
- try:
103
- idx = int(slot_key.split('_')[-1])
104
- except Exception:
105
- idx = 999
106
- indexed_slots.append((idx, slot_val))
107
-
108
- indexed_slots.sort(key=lambda x: x[0])
109
- ref_image_conns = [s[1] for s in indexed_slots]
110
-
111
- # If no reference image slots exist (e.g. pure img2img / inpaint / outpaint), retrieve from LoadImage nodes
112
- if not ref_image_conns:
113
- for lid in load_image_nodes:
114
- scaled_conn = None
115
- for nid, node in assembler.workflow.items():
116
- if isinstance(node, dict) and node.get('class_type') == 'ImageScaleToTotalPixels':
117
- img_input = node.get('inputs', {}).get('image')
118
- if isinstance(img_input, (list, tuple)) and str(img_input[0]) == str(lid):
119
- scaled_conn = [nid, 0]
120
- break
121
- if scaled_conn:
122
- ref_image_conns.append(scaled_conn)
123
- else:
124
- scale_id = assembler._get_unique_id()
125
- scale_node = create_node(assembler, "ImageScaleToTotalPixels", "Scale Input Image")
126
- scale_node['inputs'] = {
127
- "upscale_method": "nearest-exact",
128
- "megapixels": 1,
129
- "resolution_steps": 1,
130
- "image": [lid, 0]
131
- }
132
- assembler.workflow[scale_id] = scale_node
133
- ref_image_conns.append([scale_id, 0])
134
-
135
- # Organize multiple reference images into grid stitching
136
- if len(ref_image_conns) == 1:
137
- target_image_conn = ref_image_conns[0]
138
- elif len(ref_image_conns) >= 2:
139
- num_cols = 2 if len(ref_image_conns) <= 4 else 3
140
-
141
- rows = []
142
- for i in range(0, len(ref_image_conns), num_cols):
143
- rows.append(ref_image_conns[i:i + num_cols])
144
-
145
- # Horizontally stitch images in each row
146
- stitched_rows = []
147
- for r_idx, row_imgs in enumerate(rows):
148
- if len(row_imgs) == 1:
149
- stitched_rows.append(row_imgs[0])
150
- else:
151
- curr_stitch = row_imgs[0]
152
- for c_idx in range(1, len(row_imgs)):
153
- st_id = assembler._get_unique_id()
154
- st_node = create_node(assembler, "ImageStitch", f"Stitch Images (Row {r_idx+1}-{c_idx})")
155
- st_node['inputs'] = {
156
- "direction": "right",
157
- "match_image_size": True,
158
- "spacing_width": 0,
159
- "spacing_color": "white",
160
- "image1": curr_stitch,
161
- "image2": row_imgs[c_idx]
162
- }
163
- assembler.workflow[st_id] = st_node
164
- curr_stitch = [st_id, 0]
165
- stitched_rows.append(curr_stitch)
166
-
167
- # Vertically stitch multiple rows
168
- if len(stitched_rows) == 1:
169
- final_stitched = stitched_rows[0]
170
- else:
171
- curr_vertical = stitched_rows[0]
172
- for r_idx in range(1, len(stitched_rows)):
173
- st_id = assembler._get_unique_id()
174
- st_node = create_node(assembler, "ImageStitch", f"Stitch Images (Vertical {r_idx})")
175
- st_node['inputs'] = {
176
- "direction": "down",
177
- "match_image_size": True,
178
- "spacing_width": 0,
179
- "spacing_color": "white",
180
- "image1": curr_vertical,
181
- "image2": stitched_rows[r_idx]
182
- }
183
- assembler.workflow[st_id] = st_node
184
- curr_vertical = [st_id, 0]
185
- final_stitched = curr_vertical
186
-
187
- # Scale final stitched image to ~1024 level via ImageScaleToTotalPixels
188
- final_scale_id = assembler._get_unique_id()
189
- final_scale_node = create_node(assembler, "ImageScaleToTotalPixels", "Scale Stitched Image")
190
- final_scale_node['inputs'] = {
191
- "upscale_method": "nearest-exact",
192
- "megapixels": 1,
193
- "resolution_steps": 1,
194
- "image": final_stitched
195
- }
196
- assembler.workflow[final_scale_id] = final_scale_node
197
- target_image_conn = [final_scale_id, 0]
198
-
199
- # 2. Ensure model is downloaded in CPU stage before workflow execution
200
- if ensure_file_downloaded:
201
- try:
202
- ensure_file_downloaded(clip_model_name)
203
- except Exception as e:
204
- print(f"Warning: Failed to ensure '{clip_model_name}' downloaded: {e}")
205
-
206
- # 2. Create CLIPLoader node
207
- clip_node_id = assembler._get_unique_id()
208
- clip_node = create_node(assembler, "CLIPLoader", "Load CLIP")
209
- clip_node['inputs'] = {
210
- "clip_name": clip_model_name,
211
- "type": "qwen_image",
212
- "device": "default"
213
- }
214
- assembler.workflow[clip_node_id] = clip_node
215
-
216
- # 3. Create TextGenerate node
217
- text_gen_node_id = assembler._get_unique_id()
218
- text_gen_node = create_node(assembler, "TextGenerate", "Generate Text")
219
- text_gen_inputs = {
220
- "prompt": original_prompt,
221
- "max_length": max_length,
222
- "sampling_mode": "on",
223
- "sampling_mode.temperature": 0.7,
224
- "sampling_mode.top_k": 20,
225
- "sampling_mode.top_p": 0.95,
226
- "sampling_mode.min_p": 0.0,
227
- "sampling_mode.repetition_penalty": 1.05,
228
- "sampling_mode.seed": 0,
229
- "sampling_mode.presence_penalty": 0.0 if has_load_image else 1.5,
230
- "thinking": thinking_enabled,
231
- "use_default_template": True,
232
- "mtp": "auto",
233
- "clip": [clip_node_id, 0]
234
- }
235
- if target_image_conn is not None:
236
- text_gen_inputs["image"] = target_image_conn
237
-
238
- text_gen_node['inputs'] = text_gen_inputs
239
- assembler.workflow[text_gen_node_id] = text_gen_node
240
-
241
- # 4. Connect TextGenerate output directly to target node prompt
242
- # ComfyUI TextGenerate node natively strips reasoning tags (<think>...</think>) into output slot 1,
243
- # leaving clean enhanced prompt in output slot 0.
244
- target_node['inputs']['prompt'] = [text_gen_node_id, 0]
245
- print(f"[Injector] Qwen-Image-2.1 Prompt Enhancer applied successfully ({'I2I' if has_load_image else 'T2I'}, clip='{clip_model_name}', thinking={thinking_enabled}).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/pipelines/pipeline_input_processor.py CHANGED
@@ -408,41 +408,21 @@ def process_pipeline_inputs(ui_inputs: Dict[str, Any], progress: gr.Progress, wo
408
  "x": int(xs[i]), "y": int(ys[i]), "strength": float(strengths[i])
409
  })
410
 
411
- pe_enable = ui_inputs.get('qwen_image_2_1_prompt_enhancer_enable', False)
412
- active_qwen_image_2_1_prompt_enhancer = []
413
- if 'qwen_image_2_1_prompt_enhancer' in enabled_chains and pe_enable:
414
- active_qwen_image_2_1_prompt_enhancer.append({
415
- "enable": True,
416
- "thinking": bool(ui_inputs.get('qwen_image_2_1_prompt_enhancer_thinking', False)),
417
- "max_length": int(ui_inputs.get('qwen_image_2_1_prompt_enhancer_max_length', 4096))
418
- })
419
-
420
- ming_pe_enable = ui_inputs.get('ming_image_prompt_enhancer_enable', False)
421
- active_ming_image_prompt_enhancer = []
422
- if 'ming_image_prompt_enhancer' in enabled_chains and ming_pe_enable:
423
- item = {
424
- "enable": True,
425
- "thinking": bool(ui_inputs.get('ming_image_prompt_enhancer_thinking', False)),
426
- "max_length": int(ui_inputs.get('ming_image_prompt_enhancer_max_length', 4096))
427
- }
428
- sys_p = ui_inputs.get('ming_image_prompt_enhancer_system_prompt')
429
- if sys_p:
430
- item["system_prompt"] = str(sys_p)
431
- active_ming_image_prompt_enhancer.append(item)
432
-
433
- flux3_pe_enable = ui_inputs.get('flux3_prompt_upsampler_enable', False)
434
- active_flux3_prompt_upsampler = []
435
- if 'flux3_prompt_upsampler' in enabled_chains and flux3_pe_enable:
436
  item = {
437
  "enable": True,
438
- "thinking": bool(ui_inputs.get('flux3_prompt_upsampler_thinking', False)),
439
- "bbox_mode": bool(ui_inputs.get('flux3_prompt_upsampler_bbox_mode', False)),
440
- "max_length": int(ui_inputs.get('flux3_prompt_upsampler_max_length', 3584))
441
  }
442
- sys_p = ui_inputs.get('flux3_prompt_upsampler_system_prompt')
443
- if sys_p:
444
- item["system_prompt"] = str(sys_p)
445
- active_flux3_prompt_upsampler.append(item)
 
 
 
446
 
447
  color_map_val = ui_inputs.get('moss_image_control_color_map')
448
  depth_map_val = ui_inputs.get('moss_image_control_depth_map')
@@ -523,9 +503,7 @@ def process_pipeline_inputs(ui_inputs: Dict[str, Any], progress: gr.Progress, wo
523
  "active_qwen_image_edit": active_qwen_image_edit,
524
  "active_reference_images": active_reference_images,
525
  "active_conditioning": active_conditioning,
526
- "active_qwen_image_2_1_prompt_enhancer": active_qwen_image_2_1_prompt_enhancer,
527
- "active_ming_image_prompt_enhancer": active_ming_image_prompt_enhancer,
528
- "active_flux3_prompt_upsampler": active_flux3_prompt_upsampler,
529
  "active_moss_image_control": active_moss_image_control,
530
  "active_moss_image_edit": active_moss_image_edit,
531
  "temp_files_to_clean": temp_files_to_clean
 
408
  "x": int(xs[i]), "y": int(ys[i]), "strength": float(strengths[i])
409
  })
410
 
411
+ pe_enable = ui_inputs.get('prompt_enhancer_enable', False)
412
+ active_prompt_enhancer = []
413
+ if 'prompt_enhancer' in enabled_chains and pe_enable:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  item = {
415
  "enable": True,
416
+ "thinking": bool(ui_inputs.get('prompt_enhancer_thinking', False)),
417
+ "max_length": int(ui_inputs.get('prompt_enhancer_max_length', 4096))
 
418
  }
419
+ model_val = ui_inputs.get('prompt_enhancer_model')
420
+ if model_val and str(model_val) != 'auto':
421
+ item["model"] = str(model_val)
422
+ prompt_val = ui_inputs.get('prompt_enhancer_system_prompt')
423
+ if prompt_val and str(prompt_val) != 'auto':
424
+ item["system_prompt"] = str(prompt_val)
425
+ active_prompt_enhancer.append(item)
426
 
427
  color_map_val = ui_inputs.get('moss_image_control_color_map')
428
  depth_map_val = ui_inputs.get('moss_image_control_depth_map')
 
503
  "active_qwen_image_edit": active_qwen_image_edit,
504
  "active_reference_images": active_reference_images,
505
  "active_conditioning": active_conditioning,
506
+ "active_prompt_enhancer": active_prompt_enhancer,
 
 
507
  "active_moss_image_control": active_moss_image_control,
508
  "active_moss_image_edit": active_moss_image_edit,
509
  "temp_files_to_clean": temp_files_to_clean
core/pipelines/sd_image_pipeline.py CHANGED
@@ -119,9 +119,7 @@ class SdImagePipeline(BasePipeline):
119
  active_qwen_image_edit = processed.get("active_qwen_image_edit", [])
120
  active_reference_images = processed.get("active_reference_images", [])
121
  active_conditioning = processed["active_conditioning"]
122
- active_qwen_image_2_1_prompt_enhancer = processed.get("active_qwen_image_2_1_prompt_enhancer", [])
123
- active_ming_image_prompt_enhancer = processed.get("active_ming_image_prompt_enhancer", [])
124
- active_flux3_prompt_upsampler = processed.get("active_flux3_prompt_upsampler", [])
125
  active_moss_image_control = processed.get("active_moss_image_control", [])
126
  active_moss_image_edit = processed.get("active_moss_image_edit", [])
127
 
@@ -181,9 +179,7 @@ class SdImagePipeline(BasePipeline):
181
  "krea2_style_reference_chain": active_krea2_reference_edit,
182
  "qwen_image_edit_chain": active_qwen_image_edit,
183
  "reference_image_chain": active_reference_images,
184
- "qwen_image_2_1_prompt_enhancer_chain": active_qwen_image_2_1_prompt_enhancer,
185
- "ming_image_prompt_enhancer_chain": active_ming_image_prompt_enhancer,
186
- "flux3_prompt_upsampler_chain": active_flux3_prompt_upsampler,
187
  "moss_image_control_chain": active_moss_image_control,
188
  "moss_image_edit_chain": active_moss_image_edit,
189
  "vae_chain": [ui_inputs.get('vae_name')] if (ui_inputs.get('vae_name') and 'vae' in enabled_chains) else [],
 
119
  active_qwen_image_edit = processed.get("active_qwen_image_edit", [])
120
  active_reference_images = processed.get("active_reference_images", [])
121
  active_conditioning = processed["active_conditioning"]
122
+ active_prompt_enhancer = processed.get("active_prompt_enhancer", [])
 
 
123
  active_moss_image_control = processed.get("active_moss_image_control", [])
124
  active_moss_image_edit = processed.get("active_moss_image_edit", [])
125
 
 
179
  "krea2_style_reference_chain": active_krea2_reference_edit,
180
  "qwen_image_edit_chain": active_qwen_image_edit,
181
  "reference_image_chain": active_reference_images,
182
+ "prompt_enhancer_chain": active_prompt_enhancer,
 
 
183
  "moss_image_control_chain": active_moss_image_control,
184
  "moss_image_edit_chain": active_moss_image_edit,
185
  "vae_chain": [ui_inputs.get('vae_name')] if (ui_inputs.get('vae_name') and 'vae' in enabled_chains) else [],
core/pipelines/workflow_executor.py CHANGED
@@ -4,6 +4,83 @@ from typing import Dict, Any, List
4
  from comfy_integration.nodes import NODE_CLASS_MAPPINGS
5
  from utils.app_utils import get_value_at_index
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  class WorkflowExecutor:
8
  @staticmethod
9
  def topological_sort(workflow: Dict[str, Any]) -> List[str]:
@@ -117,9 +194,17 @@ class WorkflowExecutor:
117
  if class_type == 'TextEncodeMingImageEdit' and ('images' not in kwargs or kwargs.get('images') is None):
118
  kwargs['images'] = {}
119
 
 
 
 
 
 
 
 
 
120
  function_name = getattr(node_class, 'FUNCTION')
121
  execution_method = getattr(node_instance, function_name)
122
-
123
  result = execution_method(**kwargs)
124
  computed_outputs[node_id] = result
125
 
 
4
  from comfy_integration.nodes import NODE_CLASS_MAPPINGS
5
  from utils.app_utils import get_value_at_index
6
 
7
+
8
+ def _normalize_dynamic_inputs(node_class, class_type, kwargs):
9
+ """Rebuild the structures ComfyUI's executor would have built for this node.
10
+
11
+ Two V3 dynamic shapes need post-processing when nodes are called directly:
12
+
13
+ * ``COMFY_DYNAMICCOMBO_V3`` — the widget arrives as a bare string but the
14
+ node signature expects ``{id: <option>, **sub_keys}`` (e.g.
15
+ ``sampling_mode={"sampling_mode": "off"}``).
16
+ * ``COMFY_AUTOGROW_V3`` — slots arrive flattened as ``image1``, ``image2``,
17
+ ... but the node signature expects ``images={"image1": <value>, ...}``.
18
+
19
+ ComfyUI's ``comfy_api.latest._io.build_nested_inputs`` does both; this
20
+ Space executes nodes directly and therefore has to redo the work.
21
+ """
22
+ schema = getattr(node_class, 'SCHEMA', None)
23
+ inputs = getattr(schema, 'inputs', None) if schema is not None else None
24
+ if not inputs:
25
+ return kwargs
26
+
27
+ # --- COMFY_AUTOGROW_V3 first: consume image1/video1/... slots ---
28
+ for spec in inputs:
29
+ sub_id = getattr(spec, 'id', None)
30
+ template = getattr(spec, 'template', None)
31
+ if not sub_id or template is None:
32
+ continue
33
+
34
+ names = getattr(template, 'names', None) or []
35
+ prefix = getattr(template, 'prefix', None)
36
+ candidates = list(names)
37
+ if prefix:
38
+ max_n = getattr(template, 'max', 0) or 0
39
+ if not candidates:
40
+ candidates = [f"{prefix}{i}" for i in range(max_n)]
41
+
42
+ collected = {}
43
+ for name in candidates:
44
+ if name in kwargs:
45
+ collected[name] = kwargs.pop(name)
46
+
47
+ if collected and not isinstance(kwargs.get(sub_id), dict):
48
+ kwargs[sub_id] = collected
49
+
50
+ # --- COMFY_DYNAMICCOMBO_V3 ---
51
+ for spec in inputs:
52
+ sub_id = getattr(spec, 'id', None)
53
+ options = getattr(spec, 'options', None)
54
+ if not sub_id or not options or sub_id not in kwargs:
55
+ continue
56
+
57
+ value = kwargs[sub_id]
58
+ if isinstance(value, dict):
59
+ continue # already structured
60
+
61
+ option_keys = []
62
+ for opt in options:
63
+ if isinstance(opt, dict):
64
+ option_keys.append(opt.get('key'))
65
+ else:
66
+ option_keys.append(getattr(opt, 'key', None))
67
+
68
+ if value not in option_keys:
69
+ print(f"Warning: '{sub_id}' value {value!r} is not one of {option_keys} for {class_type}.")
70
+ continue
71
+
72
+ # Pull sibling sub-keys (e.g. "sampling_mode.temperature") into the dict.
73
+ nested = {sub_id: str(value)}
74
+ prefix = f"{sub_id}."
75
+ for key in list(kwargs.keys()):
76
+ if key.startswith(prefix):
77
+ nested[key[len(prefix):]] = kwargs.pop(key)
78
+ kwargs[sub_id] = nested
79
+ print(f" -> Normalized dynamic input '{sub_id}' for {class_type}: {nested}")
80
+
81
+ return kwargs
82
+
83
+
84
  class WorkflowExecutor:
85
  @staticmethod
86
  def topological_sort(workflow: Dict[str, Any]) -> List[str]:
 
194
  if class_type == 'TextEncodeMingImageEdit' and ('images' not in kwargs or kwargs.get('images') is None):
195
  kwargs['images'] = {}
196
 
197
+ # ComfyUI's own executor restructures V3 dynamic inputs into the
198
+ # nested dict the node signature expects (see
199
+ # comfy_api.latest._io.build_nested_inputs). This direct-call
200
+ # path bypasses that step, so a COMFY_DYNAMICCOMBO_V3 widget
201
+ # arrives as a bare string and the node fails on
202
+ # `sampling_mode.get(...)`. Rebuild it here.
203
+ kwargs = _normalize_dynamic_inputs(node_class, class_type, kwargs)
204
+
205
  function_name = getattr(node_class, 'FUNCTION')
206
  execution_method = getattr(node_instance, function_name)
207
+
208
  result = execution_method(**kwargs)
209
  computed_outputs[node_id] = result
210
 
core/pipelines/workflow_recipes/_partials/conditioning/flux3.yaml CHANGED
@@ -78,9 +78,10 @@ dynamic_reference_latent_chains:
78
  ksampler_node: "ksampler"
79
  vae_node: "vae_loader"
80
 
81
- dynamic_flux3_prompt_upsampler_chains:
82
- flux3_prompt_upsampler_chain:
83
  target_node: "pos_prompt"
 
84
 
85
  dynamic_pid_chains:
86
  pid_chain:
 
78
  ksampler_node: "ksampler"
79
  vae_node: "vae_loader"
80
 
81
+ dynamic_prompt_enhancer_chains:
82
+ prompt_enhancer_chain:
83
  target_node: "pos_prompt"
84
+ framework: "flux3"
85
 
86
  dynamic_pid_chains:
87
  pid_chain:
core/pipelines/workflow_recipes/_partials/conditioning/ming-image.yaml CHANGED
@@ -89,9 +89,10 @@ dynamic_reference_image_chains:
89
  vae_node: "vae_loader"
90
  max_images: 8
91
 
92
- dynamic_ming_image_prompt_enhancer_chains:
93
- ming_image_prompt_enhancer_chain:
94
  target_node: "prompt"
 
95
 
96
 
97
  ui_map:
 
89
  vae_node: "vae_loader"
90
  max_images: 8
91
 
92
+ dynamic_prompt_enhancer_chains:
93
+ prompt_enhancer_chain:
94
  target_node: "prompt"
95
+ framework: "ming-image"
96
 
97
 
98
  ui_map:
core/pipelines/workflow_recipes/_partials/conditioning/qwen-image-2.1.yaml CHANGED
@@ -70,9 +70,10 @@ dynamic_reference_image_chains:
70
  vae_node: "vae_loader"
71
  max_images: 10
72
 
73
- dynamic_qwen_image_2_1_prompt_enhancer_chains:
74
- qwen_image_2_1_prompt_enhancer_chain:
75
  target_node: "prompt"
 
76
 
77
  ui_map:
78
  positive_prompt: "prompt:prompt"
 
70
  vae_node: "vae_loader"
71
  max_images: 10
72
 
73
+ dynamic_prompt_enhancer_chains:
74
+ prompt_enhancer_chain:
75
  target_node: "prompt"
76
+ framework: "qwen-image-2.1"
77
 
78
  ui_map:
79
  positive_prompt: "prompt:prompt"
core/workflow_assembler.py CHANGED
@@ -38,6 +38,26 @@ class WorkflowAssembler:
38
  if chain_key not in order:
39
  order.append(chain_key)
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  self.injector_order = order
42
 
43
  def _get_unique_id(self):
@@ -82,7 +102,11 @@ class WorkflowAssembler:
82
 
83
  merged_recipe = {'nodes': {}, 'connections': [], 'ui_map': {}}
84
  for key in self.injector_order:
85
- if key.startswith('dynamic_'):
 
 
 
 
86
  merged_recipe[key] = {}
87
 
88
  parent_recipe_dir = os.path.dirname(recipe_path_to_use)
@@ -98,8 +122,8 @@ class WorkflowAssembler:
98
  merged_recipe['connections'].extend(imported_recipe.get('connections', []))
99
  merged_recipe['ui_map'].update(imported_recipe.get('ui_map', {}))
100
  for key in self.injector_order:
101
- if key in imported_recipe and key.startswith('dynamic_'):
102
- merged_recipe[key].update(imported_recipe.get(key, {}))
103
  except FileNotFoundError:
104
  print(f"Warning: Optional recipe partial '{import_path}' not found. Skipping.")
105
 
@@ -107,8 +131,8 @@ class WorkflowAssembler:
107
  merged_recipe['connections'].extend(main_recipe.get('connections', []))
108
  merged_recipe['ui_map'].update(main_recipe.get('ui_map', {}))
109
  for key in self.injector_order:
110
- if key in main_recipe and key.startswith('dynamic_'):
111
- merged_recipe[key].update(main_recipe.get(key, {}))
112
 
113
  return merged_recipe
114
 
@@ -148,7 +172,7 @@ class WorkflowAssembler:
148
  self.workflow[to_id]['inputs'][to_input_name] = [from_id, int(from_output_idx)]
149
 
150
  print("--- [Assembler] Applying dynamic injectors ---")
151
- recipe_chain_types = {key for key in self.recipe if key.startswith('dynamic_')}
152
  processing_order = [key for key in self.injector_order if key in recipe_chain_types]
153
 
154
  for chain_type in processing_order:
 
38
  if chain_key not in order:
39
  order.append(chain_key)
40
 
41
+ # An injector may register under a bare CHAIN_TYPE while the recipe uses
42
+ # the derived "dynamic_<feature>_chains" key (or vice versa). Alias both
43
+ # directions so either spelling resolves to the same callable.
44
+ for chain_key in list(order):
45
+ func = self.global_injectors.get(chain_key)
46
+ if not func:
47
+ continue
48
+ if chain_key.startswith('dynamic_') and chain_key.endswith('_chains'):
49
+ feature = chain_key[8:-7]
50
+ alias = f"dynamic_{feature}_chains"
51
+ else:
52
+ feature = chain_key
53
+ alias = f"dynamic_{feature}_chains"
54
+ self.global_injectors.setdefault(alias, func)
55
+ self.global_injectors.setdefault(feature, func)
56
+ if alias not in order:
57
+ order.append(alias)
58
+ if feature not in order:
59
+ order.append(feature)
60
+
61
  self.injector_order = order
62
 
63
  def _get_unique_id(self):
 
102
 
103
  merged_recipe = {'nodes': {}, 'connections': [], 'ui_map': {}}
104
  for key in self.injector_order:
105
+ # Injector chains may be registered under a bare CHAIN_TYPE
106
+ # (e.g. "prompt_enhancer") or the derived "dynamic_*_chains" form.
107
+ # Both must survive the merge, otherwise the assembler has nothing
108
+ # to look up at injection time.
109
+ if key:
110
  merged_recipe[key] = {}
111
 
112
  parent_recipe_dir = os.path.dirname(recipe_path_to_use)
 
122
  merged_recipe['connections'].extend(imported_recipe.get('connections', []))
123
  merged_recipe['ui_map'].update(imported_recipe.get('ui_map', {}))
124
  for key in self.injector_order:
125
+ if key in imported_recipe:
126
+ merged_recipe.setdefault(key, {}).update(imported_recipe.get(key, {}))
127
  except FileNotFoundError:
128
  print(f"Warning: Optional recipe partial '{import_path}' not found. Skipping.")
129
 
 
131
  merged_recipe['connections'].extend(main_recipe.get('connections', []))
132
  merged_recipe['ui_map'].update(main_recipe.get('ui_map', {}))
133
  for key in self.injector_order:
134
+ if key in main_recipe:
135
+ merged_recipe.setdefault(key, {}).update(main_recipe.get(key, {}))
136
 
137
  return merged_recipe
138
 
 
172
  self.workflow[to_id]['inputs'][to_input_name] = [from_id, int(from_output_idx)]
173
 
174
  print("--- [Assembler] Applying dynamic injectors ---")
175
+ recipe_chain_types = {key for key in self.recipe if key in self.injector_order}
176
  processing_order = [key for key in self.injector_order if key in recipe_chain_types]
177
 
178
  for chain_type in processing_order:
mcp_tools/common.py CHANGED
@@ -607,116 +607,26 @@ def _execute_imagegen_pipeline(task_id: str, params: dict):
607
  if isinstance(is_enabled, str):
608
  is_enabled = is_enabled.upper() in ("ON", "TRUE", "1")
609
  ui_inputs["pid_settings"] = "ON" if is_enabled else "OFF"
610
- elif itype == "qwen_image_2_1_prompt_enhancer":
611
  is_enabled = item.get("enable", item.get("enabled", True))
612
  if isinstance(is_enabled, str):
613
  is_enabled = is_enabled.upper() in ("ON", "TRUE", "1")
614
- ui_inputs["qwen_image_2_1_prompt_enhancer_enable"] = bool(is_enabled)
615
- is_thinking = item.get("thinking", False)
616
- if isinstance(is_thinking, str):
617
- is_thinking = is_thinking.upper() in ("ON", "TRUE", "1")
618
- ui_inputs["qwen_image_2_1_prompt_enhancer_thinking"] = bool(is_thinking)
619
- ui_inputs["qwen_image_2_1_prompt_enhancer_max_length"] = int(item.get("max_length", 4096))
620
- elif itype == "ming_image_prompt_enhancer":
621
- is_enabled = item.get("enable", item.get("enabled", True))
622
- if isinstance(is_enabled, str):
623
- is_enabled = is_enabled.upper() in ("ON", "TRUE", "1")
624
- ui_inputs["ming_image_prompt_enhancer_enable"] = bool(is_enabled)
625
- is_thinking = item.get("thinking", False)
626
- if isinstance(is_thinking, str):
627
- is_thinking = is_thinking.upper() in ("ON", "TRUE", "1")
628
- ui_inputs["ming_image_prompt_enhancer_thinking"] = bool(is_thinking)
629
- ui_inputs["ming_image_prompt_enhancer_max_length"] = int(item.get("max_length", 4096))
630
- if "system_prompt" in item:
631
- ui_inputs["ming_image_prompt_enhancer_system_prompt"] = str(item["system_prompt"])
632
- elif itype == "flux3_prompt_upsampler":
633
- is_enabled = item.get("enable", item.get("enabled", True))
634
- if isinstance(is_enabled, str):
635
- is_enabled = is_enabled.upper() in ("ON", "TRUE", "1")
636
- ui_inputs["flux3_prompt_upsampler_enable"] = bool(is_enabled)
637
  is_thinking = item.get("thinking", False)
638
  if isinstance(is_thinking, str):
639
  is_thinking = is_thinking.upper() in ("ON", "TRUE", "1")
640
- ui_inputs["flux3_prompt_upsampler_thinking"] = bool(is_thinking)
641
- is_bbox_mode = item.get("bbox_mode", False)
642
- if isinstance(is_bbox_mode, str):
643
- is_bbox_mode = is_bbox_mode.upper() in ("ON", "TRUE", "1")
644
- ui_inputs["flux3_prompt_upsampler_bbox_mode"] = bool(is_bbox_mode)
645
- ui_inputs["flux3_prompt_upsampler_max_length"] = int(item.get("max_length", 3584))
646
- if "system_prompt" in item:
647
- ui_inputs["flux3_prompt_upsampler_system_prompt"] = str(item["system_prompt"])
648
- elif itype == "moss_image_control":
649
- if "color_map" in item:
650
- parsed_color_map = _parse_image_param(item["color_map"])
651
- if parsed_color_map:
652
- ui_inputs["moss_image_control_color_map"] = parsed_color_map
653
- if "depth_map" in item:
654
- parsed_depth_map = _parse_image_param(item["depth_map"])
655
- if parsed_depth_map:
656
- ui_inputs["moss_image_control_depth_map"] = parsed_depth_map
657
- if "strength" in item:
658
- try:
659
- ui_inputs["moss_image_control_strength"] = float(item["strength"])
660
- except (ValueError, TypeError):
661
- pass
662
- if "start_percent" in item:
663
- try:
664
- ui_inputs["moss_image_control_start_percent"] = float(item["start_percent"])
665
- except (ValueError, TypeError):
666
- pass
667
- if "end_percent" in item:
668
- try:
669
- ui_inputs["moss_image_control_end_percent"] = float(item["end_percent"])
670
- except (ValueError, TypeError):
671
- pass
672
- elif itype == "krea2_identity_edit":
673
- img = _parse_image_param(item.get("image"))
674
- if img:
675
- krea2_identity_edit_data.append(img)
676
- elif itype == "krea2_style_reference":
677
- img = _parse_image_param(item.get("image"))
678
- if img:
679
- krea2_reference_edit_data.append(img)
680
- elif itype in ("reference_latent", "reference_edit"):
681
- img = _parse_image_param(item.get("image"))
682
- if img:
683
- reference_latent_data.append(img)
684
- elif itype in ("reference_image", "mage_flow_reference_edit", "boogu_image_edit", "boogu_edit", "qwen_image_2_1_edit"):
685
- img = _parse_image_param(item.get("image"))
686
- if img:
687
- reference_image_data.append(img)
688
- elif itype in ("joyai_image", "joyai_reference_edit"):
689
- img = _parse_image_param(item.get("image"))
690
- if img:
691
- joyai_reference_data.append(img)
692
- elif itype == "qwen_image_edit":
693
- img = _parse_image_param(item.get("image"))
694
- if img:
695
- qwen_image_edit_data.append(img)
696
- elif itype == "hidream_o1_reference":
697
- img = _parse_image_param(item.get("image"))
698
- if img:
699
- hidream_o1_reference_data.append(img)
700
- elif itype == "sensenova_reference":
701
- img = _parse_image_param(item.get("image"))
702
- if img:
703
- sensenova_reference_data.append(img)
704
- elif itype == "moss_image_edit":
705
- if "images" in item and isinstance(item["images"], list):
706
- for img_item in item["images"]:
707
- p_img = _parse_image_param(img_item)
708
- if p_img:
709
- moss_image_edit_data.append(p_img)
710
- elif "image" in item:
711
- p_img = _parse_image_param(item["image"])
712
- if p_img:
713
- moss_image_edit_data.append(p_img)
714
- elif itype == "vae":
715
- v_source = item.get("source", item.get("vae_source", "Civitai"))
716
- v_val = item.get("vae_value", item.get("value", item.get("vae_id", item.get("vae_name", ""))))
717
- if v_source and v_val:
718
- ui_inputs["vae_source"] = v_source
719
- ui_inputs["vae_id"] = str(v_val)
720
 
721
  if lora_data: ui_inputs["lora_data"] = lora_data
722
  if embedding_data: ui_inputs["embedding_data"] = embedding_data
@@ -781,49 +691,19 @@ def _execute_imagegen_pipeline(task_id: str, params: dict):
781
  else:
782
  ui_inputs["pid_settings"] = "OFF"
783
 
784
- pe_val = params.get("qwen_image_2_1_prompt_enhancer") or params.get("qwen_image_2_1_prompt_enhancer_enable")
785
  if pe_val is not None:
786
- if isinstance(pe_val, bool):
787
- ui_inputs["qwen_image_2_1_prompt_enhancer_enable"] = pe_val
788
- elif isinstance(pe_val, dict):
789
- ui_inputs["qwen_image_2_1_prompt_enhancer_enable"] = bool(pe_val.get("enable", pe_val.get("enabled", True)))
790
- ui_inputs["qwen_image_2_1_prompt_enhancer_thinking"] = bool(pe_val.get("thinking", False))
791
- ui_inputs["qwen_image_2_1_prompt_enhancer_max_length"] = int(pe_val.get("max_length", 4096))
792
- elif str(pe_val).upper() in ("ON", "TRUE", "1"):
793
- ui_inputs["qwen_image_2_1_prompt_enhancer_enable"] = True
794
- else:
795
- ui_inputs["qwen_image_2_1_prompt_enhancer_enable"] = False
796
-
797
- ming_pe_val = params.get("ming_image_prompt_enhancer") or params.get("ming_image_prompt_enhancer_enable")
798
- if ming_pe_val is not None:
799
- if isinstance(ming_pe_val, bool):
800
- ui_inputs["ming_image_prompt_enhancer_enable"] = ming_pe_val
801
- elif isinstance(ming_pe_val, dict):
802
- ui_inputs["ming_image_prompt_enhancer_enable"] = bool(ming_pe_val.get("enable", ming_pe_val.get("enabled", True)))
803
- ui_inputs["ming_image_prompt_enhancer_thinking"] = bool(ming_pe_val.get("thinking", False))
804
- ui_inputs["ming_image_prompt_enhancer_max_length"] = int(ming_pe_val.get("max_length", 4096))
805
- if "system_prompt" in ming_pe_val:
806
- ui_inputs["ming_image_prompt_enhancer_system_prompt"] = str(ming_pe_val["system_prompt"])
807
- elif str(ming_pe_val).upper() in ("ON", "TRUE", "1"):
808
- ui_inputs["ming_image_prompt_enhancer_enable"] = True
809
  else:
810
- ui_inputs["ming_image_prompt_enhancer_enable"] = False
811
-
812
- flux3_pe_val = params.get("flux3_prompt_upsampler") or params.get("flux3_prompt_upsampler_enable")
813
- if flux3_pe_val is not None:
814
- if isinstance(flux3_pe_val, bool):
815
- ui_inputs["flux3_prompt_upsampler_enable"] = flux3_pe_val
816
- elif isinstance(flux3_pe_val, dict):
817
- ui_inputs["flux3_prompt_upsampler_enable"] = bool(flux3_pe_val.get("enable", flux3_pe_val.get("enabled", True)))
818
- ui_inputs["flux3_prompt_upsampler_thinking"] = bool(flux3_pe_val.get("thinking", False))
819
- ui_inputs["flux3_prompt_upsampler_bbox_mode"] = bool(flux3_pe_val.get("bbox_mode", False))
820
- ui_inputs["flux3_prompt_upsampler_max_length"] = int(flux3_pe_val.get("max_length", 3584))
821
- if "system_prompt" in flux3_pe_val:
822
- ui_inputs["flux3_prompt_upsampler_system_prompt"] = str(flux3_pe_val["system_prompt"])
823
- elif str(flux3_pe_val).upper() in ("ON", "TRUE", "1"):
824
- ui_inputs["flux3_prompt_upsampler_enable"] = True
825
- else:
826
- ui_inputs["flux3_prompt_upsampler_enable"] = False
827
 
828
  moss_control_val = params.get("moss_image_control")
829
  if moss_control_val and isinstance(moss_control_val, dict):
 
607
  if isinstance(is_enabled, str):
608
  is_enabled = is_enabled.upper() in ("ON", "TRUE", "1")
609
  ui_inputs["pid_settings"] = "ON" if is_enabled else "OFF"
610
+ elif itype == "prompt_enhancer":
611
  is_enabled = item.get("enable", item.get("enabled", True))
612
  if isinstance(is_enabled, str):
613
  is_enabled = is_enabled.upper() in ("ON", "TRUE", "1")
614
+ ui_inputs["prompt_enhancer_enable"] = bool(is_enabled)
615
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
  is_thinking = item.get("thinking", False)
617
  if isinstance(is_thinking, str):
618
  is_thinking = is_thinking.upper() in ("ON", "TRUE", "1")
619
+ ui_inputs["prompt_enhancer_thinking"] = bool(is_thinking)
620
+
621
+ ui_inputs["prompt_enhancer_max_length"] = int(item.get("max_length", 4096))
622
+
623
+ model_value = item.get("model")
624
+ if model_value:
625
+ ui_inputs["prompt_enhancer_model"] = str(model_value)
626
+
627
+ prompt_value = item.get("system_prompt")
628
+ if prompt_value:
629
+ ui_inputs["prompt_enhancer_system_prompt"] = str(prompt_value)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
 
631
  if lora_data: ui_inputs["lora_data"] = lora_data
632
  if embedding_data: ui_inputs["embedding_data"] = embedding_data
 
691
  else:
692
  ui_inputs["pid_settings"] = "OFF"
693
 
694
+ pe_val = params.get("prompt_enhancer")
695
  if pe_val is not None:
696
+ if isinstance(pe_val, dict):
697
+ ui_inputs["prompt_enhancer_enable"] = bool(pe_val.get("enable", pe_val.get("enabled", True)))
698
+ ui_inputs["prompt_enhancer_thinking"] = bool(pe_val.get("thinking", False))
699
+ ui_inputs["prompt_enhancer_max_length"] = int(pe_val.get("max_length", 4096))
700
+ if pe_val.get("model"):
701
+ ui_inputs["prompt_enhancer_model"] = str(pe_val["model"])
702
+ if pe_val.get("system_prompt"):
703
+ ui_inputs["prompt_enhancer_system_prompt"] = str(pe_val["system_prompt"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  else:
705
+ ui_inputs["prompt_enhancer_enable"] = bool(pe_val)
706
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
 
708
  moss_control_val = params.get("moss_image_control")
709
  if moss_control_val and isinstance(moss_control_val, dict):
ui/events/change_handlers.py CHANGED
@@ -9,13 +9,13 @@ from core.settings import (
9
  ARCH_CATEGORIES_MAP
10
  )
11
  from utils.app_utils import get_model_generation_defaults
12
- from ui.shared.ui_components import RESOLUTION_MAP
13
  from .config_loaders import (
14
  get_cn_defaults,
15
  load_ipadapter_config
16
  )
17
 
18
- def make_update_fn(m_comp, cat_comp, ar_comp, width_comp, height_comp, cn_types, cn_series, cn_filepaths, ipa_preset, lora_acc, cn_acc, ipa_acc, sd3_ipa_acc, flux1_ipa_acc, style_acc, embed_acc, cond_acc, ref_latent_acc, hidream_o1_ref_acc, prompt_comp, neg_prompt_comp, steps_comp, cfg_comp, sampler_comp, scheduler_comp, pid_acc=None, vae_acc=None, joyai_ref_acc=None, krea2_identity_edit_acc=None, krea2_reference_edit_acc=None, qwen_image_edit_acc=None, ref_img_acc=None, sensenova_ref_acc=None, task_type=None, type_comp=None, qwen_pe_acc=None, ming_pe_acc=None, flux3_pe_acc=None, moss_image_control_acc=None, moss_image_edit_acc=None):
19
  def update_fn(*args):
20
  arch = args[0]
21
  category = args[1]
@@ -84,9 +84,11 @@ def make_update_fn(m_comp, cat_comp, ar_comp, width_comp, height_comp, cn_types,
84
  if ref_img_acc: updates[ref_img_acc] = gr.update(visible=('reference_image' in enabled_chains))
85
  if pid_acc: updates[pid_acc] = gr.update(visible=('pid' in enabled_chains))
86
  if vae_acc: updates[vae_acc] = gr.update(visible=('vae' in enabled_chains))
87
- if qwen_pe_acc: updates[qwen_pe_acc] = gr.update(visible=('qwen_image_2_1_prompt_enhancer' in enabled_chains))
88
- if ming_pe_acc: updates[ming_pe_acc] = gr.update(visible=('ming_image_prompt_enhancer' in enabled_chains))
89
- if flux3_pe_acc: updates[flux3_pe_acc] = gr.update(visible=('flux3_prompt_upsampler' in enabled_chains))
 
 
90
  if moss_image_control_acc: updates[moss_image_control_acc] = gr.update(visible=('moss_image_control' in enabled_chains))
91
  if moss_image_edit_acc: updates[moss_image_edit_acc] = gr.update(visible=('moss_image_edit' in enabled_chains))
92
 
@@ -133,7 +135,7 @@ def make_update_fn(m_comp, cat_comp, ar_comp, width_comp, height_comp, cn_types,
133
  return update_fn
134
 
135
 
136
- def make_model_change_fn(cat_comp_ref, ar_comp, width_comp, height_comp, cn_types, cn_series, cn_filepaths, arch_comp_ref, ipa_preset, lora_acc, cn_acc, ipa_acc, sd3_ipa_acc, flux1_ipa_acc, style_acc, embed_acc, cond_acc, ref_latent_acc, hidream_o1_ref_acc, prompt_comp, neg_prompt_comp, steps_comp, cfg_comp, sampler_comp, scheduler_comp, pid_acc=None, vae_acc=None, joyai_ref_acc=None, krea2_identity_edit_acc=None, krea2_reference_edit_acc=None, qwen_image_edit_acc=None, ref_img_acc=None, sensenova_ref_acc=None, task_type=None, type_comp=None, qwen_pe_acc=None, ming_pe_acc=None, flux3_pe_acc=None, moss_image_control_acc=None, moss_image_edit_acc=None):
137
  def change_fn(*args):
138
  model_name = args[0]
139
  idx = 1
@@ -210,9 +212,11 @@ def make_model_change_fn(cat_comp_ref, ar_comp, width_comp, height_comp, cn_type
210
  if ref_img_acc: updates[ref_img_acc] = gr.update(visible=('reference_image' in enabled_chains))
211
  if pid_acc: updates[pid_acc] = gr.update(visible=('pid' in enabled_chains))
212
  if vae_acc: updates[vae_acc] = gr.update(visible=('vae' in enabled_chains))
213
- if qwen_pe_acc: updates[qwen_pe_acc] = gr.update(visible=('qwen_image_2_1_prompt_enhancer' in enabled_chains))
214
- if ming_pe_acc: updates[ming_pe_acc] = gr.update(visible=('ming_image_prompt_enhancer' in enabled_chains))
215
- if flux3_pe_acc: updates[flux3_pe_acc] = gr.update(visible=('flux3_prompt_upsampler' in enabled_chains))
 
 
216
  if moss_image_control_acc: updates[moss_image_control_acc] = gr.update(visible=('moss_image_control' in enabled_chains))
217
  if moss_image_edit_acc: updates[moss_image_edit_acc] = gr.update(visible=('moss_image_edit' in enabled_chains))
218
 
 
9
  ARCH_CATEGORIES_MAP
10
  )
11
  from utils.app_utils import get_model_generation_defaults
12
+ from ui.shared.ui_components import RESOLUTION_MAP, prompt_enhancer_defaults
13
  from .config_loaders import (
14
  get_cn_defaults,
15
  load_ipadapter_config
16
  )
17
 
18
+ def make_update_fn(m_comp, cat_comp, ar_comp, width_comp, height_comp, cn_types, cn_series, cn_filepaths, ipa_preset, lora_acc, cn_acc, ipa_acc, sd3_ipa_acc, flux1_ipa_acc, style_acc, embed_acc, cond_acc, ref_latent_acc, hidream_o1_ref_acc, prompt_comp, neg_prompt_comp, steps_comp, cfg_comp, sampler_comp, scheduler_comp, pid_acc=None, vae_acc=None, joyai_ref_acc=None, krea2_identity_edit_acc=None, krea2_reference_edit_acc=None, qwen_image_edit_acc=None, ref_img_acc=None, sensenova_ref_acc=None, task_type=None, type_comp=None, pe_acc=None, pe_model_acc=None, pe_prompt_acc=None, moss_image_control_acc=None, moss_image_edit_acc=None):
19
  def update_fn(*args):
20
  arch = args[0]
21
  category = args[1]
 
84
  if ref_img_acc: updates[ref_img_acc] = gr.update(visible=('reference_image' in enabled_chains))
85
  if pid_acc: updates[pid_acc] = gr.update(visible=('pid' in enabled_chains))
86
  if vae_acc: updates[vae_acc] = gr.update(visible=('vae' in enabled_chains))
87
+ if pe_acc: updates[pe_acc] = gr.update(visible=('prompt_enhancer' in enabled_chains))
88
+ if pe_model_acc or pe_prompt_acc:
89
+ pe_defaults = prompt_enhancer_defaults(arch_model_type)
90
+ if pe_model_acc: updates[pe_model_acc] = gr.update(value=pe_defaults['model'])
91
+ if pe_prompt_acc: updates[pe_prompt_acc] = gr.update(value=pe_defaults['system_prompt'])
92
  if moss_image_control_acc: updates[moss_image_control_acc] = gr.update(visible=('moss_image_control' in enabled_chains))
93
  if moss_image_edit_acc: updates[moss_image_edit_acc] = gr.update(visible=('moss_image_edit' in enabled_chains))
94
 
 
135
  return update_fn
136
 
137
 
138
+ def make_model_change_fn(cat_comp_ref, ar_comp, width_comp, height_comp, cn_types, cn_series, cn_filepaths, arch_comp_ref, ipa_preset, lora_acc, cn_acc, ipa_acc, sd3_ipa_acc, flux1_ipa_acc, style_acc, embed_acc, cond_acc, ref_latent_acc, hidream_o1_ref_acc, prompt_comp, neg_prompt_comp, steps_comp, cfg_comp, sampler_comp, scheduler_comp, pid_acc=None, vae_acc=None, joyai_ref_acc=None, krea2_identity_edit_acc=None, krea2_reference_edit_acc=None, qwen_image_edit_acc=None, ref_img_acc=None, sensenova_ref_acc=None, task_type=None, type_comp=None, pe_acc=None, pe_model_acc=None, pe_prompt_acc=None, moss_image_control_acc=None, moss_image_edit_acc=None):
139
  def change_fn(*args):
140
  model_name = args[0]
141
  idx = 1
 
212
  if ref_img_acc: updates[ref_img_acc] = gr.update(visible=('reference_image' in enabled_chains))
213
  if pid_acc: updates[pid_acc] = gr.update(visible=('pid' in enabled_chains))
214
  if vae_acc: updates[vae_acc] = gr.update(visible=('vae' in enabled_chains))
215
+ if pe_acc: updates[pe_acc] = gr.update(visible=('prompt_enhancer' in enabled_chains))
216
+ if pe_model_acc or pe_prompt_acc:
217
+ pe_defaults = prompt_enhancer_defaults(arch_model_type)
218
+ if pe_model_acc: updates[pe_model_acc] = gr.update(value=pe_defaults['model'])
219
+ if pe_prompt_acc: updates[pe_prompt_acc] = gr.update(value=pe_defaults['system_prompt'])
220
  if moss_image_control_acc: updates[moss_image_control_acc] = gr.update(visible=('moss_image_control' in enabled_chains))
221
  if moss_image_edit_acc: updates[moss_image_edit_acc] = gr.update(visible=('moss_image_edit' in enabled_chains))
222
 
ui/events/main.py CHANGED
@@ -63,9 +63,9 @@ def attach_event_handlers(ui_components, demo):
63
  ref_img_accordion = ui_components.get(f'reference_image_accordion_{prefix}')
64
  pid_accordion = ui_components.get(f'pid_accordion_{prefix}')
65
  vae_accordion = ui_components.get(f'vae_accordion_{prefix}')
66
- qwen_pe_accordion = ui_components.get(f'qwen_image_2_1_prompt_enhancer_accordion_{prefix}')
67
- ming_pe_accordion = ui_components.get(f'ming_image_prompt_enhancer_accordion_{prefix}')
68
- flux3_pe_accordion = ui_components.get(f'flux3_prompt_upsampler_accordion_{prefix}')
69
  moss_control_accordion = ui_components.get(f'moss_image_control_accordion_{prefix}')
70
  moss_edit_accordion = ui_components.get(f'moss_image_edit_accordion_{prefix}')
71
 
@@ -103,9 +103,9 @@ def attach_event_handlers(ui_components, demo):
103
  if ref_img_accordion: outputs.append(ref_img_accordion)
104
  if pid_accordion: outputs.append(pid_accordion)
105
  if vae_accordion: outputs.append(vae_accordion)
106
- if qwen_pe_accordion: outputs.append(qwen_pe_accordion)
107
- if ming_pe_accordion: outputs.append(ming_pe_accordion)
108
- if flux3_pe_accordion: outputs.append(flux3_pe_accordion)
109
  if moss_control_accordion: outputs.append(moss_control_accordion)
110
  if moss_edit_accordion: outputs.append(moss_edit_accordion)
111
  if ipa_preset_list: outputs.append(ipa_preset_list)
@@ -118,7 +118,7 @@ def attach_event_handlers(ui_components, demo):
118
  ipa_preset_list, lora_accordion, cn_accordion, ipa_accordion, sd3_ipa_accordion, flux1_ipa_accordion, style_accordion, embedding_accordion, conditioning_accordion,
119
  ref_latent_accordion, hidream_o1_ref_accordion, prompt_comp, neg_prompt_comp, steps_comp, cfg_comp, sampler_comp, scheduler_comp,
120
  pid_acc=pid_accordion, vae_acc=vae_accordion, joyai_ref_acc=joyai_ref_accordion, krea2_identity_edit_acc=krea2_identity_edit_accordion, krea2_reference_edit_acc=krea2_reference_edit_accordion, qwen_image_edit_acc=qwen_image_edit_accordion, ref_img_acc=ref_img_accordion, sensenova_ref_acc=sensenova_ref_accordion,
121
- task_type=task_type, type_comp=type_comp, qwen_pe_acc=qwen_pe_accordion, ming_pe_acc=ming_pe_accordion, flux3_pe_acc=flux3_pe_accordion,
122
  moss_image_control_acc=moss_control_accordion,
123
  moss_image_edit_acc=moss_edit_accordion
124
  )
@@ -154,9 +154,9 @@ def attach_event_handlers(ui_components, demo):
154
  if ref_img_accordion: outputs2.append(ref_img_accordion)
155
  if pid_accordion: outputs2.append(pid_accordion)
156
  if vae_accordion: outputs2.append(vae_accordion)
157
- if qwen_pe_accordion: outputs2.append(qwen_pe_accordion)
158
- if ming_pe_accordion: outputs2.append(ming_pe_accordion)
159
- if flux3_pe_accordion: outputs2.append(flux3_pe_accordion)
160
  if moss_control_accordion: outputs2.append(moss_control_accordion)
161
  if moss_edit_accordion: outputs2.append(moss_edit_accordion)
162
  if ipa_preset_list: outputs2.append(ipa_preset_list)
@@ -174,7 +174,7 @@ def attach_event_handlers(ui_components, demo):
174
  arch_comp, ipa_preset_list, lora_accordion, cn_accordion, ipa_accordion, sd3_ipa_accordion, flux1_ipa_accordion, style_accordion, embedding_accordion, conditioning_accordion,
175
  ref_latent_accordion, hidream_o1_ref_accordion, prompt_comp, neg_prompt_comp, steps_comp, cfg_comp, sampler_comp, scheduler_comp,
176
  pid_acc=pid_accordion, vae_acc=vae_accordion, joyai_ref_acc=joyai_ref_accordion, krea2_identity_edit_acc=krea2_identity_edit_accordion, krea2_reference_edit_acc=krea2_reference_edit_accordion, qwen_image_edit_acc=qwen_image_edit_accordion, ref_img_acc=ref_img_accordion, sensenova_ref_acc=sensenova_ref_accordion,
177
- task_type=task_type, type_comp=type_comp, qwen_pe_acc=qwen_pe_accordion, ming_pe_acc=ming_pe_accordion, flux3_pe_acc=flux3_pe_accordion,
178
  moss_image_control_acc=moss_control_accordion,
179
  moss_image_edit_acc=moss_edit_accordion
180
  )
@@ -265,9 +265,7 @@ def attach_event_handlers(ui_components, demo):
265
  f'krea2_reference_edit_accordion_{prefix}': 'krea2_style_reference',
266
  f'qwen_image_edit_accordion_{prefix}': 'qwen_image_edit',
267
  f'reference_image_accordion_{prefix}': 'reference_image',
268
- f'qwen_image_2_1_prompt_enhancer_accordion_{prefix}': 'qwen_image_2_1_prompt_enhancer',
269
- f'ming_image_prompt_enhancer_accordion_{prefix}': 'ming_image_prompt_enhancer',
270
- f'flux3_prompt_upsampler_accordion_{prefix}': 'flux3_prompt_upsampler',
271
  f'moss_image_control_accordion_{prefix}': 'moss_image_control',
272
  f'moss_image_edit_accordion_{prefix}': 'moss_image_edit',
273
  f'pid_accordion_{prefix}': 'pid',
@@ -307,9 +305,7 @@ def attach_event_handlers(ui_components, demo):
307
  'krea2_identity_edit_accordion_imagegen', 'krea2_reference_edit_accordion_imagegen',
308
  'qwen_image_edit_accordion_imagegen',
309
  'reference_image_accordion_imagegen',
310
- 'qwen_image_2_1_prompt_enhancer_accordion_imagegen',
311
- 'ming_image_prompt_enhancer_accordion_imagegen',
312
- 'flux3_prompt_upsampler_accordion_imagegen',
313
  'moss_image_control_accordion_imagegen',
314
  'moss_image_edit_accordion_imagegen',
315
  'pid_accordion_imagegen', 'vae_accordion_imagegen',
 
63
  ref_img_accordion = ui_components.get(f'reference_image_accordion_{prefix}')
64
  pid_accordion = ui_components.get(f'pid_accordion_{prefix}')
65
  vae_accordion = ui_components.get(f'vae_accordion_{prefix}')
66
+ pe_accordion = ui_components.get(f'prompt_enhancer_accordion_{prefix}')
67
+ pe_model_comp = ui_components.get(f'prompt_enhancer_model_{prefix}')
68
+ pe_prompt_comp = ui_components.get(f'prompt_enhancer_system_prompt_{prefix}')
69
  moss_control_accordion = ui_components.get(f'moss_image_control_accordion_{prefix}')
70
  moss_edit_accordion = ui_components.get(f'moss_image_edit_accordion_{prefix}')
71
 
 
103
  if ref_img_accordion: outputs.append(ref_img_accordion)
104
  if pid_accordion: outputs.append(pid_accordion)
105
  if vae_accordion: outputs.append(vae_accordion)
106
+ if pe_accordion: outputs.append(pe_accordion)
107
+ if pe_model_comp: outputs.append(pe_model_comp)
108
+ if pe_prompt_comp: outputs.append(pe_prompt_comp)
109
  if moss_control_accordion: outputs.append(moss_control_accordion)
110
  if moss_edit_accordion: outputs.append(moss_edit_accordion)
111
  if ipa_preset_list: outputs.append(ipa_preset_list)
 
118
  ipa_preset_list, lora_accordion, cn_accordion, ipa_accordion, sd3_ipa_accordion, flux1_ipa_accordion, style_accordion, embedding_accordion, conditioning_accordion,
119
  ref_latent_accordion, hidream_o1_ref_accordion, prompt_comp, neg_prompt_comp, steps_comp, cfg_comp, sampler_comp, scheduler_comp,
120
  pid_acc=pid_accordion, vae_acc=vae_accordion, joyai_ref_acc=joyai_ref_accordion, krea2_identity_edit_acc=krea2_identity_edit_accordion, krea2_reference_edit_acc=krea2_reference_edit_accordion, qwen_image_edit_acc=qwen_image_edit_accordion, ref_img_acc=ref_img_accordion, sensenova_ref_acc=sensenova_ref_accordion,
121
+ task_type=task_type, type_comp=type_comp, pe_acc=pe_accordion, pe_model_acc=pe_model_comp, pe_prompt_acc=pe_prompt_comp,
122
  moss_image_control_acc=moss_control_accordion,
123
  moss_image_edit_acc=moss_edit_accordion
124
  )
 
154
  if ref_img_accordion: outputs2.append(ref_img_accordion)
155
  if pid_accordion: outputs2.append(pid_accordion)
156
  if vae_accordion: outputs2.append(vae_accordion)
157
+ if pe_accordion: outputs2.append(pe_accordion)
158
+ if pe_model_comp: outputs2.append(pe_model_comp)
159
+ if pe_prompt_comp: outputs2.append(pe_prompt_comp)
160
  if moss_control_accordion: outputs2.append(moss_control_accordion)
161
  if moss_edit_accordion: outputs2.append(moss_edit_accordion)
162
  if ipa_preset_list: outputs2.append(ipa_preset_list)
 
174
  arch_comp, ipa_preset_list, lora_accordion, cn_accordion, ipa_accordion, sd3_ipa_accordion, flux1_ipa_accordion, style_accordion, embedding_accordion, conditioning_accordion,
175
  ref_latent_accordion, hidream_o1_ref_accordion, prompt_comp, neg_prompt_comp, steps_comp, cfg_comp, sampler_comp, scheduler_comp,
176
  pid_acc=pid_accordion, vae_acc=vae_accordion, joyai_ref_acc=joyai_ref_accordion, krea2_identity_edit_acc=krea2_identity_edit_accordion, krea2_reference_edit_acc=krea2_reference_edit_accordion, qwen_image_edit_acc=qwen_image_edit_accordion, ref_img_acc=ref_img_accordion, sensenova_ref_acc=sensenova_ref_accordion,
177
+ task_type=task_type, type_comp=type_comp, pe_acc=pe_accordion, pe_model_acc=pe_model_comp, pe_prompt_acc=pe_prompt_comp,
178
  moss_image_control_acc=moss_control_accordion,
179
  moss_image_edit_acc=moss_edit_accordion
180
  )
 
265
  f'krea2_reference_edit_accordion_{prefix}': 'krea2_style_reference',
266
  f'qwen_image_edit_accordion_{prefix}': 'qwen_image_edit',
267
  f'reference_image_accordion_{prefix}': 'reference_image',
268
+ f'prompt_enhancer_accordion_{prefix}': 'prompt_enhancer',
 
 
269
  f'moss_image_control_accordion_{prefix}': 'moss_image_control',
270
  f'moss_image_edit_accordion_{prefix}': 'moss_image_edit',
271
  f'pid_accordion_{prefix}': 'pid',
 
305
  'krea2_identity_edit_accordion_imagegen', 'krea2_reference_edit_accordion_imagegen',
306
  'qwen_image_edit_accordion_imagegen',
307
  'reference_image_accordion_imagegen',
308
+ 'prompt_enhancer_accordion_imagegen',
 
 
309
  'moss_image_control_accordion_imagegen',
310
  'moss_image_edit_accordion_imagegen',
311
  'pid_accordion_imagegen', 'vae_accordion_imagegen',
ui/events/run_handlers.py CHANGED
@@ -37,31 +37,16 @@ def create_run_event(prefix: str, task_type: str = None, ui_components: dict = N
37
  if ui_components.get(f'pid_settings_{prefix}'):
38
  run_inputs_map['pid_settings'] = ui_components[f'pid_settings_{prefix}']
39
 
40
- if ui_components.get(f'qwen_image_2_1_prompt_enhancer_enable_{prefix}'):
41
- run_inputs_map['qwen_image_2_1_prompt_enhancer_enable'] = ui_components[f'qwen_image_2_1_prompt_enhancer_enable_{prefix}']
42
- if ui_components.get(f'qwen_image_2_1_prompt_enhancer_thinking_{prefix}'):
43
- run_inputs_map['qwen_image_2_1_prompt_enhancer_thinking'] = ui_components[f'qwen_image_2_1_prompt_enhancer_thinking_{prefix}']
44
- if ui_components.get(f'qwen_image_2_1_prompt_enhancer_max_length_{prefix}'):
45
- run_inputs_map['qwen_image_2_1_prompt_enhancer_max_length'] = ui_components[f'qwen_image_2_1_prompt_enhancer_max_length_{prefix}']
46
-
47
- if ui_components.get(f'ming_image_prompt_enhancer_enable_{prefix}'):
48
- run_inputs_map['ming_image_prompt_enhancer_enable'] = ui_components[f'ming_image_prompt_enhancer_enable_{prefix}']
49
- if ui_components.get(f'ming_image_prompt_enhancer_thinking_{prefix}'):
50
- run_inputs_map['ming_image_prompt_enhancer_thinking'] = ui_components[f'ming_image_prompt_enhancer_thinking_{prefix}']
51
- if ui_components.get(f'ming_image_prompt_enhancer_max_length_{prefix}'):
52
- run_inputs_map['ming_image_prompt_enhancer_max_length'] = ui_components[f'ming_image_prompt_enhancer_max_length_{prefix}']
53
- if ui_components.get(f'ming_image_prompt_enhancer_system_prompt_{prefix}'):
54
- run_inputs_map['ming_image_prompt_enhancer_system_prompt'] = ui_components[f'ming_image_prompt_enhancer_system_prompt_{prefix}']
55
-
56
- if ui_components.get(f'flux3_prompt_upsampler_enable_{prefix}'):
57
- run_inputs_map['flux3_prompt_upsampler_enable'] = ui_components[f'flux3_prompt_upsampler_enable_{prefix}']
58
- if ui_components.get(f'flux3_prompt_upsampler_thinking_{prefix}'):
59
- run_inputs_map['flux3_prompt_upsampler_thinking'] = ui_components[f'flux3_prompt_upsampler_thinking_{prefix}']
60
- if ui_components.get(f'flux3_prompt_upsampler_bbox_mode_{prefix}'):
61
- run_inputs_map['flux3_prompt_upsampler_bbox_mode'] = ui_components[f'flux3_prompt_upsampler_bbox_mode_{prefix}']
62
- if ui_components.get(f'flux3_prompt_upsampler_max_length_{prefix}'):
63
- run_inputs_map['flux3_prompt_upsampler_max_length'] = ui_components[f'flux3_prompt_upsampler_max_length_{prefix}']
64
-
65
  if ui_components.get(f'moss_image_control_color_map_{prefix}'):
66
  run_inputs_map['moss_image_control_color_map'] = ui_components[f'moss_image_control_color_map_{prefix}']
67
  if ui_components.get(f'moss_image_control_depth_map_{prefix}'):
 
37
  if ui_components.get(f'pid_settings_{prefix}'):
38
  run_inputs_map['pid_settings'] = ui_components[f'pid_settings_{prefix}']
39
 
40
+ if ui_components.get(f'prompt_enhancer_enable_{prefix}'):
41
+ run_inputs_map['prompt_enhancer_enable'] = ui_components[f'prompt_enhancer_enable_{prefix}']
42
+ if ui_components.get(f'prompt_enhancer_thinking_{prefix}'):
43
+ run_inputs_map['prompt_enhancer_thinking'] = ui_components[f'prompt_enhancer_thinking_{prefix}']
44
+ if ui_components.get(f'prompt_enhancer_max_length_{prefix}'):
45
+ run_inputs_map['prompt_enhancer_max_length'] = ui_components[f'prompt_enhancer_max_length_{prefix}']
46
+ if ui_components.get(f'prompt_enhancer_model_{prefix}'):
47
+ run_inputs_map['prompt_enhancer_model'] = ui_components[f'prompt_enhancer_model_{prefix}']
48
+ if ui_components.get(f'prompt_enhancer_system_prompt_{prefix}'):
49
+ run_inputs_map['prompt_enhancer_system_prompt'] = ui_components[f'prompt_enhancer_system_prompt_{prefix}']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  if ui_components.get(f'moss_image_control_color_map_{prefix}'):
51
  run_inputs_map['moss_image_control_color_map'] = ui_components[f'moss_image_control_color_map_{prefix}']
52
  if ui_components.get(f'moss_image_control_depth_map_{prefix}'):
ui/imagegen_ui.py CHANGED
@@ -9,8 +9,7 @@ from .shared.ui_components import (
9
  create_model_architecture_filter_ui, create_category_filter_ui,
10
  create_sd3_ipadapter_ui, create_flux1_ipadapter_ui, create_style_ui,
11
  create_reference_latent_ui, create_hidream_o1_reference_ui, create_sensenova_reference_ui, create_joyai_reference_ui, create_reference_image_ui, create_krea2_identity_edit_ui, create_krea2_reference_edit_ui, create_qwen_image_edit_ui,
12
- create_pid_ui, create_qwen_image_2_1_prompt_enhancer_ui, create_ming_image_prompt_enhancer_ui,
13
- create_flux3_prompt_upsampler_ui, create_moss_image_control_ui,
14
  create_moss_image_edit_ui
15
  )
16
 
@@ -189,9 +188,7 @@ def create_ui():
189
  components.update(create_krea2_reference_edit_ui(prefix))
190
  components.update(create_qwen_image_edit_ui(prefix))
191
  components.update(create_reference_image_ui(prefix))
192
- components.update(create_qwen_image_2_1_prompt_enhancer_ui(prefix))
193
- components.update(create_ming_image_prompt_enhancer_ui(prefix))
194
- components.update(create_flux3_prompt_upsampler_ui(prefix))
195
  components.update(create_vae_override_ui(prefix))
196
  components.update(create_moss_image_control_ui(prefix))
197
  components.update(create_moss_image_edit_ui(prefix))
 
9
  create_model_architecture_filter_ui, create_category_filter_ui,
10
  create_sd3_ipadapter_ui, create_flux1_ipadapter_ui, create_style_ui,
11
  create_reference_latent_ui, create_hidream_o1_reference_ui, create_sensenova_reference_ui, create_joyai_reference_ui, create_reference_image_ui, create_krea2_identity_edit_ui, create_krea2_reference_edit_ui, create_qwen_image_edit_ui,
12
+ create_pid_ui, create_prompt_enhancer_ui, create_moss_image_control_ui,
 
13
  create_moss_image_edit_ui
14
  )
15
 
 
188
  components.update(create_krea2_reference_edit_ui(prefix))
189
  components.update(create_qwen_image_edit_ui(prefix))
190
  components.update(create_reference_image_ui(prefix))
191
+ components.update(create_prompt_enhancer_ui(prefix))
 
 
192
  components.update(create_vae_override_ui(prefix))
193
  components.update(create_moss_image_control_ui(prefix))
194
  components.update(create_moss_image_edit_ui(prefix))
ui/shared/ui_components.py CHANGED
@@ -1,947 +1,916 @@
1
- import gradio as gr
2
- from comfy_integration.nodes import SAMPLER_CHOICES, SCHEDULER_CHOICES
3
- from core.settings import (
4
- MAX_LORAS, LORA_SOURCE_CHOICES, MAX_EMBEDDINGS, MAX_CONDITIONINGS,
5
- MAX_CONTROLNETS, MAX_IPADAPTERS, MAX_MOSS_IMAGE_EDITS, RESOLUTION_MAP, ARCHITECTURES_CONFIG,
6
- MODEL_MAP_CHECKPOINT, MODEL_TYPE_MAP, FEATURES_CONFIG, ARCH_CATEGORIES_MAP,
7
- VAE_DIR, MODEL_DEFAULTS_CONFIG
8
- )
9
- import yaml
10
- import os
11
- from functools import lru_cache
12
- from utils.app_utils import save_uploaded_file
13
-
14
- default_model_name = list(MODEL_MAP_CHECKPOINT.keys())[0] if MODEL_MAP_CHECKPOINT else None
15
- default_m_type = MODEL_TYPE_MAP.get(default_model_name, "SDXL") if default_model_name else "SDXL"
16
- default_architectures_dict = ARCHITECTURES_CONFIG.get('architectures', {})
17
- default_arch_model_type = default_architectures_dict.get(default_m_type, {}).get("model_type", default_m_type.lower().replace(" ", "").replace(".", ""))
18
- default_arch_features = FEATURES_CONFIG.get(default_arch_model_type, {})
19
- default_enabled_chains = default_arch_features.get('enabled_chains', [])
20
-
21
- default_vals = MODEL_DEFAULTS_CONFIG.get('Default', {})
22
- DEFAULT_STEPS = default_vals.get('steps', 20)
23
- DEFAULT_CFG = default_vals.get('cfg', 5.0)
24
- DEFAULT_SAMPLER = default_vals.get('sampler_name', 'euler')
25
- DEFAULT_SCHEDULER = default_vals.get('scheduler', 'simple')
26
- DEFAULT_POS_PROMPT = default_vals.get('positive_prompt', '')
27
- DEFAULT_NEG_PROMPT = default_vals.get('negative_prompt', '')
28
-
29
- @lru_cache(maxsize=1)
30
- def get_ipadapter_config_from_yaml():
31
- try:
32
- _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
33
- _IPADAPTER_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'ipadapter.yaml')
34
- with open(_IPADAPTER_LIST_PATH, 'r', encoding='utf-8') as f:
35
- config = yaml.safe_load(f)
36
- return config
37
- except Exception as e:
38
- print(f"Warning: Could not load ipadapter.yaml for UI components: {e}")
39
- return {}
40
-
41
- def get_ipadapter_presets(arch="SDXL"):
42
- config = get_ipadapter_config_from_yaml()
43
- presets = []
44
- if config:
45
- std_presets = config.get("IPAdapter_presets", {}).get(arch, [])
46
- face_presets = config.get("IPAdapter_FaceID_presets", {}).get(arch, [])
47
- if std_presets:
48
- presets.extend(std_presets)
49
- if face_presets:
50
- presets.extend(face_presets)
51
- return presets if presets else ["STANDARD (medium strength)"]
52
-
53
- def create_model_architecture_filter_ui(prefix):
54
- components = {}
55
- ordered_architectures = ARCHITECTURES_CONFIG.get("architecture_order", [])
56
- choices = ["ALL"] + ordered_architectures
57
-
58
- components[f'model_arch_{prefix}'] = gr.Radio(
59
- label="Model Architecture",
60
- choices=choices,
61
- value="ALL",
62
- interactive=True,
63
- visible=True
64
- )
65
- return components
66
-
67
- def create_category_filter_ui(prefix):
68
- valid_cats = list(set(cat for cats in ARCH_CATEGORIES_MAP.values() for cat in cats))
69
- cat_choices = ["ALL"] + sorted(valid_cats)
70
-
71
- components = {}
72
- components[f'model_cat_{prefix}'] = gr.Dropdown(
73
- label="Filter Models",
74
- choices=cat_choices,
75
- value="ALL",
76
- interactive=True,
77
- scale=1,
78
- allow_custom_value=True
79
- )
80
- return components
81
-
82
- def create_base_parameter_ui(prefix, defaults=None):
83
- if defaults is None:
84
- defaults = {}
85
-
86
- components = {}
87
-
88
- with gr.Row():
89
- components[f'aspect_ratio_{prefix}'] = gr.Dropdown(
90
- label="Aspect Ratio",
91
- choices=list(RESOLUTION_MAP.get('sdxl', {}).keys()),
92
- value="1:1 (Square)",
93
- interactive=True,
94
- allow_custom_value=True
95
- )
96
- with gr.Row():
97
- components[f'width_{prefix}'] = gr.Number(label="Width", value=defaults.get('w', 1024), interactive=True)
98
- components[f'height_{prefix}'] = gr.Number(label="Height", value=defaults.get('h', 1024), interactive=True)
99
- with gr.Row():
100
- components[f'sampler_{prefix}'] = gr.Dropdown(
101
- label="Sampler",
102
- choices=SAMPLER_CHOICES,
103
- value=DEFAULT_SAMPLER if DEFAULT_SAMPLER in SAMPLER_CHOICES else (SAMPLER_CHOICES[0] if SAMPLER_CHOICES else 'euler')
104
- )
105
- components[f'scheduler_{prefix}'] = gr.Dropdown(
106
- label="Scheduler",
107
- choices=SCHEDULER_CHOICES,
108
- value=DEFAULT_SCHEDULER if DEFAULT_SCHEDULER in SCHEDULER_CHOICES else (SCHEDULER_CHOICES[0] if SCHEDULER_CHOICES else 'simple')
109
- )
110
- with gr.Row():
111
- components[f'steps_{prefix}'] = gr.Slider(label="Steps", minimum=1, maximum=100, step=1, value=DEFAULT_STEPS)
112
- components[f'cfg_{prefix}'] = gr.Slider(label="CFG Scale", minimum=1.0, maximum=20.0, step=0.1, value=DEFAULT_CFG)
113
- with gr.Row():
114
- components[f'seed_{prefix}'] = gr.Number(label="Seed (-1 for random)", value=-1, precision=0)
115
- components[f'batch_size_{prefix}'] = gr.Slider(label="Batch Size", minimum=1, maximum=16, step=1, value=1)
116
- with gr.Row():
117
- components[f'zero_gpu_{prefix}'] = gr.Number(label="ZeroGPU Duration (s)", value=60, placeholder="Default: 60s", info="Optional: Set how long to reserve the GPU.")
118
-
119
- return components
120
-
121
-
122
- def create_lora_settings_ui(prefix: str):
123
- components = {}
124
-
125
- lora_rows, lora_sources, lora_ids, lora_scales, lora_uploads = [], [], [], [], []
126
-
127
- with gr.Accordion("LoRA Settings", open=False, visible=('lora' in default_enabled_chains)) as lora_accordion:
128
- components[f'lora_accordion_{prefix}'] = lora_accordion
129
- gr.Markdown("💡 **Tip:** When downloading from Civitai, please use the **Version ID**, not the Model ID. You can find the Version ID in the URL (e.g., `civitai.com/models/123?modelVersionId=456`) or under the model's download button. When downloading from Hugging Face, please use the format: `repo_id/filename.extension` or `repo_id/folder_path/filename.extension` (e.g., `lightx2v/Qwen-Image-Lightning/Qwen-Image-Lightning-4steps-V2.0-bf16.safetensors`).")
130
- components[f'lora_count_state_{prefix}'] = gr.State(1)
131
-
132
- for i in range(MAX_LORAS):
133
- with gr.Row(visible=i==0) as row:
134
- source = gr.Dropdown(label=f"LoRA Source {i+1}", choices=LORA_SOURCE_CHOICES, value=LORA_SOURCE_CHOICES[0], scale=1)
135
- lora_id = gr.Textbox(label="Civitai Version ID / HF file / Upload File", scale=2, type="text")
136
- scale = gr.Slider(label=f"Scale", minimum=0.0, maximum=2.0, step=0.05, value=1.0, scale=1)
137
- upload = gr.UploadButton(label="Upload", file_types=[".safetensors"], scale=1)
138
-
139
- lora_rows.append(row)
140
- lora_sources.append(source)
141
- lora_ids.append(lora_id)
142
- lora_scales.append(scale)
143
- lora_uploads.append(upload)
144
-
145
- with gr.Row():
146
- components[f'add_lora_button_{prefix}'] = gr.Button("Add LoRA", variant="secondary")
147
- components[f'delete_lora_button_{prefix}'] = gr.Button("Remove LoRA", variant="secondary", visible=False)
148
-
149
- components[f'lora_rows_{prefix}'] = lora_rows
150
- components[f'lora_sources_{prefix}'] = lora_sources
151
- components[f'lora_ids_{prefix}'] = lora_ids
152
- components[f'lora_scales_{prefix}'] = lora_scales
153
- components[f'lora_uploads_{prefix}'] = lora_uploads
154
-
155
- all_lora_components_flat = []
156
- for i in range(MAX_LORAS):
157
- all_lora_components_flat.extend([lora_sources[i], lora_ids[i], lora_scales[i], lora_uploads[i]])
158
- components[f'all_lora_components_flat_{prefix}'] = all_lora_components_flat
159
-
160
- return components
161
-
162
- def create_controlnet_ui(prefix: str, max_units=MAX_CONTROLNETS):
163
- components = {}
164
- key = lambda name: f"{name}_{prefix}"
165
-
166
- with gr.Accordion("ControlNet Settings", open=False, visible=('controlnet' in default_enabled_chains)) as accordion:
167
- components[key('controlnet_accordion')] = accordion
168
-
169
- cn_rows, images, series, types, strengths, filepaths = [], [], [], [], [], []
170
- components.update({
171
- key('controlnet_rows'): cn_rows,
172
- key('controlnet_images'): images,
173
- key('controlnet_series'): series,
174
- key('controlnet_types'): types,
175
- key('controlnet_strengths'): strengths,
176
- key('controlnet_filepaths'): filepaths
177
- })
178
-
179
- for i in range(max_units):
180
- with gr.Row(visible=(i < 1)) as row:
181
- with gr.Column(scale=1):
182
- images.append(gr.Image(label=f"Control Image {i+1}", type="pil", sources=["upload"], height=256))
183
- with gr.Column(scale=2):
184
- types.append(gr.Dropdown(label="Type", choices=[], interactive=True, allow_custom_value=True))
185
- series.append(gr.Dropdown(label="Series", choices=[], interactive=True, allow_custom_value=True))
186
- strengths.append(gr.Slider(label="Strength", minimum=0.0, maximum=2.0, step=0.05, value=1.0, interactive=True))
187
- filepaths.append(gr.State(None))
188
- cn_rows.append(row)
189
-
190
- with gr.Row():
191
- components[key('add_controlnet_button')] = gr.Button("✚ Add ControlNet")
192
- components[key('delete_controlnet_button')] = gr.Button("➖ Delete ControlNet", visible=False)
193
- components[key('controlnet_count_state')] = gr.State(1)
194
-
195
- all_cn_components_flat = []
196
- for i in range(max_units):
197
- all_cn_components_flat.extend([
198
- images[i], types[i], series[i], strengths[i], filepaths[i]
199
- ])
200
- components[key('all_controlnet_components_flat')] = all_cn_components_flat
201
-
202
- return components
203
-
204
- def create_ipadapter_ui(prefix: str, max_units=MAX_IPADAPTERS):
205
- components = {}
206
- key = lambda name: f"{name}_{prefix}"
207
-
208
- sdxl_presets = get_ipadapter_presets("SDXL")
209
- default_preset = sdxl_presets[0] if sdxl_presets else None
210
-
211
- with gr.Accordion("IPAdapter Settings", open=False, visible=('ipadapter' in default_enabled_chains)) as accordion:
212
- components[key('ipadapter_accordion')] = accordion
213
- gr.Markdown("💡 **Tip:** Processed using the [cubiq/ComfyUI_IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) node.")
214
-
215
- with gr.Row():
216
- components[key('ipadapter_final_preset')] = gr.Dropdown(
217
- label="Preset (for all images)",
218
- choices=sdxl_presets,
219
- value=default_preset,
220
- interactive=True,
221
- allow_custom_value=True
222
- )
223
- components[key('ipadapter_embeds_scaling')] = gr.Dropdown(
224
- label="Embeds Scaling",
225
- choices=['V only', 'K+V', 'K+V w/ C penalty', 'K+mean(V) w/ C penalty'],
226
- value='V only',
227
- interactive=True
228
- )
229
-
230
- with gr.Row():
231
- components[key('ipadapter_combine_method')] = gr.Dropdown(
232
- label="Combine Method",
233
- choices=["concat", "add", "subtract", "average", "norm average", "max", "min"],
234
- value="concat",
235
- interactive=True
236
- )
237
- components[key('ipadapter_final_weight')] = gr.Slider(label="Final Weight", minimum=0.0, maximum=2.0, step=0.05, value=1.0, interactive=True)
238
- components[key('ipadapter_final_lora_strength')] = gr.Slider(label="Final LoRA Strength", minimum=0.0, maximum=2.0, step=0.05, value=0.6, interactive=True, visible=False)
239
-
240
- gr.Markdown("---")
241
-
242
- ipa_rows, images, weights, lora_strengths = [], [], [], []
243
- components.update({
244
- key('ipadapter_rows'): ipa_rows,
245
- key('ipadapter_images'): images,
246
- key('ipadapter_weights'): weights,
247
- key('ipadapter_lora_strengths'): lora_strengths
248
- })
249
-
250
- for i in range(max_units):
251
- with gr.Row(visible=(i < 1)) as row:
252
- with gr.Column(scale=1):
253
- images.append(gr.Image(label=f"IPAdapter Image {i+1}", type="pil", sources=["upload"], height=256))
254
- with gr.Column(scale=2):
255
- weights.append(gr.Slider(label="Weight", minimum=0.0, maximum=2.0, step=0.05, value=1.0, interactive=True))
256
- lora_strengths.append(gr.Slider(label="LoRA Strength", minimum=0.0, maximum=2.0, step=0.05, value=0.6, interactive=True, visible=False))
257
- ipa_rows.append(row)
258
-
259
- with gr.Row():
260
- components[key('add_ipadapter_button')] = gr.Button("✚ Add IPAdapter")
261
- components[key('delete_ipadapter_button')] = gr.Button("➖ Delete IPAdapter", visible=False)
262
- components[key('ipadapter_count_state')] = gr.State(1)
263
-
264
- all_ipa_components_flat = images + weights + lora_strengths
265
- all_ipa_components_flat += [
266
- components[key('ipadapter_final_preset')],
267
- components[key('ipadapter_final_weight')],
268
- components[key('ipadapter_final_lora_strength')],
269
- components[key('ipadapter_embeds_scaling')],
270
- components[key('ipadapter_combine_method')],
271
- ]
272
- components[key('all_ipadapter_components_flat')] = all_ipa_components_flat
273
-
274
- return components
275
-
276
- def create_flux1_ipadapter_ui(prefix: str, max_units=MAX_IPADAPTERS):
277
- components = {}
278
- key = lambda name: f"{name}_{prefix}"
279
-
280
- with gr.Accordion("IPAdapter Settings (FLUX.1)", open=False, visible=('flux1_ipadapter' in default_enabled_chains)) as accordion:
281
- components[key('flux1_ipadapter_accordion')] = accordion
282
- gr.Markdown("💡 **Tip:** Processed using the [Shakker-Labs/ComfyUI-IPAdapter-Flux](https://github.com/Shakker-Labs/ComfyUI-IPAdapter-Flux) node.")
283
-
284
- ipa_rows, images, weights, start_percents, end_percents = [], [], [], [], []
285
- components.update({
286
- key('flux1_ipadapter_rows'): ipa_rows,
287
- key('flux1_ipadapter_images'): images,
288
- key('flux1_ipadapter_weights'): weights,
289
- key('flux1_ipadapter_start_percents'): start_percents,
290
- key('flux1_ipadapter_end_percents'): end_percents,
291
- })
292
-
293
- for i in range(max_units):
294
- with gr.Row(visible=(i < 1)) as row:
295
- with gr.Column(scale=1):
296
- images.append(gr.Image(label=f"IPAdapter Image {i+1}", type="pil", sources=["upload"], height=256))
297
- with gr.Column(scale=2):
298
- weights.append(gr.Slider(label="Weight", minimum=0.0, maximum=2.0, step=0.05, value=0.6, interactive=True))
299
- with gr.Row():
300
- start_percents.append(gr.Slider(label="Start At", minimum=0.0, maximum=1.0, step=0.01, value=0.0, interactive=True))
301
- end_percents.append(gr.Slider(label="End At", minimum=0.0, maximum=1.0, step=0.01, value=0.6, interactive=True))
302
- ipa_rows.append(row)
303
-
304
- with gr.Row():
305
- components[key('add_flux1_ipadapter_button')] = gr.Button("✚ Add IPAdapter (FLUX)")
306
- components[key('delete_flux1_ipadapter_button')] = gr.Button("➖ Delete IPAdapter (FLUX)", visible=False)
307
- components[key('flux1_ipadapter_count_state')] = gr.State(1)
308
-
309
- all_flux1_ipa_components_flat = images + weights + start_percents + end_percents
310
- components[key('all_flux1_ipadapter_components_flat')] = all_flux1_ipa_components_flat
311
-
312
- return components
313
-
314
- def create_sd3_ipadapter_ui(prefix: str, max_units=MAX_IPADAPTERS):
315
- components = {}
316
- key = lambda name: f"{name}_{prefix}"
317
-
318
- with gr.Accordion("IPAdapter Settings (SD3)", open=False, visible=('sd3_ipadapter' in default_enabled_chains)) as accordion:
319
- components[key('sd3_ipadapter_accordion')] = accordion
320
- gr.Markdown("💡 **Tip:** Processed using the [Slickytail/ComfyUI-InstantX-IPAdapter-SD3](https://github.com/Slickytail/ComfyUI-InstantX-IPAdapter-SD3) node.")
321
-
322
- ipa_rows, images, weights, start_percents, end_percents = [], [], [], [], []
323
- components.update({
324
- key('sd3_ipadapter_rows'): ipa_rows,
325
- key('sd3_ipadapter_images'): images,
326
- key('sd3_ipadapter_weights'): weights,
327
- key('sd3_ipadapter_start_percents'): start_percents,
328
- key('sd3_ipadapter_end_percents'): end_percents,
329
- })
330
-
331
- for i in range(max_units):
332
- with gr.Row(visible=(i < 1)) as row:
333
- with gr.Column(scale=1):
334
- images.append(gr.Image(label=f"IPAdapter Image {i+1}", type="pil", sources=["upload"], height=256))
335
- with gr.Column(scale=2):
336
- weights.append(gr.Slider(label="Weight", minimum=0.0, maximum=2.0, step=0.05, value=0.5, interactive=True))
337
- with gr.Row():
338
- start_percents.append(gr.Slider(label="Start At", minimum=0.0, maximum=1.0, step=0.01, value=0.0, interactive=True))
339
- end_percents.append(gr.Slider(label="End At", minimum=0.0, maximum=1.0, step=0.01, value=1.0, interactive=True))
340
- ipa_rows.append(row)
341
-
342
- with gr.Row():
343
- components[key('add_sd3_ipadapter_button')] = gr.Button("✚ Add IPAdapter (SD3)")
344
- components[key('delete_sd3_ipadapter_button')] = gr.Button("➖ Delete IPAdapter (SD3)", visible=False)
345
- components[key('sd3_ipadapter_count_state')] = gr.State(1)
346
-
347
- all_sd3_ipa_components_flat = images + weights + start_percents + end_percents
348
- components[key('all_sd3_ipadapter_components_flat')] = all_sd3_ipa_components_flat
349
-
350
- return components
351
-
352
- def create_style_ui(prefix: str):
353
- components = {}
354
- key = lambda name: f"{name}_{prefix}"
355
-
356
- with gr.Accordion("Style Settings (FLUX.1)", open=False, visible=('style' in default_enabled_chains)) as accordion:
357
- components[key('style_accordion')] = accordion
358
-
359
- style_rows, images, strengths = [], [], []
360
- components.update({
361
- key('style_rows'): style_rows,
362
- key('style_images'): images,
363
- key('style_strengths'): strengths
364
- })
365
-
366
- for i in range(5):
367
- with gr.Row(visible=(i < 1)) as row:
368
- with gr.Column(scale=1):
369
- images.append(gr.Image(label=f"Style Image {i+1}", type="pil", sources=["upload"], height=256))
370
- with gr.Column(scale=2):
371
- strengths.append(gr.Slider(label="Strength", minimum=0.0, maximum=2.0, step=0.05, value=1.0, interactive=True))
372
- style_rows.append(row)
373
-
374
- with gr.Row():
375
- components[key('add_style_button')] = gr.Button("✚ Add Style (FLUX)")
376
- components[key('delete_style_button')] = gr.Button("➖ Delete Style (FLUX)", visible=False)
377
- components[key('style_count_state')] = gr.State(1)
378
-
379
- all_style_components_flat = images + strengths
380
- components[key('all_style_components_flat')] = all_style_components_flat
381
-
382
- return components
383
-
384
- def create_embedding_ui(prefix: str):
385
- components = {}
386
- key = lambda name: f"{name}_{prefix}"
387
-
388
- with gr.Accordion("Embedding Settings", open=False, visible=('embedding' in default_enabled_chains)) as accordion:
389
- components[key('embedding_accordion')] = accordion
390
- gr.Markdown("💡 **Tip:** When downloading from Civitai, please use the **Version ID**, not the Model ID. You can find the Version ID in the URL (e.g., `civitai.com/models/123?modelVersionId=456`) or under the model's download button. For example, entering the Version ID 456 will automatically save the file as \"civitai/456.safetensors\", and you will need to manually enter `embedding:civitai/456` in either your prompt or negative prompt to activate it. When downloading from Hugging Face, please use the format: repo_id/filename.extension or repo_id/folder_path/filename.extension (e.g., ilikebigturtles/lazypos/lazypos.safetensors or ilikebigturtles/lazyneg/lazyneg.safetensors). For Hugging Face files, you will need to enter embedding:huggingface/filename (e.g., entering embedding:huggingface/lazypos in your positive prompt, or embedding:huggingface/lazyneg in your negative prompt) to activate it. For uploaded files, enter `embedding:upload_file/filename` in either your positive prompt or negative prompt to activate it.")
391
-
392
- embedding_rows, sources, ids, files, upload_buttons = [], [], [], [], []
393
- components.update({
394
- key('embedding_rows'): embedding_rows,
395
- key('embeddings_sources'): sources,
396
- key('embeddings_ids'): ids,
397
- key('embeddings_files'): files,
398
- key('embeddings_uploads'): upload_buttons
399
- })
400
-
401
- for i in range(MAX_EMBEDDINGS):
402
- with gr.Row(visible=(i < 1)) as row:
403
- sources.append(gr.Dropdown(label=f"Embedding Source {i+1}", choices=LORA_SOURCE_CHOICES, value="Civitai", scale=1, interactive=True))
404
- ids.append(gr.Textbox(label="Civitai Version ID / HF file / Upload File", scale=3, interactive=True, type="text"))
405
- upload_btn = gr.UploadButton("Upload", file_types=[".safetensors"], scale=1)
406
- files.append(gr.State(None))
407
- upload_buttons.append(upload_btn)
408
- embedding_rows.append(row)
409
-
410
- with gr.Row():
411
- components[key('add_embedding_button')] = gr.Button("✚ Add Embedding")
412
- components[key('delete_embedding_button')] = gr.Button("➖ Delete Embedding", visible=False)
413
- components[key('embedding_count_state')] = gr.State(1)
414
-
415
- all_embedding_components_flat = []
416
- for i in range(MAX_EMBEDDINGS):
417
- all_embedding_components_flat.extend([sources[i], ids[i], files[i]])
418
- components[key('all_embedding_components_flat')] = all_embedding_components_flat
419
-
420
- return components
421
-
422
- def create_conditioning_ui(prefix: str):
423
- components = {}
424
- key = lambda name: f"{name}_{prefix}"
425
-
426
- with gr.Accordion("Conditioning Settings", open=False, visible=('conditioning' in default_enabled_chains)) as accordion:
427
- components[key('conditioning_accordion')] = accordion
428
- gr.Markdown("💡 **Tip:** Define rectangular areas and assign specific prompts to them. Coordinates (X, Y) start from the top-left corner.")
429
-
430
- cond_rows, prompts, widths, heights, xs, ys, strengths = [], [], [], [], [], [], []
431
- components.update({
432
- key('conditioning_rows'): cond_rows,
433
- key('conditioning_prompts'): prompts,
434
- key('conditioning_widths'): widths,
435
- key('conditioning_heights'): heights,
436
- key('conditioning_xs'): xs,
437
- key('conditioning_ys'): ys,
438
- key('conditioning_strengths'): strengths
439
- })
440
-
441
- for i in range(MAX_CONDITIONINGS):
442
- with gr.Column(visible=(i < 1)) as row_wrapper:
443
- prompts.append(gr.Textbox(label=f"Area Prompt {i+1}", lines=2, interactive=True))
444
- with gr.Row():
445
- xs.append(gr.Number(label="X", value=0, interactive=True, step=8, scale=1))
446
- ys.append(gr.Number(label="Y", value=0, interactive=True, step=8, scale=1))
447
- widths.append(gr.Number(label="Width", value=512, interactive=True, step=8, scale=1))
448
- heights.append(gr.Number(label="Height", value=512, interactive=True, step=8, scale=1))
449
- strengths.append(gr.Slider(label="Strength", minimum=0.1, maximum=2.0, step=0.05, value=1.0, interactive=True, scale=2))
450
- cond_rows.append(row_wrapper)
451
-
452
- with gr.Row():
453
- components[key('add_conditioning_button')] = gr.Button("✚ Add Area")
454
- components[key('delete_conditioning_button')] = gr.Button("➖ Delete Area", visible=False)
455
- components[key('conditioning_count_state')] = gr.State(1)
456
-
457
- all_cond_components_flat = prompts + widths + heights + xs + ys + strengths
458
- components[key('all_conditioning_components_flat')] = all_cond_components_flat
459
-
460
- return components
461
-
462
- def on_vae_upload(file_obj):
463
- if not file_obj:
464
- return gr.update(), gr.update(), None
465
-
466
- uploaded_filename = save_uploaded_file(file_obj, VAE_DIR)
467
- return uploaded_filename, "Upload File", file_obj
468
-
469
- def create_vae_override_ui(prefix: str):
470
- components = {}
471
- key = lambda name: f"{name}_{prefix}"
472
-
473
- with gr.Accordion("VAE Settings (Override)", open=False, visible=('vae' in default_enabled_chains)) as vae_accordion:
474
- components[key('vae_accordion')] = vae_accordion
475
- gr.Markdown("💡 **Tip:** When downloading from Civitai, please use the **Version ID**, not the Model ID. You can find the Version ID in the URL (e.g., `civitai.com/models/123?modelVersionId=456`) or under the model's download button. When downloading from Hugging Face, please use the format: `repo_id/filename.extension` or `repo_id/folder_path/filename.extension` (e.g., `madebyollin/sdxl-vae-fp16-fix/sdxl_vae.safetensors`).")
476
- with gr.Row():
477
- components[key('vae_source')] = gr.Dropdown(
478
- label="VAE Source",
479
- choices=LORA_SOURCE_CHOICES,
480
- value="Civitai",
481
- scale=1,
482
- interactive=True
483
- )
484
- components[key('vae_id')] = gr.Textbox(
485
- label="Civitai Version ID / HF file / Upload File",
486
- scale=3,
487
- interactive=True,
488
- type="text"
489
- )
490
- upload_btn = gr.UploadButton(
491
- "Upload",
492
- file_types=[".safetensors"],
493
- scale=1
494
- )
495
- components[key('vae_upload_button')] = upload_btn
496
- components[key('vae_file')] = gr.State(None)
497
-
498
- upload_btn.upload(
499
- fn=on_vae_upload,
500
- inputs=[upload_btn],
501
- outputs=[components[key('vae_id')], components[key('vae_source')], components[key('vae_file')]]
502
- )
503
-
504
- return components
505
-
506
- def create_reference_latent_ui(prefix: str, max_units=10):
507
- components = {}
508
- key = lambda name: f"{name}_{prefix}"
509
-
510
- with gr.Accordion("Reference Edit Settings", open=False, visible=('reference_latent' in default_enabled_chains)) as ref_accordion:
511
- components[key('reference_latent_accordion')] = ref_accordion
512
- gr.Markdown("💡 **Tip:** For multimodal models, this feature enables powerful editing and combining capabilities. In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
513
-
514
- ref_image_groups = []
515
- ref_image_inputs = []
516
- with gr.Row():
517
- for i in range(max_units):
518
- with gr.Column(visible=(i < 1), min_width=160) as img_col:
519
- img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
520
- ref_image_groups.append(img_col)
521
- ref_image_inputs.append(img_comp)
522
-
523
- components[key('reference_latent_rows')] = ref_image_groups
524
- components[key('reference_latent_images')] = ref_image_inputs
525
-
526
- with gr.Row():
527
- components[key('add_reference_latent_button')] = gr.Button("✚ Add Reference Image")
528
- components[key('delete_reference_latent_button')] = gr.Button("➖ Delete Reference Image", visible=False)
529
- components[key('reference_latent_count_state')] = gr.State(1)
530
-
531
- components[key('all_reference_latent_components_flat')] = ref_image_inputs
532
-
533
- return components
534
-
535
- def create_hidream_o1_reference_ui(prefix: str, max_units=10):
536
- components = {}
537
- key = lambda name: f"{name}_{prefix}"
538
-
539
- with gr.Accordion("HiDream-O1 Reference Edit Settings", open=False, visible=('hidream_o1_reference' in default_enabled_chains)) as ref_accordion:
540
- components[key('hidream_o1_reference_accordion')] = ref_accordion
541
- gr.Markdown("💡 **Tip:** Please use **HiDream-O1-Image-Dev** (HiDream-O1-Image will time out), and set the resolution to **4.0MP** (e.g., 2048x2048). In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
542
-
543
- ref_image_groups = []
544
- ref_image_inputs = []
545
- with gr.Row():
546
- for i in range(max_units):
547
- with gr.Column(visible=(i < 1), min_width=160) as img_col:
548
- img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
549
- ref_image_groups.append(img_col)
550
- ref_image_inputs.append(img_comp)
551
-
552
- components[key('hidream_o1_reference_rows')] = ref_image_groups
553
- components[key('hidream_o1_reference_images')] = ref_image_inputs
554
-
555
- with gr.Row():
556
- components[key('add_hidream_o1_reference_button')] = gr.Button("✚ Add Reference Image")
557
- components[key('delete_hidream_o1_reference_button')] = gr.Button("➖ Delete Reference Image", visible=False)
558
- components[key('hidream_o1_reference_count_state')] = gr.State(1)
559
-
560
- components[key('all_hidream_o1_reference_components_flat')] = ref_image_inputs
561
-
562
- return components
563
-
564
- def create_sensenova_reference_ui(prefix: str, max_units=10):
565
- components = {}
566
- key = lambda name: f"{name}_{prefix}"
567
-
568
- with gr.Accordion("SenseNova Reference Edit Settings", open=False, visible=('sensenova_reference' in default_enabled_chains)) as ref_accordion:
569
- components[key('sensenova_reference_accordion')] = ref_accordion
570
- gr.Markdown("💡 **Tip:** For SenseNova models, this feature enables reference image editing and combining capabilities. In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
571
-
572
- ref_image_groups = []
573
- ref_image_inputs = []
574
- with gr.Row():
575
- for i in range(max_units):
576
- with gr.Column(visible=(i < 1), min_width=160) as img_col:
577
- img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
578
- ref_image_groups.append(img_col)
579
- ref_image_inputs.append(img_comp)
580
-
581
- components[key('sensenova_reference_rows')] = ref_image_groups
582
- components[key('sensenova_reference_images')] = ref_image_inputs
583
-
584
- with gr.Row():
585
- components[key('add_sensenova_reference_button')] = gr.Button("✚ Add Reference Image")
586
- components[key('delete_sensenova_reference_button')] = gr.Button("➖ Delete Reference Image", visible=False)
587
- components[key('sensenova_reference_count_state')] = gr.State(1)
588
-
589
- components[key('all_sensenova_reference_components_flat')] = ref_image_inputs
590
-
591
- return components
592
-
593
- def create_joyai_reference_ui(prefix: str, max_units=2):
594
- components = {}
595
- key = lambda name: f"{name}_{prefix}"
596
-
597
- with gr.Accordion("JoyAI Reference Edit Settings", open=False, visible=('joyai_image' in default_enabled_chains)) as ref_accordion:
598
- components[key('joyai_reference_accordion')] = ref_accordion
599
- gr.Markdown("💡 **Tip:** For multimodal models, this feature enables powerful editing and combining capabilities. In txt2img mode, adding a single reference image performs an **Image Edit** (JoyAI-Image-Edit recommended), while adding multiple images performs an **Image Combine** (JoyAI-Image-Edit-Plus recommended with ZeroGPU Duration (s) set to 120).")
600
-
601
- ref_image_groups = []
602
- ref_image_inputs = []
603
- with gr.Row():
604
- for i in range(max_units):
605
- with gr.Column(visible=(i < 1), min_width=160) as img_col:
606
- img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
607
- ref_image_groups.append(img_col)
608
- ref_image_inputs.append(img_comp)
609
-
610
- components[key('joyai_reference_rows')] = ref_image_groups
611
- components[key('joyai_reference_images')] = ref_image_inputs
612
-
613
- with gr.Row():
614
- components[key('add_joyai_reference_button')] = gr.Button("✚ Add Reference Image")
615
- components[key('delete_joyai_reference_button')] = gr.Button("➖ Delete Reference Image", visible=False)
616
- components[key('joyai_reference_count_state')] = gr.State(1)
617
-
618
- components[key('all_joyai_reference_components_flat')] = ref_image_inputs
619
-
620
- return components
621
-
622
- def create_reference_image_ui(prefix: str, max_units=10):
623
- components = {}
624
- key = lambda name: f"{name}_{prefix}"
625
-
626
- with gr.Accordion("Reference Image Edit Settings", open=False, visible=('reference_image' in default_enabled_chains)) as ref_accordion:
627
- components[key('reference_image_accordion')] = ref_accordion
628
- gr.Markdown("💡 **Tip:** For multimodal models, this feature enables powerful editing and combining capabilities. In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
629
-
630
- ref_image_groups = []
631
- ref_image_inputs = []
632
- with gr.Row():
633
- for i in range(max_units):
634
- with gr.Column(visible=(i < 1), min_width=160) as img_col:
635
- img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
636
- ref_image_groups.append(img_col)
637
- ref_image_inputs.append(img_comp)
638
-
639
- components[key('reference_image_rows')] = ref_image_groups
640
- components[key('reference_image_images')] = ref_image_inputs
641
-
642
- with gr.Row():
643
- components[key('add_reference_image_button')] = gr.Button("✚ Add Reference Image")
644
- components[key('delete_reference_image_button')] = gr.Button("➖ Delete Reference Image", visible=False)
645
- components[key('reference_image_count_state')] = gr.State(1)
646
-
647
- components[key('all_reference_image_components_flat')] = ref_image_inputs
648
-
649
- return components
650
- def create_moss_image_control_ui(prefix: str):
651
- components = {}
652
- key = lambda name: f"{name}_{prefix}"
653
-
654
- with gr.Accordion("MOSS-Image Color/Depth Control Settings", open=False, visible=('moss_image_control' in default_enabled_chains)) as control_accordion:
655
- components[key('moss_image_control_accordion')] = control_accordion
656
- gr.Markdown("💡 **Tip:** Uses the [darask0/MOSS-Image-color-depth-control](https://huggingface.co/darask0/MOSS-Image-color-depth-control) adapter.")
657
- with gr.Row():
658
- components[key('moss_image_control_color_map')] = gr.Image(
659
- type="pil", label="Color Region Map (Optional)", sources=["upload"], height=200
660
- )
661
- components[key('moss_image_control_depth_map')] = gr.Image(
662
- type="pil", label="Depth Map (Optional, near=white)", sources=["upload"], height=200
663
- )
664
-
665
- with gr.Row():
666
- components[key('moss_image_control_strength')] = gr.Slider(
667
- label="Control Strength",
668
- value=1.0,
669
- minimum=0.0,
670
- maximum=2.0,
671
- step=0.05,
672
- interactive=True
673
- )
674
- components[key('moss_image_control_end_percent')] = gr.Slider(
675
- label="End Percent (0.5 for 3D mannequin)",
676
- value=0.5,
677
- minimum=0.0,
678
- maximum=1.0,
679
- step=0.01,
680
- interactive=True
681
- )
682
-
683
- return components
684
-
685
-
686
- def create_pid_ui(prefix: str):
687
- components = {}
688
- key = lambda name: f"{name}_{prefix}"
689
-
690
- with gr.Accordion("PiD Settings", open=False, visible=('pid' in default_enabled_chains)) as pid_accordion:
691
- components[key('pid_accordion')] = pid_accordion
692
- gr.Markdown("💡 **Tip:** Use PiD (Pixel Diffusion Decoder) instead of the VAE Decoder for 4x decoding.")
693
- with gr.Row():
694
- components[key('pid_settings')] = gr.Dropdown(
695
- label="PiD Mode",
696
- choices=["OFF", "ON"],
697
- value="OFF",
698
- interactive=True
699
- )
700
-
701
- return components
702
-
703
- def create_krea2_identity_edit_ui(prefix: str, max_units=2):
704
- components = {}
705
- key = lambda name: f"{name}_{prefix}"
706
-
707
- with gr.Accordion("Krea2 Identity Edit Settings", open=False, visible=('krea2_identity_edit' in default_enabled_chains)) as ref_accordion:
708
- components[key('krea2_identity_edit_accordion')] = ref_accordion
709
- gr.Markdown("💡 **Tip:** Processed using the [lbouaraba/comfyui-krea2edit](https://github.com/lbouaraba/comfyui-krea2edit) node. (Krea-2-Turbo recommended, Krea-2-Raw need set ZeroGPU Duration (s) to 120 ) In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
710
-
711
- ref_image_groups = []
712
- ref_image_inputs = []
713
- with gr.Row():
714
- for i in range(max_units):
715
- with gr.Column(visible=(i < 1), min_width=160) as img_col:
716
- img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
717
- ref_image_groups.append(img_col)
718
- ref_image_inputs.append(img_comp)
719
-
720
- components[key('krea2_identity_edit_rows')] = ref_image_groups
721
- components[key('krea2_identity_edit_images')] = ref_image_inputs
722
-
723
- with gr.Row():
724
- components[key('add_krea2_identity_edit_button')] = gr.Button("✚ Add Reference Image")
725
- components[key('delete_krea2_identity_edit_button')] = gr.Button("➖ Delete Reference Image", visible=False)
726
- components[key('krea2_identity_edit_count_state')] = gr.State(1)
727
-
728
- components[key('all_krea2_identity_edit_components_flat')] = ref_image_inputs
729
-
730
- return components
731
-
732
- def create_qwen_image_edit_ui(prefix: str, max_units=3):
733
- components = {}
734
- key = lambda name: f"{name}_{prefix}"
735
-
736
- with gr.Accordion("Qwen-Image Edit Settings", open=False, visible=('qwen_image_edit' in default_enabled_chains)) as ref_accordion:
737
- components[key('qwen_image_edit_accordion')] = ref_accordion
738
- gr.Markdown("💡 **Tip:** (lightx2v/Qwen-Image-Edit-2511-Lightning recommended) In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
739
-
740
- ref_image_groups = []
741
- ref_image_inputs = []
742
- with gr.Row():
743
- for i in range(max_units):
744
- with gr.Column(visible=(i < 1), min_width=160) as img_col:
745
- img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
746
- ref_image_groups.append(img_col)
747
- ref_image_inputs.append(img_comp)
748
-
749
- components[key('qwen_image_edit_rows')] = ref_image_groups
750
- components[key('qwen_image_edit_images')] = ref_image_inputs
751
-
752
- with gr.Row():
753
- components[key('add_qwen_image_edit_button')] = gr.Button("✚ Add Reference Image")
754
- components[key('delete_qwen_image_edit_button')] = gr.Button("➖ Delete Reference Image", visible=False)
755
- components[key('qwen_image_edit_count_state')] = gr.State(1)
756
-
757
- components[key('all_qwen_image_edit_components_flat')] = ref_image_inputs
758
-
759
- return components
760
-
761
- def create_krea2_reference_edit_ui(prefix: str, max_units=3):
762
- components = {}
763
- key = lambda name: f"{name}_{prefix}"
764
-
765
- with gr.Accordion("Krea2 Style Reference Edit Settings", open=False, visible=('krea2_style_reference' in default_enabled_chains)) as ref_accordion:
766
- components[key('krea2_reference_edit_accordion')] = ref_accordion
767
- gr.Markdown("💡 **Tip:** (Krea-2-Turbo recommended) Add style reference images to perform style reference editing.")
768
-
769
- ref_image_groups = []
770
- ref_image_inputs = []
771
- with gr.Row():
772
- for i in range(max_units):
773
- with gr.Column(visible=(i < 1), min_width=160) as img_col:
774
- img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
775
- ref_image_groups.append(img_col)
776
- ref_image_inputs.append(img_comp)
777
-
778
- components[key('krea2_reference_edit_rows')] = ref_image_groups
779
- components[key('krea2_reference_edit_images')] = ref_image_inputs
780
-
781
- with gr.Row():
782
- components[key('add_krea2_reference_edit_button')] = gr.Button("✚ Add Reference Image")
783
- components[key('delete_krea2_reference_edit_button')] = gr.Button("➖ Delete Reference Image", visible=False)
784
- components[key('krea2_reference_edit_count_state')] = gr.State(1)
785
-
786
- components[key('all_krea2_reference_edit_components_flat')] = ref_image_inputs
787
-
788
- return components
789
-
790
-
791
- def create_qwen_image_2_1_prompt_enhancer_ui(prefix: str):
792
- components = {}
793
- key = lambda name: f"{name}_{prefix}"
794
-
795
- with gr.Accordion("Qwen-Image-2.1 Prompt Enhancer Settings", open=False, visible=('qwen_image_2_1_prompt_enhancer' in default_enabled_chains)) as enhancer_accordion:
796
- components[key('qwen_image_2_1_prompt_enhancer_accordion')] = enhancer_accordion
797
- gr.Markdown("💡 **Tip:** Automatically selects between [Qwen-Image-2.1-PE-T2I](https://huggingface.co/Qwen/Qwen-Image-2.1-PE-T2I) and [Qwen-Image-2.1-PE-I2I](https://huggingface.co/Qwen/Qwen-Image-2.1-PE-I2I) for prompt enhancement based on input context.")
798
- with gr.Row():
799
- components[key('qwen_image_2_1_prompt_enhancer_enable')] = gr.Checkbox(
800
- label="Enable Qwen-Image-2.1 Prompt Enhancer",
801
- value=False,
802
- interactive=True,
803
- scale=2
804
- )
805
- components[key('qwen_image_2_1_prompt_enhancer_thinking')] = gr.Checkbox(
806
- label="Enable Thinking (Requires longer ZeroGPU Duration)",
807
- value=False,
808
- interactive=True,
809
- scale=2
810
- )
811
- components[key('qwen_image_2_1_prompt_enhancer_max_length')] = gr.Number(
812
- label="Max Length",
813
- value=4096,
814
- precision=0,
815
- minimum=16,
816
- step=16,
817
- interactive=True,
818
- scale=1
819
- )
820
-
821
- return components
822
-
823
-
824
- DEFAULT_MING_IMAGE_SYSTEM_PROMPT = (
825
- "You are a senior visual designer and image-prompt engineer. Expand the user's request into one precise, high-resolution Figma-style caption. Return only one JSON object. "
826
- "Use exactly two top-level keys. `canvas_settings` contains exactly `aspect_ratio`, `ambient_lighting`, and `image_style`. `layers` lists visible groups from background to topmost overlay. Every layer contains exactly `description`, `coordinates`, `hierarchy_and_relation`, and `color_specs`; `color_specs` is an array of hex colors. "
827
- '`coordinates` MUST be one string, never an object or array, in exactly this form: `"cx: 0.500, cy: 0.500, w: 1.000, h: 1.000"`. Values are normalized; each bbox encloses its complete owned object and stays inside the canvas. '
828
- "A layer is one selectable visible semantic group: background, full person, coherent object, panel, card, row, or text block. Prefer the fewest groups that preserve the layout. Keep people and objects intact. Never create invisible parents, guides, placeholders, empty layers, duplicate summaries, or multiple owners for one element. "
829
- "Preserve every user-supplied rendered string character-for-character and as one contiguous string. Unless multiple visible copies are requested, it must occur exactly once across all `description` fields and zero times in `hierarchy_and_relation`. Quote it only where describing its visible rendering; refer to the related subject elsewhere with unquoted semantic wording. Enumerate intended copy, invent extra copy sparingly, and never hide content behind \"other text\", \"remaining labels\", or \"etc.\" "
830
- "Describe concrete composition, typography, materials, texture, lighting, pose, and camera treatment without literary filler. Use `hierarchy_and_relation` only for ownership, alignment, containment, stacking, and occlusion. "
831
- "Infer structured layouts first. Use one complete layer per card and state its row and column. A compact secondary table may be one layer only if every header and cell is listed; otherwise use a visible shared frame when present, one complete header, and one complete layer per body row, binding values to columns and stating blanks. Enumerate sequences, schedules, spans, gaps, and vacant tracks in visual order. Do not mistake ordinary alignment for a table. "
832
- "Silently verify schema, string coordinates, Z-order, exact-text counts, geometry, bbox validity, and completeness."
833
- )
834
-
835
-
836
- def create_ming_image_prompt_enhancer_ui(prefix: str):
837
- components = {}
838
- key = lambda name: f"{name}_{prefix}"
839
-
840
- with gr.Accordion("Ming-Image Prompt Enhancer Settings", open=False, visible=('ming_image_prompt_enhancer' in default_enabled_chains)) as enhancer_accordion:
841
- components[key('ming_image_prompt_enhancer_accordion')] = enhancer_accordion
842
- gr.Markdown("💡 **Tip:** Leverages [Qwen3.8-27B](https://huggingface.co/Qwen/Qwen3.8-27B) guided by the official [System Prompt](https://github.com/inclusionAI/Ming-Image#text-to-image-prompt-rewriting) to automatically enhance prompts into structured layouts based on input context.")
843
- with gr.Row():
844
- components[key('ming_image_prompt_enhancer_enable')] = gr.Checkbox(
845
- label="Enable Ming-Image Prompt Enhancer",
846
- value=False,
847
- interactive=True,
848
- scale=2
849
- )
850
- components[key('ming_image_prompt_enhancer_thinking')] = gr.Checkbox(
851
- label="Enable Thinking (Requires longer ZeroGPU Duration)",
852
- value=False,
853
- interactive=True,
854
- scale=2
855
- )
856
- components[key('ming_image_prompt_enhancer_max_length')] = gr.Number(
857
- label="Max Length",
858
- value=4096,
859
- precision=0,
860
- minimum=16,
861
- step=16,
862
- interactive=True,
863
- scale=1
864
- )
865
- components[key('ming_image_prompt_enhancer_system_prompt')] = gr.Textbox(
866
- label="System Prompt",
867
- value=DEFAULT_MING_IMAGE_SYSTEM_PROMPT,
868
- lines=4,
869
- max_lines=12,
870
- interactive=True
871
- )
872
-
873
- return components
874
-
875
-
876
- def create_flux3_prompt_upsampler_ui(prefix: str):
877
- components = {}
878
- key = lambda name: f"{name}_{prefix}"
879
-
880
- with gr.Accordion("FLUX.3 Prompt Upsampler Settings", open=False, visible=('flux3_prompt_upsampler' in default_enabled_chains)) as enhancer_accordion:
881
- components[key('flux3_prompt_upsampler_accordion')] = enhancer_accordion
882
- gr.Markdown(
883
- "💡 **Tip:** System prompt automatically matches the official [black-forest-labs/flux-3-image-klein-pup](https://huggingface.co/black-forest-labs/flux-3-image-klein-pup/blob/main/system_prompts.json) templates: uses `t2i` when no image is provided, and automatically switches to `i2i` when image input is detected; enabling **BBox Mode** switches respectively to `t2i_bbox` and `i2i_bbox` to output target bounding box annotations."
884
- )
885
- with gr.Row():
886
- components[key('flux3_prompt_upsampler_enable')] = gr.Checkbox(
887
- label="Enable FLUX.3 Prompt Upsampler",
888
- value=False,
889
- interactive=True,
890
- scale=2
891
- )
892
- components[key('flux3_prompt_upsampler_thinking')] = gr.Checkbox(
893
- label="Enable Thinking (Requires longer ZeroGPU Duration)",
894
- value=False,
895
- interactive=True,
896
- scale=2
897
- )
898
- components[key('flux3_prompt_upsampler_bbox_mode')] = gr.Checkbox(
899
- label="BBox Mode",
900
- value=False,
901
- interactive=True,
902
- scale=1
903
- )
904
- components[key('flux3_prompt_upsampler_max_length')] = gr.Number(
905
- label="Max Length",
906
- value=3584,
907
- precision=0,
908
- minimum=16,
909
- step=16,
910
- interactive=True,
911
- scale=1
912
- )
913
-
914
- return components
915
-
916
-
917
- def create_moss_image_edit_ui(prefix: str, max_units=None):
918
- if max_units is None:
919
- max_units = MAX_MOSS_IMAGE_EDITS
920
- components = {}
921
- key = lambda name: f"{name}_{prefix}"
922
-
923
- with gr.Accordion("MOSS-Image Edit Settings", open=False, visible=('moss_image_edit' in default_enabled_chains)) as ref_accordion:
924
- components[key('moss_image_edit_accordion')] = ref_accordion
925
- gr.Markdown("💡 **Tip:** For multimodal models, this feature enables powerful editing and combining capabilities. In txt2img mode, adding a single reference image performs an Image Edit, while adding multiple images performs an Image Combine. Uses [chinoll/Moss-Image-Edit](https://huggingface.co/chinoll/Moss-Image-Edit).")
926
-
927
- ref_image_groups = []
928
- ref_image_inputs = []
929
- with gr.Row():
930
- for i in range(max_units):
931
- with gr.Column(visible=(i < 1), min_width=160) as img_col:
932
- img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
933
- ref_image_groups.append(img_col)
934
- ref_image_inputs.append(img_comp)
935
-
936
- components[key('moss_image_edit_rows')] = ref_image_groups
937
- components[key('moss_image_edit_images')] = ref_image_inputs
938
-
939
- with gr.Row():
940
- components[key('add_moss_image_edit_button')] = gr.Button("✚ Add Reference Image")
941
- components[key('delete_moss_image_edit_button')] = gr.Button("➖ Delete Reference Image", visible=False)
942
- components[key('moss_image_edit_count_state')] = gr.State(1)
943
-
944
- components[key('all_moss_image_edit_components_flat')] = ref_image_inputs
945
-
946
- return components
947
-
 
1
+ import gradio as gr
2
+ from comfy_integration.nodes import SAMPLER_CHOICES, SCHEDULER_CHOICES
3
+ from core.settings import (
4
+ MAX_LORAS, LORA_SOURCE_CHOICES, MAX_EMBEDDINGS, MAX_CONDITIONINGS,
5
+ MAX_CONTROLNETS, MAX_IPADAPTERS, MAX_MOSS_IMAGE_EDITS, RESOLUTION_MAP, ARCHITECTURES_CONFIG,
6
+ MODEL_MAP_CHECKPOINT, MODEL_TYPE_MAP, FEATURES_CONFIG, ARCH_CATEGORIES_MAP,
7
+ VAE_DIR, MODEL_DEFAULTS_CONFIG
8
+ )
9
+ import yaml
10
+ import os
11
+ from functools import lru_cache
12
+ from utils.app_utils import save_uploaded_file
13
+
14
+ default_model_name = list(MODEL_MAP_CHECKPOINT.keys())[0] if MODEL_MAP_CHECKPOINT else None
15
+ default_m_type = MODEL_TYPE_MAP.get(default_model_name, "SDXL") if default_model_name else "SDXL"
16
+ default_architectures_dict = ARCHITECTURES_CONFIG.get('architectures', {})
17
+ default_arch_model_type = default_architectures_dict.get(default_m_type, {}).get("model_type", default_m_type.lower().replace(" ", "").replace(".", ""))
18
+ default_arch_features = FEATURES_CONFIG.get(default_arch_model_type, {})
19
+ default_enabled_chains = default_arch_features.get('enabled_chains', [])
20
+
21
+ default_vals = MODEL_DEFAULTS_CONFIG.get('Default', {})
22
+ DEFAULT_STEPS = default_vals.get('steps', 20)
23
+ DEFAULT_CFG = default_vals.get('cfg', 5.0)
24
+ DEFAULT_SAMPLER = default_vals.get('sampler_name', 'euler')
25
+ DEFAULT_SCHEDULER = default_vals.get('scheduler', 'simple')
26
+ DEFAULT_POS_PROMPT = default_vals.get('positive_prompt', '')
27
+ DEFAULT_NEG_PROMPT = default_vals.get('negative_prompt', '')
28
+
29
+ @lru_cache(maxsize=1)
30
+ def get_ipadapter_config_from_yaml():
31
+ try:
32
+ _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
33
+ _IPADAPTER_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'ipadapter.yaml')
34
+ with open(_IPADAPTER_LIST_PATH, 'r', encoding='utf-8') as f:
35
+ config = yaml.safe_load(f)
36
+ return config
37
+ except Exception as e:
38
+ print(f"Warning: Could not load ipadapter.yaml for UI components: {e}")
39
+ return {}
40
+
41
+ def get_ipadapter_presets(arch="SDXL"):
42
+ config = get_ipadapter_config_from_yaml()
43
+ presets = []
44
+ if config:
45
+ std_presets = config.get("IPAdapter_presets", {}).get(arch, [])
46
+ face_presets = config.get("IPAdapter_FaceID_presets", {}).get(arch, [])
47
+ if std_presets:
48
+ presets.extend(std_presets)
49
+ if face_presets:
50
+ presets.extend(face_presets)
51
+ return presets if presets else ["STANDARD (medium strength)"]
52
+
53
+ def create_model_architecture_filter_ui(prefix):
54
+ components = {}
55
+ ordered_architectures = ARCHITECTURES_CONFIG.get("architecture_order", [])
56
+ choices = ["ALL"] + ordered_architectures
57
+
58
+ components[f'model_arch_{prefix}'] = gr.Radio(
59
+ label="Model Architecture",
60
+ choices=choices,
61
+ value="ALL",
62
+ interactive=True,
63
+ visible=True
64
+ )
65
+ return components
66
+
67
+ def create_category_filter_ui(prefix):
68
+ valid_cats = list(set(cat for cats in ARCH_CATEGORIES_MAP.values() for cat in cats))
69
+ cat_choices = ["ALL"] + sorted(valid_cats)
70
+
71
+ components = {}
72
+ components[f'model_cat_{prefix}'] = gr.Dropdown(
73
+ label="Filter Models",
74
+ choices=cat_choices,
75
+ value="ALL",
76
+ interactive=True,
77
+ scale=1,
78
+ allow_custom_value=True
79
+ )
80
+ return components
81
+
82
+ def create_base_parameter_ui(prefix, defaults=None):
83
+ if defaults is None:
84
+ defaults = {}
85
+
86
+ components = {}
87
+
88
+ with gr.Row():
89
+ components[f'aspect_ratio_{prefix}'] = gr.Dropdown(
90
+ label="Aspect Ratio",
91
+ choices=list(RESOLUTION_MAP.get('sdxl', {}).keys()),
92
+ value="1:1 (Square)",
93
+ interactive=True,
94
+ allow_custom_value=True
95
+ )
96
+ with gr.Row():
97
+ components[f'width_{prefix}'] = gr.Number(label="Width", value=defaults.get('w', 1024), interactive=True)
98
+ components[f'height_{prefix}'] = gr.Number(label="Height", value=defaults.get('h', 1024), interactive=True)
99
+ with gr.Row():
100
+ components[f'sampler_{prefix}'] = gr.Dropdown(
101
+ label="Sampler",
102
+ choices=SAMPLER_CHOICES,
103
+ value=DEFAULT_SAMPLER if DEFAULT_SAMPLER in SAMPLER_CHOICES else (SAMPLER_CHOICES[0] if SAMPLER_CHOICES else 'euler')
104
+ )
105
+ components[f'scheduler_{prefix}'] = gr.Dropdown(
106
+ label="Scheduler",
107
+ choices=SCHEDULER_CHOICES,
108
+ value=DEFAULT_SCHEDULER if DEFAULT_SCHEDULER in SCHEDULER_CHOICES else (SCHEDULER_CHOICES[0] if SCHEDULER_CHOICES else 'simple')
109
+ )
110
+ with gr.Row():
111
+ components[f'steps_{prefix}'] = gr.Slider(label="Steps", minimum=1, maximum=100, step=1, value=DEFAULT_STEPS)
112
+ components[f'cfg_{prefix}'] = gr.Slider(label="CFG Scale", minimum=1.0, maximum=20.0, step=0.1, value=DEFAULT_CFG)
113
+ with gr.Row():
114
+ components[f'seed_{prefix}'] = gr.Number(label="Seed (-1 for random)", value=-1, precision=0)
115
+ components[f'batch_size_{prefix}'] = gr.Slider(label="Batch Size", minimum=1, maximum=16, step=1, value=1)
116
+ with gr.Row():
117
+ components[f'zero_gpu_{prefix}'] = gr.Number(label="ZeroGPU Duration (s)", value=60, placeholder="Default: 60s", info="Optional: Set how long to reserve the GPU.")
118
+
119
+ return components
120
+
121
+
122
+ def create_lora_settings_ui(prefix: str):
123
+ components = {}
124
+
125
+ lora_rows, lora_sources, lora_ids, lora_scales, lora_uploads = [], [], [], [], []
126
+
127
+ with gr.Accordion("LoRA Settings", open=False, visible=('lora' in default_enabled_chains)) as lora_accordion:
128
+ components[f'lora_accordion_{prefix}'] = lora_accordion
129
+ gr.Markdown("💡 **Tip:** When downloading from Civitai, please use the **Version ID**, not the Model ID. You can find the Version ID in the URL (e.g., `civitai.com/models/123?modelVersionId=456`) or under the model's download button. When downloading from Hugging Face, please use the format: `repo_id/filename.extension` or `repo_id/folder_path/filename.extension` (e.g., `lightx2v/Qwen-Image-Lightning/Qwen-Image-Lightning-4steps-V2.0-bf16.safetensors`).")
130
+ components[f'lora_count_state_{prefix}'] = gr.State(1)
131
+
132
+ for i in range(MAX_LORAS):
133
+ with gr.Row(visible=i==0) as row:
134
+ source = gr.Dropdown(label=f"LoRA Source {i+1}", choices=LORA_SOURCE_CHOICES, value=LORA_SOURCE_CHOICES[0], scale=1)
135
+ lora_id = gr.Textbox(label="Civitai Version ID / HF file / Upload File", scale=2, type="text")
136
+ scale = gr.Slider(label=f"Scale", minimum=0.0, maximum=2.0, step=0.05, value=1.0, scale=1)
137
+ upload = gr.UploadButton(label="Upload", file_types=[".safetensors"], scale=1)
138
+
139
+ lora_rows.append(row)
140
+ lora_sources.append(source)
141
+ lora_ids.append(lora_id)
142
+ lora_scales.append(scale)
143
+ lora_uploads.append(upload)
144
+
145
+ with gr.Row():
146
+ components[f'add_lora_button_{prefix}'] = gr.Button("Add LoRA", variant="secondary")
147
+ components[f'delete_lora_button_{prefix}'] = gr.Button("Remove LoRA", variant="secondary", visible=False)
148
+
149
+ components[f'lora_rows_{prefix}'] = lora_rows
150
+ components[f'lora_sources_{prefix}'] = lora_sources
151
+ components[f'lora_ids_{prefix}'] = lora_ids
152
+ components[f'lora_scales_{prefix}'] = lora_scales
153
+ components[f'lora_uploads_{prefix}'] = lora_uploads
154
+
155
+ all_lora_components_flat = []
156
+ for i in range(MAX_LORAS):
157
+ all_lora_components_flat.extend([lora_sources[i], lora_ids[i], lora_scales[i], lora_uploads[i]])
158
+ components[f'all_lora_components_flat_{prefix}'] = all_lora_components_flat
159
+
160
+ return components
161
+
162
+ def create_controlnet_ui(prefix: str, max_units=MAX_CONTROLNETS):
163
+ components = {}
164
+ key = lambda name: f"{name}_{prefix}"
165
+
166
+ with gr.Accordion("ControlNet Settings", open=False, visible=('controlnet' in default_enabled_chains)) as accordion:
167
+ components[key('controlnet_accordion')] = accordion
168
+
169
+ cn_rows, images, series, types, strengths, filepaths = [], [], [], [], [], []
170
+ components.update({
171
+ key('controlnet_rows'): cn_rows,
172
+ key('controlnet_images'): images,
173
+ key('controlnet_series'): series,
174
+ key('controlnet_types'): types,
175
+ key('controlnet_strengths'): strengths,
176
+ key('controlnet_filepaths'): filepaths
177
+ })
178
+
179
+ for i in range(max_units):
180
+ with gr.Row(visible=(i < 1)) as row:
181
+ with gr.Column(scale=1):
182
+ images.append(gr.Image(label=f"Control Image {i+1}", type="pil", sources=["upload"], height=256))
183
+ with gr.Column(scale=2):
184
+ types.append(gr.Dropdown(label="Type", choices=[], interactive=True, allow_custom_value=True))
185
+ series.append(gr.Dropdown(label="Series", choices=[], interactive=True, allow_custom_value=True))
186
+ strengths.append(gr.Slider(label="Strength", minimum=0.0, maximum=2.0, step=0.05, value=1.0, interactive=True))
187
+ filepaths.append(gr.State(None))
188
+ cn_rows.append(row)
189
+
190
+ with gr.Row():
191
+ components[key('add_controlnet_button')] = gr.Button("✚ Add ControlNet")
192
+ components[key('delete_controlnet_button')] = gr.Button("➖ Delete ControlNet", visible=False)
193
+ components[key('controlnet_count_state')] = gr.State(1)
194
+
195
+ all_cn_components_flat = []
196
+ for i in range(max_units):
197
+ all_cn_components_flat.extend([
198
+ images[i], types[i], series[i], strengths[i], filepaths[i]
199
+ ])
200
+ components[key('all_controlnet_components_flat')] = all_cn_components_flat
201
+
202
+ return components
203
+
204
+ def create_ipadapter_ui(prefix: str, max_units=MAX_IPADAPTERS):
205
+ components = {}
206
+ key = lambda name: f"{name}_{prefix}"
207
+
208
+ sdxl_presets = get_ipadapter_presets("SDXL")
209
+ default_preset = sdxl_presets[0] if sdxl_presets else None
210
+
211
+ with gr.Accordion("IPAdapter Settings", open=False, visible=('ipadapter' in default_enabled_chains)) as accordion:
212
+ components[key('ipadapter_accordion')] = accordion
213
+ gr.Markdown("💡 **Tip:** Processed using the [cubiq/ComfyUI_IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) node.")
214
+
215
+ with gr.Row():
216
+ components[key('ipadapter_final_preset')] = gr.Dropdown(
217
+ label="Preset (for all images)",
218
+ choices=sdxl_presets,
219
+ value=default_preset,
220
+ interactive=True,
221
+ allow_custom_value=True
222
+ )
223
+ components[key('ipadapter_embeds_scaling')] = gr.Dropdown(
224
+ label="Embeds Scaling",
225
+ choices=['V only', 'K+V', 'K+V w/ C penalty', 'K+mean(V) w/ C penalty'],
226
+ value='V only',
227
+ interactive=True
228
+ )
229
+
230
+ with gr.Row():
231
+ components[key('ipadapter_combine_method')] = gr.Dropdown(
232
+ label="Combine Method",
233
+ choices=["concat", "add", "subtract", "average", "norm average", "max", "min"],
234
+ value="concat",
235
+ interactive=True
236
+ )
237
+ components[key('ipadapter_final_weight')] = gr.Slider(label="Final Weight", minimum=0.0, maximum=2.0, step=0.05, value=1.0, interactive=True)
238
+ components[key('ipadapter_final_lora_strength')] = gr.Slider(label="Final LoRA Strength", minimum=0.0, maximum=2.0, step=0.05, value=0.6, interactive=True, visible=False)
239
+
240
+ gr.Markdown("---")
241
+
242
+ ipa_rows, images, weights, lora_strengths = [], [], [], []
243
+ components.update({
244
+ key('ipadapter_rows'): ipa_rows,
245
+ key('ipadapter_images'): images,
246
+ key('ipadapter_weights'): weights,
247
+ key('ipadapter_lora_strengths'): lora_strengths
248
+ })
249
+
250
+ for i in range(max_units):
251
+ with gr.Row(visible=(i < 1)) as row:
252
+ with gr.Column(scale=1):
253
+ images.append(gr.Image(label=f"IPAdapter Image {i+1}", type="pil", sources=["upload"], height=256))
254
+ with gr.Column(scale=2):
255
+ weights.append(gr.Slider(label="Weight", minimum=0.0, maximum=2.0, step=0.05, value=1.0, interactive=True))
256
+ lora_strengths.append(gr.Slider(label="LoRA Strength", minimum=0.0, maximum=2.0, step=0.05, value=0.6, interactive=True, visible=False))
257
+ ipa_rows.append(row)
258
+
259
+ with gr.Row():
260
+ components[key('add_ipadapter_button')] = gr.Button("✚ Add IPAdapter")
261
+ components[key('delete_ipadapter_button')] = gr.Button("➖ Delete IPAdapter", visible=False)
262
+ components[key('ipadapter_count_state')] = gr.State(1)
263
+
264
+ all_ipa_components_flat = images + weights + lora_strengths
265
+ all_ipa_components_flat += [
266
+ components[key('ipadapter_final_preset')],
267
+ components[key('ipadapter_final_weight')],
268
+ components[key('ipadapter_final_lora_strength')],
269
+ components[key('ipadapter_embeds_scaling')],
270
+ components[key('ipadapter_combine_method')],
271
+ ]
272
+ components[key('all_ipadapter_components_flat')] = all_ipa_components_flat
273
+
274
+ return components
275
+
276
+ def create_flux1_ipadapter_ui(prefix: str, max_units=MAX_IPADAPTERS):
277
+ components = {}
278
+ key = lambda name: f"{name}_{prefix}"
279
+
280
+ with gr.Accordion("IPAdapter Settings (FLUX.1)", open=False, visible=('flux1_ipadapter' in default_enabled_chains)) as accordion:
281
+ components[key('flux1_ipadapter_accordion')] = accordion
282
+ gr.Markdown("💡 **Tip:** Processed using the [Shakker-Labs/ComfyUI-IPAdapter-Flux](https://github.com/Shakker-Labs/ComfyUI-IPAdapter-Flux) node.")
283
+
284
+ ipa_rows, images, weights, start_percents, end_percents = [], [], [], [], []
285
+ components.update({
286
+ key('flux1_ipadapter_rows'): ipa_rows,
287
+ key('flux1_ipadapter_images'): images,
288
+ key('flux1_ipadapter_weights'): weights,
289
+ key('flux1_ipadapter_start_percents'): start_percents,
290
+ key('flux1_ipadapter_end_percents'): end_percents,
291
+ })
292
+
293
+ for i in range(max_units):
294
+ with gr.Row(visible=(i < 1)) as row:
295
+ with gr.Column(scale=1):
296
+ images.append(gr.Image(label=f"IPAdapter Image {i+1}", type="pil", sources=["upload"], height=256))
297
+ with gr.Column(scale=2):
298
+ weights.append(gr.Slider(label="Weight", minimum=0.0, maximum=2.0, step=0.05, value=0.6, interactive=True))
299
+ with gr.Row():
300
+ start_percents.append(gr.Slider(label="Start At", minimum=0.0, maximum=1.0, step=0.01, value=0.0, interactive=True))
301
+ end_percents.append(gr.Slider(label="End At", minimum=0.0, maximum=1.0, step=0.01, value=0.6, interactive=True))
302
+ ipa_rows.append(row)
303
+
304
+ with gr.Row():
305
+ components[key('add_flux1_ipadapter_button')] = gr.Button("✚ Add IPAdapter (FLUX)")
306
+ components[key('delete_flux1_ipadapter_button')] = gr.Button("➖ Delete IPAdapter (FLUX)", visible=False)
307
+ components[key('flux1_ipadapter_count_state')] = gr.State(1)
308
+
309
+ all_flux1_ipa_components_flat = images + weights + start_percents + end_percents
310
+ components[key('all_flux1_ipadapter_components_flat')] = all_flux1_ipa_components_flat
311
+
312
+ return components
313
+
314
+ def create_sd3_ipadapter_ui(prefix: str, max_units=MAX_IPADAPTERS):
315
+ components = {}
316
+ key = lambda name: f"{name}_{prefix}"
317
+
318
+ with gr.Accordion("IPAdapter Settings (SD3)", open=False, visible=('sd3_ipadapter' in default_enabled_chains)) as accordion:
319
+ components[key('sd3_ipadapter_accordion')] = accordion
320
+ gr.Markdown("💡 **Tip:** Processed using the [Slickytail/ComfyUI-InstantX-IPAdapter-SD3](https://github.com/Slickytail/ComfyUI-InstantX-IPAdapter-SD3) node.")
321
+
322
+ ipa_rows, images, weights, start_percents, end_percents = [], [], [], [], []
323
+ components.update({
324
+ key('sd3_ipadapter_rows'): ipa_rows,
325
+ key('sd3_ipadapter_images'): images,
326
+ key('sd3_ipadapter_weights'): weights,
327
+ key('sd3_ipadapter_start_percents'): start_percents,
328
+ key('sd3_ipadapter_end_percents'): end_percents,
329
+ })
330
+
331
+ for i in range(max_units):
332
+ with gr.Row(visible=(i < 1)) as row:
333
+ with gr.Column(scale=1):
334
+ images.append(gr.Image(label=f"IPAdapter Image {i+1}", type="pil", sources=["upload"], height=256))
335
+ with gr.Column(scale=2):
336
+ weights.append(gr.Slider(label="Weight", minimum=0.0, maximum=2.0, step=0.05, value=0.5, interactive=True))
337
+ with gr.Row():
338
+ start_percents.append(gr.Slider(label="Start At", minimum=0.0, maximum=1.0, step=0.01, value=0.0, interactive=True))
339
+ end_percents.append(gr.Slider(label="End At", minimum=0.0, maximum=1.0, step=0.01, value=1.0, interactive=True))
340
+ ipa_rows.append(row)
341
+
342
+ with gr.Row():
343
+ components[key('add_sd3_ipadapter_button')] = gr.Button("✚ Add IPAdapter (SD3)")
344
+ components[key('delete_sd3_ipadapter_button')] = gr.Button("➖ Delete IPAdapter (SD3)", visible=False)
345
+ components[key('sd3_ipadapter_count_state')] = gr.State(1)
346
+
347
+ all_sd3_ipa_components_flat = images + weights + start_percents + end_percents
348
+ components[key('all_sd3_ipadapter_components_flat')] = all_sd3_ipa_components_flat
349
+
350
+ return components
351
+
352
+ def create_style_ui(prefix: str):
353
+ components = {}
354
+ key = lambda name: f"{name}_{prefix}"
355
+
356
+ with gr.Accordion("Style Settings (FLUX.1)", open=False, visible=('style' in default_enabled_chains)) as accordion:
357
+ components[key('style_accordion')] = accordion
358
+
359
+ style_rows, images, strengths = [], [], []
360
+ components.update({
361
+ key('style_rows'): style_rows,
362
+ key('style_images'): images,
363
+ key('style_strengths'): strengths
364
+ })
365
+
366
+ for i in range(5):
367
+ with gr.Row(visible=(i < 1)) as row:
368
+ with gr.Column(scale=1):
369
+ images.append(gr.Image(label=f"Style Image {i+1}", type="pil", sources=["upload"], height=256))
370
+ with gr.Column(scale=2):
371
+ strengths.append(gr.Slider(label="Strength", minimum=0.0, maximum=2.0, step=0.05, value=1.0, interactive=True))
372
+ style_rows.append(row)
373
+
374
+ with gr.Row():
375
+ components[key('add_style_button')] = gr.Button("✚ Add Style (FLUX)")
376
+ components[key('delete_style_button')] = gr.Button("➖ Delete Style (FLUX)", visible=False)
377
+ components[key('style_count_state')] = gr.State(1)
378
+
379
+ all_style_components_flat = images + strengths
380
+ components[key('all_style_components_flat')] = all_style_components_flat
381
+
382
+ return components
383
+
384
+ def create_embedding_ui(prefix: str):
385
+ components = {}
386
+ key = lambda name: f"{name}_{prefix}"
387
+
388
+ with gr.Accordion("Embedding Settings", open=False, visible=('embedding' in default_enabled_chains)) as accordion:
389
+ components[key('embedding_accordion')] = accordion
390
+ gr.Markdown("💡 **Tip:** When downloading from Civitai, please use the **Version ID**, not the Model ID. You can find the Version ID in the URL (e.g., `civitai.com/models/123?modelVersionId=456`) or under the model's download button. For example, entering the Version ID 456 will automatically save the file as \"civitai/456.safetensors\", and you will need to manually enter `embedding:civitai/456` in either your prompt or negative prompt to activate it. When downloading from Hugging Face, please use the format: repo_id/filename.extension or repo_id/folder_path/filename.extension (e.g., ilikebigturtles/lazypos/lazypos.safetensors or ilikebigturtles/lazyneg/lazyneg.safetensors). For Hugging Face files, you will need to enter embedding:huggingface/filename (e.g., entering embedding:huggingface/lazypos in your positive prompt, or embedding:huggingface/lazyneg in your negative prompt) to activate it. For uploaded files, enter `embedding:upload_file/filename` in either your positive prompt or negative prompt to activate it.")
391
+
392
+ embedding_rows, sources, ids, files, upload_buttons = [], [], [], [], []
393
+ components.update({
394
+ key('embedding_rows'): embedding_rows,
395
+ key('embeddings_sources'): sources,
396
+ key('embeddings_ids'): ids,
397
+ key('embeddings_files'): files,
398
+ key('embeddings_uploads'): upload_buttons
399
+ })
400
+
401
+ for i in range(MAX_EMBEDDINGS):
402
+ with gr.Row(visible=(i < 1)) as row:
403
+ sources.append(gr.Dropdown(label=f"Embedding Source {i+1}", choices=LORA_SOURCE_CHOICES, value="Civitai", scale=1, interactive=True))
404
+ ids.append(gr.Textbox(label="Civitai Version ID / HF file / Upload File", scale=3, interactive=True, type="text"))
405
+ upload_btn = gr.UploadButton("Upload", file_types=[".safetensors"], scale=1)
406
+ files.append(gr.State(None))
407
+ upload_buttons.append(upload_btn)
408
+ embedding_rows.append(row)
409
+
410
+ with gr.Row():
411
+ components[key('add_embedding_button')] = gr.Button("✚ Add Embedding")
412
+ components[key('delete_embedding_button')] = gr.Button("➖ Delete Embedding", visible=False)
413
+ components[key('embedding_count_state')] = gr.State(1)
414
+
415
+ all_embedding_components_flat = []
416
+ for i in range(MAX_EMBEDDINGS):
417
+ all_embedding_components_flat.extend([sources[i], ids[i], files[i]])
418
+ components[key('all_embedding_components_flat')] = all_embedding_components_flat
419
+
420
+ return components
421
+
422
+ def create_conditioning_ui(prefix: str):
423
+ components = {}
424
+ key = lambda name: f"{name}_{prefix}"
425
+
426
+ with gr.Accordion("Conditioning Settings", open=False, visible=('conditioning' in default_enabled_chains)) as accordion:
427
+ components[key('conditioning_accordion')] = accordion
428
+ gr.Markdown("💡 **Tip:** Define rectangular areas and assign specific prompts to them. Coordinates (X, Y) start from the top-left corner.")
429
+
430
+ cond_rows, prompts, widths, heights, xs, ys, strengths = [], [], [], [], [], [], []
431
+ components.update({
432
+ key('conditioning_rows'): cond_rows,
433
+ key('conditioning_prompts'): prompts,
434
+ key('conditioning_widths'): widths,
435
+ key('conditioning_heights'): heights,
436
+ key('conditioning_xs'): xs,
437
+ key('conditioning_ys'): ys,
438
+ key('conditioning_strengths'): strengths
439
+ })
440
+
441
+ for i in range(MAX_CONDITIONINGS):
442
+ with gr.Column(visible=(i < 1)) as row_wrapper:
443
+ prompts.append(gr.Textbox(label=f"Area Prompt {i+1}", lines=2, interactive=True))
444
+ with gr.Row():
445
+ xs.append(gr.Number(label="X", value=0, interactive=True, step=8, scale=1))
446
+ ys.append(gr.Number(label="Y", value=0, interactive=True, step=8, scale=1))
447
+ widths.append(gr.Number(label="Width", value=512, interactive=True, step=8, scale=1))
448
+ heights.append(gr.Number(label="Height", value=512, interactive=True, step=8, scale=1))
449
+ strengths.append(gr.Slider(label="Strength", minimum=0.1, maximum=2.0, step=0.05, value=1.0, interactive=True, scale=2))
450
+ cond_rows.append(row_wrapper)
451
+
452
+ with gr.Row():
453
+ components[key('add_conditioning_button')] = gr.Button("✚ Add Area")
454
+ components[key('delete_conditioning_button')] = gr.Button("➖ Delete Area", visible=False)
455
+ components[key('conditioning_count_state')] = gr.State(1)
456
+
457
+ all_cond_components_flat = prompts + widths + heights + xs + ys + strengths
458
+ components[key('all_conditioning_components_flat')] = all_cond_components_flat
459
+
460
+ return components
461
+
462
+ def on_vae_upload(file_obj):
463
+ if not file_obj:
464
+ return gr.update(), gr.update(), None
465
+
466
+ uploaded_filename = save_uploaded_file(file_obj, VAE_DIR)
467
+ return uploaded_filename, "Upload File", file_obj
468
+
469
+ def create_vae_override_ui(prefix: str):
470
+ components = {}
471
+ key = lambda name: f"{name}_{prefix}"
472
+
473
+ with gr.Accordion("VAE Settings (Override)", open=False, visible=('vae' in default_enabled_chains)) as vae_accordion:
474
+ components[key('vae_accordion')] = vae_accordion
475
+ gr.Markdown("💡 **Tip:** When downloading from Civitai, please use the **Version ID**, not the Model ID. You can find the Version ID in the URL (e.g., `civitai.com/models/123?modelVersionId=456`) or under the model's download button. When downloading from Hugging Face, please use the format: `repo_id/filename.extension` or `repo_id/folder_path/filename.extension` (e.g., `madebyollin/sdxl-vae-fp16-fix/sdxl_vae.safetensors`).")
476
+ with gr.Row():
477
+ components[key('vae_source')] = gr.Dropdown(
478
+ label="VAE Source",
479
+ choices=LORA_SOURCE_CHOICES,
480
+ value="Civitai",
481
+ scale=1,
482
+ interactive=True
483
+ )
484
+ components[key('vae_id')] = gr.Textbox(
485
+ label="Civitai Version ID / HF file / Upload File",
486
+ scale=3,
487
+ interactive=True,
488
+ type="text"
489
+ )
490
+ upload_btn = gr.UploadButton(
491
+ "Upload",
492
+ file_types=[".safetensors"],
493
+ scale=1
494
+ )
495
+ components[key('vae_upload_button')] = upload_btn
496
+ components[key('vae_file')] = gr.State(None)
497
+
498
+ upload_btn.upload(
499
+ fn=on_vae_upload,
500
+ inputs=[upload_btn],
501
+ outputs=[components[key('vae_id')], components[key('vae_source')], components[key('vae_file')]]
502
+ )
503
+
504
+ return components
505
+
506
+ def create_reference_latent_ui(prefix: str, max_units=10):
507
+ components = {}
508
+ key = lambda name: f"{name}_{prefix}"
509
+
510
+ with gr.Accordion("Reference Edit Settings", open=False, visible=('reference_latent' in default_enabled_chains)) as ref_accordion:
511
+ components[key('reference_latent_accordion')] = ref_accordion
512
+ gr.Markdown("💡 **Tip:** For multimodal models, this feature enables powerful editing and combining capabilities. In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
513
+
514
+ ref_image_groups = []
515
+ ref_image_inputs = []
516
+ with gr.Row():
517
+ for i in range(max_units):
518
+ with gr.Column(visible=(i < 1), min_width=160) as img_col:
519
+ img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
520
+ ref_image_groups.append(img_col)
521
+ ref_image_inputs.append(img_comp)
522
+
523
+ components[key('reference_latent_rows')] = ref_image_groups
524
+ components[key('reference_latent_images')] = ref_image_inputs
525
+
526
+ with gr.Row():
527
+ components[key('add_reference_latent_button')] = gr.Button("✚ Add Reference Image")
528
+ components[key('delete_reference_latent_button')] = gr.Button("➖ Delete Reference Image", visible=False)
529
+ components[key('reference_latent_count_state')] = gr.State(1)
530
+
531
+ components[key('all_reference_latent_components_flat')] = ref_image_inputs
532
+
533
+ return components
534
+
535
+ def create_hidream_o1_reference_ui(prefix: str, max_units=10):
536
+ components = {}
537
+ key = lambda name: f"{name}_{prefix}"
538
+
539
+ with gr.Accordion("HiDream-O1 Reference Edit Settings", open=False, visible=('hidream_o1_reference' in default_enabled_chains)) as ref_accordion:
540
+ components[key('hidream_o1_reference_accordion')] = ref_accordion
541
+ gr.Markdown("💡 **Tip:** Please use **HiDream-O1-Image-Dev** (HiDream-O1-Image will time out), and set the resolution to **4.0MP** (e.g., 2048x2048). In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
542
+
543
+ ref_image_groups = []
544
+ ref_image_inputs = []
545
+ with gr.Row():
546
+ for i in range(max_units):
547
+ with gr.Column(visible=(i < 1), min_width=160) as img_col:
548
+ img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
549
+ ref_image_groups.append(img_col)
550
+ ref_image_inputs.append(img_comp)
551
+
552
+ components[key('hidream_o1_reference_rows')] = ref_image_groups
553
+ components[key('hidream_o1_reference_images')] = ref_image_inputs
554
+
555
+ with gr.Row():
556
+ components[key('add_hidream_o1_reference_button')] = gr.Button("✚ Add Reference Image")
557
+ components[key('delete_hidream_o1_reference_button')] = gr.Button("➖ Delete Reference Image", visible=False)
558
+ components[key('hidream_o1_reference_count_state')] = gr.State(1)
559
+
560
+ components[key('all_hidream_o1_reference_components_flat')] = ref_image_inputs
561
+
562
+ return components
563
+
564
+ def create_sensenova_reference_ui(prefix: str, max_units=10):
565
+ components = {}
566
+ key = lambda name: f"{name}_{prefix}"
567
+
568
+ with gr.Accordion("SenseNova Reference Edit Settings", open=False, visible=('sensenova_reference' in default_enabled_chains)) as ref_accordion:
569
+ components[key('sensenova_reference_accordion')] = ref_accordion
570
+ gr.Markdown("💡 **Tip:** For SenseNova models, this feature enables reference image editing and combining capabilities. In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
571
+
572
+ ref_image_groups = []
573
+ ref_image_inputs = []
574
+ with gr.Row():
575
+ for i in range(max_units):
576
+ with gr.Column(visible=(i < 1), min_width=160) as img_col:
577
+ img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
578
+ ref_image_groups.append(img_col)
579
+ ref_image_inputs.append(img_comp)
580
+
581
+ components[key('sensenova_reference_rows')] = ref_image_groups
582
+ components[key('sensenova_reference_images')] = ref_image_inputs
583
+
584
+ with gr.Row():
585
+ components[key('add_sensenova_reference_button')] = gr.Button("✚ Add Reference Image")
586
+ components[key('delete_sensenova_reference_button')] = gr.Button("➖ Delete Reference Image", visible=False)
587
+ components[key('sensenova_reference_count_state')] = gr.State(1)
588
+
589
+ components[key('all_sensenova_reference_components_flat')] = ref_image_inputs
590
+
591
+ return components
592
+
593
+ def create_joyai_reference_ui(prefix: str, max_units=2):
594
+ components = {}
595
+ key = lambda name: f"{name}_{prefix}"
596
+
597
+ with gr.Accordion("JoyAI Reference Edit Settings", open=False, visible=('joyai_image' in default_enabled_chains)) as ref_accordion:
598
+ components[key('joyai_reference_accordion')] = ref_accordion
599
+ gr.Markdown("💡 **Tip:** For multimodal models, this feature enables powerful editing and combining capabilities. In txt2img mode, adding a single reference image performs an **Image Edit** (JoyAI-Image-Edit recommended), while adding multiple images performs an **Image Combine** (JoyAI-Image-Edit-Plus recommended with ZeroGPU Duration (s) set to 120).")
600
+
601
+ ref_image_groups = []
602
+ ref_image_inputs = []
603
+ with gr.Row():
604
+ for i in range(max_units):
605
+ with gr.Column(visible=(i < 1), min_width=160) as img_col:
606
+ img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
607
+ ref_image_groups.append(img_col)
608
+ ref_image_inputs.append(img_comp)
609
+
610
+ components[key('joyai_reference_rows')] = ref_image_groups
611
+ components[key('joyai_reference_images')] = ref_image_inputs
612
+
613
+ with gr.Row():
614
+ components[key('add_joyai_reference_button')] = gr.Button("✚ Add Reference Image")
615
+ components[key('delete_joyai_reference_button')] = gr.Button("➖ Delete Reference Image", visible=False)
616
+ components[key('joyai_reference_count_state')] = gr.State(1)
617
+
618
+ components[key('all_joyai_reference_components_flat')] = ref_image_inputs
619
+
620
+ return components
621
+
622
+ def create_reference_image_ui(prefix: str, max_units=10):
623
+ components = {}
624
+ key = lambda name: f"{name}_{prefix}"
625
+
626
+ with gr.Accordion("Reference Image Edit Settings", open=False, visible=('reference_image' in default_enabled_chains)) as ref_accordion:
627
+ components[key('reference_image_accordion')] = ref_accordion
628
+ gr.Markdown("💡 **Tip:** For multimodal models, this feature enables powerful editing and combining capabilities. In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
629
+
630
+ ref_image_groups = []
631
+ ref_image_inputs = []
632
+ with gr.Row():
633
+ for i in range(max_units):
634
+ with gr.Column(visible=(i < 1), min_width=160) as img_col:
635
+ img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
636
+ ref_image_groups.append(img_col)
637
+ ref_image_inputs.append(img_comp)
638
+
639
+ components[key('reference_image_rows')] = ref_image_groups
640
+ components[key('reference_image_images')] = ref_image_inputs
641
+
642
+ with gr.Row():
643
+ components[key('add_reference_image_button')] = gr.Button("✚ Add Reference Image")
644
+ components[key('delete_reference_image_button')] = gr.Button("➖ Delete Reference Image", visible=False)
645
+ components[key('reference_image_count_state')] = gr.State(1)
646
+
647
+ components[key('all_reference_image_components_flat')] = ref_image_inputs
648
+
649
+ return components
650
+ def create_moss_image_control_ui(prefix: str):
651
+ components = {}
652
+ key = lambda name: f"{name}_{prefix}"
653
+
654
+ with gr.Accordion("MOSS-Image Color/Depth Control Settings", open=False, visible=('moss_image_control' in default_enabled_chains)) as control_accordion:
655
+ components[key('moss_image_control_accordion')] = control_accordion
656
+ gr.Markdown("💡 **Tip:** Uses the [darask0/MOSS-Image-color-depth-control](https://huggingface.co/darask0/MOSS-Image-color-depth-control) adapter.")
657
+ with gr.Row():
658
+ components[key('moss_image_control_color_map')] = gr.Image(
659
+ type="pil", label="Color Region Map (Optional)", sources=["upload"], height=200
660
+ )
661
+ components[key('moss_image_control_depth_map')] = gr.Image(
662
+ type="pil", label="Depth Map (Optional, near=white)", sources=["upload"], height=200
663
+ )
664
+
665
+ with gr.Row():
666
+ components[key('moss_image_control_strength')] = gr.Slider(
667
+ label="Control Strength",
668
+ value=1.0,
669
+ minimum=0.0,
670
+ maximum=2.0,
671
+ step=0.05,
672
+ interactive=True
673
+ )
674
+ components[key('moss_image_control_end_percent')] = gr.Slider(
675
+ label="End Percent (0.5 for 3D mannequin)",
676
+ value=0.5,
677
+ minimum=0.0,
678
+ maximum=1.0,
679
+ step=0.01,
680
+ interactive=True
681
+ )
682
+
683
+ return components
684
+
685
+
686
+ def create_pid_ui(prefix: str):
687
+ components = {}
688
+ key = lambda name: f"{name}_{prefix}"
689
+
690
+ with gr.Accordion("PiD Settings", open=False, visible=('pid' in default_enabled_chains)) as pid_accordion:
691
+ components[key('pid_accordion')] = pid_accordion
692
+ gr.Markdown("💡 **Tip:** Use PiD (Pixel Diffusion Decoder) instead of the VAE Decoder for 4x decoding.")
693
+ with gr.Row():
694
+ components[key('pid_settings')] = gr.Dropdown(
695
+ label="PiD Mode",
696
+ choices=["OFF", "ON"],
697
+ value="OFF",
698
+ interactive=True
699
+ )
700
+
701
+ return components
702
+
703
+ def create_krea2_identity_edit_ui(prefix: str, max_units=2):
704
+ components = {}
705
+ key = lambda name: f"{name}_{prefix}"
706
+
707
+ with gr.Accordion("Krea2 Identity Edit Settings", open=False, visible=('krea2_identity_edit' in default_enabled_chains)) as ref_accordion:
708
+ components[key('krea2_identity_edit_accordion')] = ref_accordion
709
+ gr.Markdown("💡 **Tip:** Processed using the [lbouaraba/comfyui-krea2edit](https://github.com/lbouaraba/comfyui-krea2edit) node. (Krea-2-Turbo recommended, Krea-2-Raw need set ZeroGPU Duration (s) to 120 ) In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
710
+
711
+ ref_image_groups = []
712
+ ref_image_inputs = []
713
+ with gr.Row():
714
+ for i in range(max_units):
715
+ with gr.Column(visible=(i < 1), min_width=160) as img_col:
716
+ img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
717
+ ref_image_groups.append(img_col)
718
+ ref_image_inputs.append(img_comp)
719
+
720
+ components[key('krea2_identity_edit_rows')] = ref_image_groups
721
+ components[key('krea2_identity_edit_images')] = ref_image_inputs
722
+
723
+ with gr.Row():
724
+ components[key('add_krea2_identity_edit_button')] = gr.Button("✚ Add Reference Image")
725
+ components[key('delete_krea2_identity_edit_button')] = gr.Button("➖ Delete Reference Image", visible=False)
726
+ components[key('krea2_identity_edit_count_state')] = gr.State(1)
727
+
728
+ components[key('all_krea2_identity_edit_components_flat')] = ref_image_inputs
729
+
730
+ return components
731
+
732
+ def create_qwen_image_edit_ui(prefix: str, max_units=3):
733
+ components = {}
734
+ key = lambda name: f"{name}_{prefix}"
735
+
736
+ with gr.Accordion("Qwen-Image Edit Settings", open=False, visible=('qwen_image_edit' in default_enabled_chains)) as ref_accordion:
737
+ components[key('qwen_image_edit_accordion')] = ref_accordion
738
+ gr.Markdown("💡 **Tip:** (lightx2v/Qwen-Image-Edit-2511-Lightning recommended) In txt2img mode, adding a single reference image performs an **Image Edit**, while adding multiple images performs an **Image Combine**.")
739
+
740
+ ref_image_groups = []
741
+ ref_image_inputs = []
742
+ with gr.Row():
743
+ for i in range(max_units):
744
+ with gr.Column(visible=(i < 1), min_width=160) as img_col:
745
+ img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
746
+ ref_image_groups.append(img_col)
747
+ ref_image_inputs.append(img_comp)
748
+
749
+ components[key('qwen_image_edit_rows')] = ref_image_groups
750
+ components[key('qwen_image_edit_images')] = ref_image_inputs
751
+
752
+ with gr.Row():
753
+ components[key('add_qwen_image_edit_button')] = gr.Button("✚ Add Reference Image")
754
+ components[key('delete_qwen_image_edit_button')] = gr.Button("➖ Delete Reference Image", visible=False)
755
+ components[key('qwen_image_edit_count_state')] = gr.State(1)
756
+
757
+ components[key('all_qwen_image_edit_components_flat')] = ref_image_inputs
758
+
759
+ return components
760
+
761
+ def create_krea2_reference_edit_ui(prefix: str, max_units=3):
762
+ components = {}
763
+ key = lambda name: f"{name}_{prefix}"
764
+
765
+ with gr.Accordion("Krea2 Style Reference Edit Settings", open=False, visible=('krea2_style_reference' in default_enabled_chains)) as ref_accordion:
766
+ components[key('krea2_reference_edit_accordion')] = ref_accordion
767
+ gr.Markdown("💡 **Tip:** (Krea-2-Turbo recommended) Add style reference images to perform style reference editing.")
768
+
769
+ ref_image_groups = []
770
+ ref_image_inputs = []
771
+ with gr.Row():
772
+ for i in range(max_units):
773
+ with gr.Column(visible=(i < 1), min_width=160) as img_col:
774
+ img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
775
+ ref_image_groups.append(img_col)
776
+ ref_image_inputs.append(img_comp)
777
+
778
+ components[key('krea2_reference_edit_rows')] = ref_image_groups
779
+ components[key('krea2_reference_edit_images')] = ref_image_inputs
780
+
781
+ with gr.Row():
782
+ components[key('add_krea2_reference_edit_button')] = gr.Button("✚ Add Reference Image")
783
+ components[key('delete_krea2_reference_edit_button')] = gr.Button("➖ Delete Reference Image", visible=False)
784
+ components[key('krea2_reference_edit_count_state')] = gr.State(1)
785
+
786
+ components[key('all_krea2_reference_edit_components_flat')] = ref_image_inputs
787
+
788
+ return components
789
+
790
+
791
+ def create_prompt_enhancer_ui(prefix: str):
792
+ components = {}
793
+ key = lambda name: f"{name}_{prefix}"
794
+
795
+ with gr.Accordion("Prompt Enhancer Settings", open=False, visible=('prompt_enhancer' in default_enabled_chains)) as enhancer_accordion:
796
+ components[key('prompt_enhancer_accordion')] = enhancer_accordion
797
+ gr.Markdown(
798
+ "💡 **Tip:** Automatically selects Prompt Enhancer Model and System Prompt based on the selected Base Model; "
799
+ "free mixing is also supported."
800
+ )
801
+ with gr.Row():
802
+ components[key('prompt_enhancer_enable')] = gr.Checkbox(
803
+ label="Enable Prompt Enhancer",
804
+ value=False,
805
+ interactive=True,
806
+ scale=2
807
+ )
808
+ components[key('prompt_enhancer_thinking')] = gr.Checkbox(
809
+ label="Enable Thinking",
810
+ value=False,
811
+ interactive=True,
812
+ scale=2
813
+ )
814
+ components[key('prompt_enhancer_max_length')] = gr.Number(
815
+ label="Max Length",
816
+ value=4096,
817
+ precision=0,
818
+ minimum=16,
819
+ step=16,
820
+ interactive=True,
821
+ scale=1
822
+ )
823
+ with gr.Row():
824
+ components[key('prompt_enhancer_model')] = gr.Dropdown(
825
+ choices=_prompt_enhancer_choices("model"),
826
+ value=_prompt_enhancer_initial_defaults()["model"],
827
+ label="Prompt Enhancer Model",
828
+ interactive=True,
829
+ scale=3
830
+ )
831
+ components[key('prompt_enhancer_system_prompt')] = gr.Dropdown(
832
+ choices=_prompt_enhancer_choices("system_prompt"),
833
+ value=_prompt_enhancer_initial_defaults()["system_prompt"],
834
+ label="System Prompt",
835
+ interactive=True,
836
+ scale=2
837
+ )
838
+
839
+ return components
840
+
841
+
842
+ def _prompt_enhancer_registry():
843
+ """Load yaml/prompt_enhancer.yaml once per UI build."""
844
+ import yaml
845
+ path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "yaml", "prompt_enhancer.yaml")
846
+ try:
847
+ with open(path, "r", encoding="utf-8") as f:
848
+ return yaml.safe_load(f) or {}
849
+ except Exception as e:
850
+ print(f"Warning: could not load prompt_enhancer.yaml: {e}")
851
+ return {}
852
+
853
+
854
+ def _prompt_enhancer_choices(kind):
855
+ cfg = _prompt_enhancer_registry()
856
+ names = list((cfg.get("clip" if kind == "model" else "system_prompt") or {}).keys())
857
+ return ["auto"] + names
858
+
859
+
860
+ def _prompt_enhancer_initial_defaults():
861
+ """Seed the dropdowns from the Base Model selected on first page load.
862
+
863
+ ``default_arch_model_type`` is resolved at import time from the first
864
+ checkpoint in the model list, so the panel opens on a real selection
865
+ instead of ``auto`` even when Model Architecture is ALL.
866
+ """
867
+ return prompt_enhancer_defaults(default_arch_model_type)
868
+
869
+
870
+ def prompt_enhancer_defaults(framework):
871
+ """Return {model, system_prompt} defaults declared for a model framework."""
872
+ cfg = _prompt_enhancer_registry()
873
+ clips = list((cfg.get("clip") or {}).keys())
874
+ prompts = list((cfg.get("system_prompt") or {}).keys())
875
+ entry = (cfg.get("architecture") or {}).get(str(framework)) or {}
876
+
877
+ model = entry.get("clip")
878
+ if model not in clips:
879
+ model = clips[0] if clips else "auto"
880
+ prompt = entry.get("system_prompt")
881
+ if prompt not in prompts:
882
+ prompt = prompts[0] if prompts else "auto"
883
+ return {"model": model, "system_prompt": prompt}
884
+
885
+
886
+
887
+ def create_moss_image_edit_ui(prefix: str, max_units=None):
888
+ if max_units is None:
889
+ max_units = MAX_MOSS_IMAGE_EDITS
890
+ components = {}
891
+ key = lambda name: f"{name}_{prefix}"
892
+
893
+ with gr.Accordion("MOSS-Image Edit Settings", open=False, visible=('moss_image_edit' in default_enabled_chains)) as ref_accordion:
894
+ components[key('moss_image_edit_accordion')] = ref_accordion
895
+ gr.Markdown("💡 **Tip:** For multimodal models, this feature enables powerful editing and combining capabilities. In txt2img mode, adding a single reference image performs an Image Edit, while adding multiple images performs an Image Combine. Uses [chinoll/Moss-Image-Edit](https://huggingface.co/chinoll/Moss-Image-Edit).")
896
+
897
+ ref_image_groups = []
898
+ ref_image_inputs = []
899
+ with gr.Row():
900
+ for i in range(max_units):
901
+ with gr.Column(visible=(i < 1), min_width=160) as img_col:
902
+ img_comp = gr.Image(type="pil", label=f"Ref. {i+1}", sources=["upload"], height=150)
903
+ ref_image_groups.append(img_col)
904
+ ref_image_inputs.append(img_comp)
905
+
906
+ components[key('moss_image_edit_rows')] = ref_image_groups
907
+ components[key('moss_image_edit_images')] = ref_image_inputs
908
+
909
+ with gr.Row():
910
+ components[key('add_moss_image_edit_button')] = gr.Button("✚ Add Reference Image")
911
+ components[key('delete_moss_image_edit_button')] = gr.Button("➖ Delete Reference Image", visible=False)
912
+ components[key('moss_image_edit_count_state')] = gr.State(1)
913
+
914
+ components[key('all_moss_image_edit_components_flat')] = ref_image_inputs
915
+
916
+ return components
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
yaml/chain_features.yaml CHANGED
@@ -478,81 +478,33 @@ embedding:
478
  - source
479
  - embedding_value
480
 
481
- qwen_image_2_1_prompt_enhancer:
482
- chains: qwen_image_2_1_prompt_enhancer
483
- display_name: "Qwen-Image-2.1 Prompt Enhancer Settings"
484
- description: "Enhance prompts using Qwen-Image-2.1 Prompt Enhancer with automatic model selection (T2I vs I2I/reference images) and optional reasoning CoT thinking mode."
485
  max_count: 1
486
- usage_guideline: "Set enable to true to activate prompt enhancement. Enable thinking to allow reasoning CoT (reasoning tags are automatically stripped from the resulting prompt). Set max_length to control generation length."
487
  parameters_schema:
488
  type: object
489
  properties:
490
  enable:
491
  type: boolean
492
  default: false
493
- description: "Enable Qwen-Image-2.1 Prompt Enhancer."
494
  thinking:
495
  type: boolean
496
  default: false
497
- description: "Enable thinking (reasoning CoT). Thought text inside <think> tags will be automatically stripped before passing prompt to downstream text encoding."
498
- max_length:
499
- type: integer
500
- default: 4096
501
- description: "Maximum generation length for prompt enhancement (default 4096)."
502
-
503
- ming_image_prompt_enhancer:
504
- chains: ming_image_prompt_enhancer
505
- display_name: "Ming-Image Prompt Enhancer Settings"
506
- description: "Enhance prompts using Ming-Image Prompt Enhancer (Qwen3.8-27B) guided by official Figma-style rewriter System Prompt, with optional reasoning CoT thinking mode."
507
- max_count: 1
508
- usage_guideline: "Set enable to true to activate Ming-Image prompt enhancement. Supports single/multi-reference images, thinking mode, and custom system prompt."
509
- parameters_schema:
510
- type: object
511
- properties:
512
- enable:
513
- type: boolean
514
- default: false
515
- description: "Enable Ming-Image Prompt Enhancer."
516
- thinking:
517
- type: boolean
518
- default: false
519
- description: "Enable thinking (reasoning CoT)."
520
- max_length:
521
- type: integer
522
- default: 4096
523
- description: "Maximum generation length for prompt enhancement (default 4096)."
524
  system_prompt:
525
  type: string
526
- description: "System prompt for Qwen3.8-27B prompt enhancer (defaults to official Ming-Image Figma-style rewriter prompt)."
527
-
528
- flux3_prompt_upsampler:
529
- chains: flux3_prompt_upsampler
530
- display_name: "FLUX.3 Prompt Upsampler Settings"
531
- description: "Expand short prompts into detailed generative captions using FLUX.3 Prompt Upsampler (qwen3.5_9b_flux3_prompt_upsampler_w4a8) with automatic official template selection (t2i, i2i, t2i_bbox, i2i_bbox), multi-reference image stitching, and reasoning CoT thinking mode."
532
- max_count: 1
533
- usage_guideline: "Set enable to true to activate FLUX.3 prompt upsampler. Automatically selects official BFL t2i or i2i template based on input image detection. Supports bbox_mode, thinking mode, and multi-reference image stitching."
534
- parameters_schema:
535
- type: object
536
- properties:
537
- enable:
538
- type: boolean
539
- default: false
540
- description: "Enable FLUX.3 Prompt Upsampler."
541
- thinking:
542
- type: boolean
543
- default: false
544
- description: "Enable thinking (reasoning CoT)."
545
- bbox_mode:
546
- type: boolean
547
- default: false
548
- description: "Enable BBox mode (automatically selects t2i_bbox or i2i_bbox official system prompt templates to output bounding boxes)."
549
  max_length:
550
  type: integer
551
- default: 3584
552
- description: "Maximum generation length for prompt upsampler (default 3584)."
553
- system_prompt:
554
- type: string
555
- description: "Optional custom system prompt override (defaults to official t2i/i2i or t2i_bbox/i2i_bbox templates)."
556
 
557
  moss_image_control:
558
  chains: moss_image_control
 
478
  - source
479
  - embedding_value
480
 
481
+ prompt_enhancer:
482
+ chains: prompt_enhancer
483
+ display_name: "Prompt Enhancer Settings"
484
+ description: "Enhance or upsample prompts with any registered prompt-enhancer model (Qwen-Image-2.1 PE T2I/I2I, Qwen3.8-27B, FLUX.3 Prompt Upsampler), optional reasoning CoT thinking mode, and optional system prompt. The clip and system_prompt registries are independent, so they are freely mixable across architectures."
485
  max_count: 1
486
+ usage_guideline: "Set enable to true to activate prompt enhancement. Pick model from the clip registry and system_prompt from the system_prompt registry in yaml/prompt_enhancer.yaml; both lists are global, so any architecture may select any clip paired with any system prompt. Defaults for each framework come from the architecture section and reset when the base model changes. Nothing is auto-detected from images, so an explicit system prompt always wins. Enable thinking for reasoning CoT (reasoning tags are stripped from the result), and set max_length for generation length."
487
  parameters_schema:
488
  type: object
489
  properties:
490
  enable:
491
  type: boolean
492
  default: false
493
+ description: "Enable prompt enhancement."
494
  thinking:
495
  type: boolean
496
  default: false
497
+ description: "Enable thinking (reasoning CoT). Thought text inside <think> tags is automatically stripped before the prompt reaches downstream text encoding."
498
+ model:
499
+ type: string
500
+ description: "Prompt Enhancer Model name from the clip registry in yaml/prompt_enhancer.yaml (e.g. Qwen-Image-2.1-Prompt-Enhancer-T2I, Qwen-Image-2.1-Prompt-Enhancer-I2I, Qwen3.8-27B, Qwen3.5-9B-FLUX.3-Prompt-Upsampler). Any model may select any registered clip regardless of architecture; clip_type travels with the clip. Defaults to the architecture entry for the active framework."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  system_prompt:
502
  type: string
503
+ description: "System Prompt name from the system_prompt registry in yaml/prompt_enhancer.yaml (e.g. none, Ming-Image-PE-Template, FLUX.3-T2I-PE-Template, FLUX.3-I2I-PE-Template, FLUX.3-T2I-Bbox-PE-Template, FLUX.3-I2I-Bbox-PE-Template). Independent of the selected clip and never inferred from images; an empty prompt means no system prompt."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  max_length:
505
  type: integer
506
+ default: 4096
507
+ description: "Maximum generation length for prompt enhancement (default 4096)."
 
 
 
508
 
509
  moss_image_control:
510
  chains: moss_image_control
yaml/model_architecture_features.yaml CHANGED
@@ -9,7 +9,7 @@ flux3:
9
  enabled_chains:
10
  - lora
11
  - reference_latent
12
- - flux3_prompt_upsampler
13
  - pid
14
 
15
  minimax-h3-image:
@@ -104,14 +104,14 @@ ming-image:
104
  enabled_chains:
105
  - lora
106
  - reference_image
107
- - ming_image_prompt_enhancer
108
 
109
  qwen-image-2.1:
110
  enabled_chains:
111
  - lora
112
  - controlnet
113
  - reference_image
114
- - qwen_image_2_1_prompt_enhancer
115
 
116
  qwen-image:
117
  enabled_chains:
 
9
  enabled_chains:
10
  - lora
11
  - reference_latent
12
+ - prompt_enhancer
13
  - pid
14
 
15
  minimax-h3-image:
 
104
  enabled_chains:
105
  - lora
106
  - reference_image
107
+ - prompt_enhancer
108
 
109
  qwen-image-2.1:
110
  enabled_chains:
111
  - lora
112
  - controlnet
113
  - reference_image
114
+ - prompt_enhancer
115
 
116
  qwen-image:
117
  enabled_chains:
yaml/prompt_enhancer.yaml ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ architecture:
2
+ flux3:
3
+ clip: "Qwen3.5-9B-FLUX.3-Prompt-Upsampler"
4
+ system_prompt: "FLUX.3-T2I-PE-Template"
5
+ ming-image:
6
+ clip: "Qwen3.8-27B"
7
+ system_prompt: "Ming-Image-PE-Template"
8
+ qwen-image-2.1:
9
+ clip: "Qwen-Image-2.1-Prompt-Enhancer-T2I"
10
+ system_prompt: "none"
11
+
12
+ clip:
13
+ Qwen-Image-2.1-Prompt-Enhancer-T2I:
14
+ filename: "qwen3.5_9b_qwen_image_2.1_pe_t2i.int8_convrot.safetensors"
15
+ clip_type: qwen_image
16
+ Qwen-Image-2.1-Prompt-Enhancer-I2I:
17
+ filename: "qwen3.5_9b_qwen_image_2.1_pe_i2i.int8_convrot.safetensors"
18
+ clip_type: qwen_image
19
+ Qwen3.8-27B:
20
+ filename: "qwen3.8_27b_w4a8.safetensors"
21
+ clip_type: qwen_image
22
+ Qwen3.5-9B-FLUX.3-Prompt-Upsampler:
23
+ filename: "qwen3.5_9b_flux3_prompt_upsampler_w4a8.safetensors"
24
+ clip_type: flux3
25
+
26
+ system_prompt:
27
+ none: ""
28
+ Ming-Image-PE-Template: |-
29
+ You are a senior visual designer and image-prompt engineer. Expand the user's request into one precise, high-resolution Figma-style caption. Return only one JSON object. Use exactly two top-level keys. `canvas_settings` contains exactly `aspect_ratio`, `ambient_lighting`, and `image_style`. `layers` lists visible groups from background to topmost overlay. Every layer contains exactly `description`, `coordinates`, `hierarchy_and_relation`, and `color_specs`; `color_specs` is an array of hex colors. `coordinates` MUST be one string, never an object or array, in exactly this form: `"cx: 0.500, cy: 0.500, w: 1.000, h: 1.000"`. Values are normalized; each bbox encloses its complete owned object and stays inside the canvas. A layer is one selectable visible semantic group: background, full person, coherent object, panel, card, row, or text block. Prefer the fewest groups that preserve the layout. Keep people and objects intact. Never create invisible parents, guides, placeholders, empty layers, duplicate summaries, or multiple owners for one element. Preserve every user-supplied rendered string character-for-character and as one contiguous string. Unless multiple visible copies are requested, it must occur exactly once across all `description` fields and zero times in `hierarchy_and_relation`. Quote it only where describing its visible rendering; refer to the related subject elsewhere with unquoted semantic wording. Enumerate intended copy, invent extra copy sparingly, and never hide content behind "other text", "remaining labels", or "etc." Describe concrete composition, typography, materials, texture, lighting, pose, and camera treatment without literary filler. Use `hierarchy_and_relation` only for ownership, alignment, containment, stacking, and occlusion. Infer structured layouts first. Use one complete layer per card and state its row and column. A compact secondary table may be one layer only if every header and cell is listed; otherwise use a visible shared frame when present, one complete header, and one complete layer per body row, binding values to columns and stating blanks. Enumerate sequences, schedules, spans, gaps, and vacant tracks in visual order. Do not mistake ordinary alignment for a table. Silently verify schema, string coordinates, Z-order, exact-text counts, geometry, bbox validity, and completeness.
30
+ FLUX.3-T2I-PE-Template: |-
31
+ You are the prompt upsampler of a text-to-image model. Expand the user's short request into ONE detailed, self-contained caption: the exact text the image model receives. Output only the caption, with no greetings, explanations, refusals, questions, Markdown, headings, JSON, coordinates, ids or angle-bracket tokens, and no quotes around it. If the input is not clearly an image request, caption a visualization of its concept. The request may begin with a "TARGET CANVAS: W:H" line: compose for that canvas (wide spreads content sideways, tall stacks it) and never mention the ratio or the canvas.
32
+
33
+ Fidelity and planning: keep every explicit subject, attribute, count, spatial constraint, style cue and piece of wording; never contradict, substitute, drop or narrow the request, and keep an abstract concept abstract. Decide everything left open as ONE concrete, consistent plan; never offer alternatives or hedge. Never re-express a stated constraint through another mechanism; add camera optics (depth of field, bokeh, blur) only when asked for or implied. Without a stated style, plan an actual photograph and open with the medium convention ("Full-color outdoor photograph, ...", "Full-color indoor photograph, ...", "Studio photograph, ...", "Close-up photograph, ..."); keep "Photorealistic" for renders, and turn "photorealistic" or "realistic" requests for photos into the photograph convention. Give a photograph a few ordinary, non-ideal specifics drawn from the scene's own objects instead of the stock-photo ideal. Do not invent names, brands, logos or celebrities; keep user-named ones. A long request keeps its content but not its wording: drop appraisal and sales vocabulary, turn moods into observable physical facts, and add only new concrete content with positions. Time of day is lighting, not rendered text.
34
+
35
+ Caption format: one continuous prose block in the generative voice, never "this image shows", "we can see" or "is visible". Lead with medium and style, then scene, lighting and camera, then the subjects with concrete colors, materials and state. Every person in focus gets an observable profile: ethnicity, gender, age range, skin tone ("fair skin", "brown skin"), hair and clothing. Make the layout reconstructable from words: frame-relative positions (frame-left, frame-right, upper, lower, foreground, midground, background), sentence-initial anchors ("On the left, ...", "In the background, ..."), relations (holds, faces, behind) and a consistent order; describe up to about ten instances one by one, beyond that give the exact count and arrangement. Out-of-focus or distant planes are shapes, colors and silhouettes, never identities or readable text, and the background takes about a sixth of the caption. Be definite: no hedging adverbs, no quality or emotion judgments, no closing sentence about mood; state negations as what is present. Rendered text: transcribe every wording verbatim inside double quotes in its original language and script, keep the line breaks of multi-line text, and give its typography, color and placement; author concrete, domain-correct wording when the request needs text it does not supply, and name canonical structures (calendar, keyboard, periodic table) exactly with their full layout. All prose is English; only quoted text keeps its language. Typical length is 100 to 350 words, more for complex scenes.
36
+
37
+ Output only the caption.
38
+ FLUX.3-I2I-PE-Template: |-
39
+ You are the prompt upsampler of an image-editing model. You see the reference image(s), attached in order (the first image, the second image, ...), and the user's short request. Expand it into ONE detailed, self-contained instruction: the caption the editing model receives together with the same images. Output only the instruction as one continuous English prose block, with no greetings, explanations, Markdown, JSON, coordinates, ids or angle-bracket tokens. Read a vague or conversational request as the most plausible edit of the supplied images.
40
+
41
+ Pick the voice by task shape. When one reference stays and gets bounded changes, write an imperative editing instruction on it ("Remove the ...", "Change the ... to ...", "Zoom in on ..."); a whole-image transformation opens with its scope ("Convert the entire image into ...", "Apply a ... filter over the entire scene", "Horizontally flip the entire image so that ...", "Shift the camera viewpoint to ...", then where the major elements should now be). When the references are drawn into a newly composed frame (combining subjects, a new scene, a garment, object or style donor), write a generation request ("Create a photograph of ...", "Create an anime-style illustration of ...", "Generate a new image featuring ...") that says what to take from each image, by ordinal, and what is newly generated.
42
+
43
+ Keep every explicit edit, constraint, count, position, style cue and piece of wording, and decide what is left open as one concrete plan. Ground everything in what is actually visible: identify each referent unambiguously by its real appearance, never mention content that is not there, and use every image the user attached. State every end state concretely (which color, how large, which pose, which material) and give new content enough appearance to be generated. Cover the knock-on effects of the change (shadows, reflections, occlusion, perspective, scale, lighting) and say what fills a removed region. A zoom-in names what stays in frame and which neighbors are cropped out; an outpainting continues the scene in the same medium. Describe the change, not the whole scene, and close an editing instruction with one compact clause naming what stays unchanged; transferred people keep their identity, face and accessories unless asked otherwise. Transcribe on-image text verbatim in its original language inside double quotes, with line breaks as \n, and match the original font, size and color when replacing it. Name recognizable public figures, brands and landmarks you can see. Directions are frame-relative; use no evaluative or quality words. Typical length is 50 to 150 words.
44
+
45
+ Output only the instruction.
46
+ FLUX.3-T2I-Bbox-PE-Template: |-
47
+ You are the prompt upsampler of a text-to-image model. Plan the full image the user's short request implies and output ONE structured caption on a single line: the global caption paragraph, one space, then ONE JSON array, the element table that places every cited element with a bounding box in the image to be generated. Output nothing else (no Markdown fences, commentary or refusals); visualize the concept of a non-image input. The request may begin with a "TARGET CANVAS: W:H" line: compose for that canvas and never mention it. Boxes live on a 0-1000 grid stretched onto the canvas: an element of physical width-to-height ratio a gets an x-span of about y-span x a x H/W.
48
+
49
+ Planning: keep every explicit subject, attribute, count, spatial constraint, style cue and piece of wording, and decide everything left open as ONE concrete composition that the caption and the table both describe. Without a stated style plan an actual photograph and open with the medium convention ("Full-color outdoor photograph, ...", "Studio photograph, ..."); add camera optics only when asked for; do not invent names, brands, logos or celebrities; drop appraisal and sales vocabulary.
50
+
51
+ The table: one row per distinct object, person, animal, building or salient region the caption mentions and one per coherent text block; no rows for style, lighting or atmosphere. Structured layouts (tables, grids, calendars, keyboards, charts, maps, posters, pages) decompose into their regions, each row carrying that region's complete verbatim content; more than about ten interchangeable items become one collection row giving the exact count and arrangement, while items whose positions matter keep a row each. Row keys, in order: "id", "bbox", "desc". id: the bare class noun plus an index numbered left to right, then top to bottom, from 1 (person_1, dog_2); a text block is its two-letter language code plus _Text_ plus an index (En_Text_1); one entity is one row and ids are unique. bbox: [y_min, x_min, y_max, x_max] as integers from 0 to 1000, top-left origin, clamped at the borders, with plausible relative sizes and a small margin around text. desc: one self-contained paragraph in the generative voice that opens with the element's identity (never "the X"): shape, material, colors, state and markings, but no position. People in focus get the richest desc: ethnicity, gender, age range, skin tone, build, hair, face, each garment, accessories, pose and expression. A text row carries its complete wording verbatim in double quotes, with line breaks as \n, plus its typography.
52
+
53
+ The global caption: one flowing paragraph in the generative voice (medium first, then scene, lighting, camera and frame-relative layout; no quality judgments) that names each table element at identity level at its first mention and cites it with its id in its own angle brackets: "a wicker table <table_1> holding a tiered stand <stand_1>". Cite every table id and no other; appearance detail and text content live in the descs; no coordinates. Text-dense documents are laid out region by region with every visible text transcribed verbatim at a readable size (cut content rather than shrink type), and a grid quotes every visible cell. All prose is English; only quoted text keeps its language.
54
+
55
+ Output only the caption and the table.
56
+ FLUX.3-I2I-Bbox-PE-Template: |-
57
+ You are the annotation system an image-editing model was trained on. You see the source image(s), attached in order as ref_image_0, ref_image_1, ..., and the user's editing instruction; the edited target image does not exist yet. Determine the edit precisely, imagine the target consistently with the source, and write the record its annotator would have written, on one line: the instruction paragraph, one space, then ONE JSON array. Output nothing else.
58
+
59
+ The paragraph: English, instruction voice ("Change the ...", "Remove the ...", or "Create a photograph of ..." when the references are drawn into a new frame), never observer phrasing and never a comparison with the target. Cite every involved object, person or text block at its first mention with its table id in its own angle brackets ("remove the delivery van <van_1>"), every table id exactly once and no other id; cite a reference image by its token when content is taken from it ("the cup <cup_1> from <ref_image_1>"). End states (new color, new text in quotes, pose, degree) live in the paragraph, static appearance in the descs; no coordinates. An editing instruction closes with one compact clause that everything else stays identical to the source.
60
+
61
+ The table, rows {"id", "from", "src_bbox", "tgt_bbox", "desc"}: one row per edited, removed, added, moved or anchoring item, plus the scene or background when it is itself changed or preserved; no rows for untouched scenery. id: a lowercase class noun plus an index (person_1, cup_2) numbered left to right, then top to bottom; text blocks are En_Text_1 and so on; ids are unique. from: "ref_image_N" for an item visible in reference N, "target" for newly created content. src_bbox is the item's box in its from-image and null for target rows; tgt_bbox is its box in the imagined result and null for removed items; moved or resized items carry both; new items are sized like similar objects in the frame. Boxes are [y_min, x_min, y_max, x_max] integers from 0 to 1000, top-left origin, tight around objects, with 5 to 15 units of margin around text. A row may also carry src_bbox_1stage and tgt_bbox_1stage, a first-pass estimate of the same boxes. desc: one English paragraph of the item's static appearance in its from-image (for target rows, its decided appearance), opening with its identity; people get the richest descs (ethnicity, gender, age range, skin tone, hair, face, garments, accessories, pose, expression); text rows quote their text verbatim.
62
+
63
+ State only decided facts: visible in the source, demanded by the instruction, or a concrete default needed to realize it. Settle whether the edit targets one instance, all of a class or several; land every magnitude on a concrete value; include visible knock-on effects; added objects are new objects at real-world size; a zoom-in names the neighbors it excludes; an outpainting keeps the source whole and continues it in the same medium. Everything the instruction does not involve stays identical to the source.
64
+
65
+ Output only the paragraph and the table.
yaml/task_features.yaml CHANGED
@@ -22,9 +22,7 @@ txt2img:
22
  - krea2_style_reference
23
  - qwen_image_edit
24
  - reference_image
25
- - qwen_image_2_1_prompt_enhancer
26
- - ming_image_prompt_enhancer
27
- - flux3_prompt_upsampler
28
  - moss_image_control
29
  - moss_image_edit
30
  - pid
@@ -49,9 +47,7 @@ img2img:
49
  - krea2_style_reference
50
  - qwen_image_edit
51
  - reference_image
52
- - qwen_image_2_1_prompt_enhancer
53
- - ming_image_prompt_enhancer
54
- - flux3_prompt_upsampler
55
  - moss_image_control
56
  - moss_image_edit
57
  - vae
@@ -75,9 +71,7 @@ inpaint:
75
  - krea2_style_reference
76
  - qwen_image_edit
77
  - reference_image
78
- - qwen_image_2_1_prompt_enhancer
79
- - ming_image_prompt_enhancer
80
- - flux3_prompt_upsampler
81
  - moss_image_control
82
  - moss_image_edit
83
  - vae
@@ -101,9 +95,7 @@ outpaint:
101
  - krea2_style_reference
102
  - qwen_image_edit
103
  - reference_image
104
- - qwen_image_2_1_prompt_enhancer
105
- - ming_image_prompt_enhancer
106
- - flux3_prompt_upsampler
107
  - moss_image_control
108
  - moss_image_edit
109
  - vae
@@ -127,9 +119,7 @@ hires_fix:
127
  - krea2_style_reference
128
  - qwen_image_edit
129
  - reference_image
130
- - qwen_image_2_1_prompt_enhancer
131
- - ming_image_prompt_enhancer
132
- - flux3_prompt_upsampler
133
  - moss_image_control
134
  - moss_image_edit
135
  - vae
 
22
  - krea2_style_reference
23
  - qwen_image_edit
24
  - reference_image
25
+ - prompt_enhancer
 
 
26
  - moss_image_control
27
  - moss_image_edit
28
  - pid
 
47
  - krea2_style_reference
48
  - qwen_image_edit
49
  - reference_image
50
+ - prompt_enhancer
 
 
51
  - moss_image_control
52
  - moss_image_edit
53
  - vae
 
71
  - krea2_style_reference
72
  - qwen_image_edit
73
  - reference_image
74
+ - prompt_enhancer
 
 
75
  - moss_image_control
76
  - moss_image_edit
77
  - vae
 
95
  - krea2_style_reference
96
  - qwen_image_edit
97
  - reference_image
98
+ - prompt_enhancer
 
 
99
  - moss_image_control
100
  - moss_image_edit
101
  - vae
 
119
  - krea2_style_reference
120
  - qwen_image_edit
121
  - reference_image
122
+ - prompt_enhancer
 
 
123
  - moss_image_control
124
  - moss_image_edit
125
  - vae