Files
ncuetalk_backend/backend/engines/doc_translation/__init__.py
dsyoon 46460b77f8 init
2025-12-27 14:06:26 +09:00

346 lines
14 KiB
Python

"""backend.engines.doc_translation
문서번역 엔진 모듈.
MS Word 문서를 업로드하여 한글을 영어로 번역하는 기능을 제공합니다.
"""
import os
import shutil
import re
from datetime import datetime
from typing import List
from fastapi import APIRouter, UploadFile, File, HTTPException, Form
from fastapi.responses import FileResponse
from docx import Document as DocxDocument
import openai
from dotenv import load_dotenv
# .env 파일 로드
load_dotenv()
TOOL_ID = "doc_translation"
TOOL_INFO = {
"name": "문서번역",
"description": "MS Word 문서를 업로드하면 한글을 영어로 번역합니다.",
"system_prompt": (
"당신은 전문 번역가입니다. 한국어 문서를 영어로 정확하고 자연스럽게 번역해주세요. "
"번역할 때는 원문의 의미와 뉘앙스를 최대한 보존하면서도 영어로 자연스럽게 표현해주세요. "
"대상 문장 다음 줄에 번역 문장을 제공해주세요."
)
}
# 업로드 디렉토리 설정 - 프로젝트 루트/uploads (개발챗봇과 동일)
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
UPLOAD_DIR = os.path.join(ROOT_DIR, "uploads")
os.makedirs(UPLOAD_DIR, exist_ok=True)
# OpenAI API 키 설정
openai.api_key = os.getenv("OPENAI_API_KEY")
def translate_text_with_gpt(text: str) -> str:
"""ChatGPT를 사용하여 텍스트를 한글에서 영어로 번역 (채팅용)"""
try:
client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
response = client.chat.completions.create(
model="gpt-5",
messages=[
{
"role": "system",
"content": TOOL_INFO["system_prompt"]
},
{
"role": "user",
"content": f"다음 한국어 텍스트를 영어로 번역해주세요:\n\n{text}"
}
]
)
return response.choices[0].message.content
except Exception as e:
print(f"번역 오류: {e}")
return f"번역 오류가 발생했습니다: {str(e)}"
# (Ollama 내부 모델 번역 기능 제거)
def translate_paragraph(paragraph: str) -> str:
"""단일 단락을 번역"""
try:
client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
response = client.chat.completions.create(
model="gpt-5",
messages=[
{
"role": "system",
"content": "당신은 전문 번역가입니다. 한국어 문단을 영어로 정확하고 자연스럽게 번역해주세요. 번역문만 제공하세요."
},
{
"role": "user",
"content": f"다음 한국어 문단을 영어로 번역해주세요:\n\n{paragraph}"
}
]
)
return response.choices[0].message.content.strip()
except Exception as e:
print(f"단락 번역 오류: {e}")
return f"[번역 오류: {str(e)}]"
def extract_paragraphs_from_docx(file_path: str) -> List[str]:
"""Word 문서에서 단락별 텍스트 추출"""
try:
doc = DocxDocument(file_path)
paragraphs = []
for paragraph in doc.paragraphs:
if paragraph.text.strip():
paragraphs.append(paragraph.text.strip())
return paragraphs
except Exception as e:
raise HTTPException(status_code=400, detail=f"Word 파일 읽기 오류: {str(e)}")
def create_bilingual_docx(original_paragraphs: List[str], translated_paragraphs: List[str], output_path: str):
"""원본과 번역이 번갈아 나타나는 이중언어 Word 문서 생성"""
try:
doc = DocxDocument()
# 각 단락별로 원본과 번역을 번갈아 추가
for i, (original, translated) in enumerate(zip(original_paragraphs, translated_paragraphs)):
# 원본 단락 추가 (굵은 글씨로)
original_para = doc.add_paragraph()
original_run = original_para.add_run(original)
original_run.bold = True
# 번역 단락 추가 (일반 글씨로)
translated_para = doc.add_paragraph(translated)
# 단락 사이에 여백 추가 (마지막 단락이 아닌 경우)
if i < len(original_paragraphs) - 1:
doc.add_paragraph("") # 빈 줄 추가
doc.save(output_path)
except Exception as e:
raise HTTPException(status_code=500, detail=f"이중언어 파일 생성 오류: {str(e)}")
def prepare_context(question: str, vector_store=None, model: str = "external") -> str:
"""채팅용 컨텍스트 준비 (실시간 번역)"""
if not question.strip():
return ""
# 모델 선택에 따른 번역 처리
translated = translate_text_with_gpt(question)
return translated
# FastAPI Router
router = APIRouter()
@router.post("/upload_doc")
async def upload_document(files: List[UploadFile] = File(...)):
"""MS Word 문서 업로드 및 번역 처리"""
if not files:
raise HTTPException(status_code=400, detail="파일이 업로드되지 않았습니다.")
processed_files = []
for file in files:
# MS Word 파일 확인
if not (file.filename.lower().endswith('.doc') or file.filename.lower().endswith('.docx')):
raise HTTPException(status_code=400, detail="MS Word 파일(.doc, .docx)만 업로드 가능합니다.")
# 파일명 생성: [doc_translation]_admin_YYYYMMDDHHMMSSMMM_원본파일명
timestamp = datetime.now().strftime('%Y%m%d%H%M%S%f')[:-3] # 밀리초 3자리까지
original_name = os.path.splitext(file.filename)[0]
extension = os.path.splitext(file.filename)[1]
uploaded_filename = f"[{TOOL_ID}]_admin_{timestamp}_{original_name}{extension}"
uploaded_path = os.path.join(UPLOAD_DIR, uploaded_filename)
# 파일 저장
try:
with open(uploaded_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# 단락별 텍스트 추출
paragraphs = extract_paragraphs_from_docx(uploaded_path)
if not paragraphs:
raise HTTPException(status_code=400, detail="문서에 번역할 텍스트가 없습니다.")
# 각 단락을 개별적으로 번역
translated_paragraphs = []
for paragraph in paragraphs:
translated = translate_paragraph(paragraph)
translated_paragraphs.append(translated)
# 이중언어 문서 저장 (원본 + 번역)
result_filename = f"[{TOOL_ID}]_admin_{timestamp}_{original_name}_결과{extension}"
result_path = os.path.join(UPLOAD_DIR, result_filename)
create_bilingual_docx(paragraphs, translated_paragraphs, result_path)
processed_files.append({
"original_filename": uploaded_filename,
"result_filename": result_filename,
"status": "success"
})
except Exception as e:
# 업로드된 파일이 있다면 삭제
if os.path.exists(uploaded_path):
os.remove(uploaded_path)
raise HTTPException(status_code=500, detail=f"파일 처리 오류: {str(e)}")
return {"status": "success", "files": processed_files}
@router.get("/files")
async def list_files():
"""업로드된 파일 목록 조회"""
try:
files = []
for filename in os.listdir(UPLOAD_DIR):
if filename.startswith(f"[{TOOL_ID}]") and not filename.endswith("_결과.doc") and not filename.endswith("_결과.docx"):
file_path = os.path.join(UPLOAD_DIR, filename)
stat = os.stat(file_path)
# 결과 파일 존재 여부 확인
# 파일명에서 확장자 분리
base_name, ext = os.path.splitext(filename)
result_filename = f"{base_name}_결과{ext}"
result_exists = os.path.exists(os.path.join(UPLOAD_DIR, result_filename))
files.append({
"filename": filename,
"upload_time": datetime.fromtimestamp(stat.st_ctime).isoformat(),
"size": stat.st_size,
"has_result": result_exists,
"result_filename": result_filename if result_exists else None
})
# 업로드 시간 순으로 정렬 (최신순)
files.sort(key=lambda x: x["upload_time"], reverse=True)
return files
except Exception as e:
raise HTTPException(status_code=500, detail=f"파일 목록 조회 오류: {str(e)}")
@router.get("/download/{filename}")
async def download_file(filename: str):
"""번역된 파일 다운로드"""
file_path = os.path.join(UPLOAD_DIR, filename)
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="파일을 찾을 수 없습니다.")
return FileResponse(
path=file_path,
filename=filename,
media_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
@router.post("/translate_chat")
async def translate_chat(message: str = Form(...)):
"""실시간 채팅 번역"""
if not message.strip():
raise HTTPException(status_code=400, detail="번역할 메시지가 없습니다.")
try:
translated = translate_text_with_gpt(message)
return {"translated_text": translated}
except Exception as e:
raise HTTPException(status_code=500, detail=f"번역 오류: {str(e)}")
@router.delete("/files/{filename}")
async def delete_file(filename: str):
"""파일 삭제 (서버 저장 파일명으로)"""
file_path = os.path.join(UPLOAD_DIR, filename)
if os.path.exists(file_path):
os.remove(file_path)
# 해당하는 결과 파일도 삭제
if not filename.endswith("_결과.doc") and not filename.endswith("_결과.docx"):
base_name, ext = os.path.splitext(filename)
result_filename = f"{base_name}_결과{ext}"
result_path = os.path.join(UPLOAD_DIR, result_filename)
if os.path.exists(result_path):
os.remove(result_path)
return {"status": "success", "message": "파일이 삭제되었습니다."}
@router.delete("/delete_by_original_name/{original_filename}")
async def delete_file_by_original_name(original_filename: str):
"""파일 삭제 (원본 파일명으로) - 외부 접근용"""
try:
# 업로드된 파일 중에서 원본 파일명과 일치하는 파일 찾기
deleted_files = []
for filename in os.listdir(UPLOAD_DIR):
if filename.startswith(f"[{TOOL_ID}]"):
# 실제 파일명 추출
pattern = r'^\[doc_translation\]_admin_\d{14,17}_(.+)$'
match = re.match(pattern, filename)
if match and match.group(1) == original_filename:
# 원본 파일 삭제
file_path = os.path.join(UPLOAD_DIR, filename)
if os.path.exists(file_path):
os.remove(file_path)
deleted_files.append(filename)
# 결과 파일도 삭제
if not filename.endswith("_결과.doc") and not filename.endswith("_결과.docx"):
base_name, ext = os.path.splitext(filename)
result_filename = f"{base_name}_결과{ext}"
result_path = os.path.join(UPLOAD_DIR, result_filename)
if os.path.exists(result_path):
os.remove(result_path)
deleted_files.append(result_filename)
if deleted_files:
return {"status": "success", "message": f"파일이 삭제되었습니다.", "deleted_files": deleted_files}
else:
return {"status": "error", "message": "해당하는 파일을 찾을 수 없습니다."}
except Exception as e:
raise HTTPException(status_code=500, detail=f"파일 삭제 오류: {str(e)}")
@router.get("/list_files")
async def list_all_files():
"""모든 파일 목록 조회 (원본 파일명으로) - 외부 접근용"""
try:
files = []
for filename in os.listdir(UPLOAD_DIR):
if filename.startswith(f"[{TOOL_ID}]") and not filename.endswith("_결과.doc") and not filename.endswith("_결과.docx"):
# 실제 파일명 추출
pattern = r'^\[doc_translation\]_admin_\d{14,17}_(.+)$'
match = re.match(pattern, filename)
if match:
original_name = match.group(1)
file_path = os.path.join(UPLOAD_DIR, filename)
stat = os.stat(file_path)
# 결과 파일 존재 여부 확인
base_name, ext = os.path.splitext(filename)
result_filename = f"{base_name}_결과{ext}"
result_exists = os.path.exists(os.path.join(UPLOAD_DIR, result_filename))
files.append({
"original_filename": original_name,
"server_filename": filename,
"upload_time": datetime.fromtimestamp(stat.st_ctime).isoformat(),
"size": stat.st_size,
"has_result": result_exists,
"result_filename": result_filename if result_exists else None
})
# 업로드 시간 순으로 정렬 (최신순)
files.sort(key=lambda x: x["upload_time"], reverse=True)
return {"status": "success", "files": files}
except Exception as e:
raise HTTPException(status_code=500, detail=f"파일 목록 조회 오류: {str(e)}")