Skip to content

Commit e77622b

Browse files
committed
Release v1.0.10: add Qwen3.5 GGUF support, fix Qwen handler routing, and improve post-run cleanup across nodes
1 parent 0be3f51 commit e77622b

8 files changed

Lines changed: 418 additions & 322 deletions

File tree

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
All notable changes to ComfyUI-MultiModal-Prompt-Nodes will be documented in this file.
44

55

6+
## [1.0.10] - 2026-04-02
7+
8+
- Added support for Qwen3.5 local GGUF models
9+
- Added Qwen3.5 model detection and proper handler selection (`Qwen35ChatHandler`)
10+
- Fixed incorrect fallback to `Qwen3VLChatHandler` for Qwen3.5 model names
11+
- Updated mmproj handling for Qwen3.5 (requirement checks and auto-detection flow)
12+
13+
- Improved post-run cleanup behavior for local model nodes
14+
- `VisionLLMNode`, `WanVideoPromptGenerator`, and `QwenImageEditPromptGenerator` now call `cleanup()` at the end of execution
15+
- Introduced `cleanup(finalize=False/True)` to separate regular unload from final teardown on process exit
16+
- Added safe manager re-initialization after cleanup for stable repeated runs
17+
18+
619
## [1.0.9] - 2026-03-15
720

821
- Expanded the search scope for local Qwen-family GGUF models

README.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# ComfyUI-MultiModal-Prompt-Nodes
22

3-
**Version:** 1.0.9
3+
**Version:** 1.0.10
44
**License:** GPL-3.0
55

66
Multimodal prompt generator nodes for ComfyUI, designed to generate prompts for **Qwen-Image-Edit** and **Wan2.2**.
7-
Supports **local LLM / local GGUF models** (Qwen2.5-VL, Qwen3-VL) and **Qwen API** for image and video prompt generation and enhancement.
7+
Supports **local LLM / local GGUF models** (Qwen2.5-VL, Qwen3-VL and Qwen3.5) and **Qwen API** for image and video prompt generation and enhancement.
88

99
---
1010
## Upgrade Notes for Existing Users
1111

12-
The following notes are intended for existing users upgrading to `1.0.9`.
12+
The following notes are intended for existing users upgrading to `1.0.10`.
1313

1414
### Expanded search paths for local Qwen-family GGUF models
1515
In addition to `models/LLM`, this release now searches `models/text_encoders` and its subdirectories for GGUF files. Because this changes how model paths are handled internally, you may need to reselect your models the first time you run the node after updating.
@@ -48,7 +48,7 @@ Based on extensive testing, **Wan2.2** and **Qwen-Image-Edit** respond **signifi
4848
- `concise`: Minimal keywords, focused on core elements
4949
- `creative`: Artistic interpretation with unique perspectives
5050
- **Multi-image input**: Support batch image input via ComfyUI's batch nodes (e.g., Images Batch Multiple)
51-
- **Local GGUF support**: Run Qwen2.5-VL and Qwen3-VL models locally
51+
- **Local GGUF support**: Run Qwen2.5-VL, Qwen3-VL, and Qwen3.5 models locally
5252
- **Auto-detect mmproj**: Automatic detection or manual selection
5353

5454
### 2. Qwen Image Edit Prompt Generator
@@ -96,14 +96,15 @@ pip install dashscope pillow numpy
9696

9797
**Important:** Model compatibility varies by llama-cpp-python version. Based on my testing environment:
9898

99-
| Version | Qwen2.5-VL | Qwen3-VL |
100-
|---------|------------|----------|
101-
| 0.3.16 (official) |||
102-
| 0.3.21+ (JamePeng fork) |||
99+
| Version | Qwen2.5-VL | Qwen3-VL | Qwen3.5 |
100+
|---------|------------|----------|---------|
101+
| 0.3.16 (official) ||||
102+
| 0.3.21+ (JamePeng fork) ||||
103+
| 0.3.33+ (JamePeng fork) ||||
103104

104105
***Note:** Vision input support may vary depending on your environment and configuration.
105106

106-
**Recommended Installation (JamePeng fork for Qwen3-VL support):**
107+
**Recommended Installation (JamePeng fork for Qwen3-VL and Qwen3.5 support):**
107108
Please follow the build and installation instructions provided in the JamePeng fork repository, as this fork requires a custom build and cannot be reliably installed via a simple `pip install`.
108109

109110
**Source:** https://github.com/JamePeng/llama-cpp-python
@@ -168,7 +169,7 @@ Add your Alibaba Cloud Dashscope API key to this file.
168169
### Qwen Image Edit Prompt Generator
169170

170171
**Inputs:**
171-
- `image`: Primary input image (required)
172+
- `image`: Primary input image (optional)
172173
- `prompt`: Edit instruction or image description
173174
- `prompt_style`:
174175
- `Qwen-Image-Edit`: For image editing tasks
@@ -245,14 +246,18 @@ Add your Alibaba Cloud Dashscope API key to this file.
245246
## Model Compatibility
246247

247248
### Qwen2.5-VL (Separate mmproj)
248-
- ✅ Qwen2.5-VL(3B/7B): Full vision support
249+
- ✅ Qwen2.5-VL(3B/7B/32B): Full vision support
249250
- ✅ Requires matching mmproj file
250251
- ~~❌ Insufficient adherence to user prompts under the existing system prompt configuration with **Qwen-Image-Edit**~~
251252

252253
### Qwen3-VL (Separate mmproj)
253254
- ✅ Qwen3-VL(4B/8B): Full vision support with JamePeng fork
254255
- ✅ Requires matching mmproj file
255256

257+
### Qwen3.5 (Separate mmproj)
258+
- ✅ Qwen3.5(9B/27B/35B-A3B): Full vision support with JamePeng fork
259+
- ✅ Requires matching mmproj file
260+
256261
### Model Sources
257262
- Qwen models: https://huggingface.co/Qwen
258263
- GGUF conversions: https://huggingface.co/models?search=qwen+gguf
@@ -447,9 +452,6 @@ Areas needing help:
447452

448453
See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
449454

450-
### Current Version: 1.0.9
451-
- Expanded the search scope for local Qwen-family GGUF models
452-
- Improved mmproj selection behavior
453-
- Strengthened the local prompt rewrite flow for Qwen and Wan
454-
- Expanded Qwen Image Edit Prompt Generator
455-
- Improved the robustness of Wan Video Prompt Generator
455+
### Current Version: 1.0.10
456+
- Added support for Qwen3.5 local GGUF models
457+
- Improved post-run cleanup behavior for local model nodes

__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# the Free Software Foundation, either version 3 of the License, or
77
# (at your option) any later version.
88

9-
__version__ = "1.0.8"
9+
__version__ = "1.0.10"
1010
WEB_DIRECTORY = "./web"
1111

1212
from .qwen_nodes import NODE_CLASS_MAPPINGS as qNODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as qNODE_DISPLAY_NAME_MAPPINGS

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "multimodal-prompt-nodes"
33
description = "Multimodal prompt generator nodes for ComfyUI, designed to generate prompts for QwenImageEdit and Wan2.2. Supports local LLM / local GGUF models (Qwen2.5-VL, Qwen3-VL) and Qwen API for image and video prompt generation and enhancement."
4-
version = "1.0.9"
4+
version = "1.0.10"
55
license = {file = "LICENSE"}
66
# classifiers = [
77
# # For OS-independent nodes (works on all operating systems)

qwen_nodes.py

Lines changed: 103 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -536,115 +536,122 @@ def INPUT_TYPES(s):
536536
DESCRIPTION = "Enhance your prompts using the Qwen LLM to align the behavior and capabilities of the Qwen-Image/Edit online version."
537537

538538
def rewrit(self, prompt, prompt_style, target_language, llm_model, mmproj, max_retries, device, save_tokens, image=None, image2=None, image3=None):
539-
# Collect all images
540-
all_images = []
541-
if image is not None:
542-
all_images.extend(tensor2pil(image))
543-
if image2 is not None:
544-
all_images.extend(tensor2pil(image2))
545-
if image3 is not None:
546-
all_images.extend(tensor2pil(image3))
547-
548-
# Local model processing
549-
if llm_model.startswith("Local: "):
550-
try:
551-
552-
model_filename = llm_model.replace("Local: ", "")
553-
554-
# mmproj check
555-
# vision_llm_node rewrite_prompt_with_gguf import
556-
import sys
557-
current_dir = os.path.dirname(os.path.abspath(__file__))
558-
if current_dir not in sys.path:
559-
sys.path.insert(0, current_dir)
560-
# Centralized import path handling
561-
from import_utils import ensure_local_import
562-
ensure_local_import(__file__)
563-
from vision_llm_node import rewrite_prompt_with_gguf, resolve_local_gguf_path, resolve_mmproj_path_for_model
564-
565-
# Model path retrieval
566-
model_path = resolve_local_gguf_path(model_filename)
567-
568-
# mmproj processing (same logic as Vision LLM Node)
569-
if mmproj is None:
570-
raise RuntimeError("mmproj not specified. Please select an mmproj file in the optional inputs for Local models.")
539+
try:
540+
# Collect all images
541+
all_images = []
542+
if image is not None:
543+
all_images.extend(tensor2pil(image))
544+
if image2 is not None:
545+
all_images.extend(tensor2pil(image2))
546+
if image3 is not None:
547+
all_images.extend(tensor2pil(image3))
548+
549+
# Local model processing
550+
if llm_model.startswith("Local: "):
551+
try:
571552

572-
if prompt_style == "Qwen-Image" and len(all_images) == 0:
573-
mmproj_selection = "(Not required)"
574-
else:
575-
mmproj_selection = mmproj
553+
model_filename = llm_model.replace("Local: ", "")
554+
555+
# mmproj check
556+
# vision_llm_node rewrite_prompt_with_gguf import
557+
import sys
558+
current_dir = os.path.dirname(os.path.abspath(__file__))
559+
if current_dir not in sys.path:
560+
sys.path.insert(0, current_dir)
561+
# Centralized import path handling
562+
from import_utils import ensure_local_import
563+
ensure_local_import(__file__)
564+
from vision_llm_node import rewrite_prompt_with_gguf, resolve_local_gguf_path, resolve_mmproj_path_for_model
565+
566+
# Model path retrieval
567+
model_path = resolve_local_gguf_path(model_filename)
568+
569+
# mmproj processing (same logic as Vision LLM Node)
570+
if mmproj is None:
571+
raise RuntimeError("mmproj not specified. Please select an mmproj file in the optional inputs for Local models.")
576572

577-
mmproj_path = resolve_mmproj_path_for_model(model_path, mmproj_selection)
578-
579-
print(f'[Qwen Prompt Rewriter] Using Local model')
580-
print(f'[Qwen Prompt Rewriter] Model: {model_filename}')
581-
print(f'[Qwen Prompt Rewriter] mmproj: {mmproj_selection}')
582-
print(f'[Qwen Prompt Rewriter] Using {len(all_images)} image(s)')
583-
584-
# Convert device selection to n_gpu_layers
585-
n_gpu_layers = -1 if device == "GPU" else 0
586-
587-
output_prompt = rewrite_prompt_with_gguf(
588-
prompt=prompt,
589-
model_path=model_path,
590-
mmproj_path=mmproj_path,
591-
style="qwen_image" if prompt_style == "Qwen-Image" else "qwen_image_edit",
592-
target_language=target_language,
593-
images=all_images,
594-
max_tokens=2048,
595-
temperature=0.7,
596-
n_ctx=4096,
597-
n_gpu_layers=n_gpu_layers,
598-
)
573+
if prompt_style == "Qwen-Image" and len(all_images) == 0:
574+
mmproj_selection = "(Not required)"
575+
else:
576+
mmproj_selection = mmproj
599577

600-
if target_language == "zh" and not is_acceptable_zh_output(output_prompt):
601-
print('[Qwen Prompt Rewriter] Output language mismatch (expected simplified Chinese), converting output to simplified Chinese in a second pass')
602-
protected_text, placeholders = protect_quoted_text(output_prompt, "QTXT")
578+
mmproj_path = resolve_mmproj_path_for_model(model_path, mmproj_selection)
579+
580+
print(f'[Qwen Prompt Rewriter] Using Local model')
581+
print(f'[Qwen Prompt Rewriter] Model: {model_filename}')
582+
print(f'[Qwen Prompt Rewriter] mmproj: {mmproj_selection}')
583+
print(f'[Qwen Prompt Rewriter] Using {len(all_images)} image(s)')
584+
585+
# Convert device selection to n_gpu_layers
586+
n_gpu_layers = -1 if device == "GPU" else 0
587+
603588
output_prompt = rewrite_prompt_with_gguf(
604-
prompt=build_force_translate_to_zh_prompt(protected_text, prompt_style),
589+
prompt=prompt,
605590
model_path=model_path,
606-
mmproj_path="(Not required)",
607-
style="zh_normalize",
608-
target_language="zh",
609-
images=None,
591+
mmproj_path=mmproj_path,
592+
style="qwen_image" if prompt_style == "Qwen-Image" else "qwen_image_edit",
593+
target_language=target_language,
594+
images=all_images,
610595
max_tokens=2048,
611-
temperature=0.2,
596+
temperature=0.7,
612597
n_ctx=4096,
613598
n_gpu_layers=n_gpu_layers,
614599
)
615-
output_prompt = restore_quoted_text(output_prompt, placeholders)
616-
617-
except Exception as e:
618-
raise RuntimeError(f"Local model error: {str(e)}")
619-
620-
# API processing (cloud models)
621-
else:
622-
# Load API key from api_key.txt
623-
if not os.path.exists(key_path):
624-
raise EnvironmentError(f"API key file not found: {key_path}\nPlease create this file with your Aliyun API key for cloud model usage.")
625-
626-
with open(key_path, "r", encoding="utf-8") as f:
627-
_api_key = f.read().strip()
600+
601+
if target_language == "zh" and not is_acceptable_zh_output(output_prompt):
602+
print('[Qwen Prompt Rewriter] Output language mismatch (expected simplified Chinese), converting output to simplified Chinese in a second pass')
603+
protected_text, placeholders = protect_quoted_text(output_prompt, "QTXT")
604+
output_prompt = rewrite_prompt_with_gguf(
605+
prompt=build_force_translate_to_zh_prompt(protected_text, prompt_style),
606+
model_path=model_path,
607+
mmproj_path="(Not required)",
608+
style="zh_normalize",
609+
target_language="zh",
610+
images=None,
611+
max_tokens=2048,
612+
temperature=0.2,
613+
n_ctx=4096,
614+
n_gpu_layers=n_gpu_layers,
615+
)
616+
output_prompt = restore_quoted_text(output_prompt, placeholders)
628617

629-
if not _api_key:
630-
raise EnvironmentError(f'API_KEY is not set in "{key_path}"\nPlease add your Aliyun API key to this file for cloud model usage.')
618+
except Exception as e:
619+
raise RuntimeError(f"Local model error: {str(e)}")
631620

632-
if prompt_style == "Qwen-Image":
633-
output_prompt = polish_prompt(_api_key, prompt, model=llm_model, max_retries=max_retries, target_language=target_language)
621+
# API processing (cloud models)
634622
else:
635-
# Qwen-Image-Edit requires at least one image
636-
if len(all_images) == 0:
637-
raise ValueError("Qwen-Image-Edit style requires at least one image input!")
623+
# Load API key from api_key.txt
624+
if not os.path.exists(key_path):
625+
raise EnvironmentError(f"API key file not found: {key_path}\nPlease create this file with your Aliyun API key for cloud model usage.")
638626

639-
print(f'[Qwen Prompt Rewriter] Using {len(all_images)} image(s) for Image-Edit')
640-
output_prompt = polish_prompt_edit(_api_key, prompt, all_images, model=llm_model, max_retries=max_retries, save_tokens=save_tokens, target_language=target_language)
641-
642-
print(f'[Qwen Prompt Rewriter] Style: {prompt_style}')
643-
print(f'[Qwen Prompt Rewriter] Target Language: {target_language}')
644-
print(f'[Qwen Prompt Rewriter] Original: "{prompt}"')
645-
print(f'[Qwen Prompt Rewriter] Enhanced: "{output_prompt}"')
646-
647-
return (output_prompt,)
627+
with open(key_path, "r", encoding="utf-8") as f:
628+
_api_key = f.read().strip()
629+
630+
if not _api_key:
631+
raise EnvironmentError(f'API_KEY is not set in "{key_path}"\nPlease add your Aliyun API key to this file for cloud model usage.')
632+
633+
if prompt_style == "Qwen-Image":
634+
output_prompt = polish_prompt(_api_key, prompt, model=llm_model, max_retries=max_retries, target_language=target_language)
635+
else:
636+
# Qwen-Image-Edit requires at least one image
637+
if len(all_images) == 0:
638+
raise ValueError("Qwen-Image-Edit style requires at least one image input!")
639+
640+
print(f'[Qwen Prompt Rewriter] Using {len(all_images)} image(s) for Image-Edit')
641+
output_prompt = polish_prompt_edit(_api_key, prompt, all_images, model=llm_model, max_retries=max_retries, save_tokens=save_tokens, target_language=target_language)
642+
643+
print(f'[Qwen Prompt Rewriter] Style: {prompt_style}')
644+
print(f'[Qwen Prompt Rewriter] Target Language: {target_language}')
645+
print(f'[Qwen Prompt Rewriter] Original: "{prompt}"')
646+
print(f'[Qwen Prompt Rewriter] Enhanced: "{output_prompt}"')
647+
648+
return (output_prompt,)
649+
finally:
650+
try:
651+
from vision_llm_node import cleanup as vision_cleanup
652+
vision_cleanup()
653+
except Exception:
654+
pass
648655

649656
NODE_CLASS_MAPPINGS = {
650657
"QwenImageEditPromptGenerator": QwenImageEditPromptGenerator

0 commit comments

Comments
 (0)