From 5a505137eea2fc9bb9b227df99d9e083f21fff72 Mon Sep 17 00:00:00 2001 From: arcum42 Date: Thu, 5 Jun 2025 18:27:11 -0700 Subject: [PATCH] Enable LM Studio support. Revise workflow. --- __init__.py | 24 +-- example_workflows/LLM Example.json | 2 +- nodes/llm.py | 308 ++++++++++++++++++----------- requirements.txt | 4 +- utils/helpers.py | 7 +- utils/llm_wrapper.py | 135 +++++++++---- 6 files changed, 309 insertions(+), 171 deletions(-) diff --git a/__init__.py b/__init__.py index ba03818..599c124 100644 --- a/__init__.py +++ b/__init__.py @@ -10,6 +10,10 @@ sage_config = config_manager.settings_manager.data WEB_DIRECTORY = "./js" +# LM Studio support is a work in progress and is not enabled by default. +ENABLE_LM_STUDIO = True + + DEPRECIATED_CLASS_MAPPINGS = { "Sage_SetBool": Sage_SetBool, "Sage_SetInteger": Sage_SetInteger, @@ -91,12 +95,12 @@ METADATA_CLASS_MAPPINGS = { OLLAMA_CLASS_MAPPINGS = { "Sage_OllamaLLMPromptText": Sage_OllamaLLMPromptText, - "Sage_OllamaLLMPromptVision": Sage_OllamaLLMPromptVision, - "Sage_OllamaAdvancedOptions": Sage_OllamaAdvancedOptions + "Sage_OllamaLLMPromptVision": Sage_OllamaLLMPromptVision } LMSTUDIO_CLASS_MAPPINGS = { - "Sage_LMStudioLLMPrompt": Sage_LMStudioLLMPrompt + "Sage_LMStudioLLMPromptVision": Sage_LMStudioLLMPromptVision, + "Sage_LMStudioLLMPromptText": Sage_LMStudioLLMPromptText } LLM_CLASS_MAPPINGS = { @@ -107,9 +111,8 @@ LLM_CLASS_MAPPINGS = { if llm.OLLAMA_AVAILABLE: LLM_CLASS_MAPPINGS = LLM_CLASS_MAPPINGS | OLLAMA_CLASS_MAPPINGS -# Disabling LM Studio for now, as it is endlessly frustrating. -#if llm.LMSTUDIO_AVAILABLE: -# LLM_CLASS_MAPPINGS = LLM_CLASS_MAPPINGS | LMSTUDIO_CLASS_MAPPINGS +if llm.LMSTUDIO_AVAILABLE and ENABLE_LM_STUDIO: + LLM_CLASS_MAPPINGS = LLM_CLASS_MAPPINGS | LMSTUDIO_CLASS_MAPPINGS # A dictionary that contains all nodes you want to export with their names # NOTE: names should be globally unique @@ -201,10 +204,10 @@ OLLAMA_NAME_MAPPINGS = { "Sage_OllamaLLMPromptText": "Ollama LLM Prompt (Text)", "Sage_OllamaLLMPromptVision": "Ollama LLM Prompt (Vision)" } -# "Sage_OllamaAdvancedOptions": "Ollama Advanced Options" LMSTUDIO_NAME_MAPPINGS = { - "Sage_LMStudioLLMPrompt": "LM Studio LLM Prompt" + "Sage_LMStudioLLMPromptVision": "LM Studio LLM Prompt (Vision)", + "Sage_LMStudioLLMPromptText": "LM Studio LLM Prompt (Text)" } LLM_NAME_MAPPINGS = { @@ -215,9 +218,8 @@ LLM_NAME_MAPPINGS = { if llm.OLLAMA_AVAILABLE: LLM_NAME_MAPPINGS = LLM_NAME_MAPPINGS | OLLAMA_NAME_MAPPINGS -# Since LM Studio is currently not working, we are disabling it for now. -#if llm.LMSTUDIO_AVAILABLE: -# LLM_NAME_MAPPINGS = LLM_NAME_MAPPINGS | LMSTUDIO_NAME_MAPPINGS +if llm.LMSTUDIO_AVAILABLE and ENABLE_LM_STUDIO: + LLM_NAME_MAPPINGS = LLM_NAME_MAPPINGS | LMSTUDIO_NAME_MAPPINGS # A dictionary that contains the friendly/humanly readable titles for the nodes NODE_DISPLAY_NAME_MAPPINGS = DEPRECIATED_NAME_MAPPINGS | UTILITY_NAME_MAPPINGS | SETTINGS_NAME_MAPPINGS | TEXT_NAME_MAPPINGS | \ diff --git a/example_workflows/LLM Example.json b/example_workflows/LLM Example.json index 063cbd7..fb44e75 100644 --- a/example_workflows/LLM Example.json +++ b/example_workflows/LLM Example.json @@ -1 +1 @@ -{"id":"cbb10592-6ec8-49a7-b02c-a1fd0ad54482","revision":0,"last_node_id":32,"last_link_id":40,"nodes":[{"id":3,"type":"Sage_ConstructLLMPrompt","pos":[-2371.782470703125,365.072509765625],"size":[272.8399963378906,406.3199768066406],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"prompt","name":"prompt","type":"COMBO","widget":{"name":"prompt"},"link":null},{"localized_name":"extra_instructions","name":"extra_instructions","type":"STRING","widget":{"name":"extra_instructions"},"link":null},{"localized_name":"avoid_euphemisms","name":"avoid_euphemisms","type":"BOOLEAN","widget":{"name":"avoid_euphemisms"},"link":null},{"localized_name":"be_specific","name":"be_specific","type":"BOOLEAN","widget":{"name":"be_specific"},"link":null},{"localized_name":"erotic_imagery","name":"erotic_imagery","type":"BOOLEAN","widget":{"name":"erotic_imagery"},"link":null},{"localized_name":"exaggerate_quality","name":"exaggerate_quality","type":"BOOLEAN","widget":{"name":"exaggerate_quality"},"link":null},{"localized_name":"exclude_sexual","name":"exclude_sexual","type":"BOOLEAN","widget":{"name":"exclude_sexual"},"link":null},{"localized_name":"exclude_watermarks","name":"exclude_watermarks","type":"BOOLEAN","widget":{"name":"exclude_watermarks"},"link":null},{"localized_name":"only_describe_key_elements","name":"only_describe_key_elements","type":"BOOLEAN","widget":{"name":"only_describe_key_elements"},"link":null},{"localized_name":"only_output_description","name":"only_output_description","type":"BOOLEAN","widget":{"name":"only_output_description"},"link":null},{"localized_name":"use_vulgar_language","name":"use_vulgar_language","type":"BOOLEAN","widget":{"name":"use_vulgar_language"},"link":null}],"outputs":[{"localized_name":"prompt","name":"prompt","type":"STRING","links":[1]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ConstructLLMPrompt"},"widgets_values":["text_enhancement/expand_tags","mouse girl, light blue fur, green eyes, furry, paws, safe, t-shirt, skirt bandanna,ribbon,hair clip, tripped, flower, wet, drenched in water, mud, balloon, night, stars, moon, woods",true,false,false,true,false,false,false,true,false]},{"id":1,"type":"Sage_OllamaLLMPromptText","pos":[-2072.1416015625,366.8322448730469],"size":[241.4600067138672,186.6199951171875],"flags":{},"order":10,"mode":0,"inputs":[{"localized_name":"prompt","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":1},{"localized_name":"model","name":"model","type":"COMBO","widget":{"name":"model"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"response","name":"response","type":"STRING","links":[2]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_OllamaLLMPromptText"},"widgets_values":["","gemma3:12b-it-qat",0,"fixed"]},{"id":16,"type":"Sage_TilingInfo","pos":[-980.7807006835938,542.4208374023438],"size":[210,130],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"tile_size","name":"tile_size","type":"INT","widget":{"name":"tile_size"},"link":null},{"localized_name":"overlap","name":"overlap","type":"INT","widget":{"name":"overlap"},"link":null},{"localized_name":"temporal_size","name":"temporal_size","type":"INT","widget":{"name":"temporal_size"},"link":null},{"localized_name":"temporal_overlap","name":"temporal_overlap","type":"INT","widget":{"name":"temporal_overlap"},"link":null}],"outputs":[{"localized_name":"TILING_INFO","name":"TILING_INFO","type":"TILING_INFO","links":[24]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"1a98f4bb37562ea56b04345f92b282dab301bfdf","Node name for S&R":"Sage_TilingInfo"},"widgets_values":[512,64,64,8]},{"id":19,"type":"PreviewImage","pos":[-364.0637512207031,366.8205261230469],"size":[640.96728515625,534.4432373046875],"flags":{},"order":18,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":16}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.30","Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":26,"type":"Sage_SetText","pos":[-1042.842529296875,742.5321655273438],"size":[359.6160888671875,208.86476135253906],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"prefix","name":"prefix","shape":7,"type":"STRING","link":null},{"localized_name":"suffix","name":"suffix","shape":7,"type":"STRING","link":null},{"localized_name":"str","name":"str","type":"STRING","widget":{"name":"str"},"link":null}],"outputs":[{"localized_name":"str","name":"str","type":"STRING","links":[11]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"1a98f4bb37562ea56b04345f92b282dab301bfdf","Node name for S&R":"Sage_SetText"},"widgets_values":["signature, text"]},{"id":30,"type":"Sage_UNETLoader","pos":[-1297.701416015625,362.0794677734375],"size":[311.6608581542969,102],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"unet_name","name":"unet_name","type":"COMBO","widget":{"name":"unet_name"},"link":null},{"localized_name":"weight_dtype","name":"weight_dtype","type":"COMBO","widget":{"name":"weight_dtype"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"MODEL","links":[30]},{"localized_name":"model_info","name":"model_info","type":"MODEL_INFO","links":[31]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_UNETLoader"},"widgets_values":["chroma/chroma-unlocked-v34-detail-calibrated_float8_e4m3fn_scaled_stochastic.safetensors","default"]},{"id":27,"type":"Sage_SamplerInfo","pos":[-739.228759765625,370.4992370605469],"size":[210,178],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null}],"outputs":[{"localized_name":"SAMPLER_INFO","name":"SAMPLER_INFO","type":"SAMPLER_INFO","links":[19,32]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"1a98f4bb37562ea56b04345f92b282dab301bfdf","Node name for S&R":"Sage_SamplerInfo"},"widgets_values":[0,"fixed",36,6,"euler","normal"]},{"id":22,"type":"Sage_KSamplerTiledDecoder","pos":[-658.3071899414062,659.7105712890625],"size":[274.02325439453125,466.02325439453125],"flags":{},"order":16,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":30},{"localized_name":"sampler_info","name":"sampler_info","type":"SAMPLER_INFO","link":19},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":20},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":21},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":22},{"localized_name":"vae","name":"vae","type":"VAE","link":23},{"localized_name":"tiling_info","name":"tiling_info","shape":7,"type":"TILING_INFO","link":24},{"localized_name":"advanced_info","name":"advanced_info","shape":7,"type":"ADV_SAMPLER_INFO","link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null},{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[16,36]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"1a98f4bb37562ea56b04345f92b282dab301bfdf","Node name for S&R":"Sage_KSamplerTiledDecoder"},"widgets_values":[1]},{"id":31,"type":"Sage_SaveImageWithMetadata","pos":[309.880859375,370.86395263671875],"size":[577.8004760742188,586.694091796875],"flags":{},"order":19,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":36},{"localized_name":"param_metadata","name":"param_metadata","shape":7,"type":"STRING","link":35},{"localized_name":"extra_metadata","name":"extra_metadata","shape":7,"type":"STRING","link":null},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"include_node_metadata","name":"include_node_metadata","type":"BOOLEAN","widget":{"name":"include_node_metadata"},"link":null},{"localized_name":"include_extra_pnginfo_metadata","name":"include_extra_pnginfo_metadata","type":"BOOLEAN","widget":{"name":"include_extra_pnginfo_metadata"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_SaveImageWithMetadata"},"widgets_values":["ComfyUI_Meta",true,true]},{"id":12,"type":"T5TokenizerOptions","pos":[-1293.467041015625,1034.1729736328125],"size":[210,82],"flags":{},"order":11,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":8},{"localized_name":"min_padding","name":"min_padding","type":"INT","widget":{"name":"min_padding"},"link":null},{"localized_name":"min_length","name":"min_length","type":"INT","widget":{"name":"min_length"},"link":null}],"outputs":[{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":[9]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.34","Node name for S&R":"T5TokenizerOptions"},"widgets_values":[1,0]},{"id":14,"type":"Sage_EmptyLatentImagePassthrough","pos":[-1286.6578369140625,802.5692749023438],"size":[210,170],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":null},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null},{"localized_name":"sd3","name":"sd3","type":"BOOLEAN","widget":{"name":"sd3"},"link":null}],"outputs":[{"localized_name":"latent","name":"latent","type":"LATENT","links":[22]},{"localized_name":"width","name":"width","type":"INT","links":[33]},{"localized_name":"height","name":"height","type":"INT","links":[34]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"ada34c266f8268cb86ac1f1c916c5c7ddf44d2b4","Node name for S&R":"Sage_EmptyLatentImagePassthrough"},"widgets_values":[1024,1024,1,true]},{"id":15,"type":"VAELoader","pos":[-1295.7725830078125,671.365966796875],"size":[246.71221923828125,61.361602783203125],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"vae_name","name":"vae_name","type":"COMBO","widget":{"name":"vae_name"},"link":null}],"outputs":[{"localized_name":"VAE","name":"VAE","type":"VAE","links":[23]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.22","Node name for S&R":"VAELoader"},"widgets_values":["ae.safetensors"]},{"id":11,"type":"CLIPLoader","pos":[-1294.4630126953125,508.3432312011719],"size":[273.68035888671875,106],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"clip_name","name":"clip_name","type":"COMBO","widget":{"name":"clip_name"},"link":null},{"localized_name":"type","name":"type","type":"COMBO","widget":{"name":"type"},"link":null},{"localized_name":"device","name":"device","shape":7,"type":"COMBO","widget":{"name":"device"},"link":null}],"outputs":[{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":[8]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.30","Node name for S&R":"CLIPLoader"},"widgets_values":["t5/t5xxl_fp8_e4m3fn_scaled.safetensors","chroma","default"]},{"id":29,"type":"Sage_ConstructMetadataLite","pos":[-337.2514343261719,952.8579711914062],"size":[210,170],"flags":{},"order":17,"mode":0,"inputs":[{"localized_name":"model_info","name":"model_info","type":"MODEL_INFO","link":31},{"localized_name":"sampler_info","name":"sampler_info","type":"SAMPLER_INFO","link":32},{"localized_name":"lora_stack","name":"lora_stack","shape":7,"type":"LORA_STACK","link":null},{"localized_name":"positive_string","name":"positive_string","type":"STRING","widget":{"name":"positive_string"},"link":39},{"localized_name":"negative_string","name":"negative_string","type":"STRING","widget":{"name":"negative_string"},"link":40},{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":33},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":34}],"outputs":[{"localized_name":"param_metadata","name":"param_metadata","type":"STRING","links":[35]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ConstructMetadataLite"},"widgets_values":["","",0,0]},{"id":4,"type":"Sage_ViewAnything","pos":[-1809.8695068359375,366.8186950683594],"size":[476.97430419921875,492.1040954589844],"flags":{},"order":13,"mode":0,"inputs":[{"localized_name":"any","name":"any","type":"*","link":2}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":[38]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ViewAnything"},"widgets_values":["This image is a beautifully rendered scene of a young mouse girl, completely drenched and covered in mud, but somehow still radiating a sense of playful resilience. Her fur is a vibrant, light blue, soft and realistically textured, contrasting sharply with the dark, muddy ground. Her large, expressive green eyes are captivating, full of a mixture of surprise and a hint of mischief. She's a solidly furry character, with realistically depicted paws clutching a deflated, multi-colored balloon. She's wearing a simple white t-shirt, a short, pleated skirt, and a bright red bandana tied around her neck. A delicate pink ribbon is woven into her fur, and a sparkly hair clip adds a touch of charm. \n\nThe whole thing has a genuine, cinematic quality to it. She’s clearly just tripped, face-first, into a puddle – it’s a chaotic, momentary disaster, frozen in time. The water is realistically rendered, shimmering with reflected light. She’s surrounded by a dense, enchanting wood, deep in the embrace of night. Above, a brilliant moon hangs in the inky sky, sprinkled with a breathtaking display of stars. The lighting is superb - soft moonlight filters through the trees, highlighting the texture of her fur and casting long, dramatic shadows. It’s a striking, emotionally resonant image – a perfectly captured moment of childhood adventure gone slightly wrong. The composition is tight, drawing the eye directly to the little mouse girl and emphasizing her vulnerability and charm. It’s pretty great.\n"]},{"id":13,"type":"Sage_DualCLIPTextEncode","pos":[-972.6900024414062,368.31414794921875],"size":[210,118],"flags":{},"order":15,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":9},{"localized_name":"pos","name":"pos","shape":7,"type":"STRING","link":38},{"localized_name":"neg","name":"neg","shape":7,"type":"STRING","link":11},{"localized_name":"clean","name":"clean","type":"BOOLEAN","widget":{"name":"clean"},"link":null}],"outputs":[{"localized_name":"positive","name":"positive","type":"CONDITIONING","links":[20]},{"localized_name":"negative","name":"negative","type":"CONDITIONING","links":[21]},{"localized_name":"pos_text","name":"pos_text","type":"STRING","links":[39]},{"localized_name":"neg_text","name":"neg_text","type":"STRING","links":[40]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"766095f60ef1d196b39cc26e0e4df139c9a481e3","Node name for S&R":"Sage_DualCLIPTextEncode"},"widgets_values":[false]},{"id":8,"type":"Sage_ViewAnything","pos":[-1762.41162109375,958.9226684570312],"size":[441.8359069824219,601.9115600585938],"flags":{},"order":14,"mode":0,"inputs":[{"localized_name":"any","name":"any","type":"*","link":6}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ViewAnything"},"widgets_values":["A vibrant and dynamic illustration showcases the character Blue, a whimsical creature with a striking appearance. She's mid-leap, her body in a graceful arc against a backdrop of lush greenery. Blue has pale blue skin, sharp, pointed ears topped with a bright yellow and white striped headband, and large, expressive green eyes. Her limbs are slender and elongated, exhibiting a playful elegance.\n\nShe's wearing a simple, slightly rumpled white t-shirt and a colorful, short skirt featuring a lively pattern of various hues. Her feet are bare, and they're playfully splashing in a puddle, sending tiny droplets of water into the air. The detail in the puddles shows a muddy, textured surface.\n\nA cheerful pink balloon is held in her hand, adding a touch of whimsical delight to the scene. The composition is wonderfully balanced, with Blue positioned slightly off-center to create a sense of movement and energy. \n\nThe background features a dense forest, rendered with remarkable detail – various greens and browns of the foliage are beautifully captured, creating a rich and immersive environment. Sunlight filters through the canopy above, casting dappled shadows and creating a magical atmosphere. The lighting is skillfully utilized, highlighting Blue’s form while allowing the background to retain its depth and texture. A single, delicate white flower blooms near her feet, offering a pop of contrasting color and adding to the overall charm of the image. The rendering style is exceptionally clean, employing smooth lines and vibrant colors to create a visually stunning piece. The image feels like a still from a lively animated film, bursting with personality and charm.\n"]},{"id":6,"type":"Sage_LoadImage","pos":[-2370.88525390625,960.8961791992188],"size":[308.9095458984375,603.86376953125],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"image","name":"image","type":"IMAGE","links":[4]},{"localized_name":"mask","name":"mask","type":"MASK","links":null},{"localized_name":"width","name":"width","type":"INT","links":null},{"localized_name":"height","name":"height","type":"INT","links":null},{"localized_name":"metadata","name":"metadata","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_LoadImage"},"widgets_values":["LLM Example.png","image"]},{"id":9,"type":"Sage_OllamaLLMPromptVision","pos":[-2045.856201171875,956.5042114257812],"size":[277.0155944824219,189.7512969970703],"flags":{},"order":12,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":4},{"localized_name":"prompt","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":5},{"localized_name":"model","name":"model","type":"COMBO","widget":{"name":"model"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"response","name":"response","type":"STRING","links":[6]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_OllamaLLMPromptVision"},"widgets_values":["","gemma3:12b-it-qat",1,"fixed"]},{"id":5,"type":"Sage_ConstructLLMPrompt","pos":[-2041.75244140625,1192.8970947265625],"size":[271.3215026855469,370.4588928222656],"flags":{},"order":9,"mode":0,"inputs":[{"localized_name":"prompt","name":"prompt","type":"COMBO","widget":{"name":"prompt"},"link":null},{"localized_name":"extra_instructions","name":"extra_instructions","type":"STRING","widget":{"name":"extra_instructions"},"link":null},{"localized_name":"avoid_euphemisms","name":"avoid_euphemisms","type":"BOOLEAN","widget":{"name":"avoid_euphemisms"},"link":null},{"localized_name":"be_specific","name":"be_specific","type":"BOOLEAN","widget":{"name":"be_specific"},"link":null},{"localized_name":"erotic_imagery","name":"erotic_imagery","type":"BOOLEAN","widget":{"name":"erotic_imagery"},"link":null},{"localized_name":"exaggerate_quality","name":"exaggerate_quality","type":"BOOLEAN","widget":{"name":"exaggerate_quality"},"link":null},{"localized_name":"exclude_sexual","name":"exclude_sexual","type":"BOOLEAN","widget":{"name":"exclude_sexual"},"link":null},{"localized_name":"exclude_watermarks","name":"exclude_watermarks","type":"BOOLEAN","widget":{"name":"exclude_watermarks"},"link":null},{"localized_name":"only_describe_key_elements","name":"only_describe_key_elements","type":"BOOLEAN","widget":{"name":"only_describe_key_elements"},"link":null},{"localized_name":"only_output_description","name":"only_output_description","type":"BOOLEAN","widget":{"name":"only_output_description"},"link":null},{"localized_name":"use_vulgar_language","name":"use_vulgar_language","type":"BOOLEAN","widget":{"name":"use_vulgar_language"},"link":null}],"outputs":[{"localized_name":"prompt","name":"prompt","type":"STRING","links":[5]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ConstructLLMPrompt"},"widgets_values":["description/descriptive","",true,false,false,true,false,false,false,true,false]}],"links":[[1,3,0,1,0,"STRING"],[2,1,0,4,0,"*"],[4,6,0,9,0,"IMAGE"],[5,5,0,9,1,"STRING"],[6,9,0,8,0,"*"],[8,11,0,12,0,"CLIP"],[9,12,0,13,0,"CLIP"],[11,26,0,13,2,"STRING"],[16,22,1,19,0,"IMAGE"],[19,27,0,22,1,"SAMPLER_INFO"],[20,13,0,22,2,"CONDITIONING"],[21,13,1,22,3,"CONDITIONING"],[22,14,0,22,4,"LATENT"],[23,15,0,22,5,"VAE"],[24,16,0,22,6,"TILING_INFO"],[30,30,0,22,0,"MODEL"],[31,30,1,29,0,"MODEL_INFO"],[32,27,0,29,1,"SAMPLER_INFO"],[33,14,1,29,5,"INT"],[34,14,2,29,6,"INT"],[35,29,0,31,1,"STRING"],[36,22,1,31,0,"IMAGE"],[38,4,0,13,1,"STRING"],[39,13,2,29,3,"STRING"],[40,13,3,29,4,"STRING"]],"groups":[{"id":1,"title":"Text LLM","bounding":[-2381.782470703125,291.4725036621094,1060.351318359375,580.37841796875],"color":"#3f789e","font_size":24,"flags":{}},{"id":2,"title":"Image LLM","bounding":[-2380.88525390625,881.4401245117188,1065.917236328125,691.9158325195312],"color":"#3f789e","font_size":24,"flags":{}},{"id":4,"title":"Image","bounding":[-1304.8204345703125,293.22052001953125,1591.723876953125,842.5133666992188],"color":"#3f789e","font_size":24,"flags":{}},{"id":5,"title":"Save","bounding":[297.4398193359375,296.00665283203125,600.9755859375,675.9281005859375],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":0.6830134553650712,"offset":[706.2043978661527,-150.53112388479926]}},"version":0.4} \ No newline at end of file +{"id":"cbb10592-6ec8-49a7-b02c-a1fd0ad54482","revision":0,"last_node_id":39,"last_link_id":51,"nodes":[{"id":16,"type":"Sage_TilingInfo","pos":[-980.7807006835938,542.4208374023438],"size":[210,130],"flags":{},"order":0,"mode":4,"inputs":[{"localized_name":"tile_size","name":"tile_size","type":"INT","widget":{"name":"tile_size"},"link":null},{"localized_name":"overlap","name":"overlap","type":"INT","widget":{"name":"overlap"},"link":null},{"localized_name":"temporal_size","name":"temporal_size","type":"INT","widget":{"name":"temporal_size"},"link":null},{"localized_name":"temporal_overlap","name":"temporal_overlap","type":"INT","widget":{"name":"temporal_overlap"},"link":null}],"outputs":[{"localized_name":"TILING_INFO","name":"TILING_INFO","type":"TILING_INFO","links":[24]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"1a98f4bb37562ea56b04345f92b282dab301bfdf","Node name for S&R":"Sage_TilingInfo"},"widgets_values":[512,64,64,8]},{"id":19,"type":"PreviewImage","pos":[-364.0637512207031,366.8205261230469],"size":[640.96728515625,534.4432373046875],"flags":{},"order":23,"mode":4,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":16}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.30","Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":26,"type":"Sage_SetText","pos":[-1042.842529296875,742.5321655273438],"size":[359.6160888671875,208.86476135253906],"flags":{},"order":1,"mode":4,"inputs":[{"localized_name":"prefix","name":"prefix","shape":7,"type":"STRING","link":null},{"localized_name":"suffix","name":"suffix","shape":7,"type":"STRING","link":null},{"localized_name":"str","name":"str","type":"STRING","widget":{"name":"str"},"link":null}],"outputs":[{"localized_name":"str","name":"str","type":"STRING","links":[11]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"1a98f4bb37562ea56b04345f92b282dab301bfdf","Node name for S&R":"Sage_SetText"},"widgets_values":["signature, text"]},{"id":30,"type":"Sage_UNETLoader","pos":[-1297.701416015625,362.0794677734375],"size":[311.6608581542969,102],"flags":{},"order":2,"mode":4,"inputs":[{"localized_name":"unet_name","name":"unet_name","type":"COMBO","widget":{"name":"unet_name"},"link":null},{"localized_name":"weight_dtype","name":"weight_dtype","type":"COMBO","widget":{"name":"weight_dtype"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"MODEL","links":[30]},{"localized_name":"model_info","name":"model_info","type":"MODEL_INFO","links":[31]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_UNETLoader"},"widgets_values":["chroma/chroma-unlocked-v34-detail-calibrated_float8_e4m3fn_scaled_stochastic.safetensors","default"]},{"id":27,"type":"Sage_SamplerInfo","pos":[-739.228759765625,370.4992370605469],"size":[210,178],"flags":{},"order":3,"mode":4,"inputs":[{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null}],"outputs":[{"localized_name":"SAMPLER_INFO","name":"SAMPLER_INFO","type":"SAMPLER_INFO","links":[19,32]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"1a98f4bb37562ea56b04345f92b282dab301bfdf","Node name for S&R":"Sage_SamplerInfo"},"widgets_values":[0,"fixed",36,6,"euler","normal"]},{"id":22,"type":"Sage_KSamplerTiledDecoder","pos":[-658.3071899414062,659.7105712890625],"size":[274.02325439453125,466.02325439453125],"flags":{},"order":21,"mode":4,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":30},{"localized_name":"sampler_info","name":"sampler_info","type":"SAMPLER_INFO","link":19},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":20},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":21},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":22},{"localized_name":"vae","name":"vae","type":"VAE","link":23},{"localized_name":"tiling_info","name":"tiling_info","shape":7,"type":"TILING_INFO","link":24},{"localized_name":"advanced_info","name":"advanced_info","shape":7,"type":"ADV_SAMPLER_INFO","link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null},{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[16,36]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"1a98f4bb37562ea56b04345f92b282dab301bfdf","Node name for S&R":"Sage_KSamplerTiledDecoder"},"widgets_values":[1]},{"id":31,"type":"Sage_SaveImageWithMetadata","pos":[309.880859375,370.86395263671875],"size":[577.8004760742188,586.694091796875],"flags":{},"order":24,"mode":4,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":36},{"localized_name":"param_metadata","name":"param_metadata","shape":7,"type":"STRING","link":35},{"localized_name":"extra_metadata","name":"extra_metadata","shape":7,"type":"STRING","link":null},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"include_node_metadata","name":"include_node_metadata","type":"BOOLEAN","widget":{"name":"include_node_metadata"},"link":null},{"localized_name":"include_extra_pnginfo_metadata","name":"include_extra_pnginfo_metadata","type":"BOOLEAN","widget":{"name":"include_extra_pnginfo_metadata"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_SaveImageWithMetadata"},"widgets_values":["ComfyUI_Meta",true,true]},{"id":12,"type":"T5TokenizerOptions","pos":[-1293.467041015625,1034.1729736328125],"size":[210,82],"flags":{},"order":10,"mode":4,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":8},{"localized_name":"min_padding","name":"min_padding","type":"INT","widget":{"name":"min_padding"},"link":null},{"localized_name":"min_length","name":"min_length","type":"INT","widget":{"name":"min_length"},"link":null}],"outputs":[{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":[9]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.34","Node name for S&R":"T5TokenizerOptions"},"widgets_values":[1,0]},{"id":14,"type":"Sage_EmptyLatentImagePassthrough","pos":[-1286.6578369140625,802.5692749023438],"size":[210,170],"flags":{},"order":4,"mode":4,"inputs":[{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":null},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null},{"localized_name":"sd3","name":"sd3","type":"BOOLEAN","widget":{"name":"sd3"},"link":null}],"outputs":[{"localized_name":"latent","name":"latent","type":"LATENT","links":[22]},{"localized_name":"width","name":"width","type":"INT","links":[33]},{"localized_name":"height","name":"height","type":"INT","links":[34]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"ada34c266f8268cb86ac1f1c916c5c7ddf44d2b4","Node name for S&R":"Sage_EmptyLatentImagePassthrough"},"widgets_values":[1024,1024,1,true]},{"id":15,"type":"VAELoader","pos":[-1295.7725830078125,671.365966796875],"size":[246.71221923828125,61.361602783203125],"flags":{},"order":5,"mode":4,"inputs":[{"localized_name":"vae_name","name":"vae_name","type":"COMBO","widget":{"name":"vae_name"},"link":null}],"outputs":[{"localized_name":"VAE","name":"VAE","type":"VAE","links":[23]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.22","Node name for S&R":"VAELoader"},"widgets_values":["ae.safetensors"]},{"id":11,"type":"CLIPLoader","pos":[-1294.4630126953125,508.3432312011719],"size":[273.68035888671875,106],"flags":{},"order":6,"mode":4,"inputs":[{"localized_name":"clip_name","name":"clip_name","type":"COMBO","widget":{"name":"clip_name"},"link":null},{"localized_name":"type","name":"type","type":"COMBO","widget":{"name":"type"},"link":null},{"localized_name":"device","name":"device","shape":7,"type":"COMBO","widget":{"name":"device"},"link":null}],"outputs":[{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":[8]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.30","Node name for S&R":"CLIPLoader"},"widgets_values":["t5/t5xxl_fp8_e4m3fn_scaled.safetensors","chroma","default"]},{"id":29,"type":"Sage_ConstructMetadataLite","pos":[-337.2514343261719,952.8579711914062],"size":[210,170],"flags":{},"order":22,"mode":4,"inputs":[{"localized_name":"model_info","name":"model_info","type":"MODEL_INFO","link":31},{"localized_name":"sampler_info","name":"sampler_info","type":"SAMPLER_INFO","link":32},{"localized_name":"lora_stack","name":"lora_stack","shape":7,"type":"LORA_STACK","link":null},{"localized_name":"positive_string","name":"positive_string","type":"STRING","widget":{"name":"positive_string"},"link":39},{"localized_name":"negative_string","name":"negative_string","type":"STRING","widget":{"name":"negative_string"},"link":40},{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":33},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":34}],"outputs":[{"localized_name":"param_metadata","name":"param_metadata","type":"STRING","links":[35]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ConstructMetadataLite"},"widgets_values":["","",0,0]},{"id":13,"type":"Sage_DualCLIPTextEncode","pos":[-972.6900024414062,368.31414794921875],"size":[210,118],"flags":{},"order":18,"mode":4,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":9},{"localized_name":"pos","name":"pos","shape":7,"type":"STRING","link":38},{"localized_name":"neg","name":"neg","shape":7,"type":"STRING","link":11},{"localized_name":"clean","name":"clean","type":"BOOLEAN","widget":{"name":"clean"},"link":null}],"outputs":[{"localized_name":"positive","name":"positive","type":"CONDITIONING","links":[20]},{"localized_name":"negative","name":"negative","type":"CONDITIONING","links":[21]},{"localized_name":"pos_text","name":"pos_text","type":"STRING","links":[39]},{"localized_name":"neg_text","name":"neg_text","type":"STRING","links":[40]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"766095f60ef1d196b39cc26e0e4df139c9a481e3","Node name for S&R":"Sage_DualCLIPTextEncode"},"widgets_values":[false]},{"id":3,"type":"Sage_ConstructLLMPrompt","pos":[-2670.4599609375,457.3108215332031],"size":[272.8399963378906,406.3199768066406],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"prompt","name":"prompt","type":"COMBO","widget":{"name":"prompt"},"link":null},{"localized_name":"extra_instructions","name":"extra_instructions","type":"STRING","widget":{"name":"extra_instructions"},"link":null},{"localized_name":"avoid_euphemisms","name":"avoid_euphemisms","type":"BOOLEAN","widget":{"name":"avoid_euphemisms"},"link":null},{"localized_name":"be_specific","name":"be_specific","type":"BOOLEAN","widget":{"name":"be_specific"},"link":null},{"localized_name":"erotic_imagery","name":"erotic_imagery","type":"BOOLEAN","widget":{"name":"erotic_imagery"},"link":null},{"localized_name":"exaggerate_quality","name":"exaggerate_quality","type":"BOOLEAN","widget":{"name":"exaggerate_quality"},"link":null},{"localized_name":"exclude_sexual","name":"exclude_sexual","type":"BOOLEAN","widget":{"name":"exclude_sexual"},"link":null},{"localized_name":"exclude_watermarks","name":"exclude_watermarks","type":"BOOLEAN","widget":{"name":"exclude_watermarks"},"link":null},{"localized_name":"only_describe_key_elements","name":"only_describe_key_elements","type":"BOOLEAN","widget":{"name":"only_describe_key_elements"},"link":null},{"localized_name":"only_output_description","name":"only_output_description","type":"BOOLEAN","widget":{"name":"only_output_description"},"link":null},{"localized_name":"use_vulgar_language","name":"use_vulgar_language","type":"BOOLEAN","widget":{"name":"use_vulgar_language"},"link":null}],"outputs":[{"localized_name":"prompt","name":"prompt","type":"STRING","links":[1,42]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ConstructLLMPrompt"},"widgets_values":["text_enhancement/expand_tags","mouse girl, light blue fur, green eyes, furry, paws, safe, t-shirt, skirt bandanna,ribbon,hair clip, tripped, flower, wet, drenched in water, mud, balloon, night, stars, moon, woods",true,false,false,true,false,false,false,true,false]},{"id":35,"type":"Sage_ConstructLLMPromptExtra","pos":[-2944.8125,957.9679565429688],"size":[262.3260803222656,496],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"extra_instructions","name":"extra_instructions","type":"STRING","widget":{"name":"extra_instructions"},"link":null},{"localized_name":"describe_actions","name":"describe_actions","type":"BOOLEAN","widget":{"name":"describe_actions"},"link":null},{"localized_name":"describe_mood","name":"describe_mood","type":"BOOLEAN","widget":{"name":"describe_mood"},"link":null},{"localized_name":"describe_setting","name":"describe_setting","type":"BOOLEAN","widget":{"name":"describe_setting"},"link":null},{"localized_name":"include_anomalies","name":"include_anomalies","type":"BOOLEAN","widget":{"name":"include_anomalies"},"link":null},{"localized_name":"include_camera_details","name":"include_camera_details","type":"BOOLEAN","widget":{"name":"include_camera_details"},"link":null},{"localized_name":"include_context","name":"include_context","type":"BOOLEAN","widget":{"name":"include_context"},"link":null},{"localized_name":"include_cultural_references","name":"include_cultural_references","type":"BOOLEAN","widget":{"name":"include_cultural_references"},"link":null},{"localized_name":"include_emphasis_on_details","name":"include_emphasis_on_details","type":"BOOLEAN","widget":{"name":"include_emphasis_on_details"},"link":null},{"localized_name":"include_lighting","name":"include_lighting","type":"BOOLEAN","widget":{"name":"include_lighting"},"link":null},{"localized_name":"include_marks","name":"include_marks","type":"BOOLEAN","widget":{"name":"include_marks"},"link":null},{"localized_name":"include_materials","name":"include_materials","type":"BOOLEAN","widget":{"name":"include_materials"},"link":null},{"localized_name":"include_motion","name":"include_motion","type":"BOOLEAN","widget":{"name":"include_motion"},"link":null},{"localized_name":"include_reflections","name":"include_reflections","type":"BOOLEAN","widget":{"name":"include_reflections"},"link":null},{"localized_name":"include_scale","name":"include_scale","type":"BOOLEAN","widget":{"name":"include_scale"},"link":null},{"localized_name":"include_symmetry","name":"include_symmetry","type":"BOOLEAN","widget":{"name":"include_symmetry"},"link":null},{"localized_name":"include_time_of_day","name":"include_time_of_day","type":"BOOLEAN","widget":{"name":"include_time_of_day"},"link":null},{"localized_name":"include_weather","name":"include_weather","type":"BOOLEAN","widget":{"name":"include_weather"},"link":null}],"outputs":[{"localized_name":"extra","name":"extra","type":"STRING","links":[47]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"ceb1d21c0eab6c860a8fb1499d4c97d9ebd01442","Node name for S&R":"Sage_ConstructLLMPromptExtra"},"widgets_values":["",false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]},{"id":33,"type":"Sage_LMStudioLLMPromptText","pos":[-2367.957275390625,569.9818115234375],"size":[236.8670654296875,160],"flags":{},"order":12,"mode":4,"inputs":[{"localized_name":"prompt","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":42},{"localized_name":"model","name":"model","type":"COMBO","widget":{"name":"model"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"response","name":"response","type":"STRING","links":[41]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"ceb1d21c0eab6c860a8fb1499d4c97d9ebd01442","Node name for S&R":"Sage_LMStudioLLMPromptText"},"widgets_values":["","gemma-3-12b-it",0,"fixed"]},{"id":1,"type":"Sage_OllamaLLMPromptText","pos":[-2362.033447265625,186.74798583984375],"size":[210,160],"flags":{},"order":11,"mode":0,"inputs":[{"localized_name":"prompt","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":1},{"localized_name":"model","name":"model","type":"COMBO","widget":{"name":"model"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"response","name":"response","type":"STRING","links":[2]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_OllamaLLMPromptText"},"widgets_values":["","gemma3:12b-it-qat",0,"fixed"]},{"id":9,"type":"Sage_OllamaLLMPromptVision","pos":[-2367.958740234375,965.2886962890625],"size":[215.704345703125,160],"flags":{},"order":16,"mode":4,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":4},{"localized_name":"prompt","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":48},{"localized_name":"model","name":"model","type":"COMBO","widget":{"name":"model"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"response","name":"response","type":"STRING","links":[6]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_OllamaLLMPromptVision"},"widgets_values":["","gemma3:12b-it-qat",0,"fixed"]},{"id":34,"type":"Sage_ViewAnything","pos":[-2117.596435546875,573.65576171875],"size":[796.1481323242188,294.45062255859375],"flags":{},"order":15,"mode":4,"inputs":[{"localized_name":"any","name":"any","type":"*","link":41}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":[]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ViewAnything"},"widgets_values":[]},{"id":38,"type":"Sage_ViewAnything","pos":[-2120.525634765625,1355.4837646484375],"size":[800.5404663085938,295.9146423339844],"flags":{},"order":20,"mode":4,"inputs":[{"localized_name":"any","name":"any","type":"*","link":49}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ViewAnything"},"widgets_values":[]},{"id":4,"type":"Sage_ViewAnything","pos":[-2145.150390625,191.12667846679688],"size":[826.8941650390625,290.0582580566406],"flags":{},"order":14,"mode":0,"inputs":[{"localized_name":"any","name":"any","type":"*","link":2}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":[38]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ViewAnything"},"widgets_values":["This image is a beautifully composed nighttime scene, radiating a soft, dreamy quality. A charming mouse girl, her fur a lovely shade of light blue, is the central focus. Her eyes are a vibrant, expressive green. She's clearly furry, with well-defined paws and a playful, slightly startled expression. She's wearing a simple, comfortable t-shirt and a patterned skirt. A bright bandanna is tied around her head, adding a splash of color, and a delicate ribbon and hair clip are nestled in her soft fur. The scene depicts a moment of clumsy joy—she’s tripped, and the fall has left her drenched in water and covered in mud, but she looks utterly unconcerned. A single, perfect flower lies near her. A vibrant balloon floats nearby, a testament to the fun she was having. The background is a dense, atmospheric woods, illuminated by the gentle glow of a full moon and a scattering of twinkling stars. The lighting is phenomenal, casting subtle shadows and highlighting the wetness on her fur. It's a snapshot of pure, innocent fun, perfectly captured in a moment of unexpected chaos. The composition draws your eye directly to the mouse girl, emphasizing her adorable predicament. It's genuinely a gorgeous piece.\n"]},{"id":8,"type":"Sage_ViewAnything","pos":[-2138.68701171875,958.9227905273438],"size":[821.0377807617188,312.019775390625],"flags":{},"order":19,"mode":4,"inputs":[{"localized_name":"any","name":"any","type":"*","link":6}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ViewAnything"},"widgets_values":[]},{"id":6,"type":"Sage_LoadImage","pos":[-2713.483642578125,1546.535888671875],"size":[313.3018493652344,477.951171875],"flags":{},"order":9,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"image","name":"image","type":"IMAGE","links":[4,50]},{"localized_name":"mask","name":"mask","type":"MASK","links":null},{"localized_name":"width","name":"width","type":"INT","links":null},{"localized_name":"height","name":"height","type":"INT","links":null},{"localized_name":"metadata","name":"metadata","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_LoadImage"},"widgets_values":["LLM Example.png","image"]},{"id":5,"type":"Sage_ConstructLLMPrompt","pos":[-2668.38671875,960.1046142578125],"size":[271.3215026855469,370.4588928222656],"flags":{},"order":13,"mode":0,"inputs":[{"localized_name":"prompt","name":"prompt","type":"COMBO","widget":{"name":"prompt"},"link":null},{"localized_name":"extra_instructions","name":"extra_instructions","type":"STRING","widget":{"name":"extra_instructions"},"link":47},{"localized_name":"avoid_euphemisms","name":"avoid_euphemisms","type":"BOOLEAN","widget":{"name":"avoid_euphemisms"},"link":null},{"localized_name":"be_specific","name":"be_specific","type":"BOOLEAN","widget":{"name":"be_specific"},"link":null},{"localized_name":"erotic_imagery","name":"erotic_imagery","type":"BOOLEAN","widget":{"name":"erotic_imagery"},"link":null},{"localized_name":"exaggerate_quality","name":"exaggerate_quality","type":"BOOLEAN","widget":{"name":"exaggerate_quality"},"link":null},{"localized_name":"exclude_sexual","name":"exclude_sexual","type":"BOOLEAN","widget":{"name":"exclude_sexual"},"link":null},{"localized_name":"exclude_watermarks","name":"exclude_watermarks","type":"BOOLEAN","widget":{"name":"exclude_watermarks"},"link":null},{"localized_name":"only_describe_key_elements","name":"only_describe_key_elements","type":"BOOLEAN","widget":{"name":"only_describe_key_elements"},"link":null},{"localized_name":"only_output_description","name":"only_output_description","type":"BOOLEAN","widget":{"name":"only_output_description"},"link":null},{"localized_name":"use_vulgar_language","name":"use_vulgar_language","type":"BOOLEAN","widget":{"name":"use_vulgar_language"},"link":null}],"outputs":[{"localized_name":"prompt","name":"prompt","type":"STRING","links":[48,51]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"68d55f16ed2821f4b5248d05bef62048fcbfcdc3","Node name for S&R":"Sage_ConstructLLMPrompt"},"widgets_values":["description/descriptive","",true,false,false,true,false,false,false,true,false]},{"id":39,"type":"Sage_LMStudioLLMPromptVision","pos":[-2365.02978515625,1353.274658203125],"size":[234.44346618652344,160],"flags":{},"order":17,"mode":4,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":50},{"localized_name":"prompt","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":51},{"localized_name":"model","name":"model","type":"COMBO","widget":{"name":"model"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"response","name":"response","type":"STRING","links":[49]}],"properties":{"cnr_id":"comfyui_sageutils","ver":"ceb1d21c0eab6c860a8fb1499d4c97d9ebd01442","Node name for S&R":"Sage_LMStudioLLMPromptVision"},"widgets_values":["","gemma-3-12b-it",0,"fixed"]}],"links":[[1,3,0,1,0,"STRING"],[2,1,0,4,0,"*"],[4,6,0,9,0,"IMAGE"],[6,9,0,8,0,"*"],[8,11,0,12,0,"CLIP"],[9,12,0,13,0,"CLIP"],[11,26,0,13,2,"STRING"],[16,22,1,19,0,"IMAGE"],[19,27,0,22,1,"SAMPLER_INFO"],[20,13,0,22,2,"CONDITIONING"],[21,13,1,22,3,"CONDITIONING"],[22,14,0,22,4,"LATENT"],[23,15,0,22,5,"VAE"],[24,16,0,22,6,"TILING_INFO"],[30,30,0,22,0,"MODEL"],[31,30,1,29,0,"MODEL_INFO"],[32,27,0,29,1,"SAMPLER_INFO"],[33,14,1,29,5,"INT"],[34,14,2,29,6,"INT"],[35,29,0,31,1,"STRING"],[36,22,1,31,0,"IMAGE"],[38,4,0,13,1,"STRING"],[39,13,2,29,3,"STRING"],[40,13,3,29,4,"STRING"],[41,33,0,34,0,"*"],[42,3,0,33,0,"STRING"],[47,35,0,5,1,"STRING"],[48,5,0,9,1,"STRING"],[49,39,0,38,0,"*"],[50,6,0,39,0,"IMAGE"],[51,5,0,39,1,"STRING"]],"groups":[{"id":1,"title":"Text LLM (Ollama)","bounding":[-2372.033447265625,113.14799499511719,1056.4566650390625,375.1087341308594],"color":"#3f789e","font_size":24,"flags":{}},{"id":2,"title":"Image LLM (Ollama)","bounding":[-2377.958740234375,885.3228149414062,1065.9171142578125,388.2992858886719],"color":"#3f789e","font_size":24,"flags":{}},{"id":4,"title":"Image","bounding":[-1304.8204345703125,293.22052001953125,1591.723876953125,842.5133666992188],"color":"#3f789e","font_size":24,"flags":{}},{"id":5,"title":"Save","bounding":[297.4398193359375,296.00665283203125,600.9755859375,675.9281005859375],"color":"#3f789e","font_size":24,"flags":{}},{"id":6,"title":"Text LLM (LM Studio)","bounding":[-2377.957275390625,496.3818054199219,1066.509033203125,381.7245788574219],"color":"#3f789e","font_size":24,"flags":{}},{"id":7,"title":"Construct Prompt","bounding":[-2680.4599609375,383.7108459472656,292.840087890625,489.9199523925781],"color":"#3f789e","font_size":24,"flags":{}},{"id":8,"title":"Construct Prompt 2","bounding":[-2954.8125,884.3679809570312,567.747314453125,579.5999755859375],"color":"#3f789e","font_size":24,"flags":{}},{"id":9,"title":"Base Image","bounding":[-2723.483642578125,1472.9359130859375,333.3017578125,561.5511474609375],"color":"#3f789e","font_size":24,"flags":{}},{"id":10,"title":"Image LLM (LM Studio)","bounding":[-2375.02978515625,1279.6746826171875,1065.044677734375,381.7237854003906],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":0.6830134553650712,"offset":[3265.4511978661394,-90.50302388480233]}},"version":0.4} \ No newline at end of file diff --git a/nodes/llm.py b/nodes/llm.py index da8be13..e20d7ac 100644 --- a/nodes/llm.py +++ b/nodes/llm.py @@ -23,6 +23,112 @@ try: except ImportError: OLLAMA_AVAILABLE = False +# Nodes to construct prompts for LLMs, including extra instructions and advanced options. + +class Sage_ConstructLLMPrompt(ComfyNodeABC): + @classmethod + def INPUT_TYPES(cls) -> InputTypeDict: + print("Loading llm_prompts from config manager...") + inputs: InputTypeDict = {} + inputs["required"] = {} + + prompt_list = [] + + for key in llm_prompts["base"].keys(): + category = llm_prompts["base"][key]["category"] + prompt_list.append(f"{category}/{key}") + + inputs["required"] = { + "prompt": (prompt_list, {"defaultInput": True, "multiline": True}), + "extra_instructions": (IO.STRING, {"default": "", "multiline": True}) + } + + for key in llm_prompts["extra"].keys(): + if llm_prompts["extra"][key]["category"] in ("style", "quality", "content_focus"): + if llm_prompts["extra"][key]["type"] == "boolean": + default_value = False + if "default" in llm_prompts["extra"][key]: + default_value = llm_prompts["extra"][key]["default"] + inputs["required"][key] = (IO.BOOLEAN, {"default": default_value, "tooltip": llm_prompts["extra"][key]["name"]}) + return inputs + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("prompt",) + + FUNCTION = "construct_prompt" + + CATEGORY = "Sage Utils/LLM" + EXPERIMENTAL = True + DESCRIPTION = "Construct a prompt for an LLM based on the provided image and prompt." + + def construct_prompt(self, **args) -> tuple: + prompt = args["prompt"] + extra_instructions = args.get("extra_instructions", "") + + category = prompt.split("/")[0] + prompt = prompt.split("/")[1] + + prompt = llm_prompts["base"][prompt]["prompt"] + + # Ensure prompt ends with sentence-ending punctuation + if not prompt or prompt[-1] not in ".!?": + prompt = prompt + "." + + # Add a newline to the end of the prompt + if not prompt.endswith("\n"): + prompt = f"{prompt}\n\n" + + # Add extra instructions based on the selected options + for key, value in args.items(): + if key in llm_prompts["extra"]: + if llm_prompts["extra"][key]["type"] == "boolean" and value: + prompt += f"{llm_prompts['extra'][key]['prompt']}\n\n" + + # Add a space if extra_instructions is not empty + if extra_instructions.strip(): + prompt = prompt + extra_instructions.strip() + + if not prompt: + raise ValueError("Prompt cannot be empty.") + return (prompt,) + +class Sage_ConstructLLMPromptExtra(ComfyNodeABC): + @classmethod + def INPUT_TYPES(s) -> InputTypeDict: + inputs: InputTypeDict = {} + inputs["required"] = { "extra_instructions": (IO.STRING, {"default": "", "defaultInput": True, "multiline": True}) } + for key in llm_prompts["extra"].keys(): + if llm_prompts["extra"][key]["category"] not in ("style", "quality", "content_focus"): + if llm_prompts["extra"][key]["type"] == "boolean": + default_value = False + if "default" in llm_prompts["extra"][key]: + default_value = llm_prompts["extra"][key]["default"] + inputs["required"][key] = (IO.BOOLEAN, {"default": default_value, "tooltip": llm_prompts["extra"][key]["name"]}) + return inputs + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("extra",) + + FUNCTION = "construct_extra" + CATEGORY = "Sage Utils/LLM" + EXPERIMENTAL = True + DESCRIPTION = "Construct extra instructions for an LLM based on the provided options." + def construct_extra(self, **args) -> tuple: + extra_instructions = args["extra_instructions"] + "\n\n" if args["extra_instructions"] else "" + + for key, value in args.items(): + if key in llm_prompts["extra"]: + if llm_prompts["extra"][key]["type"] == "boolean" and value: + extra_instructions += f"{llm_prompts['extra'][key]['prompt']}\n\n" + + # Remove the last newline if it exists + if extra_instructions.endswith("\n\n"): + extra_instructions = extra_instructions[:-2] + + return (extra_instructions.strip(),) + +# Ollama based nodes for LLMs. +# These nodes allow you to send prompts to LLMs and get responses. class Sage_OllamaAdvancedOptions(ComfyNodeABC): @classmethod def INPUT_TYPES(cls) -> InputTypeDict: @@ -153,112 +259,49 @@ class Sage_OllamaLLMPromptVision(ComfyNodeABC): options["seed"] = seed # Ensure the seed is included in the options response = llm.ollama_generate_vision(model=model, prompt=prompt, images=image, options=options) return (response,) - -class Sage_ConstructLLMPrompt(ComfyNodeABC): - @classmethod - def INPUT_TYPES(cls) -> InputTypeDict: - print("Loading llm_prompts from config manager...") - inputs: InputTypeDict = {} - inputs["required"] = {} + +# Nodes for LM Studio. + +# class Sage_LMStudioLLMPrompt(ComfyNodeABC): +# @classmethod +# def INPUT_TYPES(cls) -> InputTypeDict: +# models = llm.get_lmstudio_models() +# if not models: +# models = [] +# models = sorted(models) + +# return { +# "required": { +# "prompt": (IO.STRING, {"defaultInput": True, "multiline": True}), +# "model": (models, ) +# }, +# "optional": { +# "image": (IO.IMAGE, {"defaultInput": True}) +# } +# } + +# RETURN_TYPES = (IO.STRING,) +# RETURN_NAMES = ("response",) + +# FUNCTION = "get_response" + +# CATEGORY = "Sage Utils/LLM" +# EXPERIMENTAL = True +# DESCRIPTION = "Send a prompt to a language model and get a response. Optionally, you can provide an image/s to the model if it supports multimodal input. The model must be installed via Ollama." + +# def get_response(self, prompt: str, model: str, image = None) -> tuple: + +# if not llm.LMSTUDIO_AVAILABLE: +# raise ImportError("LM Studio is not available. Please install it to use this node.") - prompt_list = [] +# if model not in llm.get_lmstudio_models(): +# raise ValueError(f"Model '{model}' is not available. Available models: {llm.get_lmstudio_models()}") - for key in llm_prompts["base"].keys(): - category = llm_prompts["base"][key]["category"] - prompt_list.append(f"{category}/{key}") - - inputs["required"] = { - "prompt": (prompt_list, {"defaultInput": True, "multiline": True}), - "extra_instructions": (IO.STRING, {"default": "", "multiline": True}) - } +# response = llm.lmstudio_generate(model=model, prompt=prompt, images=image) +# return (response,) - for key in llm_prompts["extra"].keys(): - if llm_prompts["extra"][key]["category"] in ("style", "quality", "content_focus"): - if llm_prompts["extra"][key]["type"] == "boolean": - default_value = False - if "default" in llm_prompts["extra"][key]: - default_value = llm_prompts["extra"][key]["default"] - inputs["required"][key] = (IO.BOOLEAN, {"default": default_value, "tooltip": llm_prompts["extra"][key]["name"]}) - return inputs - RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("prompt",) - - FUNCTION = "construct_prompt" - - CATEGORY = "Sage Utils/LLM" - EXPERIMENTAL = True - DESCRIPTION = "Construct a prompt for an LLM based on the provided image and prompt." - - def construct_prompt(self, **args) -> tuple: - prompt = args["prompt"] - extra_instructions = args.get("extra_instructions", "") - - category = prompt.split("/")[0] - prompt = prompt.split("/")[1] - - prompt = llm_prompts["base"][prompt]["prompt"] - - # Ensure prompt ends with sentence-ending punctuation - if not prompt or prompt[-1] not in ".!?": - prompt = prompt + "." - - # Add a newline to the end of the prompt - if not prompt.endswith("\n"): - prompt = f"{prompt}\n\n" - - # Add extra instructions based on the selected options - for key, value in args.items(): - if key in llm_prompts["extra"]: - if llm_prompts["extra"][key]["type"] == "boolean" and value: - prompt += f"{llm_prompts['extra'][key]['prompt']}\n\n" - - # Add a space if extra_instructions is not empty - if extra_instructions.strip(): - prompt = prompt + extra_instructions.strip() - - if not prompt: - raise ValueError("Prompt cannot be empty.") - return (prompt,) - -class Sage_ConstructLLMPromptExtra(ComfyNodeABC): - @classmethod - def INPUT_TYPES(s) -> InputTypeDict: - inputs: InputTypeDict = {} - inputs["required"] = { "extra_instructions": (IO.STRING, {"default": "", "defaultInput": True, "multiline": True}) } - for key in llm_prompts["extra"].keys(): - if llm_prompts["extra"][key]["category"] not in ("style", "quality", "content_focus"): - if llm_prompts["extra"][key]["type"] == "boolean": - default_value = False - if "default" in llm_prompts["extra"][key]: - default_value = llm_prompts["extra"][key]["default"] - inputs["required"][key] = (IO.BOOLEAN, {"default": default_value, "tooltip": llm_prompts["extra"][key]["name"]}) - return inputs - - RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("extra",) - - FUNCTION = "construct_extra" - CATEGORY = "Sage Utils/LLM" - EXPERIMENTAL = True - DESCRIPTION = "Construct extra instructions for an LLM based on the provided options." - def construct_extra(self, **args) -> tuple: - extra_instructions = args["extra_instructions"] + "\n\n" if args["extra_instructions"] else "" - - for key, value in args.items(): - if key in llm_prompts["extra"]: - if llm_prompts["extra"][key]["type"] == "boolean" and value: - extra_instructions += f"{llm_prompts['extra'][key]['prompt']}\n\n" - - # Remove the last newline if it exists - if extra_instructions.endswith("\n\n"): - extra_instructions = extra_instructions[:-2] - - return (extra_instructions.strip(),) - -# Don't use anything below this line. - -class Sage_LMStudioLLMPrompt(ComfyNodeABC): +class Sage_LMStudioLLMPromptText(ComfyNodeABC): @classmethod def INPUT_TYPES(cls) -> InputTypeDict: models = llm.get_lmstudio_models() @@ -269,10 +312,45 @@ class Sage_LMStudioLLMPrompt(ComfyNodeABC): return { "required": { "prompt": (IO.STRING, {"defaultInput": True, "multiline": True}), - "model": (models, ) - }, - "optional": { - "image": (IO.IMAGE, {"defaultInput": True}) + "model": (models, ), + "seed": (IO.INT, {"default": 0, "min": 0, "max": 2**32 - 1, "step": 1, "tooltip": "Seed for random number generation."}) + } + } + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("response",) + + FUNCTION = "get_response" + + CATEGORY = "Sage Utils/LLM" + EXPERIMENTAL = True + DESCRIPTION = "Send a prompt to a language model and get a response. The model must be installed via Ollama." + + def get_response(self, prompt: str, model: str, seed: int = 0, options = {}) -> tuple: + options = {} + if not llm.OLLAMA_AVAILABLE: + raise ImportError("Ollama is not available. Please install it to use this node.") + + if model not in llm.get_lmstudio_models(): + raise ValueError(f"Model '{model}' is not available. Available models: {llm.get_ollama_models()}") + + options["seed"] = seed # Ensure the seed is included in the options + response = llm.lmstudio_generate(model=model, prompt=prompt, options=options) + return (response,) +class Sage_LMStudioLLMPromptVision(ComfyNodeABC): + @classmethod + def INPUT_TYPES(cls) -> InputTypeDict: + models = llm.get_lmstudio_models() + if not models: + models = [] + models = sorted(models) + + return { + "required": { + "prompt": (IO.STRING, {"defaultInput": True, "multiline": True}), + "model": (models, ), + "image": (IO.IMAGE, {"defaultInput": True}), + "seed": (IO.INT, {"default": 0, "min": 0, "max": 2**32 - 1, "step": 1, "tooltip": "Seed for random number generation."}) } } @@ -284,14 +362,18 @@ class Sage_LMStudioLLMPrompt(ComfyNodeABC): CATEGORY = "Sage Utils/LLM" EXPERIMENTAL = True DESCRIPTION = "Send a prompt to a language model and get a response. Optionally, you can provide an image/s to the model if it supports multimodal input. The model must be installed via Ollama." - - def get_response(self, prompt: str, model: str, image = None) -> tuple: - - if not llm.LMSTUDIO_AVAILABLE: - raise ImportError("LM Studio is not available. Please install it to use this node.") + + def get_response(self, prompt: str, model: str, image, seed: int, ) -> tuple: + options = {} + if not llm.OLLAMA_AVAILABLE: + raise ImportError("Ollama is not available. Please install it to use this node.") if model not in llm.get_lmstudio_models(): - raise ValueError(f"Model '{model}' is not available. Available models: {llm.get_lmstudio_models()}") + raise ValueError(f"Model '{model}' is not available or not a vision model. Available models: {llm.get_ollama_vision_models()}") - response = llm.lmstudio_generate(model=model, prompt=prompt, images=image) + if image is None: + raise ValueError("Image input is required for vision models.") + + options["seed"] = seed # Ensure the seed is included in the options + response = llm.lmstudio_generate_vision(model=model, prompt=prompt, images=image, options=options) return (response,) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index dcd20bc..5341825 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,6 @@ # will not show nodes that require them if they are not installed. # If you want to use LLM nodes, you must install one of these, depending on your preference. -# LLM support is a work in progress, most nodes are not implemented yet, and ones that are are likely to be majorly revised. +# LLM support is a work in progress, and subject to revision. ollama>=0.5.1 -#lmstudio +lmstudio diff --git a/utils/helpers.py b/utils/helpers.py index 7b959a4..461b4e6 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -413,11 +413,6 @@ def tensor_to_base64(tensor): base64_images.append(base64.b64encode(buffered.getvalue()).decode('utf-8')) return base64_images - - # self.output_dir = folder_paths.get_temp_directory() - # self.type = "temp" - # self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5)) - # self.compress_level = 1 def tensor_to_temp_image(tensor, filename=None): if tensor is None or not isinstance(tensor, torch.Tensor): @@ -447,4 +442,6 @@ def tensor_to_temp_image(tensor, filename=None): filenames.append(str(filename)) img.save(filename, format="PNG") + print(f"Saved {len(filenames)} images to {output_dir}") + print(filenames) return filenames \ No newline at end of file diff --git a/utils/llm_wrapper.py b/utils/llm_wrapper.py index f1a3534..5d1b4ed 100644 --- a/utils/llm_wrapper.py +++ b/utils/llm_wrapper.py @@ -20,6 +20,26 @@ except ImportError: LMSTUDIO_AVAILABLE = False print("LM Studio library not found.") +# Neutral functions +def clean_response(response: str) -> str: + """Clean the response from the model by removing unnecessary tags.""" + if not response: + return "" + + response = response.strip() + + # Remove "" if it exists in the response + if response.endswith(""): + response = response[:-len("")].strip() + + # Remove ">end_of_turn>" if it exists in the response + if response.endswith(">end_of_turn>"): + response = response[:-len(">end_of_turn>")].strip() + + return response + + +# Ollama functions def get_ollama_vision_models() -> list[str]: """Retrieve a list of available vision models from Ollama.""" if not OLLAMA_AVAILABLE: @@ -100,16 +120,8 @@ def ollama_generate_vision(model, prompt, images = None, options = None) -> str: ) if not response or 'response' not in response: raise ValueError("No valid response received from the model.") - response = response['response'].strip() - # Remove "" if it exists in the response - if response.endswith(""): - response = response[:-len("")].strip() - # Remove ">end_of_turn>" if it exists in the response - if response.endswith(">end_of_turn>"): - response = response[:-len(">end_of_turn>")].strip() - - return response + return clean_response(response['response']) except Exception as e: print(f"Error generating response from Ollama vision model: {e}") @@ -131,33 +143,28 @@ def ollama_generate(model, prompt, options = None) -> str: model=model, prompt=prompt, stream=False, - options=options + options=options, + keep_alive=False ) else: # If no options are provided, use the default generate function response = ollama.generate( model=model, prompt=prompt, - stream=False + stream=False, + keep_alive=False ) if not response or 'response' not in response: raise ValueError("No valid response received from the model.") - response = response['response'].strip() - # Remove "" if it exists in the response - if response.endswith(""): - response = response[:-len("")].strip() - # Remove ">end_of_turn>" if it exists in the response - if response.endswith(">end_of_turn>"): - response = response[:-len(">end_of_turn>")].strip() - - return response + return clean_response(response['response']) except Exception as e: print(f"Error generating response from Ollama: {e}") return "" -# Don't use anything below this point. +# LM Studio functions +# These functions will be similar to the Ollama functions but will use the LM Studio API. def get_lmstudio_models() -> list[str]: """Retrieve a list of available models from LM Studio.""" if not LMSTUDIO_AVAILABLE: @@ -165,21 +172,48 @@ def get_lmstudio_models() -> list[str]: try: response = lms.list_downloaded_models("llm") - models = [model.model_key for model in response] + models = [] + for model in response: + if hasattr(model, 'model_key'): + models.append(model.model_key) return models except Exception as e: print(f"Error retrieving models from LM Studio: {e}") return [] -def lmstudio_generate(model, prompt, images = None) -> str: - """Generate a response from an LM Studio model.""" +def get_lmstudio_vision_models() -> list[str]: + """Retrieve a list of available models from LM Studio.""" + if not LMSTUDIO_AVAILABLE: + return [] + + try: + response = lms.list_downloaded_models("llm") + + models=[] + + for model in response: + if hasattr(model, 'model_key'): + name = model.model_key + if hasattr(model, 'info') and hasattr(model.info, 'vision'): + models.append(name) + + return models + + except Exception as e: + print(f"Error retrieving models from LM Studio: {e}") + return [] + +def lmstudio_generate_vision(model, prompt, images = None, options = {}) -> str: + """Generate a response from an LM Studio vision model.""" if not LMSTUDIO_AVAILABLE: raise ImportError("LM Studio is not available. Please install it to use this function.") + + model_list = get_lmstudio_vision_models() - if model not in get_lmstudio_models(): - raise ValueError(f"Model '{model}' is not available. Available models: {get_lmstudio_models()}") - + if model not in model_list: + raise ValueError(f"Model '{model}' is not available. Available models: {model_list}") + seed = options.get('seed', 0) input_images = [] if images is not None: input_images = tensor_to_temp_image(images) @@ -196,23 +230,46 @@ def lmstudio_generate(model, prompt, images = None) -> str: for image in input_images: image_handles.append(lms.prepare_image(image)) chat.add_user_message(prompt, images=image_handles) - response = lms_model.respond(chat) + response = lms_model.respond(chat, config={"seed":seed}) lms_model.unload() - if not response: raise ValueError("No valid response received from the model.") - response = response['messages'][-1] - response = response.strip() - # Remove "" if it exists in the response - if response.endswith(""): - response = response[:-len("")].strip() - # Remove ">end_of_turn>" if it exists in the response - if response.endswith(">end_of_turn>"): - response = response[:-len(">end_of_turn>")].strip() - - return response + return clean_response(response.content) + + except Exception as e: + print(f"Error generating response from LM Studio vision model: {e}") + if lms_model: + lms_model.unload() + return "" + +def lmstudio_generate(model, prompt, options = {}) -> str: + """Generate a response from an LM Studio model.""" + if not LMSTUDIO_AVAILABLE: + raise ImportError("LM Studio is not available. Please install it to use this function.") + + model_list = get_lmstudio_models() + if model not in model_list: + raise ValueError(f"Model '{model}' is not available. Available models: {model_list}") + + seed = options.get('seed', 0) + lms_model = None + + try: + lms_model = lms.llm(model) + if lms_model is None: + raise ValueError(f"Model '{model}' could not be loaded from LM Studio.") + + chat = lms.Chat() + chat.add_user_message(prompt) + response = lms_model.respond(chat, config={"seed":seed}) + lms_model.unload() + + if not response: + raise ValueError("No valid response received from the model.") + + return clean_response(response.content) except Exception as e: print(f"Error generating response from LM Studio: {e}")