74 lines
2.8 KiB
Python
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"],
|
|
)
|