X commited on
Commit
c396e17
·
verified ·
1 Parent(s): fc5effb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -216
app.py CHANGED
@@ -28,13 +28,6 @@ MODEL_NAME = "sberbank-ai/rugpt3small_based_on_gpt2"
28
  EMBEDDING_MODEL = "all-MiniLM-L6-v2"
29
  SCIENCE_DATASET = "RafaelUI/ru_science"
30
  ARTICLE_LIMIT = 50
31
- MAX_LENGTH = 512
32
- TEMPERATURE = 0.8
33
- TOP_P = 0.95
34
-
35
- LOG_FILE = "query_logs.json"
36
- EMBEDDINGS_FILE = "science_embeddings.npy"
37
- ARTICLES_FILE = "science_articles.pkl"
38
 
39
  AI_NAME = "OpenAirAI"
40
  COMPANY_NAME = "OpenRussianAI"
@@ -51,85 +44,7 @@ st.set_page_config(
51
  )
52
 
53
  # ===================================================================
54
- # 2. БАЗА ЗНАНИЙ ДЛЯ КОРРЕКТНЫХ ОТВЕТОВ
55
- # ===================================================================
56
-
57
- KNOWLEDGE = {
58
- "education": """
59
- ### Образование и специальность
60
-
61
- Я, OpenAirAI, не учусь в традиционном смысле, но постоянно обучаюсь на новых данных.
62
-
63
- **Моя "специальность":**
64
- - Обработка естественного языка
65
- - Поиск информации в научных статьях
66
- - Анализ данных
67
-
68
- **Что я умею:**
69
- - Отвечать на научные вопросы
70
- - Находить информацию в статьях ru_science
71
- - Помогать с системным администрированием
72
- - Генерировать уникальные ответы
73
-
74
- **Что я не умею:**
75
- - Не имею личного опыта
76
- - Не могу принимать решения за людей
77
- - Не имею физического воплощения
78
- """,
79
- "mission": """
80
- ### Моя миссия
81
-
82
- Я создан для того, чтобы:
83
- 1. Делать знания доступными
84
- 2. Помогать в научных исследованиях
85
- 3. Популяризировать науку
86
-
87
- **Как я помогаю людям:**
88
- - Отвечаю на вопросы
89
- - Нахожу релевантные статьи
90
- - Объясняю сложные концепции простым языком
91
-
92
- **Почему это важно:**
93
- Наука должна быть доступна каждому, а я помогаю убрать барьеры между человеком и знаниями.
94
- """,
95
- "internet": """
96
- ### О существовании интернета
97
-
98
- Интернет существует как глобальная сеть, объединяющая миллиарды устройств.
99
-
100
- **Что такое интернет:**
101
- - Физическая инфраструктура (кабели, серверы)
102
- - Протоколы передачи данных (TCP/IP)
103
- - Миллионы веб-сайтов и сервисов
104
-
105
- **Интернет - это:**
106
- - Инструмент для обмена информацией
107
- - Платформа для общения
108
- - Источник знаний
109
-
110
- Я существую в интернете, поэтому могу с уверенностью сказать, что он реален.
111
- """,
112
- "projects": """
113
- ### О проектах и сотрудничестве
114
-
115
- OpenRussianAI - открытый проект.
116
-
117
- **Как присоединиться:**
118
- 1. Посетите наш сайт: https://sites.google.com/view/opruai/home
119
- 2. Подпишитесь на Hugging Face: https://huggingface.co/OpenRussianAI
120
- 3. Свяжитесь с нами: info@openrussianai.ru
121
-
122
- **Что мы делаем:**
123
- - Разрабатываем AI-помощников
124
- - Исследуем NLP
125
- - Популяризируем науку
126
-
127
- **Присоединяйтесь к сообществу!**
128
- """
129
- }
130
-
131
- # ===================================================================
132
- # 3. НЕЙРОСЕТЬ
133
  # ===================================================================
134
 
135
  class NeuralChatbot:
@@ -140,14 +55,15 @@ class NeuralChatbot:
140
  self.generator = None
141
  self.is_loaded = False
142
 
143
- self.system_prompt = f"""Ты - {AI_NAME}, научный AI-ассистент от компании {COMPANY_NAME}.
 
144
  Ты создан в {CREATION_DATE} командой {', '.join(CREATORS)}.
145
- Ты отвечаешь на русском языке, четко и по делу.
146
- Если вопрос про образование, миссию или интернет - используй знания из базы.
147
- Если вопрос про науку - ищи в статьях.
148
- Всегда давай структурированный ответ.
149
 
150
- Вопрос: """
151
 
152
  def load_model(self):
153
  with st.spinner("🧠 Загружаю нейросеть..."):
@@ -164,9 +80,9 @@ class NeuralChatbot:
164
  model=self.model,
165
  tokenizer=self.tokenizer,
166
  device=0 if torch.cuda.is_available() else -1,
167
- max_length=300,
168
- temperature=TEMPERATURE,
169
- top_p=TOP_P,
170
  do_sample=True,
171
  repetition_penalty=1.2,
172
  pad_token_id=self.tokenizer.eos_token_id
@@ -175,116 +91,65 @@ class NeuralChatbot:
175
  self.is_loaded = True
176
  return True
177
  except Exception as e:
178
- st.warning(f"Не удалось загрузить нейросеть: {e}")
179
  return False
180
 
181
  def generate(self, query):
 
 
 
 
182
  if not self.is_loaded:
183
- return self.fallback_response(query)
184
 
185
  try:
186
- prompt = self.system_prompt + query
 
187
 
 
188
  response = self.generator(
189
  prompt,
190
- max_new_tokens=250,
191
- temperature=TEMPERATURE,
192
- top_p=TOP_P,
193
  do_sample=True,
194
  repetition_penalty=1.2
195
  )[0]['generated_text']
196
 
 
197
  response = response.replace(prompt, "").strip()
198
 
199
- if len(response) < 15:
200
- return self.fallback_response(query)
 
201
 
202
  return response
203
 
204
- except:
205
- return self.fallback_response(query)
206
 
207
- def fallback_response(self, query):
208
- return f"""Я {AI_NAME} от {COMPANY_NAME}.
209
-
210
- Извините, нейросеть временно недоступна. Вот что я знаю по вашему вопросу:
211
-
212
- {self.get_knowledge(query)}
213
-
214
- Если нужна более подробная информация, попробуйте позже. 🧠"""
215
-
216
- def get_knowledge(self, query):
217
- q = query.lower()
218
- if any(w in q for w in ["образован", "специальн", "учусь", "умею"]):
219
- return KNOWLEDGE["education"]
220
- elif any(w in q for w in ["мисси", "цель", "помога", "задача"]):
221
- return KNOWLEDGE["mission"]
222
- elif any(w in q for w in ["интернет", "существу"]):
223
- return KNOWLEDGE["internet"]
224
- elif any(w in q for w in ["проект", "сотруднич", "присоедин"]):
225
- return KNOWLEDGE["projects"]
226
- else:
227
- return "Я помогаю с научными вопросами. Что именно вас интересует?"
228
-
229
- # ===================================================================
230
- # 4. ОЧИСТКА ЗАПРОСОВ ОТ СПАМА
231
- # ===================================================================
232
-
233
- def clean_query(query):
234
- """Очищает запрос от спама и мусора"""
235
- # Удаляем ссылки
236
- query = re.sub(r'http[s]?://\S+', '', query)
237
- query = re.sub(r'www\.\S+', '', query)
238
-
239
- # Удаляем email
240
- query = re.sub(r'\S+@\S+', '', query)
241
-
242
- # Удаляем телефоны
243
- query = re.sub(r'\+7\s*\(?\d{3}\)?\s*\d{3}\s*\d{2}\s*\d{2}', '', query)
244
- query = re.sub(r'8\s*\(?\d{3}\)?\s*\d{3}\s*\d{2}\s*\d{2}', '', query)
245
-
246
- # Удаляем странные символы
247
- query = re.sub(r'[^\w\s\.\?\!,;:]', ' ', query)
248
-
249
- # Удаляем множественные пробелы
250
- query = ' '.join(query.split())
251
-
252
- # Если запрос слишком длинный - берем первое осмысленное предложение
253
- if len(query) > 300:
254
- sentences = re.split(r'[.!?]', query)
255
- query = '. '.join([s for s in sentences[:3] if len(s) > 10]) + '.'
256
-
257
- return query.strip()
258
-
259
- def extract_main_question(query):
260
- """Извлекает основной вопрос из спама"""
261
- q = query.lower()
262
-
263
- # Ключевые слова вопросов
264
- question_keywords = ['что', 'как', 'почему', 'где', 'когда', 'кто', 'зачем', 'какой']
265
-
266
- # Ищем предложения с вопросительными словами
267
- sentences = re.split(r'[.!?]', q)
268
- questions = []
269
-
270
- for s in sentences:
271
- if any(keyword in s for keyword in question_keywords):
272
- questions.append(s.strip())
273
-
274
- if questions:
275
- return '. '.join(questions[:2]) + '?'
276
-
277
- # Если вопросов нет, берем первые 100 символов
278
- return query[:150] + ('...' if len(query) > 150 else '')
279
 
280
  # ===================================================================
281
- # 5. ЗАГРУЗКА СТАТЕЙ
282
  # ===================================================================
283
 
284
  @st.cache_resource
285
  def load_science_articles():
286
- if os.path.exists(ARTICLES_FILE):
287
- with open(ARTICLES_FILE, 'rb') as f:
288
  return pickle.load(f)
289
 
290
  with st.spinner("📚 Загружаю научные статьи..."):
@@ -302,7 +167,7 @@ def load_science_articles():
302
  "text": text[:2000],
303
  "source": "ru_science"
304
  })
305
- with open(ARTICLES_FILE, 'wb') as f:
306
  pickle.dump(articles, f)
307
  return articles
308
  except:
@@ -317,13 +182,13 @@ def load_embedder():
317
 
318
  @st.cache_resource
319
  def create_embeddings(_articles, _embedder):
320
- if os.path.exists(EMBEDDINGS_FILE):
321
- return np.load(EMBEDDINGS_FILE)
322
  if not _articles or _embedder is None:
323
  return np.array([])
324
  texts = [f"{a['title']}\n\n{a['text']}" for a in _articles]
325
  embeddings = _embedder.encode(texts, normalize_embeddings=True, show_progress_bar=True, batch_size=64)
326
- np.save(EMBEDDINGS_FILE, embeddings)
327
  return embeddings
328
 
329
  def search_articles(query, _articles, _embeddings, _embedder):
@@ -344,7 +209,7 @@ def search_articles(query, _articles, _embeddings, _embedder):
344
  return []
345
 
346
  # ===================================================================
347
- # 6. ОСНОВНОЙ КЛАСС
348
  # ===================================================================
349
 
350
  class OpenAirAI:
@@ -360,50 +225,33 @@ class OpenAirAI:
360
  return self.is_ready
361
 
362
  def generate_answer(self, query):
363
- # Очищаем запрос
364
- clean_q = clean_query(query)
365
-
366
- # Если запрос стал пустым
367
- if not clean_q or len(clean_q) < 3:
368
- return f"Я {self.name} от {self.company}. Чем могу помочь? Задайте конкретный вопрос. 🧠"
369
-
370
- # Извлекаем основной вопрос
371
- main_q = extract_main_question(clean_q)
372
-
373
- # Ищем статьи
374
- articles_context = search_articles(main_q, articles, embeddings, embedder)
375
-
376
- # Если есть статьи и вопрос научный
377
- if articles_context and any(w in main_q.lower() for w in ["науч", "исслед", "стать", "эксперимент"]):
378
- response = self.chatbot.generate(main_q)
379
- if articles_context:
380
- response += "\n\n📄 **Релевантные статьи:**\n"
381
- for i, art in enumerate(articles_context, 1):
382
- response += f"{i}. {art['title']}\n"
383
- return response
384
-
385
- # Генерируем ответ через нейросеть
386
- return self.chatbot.generate(main_q)
387
 
388
  # ===================================================================
389
- # 7. ИНТЕРФЕЙС
390
  # ===================================================================
391
 
 
392
  articles = load_science_articles()
393
  embedder = load_embedder()
394
  embeddings = create_embeddings(articles, embedder)
395
 
 
396
  if 'ai' not in st.session_state:
397
  st.session_state.ai = OpenAirAI()
398
  st.session_state.ai.initialize()
399
 
400
  ai = st.session_state.ai
401
 
 
402
  if "messages" not in st.session_state:
403
  st.session_state.messages = []
404
- greeting = ai.generate_answer("Привет! Представься")
 
405
  st.session_state.messages.append({"role": "assistant", "content": greeting})
406
 
 
407
  with st.sidebar:
408
  st.image("https://cdn-icons-png.flaticon.com/512/4248/4248455.png", width=80)
409
  st.title(f"🧠 {AI_NAME}")
@@ -423,33 +271,58 @@ with st.sidebar:
423
  **📊 Статистика:**
424
  - Статей: {len(articles)}
425
  - Сообщений: {len(st.session_state.messages)}
 
426
  """)
427
 
 
 
 
 
 
 
 
428
  if st.button("🗑️ Очистить чат"):
429
  st.session_state.messages = []
430
- greeting = ai.generate_answer("Привет! Представься")
431
  st.session_state.messages.append({"role": "assistant", "content": greeting})
432
  st.rerun()
433
 
 
434
  st.title(f"🧠 {AI_NAME} - Нейросетевой AI-ассистент")
435
- st.markdown(f"**{AI_NAME}** от **{COMPANY_NAME}** | Очищает запросы от спама")
436
 
 
437
  for message in st.session_state.messages:
438
  with st.chat_message(message["role"]):
439
  st.markdown(message["content"])
440
 
441
- if prompt := st.chat_input("Задайте вопрос..."):
 
 
442
  st.session_state.messages.append({"role": "user", "content": prompt})
443
  with st.chat_message("user"):
444
  st.markdown(prompt)
445
 
 
446
  with st.chat_message("assistant"):
447
- with st.spinner("🧠 Думаю..."):
 
448
  response = ai.generate_answer(prompt)
 
 
 
 
 
 
 
 
 
 
449
  st.markdown(response)
450
  st.session_state.messages.append({"role": "assistant", "content": response})
451
 
452
  st.rerun()
453
 
 
454
  st.divider()
455
- st.caption(f"🧠 {AI_NAME} от {COMPANY_NAME} | Создан в {CREATION_DATE}")
 
28
  EMBEDDING_MODEL = "all-MiniLM-L6-v2"
29
  SCIENCE_DATASET = "RafaelUI/ru_science"
30
  ARTICLE_LIMIT = 50
 
 
 
 
 
 
 
31
 
32
  AI_NAME = "OpenAirAI"
33
  COMPANY_NAME = "OpenRussianAI"
 
44
  )
45
 
46
  # ===================================================================
47
+ # 2. НЕЙРОСЕТЬ (БЕЗ IF/ELSE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  # ===================================================================
49
 
50
  class NeuralChatbot:
 
55
  self.generator = None
56
  self.is_loaded = False
57
 
58
+ # ЕДИНЫЙ СИСТЕМНЫЙ ПРОМПТ
59
+ self.system_prompt = f"""Ты - {AI_NAME}, нейросетевой AI-ассистент от компании {COMPANY_NAME}.
60
  Ты создан в {CREATION_DATE} командой {', '.join(CREATORS)}.
61
+ Ты всегда отвечаешь на русском языке, дружелюбно и профессионально.
62
+ Ты помогаешь с любыми вопросами: наука, технологии, образование, математика.
63
+ Ты генерируешь уникальные ответы каждый раз, не используя шаблоны.
64
+ Ты не используешь if/else в своих ответах, только нейросетевая генерация.
65
 
66
+ Вопрос пользователя: """
67
 
68
  def load_model(self):
69
  with st.spinner("🧠 Загружаю нейросеть..."):
 
80
  model=self.model,
81
  tokenizer=self.tokenizer,
82
  device=0 if torch.cuda.is_available() else -1,
83
+ max_length=400,
84
+ temperature=0.85,
85
+ top_p=0.95,
86
  do_sample=True,
87
  repetition_penalty=1.2,
88
  pad_token_id=self.tokenizer.eos_token_id
 
91
  self.is_loaded = True
92
  return True
93
  except Exception as e:
94
+ st.error(f"Ошибка загрузки: {e}")
95
  return False
96
 
97
  def generate(self, query):
98
+ """ГЕНЕРИРУЕТ ВСЕГДА, БЕЗ IF/ELSE"""
99
+ # Очищаем запрос
100
+ clean_q = self.clean_query(query)
101
+
102
  if not self.is_loaded:
103
+ return f"Я {AI_NAME} от {COMPANY_NAME}. Нейросеть временно недоступна. Попробуйте позже. 🧠"
104
 
105
  try:
106
+ # Формируем промпт
107
+ prompt = self.system_prompt + clean_q
108
 
109
+ # Генерируем ответ
110
  response = self.generator(
111
  prompt,
112
+ max_new_tokens=300,
113
+ temperature=0.85,
114
+ top_p=0.95,
115
  do_sample=True,
116
  repetition_penalty=1.2
117
  )[0]['generated_text']
118
 
119
+ # Убираем промпт
120
  response = response.replace(prompt, "").strip()
121
 
122
+ # Если ответ пустой - генерируем общий
123
+ if len(response) < 10:
124
+ return f"Я {AI_NAME} от {COMPANY_NAME}. Я помогаю с научными вопросами, технологиями и образованием. Что именно вас интересует? 🧠"
125
 
126
  return response
127
 
128
+ except Exception as e:
129
+ return f"Я {AI_NAME}. Произошла ошибка при генерации. Попробуйте переформулировать вопрос. 🧠"
130
 
131
+ def clean_query(self, query):
132
+ """Очистка от спама"""
133
+ query = re.sub(r'http[s]?://\S+', '', query)
134
+ query = re.sub(r'\S+@\S+', '', query)
135
+ query = re.sub(r'\+7\s*\(?\d{3}\)?\s*\d{3}\s*\d{2}\s*\d{2}', '', query)
136
+ query = re.sub(r'[^\w\s\.\?\!,;:-]', ' ', query)
137
+ query = ' '.join(query.split())
138
+
139
+ if len(query) > 300:
140
+ sentences = re.split(r'[.!?]', query)
141
+ query = '. '.join([s for s in sentences[:2] if len(s) > 10]) + '.'
142
+
143
+ return query.strip() if query else "Расскажи о себе"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  # ===================================================================
146
+ # 3. ЗАГРУЗКА СТАТЕЙ (ДЛЯ КОНТЕКСТА)
147
  # ===================================================================
148
 
149
  @st.cache_resource
150
  def load_science_articles():
151
+ if os.path.exists("science_articles.pkl"):
152
+ with open("science_articles.pkl", 'rb') as f:
153
  return pickle.load(f)
154
 
155
  with st.spinner("📚 Загружаю научные статьи..."):
 
167
  "text": text[:2000],
168
  "source": "ru_science"
169
  })
170
+ with open("science_articles.pkl", 'wb') as f:
171
  pickle.dump(articles, f)
172
  return articles
173
  except:
 
182
 
183
  @st.cache_resource
184
  def create_embeddings(_articles, _embedder):
185
+ if os.path.exists("science_embeddings.npy"):
186
+ return np.load("science_embeddings.npy")
187
  if not _articles or _embedder is None:
188
  return np.array([])
189
  texts = [f"{a['title']}\n\n{a['text']}" for a in _articles]
190
  embeddings = _embedder.encode(texts, normalize_embeddings=True, show_progress_bar=True, batch_size=64)
191
+ np.save("science_embeddings.npy", embeddings)
192
  return embeddings
193
 
194
  def search_articles(query, _articles, _embeddings, _embedder):
 
209
  return []
210
 
211
  # ===================================================================
212
+ # 4. ОСНОВНОЙ КЛАСС
213
  # ===================================================================
214
 
215
  class OpenAirAI:
 
225
  return self.is_ready
226
 
227
  def generate_answer(self, query):
228
+ """ВСЕГДА ГЕНЕРИРУЕТ ЧЕРЕЗ НЕЙРОСЕТЬ"""
229
+ return self.chatbot.generate(query)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
  # ===================================================================
232
+ # 5. ИНТЕРФЕЙС
233
  # ===================================================================
234
 
235
+ # Загрузка
236
  articles = load_science_articles()
237
  embedder = load_embedder()
238
  embeddings = create_embeddings(articles, embedder)
239
 
240
+ # Инициализация AI
241
  if 'ai' not in st.session_state:
242
  st.session_state.ai = OpenAirAI()
243
  st.session_state.ai.initialize()
244
 
245
  ai = st.session_state.ai
246
 
247
+ # История чата
248
  if "messages" not in st.session_state:
249
  st.session_state.messages = []
250
+ # ВСЕГДА ГЕНЕРИРУЕТСЯ НЕЙРОСЕТЬЮ
251
+ greeting = ai.generate_answer("Привет! Представься и расскажи о себе за 1-2 предложения")
252
  st.session_state.messages.append({"role": "assistant", "content": greeting})
253
 
254
+ # --- БОКОВАЯ ПАНЕЛЬ ---
255
  with st.sidebar:
256
  st.image("https://cdn-icons-png.flaticon.com/512/4248/4248455.png", width=80)
257
  st.title(f"🧠 {AI_NAME}")
 
271
  **📊 Статистика:**
272
  - Статей: {len(articles)}
273
  - Сообщений: {len(st.session_state.messages)}
274
+ - Модель: {MODEL_NAME.split("/")[-1]}
275
  """)
276
 
277
+ # Кнопки управления
278
+ if st.button("🔄 Новое приветствие"):
279
+ new_greeting = ai.generate_answer("Привет! Представься заново, но по-другому")
280
+ if st.session_state.messages:
281
+ st.session_state.messages[0] = {"role": "assistant", "content": new_greeting}
282
+ st.rerun()
283
+
284
  if st.button("🗑️ Очистить чат"):
285
  st.session_state.messages = []
286
+ greeting = ai.generate_answer("Привет! Представься кратко")
287
  st.session_state.messages.append({"role": "assistant", "content": greeting})
288
  st.rerun()
289
 
290
+ # --- ОСНОВНАЯ ЧАСТЬ ---
291
  st.title(f"🧠 {AI_NAME} - Нейросетевой AI-ассистент")
292
+ st.markdown(f"**{AI_NAME}** от **{COMPANY_NAME}** | 💡 Ответы генерируются нейросетью без шаблонов")
293
 
294
+ # Отображение сообщений
295
  for message in st.session_state.messages:
296
  with st.chat_message(message["role"]):
297
  st.markdown(message["content"])
298
 
299
+ # Поле ввода
300
+ if prompt := st.chat_input("Задайте любой вопрос..."):
301
+ # Добавляем сообщение пользователя
302
  st.session_state.messages.append({"role": "user", "content": prompt})
303
  with st.chat_message("user"):
304
  st.markdown(prompt)
305
 
306
+ # Генерация ответа
307
  with st.chat_message("assistant"):
308
+ with st.spinner("🧠 Нейросеть генерирует ответ..."):
309
+ # ВСЕГДА ГЕНЕРИРУЕМ ЧЕРЕЗ НЕЙРОСЕТЬ
310
  response = ai.generate_answer(prompt)
311
+
312
+ # Ищем статьи для контекста (если есть)
313
+ articles_context = search_articles(prompt, articles, embeddings, embedder)
314
+ if articles_context and len(response) < 100:
315
+ # Добавляем статьи, НО через нейросеть
316
+ context_prompt = f"Добавь в ответ ссылки на эти статьи: {', '.join([a['title'] for a in articles_context])}"
317
+ enhanced_response = ai.generate_answer(context_prompt)
318
+ if len(enhanced_response) > len(response):
319
+ response = enhanced_response
320
+
321
  st.markdown(response)
322
  st.session_state.messages.append({"role": "assistant", "content": response})
323
 
324
  st.rerun()
325
 
326
+ # --- ПОДВАЛ ---
327
  st.divider()
328
+ st.caption(f"🧠 {AI_NAME} от {COMPANY_NAME} | Создан в {CREATION_DATE} | Генеративная нейросеть")