realrebelai commited on
Commit
c03b8fa
Β·
verified Β·
1 Parent(s): 85241eb

Upload 3 files

Browse files
RebelsPromptEnhancer/__init__.py ADDED
@@ -0,0 +1,979 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import gc
3
+ import os
4
+ import re
5
+ import base64
6
+ from io import BytesIO
7
+
8
+ import numpy as np
9
+ from PIL import Image
10
+ from llama_cpp import Llama
11
+
12
+ # Vision chat handlers β€” best-effort import.
13
+ _HANDLER_CLASSES = {}
14
+ for _label, _modname in [
15
+ ("LLaVA 1.5", "Llava15ChatHandler"),
16
+ ("LLaVA 1.6", "Llava16ChatHandler"),
17
+ ("Moondream", "MoondreamChatHandler"),
18
+ ("MiniCPM-V 2.6", "MiniCPMv26ChatHandler"),
19
+ ("NanoLLaVA", "NanoLlavaChatHandler"),
20
+ ("Qwen2.5-VL", "Qwen25VLChatHandler"),
21
+ ]:
22
+ try:
23
+ _mod = __import__("llama_cpp.llama_chat_format", fromlist=[_modname])
24
+ _HANDLER_CLASSES[_label] = getattr(_mod, _modname)
25
+ except Exception:
26
+ pass
27
+
28
+
29
+ # =========================================================================
30
+ # Shared cleaning helpers
31
+ # =========================================================================
32
+
33
+ REASONING_MARKERS = (
34
+ "let me", "i'll ", "i will ", "i need", "i must", "i should",
35
+ "the prompt is", "the user", "key elements", "brainstorm",
36
+ "as per the rules", "according to the rules", "the rules say",
37
+ "let's", "okay so", "first,", "second,", "third,",
38
+ "i can say", "i might", "since this", "so i",
39
+ )
40
+
41
+ PREAMBLES = (
42
+ "here's", "here is", "sure,", "sure!", "certainly,", "of course,",
43
+ "okay,", "okay.", "alright,",
44
+ "enhanced prompt:", "expanded prompt:", "prompt:",
45
+ "output:", "answer:", "final prompt:", "final:", "example:",
46
+ "the image shows", "the image depicts", "this image shows",
47
+ "in the image", "i can see", "i see",
48
+ )
49
+
50
+
51
+ def _strip_thinking_tags(text):
52
+ for pat in (
53
+ r"<think(?:ing)?>.*?</think(?:ing)?>",
54
+ r"<\|thinking\|>.*?<\|/thinking\|>",
55
+ r"\[THINK(?:ING)?\].*?\[/THINK(?:ING)?\]",
56
+ ):
57
+ text = re.sub(pat, "", text, flags=re.DOTALL | re.IGNORECASE)
58
+ return text
59
+
60
+
61
+ def _extract_final_paragraph(text):
62
+ paragraphs = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
63
+ if len(paragraphs) < 2:
64
+ return text
65
+ full_lower = text.lower()
66
+ if sum(1 for m in REASONING_MARKERS if m in full_lower) < 2:
67
+ return text
68
+ for p in reversed(paragraphs):
69
+ lower = p.lower().lstrip()
70
+ if any(lower.startswith(m) for m in REASONING_MARKERS):
71
+ continue
72
+ lines = p.split("\n")
73
+ bullets = sum(
74
+ 1 for l in lines
75
+ if l.strip().startswith(("-", "*", "β€’", "1.", "2.", "3."))
76
+ )
77
+ if bullets > 0 and bullets >= len(lines) / 2:
78
+ continue
79
+ if len(p) < 60:
80
+ continue
81
+ if sum(1 for m in REASONING_MARKERS if m in lower) >= 2:
82
+ continue
83
+ return p
84
+ return text
85
+
86
+
87
+ def _strip_preambles(text):
88
+ for _ in range(3):
89
+ lowered = text.lower().lstrip()
90
+ stripped = False
91
+ for p in PREAMBLES:
92
+ if lowered.startswith(p):
93
+ colon = text.find(":")
94
+ newline = text.find("\n")
95
+ if 0 < colon < 60:
96
+ text = text[colon + 1:].strip()
97
+ elif 0 < newline < 80:
98
+ text = text[newline + 1:].strip()
99
+ else:
100
+ text = text[len(p):].strip(" ,.:-")
101
+ stripped = True
102
+ break
103
+ if not stripped:
104
+ break
105
+ return text
106
+
107
+
108
+ def _clean_output(text, original_input=""):
109
+ text = text.strip()
110
+ text = _strip_thinking_tags(text)
111
+ text = _extract_final_paragraph(text)
112
+ text = _strip_preambles(text)
113
+ if original_input:
114
+ raw = original_input.strip()
115
+ if raw and text.lower().startswith(raw.lower()):
116
+ text = text[len(raw):].lstrip(" ,.:-\"'\n")
117
+ return text.strip().strip('"\'')
118
+
119
+
120
+ def _free_llm(llm):
121
+ try:
122
+ del llm
123
+ except Exception:
124
+ pass
125
+ gc.collect()
126
+ if torch.cuda.is_available():
127
+ torch.cuda.empty_cache()
128
+
129
+
130
+ def _list_ggufs(exclude_mmproj=True):
131
+ try:
132
+ node_dir = os.path.dirname(os.path.abspath(__file__))
133
+ files = []
134
+ for f in os.listdir(node_dir):
135
+ if not f.lower().endswith(".gguf"):
136
+ continue
137
+ if exclude_mmproj and f.lower().startswith("mmproj"):
138
+ continue
139
+ files.append(f)
140
+ files.sort()
141
+ return files if files else ["NO_GGUF_FILES_IN_FOLDER"]
142
+ except Exception:
143
+ return ["NO_GGUF_FILES_IN_FOLDER"]
144
+
145
+
146
+ def _list_mmproj():
147
+ try:
148
+ node_dir = os.path.dirname(os.path.abspath(__file__))
149
+ files = sorted(
150
+ f for f in os.listdir(node_dir)
151
+ if f.lower().endswith(".gguf") and f.lower().startswith("mmproj")
152
+ )
153
+ return files if files else ["NO_MMPROJ_FILE_FOUND"]
154
+ except Exception:
155
+ return ["NO_MMPROJ_FILE_FOUND"]
156
+
157
+
158
+ # =========================================================================
159
+ # Style system β€” Purpose + Model Format + Aesthetic
160
+ # =========================================================================
161
+
162
+ PURPOSE_OPTIONS = ["Image", "Video", "Edit (Inpainting/I2V)"]
163
+
164
+ PURPOSE_FRAMING = {
165
+ "Image": (
166
+ "Rewrite the user's input as a detailed prompt for a static image. "
167
+ "Cover subject, environment, lighting, composition, and mood."
168
+ ),
169
+ "Video": (
170
+ "Rewrite the user's input as a detailed prompt for a video shot. "
171
+ "Cover subject motion, camera movement, pacing, lighting, and mood."
172
+ ),
173
+ "Edit (Inpainting/I2V)": (
174
+ "Rewrite the user's edit instruction as a description of the final transformed "
175
+ "scene as it appears after the edit. Do not describe the editing process."
176
+ ),
177
+ }
178
+
179
+ MODEL_FORMAT_OPTIONS = [
180
+ "Flux / Chroma (natural language)",
181
+ "Z-Image / Lumina-2 (LLM text encoder)",
182
+ "HiDream (hybrid prose + descriptors)",
183
+ "SDXL (tags + weights)",
184
+ "SD 1.5 (tags + weights)",
185
+ "Pony / Illustrious (booru tags + score)",
186
+ "LTX Video (motion-focused prose)",
187
+ "Hunyuan / Wan Video (cinematic motion prose)",
188
+ "Universal Natural Language",
189
+ ]
190
+
191
+ MODEL_FORMAT_INSTRUCTIONS = {
192
+ "Flux / Chroma (natural language)": (
193
+ "Format the output as flowing natural language in long descriptive sentences "
194
+ "suitable for Flux or Chroma. No tag syntax. No parenthesis weights. "
195
+ "No 'masterpiece' or quality boosters. Describe the scene as prose."
196
+ ),
197
+ "Z-Image / Lumina-2 (LLM text encoder)": (
198
+ "Format the output as long, richly descriptive natural-language prose suitable "
199
+ "for LLM-based text encoders (Z-Image, Lumina-2). Use complex sentences and "
200
+ "vivid concrete detail. No tag syntax, no weights."
201
+ ),
202
+ "HiDream (hybrid prose + descriptors)": (
203
+ "Format the output as flowing natural language with concrete photographic and "
204
+ "material descriptors woven in (lighting type, lens feel, surface texture). "
205
+ "Suitable for HiDream's multi-encoder pipeline. No weight syntax."
206
+ ),
207
+ "SDXL (tags + weights)": (
208
+ "Format the output as a comma-separated list of descriptive tags for SDXL. "
209
+ "Start with quality tags (masterpiece, best quality, highly detailed). "
210
+ "Use parenthesis weight syntax for emphasis like (cinematic lighting:1.2). "
211
+ "Order: subject, action, environment, lighting, camera, style, quality."
212
+ ),
213
+ "SD 1.5 (tags + weights)": (
214
+ "Format the output as a comma-separated list of descriptive tags for SD 1.5. "
215
+ "Lead with quality boosters (masterpiece, best quality, ultra-detailed). "
216
+ "Use parenthesis weight syntax for emphasis. Keep tags compact and direct. "
217
+ "Order: subject, action, environment, lighting, style, quality."
218
+ ),
219
+ "Pony / Illustrious (booru tags + score)": (
220
+ "Format the output as comma-separated booru-style tags for Pony / Illustrious. "
221
+ "Lead with score tags: score_9, score_8_up, score_7_up. Include an appropriate "
222
+ "rating tag (rating_safe, rating_questionable, rating_explicit). Use underscores "
223
+ "for multi-word tags (long_hair, blue_eyes). "
224
+ "Order: score, rating, subject, character traits, action, setting, style."
225
+ ),
226
+ "LTX Video (motion-focused prose)": (
227
+ "Format the output as a video shot description for LTX Video. "
228
+ "Lead with a clear shot description, then describe subject motion explicitly "
229
+ "(what moves and how), camera movement (pan, dolly, zoom, tracking), pacing, "
230
+ "and atmosphere. Natural language sentences. No tag syntax."
231
+ ),
232
+ "Hunyuan / Wan Video (cinematic motion prose)": (
233
+ "Format the output as a cinematic video description with detailed motion. "
234
+ "Cover camera angle, camera movement, subject action, environmental motion, "
235
+ "atmosphere, and pacing. Natural language sentences. No tag syntax."
236
+ ),
237
+ "Universal Natural Language": (
238
+ "Format the output as a flowing natural language paragraph describing the scene "
239
+ "in concrete visual detail. No tags, no weights."
240
+ ),
241
+ }
242
+
243
+ AESTHETIC_OPTIONS = [
244
+ "None (no aesthetic injection)",
245
+ "Photorealistic",
246
+ "Cinematic Film",
247
+ "Anime / Manga",
248
+ "Studio Ghibli",
249
+ "Pixar / 3D Animation",
250
+ "Comic Book / Graphic Novel",
251
+ "Concept Art",
252
+ "Oil Painting",
253
+ "Watercolor",
254
+ "Pencil Sketch",
255
+ "Cyberpunk",
256
+ "Steampunk",
257
+ "Fantasy",
258
+ "Sci-Fi",
259
+ "Horror / Dark",
260
+ "Vintage / Retro Film",
261
+ "Film Noir",
262
+ "Glamour / Editorial",
263
+ "Minimalist",
264
+ "Surreal / Dreamy",
265
+ "3D Render / CGI",
266
+ ]
267
+
268
+ AESTHETIC_DESCRIPTORS = {
269
+ "Photorealistic": (
270
+ "Visual style: photorealistic, sharp focus, accurate textures, lifelike skin and "
271
+ "materials, realistic lighting and shadows, shallow depth of field, 8k camera detail."
272
+ ),
273
+ "Cinematic Film": (
274
+ "Visual style: cinematic film aesthetic, dramatic lighting with strong key and rim, "
275
+ "filmic color grade, anamorphic framing, atmospheric haze, shallow depth of field."
276
+ ),
277
+ "Anime / Manga": (
278
+ "Visual style: anime/manga aesthetic, cel-shaded coloring, stylized features, "
279
+ "expressive large eyes, dynamic poses, vibrant saturated color palette, clean lineart."
280
+ ),
281
+ "Studio Ghibli": (
282
+ "Visual style: Studio Ghibli aesthetic, hand-painted watercolor backgrounds, "
283
+ "soft natural lighting, warm pastoral atmosphere, gentle character designs, "
284
+ "nostalgic and serene mood."
285
+ ),
286
+ "Pixar / 3D Animation": (
287
+ "Visual style: 3D animation aesthetic in the spirit of Pixar/Disney, expressive "
288
+ "stylized character proportions, polished CG surfaces, warm cinematic lighting, "
289
+ "vibrant family-friendly color palette."
290
+ ),
291
+ "Comic Book / Graphic Novel": (
292
+ "Visual style: comic book aesthetic, bold ink linework, halftone or hatching shading, "
293
+ "dynamic poses, exaggerated proportions, saturated panel colors."
294
+ ),
295
+ "Concept Art": (
296
+ "Visual style: digital concept art, painterly brushwork, atmospheric perspective, "
297
+ "value-driven composition, loose suggestion of detail over full rendering, "
298
+ "professional production-art feel."
299
+ ),
300
+ "Oil Painting": (
301
+ "Visual style: oil painting aesthetic, visible impasto brushwork, rich color depth, "
302
+ "painterly textures, classical composition, warm gallery lighting."
303
+ ),
304
+ "Watercolor": (
305
+ "Visual style: watercolor painting aesthetic, soft translucent washes, paper texture "
306
+ "visible, flowing pigment bleeds, gentle edges, limited palette."
307
+ ),
308
+ "Pencil Sketch": (
309
+ "Visual style: pencil sketch aesthetic, graphite linework, crosshatching shadows, "
310
+ "monochrome or restrained color accents, loose unfinished sketchbook feel."
311
+ ),
312
+ "Cyberpunk": (
313
+ "Visual style: cyberpunk aesthetic, neon signage, rain-slicked streets, holographic "
314
+ "interfaces, cybernetic implants, dystopian high-tech low-life atmosphere, "
315
+ "magenta-cyan color palette, deep shadows with glowing accents."
316
+ ),
317
+ "Steampunk": (
318
+ "Visual style: steampunk aesthetic, brass and copper machinery, Victorian-era styling, "
319
+ "exposed gears and clockwork, gas-lamp lighting, sepia and bronze color palette, "
320
+ "steam and soot atmosphere."
321
+ ),
322
+ "Fantasy": (
323
+ "Visual style: high fantasy aesthetic, medieval or magical setting, ethereal lighting, "
324
+ "ornate detail, mythological elements, lush detailed environments, painterly atmosphere."
325
+ ),
326
+ "Sci-Fi": (
327
+ "Visual style: science fiction aesthetic, advanced technology, sleek futuristic surfaces, "
328
+ "panel-screen lighting, industrial design, blue and white accent palette."
329
+ ),
330
+ "Horror / Dark": (
331
+ "Visual style: horror aesthetic, low-key dramatic lighting, deep shadows, "
332
+ "unsettling atmosphere, desaturated muted palette with occasional blood-red accents, "
333
+ "dread-filled mood."
334
+ ),
335
+ "Vintage / Retro Film": (
336
+ "Visual style: vintage film aesthetic, 35mm grain, faded warm color cast, soft contrast, "
337
+ "period-appropriate styling, light leaks and lens artifacts."
338
+ ),
339
+ "Film Noir": (
340
+ "Visual style: film noir aesthetic, high-contrast black-and-white or near-monochrome, "
341
+ "dramatic chiaroscuro lighting, venetian-blind shadows, urban night atmosphere, "
342
+ "cigarette smoke and rain."
343
+ ),
344
+ "Glamour / Editorial": (
345
+ "Visual style: high-fashion editorial aesthetic, polished beauty lighting, "
346
+ "magazine-shoot composition, soft skin rendering, dramatic backdrop, "
347
+ "professional studio styling."
348
+ ),
349
+ "Minimalist": (
350
+ "Visual style: minimalist aesthetic, clean composition, generous negative space, "
351
+ "limited color palette, simple geometric forms, calm uncluttered framing."
352
+ ),
353
+ "Surreal / Dreamy": (
354
+ "Visual style: surreal dreamlike aesthetic, soft hazy lighting, impossible compositions, "
355
+ "dream-logic juxtapositions, ethereal color shifts, painterly atmosphere."
356
+ ),
357
+ "3D Render / CGI": (
358
+ "Visual style: 3D render aesthetic, clean CGI surfaces, ray-traced lighting, "
359
+ "sharp reflections, subsurface scattering, Octane/Blender render quality."
360
+ ),
361
+ }
362
+
363
+
364
+ def build_layered_system_prompt(purpose, model_format, aesthetic,
365
+ extra_instructions="", append_no_think=False):
366
+ """Stitch purpose + aesthetic + model format into a single system prompt."""
367
+ parts = [PURPOSE_FRAMING[purpose]]
368
+ if aesthetic in AESTHETIC_DESCRIPTORS:
369
+ parts.append(AESTHETIC_DESCRIPTORS[aesthetic])
370
+ parts.append(MODEL_FORMAT_INSTRUCTIONS[model_format])
371
+ if extra_instructions.strip():
372
+ parts.append(extra_instructions.strip())
373
+ parts.append("Output only the prompt, nothing else.")
374
+ text = " ".join(parts)
375
+ if append_no_think:
376
+ text = text.rstrip() + " /no_think"
377
+ return text
378
+
379
+
380
+ # =========================================================================
381
+ # Node 1: Curated Qwen3.5-4B enhancer
382
+ # =========================================================================
383
+
384
+ class RebelsPromptEnhancer:
385
+ def __init__(self):
386
+ pass
387
+
388
+ _cache = {}
389
+
390
+ PRECISION_OPTIONS = ["Efficiency (UD-IQ2)", "Quality (UD-Q8)"]
391
+ PRECISION_SEARCH = {
392
+ "Efficiency (UD-IQ2)": ["qwen3.5-4b", "ud-iq2"],
393
+ "Quality (UD-Q8)": ["qwen3.5-4b", "ud-q8"],
394
+ }
395
+
396
+ @classmethod
397
+ def INPUT_TYPES(s):
398
+ return {
399
+ "required": {
400
+ "raw_prompt": ("STRING", {"multiline": True}),
401
+ "purpose": (PURPOSE_OPTIONS,),
402
+ "model_format": (MODEL_FORMAT_OPTIONS,),
403
+ "aesthetic": (AESTHETIC_OPTIONS,),
404
+ "precision": (s.PRECISION_OPTIONS,),
405
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
406
+ "lock_in": ("BOOLEAN", {
407
+ "default": False,
408
+ "label_on": "πŸ”’ LOCKED (cached)",
409
+ "label_off": "πŸ”„ LIVE (generating)",
410
+ }),
411
+ },
412
+ }
413
+
414
+ RETURN_TYPES = ("STRING", "STRING")
415
+ RETURN_NAMES = ("enhanced_prompt", "thought_process")
416
+ FUNCTION = "enhance"
417
+ CATEGORY = "Rebel AI"
418
+
419
+ @classmethod
420
+ def IS_CHANGED(cls, raw_prompt, purpose, model_format, aesthetic, precision, seed, lock_in):
421
+ if lock_in:
422
+ return f"LOCKED|{raw_prompt}|{purpose}|{model_format}|{aesthetic}|{precision}"
423
+ return float("nan")
424
+
425
+ def _find_files(self, node_dir, terms):
426
+ terms = [t.lower() for t in terms]
427
+ return [
428
+ f for f in os.listdir(node_dir)
429
+ if f.lower().endswith(".gguf") and all(t in f.lower() for t in terms)
430
+ ]
431
+
432
+ def enhance(self, raw_prompt, purpose, model_format, aesthetic, precision, seed, lock_in):
433
+ cache_key = (raw_prompt, purpose, model_format, aesthetic, precision)
434
+
435
+ if lock_in and cache_key in self._cache:
436
+ c = self._cache[cache_key]
437
+ thought = (
438
+ f"=== πŸ”’ LOCKED β€” Returning Cached Output ===\n"
439
+ f"No model load. No VRAM used. Seed ignored.\n\n"
440
+ f"=== Original Generation Info ===\n{c['meta']}\n"
441
+ f"=== Cached System Prompt ===\n{c['sys_prompt']}\n\n"
442
+ f"=== Cached Raw Output ===\n{c['raw_output']}\n\n"
443
+ f"=== Cached Final Prompt ===\n{c['final_prompt']}"
444
+ )
445
+ return (c['final_prompt'], thought)
446
+
447
+ node_dir = os.path.dirname(os.path.abspath(__file__))
448
+ terms = self.PRECISION_SEARCH[precision]
449
+ files = self._find_files(node_dir, terms)
450
+ if not files:
451
+ raise FileNotFoundError(
452
+ f"No .gguf in {node_dir} matching {terms}.\n"
453
+ f"Expected a filename containing: {' AND '.join(terms)}"
454
+ )
455
+ files.sort(key=lambda x: (len(x), x))
456
+ model_file = files[0]
457
+ model_path = os.path.join(node_dir, model_file)
458
+ n_ctx = 4096
459
+
460
+ sys_prompt = build_layered_system_prompt(
461
+ purpose, model_format, aesthetic, append_no_think=True
462
+ )
463
+
464
+ llm = Llama(model_path=model_path, n_gpu_layers=-1, verbose=False,
465
+ n_ctx=n_ctx, seed=seed)
466
+ try:
467
+ output = llm.create_chat_completion(
468
+ messages=[
469
+ {"role": "system", "content": sys_prompt},
470
+ {"role": "user", "content": raw_prompt},
471
+ ],
472
+ max_tokens=500, temperature=0.7, top_p=0.9, repeat_penalty=1.15,
473
+ stop=["\n\nUser:", "\n\nAssistant:", "Human:", "</think>", "</thinking>"],
474
+ )
475
+ raw_output = output["choices"][0]["message"]["content"].strip()
476
+ finally:
477
+ _free_llm(llm)
478
+
479
+ final_prompt = _clean_output(raw_output, raw_prompt)
480
+ if not final_prompt.strip() or len(final_prompt) < 30:
481
+ final_prompt = raw_output
482
+
483
+ meta_block = (
484
+ f"File: {model_file}\n"
485
+ f"Precision: {precision}\n"
486
+ f"Purpose: {purpose}\n"
487
+ f"Model Format: {model_format}\n"
488
+ f"Aesthetic: {aesthetic}\n"
489
+ f"Context: {n_ctx}\n"
490
+ f"Seed: {seed}\n"
491
+ f"Raw chars: {len(raw_output)}\n"
492
+ f"Clean chars: {len(final_prompt)}\n"
493
+ f"Stripped: {len(raw_output) - len(final_prompt)} chars\n"
494
+ )
495
+ thought = (
496
+ f"=== πŸ”„ LIVE β€” Fresh Generation ===\n\n"
497
+ f"=== Run Info ===\n{meta_block}\n"
498
+ f"=== Assembled System Prompt ===\n{sys_prompt}\n\n"
499
+ f"=== Raw Model Output ===\n{raw_output}\n\n"
500
+ f"=== Final Prompt ===\n{final_prompt}"
501
+ )
502
+ self._cache[cache_key] = {
503
+ "final_prompt": final_prompt, "raw_output": raw_output,
504
+ "meta": meta_block, "sys_prompt": sys_prompt,
505
+ }
506
+ return (final_prompt, thought)
507
+
508
+
509
+ # =========================================================================
510
+ # Node 2: Custom GGUF enhancer (any text model)
511
+ # =========================================================================
512
+
513
+ class RebelsPromptEnhancerCustom:
514
+ def __init__(self):
515
+ pass
516
+
517
+ _cache = {}
518
+
519
+ @classmethod
520
+ def INPUT_TYPES(s):
521
+ return {
522
+ "required": {
523
+ "raw_prompt": ("STRING", {"multiline": True}),
524
+ "model_file": (_list_ggufs(),),
525
+ "purpose": (PURPOSE_OPTIONS,),
526
+ "model_format": (MODEL_FORMAT_OPTIONS,),
527
+ "aesthetic": (AESTHETIC_OPTIONS,),
528
+ "extra_instructions": ("STRING", {
529
+ "multiline": True,
530
+ "default": "",
531
+ "placeholder": "Optional extra instructions appended to the system prompt (e.g. 'Avoid clichΓ©s.' or 'Emphasize hands.')",
532
+ }),
533
+ "system_prompt_override": ("STRING", {
534
+ "multiline": True,
535
+ "default": "",
536
+ "placeholder": "If non-empty, this REPLACES the entire layered system prompt. Leave blank to use Purpose+Format+Aesthetic.",
537
+ }),
538
+ "append_no_think": ("BOOLEAN", {
539
+ "default": False,
540
+ "label_on": "Append /no_think",
541
+ "label_off": "Don't append",
542
+ }),
543
+ "n_gpu_layers": ("INT", {"default": -1, "min": -1, "max": 999, "step": 1}),
544
+ "n_ctx": ("INT", {"default": 4096, "min": 512, "max": 32768, "step": 512}),
545
+ "max_tokens": ("INT", {"default": 500, "min": 50, "max": 4096, "step": 50}),
546
+ "temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.05}),
547
+ "top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.05}),
548
+ "repeat_penalty": ("FLOAT", {"default": 1.15, "min": 1.0, "max": 2.0, "step": 0.05}),
549
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
550
+ "lock_in": ("BOOLEAN", {
551
+ "default": False,
552
+ "label_on": "πŸ”’ LOCKED (cached)",
553
+ "label_off": "πŸ”„ LIVE (generating)",
554
+ }),
555
+ },
556
+ }
557
+
558
+ RETURN_TYPES = ("STRING", "STRING")
559
+ RETURN_NAMES = ("enhanced_prompt", "thought_process")
560
+ FUNCTION = "enhance"
561
+ CATEGORY = "Rebel AI"
562
+
563
+ @classmethod
564
+ def IS_CHANGED(cls, raw_prompt, model_file, purpose, model_format, aesthetic,
565
+ extra_instructions, system_prompt_override, append_no_think,
566
+ n_gpu_layers, n_ctx, max_tokens, temperature, top_p,
567
+ repeat_penalty, seed, lock_in):
568
+ if lock_in:
569
+ return (
570
+ f"LOCKED|{raw_prompt}|{model_file}|{purpose}|{model_format}|{aesthetic}|"
571
+ f"{extra_instructions}|{system_prompt_override}|{append_no_think}|"
572
+ f"{temperature}|{top_p}|{repeat_penalty}"
573
+ )
574
+ return float("nan")
575
+
576
+ def _build_sys_prompt(self, purpose, model_format, aesthetic,
577
+ extra_instructions, override, append_no_think):
578
+ if override.strip():
579
+ base = override.strip()
580
+ if append_no_think:
581
+ base = base.rstrip() + " /no_think"
582
+ return base
583
+ return build_layered_system_prompt(
584
+ purpose, model_format, aesthetic,
585
+ extra_instructions=extra_instructions,
586
+ append_no_think=append_no_think,
587
+ )
588
+
589
+ def enhance(self, raw_prompt, model_file, purpose, model_format, aesthetic,
590
+ extra_instructions, system_prompt_override, append_no_think,
591
+ n_gpu_layers, n_ctx, max_tokens, temperature, top_p,
592
+ repeat_penalty, seed, lock_in):
593
+ cache_key = (
594
+ raw_prompt, model_file, purpose, model_format, aesthetic,
595
+ extra_instructions, system_prompt_override, append_no_think,
596
+ temperature, top_p, repeat_penalty,
597
+ )
598
+
599
+ if lock_in and cache_key in self._cache:
600
+ c = self._cache[cache_key]
601
+ thought = (
602
+ f"=== πŸ”’ LOCKED β€” Returning Cached Output ===\n"
603
+ f"No model load. No VRAM used. Seed ignored.\n\n"
604
+ f"=== Original Generation Info ===\n{c['meta']}\n"
605
+ f"=== Cached System Prompt ===\n{c['sys_prompt']}\n\n"
606
+ f"=== Cached Raw Output ===\n{c['raw_output']}\n\n"
607
+ f"=== Cached Final Prompt ===\n{c['final_prompt']}"
608
+ )
609
+ return (c['final_prompt'], thought)
610
+
611
+ if model_file == "NO_GGUF_FILES_IN_FOLDER":
612
+ raise FileNotFoundError("Drop a .gguf in the node folder and restart ComfyUI.")
613
+ node_dir = os.path.dirname(os.path.abspath(__file__))
614
+ model_path = os.path.join(node_dir, model_file)
615
+ if not os.path.isfile(model_path):
616
+ raise FileNotFoundError(f"Selected file not found: {model_path}. Restart ComfyUI to refresh.")
617
+
618
+ sys_prompt = self._build_sys_prompt(
619
+ purpose, model_format, aesthetic,
620
+ extra_instructions, system_prompt_override, append_no_think,
621
+ )
622
+
623
+ llm = Llama(model_path=model_path, n_gpu_layers=n_gpu_layers, verbose=False,
624
+ n_ctx=n_ctx, seed=seed)
625
+ try:
626
+ output = llm.create_chat_completion(
627
+ messages=[
628
+ {"role": "system", "content": sys_prompt},
629
+ {"role": "user", "content": raw_prompt},
630
+ ],
631
+ max_tokens=max_tokens, temperature=temperature, top_p=top_p,
632
+ repeat_penalty=repeat_penalty,
633
+ stop=["\n\nUser:", "\n\nAssistant:", "Human:", "</think>", "</thinking>"],
634
+ )
635
+ raw_output = output["choices"][0]["message"]["content"].strip()
636
+ finally:
637
+ _free_llm(llm)
638
+
639
+ final_prompt = _clean_output(raw_output, raw_prompt)
640
+ if not final_prompt.strip() or len(final_prompt) < 30:
641
+ final_prompt = raw_output
642
+
643
+ gpu_label = "all" if n_gpu_layers < 0 else ("CPU only" if n_gpu_layers == 0 else f"{n_gpu_layers} layers")
644
+ override_status = "ACTIVE (custom override used)" if system_prompt_override.strip() else "inactive"
645
+ meta_block = (
646
+ f"File: {model_file}\n"
647
+ f"Purpose: {purpose}\n"
648
+ f"Model Format: {model_format}\n"
649
+ f"Aesthetic: {aesthetic}\n"
650
+ f"Override: {override_status}\n"
651
+ f"GPU layers: {gpu_label}\n"
652
+ f"Context: {n_ctx}\n"
653
+ f"Max tokens: {max_tokens}\n"
654
+ f"Temperature: {temperature}\n"
655
+ f"top_p: {top_p}\n"
656
+ f"repeat_penalty: {repeat_penalty}\n"
657
+ f"/no_think: {'yes' if append_no_think else 'no'}\n"
658
+ f"Seed: {seed}\n"
659
+ f"Raw chars: {len(raw_output)}\n"
660
+ f"Clean chars: {len(final_prompt)}\n"
661
+ f"Stripped: {len(raw_output) - len(final_prompt)} chars\n"
662
+ )
663
+ thought = (
664
+ f"=== πŸ”„ LIVE β€” Fresh Generation (Custom) ===\n\n"
665
+ f"=== Run Settings ===\n{meta_block}\n"
666
+ f"=== Assembled System Prompt ===\n{sys_prompt}\n\n"
667
+ f"=== Raw Model Output ===\n{raw_output}\n\n"
668
+ f"=== Final Prompt ===\n{final_prompt}"
669
+ )
670
+ self._cache[cache_key] = {
671
+ "final_prompt": final_prompt, "raw_output": raw_output,
672
+ "meta": meta_block, "sys_prompt": sys_prompt,
673
+ }
674
+ return (final_prompt, thought)
675
+
676
+
677
+ # =========================================================================
678
+ # Node 3: Image-to-Prompt vision node
679
+ # =========================================================================
680
+
681
+ class RebelsImageToPrompt:
682
+ def __init__(self):
683
+ pass
684
+
685
+ _cache = {}
686
+
687
+ VISION_TASK_OPTIONS = [
688
+ "Caption (plain description)",
689
+ "Caption + Format (apply model_format below)",
690
+ "SD/Booru Tags",
691
+ "Pose & Anatomy Focus",
692
+ "Custom Instruction",
693
+ ]
694
+
695
+ VISION_TASK_INSTRUCTIONS = {
696
+ "Caption (plain description)": (
697
+ "Describe this image in one detailed paragraph covering subject, composition, "
698
+ "lighting, colors, mood, and notable details. Output only the description."
699
+ ),
700
+ "SD/Booru Tags": (
701
+ "Generate a comma-separated list of descriptive tags for this image. "
702
+ "Include subject, action, setting, lighting, mood, and style tags. "
703
+ "Output only the tags, comma-separated."
704
+ ),
705
+ "Pose & Anatomy Focus": (
706
+ "Describe the subject's pose, body position, expression, framing, and what's "
707
+ "visible in detail. Be precise about positioning. Output only the description."
708
+ ),
709
+ }
710
+
711
+ @classmethod
712
+ def INPUT_TYPES(s):
713
+ handler_options = ["Auto-detect"] + list(_HANDLER_CLASSES.keys())
714
+ if not _HANDLER_CLASSES:
715
+ handler_options = ["NO_VISION_HANDLERS_AVAILABLE"]
716
+
717
+ return {
718
+ "required": {
719
+ "image": ("IMAGE",),
720
+ "model_file": (_list_ggufs(),),
721
+ "mmproj_file": (_list_mmproj(),),
722
+ "chat_handler": (handler_options,),
723
+ "vision_task": (s.VISION_TASK_OPTIONS,),
724
+ "model_format": (MODEL_FORMAT_OPTIONS,),
725
+ "aesthetic": (AESTHETIC_OPTIONS,),
726
+ "custom_instruction": ("STRING", {
727
+ "multiline": True,
728
+ "default": "",
729
+ "placeholder": "Used when vision_task is 'Custom Instruction'.",
730
+ }),
731
+ "n_gpu_layers": ("INT", {"default": -1, "min": -1, "max": 999, "step": 1}),
732
+ "n_ctx": ("INT", {"default": 4096, "min": 512, "max": 32768, "step": 512}),
733
+ "max_tokens": ("INT", {"default": 400, "min": 50, "max": 2048, "step": 50}),
734
+ "temperature": ("FLOAT", {"default": 0.4, "min": 0.0, "max": 2.0, "step": 0.05}),
735
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
736
+ "lock_in": ("BOOLEAN", {
737
+ "default": False,
738
+ "label_on": "πŸ”’ LOCKED (cached)",
739
+ "label_off": "πŸ”„ LIVE (analyzing)",
740
+ }),
741
+ },
742
+ }
743
+
744
+ RETURN_TYPES = ("STRING", "STRING")
745
+ RETURN_NAMES = ("image_prompt", "thought_process")
746
+ FUNCTION = "describe"
747
+ CATEGORY = "Rebel AI"
748
+
749
+ @classmethod
750
+ def IS_CHANGED(cls, image, model_file, mmproj_file, chat_handler, vision_task,
751
+ model_format, aesthetic, custom_instruction, n_gpu_layers,
752
+ n_ctx, max_tokens, temperature, seed, lock_in):
753
+ if lock_in:
754
+ return (f"LOCKED|{model_file}|{mmproj_file}|{chat_handler}|{vision_task}|"
755
+ f"{model_format}|{aesthetic}|{custom_instruction}")
756
+ try:
757
+ img_hash = hash(image.detach().cpu().numpy().tobytes())
758
+ except Exception:
759
+ img_hash = "noimg"
760
+ return f"LIVE|{img_hash}|{seed}|{float('nan')}"
761
+
762
+ def _resolve_handler(self, chat_handler_choice, model_file):
763
+ if chat_handler_choice == "Auto-detect":
764
+ mf = model_file.lower()
765
+ if "moondream" in mf and "Moondream" in _HANDLER_CLASSES:
766
+ return _HANDLER_CLASSES["Moondream"], "Moondream"
767
+ if "qwen" in mf and "Qwen2.5-VL" in _HANDLER_CLASSES:
768
+ return _HANDLER_CLASSES["Qwen2.5-VL"], "Qwen2.5-VL"
769
+ if "minicpm" in mf and "MiniCPM-V 2.6" in _HANDLER_CLASSES:
770
+ return _HANDLER_CLASSES["MiniCPM-V 2.6"], "MiniCPM-V 2.6"
771
+ if "nano" in mf and "llava" in mf and "NanoLLaVA" in _HANDLER_CLASSES:
772
+ return _HANDLER_CLASSES["NanoLLaVA"], "NanoLLaVA"
773
+ if "llava" in mf:
774
+ if "LLaVA 1.6" in _HANDLER_CLASSES:
775
+ return _HANDLER_CLASSES["LLaVA 1.6"], "LLaVA 1.6"
776
+ if "LLaVA 1.5" in _HANDLER_CLASSES:
777
+ return _HANDLER_CLASSES["LLaVA 1.5"], "LLaVA 1.5"
778
+ if "LLaVA 1.5" in _HANDLER_CLASSES:
779
+ return _HANDLER_CLASSES["LLaVA 1.5"], "LLaVA 1.5 (fallback)"
780
+ raise RuntimeError("No vision chat handler available.")
781
+ if chat_handler_choice not in _HANDLER_CLASSES:
782
+ raise RuntimeError(
783
+ f"Chat handler '{chat_handler_choice}' isn't available. "
784
+ f"Available: {list(_HANDLER_CLASSES.keys())}"
785
+ )
786
+ return _HANDLER_CLASSES[chat_handler_choice], chat_handler_choice
787
+
788
+ def _tensor_to_data_uri(self, image_tensor):
789
+ if image_tensor.dim() == 4:
790
+ img = image_tensor[0]
791
+ else:
792
+ img = image_tensor
793
+ img_np = (img.detach().cpu().numpy() * 255.0).clip(0, 255).astype(np.uint8)
794
+ pil = Image.fromarray(img_np)
795
+ buf = BytesIO()
796
+ pil.save(buf, format="PNG")
797
+ b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
798
+ return f"data:image/png;base64,{b64}"
799
+
800
+ def _build_instruction(self, vision_task, model_format, aesthetic, custom_instruction):
801
+ if vision_task == "Custom Instruction":
802
+ return custom_instruction.strip() or "Describe this image."
803
+
804
+ if vision_task == "Caption + Format (apply model_format below)":
805
+ base = (
806
+ "Describe this image faithfully, then format the description according "
807
+ "to the rules below."
808
+ )
809
+ base += " " + MODEL_FORMAT_INSTRUCTIONS[model_format]
810
+ if aesthetic in AESTHETIC_DESCRIPTORS:
811
+ base += " " + AESTHETIC_DESCRIPTORS[aesthetic]
812
+ base += " Output only the formatted prompt."
813
+ return base
814
+
815
+ base = self.VISION_TASK_INSTRUCTIONS[vision_task]
816
+ if aesthetic in AESTHETIC_DESCRIPTORS:
817
+ base += " " + AESTHETIC_DESCRIPTORS[aesthetic]
818
+ return base
819
+
820
+ def describe(self, image, model_file, mmproj_file, chat_handler, vision_task,
821
+ model_format, aesthetic, custom_instruction, n_gpu_layers,
822
+ n_ctx, max_tokens, temperature, seed, lock_in):
823
+ cache_key = (model_file, mmproj_file, chat_handler, vision_task,
824
+ model_format, aesthetic, custom_instruction, temperature)
825
+
826
+ if lock_in and cache_key in self._cache:
827
+ c = self._cache[cache_key]
828
+ thought = (
829
+ f"=== πŸ”’ LOCKED β€” Returning Cached Caption ===\n"
830
+ f"No model load. New image ignored.\n\n"
831
+ f"=== Original Run Info ===\n{c['meta']}\n"
832
+ f"=== Cached Instruction ===\n{c['instruction']}\n\n"
833
+ f"=== Cached Raw Output ===\n{c['raw_output']}\n\n"
834
+ f"=== Cached Image Prompt ===\n{c['final_prompt']}"
835
+ )
836
+ return (c['final_prompt'], thought)
837
+
838
+ if model_file == "NO_GGUF_FILES_IN_FOLDER":
839
+ raise FileNotFoundError("Drop a vision-capable .gguf in the node folder and restart ComfyUI.")
840
+ if mmproj_file == "NO_MMPROJ_FILE_FOUND":
841
+ raise FileNotFoundError("No mmproj file found. Vision models need a paired mmproj-*.gguf.")
842
+ if not _HANDLER_CLASSES:
843
+ raise RuntimeError("No vision chat handlers available. Update llama-cpp-python.")
844
+
845
+ node_dir = os.path.dirname(os.path.abspath(__file__))
846
+ model_path = os.path.join(node_dir, model_file)
847
+ mmproj_path = os.path.join(node_dir, mmproj_file)
848
+ if not os.path.isfile(model_path):
849
+ raise FileNotFoundError(f"Model not found: {model_path}")
850
+ if not os.path.isfile(mmproj_path):
851
+ raise FileNotFoundError(f"mmproj not found: {mmproj_path}")
852
+
853
+ handler_cls, handler_label = self._resolve_handler(chat_handler, model_file)
854
+ instruction = self._build_instruction(vision_task, model_format, aesthetic, custom_instruction)
855
+ img_uri = self._tensor_to_data_uri(image)
856
+
857
+ chat_handler_instance = handler_cls(clip_model_path=mmproj_path, verbose=False)
858
+ llm = Llama(
859
+ model_path=model_path,
860
+ chat_handler=chat_handler_instance,
861
+ n_gpu_layers=n_gpu_layers,
862
+ verbose=False,
863
+ n_ctx=n_ctx,
864
+ seed=seed,
865
+ logits_all=True,
866
+ )
867
+ try:
868
+ output = llm.create_chat_completion(
869
+ messages=[
870
+ {
871
+ "role": "user",
872
+ "content": [
873
+ {"type": "image_url", "image_url": {"url": img_uri}},
874
+ {"type": "text", "text": instruction},
875
+ ],
876
+ },
877
+ ],
878
+ max_tokens=max_tokens,
879
+ temperature=temperature,
880
+ top_p=0.9,
881
+ repeat_penalty=1.1,
882
+ )
883
+ raw_output = output["choices"][0]["message"]["content"].strip()
884
+ finally:
885
+ _free_llm(llm)
886
+ try:
887
+ del chat_handler_instance
888
+ except Exception:
889
+ pass
890
+ gc.collect()
891
+ if torch.cuda.is_available():
892
+ torch.cuda.empty_cache()
893
+
894
+ final_prompt = _clean_output(raw_output)
895
+ if not final_prompt.strip() or len(final_prompt) < 20:
896
+ final_prompt = raw_output
897
+
898
+ gpu_label = "all" if n_gpu_layers < 0 else ("CPU only" if n_gpu_layers == 0 else f"{n_gpu_layers} layers")
899
+ meta_block = (
900
+ f"Model file: {model_file}\n"
901
+ f"mmproj file: {mmproj_file}\n"
902
+ f"Chat handler: {handler_label}\n"
903
+ f"Vision task: {vision_task}\n"
904
+ f"Model Format: {model_format}\n"
905
+ f"Aesthetic: {aesthetic}\n"
906
+ f"GPU layers: {gpu_label}\n"
907
+ f"Context: {n_ctx}\n"
908
+ f"Max tokens: {max_tokens}\n"
909
+ f"Temperature: {temperature}\n"
910
+ f"Seed: {seed}\n"
911
+ f"Raw chars: {len(raw_output)}\n"
912
+ f"Clean chars: {len(final_prompt)}\n"
913
+ )
914
+ thought = (
915
+ f"=== πŸ”„ LIVE β€” Image Analysis ===\n\n"
916
+ f"=== Run Settings ===\n{meta_block}\n"
917
+ f"=== Instruction Sent ===\n{instruction}\n\n"
918
+ f"=== Raw Model Output ===\n{raw_output}\n\n"
919
+ f"=== Image Prompt (downstream) ===\n{final_prompt}"
920
+ )
921
+ self._cache[cache_key] = {
922
+ "final_prompt": final_prompt, "raw_output": raw_output,
923
+ "meta": meta_block, "instruction": instruction,
924
+ }
925
+ return (final_prompt, thought)
926
+
927
+
928
+ # =========================================================================
929
+ # Node 4: Locker
930
+ # =========================================================================
931
+
932
+ class RebelsPromptLocker:
933
+ def __init__(self):
934
+ pass
935
+
936
+ @classmethod
937
+ def INPUT_TYPES(s):
938
+ return {
939
+ "required": {
940
+ "text_input": ("STRING", {"forceInput": True}),
941
+ "lock_in_prompt": ("BOOLEAN", {
942
+ "default": False,
943
+ "label_on": "LOCKED IN",
944
+ "label_off": "PAUSED",
945
+ }),
946
+ },
947
+ }
948
+
949
+ RETURN_TYPES = ("STRING",)
950
+ RETURN_NAMES = ("text_output",)
951
+ FUNCTION = "execute"
952
+ CATEGORY = "Rebel AI"
953
+ OUTPUT_NODE = True
954
+
955
+ def execute(self, text_input, lock_in_prompt):
956
+ if not lock_in_prompt:
957
+ raise ValueError(
958
+ "πŸ›‘ WORKFLOW PAUSED: Toggle 'lock_in_prompt' to LOCKED IN to pass the text through."
959
+ )
960
+ return {"ui": {"text": [text_input]}, "result": (text_input,)}
961
+
962
+
963
+ NODE_CLASS_MAPPINGS = {
964
+ "RebelsPromptEnhancer": RebelsPromptEnhancer,
965
+ "RebelsPromptEnhancerCustom": RebelsPromptEnhancerCustom,
966
+ "RebelsImageToPrompt": RebelsImageToPrompt,
967
+ "RebelsPromptLocker": RebelsPromptLocker,
968
+ }
969
+
970
+ NODE_DISPLAY_NAME_MAPPINGS = {
971
+ "RebelsPromptEnhancer": "πŸš€ Rebels Prompt Enhancer",
972
+ "RebelsPromptEnhancerCustom": "πŸ§ͺ Rebels Prompt Enhancer (Custom GGUF)",
973
+ "RebelsImageToPrompt": "πŸ‘οΈ Rebels Image to Prompt",
974
+ "RebelsPromptLocker": "πŸ”’ Rebels Prompt Locker",
975
+ }
976
+
977
+ WEB_DIRECTORY = "./web"
978
+
979
+ __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
RebelsPromptEnhancer/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ llama-cpp-python
2
+ torch
RebelsPromptEnhancer/web/js/rebels_locker_display.js ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { app } from "../../../scripts/app.js";
2
+ import { ComfyWidgets } from "../../../scripts/widgets.js";
3
+
4
+ app.registerExtension({
5
+ name: "RebelAI.PromptLocker.Display",
6
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
7
+ if (nodeData.name !== "RebelsPromptLocker") return;
8
+
9
+ const onExecuted = nodeType.prototype.onExecuted;
10
+ nodeType.prototype.onExecuted = function (message) {
11
+ onExecuted?.apply(this, arguments);
12
+
13
+ // Remove any existing display widget so we don't stack them on re-runs.
14
+ if (this.widgets) {
15
+ const idx = this.widgets.findIndex(w => w.name === "locked_text_display");
16
+ if (idx !== -1) {
17
+ this.widgets[idx].onRemove?.();
18
+ this.widgets.splice(idx, 1);
19
+ }
20
+ }
21
+
22
+ const text = (message?.text || []).join("");
23
+
24
+ const widget = ComfyWidgets["STRING"](
25
+ this,
26
+ "locked_text_display",
27
+ ["STRING", { multiline: true }],
28
+ app
29
+ ).widget;
30
+
31
+ // Read-only, but otherwise use ComfyUI's native widget styling so it
32
+ // matches the size/weight/font of a normal text widget.
33
+ widget.inputEl.readOnly = true;
34
+ widget.inputEl.style.fontSize = "13px";
35
+ widget.inputEl.style.lineHeight = "1.4";
36
+ widget.value = text;
37
+
38
+ requestAnimationFrame(() => {
39
+ const sz = this.computeSize();
40
+ if (sz[1] < this.size[1]) sz[1] = this.size[1];
41
+ this.onResize?.(sz);
42
+ app.graph.setDirtyCanvas(true, false);
43
+ });
44
+ };
45
+ },
46
+ });