"""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() # Avoid splitting tiny fragments 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"), ] # Casual: full contraction set. Neutral: core negatives only (subtler). 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) # Sentence-level paraphrase first (tone-aware) 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)