Download api/product_visual.py from zlcy/abc: direct link, hf CLI and curl.
- Browser
- Download file 7.55 kB
-
https://huggingface.co/spaces/zlcy/abc/resolve/main/api/product_visual.py
- Command line
-
hf download hf://spaces/zlcy/abc/api/product_visual.py
-
curl -L -o product_visual.py https://huggingface.co/spaces/zlcy/abc/resolve/main/api/product_visual.py
7.55 kB
| """商品视觉 API 路由(Product Visual) | |
| 对齐 Xiaoye AI 商品图工作台能力: | |
| - POST /api/product-visual/tasks 创建商品素材包/详情页任务 | |
| - GET /api/product-visual/tasks 查询任务列表(?ids=a,b) | |
| - GET /api/product-visual/tasks/{task_id} 查询单个任务 | |
| - POST /api/product-visual/tasks/{task_id}/items/{seq}/retry 单张重试 | |
| 请求体(multipart/form-data): | |
| - client_task_id: str | |
| - prompt: str 商品描述/卖点/规格 | |
| - scenario: asset_pack | detail_page | |
| - platform: auto | taobao_tmall | jd | pdd | douyin | xiaohongshu | amazon | tiktok_shop | shopee | |
| - output_count: 6 | 8 | 10 | 12 | |
| - model: str (默认 gpt-image-2) | |
| - image: 文件(一张或多张商品图,字段名 image,可重复) | |
| """ | |
| from __future__ import annotations | |
| from fastapi import APIRouter, Header, HTTPException, Query, Request, Response | |
| from fastapi.concurrency import run_in_threadpool | |
| from api.image_inputs import parse_image_edit_request, read_image_sources | |
| from api.support import require_identity, resolve_image_base_url | |
| from services.product_visual_service import PLATFORM_SPECS, product_visual_service | |
| def _clean(value: object, default: str = "") -> str: | |
| return str(value if value is not None else default).strip() | |
| def _extract_product_visual_fields(payload: dict[str, object]) -> dict[str, object]: | |
| """从请求载荷中提取商品视觉参数,非法值回退默认。""" | |
| scenario = _clean(payload.get("scenario"), "asset_pack").lower() | |
| if scenario not in {"asset_pack", "detail_page"}: | |
| scenario = "asset_pack" | |
| platform = _clean(payload.get("platform"), "auto").lower() | |
| if platform not in PLATFORM_SPECS: | |
| platform = "auto" | |
| try: | |
| output_count = int(payload.get("output_count") or 8) | |
| except (TypeError, ValueError): | |
| output_count = 8 | |
| if output_count not in {6, 8, 10, 12}: | |
| output_count = 8 | |
| return {"scenario": scenario, "platform": platform, "output_count": output_count} | |
| def create_router() -> APIRouter: | |
| router = APIRouter() | |
| async def create_product_visual_task( | |
| request: Request, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| identity = require_identity(authorization) | |
| payload, image_sources, _mask_sources = await parse_image_edit_request(request) | |
| client_task_id = str(payload.get("client_task_id") or "").strip() | |
| if not client_task_id: | |
| raise HTTPException(status_code=400, detail={"error": "client_task_id is required"}) | |
| prompt = str(payload.get("prompt") or "").strip() | |
| if not prompt: | |
| raise HTTPException(status_code=400, detail={"error": "prompt is required"}) | |
| fields = _extract_product_visual_fields(payload) | |
| scenario = fields["scenario"] | |
| platform = fields["platform"] | |
| output_count = fields["output_count"] | |
| model = str(payload.get("model") or "gpt-image-2").strip() | |
| mode = str(payload.get("mode") or "concurrent").strip().lower() | |
| images = await read_image_sources(image_sources) | |
| try: | |
| return await run_in_threadpool( | |
| product_visual_service.submit, | |
| identity, | |
| client_task_id=client_task_id, | |
| scenario=scenario, | |
| platform=platform, | |
| output_count=output_count, | |
| prompt=prompt, | |
| images=images, | |
| model=model, | |
| mode=mode, | |
| base_url=resolve_image_base_url(request), | |
| ) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc | |
| async def list_product_visual_tasks( | |
| ids: str = Query(default=""), | |
| authorization: str | None = Header(default=None), | |
| ): | |
| identity = require_identity(authorization) | |
| task_ids = [item.strip() for item in ids.split(",") if item.strip()] | |
| return await run_in_threadpool(product_visual_service.list_tasks, identity, task_ids) | |
| async def get_product_visual_task( | |
| task_id: str, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| identity = require_identity(authorization) | |
| task = await run_in_threadpool(product_visual_service.get_task, identity, task_id) | |
| if task is None: | |
| raise HTTPException(status_code=404, detail={"error": "商品视觉任务不存在"}) | |
| return task | |
| async def get_product_visual_source_image( | |
| task_id: str, | |
| index: int, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| identity = require_identity(authorization) | |
| source = await run_in_threadpool( | |
| product_visual_service.get_source_image, | |
| identity, | |
| task_id, | |
| index, | |
| ) | |
| if source is None: | |
| raise HTTPException(status_code=404, detail={"error": "参考原图不存在"}) | |
| content, media_type, filename = source | |
| return Response( | |
| content=content, | |
| media_type=media_type, | |
| headers={ | |
| "Cache-Control": "private, max-age=86400", | |
| "Content-Disposition": f'inline; filename="{filename}"', | |
| }, | |
| ) | |
| async def retry_product_visual_item( | |
| task_id: str, | |
| seq: int, | |
| request: Request, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| identity = require_identity(authorization) | |
| try: | |
| return await run_in_threadpool( | |
| product_visual_service.retry_item, | |
| identity, | |
| task_id, | |
| seq, | |
| resolve_image_base_url(request), | |
| ) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc | |
| async def iterate_product_visual_item( | |
| task_id: str, | |
| seq: int, | |
| request: Request, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| identity = require_identity(authorization) | |
| try: | |
| payload = await request.json() | |
| except Exception: # noqa: BLE001 | |
| raise HTTPException(status_code=400, detail={"error": "请求体必须是 JSON"}) from None | |
| if not isinstance(payload, dict): | |
| raise HTTPException(status_code=400, detail={"error": "请求体必须是 JSON"}) from None | |
| revision = _clean(payload.get("revision")) | |
| if not revision: | |
| raise HTTPException(status_code=400, detail={"error": "修改意见不能为空"}) | |
| mask_data_url = _clean(payload.get("mask")) | |
| try: | |
| return await run_in_threadpool( | |
| product_visual_service.iterate_item, | |
| identity, | |
| task_id, | |
| seq, | |
| revision, | |
| resolve_image_base_url(request), | |
| mask_data_url=mask_data_url, | |
| ) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc | |
| return router | |