Skip to content

Commit

Permalink
Merge pull request #114 from JNU-econovation/develop
Browse files Browse the repository at this point in the history
v1.1.1: 테스트 기간 종료 후 구글 플레이스토어 정식 출시에 따른 변경 및 수정사항 반영
  • Loading branch information
Kyeong6 authored Feb 5, 2025
2 parents 8231b39 + 5ce1210 commit 5046aa3
Show file tree
Hide file tree
Showing 39 changed files with 1,602 additions and 1,017 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ redis.conf

# test/data
server/test/image
server/test/test_image

# food.csv(대용량)
server/data/food.csv
381 changes: 236 additions & 145 deletions server/apis/food_analysis.py

Large diffs are not rendered by default.

149 changes: 92 additions & 57 deletions server/apis/food_image.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import os
import base64
import redis
import aiofiles
import time
from datetime import datetime, timedelta
from openai import OpenAI
from openai import AsyncOpenAI
from pinecone.grpc import PineconeGRPC as Pinecone
from core.config import settings
from errors.business_exception import RateLimitExceeded, ImageAnalysisError, ImageProcessingError
from errors.server_exception import FileAccessError, ServiceConnectionError, ExternalAPIError
from logs.logger_config import get_logger
import time

# 환경에 따른 설정 파일 로드
if os.getenv("APP_ENV") == "prod":
from core.config_prod import settings
elif os.getenv("APP_ENV") == "dev":
from core.config_dev import settings
else:
from core.config_local import settings

# 환경에 따른 설정 파일 로드
if os.getenv("APP_ENV") in ["prod", "dev"]:
Expand All @@ -39,11 +33,20 @@
# 요청 제한 설정
RATE_LIMIT = settings.RATE_LIMIT # 하루 최대 요청 가능 횟수

# 프롬프트 캐싱
CACHE_TTL = 3600

# 공용 로거
logger = get_logger()

# Chatgpt API 사용
client = OpenAI(api_key = settings.OPENAI_API_KEY)
# OpenAI API 사용
client = AsyncOpenAI(api_key = settings.OPENAI_API_KEY)

# Upsage API 사용
upstage = AsyncOpenAI(
api_key = settings.UPSTAGE_API_KEY,
base_url="https://api.upstage.ai/v1/solar"
)

# Pinecone 설정
pc = Pinecone(api_key=settings.PINECONE_API_KEY)
Expand Down Expand Up @@ -91,48 +94,71 @@ async def process_image_to_base64(file):


# prompt를 불러오기
def read_prompt(filename):
with open(filename, 'r', encoding='utf-8') as file:
prompt = file.read().strip()
return prompt
async def read_prompt(filename):

# Redis에서 캐싱된 프롬프트 확인
cached_prompt = redis_client.get(f"prompt:{filename}")

if cached_prompt:
# logger.info(f"Redis 캐싱 프롬프트 사용: {filename}")
return cached_prompt

try:
async with aiofiles.open(filename, 'r', encoding='utf-8') as file:
prompt = (await file.read()).strip()

if not prompt:
logger.error("프롬프트 파일 비어있음")
raise FileAccessError()

# Redis에 프롬프트 캐싱(TTL : 1 hr)
redis_client.setex(f"prompt:{filename}", CACHE_TTL, prompt)
logger.info(f"Redis 프롬프트 캐싱 완료: {filename}")

return prompt

except Exception as e:
logger.error(f"프롬프트 파일 읽기 실패: {e}")
raise FileAccessError()


# 음식 이미지 분석 API: prompt_type은 함수명과 동일
def food_image_analyze(image_base64: str):
async def food_image_analyze(image_base64: str):

# prompt 타입 설정
prompt_file = os.path.join(settings.PROMPT_PATH, "food_image_analyze.txt")
prompt = read_prompt(prompt_file)
prompt_file = os.path.join(settings.PROMPT_PATH, "image_detection.txt")
prompt = await read_prompt(prompt_file)

# prompt 내용 없을 경우
if not prompt:
logger.error("food_image_analyze.txt에 prompt 내용 미존재")
logger.error("image_detection.txt에 prompt 내용 미존재")
raise FileAccessError()

# OpenAI API 호출
response = client.chat.completions.create(
response = await client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": prompt},
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_base64}"
"url": f"data:image/jpeg;base64,{image_base64}",
# 성능이 좋아지지만, token 소모 큼(tradeoff): 검증 필요
# "detail": "high"
}
}
]
}
},
{"role": "system", "content": prompt}
],
temperature=0.0,
max_tokens=300
)

result = response.choices[0].message.content
# print(result)

# 음식명(반환값)이 존재하지 않을 경우
if not result:
Expand All @@ -143,50 +169,59 @@ def food_image_analyze(image_base64: str):
return result


# 제공받은 음식의 벡터 임베딩 값 변환 작업 수행
def get_embedding(text, model="text-embedding-3-small"):
text = text.replace("\n", " ")
embedding = client.embeddings.create(input=[text], model=model).data[0].embedding
return embedding
# 제공받은 음식의 벡터 임베딩 값 변환 작업 수행(Upstage-Embedding 사용)
async def get_embedding(text, model="embedding-query"):
try:
text = text.replace("\n", " ")
response = await upstage.embeddings.create(input=[text], model=model)
return response.data[0].embedding
except Exception as e:
logger.error(f"텍스트 임베딩 변환 실패: {e}")
raise ExternalAPIError()


# 벡터 임베딩을 통한 유사도 분석 진행(Pinecone)
def search_similar_food(query_name, top_k=3, score_threshold=0.7):

async def search_similar_food(query_name, top_k=3, score_threshold=0.7, candidate_multiplier=2):
try:
query_vector = get_embedding(query_name)
except Exception as e:
logger.error(f"OpenAI API 텍스트 임베딩 실패: {e}")
raise ExternalAPIError()
# 음식명 Embedding Vector 변환
query_vector = await get_embedding(query_name)

# Pinecone에서 유사도 검색
results = index.query(
vector=query_vector,
top_k=top_k * candidate_multiplier,
include_metadata=True
)

# 결과 처리 (점수 필터링 적용)
candidates = [
{
'food_pk': match['id'],
'food_name': match['metadata']['food_name'],
'score': match['score']
}
for match in results['matches'] if match['score'] >= score_threshold
]

# Pinecone에서 유사도 검색
results = index.query(
vector=query_vector,
# 결과값 갯수 설정
top_k=top_k,
# 메타데이터 포함 유무
include_metadata=True
)
# 유사도 점수를 기준으로 내림차순 정렬
sorted_candidates = sorted(candidates, key=lambda x: x["score"], reverse=True)

# 결과 처리 (점수 필터링 적용)
similar_foods = [
{
'food_pk': match['id'],
'food_name': match['metadata']['food_name'],
'score': match['score']
}
for match in results['matches'] if match['score'] >= score_threshold
]
# 상위 top_k개 선택
final_results = sorted_candidates[:top_k]

# null로 채워서 항상 top_k 크기로 반환
while len(similar_foods) < top_k:
similar_foods.append({'food_name': None, 'food_pk': None})
# null로 채워서 항상 top_k 크기로 반환
while len(final_results) < top_k:
final_results.append({'food_name': None, 'food_pk': None})

return similar_foods[:top_k]
return final_results

except Exception as e:
logger.error(f"유사도 검색 실패: {e}")
raise ExternalAPIError()


# Redis의 정의된 잔여 기능 횟수 확인
def get_remaining_requests(member_id: int):
async def get_remaining_requests(member_id: int):

try:
# Redis 키 생성
Expand Down
9 changes: 1 addition & 8 deletions server/apis/swagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from starlette.status import HTTP_401_UNAUTHORIZED

# 환경에 따른 설정 파일 로드
if os.getenv("APP_ENV") == "prod":
from core.config_prod import settings
elif os.getenv("APP_ENV") == "dev":
from core.config_dev import settings
else:
from core.config_local import settings
from core.config import settings

# HTTP 기본 인증을 사용하는 Security 객체 생성
security = HTTPBasic()
Expand Down
18 changes: 18 additions & 0 deletions server/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os

# 환경 설정 로드 함수
def load_settings():

# 기본값을 'local'로 설정
env = os.getenv("APP_ENV", "local").lower()

if env == "prod":
from core.config_prod import settings
elif env == "dev":
from core.config_dev import settings
else:
from core.config_local import settings

return settings

settings = load_settings()
3 changes: 3 additions & 0 deletions server/core/config_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class Settings:
# OpenAI
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

# Upstage
UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY")

# Data
DATA_PATH = os.getenv("DATA_PATH")
DOCKER_DATA_PATH = os.getenv("DOCKER_DATA_PATH")
Expand Down
6 changes: 6 additions & 0 deletions server/core/config_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ class Settings:
# OpenAI
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

# Upstage
UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY")

# Data
DATA_PATH = os.getenv("DATA_PATH")
DOCKER_DATA_PATH = os.getenv("DOCKER_DATA_PATH")
PROMPT_PATH = os.getenv("PROMPT_PATH")

# Test
TEST_PATH = os.getenv("TEST_PATH")

# Log
LOG_PATH = os.getenv("LOG_PATH")

Expand Down
3 changes: 3 additions & 0 deletions server/core/config_prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class Settings:
# OpenAI
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

# Upstage
UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY")

# Data
DATA_PATH = os.getenv("DATA_PATH")
PROMPT_PATH = os.getenv("PROMPT_PATH")
Expand Down
Loading

0 comments on commit 5046aa3

Please sign in to comment.