# app.py - Encapsulated version of the Colab notebook for deployment as a web app (e.g., on Hugging Face Spaces)
# This refactors the original 1501 lines into a modular structure: global model loading, a main processing function,
# and a Gradio interface. Based on the notebook's structure, which includes text preprocessing, sign tagging with
# Hugging Face models, span tagging, and recursive simplification. Full original functions are assumed to be
# integrated here (placeholders for complex ones like SSCCVsimplify). Adjust as needed if you have the exact defs.
'''
cd d:\TextSimplificationDemo
# Delete old venv (if needed)
Remove-Item -Recurse -Force .\venv
# Create venv with Python 3.11
py -3.11 -m venv venv
# Activate it
.\venv\Scripts\Activate.ps1
# Upgrade pip
python -m pip install --upgrade pip
# Install packages
pip install -r requirements.txt
# Install spacy-alignments safely
pip install spacy-alignments --only-binary :all:
# Download spaCy model
python -m spacy download en_core_web_sm
# Run the app
python app.py
# Interface at http://127.0.0.1:7860/
EXAMPLE SENTENCE:
John is one test, Mary, who lives in London, loves exams, and Bill eats bread.
John went to Wolverhampton, Bill set out for Coventry, and Mary returned from Birmingham.
The Environment Agency said record winter rainfall had helped to replenish reservoirs and groundwater, but reservoir storage has now fallen to 7.4% below the average for this time of year.
The Environment Agency said record winter rainfall had helped to replenish reservoirs and groundwater, but reservoir storage has now fallen to 7.4 below the average for this time of year.
Reservoir storage of 8.2 has been recorded and Mary went home.
The demo loops infinitely when more than 1 sentence is included on the line.
John, who lives in Wolverhampton, which is a town in the West Midlands, went home.
'''
import spaces
import re
import sys
import torch
from transformers import AutoModelForTokenClassification, AutoTokenizer
import spacy
import xml.etree.ElementTree as ET
import spacy_alignments as tokenizations
import xml.dom.minidom
import pandas as pd
import gradio as gr
import os
from collections import deque # add this at the top of app.py if not already there
seen = set()
import io
hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
device = "cuda" if torch.cuda.is_available() else "cpu"
print("Device:", device)
# Fix for Windows Unicode printing issues
if sys.platform == "win32":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
# Global configurations
test_on_unrestricted_text = True # Set to False for test sentences
# Load spaCy model
nlp = spacy.load("en_core_web_sm") # Assuming this is used; load once
# Load Hugging Face models and tokenizers (done globally to avoid reloading)
device = "cuda" if torch.cuda.is_available() else "cpu"
sign_tagging_model_name = "RJ3vans/SignTagger"
SignTaggingTokenizer = AutoTokenizer.from_pretrained(sign_tagging_model_name, token=hf_token)
SignTaggingModel = AutoModelForTokenClassification.from_pretrained(
sign_tagging_model_name, token=hf_token
).to(device)
SignTaggingModel.eval()
ssccv_span_tagging_model_name = "RJ3vans/SSCCVspanTagger"
SSCCVspanTaggingTokenizer = AutoTokenizer.from_pretrained(ssccv_span_tagging_model_name, token=hf_token)
SSCCVspanTaggingModel = AutoModelForTokenClassification.from_pretrained(
ssccv_span_tagging_model_name, token=hf_token
).to(device)
SSCCVspanTaggingModel.eval()
ccv_span_tagging_model_name = "RJ3vans/CCVspanTagger"
CCVspanTaggingTokenizer = AutoTokenizer.from_pretrained(ccv_span_tagging_model_name, token=hf_token)
CCVspanTaggingModel = AutoModelForTokenClassification.from_pretrained(
ccv_span_tagging_model_name, token=hf_token
).to(device)
CCVspanTaggingModel.eval()
cmv1_span_tagging_model_name = "RJ3vans/CMV1spanTagger"
CMV1spanTaggingTokenizer = AutoTokenizer.from_pretrained(cmv1_span_tagging_model_name, token=hf_token)
CMV1spanTaggingModel = AutoModelForTokenClassification.from_pretrained(
cmv1_span_tagging_model_name, token=hf_token
).to(device)
CMV1spanTaggingModel.eval()
cmn1_span_tagging_model_name = "RJ3vans/CMN1spanTagger"
CMN1spanTaggingTokenizer = AutoTokenizer.from_pretrained(cmn1_span_tagging_model_name, token=hf_token)
CMN1spanTaggingModel = AutoModelForTokenClassification.from_pretrained(
cmn1_span_tagging_model_name, token=hf_token
).to(device)
CMN1spanTaggingModel.eval()
# Define label lists (extracted from notebook summary; expand with full 70+ if needed)
sign_label_list = [
"M:N_CCV",
"M:N_CIN",
"M:N_CLA",
"M:N_CLAdv",
"M:N_CLN",
"M:N_CLP",
"M:N_CLQ",
"M:N_CLV",
"M:N_CMA1",
"M:N_CMAdv",
"M:N_CMN1",
"M:N_CMN2",
"M:N_CMN3",
"M:N_CMN4",
"M:N_CMP",
"M:N_CMP2",
"M:N_CMV1",
"M:N_CMV2",
"M:N_CMV3",
"M:N_COMBINATORY",
"M:N_CPA",
"M:N_ESAdvP",
"M:N_ESCCV",
"M:N_ESCM",
"M:N_ESMA",
"M:N_ESMAdvP",
"M:N_ESMI",
"M:N_ESMN",
"M:N_ESMP",
"M:N_ESMV",
"M:N_HELP",
"M:N_SPECIAL",
"M:N_SSCCV",
"M:N_SSCM",
"M:N_SSMA",
"M:N_SSMAdvP",
"M:N_SSMI",
"M:N_SSMN",
"M:N_SSMP",
"M:N_SSMV",
"M:N_STQ",
"M:N_V",
"M:N_nan",
"M:Y_CCV",
"M:Y_CIN",
"M:Y_CLA",
"M:Y_CLAdv",
"M:Y_CLN",
"M:Y_CLP",
"M:Y_CLQ",
"M:Y_CLV",
"M:Y_CMA1",
"M:Y_CMAdv",
"M:Y_CMN1",
"M:Y_CMN2",
"M:Y_CMN4",
"M:Y_CMP",
"M:Y_CMP2",
"M:Y_CMV1",
"M:Y_CMV2",
"M:Y_CMV3",
"M:Y_COMBINATORY",
"M:Y_CPA",
"M:Y_ESAdvP",
"M:Y_ESCCV",
"M:Y_ESCM",
"M:Y_ESMA",
"M:Y_ESMAdvP",
"M:Y_ESMI",
"M:Y_ESMN",
"M:Y_ESMP",
"M:Y_ESMV",
"M:Y_HELP",
"M:Y_SPECIAL",
"M:Y_SSCCV",
"M:Y_SSCM",
"M:Y_SSMA",
"M:Y_SSMAdvP",
"M:Y_SSMI",
"M:Y_SSMN",
"M:Y_SSMP",
"M:Y_SSMV",
"M:Y_STQ",
]
SSCCVspan_label_list = [
"AFTER_ADJECTIVAL",
"AFTER_ADVERBIAL",
"AFTER_CLEFT_CLAUSE",
"AFTER_COGNITIVE_COMMUNICATIVE_VP",
"AFTER_COMPLEX_NP",
"AFTER_COMPLEX_PHRASE",
"AFTER_FREE_RELATIVE_CLAUSE",
"AFTER_INTENSIFYING_CLAUSE",
"AFTER_REPORTING_CLAUSE",
"AFTER_RESTRICTIVE_CLAUSE_GENERIC_HEAD",
"AFTER_WH_PHRASE",
"BEFORE_ADJECTIVAL",
"BEFORE_ADVERBIAL",
"BEFORE_CLEFT_CLAUSE",
"BEFORE_COGNITIVE_COMMUNICATIVE_VP",
"BEFORE_COMPLEX_NP",
"BEFORE_COMPLEX_PHRASE",
"BEFORE_FREE_RELATIVE_CLAUSE",
"BEFORE_INTENSIFYING_CLAUSE",
"BEFORE_REPORTING_CLAUSE",
"BEFORE_RESTRICTIVE_CLAUSE_GENERIC_HEAD",
"BEFORE_WH_PHRASE",
"ERROR",
"IN_ADJECTIVAL",
"IN_ADJECTIVAL_AFTERSIGN",
"IN_ADJECTIVAL_BEFORESIGN",
"IN_ADVERBIAL",
"IN_ADVERBIAL_AFTERSIGN",
"IN_ADVERBIAL_BEFORESIGN",
"IN_CLEFT_CLAUSE",
"IN_CLEFT_CLAUSE_AFTERSIGN",
"IN_CLEFT_CLAUSE_BEFORESIGN",
"IN_COGNITIVE_COMMUNICATIVE_VP",
"IN_COGNITIVE_COMMUNICATIVE_VP_AFTERSIGN",
"IN_COGNITIVE_COMMUNICATIVE_VP_BEFORESIGN",
"IN_COMPLEX_NP",
"IN_COMPLEX_NP_AFTERSIGN",
"IN_COMPLEX_NP_BEFORESIGN",
"IN_COMPLEX_PHRASE",
"IN_COMPLEX_PHRASE_AFTERSIGN",
"IN_COMPLEX_PHRASE_BEFORESIGN",
"IN_FREE_RELATIVE_CLAUSE",
"IN_FREE_RELATIVE_CLAUSE_AFTERSIGN",
"IN_FREE_RELATIVE_CLAUSE_BEFORESIGN",
"IN_INTENSIFYING_CLAUSE",
"IN_INTENSIFYING_CLAUSE_AFTERSIGN",
"IN_INTENSIFYING_CLAUSE_BEFORESIGN",
"IN_REPORTING_CLAUSE_AFTERSIGN",
"IN_REPORTING_CLAUSE_BEFORESIGN",
"IN_RESTRICTIVE_CLAUSE_GENERIC_HEAD",
"IN_RESTRICTIVE_CLAUSE_GENERIC_HEAD_AFTERSIGN",
"IN_RESTRICTIVE_CLAUSE_GENERIC_HEAD_BEFORESIGN",
"IN_WH_PHRASE",
"IN_WH_PHRASE_AFTERSIGN",
"IN_WH_PHRASE_BEFORESIGN",
"UNKNOWN",
]
CCVspan_label_list = [
"AFTER_COMPOUND",
"BEFORE_COMPOUND",
"IN_COMPOUND",
"IN_COMPOUND_AFTERSIGN",
"IN_COMPOUND_BEFORESIGN",
"NOT_CLAUSE_COORDINATOR",
"UNKNOWN",
]
CMV1span_label_list = [
"AFTER_COMPOUND",
"BEFORE_COMPOUND",
"IN_COMPOUND",
"IN_COMPOUND_AFTERSIGN",
"IN_COMPOUND_BEFORESIGN",
"NOT_VP_COORDINATOR",
"UNKNOWN",
]
CMN1span_label_list = [
"AFTER_COMPOUND",
"BEFORE_COMPOUND",
"IN_COMPOUND",
"IN_COMPOUND_AFTERSIGN",
"IN_COMPOUND_BEFORESIGN",
"NOT_NP_COORDINATOR",
"UNKNOWN",
]
required_sign_tags = [
"M:Y_SSCCV",
"M:Y_CCV",
"M:Y_CMV1",
"M:Y_CMP",
"M:Y_CMN1",
"M:Y_CLN",
"M:Y_CLV",
"M:Y_CLP",
]
###############################################################################
# Helper functions (refactored from notebook)
###############################################################################
###############################################################################
# Third-person (and similar) pronouns that usually need an antecedent.
# Omit I/you/me — deictic, not anaphoric in the same way.
_ANAPHORIC_PRONOUNS = {
"he", "she", "it", "they",
"him", "her", "them",
# "we", "us", # optional; remove if you prefer
}
def sentence_has_anaphoric_pronoun(sent: str) -> bool:
tokens = re.findall(r"[A-Za-z']+", (sent or "").lower())
return any(t in _ANAPHORIC_PRONOUNS for t in tokens)
def order_sentences_for_discourse(sents):
"""
Print sentences with anaphoric pronouns last so pronouns
are less likely to precede their antecedents.
Stable within each group (original relative order kept).
"""
without = []
with_pron = []
for s in sents:
s = (s or "").strip()
if not s:
continue
if sentence_has_anaphoric_pronoun(s):
with_pron.append(s)
else:
without.append(s)
return without + with_pron
###############################################################################
###############################################################################
def split_into_sentences(text):
"""
Split plain text into sentences using spaCy.
Falls back to a conservative regex if spaCy returns a single
span that still contains '. ' + capital letter.
"""
text = (text or "").strip()
if not text:
return []
blocks = [b.strip() for b in text.split("\n") if b.strip()]
sents = []
for block in blocks:
doc = nlp(block)
piece = [s.text.strip() for s in doc.sents if s.text.strip()]
# Fallback: spaCy sometimes keeps two sentences as one span
expanded = []
for p in piece:
parts = re.split(r'(?<=[.!?])\s+(?=[A-Z])', p)
expanded.extend(x.strip() for x in parts if x.strip())
sents.extend(expanded)
# de-dupe while preserving order (can happen with odd whitespace)
seen = set()
unique = []
for s in sents:
key = re.sub(r"\s+", " ", s.lower())
if key not in seen:
seen.add(key)
unique.append(s)
return unique
###############################################################################
###############################################################################
def strip_xml_tags(text):
"""Remove all and tags."""
text = re.sub(r']+>', '', text)
text = re.sub(r']+>', '', text)
text = re.sub(r'\s+', ' ', text).strip()
return text
###############################################################################
###############################################################################
def clean_for_spaCy(sent):
print("CLEANING sent BEFORE spaCy processes it", sent)
# Single [...] region; no (.|\s)* — avoids ReDoS
# Supports: [,], [and], [who], [, who], [, and], [,_and] (legacy)
m = re.search(
r"^(.*?)"
r"\["
r"("
r"[,;:(]|"
r"and|but|or|that|what|when|where|which|while|who"
r")"
r"("
r"_(?:and|but|or|that|what|when|where|which|while|who)|"
r"\s+(?:and|but|or|that|what|when|where|which|while|who)"
r")?"
r"\]"
r"(.*)$",
sent,
flags=re.IGNORECASE | re.DOTALL,
)
if m:
prefix = m.group(1)
head = m.group(2)
extra = m.group(3) or ""
suffix = m.group(4)
if extra.startswith("_"):
# legacy bigram: ,_and → ,_and inside brackets
inside = head + extra
elif extra:
# [, who] / [, and]
inside = f"{head} {extra.strip()}"
else:
inside = head
clean_sent = f"{prefix}[{inside}]{suffix}"
else:
clean_sent = sent
clean_sent = re.sub(r"\s+", " ", clean_sent).strip()
print("clean_sent is\n" + clean_sent)
return clean_sent
###############################################################################
###############################################################################
# Tidying up predicted tags involving [ and ].
def sign_cleaning(
aligned_tokens, aligned_pos_tags, aligned_lemmas, aligned_predictions
):
clean_tokens = []
clean_pos_tags = []
clean_lemmas = []
clean_predictions = []
x = 0
while x < len(aligned_tokens):
# and
if aligned_tokens[x] == "[" and aligned_tokens[x + 1] == ":]":
clean_predictions.append(aligned_predictions[x])
clean_tokens.append(":")
clean_pos_tags.append(":")
clean_lemmas.append(":")
x += 1
elif aligned_tokens[x] == "[" and aligned_tokens[x + 2] == "]":
clean_predictions.append(aligned_predictions[x])
clean_tokens.append(aligned_tokens[x + 1])
clean_pos_tags.append(aligned_pos_tags[x + 1])
clean_lemmas.append(aligned_lemmas[x + 1])
x += 1
# comma-and
elif aligned_tokens[x] == "[" and aligned_tokens[x + 4] == "]":
clean_predictions.append(aligned_predictions[x])
clean_predictions.append(aligned_predictions[x])
clean_predictions.append(aligned_predictions[x])
clean_pos_tags.append(aligned_pos_tags[x + 1])
clean_pos_tags.append(aligned_pos_tags[x + 2])
clean_pos_tags.append(aligned_pos_tags[x + 3])
clean_lemmas.append(aligned_lemmas[x + 1])
clean_lemmas.append(aligned_lemmas[x + 2])
clean_lemmas.append(aligned_lemmas[x + 3])
clean_tokens.append(aligned_tokens[x + 1])
clean_tokens.append(aligned_tokens[x + 2])
clean_tokens.append(aligned_tokens[x + 3])
x += 3
elif aligned_tokens[x] == "]" and aligned_tokens[x - 2] == "[":
True
elif aligned_tokens[x] == "]" and aligned_tokens[x - 4] == "[":
True
else:
clean_tokens.append(aligned_tokens[x])
clean_pos_tags.append(aligned_pos_tags[x])
clean_lemmas.append(aligned_lemmas[x])
clean_predictions.append(aligned_predictions[x])
x += 1
new_clean_predictions = improve_predicted_tag_sequence(clean_predictions)
clean_predictions = new_clean_predictions
return (clean_tokens, clean_pos_tags, clean_lemmas, clean_predictions)
###############################################################################
###############################################################################
def improve_predicted_tag_sequence(predictions):
new_predictions = []
# print('IMPROVING PREDICTED TAG SEQUENCE')
# print(predictions)
core_tags = []
x = 0
sign_position = False
while x < len(predictions):
px = predictions[x]
core_tag = re.sub(
"(IN_|BEFORE_|AFTER_|_AFTERSIGN|_BEFORESIGN)",
"",
px,
)
core_tags.append(core_tag)
x += 1
most_frequent_core_tag = max(set(core_tags), key=core_tags.count)
sign_tag = re.compile(r"IN_" + most_frequent_core_tag + "$")
x = 0
while x < len(predictions):
px = predictions[x]
if re.match(
sign_tag,
px,
):
sign_position = x
break
x += 1
improved_predicted_tag_sequence = []
x = 0
while x < len(predictions):
px = predictions[x]
if x < sign_position:
if px == "UNKNOWN" or px == "NOT_CLAUSE_COORDINATOR":
px = "IN_" + core_tag + "_BEFORESIGN"
improved_predicted_tag_sequence.append(px)
x += 1
predictions = improved_predicted_tag_sequence
# print('improved_predicted_tag_sequence:', improved_predicted_tag_sequence)
x = 0
previous_prediction = ""
while x < len(predictions):
if x > 0:
if (
predictions[x] == "UNKNOWN"
or predictions[x] == "NOT_CLAUSE_COORDINATOR"
):
# new_predictions.append(previous_prediction)
try:
new_predictions.append(new_predictions[-1])
except:
new_predictions.append(predictions[x])
else:
new_predictions.append(predictions[x])
else:
new_predictions.append(predictions[x])
x += 1
# print('new_predictions:', new_predictions)
return new_predictions
###############################################################################
###############################################################################
def tag_SSCCV_span(sentence):
if re.search("\[\:\]\s*$", sentence):
sentence = sentence + " blah blah blah."
tokens = SSCCVspanTaggingTokenizer.tokenize(
SSCCVspanTaggingTokenizer.decode(SSCCVspanTaggingTokenizer.encode(sentence))
)
inputs = SSCCVspanTaggingTokenizer.encode(sentence, return_tensors="pt").to(device)
with torch.no_grad():
outputs = SSCCVspanTaggingModel(inputs)[0]
predictions = torch.argmax(outputs, dim=2)
SSCCV_span_tagged_tokens = [
(token, SSCCVspan_label_list[prediction])
for token, prediction in zip(tokens, predictions[0].tolist())
]
SSCCV_span_tagged_predictions = [p for t, p in SSCCV_span_tagged_tokens]
aligned_tokens = []
aligned_predictions = []
aligned_pos_tags = []
aligned_lemmas = []
sc_sentence = (
clean_for_spaCy(sentence) if "[" in sentence and "]" in sentence else sentence
)
doc = nlp(sc_sentence)
spacy_pos_tags = [t.tag_ for t in doc]
spacy_tokens = [t.text for t in doc]
spacy_lemmas = [t.lemma_ for t in doc]
print('Checking proper nouns after building doc')
for t in doc:
if 'NNP' in t.tag_:
print(t.tag_, t.text)
a2b, b2a = tokenizations.get_alignments(spacy_tokens, tokens)
for i in range(len(spacy_tokens)):
for j in a2b[i]:
aligned_tokens.append(spacy_tokens[i])
aligned_pos_tags.append(spacy_pos_tags[i])
aligned_lemmas.append(spacy_lemmas[i])
aligned_predictions.append(SSCCV_span_tagged_predictions[j])
break
clean_tokens, clean_pos_tags, clean_lemmas, clean_predictions = sign_cleaning(
aligned_tokens, aligned_pos_tags, aligned_lemmas, aligned_predictions
)
simplifiable = all("COMPLEX_NP" in p for p in clean_predictions)
if not simplifiable:
True # placeholder for future logic
return (
clean_tokens,
clean_pos_tags,
clean_lemmas,
clean_predictions,
) # <- added return
###############################################################################
###############################################################################
def tag_CCV_span(sentence):
if re.search("\[\:\]\s*$", sentence):
sentence = sentence + " blah blah blah."
tokens = CCVspanTaggingTokenizer.tokenize(
CCVspanTaggingTokenizer.decode(CCVspanTaggingTokenizer.encode(sentence))
)
inputs = CCVspanTaggingTokenizer.encode(sentence, return_tensors="pt").to(device)
with torch.no_grad():
outputs = CCVspanTaggingModel(inputs)[0]
predictions = torch.argmax(outputs, dim=2)
CCV_span_tagged_tokens = [
(token, CCVspan_label_list[prediction])
for token, prediction in zip(tokens, predictions[0].tolist())
]
CCV_span_tagged_predictions = [p for t, p in CCV_span_tagged_tokens]
aligned_tokens = []
aligned_predictions = []
aligned_pos_tags = []
aligned_lemmas = []
sc_sentence = (
clean_for_spaCy(sentence) if "[" in sentence and "]" in sentence else sentence
)
doc = nlp(sc_sentence)
spacy_pos_tags = [t.tag_ for t in doc]
spacy_tokens = [t.text for t in doc]
spacy_lemmas = [t.lemma_ for t in doc]
a2b, b2a = tokenizations.get_alignments(spacy_tokens, tokens)
for i in range(len(spacy_tokens)):
for j in a2b[i]:
aligned_tokens.append(spacy_tokens[i])
aligned_pos_tags.append(spacy_pos_tags[i])
aligned_lemmas.append(spacy_lemmas[i])
aligned_predictions.append(CCV_span_tagged_predictions[j])
break
clean_tokens, clean_pos_tags, clean_lemmas, clean_predictions = sign_cleaning(
aligned_tokens, aligned_pos_tags, aligned_lemmas, aligned_predictions
)
simplifiable = all("COMPOUND" in p for p in clean_predictions)
if not simplifiable:
True # placeholder for future logic
return (
clean_tokens,
clean_pos_tags,
clean_lemmas,
clean_predictions,
) # added return
###############################################################################
###############################################################################
def tag_CMV1_span(sentence):
if re.search("\[\:\]\s*$", sentence):
sentence = sentence + " blah blah blah."
tokens = CMV1spanTaggingTokenizer.tokenize(
CMV1spanTaggingTokenizer.decode(CMV1spanTaggingTokenizer.encode(sentence))
)
inputs = CMV1spanTaggingTokenizer.encode(sentence, return_tensors="pt").to(device)
with torch.no_grad():
outputs = CMV1spanTaggingModel(inputs)[0]
predictions = torch.argmax(outputs, dim=2)
CMV1_span_tagged_tokens = [
(token, CMV1span_label_list[prediction])
for token, prediction in zip(tokens, predictions[0].tolist())
]
CMV1_span_tagged_predictions = [p for t, p in CMV1_span_tagged_tokens]
aligned_tokens = []
aligned_predictions = []
aligned_pos_tags = []
aligned_lemmas = []
sc_sentence = (
clean_for_spaCy(sentence) if "[" in sentence and "]" in sentence else sentence
)
doc = nlp(sc_sentence)
spacy_pos_tags = [t.tag_ for t in doc]
spacy_tokens = [t.text for t in doc]
spacy_lemmas = [t.lemma_ for t in doc]
a2b, b2a = tokenizations.get_alignments(spacy_tokens, tokens)
for i in range(len(spacy_tokens)):
for j in a2b[i]:
aligned_tokens.append(spacy_tokens[i])
aligned_pos_tags.append(spacy_pos_tags[i])
aligned_lemmas.append(spacy_lemmas[i])
aligned_predictions.append(CMV1_span_tagged_predictions[j])
break
clean_tokens, clean_pos_tags, clean_lemmas, clean_predictions = sign_cleaning(
aligned_tokens, aligned_pos_tags, aligned_lemmas, aligned_predictions
)
simplifiable = all("COMPOUND" in p for p in clean_predictions)
if not simplifiable:
True # placeholder for future logic
return (
clean_tokens,
clean_pos_tags,
clean_lemmas,
clean_predictions,
) # added return
###############################################################################
###############################################################################
def tag_CMN1_span(sentence):
if re.search("\[\:\]\s*$", sentence):
sentence = sentence + " blah blah blah."
tokens = CMN1spanTaggingTokenizer.tokenize(
CMN1spanTaggingTokenizer.decode(CMN1spanTaggingTokenizer.encode(sentence))
)
inputs = CMN1spanTaggingTokenizer.encode(sentence, return_tensors="pt").to(device)
with torch.no_grad():
outputs = CMN1spanTaggingModel(inputs)[0]
predictions = torch.argmax(outputs, dim=2)
CMN1_span_tagged_tokens = [
(token, CMN1span_label_list[prediction])
for token, prediction in zip(tokens, predictions[0].tolist())
]
CMN1_span_tagged_predictions = [p for t, p in CMN1_span_tagged_tokens]
aligned_tokens = []
aligned_predictions = []
aligned_pos_tags = []
aligned_lemmas = []
sc_sentence = (
clean_for_spaCy(sentence) if "[" in sentence and "]" in sentence else sentence
)
doc = nlp(sc_sentence)
spacy_pos_tags = [t.tag_ for t in doc]
spacy_tokens = [t.text for t in doc]
spacy_lemmas = [t.lemma_ for t in doc]
a2b, b2a = tokenizations.get_alignments(spacy_tokens, tokens)
for i in range(len(spacy_tokens)):
for j in a2b[i]:
aligned_tokens.append(spacy_tokens[i])
aligned_pos_tags.append(spacy_pos_tags[i])
aligned_lemmas.append(spacy_lemmas[i])
aligned_predictions.append(CMN1_span_tagged_predictions[j])
break
clean_tokens, clean_pos_tags, clean_lemmas, clean_predictions = sign_cleaning(
aligned_tokens, aligned_pos_tags, aligned_lemmas, aligned_predictions
)
simplifiable = all("COMPOUND" in p for p in clean_predictions)
if not simplifiable:
True # placeholder for future logic
return (
clean_tokens,
clean_pos_tags,
clean_lemmas,
clean_predictions,
) # added return
###############################################################################
###############################################################################
def merge_wordpieces(sign_tagged_tokens):
"""
Merge BERT WordPiece tokens back into whole words.
Example: [('ma', 'M:N_CCV'), ('##ry', 'M:N_CCV')] → [('mary', 'M:N_CCV')]
"""
merged = []
for tok, lab in sign_tagged_tokens:
if tok in ('[CLS]', '[SEP]'):
continue
if tok.startswith('##') and merged:
prev_tok, prev_lab = merged[-1]
merged[-1] = (prev_tok + tok[2:], prev_lab)
else:
# also strip a lone '#' that sometimes appears after bad cleaning
if tok in ('#', '##'):
continue
merged.append((tok, lab))
return merged
###############################################################################
###############################################################################
def tag_all_signs_in_sent(sent):
"""Your original Colab function - returns a LIST of strings with tags inserted"""
sent = re.sub("do not", "don't", sent)
# Remove any existing square brackets around signs (your original line)
sent = re.sub(
r"\[((,|;|:|and|but|or|that|what|when|where|which|while|who)(\_(and|but|or|that|what|when|where|which|while|who))?)\]",
r"\1\2",
sent,
re.IGNORECASE,
)
tokens = SignTaggingTokenizer.tokenize(
SignTaggingTokenizer.decode(SignTaggingTokenizer.encode(sent))
)
inputs = SignTaggingTokenizer.encode(sent, return_tensors="pt").to(device)
with torch.no_grad():
outputs = SignTaggingModel(inputs)[0]
predictions = torch.argmax(outputs, dim=2)
sign_tagged_tokens = [
(token, sign_label_list[prediction])
for token, prediction in zip(tokens, predictions[0].tolist())
]
# NEW: merge sub-word pieces before any further processing
sign_tagged_tokens = merge_wordpieces(sign_tagged_tokens)
print("sign_tagged_tokens:", sign_tagged_tokens)
relevant_sign_offsets = []
relevant_tags = []
t = 0
while t < len(sign_tagged_tokens):
lab = sign_tagged_tokens[t][1]
if re.match("M:Y", lab):
relevant_sign_offsets.append(t)
relevant_tags.append(lab)
t += 1
tagged_sent_list = [tok for tok, _ in sign_tagged_tokens]
x = 0
while x < len(relevant_sign_offsets):
o = relevant_sign_offsets[x]
rtag = relevant_tags[x]
tagged_sent_list[o] = "<" + rtag + ">" + tagged_sent_list[o] + "" + rtag + ">"
x += 1
# Build the final list (excluding [CLS] and [SEP])
tagged_sent = []
x = 0
while x < len(tagged_sent_list):
if sign_tagged_tokens[x][0] not in ["[CLS]", "[SEP]"]:
tagged_sent.append(tagged_sent_list[x])
x += 1
print(f"Returning tagged_sent: result = {tagged_sent}")
return tagged_sent # <-- This returns a LIST, not a string
###############################################################################
###############################################################################
def split_coordinated_relative_body(body):
"""
Split 'Steven kicked and who Mary liked'
-> ['Steven kicked', 'Mary liked']
Also handles 'and which', bare 'and' between relative-like conjuncts.
"""
body = body.strip()
# Prefer explicit repeated relative marker
parts = re.split(
r"\s+and\s+(?:who|which|whom|that)\s+",
body,
flags=re.IGNORECASE,
)
if len(parts) >= 2:
return [p.strip() for p in parts if p.strip()]
return [body]
###############################################################################
###############################################################################
def _region_text(elem):
if elem is None:
return ""
parts = []
for w in elem.findall("W"):
if w.text and str(w.text).strip():
parts.append(str(w.text).strip())
return " ".join(parts).strip()
###############################################################################
###############################################################################
def _split_coord_relative_body(body):
"""
'Steven kicked and who Mary liked'
-> ['Steven kicked', 'Mary liked']
"""
body = re.sub(r"\s+", " ", (body or "")).strip()
if not body:
return []
parts = re.split(
r"\s+and\s+(?:who|which|whom|that)\s+",
body,
flags=re.IGNORECASE,
)
parts = [p.strip() for p in parts if p.strip()]
return parts if parts else [body]
###############################################################################
###############################################################################
def _tidy_plain(s):
s = re.sub(r"\s+", " ", (s or "")).strip()
s = re.sub(r"\s+([.,!?;:])", r"\1", s)
s = re.sub(r",+\s*\.", ".", s)
s = re.sub(r",+\s*$", "", s)
if s and not s.endswith((".", "!", "?")):
s += "."
if s:
s = s[0].upper() + s[1:]
return s
###############################################################################
# Placeholder for simplification functions (core of the 1501 lines; implement from original)
###############################################################################
def SSCCVsimplify(xml_sent):
simplification = []
xml_sent2 = xml_sent
xml_string = ET.tostring(xml_sent).decode("utf-8")
xml_for_printing = xml.dom.minidom.parseString(xml_string)
pretty_xml_as_string = xml_for_printing.toprettyxml()
# print(pretty_xml_as_string)
simp_sent1 = ET.Element("S")
simp_sent2 = ET.Element("S")
if xml_sent.attrib["TYPE"] == "COMPLEX_NP":
complex_NP_type = get_COMPLEX_NP_type(xml_sent)
print("SSCCV complex_NP_type:", complex_NP_type)
# print('SSCCV SIMPLIFYING:', complex_NP_type + '\nINPUT SENTENCE:\n'+ pretty_xml_as_string, file=sys.stderr)
#######################################################################
#######################################################################
#######################################################################
if complex_NP_type == "SUBJECT_RELATIVISED":
# Subject relative, e.g.:
# John, who lives in London, went home.
# John, who lives in London and who works in Paris, went home.
#
# Matrix: [BEFORE] + head + [AFTER]
# Relative: head + each conjunct (predication)
#
# Coordinated body "lives in London and who works in Paris"
# -> ["lives in London", "works in Paris"]
# -> "John lives in London." / "John works in Paris."
before_el = xml_sent.find("BEFORE_COMPLEX_CONSTITUENT")
head_el = xml_sent.find("IN_COMPLEX_CONSTITUENT_BEFORESIGN")
body_el = xml_sent.find("IN_COMPLEX_CONSTITUENT_AFTERSIGN")
after_el = xml_sent.find("AFTER_COMPLEX_CONSTITUENT")
head = re.sub(r"[,\s]+$", "", _region_text(head_el)).strip()
body = re.sub(r"^[,\s]+", "", _region_text(body_el)).strip()
after = re.sub(r"^[,\s]+", "", _region_text(after_el)).strip()
before = _region_text(before_el)
matrix = _tidy_plain(" ".join(x for x in [before, head, after] if x))
conjuncts = _split_coord_relative_body(body)
out = []
if matrix:
out.append(matrix)
for conj in conjuncts:
if conj and head:
out.append(_tidy_plain(f"{head} {conj}"))
return out
#######################################################################
#######################################################################
#######################################################################
# '''
# [John saw] [the bucket] [into] [which] [the coin had fallen][].
# [John saw] [the bucket][].
# [The coin had fallen] [into] [the bucket]
#
# '''
elif complex_NP_type == "PREPOSITION_OBJECT_RELATIVISED":
copula = ""
xml_prep = ET.Element("W")
for vor_sent_el in xml_sent:
if vor_sent_el.tag == "IN_COMPLEX_CONSTITUENT_BEFORESIGN":
for vor_sent_subel in vor_sent_el:
if vor_sent_subel.attrib["POS"] == "IN":
xml_prep = vor_sent_subel
vor_sent_matrix_subject = ET.Element("NA")
vor_sent_el_bcc = ET.Element("NA")
vor_sent_prep = ET.Element("NA")
vor_sent_acc = ET.Element("NA")
vor_sent2_prep = ET.Element("NA")
vor_sent2_matrix_subject = ET.Element("NA")
vor_sent2_clause = ET.Element("NA")
for vor_sent_el in xml_sent:
if vor_sent_el.tag == "IN_COMPLEX_CONSTITUENT":
try:
if re.match("(\,|\:|\;)$", vor_sent_el[-2].text):
punctuation_boundary = True
except:
True
# DELETING STRAY COMMA IMMEDIATELY PRECEDING THE SIGN DETECTED BY THE BERT MODEL
elif vor_sent_el.tag == "IN_COMPLEX_CONSTITUENT_BEFORESIGN":
if re.match("(\,|\:|\;)$", vor_sent_el[-1].text):
del vor_sent_el[-1]
# DELETING STRAY COMMA IMMEDIATELY FOLLOWING THE SIGN DETECTED BY THE BERT MODEL
elif vor_sent_el.tag == "AFTER_COMPLEX_CONSTITUENT":
if re.match("(\,|\:|\;)$", vor_sent_el[0].text):
del vor_sent_el[0]
for vor_sent_el in xml_sent:
if vor_sent_el.tag == "BEFORE_COMPLEX_CONSTITUENT":
vor_sent_el_bcc = vor_sent_el
elif vor_sent_el.tag == "IN_COMPLEX_CONSTITUENT_BEFORESIGN":
vor_sent_prep = xml_prep
vor_sent2_prep = xml_prep
vor_sent_matrix_subject = vor_sent_el
vor_sent_matrix_subject.remove(vor_sent_matrix_subject[-1])
elif vor_sent_el.tag == "AFTER_COMPLEX_CONSTITUENT":
if re.match("(\,|\:|\;)$", vor_sent_el[0].text):
del vor_sent_el[0]
vor_sent_acc = vor_sent_el
simp_sent1.append(vor_sent_el_bcc)
simp_sent1.append(vor_sent_matrix_subject)
simp_sent1.append(vor_sent_acc)
for vor_sent2_el in xml_sent2:
if vor_sent2_el.tag == "IN_COMPLEX_CONSTITUENT_BEFORESIGN":
try:
if vor_sent2_el[-1].attrib["POS"] == "IN":
vor_sent2_prep = vor_sent2_el[-1]
vor_sent2_matrix_subject = vor_sent2_el
except:
True
elif vor_sent2_el.tag == "IN_COMPLEX_CONSTITUENT_AFTERSIGN":
vor_sent2_clause = vor_sent2_el
simp_sent2.append(vor_sent2_clause)
simp_sent2.append(vor_sent2_prep)
simp_sent2.append(vor_sent2_matrix_subject)
#######################################################################
#######################################################################
#######################################################################
# '''
# [John saw] [the bucket] [which] [Peter despised][].
# [John saw] [the bucket][].
# [Peter despised] [the bucket].
#
# '''
elif complex_NP_type == "VERB_OBJECT_RELATIVISED":
# Object relative, e.g.:
# John, who Steven kicked, went home.
# John, who Steven kicked and who Mary liked, went home.
#
# Matrix: head + AFTER
# Relative(s): each conjunct + head as object
before_el = xml_sent.find("BEFORE_COMPLEX_CONSTITUENT")
head_el = xml_sent.find("IN_COMPLEX_CONSTITUENT_BEFORESIGN")
body_el = xml_sent.find("IN_COMPLEX_CONSTITUENT_AFTERSIGN")
after_el = xml_sent.find("AFTER_COMPLEX_CONSTITUENT")
head = re.sub(r"[,\s]+$", "", _region_text(head_el)).strip()
body = re.sub(r"^[,\s]+", "", _region_text(body_el)).strip()
after = re.sub(r"^[,\s]+", "", _region_text(after_el)).strip()
before = _region_text(before_el)
matrix = _tidy_plain(" ".join(x for x in [before, head, after] if x))
conjuncts = _split_coord_relative_body(body)
out = []
if matrix:
out.append(matrix)
for conj in conjuncts:
if conj and head:
out.append(_tidy_plain(f"{conj} {head}"))
return out # leave function here; do not use simp_sent1/simp_sent2
if len(simp_sent1) > 0 and len(simp_sent2) > 0:
s1_xml_string = ET.tostring(simp_sent1).decode("utf-8")
s1_xml_for_printing = xml.dom.minidom.parseString(s1_xml_string)
s1_pretty_xml_as_string = s1_xml_for_printing.toprettyxml()
s2_xml_string = ET.tostring(simp_sent2).decode("utf-8")
s2_xml_for_printing = xml.dom.minidom.parseString(s2_xml_string)
s2_pretty_xml_as_string = s2_xml_for_printing.toprettyxml()
"""
Be aware that simp_sent1 and simp_sent2 have a different structure
from xml_sent. This affects the xml_to_text function/process.
"""
try:
simplification.append(simplified_sent_xml_to_text(simp_sent1))
simplification.append(simplified_sent_xml_to_text(simp_sent2))
except Exception as e:
print("SSCCVsimplify xml_to_text error:", e)
print("simp_sent1:", ET.tostring(simp_sent1).decode("utf-8"))
print("simp_sent2:", ET.tostring(simp_sent2).decode("utf-8"))
# print('EXITING SSCCVsimplify:', simplification, file=sys.stderr)
return simplification
###############################################################################
###############################################################################
def CompoundSimplify(xml_sent):
"""
Reconstruct two sentences from the compound XML structure.
sent1 = BEFORE + BEFORESIGN + AFTER
sent2 = BEFORE + AFTERSIGN + AFTER
The coordinator (IN_COMPOUND_CONSTITUENT) is discarded.
Recovery: if AFTERSIGN is empty but IN_COMPOUND contains tokens
after the coordinator, treat those tokens as AFTERSIGN.
"""
try:
def get_text(elem):
if elem is None:
return ""
parts = []
for w in elem.findall("W"):
if w.text:
t = str(w.text).strip()
t = re.sub(r"[\[\]]+", "", t)
if t:
parts.append(t)
return " ".join(parts).strip()
def get_tokens(elem):
if elem is None:
return []
parts = []
for w in elem.findall("W"):
if w.text:
t = str(w.text).strip()
t = re.sub(r"[\[\]]+", "", t)
if t:
parts.append(t)
return parts
before_elem = xml_sent.find(".//BEFORE_COMPOUND_CONSTITUENT")
beforesign_elem = xml_sent.find(".//IN_COMPOUND_CONSTITUENT_BEFORESIGN")
sign_elem = xml_sent.find(".//IN_COMPOUND_CONSTITUENT")
aftersign_elem = xml_sent.find(".//IN_COMPOUND_CONSTITUENT_AFTERSIGN")
after_elem = xml_sent.find(".//AFTER_COMPOUND_CONSTITUENT")
before_text = get_text(before_elem)
beforesign_text = get_text(beforesign_elem)
aftersign_text = get_text(aftersign_elem)
after_text = get_text(after_elem)
# ----- recovery: second clause folded into IN_COMPOUND -----
if not aftersign_text and sign_elem is not None:
sign_toks = get_tokens(sign_elem)
coord = {",", ";", ":", "and", "but", "or", "nor"}
i = 0
while i < len(sign_toks) and sign_toks[i].lower() in coord:
i += 1
if i < len(sign_toks):
aftersign_text = " ".join(sign_toks[i:]).strip()
# -----------------------------------------------------------
sent1 = " ".join(
x for x in [before_text, beforesign_text, after_text] if x
).strip()
sent2 = " ".join(
x for x in [before_text, aftersign_text, after_text] if x
).strip()
def tidy(s):
s = re.sub(r"\s+", " ", s).strip()
s = re.sub(r"\s+([.,!?;:])", r"\1", s)
s = re.sub(r"([.,!?;:])\s+", r"\1 ", s)
# decimals: 8 . 2 → 8.2
s = re.sub(r"(\d)\s*\.\s*(\d)", r"\1.\2", s)
s = re.sub(r",+\s*\.", ".", s)
s = re.sub(r",+\s*$", "", s)
if s and not s.endswith((".", "!", "?")):
s += "."
if s:
s = s[0].upper() + s[1:]
return s
sent1 = tidy(sent1)
sent2 = tidy(sent2)
result = []
if sent1:
result.append(sent1)
if sent2 and sent2 != sent1:
result.append(sent2)
print(f"CompoundSimplify produced {len(result)} sentences: {result}")
return result
except Exception as e:
print(f"CompoundSimplify error: {e}")
return []
###############################################################################
###############################################################################
def create_xml_sent(tokens, pos_tags, lemmas, predictions, sign_tag):
xml_sent = ET.Element("SENT")
print(
"tokens:",
tokens,
"\npos_tags:",
pos_tags,
"\nlemmas:",
lemmas,
"\npredictions:",
predictions,
"\nsign_tag:",
sign_tag,
)
before_complex_constituent = None
in_complex_constituent_beforesign = None
in_complex_constituent = None
in_complex_constituent_aftersign = None
after_complex_constituent = None
if sign_tag in "M:Y_SSCCV":
before_complex_constituent = ET.SubElement(
xml_sent, "BEFORE_COMPLEX_CONSTITUENT"
)
in_complex_constituent_beforesign = ET.SubElement(
xml_sent, "IN_COMPLEX_CONSTITUENT_BEFORESIGN"
)
in_complex_constituent = ET.SubElement(xml_sent, "IN_COMPLEX_CONSTITUENT")
in_complex_constituent_aftersign = ET.SubElement(
xml_sent, "IN_COMPLEX_CONSTITUENT_AFTERSIGN"
)
after_complex_constituent = ET.SubElement(xml_sent, "AFTER_COMPLEX_CONSTITUENT")
elif sign_tag in ["M:Y_CCV", "M:Y_CMV1", "M:Y_CMN1"]:
before_complex_constituent = ET.SubElement(
xml_sent, "BEFORE_COMPOUND_CONSTITUENT"
)
in_complex_constituent_beforesign = ET.SubElement(
xml_sent, "IN_COMPOUND_CONSTITUENT_BEFORESIGN"
)
in_complex_constituent = ET.SubElement(xml_sent, "IN_COMPOUND_CONSTITUENT")
in_complex_constituent_aftersign = ET.SubElement(
xml_sent, "IN_COMPOUND_CONSTITUENT_AFTERSIGN"
)
after_complex_constituent = ET.SubElement(
xml_sent, "AFTER_COMPOUND_CONSTITUENT"
)
core_prediction = predictions[0]
core_prediction = re.sub("IN_", "", core_prediction)
core_prediction = re.sub("BEFORE_", "", core_prediction)
core_prediction = re.sub("AFTER_", "", core_prediction)
core_prediction = re.sub("_AFTERSIGN", "", core_prediction)
core_prediction = re.sub("_BEFORESIGN", "", core_prediction)
xml_sent.attrib["TYPE"] = core_prediction
x = 0
while x < len(predictions):
if "_BEFORESIGN" in predictions[x]:
word = ET.SubElement(in_complex_constituent_beforesign, "W")
word.text = tokens[x]
word.attrib["POS"] = pos_tags[x]
word.attrib["LEMMA"] = lemmas[x]
elif "_AFTERSIGN" in predictions[x]:
if re.match(
"(and|but|or|that|what|when|where|which|while|who)",
tokens[x],
re.IGNORECASE,
):
if x > 0 and tokens[x - 1] in [",", ":", ";"]:
word = ET.SubElement(in_complex_constituent, "W")
word.text = tokens[x]
word.attrib["POS"] = pos_tags[x]
word.attrib["LEMMA"] = lemmas[x]
else:
word = ET.SubElement(in_complex_constituent_aftersign, "W")
word.text = tokens[x]
word.attrib["POS"] = pos_tags[x]
word.attrib["LEMMA"] = lemmas[x]
else:
word = ET.SubElement(in_complex_constituent_aftersign, "W")
word.text = tokens[x]
word.attrib["POS"] = pos_tags[x]
word.attrib["LEMMA"] = lemmas[x]
elif "IN_" in predictions[x]:
word = ET.SubElement(in_complex_constituent, "W")
word.text = tokens[x]
word.attrib["POS"] = pos_tags[x]
word.attrib["LEMMA"] = lemmas[x]
elif "BEFORE_" in predictions[x]:
word = ET.SubElement(before_complex_constituent, "W")
word.text = tokens[x]
word.attrib["POS"] = pos_tags[x]
word.attrib["LEMMA"] = lemmas[x]
elif "AFTER_" in predictions[x]:
word = ET.SubElement(after_complex_constituent, "W")
word.text = tokens[x]
word.attrib["POS"] = pos_tags[x]
word.attrib["LEMMA"] = lemmas[x]
x += 1
# Print the XML (as you requested)
print("BUILT XML SENTENCE:\n" + ET.tostring(xml_sent).decode("utf-8"))
return xml_sent
#############################################################################
#############################################################################
def get_COMPLEX_NP_type(xml_sent):
complex_NP_type = "UNKNOWN"
final_icca_pos_tag = "UNKNOWN"
first_icca_pos_tag = "UNKNOWN"
final_iccb_pos_tag = "UNKNOWN"
punct = re.compile(r"^[\,\:\;]$")
for el in xml_sent:
if el.tag == "IN_COMPLEX_CONSTITUENT_AFTERSIGN":
try:
# first *content* token, not a leading comma
for w in el:
if w.text and not punct.match(w.text.strip()):
first_icca_pos_tag = w.attrib.get("POS", "UNKNOWN")
break
final_icca_pos_tag = el[-1].attrib["POS"]
except Exception:
pass
elif el.tag == "IN_COMPLEX_CONSTITUENT_BEFORESIGN":
try:
# last *content* token, not a trailing comma
for w in reversed(list(el)):
if w.text and not punct.match(w.text.strip()):
final_iccb_pos_tag = w.attrib.get("POS", "UNKNOWN")
break
except Exception:
pass
if final_iccb_pos_tag in ["IN"]:
complex_NP_type = "PREPOSITION_OBJECT_RELATIVISED"
elif first_icca_pos_tag in [
"NNP",
"NNPS",
"NN",
"DT",
"CD",
] and final_icca_pos_tag in ["VB", "VBD", "VBG", "VBN", "VBZ"]:
complex_NP_type = "VERB_OBJECT_RELATIVISED"
elif first_icca_pos_tag in ["VB", "VBD", "VBG", "VBN", "VBZ"]:
complex_NP_type = "SUBJECT_RELATIVISED"
return complex_NP_type
#############################################################################
#############################################################################
def simplified_sent_xml_to_text(xml):
parts = []
for el in xml:
if el.tag == "W":
if el.text:
parts.append(el.text.strip())
else:
for sub_el in el:
if sub_el.text:
parts.append(sub_el.text.strip())
# Join the tokens with single spaces
text = " ".join(parts).strip()
# Clean up any leftover multiple spaces
text = re.sub(r"\s+", " ", text)
# Ensure the sentence ends with a full stop
if text and not text.endswith((".", "!", "?")):
text += "."
# Capitalise only the first character, leave everything else unchanged
if text:
text = text[0].upper() + text[1:]
return text
#############################################################################
###############################################################################
def clean_bpe_text(text):
"""Remove BPE fragments, labels, brackets, and normalize whitespace."""
text = re.sub(r"##", "", text) # remove subword markers
text = re.sub(r"\s*\[[^\]]+\]\s*\(M:Y_[^\)]+\)", "", text) # remove labels
text = re.sub(r"\s*\[[^\]]+\]", "", text) # remove brackets
text = re.sub(r"\s*\(M:Y_[^\)]+\)", "", text)
text = re.sub(r"\s*\(M:N_[^\)]+\)", "", text)
text = re.sub(r"M Y _[^\s]+", "", text)
text = re.sub(r"\s+", " ", text).strip()
return text
###############################################################################
###############################################################################
def clean_output(text):
if not text:
return ""
# brackets and WordPiece fragments
text = re.sub(r'[\[\]]+', '', text)
text = re.sub(r'\s*##\s*', '', text)
text = re.sub(r'#', '', text)
text = re.sub(r'\s+', ' ', text).strip()
# normal spacing around punctuation
text = re.sub(r'\s+([.,!?;:])', r'\1', text)
text = re.sub(r'([.,!?;:])\s+', r'\1 ', text)
# --- fix the trailing ",." pattern ---
text = re.sub(r',+\s*\.', '.', text) # ",." or ", ." → "."
text = re.sub(r',+\s*$', '', text) # trailing commas at end of string
text = re.sub(r'\.+$', '.', text) # collapse multiple periods
text = text.strip()
if text:
text = text[0].upper() + text[1:]
if not text.endswith(('.', '!', '?')):
text += '.'
return text.strip()
###############################################################################
###############################################################################
def drop_bracket_tokens(tokens, pos, lemmas, preds):
"""Remove literal [ and ] that the tokenizer sometimes emits"""
kept_tokens, kept_pos, kept_lemmas, kept_preds = [], [], [], []
for t, p, l, pr in zip(tokens, pos, lemmas, preds):
if t in ('[', ']'):
continue
kept_tokens.append(t)
kept_pos.append(p)
kept_lemmas.append(l)
kept_preds.append(pr)
return kept_tokens, kept_pos, kept_lemmas, kept_preds
###############################################################################
###############################################################################
def build_single_sign_bracketed(tagged_list, mode="ccv"):
"""
mode = "ccv" → bracket only the rightmost CCV sign
mode = "cmv1" → bracket only the rightmost CMV1 sign
mode = "cmn1" → bracket only the rightmost CMN1 sign
mode = "ssccv" → bracket one relative SSCCV trigger:
- nested relatives (2+ opens before next ESCCV): innermost
(rightmost preferred trigger before that ESCCV)
- coordinated relatives (CCV between opens): leftmost
- otherwise: leftmost relative trigger
- fallback: leftmost SSCCV/ESCCV (e.g. bare "that")
"""
def surface_of(item):
if item.startswith("" in item:
start = item.find(">") + 1
end = item.rfind("<")
if end > start:
return item[start:end]
return item
ccv_indices = []
cmv1_indices = []
cmn1_indices = []
ssccv_indices = []
esccv_indices = []
for i, item in enumerate(tagged_list):
s = str(item)
if "" in s or "M:Y_CCV" in s:
ccv_indices.append(i)
if "" in s or "M:Y_CMV1" in s:
cmv1_indices.append(i)
if "" in s or "M:Y_CMN1" in s:
cmn1_indices.append(i)
if "M:Y_SSCCV" in s:
ssccv_indices.append(i)
if "M:Y_ESCCV" in s:
esccv_indices.append(i)
if i not in ssccv_indices:
ssccv_indices.append(i)
target = set()
if mode == "ccv" and ccv_indices:
target.add(ccv_indices[-1])
elif mode == "cmv1" and cmv1_indices:
target.add(cmv1_indices[-1])
elif mode == "cmn1" and cmn1_indices:
target.add(cmn1_indices[-1])
elif mode == "ssccv" and ssccv_indices:
relative_pronouns = {"who", "which", "whom", "whose"}
preferred = []
for i in ssccv_indices:
surface = surface_of(tagged_list[i]).strip().lower()
# Direct relative pronoun as the tagged sign
if surface in relative_pronouns:
preferred.append(i)
continue
# Punctuation that introduces a relative (e.g. "," before "who")
if surface in {",", ";", ":"} and i + 1 < len(tagged_list):
nxt = surface_of(tagged_list[i + 1]).strip().lower()
if nxt in relative_pronouns or nxt == "that":
preferred.append(i)
continue
if preferred:
nested = False
coordinated = False
if esccv_indices:
first_es = min(esccv_indices)
opens_before_close = [i for i in preferred if i < first_es]
if len(opens_before_close) >= 2:
first_open = opens_before_close[0]
last_open = opens_before_close[-1]
# CCV between relative openers → coordinated relatives, not nesting
ccv_between = any(
first_open < j < last_open for j in ccv_indices
)
if ccv_between:
coordinated = True
target.add(opens_before_close[0]) # leftmost / whole relative
else:
nested = True
target.add(opens_before_close[-1]) # innermost
if not nested and not coordinated:
target.add(preferred[0]) # single relative or siblings
else:
pure_ssccv = [
i for i, item in enumerate(tagged_list)
if "M:Y_SSCCV" in str(item)
]
target.add((pure_ssccv or ssccv_indices)[0])
parts = []
for i, item in enumerate(tagged_list):
if item.startswith(""):
start = item.find(">") + 1
end = item.rfind("<")
surface = item[start:end] if end > start else item
if i in target:
parts.append(f"[{surface}]")
else:
parts.append(surface)
else:
parts.append(item)
text = " ".join(parts)
# spacing
text = re.sub(r"\s+\[([,;:])", r"[\1", text)
text = re.sub(r"(?", input_text)
input_text = re.sub(r"mainly f or", "mainly for", input_text)
input_text = re.sub(r"sothat", "so that", input_text)
input_text = re.sub(r"in alabama", "in Alabama", input_text)
input_text = re.sub(
r"LEISURE DESIGN IS IMPORTANT WHY is",
"LEISURE DESIGN IS IMPORTANT\n\nWhy is",
input_text,
)
input_text = re.sub(r"WHEN", "When", input_text)
input_text = re.sub(r"WHAT'S", "What's", input_text)
input_text = re.sub(r"(\s+)an'", " and", input_text)
input_text = re.sub(r"An'", "And", input_text)
# =======================================================
raw_paragraphs = re.split(r"\n\s*\n", input_text)
output_paragraphs = []
for para in raw_paragraphs:
para = para.strip()
if not para:
continue
para = re.sub(r"\s*\n\s*", " ", para)
para = re.sub(r"\s+", " ", para).strip()
sents = split_into_sentences(para)
if not sents:
continue
print(f"Paragraph → {len(sents)} sentence(s):")
for i, s in enumerate(sents, 1):
print(f" [{i}] {s}")
para_simplified = []
for original_sent in sents:
working_set = deque([original_sent])
seen = set()
sentence_final = []
iters = 0
while working_set:
iters += 1
if iters > MAX_ITERS:
print(
f"ABORT: exceeded {MAX_ITERS} iterations; "
f"draining {len(working_set)} item(s) to final"
)
while working_set:
sentence_final.append(
clean_output(working_set.popleft())
)
break
current = working_set.popleft().strip()
if not current:
continue
current = clean_output(current)
if not current:
continue
key = re.sub(r"\s+", " ", current.lower())
if key in seen:
print(f"SKIP duplicate: {current[:80]}...")
sentence_final.append(current)
continue
seen.add(key)
print(
f"\n--- iter {iters} | queue={len(working_set)} | "
f"{current[:100]}..."
)
tagged_list = tag_all_signs_in_sent(current)
has_ssccv = any(
any(x in t for x in ["M:Y_SSCCV", "M:Y_ESCCV"])
for t in tagged_list
)
has_ccv = any(
"" in t or "M:Y_CCV" in t for t in tagged_list
)
has_cmv1 = any(
"" in t or "M:Y_CMV1" in t for t in tagged_list
)
has_cmn1 = any(
"" in t or "M:Y_CMN1" in t for t in tagged_list
)
if has_ssccv:
mode = "ssccv"
xml_tag = "M:Y_SSCCV"
simplifier = SSCCVsimplify
span_fn = tag_SSCCV_span
elif has_ccv:
mode = "ccv"
xml_tag = "M:Y_CCV"
simplifier = CompoundSimplify
span_fn = tag_CCV_span
elif has_cmv1:
mode = "cmv1"
xml_tag = "M:Y_CMV1"
simplifier = CompoundSimplify
span_fn = tag_CMV1_span
elif has_cmn1:
mode = "cmn1"
xml_tag = "M:Y_CMN1"
simplifier = CompoundSimplify
span_fn = tag_CMN1_span
else:
sentence_final.append(current)
continue
bracketed_version = build_single_sign_bracketed(
tagged_list, mode=mode
)
print(f"\nProcessing: {current}")
print(f"Bracketed version ({mode}): {bracketed_version}")
cleaned = clean_for_spaCy(bracketed_version)
tokens, pos, lemmas, preds = span_fn(cleaned)
tokens, pos, lemmas, preds = drop_bracket_tokens(
tokens, pos, lemmas, preds
)
cleaned_tokens, clean_pos, clean_lemmas, cleaned_preds = (
sign_cleaning(tokens, pos, lemmas, preds)
)
improved_preds = improve_predicted_tag_sequence(
cleaned_preds
)
xml_sent = create_xml_sent(
cleaned_tokens,
clean_pos,
clean_lemmas,
improved_preds,
xml_tag,
)
simps = simplifier(xml_sent)
if not simps:
print(
"Simplifier returned []; forcing final (no re-queue)"
)
sentence_final.append(current)
continue
queued_any = False
for s in simps:
plain_s = clean_output(s)
if not plain_s:
continue
plain_key = re.sub(r"\s+", " ", plain_s.lower())
if plain_key == key:
print(
f" No progress (same as input): "
f"{plain_s[:80]}..."
)
sentence_final.append(plain_s)
continue
if plain_key in seen:
print(
f" Skip already-seen result: "
f"{plain_s[:80]}..."
)
sentence_final.append(plain_s)
continue
working_set.append(plain_s)
queued_any = True
print(f" → Queued: {plain_s}")
if not queued_any:
sentence_final.append(current)
# ★ CHANGE 1: order this original sentence's finals
for s in order_sentences_for_discourse(sentence_final):
s = clean_output(s)
if s:
para_simplified.append(s)
if para_simplified:
# ★ CHANGE 2 (optional): order whole paragraph
para_simplified = order_sentences_for_discourse(para_simplified)
output_paragraphs.append(" ".join(para_simplified))
final_output = "\n\n".join(output_paragraphs)
print(f"\n=== FINAL OUTPUT ===\n{final_output}")
return final_output
except Exception as e:
import traceback
print(traceback.format_exc())
return f"ERROR: {str(e)}"
###########################################################################
# Main processing function (encapsulates the pipeline)
# ================== GRADIO INTERFACE ===================
print(" All three models loaded successfully!")
print("Creating Gradio interface...")
custom_css = """
/* Slightly larger base font */
.gradio-container, .gradio-container * {
font-size: 16px !important;
}
/* Input = original text → bright pink */
.input-text textarea {
font-size: 17px !important;
color: #ffd0d0 !important; /* DeepPink */
font-weight: 500 !important;
line-height: 1.45 !important;
}
/* Output = simplified text → bright green */
.output-text textarea {
font-size: 17px !important;
color: #d0ffd0 !important; /* bright green */
font-weight: 500 !important;
line-height: 1.45 !important;
}
/* Labels under the boxes */
.input-text label,
.output-text label {
font-size: 15px !important;
font-weight: 600 !important;
}
.input-text label {
color: #ffd0d0 !important;
}
.output-text label {
color: #d0ffd0 !important;
}
"""
with gr.Blocks(
title="Syntactic Sentence Simplifier",
css=custom_css,
) as demo:
gr.Markdown("# Syntactic Sentence Simplifier")
gr.Markdown(
"Paste or type your text below. Uses Hugging Face models + simplification logic."
)
input_text = gr.Textbox(
lines=10,
label="Original text",
placeholder="Paste your text here...",
elem_classes=["input-text"],
)
output_text = gr.Textbox(
lines=10,
label="Simplified text",
elem_classes=["output-text"],
)
btn = gr.Button("Process & Simplify", variant="primary")
btn.click(
fn=process_text,
inputs=input_text,
outputs=output_text,
)
print(" Gradio interface ready. Starting web server...")
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
css=custom_css, # your pink/green CSS string
)
'''
test = (
"Joshua told me that John, who lives in London, is going to Wolverhampton, Jack loves Norwich, and Mary, who is a bit nervous, adores Birmingham, just as Fred predicted."
)
print(process_text(test))
'''