feat: Release v0.10 - Modular Architecture & External Config

- Refactor codebase into src/ (preprocessing, translation, assembly)
- Add pipeline/ scripts for individual stages
- Externalize configuration to config/config.yaml
- Fix Cover Image preservation
- Update documentation and manuals
This commit is contained in:
谭凯
2026-01-31 22:49:44 +08:00
parent 9ef82393be
commit 7a93c52b42
306 changed files with 30313 additions and 1071 deletions
+149
View File
@@ -0,0 +1,149 @@
from pathlib import Path
from typing import List, Dict, Optional
from bs4 import BeautifulSoup
from src.common.data_model import BookStructure, ManifestEntry
from src.assembly.format_restorer import FormatRestorer
from src.common.utils import setup_logger
logger = setup_logger("backfill_engine")
class BackfillEngine:
"""
Applies translations back to the BookStructure.
"""
def __init__(self, llm_client=None):
self.restorer = FormatRestorer()
self.llm_client = llm_client
async def backfill(self, structure: BookStructure, manifest_entries: List[ManifestEntry], mode: str = "bilingual") -> BookStructure:
"""
Modifies the BookStructure in-place with translations.
Args:
structure: The BookStructure (from book_structure.json).
manifest_entries: List of translations.
mode: 'bilingual' or 'target_only'.
"""
logger.info(f"Backfilling with mode: {mode}")
# Index manifest by file and element ID for faster lookup
# Map: file_path -> element_id -> ManifestEntry
manifest_map: Dict[str, Dict[str, ManifestEntry]] = {}
for entry in manifest_entries:
if not entry.translated_text:
continue # Skip untranslated entries
if entry.file_path not in manifest_map:
manifest_map[entry.file_path] = {}
manifest_map[entry.file_path][entry.element_id] = entry
# Iterate resources in structure
for item_id, resource in structure.resources.items():
if resource.media_type != "application/xhtml+xml" or resource.href not in manifest_map:
continue
file_entries = manifest_map[resource.href]
if not file_entries:
continue
logger.debug(f"Processing {resource.href} with {len(file_entries)} translations")
soup = BeautifulSoup(resource.content, 'html.parser')
modified = False
for element_id, entry in file_entries.items():
element = soup.find(id=element_id)
if not element:
logger.warning(f"Element {element_id} not found in {resource.href}")
continue
# Restore formatting with improved logic
restored_html, success = self.restorer.restore(
entry.translated_text,
entry.placeholders,
context_id=element_id
)
# If restoration failed and LLM is available, try to repair
if not success and self.llm_client:
logger.info(f"Attempting LLM repair for {element_id}...")
repaired_text = await self._repair_placeholders(entry)
if repaired_text:
# Retry restoration with repaired text
repaired_html, repaired_success = self.restorer.restore(
repaired_text,
entry.placeholders,
context_id=f"{element_id}-REPAIR"
)
if repaired_success:
logger.info(f"LLM Repair successful for {element_id}")
restored_html = repaired_html
# Update entry to reflect repair (optional, but good for logs)
entry.translated_text = repaired_text
else:
logger.warning(f"LLM Repair failed validation for {element_id}")
# Create translated tag
new_tag = soup.new_tag(element.name)
# Parse restored HTML to get content nodes
# Use html.parser but be careful about fragments
# Wrap in div just to parse then extract children
inner_soup = BeautifulSoup(f"<div>{restored_html}</div>", 'html.parser')
# inner_soup.div shouldn't be None if restored_html exists
container = inner_soup.find('div')
if container:
for child in list(container.children):
new_tag.append(child)
else:
# Fallback
new_tag.string = entry.translated_text
# Copy classes and add 'translation'
classes = element.get('class', [])
if isinstance(classes, str):
classes = classes.split()
new_tag['class'] = classes + ['translation']
# Copy style
style = element.get('style')
if style:
new_tag['style'] = style
if mode == "bilingual":
element.insert_after(new_tag)
else:
element.replace_with(new_tag)
modified = True
if modified:
resource.content = str(soup)
return structure
async def _repair_placeholders(self, entry: ManifestEntry) -> Optional[str]:
"""Ask LLM to fix placeholders in translated text."""
try:
system_prompt = "You are a translation repair assistant."
user_prompt = f"""
The following translation has incorrect placeholders.
Please fix the placeholders in the Translated text so they match the Original text structure EXACTLY.
Do NOT change the Chinese translation content, only fix the φXφ tags.
Original: {entry.original_text}
Translated (Broken): {entry.translated_text}
Output ONLY the fixed Translated text.
"""
# Use raw completion as we don't have short ID context here
# But LLMClient has raw_chat_completion
repaired = await self.llm_client.raw_chat_completion(system_prompt, user_prompt)
return repaired.strip()
except Exception as e:
logger.error(f"LLM Repair error: {e}")
return None