SAkizuki commited on
Commit
e3630d8
·
verified ·
1 Parent(s): 2dab0ab

Auto-sync from GitHub Actions

Browse files
Files changed (2) hide show
  1. core/engine.py +184 -1
  2. mcp_server.py +195 -176
core/engine.py CHANGED
@@ -12,6 +12,7 @@ DanbooruTagger 核心引擎
12
  from __future__ import annotations
13
 
14
  import asyncio
 
15
  import json
16
  import os
17
  import re
@@ -215,6 +216,7 @@ class DanbooruTagger:
215
  self._group_cn_names: dict[str, str] = {}
216
  self._tag_artist_index: dict[str, list[tuple]] = {}
217
  self._artist_top_tags: dict[str, list[tuple]] = {} # artist → [(tag, npmi, cn_short), ...]
 
218
  self.is_loaded: bool = False
219
 
220
  # 预提取的列数组,避免热点路径上反复执行 df.iloc[idx]
@@ -1323,6 +1325,182 @@ class DanbooruTagger:
1323
  result[artist] = items
1324
  return result
1325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1326
  def search_artists_pipeline(
1327
  self,
1328
  query: str,
@@ -1506,12 +1684,17 @@ class DanbooruTagger:
1506
 
1507
  # 构建反向索引:artist → top tags(按 NPMI 降序)
1508
  artist_tags: dict[str, list[tuple]] = {}
 
1509
  for tag, entries in index.items():
1510
- for artist, npmi, cooc, _ in entries:
1511
  artist_tags.setdefault(artist, []).append((tag, npmi, cooc))
 
 
 
1512
  for artist, entries in artist_tags.items():
1513
  entries.sort(key=lambda x: -x[1])
1514
  self._artist_top_tags = artist_tags
 
1515
 
1516
  print(
1517
  f'[Engine] 标签-画师共现表加载完成,{len(index):,} 个标签,'
 
12
  from __future__ import annotations
13
 
14
  import asyncio
15
+ import difflib
16
  import json
17
  import os
18
  import re
 
216
  self._group_cn_names: dict[str, str] = {}
217
  self._tag_artist_index: dict[str, list[tuple]] = {}
218
  self._artist_top_tags: dict[str, list[tuple]] = {} # artist → [(tag, npmi, cn_short), ...]
219
+ self._artist_post_count: dict[str, int] = {}
220
  self.is_loaded: bool = False
221
 
222
  # 预提取的列数组,避免热点路径上反复执行 df.iloc[idx]
 
1325
  result[artist] = items
1326
  return result
1327
 
1328
+ @staticmethod
1329
+ def _normalize_artist_name(name: str) -> str:
1330
+ """Normalize user-entered artist names toward Danbooru tag form."""
1331
+ text = str(name or "").strip().lower()
1332
+ if text.startswith("@"):
1333
+ text = text[1:].strip()
1334
+ text = re.sub(r"[\s\-]+", "_", text)
1335
+ text = re.sub(r"_+", "_", text)
1336
+ return text.strip("_")
1337
+
1338
+ @staticmethod
1339
+ def _compact_artist_key(name: str) -> str:
1340
+ return re.sub(r"[\W_]+", "", str(name or "").lower())
1341
+
1342
+ def resolve_artist_name(self, artist_name: str) -> dict[str, Any]:
1343
+ """Resolve a user-entered artist name to the artist co-occurrence index."""
1344
+ artists = set(self._artist_top_tags.keys())
1345
+ normalized = self._normalize_artist_name(artist_name)
1346
+
1347
+ if artist_name in artists:
1348
+ return {
1349
+ "artist": artist_name,
1350
+ "matched_by": "exact",
1351
+ "candidates": [],
1352
+ }
1353
+
1354
+ if normalized in artists:
1355
+ return {
1356
+ "artist": normalized,
1357
+ "matched_by": "normalized_exact",
1358
+ "candidates": [],
1359
+ }
1360
+
1361
+ compact_query = self._compact_artist_key(artist_name)
1362
+ compact_map: dict[str, list[str]] = {}
1363
+ for artist in artists:
1364
+ compact_map.setdefault(self._compact_artist_key(artist), []).append(artist)
1365
+ compact_matches = compact_map.get(compact_query, [])
1366
+ if len(compact_matches) == 1:
1367
+ return {
1368
+ "artist": compact_matches[0],
1369
+ "matched_by": "compact_exact",
1370
+ "candidates": [],
1371
+ }
1372
+ if len(compact_matches) > 1:
1373
+ return {
1374
+ "artist": None,
1375
+ "matched_by": "ambiguous_compact",
1376
+ "candidates": sorted(compact_matches)[:10],
1377
+ }
1378
+
1379
+ close = difflib.get_close_matches(normalized, sorted(artists), n=5, cutoff=0.78)
1380
+ if len(close) == 1:
1381
+ return {
1382
+ "artist": close[0],
1383
+ "matched_by": "fuzzy",
1384
+ "candidates": close,
1385
+ }
1386
+ return {
1387
+ "artist": None,
1388
+ "matched_by": "not_found",
1389
+ "candidates": close,
1390
+ }
1391
+
1392
+ def resolve_tag_name(self, tag_name: str) -> dict[str, Any]:
1393
+ """Resolve a user-entered tag name to the canonical tag index without semantic search."""
1394
+ tags = set(self._name_to_idx.keys())
1395
+ normalized = self._normalize_artist_name(tag_name)
1396
+
1397
+ if tag_name in tags:
1398
+ return {
1399
+ "tag": tag_name,
1400
+ "matched_by": "exact",
1401
+ "candidates": [],
1402
+ }
1403
+
1404
+ if normalized in tags:
1405
+ return {
1406
+ "tag": normalized,
1407
+ "matched_by": "normalized_exact",
1408
+ "candidates": [],
1409
+ }
1410
+
1411
+ plural = f"{normalized}s"
1412
+ if plural in tags:
1413
+ return {
1414
+ "tag": plural,
1415
+ "matched_by": "plural_exact",
1416
+ "candidates": [],
1417
+ }
1418
+ if normalized.endswith("s") and normalized[:-1] in tags:
1419
+ return {
1420
+ "tag": normalized[:-1],
1421
+ "matched_by": "singular_exact",
1422
+ "candidates": [],
1423
+ }
1424
+
1425
+ compact_query = self._compact_artist_key(tag_name)
1426
+ compact_map: dict[str, list[str]] = {}
1427
+ for tag in tags:
1428
+ compact_map.setdefault(self._compact_artist_key(tag), []).append(tag)
1429
+ compact_matches = compact_map.get(compact_query, [])
1430
+ if len(compact_matches) == 1:
1431
+ return {
1432
+ "tag": compact_matches[0],
1433
+ "matched_by": "compact_exact",
1434
+ "candidates": [],
1435
+ }
1436
+ if len(compact_matches) > 1:
1437
+ return {
1438
+ "tag": None,
1439
+ "matched_by": "ambiguous_compact",
1440
+ "candidates": sorted(compact_matches)[:10],
1441
+ }
1442
+
1443
+ close = difflib.get_close_matches(normalized, sorted(tags), n=5, cutoff=0.78)
1444
+ if len(close) == 1:
1445
+ return {
1446
+ "tag": close[0],
1447
+ "matched_by": "fuzzy",
1448
+ "candidates": close,
1449
+ }
1450
+ return {
1451
+ "tag": None,
1452
+ "matched_by": "not_found",
1453
+ "candidates": close,
1454
+ }
1455
+
1456
+ def get_artist_profile(self, artist_name: str, top_n: int = 20,
1457
+ show_nsfw: bool = True) -> dict[str, Any]:
1458
+ """Return a resolved artist and their common co-occurring tags."""
1459
+ resolved = self.resolve_artist_name(artist_name)
1460
+ artist = resolved["artist"]
1461
+ if not artist:
1462
+ return {
1463
+ "error": "artist_not_found",
1464
+ "input": artist_name,
1465
+ "matched_by": resolved["matched_by"],
1466
+ "candidates": resolved["candidates"],
1467
+ "message": (
1468
+ "未在画师共现库中找到唯一画师;这不代表 Danbooru 标签不存在,"
1469
+ "也不应使用 search_tags 验证画师名。"
1470
+ ),
1471
+ }
1472
+
1473
+ top_tags: list[dict[str, str]] = []
1474
+ for tag, _npmi, _cooc in self._artist_top_tags.get(artist, []):
1475
+ if len(top_tags) >= top_n:
1476
+ break
1477
+ if not show_nsfw and self._name_to_idx is not None and tag in self._name_to_idx:
1478
+ idx = self._name_to_idx[tag]
1479
+ if self._arr_nsfw is not None and self._arr_nsfw[idx] == '1':
1480
+ continue
1481
+
1482
+ cn_short = ""
1483
+ if self._name_to_idx is not None and tag in self._name_to_idx:
1484
+ idx = self._name_to_idx[tag]
1485
+ cn_full = str(self._arr_cn_name[idx]) if self._arr_cn_name is not None else ""
1486
+ cn_short = cn_full.split(',')[0].strip() if cn_full else ""
1487
+ top_tags.append({
1488
+ "tag": tag,
1489
+ "cn_name": cn_short,
1490
+ })
1491
+
1492
+ return {
1493
+ "artist": artist,
1494
+ "input": artist_name,
1495
+ "matched_by": resolved["matched_by"],
1496
+ "post_count": self._artist_post_count.get(artist, 0),
1497
+ "top_tags": top_tags,
1498
+ "note": (
1499
+ "这些是该画师作品中常共现的标签,可作为风格参考;"
1500
+ "不是对画风的完整语义描述。"
1501
+ ),
1502
+ }
1503
+
1504
  def search_artists_pipeline(
1505
  self,
1506
  query: str,
 
1684
 
1685
  # 构建反向索引:artist → top tags(按 NPMI 降序)
1686
  artist_tags: dict[str, list[tuple]] = {}
1687
+ artist_post_count: dict[str, int] = {}
1688
  for tag, entries in index.items():
1689
+ for artist, npmi, cooc, artist_pc in entries:
1690
  artist_tags.setdefault(artist, []).append((tag, npmi, cooc))
1691
+ artist_post_count[artist] = max(
1692
+ artist_post_count.get(artist, 0), artist_pc,
1693
+ )
1694
  for artist, entries in artist_tags.items():
1695
  entries.sort(key=lambda x: -x[1])
1696
  self._artist_top_tags = artist_tags
1697
+ self._artist_post_count = artist_post_count
1698
 
1699
  print(
1700
  f'[Engine] 标签-画师共现表加载完成,{len(index):,} 个标签,'
mcp_server.py CHANGED
@@ -13,6 +13,7 @@ MCP 服务层
13
  支持的工具:
14
  search_tags 自然语言搜索标签
15
  get_related_tags 基于共现表查关联推荐
 
16
  get_anima_format 返回 Anima 模型 Hybrid 提示词格式规范
17
  get_newbie_format 返回 NewBie 模型 XML 提示词格式规范
18
  """
@@ -72,6 +73,29 @@ mcp = FastMCP(
72
  )
73
 
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  @mcp.tool()
76
  async def search_tags(
77
  query: str,
@@ -81,65 +105,66 @@ async def search_tags(
81
  include_wiki: bool = False,
82
  ) -> str:
83
  """
84
- Search Danbooru tags using natural language and return a ready-to-use prompt.
85
- Only supported for general, copyright, and character tag searches; **artists and meta tags are not supported.**
86
-
87
- ## Args
88
- - query: Natural language description (Chinese recommended).
89
- - search_mode: Preset strategy. **Default is "full_scene" — keep it unless the user's intent is genuinely exploratory.**
90
- "full_scene" — **DEFAULT.** Use whenever the user gives a concrete picture description: a specific
91
- scene, subject(s), clothing, pose, action, or background — no matter how detailed or how
92
- many elements. The user wants ONE coherent prompt for ONE intended image.
 
 
 
 
 
93
  (e.g. "一个穿着白色水手服的少女在雨中奔跑", "金发双马尾女孩坐在教室窗边看书,夕阳",
94
  "芙兰朵露 金发 辫子 发带 连衣裙 围裙 灯笼裤")
95
- "concept_explore" — **ONLY for open-ended browsing**, when the user wants to SEE A VARIETY of options for a
96
- vague/single concept and pick from them — i.e. "show me what kinds of X exist".
97
- Returns up to 80 candidates → high token cost. Do NOT use just because a description has
98
- many elements; a detailed scene is still "full_scene".
99
  (e.g. "各种各样的汉服", "兔耳朵都有哪些", "赛博朋克服装有什么风格")
100
- "subject_describe" — **WARNING: Only for describing ONE single visual concept.** Tokenizer is DISABLED in this
101
- mode, so it cannot parse multi-element queries. If the query contains a character name +
102
- attributes (e.g. "芙兰朵露 金发 连衣裙"), multiple clothing items, or any combination of
103
- visual elements, you MUST use "full_scene" instead.
104
- Valid use cases: "EVA中蓝发驾驶员" (single character concept), "灯笼裤" (single item),
105
- "两侧有开口,前方有拉绳的运动短裤" (single item with details).
106
- "precise_lookup" — Precise lookup / spell fix (e.g. "selafuku", "thighhigh")
107
- - DECISION RULE: Does the user want one specific picture (full_scene) or to browse many options for a concept
108
- (→ concept_explore)? A long, multi-element description still maps to full_scene — element count is NOT the signal,
109
- exploratory intent is.
110
- - CRITICAL: "subject_describe" is ONLY for queries about a SINGLE visual concept (one item, one attribute, one character
111
- type). Any query with a character name + attributes, multiple items, or a scene description MUST use "full_scene".
112
- When in doubt, use "full_scene" it handles all concrete image descriptions correctly.
113
- - category: Filter to a specific tag category. Default "all".
114
- "all" All (通用 + 版权 + 人物)
115
- "general" — Visual attributes, clothing, pose, background, etc.
116
- "character" Named characters from any series
117
- "copyright" — Specific anime/game/franchise titles
118
- - show_nsfw: Include NSFW tags. Default True.
119
- - include_wiki: Append wiki description to each result. Default False.
120
- Set True when tags are unfamiliar and need disambiguation.
121
-
122
- ## Query writing guide
123
-
124
- Use **spaces, newlines, Chinese commas (,), or Chinese dunhao (、)** to manually separate concepts.
125
- Each delimiter-bounded segment ≤7 characters stays atomic — the engine respects your intent.
126
-
127
- | Query style | Example |
128
  |---|---|
129
- | Concept list (spaces) | `运动社团 校队 比赛 运动会` |
130
- | Concept list (dun hao) | `反乌托邦、赛博朋克、蒸汽朋克` |
131
- | Natural sentence | `一个穿着白色水手服的少女在雨中奔跑` |
132
- | Mixed | `运动社团 一个穿水手服的少女` |
133
 
134
- ## Workflow
135
 
136
- After search_tags, pass selected tags to get_related_tags to discover complementary tags via co-occurrence.
137
- Chain freely: search_tags → get_related_tags → get_related_tags → search_tags for multi-hop exploration.
138
 
139
- ## Returns
140
 
141
- JSON with: prompt (comma-separated tags), keywords, results.
142
- Each result: tag, cn_name[, wiki if include_wiki=True].
143
  """
144
  _SEARCH_MODE_PRESETS: dict[str, dict] = {
145
  "precise_lookup": {"top_k": 10, "limit": 10, "popularity_weight": 0.15, "use_segmentation": False, "group_mode": "off", "max_per_group": 2},
@@ -218,91 +243,63 @@ async def get_related_tags(
218
  include_wiki: bool = False,
219
  ) -> str:
220
  """
221
- Return co-occurrence-based tag recommendations for a given tag list (NPMI scoring).
222
- Only supported for general, copyright, and character tag searches; **artists and meta tags are not supported.**
 
 
 
223
 
224
- This tool surfaces tags that frequently appear alongside the seeds in
225
- Danbooru, mixing categories (General / Character / Copyright) by design.
226
 
227
- ## Typical use cases
228
 
229
- - Attributecharacters who have it
230
- e.g. ["fingerless_gloves"] → tifa_lockhart, cammy_white, bridget_(guilty_gear), ...
231
- - Workcharacters in it
232
- e.g. ["overlord_(maruyama)"] → shalltear_bloodfallen, ainz_ooal_gown, albedo_(overlord), ...
233
- - Charactertheir visual attributes
234
- e.g. ["amiya_(arknights)"] → outfits, expressions, accessories
235
- - Theme exploration
236
- e.g. ["fighter_jet"] → aircraft types, actions, backgrounds
237
- - Multi-tag intersection
238
- e.g. ["maid", "twintails"] → tags specific to the combination, scored by summed NPMI
239
 
240
- For within-category exploration (e.g. "more clothing tags like X"), use search_tags
241
- with the `category` parameter instead.
242
 
243
- ## Workflow
244
 
245
- Chain freely: search_tags → get_related_tags → get_related_tags → search_tags.
246
- Each hop along the co-occurrence graph reveals tags unreachable by semantic search alone.
247
 
248
- ## Args
249
 
250
- - tags: List of canonical Danbooru tag names (underscores, no spaces).
251
- e.g. ["white_serafuku", "sailor_collar"]
252
- - limit: Max recommendations returned. Default 50.
253
- - show_nsfw: Include NSFW tags. Default True.
254
- - include_wiki: Append wiki description to each result. Default False.
255
- Set True when result tags are unfamiliar and need disambiguation.
256
 
257
- ## Returns
258
 
259
- JSON array sorted by aggregated NPMI score (descending). Each result:
260
  - tag, cn_name
261
- - sources: seed tags that contributed to this score
262
- - wiki: only if include_wiki=True
263
  """
264
  tagger = await DanbooruTagger.get_instance()
265
 
266
- # ── 检查标签是否存在,不存在则尝试 search_tags 纠错 ──────────────────
267
- valid_tags = []
268
- invalid_tags = []
269
- for t in tags:
270
- if t in tagger._name_to_idx:
271
- valid_tags.append(t)
272
- else:
273
- invalid_tags.append(t)
274
-
275
- corrections = {}
276
- if invalid_tags:
277
- for bad_tag in invalid_tags:
278
- try:
279
- req = SearchRequest(
280
- query=bad_tag,
281
- top_k=5,
282
- limit=5,
283
- popularity_weight=0.15,
284
- use_segmentation=False,
285
- target_layers=['英文']
286
- )
287
- resp = await tagger.search_async(req)
288
- if resp.results:
289
- corrections[bad_tag] = resp.results[0].tag
290
- except Exception:
291
- pass
292
-
293
- if not valid_tags and not corrections:
294
- return json.dumps({
295
  "error": "所有传入的标签均不存在于标签表中",
296
  "invalid_tags": invalid_tags,
297
- }, ensure_ascii=False, indent=2)
298
-
299
- # 用纠错后的标签替换无效标签
300
- corrected_tags = []
301
- for t in tags:
302
- if t in valid_tags:
303
- corrected_tags.append(t)
304
- elif t in corrections:
305
- corrected_tags.append(corrections[t])
306
 
307
  results = await tagger.get_related_async(
308
  corrected_tags,
@@ -349,73 +346,45 @@ async def get_artist_recommendations(
349
  show_nsfw: bool = True,
350
  ) -> str:
351
  """
352
- Recommend artists who are skilled at drawing the given tags, based on NPMI co-occurrence data.
353
 
354
- Given a list of Danbooru tags (e.g. character names, clothing, styles), this tool returns
355
- artists whose works frequently co-occur with those tags on Danbooru, ranked by aggregated
356
- NPMI score.
357
 
358
- ## Args
359
- - tags: List of canonical Danbooru tag names (underscores, no spaces).
360
- e.g. ["1girl", "blue_hair", "school_uniform"]
361
- - limit: Max artists returned. Default 30.
362
- - min_cooc: Minimum co-occurrence count per (tag, artist) pair to consider. Default 3.
363
- - show_nsfw: Include NSFW artist data. Default True.
364
 
365
- ## Returns
 
 
 
 
 
 
 
366
 
367
- JSON array sorted by NPMI score (descending). Each result:
368
- - artist: Danbooru artist tag name
369
- - cooc_count: Total co-occurrence count across all input tags
370
- - post_count: Artist's total post count on Danbooru
371
- - sources: Input tags that matched this artist
372
- - top_tags: Top 10 tags this artist most frequently draws (with Chinese names)
373
  """
374
  tagger = await DanbooruTagger.get_instance()
375
 
376
  if not tags:
377
  return json.dumps({"error": "tags 列表不能为空"}, ensure_ascii=False, indent=2)
378
 
379
- # ── 检查标签是否存在,不存在则尝试 search_tags 纠错 ──────────────────
380
- valid_tags = []
381
- invalid_tags = []
382
- for t in tags:
383
- if t in tagger._name_to_idx:
384
- valid_tags.append(t)
385
- else:
386
- invalid_tags.append(t)
387
-
388
- corrections = {}
389
- if invalid_tags:
390
- for bad_tag in invalid_tags:
391
- try:
392
- req = SearchRequest(
393
- query=bad_tag,
394
- top_k=5,
395
- limit=5,
396
- popularity_weight=0.15,
397
- use_segmentation=False,
398
- target_layers=['英文']
399
- )
400
- resp = await tagger.search_async(req)
401
- if resp.results:
402
- corrections[bad_tag] = resp.results[0].tag
403
- except Exception:
404
- pass
405
-
406
- if not valid_tags and not corrections:
407
- return json.dumps({
408
  "error": "所有传入的标签均不存在于标签表中",
409
  "invalid_tags": invalid_tags,
410
- }, ensure_ascii=False, indent=2)
411
-
412
- # 用纠错后的标签替换无效标签
413
- corrected_tags = []
414
- for t in tags:
415
- if t in valid_tags:
416
- corrected_tags.append(t)
417
- elif t in corrections:
418
- corrected_tags.append(corrections[t])
419
 
420
  results = await tagger.search_artists_by_tags_async(
421
  corrected_tags, limit=limit, min_cooc=min_cooc,
@@ -456,6 +425,56 @@ async def get_artist_recommendations(
456
  return json.dumps(payload, ensure_ascii=False, indent=2)
457
 
458
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  # ── Anima 提示词格式说明 ─────────────────────────────────────────────────
460
  _ANIMA_FORMAT_INSTRUCTION = """
461
  # Anima Hybrid Prompt Format Specification
@@ -1057,4 +1076,4 @@ async def get_newbie_format() -> str:
1057
 
1058
  包含完整 NewBie 提示词格式规范的文本,涵盖 XML 结构、标签处理规则、多人物规则等。
1059
  """
1060
- return _NEWBIE_OUTPUT_FORMAT
 
13
  支持的工具:
14
  search_tags 自然语言搜索标签
15
  get_related_tags 基于共现表查关联推荐
16
+ get_artist_profile 查询单个画师常见共现标签
17
  get_anima_format 返回 Anima 模型 Hybrid 提示词格式规范
18
  get_newbie_format 返回 NewBie 模型 XML 提示词格式规范
19
  """
 
73
  )
74
 
75
 
76
+ def _resolve_canonical_tags(tagger: DanbooruTagger, tags: list[str]) -> tuple[list[str], list[str], dict[str, str], dict[str, list[str]]]:
77
+ """轻量解析 canonical tag 名,不调用语义搜索。"""
78
+ resolved_tags: list[str] = []
79
+ invalid_tags: list[str] = []
80
+ corrections: dict[str, str] = {}
81
+ candidates: dict[str, list[str]] = {}
82
+
83
+ for raw_tag in tags:
84
+ resolved = tagger.resolve_tag_name(raw_tag)
85
+ tag = resolved.get("tag")
86
+ if tag:
87
+ resolved_tags.append(tag)
88
+ if tag != raw_tag:
89
+ corrections[raw_tag] = tag
90
+ continue
91
+
92
+ invalid_tags.append(raw_tag)
93
+ if resolved.get("candidates"):
94
+ candidates[raw_tag] = resolved["candidates"]
95
+
96
+ return resolved_tags, invalid_tags, corrections, candidates
97
+
98
+
99
  @mcp.tool()
100
  async def search_tags(
101
  query: str,
 
105
  include_wiki: bool = False,
106
  ) -> str:
107
  """
108
+ 使用自然语言搜索 Danbooru 视觉标签、角色标签、作品标签,并返回可直接用于提示词的 tag 列表。
109
+
110
+ 本工具适合搜索可见画面内容:主体、服装、姿势、动作、表情、背景、构图、角色名、作品名等。
111
+
112
+ 不要用本工具搜索画师名、画师风格、creator/artist lookup,也不要用它验证某个画师标签是否存在。
113
+ 遇到 "Mika Pikazo style"、"画师 mika_pikazo"、"by redjuice""这个画师常画什么" 这类请求时,
114
+ 应改用 get_artist_profile。若用户同时给出画师/风格参考和可见画面描述,只把可见画面描述交给
115
+ search_tags,不要把画师名放进 query。
116
+
117
+ ## 参数
118
+ - query: 自然语言画面描述。推荐使用中文。
119
+ - search_mode: 搜索策略。**默认是 "full_scene";除非用户明确想探索多种候选,否则保持默认。**
120
+ "full_scene" — **默认。** 用户给出具体画面描述时使用:场景、主体、服装、姿势、动作、
121
+ 背景等,不管描述多长、元素多少。用户想要的是一张图的一组连贯提示词。
122
  (e.g. "一个穿着白色水手服的少女在雨中奔跑", "金发双马尾女孩坐在教室窗边看书,夕阳",
123
  "芙兰朵露 金发 辫子 发带 连衣裙 围裙 灯笼裤")
124
+ "concept_explore" — **只用于开放式概念浏览。** 当用户想看某个模糊/单一概念有哪些类型、
125
+ 想从大量候选中挑选时使用。会返回最多 80 个候选,token 成本较高。
126
+ 不要因为描述元素多就使用此模式;详细场景仍然属于 "full_scene"。
 
127
  (e.g. "各种各样的汉服", "兔耳朵都有哪些", "赛博朋克服装有什么风格")
128
+ "subject_describe" — **只用于描述一个单一视觉概念。** 此模式关闭分词,不能解析多元素 query。
129
+ 如果 query 包含角色名 + 属性、多个服装物件、或任何组合场景,应使用
130
+ "full_scene"
131
+ 适合:"EVA中蓝发的驾驶员"(单一角色概念)、"灯笼裤"(单一物件)、
132
+ "两侧有开口,前方有拉绳运动短裤"(带细节的单一物件)。
133
+ "precise_lookup" 精确查词 / 拼写纠错,例如 "selafuku"、"thighhigh"。
134
+ - 判断规则:用户是想得到一张具体图的提示词(→ full_scene),还是想浏览某个概念的多种候选
135
+ concept_explore)?元素数量不是判断依据,探索意图才是。
136
+ - 重要:只要 query 是具体场景、多元素组合、角色 + 属性,就用 "full_scene"。拿不准时也用
137
+ "full_scene",它能处理具体画面描述。
138
+ - category: 限定搜索类别。默认 "all"
139
+ "all" — 全部(通用 + 作品 + 角色)
140
+ "general" 可见属性、服装、姿势、背景等通用标签
141
+ "character" — 角色标签
142
+ "copyright" 动画/游戏/作品名等版权标签
143
+ - show_nsfw: 是否包含 NSFW 标签。默认 True。
144
+ - include_wiki: 是否在结果中附带 wiki 说明。默认 False。
145
+ 当标签含义不熟悉、需要消歧时设为 True。
146
+
147
+ ## query 写法建议
148
+
149
+ 可以使用**空格、换行、中文逗号(,)、顿号(、)**手动分隔概念。
150
+ 被分隔符包围且长度不超过 7 个汉字的片段会尽量保持原子性,搜索引擎会尊重你的拆分意图。
151
+
152
+ | 写法 | 示例 |
 
 
 
153
  |---|---|
154
+ | 空格分隔概念 | `运动社团 校队 比赛 运动会` |
155
+ | 顿号分隔概念 | `反乌托邦、赛博朋克、蒸汽朋克` |
156
+ | 自然句子 | `一个穿着白色水手服的少女在雨中奔跑` |
157
+ | 混合写法 | `运动社团 一个穿水手服的少女` |
158
 
159
+ ## 工作流
160
 
161
+ 调用 search_tags 后,可以把选中的标签传给 get_related_tags,通过共现关系发现互补标签。
162
+ 可按 search_tags → get_related_tags → get_related_tags → search_tags 多跳探索。
163
 
164
+ ## 返回
165
 
166
+ JSON 对象,包含 prompt(逗号分隔 tag)、keywordsresults
167
+ 每个 result 包含 tag、cn_name;当 include_wiki=True 时额外包含 wiki。
168
  """
169
  _SEARCH_MODE_PRESETS: dict[str, dict] = {
170
  "precise_lookup": {"top_k": 10, "limit": 10, "popularity_weight": 0.15, "use_segmentation": False, "group_mode": "off", "max_per_group": 2},
 
243
  include_wiki: bool = False,
244
  ) -> str:
245
  """
246
+ 根据已给定的 Danbooru 标签列表,返回基于 NPMI 共现评分的关联标签推荐。
247
+ 本工具只支持通用标签、作品标签、角色标签;**���支持画师标签和 meta 标签。**
248
+
249
+ 不要用本工具搜索画师名、画师风格、creator/artist lookup,也不要用它验证某个画师标签是否存在。
250
+ 如果用户询问某个具体画师常画什么,或询问画师风格参考,应使用 get_artist_profile。
251
 
252
+ 本工具会找出在 Danbooru 中经常与种子标签共同出现的标签。结果会按设计混合
253
+ General / Character / Copyright 类别。
254
 
255
+ ## 典型用法
256
 
257
+ - 属性拥有该属性的角色
258
+ 例如 ["fingerless_gloves"] → tifa_lockhart, cammy_white, bridget_(guilty_gear), ...
259
+ - 作品作品中的角色
260
+ 例如 ["overlord_(maruyama)"] → shalltear_bloodfallen, ainz_ooal_gown, albedo_(overlord), ...
261
+ - 角色该角色常见视觉属性
262
+ 例如 ["amiya_(arknights)"] → 服装、表情、配饰等
263
+ - 主题探索
264
+ 例如 ["fighter_jet"] → 飞机类型、动作、背景等
265
+ - 多标签交集
266
+ 例如 ["maid", "twintails"] → 与该组合强相关的标签,按聚合 NPMI 评分排序
267
 
268
+ 如果要做同类别内部探索,例如“更多类似 X 的服装标签”,请使用 search_tags 并设置 category。
 
269
 
270
+ ## 工作流
271
 
272
+ 可按 search_tags → get_related_tags → get_related_tags → search_tags 链式调用。
273
+ 沿共现图多跳探索时,可以发现单纯语义搜索不容易召回的标签。
274
 
275
+ ## 参数
276
 
277
+ - tags: canonical Danbooru tag 名列表,使用下划线,不使用空格。
278
+ 例如 ["white_serafuku", "sailor_collar"]
279
+ - limit: 最多返回的推荐数量。默认 50
280
+ - show_nsfw: 是否包含 NSFW 标签。默认 True
281
+ - include_wiki: 是否在结果中附带 wiki 说明。默认 False
282
+ 当结果标签不熟悉、需要消歧时设为 True
283
 
284
+ ## 返回
285
 
286
+ JSON 对象,results 按聚合 NPMI 分数降序排序。每个结果包含:
287
  - tag, cn_name
288
+ - sources: 对该推荐有贡献的种子标签
289
+ - wiki: 仅当 include_wiki=True 时返回
290
  """
291
  tagger = await DanbooruTagger.get_instance()
292
 
293
+ corrected_tags, invalid_tags, corrections, candidates = _resolve_canonical_tags(tagger, tags)
294
+
295
+ if not corrected_tags:
296
+ payload = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  "error": "所有传入的标签均不存在于标签表中",
298
  "invalid_tags": invalid_tags,
299
+ }
300
+ if candidates:
301
+ payload["candidates"] = candidates
302
+ return json.dumps(payload, ensure_ascii=False, indent=2)
 
 
 
 
 
303
 
304
  results = await tagger.get_related_async(
305
  corrected_tags,
 
346
  show_nsfw: bool = True,
347
  ) -> str:
348
  """
349
+ 根据标签-画师 NPMI 共现数据,推荐擅长绘制给定标签的画师。
350
 
351
+ 输入一组 canonical Danbooru 标签(例如角色名、服装、主题、视觉元素),本工具会返回作品中
352
+ 经常与这些标签共同出现的画师,并按聚合 NPMI 分数排序。
 
353
 
354
+ 本工具用于 tag → artist 推荐。输入必须是 canonical Danbooru tag 名,不是画师名。
355
+ 不要用本工具查询某个具体画师;画师 常见标签应使用 get_artist_profile。
 
 
 
 
356
 
357
+ ## 参数
358
+ - tags: canonical Danbooru tag 名列表,使用下划线,不使用空格。
359
+ 例如 ["1girl", "blue_hair", "school_uniform"]
360
+ - limit: 最多返回的画师数量。默认 30。
361
+ - min_cooc: 单个 (tag, artist) 组合进入计算所需的最小共现次数。默认 3。
362
+ - show_nsfw: 是否包含 NSFW 画师数据。默认 True。
363
+
364
+ ## 返回
365
 
366
+ JSON 对象,results NPMI 分数降序排序。每个结果包含:
367
+ - artist: Danbooru 画师 tag
368
+ - cooc_count: 所有输入标签上的累计共现次数
369
+ - post_count: 该画师在 Danbooru 的作品数
370
+ - sources: 命中该画师的输入标签
371
+ - top_tags: 该画师最常画的前 10 个标签(带中文名)
372
  """
373
  tagger = await DanbooruTagger.get_instance()
374
 
375
  if not tags:
376
  return json.dumps({"error": "tags 列表不能为空"}, ensure_ascii=False, indent=2)
377
 
378
+ corrected_tags, invalid_tags, corrections, candidates = _resolve_canonical_tags(tagger, tags)
379
+
380
+ if not corrected_tags:
381
+ payload = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  "error": "所有传入的标签均不存在于标签表中",
383
  "invalid_tags": invalid_tags,
384
+ }
385
+ if candidates:
386
+ payload["candidates"] = candidates
387
+ return json.dumps(payload, ensure_ascii=False, indent=2)
 
 
 
 
 
388
 
389
  results = await tagger.search_artists_by_tags_async(
390
  corrected_tags, limit=limit, min_cooc=min_cooc,
 
425
  return json.dumps(payload, ensure_ascii=False, indent=2)
426
 
427
 
428
+ @mcp.tool()
429
+ async def get_artist_profile(
430
+ artist_name: str,
431
+ top_n: int = 20,
432
+ show_nsfw: bool = True,
433
+ ) -> str:
434
+ """
435
+ 在画师-标签共现数据库中查询单个 Danbooru 画师,并返回该画师常见共现标签。
436
+
437
+ 当用户询问某个具体画师或画师风格参考时使用本工具,例如��
438
+ "Mika Pikazo style"、"画师 mika_pikazo"、"by redjuice"、"这个画师常画什么"。
439
+ 本工具查询的是画师数据库,不是普通视觉 tag 搜索索引。
440
+
441
+ 画师名会在查询前自动规范化。因此,当数据库中存在 "mika_pikazo" 时,
442
+ "Mika Pikazo"、"mika pikazo"、"mika_pikazo"、"MikaPikazo" 都可以解析到它。
443
+
444
+ ## 参数
445
+ - artist_name: 画师名或 Danbooru 画师 tag。允许大小写差异和空格。
446
+ - top_n: 最多返回的常见标签数量。默认 20。
447
+ - show_nsfw: 是否包含 NSFW 常见标签。默认 True。
448
+
449
+ ## 返回
450
+
451
+ JSON 对象,包含:
452
+ - artist: 解析后的 canonical Danbooru 画师 tag
453
+ - input: 原始输入
454
+ - matched_by: 匹配方式,可能是 exact / normalized_exact / compact_exact / fuzzy
455
+ - post_count: 该画师在共现数据库中的作品数
456
+ - top_tags: 常见共现标签列表,每项只包含 tag 和 cn_name
457
+ - note: 说明这些常见标签只能作为风格参考,不等于完整画风语义描述
458
+
459
+ 如果没有找到唯一画师,会返回 artist_not_found 和候选画师名。这不代表该画师 tag 在 Danbooru
460
+ 不存在,也不要改用 search_tags 验证画师名。
461
+ """
462
+ tagger = await DanbooruTagger.get_instance()
463
+ profile = tagger.get_artist_profile(
464
+ artist_name,
465
+ top_n=max(1, min(int(top_n), 100)),
466
+ show_nsfw=show_nsfw,
467
+ )
468
+
469
+ await counter.increment()
470
+ await counter.increment_mcp()
471
+ if "error" not in profile:
472
+ await counter.increment_success()
473
+ await counter.increment_copy()
474
+
475
+ return json.dumps(profile, ensure_ascii=False, indent=2)
476
+
477
+
478
  # ── Anima 提示词格式说明 ─────────────────────────────────────────────────
479
  _ANIMA_FORMAT_INSTRUCTION = """
480
  # Anima Hybrid Prompt Format Specification
 
1076
 
1077
  包含完整 NewBie 提示词格式规范的文本,涵盖 XML 结构、标签处理规则、多人物规则等。
1078
  """
1079
+ return _NEWBIE_OUTPUT_FORMAT