feat(接口5): 生发机制对齐接口2 + 统一ComfyUI重绘提示词

接口5改造(生发机制与接口2一致,按性别分流):
- generate_hairline_pngs: 生发图来源从 _grow_from_texture(局部inpaint)
  改为按性别分流——female走 generate_grow_results_swap(swapHair+Flux整帧重绘),
  male走 generate_grow_results(ComfyUI add_hair)
- 新增参数 redraw_max_side/unet_name/v2_defaults(female路径透传)
- 接口5 handler 加 flux_model/redraw_max_side Form参数
- 保留接口5独有输出: 3档叠图(middle/high/low)/中心点/face_measure
- 已验证: female日志出现"接口2女 管线降分辨率max_side=640"+swap+Flux;
  male走add_hair.json; generate_grow_image=false正确跳过生发

统一ComfyUI重绘提示词:
- "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" → "填充遮罩区域的头发"
- 覆盖: _REDRAW_PROMPT/_DEFAULT_PROMPT常量 + app.py各接口Form默认 +
  工作流JSON节点60(add_hair/0716add-hair-api/hair_repaint) + 全部测试页输入框
This commit is contained in:
xsl
2026-07-27 23:36:15 +08:00
parent b354c8c627
commit b61ea6f33b
21 changed files with 77 additions and 51 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
"60": { "60": {
"class_type": "JjkText", "class_type": "JjkText",
"inputs": { "inputs": {
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" "text": "填充遮罩区域的头发"
} }
}, },
"22": { "22": {
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -410,7 +410,7 @@
}, },
"60": { "60": {
"inputs": { "inputs": {
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" "text": "填充遮罩区域的头发"
}, },
"class_type": "JjkText", "class_type": "JjkText",
"_meta": { "_meta": {
+18 -11
View File
@@ -775,7 +775,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的文本"),
flux_model: Optional[str] = Form(default=None, description="Flux 模型文件名(切换模型用)。None=工作流默认;如 flux-2-klein-9b-Q5_K_M.gguf / flux-2-klein-9b-Q4_K_M.gguf / flux2.0/flux-2-klein-9b-fp8.safetensors"), flux_model: Optional[str] = Form(default=None, description="Flux 模型文件名(切换模型用)。None=工作流默认;如 flux-2-klein-9b-Q5_K_M.gguf / flux-2-klein-9b-Q4_K_M.gguf / flux2.0/flux-2-klein-9b-fp8.safetensors"),
redraw_max_side: Optional[int] = Form(default=None, description="重绘压图长边像素。None=默认896;0=不缩图(原图直送);其他如 768/640/1024"), redraw_max_side: Optional[int] = Form(default=None, description="重绘压图长边像素。None=默认896;0=不缩图(原图直送);其他如 768/640/1024"),
): ):
@@ -890,7 +890,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)
@@ -1019,10 +1019,13 @@ async def face_features(
- 必填 `gender``male`/`female`),决定发型集合(female 5 / male 4)。 - 必填 `gender``male`/`female`),决定发型集合(female 5 / male 4)。
- 必填 `hair_style`(发型序号,逗号分隔如 `1,2,3`),决定返回哪些发际线类型。缺失/越界/非法返回 `1007`。 - 必填 `hair_style`(发型序号,逗号分隔如 `1,2,3`),决定返回哪些发际线类型。缺失/越界/非法返回 `1007`。
`female`1=ellipse,2=flower,3=heart,4=straight,5=wave`male`1=ellipse,2=inverse_arc,3=m,4=straight。 `female`1=ellipse,2=flower,3=heart,4=straight,5=wave`male`1=ellipse,2=inverse_arc,3=m,4=straight。
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数。 - 可选 `use_mask` / `prompt`:同接口2 的生发控制参数(仅 male 路径生效)
注:生发黑模板固定取 `hairline_texture_black/`middle 档),即三档叠图分别用各自贴图、但生发目标固定 middle - 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(最耗时)
- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(ComfyUI 生发,全流程最耗时)。
`false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,大幅降低耗时。 `false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,大幅降低耗时。
- **生发机制(同接口2,按性别分流)**:
`female` 走「换发型 + Flux-2 整帧重绘」(依赖 change_hair:8801 与 ComfyUI:8188);
`male` 走 ComfyUI `add_hair` 原生 inpaint。
- 可选 `flux_model` / `redraw_max_side`:同接口2(仅 female 路径生效)。
**返回说明** **返回说明**
@@ -1099,9 +1102,11 @@ async def hairline_generate(
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"), image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
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,仅 male 路径生效"),
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"), prompt: str = Form(default="填充遮罩区域的头发", description="ComfyUI 提示词(同接口2,仅 male 路径生效),会替换工作流节点60的文本"),
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(ComfyUI 生发,最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"), generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"),
flux_model: Optional[str] = Form(default=None, description="Flux 模型文件名(同接口2,切换模型用)。None=工作流默认"),
redraw_max_side: Optional[int] = Form(default=None, description="重绘压图长边像素(同接口2,仅 female 路径生效)。None=默认896;0=不缩图(原图直送);其他如 768/640/1024"),
): ):
if gender not in ("male", "female"): if gender not in ("male", "female"):
return err(1004, "gender 必填且只能为 male / female") return err(1004, "gender 必填且只能为 male / female")
@@ -1126,7 +1131,9 @@ async def hairline_generate(
res = await run_in_threadpool( res = await run_in_threadpool(
generate_hairline_pngs, image, gender, hair_styles, use_mask, prompt, generate_hairline_pngs, image, gender, hair_styles, use_mask, prompt,
generate_grow_image=generate_grow_image) generate_grow_image=generate_grow_image,
redraw_max_side=redraw_max_side, unet_name=flux_model,
v2_defaults=_V2_FINAL_DEFAULTS)
if res is None: if res is None:
return err(1001, "无法识别人像") return err(1001, "无法识别人像")
@@ -1494,7 +1501,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"),
@@ -1660,7 +1667,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()
+1 -1
View File
@@ -55,7 +55,7 @@ def gpu_used():
def call(img_path, hair_num, model_file, res_val): def call(img_path, hair_num, model_file, res_val):
fd = {"gender": "female", "hair_style": str(hair_num), "use_mask": "true", fd = {"gender": "female", "hair_style": str(hair_num), "use_mask": "true",
"prompt": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"} "prompt": "填充遮罩区域的头发"}
if model_file: if model_file:
fd["flux_model"] = model_file fd["flux_model"] = model_file
if res_val != "": if res_val != "":
+1 -1
View File
@@ -54,7 +54,7 @@ def call(img_path, model_file, res_val):
"gender": "female", "gender": "female",
"hair_style": "2", # 花瓣形 "hair_style": "2", # 花瓣形
"use_mask": "true", "use_mask": "true",
"prompt": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", "prompt": "填充遮罩区域的头发",
} }
if model_file: if model_file:
fd["flux_model"] = model_file fd["flux_model"] = model_file
+1 -1
View File
@@ -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
View File
@@ -410,7 +410,7 @@
}, },
"60": { "60": {
"inputs": { "inputs": {
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" "text": "填充遮罩区域的头发"
}, },
"class_type": "JjkText", "class_type": "JjkText",
"_meta": { "_meta": {
+2 -2
View File
@@ -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")
@@ -62,7 +62,7 @@ def run_redraw(image_bytes: bytes, mask_bytes: bytes,
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 时延敏感路径用) front: True 时任务插到 ComfyUI 队列最前(接口2 时延敏感路径用)
unet_name: 非 None 时切换 Flux 模型(如 flux-2-klein-9b-Q5_K_M.gguf),None 用工作流默认 unet_name: 非 None 时切换 Flux 模型(如 flux-2-klein-9b-Q5_K_M.gguf),None 用工作流默认
+37 -18
View File
@@ -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
@@ -410,14 +410,22 @@ def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None,
def generate_hairline_pngs(image_bgr: np.ndarray, gender: str, def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
hair_styles: list[int], use_mask: bool = True, hair_styles: list[int], use_mask: bool = True,
prompt: str | None = None, prompt: str | None = None,
generate_grow_image: bool = True): generate_grow_image: bool = True,
redraw_max_side: int | None = None,
unet_name: str | None = None,
v2_defaults: dict | None = None):
"""接口5:对选中发型返回 middle/high/low 三档发际线透明叠图 + 生发图(同接口2)。 """接口5:对选中发型返回 middle/high/low 三档发际线透明叠图 + 生发图(同接口2)。
入参同接口2:先选 gender,再多选 hair_styles(必填,1-indexed 按贴图排序)。 入参同接口2:先选 gender,再多选 hair_styles(必填,1-indexed 按贴图排序)。
每个选中发型返回三档叠图(middle/high/low,RGBA 透明层只含发际线曲线)与一张生发图; 每个选中发型返回三档叠图(middle/high/low,RGBA 透明层只含发际线曲线)与一张生发图;
三档贴图同名,生发黑模板固定取自 hairline_texture_black/middle),故生发目标固定 middle 档。
use_mask/prompt:同接口2 的生发参数。 生发机制(同接口2,按性别分流):
generate_grow_image(默认 True):是否生成生发图(ComfyUI,最耗时)。False 时跳过生发, - femalegenerate_grow_results_swapswapHair + Flux-2 整帧重绘)
- malegenerate_grow_resultsComfyUI add_hair inpaint
redraw_max_side / unet_name / v2_defaultsfemale 路径参数,同接口2。
male 路径仅用 unet_nameredraw_max_side/v2_defaults 对 male 无意义(忽略)。
use_mask/prompt:仅 male 路径生效(同接口2 male)。
generate_grow_image(默认 True):是否生成生发图(最耗时)。False 时跳过生发,
各发型 grown_png 恒为 None,可大幅降低耗时(仅留三档发际线叠图与中心点)。 各发型 grown_png 恒为 None,可大幅降低耗时(仅留三档发际线叠图与中心点)。
Returns: {"images":[{hairline_type,order,overlays:{middle,high,low}((H,W,4) RGBA 透明层),grown_png}], Returns: {"images":[{hairline_type,order,overlays:{middle,high,low}((H,W,4) RGBA 透明层),grown_png}],
"best_centers":{"middle":(x,y),"high":(x,y),"low":(x,y)}};无人脸 None。 "best_centers":{"middle":(x,y),"high":(x,y),"low":(x,y)}};无人脸 None。
@@ -439,11 +447,27 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
# 三档贴图表(同性别、同 key 顺序,因三个文件夹同名) # 三档贴图表(同性别、同 key 顺序,因三个文件夹同名)
tex_by_level = {lv: get_texture_map(lv)[gender] for lv in _TEXTURE_DIRS} tex_by_level = {lv: get_texture_map(lv)[gender] for lv in _TEXTURE_DIRS}
# use_mask=False:干净原图+空遮罩与贴图无关,只跑一次 ComfyUI,选中项复用 # 生发图(同接口2,按性别分流):一次性算出所有选中发型的生发图,按 order 对应回叠图。
# generate_grow_image=False:完全跳过生发(最耗时),grown_png 恒为 None # female→generate_grow_results_swapswapHair+Flux-2 整帧重绘);
shared_grown = None # male→generate_grow_resultsComfyUI add_hair inpaint)。
if generate_grow_image and not use_mask: # generate_grow_image=False 时跳过,grown_by_order 为空 dict(各发型 grown_png 恒 None)。
shared_grown = _grow_from_texture(image_bgr, ctx, None, use_mask=False, prompt=prompt) grown_by_order: dict[int, bytes | None] = {}
if generate_grow_image:
try:
if gender == "female":
items = generate_grow_results_swap(
image_bgr, hair_styles, v2_defaults or {},
redraw_max_side=redraw_max_side, unet_name=unet_name)
else:
items = generate_grow_results(
image_bgr, gender, use_mask, prompt, hair_styles,
unet_name=unet_name)
if items is None:
return None # 无人脸(同接口2 的 None 语义)
for it in items:
grown_by_order[it["order"]] = it.get("grown_png")
except Exception as e: # noqa: BLE001 整批生发失败不拖垮叠图主结果
logger.warning("接口5 生发批量失败(gender=%s):%s", gender, e)
def _center_of(overlay): def _center_of(overlay):
"""从某档发际线透明叠图取面部中轴处的发际线中点 (x,y),无像素返回 None。""" """从某档发际线透明叠图取面部中轴处的发际线中点 (x,y),无像素返回 None。"""
@@ -456,18 +480,13 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
images, best_centers = [], None images, best_centers = [], None
for s in hair_styles: # s = 1-indexed 发型序号 for s in hair_styles: # s = 1-indexed 发型序号
key, mid_path = tex_by_level["middle"][s - 1] key, _mid_path = tex_by_level["middle"][s - 1]
overlays = {} overlays = {}
for lv in _TEXTURE_DIRS: for lv in _TEXTURE_DIRS:
white = load_texture_rgba(tex_by_level[lv][s - 1][1]) white = load_texture_rgba(tex_by_level[lv][s - 1][1])
overlays[lv] = build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white) overlays[lv] = build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white)
# 生发:固定 middle 黑模板generate_grow_image=False 时跳过,恒 None # 生发图:从按性别算好的结果里按 order 取generate_grow_image=False 时缺省 None
if not generate_grow_image: grown_png = grown_by_order.get(s)
grown_png = None
elif not use_mask:
grown_png = shared_grown
else:
grown_png = _grow_from_texture(image_bgr, ctx, mid_path, use_mask=True, prompt=prompt)
images.append({"hairline_type": key, "order": s, images.append({"hairline_type": key, "order": s,
"overlays": overlays, "grown_png": grown_png}) "overlays": overlays, "grown_png": grown_png})
# best_centers:首个选中发型三档(middle/high/low)发际线中点 # best_centers:首个选中发型三档(middle/high/low)发际线中点
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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,
) )
+2 -2
View File
@@ -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');
+2 -2
View File
@@ -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 = '⏳ 重绘中...';
+1 -1
View File
@@ -117,7 +117,7 @@
<div class="hint">JPG/PNG &nbsp;|&nbsp; 生发图生成较慢(数十秒~数分钟),请耐心等待</div> <div class="hint">JPG/PNG &nbsp;|&nbsp; 生发图生成较慢(数十秒~数分钟),请耐心等待</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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -104,7 +104,7 @@
<div class="hint">JPG/PNG &nbsp;|&nbsp; 生发图生成较慢(数十秒~数分钟),请耐心等待 &nbsp;|&nbsp; 工作流: add_hair2.json</div> <div class="hint">JPG/PNG &nbsp;|&nbsp; 生发图生成较慢(数十秒~数分钟),请耐心等待 &nbsp;|&nbsp; 工作流: 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>