Merge commit '397efb86dc84197b74d9a3b16a11b1d0d534ad9e' as 'integrations/document-ai'
This commit is contained in:
848
integrations/document-ai/src/fastapi_port_20080.py
Normal file
848
integrations/document-ai/src/fastapi_port_20080.py
Normal file
@@ -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"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🤖 AI 번역 시스템</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #2563eb, #3b82f6);
|
||||
color: white;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}}
|
||||
.header h1 {{ font-size: 2.5rem; margin-bottom: 10px; }}
|
||||
.header p {{ font-size: 1.1rem; opacity: 0.9; }}
|
||||
.server-info {{
|
||||
background: #f0f9ff;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #bae6fd;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}}
|
||||
.info-item {{ display: flex; align-items: center; gap: 10px; color: #0369a1; }}
|
||||
.status {{ padding: 20px; border-radius: 8px; margin: 20px; }}
|
||||
.status-good {{ background: #d1fae5; color: #065f46; }}
|
||||
.status-bad {{ background: #fee2e2; color: #dc2626; }}
|
||||
.main-content {{ padding: 40px; }}
|
||||
.upload-area {{
|
||||
border: 3px dashed #e5e7eb;
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
border-radius: 16px;
|
||||
transition: all 0.3s ease;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
.upload-area:hover {{ border-color: #2563eb; background: #f8fafc; }}
|
||||
.upload-area h3 {{ font-size: 1.5rem; margin-bottom: 20px; color: #374151; }}
|
||||
input[type="file"] {{ margin: 20px 0; padding: 10px; }}
|
||||
button {{
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
padding: 15px 30px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.3s ease;
|
||||
}}
|
||||
button:hover {{ background: #1d4ed8; }}
|
||||
button:disabled {{ background: #9ca3af; cursor: not-allowed; }}
|
||||
.progress {{ margin: 20px 0; padding: 20px; background: #f8fafc; border-radius: 12px; display: none; }}
|
||||
.progress-bar {{ width: 100%; height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; }}
|
||||
.progress-fill {{ height: 100%; background: #2563eb; width: 0%; transition: width 0.3s ease; }}
|
||||
.links {{ margin: 30px 0; }}
|
||||
.links h3 {{ margin-bottom: 15px; color: #374151; }}
|
||||
.links ul {{ list-style: none; }}
|
||||
.links li {{ margin: 8px 0; }}
|
||||
.links a {{
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.3s ease;
|
||||
}}
|
||||
.links a:hover {{ background: #eff6ff; }}
|
||||
.result {{
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #0ea5e9;
|
||||
border-radius: 12px;
|
||||
display: none;
|
||||
}}
|
||||
.result h4 {{ color: #0369a1; margin-bottom: 15px; }}
|
||||
.result-actions {{ display: flex; gap: 15px; }}
|
||||
.btn {{
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}}
|
||||
.btn-primary {{ background: #2563eb; color: white; }}
|
||||
.btn-secondary {{ background: #f8fafc; color: #374151; border: 1px solid #e5e7eb; }}
|
||||
@media (max-width: 768px) {{
|
||||
.server-info {{ grid-template-columns: 1fr; }}
|
||||
.result-actions {{ flex-direction: column; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🤖 AI 번역 시스템</h1>
|
||||
<p>PDF/문서를 다국어 HTML로 자동 변환</p>
|
||||
</div>
|
||||
|
||||
<div class="server-info">
|
||||
<div class="info-item">
|
||||
<span>🖥️</span>
|
||||
<span>Mac Mini: {MAC_MINI_IP}:{MAC_MINI_PORT}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>💾</span>
|
||||
<span>NAS: {NAS_IP} (DS1525+)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status {'status-good' if nas_status['status'] == 'connected' else 'status-bad'}">
|
||||
{'✅ NAS 연결됨: ' + nas_status.get('mount_point', '') if nas_status['status'] == 'connected' else '❌ NAS 연결 실패: ' + nas_status.get('error', '')}
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="upload-area">
|
||||
<h3>📁 파일 업로드</h3>
|
||||
<p style="color: #6b7280; margin-bottom: 20px;">PDF, TXT, DOCX 파일 지원 (최대 100MB)</p>
|
||||
<form id="uploadForm" enctype="multipart/form-data">
|
||||
<input type="file" id="fileInput" accept=".pdf,.txt,.docx,.doc" style="margin: 20px 0;" />
|
||||
<br>
|
||||
<button type="submit" {'disabled' if nas_status['status'] != 'connected' else ''}>
|
||||
{'📤 파일 업로드' if nas_status['status'] == 'connected' else '❌ NAS 연결 필요'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="progress" class="progress">
|
||||
<div style="margin-bottom: 10px; font-weight: 500;" id="progressText">처리 중...</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result" class="result">
|
||||
<h4>🎉 변환 완료!</h4>
|
||||
<div class="result-actions">
|
||||
<a href="#" id="downloadBtn" class="btn btn-primary">📥 HTML 다운로드</a>
|
||||
<a href="http://{MAC_MINI_IP}:{MAC_MINI_PORT}/nas-info" class="btn btn-secondary">📊 NAS 정보</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<h3>🔗 시스템 링크</h3>
|
||||
<ul>
|
||||
<li><a href="http://{MAC_MINI_IP}:{MAC_MINI_PORT}/system-status" target="_blank">📊 시스템 상태</a></li>
|
||||
<li><a href="http://{MAC_MINI_IP}:{MAC_MINI_PORT}/nas-info" target="_blank">💾 NAS 정보</a></li>
|
||||
<li><a href="http://{MAC_MINI_IP}:{MAC_MINI_PORT}/docs" target="_blank">📖 API 문서</a></li>
|
||||
<li><a href="/Volumes/Media/Document-upload" onclick="alert('Finder에서 열기: /Volumes/Media/Document-upload'); return false;">📁 NAS 폴더 (로컬)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentJobId = null;
|
||||
|
||||
document.getElementById('uploadForm').onsubmit = async function(e) {{
|
||||
e.preventDefault();
|
||||
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const progressDiv = document.getElementById('progress');
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
if (!fileInput.files[0]) {{
|
||||
alert('파일을 선택해주세요.');
|
||||
return;
|
||||
}}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileInput.files[0]);
|
||||
|
||||
// UI 업데이트
|
||||
progressDiv.style.display = 'block';
|
||||
resultDiv.style.display = 'none';
|
||||
document.getElementById('progressText').textContent = '업로드 중...';
|
||||
document.getElementById('progressFill').style.width = '10%';
|
||||
|
||||
try {{
|
||||
const response = await fetch('/upload', {{
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {{
|
||||
currentJobId = result.job_id;
|
||||
document.getElementById('progressText').textContent = '업로드 완료, 처리 시작...';
|
||||
document.getElementById('progressFill').style.width = '20%';
|
||||
|
||||
// 진행 상황 모니터링
|
||||
monitorJob();
|
||||
}} else {{
|
||||
throw new Error(result.detail || '업로드 실패');
|
||||
}}
|
||||
}} catch (error) {{
|
||||
progressDiv.style.display = 'none';
|
||||
alert('업로드 실패: ' + error.message);
|
||||
}}
|
||||
}};
|
||||
|
||||
async function monitorJob() {{
|
||||
if (!currentJobId) return;
|
||||
|
||||
try {{
|
||||
const response = await fetch(`/status/${{currentJobId}}`);
|
||||
const status = await response.json();
|
||||
|
||||
// 진행률 업데이트
|
||||
document.getElementById('progressText').textContent = status.message;
|
||||
document.getElementById('progressFill').style.width = status.progress + '%';
|
||||
|
||||
if (status.status === 'completed') {{
|
||||
// 완료 처리
|
||||
document.getElementById('progress').style.display = 'none';
|
||||
document.getElementById('result').style.display = 'block';
|
||||
document.getElementById('downloadBtn').href = `/download/${{currentJobId}}`;
|
||||
|
||||
}} else if (status.status === 'error') {{
|
||||
document.getElementById('progress').style.display = 'none';
|
||||
alert('처리 실패: ' + status.message);
|
||||
|
||||
}} else {{
|
||||
// 계속 모니터링
|
||||
setTimeout(monitorJob, 2000);
|
||||
}}
|
||||
|
||||
}} catch (error) {{
|
||||
console.error('상태 확인 오류:', error);
|
||||
setTimeout(monitorJob, 5000);
|
||||
}}
|
||||
}}
|
||||
|
||||
// 페이지 로드시 서버 상태 확인
|
||||
window.onload = function() {{
|
||||
console.log('🚀 AI 번역 시스템 로드 완료');
|
||||
console.log('📍 서버: http://{MAC_MINI_IP}:{MAC_MINI_PORT}');
|
||||
}};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
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"""<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{base_name} - AI 번역 결과</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 40px;
|
||||
line-height: 1.6;
|
||||
background: #f8fafc;
|
||||
}}
|
||||
.container {{ max-width: 900px; margin: 0 auto; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 25px rgba(0,0,0,0.1); }}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #2563eb, #3b82f6);
|
||||
color: white;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}}
|
||||
.header h1 {{ font-size: 2rem; margin-bottom: 10px; }}
|
||||
.meta {{ background: #f0f9ff; padding: 20px; border-bottom: 1px solid #bae6fd; }}
|
||||
.meta-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }}
|
||||
.meta-item {{ display: flex; align-items: center; gap: 8px; color: #0369a1; }}
|
||||
.content {{ padding: 40px; }}
|
||||
.section {{ margin-bottom: 30px; }}
|
||||
.section h2 {{ color: #1f2937; border-bottom: 2px solid #e5e7eb; padding-bottom: 10px; }}
|
||||
.info-box {{ background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin: 20px 0; }}
|
||||
.footer {{ background: #f8fafc; padding: 20px 40px; text-align: center; color: #6b7280; font-size: 0.9rem; }}
|
||||
.system-info {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; }}
|
||||
.system-card {{ background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 15px; }}
|
||||
@media (max-width: 768px) {{
|
||||
body {{ padding: 20px; }}
|
||||
.meta-grid, .system-info {{ grid-template-columns: 1fr; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📄 {base_name}</h1>
|
||||
<p>AI 번역 시스템 처리 결과</p>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<div class="meta-grid">
|
||||
<div class="meta-item">
|
||||
<span>⏰</span>
|
||||
<span>{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span>📁</span>
|
||||
<span>{nas_file_path.name}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span>🆔</span>
|
||||
<span>{job_id[:8]}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span>🖥️</span>
|
||||
<span>{MAC_MINI_IP}:{MAC_MINI_PORT}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="section">
|
||||
<h2>📋 문서 정보</h2>
|
||||
<div class="info-box">
|
||||
<p><strong>원본 파일:</strong> {nas_file_path.name}</p>
|
||||
<p><strong>저장 위치:</strong> {nas_html_path}</p>
|
||||
<p><strong>처리 서버:</strong> Mac Mini ({MAC_MINI_IP}:{MAC_MINI_PORT})</p>
|
||||
<p><strong>NAS 저장소:</strong> DS1525+ ({NAS_IP})</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>🤖 AI 처리 결과</h2>
|
||||
<div class="info-box">
|
||||
<h3>테스트 모드</h3>
|
||||
<p>현재 AI 번역 시스템이 테스트 모드로 실행 중입니다.</p>
|
||||
<p>실제 AI 모델(NLLB, KoBART)이 연동되면 이 부분에 다음 내용이 표시됩니다:</p>
|
||||
<ul style="margin-left: 20px;">
|
||||
<li>자동 언어 감지 결과</li>
|
||||
<li>한국어 번역 텍스트</li>
|
||||
<li>문서 요약</li>
|
||||
<li>다국어 지원 인터페이스</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>🌐 시스템 구성</h2>
|
||||
<div class="system-info">
|
||||
<div class="system-card">
|
||||
<h4>Mac Mini M4 Pro</h4>
|
||||
<p>IP: {MAC_MINI_IP}:{MAC_MINI_PORT}</p>
|
||||
<p>역할: AI 처리 서버</p>
|
||||
<p>모델: NLLB + KoBART</p>
|
||||
</div>
|
||||
<div class="system-card">
|
||||
<h4>Synology DS1525+</h4>
|
||||
<p>IP: {NAS_IP}</p>
|
||||
<p>역할: 파일 저장소</p>
|
||||
<p>마운트: /Volumes/Media</p>
|
||||
</div>
|
||||
<div class="system-card">
|
||||
<h4>네트워크</h4>
|
||||
<p>연결: 2.5GbE</p>
|
||||
<p>접근: VPN 필요</p>
|
||||
<p>프로토콜: SMB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>🔗 관련 링크</h2>
|
||||
<div class="info-box">
|
||||
<p><a href="http://{MAC_MINI_IP}:{MAC_MINI_PORT}" target="_blank">🏠 메인 페이지</a></p>
|
||||
<p><a href="http://{MAC_MINI_IP}:{MAC_MINI_PORT}/system-status" target="_blank">📊 시스템 상태</a></p>
|
||||
<p><a href="http://{MAC_MINI_IP}:{MAC_MINI_PORT}/nas-info" target="_blank">💾 NAS 정보</a></p>
|
||||
<p><a href="http://{MAC_MINI_IP}:{MAC_MINI_PORT}/docs" target="_blank">📖 API 문서</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>AI 번역 시스템 v1.0.0 | Mac Mini M4 Pro + Synology DS1525+ | 자동 생성 문서</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
# 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)
|
||||
Reference in New Issue
Block a user