Compare commits

..
55 Commits
Author SHA1 Message Date
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
Your Name ff1187ae6c v1.0.9: Live terminal streaming, RAW_TRACE output, per-node prompt persistence, stream display helper 2026-05-17 21:27:35 +02:00
Your Name a95561e1ae v1.0.8: Expand Linux wheel compatibility matrix (CUDA 12.8 cp310-cp314) and lock local fork 2026-05-17 21:11:40 +02:00
Your Name 2184578cfc v1.0.7: Windows GGUF PATH overflow fix, per-node prompt persistence, return_raw_trace optional, WinError 206 runtime fix 2026-05-17 20:00:44 +02:00
Your Name b2295ab23a Fix Windows GGUF llama_cpp import runtime 2026-05-16 13:59:34 +02:00
Your Name 972998ef98 Fix stream toggle bug and retain versioned releases 2026-05-16 13:47:00 +02:00
Your Name c091b30faf Fix GGUF workflow compatibility and release v1.0.4 2026-05-16 12:46:38 +02:00
Your Name 4815f3bc24 Update published description for 1.0.3 2026-05-16 12:27:24 +02:00
Your Name 406079392f Expose GGUF performance controls for 1.0.2 2026-05-16 12:13:44 +02:00
Your Name 9e18c83b5e Fix ThinkingLLM node reload imports 2026-05-16 11:08:07 +02:00
Your Name 2250058c4b Add legacy AILab node aliases 2026-05-16 10:55:35 +02:00
Marcel 9532eebb74 Add rolling \r terminal display for compact streaming mode 2026-05-16 10:18:19 +02:00
Marcel f1ebccdd18 Restore __init__.py to clean committed version 2026-05-16 10:16:34 +02:00
Your Name 1cae110284 Strip BOM, add test suite with collision/toggle/streaming coverage (14/14) 2026-05-16 09:42:21 +02:00
Your Name e1204a321b Rename node class keys to ThinkingLLM_* to prevent workflow collision with Qwen3 AILab_* keys 2026-05-16 09:35:20 +02:00
Your Name aec778b8b7 Add Aggressive uncensored Gemma 4 HF entries 2026-05-16 03:29:28 +02:00
Your Name 4046784770 Add generation params to input_signature for cache invalidation 2026-05-16 03:00:20 +02:00
Your Name 9629fddc5f Add thread-based interrupt for all non-streaming generation paths 2026-05-16 02:53:03 +02:00
Your Name b0e2e91f15 Fix: make tqdm import conditional, guard download tqdm_class 2026-05-16 02:36:54 +02:00
Your Name 56e888b4fb Clean unused hf_hub_download imports 2026-05-16 02:23:27 +02:00
Your Name 74a35412a4 Add visible model download progress 2026-05-16 02:19:26 +02:00
Your Name 9441cb86b9 Retry clean HF snapshot download 2026-05-16 02:08:55 +02:00
Your Name 9dff35241b Unify streaming terminal styling 2026-05-16 02:04:51 +02:00
Your Name da1cf750f1 Default keep model loaded to off 2026-05-16 01:54:32 +02:00
Your Name fc8d3f9b21 Refresh incomplete HF VL snapshots 2026-05-16 01:47:11 +02:00
Your Name d6d88d6ec6 Fix loader typing and set version 1.0.1 2026-05-16 01:43:16 +02:00
Your Name f38739bc7d Fix Gemma HF catalog repo ids 2026-05-16 01:23:45 +02:00
Your Name b841a936cb Use AutoProcessor for Gemma tokenizer loading 2026-05-16 01:20:56 +02:00
Your Name b73db55a95 Fix HF tokenizer backend fallback 2026-05-16 01:16:07 +02:00
Your Name b3d55e94c2 Auto-install llama_cpp on Linux for GGUF 2026-05-16 01:11:14 +02:00
Your Name e9e6c3abd2 Fix GGUF node registration and Gemma HF ids 2026-05-16 01:08:40 +02:00
Your Name b58ecc6590 Update custom_nodes.json description 2026-05-15 14:10:57 +02:00
Your Name b2a9718cac Add node screenshot and workflow tips to README 2026-05-15 14:06:07 +02:00
Your Name 10ff0bd91f Add ComfyUI Manager install instructions to README 2026-05-15 13:57:28 +02:00
Your Name 4d845474c7 Expand supported models list with Gemma 4 family 2026-05-15 13:53:58 +02:00
Your Name b4d9aa9a6d Add demo GIF and YouTube link to README 2026-05-15 13:50:46 +02:00
Marcel 4150252005 Add YouTube demo link to README 2026-05-15 13:43:21 +02:00
29 changed files with 5810 additions and 1056 deletions
+27
View File
@@ -0,0 +1,27 @@
name: Create GitHub release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'goodguy1963' }}
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create or update release for tag
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
generate_release_notes: true
make_latest: true
append_body: false
+2
View File
@@ -58,6 +58,8 @@ temp/
*.jpg
*.jpeg
*.gif
!docs/*.gif
!docs/*.png
*.webp
*.mp4
*.avi
+463
View File
@@ -0,0 +1,463 @@
import contextlib
import importlib
import importlib.metadata
import os
import platform
import re
import subprocess
import sys
DEFAULT_JAMEPENG_GIT_SPEC = "llama-cpp-python @ git+https://github.com/JamePeng/llama-cpp-python.git"
KNOWN_LINUX_WHEEL_SPECS = {
("cp312", "linux_x86_64", "12.4"): (
"https://github.com/JamePeng/llama-cpp-python/releases/download/"
"v0.3.34-cu124-Basic-linux-20260331/"
"llama_cpp_python-0.3.34+cu124.basic-cp312-cp312-linux_x86_64.whl"
),
("cp310", "linux_x86_64", "12.8"): (
"https://github.com/JamePeng/llama-cpp-python/releases/download/"
"v0.3.35-cu128-Basic-linux-20260406/"
"llama_cpp_python-0.3.35+cu128.basic-cp310-cp310-linux_x86_64.whl"
),
("cp311", "linux_x86_64", "12.8"): (
"https://github.com/JamePeng/llama-cpp-python/releases/download/"
"v0.3.35-cu128-Basic-linux-20260406/"
"llama_cpp_python-0.3.35+cu128.basic-cp311-cp311-linux_x86_64.whl"
),
("cp312", "linux_x86_64", "12.8"): (
"https://github.com/JamePeng/llama-cpp-python/releases/download/"
"v0.3.35-cu128-Basic-linux-20260406/"
"llama_cpp_python-0.3.35+cu128.basic-cp312-cp312-linux_x86_64.whl"
),
("cp313", "linux_x86_64", "12.8"): (
"https://github.com/JamePeng/llama-cpp-python/releases/download/"
"v0.3.35-cu128-Basic-linux-20260406/"
"llama_cpp_python-0.3.35+cu128.basic-cp313-cp313-linux_x86_64.whl"
),
("cp314", "linux_x86_64", "12.8"): (
"https://github.com/JamePeng/llama-cpp-python/releases/download/"
"v0.3.35-cu128-Basic-linux-20260406/"
"llama_cpp_python-0.3.35+cu128.basic-cp314-cp314-linux_x86_64.whl"
),
("cp312", "linux_x86_64", "13.0"): (
"https://github.com/JamePeng/llama-cpp-python/releases/download/"
"v0.3.34-cu130-Basic-linux-20260331/"
"llama_cpp_python-0.3.34+cu130.basic-cp312-cp312-linux_x86_64.whl"
),
}
VISION_HANDLER_NAMES = ("Qwen3VLChatHandler", "Qwen25VLChatHandler", "Gemma4ChatHandler", "MTMDChatHandler")
_LAST_BACKEND_INFO: dict | None = None
_PRINTED_BACKEND_LINES: set[str] = set()
class _PathOnlyDllDirectoryHandle:
def close(self) -> None:
return None
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def _normalize_bool_env(name: str, default: bool = True) -> bool:
value = os.getenv(name)
if value is None:
return default
return value.strip().lower() not in {"0", "false", "no", "off"}
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
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:
return wheel_override, "environment wheel override"
spec_override = os.getenv("THINKINGLLM_LLAMA_CPP_LINUX_SPEC", "").strip()
if spec_override:
return spec_override, "environment spec override"
py_tag = f"cp{sys.version_info.major}{sys.version_info.minor}"
machine = platform.machine().lower()
if machine in {"x86_64", "amd64"}:
platform_tag = "linux_x86_64"
elif machine in {"aarch64", "arm64"}:
platform_tag = "linux_aarch64"
else:
platform_tag = f"linux_{machine or 'unknown'}"
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)
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:
for module_name in list(sys.modules):
if module_name == "llama_cpp" or module_name.startswith("llama_cpp."):
sys.modules.pop(module_name, None)
importlib.invalidate_caches()
def _prepend_to_path_once(path: str) -> None:
current = os.environ.get("PATH", "")
normalized_path = os.path.normcase(os.path.normpath(path))
current_entries = [entry for entry in current.split(os.pathsep) if entry]
for entry in current_entries:
if os.path.normcase(os.path.normpath(entry)) == normalized_path:
return
os.environ["PATH"] = path if not current else path + os.pathsep + current
def _is_windows_path_length_error(exc: OSError) -> bool:
return platform.system().lower() == "windows" and getattr(exc, "winerror", None) == 206
def _get_windows_runtime_dll_dirs() -> list[str]:
runtime_dirs: list[str] = []
try:
import torch
torch_lib_dir = os.path.join(os.path.dirname(torch.__file__), "lib")
if os.path.isdir(torch_lib_dir):
runtime_dirs.append(torch_lib_dir)
except Exception:
pass
return runtime_dirs
@contextlib.contextmanager
def relax_windows_dll_directory_for_long_paths():
if platform.system().lower() != "windows" or not hasattr(os, "add_dll_directory"):
yield
return
original_add_dll_directory = os.add_dll_directory
path_was_present = "PATH" in os.environ
original_path = os.environ.get("PATH")
def _safe_add_dll_directory(path):
path_str = os.fspath(path)
try:
return original_add_dll_directory(path_str)
except OSError as exc:
if not _is_windows_path_length_error(exc) or not os.path.isdir(path_str):
raise
_prepend_to_path_once(path_str)
return _PathOnlyDllDirectoryHandle()
os.add_dll_directory = _safe_add_dll_directory
try:
for runtime_dir in _get_windows_runtime_dll_dirs():
_safe_add_dll_directory(runtime_dir)
_prepend_to_path_once(runtime_dir)
yield
finally:
if path_was_present:
os.environ["PATH"] = original_path or ""
else:
os.environ.pop("PATH", None)
os.add_dll_directory = original_add_dll_directory
_relax_windows_dll_directory_for_long_paths = relax_windows_dll_directory_for_long_paths
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")
llama_class = getattr(llama_cpp, "Llama", None)
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")
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)
except Exception as first_exc:
if platform.system().lower() != "linux" or not _normalize_bool_env("THINKINGLLM_AUTO_INSTALL_LLAMA_CPP", True):
raise RuntimeError(
"[QwenVL] llama_cpp is unavailable or incompatible. Install the GGUF vision dependency first. "
"See docs/LLAMA_CPP_PYTHON_VISION_INSTALL.md"
) 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",
"pip",
"install",
"--upgrade",
"--force-reinstall",
"--no-cache-dir",
install_spec,
]
print(
f"[QwenVL] llama_cpp check failed on Linux; attempting automatic install "
f"using {install_reason}: {install_spec}"
)
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()
raise RuntimeError(
"[QwenVL] Automatic llama_cpp installation failed on Linux. "
f"Command: {' '.join(install_cmd)}\nstdout:\n{stdout or '<empty>'}\nstderr:\n{stderr or '<empty>'}"
) from first_exc
try:
return _import_llama_cpp_backend(require_vision_handlers=require_vision_handlers)
except Exception as second_exc:
raise RuntimeError(
"[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
+562 -81
View File
@@ -14,8 +14,12 @@
# Source: https://github.com/1038lab/ComfyUI-QwenVL
import gc
import inspect
import json
import os
import platform
import shutil
import time
from enum import Enum
from pathlib import Path
import hashlib
@@ -24,11 +28,34 @@ import numpy as np
import psutil
import torch
from PIL import Image
from huggingface_hub import snapshot_download
from transformers import AutoProcessor, AutoTokenizer, BitsAndBytesConfig
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:
tqdm = None
try:
from transformers import AutoProcessor, AutoTokenizer, BitsAndBytesConfig
except ImportError:
AutoProcessor = None
AutoTokenizer = None
BitsAndBytesConfig = None
from AILab_StreamDisplay import TerminalStreamDisplay
from comfy.model_management import throw_exception_if_processing_interrupted
try:
from comfy.utils import ProgressBar
except Exception:
ProgressBar = None
try:
from server import PromptServer
except Exception:
PromptServer = None
# SageAttention support
try:
from sageattention.core import (
@@ -37,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
@@ -55,6 +85,153 @@ QWEN_MIN_REASONING_BUDGET_TOKENS = 1024
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.25
_HF_TOKEN_ENV_VARS = ("HF_TOKEN", "HUGGING_FACE_HUB_TOKEN")
def _format_download_size(num_bytes) -> str:
if num_bytes is None:
return "? B"
value = float(num_bytes)
units = ["B", "KB", "MB", "GB", "TB"]
for unit in units:
if value < 1024.0 or unit == units[-1]:
if unit == "B":
return f"{int(value)} {unit}"
return f"{value:.1f} {unit}"
value /= 1024.0
return f"{value:.1f} TB"
class _DownloadProgressReporter:
def __init__(self, node_id=None, label: str = "QwenVL Download", repo_id: str | None = None):
self.node_id = node_id
self.label = label
self.repo_id = repo_id
self.progress_bar = ProgressBar(1, node_id=node_id) if ProgressBar is not None else None
self._last_text_at = 0.0
self._last_terminal_at = 0.0
self._last_percent = None
def _emit_ui_text(self, text: str, *, force: bool = False) -> None:
if PromptServer is None or self.node_id is None:
return
now = time.monotonic()
if not force and (now - self._last_text_at) < _DOWNLOAD_STATUS_INTERVAL_SECONDS:
return
PromptServer.instance.send_progress_text(text, self.node_id)
self._last_text_at = now
def _emit_terminal_text(self, text: str, *, force: bool = False) -> None:
now = time.monotonic()
if not force and (now - self._last_terminal_at) < _DOWNLOAD_STATUS_INTERVAL_SECONDS:
return
print(text)
self._last_terminal_at = now
def _build_message(self, stage: str, detail: str | None = None, current: int | float | None = None, total: int | float | None = None) -> str:
lines = [self.label, f"Stage: {stage}"]
if self.repo_id:
lines.append(f"Source: {self.repo_id}")
if detail:
lines.append(f"File: {detail}")
if total and total > 0:
percent = int(min(100, max(0, round((float(current or 0) / float(total)) * 100))))
lines.append(f"Progress: {percent}% ({_format_download_size(current)} / {_format_download_size(total)})")
elif current is not None:
lines.append(f"Downloaded: {_format_download_size(current)}")
return "\n".join(lines)
def stage(self, stage: str, *, detail: str | None = None, force: bool = True) -> None:
message = self._build_message(stage, detail=detail)
self._emit_ui_text(message, force=force)
self._emit_terminal_text(f"[{self.label}] {stage}{f' - {detail}' if detail else ''}", force=force)
def progress(self, stage: str, *, detail: str | None = None, current: int | float | None = None, total: int | float | None = None, force: bool = False) -> None:
if self.progress_bar is not None and total is not None and total > 0:
self.progress_bar.update_absolute(min(float(current or 0), float(total)), total=float(total))
percent = None
if total and total > 0:
percent = int(min(100, max(0, round((float(current or 0) / float(total)) * 100))))
message = self._build_message(stage, detail=detail, current=current, total=total)
self._emit_ui_text(message, force=force or percent != self._last_percent)
suffix = ""
if percent is not None:
suffix = f" {percent}% ({_format_download_size(current)} / {_format_download_size(total)})"
elif current is not None:
suffix = f" {_format_download_size(current)}"
self._emit_terminal_text(f"[{self.label}] {stage}{f' - {detail}' if detail else ''}{suffix}", force=force or percent != self._last_percent)
self._last_percent = percent
def finish(self, *, detail: str | None = None, force: bool = True) -> None:
if self.progress_bar is not None:
self.progress_bar.update_absolute(1, total=1)
message = self._build_message("Completed", detail=detail, current=1, total=1)
self._emit_ui_text(message, force=force)
self._emit_terminal_text(f"[{self.label}] Completed{f' - {detail}' if detail else ''}", force=force)
def fail(self, detail: str) -> None:
message = self._build_message("Failed", detail=detail)
self._emit_ui_text(message, force=True)
self._emit_terminal_text(f"[{self.label}] Failed - {detail}", force=True)
def make_tqdm_class(self):
if tqdm is None:
return None
reporter = self
class _NodeDownloadTqdm(tqdm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
reporter.progress(
"Downloading",
detail=getattr(self, "desc", None),
current=getattr(self, "n", 0),
total=getattr(self, "total", None),
force=True,
)
def update(self, n=1):
result = super().update(n)
reporter.progress(
"Downloading",
detail=getattr(self, "desc", None),
current=getattr(self, "n", 0),
total=getattr(self, "total", None),
)
return result
def refresh(self, *args, **kwargs):
result = super().refresh(*args, **kwargs)
reporter.progress(
"Downloading",
detail=getattr(self, "desc", None),
current=getattr(self, "n", 0),
total=getattr(self, "total", None),
)
return result
def close(self):
reporter.progress(
"Downloading",
detail=getattr(self, "desc", None),
current=(getattr(self, "total", None) or getattr(self, "n", 0)),
total=(getattr(self, "total", None) or getattr(self, "n", 0) or None),
force=True,
)
return super().close()
return _NodeDownloadTqdm
def _maybe_emit_hf_stream_heartbeat(stage_label: str, started_at: float, last_status_at: float, full_text: str) -> float:
now = time.monotonic()
if (now - last_status_at) < _STREAM_HEARTBEAT_INTERVAL_SECONDS:
return last_status_at
elapsed = max(0.0, now - started_at)
print(f"[QwenVL HF] {stage_label}: still generating... ({len(full_text)} chars, {elapsed:.1f}s)")
return now
def _build_workflow_fingerprint(extra_pnginfo):
"""Return a short stable fingerprint for a workflow dict so node state
@@ -237,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):
@@ -259,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
@@ -277,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
@@ -294,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)
@@ -491,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)"
@@ -648,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:
@@ -900,7 +1160,150 @@ def resolve_attention_mode(mode, force_sdpa=False):
print("[QwenVL] Auto mode: Using SDPA")
return "sdpa"
def ensure_model(model_name):
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
if not _model_snapshot_has_weights(target):
return False
if not (target / "config.json").exists():
return False
if require_processor:
processor_files = (
"preprocessor_config.json",
"processor_config.json",
"image_processor_config.json",
"video_preprocessor_config.json",
)
if not any((target / name).exists() for name in processor_files):
return False
return True
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)
shutil.rmtree(target, ignore_errors=False)
reporter.stage("Preparing download", detail=target.name)
try:
download_kwargs = {
"repo_id": repo_id,
"local_dir": str(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(_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", hf_token: str | None = None) -> None:
if target_path.exists():
print(f"[QwenVL] Using cached file: {target_path}")
return
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)
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, hf_token: str | None = None):
info = HF_ALL_MODELS.get(model_name)
if not info:
raise ValueError(f"Model '{model_name}' not in config")
@@ -928,16 +1331,54 @@ def ensure_model(model_name):
models_dir.mkdir(parents=True, exist_ok=True)
target = models_dir / repo_id.split("/")[-1]
# If already downloaded (has weights), use local without calling snapshot_download
if target.exists() and target.is_dir():
if any(target.glob("*.safetensors")) or any(target.glob("*.bin")):
return str(target)
# Only trust an existing snapshot if it also contains the metadata needed
# by the selected backend. This avoids reusing stale or partial downloads.
if _model_snapshot_has_required_files(target, require_processor=require_processor):
return str(target)
snapshot_download(
repo_id=repo_id,
local_dir=str(target),
ignore_patterns=["*.md", ".git*"],
if target.exists() and target.is_dir():
print(f"[QwenVL] Existing model snapshot is incomplete, refreshing: {target}")
_download_model_snapshot(
repo_id,
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):
print(f"[QwenVL] Refreshed snapshot is still incomplete, retrying clean download: {target}")
_download_model_snapshot(
repo_id,
target,
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):
missing = []
if not (target / "config.json").exists():
missing.append("config.json")
if require_processor and not any(
(target / name).exists()
for name in (
"preprocessor_config.json",
"processor_config.json",
"image_processor_config.json",
"video_preprocessor_config.json",
)
):
missing.append("processor metadata")
if not _model_snapshot_has_weights(target):
missing.append("model weights")
missing_str = ", ".join(missing) if missing else "required files"
raise FileNotFoundError(
f"[QwenVL] Downloaded model snapshot is incomplete for {repo_id}: missing {missing_str} in {target}"
)
return str(target)
def enforce_memory(model_name, quantization, device_info):
@@ -993,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):
@@ -1025,6 +1466,8 @@ class QwenVLBase:
use_compile,
device_choice,
keep_model_loaded,
unique_id=None,
hf_token: str | None = None,
):
quant = Quantization.from_value(quant_value) # Skip enforce_memory for now
@@ -1061,7 +1504,13 @@ class QwenVLBase:
ensure_cuda_vram_headroom("QwenVL", min_free_gb=1.0, min_free_ratio=0.08)
return
self.clear()
model_path = ensure_model(model_name)
model_path = ensure_model(
model_name,
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)
# Handle attention mode for loading
@@ -1090,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:
@@ -1233,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)
@@ -1267,59 +1715,71 @@ class QwenVLBase:
else:
kwargs["do_sample"] = False
# Optional: terminal streaming with rolling 200-char window
# Optional: staged readable terminal streaming
if stream_to_terminal:
try:
from AILab_StreamDisplay import TerminalStreamDisplay
from transformers import TextIteratorStreamer
from threading import Thread
sd = TerminalStreamDisplay("QwenVL HF", compact=True)
stage_label = "STREAMING"
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})
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_compact(token_str)
stream_display.push_compact(token_str)
thread.join()
sd.end_compact()
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
# Generate with memory monitoring
try:
outputs = self.model.generate(**model_inputs, **kwargs)
throw_exception_if_processing_interrupted()
except torch.cuda.OutOfMemoryError as e:
# Clear memory and retry with reduced parameters
print(f"[QwenVL] OOM detected, clearing memory and retrying...")
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
# Retry with smaller max_tokens
reduced_tokens = max(256, max_tokens // 2)
kwargs["max_new_tokens"] = reduced_tokens
print(f"[QwenVL] Retrying with reduced tokens: {reduced_tokens}")
# Generate with interrupt support via background thread
import threading, queue as qmod
abort_event = threading.Event()
output_queue: qmod.Queue = qmod.Queue()
worker_error: Exception | None = None
def _generate_worker():
nonlocal worker_error
try:
outputs = self.model.generate(**model_inputs, **kwargs)
throw_exception_if_processing_interrupted()
except torch.cuda.OutOfMemoryError:
print(f"[QwenVL] OOM still occurs with {reduced_tokens} tokens, falling back to CPU")
# Fallback to CPU if GPU still OOM
device_backup = model_inputs["input_ids"].device
for key in model_inputs:
if torch.is_tensor(model_inputs[key]):
model_inputs[key] = model_inputs[key].cpu()
outputs = self.model.generate(**model_inputs, **kwargs)
throw_exception_if_processing_interrupted()
result = self.model.generate(**model_inputs, **kwargs)
output_queue.put(("ok", result))
except Exception as exc:
worker_error = exc
output_queue.put(("error", None))
worker = threading.Thread(target=_generate_worker, daemon=True)
worker.start()
try:
while True:
try:
kind, result = output_queue.get(timeout=0.25)
except qmod.Empty:
throw_exception_if_processing_interrupted()
continue
if kind == "error":
break
outputs = result
break
finally:
abort_event.set()
worker.join(timeout=5.0)
if worker_error:
raise worker_error
throw_exception_if_processing_interrupted()
if torch.cuda.is_available():
torch.cuda.synchronize()
@@ -1328,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)
@@ -1344,10 +1804,19 @@ class QwenVLBase:
use_torch_compile=bool(use_torch_compile),
device=device,
enable_thinking=bool(enable_thinking),
max_tokens=max_tokens,
temperature=temperature,
top_p=top_p,
repetition_penalty=repetition_penalty,
)
# 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:
@@ -1358,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:
@@ -1385,7 +1855,10 @@ class QwenVLBase:
use_torch_compile,
device,
keep_model_loaded,
unique_id=unique_id,
hf_token=hf_token,
)
hf_token = None
try:
text, raw_text = self.generate(
prompt,
@@ -1425,11 +1898,11 @@ class QwenVLBase:
if not keep_model_loaded:
self.clear()
class AILab_QwenVL(QwenVLBase):
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]
@@ -1440,11 +1913,12 @@ class AILab_QwenVL(QwenVLBase):
"preset_prompt": (prompts, {"default": default_prompt, "tooltip": TOOLTIPS["preset_prompt"] + "\n\nSelect 'No preset' to use only the custom prompt or image input."}),
"custom_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": TOOLTIPS["custom_prompt"]}),
"max_tokens": ("INT", {"default": 8192, "min": 64, "max": 8192, "tooltip": TOOLTIPS["max_tokens"]}),
"keep_model_loaded": ("BOOLEAN", {"default": True, "tooltip": TOOLTIPS["keep_model_loaded"]}),
"keep_model_loaded": ("BOOLEAN", {"default": False, "tooltip": TOOLTIPS["keep_model_loaded"]}),
"seed": ("INT", {"default": 1, "min": 1, "max": 2**32 - 1, "tooltip": TOOLTIPS["seed"] + "\n\n💡 Cache Info: Prompts are cached automatically. Use the same inputs (model, preset, custom prompt, image/video) to reuse cached prompts and avoid regeneration.\n\n🔒 Fixed Seed Mode: Set seed = 1 to ignore image/video changes and only use text-based caching. Perfect for keeping the same prompt regardless of media input variations."}),
"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",),
@@ -1461,21 +1935,22 @@ class AILab_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="AILab_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("AILab_QwenVL", unique_id, extra_pnginfo)
key = _make_node_state_key("ThinkingLLM_QwenVL", unique_id, extra_pnginfo)
entry = NODE_PROMPT_STATE.get(key, {})
raw_trace = entry.get("raw_trace", "") if isinstance(entry, dict) else ""
return (result[0], raw_trace)
class AILab_QwenVL_Advanced(QwenVLBase):
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]
@@ -1498,11 +1973,12 @@ class AILab_QwenVL_Advanced(QwenVLBase):
"num_beams": ("INT", {"default": 1, "min": 1, "max": 8, "tooltip": TOOLTIPS["num_beams"]}),
"repetition_penalty": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.0, "tooltip": TOOLTIPS["repetition_penalty"]}),
"frame_count": ("INT", {"default": 16, "min": 1, "max": 64, "tooltip": TOOLTIPS["frame_count"]}),
"keep_model_loaded": ("BOOLEAN", {"default": True, "tooltip": TOOLTIPS["keep_model_loaded"]}),
"keep_model_loaded": ("BOOLEAN", {"default": False, "tooltip": TOOLTIPS["keep_model_loaded"]}),
"seed": ("INT", {"default": 1, "min": 1, "max": 2**32 - 1, "tooltip": TOOLTIPS["seed"] + "\n\n💡 Cache Info: Prompts are cached automatically. Use same inputs (model, preset, custom prompt, image/video) to reuse cached prompts and avoid regeneration.\n\n🔒 Fixed Seed Mode: Set seed = 1 to ignore image/video changes and only use text-based caching. Perfect for keeping the same prompt regardless of media input variations."}),
"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",),
@@ -1519,22 +1995,27 @@ class AILab_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="AILab_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("AILab_QwenVL_Advanced", unique_id, extra_pnginfo)
key = _make_node_state_key("ThinkingLLM_QwenVL_Advanced", unique_id, extra_pnginfo)
entry = NODE_PROMPT_STATE.get(key, {})
raw_trace = entry.get("raw_trace", "") if isinstance(entry, dict) else ""
return (result[0], raw_trace)
NODE_CLASS_MAPPINGS = {
"AILab_QwenVL": AILab_QwenVL,
"AILab_QwenVL_Advanced": AILab_QwenVL_Advanced,
"ThinkingLLM_QwenVL": ThinkingLLM_QwenVL,
"ThinkingLLM_QwenVL_Advanced": ThinkingLLM_QwenVL_Advanced,
"AILab_QwenVL": ThinkingLLM_QwenVL,
"AILab_QwenVL_Advanced": ThinkingLLM_QwenVL_Advanced,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ThinkingLLM_QwenVL": "ThinkingLLM",
"ThinkingLLM_QwenVL_Advanced": "ThinkingLLM Advanced",
"AILab_QwenVL": "ThinkingLLM",
"AILab_QwenVL_Advanced": "ThinkingLLM Advanced",
}
+996 -193
View File
File diff suppressed because it is too large Load Diff
+204 -186
View File
@@ -19,14 +19,13 @@ import json
import os
import queue
import re
import shutil
import threading
import time
from dataclasses import dataclass
from pathlib import Path
import torch
from huggingface_hub import hf_hub_download, snapshot_download
from llama_cpp import Llama # type: ignore[import-untyped]
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
@@ -36,11 +35,18 @@ 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,
build_node_input_signature,
download_hf_file_to_path,
ensure_cuda_vram_headroom,
estimate_qwen_text_tokens,
get_cache_key,
@@ -54,7 +60,27 @@ 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
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.",
"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 +121,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:
@@ -133,63 +181,16 @@ def _maybe_emit_prompt_waiting_heartbeat(stage_label: str, started_at: float, la
return now
def _compact_progress_line_width(limit: int = 60) -> int:
terminal_half_width = max(24, shutil.get_terminal_size(fallback=(120, 20)).columns // 2)
return max(24, min(int(limit), terminal_half_width))
def _normalize_compact_progress_text(text: str) -> str:
return re.sub(r"[ \t]+", " ", (text or "").replace("\r", "").replace("\n", " ")).strip()
def _split_compact_progress_lines(text: str, line_width: int) -> tuple[list[str], str]:
lines: list[str] = []
remaining = text.lstrip()
while len(remaining) > line_width:
split_at = remaining.rfind(" ", 0, line_width + 1)
if split_at <= 0:
split_at = line_width
line = remaining[:split_at].strip()
if line:
lines.append(line)
remaining = remaining[split_at:].lstrip()
return lines, remaining
def _maybe_emit_prompt_compact_progress(stage_label: str, started_at: float, last_status_at: float, full_text: str, progress_state: dict, *, force: bool = False, final: bool = False) -> float:
def _maybe_emit_prompt_background_heartbeat(stage_label: str, started_at: float, last_status_at: float, full_text: str = "") -> float:
now = time.monotonic()
if not final and not force and (now - last_status_at) < _STREAM_HEARTBEAT_INTERVAL_SECONDS:
if (now - last_status_at) < _STREAM_HEARTBEAT_INTERVAL_SECONDS:
return last_status_at
rendered_text = _normalize_compact_progress_text(clean_model_output(full_text, OutputCleanConfig(mode="prompt")) or full_text)
if not rendered_text:
elapsed = max(0.0, now - started_at)
if not full_text:
print("[QwenVL GGUF] generating...")
return now
line_width = int(progress_state.get("line_width") or _compact_progress_line_width())
progress_state["line_width"] = line_width
previous_snapshot = progress_state.get("rendered_snapshot", "")
pending_text = progress_state.get("pending_text", "")
if previous_snapshot and rendered_text.startswith(previous_snapshot):
delta = rendered_text[len(previous_snapshot):]
pending_text = f"{pending_text}{delta}"
else:
pending_text = rendered_text
progress_state["rendered_snapshot"] = rendered_text
lines, pending_text = _split_compact_progress_lines(pending_text, line_width)
emitted = False
for line in lines:
print(line)
emitted = True
if final and pending_text:
print(pending_text)
pending_text = ""
emitted = True
progress_state["pending_text"] = pending_text
if emitted or final:
return now
return last_status_at
print(f"[QwenVL GGUF] {stage_label}: generation in progress... ({len(full_text)} chars buffered, {elapsed:.1f}s)")
return now
def _install_llama_abort_callback(llm, should_abort):
@@ -324,12 +325,15 @@ def _find_existing_local_file(base_dir: Path, filename: str) -> Path | None:
return None
class AILab_QwenVL_GGUF_PromptEnhancer:
class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("ENHANCED_OUTPUT", "RAW_TRACE")
FUNCTION = "process"
CATEGORY = "ThinkingLLM"
def _load_backend(self):
return ensure_llama_cpp_backend(require_vision_handlers=False)
def __init__(self):
self.llm = None
self.current_signature = None
@@ -446,7 +450,7 @@ class AILab_QwenVL_GGUF_PromptEnhancer:
except Exception:
pass
for scan_dir in scan_dirs:
local_models = AILab_QwenVL_GGUF_PromptEnhancer._scan_local_gguf_text_models(scan_dir, existing_filenames)
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"))
@@ -462,21 +466,22 @@ class AILab_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": True, "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"]}),
"hf_token": ("STRING", {"default": "", "multiline": False, "tooltip": GGUF_PROMPT_TOOLTIPS["hf_token"]}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
@@ -484,8 +489,9 @@ class AILab_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:
@@ -498,7 +504,8 @@ class AILab_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
@@ -510,7 +517,8 @@ class AILab_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()
@@ -520,7 +528,8 @@ class AILab_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")
def _resolve_model_path(self, model_name):
models = self.gguf_models.get("models") or {}
@@ -567,7 +576,7 @@ class AILab_QwenVL_GGUF_PromptEnhancer:
return base_dir / model_name
def _maybe_download_model(self, model_name, resolved):
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
@@ -594,44 +603,39 @@ class AILab_QwenVL_GGUF_PromptEnhancer:
attempted.append(repo_id)
print(f"[QwenVL] Downloading GGUF {filename} from {repo_id}")
try:
downloaded = hf_hub_download(
repo_id=repo_id,
filename=filename,
repo_type="model",
local_dir=str(target_dir),
)
downloaded_path = Path(downloaded)
if downloaded_path.exists() and downloaded_path.resolve() != resolved.resolve():
resolved.parent.mkdir(parents=True, exist_ok=True)
downloaded_path.replace(resolved)
except Exception as exc:
print(f"[QwenVL] hf_hub_download failed from {repo_id}: {exc}")
if resolved.exists():
break
try:
snapshot_download(
repo_id=repo_id,
repo_type="model",
local_dir=str(target_dir),
allow_patterns=[filename, f"**/{filename}"],
download_hf_file_to_path(
[repo_id],
filename,
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] Filtered snapshot failed from {repo_id}: {exc}")
print(f"[QwenVL] Download failed from {repo_id}: {exc}")
if resolved.exists():
break
found = list(target_dir.rglob(filename))
if found:
resolved.parent.mkdir(parents=True, exist_ok=True)
found[0].replace(resolved)
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):
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)
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)
@@ -640,8 +644,9 @@ class AILab_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():
@@ -650,7 +655,8 @@ class AILab_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:
@@ -679,7 +685,8 @@ class AILab_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:
@@ -693,7 +700,7 @@ class AILab_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(
@@ -710,11 +717,12 @@ class AILab_QwenVL_GGUF_PromptEnhancer:
enable_thinking=True,
):
"""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,
@@ -747,29 +755,38 @@ class AILab_QwenVL_GGUF_PromptEnhancer:
full_text = ""
stage_started_at = time.monotonic()
last_status_at = stage_started_at
compact_progress_state = {"rendered_snapshot": "", "pending_text": ""}
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:
@@ -787,38 +804,32 @@ class AILab_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)
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_compact_progress(stage_label, stage_started_at, last_status_at, full_text, compact_progress_state)
else:
last_status_at = _maybe_emit_prompt_waiting_heartbeat(stage_label, stage_started_at, last_status_at)
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:
had_text_before = bool(full_text)
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)
else:
last_status_at = _maybe_emit_prompt_compact_progress(
stage_label,
stage_started_at,
last_status_at,
full_text,
compact_progress_state,
force=not had_text_before,
)
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":
@@ -835,19 +846,13 @@ class AILab_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()
elif full_text:
_maybe_emit_prompt_compact_progress(
stage_label,
stage_started_at,
last_status_at,
full_text,
compact_progress_state,
final=True,
)
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"))
@@ -919,8 +924,9 @@ class AILab_QwenVL_GGUF_PromptEnhancer:
unique_id=None,
extra_pnginfo=None,
enable_thinking=True,
hf_token="",
):
node_class = "AILab_QwenVL_GGUF_PromptEnhancer"
node_class = "ThinkingLLM_QwenVL_GGUF_PromptEnhancer"
input_signature = build_node_input_signature(
model_name=model_name,
prompt_text=prompt_text,
@@ -929,6 +935,10 @@ class AILab_QwenVL_GGUF_PromptEnhancer:
english_output=bool(english_output),
device=device,
enable_thinking=bool(enable_thinking),
max_tokens=max_tokens,
temperature=temperature,
top_p=top_p,
repetition_penalty=repetition_penalty,
)
# Auto-retrieve saved prompt when seed is fixed (no keep_last_prompt needed)
@@ -937,13 +947,14 @@ class AILab_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(
@@ -953,7 +964,7 @@ class AILab_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:
@@ -964,7 +975,7 @@ class AILab_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, {})
@@ -980,18 +991,25 @@ class AILab_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.")
else:
print("[QwenVL GGUF] Prompt enhancer compact terminal progress is active; enable stream_tokens_to_terminal for full readable chunk output.")
self._load_model(model_name, device, enable_thinking=effective_thinking)
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,
@@ -1004,11 +1022,6 @@ class AILab_QwenVL_GGUF_PromptEnhancer:
initial_stage_label="INITIAL GENERATION",
enable_thinking=effective_thinking,
)
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(
@@ -1044,18 +1057,21 @@ class AILab_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):
@@ -1069,9 +1085,11 @@ class AILab_QwenVL_GGUF_PromptEnhancer:
NODE_CLASS_MAPPINGS = {
"AILab_QwenVL_GGUF_PromptEnhancer": AILab_QwenVL_GGUF_PromptEnhancer,
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer": ThinkingLLM_QwenVL_GGUF_PromptEnhancer,
"AILab_QwenVL_GGUF_PromptEnhancer": ThinkingLLM_QwenVL_GGUF_PromptEnhancer,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer": "ThinkingLLM Prompt Enhancer (GGUF)",
"AILab_QwenVL_GGUF_PromptEnhancer": "ThinkingLLM Prompt Enhancer (GGUF)",
}
+144 -33
View File
@@ -6,14 +6,18 @@
import gc
import hashlib
import importlib
import json
import platform
import re
import time
from enum import Enum
from pathlib import Path
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import subprocess
import sys
from transformers import AutoModelForCausalLM, AutoProcessor, AutoTokenizer, BitsAndBytesConfig
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output, prompt_output_guard
from AILab_StreamDisplay import TerminalStreamDisplay
@@ -26,9 +30,11 @@ 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,
ensure_model,
ensure_cuda_vram_headroom,
get_cache_key,
get_alternative_cache_key,
@@ -60,6 +66,7 @@ DEFAULT_STYLES = {
}
_EMPTY_THINK_RE = re.compile(r"<think[^>]*>\s*</think>", flags=re.IGNORECASE | re.DOTALL)
_STREAM_HEARTBEAT_INTERVAL_SECONDS = 3.0
def _describe_prompt_enhancer_thinking(requested_thinking: bool, effective_thinking: bool, raw_text: str) -> str:
@@ -82,6 +89,18 @@ def _describe_prompt_enhancer_thinking(requested_thinking: bool, effective_think
)
def _maybe_emit_hf_prompt_stream_heartbeat(stage_label: str, started_at: float, last_status_at: float, full_text: str) -> float:
now = time.monotonic()
if (now - last_status_at) < _STREAM_HEARTBEAT_INTERVAL_SECONDS:
return last_status_at
cleaned_preview = clean_model_output(full_text, OutputCleanConfig(mode="prompt"))
if cleaned_preview and len(cleaned_preview.strip()) >= 32:
return now
elapsed = max(0.0, now - started_at)
print(f"[QwenVL HF] {stage_label}: reasoning hidden; still generating... ({len(full_text)} chars, {elapsed:.1f}s)")
return now
def _load_prompt_styles() -> dict[str, str]:
try:
with open(SYSTEM_PROMPTS_PATH, "r", encoding="utf-8") as fh:
@@ -106,8 +125,20 @@ 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 AILab_QwenVL_PromptEnhancer(QwenVLBase):
class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase):
STYLES = PROMPT_STYLES
RETURN_TYPES = ("STRING", "STRING")
@@ -118,13 +149,14 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
def __init__(self):
super().__init__()
self.text_model = None
self.text_processor = None
self.text_tokenizer = None
self.text_signature = None
@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")
@@ -135,18 +167,19 @@ class AILab_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": True}),
"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": {
@@ -176,8 +209,9 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
unique_id=None,
extra_pnginfo=None,
enable_thinking=True,
hf_token="",
):
node_class = "AILab_QwenVL_PromptEnhancer"
node_class = "ThinkingLLM_QwenVL_PromptEnhancer"
input_signature = build_node_input_signature(
model_name=model_name,
quantization=quantization,
@@ -188,6 +222,10 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
enhancement_style=enhancement_style,
custom_system_prompt=custom_system_prompt,
enable_thinking=bool(enable_thinking),
max_tokens=max_tokens,
temperature=temperature,
top_p=top_p,
repetition_penalty=repetition_penalty,
)
# Auto-retrieve saved prompt when seed is fixed (no keep_last_prompt needed)
@@ -196,13 +234,14 @@ class AILab_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(
@@ -229,7 +268,9 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
keep_model_loaded,
seed,
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:
@@ -250,7 +291,9 @@ class AILab_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 ""
@@ -259,7 +302,8 @@ class AILab_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)
@@ -281,6 +325,7 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
unique_id=None,
extra_pnginfo=None,
enable_thinking=True,
hf_token: str | None = None,
):
output = self.run(
model_name=model_name,
@@ -302,18 +347,28 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
device=device,
unique_id=unique_id,
extra_pnginfo=extra_pnginfo,
node_class="AILab_QwenVL_PromptEnhancer",
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):
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:
raise ValueError(f"[QwenVL] Missing repo_id for text model: {model_name}")
prefers_processor = "gemma" in model_name.lower() or "gemma" in repo_id.lower()
model_source = ensure_model(
model_name,
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":
device = "cuda" if torch.cuda.is_available() else ("mps" if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available() else "cpu")
else:
@@ -337,6 +392,7 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
return
self.text_model = None
self.text_processor = None
self.text_tokenizer = None
self.text_signature = None
@@ -347,8 +403,57 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
load_kwargs["torch_dtype"] = torch.float16 if device == "cuda" else torch.float32
print(f"[QwenVL] Loading text model {model_name} ({quantization})")
self.text_tokenizer = AutoTokenizer.from_pretrained(repo_id, trust_remote_code=True)
self.text_model = AutoModelForCausalLM.from_pretrained(repo_id, trust_remote_code=True, **load_kwargs).eval()
tokenizer_kwargs = {"trust_remote_code": True}
def _load_tokenizer(prefer_slow: bool = False):
if prefers_processor:
processor = AutoProcessor.from_pretrained(model_source, use_fast=not prefer_slow, **tokenizer_kwargs)
tokenizer = getattr(processor, "tokenizer", None)
if tokenizer is None:
raise ValueError(f"[QwenVL] AutoProcessor for {repo_id} did not expose a tokenizer")
self.text_processor = processor
return tokenizer
self.text_processor = None
return AutoTokenizer.from_pretrained(model_source, use_fast=not prefer_slow, **tokenizer_kwargs)
try:
self.text_tokenizer = _load_tokenizer(prefer_slow=False)
except ValueError as exc:
message = str(exc)
missing_backend = "sentencepiece or tiktoken" in message.lower()
if missing_backend:
print("[QwenVL] Tokenizer backend missing; installing sentencepiece and tiktoken into the active ComfyUI Python environment.")
install_cmd = [
sys.executable,
"-m",
"pip",
"install",
"--upgrade",
"sentencepiece",
"tiktoken",
]
result = subprocess.run(install_cmd, capture_output=True, text=True)
if result.returncode != 0:
stdout = (result.stdout or "").strip()
stderr = (result.stderr or "").strip()
raise RuntimeError(
"[QwenVL] Failed to install tokenizer backends automatically. "
f"Command: {' '.join(install_cmd)}\nstdout:\n{stdout or '<empty>'}\nstderr:\n{stderr or '<empty>'}"
) from exc
importlib.invalidate_caches()
for module_name in ("sentencepiece", "tiktoken"):
try:
importlib.import_module(module_name)
except Exception:
pass
try:
self.text_tokenizer = _load_tokenizer(prefer_slow=True)
except Exception as retry_exc:
raise RuntimeError(
f"[QwenVL] Failed to load tokenizer for {repo_id}. "
f"Initial error: {exc}. Retry error: {retry_exc}"
) from retry_exc
self.text_model = AutoModelForCausalLM.from_pretrained(model_source, trust_remote_code=True, **load_kwargs).eval()
self.text_model.to(device)
ensure_cuda_vram_headroom("QwenVL PromptEnhancer HF", min_free_gb=1.0, min_free_ratio=0.08)
# Detect architecture from loaded model config
@@ -373,10 +478,13 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
keep_model_loaded,
seed,
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)
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":
@@ -416,19 +524,19 @@ class AILab_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,
@@ -449,12 +557,14 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
try:
from transformers import TextIteratorStreamer
from threading import Thread
stream_display = TerminalStreamDisplay("QwenVL HF", suppress_planning=True, compact=True)
stage_label = "INITIAL GENERATION"
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})
stream_display.start_stage(stage_label)
thread.start()
full_streamed = ""
for token_str in streamer:
@@ -464,13 +574,14 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
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
self.text_tokenizer = None
self.text_signature = None
if torch.cuda.is_available():
@@ -490,11 +601,9 @@ class AILab_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
self.text_tokenizer = None
self.text_signature = None
if torch.cuda.is_available():
@@ -503,9 +612,11 @@ class AILab_QwenVL_PromptEnhancer(QwenVLBase):
return result, raw_text
NODE_CLASS_MAPPINGS = {
"AILab_QwenVL_PromptEnhancer": AILab_QwenVL_PromptEnhancer,
"ThinkingLLM_QwenVL_PromptEnhancer": ThinkingLLM_QwenVL_PromptEnhancer,
"AILab_QwenVL_PromptEnhancer": ThinkingLLM_QwenVL_PromptEnhancer,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ThinkingLLM_QwenVL_PromptEnhancer": "ThinkingLLM Prompt Enhancer",
"AILab_QwenVL_PromptEnhancer": "ThinkingLLM Prompt Enhancer",
}
+220 -34
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,45 +328,86 @@ class TerminalStreamDisplay:
sys.stdout.write(msg)
sys.stdout.flush()
def _write_stdout_line(self, text: str):
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 _write_wrapped_stream_text(self, text: str):
if not text:
return
msg = f"{text}\n"
sys.stdout.write(msg)
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 push_compact(self, text: str):
"""Append *text* and print readable throttled snapshots.
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)
Carriage-return rewrites look fine in a raw terminal, but ComfyUI's log
capture turns them into duplicated high-speed garbage. Emit a compact
line snapshot only when enough text or time has accumulated.
"""
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_stdout_line(f"[{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 readable snapshot."""
tail = self._compact_tail()
if tail and tail != self._last_compact_tail:
self._stage_updates += 1
self._write_stdout_line(f"[{self.label}] {tail}")
"""Finish compact mode with one final newline-terminated snapshot."""
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._last_compact_emit_at = 0.0
self._compact_active = False
self._buffer = ""
self._stream_pending_text = ""
self._stream_col = 0
self._stream_need_space = False
+5
View File
@@ -1,6 +1,7 @@
{
"_preset_prompts": [
"🚫 No preset (image-only)",
"🎦 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 +24,7 @@
],
"qwenvl": {
"🚫 No preset (image-only)": "",
"🎦 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 +42,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",
}
+96 -15
View File
@@ -1,11 +1,27 @@
# 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.
> GPL-3.0 fork of Deaquay/ComfyUI-Qwen3.5-Uncensored, itself derived from huchukato/ComfyUI-QwenVL-Mod and 1038lab/ComfyUI-QwenVL. See [LICENSE](LICENSE).
![ThinkingLLM Demo](docs/DemoThinkingLLM%20(1).gif)
![Node screenshot](docs/Screenshot%20of%20Notes.png)
> 🎥 Prefer a narrated version? **[Watch the demo on YouTube](https://youtu.be/K1JsvnzujOw)**
## 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.
@@ -33,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
@@ -45,39 +70,93 @@ This is the recommended default path.
The HF path is the most straightforward option for Windows and Linux when you want the fewest install complications.
HF tokenizer note: some Gemma and Qwen variants need `sentencepiece` or `tiktoken` available in the ComfyUI Python environment. ThinkingLLM now declares those dependencies and will attempt to install them automatically if a tokenizer backend is missing at first load.
### GGUF / llama.cpp nodes
- `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. Use the setup notes in [docs/LLAMA_CPP_PYTHON_VISION_INSTALL.md](docs/LLAMA_CPP_PYTHON_VISION_INSTALL.md).
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
ThinkingLLM works with these model families:
ThinkingLLM supports these model families out of the box, with pre-configured entries in `hf_models.json` and `gguf_models.json`.
### Vision-language (HF)
### Qwen — Vision-language (HF)
- Qwen3.5 4B / 9B
- Qwen3-VL 4B / 8B (Instruct + abliterated)
- Qwen3-VL 4B / 8B (Instruct, abliterated, unredacted)
- Qwen3-VL 32B Heretic
- Qwen2.5-VL 3B / 7B
### Vision-language (GGUF)
- Qwen3-VL 4B / 8B (Q4 K M through F16)
- Qwen2.5-VL 3B / 7B (Q4 K M through F16)
### Qwen — Vision-language (GGUF)
- Qwen3.5 4B / 9B / 27B (Q4 K M through BF16)
- Qwen3-VL 4B / 8B / 32B (Instruct, abliterated, thinking)
- Qwen2.5-VL 3B / 7B
### Text-only (HF)
### Qwen — Text-only (HF)
- Qwen3 0.6B / 4B / 8B
- Qwen3.5 4B / 9B
- Qwen3.5 4B / 9B (base + heretic + uncensored)
### Text-only (GGUF)
- Qwen3 4B ablitterated / Josiefied (Q4 K M through F16)
- Qwen3 8B ablitterated / Josiefied (Q4 K M through F16)
### Qwen — Text-only (GGUF)
- Qwen3 4B / 8B (abliterated, Josiefied, base)
- Qwen3.5 4B / 9B / 27B (base + uncensored HauhauCS)
Pre-configured model entries are in `hf_models.json` and `gguf_models.json`. Local GGUF models placed in `models/LLM/GGUF/` with a matching mmproj are auto-discovered.
### 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
Local GGUF models placed in `models/LLM/GGUF/` with a matching mmproj file are auto-discovered. Pre-configured entries live in `hf_models.json` and `gguf_models.json`.
## Quick install
### ComfyUI Manager (recommended)
1. Open **ComfyUI Manager** → **Install via Registry**
2. Search for `ThinkingLLM`
3. Click **Install**
### Manual install
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/goodguy1963/ComfyUI-ThinkingLLM.git
@@ -100,7 +179,9 @@ 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 needs the matching Linux wheel or compatible local build.
- 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.
+5 -3
View File
@@ -1,11 +1,11 @@
"""
{
"name": "ComfyUI-ThinkingLLM",
"description": "ThinkingLLM multimodal fork with HF and GGUF paths. GPL-3.0 lineage preserved from Deaquay/ComfyUI-Qwen3.5-Uncensored, huchukato/ComfyUI-QwenVL-Mod, and 1038lab/ComfyUI-QwenVL.",
"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": "2.2.5",
"version": "2.3.9",
"url": "https://github.com/goodguy1963/ComfyUI-ThinkingLLM",
"category": "image"
"category": "image"
}
"""
@@ -31,6 +31,8 @@ def load_modules_from_directory(directory):
try:
spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec is None or spec.loader is None:
raise ImportError(f"Unable to create import spec for {module_name}")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
+2 -2
View File
@@ -6,8 +6,8 @@
"https://github.com/goodguy1963/ComfyUI-ThinkingLLM"
],
"install_type": "git-clone",
"description": "ThinkingLLM multimodal fork with Hugging Face and GGUF nodes, prompt caching, WAN 2.2-oriented workflows, and GPL-preserved lineage from ComfyUI-Qwen3.5-Uncensored and earlier QwenVL forks.",
"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": "2.2.5"
"version": "2.3.9"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

+88 -8
View File
@@ -15,6 +15,66 @@ Release wheels (download `.whl` here):
Stop ComfyUI before installing/replacing packages, especially on Windows portable.
## Linux auto-install in ThinkingLLM
On Linux, ThinkingLLM now performs a backend check the first time you run a GGUF node.
- 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 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)
@@ -52,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)**
@@ -71,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
@@ -88,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).
+2
View File
@@ -19,4 +19,6 @@
- Metadata has been rebranded to `ComfyUI-ThinkingLLM` with publisher id `goodguy1963`.
- Repository URLs currently point at the planned GitHub location `https://github.com/goodguy1963/ComfyUI-ThinkingLLM` and need a real remote repo before registry publication.
- Registry and ComfyUI-Manager publication use the semantic version in `pyproject.toml` as the source of truth.
- Keep the same version mirrored in the `__init__.py` metadata block and `custom_nodes.json` so local ComfyUI metadata and manager-facing metadata stay aligned.
- README still contains historical upstream links and changelog references beyond the top-level fork identity; that is acceptable for attribution, but a docs pass should normalize publish-facing instructions before release.
Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

-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)",
+91 -1
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",
@@ -333,6 +372,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 +459,30 @@
"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
}
},
"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 +621,4 @@
}
}
}
}
}
+66 -6
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,8 +142,18 @@
"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": "HauhauCS/Gemma-4-E4B-Uncensored",
"repo_id": "prithivMLmods/gemma-4-E4B-it-Uncensored-MAX",
"default": false,
"quantized": false,
"vram_requirement": {
@@ -153,7 +163,7 @@
}
},
"Gemma-4-E2B-Uncensored [DL: 10.3GB, VRAM: 4.0GB]": {
"repo_id": "HauhauCS/Gemma-4-E2B-Uncensored",
"repo_id": "prithivMLmods/gemma-4-E2B-it-Uncensored-MAX",
"default": false,
"quantized": false,
"vram_requirement": {
@@ -171,6 +181,26 @@
"8bit": 22.0,
"4bit": 14.0
}
},
"Gemma-4-E4B-Uncensored-Aggressive [DL: 16GB, VRAM: 7.0GB]": {
"repo_id": "HauhauCS/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive",
"default": false,
"quantized": false,
"vram_requirement": {
"full": 7.0,
"8bit": 4.5,
"4bit": 2.5
}
},
"Gemma-4-E2B-Uncensored-Aggressive [DL: 10.3GB, VRAM: 4.0GB]": {
"repo_id": "HauhauCS/Gemma-4-E2B-Uncensored-HauhauCS-Aggressive",
"default": false,
"quantized": false,
"vram_requirement": {
"full": 4.0,
"8bit": 2.5,
"4bit": 1.5
}
}
},
"hf_text_models": {
@@ -274,8 +304,18 @@
"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": "HauhauCS/Gemma-4-E4B-Uncensored",
"repo_id": "prithivMLmods/gemma-4-E4B-it-Uncensored-MAX",
"default": false,
"quantized": false,
"vram_requirement": {
@@ -285,7 +325,7 @@
}
},
"Gemma-4-E2B-Uncensored [DL: 10.3GB, VRAM: 4.0GB]": {
"repo_id": "HauhauCS/Gemma-4-E2B-Uncensored",
"repo_id": "prithivMLmods/gemma-4-E2B-it-Uncensored-MAX",
"default": false,
"quantized": false,
"vram_requirement": {
@@ -303,6 +343,26 @@
"8bit": 22.0,
"4bit": 14.0
}
},
"Gemma-4-E4B-Uncensored-Aggressive [DL: 16GB, VRAM: 7.0GB]": {
"repo_id": "HauhauCS/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive",
"default": false,
"quantized": false,
"vram_requirement": {
"full": 7.0,
"8bit": 4.5,
"4bit": 2.5
}
},
"Gemma-4-E2B-Uncensored-Aggressive [DL: 10.3GB, VRAM: 4.0GB]": {
"repo_id": "HauhauCS/Gemma-4-E2B-Uncensored-HauhauCS-Aggressive",
"default": false,
"quantized": false,
"vram_requirement": {
"full": 4.0,
"8bit": 2.5,
"4bit": 1.5
}
}
}
}
}
+4 -4
View File
@@ -1,9 +1,9 @@
[project]
name = "ComfyUI-ThinkingLLM"
description = "ThinkingLLM multimodal ComfyUI fork with Hugging Face and GGUF backends, WAN 2.2 workflows, prompt tooling, and preserved GPL fork lineage from the Qwen3.5-Uncensored branch."
version = "2.2.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."
version = "2.3.9"
license = {file = "LICENSE"}
dependencies = ["transformers>=5.2.0", "torch", "huggingface-hub", "hf_xet", "psutil", "numpy", "Pillow", "opencv-python", "bitsandbytes", "accelerate", "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"
+3
View File
@@ -11,6 +11,9 @@ Pillow
opencv-python
bitsandbytes
accelerate
sentencepiece
tiktoken
faster-whisper
# For running GGUF models with vision support
# The [server] extra installs necessary components for multimodal models
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())
+66
View File
@@ -1,5 +1,71 @@
# ComfyUI-QwenVL Update Log
## 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
- **Live Terminal Token Streaming**: Both Prompt Enhancer nodes (HF and GGUF) now have a `stream_tokens_to_terminal` toggle. When enabled, readable generation progress and thinking-status summaries are shown in the ComfyUI terminal in real time.
- **RAW_TRACE Second Output**: Both Prompt Enhancer nodes now return a second string output (`RAW_TRACE`) containing the unprocessed model output, reasoning trace, and retry history — useful for prompt debugging and model analysis.
- **Per-Node Prompt State with JSON Persistence**: `keep_last_prompt` is now backed by durable per-node state (survives idle, module reload, and ComfyUI restart). Fixed-seed prompts are automatically reused between runs. Each node instance stores its own prompt independently via workflow fingerprinting.
- **Local Fork Lock**: `.no_auto_update` sentinel prevents ComfyUI Manager from overwriting this local fork with upstream updates.
### Installer
- Linux CUDA 12.8 auto-install matrix expanded to cover Python cp310–cp314 via JamePeng prebuilt wheels.
## Version 1.0.6 (2026/05/16) — Windows GGUF Backend Import Fix
### 1.0.6 Changes
- Fixed Windows GGUF backend startup on portable ComfyUI installs by adding PyTorch runtime DLL directories before importing `llama_cpp`.
- Kept a fallback for nested portable installs where `os.add_dll_directory(...)` can fail with `WinError 206`, so the loader falls back to `PATH` instead of treating `llama_cpp` as missing.
- Added regression coverage for both the runtime DLL directory discovery and the long-path DLL-directory fallback.
### 1.0.6 Validation
- Embedded Python backend probe passes: `ensure_llama_cpp_backend(require_vision_handlers=True)` resolves `Llama`.
- Local regression suite passes: `python tests/test_new_features.py`
## Version 1.0.5 (2026/05/16) — Streaming Toggle Fix And Release Retention
### 1.0.5 Changes
- Stopped the GGUF prompt enhancer from printing generated prompt text to the terminal when `stream_tokens_to_terminal` is turned off.
- Kept the rest of the node set unchanged after confirming the stream-toggle wiring is correct for the HF node, GGUF node, HF prompt enhancer, and wrapper nodes.
- Added regression coverage so the GGUF prompt enhancer cannot reintroduce an off-toggle text-stream fallback.
### Release Retention
- Added tag-driven GitHub release automation so each version tag creates or updates only its own release entry.
- Older releases remain available side-by-side instead of being replaced by the newest published version.
### 1.0.5 Validation
- Local regression suite passes: `python tests/test_new_features.py`
## Version 1.0.4 (2026/05/16) — GGUF Workflow Compatibility Fix
### 1.0.4 Changes
- Restored legacy widget ordering compatibility for `ThinkingLLM_QwenVL_GGUF_Advanced` so older saved workflows no longer shift values into the wrong advanced GGUF fields.
- Added compatibility shims for deprecated legacy fields (`seed_mode` and `unload_after_run`) so old workflows deserialize cleanly without affecting current runtime behavior.
- Added regression tests that lock the advanced GGUF widget order and `process()` signature alignment.
### 1.0.4 Validation
- Local regression suite passes: `python tests/test_new_features.py`
## Version 2.2.5-local-locked (2026/05/14) — Complete Feature Parity
### 🎯 All 6 Nodes Now Have:
+432 -5
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,18 +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",
"n_ubatch",
"n_threads",
"n_threads_batch",
"flash_attn",
"offload_kqv",
"ctx_checkpoints",
]);
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) {
@@ -34,17 +125,353 @@ 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, -4];
}
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 = findWidget(node, RECOMMENDATION_WIDGET_NAME);
if (widget) {
return widget;
}
widget = ComfyWidgets.STRING(node, RECOMMENDATION_WIDGET_NAME, ["STRING", { multiline: true }], app).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;
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."
]
}
]
}
+49
View File
@@ -0,0 +1,49 @@
{
"preset_prompt": {
"🚫 No preset (image-only)": "",
"🎦 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."
}
}