"""app.py — Green SM KZ Relevance Space РЕЖИМ STUB (без ML-модели): все непустые тексты считаются релевантными. Когда обучена relevance-модель: 1. Раскомментировать блок «── Real model ──». 2. Удалить класс StubRelevanceService. 3. В get_service() заменить StubRelevanceService на RelevanceService. 4. Удалить этот комментарий. """ from __future__ import annotations from typing import List, Optional import gradio as gr # Нормализация казахского/русского текста (работает и в stub-режиме) from inference import clean_text # ── Real model (раскомментировать после обучения) ──────────────────────────── # import os # import torch # from inference import clean_text, load_relevance_model, infer_relevance # # RELEVANCE_REPO = "DanielNRU/GreenSMKZ-relevance" # USE_CUDA = os.getenv("USE_CUDA", "1") == "1" # # class RelevanceService: # def __init__(self, repo_id: str, use_cuda: bool = True): # device = torch.device("cuda" if use_cuda and torch.cuda.is_available() else "cpu") # self.model, self.tokenizer, self.max_length, self.threshold = \ # load_relevance_model(repo_id, device) # self.device = device # # def analyze_text(self, text: str, threshold: Optional[float] = None): # thr = threshold if threshold is not None else self.threshold # text_clean = clean_text(text) # preds, probs = infer_relevance( # [text_clean], self.model, self.tokenizer, # self.max_length, thr, self.device, batch_size=1, # ) # return int(preds[0]), float(probs[0]) # ───────────────────────────────────────────────────────────────────────────── class StubRelevanceService: """Заглушка: всегда возвращает (label=1, prob=1.0).""" threshold: float = 0.5 def analyze_text( self, text: str, threshold: Optional[float] = None, # noqa: ARG002 ) -> tuple[int, float]: return 1, 1.0 _service: Optional[StubRelevanceService] = None def get_service() -> StubRelevanceService: global _service if _service is None: _service = StubRelevanceService() return _service def _format_pct(p: float) -> str: return f"{p * 100:.1f}%" def analyze_single_text(text: str, rel_thr: float) -> tuple[str, str]: """Gradio endpoint. api_name=analyze_single_text (без слеша). Текст нормализуется через clean_text (RU/KZ-cyr/KZ-lat) перед обработкой. STUB: всегда возвращает ("релевантно", "100.0%") для непустого текста. """ if not text or not text.strip(): return "пустой текст", "0.0%" # Нормализация: удаление URL, перевод строк, KZ-лат → кириллица text_clean = clean_text(text) if not text_clean: return "пустой текст", "0.0%" try: svc = get_service() label, prob = svc.analyze_text(text_clean, threshold=rel_thr) label_str = "релевантно" if label == 1 else "нерелевантно" prob_str = _format_pct(prob) return label_str, prob_str except Exception as e: print(f"[ERROR] analyze_single_text / GSMKZ-relevance: {e}") return "ошибка", "0.0%" # ── Issue #222 / Шаг 8: батч-endpoint ──────────────────────────────────────── def analyze_batch(texts: List[str], rel_thr: float = 0.5) -> List[List[str]]: """Батч-анализ релевантности. Принимает список текстов, возвращает [[label_str, prob_str], ...]. Пустые строки получают ["нерелевантно", "0.0%"] без вызова модели. STUB: все непустые тексты → ["релевантно", "100.0%"]. Args: texts: список текстов (str), может содержать пустые строки. rel_thr: порог релевантности (игнорируется STUB-моделью). Returns: Список пар [[label_str, prob_str], ...] той же длины что и texts. """ svc = get_service() results: List[List[str]] = [] for text in texts: if not text or not str(text).strip(): results.append(["нерелевантно", "0.0%"]) continue try: text_clean = clean_text(str(text)) if not text_clean: results.append(["нерелевантно", "0.0%"]) continue label, prob = svc.analyze_text(text_clean, threshold=rel_thr) label_str = "релевантно" if label == 1 else "нерелевантно" results.append([label_str, _format_pct(prob)]) except Exception as e: print(f"[ERROR] analyze_batch / GSMKZ-relevance (item): {e}") results.append(["ошибка", "0.0%"]) return results # ───────────────────────────────────────────────────────────────────────────── _default_thr: float = StubRelevanceService.threshold with gr.Blocks(title="Green SM KZ — Релевантность [STUB]") as demo: gr.Markdown("# Green SM KZ — Определение релевантности сообщения") gr.Markdown( "> ⚠️ **STUB-режим**: ML-модель не используется. \n" "> Все непустые тексты автоматически получают `label=1`, `prob=100.0%`. \n" "> Заменить на реальную модель после обучения." ) gr.Markdown( "Поддерживаемые языки: **русский**, **казахский** (кириллица и латиница) \n" "Нормализация текста (KZ-лат → кириллица, удаление URL) активна в любом режиме." ) inp_text = gr.Textbox( label="Текст сообщения", placeholder="Вставьте сообщение на русском или казахском языке...", lines=8, ) rel_thr_slider = gr.Slider( minimum=0.0, maximum=1.0, value=_default_thr, step=0.01, label="Порог релевантности (игнорируется в stub-режиме)", interactive=False, ) btn = gr.Button("Анализировать") out_label = gr.Textbox( label="Метка (релевантно / нерелевантно)", interactive=False, ) out_prob = gr.Textbox( label="Вероятность релевантности", interactive=False, ) # api_name БЕЗ ведущего слеша — именно так вызывает ba_scraper btn.click( fn=analyze_single_text, inputs=[inp_text, rel_thr_slider], outputs=[out_label, out_prob], api_name="analyze_single_text", ) # Issue #222 / Шаг 8: регистрируем батч-endpoint через gr.api() # HFBatchSender вызывает: client.predict(texts, rel_thr, api_name="/analyze_batch") # Возвращает: [[label_str, prob_str], ...] demo.load(fn=None) # dummy — нужен только чтобы demo.fns были инициализированы 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)