Feat: v0.12 Pipeline Separation, Spacing Fix, and Idempotent Restoration

This commit is contained in:
谭凯
2026-02-01 10:39:07 +08:00
parent 7a93c52b42
commit 8e58415173
42 changed files with 3741 additions and 110 deletions
+27 -69
View File
@@ -1,9 +1,8 @@
from pathlib import Path
from typing import List, Dict, Optional
from bs4 import BeautifulSoup
from bs4 import BeautifulSoup, NavigableString
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")
@@ -11,11 +10,13 @@ logger = setup_logger("backfill_engine")
class BackfillEngine:
"""
Applies translations back to the BookStructure.
Now simplified to only inject 'translated_html' or fallback to text.
No format restoration logic here.
"""
def __init__(self, llm_client=None):
self.restorer = FormatRestorer()
self.llm_client = llm_client
def __init__(self):
# No more Restorer or LLM Client
pass
async def backfill(self, structure: BookStructure, manifest_entries: List[ManifestEntry], mode: str = "bilingual") -> BookStructure:
"""
@@ -28,18 +29,17 @@ class BackfillEngine:
"""
logger.info(f"Backfilling with mode: {mode}")
# Index manifest by file and element ID for faster lookup
# Map: file_path -> element_id -> ManifestEntry
# Index manifest
manifest_map: Dict[str, Dict[str, ManifestEntry]] = {}
for entry in manifest_entries:
if not entry.translated_text:
continue # Skip untranslated entries
continue
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
# Iterate resources
for item_id, resource in structure.resources.items():
if resource.media_type != "application/xhtml+xml" or resource.href not in manifest_map:
continue
@@ -59,50 +59,29 @@ class BackfillEngine:
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}")
# Determine content to inject
# Prefer translated_html (rich text), fallback to translated_text (plain text)
html_content = entry.translated_html
plain_text = entry.translated_text
# 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)
if html_content:
# Parse HTML fragment
# Wrap in div to handle multiple top-level nodes
inner_soup = BeautifulSoup(f"<div>{html_content}</div>", 'html.parser')
container = inner_soup.find('div')
if container:
for child in list(container.children):
new_tag.append(child)
else:
new_tag.string = plain_text
else:
# Fallback
new_tag.string = entry.translated_text
# Fallback to plain text
new_tag.string = plain_text
# Copy attributes
# Copy classes and add 'translation'
classes = element.get('class', [])
if isinstance(classes, str):
@@ -114,6 +93,7 @@ class BackfillEngine:
if style:
new_tag['style'] = style
# Injection Strategy
if mode == "bilingual":
element.insert_after(new_tag)
else:
@@ -125,25 +105,3 @@ class BackfillEngine:
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