Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea25a8726f | ||
|
|
21e90d0ba4 | ||
|
|
abe35d6dfb | ||
|
|
028b2df006 | ||
|
|
de5d3e5da3 | ||
|
|
90df6de975 | ||
|
|
6faa6bade2 | ||
|
|
1189b6c4b3 | ||
|
|
c49cbd1ceb | ||
|
|
00f066d34f | ||
|
|
21906a9dda | ||
|
|
dc80a9e503 | ||
|
|
7f0fe4a6de | ||
|
|
79a1dff210 | ||
|
|
641df140e2 | ||
|
|
bcd8f4ae35 | ||
|
|
7b07a4d2ac | ||
|
|
f214136331 | ||
|
|
7e75bdedf4 | ||
|
|
ff1187ae6c | ||
|
|
a95561e1ae | ||
|
|
2184578cfc | ||
|
|
b2295ab23a | ||
|
|
972998ef98 | ||
|
|
c091b30faf | ||
|
|
4815f3bc24 | ||
|
|
406079392f | ||
|
|
9e18c83b5e | ||
|
|
2250058c4b | ||
|
|
9532eebb74 | ||
|
|
f1ebccdd18 | ||
|
|
1cae110284 | ||
|
|
e1204a321b | ||
|
|
aec778b8b7 | ||
|
|
4046784770 | ||
|
|
9629fddc5f | ||
|
|
b0e2e91f15 | ||
|
|
56e888b4fb | ||
|
|
74a35412a4 | ||
|
|
9441cb86b9 | ||
|
|
9dff35241b | ||
|
|
da1cf750f1 | ||
|
|
fc8d3f9b21 | ||
|
|
d6d88d6ec6 | ||
|
|
f38739bc7d | ||
|
|
b841a936cb | ||
|
|
b73db55a95 | ||
|
|
b3d55e94c2 | ||
|
|
e9e6c3abd2 | ||
|
|
b58ecc6590 | ||
|
|
b2a9718cac | ||
|
|
10ff0bd91f | ||
|
|
4d845474c7 | ||
|
|
b4d9aa9a6d | ||
|
|
4150252005 |
@@ -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
|
||||
@@ -58,6 +58,8 @@ temp/
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.gif
|
||||
!docs/*.gif
|
||||
!docs/*.png
|
||||
*.webp
|
||||
*.mp4
|
||||
*.avi
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+204
-186
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
.gif)
|
||||

|
||||
|
||||
> 🎥 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
@@ -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
@@ -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 |
@@ -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).
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
+1642
-7
File diff suppressed because it is too large
Load Diff
@@ -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())
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user