From 18e53355a0744d7b53a9c5c959dba11d03b6769e Mon Sep 17 00:00:00 2001 From: hyungi Date: Fri, 25 Jul 2025 06:40:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=B1=EA=B7=B8=EB=9D=BC=EC=9A=B4?= =?UTF-8?q?=EB=93=9C=20AI=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 백그라운드에서 AI 모델을 항상 로딩된 상태로 유지 - 실시간 모델 상태 모니터링 대시보드 웹페이지 구현 - 시스템 성능 지표 수집 및 시각화 - AI 모델 재시작, 캐시 정리 등 관리 기능 - 작업 큐 시스템으로 처리 효율성 향상 - psutil 의존성 추가로 시스템 모니터링 강화 --- requirements.txt | 3 + src/background_ai_service.py | 283 ++++++++++++++++ src/fastapi_with_dashboard.py | 400 +++++++++++++++++++++++ templates/dashboard.html | 585 ++++++++++++++++++++++++++++++++++ 4 files changed, 1271 insertions(+) create mode 100644 src/background_ai_service.py create mode 100644 src/fastapi_with_dashboard.py create mode 100644 templates/dashboard.html diff --git a/requirements.txt b/requirements.txt index 95035c5..7faca7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,3 +30,6 @@ pandas>=2.0.0 # 추가 도구 requests>=2.31.0 pathlib2>=2.3.7 + +# 시스템 모니터링 및 백그라운드 서비스 +psutil>=5.9.0 diff --git a/src/background_ai_service.py b/src/background_ai_service.py new file mode 100644 index 0000000..3566299 --- /dev/null +++ b/src/background_ai_service.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +""" +백그라운드 AI 모델 서비스 및 모니터링 시스템 +실시간 모델 상태 추적 및 성능 지표 수집 +""" + +import asyncio +import time +import psutil +import threading +from datetime import datetime, timedelta +from dataclasses import dataclass, asdict +from typing import Dict, List, Optional +from pathlib import Path +import json +import logging + +from integrated_translation_system import IntegratedTranslationSystem + +@dataclass +class ModelStatus: + """개별 모델 상태 정보""" + name: str + status: str # "loading", "ready", "error", "unloaded" + load_time: Optional[float] = None + memory_usage_mb: float = 0.0 + last_used: Optional[datetime] = None + error_message: Optional[str] = None + total_processed: int = 0 + +@dataclass +class SystemMetrics: + """전체 시스템 성능 지표""" + total_memory_usage_mb: float + cpu_usage_percent: float + active_jobs: int + queued_jobs: int + completed_jobs_today: int + average_processing_time: float + uptime_seconds: float + +class BackgroundAIService: + """백그라운드 AI 모델 서비스 관리자""" + + def __init__(self): + self.models_status: Dict[str, ModelStatus] = {} + self.translation_system: Optional[IntegratedTranslationSystem] = None + self.start_time = datetime.now() + self.job_queue = asyncio.Queue() + self.active_jobs = {} + self.completed_jobs = [] + self.metrics_history = [] + + # 로깅 설정 + self.logger = logging.getLogger(__name__) + self.logger.setLevel(logging.INFO) + + # 모델 상태 초기화 + self._initialize_model_status() + + # 백그라운드 워커 및 모니터링 시작 + self.worker_task = None + self.monitoring_task = None + + def _initialize_model_status(self): + """모델 상태 초기화""" + model_names = ["NLLB 번역", "KoBART 요약"] + + for name in model_names: + self.models_status[name] = ModelStatus( + name=name, + status="unloaded" + ) + + async def start_service(self): + """백그라운드 서비스 시작""" + self.logger.info("🚀 백그라운드 AI 서비스 시작") + + # 모델 로딩 시작 + asyncio.create_task(self._load_models()) + + # 백그라운드 워커 시작 + self.worker_task = asyncio.create_task(self._process_job_queue()) + + # 모니터링 시작 + self.monitoring_task = asyncio.create_task(self._collect_metrics()) + + self.logger.info("✅ 백그라운드 서비스 준비 완료") + + async def _load_models(self): + """AI 모델들을 순차적으로 로딩""" + try: + # NLLB 모델 로딩 시작 + self.models_status["NLLB 번역"].status = "loading" + load_start = time.time() + + self.translation_system = IntegratedTranslationSystem() + + # 실제 모델 로딩 + success = await asyncio.to_thread(self.translation_system.load_models) + + if success: + load_time = time.time() - load_start + + # 각 모델 상태 업데이트 + self.models_status["NLLB 번역"].status = "ready" + self.models_status["NLLB 번역"].load_time = load_time + + self.models_status["KoBART 요약"].status = "ready" + self.models_status["KoBART 요약"].load_time = load_time + + self.logger.info(f"✅ 모든 AI 모델 로딩 완료 ({load_time:.1f}초)") + + else: + # 로딩 실패 + for model_name in self.models_status.keys(): + self.models_status[model_name].status = "error" + self.models_status[model_name].error_message = "모델 로딩 실패" + + self.logger.error("❌ AI 모델 로딩 실패") + + except Exception as e: + self.logger.error(f"❌ 모델 로딩 중 오류: {e}") + for model_name in self.models_status.keys(): + self.models_status[model_name].status = "error" + self.models_status[model_name].error_message = str(e) + + def _get_memory_usage(self) -> float: + """현재 프로세스의 메모리 사용량 반환 (MB)""" + try: + process = psutil.Process() + return process.memory_info().rss / 1024 / 1024 # bytes to MB + except: + return 0.0 + + async def _collect_metrics(self): + """주기적으로 시스템 메트릭 수집""" + while True: + try: + # 메모리 사용량 업데이트 + total_memory = self._get_memory_usage() + + # 각 모델별 메모리 사용량 추정 (실제로는 더 정교한 측정 필요) + if self.models_status["NLLB 번역"].status == "ready": + self.models_status["NLLB 번역"].memory_usage_mb = total_memory * 0.6 + if self.models_status["KoBART 요약"].status == "ready": + self.models_status["KoBART 요약"].memory_usage_mb = total_memory * 0.4 + + # 전체 시스템 메트릭 수집 + metrics = SystemMetrics( + total_memory_usage_mb=total_memory, + cpu_usage_percent=psutil.cpu_percent(), + active_jobs=len(self.active_jobs), + queued_jobs=self.job_queue.qsize(), + completed_jobs_today=len([ + job for job in self.completed_jobs + if job.get('completed_at', datetime.min).date() == datetime.now().date() + ]), + average_processing_time=self._calculate_average_processing_time(), + uptime_seconds=(datetime.now() - self.start_time).total_seconds() + ) + + # 메트릭 히스토리에 추가 (최근 100개만 유지) + self.metrics_history.append({ + 'timestamp': datetime.now(), + 'metrics': metrics + }) + if len(self.metrics_history) > 100: + self.metrics_history.pop(0) + + await asyncio.sleep(5) # 5초마다 수집 + + except Exception as e: + self.logger.error(f"메트릭 수집 오류: {e}") + await asyncio.sleep(10) + + def _calculate_average_processing_time(self) -> float: + """최근 처리된 작업들의 평균 처리 시간 계산""" + recent_jobs = [ + job for job in self.completed_jobs[-20:] # 최근 20개 + if 'processing_time' in job + ] + + if not recent_jobs: + return 0.0 + + return sum(job['processing_time'] for job in recent_jobs) / len(recent_jobs) + + async def _process_job_queue(self): + """작업 큐 처리 워커""" + while True: + try: + # 큐에서 작업 가져오기 + job = await self.job_queue.get() + job_id = job['job_id'] + + # 활성 작업에 추가 + self.active_jobs[job_id] = { + 'start_time': datetime.now(), + 'job_data': job + } + + # 실제 AI 처리 + await self._process_single_job(job) + + # 완료된 작업으로 이동 + processing_time = (datetime.now() - self.active_jobs[job_id]['start_time']).total_seconds() + + self.completed_jobs.append({ + 'job_id': job_id, + 'completed_at': datetime.now(), + 'processing_time': processing_time + }) + + # 활성 작업에서 제거 + del self.active_jobs[job_id] + + # 모델 사용 시간 업데이트 + for model_status in self.models_status.values(): + if model_status.status == "ready": + model_status.last_used = datetime.now() + model_status.total_processed += 1 + + except Exception as e: + self.logger.error(f"작업 처리 오류: {e}") + if job_id in self.active_jobs: + del self.active_jobs[job_id] + + async def _process_single_job(self, job): + """개별 작업 처리""" + # 기존 번역 시스템 로직 사용 + if self.translation_system and self.models_status["NLLB 번역"].status == "ready": + result = await asyncio.to_thread( + self.translation_system.process_document, + job['file_path'] + ) + return result + else: + raise Exception("AI 모델이 준비되지 않음") + + async def add_job(self, job_data: Dict): + """새 작업을 큐에 추가""" + await self.job_queue.put(job_data) + + def get_dashboard_data(self) -> Dict: + """대시보드용 데이터 반환""" + current_metrics = self.metrics_history[-1]['metrics'] if self.metrics_history else None + + return { + 'models_status': {name: asdict(status) for name, status in self.models_status.items()}, + 'current_metrics': asdict(current_metrics) if current_metrics else None, + 'recent_metrics': [ + { + 'timestamp': entry['timestamp'].isoformat(), + 'metrics': asdict(entry['metrics']) + } + for entry in self.metrics_history[-20:] # 최근 20개 + ], + 'active_jobs': len(self.active_jobs), + 'completed_today': len([ + job for job in self.completed_jobs + if job.get('completed_at', datetime.min).date() == datetime.now().date() + ]) + } + + async def restart_models(self): + """모델 재시작""" + self.logger.info("🔄 AI 모델 재시작 중...") + + # 모든 모델 상태를 재로딩으로 설정 + for model_status in self.models_status.values(): + model_status.status = "loading" + model_status.error_message = None + + # 기존 시스템 정리 + if self.translation_system: + del self.translation_system + + # 모델 재로딩 + await self._load_models() + +# 전역 서비스 인스턴스 +ai_service = BackgroundAIService() \ No newline at end of file diff --git a/src/fastapi_with_dashboard.py b/src/fastapi_with_dashboard.py new file mode 100644 index 0000000..71d5d76 --- /dev/null +++ b/src/fastapi_with_dashboard.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +""" +Mac Mini (192.168.1.122) FastAPI + DS1525+ (192.168.1.227) 연동 +백그라운드 AI 서비스 및 대시보드 통합 버전 +""" + +from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks, Request +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse, FileResponse, JSONResponse +import asyncio +import json +import uuid +from pathlib import Path +from typing import Dict, List, Optional +import time +import shutil +from dataclasses import dataclass, asdict +import aiofiles +from datetime import datetime +import subprocess +import logging + +# 백그라운드 AI 서비스 임포트 +from background_ai_service import ai_service + +# 실제 네트워크 설정 +MAC_MINI_IP = "192.168.1.122" +NAS_IP = "192.168.1.227" + +# NAS 마운트 경로 (Finder에서 연결 시 생성되는 경로) +NAS_MOUNT_POINT = Path("/Volumes/DS1525+") +DOCUMENT_UPLOAD_BASE = NAS_MOUNT_POINT / "Document-upload" + +# 세부 경로들 +NAS_ORIGINALS_PATH = DOCUMENT_UPLOAD_BASE / "originals" +NAS_TRANSLATED_PATH = DOCUMENT_UPLOAD_BASE / "translated" +NAS_STATIC_HOSTING_PATH = DOCUMENT_UPLOAD_BASE / "static-hosting" +NAS_METADATA_PATH = DOCUMENT_UPLOAD_BASE / "metadata" + +# 로컬 작업 디렉토리 +LOCAL_WORK_PATH = Path.home() / "Scripts" / "nllb-translation-system" + +# 로깅 설정 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +app = FastAPI( + title="AI 번역 시스템 with 대시보드", + description=f"Mac Mini ({MAC_MINI_IP}) + DS1525+ ({NAS_IP}) 연동 + 실시간 모니터링", + version="2.0.0" +) + +# 정적 파일 및 템플릿 +app.mount("/static", StaticFiles(directory=str(LOCAL_WORK_PATH / "static")), name="static") +templates = Jinja2Templates(directory=str(LOCAL_WORK_PATH / "templates")) + +# 작업 상태 관리 (백그라운드 서비스와 연동) +processing_jobs: Dict[str, Dict] = {} + +def check_nas_connection(): + """NAS 연결 상태 실시간 확인""" + try: + # 1. ping 테스트 + ping_result = subprocess.run( + ["ping", "-c", "1", "-W", "3000", NAS_IP], + capture_output=True, + timeout=5 + ) + + if ping_result.returncode != 0: + return {"status": "offline", "error": f"NAS {NAS_IP} ping 실패"} + + # 2. 마운트 상태 확인 + if not NAS_MOUNT_POINT.exists(): + return {"status": "not_mounted", "error": "마운트 포인트 없음"} + + # 3. 실제 마운트 확인 + mount_result = subprocess.run( + ["mount"], + capture_output=True, + text=True + ) + + if str(NAS_MOUNT_POINT) not in mount_result.stdout: + return {"status": "not_mounted", "error": "NAS가 마운트되지 않음"} + + # 4. Document-upload 폴더 확인 + if not DOCUMENT_UPLOAD_BASE.exists(): + return {"status": "folder_missing", "error": "Document-upload 폴더 없음"} + + return {"status": "connected", "mount_point": str(NAS_MOUNT_POINT)} + + except subprocess.TimeoutExpired: + return {"status": "timeout", "error": "연결 타임아웃"} + except Exception as e: + return {"status": "error", "error": str(e)} + +@app.on_event("startup") +async def startup_event(): + """서버 시작시 전체 시스템 상태 확인 및 백그라운드 서비스 시작""" + logger.info(f"🚀 Mac Mini AI 번역 서버 시작 (v2.0)") + logger.info(f"📍 Mac Mini IP: {MAC_MINI_IP}") + logger.info(f"📍 NAS IP: {NAS_IP}") + logger.info("-" * 50) + + # 백그라운드 AI 서비스 시작 + await ai_service.start_service() + + # NAS 연결 상태 확인 + nas_status = check_nas_connection() + + if nas_status["status"] == "connected": + logger.info(f"✅ NAS 연결 정상: {nas_status['mount_point']}") + + # 필요한 폴더 구조 자동 생성 + try: + current_month = datetime.now().strftime("%Y-%m") + + folders_to_create = [ + NAS_ORIGINALS_PATH / current_month / "pdfs", + NAS_ORIGINALS_PATH / current_month / "docs", + NAS_ORIGINALS_PATH / current_month / "txts", + NAS_TRANSLATED_PATH / current_month / "english-to-korean", + NAS_TRANSLATED_PATH / current_month / "japanese-to-korean", + NAS_TRANSLATED_PATH / current_month / "korean-only", + NAS_STATIC_HOSTING_PATH / "docs", + NAS_STATIC_HOSTING_PATH / "assets", + NAS_METADATA_PATH / "processing-logs" + ] + + for folder in folders_to_create: + folder.mkdir(parents=True, exist_ok=True) + + logger.info(f"✅ 폴더 구조 확인/생성 완료") + + except Exception as e: + logger.warning(f"⚠️ 폴더 생성 실패: {e}") + + else: + logger.error(f"❌ NAS 연결 실패: {nas_status['error']}") + logger.info("해결 방법:") + logger.info("1. NAS 전원 및 네트워크 상태 확인") + logger.info("2. Finder에서 DS1525+ 수동 연결:") + logger.info(f" - 이동 → 서버에 연결 → smb://{NAS_IP}") + logger.info("3. 연결 후 서버 재시작") + +# ========================================== +# 기존 엔드포인트들 +# ========================================== + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + """메인 페이지""" + nas_status = check_nas_connection() + return templates.TemplateResponse("index.html", { + "request": request, + "nas_status": nas_status + }) + +@app.get("/system-status") +async def system_status(): + """전체 시스템 상태 확인""" + + # NAS 상태 + nas_status = check_nas_connection() + + # Mac Mini 상태 + mac_status = { + "ip": MAC_MINI_IP, + "hostname": subprocess.getoutput("hostname"), + "uptime": subprocess.getoutput("uptime"), + "disk_usage": {} + } + + # 디스크 사용량 확인 + try: + disk_result = subprocess.run( + ["df", "-h", str(Path.home())], + capture_output=True, + text=True + ) + if disk_result.returncode == 0: + lines = disk_result.stdout.strip().split('\n') + if len(lines) > 1: + parts = lines[1].split() + mac_status["disk_usage"] = { + "total": parts[1], + "used": parts[2], + "available": parts[3], + "percentage": parts[4] + } + except: + pass + + # AI 모델 상태 (백그라운드 서비스에서 가져오기) + ai_status = ai_service.get_dashboard_data() + + # 작업 통계 + job_stats = { + "total_jobs": len(processing_jobs), + "completed": len([j for j in processing_jobs.values() if j["status"] == "completed"]), + "processing": len([j for j in processing_jobs.values() if j["status"] == "processing"]), + "failed": len([j for j in processing_jobs.values() if j["status"] == "error"]) + } + + return { + "timestamp": datetime.now().isoformat(), + "nas": nas_status, + "mac_mini": mac_status, + "ai_models": ai_status, + "jobs": job_stats + } + +@app.post("/upload") +async def upload_file(background_tasks: BackgroundTasks, file: UploadFile = File(...)): + """파일 업로드 (백그라운드 AI 서비스 연동)""" + + # NAS 연결 상태 먼저 확인 + nas_status = check_nas_connection() + if nas_status["status"] != "connected": + raise HTTPException( + status_code=503, + detail=f"NAS 연결 실패: {nas_status['error']}. Finder에서 DS1525+를 연결해주세요." + ) + + # 파일 검증 + if not file.filename: + raise HTTPException(status_code=400, detail="파일이 선택되지 않았습니다.") + + allowed_extensions = {'.pdf', '.txt', '.docx', '.doc'} + file_ext = Path(file.filename).suffix.lower() + + if file_ext not in allowed_extensions: + raise HTTPException( + status_code=400, + detail=f"지원되지 않는 파일 형식입니다. 지원 형식: {', '.join(allowed_extensions)}" + ) + + # 파일 크기 확인 (100MB 제한) + content = await file.read() + if len(content) > 100 * 1024 * 1024: + raise HTTPException(status_code=413, detail="파일 크기는 100MB를 초과할 수 없습니다.") + + # 고유 작업 ID 생성 + job_id = str(uuid.uuid4()) + + # NAS에 체계적으로 저장 + current_month = datetime.now().strftime("%Y-%m") + file_type_folder = { + '.pdf': 'pdfs', + '.doc': 'docs', + '.docx': 'docs', + '.txt': 'txts' + }.get(file_ext, 'others') + + nas_original_dir = NAS_ORIGINALS_PATH / current_month / file_type_folder + nas_original_dir.mkdir(parents=True, exist_ok=True) + + # 안전한 파일명 생성 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + safe_filename = f"{timestamp}_{job_id[:8]}_{file.filename}".replace(" ", "_") + nas_file_path = nas_original_dir / safe_filename + + # 파일 저장 + try: + async with aiofiles.open(nas_file_path, 'wb') as f: + await f.write(content) + + logger.info(f"📁 파일 저장 완료: {nas_file_path}") + + except Exception as e: + raise HTTPException(status_code=500, detail=f"파일 저장 실패: {str(e)}") + + # 작업 상태 초기화 + job = { + "id": job_id, + "filename": file.filename, + "file_type": file_ext, + "status": "uploaded", + "progress": 0, + "message": f"파일 업로드 완료, NAS에 저장됨 ({nas_file_path.name})", + "nas_original_path": str(nas_file_path), + "created_at": time.time() + } + + processing_jobs[job_id] = job + + # 백그라운드 AI 서비스에 작업 추가 + await ai_service.add_job({ + 'job_id': job_id, + 'file_path': str(nas_file_path), + 'original_filename': file.filename + }) + + return { + "job_id": job_id, + "message": "파일 업로드 완료, 백그라운드 AI 처리를 시작합니다.", + "nas_path": str(nas_file_path) + } + +@app.get("/status/{job_id}") +async def get_job_status(job_id: str): + """작업 상태 조회""" + if job_id not in processing_jobs: + raise HTTPException(status_code=404, detail="작업을 찾을 수 없습니다.") + + return processing_jobs[job_id] + +# ========================================== +# 새로운 대시보드 엔드포인트들 +# ========================================== + +@app.get("/dashboard", response_class=HTMLResponse) +async def dashboard(request: Request): + """AI 모델 대시보드 페이지""" + return templates.TemplateResponse("dashboard.html", { + "request": request + }) + +@app.get("/api/dashboard") +async def get_dashboard_data(): + """대시보드용 실시간 데이터 API""" + return ai_service.get_dashboard_data() + +@app.post("/api/restart-models") +async def restart_models(): + """AI 모델 재시작 API""" + try: + await ai_service.restart_models() + return {"message": "AI 모델 재시작이 완료되었습니다."} + except Exception as e: + raise HTTPException(status_code=500, detail=f"모델 재시작 실패: {str(e)}") + +@app.post("/api/clear-cache") +async def clear_cache(): + """시스템 캐시 정리 API""" + try: + # 여기서 실제 캐시 정리 로직 구현 + # 예: 임시 파일 삭제, 메모리 정리 등 + + import gc + gc.collect() # 가비지 컬렉션 실행 + + return {"message": "시스템 캐시가 정리되었습니다."} + except Exception as e: + raise HTTPException(status_code=500, detail=f"캐시 정리 실패: {str(e)}") + +@app.get("/api/models/status") +async def get_models_status(): + """AI 모델 상태만 조회하는 API""" + dashboard_data = ai_service.get_dashboard_data() + return dashboard_data.get('models_status', {}) + +@app.get("/api/metrics/history") +async def get_metrics_history(): + """성능 지표 히스토리 조회 API""" + dashboard_data = ai_service.get_dashboard_data() + return dashboard_data.get('recent_metrics', []) + +# ========================================== +# 유틸리티 함수들 +# ========================================== + +async def save_processing_metadata(job_id: str, original_path: Path, html_path: Path, metadata: Dict): + """처리 메타데이터 NAS에 저장""" + + log_data = { + "job_id": job_id, + "timestamp": datetime.now().isoformat(), + "original_file": str(original_path), + "html_file": str(html_path), + "metadata": metadata, + "nas_ip": NAS_IP, + "mac_mini_ip": MAC_MINI_IP, + "ai_service_version": "2.0" + } + + # 월별 로그 파일에 추가 + current_month = datetime.now().strftime("%Y-%m") + log_file = NAS_METADATA_PATH / "processing-logs" / f"{current_month}.jsonl" + + try: + async with aiofiles.open(log_file, 'a', encoding='utf-8') as f: + await f.write(json.dumps(log_data, ensure_ascii=False) + '\n') + except Exception as e: + logger.error(f"메타데이터 저장 실패: {e}") + +if __name__ == "__main__": + import uvicorn + + logger.info(f"🚀 Mac Mini AI 번역 서버 with 대시보드") + logger.info(f"📡 서버 주소: http://{MAC_MINI_IP}:8080") + logger.info(f"📊 대시보드: http://{MAC_MINI_IP}:8080/dashboard") + logger.info(f"📁 NAS 주소: {NAS_IP}") + + uvicorn.run(app, host="0.0.0.0", port=8080) \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..233625c --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,585 @@ + + + + + + AI 모델 대시보드 - NLLB 번역 시스템 + + + +
+
+
+ 실시간 업데이트 +
+
+ +
+
+

🤖 AI 모델 대시보드

+

NLLB 번역 시스템 - 실시간 모니터링

+
+ +
+ +
+
+ 🧠 +

AI 모델 상태

+
+
+ +
+ +
+ + +
+
+ + +
+
+ 📊 +

시스템 성능

+
+
+ +
+
+ + +
+
+ 📈 +

작업 통계

+
+
+ +
+ +
+ +
+
+ + +
+
+ +

활성 작업

+
+
+ +
+
+
+
+ + + + \ No newline at end of file