iamgroot1212 commited on
Commit
54d5574
·
verified ·
1 Parent(s): a68b48e

Upload JC.py

Browse files
Files changed (1) hide show
  1. scripts/JC.py +451 -0
scripts/JC.py ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import AutoProcessor, LlavaForConditionalGeneration, BitsAndBytesConfig
3
+ import folder_paths
4
+ from pathlib import Path
5
+ from PIL import Image
6
+ from torchvision.transforms import ToPILImage
7
+ import json
8
+ import gc
9
+ import os
10
+
11
+ class ModelLoadError(Exception):
12
+ pass
13
+
14
+ def handle_model_error(e, cleanup_func=None):
15
+ if cleanup_func:
16
+ cleanup_func()
17
+ if torch.cuda.is_available():
18
+ torch.cuda.empty_cache()
19
+ gc.collect()
20
+ raise ModelLoadError(f"Error loading model: {str(e)}")
21
+
22
+ def cleanup_model_resources(model=None, processor=None):
23
+ if model is not None:
24
+ del model
25
+ if processor is not None:
26
+ del processor
27
+ if torch.cuda.is_available():
28
+ torch.cuda.empty_cache()
29
+ gc.collect()
30
+
31
+ def validate_model_parameters(quantization, valid_modes):
32
+ if quantization not in valid_modes:
33
+ raise ValueError(f"Invalid quantization mode: {quantization}. Valid modes: {', '.join(valid_modes)}")
34
+
35
+ with open(Path(__file__).parent / "jc_data.json", "r", encoding="utf-8") as f:
36
+ config = json.load(f)
37
+ CAPTION_TYPE_MAP = config["caption_type_map"]
38
+ EXTRA_OPTIONS = config["extra_options"]
39
+ MEMORY_EFFICIENT_CONFIGS = config["memory_efficient_configs"]
40
+ MODEL_SETTINGS = config["model_settings"]
41
+ CAPTION_LENGTH_CHOICES = config["caption_length_choices"]
42
+ HF_MODELS = config["hf_models"]
43
+
44
+ # --- Custom Models Merge Logic (for HF models only) ---
45
+ custom_path = Path(__file__).parent / "custom_models.json"
46
+
47
+ if custom_path.exists():
48
+ try:
49
+ with open(custom_path, "r", encoding="utf-8") as f:
50
+ custom_data = json.load(f) or {}
51
+ HF_MODELS.update(custom_data.get("hf_models", {}))
52
+ print("[JoyCaption] ✅ Loaded custom HF custom models.")
53
+ except Exception as e:
54
+ print(f"[JoyCaption] ⚠️ Failed to load custom models → {e}")
55
+ else:
56
+ print("[JoyCaption] ℹ️ No custom models found, skipping user-defined HF models.")
57
+ # ------------------------------------------------------
58
+
59
+ def build_prompt(caption_type: str, caption_length: str | int, extra_options: list[str], name_input: str) -> str:
60
+ """Constructs the prompt for the model based on user selections."""
61
+ if caption_length == "any":
62
+ map_idx = 0
63
+ elif isinstance(caption_length, str) and caption_length.isdigit():
64
+ map_idx = 1
65
+ else:
66
+ map_idx = 2
67
+
68
+ prompt = CAPTION_TYPE_MAP[caption_type][map_idx]
69
+
70
+ if extra_options:
71
+ prompt += " " + " ".join(extra_options)
72
+
73
+ return prompt.format(
74
+ name=name_input or "{NAME}",
75
+ length=caption_length,
76
+ word_count=caption_length,
77
+ )
78
+
79
+ _MODEL_CACHE = {}
80
+
81
+ class JC_Models:
82
+ """Handles loading, caching, and running the LLaVA models."""
83
+ def __init__(self, model: str, memory_mode: str):
84
+ cache_key = f"{model}_{memory_mode}"
85
+
86
+ if cache_key in _MODEL_CACHE:
87
+ try:
88
+ self.processor = _MODEL_CACHE[cache_key]["processor"]
89
+ self.model = _MODEL_CACHE[cache_key]["model"]
90
+ self.device = _MODEL_CACHE[cache_key]["device"]
91
+ if not next(self.model.parameters()).is_cuda:
92
+ raise RuntimeError("Cached model not on GPU")
93
+ print(f"Using cached model: {cache_key}")
94
+ return
95
+ except Exception as e:
96
+ print(f"Cache validation failed: {e}, reloading model...")
97
+ if cache_key in _MODEL_CACHE:
98
+ del _MODEL_CACHE[cache_key]
99
+ torch.cuda.empty_cache()
100
+
101
+ checkpoint_path = Path(folder_paths.models_dir) / "LLM" / Path(model).stem
102
+ if not checkpoint_path.exists():
103
+ from huggingface_hub import snapshot_download
104
+ snapshot_download(repo_id=model, local_dir=str(checkpoint_path), force_download=False, local_files_only=False)
105
+
106
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
107
+
108
+ if self.device == "cuda":
109
+ torch.backends.cudnn.benchmark = True
110
+ if hasattr(torch.backends, 'cuda'):
111
+ if hasattr(torch.backends.cuda, 'matmul'):
112
+ torch.backends.cuda.matmul.allow_tf32 = True
113
+ if hasattr(torch.backends.cuda, 'allow_tf32'):
114
+ torch.backends.cuda.allow_tf32 = True
115
+
116
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:128"
117
+
118
+ self.processor = AutoProcessor.from_pretrained(
119
+ str(checkpoint_path),
120
+ use_fast=True,
121
+ image_processor_type="CLIPImageProcessor",
122
+ image_size=336
123
+ )
124
+
125
+ # Robustly handle SizeDict, dict, or tuple for PIL compatibility
126
+ if hasattr(self.processor, 'image_processor') and hasattr(self.processor.image_processor, 'size'):
127
+ size_raw = self.processor.image_processor.size
128
+
129
+ # Check if it has dictionary-like keys first (handles SizeDict and dict)
130
+ if hasattr(size_raw, 'get') or isinstance(size_raw, dict):
131
+ h = size_raw.get('height', size_raw.get('shortest_edge', 336))
132
+ w = size_raw.get('width', size_raw.get('shortest_edge', 336))
133
+ self.target_size = (int(w), int(h))
134
+ elif isinstance(size_raw, (list, tuple)):
135
+ self.target_size = (int(size_raw[0]), int(size_raw[1])) if len(size_raw) >= 2 else (int(size_raw[0]), int(size_raw[0]))
136
+ else:
137
+ self.target_size = (int(size_raw), int(size_raw))
138
+ else:
139
+ self.target_size = (336, 336)
140
+
141
+ # Final safety: Ensure it's a tuple of plain ints
142
+ self.target_size = tuple(map(int, self.target_size))
143
+
144
+ model_kwargs = {
145
+ "device_map": "cuda" if self.device == "cuda" else "cpu",
146
+ }
147
+
148
+ try:
149
+ if "FP8-Dynamic" in model:
150
+ print("Loading FP8 model with automatic configuration...")
151
+ self.model = LlavaForConditionalGeneration.from_pretrained(
152
+ str(checkpoint_path),
153
+ torch_dtype="auto",
154
+ **model_kwargs
155
+ )
156
+ elif memory_mode == "Full Precision (bf16)":
157
+ self.model = LlavaForConditionalGeneration.from_pretrained(
158
+ str(checkpoint_path),
159
+ torch_dtype=torch.bfloat16,
160
+ **model_kwargs
161
+ )
162
+ elif memory_mode == "Balanced (8-bit)":
163
+ qnt_config = BitsAndBytesConfig(
164
+ load_in_8bit=True,
165
+ bnb_8bit_compute_dtype=torch.float16,
166
+ bnb_8bit_use_double_quant=True,
167
+ llm_int8_skip_modules=["vision_tower", "multi_modal_projector"],
168
+ llm_int8_enable_fp32_cpu_offload=True
169
+ )
170
+ self.model = LlavaForConditionalGeneration.from_pretrained(
171
+ str(checkpoint_path),
172
+ torch_dtype=torch.float16,
173
+ quantization_config=qnt_config,
174
+ **model_kwargs
175
+ )
176
+ else:
177
+ qnt_config = BitsAndBytesConfig(
178
+ load_in_4bit=True,
179
+ bnb_4bit_compute_dtype=torch.float16,
180
+ bnb_4bit_quant_type="nf4",
181
+ bnb_4bit_use_double_quant=True,
182
+ llm_int8_skip_modules=["vision_tower", "multi_modal_projector"],
183
+ llm_int8_enable_fp32_cpu_offload=True
184
+ )
185
+ self.model = LlavaForConditionalGeneration.from_pretrained(
186
+ str(checkpoint_path),
187
+ torch_dtype="auto",
188
+ quantization_config=qnt_config,
189
+ **model_kwargs
190
+ )
191
+
192
+ self.model.eval()
193
+
194
+ if self.device == "cuda" and not next(self.model.parameters()).is_cuda:
195
+ raise RuntimeError("Model failed to load on GPU")
196
+
197
+ if memory_mode == "Global Cache":
198
+ _MODEL_CACHE[cache_key] = {
199
+ "processor": self.processor,
200
+ "model": self.model,
201
+ "device": self.device
202
+ }
203
+
204
+ except Exception as e:
205
+ cleanup_model_resources(self.model, self.processor)
206
+ handle_model_error(e)
207
+
208
+ @torch.inference_mode()
209
+ def generate(self, image: Image.Image, system: str, prompt: str, max_new_tokens: int, temperature: float, top_p: float, top_k: int) -> str:
210
+ """Generates a caption for the given image."""
211
+ convo = [
212
+ {"role": "system", "content": system.strip()},
213
+ {"role": "user", "content": prompt.strip()},
214
+ ]
215
+
216
+ convo_string = self.processor.apply_chat_template(convo, tokenize=False, add_generation_prompt=True)
217
+ assert isinstance(convo_string, str)
218
+
219
+ if image.mode != 'RGB':
220
+ image = image.convert('RGB')
221
+
222
+ image = image.resize(self.target_size, Image.Resampling.LANCZOS)
223
+
224
+ inputs = self.processor(text=[convo_string], images=[image], return_tensors="pt").to(self.device)
225
+
226
+ if hasattr(inputs, 'pixel_values') and inputs['pixel_values'] is not None:
227
+ inputs['pixel_values'] = inputs['pixel_values'].to(self.model.dtype)
228
+
229
+ with torch.cuda.amp.autocast(enabled=True):
230
+ generate_ids = self.model.generate(
231
+ **inputs,
232
+ max_new_tokens=max_new_tokens,
233
+ do_sample=True if temperature > 0 else False,
234
+ suppress_tokens=None,
235
+ use_cache=True,
236
+ temperature=temperature,
237
+ top_k=None if top_k == 0 else top_k,
238
+ top_p=top_p,
239
+ )[0]
240
+
241
+ generate_ids = generate_ids[inputs['input_ids'].shape[1]:]
242
+ caption = self.processor.tokenizer.decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)
243
+ return caption.strip()
244
+
245
+ class JC_ExtraOptions:
246
+ """A node to collect extra options for captioning."""
247
+ @classmethod
248
+ def INPUT_TYPES(cls):
249
+ inputs = {"required": {}}
250
+ for key, value in EXTRA_OPTIONS.items():
251
+ inputs["required"][key] = ("BOOLEAN", {"default": value["default"]})
252
+ inputs["required"]["character_name"] = ("STRING", {"default": "", "multiline": True, "placeholder": "Character Name"})
253
+ return inputs
254
+
255
+ RETURN_TYPES = ("JOYCAPTION_EXTRA_OPTIONS",)
256
+ RETURN_NAMES = ("extra_options",)
257
+ FUNCTION = "get_extra_options"
258
+ CATEGORY = "🧪AILab/📝JoyCaption"
259
+
260
+ def get_extra_options(self, character_name, **kwargs):
261
+ ret_list = []
262
+ for key, value in EXTRA_OPTIONS.items():
263
+ if kwargs.get(key, False):
264
+ ret_list.append(value["description"])
265
+ return ([ret_list, character_name],)
266
+
267
+ class JC:
268
+ """The main, simple JoyCaption node."""
269
+ @classmethod
270
+ def INPUT_TYPES(cls):
271
+ model_list = list(HF_MODELS.keys())
272
+ return {
273
+ "required": {
274
+ "image": ("IMAGE",),
275
+ "model": (model_list, {"default": model_list[1], "tooltip": "Select the AI model to use for caption generation"}),
276
+ "quantization": (list(MEMORY_EFFICIENT_CONFIGS.keys()), {"default": "Balanced (8-bit)", "tooltip": "Choose between speed and quality. 8-bit is recommended for most users"}),
277
+ "prompt_style": (list(CAPTION_TYPE_MAP.keys()), {"default": "Descriptive", "tooltip": "Select the style of caption you want to generate"}),
278
+ "caption_length": (CAPTION_LENGTH_CHOICES, {"default": "any", "tooltip": "Control the length of the generated caption"}),
279
+ "memory_management": (["Keep in Memory", "Clear After Run", "Global Cache"], {"default": "Keep in Memory", "tooltip": "Choose how to manage model memory. 'Keep in Memory' for faster processing, 'Clear After Run' for limited VRAM, 'Global Cache' for fastest processing if you have enough VRAM"}),
280
+ },
281
+ "optional": {
282
+ "extra_options": ("JOYCAPTION_EXTRA_OPTIONS", {"tooltip": "Additional options to customize the caption generation"}),
283
+ }
284
+ }
285
+
286
+ RETURN_TYPES = ("STRING",)
287
+ RETURN_NAMES = ("STRING",)
288
+ FUNCTION = "generate"
289
+ CATEGORY = "🧪AILab/📝JoyCaption"
290
+
291
+ def __init__(self):
292
+ self.predictor = None
293
+ self.current_memory_mode = None
294
+ self.current_model = None
295
+
296
+ def generate(self, image, model, quantization, prompt_style, caption_length, memory_management, extra_options=None):
297
+ try:
298
+ validate_model_parameters(quantization, list(MEMORY_EFFICIENT_CONFIGS.keys()))
299
+
300
+ if memory_management == "Global Cache":
301
+ try:
302
+ model_name = HF_MODELS[model]["name"]
303
+ self.predictor = JC_Models(model_name, quantization)
304
+ except Exception as e:
305
+ return (f"Error loading model: {e}",)
306
+ elif self.predictor is None or self.current_memory_mode != quantization or self.current_model != model:
307
+ if self.predictor is not None:
308
+ del self.predictor
309
+ self.predictor = None
310
+ torch.cuda.empty_cache()
311
+ gc.collect()
312
+ try:
313
+ model_name = HF_MODELS[model]["name"]
314
+ self.predictor = JC_Models(model_name, quantization)
315
+ self.current_memory_mode = quantization
316
+ self.current_model = model
317
+ except Exception as e:
318
+ return (f"Error loading model: {e}",)
319
+
320
+ prompt = build_prompt(prompt_style, caption_length, extra_options[0] if extra_options else [], extra_options[1] if extra_options else "{NAME}")
321
+ system_prompt = MODEL_SETTINGS["default_system_prompt"]
322
+ pil_image = ToPILImage()(image[0].permute(2, 0, 1))
323
+
324
+ response = self.predictor.generate(
325
+ image=pil_image,
326
+ system=system_prompt,
327
+ prompt=prompt,
328
+ max_new_tokens=MODEL_SETTINGS["default_max_tokens"],
329
+ temperature=MODEL_SETTINGS["default_temperature"],
330
+ top_p=MODEL_SETTINGS["default_top_p"],
331
+ top_k=MODEL_SETTINGS["default_top_k"],
332
+ )
333
+
334
+ if memory_management == "Clear After Run":
335
+ del self.predictor
336
+ self.predictor = None
337
+ torch.cuda.empty_cache()
338
+ gc.collect()
339
+
340
+ return (response,)
341
+ except Exception as e:
342
+ if memory_management == "Clear After Run":
343
+ del self.predictor
344
+ self.predictor = None
345
+ torch.cuda.empty_cache()
346
+ gc.collect()
347
+ raise e
348
+
349
+ class JC_adv:
350
+ """The advanced JoyCaption node with more settings."""
351
+ @classmethod
352
+ def INPUT_TYPES(cls):
353
+ model_list = list(HF_MODELS.keys())
354
+ return {
355
+ "required": {
356
+ "image": ("IMAGE",),
357
+ "model": (model_list, {"default": model_list[1], "tooltip": "Select the AI model to use for caption generation"}),
358
+ "quantization": (list(MEMORY_EFFICIENT_CONFIGS.keys()), {"default": "Balanced (8-bit)", "tooltip": "Choose between speed and quality. 8-bit is recommended for most users"}),
359
+ "prompt_style": (list(CAPTION_TYPE_MAP.keys()), {"default": "Descriptive", "tooltip": "Select the style of caption you want to generate"}),
360
+ "caption_length": (CAPTION_LENGTH_CHOICES, {"default": "any", "tooltip": "Control the length of the generated caption"}),
361
+ "max_new_tokens": ("INT", {"default": MODEL_SETTINGS["default_max_tokens"], "min": 1, "max": 2048, "tooltip": "Maximum number of tokens to generate. Higher values allow longer captions"}),
362
+ "temperature": ("FLOAT", {"default": MODEL_SETTINGS["default_temperature"], "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "Control the randomness of the output. Higher values make the output more creative but less predictable"}),
363
+ "top_p": ("FLOAT", {"default": MODEL_SETTINGS["default_top_p"], "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Control the diversity of the output. Higher values allow more diverse word choices"}),
364
+ "top_k": ("INT", {"default": MODEL_SETTINGS["default_top_k"], "min": 0, "max": 100, "tooltip": "Limit the number of possible next tokens. Lower values make the output more focused"}),
365
+ "custom_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Custom prompt template. If empty, will use the selected prompt style"}),
366
+ "memory_management": (["Keep in Memory", "Clear After Run", "Global Cache"], {"default": "Keep in Memory", "tooltip": "Choose how to manage model memory. 'Keep in Memory' for faster processing, 'Clear After Run' for limited VRAM, 'Global Cache' for fastest processing if you have enough VRAM"}),
367
+ },
368
+ "optional": {
369
+ "extra_options": ("JOYCAPTION_EXTRA_OPTIONS", {"tooltip": "Additional options to customize the caption generation"}),
370
+ }
371
+ }
372
+
373
+ RETURN_TYPES = ("STRING", "STRING")
374
+ RETURN_NAMES = ("PROMPT", "STRING")
375
+ FUNCTION = "generate"
376
+ CATEGORY = "🧪AILab/📝JoyCaption"
377
+
378
+ def __init__(self):
379
+ self.predictor = None
380
+ self.current_memory_mode = None
381
+ self.current_model = None
382
+
383
+ def generate(self, image, model, quantization, prompt_style, caption_length, max_new_tokens, temperature, top_p, top_k, custom_prompt, memory_management, extra_options=None):
384
+ try:
385
+ validate_model_parameters(quantization, list(MEMORY_EFFICIENT_CONFIGS.keys()))
386
+
387
+ if memory_management == "Global Cache":
388
+ try:
389
+ model_name = HF_MODELS[model]["name"]
390
+ self.predictor = JC_Models(model_name, quantization)
391
+ except Exception as e:
392
+ return (f"Error loading model: {e}", "")
393
+ elif self.predictor is None or self.current_memory_mode != quantization or self.current_model != model:
394
+ if self.predictor is not None:
395
+ del self.predictor
396
+ self.predictor = None
397
+ torch.cuda.empty_cache()
398
+ gc.collect()
399
+ try:
400
+ model_name = HF_MODELS[model]["name"]
401
+ self.predictor = JC_Models(model_name, quantization)
402
+ self.current_memory_mode = quantization
403
+ self.current_model = model
404
+ except Exception as e:
405
+ return (f"Error loading model: {e}", "")
406
+
407
+ if custom_prompt and custom_prompt.strip():
408
+ prompt = custom_prompt.strip()
409
+ else:
410
+ prompt = build_prompt(prompt_style, caption_length, extra_options[0] if extra_options else [], extra_options[1] if extra_options else "{NAME}")
411
+
412
+ system_prompt = MODEL_SETTINGS["default_system_prompt"]
413
+ pil_image = ToPILImage()(image[0].permute(2, 0, 1))
414
+
415
+ response = self.predictor.generate(
416
+ image=pil_image,
417
+ system=system_prompt,
418
+ prompt=prompt,
419
+ max_new_tokens=max_new_tokens,
420
+ temperature=temperature,
421
+ top_p=top_p,
422
+ top_k=top_k,
423
+ )
424
+
425
+ if memory_management == "Clear After Run":
426
+ del self.predictor
427
+ self.predictor = None
428
+ torch.cuda.empty_cache()
429
+ gc.collect()
430
+
431
+ return (prompt, response)
432
+ except Exception as e:
433
+ if memory_management == "Clear After Run":
434
+ del self.predictor
435
+ self.predictor = None
436
+ torch.cuda.empty_cache()
437
+ gc.collect()
438
+ raise e
439
+
440
+ NODE_CLASS_MAPPINGS = {
441
+ "JC": JC,
442
+ "JC_adv": JC_adv,
443
+ "JC_ExtraOptions": JC_ExtraOptions,
444
+ }
445
+
446
+ NODE_DISPLAY_NAME_MAPPINGS = {
447
+ "JC": "JoyCaption",
448
+ "JC_adv": "JoyCaption (Advanced)",
449
+ "JC_ExtraOptions": "JoyCaption Extra Options",
450
+ }
451
+