first commit

This commit is contained in:
AGX 2025-08-27 13:41:46 +09:00
commit 96c3db9992
51 changed files with 6817 additions and 0 deletions

57
.env.example Normal file
View File

@ -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

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
bin/
include/
lib/
lib64/
share/
local/
build/
.vscode/
.venv/
pyvenv.cfg
*.pyc
*.pyo
*.pyd
*.pyw
*.pyz
*.pywz
*.pyzw
*.pyzwz
*.pyzwzw

384
README.md Normal file
View File

@ -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` 명령어로 시스템 상태를 모니터링하세요!

1
app/__init__.py Normal file
View File

@ -0,0 +1 @@
# 인페인팅 서버 애플리케이션

1
app/api/__init__.py Normal file
View File

@ -0,0 +1 @@
# API 모듈

429
app/api/endpoints.py Normal file
View File

@ -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

1
app/core/__init__.py Normal file
View File

@ -0,0 +1 @@
# 코어 모듈

67
app/core/config.py Normal file
View File

@ -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()

226
app/core/session_pool.py Normal file
View File

@ -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()

329
app/core/worker_manager.py Normal file
View File

@ -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()

1
app/models/__init__.py Normal file
View File

@ -0,0 +1 @@
# 모델 모듈

190
app/models/migan.py Normal file
View File

@ -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)
}

174
app/models/rembg_model.py Normal file
View File

@ -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
}

106
app/models/schemas.py Normal file
View File

@ -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="에러 발생 시간")

159
app/models/simple_lama.py Normal file
View File

@ -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
}

920
app/monitoring/dashboard.py Normal file
View File

@ -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"
)

1
app/utils/__init__.py Normal file
View File

@ -0,0 +1 @@
# 유틸리티 모듈

517
app/utils/gpu_monitor.py Normal file
View File

@ -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()

247
app/utils/image_utils.py Normal file
View File

@ -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

1
lib64 Symbolic link
View File

@ -0,0 +1 @@
lib

0
logs/main.log Normal file
View File

46
logs/main_server.log Normal file
View File

@ -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

1
logs/main_server.pid Normal file
View File

@ -0,0 +1 @@
71852

47
logs/monitoring.log Normal file
View File

@ -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

14
logs/pip_install.log Normal file
View File

@ -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

120
main.py Normal file
View File

@ -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"
)

35
requirements.txt Normal file
View File

@ -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

708
scripts/install_deps.sh Executable file
View File

@ -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 "$@"

488
scripts/start_server.sh Executable file
View File

@ -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 "$@"

465
scripts/status.sh Executable file
View File

@ -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 "$@"

323
scripts/stop_server.sh Executable file
View File

@ -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 "$@"

204
tests/README.md Normal file
View File

@ -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
```
이 스크립트를 사용하여 서버의 안정성을 지속적으로 모니터링할 수 있습니다.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -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()

371
tests/scripts/test_api.py Normal file
View File

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB