asefasdfcv's picture
Update main.py
3e28ecb verified
raw
history blame
19.2 kB
"""
FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฉ”์ธ ๋ชจ๋“ˆ
"""
import os
import sys
import logging
import tempfile
import traceback
import time
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
# ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ • ๋ฐ ์ตœ์ ํ™”
CACHE_DIRS = {
'TRANSFORMERS_CACHE': '/tmp/transformers_cache',
'HF_HOME': '/tmp/huggingface_cache',
'TORCH_HOME': '/tmp/torch_hub_cache',
'UPLOADS_DIR': '/tmp/uploads'
}
# ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •
for key, path in CACHE_DIRS.items():
os.environ[key] = path
os.makedirs(path, exist_ok=True)
# ์ถ”๊ฐ€ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ตœ์ ํ™”
os.environ['HF_HUB_DISABLE_TELEMETRY'] = '1'
os.environ['TRANSFORMERS_VERBOSITY'] = 'error'
# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ จ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ (๊ธฐ๋ณธ๊ฐ’ ์„ค์ •)
# ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ๋Š” .env ํŒŒ์ผ์ด๋‚˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์„ค์ •ํ•ด์•ผ ํ•จ
os.environ.setdefault('DB_HOST', 'localhost')
os.environ.setdefault('DB_PORT', '3306')
os.environ.setdefault('DB_USER', 'username') # ์‹ค์ œ ์‚ฌ์šฉ์‹œ ๋ณ€๊ฒฝ ํ•„์š”
os.environ.setdefault('DB_PASSWORD', 'password') # ์‹ค์ œ ์‚ฌ์šฉ์‹œ ๋ณ€๊ฒฝ ํ•„์š”
os.environ.setdefault('DB_NAME', 'foundlost')
# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ™˜๊ฒฝ ์„ค์ • (development, production, test)
os.environ.setdefault('APP_ENV', 'development')
# ๋กœ๊น… ์„ค์ • ๊ฐœ์„ 
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('/tmp/app.log')
]
)
logger = logging.getLogger(__name__)
# ๋ชจ๋ธ ํด๋ž˜์Šค ์ •์˜ - Spring Boot์™€ ํ˜ธํ™˜๋˜๋„๋ก ์ˆ˜์ •
from pydantic import BaseModel, Field
class SpringMatchRequest(BaseModel):
"""Spring Boot์—์„œ ๋ณด๋‚ด๋Š” ์š”์ฒญ ๊ตฌ์กฐ์— ๋งž์ถ˜ ๋ชจ๋ธ"""
category: Optional[int] = None
title: Optional[str] = None
color: Optional[str] = None
content: Optional[str] = None
detail: Optional[str] = None # Spring์—์„œ detail์ด๋ผ๋Š” ํ•„๋“œ๋ช… ์‚ฌ์šฉ
location: Optional[str] = None
image_url: Optional[str] = None
threshold: Optional[float] = 0.7
class MatchingResult(BaseModel):
total_matches: int
similarity_threshold: float
matches: List[Dict[str, Any]]
class MatchingResponse(BaseModel):
success: bool
message: str
result: Optional[MatchingResult] = None
# ๋ชจ๋ธ ์ดˆ๊ธฐํ™” (์‹ฑ๊ธ€ํ†ค์œผ๋กœ ๋กœ๋“œ)
clip_model = None
def get_clip_model(force_reload=False):
"""
ํ•œ๊ตญ์–ด CLIP ๋ชจ๋ธ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ (์‹ฑ๊ธ€ํ†ค ํŒจํ„ด)
Args:
force_reload (bool): ๋ชจ๋ธ ๊ฐ•์ œ ์žฌ๋กœ๋”ฉ ์—ฌ๋ถ€
"""
global clip_model
# ๋ชจ๋ธ ๋กœ๋”ฉ ์‹œ์ž‘ ์‹œ๊ฐ„ ๊ธฐ๋ก
start_time = time.time()
if clip_model is None or force_reload:
try:
# ๋กœ๊น… ๋ฐ ์„ฑ๋Šฅ ์ถ”์ 
logger.info("๐Ÿ”„ CLIP ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์‹œ์ž‘...")
# ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๊ธฐ๋ก (๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ)
try:
import psutil
process = psutil.Process(os.getpid())
logger.info(f"๋ชจ๋ธ ๋กœ๋“œ ์ „ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰: {process.memory_info().rss / 1024 / 1024:.2f} MB")
except ImportError:
pass
# ๋ชจ๋ธ ๋กœ๋“œ
from models.clip_model import KoreanCLIPModel
clip_model = KoreanCLIPModel()
# ๋กœ๋”ฉ ์‹œ๊ฐ„ ๋กœ๊น…
load_time = time.time() - start_time
logger.info(f"โœ… CLIP ๋ชจ๋ธ ๋กœ๋“œ ์™„๋ฃŒ (์†Œ์š”์‹œ๊ฐ„: {load_time:.2f}์ดˆ)")
# ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๊ธฐ๋ก (๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ)
try:
import psutil
process = psutil.Process(os.getpid())
logger.info(f"๋ชจ๋ธ ๋กœ๋“œ ํ›„ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰: {process.memory_info().rss / 1024 / 1024:.2f} MB")
except ImportError:
pass
return clip_model
except Exception as e:
# ์ƒ์„ธํ•œ ์—๋Ÿฌ ๋กœ๊น…
logger.error(f"โŒ CLIP ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {str(e)}")
logger.error(f"์—๋Ÿฌ ์ƒ์„ธ: {traceback.format_exc()}")
# ์‹คํŒจ ์‹œ None ๋ฐ˜ํ™˜
return None
return clip_model
# ๋‚ด๋ถ€์ ์œผ๋กœ ์Šต๋“๋ฌผ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜
async def fetch_found_items(limit=100, offset=0):
"""
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์Šต๋“๋ฌผ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜
Args:
limit (int): ์กฐํšŒํ•  ์ตœ๋Œ€ ํ•ญ๋ชฉ ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: 100)
offset (int): ์กฐํšŒ ์‹œ์ž‘ ์œ„์น˜ (๊ธฐ๋ณธ๊ฐ’: 0)
Returns:
list: ์Šต๋“๋ฌผ ๋ฐ์ดํ„ฐ ๋ชฉ๋ก
"""
try:
# ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํ™•์ธ - ํ…Œ์ŠคํŠธ ๋ชจ๋“œ์ธ ๊ฒฝ์šฐ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜
if os.getenv('APP_ENV') == 'test':
logger.info("ํ…Œ์ŠคํŠธ ๋ชจ๋“œ: ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ")
# ์˜ˆ์‹œ ๋ฐ์ดํ„ฐ - ํ…Œ์ŠคํŠธ์šฉ
sample_found_items = [
{
"id": 1,
"item_category_id": 1,
"title": "๊ฒ€์ • ๊ฐ€์ฃฝ ์ง€๊ฐ‘",
"color": "๊ฒ€์ •์ƒ‰",
"content": "๊ฐ•๋‚จ์—ญ ๊ทผ์ฒ˜์—์„œ ๊ฒ€์ •์ƒ‰ ๊ฐ€์ฃฝ ์ง€๊ฐ‘์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.",
"location": "๊ฐ•๋‚จ์—ญ",
"image": None,
"category": "์ง€๊ฐ‘"
},
{
"id": 2,
"item_category_id": 1,
"title": "๊ฐˆ์ƒ‰ ๊ฐ€์ฃฝ ์ง€๊ฐ‘",
"color": "๊ฐˆ์ƒ‰",
"content": "์„œ์šธ๋Œ€์ž…๊ตฌ์—ญ ๊ทผ์ฒ˜์—์„œ ๊ฐˆ์ƒ‰ ๊ฐ€์ฃฝ ์ง€๊ฐ‘์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.",
"location": "์„œ์šธ๋Œ€์ž…๊ตฌ์—ญ",
"image": None,
"category": "์ง€๊ฐ‘"
}
]
return sample_found_items
# ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ
# ๊ธฐ๋Šฅ์ด ๊ฒ€์ฆ๋˜๋ฉด limit ๊ฐ’์„ ๋Š˜๋ฆด ์ˆ˜ ์žˆ์Œ
logger.info(f"๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์Šต๋“๋ฌผ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์ค‘ (limit: {limit}, offset: {offset})...")
# db_connector ๋ชจ๋“ˆ์˜ ํ•จ์ˆ˜ ํ˜ธ์ถœ
from db_connector import fetch_found_items as db_fetch_found_items
found_items = await db_fetch_found_items(limit=limit, offset=offset)
logger.info(f"๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ {len(found_items)}๊ฐœ์˜ ์Šต๋“๋ฌผ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์™„๋ฃŒ")
return found_items
except Exception as e:
logger.error(f"์Šต๋“๋ฌผ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
logger.error(traceback.format_exc())
# ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๋นˆ ๋ชฉ๋ก ๋ฐ˜ํ™˜
return []
# 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.on_event("startup")
async def startup_event():
"""
์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ ์‹คํ–‰๋˜๋Š” ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
"""
logger.info("์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์ค‘...")
try:
# ๋ชจ๋ธ ์‚ฌ์ „ ๋‹ค์šด๋กœ๋“œ (๋น„๋™๊ธฐ์ ์œผ๋กœ)
from models.clip_model import preload_clip_model
preload_clip_model()
logger.info("๋ชจ๋ธ ์‚ฌ์ „ ๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ")
# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ
if os.getenv('APP_ENV') != 'test':
try:
from db_connector import get_db_connection
with get_db_connection() as connection:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
result = cursor.fetchone()
if result:
logger.info("โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ์„ฑ๊ณต")
else:
logger.warning("โš ๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ์—†์Œ")
except Exception as db_error:
logger.error(f"โŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ์‹คํŒจ: {str(db_error)}")
logger.error(traceback.format_exc())
except Exception as e:
logger.error(f"์‹œ์ž‘ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
logger.error(traceback.format_exc())
# ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
@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)}"}
)
# ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ชจ๋“ˆ ์ž„ํฌํŠธ
from utils.similarity import calculate_similarity, find_similar_items, CATEGORY_WEIGHT, ITEM_NAME_WEIGHT, COLOR_WEIGHT, CONTENT_WEIGHT
# ์ด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ์กฐํšŒ ํ•จ์ˆ˜ ์ž„ํฌํŠธ
from db_connector import count_found_items
# API ์—”๋“œํฌ์ธํŠธ ์ •์˜ - Spring Boot์— ๋งž๊ฒŒ ์ˆ˜์ •
@app.post("/api/matching/find-similar", response_model=MatchingResponse)
async def find_similar_items_api(
request: dict,
threshold: float = Query(0.7, description="์œ ์‚ฌ๋„ ์ž„๊ณ„๊ฐ’ (0.0 ~ 1.0)"),
limit: int = Query(10, description="๋ฐ˜ํ™˜ํ•  ์ตœ๋Œ€ ํ•ญ๋ชฉ ์ˆ˜"),
db_limit: int = Query(100, description="๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์กฐํšŒํ•  ์Šต๋“๋ฌผ ์ˆ˜")
):
"""
Spring Boot์—์„œ ๋ณด๋‚ด๋Š” ์š”์ฒญ ๊ตฌ์กฐ์— ๋งž์ถฐ ์‚ฌ์šฉ์ž ๊ฒŒ์‹œ๊ธ€๊ณผ ์œ ์‚ฌํ•œ ์Šต๋“๋ฌผ์„ ์ฐพ๋Š” API
Args:
request (dict): ์š”์ฒญ ๋ฐ์ดํ„ฐ
threshold (float): ์œ ์‚ฌ๋„ ์ž„๊ณ„๊ฐ’ (๊ธฐ๋ณธ๊ฐ’: 0.7)
limit (int): ๋ฐ˜ํ™˜ํ•  ์ตœ๋Œ€ ํ•ญ๋ชฉ ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: 10)
db_limit (int): ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์กฐํšŒํ•  ์Šต๋“๋ฌผ ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: 100)
"""
try:
logger.info(f"์œ ์‚ฌ ์Šต๋“๋ฌผ ๊ฒ€์ƒ‰ ์š”์ฒญ: threshold={threshold}, limit={limit}, db_limit={db_limit}")
logger.debug(f"์š”์ฒญ ๋ฐ์ดํ„ฐ: {request}")
# ์š”์ฒญ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜
user_post = {}
# ์ค‘์š”: lostItemId ์ €์žฅ
lostItemId = request.get('lostItemId')
# Spring Boot์—์„œ ๋ณด๋‚ด๋Š” ํ•„๋“œ๋ช… ๋งคํ•‘
if 'category' in request:
user_post['category'] = request['category']
elif 'itemCategoryId' in request:
user_post['category'] = request['itemCategoryId']
# ์ œ๋ชฉ ํ•„๋“œ
if 'title' in request:
user_post['item_name'] = request['title']
# ์ƒ‰์ƒ ํ•„๋“œ
if 'color' in request:
user_post['color'] = request['color']
# ๋‚ด์šฉ ํ•„๋“œ (Spring Boot์—์„œ๋Š” detail๋กœ ๋ณด๋ƒ„)
if 'detail' in request:
user_post['content'] = request['detail']
elif 'content' in request:
user_post['content'] = request['content']
# ์œ„์น˜ ํ•„๋“œ
if 'location' in request:
user_post['location'] = request['location']
# ์ด๋ฏธ์ง€ URL ํ•„๋“œ
if 'image' in request and request['image']:
user_post['image_url'] = request['image']
elif 'image_url' in request and request['image_url']:
user_post['image_url'] = request['image_url']
# ์š”์ฒญ์— ๋“ค์–ด์˜จ threshold ๊ฐ’์ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉ
if 'threshold' in request and request['threshold']:
threshold = float(request['threshold'])
# ์ด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ์กฐํšŒ (์„ฑ๋Šฅ ์ธก์ •์šฉ)
total_count = await count_found_items()
logger.info(f"๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋‚ด ์ด ์Šต๋“๋ฌผ ๊ฐœ์ˆ˜: {total_count}")
# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์Šต๋“๋ฌผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ (์ง€์ •๋œ ๊ฐœ์ˆ˜๋งŒํผ)
found_items = await fetch_found_items(limit=db_limit, offset=0)
logger.info(f"๊ฒ€์ƒ‰ํ•  ์Šต๋“๋ฌผ ์ˆ˜: {len(found_items)}")
# ์‚ฌ์šฉ์ž์—๊ฒŒ ์ง„ํ–‰ ์ƒํ™ฉ ์•Œ๋ฆผ
if len(found_items) == 0:
return MatchingResponse(
success=False,
message="์Šต๋“๋ฌผ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.",
result=None
)
# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ฐ€์ ธ์˜จ ๋น„์œจ ๊ณ„์‚ฐ
db_coverage = len(found_items) / max(1, total_count) * 100
logger.info(f"์ด ๋ฐ์ดํ„ฐ ์ค‘ {db_coverage:.2f}% ๊ฒ€์ƒ‰ ({len(found_items)}/{total_count})")
# CLIP ๋ชจ๋ธ ๋กœ๋“œ
clip_model_instance = get_clip_model()
if clip_model_instance is None:
return MatchingResponse(
success=False,
message="CLIP ๋ชจ๋ธ ๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
result=None
)
# ์œ ์‚ฌ๋„ ๊ณ„์‚ฐ ์‹œ์ž‘ ์‹œ๊ฐ„ ๊ธฐ๋ก
start_time = time.time()
# ์œ ์‚ฌํ•œ ํ•ญ๋ชฉ ์ฐพ๊ธฐ
similar_items = find_similar_items(user_post, found_items, threshold, clip_model_instance)
# ์œ ์‚ฌ๋„ ๊ณ„์‚ฐ ์†Œ์š” ์‹œ๊ฐ„
similarity_time = time.time() - start_time
logger.info(f"์œ ์‚ฌ๋„ ๊ณ„์‚ฐ ์†Œ์š” ์‹œ๊ฐ„: {similarity_time:.2f}์ดˆ (ํ•ญ๋ชฉ๋‹น ํ‰๊ท : {similarity_time/max(1, len(found_items))*1000:.2f}ms)")
# ์œ ์‚ฌ๋„ ์„ธ๋ถ€ ์ •๋ณด ๋กœ๊น…
logger.info("===== ์œ ์‚ฌ๋„ ์„ธ๋ถ€ ์ •๋ณด =====")
for idx, item in enumerate(similar_items[:5]): # ์ƒ์œ„ 5๊ฐœ๋งŒ ๋กœ๊น…
logger.info(f"ํ•ญ๋ชฉ {idx+1}: {item['item']['title']}")
logger.info(f" ์ตœ์ข… ์œ ์‚ฌ๋„: {item['similarity']:.4f}")
details = item['details']
logger.info(f" ํ…์ŠคํŠธ ์œ ์‚ฌ๋„: {details['text_similarity']:.4f}")
if details['image_similarity'] is not None:
logger.info(f" ์ด๋ฏธ์ง€ ์œ ์‚ฌ๋„: {details['image_similarity']:.4f}")
category_sim = details['details']['category']
item_name_sim = details['details']['item_name']
color_sim = details['details']['color']
content_sim = details['details']['content']
logger.info(f" ์นดํ…Œ๊ณ ๋ฆฌ ์œ ์‚ฌ๋„: {category_sim:.4f} (๊ฐ€์ค‘์น˜: {CATEGORY_WEIGHT:.2f})")
logger.info(f" ๋ฌผํ’ˆ๋ช… ์œ ์‚ฌ๋„: {item_name_sim:.4f} (๊ฐ€์ค‘์น˜: {ITEM_NAME_WEIGHT:.2f})")
logger.info(f" ์ƒ‰์ƒ ์œ ์‚ฌ๋„: {color_sim:.4f} (๊ฐ€์ค‘์น˜: {COLOR_WEIGHT:.2f})")
logger.info(f" ๋‚ด์šฉ ์œ ์‚ฌ๋„: {content_sim:.4f} (๊ฐ€์ค‘์น˜: {CONTENT_WEIGHT:.2f})")
logger.info("==========================")
# ๊ฒฐ๊ณผ ์ œํ•œ
similar_items = similar_items[:limit]
# Spring Boot ์‘๋‹ต ํ˜•์‹์— ๋งž๊ฒŒ ๊ฒฐ๊ณผ ๊ตฌ์„ฑ
matches = []
for item in similar_items:
found_item = item['item']
# ์Šต๋“๋ฌผ ์ •๋ณด ๊ตฌ์„ฑ (์ถ”๊ฐ€ ํ•„๋“œ ํฌํ•จ)
found_item_info = {
"id": found_item["id"],
"user_id": found_item.get("user_id", None),
"item_category_id": found_item["item_category_id"],
"title": found_item["title"],
"color": found_item["color"],
"lost_at": found_item.get("lost_at", None),
"location": found_item["location"],
"detail": found_item["content"],
"image": found_item.get("image", None),
"status": found_item.get("status", "ACTIVE"),
"storedAt": found_item.get("storedAt", None),
"majorCategory": found_item.get("majorCategory", None), # ์ถ”๊ฐ€: ๋Œ€๋ถ„๋ฅ˜
"minorCategory": found_item.get("minorCategory", None), # ์ถ”๊ฐ€: ์†Œ๋ถ„๋ฅ˜
"management_id": found_item.get("management_id", None) # ์ถ”๊ฐ€: ๊ด€๋ฆฌ ๋ฒˆํ˜ธ
}
match_item = {
"lostItemId": lostItemId, # ์š”์ฒญ ๋ฐ›์€ lostItemId ์‚ฌ์šฉ
"foundItemId": found_item["id"],
"item": found_item_info,
"similarity": round(item["similarity"], 4)
}
matches.append(match_item)
# ์‘๋‹ต ๊ฒฐ๊ณผ ๊ตฌ์„ฑ
result = {
"total_matches": len(matches),
"similarity_threshold": threshold,
"matches": matches,
"db_coverage_percent": round(db_coverage, 2) # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปค๋ฒ„๋ฆฌ์ง€ ์ถ”๊ฐ€
}
response_data = {
"success": True,
"message": f"{len(matches)}๊ฐœ์˜ ์œ ์‚ฌํ•œ ์Šต๋“๋ฌผ์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค. (์ด {len(found_items)}๊ฐœ ์ค‘ ๊ฒ€์ƒ‰)",
"result": result
}
# ์‘๋‹ต ๋กœ๊น…
logger.info(f"์‘๋‹ต ๋ฐ์ดํ„ฐ: {len(matches)}๊ฐœ์˜ ์œ ์‚ฌํ•œ ์Šต๋“๋ฌผ ๋ฐœ๊ฒฌ")
return MatchingResponse(**response_data)
except Exception as e:
logger.error(f"API ํ˜ธ์ถœ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
logger.error(traceback.format_exc())
# ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ๋ฐ˜ํ™˜ (๊ฐœ๋ฐœ์šฉ)
error_response = {
"success": False,
"message": f"์š”์ฒญ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}",
"error_detail": traceback.format_exc()
}
return JSONResponse(status_code=500, content=error_response)
@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"
}
# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰
if __name__ == "__main__":
import uvicorn
print("์„œ๋ฒ„ ์‹คํ–‰ ์‹œ๋„ ์ค‘...")
try:
uvicorn.run(
"main:app",
host="0.0.0.0",
port=7860, # ํ—ˆ๊น…ํŽ˜์ด์Šค ์ŠคํŽ˜์ด์Šค์—์„œ ์‚ฌ์šฉํ•  ๊ธฐ๋ณธ ํฌํŠธ
log_level="info",
reload=True
)
except Exception as e:
print(f"์„œ๋ฒ„ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
traceback.print_exc()