Replace the prior release tree with the current plugin, frontend, tests, and documentation. Document retired node IDs and the public Gitea update source.
175 lines
5.4 KiB
Python
175 lines
5.4 KiB
Python
"""
|
|
Flux2 图像编辑节点
|
|
通过 api.o1key.cn 调用 Flux2 + SeedVR2 远程服务进行图像编辑和超分辨率
|
|
|
|
功能:
|
|
- 接收主图和参考图
|
|
- 上传到远程服务器执行图像编辑
|
|
- 轮询等待 SeedVR2 超分辨率结果
|
|
- 返回最终放大后的图像
|
|
"""
|
|
|
|
import time
|
|
from io import BytesIO
|
|
from typing import Tuple
|
|
|
|
import torch
|
|
from PIL import Image
|
|
|
|
from ..utils.image_utils import tensor_to_pil, pil_to_tensor
|
|
from ..utils.config import get_runtime_config_signature
|
|
from ..clients.flux_edit_client import FluxEditClient
|
|
|
|
|
|
class FluxImageEdit:
|
|
"""
|
|
Flux2 图像编辑节点
|
|
|
|
通过远程 API 将主图与参考图结合,按照提示词进行图像编辑,
|
|
并经 SeedVR2 超分辨率放大后返回最终结果。
|
|
"""
|
|
|
|
SIZES = ["2K", "4K"]
|
|
|
|
def __init__(self):
|
|
self.client = None
|
|
self._client_config_signature = None
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"主图": ("IMAGE",),
|
|
"参考图": ("IMAGE",),
|
|
"提示词": ("STRING", {
|
|
"default": "Replace the woman's underwear in Figure 1 with the strapless bra in Figure 2",
|
|
"multiline": True,
|
|
}),
|
|
"分辨率": (cls.SIZES, {
|
|
"default": "4K",
|
|
}),
|
|
"轮询间隔": ("INT", {
|
|
"default": 15,
|
|
"min": 5,
|
|
"max": 60,
|
|
"step": 5,
|
|
}),
|
|
"seed": ("INT", {
|
|
"default": 0,
|
|
"min": 0,
|
|
"max": 0xffffffffffffffff,
|
|
}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("输出图像",)
|
|
FUNCTION = "generate"
|
|
CATEGORY = "image/edit"
|
|
|
|
def _image_to_jpeg_bytes(self, image: Image.Image, quality: int = 92) -> bytes:
|
|
"""将 PIL Image 转为 JPEG 二进制"""
|
|
if image.mode in ("RGBA", "P", "LA"):
|
|
image = image.convert("RGB")
|
|
buf = BytesIO()
|
|
image.save(buf, format="JPEG", quality=quality)
|
|
return buf.getvalue()
|
|
|
|
def generate(
|
|
self,
|
|
主图: torch.Tensor,
|
|
参考图: torch.Tensor,
|
|
提示词: str,
|
|
分辨率: str,
|
|
轮询间隔: int,
|
|
seed: int,
|
|
) -> Tuple[torch.Tensor]:
|
|
"""
|
|
执行图像编辑
|
|
|
|
Args:
|
|
主图: 要编辑的原始图像 (ComfyUI tensor, [B, H, W, C])
|
|
参考图: 参考/风格图像 (ComfyUI tensor, [B, H, W, C])
|
|
提示词: 编辑指令
|
|
分辨率: 超分辨率目标 ("2K" 或 "4K",会自动映射为 2048/4096)
|
|
轮询间隔: 轮询秒数
|
|
seed: 随机种子
|
|
|
|
Returns:
|
|
输出图像 tensor (IMAGE,)
|
|
"""
|
|
start_time = time.time()
|
|
|
|
try:
|
|
# 初始化客户端
|
|
config_signature = get_runtime_config_signature()
|
|
if self.client is None or config_signature != self._client_config_signature:
|
|
self.client = FluxEditClient()
|
|
self._client_config_signature = config_signature
|
|
|
|
# Tensor → PIL(取第一张)
|
|
main_pils = tensor_to_pil(主图)
|
|
ref_pils = tensor_to_pil(参考图)
|
|
|
|
if not main_pils:
|
|
raise ValueError("主图不能为空")
|
|
if not ref_pils:
|
|
raise ValueError("参考图不能为空")
|
|
|
|
main_img = main_pils[0]
|
|
ref_img = ref_pils[0]
|
|
|
|
# PIL → JPEG bytes
|
|
main_bytes = self._image_to_jpeg_bytes(main_img)
|
|
ref_bytes = self._image_to_jpeg_bytes(ref_img)
|
|
|
|
print(f"Flux Edit: 开始处理 | 主图 {main_img.size} | 参考图 {ref_img.size} | 分辨率 {分辨率} | seed {seed}")
|
|
|
|
# 进度回调
|
|
def progress_callback(status_str: str):
|
|
print(f"Flux Edit: {status_str}")
|
|
|
|
# 提交任务并等待结果
|
|
result_bytes = self.client.submit_and_wait(
|
|
image_bytes=main_bytes,
|
|
mask_bytes=ref_bytes,
|
|
prompt=提示词,
|
|
size=分辨率,
|
|
poll_interval=轮询间隔,
|
|
progress_callback=progress_callback,
|
|
)
|
|
|
|
# 解码结果
|
|
result_img = Image.open(BytesIO(result_bytes))
|
|
if result_img.mode != "RGB":
|
|
result_img = result_img.convert("RGB")
|
|
|
|
print(f"Flux Edit: 结果图像尺寸 {result_img.size}")
|
|
|
|
# 转为 tensor
|
|
output_tensor = pil_to_tensor([result_img])
|
|
|
|
# 打印耗时
|
|
elapsed = time.time() - start_time
|
|
if elapsed < 60:
|
|
time_str = f"{elapsed:.1f}s"
|
|
else:
|
|
minutes = int(elapsed // 60)
|
|
seconds = elapsed % 60
|
|
time_str = f"{minutes}m {seconds:.0f}s"
|
|
print(f"Flux Edit: 完成!总耗时 {time_str}")
|
|
|
|
return (output_tensor,)
|
|
|
|
except ValueError as e:
|
|
if str(e) == "未授权!":
|
|
print("请联系作者授权后方可使用!")
|
|
raise ValueError("未授权!") from None
|
|
print(f"Flux Edit: ❌ {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
print(f"Flux Edit: ❌ {error_msg}")
|
|
raise RuntimeError(error_msg) from None
|