"""app.py - Gradio UI for SentimentDetector (Hugging Face Space entry point).""" import gradio as gr import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np import pandas as pd from detector import SentimentDetector, TextProfile MAX_CHARS = 4000 MAX_EXPLAINED = 12 ICON = {"positive": "🟒", "negative": "πŸ”΄", "neutral": "βšͺ", "mixed": "🟑"} detector = SentimentDetector() EXAMPLES = [ ["I got into the coding club! So excited πŸŽ‰ Then I found out the first meeting is at 6 AM. " "Oh great, just what I needed... πŸ™„ Whatever, I'll survive. Actually, the seniors were super welcoming ☺️", True, True], ["Love waiting 2 hours for a delayed flight. Best. Airline. Ever. πŸ™ƒ", True, True], ["Just shipped my first ML project and it actually works! πŸš€πŸ”₯", True, True], ["Yeah right, because 'group projects' always go smoothly /s", True, True], ["Perfect. Just perfect. My laptop died right before the deadline.", True, True], ["The package arrived on Tuesday. Nothing special, nothing terrible.", True, True], ["I'm literally dying πŸ˜‚πŸ˜‚ this meme is too good", True, True], ] def summary_md(p: TextProfile) -> str: return ( f"### {ICON[p.overall_label]} Current mood: **{p.overall_label.title()}** ({p.overall:+.2f})\n\n" f"| Trend | Volatility | Sarcasm rate | Dominant emotion | Sarcasm-awareness shift |\n" f"|---|---|---|---|---|\n" f"| {p.trend} ({p.slope:+.2f}/sentence) | {p.volatility:.2f} | {p.sarcasm_rate:.0%} | " f"{p.dominant_emotion} | {p.sarcasm_shift:+.2f} |\n\n" f"*{len(p.sentences)} sentences, {p.emoji_count} emoji/emoticons. " f"β€œCurrent mood” weights recent sentences more heavily.*" ) def make_figure(p: TextProfile): n = len(p.sentences) x = np.arange(1, n + 1) lit = [r.literal for r in p.sentences] inn = [r.intended for r in p.sentences] fig, (a1, a2) = plt.subplots(1, 2, figsize=(10, 3.6), gridspec_kw={"width_ratios": [2, 1]}) a1.axhline(0, color="#999", lw=0.8) a1.plot(x, lit, "--o", color="#9ca3af", label="Literal (surface)") a1.plot(x, inn, "-o", color="#4f46e5", label="Intended") sx = [i + 1 for i, r in enumerate(p.sentences) if r.is_sarcastic] if sx: a1.scatter(sx, [inn[i - 1] for i in sx], s=200, facecolors="none", edgecolors="#dc2626", linewidths=2, label="Sarcasm/irony") a1.set_ylim(-1.05, 1.05) a1.set_xticks(x if n <= 15 else a1.get_xticks()) a1.set_xlabel("Sentence") a1.set_ylabel("Sentiment") a1.set_title("Sentiment trajectory") a1.legend(fontsize=8, loc="best") labels = list(p.emotion_mean) a2.barh(labels, [p.emotion_mean[k] for k in labels], color="#6366f1") a2.set_xlim(0, 1) a2.set_title("Average emotion") fig.tight_layout() return fig def to_dataframe(p: TextProfile) -> pd.DataFrame: rows = [] for i, r in enumerate(p.sentences, 1): signals = list(r.cues) rows.append({ "#": i, "Sentence": r.text, "Literal": round(r.literal, 2), "Intended": round(r.intended, 2), "Label": f"{ICON[r.label]} {r.label}", "Sarcasm": f"{r.sarcasm:.0%}", "Emoji": " ".join(r.emojis), "Emotion": r.emotion, "Signals": "; ".join(signals), }) return pd.DataFrame(rows) def explanation(p: TextProfile, emoji_aware: bool): tokens = [] for r in p.sentences[:MAX_EXPLAINED]: tokens += detector.explain(r.text, emoji_aware) + [("\n", None)] return tokens def analyze_ui(text, sarcasm_aware, emoji_aware): text = (text or "").strip() if not text: raise gr.Error("Please paste some text written by one person.") p = detector.analyze(text[:MAX_CHARS], sarcasm_aware=sarcasm_aware, emoji_aware=emoji_aware) return summary_md(p), make_figure(p), to_dataframe(p), explanation(p, emoji_aware) with gr.Blocks(title="SentimentDetector") as demo: gr.Markdown( "# 🎭 SentimentDetector\n" "Sentiment analysis that reads **irony, sarcasm and emojis** - not just words. " "Paste several sentences written by one person to see their sentiment trajectory.\n\n" "Toggle the switches to run an **ablation** and see what each component contributes." ) with gr.Row(): with gr.Column(scale=5): inp = gr.Textbox(lines=8, label="Text written by one person", placeholder="Oh great, another Monday. πŸ™„ ...") with gr.Row(): sarc = gr.Checkbox(True, label="Sarcasm-aware") emo = gr.Checkbox(True, label="Emoji-aware") btn = gr.Button("Analyze", variant="primary") gr.Examples(EXAMPLES, inputs=[inp, sarc, emo]) with gr.Column(scale=5): summary = gr.Markdown() plot = gr.Plot(label="Trajectory") table = gr.Dataframe(label="Per-sentence breakdown", interactive=False, wrap=True) hl = gr.HighlightedText( label="Which words drive the literal sentiment? (leave-one-word-out occlusion)", color_map={"pushes positive": "#86efac", "pushes negative": "#fca5a5"}, combine_adjacent=False, show_legend=True) btn.click(analyze_ui, [inp, sarc, emo], [summary, plot, table, hl]) gr.Markdown( "**Limitations.** English only; models were trained on tweets; sarcasm detection is inherently " "ambiguous and fused here from a classifier plus heuristic cues - treat outputs as probabilistic, " "not as ground truth. The word-influence view explains the *literal* model read, not the sarcasm layer. " "Do not use this to profile people without their consent." ) if __name__ == "__main__": detector.warmup() demo.launch()