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:
@@ -29,7 +29,7 @@
|
|||||||
"60": {
|
"60": {
|
||||||
"class_type": "JjkText",
|
"class_type": "JjkText",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
"text": "填充遮罩区域的头发"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"22": {
|
"22": {
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -410,7 +410,7 @@
|
|||||||
},
|
},
|
||||||
"60": {
|
"60": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
"text": "填充遮罩区域的头发"
|
||||||
},
|
},
|
||||||
"class_type": "JjkText",
|
"class_type": "JjkText",
|
||||||
"_meta": {
|
"_meta": {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
+2
-2
@@ -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
@@ -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 时跳过生发,
|
- female:generate_grow_results_swap(swapHair + Flux-2 整帧重绘)
|
||||||
|
- male:generate_grow_results(ComfyUI add_hair inpaint)
|
||||||
|
redraw_max_side / unet_name / v2_defaults:female 路径参数,同接口2。
|
||||||
|
male 路径仅用 unet_name;redraw_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_swap(swapHair+Flux-2 整帧重绘);
|
||||||
shared_grown = None
|
# male→generate_grow_results(ComfyUI 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
@@ -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 = '⏳ 重绘中...';
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,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