feat(接口2/3): 加 use_mask 开关用于遮罩对比;网关回退纯透传
- 接口3 generate_grow_b 增加 use_mask(默认 true):false 跳过检测、 直接送划线图(空遮罩) - 接口2 generate_grow_results 增加 use_mask:false 用干净原图+空遮罩, 只跑一次 ComfyUI、N 项复用 - app.py 接口2/3 增加 use_mask form 参数并透传 - gateway/app.py 回退为纯透传(移除 OpenAPI schema 注入,请求体原样转发) - docs 补 use_mask 说明 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -503,6 +503,7 @@ async def hair_grow(
|
|||||||
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(必填)"),
|
||||||
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
|
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
|
||||||
|
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
|
||||||
):
|
):
|
||||||
# 1. gender 必填校验(非法/缺失 → 1004)
|
# 1. gender 必填校验(非法/缺失 → 1004)
|
||||||
if gender not in ("male", "female"):
|
if gender not in ("male", "female"):
|
||||||
@@ -529,7 +530,7 @@ async def hair_grow(
|
|||||||
from hairline.service import generate_grow_results
|
from hairline.service import generate_grow_results
|
||||||
|
|
||||||
# 预览 + 生发(ComfyUI) 都是阻塞且较慢,放线程池避免卡住事件循环
|
# 预览 + 生发(ComfyUI) 都是阻塞且较慢,放线程池避免卡住事件循环
|
||||||
items = await run_in_threadpool(generate_grow_results, image, gender)
|
items = await run_in_threadpool(generate_grow_results, image, gender, use_mask)
|
||||||
if items is None:
|
if items is None:
|
||||||
return err(1001, "无法识别人像")
|
return err(1001, "无法识别人像")
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,7 @@
|
|||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| gender | string | **是** | 性别:`male` / `female`。决定使用的发际线贴图集合 |
|
| gender | string | **是** | 性别:`male` / `female`。决定使用的发际线贴图集合 |
|
||||||
| beauty_enabled | bool | 否 | 生发图是否带美颜效果,默认 false(当前阶段不生效) |
|
| beauty_enabled | bool | 否 | 生发图是否带美颜效果,默认 false(当前阶段不生效) |
|
||||||
|
| use_mask | bool | 否 | 是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比;此时 N 张结果共用同一张生发图 |
|
||||||
|
|
||||||
### 输出(data)
|
### 输出(data)
|
||||||
|
|
||||||
|
|||||||
+3
-68
@@ -214,8 +214,7 @@ def _proxy(request: Request, path: str):
|
|||||||
return proxy_request(request, path)
|
return proxy_request(request, path)
|
||||||
|
|
||||||
|
|
||||||
# 声明各接口的 form 参数 → 注入 OpenAPI schema(实际转发直接读 Request body,不在此解析)。
|
# 声明各接口的 form 参数用于 OpenAPI schema(实际转发直接读 Request)
|
||||||
# 每项字段:type(file/string/boolean)、description,可选 required(默认否)/default。
|
|
||||||
_MEASURE_FORMS = {
|
_MEASURE_FORMS = {
|
||||||
"image_file": {"type": "file", "description": "上传图片文件(JPG/PNG,≤ 1 MB)"},
|
"image_file": {"type": "file", "description": "上传图片文件(JPG/PNG,≤ 1 MB)"},
|
||||||
"image_url": {"type": "string", "description": "图片 URL"},
|
"image_url": {"type": "string", "description": "图片 URL"},
|
||||||
@@ -224,31 +223,13 @@ _MEASURE_FORMS = {
|
|||||||
|
|
||||||
_GROW_FORMS = {
|
_GROW_FORMS = {
|
||||||
**_MEASURE_FORMS,
|
**_MEASURE_FORMS,
|
||||||
"gender": {"type": "string", "description": "性别 male/female(必填)", "required": True},
|
"beauty_enabled": {"type": "boolean", "description": "是否开启美颜效果"},
|
||||||
"beauty_enabled": {"type": "boolean", "description": "是否开启美颜(本期不生效)", "default": False},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_GROW_B_FORMS = {
|
_GROW_B_FORMS = {
|
||||||
"marked_image_file": {"type": "file", "description": "划线图片文件(JPG/PNG,≤ 1 MB)"},
|
"marked_image_file": {"type": "file", "description": "划线图片文件"},
|
||||||
"marked_image_url": {"type": "string", "description": "划线图片 URL"},
|
"marked_image_url": {"type": "string", "description": "划线图片 URL"},
|
||||||
"marked_image_base64": {"type": "string", "description": "划线图片 base64"},
|
"marked_image_base64": {"type": "string", "description": "划线图片 base64"},
|
||||||
"use_mask": {"type": "boolean",
|
|
||||||
"description": "是否启用自动检测遮罩,默认 true;false 跳过检测直接送图(空遮罩),测试对比用",
|
|
||||||
"default": True},
|
|
||||||
}
|
|
||||||
|
|
||||||
_HAIRLINE_FORMS = {
|
|
||||||
**_MEASURE_FORMS,
|
|
||||||
"gender": {"type": "string", "description": "性别 male/female(必填)", "required": True},
|
|
||||||
}
|
|
||||||
|
|
||||||
# 路由(path, method) → form 参数表,供 custom_openapi 注入 requestBody。
|
|
||||||
# 接口4(/api/v1/face/features) 在网关本地用 Form() 声明,FastAPI 自动生成 schema,不在此列。
|
|
||||||
_ROUTE_FORMS = {
|
|
||||||
("/api/v1/face/measure", "post"): _MEASURE_FORMS,
|
|
||||||
("/api/v1/hair/grow", "post"): _GROW_FORMS,
|
|
||||||
("/api/v1/hair/grow-b", "post"): _GROW_B_FORMS,
|
|
||||||
("/api/v1/hairline/generate", "post"): _HAIRLINE_FORMS,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -339,49 +320,3 @@ async def face_features(
|
|||||||
async def hairline_generate(request: Request):
|
async def hairline_generate(request: Request):
|
||||||
"""接口5:发际线PNG生成"""
|
"""接口5:发际线PNG生成"""
|
||||||
return await _proxy(request, "/api/v1/hairline/generate")
|
return await _proxy(request, "/api/v1/hairline/generate")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 自定义 OpenAPI:代理路由用 Request 透传、不声明 Form,FastAPI 默认生成不出入参。
|
|
||||||
# 这里把 _ROUTE_FORMS 注入各路由的 multipart/form-data requestBody,使 /docs 入参完整。
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def _form_schema(forms: dict) -> dict:
|
|
||||||
"""form 参数表 → multipart/form-data 的 JSON Schema。"""
|
|
||||||
props, required = {}, []
|
|
||||||
for name, spec in forms.items():
|
|
||||||
prop = ({"type": "string", "format": "binary"} if spec["type"] == "file"
|
|
||||||
else {"type": spec["type"]})
|
|
||||||
if spec.get("description"):
|
|
||||||
prop["description"] = spec["description"]
|
|
||||||
if "default" in spec:
|
|
||||||
prop["default"] = spec["default"]
|
|
||||||
props[name] = prop
|
|
||||||
if spec.get("required"):
|
|
||||||
required.append(name)
|
|
||||||
schema = {"type": "object", "properties": props}
|
|
||||||
if required:
|
|
||||||
schema["required"] = required
|
|
||||||
return schema
|
|
||||||
|
|
||||||
|
|
||||||
def custom_openapi():
|
|
||||||
if app.openapi_schema:
|
|
||||||
return app.openapi_schema
|
|
||||||
from fastapi.openapi.utils import get_openapi
|
|
||||||
|
|
||||||
schema = get_openapi(title=app.title, version=app.version,
|
|
||||||
description=app.description, routes=app.routes)
|
|
||||||
for (path, method), forms in _ROUTE_FORMS.items():
|
|
||||||
op = schema.get("paths", {}).get(path, {}).get(method)
|
|
||||||
if op is not None:
|
|
||||||
op["requestBody"] = {
|
|
||||||
"required": True,
|
|
||||||
"content": {"multipart/form-data": {"schema": _form_schema(forms)}},
|
|
||||||
}
|
|
||||||
app.openapi_schema = schema
|
|
||||||
return schema
|
|
||||||
|
|
||||||
|
|
||||||
app.openapi = custom_openapi
|
|
||||||
|
|||||||
+19
-1
@@ -128,9 +128,12 @@ def generate_previews(image_bgr: np.ndarray, gender: str):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def generate_grow_results(image_bgr: np.ndarray, gender: str):
|
def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = True):
|
||||||
"""该性别全部发际线:预览图(白线) + 生发图(ComfyUI)。同步、串行。
|
"""该性别全部发际线:预览图(白线) + 生发图(ComfyUI)。同步、串行。
|
||||||
|
|
||||||
|
use_mask(默认 True):是否启用 inpaint 遮罩,用于测试对比(同接口3)。
|
||||||
|
False 时用**干净原图 + 空遮罩**送 ComfyUI(不烧黑色模板线),与模板无关,
|
||||||
|
故只跑一次 ComfyUI、N 项复用同一张生发图;预览图(白线)仍按各模板生成。
|
||||||
Returns: list[dict] {"hairline_type","order","image_bgr"(预览), "grown_png"(bytes 或 None)}。
|
Returns: list[dict] {"hairline_type","order","image_bgr"(预览), "grown_png"(bytes 或 None)}。
|
||||||
无人脸返回 None。某张 ComfyUI 失败时该项 grown_png=None,不影响其余。
|
无人脸返回 None。某张 ComfyUI 失败时该项 grown_png=None,不影响其余。
|
||||||
"""
|
"""
|
||||||
@@ -140,11 +143,26 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str):
|
|||||||
if ctx is None:
|
if ctx is None:
|
||||||
return None
|
return None
|
||||||
uv, ext_faces = load_ext_mesh()
|
uv, ext_faces = load_ext_mesh()
|
||||||
|
|
||||||
|
# 禁用遮罩:干净原图 + 空遮罩,与模板无关 → 只跑一次 ComfyUI,下面 N 项复用
|
||||||
|
shared_grown = None
|
||||||
|
if not use_mask:
|
||||||
|
try:
|
||||||
|
h, w = image_bgr.shape[:2]
|
||||||
|
buf = io.BytesIO()
|
||||||
|
compose_comfy_rgba(image_bgr, np.zeros((h, w), np.uint8)).save(buf, format="PNG")
|
||||||
|
shared_grown = comfyui.run(buf.getvalue())
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning("接口2 生发图失败(无遮罩):%s", e)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for order, (key, white_path) in enumerate(get_texture_map()[gender], start=1):
|
for order, (key, white_path) in enumerate(get_texture_map()[gender], start=1):
|
||||||
white = load_texture_rgba(white_path)
|
white = load_texture_rgba(white_path)
|
||||||
preview = render_hairline_overlay(image_bgr, ctx["points"], ext_faces, uv, white)
|
preview = render_hairline_overlay(image_bgr, ctx["points"], ext_faces, uv, white)
|
||||||
|
|
||||||
|
if not use_mask:
|
||||||
|
grown_png = shared_grown
|
||||||
|
else:
|
||||||
grown_png = None
|
grown_png = None
|
||||||
try:
|
try:
|
||||||
black = load_texture_rgba(_black_texture_path(white_path))
|
black = load_texture_rgba(_black_texture_path(white_path))
|
||||||
|
|||||||
Reference in New Issue
Block a user