Files
ocr_performance_lab/router/ocr_router.py
2025-08-12 12:25:56 +09:00

142 lines
4.8 KiB
Python

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_upstage_ocr_api,
celery_app,
parse_ocr_text,
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, parse_ocr_text)
@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.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,
}
)