Перейти к содержимому
Ghostchars

Как Ghostchars решает, что удалять

Один проход по тексту, одно решение на символ и понятная причина у каждого решения. Ни модели, ни эвристик, ни оценок.

Четыре прохода

Очистка идёт в жёстком порядке. На отдельные символы смотрит только третий проход; первые два нужны, чтобы третий получил предсказуемую строку.

  1. Нормализация (по желанию)

    Если вы попросили, текст сначала приводится к NFC или NFKC. Именно это побеждает стеганографию на формах нормализации, где сообщение закодировано выбором между «é» одной кодовой позицией и «e» плюс комбинируемый акцент. NFKC идёт дальше и сводит стилизованные математические буквы, лигатуры и полноширинные формы; это потеря информации, поэтому по умолчанию выключено.

  2. Сведение концов строк и пробельных серий (по желанию)

    CRLF и CR становятся LF; полоса цитаты из терминала в начале строки уходит вместе с пробелом после неё; длинное тире с пробелами становится дефисом с пробелами; пробелы и табуляции в конце каждой строки срезаются; серии пробелов внутри строки схлопываются в один. Это переписывание по шаблону для всей строки, а не решения по каждому символу, и каждое вынесено в свой параметр, потому что каждое меняет видимый текст.

  3. Классификация каждого символа

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

  4. Сборка с учётом контекста

    Результат собирается символ за символом. Символ, чей управляющий параметр выключен, копируется как есть и попадает в отчёт как найденный, но не тронутый. Символ с включённым параметром удаляется или заменяется. Всё это время движок помнит последний символ, который он действительно СОХРАНИЛ, а не последний увиденный, потому что именно к нему прикрепляется соединитель или селектор вариантов, а склейка основу не сдвигает.

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

Порядок принятия решений

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

#По категориямУправляющий параметр
1Нулевая ширина и невидимыеБазовый проход (всегда включено)
2Переопределения направленияБазовый проход (всегда включено)
3Метки направленияBidi-метки --bidi
4Соединители, селекторы и заполнителиАгрессивно -a
5Любой другой форматирующий символБазовый проход (всегда включено)
6Символы частного использованияСимволы частного использования --pua
7Экзотические пробелыПробелы -s
8Разделители строкКонцы строк -n
9Управляющие символыУправляющие символы --controls
10Типографская пунктуацияПунктуация --punct
11Полосы цитат из терминалаПолосы цитат --gutters
12Буквы-двойникиБуквы-двойники --confusables

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

Почему решает контекст

Четыре семейства символов невидимы И несут нагрузку: соединители, селекторы вариантов, заполнители отдельных письменностей и метки направления. Одна и та же кодовая позиция в одном месте оказывается контрабандой, а в другом правильным написанием, поэтому движок сначала смотрит на соседей. Именно здесь ошибается большинство чистильщиков, причём в обе стороны: они либо оставляют спрятанные символы, либо ломают настоящие.

Эмодзи-семья

Эмодзи-семья складывается из трёх или четырёх человек, сваренных вместе соединителями нулевой ширины. Уберите их, и одно эмодзи станет тремя. Ghostchars сохраняет соединитель, у которого с обеих сторон стоят базовые эмодзи, и удаляет ту же кодовую позицию, когда она оказывается между двумя латинскими буквами: там она ничего не соединяет и хорошо прячется.

Персидское слово

В персидском продолженные формы и множественное число пишутся через разъединитель нулевой ширины: это не украшение, это орфография. Ghostchars сохраняет соединитель или разъединитель, если символы с обеих сторон принадлежат одной соединяющей письменности (арабской, персидской, деванагари, малаялам), и удаляет его во всех остальных случаях. То же правило защищает кхмерские гласные, монгольские селекторы и заполнители чамо хангыля рядом с их собственными письменностями.

Флаг Шотландии

Флаг региона состоит из чёрного флага, за которым идут теговые символы с кодом региона и завершающий тег. Те же теговые символы, стоящие сами по себе, дают атаку ASCII smuggling: невидимые инструкции, которые прочитает языковая модель. Ghostchars разбирает всю последовательность: теговые символы внутри полного флага сохраняются, а каждый теговый символ вне флага удаляется.

Предложение на иврите

Метками «слева направо» и «справа налево» текст со смешанным направлением удерживает пунктуацию на правильной стороне. Ghostchars сохраняет bidi-метку, у которой в соседях стоит символ письма справа налево, и удаляет ту, что затерялась в английском абзаце, где ей остаётся единственная функция: нести бит. Пять символов переопределения и встраивания такого доверия не получают: они способны переставить текст, который писали не вы, поэтому удаляются всегда.

Каждое такое решение попадает в отчёт. Сохранённый символ показан как «оставлен: несёт нагрузку» вместе с параметром, который всё-таки его удалит, так что ничего не решается молча и ничего от вас не скрывается.

Проверено на одиннадцати опубликованных схемах

В инструменте командной строки есть состязательный тест, который реализует одиннадцать документированных техник сокрытия текста парами «кодировщик и декодировщик». Для каждой он прячет секрет, очищает текст и проверяет две вещи: что декодировщик больше не может достать секрет и что видимый текст-обёртка остался побайтово тем же.

СхемаЧем побеждается
Zero-width binary (ZWSP / ZWNJ / ZWJ)базовый проход
Word-joiner and invisible-maths binaryбазовый проход
Tag-block ASCII smugglingбазовый проход
Variation-selector byte smugglingбазовый проход
Bidi mark binary (LRM / RLM)базовый проход
SNOW trailing whitespaceПробелы в конце строк --trailing
Cyrillic homoglyph binaryБуквы-двойники --confusables
NFC / NFD normalisation-form binaryНормализация (NFC) --nfc
NBSP / space homoglyph binaryПробелы -s
Single / double space binaryСхлопывание пробелов --collapse
Curly / straight apostrophe binaryПунктуация --punct

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

Один движок в двух местах

Браузерный движок повторяет инструмент командной строки на Python строка в строку, а не переосмысливает его.

Генератор прогоняет реализацию на Python по корпусу фикстур (каждая перечисленная кодовая позиция, каждый контекстный случай, все одиннадцать стеганографических нагрузок, реалистичная смешанная проза) в четырнадцати сочетаниях параметров и записывает очищенный вывод и каждую находку. Движок на TypeScript тестируется против этой записи. Любое расхождение между ними роняет сборку, поэтому «сайт делает то же, что и CLI» остаётся результатом теста, а не обещанием.

Это важно по практической причине: вы можете проверить файл в pre-commit-хуке через CLI и получить ровно тот результат, который дал бы сайт.

Тот же движок работает и в третьем месте. Отчёт по документу распаковывает .docx или .odt и читает XML внутри, а не вставленную строку, поэтому символ получает в файле тот же вердикт, что и в поле на главной странице, а скрытые фрагменты, исправления и метаданные вокруг него попадают в отчёт, но не удаляются.

Чего это не умеет

Всё вышесказанное говорит о символах. Эти четыре вещи к символам не относятся, и никакая очистка их не касается.

  • Статистические водяные знаки: Схемы вроде SynthID-Text и метода «зелёного списка» Кирхенбауэра смещают то, какие СЛОВА выбирает модель. Сигнал живёт в выборе слов на протяжении целого фрагмента, а не в отдельном символе. Изменить его может только переписывание текста, а этот инструмент текст не переписывает.
  • Стилометрическое определение: Детекторы вроде GPTZero оценивают, насколько предсказуемо каждое слово и насколько сильно эта предсказуемость колеблется. Удаление пробела нулевой ширины не сдвинет эту оценку ни на балл. Отчёт о привычках письма измеряет стоящие за ней привычки, которые можно править (разброс длин предложений, слова-приметы, дежурные связки), и всё равно отказывается выдавать вероятность.
  • Сокрытие на уровне формата: Белый текст по белому, скрытые фрагменты, комментарии HTML и метаданные PDF прячутся ВНЕ текста. Инструмент для документов сообщает о них (скрытые фрагменты, исправления, примечания, идентификаторы сеансов правки, метаданные об авторстве), но намеренно их не удаляет, потому что удаление меняет документ, а это должно быть вашим решением.
  • Каналы меньше бита на эмодзи: Цепочка соединителей между эмодзи и селектор вариантов после эмодзи сохраняются по умолчанию, потому что так эмодзи и устроены. Настойчивый отправитель всё же может спрятать там немного данных. Включите «Агрессивно», если они должны исчезнуть, и примите, что ваши эмодзи развалятся.

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

Вопросы

Почему просто не удалить все невидимые символы?

Потому что некоторые из них несут нагрузку. Удаление всех соединителей нулевой ширины превращает эмодзи-семью в трёх отдельных людей и портит написание персидских слов. Удаление всех bidi-меток ломает вёрстку смешанного текста на иврите и английском. Чистильщик, который делает это молча, хуже, чем никакого: вы не видите, что он забрал.

Браузерная версия совпадает с инструментом командной строки?

Да, и это проверяется тестами. Эталонные фикстуры, сгенерированные реализацией на Python, привязывают движок на TypeScript к побайтово одинаковому выводу для каждой фикстуры и каждого сочетания параметров; сборка падает при любом различии.

Он может сказать, написан ли текст ИИ?

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

Мой текст покидает браузер?

Нет. Никакой обработки на стороне сервера нет, и отправлять некуда: движок написан на JavaScript и работает на вашем устройстве, а сайт остаётся набором статических файлов. Отключитесь от сети, и всё продолжит работать.

Анонимная статистика?

Ghostchars обрабатывает ваш текст в браузере и никуда его не отправляет. Мы хотели бы считать просмотры страниц через собственный сервер аналитики, почти без cookie: содержимое не собирается никогда.