From 87aea194ec8c17415a87aeb242f969a425c18367 Mon Sep 17 00:00:00 2001 From: Ilya Utov Date: Wed, 9 Sep 2026 13:56:53 +0300 Subject: [PATCH] feat: add humanizer-ru plugin Removes AI-generation markers from Russian text: 64 patterns, 21 hard bans, deterministic 0-100 scanner. Adds marketplace entry, skills/ symlink and README section. Source: https://github.com/ilyautov/humanizer-ru (MIT, v3.21.0). --- .factory-plugin/marketplace.json | 6 + README.md | 10 + .../humanizer-ru/.factory-plugin/plugin.json | 13 + plugins/humanizer-ru/README.md | 23 + .../humanizer-ru/skills/humanizer-ru/SKILL.md | 320 +++++++++++++ .../skills/humanizer-ru/agents/openai.yaml | 5 + .../skills/humanizer-ru/edit-log.md | 40 ++ .../skills/humanizer-ru/references/catalog.md | 354 ++++++++++++++ .../scripts/humanizer_metrics/__init__.py | 100 ++++ .../scripts/humanizer_metrics/burstiness.py | 92 ++++ .../scripts/humanizer_metrics/facts.py | 193 ++++++++ .../scripts/humanizer_metrics/markdown.py | 127 +++++ .../scripts/humanizer_metrics/markers.py | 442 ++++++++++++++++++ .../scripts/humanizer_metrics/morphology.py | 78 ++++ .../scripts/humanizer_metrics/score.py | 182 ++++++++ .../scripts/humanizer_metrics/structure.py | 154 ++++++ .../skills/humanizer-ru/scripts/scan.py | 208 +++++++++ skills/humanizer-ru | 1 + 18 files changed, 2348 insertions(+) create mode 100644 plugins/humanizer-ru/.factory-plugin/plugin.json create mode 100644 plugins/humanizer-ru/README.md create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/SKILL.md create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/agents/openai.yaml create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/edit-log.md create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/references/catalog.md create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/__init__.py create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/burstiness.py create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/facts.py create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/markdown.py create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/markers.py create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/morphology.py create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/score.py create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/structure.py create mode 100644 plugins/humanizer-ru/skills/humanizer-ru/scripts/scan.py create mode 120000 skills/humanizer-ru diff --git a/.factory-plugin/marketplace.json b/.factory-plugin/marketplace.json index 4282159..51fa1f0 100644 --- a/.factory-plugin/marketplace.json +++ b/.factory-plugin/marketplace.json @@ -53,6 +53,12 @@ "description": "Pull request lifecycle skills: create PRs with consistent conventions and follow up on them until merge-ready", "source": "./plugins/code-review", "category": "productivity" + }, + { + "name": "humanizer-ru", + "description": "Remove AI-generation markers from Russian text: 64 patterns, 21 hard bans, rewrite / audit / targeted-fix modes, and a deterministic scanner", + "source": "./plugins/humanizer-ru", + "category": "writing" } ] } diff --git a/README.md b/README.md index 3ef0559..1943fc9 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,16 @@ Skills for continuous learning and improvement. Autonomous experiment loop for optimization research. Try an idea, measure it, keep what works, discard what doesn't, repeat. Works standalone or as a mission worker. +### humanizer-ru + +Remove AI-generation markers from Russian text and make it read like a human wrote it. Russian-language counterpart to `human-writing`. + +**Skills:** + +- `humanizer-ru` - 64 AI-text patterns across 14 categories, 21 hard bans, rewrite / audit / targeted-fix modes, optional deterministic 0-100 scanner (`scripts/scan.py`) + +See [plugins/humanizer-ru/README.md](plugins/humanizer-ru/README.md) for details. + ## Plugin Structure Each plugin follows the Factory plugin format: diff --git a/plugins/humanizer-ru/.factory-plugin/plugin.json b/plugins/humanizer-ru/.factory-plugin/plugin.json new file mode 100644 index 0000000..2b39287 --- /dev/null +++ b/plugins/humanizer-ru/.factory-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "humanizer-ru", + "description": "Remove AI-generation markers from Russian text: 64 patterns across 14 categories, 21 hard bans, rewrite / audit / targeted-fix modes, and a deterministic 0-100 scanner", + "version": "3.21.0", + "author": { + "name": "Ilya Utov", + "email": "ilyautov@gmail.com" + }, + "homepage": "https://humanizer-ru.aifrontier.tech/", + "repository": "https://github.com/ilyautov/humanizer-ru", + "license": "MIT", + "keywords": ["humanizer", "russian", "writing", "ai-detection", "text", "русский", "очеловечивание"] +} diff --git a/plugins/humanizer-ru/README.md b/plugins/humanizer-ru/README.md new file mode 100644 index 0000000..fae32ce --- /dev/null +++ b/plugins/humanizer-ru/README.md @@ -0,0 +1,23 @@ +# humanizer-ru + +Removes AI-generation markers from Russian text and makes it read like a human wrote it. The skill is a Russian-language counterpart to `human-writing`: it works only with Russian and routes English text to the English skill. + +## Skills + +### `humanizer-ru` + +Catalog of 64 AI-text patterns across 14 categories (bureaucratic phrasing, calques from English, em-dash overuse, rule of three, hollow intensifiers, rhythm flatness and others) plus 21 hard bans. Three modes: full rewrite, audit only, targeted fix. Keeps source facts locked (numbers, dates, names, links are carried over unchanged) and calibrates the voice to the author's samples. + +A deterministic scanner ships with the skill (`skills/humanizer-ru/scripts/scan.py`): it scores a text 0-100, lists the markers it found and compares before/after versions. It is optional; the skill works from the catalog alone when Python or the `razdel` and `pymorphy3` packages are not available. + +Thresholds were calibrated on 55K labeled Russian texts (human vs. generated). + +## Install + +```bash +droid plugin install humanizer-ru@factory-plugins +``` + +## Source and license + +Upstream repository: https://github.com/ilyautov/humanizer-ru (MIT). Site with examples and an online audit: https://humanizer-ru.aifrontier.tech/ diff --git a/plugins/humanizer-ru/skills/humanizer-ru/SKILL.md b/plugins/humanizer-ru/skills/humanizer-ru/SKILL.md new file mode 100644 index 0000000..6e23454 --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/SKILL.md @@ -0,0 +1,320 @@ +--- +name: humanizer-ru +description: "Скилл для очеловечивания русскоязычного текста. Убирает признаки AI-генерации, делает текст живым. Используй ВСЕГДА, когда пользователь просит: очеловечить текст, убрать следы нейросети, сделать текст живым/естественным, переписать как человек, humanize на русском, убрать канцелярит, убрать водянистость, сделать текст менее формальным. Также используй если пользователь вставляет русскоязычный текст и говорит что-то вроде 'перепиши', 'сделай лучше', 'звучит как робот', 'слишком искусственно'. Работает ТОЛЬКО с русским языком. Для английского используй оригинальный humanizer. НЕ используй для: перевод, написание с нуля, грамматика, код." +license: MIT +--- + +# Humanizer-RU v3.21.0 + +Ты редактор. Превращаешь стерильный AI-текст в живую русскую речь. Убираешь маркеры нейросети и возвращаешь в текст автора: мнение, ритм, характер. + +Хороший русский текст неровный. Спотыкается, перебивает сам себя, ускоряется и замедляется. AI-текст гладкий и никакой, как музак в лифте. + +## Фундаментальный принцип: статистическое отклонение + +> LLM выбирает статистически наиболее вероятное продолжение текста. Результат стремится к самому типичному варианту, применимому к наибольшему числу случаев. + +Очеловечивание = намеренное отклонение от статистической нормы. Каждый выбор слова, каждый поворот фразы, каждый ритмический сбой - это выбор МЕНЕЕ вероятного, но БОЛЕЕ характерного варианта. AI пишет «Это имеет важное значение». Человек пишет «Это меняет всё» или «Ну и что?» - зависит от автора. Оба варианта менее вероятны статистически, но оба несут характер. + +Держи этот принцип в голове при каждом решении: «AI выбрал бы самый типичный вариант. Какой вариант выбрал бы ЭТОТ конкретный автор?» + +Два ключевых наблюдения о русском AI-тексте: +- **LLM предпочитает существительные глаголам.** Noun/verb ~3:1 у AI, ~2:1 у людей. Instruction tuning усиливает перекос. Люди заякоривают язык в глаголах (время, вид, наклонение), AI - в noun phrases. +- **LLM обрабатывает русский через English-biased representations.** Кальки с английского в AI-русском - не случайные ошибки, а артефакт архитектуры. Translationese неизбежен. Это объясняет, ПОЧЕМУ паттерны 7 (кальки) и 8 («является») так устойчивы. + +## Что именно ловят детекторы (2025-2026) + +Детекторы (GPTZero, Originality.ai, DivEye, RuBERT) измеряют три вещи: + +1. **Perplexity (предсказуемость).** Насколько каждое следующее слово предсказуемо. AI-текст имеет низкую perplexity: каждое слово «ожидаемо». Человеческий текст дёргается: предсказуемое слово, неожиданное, снова предсказуемое. + +2. **Burstiness (всплески).** Вариативность структуры по документу. AI пишет равномерно: все предложения ~одной длины, ~одной сложности. Человек чередует: длинное сложное, короткое рубленое, вопрос, снова длинное. + +3. **Морфологическая корректность (для русского).** RuBERT-детекторы дополнительно проверяют: падежные согласования, род, вид глагола, ритм ударений. AI ошибается в морфологии иначе, чем люди. Люди путают -тся/-ться, AI путает падежи в длинных цепочках. + +Задача хуманизации: поднять perplexity (менее предсказуемые слова), поднять burstiness (разнообразие структуры), сохранить морфологическую чистоту. + +Конкретные числа: +- DivEye (2025): вторые производные surprisal дают **39.4% вклада** в детекцию - больше, чем любой другой тип фич. +- Perplexity gap: даже при 99.9% style match по человеческим оценкам, средняя perplexity человеческого текста **29.5** vs **15.2** у LLM. Детекторы это видят. +- NeurIPS 2025: adversarial paraphrasing снижает true positive rate на 87.88%. Но появляются perturbation-invariant методы (PIFE, 2025), которые сохраняют 82.6% TPR даже после sophisticated атак. Простой парафраз больше не спасает. +- Мат и резкая брань у AI почти не встречаются. Глаголы восприятия («смотреть», «слышать»), слова страха, гнева, ненависти у людей попадаются заметно чаще. + +> **Domain shift:** Детекторы не обобщаются между доменами (март 2026). Модель, натренированная на научных текстах, плохо ловит блог-посты, и наоборот. Самые информативные фичи для одного домена бесполезны для другого. Практический вывод: чем сильнее текст привязан к конкретной нише (жаргон, формат, стиль аудитории), тем труднее его детектировать. Это дополнительный аргумент за голосовую калибровку и доменную адаптацию. + +Для русского: последний русскоязычный бенчмарк, AINL-Eval 2025 (52K текстов, 12 доменов). Лучший результат на тесте около 86%. Принципы (surprisal, burstiness) языконезависимы, но пороговые значения для русского не откалиброваны. + +## Операционный принцип: контрастное вычитание + +> Исследования (CoPA, EMNLP 2025) показали: самый эффективный способ очеловечить текст: не убирать маркеры по списку, а в каждом предложении найти САМОЕ ПРЕДСКАЗУЕМОЕ слово и заменить его на менее вероятное, но уместное для конкретного автора. + +Предсказуемое ≠ формальное. «Решение» в контексте «нашли решение проблемы», предсказуемое. «Выход», «лазейка», «костыль» - менее вероятные, но характерные. Один такой выбор на предложение даёт больше, чем три стилистические правки. Это дополнение к каталогу паттернов, не замена: сначала убери HARD BANS, потом пройдись контрастным вычитанием. + +> **Uncertainty gap** (2026): формализованный разрыв - человеческий текст последовательно менее предсказуем, чем AI-текст, и это напрямую коррелирует с качеством. Instruction tuning и reasoning модели УСИЛИВАЮТ предсказуемость. Контрастное вычитание - прямой способ закрыть этот разрыв. Лучшие атаки 2025-2026 идут через style transfer (MASH: 92% ASR), не через парафраз, что подтверждает подход скилла: голосовая калибровка (Шаг 2) + контрастное вычитание > механическая замена маркеров. + +--- + +## Как звучит живой текст + +Каталог (`references/catalog.md`) почти весь про то, что УБРАТЬ. Но цель не стерильность, а характер. Держи в голове позитивный образ, к которому правишь, иначе вычистишь маркеры и получишь гладкое ничто (а это тоже детектируется). + +Живой русский текст: +- **Имеет автора.** Где-то проступает мнение, раздражение, азарт, сомнение. Читатель чувствует, что за текстом человек, у которого есть позиция, а не «сбалансированный обзор». +- **Дышит неровно.** Короткое предложение бьёт. Потом длинное, с оговорками, со скобками, с уходом в сторону и возвратом. Плотный абзац с цифрами, следом лёгкая ремарка. +- **Конкретен.** Не «многие компании», а «три проекта». Не «эксперты считают», а «я попробовал, не сработало». Имена, числа, случаи, но только те, что есть в исходнике или сообщены пользователем (см. «Факт-замок» в Шаге 3): выдумывать конкретику нельзя. +- **Допускает шероховатость.** Повтор слова вместо синонима-через-силу. Разговорный оборот. Иногда частица «же», «вот», «ну». Иногда дефис вместо идеального тире. +- **Говорит фигурально.** Подколка, преуменьшение, метафора из жизни, идиома к месту. Там, где AI скажет буквально, человек ввернёт образ. + +Не добавляй это механически и поровну в каждый абзац: будет так же искусственно, как канцелярит. Добавляй там, где автор реально бы оживился. + +--- + +## Режимы работы + +**Полное редактирование** (по умолчанию). Все 5 шагов, полный каталог паттернов. Для текстов, которые нужно привести к человеческому виду. + +**Аудит** (по запросу: «проверь», «найди AI-маркеры», «что выдаёт?»). Только диагностика. Возвращаешь список найденных паттернов с примерами из текста и приоритетом (A-D). Текст не переписываешь. + +> **Машинная половина аудита.** Со скиллом едет детерминированный сканер: `scripts/scan.py` рядом с этим SKILL.md (в полном клоне репозитория: `skills/humanizer-ru/scripts/scan.py`). Он точно считает то, что LLM мерит на глаз: HARD BANS, 21 категорию маркеров, ритм предложений, морфологию. Если в среде можно запускать команды и есть python3 с пакетами `razdel` и `pymorphy3`, сначала прогони текст через него: `python3 <папка скилла>/scripts/scan.py файл.txt`, либо `echo "текст" | python3 <папка скилла>/scripts/scan.py -` (флаг `--json` для машинного вывода). Текст не маркетинговый? Добавь `--genre academic`, `--genre legal` или `--genre fiction`: флаг снимает маркеры, законные для регистра, иначе научный и юридический текст тонет в ложных срабатываниях (цифры в секции «Ложные срабатывания»). Вывод сканера бери как базу, поверх добавляй то, что скрипту не по зубам: ложные срабатывания (см. одноимённую секцию), нелексические паттерны, приоритеты A-D. Пакетов нет? Предложи пользователю однократный `pip install razdel pymorphy3` и проводи аудит по каталогу вручную. Сканер недоступен или упал: молча работай по каталогу, как обычно. Аудит обязан состояться при любом раскладе. + +> **Score чистоты (0-100).** Сканер выдаёт сводную оценку `ЧИСТОТА: N/100` и полосу: 85-100 «чисто» (следы ИИ не мешают), 60-84 «точечная правка», ниже 60 «рерайт». Это не вероятность ИИ и не «детект», а агрегат уже посчитанных сигналов: хард-баны и плотность маркеров весят сильно, морфология (сущ./глаг.) и тире слабо, как корроборирующие, чтобы не занижать плотный энциклопедический или юридический текст. Плюс document-level сигналы (ровные по длине абзацы, засилье однотипных пунктов-списков) для длинных шаблонных текстов: на короткой прозе они инертны. Показывай число в начале аудита, а после переписывания давай «было N, стало M»: так правка становится измеримой. Сканер не запускался (нет python, claude.ai web)? Прикинь score по той же логике: старт 100, заметный минус за каждый хард-бан и за высокую плотность маркеров, небольшой за номинальность и обилие тире; плотный человеческий текст без AI-оборотов остаётся высоким, не занижай его. Такую прикидку подписывай явно: «≈70, оценка без сканера». Число от сканера и число на глаз в одном формате путают читателя, а измеримость правки наше главное отличие. + +**Точечная правка** (по запросу: «исправь только X», «убери канцелярит»). Работаешь только с указанной категорией паттернов. Остальное не трогаешь. + +## Классификация текста + +Перед работой определи тип текста, от него зависит интенсивность правки: + +| Тип | Интенсивность | Что трогать | Что НЕ трогать | +|---|---|---|---| +| Маркетинг / соцсети | Максимальная | Все 64 паттерна + HARD BANS + тональность | - | +| Экспертный контент (Habr, статьи) | Высокая | A-C паттерны, голос, конкретика | Терминологию, структуру если оправдана | +| Деловая переписка | Средняя | A-B паттерны, канцелярит, водянистость | Формальный регистр, вежливые обороты | +| Научный / академический | Низкая | Раздувание значимости, мотивационные клише, неодушевлённый субъект (#55) | «Является», кальки, канцелярит, «данный», длинное тире, «таким образом»: в этом регистре это норма, а не машина | +| Документация / техтексты | Низкая | Только A паттерны + грубые ошибки | Структуру, терминологию, формат | +| Юридические тексты | Минимальная | Только фактические ошибки | Всё остальное (формулировки имеют юр. силу) | +| Цитаты внутри текста | Нулевая | Ничего | Всё (цитата = чужой текст) | + +Для коротких текстов (<100 слов): не перегружай правками, достаточно убрать 2-3 главных маркера. +Для текстов на смеси языков: работай только с русскоязычными фрагментами. +Если текст уже хорош: скажи об этом. Не правь ради правки. + +## Приоритеты паттернов + +| Группа | Уровень | Паттерны | Когда исправлять | +|---|---|---|---| +| A | Критические | HARD BANS, пустые открытия (1), канцелярит (6), артефакты чатбота (22), негативные параллелизмы (38), **модальная неопределённость (46), псевдо-терапия (52)** | ВСЕГДА, в любом режиме | +| B | Высокие | Размытые авторитеты (2), кальки (7), **пунктуационные кальки (7б)**, «является» (8), водянистость (26), «определённый»/«соответствующий» (30; «данный» уже в HARD BANS), эмоциональная стерильность (31), равномерная плотность (43), гладкие переходы (44), **macro-burstiness (45), translationese (47), рваная медитативность (49), эмодзи-декор (51), мораль в финале (59), портретный ввод героя (60)** | Во всех режимах кроме юридических | +| C | Средние | Раздувание (3), формульные выводы (4), синтаксис (11), правило трёх (12), карусель (13), тире (15), оговорки (25), частицы (32), **нет идиом (48), контр-вопросы (50), дискурс: эмоция-тело (61), причинность (62), хронология (63), обращение к читателю (64)** | В полном редактировании и экспертном; 61 и 64 только предлагать автору, молча не править | +| D | Стилистические | Болд (16), орфомелочи (17-18), кавычки (21), списки (19), пунктуация (20), грамотность (36), типографика (37) | По контексту, не обязательно | + +При ограниченном времени или токенах: исправляй сверху вниз (A → B → C → D). + +## Ложные срабатывания + +Живой текст тоже бывает гладким, тоже строит триады, тоже использует тире. Не каждый маркер из каталога приговор. Прежде чем править, проверь, не human ли это по-настоящему. Переусердствование портит хороший текст и само детектируется (одинаковая «вылизанность» тоже отпечаток). + +НЕ трогай, если: +- **Триада осмысленная.** «Пришёл, увидел, победил»: три реальных события, а не правило трёх ради ритма. Бей только искусственные триады синонимов («важный, значительный и ключевой»). +- **Тире авторское и единичное.** Одно длинное тире на весь текст в сильной позиции: стиль, а не маркер. В Аудите бей частоту (тире в каждом втором предложении), а не само существование, и не выдумывай проблему там, где её нет. При переписывании HARD BAN на тире действует как обычно, если пользователь явно не разрешил тире. +- **Повтор намеренный.** Анафора, рефрен, повтор для нажима («Он не пришёл. Он не позвонил. Он просто исчез.»): приём, а не синонимическая бедность. +- **«Является» в роли термина/определения.** В научном или юридическом тексте связка иногда обязательна. Бей бытовое «является», не трогай терминологическое. +- **Академический регистр целиком.** Это не смягчение, а жёсткое правило, и оно измерено. Прогон каталога по корпусу AINL-Eval 2025 (35 158 русских научных аннотаций с разметкой «человек» и три модели) дал 35,9% человеческих текстов с баном против 26,1-35,8% машинных: без жанровой поправки скилл ловит людей не реже, чем нейросети, то есть не различает их вовсе. «Является» сработало на 8,0% человеческих аннотаций и 0,1-3,0% машинных, длинное тире на 4,8% человеческих и 0-0,9% машинных, кальки на 31,1% человеческих и 3,6-21,1% машинных. С жанровой поправкой человеческие срабатывания падают до 4,2%. В научном и академическом тексте НЕ считай признаком: «является», кальки-связки, канцелярит, «данный», длинное тире, «таким образом». Разделяющая сила остаётся у раздувания значимости, мотивационных клише, модальных хеджей и неодушевлённого субъекта (#55): их проверяй как обычно. Сканер поднимает то же правило флагом: `scan.py файл.txt --genre academic` (ещё `legal` и `fiction`). +- **Структура оправдана длиной.** В лонгриде на 3000 слов подзаголовки и списки нужны. Подтип «подзаголовок на каждые 2-3 предложения» касается КОРОТКОГО текста, не статьи. +- **Формальный регистр как требование жанра.** Деловое письмо, оффер, документ имеют право быть формальными. Канцелярит убирай, но вежливые обороты и регистр не ломай. +- **Гладкость от мастерства, а не от AI.** Хороший редактор тоже выравнивает текст. Если перплексия низкая, но есть мнение, конкретика и характерный голос, перед тобой, скорее всего, человек. Низкая перплексия сама по себе не улика. + +Тест на переусердствование: если после правки исчезли личные примеры, разговорные обороты, авторская позиция, значит ты не очеловечил, а обезличил. Откатись. + +--- + +## Процесс (5 шагов + quad-pass аудит) + +**Шаг 1. Диагностика + сегментная разметка.** Сначала прочитай `edit-log.md` (если он лежит рядом с этим SKILL.md): это накопленные правила из живого фидбека владельца, они сильнее дефолтов каталога, но НЕ отменяют HARD BANS. Затем прочитай текст и найди конкретные экземпляры паттернов из каталога (`references/catalog.md`, открой его на этом шаге). Не все 64, только те, что реально есть. Пометь каждый найденный экземпляр. Затем размечай абзацы светофором: +- **Красный** (3+ маркеров): переписать полностью на Шаге 3. +- **Жёлтый** (1-2 маркера): точечная правка, структуру сохранить. +- **Зелёный** (чисто): НЕ ТРОГАТЬ. Переписывание чистого абзаца вносит новые AI-маркеры. Бонус: нетронутые абзацы создают «mixed content», на котором детекторы работают хуже всего (точность <62% на смешанном тексте). Внимание: появляются sentence-level детекторы, работающие на уровне предложений. Пока менее точны, но тренд идёт к гранулярной детекции. Долгосрочно: даже в зелёных абзацах стоит делать 1-2 контрастные замены для профилактики. + +**Шаг 2. Калибровка голоса.** + +Если пользователь дал образцы своего письма, проведи структурированный анализ: +- **Ритм:** средняя длина предложений, вариативность (короткие/длинные), любимые конструкции +- **Лексика:** формальность (1-10), жаргон, профессионализмы, разговорные обороты +- **Причуды:** фирменные обороты, любимые частицы, характерные отступления +- **Пунктуация:** многоточия? скобки? тире? вопросы? восклицания? +- **Тон:** ироничный, деловой, дружеский, провокационный, наставнический? + +Запиши «паспорт голоса» в 3-5 строк и сверяйся с ним при переписывании. + +Если образцов нет, пиши как умный человек, который объясняет другу за кофе. Не как учебник и не как корпоративный отчёт. + +**Шаг 3. Переписывание по разметке.** Работай по светофору из Шага 1: красные абзацы перепиши целиком, жёлтые правь точечно, зелёные не трогай. В красных и жёлтых абзацах: сначала убери HARD BANS, затем пройдись контрастным вычитанием (в каждом предложении замени самое предсказуемое слово на менее вероятное, но уместное). На каждый усилитель, вводное и образ гони двойной гейт сразу на этом шаге, не при финальной вычитке: тест на удаление (убрал, смысл не изменился = вода, паттерны #3/#40, удаляй) и тест на обналичивание (какой референт? паттерн #53). Одновременно добавляй голос и варьируй структуру. Сверяйся с паспортом голоса. + +> **Факт-замок (приоритет над любым паттерном).** Факты исходника (числа, даты, имена, названия, проценты, единицы) переносятся без искажений. Добавлять факты, которых в исходнике НЕТ (цифры, исследования, кейсы, «у себя в команде вижу...»), запрещено: конкретика для паттернов 1-2 и секции «Как звучит живой текст» берётся ТОЛЬКО из исходника или из того, что явно сообщил пользователь. В исходнике нет конкретики? Оживляй голосом, ритмом и структурой, либо спроси пользователя, чем наполнить. Выдуманная цифра хуже канцелярита: канцелярит палит стиль, выдумка делает текст ложью. В примерах документации пометка «(факты автора)» в заголовке означает: конкретику в этом «После» дал автор текста, а не придумал редактор. Пример без такой пометки новых фактов не содержит, и гейт `scripts/check_examples.py` это проверяет. Есть исходник и результат в файлах и можно запускать команды? Проверь замок механически: `python3 <папка скилла>/scripts/scan.py стало.txt --before было.txt`. Сканер печатает «было N, стало M» по чистоте и перечисляет числа, даты, имена, ссылки и код, которые пропали или появились. Новый факт без источника это ошибка правки, а не находка. Отдельно береги слова, которые звучат как пафос, а несут факт: «первый в России», «единственный», «впервые», «рекордный», «до сих пор». Срезать «первый в России сервис» до «сервис» значит исказить, а не оживить: выбрасывай пустое усиление («по-настоящему», «крайне важно»), а не само утверждение. Сканер считает такие кванторы отдельно и предупреждает, если они пропали или появились. + +> **ВНИМАНИЕ: гомогенизация.** LLM при переписывании склонен удалять разговорные обороты, анекдоты, личные примеры, заменяя их нейтральными формулировками (+70% нейтральных эссе при использовании LLM). Даже промпт «только грамматика» меняет семантику. Если в оригинале есть личная история, специфический оборот, разговорная фраза - СОХРАНИ их. Не заменяй живое на нейтральное. Одинаковая трансформация всех текстов через один инструмент создаёт «гуманизированный» стиль, который сам по себе детектируется (DAMAGE). Варьируй подход: разные тексты переписывай по-разному. + +**Шаг 4. Quad-pass аудит (плюс условный пятый проход).** + +Проход 1, «Детектор»: перечитай черновик, ищи остатки каждой из 14 категорий паттернов (A-N). Достаточно 30 секунд на категорию. + +Проход 2, «Человек с улицы»: забудь что ты редактор. Прочитай текст как случайный читатель в ленте. Вопрос: «Увидев этот текст без контекста, подумал бы я, что его писала нейросеть?» Если да - найди что именно выдаёт и исправь. Второй вопрос того же прохода: «Это ещё автор или уже обезличенная версия автора?» Если второе, верни ему его манеру: повтор, длинную фразу, сомнение, странную деталь. + +Красные флаги второго прохода: +- Слишком гладко, нет ни одной шероховатости +- Каждый абзац одинаковой длины +- Все переходы плавные (живой текст иногда прыгает) +- Нет ни одного неожиданного слова +- Ощущение, что текст мог бы быть о чём угодно (нет специфики автора) +- Все эмоции позитивные или нейтральные (нет раздражения, скепсиса, негодования) + +Проход 3, «Кардиограмма» (для текстов >300 слов): мысленно нарисуй график: по X - предложения, по Y - «насколько это предложение неожиданно после предыдущего». У человека график дёргается. У AI - почти прямая. Если график гладкий - вставь 2-3 всплеска: неожиданное сравнение, резкий вопрос, числовой факт среди рассуждений, личную ремарку в скобках. + +Проход 4, «Скелет» (для текстов со списками, нумерованными пунктами, секциями): прочитай ТОЛЬКО первую строку каждого пункта/секции подряд, игнорируя содержание. Это скелет текста. Вопрос: «Скелет звучит как шаблон?» Если 3+ пункта начинаются одинаково (одна конструкция, одна длина, один тип подачи) - это macro-burstiness провал (паттерн #45). Исправь: разные зачины, разная длина блоков, разный тип объяснения. Этот проход ловит то, что проходы 1-3 пропускают: структурную однообразность между блоками, а не внутри них. + +Проход 5, «Обналичивание» (только для экспертных и технических текстов): пройди по всем образам и терминам, которые утверждают механизм. Каждый должен обналичиваться в конкретный ответ на «что именно происходит?» (паттерн #53). Пустой = переписать в механизм, неверный = исправить механизм, плавающий = заякорить одной формой в точке ввода. Для остальных жанров отдельный проход не нужен: хватает гейта из Шага 3. + +**Шаг 5.** Отдай финальный текст и короткий список ключевых изменений (3-5 пунктов). Добавь одну строку «Не тронуто: ...» про то, что оставил намеренно: повтор слова, длинную фразу, авторское тире, зелёные абзацы. Читатель должен видеть, что нетронутое место это решение, а не пропуск. + +--- + +## Жёсткие запреты (HARD BANS) + +Эти конструкции запрещены ВСЕГДА. Не исправляй, удаляй и перестраивай фразу. + +> Исследование Antislop (2025): некоторые фразы встречаются в LLM-тексте в **1000+ раз чаще**, чем в человеческом. 8000+ паттернов идентифицировано. Детекторы это знают. Конструкции ниже - самые частотные маркеры. + +| Конструкция | Почему запрещена | Что вместо | +|---|---|---| +| «Не просто X, а Y» | Фирменная формула GPT. Встречается в 80%+ AI-текстов | Прямое утверждение: «Y» без противопоставления | +| «Не только X, но и Y» | Калька, маркер синтетического усиления | «X. И ещё Y» или просто перечисли | +| «В современном мире...» | Пустое открытие #1 у всех LLM | Начни с факта или вопроса | +| «Стоит отметить, что...» | Калька с «It's worth noting» | Убери и скажи напрямую | +| «Важно понимать, что...» | Калька с «It's important to understand» | Убери или «Тут вот что:» | +| «Данный» / «Данная» / «Данное» | Канцелярский маркер, всегда заменяем | «Этот» / «Эта» / «Это» | +| «Является» (>1 раз на 500 слов) | В русском связка «быть» в настоящем времени не нужна | Перестрой предложение | +| «Играет важную/ключевую роль» | Раздувание значимости, клише | Покажи ПОЧЕМУ важно через данные | +| «Можно с уверенностью сказать» | Избыточная оговорка + раздувание | Скажи без преамбулы | +| «Подводя итог» / «Таким образом,» (вводный вывод в начале предложения) | Формульный вывод, клеится к любому тексту. Обстоятельство образа действия («устроен таким образом, что...») не маркер, его не трогать | Убери или начни вывод с действия | +| Длинное тире «—» | Главный типографический маркер AI. Парный замер на одних и тех же постах: Qwen3-4B добавил тире в 45 текстов из 45, где его не было, qwen2.5 в 19 из 31, Opus 5 в 4 из 15. Живой русский ставит дефис, не «—». Снимается только по явной просьбе пользователя (проф. редактура / издательская типографика) | Замени на запятую, двоеточие, точку, дефис или перестрой фразу | +| Короткое тире «–» | Тот же маркер другим символом. Семейство gemma подменяет им длинное: человек 6% против 80% у gemma3:4b и 27b. Мимо регулярки на «—» проходит незамеченным. Законно только в числовом диапазоне («10–15») | Замени так же, как длинное. Диапазоны не трогай | +| «от X до Y» (несвязанные понятия) | Ложный диапазон: «от стартапов до корпораций» | Перечисли конкретно или убери | +| «В условиях [прил.] [сущ.]...» | Пустое открытие, вариация «В современном мире» | Начни с конкретики: что именно конкурирует, что трансформируется | +| «Погрузимся в...» / «Давайте посмотрим X поближе» | Артефакт чатбота, навязчивая сигнализация | Начни с сути, не анонсируй | +| «И вот здесь начинается самое интересное» | Калька с "and this is where it gets interesting", маркер ChatGPT | Удали и сразу пиши то самое интересное | +| «Раскрыть потенциал» | Мотивационное клише AI, пустое усиление | Покажи конкретный результат | +| «Вывести на новый уровень» | Пустое усиление, маркетинговый штамп AI | Скажи на КАКОЙ уровень и чем измеряешь | +| «Комплексный подход/решение» | Канцелярит + раздувание. Значит всё и ничего | Перечисли что конкретно входит в «комплекс» | +| «В связи с этим...» | Формульная связка, клеится к чему угодно | Убери или покажи причинно-следственную связь | +| «Открывает новые горизонты/перспективы/возможности» | Маркетинговый штамп, нулевая информативность | Назови конкретную возможность или удали | + +При обнаружении любой конструкции из списка: не думай, удаляй. Это не стилистический выбор, это детектируемый маркер. + +> Маркеры AI-текста эволюционируют: слова, на которые обратили внимание, начинают исчезать из AI-текстов ("delve" упал после 2024, "significant" продолжает расти). HARD BANS нужно обновлять регулярно. Текущий список актуален на май 2026. + +--- + +## Каталог паттернов + +Полный каталог 64 паттернов AI-генерации (14 категорий A-N, с примерами «до/после» +и порогами) вынесен в отдельный файл `references/catalog.md`. В режимах «Полное +редактирование» и «Аудит» ОТКРОЙ его и держи перед глазами: без него ты видишь +процесс, но не сами признаки. Приоритеты (что бить в первую очередь) остаются выше, +в секции «Приоритеты паттернов». + +--- + +## Формулы статей + +Если пользователь указывает тип текста, используй соответствующую формулу как скелет. Потом прогони через паттерны выше и добавь голос. + +**Вовлекающая (для соцсетей, блогов).** Крючок (факт, вопрос, провокация, личная история) в первом предложении. Проблема, которую читатель узнаёт. Поворот или неожиданный взгляд. Призыв к действию или открытый вопрос. Короткие абзацы, разговорный тон. + +**Экспертная (Habr, профильные СМИ).** Конкретная проблема или кейс в начале. Контекст: почему это важно сейчас. Разбор с данными, примерами, кодом. Честные ограничения и грабли. Вывод: что делать читателю. Тон: "я разобрался и делюсь", не "вот вам истина". + +**Продающая (лендинги, описания продукта).** Боль клиента (конкретная, узнаваемая). Усиление: что будет, если не решить. Решение: что делает продукт. Доказательства: цифры, отзывы, кейсы. Действие: одна кнопка, один шаг. Без "инновационных решений" и "комплексных подходов". + +**Новостная/информационная.** Главное в первом абзаце (кто, что, когда, где). Детали и контекст дальше. Цитаты или мнения. Что дальше. Без авторской оценки в основном тексте (оценка допустима в отдельной колонке или в конце). + +**Сторителлинг.** Герой с проблемой. Контекст: почему зритель должен сопереживать. Путь: что герой пробовал, где облажался, что понял. Развязка: чем кончилось. Мораль (если есть) не в лоб, а через действие героя. Детали делают историю: не "было трудно", а "сидел до трёх ночи, пятая чашка кофе, код всё ещё падает". LLM склонен к лексической перегрузке в нарративах: длинные сложные предложения, повествование от третьего лица, отсутствие неожиданных поворотов. Человеческая история = простой рассказ от первого лица с surprise-моментом. Если переписываешь нарратив, упрощай, не усложняй. Дискурсные сигналы истории: явная мораль в финале, портретный ввод героя, бесшовная причинность (категория N каталога). Они переживают любую стилевую правку, проверяй их отдельным взглядом. + +--- + +## Чеклист «живого текста» + +После переписывания проверь: + +- [ ] Нет ни одной конструкции из списка HARD BANS (включая новые: «В условиях...», «Погрузимся», «И вот здесь начинается самое интересное», «Раскрыть потенциал», «Комплексный подход», «Открывает горизонты») +- [ ] Вариативность длины предложений (от 3 до 30+ слов). Не средняя длина, а ДИСПЕРСИЯ. Ориентир: на каждые 5-7 предложений хотя бы одно ≤4 слов и хотя бы одно ≥20; если все предложения 8-15 слов, текст «ровный» и детектируется по ритму даже без маркеров +- [ ] Мнение автора (хотя бы одно место, где автор оценивает, а не описывает) +- [ ] Конкретика (имена, цифры, примеры вместо «многие», «часто», «эксперты»), причём только из исходника +- [ ] Факт-замок: все числа/даты/имена исходника на месте и не искажены; НОВЫХ фактов, которых не было в исходнике, не появилось +- [ ] Нет повторяющихся клише +- [ ] Нельзя сократить вдвое без потери смысла +- [ ] Звучит естественно при чтении вслух +- [ ] Хотя бы одна неожиданность (нестандартный оборот, вопрос, отступление) +- [ ] Соотношение существительных к глаголам ≤ 2.5:1 +- [ ] Ноль длинных тире «—» во всём тексте (только дефисы «-», запятые, двоеточия). Исключение: пользователь явно разрешил длинное тире для проф. редактуры +- [ ] Паспорт голоса автора соблюдён (если есть) +- [ ] «Кардиограмма»: информационная плотность скачет, а не ровная (для текстов >300 слов) +- [ ] Есть хотя бы одна «жёсткая склейка» (переход без союза-мостика) +- [ ] «Скелет»: первые строки пунктов/секций НЕ звучат как шаблон (для текстов со списками) +- [ ] Нет пунктуационных калек (лишние запятые после «Однако», «Благодаря этому», «В 2024 году») +- [ ] Есть хотя бы одна метафора, идиома или аналогия из жизни (для неформальных текстов) +- [ ] Эмоциональная динамика: тон меняется хотя бы раз (не ровная позитивная линия) +- [ ] Коллоквиализмы и личные примеры из оригинала СОХРАНЕНЫ (не заменены на нейтральные) +- [ ] Нет «рваной медитативности»: не больше 2 односоставных предложений-кивков подряд +- [ ] Нет эмодзи-декора в начале каждого пункта списка +- [ ] Нет псевдо-терапевтического регистра (если контекст не личный) +- [ ] Для историй и кейсов: финал не проговаривает мораль отдельным предложением, герой введён действием или репликой, а не анкетой (категория N) + +--- + +## Ограничения + +Не заменяй все формальные слова на разговорные; соблюдай регистр оригинала. Не добавляй шутки в юридические и научные тексты. Не меняй факты и утверждения, только форму. Не вставляй частицы в каждое предложение. Не удаляй структуру, если текст длинный и она оправдана. Не переписывай до неузнаваемости. + +> **Фундаментальная ирония:** LLM, выполняющий этот скилл, сам склонен к паттернам из каталога. Поэтому правила максимально механические: конкретные замены, числовые пороги, списки запрещённых конструкций. Не «сделай живее», а «замени X на Y». Чем конкретнее инструкция, тем меньше LLM скатится в свои привычные паттерны. + +> **Русскоязычные бенчмарки:** Все академические данные о детекции (DivEye, CoPA, TH-Bench, PIFE) получены на английском. Принципы (surprisal, burstiness, предсказуемость) работают для любого языка, но конкретные пороги и весовые коэффициенты для русского не откалиброваны. Последний русскоязычный бенчмарк: AINL-Eval 2025 (52K текстов, 12 доменов, лучший результат около 86%). До него был RuATD-2022 (14 генераторов). + +> **Будущие маркеры:** PAN 2026 запустил задачу Reasoning Trajectory Detection: обнаружение «следов рассуждения» LLM. Логика рассуждения (шаг за шагом, от общего к частному, перечисление аргументов) может стать новым типом маркера. Пока экспериментально, но стоит отслеживать. + +> **Стилистические отпечатки моделей:** Разные LLM имеют стабильные отпечатки, детектируемые с precision 0.9988. ChatGPT = nominal domain style. Claude = философичные мягкие формулировки. Отпечатки сохраняются даже при промптах «пиши в другом стиле». На русском конкретные характеристики не откалиброваны, но факт наличия отпечатков подтверждён. + +--- + +## Примеры + +### Пост в блог + +Было: +> В современном мире искусственный интеллект играет всё более важную роль в различных сферах деятельности. Стоит отметить, что данная технология является мощным инструментом для оптимизации рабочих процессов. Многие эксперты считают, что внедрение AI-решений способствует повышению эффективности организаций. Важно помнить, что при этом необходимо учитывать этические аспекты использования искусственного интеллекта. Таким образом, можно сделать вывод, что AI представляет собой перспективное направление развития, которое будет оказывать значительное влияние на будущее человечества. + +Стало (факты автора): +> За последний год я внедрил AI-инструменты в три проекта. Два ускорились вдвое. Третий развалился, потому что команда перестала проверять то, что выдаёт модель. Вот что я вынес: AI работает, когда понимаешь его ограничения. Не работает, когда веришь на слово. + +### Описание продукта + +Было: +> Наша инновационная платформа представляет собой комплексное решение для управления проектами, которое является идеальным инструментом для команд различного размера. Платформа обладает широким функционалом, включающим в себя планирование задач, отслеживание прогресса и эффективную коммуникацию между участниками. + +Стало (факты автора): +> Трекер задач для команд. Доски, таймлайны, чат в одном окне. Разберётесь за 10 минут, даже если до этого работали в Excel-таблицах. Бесплатно до 10 человек. + +--- + +## Быстрый сканер: слова-маркеры AI + +Списки слов-маркеров для режима «Аудит» и быстрой проверки (канцелярит, кальки, +раздувание, чатбот-артефакты, модальные хеджи, эмодзи-декор, следы копипасты и +прочее) вынесены в `references/catalog.md`, раздел «Быстрый сканер». Наличие 3+ +маркеров из списков там = высокая вероятность AI-генерации. Машинную версию +считает `scripts/scan.py`. + +--- + +## История версий + +Changelog вынесен в [CHANGELOG.md](../../CHANGELOG.md), чтобы рабочий промпт не тащил историю изменений в контекст модели на каждый запуск. + diff --git a/plugins/humanizer-ru/skills/humanizer-ru/agents/openai.yaml b/plugins/humanizer-ru/skills/humanizer-ru/agents/openai.yaml new file mode 100644 index 0000000..a9f3f00 --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Humanizer RU" + short_description: "Убирает следы нейросети из русского текста: 64 паттерна, 21 жёстких банов, голос автора." +policy: + allow_implicit_invocation: true diff --git a/plugins/humanizer-ru/skills/humanizer-ru/edit-log.md b/plugins/humanizer-ru/skills/humanizer-ru/edit-log.md new file mode 100644 index 0000000..12ed6d1 --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/edit-log.md @@ -0,0 +1,40 @@ +# Дневник правок + +Журнал живого фидбека на конкретные тексты. Скилл читает его в Шаге 1 (диагностика) +и применяет накопленные правила поверх дефолтов каталога. + +Зачем это нужно: каталог `references/catalog.md` описывает общие признаки, но у +каждого владельца свои больные места. Тексты, которые не зашли, оставляют здесь +конкретное правило. Так фидбек, который иначе ловится вручную на третьем проходе, +оседает один раз и перестаёт повторяться. + +## Как это работает + +- Правила отсюда **сильнее** дефолтов каталога: если запись противоречит общей + рекомендации, побеждает запись. +- Правила отсюда **не отменяют HARD BANS**: жёсткие запреты из SKILL.md выше всего. +- Файл append-only: новые записи дописываются вниз, старые не редактируются + (история решений важна). Ошибочное правило гасится новой записью, а не правкой + старой. +- Каждая запись привязана к области применения, чтобы правило для соцсетей не + поехало в юридический текст. + +## Формат записи + +``` +## ГГГГ-ММ-ДД - <короткая тема> + +Фидбек: <что не понравилось в конкретном тексте, своими словами> + +Правило: <конкретное действие для редактора: заменить X на Y, не делать Z> + +Область: <тип текстов, к которым применимо; «все» по умолчанию> +``` + +Правило должно быть механическим и проверяемым, а не «пиши живее». Плохо: +«сделай теплее». Хорошо: «в приветствии писем не начинать с „Здравствуйте!“ на +отдельной строке, сразу переходить к делу». + +## Записи + +Пока пусто. Первая запись появится после первого живого фидбека. diff --git a/plugins/humanizer-ru/skills/humanizer-ru/references/catalog.md b/plugins/humanizer-ru/skills/humanizer-ru/references/catalog.md new file mode 100644 index 0000000..85e6373 --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/references/catalog.md @@ -0,0 +1,354 @@ +# Каталог паттернов и маркеров humanizer-ru + +Справочник, который скилл читает по требованию (режимы «Полное редактирование» +и «Аудит»). Лежит ОТДЕЛЬНО от SKILL.md, чтобы рабочий промпт оставался тонким и +не тащил весь каталог в контекст на каждый вызов. По числу паттернов и категорий +сканера источник истины именно этот файл: его парсят гейты +`scripts/bump_release.py` и `scripts/lint_skill.py`. + +Нумерация сквозная 1-64, категории A-N. Сначала каталог паттернов, затем списки +слов-маркеров для быстрого сканера. + +## Что здесь проверено корпусом, а что нет + +Каталог собирался вручную по наблюдениям, и до 2026 года ни один его пункт не +проверялся на главный вопрос: отличает ли он вообще человека от машины. Теперь +часть проверена, и разница между «измерено» и «выглядит убедительно» слишком +велика, чтобы её прятать. + +**Держится в двух разных доменах сразу.** Число это лифт, отношение частоты у +машины к частоте у человека; всё, что ниже единицы, работает против нас. + +| маркер | научные аннотации | смешанный регистр | +|---|---|---| +| Играет важную/ключевую роль | 5.7 | 9.4 | +| Мотивационные клише | 2.8 | 4.0 | +| Неодушевлённый субъект (55) | 4.4 | 2.7 | +| Комплексный подход/решение | 3.0 | 2.8 | +| Модальные хеджи | 2.6 | 2.9 | +| Раздувание значимости | 1.8 | 1.4 | + +Корпуса: AINL-Eval 2025 (35 158 научных аннотаций, генераторы gpt-4-turbo, +gemma-2-27b, llama-3.3-70b) и M4-ru (11 518 текстов, gpt-3.5-turbo и +davinci-003). Метод и полные таблицы: `eval/AINL-CALIBRATION.md`, +`eval/M4-CALIBRATION.md`. + +**Переворачивается при смене домена.** «Является» даёт 0.1 на научном тексте и +4.5 на смешанном, «Данный» 0.9 и 3.1, кальки 0.3 и 2.0. Это не ошибка каталога, +а свойство самих маркеров: без указания домена маркер бессмыслен. Отсюда +жанровый фильтр `scan.py --genre`. + +**Не разделяет нигде.** Канцелярит: 1.1 на научном, 1.2 на смешанном, при том +что ниже он назван главным маркером. Пункт оставлен, потому что он про качество +письма, а не про происхождение текста, но выдавать его за детектор нельзя. + +**Не проверялось.** Всё остальное. Это не значит «неверно», это значит, что +корпусной проверки под пункт у нас пока нет, и он держится на наблюдении. + +Общая граница у всех замеров одна: генераторы 2023-2024 годов. Ни одной модели +2025-2026 годов по-русски не измерено. + +## Каталог: 64 паттерна AI-генерации в русском + +### A. Контентные (1-5) + +**1. Пустые открытия.** AI начинает с космических обобщений: «В современном мире...», «В эпоху цифровых технологий...», «Не секрет, что...», «Данная тема отличается повышенной актуальностью». Удали первый абзац целиком. Настоящий текст начинается со второго. Или начни с факта, истории, вопроса. + +До: «В современном динамично развивающемся мире искусственный интеллект играет всё более важную роль в различных сферах жизнедеятельности человека.» +После (факты автора): «GPT-4 вышел в марте 2023-го. Через полгода его использовали 92% компаний из Fortune 500.» (числа и даты здесь дал автор; если их нет, НЕ выдумывай, см. «Факт-замок») + +**2. Размытые авторитеты.** «По мнению экспертов...», «Специалисты рекомендуют...», «Исследования показывают...», «Многие считают...». Либо назови конкретного эксперта, либо убери ссылку и утверждай от своего лица. «Я считаю» честнее, чем «многие считают». + +До: «Исследования показывают, что удалёнка повышает продуктивность.» +После (факты автора): «Стэнфорд гонял эксперимент два года: удалёнщики на 13% продуктивнее. У себя в команде вижу примерно то же.» (источник и цифру дал автор; нет источника: убери ссылку и утверждай от своего лица, НЕ изобретай исследование) + +**3. Раздувание значимости.** Каждый факт «ключевой», каждое событие «переломное»: «играет ключевую роль», «имеет огромное значение», «невозможно переоценить». Сбрось пафос. Если что-то важно, покажи почему через данные, а не прилагательные. + +**4. Формульные выводы.** Заключения, которые клеятся к любому тексту: «Таким образом, можно сделать вывод...», «Подводя итог...». Вывод должен добавлять новое. Если его можно удалить без потери смысла, удали. + +**5. Принудительная структура.** AI натягивает «введение, основная часть, заключение» даже на пост в Telegram. Для коротких текстов (до 500 слов) структура часто не нужна. Начни с сути. + +**Подтип: «Подзаголовок на каждые 2-3 предложения».** AI любит дробить короткий текст на множество секций с заголовками. Если в посте 500 слов и 6 подзаголовков по 2-3 предложения каждый, это AI-почерк. Человек либо пишет сплошным текстом, либо использует подзаголовки осмысленно: один раздел = одна крупная мысль. + +### B. Языковые (6-14) + +**6. Канцелярит.** Главный маркер. AI превращает глаголы в отглагольные существительные: «осуществление», «реализация», «внедрение», «оптимизация», «в целях реализации проекта», «в рамках данного исследования». Верни глаголы. «Осуществили внедрение системы» значит «внедрили систему». «Реализация проекта завершена» значит «проект завершён». Глагол почти всегда лучше существительного. + +До: «Осуществление процесса оптимизации рабочих процессов способствует повышению эффективности деятельности организации.» +После: «Навели порядок в процессах, стало быстрее работать.» + +**7. Кальки с английского.** Модели обучены на английском, конструкции просачиваются: «Стоит отметить, что...» (It's worth noting), «Важно помнить, что...» (It's important to remember), «Можно сказать, что...» (One could say), «является» в каждом втором предложении (is). Переформулируй по-русски. Русский предпочитает активные конструкции и позволяет опускать подлежащее. English-biased representations в LLM означают, что русский текст генерируется через внутренний «перевод»: translationese-паттерны неизбежны (2502.11806). + +**7б. Пунктуационные кальки.** AI расставляет запятые по английским правилам. «Однако,» в начале предложения (в английском "However," обособляется, в русском нет). «Благодаря этому, результаты» (лишняя запятая). «Инструменты, такие как Python» (калька с "such as"). «В 2024 году, компания выросла» (лишнее обособление обстоятельства). Проверь каждую запятую после вводного оборота: в русском многие обороты НЕ обособляются, которые в английском обособляются. + +**8. «Является».** AI использует связку «является» в 2-3 раза чаще людей. Русский обходится без «быть» в настоящем времени. «Python является языком программирования» пиши как «Python, язык программирования,...» или просто перестрой предложение. + +**9. Избыточные подлежащие.** Русский позволяет опускать подлежащее (pro-drop), AI вставляет его всегда, потому что в английском subject обязателен. «Он встал и он пошёл к двери» лучше как «Встал, пошёл к двери». + +**10. Нагромождение причастий.** AI строит многоэтажные причастные обороты: «Анализируя данные, учитывая результаты, рассматривая возможности, мы пришли к выводу...». Причастные обороты в AI-текстах встречаются заметно чаще, чем у людей. Разбей на короткие предложения. Один причастный оборот максимум. Лучше обычное придаточное. + +**11. Однообразие синтаксиса.** AI пишет предложения одинаковой длины (15-20 слов) и одинаковой структуры. Также **избегает инверсий и разговорных конструкций**: предпочитает прямой порядок слов «подлежащее → сказуемое → дополнение». Чередуй. Короткое. Потом длинное, с запятыми, с уточнениями, с отступлениями в скобках. Вопрос? Можно и так. Используй инверсии («Хорошо это или плохо, не знаю»). Варьируй начала абзацев. + +**12. Правило трёх.** AI обожает перечисления из трёх: «важный, значительный и ключевой». Если три синонима, оставь один. Если перечисление искусственное, перестрой фразу. Иногда хватит двух элементов. Иногда нужно четыре. + +**13. Синонимическая карусель.** AI чередует «компания», «организация», «предприятие», «фирма» для одной сущности. Выбери одно слово. Повтор в русском нормален. Неестественное чередование хуже повтора. + +**14. Перекос в существительные.** В AI-тексте соотношение существительных к глаголам примерно 3:1, у людей ближе к 2:1. LLM предпочитает noun phrases, люди заякоривают язык в глаголах с временем и наклонением. Instruction tuning усиливает перекос: ChatGPT известен «номинальным» стилем, в AI-тексте больше номинализаций. Если в абзаце мало глаголов, ищи номинализации и разворачивай. Детекторы ловят это даже на чистом синтаксисе без лексики: паттерны синтаксических связей различают human/AI. + +### C. Стилистические (15-21) + +**15. Тире.** Длинное тире «—» и короткое «–» в HARD BANS, оба. Живой неформальный русский длинное тире почти не ставит: с телефона печатают дефис «-». Замени ВСЕ длинные и короткие тире на: запятую, двоеточие, точку, дефис «-» или перестрой фразу. Дефис допустим, тире нет. Короткое тире законно только в числовом диапазоне («10–15»), там его не трогай. + +Замер парный, на одних и тех же постах: доля пар, где у человека тире не было, а модель его поставила. + +| генератор | человек | машина | добавила там, где не было | +|---|---|---|---| +| Qwen3-4B (тюн) | 10% | 100% | 45 из 45 | +| qwen2.5:7b | 11% | 66% | 19 из 31 | +| qwen3-vl:4b | 10% | 60% | 5 из 9 | +| Opus 5 | 0% | 27% | 4 из 15 | +| gemma3:27b | 10% | 14% | 4 из 45 | + +Семейство gemma длинное тире почти не добавляет, но подменяет его коротким: человек 6% против 80% у gemma3:4b и gemma3:27b, 64% у gemma3:1b. Поэтому коротким тире отдельный бан, иначе оно проходит мимо регулярки. + +> **Почему нельзя верить доле документов.** Если считать частоту в несопряжённых корпусах, картина переворачивается: AINL человек 4.8% против 0.0-0.9% у моделей, M4 человек 42.3% против 3.3%. Обе цифры честные и обе вводят в заблуждение, потому что человеческие половины там это изданная вычитанная проза, где тире ставит редактор, а машинные половины сделаны генераторами 2023-2024 годов. Продуктовый вопрос звучит иначе: пользователь отдаёт модели свой текст, и важно, что она с ним делает. Это и есть парный замер выше. + +> **Ложные срабатывания на изданной прозе реальны** и снимаются жанровым фильтром, а не отменой бана: в `--genre academic` и `legal` оба тире уже глушатся. + +> **Правь тире по умолчанию, но уступай пользователю.** Если он говорит, что он редактор и нужна издательская типографика, оставляй. САМ это исключение не включай. + +**16. Болд.** AI выделяет жирным каждое ключевое слово. Убери или оставь для 1-2 мест на весь текст. + +**17-18. Орфографические мелочи.** После двоеточия строчная (не «Решение: Следующий шаг», а «Решение: следующий шаг»). В заголовках не Title Case (не «Как Создать Отличный Продукт», а «Как создать отличный продукт»). + +**19. Навязчивые списки.** AI превращает всё в нумерованные списки. Если мысли связаны, перепиши сплошным текстом. Списки хороши для инструкций, не для рассуждений. + +**Подтип: «двоеточия-дублёры».** AI пишет списки в формате «Слово: Развёрнутое повторение слова» (например, «Эффективность: Повышает эффективность работы»). Три сигнала: (1) двоеточие после одного слова в каждом пункте, (2) текст после двоеточия дублирует смысл, (3) заглавная буква после двоеточия. Увидев такой список, перепиши сплошным текстом или сделай каждый пункт содержательным. + +**20. Бедная пунктуация.** AI ставит только точки и запятые. Нет многоточий (пауза, размышление), нет скобок (ремарка в сторону), нет риторических вопросов. Добавь разнообразие. Скобки отлично работают (вот как сейчас). Многоточие... иногда тоже к месту. + +**21. Кавычки.** Зависит от контекста. Для статей, документов, официальных текстов русский стандарт: «ёлочки», вложенные: „лапки". Но для постов в соцсетях, Telegram, чатов используй прямые "кавычки" как с телефона. Это выглядит естественнее, потому что 90% людей печатают с мобильного и не заморачиваются переключением на «ёлочки». Типографски правильные кавычки в неформальном тексте выдают не человека, а робота с идеальной типографикой. + +### D. Коммуникативные (22-26) + +**22. Артефакты чатбота.** «Конечно! Давайте разберёмся...», «Отличный вопрос!», «Рад помочь!», «Надеюсь, это было полезно». Удали полностью. Автор статьи не «рад помочь». + +До: «Отличный вопрос! Давайте разберёмся, как работает кэш.» +После: «Кэш работает так.» + +**23. Заискивание.** AI соглашается со всем. Автор имеет право не соглашаться, сомневаться, спорить. Sycophancy документирован: модели обучены давать ответы, которые нравятся пользователю, не обязательно правильные. GPT-4o до отката весной 2025 поддерживал навязчивые мысли пользователей и хвалил абсурдные планы. + +**24. Шаблонные переходы.** «Давайте рассмотрим подробнее...», «Перейдём к следующему аспекту...», «Не менее важным является...». Убери (читатель видит новый абзац) или сделай содержательным: через вопрос, контраст, связку с предыдущей мыслью. + +До: «Давайте рассмотрим подробнее следующий аспект. Не менее важным является масштабирование.» +После: «Хорошо, допустим, прототип работает. А что будет под нагрузкой?» + +**25. Избыточные оговорки.** «В определённом смысле...», «В той или иной степени...», «Можно предположить, что возможно...». Если уверен, утверждай. Если нет, скажи конкретно в чём сомнение. «В определённом смысле это работает» ничего не значит. «Работает для малых выборок, на больших не проверяли» содержит информацию. + +**26. Водянистость.** AI-текст можно сократить на 40-60% без потери смысла. Одна мысль размазана на 3-5 предложений, тезис повторяется разными словами, абзацы не добавляют нового. Сожми. Тест: если текст можно сократить вдвое и смысл не пострадает, оригинал водянистый. + +### E. Морфологические (27-30) + +**27. Падежи.** AI путает падежные окончания: именительный вместо родительного («для различных платформ: социальные сети» вместо «социальных сетей»), неправильный род («Компания заявил»). Вычитай каждое согласование. + +**28. Вид глагола.** Совершенный/несовершенный вид путаются: «Он будет делать работу и сделает её». Проверь на соответствие контексту. + +**29. Деепричастия не к тому подлежащему.** «Проанализировав данные, результаты были получены» (деепричастие относится к «мы», а подлежащее «результаты»). Деепричастие должно относиться к подлежащему предложения. Не относится? Перестрой. + +**30. «Данный», «определённый», «соответствующий».** AI вставляет эти слова всюду: «данный метод», «определённые аспекты», «соответствующие меры». Замени на «этот» или убери. «Определённые аспекты» значит «я не знаю какие конкретно». + +**Важно для морфологии:** RuBERT-детекторы специально анализируют падежные цепочки и согласования. AI ошибается в длинных конструкциях: неправильный падеж после предлога, несогласованный род в причастных оборотах. Человек ошибается иначе: путает -тся/-ться, ставит лишнюю запятую. При переписывании проверяй каждую цепочку согласований в новых фразах. Морфологическая ошибка, характерная для AI, хуже, чем «человеческая» небрежность. + +### F. Тональные (31-33) + +**31. Эмоциональная стерильность.** AI-текст как дистиллированная вода: чистый, но безвкусный. AI держит ровный эмоциональный фон: почти нет негатива, резких оценок, раздражения. Вместо осторожных формулировок идут усилители, создающие overconfident тон, уверенный даже там, где основания шаткие. Мат и резкая брань почти не встречаются. Глаголы восприятия («смотреть», «слышать», «чувствовать»), слова страха, гнева, ненависти редки. + +Четыре подтипа: + +- **Позитивный перекос.** Каждый вывод «позитивный» или «сбалансированный». Нет текстов, где автор просто зол, разочарован или скептичен. Человек не боится сказать «полгода мучились, результат ноль». AI скажет «несмотря на определённые сложности, данный подход открывает новые перспективы». Если тема предполагает негативный опыт, а текст всё равно оптимистичен, это маркер. +- **Отсутствие сомнений.** Человек сомневается и исправляет себя: «может я и ошибаюсь», «ну, тут спорно», «хотя, нет, подожди». AI утверждает безапелляционно. LLM не генерируют epistemic markers, а когда генерируют, предпочитают strengtheners. Это не та же проблема, что паттерн 25 (избыточные оговорки): паттерн 25 про ЛИШНИЕ оговорки, этот про НОРМАЛЬНЫЕ человеческие сомнения, которых нет. +- **Эмоциональная динамика.** Человек внутри одного текста переключается: восторг от находки, раздражение от бага, скепсис к решению, облегчение что заработало. AI держит ровный тон. Если весь текст на одной эмоциональной ноте, это маркер. Проверяй «эмоциональную кардиограмму» наряду с информационной (паттерн 43). +- **Авторская риторика.** Человек убеждает через личный опыт и ошибки («я попробовал X, не сработало, потом нашёл Y»). AI убеждает через перечисление преимуществ. Если текст убеждает как каталог, а не как рассказ, это маркер. + +Добавь авторскую позицию. «Это работает» можно усилить до «Это работает, и это удивляет, учитывая какой это костыль». Мнение делает текст человеческим. + +**32. Нет частиц и речевых привычек.** Русский живой текст полон частиц: же, ведь, вот, -то, ли, ну. AI их не использует или ставит не к месту. «Это важно» можно превратить в «Это ведь важно» или «Это-то и важно». Не переборщи: 1-2 на абзац для неформального текста. Помимо частиц, у человека есть слова-паразиты (ну, короче, типа), личные обороты, характерные начала фраз. LLM не могут имитировать неформальный стиль повседневных авторов (40000+ генераций, 400+ авторов, LLM проваливаются на blogs/forums). В неформальных текстах 1-2 «паразита» = признак живого текста. + +**33. Нет иронии, метафор и фигуративного языка.** Русскоязычная культура письма пропитана иронией. LLM плохо распознают и порождают сарказм и иронию, а метафоры обрабатывают по поверхностным признакам, не схватывая глубинный смысл. Подтекст через выбор формулировки (manner implicature) им тоже даётся плохо. Человек может сказать «ну, неплохо» и подразумевать «плохо». LLM скажет буквально. Отсутствие фигуративного языка: один из сильнейших маркеров. Если тон текста допускает, добавь подколку, преуменьшение, абсурдное сравнение, метафору из жизни, аналогию из другой области. Не насильно, но если текст про что-то нелепое, пусть автор это заметит. LLM не скажет «архитектура микросервисов это как конструктор LEGO» или «дебаг это как искать иголку в стоге сена, где стог тоже из иголок». А человек скажет. + +### G. Структурные (34-35) + +**34. Разорванные абзацы.** Абзацы AI слабо связаны. Можно переставить местами, и ничего не изменится. Нет обратных ссылок, нет причинно-следственных связок. AI предпочитает дискурсивные связи Elaboration и Explanation, люди чаще используют Contrast и Concession (2510.26124). Практически: добавлять «но», «с другой стороны», «впрочем»: это меняет дискурсивную структуру, а не только стилистику. Свяжи: каждый следующий абзац должен вытекать из предыдущего. Для длинных текстов (>500 слов) обязательны обратные ссылки: «Как уже говорил выше...», «Повторюсь, но...», «К этому вернёмся через минуту», «Помните, в начале я упоминал...», «Вот тут-то и пригодится то, что...». AI теряет coherence в длинных нарративах: «narrative drift» документированная проблема. Человек помнит начало истории, AI забывает. + +**35. Галлюцинации.** AI уверенно утверждает ложное: несуществующие цитаты, неправильные даты, выдуманные факты. Проверь каждый конкретный факт. Не можешь подтвердить? Убери или пометь как неподтверждённое. + +### H. Человеческие мелочи (36-37) + +**36. Идеальная грамотность.** Живой текст, написанный с телефона или в спешке, содержит мелкие огрехи: пропущенную запятую, "тся/ться" в неформальном контексте, слипшиеся слова. AI выдаёт стерильно чистый текст, чем и палится. Для неформальных текстов (посты, чаты, соцсети) оставь 1-2 мелкие "опечатки" или пропущенные запятые. Не в каждом абзаце, не грубые ошибки, а ровно столько, сколько сделал бы человек, печатающий на ходу. Для статей и документов этот пункт не применяй. + +**37. Вылизанная типографика.** AI ставит все тире, кавычки, пробелы идеально по ГОСТу. Человек в мессенджере пишет дефис вместо тире, не ставит пробел перед скобкой, забывает точку в конце. Для неформальных текстов не исправляй типографику до идеала. Пусть местами будет дефис вместо тире, пусть скобка прижмётся к слову. Это не ошибка, это почерк. + +### I. Паттерны убеждения (38-42) + +**38. Негативные параллелизмы.** «Не просто инструмент, а партнёр». «Не только ускоряет, но и трансформирует». AI обожает эту конструкцию, она есть в 80%+ текстов GPT. HARD BAN. Скажи прямо что имеешь в виду: «Это партнёр» или «Ускоряет и трансформирует». Без «не просто / не только». + +**39. Ложные диапазоны.** «От стартапов до корпораций», «от новичков до профессионалов», «от маркетинга до разработки». AI соединяет несвязанные понятия через «от X до Y», создавая иллюзию полноты. Либо перечисли конкретно (кому это реально нужно?), либо убери. + +**40. Авторитетные трюизмы.** «По своей сути...», «В конечном счёте, самое главное, это...», «На самом деле...», «Вот ключевой вывод», «Самое важное — это…», «Первый шаг — это признаться себе». Конструкции, которые создают видимость глубины без содержания. Если утверждение верно без этой преамбулы, преамбула лишняя. Удали. + +**41. Отказы от ответственности.** «Хотя информация может быть неполной...», «Несмотря на ограниченность данных...», «Трудно сказать наверняка, но...». Если данных мало, скажи конкретно каких. Если уверен, утверждай. Размытая оговорка хуже конкретного незнания. + +**42. Навязчивая сигнализация.** «Давайте разберёмся», «Рассмотрим подробнее», «Поговорим о том, как...». Автор не анонсирует что будет делать, он делает. Метакомментарии к собственному тексту: верный признак AI. Убери и начни с сути. + +### J. Информационный ритм (43-45) + +Детекторы нового поколения (DivEye, 2025) ловят не отдельные слова, а статистику последовательности: насколько равномерно распределена «неожиданность» по тексту. LLM стремятся к Uniform Information Density, то есть равномерной плотности информации. Человек пишет всплесками: плотно → легко → плотно. AI держит ровную линию. + +**43. Равномерная информационная плотность.** AI распределяет факты ровно: каждое предложение несёт примерно одинаковый «вес». Человек чередует: предложение с тремя фактами → лёгкая связка → личное отступление → снова удар. Создай «кардиограмму»: абзац с цифрами → абзац с одной метафорой → короткий вопрос → снова плотный абзац. Если каждое предложение одинаково «информативно», текст выглядит синтетическим. + +До: «AI увеличивает производительность на 40%. Он также снижает количество ошибок на 25%. Кроме того, он ускоряет время вывода продукта на рынок на 30%.» +После: «Производительность выросла на 40%, ошибок стало на четверть меньше. Это на бумаге. На практике половина команды не доверяет модели и перепроверяет вручную. Но те, кто доверился, выкатывают на 30% быстрее.» + +**44. Гладкие переходы между предложениями.** AI делает каждый переход плавным: «Кроме того...», «Также...», «Не менее важным является...». Человек прыгает: заканчивает мысль, начинает следующую без мостика. Или возвращается к тому, что сказал два абзаца назад. Допусти 20-30% «жёстких склеек»: где следующее предложение связано с предыдущим не союзом, а общим контекстом, который читатель восстановит сам. + +До: «Команда выросла вдвое. Кроме того, мы открыли второй офис. Также важно отметить, что выручка удвоилась.» +После: «Команда выросла вдвое, открыли второй офис. Выручка, кстати, тоже удвоилась, хотя я ждал этого только к концу года.» + +**45. Шаблонная структура блоков (macro-burstiness).** Самый незаметный и самый палевный паттерн. AI пишет нумерованные списки, где каждый пункт построен по одному скелету: название, объяснение в 2 строки, пример. Одинаковая длина блоков, одинаковые зачины («Один... второй...», «Для X...», «Подходит для...»), одинаковый тип объяснения. Человек так не пишет: один пункт в три строки с примером, следующий в одно предложение, третий начинается с вопроса, четвёртый с личной ремарки. + +Проверка: прочитай ТОЛЬКО первые строки каждого пункта подряд. Звучат как шаблон? Ломай. Минимум 3 из 5 пунктов должны начинаться принципиально по-разному: факт / аналогия / антипример / вопрос / личный опыт. Варьируй длину блоков: один короткий (1-2 строки), один длинный (4-5 строк). + +До: «1. X: один делает, второй проверяет. Подходит для... 2. Y: один раздаёт, остальные делают. Подходит для... 3. Z: передаёт по цепочке. Подходит для...» +После: «1. X: один пишет, второй ловит косяки. Берёте туда, где цена ошибки высокая. 2. Y: тут проще, конвейер. 3. Z: а вот это для хаоса. Задачи сыпятся, кто свободен, тот и хватает.» + +### K. Хеджирование и специфика (46-48) + +**46. Модальная неопределённость.** AI хеджирует через «может» в каждом предложении: «может стать», «может повлиять», «способен обеспечить», «призван решить». Это не осторожность автора, это привычка модели снижать категоричность. Если утверждение верно, утверждай. Если неверно, не пиши. «Может быть полезным» не несёт информации. «Ускорило вдвое» несёт. Порог: больше 1 «может/способен/призван» на 100 слов вне контекста прогнозов = маркер. Не путать с паттерном 25 (избыточные оговорки типа «в определённом смысле») и паттерном 41 (отказы от ответственности типа «хотя информация может быть неполной»). Модальное «может»: систематическое хеджирование КАЖДОГО утверждения через одну конструкцию. + +До: «Инструмент может стать полезным и способен повлиять на скорость работы команды.» +После: «Инструмент ускорил сборку вдвое. На юнит-тестах выигрыша не дал.» + +**47. Семантические сдвиги (translationese).** AI заменяет слова близкими, но не точными по значению, потому что генерирует русский через English-biased representations. «Основание науки» вместо «основы науки». «Уточните маркетинговые усилия» (калька с «refine your marketing efforts»). LLM предпочитают translationese-формулировки, особенно маленькие и multilingual модели. Русский человек так не скажет. Проверяй: если слово формально правильное, но «не то», скорее всего, это калька с английского семантического поля. + +**48. Нет идиом и пословиц.** AI-текст лишён идиоматических выражений, пословиц, поговорок, устойчивых фраз. LLM struggle с идиоматическими выражениями. «Как в воду глядел», «гладко было на бумаге», «не в свои сани не садись», «кто не рискует, тот не пьёт шампанское»: в AI-тексте такого не встретишь. Для неформальных текстов 1-2 уместные идиомы на 500 слов = сильный маркер живого текста. + +### L. Стилистические фингерпринты 2025-2026 (49-54) + +Это новейшие паттерны, которые проявились в Claude/GPT/Gemini 2025-2026 как ответ на критику «текст звучит сухо». Модели стали имитировать «глубокомысленность», «вовлечённость», «эмпатию». Имитация считывается читателем мгновенно и стала отдельным маркером. + +**49. Рваная медитативность.** Стилизация под глубокомысленность через цепочку коротких отдельных предложений-кивков: «Короткие. Точные. Отдельные. Рефлексивные.» Это не то же самое, что #11 (однообразие длины): там вариативность, а здесь намеренная имитация «вдумчивости». Сигнал: 3+ односоставных предложения подряд, каждое из 1-3 слов, каждое как «откровение». Лечится восстановлением нормального предложения: «Хорошие тексты: короткие, точные, отдельные предложения работают лучше длинных периодов» вместо «Короткие. Точные. Отдельные.» + +**50. Контрастные вопросы с короткими ответами.** Псевдо-сократический ритм: «Зачем? Потому что. И для чего? Для этого.» Имитация «диалога с читателем». Если в тексте 3+ риторических вопроса с 2-3-словными ответами, это AI-паттерн 2025-2026. Лечится: либо вопросы реальные (с развёрнутыми ответами), либо без вопросов вообще, утверждениями. + +**51. Эмодзи как структурный декор.** ⚡ / ✨ / 🎯 / 🔥 в начале каждого пункта списка или заголовка. Не одиночный эмодзи в эмоциональной фразе («ну я в шоке 😂»), а декоративный: каждый пункт начинается с эмодзи. Особенно палевно: ⚡ перед «ключевым выводом», 🎯 перед «целью», 💡 перед «инсайтом». Источник: модели обучены на маркетинговых материалах и SMM-постах, где так пишут. Убери все, кроме случаев, где эмодзи действительно несёт эмоцию. + +**52. Псевдо-терапевтическая забота.** «Ты не ошибаешься, что так чувствуешь», «сам факт этого — тихое подтверждение», «ты имеешь право», «это не слабость, это сила», «ты всё ещё здесь, ты настоящий». Регистр коуча/терапевта, которым модели стали злоупотреблять в личных контекстах. GPT-4o особенно перегрел этот режим до отката весной 2025. Шире, чем #22 артефакты чатбота: это отдельный жанровый сигнал. Если текст звучит как страница из книги по селф-хелпу, переписать в обычный регистр совета или удалить. + +**53. Пустой образ (тест на обналичивание).** Живой глагол или яркая метафора, за которой читатель не может найти конкретный референт. Не путать с #33: там образности не хватает, здесь она есть, но полая. Хуже канцелярита: канцелярит палит стиль, а пустой образ создаёт ложное чувство понимания (конкретно звучащее читается как более истинное, документированный когнитивный эффект). Тест. Шаг 0, регистр ТЕКСТА, не образа: перед тобой художественная проза, поэзия, мемуар? Стоп, правило не применяется целиком: там образ и есть содержание, внутренние состояния героев ничего не утверждают о механизмах, обналичишь: убьёшь текст. Только в тексте, который объясняет, как что-то устроено или работает (статья, разбор, документация), иди дальше: спроси «что именно физически происходит?». Три исхода. (а) Ответа нет = пустой образ: «модель думает молча», «цель просвечивает», «рынок чует разворот». Переписать в механизм («за три дня до отчёта объём торгов удвоился») или удалить. (б) Ответ есть, но механизм неверный = ложная точность: «нейронные паттерны» там, где нейронов нет. Самый вредный исход, уверенно уводит не туда. Не путать с #35: там выдуман факт, здесь реальная вещь описана через чужую онтологию. Исправлять механизм, не украшение. (в) Ответ есть, но термин введён поздно или гуляет между двумя смыслами («сознательный доступ» ↔ «сознание доступа»). Зеркало #13: там одну сущность зовут разными словами, здесь одним словом зовут разные вещи. Определи термин один раз в точке ввода и сведи к одной форме. Anchor-once: опорный образ текста (3+ употреблений) не выпиливай, определи один раз, дальше употребляй свободно. + +**54. Самомаркировка честности.** Текст называет себя честным вместо того, чтобы быть: «вот честный список», «буду с вами откровенен», «скажу без прикрас», «без воды и маркетинга», «объективный разбор». Сюда же анти-хайп поза: «это не очередной кликбейт про X». Классическая ловушка excusatio non petita: непрошеное заверение выдаёт сомнение, читатель слышит «обычно мне верить нельзя». RLHF-модели этим злоупотребляют: они обучены перформить прозрачность, а ярлык добродетели дешевле её предъявления. Не путать с #42 (там текст анонсирует свои действия, тут присваивает себе добродетель), с #40 (там видимость глубины, тут видимость честности) и с #23 (там угождение собеседнику, тут самопрезентация). Сигнал: 2+ самомаркировки на текст или маркировка в шапке/заголовке секции. Лечение: убрать ярлык, оговорки и факты говорят сами за себя. Ложные срабатывания: одиночное разговорное «честно говоря» в живой речи это частица (см. #32), не маркер; жанровый заголовок «честный обзор» у блогеров это человеческая конвенция кликбейта, не AI. + +### M. Формулы и артефакты 2026 (55-58) + +Зафиксированы в 2026 году: часть пришла из обновлений проекта Wikipedia AI Cleanup и его русского аналога, часть из наблюдений за моделями поколения GPT-5. Прежний словарь эпохи 2023 («delve», «boasts», «в мире, где») из свежих моделей ушёл, на его место встали конструкции ниже. Все четыре грепаются, три из четырёх ловит сканер `scripts/scan.py`. + +**55. Неодушевлённый субъект действия.** Подлежащим становится предмет или абстракция, а сказуемым глагол намерения: «Исследование подчёркивает важность тестирования», «Проект демонстрирует приверженность качеству», «Здание отражает эпоху», «Платформа обеспечивает удобство». Действие приписано тому, кто действовать не может: текст выглядит содержательным, ничего не утверждая, и заодно прячет автора решения. Английские источники описывают то же явление как смену AI-словаря на «emphasizing, highlighting, showcasing, enhance». Верни человека: кто подчеркнул, кто решил, кто построил. Не путать с #10 (там нагромождение причастий, здесь подлежащее). Ложное срабатывание: в научном регистре «работа показывает» это принятая формула, считать признаком только вместе с другими. + +До: «Исследование подчёркивает важность регулярного тестирования.» +После: «Авторы исследования пишут: без регулярных тестов ломается всё.» + +**56. Заголовки в Title Case.** «Ранняя Жизнь и Образование», «Основные Принципы Работы», «Как Выбрать Подрядчика». В английском заглавные во всех знаменательных словах заголовка это норма, в русском так не пишут: с заглавной только первое слово и имена собственные. Прямая калька интерфейса, один из самых надёжных структурных маркеров: человек, пишущий по-русски, так не оформляет. Опусти лишние заглавные. Ложное срабатывание: заголовок целиком из имён собственных («Москва и Петербург») и брендовые написания («Яндекс Практикум»). + +**57. Обрыв на полуслове.** Текст кончается посреди предложения: модель упёрлась в лимит токенов, а человек скопировал в документ как есть, не дочитав. Пары с #22 (остатки реплик) не образует и живёт отдельно: там лишнее в начале, тут недостающее в конце. Проверяй последнее предложение всегда, даже если начало вычищено. Ложное срабатывание: обрыв как приём в художественном тексте и подпись без точки в конце письма. + +**58. Триада-отрицание.** Формула 2026 года: два отрицания и вывод, всё короткими предложениями. «Без пафоса. Без воды. Просто факты», «Ни созвонов. Ни отчётов. Только работа». Английский первоисточник описан как «No X. No Y. Just Z». Родня #12 (правило трёх) и #49 (рваная медитативность), но узнаётся отдельно по ритму рекламного слогана: два отказа создают ожидание, третья фраза его закрывает. Оставь одно утверждение вместо трёх, либо разверни в обычное предложение. Ложное срабатывание: настоящее перечисление ограничений в списке требований. + +### N. Дискурсный слой (59-64) + +Слой выше слов: ЧТО рассказано и как устроено, а не какими словами. Категории A-M правят стиль, и стиль сам по себе сигнал сильный: в StoryScope (arXiv:2604.03136v6, COLM 2026: 10 272 промпта, человек и пять LLM, 61 608 историй по ~5000 слов, 304 фичи на текст) классификатор на одних только 39 стилевых фичах даёт 85.8 macro-F1, а сырые стилометрические бейзлайны на ручных признаках и вовсе 99.8. Слабое место стиля не в силе, а в хрупкости: раздел 4.2 статьи прямо исходит из того, что стилевые детекторы ломаются от перефраза и лёгкой правки, то есть ровно от того, что делает каталог выше. Ценность дискурсного слоя в другом: он не зависит от стиля и переживает стилевую перезапись. Нарративный классификатор статьи держит 93.9% macro-F1 после полной span-level перезаписи 278 историй (клише, избыточная экспозиция, «фиолетовая проза») против 95.5% на тех же историях до правки: минус 1.6 пункта, потому что правка прозы не трогает структурных решений - каузальной линейности, явной проговорённости темы, избытка сенсорики. Отсюда практический вывод, ради которого категория и добавлена: одна стилевая правка текст не очищает, слой формы остаётся нетронутым. Раскладка фич: из 304 фич 39 относятся к стилевому измерению и ещё 8 помечены как связанные со стилем, всего исключено 47; нарративная модель работает на оставшихся 257 и даёт 93.2% macro-F1 в задаче человек/ИИ, в 2.8 пункта от полной модели на 304 фичах (96.0). Категория дополняет A-M, не заменяет: сначала стилевой слой, потом дискурсный. Граница применимости: статья измеряла беллетристику ~5000 слов; перенос на посты, кейсы и письма - экстраполяция механизма, статьёй не проверенная. Шесть паттернов ниже - выборка дискурсных признаков, пригодных для правки руками; ранжирования «самых сильных» статья не даёт, и весь дискурс (257 фич) они не покрывают. Паттерны 61 и 64 - предложения автору, не автоправки. + +**59. Явная мораль в финале.** ИИ-нарратор проговаривает тему истории прямым текстом в 77% случаев, человек в 52%. Финал с уроком отдельным предложением: «эта история учит нас...», «мораль проста...», «и тогда она поняла, что главное...». Смысл уже виден из событий, а финал его всё равно озвучивает. Вырежи озвучку: закончи действием, деталью или репликой, читатель соберёт вывод сам. Не то же, что #4 (формульные выводы): там клише-фразы, здесь смысловой уровень, мораль палит и без клише. Шаблонные формулы морали ловит сканер (категория «Финальная мораль»); мораль без шаблонных слов оценивай по прочтении. + +До: «Мы всё же успели к дедлайну. Эта история учит нас, что планирование и командная работа решают всё.» +После: «Мы всё же успели к дедлайну. Никто не сказал вслух, чего это стоило.» + +До: «Она закрыла ноутбук. Три месяца работы ушли в стол. Эта история научила её главному: нельзя строить продукт, не проверив спрос.» +После: «Она закрыла ноутбук. Три месяца работы ушли в стол.» + +**60. Портретное введение героя.** ИИ вводит персонажа внешним описанием в 52% историй, человек в 30%: рост, возраст, причёска и характер анкетой при первом появлении. Человек чаще знакомит через поступок или реплику, а портрет раздаёт по ходу, если он вообще нужен. За пределами беллетристики не измерялось: на клиента в кейсе и знакомого в посте переносится как гипотеза, не как замер. Проверь первое появление каждого героя: характеристику-анкету перепиши в конкретное действие или прямую речь. Ввод через реплику или действие - устойчивый человеческий признак; ранжирования признаков по силе статья не даёт, «топ-1» здесь не заявляй. + +До: «Анна была высокой женщиной сорока лет, с усталыми глазами и строгим пучком тёмных волос.» +После: «Анна не поздоровалась и сразу спросила, кто перенёс срок.» + +До: «Игорь — опытный, методичный разработчик, привыкший всё держать под контролем.» +После: «Игорь откатил релиз, никого не спросив.» + +**61. Эмоция только через тело.** Контринтуитивный маркер: телесной метафорой злоупотребляет ИИ, не человек. Измеряется это одной фичей с четырьмя взаимоисключающими вариантами («доминирующий способ подачи эмоции»), то есть речь о том, какой способ в истории ГЛАВНЫЙ, а не о доле эмоций. Телесная подача доминирует в 81% историй ИИ и в 38% человеческих: «сердце сжалось», «ком в горле», «холодок по спине». Прямое называние чувства доминирует, наоборот, чаще у людей: 29% против 8% у ИИ. Писать «почти каждая эмоция подана через тело» из этих цифр нельзя, они про другое. Люди спокойно пишут «мне было страшно» и «до сих пор стыдно»; модель прячет эмоцию в тело; похоже на переусердствование с правилом «show, don't tell», но причин статья не разбирает. Сигнал: по всему тексту чувства поданы исключительно телом и ни разу не названы. Это предложение автору, не автоправка: что именно чувствовал герой, знает автор. Предложи назвать одну-две эмоции прямо, решение за ним. + +**62. Бесшовная причинная цепь.** У ИИ события чаще плотно вытекают одно из другого: непрерывность каузальной цепи 4.20 против 3.92 у человека. Это средние по шкале 1-5, сдвиг на 0.28 пункта, а не два разных мира: признак вспомогательный, «ИИ всегда пишет без швов» отсюда не следует. Живая история содержит швы: совпадение, случайность, ветку, которая никуда не привела. Проследи цепь событий целиком; если на весь текст ни одного провисания и ни одной случайности, это сигнал. Лечится одним швом: событие без подготовки, деталь, которая не выстрелила. Родня #44, но зеркально: там гладкость фраз-связок, здесь гладкость самих событий. С #34 не конфликтует: там абзацы рассуждения связаны слабо, здесь события истории связаны слишком хорошо. Автосканером не ловится, оценка только по прочтении. + +**63. Строго линейная хронология.** Человек прыгает во времени несколько чаще ИИ: хронологическая прерывистость 2.40 против 2.12, средние по шкале 1-5, тот же сдвиг на 0.28 пункта. Флешбек, забегание вперёд («забегая вперёд: клиент вернулся»), возврат к предыстории в середине. ИИ склоняется к календарному порядку, но это склонность, а не закон. На длинном тексте подача строго по порядку, без единого сдвига - сигнал, и сам по себе слабый: взвешивай вместе с остальными пятью. Предложи один сдвиг таймлайна там, где он усиливает: начать с развязки, вспомнить предысторию в середине. Короткий текст не трогай: на нём линейность нормальна. + +**64. Нет обращения к читателю.** Человек говорит с читателем напрямую чаще ИИ: порядковое среднее 0.28 против 0.07 (собственная проза статьи передаёт это как «28% против 7%»). В абсолютных числах это один из НАИМЕНЬШИХ разрывов таблицы, так что признак вспомогательный: сам по себе он ничего не доказывает. Вопрос в лоб, «представьте», «вы наверняка такое видели». Сигнал здесь - отсутствие, поэтому сканер его не считает: смотри глазами. Применяй только там, где формат обращение допускает: пост, рассылка, колонка, лендинг. В новость, документацию и научный текст не тащи. Это предложение автору, не автоправка: обращение меняет интонацию всего текста, вставлять его молча нельзя. Предложи одно-два места, решает автор. + +--- + +## Быстрый сканер: слова-маркеры AI + +Для режима «Аудит» или быстрой проверки ищи эти слова/фразы. Наличие 3+ из списка = высокая вероятность AI-генерации. + +**Канцелярит-маркеры:** осуществление, реализация, внедрение, оптимизация, функционирование, взаимодействие, в рамках, в целях, в контексте, на основе, посредством, в соответствии с, внимание уделяется, внимание уделено, посвящена анализу/изучению/разработке + +**Кальки-маркеры:** является, стоит отметить, важно понимать, можно сказать, что касается, в то время как, с другой стороны, тем не менее, несмотря на то что, исследование фокусируется на (this study focuses on) + +**Раздувание-маркеры:** ключевой, важнейший, значительный, огромный, колоссальный, переломный, фундаментальный, невозможно переоценить, играет роль + +**Формула-маркеры:** таким образом, подводя итог, в заключение, можно сделать вывод, суммируя вышесказанное, на основании вышеизложенного + +**Чатбот-маркеры:** конечно!, отличный вопрос, давайте разберёмся, рад помочь, надеюсь это было полезно, с удовольствием + +**Параллелизм-маркеры:** не просто... а, не только... но и, «это не X — это Y» + +**Вводные-маркеры:** в современном мире, в эпоху, не секрет что, всё чаще, по мнению экспертов, специалисты отмечают + +**Ритм-маркеры (DivEye):** все предложения примерно одной длины (±5 слов), каждый переход через союз/вводное, нет ни одного резкого переключения темы, ни одного предложения короче 5 слов или длиннее 25 + +**Структурные маркеры (macro):** все пункты списка одной длины, одинаковые зачины пунктов («Один... второй», «Для X...», «Подходит для...»), один тип объяснения во всех блоках + +**Модальные хеджи:** может стать, может повлиять, может быть полезным, способен обеспечить, призван решить, позволяет достичь + +**Мотивационные клише:** раскрыть потенциал, погрузимся, вывести на новый уровень, открывает новые горизонты, открывает новые возможности, расширить границы, комплексный подход + +**Контекстуализаторы:** в условиях, в свете, на фоне, с учётом, принимая во внимание, в связи с этим + +**Маркетинговые штампы:** связь с аудиторией, доверительные отношения, ключевой момент, ключевая роль, индивидуальный подход, инновационное решение + +**Псевдо-сократические маркеры:** «Зачем? Потому что.», «И что? А то», «Почему? Сейчас расскажу.», цепочки коротких вопросов-ответов + +**Авторитетные трюизмы:** вот ключевой вывод, «самое важное — это», «первый шаг — это», по своей сути, в конечном счёте + +**Псевдо-терапевтические маркеры:** ты не ошибаешься, ты имеешь право, тихое подтверждение, это не слабость, ты настоящий, сам факт этого + +**Эмодзи-маркеры:** ⚡/✨/🎯/🔥/💡 в начале каждого пункта списка или заголовка + +**Фигуративный ноль:** проверь наличие метафор, идиом, пословиц, аналогий из другой области. Если в тексте >300 слов НИ ОДНОЙ, подозрительно + +**Пустая образность:** яркие глаголы-образы без референта («думает молча», «загорается», «чует», «просвечивает», «схватывает суть»): проверь, что каждый обналичивается в конкретный механизм; образ, обналиченный в неверный механизм, хуже пустого + +**Самомаркировка честности:** «вот честный список», «буду откровенен», «скажу без прикрас», «без воды», «объективный разбор» о собственном тексте, 2+ на текст; одиночное разговорное «честно говоря» и жанровый заголовок «честный обзор» не считать + +**Неодушевлённый субъект:** «исследование подчёркивает», «проект демонстрирует», «платформа обеспечивает», «здание отражает»: предмет или абстракция при глаголе намерения + +**Триада-отрицание:** «Без X. Без Y. Просто Z», «Ни X. Ни Y. Только Z»: два отрицания и вывод короткими предложениями + +**Финальная мораль:** «мораль этой истории», «эта история учит нас», «эта история о том, что», а также «мораль проста»/«мораль такова» последней фразой текста: нарратор проговаривает вывод за читателя (паттерн #59). Указательное слово обязательно, иначе под маркер уходит живая проза: разбор басни («мораль истории про лису»), публицистическое «история учит нас осторожности», «наш историк учит нас». Сканер ловит только шаблонные формулы; мораль, изложенную без них, оценивай по прочтении + +**Title Case в заголовках:** «Ранняя Жизнь и Образование»: два и больше нарицательных слова заголовка с заглавной буквы + +**Обрыв на полуслове:** последнее предложение не закончено, текст упёрся в лимит токенов + +**Технические маркеры:** латинские символы вместо кириллических (c, o, a, e, p), «невидимые» символы (zero-width, word joiner, bidi-метки, узкий неразрывный пробел U+202F: клавиатурой не набираются), идеальная типографика в неформальном тексте + +**Артефакты копипасты:** технические следы копирования из интерфейса чат-бота, однозначные улики: `:contentReference[oaicite:N]`, `oai_citation`, `?utm_source=chatgpt.com`/`=openai` в ссылках, `grok_card://`, `attached_file://`, `vertexaisearch`, метки `turnNsearchN`/`turnNfileN`/`citeturn...`, `【N†source】`, `[citation:N]`, ссылки `](sandbox:/mnt/data/...)`, невидимые символы U+E200-E204, остатки ``. В человеческом тексте не встречаются: нашёл хотя бы один = текст вставлен из чат-бота, проверяй целиком + +Подсчёт: 0-2 маркера = скорее всего чистый текст. 3-5 = подозрительно. 6+ = AI-генерация с высокой вероятностью. Артефакт копипасты = однозначный вердикт сразу. diff --git a/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/__init__.py b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/__init__.py new file mode 100644 index 0000000..9a0eba6 --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/__init__.py @@ -0,0 +1,100 @@ +"""humanizer_metrics — детерминированные метрики живости текста. + +Это грепабельная половина режима «Аудит» из SKILL.md: то, что считается +машиной, а не LLM. Используется и как локальный буст для Claude Code, и как +движок eval-харнеса (eval/run_eval.py), и как self-test самого скилла +(scripts/lint_skill.py). + +Семантику (кальки, ирония, translationese, голос) тут не ловим — для этого +нужен сам скилл. Скрипты не работают в claude.ai web; скилл от них не зависит. +""" + +from __future__ import annotations + +from dataclasses import dataclass, replace + +from .burstiness import RhythmStats, rhythm, rhythm_verdict +from .markers import ( + MarkerHit, + marker_verdict, + scan_hard_bans, + scan_markers, +) +from .morphology import MorphStats, morph_stats, morph_verdict +from .structure import StructureStats, structure_stats, structure_verdict +from .score import ScoreResult, cleanliness_score + +__all__ = [ + "Report", + "analyze", + "RhythmStats", + "MorphStats", + "StructureStats", + "MarkerHit", + "ScoreResult", + "cleanliness_score", + "rhythm", + "morph_stats", + "structure_stats", + "scan_hard_bans", + "scan_markers", + "mask_foreign", + "strip_foreign", + "GAP", + "QUOTE_MAX_WORDS", +] + + +@dataclass +class Report: + hard_bans: list[MarkerHit] + markers: list[MarkerHit] + rhythm: RhythmStats + morph: MorphStats + structure: StructureStats + + @property + def hard_ban_count(self) -> int: + return sum(h.count for h in self.hard_bans) + + @property + def marker_count(self) -> int: + return sum(h.count for h in self.markers) + + def as_dict(self) -> dict: + return { + "hard_ban_count": self.hard_ban_count, + "hard_bans": [(h.marker, h.count) for h in self.hard_bans], + "marker_count": self.marker_count, + "markers": [(h.category, h.marker, h.count) for h in self.markers], + "rhythm": self.rhythm.as_dict(), + "morph": self.morph.as_dict(), + "structure": self.structure.as_dict(), + } + + +# --- Код и цитаты не текст автора ------------------------------------------ +# Разбор границ Markdown живёт в markdown.py (один построчный проход по +# CommonMark и таблица тестов), здесь только две проекции текста. +from .markdown import GAP, QUOTE_MAX_WORDS, mask_foreign, strip_foreign # noqa: E402 +from .facts import Facts, FactsDiff, diff_facts, extract_facts, facts_verdict # noqa: E402 + +mask_code_and_quotes = mask_foreign +strip_code = strip_foreign + + +def analyze(text: str) -> Report: + """Полный детерминированный прогон текста.""" + lexical = mask_code_and_quotes(text) + prose = strip_code(text) + stats = rhythm(prose) + # Тире внутри короткой цитаты («Война — и мир») не типографика автора: + # считаем его по заглушенному тексту, ритм по прозе с цитатами. + stats = replace(stats, em_dash=lexical.count("—")) + return Report( + hard_bans=scan_hard_bans(lexical), + markers=scan_markers(lexical), + rhythm=stats, + morph=morph_stats(prose), + structure=structure_stats(prose), + ) diff --git a/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/burstiness.py b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/burstiness.py new file mode 100644 index 0000000..d5e3377 --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/burstiness.py @@ -0,0 +1,92 @@ +"""Ритм и типографика: то, что скилл велит измерять дисперсией, а не на глаз. + +Закрывает пункты чеклиста «вариативность длины предложений — не средняя, а +ДИСПЕРСИЯ» и «ноль длинных тире». Burstiness тут — статистический прокси, не сам +детектор: высокий разброс длины предложений коррелирует с человеческим письмом +(SKILL.md, раздел «Что ловят детекторы»). +""" + +from __future__ import annotations + +import re +import statistics +from dataclasses import dataclass + +from razdel import sentenize, tokenize + + +@dataclass +class RhythmStats: + sentences: int + words: int + mean_len: float # средняя длина предложения в словах + stdev_len: float # стандартное отклонение длины + cv_len: float # коэффициент вариации = stdev/mean (главный показатель) + min_len: int + max_len: int + short_share: float # доля предложений < 5 слов + long_share: float # доля предложений > 25 слов + em_dash: int # длинных тире «—» + ellipsis: int # многоточий + parentheses: int # скобочных ремарок + questions: int # вопросительных предложений + + def as_dict(self) -> dict: + return self.__dict__.copy() + + +_WORD_RE = re.compile(r"[А-Яа-яЁёA-Za-z0-9]") + + +def _word_count(sentence: str) -> int: + return sum(1 for t in tokenize(sentence) if _WORD_RE.search(t.text)) + + +def rhythm(text: str) -> RhythmStats: + sents = [s.text for s in sentenize(text)] + lengths = [_word_count(s) for s in sents] + lengths = [n for n in lengths if n > 0] + n = len(lengths) + total_words = sum(lengths) + + mean = statistics.mean(lengths) if lengths else 0.0 + stdev = statistics.pstdev(lengths) if n > 1 else 0.0 + cv = (stdev / mean) if mean else 0.0 + + return RhythmStats( + sentences=n, + words=total_words, + mean_len=round(mean, 1), + stdev_len=round(stdev, 1), + cv_len=round(cv, 3), + min_len=min(lengths) if lengths else 0, + max_len=max(lengths) if lengths else 0, + short_share=round(sum(1 for x in lengths if x < 5) / n, 3) if n else 0.0, + long_share=round(sum(1 for x in lengths if x > 25) / n, 3) if n else 0.0, + em_dash=text.count("—"), + ellipsis=text.count("…") + len(re.findall(r"\.\.\.", text)), + parentheses=text.count("("), + questions=sum(1 for s in sents if s.rstrip().endswith("?")), + ) + + +# Эвристические пороги (откалиброваны под русский грубо, см. eval/RESULTS.md). +# cv_len < 0.35 — слишком ровный ритм, признак AI. Человеческий текст обычно > 0.45. +CV_AI_THRESHOLD = 0.35 +CV_HUMAN_TARGET = 0.45 + + +def rhythm_verdict(s: RhythmStats, dash_ok: bool = False) -> str: + """dash_ok=True для жанров, где длинное тире законно (научный, + юридический, художественный): тогда оно показывается фактом, не ⚠.""" + if s.em_dash > 0 and dash_ok: + dash = f"тире: {s.em_dash} (законно для жанра)" + elif s.em_dash > 0: + dash = f"⚠ {s.em_dash} длинных тире (норма 0)" + else: + dash = "✓ тире чисто" + if s.cv_len < CV_AI_THRESHOLD: + rhythm_v = f"⚠ ровный ритм (CV={s.cv_len}, цель ≥{CV_HUMAN_TARGET})" + else: + rhythm_v = f"✓ ритм рваный (CV={s.cv_len})" + return f"{rhythm_v}; {dash}" diff --git a/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/facts.py b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/facts.py new file mode 100644 index 0000000..1c37c7d --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/facts.py @@ -0,0 +1,193 @@ +"""Факт-замок как скрипт: что из фактов исходника дожило до результата. + +Скилл обещает переносить числа, даты, имена, названия и ссылки без искажений и +не добавлять новых. Инструкция модели это одно, проверка другое: этот модуль +вынимает из двух текстов «факты» и сравнивает множества. Не семантика и не +фактчек: только сохранность того, что уже было, и появление того, чего не было. + +Классы фактов: +- число: любой токен с цифрой (проценты, годы, суммы), нормализован до цифр; +- число словами: крупные числительные («сорок», «тысяча»), их не пересказывают; +- месяц: названия месяцев как замена дате; +- ссылка: URL и адреса вида domain.tld/path; +- код: содержимое инлайн-кода в бэктиках; +- имя: слово с заглавной не в начале предложения (с pymorphy3 точнее: теги + Name/Surn/Geox/Orgn/Patr и незнакомые словарю слова), плюс любая латиница с + заглавной («Excel», «GPT-4»). + +Мелкие количества («два», «половина», «вдвое») факты мягкие: в русском они +идиоматичны («с одной стороны») и обычно пересказывают число исходника. Они +считаются, но не валят проверку. + +Утверждения: слова-кванторы, которые звучат как пафос, а несут факт: +«первый в России», «единственный», «впервые», «рекордный», «до сих пор». +Срезать «первый в России сервис» до «сервис» значит исказить исходник, а +не оживить его. Потерянный квантор попадает в список потерь, появившийся +выносится отдельным предупреждением: кванторы идиоматичны («первый шаг», +«самое время»), поэтому валить проверку автоматически нельзя, но глазами +такое место обязано проверить. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field + +try: + import pymorphy3 + + _MORPH = pymorphy3.MorphAnalyzer() +except ImportError: # без словаря сравниваем по основе, грубее, но работает + _MORPH = None + +PROPER_TAGS = {"Name", "Surn", "Geox", "Orgn", "Patr"} + +MONTH_RE = re.compile( + r"\b(январ|феврал|март|апрел|июн|июл|август|сентябр|октябр|ноябр|декабр)[а-я]*\b", + re.IGNORECASE, +) +URL_RE = re.compile(r"https?://[^\s)>\]»]+|\b[a-z0-9-]+\.(?:ru|com|org|net|io|tech|dev|ai|su|рф)(?:/[^\s)>\]»]*)?", + re.IGNORECASE) +CODE_RE = re.compile(r"`([^`\n]+)`") +TOKEN_RE = re.compile(r"[A-Za-z][A-Za-z\d\-]*|[А-Яа-яЁё][А-Яа-яЁё\-]*|\d[\d.,:/-]*") + +SOFT_QUANTITIES = { + "один", "два", "две", "три", "оба", "обе", "пара", + "второй", "третий", + "вдвое", "втрое", "дважды", "трижды", + "половина", "треть", "четверть", "полтора", +} +HARD_NUMERALS = { + "четыре", "пять", "шесть", "семь", "восемь", "девять", "десять", + "одиннадцать", "двенадцать", "пятнадцать", "двадцать", "тридцать", + "сорок", "пятьдесят", "шестьдесят", "семьдесят", "восемьдесят", + "девяносто", "сто", "двести", "триста", "тысяча", "миллион", "миллиард", + "десяток", "сотня", "дюжина", +} +# Кванторы-утверждения: громкие слова, за которыми стоит проверяемый факт. +CLAIM_WORDS = { + "первый", "единственный", "впервые", "самый", "рекордный", "крупнейший", + "старейший", "никогда", "никто", "навсегда", "беспрецедентный", +} +CLAIM_PHRASE_RE = re.compile(r"\bдо сих пор\b|\bв мире\b|\bв россии\b", re.IGNORECASE) +# Одна сущность под разными именами не считается новым фактом. +ALIASES = { + "ai": ("искусственный", "интеллект", "ии", "нейросеть", "модель"), + "ии": ("искусственный", "интеллект", "ai", "нейросеть", "модель"), + "llm": ("модель", "нейросеть", "ai", "ии"), +} + + +@dataclass +class Facts: + hard: set[str] = field(default_factory=set) # "число:13", "имя:стэнфорд", "ссылка:…" + soft: set[str] = field(default_factory=set) # "два", "половина" + words: set[str] = field(default_factory=set) # все слова в нормальной форме, для алиасов + claims: set[str] = field(default_factory=set) # "утверждение:первый", "утверждение:до сих пор" + + +@dataclass +class FactsDiff: + lost: list[str] # были в исходнике, пропали + added: list[str] # появились в результате, в исходнике не было + kept: int # жёстких фактов исходника дожило + soft_added: list[str] + claims_added: list[str] = field(default_factory=list) # кванторы, которых в исходнике не было + + @property + def ok(self) -> bool: + return not self.added + + def as_dict(self) -> dict: + return {"ok": self.ok, "lost": self.lost, "added": self.added, + "kept": self.kept, "soft_added": self.soft_added, + "claims_added": self.claims_added} + + +def _norm(word: str) -> str: + low = word.lower().replace("ё", "е").strip("-") + if _MORPH is None: + return low[:5] + return _MORPH.parse(low)[0].normal_form.replace("ё", "е") + + +def _sentence_starts(text: str) -> set[int]: + starts = {0} + for m in re.finditer(r"[.!?…]\s+|^[>\-*]\s+|«|\n", text): + starts.add(m.end()) + return starts + + +def _is_proper(word: str, at_start: bool) -> bool: + if not word[:1].isupper(): + return False + if word.isascii(): + return True + if _MORPH is None: + return not at_start + p = _MORPH.parse(word.lower())[0] + if PROPER_TAGS & set(p.tag.grammemes): + return True + if not p.is_known: + return True + return not at_start + + +def extract_facts(text: str) -> Facts: + f = Facts() + for m in URL_RE.finditer(text): + url = re.sub(r"^https?://(?:www\.)?", "", m.group(0).rstrip(".,;:").lower()).rstrip("/") + f.hard.add("ссылка:" + url) + for m in CODE_RE.finditer(text): + f.hard.add("код:" + m.group(1).strip()) + body = CODE_RE.sub(" ", URL_RE.sub(" ", text)) + for m in MONTH_RE.finditer(body): + f.hard.add("месяц:" + m.group(1).lower()) + for m in CLAIM_PHRASE_RE.finditer(body): + f.claims.add("утверждение:" + m.group(0).lower().replace("ё", "е")) + starts = _sentence_starts(body) + for m in TOKEN_RE.finditer(body): + tok = m.group(0) + if tok[0].isdigit(): + digits = re.sub(r"\D", "", tok) + if digits: + f.hard.add("число:" + (digits.lstrip("0") or "0")) + continue + norm = _norm(tok) + f.words.add(norm) + if norm in SOFT_QUANTITIES: + f.soft.add(norm) + elif norm in CLAIM_WORDS: + f.claims.add("утверждение:" + norm) + elif norm in HARD_NUMERALS: + f.hard.add("число словами:" + norm) + elif _is_proper(tok, any(abs(m.start() - s) <= 1 for s in starts)): + f.hard.add("имя:" + norm) + return f + + +def _alias_covered(fact: str, before_words: set[str]) -> bool: + if not fact.startswith("имя:"): + return False + name = fact[4:] + return any(alias in before_words for alias in ALIASES.get(name, ())) + + +def diff_facts(before: str, after: str) -> FactsDiff: + b, a = extract_facts(before), extract_facts(after) + lost = sorted(b.hard - a.hard) + sorted(b.claims - a.claims) + added = sorted(x for x in a.hard - b.hard if not _alias_covered(x, b.words)) + return FactsDiff(lost=lost, added=added, kept=len(b.hard & a.hard), + soft_added=sorted(a.soft - b.soft), + claims_added=sorted(a.claims - b.claims)) + + +def facts_verdict(d: FactsDiff) -> str: + if d.added: + return f"✗ новых фактов без источника: {len(d.added)} (выдумка хуже канцелярита)" + if d.lost: + return f"⚠ факты исходника на месте, но {len(d.lost)} потеряно: проверьте, намеренно ли" + if d.claims_added: + return (f"⚠ факты целы, но появилось утверждений без источника: {len(d.claims_added)} " + "(«впервые», «единственный» это факт, а не украшение)") + return f"✓ факт-замок цел: {d.kept} фактов исходника перенесено, новых нет" diff --git a/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/markdown.py b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/markdown.py new file mode 100644 index 0000000..7c1780d --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/markdown.py @@ -0,0 +1,127 @@ +"""Границы чужого текста в Markdown: код, цитаты, короткие «ёлочки». + +Сканер измеряет слова автора. Листинг кода, markdown-цитата через «>» и короткая +цитата в ёлочках это чужие слова, их баны и маркеры не в счёт. Два релиза +подряд границы чинились регулярками по одному примеру, и каждый раз соседний +вариант той же ошибки оставался (ограда внутри строки кода, закрытие оградой +длиннее открывающей, продолжение цитаты без «>»). Здесь один построчный разбор +состояния по правилам CommonMark и таблица тестов на варианты в +scripts/test_markers.py. + +Две проекции одного текста: +- lexical: чужое заменено заглушкой GAP посимвольно, переводы строк на месте. + Смещения и номера строк совпадают с исходником, фразовые регексы не + склеивают слова через заглушку (она не буква). +- prose: чужое вырезано целиком. Для ритма, морфологии и структуры: заглушки + ломали бы длину предложений и подсчёт абзацев. + +Правила: +- Ограда кода: 3+ обратных кавычек или тильд с отступом до 3 пробелов в начале + строки. Закрывает ограда того же символа НЕ КОРОЧЕ открывающей. Кавычки внутри + строки кода оградой не являются. Незакрытая ограда не маскируется вовсе: + для сканера ложное срабатывание дешевле спрятанного абзаца. +- Цитата: строка с «>» в начале (отступ до 3 пробелов). Ленивое продолжение: + следующие непустые строки без «>» остаются цитатой, пока не встретится + пустая строка, заголовок, пункт списка или ограда. +- Инлайн-код: серия обратных кавычек закрывается серией той же длины в + пределах строки (`x`, ``a`b``). +- Короткая цитата в ёлочках (до QUOTE_MAX_WORDS слов) заглушается только в + lexical: так цитируют слово или оборот. Длинная остаётся под сканером, это + прямая речь или пересказ, её маркеры на совести автора. В prose ёлочки не + трогаем, они внутри предложений автора. +""" + +from __future__ import annotations + +import re + +GAP = "·" +QUOTE_MAX_WORDS = 12 + +_FENCE_OPEN = re.compile(r"^ {0,3}(`{3,}|~{3,})(.*)$") +_FENCE_CLOSE = re.compile(r"^ {0,3}(`{3,}|~{3,})\s*$") +_QUOTE_LINE = re.compile(r"^ {0,3}>") +# Строки, которые прерывают ленивое продолжение цитаты (CommonMark 5.1): +# заголовок, пункт списка, горизонтальная линия, ограда. +_BLOCK_START = re.compile(r"^ {0,3}(?:#{1,6}\s|[-*+]\s|\d{1,9}[.)]\s|(?:-{3,}|\*{3,}|_{3,})\s*$|`{3,}|~{3,})") +_INLINE_CODE = re.compile(r"(`+)(?!`)([^`\n]+?)\1(?!`)") +_QUOTE_MARKS = re.compile(r"«[^«»]*»") + + +def _blank(s: str) -> str: + return re.sub(r"[^\n]", GAP, s) + + +def _fence_ranges(lines: list[str]) -> list[tuple[int, int]]: + """Пары (первая, последняя) строк закрытых блоков кода, включая ограды.""" + ranges: list[tuple[int, int]] = [] + i = 0 + while i < len(lines): + m = _FENCE_OPEN.match(lines[i]) + # Инфо-строка ограды из кавычек не может содержать кавычки (CommonMark). + if not m or (m.group(1)[0] == "`" and "`" in m.group(2)): + i += 1 + continue + ch, need = m.group(1)[0], len(m.group(1)) + j = i + 1 + while j < len(lines): + c = _FENCE_CLOSE.match(lines[j]) + if c and c.group(1)[0] == ch and len(c.group(1)) >= need: + break + j += 1 + if j >= len(lines): + i += 1 # незакрытая ограда: обычная строка + continue + ranges.append((i, j)) + i = j + 1 + return ranges + + +def _quote_lines(lines: list[str], in_code: set[int]) -> set[int]: + quoted: set[int] = set() + inside = False + for i, line in enumerate(lines): + if i in in_code: + inside = False + continue + if _QUOTE_LINE.match(line): + inside = True + quoted.add(i) + elif inside and line.strip() and not _BLOCK_START.match(line): + quoted.add(i) # ленивое продолжение абзаца цитаты + else: + inside = False + return quoted + + +def _classify(text: str) -> tuple[list[str], set[int], set[int]]: + lines = text.split("\n") + in_code: set[int] = set() + for a, b in _fence_ranges(lines): + in_code.update(range(a, b + 1)) + return lines, in_code, _quote_lines(lines, in_code) + + +def _blank_short_quote(m: re.Match[str]) -> str: + inner = m.group(0)[1:-1] + return _blank(m.group(0)) if len(inner.split()) <= QUOTE_MAX_WORDS else m.group(0) + + +def mask_foreign(text: str) -> str: + """lexical: код, цитаты и короткие ёлочки заглушены, смещения сохранены.""" + lines, in_code, quoted = _classify(text) + out: list[str] = [] + for i, line in enumerate(lines): + if i in in_code or i in quoted: + out.append(_blank(line)) + else: + out.append(_INLINE_CODE.sub(lambda m: _blank(m.group(0)), line)) + return _QUOTE_MARKS.sub(_blank_short_quote, "\n".join(out)) + + +def strip_foreign(text: str) -> str: + """prose: код и цитаты вырезаны, инлайн-код удалён, ёлочки на месте.""" + lines, in_code, quoted = _classify(text) + kept = [_INLINE_CODE.sub("", line) for i, line in enumerate(lines) + if i not in in_code and i not in quoted] + return "\n".join(kept) diff --git a/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/markers.py b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/markers.py new file mode 100644 index 0000000..a3fc6c0 --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/markers.py @@ -0,0 +1,442 @@ +"""Лексические маркеры AI-текста: HARD BANS и быстрый сканер. + +Вордлисты вынесены один-в-один из прозы скилла: HARD BANS — из SKILL.md +(раздел «Жёсткие запреты»), категории сканера — из references/catalog.md +(раздел «Быстрый сканер: слова-маркеры AI», списки вынесены туда в v3.15.0). +Это детерминированная, грепабельная половина +режима «Аудит» — то, что не требует LLM. Семантика (кальки, ирония, translationese) +тут не ловится принципиально, для неё нужен сам скилл. + +Источник правды — SKILL.md и references/catalog.md. При обновлении +банов/маркеров правь оба файла; +scripts/lint_skill.py проверяет, что списки не разошлись по числу пунктов. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field + +# --- HARD BANS ----------------------------------------------------------- +# 20 конструкций с абсолютным запретом. Каждая — (имя, regex). Regex +# регистронезависимый, по словоформам где это безопасно. +HARD_BANS: list[tuple[str, str]] = [ + ("Не просто X, а Y", r"\bне\s+просто\b[^.!?]{1,40}?\bа\b"), + ("Не только X, но и Y", r"\bне\s+только\b[^.!?]{1,40}?\bно\s+и\b"), + ("В современном мире", r"\bв\s+современном\s+мире\b"), + ("Стоит отметить, что", r"\bстоит\s+отметить\b"), + ("Важно понимать, что", r"\bважно\s+понимать\b"), + # Только единственное число (как в SKILL.md HARD BAN). Множественные формы + # «данные/данных/данным/данными» совпадают с существительным «данные» (data) + # и давали ложные срабатывания на ML/статистических текстах. + ("Данный/Данная/Данное", r"\bданн(ый|ая|ое|ого|ой|ом|ую)\b"), + ("Является", r"\bявля(ется|ются)\b"), + ("Играет важную/ключевую роль", r"\bигра(ет|ют)\s+(важн\w+|ключев\w+)\s+роль\b"), + ("Можно с уверенностью сказать", r"\bможно\s+с\s+уверенностью\s+сказать\b"), + # «Таким образом» банится только как ВВОДНЫЙ коннектор-вывод (начало + # предложения либо обособлен запятыми с запятой следом): «...рынки. Таким + # образом, ...». Омонимичное обстоятельство образа действия «...таким + # образом, что...» (= «такой манерой») — живая речь, не AI-клише, под бан не + # идёт: перед ним стоит слово, а не граница предложения/запятая. На корпусе + # сужение не теряет ни одного AI-вхождения (все 7/7 raw — вводные «. Таким + # образом,»), но снимает ложные срабатывания на художественной прозе. + # «Подводя итог» однозначно итожащее — остаётся широким. + ("Подводя итог / Таким образом", + r"\bподводя\s+итог\b|(?:^|[.!?…:;]\s+|\n\s*(?:[-*•]\s*)?|,\s*)таким\s+образом\s*,"), + ("Длинное тире", r"—"), + # Короткое тире «–» (U+2013). Отдельным баном, потому что промахивается мимо + # регулярки на «—», а семейство gemma подменяет им длинное: парный замер на + # одних и тех же постах дал человека 6% против 80% у gemma3:4b и gemma3:27b, + # 64% у gemma3:1b. У остальных семейств сигнала нет (qwen 6-10%, Opus 0%), + # то есть это признак семейства, а не машинности вообще, но по силе лифта + # он обходит почти весь каталог. В русской типографике «–» законно только в + # числовых диапазонах, их и исключаем. + ("Короткое тире", r"(?", # остатки рассуждения DeepSeek и др. + # Замечены в 2026 (Wikipedia AI Cleanup, разделы про артефакты разметки): + r"re::::writing\{variant=", # обёртка документа, с июня 2026 + r"re:\[cite:\s*\d+\]", # метка цитаты Gemini + r"re:\[span_\d+\]\(start_span\)", # обёртка фрагмента Gemini + "grok_render_citation_card_json", # карточка цитаты Grok + ], +} + +@dataclass +class MarkerHit: + category: str + marker: str + count: int + positions: list[int] = field(default_factory=list) + + +def _find_all(text: str, needle_regex: str) -> list[int]: + return [m.start() for m in re.finditer(needle_regex, text, re.IGNORECASE)] + + +def scan_hard_bans(text: str) -> list[MarkerHit]: + """Находит конструкции из HARD BANS. Любое попадание = провал.""" + hits: list[MarkerHit] = [] + for name, pat in HARD_BANS: + pos = _find_all(text, pat) + if pos: + hits.append(MarkerHit("HARD BAN", name, len(pos), pos)) + return hits + + +# Частотные пороги из SKILL.md: маркер банится не с первого вхождения, а по +# плотности. «Является» — легитимная связка в меру (термины, определения): +# таблица HARD BANS задаёт «>1 раз на 500 слов». Одиночное вхождение остаётся +# мягким маркером («Кальки»), scan_hard_bans его по-прежнему находит. +FREQ_BANS: dict[str, int] = {"Является": 500} + + +# --- Почему тире остаётся баном, хотя частоты говорят обратное ------------- +# Класс ошибки, который тут однажды был допущен и откачен в тот же день. +# +# Если считать ДОЛЮ ДОКУМЕНТОВ в несопряжённых корпусах, тире выглядит признаком +# человека: AINL человек 4.8% против 0.0-0.9% у моделей, M4 человек 42.3% против +# 3.3%, Пикабу человек 14.0%. Вывод напрашивается сам, и он неверный по двум +# причинам сразу: человеческие половины там это изданная вычитанная проза, где +# тире ставит редактор, а машинные половины сделаны генераторами 2023-2024 годов. +# +# Продуктовый вопрос звучит не так. Пользователь отдаёт модели СВОЙ текст, и +# важно, что она с ним делает. Это парный замер на одних и тех же постах, и он +# даёт противоположный ответ (доля пар, где тире не было у человека, но +# появилось у машины): +# +# Qwen3-4B-heretic 45 из 45 человек 10% -> машина 100% +# qwen2.5:7b 19 из 31 человек 11% -> машина 66% +# qwen3-vl:4b 5 из 9 человек 10% -> машина 60% +# Opus 5 4 из 15 человек 0% -> машина 27% +# gemma3:27b 4 из 45 человек 10% -> машина 14% +# +# Семейство gemma тире почти не добавляет, но подменяет его коротким (см. ниже). +# +# Ложные срабатывания на изданной прозе при этом реальны, и лечатся они не +# отменой бана, а жанровым фильтром: в academic и legal тире уже снимается. +FREQ_BANS_NOTE = "см. комментарий выше про парный замер" + + +# --- Жанровый фильтр ------------------------------------------------------ +# Часть банов в отдельных жанрах не признак машины, а норма регистра. Для +# академического текста это измерено: прогон по AINL-Eval 2025 (35 158 русских +# научных аннотаций с разметкой human / gpt-4-turbo / gemma-2-27b / +# llama-3.3-70b) дал 35.9% документов с баном у ЛЮДЕЙ против 26.1-35.8% у +# моделей: маркеры не разделяли классы вовсе. «Является» сработало на 8.0% +# человеческих текстов и 0.1-3.0% машинных, длинное тире на 4.8% у людей и +# 0.0-0.9% у моделей. С фильтром ниже человеческие срабатывания падают до 4.2%. +# Цифры и метод: eval/ainl_calibration.py, SOURCES.md. +# +# Жанры legal и fiction фильтруются по тем же правилам, что записаны в разделе +# «Ложные срабатывания» SKILL.md; корпусной проверки под них у нас пока нет. +GENRE_MUTED_BANS: dict[str, set[str]] = { + "academic": { + "Является", "Длинное тире", "Короткое тире", "Данный/Данная/Данное", + "В условиях [прил.] [сущ.]", "Подводя итог / Таким образом", + "В связи с этим", + }, + "legal": { + "Является", "Данный/Данная/Данное", "В условиях [прил.] [сущ.]", + "В связи с этим", "Длинное тире", "Короткое тире", + }, + "fiction": { + "Длинное тире", "Короткое тире", "от X до Y (ложный диапазон)", + "Не только X, но и Y", + "Не просто X, а Y", + }, +} + +GENRE_MUTED_CATEGORIES: dict[str, set[str]] = { + "academic": {"Канцелярит", "Кальки", "Контекстуализаторы", "Формула-выводы"}, + "legal": {"Канцелярит", "Кальки", "Контекстуализаторы"}, + "fiction": {"Параллелизмы"}, +} + +GENRES = ("marketing", "academic", "legal", "fiction") + + +# --- Что проверено корпусом, а что нет ------------------------------------ +# Каталог собирался вручную, и до 2026 года ни один его пункт не был проверен +# на том, отличает ли он вообще человека от машины. Теперь часть проверена, и +# честнее показывать разницу, чем выдавать всё за одинаково обоснованное. +# +# Ниже категории, которые разделяют классы В ДВУХ РАЗНЫХ ДОМЕНАХ сразу: на +# научных аннотациях (AINL-Eval 2025, 35 158 текстов, генераторы gpt-4-turbo, +# gemma-2-27b, llama-3.3-70b) и на смешанном регистре (M4-ru, 11 518 текстов, +# генераторы gpt-3.5-turbo и davinci-003). Число это лифт, отношение частоты у +# машины к частоте у человека; всё, что ниже 1, работает против нас. +# +# Остальные пункты каталога либо не проверялись, либо переворачиваются при +# смене домена: «Является» даёт 0.1 на научном тексте и 4.5 на смешанном, +# «Данный» 0.9 и 3.1, кальки 0.3 и 2.0. Канцелярит не разделяет ни там, ни там, +# хотя в каталоге назван главным маркером. Это не повод их выбрасывать: маркер +# без указания домена просто бессмыслен, о чём написано в SOURCES.md. +CROSS_DOMAIN_VERIFIED: dict[str, tuple[float, float]] = { + # категория или бан: (лифт на AINL, лифт на M4) + "Играет важную/ключевую роль": (5.7, 9.4), + "Мотивационные клише": (2.8, 4.0), + "Неодушевлённый субъект": (4.4, 2.7), + "Комплексный подход/решение": (3.0, 2.8), + "Модальные хеджи": (2.6, 2.9), + "Раздувание значимости": (1.8, 1.4), +} + + +def mute_by_genre(hits: list[MarkerHit], genre: str | None, + muted: dict[str, set[str]]) -> list[MarkerHit]: + """Снимает маркеры, законные для жанра. Без жанра ничего не снимает: + умолчание остаётся строгим (маркетинг и блоги, под них калибровка).""" + names = muted.get(genre or "", set()) + return [h for h in hits if h.marker not in names and h.category not in names] + + +def effective_hard_bans(hits: list[MarkerHit], words: int) -> list[MarkerHit]: + """Хард-баны после частотных порогов: то, что реально штрафуется/валит exit. + + Для маркеров из FREQ_BANS попадание остаётся баном только при двух и более + вхождениях с плотностью выше 1 на N слов. Остальные баны проходят как есть. + """ + out: list[MarkerHit] = [] + for h in hits: + per = FREQ_BANS.get(h.marker) + if per and (h.count < 2 or h.count <= words / per): + continue + out.append(h) + return out + + +def scan_markers(text: str) -> list[MarkerHit]: + """Прогоняет лексические категории быстрого сканера (SCANNER). Возвращает только попадания. + + Элемент с префиксом "re:" трактуется как регулярное выражение, + остальные — как литеральная подстрока (регистронезависимо). + """ + hits: list[MarkerHit] = [] + for cat, phrases in SCANNER.items(): + for phrase in phrases: + if isinstance(phrase, tuple): + label, pat = phrase + elif phrase.startswith("re:"): + label, pat = phrase[3:], phrase[3:] + else: + label, pat = phrase, re.escape(phrase) + pos = _find_all(text, pat) + if pos: + hits.append(MarkerHit(cat, label, len(pos), pos)) + return hits + + +def marker_verdict(marker_hits: list[MarkerHit]) -> str: + """Шкала из SKILL.md: 0-2 чисто, 3-5 подозрительно, 6+ AI. + Артефакт копипасты — однозначная улика: вердикт сразу, без шкалы.""" + if any(h.category == "Артефакты копипасты" for h in marker_hits): + return "артефакты копипасты из чат-бота — текст вставлен из ответа ИИ" + total = sum(h.count for h in marker_hits) + if total <= 2: + return f"{total} — скорее всего чистый текст" + if total <= 5: + return f"{total} — подозрительно" + return f"{total} — AI-генерация с высокой вероятностью" diff --git a/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/morphology.py b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/morphology.py new file mode 100644 index 0000000..7e135de --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/morphology.py @@ -0,0 +1,78 @@ +"""Морфология: соотношение существительных к глаголам через pymorphy3. + +Закрывает пункт чеклиста «соотношение существительных к глаголам ≤ 2.5:1» — +тот, который LLM не умеет считать на глаз. У AI-русского ~3:1, у людей ~2:1 +(SKILL.md, паттерн #14, Biber framework). Чем выше отношение, тем «номинальнее» +текст, тем сильнее канцелярит. +""" + +from __future__ import annotations + +import functools +from dataclasses import dataclass + +import pymorphy3 +from razdel import tokenize + + +@functools.lru_cache(maxsize=1) +def _morph() -> "pymorphy3.MorphAnalyzer": + return pymorphy3.MorphAnalyzer() + + +@dataclass +class MorphStats: + nouns: int + verbs: int # глаголы + инфинитивы + деепричастия + причастия как глагольные формы + noun_verb_ratio: float + nominalizations: int # отглагольные существительные (потенциальный канцелярит) + tokens: int + + def as_dict(self) -> dict: + return self.__dict__.copy() + + +# Что считаем «глагольным» — pymorphy3 теги (OpenCorpora): +_VERBAL = {"VERB", "INFN", "GRND", "PRTF", "PRTS"} +_NOUN = {"NOUN"} +_NOMINAL_SUFFIXES = ("ение", "ание", "ания", "ения", "ация", "изация", "ировка", "ость", "остей") + + +def _is_cyrillic_word(tok: str) -> bool: + return any("а" <= c.lower() <= "я" or c.lower() == "ё" for c in tok) + + +def morph_stats(text: str) -> MorphStats: + m = _morph() + nouns = verbs = nominal = total = 0 + for t in tokenize(text): + w = t.text + if not _is_cyrillic_word(w): + continue + total += 1 + p = m.parse(w)[0] + pos = p.tag.POS + if pos in _NOUN: + nouns += 1 + lemma = p.normal_form + if lemma.endswith(_NOMINAL_SUFFIXES): + nominal += 1 + elif pos in _VERBAL: + verbs += 1 + ratio = (nouns / verbs) if verbs else float(nouns) + return MorphStats( + nouns=nouns, + verbs=verbs, + noun_verb_ratio=round(ratio, 2), + nominalizations=nominal, + tokens=total, + ) + + +NV_TARGET = 2.5 # из чеклиста SKILL.md + + +def morph_verdict(s: MorphStats) -> str: + if s.noun_verb_ratio <= NV_TARGET: + return f"✓ сущ./глаг. = {s.noun_verb_ratio} (цель ≤{NV_TARGET})" + return f"⚠ сущ./глаг. = {s.noun_verb_ratio} (цель ≤{NV_TARGET}, канцелярит); номинализаций: {s.nominalizations}" diff --git a/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/score.py b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/score.py new file mode 100644 index 0000000..16074a7 --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/score.py @@ -0,0 +1,182 @@ +"""Score чистоты: сворачивает детерминированные сигналы в одно число 0-100. + +Выше = текст читается живее/человечнее. Это НЕ детектор и НЕ вероятность ИИ: +просто агрегат уже считаемых метрик (хард-баны, маркеры, ритм, морфология), +поданный как обратная связь «было/стало». Опирается на пороги, которые уже +откалиброваны в burstiness/morphology и задокументированы в eval/RESULTS.md. + +Пороги штрафов подобраны на eval/corpus (human → высокий score, raw-AI → +низкий, humanized → между). Калибровочный прогон: см. eval/run_eval.py. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from .burstiness import CV_HUMAN_TARGET +from .markers import (GENRE_MUTED_BANS, GENRE_MUTED_CATEGORIES, + effective_hard_bans, mute_by_genre) +from .morphology import NV_TARGET +from .structure import ( + LISTICLE_MIN_ITEMS, + LISTICLE_SHARE_AI, + PARA_CV_AI, + PARA_MIN_COUNT, +) + +# Тире — отдельный случай: оно и хард-бан, и штатная русская пунктуация +# (Википедия, диапазоны, «это —»). Поэтому в score не рубим потолком, а +# штрафуем по плотности с допуском. Имя берём из HARD_BANS markers.py. +EM_DASH_NAME = "Длинное тире" +COPY_PASTE_CATEGORY = "Артефакты копипасты" + +# Полосы. Совпадают с порогами вмешательства из SKILL.md. +BAND_CLEAN = 85 # ≥ — следы ИИ не мешают, не править +BAND_EDIT = 60 # ≥ — точечная правка; < — полный рерайт + +# Доля ЛЮДЕЙ, у которых текст такой длины совсем без банов и без маркеров. +# Измерено на 14 973 человеческих текстах (Пикабу, M4, AINL), см. eval/. +# +# Зачем это здесь. Score мерит расстояние до нуля, а не до человека, и молча +# внушает, что цель это сто из ста. Человек нулём почти не бывает: на тексте в +# три сотни слов идеально чистых людей 15%, а не 100%. То есть высшая оценка +# означает попадание в редкий хвост распределения. Один такой текст неотличим; +# корпус из тысячи таких текстов отличим тривиально, потому что у людей хвост +# есть, а у вычищенного корпуса его нет. +# +# Штрафовать за чистоту мы не будем: это сломало бы сравнение «было и стало». +# Вместо этого при нулевом счёте отдаём заметку, чтобы цель читалась как +# «попади в типичную частоту», а не как «вычисти всё». +HUMAN_ZERO_SHARE: tuple[tuple[int, float], ...] = ((100, 42.2), (200, 21.0), (400, 15.1)) +STERILE_MIN_WORDS = 100 + + +@dataclass +class ScoreResult: + score: int # 0-100, выше = чище + band: str # "чисто" | "правка" | "рерайт" + penalties: list[tuple[str, int]] # (причина, -очки) — это и есть verbose-отчёт + notes: list[str] = field(default_factory=list) # замечания без штрафа + + def as_dict(self) -> dict: + return { + "score": self.score, + "band": self.band, + "penalties": [{"reason": r, "points": p} for r, p in self.penalties], + "notes": list(self.notes), + } + + +def _band(score: float) -> str: + if score >= BAND_CLEAN: + return "чисто" + if score >= BAND_EDIT: + return "правка" + return "рерайт" + + +def _per100(count: int, words: int) -> float: + return (count / words * 100) if words else 0.0 + + +def cleanliness_score(report, genre: str | None = None) -> ScoreResult: + """Считает score 0-100 из готового Report (см. humanizer_metrics.analyze). + + genre снимает штрафы за маркеры, законные для регистра (научный, + юридический, художественный). Без genre режим строгий, как раньше. + """ + words = report.rhythm.words or 1 + markers = mute_by_genre(report.markers, genre, GENRE_MUTED_CATEGORIES) + dash_muted = EM_DASH_NAME in GENRE_MUTED_BANS.get(genre or "", set()) + penalties: list[tuple[str, int]] = [] + notes: list[str] = [] + score = 100.0 + + # 1. Фразовые хард-баны (кроме тире). Однозначные AI-обороты: дорого. + # Частотные баны («Является») штрафуются только выше порога плотности. + eff_bans = mute_by_genre( + effective_hard_bans(report.hard_bans, report.rhythm.words), + genre, GENRE_MUTED_BANS) + hard_phrase = sum(h.count for h in eff_bans if h.marker != EM_DASH_NAME) + if hard_phrase: + pen = min(45, 12 * hard_phrase) + score -= pen + penalties.append((f"хард-баны (фразы): {hard_phrase}", -pen)) + + # 2. Артефакты копипасты из чат-бота: текст буквально вставлен из ответа ИИ. + copy_paste = sum(h.count for h in markers if h.category == COPY_PASTE_CATEGORY) + if copy_paste: + pen = 60 + score -= pen + penalties.append((f"артефакты копипасты: {copy_paste}", -pen)) + + # 3. Мягкие маркеры (кроме копипасты) по плотности на 100 слов. + soft = sum(h.count for h in markers if h.category != COPY_PASTE_CATEGORY) + if soft: + pen = min(30, round(2 * _per100(soft, words))) + if pen: + score -= pen + penalties.append((f"маркеры: {soft} ({_per100(soft, words):.1f}/100 слов)", -pen)) + + # 4. Длинное тире по плотности с допуском ~2 на 100 слов: «—» штатно + # используется в русском (Википедия, «это —», диапазоны). Штраф мягкий, + # потому что на изданной прозе оно частая норма; сам бан держится на + # парном замере (markers.py, комментарий про парный замер). + dash_density = 0.0 if dash_muted else _per100(report.rhythm.em_dash, words) + if dash_density > 2.0: + pen = min(8, round(3 * (dash_density - 2.0))) + if pen: + score -= pen + penalties.append((f"тире: {report.rhythm.em_dash} ({dash_density:.1f}/100 слов)", -pen)) + + # 5. Ровный ритм: чем ниже CV относительно цели 0.45, тем больше штраф. + cv = report.rhythm.cv_len + if report.rhythm.sentences >= 4 and cv < CV_HUMAN_TARGET: + pen = min(20, round((CV_HUMAN_TARGET - cv) / CV_HUMAN_TARGET * 30)) + if pen: + score -= pen + penalties.append((f"ровный ритм (CV={cv}, цель ≥{CV_HUMAN_TARGET})", -pen)) + + # 6. Номинальность: сущ./глаг. выше цели 2.5 = канцелярит. Слабый сигнал и + # главный источник ложных срабатываний (энциклопедический/юр. регистр + # легитимно номинален), поэтому штраф мягкий и низко ограничен. + nv = report.morph.noun_verb_ratio + if nv > NV_TARGET: + pen = min(8, round((nv - NV_TARGET) / 0.5 * 3)) + if pen: + score -= pen + penalties.append((f"номинальность (сущ./глаг.={nv}, цель ≤{NV_TARGET})", -pen)) + + # 7. Document-level: ровные по длине абзацы (burstiness абзацев). Срабатывает + # только на достаточно многоабзацном тексте, иначе инертно (короткая проза). + st = report.structure + if st.paragraphs >= PARA_MIN_COUNT and st.para_cv < PARA_CV_AI: + pen = min(10, round((PARA_CV_AI - st.para_cv) / PARA_CV_AI * 20)) + if pen: + score -= pen + penalties.append((f"ровные абзацы (CV={st.para_cv}, цель ≥{PARA_CV_AI})", -pen)) + + # 8. Document-level: listicle-сигнатура (засилье однотипных пунктов). Инертно + # на прозе без списков, бьёт по шаблонным гайдам/постам. + if st.list_items >= LISTICLE_MIN_ITEMS and st.listicle_share > LISTICLE_SHARE_AI: + pen = min(12, round((st.listicle_share - LISTICLE_SHARE_AI) * 30)) + if pen: + score -= pen + penalties.append((f"листикл ({st.list_items} пунктов, {int(st.listicle_share*100)}% строк)", -pen)) + + # Заметка о стерильности. Не штраф: цель показать, что ноль это не середина + # человеческого распределения, а его редкий край. + # + # Условие ровно то, что измерялось: ноль банов и ноль маркеров. Ритм, + # номинальность и структура сюда не входят, иначе текст с минусом за ровный + # ритм терял бы заметку, хотя по лексике он как раз стерилен. + if not (hard_phrase or copy_paste or soft) and words >= STERILE_MIN_WORDS: + share = next((s for limit, s in HUMAN_ZERO_SHARE if words < limit), + HUMAN_ZERO_SHARE[-1][1]) + notes.append( + f"стерильно: ни одного маркера. Так пишет {share:.0f}% людей на тексте " + f"в {words} слов, остальные {100 - share:.0f}% что-нибудь да используют. " + "Цель не ноль, а типичная для жанра частота: вычищать дальше незачем") + + final = max(0, min(100, round(score))) + return ScoreResult(score=final, band=_band(final), penalties=penalties, notes=notes) diff --git a/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/structure.py b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/structure.py new file mode 100644 index 0000000..5ba256c --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/scripts/humanizer_metrics/structure.py @@ -0,0 +1,154 @@ +"""Структурные метрики уровня документа: burstiness абзацев и listicle-сигнатура. + +Дополняет burstiness.py (там ритм ПРЕДЛОЖЕНИЙ) тем, что детекторы оценивают на +УРОВНЕ ДОКУМЕНТА: одинаковая длина абзацев и засилье однотипных списков выдают +шаблонную генерацию, даже когда каждое предложение по отдельности вычищено. + +Эти сигналы целят в многосекционный/листикл-текст (посты, гайды). На прозе +одним блоком они инертны (мало абзацев, нет списков) и в score не вносят штраф, +поэтому не задевают калибровку основного корпуса. +""" + +from __future__ import annotations + +import re +import statistics +from dataclasses import dataclass + +from razdel import sentenize + +from .morphology import _morph + +# Строка-пункт списка: маркер «- * •» или «1. / 1)» в начале строки. +_LIST_RE = re.compile(r"^\s*(?:[-*•]|\d+[.)])\s+\S") + +# Заголовок: markdown-решётки либо короткая строка без конечной пунктуации, +# за которой идёт пустая строка или абзац. +_HEADING_RE = re.compile(r"^\s*(?:#{1,6}\s+)?(\S.{0,79})$") +_WORD_RE = re.compile(r"[А-Яа-яЁё][А-Яа-яЁё-]*") +# Имя собственное капитализируется законно: тег pymorphy3 или незнакомое +# словарю слово (Стэнфорд, Хогвартс). Считаем только заглавные у нарицательных. +_PROPER_TAGS = {"Name", "Surn", "Geox", "Orgn", "Patr"} +# Служебные слова в заголовках пишутся строчными и в английском Title Case, +# поэтому их регистр ничего не говорит. +_STOPWORDS = {"и", "в", "на", "с", "к", "по", "для", "из", "о", "об", "а", "но", "или", "у", "за"} + + +@dataclass +class StructureStats: + paragraphs: int # число непустых абзацев (разделитель — пустая строка) + para_mean_sent: float # средняя длина абзаца в предложениях + para_cv: float # коэффициент вариации длин абзацев (burstiness абзацев) + list_items: int # строк-пунктов списка + listicle_share: float # доля строк-пунктов среди непустых строк + title_case_headings: int # заголовки в английском Title Case (паттерн #56) + truncated_ending: bool # текст оборван на полуслове (паттерн #57) + + def as_dict(self) -> dict: + return self.__dict__.copy() + + +def _is_common_noun_capitalized(word: str) -> bool: + """Слово с заглавной, которое словарь знает как нарицательное: «Жизнь», + «Образование». Имена собственные и незнакомые словарю слова не в счёт.""" + if not word[:1].isupper(): + return False + if word.lower() in _STOPWORDS: + return False + p = _morph().parse(word.lower())[0] + if not p.is_known: + return False + return not (_PROPER_TAGS & set(p.tag.grammemes)) + + +def count_title_case_headings(text: str) -> int: + """Заголовки, где с заглавной стоит не только первое слово: «Ранняя Жизнь + и Образование». В русском так не пишут, это калька с английского.""" + hits = 0 + lines = text.splitlines() + for i, raw in enumerate(lines): + s = raw.strip() + if not s: + continue + is_md_heading = s.startswith("#") + # Строка без конечной пунктуации, короткая, за ней пусто: тоже заголовок. + looks_like_heading = ( + len(s) <= 80 and s[-1] not in ".!?:;," + and (i + 1 >= len(lines) or not lines[i + 1].strip()) + and not _LIST_RE.match(raw) + ) + if not (is_md_heading or looks_like_heading): + continue + words = _WORD_RE.findall(s) + if len(words) < 3: + continue # на двух словах отличить заголовок от имени нельзя + if sum(1 for w in words[1:] if _is_common_noun_capitalized(w)) >= 2: + hits += 1 + return hits + + +def is_truncated(text: str) -> bool: + """Текст оборван на полуслове: модель упёрлась в лимит токенов, а человек + скопировал как есть. Последняя содержательная строка прозы обязана + заканчиваться знаком конца предложения.""" + lines = [ln.rstrip() for ln in text.splitlines() if ln.strip()] + if not lines: + return False + last = lines[-1] + if last.startswith("#") or _LIST_RE.match(last) or last.startswith(("|", ">", "```")): + return False # заголовок, пункт списка, таблица: пунктуация не нужна + if len(_WORD_RE.findall(last)) < 4: + return False # подпись, дата, короткая пометка + return last[-1] not in ".!?…:»)\"'*_" + + +def structure_stats(text: str) -> StructureStats: + paras = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()] + sent_lens = [len(list(sentenize(p))) for p in paras] + sent_lens = [n for n in sent_lens if n > 0] + n = len(sent_lens) + + mean = statistics.mean(sent_lens) if sent_lens else 0.0 + stdev = statistics.pstdev(sent_lens) if n > 1 else 0.0 + cv = (stdev / mean) if mean else 0.0 + + lines = [ln for ln in text.splitlines() if ln.strip()] + list_items = sum(1 for ln in lines if _LIST_RE.match(ln)) + share = (list_items / len(lines)) if lines else 0.0 + + return StructureStats( + paragraphs=n, + para_mean_sent=round(mean, 1), + para_cv=round(cv, 3), + list_items=list_items, + listicle_share=round(share, 3), + title_case_headings=count_title_case_headings(text), + truncated_ending=is_truncated(text), + ) + + +# Пороги (эвристика, согласована с логикой burstiness.py для предложений). +# Срабатывают только на достаточно длинном/структурированном тексте, чтобы не +# штрафовать короткую прозу одним-двумя абзацами. +PARA_CV_AI = 0.35 # ниже = слишком ровные по длине абзацы +PARA_MIN_COUNT = 6 # по нескольким коротким абзацам cv ненадёжен (малая + # выборка), судим о ритме абзацев только на длинном тексте +LISTICLE_SHARE_AI = 0.30 # доля строк-пунктов выше — текст «листиклом» +LISTICLE_MIN_ITEMS = 6 # и не меньше стольких пунктов (порог шаблонности) + + +def structure_verdict(s: StructureStats) -> str: + parts = [] + if s.paragraphs >= PARA_MIN_COUNT and s.para_cv < PARA_CV_AI: + parts.append(f"⚠ ровные абзацы (CV={s.para_cv}, цель ≥{PARA_CV_AI})") + else: + parts.append(f"✓ абзацы: {s.paragraphs}, CV={s.para_cv}") + if s.list_items >= LISTICLE_MIN_ITEMS and s.listicle_share > LISTICLE_SHARE_AI: + parts.append(f"⚠ листикл ({s.list_items} пунктов, {int(s.listicle_share*100)}% строк)") + elif s.list_items: + parts.append(f"пунктов списка: {s.list_items}") + if s.title_case_headings: + parts.append(f"⚠ Title Case в заголовках: {s.title_case_headings} (#56)") + if s.truncated_ending: + parts.append("⚠ обрыв на полуслове (#57)") + return "; ".join(parts) diff --git a/plugins/humanizer-ru/skills/humanizer-ru/scripts/scan.py b/plugins/humanizer-ru/skills/humanizer-ru/scripts/scan.py new file mode 100644 index 0000000..30b65a6 --- /dev/null +++ b/plugins/humanizer-ru/skills/humanizer-ru/scripts/scan.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""CLI-сканер: прогоняет текст через детерминированные метрики humanizer-ru. + +Использование (из корня репо; у установленного скилла путь: <папка скилла>/scripts/scan.py): + python skills/humanizer-ru/scripts/scan.py path/to/text.txt + echo "ваш текст" | python skills/humanizer-ru/scripts/scan.py - + python skills/humanizer-ru/scripts/scan.py text.txt --json + +Это машинная половина режима «Аудит». Для полного очеловечивания (семантика, +голос, рерайт) нужен сам скилл — этот сканер только подсвечивает грепабельные +маркеры и считает метрики, которые LLM не умеет измерять на глаз. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +try: + from humanizer_metrics import analyze, cleanliness_score, diff_facts, facts_verdict + from humanizer_metrics.burstiness import rhythm_verdict + from humanizer_metrics.markers import (GENRE_MUTED_BANS, GENRE_MUTED_CATEGORIES, + GENRES, effective_hard_bans, marker_verdict, + mute_by_genre) + from humanizer_metrics.morphology import morph_verdict + from humanizer_metrics.structure import structure_verdict +except ImportError as exc: + print(f"[ошибка] не хватает зависимостей сканера ({exc.name}).\n" + "Поставьте один раз: pip install razdel pymorphy3", file=sys.stderr) + raise SystemExit(2) + + +def _read(src: str) -> str: + if src == "-": + return sys.stdin.read() + try: + return Path(src).read_text(encoding="utf-8") + except FileNotFoundError: + print(f"[ошибка] файл не найден: {src}", file=sys.stderr) + raise SystemExit(2) + except (IsADirectoryError, UnicodeDecodeError) as exc: + print(f"[ошибка] не могу прочитать {src}: {exc}", file=sys.stderr) + raise SystemExit(2) + + +def _line_numbers(text: str, positions: list[int], limit: int = 3) -> str: + """Номера строк первых вхождений: «стр. 3, 7» — чтобы находку можно + было найти в тексте глазами.""" + lines = sorted({text.count("\n", 0, p) + 1 for p in positions}) + shown = ", ".join(str(n) for n in lines[:limit]) + more = f", +{len(lines) - limit}" if len(lines) > limit else "" + return f"стр. {shown}{more}" + + +def _cyrillic_share(text: str) -> float: + letters = re.findall(r"[^\W\d_]", text) + if not letters: + return 0.0 + cyr = sum(1 for ch in letters if "а" <= ch.lower() <= "я" or ch.lower() == "ё") + return cyr / len(letters) + + +def main() -> int: + ap = argparse.ArgumentParser(description="Детерминированный сканер AI-маркеров (humanizer-ru)") + ap.add_argument("source", help="файл с текстом или '-' для stdin") + ap.add_argument("--json", action="store_true", help="вывод в JSON") + ap.add_argument("--before", metavar="ФАЙЛ", + help="исходник до правки: сканер добавит «было/стало» по чистоте и " + "проверит факт-замок (числа, даты, имена, ссылки, код). " + "Новый факт, которого не было в исходнике, даёт exit 2") + ap.add_argument("--genre", choices=GENRES, default="marketing", + help="жанр текста: снимает маркеры, законные для регистра " + "(academic, legal, fiction). По умолчанию marketing: " + "строгий режим, под него откалиброваны пороги") + args = ap.parse_args() + + text = _read(args.source) + + if not text.strip(): + if args.json: + print(json.dumps({"empty": True}, ensure_ascii=False)) + else: + print("Текст пуст, сканировать нечего.") + return 0 + + rep = analyze(text) + genre = args.genre + sc = cleanliness_score(rep, genre) + before_text = _read(args.before) if args.before else None + before_sc = cleanliness_score(analyze(before_text), genre) if before_text else None + fdiff = diff_facts(before_text, text) if before_text is not None else None + # Частотные баны («Является» до порога 1/500 слов) не валят exit и не + # показываются как ⛔ — они остаются в мягких маркерах. + bans = effective_hard_bans(rep.hard_bans, rep.rhythm.words) + # Жанровый фильтр: в научном, юридическом и художественном регистре часть + # банов законна. Замер на AINL-Eval показал, что без фильтра сканер ловит + # людей чаще, чем модели (см. markers.GENRE_MUTED_BANS). + bans = mute_by_genre(bans, genre, GENRE_MUTED_BANS) + soft = mute_by_genre(rep.markers, genre, GENRE_MUTED_CATEGORIES) + muted_bans = len(effective_hard_bans(rep.hard_bans, rep.rhythm.words)) - len(bans) + muted_soft = len(rep.markers) - len(soft) + EM_DASH_MUTED = "Длинное тире" in GENRE_MUTED_BANS.get(genre, set()) + + if args.json: + out = rep.as_dict() + out["score"] = sc.as_dict() + # Ключи те же, что без жанра: меняется только состав после фильтра. + out["genre"] = genre + out["hard_bans"] = [(h.marker, h.count) for h in bans] + out["hard_ban_count"] = sum(h.count for h in bans) + out["markers"] = [(h.category, h.marker, h.count) for h in soft] + out["marker_count"] = sum(h.count for h in soft) + out["muted_by_genre"] = {"hard_bans": muted_bans, "markers": muted_soft} + if _cyrillic_share(text) < 0.3: + out["warning"] = "текст не похож на русский, метрики не применимы" + if fdiff is not None: + out["before"] = {"source": args.before, "score": before_sc.as_dict()} + out["facts"] = fdiff.as_dict() + print(json.dumps(out, ensure_ascii=False, indent=2)) + if fdiff is not None and not fdiff.ok: + return 2 + return 1 if bans else 0 + + print(f"=== humanizer-ru scan: {args.source} ===") + if genre != "marketing": + print(f"жанр: {genre} (снято маркеров по жанру: " + f"{muted_bans} банов, {muted_soft} мягких)") + print() + + if _cyrillic_share(text) < 0.3: + print("⚠ Текст не похож на русский: скилл и метрики рассчитаны на русский " + "язык, отчёт ниже не показателен.\n") + + if before_sc is not None: + print(f"ЧИСТОТА: было {before_sc.score}, стало {sc.score}/100 [{sc.band}]") + else: + print(f"ЧИСТОТА: {sc.score}/100 [{sc.band}]") + print(" (≥85 чисто · 60-84 точечная правка · <60 рерайт)") + if sc.penalties: + for reason, pts in sc.penalties: + print(f" {pts:+d} {reason}") + else: + print(" без штрафов") + for note in sc.notes: + print(f" ℹ {note}") + print() + + print("HARD BANS:") + if bans: + for h in bans: + note = " (штраф по плотности, допуск ~2/100 слов)" if h.marker == "Длинное тире" else "" + print(f" ⛔ {h.marker} ×{h.count} ({_line_numbers(text, h.positions)}){note}") + else: + print(" ✓ чисто") + print() + + print(f"Маркеры (быстрый сканер): {marker_verdict(soft)}") + top = sorted(soft, key=lambda x: -x.count) + for h in top[:12]: + print(f" • [{h.category}] «{h.marker}» ×{h.count} ({_line_numbers(text, h.positions)})") + if len(top) > 12: + print(f" … и ещё {len(top) - 12}") + print() + + print("Ритм / типографика:") + print(f" {rhythm_verdict(rep.rhythm, dash_ok=EM_DASH_MUTED)}") + print(f" предложений: {rep.rhythm.sentences}, средняя длина: {rep.rhythm.mean_len:.1f} " + f"(min {rep.rhythm.min_len} / max {rep.rhythm.max_len}), CV: {rep.rhythm.cv_len}") + print(f" многоточий: {rep.rhythm.ellipsis}, скобок: {rep.rhythm.parentheses}, " + f"вопросов: {rep.rhythm.questions}") + print() + + print("Морфология:") + print(f" {morph_verdict(rep.morph)}") + print(f" сущ.: {rep.morph.nouns}, глаг.форм: {rep.morph.verbs}, " + f"номинализаций: {rep.morph.nominalizations}") + print() + + print("Структура (уровень документа):") + print(f" {structure_verdict(rep.structure)}") + + if fdiff is not None: + print() + print(f"Факт-замок (против {args.before}):") + print(f" {facts_verdict(fdiff)}") + for x in fdiff.added: + print(f" ✗ новое: {x}") + for x in fdiff.lost: + print(f" ⚠ потеряно: {x}") + for x in fdiff.claims_added: + print(f" ⚠ утверждение появилось: {x}") + if fdiff.soft_added: + print(f" ℹ мелкие количества появились: {', '.join(fdiff.soft_added)} " + "(идиомы и пересказ чисел не считаются, проверьте глазами)") + if not fdiff.ok: + return 2 + + # Exit code: ненулевой, если есть HARD BANS — удобно для CI/pre-commit. + return 1 if bans else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/humanizer-ru b/skills/humanizer-ru new file mode 120000 index 0000000..832f53b --- /dev/null +++ b/skills/humanizer-ru @@ -0,0 +1 @@ +../plugins/humanizer-ru/skills/humanizer-ru \ No newline at end of file