-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathtrigger_words.py
More file actions
378 lines (298 loc) · 14.2 KB
/
Copy pathtrigger_words.py
File metadata and controls
378 lines (298 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
"""
LoRA Trigger Word Management
Handles fetching, filtering, and processing of LoRA trigger words from Civitai
"""
import re
from functools import lru_cache
from .lora_utils import load_and_save_tags
from .config_utils import parse_lora_definition
def _should_omit_trigger(trigger, omit_normalized):
"""
Check if a trigger word should be omitted using token-based word boundary matching.
Uses regex word boundaries so "blue" omits "blue" but not "blueberry".
Falls back to exact match for multi-word omit terms.
Args:
trigger: The cleaned trigger string (lowercase)
omit_normalized: List of normalized omit terms (lowercase, stripped)
Returns:
True if the trigger should be omitted
"""
for omit_term in omit_normalized:
# Use word boundary regex: ensures "blue" matches "blue" but not "blueberry"
if re.search(r'\b' + re.escape(omit_term) + r'\b', trigger, re.IGNORECASE):
return True
return False
@lru_cache(maxsize=128)
def _get_filtered_lora_triggers_cached(lora_string, omit_tuple):
"""
Cached internal implementation. Takes hashable args (tuple instead of list).
"""
active_loras = parse_lora_definition(lora_string)
trigger_list = []
# Normalize omit list: strip whitespace and trailing commas, lowercase
omit_normalized = [str(t).lower().strip().rstrip(',').strip() for t in omit_tuple]
for lora_def in active_loras:
lname, lstr_m, lstr_c = lora_def
try:
civitai_tags_list = load_and_save_tags(lname, force_fetch=False)
if len(civitai_tags_list) > 0:
for tags in civitai_tags_list:
# Clean the trigger: strip whitespace and trailing commas
cleaned_tags = tags.strip().rstrip(',').strip()
# Check if this trigger should be omitted (token-based word boundary matching)
if not _should_omit_trigger(cleaned_tags.lower(), omit_normalized):
trigger_list.append(cleaned_tags)
except Exception as e:
pass
return trigger_list
def get_filtered_lora_triggers(lora_string, omit_list, lookup_triggers=True):
"""
Get LoRA trigger words with filtering applied.
Args:
lora_string: LoRA definition string (e.g., "lora1.safetensors:0.8:0.6 + lora2.safetensors:1.0:1.0")
omit_list: List of trigger words to omit
lookup_triggers: Whether to lookup triggers from Civitai
Returns:
List of filtered trigger words
"""
if not lookup_triggers or lora_string == "None":
return []
# Convert list to tuple for hashable cache key
return list(_get_filtered_lora_triggers_cached(lora_string, tuple(omit_list)))
def get_trigger_placement_for_lora(lora_name, config, lora_triggerwords_mode):
"""
Determine where to place trigger words for a specific LoRA.
Args:
lora_name: The LoRA filename (e.g., "XL/Tmp-Trendy/lora1.safetensors")
config: Configuration dictionary
lora_triggerwords_mode: The global mode (None, Append To End, Append To Start, Read From Config)
Returns:
str: "start", "end", or "none"
"""
if lora_triggerwords_mode == "None":
return "none"
elif lora_triggerwords_mode == "Append To End":
return "end"
elif lora_triggerwords_mode == "Append To Start":
return "start"
elif lora_triggerwords_mode == "Read From Config":
# Check if config has lora_triggerwords_append_settings
settings = config.get("lora_triggerwords_append_settings", {})
# Normalize the lora_name path (handle both / and \)
normalized_lora = lora_name.replace('\\', '/')
# print(f"[DEBUG] lora_name: {lora_name}")
# print(f"[DEBUG] normalized_lora: {normalized_lora}")
# print(f"[DEBUG] settings: {settings}")
# Try exact match first (with original path)
if lora_name in settings:
placement = settings[lora_name].lower()
if placement in ["start", "end"]:
# print(f"[DEBUG] Exact match (original): {placement}")
return placement
# Try exact match with normalized path
if normalized_lora != lora_name and normalized_lora in settings:
placement = settings[normalized_lora].lower()
if placement in ["start", "end"]:
# print(f"[DEBUG] Exact match (normalized): {placement}")
return placement
# Try matching without extension (normalized)
lora_base = normalized_lora.replace('.safetensors', '').replace('.ckpt', '').replace('.pt', '')
if lora_base in settings:
placement = settings[lora_base].lower()
if placement in ["start", "end"]:
# print(f"[DEBUG] Base name match: {placement}")
return placement
# Try matching against folders (check if lora is in any configured folder)
for setting_key, placement in settings.items():
# Normalize the setting key as well
normalized_key = setting_key.replace('\\', '/')
# print(f"[DEBUG] Checking folder: '{normalized_key}' vs '{normalized_lora}'")
# Check if this is a folder setting (ends with /)
if normalized_key.endswith('/'):
# Check if the normalized lora path starts with this normalized folder path
if normalized_lora.startswith(normalized_key):
placement_lower = placement.lower()
if placement_lower in ["start", "end"]:
# print(f"[DEBUG] Folder match! '{normalized_lora}' starts with '{normalized_key}' -> {placement_lower}")
return placement_lower
# Default to end if not specified in config
# print(f"[DEBUG] No match found, defaulting to 'end'")
return "end"
return "none"
def collect_unique_prompts_with_triggers(expanded_configs, lora_triggerwords_mode):
"""
Collect all unique prompts from configs, applying LoRA trigger words where needed.
Args:
expanded_configs: List of expanded configuration dictionaries
lora_triggerwords_mode: Mode for trigger word placement
Returns:
tuple: (unique_positives set, unique_negatives set)
"""
unique_positives = set()
unique_negatives = set()
for conf in expanded_configs:
full_positive = conf["positive"]
if lora_triggerwords_mode != "None" and conf["lora"] != "None":
omit_list = conf.get("lora_omit_triggers", [])
trigger_list = get_filtered_lora_triggers(
conf["lora"],
omit_list,
lookup_triggers=True
)
if not trigger_list:
print(f"[GridTester] ℹ️ No trigger words found for LoRA: {conf['lora'][:60]}")
if trigger_list:
# Parse the lora string to get individual loras
active_loras = parse_lora_definition(conf["lora"])
# Separate triggers by placement
start_triggers = []
end_triggers = []
for lora_def in active_loras:
lname, _, _ = lora_def
placement = get_trigger_placement_for_lora(lname, conf, lora_triggerwords_mode)
# Get triggers specific to this lora
try:
lora_triggers = load_and_save_tags(lname, force_fetch=False)
omit_normalized = [str(t).lower().strip().rstrip(',').strip() for t in omit_list]
for tag in lora_triggers:
cleaned_tag = tag.strip().rstrip(',').strip()
if not _should_omit_trigger(cleaned_tag.lower(), omit_normalized):
if placement == "start":
start_triggers.append(cleaned_tag)
elif placement == "end":
end_triggers.append(cleaned_tag)
except:
pass
# Build the full prompt with triggers in correct positions
parts = []
if start_triggers:
parts.append(", ".join(start_triggers))
parts.append(conf['positive'])
if end_triggers:
parts.append(", ".join(end_triggers))
full_positive = ", ".join(parts)
# Show omitted triggers only once during pre-encoding
if omit_list:
all_triggers = []
active_loras = parse_lora_definition(conf["lora"])
for lora_def in active_loras:
lname, _, _ = lora_def
try:
tags = load_and_save_tags(lname, force_fetch=False)
all_triggers.extend([t.strip().rstrip(',').strip() for t in tags])
except:
pass
omitted = set(all_triggers) - set(start_triggers + end_triggers)
if omitted:
print(f"[GridTester] 🚫 Omitted triggers: {', '.join(omitted)}")
# Apply model-specific prompt prefix/suffix (wraps entire prompt+triggers)
full_positive = _apply_model_prompt_affixes(full_positive, conf)
unique_positives.add(full_positive)
unique_negatives.add(conf["negative"])
return unique_positives, unique_negatives
_build_prompt_cache = {}
def clear_trigger_caches():
"""
Clear all trigger word caches. Must be called at the start of each generation run
to ensure trigger words are re-read from loras_tags.json (which may have been updated
by a previous run's CivitAI fetch or manual edit).
Without this, @lru_cache on _get_filtered_lora_triggers_cached and the module-level
_build_prompt_cache dict persist stale results across runs within the same ComfyUI session.
"""
global _build_prompt_cache
_build_prompt_cache.clear()
_get_filtered_lora_triggers_cached.cache_clear()
print("[GridTester] 🔄 Trigger word caches cleared")
def _apply_model_prompt_affixes(prompt, config):
"""
Apply model-specific prompt prefix and suffix to a prompt string.
These are quality tags that should always wrap the prompt for specific model families,
e.g. "score_9, score_8_up" for Pony or "masterpiece, best quality" for SD1.5.
Args:
prompt: The assembled prompt string (may already include LoRA triggers)
config: Configuration dictionary with optional model_prompt_prefix/suffix
Returns:
The prompt with prefix/suffix applied
"""
prefix = config.get("model_prompt_prefix", "").strip()
suffix = config.get("model_prompt_suffix", "").strip()
if not prefix and not suffix:
return prompt
parts = []
if prefix:
parts.append(prefix)
parts.append(prompt)
if suffix:
parts.append(suffix)
return ", ".join(parts)
def build_prompt_with_triggers(config, lora_triggerwords_mode):
"""
Build final prompt with LoRA triggers and model-specific prefix/suffix applied.
Results are cached based on relevant config fields to avoid redundant work.
Args:
config: Configuration dictionary
lora_triggerwords_mode: Mode for trigger word placement
Returns:
tuple: (final_prompt, trigger_string)
"""
if config["lora"] == "None" or lora_triggerwords_mode == "None":
prompt = _apply_model_prompt_affixes(config["positive"], config)
return prompt, ""
omit_list = config.get("lora_omit_triggers", [])
append_settings = config.get("lora_triggerwords_append_settings", {})
# Build a hashable cache key from relevant config fields
cache_key = (
config["lora"],
config["positive"],
tuple(omit_list),
tuple(sorted(append_settings.items())) if append_settings else (),
lora_triggerwords_mode,
config.get("model_prompt_prefix", ""),
config.get("model_prompt_suffix", "")
)
if cache_key in _build_prompt_cache:
return _build_prompt_cache[cache_key]
# Parse the lora string to get individual loras
active_loras = parse_lora_definition(config["lora"])
# Separate triggers by placement
start_triggers = []
end_triggers = []
for lora_def in active_loras:
lname, _, _ = lora_def
placement = get_trigger_placement_for_lora(lname, config, lora_triggerwords_mode)
# Get triggers specific to this lora
try:
lora_triggers = load_and_save_tags(lname, force_fetch=False)
omit_normalized = [str(t).lower().strip().rstrip(',').strip() for t in omit_list]
for tag in lora_triggers:
cleaned_tag = tag.strip().rstrip(',').strip()
if not _should_omit_trigger(cleaned_tag.lower(), omit_normalized):
if placement == "start":
start_triggers.append(cleaned_tag)
elif placement == "end":
end_triggers.append(cleaned_tag)
except:
pass
# Build the trigger string for return value
all_triggers = start_triggers + end_triggers
trigger_string = ""
if all_triggers:
trigger_string = ", " + ", ".join(all_triggers)
print(f"[GridTester] 🏷️ Trigger words for {config['lora'][:40]}: {', '.join(all_triggers[:5])}" +
(f" (+{len(all_triggers)-5} more)" if len(all_triggers) > 5 else ""))
# Build the full prompt with triggers in correct positions
parts = []
if start_triggers:
parts.append(", ".join(start_triggers))
parts.append(config['positive'])
if end_triggers:
parts.append(", ".join(end_triggers))
final_prompt = ", ".join(parts)
# Apply model-specific prefix/suffix (wraps the entire prompt+triggers)
final_prompt = _apply_model_prompt_affixes(final_prompt, config)
result = (final_prompt, trigger_string)
# Store in cache (limit size to prevent memory issues)
if len(_build_prompt_cache) > 256:
_build_prompt_cache.clear()
_build_prompt_cache[cache_key] = result
return result