SAkizuki commited on
Commit
e2abd1d
·
verified ·
1 Parent(s): 8938d3d

Auto-sync from GitHub Actions

Browse files
Files changed (5) hide show
  1. config/ui_text.yaml +115 -0
  2. core/engine.py +3 -3
  3. core/ui_text.py +54 -0
  4. requirements.txt +2 -1
  5. ui_nicegui.py +172 -200
config/ui_text.yaml ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ sponsor:
2
+ body: >-
3
+ 如果你喜欢这个小工具,可以自愿赞赏一点维护成本。它会继续免费开放;赞赏不会解锁任何额外功能、不会提高额度、不会影响搜索结果,量力而行就好,未成年人请勿赞赏。
4
+ toolchain_prompt: >-
5
+ 如果你想继续折腾,也可以看看基于这个小工具的更多工具链
6
+
7
+ help:
8
+ update_title: 最近更新:标签工作区与查询理解
9
+ update_summary: >-
10
+ 本次更新优化了标签已选区,现已支持历史记录、收藏等功能。这些信息始终保存在你的计算机中,不会被服务器收集。
11
+ guide_markdown: |-
12
+ ### 1. 搜索与查询理解
13
+
14
+ 搜索后的“查询理解”优化了原先的分词筛选功能,增加了是否覆盖的标记:
15
+
16
+ - **绿色**:这个概念已有标签被加入工作区。
17
+ - **黄色**:搜索结果中存在候选,但你还没有选择。
18
+ - **红色**:当前结果没有覆盖;点击它会沿用当前参数发起补充搜索,不会清空工作区。
19
+ - 点击绿色或黄色概念,可以只查看来自该概念的搜索结果;点击“全部结果”恢复完整列表。
20
+
21
+ ### 2. 标签工作区
22
+
23
+ 勾选搜索结果、关联标签、同类标签或推荐画师后,它们都会进入同一个工作区,并自动保存在当前浏览器中。
24
+
25
+ - 使用标签两侧的 `− / +` 调整 Prompt 权重。
26
+ - 使用“撤销 / 恢复”回退最近的工作区修改。
27
+ - “清空已选”只清空当前工作区标签,不会删除历史和收藏。
28
+ - SDXL、NAI、Anima 复制格式会随工作区保存;画师标签在 Anima 格式下会自动使用 `@画师名`。
29
+
30
+ ### 3. 历史与继续搜索
31
+
32
+ 每次搜索都会记录查询、当时的搜索设置和工作区快照:
33
+
34
+ - **重新搜索**:恢复历史查询和当时的搜索参数,然后立即搜索。
35
+ - **恢复工作区**:恢复当时的已选标签、权重和格式。
36
+ - **追加查询**:使用当前页面的搜索参数重新执行历史查询,并保留当前工作区。
37
+
38
+ ### 4. 收藏
39
+
40
+ “保存收藏”会保存当前标签组合、权重、来源和 Prompt 格式。收藏可以恢复、合并或直接复制;合并时,同名标签保留当前工作区已有权重。
41
+
42
+ ### 5. Prompt 导入与 Alias 纠错
43
+
44
+ 点击“导入 Prompt”后粘贴标签字符串。全角逗号和半角逗号会等价解析;上传或粘贴完成后,需要再次点击确认才会导入。
45
+
46
+ - Alias 是 Danbooru 中标记弃用标签的数据接口,导入 Prompt 后,系统将自动替换弃用标签,并展示替换关系。
47
+ - 重复标签会合并,不会重复加入。
48
+ - 无法唯一识别或目标不在当前标签库中的内容进入“待确认”,不会被强行加入。
49
+
50
+ ### 6. 关联、同类与画师推荐
51
+
52
+ - **关联推荐**根据标签共现关系补充经常一起出现的标签。
53
+ - **同类标签**根据 Tag Group 展示同一语义组中的其他标签。
54
+ - **推荐擅长画师**根据标签与画师的共现数据推荐画师,每页显示 8 位;悬停可查看常见标签。
55
+
56
+ 推荐原因只展示系统实际使用的数据来源,不会虚构标签关系。
57
+
58
+ ### 7. 备份与迁移
59
+
60
+ “备份 / 迁移”可以导出完整 JSON,内容包括当前配置、工作区、原始搜索查询、历史和收藏。文件仅在你主动下载或上传时离开浏览器。导入时可以选择合并或替换。
61
+
62
+ ### 8. 反馈与隐私
63
+
64
+ 你可以提交整次搜索问题或标签翻译错误。反馈只记录当前查询、搜索设置、应用版本和你主动填写的说明;不会上传浏览器指纹、收藏、完整历史或未经提交的工作区等任何个人信息。
65
+
66
+ documentation:
67
+ title: 文档与集成
68
+ subtitle: 查看使用指南、接口文档、插件与公开服务接入说明。
69
+ links:
70
+ - label: 工具链使用指南
71
+ url: http://intro.sakizuki.site/index.html
72
+ - label: API 文档
73
+ url: /api/docs
74
+ - label: MCP 接入
75
+ url: https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline#mcp-接口
76
+ - label: ComfyUI 插件
77
+ url: https://github.com/SuzumiyaAkizuki/ComfyUI-DanbooruSearcher
78
+ - label: 备用服务
79
+ url: "{alternate_url}"
80
+ - label: GitHub
81
+ url: https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline
82
+ copyright_markdown: |-
83
+ **接口使用与版权声明**
84
+
85
+ 任何用户都可以免费将本服务的 API 或 MCP 接口接入自己的私有或公开服务中。
86
+
87
+ 如果接入公开服务,务必在主页提供链接到本服务主地址的友情链接:
88
+ ```
89
+ URL: <https://huggingface.co/spaces/SAkizuki/DanbooruSearch>
90
+ 简介: 基于语义匹配的 Danbooru 标签搜索引擎,支持多维匹配与共现关联推荐。
91
+ 作者:[凉宫秋月](https://github.com/SuzumiyaAkizuki)
92
+ ```
93
+
94
+ notice:
95
+ title: 注意事项
96
+ subtitle: 使用搜索结果和对外接入能力前,请留意以下说明。
97
+ body_markdown: |-
98
+ **AI 辅助**:基于语义匹配,结果未必绝对准确 (Results may contain errors)
99
+
100
+ **内容警告**:查找结果可能包含 NSFW 内容 (May include NSFW content)
101
+
102
+ **检索限制**:仅支持中/英双语查找,更推荐中文 (CN/EN only, CN is preferred)
103
+
104
+ **标签范围**:仅显示特征、角色与作品标签,且频数须 ≥ 100 (General, Character & Copyright only, Freq ≥ 100)
105
+
106
+
107
+ dialogs:
108
+ prompt_import_description: >-
109
+ 支持半角/全角逗号或换行分隔,以及 (tag:1.2)、1.2::tag::、{tag}、[tag] 和 @artist。无法确认的内容会保留在待确认区,不会进入最终 Prompt。
110
+ backup_description: >-
111
+ 导出文件包含当前配置、工作区、原始搜索查询、历史和收藏。文件只会在你主动下载或上传时离开浏览器。
112
+ search_feedback_privacy: >-
113
+ 提交后会记录当前查询、搜索设置、应用版本和提交时间;不会上传账号信息、浏览器指纹、搜索历史或收藏等任何个人信息。
114
+ translation_feedback_privacy: >-
115
+ 提交后会记录当前查询、该词条、当前翻译、搜索设置、应用版本和提交时间;不会上传账号信息、浏览器指纹、搜索历史或收藏等任何个人信息。
core/engine.py CHANGED
@@ -542,9 +542,7 @@ class DanbooruTagger:
542
  top_i_list = top_i.tolist()
543
 
544
  for i, source_word in enumerate(queries):
545
- cur_weights = query_weights[i]
546
  kq = cur_pvk_per_q[i].get(ln, 1)
547
- layer_w = cur_weights.get(ln, 1.0)
548
  row_v = top_v_list[i]
549
  row_i = top_i_list[i]
550
  # 仅取该 query 自己的配额条数
@@ -560,7 +558,9 @@ class DanbooruTagger:
560
  tag_name = self._arr_name[idx]
561
  count = self._arr_post_count[idx]
562
  pop_score = self._arr_pop_score[idx]
563
- final_score = score * layer_w * (1 - w_pop) + pop_score * w_pop
 
 
564
  if tag_name not in final or final_score > final[tag_name].final_score:
565
  final[tag_name] = TagResult(
566
  tag=tag_name, cn_name=self._arr_cn_name[idx], category=cat_text,
 
542
  top_i_list = top_i.tolist()
543
 
544
  for i, source_word in enumerate(queries):
 
545
  kq = cur_pvk_per_q[i].get(ln, 1)
 
546
  row_v = top_v_list[i]
547
  row_i = top_i_list[i]
548
  # 仅取该 query 自己的配额条数
 
558
  tag_name = self._arr_name[idx]
559
  count = self._arr_post_count[idx]
560
  pop_score = self._arr_pop_score[idx]
561
+ # 意图层权重仅用于上方的分层召回配额;最终排序统一比较原始语义分,
562
+ # 避免核心词/扩展词的视图权重覆盖正确的跨层语义顺序。
563
+ final_score = score * (1 - w_pop) + pop_score * w_pop
564
  if tag_name not in final or final_score > final[tag_name].final_score:
565
  final[tag_name] = TagResult(
566
  tag=tag_name, cn_name=self._arr_cn_name[idx], category=cat_text,
core/ui_text.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from typing import Any
3
+
4
+ import yaml
5
+
6
+
7
+ DEFAULT_UI_TEXT_PATH = Path(__file__).resolve().parents[1] / "config" / "ui_text.yaml"
8
+ REQUIRED_TEXT_PATHS = (
9
+ ("sponsor", "body"),
10
+ ("sponsor", "toolchain_prompt"),
11
+ ("help", "update_title"),
12
+ ("help", "update_summary"),
13
+ ("help", "guide_markdown"),
14
+ ("documentation", "title"),
15
+ ("documentation", "subtitle"),
16
+ ("documentation", "copyright_markdown"),
17
+ ("notice", "title"),
18
+ ("notice", "subtitle"),
19
+ ("notice", "body_markdown"),
20
+ ("dialogs", "prompt_import_description"),
21
+ ("dialogs", "backup_description"),
22
+ ("dialogs", "search_feedback_privacy"),
23
+ ("dialogs", "translation_feedback_privacy"),
24
+ )
25
+
26
+
27
+ def load_ui_text(path: Path = DEFAULT_UI_TEXT_PATH) -> dict[str, Any]:
28
+ """Load editable UI copy and fail with a clear startup error if it is invalid."""
29
+ try:
30
+ with path.open("r", encoding="utf-8") as file:
31
+ content = yaml.safe_load(file)
32
+ except (OSError, yaml.YAMLError) as exc:
33
+ raise RuntimeError(f"无法读取 UI 文案配置 {path}: {exc}") from exc
34
+
35
+ if not isinstance(content, dict):
36
+ raise RuntimeError(f"UI 文案配置顶层必须是对象: {path}")
37
+
38
+ for section, key in REQUIRED_TEXT_PATHS:
39
+ section_content = content.get(section)
40
+ value = section_content.get(key) if isinstance(section_content, dict) else None
41
+ if not isinstance(value, str) or not value.strip():
42
+ raise RuntimeError(f"UI 文案配置缺少非空文本: {section}.{key} ({path})")
43
+
44
+ links = content.get("documentation", {}).get("links")
45
+ if not isinstance(links, list) or not links:
46
+ raise RuntimeError(f"UI 文案配置缺少链接列表: documentation.links ({path})")
47
+ for index, link in enumerate(links):
48
+ if not isinstance(link, dict):
49
+ raise RuntimeError(f"UI 文案配置链接格式错误: documentation.links[{index}] ({path})")
50
+ if not isinstance(link.get("label"), str) or not link["label"].strip():
51
+ raise RuntimeError(f"UI 文案配置链接缺少名称: documentation.links[{index}] ({path})")
52
+ if not isinstance(link.get("url"), str) or not link["url"].strip():
53
+ raise RuntimeError(f"UI 文案配置链接缺少地址: documentation.links[{index}] ({path})")
54
+ return content
requirements.txt CHANGED
@@ -10,4 +10,5 @@ fastapi
10
  uvicorn
11
  modelscope
12
  oss2
13
- mcp==1.27.0
 
 
10
  uvicorn
11
  modelscope
12
  oss2
13
+ mcp==1.27.0
14
+ PyYAML>=6.0,<7
ui_nicegui.py CHANGED
@@ -35,6 +35,7 @@ from core import counter, telemetry
35
  from api_fastapi import app as api_app
36
  from core.engine import DanbooruTagger
37
  from core.models import RelatedTag, SearchRequest
 
38
  from core.prompt_import import (
39
  WORKSPACE_GROUP_ORDER,
40
  PromptImportResult,
@@ -174,11 +175,7 @@ SPONSOR_IMAGE_URL = "https://akizukipic.oss-cn-beijing.aliyuncs.com/img/20250112
174
  SPONSOR_TOOLCHAIN_URL = "http://intro.sakizuki.site/index.html"
175
  SPONSOR_NOTICE_TEXT = "喜欢的话,可以请作者喝杯咖啡"
176
  SPONSOR_TITLE = "谢谢你愿意支持"
177
- SPONSOR_BODY = (
178
- "如果你喜欢这个小工具,可以自愿赞赏一点维护成本。"
179
- "它会继续免费开放;赞赏不会解锁任何额外功能、不会提高额度、不会影响搜索结果,"
180
- "量力而行就好,未成年人请勿赞赏。"
181
- )
182
 
183
 
184
  def _resolve_group_render_limit(default: int = 80) -> int:
@@ -264,7 +261,7 @@ def _sanitize_restored_config(cfg: dict) -> dict:
264
 
265
  for key in (
266
  'show_nsfw', 'use_segmentation', 'sw_semantic', 'sw_layer',
267
- 'sw_source', 'notice_expanded', 'mcp_notice_dismissed',
268
  ):
269
  if isinstance(cfg.get(key), bool):
270
  safe[key] = cfg[key]
@@ -494,8 +491,6 @@ class DanbooruSearchUI:
494
 
495
  self.bad_case_btn = None
496
 
497
- self.mcp_notice = None
498
- self.notice_expansion = None
499
  self.announcement_banner = None
500
  self.dismissed_announcement_version = ''
501
  self.help_dialog = None
@@ -638,11 +633,11 @@ class DanbooruSearchUI:
638
  with ui.dialog() as self.sponsor_dialog, ui.card().classes('w-full max-w-sm'):
639
  with ui.column().classes('w-full items-center gap-2 text-center'):
640
  ui.label(SPONSOR_TITLE).classes('text-base font-bold text-gray-800')
641
- ui.label(SPONSOR_BODY).classes('text-sm text-gray-600 leading-relaxed')
642
  ui.image(SPONSOR_IMAGE_URL).classes('w-60 max-w-full rounded border border-gray-200')
643
  ui.label('微信赞赏码').classes('text-xs text-gray-400')
644
  ui.link(
645
- '如果你想继续折腾,也可以看看基于这个小工具的更多工具链',
646
  SPONSOR_TOOLCHAIN_URL,
647
  new_tab=True,
648
  ).classes('text-xs text-blue-500 hover:text-blue-700 hover:underline')
@@ -665,99 +660,51 @@ class DanbooruSearchUI:
665
  ui.button(icon='close', on_click=self.help_dialog.close).props('flat dense round color=grey-7')
666
 
667
  with ui.scroll_area().classes('w-full h-[72vh]'):
668
- with ui.column().classes('w-full gap-4 px-5 py-4'):
669
- with ui.element('div').classes(
670
- 'w-full rounded-lg border border-blue-200 bg-blue-50 px-4 py-3'
671
- ):
672
- ui.label('最近更新:标签工作区与查询理解').classes(
673
- 'text-base font-bold text-blue-900'
674
- )
675
- ui.label(
676
- '本次更新优化了标签已选区,现已支持历史记录、收藏等功能。这些信息始终保存在本地。'
677
- ).classes('text-sm text-blue-800 mt-1')
678
-
679
- ui.markdown("""
680
- ### 1. 搜索与查询理解
681
-
682
- 搜索后的“查询理解”优化了原先的分词筛选功能,增加了是否覆盖的标记:
683
-
684
- - **绿色**:这个概念已有标签被加入工作区。
685
- - **黄色**:搜索结果中存在候选,但你还没有选择。
686
- - **红色**:当前结果没有覆盖;点击它会沿用当前参数发起补充搜索,不会清空工作区。
687
- - 点击绿色或黄色概念,可以只查看来自该概念的搜索结果;点击“全部结果”恢复完整列表。
688
-
689
- ### 2. 标签工作区
690
-
691
- 勾选搜索结果、关联标签、同类标签或推荐画师后,它们都会进入同一个工作区,并自动保存在当前浏览器中。
692
-
693
- - 使用标签两侧的 `− / +` 调整 Prompt 权重。
694
- - 使用“撤销 / 恢复”回退最近的工作区修改。
695
- - “清空已选”只清空当前工作区标签,不会删除历史和收藏。
696
- - SDXL、NAI、Anima 复制格式会随工作区保存;画师标签在 Anima 格式下会自动使用 `@画师名`。
697
-
698
- ### 3. 历史与继续搜索
699
-
700
- 每次搜索都会记录查询、当时的搜索设置和工作区快照:
701
-
702
- - **重新搜索**:恢复历史查询和当时的搜索参数,然后立即搜索。
703
- - **恢复工作区**:恢复当时的已选标签、权重和格式。
704
- - **追加查询**:使用当前页面的搜索参数重新执行历史查询,并保留当前工作区。
705
-
706
- ### 4. 收藏
707
-
708
- “保存收藏”会保存当前标签组合、权重、来源和 Prompt 格式。收藏可以恢复、合并或直接复制;合并时,同名标签保留当前工作区已有权重。
709
-
710
- ### 5. Prompt 导入与 Alias 纠错
711
-
712
- 点击“导入 Prompt”后粘贴标签字符串。全角逗号和半角逗号会等价解析;上传或粘贴完成后,需要再次点击确认才会导入。
713
-
714
- - Alias 是 Danbooru 中标记弃用标签的数据接口,导入 Prompt 后,系统将自动替换弃用标签,并展示替换关系。
715
- - 重复标签会合并,不会重复加入。
716
- - 无法唯一识别或目标不在当前标签库中的内容进入“待确认”,不会被强行加入。
717
-
718
- ### 6. 关联、同类与画师推荐
719
-
720
- - **关联推荐**根据标签共现关系补充经常一起出现的标签。
721
- - **同类标签**根据 Tag Group 展示同一语义组中的其他标签。
722
- - **推荐擅长画师**根据标签与画师的共现数据推荐画师,每页显示 8 位;悬停可查看常见标签。
723
-
724
- 推荐原因只展示系统实际使用的数据来源,不会虚构标签关系。
725
-
726
- ### 7. 备份与迁移
727
-
728
- “备份 / 迁移”可以导出完整 JSON,内容包括当前配置、工作区、原始搜索查询、历史和收藏。文件仅在你主动下载或上传时离开浏览器。导入时可以选择合并或替换。
729
-
730
- ### 8. 反馈与隐私
731
-
732
- 你可以提交整次搜索问题或标签翻译错误。反馈只记录当前查询、搜索设置、应用版本和你主动填写的说明;不会上传浏览器指纹、收藏、完整历史或未经提交的工作区等任何个人信息。
733
- """).classes('w-full text-sm text-slate-700 leading-relaxed')
734
-
735
- ui.separator()
736
-
737
- with ui.column().classes('w-full gap-2'):
738
- ui.label('文档与集成').classes('text-sm font-bold text-slate-700')
739
- with ui.row().classes('w-full gap-3 flex-wrap'):
740
- ui.link('工具链使用指南', 'http://intro.sakizuki.site/index.html', new_tab=True) \
741
- .classes('text-sm text-blue-600')
742
- ui.link('API 文档', '/api/docs', new_tab=True).classes('text-sm text-blue-600')
743
- ui.link(
744
- 'MCP 接入',
745
- 'https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline#mcp-接口',
746
- new_tab=True,
747
- ).classes('text-sm text-blue-600')
748
- ui.link(
749
- 'ComfyUI 插件',
750
- 'https://github.com/SuzumiyaAkizuki/ComfyUI-DanbooruSearcher',
751
- new_tab=True,
752
- ).classes('text-sm text-blue-600')
753
- ui.link('备用服务', alternate_url, new_tab=True).classes('text-sm text-blue-600')
754
- ui.link(
755
- 'GitHub',
756
- 'https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline',
757
- new_tab=True,
758
- ).classes('text-sm text-blue-600')
759
-
760
- ui.separator()
761
 
762
  with ui.element('div').classes(
763
  'w-full rounded-lg border border-red-200 bg-red-50 px-4 py-3'
@@ -1128,6 +1075,91 @@ class DanbooruSearchUI:
1128
  border-left: 3px solid #4a90e2;
1129
  border-radius: 8px;
1130
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1131
 
1132
  /* 强制双栏并排 */
1133
  .two-col-layout {
@@ -1229,76 +1261,6 @@ class DanbooruSearchUI:
1229
  .props('flat dense no-caps color=grey-6') \
1230
  .classes('text-xs mt-1')
1231
 
1232
- # ── 公告栏(标签组 + MCP)─────────────────────────────────────────────
1233
-
1234
- def _build_group_notice(self):
1235
- self.mcp_notice = ui.card().classes(
1236
- 'w-full bg-green-50 border-l-4 border-green-500 p-0 overflow-hidden'
1237
- )
1238
- with self.mcp_notice:
1239
- with ui.column().classes('px-4 py-3 w-full gap-2'):
1240
- # ── 画师查找公告 ──
1241
- with ui.row().classes('items-center justify-between w-full'):
1242
- with ui.row().classes('items-center gap-1'):
1243
- ui.label('🧪 新功能:推荐擅长画师(beta)').classes('text-sm font-bold text-green-800')
1244
- ui.button(icon='close').props('flat dense round color=grey-6') \
1245
- .on_click(self._dismiss_mcp_notice)
1246
- ui.html(
1247
- '基于标签共现数据,根据已选标签查找对应的擅长画师。'
1248
- '鼠标悬停画师行可查看该画师最常画的标签'
1249
- ).classes('text-xs text-green-900')
1250
- ui.separator().classes('my-1')
1251
- # ── 标签组扩展 ──
1252
- ui.html(
1253
- '【标签组扩展】 勾选标签后,搜索结果下方会出现<b>同类标签</b>区域,'
1254
- '展示已选标签所属分组中的其他标签,勾选即可加入已选。'
1255
- ).classes('text-xs text-green-900')
1256
- ui.separator().classes('my-1')
1257
- # ── MCP 服务 ──
1258
- ui.html(
1259
- '【MCP 服务】 支持通过 MCP 协议接入 AI Agent(如 Claude Desktop)。'
1260
- '免配置托管版体验:'
1261
- '<a href="https://huggingface.co/spaces/SAkizuki/WenQiuYue" '
1262
- 'target="_blank" rel="noopener noreferrer" '
1263
- 'class="text-green-700 font-bold underline">问秋月 Space</a>,'
1264
- '<span class="text-gray-500 ml-1">API 额度有限,仅供体验。</span>'
1265
- '&nbsp;'
1266
- '<a href="https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline#mcp-接口" '
1267
- 'target="_blank" rel="noopener noreferrer" '
1268
- 'class="text-green-700 underline">接入文档 →</a>'
1269
- ).classes('text-xs text-green-900')
1270
-
1271
- def _dismiss_mcp_notice(self):
1272
- if self.mcp_notice:
1273
- self.mcp_notice.set_visibility(False)
1274
- self._save_config()
1275
-
1276
- # ── 注意事项 ──────────────────────────────────────────────────────────
1277
-
1278
- def _build_notice(self):
1279
- with ui.card().classes('w-full bg-orange-50 border-l-4 border-orange-500 p-0 overflow-hidden'):
1280
- with ui.expansion(value=True).classes('w-full') as notice_expansion:
1281
- self.notice_expansion = notice_expansion
1282
- notice_expansion.on('update:model-value', lambda _: self._save_config())
1283
- notice_expansion.add_slot('header', '''
1284
- <div class="flex items-center gap-2 px-4 py-2 w-full flex-wrap">
1285
- <span class="text-base font-bold text-orange-800">⚠️ 注意事项 / Note</span>
1286
- <span v-if="!props.expanded" class="text-sm text-gray-600 ml-1">
1287
- 如果觉得好用,请点击顶部给本 Space 点个
1288
- <strong>Like ❤️</strong>,或前往 GitHub 点个 <strong>Star ⭐</strong>!
1289
- </span>
1290
- </div>
1291
- ''')
1292
- ui.markdown("""
1293
- - **AI 辅助**:基于语义匹配,结果未必绝对准确(Results may contain errors)
1294
- - **内容警告**:查找结果可能包含 NSFW 内容 (May include NSFW content)
1295
- - **检索限制**:仅支持中/英双语查找 ,更推荐中文(CN/EN only,CN is preferred)
1296
- - **标签范围**:仅显示特征、角色与作品标签,且频数须 ≥ 100 (General, Character & Copyright only, Freq ≥ 100)
1297
- - **集成与接口**:[ComfyUI 插件](https://github.com/SuzumiyaAkizuki/ComfyUI-DanbooruSearcher) · [API 文档](/api/docs) · [MCP 接入](https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline#mcp-接口)
1298
- - **支持作者**:如果觉得好用,欢迎点击顶部给本 Space 点个 **Like ❤️**,或前往 [GitHub](https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline) 点个 **Star ⭐**!也可以自愿赞赏一点维护成本;不影响任何功能使用。
1299
- - **🚀 首次使用?[点击查看使用指南](http://intro.sakizuki.site/index.html)**,了解五种搜索模式与进阶技巧
1300
- """).classes('text-sm text-gray-800 px-4 pb-3')
1301
-
1302
  # ── 搜索卡片 ─────────────────────────────────────────────────────────
1303
 
1304
  def _build_search_card(self):
@@ -1313,12 +1275,21 @@ class DanbooruSearchUI:
1313
  ui.label(
1314
  '基于语义匹配的标签搜索引擎,支持多维匹配与共现关联推荐。'
1315
  ).classes('text-sm text-gray-500 -mt-1 mb-1')
1316
- ui.html(
1317
- '<a href="http://intro.sakizuki.site/index.html" '
1318
- 'target="_blank" rel="noopener noreferrer" '
1319
- 'class="text-blue-600 hover:text-blue-800 underline font-medium">'
1320
- '查看工具链介绍 / 使用指南 →</a>'
1321
- ).classes('text-sm mb-3')
 
 
 
 
 
 
 
 
 
1322
 
1323
  with ui.row().classes('w-full gap-3 items-stretch'):
1324
  self.search_input = ui.textarea(
@@ -1662,10 +1633,9 @@ class DanbooruSearchUI:
1662
  def _open_prompt_import_dialog(self):
1663
  with ui.dialog() as dialog, ui.card().classes('w-full max-w-3xl'):
1664
  ui.label('导入 Prompt').classes('text-lg font-bold')
1665
- ui.label(
1666
- '支持半角/全角逗号或换行分隔,以及 (tag:1.2)、1.2::tag::、{tag}、[tag] 和 @artist。'
1667
- '无法确认的内容会保留在待确认区,不会进入最终 Prompt。'
1668
- ).classes('text-sm text-gray-600')
1669
  prompt_input = ui.textarea(
1670
  label='粘贴 Prompt',
1671
  placeholder='1girl, (white_serafuku:1.2), {rain}, @artist_name',
@@ -2180,10 +2150,9 @@ class DanbooruSearchUI:
2180
  def _open_backup_dialog(self):
2181
  with ui.dialog() as dialog, ui.card().classes('w-full max-w-2xl'):
2182
  ui.label('本地数据备份与迁移').classes('text-lg font-bold')
2183
- ui.label(
2184
- '导出文件包含当前配置、工作区、原始搜索查询、历史和收藏。'
2185
- '文件只会在你主动下载或上传时离开浏览器。'
2186
- ).classes('text-sm text-orange-700 bg-orange-50 rounded p-3')
2187
  ui.button(
2188
  '导出完整 JSON', icon='download', on_click=self._export_backup,
2189
  ).props('unelevated color=primary').classes('w-full')
@@ -3160,11 +3129,16 @@ class DanbooruSearchUI:
3160
  )
3161
  if self._client_connected() and not self._storage_failure_notified:
3162
  self._storage_failure_notified = True
3163
- ui.notify(
3164
- '本地工作区数据暂未恢复,已停止覆盖旧数据;连接恢复后会自动重试。',
3165
- type='warning',
3166
- timeout=5000,
3167
- )
 
 
 
 
 
3168
  except asyncio.CancelledError:
3169
  was_cancelled = True
3170
  finally:
@@ -4669,10 +4643,9 @@ class DanbooruSearchUI:
4669
  with ui.dialog() as dialog, ui.card().classes('w-full max-w-lg'):
4670
  ui.label('反馈搜索问题').classes('text-base font-bold text-gray-800')
4671
  ui.label(f'当前搜索词:{query}').classes('text-sm text-gray-600')
4672
- ui.label(
4673
- '提交后会记录当前查询、搜索设置、应用版本和提交时间;'
4674
- '不会上传账号信息、浏览器指纹、搜索历史或收藏。'
4675
- ).classes('text-xs text-slate-500 bg-slate-50 rounded p-2')
4676
  detail_input = ui.textarea(
4677
  label='具体问题(可选)',
4678
  placeholder='例如:结果偏题、缺少某个关键标签、召回了不相关角色/作品...',
@@ -4729,10 +4702,9 @@ class DanbooruSearchUI:
4729
  ui.label('反馈翻译错误').classes('text-base font-bold text-gray-800')
4730
  ui.label(f'词条:{tag}').classes('text-sm font-mono text-gray-700')
4731
  ui.label(f'当前中文名:{current_cn_first or current_cn_name or "(空)"}').classes('text-sm text-gray-600')
4732
- ui.label(
4733
- '提交后会记录当前查询、该���条、当前翻译、搜索设置、应用版本和提交时间;'
4734
- '不会上传账号信息、浏览器指纹、搜索历史或收藏。'
4735
- ).classes('text-xs text-slate-500 bg-slate-50 rounded p-2')
4736
  suggested_input = ui.input(
4737
  label='建议中文名(可选)',
4738
  placeholder='如果有更合适的译名,可以填在这里',
 
35
  from api_fastapi import app as api_app
36
  from core.engine import DanbooruTagger
37
  from core.models import RelatedTag, SearchRequest
38
+ from core.ui_text import load_ui_text
39
  from core.prompt_import import (
40
  WORKSPACE_GROUP_ORDER,
41
  PromptImportResult,
 
175
  SPONSOR_TOOLCHAIN_URL = "http://intro.sakizuki.site/index.html"
176
  SPONSOR_NOTICE_TEXT = "喜欢的话,可以请作者喝杯咖啡"
177
  SPONSOR_TITLE = "谢谢你愿意支持"
178
+ UI_TEXT = load_ui_text()
 
 
 
 
179
 
180
 
181
  def _resolve_group_render_limit(default: int = 80) -> int:
 
261
 
262
  for key in (
263
  'show_nsfw', 'use_segmentation', 'sw_semantic', 'sw_layer',
264
+ 'sw_source',
265
  ):
266
  if isinstance(cfg.get(key), bool):
267
  safe[key] = cfg[key]
 
491
 
492
  self.bad_case_btn = None
493
 
 
 
494
  self.announcement_banner = None
495
  self.dismissed_announcement_version = ''
496
  self.help_dialog = None
 
633
  with ui.dialog() as self.sponsor_dialog, ui.card().classes('w-full max-w-sm'):
634
  with ui.column().classes('w-full items-center gap-2 text-center'):
635
  ui.label(SPONSOR_TITLE).classes('text-base font-bold text-gray-800')
636
+ ui.label(UI_TEXT['sponsor']['body']).classes('text-sm text-gray-600 leading-relaxed')
637
  ui.image(SPONSOR_IMAGE_URL).classes('w-60 max-w-full rounded border border-gray-200')
638
  ui.label('微信赞赏码').classes('text-xs text-gray-400')
639
  ui.link(
640
+ UI_TEXT['sponsor']['toolchain_prompt'],
641
  SPONSOR_TOOLCHAIN_URL,
642
  new_tab=True,
643
  ).classes('text-xs text-blue-500 hover:text-blue-700 hover:underline')
 
660
  ui.button(icon='close', on_click=self.help_dialog.close).props('flat dense round color=grey-7')
661
 
662
  with ui.scroll_area().classes('w-full h-[72vh]'):
663
+ with ui.column().classes('w-full gap-5 px-5 py-4'):
664
+ with ui.column().classes('help-section'):
665
+ with ui.element('div').classes('help-section-heading'):
666
+ ui.label(UI_TEXT['help']['update_title']).classes(
667
+ 'help-section-heading-title'
668
+ )
669
+ ui.label(UI_TEXT['help']['update_summary']).classes(
670
+ 'help-section-heading-subtitle'
671
+ )
672
+ ui.markdown(UI_TEXT['help']['guide_markdown']).classes('help-content')
673
+
674
+ with ui.column().classes('help-section'):
675
+ with ui.element('div').classes(
676
+ 'help-section-heading help-section-heading--documentation'
677
+ ):
678
+ ui.label(UI_TEXT['documentation']['title']).classes(
679
+ 'help-section-heading-title'
680
+ )
681
+ ui.label(UI_TEXT['documentation']['subtitle']).classes(
682
+ 'help-section-heading-subtitle'
683
+ )
684
+ with ui.column().classes('help-content gap-3'):
685
+ with ui.row().classes('w-full gap-3 flex-wrap'):
686
+ for link in UI_TEXT['documentation']['links']:
687
+ link_url = (
688
+ alternate_url
689
+ if link['url'] == '{alternate_url}'
690
+ else link['url']
691
+ )
692
+ ui.link(link['label'], link_url, new_tab=True).classes('help-link')
693
+ ui.markdown(
694
+ UI_TEXT['documentation']['copyright_markdown']
695
+ ).classes('help-content')
696
+
697
+ with ui.column().classes('help-section'):
698
+ with ui.element('div').classes(
699
+ 'help-section-heading help-section-heading--notice'
700
+ ):
701
+ ui.label(UI_TEXT['notice']['title']).classes(
702
+ 'help-section-heading-title'
703
+ )
704
+ ui.label(UI_TEXT['notice']['subtitle']).classes(
705
+ 'help-section-heading-subtitle'
706
+ )
707
+ ui.markdown(UI_TEXT['notice']['body_markdown']).classes('help-content')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708
 
709
  with ui.element('div').classes(
710
  'w-full rounded-lg border border-red-200 bg-red-50 px-4 py-3'
 
1075
  border-left: 3px solid #4a90e2;
1076
  border-radius: 8px;
1077
  }
1078
+ .homepage-support-note {
1079
+ color: #64748b;
1080
+ font-size: 12px;
1081
+ line-height: 1.6;
1082
+ }
1083
+ .homepage-support-note a {
1084
+ color: #4a90e2;
1085
+ font-weight: 500;
1086
+ text-decoration: none;
1087
+ }
1088
+ .homepage-support-note a:hover {
1089
+ color: #2563a8;
1090
+ text-decoration: underline;
1091
+ }
1092
+ .help-section {
1093
+ width: 100%;
1094
+ gap: 12px;
1095
+ background: #ffffff;
1096
+ }
1097
+ .help-section-heading {
1098
+ display: flex;
1099
+ flex-direction: column;
1100
+ width: 100%;
1101
+ gap: 5px;
1102
+ padding: 12px 16px;
1103
+ border: 1px solid #b9d7f7;
1104
+ border-radius: 8px;
1105
+ background: #eef6ff;
1106
+ }
1107
+ .help-section-heading-title {
1108
+ color: #174f91;
1109
+ font-size: 15px;
1110
+ font-weight: 700;
1111
+ line-height: 1.4;
1112
+ }
1113
+ .help-section-heading-subtitle {
1114
+ color: #2563a8;
1115
+ font-size: 13px;
1116
+ font-weight: 400;
1117
+ line-height: 1.55;
1118
+ }
1119
+ .help-section-heading--documentation {
1120
+ border-color: #a7e3c4;
1121
+ background: #ecfdf5;
1122
+ }
1123
+ .help-section-heading--documentation .help-section-heading-title {
1124
+ color: #047857;
1125
+ }
1126
+ .help-section-heading--documentation .help-section-heading-subtitle {
1127
+ color: #16855f;
1128
+ }
1129
+ .help-section-heading--notice {
1130
+ border-color: #fed7aa;
1131
+ background: #fff7ed;
1132
+ }
1133
+ .help-section-heading--notice .help-section-heading-title {
1134
+ color: #c2410c;
1135
+ }
1136
+ .help-section-heading--notice .help-section-heading-subtitle {
1137
+ color: #b45309;
1138
+ }
1139
+ .help-content {
1140
+ width: 100%;
1141
+ color: #334155;
1142
+ font-size: 14px;
1143
+ line-height: 1.75;
1144
+ }
1145
+ .help-section > .help-content {
1146
+ box-sizing: border-box;
1147
+ padding-right: 16px;
1148
+ padding-left: 16px;
1149
+ }
1150
+ .help-content h3,
1151
+ .help-content h4 {
1152
+ color: #334155;
1153
+ }
1154
+ .help-link {
1155
+ color: #2563a8;
1156
+ font-size: 14px;
1157
+ line-height: 1.6;
1158
+ }
1159
+ .help-link:hover {
1160
+ color: #174f86;
1161
+ text-decoration: underline;
1162
+ }
1163
 
1164
  /* 强制双栏并排 */
1165
  .two-col-layout {
 
1261
  .props('flat dense no-caps color=grey-6') \
1262
  .classes('text-xs mt-1')
1263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1264
  # ── 搜索卡片 ─────────────────────────────────────────────────────────
1265
 
1266
  def _build_search_card(self):
 
1275
  ui.label(
1276
  '基于语义匹配的标签搜索引擎,支持多维匹配与共现关联推荐。'
1277
  ).classes('text-sm text-gray-500 -mt-1 mb-1')
1278
+ with ui.row().classes(
1279
+ 'w-full items-center justify-between gap-x-4 gap-y-1 flex-wrap mb-3'
1280
+ ):
1281
+ ui.link(
1282
+ '查看工具链介绍 / 使用指南 →',
1283
+ 'http://intro.sakizuki.site/index.html',
1284
+ new_tab=True,
1285
+ ).classes('text-sm text-blue-600 hover:text-blue-800 font-medium')
1286
+ ui.html(
1287
+ '觉得好用?给 '
1288
+ '<a href="https://huggingface.co/spaces/SAkizuki/DanbooruSearch">'
1289
+ 'Space 点个 Like ❤️</a>,或到 '
1290
+ '<a href="https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline">'
1291
+ 'GitHub 点 Star ⭐</a>'
1292
+ ).classes('homepage-support-note')
1293
 
1294
  with ui.row().classes('w-full gap-3 items-stretch'):
1295
  self.search_input = ui.textarea(
 
1633
  def _open_prompt_import_dialog(self):
1634
  with ui.dialog() as dialog, ui.card().classes('w-full max-w-3xl'):
1635
  ui.label('导入 Prompt').classes('text-lg font-bold')
1636
+ ui.label(UI_TEXT['dialogs']['prompt_import_description']).classes(
1637
+ 'text-sm text-gray-600'
1638
+ )
 
1639
  prompt_input = ui.textarea(
1640
  label='粘贴 Prompt',
1641
  placeholder='1girl, (white_serafuku:1.2), {rain}, @artist_name',
 
2150
  def _open_backup_dialog(self):
2151
  with ui.dialog() as dialog, ui.card().classes('w-full max-w-2xl'):
2152
  ui.label('本地数据备份与迁移').classes('text-lg font-bold')
2153
+ ui.label(UI_TEXT['dialogs']['backup_description']).classes(
2154
+ 'text-sm text-orange-700 bg-orange-50 rounded p-3'
2155
+ )
 
2156
  ui.button(
2157
  '导出完整 JSON', icon='download', on_click=self._export_backup,
2158
  ).props('unelevated color=primary').classes('w-full')
 
3129
  )
3130
  if self._client_connected() and not self._storage_failure_notified:
3131
  self._storage_failure_notified = True
3132
+ try:
3133
+ with self.client:
3134
+ ui.notify(
3135
+ '本地工作区数据暂未恢复,已停止覆盖旧数据;连接恢复后会自动重试。',
3136
+ type='warning',
3137
+ timeout=5000,
3138
+ )
3139
+ except RuntimeError:
3140
+ # The client may disconnect after the connection check.
3141
+ pass
3142
  except asyncio.CancelledError:
3143
  was_cancelled = True
3144
  finally:
 
4643
  with ui.dialog() as dialog, ui.card().classes('w-full max-w-lg'):
4644
  ui.label('反馈搜索问题').classes('text-base font-bold text-gray-800')
4645
  ui.label(f'当前搜索词:{query}').classes('text-sm text-gray-600')
4646
+ ui.label(UI_TEXT['dialogs']['search_feedback_privacy']).classes(
4647
+ 'text-xs text-slate-500 bg-slate-50 rounded p-2'
4648
+ )
 
4649
  detail_input = ui.textarea(
4650
  label='具体问题(可选)',
4651
  placeholder='例如:结果偏题、缺少某个关键标签、召回了不相关角色/作品...',
 
4702
  ui.label('反馈翻译错误').classes('text-base font-bold text-gray-800')
4703
  ui.label(f'词条:{tag}').classes('text-sm font-mono text-gray-700')
4704
  ui.label(f'当前中文名:{current_cn_first or current_cn_name or "(空)"}').classes('text-sm text-gray-600')
4705
+ ui.label(UI_TEXT['dialogs']['translation_feedback_privacy']).classes(
4706
+ 'text-xs text-slate-500 bg-slate-50 rounded p-2'
4707
+ )
 
4708
  suggested_input = ui.input(
4709
  label='建议中文名(可选)',
4710
  placeholder='如果有更合适的译名,可以填在这里',