Discord 웹훅 URL을 설정하고 조회하는 API 엔드포인트를 추가하였습니다. 마이크로 배치 처리를 위한 설정을 추가하고, 인페인팅 및 배경 제거 작업에서 배치 처리를 지원하도록 로직을 개선하였습니다. 서버 시작 및 종료 시 Discord 알림을 전송하도록 수정하였으며, 모델 처리 시간 통계를 기록하는 방식을 개선하였습니다.
This commit is contained in:
parent
c18667c17d
commit
1617ec95ec
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""현재까지의 통계를 계산하여 반환합니다."""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
1475
logs/main.log
1475
logs/main.log
File diff suppressed because it is too large
Load Diff
|
|
@ -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']
|
||||
[0;93m2025-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.[m
|
||||
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']
|
||||
[0;93m2025-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.[m
|
||||
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']
|
||||
[0;93m2025-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.[m
|
||||
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']
|
||||
[0;93m2025-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.[m
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
56165
|
||||
77869
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
56411
|
||||
78184
|
||||
|
|
|
|||
34
main.py
34
main.py
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
54
status.json
54
status.json
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue