File size: 5,790 Bytes
4736b82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""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()