EPUB как второй вход в конвейер; листинги кода не переводятся и не ломают приёмку
- epub2html.py: EPUB -> тот же XHTML, что pdf2html.py, только стандартная библиотека. Уровень глав определяется по книге, а не берётся из <h1>: в EPUB там обычно название и части. - pdf2html.py: листинг узнаётся по флагу моноширинного шрифта PyMuPDF, переносы и отступы внутри <pre> сохраняются, де-дефисация к коду не применяется. - translate.py: <pre> исключён из проверок языка (английский код утягивал долю кириллицы ниже порога приёмки), рабочий каталог привязан к книге, устаревшие главы прошлого прогона чистятся, инлайновый <code> переживает переводчика. - bookhtml.py: общий каркас документа и CSS для обоих входов. - Тесты: test_epub2html.py на собранном в памяти EPUB, четыре новых случая в test_translate.py.
This commit is contained in:
+35
-5
@@ -11,6 +11,7 @@
|
||||
поломке абзац сохраняется переведённым, но без внутренней разметки.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
@@ -19,10 +20,15 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
BLOCK = re.compile(r"^<(p|h1|h2)([^>]*)>(.*)</\1>$")
|
||||
TAG = re.compile(r"</?([ib])>")
|
||||
MARK = re.compile(r"⟦(/?)([ib])⟧")
|
||||
INLINE = ("i", "b", "code")
|
||||
TAG = re.compile(r"</?(%s)>" % "|".join(INLINE))
|
||||
MARK = re.compile(r"⟦(/?)(%s)⟧" % "|".join(INLINE))
|
||||
# картинки и пустые блоки не переводим
|
||||
SKIP = re.compile(r"<img\b")
|
||||
# Листинги кода не переводятся вовсе: BLOCK их не узнаёт, поэтому строки <pre>
|
||||
# доходят до результата нетронутыми. Здесь <pre> вырезается только из проверок
|
||||
# языка — иначе английский код утянул бы долю кириллицы ниже порога приёмки.
|
||||
PRE = re.compile(r"<pre\b.*?</pre>", re.S)
|
||||
|
||||
|
||||
def detect_language(text):
|
||||
@@ -44,7 +50,7 @@ def to_marks(s):
|
||||
def from_marks(s):
|
||||
"""Маркеры -> теги. Возвращает (html, ok): ok=False если разметка разъехалась."""
|
||||
escaped = html.escape(s, quote=False)
|
||||
depth = {"i": 0, "b": 0}
|
||||
depth = dict.fromkeys(INLINE, 0)
|
||||
ok = True
|
||||
for slash, tag in MARK.findall(escaped):
|
||||
depth[tag] += -1 if slash else 1
|
||||
@@ -82,6 +88,8 @@ def split_chapters(blocks):
|
||||
|
||||
def write_input(chapters, extracted):
|
||||
extracted.mkdir(parents=True, exist_ok=True)
|
||||
for stale in extracted.glob("chapter_*.json"):
|
||||
stale.unlink() # главы прошлого прогона иначе уедут в перевод
|
||||
index = []
|
||||
for n, ch in enumerate(chapters):
|
||||
paragraphs = [to_marks(b[3]) for b in ch]
|
||||
@@ -109,6 +117,25 @@ def write_input(chapters, extracted):
|
||||
return index
|
||||
|
||||
|
||||
def claim_workdir(workdir, source, blocks):
|
||||
"""Рабочий каталог принадлежит одной книге. Имена chapter_NNN.json у всех
|
||||
книг одинаковы, а внешний репозиторий пропускает главы, помеченные
|
||||
готовыми, — без этой привязки вторая книга молча соберётся из перевода
|
||||
первой."""
|
||||
digest = hashlib.sha256(
|
||||
"\n".join(b[3] for b in blocks).encode("utf-8")).hexdigest()[:16]
|
||||
claim = workdir / "bridge_source.json"
|
||||
now = {"source": source.name, "blocks": len(blocks), "sha256": digest}
|
||||
if claim.exists():
|
||||
was = json.loads(claim.read_text(encoding="utf-8"))
|
||||
if was.get("sha256") != digest:
|
||||
sys.exit("рабочий каталог занят другой книгой (%s, блоков %s). "
|
||||
"Возьми чистый --workdir." % (was.get("source"), was.get("blocks")))
|
||||
else:
|
||||
claim.write_text(json.dumps(now, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8")
|
||||
|
||||
|
||||
def run_translator(repo, workdir, extracted, workers):
|
||||
script = repo / "03_translate_parallel.py"
|
||||
if not script.exists():
|
||||
@@ -150,7 +177,7 @@ def verify_output(out, target):
|
||||
"""Совпадение числа абзацев ещё не значит, что перевод состоялся: внешний
|
||||
скрипт при ошибке API молча подставляет оригинал. Проверяем язык результата.
|
||||
"""
|
||||
text = re.sub("<[^>]+>", "", out.read_text(encoding="utf-8"))
|
||||
text = re.sub("<[^>]+>", "", PRE.sub(" ", out.read_text(encoding="utf-8")))
|
||||
lang, share = detect_language(text)
|
||||
stub = text.count("[UNTRANSLATED]")
|
||||
print("результат: %s (кириллица %.0f%%), заглушек [UNTRANSLATED]: %d"
|
||||
@@ -186,7 +213,9 @@ def main():
|
||||
plain = " ".join(re.sub("<[^>]+>", "", b[3]) for b in blocks)
|
||||
lang, share = detect_language(plain)
|
||||
chapters = split_chapters(blocks)
|
||||
print("блоков: %d, глав: %d, знаков: %d" % (len(blocks), len(chapters), len(plain)))
|
||||
pre_n = len(PRE.findall(args.source.read_text(encoding="utf-8")))
|
||||
print("блоков: %d, глав: %d, знаков: %d, листингов <pre> (не переводятся): %d"
|
||||
% (len(blocks), len(chapters), len(plain), pre_n))
|
||||
print("язык источника: %s (кириллица %.0f%%)" % (lang, share * 100))
|
||||
|
||||
if lang == args.target and not args.force:
|
||||
@@ -199,6 +228,7 @@ def main():
|
||||
return
|
||||
|
||||
args.workdir.mkdir(parents=True, exist_ok=True)
|
||||
claim_workdir(args.workdir, args.source, blocks)
|
||||
extracted = (args.workdir / "extracted").resolve()
|
||||
write_input(chapters, extracted)
|
||||
run_translator(args.repo.resolve(), args.workdir.resolve(), extracted, args.workers)
|
||||
|
||||
Reference in New Issue
Block a user