| """Sentence type classification — spaCy-driven when available."""
|
|
|
| from __future__ import annotations
|
|
|
| import re
|
| from functools import lru_cache
|
|
|
| from app.pipeline.nlp import get_nlp
|
|
|
| _REWRITEABLE = frozenset(
|
| {"simple_declarative", "compound", "because_clause"}
|
| )
|
| _CITATION = re.compile(
|
| r"(?:\[[0-9]+(?:\s*[-,]\s*[0-9]+)*\]"
|
| r"|\([A-Z][A-Za-z'-]+(?:\s+et\s+al\.)?,?\s+(?:19|20)\d{2}[a-z]?\)"
|
| r"|\bdoi:\s*10\.\d{4,9}/\S+)",
|
| re.I,
|
| )
|
|
|
|
|
| @lru_cache(maxsize=4096)
|
| def classify_sentence(text: str) -> str:
|
| """Tag sentence type for rewrite eligibility."""
|
| t = (text or "").strip()
|
| if not t:
|
| return "empty"
|
| if re.match(r"^#{1,6}\s", t) or (len(t.split()) <= 6 and t.isupper()):
|
| return "heading"
|
| if re.match(r"^(\d+[\.\)]\s+|[-*•]\s+)", t):
|
| return "list_item"
|
| if t.startswith(('"', "'", "\u201c", "\u2018")) and (
|
| t.count('"') >= 2 or t.count("\u201c") or t.count("'") >= 2
|
| ):
|
| return "quoted"
|
| if _CITATION.search(t):
|
| return "citation"
|
| if "?" in t or t.endswith("?"):
|
| return "question"
|
| if len(t.split()) < 3:
|
| return "too_short"
|
|
|
| if len(t.split()) > 70:
|
| return "too_long"
|
|
|
| nlp = get_nlp()
|
| if nlp is not None:
|
| return _classify_spacy(t, nlp)
|
|
|
| return _classify_regex(t)
|
|
|
|
|
| def _classify_spacy(text: str, nlp) -> str:
|
| doc = nlp(text)
|
|
|
| for t in doc:
|
| if t.lemma_.lower() == "because" and t.pos_ in {"SCONJ", "ADP"}:
|
| return "because_clause"
|
|
|
| for t in doc:
|
| if t.dep_ == "mark" and t.head.dep_ in {"advcl", "acl"}:
|
| if t.lemma_.lower() != "because":
|
| return "complex"
|
| if t.pos_ == "SCONJ" and t.lemma_.lower() != "because":
|
| if t.head.dep_ in {"advcl", "acl", "ROOT"} or t.dep_ == "mark":
|
| return "complex"
|
|
|
| if any(t.dep_ == "relcl" for t in doc) and len(text.split()) >= 10:
|
| return "complex"
|
|
|
| if "," in text and any(t.dep_ == "cc" and t.head.dep_ in {"conj", "ROOT"} for t in doc):
|
| if len(text.split()) <= 28:
|
| return "compound"
|
| return "complex"
|
| return "simple_declarative"
|
|
|
|
|
| def _classify_regex(text: str) -> str:
|
| low = text.lower()
|
| if re.search(r"\bbecause\b", low):
|
| return "because_clause"
|
| if re.search(r"\b(and|but|or|so|yet)\b", low) and "," in text:
|
| if re.search(
|
| r"\b(although|though|while|whilst|whereas|unless|until|since|if|when|"
|
| r"whenever|wherever|whether|before|after)\b",
|
| low,
|
| ):
|
| return "complex"
|
| return "compound" if len(text.split()) <= 28 else "complex"
|
| if re.search(
|
| r"\b(although|though|while|whilst|whereas|unless|until|since|if|when|"
|
| r"whenever|wherever|whether|before|after)\b",
|
| low,
|
| ):
|
| return "complex"
|
| if re.search(r"\b(who|whom|whose|which|that)\b", low) and len(text.split()) > 12:
|
| return "complex"
|
| return "simple_declarative"
|
|
|
|
|
| def is_rewriteable_type(sentence_type: str) -> bool:
|
| return sentence_type in _REWRITEABLE
|
|
|