import os import gradio as gr import pandas as pd import numpy as np import json import plotly.express as px from huggingface_hub import InferenceClient # Load or download spaCy English model dynamically import spacy try: nlp = spacy.load("en_core_web_sm") except OSError: import spacy.cli spacy.cli.download("en_core_web_sm") nlp = spacy.load("en_core_web_sm") def load_data(file_obj): """Safely loads CSV, Excel, or TXT file into a Pandas DataFrame.""" if file_obj is None: return None, gr.update(choices=[], visible=False), "Please upload a file." file_path = file_obj.name ext = os.path.splitext(file_path)[1].lower() try: if ext == '.csv': df = pd.read_csv(file_path) elif ext in ['.xls', '.xlsx']: df = pd.read_excel(file_path) elif ext == '.txt': with open(file_path, 'r', encoding='utf-8') as f: content = f.read() df = pd.DataFrame({'text': [content]}) else: return None, gr.update(choices=[], visible=False), "Unsupported file format. Please upload .csv, .xlsx, or .txt." # Find object/string columns for dropdown string_cols = [col for col in df.columns if df[col].dtype == 'object' or df[col].astype(str).str.len().mean() > 5] if not string_cols: string_cols = list(df.columns) return df, gr.update(choices=string_cols, value=string_cols[0], visible=True), f"Successfully loaded dataset with {len(df)} rows." except Exception as e: return None, gr.update(choices=[], visible=False), f"Error loading file: {str(e)}" def get_highlighted_text(text, entities): """Helper to convert entities into Gradio's HighlightedText list-of-tuples format.""" # entities list of dicts: {"start": int, "end": int, "label": str} # Sort entities by start index entities = sorted(entities, key=lambda x: x["start"]) highlighted = [] last_idx = 0 for ent in entities: start, end, label = ent["start"], ent["end"], ent["label"] if start < last_idx: continue # Avoid overlapping issues if start > last_idx: highlighted.append((text[last_idx:start], None)) highlighted.append((text[start:end], label)) last_idx = end if last_idx < len(text): highlighted.append((text[last_idx:], None)) return highlighted def run_spacy_ner(text): """Runs local SpaCy NER on a single string.""" doc = nlp(text) entities = [] for ent in doc.ents: entities.append({ "text": ent.text, "label": ent.label_, "start": ent.start_char, "end": ent.end_char }) return entities def run_transformer_ner_api(text, hf_token, model_name): """Runs state-of-the-art transformer NER using student's personal HF token.""" if not hf_token: raise ValueError("Hugging Face API Token is required for Transformer Mode.") client = InferenceClient(token=hf_token) # We use HF Token Classification API try: # returns list of dicts: [{'entity_group': 'PER', 'score': 0.99, 'word': '...', 'start': 0, 'end': 5}] response = client.token_classification(text, model=model_name) except Exception as e: raise RuntimeError(f"Hugging Face Inference API error: {str(e)}") entities = [] for item in response: # Standardize labels from CONLL/standard formats label = item.get("entity_group", item.get("entity", "ENTITY")) if label.startswith("B-") or label.startswith("I-"): label = label[2:] # Strip BIO prefixes for clean visualization entities.append({ "text": item.get("word", ""), "label": label, "start": item.get("start", 0), "end": item.get("end", 0) }) return entities def analyze_ner(text_input, file_obj, text_col, method, hf_token, hf_model): # Determine the input documents docs = [] if file_obj is not None: df, _, _ = load_data(file_obj) if df is not None and text_col in df.columns: docs = df[text_col].astype(str).fillna("").tolist() elif text_input and text_input.strip(): docs = [text_input] if not docs: return None, None, None, None, "Please enter text or upload a valid dataset first." all_extracted = [] # Process documents for doc_idx, doc_text in enumerate(docs): try: if method == "spaCy (Local & Fast)": ents = run_spacy_ner(doc_text) else: ents = run_transformer_ner_api(doc_text, hf_token, hf_model) for e in ents: all_extracted.append({ "Doc_Index": doc_idx + 1, "Entity_Text": e["text"], "Label": e["label"], "Start_Char": e["start"], "End_Char": e["end"], "Context": f"...{doc_text[max(0, e['start']-30):min(len(doc_text), e['end']+30)]}..." }) except Exception as e: return None, None, None, None, f"Error processing row {doc_idx + 1}: {str(e)}" if not all_extracted: return ( [("No entities found in the text.", None)], pd.DataFrame(), None, None, "Analysis finished: No named entities were detected." ) df_ents = pd.DataFrame(all_extracted) # 1. Visualization format for the first document (to show beautiful color-highlighted text in UI) first_doc_text = docs[0] first_doc_ents = [e for e in all_extracted if e["Doc_Index"] == 1] # Standardize keys highlight_ents = [{"start": e["Start_Char"], "end": e["End_Char"], "label": e["Label"]} for e in first_doc_ents] highlighted_output = get_highlighted_text(first_doc_text, highlight_ents) # 2. Statistics Bar Chart label_counts = df_ents["Label"].value_counts().reset_index() label_counts.columns = ["Entity Type", "Count"] fig = px.bar( label_counts, x="Entity Type", y="Count", color="Entity Type", title="Distribution of Extracted Entity Types", template="plotly_dark" ) fig.update_layout(height=350, margin=dict(l=20, r=20, t=40, b=20)) # 3. Save export files csv_path = "extracted_entities.csv" json_path = "extracted_entities.json" df_ents.to_csv(csv_path, index=False) # Save formatted JSON with open(json_path, 'w', encoding='utf-8') as f: json.dump(all_extracted, f, indent=4, ensure_ascii=False) # Clean table for UI display df_table = df_ents[["Doc_Index", "Entity_Text", "Label", "Context"]].copy() return highlighted_output, df_table, fig, csv_path, json_path custom_css = """ body { background-color: #0b0f19; color: #f3f4f6; } .gradio-container { font-family: 'Inter', sans-serif !important; } h1, h2 { color: #6366f1 !important; } """ with gr.Blocks(theme=gr.themes.Default(primary_hue="indigo", secondary_hue="slate"), css=custom_css) as demo: df_state = gr.State() gr.HTML("""
Extract and analyze people, places, dates, and organizations from raw text or datasets. Runs locally on standard models, or unlocks state-of-the-art Transformer models using your personal Hugging Face Token.