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
from datetime import datetime
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
import cv2
@ -26,12 +26,43 @@ import base64
import io
from ..monitoring.dashboard import monitoring_data
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__)
router = APIRouter()
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):
"""이미지를 지정된 형식으로 인코딩"""
@ -104,16 +135,17 @@ def create_response(
@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())
uptime = time.time() - start_time
start_time = getattr(request.app.state, 'start_time', time.time())
current_time = time.time()
uptime_seconds = current_time - start_time
return HealthResponse(
status="healthy",
timestamp=datetime.now().isoformat(),
version="1.0.0",
uptime=uptime
status="ok",
uptime=uptime_seconds,
timestamp=datetime.fromtimestamp(current_time).isoformat(),
version="1.0.0"
)
@ -199,17 +231,32 @@ async def inpaint_image(
model_name = request.model_name or "simple-lama"
# 워커에서 인페인팅 실행
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 settings.USE_MICRO_BATCHING and model_name == "simple-lama":
# SimpleLama는 배치 관리자를 통해 처리
job_data = {
"image": image,
"mask": mask,
"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,
}
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:
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(

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
from typing import Dict, Any, Optional, ClassVar
from pydantic_settings import BaseSettings
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
class Settings(BaseSettings):
@ -30,6 +34,11 @@ class Settings(BaseSettings):
# 유휴 세션 자동 제거 시간 (초). 0이면 비활성화.
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_NAME: str = "Inpaint & RemoveBG Server"
@ -108,3 +117,15 @@ class Settings(BaseSettings):
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,
}
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:
# 개별 모델 통계 업데이트
if model_name in self.data:
stats = self.data[model_name]
stats['count'] += 1
stats['total_time'] += duration
stats['min_time'] = min(stats['min_time'], duration)
stats['max_time'] = max(stats['max_time'], duration)
# 모델별 통계 업데이트
self.data[model_name]['count'] += count
self.data[model_name]['total_time'] += duration * count # 전체 시간에 (평균시간 * 개수) 를 더함
self.data[model_name]['min_time'] = min(self.data[model_name]['min_time'], duration)
self.data[model_name]['max_time'] = max(self.data[model_name]['max_time'], duration)
# 전체 통계 업데이트
total_stats = self.data['total']
total_stats['count'] += 1
total_stats['total_time'] += duration
total_stats['min_time'] = min(total_stats['min_time'], duration)
total_stats['max_time'] = max(total_stats['max_time'], duration)
self.data['total']['count'] += count
self.data['total']['total_time'] += duration * count
self.data['total']['min_time'] = min(self.data['total']['min_time'], duration)
self.data['total']['max_time'] = max(self.data['total']['max_time'], duration)
def get_stats(self) -> dict:
"""현재까지의 통계를 계산하여 반환합니다."""

View File

@ -329,39 +329,62 @@ class WorkerManager:
}
async def process_inpaint(self, **kwargs) -> Optional[np.ndarray]:
"""인페인팅 작업을 처리합니다."""
try:
# 배치 처리를 사용하지 않는 모델 (예: Migan)을 위한 메서드
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
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:
start_time = time.time()
# session.model 에서 실제 모델 객체의 메서드를 호출해야 함
# Migan은 단일 이미지만 처리하므로 기존 로직 유지
result = await session.model.inpaint(
image=kwargs['image'],
mask=kwargs['mask']
image=kwargs["image"],
mask=kwargs["mask"],
)
duration = time.time() - start_time
stats_manager.record_time(stats_model_key, duration)
logger.info(f"'{model_name}' inpainting processed in {duration:.3f}s")
return result
# _execute_task 대신 직접 실행
return await _inpaint()
async def process_inpaint_batch(self, batch_data: List[Dict[str, Any]]) -> List[np.ndarray]:
"""SimpleLama 배치 인페인팅 작업을 처리합니다."""
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()
return result
images = [item['image'] for item in batch_data]
masks = [item['mask'] for item in batch_data]
except Exception as e:
logger.error(f"인페인팅 처리 실패: {e}", exc_info=True)
return None
# 수정된 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]]:
"""배경 제거 작업을 처리합니다."""
try:

View File

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

View File

@ -5,18 +5,35 @@ import logging
import requests
import socket
from datetime import datetime
from pathlib import Path
from ..core.config import settings
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"):
"""
Discord 웹훅으로 알림을 보냅니다.
"""
webhook_url = settings.DISCORD_WEBHOOK_URL
"""Discord 웹훅으로 알림을 보냅니다."""
webhook_url = get_webhook_url()
if not webhook_url:
logger.warning("Discord 웹훅 URL이 설정되지 않아 알림을 보낼 수 없습니다.")
if "시작" in message or "종료" in message:
logger.warning("Discord 웹훅 URL이 설정되지 않아 알림을 보낼 수 없습니다.")
return
hostname = socket.gethostname()

File diff suppressed because it is too large Load Diff

View File

@ -1,71 +1,127 @@
INFO: Started server process [56165]
2025-08-29 23:46:34,096 - uvicorn.error - INFO - Started server process [56165]
INFO: Started server process [77869]
2025-08-30 01:55:09,899 - uvicorn.error - INFO - Started server process [77869]
INFO: Waiting for application startup.
2025-08-29 23:46:34,097 - uvicorn.error - INFO - Waiting for application startup.
2025-08-29 23:46:34,098 - main - INFO - 🚀 인페인팅 서버 시작 중...
2025-08-29 23:46:34,098 - main - INFO - ✅ 공유 객체를 app.state에 저장 완료
2025-08-29 23:46:34,098 - main - INFO - 🔄 상태 저장 백그라운드 작업 생성 중...
2025-08-29 23:46:34,099 - main - INFO - ✅ 상태 저장 백그라운드 작업 생성 완료
2025-08-29 23:46:34,099 - main - INFO - 🚀 세션 풀 초기화 (CUDA 자동 감지)
2025-08-29 23:46:34,100 - 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-29 23:46:34,100 - 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-29 23:46:37,259 - app.models.simple_lama - INFO - Loading Simple LAMA model...
2025-08-29 23:46:41,562 - 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-29 23:46:41,564 - 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-29 23:46:41,566 - app.models.simple_lama - INFO - Loading Simple LAMA model...
2025-08-29 23:46:43,429 - 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-29 23:46:43,429 - 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-29 23:46:43,432 - 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-29 23:46:43,491 - app.models.migan - INFO - MIGAN ONNX 런타임 세션 생성 시도...
2025-08-29 23:46:43,492 - 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-29 23:46:46,412 - 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-29 23:46:46,413 - 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-29 23:46:46,414 - app.models.migan - INFO - Loading MIGAN ONNX model...
2025-08-29 23:46:46,415 - app.models.migan - INFO - MIGAN ONNX 런타임 세션 생성 시도...
2025-08-29 23:46:46,415 - 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-29 23:46:47,630 - 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-29 23:46:47,632 - 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-29 23:46:47,634 - 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-29 23:46:47,638 - 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-29 23:46:49,612 - 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-29 23:46:49,613 - 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-29 23:47:02,614 - 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-29 23:47:02,615 - 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-29 23:47:02,617 - 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-29 23:47:15,228 - 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-29 23:47:15,230 - 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-29 23:47:15,232 - app.core.session_pool - INFO - Session pools initialized successfully
2025-08-29 23:47:15,232 - main - INFO - ✅ 세션 풀 초기화 완료
2025-08-29 23:47:15,233 - 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-29 23:47:15,234 - main - INFO - ✅ 워커 매니저 시작 완료
2025-08-29 23:47:15,234 - main - INFO - 🎉 인페인팅 서버 시작 완료!
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:09,900 - uvicorn.error - INFO - Waiting for application startup.
2025-08-30 01:55:09,901 - main - INFO - 🚀 인페인팅 서버 시작 중...
2025-08-30 01:55:09,901 - main - INFO - ✅ 공유 객체를 app.state에 저장 완료
2025-08-30 01:55:09,902 - main - INFO - 🔄 상태 저장 백그라운드 작업 생성 중...
2025-08-30 01:55:09,902 - main - INFO - ✅ 상태 저장 백그라운드 작업 생성 완료
2025-08-30 01:55:09,902 - main - INFO - 🚀 세션 풀 초기화 (CUDA 자동 감지)
2025-08-30 01:55:09,902 - app.core.session_pool - INFO - Initializing dynamic session pools...
2025-08-30 01:55:09,903 - app.core.session_pool - INFO - Pre-loading 2 sessions for simple_lama
2025-08-30 01:55:09,903 - main - INFO - 🔄 상태 저장 백그라운드 작업 시작됨
2025-08-30 01:55:09,905 - app.core.session_pool - INFO - Creating new session simple_lama_0 for simple_lama...
2025-08-30 01:55:13,106 - app.models.simple_lama - INFO - Loading Simple LAMA model...
2025-08-30 01:55:17,237 - app.models.simple_lama - INFO - 실제 SimpleLama 모델 로딩 완료
2025-08-30 01:55:17,239 - app.models.simple_lama - INFO - Simple LAMA model loaded successfully
2025-08-30 01:55:17,240 - app.core.session_pool - INFO - Successfully created session simple_lama_0
2025-08-30 01:55:17,241 - app.core.session_pool - INFO - Creating new session simple_lama_1 for simple_lama...
2025-08-30 01:55:17,241 - app.models.simple_lama - INFO - Loading Simple LAMA model...
2025-08-30 01:55:18,996 - app.models.simple_lama - INFO - 실제 SimpleLama 모델 로딩 완료
2025-08-30 01:55:18,997 - app.models.simple_lama - INFO - Simple LAMA model loaded successfully
2025-08-30 01:55:18,997 - app.core.session_pool - INFO - Successfully created session simple_lama_1
2025-08-30 01:55:18,998 - app.core.session_pool - INFO - Pre-loading 2 sessions for migan
2025-08-30 01:55:18,999 - app.core.session_pool - INFO - Creating new session migan_0 for migan...
2025-08-30 01:55:19,063 - app.models.migan - INFO - Loading MIGAN ONNX model...
2025-08-30 01:55:19,064 - app.models.migan - INFO - MIGAN ONNX 런타임 세션 생성 시도...
2025-08-30 01:55:19,064 - app.models.migan - INFO - MIGAN ONNX providers 설정: ['CUDAExecutionProvider', 'CPUExecutionProvider']
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-30 01:55:21,872 - app.models.migan - INFO - MIGAN ONNX 세션 생성 완료. Providers: ['CUDAExecutionProvider', 'CPUExecutionProvider']
2025-08-30 01:55:21,873 - app.models.migan - INFO - MIGAN ONNX model loaded successfully
2025-08-30 01:55:21,874 - app.core.session_pool - INFO - Successfully created session migan_0
2025-08-30 01:55:21,874 - app.core.session_pool - INFO - Creating new session migan_1 for migan...
2025-08-30 01:55:21,875 - app.models.migan - INFO - Loading MIGAN ONNX model...
2025-08-30 01:55:21,875 - app.models.migan - INFO - MIGAN ONNX 런타임 세션 생성 시도...
2025-08-30 01:55:21,875 - app.models.migan - INFO - MIGAN ONNX providers 설정: ['CUDAExecutionProvider', 'CPUExecutionProvider']
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-30 01:55:23,132 - app.models.migan - INFO - MIGAN ONNX 세션 생성 완료. Providers: ['CUDAExecutionProvider', 'CPUExecutionProvider']
2025-08-30 01:55:23,133 - app.models.migan - INFO - MIGAN ONNX model loaded successfully
2025-08-30 01:55:23,134 - app.core.session_pool - INFO - Successfully created session migan_1
2025-08-30 01:55:23,134 - app.core.session_pool - INFO - Pre-loading 2 sessions for rembg
2025-08-30 01:55:23,136 - app.core.session_pool - INFO - Creating new session rembg_0 for rembg...
2025-08-30 01:55:23,137 - app.core.session_pool - INFO - Creating new session rembg_1 for rembg...
2025-08-30 01:55:23,139 - app.models.rembg_model - INFO - Loading REMBG model (birefnet-general-lite)...
2025-08-30 01:55:25,141 - app.models.rembg_model - INFO - rembg 모듈 임포트 성공 (세션 생성은 지연 로딩)
2025-08-30 01:55:25,141 - app.models.rembg_model - INFO - 🔧 rembg 새 세션 생성 필요: birefnet-general-lite_cuda_True
2025-08-30 01:55:25,142 - app.models.rembg_model - WARNING - rembg.sessions import 실패, 기본 방식 사용
2025-08-30 01:55:25,142 - app.models.rembg_model - INFO - rembg 세션 생성 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-30 01:55:38,145 - app.models.rembg_model - INFO - REMBG model (birefnet-general-lite) loaded successfully
2025-08-30 01:55:38,146 - app.models.rembg_model - INFO - Loading REMBG model (birefnet-general-lite)...
2025-08-30 01:55:38,146 - app.models.rembg_model - INFO - rembg 모듈 임포트 성공 (세션 생성은 지연 로딩)
2025-08-30 01:55:38,147 - app.models.rembg_model - INFO - 🔧 rembg 새 세션 생성 필요: birefnet-general-lite_cuda_True
2025-08-30 01:55:38,148 - app.models.rembg_model - WARNING - rembg.sessions import 실패, 기본 방식 사용
2025-08-30 01:55:38,148 - app.models.rembg_model - INFO - rembg 세션 생성 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-30 01:55:50,693 - app.models.rembg_model - INFO - REMBG model (birefnet-general-lite) loaded successfully
2025-08-30 01:55:50,694 - 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_1
2025-08-30 01:55:50,696 - app.core.session_pool - INFO - Session pools initialized successfully
2025-08-30 01:55:50,697 - main - INFO - ✅ 세션 풀 초기화 완료
2025-08-30 01:55:50,697 - app.core.worker_manager - INFO - Starting worker manager...
2025-08-30 01:55:50,698 - app.core.worker_manager - INFO - Worker manager started with 10 workers
2025-08-30 01:55:50,698 - main - INFO - ✅ 워커 매니저 시작 완료
2025-08-30 01:55:50,698 - app.core.batch_manager - INFO - Starting BatchManager...
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.
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)
2025-08-29 23:47:15,236 - 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:57060 - "GET /api/v1/health HTTP/1.1" 200 OK
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:52600 - "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.
Fan control not available
INFO: Application startup complete.
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
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):

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.api.endpoints import router
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 os
import logging.config
# 로그 디렉토리 생성
log_dir = "logs"
@ -202,8 +206,9 @@ async def save_status_periodically():
@asynccontextmanager
async def lifespan(app: FastAPI):
"""애플리케이션 생명주기 관리"""
# 시작 시
# 서버 시작 시
logger.info("🚀 인페인팅 서버 시작 중...")
app.state.start_time = time.time() # settings 대신 app.state에 저장
# app.state에 공유 객체 저장
app.state.worker_manager = worker_manager
@ -226,7 +231,15 @@ async def lifespan(app: FastAPI):
await worker_manager.start()
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("🎉 인페인팅 서버 시작 완료!")
send_discord_notification("✅ 서버가 성공적으로 시작되었습니다.", level="success")
except Exception as e:
logger.error(f"❌ 서버 시작 실패: {e}")
@ -234,7 +247,7 @@ async def lifespan(app: FastAPI):
yield
# 종료 시
# 서버 종료 시
logger.info("🛑 인페인팅 서버 종료 중...")
# 상태 저장 백그라운드 작업 취소
@ -245,7 +258,19 @@ async def lifespan(app: FastAPI):
await worker_manager.stop()
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("👋 인페인팅 서버 종료 완료")
send_discord_notification("👋 서버가 종료되었습니다.", level="info")
except Exception as e:
logger.error(f"❌ 서버 종료 중 오류: {e}")
@ -253,9 +278,8 @@ async def lifespan(app: FastAPI):
# 메인 애플리케이션 생성
app = FastAPI(
title="인페인팅 서버",
description="Simple LAMA, MIGAN, REMBG를 활용한 병렬 처리 인페인팅 서버 (iopaint 호환)",
version="1.0.0",
title=settings.APP_NAME,
version=settings.APP_VERSION,
lifespan=lifespan
)

View File

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