# 27. Определение глав в загружаемых книгах: стандарты · практика · алгоритмы · дизайн **Оркестратор №15, 09.08.2026.** Заказ владельца после находки: ingest-слой границ глав захардкожен под CJK-форму, выведенную из одной стенд-книги (не-CJK txt молча становится одной главой; epub режется по spine с игнором оглавления; print-TOC порождает главы-огрызки). Источники: веб-ресёрч с URL-грунтовкой (§1–§2) · панель двух независимых моделей (§5) · карта по боевому коду (§6). **Статус: фактура и дизайн-синтез готовы; дизайн владельцем НЕ ратифицирован — стройка не санкционирована.** Носители работ — строки бэклога 160–162; жизненный цикл дерева (§3а) — верхнеуровневое решение владельца 09.08, детали обстучать при реализации. ## §1. Три ключевых ответа 1. **Не-эвристического алгоритма для голого txt не существует, и это доказано числами:** Chapter Captor (EMNLP 2020, 9126 романов Gutenberg) — распознавание ЗАГОЛОВКОВ гибридом regex+BERT даёт F1=0.77, а предсказание границ БЕЗ заголовков (семантическая сегментация) — F1=0.453 [arxiv.org/abs/2011.04163]. Главы — авторская разметка, а не тематический сдвиг. Все зрелые инструменты (Calibre, chapterize, kaf-cli, AozoraEpub3) — каскад «статистика формата строк → языковые паттерны → пороги правдоподобия». 2. **У epub оглавление есть по стандарту и обязательно.** EPUB 3: публикация «MUST contain an EPUB navigation document» с toc nav — иерархия заголовков + ссылки файл#якорь [w3.org/TR/epub-33/#sec-nav-toc]. EPUB 2: обязательный NCX (вложенность томов/глав; navLabel+ с xml:lang — единственный стандартный носитель МУЛЬТИЯЗЫЧНЫХ названий) [idpf.org/epub/20/spec/OPF_2.0.1_draft.htm]. FB2 (жив, критичен для ru-рынка): дерево глав = сама разметка body/section+title [github.com/gribuser/fb2]. Для epub/fb2 границы И названия достаются из стандарта без эвристик — текущий разрез «по spine с игнором nav» выбрасывает гарантированную структуру. 3. **Print-TOC — не помеха, а источник.** Устоявшееся направление (ICDAR Book Structure Extraction 2009–2013): детект TOC-блока → парсинг → линковка в тело матчем заголовков — границы плюс канонические названия даром; TOC-гейты дёшевы (кандидаты ближе 4 строк = блок оглавления). ## §2. Несущие факты по слоям **EPUB:** резать по spine неверно ПО СТАНДАРТУ — якоря nav легально ведут внутрь файла (несколько глав в одном xhtml), глава легально живёт в нескольких файлах; правильный алгоритм: поток = spine linear="yes", границы = якоря nav/NCX, спроецированные на поток. Порядку/полноте nav верить нельзя (с EPUB 3.3 порядок — SHOULD, полнота не требовалась никогда) — нормализация: сортировка целей по позиции в spine, непокрытые документы прикреплять к предыдущей главе. Служебные страницы фильтруются каскадом штатных сигналов: manifest properties (nav, cover-image) → spine linear="no" → landmarks (epub:type обязателен) → epub:type/DPUB-ARIA в контенте (добровольный — сигнал, не гарантия). Однофайловый epub и битый nav → фолбэк на txt-эвристики. **TXT (Calibre как эталон зрелости):** (1) статистический детект формата абзацев; (2) списки словесных паттернов — у Calibre только en+de(!), CJK-миры держат свои инструменты: kaf-cli zh «第.{1,8}章» + том «第…[卷部]», AozoraEpub3 ja — 話/章/篇/部/節/幕/プロローグ/序章/終章/間章; (3) пороги правдоподобия: min_chapters=ceil(wordcount/7000) ≤ hits < 150, заголовок ≤35 симв., ≥3 глав; (4) сцены «* * *» — отдельный класс, не главы; (5) конвенция Gutenberg «4 пустые строки перед главой» — пустострочные раны как вторичный сигнал [W3C WCAG T3]. **Готовой мультиязычной библиотеки паттернов-как-данных не существует** — собирать свою в структурный пак из проверенных списков (сид: kaf-cli + AozoraEpub3 + Calibre + chapterize; regex-набор Chapter Captor — сид en); прецедент конфиг-схемы — ParserConfig oomol-lab (chapter/volume/section_patterns). **Иерархия:** том/часть над главой — отдельный уровень паттернов у всех зрелых инструментов; плоский список ломается на «Том 2, Глава 1» (нумерация перезапускается) — в модели структуры нужен уровень volume/part. **Место LLM:** полная LLM-разметка книги (LumberChunker, EMNLP 2024) — дороже/медленнее по признанию авторов, цена линейна от объёма, недетерминизм ломает снапшоты — как основной путь отвергнута. Доказанный в проде паттерн (LILAC, FSE'24, лог-парсинг): **LLM один раз ВЫВОДИТ шаблон, дальше режет детерминированный код**; перенос на книги естественен (вызов на семплах строк-кандидатов → шаблон книги → в снапшот → перепрогоны $0). Для книг такой паттерн не опубликован. ## §3. Рекомендуемый каскад (предложение оркестратора, НЕ ратифицировано) LLM никогда не режет сама; по цене источника: 1. **Структурные источники ($0, без эвристик):** epub nav → NCX → fb2 section/title; скрейп-epub (WebToEpub/FanFicFare) — границы точны по построению, доверять. Названия на языке оригинала — оттуда же. 2. **TXT:** print-TOC-блок → исключить из границ + оракул имён; языко-независимый индуктор (кластеризация скелетов строк + скоринг: изоляция · монотонная нумерация · коридор hits/wordcount) + словари маркеров per-language как ДАННЫЕ; конкурирующие схемы-кандидаты. 3. **Fallback:** один дешёвый LLM-вызов выводит шаблон книги; шаблон — артефакт снапшота. 4. **Всегда:** громкие WARN вместо молчаливой деградации («1 глава на 20 МБ», «скачок нумерации», «глава-огрызок»). **Два жёстких требования к дизайну (владелец, 09.08):** модель «что бывает заголовком» выводится из КОРПУСА разнообразных книг, не из стенд-книги, и приёмка детекта — прогоном по корпусу (полигон); схема данных проектируется от общего кейса, а не наращиванием заплаток (исторически слой выпал из карты общности: маркеры ушли в данные, форма шаблона осталась в Go). **Цена итераций:** правки детекта двигают нарезку → реснапшот-класс на прогнанных книгах — строить один раз, ДО открытия интейка книг, пока платная книга одна. ### §3а. Жизненный цикл дерева глав (решение владельца 09.08, верхнеуровнево; детали обстучать при реализации) 1. **Парсинг (загрузка):** дерево показывается сразу — черновая структура с пометкой «может быть неточно», оригинальные названия + служебный рендер «Глава N» ($0, локаль интерфейса). ⚠ Открытая продуктовая развилка: владелец предлагал давать «примерный перевод названий» уже на этой стадии — это платный вызов ДО старта прогона (деньги начинаются с загрузки, а не с явного запуска); рекомендация оркестратора — перевод со старта прогона (п.2), решение при ратификации дизайна. 2. **Старт прогона:** мини-сессия названий (§5а; требует роли title — Этап 1) даёт провизорный перевод дерева в первые секунды прогона. 3. **Черновая волна:** структура ЖИВАЯ — in-band метки черновика (Этап 2; до его постройки стадия пропускается) корректируют её по ходу: число глав может измениться против эвристики — недоразмеченное добавится, ложное удалится. Freeze на этой стадии относится к НАРЕЗКЕ/деньгам, не к виду: якорей читателей ещё нет, вид-плоскость легально ревизуется. 4. **Подпись банка (майнинг-стоп):** перегенерация дерева — ревизия структуры по накопленным меткам + адресная ревизия названий по подписанному банку (§5а фаза 2). С этой точки структура затвердевает, дерево показывает поглавный прогресс перевода. ## §4. Что НЕ брать Тематическая сегментация как основа (F1=0.453) — максимум tie-breaker · полная LLM-разметка (цена/недетерминизм) · вебновелльные скрейперы как образец детекта (у них глава = страница сайта) · универсальный код без языковых данных (ни один инструмент так не решил). ## §5. Панель двух независимых дизайнеров (Fable-5 · Opus-5) — конвергенция Обе модели проектировали алгоритм с нуля по одинаковому промту без наводок (только задача и свойства движка; репо и этот док им были недоступны; веб — только для фактов). **Сошлись на ~13 несущих решениях**, все совместимы с §1–§3: 1. Один общий движок + формато-адаптеры → единое IR (поток блоков с атрибутами); языковое — ТОЛЬКО данными (грамматика шаблонов `{marker}{numeral}{sep}{name}`, читатели числительных, лексиконы, ВСЕ пороги). 2. **Индукция шаблона вместо построчных порогов**: кандидаты кластеризуются по скелету, оценивается КЛАСТЕР (доминирующее семейство + возрастающий ряд номеров); одиночное «Глава» в диалоге не примыкает к кластеру из 300 — отсекается по построению. 3. EPUB: nav/NCX приоритетно с валидацией; FB2: секции с проверкой вырожденности; TXT: индуктор. Print-TOC: исключить из границ + оракул названий/количества (LCS-выравнивание с телом — сильнейший валидатор). 4. **Кросс-аудит всегда**: текстовый индуктор гоняется и на EPUB/FB2 как независимая $0-гипотеза против деклараций формата (битые TOC скрейпов — массовый случай); правило маржи между гипотезами; неоднозначность = первоклассный громкий исход с выбором пользователя. 5. Скоринг-вектор: правдоподобие количества · распределение длин (runt/whale) · монотонность нумерации · согласие источников · доминантность шаблона · headSpread (границы сгрудились в начале = TOC). Три вердикта: OK / DEGRADED-с-badge / FAILED. 6. **Честный FAILED**: псевдо-главы не выдумываются никогда; дерево показывает «структура не распознана» + технические «Фрагменты» (визуально НЕ главы); перевод НЕ блокируется. Выходы: выбор из отвергнутых кластеров одним кликом · ручной редактор структуры (правки = отдельный слой) · opt-in LLM-классификатор кандидатов со сметой. 7. **Инвариант сохранения символов**: Σ длин глав + служебного == длине тела, точно; нарушение = баг движка, платные вызовы не стартуют. 8. **Развязка денег и структуры** (несущее, оба независимо): оплата/память ключуются контент-хешами СЕГМЕНТОВ, глава — группировка-вид; пере-нарезка = перегруппировка оплаченного. Structure freeze после старта перевода (нарезка); ревизия — только явной миграцией с отчётом «что осиротеет, что перекупится, почём» ДО применения. 9. Якоря читателя — на хешах абзацев; id главы — контент-производный (хеш первого окна тела+титула — правка в глубине главы id не двигает); миграционная карта same/moved/split/merged/gone/new, осиротевшее показывается, не выбрасывается. 10. LLM в детекте — только opt-in классификатор ПОВЕРХ намайненных кандидатов, результат замораживается в снапшоте (replay, не повторный вызов). 11. Названия: лестница источников с провенансом; отсутствующее — служебная метка `generated:true` (локаль интерфейса), НЕ фабрикация; **номер не переводится, а перерендеривается** ($0); перевод имён — батч-роль title (§5а); канон названия сверяется $0-гейтом с заголовком в переведённом теле. 12. Калибровочный корпус из УРОДЛИВЫХ реальных книг (скрейпы/самиздат/конверсии, все скрипты; хеши+смещения, не тексты) + CI-запрет регрессий + запрет подгонки порогов без добавления книги в корпус. 13. Детерминизм механики: целочисленные баллы, сортированные обходы, фиксированные тай-брейки, побайтовый golden на structure.json. Расхождения — только в глубине: Opus типизировал провалы (отдельно «гибрид: половина книги размечена» и «nav и NCX согласны, но оба неверны — согласие ≠ правота»), добавил детект бойлерплейта скрейпов; Fable детальнее проработал пользовательский редактор структуры. In-band-разметку моделью в потоке перевода не предложил ни один (оба заменили модель-сейфгард бесплатным кросс-аудитом) — она остаётся ТРЕТЬИМ эшелоном (идея владельца): для книг без кандидатов, для коррекции структуры по ходу черновой волны (§3а п.3) и как канал названий; её выход проходит тот же валидатор и не двигает замороженную нарезку. ### §5а. Мини-сессия названий (роль title; уточнено с владельцем 09.08) Названия переводит НЕ волна тела, а отдельная батч-роль `title` (механика InternalCall, чекпойнт/реплей/телеметрия наследуются): **редакторский слот, не черновой** — объём копеечный (единицы тысяч токенов на книгу), видимость максимальная; **инъекция банка той же механикой** `memory.Select` — спойлер-окна работают естественно (у названия есть номер главы). Синхронизация с банком **двухфазная** (полный банк существует только после майнинг-стопа и подписи): фаза 1 на старте прогона с сид-банком → названия провизорны; фаза 2 после подписи банка → адресная ревизия только названий, где термы разошлись (дифф по попаданиям). Волны потом переводят подзаголовок в прозе тела — $0-канон-гейт сверяет его с названием дерева, расхождение = громкий флаг. Номер рендерится всегда бесплатно. ## §6. Рекомендации реализации, обстучанные о боевой код (карта по рабочему дереву с незакоммиченным паком блокеров) ### Жёсткие правды кода 1. **Деньги ключуются ПОЗИЦИЯМИ, не контентом.** RequestHash несёт плотный номер главы и ChunkIdx (render.go:292-317), chunk_status ищется строго по (book, chapter, chunk_idx, stage) (migrate.go:125); контент-хеш `msgsContentHash` — только guard резюме, не ключ поиска. Сдвиг ОДНОЙ границы главы перенумеровывает хвост книги и промахивает все его чекпойнты — перекупка хвоста при неизменных байтах. 2. **Любой новый детектор границ = bump chunkerVersion → движение обоих волновых снапшотов → moveOther → $0-репин не работает вовсе** (repin.go:47-91): перекупаются обе волны на 100%. Признанный отложенный долг D15.2 (snapshot.go:53-64: «content-addressed reuse … must be designed»). Следствие: **итерации детекта до развязки денег стоят полный прогон книги; единственное дешёвое окно — пока платная книга одна.** 3. **Нарезка сама зависит от глав**: паковка и edit-юниты рестартуют на границе главы (chunker.go:120-145, 330-397), инжекция банка — спойлер-окна since/until в плотных ординалах (memory.go:642-650) и sticky-reset на границе (wave.go:82-95). «Структура = вид» требует развязки чанкера от глав — само по себе одноразовый resnapshot. 4. **Дыра Р6**: ревизия структуры, не двигающая снапшот (ручной редактор, структура-как-данные), сегодня перекупила бы хвост МОЛЧА — projectRebill пропускает строки с совпавшим снапшотом, контент/позиционная ось невидима консент-гейту (rebill.go:32-35, 144-146). Закрывается дёшево read-path-расширением projectRebill — и это ровно миграционный отчёт §5 п.8. 5. **Паттерн-паки структуры нельзя класть в существующие планы данных**: pack.Version() и EmbeddedVersion фолдятся в волновой снапшот (snapshot.go:441-442) — файл паттернов в langpack инвалидировал бы переводческие снапшоты пары, в embedded — всех книг. Нужен ТРЕТИЙ версионный план (structure-pack), фолдящийся только в manifestKey ($0-пересборка). Рядом ловушка: добавление нового поля в снапшот само ломает репин всех старых строк (unknown field = moveOther, repin.go:70-84) — одноразовая цена, планировать в то же окно. 6. **Параграф-уровня в движке нет нигде** (параграфы транзиентны, chunker.go:458-466), параграф легально пересекает чанки — «якоря на хешах абзацев» (§5 п.9) — целиком новая плоскость; ближний эквивалент сегодня — edit-юнит (ПТ-21) + chapter-ID манифеста. ### Что переиспользуется (строить не надо) Манифест-сайдкар пака блокеров = **~60% механики structure.json**: версионируемый документ с ключом годности и cutTag, стейл-детект с фолбэком, атомарная запись (artifact.go), $0-команда tmctl manifest, шов read-моделей → status/export; расширение содержимого двигает ТОЛЬКО файл (в снапшот не входит). · projectRebill/checkRebillConsent = готовый отчёт «что перекупится и почём» ДО применения. · matchHeaderLine + parseSectionNumeral + HeadingRule = готовая грамматика с CJK-читателем числительных. · splitTextChapters = готовый первый паттерн-пак индуктора. · Лестница кодировок, zip/OPF/xhtml-транспорт, ruby-захват — как есть. · ApplyHeading = $0-канал показа названий (не в чекпойнтах). · Терминологист-батчи + InternalCall = прецедент мини-сессии названий. · WaveSignatureStop (exit 3) = прецедент «громкий исход с выбором пользователя». · Контракт 14 и платформа УЖЕ готовы: Chapter.id opaque, number «не ключ», heading nullable «из данных книги», эпоха курсора = generation of the manifest, resync_required; платформенная таблица chapters под opaque id построена (00002_readmodel.sql:91-106). ### Этапность (носители — строки бэклога 160–162; ратификация дизайна — владелец) - **Этап 0 (строка 160) — $0, wire-нейтрально, можно немедленно:** titleRaw в манифест (subtitle уже добывается в matchHeaderLine и выбрасывается — только пронести) + provenance у heading (rendered/data) + тип единицы (chapter/fragment) + вердикт структуры; бамп manifestVersion. Закрытие дыры Р6 в projectRebill. Слой пользовательских правок — ОТДЕЛЬНЫЙ сайдкар (в манифест нельзя: он unconditionally перестраивается, hand-edit=invalid — manifest.go:36-38, 456-459). - **Этап 1 (строка 161) — «большой перекрой», единственное resnapshot-окно, пока платная книга одна:** IR-слой + формато-адаптеры (epub nav/NCX + фильтры linear/properties — сейчас opfPackage их не парсит, ingest.go:438-447; fb2) + индуктор с валидаторами и вердиктами + третий версионный план паттернов; развязка чанкера от глав (главы = группировка-вид); спойлер-окна банка с ординалов на chapter-ID (bank-only move — репиннимый класс); контент-адресуемый resume (долг D15.2) — чтобы будущие ревизии структуры перестали стоить хвост книги; формула Chapter.ID «окно» — до материализации глав у пользователей; стадия title (§5а; аддитивный фолд стадии двигает снапшот — в это же окно); аддитивное расширение контракта 14 (fragment-тип, вердикт, badge — минорный бамп). Всё — одним согласованным resnapshot стенд-книги с консент-отчётом. - **Этап 2 (строка 162) — после:** in-band метки глав в потоке перевода (третий эшелон + коррекция структуры по ходу черновой волны, §3а; промпт-сдвиг волны) + канон-гейт названий ($0) + миграционные карты якорей + калибровочный корпус в CI (полигон). ### Граница доверия LLM ↔ статика Языковые данные руками не пишутся никогда: сид — проверенные списки зрелых инструментов (zh/ja/en/de готовы); на новом языке/книге с низкой уверенностью универсальных сигналов — один дешёвый LLM-вызов ВЫВОДИТ шаблон книги, шаблон проходит тот же статический валидатор, замораживается в снапшот, накопленное абсорбируется в паттерн-пак. LLM — генератор и арбитр данных; статика — исполнитель, валидатор (сохранение символов, монотонность, коридоры — валидировать LLM-выход может только она) и масштабатор. Полный LLM-проход по книге отвергнут (§4).