Added MinIO integration and logic
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
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 (
|
||||
@@ -15,112 +14,127 @@ from tasks import (
|
||||
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(files: List[UploadFile], ocr_task):
|
||||
results = []
|
||||
for file in files:
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="파일 이름이 없습니다.")
|
||||
async def _process_ocr_request(file: UploadFile, ocr_task):
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="파일 이름이 없습니다.")
|
||||
|
||||
tmp_path = ""
|
||||
try:
|
||||
suffix = os.path.splitext(file.filename)[-1]
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file:
|
||||
content = await file.read()
|
||||
tmp_file.write(content)
|
||||
tmp_path = tmp_file.name
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"파일 저장 실패: {str(e)}")
|
||||
finally:
|
||||
await file.close()
|
||||
request_id = create_key()
|
||||
task_id = create_key()
|
||||
bucket_name = MINIO_BUCKET_NAME
|
||||
object_name = f"{request_id}/{file.filename}"
|
||||
|
||||
request_id = create_key()
|
||||
task_id = create_key()
|
||||
# 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(
|
||||
tmp_path=tmp_path, 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)
|
||||
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)
|
||||
|
||||
try:
|
||||
redis_client.hset("ocr_task_mapping", request_id, task_id)
|
||||
except Exception as e:
|
||||
if tmp_path and os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"작업 정보 저장 오류: {str(e)}"
|
||||
)
|
||||
# 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(),
|
||||
"task_id": task_id,
|
||||
"initial_file": file.filename,
|
||||
}
|
||||
redis_client.rpush(f"ocr_status:{request_id}", json.dumps(log_entry))
|
||||
except Exception:
|
||||
pass
|
||||
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
|
||||
|
||||
results.append(
|
||||
{
|
||||
"message": "OCR 작업이 접수되었습니다.",
|
||||
"request_id": request_id,
|
||||
"task_id": task_id,
|
||||
"status_check_url": f"/ocr/progress/{request_id}",
|
||||
"filename": file.filename,
|
||||
}
|
||||
)
|
||||
return JSONResponse(content={"results": results})
|
||||
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(files: List[UploadFile] = File(...)):
|
||||
return await _process_ocr_request(files, parse_ocr_text)
|
||||
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(files: List[UploadFile] = File(...)):
|
||||
return await _process_ocr_request(files, call_upstage_ocr_api)
|
||||
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 진행 상태 및 결과 조회")
|
||||
@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} 작업을 찾을 수 없습니다.")
|
||||
|
||||
result = AsyncResult(task_id, app=celery_app)
|
||||
status = result.status
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"ID {request_id} 작업을 찾을 수 없습니다."
|
||||
)
|
||||
|
||||
# 1) 진행 로그 조회
|
||||
try:
|
||||
logs = redis_client.lrange(f"ocr_status:{request_id}", 0, -1)
|
||||
parsed_logs = [json.loads(log) for log in logs]
|
||||
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
|
||||
if status == "SUCCESS":
|
||||
try:
|
||||
result_str = redis_client.get(f"ocr_result:{task_id}")
|
||||
if result_str:
|
||||
final_result = json.loads(result_str)
|
||||
except Exception as e:
|
||||
final_result = {"error": f"결과 조회 실패: {str(e)}"}
|
||||
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": status,
|
||||
"celery_status": celery_status, # 원래 Celery 상태(참고용)
|
||||
"status": display_status, # 사용자가 보기 쉬운 최종 상태
|
||||
"progress_logs": parsed_logs,
|
||||
"final_result": final_result,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user