fetchit-engine 0.1.2__tar.gz → 0.3.0__tar.gz
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.
- {fetchit_engine-0.1.2/fetchit_engine.egg-info → fetchit_engine-0.3.0}/PKG-INFO +43 -43
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/fetchit_engine/core.py +160 -23
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/fetchit_engine/ruleset.json +609 -389
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0/fetchit_engine.egg-info}/PKG-INFO +43 -43
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/pyproject.toml +1 -1
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/setup.cfg +4 -4
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/LICENSE +0 -0
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/NOTICE +0 -0
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/README.md +0 -0
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/fetchit_engine/__init__.py +0 -0
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/fetchit_engine/spell_core.py +0 -0
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/fetchit_engine.egg-info/SOURCES.txt +0 -0
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/fetchit_engine.egg-info/dependency_links.txt +0 -0
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/fetchit_engine.egg-info/requires.txt +0 -0
- {fetchit_engine-0.1.2 → fetchit_engine-0.3.0}/fetchit_engine.egg-info/top_level.txt +0 -0
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: fetchit-engine
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Deterministic text cleanup and AI-writing heuristics. Runs entirely in your process; text never leaves it.
|
|
5
|
-
Author: Outta Work Solutions
|
|
6
|
-
License-Expression: Apache-2.0
|
|
7
|
-
Project-URL: Homepage, https://fetchitai.com/developers
|
|
8
|
-
Project-URL: Repository, https://github.com/OuttaWorkSolutions/fetchit-engine
|
|
9
|
-
Project-URL: Changelog, https://github.com/OuttaWorkSolutions/fetchit-engine/blob/main/CHANGELOG.md
|
|
10
|
-
Project-URL: Issues, https://github.com/OuttaWorkSolutions/fetchit-engine/issues
|
|
11
|
-
Keywords: ai text,cleanup,invisible characters,em dash,text hygiene
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier: Topic :: Text Processing :: Linguistic
|
|
15
|
-
Requires-Python: >=3.8
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
License-File: LICENSE
|
|
18
|
-
License-File: NOTICE
|
|
19
|
-
Provides-Extra: spell
|
|
20
|
-
Requires-Dist: pyspellchecker>=0.7; extra == "spell"
|
|
21
|
-
Dynamic: license-file
|
|
22
|
-
|
|
23
|
-
# fetchit-engine
|
|
24
|
-
|
|
25
|
-
Deterministic text cleanup and AI-writing heuristics that run entirely in your
|
|
26
|
-
process. Your users text never leaves it. Pure standard library, no dependencies.
|
|
27
|
-
|
|
28
|
-
```python
|
|
29
|
-
from fetchit_engine import clean, apply_edits
|
|
30
|
-
|
|
31
|
-
r = clean(ai_draft, {"mode": "auto"}) # apply only auto-safe edits unattended
|
|
32
|
-
publish(r["cleaned"]["text"])
|
|
33
|
-
if r["aiReport"].get("level") == "high":
|
|
34
|
-
review_queue.put(ai_draft, r) # hand a human the full result
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Offsets are code points. See the [repository README](https://github.com/OuttaWorkSolutions/fetchit-engine#the-design-in-four-claims)
|
|
38
|
-
for the full CleanResult contract, shared 1:1 with @fetchitai/engine (JavaScript).
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
## License
|
|
42
|
-
|
|
43
|
-
Apache-2.0. Free for everyone, including commercial use.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fetchit-engine
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Deterministic text cleanup and AI-writing heuristics. Runs entirely in your process; text never leaves it.
|
|
5
|
+
Author: Outta Work Solutions
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://fetchitai.com/developers
|
|
8
|
+
Project-URL: Repository, https://github.com/OuttaWorkSolutions/fetchit-engine
|
|
9
|
+
Project-URL: Changelog, https://github.com/OuttaWorkSolutions/fetchit-engine/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/OuttaWorkSolutions/fetchit-engine/issues
|
|
11
|
+
Keywords: ai text,cleanup,invisible characters,em dash,text hygiene
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
License-File: NOTICE
|
|
19
|
+
Provides-Extra: spell
|
|
20
|
+
Requires-Dist: pyspellchecker>=0.7; extra == "spell"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# fetchit-engine
|
|
24
|
+
|
|
25
|
+
Deterministic text cleanup and AI-writing heuristics that run entirely in your
|
|
26
|
+
process. Your users text never leaves it. Pure standard library, no dependencies.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from fetchit_engine import clean, apply_edits
|
|
30
|
+
|
|
31
|
+
r = clean(ai_draft, {"mode": "auto"}) # apply only auto-safe edits unattended
|
|
32
|
+
publish(r["cleaned"]["text"])
|
|
33
|
+
if r["aiReport"].get("level") == "high":
|
|
34
|
+
review_queue.put(ai_draft, r) # hand a human the full result
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Offsets are code points. See the [repository README](https://github.com/OuttaWorkSolutions/fetchit-engine#the-design-in-four-claims)
|
|
38
|
+
for the full CleanResult contract, shared 1:1 with @fetchitai/engine (JavaScript).
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
Apache-2.0. Free for everyone, including commercial use.
|
|
@@ -15,7 +15,7 @@ import json
|
|
|
15
15
|
import os
|
|
16
16
|
import re
|
|
17
17
|
|
|
18
|
-
ENGINE_VERSION = "0.
|
|
18
|
+
ENGINE_VERSION = "0.3.0"
|
|
19
19
|
|
|
20
20
|
# --- ruleset (single source of truth, shared with the JS package) -----------
|
|
21
21
|
_RULESET_PATH = os.path.join(
|
|
@@ -36,6 +36,14 @@ _INVISIBLE_RANGES = RULESET["invisibleRanges"]
|
|
|
36
36
|
_ODD_SPACE_RANGES = RULESET["oddSpaceRanges"]
|
|
37
37
|
_ODD_SPACE_RULE_ID = RULESET["oddSpaceRuleId"]
|
|
38
38
|
_ODD_SPACE_LABEL = RULESET["oddSpaceLabel"]
|
|
39
|
+
_CONFUSABLE_RULE_ID = RULESET["confusableRuleId"]
|
|
40
|
+
_CONFUSABLE_LABEL = RULESET["confusableLabel"]
|
|
41
|
+
_TYPOGRAPHY_RULE_ID = RULESET["typographyRuleId"]
|
|
42
|
+
_TYPOGRAPHY_LABEL = RULESET["typographyLabel"]
|
|
43
|
+
# code point -> replacement, built once from the shared tables so both
|
|
44
|
+
# languages derive the same lookup from the same data.
|
|
45
|
+
_CONFUSABLES = {c["cp"]: c["to"] for c in RULESET["confusables"]}
|
|
46
|
+
_TYPOGRAPHY = {c["cp"]: c["to"] for c in RULESET["typography"]}
|
|
39
47
|
_T = RULESET["thresholds"]
|
|
40
48
|
MIN_CHARS = _T["MIN_CHARS"]
|
|
41
49
|
MIN_WORDS = _T["MIN_WORDS"]
|
|
@@ -43,10 +51,12 @@ _LEVEL_MODERATE = _T["levelModerate"]
|
|
|
43
51
|
_LEVEL_HIGH = _T["levelHigh"]
|
|
44
52
|
|
|
45
53
|
# --- regexes that are logic, not data (kept in code; may contain dashes) -----
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
|
|
54
|
+
# A RUN of em dashes (U+2014) / horizontal bars (U+2015), optionally
|
|
55
|
+
# space-separated, is one match, so "a——b" produces one comma, not two edits;
|
|
56
|
+
# an en dash (U+2013) matches only when spaced on both sides, so numeric
|
|
57
|
+
# ranges like 3–5 survive. Only spaces/tabs are consumed so line breaks
|
|
58
|
+
# survive. Mirrors EM_DASH_RE in the JS engine.
|
|
59
|
+
_EM_DASH = re.compile(r"[ \t]*[—―](?:[ \t]*[—―])*[ \t]*|[ \t]+–[ \t]+")
|
|
50
60
|
_MULTI_SPACE = re.compile(r"[ \t]{2,}")
|
|
51
61
|
_SPACE_BEFORE_PUNCT = re.compile(r"[ \t]+([,.;:!?])")
|
|
52
62
|
|
|
@@ -57,8 +67,16 @@ _RULE_OF_THREE = re.compile(
|
|
|
57
67
|
r"\b[A-Za-z]+,\s+[A-Za-z]+,\s+and\s+[A-Za-z]+\b", re.IGNORECASE | re.ASCII
|
|
58
68
|
)
|
|
59
69
|
_LIST_MARKER = re.compile(r"^\s*(?:[-*•·]|\d+[.)])\s+", re.MULTILINE | re.ASCII)
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
# U+2019 is included deliberately. Word processors and AI assistants emit the
|
|
71
|
+
# curly apostrophe, and matching only the straight one made this signal report
|
|
72
|
+
# "almost no contractions" on prose that was full of them, inflating the score
|
|
73
|
+
# by 10 points, while _WORD split "don’t" into two words.
|
|
74
|
+
_CONTRACTION = re.compile(r"\b[A-Za-z]+['’](?:t|s|re|ve|ll|d|m)\b", re.IGNORECASE | re.ASCII)
|
|
75
|
+
_WORD = re.compile(r"[A-Za-z'’]+")
|
|
76
|
+
# Markdown that survived a paste out of a chat window into running prose.
|
|
77
|
+
_MARKDOWN = re.compile(
|
|
78
|
+
r"\*\*[^*\n]+\*\*|__[^_\n]+__|^#{1,6}[ \t]|\[[^\]\n]+\]\([^)\n]+\)", re.MULTILINE
|
|
79
|
+
)
|
|
62
80
|
# Sentence split: same as the JS splitSentences(). ASCII whitespace only.
|
|
63
81
|
_SENTENCE_SPLIT = re.compile(r"(?<=[.!?])[ \t\n\r\f\v]+", re.ASCII)
|
|
64
82
|
# One shared whitespace set for trimming and word splitting, identical in both
|
|
@@ -85,14 +103,62 @@ def _split_ws(s):
|
|
|
85
103
|
return [w for w in re.split(r"[ \t\n\r\f\v]+", s) if w]
|
|
86
104
|
|
|
87
105
|
# Rule metadata for edits produced by the dash/space normalization passes.
|
|
88
|
-
|
|
106
|
+
_DASH_RULE_ID = "dash.spaced"
|
|
107
|
+
_DASH_COMMA_RULE = (_DASH_RULE_ID, "dash", "Replaced a dash with a comma")
|
|
108
|
+
_DASH_SPACE_RULE = (_DASH_RULE_ID, "dash", "Replaced a spaced dash with a space")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _dash_rule_for(rep):
|
|
112
|
+
return _DASH_COMMA_RULE if rep[:1] == "," else _DASH_SPACE_RULE
|
|
113
|
+
|
|
114
|
+
|
|
89
115
|
_COLLAPSE_RULE = ("space.collapse", "space", "Collapsed repeated spaces")
|
|
116
|
+
_CONFUSABLE_RULE = (_CONFUSABLE_RULE_ID, "homoglyph", "Replaced a " + _CONFUSABLE_LABEL)
|
|
117
|
+
_TYPOGRAPHY_RULE = (_TYPOGRAPHY_RULE_ID, "typography", "Normalized a " + _TYPOGRAPHY_LABEL)
|
|
90
118
|
_SPACE_BEFORE_RULE = ("punct.space-before", "space", "Removed a space before punctuation")
|
|
91
119
|
|
|
120
|
+
# What a matched dash run becomes. A clause dash reads as a pause, so the
|
|
121
|
+
# default replacement is a comma. The comma is withheld (single space instead,
|
|
122
|
+
# the old behavior, tidied by the collapse and space-before-punct passes) when
|
|
123
|
+
# a comma cannot sit there: at a text or line boundary, next to punctuation or
|
|
124
|
+
# a bracket it would double up against, next to a dash the match could not
|
|
125
|
+
# consume, or between digits, where the dash is a range rather than a pause.
|
|
126
|
+
# When the next character is whitespace the comma takes no trailing space, so
|
|
127
|
+
# it hugs the word before a line break. Mirrors dashReplacement in the JS
|
|
128
|
+
# engine.
|
|
129
|
+
_DASH_CP = frozenset((0x2014, 0x2015, 0x2013))
|
|
130
|
+
_NO_COMMA_BEFORE = frozenset(ord(c) for c in ",.;:!?([{")
|
|
131
|
+
_NO_COMMA_AFTER = frozenset(ord(c) for c in ",.;:!?)]}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _cp_before(s, i):
|
|
135
|
+
return ord(s[i - 1]) if i > 0 else -1
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _cp_after(s, i):
|
|
139
|
+
return ord(s[i]) if i < len(s) else -1
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _dash_replacement(before_cp, after_cp):
|
|
143
|
+
blocked_before = (before_cp < 0 or before_cp in _WS
|
|
144
|
+
or before_cp in _NO_COMMA_BEFORE or before_cp in _DASH_CP)
|
|
145
|
+
blocked_after = after_cp < 0 or after_cp in _NO_COMMA_AFTER or after_cp in _DASH_CP
|
|
146
|
+
if blocked_before or blocked_after:
|
|
147
|
+
return " "
|
|
148
|
+
if 0x30 <= before_cp <= 0x39 and 0x30 <= after_cp <= 0x39:
|
|
149
|
+
return " "
|
|
150
|
+
return "," if after_cp in _WS else ", "
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _dash_match_replacement(m):
|
|
154
|
+
return _dash_replacement(_cp_before(m.string, m.start()), _cp_after(m.string, m.end()))
|
|
155
|
+
|
|
92
156
|
# Priority when several passes touch the same characters and their cells merge.
|
|
93
157
|
# Higher wins the label. Dash beats space cleanup beats invisible.
|
|
94
158
|
_RULE_PRIORITY = {
|
|
159
|
+
"homoglyph.mixed-script": 45,
|
|
95
160
|
"dash.spaced": 40,
|
|
161
|
+
"typography.smart": 35,
|
|
96
162
|
"space.lookalike": 30,
|
|
97
163
|
"space.collapse": 20,
|
|
98
164
|
"punct.space-before": 15,
|
|
@@ -181,14 +247,22 @@ def _regex_pass(cells, regex, replacement, rule):
|
|
|
181
247
|
merged = cells[ci:cj]
|
|
182
248
|
src0 = merged[0].src0
|
|
183
249
|
src1 = merged[-1].src1
|
|
250
|
+
if callable(replacement):
|
|
251
|
+
rep = replacement(m)
|
|
252
|
+
elif "\\" in replacement:
|
|
253
|
+
rep = m.expand(replacement)
|
|
254
|
+
else:
|
|
255
|
+
rep = replacement
|
|
256
|
+
# `rule` may depend on the replacement chosen (the dash pass labels
|
|
257
|
+
# comma and space outcomes differently while keeping one ruleId).
|
|
258
|
+
base = rule(rep) if callable(rule) else rule
|
|
184
259
|
# if any merged cell already carried a higher-priority rule, keep it
|
|
185
|
-
best =
|
|
186
|
-
best_pri = _RULE_PRIORITY.get(
|
|
260
|
+
best = base
|
|
261
|
+
best_pri = _RULE_PRIORITY.get(base[0], 0)
|
|
187
262
|
for c in merged:
|
|
188
263
|
if c.rule and _RULE_PRIORITY.get(c.rule[0], 0) > best_pri:
|
|
189
264
|
best = c.rule
|
|
190
265
|
best_pri = _RULE_PRIORITY.get(c.rule[0], 0)
|
|
191
|
-
rep = m.expand(replacement) if "\\" in replacement else replacement
|
|
192
266
|
out.append(_Cell(rep, src0, src1, best))
|
|
193
267
|
last = cj
|
|
194
268
|
changed = True
|
|
@@ -208,6 +282,8 @@ def _build_cells(text, disabled=frozenset()):
|
|
|
208
282
|
cells.append(_Cell("", i, i + 1, (inv["id"], "invisible", "Removed " + inv["label"])))
|
|
209
283
|
elif _is_odd_space(cp) and _ODD_SPACE_RULE_ID not in disabled:
|
|
210
284
|
cells.append(_Cell(" ", i, i + 1, (_ODD_SPACE_RULE_ID, "space", "Normalized a " + _ODD_SPACE_LABEL)))
|
|
285
|
+
elif cp in _TYPOGRAPHY and _TYPOGRAPHY_RULE_ID not in disabled:
|
|
286
|
+
cells.append(_Cell(_TYPOGRAPHY[cp], i, i + 1, _TYPOGRAPHY_RULE))
|
|
211
287
|
else:
|
|
212
288
|
cells.append(_Cell(ch, i, i + 1, None))
|
|
213
289
|
return cells
|
|
@@ -239,25 +315,70 @@ def _cells_to_edits(cells, text):
|
|
|
239
315
|
return edits
|
|
240
316
|
|
|
241
317
|
|
|
318
|
+
def _is_latin_letter(t):
|
|
319
|
+
return len(t) == 1 and (65 <= ord(t) <= 90 or 97 <= ord(t) <= 122)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _is_confusable_text(t):
|
|
323
|
+
return len(t) == 1 and ord(t) in _CONFUSABLES
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _homoglyph_pass(cells, disabled=frozenset()):
|
|
327
|
+
"""A confusable is only a problem when it hides inside a word that is
|
|
328
|
+
otherwise Latin. Replacing them wholesale would destroy genuine Cyrillic or
|
|
329
|
+
Greek text, so a run is rewritten only when it MIXES scripts. Cells whose
|
|
330
|
+
text is empty (an invisible character already removed) are transparent, so
|
|
331
|
+
"a<ZWSP>pple" with a Cyrillic a is still seen as one word.
|
|
332
|
+
Mirrors homoglyphPass in the JS engine."""
|
|
333
|
+
if _CONFUSABLE_RULE_ID in disabled:
|
|
334
|
+
return cells
|
|
335
|
+
wordish = lambda c: _is_latin_letter(c.text) or _is_confusable_text(c.text)
|
|
336
|
+
i = 0
|
|
337
|
+
while i < len(cells):
|
|
338
|
+
if not wordish(cells[i]):
|
|
339
|
+
i += 1
|
|
340
|
+
continue
|
|
341
|
+
j = i
|
|
342
|
+
last_word = i
|
|
343
|
+
has_latin = False
|
|
344
|
+
has_confusable = False
|
|
345
|
+
while j < len(cells) and (wordish(cells[j]) or cells[j].text == ""):
|
|
346
|
+
if _is_latin_letter(cells[j].text):
|
|
347
|
+
has_latin = True
|
|
348
|
+
last_word = j
|
|
349
|
+
elif _is_confusable_text(cells[j].text):
|
|
350
|
+
has_confusable = True
|
|
351
|
+
last_word = j
|
|
352
|
+
j += 1
|
|
353
|
+
if has_latin and has_confusable:
|
|
354
|
+
for k in range(i, last_word + 1):
|
|
355
|
+
if not _is_confusable_text(cells[k].text):
|
|
356
|
+
continue
|
|
357
|
+
cells[k] = _Cell(_CONFUSABLES[ord(cells[k].text)],
|
|
358
|
+
cells[k].src0, cells[k].src1, _CONFUSABLE_RULE)
|
|
359
|
+
i = max(j, i + 1)
|
|
360
|
+
return cells
|
|
361
|
+
|
|
362
|
+
|
|
242
363
|
def _clean_cells(text, disabled=frozenset()):
|
|
243
364
|
"""Full clean pipeline over cells. Mirrors clean_text() in text_tools.py:
|
|
244
365
|
rebuild (invisible + odd space), then, only if a dash was present, the em
|
|
245
366
|
dash pass plus multi-space collapse and space-before-punct tidy. A disabled
|
|
246
367
|
rule id skips its pass entirely, so cleaned text and the edit list agree."""
|
|
247
|
-
cells = _build_cells(text, disabled)
|
|
368
|
+
cells = _homoglyph_pass(_build_cells(text, disabled), disabled)
|
|
248
369
|
current = "".join(c.text for c in cells)
|
|
249
370
|
# Run the dash/space passes to a FIXED POINT, not once. A single pass is
|
|
250
|
-
# not idempotent:
|
|
251
|
-
# the spaced-en-dash rule ("X—– Y" -> "X – Y", and only
|
|
252
|
-
#
|
|
253
|
-
# contract. Each enabled dash pass strictly
|
|
254
|
-
#
|
|
255
|
-
# Mirrors cleanCells() in the JS engine
|
|
371
|
+
# not idempotent: replacing an em dash can manufacture the spacing that
|
|
372
|
+
# arms the spaced-en-dash rule ("X—– Y" -> "X – Y", and only the next
|
|
373
|
+
# iteration reaches "X, Y"), which would break the documented
|
|
374
|
+
# clean(clean(x)) == clean(x) contract. Each enabled dash pass strictly
|
|
375
|
+
# reduces the dash count, so this terminates; the equality check breaks
|
|
376
|
+
# when the dash rule is disabled. Mirrors cleanCells() in the JS engine.
|
|
256
377
|
for _guard in range(8):
|
|
257
378
|
if not _EM_DASH.search(current):
|
|
258
379
|
break
|
|
259
|
-
if
|
|
260
|
-
cells, _ = _regex_pass(cells, _EM_DASH,
|
|
380
|
+
if _DASH_RULE_ID not in disabled:
|
|
381
|
+
cells, _ = _regex_pass(cells, _EM_DASH, _dash_match_replacement, _dash_rule_for)
|
|
261
382
|
if _COLLAPSE_RULE[0] not in disabled:
|
|
262
383
|
cells, _ = _regex_pass(cells, _MULTI_SPACE, " ", _COLLAPSE_RULE)
|
|
263
384
|
if _SPACE_BEFORE_RULE[0] not in disabled:
|
|
@@ -281,12 +402,12 @@ def rebuild_text(text):
|
|
|
281
402
|
|
|
282
403
|
|
|
283
404
|
def remove_em_dashes(text):
|
|
284
|
-
"""Replace
|
|
285
|
-
|
|
405
|
+
"""Replace clause dashes with a comma where one fits, else a space.
|
|
406
|
+
Returns (new_text, count); count is the number of matched dash groups."""
|
|
286
407
|
count = len(_EM_DASH.findall(text))
|
|
287
408
|
if not count:
|
|
288
409
|
return text, 0
|
|
289
|
-
new = _EM_DASH.sub(
|
|
410
|
+
new = _EM_DASH.sub(_dash_match_replacement, text)
|
|
290
411
|
new = _MULTI_SPACE.sub(" ", new)
|
|
291
412
|
new = _SPACE_BEFORE_PUNCT.sub(r"\1", new)
|
|
292
413
|
return new, count
|
|
@@ -404,6 +525,16 @@ def analyze_ai_signals(text):
|
|
|
404
525
|
"message": "Some rule-of-three phrasing"})
|
|
405
526
|
|
|
406
527
|
# 6. List structure.
|
|
528
|
+
markdown_hits = len(_MARKDOWN.findall(text))
|
|
529
|
+
if markdown_hits >= 2:
|
|
530
|
+
score += 10
|
|
531
|
+
signals.append({"id": "signal.markdown-artifacts", "points": 10,
|
|
532
|
+
"message": "Markdown left in the text (%d marks), a sign of a paste from a chat window" % markdown_hits})
|
|
533
|
+
elif markdown_hits == 1:
|
|
534
|
+
score += 5
|
|
535
|
+
signals.append({"id": "signal.markdown-artifacts", "points": 5,
|
|
536
|
+
"message": "A markdown mark left in the text"})
|
|
537
|
+
|
|
407
538
|
enum_hits = sum(len(re.findall(r"\b" + re.escape(w["text"]) + r"\b", lowered, re.ASCII))
|
|
408
539
|
for w in _ENUMERATORS)
|
|
409
540
|
marker_hits = len(_LIST_MARKER.findall(text))
|
|
@@ -500,6 +631,10 @@ def clean(text, options=None):
|
|
|
500
631
|
# Dashes count by consumed CHARACTER too: with the fixed-point dash pass, a
|
|
501
632
|
# chain like "—– " merges into one edit that removed two dashes.
|
|
502
633
|
dashes_n = _count_consumed(lambda cp: cp in (0x2014, 0x2015, 0x2013))
|
|
634
|
+
# Same measured-by-consumption rule: a confusable left inside a genuinely
|
|
635
|
+
# Cyrillic word is never consumed, so it is never counted.
|
|
636
|
+
homoglyphs_n = _count_consumed(lambda cp: cp in _CONFUSABLES)
|
|
637
|
+
typography_n = _count_consumed(lambda cp: cp in _TYPOGRAPHY)
|
|
503
638
|
|
|
504
639
|
return {
|
|
505
640
|
"engineVersion": ENGINE_VERSION,
|
|
@@ -514,6 +649,8 @@ def clean(text, options=None):
|
|
|
514
649
|
"invisible": invisible_n,
|
|
515
650
|
"oddSpaces": oddspace_n,
|
|
516
651
|
"dashes": dashes_n,
|
|
652
|
+
"homoglyphs": homoglyphs_n,
|
|
653
|
+
"typography": typography_n,
|
|
517
654
|
"hidden": invisible_n + oddspace_n,
|
|
518
655
|
"flagged": len(flags),
|
|
519
656
|
},
|