Files
pdf2epub/README.md
T

237 lines
13 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;
- позиционирование не сохраняется намеренно — текст должен течь под любой
размер шрифта на читалке.
## Подключение как скил 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
```
`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 тыс. знаков.