Files
pdf2epub/README.md
T
chesirecatt 0320d2d331 Мусор в оглавлении сканов: фильтр поворота, проверка заголовков, вход для 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
2026-08-25 19:03:08 +03:00

276 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pdf2epub
Конвертация PDF, собранного из электронной книги (Producer: calibre), в EPUB/AZW3
для Kindle **с сохранением форматирования**: курсив, полужирный, заголовки глав,
врезки, иллюстрации, склейка абзацев через границы страниц.
## Зачем не `ebook-convert` напрямую
Прямая конвертация `ebook-convert book.pdf book.epub` теряет курсив: poppler
определяет начертание по имени шрифта и не опознаёт сокращённые имена вроде
`MinionPro-It` (без подстроки `Italic`). На «The Unicorn Project» получилось
**435 курсивных фрагментов в PDF против 1 тега `<i>` в EPUB**. Плюс calibre
размечает основной текст как `<h2>` (2817 штук), потому что определяет заголовки
по кеглю относительно самого частого.
`scripts/pdf2html.py` извлекает текст через PyMuPDF, где начертание берётся из
свойств span'а, и сам раскладывает блоки по семантике.
## Что делает скрипт
- курсив/полужирный — по имени шрифта span'а (`-It`, `Italic`, `Bold`, `Semibold`);
- заголовки — по кеглю: `>= 28` или декоративный шрифт — `h1.title`,
`>= 24` — `h1` (главы и части), `>= 17` — `h2` (подзаголовок с датой);
- врезки (дневник, чаты, слайды) — отдельный шрифт семейства → `p.note`;
- абзац = блок PDF; блок без красной строки в начале страницы приклеивается
к абзацу с предыдущей страницы;
- переносы на конце строки снимаются, соседние `</i><i>` схлопываются;
- иллюстрации выгружаются в `images/`, обложка рендерится со страницы 1 в 150 dpi;
- повёрнутый на 90° текст выбрасывается целиком: боковые врезки и подписи к
таблицам распознаются в кашу («aoHegoduenueArdng») и лезут в заголовки, потому
что кегль у них крупный. У Брикмана это 105 строк из 17 300, и все до одной —
брак;
- мусорный заголовок понижается до `<p>`, а не удаляется: оглавление чистится,
текст остаётся. Проверка — `looks_garbled()` в `scripts/bookhtml.py`, шесть
признаков структуры, любых двух хватает. Вместе с фильтром поворота это увело
Брикмана с 99 `<h1>` до 48;
- позиционирование не сохраняется намеренно — текст должен течь под любой
размер шрифта на читалке.
## DjVu со своим текстовым слоем: `scripts/djvu2html.py`
У сканов в DjVu слой распознавания обычно уже есть, и он лучше нашего прогона
через `ocrmypdf`. Замер на Прате (C++ 6-е рус. изд., 1244 полосы): слов со
смесью алфавитов внутри слова было 433 на 391 тысячу слов (10,9 на 10 000),
после сборки из родного слоя — 109 (2,8).
⚠️ **Через PDF этот слой не проходит.** `ddjvu -format=pdf` кладёт страницы
картинками, `pdftotext` после этого отдаёт пустоту. Текст берётся напрямую из
`djvutxt --detail=line`, скрипт разбирает его сам.
Чего в слое DjVu нет вовсе — курсива и полужирного: в скане их и не было.
Заголовки опознаются высотой строки, листинги — пунктуацией.
⚠️ **Высота строки в DjVu — это габарит с выносными элементами, а не кегль.**
Строка с «Ц» или «р» выше соседней на те же 10%, поэтому пороги взяты по
измеренным разрывам, а не «чуть выше основного текста». У Праты при основной
строке 78: колонтитулы 85–90, разделы 105–125, названия глав 170–185.
Строки заголовка склеиваются подряд: название главы занимает две-три строки, и
без склейки «Класс string и стандартная библиотека шаблонов» разваливается на
четыре пункта оглавления.
Самопроверки без сети: `python3 scripts/djvu2html.py --selftest` и
`python3 scripts/pdf2html.py --selftest --selftest` (флаг занимает оба
обязательных аргумента).
## Подключение как скил Claude Code
Репозиторий одновременно является скилом (`SKILL.md` в корне) и клонируется
**прямо в каталог скила** — никаких симлинков и зависимости от `~/Projects`,
структура каталогов на разных ПК может не совпадать:
```bash
git clone https://git.katze-lab.ru/chesirecatt/pdf2epub.git ~/.claude/skills/pdf-to-kindle
```
Скил `pdf-to-kindle` появится в списке доступных при следующем запуске сессии.
Ручной клон нужен только если нет синхронизации памяти. **Штатный путь —
автоматический:** репозиторий перечислен в массиве `SKILL_REPOS` в `sync.sh`
репозитория [`claude-memory`](https://git.katze-lab.ru/chesirecatt/claude-memory),
который крутится по таймеру `claude-memory-sync.timer` раз в 10 минут. На новом
ПК скил клонируется сам при первом же прогоне синхронизации памяти, дальше
обновляется. Ручных шагов и правки systemd-юнита не требуется.
Синхронизация односторонняя (`pull`): правки вносятся и пушатся руками из
`~/.claude/skills/pdf-to-kindle`.
Чтобы добавить в автосинхронизацию ещё один скил — строка в `SKILL_REPOS`
формата `"<git-url> <имя каталога скила>"`.
## Требования
PyMuPDF и calibre:
```bash
python3 -m venv ~/.venvs/pdf2epub
~/.venvs/pdf2epub/bin/pip install pymupdf
sudo dnf install calibre # если ebook-convert ещё нет
```
## Использование
Разведка — какие в PDF шрифты и кегли (пороги в скрипте подобраны под вёрстку
15pt/letter, для другой книги их правят по этому выводу):
```bash
~/.venvs/pdf2epub/bin/python scripts/pdf2html.py --fonts book.pdf
```
Конвертация:
```bash
~/.venvs/pdf2epub/bin/python scripts/pdf2html.py book.pdf out/book.html
ebook-convert out/book.html "Название.epub" \
--title="Название" \
--authors="Автор" \
--language=en \
--cover=out/images/cover.jpg \
--level1-toc='//h:h1' --level2-toc='//h:h2' \
--page-breaks-before='//h:h1' \
--no-default-epub-cover
ebook-convert "Название.epub" "Название.azw3"
```
`--level1-toc`/`--level2-toc` обязательны: без них calibre строит оглавление
своей эвристикой и ломает разбивку по главам.
## Куда класть на Kindle
- `.epub` — через Send to Kindle (веб или почта), Amazon конвертирует на своей стороне;
- `.azw3` — по USB прямо в `documents/` на устройстве.
## Перевод (опционально)
Перевод делает сторонний проект
[`vetermanve/book_translator`](https://github.com/vetermanve/book_translator)
(DeepSeek или локальная Ollama). **Репозиторий не модифицируется** — он
вызывается через свой штатный CLI и форматы файлов. `scripts/translate.py` —
мост: раскладывает наш XHTML в его входной JSON, запускает
`03_translate_parallel.py --all`, собирает результат обратно.
Подготовка (один раз):
```bash
git clone https://github.com/vetermanve/book_translator.git ~/Projects/book_translator
# их requirements.txt неполон: openai и pyyaml импортируются, но не перечислены
~/.venvs/pdf2epub/bin/pip install openai pyyaml python-dotenv
umask 077
mkdir -p out/tr
cat > out/tr/.env <<'EOF'
USE_LOCAL_MODEL=false
DEEPSEEK_API_KEY=<ключ>
EOF
```
Без `pyyaml` каждый запрос падает внутри `_create_system_prompt` **до** обращения
к API, а сторонний скрипт молча подставляет `[UNTRANSLATED]` и рапортует
«API запросов: 0, ошибок: N». Ключ держать только в `.env` с правами `600`,
в командную строку и логи не выводить.
Сначала проверка, нужен ли перевод вообще (язык, объём; ничего не тратит):
```bash
~/.venvs/pdf2epub/bin/python scripts/translate.py out/book.html out/book.ru.html \
--repo ~/Projects/book_translator --workdir out/tr --check-only
```
Если книга уже на целевом языке, скрипт откажется переводить (`--force`
переопределяет). Сам перевод:
```bash
~/.venvs/pdf2epub/bin/python scripts/translate.py out/book.html out/book.ru.html \
--repo ~/Projects/book_translator --workdir out/tr --workers 12
```
Прерванный перевод возобновляется: сторонний скрипт помнит готовые главы. Это
же свойство мешает при **неудачном** прогоне — упавшие главы он считает
готовыми, поэтому перед повторной попыткой:
```bash
rm -rf out/tr/progress out/tr/context out/tr/translations
```
После сборки скрипт сам проверяет язык результата и завершается с ошибкой,
если текст остался на языке оригинала или содержит `[UNTRANSLATED]` —
совпадения числа абзацев мало, при сбое API сторонний скрипт подставляет
оригинал. Файл, не прошедший проверку, упаковывать нельзя.
Дальше `out/book.ru.html` упаковывается тем же `ebook-convert` с
`--language=ru`.
Курсив и полужирный переживают внешний переводчик в виде маркеров
`⟦i⟧…⟦/i⟧`; на обратном пути баланс маркеров проверяется по каждому абзацу.
Картинки, заголовки и порядок блоков вообще не покидают наш скрипт — наружу
уходит только текст, сборка идёт по индексам. В отчёте после перевода:
«без перевода» — главы, вернувшиеся с другим числом абзацев (оставлен
оригинал), «разметка потеряна» — абзацы, где модель испортила маркеры и
курсив снят. Оба числа должны быть близки к нулю.
Самопроверка моста без сети и ключей:
```bash
cd scripts && python3 test_translate.py
```
## Озвучка (опционально)
Синтез тоже делает `book_translator` (`05_create_audiobook.py`, движок
edge-tts от Microsoft — бесплатный, нужен интернет). В `scripts/audiobook.py`
две обвязки; чужой репозиторий по-прежнему не трогаем.
```bash
pip install edge-tts pydub # ffmpeg нужен отдельно, из пакетов системы
# 1. снять маркеры разметки — иначе TTS проговорит ⟦i⟧
python3 scripts/audiobook.py prep out/tr/translations out/tr/translations_tts
# 2. синтез (в фоне; на 12 часов звучания уходит 1.5–3 часа)
cd out/tr && python3 ~/Projects/book_translator/05_create_audiobook.py \
--translations-dir translations_tts --voice dmitry --rate '+0%'
# 3. проверка ДО того, как скрипт уберёт temp_audio/
python3 scripts/audiobook.py verify out/tr/translations_tts out/tr/audiobook
# 4. нарезка по главам — тоже до уборки temp_audio/
python3 scripts/audiobook.py split out/tr/translations_tts out/tr/audiobook \
out/tr/tracks --album "Название книги" --author "Автор" --gap 0.3
```
`verify` находит главы с недостающими фрагментами, битые и нулевые mp3 и главы,
которые короче расчётной длительности. Норму «секунд на знак» он берёт как
медиану по всем главам, поэтому не зависит от выбранного голоса и скорости.
Код возврата ненулевой, если что-то не так.
Повторный запуск синтеза добирает пропущенное: сторонний скрипт пропускает
фрагмент, если файл существует **и непустой**. Битые файлы, названные `verify`,
надо удалить руками — их он проверять не станет.
Ограничения чужой стадии: на выходе один слитный `audiobook_complete.mp3` без
разбивки по главам и без меток — для плеера неудобно, разбивка здесь не
реализована. Для технической книги стоит сначала прогнать фонетику
(`07_extract_terms.py` + `08_generate_phonetics.py`), иначе русский голос
исковеркает все английские термины.
## Проверка результата
```bash
python3 - <<'EOF'
import re
t = open('out/book.html').read()
print('курсив:', t.count('<i>'), 'полужирный:', t.count('<b>'))
print('заголовки:', len(re.findall(r'<h1', t)))
print('двойные пробелы:', re.sub('<[^>]+>', '', t).count(' '))
EOF
```
Двойных пробелов должно быть единицы, курсива — сотни. Ноль курсива означает,
что имена шрифтов в PDF не попали под правило в `style()`.
## Ограничения
- рассчитан на PDF с текстовым слоем; сканы требуют OCR и сюда не годятся;
- пороги кеглей и семейство шрифта для врезок — константы под конкретную вёрстку,
правятся руками по выводу `--fonts`;
- колонтитулы и номера страниц не вырезаются: в PDF от calibre их нет.
Для типографского PDF понадобится отбрасывать блоки по координате `y`.
## Проверено на
`The Unicorn Project` (Gene Kim), 399 страниц: 40 заголовков, оглавление на
62 пункта, 430 курсивных фрагментов, 178 врезок, 5 иллюстраций, ~733 тыс. знаков.