import json import logging from datetime import datetime from celery import chain from celery.result import AsyncResult from config.setting import MINIO_BUCKET_NAME from fastapi import APIRouter, File, HTTPException, UploadFile from fastapi.responses import JSONResponse from tasks import ( call_paddle_ocr, call_tesseract_ocr, call_tesstrain_ocr, call_upstage_ocr_api, celery_app, store_ocr_result, ) from utils.checking_keys import create_key from utils.minio_utils import upload_file_to_minio from utils.redis_utils import get_redis_client logger = logging.getLogger(__name__) router = APIRouter(prefix="/ocr", tags=["OCR"]) redis_client = get_redis_client() async def _process_ocr_request(file: UploadFile, ocr_task): if not file.filename: raise HTTPException(status_code=400, detail="파일 이름이 없습니다.") request_id = create_key() task_id = create_key() bucket_name = MINIO_BUCKET_NAME object_name = f"{request_id}/{file.filename}" # MinIO에 파일 업로드 후 presigned URL 생성 presigned_url = upload_file_to_minio( file=file, bucket_name=bucket_name, object_name=object_name ) logger.info(f"[MinIO] ✅ presigned URL 생성 완료: {presigned_url}") task_chain = chain( ocr_task.s( presigned_url=presigned_url, request_id=request_id, file_name=file.filename ), store_ocr_result.s(request_id=request_id, task_id=task_id), ) task_chain.apply_async(task_id=task_id) # Redis에 request_id → task_id 매핑 저장 try: redis_client.hset("ocr_task_mapping", request_id, task_id) except Exception as e: raise HTTPException(status_code=500, detail=f"작업 정보 저장 오류: {str(e)}") try: log_entry = { "status": "작업 접수", "timestamp": datetime.now().isoformat(), "initial_file": file.filename, } redis_client.rpush(f"ocr_status:{request_id}", json.dumps(log_entry)) except Exception: pass return JSONResponse( content={ "message": "OCR 작업이 접수되었습니다.", "request_id": request_id, "status_check_url": f"/ocr/progress/{request_id}", "filename": file.filename, } ) @router.post("/paddle", summary="[Paddle] 파일 업로드 기반 비동기 OCR") async def ocr_paddle_endpoint(file: UploadFile = File(...)): return await _process_ocr_request(file, call_paddle_ocr) @router.post("/upstage", summary="[Upstage] 파일 업로드 기반 비동기 OCR") async def ocr_upstage_endpoint(file: UploadFile = File(...)): return await _process_ocr_request(file, call_upstage_ocr_api) @router.post("/tesseract", summary="[Tesseract] 기본 모델 비동기 OCR") async def ocr_tesseract_endpoint(file: UploadFile = File(...)): return await _process_ocr_request(file, call_tesseract_ocr) @router.post("/tesstrain", summary="[Tesseract] 훈련된 모델 비동기 OCR") async def ocr_tesstrain_endpoint(file: UploadFile = File(...)): return await _process_ocr_request(file, call_tesstrain_ocr) @router.get("/progress/{request_id}", summary="OCR 진행 상태 및 결과 조회") async def check_progress(request_id: str): task_id = redis_client.hget("ocr_task_mapping", request_id) if not task_id: raise HTTPException( status_code=404, detail=f"ID {request_id} 작업을 찾을 수 없습니다." ) # 1) 진행 로그 조회 try: logs_raw = redis_client.lrange(f"ocr_status:{request_id}", 0, -1) parsed_logs = [json.loads(x) for x in logs_raw] except Exception as e: parsed_logs = [{"status": "로그 조회 실패", "error": str(e)}] # 2) 로그 기반 파생 상태(dervived_status) 계산 derived_status = None if parsed_logs: last = parsed_logs[-1].get("status") if last in ("모든 작업 완료", "작업 완료"): derived_status = "SUCCESS" elif last == "작업 오류 발생": derived_status = "FAILURE" # 3) Celery 상태 (가능하면 조회, 실패해도 무시) celery_status = "PENDING" try: result = AsyncResult(task_id, app=celery_app) celery_status = result.status or "PENDING" except Exception: pass # 4) **상태와 무관하게** 결과 먼저 조회 final_result = None try: result_str = redis_client.get(f"ocr_result:{task_id}") if result_str: final_result = json.loads(result_str) # 결과가 있으면 상태를 SUCCESS로 정규화 if derived_status is None and celery_status not in ("FAILURE", "REVOKED"): derived_status = "SUCCESS" except Exception as e: # 결과 조회 실패도 노출 final_result = {"error": f"결과 조회 실패: {str(e)}"} # 5) 최종 표시 상태 선택(로그/결과가 더 신뢰되면 그걸 우선) display_status = derived_status or celery_status return JSONResponse( content={ "request_id": request_id, "task_id": task_id, "celery_status": celery_status, # 원래 Celery 상태(참고용) "status": display_status, # 사용자가 보기 쉬운 최종 상태 "progress_logs": parsed_logs, "final_result": final_result, } )