Files
ai-server/integrations/document-ai/src/fastapi_final.py

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)