Обычная книга почти полностью линейна. Можно вернуться назад, заглянуть вперёд, открыть сноску, но сам мир существует только внутри последовательного текста.
Мне хочется добавить к книге ещё одно измерение - срез мира в конкретной точке рассказа. У персонажей, мест и других важных сущностей есть отдельные карточки, которые меняются по мере чтения. Открываешь персонажа на условной 67-й странице и видишь только то состояние, до которого он успел дойти к этому месту. Листаешь дальше - карточка перестраивается. Возвращаешься назад - возвращается и старая версия.
Я пока называю это "Книгой 4Д". Технически с количеством измерений можно поспорить, но название уже прижилось. Или, в шутку, "Книга 3.0", потому что "Книга 2.0" - это уже гипертекстовый интернет.
Для читателя
Книга читается совершенно обычно, но для неё нужна читалка, понимающая расширенный формат. Рядом с текстом доступен список уже появившихся персонажей, мест и других сущностей. Упоминания в тексте тоже можно связать с соответствующей карточкой.
Главное отличие от обычной энциклопедии - карточка не знает всю книгу сразу.
Допустим, Марек в начале рассказа известен как придворный лекарь. Потом выясняется, что он бывал в каком-то странном месте, позже обнаруживается его связь с другим персонажем, а ближе к финалу становится понятна причина нескольких старых поступков.
В обычной вики всё это живёт в одной статье и моментально превращается в склад спойлеров. Здесь у Марека существует несколько последовательных состояний.
На раннем этапе карточка может выглядеть так:
Марек - придворный лекарь, много лет работающий при дворе. Производит впечатление спокойного и довольно закрытого человека.
Через несколько глав:
Марек - придворный лекарь. Недавние события показали, что он знаком с людьми, о которых раньше не упоминал. Причины этого пока неизвестны.
А в конце это уже полноценная статья с тем, что действительно стало известно за весь рассказ.
При этом нельзя дочитать книгу, вернуться в начало и случайно увидеть финальную версию. Карточка определяется не прогрессом аккаунта, а текущим местом в тексте. Вернулся назад - вернулся и мир.
Для автора
Та же система может быть полезна ещё до публикации.
После нескольких глав можно открыть книгу в авторском режиме, перейти на нужное место и посмотреть, каким получился персонаж именно здесь.
Это важно потому, что сам автор уже знает всю историю. После написания финала довольно трудно честно посмотреть на начало и забыть всё, что будет дальше. А просчитанная карточка содержит только материал до выбранного места.
Можно листать версии одного персонажа:
001
002
003
...
012
и смотреть, действительно ли он развивается.
Если между 003 и 012 карточка почти не изменилась, хотя персонаж всё это время активно присутствует в книге, стоит хотя бы проверить, что там произошло.
То же относится к местам и другим сущностям. Можно обнаружить, что вокруг какой-то башни несколько глав нагонялась важность, потом она выпала из рассказа и к ней никто больше не вернулся.
Это не автоматический литературный судья. Стабильный персонаж не обязательно плохой персонаж, а забытая на сто страниц локация может быть забыта специально. Но временные слепки делают такие вещи видимыми.
Получается возможность смотреть на книгу не только по главам, а поперёк текста - по одной сущности и её изменениям.
Слепки
Карточки не генерируются во время чтения. Книга перед публикацией полностью просчитывается и отдаётся читателю уже готовым файлом.
В исходном тексте автор ставит простую команду:
{sum marek}
Она означает только одно:
Здесь состояние Марека достаточно изменилось. Создай новый слепок.
Маркеры специально никак не нумеруются. Автору не нужно следить, какая сейчас версия персонажа и как должен называться следующий файл.
Сборщик сам идёт по книге сверху вниз:
первый {sum marek} -> 001.txt
второй {sum marek} -> 002.txt
третий {sum marek} -> 003.txt
...
шестой {sum marek} -> 006.txt
Для другого персонажа счёт начинается отдельно.
Количество точек полностью остаётся на усмотрение автора. У второстепенного персонажа может быть пять больших состояний на всю книгу. У главного - несколько десятков, иногда по несколько изменений за одну главу.
Это специально должно быть ненапряжно. В любой момент при написании можно решить: "Вот теперь персонаж уже заметно другой", поставить {sum marek} и продолжить текст.
Во время чтения читалка просто считает пройденные маркеры. Если читатель дошёл до шестого {sum marek}, ему показывается 006.txt. Пятый уже неактуален, седьмой ещё недоступен.
Скрытые патчи
Не всё, что известно автору о мире, хочется впихивать в основной текст.
Иногда это мелкая подробность. Иногда пасхалка. Иногда информация, которая поможет правильно понять персонажа, но будет выглядеть совершенно лишней внутри сцены. Для этого у каждой сущности есть собственный файл патчей.
Например:
entities/
marek/
patches.txt
summaries/
Сам файл может быть обычным секционным текстом:
[init]
Марек - придворный лекарь.
Ему около сорока лет.
Несколько лет работает при дворе.
[cave_chest]
Кроме описанного в книге предмета Марек заметил на дне сундука маленький стеклянный ключ.
Учитывать, что он хорошо запомнил его форму, но пока не понял назначения. Не делать ключ центральной темой карточки.
[final]
После событий рассказа Марек покинул столицу.
Патч может быть дополнительным куском содержания, сухим фактом или прямой инструкцией для саммаризатора. Это внутренний материал книги, читатель его напрямую не видит.
Названия патчей произвольные. cave_chest, first_visit, after_trial - любые понятные автору обозначения. init и final служебные.
Порядок секций в patches.txt технически не важен. Сборщик ищет патч по имени.
В основном тексте патч подключается отдельным маркером:
{patch marek cave_chest}
patch и sum никак не связаны между собой. Первый добавляет скрытый материал, второй создаёт слепок.
Например:
Марек открыл сундук и достал медальон.
{patch marek cave_chest}
Он закрыл крышку и вернулся к остальным.
...
{sum marek}
Пасхалки
Патчи позволяют сделать карточки не просто пересказом уже прочитанного.
Например, по основному тексту Марек открыл сундук, забрал оттуда медальон и пошёл дальше. Для сюжета этого достаточно.
Но патч cave_chest говорит, что он ещё заметил стеклянный ключ.
В самой книге эта подробность ничего не забивает. Большинство читателей вообще о ней не узнают. Но человек, который после сцены полез читать обновившуюся карточку Марека, может увидеть что-нибудь вроде:
Кроме медальона Марек обратил внимание на небольшой стеклянный предмет необычной формы. Его назначение осталось ему непонятно.
Через двести страниц внезапно выясняется, что такой ключ действительно важен.
Внимательный читатель мог заметить признак намного раньше и почувствовать себя очень умным Шерлоком. Остальные ничего не потеряли - основной текст остаётся полноценным сам по себе.
Карточки таким образом становятся дополнительным каналом повествования. Не обязательной второй книгой и не развилкой сюжета, а более подробным слоем того же мира.
Открытые патчи
Патч можно вставить не только в скрытый текст для саммаризатора, но и прямо в основную книгу:
{patch! hotel scene1}
Такой маркер заменяется содержимым секции scene1 из патчей сущности hotel. В готовой книге вставленный текст уже ничем не отличается от обычного текста автора.
Если нужного патча нет, маркер заменяется пустотой. Поэтому в основном тексте можно заранее оставить места для дополнительных сцен и фрагментов, которые существуют только в некоторых сборках книги.
Слои патчей
Патчи одной сущности могут храниться в нескольких файлах:
entities/
hotel/
patches-01.txt
patches-02-r.txt
patches-03-director.txt
При сборке они читаются по порядку и объединяются в один набор. Если один и тот же ключ определён несколько раз, используется версия из более позднего файла.
Например, scene1 может сначала содержать обычную сцену, затем вариант с другим возрастным рейтингом, а в следующем слое - режиссёрскую версию. В основном тексте при этом остаётся один маркер:
{patch! hotel scene1}
Так несколько редакций одной сцены могут жить рядом с книгой, не требуя отдельных копий основного текста. Для более мягкой, сокращённой или базовой сборки поздние файлы патчей просто не включаются в сборку. Процесс такой очистки оставляем за рамками статьи.
Скрытый текст
Обработка патчей специально максимально механическая.
Сначала сборщик вообще не использует нейросеть. Он проходит по книге и вместо каждого:
{patch marek cave_chest}
вставляет содержимое соответствующей секции cave_chest из:
entities/marek/patches.txt
Получается временная рабочая версия книги. В ней патчи уже неотличимы от остального контекста.
Условно было:
Марек открыл сундук и достал медальон.
{patch marek cave_chest}
Он закрыл крышку.
Стало:
Марек открыл сундук и достал медальон.
Кроме описанного в книге предмета Марек заметил на дне сундука маленький стеклянный ключ.
Учитывать, что он хорошо запомнил его форму, но пока не понял назначения.
Не делать ключ центральной темой карточки.
Он закрыл крышку.
Читатель никогда не увидит эту редакцию. Она существует только для сборки.
После этого саммаризатору вообще не нужно знать, что такое патчи. Для него есть один большой текст.
Когда встречается:
{sum marek}
берётся весь рабочий текст от начала книги до этого места и создаётся новый слепок Марека с упором на его актуальное состояние.
Результат сохраняется, например:
entities/marek/summaries/006.txt
Таким образом, логика сборки состоит почти из двух действий:
book + patch placeholders
↓
механически развёрнутый текст
↓
{sum entity}
↓
саммаризация текста от начала до этой точки
↓
готовая версия карточки
Init и final
Начальные сведения о персонаже - тоже обычный патч. Для этого зарезервировано имя:
[init]
init считается скрытым патчем в самом начале книги и попадает во все последующие слепки сущности. Сам по себе он не создаёт карточку и не делает сущность доступной читателю - это всегда происходит только после первого {sum ...}.
Если персонаж должен быть доступен с самого начала, автор ставит явную саммаризацию перед рассказом.
Текста книги перед маркером ещё нет, поэтому первый слепок собирается из init. Если такого маркера нет, сущность появится только после первого {sum ...} уже по ходу рассказа. Так персонажи, места и другие сущности можно вводить постепенно без отдельной логики показа и скрытия.
В конце автоматически применяется:
[final]
Он нужен для сведений, которые должны появиться только после завершения рассказа: дальнейшая судьба персонажа, постсюжетные детали и другие PS.
Технически это те же самые патчи. Отдельные механики для "описания персонажа в начале" и "послесловия персонажа" не нужны.
Исправление карточек
Готовые саммари можно редактировать руками. Если книга уже собрана и не будет пересчитываться, никто не мешает поправить неудачную формулировку прямо в 006.txt.
Но если проблема смысловая, лучше исправить источник.
Например, саммаризатор написал:
Марек понял, что стеклянный ключ связан с дворцом.
А по задумке он пока только что-то подозревает.
Тогда полезнее добавить в соответствующий патч:
На этом этапе Марек не знает назначения ключа.
Он только замечает возможное сходство с материалом дворцовых печатей.
Не описывать связь как установленный факт.
После новой саммаризации правильное ограничение сохранится.
Так патчи могут появляться не только во время написания сюжета, но и при проверке получившихся карточек.
Формат файла
Пока исходная структура выглядит примерно так:
book.txt
prompt.txt
entities/
marek/
patches.txt
summaries/
001.txt
002.txt
003.txt
...
final.txt
alina/
patches.txt
summaries/
001.txt
002.txt
...
final.txt
hotel/
patches-01.txt
patches-02-r.txt
patches-03-director.txt
summaries/
001.txt
002.txt
...
final.txt
prompt.txt содержит общую инструкцию для саммаризации всех сущностей книги.
Форматы самих файлов здесь не принципиальны. txt удобен как самый примитивный вариант для прототипа. Карточки потом могут быть Markdown, HTML или чем-нибудь ещё.
При публикации всё это можно упаковать в один файл-контейнер. По сути получится маленькая база данных книги: основной текст, скрытые патчи и заранее рассчитанные версии всех сущностей.
Читалке нейросеть не нужна вообще. Она только знает текущую позицию в тексте и выбирает нужный готовый файл.
Обычная книга всё ещё внутри
Мне важно, чтобы новый формат не заменял обычную книгу.
Основной текст остаётся обычным текстом. После обработки технических маркеров скрытые патчи и {sum ...} исчезают, а открытые патчи заменяются своим содержимым. Получается нормальная линейная книга.
Её можно перегнать в FB2, EPUB или другой обычный формат и читать без всех дополнительных функций. В худшем случае, если открыть сырой исходник неподходящей программой, среди текста иногда будут попадаться странные {sum marek}.
Весь дополнительный слой существует вокруг книги, но сама книга от него не зависит.
Здесь намеренно рассматривается только сама идея версионированных карточек сущностей. Форматирование книги, ссылки из текста на персонажей и места, сноски, иллюстрации, интерфейс читалки и другие способы обогащения пока оставлены за скобками. Они могут быть полезны и хорошо ложатся на такой формат, выходят за рамки статьи.
Пока вся основная механика сводится к трём командам:
{patch ENTITY NAME}
Подставить в эту точку скрытый авторский материал.
{patch! ENTITY NAME}
Подставить патч прямо в основной текст.
{sum ENTITY}
Сохранить здесь новое состояние сущности.
Всё остальное можно наращивать позже. Главное для меня сейчас - сама идея книги, у которой персонажи и места существуют не одной окончательной статьёй, а меняются вместе с положением читателя в рассказе.
Конвертер
Для работы с форматом понадобится несколько вспомогательных инструментов. Пока это выглядит не как большой сложный редактор, а как набор простых операций, которые можно сначала собрать даже в виде консольной утилиты. По этому описанию такой сборщик можно сделать практически на любом языке или поверх существующей утилиты для работы с нейромоделями. Писать книгу можно в любом удобном редакторе и пересобирать после завершения работы над очередной частью.
Перед сборкой файлы patches*.txt каждой сущности читаются по порядку и объединяются. Более позднее определение одинакового ключа заменяет раннее. Открытые патчи сразу подставляются в основной текст; если нужного ключа нет, маркер исчезает.
Программа проходит основной текст книги от начала до конца. Каждый раз, когда встречается маркер:
{sum marek}
она делает полный срез книги от начала текста до этого места.
Получившийся срез пока всё ещё содержит обычные маркеры патчей:
{patch marek cave_chest}
Следующим этапом отдельная функция проходит уже только по этому срезу и механически заменяет все такие маркеры содержимым соответствующих секций из патчей сущностей. init при этом считается скрытым патчем в нулевой позиции и добавляется в каждый слепок сущности.
После такой прошивки получается законченный рабочий слепок книги на конкретный момент. В нём есть основной текст только до текущего {sum marek} и все патчи, которые успели встретиться внутри этого участка книги. Никакие будущие патчи туда попасть уже не могут.
Затем в начало этого текста добавляется системный промпт из prompt.txt с инструкцией сделать актуальную карточку Марека. Весь получившийся ковёр отправляется на саммаризацию.
Результат сохраняется под следующим по порядку номером:
entities/marek/summaries/001.txt
entities/marek/summaries/002.txt
entities/marek/summaries/003.txt
Номера определяются автоматически по порядку маркеров {sum marek} в книге. Автор ими не занимается.
В упрощённом виде сборка выглядит так:
найти {sum ENTITY}
↓
взять срез книги от начала до этого маркера
↓
прошить патчи внутри полученного среза
↓
добавить системный промпт
↓
отправить весь слепок на саммаризацию
↓
сохранить следующий summaries/NNN.txt
После этого программа продолжает перебирать основной текст до следующего маркера саммаризации и повторяет тот же цикл.
Саммаризация
После подготовки текстового слепка книги остаётся превратить его в карточку нужной сущности. К этому моменту срез уже сделан, все патчи прошиты, а техническая обработка закончена.
К готовому слепку добавляется системный промпт из prompt.txt, после чего весь материал отправляется нейросети на саммаризацию.
Для первого прототипа промпт может быть совсем примитивным:
Составь актуальную карточку сущности <ENTITY> по переданному слепку книги.
Используй только сведения из переданного текста.
Учитывай весь доступный материал, но описывай прежде всего последнее известное состояние сущности.
Сохраняй важные ранее известные факты, если последующие события их не изменили.
Не додумывай отсутствующие сведения.
Не пересказывай весь сюжет - собери именно карточку сущности.
Верни только готовый текст карточки.
Для реальной книги такой промпт почти наверняка придётся уточнять по результатам первых сборок. Можно задать желаемый размер и структуру карточки, правила работы с противоречивой или неполной информацией, степень внимания к отношениям персонажей, состоянию мест и предметов, стиль текста и другие требования конкретного проекта.
Сам prompt.txt остаётся обычным исходным файлом книги. Его можно править вместе с текстом и патчами, а затем пересобирать карточки, пока результат не начнёт устраивать автора.
Авторазметка старых книг
Для портирования уже написанной книги на этот формат первичную расстановку маркеров {sum ...} тоже можно поручить нейросети. В начале запроса достаточно перечислить сущности и их ключи, которые нужно отслеживать, а затем попросить пройти текст и поставить маркер после тех абзацев, где состояние соответствующей сущности заметно изменилось.
Например:
Нужно разметить книгу маркерами саммаризации для следующих сущностей:
marek — Марек
alina — Алина
hotel — гостиница
glass_key — стеклянный ключ
Не переписывай и не сокращай исходный текст.
Пройди книгу последовательно от начала до конца. После абзаца, в котором состояние, положение, свойства, отношения или известная читателю информация о сущности заметно изменились, вставь отдельной строкой:
{sum KEY}
Используй только ключи из списка выше.
Не ставь маркер после каждого упоминания сущности и не отмечай мелкие изменения, которые не меняют её текущую карточку.
Если в одном месте заметно изменились несколько сущностей, поставь несколько маркеров.
Верни исходный текст с добавленными маркерами.
Так можно быстро получить первую разметку старой книги, а потом вручную убрать лишние точки или добавить пропущенные. Дальше она обрабатывается тем же сборщиком, что и книга, изначально написанная с такой разметкой.
Читалка
Готовая книга уже не требует нейросети: все состояния сущностей рассчитаны заранее. Читалка только следит за текущим местом в тексте и показывает подходящую версию карточки.
Интерфейс можно представить примерно как Obsidian: основной текст в центре, рядом список доступных персонажей, мест и других сущностей. На телефоне этот список можно убрать в боковую панель.
Если читатель прошёл очередной маркер {sum marek}, карточка Марека переключается на следующую версию. При возврате назад возвращается и старая карточка. Будущие версии заранее недоступны.
Сущность появляется в списке только после первого маркера саммаризации. Если она должна быть доступна с начала книги, первый {sum ...} ставится перед рассказом и строит карточку из init. В остальных случаях персонажи и места появляются постепенно вместе с сюжетом.
Остальное уже зависит от выбранного формата книги и интерфейса читалки. Для первого прототипа достаточно текста, списка сущностей и автоматического переключения их карточек по мере чтения.
Кросспрогрессия сущностей по книгам
Состояние сущности можно переносить между книгами, не меняя сам формат.
Для этого достаточно внешнего инструмента, который по заданному конфигу берёт последний слепок выбранной сущности из одной книги и превращает его в init другой.
Например, последний слепок Марека из первой книги может стать его начальным состоянием в третьей. Во второй книге Марека может не быть вообще, но его состояние всё равно хранится в init-патче. Когда в третьей впервые встретится {sum marek}, персонаж продолжит накапливать информацию с того места, где остановился раньше. Если предыдущие книги позже изменятся, достаточно снова прогнать конвертер, и начальное состояние Марека в следующих книгах тоже обновится.
То же относится не только к персонажам, но и к местам, предметам и другим сущностям со стабильной идентичностью.
Для самой книги ничего нового не появляется: никаких дополнительных маркеров или связей между томами. Кросспрогрессия остаётся вспомогательным инструментом автора для портов и бэкпортов состояния между книгами. Точная реализация зависит от целей автора и делается парой запросов к нейросети.