dnkdm commited on
Commit
aeee61d
·
1 Parent(s): a7158ed

Add NER application with Gradio interface

Browse files
Files changed (3) hide show
  1. README.md +175 -7
  2. app.py +596 -0
  3. requirements.txt +4 -0
README.md CHANGED
@@ -1,14 +1,182 @@
1
  ---
2
- title: Russian Ner
3
- emoji: 🐠
4
- colorFrom: yellow
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.3.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: 'NER для русского текста: извлечение ФИО, организаций, локаци'
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Russian NER - Извлечение именованных сущностей
3
+ emoji: 🏷️
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: 4.0.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
 
11
  ---
12
 
13
+ # 🏷️ Russian NER Извлечение именованных сущностей
14
+
15
+ Веб-приложение для автоматического извлечения именованных сущностей (Named Entity Recognition) из текстов на русском языке.
16
+
17
+ ## 📋 Описание задачи
18
+
19
+ **Named Entity Recognition (NER)** — это задача извлечения и классификации именованных сущностей в тексте. Приложение распознаёт следующие типы сущностей:
20
+
21
+ | Тип | Описание | Примеры |
22
+ |-----|----------|---------|
23
+ | **PER** | Персоны (ФИО) | Владимир Путин, Иван Петров |
24
+ | **ORG** | Организации | Яндекс, Сбербанк, МГУ |
25
+ | **LOC** | Локации (места) | Москва, Россия, Невский проспект |
26
+ | **MISC** | Прочее | Названия событий, продуктов и т.д. |
27
+
28
+ ## 🤖 Выбранные модели
29
+
30
+ ### Модель 1: WikiNEuRal (multilingual)
31
+ - **Hugging Face:** [Babelscape/wikineural-multilingual-ner](https://huggingface.co/Babelscape/wikineural-multilingual-ner)
32
+ - **Архитектура:** mBERT (multilingual BERT)
33
+ - **Почему выбрана:**
34
+ - Поддержка 9 языков, включая русский
35
+ - Распознаёт все 4 типа сущностей (PER, ORG, LOC, MISC)
36
+ - 457K+ загрузок — проверенная сообществом
37
+ - Хорошо работает на текстах общей тематики
38
+
39
+ ### Модель 2: XLM-RoBERTa NER
40
+ - **Hugging Face:** [Davlan/xlm-roberta-base-ner-hrl](https://huggingface.co/Davlan/xlm-roberta-base-ner-hrl)
41
+ - **Архитектура:** XLM-RoBERTa
42
+ - **Почему выбрана:**
43
+ - Сильная база (XLM-RoBERTa) для славянских языков
44
+ - Хорошая точность на именах и организациях
45
+ - Альтернатива для сравнения результатов
46
+
47
+ ## ✨ Функциональность
48
+
49
+ ### Базовые функции
50
+ - ✅ Ввод текста с ограничением 2000 символов
51
+ - ✅ Извлечение сущностей с указанием типа и уверенности
52
+ - ✅ Визуальная подсветка сущностей в тексте (цветовая маркировка)
53
+ - ✅ Блок примеров для быстрого тестирования
54
+ - ✅ Корректная обработка ошибок
55
+
56
+ ### Расширенные функции (на "отлично")
57
+ - ✅ **Переключатель моделей** — выбор из 2 моделей через Dropdown
58
+ - ✅ **Сравнение моделей** — side-by-side результаты обеих моделей
59
+ - ✅ **Измерение latency** — отображение времени обработки в миллисекундах
60
+ - ✅ **История запросов** — последние 10 запросов с результатами
61
+ - ✅ **Пакетная обработка** — загрузка CSV/TXT и выдача результатов таблицей
62
+
63
+ ## 📊 Примеры работы
64
+
65
+ ### Вход:
66
+ ```
67
+ Владимир Путин встретился с президентом Франции Эммануэлем Макроном в Москве для обсуждения вопросов безопасности.
68
+ ```
69
+
70
+ ### Выход:
71
+ | Текст | Тип | Описание | Уверенность |
72
+ |-------|-----|----------|-------------|
73
+ | Владимир Путин | PER | Персона (ФИО) | 99.2% |
74
+ | Франции | LOC | Локация (место) | 98.7% |
75
+ | Эммануэлем Макроном | PER | Персона (ФИО) | 98.9% |
76
+ | Москве | LOC | Локация (место) | 99.1% |
77
+
78
+ ### Подсветка в тексте:
79
+ - 🔵 **Владимир Путин** — PER
80
+ - 🔵 **Эммануэлем Макроном** — PER
81
+ - 🟠 **Франции** — LOC
82
+ - 🟠 **Москве** — LOC
83
+
84
+ ---
85
+
86
+ ### Вход:
87
+ ```
88
+ Компания Яндекс открыла новый офис в Санкт-Петербурге рядом с Невским проспектом.
89
+ ```
90
+
91
+ ### Выход:
92
+ | Текст | Тип | Описание | Уверенность |
93
+ |-------|-----|----------|-------------|
94
+ | Яндекс | ORG | Организация | 98.5% |
95
+ | Санкт-Петербурге | LOC | Локация (место) | 99.3% |
96
+ | Невским проспектом | LOC | Локация (место) | 97.8% |
97
+
98
+ ## ⚠️ Ограничения решения
99
+
100
+ ### Технические ограничения
101
+ - **CPU-режим:** Приложение работает без GPU для совместимости с бесплатным Hugging Face Spaces
102
+ - **Лимит текста:** Максимум 2000 символов на один запрос
103
+ - **Лимит пакетной обработки:** Максимум 100 строк в файле
104
+ - **Время загрузки:** Первый запрос может занять 30-60 секунд (загрузка модели)
105
+
106
+ ### Ограничения моделей
107
+ - Модели обучены на Wikipedia и новостных текстах — могут хуже работать на сленге, диалектах
108
+ - Редкие имена и новые организации могут не распознаваться
109
+ - Сложные случаи (омонимия, сокращения) могут давать ошибки
110
+ - MISC-категория может быть неточной
111
+
112
+ ### Примеры сложных случаев
113
+ | Текст | Проблема |
114
+ |-------|----------|
115
+ | "Петров выиграл Петрова" | Омонимия: фамилия vs название турнира |
116
+ | "ВТБ" | Сокращения могут не распознаваться |
117
+ | "пойти в яндекс" | Неформальное написание |
118
+
119
+ ## 🚀 Как использовать
120
+
121
+ ### Локальный запуск
122
+ ```bash
123
+ # Клонировать репозиторий
124
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/russian-ner
125
+
126
+ # Установить зависимости
127
+ pip install -r requirements.txt
128
+
129
+ # Запустить приложение
130
+ python app.py
131
+ ```
132
+
133
+ ### Пакетная обработка
134
+ 1. Подготовьте CSV-файл с колонкой `text`:
135
+ ```csv
136
+ text
137
+ "Иван Петров работает в Яндексе."
138
+ "Мария Сидорова живёт в Казани."
139
+ ```
140
+
141
+ 2. Или TXT-файл (каждая строка — отдельный текст):
142
+ ```
143
+ Иван Петров работает в Яндексе.
144
+ Мария Сидорова живёт в Казани.
145
+ ```
146
+
147
+ 3. Загрузите файл во вкладке "Пакетная обработка"
148
+
149
+ ## 🔒 Правила безопасного использования
150
+
151
+ ⚠️ **ВАЖНО: Не вводите реальные персональные данные!**
152
+
153
+ - Это демонстрационное приложение
154
+ - Данные не сохраняются на сервере, но проходят через модели Hugging Face
155
+ - Для обработки конфиденциальных данных используйте локальный запуск
156
+ - Не используйте для обработки паспортных данных, медицинских записей и т.п.
157
+
158
+ ## 📁 Структура проекта
159
+
160
+ ```
161
+ aimod/
162
+ ├── app.py # Главное Gradio-приложение
163
+ ├── requirements.txt # Зависимости Python
164
+ └── README.md # Документация (этот файл)
165
+ ```
166
+
167
+ ## 🛠️ Технологии
168
+
169
+ - **Gradio** — веб-интерфейс
170
+ - **Transformers** — работа с моделями NLP
171
+ - **PyTorch** — бэкенд для моделей
172
+ - **Pandas** — обработка табличных данных
173
+
174
+ ## 📚 Ссылки
175
+
176
+ - [Hugging Face Transformers](https://huggingface.co/docs/transformers)
177
+ - [Gradio Documentation](https://gradio.app/docs/)
178
+ - [WikiNEuRal Paper](https://aclanthology.org/2021.findings-emnlp.215/)
179
+
180
+ ## 📝 Лицензия
181
+
182
+ MIT License
app.py ADDED
@@ -0,0 +1,596 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NER-приложение для извлечения именованных сущностей из русского текста.
3
+ Hugging Face Spaces + Gradio
4
+
5
+ Автор: Студент
6
+ Задача: Извлечение сущностей (ФИО, организации, города)
7
+ """
8
+
9
+ import time
10
+ import gradio as gr
11
+ from transformers import pipeline
12
+ import pandas as pd
13
+ from collections import deque
14
+ from datetime import datetime
15
+ import io
16
+
17
+ # ============== КОНСТАНТЫ ==============
18
+ MAX_CHARS = 2000
19
+ MAX_BATCH_ROWS = 100
20
+ HISTORY_SIZE = 10
21
+
22
+ # Доступные модели NER
23
+ MODELS = {
24
+ "WikiNEuRal (multilingual)": "Babelscape/wikineural-multilingual-ner",
25
+ "XLM-RoBERTa NER": "Davlan/xlm-roberta-base-ner-hrl"
26
+ }
27
+
28
+ # Цветовая схема для подсветки сущностей
29
+ COLOR_MAP = {
30
+ "PER": "#3b82f6", # Синий — персоны
31
+ "ORG": "#22c55e", # Зелёный — организации
32
+ "LOC": "#f97316", # Оранжевый — локации
33
+ "MISC": "#a855f7" # Фиолетовый — прочее
34
+ }
35
+
36
+ ENTITY_LABELS = {
37
+ "PER": "Персона (ФИО)",
38
+ "ORG": "Организация",
39
+ "LOC": "Локация (место)",
40
+ "MISC": "Прочее"
41
+ }
42
+
43
+ # ============== ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ==============
44
+ pipelines_cache = {} # Кэш загруженных моделей
45
+ history = deque(maxlen=HISTORY_SIZE) # История запросов
46
+
47
+
48
+ # ============== ФУНКЦИИ ЗАГРУЗКИ МОДЕЛЕЙ ==============
49
+ def load_model(model_key: str):
50
+ """Ленивая загрузка модели по ключу."""
51
+ if model_key not in pipelines_cache:
52
+ model_name = MODELS[model_key]
53
+ pipelines_cache[model_key] = pipeline(
54
+ "ner",
55
+ model=model_name,
56
+ aggregation_strategy="simple"
57
+ )
58
+ return pipelines_cache[model_key]
59
+
60
+
61
+ # ============== ФУНКЦИИ ОБРАБОТКИ ==============
62
+ def validate_input(text: str) -> tuple[bool, str]:
63
+ """Валидация входного текста."""
64
+ if text is None or not text.strip():
65
+ return False, "Ошибка: введите текст для анализа."
66
+
67
+ text = text.strip()
68
+ if len(text) > MAX_CHARS:
69
+ return False, f"Ошибка: текст слишком длинный ({len(text)} символов). Максимум: {MAX_CHARS}."
70
+
71
+ return True, text
72
+
73
+
74
+ def normalize_entity_type(entity_group: str) -> str:
75
+ """Нормализация типа сущности (убираем префиксы B-, I- и т.д.)."""
76
+ # Убираем возможные префиксы BIO-разметки
77
+ for prefix in ["B-", "I-", "E-", "S-", "L-", "U-"]:
78
+ if entity_group.startswith(prefix):
79
+ return entity_group[2:]
80
+ return entity_group
81
+
82
+
83
+ def process_entities(entities: list) -> list[dict]:
84
+ """Обработка и нормализация списка сущностей."""
85
+ processed = []
86
+ for ent in entities:
87
+ entity_type = normalize_entity_type(ent.get("entity_group", ent.get("entity", "UNKNOWN")))
88
+ processed.append({
89
+ "text": ent["word"],
90
+ "type": entity_type,
91
+ "label": ENTITY_LABELS.get(entity_type, entity_type),
92
+ "score": round(ent["score"], 4),
93
+ "start": ent["start"],
94
+ "end": ent["end"]
95
+ })
96
+ return processed
97
+
98
+
99
+ def create_highlighted_text(text: str, entities: list) -> list:
100
+ """Создание данных для подсветки текста."""
101
+ if not entities:
102
+ return [(text, None)]
103
+
104
+ # Сортируем сущности по позиции начала
105
+ sorted_entities = sorted(entities, key=lambda x: x["start"])
106
+
107
+ highlighted = []
108
+ last_end = 0
109
+
110
+ for ent in sorted_entities:
111
+ start, end = ent["start"], ent["end"]
112
+
113
+ # Добавляем текст до сущности
114
+ if start > last_end:
115
+ highlighted.append((text[last_end:start], None))
116
+
117
+ # Добавляем сущность с меткой
118
+ entity_text = text[start:end]
119
+ entity_type = ent["type"]
120
+ highlighted.append((entity_text, entity_type))
121
+
122
+ last_end = end
123
+
124
+ # Добавляем оставшийся текст
125
+ if last_end < len(text):
126
+ highlighted.append((text[last_end:], None))
127
+
128
+ return highlighted
129
+
130
+
131
+ def entities_to_dataframe(entities: list) -> pd.DataFrame:
132
+ """Преобразование списка сущностей в DataFrame."""
133
+ if not entities:
134
+ return pd.DataFrame(columns=["Текст", "Тип", "Описание", "Уверенность"])
135
+
136
+ data = []
137
+ for ent in entities:
138
+ data.append({
139
+ "Текст": ent["text"],
140
+ "Тип": ent["type"],
141
+ "Описание": ent["label"],
142
+ "Уверенность": f"{ent['score']:.2%}"
143
+ })
144
+
145
+ return pd.DataFrame(data)
146
+
147
+
148
+ def add_to_history(text: str, model: str, entities: list, latency: float):
149
+ """Добавление запроса в историю."""
150
+ timestamp = datetime.now().strftime("%H:%M:%S")
151
+ entity_count = len(entities)
152
+ entity_types = ", ".join(set(e["type"] for e in entities)) if entities else "—"
153
+
154
+ history.appendleft({
155
+ "Время": timestamp,
156
+ "Модель": model.split()[0], # Короткое название
157
+ "Текст": text[:50] + "..." if len(text) > 50 else text,
158
+ "Найдено": entity_count,
159
+ "Типы": entity_types,
160
+ "Latency": f"{latency} мс"
161
+ })
162
+
163
+
164
+ # ============== ОСНОВНЫЕ ФУНКЦИИ ОБРАБОТКИ ==============
165
+ def process_single_text(text: str, model_choice: str):
166
+ """Обработка одиночного текста."""
167
+ # Валидация
168
+ is_valid, result = validate_input(text)
169
+ if not is_valid:
170
+ return result, None, None, "—"
171
+
172
+ text = result
173
+
174
+ try:
175
+ # Загрузка модели и обработка
176
+ pipe = load_model(model_choice)
177
+
178
+ t0 = time.time()
179
+ raw_entities = pipe(text)
180
+ latency = round((time.time() - t0) * 1000, 1)
181
+
182
+ # Обработка результатов
183
+ entities = process_entities(raw_entities)
184
+
185
+ # Создание выходных данных
186
+ highlighted = create_highlighted_text(text, entities)
187
+ df = entities_to_dataframe(entities)
188
+
189
+ # Добавление в историю
190
+ add_to_history(text, model_choice, entities, latency)
191
+
192
+ status = f"Найдено сущностей: {len(entities)}"
193
+ return status, highlighted, df, f"{latency} мс"
194
+
195
+ except Exception as e:
196
+ return f"Ошибка: {type(e).__name__}: {e}", None, None, "—"
197
+
198
+
199
+ def compare_models(text: str):
200
+ """Сравнение результатов двух моделей."""
201
+ # Валидация
202
+ is_valid, result = validate_input(text)
203
+ if not is_valid:
204
+ return result, None, None, "—", None, None, "—"
205
+
206
+ text = result
207
+ results = {}
208
+
209
+ try:
210
+ for model_key in MODELS.keys():
211
+ pipe = load_model(model_key)
212
+
213
+ t0 = time.time()
214
+ raw_entities = pipe(text)
215
+ latency = round((time.time() - t0) * 1000, 1)
216
+
217
+ entities = process_entities(raw_entities)
218
+ highlighted = create_highlighted_text(text, entities)
219
+ df = entities_to_dataframe(entities)
220
+
221
+ results[model_key] = {
222
+ "highlighted": highlighted,
223
+ "df": df,
224
+ "latency": f"{latency} мс",
225
+ "count": len(entities)
226
+ }
227
+
228
+ model_keys = list(MODELS.keys())
229
+ m1, m2 = model_keys[0], model_keys[1]
230
+
231
+ status = f"Модель 1: {results[m1]['count']} сущностей | Модель 2: {results[m2]['count']} сущностей"
232
+
233
+ return (
234
+ status,
235
+ results[m1]["highlighted"],
236
+ results[m1]["df"],
237
+ results[m1]["latency"],
238
+ results[m2]["highlighted"],
239
+ results[m2]["df"],
240
+ results[m2]["latency"]
241
+ )
242
+
243
+ except Exception as e:
244
+ error_msg = f"Ошибка: {type(e).__name__}: {e}"
245
+ return error_msg, None, None, "—", None, None, "—"
246
+
247
+
248
+ def process_batch(file, model_choice: str):
249
+ """Пакетная обработка файла (CSV или TXT)."""
250
+ if file is None:
251
+ return "Ошибка: загрузите файл.", None, None
252
+
253
+ try:
254
+ # Определяем тип файла и читаем
255
+ file_path = file.name
256
+
257
+ if file_path.endswith('.csv'):
258
+ df_input = pd.read_csv(file_path)
259
+ if 'text' not in df_input.columns:
260
+ return "Ошибка: CSV должен содержать колонку 'text'.", None, None
261
+ texts = df_input['text'].tolist()
262
+ else: # TXT
263
+ with open(file_path, 'r', encoding='utf-8') as f:
264
+ texts = [line.strip() for line in f if line.strip()]
265
+
266
+ if len(texts) > MAX_BATCH_ROWS:
267
+ return f"Ошибка: слишком много строк ({len(texts)}). Максимум: {MAX_BATCH_ROWS}.", None, None
268
+
269
+ if not texts:
270
+ return "Ошибка: файл пустой или не содержит текстов.", None, None
271
+
272
+ # Загрузка модели
273
+ pipe = load_model(model_choice)
274
+
275
+ # Обработка каждого текста
276
+ results = []
277
+ t0 = time.time()
278
+
279
+ for i, text in enumerate(texts):
280
+ if len(text) > MAX_CHARS:
281
+ text = text[:MAX_CHARS]
282
+
283
+ try:
284
+ raw_entities = pipe(text)
285
+ entities = process_entities(raw_entities)
286
+
287
+ # Собираем сущности по типам
288
+ per_list = [e["text"] for e in entities if e["type"] == "PER"]
289
+ org_list = [e["text"] for e in entities if e["type"] == "ORG"]
290
+ loc_list = [e["text"] for e in entities if e["type"] == "LOC"]
291
+ misc_list = [e["text"] for e in entities if e["type"] == "MISC"]
292
+
293
+ results.append({
294
+ "№": i + 1,
295
+ "Текст": text[:100] + "..." if len(text) > 100 else text,
296
+ "PER": ", ".join(per_list) if per_list else "—",
297
+ "ORG": ", ".join(org_list) if org_list else "—",
298
+ "LOC": ", ".join(loc_list) if loc_list else "—",
299
+ "MISC": ", ".join(misc_list) if misc_list else "—",
300
+ "Всего": len(entities)
301
+ })
302
+ except Exception as e:
303
+ results.append({
304
+ "№": i + 1,
305
+ "Текст": text[:100] + "...",
306
+ "PER": "ОШИБКА",
307
+ "ORG": str(e)[:30],
308
+ "LOC": "—",
309
+ "MISC": "—",
310
+ "Всего": 0
311
+ })
312
+
313
+ total_latency = round((time.time() - t0) * 1000, 1)
314
+ df_results = pd.DataFrame(results)
315
+
316
+ # Создаём CSV для скачивания
317
+ csv_buffer = io.StringIO()
318
+ df_results.to_csv(csv_buffer, index=False, encoding='utf-8')
319
+ csv_content = csv_buffer.getvalue()
320
+
321
+ status = f"Обработано: {len(texts)} текстов за {total_latency} мс"
322
+ return status, df_results, csv_content
323
+
324
+ except Exception as e:
325
+ return f"Ошибка: {type(e).__name__}: {e}", None, None
326
+
327
+
328
+ def get_history_df():
329
+ """Получение истории запросов как DataFrame."""
330
+ if not history:
331
+ return pd.DataFrame(columns=["Время", "Модель", "Текст", "Найдено", "Типы", "Latency"])
332
+ return pd.DataFrame(list(history))
333
+
334
+
335
+ def clear_history():
336
+ """Очистка истории запросов."""
337
+ history.clear()
338
+ return pd.DataFrame(columns=["Время", "Модель", "Текст", "Найдено", "Типы", "Latency"]), "История очищена"
339
+
340
+
341
+ # ============== GRADIO ИНТЕРФЕЙС ==============
342
+ def create_interface():
343
+ """Создание Gradio интерфейса."""
344
+
345
+ with gr.Blocks(
346
+ theme=gr.themes.Soft(),
347
+ title="Russian NER — Извлечение сущностей",
348
+ css="""
349
+ .entity-legend {
350
+ display: flex;
351
+ gap: 20px;
352
+ margin: 10px 0;
353
+ flex-wrap: wrap;
354
+ }
355
+ .entity-legend-item {
356
+ display: flex;
357
+ align-items: center;
358
+ gap: 5px;
359
+ }
360
+ .entity-color {
361
+ width: 16px;
362
+ height: 16px;
363
+ border-radius: 3px;
364
+ }
365
+ """
366
+ ) as demo:
367
+
368
+ # Заголовок
369
+ gr.Markdown("""
370
+ # Russian NER — Извлечение именованных сущностей
371
+
372
+ Приложение для автоматического извлечения именованных сущностей из русского текста:
373
+ **персоны (ФИО)**, **организации**, **локации (города, страны)** и **прочее**.
374
+
375
+ ---
376
+ """)
377
+
378
+ # Легенда цветов
379
+ gr.HTML("""
380
+ <div class="entity-legend">
381
+ <div class="entity-legend-item">
382
+ <div class="entity-color" style="background-color: #3b82f6;"></div>
383
+ <span><b>PER</b> — Персоны</span>
384
+ </div>
385
+ <div class="entity-legend-item">
386
+ <div class="entity-color" style="background-color: #22c55e;"></div>
387
+ <span><b>ORG</b> — Организации</span>
388
+ </div>
389
+ <div class="entity-legend-item">
390
+ <div class="entity-color" style="background-color: #f97316;"></div>
391
+ <span><b>LOC</b> — Локации</span>
392
+ </div>
393
+ <div class="entity-legend-item">
394
+ <div class="entity-color" style="background-color: #a855f7;"></div>
395
+ <span><b>MISC</b> — Прочее</span>
396
+ </div>
397
+ </div>
398
+ """)
399
+
400
+ with gr.Tabs():
401
+ # ==================== ВКЛАДКА 1: АНАЛИЗ ТЕКСТА ====================
402
+ with gr.Tab("Анализ текста"):
403
+ gr.Markdown("### Введите текст для извлечения сущностей")
404
+
405
+ with gr.Row():
406
+ with gr.Column(scale=2):
407
+ model_dropdown = gr.Dropdown(
408
+ choices=list(MODELS.keys()),
409
+ value=list(MODELS.keys())[0],
410
+ label="Выберите модель NER",
411
+ interactive=True
412
+ )
413
+ with gr.Column(scale=1):
414
+ latency_box = gr.Textbox(
415
+ label="Время обработки",
416
+ value="—",
417
+ interactive=False
418
+ )
419
+
420
+ text_input = gr.Textbox(
421
+ label=f"Текст для анализа (максимум {MAX_CHARS} символов)",
422
+ placeholder="Введите или вставьте текст на русском языке...",
423
+ lines=5
424
+ )
425
+
426
+ process_btn = gr.Button("Обработать", variant="primary", size="lg")
427
+
428
+ status_box = gr.Textbox(label="Статус", interactive=False)
429
+
430
+ gr.Markdown("### Результат с подсветкой сущностей")
431
+ highlighted_text = gr.HighlightedText(
432
+ label="Найденные сущности в тексте",
433
+ combine_adjacent=True,
434
+ color_map=COLOR_MAP
435
+ )
436
+
437
+ gr.Markdown("### Таблица извлечённых сущностей")
438
+ entities_table = gr.Dataframe(
439
+ headers=["Текст", "Тип", "Описание", "Уверенность"],
440
+ label="Извлечённые сущности",
441
+ wrap=True
442
+ )
443
+
444
+ # Обработчик
445
+ process_btn.click(
446
+ fn=process_single_text,
447
+ inputs=[text_input, model_dropdown],
448
+ outputs=[status_box, highlighted_text, entities_table, latency_box]
449
+ )
450
+
451
+ # Примеры
452
+ gr.Markdown("### Примеры текстов")
453
+ gr.Examples(
454
+ examples=[
455
+ ["Владимир Путин встретился с президентом Франции Эммануэлем Макроном в Москве для обсуждения вопросов безопасности."],
456
+ ["Компания Яндекс открыла новый офис в Санкт-Петербурге рядом с Невским проспектом."],
457
+ ["Сбербанк и ВТБ объявили о запуске совместного проекта в Казани при поддержке Министерства финансов."],
458
+ ["Иван Петров работает программистом в компании Mail.ru Group в Москве с 2020 года."],
459
+ ["Александр Сергеевич Пушкин родился в Москве в 1799 году и стал величайшим русским поэтом."]
460
+ ],
461
+ inputs=text_input,
462
+ label="Нажмите на пример для автозаполнения"
463
+ )
464
+
465
+ # ==================== ВКЛАДКА 2: СРАВНЕНИЕ МОДЕЛЕЙ ====================
466
+ with gr.Tab("Сравнение моделей"):
467
+ gr.Markdown("""
468
+ ### Сравнение результатов двух моделей
469
+ Введите текст, чтобы увидеть, как разные модели распознают сущности.
470
+ """)
471
+
472
+ compare_input = gr.Textbox(
473
+ label=f"Текст для сравнения (максимум {MAX_CHARS} символов)",
474
+ placeholder="Введите текст для сравнения моделей...",
475
+ lines=4
476
+ )
477
+
478
+ compare_btn = gr.Button("Сравнить модели", variant="primary", size="lg")
479
+ compare_status = gr.Textbox(label="Статус сравнения", interactive=False)
480
+
481
+ model_keys = list(MODELS.keys())
482
+
483
+ with gr.Row():
484
+ with gr.Column():
485
+ gr.Markdown(f"#### {model_keys[0]}")
486
+ highlight_1 = gr.HighlightedText(
487
+ label="Результат модели 1",
488
+ color_map=COLOR_MAP
489
+ )
490
+ table_1 = gr.Dataframe(label="Сущности (модель 1)")
491
+ latency_1 = gr.Textbox(label="Время", interactive=False)
492
+
493
+ with gr.Column():
494
+ gr.Markdown(f"#### {model_keys[1]}")
495
+ highlight_2 = gr.HighlightedText(
496
+ label="Результат модели 2",
497
+ color_map=COLOR_MAP
498
+ )
499
+ table_2 = gr.Dataframe(label="Сущно��ти (модель 2)")
500
+ latency_2 = gr.Textbox(label="Время", interactive=False)
501
+
502
+ compare_btn.click(
503
+ fn=compare_models,
504
+ inputs=[compare_input],
505
+ outputs=[compare_status, highlight_1, table_1, latency_1, highlight_2, table_2, latency_2]
506
+ )
507
+
508
+ # ==================== ВКЛАДКА 3: ПАКЕТНАЯ ОБРАБОТКА ====================
509
+ with gr.Tab("Пакетная обработка"):
510
+ gr.Markdown(f"""
511
+ ### Массовая обработка текстов из файла
512
+
513
+ Загрузите файл **CSV** (с колонкой `text`) или **TXT** (каждая строка — отдельный текст).
514
+
515
+ **Ограничения:** максимум {MAX_BATCH_ROWS} строк, {MAX_CHARS} символов на текст.
516
+ """)
517
+
518
+ with gr.Row():
519
+ batch_model = gr.Dropdown(
520
+ choices=list(MODELS.keys()),
521
+ value=list(MODELS.keys())[0],
522
+ label="Модель для обработки"
523
+ )
524
+ batch_file = gr.File(
525
+ label="Загрузите CSV или TXT файл",
526
+ file_types=[".csv", ".txt"]
527
+ )
528
+
529
+ batch_btn = gr.Button("Обработать файл", variant="primary", size="lg")
530
+ batch_status = gr.Textbox(label="Статус обработки", interactive=False)
531
+
532
+ batch_results = gr.Dataframe(
533
+ label="Результаты обработки",
534
+ wrap=True
535
+ )
536
+
537
+ batch_download = gr.Textbox(
538
+ label="CSV для скачивания (скопируйте содержимое)",
539
+ lines=5,
540
+ visible=True
541
+ )
542
+
543
+ batch_btn.click(
544
+ fn=process_batch,
545
+ inputs=[batch_file, batch_model],
546
+ outputs=[batch_status, batch_results, batch_download]
547
+ )
548
+
549
+ # ==================== ВКЛАДКА 4: ИСТОРИЯ ====================
550
+ with gr.Tab("История запросов"):
551
+ gr.Markdown(f"""
552
+ ### История последних {HISTORY_SIZE} запросов
553
+
554
+ Здесь отображаются ваши недавние запросы с результатами.
555
+ """)
556
+
557
+ refresh_btn = gr.Button("Обновить историю", variant="secondary")
558
+ clear_btn = gr.Button("Очистить историю", variant="stop")
559
+
560
+ history_status = gr.Textbox(label="Статус", interactive=False, visible=False)
561
+ history_table = gr.Dataframe(
562
+ label="История запросов",
563
+ headers=["Время", "Модель", "Текст", "Найдено", "Типы", "Latency"],
564
+ wrap=True
565
+ )
566
+
567
+ refresh_btn.click(
568
+ fn=get_history_df,
569
+ outputs=[history_table]
570
+ )
571
+
572
+ clear_btn.click(
573
+ fn=clear_history,
574
+ outputs=[history_table, history_status]
575
+ )
576
+
577
+ # Футер
578
+ gr.Markdown("""
579
+ ---
580
+
581
+ **Модели:**
582
+ - [Babelscape/wikineural-multilingual-ner](https://huggingface.co/Babelscape/wikineural-multilingual-ner) — мультиязычная модель NER
583
+ - [Davlan/xlm-roberta-base-ner-hrl](https://huggingface.co/Davlan/xlm-roberta-base-ner-hrl) — XLM-RoBERTa для NER
584
+
585
+ **Ограничения:** CPU-режим, максимум 2000 символов на текст.
586
+
587
+ **Внимание:** Не вводите реальные персональные данные в демонстрационных целях.
588
+ """)
589
+
590
+ return demo
591
+
592
+
593
+ # ============== ЗАПУСК ==============
594
+ if __name__ == "__main__":
595
+ demo = create_interface()
596
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ transformers>=4.35.0
3
+ torch>=2.0.0
4
+ pandas>=2.0.0