143 lines
5.7 KiB
Python
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)
|