Initial commit
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user