any2human / app /engine /classify /__init__.py
idnameraj's picture
Upload 89 files
31bce5e verified
Raw
History Blame Contribute Delete
3.46 kB
"""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"
# Allow longer prose through paraphrase/lexical; only extreme length is skipped.
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)
# because as mark / SCONJ
for t in doc:
if t.lemma_.lower() == "because" and t.pos_ in {"SCONJ", "ADP"}:
return "because_clause"
# Subordinate clauses via mark / advcl / SCONJ
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"
# Relative clauses on longer sentences are unsafe to slot-rebuild
if any(t.dep_ == "relcl" for t in doc) and len(text.split()) >= 10:
return "complex"
# Coordinating compound with comma
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