import os import traceback from typing import Dict, List, Optional, Tuple, Any import gradio as gr from inference import ( clean_text, load_sentiment_model, infer_sentiment_single, _SENTIMENT_NEGATIVE_THRESHOLD, _SENTIMENT_POSITIVE_THRESHOLD, ) # ── Модель тональности Green SM KZ ────────────────────────────────────────── SENTIMENT_REPO = "DanielNRU/GreenSMKZ-sentiment" USE_CUDA_ENV = os.getenv("USE_CUDA", "1") USE_CUDA = USE_CUDA_ENV == "1" def normalize_tone_label(value: Any) -> str: if value is None: return "—" text = str(value).strip().lower() if not text or text in {"—", "-", "none", "null"}: return "—" if any(x in text for x in ["негатив", "negative", "neg"]): return "негатив" if any(x in text for x in ["нейтрал", "neutral", "neu"]): return "нейтрально" if any(x in text for x in ["позитив", "positive", "pos"]): return "позитив" if text in {"-1", "-1.0"}: return "негатив" if text in {"0", "0.0"}: return "нейтрально" if text in {"1", "1.0"}: return "позитив" return "—" def normalize_prob_value(v: Any) -> Optional[float]: try: x = float(v) except Exception: return None if x > 1.0: x = x / 100.0 if 0.0 <= x <= 1.0: return x return None def normalize_prob_dict(probs: Any) -> Dict[str, float]: result: Dict[str, float] = {} if not isinstance(probs, dict): return result for k, v in probs.items(): key = normalize_tone_label(k) if key == "—": continue val = normalize_prob_value(v) if val is None: continue result[key] = val return result def choose_tone_from_probs(tone_probs: Dict[str, float]) -> str: if not tone_probs: return "—" best_label = None best_prob = -1.0 for key in ["негатив", "нейтрально", "позитив"]: val = tone_probs.get(key) if val is not None and val > best_prob: best_prob = val best_label = key return best_label if best_label is not None else "—" def build_probs_block(tone_probs: Dict[str, float]) -> str: if not tone_probs: return "—" lines = [] for key in ["негатив", "нейтрально", "позитив"]: if key in tone_probs: try: lines.append(f"{key}: {tone_probs[key] * 100:.1f}%") except Exception: continue return "\n".join(lines) if lines else "—" class SentimentService: def __init__(self, sentiment_model_dir: str, use_cuda: bool = True): import torch device = torch.device("cuda" if use_cuda and torch.cuda.is_available() else "cpu") print(f"[INFO] SentimentService device: {device}") print(f"[INFO] Загружаем модель: {sentiment_model_dir}") self.model, self.tokenizer, self.max_length = load_sentiment_model( sentiment_model_dir, device ) self.device = device def analyze_text( self, text: str, negative_threshold: Optional[float] = None, positive_threshold: Optional[float] = None, ) -> Tuple[str, Dict[str, float]]: text_clean = clean_text(text) if not text_clean: return "—", {} try: res: Any = infer_sentiment_single( text_clean, self.model, self.tokenizer, self.max_length, self.device, negative_threshold=negative_threshold if negative_threshold is not None else _SENTIMENT_NEGATIVE_THRESHOLD, positive_threshold=positive_threshold if positive_threshold is not None else _SENTIMENT_POSITIVE_THRESHOLD, ) print(f"[DEBUG] infer_sentiment_single raw result: {res!r}") except Exception as e: print(f"[ERROR] infer_sentiment_single exception: {e}") traceback.print_exc() return "—", {} tone_str = "—" tone_probs: Dict[str, float] = {} if isinstance(res, dict): tone_str = normalize_tone_label(res.get("tone_str")) tone_probs = normalize_prob_dict(res.get("tone_probs")) if tone_str == "—" and tone_probs: tone_str = choose_tone_from_probs(tone_probs) elif isinstance(res, str): tone_str = normalize_tone_label(res) else: print(f"[WARN] infer_sentiment_single вернул неожиданный тип: {type(res)}") return "—", {} print(f"[DEBUG] normalized tone_str: {tone_str!r}") print(f"[DEBUG] normalized tone_probs: {tone_probs}") return tone_str, tone_probs _service: Optional[SentimentService] = None def get_service() -> SentimentService: global _service if _service is None: print("[INFO] Инициализация SentimentService (GSMKZ)...") _service = SentimentService(SENTIMENT_REPO, use_cuda=USE_CUDA) return _service def analyze_single_text(text: str, neg_threshold: float, pos_threshold: float): """Gradio endpoint. api_name=analyze_single_text (без слеша).""" text = text or "" if not text.strip(): return "—", "—" try: svc = get_service() tone_str, tone_probs = svc.analyze_text( text, negative_threshold=float(neg_threshold), positive_threshold=float(pos_threshold), ) except Exception as e: print(f"[ERROR] analyze_single_text / GSMKZ-sentiment: {e}") traceback.print_exc() return "—", "—" tone_short = normalize_tone_label(tone_str) if tone_short == "—" and tone_probs: tone_short = choose_tone_from_probs(tone_probs) probs_block = build_probs_block(tone_probs) print(f"[DEBUG] tone_short: {tone_short!r}") print(f"[DEBUG] probs_block:\n{probs_block}") return tone_short, probs_block # ── Issue #222 / Шаг 8: батч-endpoint ──────────────────────────────────────── def analyze_batch( texts: List[str], neg_thr: float = _SENTIMENT_NEGATIVE_THRESHOLD, pos_thr: float = _SENTIMENT_POSITIVE_THRESHOLD, ) -> List[List[str]]: """Батч-анализ тональности. Принимает список текстов, возвращает [[label_str, probs_block_str], ...]. Пустые строки получают ["нейтрально", "нейтрально: 100.0%"] без вызова модели. Args: texts: список текстов. neg_thr: порог для класса «негатив». pos_thr: порог для класса «позитив». Returns: Список пар [[tone_label, probs_block], ...] той же длины что и texts. """ svc = get_service() results: List[List[str]] = [] _neutral_fallback = ["нейтрально", "нейтрально: 100.0%\nнегативн: 0.0%\nпозитивн: 0.0%"] for text in texts: if not text or not str(text).strip(): results.append(_neutral_fallback) continue try: tone_str, tone_probs = svc.analyze_text( str(text), negative_threshold=float(neg_thr), positive_threshold=float(pos_thr), ) tone_short = normalize_tone_label(tone_str) if tone_short == "—" and tone_probs: tone_short = choose_tone_from_probs(tone_probs) probs_block = build_probs_block(tone_probs) results.append([tone_short, probs_block]) except Exception as e: print(f"[ERROR] analyze_batch / GSMKZ-sentiment (item): {e}") results.append(["ошибка", "—"]) return results # ───────────────────────────────────────────────────────────────────────────── with gr.Blocks(title="Green SM KZ — Тональность") as demo: gr.Markdown("# Green SM KZ — Определение тональности сообщения") gr.Markdown( "Модель: `DanielNRU/GreenSMKZ-sentiment` \n" "Поддерживаемые языки: **русский**, **казахский** (кириллица и латиница) \n" f"Дефолтный порог негатива: `{_SENTIMENT_NEGATIVE_THRESHOLD}` · " f"Дефолтный порог позитива: `{_SENTIMENT_POSITIVE_THRESHOLD}`" ) inp_text = gr.Textbox( label="Текст сообщения", placeholder="Вставьте сообщение на русском или казахском языке...", lines=8, ) neg_threshold_slider = gr.Slider( minimum=0.0, maximum=1.0, value=_SENTIMENT_NEGATIVE_THRESHOLD, step=0.01, label=f"Порог негатива (neg_threshold, default={_SENTIMENT_NEGATIVE_THRESHOLD})", ) pos_threshold_slider = gr.Slider( minimum=0.0, maximum=1.0, value=_SENTIMENT_POSITIVE_THRESHOLD, step=0.01, label=f"Порог позитива (pos_threshold, default={_SENTIMENT_POSITIVE_THRESHOLD})", ) btn = gr.Button("Анализировать") out_tone = gr.Textbox(label="Тональность", interactive=False) out_details = gr.Textbox( label="Подробные вероятности", interactive=False, lines=6, ) # api_name БЕЗ ведущего слеша — именно так вызывает ba_scraper btn.click( fn=analyze_single_text, inputs=[inp_text, neg_threshold_slider, pos_threshold_slider], outputs=[out_tone, out_details], api_name="analyze_single_text", ) # Issue #222 / Шаг 8: регистрируем батч-endpoint через gr.api() # HFBatchSender вызывает: client.predict(texts, neg_thr, pos_thr, api_name="/analyze_batch") # Возвращает: [[tone_label, probs_block], ...] gr.api( fn=analyze_batch, api_name="analyze_batch", ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)