From 5fbc03a6df5d1ccf3b90d0962e5ca7f1f4a7f9e0 Mon Sep 17 00:00:00 2001 From: xsl Date: Fri, 17 Jul 2026 00:00:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=8F=A312/=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?2=20=E5=8F=91=E9=99=85=E7=BA=BF=E9=87=8D=E7=BB=98=E6=94=B9?= =?UTF-8?q?=E8=B5=B0=20local=5Ftest=20=E5=A4=96=E9=83=A8=20ComfyUI=20?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端 generate_hairline_redraw 跳过内置 Flux-2 重绘,改为产出 final(接缝融合基底) + 纯红遮罩 PNG(redraw_band_mask_base64,遮罩区=(255,0,0,255)、其余全透明)。 接口2 female(generate_grow_results_swap) 取 final+遮罩后在后端调 local_test (0716add-hair.json 工作流) 完成重绘,结果作为生发图返回;male 分支不变。 测试页 test_interface12.html / test_interface12_final.html 改为两阶段: 先生成 final+纯红遮罩,再前端调 local_test 重绘并展示;color_match 默认不勾选。 local_test/app.py 加 CORS 头(OPTIONS 预检),支持浏览器跨域直连。 --- .gitignore | 4 + 0716add-hair.json | 1 + face_analysis/hairline_grow.py | 74 ++++++++-------- hairline/service.py | 55 +++++++++--- local_test/app.py | 21 +++++ local_test/hair_service.log | 4 - local_test/start.sh | 4 +- static/test_interface12.html | 136 ++++++++++++++++++++++------- static/test_interface12_final.html | 93 +++++++++++++++++--- 9 files changed, 291 insertions(+), 101 deletions(-) create mode 100644 0716add-hair.json delete mode 100644 local_test/hair_service.log diff --git a/.gitignore b/.gitignore index 2f2f0e8..7df2e12 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,7 @@ image/test/ # 批量报告输出(生成图+原图,体积大,不入 git) static/report_hairline_v2/ static/report_hairline_v2.zip + +# local_test 运行期日志 / pid(不入 git) +local_test/hair_service.log +local_test/hair_service.pid diff --git a/0716add-hair.json b/0716add-hair.json new file mode 100644 index 0000000..bbc94fd --- /dev/null +++ b/0716add-hair.json @@ -0,0 +1 @@ +{"id":"431f1189-2c6a-448d-925e-5f6c85416393","revision":0,"last_node_id":63,"last_link_id":104,"nodes":[{"id":37,"type":"Image To Mask","pos":[330.00017074563834,-900.0000465455873],"size":[280,110],"flags":{},"order":13,"mode":0,"inputs":[{"label":"图像","localized_name":"图像","name":"image","type":"IMAGE","link":47},{"label":"方法","localized_name":"方法","name":"method","type":"COMBO","widget":{"name":"method"},"link":null}],"outputs":[{"label":"遮罩","localized_name":"遮罩","name":"MASK","type":"MASK","links":[79]}],"properties":{"Node name for S&R":"Image To Mask","cnr_id":"masquerade-nodes-comfyui","ver":"432cb4d146a391b387a0cd25ace824328b5b61cf","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":["intensity"],"color":"#223","bgcolor":"#335"},{"id":33,"type":"Mask Fill Holes","pos":[320.0000351119579,-1320.0000465455873],"size":[290,80],"flags":{},"order":9,"mode":0,"inputs":[{"label":"遮罩","localized_name":"遮罩","name":"masks","type":"MASK","link":38}],"outputs":[{"label":"遮罩","localized_name":"遮罩","name":"MASKS","type":"MASK","links":[41]}],"properties":{"cnr_id":"was-node-suite-comfyui","ver":"1cd8d304eda256c412b8589ce1f00be3c61cf9ec","Node name for S&R":"Mask Fill Holes","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":[],"color":"#223","bgcolor":"#335"},{"id":32,"type":"LayerUtility: ImageScaleByAspectRatio V2","pos":[650.0000351119579,-1399.9999109119071],"size":[320,480],"flags":{},"order":14,"mode":0,"inputs":[{"label":"图像","localized_name":"image","name":"image","shape":7,"type":"IMAGE","link":101},{"label":"遮罩","localized_name":"mask","name":"mask","shape":7,"type":"MASK","link":79},{"localized_name":"aspect_ratio","name":"aspect_ratio","type":"COMBO","widget":{"name":"aspect_ratio"},"link":null},{"label":"proportional_width","localized_name":"proportional_width","name":"proportional_width","type":"INT","widget":{"name":"proportional_width"},"link":55},{"label":"proportional_height","localized_name":"proportional_height","name":"proportional_height","type":"INT","widget":{"name":"proportional_height"},"link":56},{"localized_name":"fit","name":"fit","type":"COMBO","widget":{"name":"fit"},"link":null},{"localized_name":"method","name":"method","type":"COMBO","widget":{"name":"method"},"link":null},{"localized_name":"round_to_multiple","name":"round_to_multiple","type":"COMBO","widget":{"name":"round_to_multiple"},"link":null},{"localized_name":"scale_to_side","name":"scale_to_side","type":"COMBO","widget":{"name":"scale_to_side"},"link":null},{"localized_name":"scale_to_length","name":"scale_to_length","type":"INT","widget":{"name":"scale_to_length"},"link":null},{"localized_name":"background_color","name":"background_color","type":"STRING","widget":{"name":"background_color"},"link":null}],"outputs":[{"label":"图像","localized_name":"image","name":"image","type":"IMAGE","links":[57]},{"label":"遮罩","localized_name":"mask","name":"mask","type":"MASK","links":[58]},{"label":"原始大小","localized_name":"original_size","name":"original_size","type":"BOX"},{"label":"width","localized_name":"width","name":"width","type":"INT"},{"label":"height","localized_name":"height","name":"height","type":"INT"}],"properties":{"cnr_id":"comfyui_layerstyle","ver":"a46b1e6d26d45be9784c49f7065ba44700ef2b63","Node name for S&R":"LayerUtility: ImageScaleByAspectRatio V2","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}}},"widgets_values":["custom",1,1,"letterbox","lanczos","8","None",1024,"#000000"],"color":"rgba(38, 73, 116, 0.7)"},{"id":14,"type":"GetImageSize+","pos":[1400.0001707456386,-930.0000465455873],"size":[320,120.15625],"flags":{},"order":17,"mode":0,"inputs":[{"label":"图像","localized_name":"图像","name":"image","type":"IMAGE","link":62}],"outputs":[{"label":"宽度","localized_name":"宽度","name":"width","type":"INT","links":[3,7]},{"label":"高度","localized_name":"高度","name":"height","type":"INT","links":[4,8]},{"label":"count","localized_name":"count","name":"count","type":"INT"}],"properties":{"cnr_id":"comfyui_essentials","ver":"9d9f4bedfc9f0321c19faf71855e228c93bd0dc9","Node name for S&R":"GetImageSize+","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":[],"color":"#223","bgcolor":"#335"},{"id":13,"type":"VAEEncode","pos":[1409.9998994782773,-1100.0000465455873],"size":[320,100],"flags":{},"order":18,"mode":0,"inputs":[{"label":"图像","localized_name":"像素","name":"pixels","type":"IMAGE","link":63},{"label":"VAE","localized_name":"vae","name":"vae","type":"VAE","link":18}],"outputs":[{"label":"Latent","localized_name":"Latent","name":"LATENT","type":"LATENT","links":[6]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"VAEEncode","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":[],"color":"#223","bgcolor":"#335"},{"id":9,"type":"SamplerCustomAdvanced","pos":[2500.000170745638,-1220.0000465455873],"size":[270,330],"flags":{},"order":24,"mode":0,"inputs":[{"label":"噪波生成","localized_name":"噪波","name":"noise","type":"NOISE","link":9},{"label":"引导","localized_name":"引导器","name":"guider","type":"GUIDER","link":10},{"label":"采样器","localized_name":"采样器","name":"sampler","type":"SAMPLER","link":11},{"label":"Sigmas","localized_name":"西格玛","name":"sigmas","type":"SIGMAS","link":12},{"label":"Latent","localized_name":"Latent图像","name":"latent_image","type":"LATENT","link":13}],"outputs":[{"label":"输出","localized_name":"Latent","name":"output","type":"LATENT","slot_index":0,"links":[14]},{"label":"降噪输出","localized_name":"降噪Latent","name":"denoised_output","type":"LATENT"}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"SamplerCustomAdvanced","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":[],"color":"#223","bgcolor":"#335"},{"id":22,"type":"CLIPTextEncode","pos":[1300.0001707456386,-1709.999910911907],"size":[390,140],"flags":{},"order":7,"mode":0,"inputs":[{"label":"CLIP","localized_name":"clip","name":"clip","type":"CLIP","link":92},{"label":"text","localized_name":"文本","name":"text","type":"STRING","widget":{"name":"text"},"link":98}],"outputs":[{"label":"条件","localized_name":"条件","name":"CONDITIONING","type":"CONDITIONING","slot_index":0,"links":[24]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"CLIPTextEncode","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}}},"widgets_values":["The yellow part of the picture is replaced by a black bowl."],"color":"#232","bgcolor":"#353"},{"id":10,"type":"VAEDecode","pos":[2759.9998994782773,-1370.0000465455873],"size":[320,100],"flags":{},"order":25,"mode":0,"inputs":[{"label":"Latent","localized_name":"Latent","name":"samples","type":"LATENT","link":14},{"label":"VAE","localized_name":"vae","name":"vae","type":"VAE","link":15}],"outputs":[{"label":"图像","localized_name":"图像","name":"IMAGE","type":"IMAGE","slot_index":0,"links":[95]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"VAEDecode","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":[],"color":"#223","bgcolor":"#335"},{"id":39,"type":"ImageScale","pos":[320.0000351119579,-1150.0000465455873],"size":[290,210],"flags":{},"order":12,"mode":0,"inputs":[{"label":"图像","localized_name":"图像","name":"image","type":"IMAGE","link":44},{"localized_name":"缩放算法","name":"upscale_method","type":"COMBO","widget":{"name":"upscale_method"},"link":null},{"label":"width","localized_name":"宽度","name":"width","type":"INT","widget":{"name":"width"},"link":45},{"label":"height","localized_name":"高度","name":"height","type":"INT","widget":{"name":"height"},"link":46},{"localized_name":"裁剪","name":"crop","type":"COMBO","widget":{"name":"crop"},"link":null}],"outputs":[{"label":"图像","localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[47]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"ImageScale","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":["nearest-exact",512,512,"disabled"],"color":"#223","bgcolor":"#335"},{"id":36,"type":"Convert Masks to Images","pos":[320.0000351119579,-1250.0000465455873],"size":[290,80],"flags":{},"order":11,"mode":0,"inputs":[{"label":"遮罩","localized_name":"遮罩","name":"masks","type":"MASK","link":41}],"outputs":[{"label":"图像","localized_name":"图像","name":"IMAGES","type":"IMAGE","links":[44]}],"properties":{"cnr_id":"was-node-suite-comfyui","ver":"1cd8d304eda256c412b8589ce1f00be3c61cf9ec","Node name for S&R":"Convert Masks to Images","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":[],"color":"#223","bgcolor":"#335"},{"id":44,"type":"ImageAndMaskPreview","pos":[639.9998994782773,-990.0000465455873],"size":[290,200],"flags":{},"order":15,"mode":0,"inputs":[{"label":"图像","localized_name":"图像","name":"image","shape":7,"type":"IMAGE","link":57},{"label":"遮罩","localized_name":"遮罩","name":"mask","shape":7,"type":"MASK","link":58},{"label":"遮罩不透明","localized_name":"遮罩不透明","name":"mask_opacity","type":"FLOAT","widget":{"name":"mask_opacity"},"link":null},{"label":"遮罩颜色","localized_name":"遮罩颜色","name":"mask_color","type":"STRING","widget":{"name":"mask_color"},"link":null},{"label":"穿透","localized_name":"穿透","name":"pass_through","type":"BOOLEAN","widget":{"name":"pass_through"},"link":null}],"outputs":[{"label":"图像","localized_name":"图像","name":"composite","type":"IMAGE","links":[59,62,63]}],"properties":{"cnr_id":"comfyui-kjnodes","ver":"aeab1a7de581dc7253f48257392e8287bbeee4a9","Node name for S&R":"ImageAndMaskPreview","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":[1,"FFFF00",true],"color":"#223","bgcolor":"#335"},{"id":7,"type":"EmptySD3LatentImage","pos":[1789.9998994782773,-990.0000465455873],"size":[310,170],"flags":{},"order":20,"mode":0,"inputs":[{"label":"width","localized_name":"宽度","name":"width","type":"INT","widget":{"name":"width"},"link":7},{"label":"height","localized_name":"高度","name":"height","type":"INT","widget":{"name":"height"},"link":8},{"localized_name":"批量大小","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null}],"outputs":[{"label":"Latent","localized_name":"Latent","name":"LATENT","type":"LATENT","slot_index":0,"links":[13]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"EmptySD3LatentImage","ue_properties":{"version":"7.7","widget_ue_connectable":{"width":true,"height":true},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":[1024,1024,1],"color":"#223","bgcolor":"#335"},{"id":62,"type":"ColorMatch","pos":[2790.000170745638,-1210.0000465455873],"size":[270,200],"flags":{},"order":26,"mode":0,"inputs":[{"label":"参考图像","localized_name":"参考图像","name":"image_ref","type":"IMAGE","link":102},{"label":"目标图像","localized_name":"目标图像","name":"image_target","type":"IMAGE","link":95},{"label":"方法","localized_name":"方法","name":"method","type":"COMBO","widget":{"name":"method"},"link":null},{"label":"强度","localized_name":"强度","name":"strength","shape":7,"type":"FLOAT","widget":{"name":"strength"},"link":null},{"label":"多线程","localized_name":"多线程","name":"multithread","shape":7,"type":"BOOLEAN","widget":{"name":"multithread"},"link":null}],"outputs":[{"label":"图像","localized_name":"图像","name":"image","type":"IMAGE","links":[96,97]}],"properties":{"cnr_id":"comfyui-kjnodes","ver":"5b38397a6430fdb16c7bd14a6bd64c2b0e69a5f0","Node name for S&R":"ColorMatch","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.6.2"},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":["mkl",1,true],"color":"#223","bgcolor":"#335"},{"id":1,"type":"BasicScheduler","pos":[2150.000170745638,-1220.0000465455873],"size":[320,180],"flags":{},"order":22,"mode":0,"inputs":[{"label":"模型","localized_name":"模型","name":"model","type":"MODEL","link":1},{"localized_name":"调度器","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"步数","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"降噪","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"label":"Sigmas","localized_name":"Sigmas","name":"SIGMAS","type":"SIGMAS","links":[12]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"BasicScheduler","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":["simple",6,1],"color":"#223","bgcolor":"#335"},{"id":6,"type":"RandomNoise","pos":[1770.0001707456386,-1170.0000465455873],"size":[310,110],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"噪波随机种","name":"noise_seed","type":"INT","widget":{"name":"noise_seed"},"link":null}],"outputs":[{"label":"噪波生成","localized_name":"噪波","name":"NOISE","type":"NOISE","links":[9]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"RandomNoise","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}}},"widgets_values":[1109407891267066,"randomize"],"color":"#2a363b","bgcolor":"#3f5159"},{"id":8,"type":"KSamplerSelect","pos":[1750.0001707456386,-1290.0000465455873],"size":[320,110],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"采样器名称","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null}],"outputs":[{"label":"采样器","localized_name":"采样器","name":"SAMPLER","type":"SAMPLER","links":[11]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"KSamplerSelect","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":["euler"],"color":"#223","bgcolor":"#335"},{"id":31,"type":"easy imageSize","pos":[-29.999964888042086,-1380.0000465455873],"size":[290,170],"flags":{},"order":8,"mode":0,"inputs":[{"label":"图像","localized_name":"图像","name":"image","type":"IMAGE","link":100}],"outputs":[{"label":"宽度","localized_name":"宽度","name":"width_int","type":"INT","links":[45,55]},{"label":"高度","localized_name":"高度","name":"height_int","type":"INT","links":[46,56]}],"properties":{"cnr_id":"comfyui-easy-use","ver":"501d97bb5ca8d61b486d47379fb1f21a41b772d7","Node name for S&R":"easy imageSize","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":["Width: 767 , Height: 747"],"color":"#223","bgcolor":"#335"},{"id":20,"type":"BasicGuider","pos":[2170.000170745638,-1350.0000465455873],"size":[270,100],"flags":{},"order":23,"mode":0,"inputs":[{"label":"模型","localized_name":"模型","name":"model","type":"MODEL","link":25},{"label":"条件","localized_name":"条件","name":"conditioning","type":"CONDITIONING","link":104}],"outputs":[{"label":"引导","localized_name":"引导器","name":"GUIDER","type":"GUIDER","slot_index":0,"links":[10]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"BasicGuider","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":[],"color":"#223","bgcolor":"#335"},{"id":5,"type":"ReferenceLatent","pos":[1409.9998994782773,-1220.0000465455873],"size":[310,100],"flags":{},"order":21,"mode":0,"inputs":[{"label":"conditioning","localized_name":"条件","name":"conditioning","type":"CONDITIONING","link":5},{"label":"latent","localized_name":"Latent","name":"latent","shape":7,"type":"LATENT","link":6}],"outputs":[{"label":"CONDITIONING","localized_name":"条件","name":"CONDITIONING","type":"CONDITIONING","links":[104]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"ReferenceLatent","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":[],"color":"#223","bgcolor":"#335"},{"id":19,"type":"FluxGuidance","pos":[1420.0001707456386,-1350.0000465455873],"size":[310,110],"flags":{},"order":10,"mode":0,"inputs":[{"label":"条件","localized_name":"条件","name":"conditioning","type":"CONDITIONING","link":24},{"localized_name":"引导","name":"guidance","type":"FLOAT","widget":{"name":"guidance"},"link":null}],"outputs":[{"label":"条件","localized_name":"条件","name":"CONDITIONING","type":"CONDITIONING","slot_index":0,"links":[5]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"FluxGuidance","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}}},"widgets_values":[1],"color":"#233","bgcolor":"#355"},{"id":2,"type":"ModelSamplingFlux","pos":[2130.000170745638,-1020.0000465455873],"size":[320,200],"flags":{},"order":19,"mode":0,"inputs":[{"label":"模型","localized_name":"模型","name":"model","type":"MODEL","link":2},{"localized_name":"最大移位","name":"max_shift","type":"FLOAT","widget":{"name":"max_shift"},"link":null},{"localized_name":"基础移位","name":"base_shift","type":"FLOAT","widget":{"name":"base_shift"},"link":null},{"label":"width","localized_name":"宽度","name":"width","type":"INT","widget":{"name":"width"},"link":3},{"label":"height","localized_name":"高度","name":"height","type":"INT","widget":{"name":"height"},"link":4}],"outputs":[{"label":"模型","localized_name":"模型","name":"MODEL","type":"MODEL","slot_index":0,"links":[1,25]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"ModelSamplingFlux","ue_properties":{"version":"7.7","widget_ue_connectable":{"width":true,"height":true},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":[1.15,0.5,1024,1024],"color":"#223","bgcolor":"#335"},{"id":16,"type":"UNETLoader","pos":[1010.0000351119579,-1390.0000465455873],"size":[310,150],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"UNet名称","name":"unet_name","type":"COMBO","widget":{"name":"unet_name"},"link":null},{"localized_name":"数据类型","name":"weight_dtype","type":"COMBO","widget":{"name":"weight_dtype"},"link":null}],"outputs":[{"label":"模型","localized_name":"模型","name":"MODEL","type":"MODEL","slot_index":0,"links":[2]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"UNETLoader","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}}},"widgets_values":["flux2.0/flux-2-klein-9b-fp8.safetensors","fp8_e4m3fn"],"color":"#223","bgcolor":"#335"},{"id":3,"type":"VAELoader","pos":[1010.0000351119579,-1250.0000465455873],"size":[310,110],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"vae名称","name":"vae_name","type":"COMBO","widget":{"name":"vae_name"},"link":null}],"outputs":[{"label":"VAE","localized_name":"VAE","name":"VAE","type":"VAE","slot_index":0,"links":[15,18]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"VAELoader","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":["flux2-vae.safetensors"],"color":"#223","bgcolor":"#335"},{"id":61,"type":"CLIPLoader","pos":[1000.0000351119579,-1110.0000465455873],"size":[350,180],"flags":{},"order":4,"mode":0,"inputs":[{"label":"clip_name","localized_name":"CLIP名称","name":"clip_name","type":"COMBO","widget":{"name":"clip_name"},"link":null},{"label":"type","localized_name":"类型","name":"type","type":"COMBO","widget":{"name":"type"},"link":null},{"label":"device","localized_name":"设备","name":"device","shape":7,"type":"COMBO","widget":{"name":"device"},"link":null}],"outputs":[{"label":"CLIP","localized_name":"CLIP","name":"CLIP","type":"CLIP","links":[92]}],"properties":{"cnr_id":"comfy-core","ver":"0.8.2","Node name for S&R":"CLIPLoader","ue_properties":{"version":"7.2.2","widget_ue_connectable":{},"input_ue_unconnectable":{}},"hasSecondTab":false,"models":[{"name":"qwen_3_4b.safetensors","directory":"text_encoders","url":"https://huggingface.co/Comfy-Org/flux2-klein-4B/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors"}],"secondTabText":"Send Back","enableTabs":false,"secondTabOffset":80,"tabWidth":65,"secondTabWidth":65,"tabXOffset":10,"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"},"widget_ue_connectable":{"clip_name":true,"type":true,"device":true}},"widgets_values":["qwen_3_8b_fp8mixed.safetensors","flux2","default"],"color":"#223","bgcolor":"#335"},{"id":45,"type":"PreviewImage","pos":[710.0000351119579,-2120.000046545588],"size":[490,500],"flags":{},"order":16,"mode":0,"inputs":[{"label":"图像","localized_name":"图像","name":"images","type":"IMAGE","link":59}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"PreviewImage","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}}},"widgets_values":[],"color":"#233","bgcolor":"#355"},{"id":53,"type":"Image Comparer (rgthree)","pos":[1804.9843155517362,-2156.5215467241615],"size":[650,550],"flags":{},"order":28,"mode":0,"inputs":[{"dir":3,"label":"图像_A","name":"image_a","type":"IMAGE","link":97},{"dir":3,"label":"图像_B","name":"image_b","type":"IMAGE","link":75}],"outputs":[],"properties":{"cnr_id":"rgthree-comfy","ver":"84a146fee39f7b3a8c6631dcac29bc13b077eb49","comparer_mode":"Slide","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}}},"widgets_values":[[{"name":"A","selected":true,"url":"/api/view?filename=rgthree.compare._temp_nqvym_00029_.png&type=temp&subfolder=&rand=0.5791789168392755"},{"name":"B","selected":true,"url":"/api/view?filename=rgthree.compare._temp_nqvym_00030_.png&type=temp&subfolder=&rand=0.21946935876437912"}]],"color":"#2a363b","bgcolor":"#3f5159"},{"id":17,"type":"SaveImage","pos":[2480.000170745638,-2170.000046545588],"size":[570,580],"flags":{},"order":27,"mode":0,"inputs":[{"label":"图像","localized_name":"图片","name":"images","type":"IMAGE","link":96},{"localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.41","Node name for S&R":"SaveImage","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}}},"widgets_values":["ComfyUI"],"color":"#2a363b","bgcolor":"#3f5159"},{"id":60,"type":"JjkText","pos":[1290.0001707456386,-2100.000046545588],"size":[410,320],"flags":{},"order":5,"mode":0,"inputs":[{"label":"text","localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"label":"text","localized_name":"text","name":"text","type":"STRING","links":[98]}],"properties":{"Node name for S&R":"JjkText","cnr_id":"ComfyUI-Jjk-Nodes","ver":"b3c99bb78a99551776b5eab1a820e1cd58f84f31","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.7"},"ttNbgOverride":{"color":"#223","bgcolor":"#335","groupcolor":"#88A"}},"widgets_values":["填充遮罩区域的头发,皮肤加一点磨皮"],"color":"#223","bgcolor":"#335"},{"id":26,"type":"LoadImage","pos":[20.000035111957914,-2120.000046545588],"size":[590,530],"flags":{},"order":6,"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":[75,100,101,102]},{"label":"遮罩","localized_name":"遮罩","name":"MASK","type":"MASK","links":[38]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"LoadImage","ue_properties":{"version":"7.7","widget_ue_connectable":{},"input_ue_unconnectable":{}},"#sdppp_variant":"default","#sdppp_simple_content":"canvas","#sdppp_simple_mask":"canvas","#sdppp_simple_boundary":"canvas","#sdppp_label":"","image":"clipspace/clipspace-painted-masked-1784202952306.png [input]"},"widgets_values":["clipspace/clipspace-painted-masked-1784202952306.png [input]","image"],"color":"#2a363b","bgcolor":"#3f5159"}],"links":[[1,2,0,1,0,"MODEL"],[2,16,0,2,0,"MODEL"],[3,14,0,2,3,"INT"],[4,14,1,2,4,"INT"],[5,19,0,5,0,"CONDITIONING"],[6,13,0,5,1,"LATENT"],[7,14,0,7,0,"INT"],[8,14,1,7,1,"INT"],[9,6,0,9,0,"NOISE"],[10,20,0,9,1,"GUIDER"],[11,8,0,9,2,"SAMPLER"],[12,1,0,9,3,"SIGMAS"],[13,7,0,9,4,"LATENT"],[14,9,0,10,0,"LATENT"],[15,3,0,10,1,"VAE"],[18,3,0,13,1,"VAE"],[24,22,0,19,0,"CONDITIONING"],[25,2,0,20,0,"MODEL"],[38,26,1,33,0,"MASK"],[41,33,0,36,0,"MASK"],[44,36,0,39,0,"IMAGE"],[45,31,0,39,2,"INT"],[46,31,1,39,3,"INT"],[47,39,0,37,0,"IMAGE"],[55,31,0,32,3,"INT"],[56,31,1,32,4,"INT"],[57,32,0,44,0,"IMAGE"],[58,32,1,44,1,"MASK"],[59,44,0,45,0,"IMAGE"],[62,44,0,14,0,"IMAGE"],[63,44,0,13,0,"IMAGE"],[75,26,0,53,1,"IMAGE"],[79,37,0,32,1,"MASK"],[92,61,0,22,0,"CLIP"],[95,10,0,62,1,"IMAGE"],[96,62,0,17,0,"IMAGE"],[97,62,0,53,0,"IMAGE"],[98,60,0,22,1,"STRING"],[100,26,0,31,0,"IMAGE"],[101,26,0,32,0,"IMAGE"],[102,26,0,62,0,"IMAGE"],[104,5,0,20,1,"CONDITIONING"]],"groups":[{"id":2,"title":"图片上传(图片上右键open in maskeditor画遮罩范围,不用涂)","bounding":[-10,-2270,1249.2113037109375,726.7728881835938],"color":"#A88","font_size":45,"flags":{}},{"id":5,"title":"提示词【很重要】","bounding":[1260,-2260,481.13201904296875,714.9009399414062],"color":"#8A8","font_size":24,"flags":{}},{"id":6,"title":"最终效果区","bounding":[1760,-2260,1330.0355224609375,711.3678588867188],"color":"#3f789e","font_size":24,"flags":{}},{"id":7,"title":"核心工作区","bounding":[990,-1480,379.9303894042969,546.5068969726562],"color":"#3f789e","font_size":24,"flags":{}},{"id":9,"title":"Group","bounding":[310,-1470,635.491455078125,641.5743408203125],"color":"#3f789e","font_size":24,"flags":{}},{"id":10,"title":"Group","bounding":[-50,-1460,321.03936767578125,291.3662414550781],"color":"#3f789e","font_size":24,"flags":{}},{"id":11,"title":"Group","bounding":[1390,-1470,1745.5640869140625,621.2001342773438],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"links_added_by_ue":[],"VHS_KeepIntermediate":true,"ue_links":[],"VHS_MetadataImage":true,"0246.VERSION":[0,0,4],"VHS_latentpreviewrate":0,"frontendVersion":"1.41.21","VHS_latentpreview":false,"ds":{"scale":0.45,"offset":[1726.6666011315049,2886.944475612862]},"workflowRendererVersion":"LG","favoritedWidgets":{"favorites":[{"nodeLocatorId":"26","widgetName":"$$canvas-image-preview"}]}},"version":0.4} \ No newline at end of file diff --git a/face_analysis/hairline_grow.py b/face_analysis/hairline_grow.py index 5cfc5f1..229143b 100644 --- a/face_analysis/hairline_grow.py +++ b/face_analysis/hairline_grow.py @@ -102,11 +102,26 @@ def _png_b64(bgr_or_gray): def _gray_b64(gray_float): - """0~1 的浮点图 → 灰度 PNG data URI。""" + """0~1 的浮字图 → 灰度 PNG data URI。""" g = np.clip(gray_float * 255.0, 0, 255).astype(np.uint8) return _png_b64(g) +def _red_mask_b64(mask_bool, h, w): + """布尔遮罩 → 纯红 alpha PNG data URI。 + 遮罩区域 RGBA=(255,0,0,255),其余区域 RGBA=(0,0,0,0)。 + 供外部 ComfyUI 重绘服务(如 local_test)按 alpha 通道识别重绘区。 + + 注意:cv2.imencode 写 PNG 用的是 **BGRA** 顺序(B,G,R,A),所以要得到 + 浏览器显示的红色 R=255,需赋值 (B=0,G=0,R=255,A=255)。 + """ + m = (mask_bool.astype(np.uint8)) * 255 if mask_bool is not None else np.zeros((h, w), np.uint8) + rgba = np.zeros((h, w, 4), np.uint8) + rgba[m > 0] = (0, 0, 255, 255) # BGRA: B=0,G=0,R=255 → PNG 读出为红色 + 不透明 + ok, buf = cv2.imencode(".png", rgba) + return "data:image/png;base64," + base64.b64encode(buf.tobytes()).decode() if ok else "" + + # --------------------------------------------------------------------------- # 步骤1:接口9 头发遮罩(复用 head_mask 构件) # --------------------------------------------------------------------------- @@ -1009,19 +1024,17 @@ def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="seg comfyui_prompt=None, beauty_alpha=0.6, band_lo_mult=0.5, band_hi_mult=1.5, rid=None): """接口12 发际线带重绘。内部先跑接口11 核心拿到 ④ final,再取 ⑤-① 发际线重绘带 - (外推↔内推之间、经 baseline 截断只留上部)作遮罩,用 Flux-2(ComfyUI,hair_repaint.json) - 重绘(band 经 alpha 送进 ComfyUI 决定加发位置,ComfyUI 输出为整帧重绘+美颜图)。 + (外推↔内推之间、经 baseline 截断只留上部)作遮罩。 - **同时产出两版结果供对比**: - - `redraw_full`:ComfyUI 整帧输出(全脸美颜 + 全脸重绘),与手动跑 ComfyUI 一致。 - - `redraw_band`:加发只在发际线带、美颜保留全脸。band 内完全用 ComfyUI 重绘(加发), - band 外用 `final` 结构 + 按 `beauty_alpha` 融入 ComfyUI 的全脸美颜。 + **本接口不再做 Flux-2 重绘**:只产出 `final`(接缝融合基底)+ 纯红遮罩 + `redraw_band_mask`(RGBA,遮罩区=(255,0,0,255)、其余全透明),重绘交给前端调 + 外部 ComfyUI 重绘服务(见 local_test)完成。旧的 `redraw_full` / `redraw_band` + 字段保留为空,仅作结构兼容。 返回可直接进 ok() 的 data dict。未检出人脸抛 NoFaceError。 - comfyui_prompt:Flux-2 提示词,None 用默认「补充遮罩区域的头发,加一点美颜」。 - beauty_alpha:redraw_band 版 band 外的全脸美颜融入强度(0=完全保留 final 无美颜, - 1=band 外也完全用 ComfyUI 输出≈redraw_full),默认 0.6。 + comfyui_prompt:保留入参,但本接口不再使用(重绘提示词由外部服务自行决定)。 + beauty_alpha:保留入参,但本接口不再使用(美颜由外部服务控制)。 band_lo_mult / band_hi_mult:重绘带外推倍率(相对 hairline_push_cm),带位于 lo×push ~ hi×push 之间(内轮廓=0×、原外推线=1.0×),默认 0.5 / 1.5。 其余参数含义与接口11 相同(用于内部生成 final 与重绘带)。 @@ -1050,8 +1063,7 @@ def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="seg upper_mask = mask_viz.get("_upper_mask") push_px = int(round(max(0.0, hairline_push_cm) * px_per_cm)) redraw_band_overlay_b64 = "" - redraw_full_b64 = "" # A:ComfyUI 整帧(全脸美颜+全脸重绘) - redraw_band_b64 = "" # B:加发只在发际线带、美颜保留全脸 + redraw_band_mask_b64 = "" # 纯红 alpha PNG(遮罩区=(255,0,0,255),其余全透明) redraw_info = {"enabled": False} band_mask = None try: @@ -1062,37 +1074,19 @@ def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="seg logger.info("[%s] 重绘带 push_px=%d lo_mult=%s hi_mult=%s band_pixels=%d", rid, push_px, band_lo_mult, band_hi_mult, int(band_mask.sum())) redraw_band_overlay_b64 = _jpg_b64(_overlay(final, band_mask, (255, 0, 255))) + # 纯红遮罩 PNG(供外部重绘服务按 alpha 识别重绘区) + redraw_band_mask_b64 = _red_mask_b64(band_mask, h, w) redraw_info = {"enabled": True, "band_pixels": int(band_mask.sum()), "push_px": push_px, "band_lo_mult": float(band_lo_mult), "band_hi_mult": float(band_hi_mult)} except Exception as ex: # noqa: BLE001 logger.exception("[%s] 重绘带计算失败,整个重绘跳过", rid) redraw_info = {"enabled": False, "error": f"band: {ex}"} - # ② Flux-2 路:final + band 调 ComfyUI(hair_repaint.json 整图重绘 + reference latent - # 保色 + ColorMatch + 美颜)。band 经 alpha 送进 ComfyUI 决定加发位置。 - beauty_alpha = float(min(max(beauty_alpha, 0.0), 1.0)) - if redraw_info.get("enabled"): - try: - prompt = comfyui_prompt if comfyui_prompt else "补充遮罩区域的头发,加一点美颜" - redraw_c_raw = _call_comfyui(final, band_mask, prompt=prompt) - # A:整帧输出(与手动跑 ComfyUI 一致) - redraw_full_b64 = _jpg_b64(redraw_c_raw) - # B:加发只在 band、美颜保留全脸。 - # alpha = band 内 1(羽化边缘);band 外 = beauty_alpha。 - # band 内完全用 ComfyUI(加发);band 外用 final 结构 + beauty_alpha 融入全脸美颜。 - feather_px = max(8, int(round(push_px * 0.6))) - band_a = _feather_alpha(band_mask, "feather", feather_px, 0) # 0..1,band 外=0 - a = band_a + (1.0 - band_a) * beauty_alpha - a3 = a[:, :, None] - band_mix = (final.astype(np.float32) * (1.0 - a3) - + redraw_c_raw.astype(np.float32) * a3) - redraw_band_b64 = _jpg_b64(np.clip(band_mix, 0, 255).astype(np.uint8)) - redraw_info["beauty_alpha"] = beauty_alpha - logger.info("[%s] Flux-2 重绘完成:redraw_full(整帧) + redraw_band(局部加发+全脸美颜 beauty_alpha=%.2f)", - rid, beauty_alpha) - except Exception as ex: # noqa: BLE001 - logger.warning("[%s] Flux-2 路重绘失败,跳过: %s", rid, ex) - redraw_info["c_error"] = str(ex) + # ② Flux-2 重绘已下线:本接口现在只产出 final(接缝融合基底)+ 纯红重绘带遮罩, + # 重绘交给前端调外部 ComfyUI 重绘服务(见 local_test)完成。 + # 下面保留 redraw_full_b64 / redraw_band_b64 为空,保持返回结构兼容(旧字段)。 + redraw_full_b64 = "" + redraw_band_b64 = "" t_redraw = time.time() - t0 data = { @@ -1116,9 +1110,11 @@ def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="seg "final_base64": _jpg_b64(final), # ⑤-① 发际线重绘带(紫,已按 baseline 截断只留上部) "redraw_band_overlay_base64": redraw_band_overlay_b64, - # A:ComfyUI 整帧重绘+美颜(与手动跑 ComfyUI 一致) + # ⑤-② 发际线重绘带遮罩(纯红 alpha PNG,遮罩区=(255,0,0,255)) + "redraw_band_mask_base64": redraw_band_mask_b64, + # A:ComfyUI 整帧重绘+美颜(已下线,保留空字段兼容旧前端) "redraw_full_base64": redraw_full_b64, - # B:加发只在发际线带、美颜保留全脸 + # B:加发只在发际线带、美颜保留全脸(已下线,保留空字段兼容旧前端) "redraw_band_base64": redraw_band_b64, # 兼容旧字段:指向 A(整帧版) "redraw_c_base64": redraw_full_b64, diff --git a/hairline/service.py b/hairline/service.py index 7418962..99667b4 100644 --- a/hairline/service.py +++ b/hairline/service.py @@ -40,6 +40,27 @@ _REPO = os.path.dirname(os.path.dirname(__file__)) _TEXTURE_DIR = os.path.join(_REPO, "hairline_texture") _BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black") +# 外部重绘服务(local_test,0716add-hair.json 工作流)。接口2 female 用它替代原 Flux-2 重绘。 +# 可用 HAIR_LOCAL_REDRAW_URL 覆盖(如线上改地址),默认 http://127.0.0.1:8899。 +_LOCAL_REDRAW_URL = os.getenv("HAIR_LOCAL_REDRAW_URL", "http://127.0.0.1:8899").rstrip("/") + + +def _call_local_redraw(image_png_bytes, mask_png_bytes, timeout=300.0): + """调外部重绘服务(local_test /api/generate):传 final 图 + 纯红遮罩 PNG, + 返回重绘后的 PNG bytes。失败抛异常(调用方负责 try/except 跳过)。 + """ + import requests + files = { + "image": ("final.jpg", image_png_bytes, "image/jpeg"), + "mask": ("mask.png", mask_png_bytes, "image/png"), + } + resp = requests.post(_LOCAL_REDRAW_URL + "/api/generate", files=files, timeout=timeout) + resp.raise_for_status() + if "image/" not in resp.headers.get("Content-Type", ""): + # 服务返回了 JSON 错误 + raise RuntimeError(f"local_test 返回非图片: {resp.text[:200]}") + return resp.content + # 发际线贴图档位:middle=默认(hairline_texture/),high/low 各自独立文件夹。 _TEXTURE_DIRS = { "middle": _TEXTURE_DIR, @@ -215,11 +236,12 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | None, redraw_defaults: dict): - """接口2 女性专用:发际线透明叠图(同 generate_grow_results)+ 换发型+Flux-2 整帧重绘图。 + """接口2 女性专用:发际线透明叠图(同 generate_grow_results)+ 换发型重绘图。 - 与 generate_grow_results 的差异仅在 grown 图的来源:这里对每个选中发型把 female key 映射到 - change_hair 的 chang_* hair_id,调 face_analysis.hairline_grow.generate_hairline_redraw - (= 接口12 final 管线,参数用 redraw_defaults),取 `redraw_full`(整帧重绘)作为生发图。 + grown 图来源(新流程):对每个选中发型把 female key 映射到 change_hair 的 chang_* hair_id, + 调 face_analysis.hairline_grow.generate_hairline_redraw(= 接口12 final 管线,参数用 + redraw_defaults)拿到 ④ final(接缝融合基底)+ ⑤-② 纯红遮罩 PNG,再**后端调外部重绘服务 + local_test**(0716add-hair.json 工作流)完成发际线带重绘,重绘结果作为生发图。 overlay 仍是发际线曲线透明层(与 generate_grow_results 完全一致)。 Returns: list[dict] {"hairline_type","order","overlay","grown_png"(jpg bytes 或 None)}; @@ -251,12 +273,25 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N else: try: data = generate_hairline_redraw(image_bgr, chang_id, **redraw_defaults) - b64 = (data.get("steps") or {}).get("redraw_full_base64") or "" - if b64.startswith("data:"): - b64 = b64.split(",", 1)[1] - grown_png = base64.b64decode(b64) if b64 else None - if grown_png is None: - logger.warning("接口2 换发型:type=%s 整帧重绘为空(可能 ComfyUI 未生效)", key) + steps = data.get("steps") or {} + # ④ final(接缝融合基底)+ ⑤-② 纯红遮罩 PNG + final_b64 = steps.get("final_base64") or "" + mask_b64 = steps.get("redraw_band_mask_base64") or "" + if not final_b64 or not mask_b64: + logger.warning("接口2 换发型:type=%s final/遮罩缺失(final=%d mask=%d)", + key, len(final_b64), len(mask_b64)) + else: + # 去掉 data URI 前缀 + if final_b64.startswith("data:"): + final_b64 = final_b64.split(",", 1)[1] + if mask_b64.startswith("data:"): + mask_b64 = mask_b64.split(",", 1)[1] + final_bytes = base64.b64decode(final_b64) + mask_bytes = base64.b64decode(mask_b64) + # 后端调外部重绘服务(local_test),返回重绘后的 PNG + grown_png = _call_local_redraw(final_bytes, mask_bytes) + if grown_png is None: + logger.warning("接口2 换发型:type=%s 重绘结果为空", key) except NoFaceError: logger.warning("接口2 换发型:type=%s 未检出人脸", key) except Exception as e: # noqa: BLE001 单张失败不拖垮整请求 diff --git a/local_test/app.py b/local_test/app.py index 8a92bf6..81c3b3c 100644 --- a/local_test/app.py +++ b/local_test/app.py @@ -12,6 +12,27 @@ from PIL import Image, ImageFilter app = Flask(__name__) COMFYUI_URL = "http://127.0.0.1:8188" +# 允许浏览器跨域直连本服务(如 hair 项目的测试页)。 +# 不引入 flask-cors 依赖,直接在响应头 + OPTIONS 预检里处理。 +_CORS_HEADERS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", +} + + +@app.after_request +def _add_cors(resp): + for k, v in _CORS_HEADERS.items(): + resp.headers[k] = v + return resp + + +@app.route("/api/generate", methods=["OPTIONS"]) +def _generate_preflight(): + """CORS 预检:浏览器 POST 前会先发 OPTIONS。""" + return ("", 204) + def build_workflow(image_filename, prompt_text, seed=None): """Build ComfyUI API workflow from the 0716add-hair.json structure.""" diff --git a/local_test/hair_service.log b/local_test/hair_service.log deleted file mode 100644 index 3babec2..0000000 --- a/local_test/hair_service.log +++ /dev/null @@ -1,4 +0,0 @@ - * Serving Flask app 'app' - * Debug mode: off -Address already in use -Port 8899 is in use by another program. Either identify and stop that program, or start the server with a different port. diff --git a/local_test/start.sh b/local_test/start.sh index 4832fef..19aec1b 100755 --- a/local_test/start.sh +++ b/local_test/start.sh @@ -21,12 +21,12 @@ fi # 检查 ComfyUI 是否运行 if ! curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8188/ >/dev/null 2>&1; then echo "警告: ComfyUI 未运行 (http://127.0.0.1:8188)" - echo "请先启动 ComfyUI: python /home/ubuntu/ComfyUI/main.py --listen" + echo "请先启动 ComfyUI: python /home/xsl/ComfyUI/main.py --listen" fi # 启动服务 echo "启动发型补全服务..." -/home/ubuntu/ComfyUI/venv/bin/python app.py >> "$LOG_FILE" 2>&1 & +/home/xsl/miniconda3/bin/python app.py >> "$LOG_FILE" 2>&1 & PID=$! echo "$PID" > "$PID_FILE" diff --git a/static/test_interface12.html b/static/test_interface12.html index 08b6b42..5c39a8e 100644 --- a/static/test_interface12.html +++ b/static/test_interface12.html @@ -47,22 +47,32 @@ .log-panel .lvl-err { color: #f48771; } .lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.85); display: none; align-items: center; justify-content: center; z-index: 50; cursor: zoom-out; } .lightbox img { max-width: 95%; max-height: 95%; } + .hidden { display: none; }
-

接口12 — 发际线带重绘 (Flux-2 保色重绘)

+

接口12 — 发际线带重绘 (final + 纯红遮罩 → 外部 ComfyUI 重绘)

- 内部先跑接口11拿到 ④接缝融合最终图(final),再取 ⑤-① 发际线重绘带(发际线外推 band_lo_mult×push ~ band_hi_mult×push、经 baseline 截断只留上部)作遮罩, - 调 Flux-2(ComfyUI) 保色重绘,重绘结果与 final 融合。
- 接口11 参数用于内部生成 final 与重绘带;comfyui_prompt 控制 Flux-2 提示词。⚠️ 需 ComfyUI(:8188) 在跑。 + 内部先跑接口11拿到 ④接缝融合最终图(final),再取 ⑤-① 发际线重绘带(发际线外推 band_lo_mult×push ~ band_hi_mult×push、经 baseline 截断只留上部)生成 + 纯红遮罩 PNG(遮罩区=(255,0,0,255),其余全透明)。
+ 前端拿到 final + 遮罩后,调 外部重绘服务(local_test,默认 :8899)完成重绘。⚠️ 需 ComfyUI(:8188) 在跑。 对照仅生成不重绘:接口11 调试页
- + + +
+
+ + +
+
+ +
@@ -99,7 +109,7 @@
-
multiband/feather 生效;seamless/two_stage 自带调色
+
multiband/feather 生效;seamless/two_stage 自带调色
@@ -119,23 +129,23 @@
- - + +
-
遮罩固定 pushed,重绘固定 Flux-2;隐藏参数(seg_model/hairline_edge/is_hr/edge_erode_px/mb_feather_px/transition_band_px/inpainting_fill/mask_blur/mask_dilate_scale)按原默认值随请求提交。换发型走 GPU + Flux-2 重绘,单次约 15~30s。
+
遮罩固定 pushed;后端只产出 final+纯红遮罩(不做重绘)。隐藏参数(seg_model/hairline_edge/is_hr/edge_erode_px/mb_feather_px/transition_band_px/inpainting_fill/mask_blur/mask_dilate_scale)按默认值随请求提交。beauty_alpha/comfyui_prompt 已不再生效。