Feat: v0.12 Pipeline Separation, Spacing Fix, and Idempotent Restoration
This commit is contained in:
+27
-69
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user