commit cd28dac9e25ead3ed1bda19cd78e578869008545 Author: hujuying <105225728+hujuying@users.noreply.github.com> Date: Tue Sep 2 05:18:48 2025 +0800 Add files via upload diff --git a/README.md b/README.md new file mode 100644 index 0000000..43947c7 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# ComfyUI Qwen-Image 节点 + +本仓库提供了 [魔搭社区开放API](https://modelscope.cn/) 的 Qwen-Image 模型在 ComfyUI 中的节点实现。 + +## 特性 + +- 支持通过 [魔搭社区](https://modelscope.cn/) 的 API 调用 Qwen-Image 模型 +- 支持图像尺寸、采样步数、引导系数等参数设置 +- 支持随机种子与固定种子 +- 支持负向提示词 +- 支持 API Token 保存(首次填写后自动保存到 config.json) +- 支持图像编辑功能 (Qwen-Image-Edit 模型) + +## 安装 + +1. 克隆本仓库到 ComfyUI 的 `custom_nodes` 目录下: + +``` +cd ComfyUI/custom_nodes +git clone https://github.com/111496583yzy/comfyui-modelscope-qwen-image.git comfyui-qwen-image +``` + +2. 重启 ComfyUI 服务 + +## 使用方法 + +### 1. 获取魔搭API Token + +访问 [魔搭社区](https://modelscope.cn/) 并登录,在个人资料页获取 API Token。 + +### 2. Qwen-Image 生图节点 + +在 ComfyUI 编辑器中添加 `Qwen-Image 生图节点`,设置以下参数: + +- **prompt**: 文本提示词 +- **api_token**: 魔搭API Token (首次填写后会自动保存) +- **model**: 模型名称(默认为 "Qwen/Qwen-Image") +- **negative_prompt**: 负向提示词(可选) +- **width/height**: 图像宽高(默认512x512) +- **seed**: 随机种子(-1表示使用随机种子) +- **steps**: 采样步数(默认30) +- **guidance**: 引导系数(默认7.5) + +### 3. Qwen-Image 图像编辑节点 + +在 ComfyUI 编辑器中添加 `Qwen-Image 图像编辑节点`,设置以下参数: + +- **image**: 要编辑的原始图像 +- **prompt**: 描述要进行的编辑的文本提示词 +- **api_token**: 魔搭API Token (首次填写后会自动保存) +- **model**: 模型名称(默认为 "Qwen/Qwen-Image-Edit") +- **negative_prompt**: 负向提示词(可选) +- **width/height**: 图像宽高(默认512x512,范围64-1664) +- **steps**: 采样步数(范围1-100,默认30) +- **guidance**: 引导系数(范围1.5-20.0,默认3.5) +- **seed**: 随机种子(-1表示使用随机种子,0-2147483647为固定种子) + +## 工作流示例 + +### 文本生图 + +1. 添加 `Qwen-Image 生图节点` 并设置提示词和其他参数 +2. 连接输出到 `Preview Image` 节点 + +### 图像编辑 + +1. 准备一张原始图像(使用 `Load Image` 或其他方式) +2. 添加 `Qwen-Image 图像编辑节点` +3. 将原始图像连接到编辑节点的 `image` 输入 +4. 设置编辑提示词(如"把狗变成猫") +5. 连接输出到 `Preview Image` 节点 + +## 注意事项 + +- API 调用需要网络连接 +- 高峰时期可能需要等待较长时间 +- 请遵守魔搭社区的使用政策 +- 如遇到错误代码429,表示请求过多,需要等待一段时间后重试 + +## License + +MIT diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..8dde8ea --- /dev/null +++ b/__init__.py @@ -0,0 +1,11 @@ +from .modelscope_image_node import NODE_CLASS_MAPPINGS as IMAGE_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as IMAGE_DISPLAY_MAPPINGS +from .modelscope_vision_node import NODE_CLASS_MAPPINGS as VISION_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as VISION_DISPLAY_MAPPINGS +from .modelscope_text_node import NODE_CLASS_MAPPINGS as TEXT_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as TEXT_DISPLAY_MAPPINGS + +NODE_CLASS_MAPPINGS = {**IMAGE_MAPPINGS, **VISION_MAPPINGS, **TEXT_MAPPINGS} +NODE_DISPLAY_NAME_MAPPINGS = {**IMAGE_DISPLAY_MAPPINGS, **VISION_DISPLAY_MAPPINGS, **TEXT_DISPLAY_MAPPINGS} + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] + +WEB_DIRECTORY = "./js" +__version__ = "1.0.0" \ No newline at end of file diff --git a/install_dependencies.py b/install_dependencies.py new file mode 100644 index 0000000..4ab977f --- /dev/null +++ b/install_dependencies.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import subprocess +import sys +import os + +def install_package(package): + """安装Python包""" + try: + subprocess.check_call([sys.executable, "-m", "pip", "install", package]) + return True + except subprocess.CalledProcessError as e: + print(f"安装 {package} 失败: {e}") + return False + +def check_package(package_name): + """检查包是否已安装""" + try: + __import__(package_name) + return True + except ImportError: + return False + +def main(): + print("=" * 60) + print("Qwen-Image ComfyUI 插件依赖安装工具") + print("=" * 60) + + # 检查核心依赖 + core_deps = { + 'requests': 'requests', + 'PIL': 'pillow', + 'torch': 'torch', + 'numpy': 'numpy' + } + + print("\n🔍 检查核心依赖...") + missing_core = [] + for import_name, package_name in core_deps.items(): + if check_package(import_name): + print(f"✅ {package_name} 已安装") + else: + print(f"❌ {package_name} 未安装") + missing_core.append(package_name) + + # 检查图生文功能依赖 + print("\n🔍 检查图生文功能依赖...") + vision_deps = { + 'openai': 'openai', + 'httpx': 'httpx[socks]', + 'socksio': 'socksio' + } + + missing_vision = [] + for import_name, package_name in vision_deps.items(): + if check_package(import_name): + print(f"✅ {package_name} 已安装") + else: + print(f"❌ {package_name} 未安装") + missing_vision.append(package_name) + + # 安装缺失的依赖 + all_missing = missing_core + missing_vision + + if not all_missing: + print("\n🎉 所有依赖都已安装!") + return + + print(f"\n📦 需要安装 {len(all_missing)} 个依赖包:") + for pkg in all_missing: + print(f" - {pkg}") + + response = input("\n是否现在安装这些依赖?(y/n): ").lower().strip() + + if response in ['y', 'yes', '是']: + print("\n🚀 开始安装依赖...") + success_count = 0 + + for package in all_missing: + print(f"\n📦 安装 {package}...") + if install_package(package): + print(f"✅ {package} 安装成功") + success_count += 1 + else: + print(f"❌ {package} 安装失败") + + print(f"\n📊 安装结果: {success_count}/{len(all_missing)} 个包安装成功") + + if success_count == len(all_missing): + print("🎉 所有依赖安装完成!请重启ComfyUI。") + else: + print("⚠️ 部分依赖安装失败,请手动安装或检查网络连接。") + print("\n手动安装命令:") + for package in all_missing: + print(f" pip install {package}") + else: + print("\n取消安装。") + print("\n手动安装命令:") + for package in all_missing: + print(f" pip install {package}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/modelscope_config.json b/modelscope_config.json new file mode 100644 index 0000000..18950ac --- /dev/null +++ b/modelscope_config.json @@ -0,0 +1,37 @@ +{ + "default_model": "Qwen/Qwen-Image", + "timeout": 720, + "image_download_timeout": 30, + "default_prompt": "A beautiful landscape", + "default_negative_prompt": "", + "default_width": 512, + "default_height": 512, + "default_seed": -1, + "default_steps": 30, + "default_guidance": 7.5, + "default_text_model": "Qwen/Qwen3-Coder-480B-A35B-Instruct", + "default_system_prompt": "You are a helpful assistant.", + "default_user_prompt": "你好", + "api_token": "", + "image_models": [ + "Qwen/Qwen-Image", + "MusePublic/489_ckpt_FLUX_1", + "black-forest-labs/FLUX.1-Krea-dev", + "MAILAND/majicflus_v1" + ], + "image_edit_models": [ + "Qwen/Qwen-Image-Edit", + "MusePublic/FLUX.1-Kontext-Dev" + ], + "text_models": [ + "Qwen/Qwen3-Coder-480B-A35B-Instruct", + "Qwen/Qwen3-Coder-480B-A35B-Chat", + "Qwen/Qwen3-Coder-140B-A35B-Instruct", + "Qwen/Qwen3-Coder-70B-A35B-Instruct" + ], + "vision_models": [ + "stepfun-ai/step3", + "Qwen/Qwen-VL-Chat", + "Qwen/Qwen-VL" + ] +} \ No newline at end of file diff --git a/modelscope_image_node.py b/modelscope_image_node.py new file mode 100644 index 0000000..34e030a --- /dev/null +++ b/modelscope_image_node.py @@ -0,0 +1,522 @@ +import requests +import json +import time +import torch +import numpy as np +from PIL import Image +from io import BytesIO +import os +import folder_paths +import base64 +import tempfile + +def load_config(): + config_path = os.path.join(os.path.dirname(__file__), 'modelscope_config.json') + try: + with open(config_path, 'r', encoding='utf-8') as f: + return json.load(f) + except: + return { + "default_model": "Qwen/Qwen-Image", + "timeout": 720, + "image_download_timeout": 30, + "default_prompt": "A beautiful landscape" + } + +def save_config(config: dict) -> bool: + config_path = os.path.join(os.path.dirname(__file__), 'modelscope_config.json') + try: + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(config, f, ensure_ascii=False, indent=2) + return True + except Exception as e: + print(f"保存配置失败: {e}") + return False + +def save_api_token(token): + token_path = os.path.join(os.path.dirname(__file__), '.qwen_token') + try: + with open(token_path, 'w', encoding='utf-8') as f: + f.write(token) + except Exception as e: + print(f"保存token失败(.qwen_token): {e}") + try: + cfg = load_config() + cfg["api_token"] = token + if save_config(cfg): + return True + return False + except Exception as e: + print(f"保存token失败(config.json): {e}") + return False + +def load_api_token(): + token_path = os.path.join(os.path.dirname(__file__), '.qwen_token') + try: + cfg = load_config() + token_from_cfg = cfg.get("api_token", "").strip() + if token_from_cfg: + return token_from_cfg + except Exception as e: + print(f"读取config.json中的token失败: {e}") + try: + if os.path.exists(token_path): + with open(token_path, 'r', encoding='utf-8') as f: + token = f.read().strip() + return token if token else "" + return "" + except Exception as e: + print(f"加载token失败: {e}") + return "" + +def tensor_to_base64_url(image_tensor): + try: + if len(image_tensor.shape) == 4: + image_tensor = image_tensor.squeeze(0) + + if image_tensor.max() <= 1.0: + image_np = (image_tensor.cpu().numpy() * 255).astype(np.uint8) + else: + image_np = image_tensor.cpu().numpy().astype(np.uint8) + + pil_image = Image.fromarray(image_np) + + buffer = BytesIO() + pil_image.save(buffer, format='JPEG', quality=85) + img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + + return f"data:image/jpeg;base64,{img_base64}" + + except Exception as e: + print(f"图像转换失败: {e}") + raise Exception(f"图像格式转换失败: {str(e)}") + +class ModelScopeImageNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + config = load_config() + saved_token = load_api_token() + return { + "required": { + "prompt": ("STRING", { + "multiline": True, + "default": config.get("default_prompt", "A beautiful landscape") + }), + "api_token": ("STRING", { + "default": saved_token, + "placeholder": "请输入您的魔搭API Token" + }), + }, + "optional": { +"model": (config.get("image_models", ["Qwen/Qwen-Image"]), { + "default": config.get("default_model", "Qwen/Qwen-Image") +}), + "negative_prompt": ("STRING", { + "multiline": True, + "default": config.get("default_negative_prompt", "") + }), + "width": ("INT", { + "default": config.get("default_width", 512), + "min": 64, + "max": 2048, + "step": 64 + }), + "height": ("INT", { + "default": config.get("default_height", 512), + "min": 64, + "max": 2048, + "step": 64 + }), + "seed": ("INT", { + "default": config.get("default_seed", -1), + "min": -1, + "max": 2147483647 + }), + "steps": ("INT", { + "default": config.get("default_steps", 30), + "min": 1, + "max": 100 + }), + "guidance": ("FLOAT", { + "default": config.get("default_guidance", 7.5), + "min": 1.5, + "max": 20.0, + "step": 0.1 + }), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "generate_image" + CATEGORY = "ModelScopeAPI" + + def generate_image(self, prompt, api_token, model="Qwen/Qwen-Image", negative_prompt="", width=512, height=512, seed=-1, steps=30, guidance=7.5): + config = load_config() + if not api_token or api_token.strip() == "": + raise Exception("请输入有效的API Token") + saved_token = load_api_token() + if api_token != saved_token: + if save_api_token(api_token): + print("✅ API Token已自动保存") + else: + print("⚠️ API Token保存失败,但不影响当前使用") + try: + url = 'https://api-inference.modelscope.cn/v1/images/generations' + payload = { + 'model': model, + 'prompt': prompt, + 'size': f"{width}x{height}", + 'steps': steps, + 'guidance': guidance + } + if negative_prompt.strip(): + payload['negative_prompt'] = negative_prompt + print(f"🚫 负向提示词: {negative_prompt}") + if seed != -1: + payload['seed'] = seed + print(f"🎯 使用指定种子: {seed}") + else: + import random + random_seed = random.randint(0, 2147483647) + payload['seed'] = random_seed + print(f"🎲 使用随机种子: {random_seed}") + print(f"📐 图像尺寸: {width}x{height}") + print(f"🔧 采样步数: {steps}") + print(f"🎨 引导系数: {guidance}") + headers = { + 'Authorization': f'Bearer {api_token}', + 'Content-Type': 'application/json', + 'X-ModelScope-Async-Mode': 'true' + } + submission_response = requests.post( + url, + data=json.dumps(payload, ensure_ascii=False).encode('utf-8'), + headers=headers, + timeout=config.get("timeout", 60) + ) + if submission_response.status_code == 400: + print("⚠️ 提交失败,尝试使用最小参数重试...") + minimal_payload = { + 'model': model, + 'prompt': prompt + } + submission_response = requests.post( + url, + data=json.dumps(minimal_payload, ensure_ascii=False).encode('utf-8'), + headers=headers, + timeout=config.get("timeout", 60) + ) + if submission_response.status_code != 200: + raise Exception(f"API请求失败: {submission_response.status_code}, {submission_response.text}") + submission_json = submission_response.json() + image_url = None + if 'task_id' in submission_json: + task_id = submission_json['task_id'] + print(f"🕒 已提交任务,任务ID: {task_id},开始轮询...") + poll_start = time.time() + max_wait_seconds = max(60, config.get('timeout', 720)) + while True: + task_resp = requests.get( + f"https://api-inference.modelscope.cn/v1/tasks/{task_id}", + headers={ + 'Authorization': f'Bearer {api_token}', + 'X-ModelScope-Task-Type': 'image_generation' + }, + timeout=config.get("image_download_timeout", 120) + ) + if task_resp.status_code != 200: + raise Exception(f"任务查询失败: {task_resp.status_code}, {task_resp.text}") + task_data = task_resp.json() + status = task_data.get('task_status') + if status == 'SUCCEED': + output_images = task_data.get('output_images') or [] + if not output_images: + raise Exception("任务成功但未返回图片URL") + image_url = output_images[0] + print("✅ 任务完成,开始下载图片...") + break + if status == 'FAILED': + raise Exception(f"任务失败: {task_data}") + if time.time() - poll_start > max_wait_seconds: + raise Exception("任务轮询超时,请稍后重试或降低并发") + time.sleep(5) + elif 'images' in submission_json and len(submission_json['images']) > 0: + image_url = submission_json['images'][0]['url'] + print(f"⬇️ 下载生成的图片...") + else: + raise Exception(f"未识别的API返回格式: {submission_json}") + img_response = requests.get(image_url, timeout=config.get("image_download_timeout", 30)) + if img_response.status_code != 200: + raise Exception(f"图片下载失败: {img_response.status_code}") + pil_image = Image.open(BytesIO(img_response.content)) + if pil_image.mode != 'RGB': + pil_image = pil_image.convert('RGB') + image_np = np.array(pil_image).astype(np.float32) / 255.0 + image_tensor = torch.from_numpy(image_np)[None,] + print(f"🎉 图片处理完成!") + return (image_tensor,) + except Exception as e: + print(f"Qwen-Image API调用失败: {str(e)}") + error_image = Image.new('RGB', (width, height), color='red') + error_np = np.array(error_image).astype(np.float32) / 255.0 + error_tensor = torch.from_numpy(error_np)[None,] + return (error_tensor,) + +class ModelScopeImageEditNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + config = load_config() + saved_token = load_api_token() + return { + "required": { + "image": ("IMAGE",), + "prompt": ("STRING", { + "multiline": True, + "default": "修改图片中的内容" + }), + "api_token": ("STRING", { + "default": saved_token, + "placeholder": "请输入您的魔搭API Token" + }), + }, + "optional": { +"model": (config.get("image_edit_models", ["Qwen/Qwen-Image-Edit"]), { + "default": "Qwen/Qwen-Image-Edit" +}), + "negative_prompt": ("STRING", { + "multiline": True, + "default": "" + }), + "width": ("INT", { + "default": 512, + "min": 64, + "max": 1664, + "step": 8 + }), + "height": ("INT", { + "default": 512, + "min": 64, + "max": 1664, + "step": 8 + }), + "steps": ("INT", { + "default": 30, + "min": 1, + "max": 100, + "step": 1 + }), + "guidance": ("FLOAT", { + "default": 3.5, + "min": 1.5, + "max": 20.0, + "step": 0.1 + }), + "seed": ("INT", { + "default": -1, + "min": -1, + "max": 2147483647 + }), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("edited_image",) + FUNCTION = "edit_image" + CATEGORY = "ModelScopeAPI" + + def edit_image(self, image, prompt, api_token, model="Qwen/Qwen-Image-Edit", negative_prompt="", + width=512, height=512, steps=30, guidance=3.5, seed=-1): + config = load_config() + if not api_token or api_token.strip() == "": + raise Exception("请输入有效的API Token") + saved_token = load_api_token() + if api_token != saved_token: + if save_api_token(api_token): + print("✅ API Token已自动保存") + else: + print("⚠️ API Token保存失败,但不影响当前使用") + + try: + # 将图像转换为临时文件并上传获取URL + temp_img_path = None + image_url = None + try: + # 保存图像到临时文件 + temp_img_path = os.path.join(tempfile.gettempdir(), f"qwen_edit_temp_{int(time.time())}.jpg") + if len(image.shape) == 4: + img = image[0] + else: + img = image + + i = 255. * img.cpu().numpy() + img_pil = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + img_pil.save(temp_img_path) + print(f"✅ 图像已保存到临时文件: {temp_img_path}") + + # 上传图像到kefan.cn获取URL + upload_url = 'https://ai.kefan.cn/api/upload/local' + with open(temp_img_path, 'rb') as img_file: + files = {'file': img_file} + upload_response = requests.post( + upload_url, + files=files, + timeout=30 + ) + if upload_response.status_code == 200: + upload_data = upload_response.json() + # 修复这里的判断逻辑,kefan.cn返回code=200表示成功 + if upload_data.get('success') == True and 'data' in upload_data: + image_url = upload_data['data'] + print(f"✅ 图像已上传成功,获取URL: {image_url}") + else: + print(f"⚠️ 图像上传返回错误: {upload_response.text}") + else: + print(f"⚠️ 图像上传失败: {upload_response.status_code}, {upload_response.text}") + except Exception as e: + print(f"⚠️ 图像上传异常: {str(e)}") + + # 如果上传失败,回退到base64 + if not image_url: + print("⚠️ 图像URL获取失败,回退到使用base64") + image_data = tensor_to_base64_url(image) + payload = { + 'model': model, + 'prompt': prompt, + 'image': image_data + } + else: + payload = { + 'model': model, + 'prompt': prompt, + 'image_url': image_url + } + + if negative_prompt.strip(): + payload['negative_prompt'] = negative_prompt + print(f"🚫 负向提示词: {negative_prompt}") + + # 添加新参数 + if width != 512 or height != 512: + size = f"{width}x{height}" + payload['size'] = size + print(f"📏 图像尺寸: {size}") + + if steps != 30: + payload['steps'] = steps + print(f"🔄 采样步数: {steps}") + + if guidance != 3.5: + payload['guidance'] = guidance + print(f"🧭 引导系数: {guidance}") + + if seed != -1: + payload['seed'] = seed + print(f"🎲 随机种子: {seed}") + + headers = { + 'Authorization': f'Bearer {api_token}', + 'Content-Type': 'application/json', + 'X-ModelScope-Async-Mode': 'true' + } + + print(f"🖼️ 开始编辑图片...") + print(f"✏️ 编辑提示: {prompt}") + + url = 'https://api-inference.modelscope.cn/v1/images/generations' + submission_response = requests.post( + url, + data=json.dumps(payload, ensure_ascii=False).encode('utf-8'), + headers=headers, + timeout=config.get("timeout", 60) + ) + + if submission_response.status_code != 200: + raise Exception(f"API请求失败: {submission_response.status_code}, {submission_response.text}") + + submission_json = submission_response.json() + result_image_url = None + + if 'task_id' in submission_json: + task_id = submission_json['task_id'] + print(f"🕒 已提交任务,任务ID: {task_id},开始轮询...") + poll_start = time.time() + max_wait_seconds = max(60, config.get('timeout', 720)) + + while True: + task_resp = requests.get( + f"https://api-inference.modelscope.cn/v1/tasks/{task_id}", + headers={ + 'Authorization': f'Bearer {api_token}', + 'X-ModelScope-Task-Type': 'image_generation' + }, + timeout=config.get("image_download_timeout", 120) + ) + + if task_resp.status_code != 200: + raise Exception(f"任务查询失败: {task_resp.status_code}, {task_resp.text}") + + task_data = task_resp.json() + status = task_data.get('task_status') + + if status == 'SUCCEED': + output_images = task_data.get('output_images') or [] + if not output_images: + raise Exception("任务成功但未返回图片URL") + result_image_url = output_images[0] + print("✅ 任务完成,开始下载编辑后的图片...") + break + + if status == 'FAILED': + error_message = task_data.get('errors', {}).get('message', '未知错误') + error_code = task_data.get('errors', {}).get('code', '未知错误码') + raise Exception(f"任务失败: 错误码 {error_code}, 错误信息: {error_message}") + + if time.time() - poll_start > max_wait_seconds: + raise Exception("任务轮询超时,请稍后重试或降低并发") + + time.sleep(5) + else: + raise Exception(f"未识别的API返回格式: {submission_json}") + + img_response = requests.get(result_image_url, timeout=config.get("image_download_timeout", 30)) + if img_response.status_code != 200: + raise Exception(f"图片下载失败: {img_response.status_code}") + + pil_image = Image.open(BytesIO(img_response.content)) + if pil_image.mode != 'RGB': + pil_image = pil_image.convert('RGB') + + image_np = np.array(pil_image).astype(np.float32) / 255.0 + image_tensor = torch.from_numpy(image_np)[None,] + + # 清理临时文件 + if temp_img_path and os.path.exists(temp_img_path): + try: + os.remove(temp_img_path) + except: + pass + + print(f"🎉 图片编辑完成!") + return (image_tensor,) + + except Exception as e: + print(f"Qwen-Image-Edit API调用失败: {str(e)}") + # 返回原图像作为错误回退 + return (image.unsqueeze(0),) + +NODE_CLASS_MAPPINGS = { + "ModelScopeImageNode": ModelScopeImageNode, + "ModelScopeImageEditNode": ModelScopeImageEditNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "ModelScopeImageNode": "ModelScope-Image 生图节点", + "ModelScopeImageEditNode": "ModelScope-Image 图像编辑节点" +} diff --git a/modelscope_text_node.py b/modelscope_text_node.py new file mode 100644 index 0000000..1797510 --- /dev/null +++ b/modelscope_text_node.py @@ -0,0 +1,230 @@ +import requests +import json +import time +import os + +try: + from openai import OpenAI + OPENAI_AVAILABLE = True +except ImportError: + print("⚠️ 警告: 未安装openai库,文本生成功能将不可用") + print("请运行: pip install openai") + OPENAI_AVAILABLE = False + OpenAI = None + +def load_config(): + config_path = os.path.join(os.path.dirname(__file__), 'modelscope_config.json') + try: + with open(config_path, 'r', encoding='utf-8') as f: + return json.load(f) + except: + return { + "default_model": "Qwen/Qwen-Image", + "timeout": 720, + "image_download_timeout": 30, + "default_prompt": "A beautiful landscape" + } + +def load_api_token(): + token_path = os.path.join(os.path.dirname(__file__), '.qwen_token') + try: + cfg = load_config() + token_from_cfg = cfg.get("api_token", "").strip() + if token_from_cfg: + return token_from_cfg + except Exception as e: + print(f"读取config.json中的token失败: {e}") + try: + if os.path.exists(token_path): + with open(token_path, 'r', encoding='utf-8') as f: + token = f.read().strip() + return token if token else "" + return "" + except Exception as e: + print(f"加载token失败: {e}") + return "" + +def save_api_token(token): + token_path = os.path.join(os.path.dirname(__file__), '.qwen_token') + try: + with open(token_path, 'w', encoding='utf-8') as f: + f.write(token) + cfg = load_config() + cfg["api_token"] = token + config_path = os.path.join(os.path.dirname(__file__), 'config.json') + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(cfg, f, ensure_ascii=False, indent=2) + return True + except Exception as e: + print(f"保存token失败: {e}") + return False + +class ModelScopeTextNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + if not OPENAI_AVAILABLE: + return { + "required": { + "error_message": ("STRING", { + "default": "请先安装openai库: pip install openai", + "multiline": True + }), + } + } + config = load_config() + saved_token = load_api_token() + return { + "required": { + "user_prompt": ("STRING", { + "multiline": True, + "default": config.get("default_user_prompt", "你好") + }), + "api_token": ("STRING", { + "default": saved_token, + "placeholder": "请输入您的魔搭API Token" + }), + }, + "optional": { + "system_prompt": ("STRING", { + "multiline": True, + "default": config.get("default_system_prompt", "You are a helpful assistant.") + }), + "model": ("STRING", { + "default": config.get("default_text_model", "Qwen/Qwen3-Coder-480B-A35B-Instruct") + }), + "max_tokens": ("INT", { + "default": 2000, + "min": 100, + "max": 8000 + }), + "temperature": ("FLOAT", { + "default": 0.7, + "min": 0.1, + "max": 2.0, + "step": 0.1 + }), + "stream": ("BOOLEAN", { + "default": True + }), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("response",) + FUNCTION = "generate_text" + CATEGORY = "ModelScopeAPI" + + def generate_text(self, user_prompt="", api_token="", system_prompt="You are a helpful assistant.", model="Qwen/Qwen3-Coder-480B-A35B-Instruct", max_tokens=2000, temperature=0.7, stream=True, error_message=""): + if not OPENAI_AVAILABLE: + return ("请先安装openai库: pip install openai",) + + config = load_config() + + if not api_token or api_token.strip() == "": + raise Exception("请输入有效的API Token") + + saved_token = load_api_token() + if api_token != saved_token: + if save_api_token(api_token): + print("✅ API Token已自动保存") + else: + print("⚠️ API Token保存失败,但不影响当前使用") + + try: + print(f"💬 开始文本生成...") + print(f"🤖 模型: {model}") + print(f"📝 用户提示: {user_prompt[:50]}...") + print(f"⚙️ 系统提示: {system_prompt[:50]}...") + print(f"🌡️ 温度: {temperature}") + print(f"📊 最大tokens: {max_tokens}") + print(f"⚡ 流式输出: {stream}") + + client = OpenAI( + base_url='https://api-inference.modelscope.cn/v1', + api_key=api_token + ) + + messages = [ + { + 'role': 'system', + 'content': system_prompt + }, + { + 'role': 'user', + 'content': user_prompt + } + ] + + print(f"🚀 发送API请求...") + + response = client.chat.completions.create( + model=model, + messages=messages, + max_tokens=max_tokens, + temperature=temperature, + stream=stream + ) + + if stream: + print("📡 接收流式响应...") + full_response = "" + for chunk in response: + if chunk.choices[0].delta.content: + content = chunk.choices[0].delta.content + full_response += content + print(content, end='', flush=True) + + print(f"\n✅ 流式生成完成!") + print(f"📄 总长度: {len(full_response)} 字符") + return (full_response,) + else: + result = response.choices[0].message.content + print(f"✅ 文本生成完成!") + print(f"📄 结果长度: {len(result)} 字符") + print(f"📝 结果预览: {result[:100]}...") + return (result,) + + except Exception as e: + error_msg = f"文本生成失败: {str(e)}" + print(f"❌ {error_msg}") + return (error_msg,) + +if OPENAI_AVAILABLE: + NODE_CLASS_MAPPINGS = { + "ModelScopeTextNode": ModelScopeTextNode + } + + NODE_DISPLAY_NAME_MAPPINGS = { + "ModelScopeTextNode": "ModelScope-Text 文本生成节点" + } +else: + class OpenAINotInstalledNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "install_command": ("STRING", { + "default": "pip install openai", + "multiline": False + }), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("message",) + FUNCTION = "show_install_message" + CATEGORY = "ModelScopeAPI" + + def show_install_message(self, install_command): + return ("请先安装openai库才能使用文本生成功能: " + install_command,) + + NODE_CLASS_MAPPINGS = { + "ModelScopeTextNode": OpenAINotInstalledNode + } + + NODE_DISPLAY_NAME_MAPPINGS = { + "ModelScopeTextNode": "ModelScope-Text 文本生成节点 (需要安装openai)" + } \ No newline at end of file diff --git a/modelscope_vision_node.py b/modelscope_vision_node.py new file mode 100644 index 0000000..95c1f30 --- /dev/null +++ b/modelscope_vision_node.py @@ -0,0 +1,240 @@ +import requests +import json +import time +import torch +import numpy as np +from PIL import Image +from io import BytesIO +import os +import base64 +import tempfile + +try: + from openai import OpenAI + OPENAI_AVAILABLE = True +except ImportError: + print("⚠️ 警告: 未安装openai库,图生文功能将不可用") + print("请运行: pip install openai") + OPENAI_AVAILABLE = False + OpenAI = None + +def load_config(): + config_path = os.path.join(os.path.dirname(__file__), 'modelscope_config.json') + try: + with open(config_path, 'r', encoding='utf-8') as f: + return json.load(f) + except: + return { + "default_model": "Qwen/Qwen-Image", + "timeout": 720, + "image_download_timeout": 30, + "default_prompt": "A beautiful landscape" + } + +def load_api_token(): + token_path = os.path.join(os.path.dirname(__file__), '.qwen_token') + try: + cfg = load_config() + token_from_cfg = cfg.get("api_token", "").strip() + if token_from_cfg: + return token_from_cfg + except Exception as e: + print(f"读取config.json中的token失败: {e}") + try: + if os.path.exists(token_path): + with open(token_path, 'r', encoding='utf-8') as f: + token = f.read().strip() + return token if token else "" + return "" + except Exception as e: + print(f"加载token失败: {e}") + return "" + +def save_api_token(token): + token_path = os.path.join(os.path.dirname(__file__), '.qwen_token') + try: + with open(token_path, 'w', encoding='utf-8') as f: + f.write(token) + cfg = load_config() + cfg["api_token"] = token + config_path = os.path.join(os.path.dirname(__file__), 'config.json') + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(cfg, f, ensure_ascii=False, indent=2) + return True + except Exception as e: + print(f"保存token失败: {e}") + return False + +def tensor_to_base64_url(image_tensor): + try: + if len(image_tensor.shape) == 4: + image_tensor = image_tensor.squeeze(0) + + if image_tensor.max() <= 1.0: + image_np = (image_tensor.cpu().numpy() * 255).astype(np.uint8) + else: + image_np = image_tensor.cpu().numpy().astype(np.uint8) + + pil_image = Image.fromarray(image_np) + + buffer = BytesIO() + pil_image.save(buffer, format='JPEG', quality=85) + img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + + return f"data:image/jpeg;base64,{img_base64}" + + except Exception as e: + print(f"图像转换失败: {e}") + raise Exception(f"图像格式转换失败: {str(e)}") + +class ModelScopeVisionNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + if not OPENAI_AVAILABLE: + return { + "required": { + "error_message": ("STRING", { + "default": "请先安装openai库: pip install openai", + "multiline": True + }), + } + } + config = load_config() + saved_token = load_api_token() + return { + "required": { + "image": ("IMAGE",), + "prompt": ("STRING", { + "multiline": True, + "default": config.get("default_prompt", "描述这幅图") + }), + "api_token": ("STRING", { + "default": saved_token, + "placeholder": "请输入您的魔搭API Token" + }), + }, + "optional": { + "model": ("STRING", { + "default": config.get("default_vision_model", "stepfun-ai/step3") + }), + "max_tokens": ("INT", { + "default": 1000, + "min": 100, + "max": 4000 + }), + "temperature": ("FLOAT", { + "default": 0.7, + "min": 0.1, + "max": 2.0, + "step": 0.1 + }), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("description",) + FUNCTION = "analyze_image" + CATEGORY = "ModelScopeAPI" + + def analyze_image(self, image=None, prompt="", api_token="", model="stepfun-ai/step3", max_tokens=1000, temperature=0.7, error_message=""): + if not OPENAI_AVAILABLE: + return ("请先安装openai库: pip install openai",) + + config = load_config() + + if not api_token or api_token.strip() == "": + raise Exception("请输入有效的API Token") + + saved_token = load_api_token() + if api_token != saved_token: + if save_api_token(api_token): + print("✅ API Token已自动保存") + else: + print("⚠️ API Token保存失败,但不影响当前使用") + + try: + print(f"🔍 开始分析图像...") + print(f"📝 提示词: {prompt}") + print(f"🤖 模型: {model}") + + image_url = tensor_to_base64_url(image) + print(f"🖼️ 图像已转换为base64格式") + + client = OpenAI( + base_url='https://api-inference.modelscope.cn/v1', + api_key=api_token + ) + + messages = [{ + 'role': 'user', + 'content': [{ + 'type': 'text', + 'text': prompt, + }, { + 'type': 'image_url', + 'image_url': { + 'url': image_url, + }, + }], + }] + + print(f"🚀 发送API请求...") + + response = client.chat.completions.create( + model=model, + messages=messages, + max_tokens=max_tokens, + temperature=temperature, + stream=False + ) + + description = response.choices[0].message.content + print(f"✅ 分析完成!") + print(f"📄 结果: {description[:100]}...") + + return (description,) + + except Exception as e: + error_msg = f"图像分析失败: {str(e)}" + print(f"❌ {error_msg}") + return (error_msg,) + +if OPENAI_AVAILABLE: + NODE_CLASS_MAPPINGS = { + "ModelScopeVisionNode": ModelScopeVisionNode + } + + NODE_DISPLAY_NAME_MAPPINGS = { + "ModelScopeVisionNode": "ModelScope-Vision 图生文节点" + } +else: + class OpenAINotInstalledNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "install_command": ("STRING", { + "default": "pip install openai", + "multiline": False + }), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("message",) + FUNCTION = "show_install_message" + CATEGORY = "ModelScopeAPI" + + def show_install_message(self, install_command): + return ("请先安装openai库才能使用图生文功能: " + install_command,) + + NODE_CLASS_MAPPINGS = { + "ModelScopeVisionNode": OpenAINotInstalledNode + } + + NODE_DISPLAY_NAME_MAPPINGS = { + "ModelScopeVisionNode": "ModelScope-Vision 图生文节点 (需要安装openai)" + } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..724b23c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +# Qwen-Image ComfyUI Plugin Requirements + +# Core dependencies (usually already available in ComfyUI) +requests>=2.25.0 +pillow>=8.0.0 +torch>=1.9.0 +numpy>=1.20.0 + +# Additional dependencies for vision functionality +openai>=1.0.0 + +# Network and proxy support +httpx[socks]>=0.24.0 +socksio>=1.0.0 + +# Optional dependencies for enhanced functionality +# pydantic-settings # For advanced configuration management \ No newline at end of file diff --git a/troubleshoot.py b/troubleshoot.py new file mode 100644 index 0000000..39c9e95 --- /dev/null +++ b/troubleshoot.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Qwen-Image ComfyUI 插件故障排除工具 +自动诊断和解决常见问题 +""" + +import os +import sys +import subprocess +import json + +def print_header(title): + """打印标题""" + print("\n" + "=" * 60) + print(f" {title}") + print("=" * 60) + +def print_section(title): + """打印章节标题""" + print(f"\n🔍 {title}") + print("-" * 40) + +def run_command(command, description): + """运行命令并返回结果""" + print(f"📋 {description}") + print(f"💻 命令: {command}") + + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30) + if result.returncode == 0: + print("✅ 成功") + if result.stdout.strip(): + print(f"📄 输出: {result.stdout.strip()}") + return True, result.stdout + else: + print("❌ 失败") + if result.stderr.strip(): + print(f"🚨 错误: {result.stderr.strip()}") + return False, result.stderr + except subprocess.TimeoutExpired: + print("⏰ 超时") + return False, "命令执行超时" + except Exception as e: + print(f"💥 异常: {str(e)}") + return False, str(e) + +def check_python_environment(): + """检查Python环境""" + print_section("Python环境检查") + + # Python版本 + run_command("python --version", "检查Python版本") + + # pip版本 + run_command("pip --version", "检查pip版本") + + # 已安装的包 + print("\n📦 检查关键包安装状态:") + packages = ['requests', 'pillow', 'torch', 'numpy', 'openai', 'httpx', 'socksio'] + + for package in packages: + try: + __import__(package) + print(f"✅ {package}") + except ImportError: + print(f"❌ {package} (未安装)") + +def check_files(): + """检查文件完整性""" + print_section("文件完整性检查") + + required_files = [ + '__init__.py', + 'qwen_image_node.py', + 'qwen_vision_node.py', + 'config.json', + 'requirements.txt' + ] + + for file in required_files: + if os.path.exists(file): + size = os.path.getsize(file) + print(f"✅ {file} ({size} bytes)") + else: + print(f"❌ {file} (缺失)") + +def check_config(): + """检查配置文件""" + print_section("配置文件检查") + + try: + with open('config.json', 'r', encoding='utf-8') as f: + config = json.load(f) + + print("✅ config.json 格式正确") + + # 检查关键配置项 + key_configs = [ + 'default_model', + 'default_vision_model', + 'timeout', + 'default_prompt' + ] + + for key in key_configs: + if key in config: + print(f"✅ {key}: {config[key]}") + else: + print(f"❌ {key} (缺失)") + + except Exception as e: + print(f"❌ config.json 读取失败: {e}") + +def check_network(): + """检查网络连接""" + print_section("网络连接检查") + + # 检查基本网络连接 + run_command("ping -c 3 8.8.8.8", "检查基本网络连接") + + # 检查API服务器连接 + try: + import requests + response = requests.get('https://api-inference.modelscope.cn', timeout=10) + print(f"✅ API服务器连接正常 (状态码: {response.status_code})") + except Exception as e: + print(f"❌ API服务器连接失败: {e}") + + # 检查代理设置 + proxy_vars = ['HTTP_PROXY', 'HTTPS_PROXY', 'SOCKS_PROXY'] + print("\n🌐 代理环境变量:") + for var in proxy_vars: + value = os.environ.get(var) + if value: + print(f"✅ {var}: {value}") + else: + print(f"⚪ {var}: 未设置") + +def check_token(): + """检查API Token""" + print_section("API Token检查") + + token_sources = ['.qwen_token', 'config.json'] + token_found = False + + for source in token_sources: + if source == '.qwen_token' and os.path.exists(source): + try: + with open(source, 'r', encoding='utf-8') as f: + token = f.read().strip() + if token: + print(f"✅ 在 {source} 中找到token (长度: {len(token)})") + token_found = True + else: + print(f"⚪ {source} 存在但为空") + except Exception as e: + print(f"❌ 读取 {source} 失败: {e}") + + elif source == 'config.json': + try: + with open(source, 'r', encoding='utf-8') as f: + config = json.load(f) + token = config.get('api_token', '').strip() + if token: + print(f"✅ 在 {source} 中找到token (长度: {len(token)})") + token_found = True + else: + print(f"⚪ {source} 中token为空") + except Exception as e: + print(f"❌ 读取 {source} 失败: {e}") + + if not token_found: + print("❌ 未找到有效的API token") + +def run_diagnostic_tests(): + """运行诊断测试""" + print_section("诊断测试") + + tests = [ + ("python verify_installation.py", "运行安装验证"), + ("python test_vision_with_proxy.py", "运行代理测试"), + ] + + for command, description in tests: + if os.path.exists(command.split()[1]): + success, output = run_command(command, description) + if not success: + print(f"⚠️ {description} 失败,请查看详细输出") + else: + print(f"⚪ {command.split()[1]} 不存在,跳过测试") + +def suggest_solutions(): + """建议解决方案""" + print_section("建议解决方案") + + solutions = [ + "🔧 安装缺失依赖: python install_dependencies.py", + "🔍 验证安装: python verify_installation.py", + "🌐 测试代理: python test_vision_with_proxy.py", + "📖 查看快速指南: cat QUICKSTART.md", + "🔗 查看代理指南: cat PROXY_GUIDE.md", + "🖼️ 查看图生文指南: cat VISION_GUIDE.md", + "🔄 重启ComfyUI以加载更新", + "🧹 清理Python缓存: rm -rf __pycache__", + ] + + for solution in solutions: + print(solution) + +def main(): + print_header("Qwen-Image ComfyUI 插件故障排除工具") + + print("🚀 开始全面诊断...") + + # 运行所有检查 + check_python_environment() + check_files() + check_config() + check_network() + check_token() + run_diagnostic_tests() + suggest_solutions() + + print_header("诊断完成") + + print("\n💡 根据上述诊断结果:") + print("1. 如果发现缺失依赖,运行: python install_dependencies.py") + print("2. 如果网络有问题,查看: PROXY_GUIDE.md") + print("3. 如果token有问题,重新输入API token") + print("4. 如果文件缺失,重新下载插件") + print("5. 完成修复后,重启ComfyUI") + + print("\n📞 如果问题仍然存在:") + print("- 查看ComfyUI控制台的完整错误日志") + print("- 尝试在不同网络环境下测试") + print("- 确认ComfyUI版本兼容性") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/verify_installation.py b/verify_installation.py new file mode 100644 index 0000000..8a672c5 --- /dev/null +++ b/verify_installation.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys + +def check_files(): + required_files = [ + '__init__.py', + 'qwen_image_node.py', + 'qwen_vision_node.py', + 'qwen_text_node.py', + 'config.json', + 'README.md', + 'requirements.txt' + ] + + print("📁 检查文件完整性...") + missing_files = [] + + for file in required_files: + if os.path.exists(file): + print(f"✅ {file}") + else: + print(f"❌ {file} (缺失)") + missing_files.append(file) + + return len(missing_files) == 0 + +def check_dependencies(): + print("\n📦 检查依赖包...") + + deps = { + 'requests': '网络请求', + 'PIL': '图像处理', + 'torch': '深度学习框架', + 'numpy': '数值计算', + 'openai': '文本生成和图生文功能', + 'httpx': '高级HTTP客户端', + 'socksio': 'SOCKS代理支持' + } + + missing_deps = [] + + for dep, desc in deps.items(): + try: + __import__(dep) + print(f"✅ {dep} ({desc})") + except ImportError: + print(f"❌ {dep} ({desc}) - 未安装") + missing_deps.append(dep) + + return len(missing_deps) == 0, missing_deps + +def check_proxy_support(): + print("\n🌐 检查代理支持...") + + try: + import httpx + try: + import socksio + print("✅ SOCKS代理支持已安装") + return True + except ImportError: + print("⚠️ SOCKS代理支持未安装,如果使用代理可能会出错") + print(" 建议运行: pip install httpx[socks] socksio") + return False + except ImportError: + print("❌ httpx未安装") + return False + +def check_node_loading(): + print("\n🔧 检查节点加载...") + + try: + from qwen_image_node import QwenImageNode + node = QwenImageNode() + input_types = node.INPUT_TYPES() + print("✅ 文生图节点加载成功") + + from qwen_vision_node import QwenVisionNode, OPENAI_AVAILABLE + if OPENAI_AVAILABLE: + vision_node = QwenVisionNode() + vision_input_types = vision_node.INPUT_TYPES() + print("✅ 图生文节点加载成功") + else: + print("⚠️ 图生文节点加载成功,但OpenAI库不可用") + + from qwen_text_node import QwenTextNode + if OPENAI_AVAILABLE: + text_node = QwenTextNode() + text_input_types = text_node.INPUT_TYPES() + print("✅ 文本生成节点加载成功") + else: + print("⚠️ 文本生成节点加载成功,但OpenAI库不可用") + + return True + except Exception as e: + print(f"❌ 节点加载失败: {e}") + return False + +def check_config(): + print("\n⚙️ 检查配置文件...") + + try: + import json + with open('config.json', 'r', encoding='utf-8') as f: + config = json.load(f) + + required_keys = [ + 'default_model', + 'default_vision_model', + 'default_text_model', + 'timeout', + 'default_prompt' + ] + + missing_keys = [] + for key in required_keys: + if key in config: + print(f"✅ {key}: {config[key]}") + else: + print(f"❌ {key} (缺失)") + missing_keys.append(key) + + return len(missing_keys) == 0 + except Exception as e: + print(f"❌ 配置文件读取失败: {e}") + return False + +def main(): + print("=" * 60) + print("Qwen-Image ComfyUI 插件安装验证") + print("=" * 60) + + checks = [ + ("文件完整性", check_files), + ("依赖包", lambda: check_dependencies()[0]), + ("代理支持", check_proxy_support), + ("配置文件", check_config), + ("节点加载", check_node_loading), + ] + + passed = 0 + total = len(checks) + + for check_name, check_func in checks: + print(f"\n🔍 {check_name}检查...") + try: + if check_func(): + passed += 1 + print(f"✅ {check_name}检查通过") + else: + print(f"❌ {check_name}检查失败") + except Exception as e: + print(f"❌ {check_name}检查出错: {e}") + + print("-" * 40) + + deps_ok, missing_deps = check_dependencies() + if not deps_ok: + print(f"\n📦 缺失的依赖包: {', '.join(missing_deps)}") + print("运行以下命令安装:") + print("python install_dependencies.py") + print("或手动安装:") + for dep in missing_deps: + if dep == 'httpx': + print(f" pip install httpx[socks]") + else: + print(f" pip install {dep}") + + print(f"\n📊 验证结果: {passed}/{total} 项检查通过") + + if passed >= total - 1: + print("\n🎉 插件安装验证成功!") + print("\n📋 下一步操作:") + print("1. 将整个插件文件夹复制到 ComfyUI/custom_nodes/ 目录") + print("2. 重启ComfyUI") + print("3. 在节点列表中查找 'QwenImage' 分类") + print("4. 准备好您的魔搭API Token") + + current_path = os.getcwd() + if 'custom_nodes' in current_path: + print("\n✅ 检测到您已在ComfyUI的custom_nodes目录中") + print(" 请直接重启ComfyUI即可使用") + else: + print(f"\n📁 当前路径: {current_path}") + print(" 请确保将插件复制到正确的ComfyUI目录") + + if not check_proxy_support(): + print("\n⚠️ 代理支持提醒:") + print(" 如果您使用代理上网,建议安装代理支持包:") + print(" pip install httpx[socks] socksio") + else: + print("\n⚠️ 插件安装验证失败,请修复上述问题后重试") + +if __name__ == "__main__": + main() \ No newline at end of file