Мусор в оглавлении сканов: фильтр поворота, проверка заголовков, вход для DjVu

Причиной мусорных заголовков у Брикмана оказался не порог кегля, а повёрнутый
на 90° текст: боковые врезки и подписи к таблицам распознаются в кашу
(«aoHegoduenueArdng») и попадают в h1, потому что кегль у них крупный. Замер:
105 строк из 17 300, все до одной брак. Блоки с неgоризонтальным направлением
строки выбрасываются целиком (is_rotated).

Остаток — подписи внутри иллюстраций — понижается до <p>, а не удаляется:
looks_garbled() в bookhtml.py, шесть признаков структуры, любых двух хватает.
Проверка повторяется после склейки соседних заголовков: по отдельности «LF»,
«FF» и «TIT» проходят как аббревиатуры, склеенные — обрывок таблицы.
Вместе: 99 h1 у Брикмана против 48.

djvu2html.py — третий вход конвейера. Родной текстовый слой DjVu чище нашего
OCR (у Праты 10,9 против 2,8 слов со смесью алфавитов на 10 000), а через PDF
он не проходит: ddjvu -format=pdf кладёт страницы картинками.

Самопроверки: djvu2html.py --selftest, pdf2html.py --selftest --selftest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XCiPWRi3Jqy4saD2LwU1Dp
This commit is contained in:
chesirecatt
2026-08-25 19:03:08 +03:00
parent 356b9ddb22
commit 0320d2d331
5 changed files with 356 additions and 6 deletions
+55
View File
@@ -6,6 +6,14 @@
строки, а всё, чего не узнал, доносит до результата нетронутым.
"""
import html
import re
# Брак OCR-а внутри слова: цифра вплотную к букве («30HWod1Q»), смесь алфавитов
# в одном слове («aoHegoduenue»), заглавная посреди слова.
DIGIT_WORD = re.compile(r"[^\W\d_][\d]|[\d][^\W\d_]")
WORD = re.compile(r"\w+")
LAT, CYR = re.compile(r"[A-Za-z]"), re.compile(r"[А-Яа-яЁё]")
MIDCAP = re.compile(r"[a-zа-яё][A-ZА-ЯЁ]")
CSS = """
body { margin: 0 1em; }
@@ -28,6 +36,53 @@ code { font-family: monospace; font-size: 0.9em; }
"""
def looks_garbled(t):
"""Заголовок ли это вообще, или подпись из схемы, распознанная по буквам.
Основную часть брака снимает фильтр поворота в главном цикле — здесь остаётся
то, что набрано горизонтально: подписи внутри иллюстраций и обрывки таблиц
(«LF. FF. FT. TIT», «0|91239123», «В»).
Безусловный брак — три случая, каждый сам по себе: в заголовке нет ни одной
буквы; заголовок короче трёх знаков; в нём есть палка (в наборе её не бывает,
в распознанном скане она попадается постоянно).
Дальше шесть признаков структуры, любых двух хватает. Одного мало: «C++11 и
лямбды» даёт «не-букв больше трети», «Python3» — цифру вплотную к букве,
и оба заголовка настоящие.
"""
t = t.strip()
words = WORD.findall(t)
if not words or not any(c.isalpha() for c in t):
return True
if "|" in t:
return True
# Заголовок короче четырёх знаков — обрывок («В», «лов», «юн»). Аббревиатуры
# целиком заглавными («API», «AI») настоящими заголовками бывают, их щадим —
# но только если букв в них не одна и та же: «TT» это обрывок таблицы.
if len(t) < 4 and not (t.isupper() and len(set(t)) >= 2):
return True
# Слова из двух букв по кругу: «TIT. ITITIT», «LF. FF. FT». Нужны минимум два
# таких слова подряд и ни одного нормального, иначе под нож попадёт «Часть III».
long_words = [w for w in words if len(w) >= 3]
if len(long_words) >= 2 and all(len(set(w.lower())) <= 2 for w in long_words):
return True
letters = sum(c.isalpha() for c in t)
toks = t.split()
# Обрывок — короткое слово без цифр внутри: «LF.», «оо». Цифры исключены
# намеренно, иначе «C++11» и «Qt 6» считались бы обрывками.
frags = [w for w in toks if not any(c.isdigit() for c in w)
and sum(c.isalpha() for c in w) < 3]
signals = (
len(frags) * 2 > len(toks), # обрывки слов
letters * 3 < len(t) * 2, # не-букв больше трети
bool(DIGIT_WORD.search(t)),
any(LAT.search(w) and CYR.search(w) for w in words), # смесь алфавитов в слове
any(MIDCAP.search(w) for w in words), # заглавная посреди слова
next((c.islower() for c in t if c.isalpha()), False), # начинается со строчной
)
return sum(signals) >= 2
def document(title, parts):
"""parts: [(tag, cls, inner_html)]; tag 'figure' — псевдотег для картинки."""
buf = ['<?xml version="1.0" encoding="utf-8"?>',