Merge commit '397efb86dc84197b74d9a3b16a11b1d0d534ad9e' as 'integrations/document-ai'
This commit is contained in:
462
integrations/document-ai/src/fastapi_final.py
Normal file
462
integrations/document-ai/src/fastapi_final.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user