142 lines
4.8 KiB
Python
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,
|
|
}
|
|
)
|