perf(接口2): 稳定混跑耗时至12s内 —— ComfyUI插队 + CLIP挪CPU + 提示词全局统一
问题:接口2 与接口3/5 乱序调用时耗时抖动(最差 15~22s)。两个根因: 1. GPU 24G 常驻 21.4G,Flux-2(3.9G) 无法完全驻留显存,每次采样动态换页, 速度随空闲显存波动(2s~8s); 2. ComfyUI 单队列 FIFO,接口2 排在接口3/5 批量任务后面。 改动: - hairline/comfyui.py: run() 新增 front 参数,/prompt 带 "front": true 插队到队列最前; redraw.py 透传;service.py 接口2 三处调用(女重绘 + 男有/无遮罩)传 front=True, 接口3/5 仍走普通队列。 - add_hair.json / 0716add-hair-api.json: 节点61 CLIPLoader device default→cpu。 qwen CLIP(4G) 不再占显存(文本条件缓存常年命中),ComfyUI 显存 8.8G→4.5G, Flux-2 完全驻留,采样稳定 ~3-5s。代价:换 prompt 后首次请求 CPU 编码 ~11s(一次性)。 - 提示词全局统一为「填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜」: app.py 4处默认值、service.py _REDRAW_PROMPT、redraw.py _DEFAULT_PROMPT、 4个工作流节点60内置文案、测试页(test_interface2/3/7/12/12_final)、local_test。 任何两个不同 prompt 交替提交都会打爆 CLIP 编码缓存(--cache-classic 只存最近一次), 之前测试页旧文案与服务端不一致导致交替测试每次 +11s。 - app.py: 接口7 /api/v1/hair/grow-v2 下线(业务弃用;add_hair2.json 的 Klein-9b 会把常驻 Klein-4b 挤出显存)。保留 stub 返回 1007 明确报错,避免裸 404。 实测(1024 档):接口2女 8.5~10s、接口2男 ~5s、接口3 ~7-10s,交替混跑无尖刺。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+327
-1
@@ -1 +1,327 @@
|
|||||||
{"16": {"class_type": "UNETLoader", "inputs": {"unet_name": "flux-2-klein-4b-fp8.safetensors", "weight_dtype": "fp8_e4m3fn_fast"}}, "3": {"class_type": "VAELoader", "inputs": {"vae_name": "flux2-vae.safetensors"}}, "61": {"class_type": "CLIPLoader", "inputs": {"clip_name": "qwen_3_4b.safetensors", "type": "flux2", "device": "default"}}, "26": {"class_type": "LoadImage", "inputs": {"image": "placeholder.png"}}, "60": {"class_type": "JjkText", "inputs": {"text": "填充遮罩区域的头发,皮肤加一点磨皮"}}, "22": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["61", 0], "text": ["60", 0]}}, "31": {"class_type": "easy imageSize", "inputs": {"image": ["26", 0]}}, "33": {"class_type": "Mask Fill Holes", "inputs": {"masks": ["26", 1]}}, "36": {"class_type": "Convert Masks to Images", "inputs": {"masks": ["33", 0]}}, "39": {"class_type": "ImageScale", "inputs": {"image": ["36", 0], "upscale_method": "nearest-exact", "width": ["31", 0], "height": ["31", 1], "crop": "disabled"}}, "37": {"class_type": "Image To Mask", "inputs": {"image": ["39", 0], "method": "intensity"}}, "32": {"class_type": "LayerUtility: ImageScaleByAspectRatio V2", "inputs": {"image": ["26", 0], "mask": ["37", 0], "aspect_ratio": "custom", "proportional_width": ["31", 0], "proportional_height": ["31", 1], "fit": "letterbox", "method": "lanczos", "round_to_multiple": "8", "scale_to_side": "None", "scale_to_length": 1024, "background_color": "#000000"}}, "44": {"class_type": "ImageAndMaskPreview", "inputs": {"image": ["32", 0], "mask": ["32", 1], "mask_opacity": 1, "mask_color": "FFFF00", "pass_through": true}}, "14": {"class_type": "GetImageSize+", "inputs": {"image": ["44", 0]}}, "13": {"class_type": "VAEEncode", "inputs": {"pixels": ["44", 0], "vae": ["3", 0]}}, "2": {"class_type": "ModelSamplingFlux", "inputs": {"model": ["16", 0], "max_shift": 1.15, "base_shift": 0.5, "width": ["14", 0], "height": ["14", 1]}}, "19": {"class_type": "FluxGuidance", "inputs": {"conditioning": ["22", 0], "guidance": 1}}, "5": {"class_type": "ReferenceLatent", "inputs": {"conditioning": ["19", 0], "latent": ["13", 0]}}, "7": {"class_type": "EmptySD3LatentImage", "inputs": {"width": ["14", 0], "height": ["14", 1], "batch_size": 1}}, "1": {"class_type": "BasicScheduler", "inputs": {"model": ["2", 0], "scheduler": "simple", "steps": 4, "denoise": 1}}, "20": {"class_type": "BasicGuider", "inputs": {"model": ["2", 0], "conditioning": ["5", 0]}}, "6": {"class_type": "RandomNoise", "inputs": {"noise_seed": 0}}, "8": {"class_type": "KSamplerSelect", "inputs": {"sampler_name": "euler"}}, "9": {"class_type": "SamplerCustomAdvanced", "inputs": {"noise": ["6", 0], "guider": ["20", 0], "sampler": ["8", 0], "sigmas": ["1", 0], "latent_image": ["7", 0]}}, "10": {"class_type": "VAEDecode", "inputs": {"samples": ["9", 0], "vae": ["3", 0]}}, "62": {"class_type": "ColorMatch", "inputs": {"image_ref": ["26", 0], "image_target": ["10", 0], "method": "mkl", "strength": 1, "multithread": true}}, "17": {"class_type": "SaveImage", "inputs": {"images": ["62", 0], "filename_prefix": "hair_inpaint"}}}
|
{
|
||||||
|
"16": {
|
||||||
|
"class_type": "UNETLoader",
|
||||||
|
"inputs": {
|
||||||
|
"unet_name": "flux-2-klein-4b-fp8.safetensors",
|
||||||
|
"weight_dtype": "fp8_e4m3fn_fast"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"class_type": "VAELoader",
|
||||||
|
"inputs": {
|
||||||
|
"vae_name": "flux2-vae.safetensors"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"61": {
|
||||||
|
"class_type": "CLIPLoader",
|
||||||
|
"inputs": {
|
||||||
|
"clip_name": "qwen_3_4b.safetensors",
|
||||||
|
"type": "flux2",
|
||||||
|
"device": "cpu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"26": {
|
||||||
|
"class_type": "LoadImage",
|
||||||
|
"inputs": {
|
||||||
|
"image": "placeholder.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"60": {
|
||||||
|
"class_type": "JjkText",
|
||||||
|
"inputs": {
|
||||||
|
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"22": {
|
||||||
|
"class_type": "CLIPTextEncode",
|
||||||
|
"inputs": {
|
||||||
|
"clip": [
|
||||||
|
"61",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"text": [
|
||||||
|
"60",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"31": {
|
||||||
|
"class_type": "easy imageSize",
|
||||||
|
"inputs": {
|
||||||
|
"image": [
|
||||||
|
"26",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"33": {
|
||||||
|
"class_type": "Mask Fill Holes",
|
||||||
|
"inputs": {
|
||||||
|
"masks": [
|
||||||
|
"26",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"36": {
|
||||||
|
"class_type": "Convert Masks to Images",
|
||||||
|
"inputs": {
|
||||||
|
"masks": [
|
||||||
|
"33",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"39": {
|
||||||
|
"class_type": "ImageScale",
|
||||||
|
"inputs": {
|
||||||
|
"image": [
|
||||||
|
"36",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"upscale_method": "nearest-exact",
|
||||||
|
"width": [
|
||||||
|
"31",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"height": [
|
||||||
|
"31",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"crop": "disabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"37": {
|
||||||
|
"class_type": "Image To Mask",
|
||||||
|
"inputs": {
|
||||||
|
"image": [
|
||||||
|
"39",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"method": "intensity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"32": {
|
||||||
|
"class_type": "LayerUtility: ImageScaleByAspectRatio V2",
|
||||||
|
"inputs": {
|
||||||
|
"image": [
|
||||||
|
"26",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"mask": [
|
||||||
|
"37",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"aspect_ratio": "custom",
|
||||||
|
"proportional_width": [
|
||||||
|
"31",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"proportional_height": [
|
||||||
|
"31",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"fit": "letterbox",
|
||||||
|
"method": "lanczos",
|
||||||
|
"round_to_multiple": "8",
|
||||||
|
"scale_to_side": "None",
|
||||||
|
"scale_to_length": 1024,
|
||||||
|
"background_color": "#000000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"44": {
|
||||||
|
"class_type": "ImageAndMaskPreview",
|
||||||
|
"inputs": {
|
||||||
|
"image": [
|
||||||
|
"32",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"mask": [
|
||||||
|
"32",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"mask_opacity": 1,
|
||||||
|
"mask_color": "FFFF00",
|
||||||
|
"pass_through": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"14": {
|
||||||
|
"class_type": "GetImageSize+",
|
||||||
|
"inputs": {
|
||||||
|
"image": [
|
||||||
|
"44",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"13": {
|
||||||
|
"class_type": "VAEEncode",
|
||||||
|
"inputs": {
|
||||||
|
"pixels": [
|
||||||
|
"44",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"vae": [
|
||||||
|
"3",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"class_type": "ModelSamplingFlux",
|
||||||
|
"inputs": {
|
||||||
|
"model": [
|
||||||
|
"16",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"max_shift": 1.15,
|
||||||
|
"base_shift": 0.5,
|
||||||
|
"width": [
|
||||||
|
"14",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"height": [
|
||||||
|
"14",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"19": {
|
||||||
|
"class_type": "FluxGuidance",
|
||||||
|
"inputs": {
|
||||||
|
"conditioning": [
|
||||||
|
"22",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"guidance": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"class_type": "ReferenceLatent",
|
||||||
|
"inputs": {
|
||||||
|
"conditioning": [
|
||||||
|
"19",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"latent": [
|
||||||
|
"13",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"class_type": "EmptySD3LatentImage",
|
||||||
|
"inputs": {
|
||||||
|
"width": [
|
||||||
|
"14",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"height": [
|
||||||
|
"14",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"batch_size": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"class_type": "BasicScheduler",
|
||||||
|
"inputs": {
|
||||||
|
"model": [
|
||||||
|
"2",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"scheduler": "simple",
|
||||||
|
"steps": 4,
|
||||||
|
"denoise": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"class_type": "BasicGuider",
|
||||||
|
"inputs": {
|
||||||
|
"model": [
|
||||||
|
"2",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"conditioning": [
|
||||||
|
"5",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"class_type": "RandomNoise",
|
||||||
|
"inputs": {
|
||||||
|
"noise_seed": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"class_type": "KSamplerSelect",
|
||||||
|
"inputs": {
|
||||||
|
"sampler_name": "euler"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"class_type": "SamplerCustomAdvanced",
|
||||||
|
"inputs": {
|
||||||
|
"noise": [
|
||||||
|
"6",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"guider": [
|
||||||
|
"20",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"sampler": [
|
||||||
|
"8",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"sigmas": [
|
||||||
|
"1",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"latent_image": [
|
||||||
|
"7",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"class_type": "VAEDecode",
|
||||||
|
"inputs": {
|
||||||
|
"samples": [
|
||||||
|
"9",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"vae": [
|
||||||
|
"3",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"62": {
|
||||||
|
"class_type": "ColorMatch",
|
||||||
|
"inputs": {
|
||||||
|
"image_ref": [
|
||||||
|
"26",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"image_target": [
|
||||||
|
"10",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"method": "mkl",
|
||||||
|
"strength": 1,
|
||||||
|
"multithread": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"17": {
|
||||||
|
"class_type": "SaveImage",
|
||||||
|
"inputs": {
|
||||||
|
"images": [
|
||||||
|
"62",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"filename_prefix": "hair_inpaint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+450
-1
@@ -1 +1,450 @@
|
|||||||
{"1": {"inputs": {"scheduler": "simple", "steps": 4, "denoise": 1, "model": ["2", 0]}, "class_type": "BasicScheduler", "_meta": {"title": "基本调度器"}}, "2": {"inputs": {"max_shift": 1.15, "base_shift": 0.5, "width": ["14", 0], "height": ["14", 1], "model": ["16", 0]}, "class_type": "ModelSamplingFlux", "_meta": {"title": "采样算法(Flux)"}}, "3": {"inputs": {"vae_name": "flux2-vae.safetensors"}, "class_type": "VAELoader", "_meta": {"title": "加载VAE"}}, "5": {"inputs": {"conditioning": ["19", 0], "latent": ["13", 0]}, "class_type": "ReferenceLatent", "_meta": {"title": "参考Latent"}}, "6": {"inputs": {"noise_seed": 217742615722421}, "class_type": "RandomNoise", "_meta": {"title": "随机噪波"}}, "7": {"inputs": {"width": ["14", 0], "height": ["14", 1], "batch_size": 1}, "class_type": "EmptySD3LatentImage", "_meta": {"title": "空Latent图像(SD3)"}}, "8": {"inputs": {"sampler_name": "euler"}, "class_type": "KSamplerSelect", "_meta": {"title": "K采样器选择"}}, "9": {"inputs": {"noise": ["6", 0], "guider": ["20", 0], "sampler": ["8", 0], "sigmas": ["1", 0], "latent_image": ["7", 0]}, "class_type": "SamplerCustomAdvanced", "_meta": {"title": "自定义采样器(高级)"}}, "10": {"inputs": {"samples": ["9", 0], "vae": ["3", 0]}, "class_type": "VAEDecode", "_meta": {"title": "VAE解码"}}, "13": {"inputs": {"pixels": ["44", 0], "vae": ["3", 0]}, "class_type": "VAEEncode", "_meta": {"title": "VAE编码"}}, "14": {"inputs": {"image": ["44", 0]}, "class_type": "GetImageSize+", "_meta": {"title": "🔧 Get Image Size"}}, "16": {"inputs": {"unet_name": "flux-2-klein-4b-fp8.safetensors", "weight_dtype": "fp8_e4m3fn_fast"}, "class_type": "UNETLoader", "_meta": {"title": "UNet加载器"}}, "17": {"inputs": {"filename_prefix": "ComfyUI", "images": ["62", 0]}, "class_type": "SaveImage", "_meta": {"title": "保存图像"}}, "19": {"inputs": {"guidance": 1, "conditioning": ["22", 0]}, "class_type": "FluxGuidance", "_meta": {"title": "Flux引导"}}, "20": {"inputs": {"model": ["2", 0], "conditioning": ["5", 0]}, "class_type": "BasicGuider", "_meta": {"title": "基本引导器"}}, "22": {"inputs": {"text": ["60", 0], "clip": ["61", 0]}, "class_type": "CLIPTextEncode", "_meta": {"title": "CLIP文本编码"}}, "26": {"inputs": {"image": "clipspace/clipspace-painted-masked-1781655058499.png [input]"}, "class_type": "LoadImage", "_meta": {"title": "加载图像"}}, "31": {"inputs": {"image": ["26", 0]}, "class_type": "easy imageSize", "_meta": {"title": "图像尺寸"}}, "32": {"inputs": {"aspect_ratio": "custom", "proportional_width": ["31", 0], "proportional_height": ["31", 1], "fit": "letterbox", "method": "lanczos", "round_to_multiple": "8", "scale_to_side": "None", "scale_to_length": 1024, "background_color": "#000000", "image": ["26", 0], "mask": ["37", 0]}, "class_type": "LayerUtility: ImageScaleByAspectRatio V2", "_meta": {"title": "LayerUtility: ImageScaleByAspectRatio V2"}}, "33": {"inputs": {"masks": ["26", 1]}, "class_type": "Mask Fill Holes", "_meta": {"title": "Mask Fill Holes"}}, "36": {"inputs": {"masks": ["33", 0]}, "class_type": "Convert Masks to Images", "_meta": {"title": "Convert Masks to Images"}}, "37": {"inputs": {"method": "intensity", "image": ["39", 0]}, "class_type": "Image To Mask", "_meta": {"title": "Image To Mask"}}, "39": {"inputs": {"upscale_method": "nearest-exact", "width": ["31", 0], "height": ["31", 1], "crop": "disabled", "image": ["36", 0]}, "class_type": "ImageScale", "_meta": {"title": "缩放图像"}}, "44": {"inputs": {"mask_opacity": 1, "mask_color": "FFFF00", "pass_through": true, "image": ["32", 0], "mask": ["32", 1]}, "class_type": "ImageAndMaskPreview", "_meta": {"title": "ImageAndMaskPreview"}}, "45": {"inputs": {"images": ["44", 0]}, "class_type": "PreviewImage", "_meta": {"title": "预览图像"}}, "53": {"inputs": {"rgthree_comparer": {"images": [{"name": "A", "selected": true, "url": "/api/view?filename=rgthree.compare._temp_zuixa_00119_.png&type=temp&subfolder=&rand=0.7926013627811991"}, {"name": "B", "selected": true, "url": "/api/view?filename=rgthree.compare._temp_zuixa_00120_.png&type=temp&subfolder=&rand=0.43692946883795813"}]}, "image_a": ["62", 0], "image_b": ["26", 0]}, "class_type": "Image Comparer (rgthree)", "_meta": {"title": "Image Comparer (rgthree)"}}, "60": {"inputs": {"text": "补充遮罩区补充遮罩区域内的头发,头发填满遮罩区域。发际线往下挡住额头"}, "class_type": "JjkText", "_meta": {"title": "Text"}}, "61": {"inputs": {"clip_name": "qwen_3_4b.safetensors", "type": "flux2", "device": "default"}, "class_type": "CLIPLoader", "_meta": {"title": "加载CLIP"}}, "62": {"inputs": {"method": "mkl", "strength": 1, "multithread": true, "image_ref": ["26", 0], "image_target": ["10", 0]}, "class_type": "ColorMatch", "_meta": {"title": "Color Match"}}}
|
{
|
||||||
|
"1": {
|
||||||
|
"inputs": {
|
||||||
|
"scheduler": "simple",
|
||||||
|
"steps": 4,
|
||||||
|
"denoise": 1,
|
||||||
|
"model": [
|
||||||
|
"2",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "BasicScheduler",
|
||||||
|
"_meta": {
|
||||||
|
"title": "基本调度器"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"inputs": {
|
||||||
|
"max_shift": 1.15,
|
||||||
|
"base_shift": 0.5,
|
||||||
|
"width": [
|
||||||
|
"14",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"height": [
|
||||||
|
"14",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"model": [
|
||||||
|
"16",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "ModelSamplingFlux",
|
||||||
|
"_meta": {
|
||||||
|
"title": "采样算法(Flux)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"inputs": {
|
||||||
|
"vae_name": "flux2-vae.safetensors"
|
||||||
|
},
|
||||||
|
"class_type": "VAELoader",
|
||||||
|
"_meta": {
|
||||||
|
"title": "加载VAE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"inputs": {
|
||||||
|
"conditioning": [
|
||||||
|
"19",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"latent": [
|
||||||
|
"13",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "ReferenceLatent",
|
||||||
|
"_meta": {
|
||||||
|
"title": "参考Latent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"inputs": {
|
||||||
|
"noise_seed": 217742615722421
|
||||||
|
},
|
||||||
|
"class_type": "RandomNoise",
|
||||||
|
"_meta": {
|
||||||
|
"title": "随机噪波"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"inputs": {
|
||||||
|
"width": [
|
||||||
|
"14",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"height": [
|
||||||
|
"14",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"batch_size": 1
|
||||||
|
},
|
||||||
|
"class_type": "EmptySD3LatentImage",
|
||||||
|
"_meta": {
|
||||||
|
"title": "空Latent图像(SD3)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"inputs": {
|
||||||
|
"sampler_name": "euler"
|
||||||
|
},
|
||||||
|
"class_type": "KSamplerSelect",
|
||||||
|
"_meta": {
|
||||||
|
"title": "K采样器选择"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"inputs": {
|
||||||
|
"noise": [
|
||||||
|
"6",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"guider": [
|
||||||
|
"20",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"sampler": [
|
||||||
|
"8",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"sigmas": [
|
||||||
|
"1",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"latent_image": [
|
||||||
|
"7",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "SamplerCustomAdvanced",
|
||||||
|
"_meta": {
|
||||||
|
"title": "自定义采样器(高级)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"inputs": {
|
||||||
|
"samples": [
|
||||||
|
"9",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"vae": [
|
||||||
|
"3",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "VAEDecode",
|
||||||
|
"_meta": {
|
||||||
|
"title": "VAE解码"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"13": {
|
||||||
|
"inputs": {
|
||||||
|
"pixels": [
|
||||||
|
"44",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"vae": [
|
||||||
|
"3",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "VAEEncode",
|
||||||
|
"_meta": {
|
||||||
|
"title": "VAE编码"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"14": {
|
||||||
|
"inputs": {
|
||||||
|
"image": [
|
||||||
|
"44",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "GetImageSize+",
|
||||||
|
"_meta": {
|
||||||
|
"title": "🔧 Get Image Size"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"16": {
|
||||||
|
"inputs": {
|
||||||
|
"unet_name": "flux-2-klein-4b-fp8.safetensors",
|
||||||
|
"weight_dtype": "fp8_e4m3fn_fast"
|
||||||
|
},
|
||||||
|
"class_type": "UNETLoader",
|
||||||
|
"_meta": {
|
||||||
|
"title": "UNet加载器"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"17": {
|
||||||
|
"inputs": {
|
||||||
|
"filename_prefix": "ComfyUI",
|
||||||
|
"images": [
|
||||||
|
"62",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "SaveImage",
|
||||||
|
"_meta": {
|
||||||
|
"title": "保存图像"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"19": {
|
||||||
|
"inputs": {
|
||||||
|
"guidance": 1,
|
||||||
|
"conditioning": [
|
||||||
|
"22",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "FluxGuidance",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Flux引导"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"inputs": {
|
||||||
|
"model": [
|
||||||
|
"2",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"conditioning": [
|
||||||
|
"5",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "BasicGuider",
|
||||||
|
"_meta": {
|
||||||
|
"title": "基本引导器"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"22": {
|
||||||
|
"inputs": {
|
||||||
|
"text": [
|
||||||
|
"60",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"clip": [
|
||||||
|
"61",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "CLIPTextEncode",
|
||||||
|
"_meta": {
|
||||||
|
"title": "CLIP文本编码"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"26": {
|
||||||
|
"inputs": {
|
||||||
|
"image": "clipspace/clipspace-painted-masked-1781655058499.png [input]"
|
||||||
|
},
|
||||||
|
"class_type": "LoadImage",
|
||||||
|
"_meta": {
|
||||||
|
"title": "加载图像"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"31": {
|
||||||
|
"inputs": {
|
||||||
|
"image": [
|
||||||
|
"26",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "easy imageSize",
|
||||||
|
"_meta": {
|
||||||
|
"title": "图像尺寸"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"32": {
|
||||||
|
"inputs": {
|
||||||
|
"aspect_ratio": "custom",
|
||||||
|
"proportional_width": [
|
||||||
|
"31",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"proportional_height": [
|
||||||
|
"31",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"fit": "letterbox",
|
||||||
|
"method": "lanczos",
|
||||||
|
"round_to_multiple": "8",
|
||||||
|
"scale_to_side": "None",
|
||||||
|
"scale_to_length": 1024,
|
||||||
|
"background_color": "#000000",
|
||||||
|
"image": [
|
||||||
|
"26",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"mask": [
|
||||||
|
"37",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "LayerUtility: ImageScaleByAspectRatio V2",
|
||||||
|
"_meta": {
|
||||||
|
"title": "LayerUtility: ImageScaleByAspectRatio V2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"33": {
|
||||||
|
"inputs": {
|
||||||
|
"masks": [
|
||||||
|
"26",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "Mask Fill Holes",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Mask Fill Holes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"36": {
|
||||||
|
"inputs": {
|
||||||
|
"masks": [
|
||||||
|
"33",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "Convert Masks to Images",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Convert Masks to Images"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"37": {
|
||||||
|
"inputs": {
|
||||||
|
"method": "intensity",
|
||||||
|
"image": [
|
||||||
|
"39",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "Image To Mask",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Image To Mask"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"39": {
|
||||||
|
"inputs": {
|
||||||
|
"upscale_method": "nearest-exact",
|
||||||
|
"width": [
|
||||||
|
"31",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"height": [
|
||||||
|
"31",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"crop": "disabled",
|
||||||
|
"image": [
|
||||||
|
"36",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "ImageScale",
|
||||||
|
"_meta": {
|
||||||
|
"title": "缩放图像"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"44": {
|
||||||
|
"inputs": {
|
||||||
|
"mask_opacity": 1,
|
||||||
|
"mask_color": "FFFF00",
|
||||||
|
"pass_through": true,
|
||||||
|
"image": [
|
||||||
|
"32",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"mask": [
|
||||||
|
"32",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "ImageAndMaskPreview",
|
||||||
|
"_meta": {
|
||||||
|
"title": "ImageAndMaskPreview"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"45": {
|
||||||
|
"inputs": {
|
||||||
|
"images": [
|
||||||
|
"44",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "PreviewImage",
|
||||||
|
"_meta": {
|
||||||
|
"title": "预览图像"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"53": {
|
||||||
|
"inputs": {
|
||||||
|
"rgthree_comparer": {
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"name": "A",
|
||||||
|
"selected": true,
|
||||||
|
"url": "/api/view?filename=rgthree.compare._temp_zuixa_00119_.png&type=temp&subfolder=&rand=0.7926013627811991"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "B",
|
||||||
|
"selected": true,
|
||||||
|
"url": "/api/view?filename=rgthree.compare._temp_zuixa_00120_.png&type=temp&subfolder=&rand=0.43692946883795813"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"image_a": [
|
||||||
|
"62",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"image_b": [
|
||||||
|
"26",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "Image Comparer (rgthree)",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Image Comparer (rgthree)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"60": {
|
||||||
|
"inputs": {
|
||||||
|
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
||||||
|
},
|
||||||
|
"class_type": "JjkText",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"61": {
|
||||||
|
"inputs": {
|
||||||
|
"clip_name": "qwen_3_4b.safetensors",
|
||||||
|
"type": "flux2",
|
||||||
|
"device": "cpu"
|
||||||
|
},
|
||||||
|
"class_type": "CLIPLoader",
|
||||||
|
"_meta": {
|
||||||
|
"title": "加载CLIP"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"62": {
|
||||||
|
"inputs": {
|
||||||
|
"method": "mkl",
|
||||||
|
"strength": 1,
|
||||||
|
"multithread": true,
|
||||||
|
"image_ref": [
|
||||||
|
"26",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"image_target": [
|
||||||
|
"10",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "ColorMatch",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Color Match"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -757,7 +757,7 @@ async def hair_grow(
|
|||||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"),
|
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"),
|
||||||
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
|
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
|
||||||
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
|
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
|
||||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
||||||
):
|
):
|
||||||
# 1. gender 必填校验(非法/缺失 → 1004)
|
# 1. gender 必填校验(非法/缺失 → 1004)
|
||||||
if gender not in ("male", "female"):
|
if gender not in ("male", "female"):
|
||||||
@@ -810,117 +810,15 @@ async def hair_grow(
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 接口 7:C 端生发 v2(add_hair2.json 工作流)
|
# 接口 7:C 端生发 v2 —— 已弃用(add_hair2.json 用 Klein-9b 大模型,会把常驻的
|
||||||
|
# Klein-4b/Flux 挤出显存,导致接口2/3/5 耗时抖动;且业务已不再调用)。
|
||||||
|
# 保留路由返回明确错误,避免老客户端拿到裸 404。
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_WORKFLOW2_PATH = os.path.join(os.path.dirname(__file__), "add_hair2.json")
|
@app.post("/api/v1/hair/grow-v2", include_in_schema=False, deprecated=True)
|
||||||
|
async def hair_grow_v2():
|
||||||
|
"""接口7 已弃用:请改用 /api/v1/hair/grow(接口2)。"""
|
||||||
@app.post(
|
return err(1007, "接口7(/api/v1/hair/grow-v2)已弃用,请使用 /api/v1/hair/grow")
|
||||||
"/api/v1/hair/grow-v2",
|
|
||||||
summary="接口7 C端生发 v2(add_hair2 工作流)",
|
|
||||||
tags=["生发"],
|
|
||||||
description=f"""
|
|
||||||
输入用户正面照 + **性别** + **发型序号**,使用 add_hair2.json 工作流生成指定发际线类型的预览图与生发图。
|
|
||||||
功能与接口2 完全一致,仅 ComfyUI 工作流不同。
|
|
||||||
|
|
||||||
{_image_fields_desc}
|
|
||||||
|
|
||||||
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- **gender**(必填):`male` / `female`。决定返回的贴图集合(female 5 张 / male 4 张)。
|
|
||||||
非法或缺失返回 `1004`。
|
|
||||||
- **hair_style**(必填):`int`,发型序号。`female`:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave;
|
|
||||||
`male`:1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界返回 `1007`。
|
|
||||||
- **beauty_enabled**:本期保留但不生效。
|
|
||||||
|
|
||||||
`hairline_type` 取值:`ellipse` / `flower` / `heart` / `straight` / `wave`(female),
|
|
||||||
`ellipse` / `m` / `straight` / `inverse_arc`(male)。
|
|
||||||
""",
|
|
||||||
responses={
|
|
||||||
200: {
|
|
||||||
"description": "成功",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"example": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "success",
|
|
||||||
"request_id": "mock-request-id",
|
|
||||||
"data": {
|
|
||||||
"results": [
|
|
||||||
{"image_base64": "iVBORw0KGgo...", "hairline_type": "ellipse", "order": 1},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
400: {
|
|
||||||
"description": "参数错误 / 图片识别失败",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"examples": {
|
|
||||||
"图片参数错误": {"value": {"code": 1007, "message": "图片参数错误:必须且只能传 image_file / image_url / image_base64 其中一个", "request_id": "x", "data": None}},
|
|
||||||
"非正面照": {"value": {"code": 1003, "message": "角度问题,请上传正面照", "request_id": "x", "data": None}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
async def hair_grow_v2(
|
|
||||||
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG)"),
|
|
||||||
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
|
||||||
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
|
||||||
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
|
||||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"),
|
|
||||||
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
|
|
||||||
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
|
|
||||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
|
||||||
):
|
|
||||||
# 1. gender 必填校验(非法/缺失 → 1004)
|
|
||||||
if gender not in ("male", "female"):
|
|
||||||
return err(1004, "gender 必填且只能为 male / female")
|
|
||||||
|
|
||||||
# 2. hair_style 必填校验(解析逗号分隔,越界 → 1007)
|
|
||||||
max_styles = {"female": 5, "male": 4}[gender]
|
|
||||||
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
|
||||||
if hair_styles is None:
|
|
||||||
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
|
||||||
|
|
||||||
# 3. 三选一取图
|
|
||||||
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
|
|
||||||
if e is not None:
|
|
||||||
return e
|
|
||||||
|
|
||||||
image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)
|
|
||||||
if image is None:
|
|
||||||
return err(1008, "图片格式不支持(仅 JPG / PNG)")
|
|
||||||
|
|
||||||
try:
|
|
||||||
from fastapi.concurrency import run_in_threadpool
|
|
||||||
from hairline.service import generate_grow_results
|
|
||||||
|
|
||||||
# 预览 + 生发(ComfyUI) 都是阻塞且较慢,放线程池避免卡住事件循环
|
|
||||||
items = await run_in_threadpool(generate_grow_results, image, gender, use_mask, prompt, hair_styles, _WORKFLOW2_PATH)
|
|
||||||
if items is None:
|
|
||||||
return err(1001, "无法识别人像")
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for p in items:
|
|
||||||
results.append({
|
|
||||||
"image_base64": _jpg_b64(p["image_bgr"]), # 预览图 JPG
|
|
||||||
"grown_image_base64": (_png_to_jpg_b64(p["grown_png"]) # 生发图 JPG
|
|
||||||
if p["grown_png"] else None),
|
|
||||||
"hairline_type": p["hairline_type"],
|
|
||||||
"order": p["order"],
|
|
||||||
})
|
|
||||||
return ok({"results": results})
|
|
||||||
except Exception as ex: # noqa: BLE001
|
|
||||||
logger.exception("接口7 处理异常")
|
|
||||||
return err(1007, f"处理失败:{ex}")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -970,7 +868,7 @@ async def hair_grow_b(
|
|||||||
marked_image_url: Optional[str] = Form(default=None, description="划线图片 URL"),
|
marked_image_url: Optional[str] = Form(default=None, description="划线图片 URL"),
|
||||||
marked_image_base64: Optional[str] = Form(default=None, description="划线图片 base64"),
|
marked_image_base64: Optional[str] = Form(default=None, description="划线图片 base64"),
|
||||||
use_mask: bool = Form(default=True, description="是否画发际线(测试对比用)。false 时跳过划线检测、直接送划线图"),
|
use_mask: bool = Form(default=True, description="是否画发际线(测试对比用)。false 时跳过划线检测、直接送划线图"),
|
||||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
||||||
):
|
):
|
||||||
# 划线图三选一取图(只需这一张)
|
# 划线图三选一取图(只需这一张)
|
||||||
marked_raw, e = await resolve_image_bytes(marked_image_file, marked_image_url, marked_image_base64)
|
marked_raw, e = await resolve_image_bytes(marked_image_file, marked_image_url, marked_image_base64)
|
||||||
@@ -1178,7 +1076,7 @@ async def hairline_generate(
|
|||||||
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
||||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-5 male:1-4"),
|
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-5 male:1-4"),
|
||||||
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,测试对比用)"),
|
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,测试对比用)"),
|
||||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
|
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
|
||||||
):
|
):
|
||||||
if gender not in ("male", "female"):
|
if gender not in ("male", "female"):
|
||||||
return err(1004, "gender 必填且只能为 male / female")
|
return err(1004, "gender 必填且只能为 male / female")
|
||||||
@@ -1570,7 +1468,7 @@ async def hairline_grow_v2(
|
|||||||
inpainting_fill: int = Form(default=1, description="change_hair服务端重绘填充:0=保留原图 | 1=噪声 | 2=纯色 | 3=潜变量。默认 1"),
|
inpainting_fill: int = Form(default=1, description="change_hair服务端重绘填充:0=保留原图 | 1=噪声 | 2=纯色 | 3=潜变量。默认 1"),
|
||||||
mask_blur: int = Form(default=11, description="change_hair服务端遮罩边缘模糊像素,默认 11"),
|
mask_blur: int = Form(default=11, description="change_hair服务端遮罩边缘模糊像素,默认 11"),
|
||||||
mask_dilate_scale: float = Form(default=1.0, description="change_hair服务端遮罩膨胀缩放,默认 1.0"),
|
mask_dilate_scale: float = Form(default=1.0, description="change_hair服务端遮罩膨胀缩放,默认 1.0"),
|
||||||
comfyui_prompt: Optional[str] = Form(default=None, description="Flux-2 重绘提示词,None 用默认「填充遮罩区域的头发,皮肤加一点磨皮」"),
|
comfyui_prompt: Optional[str] = Form(default=None, description="Flux-2 重绘提示词,None 用默认「填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜」"),
|
||||||
beauty_alpha: float = Form(default=0.6, description="redraw_band 版 band 外的全脸美颜融入强度(0=band外无美颜纯用final,1≈整帧版),默认 0.6"),
|
beauty_alpha: float = Form(default=0.6, description="redraw_band 版 band 外的全脸美颜融入强度(0=band外无美颜纯用final,1≈整帧版),默认 0.6"),
|
||||||
band_lo_mult: float = Form(default=0.5, description="重绘带外推倍率下限(相对 hairline_push_cm,内轮廓=0×、原外推线=1.0×),默认 0.5"),
|
band_lo_mult: float = Form(default=0.5, description="重绘带外推倍率下限(相对 hairline_push_cm,内轮廓=0×、原外推线=1.0×),默认 0.5"),
|
||||||
band_hi_mult: float = Form(default=1.5, description="重绘带外推倍率上限(相对 hairline_push_cm),默认 1.5"),
|
band_hi_mult: float = Form(default=1.5, description="重绘带外推倍率上限(相对 hairline_push_cm),默认 1.5"),
|
||||||
@@ -1736,7 +1634,7 @@ async def hairline_grow_v2_final_v2(
|
|||||||
async def api_redraw(
|
async def api_redraw(
|
||||||
image_file: UploadFile = File(..., description="人物图片(JPG/PNG)"),
|
image_file: UploadFile = File(..., description="人物图片(JPG/PNG)"),
|
||||||
mask_file: UploadFile = File(..., description="遮罩图片(PNG,支持红/白/alpha 格式)"),
|
mask_file: UploadFile = File(..., description="遮罩图片(PNG,支持红/白/alpha 格式)"),
|
||||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮",
|
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜",
|
||||||
description="ComfyUI 提示词"),
|
description="ComfyUI 提示词"),
|
||||||
):
|
):
|
||||||
image_bytes = await image_file.read()
|
image_bytes = await image_file.read()
|
||||||
|
|||||||
@@ -1107,7 +1107,7 @@ def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="seg
|
|||||||
"hairline_id": hairline_id,
|
"hairline_id": hairline_id,
|
||||||
"blend_method": blend_method,
|
"blend_method": blend_method,
|
||||||
"hairline_push_cm": round(float(hairline_push_cm), 2),
|
"hairline_push_cm": round(float(hairline_push_cm), 2),
|
||||||
"comfyui_prompt": comfyui_prompt or "补充遮罩区域的头发,加一点美颜",
|
"comfyui_prompt": comfyui_prompt or "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜",
|
||||||
"beauty_alpha": beauty_alpha,
|
"beauty_alpha": beauty_alpha,
|
||||||
"px_per_cm": round(float(px_per_cm), 4),
|
"px_per_cm": round(float(px_per_cm), 4),
|
||||||
"mask_pixels": mask_viz["mask_pixels"],
|
"mask_pixels": mask_viz["mask_pixels"],
|
||||||
|
|||||||
+1
-1
@@ -410,7 +410,7 @@
|
|||||||
},
|
},
|
||||||
"60": {
|
"60": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"text": "补充遮罩区域的头发,加一点美颜"
|
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
||||||
},
|
},
|
||||||
"class_type": "JjkText",
|
"class_type": "JjkText",
|
||||||
"_meta": {
|
"_meta": {
|
||||||
|
|||||||
+8
-3
@@ -86,11 +86,13 @@ def _get_output_node(workflow_path: str | None = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def run(rgba_png_bytes: bytes, timeout: float = COMFY_TIMEOUT, prompt: str = None,
|
def run(rgba_png_bytes: bytes, timeout: float = COMFY_TIMEOUT, prompt: str = None,
|
||||||
workflow_path: str | None = None) -> bytes:
|
workflow_path: str | None = None, front: bool = False) -> bytes:
|
||||||
"""提交一次生发任务,返回输出 PNG 字节。失败抛异常。
|
"""提交一次生发任务,返回输出 PNG 字节。失败抛异常。
|
||||||
|
|
||||||
prompt:非 None 时替换工作流节点60(JjkText)的文本;None 时用工作流内置默认提示词。
|
prompt:非 None 时替换工作流节点60(JjkText)的文本;None 时用工作流内置默认提示词。
|
||||||
workflow_path:工作流 JSON 路径,None 则用默认 add_hair.json。
|
workflow_path:工作流 JSON 路径,None 则用默认 add_hair.json。
|
||||||
|
front:True 时任务插到 ComfyUI 队列最前(server 端 "front" 字段,队列号取负)。
|
||||||
|
接口2 对时延敏感用 True,避免排在接口3/5 的批量任务后面;其余接口保持 False。
|
||||||
"""
|
"""
|
||||||
path = workflow_path or _WORKFLOW_DEFAULT
|
path = workflow_path or _WORKFLOW_DEFAULT
|
||||||
output_node = _get_output_node(path)
|
output_node = _get_output_node(path)
|
||||||
@@ -134,8 +136,11 @@ def run(rgba_png_bytes: bytes, timeout: float = COMFY_TIMEOUT, prompt: str = Non
|
|||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 3. 提交
|
# 3. 提交(front=True 时插队到队列最前)
|
||||||
r = cli.post("/prompt", json={"prompt": wf, "client_id": client_id})
|
payload = {"prompt": wf, "client_id": client_id}
|
||||||
|
if front:
|
||||||
|
payload["front"] = True
|
||||||
|
r = cli.post("/prompt", json=payload)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
prompt_id = r.json()["prompt_id"]
|
prompt_id = r.json()["prompt_id"]
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -16,7 +16,7 @@ from . import comfyui
|
|||||||
|
|
||||||
logger = logging.getLogger("hair.worker")
|
logger = logging.getLogger("hair.worker")
|
||||||
|
|
||||||
_DEFAULT_PROMPT = "填充遮罩区域的头发,皮肤加一点磨皮"
|
_DEFAULT_PROMPT = "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
||||||
_REPO = os.path.dirname(os.path.dirname(__file__))
|
_REPO = os.path.dirname(os.path.dirname(__file__))
|
||||||
_REPAINT_WORKFLOW = os.path.join(_REPO, "0716add-hair-api.json")
|
_REPAINT_WORKFLOW = os.path.join(_REPO, "0716add-hair-api.json")
|
||||||
|
|
||||||
@@ -55,14 +55,16 @@ def _process_mask_to_rgba(image_bytes: bytes, mask_bytes: bytes) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def run_redraw(image_bytes: bytes, mask_bytes: bytes,
|
def run_redraw(image_bytes: bytes, mask_bytes: bytes,
|
||||||
prompt: str | None = None, timeout: float = 300.0) -> bytes:
|
prompt: str | None = None, timeout: float = 300.0,
|
||||||
|
front: bool = False) -> bytes:
|
||||||
"""直接调 ComfyUI 重绘 — 替代 local_test /api/generate。
|
"""直接调 ComfyUI 重绘 — 替代 local_test /api/generate。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
image_bytes: 人物图片字节(JPG/PNG)
|
image_bytes: 人物图片字节(JPG/PNG)
|
||||||
mask_bytes: 遮罩图片字节(支持红/白/alpha 遮罩格式)
|
mask_bytes: 遮罩图片字节(支持红/白/alpha 遮罩格式)
|
||||||
prompt: 提示词,None 用默认 "填充遮罩区域的头发,皮肤加一点磨皮"
|
prompt: 提示词,None 用默认 "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
||||||
timeout: ComfyUI 超时秒数
|
timeout: ComfyUI 超时秒数
|
||||||
|
front: True 时任务插到 ComfyUI 队列最前(接口2 时延敏感路径用)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
重绘后的 PNG 图片字节
|
重绘后的 PNG 图片字节
|
||||||
@@ -73,4 +75,4 @@ def run_redraw(image_bytes: bytes, mask_bytes: bytes,
|
|||||||
"""
|
"""
|
||||||
rgba_png = _process_mask_to_rgba(image_bytes, mask_bytes)
|
rgba_png = _process_mask_to_rgba(image_bytes, mask_bytes)
|
||||||
return comfyui.run(rgba_png, timeout=timeout, prompt=prompt,
|
return comfyui.run(rgba_png, timeout=timeout, prompt=prompt,
|
||||||
workflow_path=_REPAINT_WORKFLOW)
|
workflow_path=_REPAINT_WORKFLOW, front=front)
|
||||||
|
|||||||
+9
-4
@@ -46,7 +46,7 @@ _BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black")
|
|||||||
# 关键:ComfyUI 单卡显存装不下 Flux(7.7G)+qwen CLIP(3.9G) 同驻,靠缓存 CLIP 文本条件避免重载。
|
# 关键:ComfyUI 单卡显存装不下 Flux(7.7G)+qwen CLIP(3.9G) 同驻,靠缓存 CLIP 文本条件避免重载。
|
||||||
# prompt 不同会使缓存失效 → 重载 CLIP 并挤出 Flux(每次 +4s)。三接口用同一字符串即可全程命中。
|
# prompt 不同会使缓存失效 → 重载 CLIP 并挤出 Flux(每次 +4s)。三接口用同一字符串即可全程命中。
|
||||||
# 与 app.py 接口2/接口3 的默认 prompt 保持一致;可用 REDRAW_PROMPT 覆盖。
|
# 与 app.py 接口2/接口3 的默认 prompt 保持一致;可用 REDRAW_PROMPT 覆盖。
|
||||||
_REDRAW_PROMPT = os.getenv("REDRAW_PROMPT", "填充遮罩区域的头发,皮肤加一点磨皮")
|
_REDRAW_PROMPT = os.getenv("REDRAW_PROMPT", "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
||||||
|
|
||||||
# 接口2 女重绘整条管线(swapHair + ComfyUI)送模型前限边。真实照片常达 1257x1495:
|
# 接口2 女重绘整条管线(swapHair + ComfyUI)送模型前限边。真实照片常达 1257x1495:
|
||||||
# 全分辨率 ComfyUI 重绘要 13~21s 且激活显存把模型挤出。女性路径含 swapHair(SD WebUI ~5.3s
|
# 全分辨率 ComfyUI 重绘要 13~21s 且激活显存把模型挤出。女性路径含 swapHair(SD WebUI ~5.3s
|
||||||
@@ -77,8 +77,9 @@ def _call_local_redraw(image_png_bytes, mask_png_bytes, timeout=300.0):
|
|||||||
mask_png_bytes = cv2.imencode(".png", msk_s)[1].tobytes()
|
mask_png_bytes = cv2.imencode(".png", msk_s)[1].tobytes()
|
||||||
logger.info("接口2女 缩图送 Comfy: %dx%d → %dx%d (max_side=%d)",
|
logger.info("接口2女 缩图送 Comfy: %dx%d → %dx%d (max_side=%d)",
|
||||||
orig_w, orig_h, nw, nh, _REDRAW_MAX_SIDE)
|
orig_w, orig_h, nw, nh, _REDRAW_MAX_SIDE)
|
||||||
|
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前,避免排在接口3/5 的批量任务后面
|
||||||
out = run_redraw(image_png_bytes, mask_png_bytes, timeout=timeout,
|
out = run_redraw(image_png_bytes, mask_png_bytes, timeout=timeout,
|
||||||
prompt=_REDRAW_PROMPT)
|
prompt=_REDRAW_PROMPT, front=True)
|
||||||
if scale < 1.0 and out:
|
if scale < 1.0 and out:
|
||||||
out = _upscale_png_to(out, orig_w, orig_h)
|
out = _upscale_png_to(out, orig_w, orig_h)
|
||||||
return out
|
return out
|
||||||
@@ -233,7 +234,9 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T
|
|||||||
img_s, msk_s, gsc = _prep_comfy_input(image_bgr, np.zeros((h, w), np.uint8))
|
img_s, msk_s, gsc = _prep_comfy_input(image_bgr, np.zeros((h, w), np.uint8))
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
compose_comfy_rgba(img_s, msk_s).save(buf, format="PNG", compress_level=1)
|
compose_comfy_rgba(img_s, msk_s).save(buf, format="PNG", compress_level=1)
|
||||||
shared_grown = comfyui.run(buf.getvalue(), prompt=prompt, workflow_path=workflow_path)
|
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前
|
||||||
|
shared_grown = comfyui.run(buf.getvalue(), prompt=prompt,
|
||||||
|
workflow_path=workflow_path, front=True)
|
||||||
if gsc < 1.0 and shared_grown:
|
if gsc < 1.0 and shared_grown:
|
||||||
shared_grown = _upscale_png_to(shared_grown, w, h)
|
shared_grown = _upscale_png_to(shared_grown, w, h)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
@@ -256,7 +259,9 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T
|
|||||||
m_s, msk_s, gsc = _prep_comfy_input(marked, mask)
|
m_s, msk_s, gsc = _prep_comfy_input(marked, mask)
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
compose_comfy_rgba(m_s, msk_s).save(buf, format="PNG", compress_level=1)
|
compose_comfy_rgba(m_s, msk_s).save(buf, format="PNG", compress_level=1)
|
||||||
grown_png = comfyui.run(buf.getvalue(), prompt=prompt, workflow_path=workflow_path)
|
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前
|
||||||
|
grown_png = comfyui.run(buf.getvalue(), prompt=prompt,
|
||||||
|
workflow_path=workflow_path, front=True)
|
||||||
if gsc < 1.0 and grown_png:
|
if gsc < 1.0 and grown_png:
|
||||||
grown_png = _upscale_png_to(grown_png, w, h)
|
grown_png = _upscale_png_to(grown_png, w, h)
|
||||||
except Exception as e: # noqa: BLE001 单张失败不拖垮整请求
|
except Exception as e: # noqa: BLE001 单张失败不拖垮整请求
|
||||||
|
|||||||
+1
-1
@@ -206,7 +206,7 @@ def generate():
|
|||||||
return jsonify({"error": msg}), 400
|
return jsonify({"error": msg}), 400
|
||||||
image_file = request.files["image"]
|
image_file = request.files["image"]
|
||||||
mask_file = request.files["mask"]
|
mask_file = request.files["mask"]
|
||||||
prompt_text = request.form.get("prompt", "填充遮罩区域的头发,皮肤加一点磨皮")
|
prompt_text = request.form.get("prompt", "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
||||||
log.info(
|
log.info(
|
||||||
"收到请求: image=%s mask=%s prompt=%r",
|
"收到请求: image=%s mask=%s prompt=%r",
|
||||||
image_file.filename, mask_file.filename, prompt_text,
|
image_file.filename, mask_file.filename, prompt_text,
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ def prep_and_upload():
|
|||||||
|
|
||||||
|
|
||||||
def run_once(fname, model, dtype, steps):
|
def run_once(fname, model, dtype, steps):
|
||||||
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮")
|
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
||||||
wf["16"]["inputs"]["unet_name"] = model
|
wf["16"]["inputs"]["unet_name"] = model
|
||||||
wf["16"]["inputs"]["weight_dtype"] = dtype
|
wf["16"]["inputs"]["weight_dtype"] = dtype
|
||||||
wf["1"]["inputs"]["steps"] = steps
|
wf["1"]["inputs"]["steps"] = steps
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def upload(scale=1.0):
|
|||||||
|
|
||||||
|
|
||||||
def run(fname, steps):
|
def run(fname, steps):
|
||||||
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮")
|
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
||||||
wf["16"]["inputs"]["unet_name"] = MODEL
|
wf["16"]["inputs"]["unet_name"] = MODEL
|
||||||
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
||||||
wf["1"]["inputs"]["steps"] = steps
|
wf["1"]["inputs"]["steps"] = steps
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ SEED = 123456789
|
|||||||
imgs = []
|
imgs = []
|
||||||
labels = []
|
labels = []
|
||||||
for steps in [2, 3, 4, 6]:
|
for steps in [2, 3, 4, 6]:
|
||||||
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮", seed=SEED)
|
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", seed=SEED)
|
||||||
wf["16"]["inputs"]["unet_name"] = MODEL
|
wf["16"]["inputs"]["unet_name"] = MODEL
|
||||||
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
||||||
wf["1"]["inputs"]["steps"] = steps
|
wf["1"]["inputs"]["steps"] = steps
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ button { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer;
|
|||||||
</div>
|
</div>
|
||||||
<div class="controls" style="margin-top:16px">
|
<div class="controls" style="margin-top:16px">
|
||||||
<label>提示词:</label>
|
<label>提示词:</label>
|
||||||
<input type="text" id="promptInput" value="填充遮罩区域的头发,皮肤加一点磨皮">
|
<input type="text" id="promptInput" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜">
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:center; margin-top:16px">
|
<div style="text-align:center; margin-top:16px">
|
||||||
<button class="btn-generate" id="generateBtn" disabled>🚀 生成</button>
|
<button class="btn-generate" id="generateBtn" disabled>🚀 生成</button>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ resp = requests.post(
|
|||||||
"image": ("original.jpg", img_data, "image/jpeg"),
|
"image": ("original.jpg", img_data, "image/jpeg"),
|
||||||
"mask": ("mask.png", mask_data, "image/png"),
|
"mask": ("mask.png", mask_data, "image/png"),
|
||||||
},
|
},
|
||||||
data={"prompt": "填充遮罩区域的头发,皮肤加一点磨皮"},
|
data={"prompt": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"},
|
||||||
timeout=600,
|
timeout=600,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="upload-row" style="margin-top:10px">
|
<div class="upload-row" style="margin-top:10px">
|
||||||
<label style="font-size:13px;font-weight:600;white-space:nowrap">重绘提示词</label>
|
<label style="font-size:13px;font-weight:600;white-space:nowrap">重绘提示词</label>
|
||||||
<input type="text" id="localTestPrompt" value="填充遮罩区域的头发,皮肤加一点磨皮" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
<input type="text" id="localTestPrompt" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
||||||
</div>
|
</div>
|
||||||
<div class="upload-row" style="margin-top:10px">
|
<div class="upload-row" style="margin-top:10px">
|
||||||
<label style="font-size:13px;font-weight:600;white-space:nowrap">X-Internal-Token</label>
|
<label style="font-size:13px;font-weight:600;white-space:nowrap">X-Internal-Token</label>
|
||||||
@@ -354,7 +354,7 @@ function dataUriToBlob(dataUri) {
|
|||||||
|
|
||||||
async function runLocalRedraw() {
|
async function runLocalRedraw() {
|
||||||
if (!_finalDataUri || !_maskDataUri) { setStatus('缺少 final 或遮罩,请先生成', 'error'); return; }
|
if (!_finalDataUri || !_maskDataUri) { setStatus('缺少 final 或遮罩,请先生成', 'error'); return; }
|
||||||
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发,皮肤加一点磨皮';
|
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜';
|
||||||
const btn = $('redrawBtn');
|
const btn = $('redrawBtn');
|
||||||
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
|
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
|
||||||
flog('===== 调后端重绘 =====', 'info');
|
flog('===== 调后端重绘 =====', 'info');
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="upload-row" style="margin-top:10px">
|
<div class="upload-row" style="margin-top:10px">
|
||||||
<label style="font-size:13px;font-weight:600;white-space:nowrap">重绘提示词</label>
|
<label style="font-size:13px;font-weight:600;white-space:nowrap">重绘提示词</label>
|
||||||
<input type="text" id="localTestPrompt" value="填充遮罩区域的头发,皮肤加一点磨皮" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
<input type="text" id="localTestPrompt" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
||||||
</div>
|
</div>
|
||||||
<div class="params">
|
<div class="params">
|
||||||
<div class="pf">
|
<div class="pf">
|
||||||
@@ -169,7 +169,7 @@ async function submitTest() {
|
|||||||
|
|
||||||
async function runLocalRedraw() {
|
async function runLocalRedraw() {
|
||||||
if (!_finalDataUri || !_maskDataUri) { setStatus('缺少 final 或遮罩,请先生成', 'error'); return; }
|
if (!_finalDataUri || !_maskDataUri) { setStatus('缺少 final 或遮罩,请先生成', 'error'); return; }
|
||||||
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发,皮肤加一点磨皮';
|
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜';
|
||||||
const btn = $('redrawBtn');
|
const btn = $('redrawBtn');
|
||||||
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
|
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
<div class="hint">JPG/PNG | 生发图生成较慢(数十秒~数分钟),请耐心等待</div>
|
<div class="hint">JPG/PNG | 生发图生成较慢(数十秒~数分钟),请耐心等待</div>
|
||||||
<div style="margin-top:10px;display:flex;align-items:center;gap:8px">
|
<div style="margin-top:10px;display:flex;align-items:center;gap:8px">
|
||||||
<label style="font-size:13px;font-weight:600;color:#374151;white-space:nowrap">💬 提示词</label>
|
<label style="font-size:13px;font-weight:600;color:#374151;white-space:nowrap">💬 提示词</label>
|
||||||
<input type="text" id="promptInput" value="补充遮罩区域的头发,加一点美颜" style="flex:1;padding:8px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;max-width:500px">
|
<input type="text" id="promptInput" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" style="flex:1;padding:8px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;max-width:500px">
|
||||||
</div>
|
</div>
|
||||||
<div id="statusBar" class="status hidden"></div>
|
<div id="statusBar" class="status hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="upload-group" style="margin-top:14px">
|
<div class="upload-group" style="margin-top:14px">
|
||||||
<div class="label">💬 提示词(prompt)</div>
|
<div class="label">💬 提示词(prompt)</div>
|
||||||
<input type="text" id="promptInput" value="补充遮罩区域的头发,加一点美颜" style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:14px;max-width:500px">
|
<input type="text" id="promptInput" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:14px;max-width:500px">
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:14px;display:flex;gap:12px;align-items:center">
|
<div style="margin-top:14px;display:flex;gap:12px;align-items:center">
|
||||||
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<div class="hint">JPG/PNG | 生发图生成较慢(数十秒~数分钟),请耐心等待 | 工作流: add_hair2.json</div>
|
<div class="hint">JPG/PNG | 生发图生成较慢(数十秒~数分钟),请耐心等待 | 工作流: add_hair2.json</div>
|
||||||
<div style="margin-top:10px;display:flex;align-items:center;gap:8px">
|
<div style="margin-top:10px;display:flex;align-items:center;gap:8px">
|
||||||
<label style="font-size:13px;font-weight:600;color:#374151;white-space:nowrap">💬 提示词</label>
|
<label style="font-size:13px;font-weight:600;color:#374151;white-space:nowrap">💬 提示词</label>
|
||||||
<input type="text" id="promptInput" value="补充遮罩区域的头发,加一点美颜" style="flex:1;padding:8px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;max-width:500px">
|
<input type="text" id="promptInput" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" style="flex:1;padding:8px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;max-width:500px">
|
||||||
</div>
|
</div>
|
||||||
<div id="statusBar" class="status hidden"></div>
|
<div id="statusBar" class="status hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user