diff --git a/face_features.py b/face_features.py index f5f87c1..0710c66 100644 --- a/face_features.py +++ b/face_features.py @@ -55,17 +55,21 @@ _client = None def _load_api_key() -> str | None: - """ARK_API_KEY 环境变量优先,否则读 worker_config.json 的 ark_api_key。""" + """ARK_API_KEY 环境变量优先,否则读 worker_config.json / gateway/config.json 的 ark_api_key。""" key = os.getenv("ARK_API_KEY") if key: return key - cfg = os.path.join(os.path.dirname(__file__), "worker_config.json") - if os.path.isfile(cfg): - try: - with open(cfg, encoding="utf-8") as f: - return json.load(f).get("ark_api_key") - except Exception as e: # noqa: BLE001 - logger.warning("读取 worker_config.json ark_api_key 失败:%s", e) + base = os.path.dirname(__file__) + for cfg_name in ("worker_config.json", "gateway/config.json"): + cfg = os.path.join(base, cfg_name) + if os.path.isfile(cfg): + try: + with open(cfg, encoding="utf-8") as f: + v = json.load(f).get("ark_api_key") + if v: + return v + except Exception as e: # noqa: BLE001 + logger.warning("读取 %s ark_api_key 失败:%s", cfg_name, e) return None diff --git a/gateway/app.py b/gateway/app.py index d303f93..7da2d43 100644 --- a/gateway/app.py +++ b/gateway/app.py @@ -5,11 +5,17 @@ """ import asyncio +import base64 +import json import logging import time from contextlib import asynccontextmanager +from io import BytesIO from pathlib import Path -from fastapi import FastAPI, Request +from typing import Optional + +from fastapi import FastAPI, File, Form, Request, UploadFile +from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from gateway.config import load_config @@ -217,9 +223,6 @@ _GROW_B_FORMS = { "marked_image_file": {"type": "file", "description": "划线图片文件"}, "marked_image_url": {"type": "string", "description": "划线图片 URL"}, "marked_image_base64": {"type": "string", "description": "划线图片 base64"}, - "original_image_file": {"type": "file", "description": "原始用户照片文件"}, - "original_image_url": {"type": "string", "description": "原始用户照片 URL"}, - "original_image_base64": {"type": "string", "description": "原始用户照片 base64"}, } @@ -242,9 +245,67 @@ async def hair_grow_b(request: Request): @app.post("/api/v1/face/features", tags=["人脸分析"]) -async def face_features(request: Request): - """接口4:用户特征分析""" - return await _proxy(request, "/api/v1/face/features") +async def face_features( + image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + image_url: Optional[str] = Form(default=None, description="图片 URL"), + image_base64: Optional[str] = Form(default=None, description="图片 base64(需带前缀)"), +): + """接口4:用户特征分析 — 本机直接调豆包视觉模型,不经过 worker。""" + import uuid as _uuid + + # 三选一校验 + provided = [x for x in (image_file, image_url, image_base64) if x] + if len(provided) != 1: + return JSONResponse(status_code=400, content={ + "code": 1007, "message": "图片参数错误:必须且只能传 image_file / image_url / image_base64 其中一个", + "request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None, + }) + + img_bytes = None + if image_file: + raw = await image_file.read() + if len(raw) > 1_000_000: + return JSONResponse(status_code=400, content={ + "code": 1006, "message": "文件超出 1 MB 限制", + "request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None, + }) + img_bytes = raw + elif image_base64: + b64 = image_base64 + if "," in b64: + b64 = b64.split(",", 1)[1] + try: + img_bytes = base64.b64decode(b64) + except Exception: + return JSONResponse(status_code=400, content={ + "code": 1008, "message": "图片格式不支持(base64 解码失败)", + "request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None, + }) + + from fastapi.concurrency import run_in_threadpool + from face_features import analyze_features, has_face + + try: + feats = await run_in_threadpool(analyze_features, img_bytes, image_url) + except Exception as ex: + logger.exception("接口4 豆包调用失败") + return JSONResponse(status_code=503, content={ + "code": 1007, "message": f"分析服务异常:{ex}", + "request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None, + }) + + if not has_face(feats): + return JSONResponse(status_code=400, content={ + "code": 1001, "message": "无法识别人像", + "request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None, + }) + + return JSONResponse(status_code=200, content={ + "code": 0, + "message": "success", + "request_id": f"gw-{_uuid.uuid4().hex[:8]}", + "data": {"features": json.dumps(feats, ensure_ascii=False)}, + }) @app.post("/api/v1/hairline/generate", tags=["人脸分析"]) diff --git a/gateway/config.example.json b/gateway/config.example.json index 04f6024..d47dd26 100644 --- a/gateway/config.example.json +++ b/gateway/config.example.json @@ -13,6 +13,7 @@ "unhealthy_threshold": 2, "healthy_threshold": 1 }, + "ark_api_key": "", "dispatch": { "per_worker_concurrency": 1, "queue_wait_seconds": 30,