463 lines
16 KiB
Python
463 lines
16 KiB
Python
#!/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)
|