""" 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)