Add files via upload
This commit is contained in:
+1
-183
@@ -1,183 +1 @@
|
||||
{
|
||||
"id": "6d37ea84-d0ab-4123-8c8c-965b62a18230",
|
||||
"revision": 0,
|
||||
"last_node_id": 20,
|
||||
"last_link_id": 33,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 17,
|
||||
"type": "easy showAnything",
|
||||
"pos": [
|
||||
2732.3588101299074,
|
||||
397.60778722933657
|
||||
],
|
||||
"size": [
|
||||
482.4299999999994,
|
||||
553.9499999999997
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"label": "输入任何",
|
||||
"name": "anything",
|
||||
"shape": 7,
|
||||
"type": "*",
|
||||
"link": 33
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "output",
|
||||
"type": "*",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-easy-use",
|
||||
"ver": "1.3.3",
|
||||
"Node name for S&R": "easy showAnything"
|
||||
},
|
||||
"widgets_values": [
|
||||
"一位身着精致淡紫色缎面旗袍的年轻东方女性,旗袍上饰有金色龙凤刺绣图案,高开叉设计优雅地露出修长美腿,搭配红色蕾丝袖口与红色长筒袜。她发髻高挽,佩戴华丽金色流苏发簪,妆容精致,眼神温柔而自信。她正轻盈地舞动,裙摆随动作飘逸,姿态曼妙动人。背景为典雅中式室内,后方悬挂一幅水墨山水画,暖色调灯光营造出温馨柔和的氛围。整体画面构图为中景特写,聚焦人物上半身及动态,视角平视,强调人物神韵与服饰细节。风格融合古典东方美学与现代时尚感,色彩以淡紫、金、红为主调,光影细腻柔美,呈现出高级写真质感。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
1869.8370214843746,
|
||||
350.8748266601562
|
||||
],
|
||||
"size": [
|
||||
290.8019714355469,
|
||||
570.7747192382812
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"label": "图像",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
32
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "遮罩",
|
||||
"name": "MASK",
|
||||
"type": "MASK"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.49",
|
||||
"Node name for S&R": "LoadImage",
|
||||
"ue_properties": {
|
||||
"version": "7.0.1",
|
||||
"widget_ue_connectable": {
|
||||
"image": true,
|
||||
"upload": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"ebe510f1a069ba899eec0f82fefd3746d26f2f95ec2378a10ec1cd56cbc53642.png",
|
||||
"image"
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "ModelScopeImageCaptionNode",
|
||||
"pos": [
|
||||
2266.803982765229,
|
||||
414.0277931000381
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
268
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 32
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "description",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
33
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelScopeImageCaptionNode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"***已保存1个Token***",
|
||||
"帮我拆解这张图片的提示词,要求从主体内容、场景设定、风格参考、色条色彩、构图视角、细节补充等这些角度来用文字描述图片,并汇总生成一个能让我用 ai绘画工具的文生图提示词,字数在 800 字以内,只要求输出汇总后的最终提示词,不需要无用信息。如:提示词标题,总字数等信息。结果使用中文输出",
|
||||
"我要求在分析好信息后增加图中人物在跳舞的内容",
|
||||
"Qwen/Qwen3-VL-8B-Instruct",
|
||||
1000,
|
||||
0.7
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
32,
|
||||
5,
|
||||
0,
|
||||
20,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
33,
|
||||
20,
|
||||
0,
|
||||
17,
|
||||
0,
|
||||
"*"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 2,
|
||||
"title": "魔搭-图片编辑",
|
||||
"bounding": [
|
||||
1663.396484375,
|
||||
122.82080078125,
|
||||
1826.526123046875,
|
||||
960.4658203125
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.1000000000000003,
|
||||
"offset": [
|
||||
-1628.9576538857762,
|
||||
-206.80605453648508
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.30.1"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
{"id":"6d37ea84-d0ab-4123-8c8c-965b62a18230","revision":0,"last_node_id":21,"last_link_id":35,"nodes":[{"id":17,"type":"easy showAnything","pos":[2732.3588101299074,397.60778722933657],"size":[482.4299999999994,553.9499999999997],"flags":{},"order":2,"mode":0,"inputs":[{"label":"输入任何","localized_name":"输入任何","name":"anything","shape":7,"type":"*","link":35}],"outputs":[{"localized_name":"输出","name":"output","type":"*","links":null}],"properties":{"cnr_id":"comfyui-easy-use","ver":"1.3.3","Node name for S&R":"easy showAnything"},"widgets_values":["一位身着飘逸长裙的女性在梦幻般的云海之上翩翩起舞,裙摆如花瓣般随风扬起,姿态轻盈灵动。她周围环绕着半透明的光晕与漂浮的星尘,脚下是流动的彩色云雾,营造出超现实的仙境氛围。背景为渐变的暮色天空,从深紫到柔和的粉橙,点缀着稀疏的星光,远处隐约可见朦胧的山峦轮廓。整体画面采用电影级CG渲染风格,细腻写实与奇幻元素融合,光影柔和,色彩饱和度适中,带有轻微的柔焦效果。构图采用中景仰视视角,人物居于画面中央偏上,强调其优雅与自由感。细节上,女性发丝随风飘动,裙褶层次丰富,云层纹理细腻,星尘粒子动态自然,地面云雾呈现流动感。画面充满动感与诗意,传达出自由、浪漫与梦幻的主题。\n\n提示词:一位身着飘逸长裙的女性在梦幻云海之上翩翩起舞,裙摆如花瓣飞扬,周身环绕半透明光晕与星尘,脚下是流动彩云,背景为渐变暮色天空(深紫至粉橙),点缀稀疏星光与朦胧远山。电影级CG渲染风格,细腻写实融合奇幻元素,光影柔和,色彩饱和适中,带柔焦效果。中景仰视构图,人物居画面中央偏上,突出优雅自由感。细节:发丝飘动,裙褶层次丰富,云层纹理细腻,星尘粒子动态自然,云雾流动。画面充满动感诗意,传达自由浪漫梦幻主题。"]},{"id":21,"type":"ModelScopeImageCaptionNode","pos":[2266.803982765229,414.0277931000381],"size":[400,268],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"image","name":"image","shape":7,"type":"IMAGE","link":34},{"localized_name":"api_tokens","name":"api_tokens","type":"STRING","widget":{"name":"api_tokens"},"link":null},{"localized_name":"prompt1","name":"prompt1","shape":7,"type":"STRING","widget":{"name":"prompt1"},"link":null},{"localized_name":"prompt2","name":"prompt2","shape":7,"type":"STRING","widget":{"name":"prompt2"},"link":null},{"localized_name":"model","name":"model","shape":7,"type":"COMBO","widget":{"name":"model"},"link":null},{"localized_name":"max_tokens","name":"max_tokens","shape":7,"type":"INT","widget":{"name":"max_tokens"},"link":null},{"localized_name":"temperature","name":"temperature","shape":7,"type":"FLOAT","widget":{"name":"temperature"},"link":null}],"outputs":[{"localized_name":"description","name":"description","type":"STRING","links":[35]}],"properties":{"Node name for S&R":"ModelScopeImageCaptionNode"},"widgets_values":["***已保存1个Token***","帮我拆解这张图片的提示词,要求从主体内容、场景设定、风格参考、色条色彩、构图视角、细节补充等这些角度来用文字描述图片,并汇总生成一个能让我用 ai绘画工具的文生图提示词,字数在 800 字以内,只要求输出汇总后的最终提示词,不需要无用信息。如:提示词标题,总字数等信息。结果使用中文输出","我要求在分析好信息后增加图中人物在跳舞的内容","Qwen/Qwen3-VL-8B-Instruct",1000,0.7]},{"id":5,"type":"LoadImage","pos":[1869.8370214843746,350.8748266601562],"size":[290.8019714355469,570.7747192382812],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"label":"图像","localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[34]},{"label":"遮罩","localized_name":"遮罩","name":"MASK","type":"MASK"}],"properties":{"cnr_id":"comfy-core","ver":"0.3.49","Node name for S&R":"LoadImage","ue_properties":{"version":"7.0.1","widget_ue_connectable":{"image":true,"upload":true}}},"widgets_values":["ebe510f1a069ba899eec0f82fefd3746d26f2f95ec2378a10ec1cd56cbc53642.png","image"],"color":"#232","bgcolor":"#353"}],"links":[[34,5,0,21,0,"IMAGE"],[35,21,0,17,0,"*"]],"groups":[{"id":2,"title":"魔搭-图片编辑","bounding":[1663.396484375,122.82080078125,1826.526123046875,960.4658203125],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":1.1000000000000005,"offset":[-1589.768328765161,-104.21134851168563]},"frontendVersion":"1.30.1"},"version":0.4}
|
||||
File diff suppressed because one or more lines are too long
+41
-2
@@ -19,7 +19,9 @@
|
||||
"MusePublic/489_ckpt_FLUX_1",
|
||||
"MusePublic/flux-high-res",
|
||||
"black-forest-labs/FLUX.1-Krea-dev",
|
||||
"MAILAND/majicflus_v1"
|
||||
"MAILAND/majicflus_v1",
|
||||
"MoYouuu/MYHuman-QWen",
|
||||
"Tongyi-MAI/Z-Image-Turbo"
|
||||
],
|
||||
"image_edit_models": [
|
||||
"Qwen/Qwen-Image-Edit",
|
||||
@@ -47,5 +49,42 @@
|
||||
"Qwen/Qwen2-VL-7B-Instruct",
|
||||
"Qwen/QVQ-72B-Preview",
|
||||
"PaddlePaddle/ERNIE-4.5-VL-28B-A3B-PT"
|
||||
],
|
||||
"default_lora_model": "",
|
||||
"default_lora_weight": 0.8,
|
||||
"lora_presets": [
|
||||
{
|
||||
"name": "无LoRA",
|
||||
"model_id": "",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"name": "Qwen-小红书风格美人",
|
||||
"model_id": "qiyuanai/TikTok_Xiaohongshu_career_line_beauty_v1",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"name": "Qwen-小红书甜妹",
|
||||
"model_id": "wilderkid/Rednote_SweetGirl",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"name": "Qwen-小红薯-风格插画-排版",
|
||||
"model_id": "YJ777YJ/xiaohshu",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"name": "Qwen-真实人像小红书风格",
|
||||
"model_id": "chy1125677992/qwxhs",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"name": "Qwen-抖音小红书手持手机自拍美女",
|
||||
"model_id": "qiyuanai/shouchizipai_v1",
|
||||
"weight": 0.8
|
||||
}
|
||||
],
|
||||
"api_tokens": [
|
||||
"ms"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ from io import BytesIO
|
||||
import os
|
||||
import base64
|
||||
import re
|
||||
from .modelscope_image_node import load_config, load_api_tokens, save_api_tokens, tensor_to_base64_url
|
||||
from .modelscope_image_node import load_config, save_config, tensor_to_base64_url
|
||||
|
||||
# 检查openai库是否可用
|
||||
try:
|
||||
@@ -17,6 +17,26 @@ try:
|
||||
except ImportError:
|
||||
OPENAI_AVAILABLE = False
|
||||
|
||||
# 仅与modelscope_config.json交互的API Token管理函数
|
||||
def load_api_tokens():
|
||||
try:
|
||||
cfg = load_config()
|
||||
tokens_from_cfg = cfg.get("api_tokens", [])
|
||||
if tokens_from_cfg and isinstance(tokens_from_cfg, list):
|
||||
return [token.strip() for token in tokens_from_cfg if token.strip()]
|
||||
except Exception as e:
|
||||
print(f"读取config中的tokens失败: {e}")
|
||||
return []
|
||||
|
||||
def save_api_tokens(tokens):
|
||||
try:
|
||||
cfg = load_config()
|
||||
cfg["api_tokens"] = tokens
|
||||
return save_config(cfg)
|
||||
except Exception as e:
|
||||
print(f"保存tokens到config失败: {e}")
|
||||
return False
|
||||
|
||||
class ModelScopeImageCaptionNode:
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -40,7 +60,6 @@ class ModelScopeImageCaptionNode:
|
||||
]
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"api_tokens": ("STRING", {
|
||||
"default": f"***已保存{len(saved_tokens)}个Token***" if saved_tokens else "",
|
||||
"placeholder": "请输入API Token(支持多个,用逗号/换行分隔)",
|
||||
@@ -48,6 +67,8 @@ class ModelScopeImageCaptionNode:
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
# 关键修改:将image设置为可选输入
|
||||
"image": ("IMAGE", {"optional": True}),
|
||||
"prompt1": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "详细描述这张图片的内容,包括主体、背景、颜色、风格等信息"
|
||||
@@ -56,9 +77,8 @@ class ModelScopeImageCaptionNode:
|
||||
"multiline": True,
|
||||
"default": ""
|
||||
}),
|
||||
# 添加模型下拉选择
|
||||
"model": (supported_models, {
|
||||
"default": "Qwen/Qwen3-VL-8B-Instruct" # 默认选中原模型
|
||||
"default": "Qwen/Qwen3-VL-8B-Instruct"
|
||||
}),
|
||||
"max_tokens": ("INT", {
|
||||
"default": 1000,
|
||||
@@ -87,12 +107,24 @@ class ModelScopeImageCaptionNode:
|
||||
# 支持多种分隔符拆分Token
|
||||
tokens = re.split(r'[,;\n]+', token_input)
|
||||
return [token.strip() for token in tokens if token.strip()]
|
||||
|
||||
def create_blank_image(self, width=64, height=64):
|
||||
"""创建空白图像张量(符合ComfyUI的图像格式要求)"""
|
||||
# 创建白色背景的RGB图像
|
||||
blank_np = np.ones((height, width, 3), dtype=np.uint8) * 255
|
||||
# 转换为ComfyUI格式的张量 (batch, height, width, channels)
|
||||
blank_tensor = torch.from_numpy(blank_np).unsqueeze(0).float() / 255.0
|
||||
return blank_tensor
|
||||
|
||||
# 调整参数顺序,加入新的prompt2参数
|
||||
def generate_caption(self, image=None, api_tokens="", prompt1="详细描述这张图片的内容", prompt2="", model="Qwen/Qwen3-VL-8B-Instruct", max_tokens=1000, temperature=0.7):
|
||||
if not OPENAI_AVAILABLE:
|
||||
return ("请先安装openai库: pip install openai",)
|
||||
|
||||
# 关键修改:处理输入图像为空的情况
|
||||
if image is None:
|
||||
print("⚠️ 未输入图像,自动生成空白图像作为输入")
|
||||
image = self.create_blank_image()
|
||||
|
||||
# 处理提示词合并
|
||||
prompt_parts = []
|
||||
if prompt1.strip():
|
||||
@@ -100,13 +132,12 @@ class ModelScopeImageCaptionNode:
|
||||
if prompt2.strip():
|
||||
prompt_parts.append(prompt2.strip())
|
||||
|
||||
# 如果两个提示词都为空,使用默认提示
|
||||
if not prompt_parts:
|
||||
prompt = "详细描述这张图片的内容,包括主体、背景、颜色、风格等信息"
|
||||
else:
|
||||
prompt = ", ".join(prompt_parts)
|
||||
|
||||
# 解析Token列表(支持多个)
|
||||
# 解析Token列表
|
||||
tokens = self.parse_api_tokens(api_tokens)
|
||||
if not tokens:
|
||||
raise Exception("请提供至少一个有效的API Token")
|
||||
@@ -122,7 +153,7 @@ class ModelScopeImageCaptionNode:
|
||||
try:
|
||||
print(f"🔍 开始生成图像描述...")
|
||||
print(f"📝 提示词: {prompt}")
|
||||
print(f"🤖 模型: {model}") # 显示选中的模型
|
||||
print(f"🤖 模型: {model}")
|
||||
print(f"🔑 可用Token数量: {len(tokens)}")
|
||||
|
||||
# 转换图像为base64格式
|
||||
@@ -149,13 +180,11 @@ class ModelScopeImageCaptionNode:
|
||||
try:
|
||||
print(f"🔄 尝试使用第 {i+1}/{len(tokens)} 个Token...")
|
||||
|
||||
# 初始化OpenAI客户端
|
||||
client = OpenAI(
|
||||
base_url='https://api-inference.modelscope.cn/v1',
|
||||
api_key=token
|
||||
)
|
||||
|
||||
# 调用API(使用选中的模型)
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
@@ -164,7 +193,6 @@ class ModelScopeImageCaptionNode:
|
||||
stream=False
|
||||
)
|
||||
|
||||
# 成功获取结果
|
||||
description = response.choices[0].message.content
|
||||
print(f"✅ 第 {i+1} 个Token调用成功!")
|
||||
print(f"📄 结果预览: {description[:100]}...")
|
||||
|
||||
+398
-112
@@ -9,80 +9,83 @@ import os
|
||||
import folder_paths
|
||||
import base64
|
||||
import tempfile
|
||||
import re
|
||||
|
||||
# -------------------------- 核心配置管理 --------------------------
|
||||
def load_config():
|
||||
"""从modelscope_config.json加载配置,确保优先使用配置文件中的lora_presets"""
|
||||
config_path = os.path.join(os.path.dirname(__file__), 'modelscope_config.json')
|
||||
default_config = {
|
||||
"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_lora_weight": 0.8,
|
||||
"image_models": ["Qwen/Qwen-Image"],
|
||||
"image_edit_models": ["Qwen/Qwen-Image-Edit"],
|
||||
"lora_presets": [
|
||||
{"name": "无LoRA", "model_id": "", "weight": 0.8}
|
||||
],
|
||||
"api_tokens": []
|
||||
}
|
||||
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"
|
||||
}
|
||||
config = json.load(f)
|
||||
# 确保配置文件中存在所有必要字段,缺失则补充则补充默认值
|
||||
for key, value in default_config.items():
|
||||
if key not in config:
|
||||
config[key] = value
|
||||
return config
|
||||
except Exception as e:
|
||||
print(f"读取配置文件失败,使用默认配置: {e}")
|
||||
return default_config
|
||||
|
||||
def save_config(config: dict) -> bool:
|
||||
"""保存配置到modelscope_config.json"""
|
||||
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}")
|
||||
print(f"保存配置文件失败: {e}")
|
||||
return False
|
||||
|
||||
# -------------------------- API Token管理 --------------------------
|
||||
def save_api_tokens(tokens):
|
||||
"""保存多个API Token"""
|
||||
tokens_path = os.path.join(os.path.dirname(__file__), '.qwen_tokens')
|
||||
try:
|
||||
with open(tokens_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(tokens)) # 每个token一行
|
||||
except Exception as e:
|
||||
print(f"保存tokens失败(.qwen_tokens): {e}")
|
||||
|
||||
try:
|
||||
cfg = load_config()
|
||||
cfg["api_tokens"] = tokens
|
||||
if save_config(cfg):
|
||||
return True
|
||||
return False
|
||||
return save_config(cfg)
|
||||
except Exception as e:
|
||||
print(f"保存tokens失败(config.json): {e}")
|
||||
print(f"保存API tokens失败: {e}")
|
||||
return False
|
||||
|
||||
def load_api_tokens():
|
||||
"""加载多个API Token"""
|
||||
tokens_path = os.path.join(os.path.dirname(__file__), '.qwen_tokens')
|
||||
try:
|
||||
cfg = load_config()
|
||||
tokens_from_cfg = cfg.get("api_tokens", [])
|
||||
if tokens_from_cfg and isinstance(tokens_from_cfg, list):
|
||||
return [token.strip() for token in tokens_from_cfg if token.strip()]
|
||||
except Exception as e:
|
||||
print(f"读取config.json中的tokens失败: {e}")
|
||||
|
||||
try:
|
||||
if os.path.exists(tokens_path):
|
||||
with open(tokens_path, 'r', encoding='utf-8') as f:
|
||||
tokens = [line.strip() for line in f.read().split('\n') if line.strip()]
|
||||
return tokens if tokens else []
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"加载tokens失败: {e}")
|
||||
print(f"加载API tokens失败: {e}")
|
||||
return []
|
||||
|
||||
def parse_api_tokens(token_input):
|
||||
"""解析输入的API Tokens(支持逗号、分号、换行分隔)"""
|
||||
if not token_input or token_input.strip() in ["", "***已保存***"]:
|
||||
return load_api_tokens()
|
||||
|
||||
# 支持多种分隔符
|
||||
import re
|
||||
tokens = re.split(r'[,;\n]+', token_input)
|
||||
return [token.strip() for token in tokens if token.strip()]
|
||||
|
||||
# -------------------------- 图像转换工具 --------------------------
|
||||
def tensor_to_base64_url(image_tensor):
|
||||
try:
|
||||
if len(image_tensor.shape) == 4:
|
||||
@@ -93,8 +96,7 @@ def tensor_to_base64_url(image_tensor):
|
||||
else:
|
||||
image_np = image_tensor.cpu().numpy().astype(np.uint8)
|
||||
|
||||
pil_image = Image.fromarray(image_np)
|
||||
|
||||
pil_image = Image.fromarrayarray(image_np)
|
||||
buffer = BytesIO()
|
||||
pil_image.save(buffer, format='JPEG', quality=85)
|
||||
img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
@@ -102,10 +104,193 @@ def tensor_to_base64_url(image_tensor):
|
||||
return f"data:image/jpeg;base64,{img_base64}"
|
||||
|
||||
except Exception as e:
|
||||
print(f"图像转换失败: {e}")
|
||||
raise Exception(f"图像格式转换失败: {str(e)}")
|
||||
|
||||
# -------------------------- LoRA预设管理节点 --------------------------
|
||||
class ModelScopeLoraPresetNode:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
# 从配置文件加载LoRA预设列表
|
||||
config = load_config()
|
||||
lora_presets = config.get("lora_presets", [])
|
||||
preset_names = [preset.get("name", "无LoRA") for preset in lora_presets]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"action": (["查看预设", "添加预设", "删除预设", "保存预设"], {"default": "查看预设"}),
|
||||
},
|
||||
"optional": {
|
||||
"preset_name": ("STRING", {"default": "自定义LoRA", "label": "预设名称"}),
|
||||
"lora_model_id": ("STRING", {"default": "", "label": "LoRA模型ID", "placeholder": "例如:qiyuanai/TikTok_Xiaohongshu_career_line_beauty_v1"}),
|
||||
"default_weight": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1, "label": "默认权重"}),
|
||||
"target_preset": (preset_names, {"default": preset_names[0] if preset_names else "无LoRA", "label": "目标预设"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "FLOAT", "STRING")
|
||||
RETURN_NAMES = ("lora_model_id", "lora_weight", "preset_info")
|
||||
FUNCTION = "manage_lora_presets"
|
||||
CATEGORY = "ModelScopeAPI/LoRA"
|
||||
|
||||
def manage_lora_presets(self, action, preset_name="", lora_model_id="", default_weight=0.8, target_preset=""):
|
||||
# 所有操作均基于配置文件中的LoRA预设
|
||||
config = load_config()
|
||||
lora_presets = config.get("lora_presets", [])
|
||||
preset_info = f"当前共有 {len(lora_presets)} 个LoRA预设"
|
||||
|
||||
if action == "查看预设":
|
||||
info_lines = ["=== LoRA预设列表 ==="]
|
||||
for i, preset in enumerate(lora_presets):
|
||||
info_lines.append(f"{i+1}. {preset.get('name')} | ID: {preset.get('model_id')} | 权重: {preset.get('weight')}")
|
||||
preset_info = "\n".join(info_lines)
|
||||
selected_preset = next((p for p in lora_presets if p.get("name") == target_preset), {"model_id": "", "weight": 0.8})
|
||||
return (selected_preset.get("model_id"), selected_preset.get("weight"), preset_info)
|
||||
|
||||
elif action == "添加预设":
|
||||
if not preset_name or preset_name.strip() == "":
|
||||
raise Exception("预设名称不能为空")
|
||||
|
||||
if any(p.get("name") == preset_name for p in lora_presets):
|
||||
raise Exception(f"已存在名为 {preset_name} 的预设")
|
||||
|
||||
new_preset = {
|
||||
"name": preset_name.strip(),
|
||||
"model_id": lora_model_id.strip(),
|
||||
"weight": float(default_weight)
|
||||
}
|
||||
lora_presets.append(new_preset)
|
||||
config["lora_presets"] = lora_presets
|
||||
save_config(config)
|
||||
preset_info = f"成功添加预设: {preset_name} | ID: {lora_model_id}"
|
||||
return (lora_model_id, default_weight, preset_info)
|
||||
|
||||
elif action == "删除预设":
|
||||
if target_preset == "无LoRA":
|
||||
raise Exception("不能删除默认的无LoRA预设")
|
||||
|
||||
original_count = len(lora_presets)
|
||||
lora_presets = [p for p in lora_presets if p.get("name") != target_preset]
|
||||
if len(lora_presets) == original_count:
|
||||
raise Exception(f"未找到预设: {target_preset}")
|
||||
|
||||
config["lora_presets"] = lora_presets
|
||||
save_config(config)
|
||||
preset_info = f"成功删除预设: {target_preset}"
|
||||
return ("", 0.8, preset_info)
|
||||
|
||||
elif action == "保存预设":
|
||||
updated = False
|
||||
for i, preset in enumerate(lora_presets):
|
||||
if preset.get("name") == target_preset:
|
||||
lora_presets[i]["model_id"] = lora_model_id.strip()
|
||||
lora_presets[i]["weight"] = float(default_weight)
|
||||
updated = True
|
||||
break
|
||||
|
||||
if not updated:
|
||||
raise Exception(f"未找到预设: {target_preset}")
|
||||
|
||||
config["lora_presets"] = lora_presets
|
||||
save_config(config)
|
||||
preset_info = f"成功更新预设: {target_preset} | 新ID: {lora_model_id} | 新权重: {default_weight}"
|
||||
return (lora_model_id, default_weight, preset_info)
|
||||
|
||||
return ("", 0.8, preset_info)
|
||||
|
||||
# -------------------------- 单LoRA加载节点 --------------------------
|
||||
class ModelScopeSingleLoraLoaderNode:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
# 从配置文件加载LoRA预设选项
|
||||
config = load_config()
|
||||
lora_presets = config.get("lora_presets", [])
|
||||
preset_options = [preset.get("name", "无LoRA") for preset in lora_presets]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"lora_preset": (preset_options, {"default": preset_options[0], "label": "LoRA预设"}),
|
||||
},
|
||||
"optional": {
|
||||
"lora_weight": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1, "label": "自定义权重"}),
|
||||
"use_custom_weight": ("BOOLEAN", {"default": False, "label_on": "使用自定义权重", "label_off": "使用预设权重"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "FLOAT")
|
||||
RETURN_NAMES = ("lora_id", "lora_weight")
|
||||
FUNCTION = "load_single_lora"
|
||||
CATEGORY = "ModelScopeAPI/LoRA"
|
||||
|
||||
def load_single_lora(self, lora_preset, lora_weight=0.8, use_custom_weight=False):
|
||||
# 从配置文件读取选中的LoRA信息
|
||||
config = load_config()
|
||||
lora_presets = config.get("lora_presets", [])
|
||||
|
||||
selected_preset = next((p for p in lora_presets if p.get("name") == lora_preset), {"model_id": "", "weight": 0.8})
|
||||
lora_id = selected_preset.get("model_id", "")
|
||||
final_weight = lora_weight if use_custom_weight else selected_preset.get("weight", 0.8)
|
||||
|
||||
return (lora_id, final_weight)
|
||||
|
||||
# -------------------------- 多LoRA加载节点 --------------------------
|
||||
class ModelScopeMultiLoraLoaderNode:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
# 从配置文件加载LoRA预设选项
|
||||
config = load_config()
|
||||
lora_presets = config.get("lora_presets", [])
|
||||
preset_options = [preset.get("name", "无LoRA") for preset in lora_presets]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"lora1_preset": (preset_options, {"default": preset_options[0], "label": "LoRA 1 预设"}),
|
||||
"lora2_preset": (preset_options, {"default": preset_options[0], "label": "LoRA 2 预设"}),
|
||||
"lora3_preset": (preset_options, {"default": preset_options[0], "label": "LoRA 3 预设"}),
|
||||
},
|
||||
"optional": {
|
||||
"lora1_weight": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1, "label": "LoRA 1 权重"}),
|
||||
"lora2_weight": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1, "label": "LoRA 2 权重"}),
|
||||
"lora3_weight": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1, "label": "LoRA 3 权重"}),
|
||||
"lora1_use_custom": ("BOOLEAN", {"default": False, "label_on": "LoRA1用自定义权重", "label_off": "用预设权重"}),
|
||||
"lora2_use_custom": ("BOOLEAN", {"default": False, "label_on": "LoRA2用自定义权重", "label_off": "用预设权重"}),
|
||||
"lora3_use_custom": ("BOOLEAN", {"default": False, "label_on": "LoRA3用自定义权重", "label_off": "用预设权重"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "FLOAT", "FLOAT", "FLOAT")
|
||||
RETURN_NAMES = ("lora1_id", "lora2_id", "lora3_id", "lora1_w", "lora2_w", "lora3_w")
|
||||
FUNCTION = "load_multi_lora"
|
||||
CATEGORY = "ModelScopeAPI/LoRA"
|
||||
|
||||
def load_multi_lora(self, lora1_preset, lora2_preset, lora3_preset,
|
||||
lora1_weight=0.8, lora2_weight=0.8, lora3_weight=0.8,
|
||||
lora1_use_custom=False, lora2_use_custom=False, lora3_use_custom=False):
|
||||
# 从配置文件读取多个LoRA信息
|
||||
config = load_config()
|
||||
lora_presets = config.get("lora_presets", [])
|
||||
|
||||
def get_lora_info(preset_name, custom_weight, use_custom):
|
||||
preset = next((p for p in lora_presets if p.get("name") == preset_name), {"model_id": "", "weight": 0.8})
|
||||
model_id = preset.get("model_id", "")
|
||||
final_weight = custom_weight if use_custom else preset.get("weight", 0.8)
|
||||
return model_id, final_weight
|
||||
|
||||
lora1_id, lora1_w = get_lora_info(lora1_preset, lora1_weight, lora1_use_custom)
|
||||
lora2_id, lora2_w = get_lora_info(lora2_preset, lora2_weight, lora2_use_custom)
|
||||
lora3_id, lora3_w = get_lora_info(lora3_preset, lora3_weight, lora3_use_custom)
|
||||
|
||||
return (lora1_id, lora2_id, lora3_id, lora1_w, lora2_w, lora3_w)
|
||||
|
||||
# -------------------------- 生图节点 --------------------------
|
||||
class ModelScopeImageNode:
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -114,6 +299,7 @@ class ModelScopeImageNode:
|
||||
def INPUT_TYPES(cls):
|
||||
config = load_config()
|
||||
saved_tokens = load_api_tokens()
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {
|
||||
@@ -162,6 +348,12 @@ class ModelScopeImageNode:
|
||||
"max": 20.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
"lora1_id": ("STRING", {"default": "", "label": "LoRA1 模型ID"}),
|
||||
"lora1_w": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1, "label": "LoRA1 权重"}),
|
||||
"lora2_id": ("STRING", {"default": "", "label": "LoRA2 模型ID"}),
|
||||
"lora2_w": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1, "label": "LoRA2 权重"}),
|
||||
"lora3_id": ("STRING", {"default": "", "label": "LoRA3 模型ID"}),
|
||||
"lora3_w": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1, "label": "LoRA3 权重"}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,25 +362,49 @@ class ModelScopeImageNode:
|
||||
FUNCTION = "generate_image"
|
||||
CATEGORY = "ModelScopeAPI"
|
||||
|
||||
def generate_image(self, prompt, api_tokens, model="Qwen/Qwen-Image", negative_prompt="", width=512, height=512, seed=-1, steps=30, guidance=7.5):
|
||||
def generate_image(self, prompt, api_tokens, model="Qwen/Qwen-Image", negative_prompt="", width=512, height=512, seed=-1, steps=30, guidance=7.5,
|
||||
lora1_id="", lora1_w=0.8, lora2_id="", lora2_w=0.8, lora3_id="", lora3_w=0.8):
|
||||
config = load_config()
|
||||
tokens = parse_api_tokens(api_tokens)
|
||||
|
||||
if not tokens:
|
||||
raise Exception("请提供至少一个有效的API Token")
|
||||
|
||||
# 保存Token(如果提供了新的)
|
||||
# 保存新Token(如果有变化)
|
||||
if api_tokens and api_tokens.strip() not in ["", "***已保存{}个Token***".format(len(load_api_tokens()))]:
|
||||
if save_api_tokens(tokens):
|
||||
print(f"✅ 已保存 {len(tokens)} 个API Token")
|
||||
else:
|
||||
print("⚠️ API Token保存失败,但不影响当前使用")
|
||||
|
||||
# 轮询尝试每个Token
|
||||
print(f"🔍 开始生成图像...")
|
||||
print(f"📝 提示词: {prompt}")
|
||||
print(f"❌ 反向提示词: {negative_prompt if negative_prompt else '无'}")
|
||||
print(f"🤖 模型: {model}")
|
||||
print(f"🔑 可用Token数量: {len(tokens)}")
|
||||
print(f"📐 尺寸: {width}x{height}")
|
||||
print(f"🔄 步数: {steps}")
|
||||
print(f"🧭 引导系数: {guidance}")
|
||||
print(f"🔢 种子: {seed if seed != -1 else '随机'}")
|
||||
|
||||
# 打印LoRA信息
|
||||
lora_info = []
|
||||
if lora1_id.strip():
|
||||
lora_info.append(f"LoRA1: {lora1_id} (权重: {lora1_w})")
|
||||
if lora2_id.strip():
|
||||
lora_info.append(f"LoRA2: {lora2_id} (权重: {lora2_w})")
|
||||
if lora3_id.strip():
|
||||
lora_info.append(f"LoRA3: {lora3_id} (权重: {lora3_w})")
|
||||
if lora_info:
|
||||
print(f"🔧 LoRA配置: {', '.join(lora_info)}")
|
||||
else:
|
||||
print("🔧 未使用LoRA")
|
||||
|
||||
last_exception = None
|
||||
for i, token in enumerate(tokens):
|
||||
try:
|
||||
print(f"🔄 尝试使用第 {i+1} 个API Token...")
|
||||
print(f"🔄 尝试使用第 {i+1}/{len(tokens)} 个Token...")
|
||||
|
||||
url = 'https://api-inference.modelscope.cn/v1/images/generations'
|
||||
payload = {
|
||||
'model': model,
|
||||
@@ -197,21 +413,40 @@ class ModelScopeImageNode:
|
||||
'steps': steps,
|
||||
'guidance': guidance
|
||||
}
|
||||
|
||||
lora_dict = {}
|
||||
if lora1_id and lora1_id.strip() != "":
|
||||
lora_dict[lora1_id.strip()] = float(lora1_w)
|
||||
if lora2_id and lora2_id.strip() != "":
|
||||
lora_dict[lora2_id.strip()] = float(lora2_w)
|
||||
if lora3_id and lora3_id.strip() != "":
|
||||
lora_dict[lora3_id.strip()] = float(lora3_w)
|
||||
|
||||
if lora_dict:
|
||||
payload['loras'] = lora_dict
|
||||
first_lora_id = next(iter(lora_dict.keys()))
|
||||
first_lora_w = next(iter(lora_dict.values()))
|
||||
payload['lora'] = first_lora_id
|
||||
payload['lora_weight'] = first_lora_w
|
||||
|
||||
if negative_prompt.strip():
|
||||
payload['negative_prompt'] = negative_prompt
|
||||
if seed != -1:
|
||||
payload['seed'] = seed
|
||||
else:
|
||||
import random
|
||||
random_seed = random.randint(0, 2147483647)
|
||||
payload['seed'] = random_seed
|
||||
payload['seed'] = random.randint(0, 2147483647)
|
||||
print(f"🎲 随机生成种子: {payload['seed']}")
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Content-Type': 'application/json',
|
||||
'X-ModelScope-Async-Mode': 'true'
|
||||
'X-ModelScope-Async-Mode': 'true',
|
||||
'X-ModelScope-Task-Type': 'text-to-image-generation',
|
||||
'X-ModelScope-Request-Params': json.dumps({'loras': lora_dict} if lora_dict else {})
|
||||
}
|
||||
|
||||
print(f"🚀 发送API请求到 {model}...")
|
||||
submission_response = requests.post(
|
||||
url,
|
||||
data=json.dumps(payload, ensure_ascii=False).encode('utf-8'),
|
||||
@@ -220,11 +455,16 @@ class ModelScopeImageNode:
|
||||
)
|
||||
|
||||
if submission_response.status_code == 400:
|
||||
# 尝试使用最小参数重试
|
||||
print("⚠️ 标准请求参数失败,尝试简化参数...")
|
||||
minimal_payload = {
|
||||
'model': model,
|
||||
'prompt': prompt
|
||||
}
|
||||
if lora_dict:
|
||||
minimal_payload['loras'] = lora_dict
|
||||
minimal_payload['lora'] = first_lora_id
|
||||
minimal_payload['lora_weight'] = first_lora_w
|
||||
|
||||
submission_response = requests.post(
|
||||
url,
|
||||
data=json.dumps(minimal_payload, ensure_ascii=False).encode('utf-8'),
|
||||
@@ -237,9 +477,10 @@ class ModelScopeImageNode:
|
||||
|
||||
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},开始轮询...")
|
||||
print(f"📌 获取任务ID: {task_id}, 开始轮询结果...")
|
||||
poll_start = time.time()
|
||||
max_wait_seconds = max(60, config.get('timeout', 720))
|
||||
while True:
|
||||
@@ -251,53 +492,59 @@ class ModelScopeImageNode:
|
||||
},
|
||||
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')
|
||||
print(f"⌛ 任务状态: {status} (已等待 {int(time.time() - poll_start)} 秒)")
|
||||
|
||||
if status == 'SUCCEED':
|
||||
output_images = task_data.get('output_images') or []
|
||||
if not output_images:
|
||||
raise Exception("任务成功但未返回图片URL")
|
||||
image_url = output_images[0]
|
||||
print("✅ 任务完成,开始下载图片...")
|
||||
print(f"✅ 任务完成,获取图片URL")
|
||||
break
|
||||
if status == 'FAILED':
|
||||
raise Exception(f"任务失败: {task_data}")
|
||||
if time.time() - poll_start > max_wait_seconds:
|
||||
raise Exception("任务轮询超时,请稍后重试或降低并发")
|
||||
raise Exception(f"任务轮询超时 ({max_wait_seconds}秒),请稍后重试或降低并发")
|
||||
time.sleep(5)
|
||||
elif 'images' in submission_json and len(submission_json['images']) > 0:
|
||||
image_url = submission_json['images'][0]['url']
|
||||
print(f"⬇️ 下载生成的图片...")
|
||||
print(f"✅ 直接获取图片URL")
|
||||
else:
|
||||
raise Exception(f"未识别的API返回格式: {submission_json}")
|
||||
|
||||
print(f"📥 下载图片...")
|
||||
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}")
|
||||
|
||||
print(f"🖼️ 处理图片数据...")
|
||||
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"🎉 图片处理完成!使用的第 {i+1} 个API Token")
|
||||
|
||||
print(f"✅ 第 {i+1} 个Token调用成功,图像生成完成!")
|
||||
return (image_tensor,)
|
||||
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
print(f"⚠️ 第 {i+1} 个API Token失败: {str(e)}")
|
||||
if i < len(tokens) - 1: # 不是最后一个Token
|
||||
print(f"➡️ 尝试下一个API Token...")
|
||||
print(f"❌ 第 {i+1} 个Token调用失败: {str(e)}")
|
||||
if i < len(tokens) - 1:
|
||||
print(f"⏳ 准备尝试下一个Token...")
|
||||
continue
|
||||
else:
|
||||
break # 所有Token都失败了
|
||||
break
|
||||
|
||||
# 所有Token都失败
|
||||
raise Exception(f"所有 {len(tokens)} 个API Token都失败了。最后的错误: {str(last_exception)}")
|
||||
|
||||
|
||||
# -------------------------- 编辑节点(已添加LoRA功能) --------------------------
|
||||
class ModelScopeImageEditNode:
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -307,7 +554,6 @@ class ModelScopeImageEditNode:
|
||||
config = load_config()
|
||||
saved_tokens = load_api_tokens()
|
||||
|
||||
# 获取模型列表
|
||||
edit_models = config.get("image_edit_models", ["Qwen/Qwen-Image-Edit"])
|
||||
gen_models = config.get("image_models", ["Qwen/Qwen-Image"])
|
||||
|
||||
@@ -369,6 +615,13 @@ class ModelScopeImageEditNode:
|
||||
"min": -1,
|
||||
"max": 2147483647
|
||||
}),
|
||||
# LoRA相关参数(与生图节点保持一致)
|
||||
"lora1_id": ("STRING", {"default": "", "label": "LoRA1 模型ID"}),
|
||||
"lora1_w": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1, "label": "LoRA1 权重"}),
|
||||
"lora2_id": ("STRING", {"default": "", "label": "LoRA2 模型ID"}),
|
||||
"lora2_w": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1, "label": "LoRA2 权重"}),
|
||||
"lora3_id": ("STRING", {"default": "", "label": "LoRA3 模型ID"}),
|
||||
"lora3_w": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1, "label": "LoRA3 权重"}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,51 +632,68 @@ class ModelScopeImageEditNode:
|
||||
|
||||
def edit_image(self, image, prompt, api_tokens, image_gen_mode=False, gen_model="Qwen/Qwen-Image",
|
||||
edit_model="Qwen/Qwen-Image-Edit", negative_prompt="",
|
||||
width=512, height=512, steps=30, guidance=3.5, seed=-1):
|
||||
width=512, height=512, steps=30, guidance=3.5, seed=-1,
|
||||
lora1_id="", lora1_w=0.8, lora2_id="", lora2_w=0.8, lora3_id="", lora3_w=0.8):
|
||||
config = load_config()
|
||||
tokens = parse_api_tokens(api_tokens)
|
||||
|
||||
if not tokens:
|
||||
raise Exception("请提供至少一个有效的API Token")
|
||||
|
||||
# 保存Token(如果提供了新的)
|
||||
# 保存新Token(如果有变化)
|
||||
if api_tokens and api_tokens.strip() not in ["", "***已保存{}个Token***".format(len(load_api_tokens()))]:
|
||||
if save_api_tokens(tokens):
|
||||
print(f"✅ 已保存 {len(tokens)} 个API Token")
|
||||
else:
|
||||
print("⚠️ API Token保存失败,但不影响当前使用")
|
||||
|
||||
# 根据开关选择使用的模型
|
||||
if image_gen_mode:
|
||||
model = gen_model
|
||||
mode_name = "图生图"
|
||||
mode = "图生图模式" if image_gen_mode else "图像编辑模式"
|
||||
model = gen_model if image_gen_mode else edit_model
|
||||
|
||||
print(f"🔍 开始图像编辑...")
|
||||
print(f"📝 提示词: {prompt}")
|
||||
print(f"❌ 反向提示词: {negative_prompt if negative_prompt else '无'}")
|
||||
print(f"🤖 模型: {model} ({mode})")
|
||||
print(f"🔑 可用Token数量: {len(tokens)}")
|
||||
print(f"📐 尺寸: {width}x{height}")
|
||||
print(f"🔄 步数: {steps}")
|
||||
print(f"🧭 引导系数: {guidance}")
|
||||
print(f"🔢 种子: {seed if seed != -1 else '随机'}")
|
||||
|
||||
# 打印LoRA信息
|
||||
lora_info = []
|
||||
if lora1_id.strip():
|
||||
lora_info.append(f"LoRA1: {lora1_id} (权重: {lora1_w})")
|
||||
if lora2_id.strip():
|
||||
lora_info.append(f"LoRA2: {lora2_id} (权重: {lora2_w})")
|
||||
if lora3_id.strip():
|
||||
lora_info.append(f"LoRA3: {lora3_id} (权重: {lora3_w})")
|
||||
if lora_info:
|
||||
print(f"🔧 LoRA配置: {', '.join(lora_info)}")
|
||||
else:
|
||||
model = edit_model
|
||||
mode_name = "图像编辑"
|
||||
print("🔧 未使用LoRA")
|
||||
|
||||
# 轮询尝试每个Token
|
||||
last_exception = None
|
||||
for i, token in enumerate(tokens):
|
||||
try:
|
||||
print(f"🔄 尝试使用第 {i+1} 个API Token...")
|
||||
print(f"🔄 尝试使用第 {i+1}/{len(tokens)} 个Token...")
|
||||
|
||||
# 将图像转换为临时文件并上传获取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_np = 255. * img.cpu().numpy()
|
||||
img_pil = Image.fromarray(np.clip(img_np, 0, 255).astype(np.uint8))
|
||||
img_pil.save(temp_img_path)
|
||||
print(f"✅ 图像已保存到临时文件: {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}
|
||||
@@ -436,17 +706,13 @@ class ModelScopeImageEditNode:
|
||||
upload_data = upload_response.json()
|
||||
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}")
|
||||
print(f"📤 图像上传成功,URL: {image_url[:50]}...")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 图像上传异常: {str(e)}")
|
||||
print(f"⚠️ 图像上传失败,将使用base64编码: {str(e)}")
|
||||
|
||||
# 如果上传失败,回退到base64
|
||||
# 构建请求 payload
|
||||
if not image_url:
|
||||
print("⚠️ 图像URL获取失败,回退到使用base64")
|
||||
print("🔄 转换图像为base64格式...")
|
||||
image_data = tensor_to_base64_url(image)
|
||||
payload = {
|
||||
'model': model,
|
||||
@@ -460,33 +726,48 @@ class ModelScopeImageEditNode:
|
||||
'image_url': image_url
|
||||
}
|
||||
|
||||
# 构建LoRA参数
|
||||
lora_dict = {}
|
||||
if lora1_id and lora1_id.strip() != "":
|
||||
lora_dict[lora1_id.strip()] = float(lora1_w)
|
||||
if lora2_id and lora2_id.strip() != "":
|
||||
lora_dict[lora2_id.strip()] = float(lora2_w)
|
||||
if lora3_id and lora3_id.strip() != "":
|
||||
lora_dict[lora3_id.strip()] = float(lora3_w)
|
||||
|
||||
if lora_dict:
|
||||
payload['loras'] = lora_dict
|
||||
first_lora_id = next(iter(lora_dict.keys()))
|
||||
first_lora_w = next(iter(lora_dict.values()))
|
||||
payload['lora'] = first_lora_id
|
||||
payload['lora_weight'] = first_lora_w
|
||||
|
||||
# 添加其他参数
|
||||
if negative_prompt.strip():
|
||||
payload['negative_prompt'] = negative_prompt
|
||||
|
||||
# 添加新参数
|
||||
if width != 512 or height != 512:
|
||||
size = f"{width}x{height}"
|
||||
payload['size'] = size
|
||||
|
||||
payload['size'] = f"{width}x{height}"
|
||||
if steps != 30:
|
||||
payload['steps'] = steps
|
||||
|
||||
if guidance != 3.5:
|
||||
payload['guidance'] = guidance
|
||||
|
||||
if seed != -1:
|
||||
payload['seed'] = seed
|
||||
else:
|
||||
import random
|
||||
payload['seed'] = random.randint(0, 2147483647)
|
||||
print(f"🎲 随机生成种子: {payload['seed']}")
|
||||
|
||||
# 设置请求头
|
||||
headers = {
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Content-Type': 'application/json',
|
||||
'X-ModelScope-Async-Mode': 'true'
|
||||
'X-ModelScope-Async-Mode': 'true',
|
||||
'X-ModelScope-Task-Type': 'image-to-image-generation',
|
||||
'X-ModelScope-Request-Params': json.dumps({'loras': lora_dict} if lora_dict else {})
|
||||
}
|
||||
|
||||
print(f"🖼️ 开始{mode_name}...")
|
||||
print(f"✏️ 编辑提示: {prompt}")
|
||||
print(f"🧠 使用模型: {model}")
|
||||
|
||||
print(f"🚀 发送API请求到 {model}...")
|
||||
url = 'https://api-inference.modelscope.cn/v1/images/generations'
|
||||
submission_response = requests.post(
|
||||
url,
|
||||
@@ -503,7 +784,7 @@ class ModelScopeImageEditNode:
|
||||
|
||||
if 'task_id' in submission_json:
|
||||
task_id = submission_json['task_id']
|
||||
print(f"🕒 已提交任务,任务ID: {task_id},开始轮询...")
|
||||
print(f"📌 获取任务ID: {task_id}, 开始轮询结果...")
|
||||
poll_start = time.time()
|
||||
max_wait_seconds = max(60, config.get('timeout', 720))
|
||||
|
||||
@@ -522,31 +803,31 @@ class ModelScopeImageEditNode:
|
||||
|
||||
task_data = task_resp.json()
|
||||
status = task_data.get('task_status')
|
||||
print(f"⌛ 任务状态: {status} (已等待 {int(time.time() - poll_start)} 秒)")
|
||||
|
||||
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("✅ 任务完成,开始下载编辑后的图片...")
|
||||
print(f"✅ 任务完成,获取图片URL")
|
||||
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("任务轮询超时,请稍后重试或降低并发")
|
||||
|
||||
raise Exception(f"任务轮询超时 ({max_wait_seconds}秒),请稍后重试或降低并发")
|
||||
time.sleep(5)
|
||||
else:
|
||||
raise Exception(f"未识别的API返回格式: {submission_json}")
|
||||
|
||||
print(f"📥 下载编辑后的图片...")
|
||||
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}")
|
||||
|
||||
print(f"🖼️ 处理图片数据...")
|
||||
pil_image = Image.open(BytesIO(img_response.content))
|
||||
if pil_image.mode != 'RGB':
|
||||
pil_image = pil_image.convert('RGB')
|
||||
@@ -558,38 +839,43 @@ class ModelScopeImageEditNode:
|
||||
if temp_img_path and os.path.exists(temp_img_path):
|
||||
try:
|
||||
os.remove(temp_img_path)
|
||||
print(f"🧹 已删除临时图像文件")
|
||||
except:
|
||||
pass
|
||||
print(f"⚠️ 无法删除临时图像文件 {temp_img_path}")
|
||||
|
||||
print(f"🎉 {mode_name}完成!使用的第 {i+1} 个API Token")
|
||||
print(f"✅ 第 {i+1} 个Token调用成功,图像编辑完成!")
|
||||
return (image_tensor,)
|
||||
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
print(f"⚠️ 第 {i+1} 个API Token失败: {str(e)}")
|
||||
print(f"❌ 第 {i+1} 个Token调用失败: {str(e)}")
|
||||
# 清理临时文件
|
||||
if temp_img_path and os.path.exists(temp_img_path):
|
||||
try:
|
||||
os.remove(temp_img_path)
|
||||
except:
|
||||
pass
|
||||
if i < len(tokens) - 1: # 不是最后一个Token
|
||||
print(f"➡️ 尝试下一个API Token...")
|
||||
if i < len(tokens) - 1:
|
||||
print(f"⏳ 准备尝试下一个Token...")
|
||||
continue
|
||||
else:
|
||||
break # 所有Token都失败了
|
||||
break
|
||||
|
||||
# 所有Token都失败
|
||||
raise Exception(f"所有 {len(tokens)} 个API Token都失败了。最后的错误: {str(last_exception)}")
|
||||
|
||||
|
||||
# 节点映射
|
||||
# -------------------------- 节点映射 --------------------------
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ModelScopeImageNode": ModelScopeImageNode,
|
||||
"ModelScopeImageEditNode": ModelScopeImageEditNode
|
||||
"ModelScopeImageEditNode": ModelScopeImageEditNode,
|
||||
"ModelScopeLoraPresetNode": ModelScopeLoraPresetNode,
|
||||
"ModelScopeSingleLoraLoaderNode": ModelScopeSingleLoraLoaderNode,
|
||||
"ModelScopeMultiLoraLoaderNode": ModelScopeMultiLoraLoaderNode
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ModelScopeImageNode": "ModelScope-Image 生图节点",
|
||||
"ModelScopeImageEditNode": "ModelScope-Image 图像编辑节点"
|
||||
}
|
||||
"ModelScopeImageEditNode": "ModelScope-Image 图像编辑节点",
|
||||
"ModelScopeLoraPresetNode": "ModelScope-LoRA 预设管理",
|
||||
"ModelScopeSingleLoraLoaderNode": "ModelScope-LoRA 单LoRA加载",
|
||||
"ModelScopeMultiLoraLoaderNode": "ModelScope-LoRA 多LoRA加载"
|
||||
}
|
||||
+20
-23
@@ -22,39 +22,36 @@ def load_config():
|
||||
"default_model": "Qwen/Qwen-Image",
|
||||
"timeout": 720,
|
||||
"image_download_timeout": 30,
|
||||
"default_prompt": "A beautiful landscape"
|
||||
"default_prompt": "A beautiful landscape",
|
||||
"default_text_model": "Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
||||
"default_system_prompt": "You are a helpful assistant.",
|
||||
"default_user_prompt": "你好",
|
||||
"api_token": ""
|
||||
}
|
||||
|
||||
def save_config(config):
|
||||
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 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
|
||||
return cfg.get("api_token", "").strip()
|
||||
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}")
|
||||
print(f"读取 config.json中的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
|
||||
return save_config(cfg)
|
||||
except Exception as e:
|
||||
print(f"保存token失败: {e}")
|
||||
return False
|
||||
@@ -83,7 +80,7 @@ class ModelScopeTextNode:
|
||||
"default": config.get("default_user_prompt", "你好")
|
||||
}),
|
||||
"api_token": ("STRING", {
|
||||
"default": "",
|
||||
"default": saved_token,
|
||||
"placeholder": "请输入您的魔搭API Token",
|
||||
"multiline": False
|
||||
}),
|
||||
@@ -132,7 +129,7 @@ class ModelScopeTextNode:
|
||||
saved_token = load_api_token()
|
||||
if api_token != saved_token:
|
||||
if save_api_token(api_token):
|
||||
print("✅ API Token已自动保存")
|
||||
print("✅ API Token已自动保存到modelscope_config.json")
|
||||
else:
|
||||
print("⚠️ API Token保存失败,但不影响当前使用")
|
||||
|
||||
|
||||
+20
-23
@@ -19,7 +19,7 @@ except ImportError:
|
||||
OpenAI = None
|
||||
|
||||
def load_config():
|
||||
config_path = os.path.join(os.path.dirname(__file__), 'modelscope_config.json')
|
||||
config_path = os.path.join.join(os.path.dirname(__file__), 'modelscope_config.json')
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
@@ -28,39 +28,36 @@ def load_config():
|
||||
"default_model": "Qwen/Qwen-Image",
|
||||
"timeout": 720,
|
||||
"image_download_timeout": 30,
|
||||
"default_prompt": "A beautiful landscape"
|
||||
"default_prompt": "A beautiful landscape",
|
||||
"api_token": "" # 确保默认默认配置中添加api_token字段
|
||||
}
|
||||
|
||||
def save_config(config):
|
||||
"""保存配置到modelscope_config.json"""
|
||||
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 load_api_token():
|
||||
token_path = os.path.join(os.path.dirname(__file__), '.qwen_token')
|
||||
"""仅从modelscope_config.json读取API Token"""
|
||||
try:
|
||||
cfg = load_config()
|
||||
token_from_cfg = cfg.get("api_token", "").strip()
|
||||
if token_from_cfg:
|
||||
return token_from_cfg
|
||||
return cfg.get("api_token", "").strip()
|
||||
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')
|
||||
"""仅将API Token保存到modelscope_config.json"""
|
||||
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
|
||||
cfg["api_token"] = token.strip()
|
||||
return save_config(cfg)
|
||||
except Exception as e:
|
||||
print(f"保存token失败: {e}")
|
||||
return False
|
||||
@@ -154,7 +151,7 @@ class ModelScopeVisionNode:
|
||||
saved_token = load_api_token()
|
||||
if api_token != saved_token:
|
||||
if save_api_token(api_token):
|
||||
print("✅ API Token已自动保存")
|
||||
print("✅ API Token已自动保存到modelscope_config.json")
|
||||
else:
|
||||
print("⚠️ API Token保存失败,但不影响当前使用")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user