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 +5 -5
- package/README.md +8 -8
- package/package.json +2 -2
- package/skills/humanizer-ru/SKILL.md +34 -17
- package/skills/humanizer-ru/agents/openai.yaml +1 -1
- package/skills/humanizer-ru/references/audit.md +35 -1
- package/skills/humanizer-ru/references/catalog.md +40 -3
- package/skills/humanizer-ru/scripts/humanizer_metrics/__init__.py +12 -0
- package/skills/humanizer-ru/scripts/humanizer_metrics/facts.py +30 -2
- package/skills/humanizer-ru/scripts/humanizer_metrics/lexical.py +86 -0
- package/skills/humanizer-ru/scripts/humanizer_metrics/markers.py +70 -4
- package/skills/humanizer-ru/scripts/humanizer_metrics/mcp_server.py +2 -0
- package/skills/humanizer-ru/scripts/humanizer_metrics/repeats.py +139 -0
- package/skills/humanizer-ru/scripts/humanizer_metrics/score.py +76 -2
- package/skills/humanizer-ru/scripts/scan.py +21 -0
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)
|
|
8
|
-
[](https://github.com/ilyautov/humanizer-ru/blob/main/CHANGELOG.md)
|
|
9
9
|
[](https://github.com/ilyautov/humanizer-ru/stargazers)
|
|
10
10
|
[](https://skills.sh/ilyautov/humanizer-ru/humanizer-ru)
|
|
11
11
|
[](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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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. Убирает
|
|
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)
|
|
8
|
-
[](https://github.com/ilyautov/humanizer-ru/blob/main/CHANGELOG.md)
|
|
9
9
|
[](https://github.com/ilyautov/humanizer-ru/stargazers)
|
|
10
10
|
[](https://skills.sh/ilyautov/humanizer-ru/humanizer-ru)
|
|
11
11
|
[](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: убирает следы нейросети из русского текста.
|
|
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), [
|
|
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
|
-
**
|
|
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
|
|
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-текстов), «стоит отметить», «является», длинное тире, отсутствие частиц «же/ведь/вот», равномерная информационная плотность. Скилл ловит и заменяет все
|
|
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
|
-
| Паттернов |
|
|
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.
|
|
4
|
-
"description": "Agent skill for DeepSeek Harness: rewrites Russian text to remove
|
|
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.
|
|
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
|
-
**Сканер.** Если доступны
|
|
50
|
-
|
|
49
|
+
**Сканер.** Если доступны команды, первый вызов инструмента в редактуре это
|
|
50
|
+
сканер, до каталога и до правки: `python3 <папка скилла>/scripts/scan.py файл.txt` или
|
|
51
51
|
`echo "текст" | python3 <папка скилла>/scripts/scan.py -`. Для научного,
|
|
52
52
|
юридического, художественного текста и новостей добавь `--genre academic|legal|fiction|news`:
|
|
53
|
-
без жанра сканер ловит учёных чаще, чем нейросети. Запомни балл «было».
|
|
54
|
-
|
|
55
|
-
|
|
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: "Убирает следы нейросети из русского текста:
|
|
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
|
-
## Каталог:
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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()
|