Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
35ebb67
Merge branch 'main' of https://github.com/DearBelly/DearBelly-BE-Fastโ€ฆ
youyeon11 Aug 28, 2025
eb49336
feat: ๋ฌด์ค‘๋‹จ ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ
youyeon11 Aug 30, 2025
999c6ce
feat: ํฌํŠธ ๋งคํ•‘์„ ์œ„ํ•œ Nginx ์„ค์ •
youyeon11 Aug 30, 2025
494d3c1
feat: ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰์œผ๋กœ ๋ณ€๊ฒฝ
youyeon11 Aug 30, 2025
b5a1236
feat: docker-compose.yml ์ถ”๊ฐ€
youyeon11 Sep 1, 2025
71b6f13
feat: docker-compose.yml ์ถ”๊ฐ€
youyeon11 Sep 1, 2025
a02453e
Merge pull request #18 from DearBelly/feat/docker-blue-green-17
youyeon11 Sep 2, 2025
3982bac
fix: docker compose pull ์ด๋ฏธ์ง€๋ช… -> ์„œ๋น„์Šค๋ช…
youyeon11 Sep 2, 2025
ff22c6e
fix: ์ด๋ฏธ์ง€ ์ด๋ฆ„ ์˜คํƒ€ ์ˆ˜์ •
youyeon11 Sep 2, 2025
4e68318
feat: pytest ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ
youyeon11 Sep 2, 2025
fcb9b54
feat: pytest ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ
youyeon11 Sep 2, 2025
2dbc3a9
fix: S3์˜ ์ด๋ฏธ์ง€ Path ์ €์žฅ ์‚ญ์ œ -> ์ด๋ฏธ์ง€ ์ž์ฒด๋ฅผ ์ธ์ž๋กœ ์„ค์ •
youyeon11 Sep 2, 2025
c4d5482
fix: S3์˜ ์ด๋ฏธ์ง€ Path ์ €์žฅ ์‚ญ์ œ -> ์ด๋ฏธ์ง€ ์ž์ฒด๋ฅผ ์ธ์ž๋กœ ์„ค์ •
youyeon11 Sep 2, 2025
a47a056
fix: ํ•˜๋‚˜์˜ ๊ณตํ†ต๋œ AsyncRedis๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •
youyeon11 Sep 2, 2025
f7376e2
feat: XACK ์ดํ›„ XDEL์„ ํ†ตํ•œ ์™„๋ฃŒ์— ๋Œ€ํ•œ ๋ฉ”์‹œ์ง€ ์‚ญ์ œ
youyeon11 Sep 2, 2025
b915cb5
fix: Redis๋ฅผ ๋น„๋™๊ธฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€ํ™˜
youyeon11 Sep 2, 2025
46c32a1
fix: predict ํ•จ์ˆ˜ async ์‚ญ์ œ(Pillow, pytorch์™€์˜ ํ˜ธํ™˜)
youyeon11 Sep 3, 2025
437caf4
fix: msg List-> str ๋ณ€๊ฒฝ
youyeon11 Sep 3, 2025
6a8e492
Merge pull request #19 from DearBelly/feat/s3-read-5
youyeon11 Sep 3, 2025
5839810
refactor: snake case -> camel case๋กœ ๋ณ€๊ฒฝ (Spring ์„œ๋ฒ„์™€์˜ ํ†ต์ผ์„ฑ)
youyeon11 Sep 7, 2025
39bb4ff
refactor: snake case -> camel case๋กœ ๋ณ€๊ฒฝ (Spring ์„œ๋ฒ„์™€์˜ ํ†ต์ผ์„ฑ)
youyeon11 Sep 7, 2025
43cb041
fix: Message ์ž‘์„ฑ ๋ฐ ํ•„๋“œ ์ˆ˜์ •
youyeon11 Sep 7, 2025
fd8dea8
fix: Message์— ๋Œ€ํ•œ ์ „์ฒ˜๋ฆฌ ์ถ”๊ฐ€(์•ˆ์ „ํ•œ ์ „์ฒ˜๋ฆฌ ํ›„ ์ฝ๊ธฐ)
youyeon11 Sep 7, 2025
35cf9a4
fix: ๋ฐœํ–‰ํ•˜๋Š” Message ๋‚ด์šฉ ์ˆ˜์ •(correlationId, type, payload ๊ฐ๊ฐ ํ•„๋“œ๋กœ ๋ฐœํ–‰)
youyeon11 Sep 7, 2025
e8e0651
fix: ๋ฉ”์‹œ์ง€ Consumer์˜ ์•ˆ์ „ํ•œ ๋ฉ”์‹œ์ง€ ์ฝ๊ธฐ ์ „์ฒ˜๋ฆฌ ํ•จ์ˆ˜ ์ถ”๊ฐ€
youyeon11 Sep 7, 2025
bfe07a6
feat: BaseModle๋กœ validate ์ถ”๊ฐ€
youyeon11 Sep 7, 2025
e76098c
Merge pull request #21 from DearBelly/fix/redis-task-10
youyeon11 Sep 7, 2025
bb4eb0a
feat: open AI API kye ์ถ”๊ฐ€
youyeon11 Sep 7, 2025
38eaf14
feat: openAI ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€
youyeon11 Sep 7, 2025
db90904
del: ๋ถˆํ•„์š”ํ•œ boto ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ญ์ œ
youyeon11 Sep 7, 2025
e885d49
feat: OpenAI ์งˆ๋ฌธ ํ›„ ์„ค๋ช… ๋ฐ˜ํ™˜ ์ถ”๊ฐ€
youyeon11 Sep 7, 2025
022e9f3
feat: OpenAI ์งˆ๋ฌธ ํ›„ ์„ค๋ช… ๋ฐ˜ํ™˜ ์ถ”๊ฐ€
youyeon11 Sep 7, 2025
0541d27
fix: ๋ณ€์ˆ˜๋ช… ์˜คํƒ€ ์ˆ˜์ •
youyeon11 Sep 7, 2025
2d1b01e
Merge pull request #22 from DearBelly/feat/add-openai-20
youyeon11 Sep 7, 2025
5d56d32
fix: ๊ถŒํ•œ ์ถ”๊ฐ€
youyeon11 Sep 7, 2025
1125d0d
fix: ์Šคํฌ๋ฆฝํŠธ ์ˆ˜์ •
youyeon11 Sep 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4

- name: Transfer deploy file to EC2
uses: appleboy/scp-action@master
with:
host: ${{ secrets.REMOTE_HOST }}
username: ${{ secrets.REMOTE_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: ./deployment/
target: /home/ubuntu/deployment

deploy:
name: Deploy
Expand All @@ -52,8 +60,8 @@ jobs:
port: 22
script: |
# 1. cd
mkdir -p ~/dearbelly
cd ~/dearbelly
mkdir -p ~/dearbelly/deployment
cd ~/dearbelly/deployment

# 2. .env file
echo "${{ secrets.ENV }}" > .env
Expand All @@ -69,5 +77,5 @@ jobs:
docker pull ${{ secrets.ECR_URI }}/dearbelly-cv:latest

# 6. docker start
# TODO : ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ
docker run -d --name app-blue -p 8000:8000 ${{ secrets.ECR_URI }}/dearbelly-cv:latest
sudo chmod +x deploy.sh
source deploy.sh
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ MANIFEST
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
# Unit tests / coverage reports
htmlcov/
.tox/
.nox/
Expand Down
27 changes: 20 additions & 7 deletions app/api/endpoints/predictions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,43 @@
import redis.asyncio as redis
import uuid
from datetime import datetime
import json

router = APIRouter()

def get_redis_client(request: Request) -> redis.Redis:
return request.app.state.redis_client

"""
ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์ž„์‹œ์ ์ธ API
"""
@router.post("/predict", status_code=202)
async def create_prediction_job(job_request: JobRequest, redis_client: redis.Redis = Depends(get_redis_client)):
correlation_id = str(uuid.uuid4())
correlationId = str(uuid.uuid4())
job = ImageJob(
correlationId=correlationId,
presignedUrl=job_request.presigned_url,
presignedUrl=job_request.presignedUrl,
replyQueue=settings.STREAM_RESULT,
callbackUrl=None,
contentType="image/jpeg",
contentType=job_request.contentType,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

job_request ๊ฐ์ฒด (ํƒ€์ž…: JobRequest)์—๋Š” contentType ์†์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค. JobRequest ๋ชจ๋ธ ์ •์˜์— contentType ํ•„๋“œ๊ฐ€ ๋ˆ„๋ฝ๋œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ด ์ฝ”๋“œ๋Š” ์‹คํ–‰ ์‹œ AttributeError๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. JobRequest ์Šคํ‚ค๋งˆ์— contentType์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์š”์ฒญ ๋ณธ๋ฌธ์— ํ•ด๋‹น ํ•„๋“œ๋ฅผ ํฌํ•จํ•˜๋„๋ก ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

createdAt=datetime.utcnow().isoformat(),
ttlSec=3600,
)

await redis_client.xadd(
# ํƒ€์ž…์— ๋งž๋„๋ก ๋„ฃ์–ด์ฃผ๊ธฐ
correlationId = job.correlationId
payload = json.dumps(job.dict())
Comment on lines +30 to +31

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

์ด ๋ถ€๋ถ„์—๋Š” ๋‘ ๊ฐ€์ง€ ๊ฐœ์„ ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค:

  1. 30๋ฒˆ์งธ ์ค„์˜ correlationId = job.correlationId ํ• ๋‹น์€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. correlationId ๋ณ€์ˆ˜๋Š” ์ด๋ฏธ ์ด์ „์— ์ •์˜๋˜์—ˆ์œผ๋ฏ€๋กœ ์ด ์ค„์€ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. 31๋ฒˆ์งธ ์ค„์—์„œ Pydantic ๋ชจ๋ธ์„ ์ง๋ ฌํ™”ํ•˜๊ธฐ ์œ„ํ•ด job.dict()๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ์ด๋Š” ์˜ค๋ž˜๋œ(deprecated) ๋ฉ”์†Œ๋“œ์ž…๋‹ˆ๋‹ค. job.model_dump_json(by_alias=True)์„ ์‚ฌ์šฉํ•˜์—ฌ ๋” ๋ช…ํ™•ํ•˜๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ JSON ๋ฌธ์ž์—ด์„ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
Suggested change
correlationId = job.correlationId
payload = json.dumps(job.dict())
payload = job.model_dump_json(by_alias=True)

print("๋ถ„์„ ๊ฒฐ๊ณผ ๋ฐœํ–‰ ์‹œ์ž‘...")
entry_id = await redis_client.xadd(
settings.STREAM_JOB,
{"json": job.model_dump_json()},
{
"type": "image_results",
"payload": payload,
"correlationId": correlationId,
},
Comment on lines +35 to +39

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Redis Stream์— ๋ฐœํ–‰ํ•˜๋Š” ์ž‘์—…์˜ type์ด ์ž˜๋ชป ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ "image_results"๋กœ ์„ค์ •๋˜์–ด ์žˆ์œผ๋‚˜, ์›Œ์ปค(app/worker/worker.py)๋Š” "image_jobs" ํƒ€์ž…์˜ ์ž‘์—…์„ ๊ธฐ๋Œ€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ถˆ์ผ์น˜๋กœ ์ธํ•ด ์ž‘์—…์ด ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š๊ณ  Dead Letter Queue (DLQ)๋กœ ๋ณด๋‚ด์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ž‘์—… ํƒ€์ž…์„ "image_jobs"๋กœ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

        {
            "type": "image_jobs",
            "payload": payload,
            "correlationId": correlationId,
        }

maxlen=10_000,
approximate=True,
)

return {"job_id": correlation_id}
print("๋ถ„์„ ๊ฒฐ๊ณผ ๋ฐœํ–‰ ์™„๋ฃŒ...")

return {"job_id": correlationId}
7 changes: 2 additions & 5 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@
load_dotenv()

redis_url = os.getenv("REDIS_SERVER_URL")
bucket_name = os.getenv("BUCKET_NAME")
s3_region = os.getenv("S3_REGION")

openai_key = os.getenv("OPENAI_API_KEY")

class Settings(BaseSettings):
REDIS_URL: str = redis_url
S3_REGION: str = bucket_name
BUCKET_NAME: str = s3_region
OPENAI_KEY: str = openai_key

STREAM_JOB: str = "image.jobs" # SpringBoot์—์„œ job ๋ฐœํ–‰ (FastAPI์—์„œ listen)
STREAM_RESULT: str = "image.results" # FastAPI์—์„œ ๊ฒฐ๊ณผ ๋ฐœํ–‰ (SpringBoot์—์„œ listen)
Expand Down
22 changes: 10 additions & 12 deletions app/core/lifespan.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@

import asyncio
from contextlib import asynccontextmanager
import redis.asyncio as redis

import anyio.to_thread
import redis
from fastapi import FastAPI

from app.worker.redis_client import redis_client
from app.core.config import settings
from app.worker.worker import JobWorker

@asynccontextmanager
async def lifespan(app: FastAPI):
redis_client = redis.from_url(settings.REDIS_URL, decode_responses=True)
limiter = anyio.to_thread.current_default_thread_limiter()
limiter.total_tokens = 100

try:
await redis_client.xgroup_create(
name=settings.STREAM_JOB,
groupname=settings.GROUP_NAME,
id="$",
mkstream=True
)
except redis.ResponseError as e:
if "BUSYGROUP" not in str(e):
raise
redis_client.xgroup_create(
stream_name=settings.STREAM_JOB,
group_name=settings.GROUP_NAME,
)
Comment on lines +18 to +21

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

redis_client.xgroup_create๋Š” ์ฝ”๋ฃจํ‹ด(coroutine)์ด๋ฏ€๋กœ await ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. await๊ฐ€ ์—†์œผ๋ฉด ๊ทธ๋ฃน์ด ์‹ค์ œ๋กœ ์ƒ์„ฑ๋˜์ง€ ์•Š์•„ ์›Œ์ปค๊ฐ€ ์ŠคํŠธ๋ฆผ ๋ฉ”์‹œ์ง€๋ฅผ ์ฝ์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Suggested change
redis_client.xgroup_create(
stream_name=settings.STREAM_JOB,
group_name=settings.GROUP_NAME,
)
await redis_client.xgroup_create(
stream_name=settings.STREAM_JOB,
group_name=settings.GROUP_NAME,
)


worker = JobWorker(redis_client)
worker_task = asyncio.create_task(worker.run())
Expand Down
31 changes: 18 additions & 13 deletions app/schemas/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@
from pydantic import BaseModel, Field

class JobRequest(BaseModel):
presigned_url: str = Field(..., description="๋‹ค์šด๋กœ๋“œํ•  ์ด๋ฏธ์ง€์˜ Presigned URL")
presignedUrl: str = Field(..., description="๋‹ค์šด๋กœ๋“œํ•  ์ด๋ฏธ์ง€์˜ Presigned URL")

"""
FastAPI๊ฐ€ image.results Stream์— ๋ฐœํ–‰ํ•˜๋Š” ๋ฉ”์‹œ์ง€
"""
class JobResult(BaseModel):
pill_name: str
correlation_id: str
label: str
confidence: float
finished_at: str
correlationId: str
pillName: str
isSafe: int
description: str
finishedAt: str

"""
Spring์œผ๋กœ๋ถ€ํ„ฐ ๋ฐ›๋Š” Job(image.jobs ๊ตฌ๋…)
"""
class ImageJob(BaseModel):
correlation_id: str = Field(alias="correlationId")
presigned_url: str = Field(alias="presignedUrl")
reply_queue: str = Field(alias="replyQueue")
callback_url: str | None = Field(alias="callbackUrl")
content_type: str = Field(alias="contentType")
created_at: str = Field(alias="createdAt")
ttl_sec: int = Field(alias="ttlSec")
correlationId: str = Field(alias="correlationId")
presignedUrl: str = Field(alias="presignedUrl")
replyQueue: str = Field(alias="replyQueue")
contentType: str = Field(alias="contentType")
createdAt: str = Field(alias="createdAt")
ttlSec: int = Field(alias="ttlSec")
Comment on lines 4 to +26

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Pydantic ๋ชจ๋ธ๋“ค์˜ ํ•„๋“œ๋ช…์ด camelCase๋กœ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Python์—์„œ๋Š” PEP 8 ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ์— ๋”ฐ๋ผ snake_case๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ํ‘œ์ค€์ž…๋‹ˆ๋‹ค. ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ์˜ JSON ๋ฐ์ดํ„ฐ ๊ตํ™˜์„ ์œ„ํ•ด camelCase๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด, Python ์ฝ”๋“œ ๋‚ด์—์„œ๋Š” snake_case๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  Pydantic์˜ Field(alias=...)๋ฅผ ํ†ตํ•ด JSON ํ•„๋“œ๋ช…์„ ๋งคํ•‘ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Python ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ์™ธ๋ถ€ API์™€์˜ ํ˜ธํ™˜์„ฑ์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

class JobRequest(BaseModel):
    presigned_url: str = Field(..., alias="presignedUrl", description="๋‹ค์šด๋กœ๋“œํ•  ์ด๋ฏธ์ง€์˜ Presigned URL")

"""
FastAPI๊ฐ€ image.results Stream์— ๋ฐœํ–‰ํ•˜๋Š” ๋ฉ”์‹œ์ง€
"""
class JobResult(BaseModel):
    correlation_id: str = Field(alias="correlationId")
    pill_name: str = Field(alias="pillName")
    is_safe: int = Field(alias="isSafe")
    description: str
    finished_at: str = Field(alias="finishedAt")

"""
Spring์œผ๋กœ๋ถ€ํ„ฐ ๋ฐ›๋Š” Job(image.jobs ๊ตฌ๋…)
"""
class ImageJob(BaseModel):
    correlation_id: str = Field(alias="correlationId")
    presigned_url: str = Field(alias="presignedUrl")
    reply_queue: str = Field(alias="replyQueue")
    content_type: str = Field(alias="contentType")
    created_at: str = Field(alias="createdAt")
    ttl_sec: int = Field(alias="ttlSec")

66 changes: 66 additions & 0 deletions app/services/openai_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from app.core.config import settings
from openai import OpenAI
import json

client = OpenAI(api_key=settings.OPENAI_KEY)

class PregnancySafetyChecker:
def __init__(self, client: OpenAI):
self.client = client

"""
- isSafe: ์•ˆ์ „ํ•˜๋ฉด 1, ์•ˆ์ „ํ•˜์ง€ ์•Š์œผ๋ฉด 0
- description: ๋ณต์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ์„ค๋ช…
"""
def ask_chatgpt_about_pregnancy_safety(self, pill_name: str) -> tuple[str, int]:
prompt = f"""
์•ฝ ์ด๋ฆ„: {pill_name}
์งˆ๋ฌธ: ์ด ์•ฝ์€ ์ž„์‚ฐ๋ถ€๊ฐ€ ๋ณต์šฉํ•ด๋„ ์•ˆ์ „ํ•œ๊ฐ€์š”? ๋ณต์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€์™€ ์ฃผ์˜์‚ฌํ•ญ์„ ์•Œ๋ ค์ฃผ์„ธ์š”.
description ์•ˆ์—๋Š” ๋ฌธ์žฅ๋งˆ๋‹ค \\n ์„ ์ ์šฉํ•˜์„ธ์š”.
๊ฒฐ๊ณผ๋ฅผ JSON ํ˜•์‹์œผ๋กœ ์ •ํ™•ํžˆ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”. ์„ค๋ช…์ด๋‚˜ ๋‹ค๋ฅธ ํ…์ŠคํŠธ๋ฅผ ์ ˆ๋Œ€ ๋ง๋ถ™์ด์ง€ ๋งˆ์„ธ์š”.
์Šคํ‚ค๋งˆ:
{{
"description": "๋ณต์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ๋ฐ ์ฃผ์˜์‚ฌํ•ญ์— ๋Œ€ํ•œ ์„ค๋ช…",
"isSafe": 1 ๋˜๋Š” 0
}}
"""

response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=600,
response_format={"type": "json_object"}
)
print("GPT Asking ์„ฑ๊ณต...")
raw = response.choices[0].message.content.strip()

try:
data = json.loads(raw)
except json.JSONDecodeError:
start = raw.find("{")
end = raw.rfind("}")
if start != -1 and end != -1 and start < end:
data = json.loads(raw[start:end+1])
else:
# ๋””๋ฒ„๊น…
preview = raw[:200].replace("\n", "\\n")
raise ValueError(f"์‘๋‹ต์ด ์œ ํšจํ•œ JSON์ด ์•„๋‹™๋‹ˆ๋‹ค. preview='{preview}'")

description = data.get("description")
isSafe = data.get("isSafe")

if isinstance(isSafe, bool):
isSafe = 1 if isSafe else 0
elif isinstance(isSafe, str):
isSafe = 1 if isSafe.strip() in {"1", "true", "True"} else 0
elif not isinstance(isSafe, int):
isSafe = 0

if not isinstance(description, str):
description = "" # ์•ˆ์ „์žฅ์น˜

return description, int(isSafe)


checker = PregnancySafetyChecker(client)
5 changes: 3 additions & 2 deletions app/services/predictor_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from PIL import Image
import json
from pathlib import Path
from io import BytesIO

class LightCNN(nn.Module):
def __init__(self, num_classes):
Expand Down Expand Up @@ -53,8 +54,8 @@ def _load_model(self, model_path: Path) -> LightCNN:
model.eval()
return model

def predict(self, image_path: Path) -> tuple[str, str, float]:
image = Image.open(image_path).convert('RGB')
def predict(self, stream_file: BytesIO) -> tuple[str, str, float]:
image = Image.open(stream_file).convert('RGB')
input_tensor = self.transform(image).unsqueeze(0).to(self.device)

with torch.no_grad():
Expand Down
22 changes: 5 additions & 17 deletions app/services/s3_service.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@

import boto3
from botocore.config import Config as BotoConfig
from pathlib import Path
import requests

from app.core.config import settings
from io import BytesIO

class S3Service:
def __init__(self):
self.client = boto3.client(
"s3",
region_name=settings.S3_REGION,
config=BotoConfig(
retries={"max_attempts": 5, "mode": "standard"},
read_timeout=30,
connect_timeout=5,
),
)
pass

def download_file_from_presigned_url(self, presigned_url: str, destination: Path):
def download_file_from_presigned_url(self, presigned_url: str) -> BytesIO:
response = requests.get(presigned_url)
response.raise_for_status()
with open(destination, "wb") as f:
f.write(response.content)
# response์˜ content๋ฅผ BytesIO๋กœ ๊ฐ์‹ธ ๋ฐ˜ํ™˜
return BytesIO(response.content)

s3_service = S3Service()
Loading