""" Manifest 管理器模块 (Manifest Manager Module) 该模块是系统的单一真理源 (SSOT)。 它记录了每一段文本的原始状态、清洗后的文本、哈希值以及翻译状态。 所有对翻译流程的操作(提取、翻译、回填)都必须通过修改此 Manifest 进行。 """ import json import os import hashlib from typing import List, Dict, Optional, Any from pathlib import Path from loguru import logger from dataclasses import dataclass, asdict, field @dataclass class ManifestItem: """代表一个翻译单元(通常是一个段落)""" global_id: str source_file: str original_html: str clean_text: str text_hash: str tag: str tag_attrs: Dict[str, Any] = field(default_factory=dict) # 外层标签的属性 (class, style...) translation: Optional[str] = None status: str = "pending" # pending, translated, ignored, failed error_msg: Optional[str] = None model_used: Optional[str] = None # 记录使用的模型 quality_score: Optional[int] = None # 记录质量评分 # === 中文模式专用字段 === text_with_placeholders: str = "" # 带占位符的文本 placeholder_map: Dict[str, str] = field(default_factory=dict) # 占位符映射表 {id: html_string} paragraph_type: str = "body" # 段落类型:chapter/section/subsection/epigraph/body translation_with_placeholders: str = "" # 带占位符的译文 translation_with_original_html: str = "" # 还原后的最终 HTML (中文模式) metadata: Dict[str, Any] = field(default_factory=dict) def to_dict(self): return asdict(self) class ManifestManager: """ 负责 Manifest 的生命周期管理。 """ def __init__(self, manifest_path: str): self.manifest_path = Path(manifest_path) self.data: Dict[str, Any] = { "book_id": "", "metadata": {}, "chapter_range": { "start_title": None, "end_title": None, "included_files": [] }, "items": [] } self._items_by_id: Dict[str, ManifestItem] = {} def load(self) -> bool: """从文件加载 Manifest。如果文件不存在则返回 False。""" if self.manifest_path.exists(): try: with open(self.manifest_path, 'r', encoding='utf-8') as f: self.data = json.load(f) # 重建对象映射 self._items_by_id = { item['global_id']: ManifestItem(**item) for item in self.data["items"] } logger.info(f"成功从 {self.manifest_path} 加载 Manifest, 包含 {len(self._items_by_id)} 个项目") return True except Exception as e: logger.error(f"加载 Manifest 失败: {e}") return False return False def save(self): """将当前状态保存到 Manifest 文件。""" # 确保目录存在 self.manifest_path.parent.mkdir(parents=True, exist_ok=True) # 同步 items 到 data 字典 self.data["items"] = [item.to_dict() for item in self._items_by_id.values()] with open(self.manifest_path, 'w', encoding='utf-8') as f: json.dump(self.data, f, ensure_ascii=False, indent=2) # logger.debug(f"Manifest 已保存到 {self.manifest_path}") def init_manifest(self, book_id: str, metadata: Dict, chapter_range: Dict = None): """初始化一个新的 Manifest。""" self.data = { "book_id": book_id, "metadata": metadata, "chapter_range": chapter_range or { "start_title": None, "end_title": None, "included_files": [] }, "items": [] } self._items_by_id = {} self.save() def get_chapter_range(self) -> Dict: """获取记录的章节范围""" return self.data.get("chapter_range", {}) def add_item(self, source_file: str, original_html: str, clean_text: str, tag: str, metadata: Dict = None) -> ManifestItem: """添加一个新的翻译项并分配 ID。""" # 生成全局 ID new_index = len(self._items_by_id) + 1 global_id = f"p_{new_index:05d}" # 生成内容哈希 (用于排重和缓存) text_hash = hashlib.sha256(clean_text.encode('utf-8')).hexdigest() item = ManifestItem( global_id=global_id, source_file=source_file, original_html=original_html, clean_text=clean_text, text_hash=text_hash, tag=tag, metadata=metadata or {} ) self._items_by_id[global_id] = item return item def get_items(self, status: str = None, file_name: str = None) -> List[ManifestItem]: """按状态或文件名查询项目。""" items = list(self._items_by_id.values()) if status: items = [i for i in items if i.status == status] if file_name: items = [i for i in items if i.source_file == file_name] # 必须按 ID 顺序返回以保证分块正确 return sorted(items, key=lambda x: x.global_id) def update_item(self, global_id: str, translation: str, status: str = "translated", error: str = None, model: str = None, score: int = None): """更新翻译结果。""" if global_id in self._items_by_id: item = self._items_by_id[global_id] if translation is not None: item.translation = translation item.status = status if error: item.error_msg = error if model: item.model_used = model if score is not None: item.quality_score = score else: logger.warning(f"尝试更新不存在的 ID: {global_id}") @property def stats(self) -> Dict: """获取翻译进度统计。""" total = len(self._items_by_id) if total == 0: return {"progress": "0%"} translated = sum(1 for i in self._items_by_id.values() if i.status == "translated") ignored = sum(1 for i in self._items_by_id.values() if i.status == "ignored") failed = sum(1 for i in self._items_by_id.values() if i.status == "failed") return { "total": total, "translated": translated, "ignored": ignored, "failed": failed, "pending": total - translated - ignored - failed, "progress_percent": round((translated + ignored) / total * 100, 1) }