File size: 6,084 Bytes
851d18f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
from __future__ import annotations

import json
from pathlib import Path

import gradio as gr
import pandas as pd
import torch
from huggingface_hub import hf_hub_download
from transformers import AutoModelForSequenceClassification, AutoTokenizer

MODEL_ID = "proxy3d/multi-motions-28"
MAX_LENGTH = 64

DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_ID)
model.to(DEVICE)
model.eval()

# Prefer the thresholds published with the model. Fall back to 0.5 if unavailable.
try:
    thresholds_path = hf_hub_download(
        repo_id=MODEL_ID,
        filename="thresholds.json",
        repo_type="model",
    )
    with open(thresholds_path, "r", encoding="utf-8") as f:
        THRESHOLDS = json.load(f)
except Exception:
    THRESHOLDS = {}

ID2LABEL = {
    int(k): v for k, v in model.config.id2label.items()
}
LABELS = [ID2LABEL[i] for i in range(len(ID2LABEL))]


def _predict_scores(text: str) -> list[tuple[str, float]]:
    text = (text or "").strip()
    if not text:
        return []

    encoded = tokenizer(
        text,
        return_tensors="pt",
        truncation=True,
        max_length=MAX_LENGTH,
    )
    encoded = {k: v.to(DEVICE) for k, v in encoded.items()}

    with torch.inference_mode():
        logits = model(**encoded).logits[0]
        probs = torch.sigmoid(logits).detach().cpu().float().tolist()

    return [(ID2LABEL[i], float(probs[i])) for i in range(len(probs))]


def predict(text: str):
    scores = _predict_scores(text)

    if not scores:
        empty = pd.DataFrame(columns=["emotion", "score", "threshold", "active"])
        return (
            "Введите текст / Enter text.",
            empty,
            empty,
        )

    rows = []
    active = []
    for label, score in scores:
        threshold = float(THRESHOLDS.get(label, 0.5))
        is_active = score >= threshold
        rows.append(
            {
                "emotion": label,
                "score": score,
                "threshold": threshold,
                "active": "✓" if is_active else "",
            }
        )
        if is_active:
            active.append((label, score))

    rows.sort(key=lambda x: x["score"], reverse=True)
    top_df = pd.DataFrame(rows[:10])
    all_df = pd.DataFrame(rows)

    if active:
        active.sort(key=lambda x: x[1], reverse=True)
        active_text = ", ".join(f"**{label}** ({score:.3f})" for label, score in active)
        summary = f"### Активные эмоции / Active labels\n{active_text}"
    else:
        top_label, top_score = rows[0]["emotion"], rows[0]["score"]
        summary = (
            "### Активные эмоции / Active labels\n"
            "Ни один класс не превысил свой tuned threshold. "
            f"Максимальный score: **{top_label}** ({top_score:.3f})."
        )

    return summary, top_df, all_df


EXAMPLES = [
    ["Я боюсь опоздать на рейс."],
    ["Мне наконец ответили — какое облегчение."],
    ["Спасибо, это действительно очень помогло."],
    ["Я совсем не понимаю, почему это произошло."],
    ["I'm worried I'll miss my flight."],
    ["Thank you so much, this really helped me."],
    ["I can't believe this actually happened!"],
    ["I'm disappointed, but I understand the decision."],
]


with gr.Blocks(title="Multi-Motions 28 — EN/RU Emotion Classifier") as demo:
    gr.Markdown(
        """
# Multi-Motions 28

**English + Russian · 28 GoEmotions classes · multi-label emotion classification**

Введите английский или русский текст. Модель возвращает полный вектор confidence scores,
а также labels, прошедшие индивидуальные tuned thresholds.

**Model:** [proxy3d/multi-motions-28](https://huggingface.co/proxy3d/multi-motions-28)  
**Author:** Ilya Zelenskiy (proxy3d) / Илья Зеленский (proxy3d)  
**Telegram:** [t.me/greenruff](https://t.me/greenruff)  

**Communication Styles LLM:**  
[Article](https://iproxy3d.github.io/communication-styles-llm/) ·
[GitHub](https://github.com/iproxy3d/communication-styles-llm)
        """
    )

    text = gr.Textbox(
        label="Text / Текст",
        placeholder="Введите сообщение на русском или английском...",
        lines=4,
    )
    run = gr.Button("Analyze / Анализировать", variant="primary")

    summary = gr.Markdown()

    gr.Markdown("### Top-10 scores")
    top_table = gr.Dataframe(
        headers=["emotion", "score", "threshold", "active"],
        datatype=["str", "number", "number", "str"],
        interactive=False,
    )

    with gr.Accordion("All 28 scores / Все 28 классов", open=False):
        all_table = gr.Dataframe(
            headers=["emotion", "score", "threshold", "active"],
            datatype=["str", "number", "number", "str"],
            interactive=False,
        )

    gr.Examples(
        examples=EXAMPLES,
        inputs=text,
        label="Examples / Примеры",
    )

    gr.Markdown(
        """
---
### Notes

- Scores are sigmoid outputs for all 28 GoEmotions labels.
- This is a **multi-label** classifier: more than one emotion may be active.
- The `active` column uses the per-label thresholds published with the model.
- Native-Russian transfer was additionally evaluated on CEDR and SemEval-2025 RU.

For implementation details, benchmarks and usage examples, see the
[model page](https://huggingface.co/proxy3d/multi-motions-28).
        """
    )

    run.click(
        fn=predict,
        inputs=text,
        outputs=[summary, top_table, all_table],
    )
    text.submit(
        fn=predict,
        inputs=text,
        outputs=[summary, top_table, all_table],
    )

if __name__ == "__main__":
    demo.launch()