wasifch2
Feature: add non-English warning banner for sentiment and NER
24d61ed
Raw
History Blame Contribute Delete
23.7 kB
"""
🔬 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)