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

849 lines
32 KiB
Python

#!/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)