File size: 7,228 Bytes
5a2d956
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
"""
NLTK Text Processing Playground (Gradio)

Features
- Type text or drag-and-drop a .txt or .docx file
- Menu of steps:
    1) Install & download required NLTK resources
    2) Tokenize text
    3) Remove stopwords
    4) Stem words (Porter)
    5) Lemmatize words (WordNet)
    6) Tag parts of speech
    7) Extract named entities
- Prints results to screen for only the steps you select
"""

import io
import os
import re
from typing import List, Tuple, Optional

import gradio as gr

# --- NLTK imports are inside functions so the app can start even if resources aren't ready ---

SUPPORTED_EXTS = {".txt", ".docx"}

def read_text_from_inputs(text_input: str, file_obj: Optional[gr.File]) -> str:
    """
    Returns a single text string from either the text box or the uploaded file.
    If both are provided, file content takes precedence.
    """
    if file_obj is not None:
        name = getattr(file_obj, "name", None) or ""
        ext = os.path.splitext(name)[1].lower()
        file_bytes = file_obj.read()
        if ext == ".txt":
            try:
                return file_bytes.decode("utf-8")
            except Exception:
                # Fallback: best-effort decode
                return file_bytes.decode(errors="ignore")
        elif ext == ".docx":
            from docx import Document  # python-docx
            with io.BytesIO(file_bytes) as buf:
                doc = Document(buf)
                return "\n".join(p.text for p in doc.paragraphs)
        else:
            raise gr.Error(f"Unsupported file type: {ext}. Use one of: {', '.join(SUPPORTED_EXTS)}.")
    # fallback to text area
    return text_input or ""


def setup_nltk() -> str:
    """
    Installs/downloads the corpora and models needed for the lab.
    Safe to run multiple times; NLTK skips existing files.
    """
    import nltk
    downloaded = []
    for pkg in [
        "punkt",
        "stopwords",
        "wordnet",
        "averaged_perceptron_tagger",
        "maxent_ne_chunker",
        "words",
    ]:
        try:
            nltk.download(pkg, quiet=True)
            downloaded.append(pkg)
        except Exception as e:
            downloaded.append(f"{pkg} (error: {e})")
    return "NLTK resources ready:\n- " + "\n- ".join(downloaded)


def pipeline(text: str, steps: List[str]) -> str:
    """
    Runs the selected steps in a fixed logical order and returns a Markdown report.
    """
    if not text.strip():
        return "⚠️ **No input text found.** Type text or upload a .txt/.docx file."

    # Import here (after optional setup) to avoid early failures
    import nltk
    from nltk.tokenize import word_tokenize
    from nltk.corpus import stopwords
    from nltk.stem import PorterStemmer, WordNetLemmatizer
    from nltk import pos_tag
    from nltk.chunk import ne_chunk

    report_sections = []
    current_tokens = []
    filtered_tokens = []
    stemmed_tokens = []
    lemmatized_tokens = []

    # 1) Tokenize
    if "Tokenize text" in steps:
        current_tokens = word_tokenize(text)
        report_sections.append(
            "### 1) Tokens\n```\n" + repr(current_tokens) + "\n```"
        )

    # 2) Stopword removal (case-insensitive)
    if "Remove stopwords" in steps:
        if not current_tokens:
            current_tokens = word_tokenize(text)
        stop_words = set(stopwords.words("english"))
        filtered_tokens = [w for w in current_tokens if w.lower() not in stop_words]
        report_sections.append(
            "### 2) Filtered (stopwords removed)\n```\n" + repr(filtered_tokens) + "\n```"
        )

    # 3) Stem
    if "Stem words" in steps:
        if not filtered_tokens:
            # If user skipped stopwords, stem tokens directly
            base = current_tokens or word_tokenize(text)
        else:
            base = filtered_tokens
        stemmer = PorterStemmer()
        stemmed_tokens = [stemmer.stem(w) for w in base]
        report_sections.append(
            "### 3) Stemmed (Porter)\n```\n" + repr(stemmed_tokens) + "\n```"
        )

    # 4) Lemmatize
    if "Lemmatize words" in steps:
        if not filtered_tokens:
            base = current_tokens or word_tokenize(text)
        else:
            base = filtered_tokens
        lemmatizer = WordNetLemmatizer()
        lemmatized_tokens = [lemmatizer.lemmatize(w) for w in base]
        report_sections.append(
            "### 4) Lemmatized (WordNet, default POS=noun)\n```\n"
            + repr(lemmatized_tokens)
            + "\n```"
        )

    # Choose a reasonable token sequence for downstream steps
    downstream = (
        lemmatized_tokens
        or stemmed_tokens
        or filtered_tokens
        or current_tokens
        or word_tokenize(text)
    )

    # 5) POS tagging
    if "Tag parts of speech" in steps:
        tags = pos_tag(downstream)
        report_sections.append("### 5) POS Tags\n```\n" + repr(tags) + "\n```")

    # 6) Named entities
    if "Extract named entities" in steps:
        # ne_chunk expects POS-tagged input
        tagged = pos_tag(downstream)
        tree = ne_chunk(tagged)
        # Pretty-print the chunk tree as text
        report_sections.append(
            "### 6) Named Entities (chunk tree)\n```\n" + tree.pformat() + "\n```"
        )

    if not report_sections:
        return "ℹ️ **No steps selected.** Choose at least one item from the menu."

    header = "# NLTK Processing Report\n"
    return header + "\n\n".join(report_sections)


with gr.Blocks(title="NLTK Text Processing Playground") as demo:
    gr.Markdown(
        """
# NLTK Text Processing Playground

**Type text** _or_ **drop a `.txt` / `.docx` file**.  
Select the steps you want and click **Process**.  
If this is your first time, click **Prepare NLTK Resources**.

"""
    )

    with gr.Row():
        text_in = gr.Textbox(
            label="Text input",
            placeholder="Type or paste text here (or upload a file instead)…",
            lines=8,
        )
    file_in = gr.File(
        label="Optional: upload a .txt or .docx file",
        file_types=[".txt", ".docx"],
        file_count="single",
    )

    steps = gr.CheckboxGroup(
        choices=[
            "Tokenize text",
            "Remove stopwords",
            "Stem words",
            "Lemmatize words",
            "Tag parts of speech",
            "Extract named entities",
        ],
        value=["Tokenize text", "Remove stopwords", "Lemmatize words", "Tag parts of speech", "Extract named entities"],
        label="Menu — select one or more steps",
    )

    with gr.Row():
        setup_btn = gr.Button("🧰 Prepare NLTK Resources")
        run_btn = gr.Button("▶️ Process")

    output = gr.Markdown(label="Output")

    # Wire up actions
    setup_btn.click(fn=lambda: setup_nltk(), outputs=output)

    def run_pipeline(text_input, file_input, selected_steps):
        text = read_text_from_inputs(text_input, file_input)
        return pipeline(text, selected_steps)

    run_btn.click(
        fn=run_pipeline,
        inputs=[text_in, file_in, steps],
        outputs=output,
    )

if __name__ == "__main__":
    # Launch Gradio app. Share=False by default; set share=True if you want a public link.
    demo.launch()