# 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()