feat: 接口12/接口2 发际线重绘改走 local_test 外部 ComfyUI 服务

后端 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 预检),支持浏览器跨域直连。
This commit is contained in:
xsl
2026-07-17 00:00:15 +08:00
parent 0bbb15d668
commit 5fbc03a6df
9 changed files with 291 additions and 101 deletions
+45 -10
View File
@@ -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_test0716add-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 单张失败不拖垮整请求