auto_import.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import os
  2. import json
  3. import asyncio
  4. import aiohttp
  5. import aiofiles
  6. from typing import Dict, Any, Tuple, Optional
  7. from fastapi import APIRouter, HTTPException, Request
  8. from pydantic import BaseModel, Field
  9. from app.core.config import settings
  10. from app.core.logger import get_logger
  11. from app.utils.scheme import CardType
  12. logger = get_logger(__name__)
  13. router = APIRouter()
  14. # 定义请求参数模型
  15. class AutoImportRequest(BaseModel):
  16. card_name: str = Field(..., description="卡牌名称")
  17. cardNo: Optional[str] = Field(None, description="卡牌编号")
  18. card_type: CardType = Field(CardType.pokemon, description="卡牌类型")
  19. is_reflect_card: bool = Field(True, description="是否是反光卡")
  20. strict_mode: bool = Field(False, description="如果为True,必须提供所有4张主图")
  21. path_front_ring: Optional[str] = Field(None, description="正面环光图绝对路径")
  22. path_front_coaxial: Optional[str] = Field(None, description="正面同轴光图绝对路径")
  23. path_back_ring: Optional[str] = Field(None, description="背面环光图绝对路径")
  24. path_back_coaxial: Optional[str] = Field(None, description="背面同轴光图绝对路径")
  25. path_front_gray: Optional[str] = Field(None, description="正面灰度图绝对路径")
  26. path_back_gray: Optional[str] = Field(None, description="背面灰度图绝对路径")
  27. # --- 内部辅助函数 ---
  28. async def call_api_with_file(
  29. session: aiohttp.ClientSession,
  30. url: str,
  31. file_path: str,
  32. params: Dict[str, Any] = None,
  33. form_fields: Dict[str, Any] = None,
  34. file_field_name: str = 'file'
  35. ) -> Tuple[int, bytes]:
  36. """通用的文件上传API调用函数"""
  37. form_data = aiohttp.FormData()
  38. if form_fields:
  39. for key, value in form_fields.items():
  40. form_data.add_field(key, str(value))
  41. if not os.path.exists(file_path):
  42. raise FileNotFoundError(f"文件不存在: {file_path}")
  43. async with aiofiles.open(file_path, 'rb') as f:
  44. content = await f.read()
  45. form_data.add_field(
  46. file_field_name,
  47. content,
  48. filename=os.path.basename(file_path),
  49. content_type='image/jpeg'
  50. )
  51. try:
  52. async with session.post(url, data=form_data, params=params) as response:
  53. response_content = await response.read()
  54. if not response.ok:
  55. logger.error(
  56. f"[API Error] {url} -> Status: {response.status}, Msg: {response_content.decode('utf-8')[:100]}")
  57. return response.status, response_content
  58. except Exception as e:
  59. logger.error(f"[Conn Error] {url} -> {e}")
  60. raise e
  61. async def process_main_image(
  62. session: aiohttp.ClientSession,
  63. image_path: str,
  64. score_type: str,
  65. is_reflect_card: str
  66. ) -> Dict[str, Any]:
  67. """调用推理服务,处理主图片"""
  68. logger.info(f"处理主图: {score_type} -> {os.path.basename(image_path)}")
  69. inference_base_url = settings.SCORE_UPDATE_SERVER_URL
  70. # 1. 获取转正后的图片
  71. rectify_url = f"{inference_base_url}/api/card_inference/card_rectify_and_center"
  72. rectify_status, rectified_image_bytes = await call_api_with_file(
  73. session, url=rectify_url, file_path=image_path
  74. )
  75. if rectify_status >= 300:
  76. raise HTTPException(status_code=500, detail=f"图片转正失败: {score_type}")
  77. # 2. 获取分数JSON
  78. score_url = f"{inference_base_url}/api/card_score/score_inference"
  79. score_params = {"score_type": score_type, "is_reflect_card": is_reflect_card}
  80. score_status, score_json_bytes = await call_api_with_file(
  81. session, url=score_url, file_path=image_path, params=score_params
  82. )
  83. if score_status >= 300:
  84. raise HTTPException(status_code=500, detail=f"推理分数失败: {score_type}")
  85. return {
  86. "image_type": score_type,
  87. "rectified_image": rectified_image_bytes,
  88. "score_json": json.loads(score_json_bytes)
  89. }
  90. async def create_card_record(session: aiohttp.ClientSession, base_url: str, card_name: str, cardNo: Optional[str],
  91. card_type: CardType) -> int:
  92. """调用自身服务创建新的卡牌记录"""
  93. url = f"{base_url}{settings.API_PREFIX}/cards/created"
  94. params = {'card_name': card_name, 'card_type': card_type.value}
  95. if cardNo:
  96. params['cardNo'] = cardNo
  97. async with session.post(url, params=params) as response:
  98. if response.status == 201:
  99. data = await response.json()
  100. return data.get('id')
  101. else:
  102. text = await response.text()
  103. raise HTTPException(status_code=response.status, detail=f"创建卡牌记录失败: {text}")
  104. async def upload_main_image(session: aiohttp.ClientSession, base_url: str, card_id: int,
  105. processed_data: Dict[str, Any]):
  106. """将处理后的主图和JSON上传到自身服务"""
  107. image_type = processed_data['image_type']
  108. url = f"{base_url}{settings.API_PREFIX}/images/insert/{card_id}"
  109. form_data = aiohttp.FormData()
  110. form_data.add_field('image_type', image_type)
  111. form_data.add_field('json_data_str', json.dumps(processed_data['score_json'], ensure_ascii=False))
  112. form_data.add_field(
  113. 'image',
  114. processed_data['rectified_image'],
  115. filename=f'{image_type}_rectified.jpg',
  116. content_type='image/jpeg'
  117. )
  118. async with session.post(url, data=form_data) as response:
  119. if response.status != 201:
  120. logger.error(f"[主图上传失败] {image_type} code={response.status}")
  121. raise HTTPException(status_code=500, detail=f"主图保存失败: {image_type}")
  122. async def upload_gray_image(session: aiohttp.ClientSession, base_url: str, card_id: int, image_type: str,
  123. file_path: str):
  124. """将灰度图源文件上传到自身服务"""
  125. url = f"{base_url}{settings.API_PREFIX}/images/insert/gray/{card_id}"
  126. form_fields = {'image_type': image_type}
  127. status, _ = await call_api_with_file(
  128. session, url=url, file_path=file_path, form_fields=form_fields, file_field_name='image'
  129. )
  130. if status != 201:
  131. logger.error(f"[灰度图上传失败] {image_type} code={status}")
  132. raise HTTPException(status_code=500, detail=f"灰度图保存失败: {image_type}")
  133. # --- 暴露的API接口 ---
  134. @router.post("/process_and_import", summary="自动化处理并导入卡牌数据")
  135. async def auto_import_script_api(request: Request, req_data: AutoImportRequest):
  136. """
  137. 接收本地的图片路径,自动转正、推理分数、建立卡片关联并入库。
  138. 参数如下:
  139. card_name: 卡牌名称
  140. cardNo: 卡牌编号
  141. card_type: 卡牌类型
  142. is_reflect_card: 是否是反光卡
  143. strict_mode: 如果为True,必须提供所有4张主图
  144. path_front_ring: 正面环光图绝对路径
  145. path_front_coaxial: 正面同轴光图绝对路径
  146. path_back_ring: 背面环光图绝对路径
  147. path_back_coaxial: 背面同轴光图绝对路径
  148. path_front_gray: 正面灰度图绝对路径
  149. path_back_gray: 背面灰度图绝对路径
  150. """
  151. # 动态获取当前服务的 base_url (例如 http://127.0.0.1:7755)
  152. local_base_url = str(request.base_url).rstrip('/')
  153. main_inputs = {
  154. "front_ring": req_data.path_front_ring,
  155. "front_coaxial": req_data.path_front_coaxial,
  156. "back_ring": req_data.path_back_ring,
  157. "back_coaxial": req_data.path_back_coaxial
  158. }
  159. gray_inputs = {
  160. "front_gray": req_data.path_front_gray,
  161. "back_gray": req_data.path_back_gray
  162. }
  163. # 1. 严格模式与路径检查
  164. provided_main_count = sum(1 for p in main_inputs.values() if p is not None)
  165. if req_data.strict_mode and provided_main_count != 4:
  166. raise HTTPException(status_code=400,
  167. detail=f"严格模式开启,必须提供所有4张主图。当前提供了 {provided_main_count} 张。")
  168. if not req_data.strict_mode and provided_main_count == 0 and not any(gray_inputs.values()):
  169. raise HTTPException(status_code=400, detail="未提供任何图片,无法创建。")
  170. for p in list(main_inputs.values()) + list(gray_inputs.values()):
  171. if p and not os.path.exists(p):
  172. raise HTTPException(status_code=400, detail=f"图片文件不存在: {p}")
  173. is_reflect_str = "true" if req_data.is_reflect_card else "false"
  174. async with aiohttp.ClientSession() as session:
  175. try:
  176. # Step 1: 主图并发推理
  177. logger.info(f"--- 开始自动导入任务: {req_data.card_name} ---")
  178. processing_tasks = []
  179. for img_type, path in main_inputs.items():
  180. if path:
  181. processing_tasks.append(process_main_image(session, path, img_type, is_reflect_str))
  182. processed_results = []
  183. if processing_tasks:
  184. processed_results = await asyncio.gather(*processing_tasks)
  185. # Step 2: 在自身数据库创建卡片记录 (携带新加的 cardNo)
  186. card_id = await create_card_record(
  187. session, local_base_url, req_data.card_name, req_data.cardNo, req_data.card_type
  188. )
  189. logger.info(f"卡片记录创建成功,ID: {card_id}")
  190. # Step 3: 并发调用自身的图片保存接口
  191. upload_tasks = []
  192. for res in processed_results:
  193. upload_tasks.append(upload_main_image(session, local_base_url, card_id, res))
  194. for img_type, path in gray_inputs.items():
  195. if path:
  196. upload_tasks.append(upload_gray_image(session, local_base_url, card_id, img_type, path))
  197. if upload_tasks:
  198. await asyncio.gather(*upload_tasks)
  199. logger.info(f"--- 自动导入流程结束, Card ID: {card_id} ---")
  200. return {
  201. "message": "导入成功",
  202. "card_id": card_id,
  203. "card_name": req_data.card_name,
  204. "cardNo": req_data.cardNo
  205. }
  206. except HTTPException:
  207. raise
  208. except Exception as e:
  209. logger.error(f"[流程终止] 发生异常: {e}")
  210. raise HTTPException(status_code=500, detail=f"自动化处理异常: {str(e)}")