150 lines
5.3 KiB
Python
150 lines
5.3 KiB
Python
"""
|
|
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
|
|
translation: Optional[str] = None
|
|
status: str = "pending" # pending, translated, ignored, failed
|
|
error_msg: Optional[str] = None
|
|
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": {},
|
|
"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):
|
|
"""初始化一个新的 Manifest。"""
|
|
self.data = {
|
|
"book_id": book_id,
|
|
"metadata": metadata,
|
|
"items": []
|
|
}
|
|
self._items_by_id = {}
|
|
self.save()
|
|
|
|
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):
|
|
"""更新翻译结果。"""
|
|
if global_id in self._items_by_id:
|
|
item = self._items_by_id[global_id]
|
|
item.translation = translation
|
|
item.status = status
|
|
item.error_msg = error
|
|
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)
|
|
}
|