From 4c7681b33805cb7bb0316770cffdae1a9c4fad18 Mon Sep 17 00:00:00 2001 From: xsl Date: Mon, 15 Jun 2026 23:35:24 +0800 Subject: [PATCH] =?UTF-8?q?perf(=E5=9B=BE=E7=89=87):=20=E6=8E=A5=E5=8F=A32?= =?UTF-8?q?/3/5=20=E8=BF=94=E5=9B=9E=20JPG(=E4=BD=93=E7=A7=AF~9=C3=97?= =?UTF-8?q?=E2=86=93)=EF=BC=8C=E6=8E=A5=E5=8F=A31=20=E6=A0=87=E6=B3=A8?= =?UTF-8?q?=E5=9B=BE=E4=BB=8D=20PNG(=E9=80=8F=E6=98=8E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app.py: 接口2(预览+生发)/3(生发)/5(发际线叠图) 编码改 JPG(质量90,env JPG_QUALITY); 接口1 annotated_image 含透明仍 PNG。_png_to_jpg_b64 把 ComfyUI 的 PNG 重编码为 JPG(无法解码则透传) - gateway/forward.py: 落盘按内容嗅探扩展名(PNG头→.png 否则.jpg),原先硬编码 .png - 测试/文档同步;实测接口5 一张 59KB(JPG) vs 548KB(PNG)。pytest 44 全绿 Co-Authored-By: Claude Opus 4.8 --- app.py | 31 +++++++++++++++++++++++-------- docs/实现说明.md | 2 ++ gateway/forward.py | 5 +++-- tests/test_api.py | 15 ++++++++------- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/app.py b/app.py index cc6fe89..125bafc 100644 --- a/app.py +++ b/app.py @@ -226,6 +226,24 @@ async def resolve_image_bytes(image_file, image_url, image_base64): return None, err(1008, "base64 解码失败") +# 接口2/3/5 返回不透明照片,用 JPG 显著减小体积(接口1 标注图含透明,仍用 PNG) +_JPG_QUALITY = int(os.getenv("JPG_QUALITY", "90")) + + +def _jpg_b64(bgr) -> str: + """BGR 图 → JPG base64。""" + _ok, buf = cv2.imencode(".jpg", bgr, [cv2.IMWRITE_JPEG_QUALITY, _JPG_QUALITY]) + return base64.b64encode(buf.tobytes()).decode() + + +def _png_to_jpg_b64(png_bytes) -> str: + """ComfyUI 返回的 PNG 字节 → 重编码为 JPG base64;无法解码则原样透传。""" + img = cv2.imdecode(np.frombuffer(png_bytes, np.uint8), cv2.IMREAD_COLOR) + if img is None: + return base64.b64encode(png_bytes).decode() + return _jpg_b64(img) + + # --------------------------------------------------------------------------- # 通用图片请求 Body(JSON 方式,用于 url / base64) # --------------------------------------------------------------------------- @@ -514,12 +532,10 @@ async def hair_grow( results = [] for p in items: - _ok, png = cv2.imencode(".png", p["image_bgr"]) - grown_b64 = (base64.b64encode(p["grown_png"]).decode() - if p["grown_png"] else None) results.append({ - "image_base64": base64.b64encode(png.tobytes()).decode(), - "grown_image_base64": grown_b64, + "image_base64": _jpg_b64(p["image_bgr"]), # 预览图 JPG + "grown_image_base64": (_png_to_jpg_b64(p["grown_png"]) # 生发图 JPG + if p["grown_png"] else None), "hairline_type": p["hairline_type"], "order": p["order"], }) @@ -602,7 +618,7 @@ async def hair_grow_b( if res["status"] == "no_line": return err(1001, "未检测到发际线划线,请确认划线图额头有清晰的手绘发际线") - grown_b64 = base64.b64encode(res["grown_png"]).decode() if res["grown_png"] else None + grown_b64 = _png_to_jpg_b64(res["grown_png"]) if res["grown_png"] else None # 生发图 JPG data = { "hair_growth_image_base64": grown_b64, "hairline_type": "custom", @@ -783,9 +799,8 @@ async def hairline_generate( hairline_images = [] for it in res["images"]: - _ok, png = cv2.imencode(".png", it["image_bgr"]) hairline_images.append({ - "image_base64": base64.b64encode(png.tobytes()).decode(), + "image_base64": _jpg_b64(it["image_bgr"]), # 发际线叠图 JPG "order": it["order"], }) c = res["best_center"] diff --git a/docs/实现说明.md b/docs/实现说明.md index a511b9d..9af7ccd 100644 --- a/docs/实现说明.md +++ b/docs/实现说明.md @@ -35,6 +35,8 @@ | 4 | (网关本机产出,无图片字段,`features` 为 JSON 字符串) | — | > 实现建议:递归遍历 data,凡 key 以 `_base64` 结尾就落盘改 `_url`,自动覆盖嵌套/新增字段。 +> **图片格式**:接口1 标注图含透明用 **PNG**;接口2/3/5 是不透明照片用 **JPG**(小很多,~9×)。 +> 网关落盘按内容嗅探扩展名(PNG 头→`.png`,否则 `.jpg`)。 ### 错误码 diff --git a/gateway/forward.py b/gateway/forward.py index 2372233..5965e63 100644 --- a/gateway/forward.py +++ b/gateway/forward.py @@ -102,8 +102,9 @@ def rewrite_base64_to_url( if key.endswith("_base64") and isinstance(value, str): img_bytes = _decode_base64_value(value) if img_bytes is not None: - # 生成文件名并落盘 - filename = f"{uuid.uuid4().hex}.png" + # 按内容嗅探扩展名:PNG(接口1标注图,含透明) / JPEG(接口2/3/5 照片) + ext = "png" if img_bytes[:8] == b"\x89PNG\r\n\x1a\n" else "jpg" + filename = f"{uuid.uuid4().hex}.{ext}" filepath = Path(static_dir) / filename filepath.write_bytes(img_bytes) diff --git a/tests/test_api.py b/tests/test_api.py index ff11a9d..220b80a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -82,9 +82,10 @@ def test_grow_missing_gender_1004(client): assert r.json()["code"] == 1004 -_PNG_1x1 = base64.b64decode( - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" -) +# mock ComfyUI 输出:一张合法 PNG(worker 会把它重编码成 JPG) +import cv2 as _cv2 +import numpy as _np +_PNG_1x1 = _cv2.imencode(".png", _np.full((8, 8, 3), 200, _np.uint8))[1].tobytes() def test_grow_female_returns_5(client, monkeypatch): @@ -99,8 +100,8 @@ def test_grow_female_returns_5(client, monkeypatch): results = body["data"]["results"] assert [x["hairline_type"] for x in results] == ["ellipse", "flower", "heart", "straight", "wave"] assert [x["order"] for x in results] == [1, 2, 3, 4, 5] - assert base64.b64decode(results[0]["image_base64"])[:8] == b"\x89PNG\r\n\x1a\n" - assert base64.b64decode(results[0]["grown_image_base64"])[:8] == b"\x89PNG\r\n\x1a\n" + assert base64.b64decode(results[0]["image_base64"])[:3] == b"\xff\xd8\xff" # JPEG + assert base64.b64decode(results[0]["grown_image_base64"])[:3] == b"\xff\xd8\xff" # JPEG assert "image_url" not in results[0] @@ -126,7 +127,7 @@ def test_growb_success(client, monkeypatch): assert body["code"] == 0, body d = body["data"] assert d["hairline_type"] == "custom" - assert base64.b64decode(d["hair_growth_image_base64"])[:8] == b"\x89PNG\r\n\x1a\n" + assert base64.b64decode(d["hair_growth_image_base64"])[:3] == b"\xff\xd8\xff" # JPEG assert "best_hairline_image_base64" not in d # 已去掉该字段 assert "best_hairline_image_url" not in d @@ -145,7 +146,7 @@ def test_hairline_gen_female(client): assert body["code"] == 0, body d = body["data"] assert [x["order"] for x in d["hairline_images"]] == [1, 2, 3, 4, 5] - assert base64.b64decode(d["hairline_images"][0]["image_base64"])[:8] == b"\x89PNG\r\n\x1a\n" + assert base64.b64decode(d["hairline_images"][0]["image_base64"])[:3] == b"\xff\xd8\xff" # JPEG c = d["best_hairline_center_point"] assert 0 <= c["x"] <= 682 and 0 <= c["y"] <= 811 # 落在原图范围内 assert "image_url" not in d["hairline_images"][0]