feat: 新增接口22生发控制与测试页
This commit is contained in:
+20
-29
@@ -8,11 +8,7 @@ from conftest import fixture
|
||||
|
||||
import app as app_module
|
||||
|
||||
# 测试自带固定密码,避免依赖 worker_config.json 的实际值(中间件运行期读模块全局)
|
||||
app_module.ACCEPT_PASSWORDS = ["testpass"]
|
||||
|
||||
URL = "/api/v1/face/measure"
|
||||
H = {"X-Internal-Token": "testpass"}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@@ -22,18 +18,13 @@ def client():
|
||||
yield c
|
||||
|
||||
|
||||
def _post(client, fixture_name=None, headers=H, data=None, extra_files=None):
|
||||
def _post(client, fixture_name=None, data=None, extra_files=None):
|
||||
files = {}
|
||||
if fixture_name:
|
||||
files["image_file"] = (fixture_name, open(fixture(fixture_name), "rb"), "application/octet-stream")
|
||||
if extra_files:
|
||||
files.update(extra_files)
|
||||
return client.post(URL, headers=headers, files=files or None, data=data)
|
||||
|
||||
|
||||
def test_auth_missing_token_401(client):
|
||||
r = _post(client, "frontal.jpg", headers={})
|
||||
assert r.status_code == 401
|
||||
return client.post(URL, files=files or None, data=data)
|
||||
|
||||
|
||||
def _patch_health_ok(monkeypatch):
|
||||
@@ -107,7 +98,7 @@ def test_tcp_probe_real_socket():
|
||||
|
||||
|
||||
def test_param_none_provided_1007(client):
|
||||
r = client.post(URL, headers=H)
|
||||
r = client.post(URL)
|
||||
assert r.json()["code"] == 1007
|
||||
|
||||
|
||||
@@ -131,7 +122,7 @@ GROW = "/api/v1/hair/grow"
|
||||
|
||||
def test_grow_missing_gender_1004(client):
|
||||
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
|
||||
r = client.post(GROW, headers=H, files=files)
|
||||
r = client.post(GROW, files=files)
|
||||
assert r.json()["code"] == 1004
|
||||
|
||||
|
||||
@@ -148,7 +139,7 @@ def test_grow_female_returns_5(client, monkeypatch):
|
||||
|
||||
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
|
||||
# hair_style 必填(cb1989c 起):指定全部 5 种发型以验证完整管线
|
||||
r = client.post(GROW, headers=H, files=files, data={"gender": "female", "hair_style": "1,2,3,4,5"})
|
||||
r = client.post(GROW, files=files, data={"gender": "female", "hair_style": "1,2,3,4,5"})
|
||||
body = r.json()
|
||||
assert body["code"] == 0, body
|
||||
results = body["data"]["results"]
|
||||
@@ -163,13 +154,13 @@ GROWB = "/api/v1/hair/grow-b"
|
||||
|
||||
|
||||
def test_growb_missing_marked_1007(client):
|
||||
r = client.post(GROWB, headers=H) # 一张图都没传
|
||||
r = client.post(GROWB) # 一张图都没传
|
||||
assert r.json()["code"] == 1007
|
||||
|
||||
|
||||
def test_growb_no_line_1001(client):
|
||||
files = {"marked_image_file": ("m.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
|
||||
r = client.post(GROWB, headers=H, files=files) # 无划线 → 拒识
|
||||
r = client.post(GROWB, files=files) # 无划线 → 拒识
|
||||
assert r.json()["code"] == 1001
|
||||
|
||||
|
||||
@@ -177,7 +168,7 @@ def test_growb_success(client, monkeypatch):
|
||||
import hairline.comfyui as comfy
|
||||
monkeypatch.setattr(comfy, "run", lambda *a, **k: _PNG_1x1)
|
||||
files = {"marked_image_file": ("m.jpg", open(fixture("marked_hairline.jpg"), "rb"), "application/octet-stream")}
|
||||
body = client.post(GROWB, headers=H, files=files).json() # 只传划线图一张
|
||||
body = client.post(GROWB, files=files).json() # 只传划线图一张
|
||||
assert body["code"] == 0, body
|
||||
d = body["data"]
|
||||
assert d["hairline_type"] == "custom"
|
||||
@@ -190,7 +181,7 @@ HLGEN = "/api/v1/hairline/generate"
|
||||
|
||||
|
||||
def test_hairline5_restored_schema_has_only_historical_parameters(client):
|
||||
schema = client.get("/openapi.json", headers=H).json()
|
||||
schema = client.get("/openapi.json").json()
|
||||
ref = schema["paths"][HLGEN]["post"]["requestBody"]["content"]["multipart/form-data"]["schema"]["$ref"]
|
||||
fields = schema["components"]["schemas"][ref.rsplit("/", 1)[-1]]["properties"]
|
||||
assert set(fields) == {"image_file", "image_url", "image_base64", "gender", "hair_style",
|
||||
@@ -204,7 +195,7 @@ def test_hairline5_restored_schema_has_only_historical_parameters(client):
|
||||
def test_hairline5_restored_three_levels_and_original_coordinate_contract(client, gender):
|
||||
from pathlib import Path
|
||||
photo = Path(fixture("frontal.jpg")).read_bytes()
|
||||
body = client.post(HLGEN, headers=H, files={"image_file": ("frontal.jpg", photo, "image/jpeg")},
|
||||
body = client.post(HLGEN, files={"image_file": ("frontal.jpg", photo, "image/jpeg")},
|
||||
data={"gender": gender, "hair_style": "1,3", "generate_grow_image": "false"}).json()
|
||||
assert body["code"] == 0, body
|
||||
data = body["data"]
|
||||
@@ -225,13 +216,13 @@ def test_hairline5_restored_three_levels_and_original_coordinate_contract(client
|
||||
|
||||
def test_hairline_gen_missing_gender_1004(client):
|
||||
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
|
||||
assert client.post(HLGEN, headers=H, files=files).json()["code"] == 1004
|
||||
assert client.post(HLGEN, files=files).json()["code"] == 1004
|
||||
|
||||
|
||||
def test_hairline_gen_missing_hairstyle_1007(client):
|
||||
# hair_style 已改为必填(同接口2):只给 gender、不给 hair_style → 1007
|
||||
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
|
||||
assert client.post(HLGEN, headers=H, files=files, data={"gender": "female"}).json()["code"] == 1007
|
||||
assert client.post(HLGEN, files=files, data={"gender": "female"}).json()["code"] == 1007
|
||||
|
||||
|
||||
def test_hairline_gen_female(client, monkeypatch):
|
||||
@@ -240,7 +231,7 @@ def test_hairline_gen_female(client, monkeypatch):
|
||||
monkeypatch.setattr(comfy, "run", lambda *a, **k: _PNG_1x1)
|
||||
|
||||
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
|
||||
body = client.post(HLGEN, headers=H, files=files, data={"gender": "female", "hair_style": "1,3"}).json()
|
||||
body = client.post(HLGEN, files=files, data={"gender": "female", "hair_style": "1,3"}).json()
|
||||
assert body["code"] == 0, body
|
||||
d = body["data"]
|
||||
imgs = d["hairline_images"]
|
||||
@@ -279,7 +270,7 @@ HLGEN52 = "/api/v1/hairline/generate-52"
|
||||
|
||||
|
||||
def test_hairline52_legacy_form_parameters_and_defaults_are_preserved(client):
|
||||
schema = client.get("/openapi.json", headers=H).json()
|
||||
schema = client.get("/openapi.json").json()
|
||||
body = schema["paths"][HLGEN52]["post"]["requestBody"]["content"]["multipart/form-data"]["schema"]
|
||||
body = schema["components"]["schemas"][body["$ref"].rsplit("/", 1)[-1]]
|
||||
fields = body["properties"]
|
||||
@@ -310,7 +301,7 @@ def test_hairline52_legacy_request_keeps_fields_and_original_coordinates(client,
|
||||
return _PNG_1x1
|
||||
monkeypatch.setattr(comfy, "run", generate)
|
||||
form = {"gender": "male", "hair_style": "1", "use_mask": "false", "prompt": "legacy prompt"}
|
||||
kwargs = {"headers": H, "data": form}
|
||||
kwargs = {"data": form}
|
||||
if image_kind == "file":
|
||||
kwargs["files"] = {"image_file": ("frontal.jpg", raw, "image/jpeg")}
|
||||
elif image_kind == "base64":
|
||||
@@ -385,13 +376,13 @@ def test_hairline52_height_compare_uses_same_new_rule(client, monkeypatch, norma
|
||||
|
||||
def test_hairline52_gen_missing_gender_1004(client):
|
||||
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
|
||||
assert client.post(HLGEN52, headers=H, files=files).json()["code"] == 1004
|
||||
assert client.post(HLGEN52, files=files).json()["code"] == 1004
|
||||
|
||||
|
||||
def test_hairline52_gen_missing_hairstyle_1007(client):
|
||||
# hair_style 已改为必填(同接口2):只给 gender、不给 hair_style → 1007
|
||||
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
|
||||
assert client.post(HLGEN52, headers=H, files=files, data={"gender": "female"}).json()["code"] == 1007
|
||||
assert client.post(HLGEN52, files=files, data={"gender": "female"}).json()["code"] == 1007
|
||||
|
||||
|
||||
def test_hairline52_gen_female(client, monkeypatch):
|
||||
@@ -400,7 +391,7 @@ def test_hairline52_gen_female(client, monkeypatch):
|
||||
monkeypatch.setattr(comfy, "run", lambda *a, **k: _PNG_1x1)
|
||||
|
||||
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
|
||||
body = client.post(HLGEN52, headers=H, files=files,
|
||||
body = client.post(HLGEN52, files=files,
|
||||
data={"gender": "female", "hair_style": "1,3", "normalize_image": "true"}).json()
|
||||
assert body["code"] == 0, body
|
||||
d = body["data"]
|
||||
@@ -422,7 +413,7 @@ def test_hairline52_gen_female(client, monkeypatch):
|
||||
assert d["high_hairline_center_point"] is None and d["low_hairline_center_point"] is None
|
||||
rule = d["height_rule"]
|
||||
assert rule["target_upper_height_px"] == pytest.approx(
|
||||
(rule["middle_height_px"] + rule["lower_height_px"]) / 2)
|
||||
(rule["middle_height_px"] + rule["lower_height_px"]) * 0.45)
|
||||
for x in imgs:
|
||||
assert x["height_placement"]["new_center"]["y"] == pytest.approx(rule["target_center"]["y"], abs=1)
|
||||
assert base64.b64decode(imgs[0]["grown_image_base64"])[:3] == b"\xff\xd8\xff"
|
||||
@@ -512,6 +503,6 @@ def _assert_vertical_lines(data, variant):
|
||||
|
||||
def test_measure_v2_vertical_lines_have_no_head_edges(client):
|
||||
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
|
||||
body = client.post("/api/v1/face/measure-v2", headers=H, files=files).json()
|
||||
body = client.post("/api/v1/face/measure-v2", files=files).json()
|
||||
assert body["code"] == 0, body
|
||||
_assert_vertical_lines(body["data"], "v6")
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
from starlette.requests import Request
|
||||
|
||||
|
||||
def test_legacy_hairline_forwarding(monkeypatch):
|
||||
name, path = "hairline_generate", "/api/v1/hairline/generate"
|
||||
@pytest.mark.parametrize(("name", "path"), [
|
||||
("hairline_generate", "/api/v1/hairline/generate"),
|
||||
("hair_grow_22", "/api/v1/hair/grow-22"),
|
||||
])
|
||||
def test_hairline_forwarding(monkeypatch, name, path):
|
||||
gateway = importlib.import_module("gateway.app")
|
||||
request = Request({"type": "http", "method": "POST", "path": path, "headers": []})
|
||||
calls = []
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import base64
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import app as worker
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("densities", "labels"), [
|
||||
(["dense"], ["适中偏多"]),
|
||||
(["sparse", "normal", "dense"], ["稀疏", "正常", "适中偏多"]),
|
||||
([], []),
|
||||
])
|
||||
def test_interface22_selects_prompts_and_aligns_output_size(monkeypatch, densities, labels):
|
||||
from hairline import service
|
||||
|
||||
prompts = []
|
||||
rounded_png = cv2.imencode(".png", np.zeros((15, 13, 3), np.uint8))[1].tobytes()
|
||||
|
||||
def generate(image, styles, defaults, *, prompt, hairline_height, **kwargs):
|
||||
assert styles == [1, 3]
|
||||
assert hairline_height == -1
|
||||
prompts.append(prompt)
|
||||
return [
|
||||
{"order": order, "hairline_type": key, "grown_png": rounded_png,
|
||||
"overlay": np.zeros((16, 19, 4), np.uint8)}
|
||||
for order, key in [(1, "ellipse"), (3, "heart")]
|
||||
]
|
||||
|
||||
monkeypatch.setattr(service, "generate_grow_results_swap", generate)
|
||||
image = cv2.imencode(".png", np.zeros((16, 19, 3), np.uint8))[1].tobytes()
|
||||
form = {"gender": "female", "hair_style": "1,3", "hairline_height": "-1"}
|
||||
if densities:
|
||||
form["hair_density"] = densities
|
||||
client = TestClient(worker.app, client=("127.0.0.1", 50000))
|
||||
response = client.post("/api/v1/hair/grow-22", data=form,
|
||||
files={"image_file": ("input.png", image, "image/png")})
|
||||
if not densities:
|
||||
assert response.status_code == 422
|
||||
assert prompts == []
|
||||
return
|
||||
assert response.status_code == 200
|
||||
assert prompts == [f"填充遮罩区域的头发,发量{label},人物增加一点磨皮" for label in labels]
|
||||
results = response.json()["data"]["results"]
|
||||
assert [(item["hair_density"], item["order"]) for item in results] == [
|
||||
(density, order) for density in densities for order in (1, 3)
|
||||
]
|
||||
for item in results:
|
||||
jpg = np.frombuffer(base64.b64decode(item["grown_image_base64"]), np.uint8)
|
||||
assert cv2.imdecode(jpg, cv2.IMREAD_COLOR).shape[:2] == (16, 19)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dy", [-5, 5])
|
||||
def test_height_moves_paste_and_redraw_band_after_swap(monkeypatch, dy):
|
||||
from face_analysis import hairline_grow as grow
|
||||
|
||||
image = np.zeros((80, 80, 3), np.uint8)
|
||||
source_mask = np.zeros((80, 80), dtype=bool)
|
||||
source_mask[25:45, 20:60] = True
|
||||
inner = np.array([[20, 42], [59, 42]])
|
||||
outer = np.array([[20, 32], [59, 32]])
|
||||
upper = np.ones_like(source_mask)
|
||||
monkeypatch.setattr(grow.detector, "detect", lambda image: object())
|
||||
monkeypatch.setattr(grow, "estimate_scale_factor", lambda *args: 50)
|
||||
monkeypatch.setattr(grow, "compute_mask", lambda *args, **kwargs: (
|
||||
source_mask, {"_inner_pts": inner, "_outer_pts": outer, "_upper_mask": upper,
|
||||
"mask_pixels": int(source_mask.sum())}))
|
||||
|
||||
def swap(original, hairline_id, is_hr, ext_mask, denoising_strength, **kwargs):
|
||||
assert original is image
|
||||
assert ext_mask is source_mask
|
||||
generated = np.zeros_like(image)
|
||||
generated[30, :, :] = 180
|
||||
return generated
|
||||
|
||||
def composite(original, generated, mask, *args, **kwargs):
|
||||
assert np.flatnonzero(mask[:, 40]).tolist() == list(range(25 + dy, 45 + dy))
|
||||
assert np.flatnonzero(generated[:, 40, 0]).tolist() == [30 + dy]
|
||||
return np.where(mask[..., None], generated, original), mask.astype(np.float32)
|
||||
|
||||
def encode_red_mask(mask, h, w):
|
||||
assert np.flatnonzero(mask[:, 40]).tolist() == list(range(27 + dy, 38 + dy))
|
||||
return "verified"
|
||||
|
||||
monkeypatch.setattr(grow, "_call_swap", swap)
|
||||
monkeypatch.setattr(grow, "_composite", composite)
|
||||
monkeypatch.setattr(grow, "_red_mask_b64", encode_red_mask)
|
||||
result = grow.generate_hairline_redraw(image, "chang_tuoyuan", paste_dy=dy)
|
||||
assert result["steps"]["redraw_band_mask_base64"] == "verified"
|
||||
@@ -16,21 +16,21 @@ def _landmarks(width=320, height=400):
|
||||
return lm
|
||||
|
||||
|
||||
def test_target_uses_mean_courts_without_old_estimation_ratio():
|
||||
def test_target_is_lowered_by_five_percent_of_brow_to_chin():
|
||||
rule = height_target(_landmarks(), 320, 400)
|
||||
assert rule["middle_height_px"] == pytest.approx(80, abs=1e-4)
|
||||
assert rule["lower_height_px"] == pytest.approx(60, abs=1e-4)
|
||||
assert rule["target_upper_height_px"] == pytest.approx(70, abs=1e-4)
|
||||
assert rule["target_center"]["y"] == pytest.approx(110, abs=1e-4)
|
||||
assert rule["target_upper_height_px"] == pytest.approx(63, abs=1e-4)
|
||||
assert rule["target_center"]["y"] == pytest.approx(117, abs=1e-4)
|
||||
|
||||
|
||||
def test_reference_uses_unrounded_measurement_and_scale():
|
||||
ref = {"brow_center": (161.4, 181.3), "nose_bottom": (160.0, 261.9),
|
||||
"chin_tip": (160.0, 321.5), "px_per_cm": 20}
|
||||
rule = height_target(_landmarks(), 320, 400, ref)
|
||||
assert rule["target_upper_height_px"] == pytest.approx(70.1)
|
||||
assert rule["target_center"]["y"] == pytest.approx(111.2)
|
||||
assert rule["target_upper_height_cm"] == pytest.approx(3.505)
|
||||
assert rule["target_upper_height_px"] == pytest.approx(63.09)
|
||||
assert rule["target_center"]["y"] == pytest.approx(118.21)
|
||||
assert rule["target_upper_height_cm"] == pytest.approx(3.1545)
|
||||
assert rule["measurement_source"] == "face_measure"
|
||||
|
||||
|
||||
|
||||
+10
-14
@@ -66,22 +66,21 @@ def test_rewritten_peer_or_invalid_policy_fail_closed(policy):
|
||||
assert not access.check_access(request("127.0.0.1"))[0]
|
||||
|
||||
|
||||
def test_worker_guard_covers_all_interfaces_before_auth(policy, monkeypatch):
|
||||
def test_worker_guard_covers_all_interfaces(policy):
|
||||
import app as worker
|
||||
monkeypatch.setattr(worker, "ACCEPT_PASSWORDS", ["access-test"])
|
||||
headers = {"X-Internal-Token": "access-test"}
|
||||
with TestClient(worker.app, client=("203.0.113.9", 50000)) as client:
|
||||
assert client.post('/api/v1/hairline/generate-52').status_code == 403
|
||||
assert client.post('/api/v1/hairline/generate-52/', headers=headers).status_code == 403
|
||||
assert client.post('/api/v1/hairline/generate-52/').status_code == 403
|
||||
assert client.post('/api/v1/debug/hairline-height-compare').status_code == 403
|
||||
assert client.post('/api/v1/hairline/generate', headers=headers).status_code == 403
|
||||
assert client.post('/api/v1/face/measure', headers=headers).status_code == 403
|
||||
assert client.post('/api/v1/redraw', headers=headers).status_code == 403
|
||||
assert client.post('/api/v1/hairline/generate').status_code == 403
|
||||
assert client.post('/api/v1/face/measure').status_code == 403
|
||||
assert client.post('/api/v1/redraw').status_code == 403
|
||||
assert client.post('/api/v1/hair/grow-22').status_code == 403
|
||||
assert client.get('/health').status_code == 403
|
||||
assert client.get('/openapi.json').status_code == 403
|
||||
policy.write_text('{"allowed_ips": ["203.0.113.9"]}')
|
||||
assert client.post('/api/v1/hairline/generate-52', headers=headers).json()['code'] == 1004
|
||||
assert client.post('/api/v1/hairline/generate-52').status_code == 401
|
||||
assert client.post('/api/v1/hairline/generate-52').json()['code'] == 1004
|
||||
assert client.post('/api/v1/hair/grow-22').status_code == 422
|
||||
|
||||
|
||||
def test_gateway_does_not_apply_worker_ip_policy(policy, monkeypatch):
|
||||
@@ -105,16 +104,13 @@ def test_gateway_does_not_apply_worker_ip_policy(policy, monkeypatch):
|
||||
("/api/v1/hairline/generate-52", 1004),
|
||||
("/api/v1/face/b-end-points", 1007),
|
||||
])
|
||||
def test_52_and_72_use_worker_transport_auth_not_client_bearer(policy, monkeypatch, path, expected_code):
|
||||
def test_52_and_72_accept_allowlisted_peer_without_token(policy, path, expected_code):
|
||||
import app as worker
|
||||
|
||||
policy.write_text('{"allowed_ips": ["49.232.51.171"]}')
|
||||
monkeypatch.setattr(worker, "ACCEPT_PASSWORDS", ["access-test"])
|
||||
client = TestClient(worker.app, client=("49.232.51.171", 50000))
|
||||
try:
|
||||
denied = client.post(path, headers={"Authorization": "Bearer client-access-token"})
|
||||
assert denied.status_code == 401 and denied.json()["code"] == 1009
|
||||
accepted = client.post(path, headers={"X-Internal-Token": "access-test"})
|
||||
accepted = client.post(path)
|
||||
assert accepted.status_code == 200 and accepted.json()["code"] == expected_code
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
Reference in New Issue
Block a user