RJ3vans commited on
Commit
2d66f82
·
verified ·
1 Parent(s): a11f124

Upload hf_app_SSCCV_CCV_CMN1_CMV1.py

Browse files
Files changed (1) hide show
  1. hf_app_SSCCV_CCV_CMN1_CMV1.py +968 -0
hf_app_SSCCV_CCV_CMN1_CMV1.py ADDED
@@ -0,0 +1,968 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py — Syntactic Sentence Simplifier (Hugging Face Spaces / ZeroGPU)
2
+ # Models: SignTagger, SSCCVspanTagger, CCVspanTagger, CMV1spanTagger, CMN1spanTagger
3
+
4
+ # --- ZeroGPU: import spaces BEFORE torch / transformers ---
5
+ import spaces
6
+
7
+ import os
8
+ import re
9
+ import sys
10
+ from collections import deque
11
+
12
+ import torch
13
+ from transformers import AutoModelForTokenClassification, AutoTokenizer
14
+ import spacy
15
+ import xml.etree.ElementTree as ET
16
+ import xml.dom.minidom
17
+ import gradio as gr
18
+
19
+ try:
20
+ import spacy_alignments as tokenizations
21
+ except ImportError:
22
+ tokenizations = None
23
+
24
+ # Optional Hub token (Space secret: HF_TOKEN)
25
+ hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
26
+
27
+ device = "cuda" if torch.cuda.is_available() else "cpu"
28
+ print(f"Device: {device} | cuda available: {torch.cuda.is_available()}")
29
+
30
+
31
+ def _load_tok_model(name):
32
+ tok = AutoTokenizer.from_pretrained(name, token=hf_token)
33
+ model = AutoModelForTokenClassification.from_pretrained(name, token=hf_token)
34
+ model.to(device)
35
+ model.eval()
36
+ return tok, model
37
+
38
+
39
+ # ---------- Models ----------
40
+ sign_tagging_model_name = "RJ3vans/SignTagger"
41
+ ssccv_span_tagging_model_name = "RJ3vans/SSCCVspanTagger"
42
+ ccv_span_tagging_model_name = "RJ3vans/CCVspanTagger"
43
+ cmv1_span_tagging_model_name = "RJ3vans/CMV1spanTagger"
44
+ cmn1_span_tagging_model_name = "RJ3vans/CMN1spanTagger"
45
+
46
+ SignTaggingTokenizer, SignTaggingModel = _load_tok_model(sign_tagging_model_name)
47
+ SSCCVspanTaggingTokenizer, SSCCVspanTaggingModel = _load_tok_model(
48
+ ssccv_span_tagging_model_name
49
+ )
50
+ CCVspanTaggingTokenizer, CCVspanTaggingModel = _load_tok_model(
51
+ ccv_span_tagging_model_name
52
+ )
53
+ CMV1spanTaggingTokenizer, CMV1spanTaggingModel = _load_tok_model(
54
+ cmv1_span_tagging_model_name
55
+ )
56
+ CMN1spanTaggingTokenizer, CMN1spanTaggingModel = _load_tok_model(
57
+ cmn1_span_tagging_model_name
58
+ )
59
+
60
+ # Label lists (must match your fine-tuned models’ config.id2label order)
61
+ sign_label_list = list(SignTaggingModel.config.id2label.values())
62
+ SSCCVspan_label_list = list(SSCCVspanTaggingModel.config.id2label.values())
63
+ CCVspan_label_list = list(CCVspanTaggingModel.config.id2label.values())
64
+ CMV1span_label_list = list(CMV1spanTaggingModel.config.id2label.values())
65
+ CMN1span_label_list = list(CMN1spanTaggingModel.config.id2label.values())
66
+
67
+ # spaCy
68
+ try:
69
+ nlp = spacy.load("en_core_web_sm")
70
+ except OSError:
71
+ from spacy.cli import download
72
+
73
+ download("en_core_web_sm")
74
+ nlp = spacy.load("en_core_web_sm")
75
+
76
+ print("All models loaded successfully!")
77
+
78
+ # =====================================================================
79
+ # Helpers — keep behaviour aligned with your local working app
80
+ # =====================================================================
81
+
82
+ _ANAPHORIC_PRONOUNS = {
83
+ "he",
84
+ "she",
85
+ "it",
86
+ "they",
87
+ "him",
88
+ "her",
89
+ "them",
90
+ "his",
91
+ "hers",
92
+ "its",
93
+ "their",
94
+ "theirs",
95
+ }
96
+
97
+
98
+ def sentence_has_anaphoric_pronoun(s: str) -> bool:
99
+ words = re.findall(r"[A-Za-z']+", s.lower())
100
+ return any(w in _ANAPHORIC_PRONOUNS for w in words)
101
+
102
+
103
+ def order_sentences_for_discourse(sentences):
104
+ """Non-pronoun sentences first; pronoun-bearing last (stable)."""
105
+ no_pron = []
106
+ with_pron = []
107
+ for s in sentences:
108
+ if sentence_has_anaphoric_pronoun(s):
109
+ with_pron.append(s)
110
+ else:
111
+ no_pron.append(s)
112
+ return no_pron + with_pron
113
+
114
+
115
+ def clean_output(s: str) -> str:
116
+ if not s:
117
+ return ""
118
+ s = str(s)
119
+ s = re.sub(r"\s*\(\s*m\s*:\s*y[^)]*\)\s*", " ", s, flags=re.I)
120
+ s = re.sub(r"M:Y_[A-Za-z0-9_]+", "", s, flags=re.I)
121
+ s = re.sub(r"##\w*", "", s)
122
+ s = re.sub(r"[\[\]]+", "", s)
123
+ s = re.sub(r"\s+", " ", s).strip()
124
+ s = re.sub(r"\s+([.,!?;:])", r"\1", s)
125
+ s = re.sub(r",+\s*\.", ".", s)
126
+ s = re.sub(r",+\s*$", "", s)
127
+ if s and not s.endswith((".", "!", "?")):
128
+ s += "."
129
+ if s:
130
+ s = s[0].upper() + s[1:]
131
+ return s
132
+
133
+
134
+ def clean_for_spaCy(sent: str) -> str:
135
+ """Normalize one [...] trigger for spaCy; avoid catastrophic backtracking."""
136
+ if "[" not in sent or "]" not in sent:
137
+ return sent
138
+ # Prefer a simple pass-through if already well-formed
139
+ m = re.search(
140
+ r"\[(\s*[,;:]?\s*(?:and|but|or|that|what|when|where|which|while|who)"
141
+ r"(?:\s+(?:and|but|or|that|what|when|where|which|while|who))?)\s*\]",
142
+ sent,
143
+ flags=re.I,
144
+ )
145
+ if not m:
146
+ return sent
147
+ return sent # bracketed form is intentional for span taggers
148
+
149
+
150
+ def drop_bracket_tokens(tokens, pos, lemmas, preds):
151
+ out_t, out_p, out_l, out_pr = [], [], [], []
152
+ for t, p, l, pr in zip(tokens, pos, lemmas, preds):
153
+ if t in ("[", "]"):
154
+ continue
155
+ out_t.append(t)
156
+ out_p.append(p)
157
+ out_l.append(l)
158
+ out_pr.append(pr)
159
+ return out_t, out_p, out_l, out_pr
160
+
161
+
162
+ def sign_cleaning(tokens, pos_tags, lemmas, predictions):
163
+ """Merge BPE pieces; keep lists aligned. Lightweight version."""
164
+ clean_tokens, clean_pos, clean_lemmas, clean_preds = [], [], [], []
165
+ i = 0
166
+ while i < len(tokens):
167
+ tok = tokens[i]
168
+ if tok.startswith("##") and clean_tokens:
169
+ clean_tokens[-1] = clean_tokens[-1] + tok[2:]
170
+ else:
171
+ clean_tokens.append(tok)
172
+ clean_pos.append(pos_tags[i] if i < len(pos_tags) else "XX")
173
+ clean_lemmas.append(lemmas[i] if i < len(lemmas) else tok.lower())
174
+ clean_preds.append(predictions[i] if i < len(predictions) else "UNKNOWN")
175
+ i += 1
176
+ return clean_tokens, clean_pos, clean_lemmas, clean_preds
177
+
178
+
179
+ def improve_predicted_tag_sequence(preds):
180
+ """Map error / NOT_* labels to a neutral tag when needed."""
181
+ out = []
182
+ for p in preds:
183
+ p = str(p)
184
+ if p in (
185
+ "UNKNOWN",
186
+ "ERROR",
187
+ "NOT_CLAUSE_COORDINATOR",
188
+ "NOT_VP_COORDINATOR",
189
+ "NOT_NP_COORDINATOR",
190
+ ):
191
+ out.append(p)
192
+ else:
193
+ out.append(p)
194
+ return out
195
+
196
+
197
+ def normalize_compound_preds(preds):
198
+ """Ensure compound-region labels are usable by create_xml_sent."""
199
+ mapped = []
200
+ for p in preds:
201
+ p = str(p)
202
+ p = p.replace("BEFORE_COMPOUND_CONSTITUENT", "BEFORE_COMPOUND")
203
+ p = p.replace("AFTER_COMPOUND_CONSTITUENT", "AFTER_COMPOUND")
204
+ mapped.append(p)
205
+ return mapped
206
+
207
+
208
+ def tag_all_signs_in_sent(sent: str):
209
+ sent = re.sub(r"do not", "don't", sent, flags=re.I)
210
+ sent = re.sub(
211
+ r"\[((,|;|:|and|but|or|that|what|when|where|which|while|who)"
212
+ r"(_(and|but|or|that|what|when|where|which|while|who))?)\]",
213
+ r"\1\2",
214
+ sent,
215
+ flags=re.I,
216
+ )
217
+
218
+ enc = SignTaggingTokenizer.encode(sent, return_tensors="pt")
219
+ if hasattr(enc, "to"):
220
+ inputs = enc.to(device)
221
+ else:
222
+ inputs = enc
223
+ if torch.is_tensor(inputs):
224
+ inputs = inputs.to(device)
225
+
226
+ with torch.no_grad():
227
+ outputs = SignTaggingModel(inputs)[0]
228
+ predictions = torch.argmax(outputs, dim=2)
229
+
230
+ tokens = SignTaggingTokenizer.convert_ids_to_tokens(inputs[0].tolist())
231
+ # Drop special tokens for surface list building
232
+ sign_tagged_tokens = [
233
+ (tok, sign_label_list[pred])
234
+ for tok, pred in zip(tokens, predictions[0].tolist())
235
+ ]
236
+
237
+ print("sign_tagged_tokens:", sign_tagged_tokens)
238
+
239
+ relevant_sign_offsets = []
240
+ relevant_tags = []
241
+ for t, (tok, lab) in enumerate(sign_tagged_tokens):
242
+ if str(lab).startswith("M:Y"):
243
+ relevant_sign_offsets.append(t)
244
+ relevant_tags.append(lab)
245
+
246
+ tagged_sent_list = [tok for tok, _ in sign_tagged_tokens]
247
+ for o, rtag in zip(relevant_sign_offsets, relevant_tags):
248
+ tagged_sent_list[o] = f"<{rtag}>{tagged_sent_list[o]}</{rtag}>"
249
+
250
+ tagged_sent = [
251
+ tagged_sent_list[x]
252
+ for x in range(len(sign_tagged_tokens))
253
+ if sign_tagged_tokens[x][0] not in ("[CLS]", "[SEP]", "<s>", "</s>")
254
+ ]
255
+ print(f"Returning tagged_sent: result = {tagged_sent}")
256
+ return tagged_sent
257
+
258
+
259
+ def build_single_sign_bracketed(tagged_list, mode="ccv"):
260
+ def surface_of(item):
261
+ item = str(item)
262
+ if item.startswith("<M:Y_") and ">" in item:
263
+ start = item.find(">") + 1
264
+ end = item.rfind("<")
265
+ if end > start:
266
+ return item[start:end]
267
+ return item
268
+
269
+ ccv_indices, cmv1_indices, cmn1_indices = [], [], []
270
+ ssccv_indices, esccv_indices = [], []
271
+
272
+ for i, item in enumerate(tagged_list):
273
+ s = str(item)
274
+ if "M:Y_CCV" in s:
275
+ ccv_indices.append(i)
276
+ if "M:Y_CMV1" in s:
277
+ cmv1_indices.append(i)
278
+ if "M:Y_CMN1" in s:
279
+ cmn1_indices.append(i)
280
+ if "M:Y_SSCCV" in s:
281
+ ssccv_indices.append(i)
282
+ if "M:Y_ESCCV" in s:
283
+ esccv_indices.append(i)
284
+ if i not in ssccv_indices:
285
+ ssccv_indices.append(i)
286
+
287
+ target = set()
288
+ if mode == "ccv" and ccv_indices:
289
+ target.add(ccv_indices[-1])
290
+ elif mode == "cmv1" and cmv1_indices:
291
+ target.add(cmv1_indices[-1])
292
+ elif mode == "cmn1" and cmn1_indices:
293
+ target.add(cmn1_indices[-1])
294
+ elif mode == "ssccv" and ssccv_indices:
295
+ relative_pronouns = {"who", "which", "whom", "whose"}
296
+ preferred = []
297
+ for i in ssccv_indices:
298
+ surface = surface_of(tagged_list[i]).strip().lower()
299
+ if surface in relative_pronouns:
300
+ preferred.append(i)
301
+ continue
302
+ if surface in {",", ";", ":"} and i + 1 < len(tagged_list):
303
+ nxt = surface_of(tagged_list[i + 1]).strip().lower()
304
+ if nxt in relative_pronouns or nxt == "that":
305
+ preferred.append(i)
306
+ if preferred:
307
+ nested = coordinated = False
308
+ if esccv_indices:
309
+ first_es = min(esccv_indices)
310
+ opens_before_close = [i for i in preferred if i < first_es]
311
+ if len(opens_before_close) >= 2:
312
+ first_open, last_open = opens_before_close[0], opens_before_close[-1]
313
+ ccv_between = any(
314
+ first_open < j < last_open for j in ccv_indices
315
+ )
316
+ if ccv_between:
317
+ coordinated = True
318
+ target.add(opens_before_close[0])
319
+ else:
320
+ nested = True
321
+ target.add(opens_before_close[-1])
322
+ if not nested and not coordinated:
323
+ target.add(preferred[0])
324
+ else:
325
+ pure = [i for i, it in enumerate(tagged_list) if "M:Y_SSCCV" in str(it)]
326
+ target.add((pure or ssccv_indices)[0])
327
+
328
+ parts = []
329
+ for i, item in enumerate(tagged_list):
330
+ item = str(item)
331
+ if item.startswith("<M:Y_") and item.endswith(">"):
332
+ start = item.find(">") + 1
333
+ end = item.rfind("<")
334
+ surface = item[start:end] if end > start else item
335
+ parts.append(f"[{surface}]" if i in target else surface)
336
+ else:
337
+ parts.append(item)
338
+
339
+ text = " ".join(parts)
340
+ text = re.sub(r"\s+\[([,;:])", r"[\1", text)
341
+ text = re.sub(r"(?<!\[)\[([A-Za-z])", r" [\1", text)
342
+ text = re.sub(r"\[\s+", "[", text)
343
+ text = re.sub(r"\s+\]", "]", text)
344
+ text = re.sub(r"\s+", " ", text).strip()
345
+ text = re.sub(
346
+ r"\[([,;:])\]\s+(who|which|that|whom|whose)\b",
347
+ r"[\1 \2]",
348
+ text,
349
+ flags=re.I,
350
+ )
351
+ return text
352
+
353
+
354
+ def _align_span(sentence, tokenizer, model, label_list):
355
+ if re.search(r"\[:\]\s*$", sentence):
356
+ sentence = sentence + " blah blah blah."
357
+
358
+ inputs = tokenizer.encode(sentence, return_tensors="pt").to(device)
359
+ with torch.no_grad():
360
+ outputs = model(inputs)[0]
361
+ predictions = torch.argmax(outputs, dim=2)
362
+ tokens = tokenizer.convert_ids_to_tokens(inputs[0].tolist())
363
+ span_tagged = [
364
+ (tok, label_list[pred])
365
+ for tok, pred in zip(tokens, predictions[0].tolist())
366
+ ]
367
+ span_preds = [p for _, p in span_tagged]
368
+
369
+ sc = clean_for_spaCy(sentence) if ("[" in sentence and "]" in sentence) else sentence
370
+ doc = nlp(sc)
371
+ spacy_tokens = [t.text for t in doc]
372
+ spacy_pos = [t.tag_ for t in doc]
373
+ spacy_lemmas = [t.lemma_ for t in doc]
374
+
375
+ # Strip special tokens from BERT side for alignment
376
+ bert_toks = [t for t in tokens if t not in ("[CLS]", "[SEP]", "<s>", "</s>")]
377
+ bert_preds = [
378
+ p
379
+ for t, p in zip(tokens, span_preds)
380
+ if t not in ("[CLS]", "[SEP]", "<s>", "</s>")
381
+ ]
382
+
383
+ aligned_tokens, aligned_pos, aligned_lemmas, aligned_preds = [], [], [], []
384
+ if tokenizations is not None:
385
+ a2b, _ = tokenizations.get_alignments(spacy_tokens, bert_toks)
386
+ for i in range(len(spacy_tokens)):
387
+ if a2b[i]:
388
+ j = a2b[i][0]
389
+ aligned_tokens.append(spacy_tokens[i])
390
+ aligned_pos.append(spacy_pos[i])
391
+ aligned_lemmas.append(spacy_lemmas[i])
392
+ aligned_preds.append(
393
+ bert_preds[j] if j < len(bert_preds) else "UNKNOWN"
394
+ )
395
+ else:
396
+ aligned_tokens.append(spacy_tokens[i])
397
+ aligned_pos.append(spacy_pos[i])
398
+ aligned_lemmas.append(spacy_lemmas[i])
399
+ aligned_preds.append("UNKNOWN")
400
+ else:
401
+ aligned_tokens, aligned_pos, aligned_lemmas = (
402
+ spacy_tokens,
403
+ spacy_pos,
404
+ spacy_lemmas,
405
+ )
406
+ aligned_preds = ["UNKNOWN"] * len(spacy_tokens)
407
+
408
+ return sign_cleaning(
409
+ aligned_tokens, aligned_pos, aligned_lemmas, aligned_preds
410
+ )
411
+
412
+
413
+ def tag_CCV_span(sentence):
414
+ return _align_span(
415
+ sentence, CCVspanTaggingTokenizer, CCVspanTaggingModel, CCVspan_label_list
416
+ )
417
+
418
+
419
+ def tag_SSCCV_span(sentence):
420
+ return _align_span(
421
+ sentence,
422
+ SSCCVspanTaggingTokenizer,
423
+ SSCCVspanTaggingModel,
424
+ SSCCVspan_label_list,
425
+ )
426
+
427
+
428
+ def tag_CMV1_span(sentence):
429
+ return _align_span(
430
+ sentence,
431
+ CMV1spanTaggingTokenizer,
432
+ CMV1spanTaggingModel,
433
+ CMV1span_label_list,
434
+ )
435
+
436
+
437
+ def tag_CMN1_span(sentence):
438
+ return _align_span(
439
+ sentence,
440
+ CMN1spanTaggingTokenizer,
441
+ CMN1spanTaggingModel,
442
+ CMN1span_label_list,
443
+ )
444
+
445
+
446
+ def create_xml_sent(tokens, pos_tags, lemmas, predictions, sign_tag):
447
+ xml_sent = ET.Element("SENT")
448
+
449
+ if sign_tag == "M:Y_SSCCV":
450
+ before_complex_constituent = ET.SubElement(
451
+ xml_sent, "BEFORE_COMPLEX_CONSTITUENT"
452
+ )
453
+ in_complex_constituent_beforesign = ET.SubElement(
454
+ xml_sent, "IN_COMPLEX_CONSTITUENT_BEFORESIGN"
455
+ )
456
+ in_complex_constituent = ET.SubElement(xml_sent, "IN_COMPLEX_CONSTITUENT")
457
+ in_complex_constituent_aftersign = ET.SubElement(
458
+ xml_sent, "IN_COMPLEX_CONSTITUENT_AFTERSIGN"
459
+ )
460
+ after_complex_constituent = ET.SubElement(
461
+ xml_sent, "AFTER_COMPLEX_CONSTITUENT"
462
+ )
463
+ xml_sent.attrib["TYPE"] = "COMPLEX_NP"
464
+ elif sign_tag in ("M:Y_CCV", "M:Y_CMV1", "M:Y_CMN1"):
465
+ before_complex_constituent = ET.SubElement(
466
+ xml_sent, "BEFORE_COMPOUND_CONSTITUENT"
467
+ )
468
+ in_complex_constituent_beforesign = ET.SubElement(
469
+ xml_sent, "IN_COMPOUND_CONSTITUENT_BEFORESIGN"
470
+ )
471
+ in_complex_constituent = ET.SubElement(xml_sent, "IN_COMPOUND_CONSTITUENT")
472
+ in_complex_constituent_aftersign = ET.SubElement(
473
+ xml_sent, "IN_COMPOUND_CONSTITUENT_AFTERSIGN"
474
+ )
475
+ after_complex_constituent = ET.SubElement(
476
+ xml_sent, "AFTER_COMPOUND_CONSTITUENT"
477
+ )
478
+ xml_sent.attrib["TYPE"] = "COMPOUND"
479
+ else:
480
+ xml_sent.attrib["TYPE"] = str(predictions[0]) if predictions else "UNKNOWN"
481
+ print(
482
+ "BUILT XML SENTENCE:\n"
483
+ + ET.tostring(xml_sent, encoding="unicode")
484
+ )
485
+ return xml_sent
486
+
487
+ def add_w(parent, i):
488
+ w = ET.SubElement(parent, "W")
489
+ w.text = tokens[i]
490
+ w.attrib["POS"] = pos_tags[i]
491
+ w.attrib["LEMMA"] = lemmas[i]
492
+
493
+ x = 0
494
+ while x < len(tokens):
495
+ pr = str(predictions[x]) if x < len(predictions) else ""
496
+ if "_AFTERSIGN" in pr or "AFTERSIGN" in pr:
497
+ if re.match(
498
+ r"(and|but|or|that|what|when|where|which|while|who)$",
499
+ tokens[x],
500
+ re.I,
501
+ ):
502
+ if x > 0 and tokens[x - 1] in (",", ":", ";"):
503
+ add_w(in_complex_constituent, x)
504
+ else:
505
+ add_w(in_complex_constituent_aftersign, x)
506
+ else:
507
+ add_w(in_complex_constituent_aftersign, x)
508
+ elif pr.startswith("IN_") or "IN_COMPOUND" == pr or pr == "IN_COMPLEX_NP":
509
+ add_w(in_complex_constituent, x)
510
+ elif "BEFORESIGN" in pr:
511
+ add_w(in_complex_constituent_beforesign, x)
512
+ elif "BEFORE_" in pr:
513
+ add_w(before_complex_constituent, x)
514
+ elif "AFTER_" in pr:
515
+ add_w(after_complex_constituent, x)
516
+ else:
517
+ # default: treat as before-sign region for compounds
518
+ add_w(in_complex_constituent_beforesign, x)
519
+ x += 1
520
+
521
+ print(
522
+ "BUILT XML SENTENCE:\n" + ET.tostring(xml_sent, encoding="unicode")
523
+ )
524
+ return xml_sent
525
+
526
+
527
+ def simplified_sent_xml_to_text(xml_el):
528
+ parts = []
529
+ for w in xml_el.iter("W"):
530
+ if w.text:
531
+ parts.append(w.text.strip())
532
+ s = " ".join(parts)
533
+ return clean_output(s)
534
+
535
+
536
+ def get_COMPLEX_NP_type(xml_sent):
537
+ complex_NP_type = "UNKNOWN"
538
+ final_icca_pos_tag = first_icca_pos_tag = final_iccb_pos_tag = "UNKNOWN"
539
+ for el in xml_sent:
540
+ if el.tag == "IN_COMPLEX_CONSTITUENT_AFTERSIGN":
541
+ try:
542
+ first_icca_pos_tag = el[0].attrib["POS"]
543
+ final_icca_pos_tag = el[-1].attrib["POS"]
544
+ except Exception:
545
+ pass
546
+ elif el.tag == "IN_COMPLEX_CONSTITUENT_BEFORESIGN":
547
+ try:
548
+ final_iccb_pos_tag = el[-1].attrib["POS"]
549
+ except Exception:
550
+ pass
551
+ if final_iccb_pos_tag in ("IN",):
552
+ complex_NP_type = "PREPOSITION_OBJECT_RELATIVISED"
553
+ elif first_icca_pos_tag in ("NNP", "NNPS", "NN", "DT", "CD") and final_icca_pos_tag in (
554
+ "VB",
555
+ "VBD",
556
+ "VBG",
557
+ "VBN",
558
+ "VBZ",
559
+ ):
560
+ complex_NP_type = "VERB_OBJECT_RELATIVISED"
561
+ elif first_icca_pos_tag in ("VB", "VBD", "VBG", "VBN", "VBZ"):
562
+ complex_NP_type = "SUBJECT_RELATIVISED"
563
+ return complex_NP_type
564
+
565
+
566
+ def CompoundSimplify(xml_sent):
567
+ try:
568
+
569
+ def get_text(elem):
570
+ if elem is None:
571
+ return ""
572
+ parts = []
573
+ for w in elem.findall("W"):
574
+ if w.text:
575
+ t = re.sub(r"[\[\]]+", "", str(w.text).strip())
576
+ if t:
577
+ parts.append(t)
578
+ return " ".join(parts).strip()
579
+
580
+ before_elem = xml_sent.find(".//BEFORE_COMPOUND_CONSTITUENT")
581
+ beforesign_elem = xml_sent.find(".//IN_COMPOUND_CONSTITUENT_BEFORESIGN")
582
+ aftersign_elem = xml_sent.find(".//IN_COMPOUND_CONSTITUENT_AFTERSIGN")
583
+ after_elem = xml_sent.find(".//AFTER_COMPOUND_CONSTITUENT")
584
+
585
+ before_text = get_text(before_elem)
586
+ beforesign_text = get_text(beforesign_elem)
587
+ aftersign_text = get_text(aftersign_elem)
588
+ after_text = get_text(after_elem)
589
+
590
+ sent1 = " ".join(
591
+ x for x in (before_text, beforesign_text, after_text) if x
592
+ ).strip()
593
+ sent2 = " ".join(
594
+ x for x in (before_text, aftersign_text, after_text) if x
595
+ ).strip()
596
+
597
+ def tidy(s):
598
+ s = re.sub(r"\s+", " ", s).strip()
599
+ s = re.sub(r"\s+([.,!?;:])", r"\1", s)
600
+ s = re.sub(r",+\s*\.", ".", s)
601
+ s = re.sub(r",+\s*$", "", s)
602
+ if s and not s.endswith((".", "!", "?")):
603
+ s += "."
604
+ if s:
605
+ s = s[0].upper() + s[1:]
606
+ return s
607
+
608
+ sent1, sent2 = tidy(sent1), tidy(sent2)
609
+ result = []
610
+ if sent1:
611
+ result.append(sent1)
612
+ if sent2 and sent2 != sent1:
613
+ result.append(sent2)
614
+ print(f"CompoundSimplify produced {len(result)} sentences: {result}")
615
+ return result
616
+ except Exception as e:
617
+ print(f"CompoundSimplify error: {e}")
618
+ return []
619
+
620
+
621
+ def SSCCVsimplify(xml_sent):
622
+ """Subject / object relative simplification (core SUBJECT_RELATIVISED path)."""
623
+ simplification = []
624
+ simp_sent1 = ET.Element("S")
625
+ simp_sent2 = ET.Element("S")
626
+
627
+ if xml_sent.attrib.get("TYPE") != "COMPLEX_NP":
628
+ return simplification
629
+
630
+ complex_NP_type = get_COMPLEX_NP_type(xml_sent)
631
+ print("SSCCV complex_NP_type:", complex_NP_type)
632
+
633
+ if complex_NP_type == "SUBJECT_RELATIVISED":
634
+ matrix_subject = None
635
+ predication = None
636
+ before_parts = []
637
+ after_parts = []
638
+
639
+ for xs_el in xml_sent:
640
+ if xs_el.tag == "IN_COMPLEX_CONSTITUENT":
641
+ # drop trailing punctuation inside the sign region if present
642
+ try:
643
+ if len(xs_el) >= 2 and re.match(r"[,:;]$", xs_el[-2].text or ""):
644
+ pass
645
+ except Exception:
646
+ pass
647
+ elif xs_el.tag == "IN_COMPLEX_CONSTITUENT_BEFORESIGN":
648
+ matrix_subject = xs_el
649
+ elif xs_el.tag == "IN_COMPLEX_CONSTITUENT_AFTERSIGN":
650
+ # strip leading comma
651
+ kids = list(xs_el)
652
+ if kids and re.match(r"[,:;]$", kids[0].text or ""):
653
+ xs_el.remove(kids[0])
654
+ predication = xs_el
655
+ elif xs_el.tag == "BEFORE_COMPLEX_CONSTITUENT":
656
+ before_parts.append(xs_el)
657
+ elif xs_el.tag == "AFTER_COMPLEX_CONSTITUENT":
658
+ after_parts.append(xs_el)
659
+
660
+ # sent1: before + subject + after (matrix without relative body)
661
+ for bp in before_parts:
662
+ simp_sent1.append(bp)
663
+ if matrix_subject is not None:
664
+ simp_sent1.append(matrix_subject)
665
+ for ap in after_parts:
666
+ # strip leading comma on after
667
+ kids = list(ap)
668
+ if kids and re.match(r"[,:;]$", kids[0].text or ""):
669
+ ap.remove(kids[0])
670
+ simp_sent1.append(ap)
671
+
672
+ # sent2: subject + relative predication
673
+ if matrix_subject is not None:
674
+ simp_sent2.append(matrix_subject)
675
+ if predication is not None:
676
+ simp_sent2.append(predication)
677
+
678
+ elif complex_NP_type == "VERB_OBJECT_RELATIVISED":
679
+ # object relative: predication + subject; matrix keeps before/after
680
+ matrix_subject = predication = None
681
+ before_parts, after_parts = [], []
682
+ for xs_el in xml_sent:
683
+ if xs_el.tag == "IN_COMPLEX_CONSTITUENT_BEFORESIGN":
684
+ matrix_subject = xs_el
685
+ elif xs_el.tag == "IN_COMPLEX_CONSTITUENT_AFTERSIGN":
686
+ kids = list(xs_el)
687
+ if kids and re.match(r"[,:;]$", kids[0].text or ""):
688
+ xs_el.remove(kids[0])
689
+ predication = xs_el
690
+ elif xs_el.tag == "BEFORE_COMPLEX_CONSTITUENT":
691
+ before_parts.append(xs_el)
692
+ elif xs_el.tag == "AFTER_COMPLEX_CONSTITUENT":
693
+ after_parts.append(xs_el)
694
+
695
+ for bp in before_parts:
696
+ simp_sent1.append(bp)
697
+ if matrix_subject is not None:
698
+ simp_sent1.append(matrix_subject)
699
+ for ap in after_parts:
700
+ kids = list(ap)
701
+ if kids and re.match(r"[,:;]$", kids[0].text or ""):
702
+ ap.remove(kids[0])
703
+ simp_sent1.append(ap)
704
+
705
+ if predication is not None:
706
+ simp_sent2.append(predication)
707
+ if matrix_subject is not None:
708
+ simp_sent2.append(matrix_subject)
709
+
710
+ if len(simp_sent1) > 0 and len(simp_sent2) > 0:
711
+ try:
712
+ simplification.append(simplified_sent_xml_to_text(simp_sent1))
713
+ simplification.append(simplified_sent_xml_to_text(simp_sent2))
714
+ except Exception as e:
715
+ print("SSCCVsimplify xml_to_text error:", e)
716
+
717
+ return simplification
718
+
719
+
720
+ def split_into_sentences(text: str):
721
+ doc = nlp(text)
722
+ return [s.text.strip() for s in doc.sents if s.text.strip()]
723
+
724
+
725
+ # =====================================================================
726
+ # Main pipeline (GPU-decorated for ZeroGPU)
727
+ # =====================================================================
728
+
729
+ @spaces.GPU(duration=120)
730
+ def process_text(input_text):
731
+ print("=== ITERATIVE SIMPLIFICATION (priority + clean tokens) ===")
732
+ try:
733
+ MAX_ITERS = 40
734
+ if not input_text or not str(input_text).strip():
735
+ return "No input text."
736
+
737
+ input_text = str(input_text)
738
+ input_text = re.sub(r"supposingthat", "supposing that", input_text)
739
+ input_text = re.sub(r'"', "''", input_text)
740
+ input_text = re.sub(r"\.\s+([a-z])", r".\n\1", input_text)
741
+ input_text = re.sub(r"mainly f or", "mainly for", input_text)
742
+ input_text = re.sub(r"sothat", "so that", input_text)
743
+ input_text = re.sub(r"in alabama", "in Alabama", input_text)
744
+ input_text = re.sub(
745
+ r"LEISURE DESIGN IS IMPORTANT WHY is",
746
+ "LEISURE DESIGN IS IMPORTANT\n\nWhy is",
747
+ input_text,
748
+ )
749
+ input_text = re.sub(r"WHEN", "When", input_text)
750
+ input_text = re.sub(r"WHAT'S", "What's", input_text)
751
+ input_text = re.sub(r"(\s+)an'", r"\1and", input_text)
752
+ input_text = re.sub(r"An'", "And", input_text)
753
+
754
+ raw_paragraphs = re.split(r"\n\s*\n", input_text)
755
+ output_paragraphs = []
756
+
757
+ for para in raw_paragraphs:
758
+ para = para.strip()
759
+ if not para:
760
+ continue
761
+ para = re.sub(r"\s*\n\s*", " ", para)
762
+
763
+ sents = split_into_sentences(para)
764
+ print(f"Paragraph → {len(sents)} sentence(s):")
765
+ for i, s in enumerate(sents, 1):
766
+ print(f" [{i}] {s}")
767
+
768
+ para_simplified = []
769
+
770
+ for original_sent in sents:
771
+ working_set = deque([original_sent])
772
+ seen = set()
773
+ sentence_final = []
774
+ iters = 0
775
+
776
+ while working_set:
777
+ current = working_set.popleft()
778
+ iters += 1
779
+ if iters > MAX_ITERS:
780
+ print("MAX_ITERS reached; flushing rest as final")
781
+ sentence_final.append(current)
782
+ sentence_final.extend(working_set)
783
+ break
784
+
785
+ key = re.sub(r"\s+", " ", current.lower()).strip()
786
+ if key in seen:
787
+ sentence_final.append(current)
788
+ continue
789
+ seen.add(key)
790
+
791
+ print(f"\n--- iter {iters} | q={len(working_set)} | {current[:80]}...")
792
+
793
+ tagged_list = tag_all_signs_in_sent(current)
794
+ tagged_str = " ".join(str(x) for x in tagged_list)
795
+
796
+ has_ssccv = bool(
797
+ re.search(r"M:Y_SSCCV|M:Y_ESCCV", tagged_str)
798
+ )
799
+ has_ccv = "M:Y_CCV" in tagged_str
800
+ has_cmv1 = "M:Y_CMV1" in tagged_str
801
+ has_cmn1 = "M:Y_CMN1" in tagged_str
802
+
803
+ if not (has_ssccv or has_ccv or has_cmv1 or has_cmn1):
804
+ sentence_final.append(current)
805
+ print(f" No target signs → final: {current}")
806
+ continue
807
+
808
+ # Priority: SSCCV > CCV > CMV1 > CMN1
809
+ if has_ssccv:
810
+ mode, xml_tag, span_fn, simplifier = (
811
+ "ssccv",
812
+ "M:Y_SSCCV",
813
+ tag_SSCCV_span,
814
+ SSCCVsimplify,
815
+ )
816
+ elif has_ccv:
817
+ mode, xml_tag, span_fn, simplifier = (
818
+ "ccv",
819
+ "M:Y_CCV",
820
+ tag_CCV_span,
821
+ CompoundSimplify,
822
+ )
823
+ elif has_cmv1:
824
+ mode, xml_tag, span_fn, simplifier = (
825
+ "cmv1",
826
+ "M:Y_CMV1",
827
+ tag_CMV1_span,
828
+ CompoundSimplify,
829
+ )
830
+ else:
831
+ mode, xml_tag, span_fn, simplifier = (
832
+ "cmn1",
833
+ "M:Y_CMN1",
834
+ tag_CMN1_span,
835
+ CompoundSimplify,
836
+ )
837
+
838
+ bracketed_version = build_single_sign_bracketed(
839
+ tagged_list, mode=mode
840
+ )
841
+ print(f"Processing: {current}")
842
+ print(f"Bracketed version ({mode}): {bracketed_version}")
843
+
844
+ cleaned = (
845
+ clean_for_spaCy(bracketed_version)
846
+ if "[" in bracketed_version
847
+ else bracketed_version
848
+ )
849
+ tokens, pos, lemmas, preds = span_fn(cleaned)
850
+ tokens, pos, lemmas, preds = drop_bracket_tokens(
851
+ tokens, pos, lemmas, preds
852
+ )
853
+ cleaned_tokens, clean_pos, clean_lemmas, cleaned_preds = (
854
+ sign_cleaning(tokens, pos, lemmas, preds)
855
+ )
856
+ improved_preds = improve_predicted_tag_sequence(cleaned_preds)
857
+ if mode in ("ccv", "cmv1", "cmn1"):
858
+ improved_preds = normalize_compound_preds(improved_preds)
859
+
860
+ xml_sent = create_xml_sent(
861
+ cleaned_tokens,
862
+ clean_pos,
863
+ clean_lemmas,
864
+ improved_preds,
865
+ xml_tag,
866
+ )
867
+ simps = simplifier(xml_sent)
868
+
869
+ if not simps:
870
+ print("Simplifier returned []; forcing final (no re-queue)")
871
+ sentence_final.append(current)
872
+ continue
873
+
874
+ queued_any = False
875
+ for s in simps:
876
+ plain_s = clean_output(s)
877
+ if not plain_s:
878
+ continue
879
+ plain_key = re.sub(r"\s+", " ", plain_s.lower())
880
+ if plain_key == key:
881
+ sentence_final.append(plain_s)
882
+ continue
883
+ if plain_key in seen:
884
+ sentence_final.append(plain_s)
885
+ continue
886
+ working_set.append(plain_s)
887
+ queued_any = True
888
+ print(f" → Queued: {plain_s}")
889
+
890
+ if not queued_any:
891
+ sentence_final.append(current)
892
+
893
+ # discourse order within this original sentence’s results
894
+ ordered = order_sentences_for_discourse(
895
+ [clean_output(s) for s in sentence_final if clean_output(s)]
896
+ )
897
+ para_simplified.extend(ordered)
898
+
899
+ if para_simplified:
900
+ output_paragraphs.append(" ".join(para_simplified))
901
+
902
+ final_output = "\n\n".join(output_paragraphs)
903
+ print(f"\n=== FINAL OUTPUT ===\n{final_output}")
904
+ return final_output if final_output else "No simplification produced"
905
+
906
+ except Exception as e:
907
+ import traceback
908
+
909
+ print(traceback.format_exc())
910
+ return f"ERROR: {str(e)}"
911
+
912
+
913
+ # =====================================================================
914
+ # Gradio UI
915
+ # =====================================================================
916
+
917
+ custom_css = """
918
+ .input-text textarea {
919
+ font-size: 16px !important;
920
+ color: #ff1493 !important; /* bright pink */
921
+ font-weight: 500 !important;
922
+ line-height: 1.45 !important;
923
+ }
924
+ .output-text textarea {
925
+ font-size: 16px !important;
926
+ color: #00c853 !important; /* bright green */
927
+ font-weight: 500 !important;
928
+ line-height: 1.45 !important;
929
+ }
930
+ .input-text label,
931
+ .output-text label {
932
+ font-size: 15px !important;
933
+ font-weight: 600 !important;
934
+ }
935
+ .input-text label { color: #ff8a80 !important; }
936
+ .output-text label { color: #69f0ae !important; }
937
+ """
938
+
939
+ print("Creating Gradio interface...")
940
+
941
+ with gr.Blocks(title="Syntactic Sentence Simplifier") as demo:
942
+ gr.Markdown("# Syntactic Sentence Simplifier")
943
+ gr.Markdown(
944
+ "Paste or type text below. Handles relative clauses (SSCCV) and "
945
+ "coordination (CCV, CMV1, CMN1) with iterative simplification."
946
+ )
947
+ input_text = gr.Textbox(
948
+ lines=10,
949
+ label="Original text",
950
+ placeholder="Paste your text here...",
951
+ elem_classes=["input-text"],
952
+ )
953
+ output_text = gr.Textbox(
954
+ lines=10,
955
+ label="Simplified text",
956
+ elem_classes=["output-text"],
957
+ )
958
+ btn = gr.Button("Process & Simplify", variant="primary")
959
+ btn.click(fn=process_text, inputs=input_text, outputs=output_text)
960
+
961
+ print("Gradio interface ready. Starting web server...")
962
+
963
+ if __name__ == "__main__":
964
+ demo.launch(
965
+ server_name="0.0.0.0",
966
+ server_port=7860,
967
+ css=custom_css,
968
+ )