Skip to content

Commit

Permalink
Merge pull request #70 from Kyeong6/feat/#69
Browse files Browse the repository at this point in the history
feat: 이미지 검열 API 구현
  • Loading branch information
Kyeong6 authored Dec 8, 2024
2 parents 5974201 + f829fa5 commit 7453e1b
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 3 deletions.
37 changes: 37 additions & 0 deletions server/apis/image_censor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from google.cloud import vision
from logs.logger_config import get_logger
from errors.server_exception import ExternalAPIError

logger = get_logger()

# 이미지 검열을 위한 GCP API 이용
def detect_safe_search(member_id, image_base64):

try:
# 클라이언트 생성
client = vision.ImageAnnotatorClient()
image = vision.Image(content=image_base64)

response = client.safe_search_detection(image=image)
safe = response.safe_search_annotation

# 검증 결과 확인
categories = {
"adult": safe.adult,
"medical": safe.medical,
"spoofed": safe.spoof,
"violence": safe.violence,
"racy": safe.racy,
}

# "LIKELY" 또는 "VERY_LIKELY" 판정
for category, likelihood in categories.items():
# LIKELY 이상의 경우
if likelihood >= 4:
logger.info(f"유해한 이미지({category})를 업로드한 유저: {member_id}")
return False

return True
except Exception as e:
logger.error(f"GCP API 연결 오류 발생: {e}")
raise ExternalAPIError()
3 changes: 3 additions & 0 deletions server/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class Settings:
REDIS_PORT = int(os.getenv("REDIS_PORT"))
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD")
RATE_LIMIT = int(os.getenv("RATE_LIMIT"))

# GCP
GOOGLE_APPLICATION_CREDENTIALS = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")


settings = Settings()
3 changes: 3 additions & 0 deletions server/core/config_prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ class Settings:
REDIS_PORT = int(os.getenv("REDIS_PORT"))
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD")
RATE_LIMIT = int(os.getenv("RATE_LIMIT"))

# GCP
GOOGLE_APPLICATION_CREDENTIALS = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")


settings = Settings()
3 changes: 2 additions & 1 deletion server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
from fastapi import FastAPI, status
from fastapi.responses import UJSONResponse
from routers import diet_analysis, food_image_analysis, swagger_auth
from routers import diet_analysis, food_image_analysis, swagger_auth, image_censorship
from errors.handler import register_exception_handlers
from apis.food_analysis import start_scheduler
from logs.logger_config import get_logger, configure_uvicorn_logger
Expand Down Expand Up @@ -35,6 +35,7 @@ async def read_root():
# router
app.include_router(diet_analysis.router)
app.include_router(food_image_analysis.router)
app.include_router(image_censorship.router)
app.include_router(swagger_auth.router)


Expand Down
9 changes: 7 additions & 2 deletions server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ frozenlist==1.5.0
git-filter-repo==2.38.0
google-auth==2.36.0
googleapis-common-protos==1.66.0
grpcio==1.68.0
# grpcio==1.68.0
h11==0.14.0
httpcore==1.0.5
httptools==0.6.1
Expand Down Expand Up @@ -70,7 +70,7 @@ platformdirs==4.2.0
pluggy==1.5.0
propcache==0.2.0
proto-plus==1.25.0
protobuf==4.25.5
# protobuf==4.25.5
protoc-gen-openapiv2==0.0.1
pyasn1==0.6.0
pyasn1_modules==0.4.1
Expand Down Expand Up @@ -119,3 +119,8 @@ uvloop==0.19.0
watchfiles==0.21.0
websockets==12.0
yarl==1.18.0
google-api-core==2.23.0
google-cloud-vision==3.8.1
grpcio==1.68.1
grpcio-status==1.68.1
protobuf==5.29.1
42 changes: 42 additions & 0 deletions server/routers/image_censorship.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from fastapi import APIRouter, Depends, File, UploadFile
from auth.decoded_token import get_current_member
from errors.business_exception import InvalidFileFormat
from logs.logger_config import get_logger
from swagger.response_config import cencoring_image_responses
from apis.food_image import process_image_to_base64
from apis.image_censor import detect_safe_search

# 공용 로거
logger = get_logger()


router = APIRouter(
prefix="/ai/v1",
tags=["이미지 검열"]
)

# 이미지 검열 API
@router.post("/censor", responses=cencoring_image_responses)
async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)):

# 지원하는 파일 형식
ALLOWED_FILE_TYPES = ["image/jpeg", "image/png"]

# 파일 형식 검증
if file.content_type not in ALLOWED_FILE_TYPES:
raise InvalidFileFormat(allowed_types=ALLOWED_FILE_TYPES)

# 이미지 처리 및 Base64 인코딩 진행
image_base64 = await process_image_to_base64(file)

# 이미지 검열 진행
censor = detect_safe_search(member_id, image_base64)

# 응답값 구성
response = {
"success": True,
"response": censor,
"error": None
}

return response
58 changes: 58 additions & 0 deletions server/swagger/response_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,4 +358,62 @@
}
}
}
}

# 이미지 검열 API 응답 구성
cencoring_image_responses = {
401: {
"description": "인증 오류: 잘못된 인증 토큰 또는 만료된 인증 토큰",
"content": {
"application/json": {
"examples": {
"InvalidJWT": {
"summary": "잘못된 인증 토큰",
"value": {
"success": False,
"response": None,
"error": {
"code": "SECURITY_401_1",
"reason": "잘못된 인증 토큰 형식입니다.",
"http_status": status.HTTP_401_UNAUTHORIZED
}
}
},
"ExpiredJWT": {
"summary": "만료된 인증 토큰",
"value": {
"success": False,
"response": None,
"error": {
"code": "SECURITY_401_2",
"reason": "인증 토큰이 만료되었습니다.",
"http_status": status.HTTP_401_UNAUTHORIZED
}
}
}
}
}
}
},
400: {
"description": "잘못된 요청: 음식 이미지 오류 또는 API 분석 실패",
"content": {
"application/json": {
"examples": {
"InvalidFileFormat": {
"summary": "지원되지 않는 파일 형식",
"value": {
"success": False,
"response": None,
"error": {
"code": "IMAGE_400_3",
"reason": "지원되지 않는 파일 형식: image/jpeg, image/png",
"http_status": status.HTTP_400_BAD_REQUEST
}
}
}
}
}
}
}
}

0 comments on commit 7453e1b

Please sign in to comment.