Files
epub_bilingual_translator/archive/v0.02/src/translator.py
T
2026-01-19 09:51:07 +08:00

143 lines
5.7 KiB
Python

"""
EPUB 翻译器核心模块 (EPUB Translator Core Module) - Manifest 驱动版
该模块协调整体流程:
1. 使用 ManifestManager 管理状态。
2. 调用 EPUBParser 提取。
3. 调用 TextProcessor 清理。
4. 调用 LLMClient 并发翻译并更新 Manifest。
5. 调用 BilingualEPUBBuilder 构建。
"""
import asyncio
import os
from typing import List, Dict, Any
from pathlib import Path
from loguru import logger
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
from .epub_parser import EPUBParser
from .llm_client import OpenRouterClient
from .text_processor import TextProcessor
from .bilingual_builder import BilingualEPUBBuilder
from .manifest_manager import ManifestManager
class EPUBTranslator:
"""
基于 Manifest 的翻译器。
"""
def __init__(self, config: Dict, use_cache: bool = True):
self.config = config
self.console = Console()
self.use_cache = use_cache
# 组件
self.parser = None
self.llm_client = OpenRouterClient(config)
self.text_processor = TextProcessor(config)
# Manifest 管理 (存放于 cache/manifests/ 目录下)
self.manifest_dir = Path("cache/manifests")
self.manifest_dir.mkdir(parents=True, exist_ok=True)
async def translate_epub(self, epub_path: str, test_mode: bool = False, output_dir: str = None) -> str:
"""主翻译流程。"""
epub_path = Path(epub_path)
# 1. 初始化解析器
self.parser = EPUBParser(str(epub_path))
# 2. 准备 Manifest
manifest_path = self.manifest_dir / f"{epub_path.stem}_manifest.json"
manifest = ManifestManager(str(manifest_path))
# 检查是否能恢复
if not manifest.load() or not self.use_cache:
self.console.print("[yellow]初始化翻译清单...[/yellow]")
manifest.init_manifest(book_id=epub_path.name, metadata=self.parser.get_book_info())
# 提取内容
content_items = self.parser.extract_all_content_items()
for item in content_items:
self.text_processor.extract_to_manifest(item['content'], item['file_name'], manifest)
manifest.save()
stats = manifest.stats
self.console.print(f"[green]已加载清单: {stats['total']} 个段落, 已完成 {stats['progress_percent']}%[/green]")
if test_mode:
# 简化逻辑:测试模式只翻译前几个 pending 项目
pending = manifest.get_items(status="pending")[:5]
if pending:
results = await self.llm_client.translate_chunk(pending)
for pid, trans in results.items():
self.console.print(f"\n[cyan]{pid}[/cyan]: {trans}")
return "test_mode_done"
# 3. 分块并并发翻译
chunks = self.text_processor.create_chunks_from_manifest(manifest)
if chunks:
await self._translate_concurrently(chunks, manifest)
# 4. 构建双语 EPUB
self.console.print("\n[yellow]正在构建双语 EPUB...[/yellow]")
output_path = output_dir or self.config['output']['output_dir']
builder = BilingualEPUBBuilder(self.parser.book, self.config)
# 注意:Builder 现在直接从 Manifest 中读取翻译映射
translation_map = {item.global_id: item.translation for item in manifest.get_items() if item.translation}
paragraph_map = {item.global_id: {
"file_name": item.source_file,
"text": item.clean_text,
"html_element": item.original_html
} for item in manifest.get_items()}
result_file = builder.create_bilingual_epub_with_mapping(
translation_map,
paragraph_map,
output_path
)
self.console.print(f"[green]✅ 翻译完成!输出文件: {result_file}[/green]")
return result_file
async def _translate_concurrently(self, chunks: List[List[Any]], manifest: ManifestManager):
"""执行并发翻译任务。"""
total_chunks = len(chunks)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TimeElapsedColumn(),
console=self.console
) as progress:
task_id = progress.add_task(f"[cyan]并行翻译...", total=total_chunks)
# 使用可控并发
semaphore = self.llm_client.rate_limiter.semaphore
async def worker(chunk, idx):
async with semaphore:
try:
results = await self.llm_client.translate_chunk(chunk)
# 更新 manifest
for item in chunk:
if item.global_id in results:
manifest.update_item(item.global_id, results[item.global_id])
else:
manifest.update_item(item.global_id, None, status="failed", error="Missing in response")
# 每翻译完一个 chunk 就保存一次,确保断点续传
manifest.save()
except Exception as e:
logger.error(f"Chunk {idx} 翻译失败: {e}")
finally:
progress.update(task_id, advance=1)
tasks = [worker(chunk, i) for i, chunk in enumerate(chunks)]
await asyncio.gather(*tasks)