feat: 新增52号发际线接口并限制GPU worker来源IP
保留5号历史三档实现,新增中下庭高度定位、扶正裁切、矩形叠加与30案例对比页。 worker仅允许本机和配置白名单访问;网关不增加IP限制。空图片URL字段改写限定为52号,保持旧接口兼容。同步接口、裁切与部署文档。 验证:113项测试通过;1项历史数值基线失败,在未修改的a3fddbb版本复现。浏览器验证5/52及30案例对比页通过。
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
"""Nullable/deprecated worker images retain their documented gateway keys."""
|
||||
import asyncio
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from starlette.requests import Request
|
||||
|
||||
from gateway.forward import rewrite_base64_to_url
|
||||
|
||||
|
||||
def test_nullable_image_fields_keep_external_names(tmp_path):
|
||||
value = {"hairline_images": [{"image_high_base64": None, "image_low_base64": None,
|
||||
"grown_image_base64": None}],
|
||||
"high_hairline_center_point": None, "face_measure": None}
|
||||
actual = rewrite_base64_to_url(value, "https://example.test", str(tmp_path), rewrite_null_images=True)
|
||||
assert actual == {"hairline_images": [{"image_high_url": None, "image_low_url": None,
|
||||
"grown_image_url": None}],
|
||||
"high_hairline_center_point": None, "face_measure": None}
|
||||
assert not list(tmp_path.iterdir())
|
||||
|
||||
|
||||
def test_nonempty_images_still_rewrite_alongside_null_fields(tmp_path):
|
||||
import base64
|
||||
import io
|
||||
from PIL import Image
|
||||
buf = io.BytesIO()
|
||||
Image.new("RGBA", (2, 2), (255, 255, 255, 0)).save(buf, format="PNG")
|
||||
value = {"image_middle_base64": base64.b64encode(buf.getvalue()).decode(),
|
||||
"image_high_base64": None}
|
||||
actual = rewrite_base64_to_url(value, "https://example.test", str(tmp_path), rewrite_null_images=True)
|
||||
assert actual["image_middle_url"].startswith("https://example.test/static/annotations/")
|
||||
assert actual["image_high_url"] is None
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
|
||||
|
||||
def test_legacy_nullable_image_field_names_remain_unchanged(tmp_path):
|
||||
value = {"hairline_images": [{"grown_image_base64": None}], "face_measure": None}
|
||||
assert rewrite_base64_to_url(value, "https://example.test", str(tmp_path)) == value
|
||||
assert not list(tmp_path.iterdir())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path,expected_key", [
|
||||
("/api/v1/hairline/generate", "grown_image_base64"),
|
||||
("/api/v1/hairline/generate-52", "grown_image_url"),
|
||||
])
|
||||
def test_proxy_rewrites_null_images_only_for_interface52(tmp_path, monkeypatch, path, expected_key):
|
||||
import gateway.config as config
|
||||
import gateway.forward as forward
|
||||
|
||||
monkeypatch.setattr(config, "get_config", lambda: {
|
||||
"dispatch": {"request_timeout_seconds": 10}, "shared_password": "test-token",
|
||||
"public_base_url": "https://example.test", "static_dir": str(tmp_path),
|
||||
})
|
||||
monkeypatch.setattr(forward, "acquire_worker", AsyncMock(return_value=SimpleNamespace(url="http://worker")))
|
||||
monkeypatch.setattr(forward, "release_worker", AsyncMock())
|
||||
worker_response = httpx.Response(200, json={"code": 0, "data": {"hairline_images": [
|
||||
{"grown_image_base64": None}]}})
|
||||
monkeypatch.setattr(forward, "get_client", lambda: SimpleNamespace(request=AsyncMock(return_value=worker_response)))
|
||||
|
||||
async def receive():
|
||||
return {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
request = Request({"type": "http", "method": "POST", "path": path, "headers": []}, receive)
|
||||
response = asyncio.run(forward.proxy_request(request, path))
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.body)["data"]["hairline_images"] == [{expected_key: None}]
|
||||
Reference in New Issue
Block a user