feat: 新增52号发际线接口并限制GPU worker来源IP

保留5号历史三档实现,新增中下庭高度定位、扶正裁切、矩形叠加与30案例对比页。

worker仅允许本机和配置白名单访问;网关不增加IP限制。空图片URL字段改写限定为52号,保持旧接口兼容。同步接口、裁切与部署文档。

验证:113项测试通过;1项历史数值基线失败,在未修改的a3fddbb版本复现。浏览器验证5/52及30案例对比页通过。
This commit is contained in:
xsl
2026-11-04 10:20:29 +08:00
parent a3fddbbc34
commit 5b2b73f5d5
35 changed files with 2826 additions and 26 deletions
+15 -3
View File
@@ -88,6 +88,8 @@ def rewrite_base64_to_url(
obj: Any,
public_base_url: str,
static_dir: str,
*,
rewrite_null_images: bool = False,
) -> Any:
"""递归遍历响应 JSON,将所有 *_base64 字段改写为 *_url。
@@ -95,10 +97,16 @@ def rewrite_base64_to_url(
- 支持 data: URI 和原始 base64 两种格式
- 解码 base64 → 保存到 static_dir/{uuid}.png
- 删除 *_base64 字段,新增 *_url 字段指向公网 URL
- 接口52显式开启 rewrite_null_images,空图片也返回 *_url: null;旧接口保持原空字段
"""
if isinstance(obj, dict):
new_dict: Dict[str, Any] = {}
for key, value in obj.items():
if rewrite_null_images and key.endswith("_base64") and value is None:
# Deprecated/optional image fields must keep their external
# *_url names even when no image is generated.
new_dict[key[:-7] + "_url"] = None
continue
if key.endswith("_base64") and isinstance(value, str):
img_bytes = _decode_base64_value(value)
if img_bytes is not None:
@@ -118,10 +126,12 @@ def rewrite_base64_to_url(
logger.warning("字段 %s 的值无法解码为图片,保留", key)
new_dict[key] = value
else:
new_dict[key] = rewrite_base64_to_url(value, public_base_url, static_dir)
new_dict[key] = rewrite_base64_to_url(
value, public_base_url, static_dir, rewrite_null_images=rewrite_null_images)
return new_dict
elif isinstance(obj, list):
return [rewrite_base64_to_url(item, public_base_url, static_dir) for item in obj]
return [rewrite_base64_to_url(item, public_base_url, static_dir,
rewrite_null_images=rewrite_null_images) for item in obj]
else:
return obj
@@ -248,7 +258,9 @@ async def proxy_request(request: Request, path: str) -> JSONResponse:
)
# 递归改写 base64 图片字段
rewritten = rewrite_base64_to_url(worker_json, public_base_url, static_dir)
rewritten = rewrite_base64_to_url(
worker_json, public_base_url, static_dir,
rewrite_null_images=path == "/api/v1/hairline/generate-52")
return JSONResponse(
status_code=resp.status_code,