RJ3vans commited on
Commit
9aa874b
·
verified ·
1 Parent(s): 7c3c078

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +525 -141
app.py CHANGED
@@ -47,7 +47,7 @@ The demo loops infinitely when more than 1 sentence is included on the line.
47
 
48
  John, who lives in Wolverhampton, which is a town in the West Midlands, went home.
49
  '''
50
- import spaces
51
  import re
52
  import sys
53
  import torch
@@ -58,7 +58,6 @@ import spacy_alignments as tokenizations
58
  import xml.dom.minidom
59
  import pandas as pd
60
  import gradio as gr
61
- import os
62
 
63
  from collections import deque # add this at the top of app.py if not already there
64
 
@@ -66,11 +65,6 @@ seen = set()
66
 
67
  import io
68
 
69
- hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
70
-
71
- device = "cuda" if torch.cuda.is_available() else "cpu"
72
- print("Device:", device)
73
-
74
  # Fix for Windows Unicode printing issues
75
  if sys.platform == "win32":
76
  sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
@@ -85,39 +79,46 @@ nlp = spacy.load("en_core_web_sm") # Assuming this is used; load once
85
  device = "cuda" if torch.cuda.is_available() else "cpu"
86
 
87
  sign_tagging_model_name = "RJ3vans/SignTagger"
88
- SignTaggingTokenizer = AutoTokenizer.from_pretrained(sign_tagging_model_name, token=hf_token)
89
  SignTaggingModel = AutoModelForTokenClassification.from_pretrained(
90
- sign_tagging_model_name, token=hf_token
91
  ).to(device)
92
- SignTaggingModel.eval()
93
 
94
  ssccv_span_tagging_model_name = "RJ3vans/SSCCVspanTagger"
95
- SSCCVspanTaggingTokenizer = AutoTokenizer.from_pretrained(ssccv_span_tagging_model_name, token=hf_token)
96
  SSCCVspanTaggingModel = AutoModelForTokenClassification.from_pretrained(
97
- ssccv_span_tagging_model_name, token=hf_token
 
 
 
 
 
 
98
  ).to(device)
99
- SSCCVspanTaggingModel.eval()
100
 
101
  ccv_span_tagging_model_name = "RJ3vans/CCVspanTagger"
102
- CCVspanTaggingTokenizer = AutoTokenizer.from_pretrained(ccv_span_tagging_model_name, token=hf_token)
103
  CCVspanTaggingModel = AutoModelForTokenClassification.from_pretrained(
104
- ccv_span_tagging_model_name, token=hf_token
105
  ).to(device)
106
- CCVspanTaggingModel.eval()
107
 
108
  cmv1_span_tagging_model_name = "RJ3vans/CMV1spanTagger"
109
- CMV1spanTaggingTokenizer = AutoTokenizer.from_pretrained(cmv1_span_tagging_model_name, token=hf_token)
110
  CMV1spanTaggingModel = AutoModelForTokenClassification.from_pretrained(
111
- cmv1_span_tagging_model_name, token=hf_token
112
  ).to(device)
113
- CMV1spanTaggingModel.eval()
114
 
115
  cmn1_span_tagging_model_name = "RJ3vans/CMN1spanTagger"
116
- CMN1spanTaggingTokenizer = AutoTokenizer.from_pretrained(cmn1_span_tagging_model_name, token=hf_token)
117
  CMN1spanTaggingModel = AutoModelForTokenClassification.from_pretrained(
118
- cmn1_span_tagging_model_name, token=hf_token
 
 
 
 
 
 
119
  ).to(device)
120
- CMN1spanTaggingModel.eval()
121
 
122
  # Define label lists (extracted from notebook summary; expand with full 70+ if needed)
123
  sign_label_list = [
@@ -265,6 +266,17 @@ SSCCVspan_label_list = [
265
  "UNKNOWN",
266
  ]
267
 
 
 
 
 
 
 
 
 
 
 
 
268
  CCVspan_label_list = [
269
  "AFTER_COMPOUND",
270
  "BEFORE_COMPOUND",
@@ -295,8 +307,19 @@ CMN1span_label_list = [
295
  "UNKNOWN",
296
  ]
297
 
 
 
 
 
 
 
 
 
 
 
298
  required_sign_tags = [
299
  "M:Y_SSCCV",
 
300
  "M:Y_CCV",
301
  "M:Y_CMV1",
302
  "M:Y_CMP",
@@ -592,6 +615,41 @@ def improve_predicted_tag_sequence(predictions):
592
 
593
  ###############################################################################
594
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
 
596
  ###############################################################################
597
  def tag_SSCCV_span(sentence):
@@ -601,9 +659,9 @@ def tag_SSCCV_span(sentence):
601
  tokens = SSCCVspanTaggingTokenizer.tokenize(
602
  SSCCVspanTaggingTokenizer.decode(SSCCVspanTaggingTokenizer.encode(sentence))
603
  )
604
- inputs = SSCCVspanTaggingTokenizer.encode(sentence, return_tensors="pt").to(device)
605
- with torch.no_grad():
606
- outputs = SSCCVspanTaggingModel(inputs)[0]
607
  predictions = torch.argmax(outputs, dim=2)
608
 
609
  SSCCV_span_tagged_tokens = [
@@ -660,6 +718,58 @@ def tag_SSCCV_span(sentence):
660
 
661
  ###############################################################################
662
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
663
 
664
  ###############################################################################
665
  def tag_CCV_span(sentence):
@@ -669,9 +779,9 @@ def tag_CCV_span(sentence):
669
  tokens = CCVspanTaggingTokenizer.tokenize(
670
  CCVspanTaggingTokenizer.decode(CCVspanTaggingTokenizer.encode(sentence))
671
  )
672
- inputs = CCVspanTaggingTokenizer.encode(sentence, return_tensors="pt").to(device)
673
- with torch.no_grad():
674
- outputs = CCVspanTaggingModel(inputs)[0]
675
  predictions = torch.argmax(outputs, dim=2)
676
 
677
  CCV_span_tagged_tokens = [
@@ -731,9 +841,9 @@ def tag_CMV1_span(sentence):
731
  tokens = CMV1spanTaggingTokenizer.tokenize(
732
  CMV1spanTaggingTokenizer.decode(CMV1spanTaggingTokenizer.encode(sentence))
733
  )
734
- inputs = CMV1spanTaggingTokenizer.encode(sentence, return_tensors="pt").to(device)
735
- with torch.no_grad():
736
- outputs = CMV1spanTaggingModel(inputs)[0]
737
  predictions = torch.argmax(outputs, dim=2)
738
 
739
  CMV1_span_tagged_tokens = [
@@ -793,9 +903,9 @@ def tag_CMN1_span(sentence):
793
  tokens = CMN1spanTaggingTokenizer.tokenize(
794
  CMN1spanTaggingTokenizer.decode(CMN1spanTaggingTokenizer.encode(sentence))
795
  )
796
- inputs = CMN1spanTaggingTokenizer.encode(sentence, return_tensors="pt").to(device)
797
- with torch.no_grad():
798
- outputs = CMN1spanTaggingModel(inputs)[0]
799
  predictions = torch.argmax(outputs, dim=2)
800
 
801
  CMN1_span_tagged_tokens = [
@@ -845,6 +955,68 @@ def tag_CMN1_span(sentence):
845
  ) # added return
846
 
847
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
  ###############################################################################
849
 
850
  ###############################################################################
@@ -884,9 +1056,9 @@ def tag_all_signs_in_sent(sent):
884
  tokens = SignTaggingTokenizer.tokenize(
885
  SignTaggingTokenizer.decode(SignTaggingTokenizer.encode(sent))
886
  )
887
- inputs = SignTaggingTokenizer.encode(sent, return_tensors="pt").to(device)
888
- with torch.no_grad():
889
- outputs = SignTaggingModel(inputs)[0]
890
  predictions = torch.argmax(outputs, dim=2)
891
 
892
  sign_tagged_tokens = [
@@ -1205,7 +1377,96 @@ def SSCCVsimplify(xml_sent):
1205
 
1206
  return simplification
1207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1209
  ###############################################################################
1210
 
1211
  ###############################################################################
@@ -1257,6 +1518,111 @@ def rewrite_residual_list_comma(sent, coordinator="and"):
1257
  return out
1258
  ###############################################################################
1259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1260
  ###############################################################################
1261
  def CompoundSimplify(xml_sent):
1262
  """
@@ -1322,25 +1688,27 @@ def CompoundSimplify(xml_sent):
1322
  x for x in [before_text, aftersign_text, after_text] if x
1323
  ).strip()
1324
 
 
1325
  def tidy(s):
 
 
 
1326
  s = re.sub(r"\s+", " ", s).strip()
1327
  s = re.sub(r"\s+([.,!?;:])", r"\1", s)
1328
  s = re.sub(r"([.,!?;:])\s+", r"\1 ", s)
1329
-
1330
  # decimals: 8 . 2 → 8.2
1331
  s = re.sub(r"(\d)\s*\.\s*(\d)", r"\1.\2", s)
1332
-
1333
- # list-coordinator debris: "43;." / "43,." / "43;;." → "43."
1334
  s = re.sub(r"[,;:]+\s*\.", ".", s)
1335
- # dangling separators at end before a period is added
1336
  s = re.sub(r"[,;:]+\s*$", "", s)
1337
-
1338
  if s and not s.endswith((".", "!", "?")):
1339
  s += "."
1340
  if s:
1341
  s = s[0].upper() + s[1:]
1342
  return s
1343
-
 
1344
 
1345
  sent1 = tidy(sent1)
1346
  sent2 = tidy(sent2)
@@ -1397,7 +1765,26 @@ def create_xml_sent(tokens, pos_tags, lemmas, predictions, sign_tag):
1397
  )
1398
  after_complex_constituent = ET.SubElement(xml_sent, "AFTER_COMPLEX_CONSTITUENT")
1399
 
1400
- elif sign_tag in ["M:Y_CCV", "M:Y_CMV1", "M:Y_CMN1"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1401
  before_complex_constituent = ET.SubElement(
1402
  xml_sent, "BEFORE_COMPOUND_CONSTITUENT"
1403
  )
@@ -1452,6 +1839,17 @@ def create_xml_sent(tokens, pos_tags, lemmas, predictions, sign_tag):
1452
  word.attrib["POS"] = pos_tags[x]
1453
  word.attrib["LEMMA"] = lemmas[x]
1454
 
 
 
 
 
 
 
 
 
 
 
 
1455
  elif "IN_" in predictions[x]:
1456
  word = ET.SubElement(in_complex_constituent, "W")
1457
  word.text = tokens[x]
@@ -1470,6 +1868,8 @@ def create_xml_sent(tokens, pos_tags, lemmas, predictions, sign_tag):
1470
  word.attrib["POS"] = pos_tags[x]
1471
  word.attrib["LEMMA"] = lemmas[x]
1472
 
 
 
1473
  x += 1
1474
 
1475
  # Print the XML (as you requested)
@@ -1489,6 +1889,8 @@ def get_COMPLEX_NP_type(xml_sent):
1489
  final_iccb_pos_tag = "UNKNOWN"
1490
 
1491
  punct = re.compile(r"^[\,\:\;]$")
 
 
1492
 
1493
  for el in xml_sent:
1494
  if el.tag == "IN_COMPLEX_CONSTITUENT_AFTERSIGN":
@@ -1498,7 +1900,11 @@ def get_COMPLEX_NP_type(xml_sent):
1498
  if w.text and not punct.match(w.text.strip()):
1499
  first_icca_pos_tag = w.attrib.get("POS", "UNKNOWN")
1500
  break
1501
- final_icca_pos_tag = el[-1].attrib["POS"]
 
 
 
 
1502
  except Exception:
1503
  pass
1504
 
@@ -1514,15 +1920,9 @@ def get_COMPLEX_NP_type(xml_sent):
1514
 
1515
  if final_iccb_pos_tag in ["IN"]:
1516
  complex_NP_type = "PREPOSITION_OBJECT_RELATIVISED"
1517
- elif first_icca_pos_tag in [
1518
- "NNP",
1519
- "NNPS",
1520
- "NN",
1521
- "DT",
1522
- "CD",
1523
- ] and final_icca_pos_tag in ["VB", "VBD", "VBG", "VBN", "VBZ"]:
1524
  complex_NP_type = "VERB_OBJECT_RELATIVISED"
1525
- elif first_icca_pos_tag in ["VB", "VBD", "VBG", "VBN", "VBZ"]:
1526
  complex_NP_type = "SUBJECT_RELATIVISED"
1527
 
1528
  return complex_NP_type
@@ -1633,12 +2033,14 @@ def build_single_sign_bracketed(tagged_list, mode="ccv"):
1633
  mode = "ccv" → bracket only the rightmost CCV sign
1634
  mode = "cmv1" → bracket only the rightmost CMV1 sign
1635
  mode = "cmn1" → bracket only the rightmost CMN1 sign
 
1636
  mode = "ssccv" → bracket one relative SSCCV trigger:
1637
  - nested relatives (2+ opens before next ESCCV): innermost
1638
  (rightmost preferred trigger before that ESCCV)
1639
  - coordinated relatives (CCV between opens): leftmost
1640
  - otherwise: leftmost relative trigger
1641
  - fallback: leftmost SSCCV/ESCCV (e.g. bare "that")
 
1642
  """
1643
 
1644
  def surface_of(item):
@@ -1652,8 +2054,10 @@ def build_single_sign_bracketed(tagged_list, mode="ccv"):
1652
  ccv_indices = []
1653
  cmv1_indices = []
1654
  cmn1_indices = []
 
1655
  ssccv_indices = []
1656
  esccv_indices = []
 
1657
 
1658
  for i, item in enumerate(tagged_list):
1659
  s = str(item)
@@ -1663,13 +2067,17 @@ def build_single_sign_bracketed(tagged_list, mode="ccv"):
1663
  cmv1_indices.append(i)
1664
  if "<M:Y_CMN1>" in s or "M:Y_CMN1" in s:
1665
  cmn1_indices.append(i)
 
 
1666
  if "M:Y_SSCCV" in s:
1667
  ssccv_indices.append(i)
1668
  if "M:Y_ESCCV" in s:
1669
  esccv_indices.append(i)
1670
  if i not in ssccv_indices:
1671
  ssccv_indices.append(i)
1672
-
 
 
1673
  target = set()
1674
 
1675
  if mode == "ccv" and ccv_indices:
@@ -1681,6 +2089,12 @@ def build_single_sign_bracketed(tagged_list, mode="ccv"):
1681
  elif mode == "cmn1" and cmn1_indices:
1682
  target.add(cmn1_indices[-1])
1683
 
 
 
 
 
 
 
1684
  elif mode == "ssccv" and ssccv_indices:
1685
  relative_pronouns = {"who", "which", "whom", "whose"}
1686
  preferred = []
@@ -1767,7 +2181,6 @@ def build_single_sign_bracketed(tagged_list, mode="ccv"):
1767
 
1768
 
1769
  ###############################################################################
1770
- @spaces.GPU(duration=120)
1771
  def process_text(input_text):
1772
  print("=== ITERATIVE SIMPLIFICATION (priority + clean tokens) ===")
1773
  try:
@@ -1858,6 +2271,10 @@ def process_text(input_text):
1858
  any(x in t for x in ["M:Y_SSCCV", "M:Y_ESCCV"])
1859
  for t in tagged_list
1860
  )
 
 
 
 
1861
  has_ccv = any(
1862
  "<M:Y_CCV>" in t or "M:Y_CCV" in t for t in tagged_list
1863
  )
@@ -1867,12 +2284,20 @@ def process_text(input_text):
1867
  has_cmn1 = any(
1868
  "<M:Y_CMN1>" in t or "M:Y_CMN1" in t for t in tagged_list
1869
  )
 
 
 
1870
 
1871
  if has_ssccv:
1872
  mode = "ssccv"
1873
  xml_tag = "M:Y_SSCCV"
1874
  simplifier = SSCCVsimplify
1875
  span_fn = tag_SSCCV_span
 
 
 
 
 
1876
  elif has_ccv:
1877
  mode = "ccv"
1878
  xml_tag = "M:Y_CCV"
@@ -1888,6 +2313,11 @@ def process_text(input_text):
1888
  xml_tag = "M:Y_CMN1"
1889
  simplifier = CompoundSimplify
1890
  span_fn = tag_CMN1_span
 
 
 
 
 
1891
  else:
1892
  sentence_final.append(current)
1893
  continue
@@ -1922,11 +2352,12 @@ def process_text(input_text):
1922
  )
1923
  simps = simplifier(xml_sent)
1924
 
1925
- # Residual comma → same conjunction (CCV / CMV1 / CMN1 only)
1926
  if (
1927
  simps
1928
  and simplifier is CompoundSimplify
1929
- and xml_tag in ("M:Y_CCV", "M:Y_CMV1", "M:Y_CMN1")
 
1930
  ):
1931
  coord = extract_coordinator_from_xml(xml_sent)
1932
  if coord:
@@ -1940,7 +2371,17 @@ def process_text(input_text):
1940
  )
1941
  rewritten.append(s2)
1942
  simps = rewritten
1943
-
 
 
 
 
 
 
 
 
 
 
1944
  if not simps:
1945
  print(
1946
  "Simplifier returned []; forcing final (no re-queue)"
@@ -1975,14 +2416,12 @@ def process_text(input_text):
1975
  if not queued_any:
1976
  sentence_final.append(current)
1977
 
1978
- # ★ CHANGE 1: order this original sentence's finals
1979
  for s in order_sentences_for_discourse(sentence_final):
1980
  s = clean_output(s)
1981
  if s:
1982
  para_simplified.append(s)
1983
 
1984
  if para_simplified:
1985
- # ★ CHANGE 2 (optional): order whole paragraph
1986
  para_simplified = order_sentences_for_discourse(para_simplified)
1987
  output_paragraphs.append(" ".join(para_simplified))
1988
 
@@ -2043,107 +2482,52 @@ custom_css = """
2043
  }
2044
  """
2045
 
2046
- with gr.Blocks(title="Syntactic Sentence Simplifier") as demo:
 
 
 
2047
  gr.Markdown("# Syntactic Sentence Simplifier")
2048
  gr.Markdown(
2049
- """
2050
- This demo simplifies sentences containing compound constituents:
2051
-
2052
- * *clauses*
2053
- * *verb phrases*
2054
- * *noun phrases*
2055
-
2056
- and complex noun phrases modified by:
2057
-
2058
- * *nominally bound non-restrictive finite relative clauses (subject relativised and verb/preposition object relativised)*
2059
-
2060
- Paste text, or click examples to **add** them into the box below
2061
- (you can edit the text afterwards).
2062
- """
2063
  )
2064
 
2065
-
2066
- # 1) Complex input
2067
  input_text = gr.Textbox(
2068
- lines=12,
2069
  label="Original text",
2070
- placeholder="Paste your text here, or add examples below...",
2071
  elem_classes=["input-text"],
2072
  )
2073
 
2074
- # 2) Bridge button (directly under input)
2075
- btn = gr.Button("Process & Simplify", variant="primary")
2076
-
2077
- # 3) Simple output
2078
  output_text = gr.Textbox(
2079
- lines=12,
2080
  label="Simplified text",
2081
  elem_classes=["output-text"],
2082
  )
2083
 
2084
- btn.click(fn=process_text, inputs=input_text, outputs=output_text)
2085
-
2086
- # Examples / clear (under the whole stack, or under input if you prefer)
2087
- EXAMPLE_SENTENCES = [
2088
- "John kicked the ball and walked to Leeds.",
2089
- "John bought two apples and three oranges.",
2090
- "John left early and Mary stayed late.",
2091
- "John, who lives in London, went home.",
2092
- "Peter, who Sam likes, went home.",
2093
- "Fred bought a computer and a printer and switched them on.",
2094
- "Fred bought a computer and a printer, and Mary bought a PS5.",
2095
- "Mary, who lives in London, went home, and Peter stayed.",
2096
- "John, who kicked the ball and walked away, went home.",
2097
- "John, who bought a computer and a printer, went home.",
2098
- "John, who Steven tolerated and who mary despised, went home.",
2099
- "John, who lives in Shifnal, which is a town in the Black Country, went home.",
2100
- "Mary, who lives in London, kicked the ball and walked to Leeds, and John stayed home.",
2101
- "Jackie, who lives in Manchester, bought a computer and a monitor, and Fred bought bread.",
2102
- "Count Binface bought two apples and three pears and ate them, and Nigel left.",
2103
- "Count Binface, who bought two apples and three pears and ate them, went home.",
2104
- "Hadric Wode, who bought a mace and a shield and used them, left early, and Fenric, who was tired, stayed home.",
2105
- "John is one candidate, Mary ignores signals, and Bill eats grass.",
2106
- "John went to Essex, Bill set out for Sussex, and Mary returned from Halifax.",
2107
- "Joshua told me that John, who lives in New York, is going to London, Jack loves Norwich, and Mary, who is extremely confident, adores Milton Keynes, just as Fred predicted.",
2108
- "John kicked the ball and walked to Leeds, and Peter stayed home.",
2109
- "John is one test, Mary, who lives in London, loves exams, and Bill eats bread.",
2110
- "John went to Wolverhampton, Bill set out for Coventry, and Mary returned from Birmingham.",
2111
- "John, who lives in London, is going to Wolverhampton, and Mary, who is a bit nervous, adores Birmingham.",
2112
- "John kicked the ball and walked to Leeds. Mary bought a laptop and a mousemat.",
2113
- "I love France.",
2114
- "John met the dog, the cat, and the tortoise.",
2115
- "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.",
2116
- ]
2117
 
2118
- def append_example(current_text, example):
2119
- example = (example or "").strip()
2120
- if not example:
2121
- return current_text or ""
2122
- current = (current_text or "").strip()
2123
- if not current:
2124
- return example
2125
- return current + "\n\n" + example
2126
-
2127
- gr.Markdown("### Add an example to the box above (click to append; then edit freely)")
2128
-
2129
- clear_btn = gr.Button("Clear input", size="sm")
2130
- clear_btn.click(fn=lambda: "", outputs=input_text)
2131
-
2132
- for ex in EXAMPLE_SENTENCES:
2133
- label = ex if len(ex) <= 100 else ex[:97] + "..."
2134
- ex_btn = gr.Button(label, size="sm")
2135
- ex_btn.click(
2136
- fn=lambda current, s=ex: append_example(current, s),
2137
- inputs=input_text,
2138
- outputs=input_text,
2139
- )
2140
 
2141
- print("Gradio interface ready. Starting web server...")
2142
 
2143
  if __name__ == "__main__":
2144
  demo.launch(
2145
  server_name="0.0.0.0",
2146
  server_port=7860,
2147
- css=custom_css, # if you use custom_css
 
 
 
 
 
 
 
 
2148
  )
 
 
2149
 
 
47
 
48
  John, who lives in Wolverhampton, which is a town in the West Midlands, went home.
49
  '''
50
+
51
  import re
52
  import sys
53
  import torch
 
58
  import xml.dom.minidom
59
  import pandas as pd
60
  import gradio as gr
 
61
 
62
  from collections import deque # add this at the top of app.py if not already there
63
 
 
65
 
66
  import io
67
 
 
 
 
 
 
68
  # Fix for Windows Unicode printing issues
69
  if sys.platform == "win32":
70
  sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
 
79
  device = "cuda" if torch.cuda.is_available() else "cpu"
80
 
81
  sign_tagging_model_name = "RJ3vans/SignTagger"
82
+ SignTaggingTokenizer = AutoTokenizer.from_pretrained(sign_tagging_model_name)
83
  SignTaggingModel = AutoModelForTokenClassification.from_pretrained(
84
+ sign_tagging_model_name
85
  ).to(device)
 
86
 
87
  ssccv_span_tagging_model_name = "RJ3vans/SSCCVspanTagger"
88
+ SSCCVspanTaggingTokenizer = AutoTokenizer.from_pretrained(ssccv_span_tagging_model_name)
89
  SSCCVspanTaggingModel = AutoModelForTokenClassification.from_pretrained(
90
+ ssccv_span_tagging_model_name
91
+ ).to(device)
92
+
93
+ ssmn_span_tagging_model_name = "RJ3vans/SSMNspanTagger"
94
+ SSMNspanTaggingTokenizer = AutoTokenizer.from_pretrained(ssmn_span_tagging_model_name)
95
+ SSMNspanTaggingModel = AutoModelForTokenClassification.from_pretrained(
96
+ ssmn_span_tagging_model_name
97
  ).to(device)
 
98
 
99
  ccv_span_tagging_model_name = "RJ3vans/CCVspanTagger"
100
+ CCVspanTaggingTokenizer = AutoTokenizer.from_pretrained(ccv_span_tagging_model_name)
101
  CCVspanTaggingModel = AutoModelForTokenClassification.from_pretrained(
102
+ ccv_span_tagging_model_name
103
  ).to(device)
 
104
 
105
  cmv1_span_tagging_model_name = "RJ3vans/CMV1spanTagger"
106
+ CMV1spanTaggingTokenizer = AutoTokenizer.from_pretrained(cmv1_span_tagging_model_name)
107
  CMV1spanTaggingModel = AutoModelForTokenClassification.from_pretrained(
108
+ cmv1_span_tagging_model_name
109
  ).to(device)
 
110
 
111
  cmn1_span_tagging_model_name = "RJ3vans/CMN1spanTagger"
112
+ CMN1spanTaggingTokenizer = AutoTokenizer.from_pretrained(cmn1_span_tagging_model_name)
113
  CMN1spanTaggingModel = AutoModelForTokenClassification.from_pretrained(
114
+ cmn1_span_tagging_model_name
115
+ ).to(device)
116
+
117
+ cln_span_tagging_model_name = "RJ3vans/CLNspanTagger"
118
+ CLNspanTaggingTokenizer = AutoTokenizer.from_pretrained(cmn1_span_tagging_model_name)
119
+ CLNspanTaggingModel = AutoModelForTokenClassification.from_pretrained(
120
+ cln_span_tagging_model_name
121
  ).to(device)
 
122
 
123
  # Define label lists (extracted from notebook summary; expand with full 70+ if needed)
124
  sign_label_list = [
 
266
  "UNKNOWN",
267
  ]
268
 
269
+ SSMNspan_label_list = [
270
+ "AFTER_COMPLEX_NP",
271
+ "BEFORE_COMPLEX_NP",
272
+ "ERROR",
273
+ "IN_COMPLEX_NP",
274
+ "IN_COMPLEX_NP_AFTERSIGN",
275
+ "IN_COMPLEX_NP_BEFORESIGN",
276
+ "UNKNOWN",
277
+ ]
278
+
279
+
280
  CCVspan_label_list = [
281
  "AFTER_COMPOUND",
282
  "BEFORE_COMPOUND",
 
307
  "UNKNOWN",
308
  ]
309
 
310
+ CLNspan_label_list = [
311
+ "AFTER_COMPOUND",
312
+ "BEFORE_COMPOUND",
313
+ "IN_COMPOUND",
314
+ "IN_COMPOUND_AFTERSIGN",
315
+ "IN_COMPOUND_BEFORESIGN",
316
+ "NOT_NOUN_COORDINATOR",
317
+ "UNKNOWN",
318
+ ]
319
+
320
  required_sign_tags = [
321
  "M:Y_SSCCV",
322
+ "M:Y_SSMN",
323
  "M:Y_CCV",
324
  "M:Y_CMV1",
325
  "M:Y_CMP",
 
615
 
616
  ###############################################################################
617
 
618
+ ###############################################################################
619
+ def indefinite_article_for(word: str) -> str:
620
+ """Choose a/an from the following orthographic word."""
621
+ if not word:
622
+ return "a"
623
+ w = re.sub(r"[^A-Za-z]", "", word).lower()
624
+ if not w:
625
+ return "a"
626
+ # simple orthographic rule (good enough for lecturer, hour, university, …)
627
+ if w[0] in "aeiou":
628
+ return "an"
629
+ return "a"
630
+ ###############################################################################
631
+
632
+ ###############################################################################
633
+ def make_indefinite_np(np_text: str) -> str:
634
+ """
635
+ the famous lecturer → a famous lecturer
636
+ an old friend → an old friend (unchanged)
637
+ famous lecturer → a famous lecturer (if bare, add article)
638
+ """
639
+ np_text = re.sub(r"\s+", " ", (np_text or "")).strip()
640
+ if not np_text:
641
+ return np_text
642
+
643
+ m = re.match(r"^(the|a|an)\s+(.*)$", np_text, flags=re.I)
644
+ if m:
645
+ rest = m.group(2).strip()
646
+ first = rest.split()[0] if rest else ""
647
+ return f"{indefinite_article_for(first)} {rest}".strip()
648
+
649
+ first = np_text.split()[0]
650
+ return f"{indefinite_article_for(first)} {np_text}".strip()
651
+ ###############################################################################
652
+
653
 
654
  ###############################################################################
655
  def tag_SSCCV_span(sentence):
 
659
  tokens = SSCCVspanTaggingTokenizer.tokenize(
660
  SSCCVspanTaggingTokenizer.decode(SSCCVspanTaggingTokenizer.encode(sentence))
661
  )
662
+ inputs = SSCCVspanTaggingTokenizer.encode(sentence, return_tensors="pt")
663
+
664
+ outputs = SSCCVspanTaggingModel(inputs)[0]
665
  predictions = torch.argmax(outputs, dim=2)
666
 
667
  SSCCV_span_tagged_tokens = [
 
718
 
719
  ###############################################################################
720
 
721
+ ###############################################################################
722
+ def tag_SSMN_span(sentence):
723
+ """
724
+ sentence must contain the left appositive boundary in brackets, e.g.:
725
+ John[,] the famous teacher, went home.
726
+ """
727
+ if re.search(r"\[\:\]\s*$", sentence):
728
+ sentence = sentence + " blah blah blah."
729
+
730
+ tokens = SSMNspanTaggingTokenizer.tokenize(
731
+ SSMNspanTaggingTokenizer.decode(SSMNspanTaggingTokenizer.encode(sentence))
732
+ )
733
+ inputs = SSMNspanTaggingTokenizer.encode(sentence, return_tensors="pt")
734
+ if device != "cpu":
735
+ inputs = inputs.to(device)
736
+
737
+ with torch.no_grad():
738
+ outputs = SSMNspanTaggingModel(inputs)[0]
739
+ predictions = torch.argmax(outputs, dim=2)
740
+
741
+ ssmn_span_tagged_predictions = [
742
+ SSMNspan_label_list[prediction]
743
+ for prediction in predictions[0].tolist()
744
+ ]
745
+
746
+ aligned_tokens, aligned_predictions = [], []
747
+ aligned_pos_tags, aligned_lemmas = [], []
748
+
749
+ sc_sentence = (
750
+ clean_for_spaCy(sentence) if "[" in sentence and "]" in sentence else sentence
751
+ )
752
+ doc = nlp(sc_sentence)
753
+ spacy_pos_tags = [t.tag_ for t in doc]
754
+ spacy_tokens = [t.text for t in doc]
755
+ spacy_lemmas = [t.lemma_ for t in doc]
756
+
757
+ a2b, b2a = tokenizations.get_alignments(spacy_tokens, tokens)
758
+
759
+ for i in range(len(spacy_tokens)):
760
+ for j in a2b[i]:
761
+ if j < len(ssmn_span_tagged_predictions):
762
+ aligned_tokens.append(spacy_tokens[i])
763
+ aligned_pos_tags.append(spacy_pos_tags[i])
764
+ aligned_lemmas.append(spacy_lemmas[i])
765
+ aligned_predictions.append(ssmn_span_tagged_predictions[j])
766
+ break
767
+
768
+ return sign_cleaning(
769
+ aligned_tokens, aligned_pos_tags, aligned_lemmas, aligned_predictions
770
+ )
771
+ ###############################################################################
772
+
773
 
774
  ###############################################################################
775
  def tag_CCV_span(sentence):
 
779
  tokens = CCVspanTaggingTokenizer.tokenize(
780
  CCVspanTaggingTokenizer.decode(CCVspanTaggingTokenizer.encode(sentence))
781
  )
782
+ inputs = CCVspanTaggingTokenizer.encode(sentence, return_tensors="pt")
783
+
784
+ outputs = CCVspanTaggingModel(inputs)[0]
785
  predictions = torch.argmax(outputs, dim=2)
786
 
787
  CCV_span_tagged_tokens = [
 
841
  tokens = CMV1spanTaggingTokenizer.tokenize(
842
  CMV1spanTaggingTokenizer.decode(CMV1spanTaggingTokenizer.encode(sentence))
843
  )
844
+ inputs = CMV1spanTaggingTokenizer.encode(sentence, return_tensors="pt")
845
+
846
+ outputs = CMV1spanTaggingModel(inputs)[0]
847
  predictions = torch.argmax(outputs, dim=2)
848
 
849
  CMV1_span_tagged_tokens = [
 
903
  tokens = CMN1spanTaggingTokenizer.tokenize(
904
  CMN1spanTaggingTokenizer.decode(CMN1spanTaggingTokenizer.encode(sentence))
905
  )
906
+ inputs = CMN1spanTaggingTokenizer.encode(sentence, return_tensors="pt")
907
+
908
+ outputs = CMN1spanTaggingModel(inputs)[0]
909
  predictions = torch.argmax(outputs, dim=2)
910
 
911
  CMN1_span_tagged_tokens = [
 
955
  ) # added return
956
 
957
 
958
+ ###############################################################################
959
+
960
+ ###############################################################################
961
+ def tag_CLN_span(sentence):
962
+ if re.search("\[\:\]\s*$", sentence):
963
+ sentence = sentence + " blah blah blah."
964
+
965
+ tokens = CLNspanTaggingTokenizer.tokenize(
966
+ CLNspanTaggingTokenizer.decode(CLNspanTaggingTokenizer.encode(sentence))
967
+ )
968
+ inputs = CLNspanTaggingTokenizer.encode(sentence, return_tensors="pt")
969
+
970
+ outputs = CLNspanTaggingModel(inputs)[0]
971
+ predictions = torch.argmax(outputs, dim=2)
972
+
973
+ CLN_span_tagged_tokens = [
974
+ (token, CLNspan_label_list[prediction])
975
+ for token, prediction in zip(tokens, predictions[0].tolist())
976
+ ]
977
+
978
+ CLN_span_tagged_predictions = [p for t, p in CLN_span_tagged_tokens]
979
+
980
+ aligned_tokens = []
981
+ aligned_predictions = []
982
+ aligned_pos_tags = []
983
+ aligned_lemmas = []
984
+
985
+ sc_sentence = (
986
+ clean_for_spaCy(sentence) if "[" in sentence and "]" in sentence else sentence
987
+ )
988
+ doc = nlp(sc_sentence)
989
+ spacy_pos_tags = [t.tag_ for t in doc]
990
+ spacy_tokens = [t.text for t in doc]
991
+ spacy_lemmas = [t.lemma_ for t in doc]
992
+
993
+ a2b, b2a = tokenizations.get_alignments(spacy_tokens, tokens)
994
+
995
+ for i in range(len(spacy_tokens)):
996
+ for j in a2b[i]:
997
+ aligned_tokens.append(spacy_tokens[i])
998
+ aligned_pos_tags.append(spacy_pos_tags[i])
999
+ aligned_lemmas.append(spacy_lemmas[i])
1000
+ aligned_predictions.append(CLN_span_tagged_predictions[j])
1001
+ break
1002
+
1003
+ clean_tokens, clean_pos_tags, clean_lemmas, clean_predictions = sign_cleaning(
1004
+ aligned_tokens, aligned_pos_tags, aligned_lemmas, aligned_predictions
1005
+ )
1006
+
1007
+ simplifiable = all("COMPOUND" in p for p in clean_predictions)
1008
+
1009
+ if not simplifiable:
1010
+ True # placeholder for future logic
1011
+
1012
+ return (
1013
+ clean_tokens,
1014
+ clean_pos_tags,
1015
+ clean_lemmas,
1016
+ clean_predictions,
1017
+ ) # added return
1018
+
1019
+
1020
  ###############################################################################
1021
 
1022
  ###############################################################################
 
1056
  tokens = SignTaggingTokenizer.tokenize(
1057
  SignTaggingTokenizer.decode(SignTaggingTokenizer.encode(sent))
1058
  )
1059
+ inputs = SignTaggingTokenizer.encode(sent, return_tensors="pt")
1060
+
1061
+ outputs = SignTaggingModel(inputs)[0]
1062
  predictions = torch.argmax(outputs, dim=2)
1063
 
1064
  sign_tagged_tokens = [
 
1377
 
1378
  return simplification
1379
 
1380
+ ###############################################################################
1381
+
1382
+ ###############################################################################
1383
+ def looks_like_proper_name_np(np_text: str) -> bool:
1384
+ toks = np_text.split()
1385
+ return bool(toks) and all(t[:1].isupper() for t in toks if t.isalpha())
1386
+ ###############################################################################
1387
+
1388
+ ###############################################################################
1389
+ def SSMNsimplify(xml_sent):
1390
+ """
1391
+ Appositive / non-finite nominal modifier (SSMN):
1392
+
1393
+ John Smith, the famous lecturer, went home.
1394
+ → John Smith went home.
1395
+ → John Smith is a famous lecturer.
1396
+ """
1397
+ simplification = []
1398
+
1399
+ try:
1400
+ if xml_sent.attrib.get("TYPE") not in ("COMPLEX_NP", "APPOSITIVE", "SSMN"):
1401
+ # still try if elements exist
1402
+ pass
1403
+
1404
+ def elem_text(elem):
1405
+ if elem is None:
1406
+ return ""
1407
+ parts = []
1408
+ for w in elem.findall("W"):
1409
+ if not w.text:
1410
+ continue
1411
+ t = str(w.text).strip()
1412
+ t = re.sub(r"[\[\]]+", "", t)
1413
+ if t:
1414
+ parts.append(t)
1415
+ return " ".join(parts).strip()
1416
+
1417
+ def strip_edge_commas(s: str) -> str:
1418
+ s = re.sub(r"\s+", " ", (s or "")).strip()
1419
+ s = re.sub(r"^[,;:\s]+", "", s)
1420
+ s = re.sub(r"[,;:\s]+$", "", s)
1421
+ return s.strip()
1422
+
1423
+ def tidy_sent(s: str) -> str:
1424
+ s = re.sub(r"\s+", " ", (s or "")).strip()
1425
+ s = re.sub(r"\s+([.,!?;:])", r"\1", s)
1426
+ s = re.sub(r"([.,!?;:])\s+", r"\1 ", s)
1427
+ s = re.sub(r",+\s*\.", ".", s)
1428
+ if s and not s.endswith((".", "!", "?")):
1429
+ s += "."
1430
+ if s:
1431
+ s = s[0].upper() + s[1:]
1432
+ return s
1433
+
1434
+ before = elem_text(xml_sent.find(".//BEFORE_COMPLEX_CONSTITUENT"))
1435
+ head = elem_text(xml_sent.find(".//IN_COMPLEX_CONSTITUENT_BEFORESIGN"))
1436
+ # trigger comma lives in IN_COMPLEX_CONSTITUENT — discarded
1437
+ appositive = elem_text(xml_sent.find(".//IN_COMPLEX_CONSTITUENT_AFTERSIGN"))
1438
+ after = elem_text(xml_sent.find(".//AFTER_COMPLEX_CONSTITUENT"))
1439
+
1440
+ head = strip_edge_commas(head)
1441
+ appositive = strip_edge_commas(appositive)
1442
+ before = strip_edge_commas(before)
1443
+ after = strip_edge_commas(after)
1444
+
1445
+ if not head:
1446
+ return []
1447
+
1448
+ # 1) Matrix clause without appositive
1449
+ matrix = " ".join(x for x in [before, head, after] if x).strip()
1450
+ matrix = tidy_sent(matrix)
1451
 
1452
+ # 2) Equative: Head is a/an …
1453
+ if looks_like_proper_name_np(appositive):
1454
+ pred_np = appositive
1455
+ else:
1456
+ pred_np = make_indefinite_np(appositive)
1457
+ equative = tidy_sent(f"{head} is {pred_np}")
1458
+
1459
+ if matrix:
1460
+ simplification.append(matrix)
1461
+ if equative and equative != matrix:
1462
+ simplification.append(equative)
1463
+
1464
+ print(f"SSMNsimplify produced {len(simplification)} sentences: {simplification}")
1465
+ return simplification
1466
+
1467
+ except Exception as e:
1468
+ print(f"SSMNsimplify error: {e}")
1469
+ return []
1470
  ###############################################################################
1471
 
1472
  ###############################################################################
 
1518
  return out
1519
  ###############################################################################
1520
 
1521
+ ###############################################################################
1522
+ def _singular_present_form(lemma: str) -> str:
1523
+ """Map verb lemma → 3rd person singular present."""
1524
+ lemma = (lemma or "").lower()
1525
+ irregular = {
1526
+ "be": "is",
1527
+ "have": "has",
1528
+ "do": "does",
1529
+ "go": "goes",
1530
+ "say": "says",
1531
+ }
1532
+ if lemma in irregular:
1533
+ return irregular[lemma]
1534
+ if lemma.endswith(("s", "sh", "ch", "x", "z", "o")):
1535
+ return lemma + "es"
1536
+ if len(lemma) > 1 and lemma.endswith("y") and lemma[-2] not in "aeiou":
1537
+ return lemma[:-1] + "ies"
1538
+ return lemma + "s"
1539
+
1540
+
1541
+ def subject_is_singular(subj_token) -> bool:
1542
+ """True if the nominal subject should take singular agreement."""
1543
+ # Coordinated subjects → plural
1544
+ if any(c.dep_ == "cc" for c in subj_token.subtree):
1545
+ return False
1546
+ # Explicit plural morph / tags
1547
+ num = subj_token.morph.get("Number")
1548
+ if num == ["Plur"]:
1549
+ return False
1550
+ if num == ["Sing"]:
1551
+ return True
1552
+ if subj_token.tag_ in ("NNS", "NNPS", "PRP") and subj_token.lower_ in {
1553
+ "we", "they", "you", "i"
1554
+ }:
1555
+ # I/you are special; treat as not 3sg
1556
+ return False
1557
+ if subj_token.tag_ in ("NNS", "NNPS"):
1558
+ return False
1559
+ if subj_token.lower_ in {"i", "you", "we", "they"}:
1560
+ return False
1561
+ # he/she/it / singular NN/NNP / this/that
1562
+ if subj_token.lower_ in {"he", "she", "it", "this", "that"}:
1563
+ return True
1564
+ if subj_token.tag_ in ("NN", "NNP", "DT", "PRP$"):
1565
+ return True
1566
+ # fallback: whole subject span text
1567
+ text = " ".join(t.text for t in subj_token.subtree).lower()
1568
+ if " and " in text:
1569
+ return False
1570
+ return True
1571
+
1572
+
1573
+ def fix_subject_verb_agreement(sent: str) -> str:
1574
+ """
1575
+ If the subject is singular and the finite verb is a plural present (VBP),
1576
+ rewrite the verb to 3rd-person singular (lives, is, has, …).
1577
+ Leaves past (VBD), progressives, etc. unchanged.
1578
+ """
1579
+ if not sent or not sent.strip():
1580
+ return sent
1581
+
1582
+ doc = nlp(sent)
1583
+ edits = [] # (token.i, new_text)
1584
+
1585
+ for tok in doc:
1586
+ if tok.dep_ != "ROOT" and tok.dep_ not in ("ccomp", "xcomp", "advcl"):
1587
+ # Prefer sentence ROOT finite verb
1588
+ pass
1589
+
1590
+ roots = [t for t in doc if t.dep_ == "ROOT"]
1591
+ if not roots:
1592
+ return sent
1593
+ verb = roots[0]
1594
+
1595
+ # Only repair untreated present plural / base used as present
1596
+ if verb.tag_ not in ("VBP", "VB"):
1597
+ return sent
1598
+ # Skip auxiliaries that are not the lexical main verb when better root exists
1599
+ # (spaCy ROOT is usually the main verb or the finite aux)
1600
+
1601
+ subjs = [c for c in verb.children if c.dep_ in ("nsubj", "nsubjpass")]
1602
+ if not subjs:
1603
+ return sent
1604
+ subj = subjs[0]
1605
+
1606
+ if not subject_is_singular(subj):
1607
+ return sent
1608
+
1609
+ lemma = verb.lemma_.lower()
1610
+ new_form = _singular_present_form(lemma)
1611
+
1612
+ # Preserve capitalisation
1613
+ if verb.text and verb.text[0].isupper():
1614
+ new_form = new_form[0].upper() + new_form[1:]
1615
+
1616
+ if new_form == verb.text:
1617
+ return sent
1618
+
1619
+ pieces = [t.text_with_ws for t in doc]
1620
+ # Replace only this token's text, keep its following whitespace
1621
+ ws = verb.whitespace_
1622
+ pieces[verb.i] = new_form + ws
1623
+ return "".join(pieces).strip()
1624
+ ###############################################################################
1625
+
1626
  ###############################################################################
1627
  def CompoundSimplify(xml_sent):
1628
  """
 
1688
  x for x in [before_text, aftersign_text, after_text] if x
1689
  ).strip()
1690
 
1691
+
1692
  def tidy(s):
1693
+ if not s:
1694
+ return ""
1695
+ s = str(s)
1696
  s = re.sub(r"\s+", " ", s).strip()
1697
  s = re.sub(r"\s+([.,!?;:])", r"\1", s)
1698
  s = re.sub(r"([.,!?;:])\s+", r"\1 ", s)
 
1699
  # decimals: 8 . 2 → 8.2
1700
  s = re.sub(r"(\d)\s*\.\s*(\d)", r"\1.\2", s)
1701
+ # list debris only at the end / before final period
 
1702
  s = re.sub(r"[,;:]+\s*\.", ".", s)
 
1703
  s = re.sub(r"[,;:]+\s*$", "", s)
1704
+ s = s.strip()
1705
  if s and not s.endswith((".", "!", "?")):
1706
  s += "."
1707
  if s:
1708
  s = s[0].upper() + s[1:]
1709
  return s
1710
+
1711
+
1712
 
1713
  sent1 = tidy(sent1)
1714
  sent2 = tidy(sent2)
 
1765
  )
1766
  after_complex_constituent = ET.SubElement(xml_sent, "AFTER_COMPLEX_CONSTITUENT")
1767
 
1768
+ elif sign_tag in ["M:Y_SSMN"]: # SSMA later if you want ages
1769
+ xml_sent.set("TYPE", "COMPLEX_NP") # or "APPOSITIVE"
1770
+ before_complex_constituent = ET.SubElement(
1771
+ xml_sent, "BEFORE_COMPLEX_CONSTITUENT"
1772
+ )
1773
+ in_complex_constituent_beforesign = ET.SubElement(
1774
+ xml_sent, "IN_COMPLEX_CONSTITUENT_BEFORESIGN"
1775
+ )
1776
+ in_complex_constituent = ET.SubElement(
1777
+ xml_sent, "IN_COMPLEX_CONSTITUENT"
1778
+ )
1779
+ in_complex_constituent_aftersign = ET.SubElement(
1780
+ xml_sent, "IN_COMPLEX_CONSTITUENT_AFTERSIGN"
1781
+ )
1782
+ after_complex_constituent = ET.SubElement(
1783
+ xml_sent, "AFTER_COMPLEX_CONSTITUENT"
1784
+ )
1785
+
1786
+ elif sign_tag in ["M:Y_CCV", "M:Y_CMV1", "M:Y_CMN1", "M:Y_CLN"]:
1787
+ xml_sent.set("TYPE", "COMPOUND")
1788
  before_complex_constituent = ET.SubElement(
1789
  xml_sent, "BEFORE_COMPOUND_CONSTITUENT"
1790
  )
 
1839
  word.attrib["POS"] = pos_tags[x]
1840
  word.attrib["LEMMA"] = lemmas[x]
1841
 
1842
+ elif (
1843
+ "NOT_NOUN_COORDINATOR" in predictions[x]
1844
+ or "NOT_NP_COORDINATOR" in predictions[x]
1845
+ or "NOT_CLAUSE_COORDINATOR" in predictions[x]
1846
+ or "NOT_VP_COORDINATOR" in predictions[x]
1847
+ ):
1848
+ word = ET.SubElement(in_complex_constituent_beforesign, "W")
1849
+ word.text = tokens[x]
1850
+ word.attrib["POS"] = pos_tags[x]
1851
+ word.attrib["LEMMA"] = lemmas[x]
1852
+
1853
  elif "IN_" in predictions[x]:
1854
  word = ET.SubElement(in_complex_constituent, "W")
1855
  word.text = tokens[x]
 
1868
  word.attrib["POS"] = pos_tags[x]
1869
  word.attrib["LEMMA"] = lemmas[x]
1870
 
1871
+
1872
+
1873
  x += 1
1874
 
1875
  # Print the XML (as you requested)
 
1889
  final_iccb_pos_tag = "UNKNOWN"
1890
 
1891
  punct = re.compile(r"^[\,\:\;]$")
1892
+ VERB_TAGS = ["VB", "VBD", "VBG", "VBN", "VBZ", "VBP", "MD"]
1893
+ NOMINAL_TAGS = ["NNP", "NNPS", "NN", "NNS", "DT", "CD", "PRP", "PRP$"]
1894
 
1895
  for el in xml_sent:
1896
  if el.tag == "IN_COMPLEX_CONSTITUENT_AFTERSIGN":
 
1900
  if w.text and not punct.match(w.text.strip()):
1901
  first_icca_pos_tag = w.attrib.get("POS", "UNKNOWN")
1902
  break
1903
+ # last *content* token, not a trailing comma
1904
+ for w in reversed(list(el)):
1905
+ if w.text and not punct.match(w.text.strip()):
1906
+ final_icca_pos_tag = w.attrib.get("POS", "UNKNOWN")
1907
+ break
1908
  except Exception:
1909
  pass
1910
 
 
1920
 
1921
  if final_iccb_pos_tag in ["IN"]:
1922
  complex_NP_type = "PREPOSITION_OBJECT_RELATIVISED"
1923
+ elif first_icca_pos_tag in NOMINAL_TAGS and final_icca_pos_tag in VERB_TAGS:
 
 
 
 
 
 
1924
  complex_NP_type = "VERB_OBJECT_RELATIVISED"
1925
+ elif first_icca_pos_tag in VERB_TAGS:
1926
  complex_NP_type = "SUBJECT_RELATIVISED"
1927
 
1928
  return complex_NP_type
 
2033
  mode = "ccv" → bracket only the rightmost CCV sign
2034
  mode = "cmv1" → bracket only the rightmost CMV1 sign
2035
  mode = "cmn1" → bracket only the rightmost CMN1 sign
2036
+ mode = "cln" → bracket only the rightmost CLN sign
2037
  mode = "ssccv" → bracket one relative SSCCV trigger:
2038
  - nested relatives (2+ opens before next ESCCV): innermost
2039
  (rightmost preferred trigger before that ESCCV)
2040
  - coordinated relatives (CCV between opens): leftmost
2041
  - otherwise: leftmost relative trigger
2042
  - fallback: leftmost SSCCV/ESCCV (e.g. bare "that")
2043
+ mode = "ssmn" → bracket only the leftmost SSMN sign
2044
  """
2045
 
2046
  def surface_of(item):
 
2054
  ccv_indices = []
2055
  cmv1_indices = []
2056
  cmn1_indices = []
2057
+ cln_indices = []
2058
  ssccv_indices = []
2059
  esccv_indices = []
2060
+ ssmn_indices = []
2061
 
2062
  for i, item in enumerate(tagged_list):
2063
  s = str(item)
 
2067
  cmv1_indices.append(i)
2068
  if "<M:Y_CMN1>" in s or "M:Y_CMN1" in s:
2069
  cmn1_indices.append(i)
2070
+ if "<M:Y_CLN>" in s or "M:Y_CLN" in s:
2071
+ cln_indices.append(i)
2072
  if "M:Y_SSCCV" in s:
2073
  ssccv_indices.append(i)
2074
  if "M:Y_ESCCV" in s:
2075
  esccv_indices.append(i)
2076
  if i not in ssccv_indices:
2077
  ssccv_indices.append(i)
2078
+ if "M:Y_SSMN" in s:
2079
+ ssmn_indices.append(i)
2080
+
2081
  target = set()
2082
 
2083
  if mode == "ccv" and ccv_indices:
 
2089
  elif mode == "cmn1" and cmn1_indices:
2090
  target.add(cmn1_indices[-1])
2091
 
2092
+ elif mode == "cln" and cln_indices:
2093
+ target.add(cln_indices[-1])
2094
+
2095
+ elif mode == "ssmn" and ssmn_indices:
2096
+ target.add(ssmn_indices[0]) # leftmost appositive boundary
2097
+
2098
  elif mode == "ssccv" and ssccv_indices:
2099
  relative_pronouns = {"who", "which", "whom", "whose"}
2100
  preferred = []
 
2181
 
2182
 
2183
  ###############################################################################
 
2184
  def process_text(input_text):
2185
  print("=== ITERATIVE SIMPLIFICATION (priority + clean tokens) ===")
2186
  try:
 
2271
  any(x in t for x in ["M:Y_SSCCV", "M:Y_ESCCV"])
2272
  for t in tagged_list
2273
  )
2274
+ has_ssmn = any(
2275
+ any(x in t for x in ["M:Y_SSMN", "M:Y_ESMN"])
2276
+ for t in tagged_list
2277
+ )
2278
  has_ccv = any(
2279
  "<M:Y_CCV>" in t or "M:Y_CCV" in t for t in tagged_list
2280
  )
 
2284
  has_cmn1 = any(
2285
  "<M:Y_CMN1>" in t or "M:Y_CMN1" in t for t in tagged_list
2286
  )
2287
+ has_cln = any(
2288
+ "<M:Y_CLN>" in t or "M:Y_CLN" in t for t in tagged_list
2289
+ )
2290
 
2291
  if has_ssccv:
2292
  mode = "ssccv"
2293
  xml_tag = "M:Y_SSCCV"
2294
  simplifier = SSCCVsimplify
2295
  span_fn = tag_SSCCV_span
2296
+ elif has_ssmn:
2297
+ mode = "ssmn"
2298
+ xml_tag = "M:Y_SSMN"
2299
+ simplifier = SSMNsimplify
2300
+ span_fn = tag_SSMN_span
2301
  elif has_ccv:
2302
  mode = "ccv"
2303
  xml_tag = "M:Y_CCV"
 
2313
  xml_tag = "M:Y_CMN1"
2314
  simplifier = CompoundSimplify
2315
  span_fn = tag_CMN1_span
2316
+ elif has_cln:
2317
+ mode = "cln"
2318
+ xml_tag = "M:Y_CLN"
2319
+ simplifier = CompoundSimplify
2320
+ span_fn = tag_CLN_span
2321
  else:
2322
  sentence_final.append(current)
2323
  continue
 
2352
  )
2353
  simps = simplifier(xml_sent)
2354
 
2355
+ # Residual comma → same conjunction (CCV / CMV1 / CMN1 / CLN only)
2356
  if (
2357
  simps
2358
  and simplifier is CompoundSimplify
2359
+ and xml_tag
2360
+ in ("M:Y_CCV", "M:Y_CMV1", "M:Y_CMN1", "M:Y_CLN")
2361
  ):
2362
  coord = extract_coordinator_from_xml(xml_sent)
2363
  if coord:
 
2371
  )
2372
  rewritten.append(s2)
2373
  simps = rewritten
2374
+
2375
+ # Subject–verb agreement after coordinated-NP (CMN1) splits
2376
+ if simps and xml_tag == "M:Y_CMN1":
2377
+ fixed = []
2378
+ for s in simps:
2379
+ s2 = fix_subject_verb_agreement(s)
2380
+ if s2 != s:
2381
+ print(f" Agreement fix: {s!r} → {s2!r}")
2382
+ fixed.append(s2)
2383
+ simps = fixed
2384
+
2385
  if not simps:
2386
  print(
2387
  "Simplifier returned []; forcing final (no re-queue)"
 
2416
  if not queued_any:
2417
  sentence_final.append(current)
2418
 
 
2419
  for s in order_sentences_for_discourse(sentence_final):
2420
  s = clean_output(s)
2421
  if s:
2422
  para_simplified.append(s)
2423
 
2424
  if para_simplified:
 
2425
  para_simplified = order_sentences_for_discourse(para_simplified)
2426
  output_paragraphs.append(" ".join(para_simplified))
2427
 
 
2482
  }
2483
  """
2484
 
2485
+ with gr.Blocks(
2486
+ title="Syntactic Sentence Simplifier",
2487
+ css=custom_css,
2488
+ ) as demo:
2489
  gr.Markdown("# Syntactic Sentence Simplifier")
2490
  gr.Markdown(
2491
+ "Paste or type your text below. Uses Hugging Face models + simplification logic."
 
 
 
 
 
 
 
 
 
 
 
 
 
2492
  )
2493
 
 
 
2494
  input_text = gr.Textbox(
2495
+ lines=10,
2496
  label="Original text",
2497
+ placeholder="Paste your text here...",
2498
  elem_classes=["input-text"],
2499
  )
2500
 
 
 
 
 
2501
  output_text = gr.Textbox(
2502
+ lines=10,
2503
  label="Simplified text",
2504
  elem_classes=["output-text"],
2505
  )
2506
 
2507
+ btn = gr.Button("Process & Simplify", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2508
 
2509
+ btn.click(
2510
+ fn=process_text,
2511
+ inputs=input_text,
2512
+ outputs=output_text,
2513
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2514
 
2515
+ print(" Gradio interface ready. Starting web server...")
2516
 
2517
  if __name__ == "__main__":
2518
  demo.launch(
2519
  server_name="0.0.0.0",
2520
  server_port=7860,
2521
+ debug=True,
2522
+ share=False,
2523
+ )
2524
+
2525
+
2526
+
2527
+ '''
2528
+ test = (
2529
+ "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."
2530
  )
2531
+ print(process_text(test))
2532
+ '''
2533