Files
Document_Translator/app/tasks/translation.py
2025-09-04 10:21:16 +08:00

351 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
翻譯相關 Celery 任務
Author: PANJIT IT Team
Created: 2024-01-28
Modified: 2024-01-28
"""
import os
import shutil
from datetime import datetime, timedelta
from pathlib import Path
from celery import Celery, current_task
from celery.schedules import crontab
from app import create_app, db
logger = None
def get_celery_instance():
"""取得 Celery 實例"""
app = create_app()
return app.celery
# 建立 Celery 實例
celery = get_celery_instance()
# 初始化 logger
from app.utils.logger import get_logger
logger = get_logger(__name__)
from app.models.job import TranslationJob
from app.models.log import SystemLog
from app.services.translation_service import TranslationService
from app.services.notification_service import NotificationService
from app.utils.exceptions import TranslationError
@celery.task(bind=True, max_retries=3)
def process_translation_job(self, job_id: int):
"""處理翻譯任務"""
app = create_app()
with app.app_context():
try:
# 取得任務資訊
job = TranslationJob.query.get(job_id)
if not job:
raise ValueError(f"Job {job_id} not found")
logger.info(f"Starting translation job processing: {job.job_uuid}")
# 記錄任務開始
SystemLog.info(
'tasks.translation',
f'Translation job started: {job.job_uuid}',
user_id=job.user_id,
job_id=job.id,
extra_data={
'filename': job.original_filename,
'target_languages': job.target_languages,
'retry_count': self.request.retries
}
)
# 建立翻譯服務
translation_service = TranslationService()
# 執行翻譯
result = translation_service.translate_document(job.job_uuid)
if result['success']:
logger.info(f"Translation job completed successfully: {job.job_uuid}")
# 重新獲取任務以確保狀態是最新的
db.session.refresh(job)
# 發送完成通知
try:
notification_service = NotificationService()
# 發送郵件通知
notification_service.send_job_completion_notification(job)
# 發送資料庫通知 - 跳過狀態檢查,直接發送
notification_service.send_job_completion_db_notification_direct(job)
except Exception as e:
logger.warning(f"Failed to send completion notification: {str(e)}")
# 記錄完成日誌
SystemLog.info(
'tasks.translation',
f'Translation job completed: {job.job_uuid}',
user_id=job.user_id,
job_id=job.id,
extra_data={
'total_cost': result.get('total_cost', 0),
'total_sentences': result.get('total_sentences', 0),
'output_files': list(result.get('output_files', {}).keys())
}
)
else:
raise TranslationError(result.get('error', 'Unknown translation error'))
except Exception as exc:
logger.error(f"Translation job failed: {job.job_uuid}. Error: {str(exc)}")
with app.app_context():
# 更新任務狀態
job = TranslationJob.query.get(job_id)
if job:
job.error_message = str(exc)
job.retry_count = self.request.retries + 1
if self.request.retries < self.max_retries:
# 準備重試
job.update_status('RETRY')
# 計算重試延遲30s, 60s, 120s
countdown = [30, 60, 120][self.request.retries]
SystemLog.warning(
'tasks.translation',
f'Translation job retry scheduled: {job.job_uuid} (attempt {self.request.retries + 2})',
user_id=job.user_id,
job_id=job.id,
extra_data={
'error': str(exc),
'retry_count': self.request.retries + 1,
'countdown': countdown
}
)
logger.info(f"Retrying translation job in {countdown}s: {job.job_uuid}")
raise self.retry(exc=exc, countdown=countdown)
else:
# 重試次數用盡,標記失敗
job.update_status('FAILED')
# 發送失敗通知
try:
notification_service = NotificationService()
# 發送郵件通知
notification_service.send_job_failure_notification(job)
# 發送資料庫通知
notification_service.send_job_failure_db_notification(job, str(exc))
except Exception as e:
logger.warning(f"Failed to send failure notification: {str(e)}")
SystemLog.error(
'tasks.translation',
f'Translation job failed permanently: {job.job_uuid}',
user_id=job.user_id,
job_id=job.id,
extra_data={
'error': str(exc),
'total_retries': self.request.retries
}
)
# 發送失敗通知
try:
notification_service = NotificationService()
notification_service.send_job_failure_notification(job)
except Exception as e:
logger.warning(f"Failed to send failure notification: {str(e)}")
logger.error(f"Translation job failed permanently: {job.job_uuid}")
raise exc
@celery.task
def cleanup_old_files():
"""清理舊檔案(定期任務)"""
app = create_app()
with app.app_context():
try:
logger.info("Starting file cleanup task")
upload_folder = Path(app.config.get('UPLOAD_FOLDER'))
retention_days = app.config.get('FILE_RETENTION_DAYS', 7)
cutoff_date = datetime.utcnow() - timedelta(days=retention_days)
if not upload_folder.exists():
logger.warning(f"Upload folder does not exist: {upload_folder}")
return
deleted_files = 0
deleted_dirs = 0
total_size_freed = 0
# 遍歷上傳目錄中的所有 UUID 目錄
for item in upload_folder.iterdir():
if not item.is_dir():
continue
try:
# 檢查目錄的修改時間
dir_mtime = datetime.fromtimestamp(item.stat().st_mtime)
if dir_mtime < cutoff_date:
# 計算目錄大小
dir_size = sum(f.stat().st_size for f in item.rglob('*') if f.is_file())
# 檢查是否還有相關的資料庫記錄
job_uuid = item.name
job = TranslationJob.query.filter_by(job_uuid=job_uuid).first()
if job:
# 檢查任務是否已完成且超過保留期
if job.completed_at and job.completed_at < cutoff_date:
# 刪除目錄
shutil.rmtree(item)
deleted_dirs += 1
total_size_freed += dir_size
logger.info(f"Cleaned up job directory: {job_uuid}")
# 記錄清理日誌
SystemLog.info(
'tasks.cleanup',
f'Cleaned up files for completed job: {job_uuid}',
user_id=job.user_id,
job_id=job.id,
extra_data={
'files_size_mb': dir_size / (1024 * 1024),
'retention_days': retention_days
}
)
else:
# 沒有對應的資料庫記錄,直接刪除
shutil.rmtree(item)
deleted_dirs += 1
total_size_freed += dir_size
logger.info(f"Cleaned up orphaned directory: {job_uuid}")
except Exception as e:
logger.error(f"Failed to process directory {item}: {str(e)}")
continue
# 記錄清理結果
cleanup_result = {
'deleted_directories': deleted_dirs,
'total_size_freed_mb': total_size_freed / (1024 * 1024),
'retention_days': retention_days,
'cutoff_date': cutoff_date.isoformat()
}
SystemLog.info(
'tasks.cleanup',
f'File cleanup completed: {deleted_dirs} directories, {total_size_freed / (1024 * 1024):.2f} MB freed',
extra_data=cleanup_result
)
logger.info(f"File cleanup completed: {cleanup_result}")
return cleanup_result
except Exception as e:
logger.error(f"File cleanup task failed: {str(e)}")
SystemLog.error(
'tasks.cleanup',
f'File cleanup task failed: {str(e)}',
extra_data={'error': str(e)}
)
raise e
@celery.task
def send_daily_admin_report():
"""發送每日管理員報告"""
app = create_app()
with app.app_context():
try:
logger.info("Generating daily admin report")
from app.models.stats import APIUsageStats
from app.services.notification_service import NotificationService
# 取得昨日統計
yesterday = datetime.utcnow() - timedelta(days=1)
daily_stats = APIUsageStats.get_daily_statistics(days=1)
# 取得系統錯誤摘要
error_summary = SystemLog.get_error_summary(days=1)
# 準備報告內容
if daily_stats:
yesterday_data = daily_stats[0]
subject = f"每日系統報告 - {yesterday_data['date']}"
message = f"""
昨日系統使用狀況:
• 翻譯任務: {yesterday_data['total_calls']}
• 成功任務: {yesterday_data['successful_calls']}
• 失敗任務: {yesterday_data['failed_calls']}
• 總成本: ${yesterday_data['total_cost']:.4f}
• 總 Token 數: {yesterday_data['total_tokens']}
系統錯誤摘要:
• 錯誤數量: {error_summary['total_errors']}
請查看管理後台了解詳細資訊。
"""
else:
subject = f"每日系統報告 - {yesterday.strftime('%Y-%m-%d')}"
message = "昨日無翻譯任務記錄。"
# 發送管理員通知
notification_service = NotificationService()
result = notification_service.send_admin_notification(subject, message)
if result:
logger.info("Daily admin report sent successfully")
else:
logger.warning("Failed to send daily admin report")
return result
except Exception as e:
logger.error(f"Daily admin report task failed: {str(e)}")
raise e
# 定期任務設定
@celery.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
"""設定定期任務"""
# 每日凌晨 2 點執行檔案清理
sender.add_periodic_task(
crontab(hour=2, minute=0),
cleanup_old_files.s(),
name='cleanup-old-files-daily'
)
# 每日早上 8 點發送管理員報告
sender.add_periodic_task(
crontab(hour=8, minute=0),
send_daily_admin_report.s(),
name='daily-admin-report'
)