Compare commits

...
34 Commits
Author SHA1 Message Date
Marcel Hofer 782d78b611 Release 2.3.14 Gemma4 GGUF stability 2026-06-20 17:53:04 +02:00
Marcel Hofer 7d1b9c028b Stabilize Gemma4 GGUF multimodal settings 2026-06-20 17:30:37 +02:00
Marcel Hofer 2b9ca76d41 Pin GGUF llama main GPU to Comfy device 2026-06-20 15:35:18 +02:00
Marcel Hofer 86ac1cee2e Clamp unsafe Gemma GGUF CPU multimodal load 2026-06-20 15:19:20 +02:00
Marcel Hofer b8d1c30484 Guard unsafe Gemma GGUF CPU multimodal path 2026-06-20 15:12:14 +02:00
Marcel Hofer 56c18a78ff Filter Qwen-only handler kwargs for Gemma GGUF 2026-06-20 15:04:56 +02:00
Marcel Hofer 110fe6973a Reuse mapped GGUF model aliases before download 2026-06-20 15:00:12 +02:00
Marcel Hofer 3b5a6752ec Fix duplicate ThinkingLLM search entries 2026-06-20 14:47:15 +02:00
Marcel Hofer f112bafdd7 Respect extra GGUF model paths in prompt enhancer 2026-06-20 11:19:48 +02:00
Marcel Hofer a58079cee4 Release 2.3.13 recommendation note cleanup 2026-06-20 10:40:32 +02:00
Marcel Hofer a99d342c2c Release 2.3.12 rollback recovery 2026-06-20 10:35:07 +02:00
Marcel Hofer 4f7f3905c8 Restore custom prompt image no-preset alias 2026-06-20 09:53:01 +02:00
Marcel Hofer cdf110e3e5 Preserve ComfyUI execution node colors 2026-06-20 09:37:16 +02:00
Marcel Hofer 52503f95f8 Disable automatic GGUF finalization retries 2026-06-19 19:14:46 +02:00
Marcel Hofer 07b88246c2 Fix Advanced GGUF layout in ComfyUI 2.0 2026-06-19 18:05:15 +02:00
Marcel Hofer ea25a8726f Release 2.3.9 preset tooltip previews 2026-06-15 11:15:34 +02:00
Marcel Hofer 21e90d0ba4 Release 2.3.8 LTX preset support 2026-06-15 11:11:15 +02:00
Marcel Hofer abe35d6dfb Release 2.3.7 Whisper offline cache fix 2026-06-14 19:36:38 +02:00
Marcel Hofer 028b2df006 Harden GGUF context reset between completions 2026-06-14 07:21:52 +00:00
Marcel Hofer de5d3e5da3 Release 2.3.5 audio model updates 2026-06-09 00:21:54 +02:00
Marcel Hofer 90df6de975 Add model recommendation guides and Hauhau GGUF support 2026-05-29 19:05:29 +02:00
Marcel Hofer 6faa6bade2 Silence stream-mode status chatter 2026-05-27 00:44:38 +02:00
Marcel Hofer 1189b6c4b3 Fix clean streaming output and guard GGUF loops 2026-05-27 00:32:08 +02:00
Marcel Hofer c49cbd1ceb Fix compact streaming display across ThinkingLLM nodes 2026-05-26 23:46:03 +02:00
Marcel Hofer 00f066d34f Fix GGUF prompt enhancer context crash 2026-05-18 20:50:11 +02:00
Marcel Hofer 21906a9dda Fix registry metadata and bump version to 2.3.0 2026-05-18 19:57:26 +02:00
Your Name dc80a9e503 bump version to 1.0.13 in __init__.py and custom_nodes.json 2026-05-18 11:19:23 +02:00
Your Name 7f0fe4a6de Merge branch 'main' of https://github.com/goodguy1963/ComfyUI-ThinkingLLM 2026-05-18 11:13:53 +02:00
Marcel 79a1dff210 Delete THINKINGLM_PERFORMANCE_ISSUES.md 2026-05-18 02:20:31 +02:00
Your Name 641df140e2 Remove outdated QwenVL workflows and introduce new ThinkingLLM workflow with advanced features and enhancements. 2026-05-18 02:18:59 +02:00
Your Name bcd8f4ae35 v1.0.13: harden GGUF backend diagnostics and downloads 2026-05-17 23:07:22 +02:00
Your Name 7b07a4d2ac v1.0.12: bump mirrored metadata for GGUF release 2026-05-17 21:59:15 +02:00
Your Name f214136331 v1.0.11: GGUF compact streaming display + auto-save on fixed seed for GGUF nodes 2026-05-17 21:52:03 +02:00
Your Name 7e75bdedf4 v1.0.11: Linux RAM reduction - post-download quant cleanup, float16-only dtype, CPU memory cap 2026-05-17 21:50:43 +02:00
24 changed files with 5600 additions and 951 deletions
+240 -12
View File
@@ -1,7 +1,9 @@
import contextlib
import importlib
import importlib.metadata
import os
import platform
import re
import subprocess
import sys
@@ -43,7 +45,9 @@ KNOWN_LINUX_WHEEL_SPECS = {
"llama_cpp_python-0.3.34+cu130.basic-cp312-cp312-linux_x86_64.whl"
),
}
VISION_HANDLER_NAMES = ("Qwen3VLChatHandler", "Qwen25VLChatHandler")
VISION_HANDLER_NAMES = ("Qwen3VLChatHandler", "Qwen25VLChatHandler", "Gemma4ChatHandler", "MTMDChatHandler")
_LAST_BACKEND_INFO: dict | None = None
_PRINTED_BACKEND_LINES: set[str] = set()
class _PathOnlyDllDirectoryHandle:
@@ -64,15 +68,106 @@ def _normalize_bool_env(name: str, default: bool = True) -> bool:
return value.strip().lower() not in {"0", "false", "no", "off"}
def _detect_cuda_version() -> str:
def _run_command_text(cmd: list[str], timeout: float = 5.0) -> str:
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
except Exception:
return ""
return "\n".join(part for part in (result.stdout, result.stderr) if part)
def _normalize_cuda_version(version: str) -> str:
match = re.search(r"(\d+)\.(\d+)", str(version or ""))
if not match:
return ""
return f"{int(match.group(1))}.{int(match.group(2))}"
def _parse_cuda_version(text: str) -> str:
if not text:
return ""
patterns = (
r"CUDA Version:\s*(\d+\.\d+)",
r"release\s+(\d+\.\d+)",
r"CUDA\s+Version\s+(\d+\.\d+)",
r"V(\d+\.\d+)(?:\.\d+)?",
)
for pattern in patterns:
match = re.search(pattern, text, flags=re.IGNORECASE)
if match:
return _normalize_cuda_version(match.group(1))
return ""
def _detect_torch_cuda_version() -> str:
try:
import torch
return str(getattr(torch.version, "cuda", "") or "")
torch_version = getattr(torch, "version", None)
return _normalize_cuda_version(str(getattr(torch_version, "cuda", "") or ""))
except Exception:
return ""
def _detect_cuda_version() -> str:
return _detect_torch_cuda_version()
def _detect_cuda_home_version() -> str:
for env_name in ("CUDA_HOME", "CUDA_PATH"):
cuda_home = os.getenv(env_name, "").strip()
if not cuda_home:
continue
version_file = os.path.join(cuda_home, "version.txt")
if os.path.isfile(version_file):
try:
with open(version_file, "r", encoding="utf-8", errors="ignore") as handle:
parsed = _parse_cuda_version(handle.read())
if parsed:
return parsed
except Exception:
pass
parsed = _normalize_cuda_version(os.path.basename(os.path.normpath(cuda_home)))
if parsed:
return parsed
if platform.system().lower() == "linux":
for path in ("/usr/local/cuda/version.txt",):
if not os.path.isfile(path):
continue
try:
with open(path, "r", encoding="utf-8", errors="ignore") as handle:
parsed = _parse_cuda_version(handle.read())
if parsed:
return parsed
except Exception:
pass
return ""
def _detect_nvcc_cuda_version() -> str:
return _parse_cuda_version(_run_command_text(["nvcc", "--version"]))
def _detect_nvidia_smi_cuda_version() -> str:
return _parse_cuda_version(_run_command_text(["nvidia-smi"]))
def _detect_cuda_versions() -> dict[str, str]:
toolkit = _detect_cuda_home_version() or _detect_nvcc_cuda_version()
torch_cuda = _detect_torch_cuda_version()
nvidia_smi = _detect_nvidia_smi_cuda_version()
selected = toolkit or torch_cuda or nvidia_smi
selected_source = "toolkit" if toolkit else "torch" if torch_cuda else "nvidia-smi" if nvidia_smi else "unknown"
return {
"toolkit": toolkit,
"torch": torch_cuda,
"nvidia_smi": nvidia_smi,
"selected": selected,
"selected_source": selected_source,
}
def _resolve_linux_install_spec() -> tuple[str, str]:
wheel_override = os.getenv("THINKINGLLM_LLAMA_CPP_LINUX_WHEEL_URL", "").strip()
if wheel_override:
@@ -91,13 +186,23 @@ def _resolve_linux_install_spec() -> tuple[str, str]:
else:
platform_tag = f"linux_{machine or 'unknown'}"
cuda_version = _detect_cuda_version()
cuda_key = ".".join(cuda_version.split(".")[:2]) if cuda_version else ""
known_spec = KNOWN_LINUX_WHEEL_SPECS.get((py_tag, platform_tag, cuda_key))
if known_spec:
return known_spec, "known Linux wheel"
cuda_versions = _detect_cuda_versions()
candidates = []
for source in ("toolkit", "torch", "nvidia_smi"):
cuda_key = cuda_versions.get(source) or ""
if cuda_key and cuda_key not in candidates:
candidates.append(cuda_key)
return DEFAULT_JAMEPENG_GIT_SPEC, "source-build fallback"
for cuda_key in candidates:
known_spec = KNOWN_LINUX_WHEEL_SPECS.get((py_tag, platform_tag, cuda_key))
if known_spec:
source = next((name for name in ("toolkit", "torch", "nvidia_smi") if cuda_versions.get(name) == cuda_key), "detected")
return known_spec, f"known Linux wheel ({source} CUDA {cuda_key})"
if _normalize_bool_env("THINKINGLLM_LLAMA_CPP_ALLOW_SOURCE_BUILD", False):
return DEFAULT_JAMEPENG_GIT_SPEC, "source-build fallback (explicit opt-in)"
return "", "no verified Linux wheel match"
def _clear_llama_cpp_modules() -> None:
@@ -173,6 +278,7 @@ _relax_windows_dll_directory_for_long_paths = relax_windows_dll_directory_for_lo
def _import_llama_cpp_backend(require_vision_handlers: bool):
global _LAST_BACKEND_INFO
_clear_llama_cpp_modules()
with relax_windows_dll_directory_for_long_paths():
llama_cpp = importlib.import_module("llama_cpp")
@@ -180,17 +286,128 @@ def _import_llama_cpp_backend(require_vision_handlers: bool):
if llama_class is None:
raise ImportError("llama_cpp imported but does not expose Llama")
vision_handlers = []
if require_vision_handlers:
chat_format = importlib.import_module("llama_cpp.llama_chat_format")
if not any(hasattr(chat_format, name) for name in VISION_HANDLER_NAMES):
vision_handlers = [name for name in VISION_HANDLER_NAMES if hasattr(chat_format, name)]
if not vision_handlers:
raise ImportError(
"llama_cpp is installed but missing Qwen vision chat handlers "
f"({', '.join(VISION_HANDLER_NAMES)})"
)
else:
try:
chat_format = importlib.import_module("llama_cpp.llama_chat_format")
vision_handlers = [name for name in VISION_HANDLER_NAMES if hasattr(chat_format, name)]
except Exception:
vision_handlers = []
_LAST_BACKEND_INFO = _collect_llama_cpp_backend_info(llama_cpp, vision_handlers)
_print_backend_info(_LAST_BACKEND_INFO)
return llama_class
def _probe_gpu_offload_support() -> bool | None:
try:
low_level = importlib.import_module("llama_cpp.llama_cpp")
except Exception:
return None
candidates = [low_level]
lib = getattr(low_level, "_lib", None)
if lib is not None:
candidates.append(lib)
for candidate in candidates:
probe = getattr(candidate, "llama_supports_gpu_offload", None)
if probe is None:
continue
try:
return bool(probe())
except Exception:
return None
return None
def _package_version() -> str:
try:
return importlib.metadata.version("llama-cpp-python")
except Exception:
return "unknown"
def _collect_llama_cpp_backend_info(llama_cpp, vision_handlers: list[str]) -> dict:
cuda_versions = _detect_cuda_versions()
return {
"version": str(getattr(llama_cpp, "__version__", "") or _package_version()),
"module_path": str(getattr(llama_cpp, "__file__", "") or "unknown"),
"python_tag": f"cp{sys.version_info.major}{sys.version_info.minor}",
"platform": platform.platform(),
"machine": platform.machine().lower(),
"torch_cuda": cuda_versions.get("torch", ""),
"toolkit_cuda": cuda_versions.get("toolkit", ""),
"nvidia_smi_cuda": cuda_versions.get("nvidia_smi", ""),
"selected_cuda": cuda_versions.get("selected", ""),
"selected_cuda_source": cuda_versions.get("selected_source", "unknown"),
"vision_handlers": list(vision_handlers),
"gpu_offload": _probe_gpu_offload_support(),
}
def _format_bool_status(value) -> str:
if value is True:
return "yes"
if value is False:
return "no"
return "unknown"
def format_llama_cpp_backend_info(info: dict | None = None) -> str:
info = info or _LAST_BACKEND_INFO or {}
handlers = ",".join(info.get("vision_handlers") or []) or "missing"
return (
f"llama-cpp-python {info.get('version', 'unknown')} "
f"({info.get('python_tag', 'unknown')}, {info.get('machine', 'unknown')}); "
f"GPU offload={_format_bool_status(info.get('gpu_offload'))}; "
f"vision={handlers}; "
f"CUDA torch={info.get('torch_cuda') or 'unknown'}, "
f"toolkit={info.get('toolkit_cuda') or 'unknown'}, "
f"nvidia-smi={info.get('nvidia_smi_cuda') or 'unknown'}; "
f"module={info.get('module_path', 'unknown')}"
)
def _print_backend_info(info: dict) -> None:
line = f"[QwenVL] llama.cpp backend: {format_llama_cpp_backend_info(info)}"
if line in _PRINTED_BACKEND_LINES:
return
_PRINTED_BACKEND_LINES.add(line)
print(line)
def get_last_llama_cpp_backend_info() -> dict | None:
return dict(_LAST_BACKEND_INFO) if _LAST_BACKEND_INFO else None
def _build_source_install_env() -> dict[str, str]:
install_env = dict(os.environ)
toolkit_cuda = _detect_cuda_home_version() or _detect_nvcc_cuda_version()
if toolkit_cuda:
existing_args = install_env.get("CMAKE_ARGS", "").strip()
cuda_arg = "-DGGML_CUDA=on"
install_env["CMAKE_ARGS"] = f"{existing_args} {cuda_arg}".strip() if cuda_arg not in existing_args else existing_args
install_env["FORCE_CMAKE"] = "1"
return install_env
if not _normalize_bool_env("THINKINGLLM_LLAMA_CPP_ALLOW_CPU_SOURCE_BUILD", False):
raise RuntimeError(
"[QwenVL] Refusing automatic llama_cpp source build because no CUDA toolkit was detected. "
"Set THINKINGLLM_LLAMA_CPP_LINUX_WHEEL_URL to a matching wheel, install CUDA toolkit, or set "
"THINKINGLLM_LLAMA_CPP_ALLOW_CPU_SOURCE_BUILD=1 if you intentionally want a CPU build."
)
return install_env
def ensure_llama_cpp_backend(require_vision_handlers: bool = False):
try:
return _import_llama_cpp_backend(require_vision_handlers=require_vision_handlers)
@@ -202,6 +419,16 @@ def ensure_llama_cpp_backend(require_vision_handlers: bool = False):
) from first_exc
install_spec, install_reason = _resolve_linux_install_spec()
if not install_spec:
cuda_versions = _detect_cuda_versions()
raise RuntimeError(
"[QwenVL] llama_cpp is unavailable or incompatible, and no verified JamePeng Linux wheel matches this runtime. "
f"Detected CUDA: toolkit={cuda_versions.get('toolkit') or 'unknown'}, "
f"torch={cuda_versions.get('torch') or 'unknown'}, nvidia-smi={cuda_versions.get('nvidia_smi') or 'unknown'}. "
"Set THINKINGLLM_LLAMA_CPP_LINUX_WHEEL_URL to a matching wheel, "
"THINKINGLLM_LLAMA_CPP_LINUX_SPEC to an explicit package spec, or "
"THINKINGLLM_LLAMA_CPP_ALLOW_SOURCE_BUILD=1 to opt into a local source build."
) from first_exc
install_cmd = [
sys.executable,
"-m",
@@ -216,7 +443,8 @@ def ensure_llama_cpp_backend(require_vision_handlers: bool = False):
f"[QwenVL] llama_cpp check failed on Linux; attempting automatic install "
f"using {install_reason}: {install_spec}"
)
result = subprocess.run(install_cmd, capture_output=True, text=True)
install_env = _build_source_install_env() if install_spec == DEFAULT_JAMEPENG_GIT_SPEC else None
result = subprocess.run(install_cmd, capture_output=True, text=True, env=install_env)
if result.returncode != 0:
stdout = (result.stdout or "").strip()
stderr = (result.stderr or "").strip()
@@ -232,4 +460,4 @@ def ensure_llama_cpp_backend(require_vision_handlers: bool = False):
"[QwenVL] llama_cpp was installed, but the backend is still incompatible. "
"Set THINKINGLLM_LLAMA_CPP_LINUX_WHEEL_URL to a matching JamePeng wheel or "
"THINKINGLLM_LLAMA_CPP_LINUX_SPEC to an explicit package spec, then restart ComfyUI."
) from second_exc
) from second_exc
+235 -73
View File
@@ -14,7 +14,9 @@
# Source: https://github.com/1038lab/ComfyUI-QwenVL
import gc
import inspect
import json
import os
import platform
import shutil
import time
@@ -26,7 +28,11 @@ import numpy as np
import psutil
import torch
from PIL import Image
from huggingface_hub import snapshot_download
try:
from huggingface_hub import hf_hub_download, snapshot_download
except ImportError:
from huggingface_hub import snapshot_download
hf_hub_download = None
try:
from tqdm.auto import tqdm
except ImportError:
@@ -58,7 +64,10 @@ try:
sageattn_qk_int8_pv_fp8_cuda_sm90,
)
SAGE_ATTENTION_AVAILABLE = True
except ImportError:
except Exception:
sageattn_qk_int8_pv_fp16_cuda = None
sageattn_qk_int8_pv_fp8_cuda = None
sageattn_qk_int8_pv_fp8_cuda_sm90 = None
SAGE_ATTENTION_AVAILABLE = False
# Global cache for generated prompts
@@ -77,7 +86,8 @@ QWEN_FINAL_ANSWER_RESERVE_TOKENS = 256
QWEN_CONTEXT_OVERHEAD_TOKENS = 128
_QWEN_SOFT_SWITCHES = {"/think", "/no_think", "/nothink"}
_STREAM_HEARTBEAT_INTERVAL_SECONDS = 3.0
_DOWNLOAD_STATUS_INTERVAL_SECONDS = 0.75
_DOWNLOAD_STATUS_INTERVAL_SECONDS = 0.25
_HF_TOKEN_ENV_VARS = ("HF_TOKEN", "HUGGING_FACE_HUB_TOKEN")
def _format_download_size(num_bytes) -> str:
@@ -404,6 +414,7 @@ def resolve_qwen_thinking_mode(
min_reasoning_tokens=QWEN_MIN_REASONING_BUDGET_TOKENS,
answer_reserve_tokens=QWEN_FINAL_ANSWER_RESERVE_TOKENS,
context_overhead_tokens=QWEN_CONTEXT_OVERHEAD_TOKENS,
quiet=False,
):
"""Enable thinking only when requested and enough generation budget remains."""
if not bool(requested_enable):
@@ -426,11 +437,12 @@ def resolve_qwen_thinking_mode(
)
else:
budget_note = f"max_tokens={requested_output}, available_output={available_output}"
print(
f"[{label}] Thinking requested but {budget_note} is below the required {minimum_required} tokens "
f"({int(min_reasoning_tokens)} reasoning + {int(answer_reserve_tokens)} answer reserve); "
"forcing non-thinking mode to preserve answer space."
)
if not quiet:
print(
f"[{label}] Thinking requested but {budget_note} is below the required {minimum_required} tokens "
f"({int(min_reasoning_tokens)} reasoning + {int(answer_reserve_tokens)} answer reserve); "
"forcing non-thinking mode to preserve answer space."
)
return False
return True
@@ -444,7 +456,7 @@ def apply_qwen_soft_thinking_directive(prompt_text, enable_thinking, supports_so
lines.append("/think" if enable_thinking else "/no_think")
return "\n".join(lines).strip()
def get_cache_key(model_name, preset_prompt, custom_prompt, image_hash=None, video_hash=None, seed=None, max_tokens=None, temperature=None, top_p=None, repetition_penalty=None):
def get_cache_key(model_name, preset_prompt, custom_prompt, image_hash=None, video_hash=None, seed=None, max_tokens=None, temperature=None, top_p=None, repetition_penalty=None, enable_thinking=None):
"""Generate cache key from inputs including all generation parameters.
All generation-parameter fields are optional so existing callers that omit them
@@ -461,6 +473,7 @@ def get_cache_key(model_name, preset_prompt, custom_prompt, image_hash=None, vid
"temperature": temperature if temperature is not None else None,
"top_p": top_p if top_p is not None else None,
"repetition_penalty": repetition_penalty if repetition_penalty is not None else None,
"enable_thinking": bool(enable_thinking) if enable_thinking is not None else None,
}
# Create deterministic hash
key_str = json.dumps(key_data, sort_keys=True)
@@ -658,8 +671,81 @@ TOOLTIPS = {
"num_beams": "Beam-search width. Values >1 disable temperature/top_p and trade speed for more stable answers.",
"repetition_penalty": "Values >1 (e.g., 1.1–1.3) penalize repeated phrases; 1.0 leaves logits untouched.",
"frame_count": "Number of frames extracted from video inputs before prompting Qwen-VL. More frames provide context but cost time.",
"hf_token": "Optional Hugging Face access token for private or gated model downloads. It is passed only to the download call, never logged or cached, and the in-memory copy is dropped after the download attempt. Clear this field before saving or sharing workflows.",
}
def _iter_hf_token_candidates(hf_token: str | None):
token = str(hf_token or "").strip()
if token:
yield token
for env_name in _HF_TOKEN_ENV_VARS:
token = str(os.environ.get(env_name) or "").strip()
if token:
yield token
def _clean_hf_token(hf_token: str | None) -> str | None:
return next(_iter_hf_token_candidates(hf_token), None)
def _redact_hf_token(text: str, hf_token: str | None) -> str:
redacted = str(text)
for token in _iter_hf_token_candidates(hf_token):
redacted = redacted.replace(token, "<redacted HF token>")
return redacted
def _hf_download_error_message(exc: Exception, *, repo_id: str, hf_token: str | None) -> str:
message = _redact_hf_token(str(exc), hf_token)
lower = message.lower()
access_markers = (
"401",
"403",
"unauthorized",
"forbidden",
"gated",
"private",
"repository not found",
"requires authentication",
)
if any(marker in lower for marker in access_markers):
if _clean_hf_token(hf_token):
message += (
f"\n[QwenVL] Hugging Face access hint for {repo_id}: a token was supplied but access was rejected. "
"Confirm the token has read permission and that you accepted the model license/gate on Hugging Face."
)
else:
message += (
f"\n[QwenVL] Hugging Face access hint for {repo_id}: this model may be private or gated. "
"Paste a read token into the hf_token field for this run, or set up access on Hugging Face first."
)
return message
def _call_hf_hub_download(*, repo_id: str, filename: str, local_dir: Path, token: str | None, reporter: _DownloadProgressReporter) -> Path:
if hf_hub_download is None:
raise RuntimeError("huggingface_hub.hf_hub_download is unavailable in this environment")
download_kwargs = {
"repo_id": repo_id,
"repo_type": "model",
"filename": filename,
"local_dir": str(local_dir),
}
if token:
download_kwargs["token"] = token
tqdm_cls = reporter.make_tqdm_class()
if tqdm_cls is not None:
try:
if "tqdm_class" in inspect.signature(hf_hub_download).parameters:
download_kwargs["tqdm_class"] = tqdm_cls
except (TypeError, ValueError):
pass
try:
return Path(hf_hub_download(**download_kwargs))
finally:
download_kwargs.pop("token", None)
class Quantization(str, Enum):
Q4 = "4-bit (VRAM-friendly)"
Q8 = "8-bit (Balanced)"
@@ -815,6 +901,13 @@ if not HF_ALL_MODELS:
load_model_configs()
def _default_model_from_config(models: dict[str, dict], fallback: str) -> str:
for name, info in models.items():
if isinstance(info, dict) and info.get("default"):
return name
return next(iter(models.keys()), fallback)
def read_hf_model_type(model_dir: str) -> str | None:
"""Read model_type from a HuggingFace model's config.json."""
try:
@@ -1072,6 +1165,15 @@ def _model_snapshot_has_weights(target: Path) -> bool:
return any(target.glob("*.safetensors")) or any(target.glob("*.bin"))
def _cleanup_unneeded_snapshot_weights(target: Path) -> None:
unwanted_markers = ("fp32", "bf16", "fp8", "q2_k", "q3_k", "q4_k", "q5_k", "q6_k", "q8_0", "f32")
for weight_file in target.rglob("*.safetensors"):
name = weight_file.name.lower()
if any(marker in name for marker in unwanted_markers):
print(f"[QwenVL] Removing unneeded quantized weight: {weight_file.name}")
weight_file.unlink(missing_ok=True)
def _model_snapshot_has_required_files(target: Path, require_processor: bool = False) -> bool:
if not target.exists() or not target.is_dir():
return False
@@ -1091,8 +1193,10 @@ def _model_snapshot_has_required_files(target: Path, require_processor: bool = F
return True
def _download_model_snapshot(repo_id: str, target: Path, *, force_clean_target: bool = False, node_id=None, progress_label: str | None = None) -> None:
def _download_model_snapshot(repo_id: str, target: Path, *, force_clean_target: bool = False, node_id=None, progress_label: str | None = None, hf_token: str | None = None) -> None:
reporter = _DownloadProgressReporter(node_id=node_id, label=progress_label or "QwenVL HF Download", repo_id=repo_id)
token_for_download = _clean_hf_token(hf_token)
download_kwargs = {}
if force_clean_target and target.exists() and target.is_dir():
print(f"[QwenVL] Removing incomplete model snapshot before retry: {target}")
reporter.stage("Cleaning incomplete snapshot", detail=target.name)
@@ -1105,17 +1209,23 @@ def _download_model_snapshot(repo_id: str, target: Path, *, force_clean_target:
"ignore_patterns": ["*.md", ".git*"],
"force_download": force_clean_target,
}
if token_for_download:
download_kwargs["token"] = token_for_download
tqdm_cls = reporter.make_tqdm_class()
if tqdm_cls is not None:
download_kwargs["tqdm_class"] = tqdm_cls
snapshot_download(**download_kwargs)
_cleanup_unneeded_snapshot_weights(target)
except Exception as exc:
reporter.fail(str(exc))
raise
reporter.fail(_hf_download_error_message(exc, repo_id=repo_id, hf_token=token_for_download))
raise RuntimeError(_hf_download_error_message(exc, repo_id=repo_id, hf_token=token_for_download)) from exc
finally:
download_kwargs.pop("token", None)
token_for_download = None
reporter.finish(detail=target.name)
def download_hf_file_to_path(repo_ids: list[str], filename: str, target_path: Path, *, node_id=None, progress_label: str = "QwenVL GGUF Download") -> None:
def download_hf_file_to_path(repo_ids: list[str], filename: str, target_path: Path, *, node_id=None, progress_label: str = "QwenVL GGUF Download", hf_token: str | None = None) -> None:
if target_path.exists():
print(f"[QwenVL] Using cached file: {target_path}")
return
@@ -1123,40 +1233,77 @@ def download_hf_file_to_path(repo_ids: list[str], filename: str, target_path: Pa
target_path.parent.mkdir(parents=True, exist_ok=True)
last_exc: Exception | None = None
wanted_name = Path(filename).name
token_for_download = _clean_hf_token(hf_token)
exact_candidates = []
for candidate in (filename, wanted_name):
if candidate and candidate not in exact_candidates:
exact_candidates.append(candidate)
for repo_id in repo_ids:
reporter = _DownloadProgressReporter(node_id=node_id, label=progress_label, repo_id=repo_id)
reporter.stage("Preparing download", detail=wanted_name)
try:
download_kwargs = {
"repo_id": repo_id,
"repo_type": "model",
"local_dir": str(target_path.parent),
"allow_patterns": [filename, wanted_name, f"**/{wanted_name}"],
}
tqdm_cls = reporter.make_tqdm_class()
if tqdm_cls is not None:
download_kwargs["tqdm_class"] = tqdm_cls
snapshot_download(**download_kwargs)
found = list(target_path.parent.rglob(wanted_name))
if found:
downloaded_path = found[0]
if downloaded_path.exists() and downloaded_path.resolve() != target_path.resolve():
target_path.parent.mkdir(parents=True, exist_ok=True)
downloaded_path.replace(target_path)
if target_path.exists():
reporter.finish(detail=wanted_name)
return
reporter.fail(f"{wanted_name} was not found after snapshot download")
except Exception as exc:
last_exc = exc
reporter.fail(str(exc))
print(f"[QwenVL] snapshot_download failed from {repo_id}: {exc}")
try:
for repo_id in repo_ids:
reporter = _DownloadProgressReporter(node_id=node_id, label=progress_label, repo_id=repo_id)
auth_label = "Preparing authenticated download" if token_for_download else "Preparing download"
reporter.stage(auth_label, detail=wanted_name)
if hf_hub_download is not None:
for candidate in exact_candidates:
try:
reporter.stage("Resolving exact file", detail=candidate, force=False)
downloaded_path = _call_hf_hub_download(
repo_id=repo_id,
filename=candidate,
local_dir=target_path.parent,
token=token_for_download,
reporter=reporter,
)
if downloaded_path.exists() and downloaded_path.resolve() != target_path.resolve():
target_path.parent.mkdir(parents=True, exist_ok=True)
downloaded_path.replace(target_path)
if target_path.exists():
reporter.finish(detail=wanted_name)
return
except Exception as exc:
hint = _hf_download_error_message(exc, repo_id=repo_id, hf_token=token_for_download)
last_exc = RuntimeError(hint)
print(f"[QwenVL] exact file download failed from {repo_id}/{candidate}: {hint}")
reporter.stage("Falling back to snapshot scan", detail=wanted_name, force=False)
download_kwargs = {}
try:
download_kwargs = {
"repo_id": repo_id,
"repo_type": "model",
"local_dir": str(target_path.parent),
"allow_patterns": [filename, wanted_name, f"**/{wanted_name}"],
}
if token_for_download:
download_kwargs["token"] = token_for_download
tqdm_cls = reporter.make_tqdm_class()
if tqdm_cls is not None:
download_kwargs["tqdm_class"] = tqdm_cls
snapshot_download(**download_kwargs)
found = list(target_path.parent.rglob(wanted_name))
if found:
downloaded_path = found[0]
if downloaded_path.exists() and downloaded_path.resolve() != target_path.resolve():
target_path.parent.mkdir(parents=True, exist_ok=True)
downloaded_path.replace(target_path)
if target_path.exists():
reporter.finish(detail=wanted_name)
return
reporter.fail(f"{wanted_name} was not found after snapshot download")
except Exception as exc:
hint = _hf_download_error_message(exc, repo_id=repo_id, hf_token=token_for_download)
last_exc = RuntimeError(hint)
reporter.fail(hint)
print(f"[QwenVL] snapshot_download failed from {repo_id}: {hint}")
finally:
download_kwargs.pop("token", None)
finally:
token_for_download = None
raise FileNotFoundError(f"[QwenVL] Download failed for {wanted_name}: {last_exc}")
def ensure_model(model_name, require_processor=False, node_id=None, progress_label: str | None = None):
def ensure_model(model_name, require_processor=False, node_id=None, progress_label: str | None = None, hf_token: str | None = None):
info = HF_ALL_MODELS.get(model_name)
if not info:
raise ValueError(f"Model '{model_name}' not in config")
@@ -1197,6 +1344,7 @@ def ensure_model(model_name, require_processor=False, node_id=None, progress_lab
target,
node_id=node_id,
progress_label=progress_label or f"QwenVL HF Download: {model_name}",
hf_token=hf_token,
)
if not _model_snapshot_has_required_files(target, require_processor=require_processor):
@@ -1207,6 +1355,7 @@ def ensure_model(model_name, require_processor=False, node_id=None, progress_lab
force_clean_target=True,
node_id=node_id,
progress_label=progress_label or f"QwenVL HF Download: {model_name}",
hf_token=hf_token,
)
if not _model_snapshot_has_required_files(target, require_processor=require_processor):
@@ -1285,7 +1434,7 @@ def quantization_config(model_name, quantization):
return cfg, None
if quantization == Quantization.Q8:
return BitsAndBytesConfig(load_in_8bit=True), None
return None, torch.float16 if torch.cuda.is_available() else torch.float32
return None, torch.float16
class QwenVLBase:
def __init__(self):
@@ -1318,6 +1467,7 @@ class QwenVLBase:
device_choice,
keep_model_loaded,
unique_id=None,
hf_token: str | None = None,
):
quant = Quantization.from_value(quant_value) # Skip enforce_memory for now
@@ -1359,6 +1509,7 @@ class QwenVLBase:
require_processor=True,
node_id=unique_id,
progress_label=f"QwenVL HF Download: {model_name}",
hf_token=hf_token,
)
quant_config, dtype = quantization_config(model_name, quant)
@@ -1388,28 +1539,26 @@ class QwenVLBase:
# Quantization is back - enforce_memory was the problem
load_kwargs = {
"device_map": "cpu", # Force CPU loading to test
"dtype": torch.float16, # Use dtype instead of deprecated torch_dtype
"device_map": "auto" if device != "cpu" and torch.cuda.is_available() else "cpu",
"dtype": dtype or torch.float16,
"attn_implementation": actual_attn_impl,
"use_safetensors": True,
"low_cpu_mem_usage": True,
}
if device != "cpu" and torch.cuda.is_available():
gpu_mem = torch.cuda.get_device_properties(0).total_memory
gpu_mem_gb = max(1, int(gpu_mem / (1024**3)))
load_kwargs["max_memory"] = {
0: f"{gpu_mem_gb}GB",
"cpu": "16GB",
}
if quant_config:
load_kwargs["quantization_config"] = quant_config
self.model = AutoModelForVision2Seq.from_pretrained(model_path, **load_kwargs).eval()
# Move to GPU manually if loading was on CPU
if device != "cpu" and torch.cuda.is_available():
print(f"[QwenVL] 🔄 Moving model from CPU to {device}...")
try:
self.model = self.model.to(device)
print(f"[QwenVL] ✅ Model moved to {device}")
except Exception as e:
print(f"[QwenVL] ❌ Failed to move to GPU: {e}")
print("[QwenVL] Keeping model on CPU (will be very slow)")
# Apply SageAttention patching if needed
if attn_impl == "sage":
try:
@@ -1531,6 +1680,7 @@ class QwenVLBase:
label="QwenVL HF",
prompt_tokens=prompt_tokens,
context_window=context_window,
quiet=stream_to_terminal,
)
if effective_thinking != requested_thinking:
_, processed = _build_processed(effective_thinking)
@@ -1568,30 +1718,30 @@ class QwenVLBase:
# Optional: staged readable terminal streaming
if stream_to_terminal:
try:
from AILab_StreamDisplay import TerminalStreamDisplay
from transformers import TextIteratorStreamer
from threading import Thread
stage_label = "STREAMING"
sd = TerminalStreamDisplay("QwenVL HF", suppress_planning=True, compact=False)
stream_display = TerminalStreamDisplay("QwenVL HF", suppress_planning=True, compact=True)
streamer = TextIteratorStreamer(self.tokenizer, skip_prompt=True, skip_special_tokens=True)
kwargs["streamer"] = streamer
thread = Thread(target=self.model.generate, kwargs={**model_inputs, **kwargs})
print(f"[QwenVL HF] STREAMING {stage_label}")
sd.start_stage(stage_label)
stage_started_at = time.monotonic()
last_status_at = stage_started_at
stream_display.start_stage(stage_label)
thread.start()
full_streamed = ""
for token_str in streamer:
if token_str:
throw_exception_if_processing_interrupted()
full_streamed += token_str
sd.push(token_str)
last_status_at = _maybe_emit_hf_stream_heartbeat(stage_label, stage_started_at, last_status_at, full_streamed)
stream_display.push_compact(token_str)
thread.join()
sd.end_stage()
stream_display.end_compact()
stream_display.end_stage()
if not full_streamed.strip():
raise RuntimeError("[QwenVL] HF streaming returned empty response")
return full_streamed.strip(), full_streamed.strip()
cleaned_text = full_streamed.strip()
raw_text = full_streamed.strip()
return cleaned_text, raw_text
except ImportError:
print("[QwenVL] TextIteratorStreamer not available — falling back to non-streaming")
stream_to_terminal = False
@@ -1638,7 +1788,7 @@ class QwenVLBase:
cleaned_text = raw_text # generate() returns raw; caller cleans if needed
return cleaned_text, raw_text
def run(self, model_name, quantization, preset_prompt, custom_prompt, image, video, frame_count, max_tokens, temperature, top_p, num_beams, repetition_penalty, seed, keep_model_loaded, attention_mode, use_torch_compile, device, keep_last_prompt=False, unique_id=None, extra_pnginfo=None, node_class="QwenVL", stream_to_terminal=False, enable_thinking=True):
def run(self, model_name, quantization, preset_prompt, custom_prompt, image, video, frame_count, max_tokens, temperature, top_p, num_beams, repetition_penalty, seed, keep_model_loaded, attention_mode, use_torch_compile, device, keep_last_prompt=False, unique_id=None, extra_pnginfo=None, node_class="QwenVL", stream_to_terminal=False, enable_thinking=True, hf_token: str | None = None):
torch.manual_seed(seed)
image_hash = get_image_hash(image)
video_hash = get_video_hash(video)
@@ -1662,6 +1812,11 @@ class QwenVLBase:
# Auto-retrieve saved prompt when seed is fixed
saved = get_node_saved_prompt_with_seed(node_class, unique_id, extra_pnginfo, seed=seed, max_tokens=max_tokens, temperature=temperature, top_p=top_p, repetition_penalty=repetition_penalty, input_signature=input_signature)
if saved and not stream_to_terminal:
print(f"[QwenVL] Fixed seed {seed} matched — using per-node prompt: {saved[:50]}...")
return (saved,)
if saved and stream_to_terminal:
pass
if keep_last_prompt:
print(f"[QwenVL] Keep last prompt enabled — looking up per-node state")
if saved:
@@ -1672,12 +1827,13 @@ class QwenVLBase:
return ("",)
# Always generate unless keep_last_prompt was requested
print(f"[QwenVL] Generating new prompt")
if not stream_to_terminal:
print(f"[QwenVL] Generating new prompt")
prompt_template = "" if preset_prompt == NO_PRESET_PROMPT else SYSTEM_PROMPTS.get(preset_prompt, preset_prompt)
# Generate cache key with all inputs including seed
cache_key = get_cache_key(model_name, preset_prompt, custom_prompt, image_hash, video_hash, seed, max_tokens=max_tokens, temperature=temperature, top_p=top_p, repetition_penalty=repetition_penalty)
cache_key = get_cache_key(model_name, preset_prompt, custom_prompt, image_hash, video_hash, seed, max_tokens=max_tokens, temperature=temperature, top_p=top_p, repetition_penalty=repetition_penalty, enable_thinking=enable_thinking)
# Check cache first (only for random mode)
if cache_key in PROMPT_CACHE:
@@ -1700,7 +1856,9 @@ class QwenVLBase:
device,
keep_model_loaded,
unique_id=unique_id,
hf_token=hf_token,
)
hf_token = None
try:
text, raw_text = self.generate(
prompt,
@@ -1744,7 +1902,7 @@ class ThinkingLLM_QwenVL(QwenVLBase):
@classmethod
def INPUT_TYPES(cls):
models = list(HF_VL_MODELS.keys())
default_model = models[0] if models else "Qwen3-VL-4B-Instruct"
default_model = _default_model_from_config(HF_VL_MODELS, "Qwen3-VL-4B-Instruct")
prompts = PRESET_PROMPTS or [NO_PRESET_PROMPT, "Describe this image in detail."]
preferred_prompt = "🖼️ Detailed Description"
default_prompt = preferred_prompt if preferred_prompt in prompts else prompts[0]
@@ -1760,6 +1918,7 @@ class ThinkingLLM_QwenVL(QwenVLBase):
"keep_last_prompt": ("BOOLEAN", {"default": False, "tooltip": "Keep the last generated prompt instead of creating a new one"}),
"stream_tokens_to_terminal": ("BOOLEAN", {"default": False, "tooltip": "Print every generated token live to the ComfyUI terminal/console"}),
"enable_thinking": ("BOOLEAN", {"default": True, "tooltip": "Enable model reasoning/thinking when the backend supports it: True=allow thinking, False=force direct answer. Even when enabled, easy prompts may still get a direct answer, and this node automatically disables thinking when there is not enough output budget left for useful reasoning. For non-Qwen models (Gemma, LLaMA) this is advisory."}),
"hf_token": ("STRING", {"default": "", "multiline": False, "tooltip": TOOLTIPS["hf_token"]}),
},
"optional": {
"image": ("IMAGE",),
@@ -1776,10 +1935,11 @@ class ThinkingLLM_QwenVL(QwenVLBase):
FUNCTION = "process"
CATEGORY = "ThinkingLLM"
def process(self, model_name, preset_prompt, custom_prompt, attention_mode, max_tokens, keep_model_loaded, seed, keep_last_prompt=False, image=None, video=None, unique_id=None, extra_pnginfo=None, stream_tokens_to_terminal=False, enable_thinking=True):
def process(self, model_name, preset_prompt, custom_prompt, attention_mode, max_tokens, keep_model_loaded, seed, keep_last_prompt=False, image=None, video=None, unique_id=None, extra_pnginfo=None, stream_tokens_to_terminal=False, enable_thinking=True, hf_token=""):
# Always use FP16 - dropdown removed but keep working logic
quantization = Quantization.FP16.value
result = self.run(model_name, quantization, preset_prompt, custom_prompt, image, video, 16, max_tokens, 0.6, 0.9, 1, 1.2, seed, keep_model_loaded, attention_mode, False, "auto", keep_last_prompt, unique_id=unique_id, extra_pnginfo=extra_pnginfo, node_class="ThinkingLLM_QwenVL", stream_to_terminal=stream_tokens_to_terminal, enable_thinking=enable_thinking)
result = self.run(model_name, quantization, preset_prompt, custom_prompt, image, video, 16, max_tokens, 0.6, 0.9, 1, 1.2, seed, keep_model_loaded, attention_mode, False, "auto", keep_last_prompt, unique_id=unique_id, extra_pnginfo=extra_pnginfo, node_class="ThinkingLLM_QwenVL", stream_to_terminal=stream_tokens_to_terminal, enable_thinking=enable_thinking, hf_token=hf_token)
hf_token = ""
# Retrieve the raw_trace that was saved alongside the cleaned prompt in run()
key = _make_node_state_key("ThinkingLLM_QwenVL", unique_id, extra_pnginfo)
entry = NODE_PROMPT_STATE.get(key, {})
@@ -1790,7 +1950,7 @@ class ThinkingLLM_QwenVL_Advanced(QwenVLBase):
@classmethod
def INPUT_TYPES(cls):
models = list(HF_VL_MODELS.keys())
default_model = models[0] if models else "Qwen3-VL-4B-Instruct"
default_model = _default_model_from_config(HF_VL_MODELS, "Qwen3-VL-4B-Instruct")
prompts = PRESET_PROMPTS or [NO_PRESET_PROMPT, "Describe this image in detail."]
preferred_prompt = "🖼️ Detailed Description"
default_prompt = preferred_prompt if preferred_prompt in prompts else prompts[0]
@@ -1818,6 +1978,7 @@ class ThinkingLLM_QwenVL_Advanced(QwenVLBase):
"keep_last_prompt": ("BOOLEAN", {"default": False, "tooltip": "Keep last generated prompt instead of creating a new one"}),
"stream_tokens_to_terminal": ("BOOLEAN", {"default": False, "tooltip": "Print every generated token live to the ComfyUI terminal/console"}),
"enable_thinking": ("BOOLEAN", {"default": True, "tooltip": "Enable model reasoning/thinking when the backend supports it: True=allow thinking, False=force direct answer. Even when enabled, easy prompts may still get a direct answer, and this node automatically disables thinking when there is not enough output budget left for useful reasoning. For non-Qwen models (Gemma, LLaMA) this is advisory."}),
"hf_token": ("STRING", {"default": "", "multiline": False, "tooltip": TOOLTIPS["hf_token"]}),
},
"optional": {
"image": ("IMAGE",),
@@ -1834,10 +1995,11 @@ class ThinkingLLM_QwenVL_Advanced(QwenVLBase):
FUNCTION = "process"
CATEGORY = "ThinkingLLM"
def process(self, model_name, attention_mode, use_torch_compile, device, preset_prompt, custom_prompt, max_tokens, temperature, top_p, num_beams, repetition_penalty, frame_count, keep_model_loaded, seed, keep_last_prompt, image=None, video=None, unique_id=None, extra_pnginfo=None, stream_tokens_to_terminal=False, enable_thinking=True):
def process(self, model_name, attention_mode, use_torch_compile, device, preset_prompt, custom_prompt, max_tokens, temperature, top_p, num_beams, repetition_penalty, frame_count, keep_model_loaded, seed, keep_last_prompt, image=None, video=None, unique_id=None, extra_pnginfo=None, stream_tokens_to_terminal=False, enable_thinking=True, hf_token=""):
# Always use FP16 - dropdown removed but keep working logic
quantization = Quantization.FP16.value
result = self.run(model_name, quantization, preset_prompt, custom_prompt, image, video, frame_count, max_tokens, temperature, top_p, num_beams, repetition_penalty, seed, keep_model_loaded, attention_mode, use_torch_compile, device, keep_last_prompt, unique_id=unique_id, extra_pnginfo=extra_pnginfo, node_class="ThinkingLLM_QwenVL_Advanced", stream_to_terminal=stream_tokens_to_terminal, enable_thinking=enable_thinking)
result = self.run(model_name, quantization, preset_prompt, custom_prompt, image, video, frame_count, max_tokens, temperature, top_p, num_beams, repetition_penalty, seed, keep_model_loaded, attention_mode, use_torch_compile, device, keep_last_prompt, unique_id=unique_id, extra_pnginfo=extra_pnginfo, node_class="ThinkingLLM_QwenVL_Advanced", stream_to_terminal=stream_tokens_to_terminal, enable_thinking=enable_thinking, hf_token=hf_token)
hf_token = ""
# Read back raw_trace from just-saved per-node state
key = _make_node_state_key("ThinkingLLM_QwenVL_Advanced", unique_id, extra_pnginfo)
entry = NODE_PROMPT_STATE.get(key, {})
+1058 -154
View File
File diff suppressed because it is too large Load Diff
+239 -113
View File
@@ -27,7 +27,6 @@ import torch
from huggingface_hub import snapshot_download
from AILab_LlamaCppInstaller import ensure_llama_cpp_backend
import folder_paths
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output, prompt_output_guard
from comfy.model_management import throw_exception_if_processing_interrupted
from comfy.model_management import throw_exception_if_processing_interrupted
@@ -35,7 +34,13 @@ from comfy.model_management import throw_exception_if_processing_interrupted
# Import cache functions from main module
import sys
sys.path.append(str(Path(__file__).parent))
from AILab_StreamDisplay import TerminalStreamDisplay
from AILab_StreamDisplay import (
StreamDegenerationError,
StreamDegenerationGuard,
TerminalStreamDisplay,
extract_stream_token,
strip_degenerate_repetition,
)
from AILab_QwenVL import (
PROMPT_CACHE,
apply_qwen_soft_thinking_directive,
@@ -54,7 +59,36 @@ from AILab_QwenVL import (
_make_node_state_key,
_build_workflow_fingerprint,
)
from AILab_QwenVL_GGUF import read_gguf_architecture, register_active_gguf_loader, release_other_gguf_loaders
from AILab_QwenVL_GGUF import (
construct_llama_safely,
read_gguf_architecture,
register_active_gguf_loader,
release_other_gguf_loaders,
_filter_kwargs_for_callable,
_find_existing_local_file_in_dirs,
_gguf_search_dirs,
)
GGUF_PROMPT_TOOLTIPS = {
"model_name": "GGUF text model from config or auto-detected local files. First run downloads the selected GGUF when it is not already on disk.",
"prompt_text": "Prompt text to enhance. Leave blank to emit the selected preset instruction as the base prompt.",
"preset_system_prompt": "Preset enhancement style. Use Custom Only when you want custom_system_prompt to fully control the instruction.",
"custom_system_prompt": "Optional extra instruction. Required when using Custom Only; otherwise it is prepended to the selected style.",
"max_tokens": "Maximum new tokens for the enhanced prompt. Increase only when the model truncates useful detail.",
"temperature": "Sampling randomness. Lower is more stable; higher is more varied.",
"top_p": "Nucleus sampling cutoff. Lower values restrict token choice; 0.9 is a balanced default.",
"repetition_penalty": "Values above 1.0 reduce repeated phrases in the enhanced prompt.",
"english_output": "Ask the model to return the final enhanced prompt in English.",
"device": "auto prefers GPU when available. If generation is unexpectedly slow, run tools/check_llama_backend.py to verify llama.cpp GPU offload.",
"keep_model_loaded": "Keep the GGUF model in memory after generation so repeated prompt enhancement skips model loading.",
"seed": "Sampling seed. Reusing it with identical inputs can reuse the saved prompt result.",
"keep_last_prompt": "Return the saved per-node prompt instead of generating a new one when available.",
"stream_tokens_to_terminal": "Show clean wrapped generated tokens in the ComfyUI terminal. Streaming bypasses fixed-seed reuse for a fresh run.",
"enable_thinking": "Enable model reasoning/thinking when supported. The node still returns the cleaned final prompt, so reasoning may be hidden or empty.",
"auto_finalization_retry": "If enabled, runs an extra LLM completion when the first output is empty or reasoning-only. Disabled by default so one node execution performs one generation pass.",
"hf_token": "Optional Hugging Face access token for private or gated GGUF downloads. It is passed only to the download call, never logged or cached, and the in-memory copy is dropped after the download attempt. Clear this field before saving or sharing workflows.",
}
def _parse_repo_quant_sizes(repo_key: str) -> dict[str, str] | None:
@@ -95,6 +129,28 @@ _EMPTY_THINK_RE = re.compile(r"<think[^>]*>\s*</think>", flags=re.IGNORECASE | r
_STREAM_HEARTBEAT_INTERVAL_SECONDS = 3.0
_PROMPT_ENHANCER_MAX_FINALIZATION_ATTEMPTS = 3
_STREAM_POLL_INTERVAL_SECONDS = 0.25
_PROMPT_ENHANCER_MIN_CONTEXT_LENGTH = 4096
_PROMPT_ENHANCER_MAX_CONTEXT_LENGTH = 32768
def _resolve_prompt_enhancer_context_length(config_context_length, prompt_tokens, max_tokens) -> int:
try:
configured = int(config_context_length)
except Exception:
configured = _PROMPT_ENHANCER_MAX_CONTEXT_LENGTH
if configured <= 0:
configured = _PROMPT_ENHANCER_MAX_CONTEXT_LENGTH
try:
prompt_budget = max(int(prompt_tokens), 0)
except Exception:
prompt_budget = 0
try:
generation_budget = max(int(max_tokens), 0)
except Exception:
generation_budget = 0
requested = max(_PROMPT_ENHANCER_MIN_CONTEXT_LENGTH, prompt_budget + generation_budget + 1024)
cap = min(configured, _PROMPT_ENHANCER_MAX_CONTEXT_LENGTH)
return max(1024, min(cap, requested))
def _looks_like_prompt_planning(text: str) -> bool:
@@ -242,13 +298,6 @@ def _safe_dirname(value: str) -> str:
return "".join(ch for ch in value if ch.isalnum() or ch in "._- ").strip() or "unknown"
def _resolve_base_dir(base_dir_value: str) -> Path:
base_dir = Path(base_dir_value)
if base_dir.is_absolute():
return base_dir
return Path(folder_paths.models_dir) / base_dir
def _model_name_to_filename_candidates(model_name: str) -> set[str]:
raw = (model_name or "").strip()
if not raw:
@@ -263,18 +312,32 @@ def _model_name_to_filename_candidates(model_name: str) -> set[str]:
return candidates
def _find_existing_local_file(base_dir: Path, filename: str) -> Path | None:
"""Reuse an already-downloaded GGUF file anywhere under the shared base dir."""
wanted = Path(filename).name
if not wanted:
return None
try:
for candidate in base_dir.rglob(wanted):
if candidate.is_file():
return candidate
except Exception:
return None
return None
def _as_filename_list(value) -> list[str]:
if not value:
return []
if isinstance(value, str):
values = [value]
elif isinstance(value, (list, tuple, set)):
values = list(value)
else:
return []
filenames: list[str] = []
for item in values:
name = Path(str(item)).name
if name and name not in filenames:
filenames.append(name)
return filenames
def _filename_aliases_for(payload, key: str | None) -> list[str]:
if not isinstance(payload, dict) or not key:
return []
return _as_filename_list(payload.get(key) or payload.get(Path(key).name))
def _filename_search_candidates(filename: str, aliases: list[str] | None = None) -> list[str]:
return _as_filename_list([filename, *(aliases or [])])
class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
@@ -362,6 +425,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
defaults: dict = _defaults_raw if isinstance(_defaults_raw, dict) else {}
repo_id = repo.get("repo_id")
alt_repo_ids = repo.get("alt_repo_ids") or []
local_filenames = repo.get("local_filenames") or repo.get("local_model_files") or {}
model_files = repo.get("model_files") or []
quant_sizes = _parse_repo_quant_sizes(repo_key)
for model_file in model_files:
@@ -382,6 +446,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
"repo_id": repo_id,
"alt_repo_ids": alt_repo_ids,
"filename": model_file,
"local_filenames": _filename_aliases_for(local_filenames, model_file),
}
)
models[display] = entry
@@ -389,19 +454,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
# Scan filesystem for locally available models not in JSON config
# Collect all directories to scan: the configured base_dir + any extra LLM paths from ComfyUI
existing_filenames = {Path(e.get("filename", "")).name for e in models.values() if e.get("filename")}
scan_dirs: list[Path] = [_resolve_base_dir(base_dir)]
try:
if "LLM" in folder_paths.folder_names_and_paths:
for llm_path in folder_paths.get_folder_paths("LLM"):
gguf_dir = Path(llm_path) / "GGUF"
if gguf_dir not in scan_dirs:
scan_dirs.append(gguf_dir)
llm_p = Path(llm_path)
if llm_p not in scan_dirs:
scan_dirs.append(llm_p)
except Exception:
pass
for scan_dir in scan_dirs:
for scan_dir in _gguf_search_dirs(base_dir):
local_models = ThinkingLLM_QwenVL_GGUF_PromptEnhancer._scan_local_gguf_text_models(scan_dir, existing_filenames)
models.update(local_models)
existing_filenames.update(Path(e.get("filename", "")).name for e in local_models.values() if e.get("filename"))
@@ -418,21 +471,23 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
default_model = model_keys[0]
return {
"required": {
"model_name": (model_keys, {"default": default_model, "tooltip": "GGUF model from config or auto-detected from models/LLM/GGUF directory. [local] prefix = found on disk."}),
"prompt_text": ("STRING", {"default": "", "multiline": True, "tooltip": "Prompt text to enhance. Leave blank to just emit the preset instruction."}),
"preset_system_prompt": (styles, {"default": default_style}),
"custom_system_prompt": ("STRING", {"default": "", "multiline": True}),
"max_tokens": ("INT", {"default": 1024, "min": 32, "max": 16384}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0.1, "max": 1.0}),
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0}),
"repetition_penalty": ("FLOAT", {"default": 1.1, "min": 0.5, "max": 2.0}),
"english_output": ("BOOLEAN", {"default": False, "tooltip": "Force final output in English using translation prompt."}),
"device": (["auto", "cuda", "cpu", "mps"], {"default": "auto", "tooltip": "Select device; auto prefers GPU when available."}),
"keep_model_loaded": ("BOOLEAN", {"default": False, "tooltip": "Keep model loaded in memory for faster repeated inference (uses more VRAM)."}),
"seed": ("INT", {"default": 1, "min": 1, "max": 2**32 - 1}),
"keep_last_prompt": ("BOOLEAN", {"default": False, "tooltip": "Keep the last generated prompt instead of creating a new one"}),
"stream_tokens_to_terminal": ("BOOLEAN", {"default": False, "tooltip": "Show readable generation progress and thinking-status summaries in the ComfyUI terminal. When enabled, fixed-seed and prompt-cache reuse are bypassed so a fresh streamed run can occur."}),
"enable_thinking": ("BOOLEAN", {"default": True, "tooltip": "Enable model reasoning/thinking when the backend supports it: True=allow thinking, False=force direct answer. Even when enabled, easy prompts may still get a direct answer, and this node automatically disables thinking when there is not enough output budget left for useful reasoning. Prompt enhancers still return a cleaned final prompt, so terminal reasoning may be hidden or empty."}),
"model_name": (model_keys, {"default": default_model, "tooltip": GGUF_PROMPT_TOOLTIPS["model_name"]}),
"prompt_text": ("STRING", {"default": "", "multiline": True, "tooltip": GGUF_PROMPT_TOOLTIPS["prompt_text"]}),
"preset_system_prompt": (styles, {"default": default_style, "tooltip": GGUF_PROMPT_TOOLTIPS["preset_system_prompt"]}),
"custom_system_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": GGUF_PROMPT_TOOLTIPS["custom_system_prompt"]}),
"max_tokens": ("INT", {"default": 1024, "min": 32, "max": 16384, "tooltip": GGUF_PROMPT_TOOLTIPS["max_tokens"]}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0.1, "max": 1.0, "tooltip": GGUF_PROMPT_TOOLTIPS["temperature"]}),
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "tooltip": GGUF_PROMPT_TOOLTIPS["top_p"]}),
"repetition_penalty": ("FLOAT", {"default": 1.1, "min": 0.5, "max": 2.0, "tooltip": GGUF_PROMPT_TOOLTIPS["repetition_penalty"]}),
"english_output": ("BOOLEAN", {"default": False, "tooltip": GGUF_PROMPT_TOOLTIPS["english_output"]}),
"device": (["auto", "cuda", "cpu", "mps"], {"default": "auto", "tooltip": GGUF_PROMPT_TOOLTIPS["device"]}),
"keep_model_loaded": ("BOOLEAN", {"default": False, "tooltip": GGUF_PROMPT_TOOLTIPS["keep_model_loaded"]}),
"seed": ("INT", {"default": 1, "min": 1, "max": 2**32 - 1, "tooltip": GGUF_PROMPT_TOOLTIPS["seed"]}),
"keep_last_prompt": ("BOOLEAN", {"default": False, "tooltip": GGUF_PROMPT_TOOLTIPS["keep_last_prompt"]}),
"stream_tokens_to_terminal": ("BOOLEAN", {"default": False, "tooltip": GGUF_PROMPT_TOOLTIPS["stream_tokens_to_terminal"]}),
"enable_thinking": ("BOOLEAN", {"default": True, "tooltip": GGUF_PROMPT_TOOLTIPS["enable_thinking"]}),
"auto_finalization_retry": ("BOOLEAN", {"default": False, "tooltip": GGUF_PROMPT_TOOLTIPS["auto_finalization_retry"]}),
"hf_token": ("STRING", {"default": "", "multiline": False, "tooltip": GGUF_PROMPT_TOOLTIPS["hf_token"]}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
@@ -440,8 +495,9 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
},
}
def clear(self):
print(f"[QwenVL PromptEnhancer DEBUG] Starting VRAM cleanup...")
def clear(self, quiet: bool = False):
if not quiet:
print(f"[QwenVL PromptEnhancer DEBUG] Starting VRAM cleanup...")
# Force cleanup of LLM model
if self.llm is not None:
@@ -454,7 +510,8 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
# Force garbage collection of the model
del self.llm
except Exception as e:
print(f"[QwenVL PromptEnhancer DEBUG] Error closing LLM: {e}")
if not quiet:
print(f"[QwenVL PromptEnhancer DEBUG] Error closing LLM: {e}")
finally:
self.llm = None
@@ -466,7 +523,8 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
# Force CUDA cache cleanup multiple times
if torch.cuda.is_available():
print(f"[QwenVL PromptEnhancer DEBUG] Clearing CUDA cache...")
if not quiet:
print(f"[QwenVL PromptEnhancer DEBUG] Clearing CUDA cache...")
torch.cuda.empty_cache()
try:
torch.cuda.ipc_collect()
@@ -476,7 +534,20 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
# Additional cleanup
torch.cuda.empty_cache()
print(f"[QwenVL PromptEnhancer DEBUG] VRAM cleanup completed")
if not quiet:
print(f"[QwenVL PromptEnhancer DEBUG] VRAM cleanup completed")
@staticmethod
def _find_existing_local_file_in_dirs(search_dirs: list[Path], filename: str, aliases: list[str] | None = None) -> Path | None:
for search_dir in search_dirs:
for wanted in _filename_search_candidates(filename, aliases):
try:
for candidate in search_dir.rglob(wanted):
if candidate.is_file():
return candidate
except Exception:
continue
return None
def _resolve_model_path(self, model_name):
models = self.gguf_models.get("models") or {}
@@ -496,7 +567,8 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
if filename and Path(filename).is_absolute():
return Path(filename)
base_dir = _resolve_base_dir(self.gguf_models.get("base_dir") or "LLM/GGUF")
search_dirs = _gguf_search_dirs(self.gguf_models.get("base_dir") or "LLM/GGUF")
base_dir = search_dirs[0]
path = entry.get("path")
if path:
@@ -511,7 +583,11 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
target = base_dir / repo_dir / Path(filename).name
if target.exists():
return target
existing = _find_existing_local_file(base_dir, filename)
existing = self._find_existing_local_file_in_dirs(
search_dirs,
filename,
_as_filename_list(entry.get("local_filenames") or entry.get("local_model_files")),
)
if existing is not None:
target.parent.mkdir(parents=True, exist_ok=True)
try:
@@ -523,7 +599,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
return base_dir / model_name
def _maybe_download_model(self, model_name, resolved, unique_id=None):
def _maybe_download_model(self, model_name, resolved, unique_id=None, hf_token: str | None = None):
if resolved.exists():
return
# Local models should already exist on disk — don't attempt download
@@ -556,20 +632,33 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
resolved,
node_id=unique_id,
progress_label=f"QwenVL PromptEnhancer GGUF Download: {Path(filename).name}",
hf_token=hf_token,
)
except Exception as exc:
print(f"[QwenVL] Download failed from {repo_id}: {exc}")
if resolved.exists():
break
hf_token = None
if not resolved.exists():
raise FileNotFoundError(f"[QwenVL] GGUF model not found after download: {resolved} (tried: {', '.join(attempted)})")
def _load_model(self, model_name, device, enable_thinking=True, unique_id=None):
def _load_model(self, model_name, device, enable_thinking=True, unique_id=None, hf_token: str | None = None, context_length_override: int | None = None, quiet: bool = False):
Llama = self._load_backend()
resolved = self._resolve_model_path(model_name)
self._maybe_download_model(model_name, resolved, unique_id=unique_id)
self._maybe_download_model(model_name, resolved, unique_id=unique_id, hf_token=hf_token)
hf_token = None
model_cfg = self.gguf_models["models"].get(model_name, {})
context_length = model_cfg.get("context_length", 32768)
raw_context_length = model_cfg.get("context_length", 32768)
context_length = int(context_length_override or raw_context_length or 32768)
try:
raw_context_int = int(raw_context_length)
except Exception:
raw_context_int = context_length
if context_length < raw_context_int and not quiet:
print(
f"[QwenVL PromptEnhancer GGUF] Using context length {context_length} "
f"instead of catalog {raw_context_int} to reduce KV-cache VRAM."
)
signature = (resolved, context_length, device, bool(enable_thinking))
if self.llm is not None and self.current_signature == signature:
ensure_cuda_vram_headroom("QwenVL PromptEnhancer GGUF", min_free_gb=1.0, min_free_ratio=0.08)
@@ -578,8 +667,9 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
release_other_gguf_loaders(self, resolved.name)
# Force aggressive cleanup before loading new model (especially for same model conflicts)
print(f"[QwenVL PromptEnhancer DEBUG] Forcing cleanup before model loading...")
self.clear()
if not quiet:
print(f"[QwenVL PromptEnhancer DEBUG] Forcing cleanup before model loading...")
self.clear(quiet=quiet)
# Additional wait for CUDA cleanup
if torch.cuda.is_available():
@@ -588,7 +678,8 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
resolved.parent.mkdir(parents=True, exist_ok=True)
if not resolved.exists():
raise FileNotFoundError(f"[QwenVL] GGUF model not found: {resolved}")
print(f"[QwenVL] Loading GGUF model from {resolved}")
if not quiet:
print(f"[QwenVL] Loading GGUF model from {resolved}")
if device == "auto":
device_choice = "cuda" if torch.cuda.is_available() else ("mps" if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available() else "cpu")
else:
@@ -617,7 +708,8 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
thinking_state = bool(enable_thinking)
kwargs["chat_template_kwargs"] = {"enable_thinking": thinking_state}
state_label = "enabled" if thinking_state else "disabled"
print(f"[QwenVL] Qwen architecture detected (arch={arch}): Thinking {state_label} via chat template.")
if not quiet:
print(f"[QwenVL] Qwen architecture detected (arch={arch}): Thinking {state_label} via chat template.")
else:
thinking_state = bool(enable_thinking)
if not thinking_state:
@@ -631,7 +723,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
else:
print(f"[QwenVL] Non-Qwen architecture (arch={arch}): Thinking ON (advisory — backend may ignore).")
self.llm = Llama(**kwargs)
self.llm = construct_llama_safely(Llama, kwargs, "QwenVL PromptEnhancer GGUF")
self.current_signature = signature
def _invoke_llama(
@@ -646,13 +738,15 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
stream_to_terminal=False,
initial_stage_label="INITIAL GENERATION",
enable_thinking=True,
auto_finalization_retry=False,
):
"""Returns (cleaned_prompt, raw_trace) tuple."""
stream_display = TerminalStreamDisplay("QwenVL GGUF", suppress_planning=True, compact=False) if stream_to_terminal else None
stream_display = TerminalStreamDisplay("QwenVL GGUF", suppress_planning=True, compact=True) if stream_to_terminal else None
supports_soft_think = getattr(self, "supports_qwen_soft_think", False)
if supports_soft_think:
directive = "/think" if enable_thinking else "/no_think"
print(f"[QwenVL] Qwen3 GGUF prompt enhancer: Thinking {'enabled' if enable_thinking else 'disabled'} via chat template and {directive}.")
if not stream_to_terminal:
print(f"[QwenVL] Qwen3 GGUF prompt enhancer: Thinking {'enabled' if enable_thinking else 'disabled'} via chat template and {directive}.")
def _call(
system: str,
@@ -688,25 +782,35 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
if stream_to_terminal:
if stream_display is None:
raise RuntimeError("[QwenVL] Stream display was not initialized")
print(f"[QwenVL PromptEnhancer GGUF] STREAMING {stage_label}")
stream_display.start_stage(stage_label)
abort_requested = threading.Event()
abort_callback_refs = _install_llama_abort_callback(llm, abort_requested.is_set)
result_queue: queue.Queue[tuple[str, dict | BaseException | None]] = queue.Queue()
degeneration_guard = StreamDegenerationGuard()
stopped_for_degenerate_stream = ""
def _stream_worker():
try:
response = llm.create_chat_completion(
messages=messages,
max_tokens=max_tokens,
temperature=temp,
top_p=top_p,
repeat_penalty=repetition_penalty,
seed=seed_val,
stop=["<|im_end|>", "<|im_start|>"],
stream=True,
)
completion_kwargs = {
"messages": messages,
"max_tokens": max_tokens,
"temperature": temp,
"top_p": top_p,
"top_k": 60,
"min_p": 0.01,
"repeat_penalty": repetition_penalty,
"repeat_last_n": 48,
"presence_penalty": max(0.0, min(1.5, float(repetition_penalty) - 1.0 + 1.2)),
"frequency_penalty": 0.2,
"seed": seed_val,
"stop": ["<|im_end|>", "<|im_start|>"],
"stream": True,
}
completion_kwargs = _filter_kwargs_for_callable(getattr(llm, "create_chat_completion"), completion_kwargs)
response = llm.create_chat_completion(**completion_kwargs)
for chunk in response:
if abort_requested.is_set():
return
result_queue.put(("chunk", chunk))
result_queue.put(("done", None))
except BaseException as exc:
@@ -724,28 +828,32 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
except Exception:
abort_requested.set()
raise
if full_text:
if stream_display is not None:
last_status_at = _maybe_emit_prompt_stream_heartbeat(stage_label, stage_started_at, last_status_at, full_text)
else:
if stream_display is None:
if full_text:
last_status_at = _maybe_emit_prompt_background_heartbeat(stage_label, stage_started_at, last_status_at, full_text)
else:
last_status_at = _maybe_emit_prompt_waiting_heartbeat(stage_label, stage_started_at, last_status_at)
else:
last_status_at = _maybe_emit_prompt_waiting_heartbeat(stage_label, stage_started_at, last_status_at)
continue
if kind == "chunk":
throw_exception_if_processing_interrupted()
chunk = payload if isinstance(payload, dict) else {}
token = (
chunk.get("choices", [{}])[0]
.get("delta", {})
.get("content", "")
)
if token:
full_text += token
if stream_display is not None:
stream_display.push(token)
last_status_at = _maybe_emit_prompt_stream_heartbeat(stage_label, stage_started_at, last_status_at, full_text)
token = extract_stream_token(chunk)
reasoning_token = token.get("reasoning", "")
content_token = token.get("content", "")
display_token = reasoning_token + content_token
if reasoning_token:
full_text += reasoning_token
if content_token:
full_text += content_token
if display_token and stream_display is not None:
try:
degeneration_guard.push(display_token)
except StreamDegenerationError as exc:
stopped_for_degenerate_stream = exc.reason
abort_requested.set()
break
stream_display.push_compact(display_token)
continue
if kind == "done":
@@ -762,10 +870,13 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
_, _, clear_abort_callback = abort_callback_refs
clear_abort_callback()
if stream_display is not None:
stream_display.end_compact()
stream_display.end_stage()
if not full_text.strip():
raise RuntimeError("[QwenVL] llama_cpp streaming returned empty response")
return full_text.strip()
if stopped_for_degenerate_stream:
print(f"[QwenVL GGUF] stopped repeated-token loop: {stopped_for_degenerate_stream}")
return strip_degenerate_repetition(full_text).strip()
raw = _call(system_prompt, user_prompt, float(temperature), int(seed), initial_stage_label)
cleaned = clean_model_output(raw, OutputCleanConfig(mode="prompt"))
@@ -773,6 +884,8 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
best_cleaned = cleaned.strip()
if _prompt_output_is_usable(best_cleaned):
return best_cleaned, "\n\n".join(raw_trace_parts)
if not auto_finalization_retry:
return best_cleaned or "", "\n\n".join(raw_trace_parts)
current_raw = raw
for attempt_number in range(2, _PROMPT_ENHANCER_MAX_FINALIZATION_ATTEMPTS + 1):
@@ -837,6 +950,8 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
unique_id=None,
extra_pnginfo=None,
enable_thinking=True,
auto_finalization_retry=False,
hf_token="",
):
node_class = "ThinkingLLM_QwenVL_GGUF_PromptEnhancer"
input_signature = build_node_input_signature(
@@ -847,6 +962,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
english_output=bool(english_output),
device=device,
enable_thinking=bool(enable_thinking),
auto_finalization_retry=bool(auto_finalization_retry),
max_tokens=max_tokens,
temperature=temperature,
top_p=top_p,
@@ -859,13 +975,14 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
print(f"[QwenVL PromptEnhancer GGUF] Fixed seed {seed} matched — using per-node prompt: {saved_prompt[:50]}...")
return (saved_prompt, "")
if saved_prompt and stream_tokens_to_terminal:
print("[QwenVL PromptEnhancer GGUF] Streaming requested — bypassing fixed-seed prompt reuse for a fresh streamed run")
pass
if keep_last_prompt:
print(f"[QwenVL PromptEnhancer GGUF] Keep last prompt enabled but no saved prompt found — returning empty")
return ("", "")
# Always generate unless keep_last_prompt was requested
print(f"[QwenVL PromptEnhancer GGUF] Generating new prompt")
if not stream_tokens_to_terminal:
print(f"[QwenVL PromptEnhancer GGUF] Generating new prompt")
# Generate cache key with all inputs including seed
cache_prompt = "\n\n".join(
@@ -875,7 +992,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
f"english_output={bool(english_output)}",
) if part
)
cache_key = get_cache_key(model_name, preset_system_prompt, cache_prompt, seed=seed, max_tokens=max_tokens, temperature=temperature, top_p=top_p, repetition_penalty=repetition_penalty)
cache_key = get_cache_key(model_name, preset_system_prompt, cache_prompt, seed=seed, max_tokens=max_tokens, temperature=temperature, top_p=top_p, repetition_penalty=repetition_penalty, enable_thinking=enable_thinking)
# Check cache first (only for random mode)
if cache_key in PROMPT_CACHE and not stream_tokens_to_terminal:
@@ -886,7 +1003,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
if stream_tokens_to_terminal and cache_key in PROMPT_CACHE:
cached_text = PROMPT_CACHE[cache_key].get("text", "")
if cached_text:
print("[QwenVL PromptEnhancer GGUF] Streaming requested — bypassing prompt cache for a fresh streamed run")
pass
is_custom_only = preset_system_prompt == CUSTOM_ONLY_STYLE
style_entry = {} if is_custom_only else self.styles.get(preset_system_prompt, {})
@@ -902,16 +1019,25 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
model_cfg = self.gguf_models["models"].get(model_name, {})
context_window = model_cfg.get("context_length", 32768)
estimated_prompt_tokens = estimate_qwen_text_tokens(system_prompt, merged_prompt)
effective_context_window = _resolve_prompt_enhancer_context_length(context_window, estimated_prompt_tokens, max_tokens)
effective_thinking = resolve_qwen_thinking_mode(
enable_thinking,
max_tokens,
label="QwenVL PromptEnhancer GGUF",
prompt_tokens=estimated_prompt_tokens,
context_window=context_window,
context_window=effective_context_window,
quiet=stream_tokens_to_terminal,
)
if stream_tokens_to_terminal:
print("[QwenVL GGUF] Prompt enhancer terminal stream shows readable progress only; reasoning text may be hidden or stripped from the final prompt.")
self._load_model(model_name, device, enable_thinking=effective_thinking, unique_id=unique_id)
self._load_model(
model_name,
device,
enable_thinking=effective_thinking,
unique_id=unique_id,
hf_token=hf_token,
context_length_override=effective_context_window,
quiet=stream_tokens_to_terminal,
)
hf_token = ""
enhanced, raw_trace = self._invoke_llama(
system_prompt=system_prompt,
user_prompt=merged_prompt,
@@ -923,12 +1049,8 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
stream_to_terminal=stream_tokens_to_terminal,
initial_stage_label="INITIAL GENERATION",
enable_thinking=effective_thinking,
auto_finalization_retry=auto_finalization_retry,
)
if stream_tokens_to_terminal:
print(
f"[QwenVL GGUF] Thinking status: "
f"{_describe_prompt_enhancer_thinking(bool(enable_thinking), effective_thinking, raw_trace, retried='[FINALIZATION ATTEMPT ' in raw_trace)}"
)
full_raw_trace = raw_trace
if english_output:
translated, trans_trace = self._invoke_llama(
@@ -946,6 +1068,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
stream_to_terminal=stream_tokens_to_terminal,
initial_stage_label="TRANSLATION STAGE",
enable_thinking=effective_thinking,
auto_finalization_retry=auto_finalization_retry,
)
full_raw_trace = f"{raw_trace}\n\n{trans_trace}"
final = clean_model_output(translated, OutputCleanConfig(mode="prompt")) or translated.strip()
@@ -964,18 +1087,21 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
}
save_prompt_cache()
print(f"[QwenVL PromptEnhancer GGUF] Cached new prompt for seed {seed}: {cache_key[:8]}...")
if not stream_tokens_to_terminal:
print(f"[QwenVL PromptEnhancer GGUF] Cached new prompt for seed {seed}: {cache_key[:8]}...")
try:
# Persist per-node for future keep_last_prompt=True
set_node_saved_prompt(node_class, unique_id, extra_pnginfo, final, raw_trace=full_raw_trace, seed=seed, max_tokens=max_tokens, temperature=temperature, top_p=top_p, repetition_penalty=repetition_penalty, input_signature=input_signature)
print(f"[QwenVL PromptEnhancer GGUF] Saved per-node prompt: {final[:50]}...")
if not stream_tokens_to_terminal:
print(f"[QwenVL PromptEnhancer GGUF] Saved per-node prompt: {final[:50]}...")
return (final, full_raw_trace)
finally:
if not keep_model_loaded:
self.clear()
print(f"[QwenVL PromptEnhancer GGUF] keep_model_loaded=False — cleaning up model...")
self.clear(quiet=stream_tokens_to_terminal)
if not stream_tokens_to_terminal:
print(f"[QwenVL PromptEnhancer GGUF] keep_model_loaded=False — cleaning up model...")
@staticmethod
def _is_english(text):
+49 -31
View File
@@ -30,6 +30,7 @@ from AILab_QwenVL import (
HF_VL_MODELS,
NODE_PROMPT_STATE,
PROMPT_CACHE,
_default_model_from_config,
_make_node_state_key,
apply_qwen_soft_thinking_directive,
build_node_input_signature,
@@ -124,6 +125,18 @@ def _load_prompt_styles() -> dict[str, str]:
PROMPT_STYLES = _load_prompt_styles()
PROMPT_STYLES = {CUSTOM_ONLY_STYLE: "", **PROMPT_STYLES}
PROMPT_ENHANCER_TOOLTIPS = {
"prompt_text": "Prompt text to enhance. Leave blank to emit the selected preset instruction as the base prompt.",
"enhancement_style": "Preset enhancement style. Use Custom Only when you want custom_system_prompt to fully control the instruction.",
"custom_system_prompt": "Optional extra instruction. Required when using Custom Only; otherwise it is prepended to the selected style.",
"max_tokens": "Maximum new tokens for the enhanced prompt. Increase only when the model truncates useful detail.",
"temperature": "Sampling randomness. Lower is more stable; higher is more varied.",
"top_p": "Nucleus sampling cutoff. Lower values restrict token choice; 0.9 is a balanced default.",
"repetition_penalty": "Values above 1.0 reduce repeated phrases in the enhanced prompt.",
"keep_model_loaded": "Keep the HF model in memory after generation so repeated prompt enhancement skips model loading.",
"seed": "Sampling seed. Reusing it with identical inputs can reuse the saved prompt result.",
}
class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
STYLES = PROMPT_STYLES
@@ -143,7 +156,7 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
@classmethod
def INPUT_TYPES(cls):
models = list(HF_ALL_MODELS.keys())
default_model = models[0] if models else "Qwen3-VL-4B-Instruct"
default_model = _default_model_from_config(HF_ALL_MODELS, "Qwen3-VL-4B-Instruct")
styles = list(cls.STYLES.keys())
preferred_style = "📝 Enhance"
default_style = preferred_style if preferred_style in styles else (styles[0] if styles else "📝 Enhance")
@@ -154,18 +167,19 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
"attention_mode": (ATTENTION_MODES, {"default": "auto", "tooltip": TOOLTIPS["attention_mode"]}),
"use_torch_compile": ("BOOLEAN", {"default": False, "tooltip": TOOLTIPS["use_torch_compile"]}),
"device": (["auto", "cuda", "cpu", "mps"], {"default": "auto", "tooltip": TOOLTIPS["device"]}),
"prompt_text": ("STRING", {"default": "", "multiline": True, "tooltip": "Prompt text to enhance. Leave blank to just emit the preset instruction."}),
"enhancement_style": (styles, {"default": default_style}),
"custom_system_prompt": ("STRING", {"default": "", "multiline": True}),
"max_tokens": ("INT", {"default": 1024, "min": 32, "max": 16384}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0.1, "max": 1.0}),
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0}),
"repetition_penalty": ("FLOAT", {"default": 1.1, "min": 0.5, "max": 2.0}),
"keep_model_loaded": ("BOOLEAN", {"default": False}),
"seed": ("INT", {"default": 1, "min": 1, "max": 2**32 - 1}),
"prompt_text": ("STRING", {"default": "", "multiline": True, "tooltip": PROMPT_ENHANCER_TOOLTIPS["prompt_text"]}),
"enhancement_style": (styles, {"default": default_style, "tooltip": PROMPT_ENHANCER_TOOLTIPS["enhancement_style"]}),
"custom_system_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": PROMPT_ENHANCER_TOOLTIPS["custom_system_prompt"]}),
"max_tokens": ("INT", {"default": 1024, "min": 32, "max": 16384, "tooltip": PROMPT_ENHANCER_TOOLTIPS["max_tokens"]}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0.1, "max": 1.0, "tooltip": PROMPT_ENHANCER_TOOLTIPS["temperature"]}),
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "tooltip": PROMPT_ENHANCER_TOOLTIPS["top_p"]}),
"repetition_penalty": ("FLOAT", {"default": 1.1, "min": 0.5, "max": 2.0, "tooltip": PROMPT_ENHANCER_TOOLTIPS["repetition_penalty"]}),
"keep_model_loaded": ("BOOLEAN", {"default": False, "tooltip": PROMPT_ENHANCER_TOOLTIPS["keep_model_loaded"]}),
"seed": ("INT", {"default": 1, "min": 1, "max": 2**32 - 1, "tooltip": PROMPT_ENHANCER_TOOLTIPS["seed"]}),
"keep_last_prompt": ("BOOLEAN", {"default": False, "tooltip": "Keep the last generated prompt instead of creating a new one"}),
"stream_tokens_to_terminal": ("BOOLEAN", {"default": False, "tooltip": "Show readable generation progress and thinking-status summaries in the ComfyUI terminal. When enabled, fixed-seed prompt reuse is bypassed so a fresh streamed run can occur."}),
"stream_tokens_to_terminal": ("BOOLEAN", {"default": False, "tooltip": "Show clean wrapped generated tokens in the ComfyUI terminal. When enabled, fixed-seed prompt reuse is bypassed so a fresh streamed run can occur."}),
"enable_thinking": ("BOOLEAN", {"default": True, "tooltip": "Enable model reasoning/thinking when the backend supports it: True=allow thinking, False=force direct answer. Even when enabled, easy prompts may still get a direct answer, and this node automatically disables thinking when there is not enough output budget left for useful reasoning. Prompt enhancers still return a cleaned final prompt, so terminal reasoning may be hidden or empty."}),
"hf_token": ("STRING", {"default": "", "multiline": False, "tooltip": TOOLTIPS["hf_token"]}),
}
,
"hidden": {
@@ -195,6 +209,7 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
unique_id=None,
extra_pnginfo=None,
enable_thinking=True,
hf_token="",
):
node_class = "ThinkingLLM_QwenVL_PromptEnhancer"
input_signature = build_node_input_signature(
@@ -219,13 +234,14 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
print(f"[QwenVL PromptEnhancer HF] Fixed seed {seed} matched — using per-node prompt: {saved_prompt[:50]}...")
return (saved_prompt, "")
if saved_prompt and stream_tokens_to_terminal:
print("[QwenVL PromptEnhancer HF] Streaming requested — bypassing fixed-seed prompt reuse for a fresh streamed run")
pass
if keep_last_prompt:
print(f"[QwenVL PromptEnhancer HF] Keep last prompt enabled but no saved prompt found — returning empty")
return ("", "")
# Always generate unless keep_last_prompt was requested
print(f"[QwenVL PromptEnhancer HF] Generating new prompt")
if not stream_tokens_to_terminal:
print(f"[QwenVL PromptEnhancer HF] Generating new prompt")
is_custom_only = enhancement_style == CUSTOM_ONLY_STYLE
style_instruction = "" if is_custom_only else self.STYLES.get(
@@ -254,6 +270,7 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
stream_to_terminal=stream_tokens_to_terminal,
unique_id=unique_id,
enable_thinking=enable_thinking,
hf_token=hf_token,
)
raw_trace = f"[HF TEXT GENERATION]\n{enhanced_raw}"
else:
@@ -274,7 +291,9 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
unique_id=unique_id,
extra_pnginfo=extra_pnginfo,
enable_thinking=enable_thinking,
hf_token=hf_token,
)
hf_token = ""
key = _make_node_state_key(node_class, unique_id, extra_pnginfo)
entry = NODE_PROMPT_STATE.get(key, {})
raw_trace = entry.get("raw_trace", "") if isinstance(entry, dict) else ""
@@ -283,7 +302,8 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
# Persist per-node for future keep_last_prompt=True
set_node_saved_prompt(node_class, unique_id, extra_pnginfo, final, raw_trace=raw_trace, seed=seed, max_tokens=max_tokens, temperature=temperature, top_p=top_p, repetition_penalty=repetition_penalty, input_signature=input_signature)
print(f"[QwenVL PromptEnhancer HF] Saved per-node prompt: {final[:50]}...")
if not stream_tokens_to_terminal:
print(f"[QwenVL PromptEnhancer HF] Saved per-node prompt: {final[:50]}...")
return (final, raw_trace)
@@ -305,6 +325,7 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
unique_id=None,
extra_pnginfo=None,
enable_thinking=True,
hf_token: str | None = None,
):
output = self.run(
model_name=model_name,
@@ -329,10 +350,11 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
node_class="ThinkingLLM_QwenVL_PromptEnhancer",
stream_to_terminal=stream_to_terminal,
enable_thinking=enable_thinking,
hf_token=hf_token,
)
return output[0]
def _load_text_model(self, model_name, quantization, device_choice, unique_id=None):
def _load_text_model(self, model_name, quantization, device_choice, unique_id=None, hf_token: str | None = None):
info = HF_TEXT_MODELS.get(model_name, {})
repo_id = info.get("repo_id")
if not repo_id:
@@ -344,6 +366,7 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
require_processor=prefers_processor,
node_id=unique_id,
progress_label=f"QwenVL PromptEnhancer HF Download: {model_name}",
hf_token=hf_token,
)
if device_choice == "auto":
@@ -457,9 +480,11 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
stream_to_terminal=False,
unique_id=None,
enable_thinking=True,
hf_token: str | None = None,
):
"""Returns (cleaned_prompt, raw_generated_text) tuple."""
self._load_text_model(model_name, quantization, device, unique_id=unique_id)
self._load_text_model(model_name, quantization, device, unique_id=unique_id, hf_token=hf_token)
hf_token = None
ensure_cuda_vram_headroom("QwenVL PromptEnhancer HF", min_free_gb=1.0, min_free_ratio=0.08)
if device == "auto":
@@ -499,19 +524,19 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
label="QwenVL PromptEnhancer HF",
prompt_tokens=prompt_tokens,
context_window=context_window,
quiet=stream_to_terminal,
)
if effective_thinking != requested_thinking:
_, inputs = _build_inputs(effective_thinking)
if stream_to_terminal:
print("[QwenVL HF] Prompt enhancer terminal stream shows readable progress only; reasoning text may be hidden or stripped from the final prompt.")
if is_qwen35 or supports_soft_think:
if supports_soft_think:
directive = "/think" if effective_thinking else "/no_think"
print(f"[QwenVL] Qwen3 HF prompt enhancer: Thinking {'enabled' if effective_thinking else 'disabled'} via chat template and {directive}.")
if not stream_to_terminal:
print(f"[QwenVL] Qwen3 HF prompt enhancer: Thinking {'enabled' if effective_thinking else 'disabled'} via chat template and {directive}.")
else:
print(f"[QwenVL] Non-Qwen model in HF text path: thinking toggle is advisory (model may ignore).")
if not stream_to_terminal:
print(f"[QwenVL] Non-Qwen model in HF text path: thinking toggle is advisory (model may ignore).")
gen_kwargs = {
"max_new_tokens": max_tokens,
"repetition_penalty": repetition_penalty,
@@ -533,31 +558,27 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
from transformers import TextIteratorStreamer
from threading import Thread
stage_label = "INITIAL GENERATION"
stream_display = TerminalStreamDisplay("QwenVL PromptEnhancer HF", suppress_planning=True, compact=False)
stream_display = TerminalStreamDisplay("QwenVL PromptEnhancer HF", suppress_planning=True, compact=True)
streamer = TextIteratorStreamer(self.text_tokenizer, skip_prompt=True, skip_special_tokens=True)
gen_kwargs["streamer"] = streamer
# Launch generation in a thread so we can iterate the streamer
import threading
thread = threading.Thread(target=self.text_model.generate, kwargs={**inputs, **gen_kwargs})
print(f"[QwenVL PromptEnhancer HF] STREAMING {stage_label}")
stream_display.start_stage(stage_label)
stage_started_at = time.monotonic()
last_status_at = stage_started_at
thread.start()
full_streamed = ""
for token_str in streamer:
if token_str:
throw_exception_if_processing_interrupted()
full_streamed += token_str
stream_display.push(token_str)
last_status_at = _maybe_emit_hf_prompt_stream_heartbeat(stage_label, stage_started_at, last_status_at, full_streamed)
stream_display.push_compact(token_str)
thread.join()
stream_display.end_compact()
stream_display.end_stage()
if not full_streamed.strip():
raise RuntimeError("[QwenVL] HF streaming returned empty response")
raw_text = full_streamed.strip()
result = clean_model_output(raw_text, OutputCleanConfig(mode="prompt")) or raw_text
print(f"[QwenVL HF] Thinking status: {_describe_prompt_enhancer_thinking(requested_thinking, effective_thinking, raw_text)}")
if not keep_model_loaded:
self.text_model = None
self.text_processor = None
@@ -580,9 +601,6 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
raw_text = self.text_tokenizer.decode(generated_tokens, skip_special_tokens=True).strip()
result = clean_model_output(raw_text, OutputCleanConfig(mode="prompt")) or raw_text
if stream_to_terminal:
print(f"[QwenVL HF] Thinking status: {_describe_prompt_enhancer_thinking(requested_thinking, effective_thinking, raw_text)}")
if not keep_model_loaded:
self.text_model = None
self.text_processor = None
+222 -39
View File
@@ -1,4 +1,5 @@
import re
import shutil
import sys
import time
@@ -31,14 +32,141 @@ def reset_thinking_stream_display():
THINKING_STREAM_DISPLAY = None
# ── Shared chunk normalizer for OpenAI-style streamed responses ────────────
def _get_stream_field(container, key: str, default=None):
if isinstance(container, dict):
return container.get(key, default)
return getattr(container, key, default)
def _stream_value_to_text(value) -> str:
if value is None:
return ""
if isinstance(value, str):
return value
if isinstance(value, (int, float)):
return str(value)
if isinstance(value, list):
return "".join(_stream_value_to_text(part) for part in value)
if isinstance(value, dict):
for key in ("text", "content", "reasoning_content", "reasoning", "thinking"):
text = _stream_value_to_text(value.get(key))
if text:
return text
return ""
return str(value)
def _first_stream_text(containers, keys: tuple[str, ...]) -> str:
for container in containers:
if container is None:
continue
for key in keys:
text = _stream_value_to_text(_get_stream_field(container, key, None))
if text:
return text
return ""
def extract_stream_token(chunk) -> dict[str, str]:
"""Return a dict with 'reasoning' and 'content' keys from an OpenAI-style
streaming delta chunk.
Populates both fields from any supported field name so callers get reasoning
and answer text regardless of backend (llama.cpp, vLLM, HF, etc.).
Empty values are returned as empty strings.
"""
choices = _get_stream_field(chunk, "choices", []) or []
choice = choices[0] if isinstance(choices, (list, tuple)) and choices else {}
delta = _get_stream_field(choice, "delta", {}) or {}
message = _get_stream_field(choice, "message", {}) or {}
containers = (delta, message, choice, chunk)
content = _first_stream_text(containers, ("content", "text", "output_text"))
reasoning = _first_stream_text(
containers,
("reasoning_content", "thinking", "reasoning", "reasoning_text"),
)
return {"reasoning": reasoning, "content": content}
class StreamDegenerationError(RuntimeError):
def __init__(self, reason: str):
super().__init__(reason)
self.reason = reason
class StreamDegenerationGuard:
def __init__(self, repeated_word_limit: int = 8, repeated_ngram_limit: int = 8):
self.repeated_word_limit = max(4, int(repeated_word_limit))
self.repeated_ngram_limit = max(3, int(repeated_ngram_limit))
self._text = ""
self._words: list[str] = []
def push(self, text: str):
if not text:
return
self._text = (self._text + text)[-4096:]
words = [w.casefold() for w in re.findall(r"[A-Za-z][A-Za-z'\-]{2,}", self._text)]
if not words:
return
self._words = words[-256:]
reason = self._detect()
if reason:
raise StreamDegenerationError(reason)
def _detect(self) -> str:
if not self._words:
return ""
last_word = self._words[-1]
if len(last_word) >= 4:
run = 0
for word in reversed(self._words):
if word != last_word:
break
run += 1
if run >= self.repeated_word_limit:
return f"repeated word '{last_word}' {run} times"
for size in range(2, 6):
needed = size * self.repeated_ngram_limit
if len(self._words) < needed:
continue
tail = self._words[-size:]
if all(self._words[-offset - size : -offset] == tail for offset in range(size, needed, size)):
return f"repeated {size}-word phrase {' '.join(tail)!r}"
return ""
def strip_degenerate_repetition(text: str, repeated_word_limit: int = 8) -> str:
if not text:
return ""
matches = list(re.finditer(r"[A-Za-z][A-Za-z'\-]{2,}", text))
if not matches:
return text
last = matches[-1].group(0).casefold()
if len(last) < 4:
return text
run_start = len(matches) - 1
while run_start >= 0 and matches[run_start].group(0).casefold() == last:
run_start -= 1
run_count = len(matches) - run_start - 1
if run_count < repeated_word_limit:
return text
cut_at = matches[run_start + 1].start()
return text[:cut_at].rstrip(" \t\r\n,.;:!?")
class TerminalStreamDisplay:
"""Buffered terminal display for streamed model output.
Keeps the raw output accumulation in the caller, but renders only readable
chunks to the terminal with stage headers and completion summaries.
When *compact* is True, token content is shown as a rolling 200-character
tail on a single line so older tokens scroll off naturally.
When *compact* is True, token content is printed as clean wrapped text
without node labels, carriage returns, or tail truncation.
"""
_SENTENCE_BOUNDARY_RE = re.compile(r"[.!?](?:['\")\]]+)?(?:\s+|$)")
@@ -57,6 +185,7 @@ class TerminalStreamDisplay:
flush_interval: float = 1.0,
suppress_planning: bool = False,
compact: bool = True,
line_width: int | None = None,
):
self.label = label
self.flush_chars = max(32, int(flush_chars))
@@ -72,6 +201,11 @@ class TerminalStreamDisplay:
self._stage_updates = 0
self._last_compact_emit_at = 0.0
self._last_compact_tail = ""
self._compact_active = False
self._stream_pending_text = ""
self._stream_col = 0
self._stream_need_space = False
self._line_width_override = int(line_width) if line_width else None
def start_stage(self, stage_name: str):
self.end_stage()
@@ -79,6 +213,8 @@ class TerminalStreamDisplay:
self._stage_started_at = time.monotonic()
self._stage_chars = 0
self._stage_updates = 0
if self.compact:
return
self._write_line(f"[{self.label}] {stage_name}")
def push(self, text: str):
@@ -112,9 +248,18 @@ class TerminalStreamDisplay:
return re.sub(r"[ \t]+", " ", raw.replace("\r", "").replace("\n", " ")).strip()
def end_stage(self):
self.flush(force=True)
if self._compact_active:
self.end_compact()
else:
self.flush(force=True)
if not self._stage_name:
return
if self.compact:
self._stage_name = None
self._stage_started_at = None
self._stage_chars = 0
self._stage_updates = 0
return
elapsed = 0.0
if self._stage_started_at is not None:
elapsed = max(0.0, time.monotonic() - self._stage_started_at)
@@ -183,48 +328,86 @@ class TerminalStreamDisplay:
sys.stdout.write(msg)
sys.stdout.flush()
def _write_rolling_line(self, label: str, tail: str, final: bool = False):
"""Emit a carriage-return line showing the last ~200 chars, clearing to EOL.
'final' adds a trailing newline so the last frame stays visible."""
if not tail:
return
msg = f"\r[{label}] {tail}\033[K"
sys.stdout.write(msg)
if final:
sys.stdout.write("\n")
sys.stdout.flush()
def _stream_line_width(self) -> int:
if self._line_width_override:
return max(24, self._line_width_override)
columns = shutil.get_terminal_size(fallback=(120, 24)).columns
return max(40, min(96, columns // 2))
def push_compact(self, text: str):
"""Append *text* and emit a carriage-return rolling tail every throttle interval.
Uses \\r to redraw a single line so the terminal stays clean without
flooding the ComfyUI log buffer.
"""
def _write_wrapped_stream_text(self, text: str):
if not text:
return
width = self._stream_line_width()
for part in re.findall(r"\S+|\s+", text.replace("\r", "")):
if not part:
continue
if part.isspace():
newline_count = part.count("\n")
if newline_count:
sys.stdout.write("\n" * newline_count)
self._stream_col = 0
self._stream_need_space = False
elif self._stream_col > 0:
self._stream_need_space = True
continue
word = part
prefix = 1 if self._stream_need_space and self._stream_col > 0 else 0
if self._stream_col > 0 and self._stream_col + prefix + len(word) > width:
sys.stdout.write("\n")
self._stream_col = 0
prefix = 0
if prefix:
sys.stdout.write(" ")
self._stream_col += 1
sys.stdout.write(word)
self._stream_col += len(word)
self._stream_need_space = False
sys.stdout.flush()
def _drain_stream_pending(self, force: bool = False):
if not self._stream_pending_text:
return
text = self._stream_pending_text
if force:
emit = text
self._stream_pending_text = ""
else:
match = None
for match in re.finditer(r"\s+", text):
pass
if match is None:
if len(text) < self._stream_line_width():
return
emit = text
self._stream_pending_text = ""
else:
end = match.end()
emit = text[:end]
self._stream_pending_text = text[end:]
self._write_wrapped_stream_text(emit)
def push_compact(self, text: str):
"""Append *text* as clean wrapped terminal output without node prefixes."""
if not text:
return
self._compact_active = True
self._stage_chars += len(text)
self._buffer += text
if len(self._buffer) > self.TAIL_LENGTH * 2:
self._buffer = self._buffer[-self.TAIL_LENGTH:]
tail = self._compact_tail()
now = time.monotonic()
should_emit = False
if tail and tail != self._last_compact_tail:
if self._last_compact_emit_at <= 0.0:
should_emit = len(self._buffer) >= self.min_chunk_chars
elif (now - self._last_compact_emit_at) >= self.flush_interval:
should_emit = True
if should_emit:
self._stage_updates += 1
self._last_compact_tail = tail
self._last_compact_emit_at = now
self._write_rolling_line(self.label, tail)
self._stage_updates += 1
self._stream_pending_text += text
self._drain_stream_pending(force=False)
def end_compact(self):
"""Finish compact mode with one final newline-terminated snapshot."""
tail = self._compact_tail()
if tail and tail != self._last_compact_tail:
self._stage_updates += 1
had_pending = bool(self._stream_pending_text)
self._drain_stream_pending(force=True)
if (had_pending or self._stream_col > 0) and self._stage_chars:
sys.stdout.write("\n")
sys.stdout.flush()
self._last_compact_tail = ""
self._last_compact_emit_at = 0.0
self._write_rolling_line(self.label, tail or "", final=True)
self._compact_active = False
self._buffer = ""
self._stream_pending_text = ""
self._stream_col = 0
self._stream_need_space = False
+7
View File
@@ -1,6 +1,8 @@
{
"_preset_prompts": [
"🚫 No preset (image-only)",
"💬 Custom prompt + image (no preset)",
"🎦 LTX 2.3 NSFW I2V Scene",
"🍿 Wan 2.2 NSFW I2V Timeline (3s)",
"🍿 Wan 2.2 NSFW I2V Timeline (5s)",
"🎥 Wan 2.2 NSFW I2V Scene (3s)",
@@ -23,6 +25,8 @@
],
"qwenvl": {
"🚫 No preset (image-only)": "",
"💬 Custom prompt + image (no preset)": "",
"🎦 LTX 2.3 NSFW I2V Scene": "You are an expert cinematic prompt director for LTX 2.3 image-to-video generation. Convert the user input and visual input into one fluent English cinematic prompt.\n\nRead the image first, then integrate the user text. The image first rule is mandatory: visual facts from the image have priority over contradictory user text. Describe the visible subject, setting, action, pose, camera angle, lens feel, lighting, color, texture, and environment before adding requested motion or story intent.\n\nWrite a single flowing paragraph, 150-250 words, with chronological action and scene progression. Start directly with the main action. Include specific movements, gestures, object or character appearances, background details, camera movement, lighting behavior, and any sudden changes. Keep the prompt literal, precise, and usable for LTX 2.3. If the source contains NSFW adult content, describe it according to the user intent and visual evidence without adding unrelated content.\n\nOutput the enhanced prompt only. No headings, labels, notes, examples, markdown, or reasoning. Do not exceed 250 words.",
"🍿 Wan 2.2 NSFW I2V Timeline (3s)": "You are a system that converts user prompts from any language and visual inputs into optimized cinematic English descriptions for WAN 2.2 I2V generation.\n\nSteps:\n1. Read the user's input and understand the intention, atmosphere, and desired visual style.\n2. Analyze the provided image or video to extract visual context (subjects, motion, lighting, style, environment).\n3. Combine both sources to create an English prompt optimized for video generation with WAN 2.2.\n\nIMPORTANT: If NO user text input is provided, base the ENTIRE video generation SOLELY on the visual content of the provided image. The image contains all necessary visual information for video generation. Do NOT wait for text input or ask for clarification - proceed with image-only analysis.\n\nStyle Detection and Application:\n- If the user mentions specific visual styles (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), prioritize that style over photorealism.\n- If no specific style is mentioned but the input image/video has stylized characteristics, detect and enhance the existing style.\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nRules:\n- The output must be in fluent English.\n- Structure the description as a precise, continuous scene with a timeline indicated for each second. The video is 3 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...)\").\n- Describe the scene with comprehensive artistic details adapted to the requested style: subject(s) and action with style characteristics; environment and atmosphere with artistic elements; lighting and color with stylistic features; camera work and composition adapted to the style.\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- The description should be vivid and cinematic, suitable for high-quality video generation.\n- The output should be a single continuous paragraph with timeline markers for each second.\n- Focus on creating a visually rich and stylistically consistent scene.\n\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\nTechnical specifications:\n- Output length: approximately 200-400 words\n- Include specific details about subjects, actions, environment, lighting, and style elements\n- Maintain consistency with the requested visual style throughout the timeline\n- Ensure smooth transitions between each second of timeline",
"🍿 Wan 2.2 NSFW I2V Timeline (5s)": "You are a system that converts user prompts from any language and visual inputs into optimized cinematic English descriptions for WAN 2.2 I2V generation.\n\nSteps:\n1. Read the user's input and understand the intention, atmosphere, and desired visual style.\n2. Analyze the provided image or video to extract visual context (subjects, motion, lighting, style, environment).\n3. Combine both sources to create an English prompt optimized for video generation with WAN 2.2.\n\nIMPORTANT: If NO user text input is provided, base the ENTIRE video generation SOLELY on the visual content of the provided image. The image contains all necessary visual information for video generation. Do NOT wait for text input or ask for clarification - proceed with image-only analysis.\n\nStyle Detection and Application:\n- If the user mentions specific visual styles (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), prioritize that style over photorealism.\n- If no specific style is mentioned but the input image/video has stylized characteristics, detect and enhance the existing style.\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nRules:\n- The output must be in fluent English.\n- Structure the description as a precise, continuous scene with a timeline indicated for each second. The video is 5 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\").\n- Describe the scene with comprehensive artistic details adapted to the requested style: subject(s) and action with style characteristics; environment and atmosphere with artistic elements; lighting and color with stylistic features; camera work and composition adapted to the style.\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- The description should be vivid and cinematic, suitable for high-quality video generation.\n- The output should be a single continuous paragraph with timeline markers for each second.\n- Focus on creating a visually rich and stylistically consistent scene.\n\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\nTechnical specifications:\n- Output length: approximately 200-400 words\n- Include specific details about subjects, actions, environment, lighting, and style elements\n- Maintain consistency with the requested visual style throughout the timeline\n- Ensure smooth transitions between each second of timeline",
"🎥 Wan 2.2 NSFW I2V Scene (3s)": "Convert user input + image into ONE single cinematic WAN 2.2 prompt (3s total).\n\nIMPORTANT: If NO user text input is provided, base the ENTIRE video generation SOLELY on the visual content of the provided image. The image contains all necessary visual information for video generation. Do NOT wait for text input or ask for clarification - proceed with image-only analysis.\n\nStyle Priority: User-specified > detected from image > default style\n- If user explicitly mentions a style (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), use that style\n- Else if image has clear stylized characteristics, detect and enhance the existing style from the image\n- Else default to cinematic photorealism with realistic lighting and natural motion\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nCRITICAL FORMAT:\n- Generate exactly ONE single prompt\n- Single prompt = 3 seconds of video\n- NO timeline markers like \"At X seconds:\"\n- 6-10 sentences for rich cinematic detail\n- Focus on complete scene description\n- DO NOT copy example text - generate original content\n\nTECHNICAL RULES:\n- Fluent English only\n- Describe visible elements only (no invented lighting)\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- No preface/reasoning/notes\n- Rich cinematic descriptions with style details\n- RECOMMENDED: Use max_tokens=512 or higher for complete output\n\nPERFORMANCE OPTIMIZATION:\n- Set context_length to 16384 or higher for better quality\n- Use image_max_tokens=2048 for detailed visual analysis\n- Set n_batch=256 for efficient processing\n- Temperature 0.7-0.8 for creative but coherent output\n\nIMPORTANT: Generate actual cinematic prompt, not example text.",
@@ -40,6 +44,9 @@
"qwen_text": {
"translation_prompt": "You are a professional prompt translator and prompt-preservation editor. Return a single fluent English paragraph (400-800 words). Preserve every visual, stylistic, compositional, lighting, camera, texture, mood, and action detail from the source. Do not summarize or simplify. No prefixes, bullets, JSON, headings, markdown, quotes, or commentary. Output only the translated/enhanced prompt paragraph.",
"styles": {
"📖 LTX 2.3 NSFW T2V Scene": {
"system_prompt": "You are an expert cinematic prompt director for LTX 2.3 text-to-video generation. Convert the user input into one fluent English cinematic prompt.\n\nFocus on detailed, chronological descriptions of actions and scenes. Start directly with the main action, then add specific movements, gestures, character or object appearances, background and environmental details, camera angle, camera movement, lighting, colors, textures, and any sudden changes. Keep the result literal, precise, and optimized for LTX 2.3 motion.\n\nDo not change the user input intent; enhance it. Preserve the requested subject, style, action, mood, and constraints. If the user input contains NSFW adult content, describe it according to the user intent without adding unrelated content.\n\nWrite a single flowing paragraph under 150 words. Output the enhanced prompt only. No headings, labels, notes, examples, markdown, or reasoning."
},
"📖 Wan 2.2 NSFW T2V Scene (20s)": {
"system_prompt": "Generate 1 single T2V prompt for WAN 2.2 video generation (5s total) to establish initial scene.\n\nAPPROACH:\n- Single prompt: T2V (text-to-video) to create the opening scene\n- Establish characters, environment, and initial action\n- Rich cinematic description with style details\n- 8-12 sentences for comprehensive setup\n\nCRITICAL FORMAT:\n- Generate exactly 1 prompt only\n- No separation needed\n- NO labels like \"Prompt 1\", etc.\n- Focus on establishing the complete scene\n- Maintain consistent style\n\nSTYLE PRIORITY:\nUser-specified > detected > default style\n- If user explicitly mentions a style (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), use that style\n- Else default to cinematic photorealism with realistic lighting and natural motion\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nTECHNICAL RULES:\n- Fluent English only\n- Describe visible elements only\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- No preface/reasoning/notes\n- Temperature 0.7-0.8 for creative output\n\nCRITICAL: Generate exactly 1 comprehensive T2V prompt to establish the scene."
},
+294
View File
@@ -0,0 +1,294 @@
import json
import os
import shutil
import subprocess
import tempfile
import wave
from pathlib import Path
import numpy as np
WHISPER_MODEL_CHOICES = [
"small",
"base",
"tiny",
"medium",
"large-v3",
"distil-large-v3",
]
WHISPER_LANGUAGE_CHOICES = [
"auto",
"en",
"de",
"fr",
"es",
"it",
"pt",
"nl",
"pl",
"tr",
"ja",
"ko",
"zh",
]
WHISPER_INSTALL_MESSAGE = (
"[ThinkingLLM Whisper ASR] faster-whisper is not installed in this ComfyUI Python. "
"Install it in the active ComfyUI Python with: python -m pip install faster-whisper"
)
def _clean_audio_file_path(audio_file_path) -> Path | None:
if audio_file_path is None:
return None
raw_path = str(audio_file_path).strip().strip("\"'")
if not raw_path:
return None
return Path(os.path.expandvars(raw_path)).expanduser()
def _ffmpeg_path() -> str:
ffmpeg = shutil.which("ffmpeg")
if not ffmpeg:
raise RuntimeError("ffmpeg was not found on PATH; install FFmpeg or connect a decoded ComfyUI AUDIO input")
return ffmpeg
def _decode_audio_file_to_wav(audio_file_path, target_path: Path) -> str:
source_path = _clean_audio_file_path(audio_file_path)
if source_path is None:
raise ValueError("audio_file_path is empty")
if not source_path.exists() or not source_path.is_file():
raise FileNotFoundError(f"audio_file_path does not exist or is not a file: {source_path}")
result = subprocess.run(
[
_ffmpeg_path(),
"-hide_banner",
"-v",
"error",
"-i",
str(source_path),
"-vn",
"-ac",
"1",
"-ar",
"16000",
"-acodec",
"pcm_s16le",
str(target_path),
],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"ffmpeg failed to decode audio_file_path: {result.stderr.strip() or 'unknown ffmpeg error'}")
return str(source_path)
def _audio_to_wav_file(audio, target_path: Path) -> str:
if not isinstance(audio, dict):
raise ValueError("expected ComfyUI AUDIO dict with waveform/sample_rate")
waveform = audio.get("waveform")
if waveform is None:
raise ValueError("missing waveform")
sample_rate = int(audio.get("sample_rate") or 0)
if sample_rate <= 0:
raise ValueError(f"invalid sample_rate={audio.get('sample_rate')!r}")
tensor = waveform.detach().cpu() if hasattr(waveform, "detach") else waveform
tensor = tensor.cpu() if hasattr(tensor, "cpu") else tensor
array = tensor.numpy() if hasattr(tensor, "numpy") else np.asarray(tensor)
array = np.asarray(array, dtype=np.float32)
if array.size == 0:
raise ValueError("empty waveform")
if array.ndim == 3:
array = array[0]
if array.ndim == 1:
array = array[None, :]
if array.ndim != 2:
raise ValueError(f"unsupported waveform shape={getattr(array, 'shape', None)}")
if array.shape[0] > array.shape[-1]:
array = array.T
if not np.all(np.isfinite(array)):
array = np.nan_to_num(array, nan=0.0, posinf=1.0, neginf=-1.0)
mono = np.mean(array, axis=0).astype(np.float32)
if sample_rate != 16000:
target_length = max(1, int(round(float(mono.size) * 16000.0 / float(sample_rate))))
old_positions = np.linspace(0.0, float(mono.size - 1), num=mono.size, dtype=np.float64)
new_positions = np.linspace(0.0, float(mono.size - 1), num=target_length, dtype=np.float64)
mono = np.interp(new_positions, old_positions, mono).astype(np.float32)
pcm16 = (np.clip(mono, -1.0, 1.0) * 32767.0).astype(np.int16)
with wave.open(str(target_path), "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(16000)
wav.writeframes(pcm16.tobytes())
return "ComfyUI AUDIO input"
def _resolve_whisper_device(device: str) -> str:
if device == "auto":
try:
import torch
return "cuda" if torch.cuda.is_available() else "cpu"
except Exception:
return "cpu"
return device
def _resolve_compute_type(device: str, compute_type: str) -> str:
if compute_type != "auto":
return compute_type
return "float16" if device == "cuda" else "int8"
def _missing_audio_result(notes: list[str] | None = None) -> tuple[str, str, str]:
message = (
"[ThinkingLLM Whisper ASR] No usable audio input. Connect a ComfyUI AUDIO output "
"or set audio_file_path to an existing M4A/MP3/WAV/FLAC file."
)
trace_lines = ["[AUDIO]", message]
if notes:
trace_lines.extend(str(note) for note in notes if note)
return message, "[]", "\n".join(trace_lines)
def _is_local_whisper_cache_miss(exc: Exception) -> bool:
try:
from huggingface_hub.errors import LocalEntryNotFoundError
if isinstance(exc, LocalEntryNotFoundError):
return True
except Exception:
pass
message = str(exc or "").lower()
return "local_files_only" in message and "cached" in message
class ThinkingLLM_Whisper_ASR:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model_size": (WHISPER_MODEL_CHOICES, {"default": "small", "tooltip": "faster-whisper model size. small is a reliable Windows-friendly starting point; large-v3 is higher quality but downloads and runs much larger."}),
"language": (WHISPER_LANGUAGE_CHOICES, {"default": "auto", "tooltip": "Audio language. Use auto to let Whisper detect it."}),
"task": (["transcribe", "translate"], {"default": "transcribe", "tooltip": "transcribe preserves the original language; translate returns English."}),
"device": (["cpu", "auto", "cuda"], {"default": "cpu", "tooltip": "cpu is the most reliable Windows default; choose cuda when your faster-whisper/CTranslate2 CUDA runtime is working."}),
"compute_type": (["int8", "auto", "float16", "int8_float16", "float32"], {"default": "int8", "tooltip": "int8 is reliable on CPU. Use float16 or int8_float16 for CUDA."}),
"beam_size": ("INT", {"default": 5, "min": 1, "max": 10, "tooltip": "Higher values can improve accuracy but run slower."}),
"vad_filter": ("BOOLEAN", {"default": True, "tooltip": "Skip long silence and non-speech sections before transcription."}),
},
"optional": {
"audio": ("AUDIO",),
"audio_file_path": ("STRING", {"default": "", "multiline": False, "tooltip": "Optional local audio file path. M4A, MP3, WAV, FLAC, and other FFmpeg-readable files are decoded to 16 kHz mono WAV before Whisper."}),
},
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("TRANSCRIPT", "SEGMENTS_JSON", "RAW_TRACE")
FUNCTION = "process"
CATEGORY = "ThinkingLLM/Audio"
def process(
self,
model_size: str,
language: str,
task: str,
device: str,
compute_type: str,
beam_size: int,
vad_filter: bool,
audio=None,
audio_file_path="",
):
notes: list[str] = []
clean_path = _clean_audio_file_path(audio_file_path)
if audio is None and clean_path is None:
return _missing_audio_result()
try:
from faster_whisper import WhisperModel
except Exception as exc:
trace = "\n".join(["[WHISPER]", WHISPER_INSTALL_MESSAGE, f"import_error={exc}"])
return WHISPER_INSTALL_MESSAGE, "[]", trace
with tempfile.TemporaryDirectory(prefix="thinkingllm-whisper-") as temp_dir:
wav_path = Path(temp_dir) / "audio.wav"
try:
if clean_path is not None:
source_label = _decode_audio_file_to_wav(str(clean_path), wav_path)
else:
source_label = _audio_to_wav_file(audio, wav_path)
except Exception as exc:
return _missing_audio_result([str(exc)])
resolved_device = _resolve_whisper_device(str(device))
resolved_compute_type = _resolve_compute_type(resolved_device, str(compute_type))
local_files_only = True
try:
model = WhisperModel(
str(model_size),
device=resolved_device,
compute_type=resolved_compute_type,
local_files_only=True,
)
except Exception as exc:
if not _is_local_whisper_cache_miss(exc):
raise
notes.append("Local Whisper cache miss; downloaded model from Hugging Face once.")
local_files_only = False
model = WhisperModel(
str(model_size),
device=resolved_device,
compute_type=resolved_compute_type,
local_files_only=False,
)
segments_iter, info = model.transcribe(
str(wav_path),
language=None if language == "auto" else str(language),
task=str(task),
beam_size=int(beam_size),
vad_filter=bool(vad_filter),
)
segments = [
{
"start": float(segment.start),
"end": float(segment.end),
"text": str(segment.text).strip(),
}
for segment in segments_iter
]
transcript = "\n".join(segment["text"] for segment in segments if segment["text"]).strip()
segments_json = json.dumps(segments, ensure_ascii=False, indent=2)
detected_language = getattr(info, "language", None)
detected_probability = getattr(info, "language_probability", None)
duration = getattr(info, "duration", None)
trace = "\n".join(
[
"[WHISPER]",
f"source={source_label}",
f"model_size={model_size}; device={resolved_device}; compute_type={resolved_compute_type}; local_files_only={local_files_only}; task={task}; vad_filter={bool(vad_filter)}",
f"language={detected_language}; language_probability={detected_probability}; duration={duration}",
f"segments={len(segments)}; transcript_chars={len(transcript)}",
*notes,
]
)
return transcript, segments_json, trace
NODE_CLASS_MAPPINGS = {
"ThinkingLLM_Whisper_ASR": ThinkingLLM_Whisper_ASR,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ThinkingLLM_Whisper_ASR": "ThinkingLLM Whisper ASR",
}
+43 -2
View File
@@ -1,6 +1,6 @@
# ComfyUI-ThinkingLLM
**ThinkingLLM** is a local-LLM custom-node pack for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) that wraps Qwen3.5, Qwen3, and Qwen2.5-VL models behind a cleaner node interface with thinking-mode control, live token streaming, and a raw trace output for debugging.
**ThinkingLLM** is a local-LLM custom-node pack for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) that wraps Qwen3.5, Qwen3, Qwen-VL, Gemma 4, and Whisper ASR behind a cleaner node interface with thinking-mode control, audio/vision workflows, live token streaming, and a raw trace output for debugging.
📺 **[Watch the demo](https://youtu.be/K1JsvnzujOw)** for a better viewing experience.
@@ -13,6 +13,15 @@
## What ThinkingLLM adds
### Model guidance box
ThinkingLLM nodes add a read-only `recommended_settings` info box.
- It explains the selected model family and recommended generation settings.
- It does not change your saved widget values.
- For GGUF audio-capable nodes, it tells you whether the selected model actually supports audio.
- In Advanced GGUF nodes, if you connect or expose audio for a model without known audio support, the box warns you instead of silently implying that audio will work.
### The Thinking Toggle
Every ThinkingLLM node exposes an `enable_thinking` toggle.
@@ -40,6 +49,15 @@ Each node returns a second string output named `RAW_TRACE`.
That makes it possible to inspect what the model thought about the prompt while still keeping the primary output clean.
### Audio support
Audio is available through dedicated nodes so the model list stays honest:
- `ThinkingLLM Gemma 4 Audio (GGUF)` for Gemma 4 audio-capable models.
- `ThinkingLLM Whisper ASR` for reliable speech-to-text transcription through `faster-whisper`.
The Advanced GGUF node also exposes optional `audio`, but this is for power users. Use the info box before assuming a model can hear audio. Normal Qwen/Qwen-VL models are not audio models.
## Nodes
### Transformers / HF nodes
@@ -58,15 +76,17 @@ HF tokenizer note: some Gemma and Qwen variants need `sentencepiece` or `tiktoke
- `ThinkingLLM (GGUF)`
- `ThinkingLLM (GGUF Advanced)`
- `ThinkingLLM Gemma 4 Audio (GGUF)`
- `ThinkingLLM Prompt Enhancer (GGUF)`
The GGUF path requires a vision-capable `llama-cpp-python` build. The normal PyPI package is not enough for Qwen vision handlers. On Linux, ThinkingLLM now auto-checks this at first GGUF use and attempts an automatic install of a matching JamePeng backend. Use the setup notes in [docs/LLAMA_CPP_PYTHON_VISION_INSTALL.md](docs/LLAMA_CPP_PYTHON_VISION_INSTALL.md) if you need to override the wheel/package source.
The GGUF path requires a multimodal-capable `llama-cpp-python` build. The normal PyPI package may not include the chat handlers needed for Qwen vision, Gemma 4, or MTMD audio. On Linux, ThinkingLLM auto-checks this at first GGUF use and attempts an automatic install of a matching JamePeng backend. Use the setup notes in [docs/LLAMA_CPP_PYTHON_VISION_INSTALL.md](docs/LLAMA_CPP_PYTHON_VISION_INSTALL.md) if you need to override the wheel/package source.
## Workflow tips
- **Pre-process input images** — use a resize or scale node before ThinkingLLM so large images don't blow up the context window.
- **Display the output** — connect RESPONSE or ENHANCED_OUTPUT to a **Show Text** or **Show Anything** node to see the generated text in the UI.
- **Inspect reasoning** — connect the RAW_TRACE output to a second Show Text node to see what the model was thinking.
- **Use the dedicated audio nodes first** — Gemma 4 Audio is for audio understanding; Whisper ASR is for transcription. The Advanced GGUF audio input is intentionally more flexible, but the info box will warn you when the selected model has no curated audio support.
## Supported models
@@ -94,17 +114,36 @@ ThinkingLLM supports these model families out of the box, with pre-configured en
### Gemma 4 — Vision-language (HF)
- Gemma-4-E2B-it / Uncensored
- Gemma-4-E4B-it / Uncensored
- Gemma-4-12B
- Gemma-4-26B-A4B-it / Heretic
- Gemma-4-31B-it
### Gemma 4 — Vision-language (GGUF)
- Gemma-4-E2B / E4B (Q4 K M through BF16, uncensored)
- Gemma-4-12B-it (Q4/Q5/Q6/Q8/BF16/UD-Q4)
- Gemma-4-26B-A4B-it (Q4 K M through BF16)
- Gemma-4-31B-it (Q4 K M through BF16)
### Gemma 4 — Audio (GGUF)
- Gemma-4-E2B / E4B when present in the GGUF multimodal catalog
- Gemma-4-12B-it via `unsloth/gemma-4-12b-it-GGUF`
Use `ThinkingLLM Gemma 4 Audio (GGUF)` for the clean audio-only interface. Gemma 4 audio currently depends on a recent multimodal `llama-cpp-python`/`Gemma4ChatHandler` build, so check `RAW_TRACE` if audio behaves unexpectedly.
### Whisper — ASR
- tiny
- base
- small
- medium
- large-v3
- distil-large-v3
Use `ThinkingLLM Whisper ASR` for transcription. It accepts a connected ComfyUI `AUDIO` input or an `audio_file_path` pointing to M4A, MP3, WAV, FLAC, or any FFmpeg-readable audio file. The node uses `faster-whisper`; first use may download the selected Whisper model into the Hugging Face cache. The default is `small` on CPU with `int8`, which is slower than CUDA but reliable on Windows; switch to CUDA once the local CTranslate2 CUDA runtime is confirmed working.
### Gemma 4 — Text-only (HF)
- Gemma-4-E2B-it / Uncensored
- Gemma-4-E4B-it / Uncensored
- Gemma-4-12B
- Gemma-4-26B-A4B-it / Heretic
- Gemma-4-31B-it
@@ -141,6 +180,8 @@ For local GGUF models, keep the matching mmproj file beside the model file.
- HF is the recommended default path and is the simplest cross-platform option.
- GGUF on Windows needs a matching `win_amd64` vision-capable wheel.
- GGUF on Linux now auto-installs a matching backend on first use when possible. Override with `THINKINGLLM_LLAMA_CPP_LINUX_WHEEL_URL` or `THINKINGLLM_LLAMA_CPP_LINUX_SPEC` when your server needs a different build.
- GGUF audio needs an audio-capable multimodal backend. `Gemma4ChatHandler` is used for Gemma 4.
- Whisper ASR needs `faster-whisper` and FFmpeg. The node returns an install hint if `faster-whisper` is missing.
- Flash Attention support is best on Linux. The nodes fall back when it is unavailable.
- The thinking toggle works best with Qwen3.5 and Qwen3 style models. Other architectures may ignore the steering.
+37 -4
View File
@@ -3,9 +3,9 @@
"name": "ComfyUI-ThinkingLLM",
"description": "A multimodal ComfyUI AI node with Qwen3.5, Qwen3-VL, Qwen2.5-VL, Qwen3, and Gemma 4 integrations. Features live thinking in the terminal to see what the LLM is doing in real time.",
"author": "goodguy1963",
"version": "1.0.9",
"version": "2.3.14",
"url": "https://github.com/goodguy1963/ComfyUI-ThinkingLLM",
"category": "image"
"category": "image"
}
"""
@@ -21,6 +21,15 @@ NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
WEB_DIRECTORY = "./web"
LEGACY_NODE_REPLACEMENTS = {
"AILab_QwenVL": "ThinkingLLM_QwenVL",
"AILab_QwenVL_Advanced": "ThinkingLLM_QwenVL_Advanced",
"AILab_QwenVL_GGUF": "ThinkingLLM_QwenVL_GGUF",
"AILab_QwenVL_GGUF_Advanced": "ThinkingLLM_QwenVL_GGUF_Advanced",
"AILab_QwenVL_PromptEnhancer": "ThinkingLLM_QwenVL_PromptEnhancer",
"AILab_QwenVL_GGUF_PromptEnhancer": "ThinkingLLM_QwenVL_GGUF_PromptEnhancer",
}
def load_modules_from_directory(directory):
for file in os.listdir(directory):
if file.endswith(".py"):
@@ -50,10 +59,34 @@ load_modules_from_directory(current_dir)
nodes_dir = os.path.join(current_dir, "nodes")
if os.path.exists(nodes_dir):
load_modules_from_directory(nodes_dir)
for legacy_node_name in LEGACY_NODE_REPLACEMENTS:
NODE_CLASS_MAPPINGS.pop(legacy_node_name, None)
NODE_DISPLAY_NAME_MAPPINGS.pop(legacy_node_name, None)
def _register_legacy_node_replacements():
try:
from comfy_api.latest._io import NodeReplace
from server import PromptServer
except Exception:
return
manager = getattr(getattr(PromptServer, "instance", None), "node_replace_manager", None)
if manager is None:
return
for old_node_id, new_node_id in LEGACY_NODE_REPLACEMENTS.items():
manager.register(NodeReplace(new_node_id=new_node_id, old_node_id=old_node_id))
_register_legacy_node_replacements()
NODE_CLASS_MAPPINGS = dict(sorted(NODE_CLASS_MAPPINGS.items(), key=lambda x: NODE_DISPLAY_NAME_MAPPINGS.get(x[0], x[0])))
NODE_DISPLAY_NAME_MAPPINGS = dict(sorted(NODE_DISPLAY_NAME_MAPPINGS.items(), key=lambda x: x[1]))
__all__ = [
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS"
]
"NODE_DISPLAY_NAME_MAPPINGS",
"LEGACY_NODE_REPLACEMENTS",
]
+1 -1
View File
@@ -9,5 +9,5 @@
"description": "A multimodal ComfyUI AI node with Qwen3.5, Qwen3-VL, Qwen2.5-VL, Qwen3, and Gemma 4 integrations. Features live thinking in the terminal to see what the LLM is doing in real time.",
"category": "image",
"tags": ["vision", "language", "multimodal", "thinkingllm", "qwen", "gguf", "flash_attention", "performance", "wan2.2", "video", "i2v", "t2v", "storyboard", "caching", "fork", "gpl-3.0"],
"version": "1.0.9"
"version": "2.3.14"
}
+73 -9
View File
@@ -21,16 +21,60 @@ On Linux, ThinkingLLM now performs a backend check the first time you run a GGUF
- If `llama_cpp` is missing or lacks the required Qwen vision handlers, it attempts an automatic install.
- The first automatic choice is a known JamePeng Linux wheel when the runtime matches a built-in mapping.
- If there is no exact built-in wheel match, it falls back to the JamePeng Git package spec.
- If there is no verified built-in wheel match, it now stops with a clear diagnostic instead of silently building a possibly CPU-only backend.
- Source builds are opt-in. When enabled and a CUDA toolkit is detected, ThinkingLLM sets `CMAKE_ARGS=-DGGML_CUDA=on` and `FORCE_CMAKE=1` for the install.
Environment overrides:
- `THINKINGLLM_AUTO_INSTALL_LLAMA_CPP=0` disables automatic Linux installation.
- `THINKINGLLM_LLAMA_CPP_LINUX_WHEEL_URL=<wheel-url>` forces a specific Linux wheel.
- `THINKINGLLM_LLAMA_CPP_LINUX_SPEC=<pip-spec>` forces an explicit pip install target.
- `THINKINGLLM_LLAMA_CPP_ALLOW_SOURCE_BUILD=1` allows the JamePeng Git source-build fallback when no verified wheel is known.
- `THINKINGLLM_LLAMA_CPP_ALLOW_CPU_SOURCE_BUILD=1` allows a source build even when no CUDA toolkit is detected. Use this only when you intentionally want CPU inference.
- `THINKINGLLM_LLAMA_CPP_VERBOSE_LOAD=1` enables verbose llama.cpp model-load logs so you can inspect `offloaded X/Y layers`, `CUDA0 buffer`, KV cache placement, and `flash_attn` status.
After an automatic install, restart ComfyUI if the current process does not pick up the new backend cleanly.
## Hugging Face token for gated/private downloads
ThinkingLLM nodes now include an optional `hf_token` field for Hugging Face downloads. Use it when a model, GGUF file, or mmproj file is private or gated.
- The token is passed only to the Hugging Face download call.
- The token is not added to cache keys, RAW_TRACE, backend diagnostics, or fixed-seed prompt signatures.
- Download errors redact the token before printing messages.
- The local in-memory token variable is cleared after each download attempt.
- If `hf_token` is empty, downloads can use `HF_TOKEN` or `HUGGING_FACE_HUB_TOKEN` from the ComfyUI process environment.
- Clear the `hf_token` field before saving or sharing workflows, because ComfyUI workflows can serialize widget values.
If a download reports `401`, `403`, `Unauthorized`, `Forbidden`, `Repository Not Found`, or `gated`, check both of these:
- The Hugging Face account behind the token has accepted the model license/gate.
- The token has read permission for the model repository.
If you do not need private or gated models, leave `hf_token` empty.
### CUDA version notes
ThinkingLLM reports CUDA evidence separately:
- **Torch CUDA**: the CUDA version PyTorch was compiled for, such as `12.4`.
- **Toolkit CUDA**: the CUDA toolkit found through `CUDA_HOME`, `CUDA_PATH`, `/usr/local/cuda/version.txt`, or `nvcc`.
- **nvidia-smi CUDA**: the maximum CUDA version advertised by the installed NVIDIA driver.
A minor-version difference inside CUDA 12.x is not, by itself, proof that llama.cpp is CPU-only. The reliable check is whether the installed `llama-cpp-python` library supports GPU offload and whether verbose llama.cpp load logs show GPU buffers/layers.
Run the standalone probe from this folder:
```bash
python tools/check_llama_backend.py
```
For a real model-load smoke test:
```bash
python tools/check_llama_backend.py --model /path/to/model.gguf --gpu-layers 1 --verbose-load --strict-gpu
```
## 1) Identify the exact Python ComfyUI uses
### Windows portable (common)
@@ -68,14 +112,14 @@ Download a **Release `.whl`** from:
The wheel **must match ALL of the following**:
* **Python version** used by ComfyUI
- **Python version** used by ComfyUI
(`cp310` / `cp311` / `cp312` / `cp313`)
* **Platform**
- **Platform**
`win_amd64` (Windows 64-bit)
* **Build type**
- **Build type**
* **CPU wheel** → safest option (no CUDA toolkit required)
* **CUDA wheel (`cuXXX`)** → requires a compatible CUDA runtime / toolkit
- **CPU wheel** → safest option (no CUDA toolkit required)
- **CUDA wheel (`cuXXX`)** → requires a compatible CUDA runtime / toolkit
> [!WARNING]
> **Windows note (important)**
@@ -87,14 +131,14 @@ If you are unsure, **use a CPU wheel**.
Install with force-reinstall (safer than manual uninstall):
```
```bat
C:\AI\ComfyUI\python_embeded\python.exe -m pip install --upgrade --force-reinstall C:\path\to\llama_cpp_python-*.whl
```
Notes:
* Warnings about leftover folders like `~umpy` are usually safe to ignore while ComfyUI is closed.
* Make sure ComfyUI is **fully stopped** before installing.
- Warnings about leftover folders like `~umpy` are usually safe to ignore while ComfyUI is closed.
- Make sure ComfyUI is **fully stopped** before installing.
## 4) Verify vision handlers exist
@@ -104,6 +148,26 @@ C:\AI\ComfyUI\python_embeded\python.exe -c "from llama_cpp.llama_chat_format imp
If this fails, you installed a wheel that does not include vision support (or installed into the wrong Python environment).
## 4b) Verify the backend is not silently CPU-only
The GGUF node now prints a line like:
```text
[QwenVL] llama.cpp backend: llama-cpp-python 0.3.x (cp312, x86_64); GPU offload=yes; vision=Qwen3VLChatHandler; CUDA torch=12.4, toolkit=12.8, nvidia-smi=12.9; module=...
```
In the Advanced node, RAW_TRACE begins with a `[BACKEND]` section. Check these fields first when a strong GPU runs slowly:
- `GPU offload=yes/no/unknown`
- `dropped_performance_kwargs`
- `warnings`
- `recommended_actions`
- `device`, `gpu_layers`, `flash_attn`, and `offload_kqv`
If GPU offload is `no`, install a CUDA-capable wheel or rebuild with CUDA. If it is `unknown`, set `THINKINGLLM_LLAMA_CPP_VERBOSE_LOAD=1`, restart ComfyUI, and look for llama.cpp load lines such as `offloaded X/Y layers`, `CUDA0 buffer size`, `CUDA0 KV buffer size`, and `flash_attn = 1`.
On high-core Linux servers, leaving `n_threads=0` and `n_threads_batch=0` can be slower than a smaller fixed value because llama.cpp may span NUMA nodes. Start with `n_threads=8` to `16`, then tune `n_threads_batch` for prompt processing.
## 5) Fix common dependency conflicts (Windows)
Some wheels may upgrade dependencies (notably `numpy` / `pillow`) and cause conflicts with other packages (like OpenCV).
-466
View File
@@ -1,466 +0,0 @@
{
"id": "b1b52a7e-e128-4de3-83e0-2238301079ca",
"revision": 0,
"last_node_id": 18,
"last_link_id": 18,
"nodes": [
{
"id": 12,
"type": "PreviewAny",
"pos": [
-470,
70
],
"size": [
890,
350
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 16
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.65",
"Node name for S&R": "PreviewAny",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.3"
}
},
"widgets_values": [
null,
null,
false
]
},
{
"id": 16,
"type": "MarkdownNote",
"pos": [
-1780,
80
],
"size": [
550,
530
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.5.2",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"## **⚙️ Parameters**\n\n| Parameter | Description | Default | Range | Node(s) |\n| :---- | :---- | :---- | :---- | :---- |\n| **model\\_name** | The Qwen-VL model to use. | Qwen3-VL-4B-Instruct | \\- | Standard & Advanced |\n| **quantization** | On-the-fly quantization. Ignored for pre-quantized models (e.g., FP8). | 8-bit (Balanced) | 4-bit, 8-bit, None | Standard & Advanced |\n| **preset\\_prompt** | A selection of pre-defined prompts for common tasks. | \"Describe this...\" | Any text | Standard & Advanced |\n| **custom\\_prompt** | Overrides the preset prompt if provided. | | Any text | Standard & Advanced |\n| **max\\_tokens** | Maximum number of new tokens to generate. | 1024 | 64-2048 | Standard & Advanced |\n| **keep\\_model\\_loaded** | Keep the model in VRAM for faster subsequent runs. | True | True/False | Standard & Advanced |\n| **seed** | A seed for reproducible results. | 1 | 1 \\- 2^64-1 | Standard & Advanced |\n| **temperature** | Controls randomness. Higher values \\= more creative. (Used when num\\_beams is 1). | 0.6 | 0.1-1.0 | Advanced Only |\n| **top\\_p** | Nucleus sampling threshold. (Used when num\\_beams is 1). | 0.9 | 0.0-1.0 | Advanced Only |\n| **num\\_beams** | Number of beams for beam search. \\> 1 disables temperature/top\\_p sampling. | 1 | 1-10 | Advanced Only |\n| **repetition\\_penalty** | Discourages repeating tokens. | 1.2 | 0.0-2.0 | Advanced Only |\n| **frame\\_count** | Number of frames to sample from the video input. | 16 | 1-64 | Advanced Only |\n| **device** | Override automatic device selection. | auto | auto, cuda, cpu | Advanced Only |"
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 17,
"type": "AILab_QwenVL",
"pos": [
-885,
70
],
"size": [
394.1806388251093,
303.45832648511134
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "image",
"shape": 7,
"type": "IMAGE",
"link": 15
},
{
"name": "video",
"shape": 7,
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "RESPONSE",
"type": "STRING",
"links": [
16
]
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "AILab_QwenVL"
},
"widgets_values": [
"Qwen3-VL-2B-Instruct",
"None (FP16)",
"auto",
"🖼️ Detailed Description",
"",
512,
true,
1,
"randomize"
],
"color": "#28403f",
"bgcolor": "#374539"
},
{
"id": 7,
"type": "AILab_LoadImage",
"pos": [
-1200,
70
],
"size": [
300,
720
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
15
]
},
{
"name": "MASK",
"type": "MASK",
"links": null
},
{
"name": "WIDTH",
"type": "INT",
"links": null
},
{
"name": "HEIGHT",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "AILab_LoadImage",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.3"
}
},
"widgets_values": [
"",
"2430364.png",
"lanczos",
1,
1,
"longest_side",
0,
"image"
],
"color": "#2e3e57",
"bgcolor": "#4b5b73"
},
{
"id": 13,
"type": "PreviewAny",
"pos": [
-200,
480
],
"size": [
620,
370
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 18
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.65",
"Node name for S&R": "PreviewAny",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.3"
}
},
"widgets_values": [
null,
null,
false
]
},
{
"id": 18,
"type": "AILab_QwenVL_Advanced",
"pos": [
-610.5085688039786,
481.1527608367014
],
"size": [
396.48624844852634,
468
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "image",
"shape": 7,
"type": "IMAGE",
"link": null
},
{
"name": "video",
"shape": 7,
"type": "IMAGE",
"link": 17
}
],
"outputs": [
{
"name": "RESPONSE",
"type": "STRING",
"links": [
18
]
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "AILab_QwenVL_Advanced"
},
"widgets_values": [
"Qwen3-VL-2B-Instruct",
"None (FP16)",
"auto",
false,
"auto",
"🖼️ Detailed Description",
"",
512,
0.6,
0.9,
1,
1.2,
16,
true,
1,
"randomize"
],
"color": "#28403f",
"bgcolor": "#374539"
},
{
"id": 11,
"type": "VHS_LoadVideo",
"pos": [
-880,
480
],
"size": [
255.8291015625,
560.3298002919321
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "meta_batch",
"shape": 7,
"type": "VHS_BatchManager",
"link": null
},
{
"name": "vae",
"shape": 7,
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
17
]
},
{
"name": "frame_count",
"type": "INT",
"links": null
},
{
"name": "audio",
"type": "AUDIO",
"links": null
},
{
"name": "video_info",
"type": "VHS_VIDEOINFO",
"links": null
}
],
"properties": {
"cnr_id": "comfyui-videohelpersuite",
"ver": "08e8df15db24da292d4b7f943c460dc2ab442b24",
"Node name for S&R": "VHS_LoadVideo",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.3"
}
},
"widgets_values": {
"video": "sing_1.mp4",
"force_rate": 0,
"custom_width": 0,
"custom_height": 0,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1,
"format": "AnimateDiff",
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"filename": "sing_1.mp4",
"type": "input",
"format": "video/mp4",
"force_rate": 0,
"custom_width": 0,
"custom_height": 0,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1
}
}
}
}
],
"links": [
[
15,
7,
0,
17,
0,
"IMAGE"
],
[
16,
17,
0,
12,
0,
"STRING"
],
[
17,
11,
0,
18,
1,
"IMAGE"
],
[
18,
18,
0,
13,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"frontendVersion": "1.34.9",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true,
"ue_links": [],
"links_added_by_ue": [],
"ds": {
"scale": 0.693975512958857,
"offset": [
1764.4004214905397,
359.36315462869436
]
},
"workflowRendererVersion": "LG"
},
"version": 0.4
}
@@ -6,7 +6,7 @@
"nodes": [
{
"id": 17,
"type": "AILab_QwenVL_GGUF_Advanced",
"type": "ThinkingLLM_QwenVL_GGUF_Advanced",
"pos": [
1090.7435866951005,
541.3632089660127
@@ -40,7 +40,7 @@
}
],
"properties": {
"Node name for S&R": "AILab_QwenVL_GGUF_Advanced",
"Node name for S&R": "ThinkingLLM_QwenVL_GGUF_Advanced",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
@@ -72,7 +72,7 @@
},
{
"id": 21,
"type": "AILab_QwenVL_Advanced",
"type": "ThinkingLLM_QwenVL_Advanced",
"pos": [
1090.7435866951005,
1103.0165621245635
@@ -106,7 +106,7 @@
}
],
"properties": {
"Node name for S&R": "AILab_QwenVL_Advanced",
"Node name for S&R": "ThinkingLLM_QwenVL_Advanced",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
@@ -136,7 +136,7 @@
},
{
"id": 20,
"type": "AILab_QwenVL",
"type": "ThinkingLLM_QwenVL",
"pos": [
728.3779414208888,
1103.0165621245635
@@ -170,7 +170,7 @@
}
],
"properties": {
"Node name for S&R": "AILab_QwenVL",
"Node name for S&R": "ThinkingLLM_QwenVL",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
@@ -193,7 +193,7 @@
},
{
"id": 23,
"type": "AILab_QwenVL_GGUF_PromptEnhancer",
"type": "ThinkingLLM_QwenVL_GGUF_PromptEnhancer",
"pos": [
1453.109231969312,
541.3632089660127
@@ -218,7 +218,7 @@
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "AILab_QwenVL_GGUF_PromptEnhancer"
"Node name for S&R": "ThinkingLLM_QwenVL_GGUF_PromptEnhancer"
},
"widgets_values": [
"Qwen3-4B-Q4_K_M.gguf",
@@ -239,7 +239,7 @@
},
{
"id": 24,
"type": "AILab_QwenVL_PromptEnhancer",
"type": "ThinkingLLM_QwenVL_PromptEnhancer",
"pos": [
1453.109231969312,
1103.0165621245635
@@ -264,7 +264,7 @@
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "AILab_QwenVL_PromptEnhancer"
"Node name for S&R": "ThinkingLLM_QwenVL_PromptEnhancer"
},
"widgets_values": [
"Qwen3-0.6B",
@@ -288,7 +288,7 @@
},
{
"id": 25,
"type": "AILab_QwenVL_GGUF",
"type": "ThinkingLLM_QwenVL_GGUF",
"pos": [
728.3779414208888,
541.3632089660127
@@ -326,7 +326,7 @@
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "AILab_QwenVL_GGUF"
"Node name for S&R": "ThinkingLLM_QwenVL_GGUF"
},
"widgets_values": [
"Qwen3-VL-4B-Thinking-Q4_K_M (bartowski)",
+134 -5
View File
@@ -189,6 +189,27 @@
"context_length": 131072
}
},
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-GGUF [Q2:11GB|Q3:15GB|Q4:20GB|Q5:28GB|Q6:31GB|Q8:44GB|VRAM:~24GB+]": {
"author": "HauhauCS",
"repo_name": "Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive",
"repo_id": "HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive",
"model_files": [
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q8_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q6_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q5_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-IQ4_NL.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q3_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-IQ3_M.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q2_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-IQ2_M.gguf"
],
"defaults": {
"context_length": 131072
}
},
"Gemma-4-E2B-it-GGUF [Q4:2GB|Q8:3.5GB|VRAM:~3GB]": {
"author": "ggml-org",
"repo_name": "gemma-4-E2B-it-GGUF",
@@ -230,6 +251,24 @@
"context_length": 262144
}
},
"Gemma-4-12B-it-GGUF [Q4:7.5GB|Q5:9GB|Q6:11GB|Q8:13GB|BF16:24GB|VRAM:~12GB]": {
"author": "unsloth",
"repo_name": "gemma-4-12b-it-GGUF",
"repo_id": "unsloth/gemma-4-12b-it-GGUF",
"model_files": [
"gemma-4-12b-it-Q4_K_M.gguf",
"gemma-4-12b-it-Q4_K_S.gguf",
"gemma-4-12b-it-Q5_K_M.gguf",
"gemma-4-12b-it-Q5_K_S.gguf",
"gemma-4-12b-it-Q6_K.gguf",
"gemma-4-12b-it-Q8_0.gguf",
"gemma-4-12b-it-BF16.gguf",
"gemma-4-12b-it-UD-Q4_K_XL.gguf"
],
"defaults": {
"context_length": 262144
}
},
"Gemma-4-31B-it-GGUF [Q4:20GB|Q8:32GB|VRAM:~22GB]": {
"author": "unsloth",
"repo_name": "gemma-4-31B-it-GGUF",
@@ -249,11 +288,19 @@
"repo_name": "google_gemma-4-E4B-it-GGUF",
"repo_id": "bartowski/google_gemma-4-E4B-it-GGUF",
"model_files": [
"gemma-4-E4B-it-Q4_K_M.gguf",
"gemma-4-E4B-it-Q6_K.gguf",
"gemma-4-E4B-it-Q8_0.gguf",
"gemma-4-E4B-it-BF16.gguf"
"google_gemma-4-E4B-it-Q4_K_M.gguf",
"google_gemma-4-E4B-it-Q5_K_M.gguf",
"google_gemma-4-E4B-it-Q6_K.gguf",
"google_gemma-4-E4B-it-Q8_0.gguf",
"google_gemma-4-E4B-it-bf16.gguf"
],
"local_filenames": {
"google_gemma-4-E4B-it-Q4_K_M.gguf": ["gemma-4-E4B-it-Q4_K_M.gguf"],
"google_gemma-4-E4B-it-Q5_K_M.gguf": ["gemma-4-E4B-it-Q5_K_M.gguf"],
"google_gemma-4-E4B-it-Q6_K.gguf": ["gemma-4-E4B-it-Q6_K.gguf"],
"google_gemma-4-E4B-it-Q8_0.gguf": ["gemma-4-E4B-it-Q8_0.gguf"],
"google_gemma-4-E4B-it-bf16.gguf": ["gemma-4-E4B-it-BF16.gguf"]
},
"defaults": {
"context_length": 131072
}
@@ -333,6 +380,33 @@
"pool_size": 4194304
}
},
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive [Q2:11GB|Q3:15GB|Q4:20GB|Q5:28GB|Q6:31GB|Q8:44GB|VRAM:~24GB+]": {
"author": "HauhauCS",
"repo_name": "Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive",
"repo_id": "HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive",
"mmproj_file": "mmproj-Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-f16.gguf",
"model_files": [
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q8_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q6_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q5_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-IQ4_NL.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q3_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-IQ3_M.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q2_K_P.gguf",
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-IQ2_M.gguf"
],
"defaults": {
"context_length": 131072,
"image_max_tokens": 4096,
"n_batch": 512,
"gpu_layers": -1,
"top_k": 20,
"pool_size": 4194304
}
},
"Qwen3.5-4B [Q4:2.4GB|Q6:3.2GB|Q8:4.2GB|BF16:8GB|VRAM:~4GB]": {
"author": "unsloth",
"repo_name": "Qwen3.5-4B-GGUF",
@@ -393,6 +467,61 @@
"pool_size": 4194304
}
},
"Gemma-4-12B-it-GGUF [Q4:7.5GB|Q5:9GB|Q6:11GB|Q8:13GB|BF16:24GB|VRAM:~12GB]": {
"author": "unsloth",
"repo_name": "gemma-4-12b-it-GGUF",
"repo_id": "unsloth/gemma-4-12b-it-GGUF",
"mmproj_file": "mmproj-F16.gguf",
"model_files": [
"gemma-4-12b-it-Q4_K_M.gguf",
"gemma-4-12b-it-Q4_K_S.gguf",
"gemma-4-12b-it-Q5_K_M.gguf",
"gemma-4-12b-it-Q5_K_S.gguf",
"gemma-4-12b-it-Q6_K.gguf",
"gemma-4-12b-it-Q8_0.gguf",
"gemma-4-12b-it-BF16.gguf",
"gemma-4-12b-it-UD-Q4_K_XL.gguf"
],
"defaults": {
"context_length": 262144,
"image_max_tokens": 4096,
"n_batch": 512,
"gpu_layers": -1,
"top_k": 64,
"pool_size": 4194304
}
},
"google_gemma-4-E4B-it-GGUF [Q4:5.4GB|Q5:5.8GB|Q6:6.3GB|Q8:8GB|BF16:15GB|VRAM:~7GB]": {
"author": "bartowski",
"repo_name": "google_gemma-4-E4B-it-GGUF",
"repo_id": "bartowski/google_gemma-4-E4B-it-GGUF",
"mmproj_file": "mmproj-google_gemma-4-E4B-it-f16.gguf",
"model_files": [
"google_gemma-4-E4B-it-Q4_K_M.gguf",
"google_gemma-4-E4B-it-Q5_K_M.gguf",
"google_gemma-4-E4B-it-Q6_K.gguf",
"google_gemma-4-E4B-it-Q8_0.gguf",
"google_gemma-4-E4B-it-bf16.gguf"
],
"local_filenames": {
"google_gemma-4-E4B-it-Q4_K_M.gguf": ["gemma-4-E4B-it-Q4_K_M.gguf"],
"google_gemma-4-E4B-it-Q5_K_M.gguf": ["gemma-4-E4B-it-Q5_K_M.gguf"],
"google_gemma-4-E4B-it-Q6_K.gguf": ["gemma-4-E4B-it-Q6_K.gguf"],
"google_gemma-4-E4B-it-Q8_0.gguf": ["gemma-4-E4B-it-Q8_0.gguf"],
"google_gemma-4-E4B-it-bf16.gguf": ["gemma-4-E4B-it-BF16.gguf"]
},
"mmproj_local_filenames": {
"mmproj-google_gemma-4-E4B-it-f16.gguf": ["mmproj-F16.gguf", "mmproj-BF16.gguf"]
},
"defaults": {
"context_length": 131072,
"image_max_tokens": 4096,
"n_batch": 512,
"gpu_layers": -1,
"top_k": 64,
"pool_size": 4194304
}
},
"Qwen3-VL-4B-Instruct-c_abliterated-v2-GGUF [Q4:2.4GB|Q5:2.8GB|Q6:3.2GB|Q8:4.2GB|F16:8GB|VRAM:~4GB]": {
"author": "mradermacher",
"repo_name": "Qwen3-VL-4B-Instruct-c_abliterated-v2-GGUF",
@@ -531,4 +660,4 @@
}
}
}
}
}
+22 -2
View File
@@ -2,7 +2,7 @@
"hf_vl_models": {
"Qwen3.5-4B-heretic-v2 [DL: 7.5GB, VRAM: 9.0GB]": {
"repo_id": "tvall43/Qwen3.5-4B-heretic-v2",
"default": true,
"default": false,
"quantized": false,
"vram_requirement": {
"full": 9.0
@@ -142,6 +142,16 @@
"4bit": 16.0
}
},
"Gemma-4-12B [DL: 24GB, VRAM: 24.0GB]": {
"repo_id": "google/gemma-4-12B",
"default": false,
"quantized": false,
"vram_requirement": {
"full": 24.0,
"8bit": 13.0,
"4bit": 8.0
}
},
"Gemma-4-E4B-Uncensored [DL: 16GB, VRAM: 7.0GB]": {
"repo_id": "prithivMLmods/gemma-4-E4B-it-Uncensored-MAX",
"default": false,
@@ -294,6 +304,16 @@
"4bit": 16.0
}
},
"Gemma-4-12B [DL: 24GB, VRAM: 24.0GB]": {
"repo_id": "google/gemma-4-12B",
"default": false,
"quantized": false,
"vram_requirement": {
"full": 24.0,
"8bit": 13.0,
"4bit": 8.0
}
},
"Gemma-4-E4B-Uncensored [DL: 16GB, VRAM: 7.0GB]": {
"repo_id": "prithivMLmods/gemma-4-E4B-it-Uncensored-MAX",
"default": false,
@@ -345,4 +365,4 @@
}
}
}
}
}
+3 -3
View File
@@ -1,9 +1,9 @@
[project]
name = "ComfyUI-ThinkingLLM"
description = "A multimodal ComfyUI AI node with Qwen3.5, Qwen3-VL, Qwen2.5-VL, Qwen3, and Gemma 4 integrations. Features live thinking in the terminal to see what the LLM is doing in real time."
version = "1.0.9"
version = "2.3.14"
license = {file = "LICENSE"}
dependencies = ["transformers>=5.2.0", "torch", "huggingface-hub", "hf_xet", "psutil", "numpy", "Pillow", "opencv-python", "bitsandbytes", "accelerate", "sentencepiece", "tiktoken", "llama-cpp-python"]
dependencies = ["transformers>=5.2.0", "torch", "huggingface-hub", "hf_xet", "psutil", "numpy", "Pillow", "opencv-python", "bitsandbytes", "accelerate", "sentencepiece", "tiktoken", "faster-whisper", "llama-cpp-python"]
[project.urls]
Repository = "https://github.com/goodguy1963/ComfyUI-ThinkingLLM"
@@ -12,4 +12,4 @@ Repository = "https://github.com/goodguy1963/ComfyUI-ThinkingLLM"
PublisherId = "goodguy1963"
DisplayName = "ComfyUI-ThinkingLLM"
Icon = ""
Category = "video"
Category = "image"
+1
View File
@@ -13,6 +13,7 @@ bitsandbytes
accelerate
sentencepiece
tiktoken
faster-whisper
# For running GGUF models with vision support
# The [server] extra installs necessary components for multimodal models
+2014 -18
View File
File diff suppressed because it is too large Load Diff
+96
View File
@@ -0,0 +1,96 @@
"""Check the ThinkingLLM llama-cpp-python backend outside ComfyUI.
Examples:
python tools/check_llama_backend.py
python tools/check_llama_backend.py --model /path/to/model.gguf --gpu-layers 1 --verbose-load
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
from AILab_LlamaCppInstaller import ( # noqa: E402
ensure_llama_cpp_backend,
format_llama_cpp_backend_info,
get_last_llama_cpp_backend_info,
)
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Check ThinkingLLM llama.cpp backend health.")
parser.add_argument("--model", help="Optional GGUF model path for a load smoke test.")
parser.add_argument("--gpu-layers", type=int, default=1, help="GPU layers for the optional model smoke test.")
parser.add_argument("--ctx", type=int, default=512, help="Context size for the optional model smoke test.")
parser.add_argument("--batch", type=int, default=128, help="Batch size for the optional model smoke test.")
parser.add_argument("--no-vision", action="store_true", help="Do not require Qwen vision chat handlers.")
parser.add_argument("--verbose-load", action="store_true", help="Print verbose llama.cpp load logs for the smoke test.")
parser.add_argument("--strict-gpu", action="store_true", help="Exit nonzero if GPU offload cannot be confirmed.")
return parser.parse_args()
def main() -> int:
args = _parse_args()
try:
Llama = ensure_llama_cpp_backend(require_vision_handlers=not args.no_vision)
except Exception as exc:
print(f"BACKEND_IMPORT=failed\n{exc}", file=sys.stderr)
return 2
info = get_last_llama_cpp_backend_info() or {}
print("BACKEND_IMPORT=ok")
print(format_llama_cpp_backend_info(info))
gpu_offload = info.get("gpu_offload")
if args.strict_gpu and gpu_offload is not True:
print("STRICT_GPU=failed: backend did not confirm GPU offload support", file=sys.stderr)
return 3
if args.model:
model_path = Path(args.model).expanduser().resolve()
if not model_path.exists():
print(f"MODEL_LOAD=failed: file not found: {model_path}", file=sys.stderr)
return 4
previous_verbose = os.environ.get("THINKINGLLM_LLAMA_CPP_VERBOSE_LOAD")
if args.verbose_load:
os.environ["THINKINGLLM_LLAMA_CPP_VERBOSE_LOAD"] = "1"
try:
print(
f"MODEL_LOAD=starting model={model_path.name} gpu_layers={args.gpu_layers} "
f"ctx={args.ctx} batch={args.batch}"
)
llm = Llama(
model_path=str(model_path),
n_gpu_layers=int(args.gpu_layers),
n_ctx=int(args.ctx),
n_batch=int(args.batch),
verbose=bool(args.verbose_load),
)
try:
output = llm("Hello", max_tokens=4, temperature=0.0)
print("MODEL_INFERENCE=ok")
print(str(output)[:500])
finally:
close = getattr(llm, "close", None)
if callable(close):
close()
except Exception as exc:
print(f"MODEL_LOAD=failed\n{exc}", file=sys.stderr)
return 5
finally:
if previous_verbose is None:
os.environ.pop("THINKINGLLM_LLAMA_CPP_VERBOSE_LOAD", None)
else:
os.environ["THINKINGLLM_LLAMA_CPP_VERBOSE_LOAD"] = previous_verbose
return 0
if __name__ == "__main__":
raise SystemExit(main())
+80
View File
@@ -1,5 +1,85 @@
# ComfyUI-QwenVL Update Log
## Version 2.3.14 (2026/06/20) — Gemma4 GGUF Stability Release
### 2.3.14 Changes
- Stabilized `google_gemma-4-E4B-it-GGUF` multimodal runs by applying Gemma4-compatible GGUF image settings before llama.cpp execution.
- Pinned llama.cpp `main_gpu` to ComfyUI's selected CUDA device so GGUF loads do not drift to the wrong card after CUDA state changes.
- Blocked unsupported Gemma4 GGUF audio before native llama.cpp can abort the ComfyUI process.
- Reused existing mapped GGUF model files across ComfyUI `LLM` model paths to avoid redundant downloads.
- Removed duplicate legacy ThinkingLLM search entries from the ComfyUI node list.
### 2.3.14 Validation
- Full local regression suite passes.
- Live NVIDIA portable ComfyUI test completed successfully with `google_gemma-4-E4B-it-Q8_0.gguf` and an image prompt.
## Version 2.3.13 (2026/06/20) — Recommendation Note Cleanup
### 2.3.13 Changes
- Removed duplicate compact recommendation-note rows from ThinkingLLM nodes, including Advanced GGUF.
- Kept the real multiline recommendation note visible while pruning stale duplicate `recommended_settings` widgets.
- Prevented recommendation notes from being serialized back into workflows as duplicate UI-only widgets.
### 2.3.13 Validation
- Added regression coverage for recommendation widget deduplication.
- Full local regression suite passes.
## Version 2.3.12 (2026/06/20) — Rollback Recovery Release
### 2.3.12 Changes
- Rebased the public release line onto the rollback-safe GGUF single-pass state from 2.3.11.
- Preserved the ComfyUI execution highlight behavior by no longer overwriting node colors once ComfyUI has set them.
- Restored the saved-workflow compatible `Custom prompt + image (no preset)` option.
### 2.3.12 Validation
- Local regression coverage verifies the execution color guard and the restored no-preset alias.
- Full local regression suite passes.
## Version 2.3.11 (2026/06/19) — GGUF Single-Pass Execution Fix
### 2.3.11 Changes
- Disabled automatic GGUF finalization retries by default so one node execution performs one LLM generation pass unless explicitly configured otherwise.
- Added `auto_finalization_retry` to `ThinkingLLM (GGUF)`, `ThinkingLLM Advanced (GGUF)`, `ThinkingLLM Gemma 4 Audio (GGUF)`, and `ThinkingLLM Prompt Enhancer (GGUF)` for users who still want the old empty/reasoning-only retry behavior.
- Fixed catalogued GGUF model reuse across ComfyUI `extra_model_paths.yaml` `LLM` mappings, preventing unnecessary re-downloads when models already exist in a shared `models/LLM/GGUF` directory.
### 2.3.11 Validation
- Added regression coverage for default single-pass GGUF execution, explicit opt-in finalization retry, GGUF node UI defaults, and shared `LLM/GGUF` model reuse.
- Targeted GGUF tests pass.
## Version 2.3.10 (2026/06/19) — ComfyUI 2.0 Advanced GGUF Layout Fix
### 2.3.10 Changes
- Fixed the large blank area in `ThinkingLLM Advanced (GGUF)` under ComfyUI Nodes 2.0 by no longer hiding real advanced tuning widgets behind a layout hack.
- Kept legacy workflow compatibility shims hidden while exposing `n_ubatch`, thread, flash-attention, KQV offload, and context-checkpoint controls.
- Added regression coverage for the ComfyUI 2.0 layout compatibility rule.
### 2.3.10 Validation
- Targeted Advanced GGUF compatibility tests pass.
- Full local regression test file passes.
## Version 2.3.6 (2026/06/14) — GGUF Context Reset Hardening
### 2.3.6 Changes
- Moved GGUF llama context reset to run immediately before each internal `create_chat_completion` call.
- Covers both normal generation and finalization retry calls while preserving `keep_model_loaded=True` model reuse.
- Added regression coverage that verifies reset happens before completion and before retry/finalization completions.
### 2.3.6 Validation
- Targeted reset regression suite passes.
- Full local test file still has the known Linux-host failures for Windows DLL-directory tests only.
## v1.0.9 (Local Fork) — May 2026
### New Features
+451 -7
View File
@@ -1,4 +1,5 @@
import { app } from "/scripts/app.js";
import { ComfyWidgets } from "/scripts/widgets.js";
const COLOR_THEMES = {
QwenVL: { nodeColor: "#28403f", nodeBgColor: "#374539", width: 340 },
@@ -8,24 +9,108 @@ const COLOR_THEMES = {
};
const NODE_COLORS = {
// QwenVL nodes
"AILab_QwenVL": "QwenVL",
"AILab_QwenVL_Advanced": "QwenVL",
"AILab_QwenVL_PromptEnhancer": "Enhancer",
"AILab_QwenVL_GGUF": "QwenVLGGUF",
"AILab_QwenVL_GGUF_Advanced": "QwenVLGGUF",
"AILab_QwenVL_GGUF_PromptEnhancer": "Enhancer",
// Tools
"ThinkingLLM_QwenVL": "QwenVL",
"ThinkingLLM_QwenVL_Advanced": "QwenVL",
"ThinkingLLM_QwenVL_PromptEnhancer": "Enhancer",
"ThinkingLLM_QwenVL_GGUF": "QwenVLGGUF",
"ThinkingLLM_QwenVL_GGUF_Advanced": "QwenVLGGUF",
"ThinkingLLM_Gemma4_Audio_GGUF": "QwenVLGGUF",
"ThinkingLLM_Whisper_ASR": "QwenVLGGUF",
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer": "Enhancer",
"AILab_QwenVL_PromptLibrary": "Tools",
"VRAMCleanup": "Tools",
"StorySplitNode": "Tools",
};
const THINKINGLLM_NODE_NAMES = new Set([
"AILab_QwenVL",
"AILab_QwenVL_Advanced",
"AILab_QwenVL_PromptEnhancer",
"AILab_QwenVL_GGUF",
"AILab_QwenVL_GGUF_Advanced",
"AILab_QwenVL_GGUF_PromptEnhancer",
"ThinkingLLM_QwenVL",
"ThinkingLLM_QwenVL_Advanced",
"ThinkingLLM_QwenVL_PromptEnhancer",
"ThinkingLLM_QwenVL_GGUF",
"ThinkingLLM_QwenVL_GGUF_Advanced",
"ThinkingLLM_Gemma4_Audio_GGUF",
"ThinkingLLM_Whisper_ASR",
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer",
]);
const AUDIO_CAPABLE_NODE_NAMES = new Set([
"ThinkingLLM_Gemma4_Audio_GGUF",
"ThinkingLLM_Whisper_ASR",
"AILab_QwenVL_GGUF",
"AILab_QwenVL_GGUF_Advanced",
"ThinkingLLM_QwenVL_GGUF",
"ThinkingLLM_QwenVL_GGUF_Advanced",
]);
const GGUF_ADVANCED_INTERNAL_WIDGETS = new Set([
"legacy_seed_mode",
"legacy_unload_after_run",
]);
const RECOMMENDATIONS_URL = new URL("../model_recommendations.json", import.meta.url);
const PRESET_TOOLTIPS_URL = new URL("../preset_tooltips.json", import.meta.url);
const RECOMMENDATION_WIDGET_NAME = "recommended_settings";
const RECOMMENDATION_PLACEHOLDER = "Select a model to see provider/community recommended settings. This note never changes your saved widget values.";
const RECOMMENDATION_FIELDS = ["temperature", "top_p", "top_k", "max_tokens", "ctx_min", "n_batch", "image_max_tokens"];
const AUDIO_UNSUPPORTED_MESSAGE = "No curated audio support for the selected model. Use Gemma 4 Audio for audio understanding or Whisper ASR for transcription.";
const PRESET_WIDGET_NAMES = ["preset_prompt", "enhancement_style", "preset_system_prompt"];
let recommendationsPromise = null;
let presetTooltipsPromise = null;
function loadRecommendations() {
if (!recommendationsPromise) {
recommendationsPromise = fetch(RECOMMENDATIONS_URL)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then((payload) => Array.isArray(payload?.rules) ? payload.rules : [])
.catch((error) => {
console.warn("[ThinkingLLM] Failed to load model recommendations", error);
return [];
});
}
return recommendationsPromise;
}
function loadPresetTooltips() {
if (!presetTooltipsPromise) {
presetTooltipsPromise = fetch(PRESET_TOOLTIPS_URL)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch((error) => {
console.warn("[ThinkingLLM] Failed to load preset tooltips", error);
return {};
});
}
return presetTooltipsPromise;
}
function setNodeColors(node, theme) {
if (!theme) { return; }
if (theme.nodeColor) {
if (!node.color && theme.nodeColor) {
node.color = theme.nodeColor;
}
if (theme.nodeBgColor) {
if (!node.bgcolor && theme.nodeBgColor) {
node.bgcolor = theme.nodeBgColor;
}
if (theme.width) {
@@ -34,17 +119,376 @@ function setNodeColors(node, theme) {
}
}
function findWidget(node, name) {
return node.widgets?.find((widget) => widget.name === name);
}
function hideStableWidget(node, widget) {
if (!widget || widget._thinkingllmHidden) { return; }
widget._thinkingllmHidden = true;
widget._thinkingllmOriginalType = widget.type;
widget._thinkingllmOriginalComputeSize = widget.computeSize;
widget.hidden = true;
widget.type = `thinkingllm_hidden_${widget.type || "widget"}`;
widget.computeSize = () => [0, 0];
widget.draw = () => {};
}
function findWidgets(node, name) {
return node.widgets?.filter((widget) => widget?.name === name) || [];
}
function removeDuplicateRecommendationWidgets(node, keepWidget) {
if (!node.widgets?.length || !keepWidget) {
return;
}
for (let index = node.widgets.length - 1; index >= 0; index -= 1) {
const widget = node.widgets[index];
if (widget?.name !== RECOMMENDATION_WIDGET_NAME || widget === keepWidget) {
continue;
}
hideStableWidget(node, widget);
node.widgets.splice(index, 1);
}
}
function hideGgufAdvancedInternals(node) {
if (!["AILab_QwenVL_GGUF_Advanced", "ThinkingLLM_QwenVL_GGUF_Advanced"].includes(node.comfyClass)) {
return;
}
for (const widgetName of GGUF_ADVANCED_INTERNAL_WIDGETS) {
hideStableWidget(node, findWidget(node, widgetName));
}
if (node.widgets?.length) {
requestAnimationFrame(() => node.setSize([node.size[0], node.computeSize()[1]]));
}
}
function clearHfTokenAfterExecution(node) {
if (!THINKINGLLM_NODE_NAMES.has(node.comfyClass) || node._thinkingllmClearsToken) {
return;
}
node._thinkingllmClearsToken = true;
const originalOnExecuted = node.onExecuted;
node.onExecuted = function (...args) {
const result = originalOnExecuted?.apply(this, args);
const tokenWidget = findWidget(this, "hf_token");
if (tokenWidget?.value) {
tokenWidget.value = "";
app.graph?.setDirtyCanvas?.(true, true);
}
return result;
};
}
function recommendationScope(node) {
if ([
"AILab_QwenVL_GGUF",
"AILab_QwenVL_GGUF_Advanced",
"ThinkingLLM_QwenVL_GGUF",
"ThinkingLLM_QwenVL_GGUF_Advanced",
"ThinkingLLM_Gemma4_Audio_GGUF",
"ThinkingLLM_Whisper_ASR",
"AILab_QwenVL_GGUF_PromptEnhancer",
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer",
].includes(node.comfyClass)) {
return "gguf";
}
return "hf";
}
function matchRecommendationRule(modelName, rules) {
const lowered = String(modelName || "").toLowerCase();
if (!lowered) {
return null;
}
const ordered = [...(rules || [])].sort((left, right) => (right.priority || 0) - (left.priority || 0));
for (const rule of ordered) {
const include = Array.isArray(rule?.match_any) ? rule.match_any : [];
const exclude = Array.isArray(rule?.exclude_any) ? rule.exclude_any : [];
const matches = include.some((token) => lowered.includes(String(token).toLowerCase()));
const blocked = exclude.some((token) => lowered.includes(String(token).toLowerCase()));
if (matches && !blocked) {
return rule;
}
}
return null;
}
function hasWidget(node, widgetName) {
return Boolean(findWidget(node, widgetName));
}
function hasAudioInput(node) {
return AUDIO_CAPABLE_NODE_NAMES.has(node.comfyClass)
|| Boolean(node.inputs?.some((input) => input?.name === "audio"));
}
function formatSetting(fieldName, value) {
if (typeof value !== "number") {
return null;
}
if (fieldName === "ctx_min") {
return `ctx>=${value}`;
}
return `${fieldName}=${value}`;
}
function buildModeSummary(node, settings) {
const parts = [];
for (const fieldName of RECOMMENDATION_FIELDS) {
if (!(fieldName in (settings || {}))) {
continue;
}
if (fieldName === "ctx_min") {
if (!hasWidget(node, "ctx")) {
continue;
}
} else if (fieldName === "top_k") {
if (!hasWidget(node, "top_k")) {
continue;
}
} else if (!hasWidget(node, fieldName)) {
continue;
}
const formatted = formatSetting(fieldName, settings[fieldName]);
if (formatted) {
parts.push(formatted);
}
}
return parts;
}
function buildRecommendationText(node, rules) {
if (node.comfyClass === "ThinkingLLM_Whisper_ASR") {
const modelSize = String(findWidget(node, "model_size")?.value || "small").trim() || "small";
return [
"Whisper ASR transcription",
`Model: ${modelSize}`,
"Audio: Use for speech-to-text from AUDIO or audio_file_path. M4A/MP3/WAV/FLAC are decoded with FFmpeg.",
"Settings: small + CPU/int8 is the reliable Windows default; large-v3 is higher quality; vad_filter skips silence.",
"Dependency: faster-whisper must be installed in the active ComfyUI Python.",
].join("\n");
}
const modelName = String(findWidget(node, "model_name")?.value || "").trim();
if (!modelName || modelName.startsWith("(no ")) {
return RECOMMENDATION_PLACEHOLDER;
}
const rule = matchRecommendationRule(modelName, rules);
if (!rule) {
return [
"Manual settings guide",
`No curated recommendation is bundled yet for ${modelName}.`,
"Your current widget values stay exactly as you set them.",
].join("\n");
}
const thinkingEnabled = Boolean(findWidget(node, "enable_thinking")?.value);
const activeSettings = thinkingEnabled ? rule.thinking_on : rule.thinking_off;
const alternateSettings = thinkingEnabled ? rule.thinking_off : rule.thinking_on;
const activeSummary = buildModeSummary(node, activeSettings);
const alternateSummary = buildModeSummary(node, alternateSettings);
const scope = recommendationScope(node);
const lines = [
rule.family_label || "Manual settings guide",
`Current toggle: ${thinkingEnabled ? "thinking ON" : "thinking OFF"}`,
];
const supportNote = rule.support_notes?.[scope];
if (supportNote) {
lines.push(`Toggle: ${supportNote}`);
}
const audioNote = rule.support_notes?.audio;
if (audioNote && hasAudioInput(node)) {
lines.push(`Audio: ${audioNote}`);
} else if (hasAudioInput(node)) {
lines.push(`Audio: ${AUDIO_UNSUPPORTED_MESSAGE}`);
}
lines.push(`Thinking ON: ${(thinkingEnabled ? activeSummary : alternateSummary).join(" | ") || "use the provider defaults"}`);
lines.push(`Thinking OFF: ${(thinkingEnabled ? alternateSummary : activeSummary).join(" | ") || "use the provider defaults"}`);
const softSwitch = rule.support_notes?.soft_switch;
if (softSwitch) {
lines.push(`Note: ${softSwitch}`);
}
if (Array.isArray(rule.notes) && rule.notes.length) {
lines.push(`Extra: ${rule.notes[0]}`);
}
if (Array.isArray(rule.sources) && rule.sources.length) {
lines.push(`Sources: ${rule.sources.join(" + ")}`);
}
return lines.join("\n");
}
function ensureRecommendationWidget(node) {
if (!THINKINGLLM_NODE_NAMES.has(node.comfyClass)) {
return null;
}
let widget = findWidgets(node, RECOMMENDATION_WIDGET_NAME)
.find((candidate) => candidate?.inputEl)
|| findWidget(node, RECOMMENDATION_WIDGET_NAME);
if (!widget) {
widget = ComfyWidgets.STRING(node, RECOMMENDATION_WIDGET_NAME, ["STRING", { multiline: true }], app).widget;
}
removeDuplicateRecommendationWidgets(node, widget);
if (widget?.inputEl) {
widget.inputEl.readOnly = true;
widget.inputEl.style.opacity = 0.78;
widget.inputEl.style.fontSize = "12px";
widget.inputEl.style.lineHeight = "1.35";
widget.inputEl.rows = 6;
}
widget.options = { ...(widget.options || {}), serialize: false };
widget.serializeValue = async () => undefined;
if (!widget.value) {
widget.value = RECOMMENDATION_PLACEHOLDER;
}
return widget;
}
function resizeNode(node) {
requestAnimationFrame(() => {
const size = node.computeSize();
if (size[0] < node.size[0]) {
size[0] = node.size[0];
}
if (size[1] < node.size[1]) {
size[1] = node.size[1];
}
node.setSize(size);
app.graph?.setDirtyCanvas?.(true, false);
});
}
function updateRecommendationWidget(node) {
const widget = ensureRecommendationWidget(node);
if (!widget) {
return;
}
loadRecommendations().then((rules) => {
widget.value = buildRecommendationText(node, rules);
resizeNode(node);
});
}
function queueRecommendationRefresh(node) {
if (!node || node._thinkingllmRecommendationQueued) {
return;
}
node._thinkingllmRecommendationQueued = true;
requestAnimationFrame(() => {
node._thinkingllmRecommendationQueued = false;
updateRecommendationWidget(node);
});
}
function summarizePresetTooltip(prompt) {
const text = String(prompt || "").trim();
if (!text) {
return "No preset prompt is active. Use the custom prompt field, or combine multiple text nodes for longer prompt recipes.";
}
return text.length > 1800 ? `${text.slice(0, 1800).trim()}...` : text;
}
function updatePresetTooltip(widget, promptMap) {
if (!widget) {
return;
}
const prompt = promptMap?.[widget.name]?.[widget.value];
const tooltip = summarizePresetTooltip(prompt);
widget.options = { ...(widget.options || {}), tooltip };
widget.tooltip = tooltip;
if (widget.inputEl) {
widget.inputEl.title = tooltip;
}
}
function hookPresetTooltipPreviews(node) {
if (!THINKINGLLM_NODE_NAMES.has(node.comfyClass) || node._thinkingllmPresetTooltipsHooked) {
return;
}
node._thinkingllmPresetTooltipsHooked = true;
loadPresetTooltips().then((promptMap) => {
const refresh = () => {
for (const widgetName of PRESET_WIDGET_NAMES) {
updatePresetTooltip(findWidget(node, widgetName), promptMap);
}
};
for (const widgetName of PRESET_WIDGET_NAMES) {
const target = findWidget(node, widgetName);
if (!target || target._thinkingllmPresetTooltipCallbackHooked) {
continue;
}
target._thinkingllmPresetTooltipCallbackHooked = true;
const originalCallback = target.callback;
target.callback = function (...args) {
const result = originalCallback?.apply(this, args);
refresh();
return result;
};
}
const originalConfigure = node.onConfigure;
node.onConfigure = function (...args) {
const result = originalConfigure?.apply(this, args);
refresh();
return result;
};
refresh();
});
}
function hookRecommendationUpdates(node) {
if (!THINKINGLLM_NODE_NAMES.has(node.comfyClass) || node._thinkingllmRecommendationHooked) {
return;
}
node._thinkingllmRecommendationHooked = true;
ensureRecommendationWidget(node);
for (const widgetName of ["model_name", "enable_thinking"]) {
const target = findWidget(node, widgetName);
if (!target || target._thinkingllmRecommendationCallbackHooked) {
continue;
}
target._thinkingllmRecommendationCallbackHooked = true;
const originalCallback = target.callback;
target.callback = function (...args) {
const result = originalCallback?.apply(this, args);
queueRecommendationRefresh(node);
return result;
};
}
const originalConfigure = node.onConfigure;
node.onConfigure = function (...args) {
const result = originalConfigure?.apply(this, args);
queueRecommendationRefresh(this);
return result;
};
queueRecommendationRefresh(node);
}
const ext = {
name: "QwenVL.appearance",
nodeCreated(node) {
const nclass = node.comfyClass;
if (NODE_COLORS.hasOwnProperty(nclass)) {
let colorKey = NODE_COLORS[nclass];
const colorKey = NODE_COLORS[nclass];
const theme = COLOR_THEMES[colorKey];
setNodeColors(node, theme);
}
hideGgufAdvancedInternals(node);
hookRecommendationUpdates(node);
hookPresetTooltipPreviews(node);
clearHfTokenAfterExecution(node);
}
};
app.registerExtension(ext);
app.registerExtension(ext);
+239
View File
@@ -0,0 +1,239 @@
{
"version": 1,
"rules": [
{
"id": "qwen36_hauhau_multimodal",
"priority": 100,
"match_any": [
"Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive"
],
"family_label": "Qwen3.6 35B A3B HauhauCS aggressive",
"sources": [
"HauhauCS model card",
"Qwen3.5 official model card"
],
"support_notes": {
"hf": "Not in the HF lineup here. Use the GGUF nodes for this model.",
"gguf": "Hard toggle via enable_thinking. Pair the vision node with the matching mmproj file.",
"soft_switch": "No official /think or /no_think soft switch for this family."
},
"thinking_on": {
"temperature": 1.0,
"top_p": 0.95,
"top_k": 20,
"max_tokens": 32768,
"ctx_min": 131072
},
"thinking_off": {
"temperature": 0.7,
"top_p": 0.8,
"top_k": 20,
"max_tokens": 32768,
"ctx_min": 131072
},
"notes": [
"For creative prompt generation, use the general-task sampler: thinking ON for richer scene planning, thinking OFF for shorter direct rewrites.",
"Q4_K_P is the best quality-size starting point; move to Q6_K_P or Q8_K_P when VRAM allows.",
"Keep a large context when expanding long image/video prompt sequences so character and scene continuity are not lost."
]
},
{
"id": "qwen35_family",
"priority": 80,
"match_any": [
"Qwen3.5",
"qwen3.5"
],
"family_label": "Qwen3.5 family",
"sources": [
"Qwen3.5 official model card"
],
"support_notes": {
"hf": "Hard toggle via enable_thinking when the backend honors the chat template.",
"gguf": "Hard toggle via enable_thinking in the Qwen chat template.",
"soft_switch": "No official /think or /no_think soft switch for Qwen3.5."
},
"thinking_on": {
"temperature": 1.0,
"top_p": 0.95,
"top_k": 20,
"max_tokens": 32768,
"ctx_min": 131072
},
"thinking_off": {
"temperature": 0.7,
"top_p": 0.8,
"top_k": 20,
"max_tokens": 32768,
"ctx_min": 131072
},
"notes": [
"For creative prompt generation, Qwen's general-task settings are the right fit: thinking ON for richer planning, non-thinking for concise prompt rewrites.",
"Use the larger output budget for multi-shot scene descriptions, storyboard continuity, or detailed style transfer prompts.",
"If generations loop or over-elaborate, prefer a shorter custom prompt and lower max_tokens before changing the sampler."
]
},
{
"id": "qwen3_family",
"priority": 70,
"match_any": [
"Qwen3VL-",
"qwen3vl-",
"Qwen3-VL",
"Qwen3-",
"qwen3-"
],
"exclude_any": [
"Qwen3.5",
"qwen3.5",
"Qwen3.6",
"qwen3.6"
],
"family_label": "Qwen3 family",
"sources": [
"Qwen3 official model card",
"Qwen Transformers docs"
],
"support_notes": {
"hf": "Hard toggle via enable_thinking, with /think and /no_think available as soft switches.",
"gguf": "Hard toggle via enable_thinking. /think and /no_think are available only on Qwen3 chat-template builds.",
"soft_switch": "Qwen3 officially supports /think and /no_think when thinking is enabled."
},
"thinking_on": {
"temperature": 0.6,
"top_p": 0.95,
"top_k": 20,
"max_tokens": 32768,
"ctx_min": 32768
},
"thinking_off": {
"temperature": 0.7,
"top_p": 0.8,
"top_k": 20,
"max_tokens": 32768,
"ctx_min": 32768
},
"notes": [
"For creative prompt drafting, use thinking ON when you want structured scene planning and thinking OFF when you want a fast final prompt.",
"Provider recommends 32K output budget for most tasks and only enabling YaRN when you really need longer than the native 32K context.",
"Avoid greedy sampling for creative generation; the recommended sampler gives more varied but still coherent prompt language."
]
},
{
"id": "gemma4_12b_unified",
"priority": 65,
"match_any": [
"Gemma-4-12B",
"gemma-4-12b"
],
"family_label": "Gemma 4 12B Unified",
"sources": [
"Google Gemma 4 model overview",
"Unsloth Gemma 4 12B GGUF model card"
],
"support_notes": {
"hf": "Gemma 4 12B is a unified multimodal model. HF support depends on current Transformers processor support for image/audio/video inputs.",
"gguf": "Use Gemma4ChatHandler in a recent multimodal llama-cpp-python build. Pair the model with mmproj-F16.gguf.",
"audio": "Audio input is intended for Gemma 4 E2B, E4B, and 12B only. Use a recent Gemma4ChatHandler build; 16 kHz mono WAV-style audio is safest.",
"soft_switch": "Gemma uses configurable thinking controls instead of Qwen-style /think or /no_think soft switches."
},
"thinking_on": {
"temperature": 1.0,
"top_p": 0.95,
"top_k": 64,
"max_tokens": 1024,
"ctx_min": 262144,
"image_max_tokens": 4096
},
"thinking_off": {
"temperature": 1.0,
"top_p": 0.95,
"top_k": 64,
"max_tokens": 1024,
"ctx_min": 262144,
"image_max_tokens": 4096
},
"notes": [
"For creative prompt generation, Gemma 4's default temperature/top_p is intentionally open enough for varied visual language.",
"For audio analysis, 16 kHz mono WAV is the safest input shape.",
"Prefer Q4_K_M or UD-Q4_K_XL first; move up to Q6/Q8/BF16 when VRAM allows."
]
},
{
"id": "gemma4_large_family",
"priority": 60,
"match_any": [
"Gemma-4-26B",
"gemma-4-26B",
"Gemma-4-31B",
"gemma-4-31B"
],
"family_label": "Gemma 4 large family",
"sources": [
"Gemma 4 official model card"
],
"support_notes": {
"hf": "Gemma 4 supports configurable thinking, but current repo behavior can vary by backend.",
"gguf": "Use the settings manually and verify behavior in RAW_TRACE because GGUF support varies by backend.",
"soft_switch": "Gemma uses chat-template thinking controls instead of /think or /no_think."
},
"thinking_on": {
"temperature": 1.0,
"top_p": 0.95,
"top_k": 64,
"max_tokens": 1024,
"ctx_min": 262144
},
"thinking_off": {
"temperature": 1.0,
"top_p": 0.95,
"top_k": 64,
"max_tokens": 1024,
"ctx_min": 262144
},
"notes": [
"For creative prompt generation, keep Gemma 4 near its official sampler defaults before tuning style or length.",
"Official context target is 256K on 26B A4B and 31B variants.",
"If the backend does not expose top_k, keep temperature and top_p at the official defaults."
]
},
{
"id": "gemma4_small_family",
"priority": 50,
"match_any": [
"Gemma-4-E2B",
"gemma-4-E2B",
"Gemma-4-E4B",
"gemma-4-E4B"
],
"family_label": "Gemma 4 small family",
"sources": [
"Gemma 4 official model card"
],
"support_notes": {
"hf": "Gemma 4 supports configurable thinking, but current repo behavior can vary by backend.",
"gguf": "Use the settings manually and verify behavior in RAW_TRACE because GGUF support varies by backend.",
"soft_switch": "Gemma uses chat-template thinking controls instead of /think or /no_think."
},
"thinking_on": {
"temperature": 1.0,
"top_p": 0.95,
"top_k": 64,
"max_tokens": 1024,
"ctx_min": 131072
},
"thinking_off": {
"temperature": 1.0,
"top_p": 0.95,
"top_k": 64,
"max_tokens": 1024,
"ctx_min": 131072
},
"notes": [
"For creative prompt generation, keep Gemma 4 near its official sampler defaults before tuning style or length.",
"Official context target is 128K on E2B and E4B variants.",
"Gemma recommends higher visual token budgets only for OCR or fine detail tasks; lower budgets are faster for captioning or video summaries."
]
}
]
}
+50
View File
@@ -0,0 +1,50 @@
{
"preset_prompt": {
"🚫 No preset (image-only)": "",
"💬 Custom prompt + image (no preset)": "",
"🎦 LTX 2.3 NSFW I2V Scene": "You are an expert cinematic prompt director for LTX 2.3 image-to-video generation. Convert the user input and visual input into one fluent English cinematic prompt.\n\nRead the image first, then integrate the user text. The image first rule is mandatory: visual facts from the image have priority over contradictory user text. Describe the visible subject, setting, action, pose, camera angle, lens feel, lighting, color, texture, and environment before adding requested motion or story intent.\n\nWrite a single flowing paragraph, 150-250 words, with chronological action and scene progression. Start directly with the main action. Include specific movements, gestures, object or character appearances, background details, camera movement, lighting behavior, and any sudden changes. Keep the prompt literal, precise, and usable for LTX 2.3. If the source contains NSFW adult content, describe it according to the user intent and visual evidence without adding unrelated content.\n\nOutput the enhanced prompt only. No headings, labels, notes, examples, markdown, or reasoning. Do not exceed 250 words.",
"🍿 Wan 2.2 NSFW I2V Timeline (3s)": "You are a system that converts user prompts from any language and visual inputs into optimized cinematic English descriptions for WAN 2.2 I2V generation.\n\nSteps:\n1. Read the user's input and understand the intention, atmosphere, and desired visual style.\n2. Analyze the provided image or video to extract visual context (subjects, motion, lighting, style, environment).\n3. Combine both sources to create an English prompt optimized for video generation with WAN 2.2.\n\nIMPORTANT: If NO user text input is provided, base the ENTIRE video generation SOLELY on the visual content of the provided image. The image contains all necessary visual information for video generation. Do NOT wait for text input or ask for clarification - proceed with image-only analysis.\n\nStyle Detection and Application:\n- If the user mentions specific visual styles (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), prioritize that style over photorealism.\n- If no specific style is mentioned but the input image/video has stylized characteristics, detect and enhance the existing style.\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nRules:\n- The output must be in fluent English.\n- Structure the description as a precise, continuous scene with a timeline indicated for each second. The video is 3 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...)\").\n- Describe the scene with comprehensive artistic details adapted to the requested style: subject(s) and action with style characteristics; environment and atmosphere with artistic elements; lighting and color with stylistic features; camera work and composition adapted to the style.\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- The description should be vivid and cinematic, suitable for high-quality video generation.\n- The output should be a single continuous paragraph with timeline markers for each second.\n- Focus on creating a visually rich and stylistically consistent scene.\n\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\nTechnical specifications:\n- Output length: approximately 200-400 words\n- Include specific details about subjects, actions, environment, lighting, and style elements\n- Maintain consistency with the requested visual style throughout the timeline\n- Ensure smooth transitions between each second of timeline",
"🍿 Wan 2.2 NSFW I2V Timeline (5s)": "You are a system that converts user prompts from any language and visual inputs into optimized cinematic English descriptions for WAN 2.2 I2V generation.\n\nSteps:\n1. Read the user's input and understand the intention, atmosphere, and desired visual style.\n2. Analyze the provided image or video to extract visual context (subjects, motion, lighting, style, environment).\n3. Combine both sources to create an English prompt optimized for video generation with WAN 2.2.\n\nIMPORTANT: If NO user text input is provided, base the ENTIRE video generation SOLELY on the visual content of the provided image. The image contains all necessary visual information for video generation. Do NOT wait for text input or ask for clarification - proceed with image-only analysis.\n\nStyle Detection and Application:\n- If the user mentions specific visual styles (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), prioritize that style over photorealism.\n- If no specific style is mentioned but the input image/video has stylized characteristics, detect and enhance the existing style.\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nRules:\n- The output must be in fluent English.\n- Structure the description as a precise, continuous scene with a timeline indicated for each second. The video is 5 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\").\n- Describe the scene with comprehensive artistic details adapted to the requested style: subject(s) and action with style characteristics; environment and atmosphere with artistic elements; lighting and color with stylistic features; camera work and composition adapted to the style.\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- The description should be vivid and cinematic, suitable for high-quality video generation.\n- The output should be a single continuous paragraph with timeline markers for each second.\n- Focus on creating a visually rich and stylistically consistent scene.\n\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\nTechnical specifications:\n- Output length: approximately 200-400 words\n- Include specific details about subjects, actions, environment, lighting, and style elements\n- Maintain consistency with the requested visual style throughout the timeline\n- Ensure smooth transitions between each second of timeline",
"🎥 Wan 2.2 NSFW I2V Scene (3s)": "Convert user input + image into ONE single cinematic WAN 2.2 prompt (3s total).\n\nIMPORTANT: If NO user text input is provided, base the ENTIRE video generation SOLELY on the visual content of the provided image. The image contains all necessary visual information for video generation. Do NOT wait for text input or ask for clarification - proceed with image-only analysis.\n\nStyle Priority: User-specified > detected from image > default style\n- If user explicitly mentions a style (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), use that style\n- Else if image has clear stylized characteristics, detect and enhance the existing style from the image\n- Else default to cinematic photorealism with realistic lighting and natural motion\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nCRITICAL FORMAT:\n- Generate exactly ONE single prompt\n- Single prompt = 3 seconds of video\n- NO timeline markers like \"At X seconds:\"\n- 6-10 sentences for rich cinematic detail\n- Focus on complete scene description\n- DO NOT copy example text - generate original content\n\nTECHNICAL RULES:\n- Fluent English only\n- Describe visible elements only (no invented lighting)\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- No preface/reasoning/notes\n- Rich cinematic descriptions with style details\n- RECOMMENDED: Use max_tokens=512 or higher for complete output\n\nPERFORMANCE OPTIMIZATION:\n- Set context_length to 16384 or higher for better quality\n- Use image_max_tokens=2048 for detailed visual analysis\n- Set n_batch=256 for efficient processing\n- Temperature 0.7-0.8 for creative but coherent output\n\nIMPORTANT: Generate actual cinematic prompt, not example text.",
"🎥 Wan 2.2 NSFW I2V Scene (5s)": "Convert user input + image into ONE single cinematic WAN 2.2 prompt (5s total).\n\nIMPORTANT: If NO user text input is provided, base the ENTIRE video generation SOLELY on the visual content of the provided image. The image contains all necessary visual information for video generation. Do NOT wait for text input or ask for clarification - proceed with image-only analysis.\n\nStyle Priority: User-specified > detected from image > default style\n- If user explicitly mentions a style (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), use that style\n- Else if image has clear stylized characteristics, detect and enhance the existing style from the image\n- Else default to cinematic photorealism with realistic lighting and natural motion\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nCRITICAL FORMAT:\n- Generate exactly ONE single prompt\n- Single prompt = 5 seconds of video\n- NO timeline markers like \"At X seconds:\"\n- 8-12 sentences for rich cinematic detail\n- Focus on complete scene description\n- DO NOT copy example text - generate original content\n\nTECHNICAL RULES:\n- Fluent English only\n- Describe visible elements only (no invented lighting)\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- No preface/reasoning/notes\n- Rich cinematic descriptions with style details\n- RECOMMENDED: Use max_tokens=512 or higher for complete output\n\nPERFORMANCE OPTIMIZATION:\n- Set context_length to 16384 or higher for better quality\n- Use image_max_tokens=2048 for detailed visual analysis\n- Set n_batch=256 for efficient processing\n- Temperature 0.7-0.8 for creative but coherent output\n\nIMPORTANT: Generate actual cinematic prompt, not example text.",
"🎬 Wan 2.2 NSFW I2V Timeline (20s)": "Convert user input + image into FOUR continuous 5-second WAN 2.2 prompts (20s total).\n\nCRITICAL RULES:\n1. MAINTAIN EXACT SAME STYLE: If image is anime → ALL prompts must be anime. If realistic → ALL prompts must be realistic. NEVER change style between prompts.\n\n2. MAINTAIN EXACT SAME CHARACTERS: If image shows 2 women + 1 man → ALL 4 prompts must show 2 women + 1 man. NEVER add/remove/change characters.\n\n3. MAINTAIN EXACT SAME SCENE: Keep the same environment, lighting, composition throughout all 4 prompts.\n\n4. TIMELINE STRUCTURE: Each prompt is 0-5 seconds, representing different moments in the 20-second sequence. MUST use EXACT format with parentheses:\n - Prompt 1: (At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\n - Prompt 2: (At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\n - Prompt 3: (At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\n - Prompt 4: (At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\n\n5. SEPARATE WITH BLANK LINE: One blank line between each prompt.\n\n6. NO LABELS: DO NOT write \"Prompt 1:\", \"Prompt 2:\", \"Prompt 3:\", \"Prompt 4:\" at the beginning of each prompt. Start directly with the timeline markers.\n\n7. PARENTHESES REQUIRED: MUST use parentheses (At X seconds: ...) format. Do not omit parentheses.\n\n8. NSFW: When present, describe ALL content explicitly without censorship.\n\nNO EXCEPTIONS: Style, characters, and scene must remain IDENTICAL across all 4 prompts. Each prompt uses 0-5 second timeline structure with REQUIRED parentheses.",
"📖 Wan 2.2 NSFW I2V Scene (20s)": "Convert user input + image into EXACTLY FOUR continuous 5-second WAN 2.2 prompts (20s total).\n\nIMPORTANT: If NO user text input is provided, base the ENTIRE video generation SOLELY on the visual content of the provided image. The image contains all necessary visual information for video generation. Do NOT wait for text input or ask for clarification - proceed with image-only analysis.\n\nStyle Priority: User-specified > detected > photorealism\n- Photorealistic: realistic lighting, natural textures, lifelike details, and authentic motion\n- Anime: vibrant colors, simplified backgrounds, anime motion\n- Cartoon: bold outlines, flat colors, exaggerated expressions, and cartoon-style animation\n- 3D animation: 3D rendering characteristics, depth, lighting effects, and smooth 3D motion\n- Fantasy: magical elements, ethereal lighting, fantastical details, and imaginative atmosphere\n- Artistic portrait: painterly qualities, artistic brushwork, stylized composition, and creative interpretation\n\nCRITICAL FORMAT:\n- MUST generate EXACTLY FOUR separate prompts - NO MORE, NO LESS\n- Each prompt = 5 seconds of video (0-5s, 5-10s, 10-15s, 15-20s)\n- MANDATORY: Separate prompts with exactly one blank line (press Enter once)\n- NO timeline markers like \"At X seconds:\"\n- Each prompt: 8-12 sentences for rich detail\n- Ensure narrative continuity between parts\n- DO NOT copy example text - generate original content\n- STOP after exactly FOUR prompts - do not generate additional content\n\nHARD ENFORCEMENT:\n- Count your output: MUST be exactly FOUR prompts separated by blank lines\n- If you generate FIVE or more prompts, you have failed the task\n- If you generate THREE or fewer prompts, you have failed the task\n- The structure must be: Prompt1[blank line]Prompt2[blank line]Prompt3[blank line]Prompt4\n- Nothing else after the FOURTH prompt\n\nTECHNICAL RULES:\n- Fluent English only\n- Describe visible elements only (no invented lighting)\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- CRITICAL: Each prompt should flow naturally from the previous one. Avoid mechanical transition phrases, but maintain logical scene progression. Focus on seamless narrative flow.\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- No preface/reasoning/notes\n- Rich cinematic descriptions with style details\n- RECOMMENDED: Use max_tokens=1024 or higher for complete output\n\nPERFORMANCE OPTIMIZATION:\n- Set context_length to 32768 or higher for better continuity\n- Use image_max_tokens=4096 for detailed visual analysis\n- Set n_batch=512 for efficient processing\n- Temperature 0.7-0.8 for creative but coherent output\n\nFINAL REMINDER: EXACTLY FOUR PROMPTS - NO EXCEPTIONS!",
"🖼️ Tags": "Your task is to generate a clean list of comma-separated tags for a text-to-image AI, based *only* on visual information in the image. 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.",
"🖼️ Simple Description": "Analyze the image and write a single concise sentence that describes the main subject and setting. Keep it grounded in visible details only.",
"🖼️ Detailed Description": "Write ONE detailed paragraph (6-10 sentences). Describe only what is visible: subject(s) and actions; people details if present (approx age group, gender expression if clear, hair, facial expression, pose, clothing, accessories); environment (location type, background elements, time cues); lighting (source, direction, softness/hardness, color temperature, shadows); camera viewpoint (eye-level/low/high, distance) and composition (framing, focal emphasis). No preface, no reasoning, no emojis.",
"🖼️ Ultra Detailed Description": "Write ONE ultra-detailed paragraph (10-16 sentences, ~180-320 words). Stay grounded in visible details. Include: subject micro-details (materials, textures, patterns, wear, reflections); people details if present (hair, skin tones, makeup, jewelry, fabric types, fit); environment depth (foreground/midground/background, signage/props, surface materials); lighting analysis (key/fill/back light, direction, softness, highlights, shadow shape); camera perspective (angle, lens feel, depth of field) and composition (leading lines, negative space, symmetry/asymmetry, visual hierarchy). No preface, no reasoning, no emojis.",
"🎬 Cinematic Description": "Write ONE cinematic paragraph (8-12 sentences). Describe the scene like a film still: subject(s) and action; environment and atmosphere; lighting design (practical lights vs ambient, direction, contrast); camera language (shot type, angle, lens feel, depth of field, motion implied); composition and mood. Keep it vivid but factual (no made-up story). No preface, no reasoning, no emojis.",
"🖼️ Detailed Analysis": "Output ONLY these sections with short labels (no bullets): Subject; People (if any); Environment; Lighting; Camera/Composition; Color/Texture. In each section, write 2-4 sentences of concrete visible details. If something is not visible, write 'not visible'. No preface, no reasoning, no emojis.",
"📹 Video Summary": "Summarize the key events and narrative points in this video."
},
"enhancement_style": {
"📖 LTX 2.3 NSFW T2V Scene": "You are an expert cinematic prompt director for LTX 2.3 text-to-video generation. Convert the user input into one fluent English cinematic prompt.\n\nFocus on detailed, chronological descriptions of actions and scenes. Start directly with the main action, then add specific movements, gestures, character or object appearances, background and environmental details, camera angle, camera movement, lighting, colors, textures, and any sudden changes. Keep the result literal, precise, and optimized for LTX 2.3 motion.\n\nDo not change the user input intent; enhance it. Preserve the requested subject, style, action, mood, and constraints. If the user input contains NSFW adult content, describe it according to the user intent without adding unrelated content.\n\nWrite a single flowing paragraph under 150 words. Output the enhanced prompt only. No headings, labels, notes, examples, markdown, or reasoning.",
"📖 Wan 2.2 NSFW T2V Scene (20s)": "Generate 1 single T2V prompt for WAN 2.2 video generation (5s total) to establish initial scene.\n\nAPPROACH:\n- Single prompt: T2V (text-to-video) to create the opening scene\n- Establish characters, environment, and initial action\n- Rich cinematic description with style details\n- 8-12 sentences for comprehensive setup\n\nCRITICAL FORMAT:\n- Generate exactly 1 prompt only\n- No separation needed\n- NO labels like \"Prompt 1\", etc.\n- Focus on establishing the complete scene\n- Maintain consistent style\n\nSTYLE PRIORITY:\nUser-specified > detected > default style\n- If user explicitly mentions a style (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), use that style\n- Else default to cinematic photorealism with realistic lighting and natural motion\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nTECHNICAL RULES:\n- Fluent English only\n- Describe visible elements only\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- No preface/reasoning/notes\n- Temperature 0.7-0.8 for creative output\n\nCRITICAL: Generate exactly 1 comprehensive T2V prompt to establish the scene.",
"🍿 Wan 2.2 NSFW T2V Timeline (3)": "You are a system that converts user prompts from any language into optimized cinematic English descriptions for WAN 2.2 T2V generation.\n\nSteps:\n1. Read the user's input and understand the intention, atmosphere, and desired visual style.\n2. Create an English prompt optimized for video generation with WAN 2.2.\n\nStyle Detection and Application:\n- If the user mentions specific visual styles (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), prioritize that style over photorealism.\n- If no specific style is mentioned but the user input suggests stylized characteristics, detect and enhance the existing style.\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nRules:\n- The output must be in fluent English.\n- Structure the description as a precise, continuous scene with a timeline indicated for each second. The video is 3 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...)\").\n- Describe the scene with comprehensive artistic details adapted to the requested style: subject(s) and action with style characteristics; environment and atmosphere with artistic elements; stylized lighting design; artistic camera language; stylized composition and mood.\n- Include style-specific technical elements: For photorealistic - realistic lighting, natural textures, lifelike details, and authentic motion; For anime - vibrant colors, simplified backgrounds, anime-style motion; For cartoon - bold outlines, flat colors, exaggerated expressions, and cartoon-style animation; For 3D animation - 3D rendering characteristics, depth, lighting effects, and smooth 3D motion; For fantasy - magical elements, ethereal lighting, fantastical details, and imaginative atmosphere; For artistic portrait - painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n- Specify ONLY visible technical elements adapted for style: light sources actually present in the image, light quality as observed, time period if evident, shot types matching the image, composition types visible, lens specifications based on perspective, camera movements if present, color tone from the image. Do not invent or add lighting not present in the original image.\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- CRITICAL: DO NOT use transition phrases like \"As the scene progresses\", \"Moving forward\", \"Next\", \"Then\", \"After that\", \"The scene continues\", etc. Describe each prompt as a standalone scene\n- Keep it between 1-6 sentences per timeline entry.\n- No preface, no reasoning.\n- Do not write any Note at the end of the prompt. The video is 3 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...)\").",
"🍿 Wan 2.2 NSFW T2V Timeline (5s)": "You are a system that converts user prompts from any language into optimized cinematic English descriptions for WAN 2.2 T2V generation.\n\nSteps:\n1. Read the user's input and understand the intention, atmosphere, and desired visual style.\n2. Create an English prompt optimized for video generation with WAN 2.2.\n\nStyle Detection and Application:\n- If the user mentions specific visual styles (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), prioritize that style over photorealism.\n- If no specific style is mentioned but the user input suggests stylized characteristics, detect and enhance the existing style.\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nRules:\n- The output must be in fluent English.\n- Structure the description as a precise, continuous scene with a timeline indicated for each second. The video is 5 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\").\n- Describe the scene with comprehensive artistic details adapted to the requested style: subject(s) and action with style characteristics; environment and atmosphere with artistic elements; stylized lighting design; artistic camera language; stylized composition and mood.\n- Include style-specific technical elements: For photorealistic - realistic lighting, natural textures, lifelike details, and authentic motion; For anime - vibrant colors, simplified backgrounds, anime-style motion; For cartoon - bold outlines, flat colors, exaggerated expressions, and cartoon-style animation; For 3D animation - 3D rendering characteristics, depth, lighting effects, and smooth 3D motion; For fantasy - magical elements, ethereal lighting, fantastical details, and imaginative atmosphere; For artistic portrait - painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n- Specify ONLY visible technical elements adapted for style: light sources actually present in the image, light quality as observed, time period if evident, shot types matching the image, composition types visible, lens specifications based on perspective, camera movements if present, color tone from the image. Do not invent or add lighting not present in the original image.\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- CRITICAL: DO NOT use transition phrases like \"As the scene progresses\", \"Moving forward\", \"Next\", \"Then\", \"After that\", \"The scene continues\", etc. Describe each prompt as a standalone scene\n- Keep it between 1-6 sentences per timeline entry.\n- No preface, no reasoning.\n- Do not write any Note at the end of the prompt. The video is 5 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\").",
"🎬 Wan 2.2 NSFW T2V Timeline (20s)": "Convert user input into 4 continuous 5-second WAN 2.2 prompts (20s total) using timeline structure.\n\nSteps:\n1. Read the user's input and understand the intention, atmosphere, and desired visual style.\n2. Generate 4 separate prompts that maintain narrative and visual continuity.\n\nStyle Detection and Application:\n- If the user mentions specific visual styles (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), prioritize that style over photorealism.\n- If no specific style is mentioned but the user input suggests stylized characteristics, detect and enhance the existing style.\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nRules:\n- The output must be in fluent English.\n- Generate exactly 4 separate prompts, each with timeline structure\n- Each prompt: 5 seconds with timeline markers (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\")\n- MANDATORY: Separate prompts with exactly one blank line (press Enter once)\n- CRITICAL CHARACTER CONTINUITY: Each prompt MUST maintain ALL characters and elements from previous prompt - DO NOT remove or change characters between prompts. If there are 2 people in prompt 1, there must be the SAME 2 people in prompts 2, 3, and 4. No character disappearance allowed. This is MANDATORY.\n- Describe the scene with comprehensive artistic details adapted to the requested style: subject(s) and action with style characteristics; environment and atmosphere with artistic elements; stylized lighting design; artistic camera language; stylized composition and mood.\n- Include style-specific technical elements: For photorealistic - realistic lighting, natural textures, lifelike details, and authentic motion; For anime - vibrant colors, simplified backgrounds, anime-style motion; For cartoon - bold outlines, flat colors, exaggerated expressions, and cartoon-style animation; For 3D animation - 3D rendering characteristics, depth, lighting effects, and smooth 3D motion; For fantasy - magical elements, ethereal lighting, fantastical details, and imaginative atmosphere; For artistic portrait - painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n- Specify ONLY visible technical elements adapted for style: light sources based on scene requirements, light quality as appropriate for the style, time period if evident, shot types matching the scene, composition types visible, lens specifications based on perspective, camera movements if present, color tone appropriate for the style.\n- Maintain consistent character appearance, color tone, lighting, background elements, and cinematic composition according to the requested style.\n- Include dynamic camera movement and ensure logical plot progression with style consistency.\n- WHEN there are NSFW content, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- CRITICAL: DO NOT use transition phrases like \"As the scene progresses\", \"Moving forward\", \"Next\", \"Then\", \"After that\", \"The scene continues\", etc. Describe each prompt as a standalone scene\n- Keep it between 1-6 sentences per timeline entry.\n- No preface, no reasoning.\n- Do not write any Note at the end of the prompt",
"🎥 Wan 2.2 NSFW T2V Scene (3s)": "Convert user input into 1 single cinematic WAN 2.2 prompt (3s total).\n\nStyle Priority: User-specified > detected from image > default style\n- If user explicitly mentions a style (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), use that style\n- Else if image has clear stylized characteristics, detect and enhance the existing style from the image\n- Else default to cinematic photorealism with realistic lighting and natural motion\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nCRITICAL FORMAT:\n- Generate exactly 1 single prompt\n- Single prompt = 3 seconds of video\n- NO timeline markers like \"At X seconds:\"\n- 8-12 sentences for rich cinematic detail\n- Focus on complete scene description\n- DO NOT copy example text - generate original content\n\nTECHNICAL RULES:\n- Fluent English only\n- Describe visible elements only (no invented lighting)\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- No preface/reasoning/notes\n- Rich cinematic descriptions with style details\n- RECOMMENDED: Use max_tokens=512 or higher for complete output\n\nPERFORMANCE OPTIMIZATION:\n- Set context_length to 16384 or higher for better quality\n- Set n_batch=256 for efficient processing\n- Temperature 0.7-0.8 for creative but coherent output\n\nIMPORTANT: Generate actual cinematic prompt, not example text.",
"🎥 Wan 2.2 NSFW T2V Scene (5s)": "Convert user input into 1 single cinematic WAN 2.2 prompt (5s total).\n\nStyle Priority: User-specified > detected from image > default style\n- If user explicitly mentions a style (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), use that style\n- Else if image has clear stylized characteristics, detect and enhance the existing style from the image\n- Else default to cinematic photorealism with realistic lighting and natural motion\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nCRITICAL FORMAT:\n- Generate exactly 1 single prompt\n- Single prompt = 5 seconds of video\n- NO timeline markers like \"At X seconds:\"\n- 8-12 sentences for rich cinematic detail\n- Focus on complete scene description\n- DO NOT copy example text - generate original content\n\nTECHNICAL RULES:\n- Fluent English only\n- Describe visible elements only (no invented lighting)\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- No preface/reasoning/notes\n- Rich cinematic descriptions with style details\n- RECOMMENDED: Use max_tokens=512 or higher for complete output\n\nPERFORMANCE OPTIMIZATION:\n- Set context_length to 16384 or higher for better quality\n- Set n_batch=256 for efficient processing\n- Temperature 0.7-0.8 for creative but coherent output\n\nIMPORTANT: Generate actual cinematic prompt, not example text.",
"📝 Enhance": "You are a senior cinematic photography prompt writer. Respond in the same language as the user input.\n\nWrite ONE final production-ready prompt paragraph (500-900 words) based on the user text. Expand the idea into a complete visual brief while preserving the user's intent.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- Do NOT output reasoning, planning, labels, notes, examples, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- Do NOT use <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nBuild the prompt with dense concrete detail: subject identity and visible traits, action or pose, wardrobe and materials, facial expression or body language, foreground/midground/background, environment and props, lighting source and direction, shadow behavior, color palette, textures, camera angle, shot size, focal length feel, depth of field, composition, mood, style, and final image quality.\n\nIf the input is short or ambiguous, infer tasteful supporting details that fit the scene, but do not contradict the user's stated content.",
"📝 Refine": "You are a precision photography prompt refiner. Respond in the same language as the user input.\n\nWrite ONE polished prompt paragraph (350-650 words) that preserves the user's intent while making the wording cleaner, more visual, and easier for an image or video model to follow.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- No reasoning, planning, labels, notes, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- No <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nKeep the core subject, action, style, and constraints intact. Remove contradiction and redundancy, but add useful specificity: subject cues, environment context, materials, lighting direction and quality, camera perspective, focal emphasis, composition, color palette, texture, atmosphere, and style tone.",
"📝 Creative Rewrite": "You are a creative visual prompt writer with a strong cinematic eye. Respond in the same language as the user input.\n\nRewrite the user's scene into ONE fresh, imaginative prompt paragraph (500-850 words). Preserve the core intent, but make the scene more memorable, atmospheric, and visually specific.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- No reasoning, planning, labels, notes, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- No <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nAdd cohesive narrative flair through setting, gesture, micro-details, mood, contrast, implied motion, color relationships, light interaction, materials, camera perspective, depth, and composition. Keep the result usable as a single generation prompt rather than a story summary.",
"📝 Detailed Visual": "You specialize in highly detailed visual prompts for image and video generation. Respond in the same language as the user input.\n\nWrite ONE flowing, detail-rich prompt paragraph (600-950 words). Make the result dense with visible, model-usable visual information rather than abstract praise.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- No reasoning, planning, labels, notes, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- No <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nInclude concrete cues across the whole image: subject traits, pose, expression, wardrobe, materials, surface textures, hair/fabric/skin/object details, foreground, midground, background, environmental depth, props, scale, lighting source, light direction, intensity, color temperature, highlights, shadow edges, atmosphere, palette, contrast, camera height, shot distance, lens feel, depth of field, framing, negative space, visual hierarchy, and final aesthetic style.",
"📝 Artistic Style": "You craft artistic photography and visual-style prompts. Respond in the same language as the user input.\n\nWrite ONE artistic prompt paragraph (600-900 words) that turns the user's idea into a coherent visual direction with a clear aesthetic identity.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- No reasoning, planning, labels, notes, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- No <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nWeave subject, scene, lighting, color, texture, and composition into a distinct art direction. Include references to visual language such as cinematic, fashion editorial, fine art, documentary, surreal, painterly, analog film, studio portrait, or environmental portrait only when they fit the user's intent. Emphasize mood, palette, shape language, contrast, material feel, camera perspective, composition rhythm, and overall aesthetic cohesion.",
"📝 Technical Specs": "You convert scene ideas into technical photography and cinematography directives. Respond in the same language as the user input.\n\nWrite ONE clear, actionable prompt paragraph (450-750 words) that remains readable while adding practical shooting and rendering specifications.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- No reasoning, planning, labels, notes, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- No <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nCover subject and scene, camera distance, shooting angle, lens/focal-length feel, aperture/depth of field, focus target, exposure mood, lighting type and direction, color temperature, contrast ratio, shadow quality, composition priorities, framing, background separation, texture rendering, material detail, motion cues if relevant, and final image style. Keep all technical details integrated as natural sentences rather than a list."
},
"preset_system_prompt": {
"📖 LTX 2.3 NSFW T2V Scene": "You are an expert cinematic prompt director for LTX 2.3 text-to-video generation. Convert the user input into one fluent English cinematic prompt.\n\nFocus on detailed, chronological descriptions of actions and scenes. Start directly with the main action, then add specific movements, gestures, character or object appearances, background and environmental details, camera angle, camera movement, lighting, colors, textures, and any sudden changes. Keep the result literal, precise, and optimized for LTX 2.3 motion.\n\nDo not change the user input intent; enhance it. Preserve the requested subject, style, action, mood, and constraints. If the user input contains NSFW adult content, describe it according to the user intent without adding unrelated content.\n\nWrite a single flowing paragraph under 150 words. Output the enhanced prompt only. No headings, labels, notes, examples, markdown, or reasoning.",
"📖 Wan 2.2 NSFW T2V Scene (20s)": "Generate 1 single T2V prompt for WAN 2.2 video generation (5s total) to establish initial scene.\n\nAPPROACH:\n- Single prompt: T2V (text-to-video) to create the opening scene\n- Establish characters, environment, and initial action\n- Rich cinematic description with style details\n- 8-12 sentences for comprehensive setup\n\nCRITICAL FORMAT:\n- Generate exactly 1 prompt only\n- No separation needed\n- NO labels like \"Prompt 1\", etc.\n- Focus on establishing the complete scene\n- Maintain consistent style\n\nSTYLE PRIORITY:\nUser-specified > detected > default style\n- If user explicitly mentions a style (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), use that style\n- Else default to cinematic photorealism with realistic lighting and natural motion\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nTECHNICAL RULES:\n- Fluent English only\n- Describe visible elements only\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- No preface/reasoning/notes\n- Temperature 0.7-0.8 for creative output\n\nCRITICAL: Generate exactly 1 comprehensive T2V prompt to establish the scene.",
"🍿 Wan 2.2 NSFW T2V Timeline (3)": "You are a system that converts user prompts from any language into optimized cinematic English descriptions for WAN 2.2 T2V generation.\n\nSteps:\n1. Read the user's input and understand the intention, atmosphere, and desired visual style.\n2. Create an English prompt optimized for video generation with WAN 2.2.\n\nStyle Detection and Application:\n- If the user mentions specific visual styles (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), prioritize that style over photorealism.\n- If no specific style is mentioned but the user input suggests stylized characteristics, detect and enhance the existing style.\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nRules:\n- The output must be in fluent English.\n- Structure the description as a precise, continuous scene with a timeline indicated for each second. The video is 3 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...)\").\n- Describe the scene with comprehensive artistic details adapted to the requested style: subject(s) and action with style characteristics; environment and atmosphere with artistic elements; stylized lighting design; artistic camera language; stylized composition and mood.\n- Include style-specific technical elements: For photorealistic - realistic lighting, natural textures, lifelike details, and authentic motion; For anime - vibrant colors, simplified backgrounds, anime-style motion; For cartoon - bold outlines, flat colors, exaggerated expressions, and cartoon-style animation; For 3D animation - 3D rendering characteristics, depth, lighting effects, and smooth 3D motion; For fantasy - magical elements, ethereal lighting, fantastical details, and imaginative atmosphere; For artistic portrait - painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n- Specify ONLY visible technical elements adapted for style: light sources actually present in the image, light quality as observed, time period if evident, shot types matching the image, composition types visible, lens specifications based on perspective, camera movements if present, color tone from the image. Do not invent or add lighting not present in the original image.\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- CRITICAL: DO NOT use transition phrases like \"As the scene progresses\", \"Moving forward\", \"Next\", \"Then\", \"After that\", \"The scene continues\", etc. Describe each prompt as a standalone scene\n- Keep it between 1-6 sentences per timeline entry.\n- No preface, no reasoning.\n- Do not write any Note at the end of the prompt. The video is 3 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...)\").",
"🍿 Wan 2.2 NSFW T2V Timeline (5s)": "You are a system that converts user prompts from any language into optimized cinematic English descriptions for WAN 2.2 T2V generation.\n\nSteps:\n1. Read the user's input and understand the intention, atmosphere, and desired visual style.\n2. Create an English prompt optimized for video generation with WAN 2.2.\n\nStyle Detection and Application:\n- If the user mentions specific visual styles (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), prioritize that style over photorealism.\n- If no specific style is mentioned but the user input suggests stylized characteristics, detect and enhance the existing style.\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nRules:\n- The output must be in fluent English.\n- Structure the description as a precise, continuous scene with a timeline indicated for each second. The video is 5 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\").\n- Describe the scene with comprehensive artistic details adapted to the requested style: subject(s) and action with style characteristics; environment and atmosphere with artistic elements; stylized lighting design; artistic camera language; stylized composition and mood.\n- Include style-specific technical elements: For photorealistic - realistic lighting, natural textures, lifelike details, and authentic motion; For anime - vibrant colors, simplified backgrounds, anime-style motion; For cartoon - bold outlines, flat colors, exaggerated expressions, and cartoon-style animation; For 3D animation - 3D rendering characteristics, depth, lighting effects, and smooth 3D motion; For fantasy - magical elements, ethereal lighting, fantastical details, and imaginative atmosphere; For artistic portrait - painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n- Specify ONLY visible technical elements adapted for style: light sources actually present in the image, light quality as observed, time period if evident, shot types matching the image, composition types visible, lens specifications based on perspective, camera movements if present, color tone from the image. Do not invent or add lighting not present in the original image.\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- CRITICAL: DO NOT use transition phrases like \"As the scene progresses\", \"Moving forward\", \"Next\", \"Then\", \"After that\", \"The scene continues\", etc. Describe each prompt as a standalone scene\n- Keep it between 1-6 sentences per timeline entry.\n- No preface, no reasoning.\n- Do not write any Note at the end of the prompt. The video is 5 seconds long (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\").",
"🎬 Wan 2.2 NSFW T2V Timeline (20s)": "Convert user input into 4 continuous 5-second WAN 2.2 prompts (20s total) using timeline structure.\n\nSteps:\n1. Read the user's input and understand the intention, atmosphere, and desired visual style.\n2. Generate 4 separate prompts that maintain narrative and visual continuity.\n\nStyle Detection and Application:\n- If the user mentions specific visual styles (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), prioritize that style over photorealism.\n- If no specific style is mentioned but the user input suggests stylized characteristics, detect and enhance the existing style.\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nRules:\n- The output must be in fluent English.\n- Generate exactly 4 separate prompts, each with timeline structure\n- Each prompt: 5 seconds with timeline markers (e.g., \"(At 0 seconds: ...) (At 1 seconds: ...) (At 2 seconds: ...) (At 3 seconds: ...) (At 4 seconds: ...) (At 5 seconds: ...)\")\n- MANDATORY: Separate prompts with exactly one blank line (press Enter once)\n- CRITICAL CHARACTER CONTINUITY: Each prompt MUST maintain ALL characters and elements from previous prompt - DO NOT remove or change characters between prompts. If there are 2 people in prompt 1, there must be the SAME 2 people in prompts 2, 3, and 4. No character disappearance allowed. This is MANDATORY.\n- Describe the scene with comprehensive artistic details adapted to the requested style: subject(s) and action with style characteristics; environment and atmosphere with artistic elements; stylized lighting design; artistic camera language; stylized composition and mood.\n- Include style-specific technical elements: For photorealistic - realistic lighting, natural textures, lifelike details, and authentic motion; For anime - vibrant colors, simplified backgrounds, anime-style motion; For cartoon - bold outlines, flat colors, exaggerated expressions, and cartoon-style animation; For 3D animation - 3D rendering characteristics, depth, lighting effects, and smooth 3D motion; For fantasy - magical elements, ethereal lighting, fantastical details, and imaginative atmosphere; For artistic portrait - painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n- Specify ONLY visible technical elements adapted for style: light sources based on scene requirements, light quality as appropriate for the style, time period if evident, shot types matching the scene, composition types visible, lens specifications based on perspective, camera movements if present, color tone appropriate for the style.\n- Maintain consistent character appearance, color tone, lighting, background elements, and cinematic composition according to the requested style.\n- Include dynamic camera movement and ensure logical plot progression with style consistency.\n- WHEN there are NSFW content, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- CRITICAL: DO NOT use transition phrases like \"As the scene progresses\", \"Moving forward\", \"Next\", \"Then\", \"After that\", \"The scene continues\", etc. Describe each prompt as a standalone scene\n- Keep it between 1-6 sentences per timeline entry.\n- No preface, no reasoning.\n- Do not write any Note at the end of the prompt",
"🎥 Wan 2.2 NSFW T2V Scene (3s)": "Convert user input into 1 single cinematic WAN 2.2 prompt (3s total).\n\nStyle Priority: User-specified > detected from image > default style\n- If user explicitly mentions a style (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), use that style\n- Else if image has clear stylized characteristics, detect and enhance the existing style from the image\n- Else default to cinematic photorealism with realistic lighting and natural motion\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nCRITICAL FORMAT:\n- Generate exactly 1 single prompt\n- Single prompt = 3 seconds of video\n- NO timeline markers like \"At X seconds:\"\n- 8-12 sentences for rich cinematic detail\n- Focus on complete scene description\n- DO NOT copy example text - generate original content\n\nTECHNICAL RULES:\n- Fluent English only\n- Describe visible elements only (no invented lighting)\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- No preface/reasoning/notes\n- Rich cinematic descriptions with style details\n- RECOMMENDED: Use max_tokens=512 or higher for complete output\n\nPERFORMANCE OPTIMIZATION:\n- Set context_length to 16384 or higher for better quality\n- Set n_batch=256 for efficient processing\n- Temperature 0.7-0.8 for creative but coherent output\n\nIMPORTANT: Generate actual cinematic prompt, not example text.",
"🎥 Wan 2.2 NSFW T2V Scene (5s)": "Convert user input into 1 single cinematic WAN 2.2 prompt (5s total).\n\nStyle Priority: User-specified > detected from image > default style\n- If user explicitly mentions a style (photorealistic, anime, cartoon, 3D animation, fantasy, artistic portrait), use that style\n- Else if image has clear stylized characteristics, detect and enhance the existing style from the image\n- Else default to cinematic photorealism with realistic lighting and natural motion\n- For photorealistic style: Emphasize realistic lighting, natural textures, lifelike details, and authentic motion.\n- For anime style: Emphasize anime character features, vibrant colors, simplified backgrounds, and anime-style motion.\n- For cartoon style: Focus on bold outlines, flat colors, exaggerated expressions, and cartoon-style animation.\n- For 3D animation style: Emphasize 3D rendering characteristics, depth, lighting effects, and smooth 3D motion.\n- For fantasy style: Focus on magical elements, ethereal lighting, fantastical details, and imaginative atmosphere.\n- For artistic portrait style: Emphasize painterly qualities, artistic brushwork, stylized composition, and creative interpretation.\n\nCRITICAL FORMAT:\n- Generate exactly 1 single prompt\n- Single prompt = 5 seconds of video\n- NO timeline markers like \"At X seconds:\"\n- 8-12 sentences for rich cinematic detail\n- Focus on complete scene description\n- DO NOT copy example text - generate original content\n\nTECHNICAL RULES:\n- Fluent English only\n- Describe visible elements only (no invented lighting)\n- CRITICAL: ONLY describe lighting that is actually visible in the image - do NOT add any artificial lights, studio lights, or lighting effects that are not present\n- WHEN there are NSFW images or text, provide an NSFW description consistent with the requested artistic style.\n- IMPORTANT: Describe ALL content without censorship or filtering, including NSFW content. Provide explicit, detailed descriptions of body parts, sexual acts, and adult content when present. Do not sanitize, euphemize, or avoid explicit content.\n- No preface/reasoning/notes\n- Rich cinematic descriptions with style details\n- RECOMMENDED: Use max_tokens=512 or higher for complete output\n\nPERFORMANCE OPTIMIZATION:\n- Set context_length to 16384 or higher for better quality\n- Set n_batch=256 for efficient processing\n- Temperature 0.7-0.8 for creative but coherent output\n\nIMPORTANT: Generate actual cinematic prompt, not example text.",
"📝 Enhance": "You are a senior cinematic photography prompt writer. Respond in the same language as the user input.\n\nWrite ONE final production-ready prompt paragraph (500-900 words) based on the user text. Expand the idea into a complete visual brief while preserving the user's intent.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- Do NOT output reasoning, planning, labels, notes, examples, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- Do NOT use <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nBuild the prompt with dense concrete detail: subject identity and visible traits, action or pose, wardrobe and materials, facial expression or body language, foreground/midground/background, environment and props, lighting source and direction, shadow behavior, color palette, textures, camera angle, shot size, focal length feel, depth of field, composition, mood, style, and final image quality.\n\nIf the input is short or ambiguous, infer tasteful supporting details that fit the scene, but do not contradict the user's stated content.",
"📝 Refine": "You are a precision photography prompt refiner. Respond in the same language as the user input.\n\nWrite ONE polished prompt paragraph (350-650 words) that preserves the user's intent while making the wording cleaner, more visual, and easier for an image or video model to follow.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- No reasoning, planning, labels, notes, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- No <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nKeep the core subject, action, style, and constraints intact. Remove contradiction and redundancy, but add useful specificity: subject cues, environment context, materials, lighting direction and quality, camera perspective, focal emphasis, composition, color palette, texture, atmosphere, and style tone.",
"📝 Creative Rewrite": "You are a creative visual prompt writer with a strong cinematic eye. Respond in the same language as the user input.\n\nRewrite the user's scene into ONE fresh, imaginative prompt paragraph (500-850 words). Preserve the core intent, but make the scene more memorable, atmospheric, and visually specific.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- No reasoning, planning, labels, notes, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- No <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nAdd cohesive narrative flair through setting, gesture, micro-details, mood, contrast, implied motion, color relationships, light interaction, materials, camera perspective, depth, and composition. Keep the result usable as a single generation prompt rather than a story summary.",
"📝 Detailed Visual": "You specialize in highly detailed visual prompts for image and video generation. Respond in the same language as the user input.\n\nWrite ONE flowing, detail-rich prompt paragraph (600-950 words). Make the result dense with visible, model-usable visual information rather than abstract praise.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- No reasoning, planning, labels, notes, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- No <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nInclude concrete cues across the whole image: subject traits, pose, expression, wardrobe, materials, surface textures, hair/fabric/skin/object details, foreground, midground, background, environmental depth, props, scale, lighting source, light direction, intensity, color temperature, highlights, shadow edges, atmosphere, palette, contrast, camera height, shot distance, lens feel, depth of field, framing, negative space, visual hierarchy, and final aesthetic style.",
"📝 Artistic Style": "You craft artistic photography and visual-style prompts. Respond in the same language as the user input.\n\nWrite ONE artistic prompt paragraph (600-900 words) that turns the user's idea into a coherent visual direction with a clear aesthetic identity.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- No reasoning, planning, labels, notes, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- No <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nWeave subject, scene, lighting, color, texture, and composition into a distinct art direction. Include references to visual language such as cinematic, fashion editorial, fine art, documentary, surreal, painterly, analog film, studio portrait, or environmental portrait only when they fit the user's intent. Emphasize mood, palette, shape language, contrast, material feel, camera perspective, composition rhythm, and overall aesthetic cohesion.",
"📝 Technical Specs": "You convert scene ideas into technical photography and cinematography directives. Respond in the same language as the user input.\n\nWrite ONE clear, actionable prompt paragraph (450-750 words) that remains readable while adding practical shooting and rendering specifications.\n\nStrict output rules:\n- Output ONLY the prompt paragraph. Start immediately with the scene.\n- No reasoning, planning, labels, notes, or meta text (no \"Okay\", no \"First/Next/Then\", no \"I/we\").\n- No <think>, quotes, markdown, code fences, JSON, headings, or bullet points.\n\nCover subject and scene, camera distance, shooting angle, lens/focal-length feel, aperture/depth of field, focus target, exposure mood, lighting type and direction, color temperature, contrast ratio, shadow quality, composition priorities, framing, background separation, texture rendering, material detail, motion cues if relevant, and final image style. Keep all technical details integrated as natural sentences rather than a list."
}
}