opex792 commited on
Commit
b632a36
·
verified ·
1 Parent(s): ac65d39

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +187 -175
app.py CHANGED
@@ -1,23 +1,17 @@
 
 
1
  import os
2
  import time
3
  import threading
4
  import queue
5
- from typing import List, Dict, Any, Optional
6
- import logging
7
- from urllib.parse import urlparse
8
-
9
- import gradio as gr
10
  import torch
11
  import psycopg2
12
  import zlib
13
  import numpy as np
14
- from sentence_transformers import SentenceTransformer, util
 
15
  from sklearn.preprocessing import normalize
16
 
17
- # Рекомендуется использовать python-dotenv для загрузки переменных окружения
18
- # from dotenv import load_dotenv
19
- # load_dotenv()
20
-
21
  # Настройка логирования
22
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
23
 
@@ -60,10 +54,12 @@ except FileNotFoundError:
60
  movies_data = []
61
 
62
  # Очередь для необработанных фильмов
63
- movies_queue: queue.Queue = queue.Queue()
64
 
65
- # Флаги состояния
66
  processing_complete = False
 
 
67
  search_in_progress = False
68
 
69
  # Блокировка для доступа к базе данных
@@ -83,19 +79,20 @@ def get_db_connection():
83
 
84
  def setup_database():
85
  """Настраивает базу данных: создает расширение, таблицы и индексы."""
86
- with get_db_connection() as conn:
87
- if conn is None:
88
- return
89
- try:
90
- with conn.cursor() as cur:
91
- # Создаем расширение pgvector если его нет
92
- cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
93
-
94
- # Удаляем существующие таблицы если они есть
95
- # cur.execute(f"DROP TABLE IF EXISTS {embeddings_table}, {query_cache_table};")
96
-
97
- # Создаем таблицу для хранения эмбеддингов фильмов
98
- cur.execute(f"""
 
99
  CREATE TABLE {embeddings_table} (
100
  movie_id INTEGER PRIMARY KEY,
101
  embedding_crc32 BIGINT,
@@ -104,10 +101,10 @@ def setup_database():
104
  embedding vector(1024)
105
  );
106
  CREATE INDEX ON {embeddings_table} (string_crc32);
107
- """)
108
-
109
- # Создаем таблицу для кэширования запросов
110
- cur.execute(f"""
111
  CREATE TABLE {query_cache_table} (
112
  query_crc32 BIGINT PRIMARY KEY,
113
  query TEXT,
@@ -117,51 +114,60 @@ def setup_database():
117
  );
118
  CREATE INDEX ON {query_cache_table} (query_crc32);
119
  CREATE INDEX ON {query_cache_table} (created_at);
120
- """)
121
-
122
- conn.commit()
123
- logging.info("База данных успешно настроена.")
124
- except Exception as e:
125
- logging.error(f"Ошибка при настройке базы данных: {e}")
126
- conn.rollback()
 
 
127
 
128
  # Настраиваем базу данных при запуске
129
  setup_database()
130
 
131
- def calculate_crc32(text: str) -> int:
132
  """Вычисляет CRC32 для строки."""
133
  return zlib.crc32(text.encode('utf-8')) & 0xFFFFFFFF
134
 
135
- def encode_string(text: str) -> np.ndarray:
136
  """Кодирует строку в эмбеддинг."""
137
  embedding = model.encode(text, convert_to_tensor=True, normalize_embeddings=True)
138
  return embedding.cpu().numpy()
139
 
140
- def get_movies_without_embeddings() -> List[Dict[str, Any]]:
141
  """Получает список фильм��в, для которых нужно создать эмбеддинги."""
142
- with get_db_connection() as conn:
143
- if conn is None:
144
- return []
145
- try:
146
- with conn.cursor() as cur:
147
- # Получаем список ID фильмов, которые уже есть в базе
148
- cur.execute(f"SELECT movie_id FROM {embeddings_table}")
149
- existing_ids = {row[0] for row in cur.fetchall()}
150
-
151
- # Фильтруем только те фильмы, которых нет в базе
152
- movies_to_process = [movie for movie in movies_data if movie['id'] not in existing_ids]
153
-
154
- logging.info(f"Найдено {len(movies_to_process)} фильмов для обработки.")
155
- return movies_to_process
156
- except Exception as e:
157
- logging.error(f"Ошибка при получении списка фильмов для обработки: {e}")
158
- return []
 
 
 
 
159
 
160
- def get_embedding_from_db(conn, table_name: str, crc32_column: str, crc32_value: int, model_name: str) -> Optional[np.ndarray]:
 
 
161
  """Получает эмбеддинг из базы данных."""
162
  try:
163
  with conn.cursor() as cur:
164
- cur.execute(f"SELECT embedding FROM {table_name} WHERE {crc32_column} = %s AND model_name = %s", (crc32_value, model_name))
 
165
  result = cur.fetchone()
166
  if result and result[0]:
167
  # Нормализуем эмбеддинг после извлечения из БД
@@ -170,16 +176,17 @@ def get_embedding_from_db(conn, table_name: str, crc32_column: str, crc32_value:
170
  logging.error(f"Ошибка при получении эмбеддинга из БД: {e}")
171
  return None
172
 
173
- def insert_embedding(conn, table_name: str, movie_id: int, embedding_crc32: int, string_crc32: int, embedding: np.ndarray) -> bool:
174
  """Вставляет эмбеддинг в базу данных."""
175
  try:
176
  # Нормализуем эмбеддинг перед сохранением
177
  normalized_embedding = normalize(embedding.reshape(1, -1))[0]
178
  with conn.cursor() as cur:
179
  cur.execute(f"""
180
- INSERT INTO {table_name} (movie_id, embedding_crc32, string_crc32, model_name, embedding)
181
- VALUES (%s, %s, %s, %s, %s)
182
- ON CONFLICT (movie_id) DO NOTHING
 
183
  """, (movie_id, embedding_crc32, string_crc32, model_name, normalized_embedding.tolist()))
184
  conn.commit()
185
  return True
@@ -191,10 +198,12 @@ def insert_embedding(conn, table_name: str, movie_id: int, embedding_crc32: int,
191
  def process_movies():
192
  """Обрабатывает фильмы, создавая для них эмбеддинги."""
193
  global processing_complete
 
194
  logging.info("Начало обработки фильмов.")
195
-
196
  # Получаем список фильмов, которые нужно обработать
197
  movies_to_process = get_movies_without_embeddings()
 
198
  if not movies_to_process:
199
  logging.info("Все фильмы уже обработаны.")
200
  processing_complete = True
@@ -204,51 +213,55 @@ def process_movies():
204
  for movie in movies_to_process:
205
  movies_queue.put(movie)
206
 
207
- with get_db_connection() as conn:
208
- if conn is None:
209
- processing_complete = True
210
- return
211
- try:
212
- while not movies_queue.empty():
213
- if search_in_progress:
214
- time.sleep(1)
215
- continue
216
-
217
- batch = []
218
- while not movies_queue.empty() and len(batch) < batch_size:
219
- try:
220
- movie = movies_queue.get_nowait()
221
- batch.append(movie)
222
- except queue.Empty:
223
- break
224
 
225
- if not batch:
 
 
 
 
 
 
 
 
 
 
 
226
  break
227
 
228
- logging.info(f"Обработка пакета из {len(batch)} фильмов...")
229
- for movie in batch:
230
- embedding_string = f"Название: {movie['name']}\nГод: {movie['year']}\nЖанры: {movie['genresList']}\nОписание: {movie['description']}"
231
- string_crc32 = calculate_crc32(embedding_string)
232
-
233
- # Проверяем существующий эмбеддинг
234
- existing_embedding = get_embedding_from_db(conn, embeddings_table, "string_crc32", string_crc32, model_name)
235
- if existing_embedding is None:
236
- embedding = encode_string(embedding_string)
237
- embedding_crc32 = calculate_crc32(str(embedding.tolist()))
238
- if insert_embedding(conn, embeddings_table, movie['id'], embedding_crc32, string_crc32, embedding):
239
- logging.info(f"Сохранен эмбеддинг для '{movie['name']}'")
240
- else:
241
- logging.error(f"Ошибка сохранения эмбеддинга для '{movie['name']}'")
242
- else:
243
- logging.info(f"Эмбеддинг для '{movie['name']}' уже существует")
244
 
245
- except Exception as e:
246
- logging.error(f"Ошибка при обработке фильмов: {e}")
247
- finally:
248
- processing_complete = True
249
- logging.info("Обработка фильмов завершена")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
- def get_movie_embeddings(conn) -> Dict[str, np.ndarray]:
252
  """Загружает все эмбеддинги фильмов из базы данных."""
253
  movie_embeddings = {}
254
  try:
@@ -265,97 +278,96 @@ def get_movie_embeddings(conn) -> Dict[str, np.ndarray]:
265
  logging.error(f"Ошибка при загрузке эмбеддингов фильмов: {e}")
266
  return movie_embeddings
267
 
268
- def clean_query_cache(conn):
269
- """Очищает устаревшие записи из кэша запросов."""
270
- try:
271
- with conn.cursor() as cur:
272
- # Получаем общий размер кэша
273
- cur.execute(f"SELECT pg_total_relation_size('{query_cache_table}')")
274
- total_size = cur.fetchone()[0]
275
-
276
- if total_size > MAX_CACHE_SIZE:
277
- # Удаляем старые записи, пока размер не стан��т меньше максимального
278
- cur.execute(f"""
279
- DELETE FROM {query_cache_table}
280
- WHERE ctid IN (
281
- SELECT ctid
282
- FROM {query_cache_table}
283
- ORDER BY created_at ASC
284
- LIMIT (SELECT COUNT(*) / 2 FROM {query_cache_table})
285
- )
286
- """)
287
- conn.commit()
288
- logging.info("Кэш запросов очищен.")
289
- except Exception as e:
290
- logging.error(f"Ошибка при очистке кэша запросов: {e}")
291
- conn.rollback()
292
-
293
- def search_movies(query: str, top_k: int = 10) -> List[Dict[str, Any]]:
294
  """Выполняет поиск фильмов по запросу."""
295
  global search_in_progress
296
  search_in_progress = True
297
-
 
298
  try:
299
- with get_db_connection() as conn:
300
- if conn is None:
301
- return []
302
 
303
- clean_query_cache(conn)
 
304
 
305
- query_crc32 = calculate_crc32(query)
306
- query_embedding = get_embedding_from_db(conn, query_cache_table, "query_crc32", query_crc32, model_name)
307
 
308
- if query_embedding is None:
309
- query_embedding = encode_string(query)
310
- insert_embedding(conn, query_cache_table, -1, -1, query_crc32, query_embedding)
 
 
 
 
 
 
 
 
 
311
 
312
- movie_embeddings = get_movie_embeddings(conn)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
 
314
- # Вычисляем косинусное сходство
315
- similarities = util.cos_sim(query_embedding, list(movie_embeddings.values()))[0]
 
 
 
 
 
 
316
 
317
- # Сортируем результаты
318
- top_results = sorted(zip(similarities, movie_embeddings.keys()), key=lambda x: x[0], reverse=True)[:top_k]
 
 
 
 
 
 
319
 
320
- results = []
321
- for score, movie_name in top_results:
322
- movie = next((m for m in movies_data if m['name'] == movie_name), None)
323
- if movie:
324
- results.append({
325
- "name": movie['name'],
326
- "year": movie['year'],
327
- "genres": movie['genresList'],
328
- "description": movie['description'],
329
- "score": float(score)
330
- })
331
-
332
- return results
333
  except Exception as e:
334
- logging.error(f"Ошибка при поиске фильмов: {e}")
335
- return []
 
336
  finally:
 
 
337
  search_in_progress = False
338
 
339
  # Запускаем обработку фильмов в отдельном потоке
340
- threading.Thread(target=process_movies, daemon=True).start()
 
341
 
342
  # Создаем интерфейс Gradio
343
- def gradio_search(query: str) -> str:
344
- results = search_movies(query)
345
- output = ""
346
- for movie in results:
347
- output += f"Название: {movie['name']} ({movie['year']})\n"
348
- output += f"Жанры: {', '.join(movie['genres'])}\n"
349
- output += f"Описание: {movie['description']}\n"
350
- output += f"Релевантность: {movie['score']:.2f}\n\n"
351
- return output
352
-
353
  iface = gr.Interface(
354
- fn=gradio_search,
355
- inputs="text",
356
- outputs="text",
357
- title="Поиск фильмов",
358
- description="Введите запрос для поиска фильмов"
359
  )
360
 
 
361
  iface.launch()
 
1
+ import gradio as gr
2
+ from sentence_transformers import SentenceTransformer, util
3
  import os
4
  import time
5
  import threading
6
  import queue
 
 
 
 
 
7
  import torch
8
  import psycopg2
9
  import zlib
10
  import numpy as np
11
+ from urllib.parse import urlparse
12
+ import logging
13
  from sklearn.preprocessing import normalize
14
 
 
 
 
 
15
  # Настройка логирования
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
17
 
 
54
  movies_data = []
55
 
56
  # Очередь для необработанных фильмов
57
+ movies_queue = queue.Queue()
58
 
59
+ # Флаг, указывающий, что обработка фильмов завершена
60
  processing_complete = False
61
+
62
+ # Флаг, указывающий, что выполняется поиск
63
  search_in_progress = False
64
 
65
  # Блокировка для доступа к базе данных
 
79
 
80
  def setup_database():
81
  """Настраивает базу данных: создает расширение, таблицы и индексы."""
82
+ conn = get_db_connection()
83
+ if conn is None:
84
+ return
85
+
86
+ try:
87
+ with conn.cursor() as cur:
88
+ # Создаем расширение pgvector если его нет
89
+ cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
90
+
91
+ # Удаляем существующие таблицы если они есть
92
+ # cur.execute(f"DROP TABLE IF EXISTS {embeddings_table}, {query_cache_table};")
93
+
94
+ # Создаем таблицу для хранения эмбеддингов фильмов
95
+ cur.execute(f"""
96
  CREATE TABLE {embeddings_table} (
97
  movie_id INTEGER PRIMARY KEY,
98
  embedding_crc32 BIGINT,
 
101
  embedding vector(1024)
102
  );
103
  CREATE INDEX ON {embeddings_table} (string_crc32);
104
+ """)
105
+
106
+ # Создаем таблицу для кэширования запросов
107
+ cur.execute(f"""
108
  CREATE TABLE {query_cache_table} (
109
  query_crc32 BIGINT PRIMARY KEY,
110
  query TEXT,
 
114
  );
115
  CREATE INDEX ON {query_cache_table} (query_crc32);
116
  CREATE INDEX ON {query_cache_table} (created_at);
117
+ """)
118
+
119
+ conn.commit()
120
+ logging.info("База данных успешно настроена.")
121
+ except Exception as e:
122
+ logging.error(f"Ошибка при настройке базы данных: {e}")
123
+ conn.rollback()
124
+ finally:
125
+ conn.close()
126
 
127
  # Настраиваем базу данных при запуске
128
  setup_database()
129
 
130
+ def calculate_crc32(text):
131
  """Вычисляет CRC32 для строки."""
132
  return zlib.crc32(text.encode('utf-8')) & 0xFFFFFFFF
133
 
134
+ def encode_string(text):
135
  """Кодирует строку в эмбеддинг."""
136
  embedding = model.encode(text, convert_to_tensor=True, normalize_embeddings=True)
137
  return embedding.cpu().numpy()
138
 
139
+ def get_movies_without_embeddings():
140
  """Получает список фильм��в, для которых нужно создать эмбеддинги."""
141
+ conn = get_db_connection()
142
+ if conn is None:
143
+ return []
144
+
145
+ movies_to_process = []
146
+ try:
147
+ with conn.cursor() as cur:
148
+ # Получаем список ID фильмов, которые уже есть в базе
149
+ cur.execute(f"SELECT movie_id FROM {embeddings_table}")
150
+ existing_ids = {row[0] for row in cur.fetchall()}
151
+
152
+ # Фильтруем только те фильмы, которых нет в базе
153
+ for movie in movies_data:
154
+ if movie['id'] not in existing_ids:
155
+ movies_to_process.append(movie)
156
+
157
+ logging.info(f"Найдено {len(movies_to_process)} фильмов для обработки.")
158
+ except Exception as e:
159
+ logging.error(f"Ошибка при получении списка фильмов для обработки: {e}")
160
+ finally:
161
+ conn.close()
162
 
163
+ return movies_to_process
164
+
165
+ def get_embedding_from_db(conn, table_name, crc32_column, crc32_value, model_name):
166
  """Получает эмбеддинг из базы данных."""
167
  try:
168
  with conn.cursor() as cur:
169
+ cur.execute(f"SELECT embedding FROM {table_name} WHERE {crc32_column} = %s AND model_name = %s",
170
+ (crc32_value, model_name))
171
  result = cur.fetchone()
172
  if result and result[0]:
173
  # Нормализуем эмбеддинг после извлечения из БД
 
176
  logging.error(f"Ошибка при получении эмбеддинга из БД: {e}")
177
  return None
178
 
179
+ def insert_embedding(conn, table_name, movie_id, embedding_crc32, string_crc32, embedding):
180
  """Вставляет эмбеддинг в базу данных."""
181
  try:
182
  # Нормализуем эмбеддинг перед сохранением
183
  normalized_embedding = normalize(embedding.reshape(1, -1))[0]
184
  with conn.cursor() as cur:
185
  cur.execute(f"""
186
+ INSERT INTO {table_name}
187
+ (movie_id, embedding_crc32, string_crc32, model_name, embedding)
188
+ VALUES (%s, %s, %s, %s, %s)
189
+ ON CONFLICT (movie_id) DO NOTHING
190
  """, (movie_id, embedding_crc32, string_crc32, model_name, normalized_embedding.tolist()))
191
  conn.commit()
192
  return True
 
198
  def process_movies():
199
  """Обрабатывает фильмы, создавая для них эмбеддинги."""
200
  global processing_complete
201
+
202
  logging.info("Начало обработки фильмов.")
203
+
204
  # Получаем список фильмов, которые нужно обработать
205
  movies_to_process = get_movies_without_embeddings()
206
+
207
  if not movies_to_process:
208
  logging.info("Все фильмы уже обработаны.")
209
  processing_complete = True
 
213
  for movie in movies_to_process:
214
  movies_queue.put(movie)
215
 
216
+ conn = get_db_connection()
217
+ if conn is None:
218
+ processing_complete = True
219
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
+ try:
222
+ while not movies_queue.empty():
223
+ if search_in_progress:
224
+ time.sleep(1)
225
+ continue
226
+
227
+ batch = []
228
+ while not movies_queue.empty() and len(batch) < batch_size:
229
+ try:
230
+ movie = movies_queue.get_nowait()
231
+ batch.append(movie)
232
+ except queue.Empty:
233
  break
234
 
235
+ if not batch:
236
+ break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
 
238
+ logging.info(f"Обработка пакета из {len(batch)} фильмов...")
239
+
240
+ for movie in batch:
241
+ embedding_string = f"Название: {movie['name']}\nГод: {movie['year']}\nЖанры: {movie['genresList']}\nОписание: {movie['description']}"
242
+ string_crc32 = calculate_crc32(embedding_string)
243
+
244
+ # Проверяем существующий эмбеддинг
245
+ existing_embedding = get_embedding_from_db(conn, embeddings_table, "string_crc32", string_crc32, model_name)
246
+
247
+ if existing_embedding is None:
248
+ embedding = encode_string(embedding_string)
249
+ embedding_crc32 = calculate_crc32(str(embedding.tolist()))
250
+
251
+ if insert_embedding(conn, embeddings_table, movie['id'], embedding_crc32, string_crc32, embedding):
252
+ logging.info(f"Сохранен эмбеддинг для '{movie['name']}'")
253
+ else:
254
+ logging.error(f"Ошибка сохранения эмбеддинга для '{movie['name']}'")
255
+ else:
256
+ logging.info(f"Эмбеддинг для '{movie['name']}' уже существует")
257
+ except Exception as e:
258
+ logging.error(f"Ошибка при обработке фильмов: {e}")
259
+ finally:
260
+ conn.close()
261
+ processing_complete = True
262
+ logging.info("Обработка фильмов завершена")
263
 
264
+ def get_movie_embeddings(conn):
265
  """Загружает все эмбеддинги фильмов из базы данных."""
266
  movie_embeddings = {}
267
  try:
 
278
  logging.error(f"Ошибка при загрузке эмбеддингов фильмов: {e}")
279
  return movie_embeddings
280
 
281
+ def search_movies(query, top_k=10):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  """Выполняет поиск фильмов по запросу."""
283
  global search_in_progress
284
  search_in_progress = True
285
+ start_time = time.time()
286
+
287
  try:
288
+ conn = get_db_connection()
289
+ if conn is None:
290
+ return "<p>Ошибка подключения к базе данных</p>"
291
 
292
+ query_crc32 = calculate_crc32(query)
293
+ query_embedding = get_embedding_from_db(conn, query_cache_table, "query_crc32", query_crc32, model_name)
294
 
295
+ if query_embedding is None:
296
+ query_embedding = encode_string(query)
297
 
298
+ try:
299
+ with conn.cursor() as cur:
300
+ cur.execute(f"""
301
+ INSERT INTO {query_cache_table} (query_crc32, query, model_name, embedding)
302
+ VALUES (%s, %s, %s, %s)
303
+ ON CONFLICT (query_crc32) DO NOTHING
304
+ """, (query_crc32, query, model_name, query_embedding.tolist()))
305
+ conn.commit()
306
+ logging.info(f"Сохранен новый эмбеддинг запроса: {query}")
307
+ except Exception as e:
308
+ logging.error(f"Ошибка при сохранении эмбеддинга запроса: {e}")
309
+ conn.rollback()
310
 
311
+ # Используем косинусное расстояние для поиска
312
+ try:
313
+ with conn.cursor() as cur:
314
+ cur.execute(f"""
315
+ WITH query_embedding AS (
316
+ SELECT embedding
317
+ FROM {query_cache_table}
318
+ WHERE query_crc32 = %s
319
+ )
320
+ SELECT m.movie_id, 1 - (m.embedding <=> (SELECT embedding FROM query_embedding)) as similarity
321
+ FROM {embeddings_table} m, query_embedding
322
+ ORDER BY similarity DESC
323
+ LIMIT %s
324
+ """, (query_crc32, top_k))
325
+
326
+ results = cur.fetchall()
327
+ logging.info(f"Найдено {len(results)} результатов поиска.")
328
+ except Exception as e:
329
+ logging.error(f"Ошибка при выполнении поискового запроса: {e}")
330
+ results = []
331
 
332
+ results_html = "<ol>"
333
+ for movie_id, similarity in results:
334
+ # Находим название фильма по ID
335
+ movie_title = None
336
+ for movie in movies_data:
337
+ if movie['id'] == movie_id:
338
+ movie_title = movie['name']
339
+ break
340
 
341
+ if movie_title:
342
+ results_html += f"<li><strong>{movie_title}</strong> (Сходство: {similarity:.4f})</li>"
343
+ results_html += "</ol>"
344
+
345
+ search_time = time.time() - start_time
346
+ logging.info(f"Поиск выполнен за {search_time:.2f} секунд.")
347
+
348
+ return f"<p>Время поиска: {search_time:.2f} сек</p>{results_html}"
349
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  except Exception as e:
351
+ logging.error(f"Ошибка при выполнении поиска: {e}")
352
+ return "<p>Произошла ошибка при выполнении поиска.</p>"
353
+
354
  finally:
355
+ if conn:
356
+ conn.close()
357
  search_in_progress = False
358
 
359
  # Запускаем обработку фильмов в отдельном потоке
360
+ processing_thread = threading.Thread(target=process_movies)
361
+ processing_thread.start()
362
 
363
  # Создаем интерфейс Gradio
 
 
 
 
 
 
 
 
 
 
364
  iface = gr.Interface(
365
+ fn=search_movies,
366
+ inputs=gr.Textbox(lines=2, placeholder="Введите запрос для поиска фильмов..."),
367
+ outputs=gr.HTML(label="Результаты поиска"),
368
+ title="Семантический поиск фильмов",
369
+ description="Введите описание фильма, который вы ищете, и система найдет наиболее похожие фильмы."
370
  )
371
 
372
+ # Запускаем интерфейс
373
  iface.launch()