Discord 웹훅 URL을 설정하고 조회하는 API 엔드포인트를 추가하였습니다. 마이크로 배치 처리를 위한 설정을 추가하고, 인페인팅 및 배경 제거 작업에서 배치 처리를 지원하도록 로직을 개선하였습니다. 서버 시작 및 종료 시 Discord 알림을 전송하도록 수정하였으며, 모델 처리 시간 통계를 기록하는 방식을 개선하였습니다.

This commit is contained in:
AGX 2025-08-30 01:58:17 +09:00
parent c18667c17d
commit 1617ec95ec
16 changed files with 2329 additions and 231 deletions

View File

@ -6,7 +6,7 @@ import time
import logging import logging
from datetime import datetime from datetime import datetime
from typing import List from typing import List
from fastapi import APIRouter, HTTPException, Response, Query from fastapi import APIRouter, HTTPException, Response, Query, Request
from fastapi.responses import JSONResponse, StreamingResponse from fastapi.responses import JSONResponse, StreamingResponse
import cv2 import cv2
@ -26,12 +26,43 @@ import base64
import io import io
from ..monitoring.dashboard import monitoring_data from ..monitoring.dashboard import monitoring_data
from .stats import router as stats_router from .stats import router as stats_router
from ..core.stats_manager import stats_manager
from ..core.batch_manager import batch_manager
from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
router.include_router(stats_router, prefix="/api/v1", tags=["Stats"]) router.include_router(stats_router, prefix="/api/v1", tags=["Stats"])
WEBHOOK_URL_FILE = Path(settings.PROJECT_ROOT) / "webhook_url.txt"
def get_webhook_url() -> str:
if WEBHOOK_URL_FILE.exists():
return WEBHOOK_URL_FILE.read_text().strip()
return ""
def save_webhook_url(url: str):
WEBHOOK_URL_FILE.write_text(url)
@router.get("/api/v1/webhook", summary="Get Discord Webhook URL")
async def get_webhook():
"""저장된 Discord 웹훅 URL을 조회합니다."""
return {"url": get_webhook_url()}
@router.post("/api/v1/webhook", summary="Set Discord Webhook URL")
async def set_webhook(request: Request):
"""Discord 웹훅 URL을 저장합니다."""
data = await request.json()
url = data.get("url", "")
save_webhook_url(url)
# Update settings in runtime if needed, or notifier can read from file directly
settings.DISCORD_WEBHOOK_URL = url
logger.info(f"Discord 웹훅 URL이 업데이트되었습니다: {url}")
return {"message": "Webhook URL이 성공적으로 저장되었습니다."}
def encode_image_to_format(image, format: ImageFormat = ImageFormat.png, quality: int = 95): def encode_image_to_format(image, format: ImageFormat = ImageFormat.png, quality: int = 95):
"""이미지를 지정된 형식으로 인코딩""" """이미지를 지정된 형식으로 인코딩"""
@ -104,16 +135,17 @@ def create_response(
@router.get("/api/v1/health", response_model=HealthResponse, name="health_check") @router.get("/api/v1/health", response_model=HealthResponse, name="health_check")
async def health_check(): async def health_check(request: Request):
"""서버 상태 확인""" """서버 상태 확인"""
start_time = getattr(settings, 'start_time', time.time()) start_time = getattr(request.app.state, 'start_time', time.time())
uptime = time.time() - start_time current_time = time.time()
uptime_seconds = current_time - start_time
return HealthResponse( return HealthResponse(
status="healthy", status="ok",
timestamp=datetime.now().isoformat(), uptime=uptime_seconds,
version="1.0.0", timestamp=datetime.fromtimestamp(current_time).isoformat(),
uptime=uptime version="1.0.0"
) )
@ -199,17 +231,32 @@ async def inpaint_image(
model_name = request.model_name or "simple-lama" model_name = request.model_name or "simple-lama"
# 워커에서 인페인팅 실행 # 워커에서 인페인팅 실행
result = await worker_manager.process_inpaint( if settings.USE_MICRO_BATCHING and model_name == "simple-lama":
image=image, # SimpleLama는 배치 관리자를 통해 처리
mask=mask, job_data = {
model_name=model_name, "image": image,
prompt=request.prompt, "mask": mask,
negative_prompt=request.negative_prompt, "prompt": request.prompt,
sd_seed=request.sd_seed, "negative_prompt": request.negative_prompt,
num_inference_steps=request.num_inference_steps, "sd_seed": request.sd_seed,
guidance_scale=request.guidance_scale, "num_inference_steps": request.num_inference_steps,
strength=request.strength "guidance_scale": request.guidance_scale,
) "strength": request.strength,
}
result = await batch_manager.add_job(job_data)
else:
# 다른 모델들은 기존 워커 매니저를 통해 단일 처리
result = await worker_manager.process_inpaint(
image=image,
mask=mask,
model_name=model_name,
prompt=request.prompt,
negative_prompt=request.negative_prompt,
sd_seed=request.sd_seed,
num_inference_steps=request.num_inference_steps,
guidance_scale=request.guidance_scale,
strength=request.strength
)
if result is None: if result is None:
raise HTTPException(status_code=500, detail="인페인팅 처리 실패") raise HTTPException(status_code=500, detail="인페인팅 처리 실패")
@ -286,7 +333,9 @@ async def remove_background(
) )
# 모델 선택 # 모델 선택
model_name = request.model_name or "rembg" model_name = request.model_name
if not model_name or model_name == "rembg":
model_name = "birefnet-general-lite"
# 워커에서 배경 제거 실행 # 워커에서 배경 제거 실행
result_image, result_mask = await worker_manager.process_remove_bg( result_image, result_mask = await worker_manager.process_remove_bg(

141
app/core/batch_manager.py Normal file
View File

@ -0,0 +1,141 @@
import asyncio
import time
from typing import List, Dict, Any, Tuple
from asyncio import Future, Queue, Task
import uuid
import logging
from .config import settings
from .worker_manager import worker_manager
from .session_pool import ModelType
logger = logging.getLogger(__name__)
class BatchJob:
"""배치 처리를 위한 개별 작업 단위"""
def __init__(self, job_id: str, job_data: Dict[str, Any]):
self.job_id = job_id
self.job_data = job_data
self.future = asyncio.get_running_loop().create_future()
class BatchManager:
"""
마이크로 배치를 관리하는 클래스.
- 요청을 큐에 수집합니다.
- 백그라운드 태스크를 통해 큐를 감시하며 배치를 생성합니다.
- 생성된 배치를 WorkerManager에 전달하여 처리합니다.
- 처리 결과를 요청에 전달합니다.
"""
def __init__(self):
self._queue: Queue[BatchJob] = Queue()
self._batch_creation_task: Task | None = None
self._active = False
async def start(self):
"""배치 관리자를 시작합니다."""
if self._active:
logger.warning("BatchManager is already running.")
return
logger.info("Starting BatchManager...")
self._active = True
self._batch_creation_task = asyncio.create_task(self._batch_creation_loop())
logger.info("BatchManager started successfully.")
async def stop(self):
"""배치 관리자를 중지합니다."""
if not self._active:
logger.warning("BatchManager is not running.")
return
logger.info("Stopping BatchManager...")
self._active = False
if self._batch_creation_task:
self._batch_creation_task.cancel()
try:
await self._batch_creation_task
except asyncio.CancelledError:
pass # Task cancellation is expected
logger.info("BatchManager stopped.")
async def add_job(self, job_data: Dict[str, Any]) -> Any:
"""
API 엔드포인트에서 호출하는 메서드.
작업을 큐에 추가하고 결과가 나올 때까지 대기합니다.
"""
if not self._active:
raise RuntimeError("BatchManager is not running.")
job_id = str(uuid.uuid4())
job = BatchJob(job_id=job_id, job_data=job_data)
await self._queue.put(job)
logger.debug(f"Job {job.job_id} added to the batch queue. Queue size: {self._queue.qsize()}")
# 작업 결과를 기다립니다.
result = await job.future
return result
async def _batch_creation_loop(self):
"""
백그라운드에서 실행되며 큐를 감시하여 배치를 생성하는 루프.
"""
while self._active:
try:
# 첫 번째 작업을 기다립니다. 타임아웃이 발생하면 루프를 계속합니다.
first_job = await asyncio.wait_for(self._queue.get(), timeout=1.0)
except asyncio.TimeoutError:
continue
batch = [first_job]
batch_size = settings.MICRO_BATCH_SIZE
timeout = settings.MICRO_BATCH_TIMEOUT_MS / 1000.0 # 초 단위로 변환
# 타임아웃까지 또는 배치가 꽉 찰 때까지 작업을 추가로 수집합니다.
start_time = time.monotonic()
while len(batch) < batch_size and (time.monotonic() - start_time) < timeout:
try:
# 남은 시간만큼만 대기합니다.
remaining_time = timeout - (time.monotonic() - start_time)
if remaining_time <= 0:
break
job = await asyncio.wait_for(self._queue.get(), timeout=remaining_time)
batch.append(job)
except asyncio.TimeoutError:
break # 대기 시간 초과
logger.info(f"Creating a new batch with {len(batch)} jobs.")
# 배치를 처리할 별도의 태스크를 생성하여 루프가 다른 배치를 만드는 것을 막지 않도록 합니다.
asyncio.create_task(self._process_batch(batch))
async def _process_batch(self, batch: List[BatchJob]):
"""
생성된 배치를 WorkerManager에 전달하여 처리하고 결과를 전파합니다.
"""
batch_data = [job.job_data for job in batch]
try:
# WorkerManager에 배치 처리를 요청합니다.
# worker_manager의 process_inpaint는 이제 배치 데이터를 처리할 수 있어야 합니다.
results = await worker_manager.process_inpaint_batch(batch_data)
if len(results) != len(batch):
raise ValueError(f"Result count ({len(results)}) does not match batch size ({len(batch)}).")
# 결과를 각 작업의 Future에 설정합니다.
for job, result in zip(batch, results):
if isinstance(result, Exception):
job.future.set_exception(result)
else:
job.future.set_result(result)
logger.info(f"Successfully processed batch of {len(batch)} jobs.")
except Exception as e:
logger.error(f"Failed to process batch: {e}", exc_info=True)
# 모든 작업에 예외를 전파합니다.
for job in batch:
if not job.future.done():
job.future.set_exception(e)
# 전역 BatchManager 인스턴스
batch_manager = BatchManager()

View File

@ -5,6 +5,10 @@ import os
import platform import platform
from typing import Dict, Any, Optional, ClassVar from typing import Dict, Any, Optional, ClassVar
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
class Settings(BaseSettings): class Settings(BaseSettings):
@ -30,6 +34,11 @@ class Settings(BaseSettings):
# 유휴 세션 자동 제거 시간 (초). 0이면 비활성화. # 유휴 세션 자동 제거 시간 (초). 0이면 비활성화.
SESSION_IDLE_TIMEOUT: int = 1800 # 30분 SESSION_IDLE_TIMEOUT: int = 1800 # 30분
# --- 마이크로 배치 설정 ---
USE_MICRO_BATCHING: bool = True # SimpleLama에 대한 배치 처리 활성화
MICRO_BATCH_SIZE: int = 4 # 최대 배치 크기
MICRO_BATCH_TIMEOUT_MS: int = 100 # 배치 생성을 위한 최대 대기 시간 (밀리초)
# --- 서버 환경 설정 (클래스 내부로 이동) --- # --- 서버 환경 설정 (클래스 내부로 이동) ---
APP_VERSION: str = "3.0.0-dynamic-pool" APP_VERSION: str = "3.0.0-dynamic-pool"
APP_NAME: str = "Inpaint & RemoveBG Server" APP_NAME: str = "Inpaint & RemoveBG Server"
@ -108,3 +117,15 @@ class Settings(BaseSettings):
settings = Settings() settings = Settings()
# 파일에서 웹훅 URL 로드 (환경 변수보다 우선순위 낮음)
if not settings.DISCORD_WEBHOOK_URL:
try:
webhook_file = Path(settings.PROJECT_ROOT) / "webhook_url.txt"
if webhook_file.exists():
url = webhook_file.read_text().strip()
if url:
settings.DISCORD_WEBHOOK_URL = url
logger.info(f"파일에서 Discord 웹훅 URL을 로드했습니다: {url[:30]}...")
except Exception as e:
logger.warning(f"webhook_url.txt 파일 로드 실패: {e}")

View File

@ -22,23 +22,24 @@ class StatsManager:
'max_time': 0.0, 'max_time': 0.0,
} }
def record_time(self, model_name: str, duration: float): def record_time(self, model_name: str, duration: float, count: int = 1):
"""처리 시간을 기록합니다.""" """처리 시간을 기록합니다."""
if model_name not in self.model_keys:
# logger.warning(f"Unknown model for stats: {model_name}") # Assuming logger is defined elsewhere
return
with self.lock: with self.lock:
# 개별 모델 통계 업데이트 # 모델별 통계 업데이트
if model_name in self.data: self.data[model_name]['count'] += count
stats = self.data[model_name] self.data[model_name]['total_time'] += duration * count # 전체 시간에 (평균시간 * 개수) 를 더함
stats['count'] += 1 self.data[model_name]['min_time'] = min(self.data[model_name]['min_time'], duration)
stats['total_time'] += duration self.data[model_name]['max_time'] = max(self.data[model_name]['max_time'], duration)
stats['min_time'] = min(stats['min_time'], duration)
stats['max_time'] = max(stats['max_time'], duration)
# 전체 통계 업데이트 # 전체 통계 업데이트
total_stats = self.data['total'] self.data['total']['count'] += count
total_stats['count'] += 1 self.data['total']['total_time'] += duration * count
total_stats['total_time'] += duration self.data['total']['min_time'] = min(self.data['total']['min_time'], duration)
total_stats['min_time'] = min(total_stats['min_time'], duration) self.data['total']['max_time'] = max(self.data['total']['max_time'], duration)
total_stats['max_time'] = max(total_stats['max_time'], duration)
def get_stats(self) -> dict: def get_stats(self) -> dict:
"""현재까지의 통계를 계산하여 반환합니다.""" """현재까지의 통계를 계산하여 반환합니다."""

View File

@ -329,38 +329,61 @@ class WorkerManager:
} }
async def process_inpaint(self, **kwargs) -> Optional[np.ndarray]: async def process_inpaint(self, **kwargs) -> Optional[np.ndarray]:
"""인페인팅 작업을 처리합니다.""" # 배치 처리를 사용하지 않는 모델 (예: Migan)을 위한 메서드
try: model_name = kwargs.get('model_name', 'migan')
if model_name == 'migan':
model_type = ModelType.MIGAN
stats_model_key = 'migan'
else:
# SimpleLama는 이제 배치 처리를 통해서만 호출되어야 함
logger.error(f"Unsupported model for single inpaint: {model_name}. Use process_inpaint_batch for simple-lama.")
raise ValueError(f"Unsupported model for single inpaint: {model_name}")
async def _inpaint():
from ..core.session_pool import session_pool, ModelType from ..core.session_pool import session_pool, ModelType
model_name = kwargs.get('model_name', 'simple-lama')
# 모델명에 따라 세션 타입 및 통계 키 결정
if model_name == 'migan':
model_type = ModelType.MIGAN
stats_model_key = 'migan'
else:
# 기본값은 simple_lama
model_type = ModelType.SIMPLE_LAMA
stats_model_key = 'simple_lama'
# 세션 풀에서 모델 세션 가져와서 처리
async with session_pool.get_session(model_type) as session: async with session_pool.get_session(model_type) as session:
start_time = time.time() start_time = time.time()
# session.model 에서 실제 모델 객체의 메서드를 호출해야 함 # Migan은 단일 이미지만 처리하므로 기존 로직 유지
result = await session.model.inpaint( result = await session.model.inpaint(
image=kwargs['image'], image=kwargs["image"],
mask=kwargs['mask'] mask=kwargs["mask"],
) )
duration = time.time() - start_time duration = time.time() - start_time
stats_manager.record_time(stats_model_key, duration) stats_manager.record_time(stats_model_key, duration)
logger.info(f"'{model_name}' inpainting processed in {duration:.3f}s") logger.info(f"'{model_name}' inpainting processed in {duration:.3f}s")
return result
return result # _execute_task 대신 직접 실행
return await _inpaint()
except Exception as e: async def process_inpaint_batch(self, batch_data: List[Dict[str, Any]]) -> List[np.ndarray]:
logger.error(f"인페인팅 처리 실패: {e}", exc_info=True) """SimpleLama 배치 인페인팅 작업을 처리합니다."""
return None if not batch_data:
return []
# 태스크를 직접 실행하도록 수정
from ..core.session_pool import session_pool, ModelType
model_type = ModelType.SIMPLE_LAMA
stats_model_key = 'simple_lama'
batch_size = len(batch_data)
async with session_pool.get_session(model_type) as session:
start_time = time.time()
images = [item['image'] for item in batch_data]
masks = [item['mask'] for item in batch_data]
# 수정된 inpaint 메서드 호출
results = await session.model.inpaint(
images=images,
masks=masks,
)
duration = time.time() - start_time
# 통계 기록: 배치 전체 처리 시간 / 배치 크기
stats_manager.record_time(stats_model_key, duration / batch_size, count=batch_size)
logger.info(f"'simple-lama' batch of {batch_size} processed in {duration:.3f}s (avg: {duration/batch_size:.3f}s/image)")
return results
async def process_remove_bg(self, **kwargs) -> Optional[Tuple[np.ndarray, np.ndarray]]: async def process_remove_bg(self, **kwargs) -> Optional[Tuple[np.ndarray, np.ndarray]]:
"""배경 제거 작업을 처리합니다.""" """배경 제거 작업을 처리합니다."""

View File

@ -6,19 +6,27 @@ import numpy as np
import cv2 import cv2
from PIL import Image from PIL import Image
import logging import logging
from typing import Union, Tuple from typing import Union, Tuple, List
import asyncio import asyncio
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from simple_lama_inpainting import SimpleLama
# 사용하지 않는 import 정리
# from ..utils.image_utils import (
# decode_base64_to_image,
# encode_image_to_base64,
# get_image_size,
# resize_image_if_needed,
# )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SimpleLamaInpainter: class SimpleLamaInpainter:
def __init__(self, model_path: str = None, device: str = "cuda", fp16: bool = True): def __init__(self, model_path: str, device: str = "cpu", fp16: bool = False):
self.model_path = model_path self.model_path = model_path
self.device = device self._device = torch.device(device)
self.fp16 = fp16 self._fp16 = fp16
self.model = None self._model = None
self.loaded = False self.loaded = False
async def load_model(self): async def load_model(self):
@ -31,18 +39,17 @@ class SimpleLamaInpainter:
# 실제 simple-lama-inpainting 라이브러리 사용 # 실제 simple-lama-inpainting 라이브러리 사용
try: try:
from simple_lama_inpainting import SimpleLama self._model = SimpleLama(device=self._device)
self.model = SimpleLama(device=self.device)
logger.info("실제 SimpleLama 모델 로딩 완료") logger.info("실제 SimpleLama 모델 로딩 완료")
except ImportError as e: except ImportError as e:
logger.warning(f"SimpleLama 라이브러리 import 실패: {e}") logger.warning(f"SimpleLama 라이브러리 import 실패: {e}")
logger.info("fallback 모드로 전환합니다...") logger.info("fallback 모드로 전환합니다...")
# fallback으로 시뮬레이션 모드 사용 # fallback으로 시뮬레이션 모드 사용
self.model = {"type": "simple_lama_fallback", "device": self.device, "fp16": self.fp16} self._model = {"type": "simple_lama_fallback", "device": self._device, "fp16": self._fp16}
except Exception as e: except Exception as e:
logger.error(f"SimpleLama 모델 초기화 실패: {e}") logger.error(f"SimpleLama 모델 초기화 실패: {e}")
logger.info("fallback 모드로 전환합니다...") logger.info("fallback 모드로 전환합니다...")
self.model = {"type": "simple_lama_fallback", "device": self.device, "fp16": self.fp16} self._model = {"type": "simple_lama_fallback", "device": self._device, "fp16": self._fp16}
self.loaded = True self.loaded = True
logger.info("Simple LAMA model loaded successfully") logger.info("Simple LAMA model loaded successfully")
@ -68,10 +75,10 @@ class SimpleLamaInpainter:
# 텐서로 변환 (B, C, H, W) # 텐서로 변환 (B, C, H, W)
tensor = torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0) tensor = torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0)
if self.fp16: if self._fp16:
tensor = tensor.half() tensor = tensor.half()
return tensor.to(self.device) return tensor.to(self._device)
def preprocess_mask(self, mask: Union[Image.Image, np.ndarray]) -> torch.Tensor: def preprocess_mask(self, mask: Union[Image.Image, np.ndarray]) -> torch.Tensor:
"""마스크를 전처리합니다.""" """마스크를 전처리합니다."""
@ -88,10 +95,10 @@ class SimpleLamaInpainter:
# 텐서로 변환 (B, 1, H, W) # 텐서로 변환 (B, 1, H, W)
tensor = torch.from_numpy(mask).unsqueeze(0).unsqueeze(0) tensor = torch.from_numpy(mask).unsqueeze(0).unsqueeze(0)
if self.fp16: if self._fp16:
tensor = tensor.half() tensor = tensor.half()
return tensor.to(self.device) return tensor.to(self._device)
def postprocess_result(self, tensor: torch.Tensor) -> np.ndarray: def postprocess_result(self, tensor: torch.Tensor) -> np.ndarray:
"""결과를 후처리합니다.""" """결과를 후처리합니다."""
@ -108,73 +115,89 @@ class SimpleLamaInpainter:
return result return result
async def inpaint(self, image: Union[Image.Image, np.ndarray], async def inpaint(
mask: Union[Image.Image, np.ndarray]) -> np.ndarray: self,
"""인페인팅을 수행합니다.""" images: List[np.ndarray],
masks: List[np.ndarray],
**kwargs,
) -> List[np.ndarray]:
if not self.loaded: if not self.loaded:
await self.load_model() await self.load_model()
try: if not self.is_ready:
# 전처리 raise RuntimeError("SimpleLama model is not loaded yet.")
image_tensor = self.preprocess_image(image)
mask_tensor = self.preprocess_mask(mask)
# 실제 모델 추론 # 모델이 GPU에 있는지 확인
with torch.no_grad(): if self._device.type != 'cpu':
if hasattr(self.model, '__call__') and not isinstance(self.model, dict): torch.cuda.empty_cache()
# 실제 SimpleLama 모델 사용
logger.info("실제 SimpleLama 모델로 인페인팅 수행")
# SimpleLama는 PIL Image를 받으므로 변환 # 전처리
if isinstance(image, np.ndarray): pil_images = [Image.fromarray(img) for img in images]
pil_image = Image.fromarray(image) pil_masks = [Image.fromarray(mask) for mask in masks]
else:
pil_image = image
if isinstance(mask, np.ndarray): preprocessed_images = []
pil_mask = Image.fromarray(mask) preprocessed_masks = []
else: for img, mask in zip(pil_images, pil_masks):
pil_mask = mask img_tensor, mask_tensor = self._preprocess(img, mask)
preprocessed_images.append(img_tensor)
preprocessed_masks.append(mask_tensor)
# 실제 추론 수행 image_batch = torch.stack(preprocessed_images).to(self._device)
result_pil = self.model(pil_image, pil_mask) mask_batch = torch.stack(preprocessed_masks).to(self._device)
result_np = np.array(result_pil)
return result_np # 모델 호출
else: logger.info(f"실제 SimpleLama 모델로 {len(images)}개 이미지 인페인팅 수행")
# Fallback: 시뮬레이션 모드 with torch.no_grad():
logger.warning("Fallback 모드: 시뮬레이션 인페인팅 사용") # 라이브러리의 __call__ 대신 내부 torch 모델을 직접 호출
result = await self._simulate_inpainting(image_tensor, mask_tensor) inpainted_batch = self._model.model(image_batch, mask_batch)
result_np = self.postprocess_result(result)
return result_np
except Exception as e: # 후처리
logger.error(f"Inpainting failed: {e}") result_images = []
raise for inpainted_tensor in inpainted_batch:
result_pil = self._postprocess(inpainted_tensor)
result_images.append(np.array(result_pil))
async def _simulate_inpainting(self, image_tensor: torch.Tensor, return result_images
mask_tensor: torch.Tensor) -> torch.Tensor:
"""인페인팅 시뮬레이션 (실제 구현에서는 제거)"""
# 비동기 처리 시뮬레이션
await asyncio.sleep(0.1)
# 마스크 영역을 이미지의 평균 색상으로 채우기 def _preprocess(self, image: Image.Image, mask: Image.Image):
result = image_tensor.clone() """단일 이미지를 모델 입력 텐서로 전처리합니다."""
mask_bool = mask_tensor.bool() # simple_lama_inpainting.models.lama.py의 전처리 로직 참고
image = image.convert("RGB")
mask = mask.convert("L")
# 각 채널별 평균 계산 # 원본 크기 저장
for c in range(3): original_size = image.size
channel_mean = image_tensor[0, c][~mask_bool[0, 0]].mean()
result[0, c][mask_bool[0, 0]] = channel_mean
return result # 이미지 리사이즈 (모델 요구사항에 맞게)
resized_image = image.resize((512, 512), Image.Resampling.LANCZOS)
resized_mask = mask.resize((512, 512), Image.Resampling.NEAREST)
image_tensor = torch.from_numpy(np.array(resized_image, dtype=np.float32) / 255.0).permute(2, 0, 1).unsqueeze(0).squeeze(0)
mask_tensor = torch.from_numpy(np.array(resized_mask, dtype=np.float32) / 255.0).unsqueeze(0).unsqueeze(0).squeeze(0)
return image_tensor, mask_tensor
def _postprocess(self, tensor: torch.Tensor) -> Image.Image:
"""모델 출력 텐서를 PIL 이미지로 후처리합니다."""
# simple_lama_inpainting.models.lama.py의 후처리 로직 참고
result_np = tensor.permute(1, 2, 0).cpu().numpy()
result_np = np.clip(result_np * 255, 0, 255).astype(np.uint8)
# 원본 크기로 복원 (여기서는 512x512 결과를 그대로 사용)
# 필요하다면 원본 크기 정보를 받아 리사이즈하는 로직 추가
return Image.fromarray(result_np)
@property
def is_ready(self) -> bool:
return self._model is not None
def get_model_info(self) -> dict: def get_model_info(self) -> dict:
"""모델 정보를 반환합니다.""" """모델 정보를 반환합니다."""
return { return {
"model_type": "simple_lama", "model_type": "simple_lama",
"device": self.device, "device": self._device,
"fp16": self.fp16, "fp16": self._fp16,
"loaded": self.loaded, "loaded": self.loaded,
"model_path": self.model_path "model_path": self.model_path
} }

View File

@ -5,18 +5,35 @@ import logging
import requests import requests
import socket import socket
from datetime import datetime from datetime import datetime
from pathlib import Path
from ..core.config import settings from ..core.config import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
WEBHOOK_URL_FILE = Path(settings.PROJECT_ROOT) / "webhook_url.txt"
def get_webhook_url() -> str:
"""환경 변수 또는 파일에서 웹훅 URL을 가져옵니다."""
# 1. 환경 변수 확인
if settings.DISCORD_WEBHOOK_URL:
return settings.DISCORD_WEBHOOK_URL
# 2. 파일 확인
if WEBHOOK_URL_FILE.exists():
return WEBHOOK_URL_FILE.read_text().strip()
return ""
def send_discord_notification(message: str, level: str = "info"): def send_discord_notification(message: str, level: str = "info"):
""" """Discord 웹훅으로 알림을 보냅니다."""
Discord 웹훅으로 알림을 보냅니다.
""" webhook_url = get_webhook_url()
webhook_url = settings.DISCORD_WEBHOOK_URL
if not webhook_url: if not webhook_url:
logger.warning("Discord 웹훅 URL이 설정되지 않아 알림을 보낼 수 없습니다.") if "시작" in message or "종료" in message:
logger.warning("Discord 웹훅 URL이 설정되지 않아 알림을 보낼 수 없습니다.")
return return
hostname = socket.gethostname() hostname = socket.gethostname()

File diff suppressed because it is too large Load Diff

View File

@ -1,71 +1,127 @@
INFO: Started server process [56165] INFO: Started server process [77869]
2025-08-29 23:46:34,096 - uvicorn.error - INFO - Started server process [56165] 2025-08-30 01:55:09,899 - uvicorn.error - INFO - Started server process [77869]
INFO: Waiting for application startup. INFO: Waiting for application startup.
2025-08-29 23:46:34,097 - uvicorn.error - INFO - Waiting for application startup. 2025-08-30 01:55:09,900 - uvicorn.error - INFO - Waiting for application startup.
2025-08-29 23:46:34,098 - main - INFO - 🚀 인페인팅 서버 시작 중... 2025-08-30 01:55:09,901 - main - INFO - 🚀 인페인팅 서버 시작 중...
2025-08-29 23:46:34,098 - main - INFO - ✅ 공유 객체를 app.state에 저장 완료 2025-08-30 01:55:09,901 - main - INFO - ✅ 공유 객체를 app.state에 저장 완료
2025-08-29 23:46:34,098 - main - INFO - 🔄 상태 저장 백그라운드 작업 생성 중... 2025-08-30 01:55:09,902 - main - INFO - 🔄 상태 저장 백그라운드 작업 생성 중...
2025-08-29 23:46:34,099 - main - INFO - ✅ 상태 저장 백그라운드 작업 생성 완료 2025-08-30 01:55:09,902 - main - INFO - ✅ 상태 저장 백그라운드 작업 생성 완료
2025-08-29 23:46:34,099 - main - INFO - 🚀 세션 풀 초기화 (CUDA 자동 감지) 2025-08-30 01:55:09,902 - main - INFO - 🚀 세션 풀 초기화 (CUDA 자동 감지)
2025-08-29 23:46:34,100 - app.core.session_pool - INFO - Initializing dynamic session pools... 2025-08-30 01:55:09,902 - app.core.session_pool - INFO - Initializing dynamic session pools...
2025-08-29 23:46:34,100 - app.core.session_pool - INFO - Pre-loading 2 sessions for simple_lama 2025-08-30 01:55:09,903 - app.core.session_pool - INFO - Pre-loading 2 sessions for simple_lama
2025-08-29 23:46:34,100 - main - INFO - 🔄 상태 저장 백그라운드 작업 시작됨 2025-08-30 01:55:09,903 - main - INFO - 🔄 상태 저장 백그라운드 작업 시작됨
2025-08-29 23:46:34,104 - app.core.session_pool - INFO - Creating new session simple_lama_0 for simple_lama... 2025-08-30 01:55:09,905 - app.core.session_pool - INFO - Creating new session simple_lama_0 for simple_lama...
2025-08-29 23:46:37,259 - app.models.simple_lama - INFO - Loading Simple LAMA model... 2025-08-30 01:55:13,106 - app.models.simple_lama - INFO - Loading Simple LAMA model...
2025-08-29 23:46:41,562 - app.models.simple_lama - INFO - 실제 SimpleLama 모델 로딩 완료 2025-08-30 01:55:17,237 - app.models.simple_lama - INFO - 실제 SimpleLama 모델 로딩 완료
2025-08-29 23:46:41,563 - app.models.simple_lama - INFO - Simple LAMA model loaded successfully 2025-08-30 01:55:17,239 - app.models.simple_lama - INFO - Simple LAMA model loaded successfully
2025-08-29 23:46:41,564 - app.core.session_pool - INFO - Successfully created session simple_lama_0 2025-08-30 01:55:17,240 - app.core.session_pool - INFO - Successfully created session simple_lama_0
2025-08-29 23:46:41,565 - app.core.session_pool - INFO - Creating new session simple_lama_1 for simple_lama... 2025-08-30 01:55:17,241 - app.core.session_pool - INFO - Creating new session simple_lama_1 for simple_lama...
2025-08-29 23:46:41,566 - app.models.simple_lama - INFO - Loading Simple LAMA model... 2025-08-30 01:55:17,241 - app.models.simple_lama - INFO - Loading Simple LAMA model...
2025-08-29 23:46:43,429 - app.models.simple_lama - INFO - 실제 SimpleLama 모델 로딩 완료 2025-08-30 01:55:18,996 - app.models.simple_lama - INFO - 실제 SimpleLama 모델 로딩 완료
2025-08-29 23:46:43,429 - app.models.simple_lama - INFO - Simple LAMA model loaded successfully 2025-08-30 01:55:18,997 - app.models.simple_lama - INFO - Simple LAMA model loaded successfully
2025-08-29 23:46:43,429 - app.core.session_pool - INFO - Successfully created session simple_lama_1 2025-08-30 01:55:18,997 - app.core.session_pool - INFO - Successfully created session simple_lama_1
2025-08-29 23:46:43,430 - app.core.session_pool - INFO - Pre-loading 2 sessions for migan 2025-08-30 01:55:18,998 - app.core.session_pool - INFO - Pre-loading 2 sessions for migan
2025-08-29 23:46:43,432 - app.core.session_pool - INFO - Creating new session migan_0 for migan... 2025-08-30 01:55:18,999 - app.core.session_pool - INFO - Creating new session migan_0 for migan...
2025-08-29 23:46:43,491 - app.models.migan - INFO - Loading MIGAN ONNX model... 2025-08-30 01:55:19,063 - app.models.migan - INFO - Loading MIGAN ONNX model...
2025-08-29 23:46:43,491 - app.models.migan - INFO - MIGAN ONNX 런타임 세션 생성 시도... 2025-08-30 01:55:19,064 - app.models.migan - INFO - MIGAN ONNX 런타임 세션 생성 시도...
2025-08-29 23:46:43,492 - app.models.migan - INFO - MIGAN ONNX providers 설정: ['CUDAExecutionProvider', 'CPUExecutionProvider'] 2025-08-30 01:55:19,064 - app.models.migan - INFO - MIGAN ONNX providers 설정: ['CUDAExecutionProvider', 'CPUExecutionProvider']
2025-08-29 23:46:44.934790365 [W:onnxruntime:, transformer_memcpy.cc:74 ApplyImpl] 17 Memcpy nodes are added to the graph main_graph for CUDAExecutionProvider. It might have negative impact on performance (including unable to run CUDA graph). Set session_options.log_severity_level=1 to see the detail logs before this message. 2025-08-30 01:55:20.438912801 [W:onnxruntime:, transformer_memcpy.cc:74 ApplyImpl] 17 Memcpy nodes are added to the graph main_graph for CUDAExecutionProvider. It might have negative impact on performance (including unable to run CUDA graph). Set session_options.log_severity_level=1 to see the detail logs before this message.
2025-08-29 23:46:46,412 - app.models.migan - INFO - MIGAN ONNX 세션 생성 완료. Providers: ['CUDAExecutionProvider', 'CPUExecutionProvider'] 2025-08-30 01:55:21,872 - app.models.migan - INFO - MIGAN ONNX 세션 생성 완료. Providers: ['CUDAExecutionProvider', 'CPUExecutionProvider']
2025-08-29 23:46:46,413 - app.models.migan - INFO - MIGAN ONNX model loaded successfully 2025-08-30 01:55:21,873 - app.models.migan - INFO - MIGAN ONNX model loaded successfully
2025-08-29 23:46:46,413 - app.core.session_pool - INFO - Successfully created session migan_0 2025-08-30 01:55:21,874 - app.core.session_pool - INFO - Successfully created session migan_0
2025-08-29 23:46:46,414 - app.core.session_pool - INFO - Creating new session migan_1 for migan... 2025-08-30 01:55:21,874 - app.core.session_pool - INFO - Creating new session migan_1 for migan...
2025-08-29 23:46:46,414 - app.models.migan - INFO - Loading MIGAN ONNX model... 2025-08-30 01:55:21,875 - app.models.migan - INFO - Loading MIGAN ONNX model...
2025-08-29 23:46:46,415 - app.models.migan - INFO - MIGAN ONNX 런타임 세션 생성 시도... 2025-08-30 01:55:21,875 - app.models.migan - INFO - MIGAN ONNX 런타임 세션 생성 시도...
2025-08-29 23:46:46,415 - app.models.migan - INFO - MIGAN ONNX providers 설정: ['CUDAExecutionProvider', 'CPUExecutionProvider'] 2025-08-30 01:55:21,875 - app.models.migan - INFO - MIGAN ONNX providers 설정: ['CUDAExecutionProvider', 'CPUExecutionProvider']
2025-08-29 23:46:47.468053292 [W:onnxruntime:, transformer_memcpy.cc:74 ApplyImpl] 17 Memcpy nodes are added to the graph main_graph for CUDAExecutionProvider. It might have negative impact on performance (including unable to run CUDA graph). Set session_options.log_severity_level=1 to see the detail logs before this message. 2025-08-30 01:55:22.968322444 [W:onnxruntime:, transformer_memcpy.cc:74 ApplyImpl] 17 Memcpy nodes are added to the graph main_graph for CUDAExecutionProvider. It might have negative impact on performance (including unable to run CUDA graph). Set session_options.log_severity_level=1 to see the detail logs before this message.
2025-08-29 23:46:47,630 - app.models.migan - INFO - MIGAN ONNX 세션 생성 완료. Providers: ['CUDAExecutionProvider', 'CPUExecutionProvider'] 2025-08-30 01:55:23,132 - app.models.migan - INFO - MIGAN ONNX 세션 생성 완료. Providers: ['CUDAExecutionProvider', 'CPUExecutionProvider']
2025-08-29 23:46:47,631 - app.models.migan - INFO - MIGAN ONNX model loaded successfully 2025-08-30 01:55:23,133 - app.models.migan - INFO - MIGAN ONNX model loaded successfully
2025-08-29 23:46:47,632 - app.core.session_pool - INFO - Successfully created session migan_1 2025-08-30 01:55:23,134 - app.core.session_pool - INFO - Successfully created session migan_1
2025-08-29 23:46:47,632 - app.core.session_pool - INFO - Pre-loading 2 sessions for rembg 2025-08-30 01:55:23,134 - app.core.session_pool - INFO - Pre-loading 2 sessions for rembg
2025-08-29 23:46:47,634 - app.core.session_pool - INFO - Creating new session rembg_0 for rembg... 2025-08-30 01:55:23,136 - app.core.session_pool - INFO - Creating new session rembg_0 for rembg...
2025-08-29 23:46:47,634 - app.core.session_pool - INFO - Creating new session rembg_1 for rembg... 2025-08-30 01:55:23,137 - app.core.session_pool - INFO - Creating new session rembg_1 for rembg...
2025-08-29 23:46:47,638 - app.models.rembg_model - INFO - Loading REMBG model (birefnet-general-lite)... 2025-08-30 01:55:23,139 - app.models.rembg_model - INFO - Loading REMBG model (birefnet-general-lite)...
2025-08-29 23:46:49,612 - app.models.rembg_model - INFO - rembg 모듈 임포트 성공 (세션 생성은 지연 로딩) 2025-08-30 01:55:25,141 - app.models.rembg_model - INFO - rembg 모듈 임포트 성공 (세션 생성은 지연 로딩)
2025-08-29 23:46:49,612 - app.models.rembg_model - INFO - 🔧 rembg 새 세션 생성 필요: birefnet-general-lite_cuda_True 2025-08-30 01:55:25,141 - app.models.rembg_model - INFO - 🔧 rembg 새 세션 생성 필요: birefnet-general-lite_cuda_True
2025-08-29 23:46:49,613 - app.models.rembg_model - WARNING - rembg.sessions import 실패, 기본 방식 사용 2025-08-30 01:55:25,142 - app.models.rembg_model - WARNING - rembg.sessions import 실패, 기본 방식 사용
2025-08-29 23:46:49,613 - app.models.rembg_model - INFO - rembg 세션 생성 providers: ['CUDAExecutionProvider', 'CPUExecutionProvider'] 2025-08-30 01:55:25,142 - app.models.rembg_model - INFO - rembg 세션 생성 providers: ['CUDAExecutionProvider', 'CPUExecutionProvider']
2025-08-29 23:47:02,613 - app.models.rembg_model - INFO - ✅ rembg 'birefnet-general-lite' GPU 가속로 동작 (providers: ['CUDAExecutionProvider', 'CPUExecutionProvider']) 2025-08-30 01:55:38,144 - app.models.rembg_model - INFO - ✅ rembg 'birefnet-general-lite' GPU 가속로 동작 (providers: ['CUDAExecutionProvider', 'CPUExecutionProvider'])
2025-08-29 23:47:02,614 - app.models.rembg_model - INFO - REMBG model (birefnet-general-lite) loaded successfully 2025-08-30 01:55:38,145 - app.models.rembg_model - INFO - REMBG model (birefnet-general-lite) loaded successfully
2025-08-29 23:47:02,615 - app.models.rembg_model - INFO - Loading REMBG model (birefnet-general-lite)... 2025-08-30 01:55:38,146 - app.models.rembg_model - INFO - Loading REMBG model (birefnet-general-lite)...
2025-08-29 23:47:02,615 - app.models.rembg_model - INFO - rembg 모듈 임포트 성공 (세션 생성은 지연 로딩) 2025-08-30 01:55:38,146 - app.models.rembg_model - INFO - rembg 모듈 임포트 성공 (세션 생성은 지연 로딩)
2025-08-29 23:47:02,616 - app.models.rembg_model - INFO - 🔧 rembg 새 세션 생성 필요: birefnet-general-lite_cuda_True 2025-08-30 01:55:38,147 - app.models.rembg_model - INFO - 🔧 rembg 새 세션 생성 필요: birefnet-general-lite_cuda_True
2025-08-29 23:47:02,617 - app.models.rembg_model - WARNING - rembg.sessions import 실패, 기본 방식 사용 2025-08-30 01:55:38,148 - app.models.rembg_model - WARNING - rembg.sessions import 실패, 기본 방식 사용
2025-08-29 23:47:02,617 - app.models.rembg_model - INFO - rembg 세션 생성 providers: ['CUDAExecutionProvider', 'CPUExecutionProvider'] 2025-08-30 01:55:38,148 - app.models.rembg_model - INFO - rembg 세션 생성 providers: ['CUDAExecutionProvider', 'CPUExecutionProvider']
2025-08-29 23:47:15,228 - app.models.rembg_model - INFO - ✅ rembg 'birefnet-general-lite' GPU 가속로 동작 (providers: ['CUDAExecutionProvider', 'CPUExecutionProvider']) 2025-08-30 01:55:50,692 - app.models.rembg_model - INFO - ✅ rembg 'birefnet-general-lite' GPU 가속로 동작 (providers: ['CUDAExecutionProvider', 'CPUExecutionProvider'])
2025-08-29 23:47:15,229 - app.models.rembg_model - INFO - REMBG model (birefnet-general-lite) loaded successfully 2025-08-30 01:55:50,693 - app.models.rembg_model - INFO - REMBG model (birefnet-general-lite) loaded successfully
2025-08-29 23:47:15,230 - app.core.session_pool - INFO - Successfully created session rembg_0 2025-08-30 01:55:50,694 - app.core.session_pool - INFO - Successfully created session rembg_0
2025-08-29 23:47:15,230 - app.core.session_pool - INFO - Successfully created session rembg_1 2025-08-30 01:55:50,694 - app.core.session_pool - INFO - Successfully created session rembg_1
2025-08-29 23:47:15,232 - app.core.session_pool - INFO - Session pools initialized successfully 2025-08-30 01:55:50,696 - app.core.session_pool - INFO - Session pools initialized successfully
2025-08-29 23:47:15,232 - main - INFO - ✅ 세션 풀 초기화 완료 2025-08-30 01:55:50,697 - main - INFO - ✅ 세션 풀 초기화 완료
2025-08-29 23:47:15,233 - app.core.worker_manager - INFO - Starting worker manager... 2025-08-30 01:55:50,697 - app.core.worker_manager - INFO - Starting worker manager...
2025-08-29 23:47:15,234 - app.core.worker_manager - INFO - Worker manager started with 10 workers 2025-08-30 01:55:50,698 - app.core.worker_manager - INFO - Worker manager started with 10 workers
2025-08-29 23:47:15,234 - main - INFO - ✅ 워커 매니저 시작 완료 2025-08-30 01:55:50,698 - main - INFO - ✅ 워커 매니저 시작 완료
2025-08-29 23:47:15,234 - main - INFO - 🎉 인페인팅 서버 시작 완료! 2025-08-30 01:55:50,698 - app.core.batch_manager - INFO - Starting BatchManager...
2025-08-29 23:47:15,235 - app.core.session_pool - INFO - Idle session reaper started. Timeout: 1800s, Check Interval: 60s 2025-08-30 01:55:50,699 - app.core.batch_manager - INFO - BatchManager started successfully.
2025-08-30 01:55:50,699 - main - INFO - ✅ 배치 관리자 시작 완료
2025-08-30 01:55:50,699 - main - INFO - 🎉 인페인팅 서버 시작 완료!
2025-08-30 01:55:50,700 - app.utils.discord_notifier - WARNING - Discord 웹훅 URL이 설정되지 않아 알림을 보낼 수 없습니다.
2025-08-30 01:55:50,700 - app.core.session_pool - INFO - Idle session reaper started. Timeout: 1800s, Check Interval: 60s
INFO: Application startup complete. INFO: Application startup complete.
2025-08-29 23:47:15,235 - uvicorn.error - INFO - Application startup complete. 2025-08-30 01:55:50,701 - uvicorn.error - INFO - Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8008 (Press CTRL+C to quit) INFO: Uvicorn running on http://0.0.0.0:8008 (Press CTRL+C to quit)
2025-08-29 23:47:15,236 - uvicorn.error - INFO - Uvicorn running on http://0.0.0.0:8008 (Press CTRL+C to quit) 2025-08-30 01:55:50,702 - uvicorn.error - INFO - Uvicorn running on http://0.0.0.0:8008 (Press CTRL+C to quit)
INFO: 127.0.0.1:57044 - "GET /api/v1/health HTTP/1.1" 200 OK INFO: 127.0.0.1:52600 - "GET /api/v1/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:57060 - "GET /api/v1/health HTTP/1.1" 200 OK INFO: 127.0.0.1:52608 - "GET /api/v1/health HTTP/1.1" 200 OK
2025-08-30 01:56:15,715 - app.core.batch_manager - INFO - Creating a new batch with 4 jobs.
2025-08-30 01:56:15,881 - app.models.simple_lama - INFO - 실제 SimpleLama 모델로 4개 이미지 인페인팅 수행
2025-08-30 01:56:21,625 - app.core.worker_manager - INFO - 'simple-lama' batch of 4 processed in 5.908s (avg: 1.477s/image)
2025-08-30 01:56:21,626 - app.core.batch_manager - INFO - Successfully processed batch of 4 jobs.
INFO: 127.0.0.1:39480 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:39488 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:39500 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:39516 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
2025-08-30 01:56:24,783 - app.core.batch_manager - INFO - Creating a new batch with 2 jobs.
2025-08-30 01:56:24,995 - app.models.simple_lama - INFO - 실제 SimpleLama 모델로 2개 이미지 인페인팅 수행
2025-08-30 01:56:39,252 - app.core.worker_manager - INFO - 'simple-lama' batch of 2 processed in 14.460s (avg: 7.230s/image)
2025-08-30 01:56:39,253 - app.core.batch_manager - INFO - Successfully processed batch of 2 jobs.
INFO: 127.0.0.1:39524 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:39536 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
2025-08-30 01:56:39,367 - app.core.batch_manager - INFO - Creating a new batch with 2 jobs.
2025-08-30 01:56:39,469 - app.models.simple_lama - INFO - 실제 SimpleLama 모델로 2개 이미지 인페인팅 수행
2025-08-30 01:56:40,764 - app.core.worker_manager - INFO - 'simple-lama' batch of 2 processed in 1.396s (avg: 0.698s/image)
2025-08-30 01:56:40,765 - app.core.batch_manager - INFO - Successfully processed batch of 2 jobs.
INFO: 127.0.0.1:39546 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:39550 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
2025-08-30 01:57:35,001 - app.core.batch_manager - INFO - Creating a new batch with 4 jobs.
2025-08-30 01:57:35,213 - app.models.simple_lama - INFO - 실제 SimpleLama 모델로 4개 이미지 인페인팅 수행
2025-08-30 01:57:37,476 - app.core.worker_manager - INFO - 'simple-lama' batch of 4 processed in 2.472s (avg: 0.618s/image)
2025-08-30 01:57:37,480 - app.core.batch_manager - INFO - Successfully processed batch of 4 jobs.
INFO: 127.0.0.1:39274 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:39278 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:39286 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:39296 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
2025-08-30 01:57:37,511 - app.core.batch_manager - INFO - Creating a new batch with 4 jobs.
2025-08-30 01:57:37,735 - app.models.simple_lama - INFO - 실제 SimpleLama 모델로 4개 이미지 인페인팅 수행
2025-08-30 01:57:39,686 - app.core.worker_manager - INFO - 'simple-lama' batch of 4 processed in 2.173s (avg: 0.543s/image)
2025-08-30 01:57:39,687 - app.core.batch_manager - INFO - Successfully processed batch of 4 jobs.
INFO: 127.0.0.1:39308 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:39322 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:39330 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:39334 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
2025-08-30 01:57:47,368 - app.core.batch_manager - INFO - Creating a new batch with 4 jobs.
2025-08-30 01:57:47,620 - app.models.simple_lama - INFO - 실제 SimpleLama 모델로 4개 이미지 인페인팅 수행
2025-08-30 01:57:49,769 - app.core.worker_manager - INFO - 'simple-lama' batch of 4 processed in 2.399s (avg: 0.600s/image)
2025-08-30 01:57:49,770 - app.core.batch_manager - INFO - Successfully processed batch of 4 jobs.
INFO: 127.0.0.1:47538 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:47542 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:47554 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:47566 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
2025-08-30 01:57:49,800 - app.core.batch_manager - INFO - Creating a new batch with 4 jobs.
2025-08-30 01:57:50,022 - app.models.simple_lama - INFO - 실제 SimpleLama 모델로 4개 이미지 인페인팅 수행
2025-08-30 01:57:51,985 - app.core.worker_manager - INFO - 'simple-lama' batch of 4 processed in 2.184s (avg: 0.546s/image)
2025-08-30 01:57:51,986 - app.core.batch_manager - INFO - Successfully processed batch of 4 jobs.
INFO: 127.0.0.1:47580 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:47590 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:47604 - "POST /api/v1/inpaint HTTP/1.1" 200 OK
INFO: 127.0.0.1:47608 - "POST /api/v1/inpaint HTTP/1.1" 200 OK

View File

@ -1 +1 @@
56165 77869

View File

@ -1,9 +1,9 @@
INFO: Started server process [56411] INFO: Started server process [78184]
INFO: Waiting for application startup. INFO: Waiting for application startup.
Fan control not available Fan control not available
INFO: Application startup complete. INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8888 (Press CTRL+C to quit) INFO: Uvicorn running on http://0.0.0.0:8888 (Press CTRL+C to quit)
INFO: 127.0.0.1:43630 - "GET /api/simple HTTP/1.1" 200 OK INFO: 127.0.0.1:52490 - "GET /api/simple HTTP/1.1" 200 OK
Task exception was never retrieved Task exception was never retrieved
future: <Task finished name='Task-4' coro=<health_check_and_restart() done, defined at /home/ckh08045/work/inpaintServer/app/monitoring/dashboard.py:2084> exception=AttributeError("module 'asyncio' has no attribute 'to_thread'")> future: <Task finished name='Task-4' coro=<health_check_and_restart() done, defined at /home/ckh08045/work/inpaintServer/app/monitoring/dashboard.py:2084> exception=AttributeError("module 'asyncio' has no attribute 'to_thread'")>
Traceback (most recent call last): Traceback (most recent call last):

View File

@ -1 +1 @@
56411 78184

34
main.py
View File

@ -18,10 +18,14 @@ from app.core.worker_manager import worker_manager
from app.core.session_pool import SessionPool, session_pool from app.core.session_pool import SessionPool, session_pool
from app.api.endpoints import router from app.api.endpoints import router
from app.monitoring.dashboard import monitor_app from app.monitoring.dashboard import monitor_app
from app.core.batch_manager import batch_manager
# from app.utils.background_task import manage_state_background # TODO: 경로 확인 필요
from app.utils.discord_notifier import send_discord_notification
# 로깅 설정 # 로깅 설정
import logging.handlers import logging.handlers
import os import os
import logging.config
# 로그 디렉토리 생성 # 로그 디렉토리 생성
log_dir = "logs" log_dir = "logs"
@ -202,8 +206,9 @@ async def save_status_periodically():
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""애플리케이션 생명주기 관리""" """애플리케이션 생명주기 관리"""
# 시작 시 # 서버 시작 시
logger.info("🚀 인페인팅 서버 시작 중...") logger.info("🚀 인페인팅 서버 시작 중...")
app.state.start_time = time.time() # settings 대신 app.state에 저장
# app.state에 공유 객체 저장 # app.state에 공유 객체 저장
app.state.worker_manager = worker_manager app.state.worker_manager = worker_manager
@ -226,7 +231,15 @@ async def lifespan(app: FastAPI):
await worker_manager.start() await worker_manager.start()
logger.info("✅ 워커 매니저 시작 완료") logger.info("✅ 워커 매니저 시작 완료")
if settings.USE_MICRO_BATCHING:
await batch_manager.start()
logger.info("✅ 배치 관리자 시작 완료")
# app.state.background_task = asyncio.create_task(manage_state_background(app.state))
# logger.info("✅ 상태 저장 백그라운드 작업 생성 완료")
logger.info("🎉 인페인팅 서버 시작 완료!") logger.info("🎉 인페인팅 서버 시작 완료!")
send_discord_notification("✅ 서버가 성공적으로 시작되었습니다.", level="success")
except Exception as e: except Exception as e:
logger.error(f"❌ 서버 시작 실패: {e}") logger.error(f"❌ 서버 시작 실패: {e}")
@ -234,7 +247,7 @@ async def lifespan(app: FastAPI):
yield yield
# 종료 시 # 서버 종료 시
logger.info("🛑 인페인팅 서버 종료 중...") logger.info("🛑 인페인팅 서버 종료 중...")
# 상태 저장 백그라운드 작업 취소 # 상태 저장 백그라운드 작업 취소
@ -245,7 +258,19 @@ async def lifespan(app: FastAPI):
await worker_manager.stop() await worker_manager.stop()
logger.info("✅ 워커 매니저 중지 완료") logger.info("✅ 워커 매니저 중지 완료")
if settings.USE_MICRO_BATCHING:
await batch_manager.stop()
logger.info("✅ 배치 관리자 중지 완료")
# if app.state.background_task:
# app.state.background_task.cancel()
# try:
# await app.state.background_task
# except asyncio.CancelledError:
# logger.info("상태 저장 백그라운드 작업이 정상적으로 취소되었습니다.")
logger.info("👋 인페인팅 서버 종료 완료") logger.info("👋 인페인팅 서버 종료 완료")
send_discord_notification("👋 서버가 종료되었습니다.", level="info")
except Exception as e: except Exception as e:
logger.error(f"❌ 서버 종료 중 오류: {e}") logger.error(f"❌ 서버 종료 중 오류: {e}")
@ -253,9 +278,8 @@ async def lifespan(app: FastAPI):
# 메인 애플리케이션 생성 # 메인 애플리케이션 생성
app = FastAPI( app = FastAPI(
title="인페인팅 서버", title=settings.APP_NAME,
description="Simple LAMA, MIGAN, REMBG를 활용한 병렬 처리 인페인팅 서버 (iopaint 호환)", version=settings.APP_VERSION,
version="1.0.0",
lifespan=lifespan lifespan=lifespan
) )

View File

@ -6,70 +6,70 @@
"workers_by_status": { "workers_by_status": {
"idle": [ "idle": [
{ {
"id": "worker_fcfdab20", "id": "worker_1daac568",
"status": "idle", "status": "idle",
"task_count": 0, "task_count": 0,
"error_count": 0, "error_count": 0,
"last_task_at": null "last_task_at": null
}, },
{ {
"id": "worker_07900527", "id": "worker_c66b7d6b",
"status": "idle", "status": "idle",
"task_count": 0, "task_count": 0,
"error_count": 0, "error_count": 0,
"last_task_at": null "last_task_at": null
}, },
{ {
"id": "worker_fa21a361", "id": "worker_cefffbad",
"status": "idle", "status": "idle",
"task_count": 0, "task_count": 0,
"error_count": 0, "error_count": 0,
"last_task_at": null "last_task_at": null
}, },
{ {
"id": "worker_aa0517fd", "id": "worker_009a14df",
"status": "idle", "status": "idle",
"task_count": 0, "task_count": 0,
"error_count": 0, "error_count": 0,
"last_task_at": null "last_task_at": null
}, },
{ {
"id": "worker_f6949bff", "id": "worker_335c1fa5",
"status": "idle", "status": "idle",
"task_count": 0, "task_count": 0,
"error_count": 0, "error_count": 0,
"last_task_at": null "last_task_at": null
}, },
{ {
"id": "worker_2dc147eb", "id": "worker_2767d405",
"status": "idle", "status": "idle",
"task_count": 0, "task_count": 0,
"error_count": 0, "error_count": 0,
"last_task_at": null "last_task_at": null
}, },
{ {
"id": "worker_20ae03ff", "id": "worker_8ec98a8f",
"status": "idle", "status": "idle",
"task_count": 0, "task_count": 0,
"error_count": 0, "error_count": 0,
"last_task_at": null "last_task_at": null
}, },
{ {
"id": "worker_ed0f495e", "id": "worker_1c42e06e",
"status": "idle", "status": "idle",
"task_count": 0, "task_count": 0,
"error_count": 0, "error_count": 0,
"last_task_at": null "last_task_at": null
}, },
{ {
"id": "worker_06d35af9", "id": "worker_3124fda8",
"status": "idle", "status": "idle",
"task_count": 0, "task_count": 0,
"error_count": 0, "error_count": 0,
"last_task_at": null "last_task_at": null
}, },
{ {
"id": "worker_325beb67", "id": "worker_074f5bdb",
"status": "idle", "status": "idle",
"task_count": 0, "task_count": 0,
"error_count": 0, "error_count": 0,
@ -106,30 +106,38 @@
} }
}, },
"api_stats": { "api_stats": {
"total_requests": 2, "total_requests": 26,
"successful_requests": 2, "successful_requests": 26,
"failed_requests": 0, "failed_requests": 0,
"success_rate": 100.0, "success_rate": 100.0,
"endpoint_usage": { "endpoint_usage": {
"GET /api/v1/health": 2 "GET /api/v1/health": 2,
"POST /api/v1/inpaint": 24
}, },
"endpoint_stats": { "endpoint_stats": {
"GET /api/v1/health": { "GET /api/v1/health": {
"count": 2, "count": 2,
"avg_time": 0.0017114877700805664, "avg_time": 0.0014368295669555664,
"min_time": 0.0015041828155517578, "min_time": 0.0011837482452392578,
"max_time": 0.001918792724609375, "max_time": 0.001689910888671875,
"current_concurrent": 0
},
"POST /api/v1/inpaint": {
"count": 24,
"avg_time": 8.082906911770502,
"min_time": 2.5534043312072754,
"max_time": 25.198312282562256,
"current_concurrent": 0 "current_concurrent": 0
} }
}, },
"average_response_time": 0.0017114877700805664, "average_response_time": 7.461255366985615,
"min_response_time": 0.0015041828155517578, "min_response_time": 0.0011837482452392578,
"max_response_time": 0.001918792724609375, "max_response_time": 25.198312282562256,
"current_concurrent": 0, "current_concurrent": 0,
"max_concurrent": 1, "max_concurrent": 8,
"requests_per_second": 0.001614421372065027, "requests_per_second": 0.1387551056852149,
"uptime": 1238.8339467048645, "uptime": 187.38049221038818,
"recent_errors": [] "recent_errors": []
}, },
"timestamp": 1756480032.9159214 "timestamp": 1756486697.262446
} }

260
web/index.html Normal file
View File

@ -0,0 +1,260 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inpaint Server 모니터링 대시보드</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: auto; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.header h1 { color: #2c3e50; }
.status { padding: 8px 15px; border-radius: 5px; color: white; font-weight: bold; }
.status.ok { background-color: #27ae60; }
.status.error { background-color: #c0392b; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
.card { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
.card h3 { margin-top: 0; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px; color: #34495e; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.info-item { display: flex; justify-content: space-between; padding: 5px 0; }
.info-item span:first-child { font-weight: bold; color: #555; }
#gpuUsage { width: 100%; background-color: #ecf0f1; border-radius: 5px; overflow: hidden; }
#gpuUsage div { height: 20px; background-color: #3498db; text-align: center; color: white; line-height: 20px; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
th { background-color: #f2f2f2; }
.setting-item { display: flex; align-items: center; gap: 10px; margin-top: 15px; }
.setting-item input { flex-grow: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
.setting-item button { padding: 8px 12px; border: none; background-color: #007bff; color: white; border-radius: 4px; cursor: pointer; transition: background-color 0.3s; }
.setting-item button:hover { background-color: #0056b3; }
.status-message { margin-left: 10px; font-size: 0.9em; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎨 Inpaint Server 대시보드</h1>
<div id="healthStatus" class="status">확인 중...</div>
</div>
<div class="grid">
<div class="card">
<h3>시스템 정보</h3>
<div class="info-grid">
<div class="info-item"><span>OS:</span> <span id="os">N/A</span></div>
<div class="info-item"><span>Python:</span> <span id="pythonVersion">N/A</span></div>
<div class="info-item"><span>CPU 사용량:</span> <span id="cpuUsage">N/A</span></div>
<div class="info-item"><span>RAM 사용량:</span> <span id="ramUsage">N/A</span></div>
</div>
</div>
<div class="card">
<h3>GPU 정보</h3>
<div class="info-item"><span>GPU:</span> <span id="gpuName">N/A</span></div>
<div class="info-item"><span>드라이버:</span> <span id="gpuDriver">N/A</span></div>
<div class="info-item"><span>VRAM:</span> <span id="vramUsage">N/A</span></div>
<div class="info-item"><span>사용량:</span> <span id="gpuUtil">N/A</span></div>
<div id="gpuUsage">
<div id="gpuUsageBar" style="width: 0%;">0%</div>
</div>
</div>
<div class="card">
<h3>모델 로딩 통계 (ms)</h3>
<canvas id="modelLoadChart"></canvas>
</div>
<div class="card">
<h3>모델 처리 시간 (ms)</h3>
<canvas id="modelTimeChart"></canvas>
</div>
<div class="card">
<h3>API 응답 시간 (ms)</h3>
<canvas id="apiResponseChart"></canvas>
</div>
</div>
<div class="card">
<h3>설정</h3>
<div class="setting-item">
<label for="webhookUrl">Discord 웹훅 URL:</label>
<input type="text" id="webhookUrl" placeholder="Discord 웹훅 URL을 입력하세요">
<button onclick="saveWebhookUrl()">저장</button>
<p id="webhookStatus" class="status-message"></p>
</div>
</div>
</div>
<script>
const ws = new WebSocket(`ws://${window.location.host}/ws/monitoring`);
let modelLoadChart, apiResponseChart, modelTimeChart;
async function fetchHealthStatus() {
try {
const response = await fetch('/api/v1/health');
const data = await response.json();
const statusEl = document.getElementById('healthStatus');
if (response.ok) {
statusEl.textContent = `정상 (Uptime: ${new Date(data.uptime * 1000).toISOString().substr(11, 8)})`;
statusEl.className = 'status ok';
} else {
throw new Error(data.detail || 'Health check failed');
}
} catch (error) {
console.error('Health check error:', error);
const statusEl = document.getElementById('healthStatus');
statusEl.textContent = '오류';
statusEl.className = 'status error';
}
}
function createChart(ctx, type, data, options) {
return new Chart(ctx, { type, data, options });
}
function setupCharts() {
const commonOptions = {
responsive: true,
scales: { y: { beginAtZero: true } }
};
modelLoadChart = createChart(document.getElementById('modelLoadChart').getContext('2d'), 'bar', { labels: [], datasets: [{ label: 'Loading Time (ms)', data: [], backgroundColor: 'rgba(54, 162, 235, 0.6)' }] }, commonOptions);
apiResponseChart = createChart(document.getElementById('apiResponseChart').getContext('2d'), 'line', { labels: [], datasets: [{ label: 'Avg Response Time (ms)', data: [], borderColor: 'rgba(255, 99, 132, 1)', fill: false }] }, commonOptions);
modelTimeChart = createChart(document.getElementById('modelTimeChart').getContext('2d'), 'bar', { labels: [], datasets: [{ label: 'Avg Processing Time (ms)', data: [], backgroundColor: 'rgba(75, 192, 192, 0.6)' }] }, commonOptions);
}
function updateChart(chart, labels, data) {
chart.data.labels = labels;
chart.data.datasets[0].data = data;
chart.update();
}
async function fetchSystemInfo() {
// Placeholder for system info fetch
}
async function fetchGpuInfo() {
// Placeholder for GPU info fetch
}
async function fetchModelLoadStats() {
try {
const response = await fetch('/api/v1/stats/model-load');
const data = await response.json();
const labels = Object.keys(data);
const values = Object.values(data).map(d => d.avg_load_time_ms);
updateChart(modelLoadChart, labels, values);
} catch (error) {
console.error('Failed to fetch model load stats:', error);
}
}
async function fetchApiStats() {
try {
const response = await fetch('/api/v1/stats/api');
const data = await response.json();
const labels = Object.keys(data.endpoints);
const values = Object.values(data.endpoints).map(e => e.avg_response_time_ms);
updateChart(apiResponseChart, labels, values);
} catch (error) {
console.error('Failed to fetch API stats:', error);
}
}
async function fetchProcessingStats() {
try {
const response = await fetch('/api/v1/stats/processing');
const data = await response.json();
delete data.total; // Total is not needed for this chart
const labels = Object.keys(data);
const values = Object.values(data).map(m => m.avg_time);
updateChart(modelTimeChart, labels, values);
} catch (error) {
console.error('Failed to fetch processing stats:', error);
}
}
async function fetchInitialData() {
await fetchHealthStatus();
await fetchSystemInfo();
await fetchGpuInfo();
await fetchModelLoadStats();
await fetchApiStats();
await fetchProcessingStats();
await fetchWebhookUrl();
}
async function fetchWebhookUrl() {
try {
const response = await fetch('/api/v1/webhook');
if (!response.ok) throw new Error('Server response was not ok.');
const data = await response.json();
document.getElementById('webhookUrl').value = data.url || '';
} catch (error) {
console.error('Failed to load webhook URL:', error);
const statusEl = document.getElementById('webhookStatus');
statusEl.textContent = 'URL 로드 실패';
statusEl.style.color = 'red';
}
}
async function saveWebhookUrl() {
const url = document.getElementById('webhookUrl').value;
const statusEl = document.getElementById('webhookStatus');
statusEl.textContent = '저장 중...';
statusEl.style.color = 'orange';
try {
const response = await fetch('/api/v1/webhook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const data = await response.json();
if (response.ok) {
statusEl.textContent = '성공적으로 저장되었습니다!';
statusEl.style.color = 'green';
} else {
statusEl.textContent = '저장 실패: ' + (data.detail || 'Server error');
statusEl.style.color = 'red';
}
} catch (error) {
console.error('Failed to save webhook URL:', error);
statusEl.textContent = '저장 요청 실패.';
statusEl.style.color = 'red';
}
setTimeout(() => { statusEl.textContent = ''; }, 3000);
}
ws.onopen = function() {
console.log("WebSocket connection established");
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
document.getElementById('os').textContent = data.system.os;
document.getElementById('pythonVersion').textContent = data.system.python_version;
document.getElementById('cpuUsage').textContent = `${data.system.cpu_percent.toFixed(1)}%`;
document.getElementById('ramUsage').textContent = `${(data.system.ram.used / 1024**3).toFixed(2)} / ${(data.system.ram.total / 1024**3).toFixed(2)} GB (${data.system.ram.percent.toFixed(1)}%)`;
if (data.gpu) {
document.getElementById('gpuName').textContent = data.gpu.name;
document.getElementById('gpuDriver').textContent = data.gpu.driver_version;
document.getElementById('vramUsage').textContent = `${(data.gpu.memory.used / 1024).toFixed(2)} / ${(data.gpu.memory.total / 1024).toFixed(2)} GB (${data.gpu.memory.percent.toFixed(1)}%)`;
document.getElementById('gpuUtil').textContent = `${data.gpu.utilization.toFixed(1)}%`;
const gpuUsageBar = document.getElementById('gpuUsageBar');
gpuUsageBar.style.width = `${data.gpu.utilization}%`;
gpuUsageBar.textContent = `${data.gpu.utilization.toFixed(1)}%`;
}
};
ws.onclose = function() {
console.log("WebSocket connection closed");
document.getElementById('healthStatus').textContent = '연결 끊김';
document.getElementById('healthStatus').className = 'status error';
};
window.onload = () => {
setupCharts();
fetchInitialData();
setInterval(fetchInitialData, 5000); // Poll REST endpoints periodically as well
};
</script>
</body>
</html>

0
web/index.html.bak Normal file
View File