SAkizuki commited on
Commit
d632079
·
verified ·
1 Parent(s): 64e72db

Auto-sync from GitHub Actions

Browse files
Files changed (5) hide show
  1. api_fastapi.py +1 -3
  2. core/engine.py +65 -22
  3. core/models.py +2 -1
  4. mcp_server.py +3 -3
  5. ui_nicegui.py +46 -17
api_fastapi.py CHANGED
@@ -26,7 +26,6 @@ FastAPI 适配层(可选)。
26
 
27
  from __future__ import annotations
28
 
29
- import asyncio
30
  from fastapi import FastAPI
31
  from pydantic import BaseModel, Field
32
 
@@ -130,8 +129,7 @@ async def related(body: RelatedIn) -> list[RelatedTagOut]:
130
  - show_nsfw:是否包含 NSFW 标签,默认 True
131
  """
132
  tagger = await DanbooruTagger.get_instance()
133
- results = await asyncio.to_thread(
134
- tagger.get_related,
135
  body.tags,
136
  set(body.tags), # exclude 已选标签自身
137
  body.limit,
 
26
 
27
  from __future__ import annotations
28
 
 
29
  from fastapi import FastAPI
30
  from pydantic import BaseModel, Field
31
 
 
129
  - show_nsfw:是否包含 NSFW 标签,默认 True
130
  """
131
  tagger = await DanbooruTagger.get_instance()
132
+ results = await tagger.get_related_async(
 
133
  body.tags,
134
  set(body.tags), # exclude 已选标签自身
135
  body.limit,
core/engine.py CHANGED
@@ -163,9 +163,9 @@ class DanbooruTagger:
163
 
164
  _instance: Optional['DanbooruTagger'] = None
165
  _lock: Optional[asyncio.Lock] = None
166
- # 进程级搜索并发信号量:串行化 search(),避免多个 model.encode()
167
- # 并发抢占 CPU 而拖垮事件循环。
168
- _search_sem: Optional[asyncio.Semaphore] = None
169
 
170
  @classmethod
171
  def is_ready(cls) -> bool:
@@ -346,10 +346,15 @@ class DanbooruTagger:
346
 
347
  # ── 搜索 ──────────────────────────────────────────────────────────────
348
 
349
- def _encode_queries(self, queries: list[str]) -> torch.Tensor:
350
- """批量编码查询词,命中 embedding 缓存的跳过 model.encode。"""
 
 
 
 
351
  cached_vecs: list[Optional[torch.Tensor]] = [self._emb_cache.get(q) for q in queries]
352
  uncached_idx = [i for i, v in enumerate(cached_vecs) if v is None]
 
353
 
354
  if uncached_idx:
355
  uncached_texts = [queries[i] for i in uncached_idx]
@@ -362,7 +367,7 @@ class DanbooruTagger:
362
  self._emb_cache.put(queries[i], emb)
363
  cached_vecs[i] = emb
364
 
365
- return torch.stack(cached_vecs) # type: ignore[arg-type]
366
 
367
  def search(self, request: SearchRequest) -> SearchResponse:
368
  if not self.is_loaded:
@@ -396,7 +401,7 @@ class DanbooruTagger:
396
  extra_segments = []
397
  queries = [request.query]
398
 
399
- q_emb = self._encode_queries(queries)
400
 
401
  tl = request.target_layers
402
  k = request.top_k
@@ -473,17 +478,23 @@ class DanbooruTagger:
473
  # 对每个候选标签,计算其与完整原始查询(而非分词片段)的语义相似度,
474
  # 将相似度作为软因子乘入 final_score,使仅由分词碎片匹配到的噪声
475
  # 标签自然下沉,同时不硬过滤任何结果。
 
476
  full_q = q_emb[0] # queries[0] 始终为完整原始查询
477
- alpha = 0.3 if SearchRequest.use_segmentation else 0 # 一致性调节强度(0=不调节, 1=完全按一致性重排),仅在启用分词时有意义
478
- for r in final.values():
479
- idx = self._name_to_idx[r.tag]
480
- max_co = 0.0
 
 
481
  for ln, attr, _ in _LAYER_SPEC:
482
  if ln not in tl:
483
  continue
484
- max_co = max(max_co, float(torch.dot(full_q, getattr(self, attr)[idx])))
485
- # coherence=0 时最多扣 alpha=15%;coherence=1 时不扣分
486
- r.final_score = round(r.final_score * (1.0 - alpha + alpha * max_co), 4)
 
 
 
487
 
488
  # Group expand 处理(在 guaranteed_tags 之前,因为会改分数)
489
  if request.group_mode == "expand" and self._tag_to_groups:
@@ -537,27 +548,59 @@ class DanbooruTagger:
537
 
538
  tags_all = ', '.join(r.tag for r in valid)
539
  tags_sfw = ', '.join(r.tag for r in valid if r.nsfw != '1')
 
540
  response = SearchResponse(
541
  tags_all=tags_all, tags_sfw=tags_sfw,
542
  results=valid, keywords=keywords, segments=extra_segments,
 
543
  )
544
  self._search_cache.put(cache_key, response)
545
  return response
546
 
 
 
547
  @classmethod
548
- def _get_search_sem(cls) -> asyncio.Semaphore:
549
- if cls._search_sem is None:
550
- cls._search_sem = asyncio.Semaphore(1)
551
- return cls._search_sem
552
 
553
  async def search_async(self, request: SearchRequest) -> SearchResponse:
554
- """search() 的并发安全异步封装:信号量串行化 + 线程池执行。
555
 
556
  所有异步入口(MCP / API / UI)都应改用本方法,而非各自
557
- asyncio.to_thread(self.search),以共享同一个并发闸门。
 
558
  """
559
- async with self._get_search_sem():
560
- return await asyncio.to_thread(self.search, request)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
 
562
  def _apply_group_expand(self, final: dict[str, TagResult]) -> None:
563
  """expand 模式:提升同 group 标签的分数。"""
 
163
 
164
  _instance: Optional['DanbooruTagger'] = None
165
  _lock: Optional[asyncio.Lock] = None
166
+ # 进程级 CPU 并发闸门:串行化所有 CPU 密集型操作(search / get_related /
167
+ # get_group_candidates),避免并发抢占 CPU 而拖垮 asyncio 事件循环。
168
+ _cpu_sem: Optional[asyncio.Semaphore] = None
169
 
170
  @classmethod
171
  def is_ready(cls) -> bool:
 
346
 
347
  # ── 搜索 ──────────────────────────────────────────────────────────────
348
 
349
+ def _encode_queries(self, queries: list[str]) -> tuple[torch.Tensor, list[bool]]:
350
+ """批量编码查询词,命中 embedding 缓存的跳过 model.encode。
351
+
352
+ Returns:
353
+ (q_emb, hit_mask): 编码后的张量 (Q, D) 以及每个 query 是否命中缓存。
354
+ """
355
  cached_vecs: list[Optional[torch.Tensor]] = [self._emb_cache.get(q) for q in queries]
356
  uncached_idx = [i for i, v in enumerate(cached_vecs) if v is None]
357
+ hit_mask = [v is not None for v in cached_vecs]
358
 
359
  if uncached_idx:
360
  uncached_texts = [queries[i] for i in uncached_idx]
 
367
  self._emb_cache.put(queries[i], emb)
368
  cached_vecs[i] = emb
369
 
370
+ return torch.stack(cached_vecs), hit_mask # type: ignore[arg-type]
371
 
372
  def search(self, request: SearchRequest) -> SearchResponse:
373
  if not self.is_loaded:
 
401
  extra_segments = []
402
  queries = [request.query]
403
 
404
+ q_emb, hit_mask = self._encode_queries(queries)
405
 
406
  tl = request.target_layers
407
  k = request.top_k
 
478
  # 对每个候选标签,计算其与完整原始查询(而非分词片段)的语义相似度,
479
  # 将相似度作为软因子乘入 final_score,使仅由分词碎片匹配到的噪声
480
  # 标签自然下沉,同时不硬过滤任何结果。
481
+ # 批量矩阵乘法替代逐条 torch.dot,O(R*L) 降为 O(L) + O(R)。
482
  full_q = q_emb[0] # queries[0] 始终为完整原始查询
483
+ alpha = 0.3 if request.use_segmentation else 0 # 一致性调节强度(0=不调节, 1=完全按一致性重排),仅在启用分词时有意义
484
+ if alpha > 0 and final:
485
+ tag_list = list(final.keys())
486
+ tag_indices = [self._name_to_idx[t] for t in tag_list]
487
+ idx_tensor = torch.tensor(tag_indices, dtype=torch.long, device=full_q.device)
488
+ max_co = torch.zeros(len(tag_indices), device=full_q.device)
489
  for ln, attr, _ in _LAYER_SPEC:
490
  if ln not in tl:
491
  continue
492
+ emb_selected = getattr(self, attr)[idx_tensor] # (R, D)
493
+ co = (full_q.unsqueeze(0) @ emb_selected.T).squeeze(0) # (R,)
494
+ max_co = torch.maximum(max_co, co)
495
+ for i, tag in enumerate(tag_list):
496
+ r = final[tag]
497
+ r.final_score = round(r.final_score * (1.0 - alpha + alpha * float(max_co[i])), 4)
498
 
499
  # Group expand 处理(在 guaranteed_tags 之前,因为会改分数)
500
  if request.group_mode == "expand" and self._tag_to_groups:
 
548
 
549
  tags_all = ', '.join(r.tag for r in valid)
550
  tags_sfw = ', '.join(r.tag for r in valid if r.nsfw != '1')
551
+ cached_queries = [q for q, hit in zip(queries, hit_mask) if hit]
552
  response = SearchResponse(
553
  tags_all=tags_all, tags_sfw=tags_sfw,
554
  results=valid, keywords=keywords, segments=extra_segments,
555
+ cached_queries=cached_queries,
556
  )
557
  self._search_cache.put(cache_key, response)
558
  return response
559
 
560
+ # ── CPU 并发闸门(类级信号量,所有 CPU 密集型操作共享)───────────────
561
+
562
  @classmethod
563
+ def _get_cpu_sem(cls) -> asyncio.Semaphore:
564
+ if cls._cpu_sem is None:
565
+ cls._cpu_sem = asyncio.Semaphore(1)
566
+ return cls._cpu_sem
567
 
568
  async def search_async(self, request: SearchRequest) -> SearchResponse:
569
+ """search() 的并发安全异步封装:共享闸门串行化 + 线程池执行。
570
 
571
  所有异步入口(MCP / API / UI)都应改用本方法,而非各自
572
+ asyncio.to_thread(self.search),以共享同一个 CPU 并发闸门。
573
+ 包含 60 秒超时,防止异常卡死导致信号量永久泄漏。
574
  """
575
+ async with self._get_cpu_sem():
576
+ return await asyncio.wait_for(
577
+ asyncio.to_thread(self.search, request),
578
+ timeout=60.0,
579
+ )
580
+
581
+ async def get_related_async(
582
+ self,
583
+ seed_tags: list[str],
584
+ exclude: set[str] | None = None,
585
+ limit: int = 20,
586
+ show_nsfw: bool = True,
587
+ ) -> list:
588
+ """get_related() 的并发安全异步封装,共享同一个 CPU 闸门。"""
589
+ async with self._get_cpu_sem():
590
+ return await asyncio.to_thread(
591
+ self.get_related, seed_tags, exclude, limit, show_nsfw,
592
+ )
593
+
594
+ async def get_group_candidates_async(
595
+ self,
596
+ selected_tags: list[str],
597
+ show_nsfw: bool = True,
598
+ ) -> list[dict]:
599
+ """get_group_candidates() 的并发安全异步封装,共享同一个 CPU 闸门。"""
600
+ async with self._get_cpu_sem():
601
+ return await asyncio.to_thread(
602
+ self.get_group_candidates, selected_tags, show_nsfw,
603
+ )
604
 
605
  def _apply_group_expand(self, final: dict[str, TagResult]) -> None:
606
  """expand 模式:提升同 group 标签的分数。"""
core/models.py CHANGED
@@ -60,4 +60,5 @@ class SearchResponse:
60
  tags_sfw: str
61
  results: list[TagResult]
62
  keywords: list[str]
63
- segments: list[str] = field(default_factory=list) # 分隔符切分后的原始从句级片段
 
 
60
  tags_sfw: str
61
  results: list[TagResult]
62
  keywords: list[str]
63
+ segments: list[str] = field(default_factory=list) # 分隔符切分后的原始从句级片段
64
+ cached_queries: list[str] = field(default_factory=list) # 命中 emb 缓存的查询文本
mcp_server.py CHANGED
@@ -64,8 +64,9 @@ Only supported for general, copyright, and character tag searches; **artists and
64
  - search_mode: Preset strategy. Pick the one that matches your intent.
65
  "full_scene" — Full scene → prompt (e.g. "一个穿着白色水手服的少女在雨中奔跑")
66
  "concept_explore" — Vague concept exploration, broad recall (e.g. "赛博朋克服装", "兔耳朵", "中国风汉服")
67
- "subject_describe" — Describe a subject to find matching tags (e.g. "EVA中蓝发的驾驶员", "两侧有开口,前方有拉绳的运动短裤")
68
  "precise_lookup" — Precise lookup / spell fix (e.g. "selafuku", "thighhigh")
 
69
  - category: Filter to a specific tag category. Default "all".
70
  "all" — All (通用 + 版权 + 人物)
71
  "general" — Visual attributes, clothing, pose, background, etc.
@@ -258,8 +259,7 @@ JSON array sorted by aggregated NPMI score (descending). Each result:
258
  elif t in corrections:
259
  corrected_tags.append(corrections[t])
260
 
261
- results = await asyncio.to_thread(
262
- tagger.get_related,
263
  corrected_tags,
264
  set(corrected_tags),
265
  limit,
 
64
  - search_mode: Preset strategy. Pick the one that matches your intent.
65
  "full_scene" — Full scene → prompt (e.g. "一个穿着白色水手服的少女在雨中奔跑")
66
  "concept_explore" — Vague concept exploration, broad recall (e.g. "赛博朋克服装", "兔耳朵", "中国风汉服")
67
+ "subject_describe" — Describe **one** subject to find matching tags (e.g. "EVA中蓝发的驾驶员", "两侧有开口,前方有拉绳的运动短裤")
68
  "precise_lookup" — Precise lookup / spell fix (e.g. "selafuku", "thighhigh")
69
+ - HINT: In subject_describe mode, the tokenizer is disabled, and you can only describe one thing at a time. To search for multiple things at once, use concept_explore or full_scene.
70
  - category: Filter to a specific tag category. Default "all".
71
  "all" — All (通用 + 版权 + 人物)
72
  "general" — Visual attributes, clothing, pose, background, etc.
 
259
  elif t in corrections:
260
  corrected_tags.append(corrections[t])
261
 
262
+ results = await tagger.get_related_async(
 
263
  corrected_tags,
264
  set(corrected_tags),
265
  limit,
ui_nicegui.py CHANGED
@@ -56,6 +56,18 @@ class _SuppressMCPNoise(logging.Filter):
56
 
57
  logging.getLogger("uvicorn.error").addFilter(_SuppressMCPNoise())
58
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  # ── 表格列定义 ─────────────────────────────────────────────────────────────────
60
 
61
  TABLE_COLUMNS = [
@@ -150,6 +162,9 @@ class DanbooruSearchUI:
150
  self.selected_chips_container = None # 已选标签 chip 容器
151
  self.current_related: list = []
152
  self.chip_extra_selected: set = set()
 
 
 
153
 
154
  # tag -> prompt 权重,范围 [0.1, 1.9],默认 1.0
155
  self.tag_weights: dict[str, float] = {}
@@ -1233,24 +1248,31 @@ class DanbooruSearchUI:
1233
  # 分词筛选 chips
1234
  self.current_filter_keyword = 'ALL' # 新搜索默认选中"全部"
1235
  self.keywords_container.clear()
 
1236
  with self.keywords_container:
1237
  ui.label('分词筛选:').classes('text-sm text-gray-500 font-bold mr-2')
1238
  ui.chip('全部', on_click=lambda: self._filter_by_source('ALL')) \
1239
  .props('color=primary text-color=white clickable')
1240
  use_seg = self.input_segment.value if self.input_segment else True
1241
  if use_seg:
1242
- ui.chip('整句',
1243
- on_click=lambda: self._filter_by_source(self.current_query_str)) \
1244
- .props('color=grey-4 text-color=black clickable')
 
 
1245
  # 从句级原始片段(分隔符切分后未 jieba 的长片段,区别于关键词)
1246
  for seg in response.segments:
1247
- ui.chip(seg,
1248
- on_click=lambda s=seg: self._filter_by_source(s)) \
1249
- .props('color=blue-1 text-color=blue-8 clickable')
 
 
1250
  for kw in response.keywords:
1251
- ui.chip(kw,
1252
- on_click=lambda k=kw: self._filter_by_source(k)) \
1253
- .props('color=grey-4 text-color=black clickable')
 
 
1254
  else:
1255
  ui.label('(分词已关闭)').classes('text-xs text-gray-400')
1256
 
@@ -1423,25 +1445,31 @@ class DanbooruSearchUI:
1423
  self._render_related_list(merged, show_nsfw)
1424
 
1425
  def _refresh_related_from_selection(self, selected_tags: list[str], show_nsfw: bool):
1426
- """仅刷新关联推荐列表。"""
 
 
 
1427
  async def _do():
 
1428
  if not selected_tags:
1429
  self._refresh_related([], show_nsfw)
1430
  return
1431
  tagger = await DanbooruTagger.get_instance()
1432
- related = await run.io_bound(
1433
- tagger.get_related,
1434
  selected_tags,
1435
  set(selected_tags),
1436
  50,
1437
  show_nsfw,
1438
  )
1439
  self._refresh_related(related, show_nsfw)
1440
- asyncio.ensure_future(_do())
1441
 
1442
  def _refresh_group_from_selection(self, selected_tags: list[str], show_nsfw: bool):
1443
- """仅刷新同类扩展区域。"""
 
 
1444
  async def _do():
 
1445
  if not selected_tags:
1446
  if self.group_expansion_container is not None:
1447
  self.group_expansion_container.clear()
@@ -1449,13 +1477,12 @@ class DanbooruSearchUI:
1449
  ui.label('请先搜索并勾选标签…').classes('text-sm text-gray-400 italic p-4')
1450
  return
1451
  tagger = await DanbooruTagger.get_instance()
1452
- group_data = await run.io_bound(
1453
- tagger.get_group_candidates,
1454
  selected_tags,
1455
  show_nsfw,
1456
  )
1457
  self._render_group_expansion(group_data, selected_tags, show_nsfw)
1458
- asyncio.ensure_future(_do())
1459
 
1460
  def _render_group_expansion(self, group_data: list, selected_tags: list[str], show_nsfw: bool):
1461
  """渲染 Group 同类扩展区域。"""
@@ -1753,6 +1780,7 @@ if __name__ in {'__main__', '__mp_main__'}:
1753
  async def head_root():
1754
  return PlainTextResponse('')
1755
 
 
1756
  ui.run(
1757
  host=host,
1758
  port=port,
@@ -1761,3 +1789,4 @@ if __name__ in {'__main__', '__mp_main__'}:
1761
  show=not is_cloud(),
1762
  reconnect_timeout=120,
1763
  )
 
 
56
 
57
  logging.getLogger("uvicorn.error").addFilter(_SuppressMCPNoise())
58
 
59
+ # suppress MCP OAuth discovery 404 noise (clients probing .well-known/oauth-authorization-server)
60
+ class _SuppressOAuthNoise(logging.Filter):
61
+ _MARKER = ".well-known/oauth-authorization-server"
62
+
63
+ def filter(self, record: logging.LogRecord) -> bool:
64
+ if self._MARKER in record.getMessage():
65
+ return False
66
+ return True
67
+
68
+ logging.getLogger("uvicorn.access").addFilter(_SuppressOAuthNoise())
69
+ logging.getLogger("nicegui").addFilter(_SuppressOAuthNoise())
70
+
71
  # ── 表格列定义 ─────────────────────────────────────────────────────────────────
72
 
73
  TABLE_COLUMNS = [
 
162
  self.selected_chips_container = None # 已选标签 chip 容器
163
  self.current_related: list = []
164
  self.chip_extra_selected: set = set()
165
+ # 去抖任务句柄(取消旧任务避免 CPU 洪峰)
166
+ self._debounce_related_task = None # type: asyncio.Task | None
167
+ self._debounce_group_task = None # type: asyncio.Task | None
168
 
169
  # tag -> prompt 权重,范围 [0.1, 1.9],默认 1.0
170
  self.tag_weights: dict[str, float] = {}
 
1248
  # 分词筛选 chips
1249
  self.current_filter_keyword = 'ALL' # 新搜索默认选中"全部"
1250
  self.keywords_container.clear()
1251
+ cached_set = set(response.cached_queries) if response.cached_queries else set()
1252
  with self.keywords_container:
1253
  ui.label('分词筛选:').classes('text-sm text-gray-500 font-bold mr-2')
1254
  ui.chip('全部', on_click=lambda: self._filter_by_source('ALL')) \
1255
  .props('color=primary text-color=white clickable')
1256
  use_seg = self.input_segment.value if self.input_segment else True
1257
  if use_seg:
1258
+ whole = ui.chip('整句',
1259
+ on_click=lambda: self._filter_by_source(self.current_query_str))
1260
+ whole.props('color=grey-4 text-color=black clickable')
1261
+ if self.current_query_str in cached_set:
1262
+ whole.style('outline: 1px dashed rgba(128,128,128,0.3); outline-offset: 1px;')
1263
  # 从句级原始片段(分隔符切分后未 jieba 的长片段,区别于关键词)
1264
  for seg in response.segments:
1265
+ sc = ui.chip(seg,
1266
+ on_click=lambda s=seg: self._filter_by_source(s))
1267
+ sc.props('color=blue-1 text-color=blue-8 clickable')
1268
+ if seg in cached_set:
1269
+ sc.style('outline: 1px dashed rgba(128,128,128,0.3); outline-offset: 1px;')
1270
  for kw in response.keywords:
1271
+ kc = ui.chip(kw,
1272
+ on_click=lambda k=kw: self._filter_by_source(k))
1273
+ kc.props('color=grey-4 text-color=black clickable')
1274
+ if kw in cached_set:
1275
+ kc.style('outline: 1px dashed rgba(128,128,128,0.3); outline-offset: 1px;')
1276
  else:
1277
  ui.label('(分词已关闭)').classes('text-xs text-gray-400')
1278
 
 
1445
  self._render_related_list(merged, show_nsfw)
1446
 
1447
  def _refresh_related_from_selection(self, selected_tags: list[str], show_nsfw: bool):
1448
+ """仅刷新关联推荐列表(300ms 去抖,避免快速勾选产生 CPU 洪峰)。"""
1449
+ # 取消上次未执行的刷新
1450
+ if self._debounce_related_task and not self._debounce_related_task.done():
1451
+ self._debounce_related_task.cancel()
1452
  async def _do():
1453
+ await asyncio.sleep(0.3)
1454
  if not selected_tags:
1455
  self._refresh_related([], show_nsfw)
1456
  return
1457
  tagger = await DanbooruTagger.get_instance()
1458
+ related = await tagger.get_related_async(
 
1459
  selected_tags,
1460
  set(selected_tags),
1461
  50,
1462
  show_nsfw,
1463
  )
1464
  self._refresh_related(related, show_nsfw)
1465
+ self._debounce_related_task = asyncio.ensure_future(_do())
1466
 
1467
  def _refresh_group_from_selection(self, selected_tags: list[str], show_nsfw: bool):
1468
+ """仅刷新同类扩展区域(300ms 去抖,避免快速勾选产生 CPU 洪峰)。"""
1469
+ if self._debounce_group_task and not self._debounce_group_task.done():
1470
+ self._debounce_group_task.cancel()
1471
  async def _do():
1472
+ await asyncio.sleep(0.3)
1473
  if not selected_tags:
1474
  if self.group_expansion_container is not None:
1475
  self.group_expansion_container.clear()
 
1477
  ui.label('请先搜索并勾选标签…').classes('text-sm text-gray-400 italic p-4')
1478
  return
1479
  tagger = await DanbooruTagger.get_instance()
1480
+ group_data = await tagger.get_group_candidates_async(
 
1481
  selected_tags,
1482
  show_nsfw,
1483
  )
1484
  self._render_group_expansion(group_data, selected_tags, show_nsfw)
1485
+ self._debounce_group_task = asyncio.ensure_future(_do())
1486
 
1487
  def _render_group_expansion(self, group_data: list, selected_tags: list[str], show_nsfw: bool):
1488
  """渲染 Group 同类扩展区域。"""
 
1780
  async def head_root():
1781
  return PlainTextResponse('')
1782
 
1783
+
1784
  ui.run(
1785
  host=host,
1786
  port=port,
 
1789
  show=not is_cloud(),
1790
  reconnect_timeout=120,
1791
  )
1792
+