Improve EPUB restoration, book terminology and Calibre QA workflow
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import shutil
|
||||
import uuid
|
||||
import hashlib
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Dict, Set
|
||||
@@ -8,6 +8,7 @@ import ebooklib
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
from ebooklib import epub
|
||||
|
||||
from src.common.html import parse_html, translation_targets
|
||||
from src.common.data_model import BookStructure, BookMetaData, ResourceItem
|
||||
from src.common.exceptions import CleaningError
|
||||
from src.common.utils import setup_logger, ensure_directory
|
||||
@@ -50,13 +51,14 @@ class EpubCleaner:
|
||||
|
||||
if media_type == ebooklib.ITEM_DOCUMENT:
|
||||
# Clean HTML
|
||||
content_str = item.get_content().decode('utf-8')
|
||||
content_str = item.content.decode('utf-8')
|
||||
cleaned_content = self._clean_html(content_str, file_name)
|
||||
|
||||
resources[item_id] = ResourceItem(
|
||||
href=file_name,
|
||||
media_type="application/xhtml+xml",
|
||||
content=cleaned_content
|
||||
content=cleaned_content,
|
||||
properties=' '.join(getattr(item, 'properties', [])) or None
|
||||
)
|
||||
elif media_type in (ebooklib.ITEM_IMAGE, ebooklib.ITEM_STYLE, ebooklib.ITEM_FONT, ebooklib.ITEM_COVER):
|
||||
# Save asset
|
||||
@@ -68,6 +70,8 @@ class EpubCleaner:
|
||||
|
||||
# Preserve directory structure to avoid collisions
|
||||
asset_path = self.assets_dir / file_name
|
||||
if not asset_path.resolve().is_relative_to(self.assets_dir.resolve()):
|
||||
raise CleaningError(f'Unsafe asset path: {file_name}')
|
||||
ensure_directory(asset_path.parent)
|
||||
|
||||
# Ebooklib might change filenames, safer to use item.get_content()
|
||||
@@ -83,6 +87,8 @@ class EpubCleaner:
|
||||
# NCX or NAV document - preserve for TOC
|
||||
content_bytes = item.get_content()
|
||||
asset_path = self.assets_dir / file_name
|
||||
if not asset_path.resolve().is_relative_to(self.assets_dir.resolve()):
|
||||
raise CleaningError(f'Unsafe asset path: {file_name}')
|
||||
ensure_directory(asset_path.parent)
|
||||
|
||||
with open(asset_path, "wb") as f:
|
||||
@@ -109,6 +115,7 @@ class EpubCleaner:
|
||||
|
||||
# 4. Construct Structure
|
||||
structure = BookStructure(
|
||||
source_sha256=hashlib.sha256(self.input_path.read_bytes()).hexdigest(),
|
||||
metadata=metadata,
|
||||
spine=spine_ids,
|
||||
resources=resources
|
||||
@@ -146,26 +153,33 @@ class EpubCleaner:
|
||||
return "application/octet-stream"
|
||||
|
||||
def _clean_html(self, content: str, filename: str) -> str:
|
||||
soup = BeautifulSoup(content, 'html.parser')
|
||||
soup = parse_html(content)
|
||||
|
||||
# 1. Provide IDs for structural/translatable elements
|
||||
self._ensure_element_ids(soup)
|
||||
self._ensure_element_ids(soup, filename)
|
||||
|
||||
# 2. Flatten divs (Disable to prevent style loss)
|
||||
# self._flatten_divs(soup)
|
||||
|
||||
return str(soup)
|
||||
|
||||
def _ensure_element_ids(self, soup: BeautifulSoup):
|
||||
"""
|
||||
Injects UUIDs into p, h1-h6, li tags if they don't have an ID.
|
||||
This provides the anchor for translation backfilling.
|
||||
"""
|
||||
targets = soup.find_all(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li'])
|
||||
for tag in targets:
|
||||
if not tag.has_attr('id'):
|
||||
tag['id'] = f"uuid-{uuid.uuid4()}"
|
||||
|
||||
def _ensure_element_ids(self, soup, filename=''):
|
||||
targets = translation_targets(soup)
|
||||
used = set()
|
||||
for tag in soup.find_all(attrs={'id': True}):
|
||||
if tag['id'] in used:
|
||||
del tag['id']
|
||||
else:
|
||||
used.add(tag['id'])
|
||||
for index, tag in enumerate(targets):
|
||||
if not tag.get('id'):
|
||||
digest = hashlib.sha256(f'{filename}:{index}'.encode()).hexdigest()[:16]
|
||||
candidate = f'tr-{digest}'
|
||||
while candidate in used:
|
||||
candidate += '-unit'
|
||||
tag['id'] = candidate
|
||||
used.add(candidate)
|
||||
|
||||
def _flatten_divs(self, soup: BeautifulSoup):
|
||||
"""
|
||||
Converts generic divs containing only inline text/styles to p tags.
|
||||
|
||||
Reference in New Issue
Block a user