Spaces:
Sleeping
Sleeping
| """ | |
| 🔬 NLP Insight Engine — Text Analytics Dashboard | |
| Sentiment · NER · Keywords · Summarisation · Language Detection · Text Comparison | |
| Built with HuggingFace Transformers + Gradio | |
| """ | |
| import gradio as gr | |
| import json | |
| from utils.pipeline import NLPPipeline | |
| from utils.helpers import EXAMPLE_TEXTS | |
| nlp = NLPPipeline() | |
| ENT_CSS = { | |
| "PER": ("#dbeafe", "#1e40af"), | |
| "ORG": ("#dcfce7", "#166534"), | |
| "LOC": ("#fef3c7", "#92400e"), | |
| "MISC": ("#f3e8ff", "#6b21a8"), | |
| } | |
| LANG_FLAGS = { | |
| "English": "🇬🇧", "Spanish": "🇪🇸", "French": "🇫🇷", "German": "🇩🇪", | |
| "Italian": "🇮🇹", "Portuguese": "🇵🇹", "Dutch": "🇳🇱", "Turkish": "🇹🇷", | |
| "Arabic": "🇸🇦", "Urdu": "🇵🇰", "Chinese": "🇨🇳", "Japanese": "🇯🇵", | |
| "Korean": "🇰🇷", "Cyrillic": "🇷🇺", "Devanagari": "🇮🇳", | |
| } | |
| def make_warning_banner(lang): | |
| """Generate a warning banner for non-English text.""" | |
| return ( | |
| f'<div style="background:#fef3c7;border:1px solid #f59e0b;border-radius:8px;' | |
| f'padding:0.8rem 1rem;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem;">' | |
| f'<span style="font-size:1.2rem;">⚠️</span>' | |
| f'<span style="color:#92400e;font-size:0.88rem;">' | |
| f'<strong>Non-English text detected ({lang}).</strong> ' | |
| f'Sentiment and NER results may be inaccurate as the models are trained on English text.' | |
| f'</span></div>' | |
| ) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # SINGLE TEXT ANALYSIS | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def analyse(text, sentiment_mode, do_sentiment, do_ner, do_keywords, do_summary): | |
| if not text or len(text.strip()) < 20: | |
| msg = "<p style='color:#ef4444;'>Please enter at least 20 characters of text.</p>" | |
| return msg, msg, msg, msg, msg, msg, "" | |
| text = text.strip() | |
| word_count = len(text.split()) | |
| sent_count = max(1, text.count(".") + text.count("!") + text.count("?")) | |
| mode = "financial" if "Financial" in sentiment_mode else "general" | |
| sent_result = None | |
| entities = None | |
| kw = None | |
| summary_text = None | |
| # ── Language Detection (always on) ─────────────────────────────────── | |
| lang_result = nlp.detect_language(text) | |
| lang = lang_result["language"] | |
| lang_conf = lang_result["confidence"] | |
| lang_script = lang_result["script"] | |
| flag = LANG_FLAGS.get(lang, "🌐") | |
| is_non_english = lang != "English" and lang != "Unknown" | |
| lang_html = ( | |
| f'<div style="background:linear-gradient(145deg,#f8fafc,#f1f5f9);border:1px solid #e2e8f0;' | |
| f'border-radius:12px;padding:1.2rem;text-align:center;margin-bottom:1rem;">' | |
| f'<div style="font-size:2.5rem;margin-bottom:0.3rem;">{flag}</div>' | |
| f'<div style="font-family:monospace;font-size:1.4rem;font-weight:700;color:#0f172a;">{lang}</div>' | |
| f'<div style="font-size:0.78rem;color:#64748b;text-transform:uppercase;margin-top:0.2rem;">Confidence: {lang_conf:.1%}</div>' | |
| f'<div style="font-size:0.75rem;color:#94a3b8;margin-top:0.3rem;">Script: {lang_script}</div>' | |
| f'</div>' | |
| ) | |
| warning = make_warning_banner(lang) if is_non_english else "" | |
| # ── Sentiment ──────────────────────────────────────────────────────── | |
| if do_sentiment: | |
| sent_result = nlp.analyse_sentiment(text, mode=mode) | |
| label = sent_result["label"] | |
| score = sent_result["score"] | |
| emoji = {"POSITIVE": "😊", "NEGATIVE": "😟", "NEUTRAL": "😐"}.get(label, "🤔") | |
| if mode == "financial": | |
| badge = ('<span style="display:inline-block;background:#fef3c7;color:#92400e;' | |
| 'padding:0.2rem 0.5rem;border-radius:4px;font-size:0.72rem;font-weight:600;' | |
| 'font-family:monospace;margin-top:0.4rem;">📊 FinBERT — trained on financial text</span>') | |
| else: | |
| badge = ('<span style="display:inline-block;background:#dbeafe;color:#1e40af;' | |
| 'padding:0.2rem 0.5rem;border-radius:4px;font-size:0.72rem;font-weight:600;' | |
| 'font-family:monospace;margin-top:0.4rem;">💬 DistilBERT — trained on movie reviews</span>') | |
| bars_html = "" | |
| if "details" in sent_result: | |
| for d in sent_result["details"]: | |
| lbl = d["label"].title() | |
| s = d["score"] | |
| color = {"Positive": "#22c55e", "Negative": "#ef4444", "Neutral": "#64748b"}.get(lbl, "#64748b") | |
| width = max(4, int(s * 100)) | |
| bars_html += ( | |
| f'<div style="display:flex;align-items:center;margin:0.4rem 0;">' | |
| f'<span style="min-width:80px;font-weight:600;color:#334155;">{lbl}</span>' | |
| f'<div style="flex:1;background:#f1f5f9;border-radius:6px;height:24px;margin:0 0.8rem;">' | |
| f'<div style="width:{width}%;background:{color};height:100%;border-radius:6px;"></div></div>' | |
| f'<span style="font-family:monospace;color:#64748b;font-size:0.85rem;">{s:.1%}</span></div>' | |
| ) | |
| sentiment_html = ( | |
| f'{warning}' | |
| f'<div style="background:linear-gradient(145deg,#f8fafc,#f1f5f9);border:1px solid #e2e8f0;' | |
| f'border-radius:12px;padding:1rem;text-align:center;margin-bottom:1rem;">' | |
| f'<div style="font-family:monospace;font-size:1.6rem;font-weight:700;color:#0f172a;">{emoji} {label.title()}</div>' | |
| f'<div style="font-size:0.78rem;color:#64748b;text-transform:uppercase;">Confidence: {score:.1%}</div>' | |
| f'{badge}</div>{bars_html}' | |
| ) | |
| else: | |
| sentiment_html = "<p style='color:#94a3b8;'>Sentiment analysis disabled.</p>" | |
| # ── NER ────────────────────────────────────────────────────────────── | |
| if do_ner: | |
| entities = nlp.extract_entities(text) | |
| ner_warning = warning if is_non_english else "" | |
| if entities: | |
| tags = "" | |
| for ent in entities: | |
| bg, fg = ENT_CSS.get(ent["entity_group"], ("#f1f5f9", "#334155")) | |
| tags += (f'<span style="display:inline-block;padding:0.25rem 0.6rem;border-radius:6px;' | |
| f'font-size:0.82rem;font-weight:600;margin:0.2rem;font-family:monospace;' | |
| f'background:{bg};color:{fg};">{ent["word"]} <small>({ent["entity_group"]})</small></span> ') | |
| rows = "".join( | |
| f'<tr style="border-bottom:1px solid #f1f5f9;">' | |
| f'<td style="padding:0.5rem 0.8rem;">{e["word"]}</td>' | |
| f'<td style="padding:0.5rem 0.8rem;">{e["entity_group"]}</td>' | |
| f'<td style="padding:0.5rem 0.8rem;font-family:monospace;">{e["score"]:.2%}</td></tr>' | |
| for e in entities | |
| ) | |
| table = (f'<table style="width:100%;border-collapse:collapse;margin-top:1rem;">' | |
| f'<thead><tr style="border-bottom:2px solid #e2e8f0;">' | |
| f'<th style="text-align:left;padding:0.5rem 0.8rem;color:#64748b;">Entity</th>' | |
| f'<th style="text-align:left;padding:0.5rem 0.8rem;color:#64748b;">Type</th>' | |
| f'<th style="text-align:left;padding:0.5rem 0.8rem;color:#64748b;">Confidence</th>' | |
| f'</tr></thead><tbody>{rows}</tbody></table>') | |
| ner_html = f'{ner_warning}<div style="margin-bottom:1rem;">{tags}</div>{table}' | |
| else: | |
| ner_html = f'{ner_warning}<p style="color:#94a3b8;">No named entities detected.</p>' | |
| else: | |
| ner_html = "<p style='color:#94a3b8;'>NER disabled.</p>" | |
| # ── Keywords ───────────────────────────────────────────────────────── | |
| if do_keywords: | |
| kw = nlp.extract_keywords(text) | |
| if kw: | |
| max_score = kw[0][1] | |
| bars = "" | |
| for word, score in kw[:12]: | |
| width = max(6, int((score / max_score) * 100)) | |
| bars += (f'<div style="display:flex;align-items:center;margin:0.35rem 0;font-family:monospace;font-size:0.85rem;">' | |
| f'<span style="color:#0f172a;min-width:120px;margin-right:0.5rem;">{word}</span>' | |
| f'<span style="height:22px;background:linear-gradient(90deg,#0ea5e9,#38bdf8);' | |
| f'border-radius:4px;margin-right:0.6rem;min-width:4px;width:{width}%;display:inline-block;"></span>' | |
| f'<span style="color:#64748b;font-size:0.78rem;">{score:.4f}</span></div>') | |
| keywords_html = bars | |
| else: | |
| keywords_html = "<p style='color:#94a3b8;'>No keywords extracted.</p>" | |
| else: | |
| keywords_html = "<p style='color:#94a3b8;'>Keyword extraction disabled.</p>" | |
| # ── Summary ────────────────────────────────────────────────────────── | |
| if do_summary: | |
| summary_text = nlp.summarise(text) | |
| summary_len = len(summary_text.split()) | |
| compression = (1 - summary_len / word_count) * 100 if word_count else 0 | |
| summary_html = ( | |
| f'<div style="background:#f0f9ff;border-left:4px solid #0ea5e9;padding:1rem 1.2rem;' | |
| f'border-radius:0 8px 8px 0;font-size:0.95rem;line-height:1.6;color:#0f172a;">{summary_text}</div>' | |
| f'<div style="color:#64748b;font-size:0.82rem;margin-top:0.5rem;font-family:monospace;">' | |
| f'{word_count} → {summary_len} words ({compression:.0f}% reduction)</div>' | |
| ) | |
| else: | |
| summary_html = "<p style='color:#94a3b8;'>Summarisation disabled.</p>" | |
| # ── Metrics ────────────────────────────────────────────────────────── | |
| sent_label = sent_result["label"].title() if sent_result else "—" | |
| sent_emoji = {"Positive": "😊", "Negative": "😟", "Neutral": "😐"}.get(sent_label, "—") | |
| ent_count = len(entities) if entities else 0 | |
| ms = ("background:linear-gradient(145deg,#f8fafc,#f1f5f9);border:1px solid #e2e8f0;" | |
| "border-radius:12px;padding:1rem;text-align:center;") | |
| vs = "font-family:monospace;font-size:1.6rem;font-weight:700;color:#0f172a;" | |
| ls = "font-size:0.78rem;color:#64748b;text-transform:uppercase;" | |
| metrics_html = ( | |
| f'<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:0.5rem;margin-bottom:0.5rem;">' | |
| f'<div style="{ms}"><div style="{vs}">{word_count:,}</div><div style="{ls}">Words</div></div>' | |
| f'<div style="{ms}"><div style="{vs}">{sent_count}</div><div style="{ls}">Sentences</div></div>' | |
| f'<div style="{ms}"><div style="{vs}">{sent_emoji} {sent_label}</div><div style="{ls}">Sentiment</div></div>' | |
| f'<div style="{ms}"><div style="{vs}">{ent_count}</div><div style="{ls}">Entities</div></div>' | |
| f'<div style="{ms}"><div style="{vs}">{flag} {lang}</div><div style="{ls}">Language</div></div>' | |
| f'</div>' | |
| ) | |
| # ── JSON export ────────────────────────────────────────────────────── | |
| export = {"word_count": word_count, "sentence_count": sent_count, "language": lang_result} | |
| if sent_result: | |
| export["sentiment"] = sent_result | |
| if entities: | |
| export["entities"] = entities | |
| if kw: | |
| export["keywords"] = kw | |
| if summary_text: | |
| export["summary"] = summary_text | |
| json_str = json.dumps(export, indent=2, default=str) | |
| return metrics_html, sentiment_html, ner_html, keywords_html, summary_html, lang_html, json_str | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # TEXT COMPARISON | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def compare(text_a, text_b, sentiment_mode): | |
| if not text_a or len(text_a.strip()) < 20: | |
| return "<p style='color:#ef4444;'>Text A needs at least 20 characters.</p>" | |
| if not text_b or len(text_b.strip()) < 20: | |
| return "<p style='color:#ef4444;'>Text B needs at least 20 characters.</p>" | |
| mode = "financial" if "Financial" in sentiment_mode else "general" | |
| result = nlp.compare_texts(text_a.strip(), text_b.strip(), mode=mode) | |
| a = result["text_a"] | |
| b = result["text_b"] | |
| shared = result["shared_keywords"] | |
| a_label = a["sentiment"]["label"].title() | |
| b_label = b["sentiment"]["label"].title() | |
| a_score = a["sentiment"]["score"] | |
| b_score = b["sentiment"]["score"] | |
| a_emoji = {"Positive": "😊", "Negative": "😟", "Neutral": "😐"}.get(a_label, "🤔") | |
| b_emoji = {"Positive": "😊", "Negative": "😟", "Neutral": "😐"}.get(b_label, "🤔") | |
| a_lang = a["language"]["language"] | |
| b_lang = b["language"]["language"] | |
| a_flag = LANG_FLAGS.get(a_lang, "🌐") | |
| b_flag = LANG_FLAGS.get(b_lang, "🌐") | |
| a_ents = len(a["entities"]) | |
| b_ents = len(b["entities"]) | |
| a_ent_words = {e["word"].lower() for e in a["entities"]} | |
| b_ent_words = {e["word"].lower() for e in b["entities"]} | |
| shared_ents = a_ent_words & b_ent_words | |
| # Non-English warnings for comparison | |
| comp_warnings = "" | |
| non_eng = [] | |
| if a_lang != "English" and a_lang != "Unknown": | |
| non_eng.append(f"Text A ({a_lang})") | |
| if b_lang != "English" and b_lang != "Unknown": | |
| non_eng.append(f"Text B ({b_lang})") | |
| if non_eng: | |
| comp_warnings = ( | |
| f'<div style="background:#fef3c7;border:1px solid #f59e0b;border-radius:8px;' | |
| f'padding:0.8rem 1rem;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem;">' | |
| f'<span style="font-size:1.2rem;">⚠️</span>' | |
| f'<span style="color:#92400e;font-size:0.88rem;">' | |
| f'<strong>Non-English text detected in {" and ".join(non_eng)}.</strong> ' | |
| f'Sentiment and NER results may be inaccurate as the models are trained on English text.' | |
| f'</span></div>' | |
| ) | |
| cs = ("background:linear-gradient(145deg,#f8fafc,#f1f5f9);border:1px solid #e2e8f0;" | |
| "border-radius:12px;padding:1rem;text-align:center;") | |
| hdr = "font-size:0.72rem;color:#94a3b8;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.5rem;" | |
| html = f""" | |
| {comp_warnings} | |
| <div style="display:grid;grid-template-columns:1fr auto 1fr;gap:1rem;align-items:start;"> | |
| <div> | |
| <div style="{hdr}">Text A</div> | |
| <div style="{cs}margin-bottom:0.8rem;"> | |
| <div style="font-family:monospace;font-size:1.3rem;font-weight:700;">{a_emoji} {a_label}</div> | |
| <div style="font-size:0.75rem;color:#64748b;">Confidence: {a_score:.1%}</div> | |
| </div> | |
| <div style="{cs}margin-bottom:0.8rem;"> | |
| <div style="font-family:monospace;font-size:1.1rem;font-weight:700;">{a_flag} {a_lang}</div> | |
| </div> | |
| <div style="{cs}margin-bottom:0.8rem;"> | |
| <div style="font-family:monospace;font-size:1.3rem;font-weight:700;">{a["word_count"]}</div> | |
| <div style="font-size:0.75rem;color:#64748b;">Words</div> | |
| </div> | |
| <div style="{cs}"> | |
| <div style="font-family:monospace;font-size:1.3rem;font-weight:700;">{a_ents}</div> | |
| <div style="font-size:0.75rem;color:#64748b;">Entities</div> | |
| </div> | |
| </div> | |
| <div style="display:flex;align-items:center;justify-content:center;height:100%;"> | |
| <div style="font-family:monospace;font-size:1.5rem;font-weight:700;color:#cbd5e1; | |
| background:#f8fafc;border-radius:50%;width:50px;height:50px; | |
| display:flex;align-items:center;justify-content:center;border:2px solid #e2e8f0;">VS</div> | |
| </div> | |
| <div> | |
| <div style="{hdr}">Text B</div> | |
| <div style="{cs}margin-bottom:0.8rem;"> | |
| <div style="font-family:monospace;font-size:1.3rem;font-weight:700;">{b_emoji} {b_label}</div> | |
| <div style="font-size:0.75rem;color:#64748b;">Confidence: {b_score:.1%}</div> | |
| </div> | |
| <div style="{cs}margin-bottom:0.8rem;"> | |
| <div style="font-family:monospace;font-size:1.1rem;font-weight:700;">{b_flag} {b_lang}</div> | |
| </div> | |
| <div style="{cs}margin-bottom:0.8rem;"> | |
| <div style="font-family:monospace;font-size:1.3rem;font-weight:700;">{b["word_count"]}</div> | |
| <div style="font-size:0.75rem;color:#64748b;">Words</div> | |
| </div> | |
| <div style="{cs}"> | |
| <div style="font-family:monospace;font-size:1.3rem;font-weight:700;">{b_ents}</div> | |
| <div style="font-size:0.75rem;color:#64748b;">Entities</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="margin-top:1.5rem;padding:1rem;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:12px;"> | |
| <div style="font-weight:700;color:#166534;margin-bottom:0.5rem;">🔗 Shared Keywords ({len(shared)})</div> | |
| <div>{"".join(f'<span style="display:inline-block;background:#dcfce7;color:#166534;padding:0.2rem 0.5rem;border-radius:4px;font-family:monospace;font-size:0.82rem;margin:0.2rem;">{kw}</span>' for kw in shared) if shared else '<span style="color:#94a3b8;">No shared keywords found</span>'}</div> | |
| </div> | |
| <div style="margin-top:0.8rem;padding:1rem;background:#eff6ff;border:1px solid #bfdbfe;border-radius:12px;"> | |
| <div style="font-weight:700;color:#1e40af;margin-bottom:0.5rem;">🏷️ Shared Entities ({len(shared_ents)})</div> | |
| <div>{"".join(f'<span style="display:inline-block;background:#dbeafe;color:#1e40af;padding:0.2rem 0.5rem;border-radius:4px;font-family:monospace;font-size:0.82rem;margin:0.2rem;">{e}</span>' for e in shared_ents) if shared_ents else '<span style="color:#94a3b8;">No shared entities found</span>'}</div> | |
| </div> | |
| """ | |
| return html | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # GRADIO UI | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| with gr.Blocks(title="NLP Insight Engine") as demo: | |
| gr.Markdown("# 🔬 NLP Insight Engine") | |
| gr.Markdown("*Sentiment · Entities · Keywords · Summarisation · Language Detection · Text Comparison*") | |
| with gr.Tabs(): | |
| with gr.Tab("📝 Analyse Text"): | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| text_input = gr.Textbox( | |
| label="Enter text to analyse", | |
| placeholder="Paste an article, review, report, or any text here…", | |
| lines=8, max_lines=20, | |
| ) | |
| examples = gr.Examples( | |
| examples=[[v] for v in EXAMPLE_TEXTS.values()], | |
| inputs=[text_input], | |
| label="Try an example", | |
| ) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### ⚙️ Options") | |
| sentiment_mode = gr.Radio( | |
| choices=["💬 General (movie reviews)", "📊 Financial (FinBERT)"], | |
| value="💬 General (movie reviews)", | |
| label="Sentiment Model", | |
| ) | |
| do_sentiment = gr.Checkbox(label="Sentiment Analysis", value=True) | |
| do_ner = gr.Checkbox(label="Named Entity Recognition", value=True) | |
| do_keywords = gr.Checkbox(label="Keyword Extraction", value=True) | |
| do_summary = gr.Checkbox(label="Extractive Summary", value=True) | |
| analyse_btn = gr.Button("🚀 Analyse", variant="primary", size="lg") | |
| metrics_output = gr.HTML(label="Quick Metrics") | |
| with gr.Tabs(): | |
| with gr.Tab("💬 Sentiment"): | |
| sentiment_output = gr.HTML() | |
| with gr.Tab("🏷️ Entities"): | |
| ner_output = gr.HTML() | |
| with gr.Tab("🔑 Keywords"): | |
| keywords_output = gr.HTML() | |
| with gr.Tab("📝 Summary"): | |
| summary_output = gr.HTML() | |
| with gr.Tab("🌍 Language"): | |
| lang_output = gr.HTML() | |
| with gr.Tab("📥 Export JSON"): | |
| json_output = gr.Textbox(label="Analysis Results (JSON)", lines=15) | |
| analyse_btn.click( | |
| fn=analyse, | |
| inputs=[text_input, sentiment_mode, do_sentiment, do_ner, do_keywords, do_summary], | |
| outputs=[metrics_output, sentiment_output, ner_output, keywords_output, summary_output, lang_output, json_output], | |
| ) | |
| with gr.Tab("⚖️ Compare Texts"): | |
| gr.Markdown("### Compare two texts side-by-side") | |
| gr.Markdown("*Analyse sentiment, entities, keywords, and language across two texts simultaneously.*") | |
| with gr.Row(): | |
| text_a = gr.Textbox(label="Text A", placeholder="Paste first text here…", lines=6) | |
| text_b = gr.Textbox(label="Text B", placeholder="Paste second text here…", lines=6) | |
| compare_mode = gr.Radio( | |
| choices=["💬 General (movie reviews)", "📊 Financial (FinBERT)"], | |
| value="💬 General (movie reviews)", | |
| label="Sentiment Model", | |
| ) | |
| compare_btn = gr.Button("⚖️ Compare", variant="primary", size="lg") | |
| compare_output = gr.HTML(label="Comparison Results") | |
| compare_btn.click( | |
| fn=compare, | |
| inputs=[text_a, text_b, compare_mode], | |
| outputs=[compare_output], | |
| ) | |
| gr.Markdown("**💡 Try comparing:** A positive product review vs a negative one, " | |
| "or a bullish financial article vs a bearish one.") | |
| gr.Markdown( | |
| "<center style='color:#94a3b8;font-size:0.78rem;'>" | |
| "NLP Insight Engine · Built by Wasif · " | |
| "Models: distilbert-sst2 · ProsusAI/finbert · dslim/bert-base-NER · " | |
| "100% free, no API keys" | |
| "</center>" | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(ssr_mode=False) | |