feat(gateway): 接入42和72并为42/52/72启用可刷新令牌

This commit is contained in:
xsl
2026-11-07 20:50:22 +08:00
parent dc53a0ecfe
commit 51baf96d78
24 changed files with 1312 additions and 126 deletions
+88 -24
View File
@@ -6,17 +6,17 @@
import asyncio
import base64
import hmac
import json
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, File, Form, Header, Request, UploadFile
from fastapi import Depends, FastAPI, File, Form, Request, UploadFile
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from gateway.client_auth import client_auth_error, router as client_auth_router
from gateway.config import load_config
from gateway.logging_middleware import (
get_available_dates,
@@ -91,6 +91,8 @@ app = FastAPI(
lifespan=lifespan,
)
app.include_router(client_auth_router)
# 静态文件托管(阶段四完善)
static_root = Path(__file__).resolve().parent.parent / "static"
static_root.mkdir(parents=True, exist_ok=True)
@@ -250,6 +252,8 @@ async def index():
"if2_hair_grow": "/static/test_interface2.html",
"if3_hair_grow_b": "/static/test_interface3.html",
"if4_features": "/static/test_interface4.html",
"if42_features": "/static/test_interface42.html",
"if72_b_end_points": "/static/test_interface72.html",
"if5_hairline": "/static/test_interface5.html",
"if52_hairline": "/static/test_interface52.html",
"if6_measure_v2": "/static/test_interface6.html",
@@ -596,6 +600,24 @@ async def face_features(
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带前缀)"),
):
"""接口4:用户特征分析 — 豆包 6 项 + 本机 MediaPipe 脸型(face/),不经过 worker。"""
return await _face_features_response(request, image_file, image_url, image_base64)
@app.post("/api/v1/face/features-42", summary="42号接口:用户特征与两项判定", tags=["人脸分析"])
async def face_features_42(
request: Request,
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG)"),
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 Base64"),
auth_error=Depends(client_auth_error),
):
"""质检、扶正裁切后执行接口4分析,再加入颧弓外扩与太阳穴凹陷。"""
if auth_error is not None:
return auth_error
return await _face_features_response(request, image_file, image_url, image_base64, include_ratios=True)
async def _face_features_response(request, image_file, image_url, image_base64, *, include_ratios=False):
import uuid as _uuid
# 三选一校验
@@ -644,6 +666,25 @@ async def face_features(
from fastapi.concurrency import run_in_threadpool
from face_features import analyze_features
ratio_features = {}
if include_ratios:
from face_analysis.face_ratios import PortraitQualityError
try:
img_bytes, ratio_features = await _prepare_features_42(img_bytes, image_url)
except PortraitQualityError as ex:
return JSONResponse(status_code=200, content={
"code": ex.code, "message": str(ex),
"request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None,
})
except Exception:
logger.exception("接口42 图片预处理失败")
return JSONResponse(status_code=200, content={
"code": 1007, "message": "图片预处理失败",
"request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None,
})
image_url = None
try:
feats = await run_in_threadpool(analyze_features, img_bytes, image_url)
except Exception as ex:
@@ -663,10 +704,42 @@ async def face_features(
"code": 0,
"message": "success",
"request_id": f"gw-{_uuid.uuid4().hex[:8]}",
"data": {"features": json.dumps(feats, ensure_ascii=False)},
"data": {"features": json.dumps({**feats, **ratio_features}, ensure_ascii=False)},
})
async def _prepare_features_42(image_bytes, image_url):
from fastapi.concurrency import run_in_threadpool
if image_url:
import httpx
try:
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
response = await client.get(image_url)
response.raise_for_status()
image_bytes = response.content
except httpx.HTTPError as exc:
from face_analysis.face_ratios import PortraitQualityError
raise PortraitQualityError(1008, "图片下载失败(URL 不可达)") from exc
async with _if99_lock:
return await run_in_threadpool(_preprocess_and_measure_42, image_bytes)
def _preprocess_and_measure_42(image_bytes):
import cv2
from face_analysis.face_ratios import analyze_ratio_features, prepare_feature_portrait
image, landmarks = prepare_feature_portrait(image_bytes)
success, encoded = cv2.imencode(".png", image)
if not success:
raise RuntimeError("处理后图片编码失败")
ratios = analyze_ratio_features(image, landmarks)
return encoded.tobytes(), ratios
@app.post("/api/v1/hairline/generate", tags=["人脸分析"])
async def hairline_generate(request: Request):
"""接口5:发际线PNG生成"""
@@ -675,30 +748,21 @@ async def hairline_generate(request: Request):
@app.post("/api/v1/hairline/generate-52", tags=["人脸分析"],
summary="接口52:发际线新规则与人像裁切")
async def hairline_generate_52(
request: Request,
api_token: Optional[str] = Header(default=None, alias="X-API-Token",
description="52号接口客户端Token,默认yonghe1999"),
):
"""透传图片三选一、gender、hair_style及normalize_image等参数至GPU worker。
worker负责中下庭均值定位、扶正裁切和生发;网关沿用原有派发、鉴权与图片URL改写。
空图片保持原有 *_base64: null 字段。接口5保持原样。
客户端须传X-API-Token;缺少或不匹配时返回HTTP401/code1009,不进入派发队列。
"""
from uuid import uuid4
from gateway.config import DEFAULTS, get_config
expected = get_config().get("interface52_token", DEFAULTS["interface52_token"])
if (not isinstance(expected, str) or not expected or not api_token
or not hmac.compare_digest(api_token.encode("utf-8"), expected.encode("utf-8"))):
return JSONResponse(status_code=401, content={
"code": 1009, "message": "未授权:缺少或错误的 X-API-Token",
"request_id": f"gw-{uuid4().hex[:8]}", "data": None,
})
async def hairline_generate_52(request: Request, auth_error=Depends(client_auth_error)):
"""鉴权后透传图片、gender、hair_style 与 normalize_image 至 GPU worker。"""
if auth_error is not None:
return auth_error
return await _proxy(request, "/api/v1/hairline/generate-52")
@app.post("/api/v1/face/b-end-points", summary="72号接口:B 端五个关键点", tags=["人脸分析"])
async def face_b_end_points(request: Request, auth_error=Depends(client_auth_error)):
"""鉴权后转发到 worker,图片按网关规则返回 URL。"""
if auth_error is not None:
return auth_error
return await _proxy(request, "/api/v1/face/b-end-points")
@app.post("/api/v1/hair/grow-v2", tags=["生发"])
async def hair_grow_v2(request: Request):
"""接口7:C端生发 v2(add_hair2 工作流)"""