diff --git a/app.py b/app.py index 1a1f284..5f62525 100644 --- a/app.py +++ b/app.py @@ -30,14 +30,6 @@ logger = logging.getLogger("hair.worker") BASE_URL = "https://hair.xiangsilian.com" SAMPLE_IMAGE_URL = f"{BASE_URL}/static/sample.jpg" -# 分辨率门槛(短边/长边,方向无关),可由环境变量覆盖(技术方案 §8.3) -MIN_SHORT_SIDE = int(os.getenv("MIN_SHORT_SIDE", "600")) -MIN_LONG_SIDE = int(os.getenv("MIN_LONG_SIDE", "800")) -# 文件大小上限(字节),可由环境变量覆盖;设为 0 表示不限制 -MAX_FILE_BYTES = int(os.getenv("MAX_FILE_BYTES", "1000000")) # 默认 1 MB -if MAX_FILE_BYTES <= 0: - MAX_FILE_BYTES = float("inf") - # 运行期状态:模型就绪标志(/health 据此返回 200/503) _STATE = {"ready": False} @@ -103,7 +95,7 @@ app = FastAPI( | 方式 | 字段名 | 说明 | |------|--------|------| -| 文件上传 | `image_file` | `multipart/form-data`,单文件 ≤ 1 MB | +| 文件上传 | `image_file` | `multipart/form-data`,单文件上传 | | URL | `image_url` | 图片的完整 HTTP/HTTPS 地址 | | base64 | `image_base64` | 需携带前缀,如 `data:image/jpeg;base64,xxxx` | @@ -112,9 +104,9 @@ app = FastAPI( ## 图片要求 - 格式:**JPG / PNG** -- 分辨率:最低 1080×1920,最大 4000×5000 +- 分辨率:不限制 - 人脸数量:仅支持**单人**,多人返回错误码 `1005` -- 文件大小:≤ 1 MB(文件上传方式) +- 文件大小:不限制 ## 统一响应结构 @@ -134,11 +126,9 @@ app = FastAPI( | code | 说明 | |------|------| | 1001 | 无法识别人像 | -| 1002 | 人像分辨率过低 | | 1003 | 非正面照 / 角度过大 | | 1004 | 性别标签无法判定 | | 1005 | 检测到多张人脸(仅支持单人)| -| 1006 | 文件超出 1 MB 限制 | | 1007 | 图片参数错误(未传或同时传多个)| | 1008 | 图片格式不支持(仅 JPG / PNG)| """, @@ -202,7 +192,7 @@ async def resolve_image_bytes(image_file, image_url, image_base64): """三选一取图,返回 (raw_bytes, error_response)。 严格互斥:传 0 个或多个 → 1007;URL 下载失败/base64 解码失败 → 1008。 - 大小校验放到上层(统一 1006),此处只负责取到字节。 + 不限制文件大小,此处只负责取到字节。 """ provided = [x for x in (image_file, image_url, image_base64) if x] if len(provided) != 1: @@ -280,7 +270,7 @@ def _parse_hair_styles(raw: Optional[str], max_styles: int) -> Optional[list[int _image_fields_desc = ( "图片传参方式严格互斥,必须且只能选其一:\n" - "- **image_file**(multipart/form-data 上传,≤ 1 MB)\n" + "- **image_file**(multipart/form-data 上传)\n" "- **image_url**(完整 HTTP/HTTPS 地址)\n" "- **image_base64**(需携带前缀,如 `data:image/jpeg;base64,xxxx`)\n\n" "同时传多个或一个都不传,均返回错误码 `1007`。" @@ -331,20 +321,12 @@ async def _face_measure_impl(image_file, image_url, image_base64, variant="v1"): if e is not None: return None, e - # 2. 大小校验(≤ 1MB) - if len(raw) > MAX_FILE_BYTES: - return None, err(1006, "文件超出 1 MB 限制") - - # 3. 解码 + # 2. 解码(不限制文件大小 / 分辨率) image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR) if image is None: return None, err(1008, "图片格式不支持(仅 JPG / PNG)") - # 4. 分辨率(短边/长边,方向无关,可配置门槛) h, w = image.shape[:2] - short_side, long_side = min(w, h), max(w, h) - if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE: - return None, err(1002, "人像分辨率过低") try: from face_analysis.detector import detector @@ -488,7 +470,7 @@ async def _face_measure_impl(image_file, image_url, image_base64, variant="v1"): }, ) async def face_measure( - image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + 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(需带 data:image/...;base64, 前缀)"), ): @@ -587,7 +569,7 @@ async def face_measure( }, ) async def face_measure_v2( - image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + 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(需带 data:image/...;base64, 前缀)"), ): @@ -660,7 +642,7 @@ async def face_measure_v2( }, ) async def hair_grow( - image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + 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(需带 data:image/...;base64, 前缀)"), gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"), @@ -683,18 +665,11 @@ async def hair_grow( raw, e = await resolve_image_bytes(image_file, image_url, image_base64) if e is not None: return e - if len(raw) > MAX_FILE_BYTES: - return err(1006, "文件超出 1 MB 限制") image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR) if image is None: return err(1008, "图片格式不支持(仅 JPG / PNG)") - h, w = image.shape[:2] - short_side, long_side = min(w, h), max(w, h) - if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE: - return err(1002, "人像分辨率过低") - try: from fastapi.concurrency import run_in_threadpool from hairline.service import generate_grow_results @@ -781,7 +756,7 @@ _WORKFLOW2_PATH = os.path.join(os.path.dirname(__file__), "add_hair2.json") }, ) async def hair_grow_v2( - image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + 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(需带 data:image/...;base64, 前缀)"), gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"), @@ -804,18 +779,11 @@ async def hair_grow_v2( raw, e = await resolve_image_bytes(image_file, image_url, image_base64) if e is not None: return e - if len(raw) > MAX_FILE_BYTES: - return err(1006, "文件超出 1 MB 限制") image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR) if image is None: return err(1008, "图片格式不支持(仅 JPG / PNG)") - h, w = image.shape[:2] - short_side, long_side = min(w, h), max(w, h) - if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE: - return err(1002, "人像分辨率过低") - try: from fastapi.concurrency import run_in_threadpool from hairline.service import generate_grow_results @@ -883,7 +851,7 @@ async def hair_grow_v2( }, ) async def hair_grow_b( - marked_image_file: Optional[UploadFile] = File(default=None, description="划线图片文件(JPG/PNG,≤ 1 MB)"), + marked_image_file: Optional[UploadFile] = File(default=None, description="划线图片文件(JPG/PNG)"), marked_image_url: Optional[str] = Form(default=None, description="划线图片 URL"), marked_image_base64: Optional[str] = Form(default=None, description="划线图片 base64"), use_mask: bool = Form(default=True, description="是否画发际线(测试对比用)。false 时跳过划线检测、直接送划线图"), @@ -893,18 +861,11 @@ async def hair_grow_b( marked_raw, e = await resolve_image_bytes(marked_image_file, marked_image_url, marked_image_base64) if e is not None: return e - if len(marked_raw) > MAX_FILE_BYTES: - return err(1006, "文件超出 1 MB 限制") marked = cv2.imdecode(np.frombuffer(marked_raw, np.uint8), cv2.IMREAD_COLOR) if marked is None: return err(1008, "图片格式不支持(仅 JPG / PNG)") - h, w = marked.shape[:2] - short_side, long_side = min(w, h), max(w, h) - if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE: - return err(1002, "人像分辨率过低") - try: from fastapi.concurrency import run_in_threadpool from hairline.service import generate_grow_b @@ -985,7 +946,7 @@ async def hair_grow_b( }, ) async def face_features( - image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + 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(需带 data:image/...;base64, 前缀)"), ): @@ -1052,14 +1013,14 @@ async def face_features( "description": "参数错误 / 图片识别失败", "content": { "application/json": { - "example": {"code": 1002, "message": "人像分辨率过低", "request_id": "x", "data": None} + "example": {"code": 1001, "message": "无法识别人像", "request_id": "x", "data": None} } }, }, }, ) async def hairline_generate( - image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + 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(需带 data:image/...;base64, 前缀)"), gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"), @@ -1070,18 +1031,11 @@ async def hairline_generate( raw, e = await resolve_image_bytes(image_file, image_url, image_base64) if e is not None: return e - if len(raw) > MAX_FILE_BYTES: - return err(1006, "文件超出 1 MB 限制") image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR) if image is None: return err(1008, "图片格式不支持(仅 JPG / PNG)") - h, w = image.shape[:2] - short_side, long_side = min(w, h), max(w, h) - if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE: - return err(1002, "人像分辨率过低") - try: from fastapi.concurrency import run_in_threadpool from hairline.service import generate_hairline_pngs diff --git a/gateway/app.py b/gateway/app.py index b58a24d..0cf0c6f 100644 --- a/gateway/app.py +++ b/gateway/app.py @@ -218,7 +218,7 @@ def _proxy(request: Request, path: str): # 声明各接口的 form 参数用于 OpenAPI schema(实际转发直接读 Request) _MEASURE_FORMS = { - "image_file": {"type": "file", "description": "上传图片文件(JPG/PNG,≤ 1 MB)"}, + "image_file": {"type": "file", "description": "上传图片文件(JPG/PNG)"}, "image_url": {"type": "string", "description": "图片 URL"}, "image_base64": {"type": "string", "description": "图片 base64(需带前缀)"}, } @@ -261,7 +261,7 @@ async def hair_grow_b(request: Request): @app.post("/api/v1/face/features", tags=["人脸分析"]) async def face_features( - image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + 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(需带前缀)"), ): @@ -278,14 +278,7 @@ async def face_features( img_bytes = None if image_file: - raw = await image_file.read() - # TODO: 临时取消限制,后续恢复 - # if len(raw) > 1_000_000: - # return JSONResponse(status_code=200, content={ - # "code": 1006, "message": "文件超出 1 MB 限制", - # "request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None, - # }) - img_bytes = raw + img_bytes = await image_file.read() elif image_base64: b64 = image_base64 if "," in b64: diff --git a/hair-worker.service b/hair-worker.service index a686f6b..5f1de10 100644 --- a/hair-worker.service +++ b/hair-worker.service @@ -8,9 +8,6 @@ User=xsl WorkingDirectory=/home/xsl/hair # 鉴权密码:优先 worker_config.json;也可在此用环境变量覆盖 # Environment=WORKER_ACCEPT_PASSWORDS=your-strong-secret -# 分辨率门槛(可选,默认 600/800) -# Environment=MIN_SHORT_SIDE=600 -# Environment=MIN_LONG_SIDE=800 ExecStart=/home/xsl/hair/venv/bin/uvicorn app:app --host 0.0.0.0 --port 8187 Restart=always RestartSec=3 diff --git a/run_worker.sh b/run_worker.sh index ded3e2c..b2a0bd8 100755 --- a/run_worker.sh +++ b/run_worker.sh @@ -18,13 +18,6 @@ LOG_FILE="worker.log" HOST="${HOST:-0.0.0.0}" PORT="${PORT:-8187}" -# ---- 临时取消上传图片限制(分辨率 + 文件大小)---- -# 0 = 不限制。要恢复默认(600/800/1MB),删掉下面三行即可。 -export MIN_SHORT_SIDE="${MIN_SHORT_SIDE:-0}" -export MIN_LONG_SIDE="${MIN_LONG_SIDE:-0}" -export MAX_FILE_BYTES="${MAX_FILE_BYTES:-0}" -# ------------------------------------------------ - if [ ! -x "$VENV_UVICORN" ]; then echo "找不到 $VENV_UVICORN,请先创建 venv 并安装依赖(见 requirements.txt)" >&2 exit 1 diff --git a/tests/conftest.py b/tests/conftest.py index 043d7ed..50b527c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -64,11 +64,3 @@ def build_synthetic_landmarks(px_per_cm=50.0, W=1000, H=1000): lm[474] = _LM(X(cx + ie / 2 + ew / 2 - d / 2), eye_y) lm[476] = _LM(X(cx + ie / 2 + ew / 2 + d / 2), eye_y) return [lm[i] for i in range(478)], gt - - -@pytest.fixture -def oversize_file(tmp_path): - """1006 用例:>1MB 的占位文件(无需合法图片,只看字节数)。""" - p = tmp_path / "oversize.bin" - p.write_bytes(b"\x00" * 1_100_000) - return p diff --git a/tests/test_api.py b/tests/test_api.py index 220b80a..edcc3bb 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -52,17 +52,6 @@ def test_param_multiple_provided_1007(client): assert r.json()["code"] == 1007 -def test_oversize_1006(client, oversize_file): - files = {"image_file": ("oversize.bin", open(oversize_file, "rb"), "application/octet-stream")} - r = client.post(URL, headers=H, files=files) - assert r.json()["code"] == 1006 - - -def test_lowres_1002(client): - r = _post(client, "lowres.png") - assert r.json()["code"] == 1002 - - def test_no_face_1001(client): r = _post(client, "landscape.jpg") assert r.json()["code"] == 1001