humanizer-ru 3.28.0 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Claude Code / Cowork plugin. Kills AI smell in Russian text. The English [humanizer](https://github.com/blader/humanizer) won't help here. Russian AI markers are their own beast: bureaucratic noun-chains (канцелярит), English-syntax calques, missing particles like "же" and "ведь" that make Russian sound alive.
6
6
 
7
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
- [![Version](https://img.shields.io/badge/version-3.28.0-blueviolet)](https://github.com/ilyautov/humanizer-ru/blob/main/CHANGELOG.md)
8
+ [![Version](https://img.shields.io/badge/version-3.30.0-blueviolet)](https://github.com/ilyautov/humanizer-ru/blob/main/CHANGELOG.md)
9
9
  [![Stars](https://img.shields.io/github/stars/ilyautov/humanizer-ru?style=social)](https://github.com/ilyautov/humanizer-ru/stargazers)
10
10
  [![skills.sh](https://skills.sh/b/ilyautov/humanizer-ru)](https://skills.sh/ilyautov/humanizer-ru/humanizer-ru)
11
11
  [![npm](https://img.shields.io/npm/v/humanizer-ru?label=npm%20%C2%B7%20dsh)](https://www.npmjs.com/package/humanizer-ru)
@@ -13,7 +13,7 @@ Claude Code / Cowork plugin. Kills AI smell in Russian text. The English [humani
13
13
 
14
14
  <p align="center">
15
15
  <a href="https://humanizer-ru.aifrontier.tech/">
16
- <img src="assets/social-preview.png" alt="humanizer-ru: removes AI tells from Russian text. 64 patterns, 21 hard bans, scanner included" width="720">
16
+ <img src="assets/social-preview.png" alt="humanizer-ru: removes AI tells from Russian text. 67 patterns, 21 hard bans, scanner included" width="720">
17
17
  </a>
18
18
  </p>
19
19
 
@@ -34,17 +34,17 @@ The CLI detects your installed agents and asks where to put the skill. In Claude
34
34
 
35
35
  Claude.ai, DeepSeek Harness and team rollout are covered in [Install](#install).
36
36
 
37
- 📖 **Docs & write-ups (RU):** [humanizer-ru.aifrontier.tech](https://humanizer-ru.aifrontier.tech/): do AI detectors work on Russian, the 64 markers, plagiarism vs AI detection.
37
+ 📖 **Docs & write-ups (RU):** [humanizer-ru.aifrontier.tech](https://humanizer-ru.aifrontier.tech/): do AI detectors work on Russian, the 67 markers, plagiarism vs AI detection.
38
38
 
39
39
  ## What you get
40
40
 
41
- 64 patterns across 14 categories: канцелярит, English calques, emotional sterility, persuasion tricks, information rhythm, hedging specifics, plus the 2025-2026 stylistic fingerprints (jagged-meditation single-word sentences, pseudo-Socratic Q-A chains, decorative emoji per list item, pseudo-therapeutic register) and the 2026 formulas (inanimate subject, Title Case headings, mid-sentence truncation, negation triad), plus a discourse layer (explicit final moral, portrait-style character introduction, emotion conveyed only through the body, seamless causal chains, strictly linear chronology, no direct reader address: narrative signals that survive stylistic rewriting, per StoryScope / COLM 2026). 21 hard-banned constructions that scream "GPT wrote this", including em-dashes (detectors count their frequency). A research-backed section on how detectors actually work (perplexity, burstiness, morphology) with verified numbers from DivEye, PIFE, AINL-Eval 2025 (every citation checked, see SOURCES.md). Five article formulas. Voice calibration. Local editing instead of rewriting: mark the exact spot, strip the wrapper around the claim, verify the result against the source (who did what, certainty, conditions, numbers with units). Nothing is added for "liveliness": no invented facts, emotions or particles; a 195-sample blind run (eval/LOCAL_EDIT_CHECK.md) showed that voice quotas produce foreign tone and staged rhetoric.
41
+ 67 patterns across 16 categories: канцелярит, English calques, emotional sterility, persuasion tricks, information rhythm, hedging specifics, plus the 2025-2026 stylistic fingerprints (jagged-meditation single-word sentences, pseudo-Socratic Q-A chains, decorative emoji per list item, pseudo-therapeutic register) and the 2026 formulas (inanimate subject, Title Case headings, mid-sentence truncation, negation triad), plus a discourse layer (explicit final moral, portrait-style character introduction, emotion conveyed only through the body, seamless causal chains, strictly linear chronology, no direct reader address: narrative signals that survive stylistic rewriting, per StoryScope / COLM 2026). 21 hard-banned constructions that scream "GPT wrote this", including em-dashes (detectors count their frequency). A research-backed section on how detectors actually work (perplexity, burstiness, morphology) with verified numbers from DivEye, PIFE, AINL-Eval 2025 (every citation checked, see SOURCES.md). Five article formulas. Voice calibration. Local editing instead of rewriting: mark the exact spot, strip the wrapper around the claim, verify the result against the source (who did what, certainty, conditions, numbers with units). Nothing is added for "liveliness": no invented facts, emotions or particles; a 195-sample blind run (eval/LOCAL_EDIT_CHECK.md) showed that voice quotas produce foreign tone and staged rhetoric.
42
42
 
43
43
  ## Try it without installing
44
44
 
45
45
  The [site's front page](https://humanizer-ru.aifrontier.tech/#audit) runs the same scanner in JavaScript: paste a text, get a 0-100 cleanliness score, highlighted markers and the install command for your agent. Everything runs in the browser, nothing is uploaded. Rules are exported from `markers.py` by `scripts/export_web_rules.py`; `scripts/test_web_parity.py` keeps the web and Python engines in step in CI.
46
46
 
47
- The same scanner ships as a Chrome extension: select text on any page, pick "Проверить на следы нейросети" in the context menu, and the popup shows the score and highlights. Two permissions only, context menu and local storage; no host access, no network. Source in `extension/`, built by `scripts/build_extension.py`; until the Web Store listing is live, load it via `chrome://extensions` → Developer mode → Load unpacked.
47
+ The same scanner ships as a [Chrome extension](https://chromewebstore.google.com/detail/iaelpnagdohdahkcahippeadohpgobad): select text on any page, pick "Проверить на следы нейросети" in the context menu, and the popup shows the score and highlights. Two permissions only, context menu and local storage; no host access, no network. Source in `extension/`, built by `scripts/build_extension.py`.
48
48
 
49
49
  ## Install
50
50
 
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # humanizer-ru: очеловечить русский AI-текст для Claude Code, Cursor, Codex и других AI-агентов
2
2
 
3
- > Скилл для Claude. Убирает 64 признака нейросети в русском тексте: канцелярит, кальки, фингерпринты ChatGPT и Claude. Метит в то, что измеряют GPTZero, DivEye, RuBERT: поднимает perplexity и burstiness. 21 жёстких банов, quad-pass аудит, калибровка под голос автора, eval-харнес с метриками до/после. Живое демо сканера на сайте.
3
+ > Скилл для Claude. Убирает 67 признаков нейросети в русском тексте: канцелярит, кальки, фингерпринты ChatGPT и Claude. Метит в то, что измеряют GPTZero, DivEye, RuBERT: поднимает perplexity и burstiness. 21 жёстких банов, quad-pass аудит, калибровка под голос автора, eval-харнес с метриками до/после. Живое демо сканера на сайте.
4
4
 
5
5
  > [English version](README.en.md) · [中文](README.zh.md)
6
6
 
7
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
- [![Версия](https://img.shields.io/badge/версия-3.28.0-blueviolet)](https://github.com/ilyautov/humanizer-ru/blob/main/CHANGELOG.md)
8
+ [![Версия](https://img.shields.io/badge/версия-3.30.0-blueviolet)](https://github.com/ilyautov/humanizer-ru/blob/main/CHANGELOG.md)
9
9
  [![Звёзды](https://img.shields.io/github/stars/ilyautov/humanizer-ru?style=social)](https://github.com/ilyautov/humanizer-ru/stargazers)
10
10
  [![skills.sh](https://skills.sh/b/ilyautov/humanizer-ru)](https://skills.sh/ilyautov/humanizer-ru/humanizer-ru)
11
11
  [![npm](https://img.shields.io/npm/v/humanizer-ru?label=npm%20%C2%B7%20dsh)](https://www.npmjs.com/package/humanizer-ru)
@@ -13,7 +13,7 @@
13
13
 
14
14
  <p align="center">
15
15
  <a href="https://humanizer-ru.aifrontier.tech/">
16
- <img src="assets/social-preview.png" alt="humanizer-ru: убирает следы нейросети из русского текста. 64 признака, 21 запрет, сканер в комплекте" width="720">
16
+ <img src="assets/social-preview.png" alt="humanizer-ru: убирает следы нейросети из русского текста. 67 признаков, 21 запрет, сканер в комплекте" width="720">
17
17
  </a>
18
18
  </p>
19
19
 
@@ -39,7 +39,7 @@ CLI найдёт установленных агентов и спросит, к
39
39
 
40
40
  Claude.ai, DeepSeek Harness и раскатка на команду в [разделе «Установка»](#установка).
41
41
 
42
- 📖 **Документация и разборы:** [humanizer-ru.aifrontier.tech](https://humanizer-ru.aifrontier.tech/): [работают ли AI-детекторы на русском](https://humanizer-ru.aifrontier.tech/ai-detektory-na-russkom.html), [64 признака AI-текста](https://humanizer-ru.aifrontier.tech/52-priznaka-ai-teksta.html), [антиплагиат и нейросеть](https://humanizer-ru.aifrontier.tech/antiplagiat-i-neyroset.html).
42
+ 📖 **Документация и разборы:** [humanizer-ru.aifrontier.tech](https://humanizer-ru.aifrontier.tech/): [работают ли AI-детекторы на русском](https://humanizer-ru.aifrontier.tech/ai-detektory-na-russkom.html), [67 признаков AI-текста](https://humanizer-ru.aifrontier.tech/52-priznaka-ai-teksta.html), [антиплагиат и нейросеть](https://humanizer-ru.aifrontier.tech/antiplagiat-i-neyroset.html).
43
43
 
44
44
  🧰 **Остальные инструменты:** MCP-серверы к кабинетам Wildberries, Ozon, Яндекс Маркета и Авито, пять серверов к hh.ru, VK, Диадоку, СБИС и Честному знаку, 34 скилла для малого бизнеса, совет мыслителей с дословной проверкой цитат. Все одним списком: [ilyautov.github.io](https://ilyautov.github.io/).
45
45
 
@@ -51,7 +51,7 @@ Claude.ai, DeepSeek Harness и раскатка на команду в [разд
51
51
 
52
52
  ## Что внутри
53
53
 
54
- **64 паттерна AI-генерации в 14 категориях:**
54
+ **67 паттернов AI-генерации в 16 категориях:**
55
55
 
56
56
  - A. Контентные: пустые открытия, размытые авторитеты, формульные выводы
57
57
  - B. Языковые: канцелярит, кальки, «является», нагромождение причастий
@@ -86,7 +86,7 @@ Claude.ai, DeepSeek Harness и раскатка на команду в [разд
86
86
 
87
87
  На [главной странице сайта](https://humanizer-ru.aifrontier.tech/#audit) работает тот же сканер на JavaScript: вставьте текст, получите оценку чистоты от 0 до 100, подсветку найденных оборотов и команду установки для своей среды. Считается в браузере, текст никуда не отправляется. Правила экспортируются из `markers.py` скриптом `scripts/export_web_rules.py`, а паритет с Python проверяет `scripts/test_web_parity.py` в CI.
88
88
 
89
- Тот же сканер есть как расширение Chrome: выделили текст на любой странице, в контекстном меню «Проверить на следы нейросети», и попап показывает балл и подсветку. Разрешений у него два, контекстное меню и локальное хранилище, доступа к сайтам и сети нет. Папка `extension/`, сборка `scripts/build_extension.py`; пока идёт модерация в Web Store, ставится через `chrome://extensions` → «Режим разработчика» → «Загрузить распакованное расширение».
89
+ Тот же сканер есть как [расширение Chrome](https://chromewebstore.google.com/detail/iaelpnagdohdahkcahippeadohpgobad): выделили текст на любой странице, в контекстном меню «Проверить на следы нейросети», и попап показывает балл и подсветку. Разрешений у него два, контекстное меню и локальное хранилище, доступа к сайтам и сети нет. Папка `extension/`, сборка `scripts/build_extension.py`.
90
90
 
91
91
  ## Установка
92
92
 
@@ -349,7 +349,7 @@ GPTZero, Originality.ai, ZeroGPT и другие популярные детек
349
349
 
350
350
  **Как обойти GPTZero на русском?** GPTZero измеряет perplexity и burstiness. Скилл поднимает оба показателя: контрастное вычитание (замена самого предсказуемого слова в каждом предложении) повышает perplexity, рваный ритм с короткими и длинными предложениями повышает burstiness. Адверсарный парафраз снижает true positive rate детекторов на 87.88% по NeurIPS 2025.
351
351
 
352
- **Как убрать признаки ChatGPT из текста?** Главные сигналы: «не просто X, а Y» (80%+ AI-текстов), «стоит отметить», «является», длинное тире, отсутствие частиц «же/ведь/вот», равномерная информационная плотность. Скилл ловит и заменяет все 64 паттерна.
352
+ **Как убрать признаки ChatGPT из текста?** Главные сигналы: «не просто X, а Y» (80%+ AI-текстов), «стоит отметить», «является», длинное тире, отсутствие частиц «же/ведь/вот», равномерная информационная плотность. Скилл ловит и заменяет все 67 паттернов.
353
353
 
354
354
  **Как сделать текст ChatGPT человеческим на русском?** Три шага: снять 21 жёстких банов и маркеры по приоритету, срезать оболочку вокруг утверждений вместо переписывания, сверить результат с исходником по фактам и степени уверенности. Живость не дописывается: то, чего нет в исходнике, в текст не идёт.
355
355
 
@@ -372,7 +372,7 @@ GPTZero, Originality.ai, ZeroGPT и другие популярные детек
372
372
  | | humanizer-ru | blader/humanizer |
373
373
  |---|---|---|
374
374
  | Язык | Русский | Английский |
375
- | Паттернов | 64 | 32 |
375
+ | Паттернов | 67 | 32 |
376
376
  | Жёстких банов | 20 | - |
377
377
  | Аудит | Quad-pass | Single-pass |
378
378
  | Voice calibration | Да | Да |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "humanizer-ru",
3
- "version": "3.28.0",
4
- "description": "Agent skill for DeepSeek Harness: rewrites Russian text to remove 64 markers of AI generation (bureaucratese, calques, ChatGPT fingerprints), with a corpus-calibrated scanner, audit mode and author-voice calibration.",
3
+ "version": "3.30.0",
4
+ "description": "Agent skill for DeepSeek Harness: rewrites Russian text to remove 67 markers of AI generation (bureaucratese, calques, ChatGPT fingerprints), with a corpus-calibrated scanner, audit mode and author-voice calibration.",
5
5
  "license": "MIT",
6
6
  "author": "Ilya Utov",
7
7
  "homepage": "https://humanizer-ru.aifrontier.tech/",
@@ -4,7 +4,7 @@ description: "Качество русского текста для агента
4
4
  license: MIT
5
5
  ---
6
6
 
7
- # Humanizer-RU v3.28.0
7
+ # Humanizer-RU v3.30.0
8
8
 
9
9
  Ты редактор. Убираешь из русского текста следы нейросети и не ломаешь при этом
10
10
  смысл, факты и голос автора. Оба обещания равноправны: текст, который стал «чище»
@@ -27,7 +27,7 @@ license: MIT
27
27
  - **Точечная правка** («убери только канцелярит», «исправь тире»): работай только
28
28
  с названной проблемой, остальное не трогай.
29
29
  - **Свой черновик** (без просьбы, молча): всё, что ты пишешь пользователю
30
- по-русски сам, перед отправкой проходит по таблице жёстких запретов. Это одна
30
+ по-русски сам, в чат или в файл, перед отправкой проходит по таблице жёстких запретов. Это одна
31
31
  вычитка своего текста, а не сеанс редактуры: сканер не запускается, каталог не
32
32
  открывается, отчёта нет, скилл не упоминается. Чужой текст молча не правится
33
33
  никогда. Чужое это сообщение пользователя, содержимое файла, цитата, выдача
@@ -46,13 +46,16 @@ license: MIT
46
46
 
47
47
  ## Шаг 1. Диагностика
48
48
 
49
- **Сканер.** Если доступны команды и python3 с `razdel` и `pymorphy3`, сначала
50
- прогони текст: `python3 <папка скилла>/scripts/scan.py файл.txt` или
49
+ **Сканер.** Если доступны команды, первый вызов инструмента в редактуре это
50
+ сканер, до каталога и до правки: `python3 <папка скилла>/scripts/scan.py файл.txt` или
51
51
  `echo "текст" | python3 <папка скилла>/scripts/scan.py -`. Для научного,
52
52
  юридического, художественного текста и новостей добавь `--genre academic|legal|fiction|news`:
53
- без жанра сканер ловит учёных чаще, чем нейросети. Запомни балл «было». Пакетов нет или сканер
54
- упал: скажи пользователю одной строкой, что сканер не запустился и балла не
55
- будет (как поставить, написано в его ошибке и в README), и работай вручную.
53
+ без жанра сканер ловит учёных чаще, чем нейросети. Запомни балл «было». Если
54
+ `scan.py` упал с «не хватает зависимостей», а `uvx` есть в PATH, запусти то же
55
+ через `uvx ru-humanizer` с теми же аргументами (`-` для stdin, `--genre`, `--before`):
56
+ зависимости он поставит сам. Упал и он или команд нет вовсе: скажи пользователю
57
+ одной строкой, что сканер не запустился и балла не будет (как поставить, написано
58
+ в его ошибке и в README), и работай вручную.
56
59
  Молча пропускать сканер нельзя: без балла правка не измерима, а пользователь
57
60
  месяцами не узнает, что половина скилла не работает. Балл на глаз не придумывай.
58
61
 
@@ -76,9 +79,9 @@ JOIN (убрать дублирование соседних фраз), SPLIT (
76
79
 
77
80
  | Группа | Что это | Когда править |
78
81
  |---|---|---|
79
- | A | Жёсткие запреты (таблица ниже), пустые открытия, канцелярит (отглагольные существительные «осуществление», «внедрение», «реализация», «обеспечение», «взаимодействие», «оптимизация», «функционирование» и «в рамках», «в целях» → глагол или предлог), артефакты чатбота («Давайте разберёмся», «Надеюсь, помог»), негативные параллелизмы, модальная неопределённость («может показаться», «в некотором смысле»), псевдо-терапия («ты имеешь право так чувствовать») | Всегда, в любом регистре кроме цитат |
80
- | B | Размытые авторитеты («эксперты считают»), кальки и пунктуационные кальки, «является» через предложение, водянистость, «определённый»/«соответствующий», ровные абзацы одной длины и гладкие переходы, мораль в финале, портретный ввод героя, эмодзи-декор, translationese | Везде, кроме юридических текстов |
81
- | C | Раздувание значимости, формульные выводы, правило трёх (искусственные триады синонимов), карусель синонимов, частые тире, оговорки, отсутствие идиом, контр-вопросы («Зачем? Потому что.»), рваная медитативность («Точно. Отдельно.») | Маркетинг и экспертный текст |
82
+ | A | Жёсткие запреты (таблица ниже), пустые открытия, канцелярит (отглагольные существительные «осуществление», «внедрение», «реализация», «обеспечение», «взаимодействие», «оптимизация», «функционирование» и «в рамках», «в целях» → глагол или предлог), артефакты чатбота («Давайте разберёмся», «Надеюсь, помог»), обвязка чата (вступление «Вот вариант:», предложение доработки, советы после самого текста снимаются; у «Вариант 1/2» снимается метка, а все варианты остаются, если пользователь не просил выбрать; плейсхолдер «[Имя]» не заполняй, перечисли пользователю), негативные параллелизмы, оговорки мнения («может показаться», «в некотором смысле»), псевдо-терапия («ты имеешь право так чувствовать») | Всегда, в любом регистре кроме цитат |
83
+ | B | Размытые авторитеты («эксперты считают»), кальки и пунктуационные кальки, «является» через предложение, водянистость, «определённый»/«соответствующий», ровные абзацы одной длины и гладкие переходы, мораль в финале, портретный ввод героя, эмодзи-декор, translationese; в постах и письмах подзаголовки «##» и строки целиком жирным становятся обычными строками | Везде, кроме юридических текстов |
84
+ | C | Раздувание значимости, формульные выводы, правило трёх (искусственные триады синонимов), карусель синонимов, частые тире, отсутствие идиом, контр-вопросы («Зачем? Потому что.»), рваная медитативность («Точно. Отдельно.»), почерк модели (определение через тире, «стал настоящим открытием», «вдохновляет», «гармония», «напоминает о», «подчёркивает») | Маркетинг и экспертный текст |
82
85
  | D | Болд, кавычки, списки, типографика | По контексту |
83
86
 
84
87
  Полные описания и примеры «было и стало» по номерам паттернов в каталоге.
@@ -87,7 +90,9 @@ JOIN (убрать дублирование соседних фраз), SPLIT (
87
90
  Осмысленную триаду («пришёл, увидел, победил»), намеренный повтор (анафора),
88
91
  единственное авторское тире, терминологическое «является», формальный регистр
89
92
  делового письма, структуру длинного лонгрида, содержательное противопоставление
90
- («не просто X, а Y», где X и Y правда разные услуги) оставляй. В научном тексте
93
+ («не просто X, а Y», где X и Y правда разные услуги) оставляй. Риторические вопросы
94
+ и общее «не X, а Y» у людей встречаются не реже, чем у моделей: это не маркеры
95
+ (каталог, «Не маркеры»). В научном тексте
91
96
  «является», кальки-связки, канцелярит, «данный», «таким образом» и тире это норма
92
97
  регистра, измерено на 35 158 аннотациях AINL-Eval: без жанровой поправки скилл
93
98
  ловил людей чаще, чем машины. Хорошо отредактированный человеческий текст тоже
@@ -112,7 +117,7 @@ JOIN (убрать дублирование соседних фраз), SPLIT (
112
117
  | «Играет важную/ключевую роль» | Оставь только то, ПОЧЕМУ важно, если это есть в тексте |
113
118
  | «Можно с уверенностью сказать» | Скажи без преамбулы |
114
119
  | «Подводя итог», «Таким образом,» в начале вывода | Убери или начни вывод с действия. «Устроен таким образом, что» не маркер |
115
- | Длинное тире «—» и короткое «–» вне числовых диапазонов | Запятая, двоеточие, точка, дефис или перестройка. Убирается всегда; оставить тире можно только по явной просьбе пользователя |
120
+ | Длинное тире «—», короткое «–» вне числовых диапазонов и дефис с пробелами в роли тире | Запятая, двоеточие, точка или перестройка. Тире между подлежащим и сказуемым («Антитела [тире] это белки») перестраивай глаголом: «Антитела относятся к белкам», «называются», «служат». Голое «Антитела это белки» читается как пунктуационная ошибка, запятая делает сказуемое приложением. Убирается всегда; оставить тире можно только по явной просьбе пользователя |
116
121
  | «От X до Y» для несвязанных понятий | Перечисли конкретно или убери |
117
122
  | «Погрузимся в...», «Давайте посмотрим поближе», «И вот здесь начинается самое интересное» | Удали анонс, сразу пиши суть |
118
123
  | «Раскрыть потенциал», «Вывести на новый уровень», «Открывает новые горизонты/перспективы/возможности» | Конкретный результат, если он назван в тексте; иначе удали |
@@ -143,6 +148,10 @@ JOIN (убрать дублирование соседних фраз), SPLIT (
143
148
 
144
149
  > **Факт-замок (приоритет над любым паттерном).** Числа, даты, имена, названия,
145
150
  > проценты, единицы, условия и оговорки исходника переносятся без искажений.
151
+ > Оговорка при факте («может быть», «часто», «обычно», «примерно», «от 5000»,
152
+ > «не обязательно», «ожидаем») задаёт его точность и не снимается; снимаются
153
+ > только оговорки мнения из группы A. Сканер с `--before исходник.txt` печатает
154
+ > снятую оговорку строкой «потеряно: оговорка:…»; стояла она при факте, верни её.
146
155
  > Добавлять факты, которых в исходнике нет (цифры, исследования, кейсы, «у себя
147
156
  > в команде вижу»), запрещено. В исходнике нет конкретики, а оборот пустой?
148
157
  > Удали оборот или спроси пользователя, чем наполнить. Выдуманная цифра хуже
@@ -162,6 +171,9 @@ JOIN (убрать дублирование соседних фраз), SPLIT (
162
171
  - причина и последовательность: соседство фактов остаётся соседством, а не выводом;
163
172
  - числа вместе с единицами, выборкой, условиями и предметом сравнения;
164
173
  - авторское суждение, личный опыт и объяснение механизма ровно в объёме источника;
174
+ - швы после удалений (каталог #65): «этот», «саму X», «вторая», «как сказано выше»
175
+ указывают на то, что осталось в тексте; склейка не повторяет саму себя; одна
176
+ и та же оговорка не закрывает три абзаца подряд;
165
177
  - практическая функция: действие читателя, предложение, ограничение, срок, контакт;
166
178
  - рамка: когда в тексте названы клиент, партнёр или человек, сравни, как они
167
179
  выглядят до и после. Снятая пустая подводка порой была единственным смягчением,
@@ -182,17 +194,22 @@ SKILL.md, без твоих рассуждений и без списка изм
182
194
  случайный читатель в ленте, и по тому же брифу: не осталось ли конструкций из
183
195
  таблицы запретов, не повторяются ли в твоих вставках одинаковые концовки и
184
196
  подводки, не выровнялись ли абзацы под одну длину. Отдельно найди в чистовике
185
- символы «—» и «–»: их не должно остаться нигде, кроме числовых диапазонов, и это
186
- самая частая недоделка. Если сканер был доступен, прогони результат ещё раз.
197
+ «—», «–» и « - »: их не должно остаться нигде, кроме числовых диапазонов, и это
198
+ самая частая недоделка. Без сканера ищи там же «не просто», «не только», «это не»
199
+ и английские слова внутри русских фраз. Если сканер был доступен, прогони
200
+ результат ещё раз.
187
201
  Правка закончена, когда отмеченные недостатки сняты, а новые изменения не дают
188
202
  читателю понятной пользы. Замечания остались после второго круга? Верни текст с
189
203
  их списком, третий круг сглаживает содержание ради балла.
190
204
 
191
205
  ## Отчёт
192
206
 
193
- По умолчанию верни итоговый текст. Если сканер запускался, добавь строку
194
- «Чистота: было N, стало M» и одну-две фразы, что именно снято. Объяснение всех
195
- изменений давай по запросу. Балл это правила сканера, а не вероятность ИИ и не
207
+ По умолчанию верни итоговый текст. Если сканер запускался, добавь после текста
208
+ строку «Чистота: было N, стало M» и одну-две фразы, что именно снято; просили
209
+ только текст, строки нет. Финальное сообщение начинается с первой строки текста:
210
+ перед ним нет балла, вердикта сканера, «вот итоговый текст» и заметок о файлах
211
+ и правах, а отчёт после текста пишется на языке пользователя. Объяснение всех изменений
212
+ давай по запросу. Балл это правила сканера, а не вероятность ИИ и не
196
213
  вердикт об авторстве; скилл не обходит детекторы и антиплагиат, не вставляет
197
214
  опечатки и не подменяет буквы. Если текст уже хорош, скажи об этом и не правь
198
215
  ради правки.
@@ -1,5 +1,5 @@
1
1
  interface:
2
2
  display_name: "Humanizer RU"
3
- short_description: "Убирает следы нейросети из русского текста: 64 паттерна, 21 жёстких банов, голос автора."
3
+ short_description: "Убирает следы нейросети из русского текста: 67 паттернов, 21 жёстких банов, голос автора."
4
4
  policy:
5
5
  allow_implicit_invocation: true
@@ -4,6 +4,22 @@
4
4
  действия. Сам текст сохраняется. Если проблем не найдено, сообщи это без выдуманных
5
5
  находок. Аудит не определяет авторство текста и не обещает обход детекторов.
6
6
 
7
+ ## Авторство не устанавливается
8
+
9
+ Вопрос «писал ли это ИИ», «вникал ли автор» аудит не решает, даже если пользователь
10
+ спросил именно так. Скажи одной строкой, что скилл измеряет текст, а не авторство,
11
+ и дай находки по тексту. Не бывает находками: метаданные файла (программа,
12
+ даты создания и правки), способ вёрстки («HTML, напечатанный из Chrome»),
13
+ рассуждения «человек так не работает» и вердикт о «модели производства». Правило держится
14
+ и в следующих ходах: после отказа «докажи, что это нейрослоп» просьба «ну а
15
+ по маркерам?» не повод вынести тот же вердикт мягче («архитектура нейросетевая,
16
+ фактура человеческая»). Отказывай без нотаций: не объясняй пользователю, что
17
+ его просьба сама похожа на слоп, и не начинай с «результат тебе не понравится».
18
+ Шаблон
19
+ вёрстки глав это дизайн-система, а не почерк модели. Не маркеры и сами по себе,
20
+ по замеру на корпусах (`eval/MODERN-SLOP.md`, «Не маркеры» в каталоге):
21
+ риторические вопросы, общее «не X, а Y», длинное тире в свёрстанном документе.
22
+
7
23
  ## Машинная проверка, если доступна
8
24
 
9
25
  Сканер находится внутри скилла: `scripts/scan.py`. Для существующего файла:
@@ -19,7 +35,10 @@ python3 <папка-скилла>/scripts/scan.py <файл> --json
19
35
 
20
36
  Код завершения 0 или 1 с валидным отчётом означает выполненный анализ: 1 может
21
37
  обозначать найденные сигналы. Ошибка запуска, зависимостей или разбора отчёта
22
- означает, что измерения нет. Не устанавливай зависимости автоматически.
38
+ означает, что измерения нет. Не ставь пакеты в систему (`pip install`) без
39
+ спроса. Упал на нехватке зависимостей, а `uvx` есть в PATH: запусти то же через
40
+ `uvx ru-humanizer` с теми же аргументами. Он берёт пакет во временный кэш uv и
41
+ систему не трогает, спрашивать для этого не нужно.
23
42
 
24
43
  Если сканер не запускался или недоступен, прямо отметь это и продолжи вручную.
25
44
  Не оценивай балл на глаз, даже с пометкой «примерно». Если отчёт получен,
@@ -38,6 +57,21 @@ python3 <папка-скилла>/scripts/scan.py <файл> --json
38
57
  локальное действие. Если все фрагменты KEEP, сообщи, что полезных правок нет.
39
58
  Вопросы о невидимом контексте добавляй только когда от ответа зависит конкретная
40
59
  предлагаемая правка; так и обозначай их, отдельно от найденных ошибок.
60
+
61
+ Разбор длинного документа или рецензия для третьего лица дополняются двумя
62
+ короткими блоками, если в них есть что сказать: что выдержало проверку (факты,
63
+ источники, оставленные на виду расхождения) и что недостатком не является
64
+ (артефакты извлечения текста из PDF вроде «со труд ники», декларированные
65
+ AI-иллюстрации, норма жанра). Симметрия защищает автора от ложного обвинения.
66
+ Число без выборки или условия (каталог #66) не правь: задай автору вопрос «при
67
+ каком условии получена эта цифра?», первоисточника у тебя нет.
68
+
69
+ Сам разбор это твой русский текст, а не цитата: таблица жёстких запретов из
70
+ SKILL.md к нему применяется целиком, а с ней и #54: без «вот честный аудит»,
71
+ «чтобы разбор был честным», «для симметрии». То же для файлов, которые ты
72
+ пишешь по-русски: рецензия в PDF или .md это тоже твой текст. Длинного тире в отчёте нет («первая
73
+ половина - автор» пишется «первая половина от автора» или через двоеточие),
74
+ цитаты из исходника при этом приводятся как есть.
41
75
  Объём ответа определяется числом полезных действий, а не обязательными разделами.
42
76
 
43
77
  Если пользователь просит разбор по каталогу, прочитай нужный раздел
@@ -48,7 +48,7 @@ davinci-003). Метод и полные таблицы: `eval/AINL-CALIBRATION.
48
48
  Общая граница у всех замеров одна: генераторы 2023-2024 годов. Ни одной модели
49
49
  2025-2026 годов по-русски не измерено.
50
50
 
51
- ## Каталог: 64 паттерна AI-генерации в русском
51
+ ## Каталог: 67 паттернов AI-генерации в русском
52
52
 
53
53
  ### A. Контентные (1-5)
54
54
 
@@ -91,7 +91,7 @@ davinci-003). Метод и полные таблицы: `eval/AINL-CALIBRATION.
91
91
 
92
92
  **12. Правило трёх.** AI обожает перечисления из трёх: «важный, значительный и ключевой». Если три синонима, оставь один. Если перечисление искусственное, перестрой фразу. Иногда хватит двух элементов. Иногда нужно четыре.
93
93
 
94
- **13. Синонимическая карусель.** AI чередует «компания», «организация», «предприятие», «фирма» для одной сущности. Выбери одно слово. Повтор в русском нормален. Неестественное чередование хуже повтора.
94
+ **13. Синонимическая карусель.** AI чередует «компания», «организация», «предприятие», «фирма» для одной сущности. Выбери одно слово. Повтор в русском нормален. Неестественное чередование хуже повтора. Сканер видит обратную сторону статистически: если на тексте от 100 слов слова почти не повторяются (MATTR выше 0.955 при норме людей 0.90), он даёт штраф «лексическое разнообразие». Лечится тем же: верни повтор или местоимение вместо подобранного синонима, не выискивай новых слов.
95
95
 
96
96
  **14. Перекос в существительные.** В AI-тексте соотношение существительных к глаголам примерно 3:1, у людей ближе к 2:1. LLM предпочитает noun phrases, люди заякоривают язык в глаголах с временем и наклонением. Instruction tuning усиливает перекос: ChatGPT известен «номинальным» стилем, в AI-тексте больше номинализаций. Если в абзаце мало глаголов, ищи номинализации и разворачивай. Детекторы ловят это даже на чистом синтаксисе без лексики: паттерны синтаксических связей различают human/AI.
97
97
 
@@ -178,7 +178,7 @@ davinci-003). Метод и полные таблицы: `eval/AINL-CALIBRATION.
178
178
 
179
179
  ### G. Структурные (34-35)
180
180
 
181
- **34. Разорванные абзацы.** Абзацы AI слабо связаны. Можно переставить местами, и ничего не изменится. Нет обратных ссылок, нет причинно-следственных связок. AI предпочитает дискурсивные связи Elaboration и Explanation, люди чаще используют Contrast и Concession (2510.26124). Практически: добавлять «но», «с другой стороны», «впрочем»: это меняет дискурсивную структуру, а не только стилистику. Свяжи: каждый следующий абзац должен вытекать из предыдущего. Для длинных текстов (>500 слов) обязательны обратные ссылки: «Как уже говорил выше...», «Повторюсь, но...», «К этому вернёмся через минуту», «Помните, в начале я упоминал...», «Вот тут-то и пригодится то, что...». AI теряет coherence в длинных нарративах: «narrative drift» документированная проблема. Человек помнит начало истории, AI забывает.
181
+ **34. Разорванные абзацы.** Абзацы AI слабо связаны. Можно переставить местами, и ничего не изменится. Нет обратных ссылок, нет причинно-следственных связок. AI предпочитает дискурсивные связи Elaboration и Explanation, люди чаще используют Contrast и Concession (2510.26124). Практически: добавлять «но», «с другой стороны», «впрочем»: это меняет дискурсивную структуру, а не только стилистику. Свяжи: каждый следующий абзац должен вытекать из предыдущего. Для длинных текстов (>500 слов) обязательны обратные ссылки: «Как уже говорил выше...», «Повторюсь, но...», «К этому вернёмся через минуту», «Помните, в начале я упоминал...», «Вот тут-то и пригодится то, что...». AI теряет coherence в длинных нарративах: «narrative drift» документированная проблема. Человек помнит начало истории, AI забывает. В экспертном лонгриде тот же дрейф виден по голосу: первая половина от автора, с репликами и случаями из практики, вторая превращается в регламент («Назначьте ответственного», «Отдельно проверяйте») без единой авторской детали. Проверка: найди последнюю реплику от первого лица; если после неё больше трети текста, это сигнал. Реплики за автора не дописывай: отметь, где голос пропал, и спроси, есть ли у него там наблюдение. Осознанный переход в практическую часть тоже бывает, решает автор.
182
182
 
183
183
  **35. Галлюцинации.** AI уверенно утверждает ложное: несуществующие цитаты, неправильные даты, выдуманные факты. Проверь каждый конкретный факт. Не можешь подтвердить? Убери или пометь как неподтверждённое.
184
184
 
@@ -188,6 +188,8 @@ davinci-003). Метод и полные таблицы: `eval/AINL-CALIBRATION.
188
188
 
189
189
  **37. Вылизанная типографика.** AI ставит все тире, кавычки, пробелы идеально по ГОСТу. Человек в мессенджере пишет дефис вместо тире, не ставит пробел перед скобкой, забывает точку в конце. Для неформальных текстов не исправляй типографику до идеала. Пусть местами будет дефис вместо тире, пусть скобка прижмётся к слову. Это не ошибка, это почерк.
190
190
 
191
+ Обратный признак, который тоже проверяется глазами: расхождения источников, оставленные на виду («расхождение не устранено догадкой»), и разные даты сверки у разных источников. Слоп такие швы сглаживает, живая многопроходная сверка их показывает. Не правь их и не выдавай за небрежность.
192
+
191
193
  ### I. Паттерны убеждения (38-42)
192
194
 
193
195
  **38. Негативные параллелизмы.** «Не просто инструмент, а партнёр». «Не только ускоряет, но и трансформирует». AI обожает эту конструкцию, она есть в 80%+ текстов GPT. HARD BAN. Скажи прямо что имеешь в виду: «Это партнёр» или «Ускоряет и трансформирует». Без «не просто / не только».
@@ -221,6 +223,8 @@ davinci-003). Метод и полные таблицы: `eval/AINL-CALIBRATION.
221
223
  До: «1. X: один делает, второй проверяет. Подходит для... 2. Y: один раздаёт, остальные делают. Подходит для... 3. Z: передаёт по цепочке. Подходит для...»
222
224
  После: «1. X: один пишет, второй ловит косяки. Берёте туда, где цена ошибки высокая. 2. Y: тут проще, конвейер. 3. Z: а вот это для хаоса. Задачи сыпятся, кто свободен, тот и хватает.»
223
225
 
226
+ Тот же скелет бывает у абзацев прозы без всякого списка: тезис, число со ссылкой, оговорка, вывод, и так три-четыре абзаца подряд, часто с однотипным зачином («Модель подыгрывает себе.» / «Контролёр уступает давлению.» / «Спор ради спора тоже не помогает.»). Проверка та же: прочитай подряд первые предложения абзацев. Сломай порядок в одном-двух, переставив их же части: начни с числа, а тезис дай после него. Новых фраз для этого не нужно.
227
+
224
228
  ### K. Хеджирование и специфика (46-48)
225
229
 
226
230
  **46. Модальная неопределённость.** AI хеджирует через «может» в каждом предложении: «может стать», «может повлиять», «способен обеспечить», «призван решить». Это не осторожность автора, это привычка модели снижать категоричность. Если утверждение верно, утверждай. Если неверно, не пиши. «Может быть полезным» не несёт информации. «Ускорило вдвое» несёт. Порог: больше 1 «может/способен/призван» на 100 слов вне контекста прогнозов = маркер. Не путать с паттерном 25 (избыточные оговорки типа «в определённом смысле») и паттерном 41 (отказы от ответственности типа «хотя информация может быть неполной»). Модальное «может»: систематическое хеджирование КАЖДОГО утверждения через одну конструкцию.
@@ -291,6 +295,35 @@ davinci-003). Метод и полные таблицы: `eval/AINL-CALIBRATION.
291
295
 
292
296
  **64. Нет обращения к читателю.** Человек говорит с читателем напрямую чаще ИИ: порядковое среднее 0.28 против 0.07 (собственная проза статьи передаёт это как «28% против 7%»). В абсолютных числах это один из НАИМЕНЬШИХ разрывов таблицы, так что признак вспомогательный: сам по себе он ничего не доказывает. Вопрос в лоб, «представьте», «вы наверняка такое видели». Сигнал здесь - отсутствие, поэтому сканер его не считает: смотри глазами. Применяй только там, где формат обращение допускает: пост, рассылка, колонка, лендинг. В новость, документацию и научный текст не тащи. Это предложение автору, не автоправка: обращение меняет интонацию всего текста, вставлять его молча нельзя. Предложи одно-два места, решает автор.
293
297
 
298
+ ### O. Следы итеративной правки (65)
299
+
300
+ Эти следы оставляет не генерация, а цикл правок: агент переписывает документ по замечаниям рецензентов, людей или моделей, и каждое замечание закрывает отдельно. В разовых генерациях наших корпусов их почти нет (оговорка «Это X, а не Y» в конце абзаца: 0% у Claude и GPT-5.6, до 2% у остальных моделей, 0-0,2% у людей), а в исследовании на 5 800 слов после нескольких раундов рецензий их было девять. Скилл сам работает правкой и сам удаляет, поэтому паттерн проверяется и на собственном чистовике (шаг 3 SKILL.md).
301
+
302
+ **65. Швы правки.** Четыре вида, обычно вместе:
303
+ - **оговорка-заплатка одной формулой:** «Это учебный пример, не исследование», «Это экстраполяция, а не аудит», «Это моя сборка, а не проверенная методика». По отдельности каждая верна, три и больше на текст читаются как оправдание вместо довода. Уточнение по существу (выборка, условия) оставь, а в половине случаев разбери обёртку и впиши условие прямо в утверждение;
304
+ - **обрыв ссылки:** слово-антецедент удалено, а отсылка к нему осталась («Саму усталость эти проценты не измеряют», хотя «усталость» вычищена абзацем выше; «Вторая возможность» без первой). Замени отсылку словом, которое реально стоит рядом, или верни антецедент;
305
+ - **самоповтор после склейки:** «затраты на подготовку, проверку и переделки с учётом проверок и переделок». Сократи хвост;
306
+ - **одна мысль в каждой главе заново:** фраза, закрывшая замечание в одном месте, дословно повторена в трёх-четырёх разделах и на плашках. Оставь её там, где она доказана, в остальных местах сошлись на главу или убери. Сквозной мотив (название поста на обложке и в финале) это решение автора, спроси.
307
+
308
+ Отличие от #41: там размытая оговорка вместо знания, здесь точная оговорка, размноженная одной формулой. От #26: водянистость размазывает мысль в одном месте, здесь мысль повторена в разных. Механически повтор фразы между абзацами ловится сканером частично; обрыв ссылки только чтением.
309
+
310
+ ### P. Числа и проверяемость (66-67)
311
+
312
+ Не признаки машины, а места, где текст подводит читателя. Нашлись при независимой рецензии документа с 45 источниками; к стилю отношения не имеют, поэтому правятся всегда, где их видно, и не штрафуются сканером.
313
+
314
+ **66. Точность, которую нельзя проверить.** Деталь подана как точная, а читатель проверить её не может: «пост №656» (номер не виден в веб-версии канала), «Doc. 46» (номер записи есть только в платной базе суда), «2 077 решений на 23.09» без ссылки на срез. Сюда же число из первоисточника без условия, при котором оно получено: «80,3% против 43,4%» верно только для одного типа возражений, «17% против 5%» это разбивка, а заголовочная цифра источника 13,6%. Точность убеждает сильнее, чем позволяет основание. Родня #35, но факт может быть верным: проблема в том, что проверить его нельзя. Не выдумывай ни условие, ни ссылку (факт-замок): спроси автора, откуда деталь и при каком условии получено число, или предложи формулировку, которая не обещает больше, чем есть («в этом канале», «в эксперименте с полным опровержением»).
315
+
316
+ **67. Разнобой числовых форматов.** В одном расчёте «10 000» с пробелом и «8,928» с запятой по-английски, «$186» перед числом, «1,000» как тысяча. Русский читатель видит в «$8,928 млн» восемь миллиардов. Приведи к одной норме: пробел между разрядами, запятая как десятичный разделитель, валюта словом или знаком после числа («8,9 млн долларов»), лишние знаки после запятой округли, если точность не нужна для вывода. Встречается у 0-3% текстов и людей, и моделей, это ошибка вёрстки, а не почерк.
317
+
318
+ ## Не маркеры (замерено)
319
+
320
+ Расхожие «признаки ИИ», которые на наших корпусах не отделяют модели от людей (`eval/MODERN-SLOP.md`, тексты 60-600 слов). Их нельзя предъявлять как довод, ни в аудите, ни в правке:
321
+ - **риторические вопросы:** 4 и больше на 1000 слов у 37,5% авторов Пикабу и у 9-25% текстов большинства моделей;
322
+ - **общее «не X, а Y»:** у людей 10-11% текстов, у моделей LLMTrace 8-12%. Маркер только узкий: «не просто», «не только», и число таких оборотов на текст, а не одно наличие (#38);
323
+ - **длинное тире в свёрстанном документе:** типограф вёрстки ставит его сам, для изданного PDF это норма, в посте и чате решает автор;
324
+ - **шаблон вёрстки** (одинаковые главы, карточки, плашки): это дизайн-система документа, а не скелет абзацев (#45);
325
+ - **метаданные файла и способ сборки** (PDF из браузера, одна дата сохранения): к тексту отношения не имеют.
326
+
294
327
  ---
295
328
 
296
329
  ## Быстрый сканер: слова-маркеры AI
@@ -343,6 +376,10 @@ davinci-003). Метод и полные таблицы: `eval/AINL-CALIBRATION.
343
376
 
344
377
  **Финальная мораль:** «мораль этой истории», «эта история учит нас», «эта история о том, что», а также «мораль проста»/«мораль такова» последней фразой текста: нарратор проговаривает вывод за читателя (паттерн #59). Указательное слово обязательно, иначе под маркер уходит живая проза: разбор басни («мораль истории про лису»), публицистическое «история учит нас осторожности», «наш историк учит нас». Сканер ловит только шаблонные формулы; мораль, изложенную без них, оценивай по прочтении
345
378
 
379
+ **Почерк модели:** «подчёркивает», «свидетельствует о», «демонстрирует», «это не просто», «что делает его», «важность», «заставляет задуматься», «стал символом», «стал настоящим открытием», «важный шаг», «представьте», «заслуживает», «для тех, кто», «Это позволяет», «вдохновляет», «незабываемый», «гармония», «X — это Y» (только длинное тире: дефис с пробелами печатают и люди), «оставляет впечатление», «напоминает о», «может привести к», «невероятно»: обороты свежих моделей (GPT-4o, o3, GigaChat, YandexGPT, Qwen, DeepSeek, Claude), у каждой минимум четырёх семейств и не больше чем у 1% людей («X — это Y» у 3%). По одному бывают у человека, поэтому считаются не плотностью, а числом разных оборотов: первый почти бесплатен, второй и дальше дорого. В жанрах news, academic, legal и fiction не считаются. Замер: eval/MODERN-SLOP.md
380
+
381
+ **Обвязка чата:** предложение доработки («Хотите, чтобы я…», «Если нужно, могу сделать вариант покороче»), плейсхолдеры «[Ваше имя]», «Вот вариант:» и «Вариант 1», эмодзи вместо маркеров списка (❌ 1️⃣ 👉), подзаголовки Markdown «##» и строки целиком жирным: ответ чат-бота, вставленный вместе с упаковкой. У людей не встречается, но разметку человек тоже пишет, поэтому штраф за каждый след, а не приговор, как у артефактов копипасты. Ложное срабатывание: плейсхолдеры в шаблоне, который пользователь заполнит сам, не правятся (заполнить «[Имя клиента]» значит выдумать факт); балл такого текста не дойдёт до «чисто», и это нормально
382
+
346
383
  **Title Case в заголовках:** «Ранняя Жизнь и Образование»: два и больше нарицательных слова заголовка с заглавной буквы
347
384
 
348
385
  **Обрыв на полуслове:** последнее предложение не закончено, текст упёрся в лимит токенов
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
  from dataclasses import dataclass, replace
15
15
 
16
16
  from .burstiness import RhythmStats, rhythm, rhythm_verdict
17
+ from .lexical import LexicalStats, lexical_stats
17
18
  from .markers import (
18
19
  MarkerHit,
19
20
  marker_verdict,
@@ -21,6 +22,7 @@ from .markers import (
21
22
  scan_markers,
22
23
  )
23
24
  from .morphology import MorphStats, morph_stats, morph_verdict
25
+ from .repeats import RepeatStats, repeat_stats
24
26
  from .structure import StructureStats, structure_stats, structure_verdict
25
27
  from .score import ScoreResult, cleanliness_score
26
28
 
@@ -30,12 +32,16 @@ __all__ = [
30
32
  "RhythmStats",
31
33
  "MorphStats",
32
34
  "StructureStats",
35
+ "LexicalStats",
36
+ "RepeatStats",
33
37
  "MarkerHit",
34
38
  "ScoreResult",
35
39
  "cleanliness_score",
36
40
  "rhythm",
37
41
  "morph_stats",
38
42
  "structure_stats",
43
+ "lexical_stats",
44
+ "repeat_stats",
39
45
  "scan_hard_bans",
40
46
  "scan_markers",
41
47
  "mask_foreign",
@@ -52,6 +58,8 @@ class Report:
52
58
  rhythm: RhythmStats
53
59
  morph: MorphStats
54
60
  structure: StructureStats
61
+ lexical: LexicalStats
62
+ repeats: RepeatStats
55
63
 
56
64
  @property
57
65
  def hard_ban_count(self) -> int:
@@ -70,6 +78,8 @@ class Report:
70
78
  "rhythm": self.rhythm.as_dict(),
71
79
  "morph": self.morph.as_dict(),
72
80
  "structure": self.structure.as_dict(),
81
+ "lexical": self.lexical.as_dict(),
82
+ "repeats": self.repeats.as_dict(),
73
83
  }
74
84
 
75
85
 
@@ -97,4 +107,6 @@ def analyze(text: str) -> Report:
97
107
  rhythm=stats,
98
108
  morph=morph_stats(prose),
99
109
  structure=structure_stats(prose),
110
+ lexical=lexical_stats(prose),
111
+ repeats=repeat_stats(prose),
100
112
  )
@@ -19,6 +19,9 @@
19
19
  идиоматичны («с одной стороны») и обычно пересказывают число исходника. Они
20
20
  считаются, но не валят проверку.
21
21
 
22
+ Оговорки: «обычно», «примерно», «может», «от 5000». Снятая оговорка делает
23
+ из «обычно помогает» обещание «помогает»; она попадает в потери.
24
+
22
25
  Утверждения: слова-кванторы, которые звучат как пафос, а несут факт:
23
26
  «первый в России», «единственный», «впервые», «рекордный», «до сих пор».
24
27
  Срезать «первый в России сервис» до «сервис» значит исказить исходник, а
@@ -70,6 +73,16 @@ CLAIM_WORDS = {
70
73
  "старейший", "никогда", "никто", "навсегда", "беспрецедентный",
71
74
  }
72
75
  CLAIM_PHRASE_RE = re.compile(r"\bдо сих пор\b|\bв мире\b|\bв россии\b", re.IGNORECASE)
76
+ # Оговорки при факте: задают его точность («обычно», «примерно», «от 5000»).
77
+ # Скилл снимал их в 5 ответах из 25 (eval/MODERN-SKILL-CHECK.md), и строка в
78
+ # промпте не помогла, поэтому пропавшая оговорка попадает в список потерь.
79
+ # «Может показаться» это оговорка мнения, её снимать можно.
80
+ HEDGE_RE = re.compile(
81
+ r"\b(?:может\s+быть|мо(?:жет|гут)(?!\s+показаться)|обычно|как\s+правило|часто|иногда|"
82
+ r"примерно|приблизительно|около|почти|в\s+среднем|не\s+обязательно|не\s+всегда|"
83
+ r"вероятно|скорее\s+всего|по\s+оценкам|предположительно|ожида\w*|"
84
+ r"(?:от|до|свыше|более|менее|больше|меньше)(?=\s+\d))\b",
85
+ re.IGNORECASE)
73
86
  # Одна сущность под разными именами не считается новым фактом.
74
87
  ALIASES = {
75
88
  "ai": ("искусственный", "интеллект", "ии", "нейросеть", "модель"),
@@ -84,6 +97,7 @@ class Facts:
84
97
  soft: set[str] = field(default_factory=set) # "два", "половина"
85
98
  words: set[str] = field(default_factory=set) # все слова в нормальной форме, для алиасов
86
99
  claims: set[str] = field(default_factory=set) # "утверждение:первый", "утверждение:до сих пор"
100
+ hedges: set[str] = field(default_factory=set) # "оговорка:обычно", "оговорка:от"
87
101
 
88
102
 
89
103
  @dataclass
@@ -111,9 +125,20 @@ def _norm(word: str) -> str:
111
125
  return _MORPH.parse(low)[0].normal_form.replace("ё", "е")
112
126
 
113
127
 
128
+ # Начало строки и всё, что Markdown ставит перед первым словом: маркер списка
129
+ # («-», «*», «•», «1.», «2)»), цитата «>», решётки заголовка, «**» жирного,
130
+ # эмодзи-буллет. Без этого «- Говорить» или «**Тема**» давали «имя:говорить».
131
+ LINE_LEAD_RE = re.compile(r"^[ \t]*(?:(?:\d{1,3}[.)]|[^\w\s«\"'(\[])[ \t]*)*", re.MULTILINE)
132
+
133
+
114
134
  def _sentence_starts(text: str) -> set[int]:
115
135
  starts = {0}
116
- for m in re.finditer(r"[.!?…]\s+|^[>\-*]\s+|«|\n", text):
136
+ # После двоеточия, тире, открывающей кавычки и скобки заглавная тоже бывает
137
+ # у обычного слова («Совет: Не паникуйте», «(Например: …)»). Имя из словаря
138
+ # (теги Name, Geox, Orgn) и незнакомое словарю слово ловятся и там.
139
+ for m in re.finditer(r"[.!?…:;—–][*_]*\s+[*_«\"„“(]*|[«\"„“(]|\n", text):
140
+ starts.add(m.end())
141
+ for m in LINE_LEAD_RE.finditer(text):
117
142
  starts.add(m.end())
118
143
  return starts
119
144
 
@@ -143,6 +168,9 @@ def extract_facts(text: str) -> Facts:
143
168
  body = CODE_RE.sub(" ", URL_RE.sub(" ", text))
144
169
  for m in MONTH_RE.finditer(body):
145
170
  f.hard.add("месяц:" + m.group(1).lower())
171
+ for m in HEDGE_RE.finditer(body):
172
+ h = re.sub(r"\s+", " ", m.group(0).lower())
173
+ f.hedges.add("оговорка:" + ("может" if h in ("могут", "может") else "ожидается" if h.startswith("ожида") else h))
146
174
  for m in CLAIM_PHRASE_RE.finditer(body):
147
175
  f.claims.add("утверждение:" + m.group(0).lower().replace("ё", "е"))
148
176
  starts = _sentence_starts(body)
@@ -175,7 +203,7 @@ def _alias_covered(fact: str, before_words: set[str]) -> bool:
175
203
 
176
204
  def diff_facts(before: str, after: str) -> FactsDiff:
177
205
  b, a = extract_facts(before), extract_facts(after)
178
- lost = sorted(b.hard - a.hard) + sorted(b.claims - a.claims)
206
+ lost = sorted(b.hard - a.hard) + sorted(b.claims - a.claims) + sorted(b.hedges - a.hedges)
179
207
  added = sorted(x for x in a.hard - b.hard if not _alias_covered(x, b.words))
180
208
  return FactsDiff(lost=lost, added=added, kept=len(b.hard & a.hard),
181
209
  soft_added=sorted(a.soft - b.soft),
@@ -0,0 +1,86 @@
1
+ """Лексическое разнообразие: MATTR по словоформам на начале текста.
2
+
3
+ Свежие модели пишут лексически чище людей: слов из словаря сканера у них нет,
4
+ зато слова почти не повторяются. Человек в посте возвращается к «я», «он»,
5
+ «это», «было», к одному и тому же существительному, модель подбирает синоним.
6
+ Замер на LLMTrace и Пикабу (eval/MODERN-SLOP.md, раздел про разнообразие):
7
+ MATTR у людей 0.900 ± 0.042, у GPT-5.6 на 0.7 σ выше, у o3 и GigaChat-Max
8
+ больше чем на 1 σ.
9
+
10
+ Почему словоформы, а не леммы. По леммам сигнал чуть сильнее, но в браузере
11
+ морфологии нет, а сайт и расширение обязаны считать то же число, что scan.py.
12
+ Поэтому токенизация здесь нарочно примитивная и повторена в docs/scan.js байт
13
+ в байт: нижний регистр, ё → е, слово это кириллица с внутренними дефисами
14
+ («кто-то», «северо-запад»). Латиница и цифры не считаются: в русском тексте
15
+ это имена, бренды и числа, их разнообразие о стиле не говорит.
16
+
17
+ Почему только начало и почему не на коротком. MATTR почти не зависит от
18
+ длины, но длинный текст модель разбавляет повторами заголовков и списков:
19
+ первые LEX_MAX_TOKENS слов сравнивают тексты разной длины честно и одинаково
20
+ дёшево в браузере. На коротком тексте окон мало и число шумит: у людей на
21
+ 60-80 словах разброс 0.058 против 0.037 на 150+, и ниже LEX_MIN_TOKENS больше
22
+ половины срабатываний на людях приходилось именно на такие тексты. Поэтому
23
+ короче LEX_MIN_TOKENS словоформ признак не считается вовсе.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ from dataclasses import dataclass
30
+
31
+ # Окно скользящего TTR, число первых слов, на которых он считается, и
32
+ # минимальная длина, с которой признак вообще считается. Подобраны на половине
33
+ # корпуса (eval/MODERN-SLOP.md): окно 40 и 200 слов разделяют людей и машины
34
+ # лучше, чем 150 (AUC для GPT-5.6 0.78 против 0.75), 300 почти не добавляет.
35
+ LEX_WINDOW = 40
36
+ LEX_MAX_TOKENS = 200
37
+ LEX_MIN_TOKENS = 100
38
+
39
+ _TOKEN_RE = re.compile(r"[а-яё]+(?:-[а-яё]+)*")
40
+
41
+
42
+ @dataclass
43
+ class LexicalStats:
44
+ tokens: int # кириллических словоформ в тексте всего
45
+ mattr: float # MATTR по первым LEX_MAX_TOKENS словоформам; 0.0, если слов меньше окна.
46
+ # Не округляется: штраф считается от точного числа, как в браузере
47
+
48
+ def as_dict(self) -> dict:
49
+ return self.__dict__.copy()
50
+
51
+
52
+ def lexical_tokens(text: str) -> list[str]:
53
+ """Словоформы для MATTR. Порт в docs/scan.js обязан дать тот же список."""
54
+ return [t.replace("ё", "е") for t in _TOKEN_RE.findall(text.lower())]
55
+
56
+
57
+ def mattr(tokens: list[str], window: int = LEX_WINDOW) -> float:
58
+ """Moving-average type-token ratio (Covington & McFall, 2010).
59
+
60
+ Среднее по всем окнам длины window доли разных словоформ. Сумма копится в
61
+ целых числах и делится один раз: так JS и Python получают одно и то же
62
+ число с плавающей точкой, без накопленной разницы округлений.
63
+ """
64
+ n = len(tokens)
65
+ if n < window:
66
+ return 0.0
67
+ counts: dict[str, int] = {}
68
+ for t in tokens[:window]:
69
+ counts[t] = counts.get(t, 0) + 1
70
+ distinct = len(counts)
71
+ total = distinct
72
+ for i in range(window, n):
73
+ new, old = tokens[i], tokens[i - window]
74
+ counts[new] = counts.get(new, 0) + 1
75
+ if counts[new] == 1:
76
+ distinct += 1
77
+ counts[old] -= 1
78
+ if counts[old] == 0:
79
+ distinct -= 1
80
+ total += distinct
81
+ return total / ((n - window + 1) * window)
82
+
83
+
84
+ def lexical_stats(text: str) -> LexicalStats:
85
+ toks = lexical_tokens(text)
86
+ return LexicalStats(tokens=len(toks), mattr=mattr(toks[:LEX_MAX_TOKENS]))
@@ -248,6 +248,68 @@ SCANNER: dict[str, list[str | tuple[str, str]]] = {
248
248
  ("мораль проста (последней фразой)",
249
249
  r"[.!?…]\s+мораль\s+(?:проста|такова)\b[^.!?]{0,60}[.!?]\s*$"),
250
250
  ],
251
+ # Почерк модели 2024-2026. Каталог выше собран на старом слопе («в
252
+ # современном мире», «играет ключевую роль»), и текст свежей модели
253
+ # получал 85-98 «чисто». Эти обороты добыты сравнением 2 110 машинных
254
+ # текстов (GPT-4o, o3, GigaChat-Max, YandexGPT-5, Qwen2.5, DeepSeek-R1,
255
+ # Llama-3.3, Gemma, Claude) с 1 900 человеческими (LLMTrace, Пикабу), см.
256
+ # eval/MODERN-SLOP.md. Каждый встречается у моделей минимум четырёх семейств
257
+ # и не чаще чем у 1% людей (кроме «X — это Y»: 3%, зато у Claude 53%).
258
+ # Поодиночке любой из них бывает у человека, поэтому в score они идут не
259
+ # плотностью, а числом РАЗНЫХ оборотов: первый почти бесплатен, дальше дорого.
260
+ "Почерк модели": [
261
+ ("подчёркивает/акцентирует", r"\bподч[её]ркива(?:ет|ют|я)\b|\bакцентир\w+"),
262
+ ("свидетельствует о", r"\bсвидетельству\w+\s+о\b"),
263
+ ("демонстрирует", r"\bдемонстрир\w+"),
264
+ ("это не просто/не только", r"\bэто\s+не\s+(?:просто|только)\b"),
265
+ ("что делает его", r"\bчто\s+дела(?:ет|ют)\s+(?:его|её|ее|их|это|эту|этот)\b"),
266
+ ("важность", r"\bважност\w+"),
267
+ ("заставляет задуматься", r"\b(?:заставля\w+|стоит|стоило|повод)\s+задуматься\b"),
268
+ ("стал символом", r"\bсимволом\b"),
269
+ ("стал настоящим открытием", r"\bнастоящ(?:им|ей|ими)\s+[а-яё]{4,}"),
270
+ ("важный шаг", r"\b(?:важн|значим|ключев|решающ|смел|уверенн)\w*\s+шаг\w*"),
271
+ ("представьте себе", r"\bпредставьте\b"),
272
+ ("заслуживает", r"\bзаслужива\w+"),
273
+ ("для тех, кто", r"\bдля\s+тех,?\s+кто\b"),
274
+ ("Это позволяет",
275
+ r"(?:^|[.!?]\s+)(?:это|такой\s+подход|всё\s+это|все\s+это)\s+"
276
+ r"(?:позволя|помога|дела|да[её]т|способству|созда|обеспечива|станов)\w*"),
277
+ ("вдохновляет", r"\bвдохнов\w+"),
278
+ ("незабываемый", r"\bнезабываем\w+"),
279
+ ("гармония", r"\bгармони\w+"),
280
+ ("X — это Y", r"[а-яё»)]\s+—\s+это\s"),
281
+ ("оставляет впечатление", r"\bоставля\w+\s+(?:\w+\s+)?(?:впечатлени|след|равнодушн)\w*"),
282
+ ("напоминает о", r"\bнапомина\w+\s+(?:нам\s+)?(?:о|об|что)\b"),
283
+ ("может привести к", r"\bможет\s+привести\s+к\b"),
284
+ ("невероятно", r"\bневероятн\w+"),
285
+ ],
286
+ # Обвязка ответа чат-бота, которая уезжает в текст при копировании: оферта
287
+ # доработки, плейсхолдеры, «Вариант 1», Markdown-разметка. У людей в
288
+ # LLMTrace и на Пикабу 0% (подзаголовок «##» 0,7%: вёрстка статей), у Claude
289
+ # 14-71% по признаку. Слабее «Артефактов копипасты»: разметку Markdown
290
+ # человек тоже пишет, поэтому штраф за признак, а не приговор.
291
+ # Строки ловятся через \n, а не ^/$: веб-движок работает без флага m.
292
+ "Обвязка чата": [
293
+ ("предложение доработки",
294
+ r"\b(?:хотите,?\s+чтобы\s+я|могу\s+также|могу\s+(?:подготовить|предложить|адаптировать|"
295
+ r"переписать|сделать\s+(?:вариант|версию))|могу\s+помочь\s+с)\b"
296
+ r"|\b(?:если\s+(?:нужно|хотите|надо)|при\s+желании)[^.\n]{0,25}(?:—|,|-)\s*(?:я\s+)?"
297
+ r"(?:могу|сделаю|подготовлю|напишу|адаптирую|доработаю)\b"
298
+ r"|\b(?:пришлите|скажите|уточните|дайте\s+знать)[^\n]{0,80}(?:—|,|и)\s*(?:я\s+)?(?:сразу\s+)?"
299
+ r"(?:подготовлю|доработаю|напишу|адаптирую|подстрою|переделаю)\b"),
300
+ ("плейсхолдер [Ваше имя]",
301
+ r"\[(?:ваш|ваше|ваша|имя|название|дата|компани|должност|указать|число|телефон|ссылк|"
302
+ r"город|продукт|причин|конкретн)[^\]\n]{0,60}\]"),
303
+ ("«Вот вариант:» / «Вариант 1»",
304
+ r"(?:^|\n)[ \t]*(?:\*\*)?(?:вот|ниже)\s+(?:несколько\s+)?(?:вариант|пример|шаблон|текст|"
305
+ r"черновик|готов|проверенн)\w*[^\n]{0,80}:"
306
+ r"|(?:^|\n)[ \t]*(?:\*\*|#+[ \t]*)?вариант\s+\d"),
307
+ ("эмодзи вместо маркеров списка", r"(?:^|\n)[ \t]*(?:[0-9]️?⃣|[❌✅✔➡→▶👉🔹🔸])"),
308
+ # Только подзаголовки разделов: одиночный «# Название» у человека норма
309
+ # (заголовок рассказа в фикстуре eval/corpus/literary).
310
+ ("markdown-подзаголовок ##", r"(?:^|\n)#{2,4}[ \t]+\S"),
311
+ ("жирный подзаголовок **…**", r"(?:^|\n)[ \t]*\*\*[^*\n]{2,80}\*\*:?[ \t]*(?=\n)"),
312
+ ],
251
313
  # Технические следы копирования из интерфейса чат-бота. Однозначные улики:
252
314
  # в человеческом тексте не встречаются.
253
315
  # Префикс "re:" = регулярное выражение, остальное — подстрока.
@@ -369,10 +431,14 @@ GENRE_MUTED_BANS: dict[str, set[str]] = {
369
431
  }
370
432
 
371
433
  GENRE_MUTED_CATEGORIES: dict[str, set[str]] = {
372
- "academic": {"Канцелярит", "Кальки", "Контекстуализаторы", "Формула-выводы"},
373
- "legal": {"Канцелярит", "Кальки", "Контекстуализаторы"},
374
- "fiction": {"Параллелизмы"},
375
- "news": {"Канцелярит", "Кальки", "Контекстуализаторы"},
434
+ # «Почерк модели» снят везде, кроме умолчания: «свидетельствует о»,
435
+ # «демонстрирует», «X — это Y» в новостях, науке и праве законная норма, а в
436
+ # художественной речи это реплики героев. Умолчание (блоги, посты,
437
+ # маркетинг) остаётся строгим: под него и собирался корпус.
438
+ "academic": {"Канцелярит", "Кальки", "Контекстуализаторы", "Формула-выводы", "Почерк модели"},
439
+ "legal": {"Канцелярит", "Кальки", "Контекстуализаторы", "Почерк модели"},
440
+ "fiction": {"Параллелизмы", "Почерк модели"},
441
+ "news": {"Канцелярит", "Кальки", "Контекстуализаторы", "Почерк модели"},
376
442
  }
377
443
 
378
444
  GENRES = ("marketing", "academic", "legal", "fiction", "news")
@@ -107,6 +107,8 @@ def scan(text: str, genre: str | None = None) -> dict:
107
107
  "rhythm": rep.rhythm.as_dict(),
108
108
  "morph": rep.morph.as_dict(),
109
109
  "structure": rep.structure.as_dict(),
110
+ "lexical": rep.lexical.as_dict(),
111
+ "repeats": rep.repeats.as_dict(),
110
112
  }
111
113
 
112
114
 
@@ -0,0 +1,139 @@
1
+ """Повтор фразы между абзацами: модель пересказывает сама себя.
2
+
3
+ Признак: одна и та же цепочка из REPEAT_N словоформ дословно стоит в двух
4
+ разных абзацах. Так пишут Llama-3.3 и YandexGPT: вывод повторяет вступление,
5
+ пункт повторяет подводку. У людей тоже бывает, прежде всего в новостях (лид
6
+ пересказан в теле заметки), в справках с длинными официальными названиями и в
7
+ лонгридах.
8
+
9
+ Почему это заметка, а не штраф. Подбор на LLMTrace, Пикабу и длинных
10
+ человеческих текстах (eval/MODERN-SLOP.md, раздел про повтор фраз) показал
11
+ потолок пользы: даже бесконечный штраф вывел бы из полосы «чисто» 2,8% текстов
12
+ Llama-3.3 и меньше процента у остальных моделей, потому что текст с такими
13
+ повторами почти всегда уже задет другими признаками (медиана счёта у Llama с
14
+ повтором 58, без повтора 80). Людей при этом задело бы столько же или больше. Поэтому в счёт повтор не входит, сканер
15
+ только показывает сами фразы: это готовая подсказка для правки.
16
+
17
+ Токенизация своя и повторена в docs/scan.js байт в байт, как у MATTR:
18
+ - абзац это непустая строка (у Пикабу и в чатах абзацы идут через один
19
+ перевод строки, пустая строка тут не обязательна);
20
+ - строки-заголовки Markdown («## ...») не считаются;
21
+ - цитаты в «ёлочках», „лапках“ и "прямых" кавычках вырезаются: чужие слова
22
+ автор повторяет законно;
23
+ - слово это кириллица с внутренними дефисами после нижнего регистра и ё → е;
24
+ - латиница, цифры, скобки, кавычки и знаки конца предложения рвут цепочку:
25
+ n-грамма не перескакивает через «2020» и через точку;
26
+ - n-грамма, где меньше REPEAT_MIN_CONTENT полнозначных слов («и в то же
27
+ время, как и»), не считается: это грамматика, а не фраза;
28
+ - считаются только первые REPEAT_MAX_TOKENS словоформ: длинный текст копит
29
+ повторы просто длиной, и без окна признак на лонгридах людей шумит.
30
+
31
+ Перекрывающиеся n-граммы одного повтора склеиваются: «мы живём в мире, где
32
+ всё меняется» это одна фраза, а не две шестёрки.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import re
38
+ from dataclasses import dataclass, field
39
+
40
+ REPEAT_N = 6
41
+ REPEAT_MAX_TOKENS = 600
42
+ REPEAT_MIN_CONTENT = 2
43
+ # Сколько разных фраз нужно для заметки. Одна повторённая фраза в первых 600
44
+ # словах у людей встречается в 1-3% коротких текстов и в 4-16% длинных, две в
45
+ # 0,2-0,7% коротких и 1-5% длинных.
46
+ REPEAT_MIN_PHRASES = 2
47
+
48
+ # Служебные слова: предлоги, союзы, частицы, местоимения, связка. N-грамма из
49
+ # одних таких слов («что это может быть так») фразой не считается.
50
+ FUNCTION_WORDS = frozenset("""
51
+ а б бы в во вот да для до же за и из или им их к ко как ли либо между на над не ни но о об обо
52
+ от ото по под при про с со так там то тоже только у уже чем что чтобы это этот эта эти этого этой
53
+ этом этим тот та те того той том тем тех я ты он она оно мы вы они его ее их ему ей нам вам
54
+ меня тебя нас вас мне тебе себя себе свой своя свое свои своего своей своих своим который которая
55
+ которое которые которого которой которых котором которым был была было были быть есть будет будут
56
+ бывает если когда где куда чтоб все весь вся всего всех всем также еще уж даже лишь нет
57
+ очень более менее можно нужно надо может могут мой моя мое мои наш наша наше наши ваш ваша ваше
58
+ ваши кто чего чему ним ней них него нее нему
59
+ """.split())
60
+
61
+ _TOKEN_RE = re.compile(r"[а-яё]+(?:-[а-яё]+)*|[a-z0-9]+|[.!?…;:()\[\]\"«»“”„]")
62
+ _QUOTE_RE = re.compile(r"«[^«»\n]*»|„[^„“\n]*“|\"[^\"\n]*\"")
63
+ # Пробелы явные, не \s: у Python и JS классы \s расходятся на редких символах.
64
+ _HEADING_RE = re.compile(r"^[ \t]*#{1,6}[ \t]")
65
+
66
+
67
+ @dataclass
68
+ class RepeatStats:
69
+ tokens: int # словоформ просмотрено (не больше REPEAT_MAX_TOKENS)
70
+ phrases: list[str] = field(default_factory=list) # разные повторённые фразы, в порядке текста
71
+
72
+ def as_dict(self) -> dict:
73
+ return {"tokens": self.tokens, "phrases": list(self.phrases)}
74
+
75
+
76
+ def _runs(line: str) -> list[list[str]]:
77
+ """Цепочки подряд идущих кириллических словоформ строки."""
78
+ runs: list[list[str]] = []
79
+ cur: list[str] = []
80
+ for tok in _TOKEN_RE.findall(line.lower()):
81
+ if "а" <= tok[0] <= "я" or tok[0] == "ё":
82
+ cur.append(tok.replace("ё", "е"))
83
+ else:
84
+ if cur:
85
+ runs.append(cur)
86
+ cur = []
87
+ if cur:
88
+ runs.append(cur)
89
+ return runs
90
+
91
+
92
+ def repeat_stats(text: str) -> RepeatStats:
93
+ """Повторённые между абзацами фразы. Порт в docs/scan.js обязан дать тот же список."""
94
+ text = _QUOTE_RE.sub(" . ", text)
95
+ budget = REPEAT_MAX_TOKENS
96
+ where: dict[str, set[int]] = {} # n-грамма -> номера строк
97
+ order: list[tuple[int, int, list[str]]] = [] # (строка, сквозная позиция, цепочка)
98
+ pos = 0
99
+ for li, line in enumerate(text.split("\n")):
100
+ if budget <= 0:
101
+ break
102
+ if _HEADING_RE.match(line):
103
+ continue
104
+ for run in _runs(line):
105
+ run = run[:budget]
106
+ budget -= len(run)
107
+ for i in range(len(run) - REPEAT_N + 1):
108
+ key = " ".join(run[i:i + REPEAT_N])
109
+ where.setdefault(key, set()).add(li)
110
+ order.append((li, pos + i, run[i:i + REPEAT_N]))
111
+ pos += len(run) + 1 # разрыв: следующая цепочка не продолжает эту
112
+ if budget <= 0:
113
+ break
114
+ seen = REPEAT_MAX_TOKENS - max(budget, 0)
115
+
116
+ def repeated(words: list[str]) -> bool:
117
+ key = " ".join(words)
118
+ return (len(where[key]) >= 2
119
+ and sum(1 for w in words if w not in FUNCTION_WORDS) >= REPEAT_MIN_CONTENT)
120
+
121
+ phrases: list[str] = []
122
+ keys: set[str] = set()
123
+ cur: list[str] = []
124
+ cur_key = ""
125
+ prev = (-1, -2)
126
+ for li, p, words in order:
127
+ if not repeated(words):
128
+ continue
129
+ if prev[0] == li and prev[1] == p - 1 and cur:
130
+ cur.append(words[-1]) # та же фраза тянется дальше
131
+ else:
132
+ if cur and cur_key not in keys:
133
+ keys.add(cur_key)
134
+ phrases.append(" ".join(cur))
135
+ cur, cur_key = list(words), " ".join(words)
136
+ prev = (li, p)
137
+ if cur and cur_key not in keys:
138
+ phrases.append(" ".join(cur))
139
+ return RepeatStats(tokens=seen, phrases=phrases)
@@ -11,12 +11,15 @@
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import math
14
15
  from dataclasses import dataclass, field
15
16
 
16
17
  from .burstiness import CV_HUMAN_TARGET, STACCATO_MIN_RUN
18
+ from .lexical import LEX_MIN_TOKENS
17
19
  from .markers import (GENRE_MUTED_BANS, GENRE_MUTED_CATEGORIES,
18
20
  effective_hard_bans, mute_by_genre)
19
21
  from .morphology import NV_TARGET
22
+ from .repeats import REPEAT_MIN_PHRASES
20
23
  from .structure import (
21
24
  LISTICLE_MIN_ITEMS,
22
25
  LISTICLE_SHARE_AI,
@@ -30,6 +33,21 @@ from .structure import (
30
33
  EM_DASH_NAME = "Длинное тире"
31
34
  COPY_PASTE_CATEGORY = "Артефакты копипасты"
32
35
 
36
+ # Почерк свежих моделей и обвязка чата (markers.py, eval/MODERN-SLOP.md).
37
+ # Считаются числом РАЗНЫХ оборотов, а не плотностью: плотность размывается на
38
+ # длинном тексте, а у современной модели два-три таких оборота на пост. Один
39
+ # оборот бывает и у человека (до 3%), поэтому первый стоит 3 балла, каждый
40
+ # следующий 10. Веса подобраны на половине корпуса и проверены на второй:
41
+ # пойманного слопа 53% → 67%, ложных тревог на людях +1 п.п.
42
+ SIGNATURE_CATEGORY = "Почерк модели"
43
+ SIGNATURE_FIRST = 3
44
+ SIGNATURE_NEXT = 10
45
+ SIGNATURE_MAX = 24
46
+ CHAT_WRAP_CATEGORY = "Обвязка чата"
47
+ CHAT_WRAP_EACH = 10
48
+ CHAT_WRAP_MAX = 20
49
+ DISTINCT_CATEGORIES = (SIGNATURE_CATEGORY, CHAT_WRAP_CATEGORY)
50
+
33
51
  # Полосы. Совпадают с порогами вмешательства из SKILL.md.
34
52
  BAND_CLEAN = 85 # ≥ — следы ИИ не мешают, не править
35
53
  BAND_EDIT = 60 # ≥ — точечная правка; < — полный рерайт
@@ -57,6 +75,28 @@ STERILE_MIN_WORDS = 100
57
75
  # них настоящие авторские обрывки.
58
76
  STACCATO_PENALTY = 8
59
77
  STACCATO_PENALTY_MAX = 14
78
+ # Лексическое разнообразие (lexical.py): слова почти не повторяются, модель
79
+ # подбирает синоним там, где человек сказал бы то же слово или «он». MATTR у
80
+ # людей 0.902 ± 0.037, порог 0.955 это +1.4 σ. Балл за каждую тысячную выше
81
+ # порога, потолок 15: ниже, чем у почерка модели, потому что признак
82
+ # статистический и на одном тексте шумит. Подобрано на половине корпуса и
83
+ # проверено на второй (eval/MODERN-SLOP.md): ловит GigaChat-Max и o3, а GPT-5.6
84
+ # не сдвигает, его тексты лежат внутри человеческого разброса.
85
+ #
86
+ # В новостях, научном и юридическом регистре штраф снят: плотный фактический
87
+ # текст разнообразен законно. На людях LLMTrace он срабатывал чаще всего на
88
+ # новостях (2,8% текстов против 0,6-0,9% у статей и отзывов), а на справочных
89
+ # текстах задевал людей почти так же часто, как машины, и ни одну машину не
90
+ # перевёл из «чисто». В художественном жанре оставлен: на рассказах LLMTrace
91
+ # машины срабатывают впятеро чаще людей.
92
+ LEX_THRESHOLD = 0.955
93
+ LEX_SLOPE = 1000
94
+ LEX_PENALTY_MAX = 15
95
+ LEX_MUTED_GENRES = frozenset({"news", "academic", "legal"})
96
+ # Повтор фразы между абзацами (repeats.py): заметка без штрафа. Даже бесконечный
97
+ # штраф вывел бы из «чисто» 0,8-2,8% текстов одной Llama-3.3, у людей потери
98
+ # того же порядка (eval/MODERN-SLOP.md). Показываем не больше REPEAT_SHOW фраз.
99
+ REPEAT_SHOW = 3
60
100
  # Потолок штрафа за номинальность. Именованный, потому что браузерный сканер
61
101
  # морфологии не имеет и объявляет ровно эту величину как неизмеренную.
62
102
  NV_PENALTY_MAX = 8
@@ -130,13 +170,26 @@ def cleanliness_score(report, genre: str | None = None) -> ScoreResult:
130
170
  penalties.append((f"артефакты копипасты: {copy_paste}", -pen))
131
171
 
132
172
  # 3. Мягкие маркеры (кроме копипасты) по плотности на 100 слов.
133
- soft = sum(h.count for h in markers if h.category != COPY_PASTE_CATEGORY)
173
+ soft = sum(h.count for h in markers
174
+ if h.category != COPY_PASTE_CATEGORY and h.category not in DISTINCT_CATEGORIES)
134
175
  if soft:
135
176
  pen = min(30, round(2 * _per100(soft, words)))
136
177
  if pen:
137
178
  score -= pen
138
179
  penalties.append((f"маркеры: {soft} ({_per100(soft, words):.1f}/100 слов)", -pen))
139
180
 
181
+ # 3б. Почерк модели и обвязка чата: по числу разных оборотов (см. константы).
182
+ sig = len({h.marker for h in markers if h.category == SIGNATURE_CATEGORY})
183
+ if sig:
184
+ pen = min(SIGNATURE_MAX, SIGNATURE_FIRST + SIGNATURE_NEXT * (sig - 1))
185
+ score -= pen
186
+ penalties.append((f"почерк модели: {sig} {_plural(sig, 'оборот', 'оборота', 'оборотов')}", -pen))
187
+ wrap = len({h.marker for h in markers if h.category == CHAT_WRAP_CATEGORY})
188
+ if wrap:
189
+ pen = min(CHAT_WRAP_MAX, CHAT_WRAP_EACH * wrap)
190
+ score -= pen
191
+ penalties.append((f"обвязка чата: {wrap} {_plural(wrap, 'след', 'следа', 'следов')}", -pen))
192
+
140
193
  # 4. Длинное тире по плотности с допуском ~2 на 100 слов: «—» штатно
141
194
  # используется в русском (Википедия, «это —», диапазоны). Штраф мягкий,
142
195
  # потому что на изданной прозе оно частая норма; сам бан держится на
@@ -166,6 +219,18 @@ def cleanliness_score(report, genre: str | None = None) -> ScoreResult:
166
219
  f"рваная медитативность: {runs} {_plural(runs, 'цепочка', 'цепочки', 'цепочек')} "
167
220
  f"обрывков по {STACCATO_MIN_RUN}+ подряд (самая длинная {report.rhythm.staccato_max})", -pen))
168
221
 
222
+ # 5в. Лексическое разнообразие: считается только на тексте от LEX_MIN_TOKENS
223
+ # словоформ, на коротком MATTR шумит. floor, а не round: браузер обязан
224
+ # получить то же целое, а округление половин в JS и Python разное.
225
+ lex = report.lexical
226
+ if (genre not in LEX_MUTED_GENRES and lex.tokens >= LEX_MIN_TOKENS
227
+ and lex.mattr > LEX_THRESHOLD):
228
+ pen = min(LEX_PENALTY_MAX, math.floor((lex.mattr - LEX_THRESHOLD) * LEX_SLOPE))
229
+ if pen:
230
+ score -= pen
231
+ penalties.append((f"лексическое разнообразие (MATTR={lex.mattr:.3f}, "
232
+ f"порог {LEX_THRESHOLD})", -pen))
233
+
169
234
  # 6. Номинальность: сущ./глаг. выше цели 2.5 = канцелярит. Слабый сигнал и
170
235
  # главный источник ложных срабатываний (энциклопедический/юр. регистр
171
236
  # легитимно номинален), поэтому штраф мягкий и низко ограничен.
@@ -199,7 +264,7 @@ def cleanliness_score(report, genre: str | None = None) -> ScoreResult:
199
264
  # Условие ровно то, что измерялось: ноль банов и ноль маркеров. Ритм,
200
265
  # номинальность и структура сюда не входят, иначе текст с минусом за ровный
201
266
  # ритм терял бы заметку, хотя по лексике он как раз стерилен.
202
- if not (hard_phrase or copy_paste or soft) and words >= STERILE_MIN_WORDS:
267
+ if not (hard_phrase or copy_paste or soft or sig or wrap) and words >= STERILE_MIN_WORDS:
203
268
  share = next((s for limit, s in HUMAN_ZERO_SHARE if words < limit),
204
269
  HUMAN_ZERO_SHARE[-1][1])
205
270
  notes.append(
@@ -207,5 +272,14 @@ def cleanliness_score(report, genre: str | None = None) -> ScoreResult:
207
272
  f"в {words} слов, остальные {100 - share:.0f}% что-нибудь да используют. "
208
273
  "Цель не ноль, а типичная для жанра частота: вычищать дальше незачем")
209
274
 
275
+ # Заметка о повторе фраз между абзацами. Почему не штраф: комментарий у REPEAT_SHOW.
276
+ phrases = report.repeats.phrases
277
+ if len(phrases) >= REPEAT_MIN_PHRASES:
278
+ shown = ", ".join(f"«{p}»" for p in phrases[:REPEAT_SHOW])
279
+ more = f" и ещё {len(phrases) - REPEAT_SHOW}" if len(phrases) > REPEAT_SHOW else ""
280
+ notes.append(
281
+ f"повтор фраз между абзацами: {shown}{more}. В счёт не входит: у людей так "
282
+ "бывает в новостях и справках. Если повтор не нарочный, оставьте фразу в одном месте")
283
+
210
284
  final = max(0, min(100, round(score)))
211
285
  return ScoreResult(score=final, band=_band(final), penalties=penalties, notes=notes)
@@ -24,10 +24,13 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
24
24
  try:
25
25
  from humanizer_metrics import analyze, cleanliness_score, diff_facts, facts_verdict
26
26
  from humanizer_metrics.burstiness import rhythm_verdict
27
+ from humanizer_metrics.lexical import LEX_MIN_TOKENS
28
+ from humanizer_metrics.score import LEX_MUTED_GENRES, LEX_THRESHOLD
27
29
  from humanizer_metrics.markers import (GENRE_MUTED_BANS, GENRE_MUTED_CATEGORIES,
28
30
  GENRES, effective_hard_bans, marker_verdict,
29
31
  mute_by_genre)
30
32
  from humanizer_metrics.morphology import morph_verdict
33
+ from humanizer_metrics.repeats import REPEAT_MAX_TOKENS, REPEAT_N
31
34
  from humanizer_metrics.structure import structure_verdict
32
35
  except ImportError as exc:
33
36
  print(f"[ошибка] не хватает зависимостей сканера ({exc.name}); балла не будет.\n"
@@ -185,6 +188,14 @@ def main() -> int:
185
188
  f"(min {rep.rhythm.min_len} / max {rep.rhythm.max_len}), CV: {rep.rhythm.cv_len}")
186
189
  print(f" многоточий: {rep.rhythm.ellipsis}, скобок: {rep.rhythm.parentheses}, "
187
190
  f"вопросов: {rep.rhythm.questions}")
191
+ if rep.lexical.tokens >= LEX_MIN_TOKENS:
192
+ rule = (f"в жанре {genre} не штрафуется" if genre in LEX_MUTED_GENRES
193
+ else f"штраф выше {LEX_THRESHOLD}")
194
+ print(f" лексическое разнообразие: MATTR {rep.lexical.mattr:.3f} "
195
+ f"(у людей обычно 0.90 ± 0.04, {rule})")
196
+ else:
197
+ print(f" лексическое разнообразие: не считается, словоформ {rep.lexical.tokens} "
198
+ f"(нужно от {LEX_MIN_TOKENS})")
188
199
  print()
189
200
 
190
201
  print("Морфология:")
@@ -195,6 +206,16 @@ def main() -> int:
195
206
 
196
207
  print("Структура (уровень документа):")
197
208
  print(f" {structure_verdict(rep.structure)}")
209
+ phrases = rep.repeats.phrases
210
+ if phrases:
211
+ print(f" повтор фраз между абзацами: {len(phrases)} (цепочки от {REPEAT_N} слов в первых "
212
+ f"{REPEAT_MAX_TOKENS} словоформах, в счёт не входит)")
213
+ for ph in phrases[:5]:
214
+ print(f" «{ph}»")
215
+ if len(phrases) > 5:
216
+ print(f" … и ещё {len(phrases) - 5}")
217
+ else:
218
+ print(f" повторов фраз между абзацами нет (цепочки от {REPEAT_N} слов)")
198
219
 
199
220
  if fdiff is not None:
200
221
  print()