Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Auto-sync from GitHub Actions
Browse files- api_fastapi.py +2 -0
- cli.py +9 -0
- core/engine.py +175 -11
- core/models.py +2 -0
- mcp_server.py +9 -0
- ui_nicegui.py +229 -30
api_fastapi.py
CHANGED
|
@@ -46,6 +46,8 @@ class SearchIn(BaseModel):
|
|
| 46 |
use_segmentation: bool = True
|
| 47 |
target_layers: list[str] = ['่ฑๆ', 'ไธญๆๆฉๅฑ่ฏ', '้ไน', 'ไธญๆๆ ธๅฟ่ฏ']
|
| 48 |
target_categories: list[str] = ['General', 'Character', 'Copyright']
|
|
|
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
class TagOut(BaseModel):
|
|
|
|
| 46 |
use_segmentation: bool = True
|
| 47 |
target_layers: list[str] = ['่ฑๆ', 'ไธญๆๆฉๅฑ่ฏ', '้ไน', 'ไธญๆๆ ธๅฟ่ฏ']
|
| 48 |
target_categories: list[str] = ['General', 'Character', 'Copyright']
|
| 49 |
+
group_mode: str = "off"
|
| 50 |
+
max_per_group: int = 2
|
| 51 |
|
| 52 |
|
| 53 |
class TagOut(BaseModel):
|
cli.py
CHANGED
|
@@ -33,6 +33,8 @@ async def cmd_search(args):
|
|
| 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 |
|
|
@@ -61,6 +63,7 @@ async def cmd_related(args):
|
|
| 61 |
set(seed_tags), # exclude ็งๅญๆ ็ญพ่ช่บซ
|
| 62 |
args.limit,
|
| 63 |
not args.no_nsfw,
|
|
|
|
| 64 |
)
|
| 65 |
|
| 66 |
if not results:
|
|
@@ -114,6 +117,10 @@ async def main():
|
|
| 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',
|
|
@@ -131,6 +138,8 @@ async def main():
|
|
| 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':
|
|
|
|
| 33 |
use_segmentation=not args.no_seg,
|
| 34 |
target_layers=args.layers,
|
| 35 |
target_categories=args.categories,
|
| 36 |
+
group_mode=args.group_mode,
|
| 37 |
+
max_per_group=args.max_per_group,
|
| 38 |
)
|
| 39 |
resp = await asyncio.to_thread(tagger.search, request)
|
| 40 |
|
|
|
|
| 63 |
set(seed_tags), # exclude ็งๅญๆ ็ญพ่ช่บซ
|
| 64 |
args.limit,
|
| 65 |
not args.no_nsfw,
|
| 66 |
+
not args.no_group_expansion,
|
| 67 |
)
|
| 68 |
|
| 69 |
if not results:
|
|
|
|
| 117 |
p_search.add_argument('--weight', type=float, default=0.15, help='็ญๅบฆๆ้๏ผ้ป่ฎค 0.15๏ผ')
|
| 118 |
p_search.add_argument('--no-nsfw', action='store_true', help='่ฟๆปค NSFW ๅ
ๅฎน')
|
| 119 |
p_search.add_argument('--no-seg', action='store_true', help='็ฆ็จๆบ่ฝๅ่ฏ')
|
| 120 |
+
p_search.add_argument('--group-mode', choices=['off', 'expand', 'diverse'],
|
| 121 |
+
default='off', help='Group ๅค็ๆจกๅผ๏ผ้ป่ฎค off๏ผ')
|
| 122 |
+
p_search.add_argument('--max-per-group', type=int, default=2,
|
| 123 |
+
help='diverse ๆจกๅผไธๆฏไธช group ๆๅคไฟ็็ๆ ็ญพๆฐ๏ผ้ป่ฎค 2๏ผ')
|
| 124 |
p_search.add_argument(
|
| 125 |
'--layers', nargs='+', default=_all_layers,
|
| 126 |
metavar='LAYER',
|
|
|
|
| 138 |
p_related.add_argument('--limit', type=int, default=50, help='ๆจ่็ปๆไธ้๏ผ้ป่ฎค 50๏ผ')
|
| 139 |
p_related.add_argument('--no-nsfw', action='store_true', help='่ฟๆปค NSFW ๅ
ๅฎน')
|
| 140 |
p_related.add_argument('--show-sources', action='store_true', help='ๆพ็คบๆฏๆกๆจ่็ฑๅชไธช็งๅญ่งฆๅ')
|
| 141 |
+
p_related.add_argument('--no-group-expansion', action='store_true',
|
| 142 |
+
help='ๅ
ณ้ญ group ๅ็ฑปๆฉๅฑ๏ผ้ป่ฎคๅผๅฏ๏ผ')
|
| 143 |
|
| 144 |
args = parser.parse_args()
|
| 145 |
if args.cmd == 'search':
|
core/engine.py
CHANGED
|
@@ -163,6 +163,7 @@ class DanbooruTagger:
|
|
| 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()
|
|
@@ -171,6 +172,7 @@ class DanbooruTagger:
|
|
| 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
|
|
@@ -181,6 +183,8 @@ class DanbooruTagger:
|
|
| 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]
|
|
@@ -238,6 +242,7 @@ class DanbooruTagger:
|
|
| 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 |
|
|
@@ -294,6 +299,7 @@ class DanbooruTagger:
|
|
| 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')
|
|
@@ -343,6 +349,8 @@ class DanbooruTagger:
|
|
| 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:
|
|
@@ -450,6 +458,10 @@ class DanbooruTagger:
|
|
| 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:
|
|
@@ -464,11 +476,37 @@ class DanbooruTagger:
|
|
| 464 |
# ๅฏนๆๆๅ้่ฟ่กๆๅบ๏ผ็ถๅๅจไฟ็ไฟ่ฏ็ปๆ็ๅๆถๆชๆญ่ณ้ๅถๆฐ้
|
| 465 |
sorted_results = sorted(final.values(), key=lambda r: r.final_score, reverse=True)
|
| 466 |
valid: list[TagResult] = []
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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')
|
|
@@ -479,6 +517,46 @@ class DanbooruTagger:
|
|
| 479 |
self._search_cache.put(cache_key, response)
|
| 480 |
return response
|
| 481 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
# โโ ๅ
จ้ๆๅปบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 483 |
|
| 484 |
def _build_full(self) -> None:
|
|
@@ -553,6 +631,7 @@ class DanbooruTagger:
|
|
| 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)} ๆก๏ผ')
|
|
@@ -812,25 +891,24 @@ class DanbooruTagger:
|
|
| 812 |
max_score = max(npmi_scores.values())
|
| 813 |
|
| 814 |
sorted_candidates = sorted(npmi_scores.items(), key=lambda x: x[1], reverse=True)
|
| 815 |
-
|
|
|
|
|
|
|
| 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
|
| 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]),
|
|
@@ -840,6 +918,61 @@ class DanbooruTagger:
|
|
| 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')
|
|
@@ -894,4 +1027,35 @@ class DanbooruTagger:
|
|
| 894 |
f'่ๆถ {time.time() - t0:.2f}s'
|
| 895 |
)
|
| 896 |
except Exception as e:
|
| 897 |
-
print(f'[Engine] ๅ
ฑ็ฐ่กจๅ ่ฝฝๅคฑ่ดฅ: {e}')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
group_file: str = 'origin_database/tag_groups.json',
|
| 167 |
):
|
| 168 |
# ๆจกๅ่ทฏๅพ๏ผไผๅ
ไฝฟ็จๆพๅผไผ ๅ
ฅ๏ผๅฆๅไบค็ฑ platform_utils ่งฃๆ
|
| 169 |
self.model_path = model_path or resolve_model_path()
|
|
|
|
| 172 |
self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 173 |
self.paths = _CachePaths(cache_dir)
|
| 174 |
self.cooc_file = cooc_file
|
| 175 |
+
self.group_file = group_file
|
| 176 |
|
| 177 |
self.model: Optional[SentenceTransformer] = None
|
| 178 |
self.df: Optional[pd.DataFrame] = None
|
|
|
|
| 183 |
self.max_log_count: float = 15.0
|
| 184 |
self.cooc: dict[str, list[tuple[str, int]]] = {}
|
| 185 |
self._name_to_idx: dict[str, int] = {}
|
| 186 |
+
self._tag_to_groups: dict[str, set[str]] = {}
|
| 187 |
+
self._group_to_tags_idx: dict[str, np.ndarray] = {}
|
| 188 |
self.is_loaded: bool = False
|
| 189 |
|
| 190 |
# ้ขๆๅ็ๅๆฐ็ป๏ผ้ฟๅ
็ญ็น่ทฏๅพไธๅๅคๆง่ก df.iloc[idx]
|
|
|
|
| 242 |
self._name_to_idx = {n: i for i, n in enumerate(self.df['name'])}
|
| 243 |
self._rebuild_arrays_from_df()
|
| 244 |
self._normalize_embeddings()
|
| 245 |
+
self._load_groups()
|
| 246 |
self.is_loaded = True
|
| 247 |
print(f'[Engine] ๅๅงๅๅฎๆ๏ผ่ๆถ {time.time() - t0:.2f}s')
|
| 248 |
|
|
|
|
| 299 |
|
| 300 |
self.csv_path = pull('origin_database/tags_enhanced.csv')
|
| 301 |
self.cooc_file = pull('origin_database/cooccurrence_clean.parquet')
|
| 302 |
+
self.group_file = pull('origin_database/tag_groups.json')
|
| 303 |
|
| 304 |
meta_path = pull('tags_embedding/tags_metadata.parquet')
|
| 305 |
emb_path = pull('tags_embedding/danbooru_multiview_embeddings.safetensors')
|
|
|
|
| 349 |
request.use_segmentation,
|
| 350 |
tuple(sorted(request.target_layers)),
|
| 351 |
tuple(sorted(request.target_categories)),
|
| 352 |
+
request.group_mode,
|
| 353 |
+
request.max_per_group,
|
| 354 |
)
|
| 355 |
cached = self._search_cache.get(cache_key)
|
| 356 |
if cached is not None:
|
|
|
|
| 458 |
# coherence=0 ๆถๆๅคๆฃ alpha=15%๏ผcoherence=1 ๆถไธๆฃๅ
|
| 459 |
r.final_score = round(r.final_score * (1.0 - alpha + alpha * max_co), 4)
|
| 460 |
|
| 461 |
+
# Group expand ๅค็๏ผๅจ guaranteed_tags ไนๅ๏ผๅ ไธบไผๆนๅๆฐ๏ผ
|
| 462 |
+
if request.group_mode == "expand" and self._tag_to_groups:
|
| 463 |
+
self._apply_group_expand(final)
|
| 464 |
+
|
| 465 |
# ๆถ้ๆฏไธชๆฅ่ฏขๆบ็ top-1 ็ปๆ๏ผ้ซไบ้ๅผ๏ผ
|
| 466 |
guaranteed_tags: set[str] = set()
|
| 467 |
for source_word in queries:
|
|
|
|
| 476 |
# ๅฏนๆๆๅ้่ฟ่กๆๅบ๏ผ็ถๅๅจไฟ็ไฟ่ฏ็ปๆ็ๅๆถๆชๆญ่ณ้ๅถๆฐ้
|
| 477 |
sorted_results = sorted(final.values(), key=lambda r: r.final_score, reverse=True)
|
| 478 |
valid: list[TagResult] = []
|
| 479 |
+
|
| 480 |
+
if request.group_mode == "diverse" and self._tag_to_groups:
|
| 481 |
+
# diverse ๆจกๅผ๏ผๆฏไธช group ๆๅคไฟ็ max_per_group ไธชๆ ็ญพ
|
| 482 |
+
group_counter: dict[str, int] = {}
|
| 483 |
+
max_per = request.max_per_group
|
| 484 |
+
for r in sorted_results:
|
| 485 |
+
if r.final_score <= 0.45:
|
| 486 |
+
continue
|
| 487 |
+
if r.tag in guaranteed_tags:
|
| 488 |
+
# guaranteed_tags ่ฑๅ
group ไธ้
|
| 489 |
+
valid.append(r)
|
| 490 |
+
continue
|
| 491 |
+
groups = self._tag_to_groups.get(r.tag)
|
| 492 |
+
if not groups:
|
| 493 |
+
# ๆ group ไฟกๆฏ๏ผไธๅ้ๅถ
|
| 494 |
+
if len(valid) < request.limit:
|
| 495 |
+
valid.append(r)
|
| 496 |
+
continue
|
| 497 |
+
# ๆฃๆฅๆฏๅฆๆไปปไธ group ่พพไธ้
|
| 498 |
+
if any(group_counter.get(g, 0) >= max_per for g in groups):
|
| 499 |
+
continue
|
| 500 |
+
if len(valid) < request.limit:
|
| 501 |
+
valid.append(r)
|
| 502 |
+
for g in groups:
|
| 503 |
+
group_counter[g] = group_counter.get(g, 0) + 1
|
| 504 |
+
else:
|
| 505 |
+
for r in sorted_results:
|
| 506 |
+
if r.final_score <= 0.45:
|
| 507 |
+
continue
|
| 508 |
+
if len(valid) < request.limit or r.tag in guaranteed_tags:
|
| 509 |
+
valid.append(r)
|
| 510 |
|
| 511 |
tags_all = ', '.join(r.tag for r in valid)
|
| 512 |
tags_sfw = ', '.join(r.tag for r in valid if r.nsfw != '1')
|
|
|
|
| 517 |
self._search_cache.put(cache_key, response)
|
| 518 |
return response
|
| 519 |
|
| 520 |
+
def _apply_group_expand(self, final: dict[str, TagResult]) -> None:
|
| 521 |
+
"""expand ๆจกๅผ๏ผๆๅๅ group ๆ ็ญพ็ๅๆฐใ"""
|
| 522 |
+
BETA = 0.2
|
| 523 |
+
TOP_N = 20
|
| 524 |
+
|
| 525 |
+
sorted_items = sorted(final.values(), key=lambda r: r.final_score, reverse=True)
|
| 526 |
+
top_n = min(TOP_N, len(sorted_items))
|
| 527 |
+
anchor_results = sorted_items[:top_n]
|
| 528 |
+
|
| 529 |
+
# ๆถ้้็น็ปๆๆๅฑ็ๆๆ group
|
| 530 |
+
active_groups: set[str] = set()
|
| 531 |
+
for r in anchor_results:
|
| 532 |
+
groups = self._tag_to_groups.get(r.tag)
|
| 533 |
+
if groups:
|
| 534 |
+
active_groups.update(groups)
|
| 535 |
+
|
| 536 |
+
if not active_groups:
|
| 537 |
+
return
|
| 538 |
+
|
| 539 |
+
# ้ข่ฎก็ฎๆฏไธช group ็้็นๆๅคงๅ
|
| 540 |
+
group_max_score: dict[str, float] = {}
|
| 541 |
+
for g in active_groups:
|
| 542 |
+
group_max_score[g] = max(
|
| 543 |
+
(r.final_score for r in anchor_results
|
| 544 |
+
if g in self._tag_to_groups.get(r.tag, set())),
|
| 545 |
+
default=0.0,
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
# ๅฏนๆๆๅ้ๅบ็จ boost
|
| 549 |
+
for r in final.values():
|
| 550 |
+
groups = self._tag_to_groups.get(r.tag)
|
| 551 |
+
if not groups:
|
| 552 |
+
continue
|
| 553 |
+
overlap = groups & active_groups
|
| 554 |
+
if not overlap:
|
| 555 |
+
continue
|
| 556 |
+
best_group_score = max(group_max_score[g] for g in overlap)
|
| 557 |
+
boost = 1.0 + BETA * best_group_score
|
| 558 |
+
r.final_score = round(r.final_score * boost, 4)
|
| 559 |
+
|
| 560 |
# โโ ๅ
จ้ๆๅปบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 561 |
|
| 562 |
def _build_full(self) -> None:
|
|
|
|
| 631 |
self.max_log_count = float(np.log1p(self.df['post_count'].max()))
|
| 632 |
self._name_to_idx = {n: i for i, n in enumerate(self.df['name'])}
|
| 633 |
self._rebuild_arrays_from_df()
|
| 634 |
+
self._load_groups()
|
| 635 |
self._normalize_embeddings()
|
| 636 |
self._save_cache()
|
| 637 |
print(f'[Engine] ๅข้ๆดๆฐๅฎๆ๏ผ่ๆถ {time.time() - t0:.2f}s๏ผๅ
ฑ {len(self.df)} ๆก๏ผ')
|
|
|
|
| 891 |
max_score = max(npmi_scores.values())
|
| 892 |
|
| 893 |
sorted_candidates = sorted(npmi_scores.items(), key=lambda x: x[1], reverse=True)
|
| 894 |
+
|
| 895 |
+
# โโ ๆๅปบ NPMI ็ปๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 896 |
+
results: list = []
|
| 897 |
|
| 898 |
for tag_name, raw_score in sorted_candidates:
|
| 899 |
if len(results) >= limit:
|
| 900 |
break
|
|
|
|
| 901 |
idx = name_to_idx[tag_name]
|
| 902 |
nsfw = self._arr_nsfw[idx]
|
| 903 |
if nsfw == '1' and not show_nsfw:
|
| 904 |
continue
|
|
|
|
| 905 |
cat = CAT_MAP.get(self._arr_category[idx], 'Other')
|
|
|
|
| 906 |
results.append(RelatedTag(
|
| 907 |
tag=tag_name,
|
| 908 |
cn_name=str(self._arr_cn_name[idx]),
|
| 909 |
category=cat,
|
| 910 |
nsfw=nsfw,
|
| 911 |
+
cooc_count=total_cooc.get(tag_name, 0),
|
| 912 |
cooc_score=round(raw_score / max_score, 4),
|
| 913 |
sources=tag_sources.get(tag_name, []),
|
| 914 |
post_count=int(self._arr_post_count[idx]),
|
|
|
|
| 918 |
self._related_cache.put(related_key, results)
|
| 919 |
return results
|
| 920 |
|
| 921 |
+
def get_group_candidates(
|
| 922 |
+
self,
|
| 923 |
+
selected_tags: list[str],
|
| 924 |
+
show_nsfw: bool = True,
|
| 925 |
+
) -> list[dict]:
|
| 926 |
+
"""ๆ นๆฎๅทฒ้ๆ ็ญพ๏ผ่ฟๅๅ้ Group ๅๅ
ถๆๅๆ ็ญพใ"""
|
| 927 |
+
if not self._tag_to_groups or not selected_tags:
|
| 928 |
+
return []
|
| 929 |
+
|
| 930 |
+
group_hit_count: dict[str, int] = {}
|
| 931 |
+
for tag_name in selected_tags:
|
| 932 |
+
groups = self._tag_to_groups.get(tag_name)
|
| 933 |
+
if groups:
|
| 934 |
+
for g in groups:
|
| 935 |
+
group_hit_count[g] = group_hit_count.get(g, 0) + 1
|
| 936 |
+
|
| 937 |
+
if not group_hit_count:
|
| 938 |
+
return []
|
| 939 |
+
|
| 940 |
+
selected_set = set(selected_tags)
|
| 941 |
+
sorted_groups = sorted(group_hit_count.items(), key=lambda x: -x[1])
|
| 942 |
+
|
| 943 |
+
results = []
|
| 944 |
+
for group_name, hit_count in sorted_groups:
|
| 945 |
+
member_idxs = self._group_to_tags_idx.get(group_name)
|
| 946 |
+
if member_idxs is None:
|
| 947 |
+
continue
|
| 948 |
+
|
| 949 |
+
tags = []
|
| 950 |
+
for idx in member_idxs:
|
| 951 |
+
tag_name = str(self._arr_name[idx])
|
| 952 |
+
if tag_name in selected_set:
|
| 953 |
+
continue
|
| 954 |
+
nsfw = self._arr_nsfw[idx]
|
| 955 |
+
if nsfw == '1' and not show_nsfw:
|
| 956 |
+
continue
|
| 957 |
+
cat = CAT_MAP.get(self._arr_category[idx], 'Other')
|
| 958 |
+
tags.append({
|
| 959 |
+
'tag': tag_name,
|
| 960 |
+
'cn_name': str(self._arr_cn_name[idx]),
|
| 961 |
+
'category': cat,
|
| 962 |
+
'nsfw': nsfw,
|
| 963 |
+
'post_count': int(self._arr_post_count[idx]),
|
| 964 |
+
'wiki': str(self._arr_wiki[idx]) if self._arr_wiki is not None else '',
|
| 965 |
+
})
|
| 966 |
+
|
| 967 |
+
tags.sort(key=lambda x: -x['post_count'])
|
| 968 |
+
results.append({
|
| 969 |
+
'group': group_name,
|
| 970 |
+
'hit_count': hit_count,
|
| 971 |
+
'tags': tags,
|
| 972 |
+
})
|
| 973 |
+
|
| 974 |
+
return results
|
| 975 |
+
|
| 976 |
def _load_cooc(self) -> None:
|
| 977 |
csv_path = Path(self.cooc_file)
|
| 978 |
parquet_path = csv_path.with_suffix('.parquet')
|
|
|
|
| 1027 |
f'่ๆถ {time.time() - t0:.2f}s'
|
| 1028 |
)
|
| 1029 |
except Exception as e:
|
| 1030 |
+
print(f'[Engine] ๅ
ฑ็ฐ่กจๅ ่ฝฝๅคฑ่ดฅ: {e}')
|
| 1031 |
+
|
| 1032 |
+
def _load_groups(self) -> None:
|
| 1033 |
+
"""ๅ ่ฝฝ Tag Group ๆฐๆฎ๏ผๆๅปบ tagโgroup ๅ groupโidx ็ดขๅผใ"""
|
| 1034 |
+
if not Path(self.group_file).is_file():
|
| 1035 |
+
print('[Engine] ๆชๆพๅฐ Tag Group ๆฐๆฎ๏ผgroup ๅ่ฝไธๅฏ็จใ')
|
| 1036 |
+
return
|
| 1037 |
+
|
| 1038 |
+
with open(self.group_file, 'r', encoding='utf-8') as f:
|
| 1039 |
+
data = json.load(f)
|
| 1040 |
+
|
| 1041 |
+
raw_t2g = data.get('tag_to_groups', {})
|
| 1042 |
+
name_to_idx = self._name_to_idx
|
| 1043 |
+
|
| 1044 |
+
self._tag_to_groups = {}
|
| 1045 |
+
group_members: dict[str, list[int]] = {}
|
| 1046 |
+
|
| 1047 |
+
for tag_name, groups in raw_t2g.items():
|
| 1048 |
+
if tag_name not in name_to_idx:
|
| 1049 |
+
continue
|
| 1050 |
+
group_set = set(groups)
|
| 1051 |
+
self._tag_to_groups[tag_name] = group_set
|
| 1052 |
+
idx = name_to_idx[tag_name]
|
| 1053 |
+
for g in group_set:
|
| 1054 |
+
group_members.setdefault(g, []).append(idx)
|
| 1055 |
+
|
| 1056 |
+
self._group_to_tags_idx = {
|
| 1057 |
+
g: np.array(idxs, dtype=np.int64) for g, idxs in group_members.items()
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
+
print(f'[Engine] Tag Group loaded, {len(self._tag_to_groups)} tags, '
|
| 1061 |
+
f'{len(self._group_to_tags_idx)} groups')
|
core/models.py
CHANGED
|
@@ -49,6 +49,8 @@ class SearchRequest:
|
|
| 49 |
target_categories: list[str] = field(
|
| 50 |
default_factory=lambda: ['General', 'Character', 'Copyright']
|
| 51 |
)
|
|
|
|
|
|
|
| 52 |
|
| 53 |
|
| 54 |
@dataclass
|
|
|
|
| 49 |
target_categories: list[str] = field(
|
| 50 |
default_factory=lambda: ['General', 'Character', 'Copyright']
|
| 51 |
)
|
| 52 |
+
group_mode: str = "off" # "off" / "expand" / "diverse"
|
| 53 |
+
max_per_group: int = 2 # ไป
diverse ๆจกๅผ็ๆ
|
| 54 |
|
| 55 |
|
| 56 |
@dataclass
|
mcp_server.py
CHANGED
|
@@ -41,6 +41,8 @@ async def search_tags(
|
|
| 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.
|
|
@@ -60,6 +62,11 @@ Search Danbooru tags using natural language and return a ready-to-use prompt.
|
|
| 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 |
|
|
@@ -188,6 +195,8 @@ Each result: tag, cn_name, category, final_score, count[, wiki if include_wiki=T
|
|
| 188 |
show_nsfw=show_nsfw,
|
| 189 |
use_segmentation=use_segmentation,
|
| 190 |
target_categories=target_categories,
|
|
|
|
|
|
|
| 191 |
)
|
| 192 |
response = await asyncio.to_thread(tagger.search, request)
|
| 193 |
# ่ฎกๆฐ๏ผๆฏๆฌก MCP ๆ็ดข่ฐ็จๅ่ฎกๅ
ฅๆ็ดขใๆๅใๅคๅถ๏ผ่ฎฟ้ฎไธๅ
|
|
|
|
| 41 |
show_nsfw: bool = True,
|
| 42 |
include_wiki: bool = False,
|
| 43 |
category: str = "all",
|
| 44 |
+
group_mode: str = "off",
|
| 45 |
+
max_per_group: int = 2,
|
| 46 |
) -> str:
|
| 47 |
"""
|
| 48 |
Search Danbooru tags using natural language and return a ready-to-use prompt.
|
|
|
|
| 62 |
"character" โ Character: named characters from any series
|
| 63 |
Use this when you know what kind of tag you need โ e.g. looking for a
|
| 64 |
character name vs. describing a scene visually.
|
| 65 |
+
- group_mode: Tag group processing mode. Default "off".
|
| 66 |
+
"off" โ No group processing (backward compatible)
|
| 67 |
+
"expand" โ Boost same-group tags for concept exploration
|
| 68 |
+
"diverse" โ Limit tags per group for scene diversity
|
| 69 |
+
- max_per_group: Max tags per group in diverse mode. Default 2.
|
| 70 |
|
| 71 |
## Query writing guide
|
| 72 |
|
|
|
|
| 195 |
show_nsfw=show_nsfw,
|
| 196 |
use_segmentation=use_segmentation,
|
| 197 |
target_categories=target_categories,
|
| 198 |
+
group_mode=group_mode,
|
| 199 |
+
max_per_group=max_per_group,
|
| 200 |
)
|
| 201 |
response = await asyncio.to_thread(tagger.search, request)
|
| 202 |
# ่ฎกๆฐ๏ผๆฏๆฌก MCP ๆ็ดข่ฐ็จๅ่ฎกๅ
ฅๆ็ดขใๆๅใๅคๅถ๏ผ่ฎฟ้ฎไธๅ
|
ui_nicegui.py
CHANGED
|
@@ -74,7 +74,16 @@ OPTIONAL_COLS = {
|
|
| 74 |
|
| 75 |
# localStorage key ไธ้
็ฝฎ็ๆฌ๏ผ็ๆฌๅๆดๆถ่ชๅจไธขๅผๆง้
็ฝฎ
|
| 76 |
_CONFIG_LS_KEY = 'danbooru_search_config'
|
| 77 |
-
_CONFIG_VERSION =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
|
| 80 |
|
|
@@ -133,6 +142,7 @@ class DanbooruSearchUI:
|
|
| 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๏ผไฟ็ๅ
ผๅฎน
|
|
@@ -152,6 +162,10 @@ class DanbooruSearchUI:
|
|
| 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
|
|
@@ -252,6 +266,9 @@ class DanbooruSearchUI:
|
|
| 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)});")
|
|
@@ -281,6 +298,10 @@ class DanbooruSearchUI:
|
|
| 281 |
ui.run_javascript(f"localStorage.removeItem('{_CONFIG_LS_KEY}');")
|
| 282 |
return
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
if self.input_top_k and 'top_k' in cfg:
|
| 285 |
self.input_top_k.set_value(cfg['top_k'])
|
| 286 |
if self.input_limit and 'limit' in cfg:
|
|
@@ -289,6 +310,10 @@ class DanbooruSearchUI:
|
|
| 289 |
self.input_weight.set_value(cfg['popularity_weight'])
|
| 290 |
if self.input_segment and 'use_segmentation' in cfg:
|
| 291 |
self.input_segment.set_value(cfg['use_segmentation'])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
# NSFW๏ผไป
ๅจๅนณๅฐๅ
่ฎธๆถๆขๅค
|
| 294 |
if nsfw_allowed() and self.input_nsfw and 'show_nsfw' in cfg:
|
|
@@ -448,10 +473,10 @@ class DanbooruSearchUI:
|
|
| 448 |
if not DanbooruTagger.is_ready():
|
| 449 |
asyncio.ensure_future(self._hide_banner_when_ready())
|
| 450 |
|
| 451 |
-
# โโ
|
| 452 |
-
self.
|
| 453 |
|
| 454 |
-
# โโ
|
| 455 |
self._build_notice()
|
| 456 |
|
| 457 |
# โโ 2. ๆ็ดขๅก็ โโ
|
|
@@ -478,31 +503,34 @@ class DanbooruSearchUI:
|
|
| 478 |
|
| 479 |
# โโ MCP ไธ็บฟ้็ฅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 480 |
|
| 481 |
-
def
|
| 482 |
self.mcp_notice = ui.card().classes(
|
| 483 |
'w-full bg-green-50 border-l-4 border-green-500 p-0 overflow-hidden'
|
| 484 |
)
|
| 485 |
with self.mcp_notice:
|
| 486 |
-
with ui.column().classes('px-4 py-3 w-full gap-
|
| 487 |
with ui.row().classes('items-center justify-between w-full'):
|
| 488 |
-
ui.label('
|
| 489 |
ui.button(icon='close').props('flat dense round color=grey-6') \
|
| 490 |
.on_click(self._dismiss_mcp_notice)
|
| 491 |
ui.html(
|
| 492 |
-
'
|
| 493 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
'<a href="https://huggingface.co/spaces/SAkizuki/WenQiuYue" '
|
| 495 |
'target="_blank" rel="noopener noreferrer" '
|
| 496 |
'class="text-green-700 font-bold underline">้ฎ็งๆ Space</a>๏ผ'
|
| 497 |
-
'
|
| 498 |
-
'
|
| 499 |
-
'API ้ขๅบฆๆ้๏ผ็บฆ 30 ๅ
๏ผ๏ผ็จๅฎๅณๆญข๏ผไป
ไพไฝ้ชใ'
|
| 500 |
-
'</span>'
|
| 501 |
-
' '
|
| 502 |
'<a href="https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline#mcp-ๆฅๅฃ" '
|
| 503 |
'target="_blank" rel="noopener noreferrer" '
|
| 504 |
-
'class="text-green-700 underline">
|
| 505 |
-
).classes('text-
|
| 506 |
|
| 507 |
def _dismiss_mcp_notice(self):
|
| 508 |
if self.mcp_notice:
|
|
@@ -561,21 +589,33 @@ class DanbooruSearchUI:
|
|
| 561 |
self.spinner = ui.spinner(size='2em').classes('hidden')
|
| 562 |
|
| 563 |
with ui.row().classes('w-full gap-6 items-center mt-3 flex-wrap'):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
with ui.row().classes('items-center gap-2'):
|
| 565 |
ui.label('Top K (่ฏญไน็ธๅ
ณ)').classes('text-sm text-gray-600')
|
| 566 |
self.input_top_k = ui.number(value=10, min=1, max=200).classes('w-20') \
|
| 567 |
.props('outlined dense')
|
|
|
|
| 568 |
|
| 569 |
with ui.row().classes('items-center gap-2'):
|
| 570 |
ui.label('็ปๆไธ้').classes('text-sm text-gray-600')
|
| 571 |
self.input_limit = ui.number(value=80, min=10, max=500).classes('w-20') \
|
| 572 |
.props('outlined dense')
|
|
|
|
| 573 |
|
| 574 |
with ui.row().classes('items-center gap-2'):
|
| 575 |
ui.label('็ญๅบฆๆ้').classes('text-sm text-gray-600')
|
| 576 |
self.input_weight = ui.slider(min=0.0, max=1.0, value=0.15, step=0.05).classes('w-32')
|
| 577 |
ui.label().bind_text_from(self.input_weight, 'value', lambda v: f"{v:.2f}") \
|
| 578 |
.classes('text-sm font-mono text-gray-700 w-8')
|
|
|
|
| 579 |
|
| 580 |
with ui.switch('ๆพ็คบ NSFW(ๆไบบ) ๅ
ๅฎน', value=False).props('color=red') as _nsfw_sw:
|
| 581 |
if not nsfw_allowed():
|
|
@@ -591,6 +631,7 @@ class DanbooruSearchUI:
|
|
| 591 |
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
|
| 592 |
ui.label('ๅ
ณ้ญๅ็ณป็ปๅฐๅชๅน้
ๅฎๆดๅฅๅญ๏ผ้็จไบ็ฒพๅๆ็ดขๆดๅฅใ').style('font-size:14px;')
|
| 593 |
self.input_segment = _seg_sw
|
|
|
|
| 594 |
|
| 595 |
with ui.expansion('้ซ็บง้้กน', icon='tune').classes('w-full mt-2'):
|
| 596 |
with ui.column().classes('w-full p-3 gap-4'):
|
|
@@ -632,6 +673,21 @@ class DanbooruSearchUI:
|
|
| 632 |
self.sw_layer.on('update:model-value', self._update_table_columns)
|
| 633 |
self.sw_source.on('update:model-value', self._update_table_columns)
|
| 634 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
# โโ ๅทฒ้ๆ ็ญพๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 636 |
|
| 637 |
def _build_selection_bar(self):
|
|
@@ -797,6 +853,11 @@ class DanbooruSearchUI:
|
|
| 797 |
self.selection_count_label.text = '0'
|
| 798 |
show_nsfw_val = self.input_nsfw.value
|
| 799 |
self._refresh_related([], show_nsfw_val)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 800 |
self._save_staged_tags()
|
| 801 |
ui.notify('ๅทฒๆธ
็ฉบๆๆๅทฒ้ๆ ็ญพ', type='warning')
|
| 802 |
|
|
@@ -901,6 +962,17 @@ class DanbooruSearchUI:
|
|
| 901 |
with self.related_list_container:
|
| 902 |
ui.label('่ฏทๅ
ๆ็ดขๅนถๅพ้ๆ ็ญพโฆ').classes('text-sm text-gray-400 italic p-4')
|
| 903 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 905 |
# ๆธฒๆๅ
ณ่ๆจ่ๅ่กจ
|
| 906 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -934,7 +1006,9 @@ class DanbooruSearchUI:
|
|
| 934 |
except Exception:
|
| 935 |
pass
|
| 936 |
|
| 937 |
-
sources_str = 'ใ'.join(
|
|
|
|
|
|
|
| 938 |
CAT_LABEL = {'General': '้็จ', 'Character': '่ง่ฒ', 'Copyright': 'ไฝๅ'}
|
| 939 |
cat_label = CAT_LABEL.get(r.category, '')
|
| 940 |
tooltip_html = ''
|
|
@@ -975,12 +1049,18 @@ class DanbooruSearchUI:
|
|
| 975 |
|
| 976 |
# ๆ ็ญพๅ๏ผๅฏ็นๅป่ทณ่ฝฌ๏ผ+ ไธญๆๅ
|
| 977 |
with ui.column().classes('flex-grow gap-0 min-w-0'):
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 984 |
|
| 985 |
if cn_first:
|
| 986 |
ui.label(cn_first).classes('text-xs text-gray-500 truncate')
|
|
@@ -1087,6 +1167,8 @@ class DanbooruSearchUI:
|
|
| 1087 |
use_segmentation=self.input_segment.value if self.input_segment else True,
|
| 1088 |
target_layers=target_layers_list,
|
| 1089 |
target_categories=target_cats_list,
|
|
|
|
|
|
|
| 1090 |
)
|
| 1091 |
response = await run.io_bound(tagger.search, request)
|
| 1092 |
|
|
@@ -1264,6 +1346,8 @@ class DanbooruSearchUI:
|
|
| 1264 |
# โโ ๅ
ณ่ๆจ่ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1265 |
|
| 1266 |
def _refresh_related(self, related: list, show_nsfw: bool):
|
|
|
|
|
|
|
| 1267 |
selected_now = set(self._get_selected_tags())
|
| 1268 |
old_related = self.current_related
|
| 1269 |
new_tags = {r.tag for r in related}
|
|
@@ -1285,8 +1369,94 @@ class DanbooruSearchUI:
|
|
| 1285 |
show_nsfw,
|
| 1286 |
)
|
| 1287 |
self._refresh_related(related, show_nsfw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1288 |
asyncio.ensure_future(_do())
|
| 1289 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1290 |
# โโ ่กจๆ ผๅๅจๆๆดๆฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1291 |
|
| 1292 |
def _update_table_columns(self, e=None):
|
|
@@ -1299,6 +1469,35 @@ class DanbooruSearchUI:
|
|
| 1299 |
cols.append(OPTIONAL_COLS['source'])
|
| 1300 |
self.result_table.columns = cols
|
| 1301 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1302 |
# โโ NSFW ๅๆข โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1303 |
|
| 1304 |
def on_nsfw_toggle(self, e):
|
|
@@ -1443,17 +1642,17 @@ if __name__ in {'__main__', '__mp_main__'}:
|
|
| 1443 |
@app.get('/robots.txt')
|
| 1444 |
def robots_txt():
|
| 1445 |
content = (
|
| 1446 |
-
|
| 1447 |
-
|
| 1448 |
-
|
| 1449 |
-
|
| 1450 |
-
|
| 1451 |
)
|
| 1452 |
return PlainTextResponse(content)
|
| 1453 |
|
| 1454 |
@app.head('/')
|
| 1455 |
async def head_root():
|
| 1456 |
-
return PlainTextResponse(
|
| 1457 |
|
| 1458 |
ui.run(
|
| 1459 |
host=host,
|
|
@@ -1462,4 +1661,4 @@ if __name__ in {'__main__', '__mp_main__'}:
|
|
| 1462 |
reload=not is_cloud(),
|
| 1463 |
show=not is_cloud(),
|
| 1464 |
reconnect_timeout=120,
|
| 1465 |
-
)
|
|
|
|
| 74 |
|
| 75 |
# localStorage key ไธ้
็ฝฎ็ๆฌ๏ผ็ๆฌๅๆดๆถ่ชๅจไธขๅผๆง้
็ฝฎ
|
| 76 |
_CONFIG_LS_KEY = 'danbooru_search_config'
|
| 77 |
+
_CONFIG_VERSION = 5
|
| 78 |
+
|
| 79 |
+
# ๆ็ดขๆจกๅผ้ข่ฎพ
|
| 80 |
+
_SEARCH_MODE_PRESETS: dict[str, dict] = {
|
| 81 |
+
'็ฒพ็กฎๆฅ่ฏ': {'top_k': 20, 'limit': 10, 'popularity_weight': 0.15, 'use_segmentation': False, 'group_mode': 'off', 'max_per_group': 2},
|
| 82 |
+
'ๆฆๅฟตๆฉๅฑ': {'top_k': 80, 'limit': 80, 'popularity_weight': 0.15, 'use_segmentation': True, 'group_mode': 'expand', 'max_per_group': 2},
|
| 83 |
+
'ๆ่ฟฐๆฅ่ฏ': {'top_k': 20, 'limit': 20, 'popularity_weight': 0.15, 'use_segmentation': False, 'group_mode': 'off', 'max_per_group': 2},
|
| 84 |
+
'ๅฎๆดๅบๆฏ': {'top_k': 5, 'limit': 80, 'popularity_weight': 0.15, 'use_segmentation': True, 'group_mode': 'diverse', 'max_per_group': 2},
|
| 85 |
+
}
|
| 86 |
+
_SEARCH_MODE_OPTIONS = ['่ชๅฎไน'] + list(_SEARCH_MODE_PRESETS.keys())
|
| 87 |
|
| 88 |
|
| 89 |
|
|
|
|
| 142 |
|
| 143 |
self.result_table = None # ๅทฆๆ ่กจๆ ผ
|
| 144 |
self.related_list_container = None # ๅณๆ ๅ
ณ่ๆจ่ๅ่กจ
|
| 145 |
+
self.group_expansion_container = None # ๅณๆ Group ๅ็ฑปๆฉๅฑ
|
| 146 |
self.results_section = None # ๆดไธช็ปๆๅบๅ๏ผๆ็ดขๅ้่๏ผ
|
| 147 |
self.selection_count_label = None
|
| 148 |
self.selected_display = None # ๅทฒๅบๅผ textarea๏ผไฟ็ๅ
ผๅฎน
|
|
|
|
| 162 |
self.input_weight = None
|
| 163 |
self.input_nsfw = None
|
| 164 |
self.input_segment = None
|
| 165 |
+
self.input_search_mode = None
|
| 166 |
+
self.input_group_mode = None
|
| 167 |
+
self.input_max_per_group = None
|
| 168 |
+
self._applying_preset = False
|
| 169 |
self.search_input = None
|
| 170 |
self.keywords_container = None
|
| 171 |
self.spinner = None
|
|
|
|
| 266 |
'search_query': self.search_input.value if self.search_input else '',
|
| 267 |
'notice_expanded': bool(self.notice_expansion.value) if self.notice_expansion else True,
|
| 268 |
'mcp_notice_dismissed': not bool(self.mcp_notice.visible) if self.mcp_notice else False,
|
| 269 |
+
'search_mode': self.input_search_mode.value if self.input_search_mode else '่ชๅฎไน',
|
| 270 |
+
'group_mode': self.input_group_mode.value if self.input_group_mode else 'off',
|
| 271 |
+
'max_per_group': int(self.input_max_per_group.value) if self.input_max_per_group else 2,
|
| 272 |
}
|
| 273 |
js = _json.dumps(cfg, ensure_ascii=False)
|
| 274 |
ui.run_javascript(f"localStorage.setItem('{_CONFIG_LS_KEY}', {_json.dumps(js)});")
|
|
|
|
| 298 |
ui.run_javascript(f"localStorage.removeItem('{_CONFIG_LS_KEY}');")
|
| 299 |
return
|
| 300 |
|
| 301 |
+
# ๆขๅคๆ็ดขๆจกๅผ๏ผไผ่งฆๅ้ข่ฎพๅกซๅ
๏ผไฝ _applying_preset ้ฒๆญข่ๅจ่ฆ็๏ผ
|
| 302 |
+
if self.input_search_mode and 'search_mode' in cfg:
|
| 303 |
+
self.input_search_mode.set_value(cfg['search_mode'])
|
| 304 |
+
|
| 305 |
if self.input_top_k and 'top_k' in cfg:
|
| 306 |
self.input_top_k.set_value(cfg['top_k'])
|
| 307 |
if self.input_limit and 'limit' in cfg:
|
|
|
|
| 310 |
self.input_weight.set_value(cfg['popularity_weight'])
|
| 311 |
if self.input_segment and 'use_segmentation' in cfg:
|
| 312 |
self.input_segment.set_value(cfg['use_segmentation'])
|
| 313 |
+
if self.input_group_mode and 'group_mode' in cfg:
|
| 314 |
+
self.input_group_mode.set_value(cfg['group_mode'])
|
| 315 |
+
if self.input_max_per_group and 'max_per_group' in cfg:
|
| 316 |
+
self.input_max_per_group.set_value(cfg['max_per_group'])
|
| 317 |
|
| 318 |
# NSFW๏ผไป
ๅจๅนณๅฐๅ
่ฎธๆถๆขๅค
|
| 319 |
if nsfw_allowed() and self.input_nsfw and 'show_nsfw' in cfg:
|
|
|
|
| 473 |
if not DanbooruTagger.is_ready():
|
| 474 |
asyncio.ensure_future(self._hide_banner_when_ready())
|
| 475 |
|
| 476 |
+
# โโ 0. ๅ
ฌๅๆ ๏ผๅ็ฑปๆ ็ญพ + MCP๏ผโโ
|
| 477 |
+
self._build_group_notice()
|
| 478 |
|
| 479 |
+
# โโ 1. ๆณจๆไบ้กน โโ
|
| 480 |
self._build_notice()
|
| 481 |
|
| 482 |
# โโ 2. ๆ็ดขๅก็ โโ
|
|
|
|
| 503 |
|
| 504 |
# โโ MCP ไธ็บฟ้็ฅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 505 |
|
| 506 |
+
def _build_group_notice(self):
|
| 507 |
self.mcp_notice = ui.card().classes(
|
| 508 |
'w-full bg-green-50 border-l-4 border-green-500 p-0 overflow-hidden'
|
| 509 |
)
|
| 510 |
with self.mcp_notice:
|
| 511 |
+
with ui.column().classes('px-4 py-3 w-full gap-2'):
|
| 512 |
with ui.row().classes('items-center justify-between w-full'):
|
| 513 |
+
ui.label('๐งช ๆฐๅ่ฝ๏ผๅ็ฑปๆ ็ญพ๏ฟฝ๏ฟฝ๏ฟฝๅฑ๏ผbeta๏ผ').classes('text-sm font-bold text-green-800')
|
| 514 |
ui.button(icon='close').props('flat dense round color=grey-6') \
|
| 515 |
.on_click(self._dismiss_mcp_notice)
|
| 516 |
ui.html(
|
| 517 |
+
'ๅพ้ๆ ็ญพๅ๏ผๅณไพง้ขๆฟๅบ้จไผๅบ็ฐ<b>ๅ็ฑปๆ ็ญพ</b>ๅบๅ๏ผ'
|
| 518 |
+
'ๅฑ็คบๅทฒ้ๆ ็ญพๆๅฑๅ็ปไธญ็ๅ
ถไปๆ ็ญพใ<b>็นๅป่ฒๅ</b>ๅณๅฏ้ไธญ/ๅๆถ๏ผ'
|
| 519 |
+
'้ไธญ็ๆ ็ญพไผๅ ๅ
ฅๅทฒ้ๅ่กจ๏ผๅฏ็ดๆฅๅคๅถไธบ Promptใ'
|
| 520 |
+
).classes('text-xs text-green-900')
|
| 521 |
+
ui.separator().classes('my-1')
|
| 522 |
+
ui.html(
|
| 523 |
+
'MCP ๆๅกๅทฒไธ็บฟ โ ๆฏๆ้่ฟ MCP ๅ่ฎฎๆฅๅ
ฅ AI Agent๏ผๅฆ Claude Desktop๏ผใ'
|
| 524 |
+
'ๆ็ฎก็ไฝ้ช โ '
|
| 525 |
'<a href="https://huggingface.co/spaces/SAkizuki/WenQiuYue" '
|
| 526 |
'target="_blank" rel="noopener noreferrer" '
|
| 527 |
'class="text-green-700 font-bold underline">้ฎ็งๆ Space</a>๏ผ'
|
| 528 |
+
'<span class="text-gray-500 ml-1">API ้ขๅบฆๆ้๏ผไป
ไพไฝ้ชใ</span>'
|
| 529 |
+
' '
|
|
|
|
|
|
|
|
|
|
| 530 |
'<a href="https://github.com/SuzumiyaAkizuki/DanbooruSearchOnline#mcp-ๆฅๅฃ" '
|
| 531 |
'target="_blank" rel="noopener noreferrer" '
|
| 532 |
+
'class="text-green-700 underline">ๆฅๅ
ฅๆๆกฃ โ</a>'
|
| 533 |
+
).classes('text-xs text-green-900')
|
| 534 |
|
| 535 |
def _dismiss_mcp_notice(self):
|
| 536 |
if self.mcp_notice:
|
|
|
|
| 589 |
self.spinner = ui.spinner(size='2em').classes('hidden')
|
| 590 |
|
| 591 |
with ui.row().classes('w-full gap-6 items-center mt-3 flex-wrap'):
|
| 592 |
+
with ui.row().classes('items-center gap-2'):
|
| 593 |
+
ui.label('ๆ็ดขๆจกๅผ (beta)').classes('text-sm text-gray-600')
|
| 594 |
+
self.input_search_mode = ui.select(
|
| 595 |
+
_SEARCH_MODE_OPTIONS, value='่ชๅฎไน',
|
| 596 |
+
).classes('w-28').props('outlined dense')
|
| 597 |
+
self.input_search_mode.on('update:model-value', self._on_search_mode_change)
|
| 598 |
+
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
|
| 599 |
+
ui.label('้ๆฉๆจกๅผ่ชๅจๅกซๅ
ๅฏนๅบๅๆฐ๏ผๆๅจไฟฎๆนๅๆฐๅ่ชๅจๅไธบใ่ชๅฎไนใ').style('font-size:14px;')
|
| 600 |
+
|
| 601 |
with ui.row().classes('items-center gap-2'):
|
| 602 |
ui.label('Top K (่ฏญไน็ธๅ
ณ)').classes('text-sm text-gray-600')
|
| 603 |
self.input_top_k = ui.number(value=10, min=1, max=200).classes('w-20') \
|
| 604 |
.props('outlined dense')
|
| 605 |
+
self.input_top_k.on('update:model-value', self._on_param_changed)
|
| 606 |
|
| 607 |
with ui.row().classes('items-center gap-2'):
|
| 608 |
ui.label('็ปๆไธ้').classes('text-sm text-gray-600')
|
| 609 |
self.input_limit = ui.number(value=80, min=10, max=500).classes('w-20') \
|
| 610 |
.props('outlined dense')
|
| 611 |
+
self.input_limit.on('update:model-value', self._on_param_changed)
|
| 612 |
|
| 613 |
with ui.row().classes('items-center gap-2'):
|
| 614 |
ui.label('็ญๅบฆๆ้').classes('text-sm text-gray-600')
|
| 615 |
self.input_weight = ui.slider(min=0.0, max=1.0, value=0.15, step=0.05).classes('w-32')
|
| 616 |
ui.label().bind_text_from(self.input_weight, 'value', lambda v: f"{v:.2f}") \
|
| 617 |
.classes('text-sm font-mono text-gray-700 w-8')
|
| 618 |
+
self.input_weight.on('update:model-value', self._on_param_changed)
|
| 619 |
|
| 620 |
with ui.switch('ๆพ็คบ NSFW(ๆไบบ) ๅ
ๅฎน', value=False).props('color=red') as _nsfw_sw:
|
| 621 |
if not nsfw_allowed():
|
|
|
|
| 631 |
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
|
| 632 |
ui.label('ๅ
ณ้ญๅ็ณป็ปๅฐๅชๅน้
ๅฎๆดๅฅๅญ๏ผ้็จไบ็ฒพๅๆ็ดขๆดๅฅใ').style('font-size:14px;')
|
| 633 |
self.input_segment = _seg_sw
|
| 634 |
+
self.input_segment.on('update:model-value', self._on_param_changed)
|
| 635 |
|
| 636 |
with ui.expansion('้ซ็บง้้กน', icon='tune').classes('w-full mt-2'):
|
| 637 |
with ui.column().classes('w-full p-3 gap-4'):
|
|
|
|
| 673 |
self.sw_layer.on('update:model-value', self._update_table_columns)
|
| 674 |
self.sw_source.on('update:model-value', self._update_table_columns)
|
| 675 |
|
| 676 |
+
with ui.column().classes('gap-2'):
|
| 677 |
+
ui.label('ๆ ็ญพๅ็ปๆจกๅผ (beta)').classes('font-bold text-sm text-gray-700')
|
| 678 |
+
self.input_group_mode = ui.select(
|
| 679 |
+
['off', 'expand', 'diverse'], value='off',
|
| 680 |
+
).classes('w-40').props('outlined dense')
|
| 681 |
+
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
|
| 682 |
+
ui.label('off=ๅ
ณ้ญ | expand=ๅ็ฑปๅฌๅๅขๅผบ | diverse=ๅคๆ ทๆง็บฆๆ').style('font-size:14px;')
|
| 683 |
+
self.input_group_mode.on('update:model-value', self._on_param_changed)
|
| 684 |
+
|
| 685 |
+
self.input_max_per_group = ui.number(
|
| 686 |
+
value=2, min=1, max=10,
|
| 687 |
+
).classes('w-20').props('outlined dense')
|
| 688 |
+
ui.label('ๆฏ็ปๆๅคงๆ ็ญพๆฐ๏ผdiverse ๆจกๅผ๏ผ').classes('text-xs text-gray-500')
|
| 689 |
+
self.input_max_per_group.on('update:model-value', self._on_param_changed)
|
| 690 |
+
|
| 691 |
# โโ ๅทฒ้ๆ ็ญพๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 692 |
|
| 693 |
def _build_selection_bar(self):
|
|
|
|
| 853 |
self.selection_count_label.text = '0'
|
| 854 |
show_nsfw_val = self.input_nsfw.value
|
| 855 |
self._refresh_related([], show_nsfw_val)
|
| 856 |
+
# ๆธ
็ฉบ Group ๅ็ฑปๆฉๅฑ
|
| 857 |
+
if self.group_expansion_container is not None:
|
| 858 |
+
self.group_expansion_container.clear()
|
| 859 |
+
with self.group_expansion_container:
|
| 860 |
+
ui.label('่ฏทๅ
ๆ็ดขๅนถๅพ้ๆ ็ญพโฆ').classes('text-sm text-gray-400 italic p-4')
|
| 861 |
self._save_staged_tags()
|
| 862 |
ui.notify('ๅทฒๆธ
็ฉบๆๆๅทฒ้ๆ ็ญพ', type='warning')
|
| 863 |
|
|
|
|
| 962 |
with self.related_list_container:
|
| 963 |
ui.label('่ฏทๅ
ๆ็ดขๅนถๅพ้ๆ ็ญพโฆ').classes('text-sm text-gray-400 italic p-4')
|
| 964 |
|
| 965 |
+
# โโ Group ๅ็ฑปๆฉๅฑ๏ผๆๅ ๅบๅ๏ผโโ
|
| 966 |
+
ui.separator().classes('my-2')
|
| 967 |
+
with ui.row().classes('items-center gap-2 mb-1 w-full'):
|
| 968 |
+
ui.label('ๅ็ฑปๆ ็ญพ (beta)').classes('font-bold text-sm text-gray-600')
|
| 969 |
+
with ui.icon('info_outline', size='xs', color='grey').classes('cursor-help'):
|
| 970 |
+
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
|
| 971 |
+
ui.label('ๅบไบๆ ็ญพๅ็ปๆฐๆฎ๏ผๅฑ็คบๅทฒ้ๆ ็ญพๆๅฑๅ็ปไธญ็ๅ
ถไปๆ ็ญพใ็นๅปๅฑๅผๆฅ็ใ').style('font-size:14px;')
|
| 972 |
+
self.group_expansion_container = ui.column().classes('w-full gap-0')
|
| 973 |
+
with self.group_expansion_container:
|
| 974 |
+
ui.label('่ฏทๅ
ๆ็ดขๅนถๅพ้ๆ ็ญพโฆ').classes('text-sm text-gray-400 italic p-4')
|
| 975 |
+
|
| 976 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 977 |
# ๆธฒๆๅ
ณ่ๆจ่ๅ่กจ
|
| 978 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 1006 |
except Exception:
|
| 1007 |
pass
|
| 1008 |
|
| 1009 |
+
sources_str = 'ใ'.join(
|
| 1010 |
+
s.replace('tag_group:', '') for s in r.sources
|
| 1011 |
+
) if r.sources else 'โ'
|
| 1012 |
CAT_LABEL = {'General': '้็จ', 'Character': '่ง่ฒ', 'Copyright': 'ไฝๅ'}
|
| 1013 |
cat_label = CAT_LABEL.get(r.category, '')
|
| 1014 |
tooltip_html = ''
|
|
|
|
| 1049 |
|
| 1050 |
# ๆ ็ญพๅ๏ผๅฏ็นๅป่ทณ่ฝฌ๏ผ+ ไธญๆๅ
|
| 1051 |
with ui.column().classes('flex-grow gap-0 min-w-0'):
|
| 1052 |
+
with ui.row().classes('items-center gap-1'):
|
| 1053 |
+
link = ui.link(
|
| 1054 |
+
tag,
|
| 1055 |
+
f'https://danbooru.donmai.us/wiki_pages/{tag}',
|
| 1056 |
+
new_tab=True
|
| 1057 |
+
).classes('tag-link text-primary font-bold text-xs')
|
| 1058 |
+
link.on('click', self._mark_interaction)
|
| 1059 |
+
if r.sources and r.sources[0].startswith('tag_group:'):
|
| 1060 |
+
group_display = r.sources[0].replace('tag_group:', '')
|
| 1061 |
+
ui.label(group_display).classes(
|
| 1062 |
+
'text-xs text-orange-500 font-bold bg-orange-50 px-1 rounded'
|
| 1063 |
+
)
|
| 1064 |
|
| 1065 |
if cn_first:
|
| 1066 |
ui.label(cn_first).classes('text-xs text-gray-500 truncate')
|
|
|
|
| 1167 |
use_segmentation=self.input_segment.value if self.input_segment else True,
|
| 1168 |
target_layers=target_layers_list,
|
| 1169 |
target_categories=target_cats_list,
|
| 1170 |
+
group_mode=self.input_group_mode.value if self.input_group_mode else 'off',
|
| 1171 |
+
max_per_group=int(self.input_max_per_group.value) if self.input_max_per_group else 2,
|
| 1172 |
)
|
| 1173 |
response = await run.io_bound(tagger.search, request)
|
| 1174 |
|
|
|
|
| 1346 |
# โโ ๅ
ณ่ๆจ่ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1347 |
|
| 1348 |
def _refresh_related(self, related: list, show_nsfw: bool):
|
| 1349 |
+
if related is None:
|
| 1350 |
+
related = []
|
| 1351 |
selected_now = set(self._get_selected_tags())
|
| 1352 |
old_related = self.current_related
|
| 1353 |
new_tags = {r.tag for r in related}
|
|
|
|
| 1369 |
show_nsfw,
|
| 1370 |
)
|
| 1371 |
self._refresh_related(related, show_nsfw)
|
| 1372 |
+
# ๅผๆญฅๅ ่ฝฝ Group ๅ็ฑปๆฉๅฑ
|
| 1373 |
+
if selected_tags:
|
| 1374 |
+
group_data = await run.io_bound(
|
| 1375 |
+
tagger.get_group_candidates,
|
| 1376 |
+
selected_tags,
|
| 1377 |
+
show_nsfw,
|
| 1378 |
+
)
|
| 1379 |
+
self._render_group_expansion(group_data, selected_tags, show_nsfw)
|
| 1380 |
asyncio.ensure_future(_do())
|
| 1381 |
|
| 1382 |
+
def _render_group_expansion(self, group_data: list, selected_tags: list[str], show_nsfw: bool):
|
| 1383 |
+
"""ๆธฒๆ Group ๅ็ฑปๆฉๅฑๅบๅใ"""
|
| 1384 |
+
if self.group_expansion_container is None:
|
| 1385 |
+
return
|
| 1386 |
+
self.group_expansion_container.clear()
|
| 1387 |
+
|
| 1388 |
+
if not group_data:
|
| 1389 |
+
with self.group_expansion_container:
|
| 1390 |
+
ui.label('ๅทฒ้ๆ ็ญพๆ ๅ็ปไฟกๆฏ').classes('text-sm text-gray-400 italic p-2')
|
| 1391 |
+
return
|
| 1392 |
+
|
| 1393 |
+
# ๅ็ฑป่ฒ่กจ๏ผ(่ๆฏ่ฒ, ้ไธญๅๆทฑ่ฒ, ๆๅญ่ฒ)
|
| 1394 |
+
CAT_STYLE = {
|
| 1395 |
+
'General': ('rgba(59,130,246,0.12)', 'rgba(59,130,246,0.30)', 'rgb(37,99,235)'),
|
| 1396 |
+
'Character': ('rgba(34,197,94,0.12)', 'rgba(34,197,94,0.30)', 'rgb(22,163,74)'),
|
| 1397 |
+
'Copyright': ('rgba(236,72,153,0.12)', 'rgba(236,72,153,0.30)', 'rgb(219,39,119)'),
|
| 1398 |
+
}
|
| 1399 |
+
default_style = ('rgba(107,114,128,0.12)', 'rgba(107,114,128,0.30)', 'rgb(55,65,81)')
|
| 1400 |
+
|
| 1401 |
+
with self.group_expansion_container:
|
| 1402 |
+
for group_info in group_data:
|
| 1403 |
+
group_name = group_info['group']
|
| 1404 |
+
tags = group_info['tags']
|
| 1405 |
+
display_name = group_name.replace('tag_group:', '')
|
| 1406 |
+
|
| 1407 |
+
with ui.expansion(
|
| 1408 |
+
f'{display_name} ({len(tags)} ไธชๆ ็ญพ)',
|
| 1409 |
+
icon='label',
|
| 1410 |
+
).classes('w-full').props('dense'):
|
| 1411 |
+
with ui.element('div').classes('w-full grid grid-cols-2 gap-1 p-1').style('max-height: 600px; overflow-y: auto;'):
|
| 1412 |
+
for t in tags:
|
| 1413 |
+
tag = t['tag']
|
| 1414 |
+
cn_first = t['cn_name'].split(',')[0].strip() if t['cn_name'] else ''
|
| 1415 |
+
cat = t['category']
|
| 1416 |
+
wiki_text = str(t.get('wiki', ''))
|
| 1417 |
+
bg_normal, bg_selected, text_color = CAT_STYLE.get(cat, default_style)
|
| 1418 |
+
|
| 1419 |
+
# ่ฒๅๅฎนๅจ
|
| 1420 |
+
chip = ui.element('div').classes(
|
| 1421 |
+
'rounded cursor-pointer transition-all duration-150 px-2 py-1.5'
|
| 1422 |
+
).style(f'background-color: {bg_normal}; color: {text_color};')
|
| 1423 |
+
|
| 1424 |
+
with chip:
|
| 1425 |
+
with ui.row().classes('items-center gap-1 w-full'):
|
| 1426 |
+
ui.label(tag).classes('text-xs font-bold truncate flex-grow')
|
| 1427 |
+
count = t['post_count']
|
| 1428 |
+
if count > 0:
|
| 1429 |
+
if count >= 10000:
|
| 1430 |
+
count_str = f'{count/1000:.0f}k'
|
| 1431 |
+
elif count >= 1000:
|
| 1432 |
+
count_str = f'{count/1000:.1f}k'
|
| 1433 |
+
else:
|
| 1434 |
+
count_str = str(count)
|
| 1435 |
+
ui.label(count_str).classes('text-xs opacity-60')
|
| 1436 |
+
if cn_first:
|
| 1437 |
+
ui.label(cn_first).classes('text-xs opacity-70 truncate block')
|
| 1438 |
+
|
| 1439 |
+
# Wiki tooltip
|
| 1440 |
+
if wiki_text:
|
| 1441 |
+
with chip:
|
| 1442 |
+
with ui.tooltip().props('content-class="bg-black text-white shadow-4" max-width="400px"'):
|
| 1443 |
+
ui.html(wiki_text).style('font-size:13px;line-height:1.4;max-width:380px;')
|
| 1444 |
+
|
| 1445 |
+
# ็นๅป้ไธญ/ๅๆถ
|
| 1446 |
+
is_selected = [False]
|
| 1447 |
+
|
| 1448 |
+
def _toggle(e, _chip=chip, _tag=tag, _bg_n=bg_normal, _bg_s=bg_selected):
|
| 1449 |
+
if is_selected[0]:
|
| 1450 |
+
_chip.style(f'background-color: {_bg_n};')
|
| 1451 |
+
self.chip_extra_selected.discard(_tag)
|
| 1452 |
+
else:
|
| 1453 |
+
_chip.style(f'background-color: {_bg_s};')
|
| 1454 |
+
self.chip_extra_selected.add(_tag)
|
| 1455 |
+
is_selected[0] = not is_selected[0]
|
| 1456 |
+
self._render_selected_chips()
|
| 1457 |
+
|
| 1458 |
+
chip.on('click', _toggle)
|
| 1459 |
+
|
| 1460 |
# โโ ่กจๆ ผๅๅจๆๆดๆฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1461 |
|
| 1462 |
def _update_table_columns(self, e=None):
|
|
|
|
| 1469 |
cols.append(OPTIONAL_COLS['source'])
|
| 1470 |
self.result_table.columns = cols
|
| 1471 |
|
| 1472 |
+
# โโ ๆ็ดขๆจกๅผ / ๅๆฐ่ๅจ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1473 |
+
|
| 1474 |
+
def _on_search_mode_change(self, _e=None):
|
| 1475 |
+
mode = self.input_search_mode.value if self.input_search_mode else None
|
| 1476 |
+
if not mode or mode == '่ชๅฎไน' or mode not in _SEARCH_MODE_PRESETS:
|
| 1477 |
+
return
|
| 1478 |
+
preset = _SEARCH_MODE_PRESETS[mode]
|
| 1479 |
+
self._applying_preset = True
|
| 1480 |
+
try:
|
| 1481 |
+
if self.input_top_k:
|
| 1482 |
+
self.input_top_k.set_value(preset['top_k'])
|
| 1483 |
+
if self.input_limit:
|
| 1484 |
+
self.input_limit.set_value(preset['limit'])
|
| 1485 |
+
if self.input_weight:
|
| 1486 |
+
self.input_weight.set_value(preset['popularity_weight'])
|
| 1487 |
+
if self.input_segment:
|
| 1488 |
+
self.input_segment.set_value(preset['use_segmentation'])
|
| 1489 |
+
if self.input_group_mode:
|
| 1490 |
+
self.input_group_mode.set_value(preset['group_mode'])
|
| 1491 |
+
if self.input_max_per_group:
|
| 1492 |
+
self.input_max_per_group.set_value(preset['max_per_group'])
|
| 1493 |
+
finally:
|
| 1494 |
+
self._applying_preset = False
|
| 1495 |
+
|
| 1496 |
+
def _on_param_changed(self, _e=None):
|
| 1497 |
+
if not self._applying_preset and self.input_search_mode:
|
| 1498 |
+
if self.input_search_mode.value != '่ชๅฎไน':
|
| 1499 |
+
self.input_search_mode.set_value('่ชๅฎไน')
|
| 1500 |
+
|
| 1501 |
# โโ NSFW ๅๆข โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1502 |
|
| 1503 |
def on_nsfw_toggle(self, e):
|
|
|
|
| 1642 |
@app.get('/robots.txt')
|
| 1643 |
def robots_txt():
|
| 1644 |
content = (
|
| 1645 |
+
'User-agent: *\n'
|
| 1646 |
+
'Allow: /$\n'
|
| 1647 |
+
'Disallow: /api/\n'
|
| 1648 |
+
'Disallow: /_nicegui/\n'
|
| 1649 |
+
'Disallow: /socket.io/\n'
|
| 1650 |
)
|
| 1651 |
return PlainTextResponse(content)
|
| 1652 |
|
| 1653 |
@app.head('/')
|
| 1654 |
async def head_root():
|
| 1655 |
+
return PlainTextResponse('')
|
| 1656 |
|
| 1657 |
ui.run(
|
| 1658 |
host=host,
|
|
|
|
| 1661 |
reload=not is_cloud(),
|
| 1662 |
show=not is_cloud(),
|
| 1663 |
reconnect_timeout=120,
|
| 1664 |
+
)
|