Files
Mister-Link-ComfyUI-Qwen-Te…/nodes_appmatch.py

255 lines
9.8 KiB
Python

"""Qwen TeleStyle Apply node that reproduces /home/linkray/TeleStyleV2/app.py's `infer()`
function exactly - same prompt-enhancer generation calls, same resize math, same
prompt-merge logic, same pipe() call. It only knows how to *generate*; all model
loading/downloading lives in nodes_loader.py's Qwen TeleStyle Loader, which hands this node
a ready TELESTYLE_MODELS pipe.
"""
import numpy as np
import torch
from PIL import Image
def _process_vision_info(messages):
# imported lazily so a missing qwen_vl_utils only breaks the prompt-enhancer feature,
# not registration of this node (and every other node in the same package).
try:
from qwen_vl_utils import process_vision_info
except ImportError as exc:
raise ImportError(
"qwen_vl_utils is required for infer_content_prompt/infer_style_prompt. "
"Install it in ComfyUI's own venv: pip install qwen-vl-utils"
) from exc
return process_vision_info(messages)
MAX_SEED = np.iinfo(np.int32).max
CONTENT_TEXT_INPUT = (
"describe main objects (fewer than 3) with separated words, each word is separated by comma, "
"the total number of words is strictly fewer than 3"
)
STYLE_TEXT_INPUT = "describe only the artistic style, material, stroke in 5 words, not objects."
def _comfy_image_to_numpy_uint8(image):
# comfy IMAGE tensors are [B, H, W, C] float in [0, 1]; app.py's gr.Image(type="numpy")
# hands infer() an (H, W, C) uint8 array - reproduce that exact array before doing
# anything else, so every downstream line of app.py's infer() can be copied verbatim.
array = image[0].clamp(0.0, 1.0).mul(255.0).round().to(torch.uint8).cpu().numpy()
return array
def _tensor_from_pil(image: Image.Image) -> torch.Tensor:
array = np.array(image.convert("RGB")).astype(np.float32) / 255.0
return torch.from_numpy(array).unsqueeze(0)
def _resolve_runtime(models):
if isinstance(models, dict):
pipe = models.get("pipe")
if pipe is None:
raise ValueError("TELESTYLE_MODELS handle is missing 'pipe'.")
return models, pipe
return {"mode": "legacy", "pipe": models}, models
class QwenTeleStyleApply:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"models": ("TELESTYLE_MODELS",),
"prompt": (
"STRING",
{
"default": "Style Transfer the style of Figure 2 to Figure 1, and keep the content and characteristics of Figure 1.",
"multiline": True,
},
),
"seed": ("INT", {"default": 123, "min": 0, "max": MAX_SEED}),
"cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}),
"steps": ("INT", {"default": 4, "min": 1, "max": 50}),
"resolution": ("INT", {"default": 1024, "min": 256, "max": 2048, "step": 8}),
"infer_content_prompt": ("BOOLEAN", {"default": False}),
"infer_style_prompt": ("BOOLEAN", {"default": False}),
},
"optional": {
"content_ref": ("IMAGE",),
"style_ref": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "STRING")
RETURN_NAMES = ("image", "content_prompt", "style_prompt", "prompt")
FUNCTION = "infer"
CATEGORY = "image/TeleStyleV2"
def infer(
self,
models,
prompt,
seed,
cfg,
steps,
resolution,
infer_content_prompt,
infer_style_prompt,
content_ref=None,
style_ref=None,
):
runtime, pipe = _resolve_runtime(models)
if content_ref is None and style_ref is None:
raise ValueError("Connect at least one of content_ref or style_ref.")
if (infer_content_prompt or infer_style_prompt) and not pipe.has_lm_head:
raise RuntimeError(
"The text encoder this pipe was built from has no lm_head weight, so it "
"can't run infer_content_prompt/infer_style_prompt. Write the prompt "
"manually instead, or disable these toggles."
)
device = runtime.get("resolved_device", getattr(pipe, "resolved_device", "cpu"))
# from here down this is app.py's infer(), unmodified line-for-line except the
# gradio-numpy inputs are swapped for comfy IMAGE tensors converted just above the
# point app.py itself first calls Image.fromarray(), and `minedge`/`true_guidance_scale`/
# `num_inference_steps`/`randomize_seed` are renamed/dropped to match comfy conventions
# (seed randomization is comfy's own "control_after_generate" widget, not this node's job).
checkbox = []
if infer_content_prompt:
checkbox.append("infer with content prompt")
if infer_style_prompt:
checkbox.append("infer with style prompt")
content_prompt = ""
style_prompt = ""
if content_ref is not None:
content_ref = Image.fromarray(_comfy_image_to_numpy_uint8(content_ref))
content_messages = [
{
"role": "user",
"content": [
{"type": "image", "image": content_ref},
{"type": "text", "text": CONTENT_TEXT_INPUT},
],
}
]
content_text = pipe.processor.apply_chat_template(
content_messages, tokenize=False, add_generation_prompt=True
)
image_inputs, video_inputs = _process_vision_info(content_messages)
inputs = pipe.processor(
text=[content_text],
images=image_inputs,
videos=video_inputs,
padding=True,
return_tensors="pt",
)
inputs = inputs.to(device)
generated_ids = pipe.text_encoder.generate(**inputs, max_new_tokens=1024)
generated_ids_trimmed = [
out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
]
content_prompt = pipe.processor.batch_decode(
generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
)[0]
if style_ref is not None:
style_ref = Image.fromarray(_comfy_image_to_numpy_uint8(style_ref))
style_messages = [
{
"role": "user",
"content": [
{"type": "image", "image": style_ref},
{"type": "text", "text": STYLE_TEXT_INPUT},
],
}
]
style_text = pipe.processor.apply_chat_template(
style_messages, tokenize=False, add_generation_prompt=True
)
image_inputs, video_inputs = _process_vision_info(style_messages)
inputs = pipe.processor(
text=[style_text],
images=image_inputs,
videos=video_inputs,
padding=True,
return_tensors="pt",
)
inputs = inputs.to(device)
generated_ids = pipe.text_encoder.generate(**inputs, max_new_tokens=1024)
generated_ids_trimmed = [
out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
]
style_prompt = pipe.processor.batch_decode(
generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
)[0]
sw, sh, w, h = 0, 0, 0, 0
if content_ref:
w, h = content_ref.size
if w > h:
r = w / h
h = resolution
w = int(h * r) - int(h * r) % 16
else:
r = h / w
w = resolution
h = int(w * r) - int(w * r) % 16
if style_ref:
sw, sh = style_ref.size
if sw > sh:
r = sw / sh
sh = resolution
sw = int(sh * r) - int(sh * r) % 16
else:
r = sh / sw
sw = resolution
sh = int(sw * r) - int(sw * r) % 16
if content_ref and style_ref:
images = [content_ref.resize((w, h)), style_ref.resize((sw, sh))]
elif content_ref:
images = [content_ref.resize((w, h))]
elif style_ref:
images = [style_ref.resize((sw, sh))]
if "infer with content prompt" in checkbox and content_prompt not in prompt:
prompt = ",".join([prompt, content_prompt])
if "infer with style prompt" in checkbox and style_prompt not in prompt:
prompt = ",".join([prompt, style_prompt])
if "infer with content prompt" not in checkbox and content_prompt in prompt:
prompt = prompt.replace(content_prompt.strip(","), "")
if "infer with style prompt" not in checkbox and style_prompt in prompt:
prompt = prompt.replace(style_prompt.strip(), "")
prompt = prompt.strip(",")
pipe_inputs = {
"image": images,
"prompt": prompt,
"generator": torch.manual_seed(seed),
"true_cfg_scale": cfg,
"negative_prompt": " ",
"num_inference_steps": steps,
"guidance_scale": cfg,
"num_images_per_prompt": 1,
"width": w or sw,
"height": h or sh,
}
with torch.inference_mode():
result = pipe(**pipe_inputs)
output_image = result.images[0]
return (_tensor_from_pil(output_image), content_prompt, style_prompt, prompt)
NODE_CLASS_MAPPINGS = {
"Qwen TeleStyle Apply": QwenTeleStyleApply,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Qwen TeleStyle Apply": "Qwen TeleStyle Apply",
}