commit fd6815f11a4426451b42fa791f96ae71efb3c83c Author: AGX Date: Thu Jul 17 10:16:31 2025 +0900 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7947543 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin/ +include/ +lib/ +lib64/ +share/ +pyvenv.cfg diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3e81619 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY ./app /app/app + +EXPOSE 7860 +EXPOSE 5555 + +# 컨테이너 실행시 아무 것도 안 띄움 (docker-compose에서 실행) +CMD ["bash"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..3554962 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# FastAPI + Celery + Redis + Flower 메인서버 예시 + +## 실행 방법 + +```bash +docker-compose up --build +FastAPI: http://localhost:7860 + +Flower: http://localhost:5555 + +API 예시 +POST /process (파일 업로드) + +GET /result/{task_id} (결과 확인) + + +--- diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/__pycache__/__init__.cpython-310.pyc b/app/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..65ef3fe Binary files /dev/null and b/app/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/__pycache__/celery_worker.cpython-310.pyc b/app/__pycache__/celery_worker.cpython-310.pyc new file mode 100644 index 0000000..36a7165 Binary files /dev/null and b/app/__pycache__/celery_worker.cpython-310.pyc differ diff --git a/app/__pycache__/main.cpython-310.pyc b/app/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000..dd22d26 Binary files /dev/null and b/app/__pycache__/main.cpython-310.pyc differ diff --git a/app/__pycache__/supabase_auth.cpython-310.pyc b/app/__pycache__/supabase_auth.cpython-310.pyc new file mode 100644 index 0000000..44d2d95 Binary files /dev/null and b/app/__pycache__/supabase_auth.cpython-310.pyc differ diff --git a/app/__pycache__/tasks.cpython-310.pyc b/app/__pycache__/tasks.cpython-310.pyc new file mode 100644 index 0000000..f31119f Binary files /dev/null and b/app/__pycache__/tasks.cpython-310.pyc differ diff --git a/app/beat_schedule.py b/app/beat_schedule.py new file mode 100644 index 0000000..f593152 --- /dev/null +++ b/app/beat_schedule.py @@ -0,0 +1,11 @@ +# app/beat_schedule.py +from celery.schedules import crontab + +beat_schedule = { + "delete-old-files-every-10-mins": { + "task": "app.tasks.delete_old_files", + "schedule": crontab(minute="*/10"), + "args": ("/app/images", 1800), # 30분 이상된 파일 삭제 + } +} +# celery_app.conf.beat_schedule = beat_schedule diff --git a/app/celery_worker.py b/app/celery_worker.py new file mode 100644 index 0000000..7da5194 --- /dev/null +++ b/app/celery_worker.py @@ -0,0 +1,22 @@ +from celery import Celery +import os + +# Redis URL 설정 +redis_url = os.getenv("REDIS_URL", "redis://redis:6379/0") + +# Celery 앱 생성 +celery_app = Celery( + "worker", + broker=redis_url, + backend=redis_url, + include=['app.tasks'] # 태스크 모듈 포함 +) + +# Celery 설정 +celery_app.conf.update( + task_serializer='json', + accept_content=['json'], + result_serializer='json', + timezone='Asia/Seoul', + enable_utc=True, +) diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..3eaaede --- /dev/null +++ b/app/main.py @@ -0,0 +1,106 @@ +from fastapi import FastAPI, Request, HTTPException, Response, UploadFile, File, Form +from pydantic import BaseModel, Field +from typing import Dict, Optional, List +from app.celery_worker import celery_app +from app.supabase_auth import check_user_permission +from celery.result import AsyncResult +import time +import os, shutil +import uuid +import logging + +# 로거 설정 +logger = logging.getLogger(__name__) + +app = FastAPI() + +# ==== 입력 모델 ==== +class TranslateRequest(BaseModel): + toggle_states: Dict + unwanted_texts: Dict + image_data: str + user_id: str + ocr_method: Optional[str] = "paddleocr" + inpaint_method: Optional[str] = "lama" + +class InpaintRequest(BaseModel): + mask_image_data: str + image_data: str + user_id: str + inpaint_method: Optional[str] = "lama" + +class OCRRequest(BaseModel): + image_data: str + user_id: str + ocr_method: Optional[str] = "paddleocr" + +# ==== 리턴 모델 ==== +class OCRBox(BaseModel): + text: str + box: List[int] # [x1, y1, x2, y2, ...] + +class TranslateResponse(BaseModel): + ocr_texts: List[str] + ocr_boxes: List[OCRBox] + translated_texts: List[str] + inpainted_image: str # base64 + +class InpaintResponse(BaseModel): + inpainted_image: str # base64 + +class OCRResponse(BaseModel): + ocr_texts: List[str] + ocr_boxes: List[OCRBox] + +# ==== 사용자 인증 ==== +async def validate_user(user_id): + allowed = await check_user_permission(user_id) + if not allowed: + raise HTTPException(status_code=403, detail="권한이 없습니다.") + +# ==== 셀러리 태스크 ==== +def start_celery_task(task_name, **kwargs): + return celery_app.send_task(task_name, kwargs=kwargs) + +# ==== 엔드포인트 ==== +@app.post("/translate_me") +async def translate_me(req: TranslateRequest): + await validate_user(req.user_id) + filename = f"{uuid.uuid4().hex}_{int(time.time())}.png" + # (여기서 파일 경로 생성 및 저장) + task = start_celery_task("app.tasks.translate_task", **req.dict(), filename=filename) + logger.info(f"태스크 등록: {task.id}, 파일명: {filename}") + return {"task_id": task.id, "filename": filename} + +@app.post("/inpaint_me") +async def inpaint_me(req: InpaintRequest): + await validate_user(req.user_id) + task = start_celery_task("app.tasks.inpaint_task", **req.dict()) + return {"task_id": task.id} + +@app.post("/ocr_me") +async def ocr_me(req: OCRRequest): + await validate_user(req.user_id) + task = start_celery_task("app.tasks.ocr_task", **req.dict()) + return {"task_id": task.id} + +@app.post("/upload_image") +async def upload_image(user_id: str = Form(...), file: UploadFile = File(...)): + original_dir = f"images/{user_id}/original/" + os.makedirs(original_dir, exist_ok=True) + filename = file.filename + file_path = os.path.join(original_dir, filename) + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + url = f"/images/{user_id}/original/{filename}" + return {"url": url} + +@app.get("/download_image/") +def download_image(user_id: str, filename: str): + file_path = f"images/{user_id}/translated/{filename}" + if not os.path.exists(file_path): + return {"error": "파일이 없습니다"} + with open(file_path, "rb") as f: + data = f.read() + os.remove(file_path) # 다운로드 직후 삭제 + return Response(content=data, media_type="image/png") \ No newline at end of file diff --git a/app/supabase_auth.py b/app/supabase_auth.py new file mode 100644 index 0000000..2987be2 --- /dev/null +++ b/app/supabase_auth.py @@ -0,0 +1,18 @@ +from supabase import create_client +import os + +SUPABASE_URL = 'http://146.56.101.199:8000' +SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE' + +#SUPABASE_URL = os.getenv("SUPABASE_URL") +#SUPABASE_KEY = os.getenv("SUPABASE_KEY") + +supabase = create_client(SUPABASE_URL, SUPABASE_KEY) + +async def check_user_permission(user_id: str) -> bool: + # 실제 쿼리 구조는 supabase 테이블에 맞게! + user = supabase.table("users").select("id, membership_level").eq("id", user_id).execute() + print(f"user info : {user}") + if user.data and user.data[0]["membership_level"] in ["premiun", "vip"]: + return True + return False diff --git a/app/tasks.py b/app/tasks.py new file mode 100644 index 0000000..1fecde0 --- /dev/null +++ b/app/tasks.py @@ -0,0 +1,32 @@ +from app.celery_worker import celery_app + +@celery_app.task(name="app.tasks.translate_task") +def translate_task(**kwargs): + # 실제 번역 처리 로직 + return { + "ocr_texts": ["중국어1", "중국어2"], + "ocr_boxes": [ + {"text": "중국어1", "box": [10, 20, 100, 120]}, + {"text": "중국어2", "box": [110, 120, 200, 220]} + ], + "translated_texts": ["한글1", "한글2"], + "inpainted_image": "base64string...." + } + +@celery_app.task(name="app.tasks.inpaint_task") +def inpaint_task(**kwargs): + # 실제 인페인팅 처리 로직 + return { + "inpainted_image": "base64string...." + } + +@celery_app.task(name="app.tasks.ocr_task") +def ocr_task(**kwargs): + # 실제 OCR 처리 로직 + return { + "ocr_texts": ["중국어1", "중국어2"], + "ocr_boxes": [ + {"text": "중국어1", "box": [10, 20, 100, 120]}, + {"text": "중국어2", "box": [110, 120, 200, 220]} + ] + } \ No newline at end of file diff --git a/docker-compose b/docker-compose new file mode 100755 index 0000000..998f3fc Binary files /dev/null and b/docker-compose differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b91a396 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +version: "3.8" +services: + fastapi: + build: . + container_name: fastapi_app + command: uvicorn app.main:app --host 0.0.0.0 --port 7890 + volumes: + - ./app:/app/app + - ./images:/app/images + ports: + - "7890:7890" + depends_on: + - redis + celery: + build: . + container_name: celery_worker + command: celery -A app.celery_worker worker --loglevel=info --concurrency=2 + volumes: + - ./app:/app/app + depends_on: + - redis + + celery-beat: + build: . + container_name: celery_beat + command: celery -A app.celery_worker beat --loglevel=info + volumes: + - ./app:/app/app + - ./images:/app/images + depends_on: + - redis + + redis: + image: redis:6.2 + container_name: redis_server + ports: + - "6379:6379" + flower: + build: . + container_name: flower_monitor + command: celery -A app.celery_worker flower --port=5555 + ports: + - "5555:5555" + depends_on: + - redis diff --git a/lib64 b/lib64 new file mode 120000 index 0000000..7951405 --- /dev/null +++ b/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d712b01 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +fastapi +uvicorn[standard] +celery[redis] +redis +flower +python-dotenv +pillow +supabase +python-multipart