Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import joblib, json, re
|
| 3 |
+
from html import unescape
|
| 4 |
+
|
| 5 |
+
# -----------------------------
|
| 6 |
+
# 1) Точная копия preprocessor
|
| 7 |
+
# -----------------------------
|
| 8 |
+
_URL_RE = re.compile(r'https?://\S+|www\.\S+')
|
| 9 |
+
_TAG_RE = re.compile(r'[@#]\w+')
|
| 10 |
+
_NUM_RE = re.compile(r'\d+')
|
| 11 |
+
_WS_RE = re.compile(r'\s+')
|
| 12 |
+
|
| 13 |
+
def clean_text(s: str) -> str:
|
| 14 |
+
"""ДОЛЖНА совпадать с версией из обучения, иначе pickle не найдёт функцию."""
|
| 15 |
+
if not isinstance(s, str):
|
| 16 |
+
s = str(s)
|
| 17 |
+
s = unescape(s).lower()
|
| 18 |
+
s = _URL_RE.sub(' <url> ', s)
|
| 19 |
+
s = _TAG_RE.sub(' <tag> ', s)
|
| 20 |
+
s = _NUM_RE.sub(' <num> ', s)
|
| 21 |
+
s = s.replace('\n', ' ').replace('\t', ' ')
|
| 22 |
+
s = _WS_RE.sub(' ', s).strip()
|
| 23 |
+
return s
|
| 24 |
+
|
| 25 |
+
# ---------------------------------
|
| 26 |
+
# 2) Загрузка пайплайна и конфига
|
| 27 |
+
# ---------------------------------
|
| 28 |
+
# Важно: clean_text определён ДО загрузки, чтобы joblib смог десериализовать Vectorizer
|
| 29 |
+
PIPE = joblib.load("model.joblib")
|
| 30 |
+
|
| 31 |
+
DEFAULT_THRESHOLD = 0.4
|
| 32 |
+
try:
|
| 33 |
+
with open("config.json", "r", encoding="utf-8") as f:
|
| 34 |
+
cfg = json.load(f)
|
| 35 |
+
DEFAULT_THRESHOLD = float(cfg.get("threshold", DEFAULT_THRESHOLD))
|
| 36 |
+
except Exception:
|
| 37 |
+
pass # если файла нет — оставим дефолт 0.4
|
| 38 |
+
|
| 39 |
+
# ---------------------------------
|
| 40 |
+
# 3) Инференс
|
| 41 |
+
# ---------------------------------
|
| 42 |
+
def predict(comment: str, threshold: float):
|
| 43 |
+
if comment is None or not str(comment).strip():
|
| 44 |
+
return "Пустой ввод", 0.0
|
| 45 |
+
# В PIPE уже внутри есть preprocessor=clean_text, поэтому подаём сырой текст
|
| 46 |
+
proba = float(PIPE.predict_proba([comment])[0, 1])
|
| 47 |
+
label = "Токсичный" if proba >= threshold else "Не токсичный"
|
| 48 |
+
return label, round(proba, 4)
|
| 49 |
+
|
| 50 |
+
DESCRIPTION = """
|
| 51 |
+
Модель для классификации токсичных комментариев (русский язык).
|
| 52 |
+
Архитектура: **TF-IDF (char_wb 4–5) + Logistic Regression (L1, class_weight=balanced)**.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
demo = gr.Interface(
|
| 56 |
+
fn=predict,
|
| 57 |
+
inputs=[
|
| 58 |
+
gr.Textbox(label="Комментарий", lines=4, placeholder="Введите текст на русском..."),
|
| 59 |
+
gr.Slider(0.0, 1.0, value=DEFAULT_THRESHOLD, step=0.01, label="Порог классификации"),
|
| 60 |
+
],
|
| 61 |
+
outputs=[
|
| 62 |
+
gr.Textbox(label="Класс"),
|
| 63 |
+
gr.Number(label="Вероятность токсичности"),
|
| 64 |
+
],
|
| 65 |
+
title="Russian Toxic Comment Classifier — TF-IDF + Logistic Regression",
|
| 66 |
+
description=DESCRIPTION,
|
| 67 |
+
allow_flagging="never",
|
| 68 |
+
examples=[
|
| 69 |
+
["Ты полный идиот!"],
|
| 70 |
+
["Спасибо большое за помощь!"],
|
| 71 |
+
["Посмотри это <url> и скажи, что думаешь"]
|
| 72 |
+
],
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
if __name__ == "__main__":
|
| 76 |
+
demo.launch()
|