""" FastAPI 애플리케이션 메인 모듈 """ import os import sys import logging import tempfile from fastapi import FastAPI, Request, HTTPException, Query, Body from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from typing import List, Dict, Any, Optional, Union import json import base64 from io import BytesIO from PIL import Image # 캐시 디렉토리 설정 os.environ['TRANSFORMERS_CACHE'] = '/tmp/huggingface_cache' os.environ['HF_HOME'] = '/tmp/huggingface_cache' # 디렉토리 생성 os.makedirs('/tmp/huggingface_cache', exist_ok=True) os.makedirs('/tmp/uploads', exist_ok=True) # 로깅 설정 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # 필요한 모듈 임포트 from models.clip_model import KoreanCLIPModel from utils.similarity import calculate_similarity, find_similar_items from api.routes.matching_routers import LostItemPost, ImageMatchingRequest, MatchingResult, MatchingResponse # 모델 초기화 (싱글톤으로 로드) clip_model = None def get_clip_model(): """ 한국어 CLIP 모델 인스턴스를 반환 (싱글톤 패턴) """ global clip_model if clip_model is None: try: clip_model = KoreanCLIPModel() return clip_model except Exception as e: logger.error(f"CLIP 모델 초기화 실패: {str(e)}") # 실패 시 None 반환 (텍스트 기반 매칭만 가능) return None return clip_model # FastAPI 애플리케이션 생성 app = FastAPI( title="습득물 유사도 검색 API", description="한국어 CLIP 모델을 사용하여 사용자 게시글과 습득물 간의 유사도를 계산하는 API", version="1.0.0" ) # CORS 미들웨어 설정 app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 전역 예외 처리 @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): """ 전역 예외 처리기 """ logger.error(f"요청 처리 중 예외 발생: {str(exc)}") return JSONResponse( status_code=500, content={"success": False, "message": f"서버 오류가 발생했습니다: {str(exc)}"} ) # API 엔드포인트 정의 @app.post("/api/matching/find-similar", response_model=MatchingResponse) async def find_similar_items_api( request: Union[LostItemPost, ImageMatchingRequest], threshold: float = Query(0.7, description="유사도 임계값 (0.0 ~ 1.0)"), limit: int = Query(10, description="반환할 최대 항목 수") ): """ 사용자 게시글과 유사한 습득물을 찾는 API 엔드포인트 Args: request: 사용자의 분실물 게시글 또는 이미지 매칭 요청 threshold: 유사도 임계값 limit: 반환할 최대 항목 수 Returns: MatchingResponse: 매칭 결과가 포함된 응답 """ try: logger.info(f"유사 습득물 검색 요청: threshold={threshold}, limit={limit}") # 요청 데이터 변환 user_post = {} if isinstance(request, LostItemPost): user_post = request.dict() else: user_post = request.dict() # Base64 이미지가 있으면 이미지 처리 로직 추가 if user_post.get("image_base64"): try: # Base64 이미지 디코딩 base64_str = user_post["image_base64"] # Base64 문자열에서 헤더 제거 (있을 경우) if "," in base64_str: base64_str = base64_str.split(",")[1] image_data = base64.b64decode(base64_str) image = Image.open(BytesIO(image_data)) # 이미지 사용 (CLIP 모델에 전달) user_post["image"] = image logger.info("Base64 이미지 처리 완료") except Exception as e: logger.error(f"Base64 이미지 처리 실패: {str(e)}") # 여기서 DB 대신 요청에서 전달된 습득물 데이터를 사용합니다. lost_items = [] # 요청에 습득물 데이터가 있으면 사용 if hasattr(request, 'lost_items') and request.lost_items: lost_items = request.lost_items if not lost_items: return MatchingResponse( success=False, message="습득물 데이터가 없습니다. 요청에 습득물 데이터를 포함해주세요.", result=None ) # CLIP 모델 로드 clip_model_instance = get_clip_model() # 유사한 항목 찾기 similar_items = find_similar_items(user_post, lost_items, threshold, clip_model_instance) # 결과 제한 similar_items = similar_items[:limit] # 응답 구성 result = MatchingResult( total_matches=len(similar_items), similarity_threshold=threshold, matches=[ { "item": item["item"], "similarity": round(item["similarity"], 4), "details": { "text_similarity": round(item["details"]["text_similarity"], 4), "image_similarity": round(item["details"]["image_similarity"], 4) if item["details"]["image_similarity"] else None, "category_similarity": round(item["details"]["details"]["category"], 4), "item_name_similarity": round(item["details"]["details"]["item_name"], 4), "color_similarity": round(item["details"]["details"]["color"], 4), "content_similarity": round(item["details"]["details"]["content"], 4) } } for item in similar_items ] ) return MatchingResponse( success=True, message=f"{len(similar_items)}개의 유사한 습득물을 찾았습니다.", result=result ) except Exception as e: logger.error(f"API 호출 중 오류 발생: {str(e)}") raise HTTPException(status_code=500, detail=f"요청 처리 중 오류가 발생했습니다: {str(e)}") @app.get("/api/matching/test") async def test_endpoint(): """ API 테스트용 엔드포인트 Returns: dict: 테스트 응답 """ return {"message": "API가 정상적으로 작동 중입니다."} @app.get("/api/status") async def status(): """ API 상태 엔드포인트 Returns: dict: API 상태 정보 """ # CLIP 모델 로드 시도 model = get_clip_model() return { "status": "ok", "models_loaded": model is not None, "version": "1.0.0" } # 루트 엔드포인트 @app.get("/") async def root(): """ 루트 엔드포인트 - API 정보 제공 """ return { "app_name": "습득물 유사도 검색 API", "version": "1.0.0", "description": "한국어 CLIP 모델을 사용하여 사용자 게시글과 습득물 간의 유사도를 계산합니다.", "api_endpoint": "/api/matching/find-similar", "test_endpoint": "/api/matching/test", "status_endpoint": "/api/status" }