| """Rule-based syntactic rewrites using spaCy when available — structure only."""
|
|
|
| from __future__ import annotations
|
|
|
| import random
|
| import re
|
|
|
| from app.pipeline.nlp import get_nlp
|
| from app.pipeline.normalize import split_sentences_regex
|
| from app.pipeline.sentence_transform import transform_sentence
|
| from app.pipeline.tones import is_casual, is_elevated, normalize_tone
|
|
|
|
|
| def strip_hedges(sentence: str) -> str:
|
| """Hedges are stripped centrally via ai_phrases.txt in mechanics.scrub_phrases."""
|
| return sentence.strip()
|
|
|
|
|
| def maybe_split_long(sentence: str, strength: int, rng: random.Random) -> list[str]:
|
| """Split long sentences on clause-like joins — based on length, not fixed content."""
|
| words = sentence.split()
|
| min_len = {0: 22, 1: 16, 2: 12}.get(strength, 16)
|
| chance = {0: 0.45, 1: 0.7, 2: 0.9}.get(strength, 0.7)
|
| if len(words) < min_len or rng.random() > chance:
|
| return [sentence]
|
| if '"' in sentence or "'" in sentence or "“" in sentence or "”" in sentence:
|
| return [sentence]
|
|
|
| for sep in [
|
| "; ",
|
| ", and ",
|
| ", but ",
|
| ", so ",
|
| ", which ",
|
| ", although ",
|
| ", because ",
|
| ", while ",
|
| ", whereas ",
|
| " although ",
|
| " because ",
|
| " whereas ",
|
| ]:
|
| if sep not in sentence:
|
| continue
|
| left, right = sentence.split(sep, 1)
|
| left, right = left.strip(), right.strip()
|
|
|
| if len(left.split()) < 5 or len(right.split()) < 5:
|
| continue
|
| if right and right[0].islower():
|
| right = right[0].upper() + right[1:]
|
| if not left.endswith((".", "!", "?")):
|
| left += "."
|
| if not right.endswith((".", "!", "?")):
|
| right += "."
|
| return [left, right]
|
| return [sentence]
|
|
|
|
|
| def maybe_passive_to_active(sentence: str, strength: int, rng: random.Random) -> str:
|
| """Light-touch: 'X was VERBed by Y' → 'Y VERBed X' when parse looks safe."""
|
| if strength < 1 or rng.random() > 0.4 * strength:
|
| return sentence
|
| nlp = get_nlp()
|
| if nlp is None:
|
| return sentence
|
|
|
| doc = nlp(sentence)
|
| for token in doc:
|
| if token.dep_ == "nsubjpass":
|
| subj = token
|
| verb = token.head
|
| agent = None
|
| for child in verb.children:
|
| if child.dep_ == "agent":
|
| for g in child.children:
|
| if g.dep_ == "pobj":
|
| agent = g
|
| break
|
| if agent is None or verb.tag_ not in {"VBN"}:
|
| continue
|
| if len(doc) > 22:
|
| return sentence
|
| agent_span = agent.text
|
| for chunk in doc.noun_chunks:
|
| if agent in chunk:
|
| agent_span = chunk.text
|
| break
|
| subj_span = subj.text
|
| for chunk in doc.noun_chunks:
|
| if subj in chunk:
|
| subj_span = chunk.text
|
| break
|
| lemma = verb.lemma_
|
| irregular = {
|
| "make": "made",
|
| "write": "wrote",
|
| "take": "took",
|
| "give": "gave",
|
| "find": "found",
|
| "show": "showed",
|
| "do": "did",
|
| }
|
| if lemma in irregular:
|
| verb_out = irregular[lemma]
|
| elif lemma.endswith("e"):
|
| verb_out = lemma + "d"
|
| else:
|
| verb_out = lemma + "ed"
|
| rebuilt = f"{agent_span} {verb_out} {subj_span}."
|
| if rebuilt[0].islower():
|
| rebuilt = rebuilt[0].upper() + rebuilt[1:]
|
| return rebuilt
|
| return sentence
|
|
|
|
|
| def apply_tone_contractions(text: str, tone: str) -> str:
|
| tone_l = normalize_tone(tone)
|
| if is_elevated(tone_l):
|
| return text
|
| pairs = [
|
| (r"\bdo not\b", "don't"),
|
| (r"\bdoes not\b", "doesn't"),
|
| (r"\bdid not\b", "didn't"),
|
| (r"\bis not\b", "isn't"),
|
| (r"\bare not\b", "aren't"),
|
| (r"\bwas not\b", "wasn't"),
|
| (r"\bwere not\b", "weren't"),
|
| (r"\bwill not\b", "won't"),
|
| (r"\bcannot\b", "can't"),
|
| (r"\bcould not\b", "couldn't"),
|
| (r"\bwould not\b", "wouldn't"),
|
| (r"\bshould not\b", "shouldn't"),
|
| (r"\bit is\b", "it's"),
|
| (r"\bthat is\b", "that's"),
|
| (r"\bthere is\b", "there's"),
|
| (r"\bwe are\b", "we're"),
|
| (r"\bwe have\b", "we've"),
|
| (r"\byou are\b", "you're"),
|
| (r"\bthey are\b", "they're"),
|
| (r"\bI am\b", "I'm"),
|
| (r"\bI have\b", "I've"),
|
| (r"\blet us\b", "let's"),
|
| (r"\bwho is\b", "who's"),
|
| (r"\bwhat is\b", "what's"),
|
| ]
|
|
|
| use = pairs if is_casual(tone_l) else pairs[:12]
|
| out = text
|
| for pat, repl in use:
|
| out = re.sub(pat, repl, out)
|
| return out
|
|
|
|
|
| def rewrite_paragraph_structure(
|
| paragraph: str,
|
| strength: int,
|
| rng: random.Random,
|
| *,
|
| tone: str = "Neutral",
|
| ) -> str:
|
| """Structure pass: sentence transforms → passive→active → optional splits."""
|
| tone = normalize_tone(tone)
|
| nlp = get_nlp()
|
| if nlp is not None:
|
| doc = nlp(paragraph)
|
| sentences = [s.text.strip() for s in doc.sents if s.text.strip()]
|
| else:
|
| sentences = split_sentences_regex(paragraph)
|
|
|
| out_sents: list[str] = []
|
| for sent in sentences:
|
| s = strip_hedges(sent)
|
|
|
| s = transform_sentence(s, tone, strength, rng)
|
| s = maybe_passive_to_active(s, strength, rng)
|
| parts = maybe_split_long(s, strength, rng)
|
| out_sents.extend(parts)
|
|
|
| return " ".join(out_sents)
|
|
|