Files
pdf-translate-web/services.py
coder be1e01a8c6 初始提交: PDF翻译网站
功能:
- 上传PDF自动翻译成中文
- 不登录可用(次数限制)
- 登录后获得更多次数
- 会员等级制度(不同权益)
- 智能缓存(相同文件秒出结果)
- 原文译文对比查看
- 支持重新翻译(自定义要求)
- 翻译历史记录

技术栈: Flask + SQLAlchemy + Bootstrap5
2026-04-07 19:01:14 +08:00

316 lines
9.9 KiB
Python

"""
PDF翻译服务模块
"""
import os
import json
import time
import hashlib
import threading
from datetime import datetime, timedelta
from pypdf import PdfReader
from openai import OpenAI
from flask import current_app
# ==================== LLM客户端 ====================
class TranslationService:
"""翻译服务"""
def __init__(self, config):
self.config = config
self.llm_config = config['LLM_CONFIG']
self.client = OpenAI(
api_key=self.llm_config['api_key'],
base_url=self.llm_config['api_base'],
)
def translate_text(self, text, instruction=None):
"""
翻译文本
Args:
text: 待翻译文本
instruction: 用户自定义翻译要求
Returns:
翻译后的文本
"""
system_prompt = """你是一个专业的英译中翻译专家。请遵循以下规则:
1. 保持原文的格式和段落结构
2. 专业术语保持准确性,必要时保留英文原文
3. 语言流畅自然,符合中文表达习惯
4. 不要添加任何解释或注释,只输出翻译结果"""
user_prompt = f"""请将以下英文翻译成中文。直接输出中文翻译,不要解释。
英文内容:
{text}"""
if instruction:
user_prompt = f"""请将以下英文翻译成中文。
用户翻译要求:{instruction}
英文内容:
{text}"""
try:
response = self.client.chat.completions.create(
model=self.llm_config['model'],
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
max_tokens=self.llm_config['max_tokens'],
temperature=0.3,
timeout=self.llm_config['timeout'],
)
content = response.choices[0].message.content
if content and content.strip():
return content.strip()
return text
except Exception as e:
print(f"翻译错误: {e}")
return text
def extract_pdf_text(self, pdf_path):
"""提取PDF文本"""
reader = PdfReader(pdf_path)
pages = []
for i, page in enumerate(reader.pages):
text = page.extract_text()
if text.strip():
# 清理文本
text = self._clean_text(text)
pages.append({
'page': i + 1,
'text': text
})
return pages
def _clean_text(self, text):
"""清理文本"""
import re
text = re.sub(r'\n{3,}', '\n\n', text)
text = re.sub(r' {2,}', ' ', text)
text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', text)
return text.strip()
def chunk_text(self, text, max_size=2000):
"""分块"""
paragraphs = text.split('\n\n')
chunks = []
current = ""
for para in paragraphs:
if len(current) + len(para) < max_size:
current += para + '\n\n'
else:
if current:
chunks.append(current.strip())
current = para + '\n\n'
if current:
chunks.append(current.strip())
return chunks
def translate_pdf(self, pdf_path, output_path, instruction=None, progress_callback=None):
"""
翻译PDF
Args:
pdf_path: 输入PDF路径
output_path: 输出路径
instruction: 用户翻译要求
progress_callback: 进度回调函数
Returns:
翻译统计信息
"""
pages = self.extract_pdf_text(pdf_path)
total_pages = len(pages)
if progress_callback:
progress_callback(0, total_pages, "开始翻译...")
translated_pages = []
total_chunks = 0
for page_data in pages:
chunks = self.chunk_text(page_data['text'], self.llm_config['chunk_size'])
total_chunks += len(chunks)
translated_chunks = []
for i, chunk in enumerate(chunks):
translated = self.translate_text(chunk, instruction)
translated_chunks.append(translated)
if progress_callback:
progress = int((i + 1) / len(chunks) * 100 / total_pages)
progress_callback(progress, total_pages, f"翻译第{page_data['page']}")
translated_pages.append({
'page': page_data['page'],
'original': page_data['text'],
'translated': '\n\n'.join(translated_chunks)
})
# 保存结果
self._save_output(translated_pages, output_path)
if progress_callback:
progress_callback(100, total_pages, "翻译完成")
return {
'total_pages': total_pages,
'total_chunks': total_chunks,
'output_path': output_path
}
def _save_output(self, pages, output_path):
"""保存翻译结果"""
content = "# 英文PDF中文翻译\n\n> 自动翻译生成\n\n---\n\n"
for page in pages:
content += f"## 第 {page['page']}\n\n"
content += page['translated'] + "\n\n---\n\n"
with open(output_path, 'w', encoding='utf-8') as f:
f.write(content)
def save_comparison(self, pages, output_path):
"""保存对比文件(原文+译文)"""
content = "# 英文PDF翻译对比\n\n---\n\n"
for page in pages:
content += f"## 第 {page['page']}\n\n"
content += "### 原文\n\n```\n" + page['original'] + "\n```\n\n"
content += "### 译文\n\n" + page['translated'] + "\n\n---\n\n"
with open(output_path, 'w', encoding='utf-8') as f:
f.write(content)
# ==================== 缓存服务 ====================
class CacheService:
"""翻译缓存服务"""
def __init__(self, cache_dir, expire_days=30):
self.cache_dir = cache_dir
self.expire_days = expire_days
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
def compute_hash(self, file_content):
"""计算文件哈希"""
return hashlib.md5(file_content).hexdigest()
def get_cache(self, file_hash, db_model=None):
"""
获取缓存
Returns:
缓存路径或None
"""
cache_file = os.path.join(self.cache_dir, f"{file_hash}.md")
if os.path.exists(cache_file):
# 检查过期
file_time = datetime.fromtimestamp(os.path.getmtime(cache_file))
if datetime.now() - file_time > timedelta(days=self.expire_days):
os.remove(cache_file)
return None
# 更新命中计数
if db_model:
cache_record = db_model.query.filter_by(file_hash=file_hash).first()
if cache_record:
cache_record.increment_hit()
return cache_file
return None
def save_cache(self, file_hash, content):
"""保存缓存"""
cache_file = os.path.join(self.cache_dir, f"{file_hash}.md")
with open(cache_file, 'w', encoding='utf-8') as f:
f.write(content)
return cache_file
def check_cache_exists(self, file_hash):
"""检查缓存是否存在"""
cache_file = os.path.join(self.cache_dir, f"{file_hash}.md")
return os.path.exists(cache_file)
# ==================== 异步翻译任务 ====================
class TranslationTask:
"""异步翻译任务"""
tasks = {} # 任务存储
lock = threading.Lock()
@classmethod
def create_task(cls, task_id, pdf_path, output_path, config, instruction=None):
"""创建翻译任务"""
task = {
'id': task_id,
'status': 'pending',
'progress': 0,
'message': '等待开始',
'output_path': output_path,
'error': None,
'started_at': None,
'completed_at': None,
}
with cls.lock:
cls.tasks[task_id] = task
# 启动翻译线程
def run_translation():
service = TranslationService(config)
task['status'] = 'processing'
task['started_at'] = datetime.now().isoformat()
def progress_callback(progress, total, message):
with cls.lock:
task['progress'] = progress
task['message'] = message
try:
result = service.translate_pdf(
pdf_path, output_path, instruction, progress_callback
)
task['status'] = 'completed'
task['progress'] = 100
task['message'] = '翻译完成'
task['completed_at'] = datetime.now().isoformat()
task['result'] = result
except Exception as e:
task['status'] = 'failed'
task['error'] = str(e)
task['message'] = f'翻译失败: {e}'
thread = threading.Thread(target=run_translation)
thread.start()
return task_id
@classmethod
def get_task(cls, task_id):
"""获取任务状态"""
with cls.lock:
return cls.tasks.get(task_id)
@classmethod
def update_task(cls, task_id, **kwargs):
"""更新任务"""
with cls.lock:
if task_id in cls.tasks:
cls.tasks[task_id].update(kwargs)