1st_fix_login_issue

This commit is contained in:
beabigegg
2025-09-02 10:31:35 +08:00
commit a60d965317
103 changed files with 12402 additions and 0 deletions

19
app/services/__init__.py Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
業務服務模組
Author: PANJIT IT Team
Created: 2024-01-28
Modified: 2024-01-28
"""
from .dify_client import DifyClient
from .translation_service import TranslationService
from .notification_service import NotificationService
__all__ = [
'DifyClient',
'TranslationService',
'NotificationService'
]

273
app/services/dify_client.py Normal file
View File

@@ -0,0 +1,273 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Dify API 客戶端服務
Author: PANJIT IT Team
Created: 2024-01-28
Modified: 2024-01-28
"""
import time
import requests
from typing import Dict, Any, Optional
from flask import current_app
from app.utils.logger import get_logger
from app.utils.exceptions import APIError
from app.models.stats import APIUsageStats
logger = get_logger(__name__)
class DifyClient:
"""Dify API 客戶端"""
def __init__(self):
self.base_url = current_app.config.get('DIFY_API_BASE_URL', '')
self.api_key = current_app.config.get('DIFY_API_KEY', '')
self.timeout = (10, 60) # (連接超時, 讀取超時)
self.max_retries = 3
self.retry_delay = 1.6 # 指數退避基數
if not self.base_url or not self.api_key:
logger.warning("Dify API configuration is incomplete")
def _make_request(self, method: str, endpoint: str, data: Dict[str, Any] = None,
user_id: int = None, job_id: int = None) -> Dict[str, Any]:
"""發送 HTTP 請求到 Dify API"""
if not self.base_url or not self.api_key:
raise APIError("Dify API 未配置完整")
url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json',
'User-Agent': 'PANJIT-Document-Translator/1.0'
}
# 重試邏輯
last_exception = None
start_time = time.time()
for attempt in range(self.max_retries):
try:
logger.debug(f"Making Dify API request: {method} {url} (attempt {attempt + 1})")
if method.upper() == 'GET':
response = requests.get(url, headers=headers, timeout=self.timeout, params=data)
else:
response = requests.post(url, headers=headers, timeout=self.timeout, json=data)
# 計算響應時間
response_time_ms = int((time.time() - start_time) * 1000)
# 檢查響應狀態
response.raise_for_status()
# 解析響應
result = response.json()
# 記錄 API 使用統計
if user_id:
self._record_api_usage(
user_id=user_id,
job_id=job_id,
endpoint=endpoint,
response_data=result,
response_time_ms=response_time_ms,
success=True
)
logger.debug(f"Dify API request successful: {response_time_ms}ms")
return result
except requests.exceptions.RequestException as e:
last_exception = e
response_time_ms = int((time.time() - start_time) * 1000)
# 記錄失敗的 API 調用
if user_id:
self._record_api_usage(
user_id=user_id,
job_id=job_id,
endpoint=endpoint,
response_data={},
response_time_ms=response_time_ms,
success=False,
error_message=str(e)
)
logger.warning(f"Dify API request failed (attempt {attempt + 1}): {str(e)}")
# 如果是最後一次嘗試,拋出異常
if attempt == self.max_retries - 1:
break
# 指數退避
delay = self.retry_delay ** attempt
logger.debug(f"Retrying in {delay} seconds...")
time.sleep(delay)
# 所有重試都失敗了
error_msg = f"Dify API request failed after {self.max_retries} attempts: {str(last_exception)}"
logger.error(error_msg)
raise APIError(error_msg)
def _record_api_usage(self, user_id: int, job_id: Optional[int], endpoint: str,
response_data: Dict, response_time_ms: int, success: bool,
error_message: str = None):
"""記錄 API 使用統計"""
try:
# 從響應中提取使用量資訊
metadata = response_data.get('metadata', {})
APIUsageStats.record_api_call(
user_id=user_id,
job_id=job_id,
api_endpoint=endpoint,
metadata=metadata,
response_time_ms=response_time_ms,
success=success,
error_message=error_message
)
except Exception as e:
logger.warning(f"Failed to record API usage: {str(e)}")
def translate_text(self, text: str, source_language: str, target_language: str,
user_id: int = None, job_id: int = None) -> Dict[str, Any]:
"""翻譯文字"""
if not text.strip():
raise APIError("翻譯文字不能為空")
# 構建請求資料
request_data = {
'inputs': {
'text': text.strip(),
'source_language': source_language,
'target_language': target_language
},
'response_mode': 'blocking',
'user': f"user_{user_id}" if user_id else "anonymous"
}
try:
response = self._make_request(
method='POST',
endpoint='/chat-messages',
data=request_data,
user_id=user_id,
job_id=job_id
)
# 從響應中提取翻譯結果
answer = response.get('answer', '')
if not answer:
raise APIError("Dify API 返回空的翻譯結果")
return {
'success': True,
'translated_text': answer,
'source_text': text,
'source_language': source_language,
'target_language': target_language,
'metadata': response.get('metadata', {})
}
except APIError:
raise
except Exception as e:
error_msg = f"翻譯請求處理錯誤: {str(e)}"
logger.error(error_msg)
raise APIError(error_msg)
def test_connection(self) -> bool:
"""測試 Dify API 連接"""
try:
# 發送簡單的測試請求
test_data = {
'inputs': {'text': 'test'},
'response_mode': 'blocking',
'user': 'health_check'
}
response = self._make_request(
method='POST',
endpoint='/chat-messages',
data=test_data
)
return response is not None
except Exception as e:
logger.error(f"Dify API connection test failed: {str(e)}")
return False
def get_app_info(self) -> Dict[str, Any]:
"""取得 Dify 應用資訊"""
try:
response = self._make_request(
method='GET',
endpoint='/parameters'
)
return {
'success': True,
'app_info': response
}
except Exception as e:
logger.error(f"Failed to get Dify app info: {str(e)}")
return {
'success': False,
'error': str(e)
}
@classmethod
def load_config_from_file(cls, file_path: str = 'api.txt'):
"""從檔案載入 Dify API 配置"""
try:
import os
from pathlib import Path
config_file = Path(file_path)
if not config_file.exists():
logger.warning(f"Dify config file not found: {file_path}")
return
with open(config_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line.startswith('base_url:'):
base_url = line.split(':', 1)[1].strip()
current_app.config['DIFY_API_BASE_URL'] = base_url
elif line.startswith('api:'):
api_key = line.split(':', 1)[1].strip()
current_app.config['DIFY_API_KEY'] = api_key
logger.info("Dify API config loaded from file")
except Exception as e:
logger.error(f"Failed to load Dify config from file: {str(e)}")
def init_dify_config(app):
"""初始化 Dify 配置"""
with app.app_context():
# 從 api.txt 載入配置
DifyClient.load_config_from_file()
# 檢查配置完整性
base_url = app.config.get('DIFY_API_BASE_URL')
api_key = app.config.get('DIFY_API_KEY')
if base_url and api_key:
logger.info("Dify API configuration loaded successfully")
else:
logger.warning("Dify API configuration is incomplete")
logger.warning(f"Base URL: {'' if base_url else ''}")
logger.warning(f"API Key: {'' if api_key else ''}")

View File

@@ -0,0 +1,388 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
通知服務
Author: PANJIT IT Team
Created: 2024-01-28
Modified: 2024-01-28
"""
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime
from typing import Optional, List
from flask import current_app, url_for
from app.utils.logger import get_logger
from app.models.job import TranslationJob
from app.models.user import User
logger = get_logger(__name__)
class NotificationService:
"""通知服務"""
def __init__(self):
self.smtp_server = current_app.config.get('SMTP_SERVER')
self.smtp_port = current_app.config.get('SMTP_PORT', 587)
self.use_tls = current_app.config.get('SMTP_USE_TLS', False)
self.use_ssl = current_app.config.get('SMTP_USE_SSL', False)
self.auth_required = current_app.config.get('SMTP_AUTH_REQUIRED', False)
self.sender_email = current_app.config.get('SMTP_SENDER_EMAIL')
self.sender_password = current_app.config.get('SMTP_SENDER_PASSWORD', '')
self.app_name = current_app.config.get('APP_NAME', 'PANJIT Document Translator')
def _create_smtp_connection(self):
"""建立 SMTP 連線"""
try:
if self.use_ssl:
server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port)
else:
server = smtplib.SMTP(self.smtp_server, self.smtp_port)
if self.use_tls:
server.starttls()
if self.auth_required and self.sender_password:
server.login(self.sender_email, self.sender_password)
return server
except Exception as e:
logger.error(f"SMTP connection failed: {str(e)}")
return None
def _send_email(self, to_email: str, subject: str, html_content: str, text_content: str = None) -> bool:
"""發送郵件的基礎方法"""
try:
if not self.smtp_server or not self.sender_email:
logger.error("SMTP configuration incomplete")
return False
# 建立郵件
msg = MIMEMultipart('alternative')
msg['From'] = f"{self.app_name} <{self.sender_email}>"
msg['To'] = to_email
msg['Subject'] = subject
# 添加文本內容
if text_content:
text_part = MIMEText(text_content, 'plain', 'utf-8')
msg.attach(text_part)
# 添加 HTML 內容
html_part = MIMEText(html_content, 'html', 'utf-8')
msg.attach(html_part)
# 發送郵件
server = self._create_smtp_connection()
if not server:
return False
server.send_message(msg)
server.quit()
logger.info(f"Email sent successfully to {to_email}")
return True
except Exception as e:
logger.error(f"Failed to send email to {to_email}: {str(e)}")
return False
def send_job_completion_notification(self, job: TranslationJob) -> bool:
"""發送任務完成通知"""
try:
if not job.user or not job.user.email:
logger.warning(f"No email address for job {job.job_uuid}")
return False
# 準備郵件內容
subject = f"📄 翻譯完成通知 - {job.original_filename}"
# 計算處理時間
processing_time = ""
if job.processing_started_at and job.completed_at:
duration = job.completed_at - job.processing_started_at
total_seconds = int(duration.total_seconds())
if total_seconds < 60:
processing_time = f"{total_seconds}"
elif total_seconds < 3600:
minutes = total_seconds // 60
seconds = total_seconds % 60
processing_time = f"{minutes}{seconds}"
else:
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
processing_time = f"{hours}小時{minutes}"
# 生成下載連結(簡化版本)
download_links = []
for lang in job.target_languages:
download_links.append(f"{lang}: [下載翻譯檔案]")
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background-color: #2563eb; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }}
.content {{ background-color: #f8fafc; padding: 30px; border: 1px solid #e5e7eb; }}
.info-box {{ background-color: #dbeafe; border-left: 4px solid #2563eb; padding: 15px; margin: 20px 0; }}
.footer {{ background-color: #374151; color: #d1d5db; padding: 15px; text-align: center; font-size: 12px; border-radius: 0 0 8px 8px; }}
.success {{ color: #059669; font-weight: bold; }}
.download-section {{ margin: 20px 0; }}
.download-link {{ display: inline-block; background-color: #2563eb; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; margin: 5px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎉 翻譯任務完成</h1>
</div>
<div class="content">
<p>親愛的 <strong>{job.user.display_name}</strong></p>
<p class="success">您的文件翻譯任務已成功完成!</p>
<div class="info-box">
<h3>📋 任務詳細資訊</h3>
<p><strong>檔案名稱:</strong> {job.original_filename}</p>
<p><strong>任務編號:</strong> {job.job_uuid}</p>
<p><strong>來源語言:</strong> {job.source_language}</p>
<p><strong>目標語言:</strong> {', '.join(job.target_languages)}</p>
<p><strong>處理時間:</strong> {processing_time}</p>
<p><strong>完成時間:</strong> {job.completed_at.strftime('%Y-%m-%d %H:%M:%S') if job.completed_at else '未知'}</p>
{f'<p><strong>總成本:</strong> ${job.total_cost:.4f}</p>' if job.total_cost else ''}
</div>
<div class="download-section">
<h3>📥 下載翻譯檔案</h3>
<p>請登入系統下載您的翻譯檔案:</p>
<p>{'<br>'.join(download_links)}</p>
<p style="margin-top: 15px;">
<strong>注意:</strong> 翻譯檔案將在系統中保留 7 天,請及時下載。
</p>
</div>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
<p>感謝您使用 {self.app_name}</p>
<p>如有任何問題,請聯繫系統管理員。</p>
</div>
</div>
<div class="footer">
<p>此郵件由 {self.app_name} 系統自動發送,請勿回覆。</p>
<p>發送時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
</div>
</body>
</html>
"""
# 純文字版本
text_content = f"""
翻譯任務完成通知
親愛的 {job.user.display_name}
您的文件翻譯任務已成功完成!
任務詳細資訊:
- 檔案名稱: {job.original_filename}
- 任務編號: {job.job_uuid}
- 來源語言: {job.source_language}
- 目標語言: {', '.join(job.target_languages)}
- 處理時間: {processing_time}
- 完成時間: {job.completed_at.strftime('%Y-%m-%d %H:%M:%S') if job.completed_at else '未知'}
請登入系統下載您的翻譯檔案。翻譯檔案將在系統中保留 7 天。
感謝您使用 {self.app_name}
----
此郵件由系統自動發送,請勿回覆。
"""
return self._send_email(job.user.email, subject, html_content, text_content)
except Exception as e:
logger.error(f"Failed to send completion notification for job {job.job_uuid}: {str(e)}")
return False
def send_job_failure_notification(self, job: TranslationJob) -> bool:
"""發送任務失敗通知"""
try:
if not job.user or not job.user.email:
logger.warning(f"No email address for job {job.job_uuid}")
return False
subject = f"⚠️ 翻譯失敗通知 - {job.original_filename}"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background-color: #dc2626; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }}
.content {{ background-color: #f8fafc; padding: 30px; border: 1px solid #e5e7eb; }}
.error-box {{ background-color: #fef2f2; border-left: 4px solid #dc2626; padding: 15px; margin: 20px 0; }}
.footer {{ background-color: #374151; color: #d1d5db; padding: 15px; text-align: center; font-size: 12px; border-radius: 0 0 8px 8px; }}
.error {{ color: #dc2626; font-weight: bold; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>❌ 翻譯任務失敗</h1>
</div>
<div class="content">
<p>親愛的 <strong>{job.user.display_name}</strong></p>
<p class="error">很抱歉,您的文件翻譯任務處理失敗。</p>
<div class="error-box">
<h3>📋 任務資訊</h3>
<p><strong>檔案名稱:</strong> {job.original_filename}</p>
<p><strong>任務編號:</strong> {job.job_uuid}</p>
<p><strong>重試次數:</strong> {job.retry_count}</p>
<p><strong>錯誤訊息:</strong> {job.error_message or '未知錯誤'}</p>
<p><strong>失敗時間:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
<div style="margin-top: 20px;">
<p><strong>建議處理方式:</strong></p>
<ul>
<li>檢查檔案格式是否正確</li>
<li>確認檔案沒有損壞</li>
<li>稍後再次嘗試上傳</li>
<li>如問題持續,請聯繫系統管理員</li>
</ul>
</div>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
<p>如需協助,請聯繫系統管理員。</p>
</div>
</div>
<div class="footer">
<p>此郵件由 {self.app_name} 系統自動發送,請勿回覆。</p>
<p>發送時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
</div>
</body>
</html>
"""
text_content = f"""
翻譯任務失敗通知
親愛的 {job.user.display_name}
很抱歉,您的文件翻譯任務處理失敗。
任務資訊:
- 檔案名稱: {job.original_filename}
- 任務編號: {job.job_uuid}
- 重試次數: {job.retry_count}
- 錯誤訊息: {job.error_message or '未知錯誤'}
建議處理方式:
1. 檢查檔案格式是否正確
2. 確認檔案沒有損壞
3. 稍後再次嘗試上傳
4. 如問題持續,請聯繫系統管理員
如需協助,請聯繫系統管理員。
----
此郵件由 {self.app_name} 系統自動發送,請勿回覆。
"""
return self._send_email(job.user.email, subject, html_content, text_content)
except Exception as e:
logger.error(f"Failed to send failure notification for job {job.job_uuid}: {str(e)}")
return False
def send_admin_notification(self, subject: str, message: str, admin_emails: List[str] = None) -> bool:
"""發送管理員通知"""
try:
if not admin_emails:
# 取得所有管理員郵件地址
admin_users = User.get_admin_users()
admin_emails = [user.email for user in admin_users if user.email]
if not admin_emails:
logger.warning("No admin email addresses found")
return False
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background-color: #f59e0b; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }}
.content {{ background-color: #f8fafc; padding: 30px; border: 1px solid #e5e7eb; }}
.footer {{ background-color: #374151; color: #d1d5db; padding: 15px; text-align: center; font-size: 12px; border-radius: 0 0 8px 8px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔔 系統管理通知</h1>
</div>
<div class="content">
<p>系統管理員您好,</p>
<div style="background-color: #fef3c7; border-left: 4px solid #f59e0b; padding: 15px; margin: 20px 0;">
<h3>{subject}</h3>
<p>{message}</p>
</div>
<p>發送時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
<div class="footer">
<p>此郵件由 {self.app_name} 系統自動發送,請勿回覆。</p>
</div>
</div>
</body>
</html>
"""
success_count = 0
for email in admin_emails:
if self._send_email(email, f"[管理通知] {subject}", html_content):
success_count += 1
return success_count > 0
except Exception as e:
logger.error(f"Failed to send admin notification: {str(e)}")
return False
def test_smtp_connection(self) -> bool:
"""測試 SMTP 連線"""
try:
server = self._create_smtp_connection()
if server:
server.quit()
return True
return False
except Exception as e:
logger.error(f"SMTP connection test failed: {str(e)}")
return False

View File

@@ -0,0 +1,424 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
翻譯服務
Author: PANJIT IT Team
Created: 2024-01-28
Modified: 2024-01-28
"""
import hashlib
import time
from pathlib import Path
from typing import List, Dict, Any, Optional
from app.utils.logger import get_logger
from app.utils.exceptions import TranslationError, FileProcessingError
from app.services.dify_client import DifyClient
from app.models.cache import TranslationCache
from app.models.job import TranslationJob
from app.utils.helpers import generate_filename, create_job_directory
logger = get_logger(__name__)
class DocumentParser:
"""文件解析器基類"""
def __init__(self, file_path: str):
self.file_path = Path(file_path)
if not self.file_path.exists():
raise FileProcessingError(f"檔案不存在: {file_path}")
def extract_text_segments(self) -> List[str]:
"""提取文字片段"""
raise NotImplementedError
def generate_translated_document(self, translations: Dict[str, List[str]],
target_language: str, output_dir: Path) -> str:
"""生成翻譯後的文件"""
raise NotImplementedError
class DocxParser(DocumentParser):
"""DOCX 文件解析器"""
def extract_text_segments(self) -> List[str]:
"""提取 DOCX 文件的文字片段"""
try:
import docx
from docx.table import _Cell
doc = docx.Document(str(self.file_path))
text_segments = []
# 提取段落文字
for paragraph in doc.paragraphs:
text = paragraph.text.strip()
if text and len(text) > 3: # 過濾太短的文字
text_segments.append(text)
# 提取表格文字
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
text = cell.text.strip()
if text and len(text) > 3:
text_segments.append(text)
logger.info(f"Extracted {len(text_segments)} text segments from DOCX")
return text_segments
except Exception as e:
logger.error(f"Failed to extract text from DOCX: {str(e)}")
raise FileProcessingError(f"DOCX 文件解析失敗: {str(e)}")
def generate_translated_document(self, translations: Dict[str, List[str]],
target_language: str, output_dir: Path) -> str:
"""生成翻譯後的 DOCX 文件"""
try:
import docx
from docx.shared import Pt
# 開啟原始文件
doc = docx.Document(str(self.file_path))
# 取得對應的翻譯
translated_texts = translations.get(target_language, [])
text_index = 0
# 處理段落
for paragraph in doc.paragraphs:
if paragraph.text.strip() and len(paragraph.text.strip()) > 3:
if text_index < len(translated_texts):
# 保留原文,添加翻譯
original_text = paragraph.text
translated_text = translated_texts[text_index]
# 清空段落
paragraph.clear()
# 添加原文
run = paragraph.add_run(original_text)
# 添加翻譯(新行,較小字體)
paragraph.add_run('\n')
trans_run = paragraph.add_run(translated_text)
trans_run.font.size = Pt(10)
trans_run.italic = True
text_index += 1
# 處理表格(簡化版本)
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
if cell.text.strip() and len(cell.text.strip()) > 3:
if text_index < len(translated_texts):
original_text = cell.text
translated_text = translated_texts[text_index]
# 清空儲存格
cell.text = f"{original_text}\n{translated_text}"
text_index += 1
# 生成輸出檔名
output_filename = generate_filename(
self.file_path.name,
'translated',
'translated',
target_language
)
output_path = output_dir / output_filename
# 儲存文件
doc.save(str(output_path))
logger.info(f"Generated translated DOCX: {output_path}")
return str(output_path)
except Exception as e:
logger.error(f"Failed to generate translated DOCX: {str(e)}")
raise FileProcessingError(f"生成翻譯 DOCX 失敗: {str(e)}")
class PdfParser(DocumentParser):
"""PDF 文件解析器(只讀)"""
def extract_text_segments(self) -> List[str]:
"""提取 PDF 文件的文字片段"""
try:
from PyPDF2 import PdfReader
reader = PdfReader(str(self.file_path))
text_segments = []
for page in reader.pages:
text = page.extract_text()
# 簡單的句子分割
sentences = text.split('.')
for sentence in sentences:
sentence = sentence.strip()
if sentence and len(sentence) > 10:
text_segments.append(sentence)
logger.info(f"Extracted {len(text_segments)} text segments from PDF")
return text_segments
except Exception as e:
logger.error(f"Failed to extract text from PDF: {str(e)}")
raise FileProcessingError(f"PDF 文件解析失敗: {str(e)}")
def generate_translated_document(self, translations: Dict[str, List[str]],
target_language: str, output_dir: Path) -> str:
"""生成翻譯文字檔PDF 不支援直接編輯)"""
try:
translated_texts = translations.get(target_language, [])
# 生成純文字檔案
output_filename = f"{self.file_path.stem}_{target_language}_translated.txt"
output_path = output_dir / output_filename
with open(output_path, 'w', encoding='utf-8') as f:
f.write(f"翻譯結果 - {target_language}\n")
f.write("=" * 50 + "\n\n")
for i, text in enumerate(translated_texts):
f.write(f"{i+1}. {text}\n\n")
logger.info(f"Generated translated text file: {output_path}")
return str(output_path)
except Exception as e:
logger.error(f"Failed to generate translated text file: {str(e)}")
raise FileProcessingError(f"生成翻譯文字檔失敗: {str(e)}")
class TranslationService:
"""翻譯服務"""
def __init__(self):
self.dify_client = DifyClient()
# 文件解析器映射
self.parsers = {
'.docx': DocxParser,
'.doc': DocxParser, # 假設可以用 docx 處理
'.pdf': PdfParser,
# 其他格式可以稍後添加
}
def get_document_parser(self, file_path: str) -> DocumentParser:
"""取得文件解析器"""
file_ext = Path(file_path).suffix.lower()
parser_class = self.parsers.get(file_ext)
if not parser_class:
raise FileProcessingError(f"不支援的檔案格式: {file_ext}")
return parser_class(file_path)
def split_text_into_sentences(self, text: str, language: str = 'auto') -> List[str]:
"""將文字分割成句子"""
# 這裡可以使用更智能的句子分割
# 暫時使用簡單的分割方式
sentences = []
# 基本的句子分割符號
separators = ['. ', '', '', '', '!', '?']
current_text = text
for sep in separators:
parts = current_text.split(sep)
if len(parts) > 1:
sentences.extend([part.strip() + sep.rstrip() for part in parts[:-1] if part.strip()])
current_text = parts[-1]
# 添加最後一部分
if current_text.strip():
sentences.append(current_text.strip())
# 過濾太短的句子
sentences = [s for s in sentences if len(s.strip()) > 5]
return sentences
def translate_text_with_cache(self, text: str, source_language: str,
target_language: str, user_id: int = None,
job_id: int = None) -> str:
"""帶快取的文字翻譯"""
# 檢查快取
cached_translation = TranslationCache.get_translation(
text, source_language, target_language
)
if cached_translation:
logger.debug(f"Cache hit for translation: {text[:50]}...")
return cached_translation
# 呼叫 Dify API
try:
result = self.dify_client.translate_text(
text=text,
source_language=source_language,
target_language=target_language,
user_id=user_id,
job_id=job_id
)
translated_text = result['translated_text']
# 儲存到快取
TranslationCache.save_translation(
text, source_language, target_language, translated_text
)
return translated_text
except Exception as e:
logger.error(f"Translation failed for text: {text[:50]}... Error: {str(e)}")
raise TranslationError(f"翻譯失敗: {str(e)}")
def translate_document(self, job_uuid: str) -> Dict[str, Any]:
"""翻譯文件(主要入口點)"""
try:
# 取得任務資訊
job = TranslationJob.query.filter_by(job_uuid=job_uuid).first()
if not job:
raise TranslationError(f"找不到任務: {job_uuid}")
logger.info(f"Starting document translation: {job_uuid}")
# 更新任務狀態
job.update_status('PROCESSING', progress=0)
# 取得文件解析器
parser = self.get_document_parser(job.file_path)
# 提取文字片段
logger.info("Extracting text segments from document")
text_segments = parser.extract_text_segments()
if not text_segments:
raise TranslationError("文件中未找到可翻譯的文字")
# 分割成句子
logger.info("Splitting text into sentences")
all_sentences = []
for segment in text_segments:
sentences = self.split_text_into_sentences(segment, job.source_language)
all_sentences.extend(sentences)
# 去重複
unique_sentences = list(dict.fromkeys(all_sentences)) # 保持順序的去重
logger.info(f"Found {len(unique_sentences)} unique sentences to translate")
# 批次翻譯
translation_results = {}
total_sentences = len(unique_sentences)
for target_language in job.target_languages:
logger.info(f"Translating to {target_language}")
translated_sentences = []
for i, sentence in enumerate(unique_sentences):
try:
translated = self.translate_text_with_cache(
text=sentence,
source_language=job.source_language,
target_language=target_language,
user_id=job.user_id,
job_id=job.id
)
translated_sentences.append(translated)
# 更新進度
progress = (i + 1) / total_sentences * 100 / len(job.target_languages)
current_lang_index = job.target_languages.index(target_language)
total_progress = (current_lang_index * 100 + progress) / len(job.target_languages)
job.update_status('PROCESSING', progress=total_progress)
# 短暫延遲避免過快請求
time.sleep(0.1)
except Exception as e:
logger.error(f"Failed to translate sentence: {sentence[:50]}... Error: {str(e)}")
# 翻譯失敗時保留原文
translated_sentences.append(f"[翻譯失敗] {sentence}")
translation_results[target_language] = translated_sentences
# 生成翻譯文件
logger.info("Generating translated documents")
output_dir = Path(job.file_path).parent
output_files = {}
for target_language, translations in translation_results.items():
try:
# 重建翻譯映射
translation_mapping = {target_language: translations}
output_file = parser.generate_translated_document(
translations=translation_mapping,
target_language=target_language,
output_dir=output_dir
)
output_files[target_language] = output_file
# 記錄翻譯檔案到資料庫
file_size = Path(output_file).stat().st_size
job.add_translated_file(
language_code=target_language,
filename=Path(output_file).name,
file_path=output_file,
file_size=file_size
)
except Exception as e:
logger.error(f"Failed to generate translated document for {target_language}: {str(e)}")
raise TranslationError(f"生成 {target_language} 翻譯文件失敗: {str(e)}")
# 計算總成本(從 API 使用統計中取得)
total_cost = self._calculate_job_cost(job.id)
# 更新任務狀態為完成
job.update_status('COMPLETED', progress=100)
job.total_cost = total_cost
job.total_tokens = len(unique_sentences) # 簡化的 token 計算
from app import db
db.session.commit()
logger.info(f"Document translation completed: {job_uuid}")
return {
'success': True,
'job_uuid': job_uuid,
'output_files': output_files,
'total_sentences': len(unique_sentences),
'total_cost': float(total_cost),
'target_languages': job.target_languages
}
except TranslationError:
raise
except Exception as e:
logger.error(f"Document translation failed: {job_uuid}. Error: {str(e)}")
raise TranslationError(f"文件翻譯失敗: {str(e)}")
def _calculate_job_cost(self, job_id: int) -> float:
"""計算任務總成本"""
from app import db
from sqlalchemy import func
total_cost = db.session.query(
func.sum(APIUsageStats.cost)
).filter_by(job_id=job_id).scalar()
return float(total_cost) if total_cost else 0.0