first commit
|
|
@ -0,0 +1,57 @@
|
|||
# 인페인팅 서버 환경 설정 예시
|
||||
# 실제 사용 시 .env 파일로 복사하여 사용하세요
|
||||
|
||||
# 시스템 감지 (자동 설정)
|
||||
# IS_JETSON=true # Jetson Xavier 감지 시 자동 설정
|
||||
# IS_X86=true # x86 시스템 감지 시 자동 설정
|
||||
|
||||
# 서버 설정
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
WORKERS=1
|
||||
|
||||
# GPU 설정
|
||||
CUDA_DEVICE=0
|
||||
FP16_ENABLED=true
|
||||
|
||||
# Jetson 전용 설정
|
||||
JETSON_MODE=false # 자동 감지됨
|
||||
JETSON_POWER_MODE=MAXN # MAXN, 5W, 10W, 15W
|
||||
JETSON_FAN_CONTROL=true
|
||||
JETSON_TEMP_THRESHOLD=75 # Celsius
|
||||
JETSON_GPU_FREQ=1200 # MHz
|
||||
JETSON_CPU_FREQ=1900 # MHz
|
||||
JETSON_MEMORY_FREQ=1600 # MHz
|
||||
|
||||
# 세션 풀 설정 (시스템별 자동 조정)
|
||||
SIMPLE_LAMA_SESSIONS=2
|
||||
MIGAN_SESSIONS=2
|
||||
REMBG_SESSIONS=1
|
||||
|
||||
# 워커 설정 (Jetson은 더 적은 워커 사용)
|
||||
MAX_WORKERS=4 # Jetson: 4, x86: 8
|
||||
MIN_WORKERS=1 # Jetson: 1, x86: 2
|
||||
WORKER_TIMEOUT=300
|
||||
|
||||
# VRAM 관리 (Jetson은 더 보수적인 설정)
|
||||
VRAM_THRESHOLD_HIGH=0.7 # Jetson: 70%, x86: 80%
|
||||
VRAM_THRESHOLD_LOW=0.3 # Jetson: 30%, x86: 40%
|
||||
VRAM_CHECK_INTERVAL=20 # Jetson: 20초, x86: 30초
|
||||
|
||||
# 모델 경로
|
||||
SIMPLE_LAMA_MODEL_PATH=models/simple-lama
|
||||
MIGAN_MODEL_PATH=models/migan
|
||||
REMBG_MODEL_PATH=models/rembg
|
||||
|
||||
# 업로드 설정 (Jetson은 더 작은 파일 크기)
|
||||
MAX_FILE_SIZE=26214400 # Jetson: 25MB, x86: 50MB
|
||||
ALLOWED_EXTENSIONS=.jpg,.jpeg,.png,.bmp,.tiff
|
||||
|
||||
# 모니터링
|
||||
ENABLE_MONITORING=true
|
||||
MONITORING_PORT=8001
|
||||
|
||||
# Jetson 최적화 설정
|
||||
JETSON_OPTIMIZE_ON_STARTUP=true
|
||||
JETSON_AUTO_FAN_CONTROL=true
|
||||
JETSON_POWER_SAVING=false
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
bin/
|
||||
include/
|
||||
lib/
|
||||
lib64/
|
||||
share/
|
||||
local/
|
||||
build/
|
||||
.vscode/
|
||||
.venv/
|
||||
pyvenv.cfg
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.pyw
|
||||
*.pyz
|
||||
*.pywz
|
||||
*.pyzw
|
||||
*.pyzwz
|
||||
*.pyzwzw
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,384 @@
|
|||
# 🖼️ 고성능 인페인팅 서버
|
||||
|
||||
FastAPI와 딥러닝을 활용한 병렬 처리 인페인팅 서버입니다. Simple LAMA, MIGAN, REMBG 모델을 TensorRT와 CUDA를 활용하여 FP16 방식으로 최적화된 서버를 제공합니다.
|
||||
|
||||
**🚀 Jetson Xavier (ARM64) 및 x86_64 시스템을 모두 지원합니다!**
|
||||
|
||||
## ✨ 주요 기능
|
||||
|
||||
- **🚀 고성능 병렬 처리**: 동적 워커 관리로 최적의 성능 제공
|
||||
- **🎯 다중 모델 지원**: Simple LAMA, MIGAN, REMBG 인페인팅 모델
|
||||
- **⚡ GPU 최적화**: TensorRT와 CUDA FP16을 활용한 빠른 추론
|
||||
- **📊 실시간 모니터링**: 웹 기반 대시보드로 서버 상태 실시간 확인
|
||||
- **🔧 동적 스케일링**: VRAM 사용량에 따른 자동 워커 조정
|
||||
- **🛡️ 안정성**: 세션 풀과 에러 복구 메커니즘
|
||||
- **🚁 Jetson 최적화**: ARM64 아키텍처 전용 성능 튜닝
|
||||
|
||||
## 🏗️ 아키텍처
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ FastAPI │ │ 워커 매니저 │ │ 세션 풀 │
|
||||
│ 엔드포인트 │◄──►│ (동적 스케일링) │◄──►│ (모델 관리) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ 모니터링 │ │ GPU 모니터 │ │ 모델 인스턴스 │
|
||||
│ 대시보드 │ │ (VRAM 추적) │ │ (Simple LAMA, │
|
||||
└─────────────────┘ └─────────────────┘ │ MIGAN, REMBG) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## 📋 요구사항
|
||||
|
||||
### 시스템 요구사항
|
||||
|
||||
#### Jetson Xavier (ARM64)
|
||||
- Ubuntu 18.04 이상
|
||||
- Python 3.8 이상
|
||||
- CUDA 11.8
|
||||
- cuDNN 8
|
||||
- TensorRT 8.6
|
||||
- 4GB 이상 RAM 권장
|
||||
- 10GB 이상 저장공간
|
||||
|
||||
#### x86_64 시스템
|
||||
- Ubuntu 18.04 이상
|
||||
- Python 3.8 이상
|
||||
- NVIDIA GPU (GTX 1060 이상 권장)
|
||||
- CUDA 11.8 이상
|
||||
- 8GB 이상 RAM 권장
|
||||
- 10GB 이상 저장공간
|
||||
|
||||
### GPU 요구사항
|
||||
- **Jetson Xavier**: 내장 Volta GPU (8GB VRAM)
|
||||
- **x86**: NVIDIA GPU (4GB 이상 VRAM)
|
||||
|
||||
## 🚀 설치 및 실행
|
||||
|
||||
### 1. 프로젝트 클론 및 가상환경 설정
|
||||
|
||||
프로젝트가 이미 `/home/ckh08045/work/inpaintServer`에 설정되어 있습니다.
|
||||
|
||||
### 2. 의존성 설치
|
||||
|
||||
#### Jetson Xavier 전용 설치
|
||||
```bash
|
||||
# Jetson 최적화와 함께 설치
|
||||
./scripts/install_deps.sh --jetson-optimize
|
||||
|
||||
# 또는 기본 설치
|
||||
./scripts/install_deps.sh
|
||||
```
|
||||
|
||||
#### x86 시스템 설치
|
||||
```bash
|
||||
# 기본 설치
|
||||
./scripts/install_deps.sh
|
||||
|
||||
# 개발 도구 포함
|
||||
./scripts/install_deps.sh --extras
|
||||
```
|
||||
|
||||
#### 수동 설치
|
||||
```bash
|
||||
source bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. 환경 설정
|
||||
|
||||
```bash
|
||||
# 환경 설정 파일 복사 및 수정
|
||||
cp .env.example .env
|
||||
|
||||
# Jetson Xavier의 경우 자동으로 최적화된 설정이 적용됩니다
|
||||
# 필요한 경우 .env 파일에서 세부 설정 조정
|
||||
```
|
||||
|
||||
### 4. 서버 시작
|
||||
|
||||
#### Jetson Xavier 최적화 모드
|
||||
```bash
|
||||
# Jetson 최적화와 함께 시작
|
||||
./scripts/start_server.sh --jetson-optimize
|
||||
|
||||
# 또는 기본 시작
|
||||
./scripts/start_server.sh
|
||||
```
|
||||
|
||||
#### x86 시스템
|
||||
```bash
|
||||
# 기본 모드로 시작
|
||||
./scripts/start_server.sh
|
||||
|
||||
# 프로덕션 모드로 시작
|
||||
./scripts/start_server.sh --production
|
||||
|
||||
# 워커 수 지정
|
||||
./scripts/start_server.sh --workers 4
|
||||
|
||||
# GPU 디바이스 지정
|
||||
./scripts/start_server.sh --gpu 0
|
||||
```
|
||||
|
||||
### 5. 서버 상태 확인
|
||||
|
||||
```bash
|
||||
# 상태 확인
|
||||
./scripts/status.sh
|
||||
|
||||
# 상세 정보
|
||||
./scripts/status.sh --detailed
|
||||
|
||||
# 실시간 모니터링
|
||||
./scripts/status.sh --watch
|
||||
```
|
||||
|
||||
### 6. 서버 중지
|
||||
|
||||
```bash
|
||||
# 정상 종료
|
||||
./scripts/stop_server.sh
|
||||
|
||||
# 강제 종료
|
||||
./scripts/stop_server.sh --force
|
||||
```
|
||||
|
||||
## 🚁 Jetson Xavier 전용 기능
|
||||
|
||||
### 자동 시스템 감지
|
||||
- ARM64 아키텍처 자동 감지
|
||||
- Tegra 커널 자동 인식
|
||||
- Jetson 전용 설정 자동 적용
|
||||
|
||||
### 성능 최적화
|
||||
- **전력 모드**: MAXN 모드로 최고 성능
|
||||
- **GPU 클럭**: 1200MHz로 최적화
|
||||
- **메모리 클럭**: 1600MHz로 최적화
|
||||
- **팬 제어**: 온도 기반 자동 조정
|
||||
|
||||
### 메모리 관리
|
||||
- **VRAM 임계값**: 70%/30% (x86 대비 보수적)
|
||||
- **파일 크기 제한**: 25MB (x86: 50MB)
|
||||
- **워커 수**: 최대 4개 (x86: 최대 8개)
|
||||
|
||||
### 모니터링 도구
|
||||
- **jtop**: Jetson 전용 시스템 모니터링
|
||||
- **nvpmodel**: 전력 모드 관리
|
||||
- **온도 센서**: 실시간 온도 모니터링
|
||||
|
||||
## 📡 API 엔드포인트
|
||||
|
||||
### 인페인팅 API
|
||||
|
||||
#### Simple LAMA 인페인팅
|
||||
```http
|
||||
POST /inpaint/simple-lama
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
- image: 원본 이미지 파일
|
||||
- mask: 마스크 이미지 파일 (흰색 영역이 제거될 부분)
|
||||
- format: 출력 형식 (PNG/JPEG, 기본값: PNG)
|
||||
```
|
||||
|
||||
#### MIGAN 인페인팅
|
||||
```http
|
||||
POST /inpaint/migan
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
- image: 원본 이미지 파일
|
||||
- mask: 마스크 이미지 파일
|
||||
- format: 출력 형식 (PNG/JPEG, 기본값: PNG)
|
||||
```
|
||||
|
||||
### 배경 제거 API
|
||||
|
||||
```http
|
||||
POST /remove-background
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
- image: 원본 이미지 파일
|
||||
- model_name: REMBG 모델명 (u2net/u2netp/silueta, 기본값: u2net)
|
||||
- return_mask: 마스크 반환 여부 (기본값: false)
|
||||
- background_color: 새 배경색 (hex 형식, 예: #ffffff)
|
||||
- format: 출력 형식 (PNG/JPEG, 기본값: PNG)
|
||||
```
|
||||
|
||||
### 관리 API
|
||||
|
||||
```http
|
||||
GET /health # 헬스 체크
|
||||
GET /status # 서버 상태
|
||||
GET / # API 정보
|
||||
GET /docs # Swagger UI
|
||||
POST /scale-sessions # 세션 풀 크기 조정
|
||||
```
|
||||
|
||||
## 📊 모니터링
|
||||
|
||||
### 웹 대시보드
|
||||
서버 시작 후 `http://localhost:8001`에서 실시간 모니터링 대시보드에 접근할 수 있습니다.
|
||||
|
||||
### 모니터링 항목
|
||||
- **서버 상태**: 메인 서버, 모니터링 서버 상태
|
||||
- **GPU 정보**: VRAM 사용량, GPU 사용률
|
||||
- **시스템 리소스**: CPU, 메모리, 디스크 사용량
|
||||
- **워커 상태**: 활성 워커 수, 작업 큐 상태
|
||||
- **세션 풀**: 각 모델별 세션 사용 현황
|
||||
|
||||
### Jetson 전용 모니터링
|
||||
- **온도 정보**: 각 thermal zone별 온도
|
||||
- **클럭 주파수**: GPU, CPU, 메모리 클럭
|
||||
- **전력 소비**: 실시간 전력 사용량
|
||||
- **팬 속도**: PWM 팬 제어 상태
|
||||
|
||||
## ⚙️ 설정
|
||||
|
||||
### 세션 풀 비율
|
||||
기본 설정: Simple LAMA:MIGAN:REMBG = 2:2:1
|
||||
|
||||
```python
|
||||
# .env 파일에서 조정 가능
|
||||
SIMPLE_LAMA_SESSIONS=2
|
||||
MIGAN_SESSIONS=2
|
||||
REMBG_SESSIONS=1
|
||||
```
|
||||
|
||||
### 동적 스케일링
|
||||
VRAM 사용량에 따른 자동 워커 조정:
|
||||
|
||||
#### Jetson Xavier
|
||||
```python
|
||||
VRAM_THRESHOLD_HIGH=0.7 # 70% 이상시 스케일 다운
|
||||
VRAM_THRESHOLD_LOW=0.3 # 30% 이하시 스케일 업
|
||||
VRAM_CHECK_INTERVAL=20 # 20초마다 체크
|
||||
```
|
||||
|
||||
#### x86 시스템
|
||||
```python
|
||||
VRAM_THRESHOLD_HIGH=0.8 # 80% 이상시 스케일 다운
|
||||
VRAM_THRESHOLD_LOW=0.4 # 40% 이하시 스케일 업
|
||||
VRAM_CHECK_INTERVAL=30 # 30초마다 체크
|
||||
```
|
||||
|
||||
## 🔧 개발 모드
|
||||
|
||||
```bash
|
||||
# 개발 모드로 실행 (자동 리로드)
|
||||
python main.py --dev
|
||||
|
||||
# 또는
|
||||
uvicorn app.api.endpoints:app --reload
|
||||
```
|
||||
|
||||
## 📝 로그
|
||||
|
||||
로그 파일들은 `logs/` 디렉토리에 저장됩니다:
|
||||
|
||||
- `main_server.log`: 메인 서버 로그
|
||||
- `monitoring.log`: 모니터링 서버 로그
|
||||
- `error.log`: 에러 로그
|
||||
- `access.log`: 액세스 로그
|
||||
|
||||
## 🛠️ 문제해결
|
||||
|
||||
### 일반적인 문제
|
||||
|
||||
1. **CUDA 관련 오류**
|
||||
```bash
|
||||
# CUDA 설치 확인
|
||||
nvidia-smi
|
||||
nvcc --version
|
||||
|
||||
# PyTorch CUDA 호환성 확인
|
||||
python -c "import torch; print(torch.cuda.is_available())"
|
||||
```
|
||||
|
||||
2. **메모리 부족**
|
||||
```bash
|
||||
# VRAM 사용량 확인
|
||||
nvidia-smi
|
||||
|
||||
# 세션 수 줄이기
|
||||
./scripts/start_server.sh --workers 1
|
||||
```
|
||||
|
||||
3. **포트 충돌**
|
||||
```bash
|
||||
# 포트 사용 확인
|
||||
lsof -i :8000
|
||||
lsof -i :8001
|
||||
|
||||
# 다른 포트 사용
|
||||
python main.py --port 8080
|
||||
```
|
||||
|
||||
### Jetson 전용 문제
|
||||
|
||||
1. **전력 모드 문제**
|
||||
```bash
|
||||
# 전력 모드 확인
|
||||
nvpmodel -q
|
||||
|
||||
# MAXN 모드로 설정
|
||||
sudo nvpmodel -m 0
|
||||
```
|
||||
|
||||
2. **온도 문제**
|
||||
```bash
|
||||
# 온도 확인
|
||||
cat /sys/devices/virtual/thermal/thermal_zone*/temp
|
||||
|
||||
# 팬 속도 조정
|
||||
echo 255 | sudo tee /sys/devices/pwm-fan/target_pwm
|
||||
```
|
||||
|
||||
3. **메모리 클럭 문제**
|
||||
```bash
|
||||
# 메모리 클럭 확인
|
||||
cat /sys/kernel/debug/clk/emc/clk_rate
|
||||
|
||||
# 클럭 재설정
|
||||
echo 1600000000 | sudo tee /sys/kernel/debug/clk/emc/clk_rate
|
||||
```
|
||||
|
||||
### 로그 확인
|
||||
|
||||
```bash
|
||||
# 실시간 로그 확인
|
||||
tail -f logs/main_server.log
|
||||
|
||||
# 에러 로그 확인
|
||||
tail -f logs/error.log
|
||||
```
|
||||
|
||||
## 🤝 기여
|
||||
|
||||
1. Fork the Project
|
||||
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push to the Branch (`git push origin feature/AmazingFeature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## 📄 라이선스
|
||||
|
||||
이 프로젝트는 MIT 라이선스 하에 배포됩니다. 자세한 내용은 `LICENSE` 파일을 참조하세요.
|
||||
|
||||
## 🙏 감사의 말
|
||||
|
||||
- [FastAPI](https://fastapi.tiangolo.com/) - 현대적인 웹 프레임워크
|
||||
- [PyTorch](https://pytorch.org/) - 딥러닝 프레임워크
|
||||
- [Simple LAMA](https://github.com/advimman/lama) - 인페인팅 모델
|
||||
- [REMBG](https://github.com/danielgatis/rembg) - 배경 제거 도구
|
||||
- [TensorRT](https://developer.nvidia.com/tensorrt) - GPU 추론 최적화
|
||||
- [NVIDIA Jetson](https://developer.nvidia.com/embedded-computing) - 엣지 AI 플랫폼
|
||||
|
||||
---
|
||||
|
||||
**💡 팁**:
|
||||
- **Jetson Xavier**: `--jetson-optimize` 옵션으로 최고 성능을 얻으세요!
|
||||
- **x86 시스템**: GPU 메모리와 워커 수를 적절히 조정하세요
|
||||
- 모니터링 대시보드를 통해 실시간으로 성능을 확인할 수 있습니다!
|
||||
- Jetson 사용자는 `jtop` 명령어로 시스템 상태를 모니터링하세요!
|
||||
|
|
@ -0,0 +1 @@
|
|||
# 인페인팅 서버 애플리케이션
|
||||
|
|
@ -0,0 +1 @@
|
|||
# API 모듈
|
||||
|
|
@ -0,0 +1,429 @@
|
|||
"""
|
||||
API 엔드포인트
|
||||
iopaint와 호환되는 인페인팅 및 배경 제거 API를 제공합니다.
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from fastapi import APIRouter, HTTPException, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
import cv2
|
||||
|
||||
from ..core.config import settings
|
||||
from ..core.worker_manager import worker_manager
|
||||
from ..core.session_pool import session_pool
|
||||
from ..models.schemas import (
|
||||
InpaintRequest, RemoveBGRequest, PluginRequest, AdjustMaskRequest,
|
||||
InpaintResponse, RemoveBGResponse, PluginResponse, ServerConfigResponse,
|
||||
HealthResponse, ModelInfo, Device
|
||||
)
|
||||
from ..utils.image_utils import (
|
||||
decode_base64_to_image, encode_image_to_base64, concat_alpha_channel,
|
||||
pil_to_bytes, numpy_to_bytes, adjust_mask, gen_frontend_mask
|
||||
)
|
||||
from ..monitoring.dashboard import monitoring_data
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
async def health_check():
|
||||
"""서버 상태 확인"""
|
||||
start_time = getattr(settings, 'start_time', time.time())
|
||||
uptime = time.time() - start_time
|
||||
|
||||
return HealthResponse(
|
||||
status="healthy",
|
||||
timestamp=datetime.now().isoformat(),
|
||||
version="1.0.0",
|
||||
uptime=uptime
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/server-config", response_model=ServerConfigResponse)
|
||||
async def get_server_config():
|
||||
"""서버 설정 정보 반환 (iopaint 호환)"""
|
||||
try:
|
||||
# 사용 가능한 모델 목록
|
||||
models = [
|
||||
ModelInfo(
|
||||
name="simple-lama",
|
||||
type="inpainting",
|
||||
description="Simple LAMA 인페인팅 모델",
|
||||
supported_formats=["png", "jpg", "jpeg"],
|
||||
max_image_size=2048
|
||||
),
|
||||
ModelInfo(
|
||||
name="migan",
|
||||
type="inpainting",
|
||||
description="MIGAN 인페인팅 모델",
|
||||
supported_formats=["png", "jpg", "jpeg"],
|
||||
max_image_size=2048
|
||||
),
|
||||
ModelInfo(
|
||||
name="rembg",
|
||||
type="rembg",
|
||||
description="Rembg 배경 제거 모델",
|
||||
supported_formats=["png", "jpg", "jpeg"],
|
||||
max_image_size=2048
|
||||
)
|
||||
]
|
||||
|
||||
return ServerConfigResponse(
|
||||
models=models,
|
||||
max_file_size=settings.MAX_FILE_SIZE,
|
||||
supported_formats=["png", "jpg", "jpeg"],
|
||||
device=Device.cuda if settings.IS_JETSON else Device.cuda,
|
||||
is_jetson=settings.IS_JETSON
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"서버 설정 조회 실패: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"서버 설정 조회 실패: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/api/v1/inpaint", response_model=InpaintResponse)
|
||||
async def inpaint_image(request: InpaintRequest):
|
||||
"""인페인팅 API (iopaint 호환)"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# base64 이미지 디코딩
|
||||
image, alpha_channel, info, ext = decode_base64_to_image(request.image)
|
||||
mask, _, _, _ = decode_base64_to_image(request.mask, gray=True)
|
||||
|
||||
# 이미지와 마스크 크기 검증
|
||||
if image.shape[:2] != mask.shape[:2]:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"이미지 크기({image.shape[:2]})와 마스크 크기({mask.shape[:2]})가 일치하지 않습니다."
|
||||
)
|
||||
|
||||
# 이미지 크기 검증
|
||||
if not validate_image_size(image, settings.MAX_IMAGE_SIZE):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"이미지 크기가 너무 큽니다. 최대 {settings.MAX_IMAGE_SIZE}x{settings.MAX_IMAGE_SIZE}까지 지원합니다."
|
||||
)
|
||||
|
||||
# 마스크 이진화
|
||||
mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)[1]
|
||||
|
||||
# 모델 선택
|
||||
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 result is None:
|
||||
raise HTTPException(status_code=500, detail="인페인팅 처리 실패")
|
||||
|
||||
# 결과 이미지를 base64로 인코딩
|
||||
result_image = concat_alpha_channel(result, alpha_channel)
|
||||
result_base64 = encode_image_to_base64(result_image, ext)
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
# 모니터링 통계 업데이트
|
||||
monitoring_data.update_api_stats(
|
||||
endpoint="/api/v1/inpaint",
|
||||
success=True,
|
||||
response_time=processing_time * 1000 # ms로 변환
|
||||
)
|
||||
|
||||
return InpaintResponse(
|
||||
success=True,
|
||||
image=result_base64,
|
||||
processing_time=processing_time,
|
||||
seed=request.sd_seed
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"인페인팅 처리 실패: {e}")
|
||||
|
||||
# 모니터링 통계 업데이트
|
||||
processing_time = time.time() - start_time
|
||||
monitoring_data.update_api_stats(
|
||||
endpoint="/api/v1/inpaint",
|
||||
success=False,
|
||||
response_time=processing_time * 1000,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=500, detail=f"인페인팅 처리 실패: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/api/v1/remove_bg", response_model=RemoveBGResponse)
|
||||
async def remove_background(request: RemoveBGRequest):
|
||||
"""배경 제거 API"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# base64 이미지 디코딩
|
||||
image, alpha_channel, info, ext = decode_base64_to_image(request.image)
|
||||
|
||||
# 이미지 크기 검증
|
||||
if not validate_image_size(image, settings.MAX_IMAGE_SIZE):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"이미지 크기가 너무 큽니다. 최대 {settings.MAX_IMAGE_SIZE}x{settings.MAX_IMAGE_SIZE}까지 지원합니다."
|
||||
)
|
||||
|
||||
# 모델 선택
|
||||
model_name = request.model_name or "rembg"
|
||||
|
||||
# 워커에서 배경 제거 실행
|
||||
result_image, result_mask = await worker_manager.process_remove_bg(
|
||||
image=image,
|
||||
model_name=model_name
|
||||
)
|
||||
|
||||
if result_image is None or result_mask is None:
|
||||
raise HTTPException(status_code=500, detail="배경 제거 처리 실패")
|
||||
|
||||
# 결과를 base64로 인코딩
|
||||
result_base64 = encode_image_to_base64(result_image, ext)
|
||||
mask_base64 = encode_image_to_base64(result_mask, "PNG")
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
# 모니터링 통계 업데이트
|
||||
monitoring_data.update_api_stats(
|
||||
endpoint="/api/v1/remove_bg",
|
||||
success=True,
|
||||
response_time=processing_time * 1000
|
||||
)
|
||||
|
||||
return RemoveBGResponse(
|
||||
success=True,
|
||||
image=result_base64,
|
||||
mask=mask_base64,
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"배경 제거 처리 실패: {e}")
|
||||
|
||||
# 모니터링 통계 업데이트
|
||||
processing_time = time.time() - start_time
|
||||
monitoring_data.update_api_stats(
|
||||
endpoint="/api/v1/remove_bg",
|
||||
success=False,
|
||||
response_time=processing_time * 1000,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=500, detail=f"배경 제거 처리 실패: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/api/v1/run_plugin_gen_image", response_model=PluginResponse)
|
||||
async def run_plugin_generate_image(request: PluginRequest):
|
||||
"""플러그인으로 이미지 생성"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# base64 이미지 디코딩
|
||||
image, alpha_channel, info, ext = decode_base64_to_image(request.image)
|
||||
|
||||
# 이미지 크기 검증
|
||||
if not validate_image_size(image, settings.MAX_IMAGE_SIZE):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"이미지 크기가 너무 큽니다. 최대 {settings.MAX_IMAGE_SIZE}x{settings.MAX_IMAGE_SIZE}까지 지원합니다."
|
||||
)
|
||||
|
||||
# 플러그인 실행
|
||||
if request.name == "rembg":
|
||||
result_image, _ = await worker_manager.process_remove_bg(
|
||||
image=image,
|
||||
model_name=request.model_name or "rembg"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=422, detail=f"지원하지 않는 플러그인: {request.name}")
|
||||
|
||||
if result_image is None:
|
||||
raise HTTPException(status_code=500, detail="플러그인 처리 실패")
|
||||
|
||||
# 결과를 base64로 인코딩
|
||||
result_base64 = encode_image_to_base64(result_image, ext)
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
# 모니터링 통계 업데이트
|
||||
monitoring_data.update_api_stats(
|
||||
endpoint=f"/api/v1/run_plugin_gen_image/{request.name}",
|
||||
success=True,
|
||||
response_time=processing_time * 1000
|
||||
)
|
||||
|
||||
return PluginResponse(
|
||||
success=True,
|
||||
image=result_base64,
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"플러그인 이미지 생성 실패: {e}")
|
||||
|
||||
# 모니터링 통계 업데이트
|
||||
processing_time = time.time() - start_time
|
||||
monitoring_data.update_api_stats(
|
||||
endpoint=f"/api/v1/run_plugin_gen_image/{request.name}",
|
||||
success=False,
|
||||
response_time=processing_time * 1000,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=500, detail=f"플러그인 처리 실패: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/api/v1/run_plugin_gen_mask", response_model=PluginResponse)
|
||||
async def run_plugin_generate_mask(request: PluginRequest):
|
||||
"""플러그인으로 마스크 생성"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# base64 이미지 디코딩
|
||||
image, alpha_channel, info, ext = decode_base64_to_image(request.image)
|
||||
|
||||
# 이미지 크기 검증
|
||||
if not validate_image_size(image, settings.MAX_IMAGE_SIZE):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"이미지 크기가 너무 큽니다. 최대 {settings.MAX_IMAGE_SIZE}x{settings.MAX_IMAGE_SIZE}까지 지원합니다."
|
||||
)
|
||||
|
||||
# 플러그인 실행
|
||||
if request.name == "rembg":
|
||||
_, result_mask = await worker_manager.process_remove_bg(
|
||||
image=image,
|
||||
model_name=request.model_name or "rembg"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=422, detail=f"지원하지 않는 플러그인: {request.name}")
|
||||
|
||||
if result_mask is None:
|
||||
raise HTTPException(status_code=500, detail="플러그인 마스크 생성 실패")
|
||||
|
||||
# 마스크를 base64로 인코딩
|
||||
mask_base64 = encode_image_to_base64(result_mask, "PNG")
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
# 모니터링 통계 업데이트
|
||||
monitoring_data.update_api_stats(
|
||||
endpoint=f"/api/v1/run_plugin_gen_mask/{request.name}",
|
||||
success=True,
|
||||
response_time=processing_time * 1000
|
||||
)
|
||||
|
||||
return PluginResponse(
|
||||
success=True,
|
||||
mask=mask_base64,
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"플러그인 마스크 생성 실패: {e}")
|
||||
|
||||
# 모니터링 통계 업데이트
|
||||
processing_time = time.time() - start_time
|
||||
monitoring_data.update_api_stats(
|
||||
endpoint=f"/api/v1/run_plugin_gen_mask/{request.name}",
|
||||
success=False,
|
||||
response_time=processing_time * 1000,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=500, detail=f"플러그인 마스크 생성 실패: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/api/v1/adjust_mask")
|
||||
async def adjust_mask_api(request: AdjustMaskRequest):
|
||||
"""마스크 조정 API"""
|
||||
try:
|
||||
# base64 마스크 디코딩
|
||||
mask, _, _, _ = decode_base64_to_image(request.mask, gray=True)
|
||||
|
||||
# 마스크 조정
|
||||
adjusted_mask = adjust_mask(mask, request.kernel_size, request.operate)
|
||||
|
||||
# 프론트엔드용 마스크 생성
|
||||
frontend_mask = gen_frontend_mask(adjusted_mask)
|
||||
|
||||
# 결과를 PNG로 반환
|
||||
result_bytes = numpy_to_bytes(frontend_mask, "PNG")
|
||||
|
||||
return Response(
|
||||
content=result_bytes,
|
||||
media_type="image/png"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"마스크 조정 실패: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"마스크 조정 실패: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/api/v1/samplers")
|
||||
async def get_samplers():
|
||||
"""사용 가능한 샘플러 목록 반환"""
|
||||
return [
|
||||
"euler",
|
||||
"euler_a",
|
||||
"heun",
|
||||
"dpm_2",
|
||||
"dpm_2_a",
|
||||
"lms",
|
||||
"dpm_fast",
|
||||
"dpm_adaptive",
|
||||
"dpmpp_2s_a",
|
||||
"dpmpp_sde",
|
||||
"dpmpp_2m",
|
||||
"ddim",
|
||||
"uni_pc",
|
||||
"uni_pc_bh2"
|
||||
]
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def root():
|
||||
"""루트 엔드포인트"""
|
||||
return {
|
||||
"message": "인페인팅 서버 API",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs",
|
||||
"health": "/health"
|
||||
}
|
||||
|
||||
|
||||
# 유틸리티 함수
|
||||
def validate_image_size(image, max_size):
|
||||
"""이미지 크기 검증"""
|
||||
try:
|
||||
h, w = image.shape[:2]
|
||||
pixels = h * w
|
||||
max_pixels = max_size * max_size
|
||||
return pixels <= max_pixels
|
||||
except:
|
||||
return False
|
||||
|
|
@ -0,0 +1 @@
|
|||
# 코어 모듈
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
"""
|
||||
Configuration settings for the inpainting server
|
||||
"""
|
||||
import os
|
||||
import platform
|
||||
from typing import Dict, Any
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# System detection
|
||||
IS_JETSON: bool = platform.machine() == "aarch64" and "tegra" in platform.uname().release.lower()
|
||||
IS_X86: bool = platform.machine() in ["x86_64", "amd64"]
|
||||
|
||||
# Server settings
|
||||
HOST: str = "0.0.0.0"
|
||||
PORT: int = 8000
|
||||
WORKERS: int = 1
|
||||
|
||||
# GPU settings
|
||||
CUDA_DEVICE: int = 0
|
||||
FP16_ENABLED: bool = True
|
||||
|
||||
# Jetson specific settings
|
||||
JETSON_MODE: bool = IS_JETSON
|
||||
JETSON_POWER_MODE: str = "MAXN" # MAXN, 5W, 10W, 15W
|
||||
JETSON_FAN_CONTROL: bool = True
|
||||
JETSON_TEMP_THRESHOLD: int = 75 # Celsius
|
||||
|
||||
# Session pool settings
|
||||
SIMPLE_LAMA_SESSIONS: int = 2 if IS_JETSON else 2
|
||||
MIGAN_SESSIONS: int = 2 if IS_JETSON else 2
|
||||
REMBG_SESSIONS: int = 1 if IS_JETSON else 1
|
||||
|
||||
# Worker settings (Jetson은 더 적은 워커 사용)
|
||||
MAX_WORKERS: int = 4 if IS_JETSON else 8
|
||||
MIN_WORKERS: int = 1 if IS_JETSON else 2
|
||||
WORKER_TIMEOUT: int = 300 # 5 minutes
|
||||
|
||||
# VRAM management (Jetson은 더 보수적인 설정)
|
||||
VRAM_THRESHOLD_HIGH: float = 0.7 if IS_JETSON else 0.8 # 70% for Jetson
|
||||
VRAM_THRESHOLD_LOW: float = 0.3 if IS_JETSON else 0.4 # 30% for Jetson
|
||||
VRAM_CHECK_INTERVAL: int = 20 if IS_JETSON else 30 # More frequent for Jetson
|
||||
|
||||
# Model paths
|
||||
SIMPLE_LAMA_MODEL_PATH: str = "models/simple-lama"
|
||||
MIGAN_MODEL_PATH: str = "models/migan"
|
||||
REMBG_MODEL_PATH: str = "models/rembg"
|
||||
|
||||
# Upload settings
|
||||
MAX_FILE_SIZE: int = 25 * 1024 * 1024 if IS_JETSON else 50 * 1024 * 1024 # 25MB for Jetson
|
||||
ALLOWED_EXTENSIONS: set = {".jpg", ".jpeg", ".png", ".bmp", ".tiff"}
|
||||
|
||||
# Monitoring
|
||||
ENABLE_MONITORING: bool = True
|
||||
MONITORING_PORT: int = 8001
|
||||
|
||||
# Jetson performance settings
|
||||
JETSON_GPU_FREQ: int = 1200 # MHz
|
||||
JETSON_CPU_FREQ: int = 1900 # MHz
|
||||
JETSON_MEMORY_FREQ: int = 1600 # MHz
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
"""
|
||||
세션 풀 관리 시스템
|
||||
각 모델(simple-lama, migan, rembg)의 세션을 효율적으로 관리합니다.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, List, Optional, Any
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModelType(Enum):
|
||||
SIMPLE_LAMA = "simple_lama"
|
||||
MIGAN = "migan"
|
||||
REMBG = "rembg"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelSession:
|
||||
session_id: str
|
||||
model_type: ModelType
|
||||
model: Any
|
||||
created_at: float
|
||||
last_used: float
|
||||
in_use: bool = False
|
||||
|
||||
def mark_used(self):
|
||||
self.last_used = time.time()
|
||||
|
||||
def is_expired(self, timeout: int = 3600) -> bool:
|
||||
return time.time() - self.last_used > timeout
|
||||
|
||||
|
||||
class SessionPool:
|
||||
def __init__(self,
|
||||
simple_lama_count: int = 2,
|
||||
migan_count: int = 2,
|
||||
rembg_count: int = 1):
|
||||
self.pools: Dict[ModelType, List[ModelSession]] = {
|
||||
ModelType.SIMPLE_LAMA: [],
|
||||
ModelType.MIGAN: [],
|
||||
ModelType.REMBG: []
|
||||
}
|
||||
self.pool_sizes = {
|
||||
ModelType.SIMPLE_LAMA: simple_lama_count,
|
||||
ModelType.MIGAN: migan_count,
|
||||
ModelType.REMBG: rembg_count
|
||||
}
|
||||
self.locks: Dict[ModelType, asyncio.Lock] = {
|
||||
model_type: asyncio.Lock() for model_type in ModelType
|
||||
}
|
||||
self._initialized = False
|
||||
|
||||
async def initialize(self):
|
||||
"""모든 모델 세션을 초기화합니다."""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
logger.info("Initializing session pools...")
|
||||
|
||||
for model_type, count in self.pool_sizes.items():
|
||||
await self._initialize_model_pool(model_type, count)
|
||||
|
||||
self._initialized = True
|
||||
logger.info("Session pools initialized successfully")
|
||||
|
||||
async def _initialize_model_pool(self, model_type: ModelType, count: int):
|
||||
"""특정 모델의 세션 풀을 초기화합니다."""
|
||||
logger.info(f"Initializing {count} sessions for {model_type.value}")
|
||||
|
||||
for i in range(count):
|
||||
try:
|
||||
session = await self._create_session(model_type, f"{model_type.value}_{i}")
|
||||
self.pools[model_type].append(session)
|
||||
logger.info(f"Created session {session.session_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create session for {model_type.value}: {e}")
|
||||
|
||||
async def _create_session(self, model_type: ModelType, session_id: str) -> ModelSession:
|
||||
"""새로운 모델 세션을 생성합니다."""
|
||||
model = await self._load_model(model_type)
|
||||
return ModelSession(
|
||||
session_id=session_id,
|
||||
model_type=model_type,
|
||||
model=model,
|
||||
created_at=time.time(),
|
||||
last_used=time.time()
|
||||
)
|
||||
|
||||
async def _load_model(self, model_type: ModelType) -> Any:
|
||||
"""모델을 로드합니다."""
|
||||
# 실제 구현에서는 각 모델을 로드하는 로직이 들어갑니다
|
||||
if model_type == ModelType.SIMPLE_LAMA:
|
||||
return await self._load_simple_lama_model()
|
||||
elif model_type == ModelType.MIGAN:
|
||||
return await self._load_migan_model()
|
||||
elif model_type == ModelType.REMBG:
|
||||
return await self._load_rembg_model()
|
||||
else:
|
||||
raise ValueError(f"Unknown model type: {model_type}")
|
||||
|
||||
async def _load_simple_lama_model(self):
|
||||
"""Simple LAMA 모델을 로드합니다."""
|
||||
# Placeholder - 실제 모델 로딩 로직으로 대체
|
||||
await asyncio.sleep(0.1) # 시뮬레이션
|
||||
return {"model": "simple_lama", "loaded": True}
|
||||
|
||||
async def _load_migan_model(self):
|
||||
"""MIGAN 모델을 로드합니다."""
|
||||
# Placeholder - 실제 모델 로딩 로직으로 대체
|
||||
await asyncio.sleep(0.1) # 시뮬레이션
|
||||
return {"model": "migan", "loaded": True}
|
||||
|
||||
async def _load_rembg_model(self):
|
||||
"""REMBG 모델을 로드합니다."""
|
||||
# Placeholder - 실제 모델 로딩 로직으로 대체
|
||||
await asyncio.sleep(0.1) # 시뮬레이션
|
||||
return {"model": "rembg", "loaded": True}
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_session(self, model_type: ModelType):
|
||||
"""세션을 가져와서 사용 후 반환합니다."""
|
||||
session = await self._acquire_session(model_type)
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await self._release_session(session)
|
||||
|
||||
async def _acquire_session(self, model_type: ModelType) -> ModelSession:
|
||||
"""사용 가능한 세션을 획득합니다."""
|
||||
async with self.locks[model_type]:
|
||||
# 사용 가능한 세션 찾기
|
||||
for session in self.pools[model_type]:
|
||||
if not session.in_use:
|
||||
session.in_use = True
|
||||
session.mark_used()
|
||||
logger.debug(f"Acquired session {session.session_id}")
|
||||
return session
|
||||
|
||||
# 사용 가능한 세션이 없으면 대기
|
||||
logger.warning(f"No available sessions for {model_type.value}, waiting...")
|
||||
|
||||
# 세션이 사용 가능해질 때까지 대기
|
||||
while True:
|
||||
await asyncio.sleep(0.1)
|
||||
async with self.locks[model_type]:
|
||||
for session in self.pools[model_type]:
|
||||
if not session.in_use:
|
||||
session.in_use = True
|
||||
session.mark_used()
|
||||
logger.debug(f"Acquired session {session.session_id} after waiting")
|
||||
return session
|
||||
|
||||
async def _release_session(self, session: ModelSession):
|
||||
"""세션을 반환합니다."""
|
||||
async with self.locks[session.model_type]:
|
||||
session.in_use = False
|
||||
logger.debug(f"Released session {session.session_id}")
|
||||
|
||||
async def get_pool_status(self) -> Dict[str, Any]:
|
||||
"""풀 상태를 반환합니다."""
|
||||
status = {}
|
||||
for model_type in ModelType:
|
||||
pool = self.pools[model_type]
|
||||
total = len(pool)
|
||||
in_use = sum(1 for session in pool if session.in_use)
|
||||
available = total - in_use
|
||||
|
||||
status[model_type.value] = {
|
||||
"total": total,
|
||||
"in_use": in_use,
|
||||
"available": available,
|
||||
"sessions": [
|
||||
{
|
||||
"id": session.session_id,
|
||||
"in_use": session.in_use,
|
||||
"last_used": session.last_used,
|
||||
"created_at": session.created_at
|
||||
}
|
||||
for session in pool
|
||||
]
|
||||
}
|
||||
return status
|
||||
|
||||
async def cleanup_expired_sessions(self, timeout: int = 3600):
|
||||
"""만료된 세션을 정리합니다."""
|
||||
for model_type, pool in self.pools.items():
|
||||
async with self.locks[model_type]:
|
||||
expired_sessions = [s for s in pool if s.is_expired(timeout) and not s.in_use]
|
||||
for session in expired_sessions:
|
||||
pool.remove(session)
|
||||
logger.info(f"Removed expired session {session.session_id}")
|
||||
|
||||
async def scale_pool(self, model_type: ModelType, new_size: int):
|
||||
"""풀 크기를 조정합니다."""
|
||||
async with self.locks[model_type]:
|
||||
current_size = len(self.pools[model_type])
|
||||
|
||||
if new_size > current_size:
|
||||
# 세션 추가
|
||||
for i in range(current_size, new_size):
|
||||
session_id = f"{model_type.value}_{i}"
|
||||
session = await self._create_session(model_type, session_id)
|
||||
self.pools[model_type].append(session)
|
||||
logger.info(f"Added session {session_id}")
|
||||
|
||||
elif new_size < current_size:
|
||||
# 세션 제거 (사용 중이지 않은 것만)
|
||||
sessions_to_remove = []
|
||||
for session in self.pools[model_type]:
|
||||
if not session.in_use and len(sessions_to_remove) < (current_size - new_size):
|
||||
sessions_to_remove.append(session)
|
||||
|
||||
for session in sessions_to_remove:
|
||||
self.pools[model_type].remove(session)
|
||||
logger.info(f"Removed session {session.session_id}")
|
||||
|
||||
self.pool_sizes[model_type] = new_size
|
||||
|
||||
|
||||
# 전역 세션 풀 인스턴스
|
||||
session_pool = SessionPool()
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
"""
|
||||
동적 워커 관리 시스템
|
||||
VRAM 사용량에 따라 워커 수를 동적으로 조정합니다.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict, List, Optional, Callable, Any
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from ..utils.gpu_monitor import gpu_monitor
|
||||
from ..core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WorkerStatus(Enum):
|
||||
IDLE = "idle"
|
||||
BUSY = "busy"
|
||||
STARTING = "starting"
|
||||
STOPPING = "stopping"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Worker:
|
||||
worker_id: str
|
||||
status: WorkerStatus
|
||||
created_at: float
|
||||
last_task_at: Optional[float] = None
|
||||
current_task: Optional[str] = None
|
||||
task_count: int = 0
|
||||
error_count: int = 0
|
||||
|
||||
def mark_task_start(self, task_id: str):
|
||||
self.status = WorkerStatus.BUSY
|
||||
self.current_task = task_id
|
||||
self.last_task_at = time.time()
|
||||
|
||||
def mark_task_complete(self):
|
||||
self.status = WorkerStatus.IDLE
|
||||
self.current_task = None
|
||||
self.task_count += 1
|
||||
|
||||
def mark_error(self):
|
||||
self.status = WorkerStatus.ERROR
|
||||
self.error_count += 1
|
||||
|
||||
|
||||
class WorkerManager:
|
||||
def __init__(self):
|
||||
self.workers: Dict[str, Worker] = {}
|
||||
self.task_queue: asyncio.Queue = asyncio.Queue()
|
||||
self.executor = ThreadPoolExecutor(max_workers=settings.MAX_WORKERS)
|
||||
self.running = False
|
||||
self.monitor_task: Optional[asyncio.Task] = None
|
||||
self.worker_tasks: Dict[str, asyncio.Task] = {}
|
||||
|
||||
# 스케일링 설정
|
||||
self.last_scale_time = time.time()
|
||||
self.scale_cooldown = 60 # 1분 쿨다운
|
||||
|
||||
async def start(self):
|
||||
"""워커 매니저를 시작합니다."""
|
||||
if self.running:
|
||||
return
|
||||
|
||||
self.running = True
|
||||
logger.info("Starting worker manager...")
|
||||
|
||||
# 초기 워커 생성
|
||||
await self._scale_workers(settings.MIN_WORKERS)
|
||||
|
||||
# 모니터링 태스크 시작
|
||||
self.monitor_task = asyncio.create_task(self._monitor_loop())
|
||||
|
||||
logger.info(f"Worker manager started with {len(self.workers)} workers")
|
||||
|
||||
async def stop(self):
|
||||
"""워커 매니저를 중지합니다."""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
self.running = False
|
||||
logger.info("Stopping worker manager...")
|
||||
|
||||
# 모니터링 태스크 중지
|
||||
if self.monitor_task:
|
||||
self.monitor_task.cancel()
|
||||
try:
|
||||
await self.monitor_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
# 모든 워커 중지
|
||||
await self._stop_all_workers()
|
||||
|
||||
# 스레드 풀 종료
|
||||
self.executor.shutdown(wait=True)
|
||||
|
||||
logger.info("Worker manager stopped")
|
||||
|
||||
async def submit_task(self, task_func: Callable, *args, **kwargs) -> Any:
|
||||
"""태스크를 워커에게 제출합니다."""
|
||||
task_id = str(uuid.uuid4())
|
||||
|
||||
# 사용 가능한 워커 찾기
|
||||
worker = await self._get_available_worker()
|
||||
if not worker:
|
||||
logger.warning("No available workers, queuing task")
|
||||
# 큐에 추가하고 대기
|
||||
future = asyncio.Future()
|
||||
await self.task_queue.put((task_id, task_func, args, kwargs, future))
|
||||
return await future
|
||||
|
||||
# 워커에 태스크 할당
|
||||
return await self._execute_task(worker, task_id, task_func, *args, **kwargs)
|
||||
|
||||
async def _get_available_worker(self) -> Optional[Worker]:
|
||||
"""사용 가능한 워커를 찾습니다."""
|
||||
for worker in self.workers.values():
|
||||
if worker.status == WorkerStatus.IDLE:
|
||||
return worker
|
||||
return None
|
||||
|
||||
async def _execute_task(self, worker: Worker, task_id: str,
|
||||
task_func: Callable, *args, **kwargs) -> Any:
|
||||
"""워커에서 태스크를 실행합니다."""
|
||||
worker.mark_task_start(task_id)
|
||||
logger.debug(f"Executing task {task_id} on worker {worker.worker_id}")
|
||||
|
||||
try:
|
||||
# 비동기 함수인지 확인
|
||||
if asyncio.iscoroutinefunction(task_func):
|
||||
result = await task_func(*args, **kwargs)
|
||||
else:
|
||||
# 동기 함수는 스레드 풀에서 실행
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(self.executor, task_func, *args, **kwargs)
|
||||
|
||||
worker.mark_task_complete()
|
||||
logger.debug(f"Task {task_id} completed successfully")
|
||||
|
||||
# 큐에서 대기 중인 태스크가 있다면 처리
|
||||
asyncio.create_task(self._process_queue())
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
worker.mark_error()
|
||||
logger.error(f"Task {task_id} failed on worker {worker.worker_id}: {e}")
|
||||
|
||||
# 큐에서 대기 중인 태스크가 있다면 처리
|
||||
asyncio.create_task(self._process_queue())
|
||||
|
||||
raise e
|
||||
|
||||
async def _process_queue(self):
|
||||
"""큐에서 대기 중인 태스크를 처리합니다."""
|
||||
if self.task_queue.empty():
|
||||
return
|
||||
|
||||
worker = await self._get_available_worker()
|
||||
if not worker:
|
||||
return
|
||||
|
||||
try:
|
||||
task_id, task_func, args, kwargs, future = self.task_queue.get_nowait()
|
||||
result = await self._execute_task(worker, task_id, task_func, *args, **kwargs)
|
||||
future.set_result(result)
|
||||
except asyncio.QueueEmpty:
|
||||
pass
|
||||
except Exception as e:
|
||||
if 'future' in locals():
|
||||
future.set_exception(e)
|
||||
|
||||
async def _monitor_loop(self):
|
||||
"""모니터링 루프"""
|
||||
while self.running:
|
||||
try:
|
||||
await self._check_scaling()
|
||||
await self._cleanup_error_workers()
|
||||
await asyncio.sleep(settings.VRAM_CHECK_INTERVAL)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in monitor loop: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _check_scaling(self):
|
||||
"""스케일링 필요성을 확인합니다."""
|
||||
current_time = time.time()
|
||||
if current_time - self.last_scale_time < self.scale_cooldown:
|
||||
return
|
||||
|
||||
# VRAM 사용량 확인
|
||||
gpu_info = gpu_monitor.get_gpu_memory_info()
|
||||
vram_usage = gpu_info['usage_percent'] / 100.0
|
||||
|
||||
# 현재 워커 상태 분석
|
||||
total_workers = len(self.workers)
|
||||
idle_workers = sum(1 for w in self.workers.values() if w.status == WorkerStatus.IDLE)
|
||||
busy_workers = sum(1 for w in self.workers.values() if w.status == WorkerStatus.BUSY)
|
||||
queue_size = self.task_queue.qsize()
|
||||
|
||||
logger.debug(f"Scaling check - VRAM: {vram_usage:.2f}, Workers: {total_workers}, "
|
||||
f"Idle: {idle_workers}, Busy: {busy_workers}, Queue: {queue_size}")
|
||||
|
||||
# 스케일 업 조건
|
||||
should_scale_up = (
|
||||
vram_usage < settings.VRAM_THRESHOLD_LOW and
|
||||
(queue_size > 0 or idle_workers == 0) and
|
||||
total_workers < settings.MAX_WORKERS
|
||||
)
|
||||
|
||||
# 스케일 다운 조건
|
||||
should_scale_down = (
|
||||
vram_usage > settings.VRAM_THRESHOLD_HIGH or
|
||||
(idle_workers > total_workers * 0.5 and total_workers > settings.MIN_WORKERS)
|
||||
)
|
||||
|
||||
if should_scale_up:
|
||||
new_count = min(total_workers + 1, settings.MAX_WORKERS)
|
||||
await self._scale_workers(new_count)
|
||||
self.last_scale_time = current_time
|
||||
logger.info(f"Scaled up to {new_count} workers (VRAM: {vram_usage:.2f})")
|
||||
|
||||
elif should_scale_down:
|
||||
new_count = max(total_workers - 1, settings.MIN_WORKERS)
|
||||
await self._scale_workers(new_count)
|
||||
self.last_scale_time = current_time
|
||||
logger.info(f"Scaled down to {new_count} workers (VRAM: {vram_usage:.2f})")
|
||||
|
||||
async def _scale_workers(self, target_count: int):
|
||||
"""워커 수를 조정합니다."""
|
||||
current_count = len(self.workers)
|
||||
|
||||
if target_count > current_count:
|
||||
# 워커 추가
|
||||
for i in range(target_count - current_count):
|
||||
worker_id = f"worker_{uuid.uuid4().hex[:8]}"
|
||||
worker = Worker(
|
||||
worker_id=worker_id,
|
||||
status=WorkerStatus.IDLE,
|
||||
created_at=time.time()
|
||||
)
|
||||
self.workers[worker_id] = worker
|
||||
logger.debug(f"Created worker {worker_id}")
|
||||
|
||||
elif target_count < current_count:
|
||||
# 워커 제거 (유휴 상태인 것만)
|
||||
workers_to_remove = []
|
||||
for worker in self.workers.values():
|
||||
if (worker.status == WorkerStatus.IDLE and
|
||||
len(workers_to_remove) < (current_count - target_count)):
|
||||
workers_to_remove.append(worker)
|
||||
|
||||
for worker in workers_to_remove:
|
||||
worker.status = WorkerStatus.STOPPING
|
||||
del self.workers[worker.worker_id]
|
||||
logger.debug(f"Removed worker {worker.worker_id}")
|
||||
|
||||
async def _cleanup_error_workers(self):
|
||||
"""에러 상태의 워커를 정리합니다."""
|
||||
error_workers = [w for w in self.workers.values() if w.status == WorkerStatus.ERROR]
|
||||
|
||||
for worker in error_workers:
|
||||
# 에러 워커 제거 후 새 워커 생성
|
||||
del self.workers[worker.worker_id]
|
||||
logger.warning(f"Removed error worker {worker.worker_id}")
|
||||
|
||||
# 새 워커 생성
|
||||
new_worker_id = f"worker_{uuid.uuid4().hex[:8]}"
|
||||
new_worker = Worker(
|
||||
worker_id=new_worker_id,
|
||||
status=WorkerStatus.IDLE,
|
||||
created_at=time.time()
|
||||
)
|
||||
self.workers[new_worker_id] = new_worker
|
||||
logger.info(f"Created replacement worker {new_worker_id}")
|
||||
|
||||
async def _stop_all_workers(self):
|
||||
"""모든 워커를 중지합니다."""
|
||||
for worker in self.workers.values():
|
||||
worker.status = WorkerStatus.STOPPING
|
||||
|
||||
# 실행 중인 태스크가 완료될 때까지 대기
|
||||
max_wait = 30 # 30초 최대 대기
|
||||
wait_start = time.time()
|
||||
|
||||
while time.time() - wait_start < max_wait:
|
||||
busy_workers = [w for w in self.workers.values() if w.status == WorkerStatus.BUSY]
|
||||
if not busy_workers:
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
|
||||
self.workers.clear()
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""워커 매니저 상태를 반환합니다."""
|
||||
workers_by_status = {}
|
||||
for status in WorkerStatus:
|
||||
workers_by_status[status.value] = [
|
||||
{
|
||||
"id": w.worker_id,
|
||||
"created_at": w.created_at,
|
||||
"last_task_at": w.last_task_at,
|
||||
"current_task": w.current_task,
|
||||
"task_count": w.task_count,
|
||||
"error_count": w.error_count
|
||||
}
|
||||
for w in self.workers.values() if w.status == status
|
||||
]
|
||||
|
||||
return {
|
||||
"total_workers": len(self.workers),
|
||||
"queue_size": self.task_queue.qsize(),
|
||||
"workers_by_status": workers_by_status,
|
||||
"gpu_info": gpu_monitor.get_gpu_memory_info(),
|
||||
"system_memory": gpu_monitor.get_system_memory_info(),
|
||||
"running": self.running
|
||||
}
|
||||
|
||||
|
||||
# 전역 워커 매니저 인스턴스
|
||||
worker_manager = WorkerManager()
|
||||
|
|
@ -0,0 +1 @@
|
|||
# 모델 모듈
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
"""
|
||||
MIGAN 인페인팅 모델 구현
|
||||
"""
|
||||
import torch
|
||||
import numpy as np
|
||||
import cv2
|
||||
from PIL import Image
|
||||
import logging
|
||||
from typing import Union, Tuple
|
||||
import asyncio
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MiganInpainter:
|
||||
def __init__(self, model_path: str = None, device: str = "cuda", fp16: bool = True):
|
||||
self.model_path = model_path
|
||||
self.device = device
|
||||
self.fp16 = fp16
|
||||
self.model = None
|
||||
self.loaded = False
|
||||
|
||||
async def load_model(self):
|
||||
"""모델을 비동기적으로 로드합니다."""
|
||||
if self.loaded:
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info("Loading MIGAN model...")
|
||||
|
||||
# 실제 구현에서는 MIGAN 모델을 로드
|
||||
# 여기서는 플레이스홀더로 구현
|
||||
await asyncio.sleep(0.1) # 모델 로딩 시뮬레이션
|
||||
|
||||
# TODO: 실제 모델 로딩 로직
|
||||
# self.model = load_migan_model(self.model_path, device=self.device)
|
||||
|
||||
self.model = {"type": "migan", "device": self.device, "fp16": self.fp16}
|
||||
self.loaded = True
|
||||
|
||||
logger.info("MIGAN model loaded successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load MIGAN model: {e}")
|
||||
raise
|
||||
|
||||
def preprocess_image(self, image: Union[Image.Image, np.ndarray]) -> torch.Tensor:
|
||||
"""이미지를 전처리합니다."""
|
||||
if isinstance(image, Image.Image):
|
||||
image = np.array(image)
|
||||
|
||||
# RGB로 변환
|
||||
if image.shape[2] == 4: # RGBA
|
||||
image = cv2.cvtColor(image, cv2.COLOR_RGBA2RGB)
|
||||
elif image.shape[2] == 3 and image.dtype == np.uint8:
|
||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
|
||||
# 크기 조정 (MIGAN은 특정 크기를 선호할 수 있음)
|
||||
height, width = image.shape[:2]
|
||||
if height != 512 or width != 512:
|
||||
image = cv2.resize(image, (512, 512), interpolation=cv2.INTER_LANCZOS4)
|
||||
|
||||
# 정규화 (-1 to 1, MIGAN 스타일)
|
||||
image = image.astype(np.float32) / 127.5 - 1.0
|
||||
|
||||
# 텐서로 변환 (B, C, H, W)
|
||||
tensor = torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0)
|
||||
|
||||
if self.fp16:
|
||||
tensor = tensor.half()
|
||||
|
||||
return tensor.to(self.device)
|
||||
|
||||
def preprocess_mask(self, mask: Union[Image.Image, np.ndarray]) -> torch.Tensor:
|
||||
"""마스크를 전처리합니다."""
|
||||
if isinstance(mask, Image.Image):
|
||||
mask = np.array(mask)
|
||||
|
||||
# 그레이스케일로 변환
|
||||
if len(mask.shape) == 3:
|
||||
mask = cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY)
|
||||
|
||||
# 크기 조정
|
||||
if mask.shape[0] != 512 or mask.shape[1] != 512:
|
||||
mask = cv2.resize(mask, (512, 512), interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
# 이진화 (0 또는 1)
|
||||
mask = (mask > 127).astype(np.float32)
|
||||
|
||||
# 텐서로 변환 (B, 1, H, W)
|
||||
tensor = torch.from_numpy(mask).unsqueeze(0).unsqueeze(0)
|
||||
|
||||
if self.fp16:
|
||||
tensor = tensor.half()
|
||||
|
||||
return tensor.to(self.device)
|
||||
|
||||
def postprocess_result(self, tensor: torch.Tensor, original_size: Tuple[int, int]) -> np.ndarray:
|
||||
"""결과를 후처리합니다."""
|
||||
# CPU로 이동하고 numpy로 변환
|
||||
if tensor.is_cuda:
|
||||
tensor = tensor.cpu()
|
||||
if tensor.dtype == torch.float16:
|
||||
tensor = tensor.float()
|
||||
|
||||
result = tensor.squeeze(0).permute(1, 2, 0).numpy()
|
||||
|
||||
# -1 to 1 범위에서 0-255로 변환
|
||||
result = ((result + 1.0) * 127.5).clip(0, 255).astype(np.uint8)
|
||||
|
||||
# 원본 크기로 복원
|
||||
if result.shape[:2] != original_size:
|
||||
result = cv2.resize(result, (original_size[1], original_size[0]),
|
||||
interpolation=cv2.INTER_LANCZOS4)
|
||||
|
||||
return result
|
||||
|
||||
async def inpaint(self, image: Union[Image.Image, np.ndarray],
|
||||
mask: Union[Image.Image, np.ndarray]) -> np.ndarray:
|
||||
"""인페인팅을 수행합니다."""
|
||||
if not self.loaded:
|
||||
await self.load_model()
|
||||
|
||||
try:
|
||||
# 원본 크기 저장
|
||||
if isinstance(image, Image.Image):
|
||||
original_size = image.size[::-1] # (height, width)
|
||||
else:
|
||||
original_size = image.shape[:2]
|
||||
|
||||
# 전처리
|
||||
image_tensor = self.preprocess_image(image)
|
||||
mask_tensor = self.preprocess_mask(mask)
|
||||
|
||||
# 추론 (실제 구현에서는 모델 추론)
|
||||
with torch.no_grad():
|
||||
# TODO: 실제 모델 추론 로직
|
||||
# result = self.model(image_tensor, mask_tensor)
|
||||
|
||||
# 플레이스홀더: 더 정교한 인페인팅 시뮬레이션
|
||||
result = await self._simulate_advanced_inpainting(image_tensor, mask_tensor)
|
||||
|
||||
# 후처리
|
||||
result_np = self.postprocess_result(result, original_size)
|
||||
|
||||
return result_np
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MIGAN inpainting failed: {e}")
|
||||
raise
|
||||
|
||||
async def _simulate_advanced_inpainting(self, image_tensor: torch.Tensor,
|
||||
mask_tensor: torch.Tensor) -> torch.Tensor:
|
||||
"""고급 인페인팅 시뮬레이션 (실제 구현에서는 제거)"""
|
||||
# 비동기 처리 시뮬레이션
|
||||
await asyncio.sleep(0.15) # MIGAN은 더 오래 걸린다고 가정
|
||||
|
||||
result = image_tensor.clone()
|
||||
mask_bool = mask_tensor.bool()
|
||||
|
||||
# 더 정교한 인페인팅 시뮬레이션: 주변 픽셀의 가중 평균
|
||||
if mask_bool.any():
|
||||
# 간단한 inpainting 시뮬레이션
|
||||
for c in range(3):
|
||||
channel = result[0, c]
|
||||
mask_2d = mask_bool[0, 0]
|
||||
|
||||
# 마스크 영역의 경계에서 값을 가져와서 보간
|
||||
kernel = torch.ones(3, 3, device=self.device) / 9.0
|
||||
if self.fp16:
|
||||
kernel = kernel.half()
|
||||
|
||||
# 간단한 convolution 기반 인페인팅
|
||||
padded_channel = torch.nn.functional.pad(channel.unsqueeze(0).unsqueeze(0), (1, 1, 1, 1), mode='replicate')
|
||||
smoothed = torch.nn.functional.conv2d(padded_channel, kernel.unsqueeze(0).unsqueeze(0), padding=0)
|
||||
|
||||
result[0, c][mask_2d] = smoothed[0, 0][mask_2d]
|
||||
|
||||
return result
|
||||
|
||||
def get_model_info(self) -> dict:
|
||||
"""모델 정보를 반환합니다."""
|
||||
return {
|
||||
"model_type": "migan",
|
||||
"device": self.device,
|
||||
"fp16": self.fp16,
|
||||
"loaded": self.loaded,
|
||||
"model_path": self.model_path,
|
||||
"input_size": (512, 512)
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
"""
|
||||
REMBG 배경 제거 모델 구현
|
||||
"""
|
||||
import torch
|
||||
import numpy as np
|
||||
import cv2
|
||||
from PIL import Image
|
||||
import logging
|
||||
from typing import Union, Tuple
|
||||
import asyncio
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RembgProcessor:
|
||||
def __init__(self, model_name: str = "u2net", device: str = "cuda", fp16: bool = True):
|
||||
self.model_name = model_name
|
||||
self.device = device
|
||||
self.fp16 = fp16
|
||||
self.model = None
|
||||
self.loaded = False
|
||||
|
||||
async def load_model(self):
|
||||
"""모델을 비동기적으로 로드합니다."""
|
||||
if self.loaded:
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info(f"Loading REMBG model ({self.model_name})...")
|
||||
|
||||
# 실제 구현에서는 rembg 라이브러리를 사용
|
||||
# 여기서는 플레이스홀더로 구현
|
||||
await asyncio.sleep(0.1) # 모델 로딩 시뮬레이션
|
||||
|
||||
# TODO: 실제 모델 로딩 로직
|
||||
# from rembg import new_session
|
||||
# self.model = new_session(self.model_name)
|
||||
|
||||
self.model = {
|
||||
"type": "rembg",
|
||||
"model_name": self.model_name,
|
||||
"device": self.device,
|
||||
"fp16": self.fp16
|
||||
}
|
||||
self.loaded = True
|
||||
|
||||
logger.info(f"REMBG model ({self.model_name}) loaded successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load REMBG model: {e}")
|
||||
raise
|
||||
|
||||
def preprocess_image(self, image: Union[Image.Image, np.ndarray]) -> np.ndarray:
|
||||
"""이미지를 전처리합니다."""
|
||||
if isinstance(image, Image.Image):
|
||||
image = np.array(image)
|
||||
|
||||
# RGB로 변환
|
||||
if image.shape[2] == 4: # RGBA
|
||||
image = cv2.cvtColor(image, cv2.COLOR_RGBA2RGB)
|
||||
elif len(image.shape) == 3 and image.shape[2] == 3:
|
||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
|
||||
return image
|
||||
|
||||
def create_mask_from_alpha(self, rgba_image: np.ndarray) -> np.ndarray:
|
||||
"""RGBA 이미지에서 알파 채널을 마스크로 변환합니다."""
|
||||
if rgba_image.shape[2] != 4:
|
||||
raise ValueError("Input image must have 4 channels (RGBA)")
|
||||
|
||||
# 알파 채널을 마스크로 사용
|
||||
alpha_channel = rgba_image[:, :, 3]
|
||||
|
||||
# 0-255 범위의 마스크 생성
|
||||
mask = alpha_channel.astype(np.uint8)
|
||||
|
||||
return mask
|
||||
|
||||
async def remove_background(self, image: Union[Image.Image, np.ndarray]) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""배경을 제거하고 결과 이미지와 마스크를 반환합니다."""
|
||||
if not self.loaded:
|
||||
await self.load_model()
|
||||
|
||||
try:
|
||||
# 전처리
|
||||
processed_image = self.preprocess_image(image)
|
||||
original_shape = processed_image.shape
|
||||
|
||||
# 배경 제거 (실제 구현에서는 rembg 사용)
|
||||
# TODO: 실제 모델 추론 로직
|
||||
# from rembg import remove
|
||||
# result_rgba = remove(self.model, processed_image)
|
||||
|
||||
# 플레이스홀더: 배경 제거 시뮬레이션
|
||||
result_rgba = await self._simulate_background_removal(processed_image)
|
||||
|
||||
# 결과에서 RGB 이미지와 마스크 분리
|
||||
result_rgb = result_rgba[:, :, :3]
|
||||
mask = self.create_mask_from_alpha(result_rgba)
|
||||
|
||||
return result_rgb, mask
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Background removal failed: {e}")
|
||||
raise
|
||||
|
||||
async def _simulate_background_removal(self, image: np.ndarray) -> np.ndarray:
|
||||
"""배경 제거 시뮬레이션 (실제 구현에서는 제거)"""
|
||||
# 비동기 처리 시뮬레이션
|
||||
await asyncio.sleep(0.08) # REMBG는 상대적으로 빠르다고 가정
|
||||
|
||||
height, width = image.shape[:2]
|
||||
|
||||
# 간단한 전경/배경 분리 시뮬레이션
|
||||
# 중앙 영역을 전경으로, 가장자리를 배경으로 가정
|
||||
center_x, center_y = width // 2, height // 2
|
||||
|
||||
# 타원형 마스크 생성
|
||||
y, x = np.ogrid[:height, :width]
|
||||
mask = ((x - center_x) ** 2 / (width * 0.3) ** 2 +
|
||||
(y - center_y) ** 2 / (height * 0.4) ** 2) <= 1
|
||||
|
||||
# 부드러운 가장자리를 위한 가우시안 블러
|
||||
mask_float = mask.astype(np.float32)
|
||||
mask_blurred = cv2.GaussianBlur(mask_float, (51, 51), 20)
|
||||
|
||||
# RGBA 이미지 생성
|
||||
result_rgba = np.zeros((height, width, 4), dtype=np.uint8)
|
||||
result_rgba[:, :, :3] = image # RGB 채널
|
||||
result_rgba[:, :, 3] = (mask_blurred * 255).astype(np.uint8) # 알파 채널
|
||||
|
||||
return result_rgba
|
||||
|
||||
async def apply_new_background(self, foreground: np.ndarray, mask: np.ndarray,
|
||||
background: Union[np.ndarray, tuple]) -> np.ndarray:
|
||||
"""새로운 배경을 적용합니다."""
|
||||
try:
|
||||
height, width = foreground.shape[:2]
|
||||
|
||||
# 배경 준비
|
||||
if isinstance(background, tuple):
|
||||
# 단색 배경
|
||||
bg = np.full((height, width, 3), background, dtype=np.uint8)
|
||||
else:
|
||||
# 이미지 배경
|
||||
if isinstance(background, Image.Image):
|
||||
background = np.array(background)
|
||||
bg = cv2.resize(background, (width, height))
|
||||
if len(bg.shape) == 3 and bg.shape[2] == 4:
|
||||
bg = bg[:, :, :3] # RGBA에서 RGB로
|
||||
|
||||
# 마스크를 0-1 범위로 정규화
|
||||
mask_norm = mask.astype(np.float32) / 255.0
|
||||
mask_3ch = np.stack([mask_norm] * 3, axis=-1)
|
||||
|
||||
# 알파 블렌딩
|
||||
result = (foreground.astype(np.float32) * mask_3ch +
|
||||
bg.astype(np.float32) * (1 - mask_3ch))
|
||||
|
||||
return result.astype(np.uint8)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Background application failed: {e}")
|
||||
raise
|
||||
|
||||
def get_model_info(self) -> dict:
|
||||
"""모델 정보를 반환합니다."""
|
||||
return {
|
||||
"model_type": "rembg",
|
||||
"model_name": self.model_name,
|
||||
"device": self.device,
|
||||
"fp16": self.fp16,
|
||||
"loaded": self.loaded
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
"""
|
||||
API 스키마 정의
|
||||
iopaint와 호환되는 입출력 형식을 지원합니다.
|
||||
"""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Device(str, Enum):
|
||||
cpu = "cpu"
|
||||
cuda = "cuda"
|
||||
mps = "mps"
|
||||
|
||||
|
||||
class InpaintRequest(BaseModel):
|
||||
"""인페인팅 요청 스키마 (iopaint 호환)"""
|
||||
image: str = Field(..., description="base64로 인코딩된 원본 이미지")
|
||||
mask: str = Field(..., description="base64로 인코딩된 마스크 이미지")
|
||||
sd_seed: Optional[int] = Field(-1, description="Stable Diffusion 시드")
|
||||
prompt: Optional[str] = Field("", description="프롬프트")
|
||||
negative_prompt: Optional[str] = Field("", description="네거티브 프롬프트")
|
||||
num_inference_steps: Optional[int] = Field(20, description="추론 스텝 수")
|
||||
guidance_scale: Optional[float] = Field(7.5, description="가이던스 스케일")
|
||||
strength: Optional[float] = Field(1.0, description="인페인팅 강도")
|
||||
model_name: Optional[str] = Field("simple-lama", description="사용할 모델명")
|
||||
|
||||
|
||||
class RemoveBGRequest(BaseModel):
|
||||
"""배경 제거 요청 스키마"""
|
||||
image: str = Field(..., description="base64로 인코딩된 이미지")
|
||||
model_name: Optional[str] = Field("rembg", description="사용할 모델명")
|
||||
|
||||
|
||||
class PluginRequest(BaseModel):
|
||||
"""플러그인 요청 기본 스키마"""
|
||||
name: str = Field(..., description="플러그인 이름")
|
||||
image: str = Field(..., description="base64로 인코딩된 이미지")
|
||||
model_name: Optional[str] = Field(None, description="사용할 모델명")
|
||||
|
||||
|
||||
class AdjustMaskRequest(BaseModel):
|
||||
"""마스크 조정 요청 스키마"""
|
||||
mask: str = Field(..., description="base64로 인코딩된 마스크")
|
||||
kernel_size: int = Field(5, description="커널 크기")
|
||||
operate: str = Field("dilate", description="연산 타입 (dilate/erode)")
|
||||
|
||||
|
||||
class InpaintResponse(BaseModel):
|
||||
"""인페인팅 응답 스키마"""
|
||||
success: bool = Field(..., description="성공 여부")
|
||||
image: Optional[str] = Field(None, description="base64로 인코딩된 결과 이미지")
|
||||
error: Optional[str] = Field(None, description="에러 메시지")
|
||||
processing_time: Optional[float] = Field(None, description="처리 시간 (초)")
|
||||
seed: Optional[int] = Field(None, description="사용된 시드")
|
||||
|
||||
|
||||
class RemoveBGResponse(BaseModel):
|
||||
"""배경 제거 응답 스키마"""
|
||||
success: bool = Field(..., description="성공 여부")
|
||||
image: Optional[str] = Field(None, description="base64로 인코딩된 결과 이미지")
|
||||
mask: Optional[str] = Field(None, description="base64로 인코딩된 마스크")
|
||||
error: Optional[str] = Field(None, description="에러 메시지")
|
||||
processing_time: Optional[float] = Field(None, description="처리 시간 (초)")
|
||||
|
||||
|
||||
class PluginResponse(BaseModel):
|
||||
"""플러그인 응답 스키마"""
|
||||
success: bool = Field(..., description="성공 여부")
|
||||
image: Optional[str] = Field(None, description="base64로 인코딩된 결과 이미지")
|
||||
mask: Optional[str] = Field(None, description="base64로 인코딩된 마스크")
|
||||
error: Optional[str] = Field(None, description="에러 메시지")
|
||||
processing_time: Optional[float] = Field(None, description="처리 시간 (초)")
|
||||
|
||||
|
||||
class ModelInfo(BaseModel):
|
||||
"""모델 정보 스키마"""
|
||||
name: str = Field(..., description="모델명")
|
||||
type: str = Field(..., description="모델 타입 (inpainting/rembg)")
|
||||
description: Optional[str] = Field(None, description="모델 설명")
|
||||
supported_formats: List[str] = Field(default_factory=list, description="지원하는 이미지 형식")
|
||||
max_image_size: Optional[int] = Field(None, description="최대 이미지 크기")
|
||||
|
||||
|
||||
class ServerConfigResponse(BaseModel):
|
||||
"""서버 설정 응답 스키마"""
|
||||
models: List[ModelInfo] = Field(..., description="사용 가능한 모델 목록")
|
||||
max_file_size: int = Field(..., description="최대 파일 크기 (MB)")
|
||||
supported_formats: List[str] = Field(..., description="지원하는 이미지 형식")
|
||||
device: Device = Field(..., description="현재 사용 중인 디바이스")
|
||||
is_jetson: bool = Field(..., description="Jetson 시스템 여부")
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""헬스 체크 응답 스키마"""
|
||||
status: str = Field(..., description="서버 상태")
|
||||
timestamp: str = Field(..., description="현재 시간")
|
||||
version: str = Field(..., description="서버 버전")
|
||||
uptime: float = Field(..., description="가동 시간 (초)")
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""에러 응답 스키마"""
|
||||
error: str = Field(..., description="에러 메시지")
|
||||
detail: Optional[str] = Field(None, description="상세 에러 정보")
|
||||
timestamp: str = Field(..., description="에러 발생 시간")
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
"""
|
||||
Simple LAMA 인페인팅 모델 구현
|
||||
"""
|
||||
import torch
|
||||
import numpy as np
|
||||
import cv2
|
||||
from PIL import Image
|
||||
import logging
|
||||
from typing import Union, Tuple
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SimpleLamaInpainter:
|
||||
def __init__(self, model_path: str = None, device: str = "cuda", fp16: bool = True):
|
||||
self.model_path = model_path
|
||||
self.device = device
|
||||
self.fp16 = fp16
|
||||
self.model = None
|
||||
self.loaded = False
|
||||
|
||||
async def load_model(self):
|
||||
"""모델을 비동기적으로 로드합니다."""
|
||||
if self.loaded:
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info("Loading Simple LAMA model...")
|
||||
|
||||
# 실제 구현에서는 simple-lama-inpainting 라이브러리를 사용
|
||||
# 여기서는 플레이스홀더로 구현
|
||||
await asyncio.sleep(0.1) # 모델 로딩 시뮬레이션
|
||||
|
||||
# TODO: 실제 모델 로딩 로직
|
||||
# from simple_lama_inpainting import SimpleLama
|
||||
# self.model = SimpleLama(device=self.device)
|
||||
|
||||
self.model = {"type": "simple_lama", "device": self.device, "fp16": self.fp16}
|
||||
self.loaded = True
|
||||
|
||||
logger.info("Simple LAMA model loaded successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load Simple LAMA model: {e}")
|
||||
raise
|
||||
|
||||
def preprocess_image(self, image: Union[Image.Image, np.ndarray]) -> torch.Tensor:
|
||||
"""이미지를 전처리합니다."""
|
||||
if isinstance(image, Image.Image):
|
||||
image = np.array(image)
|
||||
|
||||
# RGB로 변환
|
||||
if image.shape[2] == 4: # RGBA
|
||||
image = cv2.cvtColor(image, cv2.COLOR_RGBA2RGB)
|
||||
elif image.shape[2] == 3 and image.dtype == np.uint8:
|
||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
|
||||
# 정규화 (0-1)
|
||||
image = image.astype(np.float32) / 255.0
|
||||
|
||||
# 텐서로 변환 (B, C, H, W)
|
||||
tensor = torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0)
|
||||
|
||||
if self.fp16:
|
||||
tensor = tensor.half()
|
||||
|
||||
return tensor.to(self.device)
|
||||
|
||||
def preprocess_mask(self, mask: Union[Image.Image, np.ndarray]) -> torch.Tensor:
|
||||
"""마스크를 전처리합니다."""
|
||||
if isinstance(mask, Image.Image):
|
||||
mask = np.array(mask)
|
||||
|
||||
# 그레이스케일로 변환
|
||||
if len(mask.shape) == 3:
|
||||
mask = cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY)
|
||||
|
||||
# 이진화 (0 또는 1)
|
||||
mask = (mask > 127).astype(np.float32)
|
||||
|
||||
# 텐서로 변환 (B, 1, H, W)
|
||||
tensor = torch.from_numpy(mask).unsqueeze(0).unsqueeze(0)
|
||||
|
||||
if self.fp16:
|
||||
tensor = tensor.half()
|
||||
|
||||
return tensor.to(self.device)
|
||||
|
||||
def postprocess_result(self, tensor: torch.Tensor) -> np.ndarray:
|
||||
"""결과를 후처리합니다."""
|
||||
# CPU로 이동하고 numpy로 변환
|
||||
if tensor.is_cuda:
|
||||
tensor = tensor.cpu()
|
||||
if tensor.dtype == torch.float16:
|
||||
tensor = tensor.float()
|
||||
|
||||
result = tensor.squeeze(0).permute(1, 2, 0).numpy()
|
||||
|
||||
# 0-255 범위로 변환
|
||||
result = np.clip(result * 255.0, 0, 255).astype(np.uint8)
|
||||
|
||||
return result
|
||||
|
||||
async def inpaint(self, image: Union[Image.Image, np.ndarray],
|
||||
mask: Union[Image.Image, np.ndarray]) -> 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():
|
||||
# TODO: 실제 모델 추론 로직
|
||||
# result = self.model(image_tensor, mask_tensor)
|
||||
|
||||
# 플레이스홀더: 마스크 영역을 평균 색상으로 채우기
|
||||
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)
|
||||
|
||||
# 마스크 영역을 이미지의 평균 색상으로 채우기
|
||||
result = image_tensor.clone()
|
||||
mask_bool = mask_tensor.bool()
|
||||
|
||||
# 각 채널별 평균 계산
|
||||
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
|
||||
|
||||
def get_model_info(self) -> dict:
|
||||
"""모델 정보를 반환합니다."""
|
||||
return {
|
||||
"model_type": "simple_lama",
|
||||
"device": self.device,
|
||||
"fp16": self.fp16,
|
||||
"loaded": self.loaded,
|
||||
"model_path": self.model_path
|
||||
}
|
||||
|
|
@ -0,0 +1,920 @@
|
|||
"""
|
||||
워커 감시 대시보드
|
||||
실시간으로 워커 상태, GPU 사용량, 세션 풀 상태를 모니터링합니다.
|
||||
Jetson Xavier와 x86 시스템을 모두 지원합니다.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import psutil
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse
|
||||
import uvicorn
|
||||
|
||||
from ..core.worker_manager import worker_manager
|
||||
from ..core.session_pool import session_pool
|
||||
from ..utils.gpu_monitor import gpu_monitor
|
||||
from ..core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 모니터링 앱 생성
|
||||
monitor_app = FastAPI(
|
||||
title="인페인팅 서버 모니터링 대시보드",
|
||||
description="실시간 서버 상태 모니터링 (Jetson Xavier & x86 지원)",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 연결된 WebSocket 클라이언트들
|
||||
connected_clients: List[WebSocket] = []
|
||||
|
||||
|
||||
class MonitoringData:
|
||||
def __init__(self):
|
||||
self.history: List[Dict[str, Any]] = []
|
||||
self.max_history = 100 # 최대 100개 데이터 포인트 저장
|
||||
self.api_stats = {
|
||||
"total_requests": 0,
|
||||
"successful_requests": 0,
|
||||
"failed_requests": 0,
|
||||
"endpoint_usage": {},
|
||||
"response_times": [],
|
||||
"errors": []
|
||||
}
|
||||
self.alerts = []
|
||||
|
||||
async def collect_data(self) -> Dict[str, Any]:
|
||||
"""현재 시스템 상태 데이터를 수집합니다."""
|
||||
timestamp = datetime.now().isoformat()
|
||||
|
||||
# GPU 정보
|
||||
gpu_info = gpu_monitor.get_gpu_memory_info()
|
||||
gpu_utilization = gpu_monitor.get_gpu_utilization()
|
||||
|
||||
# 시스템 메모리 정보
|
||||
system_memory = gpu_monitor.get_system_memory_info()
|
||||
|
||||
# 시스템 성능 지표
|
||||
system_performance = self._get_system_performance()
|
||||
|
||||
# 워커 매니저 상태
|
||||
worker_status = worker_manager.get_status()
|
||||
|
||||
# 세션 풀 상태
|
||||
session_status = await session_pool.get_pool_status()
|
||||
|
||||
# Jetson 전용 정보
|
||||
jetson_info = {}
|
||||
if settings.IS_JETSON:
|
||||
jetson_info = gpu_monitor.get_jetson_specific_info()
|
||||
|
||||
# API 통계
|
||||
api_stats = self._get_api_statistics()
|
||||
|
||||
# 알림 및 경고
|
||||
alerts = self._check_alerts(gpu_info, system_memory, worker_status)
|
||||
|
||||
data = {
|
||||
"timestamp": timestamp,
|
||||
"system_type": "Jetson Xavier" if settings.IS_JETSON else "x86_64",
|
||||
"gpu": {
|
||||
**gpu_info,
|
||||
"utilization": gpu_utilization
|
||||
},
|
||||
"system_memory": system_memory,
|
||||
"system_performance": system_performance,
|
||||
"workers": worker_status,
|
||||
"sessions": session_status,
|
||||
"jetson": jetson_info,
|
||||
"api_stats": api_stats,
|
||||
"alerts": alerts
|
||||
}
|
||||
|
||||
# 히스토리에 추가
|
||||
self.history.append(data)
|
||||
if len(self.history) > self.max_history:
|
||||
self.history.pop(0)
|
||||
|
||||
return data
|
||||
|
||||
def _get_system_performance(self) -> Dict[str, Any]:
|
||||
"""시스템 성능 지표를 수집합니다."""
|
||||
try:
|
||||
# CPU 사용률
|
||||
cpu_percent = psutil.cpu_percent(interval=1)
|
||||
cpu_count = psutil.cpu_count()
|
||||
cpu_freq = psutil.cpu_freq()
|
||||
|
||||
# 디스크 I/O
|
||||
disk_io = psutil.disk_io_counters()
|
||||
|
||||
# 네트워크 I/O
|
||||
net_io = psutil.net_io_counters()
|
||||
|
||||
# 프로세스 정보
|
||||
processes = len(psutil.pids())
|
||||
|
||||
# 시스템 부하
|
||||
load_avg = os.getloadavg() if hasattr(os, 'getloadavg') else [0, 0, 0]
|
||||
|
||||
return {
|
||||
"cpu": {
|
||||
"usage_percent": cpu_percent,
|
||||
"count": cpu_count,
|
||||
"frequency_mhz": cpu_freq.current if cpu_freq else 0,
|
||||
"load_average": {
|
||||
"1min": load_avg[0],
|
||||
"5min": load_avg[1],
|
||||
"15min": load_avg[2]
|
||||
}
|
||||
},
|
||||
"disk": {
|
||||
"read_bytes": disk_io.read_bytes if disk_io else 0,
|
||||
"write_bytes": disk_io.write_bytes if disk_io else 0,
|
||||
"read_count": disk_io.read_count if disk_io else 0,
|
||||
"write_count": disk_io.write_count if disk_io else 0
|
||||
},
|
||||
"network": {
|
||||
"bytes_sent": net_io.bytes_sent if net_io else 0,
|
||||
"bytes_recv": net_io.bytes_recv if net_io else 0,
|
||||
"packets_sent": net_io.packets_sent if net_io else 0,
|
||||
"packets_recv": net_io.packets_recv if net_io else 0
|
||||
},
|
||||
"processes": processes
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"시스템 성능 정보 수집 실패: {e}")
|
||||
return {}
|
||||
|
||||
def _get_api_statistics(self) -> Dict[str, Any]:
|
||||
"""API 통계 정보를 반환합니다."""
|
||||
# 실제 구현에서는 API 엔드포인트에서 이 정보를 수집해야 합니다
|
||||
return {
|
||||
"total_requests": self.api_stats["total_requests"],
|
||||
"successful_requests": self.api_stats["successful_requests"],
|
||||
"failed_requests": self.api_stats["failed_requests"],
|
||||
"success_rate": (
|
||||
(self.api_stats["successful_requests"] / max(self.api_stats["total_requests"], 1)) * 100
|
||||
),
|
||||
"endpoint_usage": self.api_stats["endpoint_usage"],
|
||||
"average_response_time": (
|
||||
sum(self.api_stats["response_times"]) / max(len(self.api_stats["response_times"]), 1)
|
||||
) if self.api_stats["response_times"] else 0,
|
||||
"recent_errors": self.api_stats["errors"][-5:] # 최근 5개 에러
|
||||
}
|
||||
|
||||
def _check_alerts(self, gpu_info: Dict, system_memory: Dict, worker_status: Dict) -> List[Dict]:
|
||||
"""시스템 상태를 확인하고 알림을 생성합니다."""
|
||||
alerts = []
|
||||
current_time = datetime.now()
|
||||
|
||||
# GPU 메모리 경고
|
||||
if gpu_info.get("usage_percent", 0) > 90:
|
||||
alerts.append({
|
||||
"level": "critical",
|
||||
"message": f"GPU 메모리 사용률이 높습니다: {gpu_info.get('usage_percent', 0):.1f}%",
|
||||
"timestamp": current_time.isoformat(),
|
||||
"category": "gpu"
|
||||
})
|
||||
elif gpu_info.get("usage_percent", 0) > 80:
|
||||
alerts.append({
|
||||
"level": "warning",
|
||||
"message": f"GPU 메모리 사용률이 높습니다: {gpu_info.get('usage_percent', 0):.1f}%",
|
||||
"timestamp": current_time.isoformat(),
|
||||
"category": "gpu"
|
||||
})
|
||||
|
||||
# 시스템 메모리 경고
|
||||
if system_memory.get("usage_percent", 0) > 90:
|
||||
alerts.append({
|
||||
"level": "critical",
|
||||
"message": f"시스템 메모리 사용률이 높습니다: {system_memory.get('usage_percent', 0):.1f}%",
|
||||
"timestamp": current_time.isoformat(),
|
||||
"category": "memory"
|
||||
})
|
||||
|
||||
# 워커 상태 경고
|
||||
if worker_status.get("active_workers", 0) == 0:
|
||||
alerts.append({
|
||||
"level": "critical",
|
||||
"message": "활성 워커가 없습니다",
|
||||
"timestamp": current_time.isoformat(),
|
||||
"category": "workers"
|
||||
})
|
||||
|
||||
# Jetson 전용 경고
|
||||
if settings.IS_JETSON:
|
||||
# 온도 경고 (실제 구현에서는 온도 정보를 가져와야 함)
|
||||
pass
|
||||
|
||||
return alerts
|
||||
|
||||
def update_api_stats(self, endpoint: str, success: bool, response_time: float, error: str = None):
|
||||
"""API 통계를 업데이트합니다."""
|
||||
self.api_stats["total_requests"] += 1
|
||||
|
||||
if success:
|
||||
self.api_stats["successful_requests"] += 1
|
||||
else:
|
||||
self.api_stats["failed_requests"] += 1
|
||||
if error:
|
||||
self.api_stats["errors"].append({
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"endpoint": endpoint,
|
||||
"error": error
|
||||
})
|
||||
|
||||
# 엔드포인트별 사용량
|
||||
if endpoint not in self.api_stats["endpoint_usage"]:
|
||||
self.api_stats["endpoint_usage"][endpoint] = 0
|
||||
self.api_stats["endpoint_usage"][endpoint] += 1
|
||||
|
||||
# 응답 시간
|
||||
self.api_stats["response_times"].append(response_time)
|
||||
if len(self.api_stats["response_times"]) > 100:
|
||||
self.api_stats["response_times"].pop(0)
|
||||
|
||||
# 에러 로그 제한
|
||||
if len(self.api_stats["errors"]) > 50:
|
||||
self.api_stats["errors"] = self.api_stats["errors"][-50:]
|
||||
|
||||
def get_history(self) -> List[Dict[str, Any]]:
|
||||
"""데이터 히스토리를 반환합니다."""
|
||||
return self.history
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""통계 정보를 반환합니다."""
|
||||
if not self.history:
|
||||
return {}
|
||||
|
||||
recent_data = self.history[-10:] # 최근 10개 데이터
|
||||
|
||||
# GPU 사용률 평균
|
||||
gpu_usage_avg = sum(d["gpu"]["usage_percent"] for d in recent_data) / len(recent_data)
|
||||
gpu_util_avg = sum(d["gpu"]["utilization"] for d in recent_data) / len(recent_data)
|
||||
|
||||
# 시스템 메모리 사용률 평균
|
||||
sys_mem_avg = sum(d["system_memory"]["usage_percent"] for d in recent_data) / len(recent_data)
|
||||
|
||||
# 워커 수 평균
|
||||
worker_avg = sum(d["workers"]["active_workers"] for d in recent_data) / len(recent_data)
|
||||
|
||||
return {
|
||||
"gpu_usage_avg": round(gpu_usage_avg, 2),
|
||||
"gpu_util_avg": round(gpu_util_avg, 2),
|
||||
"system_memory_avg": round(sys_mem_avg, 2),
|
||||
"worker_avg": round(worker_avg, 2),
|
||||
"data_points": len(recent_data)
|
||||
}
|
||||
|
||||
# 전역 모니터링 데이터 인스턴스
|
||||
monitoring_data = MonitoringData()
|
||||
|
||||
|
||||
# HTML 템플릿
|
||||
HTML_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>인페인팅 서버 모니터링</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
.header p {
|
||||
margin: 10px 0 0 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.card h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 15px 0;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.metric-label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
.metric-value {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
color: #667eea;
|
||||
}
|
||||
.chart-container {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.chart-container h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.alerts {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 5px;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
.alert.critical {
|
||||
background: #ffe6e6;
|
||||
border-left-color: #dc3545;
|
||||
color: #721c24;
|
||||
}
|
||||
.alert.warning {
|
||||
background: #fff3cd;
|
||||
border-left-color: #ffc107;
|
||||
color: #856404;
|
||||
}
|
||||
.alert.info {
|
||||
background: #d1ecf1;
|
||||
border-left-color: #17a2b8;
|
||||
color: #0c5460;
|
||||
}
|
||||
.endpoint-usage {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.endpoint-item {
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.endpoint-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.endpoint-count {
|
||||
color: #667eea;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.system-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.status-online { background: #28a745; }
|
||||
.status-offline { background: #dc3545; }
|
||||
.status-warning { background: #ffc107; }
|
||||
.refresh-time {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚀 인페인팅 서버 모니터링</h1>
|
||||
<p>실시간 서버 상태 및 성능 모니터링 대시보드</p>
|
||||
</div>
|
||||
|
||||
<!-- 시스템 개요 -->
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>🖥️ 시스템 정보</h3>
|
||||
<div class="metric">
|
||||
<span class="metric-label">시스템 타입:</span>
|
||||
<span class="metric-value" id="system-type">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">CPU 사용률:</span>
|
||||
<span class="metric-value" id="cpu-usage">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">시스템 메모리:</span>
|
||||
<span class="metric-value" id="system-memory">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">프로세스 수:</span>
|
||||
<span class="metric-value" id="process-count">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🎮 GPU 상태</h3>
|
||||
<div class="metric">
|
||||
<span class="metric-label">GPU 메모리:</span>
|
||||
<span class="metric-value" id="gpu-memory">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">GPU 사용률:</span>
|
||||
<span class="metric-value" id="gpu-util">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">GPU 온도:</span>
|
||||
<span class="metric-value" id="gpu-temp">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">GPU 클럭:</span>
|
||||
<span class="metric-value" id="gpu-clock">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>⚙️ 워커 상태</h3>
|
||||
<div class="metric">
|
||||
<span class="metric-label">활성 워커:</span>
|
||||
<span class="metric-value" id="worker-count">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">세션 풀:</span>
|
||||
<span class="metric-value" id="session-pool">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">대기열:</span>
|
||||
<span class="metric-value" id="queue-size">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">상태:</span>
|
||||
<span class="metric-value" id="worker-status">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📊 API 통계</h3>
|
||||
<div class="metric">
|
||||
<span class="metric-label">총 요청:</span>
|
||||
<span class="metric-value" id="total-requests">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">성공률:</span>
|
||||
<span class="metric-value" id="success-rate">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">평균 응답시간:</span>
|
||||
<span class="metric-value" id="avg-response-time">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">에러 수:</span>
|
||||
<span class="metric-value" id="error-count">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 시스템 성능 상세 -->
|
||||
<div class="card">
|
||||
<h3>🔍 시스템 성능 상세</h3>
|
||||
<div class="system-info">
|
||||
<div>
|
||||
<h4>CPU 정보</h4>
|
||||
<div class="metric">
|
||||
<span class="metric-label">코어 수:</span>
|
||||
<span class="metric-value" id="cpu-count">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">클럭 속도:</span>
|
||||
<span class="metric-value" id="cpu-freq">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">부하 평균 (1분):</span>
|
||||
<span class="metric-value" id="load-1min">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">부하 평균 (5분):</span>
|
||||
<span class="metric-value" id="load-5min">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>디스크 I/O</h4>
|
||||
<div class="metric">
|
||||
<span class="metric-label">읽기 (MB/s):</span>
|
||||
<span class="metric-value" id="disk-read">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">쓰기 (MB/s):</span>
|
||||
<span class="metric-value" id="disk-write">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">읽기 횟수:</span>
|
||||
<span class="metric-value" id="disk-read-count">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">쓰기 횟수:</span>
|
||||
<span class="metric-value" id="disk-write-count">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>네트워크 I/O</h4>
|
||||
<div class="metric">
|
||||
<span class="metric-label">송신 (MB):</span>
|
||||
<span class="metric-value" id="net-sent">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">수신 (MB):</span>
|
||||
<span class="metric-value" id="net-recv">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">송신 패킷:</span>
|
||||
<span class="metric-value" id="net-sent-pkts">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">수신 패킷:</span>
|
||||
<span class="metric-value" id="net-recv-pkts">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 엔드포인트 사용량 -->
|
||||
<div class="card">
|
||||
<h3>🌐 엔드포인트 사용량</h3>
|
||||
<div class="endpoint-usage" id="endpoint-usage">
|
||||
<div class="endpoint-item">
|
||||
<div class="endpoint-name">로딩 중...</div>
|
||||
<div class="endpoint-count">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 알림 및 경고 -->
|
||||
<div class="alerts">
|
||||
<h3>⚠️ 알림 및 경고</h3>
|
||||
<div id="alerts-container">
|
||||
<div class="alert info">
|
||||
<strong>정보:</strong> 모니터링 데이터를 수집 중입니다...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 차트 -->
|
||||
<div class="chart-container">
|
||||
<h3>📈 실시간 성능 차트</h3>
|
||||
<canvas id="performanceChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h3>🎯 GPU 메모리 사용량</h3>
|
||||
<canvas id="gpuChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="refresh-time">
|
||||
마지막 업데이트: <span id="last-update">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 차트 초기화
|
||||
const performanceCtx = document.getElementById('performanceChart').getContext('2d');
|
||||
const gpuCtx = document.getElementById('gpuChart').getContext('2d');
|
||||
|
||||
const performanceChart = new Chart(performanceCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'GPU 사용률 (%)',
|
||||
data: [],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
tension: 0.1
|
||||
}, {
|
||||
label: '시스템 메모리 (%)',
|
||||
data: [],
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const gpuChart = new Chart(gpuCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'GPU 메모리 사용률 (%)',
|
||||
data: [],
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// WebSocket 연결
|
||||
const ws = new WebSocket(`ws://${window.location.host}/ws`);
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
updateDashboard(data);
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
console.log('WebSocket 연결이 종료되었습니다.');
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
function updateDashboard(data) {
|
||||
// 기본 메트릭 업데이트
|
||||
document.getElementById('system-type').textContent = data.system_type;
|
||||
document.getElementById('cpu-usage').textContent = data.system_performance?.cpu?.usage_percent?.toFixed(1) + '%' || '-';
|
||||
document.getElementById('system-memory').textContent = data.system_memory?.usage_percent?.toFixed(1) + '%' || '-';
|
||||
document.getElementById('process-count').textContent = data.system_performance?.processes || '-';
|
||||
|
||||
document.getElementById('gpu-memory').textContent = data.gpu?.usage_percent?.toFixed(1) + '%' || '-';
|
||||
document.getElementById('gpu-util').textContent = data.gpu?.utilization?.toFixed(1) + '%' || '-';
|
||||
document.getElementById('gpu-temp').textContent = data.jetson?.temperature?.gpu?.toFixed(1) + '°C' || '-';
|
||||
document.getElementById('gpu-clock').textContent = data.jetson?.gpu_frequency?.toFixed(0) + 'MHz' || '-';
|
||||
|
||||
document.getElementById('worker-count').textContent = data.workers?.active_workers || '-';
|
||||
document.getElementById('session-pool').textContent = data.sessions?.total_sessions || '-';
|
||||
document.getElementById('queue-size').textContent = data.workers?.queue_size || '-';
|
||||
document.getElementById('worker-status').textContent = data.workers?.status || '-';
|
||||
|
||||
// API 통계 업데이트
|
||||
document.getElementById('total-requests').textContent = data.api_stats?.total_requests || '-';
|
||||
document.getElementById('success-rate').textContent = (data.api_stats?.success_rate?.toFixed(1) || '0') + '%';
|
||||
document.getElementById('avg-response-time').textContent = (data.api_stats?.average_response_time?.toFixed(2) || '0') + 'ms';
|
||||
document.getElementById('error-count').textContent = data.api_stats?.failed_requests || '-';
|
||||
|
||||
// 시스템 성능 상세 업데이트
|
||||
if (data.system_performance?.cpu) {
|
||||
document.getElementById('cpu-count').textContent = data.system_performance.cpu.count || '-';
|
||||
document.getElementById('cpu-freq').textContent = (data.system_performance.cpu.frequency_mhz?.toFixed(0) || '0') + 'MHz';
|
||||
document.getElementById('load-1min').textContent = data.system_performance.cpu.load_average?.toFixed(2) || '-';
|
||||
document.getElementById('load-5min').textContent = data.system_performance.cpu.load_average?.toFixed(2) || '-';
|
||||
}
|
||||
|
||||
if (data.system_performance?.disk) {
|
||||
document.getElementById('disk-read').textContent = (data.system_performance.disk.read_bytes / 1024 / 1024).toFixed(2);
|
||||
document.getElementById('disk-write').textContent = (data.system_performance.disk.write_bytes / 1024 / 1024).toFixed(2);
|
||||
document.getElementById('disk-read-count').textContent = data.system_performance.disk.read_count || '-';
|
||||
document.getElementById('disk-write-count').textContent = data.system_performance.disk.write_count || '-';
|
||||
}
|
||||
|
||||
if (data.system_performance?.network) {
|
||||
document.getElementById('net-sent').textContent = (data.system_performance.network.bytes_sent / 1024 / 1024).toFixed(2);
|
||||
document.getElementById('net-recv').textContent = (data.system_performance.network.bytes_recv / 1024 / 1024).toFixed(2);
|
||||
document.getElementById('net-sent-pkts').textContent = data.system_performance.network.packets_sent || '-';
|
||||
document.getElementById('net-recv-pkts').textContent = data.system_performance.network.packets_recv || '-';
|
||||
}
|
||||
|
||||
// 엔드포인트 사용량 업데이트
|
||||
updateEndpointUsage(data.api_stats?.endpoint_usage || {});
|
||||
|
||||
// 알림 업데이트
|
||||
updateAlerts(data.alerts || []);
|
||||
|
||||
// 차트 업데이트
|
||||
updateCharts(data);
|
||||
|
||||
// 마지막 업데이트 시간
|
||||
document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
function updateEndpointUsage(endpointUsage) {
|
||||
const container = document.getElementById('endpoint-usage');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (Object.keys(endpointUsage).length === 0) {
|
||||
container.innerHTML = '<div class="endpoint-item"><div class="endpoint-name">사용량 없음</div><div class="endpoint-count">-</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(endpointUsage).forEach(([endpoint, count]) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'endpoint-item';
|
||||
item.innerHTML = `
|
||||
<div class="endpoint-name">${endpoint}</div>
|
||||
<div class="endpoint-count">${count}</div>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function updateAlerts(alerts) {
|
||||
const container = document.getElementById('alerts-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (alerts.length === 0) {
|
||||
container.innerHTML = '<div class="alert info"><strong>정보:</strong> 현재 알림이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
alerts.forEach(alert => {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert ${alert.level}`;
|
||||
alertDiv.innerHTML = `
|
||||
<strong>${alert.level.toUpperCase()}:</strong> ${alert.message}
|
||||
<br><small>${new Date(alert.timestamp).toLocaleString()}</small>
|
||||
`;
|
||||
container.appendChild(alertDiv);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCharts(data) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
// 성능 차트 업데이트
|
||||
performanceChart.data.labels.push(timestamp);
|
||||
performanceChart.data.datasets[0].data.push(data.gpu?.utilization || 0);
|
||||
performanceChart.data.datasets[1].data.push(data.system_memory?.usage_percent || 0);
|
||||
|
||||
if (performanceChart.data.labels.length > 20) {
|
||||
performanceChart.data.labels.shift();
|
||||
performanceChart.data.datasets[0].data.shift();
|
||||
performanceChart.data.datasets[1].data.shift();
|
||||
}
|
||||
|
||||
// GPU 차트 업데이트
|
||||
gpuChart.data.labels.push(timestamp);
|
||||
gpuChart.data.datasets[0].data.push(data.gpu?.usage_percent || 0);
|
||||
|
||||
if (gpuChart.data.labels.length > 20) {
|
||||
gpuChart.data.labels.shift();
|
||||
gpuChart.data.datasets[0].data.shift();
|
||||
}
|
||||
|
||||
performanceChart.update();
|
||||
gpuChart.update();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
@monitor_app.get("/")
|
||||
async def dashboard():
|
||||
"""대시보드 HTML 페이지"""
|
||||
return HTMLResponse(content=HTML_TEMPLATE)
|
||||
|
||||
|
||||
@monitor_app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""WebSocket 연결을 처리합니다."""
|
||||
await websocket.accept()
|
||||
connected_clients.append(websocket)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# 클라이언트로부터 메시지 대기 (연결 유지용)
|
||||
await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
connected_clients.remove(websocket)
|
||||
logger.info("클라이언트 연결 해제")
|
||||
|
||||
|
||||
@monitor_app.get("/api/status")
|
||||
async def get_current_status():
|
||||
"""현재 상태를 JSON으로 반환합니다."""
|
||||
return await monitoring_data.collect_data()
|
||||
|
||||
|
||||
@monitor_app.get("/api/history")
|
||||
async def get_history():
|
||||
"""데이터 히스토리를 반환합니다."""
|
||||
return {
|
||||
"history": monitoring_data.get_history(),
|
||||
"statistics": monitoring_data.get_statistics()
|
||||
}
|
||||
|
||||
|
||||
async def broadcast_data():
|
||||
"""연결된 모든 클라이언트에게 데이터를 브로드캐스트합니다."""
|
||||
while True:
|
||||
try:
|
||||
if connected_clients:
|
||||
data = await monitoring_data.collect_data()
|
||||
message = json.dumps(data, ensure_ascii=False)
|
||||
|
||||
# 연결이 끊어진 클라이언트 제거
|
||||
disconnected = []
|
||||
for client in connected_clients:
|
||||
try:
|
||||
await client.send_text(message)
|
||||
except Exception:
|
||||
disconnected.append(client)
|
||||
|
||||
for client in disconnected:
|
||||
connected_clients.remove(client)
|
||||
|
||||
await asyncio.sleep(2) # 2초마다 업데이트
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"브로드캐스트 오류: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
@monitor_app.on_event("startup")
|
||||
async def start_monitoring():
|
||||
"""모니터링 시작"""
|
||||
logger.info("모니터링 대시보드 시작")
|
||||
|
||||
# Jetson 최적화 (시작 시)
|
||||
if settings.IS_JETSON:
|
||||
logger.info("Jetson Xavier 모드로 모니터링 시작")
|
||||
gpu_monitor.optimize_for_jetson()
|
||||
|
||||
asyncio.create_task(broadcast_data())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
# 모니터링 서버 실행
|
||||
uvicorn.run(
|
||||
"app.monitoring.dashboard:monitor_app",
|
||||
host="0.0.0.0",
|
||||
port=settings.MONITORING_PORT,
|
||||
log_level="info"
|
||||
)
|
||||
|
|
@ -0,0 +1 @@
|
|||
# 유틸리티 모듈
|
||||
|
|
@ -0,0 +1,517 @@
|
|||
"""
|
||||
GPU 메모리 모니터링 유틸리티
|
||||
Jetson Xavier와 x86 시스템을 모두 지원합니다.
|
||||
"""
|
||||
import asyncio
|
||||
import psutil
|
||||
import logging
|
||||
import subprocess
|
||||
import os
|
||||
from typing import Dict, Optional, List
|
||||
try:
|
||||
import pynvml
|
||||
NVML_AVAILABLE = True
|
||||
except ImportError:
|
||||
NVML_AVAILABLE = False
|
||||
logging.warning("pynvml not available. GPU monitoring will be limited.")
|
||||
|
||||
from ..core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JetsonMonitor:
|
||||
"""Jetson Xavier 전용 모니터링 클래스"""
|
||||
|
||||
def __init__(self):
|
||||
self.jetson_clocks_path = "/sys/kernel/debug/clk"
|
||||
self.jetson_thermal_path = "/sys/devices/virtual/thermal"
|
||||
self.jetson_power_path = "/sys/kernel/debug/tegra_pcie/pcie_power"
|
||||
|
||||
def get_gpu_memory_info(self) -> Dict[str, float]:
|
||||
"""Jetson 전용 GPU 메모리 정보를 가져옵니다."""
|
||||
try:
|
||||
# 1. Jetson GPU 클래스에서 정보 읽기
|
||||
if os.path.exists("/sys/class/nvidia-gpu"):
|
||||
try:
|
||||
# Jetson GPU 디바이스 정보 확인
|
||||
gpu_devices = [d for d in os.listdir("/sys/class/nvidia-gpu") if d.startswith("nvidia")]
|
||||
if gpu_devices:
|
||||
logger.debug(f"Jetson GPU devices found: {gpu_devices}")
|
||||
|
||||
# GPU 메모리 정보 수집
|
||||
total_memory = 0
|
||||
used_memory = 0
|
||||
|
||||
for device in gpu_devices:
|
||||
device_path = f"/sys/class/nvidia-gpu/{device}"
|
||||
|
||||
# 메모리 정보 파일들 확인
|
||||
memory_files = [
|
||||
"total_memory",
|
||||
"memory_used",
|
||||
"memory_free",
|
||||
"memory_usage"
|
||||
]
|
||||
|
||||
for mem_file in memory_files:
|
||||
file_path = f"{device_path}/{mem_file}"
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
with open(file_path, "r") as f:
|
||||
value = f.read().strip()
|
||||
logger.debug(f"{mem_file}: {value}")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 실제 메모리 정보가 있으면 반환
|
||||
if total_memory > 0:
|
||||
return {
|
||||
"total": round(total_memory / 1024, 2),
|
||||
"used": round(used_memory / 1024, 2),
|
||||
"free": round((total_memory - used_memory) / 1024, 2),
|
||||
"usage_percent": round((used_memory / total_memory) * 100, 2) if total_memory > 0 else 0
|
||||
}
|
||||
except Exception as e:
|
||||
logger.debug(f"Jetson GPU class read failed: {e}")
|
||||
|
||||
# 2. /sys/kernel/debug/gpu/memory에서 읽기 시도 (권한이 있는 경우)
|
||||
if os.path.exists("/sys/kernel/debug/gpu/memory"):
|
||||
try:
|
||||
with open("/sys/kernel/debug/gpu/memory", "r") as f:
|
||||
content = f.read()
|
||||
logger.debug(f"GPU memory debug info: {content}")
|
||||
|
||||
# 메모리 정보 파싱
|
||||
lines = content.split('\n')
|
||||
total_mb = 0
|
||||
used_mb = 0
|
||||
|
||||
for line in lines:
|
||||
if "Total" in line and "MB" in line:
|
||||
try:
|
||||
total_mb = float(line.split()[-2])
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
elif "Used" in line and "MB" in line:
|
||||
try:
|
||||
used_mb = float(line.split()[-2])
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
if total_mb > 0:
|
||||
free_mb = total_mb - used_mb
|
||||
usage_percent = (used_mb / total_mb) * 100
|
||||
return {
|
||||
"total": round(total_mb / 1024, 2), # GB
|
||||
"used": round(used_mb / 1024, 2), # GB
|
||||
"free": round(free_mb / 1024, 2), # GB
|
||||
"usage_percent": round(usage_percent, 2)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"GPU memory debug read failed: {e}")
|
||||
|
||||
# 3. tegrastats 사용 (가장 안정적)
|
||||
if self._tegrastats_available():
|
||||
return self._get_memory_from_tegrastats()
|
||||
|
||||
# 4. GV11B GPU 정보 확인 (Jetson Xavier)
|
||||
if os.path.exists("/sys/firmware/devicetree/base/gv11b"):
|
||||
logger.info("GV11B GPU (Jetson Xavier) 감지됨")
|
||||
# Jetson Xavier는 통합 메모리 사용
|
||||
return {
|
||||
"total": 8.0, # 8GB 통합 메모리
|
||||
"used": 0.0,
|
||||
"free": 8.0,
|
||||
"usage_percent": 0.0
|
||||
}
|
||||
|
||||
# 5. 기본값 반환
|
||||
logger.warning("GPU 메모리 정보를 가져올 수 없습니다. 기본값을 사용합니다.")
|
||||
return {
|
||||
"total": 8.0,
|
||||
"used": 0.0,
|
||||
"free": 8.0,
|
||||
"usage_percent": 0.0
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Jetson GPU memory read failed: {e}")
|
||||
return {
|
||||
"total": 8.0,
|
||||
"used": 0.0,
|
||||
"free": 8.0,
|
||||
"usage_percent": 0.0
|
||||
}
|
||||
|
||||
def _tegrastats_available(self) -> bool:
|
||||
"""tegrastats 명령어 사용 가능 여부 확인"""
|
||||
try:
|
||||
result = subprocess.run(["which", "tegrastats"],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
return result.returncode == 0
|
||||
except:
|
||||
return False
|
||||
|
||||
def _get_memory_from_tegrastats(self) -> Dict[str, float]:
|
||||
"""tegrastats에서 메모리 정보 추출"""
|
||||
try:
|
||||
# tegrastats -1 (한 번만 실행)
|
||||
result = subprocess.run(["tegrastats", "-1"],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
output = result.stdout
|
||||
|
||||
# GPU 메모리 정보 파싱
|
||||
# 예시: "GR3D_FREQ 0% @ 114MHz GR3D_FREQ 0% @ 114MHz"
|
||||
# "RAM 0/8192MB (lfb 0x0) @ 1600MHz"
|
||||
|
||||
total_gb = 8.0 # Jetson Xavier 기본값
|
||||
used_gb = 0.0
|
||||
|
||||
# RAM 사용량 파싱
|
||||
for line in output.split('\n'):
|
||||
if "RAM" in line and "MB" in line:
|
||||
try:
|
||||
# "RAM 1024/8192MB" 형태에서 추출
|
||||
parts = line.split()
|
||||
for part in parts:
|
||||
if "/" in part and "MB" in part:
|
||||
used_str, total_str = part.split('/')
|
||||
used_mb = float(used_str)
|
||||
total_mb = float(total_str.replace('MB', ''))
|
||||
used_gb = used_mb / 1024
|
||||
total_gb = total_mb / 1024
|
||||
break
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
free_gb = total_gb - used_gb
|
||||
usage_percent = (used_gb / total_gb) * 100 if total_gb > 0 else 0
|
||||
|
||||
return {
|
||||
"total": round(total_gb, 2),
|
||||
"used": round(used_gb, 2),
|
||||
"free": round(free_gb, 2),
|
||||
"usage_percent": round(usage_percent, 2)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"tegrastats parsing failed: {e}")
|
||||
|
||||
# 기본값 반환
|
||||
return {
|
||||
"total": 8.0,
|
||||
"used": 0.0,
|
||||
"free": 8.0,
|
||||
"usage_percent": 0.0
|
||||
}
|
||||
|
||||
def get_gpu_utilization(self) -> float:
|
||||
"""Jetson 전용 GPU 사용률을 가져옵니다."""
|
||||
try:
|
||||
# tegrastats에서 GPU 사용률 추출
|
||||
if self._tegrastats_available():
|
||||
result = subprocess.run(["tegrastats", "-1"],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
output = result.stdout
|
||||
|
||||
# GR3D_FREQ (GPU 사용률) 파싱
|
||||
for line in output.split('\n'):
|
||||
if "GR3D_FREQ" in line and "%" in line:
|
||||
try:
|
||||
# "GR3D_FREQ 45% @ 114MHz" 형태에서 추출
|
||||
parts = line.split()
|
||||
for part in parts:
|
||||
if "%" in part:
|
||||
usage = float(part.replace('%', ''))
|
||||
return min(usage, 100.0) # 100% 초과 방지
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
# 대안: /sys/kernel/debug/gpu/load에서 읽기
|
||||
if os.path.exists("/sys/kernel/debug/gpu/load"):
|
||||
try:
|
||||
with open("/sys/kernel/debug/gpu/load", "r") as f:
|
||||
load = f.read().strip()
|
||||
if load.isdigit():
|
||||
return min(float(load), 100.0)
|
||||
except:
|
||||
pass
|
||||
|
||||
return 0.0
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Jetson GPU utilization read failed: {e}")
|
||||
return 0.0
|
||||
|
||||
def get_gpu_frequency(self) -> Optional[int]:
|
||||
"""GPU 클럭 주파수를 가져옵니다 (MHz)"""
|
||||
try:
|
||||
if os.path.exists(f"{self.jetson_clocks_path}/gpcclk/clk_rate"):
|
||||
with open(f"{self.jetson_clocks_path}/gpcclk/clk_rate", "r") as f:
|
||||
freq = int(f.read().strip()) // 1000000 # Hz to MHz
|
||||
return freq
|
||||
except Exception as e:
|
||||
logger.debug(f"GPU frequency read failed: {e}")
|
||||
return None
|
||||
|
||||
def get_cpu_frequency(self) -> Optional[int]:
|
||||
"""CPU 클럭 주파수를 가져옵니다 (MHz)"""
|
||||
try:
|
||||
if os.path.exists(f"{self.jetson_clocks_path}/cpu_gpcclk/clk_rate"):
|
||||
with open(f"{self.jetson_clocks_path}/cpu_gpcclk/clk_rate", "r") as f:
|
||||
freq = int(f.read().strip()) // 1000000 # Hz to MHz
|
||||
return freq
|
||||
except Exception as e:
|
||||
logger.debug(f"CPU frequency read failed: {e}")
|
||||
return None
|
||||
|
||||
def get_memory_frequency(self) -> Optional[int]:
|
||||
"""메모리 클럭 주파수를 가져옵니다 (MHz)"""
|
||||
try:
|
||||
if os.path.exists(f"{self.jetson_clocks_path}/emc/clk_rate"):
|
||||
with open(f"{self.jetson_clocks_path}/emc/clk_rate", "r") as f:
|
||||
freq = int(f.read().strip()) // 1000000 # Hz to MHz
|
||||
return freq
|
||||
except Exception as e:
|
||||
logger.debug(f"Memory frequency read failed: {e}")
|
||||
return None
|
||||
|
||||
def get_temperature(self) -> Dict[str, float]:
|
||||
"""Jetson 온도 정보를 가져옵니다"""
|
||||
temps = {}
|
||||
try:
|
||||
if os.path.exists(self.jetson_thermal_path):
|
||||
for item in os.listdir(self.jetson_thermal_path):
|
||||
if item.startswith("thermal_zone"):
|
||||
temp_file = f"{self.jetson_thermal_path}/{item}/temp"
|
||||
if os.path.exists(temp_file):
|
||||
with open(temp_file, "r") as f:
|
||||
temp = int(f.read().strip()) / 1000.0 # mC to C
|
||||
zone_name = f"zone_{item.split('_')[-1]}"
|
||||
temps[zone_name] = temp
|
||||
except Exception as e:
|
||||
logger.debug(f"Temperature read failed: {e}")
|
||||
return temps
|
||||
|
||||
def get_power_consumption(self) -> Optional[float]:
|
||||
"""전력 소비량을 가져옵니다 (W)"""
|
||||
try:
|
||||
# Jetson 전력 모니터링 (가능한 경우)
|
||||
if os.path.exists("/sys/bus/i2c/devices/1-0040/iio_device/in_power0_input"):
|
||||
with open("/sys/bus/i2c/devices/1-0040/iio_device/in_power0_input", "r") as f:
|
||||
power = float(f.read().strip()) / 1000.0 # mW to W
|
||||
return power
|
||||
except Exception as e:
|
||||
logger.debug(f"Power consumption read failed: {e}")
|
||||
return None
|
||||
|
||||
def set_power_mode(self, mode: str) -> bool:
|
||||
"""전력 모드를 설정합니다"""
|
||||
try:
|
||||
if mode in ["MAXN", "5W", "10W", "15W"]:
|
||||
result = subprocess.run(
|
||||
["sudo", "nvpmodel", "-m", mode],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode == 0:
|
||||
logger.info(f"Power mode set to {mode}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Failed to set power mode: {result.stderr}")
|
||||
else:
|
||||
logger.error(f"Invalid power mode: {mode}")
|
||||
except Exception as e:
|
||||
logger.error(f"Power mode setting failed: {e}")
|
||||
return False
|
||||
|
||||
def set_fan_speed(self, speed: int) -> bool:
|
||||
"""팬 속도를 설정합니다 (0-255)"""
|
||||
try:
|
||||
if 0 <= speed <= 255:
|
||||
fan_path = "/sys/devices/pwm-fan/target_pwm"
|
||||
if os.path.exists(fan_path):
|
||||
with open(fan_path, "w") as f:
|
||||
f.write(str(speed))
|
||||
logger.info(f"Fan speed set to {speed}")
|
||||
return True
|
||||
else:
|
||||
logger.warning("Fan control not available")
|
||||
else:
|
||||
logger.error(f"Invalid fan speed: {speed}")
|
||||
except Exception as e:
|
||||
logger.error(f"Fan speed setting failed: {e}")
|
||||
return False
|
||||
|
||||
def get_jetson_info(self) -> Dict[str, any]:
|
||||
"""Jetson 전체 정보를 가져옵니다"""
|
||||
info = {
|
||||
"gpu_frequency": self.get_gpu_frequency(),
|
||||
"cpu_frequency": self.get_cpu_frequency(),
|
||||
"memory_frequency": self.get_memory_frequency(),
|
||||
"temperature": self.get_temperature(),
|
||||
"power_consumption": self.get_power_consumption(),
|
||||
"power_mode": self._get_current_power_mode()
|
||||
}
|
||||
return info
|
||||
|
||||
def _get_current_power_mode(self) -> str:
|
||||
"""현재 전력 모드를 가져옵니다"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["nvpmodel", "-q"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'NV Power Mode:' in line:
|
||||
return line.split(':')[-1].strip()
|
||||
except Exception:
|
||||
pass
|
||||
return "Unknown"
|
||||
|
||||
|
||||
class GPUMonitor:
|
||||
def __init__(self):
|
||||
self.initialized = False
|
||||
self.is_jetson = settings.IS_JETSON
|
||||
self.jetson_monitor = JetsonMonitor() if self.is_jetson else None
|
||||
|
||||
if NVML_AVAILABLE and not self.is_jetson:
|
||||
try:
|
||||
pynvml.nvmlInit()
|
||||
self.initialized = True
|
||||
logger.info("GPU monitoring initialized successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize GPU monitoring: {e}")
|
||||
elif self.is_jetson:
|
||||
logger.info("Jetson Xavier mode detected - using Jetson-specific monitoring")
|
||||
self.initialized = True
|
||||
|
||||
def get_gpu_memory_info(self, device_id: int = 0) -> Dict[str, float]:
|
||||
"""GPU 메모리 정보를 반환합니다."""
|
||||
if self.is_jetson:
|
||||
return self.jetson_monitor.get_gpu_memory_info()
|
||||
|
||||
if not self.initialized or not NVML_AVAILABLE:
|
||||
return {"total": 0, "used": 0, "free": 0, "usage_percent": 0}
|
||||
|
||||
try:
|
||||
handle = pynvml.nvmlDeviceGetHandleByIndex(device_id)
|
||||
mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
|
||||
|
||||
total = mem_info.total / 1024**3 # GB
|
||||
used = mem_info.used / 1024**3 # GB
|
||||
free = mem_info.free / 1024**3 # GB
|
||||
usage_percent = (used / total) * 100
|
||||
|
||||
return {
|
||||
"total": round(total, 2),
|
||||
"used": round(used, 2),
|
||||
"free": round(free, 2),
|
||||
"usage_percent": round(usage_percent, 2)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting GPU memory info: {e}")
|
||||
return {"total": 0, "used": 0, "free": 0, "usage_percent": 0}
|
||||
|
||||
def get_gpu_utilization(self, device_id: int = 0) -> float:
|
||||
"""GPU 사용률을 반환합니다."""
|
||||
if self.is_jetson:
|
||||
return self.jetson_monitor.get_gpu_utilization()
|
||||
|
||||
if not self.initialized or not NVML_AVAILABLE:
|
||||
return 0.0
|
||||
|
||||
try:
|
||||
handle = pynvml.nvmlDeviceGetHandleByIndex(device_id)
|
||||
util = pynvml.nvmlDeviceGetUtilizationRates(handle)
|
||||
return float(util.gpu)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting GPU utilization: {e}")
|
||||
return 0.0
|
||||
|
||||
def get_system_memory_info(self) -> Dict[str, float]:
|
||||
"""시스템 메모리 정보를 반환합니다."""
|
||||
mem = psutil.virtual_memory()
|
||||
return {
|
||||
"total": round(mem.total / 1024**3, 2), # GB
|
||||
"used": round(mem.used / 1024**3, 2), # GB
|
||||
"free": round(mem.free / 1024**3, 2), # GB
|
||||
"usage_percent": round(mem.percent, 2)
|
||||
}
|
||||
|
||||
def get_jetson_specific_info(self) -> Dict[str, any]:
|
||||
"""Jetson 전용 정보를 반환합니다."""
|
||||
if not self.is_jetson or not self.jetson_monitor:
|
||||
return {}
|
||||
|
||||
return self.jetson_monitor.get_jetson_info()
|
||||
|
||||
def should_scale_up(self, vram_usage: float, threshold: float) -> bool:
|
||||
"""스케일 업 여부를 결정합니다."""
|
||||
return vram_usage < threshold
|
||||
|
||||
def should_scale_down(self, vram_usage: float, threshold: float) -> bool:
|
||||
"""스케일 다운 여부를 결정합니다."""
|
||||
return vram_usage > threshold
|
||||
|
||||
def optimize_for_jetson(self) -> bool:
|
||||
"""Jetson 최적화를 수행합니다."""
|
||||
if not self.is_jetson or not self.jetson_monitor:
|
||||
return False
|
||||
|
||||
try:
|
||||
# 전력 모드 설정
|
||||
power_mode = settings.JETSON_POWER_MODE
|
||||
if power_mode != "MAXN":
|
||||
self.jetson_monitor.set_power_mode(power_mode)
|
||||
|
||||
# 팬 제어 활성화
|
||||
if settings.JETSON_FAN_CONTROL:
|
||||
# 온도에 따른 팬 속도 조정
|
||||
temps = self.jetson_monitor.get_temperature()
|
||||
max_temp = max(temps.values()) if temps else 0
|
||||
|
||||
if max_temp > settings.JETSON_TEMP_THRESHOLD:
|
||||
self.jetson_monitor.set_fan_speed(255) # 최대 속도
|
||||
elif max_temp > 60:
|
||||
self.jetson_monitor.set_fan_speed(128) # 중간 속도
|
||||
else:
|
||||
self.jetson_monitor.set_fan_speed(64) # 낮은 속도
|
||||
|
||||
logger.info("Jetson optimization completed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Jetson optimization failed: {e}")
|
||||
return False
|
||||
|
||||
def get_comprehensive_gpu_info(self) -> Dict[str, any]:
|
||||
"""GPU와 Jetson 정보를 종합적으로 반환합니다."""
|
||||
gpu_info = {
|
||||
"memory": self.get_gpu_memory_info(),
|
||||
"utilization": self.get_gpu_utilization(),
|
||||
"system_memory": self.get_system_memory_info()
|
||||
}
|
||||
|
||||
if self.is_jetson:
|
||||
gpu_info["jetson"] = self.get_jetson_specific_info()
|
||||
gpu_info["platform"] = "Jetson Xavier"
|
||||
else:
|
||||
gpu_info["platform"] = "x86_64"
|
||||
|
||||
return gpu_info
|
||||
|
||||
|
||||
# 전역 GPU 모니터 인스턴스
|
||||
gpu_monitor = GPUMonitor()
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
"""
|
||||
이미지 처리 유틸리티
|
||||
iopaint와 호환되는 이미지 변환 및 처리 함수들을 제공합니다.
|
||||
"""
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
from typing import Tuple, Optional, Union
|
||||
import numpy as np
|
||||
import cv2
|
||||
from PIL import Image
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def encode_image_to_base64(image: Union[np.ndarray, Image.Image], format: str = "PNG") -> str:
|
||||
"""이미지를 base64로 인코딩합니다."""
|
||||
try:
|
||||
if isinstance(image, np.ndarray):
|
||||
# numpy 배열을 PIL Image로 변환
|
||||
if image.dtype != np.uint8:
|
||||
image = (image * 255).astype(np.uint8)
|
||||
|
||||
if len(image.shape) == 3 and image.shape[2] == 3:
|
||||
# BGR to RGB
|
||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
|
||||
pil_image = Image.fromarray(image)
|
||||
else:
|
||||
pil_image = image
|
||||
|
||||
# PIL Image를 base64로 인코딩
|
||||
buffer = io.BytesIO()
|
||||
pil_image.save(buffer, format=format)
|
||||
img_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
|
||||
return img_str
|
||||
except Exception as e:
|
||||
logger.error(f"이미지 base64 인코딩 실패: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def decode_base64_to_image(base64_string: str, gray: bool = False) -> Tuple[np.ndarray, Optional[np.ndarray], dict, str]:
|
||||
"""base64 문자열을 이미지로 디코딩합니다 (iopaint 호환)."""
|
||||
try:
|
||||
# base64 디코딩
|
||||
image_data = base64.b64decode(base64_string)
|
||||
|
||||
# PIL Image로 로드
|
||||
pil_image = Image.open(io.BytesIO(image_data))
|
||||
|
||||
# 이미지 정보 추출
|
||||
info = {
|
||||
"format": pil_image.format,
|
||||
"mode": pil_image.mode,
|
||||
"size": pil_image.size,
|
||||
"parameters": ""
|
||||
}
|
||||
|
||||
# 확장자 결정
|
||||
ext = pil_image.format.lower() if pil_image.format else "png"
|
||||
|
||||
# numpy 배열로 변환
|
||||
if gray:
|
||||
# 그레이스케일로 변환
|
||||
if pil_image.mode in ('RGBA', 'LA', 'P'):
|
||||
pil_image = pil_image.convert('L')
|
||||
image_array = np.array(pil_image)
|
||||
else:
|
||||
# RGB로 변환
|
||||
if pil_image.mode == 'RGBA':
|
||||
# RGBA를 RGB로 변환하고 알파 채널 분리
|
||||
rgb_image = pil_image.convert('RGB')
|
||||
alpha_channel = np.array(pil_image.split()[-1])
|
||||
image_array = np.array(rgb_image)
|
||||
elif pil_image.mode == 'P':
|
||||
# 팔레트 이미지를 RGB로 변환
|
||||
image_array = np.array(pil_image.convert('RGB'))
|
||||
alpha_channel = None
|
||||
else:
|
||||
# RGB 이미지
|
||||
image_array = np.array(pil_image)
|
||||
alpha_channel = None
|
||||
|
||||
# BGR로 변환 (OpenCV 호환)
|
||||
if len(image_array.shape) == 3 and image_array.shape[2] == 3:
|
||||
image_array = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR)
|
||||
|
||||
return image_array, alpha_channel, info, ext
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"base64 이미지 디코딩 실패: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def concat_alpha_channel(image: np.ndarray, alpha_channel: Optional[np.ndarray]) -> np.ndarray:
|
||||
"""이미지와 알파 채널을 결합합니다."""
|
||||
if alpha_channel is None:
|
||||
return image
|
||||
|
||||
try:
|
||||
# BGR to RGB
|
||||
if len(image.shape) == 3 and image.shape[2] == 3:
|
||||
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
else:
|
||||
rgb_image = image
|
||||
|
||||
# RGBA 이미지 생성
|
||||
if len(rgb_image.shape) == 3:
|
||||
rgba_image = np.dstack((rgb_image, alpha_channel))
|
||||
else:
|
||||
rgba_image = np.dstack((rgb_image, rgb_image, rgb_image, alpha_channel))
|
||||
|
||||
return rgba_image
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"알파 채널 결합 실패: {e}")
|
||||
return image
|
||||
|
||||
|
||||
def pil_to_bytes(image: Image.Image, ext: str = "PNG", quality: int = 100, infos: dict = None) -> bytes:
|
||||
"""PIL Image를 바이트로 변환합니다."""
|
||||
try:
|
||||
buffer = io.BytesIO()
|
||||
|
||||
if ext.lower() in ['jpg', 'jpeg']:
|
||||
image.save(buffer, format='JPEG', quality=quality, optimize=True)
|
||||
else:
|
||||
image.save(buffer, format=ext.upper(), optimize=True)
|
||||
|
||||
return buffer.getvalue()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PIL Image를 바이트로 변환 실패: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def numpy_to_bytes(image: np.ndarray, ext: str = "PNG", quality: int = 100) -> bytes:
|
||||
"""numpy 배열을 바이트로 변환합니다."""
|
||||
try:
|
||||
# numpy 배열을 PIL Image로 변환
|
||||
if image.dtype != np.uint8:
|
||||
image = (image * 255).astype(np.uint8)
|
||||
|
||||
if len(image.shape) == 3 and image.shape[2] == 3:
|
||||
# BGR to RGB
|
||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
|
||||
pil_image = Image.fromarray(image)
|
||||
return pil_to_bytes(pil_image, ext, quality)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"numpy 배열을 바이트로 변환 실패: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def adjust_mask(mask: np.ndarray, kernel_size: int = 5, operate: str = "dilate") -> np.ndarray:
|
||||
"""마스크를 조정합니다 (dilate/erode)."""
|
||||
try:
|
||||
if kernel_size <= 0:
|
||||
return mask
|
||||
|
||||
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
|
||||
|
||||
if operate.lower() == "dilate":
|
||||
result = cv2.dilate(mask, kernel, iterations=1)
|
||||
elif operate.lower() == "erode":
|
||||
result = cv2.erode(mask, kernel, iterations=1)
|
||||
else:
|
||||
logger.warning(f"알 수 없는 마스크 연산: {operate}")
|
||||
return mask
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"마스크 조정 실패: {e}")
|
||||
return mask
|
||||
|
||||
|
||||
def gen_frontend_mask(mask: np.ndarray) -> np.ndarray:
|
||||
"""프론트엔드용 마스크를 생성합니다."""
|
||||
try:
|
||||
# 마스크를 0-255 범위로 정규화
|
||||
if mask.dtype != np.uint8:
|
||||
mask = (mask * 255).astype(np.uint8)
|
||||
|
||||
# 이진화
|
||||
_, binary_mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)
|
||||
|
||||
return binary_mask
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"프론트엔드 마스크 생성 실패: {e}")
|
||||
return mask
|
||||
|
||||
|
||||
def resize_image(image: np.ndarray, target_size: Tuple[int, int], keep_aspect: bool = True) -> np.ndarray:
|
||||
"""이미지 크기를 조정합니다."""
|
||||
try:
|
||||
if keep_aspect:
|
||||
# 종횡비 유지하면서 크기 조정
|
||||
h, w = image.shape[:2]
|
||||
target_h, target_w = target_size
|
||||
|
||||
# 종횡비 계산
|
||||
aspect = w / h
|
||||
target_aspect = target_w / target_h
|
||||
|
||||
if aspect > target_aspect:
|
||||
# 너비에 맞춤
|
||||
new_w = target_w
|
||||
new_h = int(target_w / aspect)
|
||||
else:
|
||||
# 높이에 맞춤
|
||||
new_h = target_h
|
||||
new_w = int(target_h * aspect)
|
||||
|
||||
resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
|
||||
|
||||
# 패딩으로 목표 크기 맞춤
|
||||
result = np.zeros((target_h, target_w, 3) if len(image.shape) == 3 else (target_h, target_w), dtype=image.dtype)
|
||||
y_offset = (target_h - new_h) // 2
|
||||
x_offset = (target_w - new_w) // 2
|
||||
result[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = resized
|
||||
|
||||
return result
|
||||
else:
|
||||
# 종횡비 무시하고 크기 조정
|
||||
return cv2.resize(image, target_size, interpolation=cv2.INTER_LANCZOS4)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"이미지 크기 조정 실패: {e}")
|
||||
return image
|
||||
|
||||
|
||||
def validate_image_size(image: np.ndarray, max_size: int) -> bool:
|
||||
"""이미지 크기가 제한을 초과하지 않는지 확인합니다."""
|
||||
try:
|
||||
h, w = image.shape[:2]
|
||||
pixels = h * w
|
||||
max_pixels = max_size * max_size
|
||||
|
||||
return pixels <= max_pixels
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"이미지 크기 검증 실패: {e}")
|
||||
return False
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
Traceback (most recent call last):
|
||||
File "/usr/lib/python3.8/runpy.py", line 194, in _run_module_as_main
|
||||
return _run_code(code, main_globals, None,
|
||||
File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
|
||||
exec(code, run_globals)
|
||||
File "/home/ckh08045/.local/lib/python3.8/site-packages/uvicorn/__main__.py", line 4, in <module>
|
||||
uvicorn.main()
|
||||
File "/home/ckh08045/.local/lib/python3.8/site-packages/click/core.py", line 1161, in __call__
|
||||
return self.main(*args, **kwargs)
|
||||
File "/home/ckh08045/.local/lib/python3.8/site-packages/click/core.py", line 1082, in main
|
||||
rv = self.invoke(ctx)
|
||||
File "/home/ckh08045/.local/lib/python3.8/site-packages/click/core.py", line 1443, in invoke
|
||||
return ctx.invoke(self.callback, **ctx.params)
|
||||
File "/home/ckh08045/.local/lib/python3.8/site-packages/click/core.py", line 788, in invoke
|
||||
return __callback(*args, **kwargs)
|
||||
File "/home/ckh08045/.local/lib/python3.8/site-packages/uvicorn/main.py", line 426, in main
|
||||
run(app, **kwargs)
|
||||
File "/home/ckh08045/.local/lib/python3.8/site-packages/uvicorn/main.py", line 452, in run
|
||||
server.run()
|
||||
File "/home/ckh08045/.local/lib/python3.8/site-packages/uvicorn/server.py", line 68, in run
|
||||
return asyncio.run(self.serve(sockets=sockets))
|
||||
File "/usr/lib/python3.8/asyncio/runners.py", line 44, in run
|
||||
return loop.run_until_complete(main)
|
||||
File "/usr/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
|
||||
return future.result()
|
||||
File "/home/ckh08045/.local/lib/python3.8/site-packages/uvicorn/server.py", line 76, in serve
|
||||
config.load()
|
||||
File "/home/ckh08045/.local/lib/python3.8/site-packages/uvicorn/config.py", line 456, in load
|
||||
self.loaded_app = import_from_string(self.app)
|
||||
File "/home/ckh08045/.local/lib/python3.8/site-packages/uvicorn/importer.py", line 21, in import_from_string
|
||||
module = importlib.import_module(module_str)
|
||||
File "/usr/lib/python3.8/importlib/__init__.py", line 127, in import_module
|
||||
return _bootstrap._gcd_import(name[level:], package, level)
|
||||
File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
|
||||
File "<frozen importlib._bootstrap>", line 991, in _find_and_load
|
||||
File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
|
||||
File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
|
||||
File "<frozen importlib._bootstrap_external>", line 848, in exec_module
|
||||
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
|
||||
File "/home/ckh08045/work/inpaintServer/./app/api/endpoints.py", line 19, in <module>
|
||||
from ..models.rembg_model import RembgProcessor
|
||||
File "/home/ckh08045/work/inpaintServer/./app/models/rembg_model.py", line 15, in <module>
|
||||
class RembgProcessor:
|
||||
File "/home/ckh08045/work/inpaintServer/./app/models/rembg_model.py", line 79, in RembgProcessor
|
||||
async def remove_background(self, image: Union[Image.Image, np.ndarray]) -> tuple[np.ndarray, np.ndarray]:
|
||||
TypeError: 'type' object is not subscriptable
|
||||
|
|
@ -0,0 +1 @@
|
|||
71852
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
nohup: ignoring input
|
||||
WARNING:root:pynvml not available. GPU monitoring will be limited.
|
||||
INFO: Started server process [80136]
|
||||
INFO:uvicorn.error:Started server process [80136]
|
||||
INFO: Waiting for application startup.
|
||||
INFO:uvicorn.error:Waiting for application startup.
|
||||
WARNING:app.utils.gpu_monitor:Fan control not available
|
||||
INFO: Application startup complete.
|
||||
INFO:uvicorn.error:Application startup complete.
|
||||
INFO: Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit)
|
||||
INFO:uvicorn.error:Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit)
|
||||
INFO: 127.0.0.1:37694 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:50924 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: ('127.0.0.1', 50940) - "WebSocket /ws" [accepted]
|
||||
INFO:uvicorn.error:('127.0.0.1', 50940) - "WebSocket /ws" [accepted]
|
||||
INFO: connection open
|
||||
INFO:uvicorn.error:connection open
|
||||
INFO: connection closed
|
||||
INFO:uvicorn.error:connection closed
|
||||
INFO: 127.0.0.1:59682 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: ('127.0.0.1', 59710) - "WebSocket /ws" [accepted]
|
||||
INFO:uvicorn.error:('127.0.0.1', 59710) - "WebSocket /ws" [accepted]
|
||||
INFO: connection open
|
||||
INFO:uvicorn.error:connection open
|
||||
INFO: connection closed
|
||||
INFO:uvicorn.error:connection closed
|
||||
INFO: 127.0.0.1:41518 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: ('127.0.0.1', 41536) - "WebSocket /ws" [accepted]
|
||||
INFO:uvicorn.error:('127.0.0.1', 41536) - "WebSocket /ws" [accepted]
|
||||
INFO: connection open
|
||||
INFO:uvicorn.error:connection open
|
||||
INFO: 127.0.0.1:45724 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: ('127.0.0.1', 45752) - "WebSocket /ws" [accepted]
|
||||
INFO:uvicorn.error:('127.0.0.1', 45752) - "WebSocket /ws" [accepted]
|
||||
INFO: connection open
|
||||
INFO:uvicorn.error:connection open
|
||||
INFO: connection closed
|
||||
INFO:uvicorn.error:connection closed
|
||||
INFO: connection closed
|
||||
INFO:uvicorn.error:connection closed
|
||||
INFO: 127.0.0.1:48848 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: ('127.0.0.1', 48876) - "WebSocket /ws" [accepted]
|
||||
INFO:uvicorn.error:('127.0.0.1', 48876) - "WebSocket /ws" [accepted]
|
||||
INFO: connection open
|
||||
INFO:uvicorn.error:connection open
|
||||
INFO: connection closed
|
||||
INFO:uvicorn.error:connection closed
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
Collecting fastapi==0.104.1 (from -r requirements.txt (line 1))
|
||||
Using cached fastapi-0.104.1-py3-none-any.whl.metadata (24 kB)
|
||||
Collecting uvicorn==0.24.0 (from uvicorn[standard]==0.24.0->-r requirements.txt (line 2))
|
||||
Using cached uvicorn-0.24.0-py3-none-any.whl.metadata (6.4 kB)
|
||||
Collecting python-multipart==0.0.6 (from -r requirements.txt (line 3))
|
||||
Using cached python_multipart-0.0.6-py3-none-any.whl.metadata (2.5 kB)
|
||||
Collecting pillow==10.0.1 (from -r requirements.txt (line 4))
|
||||
Using cached Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl.metadata (9.5 kB)
|
||||
Collecting numpy==1.24.3 (from -r requirements.txt (line 5))
|
||||
Using cached numpy-1.24.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (5.6 kB)
|
||||
Collecting opencv-python==4.8.1.78 (from -r requirements.txt (line 6))
|
||||
Using cached opencv_python-4.8.1.78-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (19 kB)
|
||||
ERROR: Could not find a version that satisfies the requirement torch==2.0.1+cu118 (from versions: 1.8.0, 1.8.1, 1.9.0, 1.10.0, 1.10.1, 1.10.2, 1.11.0, 1.12.0, 1.12.1, 1.13.0, 1.13.1, 2.0.0, 2.0.1, 2.1.0, 2.1.1, 2.1.2, 2.2.0, 2.2.1, 2.2.2, 2.3.0, 2.3.1, 2.4.0, 2.4.1)
|
||||
ERROR: No matching distribution found for torch==2.0.1+cu118
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
인페인팅 서버 메인 애플리케이션
|
||||
iopaint와 호환되는 API를 제공합니다.
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.worker_manager import worker_manager
|
||||
from app.core.session_pool import session_pool
|
||||
from app.api.endpoints import router
|
||||
from app.monitoring.dashboard import monitor_app
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 서버 시작 시간 기록
|
||||
settings.start_time = time.time()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""애플리케이션 생명주기 관리"""
|
||||
# 시작 시
|
||||
logger.info("🚀 인페인팅 서버 시작 중...")
|
||||
|
||||
try:
|
||||
# 세션 풀 초기화
|
||||
await session_pool.initialize()
|
||||
logger.info("✅ 세션 풀 초기화 완료")
|
||||
|
||||
# 워커 매니저 시작
|
||||
await worker_manager.start()
|
||||
logger.info("✅ 워커 매니저 시작 완료")
|
||||
|
||||
logger.info("🎉 인페인팅 서버 시작 완료!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 서버 시작 실패: {e}")
|
||||
raise
|
||||
|
||||
yield
|
||||
|
||||
# 종료 시
|
||||
logger.info("🛑 인페인팅 서버 종료 중...")
|
||||
|
||||
try:
|
||||
# 워커 매니저 중지
|
||||
await worker_manager.stop()
|
||||
logger.info("✅ 워커 매니저 중지 완료")
|
||||
|
||||
logger.info("👋 인페인팅 서버 종료 완료")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 서버 종료 중 오류: {e}")
|
||||
|
||||
|
||||
# 메인 애플리케이션 생성
|
||||
app = FastAPI(
|
||||
title="인페인팅 서버",
|
||||
description="Simple LAMA, MIGAN, REMBG를 활용한 병렬 처리 인페인팅 서버 (iopaint 호환)",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS 미들웨어 추가
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# API 라우터 포함
|
||||
app.include_router(router)
|
||||
|
||||
# 모니터링 대시보드 마운트
|
||||
app.mount("/monitoring", monitor_app, name="monitoring")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="인페인팅 서버")
|
||||
parser.add_argument("--dev", action="store_true", help="개발 모드로 실행")
|
||||
parser.add_argument("--host", default=settings.HOST, help="호스트 주소")
|
||||
parser.add_argument("--port", type=int, default=settings.PORT, help="포트 번호")
|
||||
parser.add_argument("--workers", type=int, default=settings.WORKERS, help="워커 수")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.dev:
|
||||
logger.info("🔧 개발 모드로 실행합니다")
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=True,
|
||||
log_level="info"
|
||||
)
|
||||
else:
|
||||
logger.info("🚀 프로덕션 모드로 실행합니다")
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
workers=args.workers,
|
||||
log_level="info"
|
||||
)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
python-multipart==0.0.6
|
||||
pillow==10.0.1
|
||||
numpy==1.24.3
|
||||
opencv-python==4.8.1.78
|
||||
pydantic-settings==2.8.1
|
||||
psutil==5.9.6
|
||||
requests==2.31.0
|
||||
|
||||
# PyTorch - Jetson Xavier (ARM64) 지원
|
||||
# Jetson의 경우 torch==2.0.1+nv23.11-torch2.0.0 사용 권장
|
||||
torch==2.0.1+cu118
|
||||
torchvision==0.15.2+cu118
|
||||
|
||||
# TensorRT 및 CUDA 관련
|
||||
tensorrt==8.6.1
|
||||
pycuda==2022.2.2
|
||||
|
||||
# 인페인팅 모델들
|
||||
rembg==2.0.50
|
||||
simple-lama-inpainting==0.1.0
|
||||
|
||||
# 시스템 모니터링
|
||||
asyncio-throttle==1.0.2
|
||||
aiofiles==23.2.1
|
||||
pydantic==2.5.0
|
||||
|
||||
# Jetson 전용 패키지들
|
||||
pynvml==11.5.0
|
||||
nvidia-ml-py3==7.352.0
|
||||
|
||||
# 추가 최적화 패키지들
|
||||
onnxruntime-gpu==1.16.3
|
||||
tensorflow-gpu==2.13.0
|
||||
|
|
@ -0,0 +1,708 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 인페인팅 서버 의존성 설치 스크립트
|
||||
# Jetson Xavier와 x86 시스템을 모두 지원합니다.
|
||||
# Usage: ./install_deps.sh [options]
|
||||
|
||||
set -e
|
||||
|
||||
# 색상 코드
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 로그 함수들
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 기본 설정
|
||||
PROJECT_ROOT="/home/ckh08045/work/inpaintServer"
|
||||
VENV_PATH="$PROJECT_ROOT"
|
||||
|
||||
# 시스템 감지
|
||||
detect_system() {
|
||||
log_info "시스템 감지 중..."
|
||||
|
||||
if [ "$(uname -m)" = "aarch64" ] && uname -a | grep -q "tegra"; then
|
||||
SYSTEM_TYPE="jetson"
|
||||
log_success "Jetson Xavier (ARM64) 감지됨"
|
||||
|
||||
# Jetson 버전 확인
|
||||
if [ -f "/etc/nv_tegra_release" ]; then
|
||||
JETSON_VERSION=$(cat /etc/nv_tegra_release | head -1)
|
||||
log_info "Jetson 버전: $JETSON_VERSION"
|
||||
fi
|
||||
|
||||
# CUDA 버전 확인
|
||||
if command -v nvcc &> /dev/null; then
|
||||
CUDA_VERSION=$(nvcc --version | grep "release" | awk '{print $6}' | cut -c2-)
|
||||
log_info "CUDA 버전: $CUDA_VERSION"
|
||||
fi
|
||||
|
||||
# TensorRT 버전 확인
|
||||
if [ -f "/usr/lib/aarch64-linux-gnu/libnvinfer.so" ]; then
|
||||
TENSORRT_VERSION=$(strings /usr/lib/aarch64-linux-gnu/libnvinfer.so | grep "TensorRT" | head -1)
|
||||
log_info "TensorRT: $TENSORRT_VERSION"
|
||||
fi
|
||||
|
||||
elif [ "$(uname -m)" = "x86_64" ]; then
|
||||
SYSTEM_TYPE="x86"
|
||||
log_success "x86_64 시스템 감지됨"
|
||||
|
||||
# CUDA 버전 설정
|
||||
CUDA_VERSION="11.8"
|
||||
TENSORRT_VERSION="8.6.1"
|
||||
|
||||
else
|
||||
log_error "지원되지 않는 시스템 아키텍처: $(uname -m)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Python 버전 확인
|
||||
if command -v python3 &> /dev/null; then
|
||||
PYTHON_VERSION=$(python3 --version | awk '{print $2}')
|
||||
log_info "Python 버전: $PYTHON_VERSION"
|
||||
|
||||
# Python 3.8 이상 필요
|
||||
if python3 -c "import sys; exit(0 if sys.version_info >= (3, 8) else 1)"; then
|
||||
log_success "Python 버전 확인 완료"
|
||||
else
|
||||
log_error "Python 3.8 이상이 필요합니다 (현재: $PYTHON_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_error "Python3이 설치되어 있지 않습니다"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 옵션 파싱
|
||||
FORCE_REINSTALL=false
|
||||
SKIP_CUDA_CHECK=false
|
||||
INSTALL_TENSORRT=true
|
||||
INSTALL_EXTRAS=false
|
||||
JETSON_OPTIMIZE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-f|--force)
|
||||
FORCE_REINSTALL=true
|
||||
shift
|
||||
;;
|
||||
--skip-cuda)
|
||||
SKIP_CUDA_CHECK=true
|
||||
shift
|
||||
;;
|
||||
--no-tensorrt)
|
||||
INSTALL_TENSORRT=false
|
||||
shift
|
||||
;;
|
||||
--extras)
|
||||
INSTALL_EXTRAS=true
|
||||
shift
|
||||
;;
|
||||
--jetson-optimize)
|
||||
JETSON_OPTIMIZE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [options]"
|
||||
echo "Options:"
|
||||
echo " -f, --force 기존 패키지 강제 재설치"
|
||||
echo " --skip-cuda CUDA 설치 확인 건너뛰기"
|
||||
echo " --no-tensorrt TensorRT 설치 건너뛰기"
|
||||
echo " --extras 추가 패키지 설치 (개발 도구 등)"
|
||||
echo " --jetson-optimize Jetson 최적화 설정 (Jetson 전용)"
|
||||
echo " -h, --help 이 도움말 표시"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "알 수 없는 옵션: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 시스템 요구사항 확인
|
||||
check_system_requirements() {
|
||||
log_info "시스템 요구사항 확인 중..."
|
||||
|
||||
# Ubuntu 버전 확인
|
||||
if [ -f /etc/os-release ]; then
|
||||
source /etc/os-release
|
||||
log_info "OS: $PRETTY_NAME"
|
||||
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
# Jetson은 Ubuntu 18.04 이상 권장
|
||||
if [[ "$VERSION_ID" < "18.04" ]]; then
|
||||
log_warning "Ubuntu 18.04 이상을 권장합니다 (현재: $VERSION_ID)"
|
||||
fi
|
||||
else
|
||||
# x86은 Ubuntu 18.04 이상 권장
|
||||
if [[ "$VERSION_ID" < "18.04" ]]; then
|
||||
log_warning "Ubuntu 18.04 이상을 권장합니다 (현재: $VERSION_ID)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 메모리 확인
|
||||
TOTAL_MEMORY=$(free -g | awk 'NR==2{print $2}')
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
if [ "$TOTAL_MEMORY" -lt 4 ]; then
|
||||
log_warning "4GB 이상의 RAM을 권장합니다 (현재: ${TOTAL_MEMORY}GB)"
|
||||
fi
|
||||
else
|
||||
if [ "$TOTAL_MEMORY" -lt 8 ]; then
|
||||
log_warning "8GB 이상의 RAM을 권장합니다 (현재: ${TOTAL_MEMORY}GB)"
|
||||
fi
|
||||
fi
|
||||
|
||||
log_success "시스템 요구사항 확인 완료"
|
||||
}
|
||||
|
||||
# CUDA 설치 확인
|
||||
check_cuda_installation() {
|
||||
if [ "$SKIP_CUDA_CHECK" = true ]; then
|
||||
log_info "CUDA 확인을 건너뛰었습니다"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "CUDA 설치 확인 중..."
|
||||
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
log_info "Jetson Xavier CUDA 확인 중..."
|
||||
|
||||
# Jetson 전용 확인 방법들
|
||||
jetson_checks_passed=0
|
||||
|
||||
# 1. nvcc 확인
|
||||
if command -v nvcc &> /dev/null; then
|
||||
CUDA_VERSION_INSTALLED=$(nvcc --version | grep "release" | awk '{print $6}' | cut -c2-)
|
||||
log_info "CUDA 컴파일러 버전: $CUDA_VERSION_INSTALLED"
|
||||
jetson_checks_passed=$((jetson_checks_passed + 1))
|
||||
else
|
||||
log_warning "nvcc를 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 2. tegrastats 확인 (Jetson 전용)
|
||||
if command -v tegrastats &> /dev/null; then
|
||||
log_info "tegrastats 사용 가능 (Jetson 전용)"
|
||||
jetson_checks_passed=$((jetson_checks_passed + 1))
|
||||
else
|
||||
log_warning "tegrastats를 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 3. nvpmodel 확인 (전력 모드 관리)
|
||||
if command -v nvpmodel &> /dev/null; then
|
||||
log_info "nvpmodel 사용 가능 (전력 모드 관리)"
|
||||
jetson_checks_passed=$((jetson_checks_passed + 1))
|
||||
else
|
||||
log_warning "nvpmodel을 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 4. TensorRT 확인
|
||||
if [ -f "/usr/lib/aarch64-linux-gnu/libnvinfer.so" ]; then
|
||||
TENSORRT_VERSION=$(strings /usr/lib/aarch64-linux-gnu/libnvinfer.so | grep "TensorRT" | head -1)
|
||||
log_info "TensorRT: $TENSORRT_VERSION"
|
||||
jetson_checks_passed=$((jetson_checks_passed + 1))
|
||||
else
|
||||
log_warning "TensorRT를 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 5. Jetson 버전 확인
|
||||
if [ -f "/etc/nv_tegra_release" ]; then
|
||||
JETSON_VERSION=$(cat /etc/nv_tegra_release | head -1)
|
||||
log_info "Jetson 버전: $JETSON_VERSION"
|
||||
jetson_checks_passed=$((jetson_checks_passed + 1))
|
||||
else
|
||||
log_warning "Jetson 버전 정보를 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 6. GPU 디바이스 확인 (Jetson 전용 경로들)
|
||||
gpu_detected=false
|
||||
|
||||
# Jetson GPU 클래스 확인
|
||||
if [ -d "/sys/class/nvidia-gpu" ]; then
|
||||
log_info "Jetson GPU 클래스 감지됨: /sys/class/nvidia-gpu"
|
||||
gpu_detected=true
|
||||
fi
|
||||
|
||||
# Jetson GPU 디바이스 확인
|
||||
if [ -d "/sys/devices/platform/*/nvidia-gpu" ]; then
|
||||
log_info "Jetson GPU 디바이스 감지됨"
|
||||
gpu_detected=true
|
||||
fi
|
||||
|
||||
# GV11B GPU 확인 (Jetson Xavier)
|
||||
if [ -d "/sys/firmware/devicetree/base/gv11b" ]; then
|
||||
log_info "GV11B GPU (Jetson Xavier) 감지됨"
|
||||
gpu_detected=true
|
||||
fi
|
||||
|
||||
# 디버그 GPU 경로 확인 (권한이 있는 경우)
|
||||
if [ -r "/sys/kernel/debug/gpu" ]; then
|
||||
log_info "GPU 디버그 경로 접근 가능: /sys/kernel/debug/gpu"
|
||||
gpu_detected=true
|
||||
fi
|
||||
|
||||
if [ "$gpu_detected" = true ]; then
|
||||
log_info "GPU 디바이스 감지됨"
|
||||
jetson_checks_passed=$((jetson_checks_passed + 1))
|
||||
else
|
||||
log_warning "GPU 디바이스를 찾을 수 없습니다 (권한 문제일 수 있음)"
|
||||
log_info "sudo 권한으로 실행하거나 시스템을 재부팅해보세요"
|
||||
fi
|
||||
|
||||
if [ $jetson_checks_passed -ge 3 ]; then
|
||||
log_success "Jetson Xavier CUDA 확인 완료 ($jetson_checks_passed/6 체크 통과)"
|
||||
else
|
||||
log_warning "Jetson Xavier CUDA 확인 부분 실패 ($jetson_checks_passed/6 체크 통과)"
|
||||
log_info "Jetson SDK를 재설치하거나 시스템을 재부팅해보세요"
|
||||
fi
|
||||
|
||||
else
|
||||
# x86 시스템용 기존 확인 방법
|
||||
if command -v nvidia-smi &> /dev/null; then
|
||||
log_info "NVIDIA 드라이버가 설치되어 있습니다"
|
||||
nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv,noheader
|
||||
|
||||
# CUDA 런타임 확인
|
||||
if command -v nvcc &> /dev/null; then
|
||||
CUDA_VERSION_INSTALLED=$(nvcc --version | grep "release" | awk '{print $6}' | cut -c2-)
|
||||
log_info "CUDA 버전: $CUDA_VERSION_INSTALLED"
|
||||
else
|
||||
log_warning "CUDA 컴파일러(nvcc)를 찾을 수 없습니다"
|
||||
log_info "런타임만 설치된 상태일 수 있습니다"
|
||||
fi
|
||||
|
||||
log_success "CUDA 확인 완료"
|
||||
else
|
||||
log_error "NVIDIA 드라이버가 설치되어 있지 않습니다"
|
||||
log_info "다음 명령어로 설치하세요:"
|
||||
log_info "sudo apt update && sudo apt install nvidia-driver-470"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 가상환경 활성화
|
||||
activate_venv() {
|
||||
log_info "가상환경 활성화 중..."
|
||||
|
||||
if [ ! -f "$VENV_PATH/bin/activate" ]; then
|
||||
log_error "가상환경을 찾을 수 없습니다: $VENV_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
source "$VENV_PATH/bin/activate"
|
||||
|
||||
# pip 업그레이드
|
||||
log_info "pip 업그레이드 중..."
|
||||
pip install --upgrade pip setuptools wheel
|
||||
|
||||
log_success "가상환경 활성화 완료"
|
||||
}
|
||||
|
||||
# PyTorch 설치 (시스템별)
|
||||
install_pytorch() {
|
||||
log_info "PyTorch 설치 중..."
|
||||
|
||||
# 기존 PyTorch 확인
|
||||
if pip list | grep -q torch && [ "$FORCE_REINSTALL" = false ]; then
|
||||
log_info "PyTorch가 이미 설치되어 있습니다"
|
||||
python -c "import torch; print(f'PyTorch 버전: {torch.__version__}'); print(f'CUDA 사용 가능: {torch.cuda.is_available()}')"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
log_info "Jetson Xavier용 PyTorch 설치 중..."
|
||||
|
||||
# Jetson 전용 PyTorch 설치
|
||||
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
|
||||
|
||||
# Jetson 최적화 패키지들
|
||||
pip install nvidia-ml-py3 pynvml
|
||||
|
||||
else
|
||||
log_info "x86용 PyTorch 설치 중..."
|
||||
|
||||
# x86용 PyTorch 설치
|
||||
case $CUDA_VERSION in
|
||||
"11.8")
|
||||
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
|
||||
;;
|
||||
"11.7")
|
||||
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu117
|
||||
;;
|
||||
"cpu")
|
||||
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||
;;
|
||||
*)
|
||||
log_warning "지원되지 않는 CUDA 버전: $CUDA_VERSION. 기본 버전을 설치합니다"
|
||||
pip install torch torchvision torchaudio
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 설치 확인
|
||||
python -c "import torch; print(f'PyTorch 설치 완료: {torch.__version__}'); print(f'CUDA 사용 가능: {torch.cuda.is_available()}')"
|
||||
|
||||
log_success "PyTorch 설치 완료"
|
||||
}
|
||||
|
||||
# TensorRT 설치
|
||||
install_tensorrt() {
|
||||
if [ "$INSTALL_TENSORRT" = false ]; then
|
||||
log_info "TensorRT 설치를 건너뛰었습니다"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "TensorRT 설치 중..."
|
||||
|
||||
# 기존 TensorRT 확인
|
||||
if pip list | grep -q tensorrt && [ "$FORCE_REINSTALL" = false ]; then
|
||||
log_info "TensorRT가 이미 설치되어 있습니다"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
log_info "Jetson Xavier용 TensorRT 설치 중..."
|
||||
|
||||
# Jetson에는 TensorRT가 기본적으로 포함되어 있음
|
||||
if [ -f "/usr/lib/aarch64-linux-gnu/libnvinfer.so" ]; then
|
||||
log_info "Jetson TensorRT가 이미 설치되어 있습니다"
|
||||
|
||||
# Python 바인딩만 설치
|
||||
pip install nvidia-pyindex
|
||||
pip install nvidia-tensorrt
|
||||
else
|
||||
log_warning "Jetson TensorRT를 찾을 수 없습니다"
|
||||
log_info "Jetson SDK를 재설치하거나 수동으로 설치해주세요"
|
||||
fi
|
||||
|
||||
else
|
||||
log_info "x86용 TensorRT 설치 중..."
|
||||
|
||||
# x86용 TensorRT 설치 시도
|
||||
pip install tensorrt==$TENSORRT_VERSION || {
|
||||
log_warning "TensorRT 패키지 설치에 실패했습니다"
|
||||
log_info "수동으로 NVIDIA 웹사이트에서 다운로드가 필요할 수 있습니다"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
# pycuda 설치
|
||||
pip install pycuda
|
||||
|
||||
log_success "TensorRT 설치 완료"
|
||||
}
|
||||
|
||||
# 메인 패키지 설치
|
||||
install_main_packages() {
|
||||
log_info "메인 패키지 설치 중..."
|
||||
|
||||
# requirements.txt 확인
|
||||
if [ ! -f "$PROJECT_ROOT/requirements.txt" ]; then
|
||||
log_error "requirements.txt 파일을 찾을 수 없습니다"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Jetson 전용 requirements 처리
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
log_info "Jetson Xavier용 패키지 설치 중..."
|
||||
|
||||
# Jetson에 맞는 패키지 버전으로 설치
|
||||
pip install fastapi uvicorn python-multipart pillow numpy
|
||||
|
||||
# OpenCV - Jetson 최적화 버전
|
||||
pip install opencv-python-headless || pip install opencv-python
|
||||
|
||||
# 나머지 패키지들
|
||||
pip install psutil asyncio-throttle aiofiles pydantic
|
||||
|
||||
else
|
||||
# x86용 전체 패키지 설치
|
||||
if [ "$FORCE_REINSTALL" = true ]; then
|
||||
pip install --force-reinstall -r requirements.txt
|
||||
else
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
fi
|
||||
|
||||
log_success "메인 패키지 설치 완료"
|
||||
}
|
||||
|
||||
# 모델별 의존성 설치
|
||||
install_model_dependencies() {
|
||||
log_info "모델별 의존성 설치 중..."
|
||||
|
||||
# Simple LAMA 의존성
|
||||
log_info "Simple LAMA 의존성 설치 중..."
|
||||
pip install simple-lama-inpainting || {
|
||||
log_warning "simple-lama-inpainting 패키지 설치 실패"
|
||||
log_info "수동 설치가 필요할 수 있습니다"
|
||||
}
|
||||
|
||||
# REMBG 의존성
|
||||
log_info "REMBG 의존성 설치 중..."
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
# Jetson용 REMBG (CPU 버전 권장)
|
||||
pip install rembg || pip install rembg[cpu]
|
||||
else
|
||||
pip install rembg[gpu] || pip install rembg
|
||||
fi
|
||||
|
||||
# 추가 이미지 처리 라이브러리
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
# Jetson 최적화 버전 (pillow-simd는 ARM64에서 지원되지 않음)
|
||||
pip install opencv-python-headless pillow || pip install opencv-python pillow
|
||||
else
|
||||
# x86용 최적화 버전
|
||||
pip install opencv-python-headless pillow-simd || pip install opencv-python pillow
|
||||
fi
|
||||
|
||||
log_success "모델별 의존성 설치 완료"
|
||||
}
|
||||
|
||||
# GPU 라이브러리 설치
|
||||
install_gpu_libraries() {
|
||||
log_info "GPU 라이브러리 설치 중..."
|
||||
|
||||
# NVIDIA 관련 Python 패키지
|
||||
pip install nvidia-ml-py3 pynvml
|
||||
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
log_info "Jetson 전용 GPU 라이브러리 설치 중..."
|
||||
|
||||
# Jetson 최적화 라이브러리들
|
||||
pip install onnxruntime-gpu || pip install onnxruntime
|
||||
|
||||
# Jetson 전용 패키지들
|
||||
pip install jetson-stats || log_warning "jetson-stats 설치 실패"
|
||||
|
||||
else
|
||||
log_info "x86용 GPU 라이브러리 설치 중..."
|
||||
|
||||
# 추가 GPU 가속 라이브러리
|
||||
if [ "$INSTALL_EXTRAS" = true ]; then
|
||||
pip install cupy-cuda118 || {
|
||||
log_warning "CuPy 설치 실패"
|
||||
log_info "CUDA 버전을 확인하고 수동 설치하세요"
|
||||
}
|
||||
fi
|
||||
fi
|
||||
|
||||
log_success "GPU 라이브러리 설치 완료"
|
||||
}
|
||||
|
||||
# Jetson 최적화 설정
|
||||
setup_jetson_optimization() {
|
||||
if [ "$SYSTEM_TYPE" != "jetson" ] || [ "$JETSON_OPTIMIZE" != true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Jetson Xavier 최적화 설정 중..."
|
||||
|
||||
# 전력 모드 설정
|
||||
if command -v nvpmodel &> /dev/null; then
|
||||
log_info "전력 모드를 MAXN으로 설정 중..."
|
||||
sudo nvpmodel -m 0 # MAXN 모드
|
||||
sudo nvpmodel -q
|
||||
else
|
||||
log_warning "nvpmodel을 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 팬 제어 설정
|
||||
if [ -f "/sys/devices/pwm-fan/target_pwm" ]; then
|
||||
log_info "팬 제어 활성화 중..."
|
||||
echo 128 | sudo tee /sys/devices/pwm-fan/target_pwm > /dev/null
|
||||
fi
|
||||
|
||||
# GPU 클럭 설정
|
||||
if [ -f "/sys/kernel/debug/clk/gpcclk/clk_rate" ]; then
|
||||
log_info "GPU 클럭 최적화 중..."
|
||||
# 최대 클럭으로 설정 (1200MHz)
|
||||
echo 1200000000 | sudo tee /sys/kernel/debug/clk/gpcclk/clk_rate > /dev/null
|
||||
fi
|
||||
|
||||
# 메모리 클럭 설정
|
||||
if [ -f "/sys/kernel/debug/clk/emc/clk_rate" ]; then
|
||||
log_info "메모리 클럭 최적화 중..."
|
||||
# 최대 클럭으로 설정 (1600MHz)
|
||||
echo 1600000000 | sudo tee /sys/kernel/debug/clk/emc/clk_rate > /dev/null
|
||||
fi
|
||||
|
||||
log_success "Jetson 최적화 설정 완료"
|
||||
}
|
||||
|
||||
# 개발 도구 설치 (선택적)
|
||||
install_dev_tools() {
|
||||
if [ "$INSTALL_EXTRAS" = false ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "개발 도구 설치 중..."
|
||||
|
||||
# 코드 품질 도구
|
||||
pip install black flake8 isort mypy
|
||||
|
||||
# 테스트 도구
|
||||
pip install pytest pytest-asyncio pytest-cov
|
||||
|
||||
# 프로파일링 도구
|
||||
pip install line_profiler memory_profiler
|
||||
|
||||
# Jupyter 노트북
|
||||
pip install jupyter ipykernel
|
||||
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
# Jetson 전용 개발 도구
|
||||
pip install jetson-inference || log_warning "jetson-inference 설치 실패"
|
||||
fi
|
||||
|
||||
log_success "개발 도구 설치 완료"
|
||||
}
|
||||
|
||||
# 설치 확인
|
||||
verify_installation() {
|
||||
log_info "설치 확인 중..."
|
||||
|
||||
# Python 패키지 확인
|
||||
local packages=("fastapi" "uvicorn" "torch" "PIL" "cv2" "numpy")
|
||||
|
||||
for package in "${packages[@]}"; do
|
||||
if python -c "import $package" 2>/dev/null; then
|
||||
log_success "$package 설치 확인"
|
||||
else
|
||||
log_error "$package 설치 실패"
|
||||
fi
|
||||
done
|
||||
|
||||
# CUDA 사용 가능성 확인
|
||||
if python -c "import torch; exit(0 if torch.cuda.is_available() else 1)" 2>/dev/null; then
|
||||
log_success "CUDA 사용 가능"
|
||||
python -c "import torch; print(f'사용 가능한 GPU 수: {torch.cuda.device_count()}')"
|
||||
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
python -c "
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
print(f'Jetson GPU 메모리: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB')
|
||||
print(f'GPU 이름: {torch.cuda.get_device_properties(0).name}')
|
||||
"
|
||||
fi
|
||||
else
|
||||
log_warning "CUDA를 사용할 수 없습니다"
|
||||
fi
|
||||
|
||||
# Jetson 전용 확인
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
log_info "Jetson 전용 기능 확인 중..."
|
||||
|
||||
# jetson-stats 확인
|
||||
if command -v jtop &> /dev/null; then
|
||||
log_success "jetson-stats (jtop) 사용 가능"
|
||||
fi
|
||||
|
||||
# TensorRT 확인
|
||||
if python -c "import tensorrt" 2>/dev/null; then
|
||||
log_success "TensorRT Python 바인딩 확인"
|
||||
fi
|
||||
fi
|
||||
|
||||
log_success "설치 확인 완료"
|
||||
}
|
||||
|
||||
# 설치 후 정리
|
||||
post_install_cleanup() {
|
||||
log_info "설치 후 정리 중..."
|
||||
|
||||
# pip 캐시 정리
|
||||
pip cache purge
|
||||
|
||||
# __pycache__ 정리
|
||||
find "$PROJECT_ROOT" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$PROJECT_ROOT" -name "*.pyc" -delete 2>/dev/null || true
|
||||
|
||||
log_success "정리 완료"
|
||||
}
|
||||
|
||||
# 설치 요약 정보
|
||||
print_installation_summary() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "📦 의존성 설치 완료!"
|
||||
echo "=========================================="
|
||||
echo "시스템 타입: $SYSTEM_TYPE"
|
||||
echo "Python 버전: $(python --version)"
|
||||
echo "PyTorch 버전: $(python -c 'import torch; print(torch.__version__)' 2>/dev/null || echo 'N/A')"
|
||||
echo "CUDA 사용 가능: $(python -c 'import torch; print(torch.cuda.is_available())' 2>/dev/null || echo 'N/A')"
|
||||
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
echo ""
|
||||
echo "🚀 Jetson Xavier 최적화:"
|
||||
echo " - 전력 모드: MAXN"
|
||||
echo " - GPU 클럭: 1200MHz"
|
||||
echo " - 메모리 클럭: 1600MHz"
|
||||
echo " - 팬 제어: 활성화"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "설치된 주요 패키지:"
|
||||
pip list | grep -E "(torch|fastapi|uvicorn|rembg|pillow|opencv)" | head -10
|
||||
|
||||
echo ""
|
||||
echo "다음 단계:"
|
||||
echo " 1. 서버 시작: ./scripts/start_server.sh"
|
||||
echo " 2. 상태 확인: ./scripts/status.sh"
|
||||
echo " 3. API 문서: http://localhost:8000/docs"
|
||||
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
echo " 4. Jetson 모니터링: jtop"
|
||||
echo " 5. 전력 모드 확인: nvpmodel -q"
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
}
|
||||
|
||||
# 메인 실행
|
||||
main() {
|
||||
log_info "인페인팅 서버 의존성 설치 시작"
|
||||
|
||||
detect_system
|
||||
check_system_requirements
|
||||
check_cuda_installation
|
||||
activate_venv
|
||||
install_pytorch
|
||||
install_tensorrt
|
||||
install_main_packages
|
||||
install_model_dependencies
|
||||
install_gpu_libraries
|
||||
setup_jetson_optimization
|
||||
install_dev_tools
|
||||
verify_installation
|
||||
post_install_cleanup
|
||||
print_installation_summary
|
||||
|
||||
log_success "의존성 설치가 완료되었습니다!"
|
||||
}
|
||||
|
||||
# 스크립트 실행
|
||||
main "$@"
|
||||
|
|
@ -0,0 +1,488 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 인페인팅 서버 시작 스크립트
|
||||
# Jetson Xavier와 x86 시스템을 모두 지원합니다.
|
||||
# Usage: ./start_server.sh [options]
|
||||
|
||||
set -e
|
||||
|
||||
# 색상 코드
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 로그 함수들
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 기본 설정
|
||||
PROJECT_ROOT="/home/ckh08045/work/inpaintServer"
|
||||
VENV_PATH="$PROJECT_ROOT"
|
||||
MAIN_SERVER_PORT=8000
|
||||
MONITORING_PORT=8001
|
||||
LOG_DIR="$PROJECT_ROOT/logs"
|
||||
|
||||
# 시스템 감지
|
||||
detect_system() {
|
||||
if [ "$(uname -m)" = "aarch64" ] && uname -a | grep -q "tegra"; then
|
||||
SYSTEM_TYPE="jetson"
|
||||
log_info "🚀 Jetson Xavier (ARM64) 모드로 시작합니다"
|
||||
|
||||
# Jetson 전용 설정
|
||||
WORKERS=1
|
||||
MAX_WORKERS=4
|
||||
MIN_WORKERS=1
|
||||
VRAM_THRESHOLD_HIGH=0.7
|
||||
VRAM_THRESHOLD_LOW=0.3
|
||||
VRAM_CHECK_INTERVAL=20
|
||||
MAX_FILE_SIZE=25
|
||||
|
||||
elif [ "$(uname -m)" = "x86_64" ]; then
|
||||
SYSTEM_TYPE="x86"
|
||||
log_info "💻 x86_64 모드로 시작합니다"
|
||||
|
||||
# x86 전용 설정
|
||||
WORKERS=1
|
||||
MAX_WORKERS=8
|
||||
MIN_WORKERS=2
|
||||
VRAM_THRESHOLD_HIGH=0.8
|
||||
VRAM_THRESHOLD_LOW=0.4
|
||||
VRAM_CHECK_INTERVAL=30
|
||||
MAX_FILE_SIZE=50
|
||||
|
||||
else
|
||||
log_error "지원되지 않는 시스템 아키텍처: $(uname -m)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 옵션 파싱
|
||||
PRODUCTION=false
|
||||
MONITORING_ENABLED=true
|
||||
WORKERS=1
|
||||
GPU_DEVICE=0
|
||||
JETSON_OPTIMIZE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-p|--production)
|
||||
PRODUCTION=true
|
||||
shift
|
||||
;;
|
||||
--no-monitoring)
|
||||
MONITORING_ENABLED=false
|
||||
shift
|
||||
;;
|
||||
-w|--workers)
|
||||
WORKERS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-g|--gpu)
|
||||
GPU_DEVICE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--jetson-optimize)
|
||||
JETSON_OPTIMIZE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [options]"
|
||||
echo "Options:"
|
||||
echo " -p, --production 프로덕션 모드로 실행"
|
||||
echo " --no-monitoring 모니터링 대시보드 비활성화"
|
||||
echo " -w, --workers NUM 워커 수 설정 (기본값: 시스템별 자동)"
|
||||
echo " -g, --gpu DEVICE GPU 디바이스 ID (기본값: 0)"
|
||||
echo " --jetson-optimize Jetson 최적화 설정 (Jetson 전용)"
|
||||
echo " -h, --help 이 도움말 표시"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "알 수 없는 옵션: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 환경 확인
|
||||
check_environment() {
|
||||
log_info "환경 확인 중..."
|
||||
|
||||
# 프로젝트 디렉토리 확인
|
||||
if [ ! -d "$PROJECT_ROOT" ]; then
|
||||
log_error "프로젝트 디렉토리를 찾을 수 없습니다: $PROJECT_ROOT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 가상환경 확인
|
||||
if [ ! -f "$VENV_PATH/bin/activate" ]; then
|
||||
log_error "가상환경을 찾을 수 없습니다: $VENV_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# GPU 확인 (시스템별)
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
log_info "Jetson Xavier GPU 확인 중..."
|
||||
|
||||
# Jetson 전용 확인 방법들
|
||||
jetson_gpu_checks=0
|
||||
|
||||
# 1. tegrastats 확인
|
||||
if command -v tegrastats &> /dev/null; then
|
||||
log_info "tegrastats 사용 가능"
|
||||
jetson_gpu_checks=$((jetson_gpu_checks + 1))
|
||||
else
|
||||
log_warning "tegrastats를 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 2. nvpmodel 확인
|
||||
if command -v nvpmodel &> /dev/null; then
|
||||
log_info "nvpmodel 사용 가능"
|
||||
jetson_gpu_checks=$((jetson_gpu_checks + 1))
|
||||
else
|
||||
log_warning "nvpmodel을 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 3. GPU 디바이스 확인 (Jetson 전용 경로들)
|
||||
gpu_detected=false
|
||||
|
||||
# Jetson GPU 클래스 확인
|
||||
if [ -d "/sys/class/nvidia-gpu" ]; then
|
||||
log_info "Jetson GPU 클래스 감지됨: /sys/class/nvidia-gpu"
|
||||
gpu_detected=true
|
||||
fi
|
||||
|
||||
# Jetson GPU 디바이스 확인
|
||||
if [ -d "/sys/devices/platform/*/nvidia-gpu" ]; then
|
||||
log_info "Jetson GPU 디바이스 감지됨"
|
||||
gpu_detected=true
|
||||
fi
|
||||
|
||||
# GV11B GPU 확인 (Jetson Xavier)
|
||||
if [ -d "/sys/firmware/devicetree/base/gv11b" ]; then
|
||||
log_info "GV11B GPU (Jetson Xavier) 감지됨"
|
||||
gpu_detected=true
|
||||
fi
|
||||
|
||||
# 디버그 GPU 경로 확인 (권한이 있는 경우)
|
||||
if [ -r "/sys/kernel/debug/gpu" ]; then
|
||||
log_info "GPU 디버그 경로 접근 가능: /sys/kernel/debug/gpu"
|
||||
gpu_detected=true
|
||||
fi
|
||||
|
||||
if [ "$gpu_detected" = true ]; then
|
||||
log_info "GPU 디바이스 감지됨"
|
||||
jetson_gpu_checks=$((jetson_gpu_checks + 1))
|
||||
else
|
||||
log_warning "GPU 디바이스를 찾을 수 없습니다 (권한 문제일 수 있음)"
|
||||
log_info "sudo 권한으로 실행하거나 시스템을 재부팅해보세요"
|
||||
fi
|
||||
|
||||
# 4. CUDA 확인
|
||||
if command -v nvcc &> /dev/null; then
|
||||
log_info "CUDA 컴파일러 사용 가능"
|
||||
jetson_gpu_checks=$((jetson_gpu_checks + 1))
|
||||
else
|
||||
log_warning "CUDA 컴파일러를 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
if [ $jetson_gpu_checks -ge 2 ]; then
|
||||
log_success "Jetson Xavier GPU 확인 완료 ($jetson_gpu_checks/4 체크 통과)"
|
||||
|
||||
# Jetson 정보 표시 (에러 방지)
|
||||
if command -v tegrastats &> /dev/null; then
|
||||
log_info "Jetson 시스템 정보:"
|
||||
tegrastats 2>/dev/null | head -5 || log_warning "tegrastats 실행 중 오류 발생"
|
||||
fi
|
||||
|
||||
if command -v nvpmodel &> /dev/null; then
|
||||
log_info "Jetson 전력 모드:"
|
||||
nvpmodel -q 2>/dev/null || log_warning "nvpmodel 실행 중 오류 발생"
|
||||
fi
|
||||
else
|
||||
log_warning "Jetson Xavier GPU 확인 부분 실패 ($jetson_gpu_checks/4 체크 통과)"
|
||||
fi
|
||||
|
||||
else
|
||||
# x86 시스템용 기존 확인 방법
|
||||
if ! command -v nvidia-smi &> /dev/null; then
|
||||
log_warning "nvidia-smi를 찾을 수 없습니다. GPU 기능이 제한될 수 있습니다."
|
||||
else
|
||||
log_info "GPU 정보:"
|
||||
nvidia-smi --query-gpu=name,memory.total,memory.used --format=csv,noheader
|
||||
fi
|
||||
fi
|
||||
|
||||
# 로그 디렉토리 생성
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
log_success "환경 확인 완료"
|
||||
}
|
||||
|
||||
# 가상환경 활성화
|
||||
activate_venv() {
|
||||
log_info "가상환경 활성화 중..."
|
||||
cd "$PROJECT_ROOT"
|
||||
source "$VENV_PATH/bin/activate"
|
||||
|
||||
# Python 버전 확인
|
||||
PYTHON_VERSION=$(python --version 2>&1)
|
||||
log_info "Python 버전: $PYTHON_VERSION"
|
||||
|
||||
log_success "가상환경 활성화 완료"
|
||||
}
|
||||
|
||||
# 포트 확인
|
||||
check_ports() {
|
||||
log_info "포트 사용 상태 확인 중..."
|
||||
|
||||
if lsof -Pi :$MAIN_SERVER_PORT -sTCP:LISTEN -t >/dev/null; then
|
||||
log_error "메인 서버 포트 $MAIN_SERVER_PORT가 이미 사용 중입니다"
|
||||
lsof -Pi :$MAIN_SERVER_PORT -sTCP:LISTEN
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$MONITORING_ENABLED" = true ] && lsof -Pi :$MONITORING_PORT -sTCP:LISTEN -t >/dev/null; then
|
||||
log_error "모니터링 포트 $MONITORING_PORT가 이미 사용 중입니다"
|
||||
lsof -Pi :$MONITORING_PORT -sTCP:LISTEN
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "포트 확인 완료"
|
||||
}
|
||||
|
||||
# 의존성 확인
|
||||
check_dependencies() {
|
||||
log_info "의존성 확인 중..."
|
||||
|
||||
# requirements.txt가 있는지 확인
|
||||
if [ -f "$PROJECT_ROOT/requirements.txt" ]; then
|
||||
log_info "필수 패키지 설치 확인 중..."
|
||||
|
||||
# 이미 설치된 패키지들 확인
|
||||
if python -c "import fastapi, uvicorn, torch, cv2, PIL" 2>/dev/null; then
|
||||
log_success "필수 패키지들이 이미 설치되어 있습니다"
|
||||
else
|
||||
log_warning "일부 패키지가 누락되었습니다. 수동으로 설치가 필요할 수 있습니다"
|
||||
fi
|
||||
else
|
||||
log_warning "requirements.txt를 찾을 수 없습니다"
|
||||
fi
|
||||
}
|
||||
|
||||
# Jetson 최적화
|
||||
setup_jetson_optimization() {
|
||||
if [ "$SYSTEM_TYPE" != "jetson" ] || [ "$JETSON_OPTIMIZE" != true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "🚀 Jetson Xavier 최적화 설정 중..."
|
||||
|
||||
# 전력 모드 설정
|
||||
if command -v nvpmodel &> /dev/null; then
|
||||
log_info "전력 모드를 MAXN으로 설정 중..."
|
||||
sudo nvpmodel -m 0 # MAXN 모드
|
||||
sudo nvpmodel -q
|
||||
else
|
||||
log_warning "nvpmodel을 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 팬 제어 설정
|
||||
if [ -f "/sys/devices/pwm-fan/target_pwm" ]; then
|
||||
log_info "팬 제어 활성화 중..."
|
||||
echo 128 | sudo tee /sys/devices/pwm-fan/target_pwm > /dev/null
|
||||
fi
|
||||
|
||||
# GPU 클럭 설정
|
||||
if [ -f "/sys/kernel/debug/clk/gpcclk/clk_rate" ]; then
|
||||
log_info "GPU 클럭 최적화 중..."
|
||||
echo 1200000000 | sudo tee /sys/kernel/debug/clk/gpcclk/clk_rate > /dev/null
|
||||
fi
|
||||
|
||||
# 메모리 클럭 설정
|
||||
if [ -f "/sys/kernel/debug/clk/emc/clk_rate" ]; then
|
||||
log_info "메모리 클럭 최적화 중..."
|
||||
echo 1600000000 | sudo tee /sys/kernel/debug/clk/emc/clk_rate > /dev/null
|
||||
fi
|
||||
|
||||
log_success "Jetson 최적화 설정 완료"
|
||||
}
|
||||
|
||||
# 환경 변수 설정
|
||||
setup_environment() {
|
||||
log_info "환경 변수 설정 중..."
|
||||
|
||||
# CUDA 설정
|
||||
export CUDA_VISIBLE_DEVICES=$GPU_DEVICE
|
||||
export CUDA_DEVICE_ORDER=PCI_BUS_ID
|
||||
|
||||
# PyTorch 설정
|
||||
export TORCH_CUDA_ARCH_LIST="6.0;6.1;7.0;7.5;8.0;8.6"
|
||||
|
||||
# Jetson 전용 설정
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
export CUDA_LAUNCH_BLOCKING=1
|
||||
export CUDA_CACHE_DISABLE=0
|
||||
export CUDA_CACHE_PATH="/tmp/cuda_cache"
|
||||
|
||||
# Jetson 메모리 최적화
|
||||
export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128
|
||||
fi
|
||||
|
||||
# TensorRT 설정 (있는 경우)
|
||||
if [ -d "/usr/local/tensorrt" ]; then
|
||||
export LD_LIBRARY_PATH=/usr/local/tensorrt/lib:$LD_LIBRARY_PATH
|
||||
fi
|
||||
|
||||
# 프로덕션 설정
|
||||
if [ "$PRODUCTION" = true ]; then
|
||||
export PYTHONOPTIMIZE=1
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
fi
|
||||
|
||||
log_success "환경 변수 설정 완료"
|
||||
}
|
||||
|
||||
# 서버 시작
|
||||
start_servers() {
|
||||
log_info "서버 시작 중..."
|
||||
|
||||
# 메인 서버 시작
|
||||
log_info "메인 인페인팅 서버 시작 (포트: $MAIN_SERVER_PORT, 워커: $WORKERS)..."
|
||||
|
||||
if [ "$PRODUCTION" = true ]; then
|
||||
# 프로덕션 모드: Gunicorn 사용
|
||||
gunicorn app.api.endpoints:app \
|
||||
--bind 0.0.0.0:$MAIN_SERVER_PORT \
|
||||
--workers $WORKERS \
|
||||
--worker-class uvicorn.workers.UvicornWorker \
|
||||
--max-requests 1000 \
|
||||
--max-requests-jitter 50 \
|
||||
--preload \
|
||||
--access-logfile "$LOG_DIR/access.log" \
|
||||
--error-logfile "$LOG_DIR/error.log" \
|
||||
--log-level info \
|
||||
--daemon \
|
||||
--pid "$LOG_DIR/main_server.pid"
|
||||
else
|
||||
# 개발 모드: Uvicorn 사용
|
||||
nohup python -m uvicorn app.api.endpoints:app \
|
||||
--host 0.0.0.0 \
|
||||
--port $MAIN_SERVER_PORT \
|
||||
--workers $WORKERS \
|
||||
--log-level info \
|
||||
--access-log \
|
||||
> "$LOG_DIR/main_server.log" 2>&1 &
|
||||
echo $! > "$LOG_DIR/main_server.pid"
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
|
||||
# 메인 서버 상태 확인
|
||||
if curl -s "http://localhost:$MAIN_SERVER_PORT/health" > /dev/null; then
|
||||
log_success "메인 서버 시작 완료 (PID: $(cat $LOG_DIR/main_server.pid))"
|
||||
else
|
||||
log_error "메인 서버 시작 실패"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 모니터링 서버 시작 (활성화된 경우)
|
||||
if [ "$MONITORING_ENABLED" = true ]; then
|
||||
log_info "모니터링 대시보드 시작 (포트: $MONITORING_PORT)..."
|
||||
|
||||
nohup python -m uvicorn app.monitoring.dashboard:monitor_app \
|
||||
--host 0.0.0.0 \
|
||||
--port $MONITORING_PORT \
|
||||
--log-level info \
|
||||
> "$LOG_DIR/monitoring.log" 2>&1 &
|
||||
echo $! > "$LOG_DIR/monitoring.pid"
|
||||
|
||||
sleep 2
|
||||
|
||||
# 모니터링 서버 상태 확인
|
||||
if curl -s "http://localhost:$MONITORING_PORT/" > /dev/null; then
|
||||
log_success "모니터링 대시보드 시작 완료 (PID: $(cat $LOG_DIR/monitoring.pid))"
|
||||
log_info "모니터링 URL: http://localhost:$MONITORING_PORT"
|
||||
else
|
||||
log_warning "모니터링 대시보드 시작 실패"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 상태 정보 출력
|
||||
print_status() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "🚀 인페인팅 서버 시작 완료!"
|
||||
echo "=========================================="
|
||||
echo "시스템 타입: $SYSTEM_TYPE"
|
||||
echo "메인 서버: http://localhost:$MAIN_SERVER_PORT"
|
||||
echo "API 문서: http://localhost:$MAIN_SERVER_PORT/docs"
|
||||
echo "헬스 체크: http://localhost:$MAIN_SERVER_PORT/health"
|
||||
|
||||
if [ "$MONITORING_ENABLED" = true ]; then
|
||||
echo "모니터링: http://localhost:$MONITORING_PORT"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "로그 디렉토리: $LOG_DIR"
|
||||
echo "GPU 디바이스: $GPU_DEVICE"
|
||||
echo "워커 수: $WORKERS"
|
||||
echo "프로덕션 모드: $PRODUCTION"
|
||||
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
echo ""
|
||||
echo "🚀 Jetson Xavier 최적화:"
|
||||
echo " - 전력 모드: MAXN"
|
||||
echo " - GPU 클럭: 1200MHz"
|
||||
echo " - 메모리 클럭: 1600MHz"
|
||||
echo " - 팬 제어: 활성화"
|
||||
echo " - VRAM 임계값: 70%/30%"
|
||||
echo " - 파일 크기 제한: 25MB"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "서버 중지: ./scripts/stop_server.sh"
|
||||
echo "상태 확인: ./scripts/status.sh"
|
||||
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
echo "Jetson 모니터링: jtop"
|
||||
echo "전력 모드 확인: nvpmodel -q"
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
}
|
||||
|
||||
# 메인 실행
|
||||
main() {
|
||||
log_info "인페인팅 서버 시작 스크립트 실행"
|
||||
|
||||
detect_system
|
||||
check_environment
|
||||
activate_venv
|
||||
check_ports
|
||||
check_dependencies
|
||||
setup_jetson_optimization
|
||||
setup_environment
|
||||
start_servers
|
||||
print_status
|
||||
|
||||
log_success "모든 서버가 성공적으로 시작되었습니다!"
|
||||
}
|
||||
|
||||
# 스크립트 실행
|
||||
main "$@"
|
||||
|
|
@ -0,0 +1,465 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 인페인팅 서버 상태 확인 스크립트
|
||||
# Usage: ./status.sh [options]
|
||||
|
||||
set -e
|
||||
|
||||
# 색상 코드
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 로그 함수들
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 기본 설정
|
||||
PROJECT_ROOT="/home/ckh08045/work/inpaintServer"
|
||||
LOG_DIR="$PROJECT_ROOT/logs"
|
||||
MAIN_PORT=8000
|
||||
MONITORING_PORT=8001
|
||||
|
||||
# 옵션 파싱
|
||||
DETAILED=false
|
||||
JSON_OUTPUT=false
|
||||
WATCH_MODE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-d|--detailed)
|
||||
DETAILED=true
|
||||
shift
|
||||
;;
|
||||
-j|--json)
|
||||
JSON_OUTPUT=true
|
||||
shift
|
||||
;;
|
||||
-w|--watch)
|
||||
WATCH_MODE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [options]"
|
||||
echo "Options:"
|
||||
echo " -d, --detailed 자세한 상태 정보 표시"
|
||||
echo " -j, --json JSON 형식으로 출력"
|
||||
echo " -w, --watch 실시간 모니터링 모드"
|
||||
echo " -h, --help 이 도움말 표시"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "알 수 없는 옵션: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 시스템 감지
|
||||
detect_system() {
|
||||
if [ "$(uname -m)" = "aarch64" ] && uname -a | grep -q "tegra"; then
|
||||
SYSTEM_TYPE="jetson"
|
||||
log_info "🚁 Jetson Xavier (ARM64) 모드로 상태 확인합니다"
|
||||
elif [ "$(uname -m)" = "x86_64" ]; then
|
||||
SYSTEM_TYPE="x86"
|
||||
log_info "💻 x86_64 모드로 상태 확인합니다"
|
||||
else
|
||||
SYSTEM_TYPE="unknown"
|
||||
log_warning "알 수 없는 시스템 아키텍처: $(uname -m)"
|
||||
fi
|
||||
}
|
||||
|
||||
# GPU 상태 확인
|
||||
check_gpu_status() {
|
||||
if [ "$SYSTEM_TYPE" = "jetson" ]; then
|
||||
log_info "Jetson Xavier GPU 상태 확인 중..."
|
||||
|
||||
# Jetson 전용 확인 방법들
|
||||
jetson_gpu_available=false
|
||||
|
||||
# 1. tegrastats 확인
|
||||
if command -v tegrastats &> /dev/null; then
|
||||
jetson_gpu_available=true
|
||||
log_info "tegrastats 사용 가능"
|
||||
|
||||
# Jetson 시스템 정보 표시
|
||||
log_info "Jetson 시스템 정보:"
|
||||
tegrastats -1 | head -10
|
||||
|
||||
else
|
||||
log_warning "tegrastats를 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 2. nvpmodel 확인
|
||||
if command -v nvpmodel &> /dev/null; then
|
||||
jetson_gpu_available=true
|
||||
log_info "nvpmodel 사용 가능"
|
||||
|
||||
# 전력 모드 정보 표시
|
||||
log_info "Jetson 전력 모드:"
|
||||
nvpmodel -q
|
||||
|
||||
else
|
||||
log_warning "nvpmodel을 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 3. GPU 디바이스 확인
|
||||
if [ -e "/sys/kernel/debug/gpu" ] || [ -d "/dev/nvidia*" ]; then
|
||||
jetson_gpu_available=true
|
||||
log_info "GPU 디바이스 감지됨"
|
||||
|
||||
# GPU 메모리 정보 (가능한 경우)
|
||||
if [ -e "/sys/kernel/debug/gpu/memory" ]; then
|
||||
log_info "GPU 메모리 정보:"
|
||||
cat /sys/kernel/debug/gpu/memory | head -5
|
||||
fi
|
||||
|
||||
else
|
||||
log_warning "GPU 디바이스를 찾을 수 없습니다"
|
||||
fi
|
||||
|
||||
# 4. 온도 정보 확인
|
||||
if [ -d "/sys/devices/virtual/thermal" ]; then
|
||||
log_info "Jetson 온도 정보:"
|
||||
for zone in /sys/devices/virtual/thermal/thermal_zone*; do
|
||||
if [ -f "$zone/temp" ]; then
|
||||
temp=$(cat "$zone/temp")
|
||||
temp_c=$((temp / 1000))
|
||||
zone_name=$(basename "$zone")
|
||||
echo " $zone_name: ${temp_c}°C"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$jetson_gpu_available" = true ]; then
|
||||
log_success "Jetson Xavier GPU 상태 확인 완료"
|
||||
else
|
||||
log_warning "Jetson Xavier GPU 상태 확인 실패"
|
||||
fi
|
||||
|
||||
return 0
|
||||
|
||||
elif ! command -v nvidia-smi &> /dev/null; then
|
||||
log_warning "nvidia-smi를 찾을 수 없습니다"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# x86 시스템용 기존 확인 방법
|
||||
local gpu_info=$(nvidia-smi --query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu --format=csv,noheader,nounits 2>/dev/null)
|
||||
|
||||
if [ -z "$gpu_info" ]; then
|
||||
log_error "GPU 정보를 가져올 수 없습니다"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
echo -n "\"gpu\": {"
|
||||
echo -n "\"available\": true,"
|
||||
|
||||
IFS=',' read -r name total used free util <<< "$gpu_info"
|
||||
echo -n "\"name\": \"$(echo $name | xargs)\","
|
||||
echo -n "\"memory_total\": $(echo $total | xargs),"
|
||||
echo -n "\"memory_used\": $(echo $used | xargs),"
|
||||
echo -n "\"memory_free\": $(echo $free | xargs),"
|
||||
echo -n "\"utilization\": $(echo $util | xargs)"
|
||||
echo -n "}"
|
||||
else
|
||||
echo -e "${CYAN}GPU 상태:${NC}"
|
||||
IFS=',' read -r name total used free util <<< "$gpu_info"
|
||||
printf " GPU: %s\n" "$(echo $name | xargs)"
|
||||
printf " 메모리: %s/%s MB 사용 (%.1f%%)\n" "$(echo $used | xargs)" "$(echo $total | xargs)" $(echo "scale=1; $(echo $used | xargs) * 100 / $(echo $total | xargs)" | bc -l 2>/dev/null || echo "0")
|
||||
printf " 사용률: %s%%\n" "$(echo $util | xargs)"
|
||||
fi
|
||||
}
|
||||
|
||||
# 시스템 리소스 확인
|
||||
check_system_resources() {
|
||||
local memory_info=$(free -m | awk 'NR==2{printf "%.1f/%.1f MB (%.1f%%)", $3,$2,$3*100/$2}')
|
||||
local cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | sed 's/%us,//')
|
||||
local disk_usage=$(df -h / | awk 'NR==2{print $5}')
|
||||
local load_avg=$(uptime | awk -F'load average:' '{print $2}' | xargs)
|
||||
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
echo -n "\"system\": {"
|
||||
echo -n "\"memory\": \"$memory_info\","
|
||||
echo -n "\"cpu_usage\": \"$cpu_usage\","
|
||||
echo -n "\"disk_usage\": \"$disk_usage\","
|
||||
echo -n "\"load_average\": \"$load_avg\""
|
||||
echo -n "}"
|
||||
else
|
||||
echo -e "${CYAN}시스템 리소스:${NC}"
|
||||
printf " 메모리: %s\n" "$memory_info"
|
||||
printf " CPU 사용률: %s%%\n" "$cpu_usage"
|
||||
printf " 디스크 사용률: %s\n" "$disk_usage"
|
||||
printf " 로드 평균: %s\n" "$load_avg"
|
||||
fi
|
||||
}
|
||||
|
||||
# API 엔드포인트 상태 확인
|
||||
check_api_endpoints() {
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
echo -n "\"api_endpoints\": {"
|
||||
else
|
||||
echo -e "${CYAN}API 엔드포인트 상태:${NC}"
|
||||
fi
|
||||
|
||||
local endpoints=(
|
||||
"health:http://localhost:$MAIN_PORT/health"
|
||||
"status:http://localhost:$MAIN_PORT/status"
|
||||
"docs:http://localhost:$MAIN_PORT/docs"
|
||||
)
|
||||
|
||||
local first=true
|
||||
for endpoint_info in "${endpoints[@]}"; do
|
||||
IFS=':' read -r name url <<< "$endpoint_info"
|
||||
|
||||
local response_code=$(curl -s -o /dev/null -w "%{http_code}" "$url" --connect-timeout 3 2>/dev/null || echo "000")
|
||||
local status="FAILED"
|
||||
|
||||
if [ "$response_code" = "200" ]; then
|
||||
status="OK"
|
||||
elif [ "$response_code" = "000" ]; then
|
||||
status="UNREACHABLE"
|
||||
fi
|
||||
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
[ "$first" = false ] && echo -n ","
|
||||
echo -n "\"$name\": {\"status\": \"$status\", \"code\": \"$response_code\", \"url\": \"$url\"}"
|
||||
first=false
|
||||
else
|
||||
local status_color
|
||||
case $status in
|
||||
"OK") status_color=$GREEN ;;
|
||||
"UNREACHABLE") status_color=$RED ;;
|
||||
*) status_color=$YELLOW ;;
|
||||
esac
|
||||
printf " %-10s: ${status_color}%-12s${NC} (%s)\n" "$name" "$status" "$response_code"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
echo -n "}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 로그 파일 상태 확인
|
||||
check_log_files() {
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
echo -n "\"logs\": {"
|
||||
else
|
||||
echo -e "${CYAN}로그 파일 상태:${NC}"
|
||||
fi
|
||||
|
||||
local log_files=(
|
||||
"main_server.log"
|
||||
"monitoring.log"
|
||||
"error.log"
|
||||
"access.log"
|
||||
)
|
||||
|
||||
local first=true
|
||||
for log_file in "${log_files[@]}"; do
|
||||
local log_path="$LOG_DIR/$log_file"
|
||||
local size="N/A"
|
||||
local last_modified="N/A"
|
||||
local exists=false
|
||||
|
||||
if [ -f "$log_path" ]; then
|
||||
exists=true
|
||||
size=$(du -h "$log_path" 2>/dev/null | cut -f1)
|
||||
last_modified=$(stat -c %y "$log_path" 2>/dev/null | cut -d' ' -f1,2 | cut -d'.' -f1)
|
||||
fi
|
||||
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
[ "$first" = false ] && echo -n ","
|
||||
echo -n "\"$log_file\": {\"exists\": $exists, \"size\": \"$size\", \"last_modified\": \"$last_modified\"}"
|
||||
first=false
|
||||
else
|
||||
if [ "$exists" = true ]; then
|
||||
printf " %-20s: ${GREEN}EXISTS${NC} (크기: %s, 수정: %s)\n" "$log_file" "$size" "$last_modified"
|
||||
else
|
||||
printf " %-20s: ${RED}NOT FOUND${NC}\n" "$log_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
echo -n "}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 프로세스 트리 표시 (상세 모드)
|
||||
show_process_tree() {
|
||||
if [ "$DETAILED" = true ] && [ "$JSON_OUTPUT" = false ]; then
|
||||
echo -e "${CYAN}프로세스 트리:${NC}"
|
||||
|
||||
local main_pid=$(lsof -ti:$MAIN_PORT 2>/dev/null | head -1)
|
||||
local monitoring_pid=$(lsof -ti:$MONITORING_PORT 2>/dev/null | head -1)
|
||||
|
||||
if [ -n "$main_pid" ]; then
|
||||
echo " 메인 서버 프로세스:"
|
||||
pstree -p "$main_pid" 2>/dev/null | sed 's/^/ /' || ps -f --pid "$main_pid"
|
||||
fi
|
||||
|
||||
if [ -n "$monitoring_pid" ]; then
|
||||
echo " 모니터링 서버 프로세스:"
|
||||
pstree -p "$monitoring_pid" 2>/dev/null | sed 's/^/ /' || ps -f --pid "$monitoring_pid"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 네트워크 연결 상태 확인 (상세 모드)
|
||||
check_network_connections() {
|
||||
if [ "$DETAILED" = true ]; then
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
echo -n "\"network_connections\": ["
|
||||
else
|
||||
echo -e "${CYAN}네트워크 연결:${NC}"
|
||||
fi
|
||||
|
||||
local connections=$(netstat -tulpn 2>/dev/null | grep -E ":($MAIN_PORT|$MONITORING_PORT) " || true)
|
||||
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
local first=true
|
||||
while IFS= read -r line; do
|
||||
if [ -n "$line" ]; then
|
||||
[ "$first" = false ] && echo -n ","
|
||||
echo -n "\"$line\""
|
||||
first=false
|
||||
fi
|
||||
done <<< "$connections"
|
||||
echo -n "]"
|
||||
else
|
||||
if [ -n "$connections" ]; then
|
||||
echo "$connections" | sed 's/^/ /'
|
||||
else
|
||||
echo " 연결된 포트가 없습니다"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 실시간 모니터링
|
||||
watch_status() {
|
||||
while true; do
|
||||
clear
|
||||
echo "=========================================="
|
||||
echo "🔍 인페인팅 서버 실시간 모니터링"
|
||||
echo "=========================================="
|
||||
echo "업데이트 시간: $(date)"
|
||||
echo ""
|
||||
|
||||
show_status
|
||||
|
||||
echo ""
|
||||
echo "Ctrl+C로 종료"
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
# 서버 상태 확인 함수
|
||||
check_server_status() {
|
||||
local port=$1
|
||||
local service_name=$2
|
||||
|
||||
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1; then
|
||||
local pid=$(lsof -Pi :$port -sTCP:LISTEN -t | head -1)
|
||||
local process_info=$(ps -p $pid -o pid,cmd,etime --no-headers 2>/dev/null || echo "Unknown")
|
||||
echo -e " ✅ $service_name (포트 $port): 실행 중 (PID: $pid)"
|
||||
echo -e " 프로세스: $process_info"
|
||||
else
|
||||
echo -e " ❌ $service_name (포트 $port): 중지됨"
|
||||
fi
|
||||
}
|
||||
|
||||
# 메인 상태 표시 함수
|
||||
show_status() {
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
echo "{"
|
||||
check_server_status $MAIN_PORT "main_server"
|
||||
echo ","
|
||||
check_server_status $MONITORING_PORT "monitoring_server"
|
||||
echo ","
|
||||
check_gpu_status
|
||||
echo ","
|
||||
check_system_resources
|
||||
echo ","
|
||||
check_api_endpoints
|
||||
echo ","
|
||||
check_log_files
|
||||
check_network_connections
|
||||
echo "}"
|
||||
else
|
||||
echo "=========================================="
|
||||
echo "🖼️ 인페인팅 서버 상태"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}서비스 상태:${NC}"
|
||||
check_server_status $MAIN_PORT "메인 서버"
|
||||
check_server_status $MONITORING_PORT "모니터링 서버"
|
||||
echo ""
|
||||
|
||||
check_gpu_status
|
||||
echo ""
|
||||
|
||||
check_system_resources
|
||||
echo ""
|
||||
|
||||
check_api_endpoints
|
||||
echo ""
|
||||
|
||||
check_log_files
|
||||
echo ""
|
||||
|
||||
show_process_tree
|
||||
check_network_connections
|
||||
|
||||
echo "=========================================="
|
||||
echo "💡 유용한 명령어:"
|
||||
echo " 서버 시작: ./scripts/start_server.sh"
|
||||
echo " 서버 중지: ./scripts/stop_server.sh"
|
||||
echo " 실시간 모니터링: ./scripts/status.sh -w"
|
||||
echo " 상세 정보: ./scripts/status.sh -d"
|
||||
if lsof -Pi :$MONITORING_PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
||||
echo " 웹 모니터링: http://localhost:$MONITORING_PORT"
|
||||
fi
|
||||
echo "=========================================="
|
||||
fi
|
||||
}
|
||||
|
||||
# 메인 실행
|
||||
main() {
|
||||
# 로그 디렉토리 확인
|
||||
if [ ! -d "$LOG_DIR" ]; then
|
||||
mkdir -p "$LOG_DIR"
|
||||
fi
|
||||
|
||||
# 시스템 감지
|
||||
detect_system
|
||||
|
||||
if [ "$WATCH_MODE" = true ]; then
|
||||
watch_status
|
||||
else
|
||||
show_status
|
||||
fi
|
||||
}
|
||||
|
||||
# 스크립트 실행
|
||||
main "$@"
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 인페인팅 서버 정지 스크립트
|
||||
# Jetson Xavier와 x86 시스템을 모두 지원합니다.
|
||||
|
||||
set -e
|
||||
|
||||
# 색상 코드
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 로그 함수들
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 기본 설정
|
||||
PROJECT_ROOT="/home/ckh08045/work/inpaintServer"
|
||||
PID_FILE="$PROJECT_ROOT/server.pid"
|
||||
LOG_FILE="$PROJECT_ROOT/logs/server.log"
|
||||
|
||||
# 시스템 감지
|
||||
detect_system() {
|
||||
if [ "$(uname -m)" = "aarch64" ] && uname -a | grep -q "tegra"; then
|
||||
SYSTEM_TYPE="jetson"
|
||||
log_info "🚁 Jetson Xavier (ARM64) 모드로 정지합니다"
|
||||
elif [ "$(uname -m)" = "x86_64" ]; then
|
||||
SYSTEM_TYPE="x86"
|
||||
log_info "🖥️ x86_64 모드로 정지합니다"
|
||||
else
|
||||
log_error "지원되지 않는 시스템 아키텍처: $(uname -m)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 서버 프로세스 찾기
|
||||
find_server_processes() {
|
||||
local processes=()
|
||||
|
||||
# PID 파일에서 확인
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
local pid=$(cat "$PID_FILE")
|
||||
if ps -p "$pid" > /dev/null 2>&1; then
|
||||
processes+=("$pid")
|
||||
fi
|
||||
fi
|
||||
|
||||
# 포트 8000에서 실행 중인 프로세스 찾기
|
||||
local port_processes=$(lsof -ti:8000 2>/dev/null || echo "")
|
||||
if [ -n "$port_processes" ]; then
|
||||
for pid in $port_processes; do
|
||||
if [[ ! " ${processes[@]} " =~ " ${pid} " ]]; then
|
||||
processes+=("$pid")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# 포트 8001에서 실행 중인 프로세스 찾기 (모니터링)
|
||||
local monitor_processes=$(lsof -ti:8001 2>/dev/null || echo "")
|
||||
if [ -n "$monitor_processes" ]; then
|
||||
for pid in $monitor_processes; do
|
||||
if [[ ! " ${processes[@]} " =~ " ${pid} " ]]; then
|
||||
processes+=("$pid")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Python 프로세스 중 인페인팅 서버 관련 찾기
|
||||
local python_processes=$(ps aux | grep -E "(uvicorn|python.*main\.py|python.*dashboard\.py)" | grep -v grep | awk '{print $2}' || echo "")
|
||||
if [ -n "$python_processes" ]; then
|
||||
for pid in $python_processes; do
|
||||
if [[ ! " ${processes[@]} " =~ " ${pid} " ]]; then
|
||||
processes+=("$pid")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "${processes[@]}"
|
||||
}
|
||||
|
||||
# 서버 정지
|
||||
stop_server() {
|
||||
log_info "서버 정지 중..."
|
||||
|
||||
local processes=($(find_server_processes))
|
||||
|
||||
if [ ${#processes[@]} -eq 0 ]; then
|
||||
log_warning "실행 중인 서버 프로세스를 찾을 수 없습니다"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "발견된 프로세스: ${processes[*]}"
|
||||
|
||||
# 각 프로세스에 SIGTERM 전송
|
||||
for pid in "${processes[@]}"; do
|
||||
if ps -p "$pid" > /dev/null 2>&1; then
|
||||
log_info "프로세스 $pid에 종료 신호 전송 중..."
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
|
||||
# 5초 대기 후 강제 종료
|
||||
sleep 5
|
||||
if ps -p "$pid" > /dev/null 2>&1; then
|
||||
log_warning "프로세스 $pid 강제 종료 중..."
|
||||
kill -KILL "$pid" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# PID 파일 정리
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
rm -f "$PID_FILE"
|
||||
log_info "PID 파일 정리됨"
|
||||
fi
|
||||
|
||||
# 포트 사용 상태 확인
|
||||
sleep 2
|
||||
if lsof -ti:8000 > /dev/null 2>&1; then
|
||||
log_warning "포트 8000이 여전히 사용 중입니다"
|
||||
else
|
||||
log_success "포트 8000 해제됨"
|
||||
fi
|
||||
|
||||
if lsof -ti:8001 > /dev/null 2>&1; then
|
||||
log_warning "포트 8001이 여전히 사용 중입니다"
|
||||
else
|
||||
log_success "포트 8001 해제됨"
|
||||
fi
|
||||
|
||||
log_success "서버 정지 완료"
|
||||
}
|
||||
|
||||
# 강제 정지
|
||||
force_stop() {
|
||||
log_warning "강제 정지 모드로 실행 중..."
|
||||
|
||||
# 모든 관련 프로세스 강제 종료
|
||||
local processes=($(find_server_processes))
|
||||
|
||||
for pid in "${processes[@]}"; do
|
||||
if ps -p "$pid" > /dev/null 2>&1; then
|
||||
log_info "프로세스 $pid 강제 종료 중..."
|
||||
kill -KILL "$pid" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# 포트 사용 프로세스 강제 종료
|
||||
local port_8000=$(lsof -ti:8000 2>/dev/null || echo "")
|
||||
if [ -n "$port_8000" ]; then
|
||||
log_info "포트 8000 사용 프로세스 강제 종료 중..."
|
||||
kill -KILL $port_8000 2>/dev/null || true
|
||||
fi
|
||||
|
||||
local port_8001=$(lsof -ti:8001 2>/dev/null || echo "")
|
||||
if [ -n "$port_8001" ]; then
|
||||
log_info "포트 8001 사용 프로세스 강제 종료 중..."
|
||||
kill -KILL $port_8001 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# PID 파일 정리
|
||||
rm -f "$PID_FILE"
|
||||
|
||||
log_success "강제 정지 완료"
|
||||
}
|
||||
|
||||
# 서버 상태 확인
|
||||
check_status() {
|
||||
log_info "서버 상태 확인 중..."
|
||||
|
||||
local processes=($(find_server_processes))
|
||||
|
||||
if [ ${#processes[@]} -eq 0 ]; then
|
||||
log_info "실행 중인 서버가 없습니다"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "실행 중인 서버 프로세스:"
|
||||
for pid in "${processes[@]}"; do
|
||||
if ps -p "$pid" > /dev/null 2>&1; then
|
||||
local cmd=$(ps -p "$pid" -o cmd= 2>/dev/null || echo "Unknown")
|
||||
local mem=$(ps -p "$pid" -o rss= 2>/dev/null || echo "Unknown")
|
||||
log_info "PID $pid: $cmd (메모리: ${mem}KB)"
|
||||
fi
|
||||
done
|
||||
|
||||
# 포트 상태 확인
|
||||
if lsof -ti:8000 > /dev/null 2>&1; then
|
||||
log_info "포트 8000: 사용 중 (메인 서버)"
|
||||
else
|
||||
log_info "포트 8000: 사용 안함"
|
||||
fi
|
||||
|
||||
if lsof -ti:8001 > /dev/null 2>&1; then
|
||||
log_info "포트 8001: 사용 중 (모니터링 대시보드)"
|
||||
else
|
||||
log_info "포트 8001: 사용 안함"
|
||||
fi
|
||||
}
|
||||
|
||||
# 로그 확인
|
||||
show_logs() {
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
log_info "최근 서버 로그 (마지막 50줄):"
|
||||
echo "----------------------------------------"
|
||||
tail -n 50 "$LOG_FILE" 2>/dev/null || echo "로그 파일을 읽을 수 없습니다"
|
||||
echo "----------------------------------------"
|
||||
else
|
||||
log_warning "로그 파일을 찾을 수 없습니다: $LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# 정리 작업
|
||||
cleanup() {
|
||||
log_info "정리 작업 중..."
|
||||
|
||||
# 임시 파일들 정리
|
||||
find /tmp -name "inpaint_*" -type f -mtime +1 -delete 2>/dev/null || true
|
||||
|
||||
# 로그 파일 압축 (7일 이상 된 것)
|
||||
if [ -d "$PROJECT_ROOT/logs" ]; then
|
||||
find "$PROJECT_ROOT/logs" -name "*.log" -mtime +7 -exec gzip {} \; 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log_success "정리 작업 완료"
|
||||
}
|
||||
|
||||
# 메인 함수
|
||||
main() {
|
||||
local action="stop"
|
||||
local force=false
|
||||
local status=false
|
||||
local logs=false
|
||||
local cleanup_flag=false
|
||||
|
||||
# 옵션 파싱
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-f|--force)
|
||||
force=true
|
||||
shift
|
||||
;;
|
||||
-s|--status)
|
||||
status=true
|
||||
shift
|
||||
;;
|
||||
-l|--logs)
|
||||
logs=true
|
||||
shift
|
||||
;;
|
||||
-c|--cleanup)
|
||||
cleanup_flag=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [options]"
|
||||
echo "Options:"
|
||||
echo " -f, --force 강제 정지"
|
||||
echo " -s, --status 서버 상태 확인"
|
||||
echo " -l, --logs 로그 표시"
|
||||
echo " -c, --cleanup 정리 작업 수행"
|
||||
echo " -h, --help 이 도움말 표시"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "알 수 없는 옵션: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 시스템 감지
|
||||
detect_system
|
||||
|
||||
# 디렉토리 이동
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# 로그 디렉토리 생성
|
||||
mkdir -p "$PROJECT_ROOT/logs"
|
||||
|
||||
# 상태 확인
|
||||
if [ "$status" = true ]; then
|
||||
check_status
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 로그 표시
|
||||
if [ "$logs" = true ]; then
|
||||
show_logs
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 정리 작업
|
||||
if [ "$cleanup_flag" = true ]; then
|
||||
cleanup
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 서버 정지
|
||||
if [ "$force" = true ]; then
|
||||
force_stop
|
||||
else
|
||||
stop_server
|
||||
fi
|
||||
|
||||
log_success "🚀 인페인팅 서버 정지 완료!"
|
||||
}
|
||||
|
||||
# 스크립트 실행
|
||||
main "$@"
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
# 🧪 API 테스트 가이드
|
||||
|
||||
이 디렉토리는 인페인팅 서버의 API 엔드포인트들을 테스트하기 위한 도구들을 포함합니다.
|
||||
|
||||
## 📁 디렉토리 구조
|
||||
|
||||
```
|
||||
tests/
|
||||
├── images/ # 테스트용 샘플 이미지들
|
||||
├── results/ # 테스트 결과 이미지들
|
||||
├── scripts/ # 테스트 스크립트들
|
||||
└── README.md # 이 파일
|
||||
```
|
||||
|
||||
## 🖼️ 테스트 이미지
|
||||
|
||||
### 생성된 테스트 이미지들
|
||||
|
||||
- **기본 테스트 이미지**
|
||||
- `test_image_256.png` / `test_mask_256.png` (256x256)
|
||||
- `test_image_512.png` / `test_mask_512.png` (512x512)
|
||||
- `test_image_1024.png` / `large_mask_1024.png` (1024x1024)
|
||||
|
||||
- **복잡한 테스트 이미지**
|
||||
- `complex_image_512.png` / `complex_mask_512.png` (512x512)
|
||||
- 체크무늬 패턴과 여러 도형들이 포함된 복잡한 이미지
|
||||
|
||||
### 테스트 이미지 생성
|
||||
|
||||
```bash
|
||||
cd tests/scripts
|
||||
python generate_test_images.py
|
||||
```
|
||||
|
||||
## 🚀 API 테스트
|
||||
|
||||
### 전체 테스트 실행
|
||||
|
||||
```bash
|
||||
cd tests/scripts
|
||||
python test_api.py
|
||||
```
|
||||
|
||||
### 단일 테스트 실행
|
||||
|
||||
```bash
|
||||
# 헬스 체크만
|
||||
python test_api.py --single health
|
||||
|
||||
# 서버 설정 정보만
|
||||
python test_api.py --single config
|
||||
|
||||
# 인페인팅 테스트만
|
||||
python test_api.py --single inpaint
|
||||
|
||||
# 배경 제거 테스트만
|
||||
python test_api.py --single rembg
|
||||
|
||||
# 플러그인 테스트만
|
||||
python test_api.py --single plugin
|
||||
```
|
||||
|
||||
### 다른 서버 URL로 테스트
|
||||
|
||||
```bash
|
||||
python test_api.py --url http://192.168.1.100:8000
|
||||
```
|
||||
|
||||
## 📊 테스트 항목
|
||||
|
||||
### 1. 헬스 체크 (`/health`)
|
||||
- 서버 상태 확인
|
||||
- 가동 시간 확인
|
||||
|
||||
### 2. 서버 설정 정보 (`/api/v1/server-config`)
|
||||
- 시스템 정보 (Jetson Xavier / x86_64)
|
||||
- 사용 가능한 모델 목록
|
||||
- 최대 파일 크기 제한
|
||||
|
||||
### 3. 샘플러 목록 (`/api/v1/samplers`)
|
||||
- Stable Diffusion 샘플러 목록 확인
|
||||
|
||||
### 4. 인페인팅 API (`/api/v1/inpaint`)
|
||||
- Simple LAMA 모델을 사용한 인페인팅
|
||||
- 다양한 크기의 이미지 테스트
|
||||
- 프롬프트, 시드, 설정값 테스트
|
||||
|
||||
### 5. 배경 제거 API (`/api/v1/remove_bg`)
|
||||
- REMBG 모델을 사용한 배경 제거
|
||||
- 결과 이미지와 마스크 반환
|
||||
|
||||
### 6. 플러그인 API (`/api/v1/run_plugin_gen_image`)
|
||||
- 플러그인을 통한 이미지 생성
|
||||
- REMBG 플러그인 테스트
|
||||
|
||||
## 🔧 테스트 전 준비사항
|
||||
|
||||
### 1. 서버 실행
|
||||
```bash
|
||||
# 개발 모드로 실행
|
||||
python main.py --dev
|
||||
|
||||
# 또는 프로덕션 모드로 실행
|
||||
python main.py --host 0.0.0.0 --port 8000 --workers 1
|
||||
```
|
||||
|
||||
### 2. 의존성 설치
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. 테스트 이미지 생성
|
||||
```bash
|
||||
cd tests/scripts
|
||||
python generate_test_images.py
|
||||
```
|
||||
|
||||
## 📈 테스트 결과
|
||||
|
||||
### 성공적인 테스트 결과
|
||||
```
|
||||
🧪 전체 API 테스트 시작
|
||||
==================================================
|
||||
🏥 헬스 체크 테스트...
|
||||
✅ 서버 상태: healthy
|
||||
📊 가동 시간: 15.23초
|
||||
|
||||
⚙️ 서버 설정 정보 테스트...
|
||||
✅ 서버 설정 조회 성공
|
||||
🖥️ 시스템: Jetson Xavier
|
||||
🎮 디바이스: cuda
|
||||
📁 최대 파일 크기: 25MB
|
||||
🎯 사용 가능한 모델: 3개
|
||||
|
||||
🎨 인페인팅 테스트 (test_image_512.png + test_mask_512.png)...
|
||||
✅ 인페인팅 성공!
|
||||
⏱️ 처리 시간: 2.45초
|
||||
🎲 사용된 시드: 42
|
||||
💾 결과 이미지 저장: inpaint_result_test_image_512.png
|
||||
|
||||
==================================================
|
||||
📊 테스트 결과 요약
|
||||
==================================================
|
||||
헬스 체크 ✅ PASS
|
||||
서버 설정 정보 ✅ PASS
|
||||
샘플러 목록 ✅ PASS
|
||||
인페인팅 (256x256) ✅ PASS
|
||||
인페인팅 (512x512) ✅ PASS
|
||||
배경 제거 ✅ PASS
|
||||
플러그인 이미지 생성 ✅ PASS
|
||||
==================================================
|
||||
전체: 7개, 성공: 7개, 실패: 0개
|
||||
🎉 모든 테스트가 성공했습니다!
|
||||
```
|
||||
|
||||
### 실패한 테스트 결과
|
||||
```
|
||||
❌ 인페인팅 실패: 500
|
||||
📝 에러 메시지: 워커 매니저가 초기화되지 않았습니다
|
||||
```
|
||||
|
||||
## 🐛 문제 해결
|
||||
|
||||
### 1. 연결 실패
|
||||
- 서버가 실행 중인지 확인
|
||||
- 포트 번호가 올바른지 확인
|
||||
- 방화벽 설정 확인
|
||||
|
||||
### 2. 이미지 처리 실패
|
||||
- GPU 메모리 부족 확인
|
||||
- 모델 파일이 올바른 위치에 있는지 확인
|
||||
- 로그 파일에서 상세 에러 확인
|
||||
|
||||
### 3. 타임아웃 에러
|
||||
- `timeout` 값을 늘려서 재시도
|
||||
- 서버 성능 확인
|
||||
- 네트워크 상태 확인
|
||||
|
||||
## 📝 로그 확인
|
||||
|
||||
### 서버 로그
|
||||
```bash
|
||||
tail -f logs/main_server.log
|
||||
```
|
||||
|
||||
### 모니터링 로그
|
||||
```bash
|
||||
tail -f logs/monitoring.log
|
||||
```
|
||||
|
||||
## 🔄 지속적 테스트
|
||||
|
||||
### 자동 테스트 스크립트
|
||||
```bash
|
||||
#!/bin/bash
|
||||
while true; do
|
||||
echo "🔄 테스트 실행 중... $(date)"
|
||||
python test_api.py
|
||||
echo "⏳ 60초 대기 중..."
|
||||
sleep 60
|
||||
done
|
||||
```
|
||||
|
||||
이 스크립트를 사용하여 서버의 안정성을 지속적으로 모니터링할 수 있습니다.
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 782 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -0,0 +1,163 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
테스트용 샘플 이미지 생성 스크립트
|
||||
간단한 테스트 이미지와 마스크를 생성합니다.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
import cv2
|
||||
|
||||
# 프로젝트 루트를 Python 경로에 추가
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
def create_test_image(width=512, height=512):
|
||||
"""테스트용 샘플 이미지 생성"""
|
||||
# 그라데이션 배경
|
||||
image = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
|
||||
# 수평 그라데이션 (파란색에서 빨간색)
|
||||
for x in range(width):
|
||||
ratio = x / width
|
||||
blue = int(255 * (1 - ratio))
|
||||
red = int(255 * ratio)
|
||||
image[:, x] = [blue, 0, red]
|
||||
|
||||
# 중앙에 원 그리기
|
||||
center_x, center_y = width // 2, height // 2
|
||||
radius = min(width, height) // 4
|
||||
|
||||
# 원 그리기
|
||||
cv2.circle(image, (center_x, center_y), radius, (0, 255, 0), -1)
|
||||
|
||||
# 텍스트 추가
|
||||
cv2.putText(image, "TEST", (center_x - 50, center_y + 10),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
|
||||
|
||||
return image
|
||||
|
||||
def create_test_mask(width=512, height=512):
|
||||
"""테스트용 마스크 생성 (중앙 원형 영역)"""
|
||||
mask = np.zeros((height, width), dtype=np.uint8)
|
||||
|
||||
center_x, center_y = width // 2, height // 2
|
||||
radius = min(width, height) // 4
|
||||
|
||||
# 중앙 원형 영역을 마스크로 설정 (흰색)
|
||||
cv2.circle(mask, (center_x, center_y), radius, 255, -1)
|
||||
|
||||
return mask
|
||||
|
||||
def create_complex_test_image(width=512, height=512):
|
||||
"""복잡한 테스트 이미지 생성"""
|
||||
image = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
|
||||
# 체크무늬 패턴
|
||||
square_size = 32
|
||||
for y in range(0, height, square_size):
|
||||
for x in range(0, width, square_size):
|
||||
if (x // square_size + y // square_size) % 2 == 0:
|
||||
image[y:y+square_size, x:x+square_size] = [100, 100, 100]
|
||||
else:
|
||||
image[y:y+square_size, x:x+square_size] = [200, 200, 200]
|
||||
|
||||
# 여러 도형 추가
|
||||
# 사각형
|
||||
cv2.rectangle(image, (100, 100), (200, 200), (0, 255, 0), -1)
|
||||
|
||||
# 삼각형
|
||||
pts = np.array([[300, 100], [350, 200], [250, 200]], np.int32)
|
||||
cv2.fillPoly(image, [pts], (255, 0, 0))
|
||||
|
||||
# 타원
|
||||
cv2.ellipse(image, (400, 150), (50, 30), 45, 0, 360, (0, 255, 255), -1)
|
||||
|
||||
return image
|
||||
|
||||
def create_complex_mask(width=512, height=512):
|
||||
"""복잡한 마스크 생성"""
|
||||
mask = np.zeros((height, width), dtype=np.uint8)
|
||||
|
||||
# 사각형 마스크
|
||||
cv2.rectangle(mask, (100, 100), (200, 200), 255, -1)
|
||||
|
||||
# 삼각형 마스크
|
||||
pts = np.array([[300, 100], [350, 200], [250, 200]], np.int32)
|
||||
cv2.fillPoly(mask, [pts], 255)
|
||||
|
||||
# 타원 마스크
|
||||
cv2.ellipse(mask, (400, 150), (50, 30), 45, 0, 360, 255, -1)
|
||||
|
||||
return mask
|
||||
|
||||
def save_test_images():
|
||||
"""테스트 이미지들을 저장합니다."""
|
||||
# 현재 스크립트 위치에서 상대 경로로 테스트 디렉토리 찾기
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(script_dir)
|
||||
images_dir = os.path.join(project_root, "images")
|
||||
|
||||
# 테스트 디렉토리 생성
|
||||
os.makedirs(images_dir, exist_ok=True)
|
||||
|
||||
print(f"🖼️ 테스트 이미지 생성 중...")
|
||||
print(f" 저장 위치: {images_dir}")
|
||||
|
||||
# 1. 기본 테스트 이미지 (512x512)
|
||||
print(" - 기본 테스트 이미지 생성...")
|
||||
test_image = create_test_image(512, 512)
|
||||
test_mask = create_test_mask(512, 512)
|
||||
|
||||
Image.fromarray(cv2.cvtColor(test_image, cv2.COLOR_BGR2RGB)).save(
|
||||
os.path.join(images_dir, "test_image_512.png")
|
||||
)
|
||||
Image.fromarray(test_mask).save(
|
||||
os.path.join(images_dir, "test_mask_512.png")
|
||||
)
|
||||
|
||||
# 2. 복잡한 테스트 이미지 (512x512)
|
||||
print(" - 복잡한 테스트 이미지 생성...")
|
||||
complex_image = create_complex_test_image(512, 512)
|
||||
complex_mask = create_complex_mask(512, 512)
|
||||
|
||||
Image.fromarray(cv2.cvtColor(complex_image, cv2.COLOR_BGR2RGB)).save(
|
||||
os.path.join(images_dir, "complex_image_512.png")
|
||||
)
|
||||
Image.fromarray(complex_mask).save(
|
||||
os.path.join(images_dir, "complex_mask_512.png")
|
||||
)
|
||||
|
||||
# 3. 작은 테스트 이미지 (256x256)
|
||||
print(" - 작은 테스트 이미지 생성...")
|
||||
small_image = create_test_image(256, 256)
|
||||
small_mask = create_test_mask(256, 256)
|
||||
|
||||
Image.fromarray(cv2.cvtColor(small_image, cv2.COLOR_BGR2RGB)).save(
|
||||
os.path.join(images_dir, "test_image_256.png")
|
||||
)
|
||||
Image.fromarray(small_mask).save(
|
||||
os.path.join(images_dir, "test_mask_256.png")
|
||||
)
|
||||
|
||||
# 4. 큰 테스트 이미지 (1024x1024)
|
||||
print(" - 큰 테스트 이미지 생성...")
|
||||
large_image = create_test_image(1024, 1024)
|
||||
large_mask = create_test_mask(1024, 1024)
|
||||
|
||||
Image.fromarray(cv2.cvtColor(large_image, cv2.COLOR_BGR2RGB)).save(
|
||||
os.path.join(images_dir, "test_image_1024.png")
|
||||
)
|
||||
Image.fromarray(large_mask).save(
|
||||
os.path.join(images_dir, "large_mask_1024.png")
|
||||
)
|
||||
|
||||
print(f"✅ 테스트 이미지 생성 완료!")
|
||||
print(f" - 기본 이미지: test_image_512.png, test_mask_512.png")
|
||||
print(f" - 복잡한 이미지: complex_image_512.png, complex_mask_512.png")
|
||||
print(f" - 작은 이미지: test_image_256.png, test_mask_256.png")
|
||||
print(f" - 큰 이미지: test_image_1024.png, large_mask_1024.png")
|
||||
|
||||
if __name__ == "__main__":
|
||||
save_test_images()
|
||||
|
|
@ -0,0 +1,371 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
API 테스트 스크립트
|
||||
각 엔드포인트가 제대로 작동하는지 테스트합니다.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# 프로젝트 루트를 Python 경로에 추가
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
class APITester:
|
||||
def __init__(self, base_url="http://localhost:8000"):
|
||||
self.base_url = base_url
|
||||
self.test_images_dir = os.path.join(project_root, "tests", "images")
|
||||
self.results_dir = os.path.join(project_root, "tests", "results")
|
||||
|
||||
# 결과 디렉토리 생성
|
||||
os.makedirs(self.results_dir, exist_ok=True)
|
||||
|
||||
print(f"🚀 API 테스터 초기화 완료")
|
||||
print(f" 서버 URL: {base_url}")
|
||||
print(f" 테스트 이미지: {self.test_images_dir}")
|
||||
print(f" 결과 저장: {self.results_dir}")
|
||||
|
||||
def image_to_base64(self, image_path):
|
||||
"""이미지를 base64로 인코딩"""
|
||||
try:
|
||||
with open(image_path, "rb") as image_file:
|
||||
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
|
||||
return encoded_string
|
||||
except Exception as e:
|
||||
print(f"❌ 이미지 인코딩 실패: {e}")
|
||||
return None
|
||||
|
||||
def save_base64_image(self, base64_string, filename):
|
||||
"""base64 이미지를 파일로 저장"""
|
||||
try:
|
||||
image_data = base64.b64decode(base64_string)
|
||||
filepath = os.path.join(self.results_dir, filename)
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(image_data)
|
||||
print(f" 💾 결과 이미지 저장: {filename}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 이미지 저장 실패: {e}")
|
||||
return False
|
||||
|
||||
def test_health(self):
|
||||
"""헬스 체크 테스트"""
|
||||
print("\n🏥 헬스 체크 테스트...")
|
||||
try:
|
||||
response = requests.get(f"{self.base_url}/health", timeout=10)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✅ 서버 상태: {data.get('status', 'unknown')}")
|
||||
print(f" 📊 가동 시간: {data.get('uptime', 0):.2f}초")
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ 서버 응답 실패: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" ❌ 연결 실패: {e}")
|
||||
return False
|
||||
|
||||
def test_server_config(self):
|
||||
"""서버 설정 정보 테스트"""
|
||||
print("\n⚙️ 서버 설정 정보 테스트...")
|
||||
try:
|
||||
response = requests.get(f"{self.base_url}/api/v1/server-config", timeout=10)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✅ 서버 설정 조회 성공")
|
||||
print(f" 🖥️ 시스템: {'Jetson Xavier' if data.get('is_jetson') else 'x86_64'}")
|
||||
print(f" 🎮 디바이스: {data.get('device', 'unknown')}")
|
||||
print(f" 📁 최대 파일 크기: {data.get('max_file_size', 0)}MB")
|
||||
print(f" 🎯 사용 가능한 모델: {len(data.get('models', []))}개")
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ 서버 설정 조회 실패: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" ❌ 연결 실패: {e}")
|
||||
return False
|
||||
|
||||
def test_inpaint(self, image_name="test_image_512.png", mask_name="test_mask_512.png"):
|
||||
"""인페인팅 API 테스트"""
|
||||
print(f"\n🎨 인페인팅 테스트 ({image_name} + {mask_name})...")
|
||||
|
||||
# 이미지 파일 경로
|
||||
image_path = os.path.join(self.test_images_dir, image_name)
|
||||
mask_path = os.path.join(self.test_images_dir, mask_name)
|
||||
|
||||
if not os.path.exists(image_path) or not os.path.exists(mask_path):
|
||||
print(f" ❌ 테스트 이미지 파일이 없습니다")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 이미지를 base64로 인코딩
|
||||
image_base64 = self.image_to_base64(image_path)
|
||||
mask_base64 = self.image_to_base64(mask_path)
|
||||
|
||||
if not image_base64 or not mask_base64:
|
||||
return False
|
||||
|
||||
# 인페인팅 요청
|
||||
payload = {
|
||||
"image": image_base64,
|
||||
"mask": mask_base64,
|
||||
"model_name": "simple-lama",
|
||||
"sd_seed": 42,
|
||||
"prompt": "beautiful landscape",
|
||||
"negative_prompt": "ugly, blurry",
|
||||
"num_inference_steps": 20,
|
||||
"guidance_scale": 7.5,
|
||||
"strength": 1.0
|
||||
}
|
||||
|
||||
start_time = time.time()
|
||||
response = requests.post(
|
||||
f"{self.base_url}/api/v1/inpaint",
|
||||
json=payload,
|
||||
timeout=60
|
||||
)
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✅ 인페인팅 성공!")
|
||||
print(f" ⏱️ 처리 시간: {data.get('processing_time', processing_time):.2f}초")
|
||||
print(f" 🎲 사용된 시드: {data.get('seed', 'unknown')}")
|
||||
|
||||
# 결과 이미지 저장
|
||||
if data.get('image'):
|
||||
self.save_base64_image(data['image'], f"inpaint_result_{image_name}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ 인페인팅 실패: {response.status_code}")
|
||||
try:
|
||||
error_data = response.json()
|
||||
print(f" 📝 에러 메시지: {error_data.get('detail', 'unknown error')}")
|
||||
except:
|
||||
print(f" 📝 응답 내용: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ 인페인팅 테스트 실패: {e}")
|
||||
return False
|
||||
|
||||
def test_remove_bg(self, image_name="test_image_512.png"):
|
||||
"""배경 제거 API 테스트"""
|
||||
print(f"\n🖼️ 배경 제거 테스트 ({image_name})...")
|
||||
|
||||
# 이미지 파일 경로
|
||||
image_path = os.path.join(self.test_images_dir, image_name)
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
print(f" ❌ 테스트 이미지 파일이 없습니다")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 이미지를 base64로 인코딩
|
||||
image_base64 = self.image_to_base64(image_path)
|
||||
|
||||
if not image_base64:
|
||||
return False
|
||||
|
||||
# 배경 제거 요청
|
||||
payload = {
|
||||
"image": image_base64,
|
||||
"model_name": "rembg"
|
||||
}
|
||||
|
||||
start_time = time.time()
|
||||
response = requests.post(
|
||||
f"{self.base_url}/api/v1/remove_bg",
|
||||
json=payload,
|
||||
timeout=60
|
||||
)
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✅ 배경 제거 성공!")
|
||||
print(f" ⏱️ 처리 시간: {data.get('processing_time', processing_time):.2f}초")
|
||||
|
||||
# 결과 이미지와 마스크 저장
|
||||
if data.get('image'):
|
||||
self.save_base64_image(data['image'], f"rembg_result_{image_name}")
|
||||
if data.get('mask'):
|
||||
self.save_base64_image(data['mask'], f"rembg_mask_{image_name}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ 배경 제거 실패: {response.status_code}")
|
||||
try:
|
||||
error_data = response.json()
|
||||
print(f" 📝 에러 메시지: {error_data.get('detail', 'unknown error')}")
|
||||
except:
|
||||
print(f" 📝 응답 내용: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ 배경 제거 테스트 실패: {e}")
|
||||
return False
|
||||
|
||||
def test_plugin_generate_image(self, image_name="test_image_512.png"):
|
||||
"""플러그인 이미지 생성 테스트"""
|
||||
print(f"\n🔌 플러그인 이미지 생성 테스트 ({image_name})...")
|
||||
|
||||
# 이미지 파일 경로
|
||||
image_path = os.path.join(self.test_images_dir, image_name)
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
print(f" ❌ 테스트 이미지 파일이 없습니다")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 이미지를 base64로 인코딩
|
||||
image_base64 = self.image_to_base64(image_path)
|
||||
|
||||
if not image_base64:
|
||||
return False
|
||||
|
||||
# 플러그인 요청
|
||||
payload = {
|
||||
"name": "rembg",
|
||||
"image": image_base64,
|
||||
"model_name": "rembg"
|
||||
}
|
||||
|
||||
start_time = time.time()
|
||||
response = requests.post(
|
||||
f"{self.base_url}/api/v1/run_plugin_gen_image",
|
||||
json=payload,
|
||||
timeout=60
|
||||
)
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✅ 플러그인 이미지 생성 성공!")
|
||||
print(f" ⏱️ 처리 시간: {data.get('processing_time', processing_time):.2f}초")
|
||||
|
||||
# 결과 이미지 저장
|
||||
if data.get('image'):
|
||||
self.save_base64_image(data['image'], f"plugin_result_{image_name}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ 플러그인 이미지 생성 실패: {response.status_code}")
|
||||
try:
|
||||
error_data = response.json()
|
||||
print(f" 📝 에러 메시지: {error_data.get('detail', 'unknown error')}")
|
||||
except:
|
||||
print(f" 📝 응답 내용: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ 플러그인 테스트 실패: {e}")
|
||||
return False
|
||||
|
||||
def test_samplers(self):
|
||||
"""샘플러 목록 테스트"""
|
||||
print("\n🎲 샘플러 목록 테스트...")
|
||||
try:
|
||||
response = requests.get(f"{self.base_url}/api/v1/samplers", timeout=10)
|
||||
if response.status_code == 200:
|
||||
samplers = response.json()
|
||||
print(f" ✅ 샘플러 목록 조회 성공")
|
||||
print(f" 📊 사용 가능한 샘플러: {len(samplers)}개")
|
||||
print(f" 🎯 샘플러들: {', '.join(samplers[:5])}{'...' if len(samplers) > 5 else ''}")
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ 샘플러 목록 조회 실패: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" ❌ 연결 실패: {e}")
|
||||
return False
|
||||
|
||||
def run_all_tests(self):
|
||||
"""모든 테스트 실행"""
|
||||
print("🧪 전체 API 테스트 시작")
|
||||
print("=" * 50)
|
||||
|
||||
test_results = []
|
||||
|
||||
# 1. 헬스 체크
|
||||
test_results.append(("헬스 체크", self.test_health()))
|
||||
|
||||
# 2. 서버 설정 정보
|
||||
test_results.append(("서버 설정 정보", self.test_server_config()))
|
||||
|
||||
# 3. 샘플러 목록
|
||||
test_results.append(("샘플러 목록", self.test_samplers()))
|
||||
|
||||
# 4. 인페인팅 테스트 (작은 이미지)
|
||||
test_results.append(("인페인팅 (256x256)", self.test_inpaint("test_image_256.png", "test_mask_256.png")))
|
||||
|
||||
# 5. 인페인팅 테스트 (기본 이미지)
|
||||
test_results.append(("인페인팅 (512x512)", self.test_inpaint("test_image_512.png", "test_mask_512.png")))
|
||||
|
||||
# 6. 배경 제거 테스트
|
||||
test_results.append(("배경 제거", self.test_remove_bg("test_image_512.png")))
|
||||
|
||||
# 7. 플러그인 테스트
|
||||
test_results.append(("플러그인 이미지 생성", self.test_plugin_generate_image("test_image_512.png")))
|
||||
|
||||
# 결과 요약
|
||||
print("\n" + "=" * 50)
|
||||
print("📊 테스트 결과 요약")
|
||||
print("=" * 50)
|
||||
|
||||
passed = 0
|
||||
total = len(test_results)
|
||||
|
||||
for test_name, result in test_results:
|
||||
status = "✅ PASS" if result else "❌ FAIL"
|
||||
print(f"{test_name:<25} {status}")
|
||||
if result:
|
||||
passed += 1
|
||||
|
||||
print("=" * 50)
|
||||
print(f"전체: {total}개, 성공: {passed}개, 실패: {total - passed}개")
|
||||
|
||||
if passed == total:
|
||||
print("🎉 모든 테스트가 성공했습니다!")
|
||||
else:
|
||||
print("⚠️ 일부 테스트가 실패했습니다.")
|
||||
|
||||
return passed == total
|
||||
|
||||
def main():
|
||||
"""메인 함수"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="API 테스트 스크립트")
|
||||
parser.add_argument("--url", default="http://localhost:8000", help="테스트할 서버 URL")
|
||||
parser.add_argument("--single", help="단일 테스트 실행 (health, config, inpaint, rembg, plugin)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
tester = APITester(args.url)
|
||||
|
||||
if args.single:
|
||||
# 단일 테스트 실행
|
||||
if args.single == "health":
|
||||
tester.test_health()
|
||||
elif args.single == "config":
|
||||
tester.test_server_config()
|
||||
elif args.single == "inpaint":
|
||||
tester.test_inpaint()
|
||||
elif args.single == "rembg":
|
||||
tester.test_remove_bg()
|
||||
elif args.single == "plugin":
|
||||
tester.test_plugin_generate_image()
|
||||
else:
|
||||
print(f"❌ 알 수 없는 테스트: {args.single}")
|
||||
else:
|
||||
# 전체 테스트 실행
|
||||
tester.run_all_tests()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 782 B |
|
After Width: | Height: | Size: 1.8 KiB |