891 lines
33 KiB
Python
891 lines
33 KiB
Python
import importlib.util
|
|
import pathlib
|
|
import sys
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def load_plugin_package():
|
|
for name in list(sys.modules):
|
|
if name == "lls_node_test_qwen" or name.startswith("lls_node_test_qwen."):
|
|
sys.modules.pop(name)
|
|
|
|
spec = importlib.util.spec_from_file_location(
|
|
"lls_node_test_qwen",
|
|
ROOT / "__init__.py",
|
|
submodule_search_locations=[str(ROOT)],
|
|
)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules["lls_node_test_qwen"] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
class NodeOutputStub:
|
|
def __init__(self, *args):
|
|
self.args = args
|
|
|
|
@property
|
|
def result(self):
|
|
return self.args
|
|
|
|
def __getitem__(self, index):
|
|
return self.args[index]
|
|
|
|
|
|
class QwenFolderPathsStub:
|
|
def __init__(self):
|
|
self._files = {
|
|
"diffusion_models": [
|
|
"qwen_image_fp8_e4m3fn.safetensors",
|
|
"qwen_image_2512_fp8_e4m3fn.safetensors",
|
|
"qwen_image_edit_2509_fp8_e4m3fn.safetensors",
|
|
"qwen_image_edit_2511_bf16.safetensors",
|
|
"qwen_image_layered_bf16.safetensors",
|
|
"flux1-dev.safetensors",
|
|
],
|
|
"text_encoders": ["qwen_2.5_vl_7b_fp8_scaled.safetensors"],
|
|
"vae": ["qwen_image_vae.safetensors"],
|
|
"loras": [
|
|
"Qwen-Image-Lightning-8steps-V1.0.safetensors",
|
|
"Qwen-Image-2512-Lightning-4steps-V1.0-fp32.safetensors",
|
|
"Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors",
|
|
"Qwen-Image-Edit-2511-Lightning-4steps-V1.0.safetensors",
|
|
"flux-dev-style.safetensors",
|
|
],
|
|
}
|
|
|
|
def get_filename_list(self, category):
|
|
return list(self._files.get(category, []))
|
|
|
|
|
|
class QwenClipStub:
|
|
def __init__(self, clip_name, clip_type):
|
|
self.clip_name = clip_name
|
|
self.clip_type = clip_type
|
|
|
|
|
|
class QwenVAEStub:
|
|
def __init__(self, vae_name):
|
|
self.vae_name = vae_name
|
|
|
|
|
|
class CoreQwenNodesStub:
|
|
last_unet_call = None
|
|
last_clip_call = None
|
|
last_text_encode_calls = []
|
|
last_ksampler_call = None
|
|
last_vae_encode_call = None
|
|
last_vae_decode_call = None
|
|
last_vae_load_call = None
|
|
last_lora_calls = []
|
|
|
|
@classmethod
|
|
def reset(cls):
|
|
cls.last_unet_call = None
|
|
cls.last_clip_call = None
|
|
cls.last_text_encode_calls = []
|
|
cls.last_ksampler_call = None
|
|
cls.last_vae_encode_call = None
|
|
cls.last_vae_decode_call = None
|
|
cls.last_vae_load_call = None
|
|
cls.last_lora_calls = []
|
|
|
|
class UNETLoader:
|
|
def load_unet(self, unet_name, weight_dtype):
|
|
CoreQwenNodesStub.last_unet_call = {
|
|
"unet_name": unet_name,
|
|
"weight_dtype": weight_dtype,
|
|
}
|
|
return (f"MODEL::{unet_name}",)
|
|
|
|
class CLIPLoader:
|
|
def load_clip(self, clip_name, type="stable_diffusion", device="default"):
|
|
CoreQwenNodesStub.last_clip_call = {
|
|
"clip_name": clip_name,
|
|
"type": type,
|
|
"device": device,
|
|
}
|
|
return (QwenClipStub(clip_name, type),)
|
|
|
|
class VAELoader:
|
|
def load_vae(self, vae_name):
|
|
CoreQwenNodesStub.last_vae_load_call = {"vae_name": vae_name}
|
|
return (QwenVAEStub(vae_name),)
|
|
|
|
class CLIPTextEncode:
|
|
def encode(self, clip, text):
|
|
CoreQwenNodesStub.last_text_encode_calls.append(
|
|
{
|
|
"clip_name": clip.clip_name,
|
|
"clip_type": clip.clip_type,
|
|
"text": text,
|
|
}
|
|
)
|
|
return (f"COND::{text}",)
|
|
|
|
class LoraLoaderModelOnly:
|
|
def load_lora_model_only(self, model, lora_name, strength_model):
|
|
CoreQwenNodesStub.last_lora_calls.append(
|
|
{
|
|
"model": model,
|
|
"lora_name": lora_name,
|
|
"strength_model": strength_model,
|
|
}
|
|
)
|
|
return (f"LORA::{model}::{lora_name}::{strength_model}",)
|
|
|
|
class KSampler:
|
|
def sample(
|
|
self,
|
|
model,
|
|
seed,
|
|
steps,
|
|
cfg,
|
|
sampler_name,
|
|
scheduler,
|
|
positive,
|
|
negative,
|
|
latent_image,
|
|
denoise=1.0,
|
|
):
|
|
CoreQwenNodesStub.last_ksampler_call = {
|
|
"model": model,
|
|
"seed": seed,
|
|
"steps": steps,
|
|
"cfg": cfg,
|
|
"sampler_name": sampler_name,
|
|
"scheduler": scheduler,
|
|
"positive": positive,
|
|
"negative": negative,
|
|
"latent_image": latent_image,
|
|
"denoise": denoise,
|
|
}
|
|
return ({"samples": f"SAMPLED::{seed}::{steps}"},)
|
|
|
|
class VAEDecode:
|
|
def decode(self, vae, samples):
|
|
CoreQwenNodesStub.last_vae_decode_call = {
|
|
"vae_name": vae.vae_name,
|
|
"samples": samples,
|
|
}
|
|
return (f"IMAGE::{samples['samples']}",)
|
|
|
|
class VAEEncode:
|
|
def encode(self, vae, pixels):
|
|
CoreQwenNodesStub.last_vae_encode_call = {
|
|
"vae_name": vae.vae_name,
|
|
"pixels": pixels,
|
|
}
|
|
return ({"samples": f"ENCODED::{pixels}"},)
|
|
|
|
|
|
class SD3NodesStub:
|
|
last_generate_call = None
|
|
|
|
@classmethod
|
|
def reset(cls):
|
|
cls.last_generate_call = None
|
|
|
|
class EmptySD3LatentImage:
|
|
def generate(self, width, height, batch_size=1):
|
|
SD3NodesStub.last_generate_call = {
|
|
"width": width,
|
|
"height": height,
|
|
"batch_size": batch_size,
|
|
}
|
|
return (
|
|
{
|
|
"samples": f"EMPTY::{width}x{height}::{batch_size}",
|
|
"downscale_ratio_spacial": 8,
|
|
},
|
|
)
|
|
|
|
|
|
class ModelAdvancedStub:
|
|
last_call = None
|
|
|
|
@classmethod
|
|
def reset(cls):
|
|
cls.last_call = None
|
|
|
|
class ModelSamplingAuraFlow:
|
|
def patch_aura(self, model, shift):
|
|
ModelAdvancedStub.last_call = {"model": model, "shift": shift}
|
|
return (f"AURA::{model}::{shift}",)
|
|
|
|
|
|
class FluxNodesStub:
|
|
last_scale_call = None
|
|
last_reference_calls = []
|
|
|
|
@classmethod
|
|
def reset(cls):
|
|
cls.last_scale_call = None
|
|
cls.last_reference_calls = []
|
|
|
|
class FluxKontextImageScale:
|
|
@classmethod
|
|
def execute(cls, image):
|
|
FluxNodesStub.last_scale_call = {"image": image}
|
|
return NodeOutputStub(f"SCALED::{image}")
|
|
|
|
class FluxKontextMultiReferenceLatentMethod:
|
|
@classmethod
|
|
def execute(cls, conditioning, reference_latents_method):
|
|
FluxNodesStub.last_reference_calls.append(
|
|
{
|
|
"conditioning": conditioning,
|
|
"reference_latents_method": reference_latents_method,
|
|
}
|
|
)
|
|
return NodeOutputStub(f"{conditioning}::{reference_latents_method}")
|
|
|
|
|
|
class QwenExtraNodesStub:
|
|
last_calls = []
|
|
|
|
@classmethod
|
|
def reset(cls):
|
|
cls.last_calls = []
|
|
|
|
class TextEncodeQwenImageEditPlus:
|
|
@classmethod
|
|
def execute(cls, clip, prompt, vae=None, image1=None, image2=None, image3=None):
|
|
QwenExtraNodesStub.last_calls.append(
|
|
{
|
|
"clip_name": clip.clip_name,
|
|
"prompt": prompt,
|
|
"vae_name": getattr(vae, "vae_name", None),
|
|
"image1": image1,
|
|
"image2": image2,
|
|
"image3": image3,
|
|
}
|
|
)
|
|
return NodeOutputStub(f"EDIT_COND::{prompt}::{image1}::{image2}::{image3}")
|
|
|
|
|
|
class CFGNodesStub:
|
|
last_call = None
|
|
|
|
@classmethod
|
|
def reset(cls):
|
|
cls.last_call = None
|
|
|
|
class CFGNorm:
|
|
@classmethod
|
|
def execute(cls, model, strength):
|
|
CFGNodesStub.last_call = {"model": model, "strength": strength}
|
|
return NodeOutputStub(f"CFGNORM::{model}::{strength}")
|
|
|
|
|
|
class TestQwenNodes(unittest.TestCase):
|
|
def setUp(self):
|
|
CoreQwenNodesStub.reset()
|
|
SD3NodesStub.reset()
|
|
ModelAdvancedStub.reset()
|
|
FluxNodesStub.reset()
|
|
QwenExtraNodesStub.reset()
|
|
CFGNodesStub.reset()
|
|
|
|
def test_plugin_registers_qwen_nodes(self):
|
|
plugin = load_plugin_package()
|
|
|
|
self.assertIn("LLSQwenTextToImage", plugin.NODE_CLASS_MAPPINGS)
|
|
self.assertIn("LLSQwenImageEdit", plugin.NODE_CLASS_MAPPINGS)
|
|
self.assertNotIn("LLSQwenLoRAStack", plugin.NODE_CLASS_MAPPINGS)
|
|
|
|
def test_qwen_text_node_filters_only_text_models(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import nodes as qwen_nodes
|
|
|
|
with mock.patch.object(qwen_discovery, "folder_paths", QwenFolderPathsStub()):
|
|
choices = qwen_nodes.LLSQwenTextToImage.INPUT_TYPES()["required"]["model_name"][0]
|
|
|
|
self.assertEqual(
|
|
choices,
|
|
[
|
|
"qwen_image_2512_fp8_e4m3fn.safetensors",
|
|
"qwen_image_fp8_e4m3fn.safetensors",
|
|
],
|
|
)
|
|
|
|
def test_qwen_edit_node_filters_only_edit_models(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import nodes as qwen_nodes
|
|
|
|
with mock.patch.object(qwen_discovery, "folder_paths", QwenFolderPathsStub()):
|
|
choices = qwen_nodes.LLSQwenImageEdit.INPUT_TYPES()["required"]["model_name"][0]
|
|
|
|
self.assertEqual(
|
|
choices,
|
|
[
|
|
"qwen_image_edit_2509_fp8_e4m3fn.safetensors",
|
|
"qwen_image_edit_2511_bf16.safetensors",
|
|
],
|
|
)
|
|
|
|
def test_qwen_text_node_schema_exposes_advanced_inputs_and_optional_model(self):
|
|
plugin = load_plugin_package()
|
|
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSQwenTextToImage"]
|
|
required = node_cls.INPUT_TYPES()["required"]
|
|
optional = node_cls.INPUT_TYPES()["optional"]
|
|
|
|
self.assertEqual(node_cls.RETURN_TYPES, ("IMAGE",))
|
|
self.assertEqual(node_cls.RETURN_NAMES, ("image",))
|
|
self.assertEqual(
|
|
tuple(required.keys()),
|
|
(
|
|
"model_name",
|
|
"prompt",
|
|
"width",
|
|
"height",
|
|
"steps",
|
|
"seed",
|
|
"batch_size",
|
|
"negative_prompt",
|
|
"cfg",
|
|
"sampler_name",
|
|
"scheduler",
|
|
"shift",
|
|
"enable_turbo_mode",
|
|
"turbo_lora_name",
|
|
"turbo_strength",
|
|
),
|
|
)
|
|
self.assertEqual(required["negative_prompt"][1]["default"], "")
|
|
self.assertEqual(required["cfg"][1]["default"], 4.0)
|
|
self.assertIn("euler", required["sampler_name"][0])
|
|
self.assertIn("simple", required["scheduler"][0])
|
|
self.assertEqual(required["shift"][1]["default"], 3.1)
|
|
self.assertEqual(required["enable_turbo_mode"][1]["default"], False)
|
|
self.assertEqual(optional["model"][0], "MODEL")
|
|
|
|
def test_qwen_edit_node_schema_exposes_advanced_inputs_optional_images_and_model(self):
|
|
plugin = load_plugin_package()
|
|
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSQwenImageEdit"]
|
|
required = node_cls.INPUT_TYPES()["required"]
|
|
optional = node_cls.INPUT_TYPES()["optional"]
|
|
|
|
self.assertEqual(node_cls.RETURN_TYPES, ("IMAGE",))
|
|
self.assertEqual(node_cls.RETURN_NAMES, ("image",))
|
|
self.assertEqual(
|
|
tuple(required.keys()),
|
|
(
|
|
"model_name",
|
|
"image",
|
|
"prompt",
|
|
"steps",
|
|
"seed",
|
|
"negative_prompt",
|
|
"cfg",
|
|
"sampler_name",
|
|
"scheduler",
|
|
"shift",
|
|
"cfg_norm_strength",
|
|
"reference_latents_method",
|
|
"enable_turbo_mode",
|
|
"turbo_lora_name",
|
|
"turbo_strength",
|
|
),
|
|
)
|
|
self.assertEqual(optional["image2"][0], "IMAGE")
|
|
self.assertEqual(optional["image3"][0], "IMAGE")
|
|
self.assertEqual(optional["model"][0], "MODEL")
|
|
self.assertEqual(
|
|
required["reference_latents_method"][0],
|
|
["offset", "index", "uxo/uno", "index_timestep_zero"],
|
|
)
|
|
|
|
def test_qwen_text_node_turbo_lora_choices_filter_only_text_loras(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import nodes as qwen_nodes
|
|
|
|
with mock.patch.object(qwen_discovery, "folder_paths", QwenFolderPathsStub()):
|
|
choices = qwen_nodes.LLSQwenTextToImage.INPUT_TYPES()["required"]["turbo_lora_name"][0]
|
|
|
|
self.assertEqual(
|
|
choices,
|
|
[
|
|
"(auto)",
|
|
"Qwen-Image-2512-Lightning-4steps-V1.0-fp32.safetensors",
|
|
"Qwen-Image-Lightning-8steps-V1.0.safetensors",
|
|
],
|
|
)
|
|
|
|
def test_qwen_edit_node_turbo_lora_choices_filter_only_edit_loras(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import nodes as qwen_nodes
|
|
|
|
with mock.patch.object(qwen_discovery, "folder_paths", QwenFolderPathsStub()):
|
|
choices = qwen_nodes.LLSQwenImageEdit.INPUT_TYPES()["required"]["turbo_lora_name"][0]
|
|
|
|
self.assertEqual(
|
|
choices,
|
|
[
|
|
"(auto)",
|
|
"Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors",
|
|
"Qwen-Image-Edit-2511-Lightning-4steps-V1.0.safetensors",
|
|
],
|
|
)
|
|
|
|
def test_qwen_text_runtime_rejects_incompatible_model_name(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import runtime as qwen_runtime
|
|
|
|
stub = QwenFolderPathsStub()
|
|
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
|
|
qwen_runtime,
|
|
"folder_paths",
|
|
stub,
|
|
):
|
|
with self.assertRaisesRegex(RuntimeError, "not compatible with LLSQwenTextToImage"):
|
|
qwen_runtime.run_qwen_text_to_image(
|
|
model_name="qwen_image_edit_2511_bf16.safetensors",
|
|
prompt="a cat",
|
|
negative_prompt="",
|
|
width=1024,
|
|
height=1024,
|
|
steps=20,
|
|
seed=1,
|
|
batch_size=1,
|
|
cfg=4.0,
|
|
sampler_name="euler",
|
|
scheduler="simple",
|
|
shift=3.1,
|
|
enable_turbo_mode=False,
|
|
turbo_lora_name="(auto)",
|
|
turbo_strength=1.0,
|
|
model=None,
|
|
)
|
|
|
|
def test_qwen_edit_runtime_rejects_incompatible_model_name(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import runtime as qwen_runtime
|
|
|
|
stub = QwenFolderPathsStub()
|
|
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
|
|
qwen_runtime,
|
|
"folder_paths",
|
|
stub,
|
|
):
|
|
with self.assertRaisesRegex(RuntimeError, "not compatible with LLSQwenImageEdit"):
|
|
qwen_runtime.run_qwen_image_edit(
|
|
model_name="qwen_image_fp8_e4m3fn.safetensors",
|
|
image="IMAGE::input",
|
|
image2=None,
|
|
image3=None,
|
|
prompt="edit the cat",
|
|
negative_prompt="",
|
|
steps=20,
|
|
seed=1,
|
|
cfg=4.0,
|
|
sampler_name="euler",
|
|
scheduler="simple",
|
|
shift=3.1,
|
|
cfg_norm_strength=1.0,
|
|
reference_latents_method="index_timestep_zero",
|
|
enable_turbo_mode=False,
|
|
turbo_lora_name="(auto)",
|
|
turbo_strength=1.0,
|
|
model=None,
|
|
)
|
|
|
|
def test_qwen_text_runtime_raises_when_qwen_text_encoder_is_missing(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import runtime as qwen_runtime
|
|
|
|
stub = QwenFolderPathsStub()
|
|
stub._files["text_encoders"] = []
|
|
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
|
|
qwen_runtime,
|
|
"folder_paths",
|
|
stub,
|
|
):
|
|
with self.assertRaisesRegex(RuntimeError, "Missing Qwen text encoder"):
|
|
qwen_runtime.run_qwen_text_to_image(
|
|
model_name="qwen_image_fp8_e4m3fn.safetensors",
|
|
prompt="a cat",
|
|
negative_prompt="",
|
|
width=1024,
|
|
height=1024,
|
|
steps=20,
|
|
seed=1,
|
|
batch_size=1,
|
|
cfg=4.0,
|
|
sampler_name="euler",
|
|
scheduler="simple",
|
|
shift=3.1,
|
|
enable_turbo_mode=False,
|
|
turbo_lora_name="(auto)",
|
|
turbo_strength=1.0,
|
|
model=None,
|
|
)
|
|
|
|
def test_qwen_text_runtime_raises_when_qwen_vae_is_missing(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import runtime as qwen_runtime
|
|
|
|
stub = QwenFolderPathsStub()
|
|
stub._files["vae"] = []
|
|
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
|
|
qwen_runtime,
|
|
"folder_paths",
|
|
stub,
|
|
):
|
|
with self.assertRaisesRegex(RuntimeError, "Missing Qwen VAE"):
|
|
qwen_runtime.run_qwen_text_to_image(
|
|
model_name="qwen_image_fp8_e4m3fn.safetensors",
|
|
prompt="a cat",
|
|
negative_prompt="",
|
|
width=1024,
|
|
height=1024,
|
|
steps=20,
|
|
seed=1,
|
|
batch_size=1,
|
|
cfg=4.0,
|
|
sampler_name="euler",
|
|
scheduler="simple",
|
|
shift=3.1,
|
|
enable_turbo_mode=False,
|
|
turbo_lora_name="(auto)",
|
|
turbo_strength=1.0,
|
|
model=None,
|
|
)
|
|
|
|
def test_qwen_text_runtime_executes_internal_pipeline_when_model_not_connected(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import runtime as qwen_runtime
|
|
|
|
stub = QwenFolderPathsStub()
|
|
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
|
|
qwen_runtime,
|
|
"folder_paths",
|
|
stub,
|
|
), mock.patch.object(qwen_runtime, "comfy_core_nodes", CoreQwenNodesStub), mock.patch.object(
|
|
qwen_runtime,
|
|
"nodes_sd3",
|
|
SD3NodesStub,
|
|
), mock.patch.object(qwen_runtime, "nodes_model_advanced", ModelAdvancedStub):
|
|
image = qwen_runtime.run_qwen_text_to_image(
|
|
model_name="qwen_image_fp8_e4m3fn.safetensors",
|
|
prompt="a cat",
|
|
negative_prompt="low quality",
|
|
width=1024,
|
|
height=1152,
|
|
steps=33,
|
|
seed=99,
|
|
batch_size=2,
|
|
cfg=7.5,
|
|
sampler_name="heun",
|
|
scheduler="normal",
|
|
shift=2.25,
|
|
enable_turbo_mode=False,
|
|
turbo_lora_name="(auto)",
|
|
turbo_strength=1.0,
|
|
model=None,
|
|
)
|
|
|
|
self.assertEqual(image, "IMAGE::SAMPLED::99::33")
|
|
self.assertEqual(CoreQwenNodesStub.last_unet_call["unet_name"], "qwen_image_fp8_e4m3fn.safetensors")
|
|
self.assertEqual(CoreQwenNodesStub.last_text_encode_calls[0]["text"], "a cat")
|
|
self.assertEqual(CoreQwenNodesStub.last_text_encode_calls[1]["text"], "low quality")
|
|
self.assertEqual(SD3NodesStub.last_generate_call, {"width": 1024, "height": 1152, "batch_size": 2})
|
|
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["steps"], 33)
|
|
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["cfg"], 7.5)
|
|
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["sampler_name"], "heun")
|
|
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["scheduler"], "normal")
|
|
self.assertEqual(ModelAdvancedStub.last_call["shift"], 2.25)
|
|
|
|
def test_qwen_text_runtime_uses_external_model_without_internal_unet_load(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import runtime as qwen_runtime
|
|
|
|
stub = QwenFolderPathsStub()
|
|
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
|
|
qwen_runtime,
|
|
"folder_paths",
|
|
stub,
|
|
), mock.patch.object(qwen_runtime, "comfy_core_nodes", CoreQwenNodesStub), mock.patch.object(
|
|
qwen_runtime,
|
|
"nodes_sd3",
|
|
SD3NodesStub,
|
|
), mock.patch.object(qwen_runtime, "nodes_model_advanced", ModelAdvancedStub):
|
|
image = qwen_runtime.run_qwen_text_to_image(
|
|
model_name="qwen_image_fp8_e4m3fn.safetensors",
|
|
prompt="a cat",
|
|
negative_prompt="low quality",
|
|
width=1024,
|
|
height=1024,
|
|
steps=20,
|
|
seed=5,
|
|
batch_size=1,
|
|
cfg=4.5,
|
|
sampler_name="euler",
|
|
scheduler="simple",
|
|
shift=3.1,
|
|
enable_turbo_mode=False,
|
|
turbo_lora_name="(auto)",
|
|
turbo_strength=1.0,
|
|
model="MODEL::external",
|
|
)
|
|
|
|
self.assertEqual(image, "IMAGE::SAMPLED::5::20")
|
|
self.assertIsNone(CoreQwenNodesStub.last_unet_call)
|
|
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["cfg"], 4.5)
|
|
self.assertEqual(ModelAdvancedStub.last_call["model"], "MODEL::external")
|
|
|
|
def test_qwen_text_runtime_applies_turbo_lora_on_external_model(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import runtime as qwen_runtime
|
|
|
|
stub = QwenFolderPathsStub()
|
|
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
|
|
qwen_runtime,
|
|
"folder_paths",
|
|
stub,
|
|
), mock.patch.object(qwen_runtime, "comfy_core_nodes", CoreQwenNodesStub), mock.patch.object(
|
|
qwen_runtime,
|
|
"nodes_sd3",
|
|
SD3NodesStub,
|
|
), mock.patch.object(qwen_runtime, "nodes_model_advanced", ModelAdvancedStub):
|
|
qwen_runtime.run_qwen_text_to_image(
|
|
model_name="qwen_image_fp8_e4m3fn.safetensors",
|
|
prompt="a cat",
|
|
negative_prompt="",
|
|
width=1024,
|
|
height=1024,
|
|
steps=50,
|
|
seed=99,
|
|
batch_size=1,
|
|
cfg=8.0,
|
|
sampler_name="euler",
|
|
scheduler="simple",
|
|
shift=3.1,
|
|
enable_turbo_mode=True,
|
|
turbo_lora_name="(auto)",
|
|
turbo_strength=0.75,
|
|
model="MODEL::external",
|
|
)
|
|
|
|
self.assertIsNone(CoreQwenNodesStub.last_unet_call)
|
|
self.assertEqual(CoreQwenNodesStub.last_lora_calls[0]["model"], "MODEL::external")
|
|
self.assertEqual(
|
|
CoreQwenNodesStub.last_lora_calls[0]["lora_name"],
|
|
"Qwen-Image-Lightning-8steps-V1.0.safetensors",
|
|
)
|
|
self.assertEqual(CoreQwenNodesStub.last_lora_calls[0]["strength_model"], 0.75)
|
|
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["steps"], 8)
|
|
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["cfg"], 1.0)
|
|
|
|
def test_qwen_text_runtime_rejects_incompatible_manual_turbo_lora(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import runtime as qwen_runtime
|
|
|
|
stub = QwenFolderPathsStub()
|
|
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
|
|
qwen_runtime,
|
|
"folder_paths",
|
|
stub,
|
|
):
|
|
with self.assertRaisesRegex(RuntimeError, "Turbo LoRA .* is not compatible"):
|
|
qwen_runtime.run_qwen_text_to_image(
|
|
model_name="qwen_image_fp8_e4m3fn.safetensors",
|
|
prompt="a cat",
|
|
negative_prompt="",
|
|
width=1024,
|
|
height=1024,
|
|
steps=20,
|
|
seed=1,
|
|
batch_size=1,
|
|
cfg=4.0,
|
|
sampler_name="euler",
|
|
scheduler="simple",
|
|
shift=3.1,
|
|
enable_turbo_mode=True,
|
|
turbo_lora_name="Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors",
|
|
turbo_strength=1.0,
|
|
model=None,
|
|
)
|
|
|
|
def test_qwen_edit_runtime_uses_external_model_without_internal_unet_load(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import runtime as qwen_runtime
|
|
|
|
stub = QwenFolderPathsStub()
|
|
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
|
|
qwen_runtime,
|
|
"folder_paths",
|
|
stub,
|
|
), mock.patch.object(qwen_runtime, "comfy_core_nodes", CoreQwenNodesStub), mock.patch.object(
|
|
qwen_runtime,
|
|
"nodes_model_advanced",
|
|
ModelAdvancedStub,
|
|
), mock.patch.object(qwen_runtime, "nodes_qwen", QwenExtraNodesStub), mock.patch.object(
|
|
qwen_runtime,
|
|
"nodes_flux",
|
|
FluxNodesStub,
|
|
), mock.patch.object(qwen_runtime, "nodes_cfg", CFGNodesStub):
|
|
image = qwen_runtime.run_qwen_image_edit(
|
|
model_name="qwen_image_edit_2509_fp8_e4m3fn.safetensors",
|
|
image="IMAGE::input",
|
|
image2="IMAGE::ref2",
|
|
image3="IMAGE::ref3",
|
|
prompt="turn the cat blue",
|
|
negative_prompt="low quality",
|
|
steps=28,
|
|
seed=123,
|
|
cfg=6.5,
|
|
sampler_name="heun",
|
|
scheduler="normal",
|
|
shift=2.0,
|
|
cfg_norm_strength=0.8,
|
|
reference_latents_method="index",
|
|
enable_turbo_mode=False,
|
|
turbo_lora_name="(auto)",
|
|
turbo_strength=1.0,
|
|
model="MODEL::external-edit",
|
|
)
|
|
|
|
self.assertEqual(image, "IMAGE::SAMPLED::123::28")
|
|
self.assertIsNone(CoreQwenNodesStub.last_unet_call)
|
|
self.assertEqual(FluxNodesStub.last_scale_call, {"image": "IMAGE::input"})
|
|
self.assertEqual(QwenExtraNodesStub.last_calls[0]["prompt"], "turn the cat blue")
|
|
self.assertEqual(QwenExtraNodesStub.last_calls[0]["image1"], "SCALED::IMAGE::input")
|
|
self.assertEqual(QwenExtraNodesStub.last_calls[0]["image2"], "IMAGE::ref2")
|
|
self.assertEqual(QwenExtraNodesStub.last_calls[0]["image3"], "IMAGE::ref3")
|
|
self.assertEqual(QwenExtraNodesStub.last_calls[1]["prompt"], "low quality")
|
|
self.assertEqual(FluxNodesStub.last_reference_calls[0]["reference_latents_method"], "index")
|
|
self.assertEqual(CFGNodesStub.last_call["strength"], 0.8)
|
|
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["cfg"], 6.5)
|
|
self.assertEqual(ModelAdvancedStub.last_call["model"], "MODEL::external-edit")
|
|
|
|
def test_qwen_edit_runtime_supports_2511_turbo_when_matching_lora_exists(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import discovery as qwen_discovery
|
|
from lls_node_test_qwen.qwen import runtime as qwen_runtime
|
|
|
|
stub = QwenFolderPathsStub()
|
|
with mock.patch.object(qwen_discovery, "folder_paths", stub), mock.patch.object(
|
|
qwen_runtime,
|
|
"folder_paths",
|
|
stub,
|
|
), mock.patch.object(qwen_runtime, "comfy_core_nodes", CoreQwenNodesStub), mock.patch.object(
|
|
qwen_runtime,
|
|
"nodes_model_advanced",
|
|
ModelAdvancedStub,
|
|
), mock.patch.object(qwen_runtime, "nodes_qwen", QwenExtraNodesStub), mock.patch.object(
|
|
qwen_runtime,
|
|
"nodes_flux",
|
|
FluxNodesStub,
|
|
), mock.patch.object(qwen_runtime, "nodes_cfg", CFGNodesStub):
|
|
qwen_runtime.run_qwen_image_edit(
|
|
model_name="qwen_image_edit_2511_bf16.safetensors",
|
|
image="IMAGE::input",
|
|
image2=None,
|
|
image3=None,
|
|
prompt="turn the cat blue",
|
|
negative_prompt="",
|
|
steps=20,
|
|
seed=123,
|
|
cfg=4.0,
|
|
sampler_name="euler",
|
|
scheduler="simple",
|
|
shift=3.1,
|
|
cfg_norm_strength=1.0,
|
|
reference_latents_method="index_timestep_zero",
|
|
enable_turbo_mode=True,
|
|
turbo_lora_name="(auto)",
|
|
turbo_strength=0.7,
|
|
model="MODEL::external-edit",
|
|
)
|
|
|
|
self.assertIsNone(CoreQwenNodesStub.last_unet_call)
|
|
self.assertEqual(
|
|
CoreQwenNodesStub.last_lora_calls[-1]["lora_name"],
|
|
"Qwen-Image-Edit-2511-Lightning-4steps-V1.0.safetensors",
|
|
)
|
|
self.assertEqual(CoreQwenNodesStub.last_lora_calls[-1]["strength_model"], 0.7)
|
|
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["steps"], 4)
|
|
self.assertEqual(CoreQwenNodesStub.last_ksampler_call["cfg"], 1.0)
|
|
|
|
def test_qwen_text_node_executes_runtime(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import nodes as qwen_nodes
|
|
|
|
node = qwen_nodes.LLSQwenTextToImage()
|
|
|
|
with mock.patch.object(qwen_nodes.runtime, "run_qwen_text_to_image", return_value="IMAGE::node") as runtime_mock:
|
|
result = node.generate(
|
|
model_name="qwen_image_fp8_e4m3fn.safetensors",
|
|
prompt="a cat",
|
|
width=1024,
|
|
height=1024,
|
|
steps=20,
|
|
seed=1,
|
|
batch_size=1,
|
|
negative_prompt="",
|
|
cfg=4.0,
|
|
sampler_name="euler",
|
|
scheduler="simple",
|
|
shift=3.1,
|
|
enable_turbo_mode=False,
|
|
turbo_lora_name="(auto)",
|
|
turbo_strength=1.0,
|
|
model="MODEL::external",
|
|
)
|
|
|
|
self.assertEqual(result, ("IMAGE::node",))
|
|
self.assertEqual(runtime_mock.call_args.kwargs["model"], "MODEL::external")
|
|
|
|
def test_qwen_edit_node_executes_runtime(self):
|
|
load_plugin_package()
|
|
from lls_node_test_qwen.qwen import nodes as qwen_nodes
|
|
|
|
node = qwen_nodes.LLSQwenImageEdit()
|
|
|
|
with mock.patch.object(qwen_nodes.runtime, "run_qwen_image_edit", return_value="IMAGE::node-edit") as runtime_mock:
|
|
result = node.generate(
|
|
model_name="qwen_image_edit_2511_bf16.safetensors",
|
|
image="IMAGE::input",
|
|
prompt="edit the cat",
|
|
steps=20,
|
|
seed=1,
|
|
image2=None,
|
|
image3=None,
|
|
negative_prompt="",
|
|
cfg=4.0,
|
|
sampler_name="euler",
|
|
scheduler="simple",
|
|
shift=3.1,
|
|
cfg_norm_strength=1.0,
|
|
reference_latents_method="index_timestep_zero",
|
|
enable_turbo_mode=False,
|
|
turbo_lora_name="(auto)",
|
|
turbo_strength=1.0,
|
|
model="MODEL::external",
|
|
)
|
|
|
|
self.assertEqual(result, ("IMAGE::node-edit",))
|
|
self.assertEqual(runtime_mock.call_args.kwargs["model"], "MODEL::external")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|