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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +175 -187
app.py CHANGED
@@ -1,17 +1,23 @@
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,12 +60,10 @@ except FileNotFoundError:
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,20 +83,19 @@ def get_db_connection():
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,10 +104,10 @@ def setup_database():
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,60 +117,51 @@ def setup_database():
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,17 +170,16 @@ def get_embedding_from_db(conn, table_name, crc32_column, crc32_value, model_nam
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,12 +191,10 @@ def insert_embedding(conn, table_name, movie_id, embedding_crc32, string_crc32,
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,55 +204,51 @@ def process_movies():
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,96 +265,97 @@ def get_movie_embeddings(conn):
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()
 
 
 
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
  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
 
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
  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
  );
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
  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
  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
  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
  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()