Files
2026-05-29 13:27:41 +08:00

74 lines
2.8 KiB
Python

"""POST /api/v1/parse-image —— iOS 端的截图识别接口。
multipart/form-data:
- image: file (JPEG/PNG/HEIC,客户端已压到 < 5MB)
- clusters: str (JSON 序列化的 [{"id":"<uuid>","title":"..."}, ...])
- package_name: str (可选,iOS 通常不传或传 null;Mock 实现不使用)
占坑期实现接 mock_extractor;后续接真模型时只改这里调 extractor_vision。
"""
from __future__ import annotations
import json
import logging
from typing import Optional
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
from pydantic import ValidationError
from app.mock_extractor import mock_extract_image
from app.schemas import ClusterDtoStr, ParseStrResponse
logger = logging.getLogger("shagua.parse_image")
router = APIRouter(prefix="/api/v1")
# 单张图片上限 5MB。客户端正常压完短边 1024 的 JPEG 通常 < 500KB,
# 超过 5MB 是异常情况(原图 / 客户端压缩 bug),直接拒掉省 LLM 钱。
MAX_IMAGE_BYTES = 5 * 1024 * 1024
@router.post("/parse-image", response_model=ParseStrResponse)
async def parse_image(
image: UploadFile = File(...),
clusters: str = Form("[]"),
package_name: Optional[str] = Form(None),
) -> ParseStrResponse:
image_bytes = await image.read()
if not image_bytes:
logger.info("parse_image rejected: no_image bytes=0")
raise HTTPException(status_code=422, detail="no_image")
if len(image_bytes) > MAX_IMAGE_BYTES:
logger.info("parse_image rejected: image_too_large bytes=%d", len(image_bytes))
raise HTTPException(status_code=422, detail="image_too_large")
try:
clusters_raw = json.loads(clusters) if clusters else []
if not isinstance(clusters_raw, list):
raise ValueError("clusters must be a JSON array")
cluster_dtos = [ClusterDtoStr(**c) for c in clusters_raw]
except (json.JSONDecodeError, ValueError, TypeError, ValidationError) as e:
# ValidationError 不是 ValueError 子类(pydantic v2),必须显式 catch,
# 否则客户端发字段不全的 cluster 会让接口冒 500
logger.info("parse_image rejected: invalid_clusters err=%s", e)
raise HTTPException(status_code=422, detail="invalid_clusters")
logger.info(
"parse_image request bytes=%d package=%s clusters=%d",
len(image_bytes),
package_name,
len(cluster_dtos),
)
result = await mock_extract_image(image_bytes, cluster_dtos)
if not result.get("success"):
raise HTTPException(status_code=422, detail=result.get("reason", "extract_failed"))
return ParseStrResponse(
title=result["title"],
price=result["price"],
source_app=result["source_app"],
cluster_id=result.get("cluster_id"),
typical_price=result["typical_price"],
)