Илья Поляков 1a2c03f999 Мягкий перенос U+00AD: русские PDF рвали слова пополам
Питер и ДМК переносят строку мягким дефисом, а не обычным. Ветка склейки
проверяла только "-", поэтому строки соединялись через пробел: «введен ные»,
«Практиче ски». Теперь U+00AD снимается при склейке, остатки внутри строки
удаляются, а в ошибочно опознанных листингах перенос со строкой схлопывается.

Измерено на Шоттсе (Питер, 2020, 544 стр.): было 8 мягких переносов на
проверенной выборке, стало ноль.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2PNJCFFXvizoNjTFuTaqi
2026-08-24 11:50:26 +03:00

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, структура каталогов на разных ПК может не совпадать:

git clone https://git.katze-lab.ru/chesirecatt/pdf2epub.git ~/.claude/skills/pdf-to-kindle

Скил pdf-to-kindle появится в списке доступных при следующем запуске сессии.

Ручной клон нужен только если нет синхронизации памяти. Штатный путь — автоматический: репозиторий перечислен в массиве SKILL_REPOS в sync.sh репозитория claude-memory, который крутится по таймеру claude-memory-sync.timer раз в 10 минут. На новом ПК скил клонируется сам при первом же прогоне синхронизации памяти, дальше обновляется. Ручных шагов и правки systemd-юнита не требуется.

Синхронизация односторонняя (pull): правки вносятся и пушатся руками из ~/.claude/skills/pdf-to-kindle.

Чтобы добавить в автосинхронизацию ещё один скил — строка в SKILL_REPOS формата "<git-url> <имя каталога скила>".

Требования

PyMuPDF и calibre:

python3 -m venv ~/.venvs/pdf2epub
~/.venvs/pdf2epub/bin/pip install pymupdf
sudo dnf install calibre     # если ebook-convert ещё нет

Использование

Разведка — какие в PDF шрифты и кегли (пороги в скрипте подобраны под вёрстку 15pt/letter, для другой книги их правят по этому выводу):

~/.venvs/pdf2epub/bin/python scripts/pdf2html.py --fonts book.pdf

Конвертация:

~/.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 (DeepSeek или локальная Ollama). Репозиторий не модифицируется — он вызывается через свой штатный CLI и форматы файлов. scripts/translate.py — мост: раскладывает наш XHTML в его входной JSON, запускает 03_translate_parallel.py --all, собирает результат обратно.

Подготовка (один раз):

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, в командную строку и логи не выводить.

Сначала проверка, нужен ли перевод вообще (язык, объём; ничего не тратит):

~/.venvs/pdf2epub/bin/python scripts/translate.py out/book.html out/book.ru.html \
  --repo ~/Projects/book_translator --workdir out/tr --check-only

Если книга уже на целевом языке, скрипт откажется переводить (--force переопределяет). Сам перевод:

~/.venvs/pdf2epub/bin/python scripts/translate.py out/book.html out/book.ru.html \
  --repo ~/Projects/book_translator --workdir out/tr --workers 12

Прерванный перевод возобновляется: сторонний скрипт помнит готовые главы. Это же свойство мешает при неудачном прогоне — упавшие главы он считает готовыми, поэтому перед повторной попыткой:

rm -rf out/tr/progress out/tr/context out/tr/translations

После сборки скрипт сам проверяет язык результата и завершается с ошибкой, если текст остался на языке оригинала или содержит [UNTRANSLATED] — совпадения числа абзацев мало, при сбое API сторонний скрипт подставляет оригинал. Файл, не прошедший проверку, упаковывать нельзя.

Дальше out/book.ru.html упаковывается тем же ebook-convert с --language=ru.

Курсив и полужирный переживают внешний переводчик в виде маркеров ⟦i⟧…⟦/i⟧; на обратном пути баланс маркеров проверяется по каждому абзацу. Картинки, заголовки и порядок блоков вообще не покидают наш скрипт — наружу уходит только текст, сборка идёт по индексам. В отчёте после перевода: «без перевода» — главы, вернувшиеся с другим числом абзацев (оставлен оригинал), «разметка потеряна» — абзацы, где модель испортила маркеры и курсив снят. Оба числа должны быть близки к нулю.

Самопроверка моста без сети и ключей:

cd scripts && python3 test_translate.py

Озвучка (опционально)

Синтез тоже делает book_translator (05_create_audiobook.py, движок edge-tts от Microsoft — бесплатный, нужен интернет). В scripts/audiobook.py две обвязки; чужой репозиторий по-прежнему не трогаем.

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), иначе русский голос исковеркает все английские термины.

Проверка результата

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 тыс. знаков.

S
Description
Скил Claude Code pdf-to-kindle: PDF (calibre) -> EPUB/AZW3 с сохранением курсива, глав и врезок. Ставится автоматически — репозиторий указан в SKILL_REPOS в sync.sh репозитория claude-memory, клонируется в ~/.claude/skills/pdf-to-kindle по таймеру. Ручной вариант: git clone ~/.claude/skills/pdf-to-kindle
https://git.katze-lab.ru/chesirecatt/claude-memory
Readme 352 KiB
Languages
Python 100%