Читать нас в Telegram

В жизни мне довелось работать с юридическими документами и с кодом. В обеих областях текст не создается раз и навсегда. Он постоянно меняется, может одновременно существовать в разных версиях. Можно зайти в «КонсультантПлюс» или в Git, открыть историю изменений, сравнить редакции, увидеть эволюцию текста.

Художественные тексты тоже проходят подобную эволюцию. Наброски на салфетках превращаются в рукописи, обрастают правками, проходят через редактуру и цензуру, печатаются в первом, втором издании, бронзовеют в академическом. Однако большая часть этого процесса скрыта от читателя. Текст, который мы читаем в книге, за исключением редких примечаний, — лишь один кадр эволюции. Целиком ее наблюдают только исследователи-текстологи.

Было бы классно, подумалось мне как-то, если бы эволюцию текста можно было увидеть на одной интерактивной странице. Собрать разные версии текста в наглядной демонстрации, где можно будет переключаться между версиями, наблюдать, как они переходят друг в друга, видеть общую картину.

Как-то так я себе представлял этот проект в начале

Перед тем как писать код, нужно было найти подходящий текст на роль подопытного. Перебрав несколько вариантов, я остановился на «Ревизоре» Гоголя (для демо я взял явление 6 третьего действия, которое заканчивается знаменитым восклицанием «Лабардан! Лабардан!». Это произведение хорошо изучено, в академическом издании ему и его разным версиям посвящен целый том.

Постановка задачи

Сначала договоримся о понятиях и формализуем задачу. Надеюсь, мои упрощения не оскорбят профессиональных текстологов.

Выделим три основные сущности:

  • Версия (или редакция) — это цельный снимок текста в какой-то момент. Например, первая законченная рукопись «Ревизора» — это первая версия. Дальше Гоголь что-то вычеркивает, дописывает, меняет местами, в результате чего появляется вторая версия.
  • Правка — это минимальное изменение, которое можно применить к тексту: удалить или вставить фрагмент (перемещение можно представить в виде удаления в одном месте и вставки в другом).
  • Переход между версиями — это набор правок, который превращает одну версию («исходная версия») в следующую («целевая версия»).
Пример перехода между двумя версиями текста. Красным отмечено удаление, зеленым — вставки

Версии текста не обязательно следуют друг за другом: иногда эволюция идет сразу в нескольких направлениях

Удобно представить все версии текста в виде графа. Каждая версия — это отдельный узел, а переходы между связанными версиями — это ребра.

Карта версий «Ревизора» и она же в виде графа

В своей демонстрации я хотел визуализировать две вещи:

  1. Показать карту версий целиком: какие версии есть, как они связаны между собой, где мы сейчас на графе находимся.
  2. Сделать «плеер перехода», чтобы можно было выбрать конкретный переход и увидеть, как исходная версия шаг за шагом превращается в целевую.

В плеере мне хотелось сделать:

  • подсветку вставок и удалений (что-то вроде режима редактирования в текстовых редакторах);
  • отображение прогресса текущего перехода (чтобы было видно, например, что исходный текст превратился в целевой где-то наполовину или на три четверти);
  • подсветку текущего перехода на общей карте версий;
  • возможность выбирать, в какую версию переходить, если из одной редакции сделано несколько (например, вторая рукопись «Ревизора» легла в основу первого печатного издания, а также в основу рукописи Малого театра, по которой репетировалась постановка, — и эти версии отличаются между собой).

Что получилось

Демонстрация работы визуализатора

Вот ссылка на демонстрацию. Вы можете самостоятельно проследить за историей работы над «Ревизором».

Советы пользователям:

  • Конкретный шаг — это следующая правка. Переключаться между правками можно, кликая по стрелкам, перемещая слайдер или нажимая стрелки на клавиатуре.
  • Когда вы дошли до конца текущего ребра (т. е. одна версия полностью превратилась в другую), если дальше нет ветвления, следующий шаг переместит вас в начало следующего ребра, а если ветвление есть, надо будет выбрать, в какую версию будет превращаться ваша текущая.
  • Карта версий справа от слайдера показывает, какие версии есть, как они соединены, где вы сейчас, откуда и куда идете и какую часть ребра прошли. Ее можно масштабировать (кнопки — и + или колесиком мышки): при наведении на узел всплывает подсказка с подробной информацией о версии.
  • На карте можно кликнуть на любую версию — и сразу перейти в нее.
  • В настройках (значок шестеренки) можно менять размер текста, включать и отключать автопрокрутку за правками, выбрать режим подсветки правок.

А может быть, вы хотите не смотреть историю «Ревизора», а собрать демонстрацию для другого текста? Я расскажу, как это сделать, в конце статьи. А теперь расскажу о том, как я создавал этот инструмент.

Как я это делал

Как должен выглядеть результат, я себе представил — оставалось только написать код. Что, впрочем, вышло не сразу.

Попытки номер один и два

Сначала я попытался сделать универсальный комбайн, чтобы на одной веб-странице скрипт собирал тексты, подсчитывал различия между версиями и отображал их в браузере. Однако код быстро стал слишком громоздким и медленным, и от этой идеи пришлось отказаться.

Затем я решил разделить подсчет различий между версиями и визуализатор на независимые модули и стал писать первый. По ходу работы возникло много вопросов: как считать разницу, делать ли токенизацию, как хранить информацию о правках. На них было тяжело ответить, не зная, как будет работать визуализатор. А писать модуль визуализатора, не зная, что будет поступать на вход, было слишком сложно. Получился какой-то порочный круг.

Попытка номер три. Изобрети понятный формат, а все остальное приложится

В какой-то момент я сообразил, что нужно просто придумать формат, в котором будут храниться тексты и правки. И тогда сразу станет ясно, что должен делать и в какой форме отдавать сделанное первый модуль, а также как устроить визуализатор.

Дальше довольно быстро был набросан формат JSON-файла, в котором хранится текст версий и правки. В файле предполагалось три части:

  1. метаданные (автор, источник и другая информация о тексте);
  2. все версии текста (каждая версия представляет собой упорядоченный список токенов; токены — слова, знаки препинания и знак абзаца);
  3. списки правок для каждого перехода «исходная версия — целевая версия»: правки идут по очереди, сверху вниз, слева направо.
Упрощенная структура TDJ-файла с поясняющими комментариями

Формат был зафиксирован и гордо назван TDJ — Text Dynamics JSON. Остальная часть проекта уже была делом вайбкодинга.

Модуль подсчета разницы между текстами

Модуль подсчета разницы, или TDJ Builder, я написал на Python и устроил его следующим образом.

На вход модуль принимает папку с TXT-файлами версий, а также комментарий с информацией о редакциях и связях между ними в отдельном конфигурационном файле.

Упрощенный конфигурационный файл TDJ Builder (поле parent задает связи между версиями текста)

Далее скрипт берет конфигурационный файл, строит на базе него граф версий, обходит каждое ребро, берет тексты из левого и правого узла и автоматически строит последовательность правок на основе разницы между текстами.

Как работает TDJ Builder

Как вычисляется разница между текстами? Исходный и целевой тексты разбиваются на токены (каждый токен — это слово, знак препинания или граница абзаца, пробелы отбрасываются). Затем исходная и целевая последовательности токенов сравниваются с помощью SequenceMatcher из стандартной библиотеки difflib. Получившуюся разницу скрипт переводит в последовательность удалений и вставок, которую и сохраняет в TDJ-файл в разделе текущего ребра (конкретной связки целевой и исходной версий).

Как происходит сравнение версий и извлечение правок

Как я сам себе создал проблему

В первой версии TDJ-файла я решил представить текст не просто как последовательность токенов, а как последовательность абзацев, состоящих из токенов. То есть ввел лишнюю единицу структуры, которая не требовалась. Мне казалось, что так будет больше порядка. А в итоге из-за этого считать разницу между текстами стало на порядок сложнее.

Сначала надо было однозначно сопоставить между собой абзацы исходной и целевой версии (что уже превращается в нетривиальную задачу, если в одной из версий абзац был удален или добавлен), а затем подсчитывать разницу для каждой пары абзацев. И не дай Бог правка окажется распределенной между несколькими абзацами.

В общем, тут я тоже долго думал, писал костыли, пока не сообразил, что такой структурной единицы не требуется ни для TDJ Builder, ни для визуализатора. Всем им достаточно просто символа перевода строки как рядового токена. На душе сразу стало так легко, а где-то на небесах улыбнулся старина Оккам…

Пара слов об устройстве модуля визуализации

Модуль визуализации я сделал в виде HTML-страницы с несколькими JS-скриптами, обрабатывающими входящий файл формата TDJ. Один скрипт отрисовывает карту версий и следит, чтобы на ней было отражено текущее положение плеера. Другой скрипт визуализирует текущий переход между конкретными исходной и целевой версиями: отображает исходный текст и пошагово вносит в него правки сообразно положению слайдера.

Собственно, после некоторых доработок (сделал возможным ветвление, которого сначала не было, предусмотрел небольшой язык разметки для явного указания правок и возможности делать документ двухслойным, — все это описано в документации), поставленная задача была решена.

Так в итоге организован проект

Как посмотреть в плеере свой текст?

Конечно, я бы не стал писать два больших модуля, чтобы визуализировать работу над одним лишь фрагментом «Ревизора». Модуль визуализации поддерживает возможность ставить пользовательские демо. Главное, чтобы они соответствовали формату TDJ.

  • Собираете TXT-файлы с разными версиями одного текста (также в одном документе, как я уже писал выше, с помощью разметки можно хранить два слоя);
  • Скачиваете TDJ Builder (https://github.com/zhenyacode/tdj_builder);
  • Собираете в папку TXT-редакции;
  • Пишете конфигурационный YAML;
  • Запускаете скрипт;
  • Полученный файл отдаете в визуализатор (кнопка загрузки файла расположена слева от кнопки настроек с шестеренкой).

Что дальше?

Надеюсь, вам было интересно, и вы попробовали поиграться с плеером версий. Если у вас возникли замечания или предложения, напишите мне [dunenkov.evgeniy@gmail.com]. Если вы филолог, историк или просто исследователь-любитель, я буду рад обратной связи, так как хотел бы сделать полезный инструмент. Также приветствуются советы по поводу структуры формата TDJ. Мне кажется, он может быть гораздо удобнее, но пока у меня нет свежих идей по этому поводу.

Ссылки