Jomaric commited on
Commit
a261aeb
·
0 Parent(s):

initial release

Browse files
Files changed (3) hide show
  1. README.md +24 -0
  2. app.py +314 -0
  3. requirements.txt +5 -0
README.md ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Interactive Named Entity Recognizer
3
+ emoji: 🏷️
4
+ colorFrom: purple
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ python_version: "3.10"
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # Interactive Named Entity Recognizer (NER)
14
+
15
+ This is a premium, lightweight, interactive Named Entity Recognition tool designed specifically for computational social science and humanities courses.
16
+
17
+ ## Features
18
+ - **Dual Inputs**: Paste raw text directly or upload datasets (`.csv`, `.xlsx`, or `.txt`).
19
+ - **Flexible Models**:
20
+ - **spaCy (Local & Fast)**: Runs high-speed extraction using the standard English model (`en_core_web_sm`) locally on CPU.
21
+ - **Transformers (API Mode)**: Leverages advanced deep learning models (like BERT-NER) through Hugging Face's Serverless API with your personal access token.
22
+ - **Beautiful Visual Color-Highlighting**: Leverages Gradio's native highlighted text component to render custom color backings for people, locations, and organizations.
23
+ - **Statistics & Charts**: Visualizes entity type distribution in a dark-themed Plotly chart.
24
+ - **Dual Format Exports**: Download full data reports in either **CSV** or **JSON** format.
app.py ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import pandas as pd
4
+ import numpy as np
5
+ import json
6
+ import plotly.express as px
7
+ from huggingface_hub import InferenceClient
8
+
9
+ # Load or download spaCy English model dynamically
10
+ import spacy
11
+ try:
12
+ nlp = spacy.load("en_core_web_sm")
13
+ except OSError:
14
+ import spacy.cli
15
+ spacy.cli.download("en_core_web_sm")
16
+ nlp = spacy.load("en_core_web_sm")
17
+
18
+ def load_data(file_obj):
19
+ """Safely loads CSV, Excel, or TXT file into a Pandas DataFrame."""
20
+ if file_obj is None:
21
+ return None, gr.update(choices=[], visible=False), "Please upload a file."
22
+
23
+ file_path = file_obj.name
24
+ ext = os.path.splitext(file_path)[1].lower()
25
+
26
+ try:
27
+ if ext == '.csv':
28
+ df = pd.read_csv(file_path)
29
+ elif ext in ['.xls', '.xlsx']:
30
+ df = pd.read_excel(file_path)
31
+ elif ext == '.txt':
32
+ with open(file_path, 'r', encoding='utf-8') as f:
33
+ content = f.read()
34
+ df = pd.DataFrame({'text': [content]})
35
+ else:
36
+ return None, gr.update(choices=[], visible=False), "Unsupported file format. Please upload .csv, .xlsx, or .txt."
37
+
38
+ # Find object/string columns for dropdown
39
+ string_cols = [col for col in df.columns if df[col].dtype == 'object' or df[col].astype(str).str.len().mean() > 5]
40
+ if not string_cols:
41
+ string_cols = list(df.columns)
42
+
43
+ return df, gr.update(choices=string_cols, value=string_cols[0], visible=True), f"Successfully loaded dataset with {len(df)} rows."
44
+ except Exception as e:
45
+ return None, gr.update(choices=[], visible=False), f"Error loading file: {str(e)}"
46
+
47
+ def get_highlighted_text(text, entities):
48
+ """Helper to convert entities into Gradio's HighlightedText list-of-tuples format."""
49
+ # entities list of dicts: {"start": int, "end": int, "label": str}
50
+ # Sort entities by start index
51
+ entities = sorted(entities, key=lambda x: x["start"])
52
+
53
+ highlighted = []
54
+ last_idx = 0
55
+ for ent in entities:
56
+ start, end, label = ent["start"], ent["end"], ent["label"]
57
+ if start < last_idx:
58
+ continue # Avoid overlapping issues
59
+ if start > last_idx:
60
+ highlighted.append((text[last_idx:start], None))
61
+ highlighted.append((text[start:end], label))
62
+ last_idx = end
63
+ if last_idx < len(text):
64
+ highlighted.append((text[last_idx:], None))
65
+ return highlighted
66
+
67
+ def run_spacy_ner(text):
68
+ """Runs local SpaCy NER on a single string."""
69
+ doc = nlp(text)
70
+ entities = []
71
+ for ent in doc.ents:
72
+ entities.append({
73
+ "text": ent.text,
74
+ "label": ent.label_,
75
+ "start": ent.start_char,
76
+ "end": ent.end_char
77
+ })
78
+ return entities
79
+
80
+ def run_transformer_ner_api(text, hf_token, model_name):
81
+ """Runs state-of-the-art transformer NER using student's personal HF token."""
82
+ if not hf_token:
83
+ raise ValueError("Hugging Face API Token is required for Transformer Mode.")
84
+
85
+ client = InferenceClient(token=hf_token)
86
+
87
+ # We use HF Token Classification API
88
+ try:
89
+ # returns list of dicts: [{'entity_group': 'PER', 'score': 0.99, 'word': '...', 'start': 0, 'end': 5}]
90
+ response = client.token_classification(text, model=model_name)
91
+ except Exception as e:
92
+ raise RuntimeError(f"Hugging Face Inference API error: {str(e)}")
93
+
94
+ entities = []
95
+ for item in response:
96
+ # Standardize labels from CONLL/standard formats
97
+ label = item.get("entity_group", item.get("entity", "ENTITY"))
98
+ if label.startswith("B-") or label.startswith("I-"):
99
+ label = label[2:] # Strip BIO prefixes for clean visualization
100
+
101
+ entities.append({
102
+ "text": item.get("word", ""),
103
+ "label": label,
104
+ "start": item.get("start", 0),
105
+ "end": item.get("end", 0)
106
+ })
107
+ return entities
108
+
109
+ def analyze_ner(text_input, file_obj, text_col, method, hf_token, hf_model):
110
+ # Determine the input documents
111
+ docs = []
112
+ if file_obj is not None:
113
+ df, _, _ = load_data(file_obj)
114
+ if df is not None and text_col in df.columns:
115
+ docs = df[text_col].astype(str).fillna("").tolist()
116
+ elif text_input and text_input.strip():
117
+ docs = [text_input]
118
+
119
+ if not docs:
120
+ return None, None, None, None, "Please enter text or upload a valid dataset first."
121
+
122
+ all_extracted = []
123
+
124
+ # Process documents
125
+ for doc_idx, doc_text in enumerate(docs):
126
+ try:
127
+ if method == "spaCy (Local & Fast)":
128
+ ents = run_spacy_ner(doc_text)
129
+ else:
130
+ ents = run_transformer_ner_api(doc_text, hf_token, hf_model)
131
+
132
+ for e in ents:
133
+ all_extracted.append({
134
+ "Doc_Index": doc_idx + 1,
135
+ "Entity_Text": e["text"],
136
+ "Label": e["label"],
137
+ "Start_Char": e["start"],
138
+ "End_Char": e["end"],
139
+ "Context": f"...{doc_text[max(0, e['start']-30):min(len(doc_text), e['end']+30)]}..."
140
+ })
141
+ except Exception as e:
142
+ return None, None, None, None, f"Error processing row {doc_idx + 1}: {str(e)}"
143
+
144
+ if not all_extracted:
145
+ return (
146
+ [("No entities found in the text.", None)],
147
+ pd.DataFrame(),
148
+ None, None, "Analysis finished: No named entities were detected."
149
+ )
150
+
151
+ df_ents = pd.DataFrame(all_extracted)
152
+
153
+ # 1. Visualization format for the first document (to show beautiful color-highlighted text in UI)
154
+ first_doc_text = docs[0]
155
+ first_doc_ents = [e for e in all_extracted if e["Doc_Index"] == 1]
156
+ # Standardize keys
157
+ highlight_ents = [{"start": e["Start_Char"], "end": e["End_Char"], "label": e["Label"]} for e in first_doc_ents]
158
+ highlighted_output = get_highlighted_text(first_doc_text, highlight_ents)
159
+
160
+ # 2. Statistics Bar Chart
161
+ label_counts = df_ents["Label"].value_counts().reset_index()
162
+ label_counts.columns = ["Entity Type", "Count"]
163
+ fig = px.bar(
164
+ label_counts,
165
+ x="Entity Type",
166
+ y="Count",
167
+ color="Entity Type",
168
+ title="Distribution of Extracted Entity Types",
169
+ template="plotly_dark"
170
+ )
171
+ fig.update_layout(height=350, margin=dict(l=20, r=20, t=40, b=20))
172
+
173
+ # 3. Save export files
174
+ csv_path = "extracted_entities.csv"
175
+ json_path = "extracted_entities.json"
176
+
177
+ df_ents.to_csv(csv_path, index=False)
178
+
179
+ # Save formatted JSON
180
+ with open(json_path, 'w', encoding='utf-8') as f:
181
+ json.dump(all_extracted, f, indent=4, ensure_ascii=False)
182
+
183
+ # Clean table for UI display
184
+ df_table = df_ents[["Doc_Index", "Entity_Text", "Label", "Context"]].copy()
185
+
186
+ return highlighted_output, df_table, fig, csv_path, json_path
187
+
188
+ custom_css = """
189
+ body {
190
+ background-color: #0b0f19;
191
+ color: #f3f4f6;
192
+ }
193
+ .gradio-container {
194
+ font-family: 'Inter', sans-serif !important;
195
+ }
196
+ h1, h2 {
197
+ color: #6366f1 !important;
198
+ }
199
+ """
200
+
201
+ with gr.Blocks(theme=gr.themes.Default(primary_hue="indigo", secondary_hue="slate"), css=custom_css) as demo:
202
+ df_state = gr.State()
203
+
204
+ gr.HTML("""
205
+ <div style="text-align: center; margin-bottom: 2rem;">
206
+ <h1 style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem; background: linear-gradient(to right, #6366f1, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Interactive Named Entity Recognizer</h1>
207
+ <p style="font-size: 1.1rem; color: #94a3b8; max-width: 800px; margin: 0 auto;">
208
+ Extract and analyze people, places, dates, and organizations from raw text or datasets.
209
+ Runs locally on standard models, or unlocks state-of-the-art Transformer models using your personal Hugging Face Token.
210
+ </p>
211
+ </div>
212
+ """)
213
+
214
+ with gr.Row():
215
+ # Left Panel: Input controls
216
+ with gr.Column(scale=1):
217
+ gr.Markdown("### 1. Choose Input Source")
218
+ with gr.Tabs():
219
+ with gr.TabItem("Paste Raw Text"):
220
+ text_input = gr.Textbox(
221
+ label="Source Text",
222
+ placeholder="Paste your text here (e.g., 'Apple Inc. was founded by Steve Jobs in Cupertino, California...').",
223
+ lines=10
224
+ )
225
+ with gr.TabItem("Upload Dataset File"):
226
+ file_input = gr.File(label="Upload (.csv, .xlsx, .txt)", file_types=[".csv", ".xlsx", ".txt"])
227
+ text_column_selector = gr.Dropdown(
228
+ label="Target Text Column",
229
+ choices=[],
230
+ visible=False,
231
+ interactive=True
232
+ )
233
+ status_text = gr.Markdown("No file uploaded yet.")
234
+
235
+ gr.Markdown("### 2. Configure Model")
236
+ method_selector = gr.Radio(
237
+ choices=["spaCy (Local & Fast)", "Transformers (API Mode)"],
238
+ value="spaCy (Local & Fast)",
239
+ label="Extraction Model"
240
+ )
241
+
242
+ with gr.Group() as token_group:
243
+ hf_token_input = gr.Textbox(
244
+ label="Hugging Face API Token",
245
+ placeholder="hf_...",
246
+ type="password",
247
+ visible=False,
248
+ info="Required to call advanced transformer models. Get one free at huggingface.co."
249
+ )
250
+ hf_model_input = gr.Dropdown(
251
+ choices=[
252
+ "dbmdz/bert-large-cased-finetuned-conll03-english",
253
+ "dslim/bert-base-NER",
254
+ "Babelscape/wikineural-multilingual-ner"
255
+ ],
256
+ value="dbmdz/bert-large-cased-finetuned-conll03-english",
257
+ label="Transformer Model (HF API)",
258
+ visible=False
259
+ )
260
+
261
+ run_btn = gr.Button("Extract Entities", variant="primary")
262
+
263
+ # Right Panel: Results
264
+ with gr.Column(scale=2):
265
+ gr.Markdown("### 3. Extracted Named Entities")
266
+
267
+ with gr.Tabs():
268
+ with gr.TabItem("Visual Color-Highlighting"):
269
+ highlighted_output = gr.HighlightedText(
270
+ label="First Document Entity Highlight",
271
+ combine_adjacent=False
272
+ )
273
+ with gr.TabItem("Full Analysis Table"):
274
+ table_output = gr.Dataframe(
275
+ headers=["Doc_Index", "Entity_Text", "Label", "Context"],
276
+ datatype=["number", "str", "str", "str"],
277
+ interactive=False,
278
+ wrap=True
279
+ )
280
+ with gr.TabItem("Statistics Chart"):
281
+ chart_output = gr.Plot(label="Entity Frequency Plot")
282
+
283
+ gr.Markdown("### 4. Export & Download")
284
+ with gr.Row():
285
+ download_csv = gr.File(label="Download CSV Report")
286
+ download_json = gr.File(label="Download JSON Report")
287
+
288
+ # Show/hide token field depending on model
289
+ def toggle_method_fields(method):
290
+ if method == "Transformers (API Mode)":
291
+ return gr.update(visible=True), gr.update(visible=True)
292
+ else:
293
+ return gr.update(visible=False), gr.update(visible=False)
294
+
295
+ method_selector.change(
296
+ fn=toggle_method_fields,
297
+ inputs=method_selector,
298
+ outputs=[hf_token_input, hf_model_input]
299
+ )
300
+
301
+ file_input.change(
302
+ fn=load_data,
303
+ inputs=file_input,
304
+ outputs=[df_state, text_column_selector, status_text]
305
+ )
306
+
307
+ run_btn.click(
308
+ fn=analyze_ner,
309
+ inputs=[text_input, file_input, text_column_selector, method_selector, hf_token_input, hf_model_input],
310
+ outputs=[highlighted_output, table_output, chart_output, download_csv, download_json]
311
+ )
312
+
313
+ if __name__ == "__main__":
314
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ pandas
2
+ numpy
3
+ spacy
4
+ plotly
5
+ openpyxl