Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Auto-sync from GitHub Actions
Browse files- config/ui_text.yaml +115 -0
- core/engine.py +3 -3
- core/ui_text.py +54 -0
- requirements.txt +2 -1
- 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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',
|
| 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(
|
| 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-
|
| 669 |
-
with ui.
|
| 670 |
-
'
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
).classes('
|
| 678 |
-
|
| 679 |
-
ui.
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 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 |
-
' '
|
| 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.
|
| 1317 |
-
'
|
| 1318 |
-
|
| 1319 |
-
|
| 1320 |
-
|
| 1321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
'
|
| 1667 |
-
|
| 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 |
-
|
| 3164 |
-
|
| 3165 |
-
|
| 3166 |
-
|
| 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='如果有更合适的译名,可以填在这里',
|