Spaces:
Running
Running
Download restructure.py from Rasheed/Test_AI_writing: direct link, hf CLI and curl.
- Browser
- Download file 7.21 kB
-
https://huggingface.co/spaces/Rasheed/Test_AI_writing/resolve/main/restructure.py
- Command line
-
hf download hf://spaces/Rasheed/Test_AI_writing/restructure.py
-
curl -L -o restructure.py https://huggingface.co/spaces/Rasheed/Test_AI_writing/resolve/main/restructure.py
7.21 kB
| """ | |
| Rule-based sentence RESTRUCTURING (no AI model): changes the shape of a sentence, not just its words, so that | |
| Parrot's paraphrase of it ends up further from the original. Used for the sentences an uploaded report flagged. | |
| Transforms (each one keeps every word of content; the caller checks terms, numbers and citations with | |
| textparse.guard against the ORIGINAL sentence): | |
| * drop stock openers "Furthermore, X." -> "X." | |
| * fronted clause to the end "By combining A, the method B." -> "The method B by combining A." | |
| * end clause to the front "X improves Y, although Z." -> "Although Z, X improves Y." | |
| * split long sentences "A; B." / "A, whereas B." -> "A. In contrast, B." / "A, and the B" -> "A. The B" | |
| """ | |
| import re | |
| _OPENERS = re.compile( | |
| r"^(Furthermore|Moreover|Additionally|In addition|Also|Overall|Therefore|Thus|Hence|As a result|" | |
| r"Consequently|In conclusion|Finally|Importantly|Notably|In particular|Specifically|Accordingly),\s+", re.I) | |
| _FRONT = re.compile( | |
| r"^(?P<lead>(?:Although|While|Because|Since|When|If|As|By|Through|Using|With|For|To|After|Before|During|" | |
| r"Despite|Unlike|Based on|In order to|In response to|In this|In the|Within|Across|Among)\b[^,;]{6,140}),\s+" | |
| r"(?P<main>(?:the|this|these|those|it|its|they|their|we|our|such|a|an|each|both|all|many|most|[A-Z])[^;]*?)" | |
| r"(?P<end>[.!?])$") | |
| _BACK = re.compile( | |
| r"^(?P<main>[^,;]{12,}?),\s+(?P<conj>although|though|because|since|while|whereas|as|unless)\s+" | |
| r"(?P<clause>[^,;]{8,})(?P<end>[.!?])$", re.I) | |
| _SPLITS = [ | |
| (re.compile(r";\s+(?=[a-z])"), lambda m: ". ", None), | |
| (re.compile(r",\s+whereas\s+"), lambda m: ". In contrast, ", None), | |
| (re.compile(r",\s+while\s+(?=(?:the|this|these|it|they|other|another|[a-z]+s\b))"), lambda m: ". Meanwhile, ", None), | |
| (re.compile(r",\s+and\s+(?=(?:the|this|these|it|they|such|their|its)\b)"), lambda m: ". ", None), | |
| (re.compile(r",\s+which\s+(?=(?:is|are|was|were|has|have|can|could|may|might|will|would|helps|makes|allows|leads|" | |
| r"provides|enables|improves|reduces|results)\b)"), lambda m: ". This ", None), | |
| ] | |
| _OPEN_NOCOMMA = re.compile(r"^(Also|Overall|Therefore|Thus|Hence|Furthermore|Moreover|Additionally)\s+" | |
| r"(?=(?:the|this|these|it|they|we|such|our|its|their)\b)", re.I) | |
| _VERB_AFTER_SUBJ = re.compile(r"^((?:it|they|we)\b|(?:the|this|these|such|our|its|their)\b(?:\s+\S+){1,4}?)\s+" | |
| r"(is|are|was|were|can|could|may|might|will|would|has|have|had|" | |
| r"[a-z]+(?:es|s|ed))\b", re.I) | |
| _TAIL_PP = re.compile(r"\s(in|within|across|for|through|during|among|throughout|under)\s" | |
| r"(?P<np>(?:the\s|a\s|an\s|this\s|these\s|such\s|many\s|both\s|all\s)?[a-z][\w\-]*" | |
| r"(?:\s[\w\-]+){1,6})(?P<end>[.!?])$") | |
| _VERBISH = re.compile(r"\b(is|are|was|were|be|been|being|can|could|may|might|will|would|has|have|had|do|does|" | |
| r"did|which|that|who|whom|whose|when|where|while|and|or)\b") | |
| _KEEP_CAPS = re.compile(r"^(?:[A-Z]{2,}|[A-Z][a-z]*[A-Z]|I\b)") # acronyms / CamelCase / "I" stay capitalised | |
| def _lower_first(s): | |
| return s if _KEEP_CAPS.match(s) else s[:1].lower() + s[1:] | |
| def _upper_first(s): | |
| return s[:1].upper() + s[1:] | |
| def _drop_opener(s): | |
| m = _OPENERS.match(s) | |
| if m and len(s) - m.end() > 25: | |
| return _upper_first(s[m.end():]) | |
| return None | |
| def _drop_opener_nocomma(s): | |
| """'Also the thesis focuses on X.' -> 'The thesis also focuses on X.'""" | |
| m = _OPEN_NOCOMMA.match(s) | |
| if not m: | |
| return None | |
| word, rest = m.group(1).lower(), s[m.end():] | |
| v = _VERB_AFTER_SUBJ.match(rest) | |
| if v and word in ("also", "therefore", "thus", "hence"): | |
| verb = v.group(2) | |
| if verb.lower() in ("is", "are", "was", "were", "can", "could", "may", "might", "will", "would", | |
| "has", "have", "had"): # "It is also ...", "They can also ..." | |
| return _upper_first(f"{v.group(1)} {verb} {word}{rest[v.end(2):]}") | |
| return _upper_first(f"{v.group(1)} {word} {rest[v.start(2):]}") | |
| return _upper_first(rest) if len(rest.split()) >= 6 else None | |
| def _tail_to_front(s): | |
| """'X becomes Y in educational data mining.' -> 'In educational data mining, X becomes Y.'""" | |
| if "," in s[-80:]: | |
| return None | |
| m = _TAIL_PP.search(s) | |
| if not m or _VERBISH.search(m.group("np")): | |
| return None | |
| main = s[:m.start()] | |
| if len(main.split()) < 7 or re.search(r"\b(that|which|who|whom|whose|where|such that|so that)\b", main): | |
| return None # the phrase may belong to a sub-clause: moving it changes meaning | |
| return f"{_upper_first(m.group(1))} {m.group('np')}, {_lower_first(main)}{m.group('end')}" | |
| def _front_to_back(s): | |
| m = _FRONT.match(s) | |
| if not m: | |
| return None | |
| lead, main = m.group("lead"), m.group("main") | |
| if len(main.split()) < 5: | |
| return None | |
| return f"{_upper_first(main)} {_lower_first(lead)}{m.group('end')}" | |
| def _back_to_front(s): | |
| m = _BACK.match(s) | |
| if not m or len(m.group("main").split()) < 4: | |
| return None | |
| conj = m.group("conj").lower() | |
| if conj == "whereas": | |
| return None # handled by a split instead | |
| return f"{_upper_first(conj)} {m.group('clause')}, {_lower_first(m.group('main'))}{m.group('end')}" | |
| def _split(s, min_words=24): | |
| if len(s.split()) < min_words: | |
| return None | |
| for pat, rep, _ in _SPLITS: | |
| m = pat.search(s) | |
| if not m: | |
| continue | |
| left, right = s[:m.start()], s[m.end():] | |
| if len(left.split()) < 7 or len(right.split()) < 6: | |
| continue | |
| joiner = rep(m) | |
| right = _upper_first(right) if joiner.endswith(". ") else right | |
| left = left.rstrip(" ,;") | |
| return f"{left}{joiner}{right}" | |
| return None | |
| def variants(sentence): | |
| """Restructured versions of `sentence` (may be empty). Combinations are tried as well.""" | |
| s = sentence.strip() | |
| out = [] | |
| first = [f(s) for f in (_drop_opener, _drop_opener_nocomma, _front_to_back, _back_to_front, _split, | |
| _tail_to_front)] | |
| for v in first: | |
| if v and v != s and v not in out: | |
| out.append(v) | |
| for v in list(out): # one more step on top (e.g. opener dropped, then split) | |
| for f in (_front_to_back, _back_to_front, _split, _tail_to_front): | |
| w = f(v) | |
| if w and w != v and w != s and w not in out: | |
| out.append(w) | |
| return out | |
| def restructure(sentence, guard=None, overlap=None): | |
| """The restructured version that changes the sentence's shape the most and still passes `guard` | |
| (callable(original, new) -> str|None). Returns the original sentence when nothing applies.""" | |
| best, best_ov = sentence, 101.0 | |
| for v in variants(sentence): | |
| ok = guard(sentence, v) if guard else v | |
| if not ok: | |
| continue | |
| ov = overlap(sentence, ok) if overlap else 0.0 | |
| if ov < best_ov: | |
| best, best_ov = ok, ov | |
| return best | |