Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Commit ยท
6c9a052
0
Parent(s):
Super-squash branch 'main' using huggingface_hub
Browse filesCo-authored-by: GitHub Actions <GitHub Actions@users.noreply.huggingface.co>
- .gitattributes +34 -0
- .github/workflows/sync_to_hf.yml +77 -0
- .gitignore +18 -0
- Dockerfile +12 -0
- README.md +472 -0
- api_fastapi.py +160 -0
- cli.py +143 -0
- core/counter.py +392 -0
- core/engine.py +897 -0
- core/models.py +61 -0
- mcp_server.py +283 -0
- origin_database/tags_enhanced.csv +0 -0
- platform_utils.py +298 -0
- requirements.txt +13 -0
- tags_embedding/version_data.json +4 -0
- ui_nicegui.py +1381 -0
.gitattributes
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/sync_to_hf.yml
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Sync to HuggingFace and ModelScope
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
sync:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
steps:
|
| 12 |
+
- name: Checkout GitHub repo
|
| 13 |
+
uses: actions/checkout@v4
|
| 14 |
+
with:
|
| 15 |
+
lfs: false
|
| 16 |
+
|
| 17 |
+
- name: Install Dependencies
|
| 18 |
+
run: pip install huggingface_hub
|
| 19 |
+
|
| 20 |
+
- name: Sync to HuggingFace Space
|
| 21 |
+
env:
|
| 22 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 23 |
+
run: |
|
| 24 |
+
python - << 'PYEOF'
|
| 25 |
+
import os
|
| 26 |
+
from huggingface_hub import HfApi
|
| 27 |
+
|
| 28 |
+
api = HfApi(token=os.environ["HF_TOKEN"])
|
| 29 |
+
repo_id = "SAkizuki/DanbooruSearch"
|
| 30 |
+
|
| 31 |
+
api.upload_folder(
|
| 32 |
+
folder_path=".",
|
| 33 |
+
repo_id=repo_id,
|
| 34 |
+
repo_type="space",
|
| 35 |
+
ignore_patterns=[
|
| 36 |
+
"tags_embedding/*",
|
| 37 |
+
"origin_database/*",
|
| 38 |
+
".git/*",
|
| 39 |
+
"__pycache__/*",
|
| 40 |
+
"*.pyc",
|
| 41 |
+
"*.parquet",
|
| 42 |
+
"*.csv"
|
| 43 |
+
],
|
| 44 |
+
commit_message="Auto-sync from GitHub Actions"
|
| 45 |
+
)
|
| 46 |
+
print("HF sync done")
|
| 47 |
+
PYEOF
|
| 48 |
+
|
| 49 |
+
- name: Sync to ModelScope Studio
|
| 50 |
+
env:
|
| 51 |
+
MS_TOKEN: ${{ secrets.MS_TOKEN }}
|
| 52 |
+
run: |
|
| 53 |
+
git config --global user.name "github-actions[bot]"
|
| 54 |
+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
| 55 |
+
|
| 56 |
+
export GIT_LFS_SKIP_SMUDGE=1
|
| 57 |
+
git clone https://oauth2:${MS_TOKEN}@www.modelscope.cn/studios/SAkizuki/DanbooruSearchOnline.git ms_repo
|
| 58 |
+
|
| 59 |
+
# ๆท่ดๆ น็ฎๅฝไธ็ .py ๆไปถใไพ่ตๆไปถๅ README.md
|
| 60 |
+
cp *.py ms_repo/ 2>/dev/null || true
|
| 61 |
+
cp requirements.txt ms_repo/ 2>/dev/null || true
|
| 62 |
+
cp README.md ms_repo/ 2>/dev/null || true
|
| 63 |
+
|
| 64 |
+
# ๆท่ด core ็ฎๅฝไธ็ .py ๆไปถ
|
| 65 |
+
mkdir -p ms_repo/core
|
| 66 |
+
cp core/*.py ms_repo/core/ 2>/dev/null || true
|
| 67 |
+
|
| 68 |
+
cd ms_repo
|
| 69 |
+
git add .
|
| 70 |
+
|
| 71 |
+
if ! git diff --staged --quiet; then
|
| 72 |
+
git commit -m "Auto-sync from GitHub Actions"
|
| 73 |
+
git push origin master
|
| 74 |
+
echo "MS sync done"
|
| 75 |
+
else
|
| 76 |
+
echo "No code changes to sync to MS."
|
| 77 |
+
fi
|
.gitignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*cuda*
|
| 2 |
+
*.pyc
|
| 3 |
+
core/__pychahce__
|
| 4 |
+
__pychahce__
|
| 5 |
+
my_model*/
|
| 6 |
+
*.ignore
|
| 7 |
+
tags_embedding/
|
| 8 |
+
origin_database/
|
| 9 |
+
.idea/
|
| 10 |
+
*ignore*.py
|
| 11 |
+
*.sh
|
| 12 |
+
tempsave/
|
| 13 |
+
*.env
|
| 14 |
+
*.png
|
| 15 |
+
*.bat
|
| 16 |
+
eval/
|
| 17 |
+
legacy_history.json
|
| 18 |
+
CLAUDE.md
|
Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY requirements.txt .
|
| 4 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 5 |
+
RUN useradd -m -u 1000 user
|
| 6 |
+
USER user
|
| 7 |
+
ENV HOME=/home/user \
|
| 8 |
+
PATH=/home/user/.local/bin:$PATH
|
| 9 |
+
ENV PYTHONUNBUFFERED=1
|
| 10 |
+
COPY --chown=user . /app
|
| 11 |
+
EXPOSE 7860
|
| 12 |
+
CMD ["python", "ui_nicegui.py"]
|
README.md
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: DanbooruSearch
|
| 3 |
+
emoji: ๐
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
short_description: ๅบไบ่ฏญไนๅน้
็ Danbooru ๆ ็ญพๆ็ดขๅผๆ๏ผๆฏๆๅค็ปดๅน้
ใๆบ่ฝๅ่ฏไธๅ
ฑ็ฐๅ
ณ่ๆจ่ใ
|
| 11 |
+
tags:
|
| 12 |
+
- text-to-image
|
| 13 |
+
- prompt-engineering
|
| 14 |
+
- stable-diffusion
|
| 15 |
+
- danbooru
|
| 16 |
+
- nlp
|
| 17 |
+
thumbnail: >-
|
| 18 |
+
https://akizukipic.oss-cn-beijing.aliyuncs.com/img/202604022117025.png
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
# Danbooru Tags Searcher
|
| 22 |
+
|
| 23 |
+

|
| 24 |
+
|
| 25 |
+
ๆฌ้กน็ฎๆไพไธ็งๅบไบ่ฏญไนๅน้
็ Danbooru ๆ ็ญพๆฅๆพๆนๆกใ็จๆทๅฏไปฅ้่ฟ่พๅ
ฅๆฅๅธธ่ฏญ่จๆๆจก็ณๆ่ฟฐ๏ผๆฅๆพๅนถๅน้
Danbooru ๆฐๆฎ้ไธญๅฏนๅบ็ๆ ๅๆ ็ญพ๏ผ้็จไบ่พ
ๅฉๆๅปบ Stable DiffusionใNovelAI ็ญ AI ็ป็ปๅทฅๅ
ท็ๆ็คบ่ฏ๏ผPrompt๏ผใ
|
| 26 |
+
|
| 27 |
+
็ฎๅๆฏๆไฝฟ็จๆฑ่ฏญๅ่ฑ่ฏญ่ฟ่กๆฅๆพ
|
| 28 |
+
|
| 29 |
+
**็ซๅณไฝฟ็จ๏ผ** https://huggingface.co/spaces/SAkizuki/DanbooruSearch
|
| 30 |
+
|
| 31 |
+
**ComfyUI ๆไปถ็ๆฌ๏ผ** [ComfyUI-DanbooruSearcher](https://github.com/SuzumiyaAkizuki/ComfyUI-DanbooruSearcher)
|
| 32 |
+
|
| 33 |
+
ๆฌ้กน็ฎ็ๆฐๆฎๅบ็ฑDanbooru APIๆๅ่ๆ๏ผไฝฟ็จLLM่ฟ่ก่ฏญไนๆฉๅ
ๅไธญๆ็ฟป่ฏใ่ง่ฒๅใไฝๅๅไฝฟ็จไบๆฌกๅ
ๅ็ฑปๆฐๆฎๅบBangumi API่ฟ่ก็ฒพ็กฎๆฅ่ฏข๏ผๅฐฝๅฏ่ฝๅฐ้ฟๅ
ไบๅจ่ง่ฒๅใไฝๅๅไธ็ๅนป่ง้ฎ้ขใ
|
| 34 |
+
|
| 35 |
+
**ๆฐๆฎๅบ็ๆไปฃ็ ๏ผ** https://github.com/SuzumiyaAkizuki/danbooru-tag-pipeline
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## ๆ ธๅฟ็นๆง
|
| 40 |
+
|
| 41 |
+
ๆฌ็ๆฌๅจๅบ็ก่ฏญไนๆฃ็ดขไนไธ๏ผๅผๅ
ฅไบๅฎๆด็ๆ ็ญพๅทฅไฝๆต่พ
ๅฉๅ่ฝ๏ผ
|
| 42 |
+
|
| 43 |
+
* **ๅค็ปดๅบฆ่ฏญไนๅน้
๏ผ** ๅบๅฑๆฏๆ 4 ไธช็ปดๅบฆ็ๅ้็ฉบ้ดๆฃ็ดข๏ผ่ฑๆใไธญๆๆฉๅฑ่ฏใ็ปดๅบ้ไนใไธญๆๆ ธๅฟ่ฏ๏ผ๏ผๅคงๅน
ๆๅๆจก็ณๆ่ฟฐ็ๅฝไธญ็ใ
|
| 44 |
+
* **ๆบ่ฝๅ่ฏไธๆพๅผๅ้๏ผ** ๆฏๆไธ้ฎๅผๅฏ/ๅ
ณ้ญ่ชๅจๅ่ฏใๅผๅฏๅ่ชๅจๆๅ้ฟๅฅไธญๆฆๅฟตๅๅซๆฃ็ดข๏ผๅๆถๆฏๆ็จ็ฉบๆ ผใๆข่กใไธญๆ้ๅท๏ผ๏ผ๏ผๅ้กฟๅท๏ผใ๏ผๆๅจๅ้ๆฆๅฟต๏ผ็ณป็ปไผๅฐ้ไฝ ็ๅ้ๆๅพ๏ผๅฐๆฏไธช็ๆฎตไฝไธบๆดไฝๅน้
๏ผ่ถ
่ฟ 7 ๅญ็็ๆฎตไป็ถ่ชๅจๅ่ฏ๏ผใ
|
| 45 |
+
* **ๆ ็ญพไบ็ธๅ
ณๆจ่๏ผ** ๅผๆๅ
็ฝฎๆ ็ญพๅ
ฑ็ฐๆฐๆฎใๅฝไฝ ้ไธญๆไบๆ ็ญพๅ๏ผ็ณป็ปไผ่ชๅจๆจ่ๅจ Danbooru ๅพๅบไธญ็ปๅธธไธๅฎไปฌไธๅๅบ็ฐ็็ธๅ
ณๆ ็ญพ๏ผ่พ
ๅฉ่กฅๅ
จ็ป้ข็ป่ใ
|
| 46 |
+
* **็ฒพ็ปๅๆฅ้
ไธ่ฟๆปค๏ผ** ๆฏๆๆๆ ็ญพ็ฑปๅซ๏ผGeneral้็จ / Character่ง่ฒ / Copyrightไฝๅ๏ผ่ฟ่ก็กฌ่ฟๆปคใ
|
| 47 |
+
* ๆไพ **NSFW ไฟๆคๆจกๅผ**๏ผๅผๅฏๅ่ชๅจๆจก็ณๆๆ่ฏๆฑ๏ผใ
|
| 48 |
+
* ่กจๆ ผๅ
็ฝฎ Danbooru ็ปดๅบ้ไนๆฌๆตฎๆ็คบ๏ผๅนถๆไพๅๅพๅ็ซ็ๅฟซๆท้พๆฅใ
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
## ๅ่ฝๆฆ่ง
|
| 52 |
+
|
| 53 |
+
ๆฌๅทฅๅ
ทๆไพไปฅไธไบ้กนๆ ธๅฟๅ่ฝ๏ผ
|
| 54 |
+
|
| 55 |
+
- [็ฒพ็กฎๆฅ่ฏ](#1-็ฒพ็กฎๆฅ่ฏ)๏ผๅฎน้ๆผๅ้่ฏฏ๏ผ
|
| 56 |
+
- [ๆฆๅฟตๆจก็ณๅๆฃ](#2-ๆฆๅฟตๆจก็ณๅๆฃ)
|
| 57 |
+
- [่พๅ
ฅๆ่ฟฐๆฅ่ฏ](#3-่พๅ
ฅๆ่ฟฐๆฅ่ฏ)
|
| 58 |
+
- [ๅฎๆด็ป้ขๆฅๆพ](#4-ๅฎๆด็ป้ขๆฅๆพ)
|
| 59 |
+
- [ๅ
ณ่ๆจ่](#5-ๅ
ณ่ๆจ่ๅ
ฑ็ฐๅน้
)
|
| 60 |
+
|
| 61 |
+
ๅๅ้กนๅบไบ**่ฏญไนๅ้ๅน้
**๏ผๅไธ้กนๅบไบ**ๆ ็ญพๅ
ฑ็ฐๆฐๆฎ**ใ
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## ๅๆฐ่ฏดๆ
|
| 66 |
+
|
| 67 |
+
ๅจ่ฟๅ
ฅๅ่ฝไป็ปไนๅ๏ผๅ
ไบ่งฃๅ ไธชๅ
ณ้ฎๅๆฐ๏ผๅฎไปฌไผๅฝฑๅๆๆๆ็ดขๆจกๅผ็็ปๆ่ดจ้ใ
|
| 68 |
+
|
| 69 |
+
**Top K๏ผ่ฏญไน็ธๅ
ณ๏ผ**
|
| 70 |
+
้ๅฏนๅไธชๅ่ฏ๏ผๆฃ็ดข่ฏญไนๆ็ธๅ
ณ็ๅ้ๆ ็ญพๆฐ้ใ่ฏฅๅผไฝ็จไบๆฏไธชๅ้ๆฃ็ดขๅฑ๏ผๆ็ปๅ้ๆฑ ๅคงๅฐ็บฆไธบ Top K ร ๅฑๆฐ๏ผๅๆชๆญ่ณ็ปๆไธ้ใๅผ่ถๅคง๏ผๅฌๅ่ถๅนฟ๏ผไฝ็ญๅบฆ้ซ็ๅ่ฏไผๅ ๆฎๆดๅคๅ้ข๏ผๅฏ่ฝๆคๅไฝ้ขไฝ็ฒพ็กฎ็ๅ่ฏใ็ฒพ็กฎๆฅ่ฏๅบๆฏๅปบ่ฎฎ่ฐไฝ๏ผ20๏ผ๏ผๅๆฃๆข็ดขๅบๆฏๅฏไปฅ่ฐ้ซ๏ผ80~160๏ผใ
|
| 71 |
+
|
| 72 |
+
**็ปๆไธ้**
|
| 73 |
+
ๆ็ปๅฑ็คบ็ๆ ็ญพๆปๆกๆฐใๅปบ่ฎฎ่ฎพไธบ 70~80 ไปฅ้้
ไธปๆต SDXL ๆจกๅ็ prompt ้ฟๅบฆใ็ฒพ็กฎๆฅ่ฏๆถๅฏไปฅ่ฐไฝๅฐ 10๏ผ้ฟๅ
ๅนฒๆฐใ
|
| 74 |
+
|
| 75 |
+
**็ญๅบฆๆ้๏ผ0~1๏ผ**
|
| 76 |
+
ๆงๅถๆ ็ญพๅจ Danbooru ็ซๅ
ๅๅธ้ๅฏนๆ็ปๆๅบ็ๅฝฑๅใๆฐๅผ่ถ้ซ๏ผ่ถๅพๅไบๆจ่ๅธธ็จ้ซ้ขๆ ็ญพใ้ป่ฎคๅผ 0.15 ๅจๅคงๅคๆฐๅบๆฏไธ่กจ็ฐ่ฏๅฅฝใ
|
| 77 |
+
|
| 78 |
+
**ๆบ่ฝๅ่ฏ**
|
| 79 |
+
ๅผๅฏๆถ๏ผ็จๅบไผ่ชๅจๆๅ่พๅ
ฅ็้ฟๅฅ๏ผๆๅๅ
ถไธญ็ๅ
ณ้ฎๆฆๅฟตๅๅซๆฃ็ดข๏ผๅๅๅนถ็ปๆใ้ๅๅฎๆด็ป้ขๆ่ฟฐ๏ผ่ฅๅชๆณๆฃ็ดขไธไธชๅฎๆด็ญ่ฏญ็่ฏญไน๏ผๅปบ่ฎฎๅ
ณ้ญใ
|
| 80 |
+
|
| 81 |
+
**ๆฐ็ๆฏๆๆพๅผๅ้็ฌฆ๏ผ** ็จ็ฉบๆ ผใๆข่กใๆไธญๆ้ๅท/้กฟๅท๏ผ๏ผใ๏ผๆๅจๅ้ๆฆๅฟตๆถ๏ผ็ณป็ปไผๅฐๆฏไธชๅ้็ๆฎต่งไธบ็ฌ็ซ็ๅๅญๆฆๅฟต๏ผไธๅๅ่ฟไธๆญฅๆๅใ่ฟๅจไฝ ๆ็กฎ็ฅ้่ชๅทฑๆณ่ฆๅชไบๆฆๅฟตๆถๆฏ่ชๅจๅ่ฏๆด็ฒพๅใ
|
| 82 |
+
|
| 83 |
+
| ่พๅ
ฅ็คบไพ | ๅ่ฏ็ปๆ | ่ฏดๆ |
|
| 84 |
+
|---|---|---|
|
| 85 |
+
| `่ฟๅจ็คพๅข ๆ ก้ ๆฏ่ต` | `่ฟๅจ็คพๅข` `ๆ ก้` `ๆฏ่ต` | ็ฉบๆ ผๅ้๏ผๆฏไธช็ๆฎตๅๆ ทไฟ็ |
|
| 86 |
+
| `่ฟๅจ็คพๅขใๆ ก้ใๆฏ่ต` | `่ฟๅจ็คพๅข` `ๆ ก้` `ๆฏ่ต` | ้ๅท/้กฟๅทๅ็ |
|
| 87 |
+
| `ไธไธช็ฉฟ็็ฝ่ฒๆฐดๆๆ็ๅฐๅฅณ` | `็ฉฟ็` `็ฝ่ฒ` `ๆฐดๆๆ` `ๅฐๅฅณ` | ๆ ๆพๅผๅ้๏ผ่ชๅจๅ่ฏ๏ผๅ็จ่ฏๅทฒ่ฟๆปค๏ผ |
|
| 88 |
+
|
| 89 |
+
> ๅฝๅ้ๅ็ๆไธช็ๆฎต่ถ
่ฟ **7 ไธชๅญ**ๆถ๏ผ็ณป็ปไผ้ๅๅฐ่ชๅจๅ่ฏโโๅ ไธบ่ถ
่ฟ่ฟไธช้ฟๅบฆ็็ๆฎต้ๅธธๆฏ็ญๅฅ่้ๅไธๆฆๅฟตใ่ฟไธ้ๅผๅบไบๅฏนๆๆๆ ็ญพๆ ธๅฟไธญๆๅ้ฟๅบฆ็็ป่ฎกๅๆๅพๅบใ
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## 1. ็ฒพ็กฎๆฅ่ฏ
|
| 94 |
+
|
| 95 |
+
**้็จๅบๆฏ๏ฟฝ๏ฟฝ๏ฟฝ** ไฝ ็ฅ้ๆไธชๆ ็ญพ็ๅคงๆฆๅๆณ๏ผไฝไธ็กฎๅฎๆผๅๆฏๅฆๅ็กฎ๏ผๆ่
ๅช่ฎฐๅพ้จๅๅๆณใ
|
| 96 |
+
|
| 97 |
+
ๆฌๅทฅๅ
ท้่ฟ่ฏญไน็ธไผผๅบฆๅน้
่้ๅญ็ฌฆไธฒ็ฒพ็กฎๅน้
๏ผๅ ๆญคๅ
ทๅคไธๅฎ็ๆผๅๅฎน้่ฝๅใ
|
| 98 |
+
|
| 99 |
+
**ๅปบ่ฎฎๅๆฐ๏ผ** Top K: 20 | ็ปๆไธ้: 10 | ็ญๅบฆๆ้: 0.15 | **ๅ
ณ้ญๆบ่ฝๅ่ฏ**
|
| 100 |
+
|
| 101 |
+
> ๅ
ณ้ญๆบ่ฝๅ่ฏ๏ผ็กฎไฟๆดไธช่พๅ
ฅไฝไธบไธไธชๆดไฝ่ฟ่ก่ฏญไนๅน้
๏ผไธไผ่ขซๆๆฃใ
|
| 102 |
+
|
| 103 |
+
### ็คบไพ
|
| 104 |
+
|
| 105 |
+
| ไฝ ็่พๅ
ฅ | ๅฎ้
ๆ ๅๆ ็ญพ | ่ฏดๆ |
|
| 106 |
+
|---|---|---|
|
| 107 |
+
| `selafuku` | `serafuku` | ็ปๅ
ธๆผๅ้่ฏฏ๏ผไปๅฏๅฝไธญ |
|
| 108 |
+
| `school unifor` | `school_uniform` | ๆผๅญๆฏ๏ผไปๅฏๅฝไธญ |
|
| 109 |
+
| `twintail` | `twintails` | ๅๅคๆฐๆททๆท๏ผไปๅฏๅฝไธญ |
|
| 110 |
+
| `zettai ryouiki` | `zettai_ryouiki` | ็ฝ้ฉฌ้ณ่พๅ
ฅ๏ผๅฏๅฝไธญ |
|
| 111 |
+
| `thighhigh` | `thighhighs` | ่ฏๅฝขๅไฝ๏ผไปๅฏๅฝไธญ |
|
| 112 |
+
|
| 113 |
+
---
|
| 114 |
+
|
| 115 |
+
## 2. ๆฆๅฟตๆจก็ณๅๆฃ
|
| 116 |
+
|
| 117 |
+
**้็จๅบๆฏ๏ผ** ไฝ ่ไธญๆไธไธชๆจก็ณ็ๆฆๅฟตๆ้ฃๆ ผๆ่ฑก๏ผไฝไธ็ฅ้ๅจ Danbooru ้ๅฏนๅบๅชไบๆ ็ญพ๏ผๅธๆ็ณป็ปๅธฎไฝ "ๅๆฃ"ๅบ็ธๅ
ณ็ๅ้ๆ ็ญพใ
|
| 118 |
+
|
| 119 |
+
**ๅปบ่ฎฎๅๆฐ๏ผ** Top K: 80~160 | ็ปๆไธ้: 80 | ็ญๅบฆๆ้: 0.15 | **ๅผๅฏๆบ่ฝๅ่ฏ**
|
| 120 |
+
|
| 121 |
+
### ็คบไพ
|
| 122 |
+
|
| 123 |
+
**็คบไพ 1๏ผใๅ
่ณๆตใ**
|
| 124 |
+
|
| 125 |
+
่พๅ
ฅไธไธชๅฎฝๆณๆฆๅฟต๏ผ็ณป็ปไผ่ฟๅๅ็ฑป่ณๅ็ธๅ
ณๆ ็ญพ๏ผๅ
ๆฌ็ๅ
่ณใๅคด้ฅฐ่ณใๅ
ฝๅจ่ณ็ญๅไฝใ
|
| 126 |
+
|
| 127 |
+

|
| 128 |
+
|
| 129 |
+
---
|
| 130 |
+
|
| 131 |
+
**็คบไพ 2๏ผใไธญๅฝ้ฃๅค่ฃ
ใ**
|
| 132 |
+
|
| 133 |
+
่ฟๅๆฑๆใๆ่ขใๅๆไปฃๆๅถ็ญ็ปๅๆ ็ญพ๏ผๅธฎๅฉไฝ ๅฟซ้ไบ่งฃ่ฟไธ้ฃๆ ผไธ็ๆ ็ญพไฝ็ณปใ
|
| 134 |
+
|
| 135 |
+

|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
**็คบไพ 3๏ผใๆบๆขฐไน่ขใ**
|
| 140 |
+
|
| 141 |
+
่ฟๅๅ
ๅซไน่ขใๆบๆขฐ่ใ่ตๅๆๅ
่ขไฝ็ญๆ ็ญพ๏ผ้ๅ่ฟไธๆญฅ็ปๅๅ
ณ่ๆจ่ๅ็ปๅ็ญ้ใ
|
| 142 |
+
|
| 143 |
+

|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
**็คบไพ 4๏ผใๆๆๅงฟๅฟใ**
|
| 148 |
+
|
| 149 |
+
่ฟๅๅ็ฑปๆๆๅจไฝใๆๆญฆๅจๅจไฝ็ญๆ ็ญพใ
|
| 150 |
+
|
| 151 |
+

|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
|
| 155 |
+
## 3. ่พๅ
ฅๆ่ฟฐๆฅ่ฏ
|
| 156 |
+
|
| 157 |
+
**้็จๅบๆฏ๏ผ** ไฝ ่ฝๆ่ฟฐๆไธชๅ
ทไฝ็ไบ็ฉใ่ง่ฒใๅบๆฏ๏ผไฝไธ็ฅ้ Danbooru ็ๅฏนๅบๆ ็ญพใๅฏไปฅไฝฟ็จไธญๆๆ่ฑๆ่ช็ถ่ฏญ่จๆ่ฟฐ๏ผไนๅฏไปฅ้่ฟ IP ๅ็งฐใ่ง่ฒๅคๅทๆฅๆฅๆพ่ง่ฒๆ ็ญพใ
|
| 158 |
+
|
| 159 |
+
**ๅปบ่ฎฎๅๆฐ๏ผ** Top K: 20 | ็ปๆไธ้: 20 | ็ญๅบฆๆ้: 0.15 | **ๅ
ณ้ญๆบ่ฝๅ่ฏ**
|
| 160 |
+
|
| 161 |
+
> ๅ
ณ้ญๆบ่ฝๅ่ฏ๏ผไฝฟ็ณป็ปๅฏนไฝ ็ๆดๆฎตๆ่ฟฐๅๆดไฝ่ฏญไนๅน้
๏ผ่ไธๆฏๆ่ฏใ
|
| 162 |
+
|
| 163 |
+
### ็คบไพ
|
| 164 |
+
|
| 165 |
+
**็คบไพ 1๏ผ็จๆ่ฟฐๅฎไฝ่ง่ฒ**
|
| 166 |
+
|
| 167 |
+
| ไฝ ็่พๅ
ฅ | ๆๆๅฝไธญ็่ง่ฒๆ ็ญพ | ่ฏดๆ |
|
| 168 |
+
|---|---|---|
|
| 169 |
+
| `ๅ็ฅ้้ข้กปๅผฅไธป็บฟ็NPC่ดตๆๅฅณๅญฉ` | `dunyarzad_(genshin_impact)`๏ผ่ฟช็บณๆณฝ้ป๏ผ | ้่ฟๅงๆ
่บซไปฝๆ่ฟฐๅฎไฝ่ง่ฒ |
|
| 170 |
+
| `ๅฝ่ฟ็ณไน้จ็ๅฉๆ` | `makise_kurisu`๏ผ็ง็ฌ็บข่ๆ ๏ผ | ้่ฟไบบ็ฉๅ
ณ็ณปๆ่ฟฐๅฎไฝ่ง่ฒ |
|
| 171 |
+
| `ๆ็งๅญฆ็่ถ
็ต็ฃ็ฎไธญไผไผ ้็้ฃไธชไบบ` | `shirai_kuroko`๏ผ็ฝไบ้ปๅญ๏ผ | ้่ฟ่ฝๅๅๅจไฝๆ่ฟฐๅฎไฝ่ง่ฒ |
|
| 172 |
+
| `EVA ้้ฉพ้ฉถ้ถๅทๆบ็่ๅ่ง่ฒ` | `ayanami_rei`๏ผๅๆณขไธฝ๏ผ | ้่ฟๆบไฝๅๅค่ฒๆ่ฟฐๅฎไฝ่ง่ฒ |
|
| 173 |
+
| `ๆๆฅๆน่้ๆธธๆๅๅบ็่ๆฟ` | `closure_(arknights)`๏ผๅฏ้ฒๅธๅฐ๏ผ | ้่ฟ่ไฝๆ่ฟฐๅฎไฝ่ง่ฒ |
|
| 174 |
+
|
| 175 |
+
---
|
| 176 |
+
|
| 177 |
+
**็คบไพ 2๏ผ็จๆ่ฟฐๅฎไฝๆ้ฅฐ/็ฉๅ**
|
| 178 |
+
|
| 179 |
+
| ไฝ ็่พๅ
ฅ | ๆๆๅฝไธญ็ๆ ็ญพ | ่ฏดๆ |
|
| 180 |
+
|---|---|---|
|
| 181 |
+
| `็ดง่บซ่กฃๅๅ
ฅ็ฎ่คไบง็็ๅน้ท` | `skindentation` | ้ๅธธๅ
ทไฝ็่ง่ง็ป่ๆ่ฟฐ |
|
| 182 |
+
| `ๅ้ๆ่็บฑ่ฆ็่ธๅ` | `see-through_bra` | ๆ่ดจไธ้ฎ็ๅ
ณ็ณปๆ่ฟฐ |
|
| 183 |
+
| `ไธคไพงๆๅผ็ผใๆๆ็ปณ็่ฟๅจ็ญ่ฃค` | `dolphin_shorts` | ๆ้ฅฐ็ป่ๆ่ฟฐ |
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
## 4. ๅฎๆด็ป้ขๆฅๆพ
|
| 188 |
+
|
| 189 |
+
**้็จๅบๆฏ๏ผ** ไฝ ่ไธญๅทฒ็ปๆไบไธๅน
ๅฎๆด็็ป้ข๏ผๅธๆไธๆฌกๆงๆๆดๆฎตๆ่ฟฐ่ฝฌๆขๆๅฐฝๅฏ่ฝๅค็ Danbooru ๆ ็ญพ๏ผ็จไบ AI ็ป็ป็ๅฎๆด Promptใ
|
| 190 |
+
|
| 191 |
+
**ๅปบ่ฎฎๅๆฐ๏ผ** Top K: 5 | ็ปๆไธ้: 80 | ็ญๅบฆๆ้: 0.15 | **ๅผๅฏๆบ่ฝๅ่ฏ**
|
| 192 |
+
|
| 193 |
+
> ๅผๅฏๆบ่ฝๅ่ฏ๏ผ็ณป็ปไผ่ชๅจๆๅไฝ ็้ฟๅฅ๏ผๆๅๅ
ถไธญ็ๅ
ณ้ฎๆฆๅฟตๅๅซๆฃ็ดข๏ผๅๅๅนถๅป้๏ผ่ฆ็ๅฐฝๅฏ่ฝๅค็็ป้ขๅ
็ด ใ
|
| 194 |
+
|
| 195 |
+
### ็คบไพ
|
| 196 |
+
|
| 197 |
+
**็คบไพ 1๏ผ้จไธญๅฅ่ท็ๅฐๅฅณ**
|
| 198 |
+
|
| 199 |
+
```
|
| 200 |
+
ไธไธช็ฉฟ็็ฝ่ฒๆฐดๆๆใ่่ฒ็ญ่ฃ็ๅฐๅฅณ๏ผๅจไธ็ๅคง้จ็ๅๅธ่ก้ๅฅ่ท๏ผ
|
| 201 |
+
ๅฅน็่กจๆ
ๆฏไธ็ใๆคๆใๆตๆณช๏ผๅฅน็่กฃๆๆนฟ้ใ
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
> short_dress, city, streaming_tears, street, white_serafuku, furious, fume, rain, tears, running, skirt, crying, tearing_up, urban
|
| 205 |
+
|
| 206 |
+
**็คบไพ 2๏ผ่ตๅๆๅ
ๅๅธๅคๆฏ**
|
| 207 |
+
|
| 208 |
+
```
|
| 209 |
+
่ตๅๆๅ
้ฃๆ ผ็ๅๅธ๏ผ้่น็ฏๆ็๏ผ้จๅๆฝฎๆนฟ็่ก้ๅๆ ็ฏๅ
๏ผ
|
| 210 |
+
่ฟๅค้ซๆฅผๆ็ซ๏ผไธไธช็ฉฟ็็ฎๅคนๅ
็ๅฅณๅญฉ็ซๅจ่ทฏ็ฏไธๆฝ็ใ
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
> street, reflection, lights, lamp, leather_jacket, fur_jacket, 1girl, standing, lamppost, smoking, holding_cigarette, cyberpunk, after_rain, city, urban, neon_lights, wet_floor, utility_pole
|
| 214 |
+
|
| 215 |
+
---
|
| 216 |
+
|
| 217 |
+
**็คบไพ 3๏ผๅฅๅนปๆๅบ**
|
| 218 |
+
|
| 219 |
+
```
|
| 220 |
+
ไธไธช้ถๅ็็ฒพ็ตๅฅณๆๅฃซ๏ผ่บซ็ฉฟ่ฝป็ฒ๏ผๆๆๅผ็ฎญ๏ผ็ซๅจๅบๅขไธ๏ผ
|
| 221 |
+
่ๆฏๆฏๆญฃๅจ็็ง็ๅๅธๅๅธๆปกไนไบ็ๅคฉ็ฉบ๏ผ่กฃๆไธๆ่ก่ฟนใ
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
> burning, city, dark_cloud, sky, clothes, blood_stain, blood_on_clothes, silver_hairband, standing, white_hair, cyberpunk, after_rain, holding, bow_(weapon), urban, ruins, elf, rubble, holding_bow_(weapon), arrow_(projectile), neon_lights, burning_building, wet_floor, utility_pole
|
| 225 |
+
|
| 226 |
+
---
|
| 227 |
+
|
| 228 |
+
**ๆๅทง๏ผๅ
ณไบๆ่ฟฐ็ๅๆณ**
|
| 229 |
+
|
| 230 |
+
- ๅๆฎตๆ่ฟฐไธๅ่ฆ็ด ๏ผไบบ็ฉ / ่ๆฏ / ่กจๆ
/ ๆ่ฃ
๏ผๆฏๆททๅจไธ่ตทๆๆๆดๅฅฝ
|
| 231 |
+
- ็ป่่ถๅ
ทไฝ๏ผๅน้
่ถๅ็กฎโโใ่่ฒ้ฟๅใไผไบใ็นๅซ็ๅ่ฒใ
|
| 232 |
+
- ้ๅฐๆ่งๅบ่ฏฅๆไฝๆฒกๅฝไธญ็ๆ ็ญพ๏ผๅฏไปฅๆ่ฏฅ้จๅๅ็ฌๆชๅ๏ผๅๆขๅฐใ่พๅ
ฅๆ่ฟฐๆฅ่ฏใๆจกๅผๅ็ฌๆฅ
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
## 5. ๅ
ณ่ๆจ่๏ผๅ
ฑ็ฐๅน้
๏ผ
|
| 237 |
+
|
| 238 |
+
ๅ
ณ่ๆจ่ไธๅๅ้กนๅ่ฝๆบๅถไธๅ๏ผๅฎไธไพ่ต่ฏญไน็ธไผผๅบฆ๏ผ่ๆฏๅบไบ **Danbooru ๅพๅบไธญๆ ็ญพ็ๅ
ฑ็ฐ็ป่ฎก**๏ผๅฆๆไธคไธชๆ ็ญพ็ปๅธธๅบ็ฐๅจๅไธๅผ ๅพไธ๏ผๅฐฑ่ฎคไธบๅฎไปฌ็ธๅ
ณใ
|
| 239 |
+
|
| 240 |
+
**ไฝฟ็จๆนๅผ๏ผ** ๅจๅทฆไพง็ปๆ่กจๆ ผไธญๅพ้ไฝ ๆๅ
ด่ถฃ็ๆ ็ญพ๏ผๅณไพงๅ
ณ่ๆจ่ๆ ไผ่ชๅจๅทๆฐ๏ผๆพ็คบๅจ Danbooru ๅพๅบไธญไธไฝ ้ไธญๆ ็ญพๆๅธธๅ
ฑ็ฐ็ๅ
ถไปๆ ็ญพใไนๅฏไปฅ็นๅปใๆ นๆฎๅทฒ้ๅทๆฐใๆๅจ่งฆๅใ
|
| 241 |
+
|
| 242 |
+
ๅ
ณ่ๆจ่็ๅพๅๆฏๅบไบ NPMI๏ผไบไฟกๆฏๅฝไธๅ๏ผ่ฎก็ฎ็๏ผ่ฝๆๆ่ฟๆปคๆ้ฃไบๅชๆฏๅ ไธบ่ช่บซ็ญๅบฆ้ซ่้ข็นๅบ็ฐ็ๆ ็ญพใ
|
| 243 |
+
|
| 244 |
+
### ้็จๅบๆฏ
|
| 245 |
+
|
| 246 |
+
**ๅบๆฏ 1๏ผๆฅ็ๆ็งๆ้ฅฐ/้ๅ
ท็ๅธธ่งๆญ้
**
|
| 247 |
+
|
| 248 |
+
ๅพ้ `fingerless_gloves`๏ผ้ฒๆๆๅฅ๏ผ๏ผๅ
ณ่ๆจ่ไผๅๅบ็ฉฟ้ฒๆๆๅฅ็่ง่ฒไปฌใ
|
| 249 |
+
|
| 250 |
+

|
| 251 |
+
|
| 252 |
+
---
|
| 253 |
+
|
| 254 |
+
**ๅบๆฏ 2๏ผๆฅ็ๆไธช่ง่ฒ็ๅ
ธๅ็นๅพ**
|
| 255 |
+
|
| 256 |
+
ๅพ้่ง่ฒๆ ็ญพ๏ผๅฆ `amiya_(arknights)`๏ผ๏ผๅ
ณ่ๆจ่ไผๅๅบ่ฏฅ่ง่ฒๆๅธธๅบ็ฐ็ๆ่ฃ
ใ้
ไปถใ่กจๆ
็ญๆ ็ญพ๏ผ็ธๅฝไบ่ชๅจ็ๆ่ง่ฒ็"ๆ ็ญพ็ปๅ"ใ
|
| 257 |
+
|
| 258 |
+

|
| 259 |
+
|
| 260 |
+
---
|
| 261 |
+
|
| 262 |
+
**ๅบๆฏ 3๏ผๆข็ดขๆ็ฑปไธป้ข็ๆ ็ญพไฝ็ณป**
|
| 263 |
+
|
| 264 |
+
ๅพ้ `fighter_jet`๏ผๆๆๆบ๏ผ๏ผๅ
ณ่ๆจ่ไผๅๅบๅ็ฑปๆบๅใ้ฃ่กๅจไฝใ่ๆฏ็ญ็ธๅ
ณๆ ็ญพ๏ผๅธฎๅฉไฝ ๅฟซ้ๆธๆธ
ไธไธชไธป้ขไธ็ๆ ็ญพ็ๆใ
|
| 265 |
+
|
| 266 |
+

|
| 267 |
+
|
| 268 |
+
---
|
| 269 |
+
|
| 270 |
+
**ๅบๆฏ 4๏ผ็ปๅๅคไธชๆ ็ญพ่ทๅพไบค้ๆจ่**
|
| 271 |
+
|
| 272 |
+
ๅๆถๅพ้ๅคไธชๆ ็ญพๆถ๏ผๅ
ณ่ๆจ่ไผ็ปผๅ่ฎก็ฎๅคๆ ็ญพ็ๅ
ฑ็ฐๅ
ณ็ณป๏ผๆจ่ไธ่ฟ็ปๆ ็ญพ**ๆดไฝ**ๆๅน้
็ๆ ็ญพ๏ผ่ไธๅชๆฏๅไธชๆ ็ญพ็้ปๅฑ
ใ
|
| 273 |
+
|
| 274 |
+
ๆฏๅฆๅๆถๅพ้ `maid` + `twintails`๏ผๆจ่็ปๆไผๆดๅพๅไบใๅฅณไปไธๅ้ฉฌๅฐพใๅบๆฏไธ็ปๅธธๅบ็ฐ็ๆ ็ญพ๏ผ่ไธๆฏๅ็บฏใๅฅณไปใๆใๅ้ฉฌๅฐพใๅ่ช็้ซ้ขๆญ้
ใ
|
| 275 |
+
|
| 276 |
+

|
| 277 |
+
|
| 278 |
+
---
|
| 279 |
+
|
| 280 |
+
## ไฝฟ็จๆๅทง
|
| 281 |
+
|
| 282 |
+
**ๆๅทง 1๏ผๅ
ๅฎฝๅ็ช**
|
| 283 |
+
ๅ
็จๅฎๆด็ป้ขๆฅๆพๆฟๅฐๅคง้ๅ้ๆ ็ญพ๏ผๅ็จๅ่ฏ็ญ้ chip ๆๆๅจๅปๆไธ็ธๅ
ณ็๏ผ็ถๅๅพ้ๅฉไฝๆ ็ญพ่งฆๅๅ
ณ่ๆจ่๏ผ่กฅๅ
้ๆผ็ป่ใ
|
| 284 |
+
|
| 285 |
+
**ๆๅทง 2๏ผๆฟไธๅๆ ็ญพๅซไนๆถๆฌๆตฎๆฅ็**
|
| 286 |
+
้ผ ๆ ๆฌๅๅจไปปๆๆ ็ญพ่กๆๅ
ณ่ๆจ่่กไธ๏ผไผๅผนๅบ่ฏฅๆ ็ญพ็ Danbooru ็ปดๅบ้ไน๏ผๆนไพฟๅฟซ้ๅคๆญๆฏๅฆ็ฌฆๅ้ๆฑ๏ผๆ ้่ทณ่ฝฌ็ซๅคใ
|
| 287 |
+
|
| 288 |
+
**ๆๅทง 3๏ผ็นๅปๆ ็ญพๅ็ดๆฅ่ทณ่ฝฌ**
|
| 289 |
+
ๆ ็ญพๅๆฏๅฏ็นๅป้พๆฅ๏ผ็นๅปๅๅจๆฐๆ ็ญพ้กตๆๅผ Danbooru ็ๅฏนๅบ็ปดๅบ้กต๏ผๅฏไปฅๆฅ็่ฏฆ็ป่ฏดๆใ็ธๅ
ณๅพไพๅ่ฟไนๆ ็ญพใ
|
| 290 |
+
|
| 291 |
+
**ๆๅทง 4๏ผNSFW ไฟๆคๆจกๅผ**
|
| 292 |
+
้ป่ฎคๅ
ณ้ญ NSFW ๆพ็คบใๆๅผๅๆ ็ญพ็ๅฎๆดๅ็งฐๅๅซไนไผๆพ็คบๅบๆฅ๏ผไฝๅนณๅฐ้ๅถไธ๏ผ้ญๆญๅ็ฉบ้ด๏ผๆญค้้กนไธๅฏ็จใ
|
| 293 |
+
|
| 294 |
+
**ๆๅทง 5๏ผๅคๅถๅ
จ้จ vs ๅคๅถ้ไธญ**
|
| 295 |
+
- **ๅคๅถๅ
จ้จๆ ็ญพ**๏ผๆๅฝๅๆ็ดข็ปๆไธญๆๆๆ ็ญพๅคๅถไธบ้ๅทๅ้็ๅญ็ฌฆไธฒ๏ผ้ๅๅฟซ้็ๆๅฎๆด Prompt
|
| 296 |
+
- **ๅคๅถ้ไธญๆ ็ญพ**๏ผๅชๅคๅถไฝ ๅจ่กจๆ ผไธญๆๅจๅพ้็ๆ ็ญพ๏ผ้ๅ็ฒพ้ๅไฝฟ็จ
|
| 297 |
+
|
| 298 |
+
**ๆๅทง 6๏ผ็จๆพๅผๅ้็ฌฆ็ฒพ็ปๆงๅถๅ่ฏ**
|
| 299 |
+
ๅฝไฝ ็ฅ้่ชๅทฑๆณ่ฆๅชไบ็ฌ็ซๆฆๅฟตๆถ๏ผ็จ็ฉบๆ ผๆไธญๆ้ๅทๆๅจๅ้ๅฎไปฌ๏ผ
|
| 300 |
+
- ใ`่ฟๅจ็คพๅข ๆ ก้ ๆฏ่ต ่ฟๅจไผ ไฝ่ฒ็ฅญ ็ฐๅพ้จ`ใโ ๆฏไธชๆฆๅฟตๅๆ ทไฟ็
|
| 301 |
+
- ใ`ๅไนๆ้ฆใ่ตๅๆๅ
ใ่ธๆฑฝๆๅ
ใๅบๅ`ใโ ้ๅทๅ้ๅ็
|
| 302 |
+
- ๅ้ๅ็็ญ็ๆฎต๏ผโค7 ๅญ๏ผไฝไธบๆดไฝๅน้
๏ผไธไผ่ขซ้่ฏฏๆๅผ
|
| 303 |
+
- ๅฆๆ้่ฆๆททๅ้ฟ็ญๆฅ่ฏข๏ผๅฏไปฅไธ้จๅ็จ็ฉบๆ ผๅ้ใๅฆไธ้จๅๅๆ่ช็ถ่ฏญๅฅ
|
| 304 |
+
|
| 305 |
+
---
|
| 306 |
+
|
| 307 |
+
## API ๆฅๅฃ
|
| 308 |
+
|
| 309 |
+
ๆฌๅทฅๅ
ทๅๆถๆไพ REST API๏ผๅฏ้ๆๅฐไฝ ่ชๅทฑ็ๅทฅไฝๆตไธญใ
|
| 310 |
+
|
| 311 |
+
ๅฏๅจๅ่ฎฟ้ฎ `/api/docs` ๆฅ็ๅฎๆด็ไบคไบๅผ API ๆๆกฃใ
|
| 312 |
+
|
| 313 |
+
**ๆ็ดขๆฅๅฃ**
|
| 314 |
+
```
|
| 315 |
+
POST /api/search
|
| 316 |
+
{
|
| 317 |
+
"query": "็ฝ่ฒๆฐดๆๆ็ๅฅณๅญฉ",
|
| 318 |
+
"top_k": 20,
|
| 319 |
+
"limit": 20,
|
| 320 |
+
"show_nsfw": false
|
| 321 |
+
}
|
| 322 |
+
```
|
| 323 |
+
|
| 324 |
+
**ๅ
ณ่ๆจ่ๆฅๅฃ**
|
| 325 |
+
```
|
| 326 |
+
POST /api/related
|
| 327 |
+
{
|
| 328 |
+
"tags": ["white_serafuku", "sailor_collar"],
|
| 329 |
+
"limit": 20,
|
| 330 |
+
"show_nsfw": false
|
| 331 |
+
}
|
| 332 |
+
```
|
| 333 |
+
|
| 334 |
+
่ฏฆ่ง๏ผ[API ๆๆกฃ](https://sakizuki-danboorusearch.hf.space/api/docs)
|
| 335 |
+
|
| 336 |
+
---
|
| 337 |
+
|
| 338 |
+
## MCP ๆฅๅฃ
|
| 339 |
+
|
| 340 |
+
ๆฌๅทฅๅ
ทๆไพ MCP๏ผModel Context Protocol๏ผๆๅก๏ผๆฏๆๅฐๆ็ดขๅผๆ็ดๆฅๆฅๅ
ฅ Claude DesktopใCursorใCherry Studio ็ญๆฏๆ MCP ๅ่ฎฎ็ๅคงๆจกๅๅฎขๆท็ซฏ๏ผ่ฎฉ AI ่ฝๅคๅจๅฏน่ฏไธญ็ดๆฅ่ฐ็จๆ ็ญพๆ็ดข่ฝๅใ
|
| 341 |
+
|
| 342 |
+
**MCP ๆๅกๅฐๅ๏ผ**
|
| 343 |
+
```
|
| 344 |
+
https://sakizuki-danboorusearch.hf.space/mcp/mcp
|
| 345 |
+
```
|
| 346 |
+
|
| 347 |
+
### ๆฅๅ
ฅๆนๆณ
|
| 348 |
+
|
| 349 |
+
**ๆนๆณไธ๏ผClaude Desktop**
|
| 350 |
+
|
| 351 |
+
Claude Desktop ไธๆฏๆ็ดๆฅ่ฟๆฅ Streamable HTTP ็ซฏ็น๏ผ้่ฆ้่ฟ `mcp-remote` ไฝไธบๆฌๅฐๆกฅๆฅใ
|
| 352 |
+
|
| 353 |
+
้ฆๅ
ๅ
จๅฑๅฎ่ฃ
`mcp-remote`๏ผ้่ฆๆฌๆบๅทฒๅฎ่ฃ
Node.js๏ผ๏ผ
|
| 354 |
+
|
| 355 |
+
```bash
|
| 356 |
+
npm install -g mcp-remote
|
| 357 |
+
```
|
| 358 |
+
|
| 359 |
+
็ถๅๅจ้
็ฝฎๆไปถไธญๆทปๅ ไปฅไธๅ
ๅฎน๏ผ
|
| 360 |
+
|
| 361 |
+
- Windows๏ผ`%APPDATA%\Claude\claude_desktop_config.json`
|
| 362 |
+
- macOS๏ผ`~/Library/Application Support/Claude/claude_desktop_config.json`
|
| 363 |
+
|
| 364 |
+
```json
|
| 365 |
+
{
|
| 366 |
+
"mcpServers": {
|
| 367 |
+
"danbooru-searcher": {
|
| 368 |
+
"command": "mcp-remote",
|
| 369 |
+
"args": [
|
| 370 |
+
"https://sakizuki-danboorusearch.hf.space/mcp/mcp"
|
| 371 |
+
]
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
```
|
| 376 |
+
|
| 377 |
+
ไฟๅญๅ้ๅฏ Claude Desktop๏ผๅทฅๅ
ทๅ่กจไธญๅบ็ฐ `search_tags` ๅ `get_related_tags` ๅณไธบๆๅใ
|
| 378 |
+
|
| 379 |
+
> ๆณจๆ๏ผไธ่ฆไฝฟ็จ `"url"` ๅญๆฎต็ดๆฅๅกซๅๅฐๅ๏ผClaude Desktop ไธๆฏๆ่ฏฅๆ ผๅผ๏ผไผๆ็คบ้
็ฝฎๆ ๆใไนไธๆจ่้่ฟ `npx mcp-remote` ่ฐ็จ๏ผ้ฆๆฌกๆง่กๆถ `npx` ้่ฆไธดๆถไธ่ฝฝๅ
๏ผnpm ็ผๅญๆๅๆถไผๅฏผ่ดๅฏๅจๅคฑ่ดฅใๅ
จๅฑๅฎ่ฃ
ๅฏ่ง้ฟๆญค้ฎ้ขใ
|
| 380 |
+
|
| 381 |
+
**ๆนๆณไบ๏ผๅพๅฝข็้ข๏ผCherry Studio ็ญ๏ผ**
|
| 382 |
+
|
| 383 |
+
ๅจ MCP ๆๅกๅจ็ฎก็้กต้ข็นๅปใๆทปๅ ใ๏ผๅกซๅไปฅไธไฟกๆฏ๏ผ
|
| 384 |
+
|
| 385 |
+
- ๅ็งฐ๏ผ`danbooru-searcher`๏ผๅฏ่ชๅฎไน๏ผ
|
| 386 |
+
- ็ฑปๅ๏ผ`Streamable HTTP`
|
| 387 |
+
- URL๏ผ`https://sakizuki-danboorusearch.hf.space/mcp/mcp`
|
| 388 |
+
|
| 389 |
+
> ๆณจๆ๏ผ็ฑปๅๅกๅฟ
้ๆฉ **Streamable HTTP**๏ผไธ่ฆ้ SSE๏ผๅฆๅๅทฅๅ
ทๅ่กจๆ ๆณๆญฃๅธธๅ ่ฝฝใ
|
| 390 |
+
|
| 391 |
+
ๆทปๅ ๅฎๆๅ้ๅฏๅฎขๆท็ซฏ๏ผๆๅจ MCP ็ฎก็็้ขๆๅจๅทๆฐ่ฟๆฅใ
|
| 392 |
+
|
| 393 |
+
### ๅฏ็จๅทฅๅ
ท
|
| 394 |
+
|
| 395 |
+
ๆฅๅ
ฅๅ AI ๅฏ่ฐ็จไปฅไธไธคไธชๅทฅๅ
ท๏ผ
|
| 396 |
+
|
| 397 |
+
**`search_tags`** โ ่ช็ถ่ฏญ่จๆ็ดขๆ ็ญพ
|
| 398 |
+
|
| 399 |
+
ๆฅๅไธญๆๆ่ฑๆๆ่ฟฐ๏ผ่ฟๅๅน้
็ Danbooru ๆ ็ญพๅ่กจๅๅฏ็ดๆฅ็จไบ AI ็ป็ป็ prompt ๅญ็ฌฆไธฒใๆฏๆๅๆฐ๏ผ
|
| 400 |
+
|
| 401 |
+
| ๅๆฐ | ้ป่ฎคๅผ | ่ฏดๆ |
|
| 402 |
+
|---|---|---|
|
| 403 |
+
| `query` | โ | ่ช็ถ่ฏญ่จๆ่ฟฐ๏ผๅฟ
ๅกซ๏ผๆจ่ไธญๆ๏ผใๆฏๆ็ฉบๆ ผ/้ๅท/้กฟๅทๆพๅผๅ้ๆฆๅฟต |
|
| 404 |
+
| `use_segmentation` | `true` | ๆบ่ฝๅ่ฏๅผๅ
ณ๏ผๅฎๆด็ป้ขๆ่ฟฐๆถๅผๅฏ๏ผ็ฒพ็กฎๆฅ่ฏๆถๅ
ณ้ญ |
|
| 405 |
+
| `top_k` | `5` | ๆฏไธชๅ่ฏ็่ฏญไนๅฌๅๆฐ้๏ผๅๆฃๆข็ดขๆถ่ฐ้ซ่ณ 80~160 |
|
| 406 |
+
| `limit` | `80` | ่ฟๅๆ ็ญพๆปๆฐไธ้ |
|
| 407 |
+
| `popularity_weight` | `0.15` | ็ญๅบฆๆ้๏ผๆงๅถ้ซ้ขๆ ็ญพ็ๆๅๅฝฑๅ |
|
| 408 |
+
| `show_nsfw` | `true` | ๆฏๅฆๅ
ๅซ NSFW ๆ ็ญพ |
|
| 409 |
+
| `category` | `"all"` | ๆ ็ญพ็ฑปๅซ็ญ้๏ผ`"all"` ๅ
จ้จใ`"general"` ้็จใ`"character"` ่ง่ฒใ`"copyright"` ไฝๅ |
|
| 410 |
+
|
| 411 |
+
**`get_related_tags`** โ ๅ
ฑ็ฐๅ
ณ่ๆจ่
|
| 412 |
+
|
| 413 |
+
็ปๅฎไธ็ปๅทฒ้ๆ ็ญพ๏ผ่ฟๅๅจ Danbooru ๅพๅบไธญไธไนๆๅธธๅ
ฑ็ฐ็็ธๅ
ณๆ ็ญพ๏ผๅบไบ NPMI ่ฏๅ๏ผๅฏๆๆ่ฟๆปคๆไป
ๅ ่ช่บซ็ญๅบฆ้ซ่้ข็นๅบ็ฐ็ๆ ็ญพใ
|
| 414 |
+
|
| 415 |
+
### ่ฐ็จ็คบไพ
|
| 416 |
+
|
| 417 |
+
ๆฅๅ
ฅๅ๏ผไฝ ๅฏไปฅ็ดๆฅ็จ่ช็ถ่ฏญ่จๅ่ฏ AI ไฝ ็้ๆฑ๏ผAI ไผ่ชๅจ้ๆฉๅ้็ๅๆฐ่ฐ็จๅทฅๅ
ท๏ผ
|
| 418 |
+
|
| 419 |
+
> ๅธฎๆๆใไธไธช็ฉฟ็ฝ่ฒๆฐดๆๆ็ๅฐๅฅณๅจๅคง้จไธญๅฅ่ท๏ผ่กจๆ
ๆคๆใๆตๆณช๏ผ่กฃๆๆนฟ้ใ่ฝฌๆขๆ Danbooru ๆ ็ญพ prompt
|
| 420 |
+
|
| 421 |
+
> ๆๆณๆพใๆบๆขฐไน่ขใ็ธๅ
ณ็ๆ ็ญพ๏ผๅธฎๆๅๆฃไธไธ
|
| 422 |
+
|
| 423 |
+
> ๅธฎๆๆฅไธไธใๅฝ่ฟ็ณไน้จ็ๅฉๆใๅฏนๅบ็่ง่ฒๆ ็ญพๆฏไปไน
|
| 424 |
+
|
| 425 |
+
> ๆๅทฒ็ป้ไบ `white_serafuku` ๅ `rain` ่ฟไธคไธชๆ ็ญพ๏ผๅธฎๆๆจ่ไธไบๅธธ่ง็ๆญ้
ๆ ็ญพ
|
| 426 |
+
|
| 427 |
+
> ๆฅไธไธใ่ฟๅจ็คพๅข ๆ ก้ ๆฏ่ต ่ฟๅจไผ ไฝ่ฒ็ฅญ ็ฐๅพ้จ ็ฝ็้จ ็ฏฎ็้จใ่ฟไบๆฆๅฟตๅฏนๅบ็ๆ ็ญพ
|
| 428 |
+
|
| 429 |
+
> ๆพใๅไนๆ้ฆใ่ตๅๆๅ
ใ่ธๆฑฝๆๅ
ใ็ General ็ฑปๆ ็ญพ๏ผๅชๆฅ้็จ็นๅพไธๆฅไฝๅๅ
|
| 430 |
+
|
| 431 |
+
### ๆฅ่ฏขๅไฝๆๅทง๏ผๅ็ป LLM๏ผ
|
| 432 |
+
|
| 433 |
+
ไธบไบ่ฎฉๆ็ดขๆด็ฒพๅ๏ผๅจๆ้ `query` ๆถ่ฏทๆณจๆไปฅไธๅ ็น๏ผ
|
| 434 |
+
|
| 435 |
+
1. **ๆฆๅฟตๅ่กจ็จๆพๅผๅ้็ฌฆ๏ผ** ้่ฆๆน้ๆฅ่ฏขๅคไธช็ฌ็ซๆฆๅฟตๆถ๏ผ็จ็ฉบๆ ผใไธญๆ้ๅท๏ผ๏ผ๏ผๆ้กฟๅท๏ผใ๏ผๅ้ใๆฏไธช โค7 ๅญ็็ๆฎตไผไฝไธบๅฎๆดๆฆๅฟตๅน้
๏ผไธไผ่ขซ้่ฏฏๆๅผใไพๅฆ `่ฟๅจ็คพๅข ๆ ก้ ๆฏ่ต` ่้ `่ฟๅจ็คพๅขๆ ก้ๆฏ่ต`ใ
|
| 436 |
+
|
| 437 |
+
2. **็ป้ขๆ่ฟฐๅ่ช็ถ่ฏญๅฅ๏ผ** ๅฎๆดๅบๆฏๆ่ฟฐไธ้่ฆๆพๅผๅ้๏ผ็ณป็ปไผ่ชๅจๅ่ฏใไพๅฆ `ไธไธช็ฉฟ็็ฝ่ฒๆฐดๆๆ่่ฒ็ญ่ฃ็ๅฐๅฅณๅจ้จไธญๅฅ่ท`ใ
|
| 438 |
+
|
| 439 |
+
3. **ๆฅ่ง่ฒ/ไฝๅๆถๅ
ณ้ญๅ่ฏ๏ผ** ็ฒพ็กฎๆฅๆพๆไธช่ง่ฒๅๆไฝๅๅๆถ๏ผ่ฎพ `use_segmentation=false` ๅ `category="character"`๏ผๆ `"copyright"`๏ผ๏ผ้ฟๅ
ๅๅญไธญ็่ฏๆฑ่ขซๅ็ฌๅน้
ๅฐๆ ๅ
ณๆ ็ญพใ
|
| 440 |
+
|
| 441 |
+
4. **้ฟ็ญ่ฏญไผ่ชๅจไฟๆๅฎๆด่ฏญไน๏ผ** ๅณไฝฟ็จๆพๅผๅ้็ฌฆๅๅบ็็ๆฎต่ถ
่ฟ 7 ๅญ๏ผ็ณป็ปไนไผๅฏนๅ
ถ่ชๅจๅ่ฏๅนถๅๆถไฟ็ๅๅง็ๆฎตไฝไธบ้ขๅคๆฅ่ฏข๏ผไปๅฅ็บง่ฏญไน๏ผ๏ผไธไผไธขๅคฑๆดไฝ่ฏญๅขใ
|
| 442 |
+
|
| 443 |
+
### ๆณจๆไบ้กน
|
| 444 |
+
|
| 445 |
+
HF Space ๅจๆ ๆต้ๆถไผ่ฟๅ
ฅไผ็ ็ถๆ๏ผ้ฆๆฌก่ฏทๆฑ้่ฆ็ญๅพ
ๅทๅฏๅจ๏ผ็บฆ 30~60 ็ง๏ผใ่ฅ AI ่ฐ็จ่ถ
ๆถ๏ผ็จ็ญ็ๅปๅ้่ฏๅณๅฏ๏ผๆๅ
่ฎฟ้ฎ [Space ้กต้ข](https://huggingface.co/spaces/SAkizuki/DanbooruSearch) ๅฐๅ
ถๅค้ใ
|
| 446 |
+
|
| 447 |
+
---
|
| 448 |
+
|
| 449 |
+
## ๆฐๆฎๅบ่ฏดๆ
|
| 450 |
+
|
| 451 |
+
- ๆฐๆฎๆฅๆบ๏ผDanbooru API ๆๅ
|
| 452 |
+
- ไธญๆ็ฟป่ฏไธ่ฏญไนๆฉๅ
๏ผ็ฑ LLM ่พ
ๅฉๅฎๆ
|
| 453 |
+
- ่ง่ฒๅใไฝๅๅ๏ผ้่ฟไบๆฌกๅ
ๅ็ฑปๆฐๆฎๅบ [Bangumi](https://bgm.tv) API ็ฒพ็กฎๆฅ่ฏข๏ผๅฐฝ้้ฟๅ
ๅนป่ง
|
| 454 |
+
- ไป
ๆถๅฝ Danbooru ้ขๆฐ **โฅ 100** ็ๆ ็ญพ๏ผไฟ่ฏๆ ็ญพๅฎ้
ๅฏ็จ
|
| 455 |
+
- ไป
่ฆ็ General๏ผ้็จ็นๅพ๏ผใCharacter๏ผ่ง่ฒ๏ผใCopyright๏ผไฝๅ๏ผไธ็ฑปๆ ็ญพ
|
| 456 |
+
|
| 457 |
+
**ๆฐๆฎๅบ็ๆไปฃ็ ๏ผ** https://github.com/SuzumiyaAkizuki/danbooru-tag-pipeline
|
| 458 |
+
|
| 459 |
+
---
|
| 460 |
+
|
| 461 |
+
## ๆณจๆไบ้กน
|
| 462 |
+
|
| 463 |
+
- ๆฌๅทฅๅ
ทไธบ AI ่พ
ๅฉๅทฅๅ
ท๏ผๆฃ็ดข็ปๆๆชๅฟ
100% ๅ็กฎ
|
| 464 |
+
- ๆฅๆพ็ปๆๅฏ่ฝๅ
ๅซ NSFW ๅ
ๅฎน๏ผ้ป่ฎค้่๏ผ
|
| 465 |
+
- ไป
ๆฏๆไธญๆ / ่ฑๆๅ่ฏญๆฅๆพ
|
| 466 |
+
- ๅฆๆๆฃ็ดข็ปๆไธ็ๆณ๏ผๆฌข่ฟ็นๅปใๆฒกๆๅฐ๏ผใๆ้ฎๆไบคๅ้ฆ๏ผๅธฎๅฉๆ็ปญไผๅๅผๆ
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
## ๆ็ดข็ป่ฎก
|
| 471 |
+
|
| 472 |
+

|
api_fastapi.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
api_fastapi.py
|
| 3 |
+
โโโโโโโโโโโโโโ
|
| 4 |
+
FastAPI ้้
ๅฑ๏ผๅฏ้๏ผใ
|
| 5 |
+
|
| 6 |
+
ๆผ็คบๅฆไฝๅจๅฎๅ
จไธไฟฎๆน core/ ็ๆ
ๅตไธๅฐๅผๆ API ๅใ
|
| 7 |
+
|
| 8 |
+
ๅฏๅจๆนๅผ๏ผ
|
| 9 |
+
uvicorn api_fastapi:app --host 0.0.0.0 --port 8000
|
| 10 |
+
|
| 11 |
+
่ฏทๆฑ็คบไพ๏ผ
|
| 12 |
+
POST /search
|
| 13 |
+
{
|
| 14 |
+
"query": "็ฝ่ฒๆฐดๆๆ็ๅฅณๅญฉ",
|
| 15 |
+
"top_k": 5,
|
| 16 |
+
"limit": 20
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
POST /related
|
| 20 |
+
{
|
| 21 |
+
"tags": ["white_serafuku", "sailor_collar"],
|
| 22 |
+
"limit": 20,
|
| 23 |
+
"show_nsfw": false
|
| 24 |
+
}
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import asyncio
|
| 30 |
+
from fastapi import FastAPI
|
| 31 |
+
from pydantic import BaseModel, Field
|
| 32 |
+
|
| 33 |
+
from core.engine import DanbooruTagger
|
| 34 |
+
from core.models import SearchRequest, SearchResponse, TagResult, RelatedTag
|
| 35 |
+
import core.counter as counter
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# โโ Pydantic I/O ๆจกๅ๏ผAPI ๅฑไธ็จ๏ผไธ core.models ่งฃ่ฆ๏ผโโ
|
| 39 |
+
|
| 40 |
+
class SearchIn(BaseModel):
|
| 41 |
+
query: str
|
| 42 |
+
top_k: int = Field(5, ge=1, le=50)
|
| 43 |
+
limit: int = Field(80, ge=1, le=500)
|
| 44 |
+
popularity_weight: float = Field(0.15, ge=0.0, le=1.0)
|
| 45 |
+
show_nsfw: bool = True
|
| 46 |
+
use_segmentation: bool = True
|
| 47 |
+
target_layers: list[str] = ['่ฑๆ', 'ไธญๆๆฉๅฑ่ฏ', '้ไน', 'ไธญๆๆ ธๅฟ่ฏ']
|
| 48 |
+
target_categories: list[str] = ['General', 'Character', 'Copyright']
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class TagOut(BaseModel):
|
| 52 |
+
tag: str
|
| 53 |
+
cn_name: str
|
| 54 |
+
category: str
|
| 55 |
+
nsfw: str
|
| 56 |
+
final_score: float
|
| 57 |
+
semantic_score: float
|
| 58 |
+
count: int
|
| 59 |
+
source: str
|
| 60 |
+
layer: str
|
| 61 |
+
wiki: str = ""
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class RelatedIn(BaseModel):
|
| 65 |
+
tags: list[str]
|
| 66 |
+
limit: int = Field(50, ge=1, le=200)
|
| 67 |
+
show_nsfw: bool = True
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class RelatedTagOut(BaseModel):
|
| 71 |
+
tag: str
|
| 72 |
+
cn_name: str
|
| 73 |
+
category: str
|
| 74 |
+
nsfw: str
|
| 75 |
+
cooc_count: int
|
| 76 |
+
cooc_score: float
|
| 77 |
+
sources: list[str]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class SearchOut(BaseModel):
|
| 81 |
+
tags_all: str
|
| 82 |
+
tags_sfw: str
|
| 83 |
+
results: list[TagOut]
|
| 84 |
+
keywords: list[str]
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# โโ FastAPI ๅญๅบ็จ๏ผๆ่ฝฝๅฐ NiceGUI ็ /api ่ทฏๅพไธ๏ผโโ
|
| 88 |
+
# lifespan / ้ข็ญ็ฑ ui_nicegui.py ็ @app.on_startup ็ปไธ็ฎก็๏ผๆญคๅคไธ้ๅคใ
|
| 89 |
+
app = FastAPI(
|
| 90 |
+
title="Danbooru Tag Searcher API",
|
| 91 |
+
description="้่ฟ /api/docs ๆฅ็ๅฎๆดๆฅๅฃๆๆกฃใ",
|
| 92 |
+
version="1.0.0",
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# โโ ็ซฏ็น โโ
|
| 97 |
+
|
| 98 |
+
@app.post("/search", response_model=SearchOut)
|
| 99 |
+
async def search(body: SearchIn) -> SearchOut:
|
| 100 |
+
tagger = await DanbooruTagger.get_instance()
|
| 101 |
+
|
| 102 |
+
# SearchIn โ core.models.SearchRequest๏ผไธค่
ๅญๆฎตไธไธๅฏนๅบ๏ผ็ดๆฅ่งฃๅ
๏ผ
|
| 103 |
+
request = SearchRequest(**body.model_dump())
|
| 104 |
+
|
| 105 |
+
# ๅจ็บฟ็จๆฑ ไธญ่ฟ่ก้ปๅก็ search()
|
| 106 |
+
response: SearchResponse = await asyncio.to_thread(tagger.search, request)
|
| 107 |
+
|
| 108 |
+
# ่ฎกๆฐ๏ผๆฏๆฌก API ๆ็ดข่ฐ็จๅ่ฎกๅ
ฅๆ็ดขใๆๅใๅคๅถ๏ผ่ฎฟ้ฎไธๅ
|
| 109 |
+
await counter.increment()
|
| 110 |
+
await counter.increment_success()
|
| 111 |
+
await counter.increment_copy()
|
| 112 |
+
|
| 113 |
+
return SearchOut(
|
| 114 |
+
tags_all=response.tags_all,
|
| 115 |
+
tags_sfw=response.tags_sfw,
|
| 116 |
+
results=[TagOut(**vars(r)) for r in response.results],
|
| 117 |
+
keywords=response.keywords,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@app.post("/related", response_model=list[RelatedTagOut])
|
| 122 |
+
async def related(body: RelatedIn) -> list[RelatedTagOut]:
|
| 123 |
+
"""
|
| 124 |
+
็ปๅฎๅทฒ้ๆ ็ญพๅ่กจ๏ผ่ฟๅๅบไบๅ
ฑ็ฐ่กจ็ๅ
ณ่ๆจ่ใ
|
| 125 |
+
|
| 126 |
+
- tags๏ผ็งๅญๆ ็ญพๅ่กจ๏ผDanbooru ่ฑๆๆ ็ญพๅ๏ผ
|
| 127 |
+
- limit๏ผๆๅค่ฟๅๆกๆฐ๏ผ้ป่ฎค 50
|
| 128 |
+
- show_nsfw๏ผๆฏๅฆๅ
ๅซ NSFW ๆ ็ญพ๏ผ้ป่ฎค True
|
| 129 |
+
"""
|
| 130 |
+
tagger = await DanbooruTagger.get_instance()
|
| 131 |
+
results = await asyncio.to_thread(
|
| 132 |
+
tagger.get_related,
|
| 133 |
+
body.tags,
|
| 134 |
+
set(body.tags), # exclude ๅทฒ้ๆ ็ญพ่ช่บซ
|
| 135 |
+
body.limit,
|
| 136 |
+
body.show_nsfw,
|
| 137 |
+
)
|
| 138 |
+
# ่ฎกๆฐ๏ผๆฏๆฌก API related ่ฐ็จๅ่ฎกๅ
ฅๆ็ดขใๆๅใๅคๅถ๏ผ่ฎฟ้ฎไธๅ
|
| 139 |
+
await counter.increment()
|
| 140 |
+
await counter.increment_success()
|
| 141 |
+
await counter.increment_copy()
|
| 142 |
+
|
| 143 |
+
return [
|
| 144 |
+
RelatedTagOut(
|
| 145 |
+
tag=r.tag,
|
| 146 |
+
cn_name=r.cn_name,
|
| 147 |
+
category=r.category,
|
| 148 |
+
nsfw=r.nsfw,
|
| 149 |
+
cooc_count=r.cooc_count,
|
| 150 |
+
cooc_score=r.cooc_score,
|
| 151 |
+
sources=r.sources,
|
| 152 |
+
)
|
| 153 |
+
for r in results
|
| 154 |
+
]
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@app.get("/health")
|
| 158 |
+
async def health():
|
| 159 |
+
tagger = await DanbooruTagger.get_instance()
|
| 160 |
+
return {"status": "ok", "loaded": tagger.is_loaded}
|
cli.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
cli.py
|
| 3 |
+
โโโโโโ
|
| 4 |
+
ๅฝไปค่ก้้
ๅฑ๏ผๅฏ้๏ผใ
|
| 5 |
+
|
| 6 |
+
ๅญๅฝไปค๏ผ
|
| 7 |
+
search ่ฏญไนๆ็ดขๆ ็ญพ
|
| 8 |
+
related ๅบไบๅ
ฑ็ฐ่กจๆฅๅ
ณ่ๆจ่
|
| 9 |
+
|
| 10 |
+
็จๆณ๏ผ
|
| 11 |
+
python cli.py search "็ฝ่ฒๆฐดๆๆ็ๅฅณๅญฉ" --limit 10 --no-nsfw
|
| 12 |
+
python cli.py related "white_serafuku,sailor_collar" --limit 20
|
| 13 |
+
python cli.py related "white_serafuku,sailor_collar" --no-nsfw --show-sources
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import asyncio
|
| 18 |
+
|
| 19 |
+
from core.engine import DanbooruTagger
|
| 20 |
+
from core.models import SearchRequest
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# โโ search โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 24 |
+
|
| 25 |
+
async def cmd_search(args):
|
| 26 |
+
tagger = await DanbooruTagger.get_instance()
|
| 27 |
+
request = SearchRequest(
|
| 28 |
+
query=args.query,
|
| 29 |
+
top_k=args.top_k,
|
| 30 |
+
limit=args.limit,
|
| 31 |
+
popularity_weight=args.weight,
|
| 32 |
+
show_nsfw=not args.no_nsfw,
|
| 33 |
+
use_segmentation=not args.no_seg,
|
| 34 |
+
target_layers=args.layers,
|
| 35 |
+
target_categories=args.categories,
|
| 36 |
+
)
|
| 37 |
+
resp = await asyncio.to_thread(tagger.search, request)
|
| 38 |
+
|
| 39 |
+
print(f"\n{'='*60}")
|
| 40 |
+
print(f"ๆฅ่ฏข๏ผ{args.query} | ๅ
ฑ {len(resp.results)} ๆก็ปๆ")
|
| 41 |
+
print(f"{'='*60}")
|
| 42 |
+
print(f"ๆจ่ Prompt๏ผ\n {resp.tags_sfw if args.no_nsfw else resp.tags_all}\n")
|
| 43 |
+
|
| 44 |
+
for r in resp.results:
|
| 45 |
+
nsfw_mark = "๐ด" if r.nsfw == '1' else "๐ข"
|
| 46 |
+
print(f" {nsfw_mark} [{r.final_score:.3f}] {r.tag:<30} {r.cn_name[:20]:<20} {r.category}")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# โโ related โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 50 |
+
|
| 51 |
+
async def cmd_related(args):
|
| 52 |
+
seed_tags = [t.strip() for t in args.tags.split(',') if t.strip()]
|
| 53 |
+
if not seed_tags:
|
| 54 |
+
print("[CLI] ้่ฏฏ๏ผ่ฏทๆไพ่ณๅฐไธไธช็งๅญๆ ็ญพ๏ผๅคไธชๆ ็ญพไปฅ้ๅทๅ้ใ")
|
| 55 |
+
return
|
| 56 |
+
|
| 57 |
+
tagger = await DanbooruTagger.get_instance()
|
| 58 |
+
results = await asyncio.to_thread(
|
| 59 |
+
tagger.get_related,
|
| 60 |
+
seed_tags,
|
| 61 |
+
set(seed_tags), # exclude ็งๅญๆ ็ญพ่ช่บซ
|
| 62 |
+
args.limit,
|
| 63 |
+
not args.no_nsfw,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
if not results:
|
| 67 |
+
print("[CLI] ๆชๆพๅฐๅ
ณ่ๆจ่๏ผๅ
ฑ็ฐ่กจๅฏ่ฝๆชๅ ่ฝฝ๏ผๆ็งๅญๆ ็ญพไธๅจๅบไธญ๏ผใ")
|
| 68 |
+
return
|
| 69 |
+
|
| 70 |
+
print(f"\n{'='*60}")
|
| 71 |
+
print(f"็งๅญๆ ็ญพ๏ผ{', '.join(seed_tags)} | ๅ
ฑ {len(results)} ๆกๆจ่")
|
| 72 |
+
print(f"{'='*60}")
|
| 73 |
+
|
| 74 |
+
# ่พๅบ้ๅทๅ้็ๆ ็ญพไธฒ๏ผๆนไพฟ็ดๆฅๅคๅถไฝฟ็จ๏ผ
|
| 75 |
+
tag_list = [r.tag for r in results if not (r.nsfw == '1' and args.no_nsfw)]
|
| 76 |
+
print(f"ๆจ่ๆ ็ญพ๏ผ\n {', '.join(tag_list)}\n")
|
| 77 |
+
|
| 78 |
+
# ๆ็ป่กจ
|
| 79 |
+
for r in results:
|
| 80 |
+
if r.nsfw == '1' and args.no_nsfw:
|
| 81 |
+
continue
|
| 82 |
+
nsfw_mark = "๐ด" if r.nsfw == '1' else "๐ข"
|
| 83 |
+
sources_str = f" โ {', '.join(r.sources)}" if args.show_sources else ""
|
| 84 |
+
print(
|
| 85 |
+
f" {nsfw_mark} [{r.cooc_score:.3f}] {r.tag:<30}"
|
| 86 |
+
f" {r.cn_name[:20]:<20} {r.category}"
|
| 87 |
+
f" (ๅ
ฑ็ฐ:{r.cooc_count:,}){sources_str}"
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
# โโ ๅ
ฅๅฃ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 92 |
+
|
| 93 |
+
async def main():
|
| 94 |
+
parser = argparse.ArgumentParser(
|
| 95 |
+
description="Danbooru Tag CLI Searcher",
|
| 96 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 97 |
+
epilog="""
|
| 98 |
+
็คบไพ๏ผ
|
| 99 |
+
python cli.py search "็ฝ่ฒๆฐดๆๆ็ๅฅณๅญฉ" --limit 10
|
| 100 |
+
python cli.py related "white_serafuku,sailor_collar"
|
| 101 |
+
python cli.py related "white_serafuku" --limit 30 --no-nsfw --show-sources
|
| 102 |
+
""",
|
| 103 |
+
)
|
| 104 |
+
sub = parser.add_subparsers(dest='cmd', required=True)
|
| 105 |
+
|
| 106 |
+
_all_layers = ['่ฑๆ', 'ไธญๆๆฉๅฑ่ฏ', '้ไน', 'ไธญๆๆ ธๅฟ่ฏ']
|
| 107 |
+
_all_cats = ['General', 'Character', 'Copyright']
|
| 108 |
+
|
| 109 |
+
# โโ search ๅญๅฝไปค โโ
|
| 110 |
+
p_search = sub.add_parser('search', help='่ฏญไนๆ็ดขๆ ็ญพ')
|
| 111 |
+
p_search.add_argument('query', help='ๆ็ดข่ฏ๏ผๆฏๆไธญ่ฑๆ่ช็ถ่ฏญ่จ๏ผ')
|
| 112 |
+
p_search.add_argument('--top-k', type=int, default=5, help='ๆฏๅฑ่ฟๅๆฐ้๏ผ้ป่ฎค 5๏ผ')
|
| 113 |
+
p_search.add_argument('--limit', type=int, default=80, help='็ปๆไธ้๏ผ้ป่ฎค 80๏ผ')
|
| 114 |
+
p_search.add_argument('--weight', type=float, default=0.15, help='็ญๅบฆๆ้๏ผ้ป่ฎค 0.15๏ผ')
|
| 115 |
+
p_search.add_argument('--no-nsfw', action='store_true', help='่ฟๆปค NSFW ๅ
ๅฎน')
|
| 116 |
+
p_search.add_argument('--no-seg', action='store_true', help='็ฆ็จๆบ่ฝๅ่ฏ')
|
| 117 |
+
p_search.add_argument(
|
| 118 |
+
'--layers', nargs='+', default=_all_layers,
|
| 119 |
+
metavar='LAYER',
|
| 120 |
+
help=f'ๅน้
ๅฑ็ญ้๏ผๅฏๅค้๏ผ็จ็ฉบๆ ผๅ้๏ผ้ป่ฎคๅ
จ้จ๏ผใๅฏ้ๅผ๏ผ{_all_layers}',
|
| 121 |
+
)
|
| 122 |
+
p_search.add_argument(
|
| 123 |
+
'--categories', nargs='+', default=_all_cats,
|
| 124 |
+
metavar='CAT',
|
| 125 |
+
help=f'ๆ ็ญพ็ฑปๅ็ญ้๏ผๅฏๅค้๏ผ็จ็ฉบๆ ผๅ้๏ผ้ป่ฎคๅ
จ้จ๏ผใๅฏ้ๅผ๏ผ{_all_cats}',
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
# โโ related ๅญๅฝไปค โโ
|
| 129 |
+
p_related = sub.add_parser('related', help='ๅบไบๅ
ฑ็ฐ่กจๆฅๅ
ณ่ๆจ่')
|
| 130 |
+
p_related.add_argument('tags', help='็งๅญๆ ็ญพ๏ผไปฅ่ฑๆ้ๅทๅ้๏ผๅฆ white_serafuku,sailor_collar๏ผ')
|
| 131 |
+
p_related.add_argument('--limit', type=int, default=50, help='ๆจ่็ปๆไธ้๏ผ้ป่ฎค 50๏ผ')
|
| 132 |
+
p_related.add_argument('--no-nsfw', action='store_true', help='่ฟๆปค NSFW ๅ
ๅฎน')
|
| 133 |
+
p_related.add_argument('--show-sources', action='store_true', help='ๆพ็คบๆฏๆกๆจ่็ฑๅชไธช็งๅญ่งฆๅ')
|
| 134 |
+
|
| 135 |
+
args = parser.parse_args()
|
| 136 |
+
if args.cmd == 'search':
|
| 137 |
+
await cmd_search(args)
|
| 138 |
+
elif args.cmd == 'related':
|
| 139 |
+
await cmd_related(args)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
if __name__ == "__main__":
|
| 143 |
+
asyncio.run(main())
|
core/counter.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
core/counter.py
|
| 3 |
+
โโโโโโโโโโโโโโ
|
| 4 |
+
ๆ็ฎๆไน
ๅ่ฎกๆฐๅจ๏ผๆ ธๅฟไธๅกๆๆ ๏ผๆปๆ็ดขใๆป่ฎฟ้ฎใๅคๅถๆฌกๆฐใๆๅไบคไบใ่ฏ้ข่ๅใbad_case๏ผใ
|
| 5 |
+
|
| 6 |
+
ๆฐๆฎๆไน
ๅๅฐ้ฟ้ไบ OSS๏ผ้่ฟ platform_utils ๅฑ่ฝ็ป่ใ
|
| 7 |
+
ๆ ไปปไฝๆฌๅฐ่ฝ็ๆไฝใ
|
| 8 |
+
|
| 9 |
+
count.json ็ปๆ๏ผ
|
| 10 |
+
{
|
| 11 |
+
"total": int,
|
| 12 |
+
"visits": int,
|
| 13 |
+
"copies": int,
|
| 14 |
+
"successes": int,
|
| 15 |
+
"hot_keywords": {word: count, ...},
|
| 16 |
+
"bad_cases": [{q, t}, ...],
|
| 17 |
+
"history": [{"date": "YYYY-MM-DD", "total": int}, ...] # ๆฏๆฅๅฟซ็
ง๏ผๆ่ฟ 180 ๅคฉ
|
| 18 |
+
}
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import asyncio
|
| 24 |
+
import json
|
| 25 |
+
import time
|
| 26 |
+
from collections import Counter
|
| 27 |
+
from datetime import datetime, timezone, timedelta, date
|
| 28 |
+
from typing import Optional
|
| 29 |
+
|
| 30 |
+
from platform_utils import get_counter_cfg, read_bytes, upload_bytes, CounterConfig
|
| 31 |
+
|
| 32 |
+
# โโ ๆ ธๅฟ็ถๆๅ้ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 33 |
+
_memory_count: int = 0
|
| 34 |
+
_dirty_count: int = 0
|
| 35 |
+
|
| 36 |
+
_memory_visits: int = 0
|
| 37 |
+
_dirty_visits: int = 0
|
| 38 |
+
BASE_VISITS: int = 0
|
| 39 |
+
|
| 40 |
+
_memory_copies: int = 0
|
| 41 |
+
_dirty_copies: int = 0
|
| 42 |
+
BASE_COPIES: int = 0
|
| 43 |
+
|
| 44 |
+
_memory_mcp: int = 0
|
| 45 |
+
_dirty_mcp: int = 0
|
| 46 |
+
|
| 47 |
+
_memory_successes: int = 0
|
| 48 |
+
_dirty_successes: int = 0
|
| 49 |
+
|
| 50 |
+
_memory_keywords: Counter = Counter()
|
| 51 |
+
_dirty_keywords: Counter = Counter()
|
| 52 |
+
MAX_KEYWORDS_LIMIT = 200
|
| 53 |
+
|
| 54 |
+
_memory_bad_cases: list[dict] = []
|
| 55 |
+
_dirty_bad_cases: list[dict] = []
|
| 56 |
+
MAX_BAD_CASES = 50
|
| 57 |
+
|
| 58 |
+
_memory_history: list[dict] = [] # [{"date": "YYYY-MM-DD", "total": int}, ...]
|
| 59 |
+
MAX_HISTORY_DAYS = 180
|
| 60 |
+
|
| 61 |
+
_last_sync: float = 0.0
|
| 62 |
+
_sync_lock: Optional[asyncio.Lock] = None
|
| 63 |
+
|
| 64 |
+
SYNC_INTERVAL = 1800 # ๆฏ 30 ๅ้ๅๆญฅไธๆฌก
|
| 65 |
+
SYNC_THRESHOLD = 500 # ๆๅ้กนๅข้ไนๅ่พพๅฐ 200 ๆฌกๆถๅๆญฅ
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _get_sync_lock() -> asyncio.Lock:
|
| 69 |
+
global _sync_lock
|
| 70 |
+
if _sync_lock is None:
|
| 71 |
+
_sync_lock = asyncio.Lock()
|
| 72 |
+
return _sync_lock
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# โโ ๅๅฒๅฟซ็
งๅทฅๅ
ท โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 76 |
+
|
| 77 |
+
def _today_str() -> str:
|
| 78 |
+
return datetime.now(timezone(timedelta(hours=8))).date().isoformat()
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _merge_history(
|
| 82 |
+
remote: list[dict],
|
| 83 |
+
current_total: int,
|
| 84 |
+
memory: list[dict] | None = None,
|
| 85 |
+
) -> list[dict]:
|
| 86 |
+
"""
|
| 87 |
+
ๅฐๅฝๅคฉ็ total ๅๅ
ฅ๏ผๆๆดๆฐ๏ผๅๅฒๅ่กจ๏ผไฟ็ๆ่ฟ MAX_HISTORY_DAYS ๅคฉใ
|
| 88 |
+
ๅไธๅคฉๅชไฟ็ๆๆฐไธๆก๏ผtotal ๅ่พๅคงๅผ๏ผใ
|
| 89 |
+
|
| 90 |
+
remote : ไป OSS ่ฏปๅ็ๅๅฒ๏ผๅฏ่ฝๅ ็ผๅญๅฏผ่ดไธๅฎๆด๏ผ
|
| 91 |
+
memory : ๅ
ๅญไธญๅทฒๅ ่ฝฝ็ๅๅฒ๏ผinit() ๆถไป OSS ๅฎๆด่ฏปๅ๏ผ็จไบๅ
ๅบ๏ผ
|
| 92 |
+
"""
|
| 93 |
+
today = _today_str()
|
| 94 |
+
by_date: dict[str, int] = {}
|
| 95 |
+
# ๅ
็จๅ
ๅญๆฐๆฎๆๅบ๏ผๅ็จ่ฟ็ซฏๆฐๆฎ่ฆ็๏ผ่ฟ็ซฏๆฏๆ็ปๅๅ
ฅๆ๏ผไผๅ
็บงๆด้ซ๏ผ
|
| 96 |
+
for r in (memory or []):
|
| 97 |
+
by_date[r['date']] = r['total']
|
| 98 |
+
for r in remote:
|
| 99 |
+
by_date[r['date']] = max(by_date.get(r['date'], 0), r['total'])
|
| 100 |
+
# ๅฝๅคฉ่ฎฐๅฝๅ่พๅคงๅผ๏ผ้ฟๅ
ๅๆญฅๆถๅบ้ฎ้ขๅฏผ่ดๆฐๆฎๅ้
|
| 101 |
+
by_date[today] = max(by_date.get(today, 0), current_total)
|
| 102 |
+
|
| 103 |
+
sorted_records = sorted(by_date.items(), reverse=True)[:MAX_HISTORY_DAYS]
|
| 104 |
+
# ่ฟๅๆถๆๆถ้ดๆญฃๅบ๏ผๆนไพฟ็ปๅพ
|
| 105 |
+
return [{'date': d, 'total': t} for d, t in sorted(sorted_records)]
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# โโ ่ฟ็ซฏ IO โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 109 |
+
|
| 110 |
+
_COUNTER_FILE = 'count.json'
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _parse_remote_data(raw: bytes | None) -> tuple[int, int, int, int, int, dict, list, list]:
|
| 114 |
+
"""่งฃๆ่ฟ็ซฏ JSON bytes๏ผ่ฟๅ (total, visits, copies, mcp, successes, keywords, bad_cases, history)ใ"""
|
| 115 |
+
if raw is None:
|
| 116 |
+
return 0, BASE_VISITS, BASE_COPIES, 0, 0, {}, [], []
|
| 117 |
+
try:
|
| 118 |
+
data = json.loads(raw.decode('utf-8'))
|
| 119 |
+
r_total = int(data.get('total', 0))
|
| 120 |
+
r_visits = int(data.get('visits', BASE_VISITS))
|
| 121 |
+
r_copies = int(data.get('copies', BASE_COPIES))
|
| 122 |
+
r_mcp = int(data.get('mcp', 0))
|
| 123 |
+
r_successes = int(data.get('successes', int(r_total * 0.75)))
|
| 124 |
+
r_keywords = data.get('hot_keywords', {})
|
| 125 |
+
r_bad_cases = data.get('bad_cases', [])
|
| 126 |
+
r_history = data.get('history', [])
|
| 127 |
+
return r_total, r_visits, r_copies, r_mcp, r_successes, r_keywords, r_bad_cases, r_history
|
| 128 |
+
except Exception:
|
| 129 |
+
return 0, BASE_VISITS, BASE_COPIES, 0, 0, {}, [], []
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _read_remote() -> tuple[int, int, int, int, int, dict, list, list]:
|
| 133 |
+
cfg = get_counter_cfg()
|
| 134 |
+
if not cfg.available:
|
| 135 |
+
return 0, BASE_VISITS, BASE_COPIES, 0, 0, {}, [], []
|
| 136 |
+
|
| 137 |
+
try:
|
| 138 |
+
raw = read_bytes(_COUNTER_FILE, cfg)
|
| 139 |
+
except Exception as e:
|
| 140 |
+
print(f'[Counter] ๅฏๅจ่ฏปๅ่ฟ็ซฏๆฐๆฎๅผๅธธ: {e}')
|
| 141 |
+
return 0, BASE_VISITS, BASE_COPIES, 0, 0, {}, [], []
|
| 142 |
+
|
| 143 |
+
if raw is None:
|
| 144 |
+
return 0, BASE_VISITS, BASE_COPIES, 0, 0, {}, [], []
|
| 145 |
+
return _parse_remote_data(raw)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def _sync_remote_task(
|
| 149 |
+
adds_count: int,
|
| 150 |
+
adds_visits: int,
|
| 151 |
+
adds_copies: int,
|
| 152 |
+
adds_mcp: int,
|
| 153 |
+
adds_successes: int,
|
| 154 |
+
adds_keywords: dict,
|
| 155 |
+
adds_bad_cases: list,
|
| 156 |
+
memory_history_snapshot: list,
|
| 157 |
+
) -> tuple[bool, int, int, int, int, int, dict, list, list]:
|
| 158 |
+
cfg: CounterConfig = get_counter_cfg()
|
| 159 |
+
if not cfg.available:
|
| 160 |
+
return False, 0, 0, 0, 0, 0, {}, [], []
|
| 161 |
+
|
| 162 |
+
try:
|
| 163 |
+
raw = read_bytes(_COUNTER_FILE, cfg)
|
| 164 |
+
except Exception as e:
|
| 165 |
+
print(f'[Counter] ่ฟ็ซฏ่ฏปๅๅผๅธธ๏ผไธญๆญขๆฌๆฌกๅๆญฅไปฅไฟๆคๆฐๆฎ: {e}')
|
| 166 |
+
return False, 0, 0, 0, 0, 0, {}, [], []
|
| 167 |
+
|
| 168 |
+
r_total, r_visits, r_copies, r_mcp, r_successes, r_keywords, r_bad_cases, r_history = (
|
| 169 |
+
_parse_remote_data(raw)
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
n_total = r_total + adds_count
|
| 173 |
+
n_visits = r_visits + adds_visits
|
| 174 |
+
n_copies = r_copies + adds_copies
|
| 175 |
+
n_mcp = r_mcp + adds_mcp
|
| 176 |
+
n_successes = r_successes + adds_successes
|
| 177 |
+
|
| 178 |
+
merged_keywords = Counter(r_keywords)
|
| 179 |
+
for word, count in adds_keywords.items():
|
| 180 |
+
merged_keywords[word] += count
|
| 181 |
+
top_keywords = dict(merged_keywords.most_common(MAX_KEYWORDS_LIMIT))
|
| 182 |
+
|
| 183 |
+
merged_bad_cases = (adds_bad_cases + r_bad_cases)[:MAX_BAD_CASES]
|
| 184 |
+
|
| 185 |
+
n_history = _merge_history(r_history, n_total, memory=memory_history_snapshot)
|
| 186 |
+
|
| 187 |
+
content = json.dumps({
|
| 188 |
+
'total': n_total,
|
| 189 |
+
'visits': n_visits,
|
| 190 |
+
'copies': n_copies,
|
| 191 |
+
'mcp': n_mcp,
|
| 192 |
+
'successes': n_successes,
|
| 193 |
+
'hot_keywords': top_keywords,
|
| 194 |
+
'bad_cases': merged_bad_cases,
|
| 195 |
+
'history': n_history,
|
| 196 |
+
}, ensure_ascii=False, indent=2).encode('utf-8')
|
| 197 |
+
|
| 198 |
+
commit_msg = (
|
| 199 |
+
f'Sync: ๆ็ดข:{n_total} | MCP:{n_mcp} | ๆๅ:{n_successes} | '
|
| 200 |
+
f'ๅคๅถ:{n_copies} | ่ฎฟ้ฎ:{n_visits} | bad_cases:{len(merged_bad_cases)}'
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
ok = upload_bytes(content, _COUNTER_FILE, cfg, commit_msg, retries=3, retry_delay=1.0)
|
| 204 |
+
if ok:
|
| 205 |
+
print(
|
| 206 |
+
f'[Counter] ๅๆญฅๆๅ๏ผๆ็ดข:{n_total}, MCP:{n_mcp}, ๆๅไบคไบ:{n_successes}, '
|
| 207 |
+
f'ๅคๅถ:{n_copies}, bad_cases:{len(merged_bad_cases)}'
|
| 208 |
+
)
|
| 209 |
+
return True, n_total, n_visits, n_copies, n_mcp, n_successes, top_keywords, merged_bad_cases, n_history
|
| 210 |
+
|
| 211 |
+
return False, 0, 0, 0, 0, 0, {}, [], []
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
# โโ ๅๅฐๅๆญฅๅ็จ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 215 |
+
|
| 216 |
+
async def _perform_sync():
|
| 217 |
+
global _dirty_count, _dirty_visits, _dirty_copies, _dirty_mcp, _dirty_successes
|
| 218 |
+
global _dirty_keywords, _dirty_bad_cases, _last_sync
|
| 219 |
+
global _memory_count, _memory_visits, _memory_copies, _memory_mcp, _memory_successes
|
| 220 |
+
global _memory_keywords, _memory_bad_cases, _memory_history
|
| 221 |
+
|
| 222 |
+
lock = _get_sync_lock()
|
| 223 |
+
if lock.locked():
|
| 224 |
+
return
|
| 225 |
+
|
| 226 |
+
async with lock:
|
| 227 |
+
has_dirty = (
|
| 228 |
+
_dirty_count + _dirty_visits + _dirty_copies + _dirty_mcp + _dirty_successes
|
| 229 |
+
+ len(_dirty_keywords) + len(_dirty_bad_cases)
|
| 230 |
+
) > 0
|
| 231 |
+
if not has_dirty:
|
| 232 |
+
return
|
| 233 |
+
|
| 234 |
+
c_adds, v_adds, cp_adds, m_adds, s_adds = (
|
| 235 |
+
_dirty_count, _dirty_visits, _dirty_copies, _dirty_mcp, _dirty_successes
|
| 236 |
+
)
|
| 237 |
+
k_adds = dict(_dirty_keywords)
|
| 238 |
+
bc_adds = list(_dirty_bad_cases)
|
| 239 |
+
history_snapshot = list(_memory_history)
|
| 240 |
+
|
| 241 |
+
_dirty_count = 0
|
| 242 |
+
_dirty_visits = 0
|
| 243 |
+
_dirty_copies = 0
|
| 244 |
+
_dirty_mcp = 0
|
| 245 |
+
_dirty_successes = 0
|
| 246 |
+
_dirty_keywords.clear()
|
| 247 |
+
_dirty_bad_cases.clear()
|
| 248 |
+
|
| 249 |
+
loop = asyncio.get_running_loop()
|
| 250 |
+
result = await loop.run_in_executor(
|
| 251 |
+
None, _sync_remote_task,
|
| 252 |
+
c_adds, v_adds, cp_adds, m_adds, s_adds, k_adds, bc_adds, history_snapshot,
|
| 253 |
+
)
|
| 254 |
+
success, l_total, l_visits, l_copies, l_mcp, l_successes, l_keywords, l_bad_cases, l_history = result
|
| 255 |
+
|
| 256 |
+
if success:
|
| 257 |
+
_last_sync = time.time()
|
| 258 |
+
_memory_count = max(_memory_count, l_total)
|
| 259 |
+
_memory_visits = max(_memory_visits, l_visits)
|
| 260 |
+
_memory_copies = max(_memory_copies, l_copies)
|
| 261 |
+
_memory_mcp = max(_memory_mcp, l_mcp)
|
| 262 |
+
_memory_successes = max(_memory_successes, l_successes)
|
| 263 |
+
_memory_keywords.clear()
|
| 264 |
+
_memory_keywords.update(l_keywords)
|
| 265 |
+
_memory_bad_cases.clear()
|
| 266 |
+
_memory_bad_cases.extend(l_bad_cases)
|
| 267 |
+
_memory_history.clear()
|
| 268 |
+
_memory_history.extend(l_history)
|
| 269 |
+
else:
|
| 270 |
+
_dirty_count += c_adds
|
| 271 |
+
_dirty_visits += v_adds
|
| 272 |
+
_dirty_copies += cp_adds
|
| 273 |
+
_dirty_mcp += m_adds
|
| 274 |
+
_dirty_successes += s_adds
|
| 275 |
+
_dirty_keywords.update(k_adds)
|
| 276 |
+
_dirty_bad_cases.extend(bc_adds)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
# โโ ๅ
ฌๅ
ฑ API โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 280 |
+
|
| 281 |
+
async def init():
|
| 282 |
+
"""ๅฏๅจๆถไป OSS ๆๅๆๆฐ่ฎกๆฐ๏ผๅๅงๅๅ
ๅญ็ถๆใ"""
|
| 283 |
+
global _memory_count, _memory_visits, _memory_copies, _memory_mcp, _memory_successes
|
| 284 |
+
global _memory_keywords, _memory_bad_cases, _memory_history, _last_sync
|
| 285 |
+
|
| 286 |
+
cfg = get_counter_cfg()
|
| 287 |
+
if not cfg.available:
|
| 288 |
+
print(f'[Counter] ่ฎกๆฐๅจๆช้
็ฝฎ๏ผplatform={cfg.platform}๏ผ๏ผไป
ไฝฟ็จๅ
ๅญ่ฎกๆฐใ')
|
| 289 |
+
return
|
| 290 |
+
|
| 291 |
+
loop = asyncio.get_running_loop()
|
| 292 |
+
(
|
| 293 |
+
_memory_count, _memory_visits, _memory_copies, _memory_mcp, _memory_successes,
|
| 294 |
+
r_keys, r_bad_cases, r_history,
|
| 295 |
+
) = await loop.run_in_executor(None, _read_remote)
|
| 296 |
+
|
| 297 |
+
_memory_keywords.update(r_keys)
|
| 298 |
+
_memory_bad_cases.clear()
|
| 299 |
+
_memory_bad_cases.extend(r_bad_cases)
|
| 300 |
+
_memory_history.clear()
|
| 301 |
+
_memory_history.extend(r_history)
|
| 302 |
+
_last_sync = time.time()
|
| 303 |
+
print(
|
| 304 |
+
f'[Counter] ๅๅงๅๅฎๆ๏ผ{cfg.platform}๏ผ๏ผๆ็ดข={_memory_count}, MCP={_memory_mcp}, '
|
| 305 |
+
f'่ฎฟ้ฎ={_memory_visits}, ๅคๅถ={_memory_copies}, ๅๅฒ={len(_memory_history)}ๅคฉ'
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def _check_sync():
|
| 310 |
+
cfg = get_counter_cfg()
|
| 311 |
+
if not cfg.available:
|
| 312 |
+
return
|
| 313 |
+
if (time.time() - _last_sync > SYNC_INTERVAL) or (
|
| 314 |
+
_dirty_count + _dirty_visits + _dirty_copies + _dirty_mcp + _dirty_successes
|
| 315 |
+
+ len(_dirty_keywords) + len(_dirty_bad_cases)
|
| 316 |
+
) >= SYNC_THRESHOLD:
|
| 317 |
+
asyncio.create_task(_perform_sync())
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
async def increment() -> int:
|
| 321 |
+
global _memory_count, _dirty_count
|
| 322 |
+
_memory_count += 1; _dirty_count += 1
|
| 323 |
+
_check_sync()
|
| 324 |
+
return _memory_count
|
| 325 |
+
|
| 326 |
+
async def increment_visit() -> int:
|
| 327 |
+
global _memory_visits, _dirty_visits
|
| 328 |
+
_memory_visits += 1; _dirty_visits += 1
|
| 329 |
+
_check_sync()
|
| 330 |
+
return _memory_visits
|
| 331 |
+
|
| 332 |
+
async def increment_copy() -> int:
|
| 333 |
+
global _memory_copies, _dirty_copies
|
| 334 |
+
_memory_copies += 1; _dirty_copies += 1
|
| 335 |
+
_check_sync()
|
| 336 |
+
return _memory_copies
|
| 337 |
+
|
| 338 |
+
async def increment_mcp() -> int:
|
| 339 |
+
global _memory_mcp, _dirty_mcp
|
| 340 |
+
_memory_mcp += 1; _dirty_mcp += 1
|
| 341 |
+
_check_sync()
|
| 342 |
+
return _memory_mcp
|
| 343 |
+
|
| 344 |
+
async def increment_success() -> int:
|
| 345 |
+
global _memory_successes, _dirty_successes
|
| 346 |
+
_memory_successes += 1; _dirty_successes += 1
|
| 347 |
+
_check_sync()
|
| 348 |
+
return _memory_successes
|
| 349 |
+
|
| 350 |
+
async def add_keywords(words: list[str]):
|
| 351 |
+
global _dirty_keywords, _memory_keywords
|
| 352 |
+
valid_words = [w for w in words if len(w) > 1 and w not in {'ไธไธช', '็', 'ๅจ', 'ไบ', 'ๆฏ', 'ๆ'}]
|
| 353 |
+
for word in valid_words:
|
| 354 |
+
_memory_keywords[word] += 1
|
| 355 |
+
_dirty_keywords[word] += 1
|
| 356 |
+
_check_sync()
|
| 357 |
+
|
| 358 |
+
async def add_bad_case(query: str, platform: str = '', settings: dict | None = None) -> None:
|
| 359 |
+
global _dirty_bad_cases, _memory_bad_cases
|
| 360 |
+
entry = {
|
| 361 |
+
'q': query,
|
| 362 |
+
't': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
| 363 |
+
}
|
| 364 |
+
if platform:
|
| 365 |
+
entry['platform'] = platform
|
| 366 |
+
if settings:
|
| 367 |
+
entry['settings'] = settings
|
| 368 |
+
_memory_bad_cases.insert(0, entry)
|
| 369 |
+
if len(_memory_bad_cases) > MAX_BAD_CASES:
|
| 370 |
+
_memory_bad_cases[:] = _memory_bad_cases[:MAX_BAD_CASES]
|
| 371 |
+
_dirty_bad_cases.insert(0, entry)
|
| 372 |
+
_check_sync()
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def get() -> int: return _memory_count
|
| 376 |
+
def get_visits() -> int: return _memory_visits
|
| 377 |
+
def get_copies() -> int: return _memory_copies
|
| 378 |
+
def get_mcp() -> int: return _memory_mcp
|
| 379 |
+
def get_successes() -> int: return _memory_successes
|
| 380 |
+
|
| 381 |
+
def get_hot_keywords(top_n: int = 10) -> dict:
|
| 382 |
+
return dict(_memory_keywords.most_common(top_n))
|
| 383 |
+
|
| 384 |
+
def get_bad_cases() -> list[dict]:
|
| 385 |
+
return list(_memory_bad_cases)
|
| 386 |
+
|
| 387 |
+
def get_history() -> list[dict]:
|
| 388 |
+
"""่ฟๅๆฏๆฅๆ็ดข้ๅฟซ็
ง๏ผๆ ผๅผ [{"date": "YYYY-MM-DD", "total": int}, ...]๏ผๆๆฅๆๆญฃๅบใ"""
|
| 389 |
+
return list(_memory_history)
|
| 390 |
+
|
| 391 |
+
async def force_sync():
|
| 392 |
+
await _perform_sync()
|
core/engine.py
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
core/engine.py
|
| 3 |
+
โโโโโโโโโโโโโโ
|
| 4 |
+
DanbooruTagger ๆ ธๅฟๅผๆ
|
| 5 |
+
|
| 6 |
+
็ผๅญๆ ผๅผ๏ผๅญไบ cache_dir/ ็ฎๅฝ๏ผ๏ผ
|
| 7 |
+
embeddings.safetensors โ ๅ่ทฏๅ้็ฉ้ต๏ผFP16๏ผ๏ผ่ก้กบๅบไธ metadata.parquet ๅฎๅ
จๅฏน้ฝ
|
| 8 |
+
metadata.parquet โ DataFrame๏ผname/cn_name/cn_core/wiki/nsfw/category/post_count๏ผ
|
| 9 |
+
meta.json โ ๆ ้ๅ
ๆฐๆฎ๏ผmax_log_countใschema_version๏ผ
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import asyncio
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
import re
|
| 18 |
+
import time
|
| 19 |
+
from collections import OrderedDict
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from datetime import datetime
|
| 22 |
+
from typing import Any, Optional
|
| 23 |
+
|
| 24 |
+
import jieba
|
| 25 |
+
import numpy as np
|
| 26 |
+
import pandas as pd
|
| 27 |
+
import torch
|
| 28 |
+
from safetensors.torch import load_file as st_load, save_file as st_save
|
| 29 |
+
from sentence_transformers import SentenceTransformer
|
| 30 |
+
|
| 31 |
+
from .models import SearchRequest, SearchResponse, TagResult
|
| 32 |
+
from platform_utils import (
|
| 33 |
+
PLATFORM,
|
| 34 |
+
is_cloud,
|
| 35 |
+
download_file,
|
| 36 |
+
resolve_model_path,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# LRU ็ผๅญ
|
| 41 |
+
class LRUCache:
|
| 42 |
+
def __init__(self, maxsize: int):
|
| 43 |
+
self._cache: OrderedDict[Any, Any] = OrderedDict()
|
| 44 |
+
self._maxsize = maxsize
|
| 45 |
+
|
| 46 |
+
def get(self, key: Any) -> Any:
|
| 47 |
+
if key not in self._cache:
|
| 48 |
+
return None
|
| 49 |
+
self._cache.move_to_end(key)
|
| 50 |
+
return self._cache[key]
|
| 51 |
+
|
| 52 |
+
def put(self, key: Any, value: Any) -> None:
|
| 53 |
+
if key in self._cache:
|
| 54 |
+
self._cache.move_to_end(key)
|
| 55 |
+
else:
|
| 56 |
+
if len(self._cache) >= self._maxsize:
|
| 57 |
+
self._cache.popitem(last=False)
|
| 58 |
+
self._cache[key] = value
|
| 59 |
+
|
| 60 |
+
def __len__(self) -> int:
|
| 61 |
+
return len(self._cache)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 65 |
+
# ๅธธ้
|
| 66 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 67 |
+
|
| 68 |
+
STOP_WORDS: frozenset[str] = frozenset({
|
| 69 |
+
',', '.', ':', ';', '?', '!', '"', "'", '`',
|
| 70 |
+
'(', ')', '[', ']', '{', '}', '<', '>',
|
| 71 |
+
'-', '_', '=', '+', '/', '\\', '|', '@', '#', '$', '%', '^', '&', '*', '~',
|
| 72 |
+
'๏ผ', 'ใ', '๏ผ', '๏ผ', '๏ผ', '๏ผ', '\u201c', '\u201d', '\u2018', '\u2019',
|
| 73 |
+
'๏ผ', '๏ผ', 'ใ', 'ใ', 'ใ', 'ใ', 'ใ', 'โฆ', 'โ', 'ยท',
|
| 74 |
+
' ', '\t', '\n', '\r',
|
| 75 |
+
'็', 'ๅฐ', 'ๅพ', 'ไบ', '็', '่ฟ',
|
| 76 |
+
'ๆฏ', 'ไธบ', '่ขซ', '็ป', 'ๆ', '่ฎฉ', '็ฑ',
|
| 77 |
+
'ๅจ', 'ไป', '่ช', 'ๅ', 'ๅพ', 'ๅฏน', 'ไบ',
|
| 78 |
+
'ๅ', 'ไธ', 'ๅ', 'ๆ', 'ไธ', '่', 'ไฝ', 'ๅนถ', 'ๅณ', 'ๅ', 'ไน',
|
| 79 |
+
'ๅ', 'ๅ', 'ๅง', 'ๅข', 'ๅข', 'ๅฆ', 'ๅ', 'ๅ', 'ๅ',
|
| 80 |
+
'ๆ', 'ไฝ ', 'ไป', 'ๅฅน', 'ๅฎ', 'ๆไปฌ', 'ไฝ ไปฌ', 'ไปไปฌ',
|
| 81 |
+
'่ฟ', '้ฃ', 'ๆญค', 'ๅ
ถ', '่ฐ', 'ๅฅ', 'ๆ', 'ๆฏ',
|
| 82 |
+
'่ฟไธช', '้ฃไธช', '่ฟไบ', '้ฃไบ', '่ฟ้', '้ฃ้',
|
| 83 |
+
'ไธช', 'ไฝ', 'ๅช', 'ๆก', 'ๅผ ', 'ๅน
', 'ไปถ', 'ๅฅ', 'ๅ', 'ๅฏน', 'ๅฏ',
|
| 84 |
+
'็ง', '็ฑป', '็พค', 'ไบ', '็น', 'ไปฝ', '้จ', 'ๅ',
|
| 85 |
+
'ๅพ', 'ๅคช', 'ๆด', 'ๆ', 'ๆบ', '็น', 'ๅฅฝ', '็',
|
| 86 |
+
'ไธ', 'ไธไธช', 'ไธ็ง', 'ไธไธ', 'ไธ็น', 'ไธไบ',
|
| 87 |
+
'ๆ', 'ๆ ', '้', 'ๆฒก', 'ไธ',
|
| 88 |
+
'ๆญฃๅจ', 'ๅทฒ็ป', 'ๆญฃ', 'ๅ', 'ๅผๅง', '็ปง็ปญ', 'ไธ็ด', 'ไธๆญ',
|
| 89 |
+
'็ฉฟ็', 'ๆด็', '็ฉฟ', 'ๆด',
|
| 90 |
+
'ๅธฆๆ', 'ๅ
ทๆ', 'ๆฅๆ',
|
| 91 |
+
'็่ตทๆฅ', '็ไธๅป', 'ๆพๅพ', 'ไปฟไฝ', 'ไผผไน',
|
| 92 |
+
'ๅๅ', '้ๅธธ', '็นๅซ', 'ๆฏ่พ',
|
| 93 |
+
'ๅพ็', '็ป้ข', 'ๅพๅ',
|
| 94 |
+
'ไฝไบ', 'ๅคไบ',
|
| 95 |
+
'่ฎธๅค', 'ๅคง้', 'ๅ็ง', 'ๆๆ', 'ๅ
ถไป', 'ๅ
ถๅฎ'
|
| 96 |
+
})
|
| 97 |
+
|
| 98 |
+
CAT_MAP: dict[str, str] = {
|
| 99 |
+
'0': 'General', '1': 'Artist', '3': 'Copyright', '4': 'Character', '5': 'Meta',
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
SCHEMA_VERSION = 3 # ๅ็บงๆญคๅผๅฐ่ชๅจ่งฆๅๅ
จ้้ๅปบ๏ผ็จไบ็ ดๅๆงๆ ผๅผๅๆด
|
| 103 |
+
|
| 104 |
+
# ็จๆทๆพๅผๅ้ๅ๏ผ็บฏ CJK ็ๆฎต่ถ
่ฟๆญค้ฟๅบฆไป็จ jieba ๅๅ๏ผ้ฟๅ
้ฟๅฅ่ขซๅฝไฝๅๅญๆฆๅฟต๏ผ
|
| 105 |
+
_ATOMIC_CJK_MAX_LEN = 7
|
| 106 |
+
|
| 107 |
+
# ๅ่ทฏ embedding ๅฑ้
็ฝฎ: (ๅฑๅ, tensor ๅฑๆงๅ, DataFrame ๅๅ)
|
| 108 |
+
_LAYER_SPEC: list[tuple[str, str, str]] = [
|
| 109 |
+
('่ฑๆ', 'emb_en', 'name'),
|
| 110 |
+
('ไธญๆๆฉๅฑ่ฏ', 'emb_cn', 'cn_name'),
|
| 111 |
+
('้ไน', 'emb_wiki', 'wiki'),
|
| 112 |
+
('ไธญๆๆ ธๅฟ่ฏ', 'emb_cn_core', 'cn_core'),
|
| 113 |
+
]
|
| 114 |
+
_ALL_LAYER_NAMES = [ln for ln, _, _ in _LAYER_SPEC]
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 118 |
+
# ็ผๅญ่ทฏๅพๅฉๆ
|
| 119 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 120 |
+
|
| 121 |
+
class _CachePaths:
|
| 122 |
+
def __init__(self, cache_dir: str | Path):
|
| 123 |
+
self.dir = Path(cache_dir)
|
| 124 |
+
self.embeddings = self.dir / 'danbooru_multiview_embeddings.safetensors'
|
| 125 |
+
self.metadata = self.dir / 'tags_metadata.parquet'
|
| 126 |
+
self.meta_json = self.dir / 'version_data.json'
|
| 127 |
+
|
| 128 |
+
def exists(self) -> bool:
|
| 129 |
+
return (
|
| 130 |
+
self.embeddings.is_file()
|
| 131 |
+
and self.metadata.is_file()
|
| 132 |
+
and self.meta_json.is_file()
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
def ensure_dir(self):
|
| 136 |
+
self.dir.mkdir(parents=True, exist_ok=True)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class DanbooruTagger:
|
| 140 |
+
"""ๆ ธๅฟๆ็ดขๅผๆ๏ผๅไพ๏ผ"""
|
| 141 |
+
|
| 142 |
+
_instance: Optional['DanbooruTagger'] = None
|
| 143 |
+
_lock: Optional[asyncio.Lock] = None
|
| 144 |
+
|
| 145 |
+
@classmethod
|
| 146 |
+
def is_ready(cls) -> bool:
|
| 147 |
+
return cls._instance is not None and cls._instance.is_loaded
|
| 148 |
+
|
| 149 |
+
@classmethod
|
| 150 |
+
async def get_instance(cls, **kwargs) -> 'DanbooruTagger':
|
| 151 |
+
if cls._lock is None:
|
| 152 |
+
cls._lock = asyncio.Lock()
|
| 153 |
+
async with cls._lock:
|
| 154 |
+
if cls._instance is None:
|
| 155 |
+
inst = cls(**kwargs)
|
| 156 |
+
await asyncio.to_thread(inst.load)
|
| 157 |
+
cls._instance = inst
|
| 158 |
+
return cls._instance
|
| 159 |
+
|
| 160 |
+
def __init__(
|
| 161 |
+
self,
|
| 162 |
+
model_path: Optional[str] = None,
|
| 163 |
+
csv_file: str = 'origin_database/tags_enhanced.csv',
|
| 164 |
+
cache_dir: str = 'tags_embedding',
|
| 165 |
+
cooc_file: str = 'origin_database/cooccurrence_clean.csv',
|
| 166 |
+
):
|
| 167 |
+
# ๆจกๅ่ทฏๅพ๏ผไผๅ
ไฝฟ็จๆพๅผไผ ๅ
ฅ๏ผๅฆๅไบค็ฑ platform_utils ่งฃๆ
|
| 168 |
+
self.model_path = model_path or resolve_model_path()
|
| 169 |
+
|
| 170 |
+
self.csv_path = csv_file
|
| 171 |
+
self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 172 |
+
self.paths = _CachePaths(cache_dir)
|
| 173 |
+
self.cooc_file = cooc_file
|
| 174 |
+
|
| 175 |
+
self.model: Optional[SentenceTransformer] = None
|
| 176 |
+
self.df: Optional[pd.DataFrame] = None
|
| 177 |
+
self.emb_en: Optional[torch.Tensor] = None
|
| 178 |
+
self.emb_cn: Optional[torch.Tensor] = None
|
| 179 |
+
self.emb_wiki: Optional[torch.Tensor] = None
|
| 180 |
+
self.emb_cn_core: Optional[torch.Tensor] = None
|
| 181 |
+
self.max_log_count: float = 15.0
|
| 182 |
+
self.cooc: dict[str, list[tuple[str, int]]] = {}
|
| 183 |
+
self._name_to_idx: dict[str, int] = {}
|
| 184 |
+
self.is_loaded: bool = False
|
| 185 |
+
|
| 186 |
+
# ้ขๆๅ็ๅๆฐ็ป๏ผ้ฟๅ
็ญ็น่ทฏๅพไธๅๅคๆง่ก df.iloc[idx]
|
| 187 |
+
self._arr_name: Optional[np.ndarray] = None
|
| 188 |
+
self._arr_cn_name: Optional[np.ndarray] = None
|
| 189 |
+
self._arr_category: Optional[np.ndarray] = None
|
| 190 |
+
self._arr_nsfw: Optional[np.ndarray] = None
|
| 191 |
+
self._arr_wiki: Optional[np.ndarray] = None
|
| 192 |
+
self._arr_post_count: Optional[np.ndarray] = None
|
| 193 |
+
self._arr_pop_score: Optional[np.ndarray] = None
|
| 194 |
+
|
| 195 |
+
# ไธๅฑ LRU ็ผๅญ๏ผ็บฏๅ
ๅญ๏ผ้ๅฏๅ่ชๅจ้็ญ๏ผ
|
| 196 |
+
# embedding ็ผๅญ๏ผkey=ๆๆฌ, value=ๅฝไธๅๅ็ 1-D Tensor (D,)๏ผ็บฆ 40 MB
|
| 197 |
+
self._emb_cache: LRUCache = LRUCache(maxsize=10_000)
|
| 198 |
+
# ๆ็ดข็ปๆ็ผๅญ๏ผkey=่ฏทๆฑๅๆฐ tuple, value=SearchResponse๏ผ็บฆ 100 MB
|
| 199 |
+
self._search_cache: LRUCache = LRUCache(maxsize=5_000)
|
| 200 |
+
# ๅ
ณ่ๆจ่็ผๅญ๏ผkey=(seed_tuple, limit, show_nsfw), value=list[RelatedTag]๏ผ็บฆ 20 MB
|
| 201 |
+
self._related_cache: LRUCache = LRUCache(maxsize=2_000)
|
| 202 |
+
|
| 203 |
+
# โโ ๅๅงๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 204 |
+
|
| 205 |
+
def load(self) -> None:
|
| 206 |
+
"""ๅๆญฅๅ ่ฝฝ๏ผๅจ็บฟ็จๆฑ ไธญ่ฐ็จใ"""
|
| 207 |
+
if self.is_loaded:
|
| 208 |
+
return
|
| 209 |
+
t0 = time.time()
|
| 210 |
+
|
| 211 |
+
# โโ ไบ็ซฏ็ฏๅข๏ผไปๅฏนๅบๅนณๅฐ Hub ๆๅๆฐๆฎๆไปถ โโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 212 |
+
if is_cloud():
|
| 213 |
+
self._pull_cloud_files()
|
| 214 |
+
|
| 215 |
+
# โโ ็ผๅญๆ ก้ชไธๆๅปบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 216 |
+
if not self.paths.exists():
|
| 217 |
+
print('\n' + '=' * 50)
|
| 218 |
+
print('[Engine] ๆชๆพๅฐ็ผๅญ๏ผๅผๅง้ฆๆฌกๆๅปบ๏ผ็บฆ 1~3 ๅ้๏ผ...')
|
| 219 |
+
print('=' * 50 + '\n')
|
| 220 |
+
self._load_model()
|
| 221 |
+
self._build_full()
|
| 222 |
+
else:
|
| 223 |
+
print(f'[Engine] ๅ ่ฝฝ็ผๅญ ({self.paths.dir}) ...')
|
| 224 |
+
self._load_from_cache()
|
| 225 |
+
if self._cached_schema_version() != SCHEMA_VERSION:
|
| 226 |
+
print('[Engine] ็ผๅญๆ ผๅผ็ๆฌไธ็ฌฆ๏ผ่งฆๅๅ
จ้้ๅปบ...')
|
| 227 |
+
self._load_model()
|
| 228 |
+
self._build_full()
|
| 229 |
+
elif os.path.exists(self.csv_path):
|
| 230 |
+
self._load_model()
|
| 231 |
+
self._smart_update()
|
| 232 |
+
|
| 233 |
+
if self.model is None:
|
| 234 |
+
self._load_model()
|
| 235 |
+
|
| 236 |
+
self._setup_jieba_from_memory()
|
| 237 |
+
self._load_cooc()
|
| 238 |
+
self._name_to_idx = {n: i for i, n in enumerate(self.df['name'])}
|
| 239 |
+
self._rebuild_arrays_from_df()
|
| 240 |
+
self._normalize_embeddings()
|
| 241 |
+
self.is_loaded = True
|
| 242 |
+
print(f'[Engine] ๅๅงๅๅฎๆ๏ผ่ๆถ {time.time() - t0:.2f}s')
|
| 243 |
+
|
| 244 |
+
def _normalize_embeddings(self) -> None:
|
| 245 |
+
"""
|
| 246 |
+
ๅฏนๅ่ทฏ embedding ็ฉ้ตๅ L2 ๅฝไธๅ๏ผin-place ๆฟๆข self.emb_*๏ผใ
|
| 247 |
+
ๅฝไธๅๅ search ้ถๆฎตๅฏ็ดๆฅ็จ็ฉ้ตไนๆณๅพๅฐ cosine similarity๏ผ
|
| 248 |
+
ๆ ้ๆฏๆฌก่ฐ็จ util.semantic_search ๅ
้จๅๅไธๆฌกๅฝไธๅใ
|
| 249 |
+
"""
|
| 250 |
+
for _, attr, _ in _LAYER_SPEC:
|
| 251 |
+
t = getattr(self, attr)
|
| 252 |
+
if t is None:
|
| 253 |
+
continue
|
| 254 |
+
setattr(self, attr, torch.nn.functional.normalize(t, p=2, dim=1))
|
| 255 |
+
|
| 256 |
+
def _rebuild_arrays_from_df(self) -> None:
|
| 257 |
+
"""
|
| 258 |
+
ๅฐ DataFrame ไธญๆ็ดข็ญ็น่ทฏๅพ้่ฆ็ๅ้ขๆๅไธบ numpy ๆฐ็ปใ
|
| 259 |
+
ไปปไฝไฟฎๆน self.df ่กๅ
ๅฎนๆ่กๆฐ็ๆไฝไนๅ้ฝๅฟ
้กป่ฐ็จๆญคๆนๆณๅทๆฐใ
|
| 260 |
+
"""
|
| 261 |
+
if self.df is None:
|
| 262 |
+
return
|
| 263 |
+
self._arr_name = self.df['name'].to_numpy()
|
| 264 |
+
self._arr_cn_name = self.df['cn_name'].to_numpy()
|
| 265 |
+
self._arr_category = self.df['category'].astype(str).to_numpy()
|
| 266 |
+
self._arr_nsfw = self.df['nsfw'].astype(str).to_numpy()
|
| 267 |
+
self._arr_wiki = self.df['wiki'].astype(str).to_numpy()
|
| 268 |
+
self._arr_post_count = self.df['post_count'].to_numpy()
|
| 269 |
+
# ้ข็ฎ็ญๅบฆๅฝไธๅๅ๏ผ้ฟๅ
search ไธญๆฏๆฌก np.log1p
|
| 270 |
+
max_log = self.max_log_count if self.max_log_count > 0 else 1.0
|
| 271 |
+
self._arr_pop_score = np.log1p(self._arr_post_count) / max_log
|
| 272 |
+
|
| 273 |
+
def _pull_cloud_files(self) -> None:
|
| 274 |
+
"""
|
| 275 |
+
ไปๅฝๅไบๅนณๅฐๆๅๆๆๆฐๆฎๆไปถ๏ผๅนถๅฐ่ทฏๅพๅๅๅฎไพๅฑๆงใ
|
| 276 |
+
HF / MS ็ๅทฎๅผๅฎๅ
จ็ฑ platform_utils.download_file() ๅฑ่ฝใ
|
| 277 |
+
"""
|
| 278 |
+
print(f'[Engine] ไบ็ซฏ็ฏๅข ({PLATFORM})๏ผๅผๅงๆๅๆฐๆฎๆไปถ...')
|
| 279 |
+
|
| 280 |
+
# โโ HF ๅนณๅฐ้่ฆ้ขๅคๆๅฎ repo_id๏ผSPACE_ID๏ผๅ repo_type โโโโโโโโโโ
|
| 281 |
+
extra_hf_kwargs = {}
|
| 282 |
+
if PLATFORM == 'hf':
|
| 283 |
+
extra_hf_kwargs = {
|
| 284 |
+
'hf_repo_id': os.environ.get('SPACE_ID'),
|
| 285 |
+
'hf_repo_type': 'space',
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
def pull(filename: str) -> str:
|
| 289 |
+
try:
|
| 290 |
+
return download_file(filename, **extra_hf_kwargs)
|
| 291 |
+
except Exception as e:
|
| 292 |
+
print(f'[Engine] ๆๅ {filename} ๅคฑ่ดฅ๏ผ้่ดๅฝ๏ผ: {e}')
|
| 293 |
+
return filename # ๅ้ๅฐๅๅง่ทฏๅพ๏ผ่ฎฉๅ็ปญ้ป่พๅณๅฎๆฏๅฆ้ๅปบ
|
| 294 |
+
|
| 295 |
+
self.csv_path = pull('origin_database/tags_enhanced.csv')
|
| 296 |
+
self.cooc_file = pull('origin_database/cooccurrence_clean.parquet')
|
| 297 |
+
|
| 298 |
+
meta_path = pull('tags_embedding/tags_metadata.parquet')
|
| 299 |
+
emb_path = pull('tags_embedding/danbooru_multiview_embeddings.safetensors')
|
| 300 |
+
json_path = pull('tags_embedding/version_data.json')
|
| 301 |
+
|
| 302 |
+
# ๅชๆไธไธช็ผๅญๆไปถ้ฝๆๅๆๅๆ่ฆ็่ทฏๅพ๏ผ้ฒๆญข้จๅๅคฑ่ดฅๅฏผ่ด exists() ่ฏฏๅค
|
| 303 |
+
if all(
|
| 304 |
+
Path(p).is_file()
|
| 305 |
+
for p in (meta_path, emb_path, json_path)
|
| 306 |
+
):
|
| 307 |
+
self.paths.metadata = Path(meta_path)
|
| 308 |
+
self.paths.embeddings = Path(emb_path)
|
| 309 |
+
self.paths.meta_json = Path(json_path)
|
| 310 |
+
print('[Engine] ไบ็ซฏๆฐๆฎๆไปถๆๅๅฎๆฏใ')
|
| 311 |
+
else:
|
| 312 |
+
print('[Engine] ้จๅ็ผๅญๆไปถๆๅๅคฑ่ดฅ๏ผๅฐ่งฆๅๆฌๅฐ้ๅปบใ')
|
| 313 |
+
|
| 314 |
+
# โโ ๆ็ดข โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 315 |
+
|
| 316 |
+
def _encode_queries(self, queries: list[str]) -> torch.Tensor:
|
| 317 |
+
"""ๆน้็ผ็ ๆฅ่ฏข่ฏ๏ผๅฝไธญ embedding ็ผๅญ็่ทณ่ฟ model.encodeใ"""
|
| 318 |
+
cached_vecs: list[Optional[torch.Tensor]] = [self._emb_cache.get(q) for q in queries]
|
| 319 |
+
uncached_idx = [i for i, v in enumerate(cached_vecs) if v is None]
|
| 320 |
+
|
| 321 |
+
if uncached_idx:
|
| 322 |
+
uncached_texts = [queries[i] for i in uncached_idx]
|
| 323 |
+
new_embs = self.model.encode(
|
| 324 |
+
uncached_texts, convert_to_tensor=True, show_progress_bar=False,
|
| 325 |
+
).float()
|
| 326 |
+
new_embs = torch.nn.functional.normalize(new_embs, p=2, dim=1)
|
| 327 |
+
for j, i in enumerate(uncached_idx):
|
| 328 |
+
emb = new_embs[j]
|
| 329 |
+
self._emb_cache.put(queries[i], emb)
|
| 330 |
+
cached_vecs[i] = emb
|
| 331 |
+
|
| 332 |
+
return torch.stack(cached_vecs) # type: ignore[arg-type]
|
| 333 |
+
|
| 334 |
+
def search(self, request: SearchRequest) -> SearchResponse:
|
| 335 |
+
if not self.is_loaded:
|
| 336 |
+
self.load()
|
| 337 |
+
|
| 338 |
+
cache_key = (
|
| 339 |
+
request.query,
|
| 340 |
+
request.top_k,
|
| 341 |
+
request.limit,
|
| 342 |
+
request.popularity_weight,
|
| 343 |
+
request.use_segmentation,
|
| 344 |
+
tuple(sorted(request.target_layers)),
|
| 345 |
+
tuple(sorted(request.target_categories)),
|
| 346 |
+
)
|
| 347 |
+
cached = self._search_cache.get(cache_key)
|
| 348 |
+
if cached is not None:
|
| 349 |
+
return cached
|
| 350 |
+
|
| 351 |
+
if request.use_segmentation:
|
| 352 |
+
raw_kw, raw_segments = self._smart_split(request.query)
|
| 353 |
+
keywords = [w.strip() for w in raw_kw if w.strip() and w.strip() not in STOP_WORDS]
|
| 354 |
+
# raw_segments: ๅ้็ฌฆๅๅๅ็ๅๅง็ๆฎต๏ผๆช็ป jieba๏ผ๏ผไฝไธบไปๅฅ็บงๆฅ่ฏขๆๅ
ฅใ
|
| 355 |
+
# ๆ้คไธๅฎๆด query ็ธๅใไปฅๅๅทฒๅบ็ฐๅจ keywords ไธญ็็ๆฎต๏ผ้ฟๅ
้ๅค็ผ็ ๅๆ้่จ่ใ
|
| 356 |
+
keywords_set = set(keywords)
|
| 357 |
+
extra_segments = [s for s in raw_segments if s != request.query and s not in keywords_set]
|
| 358 |
+
queries = [request.query] + extra_segments + keywords
|
| 359 |
+
else:
|
| 360 |
+
keywords = []
|
| 361 |
+
extra_segments = []
|
| 362 |
+
queries = [request.query]
|
| 363 |
+
|
| 364 |
+
q_emb = self._encode_queries(queries)
|
| 365 |
+
|
| 366 |
+
tl = request.target_layers
|
| 367 |
+
k = request.top_k
|
| 368 |
+
|
| 369 |
+
# ๆฏไธชๆฅ่ฏข่ฏๅ็ฌๅๆๅพ่ฏๅซ๏ผ้ฟๅ
้ฟๅฅๆๅพๆฑกๆ็ญๅ่ฏ
|
| 370 |
+
query_weights = [self._detect_intent(q) for q in queries]
|
| 371 |
+
active_layers = [ln for ln in _ALL_LAYER_NAMES if ln in tl]
|
| 372 |
+
|
| 373 |
+
# ้ข็ฎๆฏไธช query ร ๆฏไธช layer ็ top_k ้
้ข
|
| 374 |
+
# cur_pvk_per_q[i][ln] = ็ฌฌ i ไธช query ๅจ layer ln ็้
้ข
|
| 375 |
+
cur_pvk_per_q: list[dict[str, int]] = []
|
| 376 |
+
for cur_weights in query_weights:
|
| 377 |
+
if active_layers:
|
| 378 |
+
aw = {l: cur_weights.get(l, 1.0) for l in active_layers}
|
| 379 |
+
total_aw = sum(aw.values())
|
| 380 |
+
cur_pvk = {l: max(1, round(k * aw[l] / total_aw)) for l in active_layers}
|
| 381 |
+
else:
|
| 382 |
+
cur_pvk = {}
|
| 383 |
+
cur_pvk_per_q.append(cur_pvk)
|
| 384 |
+
|
| 385 |
+
target_cats = request.target_categories
|
| 386 |
+
w_pop = request.popularity_weight
|
| 387 |
+
|
| 388 |
+
final: dict[str, TagResult] = {}
|
| 389 |
+
|
| 390 |
+
# ๆ layer ๆน้ๅ็ฉ้ตไน + topk๏ผๅๅนถ QรL=20 ๆฌกๅฐ่ฐ็จไธบ L=4 ๆฌกๅคง่ฐ็จ
|
| 391 |
+
for ln, attr, _ in _LAYER_SPEC:
|
| 392 |
+
if ln not in tl:
|
| 393 |
+
continue
|
| 394 |
+
emb_matrix = getattr(self, attr) # (N, D)๏ผๅทฒๅฝไธๅ
|
| 395 |
+
if emb_matrix is None:
|
| 396 |
+
continue
|
| 397 |
+
|
| 398 |
+
# ่ฏฅ layer ๅจๆๆ query ไธญ็ๆๅคง้
้ข๏ผๅฐๆฐ query ไผ็ฎๅฐๅคไฝ็ hit๏ผๆๅๆๅ่ช้
้ขๆชๆญ๏ผ
|
| 399 |
+
k_max = max((cur_pvk_per_q[i].get(ln, 1) for i in range(len(queries))), default=1)
|
| 400 |
+
k_max = min(k_max, emb_matrix.shape[0])
|
| 401 |
+
|
| 402 |
+
scores = q_emb @ emb_matrix.T # (Q, N)
|
| 403 |
+
top_v, top_i = scores.topk(k_max, dim=1) # (Q, k_max)
|
| 404 |
+
top_v_list = top_v.tolist()
|
| 405 |
+
top_i_list = top_i.tolist()
|
| 406 |
+
|
| 407 |
+
for i, source_word in enumerate(queries):
|
| 408 |
+
cur_weights = query_weights[i]
|
| 409 |
+
kq = cur_pvk_per_q[i].get(ln, 1)
|
| 410 |
+
layer_w = cur_weights.get(ln, 1.0)
|
| 411 |
+
row_v = top_v_list[i]
|
| 412 |
+
row_i = top_i_list[i]
|
| 413 |
+
# ไป
ๅ่ฏฅ query ่ชๅทฑ็้
้ขๆกๆฐ
|
| 414 |
+
for j in range(min(kq, len(row_v))):
|
| 415 |
+
score = row_v[j]
|
| 416 |
+
if score < 0.35:
|
| 417 |
+
# topk ๅทฒๆๅๆฐ้ๅบ๏ผๅ็ปญ้ฝไฝไบ้ๅผ๏ผๅฏๆๅ็ปๆ
|
| 418 |
+
break
|
| 419 |
+
idx = row_i[j]
|
| 420 |
+
cat_text = CAT_MAP.get(self._arr_category[idx], 'Other')
|
| 421 |
+
if cat_text not in target_cats:
|
| 422 |
+
continue
|
| 423 |
+
tag_name = self._arr_name[idx]
|
| 424 |
+
count = self._arr_post_count[idx]
|
| 425 |
+
pop_score = self._arr_pop_score[idx]
|
| 426 |
+
final_score = score * layer_w * (1 - w_pop) + pop_score * w_pop
|
| 427 |
+
if tag_name not in final or final_score > final[tag_name].final_score:
|
| 428 |
+
final[tag_name] = TagResult(
|
| 429 |
+
tag=tag_name, cn_name=self._arr_cn_name[idx], category=cat_text,
|
| 430 |
+
nsfw=self._arr_nsfw[idx],
|
| 431 |
+
final_score=round(float(final_score), 4),
|
| 432 |
+
semantic_score=round(float(score), 4),
|
| 433 |
+
count=int(count), source=source_word, layer=ln,
|
| 434 |
+
wiki=self._arr_wiki[idx],
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
# โโ ๅ
จๅฅ่ฏญไนไธ่ดๆง่ฝฏ้ๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 438 |
+
# ๅฏนๆฏไธชๅ้ๆ ็ญพ๏ผ่ฎก็ฎๅ
ถไธๅฎๆดๅๅงๆฅ่ฏข๏ผ่้ๅ่ฏ็ๆฎต๏ผ็่ฏญไน็ธไผผๅบฆ๏ผ
|
| 439 |
+
# ๅฐ็ธไผผๅบฆไฝไธบ่ฝฏๅ ๅญไนๅ
ฅ final_score๏ผไฝฟไป
็ฑๅ่ฏ็ข็ๅน้
ๅฐ็ๅชๅฃฐ
|
| 440 |
+
# ๆ ็ญพ่ช็ถไธๆฒ๏ผๅๆถไธ็กฌ่ฟๆปคไปปไฝ็ปๆใ
|
| 441 |
+
full_q = q_emb[0] # queries[0] ๅง็ปไธบๅฎๆดๅๅงๆฅ่ฏข
|
| 442 |
+
alpha = 0.3 if SearchRequest.use_segmentation else 0 # ไธ่ดๆง่ฐ่ๅผบๅบฆ๏ผ0=ไธ่ฐ่, 1=ๅฎๅ
จๆไธ่ดๆง้ๆ๏ผ๏ผไป
ๅจๅฏ็จๅ่ฏๆถๆๆไน
|
| 443 |
+
for r in final.values():
|
| 444 |
+
idx = self._name_to_idx[r.tag]
|
| 445 |
+
max_co = 0.0
|
| 446 |
+
for ln, attr, _ in _LAYER_SPEC:
|
| 447 |
+
if ln not in tl:
|
| 448 |
+
continue
|
| 449 |
+
max_co = max(max_co, float(torch.dot(full_q, getattr(self, attr)[idx])))
|
| 450 |
+
# coherence=0 ๆถๆๅคๆฃ alpha=15%๏ผcoherence=1 ๆถไธๆฃๅ
|
| 451 |
+
r.final_score = round(r.final_score * (1.0 - alpha + alpha * max_co), 4)
|
| 452 |
+
|
| 453 |
+
# ๆถ้ๆฏไธชๆฅ่ฏขๆบ็ top-1 ็ปๆ๏ผ้ซไบ้ๅผ๏ผ
|
| 454 |
+
guaranteed_tags: set[str] = set()
|
| 455 |
+
for source_word in queries:
|
| 456 |
+
best: TagResult | None = None
|
| 457 |
+
for r in final.values():
|
| 458 |
+
if r.source == source_word and r.final_score > 0.45:
|
| 459 |
+
if best is None or r.final_score > best.final_score:
|
| 460 |
+
best = r
|
| 461 |
+
if best is not None:
|
| 462 |
+
guaranteed_tags.add(best.tag)
|
| 463 |
+
|
| 464 |
+
# ๅฏนๆๆๅ้่ฟ่กๆๅบ๏ผ็ถๅๅจไฟ็ไฟ่ฏ็ปๆ็ๅๆถๆชๆญ่ณ้ๅถๆฐ้
|
| 465 |
+
sorted_results = sorted(final.values(), key=lambda r: r.final_score, reverse=True)
|
| 466 |
+
valid: list[TagResult] = []
|
| 467 |
+
for r in sorted_results:
|
| 468 |
+
if r.final_score <= 0.45:
|
| 469 |
+
continue
|
| 470 |
+
if len(valid) < request.limit or r.tag in guaranteed_tags:
|
| 471 |
+
valid.append(r)
|
| 472 |
+
|
| 473 |
+
tags_all = ', '.join(r.tag for r in valid)
|
| 474 |
+
tags_sfw = ', '.join(r.tag for r in valid if r.nsfw != '1')
|
| 475 |
+
response = SearchResponse(
|
| 476 |
+
tags_all=tags_all, tags_sfw=tags_sfw,
|
| 477 |
+
results=valid, keywords=keywords, segments=extra_segments,
|
| 478 |
+
)
|
| 479 |
+
self._search_cache.put(cache_key, response)
|
| 480 |
+
return response
|
| 481 |
+
|
| 482 |
+
# โโ ๅ
จ้ๆๅปบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 483 |
+
|
| 484 |
+
def _build_full(self) -> None:
|
| 485 |
+
print(f'[Engine] ๅ
จ้่ฏปๅ {self.csv_path} ...')
|
| 486 |
+
raw_df = self._read_csv_robust(self.csv_path)
|
| 487 |
+
self.df = self._preprocess_raw_df(raw_df)
|
| 488 |
+
self.max_log_count = float(np.log1p(self.df['post_count'].max()))
|
| 489 |
+
self._encode_all_and_save()
|
| 490 |
+
|
| 491 |
+
def _encode_all_and_save(self) -> None:
|
| 492 |
+
print('[Engine] ๅ
จ้็ผ็ ...')
|
| 493 |
+
for _, attr, col in _LAYER_SPEC:
|
| 494 |
+
setattr(self, attr, self._encode_texts(self.df[col].tolist()))
|
| 495 |
+
self._save_cache()
|
| 496 |
+
|
| 497 |
+
# โโ ๅข้ๆดๆฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 498 |
+
|
| 499 |
+
def _smart_update(self) -> None:
|
| 500 |
+
print('[Engine] ๆฃๆฅๅข้ๅๆด...')
|
| 501 |
+
t0 = time.time()
|
| 502 |
+
|
| 503 |
+
raw_df = self._read_csv_robust(self.csv_path)
|
| 504 |
+
new_df = self._preprocess_raw_df(raw_df)
|
| 505 |
+
|
| 506 |
+
_SIG_COLS = ['cn_name', 'wiki', 'cn_core']
|
| 507 |
+
|
| 508 |
+
def _sig(df: pd.DataFrame, iloc_idx: int) -> tuple:
|
| 509 |
+
row = df.iloc[iloc_idx]
|
| 510 |
+
return tuple(str(row.get(c, '')) for c in _SIG_COLS)
|
| 511 |
+
|
| 512 |
+
cached_idx: dict[str, int] = {n: i for i, n in enumerate(self.df['name'])}
|
| 513 |
+
new_idx: dict[str, int] = {n: i for i, n in enumerate(new_df['name'])}
|
| 514 |
+
|
| 515 |
+
added_names = [n for n in new_idx if n not in cached_idx]
|
| 516 |
+
deleted_names = [n for n in cached_idx if n not in new_idx]
|
| 517 |
+
changed_names = [
|
| 518 |
+
n for n in new_idx
|
| 519 |
+
if n in cached_idx and _sig(new_df, new_idx[n]) != _sig(self.df, cached_idx[n])
|
| 520 |
+
]
|
| 521 |
+
|
| 522 |
+
if not added_names and not deleted_names and not changed_names:
|
| 523 |
+
print('[Engine] ๆฐๆฎๅทฒๆฏๆๆฐ๏ผๆ ้ๆดๆฐใ')
|
| 524 |
+
return
|
| 525 |
+
|
| 526 |
+
print(f'[Engine] ๅๆด โ ๆฐๅข: {len(added_names)} ไฟฎๆน: {len(changed_names)} ๅ ้ค: {len(deleted_names)}')
|
| 527 |
+
|
| 528 |
+
if deleted_names:
|
| 529 |
+
keep_mask = ~self.df['name'].isin(set(deleted_names))
|
| 530 |
+
keep_pos = [i for i, v in enumerate(keep_mask) if v]
|
| 531 |
+
self.df = self.df[keep_mask].reset_index(drop=True)
|
| 532 |
+
for _, attr, _ in _LAYER_SPEC:
|
| 533 |
+
setattr(self, attr, getattr(self, attr)[keep_pos])
|
| 534 |
+
cached_idx = {n: i for i, n in enumerate(self.df['name'])}
|
| 535 |
+
|
| 536 |
+
if changed_names:
|
| 537 |
+
changed_rows = new_df[new_df['name'].isin(set(changed_names))].reset_index(drop=True)
|
| 538 |
+
_vecs = {attr: self._encode_texts(changed_rows[col].tolist()) for _, attr, col in _LAYER_SPEC}
|
| 539 |
+
for j, name in enumerate(changed_rows['name']):
|
| 540 |
+
ci = cached_idx[name]
|
| 541 |
+
for _, attr, _ in _LAYER_SPEC:
|
| 542 |
+
getattr(self, attr)[ci] = _vecs[attr][j]
|
| 543 |
+
for col in changed_rows.columns:
|
| 544 |
+
self.df.at[ci, col] = changed_rows.at[j, col]
|
| 545 |
+
|
| 546 |
+
if added_names:
|
| 547 |
+
added_rows = new_df[new_df['name'].isin(set(added_names))].reset_index(drop=True)
|
| 548 |
+
for _, attr, col in _LAYER_SPEC:
|
| 549 |
+
vecs = self._encode_texts(added_rows[col].tolist())
|
| 550 |
+
setattr(self, attr, torch.cat([getattr(self, attr), vecs], dim=0))
|
| 551 |
+
self.df = pd.concat([self.df, added_rows], ignore_index=True)
|
| 552 |
+
|
| 553 |
+
self.max_log_count = float(np.log1p(self.df['post_count'].max()))
|
| 554 |
+
self._name_to_idx = {n: i for i, n in enumerate(self.df['name'])}
|
| 555 |
+
self._rebuild_arrays_from_df()
|
| 556 |
+
self._normalize_embeddings()
|
| 557 |
+
self._save_cache()
|
| 558 |
+
print(f'[Engine] ๅข้ๆดๆฐๅฎๆ๏ผ่ๆถ {time.time() - t0:.2f}s๏ผๅ
ฑ {len(self.df)} ๆก๏ผ')
|
| 559 |
+
|
| 560 |
+
# โโ ็ผๅญ I/O โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 561 |
+
|
| 562 |
+
def _save_cache(self) -> None:
|
| 563 |
+
self.paths.ensure_dir()
|
| 564 |
+
st_save(
|
| 565 |
+
{attr: getattr(self, attr).half() for _, attr, _ in _LAYER_SPEC},
|
| 566 |
+
str(self.paths.embeddings),
|
| 567 |
+
)
|
| 568 |
+
save_cols = ['name', 'cn_name', 'cn_core', 'wiki', 'nsfw', 'category', 'post_count']
|
| 569 |
+
self.df[save_cols].to_parquet(str(self.paths.metadata), index=False)
|
| 570 |
+
|
| 571 |
+
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 572 |
+
with open(self.paths.meta_json, 'w', encoding='utf-8') as f:
|
| 573 |
+
json.dump({
|
| 574 |
+
'schema_version': SCHEMA_VERSION,
|
| 575 |
+
'updated_at': current_time,
|
| 576 |
+
}, f, ensure_ascii=False, indent=4)
|
| 577 |
+
|
| 578 |
+
print(f'[Engine] ็ผๅญไฟๅญๅฎๆ๏ผ{len(self.df)} ๆก่ฎฐๅฝ๏ผ๏ผ็ๆๆถ้ด: {current_time}')
|
| 579 |
+
|
| 580 |
+
def _load_from_cache(self) -> None:
|
| 581 |
+
tensors = st_load(str(self.paths.embeddings), device=self.device)
|
| 582 |
+
for _, attr, _ in _LAYER_SPEC:
|
| 583 |
+
setattr(self, attr, tensors[attr].float())
|
| 584 |
+
self.df = pd.read_parquet(str(self.paths.metadata))
|
| 585 |
+
self.max_log_count = float(np.log1p(self.df['post_count'].max()))
|
| 586 |
+
|
| 587 |
+
def _cached_schema_version(self) -> int:
|
| 588 |
+
try:
|
| 589 |
+
with open(self.paths.meta_json, 'r', encoding='utf-8') as f:
|
| 590 |
+
return int(json.load(f).get('schema_version', 1))
|
| 591 |
+
except Exception:
|
| 592 |
+
return 0
|
| 593 |
+
|
| 594 |
+
# โโ ็ผ็ & ้ขๅค็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 595 |
+
|
| 596 |
+
def _encode_texts(self, texts: list[str]) -> torch.Tensor:
|
| 597 |
+
return self.model.encode(
|
| 598 |
+
texts, batch_size=64, show_progress_bar=False, convert_to_tensor=True,
|
| 599 |
+
).float()
|
| 600 |
+
|
| 601 |
+
def _load_model(self) -> None:
|
| 602 |
+
if self.model is not None:
|
| 603 |
+
return
|
| 604 |
+
print(f'[Engine] ๅ ่ฝฝๆจกๅ (path={self.model_path}, device={self.device})...')
|
| 605 |
+
try:
|
| 606 |
+
self.model = SentenceTransformer(self.model_path, device=self.device)
|
| 607 |
+
except Exception as e:
|
| 608 |
+
print(f'[Engine] ๆๅฎ่ทฏๅพๅ ่ฝฝๅคฑ่ดฅ๏ผๅฐ่ฏ้ๆฐ่งฃๆ: {e}')
|
| 609 |
+
fallback = resolve_model_path()
|
| 610 |
+
self.model = SentenceTransformer(fallback, device=self.device)
|
| 611 |
+
|
| 612 |
+
def _read_csv_robust(self, path: str) -> pd.DataFrame:
|
| 613 |
+
for enc in ['utf-8', 'gbk', 'gb18030']:
|
| 614 |
+
try:
|
| 615 |
+
return pd.read_csv(path, dtype=str, encoding=enc).fillna('')
|
| 616 |
+
except UnicodeDecodeError:
|
| 617 |
+
continue
|
| 618 |
+
raise ValueError('CSV ่ฏปๅๅคฑ่ดฅ๏ผ่ฏทๆฃๆฅ็ผ็ ')
|
| 619 |
+
|
| 620 |
+
def _preprocess_raw_df(self, df: pd.DataFrame) -> pd.DataFrame:
|
| 621 |
+
df = df.copy()
|
| 622 |
+
df.dropna(subset=['name'], inplace=True)
|
| 623 |
+
df = df[df['name'].str.strip() != '']
|
| 624 |
+
for col in ['cn_name', 'category', 'wiki', 'nsfw']:
|
| 625 |
+
if col not in df.columns:
|
| 626 |
+
df[col] = ''
|
| 627 |
+
df['category'] = df['category'].fillna('0')
|
| 628 |
+
df['nsfw'] = df['nsfw'].fillna('0')
|
| 629 |
+
for char in ['๏ผ', '|', 'ใ']:
|
| 630 |
+
df['cn_name'] = df['cn_name'].str.replace(char, ',', regex=False)
|
| 631 |
+
if 'post_count' not in df.columns:
|
| 632 |
+
df['post_count'] = 0
|
| 633 |
+
df['post_count'] = pd.to_numeric(df['post_count'], errors='coerce').fillna(0)
|
| 634 |
+
df['cn_name'] = df['cn_name'].fillna('')
|
| 635 |
+
df['wiki'] = df['wiki'].fillna('')
|
| 636 |
+
df['cn_core'] = df['cn_name'].str.split(',', n=1).str[0].str.strip().fillna('')
|
| 637 |
+
df.drop_duplicates(subset=['name'], inplace=True)
|
| 638 |
+
df.reset_index(drop=True, inplace=True)
|
| 639 |
+
return df
|
| 640 |
+
|
| 641 |
+
def _setup_jieba_from_memory(self) -> None:
|
| 642 |
+
if self.df is None:
|
| 643 |
+
return
|
| 644 |
+
unique_words: set[str] = set()
|
| 645 |
+
for text in self.df['cn_name'].dropna().astype(str):
|
| 646 |
+
for part in text.replace(',', ' ').split():
|
| 647 |
+
part = part.strip()
|
| 648 |
+
if len(part) > 1:
|
| 649 |
+
unique_words.add(part)
|
| 650 |
+
for word in unique_words:
|
| 651 |
+
jieba.add_word(word, 2000)
|
| 652 |
+
|
| 653 |
+
def _detect_intent(self, query: str) -> dict[str, float]:
|
| 654 |
+
"""
|
| 655 |
+
ๆ นๆฎๆฅ่ฏข่ฏ็นๅพ่ฟๅๅ่งๅพ็่ฏญไนๅ็ณปๆฐใ
|
| 656 |
+
็ณปๆฐ > 1 ่กจ็คบๅ ๆ๏ผ< 1 ่กจ็คบ้ๆใ
|
| 657 |
+
"""
|
| 658 |
+
stripped = query.replace(' ', '')
|
| 659 |
+
cn_chars = sum(1 for c in stripped if '\u4e00' <= c <= '\u9fff')
|
| 660 |
+
en_chars = sum(1 for c in stripped if c.isascii() and c.isalpha())
|
| 661 |
+
total = max(len(stripped), 1)
|
| 662 |
+
is_long = len(query) > 8
|
| 663 |
+
is_cn = cn_chars / total > 0.5
|
| 664 |
+
is_en = en_chars / total > 0.5
|
| 665 |
+
|
| 666 |
+
if is_long and is_cn:
|
| 667 |
+
return {'่ฑๆ': 0.8, 'ไธญๆๆ ธๅฟ่ฏ': 0.9, 'ไธญๆๆฉๅฑ่ฏ': 1.1, '้ไน': 1.4}
|
| 668 |
+
if is_long and is_en:
|
| 669 |
+
return {'่ฑๆ': 1.0, 'ไธญๆๆ ธๅฟ่ฏ': 0.8, 'ไธญๆๆฉๅฑ่ฏ': 0.9, '้ไน': 1.3}
|
| 670 |
+
if is_cn:
|
| 671 |
+
return {'่ฑๆ': 0.8, 'ไธญๆๆ ธๅฟ่ฏ': 1.3, 'ไธญๆๆฉๅฑ่ฏ': 1.1, '้ไน': 0.6}
|
| 672 |
+
if is_en:
|
| 673 |
+
return {'่ฑๆ': 1.3, 'ไธญๆๆ ธๅฟ่ฏ': 1.0, 'ไธญๆๆฉๅฑ่ฏ': 0.8, '้ไน': 0.6}
|
| 674 |
+
return {'่ฑๆ': 1.0, 'ไธญๆๆ ธๅฟ่ฏ': 1.0, 'ไธญๆๆฉๅฑ่ฏ': 1.0, '้ไน': 1.0}
|
| 675 |
+
|
| 676 |
+
def _smart_split(self, text: str) -> tuple[list[str], list[str]]:
|
| 677 |
+
"""ๅฐๆฅ่ฏขๆๆฌๆๅไธบๅ
ณ้ฎ่ฏๅ่กจ๏ผๅๆถ่ฟๅๅ้ๅ็ๅๅง็ๆฎตใ
|
| 678 |
+
|
| 679 |
+
ไธคๅฑๅๅ๏ผ
|
| 680 |
+
1. ้ฆๅ
ๆๅ้็ฌฆๅๅโโ็ฉบๆ ผใๆข่กใไธญๆ้ๅท/้กฟๅท/ๅๅท/ๅฅๅทๅ่งไธบ
|
| 681 |
+
็จๆทๆพๅผๆๅฎ็ๆฆๅฟต่พน็๏ผไธญๆ่ช็ถ่ฏญๅฅไธไฝฟ็จ่ฟไบ็ฌฆๅทๆฅๅ้ๆฆๅฟต๏ผใ
|
| 682 |
+
2. ่ฅๅชๆไธไธช็ๆฎต๏ผๆ ๆพๅผๅ้๏ผ๏ผๅฎๅ
จ่ตฐๅๆ jieba ้ป่พใ
|
| 683 |
+
่ฅๆๅค็ๆฎต๏ผๆฏไธช็บฏ CJK ็ๆฎตๆ้ฟๅบฆๅณๅฎ๏ผ
|
| 684 |
+
- โค _ATOMIC_CJK_MAX_LEN ๅญ โ ๅๅญๆฆๅฟต๏ผ็ดๆฅไฟ็
|
| 685 |
+
- > _ATOMIC_CJK_MAX_LEN ๅญ โ ่ตฐ jieba ๅๅ๏ผ้ฟ็ญ่ฏญ/็ญๅฅ๏ผ
|
| 686 |
+
ๆททๅๆๆฌ๏ผๅซๆ ็น/่ฑๆ๏ผ็ๆฎตๅง็ป่ตฐๅๆ้ป่พใ
|
| 687 |
+
|
| 688 |
+
Returns:
|
| 689 |
+
(tokens, raw_segments):
|
| 690 |
+
- tokens: ๅค็ๅ็ๅ
ณ้ฎ่ฏๅ่กจ๏ผๅๅญๆฆๅฟตๆ jieba ๅๅ็ปๆ๏ผ
|
| 691 |
+
- raw_segments: ๅ้็ฌฆๅๅๅ็ๅๅง็ๆฎต๏ผๆช็ป jieba๏ผ๏ผ็จไบๅค็ฒๅบฆๆฅ่ฏข
|
| 692 |
+
"""
|
| 693 |
+
segments = [s.strip() for s in re.split(r'[\s\n\r๏ผใ๏ผใ]+', text) if s.strip()]
|
| 694 |
+
if not segments:
|
| 695 |
+
return [], []
|
| 696 |
+
|
| 697 |
+
tokens: list[str] = []
|
| 698 |
+
# ๅไธ็ๆฎต โ ๆ ๆพๅผๅ้๏ผๅฎๅ
จ่ตฐๅๆ jieba ้ป่พ
|
| 699 |
+
if len(segments) == 1:
|
| 700 |
+
segment = segments[0]
|
| 701 |
+
for chunk in re.split(r'([ไธ-้พฅ]+)', segment):
|
| 702 |
+
if not chunk.strip():
|
| 703 |
+
continue
|
| 704 |
+
if re.match(r'[ไธ-้พฅ]+', chunk):
|
| 705 |
+
tokens.extend(jieba.cut(chunk))
|
| 706 |
+
else:
|
| 707 |
+
cleaned = re.sub(r'[,()\[\]{}:]', ' ', chunk)
|
| 708 |
+
for part in cleaned.split():
|
| 709 |
+
try:
|
| 710 |
+
float(part)
|
| 711 |
+
except ValueError:
|
| 712 |
+
tokens.append(part)
|
| 713 |
+
return tokens, segments
|
| 714 |
+
|
| 715 |
+
# ๅค็ๆฎต โ ๆฏไธชๅ้็็ๆฎตๆ้ฟๅบฆๅณๅฎๆฏๅฆๅๅญไฟ็
|
| 716 |
+
for segment in segments:
|
| 717 |
+
# ็บฏ CJK ็ๆฎต
|
| 718 |
+
if re.match(r'^[ไธ-้พฅ]+$', segment):
|
| 719 |
+
if len(segment) <= _ATOMIC_CJK_MAX_LEN:
|
| 720 |
+
tokens.append(segment) # ็ญ โ ๅๅญๆฆๅฟต
|
| 721 |
+
else:
|
| 722 |
+
tokens.extend(jieba.cut(segment)) # ้ฟ โ jieba ๅๅ
|
| 723 |
+
continue
|
| 724 |
+
# ๆททๅๆๆฌ โ ๅๆ jieba ๅๅ้ป่พ
|
| 725 |
+
for chunk in re.split(r'([ไธ-้พฅ]+)', segment):
|
| 726 |
+
if not chunk.strip():
|
| 727 |
+
continue
|
| 728 |
+
if re.match(r'[ไธ-้พฅ]+', chunk):
|
| 729 |
+
tokens.extend(jieba.cut(chunk))
|
| 730 |
+
else:
|
| 731 |
+
cleaned = re.sub(r'[,()\[\]{}:]', ' ', chunk)
|
| 732 |
+
for part in cleaned.split():
|
| 733 |
+
try:
|
| 734 |
+
float(part)
|
| 735 |
+
except ValueError:
|
| 736 |
+
tokens.append(part)
|
| 737 |
+
return tokens, segments
|
| 738 |
+
|
| 739 |
+
# โโ ๅ
ณ่ๆจ่ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 740 |
+
|
| 741 |
+
def get_related(
|
| 742 |
+
self,
|
| 743 |
+
seed_tags: list[str],
|
| 744 |
+
exclude: set[str] | None = None,
|
| 745 |
+
limit: int = 20,
|
| 746 |
+
show_nsfw: bool = True,
|
| 747 |
+
) -> list:
|
| 748 |
+
from .models import RelatedTag
|
| 749 |
+
import math
|
| 750 |
+
|
| 751 |
+
if not self.cooc or not seed_tags:
|
| 752 |
+
return []
|
| 753 |
+
exclude = exclude or set()
|
| 754 |
+
|
| 755 |
+
related_key = (tuple(sorted(seed_tags)), tuple(sorted(exclude)), limit, show_nsfw)
|
| 756 |
+
cached = self._related_cache.get(related_key)
|
| 757 |
+
if cached is not None:
|
| 758 |
+
return cached
|
| 759 |
+
|
| 760 |
+
# ไผฐ็ฎ่ฏญๆๅบๆปๅคงๅฐ N๏ผๅๆฐๆฎ้ไธญๅๅธ้็ๆๅคงๅผ๏ผๅนถ่ฎพ็ฝฎๅ็ไธ้
|
| 761 |
+
total_posts = float(max(self.df['post_count'].max(), 7000000.0))
|
| 762 |
+
|
| 763 |
+
npmi_scores: dict[str, float] = {}
|
| 764 |
+
total_cooc: dict[str, int] = {}
|
| 765 |
+
tag_sources: dict[str, list[str]] = {}
|
| 766 |
+
name_to_idx = self._name_to_idx
|
| 767 |
+
arr_post_count = self._arr_post_count
|
| 768 |
+
|
| 769 |
+
for seed in seed_tags:
|
| 770 |
+
if seed not in name_to_idx:
|
| 771 |
+
continue
|
| 772 |
+
|
| 773 |
+
seed_count = float(arr_post_count[name_to_idx[seed]] or 1)
|
| 774 |
+
|
| 775 |
+
for neighbor, cnt in self.cooc.get(seed, []):
|
| 776 |
+
if neighbor in exclude or neighbor == seed:
|
| 777 |
+
continue
|
| 778 |
+
if neighbor not in name_to_idx:
|
| 779 |
+
continue
|
| 780 |
+
|
| 781 |
+
neighbor_count = float(arr_post_count[name_to_idx[neighbor]] or 1)
|
| 782 |
+
|
| 783 |
+
cooc = min(float(cnt), seed_count, neighbor_count)
|
| 784 |
+
if cooc <= 0:
|
| 785 |
+
continue
|
| 786 |
+
|
| 787 |
+
# ่ฎก็ฎๅๅญ๏ผ(Cooc * N) / (Count(A) * Count(B))
|
| 788 |
+
numerator = (cooc * total_posts) / (seed_count * neighbor_count)
|
| 789 |
+
|
| 790 |
+
# ๅฟฝ็ฅ่ด็ธๅ
ณๆๅฎๅ
จไธ็ธๅ
ณ็่ฏๆก
|
| 791 |
+
if numerator <= 1.0:
|
| 792 |
+
continue
|
| 793 |
+
|
| 794 |
+
pmi = math.log(numerator)
|
| 795 |
+
|
| 796 |
+
# ่ฎก็ฎๅๆฏ๏ผ-log(P(A, B))
|
| 797 |
+
p_a_b = cooc / total_posts
|
| 798 |
+
if p_a_b >= 1.0:
|
| 799 |
+
npmi = 1.0
|
| 800 |
+
else:
|
| 801 |
+
npmi = pmi / -math.log(p_a_b)
|
| 802 |
+
|
| 803 |
+
# ๅค่ฏๆกๆ็ดขๆถ็ดฏๅ NPMI
|
| 804 |
+
npmi_scores[neighbor] = npmi_scores.get(neighbor, 0.0) + npmi
|
| 805 |
+
total_cooc[neighbor] = total_cooc.get(neighbor, 0) + cnt
|
| 806 |
+
tag_sources.setdefault(neighbor, []).append(seed)
|
| 807 |
+
|
| 808 |
+
if not npmi_scores:
|
| 809 |
+
return []
|
| 810 |
+
|
| 811 |
+
# ๅฝไธๅ็จไบๅ็ซฏๅฑ็คบ
|
| 812 |
+
max_score = max(npmi_scores.values())
|
| 813 |
+
|
| 814 |
+
sorted_candidates = sorted(npmi_scores.items(), key=lambda x: x[1], reverse=True)
|
| 815 |
+
results = []
|
| 816 |
+
|
| 817 |
+
for tag_name, raw_score in sorted_candidates:
|
| 818 |
+
if len(results) >= limit:
|
| 819 |
+
break
|
| 820 |
+
|
| 821 |
+
idx = name_to_idx[tag_name]
|
| 822 |
+
nsfw = self._arr_nsfw[idx]
|
| 823 |
+
if nsfw == '1' and not show_nsfw:
|
| 824 |
+
continue
|
| 825 |
+
|
| 826 |
+
cat = CAT_MAP.get(self._arr_category[idx], 'Other')
|
| 827 |
+
|
| 828 |
+
results.append(RelatedTag(
|
| 829 |
+
tag=tag_name,
|
| 830 |
+
cn_name=str(self._arr_cn_name[idx]),
|
| 831 |
+
category=cat,
|
| 832 |
+
nsfw=nsfw,
|
| 833 |
+
cooc_count=total_cooc[tag_name],
|
| 834 |
+
cooc_score=round(raw_score / max_score, 4),
|
| 835 |
+
sources=tag_sources.get(tag_name, []),
|
| 836 |
+
post_count=int(self._arr_post_count[idx]),
|
| 837 |
+
wiki=str(self._arr_wiki[idx]) if self._arr_wiki is not None else '',
|
| 838 |
+
))
|
| 839 |
+
|
| 840 |
+
self._related_cache.put(related_key, results)
|
| 841 |
+
return results
|
| 842 |
+
|
| 843 |
+
def _load_cooc(self) -> None:
|
| 844 |
+
csv_path = Path(self.cooc_file)
|
| 845 |
+
parquet_path = csv_path.with_suffix('.parquet')
|
| 846 |
+
|
| 847 |
+
if parquet_path.is_file() and (
|
| 848 |
+
not csv_path.is_file()
|
| 849 |
+
or parquet_path.stat().st_mtime >= csv_path.stat().st_mtime
|
| 850 |
+
):
|
| 851 |
+
read_path = parquet_path
|
| 852 |
+
is_parquet = True
|
| 853 |
+
elif csv_path.is_file():
|
| 854 |
+
read_path = csv_path
|
| 855 |
+
is_parquet = False
|
| 856 |
+
else:
|
| 857 |
+
print(f'[Engine] ๆชๆพๅฐๅ
ฑ็ฐ่กจ ({self.cooc_file})๏ผๅ
ณ่ๆจ่ๅ่ฝไธๅฏ็จใ')
|
| 858 |
+
return
|
| 859 |
+
|
| 860 |
+
print(f'[Engine] ๅ ่ฝฝๅ
ฑ็ฐ่กจ ({read_path.name})...')
|
| 861 |
+
t0 = time.time()
|
| 862 |
+
try:
|
| 863 |
+
if is_parquet:
|
| 864 |
+
df = pd.read_parquet(str(read_path))
|
| 865 |
+
else:
|
| 866 |
+
df = self._read_csv_robust(str(read_path))
|
| 867 |
+
df['count'] = pd.to_numeric(df['count'], errors='coerce').fillna(0).astype(int)
|
| 868 |
+
df.to_parquet(str(parquet_path), index=False)
|
| 869 |
+
print(f'[Engine] ๅทฒๅฐๅ
ฑ็ฐ่กจ็ผๅญไธบ {parquet_path.name}๏ผไธๆฌกๅฏๅจๅฐ็ดๆฅๅ ่ฝฝใ')
|
| 870 |
+
|
| 871 |
+
tag_a = df['tag_a'].astype(str).to_numpy()
|
| 872 |
+
tag_b = df['tag_b'].astype(str).to_numpy()
|
| 873 |
+
counts = df['count'].astype(int).to_numpy()
|
| 874 |
+
|
| 875 |
+
src = np.concatenate([tag_a, tag_b])
|
| 876 |
+
dst = np.concatenate([tag_b, tag_a])
|
| 877 |
+
cnt = np.concatenate([counts, counts])
|
| 878 |
+
|
| 879 |
+
sort_idx = np.lexsort((-cnt, src))
|
| 880 |
+
src = src[sort_idx]
|
| 881 |
+
dst = dst[sort_idx]
|
| 882 |
+
cnt = cnt[sort_idx]
|
| 883 |
+
|
| 884 |
+
unique_srcs, first_pos = np.unique(src, return_index=True)
|
| 885 |
+
end_pos = np.append(first_pos[1:], len(src))
|
| 886 |
+
|
| 887 |
+
cooc: dict[str, list[tuple[str, int]]] = {}
|
| 888 |
+
for s, start, end in zip(unique_srcs, first_pos, end_pos):
|
| 889 |
+
cooc[s] = list(zip(dst[start:end].tolist(), cnt[start:end].tolist()))
|
| 890 |
+
|
| 891 |
+
self.cooc = cooc
|
| 892 |
+
print(
|
| 893 |
+
f'[Engine] ๅ
ฑ็ฐ่กจๅ ่ฝฝๅฎๆ๏ผ{len(cooc):,} ไธช tag๏ผ'
|
| 894 |
+
f'่ๆถ {time.time() - t0:.2f}s'
|
| 895 |
+
)
|
| 896 |
+
except Exception as e:
|
| 897 |
+
print(f'[Engine] ๅ
ฑ็ฐ่กจๅ ่ฝฝๅคฑ่ดฅ: {e}')
|
core/models.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
core/models.py
|
| 3 |
+
ๆฐๆฎ็ปๆ
|
| 4 |
+
"""
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
from dataclasses import dataclass, field
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class TagResult:
|
| 10 |
+
"""ๅๆกๆ ็ญพๆ็ดข็ปๆใ"""
|
| 11 |
+
tag: str
|
| 12 |
+
cn_name: str
|
| 13 |
+
category: str
|
| 14 |
+
nsfw: str
|
| 15 |
+
final_score: float
|
| 16 |
+
semantic_score: float
|
| 17 |
+
count: int
|
| 18 |
+
source: str
|
| 19 |
+
layer: str
|
| 20 |
+
wiki: str = ""
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class RelatedTag:
|
| 25 |
+
"""ๅๆกๅ
ณ่ๆจ่็ปๆ"""
|
| 26 |
+
tag: str
|
| 27 |
+
cn_name: str
|
| 28 |
+
category: str
|
| 29 |
+
nsfw: str
|
| 30 |
+
cooc_count: int # ็ดฏ่ฎกๅ
ฑ็ฐๆฌกๆฐ
|
| 31 |
+
cooc_score: float # ๅฝไธๅๅๆฐ
|
| 32 |
+
sources: list[str] = field(default_factory=list) # ่งฆๅ่ฏฅๆจ่็็งๅญ tag ๅ่กจ
|
| 33 |
+
post_count: int = 0 # ่ฏฅๆ ็ญพ็ๅๅธๆปๆฐ๏ผไธ search_tags ็ count ๅฏน้ฝ๏ผ
|
| 34 |
+
wiki: str = "" # ๆ ็ญพ wiki ๆ่ฟฐ
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass
|
| 38 |
+
class SearchRequest:
|
| 39 |
+
"""ๆ็ดขๅๆฐ"""
|
| 40 |
+
query: str
|
| 41 |
+
top_k: int = 5
|
| 42 |
+
limit: int = 80
|
| 43 |
+
popularity_weight: float = 0.15
|
| 44 |
+
show_nsfw: bool = True
|
| 45 |
+
use_segmentation: bool = True
|
| 46 |
+
target_layers: list[str] = field(
|
| 47 |
+
default_factory=lambda: ['่ฑๆ', 'ไธญๆๆฉๅฑ่ฏ', '้ไน', 'ไธญๆๆ ธๅฟ่ฏ']
|
| 48 |
+
)
|
| 49 |
+
target_categories: list[str] = field(
|
| 50 |
+
default_factory=lambda: ['General', 'Character', 'Copyright']
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@dataclass
|
| 55 |
+
class SearchResponse:
|
| 56 |
+
"""ๆ็ดข่ฟๅๅผใ"""
|
| 57 |
+
tags_all: str
|
| 58 |
+
tags_sfw: str
|
| 59 |
+
results: list[TagResult]
|
| 60 |
+
keywords: list[str]
|
| 61 |
+
segments: list[str] = field(default_factory=list) # ๅ้็ฌฆๅๅๅ็ๅๅงไปๅฅ็บง็ๆฎต
|
mcp_server.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
mcp_server.py
|
| 3 |
+
โโโโโโโโโโโโโ
|
| 4 |
+
MCP ๆๅกๅฑ
|
| 5 |
+
|
| 6 |
+
ๆ่ฝฝๆนๅผ๏ผๅจ ui_nicegui.py ไธญ๏ผ๏ผ
|
| 7 |
+
from mcp_server import mcp
|
| 8 |
+
app.mount('/mcp', mcp.streamable_http_app())
|
| 9 |
+
|
| 10 |
+
ๆฅๅ
ฅๅฐๅ๏ผ
|
| 11 |
+
https://sakizuki-danboorusearch.hf.space/mcp/mcp
|
| 12 |
+
|
| 13 |
+
ๆฏๆ็ๅทฅๅ
ท๏ผ
|
| 14 |
+
search_tags ่ช็ถ่ฏญ่จๆ็ดขๆ ็ญพ
|
| 15 |
+
get_related_tags ๅบไบๅ
ฑ็ฐ่กจๆฅๅ
ณ่ๆจ่
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import json
|
| 19 |
+
import asyncio
|
| 20 |
+
from mcp.server.fastmcp import FastMCP
|
| 21 |
+
from mcp.server.transport_security import TransportSecuritySettings
|
| 22 |
+
from core.engine import DanbooruTagger
|
| 23 |
+
from core.models import SearchRequest
|
| 24 |
+
import core.counter as counter
|
| 25 |
+
import re
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
mcp = FastMCP(
|
| 29 |
+
name="danbooru-searcher",
|
| 30 |
+
transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False),
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@mcp.tool()
|
| 35 |
+
async def search_tags(
|
| 36 |
+
query: str,
|
| 37 |
+
use_segmentation: bool = True,
|
| 38 |
+
top_k: int = 5,
|
| 39 |
+
limit: int = 80,
|
| 40 |
+
popularity_weight: float = 0.15,
|
| 41 |
+
show_nsfw: bool = True,
|
| 42 |
+
include_wiki: bool = False,
|
| 43 |
+
category: str = "all",
|
| 44 |
+
) -> str:
|
| 45 |
+
"""
|
| 46 |
+
Search Danbooru tags using natural language and return a ready-to-use prompt.
|
| 47 |
+
|
| 48 |
+
## Args
|
| 49 |
+
- query: Natural language description (Chinese recommended).
|
| 50 |
+
- use_segmentation: Split multi-concept input into segments for separate retrieval. True for scene descriptions, False for single-concept queries.
|
| 51 |
+
- top_k: Candidates recalled per segment. Semantics change with use_segmentation โ see guide below.
|
| 52 |
+
- limit: Max tags returned.
|
| 53 |
+
- popularity_weight: Influence of tag post count on ranking (0.0โ1.0). Default 0.15.
|
| 54 |
+
- show_nsfw: Include NSFW tags. Default True.
|
| 55 |
+
- include_wiki: Append wiki description to each result. Default False.
|
| 56 |
+
- category: Filter results to a specific tag category. Default "all".
|
| 57 |
+
"all" โ All categories (้็จ + ็ๆ + ไบบ็ฉ )
|
| 58 |
+
"general" โ General: visual attributes, clothing, pose, background, etc.
|
| 59 |
+
"copyright" โ Copyright: specific anime/game/franchise titles
|
| 60 |
+
"character" โ Character: named characters from any series
|
| 61 |
+
Use this when you know what kind of tag you need โ e.g. looking for a
|
| 62 |
+
character name vs. describing a scene visually.
|
| 63 |
+
|
| 64 |
+
## Query writing guide
|
| 65 |
+
|
| 66 |
+
The `query` parameter supports explicit delimiter control for precise segmentation.
|
| 67 |
+
|
| 68 |
+
### Explicit delimiters
|
| 69 |
+
|
| 70 |
+
Use **spaces, newlines, Chinese commas (๏ผ), or Chinese dunhao (ใ)** to manually separate concepts.
|
| 71 |
+
Each delimiter-bounded segment โค7 characters stays atomic โ the engine respects your intent.
|
| 72 |
+
|
| 73 |
+
| Query style | Example | When to use |
|
| 74 |
+
|---|---|---|
|
| 75 |
+
| Concept list (spaces) | `่ฟๅจ็คพๅข ๆ ก้ ๆฏ่ต ่ฟๅจไผ` | You know the exact concepts to search |
|
| 76 |
+
| Concept list (dun hao) | `ๅไนๆ้ฆใ่ตๅๆๅ
ใ่ธๆฑฝๆๅ
` | Same, with Chinese list punctuation |
|
| 77 |
+
| Natural sentence | `ไธไธช็ฉฟ็็ฝ่ฒๆฐดๆๆ็ๅฐๅฅณๅจ้จไธญๅฅ่ท` | Scene description, let the engine auto-split |
|
| 78 |
+
| Mixed | `่ฟๅจ็คพๅข ไธไธช็ฉฟๆฐดๆๆ็ๅฐๅฅณ` | Mix concepts with descriptive phrases |
|
| 79 |
+
|
| 80 |
+
Segments >7 characters are still auto-split by jieba, but the raw segment is kept as an additional query
|
| 81 |
+
to preserve clause-level semantics (multi-granularity retrieval).
|
| 82 |
+
|
| 83 |
+
### Recommendations
|
| 84 |
+
|
| 85 |
+
1. **Concept lists โ use explicit delimiters:** Group independent concepts with spaces or dunhao.
|
| 86 |
+
`่ฟๅจ็คพๅข ๆ ก้ ๆฏ่ต ไฝ่ฒ็ฅญ ็ฐๅพ้จ` is better than `่ฟๅจ็คพๅขๆ ก้ๆฏ่ตไฝ่ฒ็ฅญ็ฐๅพ้จ`.
|
| 87 |
+
|
| 88 |
+
2. **Scene descriptions โ write naturally:** Natural Chinese with Chinese commas works well for full scenes.
|
| 89 |
+
`ไธไธช็ฉฟ็็ฝ่ฒๆฐดๆๆ๏ผ่่ฒ็ญ่ฃ็ๅฐๅฅณๅจ้จไธญๅฅ่ท` โ commas here are grammatical, not delimiters.
|
| 90 |
+
|
| 91 |
+
3. **Precise lookup โ turn off segmentation:** For finding a specific character or copyright title,
|
| 92 |
+
set `use_segmentation=False` and combine with `category` filter.
|
| 93 |
+
e.g. `query="EVAไธญ่ๅ็้ถๅทๆบ้ฉพ้ฉถๅ"` with `category="character"` and `use_segmentation=False`.
|
| 94 |
+
|
| 95 |
+
4. **Category filtering:** Use `category` to narrow results. Looking for a character?
|
| 96 |
+
`category="character"`. Building a scene prompt? `category="general"`.
|
| 97 |
+
|
| 98 |
+
## Parameter guide
|
| 99 |
+
|
| 100 |
+
### Step 1 โ Decide use_segmentation + top_k together
|
| 101 |
+
|
| 102 |
+
top_k means "candidates per segment"; its effect depends on whether segmentation is on.
|
| 103 |
+
|
| 104 |
+
Multi-concept input (scene description) โ use_segmentation=True
|
| 105 |
+
|
| 106 |
+
| Sub-scenario | top_k | Reason |
|
| 107 |
+
|-----------------------|-------|----------------------------------------------------------|
|
| 108 |
+
| Full scene โ prompt | 5 | Many segments; low top_k distributes result slots fairly |
|
| 109 |
+
| Vague concept explore | 80 | Few segments; high top_k needed for broad recall |
|
| 110 |
+
|
| 111 |
+
Single-concept input โ use_segmentation=False
|
| 112 |
+
|
| 113 |
+
top_k acts as total candidate pool size. Use 20 for all single-concept cases.
|
| 114 |
+
|
| 115 |
+
| Sub-scenario | top_k |
|
| 116 |
+
|-------------------------------|-------|
|
| 117 |
+
| Describe subject / find tag | 20 |
|
| 118 |
+
| Precise lookup / spell fix | 20 |
|
| 119 |
+
|
| 120 |
+
### Step 2 โ Decide limit independently
|
| 121 |
+
|
| 122 |
+
| Goal | limit |
|
| 123 |
+
|------------------------------|-------|
|
| 124 |
+
| Full prompt for image gen | 80 |
|
| 125 |
+
| Concept exploration | 20โ80 |
|
| 126 |
+
| Precise lookup / role search | 10โ20 |
|
| 127 |
+
|
| 128 |
+
### Auxiliary params
|
| 129 |
+
|
| 130 |
+
popularity_weight (default 0.15, rarely needs changing):
|
| 131 |
+
- Higher (0.3+): favor common, well-established tags
|
| 132 |
+
- Lower (0.0): surface niche/rare tags
|
| 133 |
+
|
| 134 |
+
include_wiki (default False):
|
| 135 |
+
- True: tag meaning matters โ disambiguation, explaining tags to user, exploring unfamiliar domains
|
| 136 |
+
- False: prompt generation (wiki irrelevant to downstream), tags already known
|
| 137 |
+
|
| 138 |
+
### Quick reference
|
| 139 |
+
|
| 140 |
+
| Scenario | use_segmentation | top_k | limit |
|
| 141 |
+
|-------------------------------|------------------|-------|-------|
|
| 142 |
+
| Full scene โ prompt (default) | True | 5 | 80 |
|
| 143 |
+
| Vague concept exploration | True | 80 | 80 |
|
| 144 |
+
| Describe subject / find tag | False | 20 | 20 |
|
| 145 |
+
| Precise lookup / spell fix | False | 20 | 10 |
|
| 146 |
+
|
| 147 |
+
### Workflow
|
| 148 |
+
|
| 149 |
+
After search_tags, pass selected tags to get_related_tags to discover complementary tags via co-occurrence (accessories, character features, scene atmosphere).
|
| 150 |
+
|
| 151 |
+
## Examples
|
| 152 |
+
|
| 153 |
+
Precise lookup / spell fix โ e.g. "selafuku", "thighhigh", "twintail"
|
| 154 |
+
โ use_segmentation=False, top_k=20, limit=10
|
| 155 |
+
|
| 156 |
+
Vague concept exploration โ e.g. "ๅ
่ณๆต", "่ตๅๆๅ
ๆ่ฃ
", "ๅ่ข"
|
| 157 |
+
โ use_segmentation=True, top_k=80, limit=80
|
| 158 |
+
|
| 159 |
+
Describe subject / find tag โ e.g. "EVAไธญ่ๅ็้ถๅทๆบ้ฉพ้ฉถๅ", "ๅฝ่ฟ็ณไน้จไธญ็ๅฉๆ"
|
| 160 |
+
โ use_segmentation=False, top_k=20, limit=20
|
| 161 |
+
|
| 162 |
+
Full scene โ prompt โ e.g. "ไธไธช็ฉฟ็็ฝ่ฒๆฐดๆๆ๏ผ่่ฒ็ญ่ฃ็ๅฐๅฅณๅจ้จไธญ็ๅๅธ้ๅฅ่ท"
|
| 163 |
+
โ use_segmentation=True, top_k=5, limit=80
|
| 164 |
+
|
| 165 |
+
## Returns
|
| 166 |
+
JSON with: prompt (comma-separated tags), keywords, results.
|
| 167 |
+
Each result: tag, cn_name, category, final_score, count[, wiki if include_wiki=True].
|
| 168 |
+
"""
|
| 169 |
+
_CATEGORY_MAP: dict[str, list[str]] = {
|
| 170 |
+
"all": ["General", "Character", "Copyright", "Artist", "Meta"],
|
| 171 |
+
"general": ["General"],
|
| 172 |
+
"character": ["Character"],
|
| 173 |
+
"copyright": ["Copyright"],
|
| 174 |
+
}
|
| 175 |
+
target_categories = _CATEGORY_MAP.get(
|
| 176 |
+
category,
|
| 177 |
+
_CATEGORY_MAP["all"], # unrecognized value โ fall back to all
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
tagger = await DanbooruTagger.get_instance()
|
| 181 |
+
request = SearchRequest(
|
| 182 |
+
query=query,
|
| 183 |
+
top_k=top_k,
|
| 184 |
+
limit=limit,
|
| 185 |
+
popularity_weight=popularity_weight,
|
| 186 |
+
show_nsfw=show_nsfw,
|
| 187 |
+
use_segmentation=use_segmentation,
|
| 188 |
+
target_categories=target_categories,
|
| 189 |
+
)
|
| 190 |
+
response = await asyncio.to_thread(tagger.search, request)
|
| 191 |
+
# ่ฎกๆฐ๏ผๆฏๆฌก MCP ๆ็ดข่ฐ็จๅ่ฎกๅ
ฅๆ็ดขใๆๅใๅคๅถ๏ผ่ฎฟ้ฎไธๅ
|
| 192 |
+
await counter.increment()
|
| 193 |
+
await counter.increment_success()
|
| 194 |
+
await counter.increment_copy()
|
| 195 |
+
await counter.increment_mcp()
|
| 196 |
+
|
| 197 |
+
results = []
|
| 198 |
+
for r in response.results:
|
| 199 |
+
if r.nsfw == '1' and not show_nsfw:
|
| 200 |
+
continue
|
| 201 |
+
item = {
|
| 202 |
+
"tag": r.tag,
|
| 203 |
+
"cn_name": r.cn_name,
|
| 204 |
+
"category": r.category,
|
| 205 |
+
"final_score": r.final_score,
|
| 206 |
+
"count": r.count,
|
| 207 |
+
}
|
| 208 |
+
if include_wiki:
|
| 209 |
+
item["wiki"] = r.wiki
|
| 210 |
+
results.append(item)
|
| 211 |
+
|
| 212 |
+
payload = {
|
| 213 |
+
"prompt": response.tags_sfw if not show_nsfw else response.tags_all,
|
| 214 |
+
"keywords": response.keywords,
|
| 215 |
+
"results": results,
|
| 216 |
+
}
|
| 217 |
+
han_chars = re.findall(r'[\u4e00-\u9fff]', query)
|
| 218 |
+
if len(query) > 0 and len(han_chars) / len(query) < 0.5:
|
| 219 |
+
payload["hint"] = (
|
| 220 |
+
"ๆฃๆตๅฐ่ฑๆๆฅ่ฏข๏ผ่ฏฅๆ็ดขๅผๆๅฏนไธญๆๆฅ่ฏขไผๅๆดๅฅฝ๏ผๅฆๆๆ็ดข็ปๆไธๅ้ขๆ๏ผๆจ่็จไธญๆ้่ฏ"
|
| 221 |
+
)
|
| 222 |
+
return json.dumps(payload, ensure_ascii=False, indent=2)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
@mcp.tool()
|
| 227 |
+
async def get_related_tags(
|
| 228 |
+
tags: list[str],
|
| 229 |
+
limit: int = 20,
|
| 230 |
+
show_nsfw: bool = True,
|
| 231 |
+
include_wiki: bool = False,
|
| 232 |
+
) -> str:
|
| 233 |
+
"""
|
| 234 |
+
Return co-occurrence-based tag recommendations for a given tag list (NPMI scoring).
|
| 235 |
+
Typical workflow: call search_tags first, then pass selected tags here to discover complementary ones.
|
| 236 |
+
|
| 237 |
+
Works well for: clothing accessories, character visual features, theme exploration, multi-tag intersections.
|
| 238 |
+
e.g. tags=["fingerless_gloves"] โ returns characters often wearing them
|
| 239 |
+
e.g. tags=["amiya_(arknights)"] โ returns outfit, expression, accessory tags
|
| 240 |
+
that define this character's appearance
|
| 241 |
+
e.g. tags=["fighter_jet"] โ returns aircraft types, action, background tags
|
| 242 |
+
e.g. tags=["maid", "twintails"] โ returns tags specific to the
|
| 243 |
+
"maid + twintails" combination, not just each tag individually
|
| 244 |
+
|
| 245 |
+
Args:
|
| 246 |
+
tags: List of Danbooru tag names, e.g. ["white_serafuku", "sailor_collar"].
|
| 247 |
+
limit: Maximum number of recommendations returned.
|
| 248 |
+
show_nsfw: Whether to include NSFW tags. Defaults to True.
|
| 249 |
+
include_wiki: Append wiki description to each result. Default False.
|
| 250 |
+
|
| 251 |
+
Returns:
|
| 252 |
+
A JSON string containing recommended tags sorted by NPMI co-occurrence score.
|
| 253 |
+
Each result: tag, cn_name, category, count, cooc_score, sources[, wiki if include_wiki=True].
|
| 254 |
+
"""
|
| 255 |
+
tagger = await DanbooruTagger.get_instance()
|
| 256 |
+
results = await asyncio.to_thread(
|
| 257 |
+
tagger.get_related,
|
| 258 |
+
tags,
|
| 259 |
+
set(tags),
|
| 260 |
+
limit,
|
| 261 |
+
show_nsfw,
|
| 262 |
+
)
|
| 263 |
+
# ่ฎกๆฐ๏ผๆฏๆฌก MCP related ่ฐ็จๅ่ฎกๅ
ฅๆ็ดขใๆๅใๅคๅถ๏ผ่ฎฟ้ฎไธๅ
|
| 264 |
+
await counter.increment()
|
| 265 |
+
await counter.increment_success()
|
| 266 |
+
await counter.increment_copy()
|
| 267 |
+
await counter.increment_mcp()
|
| 268 |
+
|
| 269 |
+
output = []
|
| 270 |
+
for r in results:
|
| 271 |
+
item = {
|
| 272 |
+
"tag": r.tag,
|
| 273 |
+
"cn_name": r.cn_name,
|
| 274 |
+
"category": r.category,
|
| 275 |
+
"count": r.post_count,
|
| 276 |
+
"cooc_score": r.cooc_score,
|
| 277 |
+
"sources": r.sources,
|
| 278 |
+
}
|
| 279 |
+
if include_wiki:
|
| 280 |
+
item["wiki"] = r.wiki
|
| 281 |
+
output.append(item)
|
| 282 |
+
|
| 283 |
+
return json.dumps(output, ensure_ascii=False, indent=2)
|
origin_database/tags_enhanced.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
platform_utils.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
platform_utils.py
|
| 3 |
+
|
| 4 |
+
็ปไธ็ๅนณๅฐๆฃๆตไธ Hub ๆไฝๅฐ่ฃ
ใ
|
| 5 |
+
|
| 6 |
+
ๆฏๆๅนณๅฐ๏ผ
|
| 7 |
+
- HuggingFace Space
|
| 8 |
+
- ModelScope ๅ็ฉบ้ด๏ผ้ญๆญ๏ผ
|
| 9 |
+
- ๆฌๅฐๅผๅ็ฏๅข
|
| 10 |
+
|
| 11 |
+
ๅฏนๅคๆด้ฒ๏ผ
|
| 12 |
+
PLATFORM : Literal['hf', 'ms', 'local']
|
| 13 |
+
is_cloud() : bool
|
| 14 |
+
get_host_port() : tuple[str, int]
|
| 15 |
+
download_file() : ไธ่ฝฝๅไธชๆไปถ๏ผ่ฟๅๆฌๅฐ่ทฏๅพ
|
| 16 |
+
upload_bytes() : ไธไผ bytes ๅฐ OSS๏ผ็จไบ่ฎกๆฐๅจๆไน
ๅ๏ผ
|
| 17 |
+
read_bytes() : ไป OSS ่ฏปๅๆไปถๅ
ๅฎน๏ผ่ฟๅ bytes | None
|
| 18 |
+
get_counter_cfg() : ่ฟๅ CounterConfig๏ผplatform / available๏ผ
|
| 19 |
+
|
| 20 |
+
็ฏๅขๅ้็บฆๅฎ๏ผ
|
| 21 |
+
|
| 22 |
+
HuggingFace Space๏ผ็ฑ HF ่ชๅจๆณจๅ
ฅ๏ผ
|
| 23 |
+
SPACE_ID Space ๅฏไธๆ ่ฏ๏ผๅญๅจๅณไปฃ่กจๅจ HF ็ฏๅข
|
| 24 |
+
SPACE_AUTHOR_NAME ไฝ่
ๅ
|
| 25 |
+
|
| 26 |
+
็จๆทๆๅจ้
็ฝฎ๏ผHF Secrets๏ผ๏ผ
|
| 27 |
+
HF_TOKEN HF ่ฎฟ้ฎไปค็๏ผไป
็จไบ download_file๏ผ้่ฎกๆฐๅจ๏ผ
|
| 28 |
+
|
| 29 |
+
ModelScope ๅ็ฉบ้ด๏ผ็ฑ้ญๆญ่ชๅจๆณจๅ
ฅ๏ผ
|
| 30 |
+
MODELSCOPE_ENVIRONMENT ๅญๅจๅณไปฃ่กจๅจ้ญๆญ็ฏๅข๏ผๅผ้ๅธธไธบ "studio"๏ผ
|
| 31 |
+
STUDIO_ID ๅ็ฉบ้ด ID๏ผๅค็จๆฃๆต๏ผ
|
| 32 |
+
|
| 33 |
+
้ญๆญๅนณๅฐๆฐๆฎๆไปถ่ฏดๆ๏ผ
|
| 34 |
+
ๆฐๆฎๆไปถ๏ผCSV / parquet / safetensors๏ผ็ดๆฅๆพๅจๅ็ฉบ้ด studio repo
|
| 35 |
+
ไธญ๏ผๅฎนๅจๅฏๅจๆถไผ่ชๅจๅๆญฅๅฐๅทฅไฝ็ฎๅฝ๏ผdownload_file() ๅจ MS ๅนณๅฐ
|
| 36 |
+
็ดๆฅ่ฟๅๆฌๅฐ่ทฏๅพ๏ผๆ ้้ขๅค้
็ฝฎ Model repoใ
|
| 37 |
+
|
| 38 |
+
้ฟ้ไบ OSS๏ผ่ฎกๆฐๅจๅฏไธๅ็ซฏ๏ผHF ไธ MS ๅ
ฑไบซๅไธๆฐๆฎ๏ผ
|
| 39 |
+
|
| 40 |
+
OSS_ACCESS_KEY_ID RAM ๅญ่ดฆๅท AccessKey ID
|
| 41 |
+
OSS_ACCESS_KEY_SECRET RAM ๅญ่ดฆๅท AccessKey Secret
|
| 42 |
+
OSS_ENDPOINT Bucket ๆๅจๅฐๅ่็น
|
| 43 |
+
ไพ: oss-cn-hangzhou.aliyuncs.com
|
| 44 |
+
๏ผๆ ้ๅ https://๏ผไปฃ็ ่ชๅจๆผๆฅ๏ผ
|
| 45 |
+
OSS_BUCKET_NAME Bucket ๅ็งฐ
|
| 46 |
+
OSS_COUNTER_DIR ่ฎกๆฐๆไปถๅจ Bucket ไธญ็ๅ็ผ็ฎๅฝ๏ผๅฏ้๏ผ
|
| 47 |
+
้ป่ฎค "danbooru_counter"
|
| 48 |
+
ๆ็ป่ทฏๅพ: {OSS_COUNTER_DIR}/count.json
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
from __future__ import annotations
|
| 52 |
+
|
| 53 |
+
import os
|
| 54 |
+
import time
|
| 55 |
+
from dataclasses import dataclass
|
| 56 |
+
from pathlib import Path
|
| 57 |
+
import oss2
|
| 58 |
+
from typing import Literal, Optional
|
| 59 |
+
|
| 60 |
+
# ๅนณๅฐๆฃๆต
|
| 61 |
+
|
| 62 |
+
def _detect_platform() -> Literal['hf', 'ms', 'local']:
|
| 63 |
+
if os.environ.get('SPACE_ID'):
|
| 64 |
+
return 'hf'
|
| 65 |
+
if os.environ.get('MODELSCOPE_ENVIRONMENT') or os.environ.get('STUDIO_ID'):
|
| 66 |
+
return 'ms'
|
| 67 |
+
return 'local'
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
PLATFORM: Literal['hf', 'ms', 'local'] = _detect_platform()
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def is_cloud() -> bool:
|
| 74 |
+
"""ๆฏๅฆ่ฟ่กๅจไปปๆไบ็ซฏๅนณๅฐใ"""
|
| 75 |
+
return PLATFORM in ('hf', 'ms')
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def get_host_port() -> tuple[str, int]:
|
| 79 |
+
"""
|
| 80 |
+
่ฟๅ NiceGUI ๅบไฝฟ็จ็ (host, port)ใ
|
| 81 |
+
HF ๅ้ญๆญๅ็ฉบ้ด้ฝไฝฟ็จ 0.0.0.0:7860๏ผๆฌๅฐไฝฟ็จ 127.0.0.1:1111ใ
|
| 82 |
+
"""
|
| 83 |
+
if is_cloud():
|
| 84 |
+
return '0.0.0.0', 7860
|
| 85 |
+
return '127.0.0.1', 11111
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def nsfw_allowed() -> bool:
|
| 89 |
+
"""
|
| 90 |
+
่ฟๅๅฝๅๅนณๅฐๆฏๅฆๅ
่ฎธ็จๆทๅผๅฏ NSFW ๆพ็คบใ
|
| 91 |
+
้ญๆญ๏ผMS๏ผๅนณๅฐ็ฆ็จ NSFW๏ผๅ
ถไฝๅนณๅฐ้ป่ฎคๅ
่ฎธใ
|
| 92 |
+
ๅฆ้ๅจไปปๆๅนณๅฐๅผบๅถ็ฆ็จ๏ผๅฏ่ฎพ็ฝฎ็ฏๅขๅ้ DISABLE_NSFW=1ใ
|
| 93 |
+
"""
|
| 94 |
+
if os.environ.get('DISABLE_NSFW', '0') == '1':
|
| 95 |
+
return False
|
| 96 |
+
return PLATFORM != 'ms'
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# ้ฟ้ไบ OSS
|
| 100 |
+
|
| 101 |
+
def _get_oss_bucket():
|
| 102 |
+
"""
|
| 103 |
+
ไป็ฏๅขๅ้่ฏปๅ OSS ้
็ฝฎ๏ผ่ฟๅ oss2.Bucket ๅฏน่ฑกใ
|
| 104 |
+
่ฅ็ฏๅขๅ้ไธๅฎๆดๆ oss2 ๆชๅฎ่ฃ
ๅ่ฟๅ Noneใ
|
| 105 |
+
"""
|
| 106 |
+
ak = os.environ.get('OSS_ACCESS_KEY_ID')
|
| 107 |
+
sk = os.environ.get('OSS_ACCESS_KEY_SECRET')
|
| 108 |
+
ep = os.environ.get('OSS_ENDPOINT')
|
| 109 |
+
bkt = os.environ.get('OSS_BUCKET_NAME')
|
| 110 |
+
if not all([ak, sk, ep, bkt]):
|
| 111 |
+
return None
|
| 112 |
+
try:
|
| 113 |
+
import oss2
|
| 114 |
+
auth = oss2.Auth(ak, sk)
|
| 115 |
+
endpoint = ep if ep.startswith('http') else f'https://{ep}'
|
| 116 |
+
return oss2.Bucket(auth, endpoint, bkt)
|
| 117 |
+
except ImportError:
|
| 118 |
+
print('[PlatformUtils] oss2 ๆชๅฎ่ฃ
๏ผOSS ่ฎกๆฐๅจไธๅฏ็จใ่ฏท pip install oss2ใ')
|
| 119 |
+
return None
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _oss_key(filename: str) -> str:
|
| 123 |
+
"""ๅฐ filename ๆผไธๅฏ้็ๅ็ผ็ฎๅฝ๏ผๅพๅฐ OSS Object Keyใ"""
|
| 124 |
+
prefix = os.environ.get('OSS_COUNTER_DIR', 'danbooru_counter').rstrip('/')
|
| 125 |
+
return f'{prefix}/{filename}'
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _oss_available() -> bool:
|
| 129 |
+
"""ๆฃๆต OSS ๅ้กน็ฏๅขๅ้ๆฏๅฆๅๅทฒ่ฎพ็ฝฎไธ oss2 ๅฏๅฏผๅ
ฅใ"""
|
| 130 |
+
return _get_oss_bucket() is not None
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# ่ฎกๆฐๅจ้
็ฝฎ
|
| 134 |
+
|
| 135 |
+
@dataclass
|
| 136 |
+
class CounterConfig:
|
| 137 |
+
platform: Literal['oss', 'local']
|
| 138 |
+
|
| 139 |
+
@property
|
| 140 |
+
def available(self) -> bool:
|
| 141 |
+
if self.platform == 'oss':
|
| 142 |
+
return _oss_available()
|
| 143 |
+
return False
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def get_counter_cfg() -> CounterConfig:
|
| 147 |
+
"""
|
| 148 |
+
่ฏปๅ่ฎกๆฐๅจ้
็ฝฎใ
|
| 149 |
+
้
็ฝฎไบ OSS ็ฏๅขๅ้ๅไฝฟ็จ OSS๏ผๅฆๅ้ๅไธบๆฌๅฐๆจกๅผ๏ผๆ ๆไน
ๅ๏ผใ
|
| 150 |
+
"""
|
| 151 |
+
if _oss_available():
|
| 152 |
+
return CounterConfig(platform='oss')
|
| 153 |
+
return CounterConfig(platform='local')
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
# ่ฎกๆฐๅจ่ฏปๅ๏ผOSS๏ผ
|
| 157 |
+
|
| 158 |
+
def read_bytes(filename: str, cfg: CounterConfig) -> Optional[bytes]:
|
| 159 |
+
"""
|
| 160 |
+
ไป OSS ่ฏปๅๆไปถๅ
ๅฎน๏ผ่ฟๅ bytesใ
|
| 161 |
+
ๆไปถไธๅญๅจ่ฟๅ None๏ผ็ฝ็ปๆๆ้ๅผๅธธๅไธๆๅบใ
|
| 162 |
+
"""
|
| 163 |
+
if not cfg.available:
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
bucket = _get_oss_bucket()
|
| 167 |
+
key = _oss_key(filename)
|
| 168 |
+
try:
|
| 169 |
+
import oss2
|
| 170 |
+
result = bucket.get_object(key)
|
| 171 |
+
return result.read()
|
| 172 |
+
except oss2.exceptions.NoSuchKey:
|
| 173 |
+
return None
|
| 174 |
+
except Exception as e:
|
| 175 |
+
print(f'[PlatformUtils] OSS ่ฏปๅๅคฑ่ดฅ ({key}): {e}')
|
| 176 |
+
raise
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def upload_bytes(
|
| 180 |
+
content: bytes,
|
| 181 |
+
filename: str,
|
| 182 |
+
cfg: CounterConfig,
|
| 183 |
+
commit_message: str = 'Update',
|
| 184 |
+
*,
|
| 185 |
+
retries: int = 3,
|
| 186 |
+
retry_delay: float = 1.0,
|
| 187 |
+
) -> bool:
|
| 188 |
+
"""
|
| 189 |
+
ๅฐ bytes ๅๅ
ฅ OSS ็ filename ่ทฏๅพใ
|
| 190 |
+
่ฟๅ True ่กจ็คบๆๅ๏ผFalse ่กจ็คบๅ
จ้จ้่ฏๅๅคฑ่ดฅใ
|
| 191 |
+
commit_message ๅๆฐไฟ็ไปฅๅ
ผๅฎน counter.py ็่ฐ็จ็ญพๅ๏ผOSS ไธไฝฟ็จใ
|
| 192 |
+
"""
|
| 193 |
+
if not cfg.available:
|
| 194 |
+
return False
|
| 195 |
+
|
| 196 |
+
bucket = _get_oss_bucket()
|
| 197 |
+
key = _oss_key(filename)
|
| 198 |
+
|
| 199 |
+
for attempt in range(retries):
|
| 200 |
+
try:
|
| 201 |
+
bucket.put_object(key, content)
|
| 202 |
+
return True
|
| 203 |
+
except Exception as e:
|
| 204 |
+
print(f'[PlatformUtils] OSS ไธไผ ๅคฑ่ดฅ๏ผ็ฌฌ {attempt + 1} ๆฌก๏ผ({key}): {e}')
|
| 205 |
+
if attempt < retries - 1:
|
| 206 |
+
time.sleep(retry_delay)
|
| 207 |
+
|
| 208 |
+
return False
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# ๆไปถไธ่ฝฝ๏ผๅผๆๆฐๆฎๆไปถ๏ผไธ่ฎกๆฐๅจๆ ๅ
ณ๏ผ
|
| 212 |
+
|
| 213 |
+
# ้ญๆญๅ็ฉบ้ดๅทฅไฝ็ฎๅฝ๏ผstudio repo ็ๆไปถไผ่ขซๅๆญฅๅฐๆญคๅค
|
| 214 |
+
_MS_WORKDIR = Path('/home/user/app')
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def download_file(
|
| 218 |
+
filename: str,
|
| 219 |
+
*,
|
| 220 |
+
# HF ไธ็จๅๆฐ
|
| 221 |
+
hf_repo_id: Optional[str] = None,
|
| 222 |
+
hf_repo_type: str = 'space',
|
| 223 |
+
hf_token: Optional[str] = None,
|
| 224 |
+
# MS ไธ็จๅๆฐ๏ผไฟ็็ญพๅๅ
ผๅฎนๆง๏ผ้ญๆญๅนณๅฐๅทฒไธๅไฝฟ็จ๏ผ
|
| 225 |
+
ms_repo_id: Optional[str] = None,
|
| 226 |
+
ms_token: Optional[str] = None,
|
| 227 |
+
ms_cache_dir: str = '/tmp/ms_cache',
|
| 228 |
+
) -> str:
|
| 229 |
+
"""
|
| 230 |
+
ไธ่ฝฝๅไธชๅผๆๆฐๆฎๆไปถ๏ผ่ฟๅๆฌๅฐ็ปๅฏน่ทฏๅพๅญ็ฌฆไธฒใ
|
| 231 |
+
|
| 232 |
+
HF ๅนณๅฐ๏ผ
|
| 233 |
+
ไป Space repo ไธ่ฝฝ๏ผhf_repo_id ้ป่ฎค่ฏปๅ็ฏๅขๅ้ SPACE_IDใ
|
| 234 |
+
|
| 235 |
+
MS ๅนณๅฐ๏ผ
|
| 236 |
+
ๆไปถๅทฒ้ studio repo ้จ็ฝฒๅฐๅฎนๅจๆฌๅฐ๏ผ็ดๆฅ่ฟๅๅทฅไฝ็ฎๅฝไธ็่ทฏๅพ๏ผ
|
| 237 |
+
ๆ ้้
็ฝฎไปปไฝ้ขๅค repoใ่ฅๆไปถไธๅญๅจๅๆๅบ FileNotFoundErrorใ
|
| 238 |
+
|
| 239 |
+
ๆฌๅฐ๏ผ
|
| 240 |
+
็ดๆฅ่ฟๅๅๅง่ทฏๅพใ
|
| 241 |
+
"""
|
| 242 |
+
if PLATFORM == 'hf':
|
| 243 |
+
from huggingface_hub import hf_hub_download
|
| 244 |
+
repo_id = hf_repo_id or os.environ.get('SPACE_ID')
|
| 245 |
+
if not repo_id:
|
| 246 |
+
raise RuntimeError('[PlatformUtils] HF ๅนณๅฐๆชๆพๅฐ SPACE_ID๏ผๆ ๆณไธ่ฝฝๆไปถใ')
|
| 247 |
+
return hf_hub_download(
|
| 248 |
+
repo_id=repo_id,
|
| 249 |
+
repo_type=hf_repo_type,
|
| 250 |
+
filename=filename,
|
| 251 |
+
token=hf_token or os.environ.get('HF_TOKEN'),
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
if PLATFORM == 'ms':
|
| 255 |
+
local_path = _MS_WORKDIR / filename
|
| 256 |
+
if not local_path.is_file():
|
| 257 |
+
raise FileNotFoundError(
|
| 258 |
+
f'[PlatformUtils] ้ญๆญๅนณๅฐๆฌๅฐๆไปถไธๅญๅจ: {local_path}\n'
|
| 259 |
+
f'่ฏท็กฎ่ฎคๅทฒๅฐ {filename} ๆไบคๅฐๅ็ฉบ้ด studio repo ไธญใ'
|
| 260 |
+
)
|
| 261 |
+
print(f'[PlatformUtils] MS ๆฌๅฐๆไปถ: {local_path}')
|
| 262 |
+
return str(local_path)
|
| 263 |
+
|
| 264 |
+
# ๆฌๅฐ็ฏๅข๏ผ็ดๆฅ่ฟๅๅๅง่ทฏๅพ๏ผ็ฑ่ฐ็จๆนไฟ่ฏๆไปถๅญๅจ๏ผ
|
| 265 |
+
return filename
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
# ๆจกๅ่ทฏๅพ่งฃๆ
|
| 269 |
+
|
| 270 |
+
LOCAL_MODEL_PATH = 'my_model_bge_m3'
|
| 271 |
+
HF_MODEL_ID = 'BAAI/bge-m3'
|
| 272 |
+
MS_MODEL_ID = 'BAAI/bge-m3' # ้ญๆญไธๅๅ๏ผ่ตฐๅฝๅ
่็น
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def resolve_model_path(prefer_local: Optional[str] = None) -> str:
|
| 276 |
+
"""
|
| 277 |
+
ๆไผๅ
็บง่งฃๆๆจกๅ่ทฏๅพ๏ผ
|
| 278 |
+
1. ๆฌๅฐ็ฎๅฝ๏ผprefer_local ๆ LOCAL_MODEL_PATH๏ผ
|
| 279 |
+
2. ๅฝๅๅนณๅฐ็ Hub Model ID๏ผ้ฆๆฌกไผ่ชๅจไธ่ฝฝ็ผๅญ๏ผ
|
| 280 |
+
่ฟๅๅฏ็ดๆฅไผ ็ป SentenceTransformer ็่ทฏๅพๆ model_id ๅญ็ฌฆไธฒใ
|
| 281 |
+
"""
|
| 282 |
+
local = prefer_local or LOCAL_MODEL_PATH
|
| 283 |
+
if os.path.exists(local):
|
| 284 |
+
print(f'[PlatformUtils] ไฝฟ็จๆฌๅฐๆจกๅ: {local}')
|
| 285 |
+
return local
|
| 286 |
+
|
| 287 |
+
if PLATFORM == 'ms':
|
| 288 |
+
print(f'[PlatformUtils] ้ญๆญ็ฏๅข๏ผไฝฟ็จ ModelScope Hub ๆจกๅ: {MS_MODEL_ID}')
|
| 289 |
+
try:
|
| 290 |
+
from modelscope import snapshot_download
|
| 291 |
+
cached = snapshot_download(MS_MODEL_ID, cache_dir='/tmp/ms_model')
|
| 292 |
+
print(f'[PlatformUtils] ๆจกๅๅทฒ็ผๅญ่ณ: {cached}')
|
| 293 |
+
return cached
|
| 294 |
+
except Exception as e:
|
| 295 |
+
print(f'[PlatformUtils] ModelScope snapshot_download ๅคฑ่ดฅ๏ผๅ้ๅฐ HF ID: {e}')
|
| 296 |
+
|
| 297 |
+
print(f'[PlatformUtils] ไฝฟ็จ HuggingFace Hub ๆจกๅ: {HF_MODEL_ID}')
|
| 298 |
+
return HF_MODEL_ID
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
nicegui
|
| 2 |
+
pandas
|
| 3 |
+
sentence-transformers
|
| 4 |
+
torch
|
| 5 |
+
jieba
|
| 6 |
+
pyarrow
|
| 7 |
+
safetensors
|
| 8 |
+
huggingface_hub
|
| 9 |
+
fastapi
|
| 10 |
+
uvicorn
|
| 11 |
+
modelscope
|
| 12 |
+
oss2
|
| 13 |
+
mcp==1.27.0
|
tags_embedding/version_data.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": 3,
|
| 3 |
+
"updated_at": "2026-05-02 20:19:08"
|
| 4 |
+
}
|
ui_nicegui.py
ADDED
|
@@ -0,0 +1,1381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ui_nicegui.py
|
| 3 |
+
โโโโโโโโโโโโโ
|
| 4 |
+
NiceGUI ๅ็ซฏๅฑ๏ผ้ๆ็๏ผใ
|
| 5 |
+
|
| 6 |
+
โธ ๅช่ด่ดฃๆธฒๆ / ไบคไบใ
|
| 7 |
+
โธ ่ฐ็จ core.engine.DanbooruTagger๏ผ้่ฟ core.models ็ๆฐๆฎ็ปๆ้ไฟกใ
|
| 8 |
+
โธ ไธๅ
ๅซไปปไฝ็ฎๆณ้ป่พใ
|
| 9 |
+
โธ ๅนณๅฐ็ธๅ
ณ้
็ฝฎ๏ผhost/port/ไบ็ซฏๅคๆญ๏ผ็ปไธ็ฑ platform_utils ๆไพใ
|
| 10 |
+
"""
|
| 11 |
+
import sys
|
| 12 |
+
sys.stdout.reconfigure(line_buffering=True)
|
| 13 |
+
print("[UI] ่ๆฌๅผๅงๆง่ก", flush=True)
|
| 14 |
+
import asyncio
|
| 15 |
+
import os
|
| 16 |
+
import json as _json
|
| 17 |
+
import subprocess
|
| 18 |
+
import traceback
|
| 19 |
+
from dataclasses import asdict
|
| 20 |
+
from fastapi.responses import PlainTextResponse
|
| 21 |
+
|
| 22 |
+
def _excepthook(exc_type, exc_value, exc_tb):
|
| 23 |
+
print("[UI] FATAL ERROR ON STARTUP:", flush=True)
|
| 24 |
+
traceback.print_exception(exc_type, exc_value, exc_tb)
|
| 25 |
+
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
| 26 |
+
|
| 27 |
+
sys.excepthook = _excepthook
|
| 28 |
+
|
| 29 |
+
from nicegui import ui, app, run
|
| 30 |
+
from core import counter
|
| 31 |
+
from api_fastapi import app as api_app
|
| 32 |
+
from core.engine import DanbooruTagger
|
| 33 |
+
from core.models import RelatedTag, SearchRequest
|
| 34 |
+
from platform_utils import is_cloud, get_host_port, nsfw_allowed
|
| 35 |
+
from mcp_server import mcp
|
| 36 |
+
|
| 37 |
+
import logging
|
| 38 |
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
| 39 |
+
logging.getLogger("huggingface_hub").setLevel(logging.WARNING)
|
| 40 |
+
logging.getLogger("mcp").setLevel(logging.WARNING)
|
| 41 |
+
logging.getLogger("mcp.server").setLevel(logging.WARNING)
|
| 42 |
+
logging.getLogger("fastmcp").setLevel(logging.WARNING)
|
| 43 |
+
# suppress MCP streamable-HTTP transport noise ("No response returned" from Starlette middleware)
|
| 44 |
+
class _SuppressMCPNoise(logging.Filter):
|
| 45 |
+
_MARKER = "No response returned"
|
| 46 |
+
|
| 47 |
+
def filter(self, record: logging.LogRecord) -> bool:
|
| 48 |
+
if self._MARKER in record.getMessage():
|
| 49 |
+
return False
|
| 50 |
+
if record.exc_info:
|
| 51 |
+
import traceback
|
| 52 |
+
tb_text = "".join(traceback.format_exception(*record.exc_info))
|
| 53 |
+
if self._MARKER in tb_text:
|
| 54 |
+
return False
|
| 55 |
+
return True
|
| 56 |
+
|
| 57 |
+
logging.getLogger("uvicorn.error").addFilter(_SuppressMCPNoise())
|
| 58 |
+
|
| 59 |
+
# โโ ่กจๆ ผๅๅฎไน โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 60 |
+
|
| 61 |
+
TABLE_COLUMNS = [
|
| 62 |
+
{'name': 'tag', 'label': 'ๅน้
ๆ ็ญพ', 'field': 'tag', 'align': 'left', 'sortable': True},
|
| 63 |
+
{'name': 'cn_name', 'label': 'ๅซไน', 'field': 'cn_name', 'align': 'left'},
|
| 64 |
+
{'name': 'nsfw', 'label': 'ๅ็บง', 'field': 'nsfw', 'align': 'center', 'sortable': True},
|
| 65 |
+
{'name': 'final_score', 'label': '็ปผๅๅ', 'field': 'final_score', 'sortable': True},
|
| 66 |
+
{'name': 'count', 'label': '็ญๅบฆ', 'field': 'count', 'sortable': True},
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
OPTIONAL_COLS = {
|
| 70 |
+
'semantic': {'name': 'semantic_score', 'label': '่ฏญไนๅ', 'field': 'semantic_score', 'sortable': True},
|
| 71 |
+
'layer': {'name': 'layer', 'label': 'ๅน้
ๅฑ', 'field': 'layer'},
|
| 72 |
+
'source': {'name': 'source', 'label': 'ๅน้
ๆฅๆบ', 'field': 'source'},
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
# localStorage key ไธ้
็ฝฎ็ๆฌ๏ผ็ๆฌๅๆดๆถ่ชๅจไธขๅผๆง้
็ฝฎ
|
| 76 |
+
_CONFIG_LS_KEY = 'danbooru_search_config'
|
| 77 |
+
_CONFIG_VERSION = 4
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# โโ ่พ
ๅฉๅฝๆฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 82 |
+
|
| 83 |
+
def _get_git_commit() -> str:
|
| 84 |
+
try:
|
| 85 |
+
return subprocess.check_output(
|
| 86 |
+
['git', 'rev-parse', '--short', 'HEAD'],
|
| 87 |
+
stderr=subprocess.DEVNULL,
|
| 88 |
+
text=True,
|
| 89 |
+
).strip()
|
| 90 |
+
except Exception:
|
| 91 |
+
return os.environ.get('COMMIT_SHA', 'unknown')[:7]
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def result_to_row(r, nsfw_visible: bool) -> dict:
|
| 95 |
+
d = asdict(r)
|
| 96 |
+
d['_nsfw_blocked'] = (r.nsfw == '1') and not nsfw_visible
|
| 97 |
+
return d
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def apply_nsfw_filter(rows: list[dict], show_nsfw: bool) -> list[dict]:
|
| 101 |
+
result = []
|
| 102 |
+
for row in rows:
|
| 103 |
+
r = dict(row)
|
| 104 |
+
r['_nsfw_blocked'] = (r.get('nsfw') == '1') and not show_nsfw
|
| 105 |
+
result.append(r)
|
| 106 |
+
return result
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _format_tag_with_weight(tag: str, weight: float, fmt: str = 'sdxl') -> str:
|
| 110 |
+
"""ๆ ผๅผๅๅไธชๆ ็ญพใ
|
| 111 |
+
sdxl: (tag:1.2) ๆ้ 1.0 ๆถ่พๅบ tag
|
| 112 |
+
nai: 1.2::tag:: ๆ้ 1.0 ๆถ่พๅบ tag
|
| 113 |
+
"""
|
| 114 |
+
if weight == 1.0:
|
| 115 |
+
return tag
|
| 116 |
+
if fmt == 'nai':
|
| 117 |
+
return f'{weight:.1f}::{tag}::'
|
| 118 |
+
return f'({tag}:{weight:.1f})'
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# โโ UI ็ฑป โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 122 |
+
|
| 123 |
+
class DanbooruSearchUI:
|
| 124 |
+
def __init__(self):
|
| 125 |
+
self.search_count_label = None
|
| 126 |
+
self.current_search_interacted = True
|
| 127 |
+
|
| 128 |
+
self.full_table_data: list[dict] = []
|
| 129 |
+
self.current_segments: list[str] = [] # ไปๅฅ็บงๅๅง็ๆฎต๏ผ็จไบๅบๅ chip ้ข่ฒ
|
| 130 |
+
self.current_query_str: str = ""
|
| 131 |
+
self.full_tags_str: str = ""
|
| 132 |
+
self.full_tags_str_sfw: str = ""
|
| 133 |
+
|
| 134 |
+
self.result_table = None # ๅทฆๆ ่กจๆ ผ
|
| 135 |
+
self.related_list_container = None # ๅณๆ ๅ
ณ่ๆจ่ๅ่กจ
|
| 136 |
+
self.results_section = None # ๆดไธช็ปๆๅบๅ๏ผๆ็ดขๅ้่๏ผ
|
| 137 |
+
self.selection_count_label = None
|
| 138 |
+
self.selected_display = None # ๅทฒๅบๅผ textarea๏ผไฟ็ๅ
ผๅฎน
|
| 139 |
+
self.selected_chips_container = None # ๅทฒ้ๆ ็ญพ chip ๅฎนๅจ
|
| 140 |
+
self.current_related: list = []
|
| 141 |
+
self.chip_extra_selected: set = set()
|
| 142 |
+
|
| 143 |
+
# tag -> prompt ๆ้๏ผ่ๅด [0.1, 1.9]๏ผ้ป่ฎค 1.0
|
| 144 |
+
self.tag_weights: dict[str, float] = {}
|
| 145 |
+
# ๅคๅถๆ ผๅผ๏ผ'sdxl' ๆ 'nai'
|
| 146 |
+
self.prompt_format: str = 'sdxl'
|
| 147 |
+
self.format_toggle_btn = None
|
| 148 |
+
|
| 149 |
+
self.init_banner = None
|
| 150 |
+
self.input_top_k = None
|
| 151 |
+
self.input_limit = None
|
| 152 |
+
self.input_weight = None
|
| 153 |
+
self.input_nsfw = None
|
| 154 |
+
self.input_segment = None
|
| 155 |
+
self.search_input = None
|
| 156 |
+
self.keywords_container = None
|
| 157 |
+
self.spinner = None
|
| 158 |
+
self.search_btn = None
|
| 159 |
+
|
| 160 |
+
self.selected_layers = {'่ฑๆ': True, 'ไธญๆๆฉๅฑ่ฏ': True, '้ไน': True, 'ไธญๆๆ ธๅฟ่ฏ': True}
|
| 161 |
+
self.selected_cats = {'General': True, 'Copyright': True, 'Character': True}
|
| 162 |
+
|
| 163 |
+
self.bad_case_btn = None
|
| 164 |
+
|
| 165 |
+
self.mcp_notice = None
|
| 166 |
+
self.notice_expansion = None
|
| 167 |
+
|
| 168 |
+
# ่กจๆ ผๆพ็คบ้้กนๅผๅ
ณ
|
| 169 |
+
self.sw_semantic = None
|
| 170 |
+
self.sw_layer = None
|
| 171 |
+
self.sw_source = None
|
| 172 |
+
|
| 173 |
+
# ๅ
ณ่ๆจ่็ checkbox ๅผ็จ
|
| 174 |
+
self._related_checkboxes: dict[str, ui.checkbox] = {}
|
| 175 |
+
|
| 176 |
+
# ้ซ็บง้้กนไธญๅๅฑ/็ฑปๅ็ checkbox ๅผ็จ๏ผ็จไบ restore ๆถๅๆญฅๆงไปถ็ถๆ
|
| 177 |
+
self._layer_checkboxes: dict[str, ui.checkbox] = {}
|
| 178 |
+
self._cat_checkboxes: dict[str, ui.checkbox] = {}
|
| 179 |
+
|
| 180 |
+
def _update_footer_text(self):
|
| 181 |
+
if self.search_count_label is not None:
|
| 182 |
+
try:
|
| 183 |
+
total = counter.get()
|
| 184 |
+
visits = counter.get_visits()
|
| 185 |
+
commit = _get_git_commit()
|
| 186 |
+
self.search_count_label.content = (
|
| 187 |
+
f'็ดฏ่ฎกๆ็ดข {total:,} ๆฌก | ็ดฏ่ฎก่ฎฟ้ฎ {visits:,} ๆฌก | '
|
| 188 |
+
f'<span class="font-mono text-gray-300">็ๆฌๅท: {commit}</span>'
|
| 189 |
+
f'<br>'
|
| 190 |
+
f'<a href="/api/docs" '
|
| 191 |
+
f'target="_blank" rel="noopener noreferrer" '
|
| 192 |
+
f'class="text-blue-400 hover:text-blue-600 hover:underline">ไฝฟ็จ API ๆๅก</a>'
|
| 193 |
+
f' | <a href="https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline#mcp-ๆฅๅฃ" '
|
| 194 |
+
f'target="_blank" rel="noopener noreferrer" '
|
| 195 |
+
f'class="text-blue-400 hover:text-blue-600 hover:underline">ไฝฟ็จ MCP ๆๅก</a>'
|
| 196 |
+
)
|
| 197 |
+
except AttributeError:
|
| 198 |
+
pass
|
| 199 |
+
|
| 200 |
+
def _mark_interaction(self, e=None):
|
| 201 |
+
if not self.current_search_interacted:
|
| 202 |
+
self.current_search_interacted = True
|
| 203 |
+
|
| 204 |
+
async def silent_success_update():
|
| 205 |
+
try:
|
| 206 |
+
await counter.increment_success()
|
| 207 |
+
except Exception:
|
| 208 |
+
pass
|
| 209 |
+
asyncio.create_task(silent_success_update())
|
| 210 |
+
|
| 211 |
+
# โโ ๅ้กต่พ
ๅฉ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 212 |
+
|
| 213 |
+
def _get_rows_per_page(self) -> int:
|
| 214 |
+
if self.result_table is None:
|
| 215 |
+
return 0
|
| 216 |
+
p = self.result_table.pagination
|
| 217 |
+
# pagination ๅฏ่ฝๆฏ int ๆ dict
|
| 218 |
+
if isinstance(p, dict):
|
| 219 |
+
return int(p.get('rowsPerPage', 0))
|
| 220 |
+
return int(p) if p else 0
|
| 221 |
+
|
| 222 |
+
def _set_rows_per_page(self, value: int):
|
| 223 |
+
if self.result_table is None:
|
| 224 |
+
return
|
| 225 |
+
allowed = {5, 7, 10, 15, 20, 25, 50, 0} # 0 = All
|
| 226 |
+
value = value if value in allowed else 0
|
| 227 |
+
p = self.result_table.pagination
|
| 228 |
+
if isinstance(p, dict):
|
| 229 |
+
p['rowsPerPage'] = value
|
| 230 |
+
self.result_table.pagination = p
|
| 231 |
+
else:
|
| 232 |
+
self.result_table.pagination = value
|
| 233 |
+
|
| 234 |
+
# โโ ้
็ฝฎๆไน
ๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 235 |
+
|
| 236 |
+
def _save_config(self):
|
| 237 |
+
"""ๅฐๅฝๅๆงไปถ็ถๆๅบๅๅๅนถๅๅ
ฅ localStorageใ"""
|
| 238 |
+
cfg = {
|
| 239 |
+
'version': _CONFIG_VERSION,
|
| 240 |
+
'top_k': int(self.input_top_k.value) if self.input_top_k else 10,
|
| 241 |
+
'limit': int(self.input_limit.value) if self.input_limit else 80,
|
| 242 |
+
'popularity_weight': float(self.input_weight.value) if self.input_weight else 0.15,
|
| 243 |
+
'show_nsfw': bool(self.input_nsfw.value) if self.input_nsfw else False,
|
| 244 |
+
'use_segmentation': bool(self.input_segment.value) if self.input_segment else True,
|
| 245 |
+
'selected_layers': dict(self.selected_layers),
|
| 246 |
+
'selected_cats': dict(self.selected_cats),
|
| 247 |
+
'sw_semantic': bool(self.sw_semantic.value) if self.sw_semantic else False,
|
| 248 |
+
'sw_layer': bool(self.sw_layer.value) if self.sw_layer else False,
|
| 249 |
+
'sw_source': bool(self.sw_source.value) if self.sw_source else False,
|
| 250 |
+
'prompt_format': self.prompt_format,
|
| 251 |
+
'rows_per_page': self._get_rows_per_page(),
|
| 252 |
+
'search_query': self.search_input.value if self.search_input else '',
|
| 253 |
+
'notice_expanded': bool(self.notice_expansion.value) if self.notice_expansion else True,
|
| 254 |
+
'mcp_notice_dismissed': not bool(self.mcp_notice.visible) if self.mcp_notice else False,
|
| 255 |
+
}
|
| 256 |
+
js = _json.dumps(cfg, ensure_ascii=False)
|
| 257 |
+
ui.run_javascript(f"localStorage.setItem('{_CONFIG_LS_KEY}', {_json.dumps(js)});")
|
| 258 |
+
|
| 259 |
+
async def _restore_config(self):
|
| 260 |
+
"""ไป localStorage ่ฏปๅ้
็ฝฎๅนถๆขๅคๆงไปถ็ถๆใ"""
|
| 261 |
+
try:
|
| 262 |
+
raw = await ui.run_javascript(
|
| 263 |
+
f"localStorage.getItem('{_CONFIG_LS_KEY}');",
|
| 264 |
+
timeout=5.0,
|
| 265 |
+
)
|
| 266 |
+
except Exception:
|
| 267 |
+
return
|
| 268 |
+
|
| 269 |
+
if not raw:
|
| 270 |
+
return
|
| 271 |
+
|
| 272 |
+
try:
|
| 273 |
+
cfg = _json.loads(raw)
|
| 274 |
+
except Exception:
|
| 275 |
+
return
|
| 276 |
+
|
| 277 |
+
if cfg.get('version') != _CONFIG_VERSION:
|
| 278 |
+
# ็ๆฌไธ็ฌฆ๏ผไธขๅผๆง้
็ฝฎ
|
| 279 |
+
ui.run_javascript(f"localStorage.removeItem('{_CONFIG_LS_KEY}');")
|
| 280 |
+
return
|
| 281 |
+
|
| 282 |
+
if self.input_top_k and 'top_k' in cfg:
|
| 283 |
+
self.input_top_k.set_value(cfg['top_k'])
|
| 284 |
+
if self.input_limit and 'limit' in cfg:
|
| 285 |
+
self.input_limit.set_value(cfg['limit'])
|
| 286 |
+
if self.input_weight and 'popularity_weight' in cfg:
|
| 287 |
+
self.input_weight.set_value(cfg['popularity_weight'])
|
| 288 |
+
if self.input_segment and 'use_segmentation' in cfg:
|
| 289 |
+
self.input_segment.set_value(cfg['use_segmentation'])
|
| 290 |
+
|
| 291 |
+
# NSFW๏ผไป
ๅจๅนณๅฐๅ
่ฎธๆถๆขๅค
|
| 292 |
+
if nsfw_allowed() and self.input_nsfw and 'show_nsfw' in cfg:
|
| 293 |
+
self.input_nsfw.set_value(cfg['show_nsfw'])
|
| 294 |
+
|
| 295 |
+
if 'selected_layers' in cfg:
|
| 296 |
+
for layer, val in cfg['selected_layers'].items():
|
| 297 |
+
if layer in self.selected_layers:
|
| 298 |
+
self.selected_layers[layer] = bool(val)
|
| 299 |
+
if layer in self._layer_checkboxes:
|
| 300 |
+
self._layer_checkboxes[layer].set_value(bool(val))
|
| 301 |
+
|
| 302 |
+
if 'selected_cats' in cfg:
|
| 303 |
+
for cat, val in cfg['selected_cats'].items():
|
| 304 |
+
if cat in self.selected_cats:
|
| 305 |
+
self.selected_cats[cat] = bool(val)
|
| 306 |
+
if cat in self._cat_checkboxes:
|
| 307 |
+
self._cat_checkboxes[cat].set_value(bool(val))
|
| 308 |
+
|
| 309 |
+
if self.sw_semantic and 'sw_semantic' in cfg:
|
| 310 |
+
self.sw_semantic.set_value(cfg['sw_semantic'])
|
| 311 |
+
if self.sw_layer and 'sw_layer' in cfg:
|
| 312 |
+
self.sw_layer.set_value(cfg['sw_layer'])
|
| 313 |
+
if self.sw_source and 'sw_source' in cfg:
|
| 314 |
+
self.sw_source.set_value(cfg['sw_source'])
|
| 315 |
+
|
| 316 |
+
if 'prompt_format' in cfg and cfg['prompt_format'] in ('sdxl', 'nai'):
|
| 317 |
+
self.prompt_format = cfg['prompt_format']
|
| 318 |
+
if self.format_toggle_btn:
|
| 319 |
+
if self.prompt_format == 'nai':
|
| 320 |
+
self.format_toggle_btn.text = 'NAI'
|
| 321 |
+
self.format_toggle_btn.props('color=purple-7')
|
| 322 |
+
else:
|
| 323 |
+
self.format_toggle_btn.text = 'SDXL'
|
| 324 |
+
self.format_toggle_btn.props('color=grey-7')
|
| 325 |
+
|
| 326 |
+
if 'rows_per_page' in cfg:
|
| 327 |
+
self._set_rows_per_page(cfg['rows_per_page'])
|
| 328 |
+
|
| 329 |
+
if self.search_input and cfg.get('search_query'):
|
| 330 |
+
self.search_input.set_value(cfg['search_query'])
|
| 331 |
+
|
| 332 |
+
if self.notice_expansion and 'notice_expanded' in cfg:
|
| 333 |
+
self.notice_expansion.set_value(cfg['notice_expanded'])
|
| 334 |
+
|
| 335 |
+
if self.mcp_notice and cfg.get('mcp_notice_dismissed'):
|
| 336 |
+
self.mcp_notice.set_visibility(False)
|
| 337 |
+
|
| 338 |
+
# ่ฅ้ซ็บง้้กนๅๆๅๆด๏ผๅๆญฅๆดๆฐ่กจๆ ผๅ
|
| 339 |
+
self._update_table_columns()
|
| 340 |
+
|
| 341 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 342 |
+
# ้กต้ขๆๅปบ
|
| 343 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 344 |
+
|
| 345 |
+
def build_page(self):
|
| 346 |
+
ui.colors(primary='#4A90E2', secondary='#5E6C84', accent='#FF6B6B')
|
| 347 |
+
ui.add_head_html('''
|
| 348 |
+
<meta name="description" content="ๅบไบ่ฏญไนๅน้
็ Danbooru ๆ ็ญพๆ็ดขๅผๆ๏ผๆฏๆไธญ่ฑๅ่ฏญๆ่ฟฐใๅค็ปดๅน้
ใๆบ่ฝๅ่ฏไธๅ
ฑ็ฐๅ
ณ่ๆจ่ใ">
|
| 349 |
+
<meta name="keywords" content="Danbooru, AI็ป็ป, Stable Diffusion, ๆ็คบ่ฏ, ๆ ็ญพๆ็ดข, RAG, Prompt, NovelAI">
|
| 350 |
+
<meta name="google-site-verification" content="cx4sl9Mb172GUFL556JFwKCP-pT3naQcmlMriy5B8ls" />
|
| 351 |
+
|
| 352 |
+
<style>
|
| 353 |
+
.nsfw-blur-cell { filter: blur(8px); opacity: 0.5; transition: all 0.3s ease;
|
| 354 |
+
pointer-events: none !important; user-select: none !important; }
|
| 355 |
+
.nsfw-checkbox-disabled { pointer-events: none !important; opacity: 0.3 !important; }
|
| 356 |
+
.nsfw-row-blocked { cursor: not-allowed !important; }
|
| 357 |
+
.related-item { transition: background-color 0.15s ease; }
|
| 358 |
+
.related-item:hover { background-color: rgba(74, 144, 226, 0.04); }
|
| 359 |
+
.tag-link { text-decoration: none; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; }
|
| 360 |
+
.tag-link:hover { text-decoration: underline; }
|
| 361 |
+
.weight-chip { display: inline-flex; align-items: center; gap: 2px;
|
| 362 |
+
border-radius: 16px; padding: 2px 6px 2px 4px;
|
| 363 |
+
background: #e3edf7; border: 1px solid #b3cde8;
|
| 364 |
+
font-size: 12px; margin: 3px; white-space: nowrap; }
|
| 365 |
+
.weight-chip.boosted { background: #fff3e0; border-color: #ffb74d; }
|
| 366 |
+
.weight-chip.reduced { background: #f3e5f5; border-color: #ce93d8; }
|
| 367 |
+
.weight-btn { cursor: pointer; width: 18px; height: 18px; border-radius: 50%;
|
| 368 |
+
display: inline-flex; align-items: center; justify-content: center;
|
| 369 |
+
font-size: 13px; font-weight: bold; line-height: 1;
|
| 370 |
+
border: none; background: rgba(0,0,0,0.08);
|
| 371 |
+
color: #555; transition: background 0.15s; padding: 0; }
|
| 372 |
+
.weight-btn:hover { background: rgba(0,0,0,0.18); }
|
| 373 |
+
.weight-label { font-family: Consolas, Monaco, monospace; font-size: 11px;
|
| 374 |
+
color: #888; min-width: 28px; text-align: center; }
|
| 375 |
+
|
| 376 |
+
/* ๅผบๅถๅๆ ๅนถๆ */
|
| 377 |
+
.two-col-layout {
|
| 378 |
+
display: flex !important;
|
| 379 |
+
flex-wrap: nowrap !important;
|
| 380 |
+
align-items: flex-start !important;
|
| 381 |
+
gap: 16px !important;
|
| 382 |
+
}
|
| 383 |
+
.two-col-layout > .col-left {
|
| 384 |
+
flex: 0 0 62% !important;
|
| 385 |
+
min-width: 0 !important;
|
| 386 |
+
max-width: 62% !important;
|
| 387 |
+
overflow: hidden;
|
| 388 |
+
}
|
| 389 |
+
.two-col-layout > .col-right {
|
| 390 |
+
flex: 0 0 36% !important;
|
| 391 |
+
min-width: 0 !important;
|
| 392 |
+
max-width: 36% !important;
|
| 393 |
+
overflow: hidden;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
/* ็ชๅฑๅ้ไธบไธไธๆๅ */
|
| 397 |
+
@media (max-width: 900px) {
|
| 398 |
+
.two-col-layout {
|
| 399 |
+
flex-wrap: wrap !important;
|
| 400 |
+
}
|
| 401 |
+
.two-col-layout > .col-left,
|
| 402 |
+
.two-col-layout > .col-right {
|
| 403 |
+
flex: 1 1 100% !important;
|
| 404 |
+
max-width: 100% !important;
|
| 405 |
+
}
|
| 406 |
+
}
|
| 407 |
+
</style>
|
| 408 |
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-QPB7EEPR5G"></script>
|
| 409 |
+
<script>
|
| 410 |
+
window.dataLayer = window.dataLayer || [];
|
| 411 |
+
function gtag(){dataLayer.push(arguments);}
|
| 412 |
+
gtag('js', new Date());
|
| 413 |
+
gtag('config', 'G-QPB7EEPR5G');
|
| 414 |
+
</script>
|
| 415 |
+
<script>
|
| 416 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 417 |
+
function openExternal(root) {
|
| 418 |
+
root.querySelectorAll('a[href^="http"]').forEach(function(a) {
|
| 419 |
+
a.setAttribute('target', '_blank');
|
| 420 |
+
a.setAttribute('rel', 'noopener noreferrer');
|
| 421 |
+
});
|
| 422 |
+
}
|
| 423 |
+
openExternal(document);
|
| 424 |
+
new MutationObserver(function(mutations) {
|
| 425 |
+
mutations.forEach(function(m) {
|
| 426 |
+
m.addedNodes.forEach(function(node) {
|
| 427 |
+
if (node.querySelectorAll) openExternal(node);
|
| 428 |
+
});
|
| 429 |
+
});
|
| 430 |
+
}).observe(document.body, { childList: true, subtree: true });
|
| 431 |
+
});
|
| 432 |
+
</script>
|
| 433 |
+
''')
|
| 434 |
+
|
| 435 |
+
with ui.column().classes('w-full max-w-7xl mx-auto p-4 gap-4'):
|
| 436 |
+
|
| 437 |
+
# โโ ๅๅงๅๆ็คบ โโ
|
| 438 |
+
self.init_banner = ui.card().classes(
|
| 439 |
+
'w-full bg-blue-50 border-l-4 border-blue-400'
|
| 440 |
+
)
|
| 441 |
+
with self.init_banner:
|
| 442 |
+
with ui.row().classes('items-center gap-3 p-2'):
|
| 443 |
+
ui.spinner(size='sm')
|
| 444 |
+
ui.label('ๅผๆๅๅงๅไธญ๏ผ่ฏท็จๅโฆ้ฆๆฌกๅ ่ฝฝ็บฆ้ 15 ็ง').classes('text-sm text-blue-700')
|
| 445 |
+
self.init_banner.set_visibility(not DanbooruTagger.is_ready())
|
| 446 |
+
if not DanbooruTagger.is_ready():
|
| 447 |
+
asyncio.ensure_future(self._hide_banner_when_ready())
|
| 448 |
+
|
| 449 |
+
# โโ 1. MCP ไธ็บฟ้็ฅ โโ
|
| 450 |
+
self._build_mcp_notice()
|
| 451 |
+
|
| 452 |
+
# โโ 2. ๆณจๆไบ้กน โโ
|
| 453 |
+
self._build_notice()
|
| 454 |
+
|
| 455 |
+
# โโ 2. ๆ็ดขๅก็ โโ
|
| 456 |
+
self._build_search_card()
|
| 457 |
+
|
| 458 |
+
# โโ 3~5. ็ปๆๅบๅ๏ผๆ็ดขๅ้่๏ผโโ
|
| 459 |
+
self.results_section = ui.column().classes('w-full gap-4')
|
| 460 |
+
self.results_section.set_visibility(False)
|
| 461 |
+
|
| 462 |
+
with self.results_section:
|
| 463 |
+
# โโ 3. ๅทฒ้ๆ ็ญพๆ โโ
|
| 464 |
+
self._build_selection_bar()
|
| 465 |
+
|
| 466 |
+
# โโ 4. ๅ่ฏ็ญ้ chips โโ
|
| 467 |
+
self.keywords_container = ui.row().classes('gap-2 items-center flex-wrap')
|
| 468 |
+
|
| 469 |
+
# โโ 5. ไธคๆ ็ปๆ โโ
|
| 470 |
+
self._build_results_columns()
|
| 471 |
+
|
| 472 |
+
# โโ 6. ๅบ้จ โโ
|
| 473 |
+
with ui.element('div').classes('w-full text-center py-4 mt-2'):
|
| 474 |
+
self.search_count_label = ui.html('ๆญฃๅจๅ ่ฝฝๆฐๆฎ...').classes('text-xs text-gray-400')
|
| 475 |
+
self._update_footer_text()
|
| 476 |
+
|
| 477 |
+
# โโ MCP ไธ็บฟ้็ฅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 478 |
+
|
| 479 |
+
def _build_mcp_notice(self):
|
| 480 |
+
self.mcp_notice = ui.card().classes(
|
| 481 |
+
'w-full bg-green-50 border-l-4 border-green-500 p-0 overflow-hidden'
|
| 482 |
+
)
|
| 483 |
+
with self.mcp_notice:
|
| 484 |
+
with ui.column().classes('px-4 py-3 w-full gap-1'):
|
| 485 |
+
with ui.row().classes('items-center justify-between w-full'):
|
| 486 |
+
ui.label('๐ MCP ๆๅกๆญฃๅผไธ็บฟ๏ผ').classes('text-base font-bold text-green-800')
|
| 487 |
+
ui.button(icon='close').props('flat dense round color=grey-6') \
|
| 488 |
+
.on_click(self._dismiss_mcp_notice)
|
| 489 |
+
ui.html(
|
| 490 |
+
'ๆฌ็ซ็ฐๅทฒๆฏๆ้่ฟ MCP ๅ่ฎฎๆฅๅ
ฅ AI Agent๏ผๅฆ Claude Desktop๏ผใ'
|
| 491 |
+
'ๅฆ้ไฝ้ช<b>ๆ็ฎก็</b>๏ผๅ
้
็ฝฎใๅผ็ฎฑๅณ็จ๏ผ๏ผ่ฏท่ฎฟ้ฎ '
|
| 492 |
+
'<a href="https://huggingface.co/spaces/SAkizuki/WenQiuYue" '
|
| 493 |
+
'target="_blank" rel="noopener noreferrer" '
|
| 494 |
+
'class="text-green-700 font-bold underline">้ฎ็งๆ Space</a>๏ผ'
|
| 495 |
+
'่ฟๅ
ฅๅ็นๅปใๆๆ ็ญพใๅณๅฏ่ฐ็จใ'
|
| 496 |
+
'<span class="text-gray-500 ml-1">'
|
| 497 |
+
'API ้ขๅบฆๆ้๏ผ็บฆ 30 ๅ
๏ผ๏ผ็จๅฎๅณๆญข๏ผไป
ไพไฝ้ชใ'
|
| 498 |
+
'</span>'
|
| 499 |
+
' '
|
| 500 |
+
'<a href="https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline#mcp-ๆฅๅฃ" '
|
| 501 |
+
'target="_blank" rel="noopener noreferrer" '
|
| 502 |
+
'class="text-green-700 underline">ๆฅ็ๆฅๅ
ฅๆๆกฃ โ</a>'
|
| 503 |
+
).classes('text-sm text-green-900')
|
| 504 |
+
|
| 505 |
+
def _dismiss_mcp_notice(self):
|
| 506 |
+
if self.mcp_notice:
|
| 507 |
+
self.mcp_notice.set_visibility(False)
|
| 508 |
+
self._save_config()
|
| 509 |
+
|
| 510 |
+
# โโ ๆณจๆไบ้กน โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 511 |
+
|
| 512 |
+
def _build_notice(self):
|
| 513 |
+
with ui.card().classes('w-full bg-orange-50 border-l-4 border-orange-500 p-0 overflow-hidden'):
|
| 514 |
+
with ui.expansion(value=True).classes('w-full') as notice_expansion:
|
| 515 |
+
self.notice_expansion = notice_expansion
|
| 516 |
+
notice_expansion.on('update:model-value', lambda _: self._save_config())
|
| 517 |
+
notice_expansion.add_slot('header', '''
|
| 518 |
+
<div class="flex items-center gap-2 px-4 py-2 w-full flex-wrap">
|
| 519 |
+
<span class="text-base font-bold text-orange-800">โ ๏ธ ๆณจๆไบ้กน / Note</span>
|
| 520 |
+
<span v-if="!props.expanded" class="text-sm text-gray-600 ml-1">
|
| 521 |
+
ๅฆๆ่งๅพๅฅฝ็จ๏ผ่ฏท็นๅป้กถ้จ็ปๆฌ Space ็นไธช
|
| 522 |
+
<strong>Like โค๏ธ</strong>๏ผๆๅๅพ GitHub ็นไธช <strong>Star โญ</strong>๏ผ
|
| 523 |
+
</span>
|
| 524 |
+
</div>
|
| 525 |
+
''')
|
| 526 |
+
ui.markdown("""
|
| 527 |
+
- **AI ่พ
ๅฉ**๏ผๅบไบ่ฏญไนๅน้
๏ผ็ปๆๆชๅฟ
็ปๅฏนๅ็กฎ(Results may contain errors)
|
| 528 |
+
- **ๅ
ๅฎน่ญฆๅ**๏ผๆฅๆพ็ปๆๅฏ่ฝๅ
ๅซ NSFW ๅ
ๅฎน (May include NSFW content)
|
| 529 |
+
- **ๆฃ็ดข้ๅถ**๏ผไป
ๆฏๆไธญ/่ฑๅ่ฏญๆฅๆพ ๏ผๆดๆจ่ไธญๆ(CN/EN only,CN is preferred)
|
| 530 |
+
- **ๆ ็ญพ่ๅด**๏ผไป
ๆพ็คบ็นๅพใ่ง่ฒไธไฝๅๆ ็ญพ๏ผไธ้ขๆฐ้กป โฅ 100 (General, Character & Copyright only, Freq โฅ 100)
|
| 531 |
+
- **้ๆไธๆฅๅฃ**๏ฟฝ๏ฟฝ๏ฟฝ[ComfyUI ๆไปถ](https://github.com/SuzumiyaAkizuki/ComfyUI-DanbooruSearcher) ยท [API ๆๆกฃ](/api/docs) ยท [MCP ๆฅๅ
ฅ](https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline#mcp-ๆฅๅฃ)
|
| 532 |
+
- **ๆฏๆไฝ่
**๏ผๅฆๆ่งๅพๅฅฝ็จ๏ผๆฌข่ฟ็นๅป้กถ้จ็ปๆฌ Space ็นไธช **Like โค๏ธ**๏ผๆๅๅพ [GitHub](https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline) ็นไธช **Star โญ**๏ผ
|
| 533 |
+
- **๐ ้ฆๆฌกไฝฟ็จ๏ผ[็นๅปๆฅ็ไฝฟ็จๆๅ](https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline)**๏ผไบ่งฃไบ็งๆ็ดขๆจกๅผไธ่ฟ้ถๆๅทง
|
| 534 |
+
""").classes('text-sm text-gray-800 px-4 pb-3')
|
| 535 |
+
|
| 536 |
+
# โโ ๆ็ดขๅก็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 537 |
+
|
| 538 |
+
def _build_search_card(self):
|
| 539 |
+
with ui.card().classes('w-full'):
|
| 540 |
+
with ui.row().classes('items-center gap-2 mb-2'):
|
| 541 |
+
ui.icon('search', size='2em', color='primary')
|
| 542 |
+
ui.label('Danbooru ๆ ็ญพๆจก็ณๆ็ดข').classes('text-2xl font-bold text-gray-800')
|
| 543 |
+
ui.label('ๅบไบ่ฏญไนๅน้
็ๆ ็ญพๆ็ดขๅผๆ๏ผๆฏๆๅค็ปดๅน้
ไธๅ
ฑ็ฐๅ
ณ่ๆจ่ใ').classes(
|
| 544 |
+
'text-sm text-gray-500 -mt-1 mb-3'
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
with ui.row().classes('w-full gap-3 items-stretch'):
|
| 548 |
+
self.search_input = ui.textarea(
|
| 549 |
+
placeholder='่พๅ
ฅ่ช็ถ่ฏญ่จๆ่ฟฐๆๆจก็ณๆฆๅฟต๏ผไพๅฆ๏ผไธไธช็ฉฟ็็ฝ่ฒๆฐดๆๆ็ๅฐๅฅณๅจ้จไธญๅฅ่ท...'
|
| 550 |
+
).classes('flex-grow text-base').props('outlined rows=2')
|
| 551 |
+
self.search_input.on('keydown.ctrl.enter', self.perform_search)
|
| 552 |
+
|
| 553 |
+
with ui.column().classes('justify-center'):
|
| 554 |
+
self.search_btn = ui.button(
|
| 555 |
+
'', on_click=self.perform_search, icon='search'
|
| 556 |
+
).classes('px-6 h-full min-h-16').props('unelevated color=dark')
|
| 557 |
+
with self.search_btn:
|
| 558 |
+
ui.label('ๆ็ดข').classes('text-sm mt-1')
|
| 559 |
+
self.spinner = ui.spinner(size='2em').classes('hidden')
|
| 560 |
+
|
| 561 |
+
with ui.row().classes('w-full gap-6 items-center mt-3 flex-wrap'):
|
| 562 |
+
with ui.row().classes('items-center gap-2'):
|
| 563 |
+
ui.label('Top K (่ฏญไน็ธๅ
ณ)').classes('text-sm text-gray-600')
|
| 564 |
+
self.input_top_k = ui.number(value=10, min=1, max=200).classes('w-20') \
|
| 565 |
+
.props('outlined dense')
|
| 566 |
+
|
| 567 |
+
with ui.row().classes('items-center gap-2'):
|
| 568 |
+
ui.label('็ปๆไธ้').classes('text-sm text-gray-600')
|
| 569 |
+
self.input_limit = ui.number(value=80, min=10, max=500).classes('w-20') \
|
| 570 |
+
.props('outlined dense')
|
| 571 |
+
|
| 572 |
+
with ui.row().classes('items-center gap-2'):
|
| 573 |
+
ui.label('็ญๅบฆๆ้').classes('text-sm text-gray-600')
|
| 574 |
+
self.input_weight = ui.slider(min=0.0, max=1.0, value=0.15, step=0.05).classes('w-32')
|
| 575 |
+
ui.label().bind_text_from(self.input_weight, 'value', lambda v: f"{v:.2f}") \
|
| 576 |
+
.classes('text-sm font-mono text-gray-700 w-8')
|
| 577 |
+
|
| 578 |
+
with ui.switch('ๆพ็คบ NSFW(ๆไบบ) ๅ
ๅฎน', value=False).props('color=red') as _nsfw_sw:
|
| 579 |
+
if not nsfw_allowed():
|
| 580 |
+
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
|
| 581 |
+
ui.label('NSFW ๅ
ๅฎนๅจๅฝๅๅนณๅฐไธๅฏ็จ').style('font-size:14px;')
|
| 582 |
+
self.input_nsfw = _nsfw_sw
|
| 583 |
+
if not nsfw_allowed():
|
| 584 |
+
self.input_nsfw.disable()
|
| 585 |
+
else:
|
| 586 |
+
self.input_nsfw.on('update:model-value', self.on_nsfw_toggle)
|
| 587 |
+
|
| 588 |
+
with ui.switch('ๆบ่ฝๅ่ฏ', value=True).props('color=primary') as _seg_sw:
|
| 589 |
+
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
|
| 590 |
+
ui.label('ๅ
ณ้ญๅ็ณป็ปๅฐๅชๅน้
ๅฎๆดๅฅๅญ๏ผ้็จไบ็ฒพๅๆ็ดขๆดๅฅใ').style('font-size:14px;')
|
| 591 |
+
self.input_segment = _seg_sw
|
| 592 |
+
|
| 593 |
+
with ui.expansion('้ซ็บง้้กน', icon='tune').classes('w-full mt-2'):
|
| 594 |
+
with ui.column().classes('w-full p-3 gap-4'):
|
| 595 |
+
with ui.row().classes('w-full gap-8 flex-wrap'):
|
| 596 |
+
with ui.column().classes('gap-2'):
|
| 597 |
+
ui.label('ๅน้
ๅฑ็ญ้').classes('font-bold text-sm text-gray-700')
|
| 598 |
+
display_map = {
|
| 599 |
+
'่ฑๆ': '่ฑๆๆ ็ญพ', 'ไธญๆๆฉๅฑ่ฏ': 'ไธญๆๆฉๅฑ่ฏ',
|
| 600 |
+
'้ไน': '็ปดๅบ้ไน', 'ไธญๆๆ ธๅฟ่ฏ': 'ไธญๆๆ ธๅฟ่ฏ',
|
| 601 |
+
}
|
| 602 |
+
for layer in ['่ฑๆ', 'ไธญๆๆฉๅฑ่ฏ', '้ไน', 'ไธญๆๆ ธๅฟ่ฏ']:
|
| 603 |
+
cb = ui.checkbox(
|
| 604 |
+
display_map.get(layer, layer), value=True,
|
| 605 |
+
on_change=lambda e, l=layer: self.selected_layers.__setitem__(l, e.value)
|
| 606 |
+
).props('color=primary dense')
|
| 607 |
+
self._layer_checkboxes[layer] = cb
|
| 608 |
+
|
| 609 |
+
with ui.column().classes('gap-2'):
|
| 610 |
+
ui.label('็ฑปๅ็ญ้').classes('font-bold text-sm text-gray-700')
|
| 611 |
+
color_map = {'General': 'blue', 'Copyright': 'pink', 'Character': 'green'}
|
| 612 |
+
label_map = {
|
| 613 |
+
'General': '้็จ (General)',
|
| 614 |
+
'Copyright': 'ไฝๅ (Copyright)',
|
| 615 |
+
'Character': '่ง่ฒ (Character)',
|
| 616 |
+
}
|
| 617 |
+
for cat in ['General', 'Copyright', 'Character']:
|
| 618 |
+
cb = ui.checkbox(
|
| 619 |
+
label_map[cat], value=True,
|
| 620 |
+
on_change=lambda e, c=cat: self.selected_cats.__setitem__(c, e.value)
|
| 621 |
+
).props(f'color={color_map[cat]} dense')
|
| 622 |
+
self._cat_checkboxes[cat] = cb
|
| 623 |
+
|
| 624 |
+
with ui.column().classes('gap-2'):
|
| 625 |
+
ui.label('่กจๆ ผๆพ็คบๅ').classes('font-bold text-sm text-gray-700')
|
| 626 |
+
self.sw_semantic = ui.switch('ๆพ็คบ่ฏญไนๅ', value=False)
|
| 627 |
+
self.sw_layer = ui.switch('ๆพ็คบๅน้
ๅฑ', value=False)
|
| 628 |
+
self.sw_source = ui.switch('ๆพ็คบๅน้
ๆฅๆบ', value=False)
|
| 629 |
+
self.sw_semantic.on('update:model-value', self._update_table_columns)
|
| 630 |
+
self.sw_layer.on('update:model-value', self._update_table_columns)
|
| 631 |
+
self.sw_source.on('update:model-value', self._update_table_columns)
|
| 632 |
+
|
| 633 |
+
# โโ ๅทฒ้ๆ ็ญพๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 634 |
+
|
| 635 |
+
def _build_selection_bar(self):
|
| 636 |
+
with ui.card().classes('w-full bg-blue-50 border border-blue-200'):
|
| 637 |
+
with ui.row().classes('w-full items-center justify-between'):
|
| 638 |
+
with ui.row().classes('items-center gap-2'):
|
| 639 |
+
ui.icon('check_circle', color='primary')
|
| 640 |
+
ui.label('ๅทฒ้ๆ ็ญพ').classes('font-bold text-primary')
|
| 641 |
+
self.selection_count_label = ui.label('0').classes(
|
| 642 |
+
'bg-primary text-white px-2 rounded-full text-sm')
|
| 643 |
+
with ui.icon('info_outline', size='sm', color='grey').classes('cursor-help'):
|
| 644 |
+
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
|
| 645 |
+
ui.html(
|
| 646 |
+
'็นๅป <b>โ</b> / <b>+</b> ๅฏ่ฐๆดๆ ็ญพๆ้๏ผๆญฅ้ฟ 0.1๏ผ่ๅด 0.1~1.9๏ผใ<br>'
|
| 647 |
+
'ๆ้ 1.0 ๆถ่พๅบๅๅงๆ ็ญพ๏ผๅ
ถไฝ่พๅบ <code>(tag:1.2)</code> ๆ ผๅผใ'
|
| 648 |
+
).style('font-size:14px;line-height:1.6;')
|
| 649 |
+
|
| 650 |
+
with ui.row().classes('items-center gap-2'):
|
| 651 |
+
with ui.button('ๆฒกๆๅฐ๏ผ', icon='help_outline').props('dense flat color=grey-6').classes('text-sm') as _bad_btn:
|
| 652 |
+
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
|
| 653 |
+
ui.html('็นๅปๆญคๅคไปฅๅ้ฆๅคฑ่ดฅๆกไพใ<br>ๆจ็ๆ็ดข่ฏๅฐ่ขซๅฟๅๆถ้็จไบไผๅๅผๆ๏ผไธๅ
ๅซไธชไบบ้็ง๏ผใ').style('font-size:14px;line-height:1.5;')
|
| 654 |
+
self.bad_case_btn = _bad_btn
|
| 655 |
+
self.bad_case_btn.disable()
|
| 656 |
+
self.bad_case_btn.on_click(self.report_bad_case)
|
| 657 |
+
self.format_toggle_btn = ui.button(
|
| 658 |
+
'SDXL', icon='swap_horiz'
|
| 659 |
+
).props('dense flat color=grey-7').classes('text-xs font-mono')
|
| 660 |
+
with self.format_toggle_btn:
|
| 661 |
+
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
|
| 662 |
+
ui.html(
|
| 663 |
+
'ๅๆขๅคๅถๆ ผๅผ๏ผ<br>'
|
| 664 |
+
'<b>SDXL</b>๏ผ<code>(tag:1.2)</code><br>'
|
| 665 |
+
'<b>NAI</b>๏ผ<code>1.2::tag::</code>'
|
| 666 |
+
).style('font-size:13px;line-height:1.7;')
|
| 667 |
+
self.format_toggle_btn.on_click(self._toggle_prompt_format)
|
| 668 |
+
copy_btn = ui.button('ๅคๅถ้ไธญ', icon='content_copy').props('dense unelevated color=primary')
|
| 669 |
+
copy_btn.on_click(self.copy_selection)
|
| 670 |
+
|
| 671 |
+
# chip ๅฎนๅจ๏ผๆฏไธชๅทฒ้ๆ ็ญพๆธฒๆไธบไธไธชๅธฆๅ ๅๆ้ฎ็ chip
|
| 672 |
+
self.selected_chips_container = ui.element('div').classes(
|
| 673 |
+
'w-full mt-2 min-h-10 p-1 rounded bg-white border border-blue-100 flex flex-wrap'
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
def _render_selected_chips(self):
|
| 677 |
+
"""้ๆฐๆธฒๆๅทฒ้ๆ ็ญพ็ chip ๅ่กจใ"""
|
| 678 |
+
if self.selected_chips_container is None:
|
| 679 |
+
return
|
| 680 |
+
self.selected_chips_container.clear()
|
| 681 |
+
tags = self._get_selected_tags()
|
| 682 |
+
if not tags:
|
| 683 |
+
with self.selected_chips_container:
|
| 684 |
+
ui.label('ๆๆ ๅทฒ้ๆ ็ญพ').classes('text-xs text-gray-400 italic p-2 self-center')
|
| 685 |
+
return
|
| 686 |
+
with self.selected_chips_container:
|
| 687 |
+
for tag in tags:
|
| 688 |
+
w = self.tag_weights.get(tag, 1.0)
|
| 689 |
+
extra_cls = 'boosted' if w > 1.0 else ('reduced' if w < 1.0 else '')
|
| 690 |
+
w_str = f'{w:.1f}'
|
| 691 |
+
with ui.element('div').classes(f'weight-chip {extra_cls}'):
|
| 692 |
+
# ๅ ้คๆ้ฎ๏ผร๏ผ
|
| 693 |
+
with ui.element('button').classes('weight-btn').props(f'title="็งป้ค {tag}"').on(
|
| 694 |
+
'click', lambda t=tag: self._remove_selected_tag(t)
|
| 695 |
+
):
|
| 696 |
+
ui.html('×')
|
| 697 |
+
# ๅๅท
|
| 698 |
+
with ui.element('button').classes('weight-btn').on(
|
| 699 |
+
'click', lambda t=tag: self._adjust_weight(t, -0.1)
|
| 700 |
+
):
|
| 701 |
+
ui.html('−')
|
| 702 |
+
# ๆ ็ญพๅ
|
| 703 |
+
ui.label(tag).style(
|
| 704 |
+
'font-family:Consolas,Monaco,monospace;font-size:12px;'
|
| 705 |
+
'color:#2c5282;max-width:150px;overflow:hidden;'
|
| 706 |
+
'text-overflow:ellipsis;white-space:nowrap;'
|
| 707 |
+
)
|
| 708 |
+
# ๆ้ๅผ๏ผไป
้ 1.0 ๆถๆพ็คบ๏ผ
|
| 709 |
+
if w != 1.0:
|
| 710 |
+
ui.label(w_str).classes('weight-label').style('color:#e65100;font-weight:bold;')
|
| 711 |
+
# ๅ ๅท
|
| 712 |
+
with ui.element('button').classes('weight-btn').on(
|
| 713 |
+
'click', lambda t=tag: self._adjust_weight(t, +0.1)
|
| 714 |
+
):
|
| 715 |
+
ui.html('+')
|
| 716 |
+
|
| 717 |
+
def _adjust_weight(self, tag: str, delta: float):
|
| 718 |
+
"""่ฐๆดๅไธชๆ ็ญพๆ้๏ผ้ณไฝๅฐ [0.1, 1.9]ใ"""
|
| 719 |
+
current = self.tag_weights.get(tag, 1.0)
|
| 720 |
+
new_w = round(current + delta, 1)
|
| 721 |
+
if new_w < 0.1:
|
| 722 |
+
ui.notify('ๆ้่ๅดไธบ 0.1 ~ 1.9๏ผๅทฒๅฐ่พพๆๅฐๅผ', type='warning', timeout=2000)
|
| 723 |
+
return
|
| 724 |
+
if new_w > 1.9:
|
| 725 |
+
ui.notify('ๆ้่ๅดไธบ 0.1 ~ 1.9๏ผๅทฒๅฐ่พพๆๅคงๅผ', type='warning', timeout=2000)
|
| 726 |
+
return
|
| 727 |
+
self.tag_weights[tag] = new_w
|
| 728 |
+
self._render_selected_chips()
|
| 729 |
+
|
| 730 |
+
def _remove_selected_tag(self, tag: str):
|
| 731 |
+
"""ไปๅทฒ้ไธญ็งป้คๆ ็ญพ๏ผๅๆญฅ่กจๆ ผ้ไธญ็ถๆ๏ผใ"""
|
| 732 |
+
self._mark_interaction()
|
| 733 |
+
current = self._get_selected_tags()
|
| 734 |
+
if tag in current:
|
| 735 |
+
current.remove(tag)
|
| 736 |
+
self.tag_weights.pop(tag, None)
|
| 737 |
+
self._set_selected_tags(current)
|
| 738 |
+
|
| 739 |
+
# โโ ไธคๆ ็ปๆ๏ผCSS ๅผบๅถๅนถๆ๏ผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 740 |
+
|
| 741 |
+
def _build_results_columns(self):
|
| 742 |
+
with ui.element('div').classes('w-full two-col-layout'):
|
| 743 |
+
# โโ ๅทฆๆ ๏ผ่ฏญไนๅน้
็ปๆ๏ผ่กจๆ ผ๏ผโโ
|
| 744 |
+
with ui.card().classes('col-left'):
|
| 745 |
+
with ui.row().classes('items-center justify-between mb-2 w-full'):
|
| 746 |
+
ui.label('ๅน้
ๆ ็ญพ็ปๆ').classes('font-bold text-lg text-gray-800')
|
| 747 |
+
ui.button('ๅคๅถๅ
จ้จๆ ็ญพ', icon='content_copy', on_click=self._copy_all_tags) \
|
| 748 |
+
.props('dense flat color=primary').classes('text-sm')
|
| 749 |
+
|
| 750 |
+
self.result_table = ui.table(
|
| 751 |
+
columns=TABLE_COLUMNS,
|
| 752 |
+
rows=[],
|
| 753 |
+
pagination=0,
|
| 754 |
+
selection='multiple',
|
| 755 |
+
row_key='tag',
|
| 756 |
+
).classes('w-full')
|
| 757 |
+
self.result_table.on('selection', self._update_selection_display)
|
| 758 |
+
self.result_table.on('link_click', self._mark_interaction)
|
| 759 |
+
self.result_table.on('pagination', lambda _: self._save_config())
|
| 760 |
+
|
| 761 |
+
# ่ชๅฎไน่กๆจกๆฟ๏ผ่ก่ๆฏ่ฒๆๅ็ฑป๏ผๆด่กๆฌๆตฎๆพ็คบ wiki๏ผNSFWๆจก็ณ่ก้คๅค๏ผ
|
| 762 |
+
self.result_table.add_slot('body', r'''
|
| 763 |
+
<q-tr :props="props"
|
| 764 |
+
:class="props.row._nsfw_blocked ? 'nsfw-row-blocked' : ''"
|
| 765 |
+
:style="{
|
| 766 |
+
'background-color':
|
| 767 |
+
props.row.category === 'General' ? 'rgba(59,130,246,0.06)' :
|
| 768 |
+
props.row.category === 'Character' ? 'rgba(34,197,94,0.06)' :
|
| 769 |
+
props.row.category === 'Copyright' ? 'rgba(236,72,153,0.06)' : ''
|
| 770 |
+
}">
|
| 771 |
+
<q-td auto-width>
|
| 772 |
+
<q-checkbox v-model="props.selected"
|
| 773 |
+
:class="props.row._nsfw_blocked ? 'nsfw-checkbox-disabled' : ''"/>
|
| 774 |
+
</q-td>
|
| 775 |
+
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
| 776 |
+
<template v-if="col.name === 'tag' || col.name === 'cn_name'">
|
| 777 |
+
<div :class="props.row._nsfw_blocked ? 'nsfw-blur-cell' : ''">
|
| 778 |
+
<template v-if="col.name === 'cn_name' && col.value">
|
| 779 |
+
<span style="font-size:14px">
|
| 780 |
+
{{ col.value.split(',')[0] }}
|
| 781 |
+
</span>
|
| 782 |
+
</template>
|
| 783 |
+
<template v-else-if="col.name === 'tag'">
|
| 784 |
+
<a :href="'https://danbooru.donmai.us/wiki_pages/'+col.value"
|
| 785 |
+
target="_blank"
|
| 786 |
+
class="text-primary hover:underline font-bold inline-flex items-center"
|
| 787 |
+
style="text-decoration:none; font-family: Consolas, Monaco, Courier New, monospace;"
|
| 788 |
+
@click.stop="$emit('link_click', col.value)">
|
| 789 |
+
{{ col.value }}
|
| 790 |
+
<q-icon name="open_in_new" size="xs" class="q-ml-xs opacity-50"/>
|
| 791 |
+
</a>
|
| 792 |
+
</template>
|
| 793 |
+
<template v-else>{{ col.value }}</template>
|
| 794 |
+
</div>
|
| 795 |
+
</template>
|
| 796 |
+
<template v-else-if="col.name === 'nsfw'">
|
| 797 |
+
<div v-if="col.value === '1'" class="text-red-500">๐ด</div>
|
| 798 |
+
<div v-else class="text-green-500">๐ข</div>
|
| 799 |
+
</template>
|
| 800 |
+
<template v-else-if="col.name === 'final_score'">
|
| 801 |
+
<q-badge :color="col.value > 0.6 ? 'green' : (col.value > 0.5 ? 'teal' : 'orange')">
|
| 802 |
+
{{ col.value }}
|
| 803 |
+
</q-badge>
|
| 804 |
+
</template>
|
| 805 |
+
<template v-else>{{ col.value }}</template>
|
| 806 |
+
</q-td>
|
| 807 |
+
<q-tooltip v-if="props.row.wiki && !props.row._nsfw_blocked"
|
| 808 |
+
content-class="bg-black text-white shadow-4"
|
| 809 |
+
max-width="500px" :offset="[10,10]">
|
| 810 |
+
<div style="font-size:14px;line-height:1.5;">
|
| 811 |
+
<span style="opacity:0.7;margin-right:4px;">{{
|
| 812 |
+
props.row.category === 'General' ? '[้็จ]' :
|
| 813 |
+
props.row.category === 'Character' ? '[่ง่ฒ]' :
|
| 814 |
+
props.row.category === 'Copyright' ? '[ไฝๅ]' : ''
|
| 815 |
+
}}</span>{{ props.row.wiki }}
|
| 816 |
+
</div>
|
| 817 |
+
</q-tooltip>
|
| 818 |
+
</q-tr>
|
| 819 |
+
''')
|
| 820 |
+
|
| 821 |
+
# โโ ๅณๆ ๏ผๅ
ณ่ๆจ่ โโ
|
| 822 |
+
with ui.card().classes('col-right'):
|
| 823 |
+
with ui.row().classes('items-center justify-between w-full mb-2'):
|
| 824 |
+
with ui.row().classes('items-center gap-2'):
|
| 825 |
+
ui.label('ๅ
ณ่ๆจ่').classes('font-bold text-lg text-gray-800')
|
| 826 |
+
with ui.icon('info_outline', size='sm', color='grey').classes('cursor-help'):
|
| 827 |
+
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
|
| 828 |
+
ui.html(
|
| 829 |
+
'ๅบไบๆ ็ญพๅ
ฑ็ฐๆฐๆฎ๏ผๅๆ่ฏญไนไนๅค็็ธๅ
ณๆง๏ผไธบๆจๆจ่ๆดๅคๅฏ่ฝ็ๆ ็ญพใ<br>ๅพ้ๅฏๅ ๅ
ฅๆ็งปๅบๅทฒ้ใๅฆ้ๆ นๆฎๆๆฐ้้กนๆดๆฐๆจ่๏ผ่ฏท็นๅปๅทๆฐๆ้ฎใ').style(
|
| 830 |
+
'font-size:14px;line-height:1.5;')
|
| 831 |
+
|
| 832 |
+
# ๆฐๅขๆๅจๅทๆฐๆ้ฎ
|
| 833 |
+
ui.button('ๆ นๆฎๅทฒ้ๅทๆฐ', icon='refresh', on_click=self._manual_refresh_related) \
|
| 834 |
+
.props('dense flat color=primary').classes('text-sm')
|
| 835 |
+
|
| 836 |
+
self.related_list_container = ui.column().classes('w-full gap-0')
|
| 837 |
+
with self.related_list_container:
|
| 838 |
+
ui.label('่ฏทๅ
ๆ็ดขๅนถๅพ้ๆ ็ญพโฆ').classes('text-sm text-gray-400 italic p-4')
|
| 839 |
+
|
| 840 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 841 |
+
# ๆธฒๆๅ
ณ่ๆจ่ๅ่กจ
|
| 842 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ๏ฟฝ๏ฟฝ๏ฟฝโโโโโโโโ
|
| 843 |
+
|
| 844 |
+
def _render_related_list(self, related: list, show_nsfw: bool):
|
| 845 |
+
self.related_list_container.clear()
|
| 846 |
+
self._related_checkboxes.clear()
|
| 847 |
+
|
| 848 |
+
filtered = [r for r in related if not (r.nsfw == '1' and not show_nsfw)]
|
| 849 |
+
if not filtered:
|
| 850 |
+
with self.related_list_container:
|
| 851 |
+
ui.label('ๆๆ ๆจ่').classes('text-sm text-gray-400 italic p-4')
|
| 852 |
+
return
|
| 853 |
+
|
| 854 |
+
selected_now = set(self._get_selected_tags())
|
| 855 |
+
|
| 856 |
+
with self.related_list_container:
|
| 857 |
+
for r in filtered:
|
| 858 |
+
tag = r.tag
|
| 859 |
+
cn_first = r.cn_name.split(',')[0].strip() if r.cn_name else ''
|
| 860 |
+
is_selected = tag in selected_now
|
| 861 |
+
score_pct = f'+{r.cooc_score * 100:.0f}%'
|
| 862 |
+
|
| 863 |
+
# ่ทๅ wiki
|
| 864 |
+
wiki_text = ''
|
| 865 |
+
try:
|
| 866 |
+
tagger = DanbooruTagger._instance
|
| 867 |
+
if tagger and tagger.df is not None and tag in tagger._name_to_idx:
|
| 868 |
+
idx = tagger._name_to_idx[tag]
|
| 869 |
+
wiki_text = str(tagger.df.iloc[idx].get('wiki', ''))
|
| 870 |
+
except Exception:
|
| 871 |
+
pass
|
| 872 |
+
|
| 873 |
+
sources_str = 'ใ'.join(r.sources) if r.sources else 'โ'
|
| 874 |
+
CAT_LABEL = {'General': '้็จ', 'Character': '่ง่ฒ', 'Copyright': 'ไฝๅ'}
|
| 875 |
+
cat_label = CAT_LABEL.get(r.category, '')
|
| 876 |
+
tooltip_html = ''
|
| 877 |
+
if wiki_text:
|
| 878 |
+
prefix = f'<span style="opacity:0.7;margin-right:4px;">[{cat_label}] </span>' if cat_label else ''
|
| 879 |
+
tooltip_html += f'<div style="margin-bottom:6px;">{prefix}{wiki_text}</div>'
|
| 880 |
+
tooltip_html += (
|
| 881 |
+
f'<div style="opacity:0.85;">'
|
| 882 |
+
f'{r.cn_name}<br>'
|
| 883 |
+
f'ๅ
ฑ็ฐ: {r.cooc_count:,} ็ธๅ
ณๅบฆ: {r.cooc_score:.2f}<br>'
|
| 884 |
+
f'ๆฅ่ช้ไธญ: {sources_str}'
|
| 885 |
+
f'</div>'
|
| 886 |
+
)
|
| 887 |
+
|
| 888 |
+
# ่ก่ๆฏ่ฒๆๅ็ฑปๅบๅ
|
| 889 |
+
CAT_BG = {
|
| 890 |
+
'General': 'background-color: rgba(59,130,246,0.06);', # ๆทก่
|
| 891 |
+
'Character': 'background-color: rgba(34,197,94,0.06);', # ๆทก็ปฟ
|
| 892 |
+
'Copyright': 'background-color: rgba(236,72,153,0.06);', # ๆทก็บข/็ฒ
|
| 893 |
+
}
|
| 894 |
+
row_bg = CAT_BG.get(r.category, '')
|
| 895 |
+
|
| 896 |
+
# ๆด่กๅฎนๅจ๏ผtooltip ๆๅจ่กไธ
|
| 897 |
+
with ui.row().classes(
|
| 898 |
+
'w-full items-center gap-2 px-3 py-2 related-item border-b border-gray-100'
|
| 899 |
+
).style(row_bg):
|
| 900 |
+
# ๆด่ก wiki tooltip
|
| 901 |
+
if tooltip_html:
|
| 902 |
+
with ui.tooltip().props('content-class="bg-black text-white shadow-4" max-width="500px"'):
|
| 903 |
+
ui.html(tooltip_html).style('font-size:14px;line-height:1.5;max-width:480px;')
|
| 904 |
+
|
| 905 |
+
# Checkbox
|
| 906 |
+
cb = ui.checkbox(
|
| 907 |
+
'', value=is_selected,
|
| 908 |
+
on_change=lambda e, t=tag: self._on_related_checkbox_change(t, e.value)
|
| 909 |
+
).props('dense')
|
| 910 |
+
self._related_checkboxes[tag] = cb
|
| 911 |
+
|
| 912 |
+
# ๆ ็ญพๅ๏ผๅฏ็นๅป่ทณ่ฝฌ๏ผ+ ไธญๆๅ
|
| 913 |
+
with ui.column().classes('flex-grow gap-0 min-w-0'):
|
| 914 |
+
link = ui.link(
|
| 915 |
+
tag,
|
| 916 |
+
f'https://danbooru.donmai.us/wiki_pages/{tag}',
|
| 917 |
+
new_tab=True
|
| 918 |
+
).classes('tag-link text-primary font-bold text-xs')
|
| 919 |
+
link.on('click', self._mark_interaction)
|
| 920 |
+
|
| 921 |
+
if cn_first:
|
| 922 |
+
ui.label(cn_first).classes('text-xs text-gray-500 truncate')
|
| 923 |
+
|
| 924 |
+
# ๅ
ณ่ๅๆฐ
|
| 925 |
+
score_color = 'green' if r.cooc_score > 0.6 else ('teal' if r.cooc_score > 0.3 else 'grey')
|
| 926 |
+
ui.label(score_pct).classes(f'text-sm font-bold text-{score_color}-600 whitespace-nowrap')
|
| 927 |
+
|
| 928 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 929 |
+
# ไบคไบ้ป่พ
|
| 930 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 931 |
+
|
| 932 |
+
async def _hide_banner_when_ready(self):
|
| 933 |
+
while not DanbooruTagger.is_ready():
|
| 934 |
+
await asyncio.sleep(1)
|
| 935 |
+
if self.init_banner:
|
| 936 |
+
self.init_banner.set_visibility(False)
|
| 937 |
+
|
| 938 |
+
def _client_alive(self) -> bool:
|
| 939 |
+
try:
|
| 940 |
+
_ = self.search_btn.client
|
| 941 |
+
return True
|
| 942 |
+
except RuntimeError:
|
| 943 |
+
return False
|
| 944 |
+
|
| 945 |
+
# โโ ๅ่ฏ็ญ้ โโโโโโโโโโโโโโโโ๏ฟฝ๏ฟฝ๏ฟฝโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 946 |
+
|
| 947 |
+
def _filter_by_source(self, keyword: str):
|
| 948 |
+
show_nsfw_val = self.input_nsfw.value
|
| 949 |
+
if not keyword or keyword == 'ALL':
|
| 950 |
+
filtered = self.full_table_data
|
| 951 |
+
else:
|
| 952 |
+
filtered = [r for r in self.full_table_data if r['source'] == keyword]
|
| 953 |
+
|
| 954 |
+
self.result_table.rows = apply_nsfw_filter(filtered, show_nsfw_val)
|
| 955 |
+
|
| 956 |
+
for child in self.keywords_container.default_slot.children:
|
| 957 |
+
if isinstance(child, ui.chip):
|
| 958 |
+
selected = (
|
| 959 |
+
(keyword == 'ALL' and child.text == 'ๅ
จ้จ')
|
| 960 |
+
or (keyword == self.current_query_str and child.text == 'ๆดๅฅ')
|
| 961 |
+
or (child.text == keyword)
|
| 962 |
+
)
|
| 963 |
+
is_segment = child.text in self.current_segments
|
| 964 |
+
if selected:
|
| 965 |
+
chip_color, text_color = 'primary', 'white'
|
| 966 |
+
elif is_segment:
|
| 967 |
+
chip_color, text_color = 'blue-1', 'blue-8'
|
| 968 |
+
else:
|
| 969 |
+
chip_color, text_color = 'grey-4', 'black'
|
| 970 |
+
child.props(f'color={chip_color} text-color={text_color}')
|
| 971 |
+
|
| 972 |
+
# โโ ๆ็ดข โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 973 |
+
|
| 974 |
+
async def perform_search(self):
|
| 975 |
+
query = self.search_input.value.strip()
|
| 976 |
+
if not query:
|
| 977 |
+
return
|
| 978 |
+
|
| 979 |
+
# ๆ็ดขๅไฟๅญ้
็ฝฎ
|
| 980 |
+
self._save_config()
|
| 981 |
+
|
| 982 |
+
self.current_query_str = query
|
| 983 |
+
self.search_btn.disable()
|
| 984 |
+
self.spinner.classes(remove='hidden')
|
| 985 |
+
ui.notify('ๆญฃๅจๆ็ดข...', type='info')
|
| 986 |
+
|
| 987 |
+
if self.bad_case_btn is not None:
|
| 988 |
+
self.bad_case_btn.disable()
|
| 989 |
+
|
| 990 |
+
target_layers_list = [k for k, v in self.selected_layers.items() if v]
|
| 991 |
+
target_cats_list = [k for k, v in self.selected_cats.items() if v]
|
| 992 |
+
|
| 993 |
+
if not target_layers_list:
|
| 994 |
+
ui.notify('่ฏท่ณๅฐ้ๆฉไธไธชๅน้
ๅฑ๏ผ', type='warning')
|
| 995 |
+
self.search_btn.enable()
|
| 996 |
+
self.spinner.classes(add='hidden')
|
| 997 |
+
return
|
| 998 |
+
|
| 999 |
+
try:
|
| 1000 |
+
tagger = await DanbooruTagger.get_instance()
|
| 1001 |
+
|
| 1002 |
+
# NSFW ไฟๆคๆจกๅผ๏ผๅผ = ไธๆพ็คบ NSFW
|
| 1003 |
+
show_nsfw_val = self.input_nsfw.value
|
| 1004 |
+
|
| 1005 |
+
request = SearchRequest(
|
| 1006 |
+
query=query,
|
| 1007 |
+
top_k=int(self.input_top_k.value),
|
| 1008 |
+
limit=int(self.input_limit.value),
|
| 1009 |
+
popularity_weight=float(self.input_weight.value),
|
| 1010 |
+
show_nsfw=show_nsfw_val,
|
| 1011 |
+
use_segmentation=self.input_segment.value if self.input_segment else True,
|
| 1012 |
+
target_layers=target_layers_list,
|
| 1013 |
+
target_categories=target_cats_list,
|
| 1014 |
+
)
|
| 1015 |
+
response = await run.io_bound(tagger.search, request)
|
| 1016 |
+
|
| 1017 |
+
# ๅๅฐ่ฎกๆฐ
|
| 1018 |
+
async def silent_counter_update():
|
| 1019 |
+
try:
|
| 1020 |
+
await counter.increment()
|
| 1021 |
+
if response.keywords:
|
| 1022 |
+
await counter.add_keywords(response.keywords)
|
| 1023 |
+
self._update_footer_text()
|
| 1024 |
+
except Exception as e:
|
| 1025 |
+
print(f"[UI] ๅๅฐ้้ปๆดๆฐ่ฎกๆฐๅคฑ่ดฅ: {e}", flush=True)
|
| 1026 |
+
asyncio.create_task(silent_counter_update())
|
| 1027 |
+
|
| 1028 |
+
if not self._client_alive():
|
| 1029 |
+
return
|
| 1030 |
+
|
| 1031 |
+
table_data = [result_to_row(r, show_nsfw_val) for r in response.results]
|
| 1032 |
+
self.full_table_data = table_data
|
| 1033 |
+
self.full_tags_str = response.tags_all
|
| 1034 |
+
self.full_tags_str_sfw = response.tags_sfw
|
| 1035 |
+
self.current_segments = list(response.segments) if response.segments else []
|
| 1036 |
+
|
| 1037 |
+
# ๆพ็คบ็ปๆๅบๅ
|
| 1038 |
+
self.results_section.set_visibility(True)
|
| 1039 |
+
|
| 1040 |
+
_saved_rpp = self._get_rows_per_page()
|
| 1041 |
+
self.result_table.rows = apply_nsfw_filter(table_data, show_nsfw_val)
|
| 1042 |
+
self._set_rows_per_page(_saved_rpp)
|
| 1043 |
+
self.result_table.selected = []
|
| 1044 |
+
self.tag_weights.clear()
|
| 1045 |
+
self.chip_extra_selected.clear()
|
| 1046 |
+
self._render_selected_chips()
|
| 1047 |
+
self._update_selection_display(None)
|
| 1048 |
+
|
| 1049 |
+
# ๆธ
็ฉบๅ
ณ่ๆจ่
|
| 1050 |
+
self._refresh_related([], show_nsfw_val)
|
| 1051 |
+
|
| 1052 |
+
# ๅ่ฏ็ญ้ chips
|
| 1053 |
+
self.keywords_container.clear()
|
| 1054 |
+
with self.keywords_container:
|
| 1055 |
+
ui.label('ๅ่ฏ็ญ้:').classes('text-sm text-gray-500 font-bold mr-2')
|
| 1056 |
+
ui.chip('ๅ
จ้จ', on_click=lambda: self._filter_by_source('ALL')) \
|
| 1057 |
+
.props('color=primary text-color=white clickable')
|
| 1058 |
+
use_seg = self.input_segment.value if self.input_segment else True
|
| 1059 |
+
if use_seg:
|
| 1060 |
+
ui.chip('ๆดๅฅ',
|
| 1061 |
+
on_click=lambda: self._filter_by_source(self.current_query_str)) \
|
| 1062 |
+
.props('color=grey-4 text-color=black clickable')
|
| 1063 |
+
# ไปๅฅ็บงๅๅง็ๆฎต๏ผๅ้็ฌฆๅๅๅๆช jieba ็้ฟ็ๆฎต๏ผๅบๅซไบๅ
ณ้ฎ่ฏ๏ผ
|
| 1064 |
+
for seg in response.segments:
|
| 1065 |
+
ui.chip(seg,
|
| 1066 |
+
on_click=lambda s=seg: self._filter_by_source(s)) \
|
| 1067 |
+
.props('color=blue-1 text-color=blue-8 clickable')
|
| 1068 |
+
for kw in response.keywords:
|
| 1069 |
+
ui.chip(kw,
|
| 1070 |
+
on_click=lambda k=kw: self._filter_by_source(k)) \
|
| 1071 |
+
.props('color=grey-4 text-color=black clickable')
|
| 1072 |
+
else:
|
| 1073 |
+
ui.label('(ๅ่ฏๅทฒๅ
ณ้ญ)').classes('text-xs text-gray-400')
|
| 1074 |
+
|
| 1075 |
+
ui.notify(f'ๆพๅฐ {len(table_data)} ไธชๆ ็ญพ', type='positive')
|
| 1076 |
+
self.current_search_interacted = False
|
| 1077 |
+
|
| 1078 |
+
if self.bad_case_btn is not None:
|
| 1079 |
+
self.bad_case_btn.enable()
|
| 1080 |
+
|
| 1081 |
+
except RuntimeError as e:
|
| 1082 |
+
if 'deleted' in str(e).lower() or 'client' in str(e).lower():
|
| 1083 |
+
return
|
| 1084 |
+
try:
|
| 1085 |
+
ui.notify(f'้่ฏฏ: {str(e)}', type='negative')
|
| 1086 |
+
except RuntimeError:
|
| 1087 |
+
pass
|
| 1088 |
+
except Exception as e:
|
| 1089 |
+
try:
|
| 1090 |
+
ui.notify(f'้่ฏฏ: {str(e)}', type='negative')
|
| 1091 |
+
except RuntimeError:
|
| 1092 |
+
pass
|
| 1093 |
+
finally:
|
| 1094 |
+
try:
|
| 1095 |
+
self.search_btn.enable()
|
| 1096 |
+
self.spinner.classes(add='hidden')
|
| 1097 |
+
except RuntimeError:
|
| 1098 |
+
pass
|
| 1099 |
+
|
| 1100 |
+
# โโ ้ๆฉ็ฎก็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1101 |
+
|
| 1102 |
+
def _get_selected_tags(self) -> list[str]:
|
| 1103 |
+
table_tags = [row['tag'] for row in self.result_table.selected] if self.result_table else []
|
| 1104 |
+
seen = set(table_tags)
|
| 1105 |
+
extra = [t for t in self.chip_extra_selected if t not in seen]
|
| 1106 |
+
return table_tags + extra
|
| 1107 |
+
|
| 1108 |
+
def _set_selected_tags(self, tags: list[str]):
|
| 1109 |
+
tag_set = set(tags)
|
| 1110 |
+
table_tag_set = {row['tag'] for row in self.result_table.rows} if self.result_table else set()
|
| 1111 |
+
self.chip_extra_selected.clear()
|
| 1112 |
+
self.chip_extra_selected.update(t for t in tag_set if t not in table_tag_set)
|
| 1113 |
+
# clean up weights for deselected tags
|
| 1114 |
+
for t in list(self.tag_weights):
|
| 1115 |
+
if t not in tag_set:
|
| 1116 |
+
del self.tag_weights[t]
|
| 1117 |
+
|
| 1118 |
+
if self.result_table is not None:
|
| 1119 |
+
self.result_table.selected = [row for row in self.result_table.rows if row.get('tag') in tag_set]
|
| 1120 |
+
|
| 1121 |
+
all_tags = self._get_selected_tags()
|
| 1122 |
+
if self.selection_count_label is not None:
|
| 1123 |
+
self.selection_count_label.text = str(len(all_tags))
|
| 1124 |
+
self._render_selected_chips()
|
| 1125 |
+
|
| 1126 |
+
def _update_selection_display(self, _e):
|
| 1127 |
+
if self.result_table is None:
|
| 1128 |
+
return
|
| 1129 |
+
self._mark_interaction()
|
| 1130 |
+
|
| 1131 |
+
all_tags = self._get_selected_tags()
|
| 1132 |
+
# clean up weights for deselected tags
|
| 1133 |
+
tag_set = set(all_tags)
|
| 1134 |
+
for t in list(self.tag_weights):
|
| 1135 |
+
if t not in tag_set:
|
| 1136 |
+
del self.tag_weights[t]
|
| 1137 |
+
# init weight for newly selected tags
|
| 1138 |
+
for t in all_tags:
|
| 1139 |
+
self.tag_weights.setdefault(t, 1.0)
|
| 1140 |
+
|
| 1141 |
+
if self.selection_count_label is not None:
|
| 1142 |
+
self.selection_count_label.text = str(len(all_tags))
|
| 1143 |
+
self._render_selected_chips()
|
| 1144 |
+
|
| 1145 |
+
show_nsfw_val = self.input_nsfw.value
|
| 1146 |
+
if all_tags:
|
| 1147 |
+
self._refresh_related_from_selection(all_tags, show_nsfw_val)
|
| 1148 |
+
else:
|
| 1149 |
+
self.chip_extra_selected.clear()
|
| 1150 |
+
self._refresh_related([], show_nsfw_val)
|
| 1151 |
+
|
| 1152 |
+
def _on_related_checkbox_change(self, tag: str, checked: bool):
|
| 1153 |
+
self._mark_interaction()
|
| 1154 |
+
current = self._get_selected_tags()
|
| 1155 |
+
if checked:
|
| 1156 |
+
if tag not in current:
|
| 1157 |
+
current.append(tag)
|
| 1158 |
+
self.tag_weights.setdefault(tag, 1.0)
|
| 1159 |
+
self._set_selected_tags(current)
|
| 1160 |
+
ui.notify(f'ๅทฒๆทปๅ {tag}', type='positive', timeout=1500)
|
| 1161 |
+
else:
|
| 1162 |
+
if tag in current:
|
| 1163 |
+
current.remove(tag)
|
| 1164 |
+
self.tag_weights.pop(tag, None)
|
| 1165 |
+
self._set_selected_tags(current)
|
| 1166 |
+
ui.notify(f'ๅทฒ็งป้ค {tag}', type='warning', timeout=1500)
|
| 1167 |
+
|
| 1168 |
+
def _manual_refresh_related(self):
|
| 1169 |
+
"""ๆๅจ่งฆๅๅ
ณ่ๆจ่ๅ่กจ็ๅทๆฐ"""
|
| 1170 |
+
self._mark_interaction()
|
| 1171 |
+
show_nsfw_val = self.input_nsfw.value
|
| 1172 |
+
all_tags = self._get_selected_tags()
|
| 1173 |
+
|
| 1174 |
+
if all_tags:
|
| 1175 |
+
self._refresh_related_from_selection(all_tags, show_nsfw_val)
|
| 1176 |
+
ui.notify('ๅทฒ่งฆๅๅ
ณ่ๆจ่ๆดๆฐ', type='info', timeout=1500)
|
| 1177 |
+
else:
|
| 1178 |
+
self.chip_extra_selected.clear()
|
| 1179 |
+
self._refresh_related([], show_nsfw_val)
|
| 1180 |
+
ui.notify('ๅทฒๆธ
็ฉบๅ
ณ่ๆจ่', type='info', timeout=1500)
|
| 1181 |
+
|
| 1182 |
+
# โโ ๅ
ณ่ๆจ่ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1183 |
+
|
| 1184 |
+
def _refresh_related(self, related: list, show_nsfw: bool):
|
| 1185 |
+
selected_now = set(self._get_selected_tags())
|
| 1186 |
+
old_related = self.current_related
|
| 1187 |
+
new_tags = {r.tag for r in related}
|
| 1188 |
+
preserved = [r for r in old_related if r.tag in selected_now and r.tag not in new_tags]
|
| 1189 |
+
merged = list(related) + preserved
|
| 1190 |
+
|
| 1191 |
+
self.current_related = merged
|
| 1192 |
+
if self.related_list_container is not None:
|
| 1193 |
+
self._render_related_list(merged, show_nsfw)
|
| 1194 |
+
|
| 1195 |
+
def _refresh_related_from_selection(self, selected_tags: list[str], show_nsfw: bool):
|
| 1196 |
+
async def _do():
|
| 1197 |
+
tagger = await DanbooruTagger.get_instance()
|
| 1198 |
+
related = await run.io_bound(
|
| 1199 |
+
tagger.get_related,
|
| 1200 |
+
selected_tags,
|
| 1201 |
+
set(selected_tags),
|
| 1202 |
+
50,
|
| 1203 |
+
show_nsfw,
|
| 1204 |
+
)
|
| 1205 |
+
self._refresh_related(related, show_nsfw)
|
| 1206 |
+
asyncio.ensure_future(_do())
|
| 1207 |
+
|
| 1208 |
+
# โโ ่กจๆ ผๅๅจๆๆดๆฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1209 |
+
|
| 1210 |
+
def _update_table_columns(self, e=None):
|
| 1211 |
+
cols = list(TABLE_COLUMNS)
|
| 1212 |
+
if self.sw_semantic and self.sw_semantic.value:
|
| 1213 |
+
cols.append(OPTIONAL_COLS['semantic'])
|
| 1214 |
+
if self.sw_layer and self.sw_layer.value:
|
| 1215 |
+
cols.append(OPTIONAL_COLS['layer'])
|
| 1216 |
+
if self.sw_source and self.sw_source.value:
|
| 1217 |
+
cols.append(OPTIONAL_COLS['source'])
|
| 1218 |
+
self.result_table.columns = cols
|
| 1219 |
+
|
| 1220 |
+
# โโ NSFW ๅๆข โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1221 |
+
|
| 1222 |
+
def on_nsfw_toggle(self, e):
|
| 1223 |
+
show_nsfw_val = self.input_nsfw.value
|
| 1224 |
+
|
| 1225 |
+
self.result_table.rows = apply_nsfw_filter(self.full_table_data, show_nsfw_val)
|
| 1226 |
+
if not show_nsfw_val:
|
| 1227 |
+
self.result_table.selected = [r for r in self.result_table.selected if r.get('nsfw') != '1']
|
| 1228 |
+
self._update_selection_display(None)
|
| 1229 |
+
|
| 1230 |
+
# โโ ๅคๅถ / ๅ้ฆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1231 |
+
|
| 1232 |
+
def _toggle_prompt_format(self):
|
| 1233 |
+
if self.prompt_format == 'sdxl':
|
| 1234 |
+
self.prompt_format = 'nai'
|
| 1235 |
+
if self.format_toggle_btn:
|
| 1236 |
+
self.format_toggle_btn.text = 'NAI'
|
| 1237 |
+
self.format_toggle_btn.props('color=purple-7')
|
| 1238 |
+
else:
|
| 1239 |
+
self.prompt_format = 'sdxl'
|
| 1240 |
+
if self.format_toggle_btn:
|
| 1241 |
+
self.format_toggle_btn.text = 'SDXL'
|
| 1242 |
+
self.format_toggle_btn.props('color=grey-7')
|
| 1243 |
+
|
| 1244 |
+
def copy_selection(self):
|
| 1245 |
+
self._mark_interaction()
|
| 1246 |
+
tags = self._get_selected_tags()
|
| 1247 |
+
prompt = ', '.join(
|
| 1248 |
+
_format_tag_with_weight(t, self.tag_weights.get(t, 1.0), self.prompt_format)
|
| 1249 |
+
for t in tags
|
| 1250 |
+
)
|
| 1251 |
+
ui.clipboard.write(prompt)
|
| 1252 |
+
fmt_label = 'NAI' if self.prompt_format == 'nai' else 'SDXL'
|
| 1253 |
+
ui.notify(f'ๅทฒๅคๅถ้ไธญๆ ็ญพ๏ผ{fmt_label} ๆ ผๅผ๏ผ!', type='positive')
|
| 1254 |
+
|
| 1255 |
+
async def silent_copy_update():
|
| 1256 |
+
try:
|
| 1257 |
+
await counter.increment_copy()
|
| 1258 |
+
except Exception:
|
| 1259 |
+
pass
|
| 1260 |
+
asyncio.create_task(silent_copy_update())
|
| 1261 |
+
|
| 1262 |
+
def _copy_all_tags(self):
|
| 1263 |
+
self._mark_interaction()
|
| 1264 |
+
show_nsfw_val = self.input_nsfw.value
|
| 1265 |
+
tags_str = self.full_tags_str if show_nsfw_val else self.full_tags_str_sfw
|
| 1266 |
+
if tags_str:
|
| 1267 |
+
ui.clipboard.write(tags_str)
|
| 1268 |
+
ui.notify('ๅทฒๅคๅถๅ
จ้จๆ ็ญพ!', type='positive')
|
| 1269 |
+
else:
|
| 1270 |
+
ui.notify('ๆๆ ๆ ็ญพๅฏๅคๅถ', type='warning')
|
| 1271 |
+
|
| 1272 |
+
async def report_bad_case(self):
|
| 1273 |
+
from platform_utils import PLATFORM
|
| 1274 |
+
query = self.current_query_str.strip()
|
| 1275 |
+
if len(query) <= 1:
|
| 1276 |
+
ui.notify('ๆ็ดข่ฏๅคช็ญ๏ผๆ ๆณๆไบคๅ้ฆใ', type='warning', timeout=2000)
|
| 1277 |
+
return
|
| 1278 |
+
if self.bad_case_btn is not None:
|
| 1279 |
+
self.bad_case_btn.disable()
|
| 1280 |
+
try:
|
| 1281 |
+
settings = {
|
| 1282 |
+
'top_k': int(self.input_top_k.value) if self.input_top_k else None,
|
| 1283 |
+
'segmentation': self.input_segment.value if self.input_segment else None,
|
| 1284 |
+
'nsfw': self.input_nsfw.value if self.input_nsfw else None,
|
| 1285 |
+
}
|
| 1286 |
+
await counter.add_bad_case(query, platform=PLATFORM, settings=settings)
|
| 1287 |
+
ui.notify('ๆ่ฐขๅ้ฆ๏ผๆไปฌไผๆ็ปญไผๅใ', type='positive', timeout=3000)
|
| 1288 |
+
except Exception as e:
|
| 1289 |
+
print(f'[UI] bad_case ่ฎฐๅฝๅผๅธธ: {e}')
|
| 1290 |
+
ui.notify('่ฎฐๅฝๅคฑ่ดฅ๏ผ่ฏท็จๅๅ่ฏใ', type='warning', timeout=3000)
|
| 1291 |
+
if self.bad_case_btn is not None:
|
| 1292 |
+
self.bad_case_btn.enable()
|
| 1293 |
+
|
| 1294 |
+
# โโ ้กต้ข่ทฏ็ฑ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1295 |
+
|
| 1296 |
+
@ui.page('/')
|
| 1297 |
+
async def main_page():
|
| 1298 |
+
app_ui = DanbooruSearchUI()
|
| 1299 |
+
app_ui.build_page()
|
| 1300 |
+
|
| 1301 |
+
async def silent_visit_update():
|
| 1302 |
+
try:
|
| 1303 |
+
await counter.increment_visit()
|
| 1304 |
+
app_ui._update_footer_text()
|
| 1305 |
+
except Exception:
|
| 1306 |
+
pass
|
| 1307 |
+
asyncio.create_task(silent_visit_update())
|
| 1308 |
+
|
| 1309 |
+
# ๆขๅค็จๆท้
็ฝฎ๏ผๅจ้กต้ขๆธฒๆๅฎๆๅๆง่ก๏ผ
|
| 1310 |
+
await app_ui._restore_config()
|
| 1311 |
+
|
| 1312 |
+
|
| 1313 |
+
# โโ ๅ
ฅๅฃ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1314 |
+
|
| 1315 |
+
if __name__ in {'__main__', '__mp_main__'}:
|
| 1316 |
+
host, port = get_host_port()
|
| 1317 |
+
|
| 1318 |
+
@app.on_startup
|
| 1319 |
+
def _warmup():
|
| 1320 |
+
async def background_init_tasks():
|
| 1321 |
+
await asyncio.sleep(5)
|
| 1322 |
+
print("[UI] ๅผๅง้ข็ญ่ฎกๆฐๅจไธๅผๆ", flush=True)
|
| 1323 |
+
await counter.init()
|
| 1324 |
+
await DanbooruTagger.get_instance()
|
| 1325 |
+
print("[UI] ๅๅฐ้ข็ญๅ
จ้จๅฎๆ๏ผ", flush=True)
|
| 1326 |
+
asyncio.create_task(background_init_tasks())
|
| 1327 |
+
|
| 1328 |
+
@app.on_shutdown
|
| 1329 |
+
def _shutdown():
|
| 1330 |
+
try:
|
| 1331 |
+
loop = asyncio.get_event_loop()
|
| 1332 |
+
if loop.is_running():
|
| 1333 |
+
loop.create_task(counter.force_sync())
|
| 1334 |
+
else:
|
| 1335 |
+
asyncio.run(counter.force_sync())
|
| 1336 |
+
except Exception as e:
|
| 1337 |
+
print(f"[UI] ๅ
ณๆบๅๆญฅๅคฑ่ดฅ: {e}")
|
| 1338 |
+
|
| 1339 |
+
app.mount('/api', api_app)
|
| 1340 |
+
|
| 1341 |
+
mcp_app = mcp.streamable_http_app()
|
| 1342 |
+
app.mount('/mcp', mcp_app)
|
| 1343 |
+
_mcp_lifespan_ctx = None
|
| 1344 |
+
@app.on_startup
|
| 1345 |
+
async def _start_mcp():
|
| 1346 |
+
global _mcp_lifespan_ctx
|
| 1347 |
+
_mcp_lifespan_ctx = mcp_app.router.lifespan_context(mcp_app)
|
| 1348 |
+
await _mcp_lifespan_ctx.__aenter__()
|
| 1349 |
+
@app.on_shutdown
|
| 1350 |
+
async def _stop_mcp():
|
| 1351 |
+
global _mcp_lifespan_ctx
|
| 1352 |
+
if _mcp_lifespan_ctx is not None:
|
| 1353 |
+
await _mcp_lifespan_ctx.__aexit__(None, None, None)
|
| 1354 |
+
|
| 1355 |
+
@app.get('/googlebd34b54f8562aa06.html')
|
| 1356 |
+
def google_verification():
|
| 1357 |
+
return PlainTextResponse('google-site-verification: googlebd34b54f8562aa06.html')
|
| 1358 |
+
|
| 1359 |
+
@app.get('/robots.txt')
|
| 1360 |
+
def robots_txt():
|
| 1361 |
+
content = (
|
| 1362 |
+
"User-agent: *\n"
|
| 1363 |
+
"Allow: /$\n"
|
| 1364 |
+
"Disallow: /api/\n"
|
| 1365 |
+
"Disallow: /_nicegui/\n"
|
| 1366 |
+
"Disallow: /socket.io/\n"
|
| 1367 |
+
)
|
| 1368 |
+
return PlainTextResponse(content)
|
| 1369 |
+
|
| 1370 |
+
@app.head('/')
|
| 1371 |
+
async def head_root():
|
| 1372 |
+
return PlainTextResponse("")
|
| 1373 |
+
|
| 1374 |
+
ui.run(
|
| 1375 |
+
host=host,
|
| 1376 |
+
port=port,
|
| 1377 |
+
title='Danbooru Tags Searcher',
|
| 1378 |
+
reload=not is_cloud(),
|
| 1379 |
+
show=not is_cloud(),
|
| 1380 |
+
reconnect_timeout=120,
|
| 1381 |
+
)
|