commit 397efb86dc84197b74d9a3b16a11b1d0d534ad9e Author: hyungi Date: Wed Aug 13 08:38:41 2025 +0900 Squashed 'integrations/document-ai/' content from commit 9093611 git-subtree-dir: integrations/document-ai git-subtree-split: 9093611c9629c0de3db760878ec9929f50add5ed diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9147ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# ========================= +# Python 가상환경 +# ========================= +# 각 개발자의 로컬 환경에 따라 내용이 다르며, +# 용량이 매우 크기 때문에 Git으로 관리하지 않습니다. +# 대신 'requirements.txt' 파일을 통해 필요한 라이브러리를 관리합니다. +nllb_env/ + +# ========================= +# AI 모델 파일 +# ========================= +# 수 GB에 달하는 매우 큰 파일들입니다. +# Git은 대용량 파일을 관리하기에 적합하지 않으므로 제외합니다. +# 모델은 별도의 방법(예: 다운로드 스크립트)으로 관리해야 합니다. +models/ + +# ========================= +# 데이터 파일 +# ========================= +# 원본 데이터나 학습 데이터는 용량이 클 수 있으므로 제외합니다. +data/ + +# ========================= +# 실행 결과물 +# ========================= +# 코드를 실행하면 자동으로 생성되는 출력 파일들입니다. +# 소스 코드가 아니므로 버전 관리 대상에서 제외합니다. +output/ + +# ========================= +# Python 캐시 파일 +# ========================= +# Python 인터프리터가 실행 속도 향상을 위해 자동으로 생성하는 파일들입니다. +__pycache__/ +*.pyc + +# ========================= +# macOS 시스템 파일 +# ========================= +# macOS의 Finder가 자동으로 생성하는 시스템 파일입니다. +.DS_Store + +# ========================= +# IDE 및 에디터 설정 +# ========================= +.idea/ +.vscode/ \ No newline at end of file diff --git a/CODING_CONVENTIONS.md b/CODING_CONVENTIONS.md new file mode 100644 index 0000000..530969d --- /dev/null +++ b/CODING_CONVENTIONS.md @@ -0,0 +1,102 @@ +# NLLB 번역 시스템 코딩 규칙 + +이 문서는 NLLB 번역 시스템 프로젝트의 코드 일관성과 품질을 유지하기 위한 코딩 규칙을 정의합니다. + +## 1. 일반 원칙 + +- **언어**: 모든 코드와 주석은 **한국어**로 작성하는 것을 원칙으로 합니다. 단, 외부 라이브러리 이름이나 기술 용어 등은 예외로 합니다. +- **인코딩**: 모든 파일은 **UTF-8**로 인코딩합니다. +- **명령형**: 커밋 메시지, 함수/메서드 설명 등은 명령형(`~하기`, `~함`)으로 작성하여 명확성을 높입니다. + +## 2. 코드 스타일 + +- **들여쓰기**: 공백 4칸을 사용합니다. +- **최대 줄 길이**: 한 줄은 120자를 넘지 않도록 합니다. +- **명명 규칙**: + - **변수**: `snake_case` (예: `translated_text`) + - **함수/메서드**: `snake_case` (예: `load_models`) + - **클래스**: `PascalCase` (예: `IntegratedTranslationSystem`) + - **상수**: `UPPER_SNAKE_CASE` (예: `NAS_MOUNT_POINT`) +- **주석**: + - 복잡한 로직이나 중요한 결정 사항에 대해서는 `#`을 사용하여 간결한 주석을 작성합니다. + - 파일의 목적과 전반적인 설명을 위해 파일 최상단에 `"""독스트링"""`을 작성합니다. +- **타입 힌팅**: 함수의 인자와 반환 값에는 타입 힌트를 명시하여 코드의 명확성을 높입니다. (예: `def process_document(input_path: str) -> TranslationResult:`) + +## 3. Python 관련 규칙 + +- **임포트**: + 1. 표준 라이브러리 + 2. 서드파티 라이브러리 + 3. 로컬 애플리케이션/라이브러리 순으로 그룹화하고, 각 그룹 사이에는 한 줄을 띄웁니다. + - 예시: + ```python + import asyncio + import json + from pathlib import Path + + from fastapi import FastAPI + import torch + + from .integrated_translation_system import IntegratedTranslationSystem + ``` +- **Docstrings**: 모든 모듈, 함수, 클래스, 메서드에 대해 Google Python 스타일 가이드를 따르는 독스트링을 작성합니다. +- **오류 처리**: `try...except` 블록을 사용하여 예상되는 오류를 명시적으로 처리하고, 오류 발생 시 로그를 남기거나 적절한 예외를 발생시킵니다. + +## 4. FastAPI 관련 규칙 + +- **엔드포인트**: + - URL 경로는 소문자와 하이픈(`-`)을 사용합니다. (예: `/system-status`) + - 응답 모델을 사용하여 API의 입출력 형식을 명확히 정의합니다. +- **비동기 처리**: I/O 바운드 작업(파일 읽기/쓰기, 네트워크 요청 등)에는 `async`와 `await`를 적극적으로 사용하여 비동기 처리를 구현합니다. +- **백그라운드 작업**: 시간이 오래 걸리는 작업(모델 로딩, 번역 등)은 `BackgroundTasks`를 사용하여 사용자 경험을 저해하지 않도록 합니다. + +## 5. 프로젝트 구조 + +- `config/`: 설정 파일 (예: `settings.json`) +- `data/`: 원본 데이터 및 학습 데이터 +- `models/`: 학습된 AI 모델 파일 +- `src/`: 핵심 소스 코드 +- `tests/`: 테스트 코드 +- `static/`, `templates/`: FastAPI 웹 인터페이스 파일 + +## 6. 네트워크 설정 + +- **Mac Mini IP**: `192.168.1.122` (AI 번역 서버 호스트) +- **DS1525+ NAS IP**: `192.168.1.227` (문서 저장소) +- **NAS 마운트 포인트**: `/Volumes/DS1525+` +- **서버 포트**: `8080` (FastAPI 웹 서비스) +- **대시보드 접속**: `http://192.168.1.122:8080/dashboard` + +### 6.1. API 키 인증 + +관리자용 API(`/api/restart-models`, `/api/clear-cache` 등)를 호출하려면 HTTP 요청 헤더에 API 키를 포함해야 합니다. + +- **헤더 이름**: `X-API-KEY` +- **API 키 값**: `config/settings.json` 파일의 `security.api_key` 값을 사용합니다. + +**cURL 예시:** + +```bash +curl -X POST http://192.168.1.122:20080/api/restart-models \ +-H "X-API-KEY: nllb-secret-key-!@#$%" +``` + +## 7. 서버 운영 및 배포 (macOS) + +이 서버는 24/7 무중단 운영을 위해 macOS의 `launchd` 서비스를 통해 관리됩니다. 이를 통해 시스템 재부팅 시 자동 시작 및 예기치 않은 종료 시 자동 재시작이 보장됩니다. + +- **서비스 설정 파일**: `~/Library/LaunchAgents/com.nllb-translation-system.app.plist` + - 이 파일은 서비스의 실행 방법, 로그 경로, 자동 재시작 여부 등을 정의합니다. + +- **서비스 제어 명령어**: + - **시작**: `launchctl start com.nllb-translation-system.app` + - **중지**: `launchctl stop com.nllb-translation-system.app` + - **재시작 (설정 파일 수정 후)**: + 1. `launchctl unload ~/Library/LaunchAgents/com.nllb-translation-system.app.plist` + 2. `launchctl load ~/Library/LaunchAgents/com.nllb-translation-system.app.plist` + +- **로그 확인**: + - **일반 로그**: `tail -f logs/service.log` + - **에러 로그**: `tail -f logs/service_error.log` + +- **주의**: 개발 및 디버깅 시에는 `launchd` 서비스를 중지(`launchctl stop ...`)한 후, 터미널에서 직접 `python src/fastapi_with_dashboard.py`를 실행해야 포트 충돌이 발생하지 않습니다. \ No newline at end of file diff --git a/check_installation.py b/check_installation.py new file mode 100644 index 0000000..00f9d92 --- /dev/null +++ b/check_installation.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +""" +환경 설정 확인 스크립트 +""" + +def check_installation(): + print("🔍 설치된 패키지 확인 중...") + + try: + import torch + print(f"✅ PyTorch: {torch.__version__}") + + # Apple Silicon MPS 확인 + if torch.backends.mps.is_available(): + print("✅ Apple Silicon MPS 가속 사용 가능") + else: + print("⚠️ MPS 가속 사용 불가 (CPU 모드)") + + except ImportError: + print("❌ PyTorch 설치 실패") + return False + + try: + import transformers + print(f"✅ Transformers: {transformers.__version__}") + except ImportError: + print("❌ Transformers 설치 실패") + return False + + try: + import sentencepiece + print("✅ SentencePiece 설치 완료") + except ImportError: + print("❌ SentencePiece 설치 실패") + return False + + try: + import accelerate + print("✅ Accelerate 설치 완료") + except ImportError: + print("❌ Accelerate 설치 실패") + return False + + # 문서 처리 라이브러리 확인 + doc_libs = [] + try: + import PyPDF2 + doc_libs.append("PyPDF2") + except ImportError: + pass + + try: + import pdfplumber + doc_libs.append("pdfplumber") + except ImportError: + pass + + try: + from docx import Document + doc_libs.append("python-docx") + except ImportError: + pass + + if doc_libs: + print(f"✅ 문서 처리: {', '.join(doc_libs)}") + else: + print("⚠️ 문서 처리 라이브러리 설치 필요") + + # 시스템 정보 + print(f"\n📊 시스템 정보:") + print(f" Python 버전: {torch.version.python}") + if torch.backends.mps.is_available(): + print(f" 디바이스: Apple Silicon (MPS)") + else: + print(f" 디바이스: CPU") + + return True + +if __name__ == "__main__": + print("🚀 NLLB 번역 시스템 환경 확인") + print("=" * 40) + + if check_installation(): + print("\n🎉 환경 설정 완료!") + print("다음 단계: NLLB 모델 다운로드") + else: + print("\n❌ 환경 설정 실패") + print("패키지 재설치 필요") diff --git a/config/settings.json b/config/settings.json new file mode 100644 index 0000000..e098750 --- /dev/null +++ b/config/settings.json @@ -0,0 +1,45 @@ +{ + "models": { + "translation": "facebook/nllb-200-3.3B", + "summarization": "ainize/kobart-news", + "embedding": "nomic-embed-text" + }, + "translation": { + "max_length": 512, + "num_beams": 4, + "early_stopping": true, + "batch_size": 4 + }, + "summarization": { + "max_length": 150, + "min_length": 30, + "num_beams": 4 + }, + "processing": { + "chunk_size": 500, + "overlap": 50, + "concurrent_chunks": 3 + }, + "output": { + "html_template": "modern", + "include_toc": true, + "include_summary": true + }, + "network": { + "mac_mini_ip": "192.168.1.122", + "nas_ip": "192.168.1.227", + "server_port": 20080 + }, + "paths": { + "nas_mount_point": "/Volumes/Media", + "document_upload_base": "Document-upload", + "originals": "originals", + "translated": "translated", + "static_hosting": "static-hosting", + "metadata": "metadata", + "local_work_path": "~/Scripts/nllb-translation-system" + }, + "security": { + "api_key": "nllb-secret-key-!@#$%" + } +} diff --git a/logs/service.log b/logs/service.log new file mode 100644 index 0000000..b66119b --- /dev/null +++ b/logs/service.log @@ -0,0 +1,11 @@ +번역 시스템 초기화 (디바이스: mps) +모델 로딩 중... + NLLB 번역 모델... +INFO: 192.168.1.122:51436 - "GET /api/dashboard HTTP/1.1" 200 OK + NLLB 모델 로드 완료 + KoBART 요약 모델... + KoBART 모델 로드 완료 +모델 로딩 완료! +INFO: 192.168.1.122:51443 - "GET /api/dashboard HTTP/1.1" 200 OK +INFO: 192.168.1.122:51518 - "GET /api/dashboard HTTP/1.1" 200 OK +INFO: 192.168.1.122:51563 - "GET /api/dashboard HTTP/1.1" 200 OK diff --git a/logs/service_error.log b/logs/service_error.log new file mode 100644 index 0000000..e59f1df --- /dev/null +++ b/logs/service_error.log @@ -0,0 +1,23 @@ +2025-07-25 06:59:45,307 - __main__ - INFO - 🚀 Mac Mini AI 번역 서버 with 대시보드 (v2.1) +2025-07-25 06:59:45,307 - __main__ - INFO - 📡 서버 주소: http://192.168.1.122:20080 +2025-07-25 06:59:45,307 - __main__ - INFO - 📊 대시보드: http://192.168.1.122:20080/dashboard +2025-07-25 06:59:45,307 - __main__ - INFO - 📁 NAS 주소: 192.168.1.227 +INFO: Started server process [20661] +INFO: Waiting for application startup. +2025-07-25 06:59:45,328 - __main__ - INFO - 🚀 Mac Mini AI 번역 서버 시작 (v2.1) +2025-07-25 06:59:45,328 - __main__ - INFO - 📍 Mac Mini IP: 192.168.1.122 +2025-07-25 06:59:45,328 - __main__ - INFO - 📍 NAS IP: 192.168.1.227 +2025-07-25 06:59:45,328 - __main__ - INFO - -------------------------------------------------- +2025-07-25 06:59:45,328 - background_ai_service - INFO - 🚀 백그라운드 AI 서비스 시작 +2025-07-25 06:59:45,328 - background_ai_service - INFO - ✅ 백그라운드 서비스 준비 완료 +2025-07-25 06:59:45,351 - __main__ - INFO - ✅ NAS 연결 정상: /Volumes/Media +2025-07-25 06:59:45,357 - __main__ - INFO - ✅ 폴더 구조 확인/생성 완료 +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:20080 (Press CTRL+C to quit) + Loading checkpoint shards: 0%| | 0/3 [00:00=2.0.0 +transformers>=4.30.0 +sentencepiece>=0.1.99 +accelerate>=0.20.0 +datasets>=2.12.0 + +# PDF 처리 +PyPDF2>=3.0.1 +pdfplumber>=0.9.0 + +# 문서 처리 +python-docx>=0.8.11 + +# 언어 감지 +langdetect>=1.0.9 + +# 웹 프레임워크 +fastapi>=0.100.0 +uvicorn[standard]>=0.22.0 +python-multipart>=0.0.6 +jinja2>=3.1.2 +aiofiles>=23.1.0 + +# 유틸리티 +tqdm>=4.65.0 +numpy>=1.24.0 +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/config_loader.py b/src/config_loader.py new file mode 100644 index 0000000..8059af0 --- /dev/null +++ b/src/config_loader.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +""" +설정 파일(settings.json) 로더 +프로젝트 전체에서 사용되는 설정을 중앙에서 관리하고 제공합니다. +""" + +import json +from pathlib import Path +from typing import Dict, Any + +class ConfigLoader: + def __init__(self, config_path: str = "config/settings.json"): + self.config_path = Path(config_path) + self.config = self._load_config() + + def _load_config(self) -> Dict[str, Any]: + """JSON 설정 파일을 읽어 딕셔너리로 반환합니다.""" + if not self.config_path.exists(): + raise FileNotFoundError(f"설정 파일을 찾을 수 없습니다: {self.config_path}") + + with open(self.config_path, 'r', encoding='utf-8') as f: + config_data = json.load(f) + + # 경로 설정에서 '~'를 실제 홈 디렉토리로 확장 + if 'paths' in config_data: + for key, value in config_data['paths'].items(): + if isinstance(value, str) and value.startswith('~/'): + config_data['paths'][key] = str(Path.home() / value[2:]) + + return config_data + + def get_section(self, section_name: str) -> Dict[str, Any]: + """설정의 특정 섹션을 반환합니다.""" + return self.config.get(section_name, {}) + + @property + def network_config(self) -> Dict[str, Any]: + return self.get_section("network") + + @property + def paths_config(self) -> Dict[str, Any]: + return self.get_section("paths") + + @property + def models_config(self) -> Dict[str, Any]: + return self.get_section("models") + +# 전역 설정 인스턴스 생성 +# 프로젝트 어디서든 `from config_loader import settings`로 불러와 사용 가능 +settings = ConfigLoader() + +if __name__ == "__main__": + # 설정 로더 테스트 + print("✅ 설정 로더 테스트") + print("-" * 30) + + network = settings.network_config + print(f"네트워크 설정: {network}") + print(f" - 서버 IP: {network.get('mac_mini_ip')}") + print(f" - 서버 포트: {network.get('server_port')}") + + paths = settings.paths_config + print(f"경로 설정: {paths}") + print(f" - 로컬 작업 경로: {paths.get('local_work_path')}") + + print("-" * 30) + print("설정 로드 완료!") \ No newline at end of file diff --git a/src/download_kobart.py b/src/download_kobart.py new file mode 100755 index 0000000..4ac930b --- /dev/null +++ b/src/download_kobart.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +KoBART 한국어 요약 모델 다운로드 및 테스트 +""" + +import torch +import time +from transformers import BartForConditionalGeneration, AutoTokenizer +from pathlib import Path + +def download_kobart_model(): + print("🔄 KoBART 한국어 요약 모델 다운로드 시작...") + print("📊 모델 크기: ~500MB, 다운로드 시간 3-5분 예상") + + model_name = "ainize/kobart-news" + + # 로컬 모델 저장 경로 + model_dir = Path("models/kobart-news") + model_dir.mkdir(parents=True, exist_ok=True) + + try: + # 1. 토크나이저 다운로드 + print("\n📥 1/2: 토크나이저 다운로드 중...") + tokenizer = AutoTokenizer.from_pretrained( + model_name, + cache_dir=str(model_dir) + ) + print("✅ 토크나이저 다운로드 완료") + + # 2. 모델 다운로드 + print("\n📥 2/2: KoBART 모델 다운로드 중...") + start_time = time.time() + + model = BartForConditionalGeneration.from_pretrained( + model_name, + cache_dir=str(model_dir), + torch_dtype=torch.float16, + low_cpu_mem_usage=True + ) + + download_time = time.time() - start_time + print(f"✅ 모델 다운로드 완료 ({download_time/60:.1f}분 소요)") + + return model, tokenizer + + except Exception as e: + print(f"❌ 다운로드 실패: {e}") + return None, None + +def test_kobart_model(model, tokenizer): + print("\n🧪 KoBART 요약 모델 테스트...") + + # Apple Silicon 최적화 + if torch.backends.mps.is_available(): + device = torch.device("mps") + model = model.to(device) + print("🚀 Apple Silicon MPS 가속 사용") + else: + device = torch.device("cpu") + print("💻 CPU 모드 사용") + + def summarize_text(text, max_length=150, min_length=30): + print(f"\n📝 요약 테스트:") + print(f"원문 ({len(text)}자):") + print(f"{text}") + + start_time = time.time() + + # 텍스트 토큰화 + inputs = tokenizer( + text, + return_tensors="pt", + max_length=1024, + truncation=True, + padding=True + ).to(device) + + # 요약 생성 + with torch.no_grad(): + summary_ids = model.generate( + **inputs, + max_length=max_length, + min_length=min_length, + num_beams=4, + early_stopping=True, + no_repeat_ngram_size=2, + length_penalty=2.0 + ) + + # 결과 디코딩 + summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True) + + process_time = time.time() - start_time + print(f"\n요약 ({len(summary)}자):") + print(f"{summary}") + print(f"소요 시간: {process_time:.2f}초") + print(f"압축률: {len(summary)/len(text)*100:.1f}%") + + return summary, process_time + + try: + # 테스트 케이스 1: 기술 문서 + tech_text = """ + 인공지능 기술이 급속히 발전하면서 우리의 일상생활과 업무 환경에 큰 변화를 가져오고 있습니다. + 특히 자연어 처리 분야에서는 번역, 요약, 질의응답 등의 기술이 크게 향상되어 실용적인 수준에 + 도달했습니다. 기계학습 알고리즘은 대량의 데이터를 학습하여 패턴을 파악하고, 이를 바탕으로 + 새로운 입력에 대해 예측이나 분류를 수행합니다. 딥러닝 기술의 발전으로 이미지 인식, 음성 인식, + 자연어 이해 등의 성능이 인간 수준에 근접하거나 이를 넘어서는 경우도 생겨나고 있습니다. + 이러한 기술들은 의료, 금융, 교육, 엔터테인먼트 등 다양한 분야에서 활용되고 있으며, + 앞으로도 더 많은 혁신을 가져올 것으로 예상됩니다. + """ + + summarize_text(tech_text.strip()) + + # 테스트 케이스 2: 뉴스 스타일 + news_text = """ + 최근 발표된 연구에 따르면 인공지능을 활용한 번역 시스템의 정확도가 크게 향상되었다고 합니다. + 특히 한국어와 영어, 일본어 간의 번역에서 기존 시스템 대비 20% 이상의 성능 개선을 보였습니다. + 연구팀은 대규모 언어 모델과 특화된 번역 모델을 결합하여 문맥을 더 정확히 이해하고 + 자연스러운 번역을 생성할 수 있게 되었다고 설명했습니다. 이번 기술은 개인 사용자뿐만 아니라 + 기업의 글로벌 비즈니스에도 큰 도움이 될 것으로 기대됩니다. + """ + + summarize_text(news_text.strip()) + + print(f"\n✅ KoBART 요약 모델 테스트 완료!") + return True + + except Exception as e: + print(f"❌ 요약 테스트 실패: {e}") + return False + +def check_total_model_size(): + """전체 모델 크기 확인""" + model_dir = Path("models") + if model_dir.exists(): + import subprocess + try: + result = subprocess.run( + ["du", "-sh", str(model_dir)], + capture_output=True, + text=True + ) + if result.returncode == 0: + size = result.stdout.strip().split()[0] + print(f"📊 전체 모델 크기: {size}") + except: + print("📊 모델 크기 확인 불가") + +if __name__ == "__main__": + print("🚀 KoBART 한국어 요약 모델 설치") + print("="*50) + + # 기존 모델 확인 + model_dir = Path("models/kobart-news") + if model_dir.exists() and any(model_dir.iterdir()): + print("✅ 기존 KoBART 모델 발견, 로딩 시도...") + try: + tokenizer = AutoTokenizer.from_pretrained(str(model_dir)) + model = BartForConditionalGeneration.from_pretrained( + str(model_dir), + torch_dtype=torch.float16, + low_cpu_mem_usage=True + ) + print("✅ 기존 모델 로딩 완료") + except: + print("⚠️ 기존 모델 손상, 재다운로드 필요") + model, tokenizer = download_kobart_model() + else: + model, tokenizer = download_kobart_model() + + if model is not None and tokenizer is not None: + print("\n🧪 요약 모델 테스트 시작...") + if test_kobart_model(model, tokenizer): + check_total_model_size() + print("\n🎉 KoBART 요약 모델 설치 및 테스트 완료!") + print("📝 다음 단계: 통합 번역 시스템 구축") + else: + print("\n❌ 요약 모델 테스트 실패") + else: + print("\n❌ KoBART 모델 다운로드 실패") diff --git a/src/download_korean_summarizer.py b/src/download_korean_summarizer.py new file mode 100755 index 0000000..9d1cb21 --- /dev/null +++ b/src/download_korean_summarizer.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +안정적인 한국어 요약 모델 +""" + +import torch +import time +from transformers import PreTrainedTokenizerFast, BartForConditionalGeneration +from pathlib import Path + +def download_korean_summarizer(): + print("🔄 한국어 요약 모델 다운로드 (대안)") + + # 더 안정적인 모델 사용 + model_name = "gogamza/kobart-summarization" + + model_dir = Path("models/korean-summarizer") + model_dir.mkdir(parents=True, exist_ok=True) + + try: + print("📥 토크나이저 다운로드 중...") + tokenizer = PreTrainedTokenizerFast.from_pretrained( + model_name, + cache_dir=str(model_dir) + ) + print("✅ 토크나이저 다운로드 완료") + + print("📥 모델 다운로드 중...") + start_time = time.time() + + model = BartForConditionalGeneration.from_pretrained( + model_name, + cache_dir=str(model_dir), + torch_dtype=torch.float16, + low_cpu_mem_usage=True + ) + + download_time = time.time() - start_time + print(f"✅ 모델 다운로드 완료 ({download_time/60:.1f}분 소요)") + + return model, tokenizer + + except Exception as e: + print(f"❌ 다운로드 실패: {e}") + + # 마지막 대안: 간단한 T5 모델 + print("\n🔄 더 간단한 모델로 재시도...") + try: + from transformers import T5Tokenizer, T5ForConditionalGeneration + + alt_model_name = "t5-small" + print(f"📥 {alt_model_name} 다운로드 중...") + + tokenizer = T5Tokenizer.from_pretrained(alt_model_name) + model = T5ForConditionalGeneration.from_pretrained( + alt_model_name, + torch_dtype=torch.float16, + low_cpu_mem_usage=True + ) + + print("✅ T5 백업 모델 다운로드 완료") + return model, tokenizer + + except Exception as e2: + print(f"❌ 백업 모델도 실패: {e2}") + return None, None + +def test_summarizer(model, tokenizer, model_type="kobart"): + print(f"\n🧪 {model_type} 요약 모델 테스트...") + + # Apple Silicon 최적화 + if torch.backends.mps.is_available(): + device = torch.device("mps") + model = model.to(device) + print("🚀 Apple Silicon MPS 가속 사용") + else: + device = torch.device("cpu") + print("💻 CPU 모드 사용") + + def summarize_korean_text(text): + print(f"\n📝 한국어 요약 테스트:") + print(f"원문 ({len(text)}자):") + print(f"{text[:200]}...") + + start_time = time.time() + + if model_type == "t5": + # T5 모델용 프롬프트 + input_text = f"summarize: {text}" + else: + # KoBART 모델용 + input_text = text + + # 텍스트 토큰화 + inputs = tokenizer( + input_text, + return_tensors="pt", + max_length=1024, + truncation=True, + padding=True + ).to(device) + + # 요약 생성 + with torch.no_grad(): + summary_ids = model.generate( + **inputs, + max_length=150, + min_length=30, + num_beams=4, + early_stopping=True, + no_repeat_ngram_size=2, + length_penalty=1.5 + ) + + # 결과 디코딩 + summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True) + + process_time = time.time() - start_time + print(f"\n📋 요약 결과 ({len(summary)}자):") + print(f"{summary}") + print(f"⏱️ 처리 시간: {process_time:.2f}초") + print(f"📊 압축률: {len(summary)/len(text)*100:.1f}%") + + return summary + + try: + # 한국어 테스트 텍스트 + korean_text = """ + 인공지능과 기계학습 기술이 급속도로 발전하면서 우리의 일상생활과 업무 환경에 + 혁신적인 변화를 가져오고 있습니다. 특히 자연어 처리 분야에서는 번역, 요약, + 대화형 AI 등의 기술이 실용적인 수준에 도달하여 다양한 서비스에 적용되고 있습니다. + 기계학습 알고리즘은 대량의 텍스트 데이터를 학습하여 언어의 패턴과 의미를 이해하고, + 이를 바탕으로 인간과 유사한 수준의 언어 처리 능력을 보여주고 있습니다. + 딥러닝 기술의 발전으로 번역의 정확도가 크게 향상되었으며, 실시간 번역 서비스도 + 일상적으로 사용할 수 있게 되었습니다. 앞으로 이러한 기술들은 교육, 의료, + 비즈니스 등 더 많은 분야에서 활용될 것으로 예상되며, 언어 장벽을 허물어 + 글로벌 소통을 더욱 원활하게 만들 것입니다. + """ + + summarize_korean_text(korean_text.strip()) + + print(f"\n✅ 요약 모델 테스트 완료!") + return True + + except Exception as e: + print(f"❌ 요약 테스트 실패: {e}") + return False + +if __name__ == "__main__": + print("🚀 한국어 요약 모델 설치 (대안)") + print("="*50) + + model, tokenizer = download_korean_summarizer() + + if model is not None and tokenizer is not None: + # 모델 타입 판단 + model_type = "t5" if "t5" in str(type(model)).lower() else "kobart" + + print(f"\n🧪 {model_type} 모델 테스트 시작...") + if test_summarizer(model, tokenizer, model_type): + print("\n🎉 한국어 요약 모델 설치 및 테스트 완료!") + print("📝 다음 단계: 통합 번역 시스템 구축") + else: + print("\n❌ 요약 모델 테스트 실패") + else: + print("\n❌ 모든 요약 모델 다운로드 실패") + print("📝 요약 기능 없이 번역만으로 진행 가능") diff --git a/src/download_nllb.py b/src/download_nllb.py new file mode 100755 index 0000000..d670f95 --- /dev/null +++ b/src/download_nllb.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +NLLB-200-3.3B 모델 다운로드 및 초기 테스트 +""" + +import os +import time +import torch +from transformers import AutoTokenizer, AutoModelForSeq2SeqLM +from pathlib import Path + +def download_nllb_model(): + print("🔄 NLLB-200-3.3B 모델 다운로드 시작...") + print("⚠️ 모델 크기: ~7GB, 다운로드 시간 10-15분 예상") + + model_name = "facebook/nllb-200-3.3B" + + # 로컬 모델 저장 경로 + model_dir = Path("models/nllb-200-3.3B") + model_dir.mkdir(parents=True, exist_ok=True) + + try: + # 1. 토크나이저 다운로드 + print("\n📥 1/2: 토크나이저 다운로드 중...") + tokenizer = AutoTokenizer.from_pretrained( + model_name, + cache_dir=str(model_dir), + local_files_only=False + ) + print("✅ 토크나이저 다운로드 완료") + + # 2. 모델 다운로드 (시간이 오래 걸림) + print("\n📥 2/2: 모델 다운로드 중...") + print("⏳ 진행률을 확인하려면 별도 터미널에서 다음 명령어 실행:") + print(f" du -sh {model_dir}/models--facebook--nllb-200-3.3B") + + start_time = time.time() + + model = AutoModelForSeq2SeqLM.from_pretrained( + model_name, + cache_dir=str(model_dir), + local_files_only=False, + torch_dtype=torch.float16, # 메모리 절약 + low_cpu_mem_usage=True + ) + + download_time = time.time() - start_time + print(f"✅ 모델 다운로드 완료 ({download_time/60:.1f}분 소요)") + + return model, tokenizer + + except Exception as e: + print(f"❌ 다운로드 실패: {e}") + return None, None + +def test_nllb_model(model, tokenizer): + print("\n🧪 NLLB 모델 기본 테스트...") + + # Apple Silicon 최적화 + if torch.backends.mps.is_available(): + device = torch.device("mps") + print("🚀 Apple Silicon MPS 가속 사용") + else: + device = torch.device("cpu") + print("💻 CPU 모드 사용") + + try: + model = model.to(device) + + # NLLB 언어 코드 + lang_codes = { + "eng_Latn": "English", + "jpn_Jpan": "Japanese", + "kor_Hang": "Korean" + } + + def translate_test(text, src_lang, tgt_lang, desc): + print(f"\n📝 {desc} 테스트:") + print(f"원문: {text}") + + tokenizer.src_lang = src_lang + encoded = tokenizer(text, return_tensors="pt", padding=True).to(device) + + start_time = time.time() + + generated_tokens = model.generate( + **encoded, + forced_bos_token_id=tokenizer.lang_code_to_id[tgt_lang], + max_length=200, + num_beams=4, + early_stopping=True, + do_sample=False + ) + + translation_time = time.time() - start_time + result = tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0] + + print(f"번역: {result}") + print(f"소요 시간: {translation_time:.2f}초") + + return result, translation_time + + # 테스트 케이스들 + print("\n" + "="*50) + + # 1. 영어 → 한국어 + en_text = "Artificial intelligence is transforming the way we work and live." + translate_test(en_text, "eng_Latn", "kor_Hang", "영어 → 한국어") + + # 2. 일본어 → 한국어 + ja_text = "人工知能は私たちの働き方と生活を変革しています。" + translate_test(ja_text, "jpn_Jpan", "kor_Hang", "일본어 → 한국어") + + # 3. 기술 문서 스타일 + tech_text = "Machine learning algorithms require large datasets for training." + translate_test(tech_text, "eng_Latn", "kor_Hang", "기술 문서") + + print("\n✅ 모든 테스트 완료!") + return True + + except Exception as e: + print(f"❌ 테스트 실패: {e}") + return False + +def check_model_size(): + """다운로드된 모델 크기 확인""" + model_dir = Path("models") + if model_dir.exists(): + import subprocess + try: + result = subprocess.run( + ["du", "-sh", str(model_dir)], + capture_output=True, + text=True + ) + if result.returncode == 0: + size = result.stdout.strip().split()[0] + print(f"📊 다운로드된 모델 크기: {size}") + except: + print("📊 모델 크기 확인 불가") + +if __name__ == "__main__": + print("🚀 NLLB-200-3.3B 모델 설치") + print("="*50) + + # 기존 모델 확인 + model_dir = Path("models/nllb-200-3.3B") + if model_dir.exists() and any(model_dir.iterdir()): + print("✅ 기존 모델 발견, 로딩 시도...") + try: + tokenizer = AutoTokenizer.from_pretrained(str(model_dir)) + model = AutoModelForSeq2SeqLM.from_pretrained( + str(model_dir), + torch_dtype=torch.float16, + low_cpu_mem_usage=True + ) + print("✅ 기존 모델 로딩 완료") + except: + print("⚠️ 기존 모델 손상, 재다운로드 필요") + model, tokenizer = download_nllb_model() + else: + model, tokenizer = download_nllb_model() + + if model is not None and tokenizer is not None: + print("\n🧪 모델 테스트 시작...") + if test_nllb_model(model, tokenizer): + check_model_size() + print("\n🎉 NLLB 모델 설치 및 테스트 완료!") + print("📝 다음 단계: KoBART 요약 모델 설치") + else: + print("\n❌ 모델 테스트 실패") + else: + print("\n❌ 모델 다운로드 실패") + print("네트워크 연결을 확인하고 다시 시도해주세요.") diff --git a/src/fastapi_final.py b/src/fastapi_final.py new file mode 100644 index 0000000..78e5378 --- /dev/null +++ b/src/fastapi_final.py @@ -0,0 +1,462 @@ +#!/usr/bin/env python3 +""" +Mac Mini (192.168.1.122) FastAPI + DS1525+ (192.168.1.227) 연동 +최종 운영 버전 +""" + +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 + +# 실제 네트워크 설정 +MAC_MINI_IP = "192.168.1.227" +NAS_IP = "192.168.1.122" + +# 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" + +app = FastAPI( + title="AI 번역 시스템", + description=f"Mac Mini ({MAC_MINI_IP}) + DS1525+ ({NAS_IP}) 연동", + version="1.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(): + """서버 시작시 전체 시스템 상태 확인""" + print(f"🚀 Mac Mini AI 번역 서버 시작") + print(f"📍 Mac Mini IP: {MAC_MINI_IP}") + print(f"📍 NAS IP: {NAS_IP}") + print("-" * 50) + + # NAS 연결 상태 확인 + nas_status = check_nas_connection() + + if nas_status["status"] == "connected": + print(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) + + print(f"✅ 폴더 구조 확인/생성 완료") + + except Exception as e: + print(f"⚠️ 폴더 생성 실패: {e}") + + else: + print(f"❌ NAS 연결 실패: {nas_status['error']}") + print("해결 방법:") + print("1. NAS 전원 및 네트워크 상태 확인") + print("2. Finder에서 DS1525+ 수동 연결:") + print(f" - 이동 → 서버에 연결 → smb://{NAS_IP}") + print("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 = { + "models_available": False, + "memory_usage": "Unknown" + } + + try: + # NLLB 모델 폴더 확인 + model_path = LOCAL_WORK_PATH / "models" + if model_path.exists(): + ai_status["models_available"] = True + except: + pass + + # 작업 통계 + 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(...)): + """파일 업로드 (NAS 연결 상태 확인 포함)""" + + # 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) + + print(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 + + # 백그라운드에서 처리 시작 + background_tasks.add_task(process_document_with_nas_final, job_id, nas_file_path) + + return { + "job_id": job_id, + "message": "파일 업로드 완료, AI 처리를 시작합니다.", + "nas_path": str(nas_file_path) + } + +async def process_document_with_nas_final(job_id: str, nas_file_path: Path): + """최종 문서 처리 파이프라인""" + + try: + processing_jobs[job_id].update({ + "status": "processing", + "progress": 10, + "message": "AI 모델 로딩 및 텍스트 추출 중..." + }) + + # 1. 통합 번역 시스템 로드 + import sys + sys.path.append(str(LOCAL_WORK_PATH / "src")) + + from integrated_translation_system import IntegratedTranslationSystem + + system = IntegratedTranslationSystem() + if not system.load_models(): + raise Exception("AI 모델 로드 실패") + + processing_jobs[job_id].update({ + "progress": 30, + "message": "문서 분석 및 언어 감지 중..." + }) + + # 2. 문서 처리 + result = system.process_document(str(nas_file_path)) + detected_lang = result.metadata["detected_language"] + + processing_jobs[job_id].update({ + "progress": 60, + "message": f"언어 감지: {detected_lang}, 다국어 HTML 생성 중..." + }) + + # 3. HTML 생성 + from html_generator import MultilingualHTMLGenerator + + generator = MultilingualHTMLGenerator() + + # 언어별 콘텐츠 준비 + contents = {} + translation_folder = "" + + if detected_lang == "korean": + contents["korean"] = result.original_text + translation_folder = "korean-only" + elif detected_lang == "english": + contents["english"] = result.original_text + contents["korean"] = result.translated_text + translation_folder = "english-to-korean" + elif detected_lang == "japanese": + contents["japanese"] = result.original_text + contents["korean"] = result.translated_text + translation_folder = "japanese-to-korean" + else: + contents[detected_lang] = result.original_text + contents["korean"] = result.translated_text + translation_folder = "unknown-language" + + # NAS 저장 경로 결정 + current_month = datetime.now().strftime("%Y-%m") + nas_translated_dir = NAS_TRANSLATED_PATH / current_month / translation_folder + nas_translated_dir.mkdir(parents=True, exist_ok=True) + + # 원본 파일명에서 HTML 파일명 생성 + base_name = Path(nas_file_path.stem).stem + # 타임스탬프와 job_id 제거 + if '_' in base_name: + parts = base_name.split('_')[2:] # 처음 2개 부분 제거 + if parts: + base_name = '_'.join(parts) + + html_filename = f"{base_name}.html" + nas_html_path = nas_translated_dir / html_filename + + # 중복 파일명 처리 + counter = 1 + while nas_html_path.exists(): + html_filename = f"{base_name}_{counter}.html" + nas_html_path = nas_translated_dir / html_filename + counter += 1 + + # HTML 생성 + generator.generate_multilingual_html( + title=base_name, + contents=contents, + summary=result.summary, + metadata={ + **result.metadata, + "nas_original_path": str(nas_file_path), + "translation_type": translation_folder, + "nas_ip": NAS_IP, + "mac_mini_ip": MAC_MINI_IP + }, + output_path=str(nas_html_path) + ) + + processing_jobs[job_id].update({ + "progress": 85, + "message": "정적 호스팅 준비 및 인덱스 업데이트 중..." + }) + + # 4. 정적 호스팅 폴더에 복사 + static_hosting_file = NAS_STATIC_HOSTING_PATH / "docs" / html_filename + shutil.copy2(nas_html_path, static_hosting_file) + + # 5. 메타데이터 저장 + await save_processing_metadata(job_id, nas_file_path, nas_html_path, result.metadata) + + processing_jobs[job_id].update({ + "progress": 100, + "status": "completed", + "message": f"처리 완료! ({translation_folder})", + "nas_translated_path": str(nas_html_path), + "static_hosting_path": str(static_hosting_file), + "detected_language": detected_lang, + "translation_type": translation_folder + }) + + print(f"✅ 작업 완료: {job_id} - {html_filename}") + + except Exception as e: + processing_jobs[job_id].update({ + "status": "error", + "progress": 0, + "message": f"처리 실패: {str(e)}" + }) + + print(f"❌ 작업 실패: {job_id} - {str(e)}") + +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 + } + + # 월별 로그 파일에 추가 + 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: + print(f"메타데이터 저장 실패: {e}") + +@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] + +if __name__ == "__main__": + import uvicorn + + print(f"🚀 Mac Mini AI 번역 서버") + print(f"📡 서버 주소: http://{MAC_MINI_IP}:8080") + print(f"📁 NAS 주소: {NAS_IP}") + + uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/src/fastapi_media_mount.py b/src/fastapi_media_mount.py new file mode 100644 index 0000000..1be3fe4 --- /dev/null +++ b/src/fastapi_media_mount.py @@ -0,0 +1,575 @@ +#!/usr/bin/env python3 +""" +/Volumes/Media 마운트 기반 FastAPI +최종 운영 버전 +""" + +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 + +# 실제 네트워크 설정 +MAC_MINI_IP = "192.168.1.122" +NAS_IP = "192.168.1.227" + +# 기존 연결된 Media 마운트 사용 +NAS_MOUNT_POINT = Path("/Volumes/Media") +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" + +app = FastAPI( + title="AI 번역 시스템", + description=f"Mac Mini ({MAC_MINI_IP}) + Media Mount 연동", + version="1.0.0" +) + +# 정적 파일 및 템플릿 (있는 경우에만) +if (LOCAL_WORK_PATH / "static").exists(): + app.mount("/static", StaticFiles(directory=str(LOCAL_WORK_PATH / "static")), name="static") + +if (LOCAL_WORK_PATH / "templates").exists(): + templates = Jinja2Templates(directory=str(LOCAL_WORK_PATH / "templates")) +else: + templates = None + +# 작업 상태 관리 +processing_jobs: Dict[str, Dict] = {} + +def check_nas_connection(): + """NAS 연결 상태 확인""" + try: + # 1. Media 마운트 확인 + if not NAS_MOUNT_POINT.exists(): + return {"status": "not_mounted", "error": "Media 마운트 포인트 없음"} + + # 2. 쓰기 권한 확인 + try: + test_file = NAS_MOUNT_POINT / ".test_write" + test_file.touch() + test_file.unlink() + except: + return {"status": "read_only", "error": "Media 마운트 읽기 전용"} + + # 3. 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 Exception as e: + return {"status": "error", "error": str(e)} + +def ensure_nas_directories(): + """NAS 디렉토리 구조 생성""" + try: + # Document-upload 기본 구조 + DOCUMENT_UPLOAD_BASE.mkdir(exist_ok=True) + + 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_STATIC_HOSTING_PATH / "index", + NAS_METADATA_PATH / "processing-logs" + ] + + for folder in folders_to_create: + folder.mkdir(parents=True, exist_ok=True) + + # README 파일 생성 + readme_content = f"""# AI 번역 시스템 문서 저장소 + +자동 생성 시간: {datetime.now().isoformat()} +Mac Mini IP: {MAC_MINI_IP} +NAS IP: {NAS_IP} + +## 폴더 구조 + +- originals/: 업로드된 원본 파일들 +- translated/: 번역된 HTML 파일들 +- static-hosting/: 웹 호스팅용 파일들 +- metadata/: 처리 로그 및 메타데이터 + +## 자동 관리 + +이 폴더는 AI 번역 시스템에서 자동으로 관리됩니다. +수동으로 파일을 수정하지 마세요. +""" + + readme_path = DOCUMENT_UPLOAD_BASE / "README.md" + with open(readme_path, 'w', encoding='utf-8') as f: + f.write(readme_content) + + return True + + except Exception as e: + print(f"❌ 디렉토리 생성 실패: {e}") + return False + +@app.on_event("startup") +async def startup_event(): + """서버 시작시 전체 시스템 상태 확인""" + print(f"🚀 Mac Mini AI 번역 서버 시작") + print(f"📍 Mac Mini IP: {MAC_MINI_IP}") + print(f"📍 NAS Mount: {NAS_MOUNT_POINT}") + print("-" * 50) + + # NAS 연결 상태 확인 + nas_status = check_nas_connection() + + if nas_status["status"] == "connected": + print(f"✅ NAS 연결 정상: {nas_status['mount_point']}") + + if ensure_nas_directories(): + print(f"✅ 폴더 구조 확인/생성 완료") + print(f"📁 문서 저장 경로: {DOCUMENT_UPLOAD_BASE}") + else: + print(f"⚠️ 폴더 생성 실패") + else: + print(f"❌ NAS 연결 실패: {nas_status['error']}") + +@app.get("/") +async def index(request: Request = None): + """메인 페이지""" + nas_status = check_nas_connection() + + if templates: + return templates.TemplateResponse("index.html", { + "request": request, + "nas_status": nas_status + }) + else: + # 템플릿이 없으면 간단한 HTML 반환 + html_content = f""" + + + + + AI 번역 시스템 + + + +
+

🤖 AI 번역 시스템

+ +
+ {'✅ NAS 연결됨: ' + nas_status.get('mount_point', '') if nas_status['status'] == 'connected' else '❌ NAS 연결 실패: ' + nas_status.get('error', '')} +
+ +
+

파일 업로드

+
+ +
+ +
+
+
+ +
+

API 엔드포인트

+ +
+
+ + + + + """ + return HTMLResponse(content=html_content) + +@app.get("/system-status") +async def system_status(): + """전체 시스템 상태""" + nas_status = check_nas_connection() + + # 파일 통계 + file_stats = {"originals": 0, "translated": 0, "total_size": 0} + + try: + if NAS_ORIGINALS_PATH.exists(): + original_files = list(NAS_ORIGINALS_PATH.rglob("*.*")) + file_stats["originals"] = len([f for f in original_files if f.is_file()]) + + if NAS_TRANSLATED_PATH.exists(): + html_files = list(NAS_TRANSLATED_PATH.rglob("*.html")) + file_stats["translated"] = len(html_files) + file_stats["total_size"] = sum(f.stat().st_size for f in html_files if f.exists()) + except: + pass + + return { + "timestamp": datetime.now().isoformat(), + "mac_mini_ip": MAC_MINI_IP, + "nas_mount": str(NAS_MOUNT_POINT), + "nas_status": nas_status, + "file_stats": file_stats, + "active_jobs": len(processing_jobs), + "document_upload_path": str(DOCUMENT_UPLOAD_BASE) + } + +@app.post("/upload") +async def upload_file(background_tasks: BackgroundTasks, file: UploadFile = File(...)): + """파일 업로드""" + + # NAS 연결 상태 먼저 확인 + nas_status = check_nas_connection() + if nas_status["status"] != "connected": + raise HTTPException( + status_code=503, + detail=f"NAS 연결 실패: {nas_status['error']}" + ) + + # 파일 검증 + 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) + + print(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"파일 저장 완료: {safe_filename}", + "nas_original_path": str(nas_file_path), + "created_at": time.time() + } + + processing_jobs[job_id] = job + + # 백그라운드 처리 시작 + background_tasks.add_task(process_document_simple, job_id, nas_file_path) + + return { + "job_id": job_id, + "message": "파일 업로드 완료, 처리를 시작합니다.", + "nas_path": str(nas_file_path) + } + +async def process_document_simple(job_id: str, nas_file_path: Path): + """간단한 문서 처리 (AI 모델 연동 전 테스트용)""" + + try: + processing_jobs[job_id].update({ + "status": "processing", + "progress": 30, + "message": "텍스트 추출 중..." + }) + + await asyncio.sleep(2) # 시뮬레이션 + + processing_jobs[job_id].update({ + "progress": 60, + "message": "언어 감지 및 번역 중..." + }) + + await asyncio.sleep(3) # 시뮬레이션 + + # 간단한 HTML 생성 (테스트용) + current_month = datetime.now().strftime("%Y-%m") + nas_translated_dir = NAS_TRANSLATED_PATH / current_month / "korean-only" + nas_translated_dir.mkdir(parents=True, exist_ok=True) + + base_name = Path(nas_file_path.stem).stem + if '_' in base_name: + parts = base_name.split('_')[2:] # 타임스탬프와 job_id 제거 + if parts: + base_name = '_'.join(parts) + + html_filename = f"{base_name}.html" + nas_html_path = nas_translated_dir / html_filename + + # 테스트용 HTML 생성 + html_content = f""" + + + + {base_name} + + + +
+
+

📄 {base_name}

+

처리 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

+

원본 파일: {nas_file_path.name}

+

Job ID: {job_id}

+
+ +
+

테스트 문서

+

이것은 AI 번역 시스템의 테스트 출력입니다.

+

실제 AI 모델이 연동되면 이 부분에 번역된 내용이 표시됩니다.

+ +

시스템 정보

+
    +
  • Mac Mini IP: {MAC_MINI_IP}
  • +
  • NAS Mount: {NAS_MOUNT_POINT}
  • +
  • 저장 경로: {nas_html_path}
  • +
+
+
+ +""" + + # HTML 파일 저장 + async with aiofiles.open(nas_html_path, 'w', encoding='utf-8') as f: + await f.write(html_content) + + processing_jobs[job_id].update({ + "progress": 100, + "status": "completed", + "message": "처리 완료!", + "nas_translated_path": str(nas_html_path) + }) + + print(f"✅ 테스트 처리 완료: {job_id} - {html_filename}") + + except Exception as e: + processing_jobs[job_id].update({ + "status": "error", + "progress": 0, + "message": f"처리 실패: {str(e)}" + }) + + print(f"❌ 처리 실패: {job_id} - {str(e)}") + +@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("/download/{job_id}") +async def download_result(job_id: str): + """결과 파일 다운로드""" + if job_id not in processing_jobs: + raise HTTPException(status_code=404, detail="작업을 찾을 수 없습니다.") + + job = processing_jobs[job_id] + + if job["status"] != "completed" or not job.get("nas_translated_path"): + raise HTTPException(status_code=400, detail="처리가 완료되지 않았습니다.") + + result_path = Path(job["nas_translated_path"]) + if not result_path.exists(): + raise HTTPException(status_code=404, detail="결과 파일을 찾을 수 없습니다.") + + return FileResponse( + path=result_path, + filename=f"{Path(job['filename']).stem}.html", + media_type="text/html" + ) + +@app.get("/nas-info") +async def nas_info(): + """NAS 정보 및 통계""" + + nas_status = check_nas_connection() + + info = { + "nas_status": nas_status, + "mount_point": str(NAS_MOUNT_POINT), + "document_upload_base": str(DOCUMENT_UPLOAD_BASE), + "folders": {}, + "statistics": { + "total_jobs": len(processing_jobs), + "completed_jobs": len([j for j in processing_jobs.values() if j["status"] == "completed"]), + "failed_jobs": len([j for j in processing_jobs.values() if j["status"] == "error"]) + } + } + + # 폴더 정보 수집 + try: + for folder_name, folder_path in [ + ("originals", NAS_ORIGINALS_PATH), + ("translated", NAS_TRANSLATED_PATH), + ("static-hosting", NAS_STATIC_HOSTING_PATH), + ("metadata", NAS_METADATA_PATH) + ]: + if folder_path.exists(): + files = list(folder_path.rglob("*.*")) + file_count = len([f for f in files if f.is_file()]) + total_size = sum(f.stat().st_size for f in files if f.is_file()) + + info["folders"][folder_name] = { + "exists": True, + "file_count": file_count, + "total_size_mb": round(total_size / (1024 * 1024), 2) + } + else: + info["folders"][folder_name] = {"exists": False} + except Exception as e: + info["error"] = str(e) + + return info + +if __name__ == "__main__": + import uvicorn + + print(f"🚀 Mac Mini AI 번역 서버") + print(f"📡 서버 주소: http://{MAC_MINI_IP}:8080") + print(f"📁 NAS Mount: {NAS_MOUNT_POINT}") + + # 시작 전 연결 확인 + nas_status = check_nas_connection() + if nas_status["status"] == "connected": + print(f"✅ NAS 연결 확인됨") + ensure_nas_directories() + else: + print(f"❌ NAS 연결 문제: {nas_status['error']}") + + uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/src/fastapi_port_20080.py b/src/fastapi_port_20080.py new file mode 100644 index 0000000..6a0b618 --- /dev/null +++ b/src/fastapi_port_20080.py @@ -0,0 +1,848 @@ +#!/usr/bin/env python3 +""" +포트 20080으로 실행하는 AI 번역 시스템 +Mac Mini (192.168.1.122:20080) + Media Mount 연동 +""" + +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 + +# 실제 네트워크 설정 (포트 20080) +MAC_MINI_IP = "192.168.1.122" +MAC_MINI_PORT = 20080 +NAS_IP = "192.168.1.227" + +# 기존 연결된 Media 마운트 사용 +NAS_MOUNT_POINT = Path("/Volumes/Media") +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" + +app = FastAPI( + title="AI 번역 시스템", + description=f"Mac Mini ({MAC_MINI_IP}:{MAC_MINI_PORT}) + Media Mount 연동", + version="1.0.0" +) + +# 정적 파일 및 템플릿 (있는 경우에만) +if (LOCAL_WORK_PATH / "static").exists(): + app.mount("/static", StaticFiles(directory=str(LOCAL_WORK_PATH / "static")), name="static") + +if (LOCAL_WORK_PATH / "templates").exists(): + templates = Jinja2Templates(directory=str(LOCAL_WORK_PATH / "templates")) +else: + templates = None + +# 작업 상태 관리 +processing_jobs: Dict[str, Dict] = {} + +def check_nas_connection(): + """NAS 연결 상태 확인""" + try: + # 1. Media 마운트 확인 + if not NAS_MOUNT_POINT.exists(): + return {"status": "not_mounted", "error": "Media 마운트 포인트 없음"} + + # 2. 쓰기 권한 확인 + try: + test_file = NAS_MOUNT_POINT / ".test_write" + test_file.touch() + test_file.unlink() + except: + return {"status": "read_only", "error": "Media 마운트 읽기 전용"} + + # 3. 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 Exception as e: + return {"status": "error", "error": str(e)} + +def ensure_nas_directories(): + """NAS 디렉토리 구조 생성""" + try: + # Document-upload 기본 구조 + DOCUMENT_UPLOAD_BASE.mkdir(exist_ok=True) + + 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_STATIC_HOSTING_PATH / "index", + NAS_METADATA_PATH / "processing-logs" + ] + + for folder in folders_to_create: + folder.mkdir(parents=True, exist_ok=True) + + # README 파일 생성 + readme_content = f"""# AI 번역 시스템 문서 저장소 + +자동 생성 시간: {datetime.now().isoformat()} +Mac Mini: {MAC_MINI_IP}:{MAC_MINI_PORT} +NAS IP: {NAS_IP} + +## 접속 정보 + +- 웹 인터페이스: http://{MAC_MINI_IP}:{MAC_MINI_PORT} +- 시스템 상태: http://{MAC_MINI_IP}:{MAC_MINI_PORT}/system-status +- NAS 정보: http://{MAC_MINI_IP}:{MAC_MINI_PORT}/nas-info +- API 문서: http://{MAC_MINI_IP}:{MAC_MINI_PORT}/docs + +## 폴더 구조 + +- originals/: 업로드된 원본 파일들 +- translated/: 번역된 HTML 파일들 +- static-hosting/: 웹 호스팅용 파일들 +- metadata/: 처리 로그 및 메타데이터 + +## VPN 접속 + +내부 네트워크에서만 접근 가능합니다. +외부에서 접속시 VPN 연결이 필요합니다. +""" + + readme_path = DOCUMENT_UPLOAD_BASE / "README.md" + with open(readme_path, 'w', encoding='utf-8') as f: + f.write(readme_content) + + return True + + except Exception as e: + print(f"❌ 디렉토리 생성 실패: {e}") + return False + +@app.on_event("startup") +async def startup_event(): + """서버 시작시 전체 시스템 상태 확인""" + print(f"🚀 Mac Mini AI 번역 서버 시작") + print(f"📍 접속 주소: http://{MAC_MINI_IP}:{MAC_MINI_PORT}") + print(f"📍 NAS Mount: {NAS_MOUNT_POINT}") + print("-" * 60) + + # NAS 연결 상태 확인 + nas_status = check_nas_connection() + + if nas_status["status"] == "connected": + print(f"✅ NAS 연결 정상: {nas_status['mount_point']}") + + if ensure_nas_directories(): + print(f"✅ 폴더 구조 확인/생성 완료") + print(f"📁 문서 저장 경로: {DOCUMENT_UPLOAD_BASE}") + else: + print(f"⚠️ 폴더 생성 실패") + else: + print(f"❌ NAS 연결 실패: {nas_status['error']}") + +@app.get("/") +async def index(request: Request = None): + """메인 페이지""" + nas_status = check_nas_connection() + + if templates: + return templates.TemplateResponse("index.html", { + "request": request, + "nas_status": nas_status + }) + else: + # 포트 20080 반영된 기본 HTML + html_content = f""" + + + + + + 🤖 AI 번역 시스템 + + + +
+
+

🤖 AI 번역 시스템

+

PDF/문서를 다국어 HTML로 자동 변환

+
+ +
+
+ 🖥️ + Mac Mini: {MAC_MINI_IP}:{MAC_MINI_PORT} +
+
+ 💾 + NAS: {NAS_IP} (DS1525+) +
+
+ +
+ {'✅ NAS 연결됨: ' + nas_status.get('mount_point', '') if nas_status['status'] == 'connected' else '❌ NAS 연결 실패: ' + nas_status.get('error', '')} +
+ +
+
+

📁 파일 업로드

+

PDF, TXT, DOCX 파일 지원 (최대 100MB)

+
+ +
+ +
+ +
+
처리 중...
+
+
+
+
+ +
+

🎉 변환 완료!

+ +
+
+ + +
+
+ + + + + """ + return HTMLResponse(content=html_content) + +@app.get("/system-status") +async def system_status(): + """전체 시스템 상태""" + nas_status = check_nas_connection() + + # 파일 통계 + file_stats = {"originals": 0, "translated": 0, "total_size": 0} + + try: + if NAS_ORIGINALS_PATH.exists(): + original_files = list(NAS_ORIGINALS_PATH.rglob("*.*")) + file_stats["originals"] = len([f for f in original_files if f.is_file()]) + + if NAS_TRANSLATED_PATH.exists(): + html_files = list(NAS_TRANSLATED_PATH.rglob("*.html")) + file_stats["translated"] = len(html_files) + file_stats["total_size"] = sum(f.stat().st_size for f in html_files if f.exists()) + except: + pass + + return { + "timestamp": datetime.now().isoformat(), + "server": f"{MAC_MINI_IP}:{MAC_MINI_PORT}", + "nas_ip": NAS_IP, + "nas_mount": str(NAS_MOUNT_POINT), + "nas_status": nas_status, + "file_stats": file_stats, + "active_jobs": len(processing_jobs), + "document_upload_path": str(DOCUMENT_UPLOAD_BASE), + "access_urls": { + "main": f"http://{MAC_MINI_IP}:{MAC_MINI_PORT}", + "system_status": f"http://{MAC_MINI_IP}:{MAC_MINI_PORT}/system-status", + "nas_info": f"http://{MAC_MINI_IP}:{MAC_MINI_PORT}/nas-info", + "api_docs": f"http://{MAC_MINI_IP}:{MAC_MINI_PORT}/docs" + } + } + +@app.post("/upload") +async def upload_file(background_tasks: BackgroundTasks, file: UploadFile = File(...)): + """파일 업로드""" + + # NAS 연결 상태 먼저 확인 + nas_status = check_nas_connection() + if nas_status["status"] != "connected": + raise HTTPException( + status_code=503, + detail=f"NAS 연결 실패: {nas_status['error']}" + ) + + # 파일 검증 + 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) + + print(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": 20, + "message": f"파일 저장 완료: {safe_filename}", + "nas_original_path": str(nas_file_path), + "created_at": time.time(), + "server": f"{MAC_MINI_IP}:{MAC_MINI_PORT}" + } + + processing_jobs[job_id] = job + + # 백그라운드 처리 시작 + background_tasks.add_task(process_document_simple, job_id, nas_file_path) + + return { + "job_id": job_id, + "message": "파일 업로드 완료, 처리를 시작합니다.", + "nas_path": str(nas_file_path), + "server": f"{MAC_MINI_IP}:{MAC_MINI_PORT}" + } + +async def process_document_simple(job_id: str, nas_file_path: Path): + """간단한 문서 처리 (AI 모델 연동 전 테스트용)""" + + try: + processing_jobs[job_id].update({ + "status": "processing", + "progress": 40, + "message": "텍스트 추출 중..." + }) + + await asyncio.sleep(2) # 시뮬레이션 + + processing_jobs[job_id].update({ + "progress": 70, + "message": "언어 감지 및 번역 중..." + }) + + await asyncio.sleep(3) # 시뮬레이션 + + # 간단한 HTML 생성 (테스트용) + current_month = datetime.now().strftime("%Y-%m") + nas_translated_dir = NAS_TRANSLATED_PATH / current_month / "korean-only" + nas_translated_dir.mkdir(parents=True, exist_ok=True) + + base_name = Path(nas_file_path.stem).stem + if '_' in base_name: + parts = base_name.split('_')[2:] # 타임스탬프와 job_id 제거 + if parts: + base_name = '_'.join(parts) + + html_filename = f"{base_name}.html" + nas_html_path = nas_translated_dir / html_filename + + # 테스트용 HTML 생성 + html_content = f""" + + + + + {base_name} - AI 번역 결과 + + + +
+
+

📄 {base_name}

+

AI 번역 시스템 처리 결과

+
+ +
+
+
+ + {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +
+
+ 📁 + {nas_file_path.name} +
+
+ 🆔 + {job_id[:8]} +
+
+ 🖥️ + {MAC_MINI_IP}:{MAC_MINI_PORT} +
+
+
+ +
+
+

📋 문서 정보

+
+

원본 파일: {nas_file_path.name}

+

저장 위치: {nas_html_path}

+

처리 서버: Mac Mini ({MAC_MINI_IP}:{MAC_MINI_PORT})

+

NAS 저장소: DS1525+ ({NAS_IP})

+
+
+ +
+

🤖 AI 처리 결과

+
+

테스트 모드

+

현재 AI 번역 시스템이 테스트 모드로 실행 중입니다.

+

실제 AI 모델(NLLB, KoBART)이 연동되면 이 부분에 다음 내용이 표시됩니다:

+
    +
  • 자동 언어 감지 결과
  • +
  • 한국어 번역 텍스트
  • +
  • 문서 요약
  • +
  • 다국어 지원 인터페이스
  • +
+
+
+ +
+

🌐 시스템 구성

+
+
+

Mac Mini M4 Pro

+

IP: {MAC_MINI_IP}:{MAC_MINI_PORT}

+

역할: AI 처리 서버

+

모델: NLLB + KoBART

+
+
+

Synology DS1525+

+

IP: {NAS_IP}

+

역할: 파일 저장소

+

마운트: /Volumes/Media

+
+
+

네트워크

+

연결: 2.5GbE

+

접근: VPN 필요

+

프로토콜: SMB

+
+
+
+ + +
+ + +
+ +""" + + # HTML 파일 저장 + async with aiofiles.open(nas_html_path, 'w', encoding='utf-8') as f: + await f.write(html_content) + + processing_jobs[job_id].update({ + "progress": 100, + "status": "completed", + "message": "처리 완료!", + "nas_translated_path": str(nas_html_path) + }) + + print(f"✅ 테스트 처리 완료: {job_id} - {html_filename}") + + except Exception as e: + processing_jobs[job_id].update({ + "status": "error", + "progress": 0, + "message": f"처리 실패: {str(e)}" + }) + + print(f"❌ 처리 실패: {job_id} - {str(e)}") + +@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("/download/{job_id}") +async def download_result(job_id: str): + """결과 파일 다운로드""" + if job_id not in processing_jobs: + raise HTTPException(status_code=404, detail="작업을 찾을 수 없습니다.") + + job = processing_jobs[job_id] + + if job["status"] != "completed" or not job.get("nas_translated_path"): + raise HTTPException(status_code=400, detail="처리가 완료되지 않았습니다.") + + result_path = Path(job["nas_translated_path"]) + if not result_path.exists(): + raise HTTPException(status_code=404, detail="결과 파일을 찾을 수 없습니다.") + + return FileResponse( + path=result_path, + filename=f"{Path(job['filename']).stem}.html", + media_type="text/html" + ) + +@app.get("/nas-info") +async def nas_info(): + """NAS 정보 및 통계""" + + nas_status = check_nas_connection() + + info = { + "server": f"{MAC_MINI_IP}:{MAC_MINI_PORT}", + "nas_ip": NAS_IP, + "nas_status": nas_status, + "mount_point": str(NAS_MOUNT_POINT), + "document_upload_base": str(DOCUMENT_UPLOAD_BASE), + "folders": {}, + "statistics": { + "total_jobs": len(processing_jobs), + "completed_jobs": len([j for j in processing_jobs.values() if j["status"] == "completed"]), + "processing_jobs": len([j for j in processing_jobs.values() if j["status"] == "processing"]), + "failed_jobs": len([j for j in processing_jobs.values() if j["status"] == "error"]) + }, + "recent_jobs": list(processing_jobs.values())[-5:] if processing_jobs else [] + } + + # 폴더 정보 수집 + try: + for folder_name, folder_path in [ + ("originals", NAS_ORIGINALS_PATH), + ("translated", NAS_TRANSLATED_PATH), + ("static-hosting", NAS_STATIC_HOSTING_PATH), + ("metadata", NAS_METADATA_PATH) + ]: + if folder_path.exists(): + files = list(folder_path.rglob("*.*")) + file_count = len([f for f in files if f.is_file()]) + total_size = sum(f.stat().st_size for f in files if f.is_file()) + + info["folders"][folder_name] = { + "exists": True, + "path": str(folder_path), + "file_count": file_count, + "total_size_mb": round(total_size / (1024 * 1024), 2) + } + else: + info["folders"][folder_name] = { + "exists": False, + "path": str(folder_path) + } + except Exception as e: + info["error"] = str(e) + + return info + +if __name__ == "__main__": + import uvicorn + + print(f"🚀 Mac Mini AI 번역 서버") + print(f"📡 접속 주소: http://{MAC_MINI_IP}:{MAC_MINI_PORT}") + print(f"📁 NAS Mount: {NAS_MOUNT_POINT}") + print(f"🔗 VPN 접속 전용 (내부 네트워크)") + + # 시작 전 연결 확인 + nas_status = check_nas_connection() + if nas_status["status"] == "connected": + print(f"✅ NAS 연결 확인됨") + ensure_nas_directories() + else: + print(f"❌ NAS 연결 문제: {nas_status['error']}") + + print("-" * 60) + uvicorn.run(app, host="0.0.0.0", port=MAC_MINI_PORT) diff --git a/src/fastapi_with_dashboard.py b/src/fastapi_with_dashboard.py new file mode 100644 index 0000000..0aa2572 --- /dev/null +++ b/src/fastapi_with_dashboard.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +""" +Mac Mini FastAPI + DS1525+ 연동 +백그라운드 AI 서비스 및 대시보드 통합 버전 (v2.1 - 설정 중앙화 및 Lifespan 적용) +""" + +from contextlib import asynccontextmanager +from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks, Request, Depends +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse, JSONResponse +import asyncio +import uuid +from pathlib import Path +from typing import Dict +import time +import shutil +import aiofiles +from datetime import datetime +import subprocess +import logging + +# 중앙 설정 로더 및 AI 서비스 임포트 +from config_loader import settings +from background_ai_service import ai_service +from security import get_api_key + +# 로깅 설정 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 설정에서 값 불러오기 +network_cfg = settings.network_config +paths_cfg = settings.paths_config + +MAC_MINI_IP = network_cfg.get("mac_mini_ip") +NAS_IP = network_cfg.get("nas_ip") +SERVER_PORT = network_cfg.get("server_port") + +NAS_MOUNT_POINT = Path(paths_cfg.get("nas_mount_point")) +DOCUMENT_UPLOAD_BASE = NAS_MOUNT_POINT / paths_cfg.get("document_upload_base") + +NAS_ORIGINALS_PATH = DOCUMENT_UPLOAD_BASE / paths_cfg.get("originals") +NAS_TRANSLATED_PATH = DOCUMENT_UPLOAD_BASE / paths_cfg.get("translated") +NAS_STATIC_HOSTING_PATH = DOCUMENT_UPLOAD_BASE / paths_cfg.get("static_hosting") +NAS_METADATA_PATH = DOCUMENT_UPLOAD_BASE / paths_cfg.get("metadata") + +LOCAL_WORK_PATH = Path(paths_cfg.get("local_work_path")) + +@asynccontextmanager +async def lifespan(app: FastAPI): + """FastAPI 라이프사이클 이벤트 핸들러""" + logger.info(f"🚀 Mac Mini AI 번역 서버 시작 (v2.1)") + 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']}") + create_nas_folders() + 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. 연결 후 서버 재시작") + + yield + + # 종료 시 실행될 코드 (필요 시) + logger.info("👋 서버 종료.") + +app = FastAPI( + title="AI 번역 시스템 with 대시보드", + description=f"Mac Mini ({MAC_MINI_IP}) + DS1525+ ({NAS_IP}) 연동 + 실시간 모니터링", + version="2.1.0", + lifespan=lifespan +) + +# 정적 파일 및 템플릿 +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)} + +def create_nas_folders(): + """NAS에 필요한 폴더 구조 자동 생성""" + 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}") + +# ========================================== +# 기존 엔드포인트들 +# ========================================== + +@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(api_key: str = Depends(get_api_key)): + """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_key: str = Depends(get_api_key)): + """시스템 캐시 정리 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 대시보드 (v2.1)") + logger.info(f"📡 서버 주소: http://{MAC_MINI_IP}:{SERVER_PORT}") + logger.info(f"📊 대시보드: http://{MAC_MINI_IP}:{SERVER_PORT}/dashboard") + logger.info(f"📁 NAS 주소: {NAS_IP}") + + uvicorn.run(app, host="0.0.0.0", port=SERVER_PORT) \ No newline at end of file diff --git a/src/html_generator.py b/src/html_generator.py new file mode 100755 index 0000000..bd8de21 --- /dev/null +++ b/src/html_generator.py @@ -0,0 +1,597 @@ +#!/usr/bin/env python3 +""" +다국어 HTML 생성기 +원문 + 번역문을 언어 전환 가능한 HTML로 생성 +""" + +from pathlib import Path +from typing import Dict, List +import json +import re +from datetime import datetime + +class MultilingualHTMLGenerator: + def __init__(self): + self.templates = self._load_templates() + + def _load_templates(self) -> Dict: + """HTML 템플릿들""" + return { + "base": """ + + + + + {title} + + + +
+
+
+ +
+
+ {language_buttons} +
+
+ +
+
+

{title}

+ +
+ + {toc_section} + + {summary_section} + +
+
+ 📖 본문 내용 +
+
+ {content_sections} +
+
+
+ + + +""", + + "language_button": """""", + + "metadata_item": """
+ + {label}: {value} +
""", + + "content_section": """
+
{content}
+
""", + + "summary_section": """
+
+ 📋 요약 +
+
+
+
문서 요약
+ {summary_content} +
+
+
""", + + "toc_section": """
+
📑 목차
+ +
""" + } + + def _generate_toc(self, content: str) -> List[Dict]: + """목차 자동 생성""" + # 간단한 헤딩 감지 (대문자로 시작하는 짧은 줄들) + lines = content.split('\n') + toc_items = [] + + for i, line in enumerate(lines): + line = line.strip() + # 헤딩 감지 조건 + if (len(line) < 100 and + len(line) > 5 and + (line.isupper() or + line.startswith(('Chapter', '장', '제', '1.', '2.', '3.', '4.', '5.')))): + + toc_items.append({ + "title": line, + "anchor": f"section-{i}", + "line_number": i + }) + + return toc_items[:10] # 최대 10개만 + + def generate_multilingual_html(self, + title: str, + contents: Dict[str, str], # {lang_code: content} + summary: str = "", + metadata: Dict = None, + output_path: str = "output.html") -> str: + """다국어 HTML 생성""" + + # 언어 정보 매핑 + lang_info = { + "korean": {"name": "한국어", "code": "ko", "icon": "🇰🇷"}, + "english": {"name": "English", "code": "en", "icon": "🇺🇸"}, + "japanese": {"name": "日本語", "code": "ja", "icon": "🇯🇵"} + } + + # 기본 메타데이터 + if metadata is None: + metadata = {} + + default_metadata = { + "처리_시간": datetime.now().strftime("%Y-%m-%d %H:%M"), + "언어_수": len(contents), + "총_문자수": sum(len(content) for content in contents.values()), + "생성_도구": "NLLB 번역 시스템" + } + default_metadata.update(metadata) + + # 언어 버튼 생성 + language_buttons = [] + for lang_key, content in contents.items(): + lang_data = lang_info.get(lang_key, {"name": lang_key.title(), "code": lang_key[:2], "icon": "🌐"}) + button_html = self.templates["language_button"].format( + lang_code=lang_data["code"], + lang_name=f"{lang_data['icon']} {lang_data['name']}" + ) + language_buttons.append(button_html) + + # 메타데이터 생성 + metadata_items = [] + metadata_icons = { + "처리_시간": "⏰", + "언어_수": "🌍", + "총_문자수": "📝", + "생성_도구": "⚙️", + "원본_언어": "🔤", + "파일_크기": "📊" + } + + for key, value in default_metadata.items(): + icon = metadata_icons.get(key, "📄") + label = key.replace("_", " ").title() + + metadata_html = self.templates["metadata_item"].format( + icon=icon, + label=label, + value=value + ) + metadata_items.append(metadata_html) + + # 콘텐츠 섹션 생성 + content_sections = [] + default_lang = list(contents.keys())[0] + + for lang_key, content in contents.items(): + lang_data = lang_info.get(lang_key, {"code": lang_key[:2]}) + + # 내용을 문단별로 정리 + formatted_content = self._format_content(content) + + section_html = self.templates["content_section"].format( + lang_code=lang_data["code"], + content=formatted_content + ) + content_sections.append(section_html) + + # 목차 생성 (첫 번째 언어 기준) + first_content = list(contents.values())[0] + toc_items = self._generate_toc(first_content) + + toc_html = "" + if toc_items: + toc_item_htmls = [] + for item in toc_items: + toc_item_htmls.append(f'
  • {item["title"]}
  • ') + + toc_html = self.templates["toc_section"].format( + toc_items="\n".join(toc_item_htmls) + ) + + # 요약 섹션 + summary_html = "" + if summary: + summary_content_sections = [] + for lang_key in contents.keys(): + lang_data = lang_info.get(lang_key, {"code": lang_key[:2]}) + if lang_key == "korean" or "korean" not in contents: + summary_content = f'
    {summary}
    ' + else: + summary_content = f'
    {summary}
    ' + summary_content_sections.append(summary_content) + + summary_html = self.templates["summary_section"].format( + summary_content="\n".join(summary_content_sections) + ) + + # 최종 HTML 생성 + default_lang_code = lang_info.get(default_lang, {"code": default_lang[:2]})["code"] + + html_content = self.templates["base"].format( + title=title, + language_buttons="\n".join(language_buttons), + metadata="\n".join(metadata_items), + toc_section=toc_html, + summary_section=summary_html, + content_sections="\n".join(content_sections), + default_lang=default_lang_code + ) + + # 파일 저장 + output_file = Path(output_path) + output_file.parent.mkdir(parents=True, exist_ok=True) + + with open(output_file, 'w', encoding='utf-8') as f: + f.write(html_content) + + print(f"다국어 HTML 생성 완료: {output_path}") + return str(output_file) + + def _format_content(self, content: str) -> str: + """내용 포맷팅""" + # 기본 텍스트 정리 + content = content.strip() + + # 문단 구분 개선 + content = re.sub(r'\n\s*\n\s*\n+', '\n\n', content) + + # 특수 문자 이스케이프 + content = content.replace('<', '<').replace('>', '>') + + return content + +def main(): + """HTML 생성기 테스트""" + generator = MultilingualHTMLGenerator() + + # 테스트 데이터 + test_contents = { + "english": """Chapter 1: Introduction to Machine Learning + +Machine learning represents one of the most transformative technologies of our time. This comprehensive guide explores the core concepts, methodologies, and applications that define this rapidly evolving field. + +The power of machine learning lies in its ability to handle complex problems that would be difficult or impossible to solve using conventional programming methods.""", + + "korean": """제1장: 기계학습 소개 + +기계학습은 우리 시대의 가장 혁신적인 기술 중 하나를 나타냅니다. 이 포괄적인 가이드는 빠르게 발전하는 이 분야를 정의하는 핵심 개념, 방법론 및 응용 분야를 탐구합니다. + +기계학습의 힘은 기존 프로그래밍 방법으로는 해결하기 어렵거나 불가능한 복잡한 문제를 처리할 수 있는 능력에 있습니다.""" + } + + test_summary = "이 문서는 기계학습의 기본 개념과 응용 분야에 대해 설명합니다. 기계학습이 현대 기술에서 차지하는 중요성과 복잡한 문제 해결 능력을 강조합니다." + + test_metadata = { + "원본_언어": "English", + "파일_크기": "2.3 MB" + } + + # HTML 생성 + output_path = generator.generate_multilingual_html( + title="기계학습 완전 가이드", + contents=test_contents, + summary=test_summary, + metadata=test_metadata, + output_path="output/test_multilingual.html" + ) + + print(f"테스트 HTML 생성됨: {output_path}") + +if __name__ == "__main__": + main() diff --git a/src/integrated_translation_system.py b/src/integrated_translation_system.py new file mode 100755 index 0000000..db78f22 --- /dev/null +++ b/src/integrated_translation_system.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 +""" +완전한 PDF -> HTML 번역 시스템 +PDF OCR -> NLLB 번역 -> KoBART 요약 -> HTML 생성 +""" + +import torch +import time +import json +from pathlib import Path +from typing import Dict, List, Optional, Tuple +import re +from dataclasses import dataclass + +# 문서 처리 +try: + import PyPDF2 + import pdfplumber + from docx import Document +except ImportError: + print("문서 처리 라이브러리 설치 필요: pip install PyPDF2 pdfplumber python-docx") + +# 번역 및 요약 모델 +from transformers import ( + AutoTokenizer, AutoModelForSeq2SeqLM, + PreTrainedTokenizerFast, BartForConditionalGeneration +) + +@dataclass +class TranslationResult: + original_text: str + translated_text: str + summary: str + processing_time: float + metadata: Dict + +class IntegratedTranslationSystem: + def __init__(self): + self.device = self._setup_device() + self.models = {} + self.tokenizers = {} + self.config = self._load_config() + + print(f"번역 시스템 초기화 (디바이스: {self.device})") + + def _setup_device(self) -> torch.device: + """최적 디바이스 설정""" + if torch.backends.mps.is_available(): + return torch.device("mps") + elif torch.cuda.is_available(): + return torch.device("cuda") + else: + return torch.device("cpu") + + def _load_config(self) -> Dict: + """설정 파일 로드""" + config_path = Path("config/settings.json") + if config_path.exists(): + with open(config_path, 'r', encoding='utf-8') as f: + return json.load(f) + + # 기본 설정 + return { + "translation": { + "chunk_size": 500, + "max_length": 512, + "num_beams": 4, + "batch_size": 4 + }, + "summarization": { + "max_length": 150, + "min_length": 30, + "num_beams": 4 + } + } + + def load_models(self): + """모든 모델 로드""" + print("모델 로딩 중...") + + # 1. NLLB 번역 모델 + print(" NLLB 번역 모델...") + try: + self.tokenizers['nllb'] = AutoTokenizer.from_pretrained("facebook/nllb-200-3.3B") + self.models['nllb'] = AutoModelForSeq2SeqLM.from_pretrained( + "facebook/nllb-200-3.3B", + torch_dtype=torch.float16, + low_cpu_mem_usage=True + ).to(self.device) + print(" NLLB 모델 로드 완료") + except Exception as e: + print(f" NLLB 모델 로드 실패: {e}") + return False + + # 2. KoBART 요약 모델 + print(" KoBART 요약 모델...") + try: + self.tokenizers['kobart'] = PreTrainedTokenizerFast.from_pretrained("gogamza/kobart-summarization") + self.models['kobart'] = BartForConditionalGeneration.from_pretrained( + "gogamza/kobart-summarization", + torch_dtype=torch.float16, + low_cpu_mem_usage=True + ).to(self.device) + print(" KoBART 모델 로드 완료") + except Exception as e: + print(f" KoBART 모델 로드 실패: {e}") + print(" 요약 없이 번역만 진행") + self.models['kobart'] = None + + print("모델 로딩 완료!") + return True + + def detect_language(self, text: str) -> str: + """언어 자동 감지""" + # 간단한 휴리스틱 언어 감지 + korean_chars = len(re.findall(r'[가-힣]', text)) + japanese_chars = len(re.findall(r'[ひらがなカタカナ一-龯]', text)) + english_chars = len(re.findall(r'[a-zA-Z]', text)) + + total_chars = len(text.replace(' ', '')) + + if total_chars == 0: + return "unknown" + + if korean_chars / total_chars > 0.3: + return "korean" + elif japanese_chars / total_chars > 0.1: + return "japanese" + elif english_chars / total_chars > 0.5: + return "english" + else: + return "unknown" + + def extract_text_from_pdf(self, pdf_path: str) -> str: + """PDF에서 텍스트 추출""" + print(f"PDF 텍스트 추출: {pdf_path}") + + text = "" + try: + # pdfplumber 우선 시도 + import pdfplumber + with pdfplumber.open(pdf_path) as pdf: + for page_num, page in enumerate(pdf.pages, 1): + page_text = page.extract_text() + if page_text: + text += f"\n\n{page_text}" + + print(f"PDF 텍스트 추출 완료: {len(text)}자") + + except Exception as e: + print(f"pdfplumber 실패: {e}") + + # PyPDF2 백업 + try: + import PyPDF2 + with open(pdf_path, 'rb') as file: + pdf_reader = PyPDF2.PdfReader(file) + for page in pdf_reader.pages: + page_text = page.extract_text() + if page_text: + text += f"\n\n{page_text}" + + print(f"PyPDF2로 텍스트 추출 완료: {len(text)}자") + + except Exception as e2: + print(f"PDF 텍스트 추출 완전 실패: {e2}") + return "" + + return self._clean_text(text) + + def _clean_text(self, text: str) -> str: + """추출된 텍스트 정리""" + # 과도한 공백 정리 + text = re.sub(r'\n\s*\n\s*\n', '\n\n', text) + text = re.sub(r'[ \t]+', ' ', text) + + # 페이지 번호 제거 + text = re.sub(r'\n\d+\n', '\n', text) + + return text.strip() + + def split_text_into_chunks(self, text: str, chunk_size: int = 500) -> List[str]: + """텍스트를 번역 가능한 청크로 분할""" + sentences = re.split(r'[.!?]\s+', text) + chunks = [] + current_chunk = "" + + for sentence in sentences: + test_chunk = current_chunk + " " + sentence if current_chunk else sentence + + if len(test_chunk) > chunk_size and current_chunk: + chunks.append(current_chunk.strip()) + current_chunk = sentence + else: + current_chunk = test_chunk + + if current_chunk: + chunks.append(current_chunk.strip()) + + print(f"텍스트 분할: {len(chunks)}개 청크") + return chunks + + def translate_text(self, text: str, src_lang: str = "english") -> str: + """NLLB로 텍스트 번역""" + if src_lang == "korean": + return text # 한국어는 번역하지 않음 + + # 언어 코드 매핑 + lang_map = { + "english": "eng_Latn", + "japanese": "jpn_Jpan", + "korean": "kor_Hang" + } + + src_code = lang_map.get(src_lang, "eng_Latn") + tgt_code = "kor_Hang" + + tokenizer = self.tokenizers['nllb'] + model = self.models['nllb'] + + # 청크별 번역 + chunks = self.split_text_into_chunks(text, self.config["translation"]["chunk_size"]) + translated_chunks = [] + + print(f"번역 시작: {src_lang} -> 한국어") + + for i, chunk in enumerate(chunks): + print(f" 청크 {i+1}/{len(chunks)} 번역 중...") + + inputs = tokenizer(chunk, return_tensors="pt", padding=True, truncation=True).to(self.device) + + with torch.no_grad(): + translated_tokens = model.generate( + **inputs, + forced_bos_token_id=tokenizer.convert_tokens_to_ids(tgt_code), + max_length=self.config["translation"]["max_length"], + num_beams=self.config["translation"]["num_beams"], + early_stopping=True + ) + + translated_chunk = tokenizer.decode(translated_tokens[0], skip_special_tokens=True) + translated_chunks.append(translated_chunk) + + result = "\n\n".join(translated_chunks) + print(f"번역 완료: {len(result)}자") + return result + + def summarize_text(self, text: str) -> str: + """KoBART로 한국어 텍스트 요약""" + if self.models['kobart'] is None: + print("요약 모델 없음, 첫 300자 반환") + return text[:300] + "..." if len(text) > 300 else text + + print("텍스트 요약 중...") + + tokenizer = self.tokenizers['kobart'] + model = self.models['kobart'] + + inputs = tokenizer( + text, + return_tensors="pt", + max_length=1024, + truncation=True, + padding=True, + return_token_type_ids=False + ).to(self.device) + + with torch.no_grad(): + summary_ids = model.generate( + input_ids=inputs['input_ids'], + attention_mask=inputs['attention_mask'], + max_length=self.config["summarization"]["max_length"], + min_length=self.config["summarization"]["min_length"], + num_beams=self.config["summarization"]["num_beams"], + early_stopping=True, + no_repeat_ngram_size=2, + length_penalty=1.2 + ) + + summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True) + print(f"요약 완료: {len(summary)}자") + return summary + + def process_document(self, input_path: str, output_dir: str = "output") -> TranslationResult: + """전체 문서 처리 파이프라인""" + start_time = time.time() + + print(f"문서 처리 시작: {input_path}") + + # 1. 텍스트 추출 + if input_path.lower().endswith('.pdf'): + original_text = self.extract_text_from_pdf(input_path) + else: + with open(input_path, 'r', encoding='utf-8') as f: + original_text = f.read() + + if not original_text: + raise ValueError("텍스트 추출 실패") + + # 2. 언어 감지 + detected_lang = self.detect_language(original_text) + print(f"감지된 언어: {detected_lang}") + + # 3. 번역 + if detected_lang == "korean": + translated_text = original_text + print("한국어 문서, 번역 생략") + else: + translated_text = self.translate_text(original_text, detected_lang) + + # 4. 요약 + summary = self.summarize_text(translated_text) + + # 5. 결과 저장 + output_path = Path(output_dir) + output_path.mkdir(exist_ok=True) + + base_name = Path(input_path).stem + + # 텍스트 파일 저장 + with open(output_path / f"{base_name}_translated.txt", 'w', encoding='utf-8') as f: + f.write(translated_text) + + with open(output_path / f"{base_name}_summary.txt", 'w', encoding='utf-8') as f: + f.write(summary) + + processing_time = time.time() - start_time + + result = TranslationResult( + original_text=original_text, + translated_text=translated_text, + summary=summary, + processing_time=processing_time, + metadata={ + "input_file": input_path, + "detected_language": detected_lang, + "original_chars": len(original_text), + "translated_chars": len(translated_text), + "summary_chars": len(summary), + "compression_ratio": len(summary) / len(translated_text) * 100 if translated_text else 0 + } + ) + + print(f"문서 처리 완료! ({processing_time/60:.1f}분 소요)") + return result + +def main(): + """메인 실행 함수""" + system = IntegratedTranslationSystem() + + if not system.load_models(): + print("모델 로딩 실패") + return None + + print("\n" + "="*60) + print("통합 번역 시스템 준비 완료!") + print("사용법:") + print(" result = system.process_document('input.pdf')") + print("="*60) + + return system + +if __name__ == "__main__": + system = main() diff --git a/src/nas_mount_setup.py b/src/nas_mount_setup.py new file mode 100755 index 0000000..d377169 --- /dev/null +++ b/src/nas_mount_setup.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +""" +NAS 자동 마운트 및 연결 설정 +DS1525+ (192.168.1.227) → Mac Mini (192.168.1.122) +""" + +import subprocess +import os +from pathlib import Path +import time + +class NASMountManager: + def __init__(self): + self.nas_ip = "192.168.1.227" + self.nas_share = "Media" # 또는 실제 공유 폴더명 + self.mount_point = Path("/Volumes/DS1525+") + self.smb_url = f"smb://{self.nas_ip}/{self.nas_share}" + + def check_nas_connection(self): + """NAS 연결 상태 확인""" + try: + # ping으로 NAS 접근 가능한지 확인 + result = subprocess.run( + ["ping", "-c", "3", self.nas_ip], + capture_output=True, + timeout=10 + ) + + if result.returncode == 0: + print(f"✅ NAS 연결 가능: {self.nas_ip}") + return True + else: + print(f"❌ NAS 연결 불가: {self.nas_ip}") + return False + + except subprocess.TimeoutExpired: + print(f"⏰ NAS 연결 타임아웃: {self.nas_ip}") + return False + except Exception as e: + print(f"❌ 연결 확인 오류: {e}") + return False + + def check_mount_status(self): + """마운트 상태 확인""" + if self.mount_point.exists(): + # 실제 마운트되어 있는지 확인 + try: + result = subprocess.run( + ["mount"], + capture_output=True, + text=True + ) + + if str(self.mount_point) in result.stdout: + print(f"✅ NAS 이미 마운트됨: {self.mount_point}") + return True + else: + print(f"📁 마운트 포인트 존재하지만 연결 안됨: {self.mount_point}") + return False + + except Exception as e: + print(f"❌ 마운트 상태 확인 실패: {e}") + return False + else: + print(f"📁 마운트 포인트 없음: {self.mount_point}") + return False + + def mount_nas(self, username=None, password=None): + """NAS 마운트""" + + if self.check_mount_status(): + return True + + if not self.check_nas_connection(): + return False + + try: + # 마운트 포인트 생성 + self.mount_point.mkdir(parents=True, exist_ok=True) + + # SMB 마운트 시도 + if username and password: + # 인증 정보가 있는 경우 + mount_cmd = [ + "mount", "-t", "smbfs", + f"//{username}:{password}@{self.nas_ip}/{self.nas_share}", + str(self.mount_point) + ] + else: + # 게스트 접근 시도 + mount_cmd = [ + "mount", "-t", "smbfs", + f"//{self.nas_ip}/{self.nas_share}", + str(self.mount_point) + ] + + print(f"🔄 NAS 마운트 시도: {self.smb_url}") + result = subprocess.run(mount_cmd, capture_output=True, text=True) + + if result.returncode == 0: + print(f"✅ NAS 마운트 성공: {self.mount_point}") + return True + else: + print(f"❌ NAS 마운트 실패: {result.stderr}") + return False + + except Exception as e: + print(f"❌ 마운트 프로세스 오류: {e}") + return False + + def unmount_nas(self): + """NAS 언마운트""" + try: + if self.check_mount_status(): + result = subprocess.run( + ["umount", str(self.mount_point)], + capture_output=True, + text=True + ) + + if result.returncode == 0: + print(f"✅ NAS 언마운트 완료: {self.mount_point}") + return True + else: + print(f"❌ 언마운트 실패: {result.stderr}") + return False + else: + print("ℹ️ NAS가 마운트되어 있지 않음") + return True + + except Exception as e: + print(f"❌ 언마운트 오류: {e}") + return False + + def create_document_upload_structure(self): + """Document-upload 폴더 구조 생성""" + if not self.check_mount_status(): + print("❌ NAS가 마운트되지 않음") + return False + + base_path = self.mount_point / "Document-upload" + + try: + # 기본 구조 생성 + folders = [ + "originals", + "originals/2025-01/pdfs", + "originals/2025-01/docs", + "originals/2025-01/txts", + "translated", + "translated/2025-01/english-to-korean", + "translated/2025-01/japanese-to-korean", + "translated/2025-01/korean-only", + "static-hosting/docs", + "static-hosting/assets", + "static-hosting/index", + "metadata/processing-logs" + ] + + for folder in folders: + folder_path = base_path / folder + folder_path.mkdir(parents=True, exist_ok=True) + print(f"📁 폴더 생성: {folder}") + + # 기본 README 파일 생성 + readme_content = """# Document Upload System + +이 폴더는 AI 번역 시스템에서 자동으로 관리됩니다. + +## 폴더 구조 + +- originals/: 업로드된 원본 파일들 (월별/타입별 정리) +- translated/: 번역된 HTML 파일들 (월별/언어별 정리) +- static-hosting/: 웹 호스팅용 파일들 +- metadata/: 처리 로그 및 인덱스 정보 + +## 자동 관리 + +- 파일은 월별로 자동 정리됩니다 +- 언어 감지에 따라 적절한 폴더에 저장됩니다 +- 메타데이터는 자동으로 기록됩니다 +""" + + readme_path = base_path / "README.md" + with open(readme_path, 'w', encoding='utf-8') as f: + f.write(readme_content) + + print(f"✅ Document-upload 구조 생성 완료: {base_path}") + return True + + except Exception as e: + print(f"❌ 폴더 구조 생성 실패: {e}") + return False + +def main(): + """NAS 설정 메인 함수""" + print("🚀 NAS 마운트 및 설정 시작") + print("=" * 50) + + mount_manager = NASMountManager() + + # 1. NAS 연결 확인 + if not mount_manager.check_nas_connection(): + print("\n❌ NAS에 연결할 수 없습니다.") + print("확인사항:") + print("- NAS 전원이 켜져 있는지") + print("- 네트워크 연결 상태") + print("- IP 주소: 192.168.1.227") + return False + + # 2. 마운트 시도 + if not mount_manager.check_mount_status(): + print("\n🔄 NAS 마운트 시도...") + + # 우선 게스트 접근 시도 + if not mount_manager.mount_nas(): + print("\n🔐 인증이 필요할 수 있습니다.") + print("Finder에서 수동으로 연결하거나:") + print("1. Finder → 이동 → 서버에 연결") + print("2. smb://192.168.1.227 입력") + print("3. 인증 정보 입력") + + return False + + # 3. 폴더 구조 생성 + print("\n📁 Document-upload 폴더 구조 생성...") + if mount_manager.create_document_upload_structure(): + print("\n🎉 NAS 설정 완료!") + print(f"📁 마운트 위치: {mount_manager.mount_point}") + print(f"📁 문서 저장 위치: {mount_manager.mount_point}/Document-upload") + return True + else: + print("\n❌ 폴더 구조 생성 실패") + return False + +if __name__ == "__main__": + main() diff --git a/src/security.py b/src/security.py new file mode 100644 index 0000000..86779ab --- /dev/null +++ b/src/security.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +API 보안 및 인증 모듈 +API 키 기반의 인증을 처리합니다. +""" + +from fastapi import Security, HTTPException +from fastapi.security import APIKeyHeader +from starlette import status + +from config_loader import settings + +# 설정에서 API 키 가져오기 +API_KEY = settings.get_section("security").get("api_key") +API_KEY_NAME = "X-API-KEY" + +# API 키 헤더 정의 +api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) + +async def get_api_key(header_value: str = Security(api_key_header)): + """ + 요청 헤더에서 API 키를 추출하고 유효성을 검증합니다. + 유효하지 않은 경우, HTTPException을 발생시킵니다. + """ + if not header_value: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="API 키가 필요합니다." + ) + if header_value != API_KEY: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="제공된 API 키가 유효하지 않습니다." + ) + return header_value + +if __name__ == "__main__": + # 보안 모듈 테스트 + print("✅ 보안 모듈 테스트") + print("-" * 30) + + print(f" - 설정된 API 키 이름: {API_KEY_NAME}") + print(f" - 설정된 API 키 값: {API_KEY}") + + # 예제: get_api_key 함수 테스트 (비동기 함수라 직접 실행은 어려움) + async def test_key_validation(): + print("\n[테스트 시나리오]") + + # 1. 유효한 키 + try: + await get_api_key(API_KEY) + print(" - 유효한 키 검증: 성공 ✅") + except HTTPException as e: + print(f" - 유효한 키 검증: 실패 ❌ ({e.detail})") + + # 2. 유효하지 않은 키 + try: + await get_api_key("invalid-key") + print(" - 유효하지 않은 키 검증: 성공 ✅") + except HTTPException as e: + print(f" - 유효하지 않은 키 검증: 실패 ❌ ({e.detail})") + + # 3. 키 없음 + try: + await get_api_key(None) + print(" - 키 없음 검증: 성공 ✅") + except HTTPException as e: + print(f" - 키 없음 검증: 실패 ❌ ({e.detail})") + + import asyncio + asyncio.run(test_key_validation()) + + print("-" * 30) \ No newline at end of file diff --git a/src/test_nllb_fixed.py b/src/test_nllb_fixed.py new file mode 100755 index 0000000..f0f8a88 --- /dev/null +++ b/src/test_nllb_fixed.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +NLLB 모델 테스트 (수정된 버전) +""" + +import torch +import time +from transformers import AutoTokenizer, AutoModelForSeq2SeqLM + +def test_nllb_fixed(): + print("🧪 NLLB 모델 테스트 (수정된 버전)") + + model_name = "facebook/nllb-200-3.3B" + + # 모델 로드 + print("📥 모델 로딩 중...") + tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir="models/nllb-200-3.3B") + model = AutoModelForSeq2SeqLM.from_pretrained( + model_name, + cache_dir="models/nllb-200-3.3B", + torch_dtype=torch.float16, + low_cpu_mem_usage=True + ) + + # Apple Silicon 최적화 + if torch.backends.mps.is_available(): + device = torch.device("mps") + model = model.to(device) + print("🚀 Apple Silicon MPS 가속 사용") + else: + device = torch.device("cpu") + print("💻 CPU 모드 사용") + + # NLLB 언어 코드 (수정된 방식) + def get_lang_id(tokenizer, lang_code): + """언어 코드를 토큰 ID로 변환""" + return tokenizer.convert_tokens_to_ids(lang_code) + + def translate_text(text, src_lang, tgt_lang, description): + print(f"\n📝 {description}:") + print(f"원문: {text}") + + start_time = time.time() + + # 입력 텍스트 토큰화 + inputs = tokenizer(text, return_tensors="pt", padding=True).to(device) + + # 번역 생성 (수정된 방식) + with torch.no_grad(): + generated_tokens = model.generate( + **inputs, + forced_bos_token_id=get_lang_id(tokenizer, tgt_lang), + max_length=200, + num_beams=4, + early_stopping=True, + do_sample=False, + pad_token_id=tokenizer.pad_token_id + ) + + # 결과 디코딩 + result = tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0] + + translation_time = time.time() - start_time + print(f"번역: {result}") + print(f"소요 시간: {translation_time:.2f}초") + + return result, translation_time + + print("\n" + "="*60) + + try: + # 1. 영어 → 한국어 + en_text = "Artificial intelligence is transforming the way we work and live." + translate_text(en_text, "eng_Latn", "kor_Hang", "영어 → 한국어 번역") + + # 2. 일본어 → 한국어 + ja_text = "人工知能は私たちの働き方と生活を変革しています。" + translate_text(ja_text, "jpn_Jpan", "kor_Hang", "일본어 → 한국어 번역") + + # 3. 기술 문서 테스트 + tech_text = "Machine learning algorithms require large datasets for training and validation." + translate_text(tech_text, "eng_Latn", "kor_Hang", "기술 문서 번역") + + print(f"\n✅ 모든 테스트 성공!") + return True + + except Exception as e: + print(f"❌ 테스트 중 오류: {e}") + return False + +if __name__ == "__main__": + if test_nllb_fixed(): + print("\n🎉 NLLB 모델 테스트 완료!") + print("📝 다음 단계: KoBART 요약 모델 설치") + else: + print("\n❌ 테스트 실패") diff --git a/src/test_summarizer_fixed.py b/src/test_summarizer_fixed.py new file mode 100755 index 0000000..bfccc55 --- /dev/null +++ b/src/test_summarizer_fixed.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +요약 모델 테스트 (토큰 오류 수정) +""" + +import torch +import time +from transformers import PreTrainedTokenizerFast, BartForConditionalGeneration + +def test_summarizer_fixed(): + print("🧪 한국어 요약 모델 테스트 (수정된 버전)") + + model_name = "gogamza/kobart-summarization" + + try: + # 모델 로드 + print("📥 모델 로딩 중...") + tokenizer = PreTrainedTokenizerFast.from_pretrained(model_name) + model = BartForConditionalGeneration.from_pretrained( + model_name, + torch_dtype=torch.float16, + low_cpu_mem_usage=True + ) + + # Apple Silicon 최적화 + if torch.backends.mps.is_available(): + device = torch.device("mps") + model = model.to(device) + print("🚀 Apple Silicon MPS 가속 사용") + else: + device = torch.device("cpu") + print("💻 CPU 모드 사용") + + def summarize_text_fixed(text): + print(f"\n📝 요약 테스트:") + print(f"원문 ({len(text)}자):") + print(f"{text[:150]}...") + + start_time = time.time() + + # 토큰화 (token_type_ids 제거) + inputs = tokenizer( + text, + return_tensors="pt", + max_length=1024, + truncation=True, + padding=True, + return_token_type_ids=False # 이 부분이 핵심! + ).to(device) + + # 요약 생성 + with torch.no_grad(): + summary_ids = model.generate( + input_ids=inputs['input_ids'], + attention_mask=inputs['attention_mask'], + max_length=150, + min_length=30, + num_beams=4, + early_stopping=True, + no_repeat_ngram_size=2, + length_penalty=1.2, + do_sample=False + ) + + # 결과 디코딩 + summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True) + + process_time = time.time() - start_time + print(f"\n📋 요약 결과 ({len(summary)}자):") + print(f"{summary}") + print(f"⏱️ 처리 시간: {process_time:.2f}초") + print(f"📊 압축률: {len(summary)/len(text)*100:.1f}%") + + return summary + + # 테스트 실행 + test_text = """ + 인공지능과 기계학습 기술이 급속도로 발전하면서 우리의 일상생활과 업무 환경에 혁신적인 변화를 가져오고 있습니다. + 특히 자연어 처리 분야에서는 번역, 요약, 대화형 AI 등의 기술이 실용적인 수준에 도달하여 다양한 서비스에 적용되고 있습니다. + 기계학습 알고리즘은 대량의 텍스트 데이터를 학습하여 언어의 패턴과 의미를 이해하고, 이를 바탕으로 인간과 유사한 수준의 + 언어 처리 능력을 보여주고 있습니다. 딥러닝 기술의 발전으로 번역의 정확도가 크게 향상되었으며, 실시간 번역 서비스도 + 일상적으로 사용할 수 있게 되었습니다. + """ + + summarize_text_fixed(test_text.strip()) + + print(f"\n✅ 요약 모델 테스트 성공!") + return True + + except Exception as e: + print(f"❌ 테스트 실패: {e}") + return False + +if __name__ == "__main__": + print("🚀 한국어 요약 모델 테스트 (수정)") + print("="*50) + + if test_summarizer_fixed(): + print("\n🎉 요약 모델 정상 작동!") + print("📝 다음 단계: 통합 번역 시스템 구축") + else: + print("\n❌ 여전히 문제 있음") + print("📝 요약 없이 번역만으로 진행 고려") 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 diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..ab11568 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,468 @@ + + + + + + AI 번역 시스템 + + + +
    +
    +

    🤖 AI 번역 시스템

    +

    PDF/문서를 다국어 HTML로 자동 변환

    +
    + + + +
    +
    + {% if nas_status.status == "connected" %} + ✅ NAS 연결됨 (DS1525+) + {% else %} + ❌ NAS 연결 실패: {{ nas_status.error }} + {% endif %} +
    +
    + 🚀 Mac Mini Ready +
    +
    + +
    + {% if nas_status.status != "connected" %} +
    +

    NAS 연결 필요

    +

    {{ nas_status.error }}

    +

    해결 방법:

    +
      +
    • Finder → 이동 → 서버에 연결
    • +
    • smb://192.168.1.227 입력
    • +
    • DS1525+ 연결 후 페이지 새로고침
    • +
    +
    + {% endif %} + +
    +
    +
    📁
    +
    파일을 드래그하거나 클릭하여 업로드
    +
    PDF, TXT, DOCX 파일 지원 (최대 100MB)
    + + +
    +
    + +
    +
    처리 진행 상황
    +
    +
    +
    +
    준비 중...
    +
    + + +
    +
    + + + + diff --git a/templates/library_organized.html b/templates/library_organized.html new file mode 100644 index 0000000..4a59b23 --- /dev/null +++ b/templates/library_organized.html @@ -0,0 +1,373 @@ + + + + + + 문서 라이브러리 - AI 번역 시스템 + + + +
    +
    +

    📚 문서 라이브러리

    +

    체계적으로 분류된 다국어 번역 문서

    +
    + + + +
    +
    + 📄 + 총 {{ total_documents }}개 문서 +
    +
    + 🗂️ + {{ categories|length }}개 카테고리 +
    +
    + 🌍 + 다국어 지원 +
    +
    + +
    +
    + + +
    + + {% if categories %} + {% for category_name, documents in categories.items() %} +
    +
    +
    + {% if category_name == "English To Korean" %} + 🇺🇸→🇰🇷 English to Korean + {% elif category_name == "Japanese To Korean" %} + 🇯🇵→🇰🇷 Japanese to Korean + {% elif category_name == "Korean Only" %} + 🇰🇷 Korean Documents + {% else %} + {{ category_name }} + {% endif %} +
    +
    {{ documents|length }}개
    +
    + +
    + {% for doc in documents %} +
    +
    {{ doc.language_display }}
    +
    {{ doc.name }}
    +
    +
    + 📅 + {{ doc.created }} +
    +
    + 📊 + {{ doc.size }} +
    +
    + 📁 + {{ doc.month }} +
    +
    + 🌐 + {{ doc.language_type.replace("-", " ")|title }} +
    +
    + +
    + {% endfor %} +
    +
    + {% endfor %} + {% else %} +
    +
    📚
    +
    아직 변환된 문서가 없습니다
    +
    PDF나 텍스트 파일을 업로드하여 다국어 문서를 만들어보세요!
    + 📤 첫 번째 문서 업로드 +
    + {% endif %} +
    +
    + + + + diff --git a/~/Library/LaunchAgents/com.nllb-translation-system.app.plist b/~/Library/LaunchAgents/com.nllb-translation-system.app.plist new file mode 100644 index 0000000..bfbce53 --- /dev/null +++ b/~/Library/LaunchAgents/com.nllb-translation-system.app.plist @@ -0,0 +1,38 @@ + + + + + + Label + com.nllb-translation-system.app + + + ProgramArguments + + + /Users/hyungi/Scripts/nllb-translation-system/nllb_env/bin/python + + /Users/hyungi/Scripts/nllb-translation-system/src/fastapi_with_dashboard.py + + + + WorkingDirectory + /Users/hyungi/Scripts/nllb-translation-system + + + RunAtLoad + + + + KeepAlive + + + + StandardOutPath + /Users/hyungi/Scripts/nllb-translation-system/logs/service.log + + + StandardErrorPath + /Users/hyungi/Scripts/nllb-translation-system/logs/service_error.log + + \ No newline at end of file