import os import io import gc import json import base64 import random import torch import numpy as np from PIL import Image, ImageDraw from scipy.ndimage import gaussian_filter from .support.cqdm import cqdm from .support.gguf_layers import get_layer_count from .support.prompt_enhancer_preset import * import folder_paths import comfy.model_management as mm import comfy.utils from llama_cpp import Llama from llama_cpp.llama_chat_format import ( Llava15ChatHandler, Llava16ChatHandler, MoondreamChatHandler, NanoLlavaChatHandler, Llama3VisionAlphaChatHandler, MiniCPMv26ChatHandler ) try: from llama_cpp.llama_chat_format import MTMDChatHandler chat_handlers += ["DeepSeek-OCR"] _MTMD = True except: _MTMD = False chat_handlers = ["None", "LLaVA-1.5", "LLaVA-1.6", "Moondream2", "nanoLLaVA", "llama3-Vision-Alpha", "MiniCPM-v2.6"] try: from llama_cpp.llama_chat_format import Gemma3ChatHandler chat_handlers += ["Gemma3"] except: Gemma3ChatHandler = None try: from llama_cpp.llama_chat_format import Gemma4ChatHandler chat_handlers += ["Gemma4"] except: Gemma3ChatHandler = None try: from llama_cpp.llama_chat_format import Qwen25VLChatHandler chat_handlers += ["Qwen2.5-VL", "MinerU2.5-Pro"] except: Qwen25VLChatHandler = None try: from llama_cpp.llama_chat_format import Qwen3VLChatHandler chat_handlers += ["Qwen3-VL", "Qwen3-VL-Thinking"] except: Qwen3VLChatHandler = None try: from llama_cpp.llama_chat_format import Qwen35ChatHandler chat_handlers += ["Qwen3.5", "Qwen3.5-Thinking", "Qwen3.6", "Qwen3.6-Thinking"] except: Qwen35ChatHandler = None try: from llama_cpp.llama_chat_format import (GLM46VChatHandler, LFM2VLChatHandler, GLM41VChatHandler) chat_handlers += ["GLM-4.6V", "GLM-4.6V-Thinking", "GLM-4.1V-Thinking", "LFM2-VL"] except: GLM46VChatHandler = None LFM2VLChatHandler = None GLM41VChatHandler = None try: from llama_cpp.llama_chat_format import LFM25VLChatHandler chat_handlers += ["LFM2.5-VL"] except: LFM25VLChatHandler = None try: from llama_cpp.llama_chat_format import GraniteDoclingChatHandler chat_handlers += ["Granite-Docling"] except: GraniteDoclingChatHandler = None try: from llama_cpp.llama_chat_format import MiniCPMv45ChatHandler chat_handlers += ["MiniCPM-v4.5", "MiniCPM-v4.5-Thinking"] except: MiniCPMv45ChatHandler = None try: from llama_cpp.llama_chat_format import MiniCPMv46ChatHandler chat_handlers += ["MiniCPM-v4.6", "MiniCPM-v4.6-Thinking"] except: MiniCPMv46ChatHandler = None try: from llama_cpp.llama_chat_format import PaddleOCRChatHandler chat_handlers += ["PaddleOCR-VL-1.5"] except: PaddleOCRChatHandler = None try: from llama_cpp.llama_chat_format import Qwen3ASRChatHandler chat_handlers += ["Qwen3-ASR"] except: Qwen3ASRChatHandler = None try: from llama_cpp.llama_chat_format import Step3VLChatHandler chat_handlers += ["Step3-VL"] except: Step3VLChatHandler = None class AnyType(str): def __ne__(self, __value: object) -> bool: return False class LLAMA_CPP_STORAGE: llm = None chat_handler = None current_config = None #states = {} messages = {} sys_prompts = {} @classmethod def clean_state(cls, id=-1): if id == -1: #cls.states.clear() cls.messages.clear() cls.sys_prompts.clear() else: #cls.states.pop(f"{id}", None) cls.messages.pop(f"{id}", None) cls.sys_prompts.pop(f"{id}", None) @classmethod def clean(cls, all=False): try: cls.llm.close() except Exception: pass try: cls.chat_handler._exit_stack.close() except Exception: pass cls.llm = None cls.chat_handler = None cls.current_config = None if all: cls.clean_state() gc.collect() mm.soft_empty_cache() @classmethod def load_model(cls, config): def get_chat_handler(chat_handler): match chat_handler: case "Qwen3.5"|"Qwen3.5-Thinking"|"Qwen3.6"|"Qwen3.6-Thinking": return Qwen35ChatHandler case "Qwen3-VL"|"Qwen3-VL-Thinking": return Qwen3VLChatHandler case "Qwen3-ASR": return Qwen3ASRChatHandler case "Qwen2.5-VL"|"MinerU2.5-Pro": return Qwen25VLChatHandler case "LLaVA-1.5": return Llava15ChatHandler case "LLaVA-1.6": return Llava16ChatHandler case "Moondream2": return MoondreamChatHandler case "nanoLLaVA": return NanoLlavaChatHandler case "llama3-Vision-Alpha": return Llama3VisionAlphaChatHandler case "MiniCPM-v2.6": return MiniCPMv26ChatHandler case "MiniCPM-v4.5"|"MiniCPM-v4.5-Thinking": return MiniCPMv45ChatHandler case "MiniCPM-v4.6"|"MiniCPM-v4.6-Thinking": return MiniCPMv46ChatHandler case "Gemma3": return Gemma3ChatHandler case "Gemma4": return Gemma4ChatHandler case "GLM-4.6V"|"GLM-4.6V-Thinking": return GLM46VChatHandler case "GLM-4.1V-Thinking": return GLM41VChatHandler case "LFM2-VL": return LFM2VLChatHandler case "LFM2.5-VL": return LFM25VLChatHandler case "Granite-Docling": return GraniteDoclingChatHandler case "DeepSeek-OCR": return MTMDChatHandler case "PaddleOCR-VL-1.5": return PaddleOCRChatHandler case "Step3-VL": return Step3VLChatHandler case "None": return None case _: raise ValueError(f'Unknow model type: "{chat_handler}"') cls.clean(all=True) cls.current_config = config.copy() model = config["model"] mmproj = config["mmproj"] chat_handler = config["chat_handler"] n_ctx = config["n_ctx"] vram_limit = config["vram_limit"] image_max_tokens = config["image_max_tokens"] image_min_tokens = config["image_min_tokens"] n_gpu_layers = -1 model_path = os.path.join(folder_paths.models_dir, 'LLM', model) handler = get_chat_handler(chat_handler) if vram_limit != -1: gguf_layers = get_layer_count(model_path) or 32 gguf_size = os.path.getsize(model_path) * 1.55 / (1024 ** 3) gguf_layer_size = gguf_size / gguf_layers if mmproj and mmproj != "None": mmproj_path = os.path.join(folder_paths.models_dir, 'LLM', mmproj) if chat_handler == "None": raise ValueError('"chat_handler" cannot be None!') if vram_limit != -1: mmproj_size = os.path.getsize(mmproj_path) * 1.55 / (1024 ** 3) n_gpu_layers = max(1, int((vram_limit - mmproj_size) / gguf_layer_size)) print(f"[llama-cpp_vlm] Loading clip: {mmproj}") think_mode = "Thinking" in chat_handler kwargs = {"clip_model_path": mmproj_path, "verbose": False} if chat_handler in ["Qwen3-VL", "Qwen3-VL-Thinking"]: kwargs["force_reasoning"] = think_mode kwargs["image_max_tokens"] = image_max_tokens kwargs["image_min_tokens"] = image_min_tokens elif chat_handler in ["MiniCPM-v4.5", "GLM-4.6V", "Qwen3.5"]: kwargs["enable_thinking"] = think_mode if _MTMD: kwargs["image_max_tokens"] = image_max_tokens kwargs["image_min_tokens"] = image_min_tokens try: cls.chat_handler = handler(**kwargs) except Exception as e: raise RuntimeError(f"{e}\nPlease update llama-cpp-python from 'https://github.com/JamePeng/llama-cpp-python/releases'") else: if vram_limit != -1: n_gpu_layers = max(1, int(vram_limit / gguf_layer_size)) if handler is not None: cls.chat_handler = handler(verbose=False) else: cls.chat_handler = None print(f"[llama-cpp_vlm] Loading model: {model}") print(f"[llama-cpp_vlm] n_gpu_layers = {n_gpu_layers}") cls.llm = Llama(model_path, chat_handler=cls.chat_handler, n_gpu_layers=n_gpu_layers, n_ctx=n_ctx, verbose=False) any_type = AnyType("*") if not hasattr(mm, "unload_all_models_backup"): mm.unload_all_models_backup = mm.unload_all_models def patched_unload_all_models(*args, **kwargs): LLAMA_CPP_STORAGE.clean(all=True) result = mm.unload_all_models_backup(*args, **kwargs) return result mm.unload_all_models = patched_unload_all_models print("[llama-cpp_vlm] Model cleanup hook applied!") llm_extensions = ['.ckpt', '.pt', '.bin', '.pth', '.safetensors', '.gguf'] folder_paths.folder_names_and_paths["LLM"] = ([os.path.join(folder_paths.models_dir, "LLM")], llm_extensions) preset_prompts = { "Empty - Nothing": "", "Normal - Describe": "Describe this @.", "Prompt Style - Tags": "Your task is to generate a clean list of comma-separated tags for a text-to-@ AI, based *only* on the visual information in the @. Limit the output to a maximum of 50 unique tags. Strictly describe visual elements like subject, clothing, environment, colors, lighting, and composition. Do not include abstract concepts, interpretations, marketing terms, or technical jargon (e.g., no 'SEO', 'brand-aligned', 'viral potential'). The goal is a concise list of visual descriptors. Avoid repeating tags.", "Prompt Style - Simple": "Analyze the @ and generate a simple, single-sentence text-to-@ prompt. Describe the main subject and the setting concisely.", "Prompt Style - Detailed": "Generate a detailed, artistic text-to-@ prompt based on the @. Combine the subject, their actions, the environment, lighting, and overall mood into a single, cohesive paragraph of about 2-3 sentences. Focus on key visual details.", "Prompt Style - Extreme Detailed": "Generate an extremely detailed and descriptive text-to-@ prompt from the @. Create a rich paragraph that elaborates on the subject's appearance, textures of clothing, specific background elements, the quality and color of light, shadows, and the overall atmosphere. Aim for a highly descriptive and immersive prompt.", "Prompt Style - Cinematic": "Act as a master prompt engineer. Create a highly detailed and evocative prompt for an @ generation AI. Describe the subject, their pose, the environment, the lighting, the mood, and the artistic style (e.g., photorealistic, cinematic, painterly). Weave all elements into a single, natural language paragraph, focusing on visual impact.", "Creative - Detailed Analysis": "Describe this @ in detail, breaking down the subject, attire, accessories, background, and composition into separate sections.", "Creative - Summarize Video": "Summarize the key events and narrative points in this video.", "Creative - Short Story": "Write a short, imaginative story inspired by this @ or video.", "Creative - Refine & Expand Prompt": "Refine and enhance the following user prompt for creative text-to-@ generation. Keep the meaning and keywords, make it more expressive and visually rich. Output **only the improved prompt text itself**, without any reasoning steps, thinking process, or additional commentary.", "Vision - *Bounding Box": 'Locate every instance that belongs to the following categories: "#". Report bbox coordinates in {"bbox_2d": [x1, y1, x2, y2], "label": "string"} JSON format as a List.' } preset_tags = list(preset_prompts.keys()) def image2base64(image): img = Image.fromarray(image) buffered = io.BytesIO() img.save(buffered, format="JPEG", quality=85) img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8') return img_base64 def parse_json(json_str): json_output = json_str.strip().removeprefix("```json").removesuffix("```") try: parsed = json.loads(json_output) except Exception as e: raise ValueError(f"Unable to load JSON data!\n{e}") return parsed def scale_image(image: torch.Tensor, max_size: int = 128): resized_frames = [] img_np = np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8) img_pil = Image.fromarray(img_np) w, h = img_pil.size scale = min(max_size / max(w, h), 1.0) new_w, new_h = int(w * scale), int(h * scale) img_resized = img_pil.resize((new_w, new_h), Image.Resampling.LANCZOS) return np.array(img_resized) def qwen3bbox(image, json): img = Image.fromarray(np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) bboxes = [] for item in json: x0, y0, x1, y1 = item["bbox_2d"] size = 1000 x0 = x0 / size * img.width y0 = y0 / size * img.height x1 = x1 / size * img.width y1 = y1 / size * img.height bboxes.append((x0, y0, x1, y1)) return bboxes def draw_bbox(image, json, mode): label_colors = {} img = Image.fromarray(np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) draw = ImageDraw.Draw(img) for item in json: try: label = item["label"] except Exception: try: label = item["text_content"] except Exception: label = "bbox" x0, y0, x1, y1 = item["bbox_2d"] if mode in ["Qwen3-VL", "Qwen2.5-VL"]: size = 1000 x0 = x0 / size * img.width y0 = y0 / size * img.height x1 = x1 / size * img.width y1 = y1 / size * img.height bbox = (x0, y0, x1, y1) if label not in label_colors: label_colors[label] = tuple(random.randint(80, 180) for _ in range(3)) color = label_colors[label] draw.rectangle(bbox, outline=color, width=4) text_y = max(0, y0 - 10) text_size = draw.textbbox((x0, text_y), label) draw.rectangle([text_size[0], text_size[1]-2, text_size[2]+4, text_size[3]+2], fill=color) draw.text((x0+2, text_y), label, fill=(255,255,255)) return torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0) class llama_cpp_model_loader: @classmethod def INPUT_TYPES(s): all_llms = folder_paths.get_filename_list("LLM") model_list = [f for f in all_llms if "mmproj" not in f.lower()] mmproj_list = ["None"]+[f for f in all_llms if "mmproj" in f.lower()] return {"required": { "model": (model_list,), "mmproj": (mmproj_list, {"default": "None"}), "chat_handler": (chat_handlers, {"default": "None"}), "n_ctx": ("INT", { "default": 8192, "min": 1024, "max": 327680, "step": 128, "tooltip": "Context length limit." }), "vram_limit": ("INT", { "default": -1, "min": -1, "max": 1024, "step": 1, "tooltip": "VRAM usage limit in GB (-1 = no limit)\nReference range; actual usage may slightly exceed." }), "image_min_tokens": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 32}), "image_max_tokens": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 32}), } } RETURN_TYPES = ("LLAMACPPMODEL",) RETURN_NAMES = ("llama_model",) FUNCTION = "loadmodel" CATEGORY = "llama-cpp-vlm" ''' @classmethod def IS_CHANGED(s, model, mmproj, chat_handler, n_ctx, vram_limit, image_min_tokens, image_max_tokens): if LLAMA_CPP_STORAGE.llm is None: return float("NaN") custom_config = { "model": model, "mmproj": mmproj, "chat_handler":chat_handler, "n_ctx": n_ctx, "vram_limit": vram_limit, "image_min_tokens": image_min_tokens, "image_max_tokens": image_max_tokens } config_str = json.dumps(custom_config, sort_keys=True, ensure_ascii=False) return config_str ''' def loadmodel(self, model, mmproj, chat_handler, n_ctx, vram_limit, image_min_tokens, image_max_tokens): custom_config = { "model": model, "mmproj": mmproj, "chat_handler":chat_handler, "n_ctx": n_ctx, "vram_limit": vram_limit, "image_min_tokens": image_min_tokens, "image_max_tokens": image_max_tokens } if not LLAMA_CPP_STORAGE.llm or LLAMA_CPP_STORAGE.current_config != custom_config: print("[llama-cpp_vlm] Loading model...") LLAMA_CPP_STORAGE.load_model(custom_config) return (custom_config,) class llama_cpp_instruct_adv: @classmethod def INPUT_TYPES(s): return { "required": { "llama_model": ("LLAMACPPMODEL",), "preset_prompt": (preset_tags, {"default": preset_tags[1]}), "custom_prompt": ("STRING", {"default": "", "multiline": True, "placeholder": 'user_prompt\n\nFor preset hints marked with an "*", this will be used to fill the placeholder (e.g., Object names in BBox detection)\nOtherwise, this will override the preset prompts.'}), "system_prompt": ("STRING", {"multiline": True, "default": ""}), "inference_mode": (["one by one", "images", "video"], { "default": "one by one", "tooltip": "one by one: Read one image at a time\nimages: \tRead all images at once\nvideo: \tTreat the input images as video" }), "max_frames": ("INT", { "default": 24, "min": 2, "max": 1024, "step": 1, "tooltip": 'Number of frames to sample evenly from input video.\n(for "video" mode only)' }), "max_size": ("INT", { "default": 256, "min": 128, "max": 16384, "step": 64, "tooltip": 'Max size of input images in "images" and "video" modes.' }), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}), "force_offload": ("BOOLEAN", { "default": False, "tooltip": "Unload the model after inference." }), "save_states": ("BOOLEAN", { "default": False, "tooltip": "Preserve the context of this conversation in RAM." }), }, "hidden": { "unique_id": "UNIQUE_ID", }, "optional": { "parameters": ("LLAMACPPARAMS",), "images": ("IMAGE",), "queue_handler": (any_type, {"tooltip": "Used to control the execution order of instruct nodes."}), }, } RETURN_TYPES = ("STRING", "STRING", "INT") RETURN_NAMES = ("output", "output_list", "state_uid") OUTPUT_IS_LIST = (False, True, False) FUNCTION = "process" CATEGORY = "llama-cpp-vlm" def sanitize_messages(self, messages): clean_messages = messages.copy() for msg in clean_messages: content = msg.get("content") if isinstance(content, list): for item in content: if isinstance(item, dict) and item.get("type") == "image_url": item["image_url"]["url"] = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAADElEQVQImWP4//8/AAX+Av5Y8msOAAAAAElFTkSuQmCC" return clean_messages def process(self, llama_model, preset_prompt, custom_prompt, system_prompt, inference_mode, max_frames, max_size, seed, force_offload, save_states, unique_id, parameters=None, images=None, queue_handler=None): if not LLAMA_CPP_STORAGE.llm: LLAMA_CPP_STORAGE.load_model(llama_model) #raise RuntimeError("The model has been unloaded or failed to load!") if parameters is None: parameters = {} if _MTMD: parameters.pop("present_penalty", None) _uid = parameters.get("state_uid", None) _parameters = parameters.copy() _parameters.pop("state_uid", None) uid = unique_id.rpartition('.')[-1] if _uid in (None, -1) else _uid last_sys_prompt = LLAMA_CPP_STORAGE.sys_prompts.get(f"{uid}", None) video_input = inference_mode == "video" system_prompts = "请将输入的图片序列当做视频而不是静态帧序列, " + system_prompt if video_input else system_prompt if last_sys_prompt != system_prompts: messages = [] LLAMA_CPP_STORAGE.clean_state() LLAMA_CPP_STORAGE.sys_prompts[f"{uid}"] = system_prompts if system_prompts.strip(): messages.append({"role": "system", "content": system_prompts}) else: if save_states: try: print(f"[llama-cpp_vlm] Loading state and history id={uid}...") #LLAMA_CPP_STORAGE.llm.load_state(LLAMA_CPP_STORAGE.states[f"{uid}"]) messages = LLAMA_CPP_STORAGE.messages.get(f"{uid}", []) except Exception as e: messages = [] else: messages = [] out1 = "" out2 = [] user_content = [] if custom_prompt.strip() and "*" not in preset_prompt: user_content.append({"type": "text", "text": custom_prompt}) else: p = preset_prompts[preset_prompt].replace("#", custom_prompt.strip()).replace("@", "video" if video_input else "image") user_content.append({"type": "text", "text": p}) if images is not None: if not hasattr(LLAMA_CPP_STORAGE.chat_handler, "clip_model_path") or LLAMA_CPP_STORAGE.chat_handler.clip_model_path is None: raise ValueError("Image input detected, but the loaded model is not configured with a mmproj module.") frames = images if video_input: indices = np.linspace(0, len(images) - 1, max_frames, dtype=int) frames = [images[i] for i in indices] if inference_mode == "one by one": tmp_list = [] image_content = { "type": "image_url", "image_url": {"url": ""} } user_content.append(image_content) messages.append({"role": "user", "content": user_content}) print(f"[llama-cpp_vlm] Start processing {len(frames)} images") for i, image in enumerate(cqdm(frames)): if mm.processing_interrupted(): raise mm.InterruptProcessingException() data = image2base64(np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) for item in user_content: if item.get("type") == "image_url": item["image_url"]["url"] = f"data:image/jpeg;base64,{data}" break output = LLAMA_CPP_STORAGE.llm.create_chat_completion(messages=messages, seed=seed, **_parameters) text = output['choices'][0]['message']['content'].removeprefix(": ").lstrip() out2.append(text) if len(frames) > 1: tmp_list.append(f"====== Image {i+1} ======") tmp_list.append(text) out1 = "\n\n".join(tmp_list) else: for image in frames: if len(frames) > 1: data = image2base64(scale_image(image, max_size)) else: data = image2base64(np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) image_content = { "type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{data}"} } user_content.append(image_content) messages.append({"role": "user", "content": user_content}) output = LLAMA_CPP_STORAGE.llm.create_chat_completion(messages=messages, seed=seed, **_parameters) out1 = output['choices'][0]['message']['content'].removeprefix(": ").lstrip() out2 = [out1] else: messages.append({"role": "user", "content": user_content}) output = LLAMA_CPP_STORAGE.llm.create_chat_completion(messages=messages, seed=seed, **_parameters) out1 = output['choices'][0]['message']['content'].removeprefix(": ").lstrip() out2 = [out1] if save_states: print(f"[llama-cpp_vlm] Saving state id={uid}...") #LLAMA_CPP_STORAGE.states[f"{uid}"] = LLAMA_CPP_STORAGE.llm.save_state() messages.append({"role": "assistant", "content": out1}) clear_message = self.sanitize_messages(messages) LLAMA_CPP_STORAGE.messages[f"{uid}"] = clear_message else: if not LLAMA_CPP_STORAGE.messages.get(f"{uid}"): LLAMA_CPP_STORAGE.sys_prompts.pop(f"{uid}", None) if force_offload: LLAMA_CPP_STORAGE.clean() else: if LLAMA_CPP_STORAGE.current_config["chat_handler"] in ["Qwen3.5", "Qwen3.5-Thinking"]: LLAMA_CPP_STORAGE.llm.n_tokens = 0 LLAMA_CPP_STORAGE.llm._ctx.memory_clear(True) if LLAMA_CPP_STORAGE.llm.is_hybrid and LLAMA_CPP_STORAGE.llm._hybrid_cache_mgr is not None: LLAMA_CPP_STORAGE.llm._hybrid_cache_mgr.clear() del messages gc.collect() return (out1, out2, uid) class llama_cpp_parameters: @classmethod def INPUT_TYPES(s): return { "required": { "max_tokens": ("INT", {"default": 1024, "min": 0, "max": 4096, "step": 1}), "top_k": ("INT", {"default": 30, "min": 0, "max": 1000, "step": 1}), "top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}), "min_p": ("FLOAT", {"default": 0.05, "min": 0.0, "max": 1.0, "step": 0.01}), "typical_p": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), "temperature": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.01}), "repeat_penalty": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), "frequency_penalty": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), "present_penalty": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 2.0, "step": 0.01}), #"tfs_z": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), #"penalty_last_n": ("INT", {"default": 64, "min": -1, "max": 8192, "step": 1}), "mirostat_mode": ("INT", {"default": 0, "min": 0, "max": 2, "step": 1}), "mirostat_eta": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}), "mirostat_tau": ("FLOAT", {"default": 5.0, "min": 0.0, "max": 10.0, "step": 0.01}), "state_uid": ("INT", { "default": -1, "min": -1, "max": 999999, "step": 1, "tooltip": "Use a specific ID to save the conversation state.\n(-1 = use node's unique_id)" }), } } RETURN_TYPES = ("LLAMACPPARAMS",) RETURN_NAMES = ("parameters",) FUNCTION = "process" CATEGORY = "llama-cpp-vlm" def process(self, **kwargs): return (kwargs,) class llama_cpp_clean_states: @classmethod def INPUT_TYPES(s): return { "required": { "any": (any_type,), "state_uid": ("INT", { "default": -1, "min": -1, "max": 999999, "step": 1, "tooltip": "Clear the saved state for a specific ID (-1 = clear all)" }), }, } RETURN_TYPES = (any_type,) RETURN_NAMES = ("any",) FUNCTION = "process" CATEGORY = "llama-cpp-vlm" def process(self, any, state_uid): print(f"[llama-cpp_vlm] Cleaning up saved states {state_uid}...") LLAMA_CPP_STORAGE.clean_state(state_uid) return (any,) class llama_cpp_unload_model: @classmethod def INPUT_TYPES(s): return {"required": {"any": (any_type,)}} RETURN_TYPES = (any_type,) RETURN_NAMES = ("any",) FUNCTION = "process" CATEGORY = "llama-cpp-vlm" def process(self, any): print("[llama-cpp_vlm] Unloading llama model...") LLAMA_CPP_STORAGE.clean() return (any,) class json_to_bbox: @classmethod def INPUT_TYPES(s): return { "required": { "json": ("STRING", {"forceInput": True}), "mode": (["simple","Qwen3-VL", "Qwen2.5-VL"], {"default": "simple"}), "label": ("STRING", { "default":"", "multiline": False, "tooltip": "Select only the BBoxes with specific labels." }), }, "optional": { "image": ("IMAGE",), } } RETURN_TYPES = ("BBOX", "IMAGE") RETURN_NAMES = ("bboxes", "image_list") OUTPUT_IS_LIST = (True, True) INPUT_IS_LIST = True FUNCTION = "process" CATEGORY = "llama-cpp-vlm" def process(self, json, mode, label, image=None): mode = mode[0] label = label[0] flat_images_list = [] original_structure = [] if image is not None: for img_batch in image: if img_batch.ndim == 3: flat_images_list.append(img_batch.unsqueeze(0)) original_structure.append(1) else: count = img_batch.shape[0] original_structure.append(count) for n in range(count): flat_images_list.append(img_batch[n:n+1]) total_images = len(flat_images_list) output_bboxes = [] processed_flat_results = [] for i, j in enumerate(json): bboxes = parse_json(j) if label != "": try: bboxes = [item for item in bboxes if item["label"] == label] except Exception: bboxes = [item for item in bboxes if item.get("text_content") == label] if total_images > 0: curr_idx = i if i < total_images else (total_images - 1) curr_img = flat_images_list[curr_idx] try: res_img = draw_bbox(curr_img[0], bboxes, mode) if res_img.ndim == 3: res_img = res_img.unsqueeze(0) elif res_img.ndim == 4 and res_img.shape[0] > 1: res_img = res_img[0:1] processed_flat_results.append(res_img) except Exception as e: print(f"Error drawing on image {curr_idx}: {e}") processed_flat_results.append(curr_img) if mode in ["Qwen3-VL", "Qwen2.5-VL"]: if total_images == 0: raise ValueError("Image required for Qwen mode") curr_idx = i if i < total_images else (total_images - 1) bbox = qwen3bbox(flat_images_list[curr_idx][0], bboxes) else: bbox = [tuple(item["bbox_2d"]) for item in bboxes] output_bboxes.append(bbox) restructured_images_list = [] cursor = 0 for count in original_structure: chunk = processed_flat_results[cursor : cursor + count] if chunk: restructured_images_list.append(torch.cat(chunk, dim=0)) cursor += count return (output_bboxes, restructured_images_list) class SEG: def __init__(self, cropped_image, cropped_mask, confidence, crop_region, bbox, label, control_net_wrapper=None): self.cropped_image = cropped_image self.cropped_mask = cropped_mask self.confidence = confidence self.crop_region = crop_region self.bbox = bbox self.label = label self.control_net_wrapper = control_net_wrapper def __repr__(self): return (f"SEG(cropped_image={self.cropped_image}, cropped_mask=shape{self.cropped_mask.shape}, confidence={self.confidence}, bbox={self.bbox}, label='{self.label}'), control_net_wrapper={self.control_net_wrapper}") class bbox_to_segs: @classmethod def INPUT_TYPES(s): return { "required": { "bboxes": ("BBOX",), "image": ("IMAGE",), "dilation": ("INT", {"default": 10, "min": 0, "max": 200, "step": 1}), "feather": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}), } } RETURN_TYPES = ("SEGS",) FUNCTION = "process" CATEGORY = "llama-cpp-vlm" def process(self, bboxes, image, dilation, feather): _batch_size, height, width, _channels = image.shape mask_shape = (height, width) seg_list = [] image_for_cropping = image[0] for bbox in bboxes: if not isinstance(bbox, (list, tuple)) or len(bbox) < 4: print(f"Warning: Skipping invalid bbox item: {bbox}") continue x1, y1, x2, y2 = map(int, bbox) x1_exp = x1 - dilation y1_exp = y1 - dilation x2_exp = x2 + dilation y2_exp = y2 + dilation crop_region = [x1_exp, y1_exp, x2_exp, y2_exp] crop_w = x2_exp - x1_exp crop_h = y2_exp - y1_exp if crop_h <= 0 or crop_w <= 0: print(f"Warning: Skipping bbox with invalid expanded size: {crop_region}") continue local_mask_np = np.zeros((crop_h, crop_w), dtype=np.float32) local_x1 = dilation local_y1 = dilation local_x2 = local_x1 + (x2 - x1) local_y2 = local_y1 + (y2 - y1) local_mask_np[local_y1:local_y2, local_x1:local_x2] = 1.0 if feather > 0: local_mask_np = gaussian_filter(local_mask_np, sigma=feather) cropped_mask_np = local_mask_np cropped_img_padded = torch.zeros((crop_h, crop_w, 3), dtype=image.dtype, device=image.device) src_x_start = max(0, x1_exp) src_y_start = max(0, y1_exp) src_x_end = min(width, x2_exp) src_y_end = min(height, y2_exp) dst_x_start = src_x_start - x1_exp dst_y_start = src_y_start - y1_exp dst_x_end = src_x_end - x1_exp dst_y_end = src_y_end - y1_exp if src_x_end > src_x_start and src_y_end > src_y_start: source_crop = image_for_cropping[src_y_start:src_y_end, src_x_start:src_x_end, :] cropped_img_padded[dst_y_start:dst_y_end, dst_x_start:dst_x_end, :] = source_crop cropped_image_tensor = cropped_img_padded.permute(2, 0, 1).unsqueeze(0) seg = SEG( cropped_image=cropped_image_tensor, cropped_mask=cropped_mask_np, confidence=np.array([0.9], dtype=np.float32), crop_region=crop_region, bbox=np.array(bbox, dtype=np.float32), label="bbox" ) seg_list.append(seg) segs = (mask_shape, seg_list) return (segs,) class bbox_to_mask: @classmethod def INPUT_TYPES(s): return { "required": { "bboxes": ("BBOX",), "image": ("IMAGE",), "dilation": ("INT", {"default": 10, "min": 0, "max": 200, "step": 1}), "feather": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}), } } RETURN_TYPES = ("MASK",) RETURN_NAMES = ("mask",) FUNCTION = "process" CATEGORY = "llama-cpp-vlm" def process(self, bboxes, image, dilation, feather): masks = [] _batch_size, height, width, _channels = image.shape mask_shape = (height, width) combined_full_mask = torch.zeros(mask_shape, dtype=torch.float32, device=image.device) for i, bbox in enumerate(bboxes): if not isinstance(bbox, (list, tuple)) or len(bbox) < 4: print(f"Warning: Skipping invalid bbox item: {bbox}") continue x1, y1, x2, y2 = map(int, bbox) x1_exp = x1 - dilation y1_exp = y1 - dilation x2_exp = x2 + dilation y2_exp = y2 + dilation crop_w = x2_exp - x1_exp crop_h = y2_exp - y1_exp if crop_h <= 0 or crop_w <= 0: continue local_mask_np = np.zeros((crop_h, crop_w), dtype=np.float32) local_x1 = dilation local_y1 = dilation local_x2 = local_x1 + (x2 - x1) local_y2 = local_y1 + (y2 - y1) local_mask_np[local_y1:local_y2, local_x1:local_x2] = 1.0 if feather > 0: local_mask_np = gaussian_filter(local_mask_np, sigma=feather) current_full_mask_np = np.zeros(mask_shape, dtype=np.float32) x1_c, y1_c = max(0, x1_exp), max(0, y1_exp) x2_c, y2_c = min(width, x2_exp), min(height, y2_exp) if x2_c > x1_c and y2_c > y1_c: current_full_mask_np[y1_c:y2_c, x1_c:x2_c] = 1.0 if feather > 0: current_full_mask_np = gaussian_filter(current_full_mask_np, sigma=feather) current_full_mask_tensor = torch.from_numpy(current_full_mask_np).to(image.device) combined_full_mask = torch.maximum(combined_full_mask, current_full_mask_tensor) masks.append(combined_full_mask.unsqueeze(0)) return (torch.cat(masks, dim=0),) class bboxes_to_bbox: @classmethod def INPUT_TYPES(s): return { "required": { "bboxes": ("BBOX",), "image_index": ("INT", {"default": 0, "min": 0, "max": 1000000, "step": 1}), "bbox_index": ("INT", { "default": 0, "min": -998, "max": 999, "step": 1, "tooltip": "BBox index in the image. Set to 999 to get all bboxes." }), } } RETURN_TYPES = ("BBOX",) RETURN_NAMES = ("bbox",) FUNCTION = "process" CATEGORY = "llama-cpp-vlm" def process(self, bboxes, image_index, bbox_index): if bbox_index != 999: return ([bboxes[image_index][bbox_index]],) return (bboxes[image_index],) # from: https://github.com/crystian/ComfyUI-Crystools class parse_json_node: @classmethod def INPUT_TYPES(s): return { "required": { "input": ("STRING", {"forceInput": True}), }, "optional": { "key": ("STRING",), "default": ("STRING",), }, } RETURN_TYPES = (any_type, "STRING", "INT", "FLOAT", "BOOLEAN") RETURN_NAMES = ("any", "string", "int", "float", "boolean") FUNCTION = "process" CATEGORY = "llama-cpp-vlm" def process(self, input, key=None, default=None): if isinstance(input, str): input = [input] result = {} for i, json in enumerate(input): val = "" if key is not None and key != "": val = get_nested_value(json.strip().removeprefix("```json").removesuffix("```"), key, default) else: raise ValueError("Key cannot be empty!") result["any"][i] = val try: result["string"][i] = str(val) except Exception as e: result["string"][i] = val try: result["int"][i] = int(val) except Exception as e: result["int"][i] = val try: result["float"][i] = float(val) except Exception as e: result["float"][i] = val try: result["boolean"][i] = val.lower() == "true" except Exception as e: result["boolean"][i] = val if len(result["any"]) == 1: result["any"] = result["any"][0] result["string"] = result["string"][0] result["int"] = result["int"][0] result["float"] = result["float"][0] result["boolean"] = result["boolean"][0] return (result["any"], result["string"], result["int"], result["float"], result["boolean"]) def get_nested_value(data, dotted_key, default=None): keys = dotted_key.split('.') for key in keys: if isinstance(data, str): data = json.loads(data) if isinstance(data, dict) and key in data: data = data[key] else: return default return data class remove_code_block: @classmethod def INPUT_TYPES(s): return { "required": { "input": ("STRING", {"forceInput": True}), }, "optional": { "label": ("STRING",), }, } RETURN_TYPES = ("STRING",) RETURN_NAMES = ("output",) FUNCTION = "process" CATEGORY = "llama-cpp-vlm" def process(self, input, label): if isinstance(input, str): input = [input] output = [] for value in input: output.append(value.strip().removeprefix(f"```{label}").removesuffix("```")) if len(output) == 1: return (output[0],) return (output,) class PromptEnhancerPreset: @classmethod def INPUT_TYPES(s): return { "required": { "preset": (["Qwen-Image [EN]", "Qwen-Image [ZH]", "Qwen-Image 2512 [EN]", "Qwen-Image 2512 [ZH]", "Qwen-Image-Edit", "Qwen-Image-Edit 2509", "Qwen-Image-Edit 2511", "Z-Image Turbo", "Flux.2 T2I", "Flux.2 I2I", "Wan T2V [EN]", "Wan T2V [ZH]", "Wan I2V [EN]", "Wan I2V [ZH]", "Wan I2V Full-Auto [EN]", "Wan I2V Full-Auto [ZH]", "Wan FLF2V [EN]", "Wan FLF2V [ZH]"], ) } } RETURN_TYPES = ("STRING",) RETURN_NAMES = ("system_prompt",) FUNCTION = "main" CATEGORY = "llama-cpp-vlm" def main(self, preset): match preset: case "Qwen-Image [EN]": return (QWEN_IMAGE_EN,) case "Qwen-Image [ZH]": return (QWEN_IMAGE_ZH,) case "Qwen-Image 2512 [EN]": return (QWEN_IMAGE_2512_EN,) case "Qwen-Image 2512 [ZH]": return (QWEN_IMAGE_2512_ZH,) case "Qwen-Image-Edit": return (QWEN_IMAGE_EDIT,) case "Qwen-Image-Edit 2509": return (QWEN_IMAGE_EDIT_2509,) case "Qwen-Image-Edit 2511": return (QWEN_IMAGE_EDIT_2511,) case "Z-Image Turbo": return (ZIMAGE_TURBO,) case "Flux.2 T2I": return (FLUX2_T2I,) case "Flux.2 I2I": return (FLUX2_I2I,) case "Wan T2V [EN]": return (WAN_T2V_EN,) case "Wan T2V [ZH]": return (WAN_T2V_ZH,) case "Wan I2V [EN]": return (WAN_I2V_EN,) case "Wan I2V [ZH]": return (WAN_I2V_ZH,) case "Wan I2V Full-Auto [EN]": return (WAN_I2V_EMPTY_EN,) case "Wan I2V Full-Auto [ZH]": return (WAN_I2V_EMPTY_ZH,) case "Wan FLF2V [EN]": return (WAN_FLF2V_EN,) case "Wan FLF2V [ZH]": return (WAN_FLF2V_ZH,) case _: raise ValueError(f'Unknow preset: "{preset}"') NODE_CLASS_MAPPINGS = { "llama_cpp_model_loader": llama_cpp_model_loader, "llama_cpp_instruct_adv": llama_cpp_instruct_adv, "llama_cpp_parameters": llama_cpp_parameters, "llama_cpp_unload_model": llama_cpp_unload_model, "llama_cpp_clean_states": llama_cpp_clean_states, "parse_json_node": parse_json_node, "json_to_bbox": json_to_bbox, "bbox_to_segs": bbox_to_segs, "bbox_to_mask": bbox_to_mask, "bboxes_to_bbox": bboxes_to_bbox, "remove_code_block": remove_code_block, "PromptEnhancerPreset": PromptEnhancerPreset, } NODE_DISPLAY_NAME_MAPPINGS = { "llama_cpp_model_loader": "Llama-cpp Model Loader", "llama_cpp_instruct_adv": "Llama-cpp Instruct", "llama_cpp_parameters": "Llama-cpp Parameters", "llama_cpp_unload_model": "Llama-cpp Unload Model", "llama_cpp_clean_states": "Llama-cpp Clean States", "parse_json_node": "Parse JSON", "json_to_bbox": "JSON to BBoxes", "bbox_to_segs": "BBoxes to SEGS", "bbox_to_mask": "BBoxes to MASK", "bboxes_to_bbox": "BBoxes to BBox", "remove_code_block": "Unpack Code Block", "PromptEnhancerPreset": "Prompt Enhancer Preset", }