File size: 6,302 Bytes
f53f655
 
 
1a90318
f53f655
1a90318
f53f655
1a90318
f53f655
 
 
 
 
 
 
 
 
 
 
 
 
 
ab82262
f53f655
 
 
1a90318
f53f655
1a90318
f53f655
 
ab82262
 
 
 
 
 
f53f655
 
 
 
 
 
 
 
 
 
 
 
1a90318
ab82262
f53f655
 
ab82262
f53f655
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a90318
 
 
f53f655
 
 
1a90318
 
f53f655
1a90318
 
f53f655
1a90318
f53f655
 
1a90318
f53f655
 
 
 
 
 
ab82262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f53f655
ab82262
f53f655
 
ab82262
f53f655
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a90318
f53f655
 
 
1a90318
 
f53f655
 
 
 
 
1a90318
f53f655
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ab82262
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# app.py
# Text Normalizer with step-by-step trace (Gradio)
# Designed to run even in sandboxed envs (no ssl, missing NLTK data).

import sys, types, re, unicodedata, string

# --- Safe SSL stub for restricted environments ---
try:
    import ssl  # noqa: F401
except Exception:  # pragma: no cover
    ssl_stub = types.ModuleType("ssl")
    class _SSLContext:  # minimal placeholder
        def __init__(self, *a, **k): pass
    def _noop(*a, **k): return None
    ssl_stub.SSLContext = _SSLContext
    ssl_stub.create_default_context = _noop
    sys.modules["ssl"] = ssl_stub

# --- Try NLTK, but provide graceful fallbacks ---
USE_NLTK = True
try:
    import nltk
    from nltk.tokenize import word_tokenize as _nltk_word_tokenize
    from nltk.corpus import stopwords
    from nltk.stem import WordNetLemmatizer
    # Attempt downloads if missing (ignore network errors)
    try:
        nltk.data.find("tokenizers/punkt")
    except LookupError:
        try: nltk.download("punkt", quiet=True)
        except Exception: pass
    # newer NLTK splits out language tables
    try:
        nltk.data.find("tokenizers/punkt_tab")
    except LookupError:
        try: nltk.download("punkt_tab", quiet=True)
        except Exception: pass
    try:
        nltk.data.find("corpora/stopwords")
    except LookupError:
        try: nltk.download("stopwords", quiet=True)
        except Exception: pass
    try:
        nltk.data.find("corpora/wordnet")
    except LookupError:
        try: nltk.download("wordnet", quiet=True)
        except Exception: pass
except Exception:
    USE_NLTK = False


if USE_NLTK:
    _lemmatizer = WordNetLemmatizer()

    try:
        _stop_set = set(stopwords.words("english"))
    except Exception:
        _stop_set = set("""a an and are as at be but by for if in into is it its no nor not of on or such that the their then there these they this to was will with you your i we he she from""".split())
else:
    # Fallbacks
    def word_tokenize(text):
        return re.findall(r"[A-Za-z']+|[0-9]+", text)
    _stop_set = set("""a an and are as at be but by for if in into is it its no nor not of on or such that the their then there these they this to was will with you your i we he she from""".split())
    class _DummyLem:
        def lemmatize(self, w): return w
    _lemmatizer = _DummyLem()

# --- Normalization steps ---
def remove_non_ascii(words):
    cleaned = []
    for w in words:
        nfkd = unicodedata.normalize("NFKD", w)
        only_ascii = "".join(ch for ch in nfkd if ord(ch) < 128)
        if only_ascii:
            cleaned.append(only_ascii)
    return cleaned

def to_lowercase(words):
    return [w.lower() for w in words]

_punct_tbl = str.maketrans("", "", string.punctuation)

def remove_punctuation(words):
    return [w.translate(_punct_tbl) for w in words if w.translate(_punct_tbl) != ""]

def remove_stopwords(words):
    return [w for w in words if w not in _stop_set]

def lemmatize_list(words):
    return [_lemmatizer.lemmatize(w) for w in words]


# Robust tokenizer wrapper: try NLTK, else regex fallback
def safe_word_tokenize(text: str):
    if not USE_NLTK:
        return re.findall(r"[A-Za-z']+|[0-9]+", text)
    try:
        return _nltk_word_tokenize(text)
    except LookupError:
        # Last-ditch attempt to fetch punkt_tab/punkt; otherwise fallback
        try:
            import nltk
            nltk.download("punkt_tab", quiet=True)
            nltk.download("punkt", quiet=True)
            return _nltk_word_tokenize(text)
        except Exception:
            return re.findall(r"[A-Za-z']+|[0-9]+", text)

def normalize_trace(text: str):

    trace = []

    tokens = safe_word_tokenize(text or "")
    trace.append(("1) Tokenize", " ".join(tokens)))

    step = remove_non_ascii(tokens)
    trace.append(("2) Remove non‑ASCII", " ".join(step)))

    step = to_lowercase(step)
    trace.append(("3) Lowercase", " ".join(step)))

    step = remove_punctuation(step)
    trace.append(("4) Remove punctuation", " ".join(step)))

    step = remove_stopwords(step)
    trace.append(("5) Remove stopwords", " ".join(step)))

    step = lemmatize_list(step)
    trace.append(("6) Lemmatize", " ".join(step)))

    final_text = " ".join(step)
    return final_text, trace

# --- Gradio UI ---
import gradio as gr

EXAMPLES = [
    "NLTK’s Punkt tokenizer handles abbreviations (e.g., Dr., U.S.A.) better than simple splits!",
    "The QUICK brown foxes were jumping over 13 lazy dogs in 2025...",
    "Text normalization cleans, standardizes, and simplifies raw text for NLP tasks.",
    "Hello—world! Café naïve façade coöperate résumé.",
]

with gr.Blocks(title="Gradio Text Normalizer", theme=gr.themes.Soft()) as demo:
    gr.Markdown("# 🧹 Text Normalizer (Step‑by‑Step)\nPaste text or pick an example, then click **Normalize** to see each step of the pipeline.")
    with gr.Row():
        with gr.Column():
            inp = gr.Textbox(label="Input text", lines=6, placeholder="Type or paste text here")
            ex = gr.Dropdown(EXAMPLES, label="Examples (optional)")
            def _use_example(x): return x
            ex.change(_use_example, ex, inp)
            btn = gr.Button("Normalize", variant="primary")
        with gr.Column():
            out = gr.Textbox(label="Final normalized text", lines=3)
            steps = gr.Dataframe(
                headers=["Step", "Output"],
                datatype=["str", "str"],
                row_count=(6, "fixed"),
                col_count=(2, "fixed"),
                label="Step-by-step trace",
            )

    def _run(text):
        final, trace = normalize_trace(text)
        # Convert trace list of tuples to rows for Dataframe
        rows = [[s, o] for (s, o) in trace]
        return final, rows

    btn.click(_run, inputs=inp, outputs=[out, steps])

    gr.Markdown(
        "### Notes\n"
        "- Uses NLTK when available; falls back to light tokenization/lemmatization otherwise.\n"
        "- Safe SSL stub included for environments missing the `ssl` module.\n"
        "- Pipeline: tokenize → remove non‑ASCII → lowercase → remove punctuation → remove stopwords → lemmatize."
    )

# Avoid deprecated/argued signatures; simple launch for Spaces/Local
if __name__ == '__main__':
    demo.launch()