fetchit-engine 0.2.0__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.2.0/fetchit_engine.egg-info → fetchit_engine-0.3.0}/PKG-INFO +43 -43
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/fetchit_engine/core.py +87 -4
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/fetchit_engine/ruleset.json +609 -389
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0/fetchit_engine.egg-info}/PKG-INFO +43 -43
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/pyproject.toml +1 -1
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/setup.cfg +4 -4
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/LICENSE +0 -0
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/NOTICE +0 -0
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/README.md +0 -0
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/fetchit_engine/__init__.py +0 -0
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/fetchit_engine/spell_core.py +0 -0
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/fetchit_engine.egg-info/SOURCES.txt +0 -0
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/fetchit_engine.egg-info/dependency_links.txt +0 -0
- {fetchit_engine-0.2.0 → fetchit_engine-0.3.0}/fetchit_engine.egg-info/requires.txt +0 -0
- {fetchit_engine-0.2.0 → 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"]
|
|
@@ -59,8 +67,16 @@ _RULE_OF_THREE = re.compile(
|
|
|
59
67
|
r"\b[A-Za-z]+,\s+[A-Za-z]+,\s+and\s+[A-Za-z]+\b", re.IGNORECASE | re.ASCII
|
|
60
68
|
)
|
|
61
69
|
_LIST_MARKER = re.compile(r"^\s*(?:[-*•·]|\d+[.)])\s+", re.MULTILINE | re.ASCII)
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
)
|
|
64
80
|
# Sentence split: same as the JS splitSentences(). ASCII whitespace only.
|
|
65
81
|
_SENTENCE_SPLIT = re.compile(r"(?<=[.!?])[ \t\n\r\f\v]+", re.ASCII)
|
|
66
82
|
# One shared whitespace set for trimming and word splitting, identical in both
|
|
@@ -97,6 +113,8 @@ def _dash_rule_for(rep):
|
|
|
97
113
|
|
|
98
114
|
|
|
99
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)
|
|
100
118
|
_SPACE_BEFORE_RULE = ("punct.space-before", "space", "Removed a space before punctuation")
|
|
101
119
|
|
|
102
120
|
# What a matched dash run becomes. A clause dash reads as a pause, so the
|
|
@@ -138,7 +156,9 @@ def _dash_match_replacement(m):
|
|
|
138
156
|
# Priority when several passes touch the same characters and their cells merge.
|
|
139
157
|
# Higher wins the label. Dash beats space cleanup beats invisible.
|
|
140
158
|
_RULE_PRIORITY = {
|
|
159
|
+
"homoglyph.mixed-script": 45,
|
|
141
160
|
"dash.spaced": 40,
|
|
161
|
+
"typography.smart": 35,
|
|
142
162
|
"space.lookalike": 30,
|
|
143
163
|
"space.collapse": 20,
|
|
144
164
|
"punct.space-before": 15,
|
|
@@ -262,6 +282,8 @@ def _build_cells(text, disabled=frozenset()):
|
|
|
262
282
|
cells.append(_Cell("", i, i + 1, (inv["id"], "invisible", "Removed " + inv["label"])))
|
|
263
283
|
elif _is_odd_space(cp) and _ODD_SPACE_RULE_ID not in disabled:
|
|
264
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))
|
|
265
287
|
else:
|
|
266
288
|
cells.append(_Cell(ch, i, i + 1, None))
|
|
267
289
|
return cells
|
|
@@ -293,12 +315,57 @@ def _cells_to_edits(cells, text):
|
|
|
293
315
|
return edits
|
|
294
316
|
|
|
295
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
|
+
|
|
296
363
|
def _clean_cells(text, disabled=frozenset()):
|
|
297
364
|
"""Full clean pipeline over cells. Mirrors clean_text() in text_tools.py:
|
|
298
365
|
rebuild (invisible + odd space), then, only if a dash was present, the em
|
|
299
366
|
dash pass plus multi-space collapse and space-before-punct tidy. A disabled
|
|
300
367
|
rule id skips its pass entirely, so cleaned text and the edit list agree."""
|
|
301
|
-
cells = _build_cells(text, disabled)
|
|
368
|
+
cells = _homoglyph_pass(_build_cells(text, disabled), disabled)
|
|
302
369
|
current = "".join(c.text for c in cells)
|
|
303
370
|
# Run the dash/space passes to a FIXED POINT, not once. A single pass is
|
|
304
371
|
# not idempotent: replacing an em dash can manufacture the spacing that
|
|
@@ -458,6 +525,16 @@ def analyze_ai_signals(text):
|
|
|
458
525
|
"message": "Some rule-of-three phrasing"})
|
|
459
526
|
|
|
460
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
|
+
|
|
461
538
|
enum_hits = sum(len(re.findall(r"\b" + re.escape(w["text"]) + r"\b", lowered, re.ASCII))
|
|
462
539
|
for w in _ENUMERATORS)
|
|
463
540
|
marker_hits = len(_LIST_MARKER.findall(text))
|
|
@@ -554,6 +631,10 @@ def clean(text, options=None):
|
|
|
554
631
|
# Dashes count by consumed CHARACTER too: with the fixed-point dash pass, a
|
|
555
632
|
# chain like "—– " merges into one edit that removed two dashes.
|
|
556
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)
|
|
557
638
|
|
|
558
639
|
return {
|
|
559
640
|
"engineVersion": ENGINE_VERSION,
|
|
@@ -568,6 +649,8 @@ def clean(text, options=None):
|
|
|
568
649
|
"invisible": invisible_n,
|
|
569
650
|
"oddSpaces": oddspace_n,
|
|
570
651
|
"dashes": dashes_n,
|
|
652
|
+
"homoglyphs": homoglyphs_n,
|
|
653
|
+
"typography": typography_n,
|
|
571
654
|
"hidden": invisible_n + oddspace_n,
|
|
572
655
|
"flagged": len(flags),
|
|
573
656
|
},
|