|
@@ -2,10 +2,8 @@ import os
|
|
|
import json
|
|
import json
|
|
|
import asyncio
|
|
import asyncio
|
|
|
import aiohttp
|
|
import aiohttp
|
|
|
-import aiofiles
|
|
|
|
|
-from typing import Dict, Any, Tuple, Optional
|
|
|
|
|
-from fastapi import APIRouter, HTTPException, Request
|
|
|
|
|
-from pydantic import BaseModel, Field
|
|
|
|
|
|
|
+from typing import Dict, Any, Tuple, Optional, Union
|
|
|
|
|
+from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Form
|
|
|
|
|
|
|
|
from app.core.config import settings
|
|
from app.core.config import settings
|
|
|
from app.core.logger import get_logger
|
|
from app.core.logger import get_logger
|
|
@@ -15,56 +13,37 @@ logger = get_logger(__name__)
|
|
|
router = APIRouter()
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
-# 定义请求参数模型
|
|
|
|
|
-class AutoImportRequest(BaseModel):
|
|
|
|
|
- card_name: str = Field(..., description="卡牌名称")
|
|
|
|
|
- cardNo: Optional[str] = Field(None, description="卡牌编号")
|
|
|
|
|
- card_type: CardType = Field(CardType.pokemon, description="卡牌类型")
|
|
|
|
|
- is_reflect_card: bool = Field(True, description="是否是反光卡")
|
|
|
|
|
- strict_mode: bool = Field(False, description="如果为True,必须提供所有4张主图")
|
|
|
|
|
-
|
|
|
|
|
- front_ring: Optional[str] = Field(None, description="正面环光图绝对路径")
|
|
|
|
|
- front_coaxial: Optional[str] = Field(None, description="正面同轴光图绝对路径")
|
|
|
|
|
- back_ring: Optional[str] = Field(None, description="背面环光图绝对路径")
|
|
|
|
|
- back_coaxial: Optional[str] = Field(None, description="背面同轴光图绝对路径")
|
|
|
|
|
- front_gray: Optional[str] = Field(None, description="正面灰度图绝对路径")
|
|
|
|
|
- back_gray: Optional[str] = Field(None, description="背面灰度图绝对路径")
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
# --- 内部辅助函数 ---
|
|
# --- 内部辅助函数 ---
|
|
|
-async def call_api_with_file(
|
|
|
|
|
|
|
+async def call_api_with_bytes(
|
|
|
session: aiohttp.ClientSession,
|
|
session: aiohttp.ClientSession,
|
|
|
url: str,
|
|
url: str,
|
|
|
- file_path: str,
|
|
|
|
|
|
|
+ file_bytes: bytes,
|
|
|
|
|
+ filename: str,
|
|
|
params: Dict[str, Any] = None,
|
|
params: Dict[str, Any] = None,
|
|
|
form_fields: Dict[str, Any] = None,
|
|
form_fields: Dict[str, Any] = None,
|
|
|
file_field_name: str = 'file'
|
|
file_field_name: str = 'file'
|
|
|
) -> Tuple[int, bytes]:
|
|
) -> Tuple[int, bytes]:
|
|
|
- """通用的文件上传API调用函数"""
|
|
|
|
|
|
|
+ """通用的文件上传API调用函数 (接收字节数据)"""
|
|
|
form_data = aiohttp.FormData()
|
|
form_data = aiohttp.FormData()
|
|
|
|
|
|
|
|
if form_fields:
|
|
if form_fields:
|
|
|
for key, value in form_fields.items():
|
|
for key, value in form_fields.items():
|
|
|
form_data.add_field(key, str(value))
|
|
form_data.add_field(key, str(value))
|
|
|
|
|
|
|
|
- if not os.path.exists(file_path):
|
|
|
|
|
- raise FileNotFoundError(f"文件不存在: {file_path}")
|
|
|
|
|
-
|
|
|
|
|
- async with aiofiles.open(file_path, 'rb') as f:
|
|
|
|
|
- content = await f.read()
|
|
|
|
|
- form_data.add_field(
|
|
|
|
|
- file_field_name,
|
|
|
|
|
- content,
|
|
|
|
|
- filename=os.path.basename(file_path),
|
|
|
|
|
- content_type='image/jpeg'
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ # 直接将内存中的字节数据添加到表单
|
|
|
|
|
+ form_data.add_field(
|
|
|
|
|
+ file_field_name,
|
|
|
|
|
+ file_bytes,
|
|
|
|
|
+ filename=filename or 'image.jpg',
|
|
|
|
|
+ content_type='image/jpeg'
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
async with session.post(url, data=form_data, params=params) as response:
|
|
async with session.post(url, data=form_data, params=params) as response:
|
|
|
response_content = await response.read()
|
|
response_content = await response.read()
|
|
|
if not response.ok:
|
|
if not response.ok:
|
|
|
logger.error(
|
|
logger.error(
|
|
|
- f"[API Error] {url} -> Status: {response.status}, Msg: {response_content.decode('utf-8')[:100]}")
|
|
|
|
|
|
|
+ f"[API Error] {url} -> Status: {response.status}, Msg: {response_content.decode('utf-8', errors='ignore')[:100]}")
|
|
|
return response.status, response_content
|
|
return response.status, response_content
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.error(f"[Conn Error] {url} -> {e}")
|
|
logger.error(f"[Conn Error] {url} -> {e}")
|
|
@@ -73,18 +52,19 @@ async def call_api_with_file(
|
|
|
|
|
|
|
|
async def process_main_image(
|
|
async def process_main_image(
|
|
|
session: aiohttp.ClientSession,
|
|
session: aiohttp.ClientSession,
|
|
|
- image_path: str,
|
|
|
|
|
|
|
+ file_bytes: bytes,
|
|
|
|
|
+ filename: str,
|
|
|
score_type: str,
|
|
score_type: str,
|
|
|
is_reflect_card: str
|
|
is_reflect_card: str
|
|
|
) -> Dict[str, Any]:
|
|
) -> Dict[str, Any]:
|
|
|
"""调用推理服务,处理主图片"""
|
|
"""调用推理服务,处理主图片"""
|
|
|
- logger.info(f"处理主图: {score_type} -> {os.path.basename(image_path)}")
|
|
|
|
|
|
|
+ logger.info(f"处理主图: {score_type} -> {filename}")
|
|
|
inference_base_url = settings.SCORE_UPDATE_SERVER_URL
|
|
inference_base_url = settings.SCORE_UPDATE_SERVER_URL
|
|
|
|
|
|
|
|
# 1. 获取转正后的图片
|
|
# 1. 获取转正后的图片
|
|
|
rectify_url = f"{inference_base_url}/api/card_inference/card_rectify_and_center"
|
|
rectify_url = f"{inference_base_url}/api/card_inference/card_rectify_and_center"
|
|
|
- rectify_status, rectified_image_bytes = await call_api_with_file(
|
|
|
|
|
- session, url=rectify_url, file_path=image_path
|
|
|
|
|
|
|
+ rectify_status, rectified_image_bytes = await call_api_with_bytes(
|
|
|
|
|
+ session, url=rectify_url, file_bytes=file_bytes, filename=filename
|
|
|
)
|
|
)
|
|
|
if rectify_status >= 300:
|
|
if rectify_status >= 300:
|
|
|
raise HTTPException(status_code=500, detail=f"图片转正失败: {score_type}")
|
|
raise HTTPException(status_code=500, detail=f"图片转正失败: {score_type}")
|
|
@@ -93,8 +73,8 @@ async def process_main_image(
|
|
|
score_url = f"{inference_base_url}/api/card_score/score_inference"
|
|
score_url = f"{inference_base_url}/api/card_score/score_inference"
|
|
|
score_params = {"score_type": score_type, "is_reflect_card": is_reflect_card}
|
|
score_params = {"score_type": score_type, "is_reflect_card": is_reflect_card}
|
|
|
|
|
|
|
|
- score_status, score_json_bytes = await call_api_with_file(
|
|
|
|
|
- session, url=score_url, file_path=image_path, params=score_params
|
|
|
|
|
|
|
+ score_status, score_json_bytes = await call_api_with_bytes(
|
|
|
|
|
+ session, url=score_url, file_bytes=file_bytes, filename=filename, params=score_params
|
|
|
)
|
|
)
|
|
|
if score_status >= 300:
|
|
if score_status >= 300:
|
|
|
raise HTTPException(status_code=500, detail=f"推理分数失败: {score_type}")
|
|
raise HTTPException(status_code=500, detail=f"推理分数失败: {score_type}")
|
|
@@ -146,13 +126,13 @@ async def upload_main_image(session: aiohttp.ClientSession, base_url: str, card_
|
|
|
|
|
|
|
|
|
|
|
|
|
async def upload_gray_image(session: aiohttp.ClientSession, base_url: str, card_id: int, image_type: str,
|
|
async def upload_gray_image(session: aiohttp.ClientSession, base_url: str, card_id: int, image_type: str,
|
|
|
- file_path: str):
|
|
|
|
|
|
|
+ file_bytes: bytes, filename: str):
|
|
|
"""将灰度图源文件上传到自身服务"""
|
|
"""将灰度图源文件上传到自身服务"""
|
|
|
url = f"{base_url}{settings.API_PREFIX}/images/insert/gray/{card_id}"
|
|
url = f"{base_url}{settings.API_PREFIX}/images/insert/gray/{card_id}"
|
|
|
form_fields = {'image_type': image_type}
|
|
form_fields = {'image_type': image_type}
|
|
|
|
|
|
|
|
- status, _ = await call_api_with_file(
|
|
|
|
|
- session, url=url, file_path=file_path, form_fields=form_fields, file_field_name='image'
|
|
|
|
|
|
|
+ status, _ = await call_api_with_bytes(
|
|
|
|
|
+ session, url=url, file_bytes=file_bytes, filename=filename, form_fields=form_fields, file_field_name='image'
|
|
|
)
|
|
)
|
|
|
if status != 201:
|
|
if status != 201:
|
|
|
logger.error(f"[灰度图上传失败] {image_type} code={status}")
|
|
logger.error(f"[灰度图上传失败] {image_type} code={status}")
|
|
@@ -162,67 +142,71 @@ async def upload_gray_image(session: aiohttp.ClientSession, base_url: str, card_
|
|
|
# --- 暴露的API接口 ---
|
|
# --- 暴露的API接口 ---
|
|
|
|
|
|
|
|
@router.post("/process_and_import", summary="自动化处理并导入卡牌数据")
|
|
@router.post("/process_and_import", summary="自动化处理并导入卡牌数据")
|
|
|
-async def auto_import_script_api(request: Request, req_data: AutoImportRequest):
|
|
|
|
|
- """
|
|
|
|
|
- 接收本地的图片路径,自动转正、推理分数、建立卡片关联并入库。
|
|
|
|
|
-参数如下:
|
|
|
|
|
-
|
|
|
|
|
- card_name: 卡牌名称
|
|
|
|
|
- cardNo: 卡牌编号
|
|
|
|
|
- card_type: 卡牌类型
|
|
|
|
|
- is_reflect_card: 是否是反光卡
|
|
|
|
|
- strict_mode: 如果为True,必须提供所有4张主图
|
|
|
|
|
- front_ring: 正面环光图绝对路径
|
|
|
|
|
- front_coaxial: 正面同轴光图绝对路径
|
|
|
|
|
- back_ring: 背面环光图绝对路径
|
|
|
|
|
- back_coaxial: 背面同轴光图绝对路径
|
|
|
|
|
- front_gray: 正面灰度图绝对路径
|
|
|
|
|
- back_gray: 背面灰度图绝对路径
|
|
|
|
|
- """
|
|
|
|
|
- # 动态获取当前服务的 base_url (例如 http://127.0.0.1:7755)
|
|
|
|
|
|
|
+async def auto_import_script_api(
|
|
|
|
|
+ request: Request,
|
|
|
|
|
+ card_name: str = Form(..., description="卡牌名称"),
|
|
|
|
|
+ cardNo: Optional[str] = Form(None, description="卡牌编号"),
|
|
|
|
|
+ card_type: CardType = Form(CardType.pokemon, description="卡牌类型"),
|
|
|
|
|
+ is_reflect_card: bool = Form(True, description="是否是反光卡"),
|
|
|
|
|
+ strict_mode: bool = Form(False, description="如果为True,必须提供所有4张主图"),
|
|
|
|
|
+
|
|
|
|
|
+ front_ring: Union[UploadFile, str, None] = File(None, description="正面环光图文件"),
|
|
|
|
|
+ front_coaxial: Union[UploadFile, str, None] = File(None, description="正面同轴光图文件"),
|
|
|
|
|
+ back_ring: Union[UploadFile, str, None] = File(None, description="背面环光图文件"),
|
|
|
|
|
+ back_coaxial: Union[UploadFile, str, None] = File(None, description="背面同轴光图文件"),
|
|
|
|
|
+ front_gray: Union[UploadFile, str, None] = File(None, description="正面灰度图文件"),
|
|
|
|
|
+ back_gray: Union[UploadFile, str, None] = File(None, description="背面灰度图文件")
|
|
|
|
|
+):
|
|
|
local_base_url = str(request.base_url).rstrip('/')
|
|
local_base_url = str(request.base_url).rstrip('/')
|
|
|
|
|
|
|
|
main_inputs = {
|
|
main_inputs = {
|
|
|
- "front_ring": req_data.front_ring,
|
|
|
|
|
- "front_coaxial": req_data.front_coaxial,
|
|
|
|
|
- "back_ring": req_data.back_ring,
|
|
|
|
|
- "back_coaxial": req_data.back_coaxial
|
|
|
|
|
|
|
+ "front_ring": front_ring,
|
|
|
|
|
+ "front_coaxial": front_coaxial,
|
|
|
|
|
+ "back_ring": back_ring,
|
|
|
|
|
+ "back_coaxial": back_coaxial
|
|
|
}
|
|
}
|
|
|
gray_inputs = {
|
|
gray_inputs = {
|
|
|
- "front_gray": req_data.front_gray,
|
|
|
|
|
- "back_gray": req_data.back_gray
|
|
|
|
|
|
|
+ "front_gray": front_gray,
|
|
|
|
|
+ "back_gray": back_gray
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- # 1. 严格模式与路径检查
|
|
|
|
|
- provided_main_count = sum(1 for p in main_inputs.values() if p is not None)
|
|
|
|
|
- if req_data.strict_mode and provided_main_count != 4:
|
|
|
|
|
- raise HTTPException(status_code=400,
|
|
|
|
|
- detail=f"严格模式开启,必须提供所有4张主图。当前提供了 {provided_main_count} 张。")
|
|
|
|
|
- if not req_data.strict_mode and provided_main_count == 0 and not any(gray_inputs.values()):
|
|
|
|
|
- raise HTTPException(status_code=400, detail="未提供任何图片,无法创建。")
|
|
|
|
|
|
|
+ # 【改动点2】过滤文件时,增加 isinstance(v, UploadFile) 的判断,剔除空字符串
|
|
|
|
|
+ valid_main_files = {
|
|
|
|
|
+ k: v for k, v in main_inputs.items()
|
|
|
|
|
+ if (v is not None) and v.filename
|
|
|
|
|
+ }
|
|
|
|
|
+ valid_gray_files = {
|
|
|
|
|
+ k: v for k, v in gray_inputs.items()
|
|
|
|
|
+ if (v is not None) and v.filename
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- for p in list(main_inputs.values()) + list(gray_inputs.values()):
|
|
|
|
|
- if p and not os.path.exists(p):
|
|
|
|
|
- raise HTTPException(status_code=400, detail=f"图片文件不存在: {p}")
|
|
|
|
|
|
|
+ provided_main_count = len(valid_main_files)
|
|
|
|
|
+ if strict_mode and provided_main_count != 4:
|
|
|
|
|
+ raise HTTPException(status_code=400, detail=f"严格模式开启,必须提供所有4张主图。")
|
|
|
|
|
+ if not strict_mode and provided_main_count == 0 and not valid_gray_files:
|
|
|
|
|
+ raise HTTPException(status_code=400, detail="未提供任何图片文件,无法创建。")
|
|
|
|
|
|
|
|
- is_reflect_str = "true" if req_data.is_reflect_card else "false"
|
|
|
|
|
|
|
+ is_reflect_str = "true" if is_reflect_card else "false"
|
|
|
|
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
async with aiohttp.ClientSession() as session:
|
|
|
try:
|
|
try:
|
|
|
- # Step 1: 主图并发推理
|
|
|
|
|
- logger.info(f"--- 开始自动导入任务: {req_data.card_name} ---")
|
|
|
|
|
- processing_tasks = []
|
|
|
|
|
- for img_type, path in main_inputs.items():
|
|
|
|
|
- if path:
|
|
|
|
|
- processing_tasks.append(process_main_image(session, path, img_type, is_reflect_str))
|
|
|
|
|
|
|
+ # 读取所有图片至内存
|
|
|
|
|
+ main_bytes_data = {k: (await v.read(), v.filename) for k, v in valid_main_files.items()}
|
|
|
|
|
+ gray_bytes_data = {k: (await v.read(), v.filename) for k, v in valid_gray_files.items()}
|
|
|
|
|
|
|
|
|
|
+ # Step 1: 主图顺序推理 (防止瞬间塞爆推理服务器)
|
|
|
|
|
+ logger.info(f"--- 开始自动导入任务: {card_name} ---")
|
|
|
processed_results = []
|
|
processed_results = []
|
|
|
- if processing_tasks:
|
|
|
|
|
- processed_results = await asyncio.gather(*processing_tasks)
|
|
|
|
|
|
|
+ for img_type, (f_bytes, f_name) in main_bytes_data.items():
|
|
|
|
|
+ if len(f_bytes) == 0:
|
|
|
|
|
+ raise HTTPException(status_code=400, detail=f"图片文件 {f_name} 内容为空")
|
|
|
|
|
+
|
|
|
|
|
+ res = await process_main_image(session, f_bytes, f_name, img_type, is_reflect_str)
|
|
|
|
|
+ processed_results.append(res)
|
|
|
|
|
|
|
|
- # Step 2: 在自身数据库创建卡片记录 (携带新加的 cardNo)
|
|
|
|
|
|
|
+ # Step 2: 在自身数据库创建卡片记录
|
|
|
card_id = await create_card_record(
|
|
card_id = await create_card_record(
|
|
|
- session, local_base_url, req_data.card_name, req_data.cardNo, req_data.card_type
|
|
|
|
|
|
|
+ session, local_base_url, card_name, cardNo, card_type
|
|
|
)
|
|
)
|
|
|
logger.info(f"卡片记录创建成功,ID: {card_id}")
|
|
logger.info(f"卡片记录创建成功,ID: {card_id}")
|
|
|
|
|
|
|
@@ -231,9 +215,8 @@ async def auto_import_script_api(request: Request, req_data: AutoImportRequest):
|
|
|
for res in processed_results:
|
|
for res in processed_results:
|
|
|
upload_tasks.append(upload_main_image(session, local_base_url, card_id, res))
|
|
upload_tasks.append(upload_main_image(session, local_base_url, card_id, res))
|
|
|
|
|
|
|
|
- for img_type, path in gray_inputs.items():
|
|
|
|
|
- if path:
|
|
|
|
|
- upload_tasks.append(upload_gray_image(session, local_base_url, card_id, img_type, path))
|
|
|
|
|
|
|
+ for img_type, (f_bytes, f_name) in gray_bytes_data.items():
|
|
|
|
|
+ upload_tasks.append(upload_gray_image(session, local_base_url, card_id, img_type, f_bytes, f_name))
|
|
|
|
|
|
|
|
if upload_tasks:
|
|
if upload_tasks:
|
|
|
await asyncio.gather(*upload_tasks)
|
|
await asyncio.gather(*upload_tasks)
|
|
@@ -242,12 +225,12 @@ async def auto_import_script_api(request: Request, req_data: AutoImportRequest):
|
|
|
return {
|
|
return {
|
|
|
"message": "导入成功",
|
|
"message": "导入成功",
|
|
|
"card_id": card_id,
|
|
"card_id": card_id,
|
|
|
- "card_name": req_data.card_name,
|
|
|
|
|
- "cardNo": req_data.cardNo
|
|
|
|
|
|
|
+ "card_name": card_name,
|
|
|
|
|
+ "cardNo": cardNo
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
except HTTPException:
|
|
except HTTPException:
|
|
|
raise
|
|
raise
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.error(f"[流程终止] 发生异常: {e}")
|
|
logger.error(f"[流程终止] 发生异常: {e}")
|
|
|
- raise HTTPException(status_code=500, detail=f"自动化处理异常: {str(e)}")
|
|
|
|
|
|
|
+ raise HTTPException(status_code=500, detail=f"自动化处理异常: {str(e)}")
|