SAkizuki commited on
Commit
bcff293
·
verified ·
1 Parent(s): d632079

Auto-sync from GitHub Actions

Browse files
Files changed (3) hide show
  1. api_fastapi.py +10 -2
  2. core/engine.py +2 -2
  3. mcp_server.py +227 -7
api_fastapi.py CHANGED
@@ -26,7 +26,8 @@ FastAPI 适配层(可选)。
26
 
27
  from __future__ import annotations
28
 
29
- from fastapi import FastAPI
 
30
  from pydantic import BaseModel, Field
31
 
32
  from core.engine import DanbooruTagger
@@ -76,6 +77,8 @@ class RelatedTagOut(BaseModel):
76
  cooc_count: int
77
  cooc_score: float
78
  sources: list[str]
 
 
79
 
80
 
81
  class SearchOut(BaseModel):
@@ -104,7 +107,10 @@ async def search(body: SearchIn) -> SearchOut:
104
  request = SearchRequest(**body.model_dump())
105
 
106
  # 并发安全的异步 search(信号量串行化 + 线程池执行)
107
- response: SearchResponse = await tagger.search_async(request)
 
 
 
108
 
109
  # 计数:每次 API 搜索调用均计入搜索、成功、复制;访问不变
110
  await counter.increment()
@@ -149,6 +155,8 @@ async def related(body: RelatedIn) -> list[RelatedTagOut]:
149
  cooc_count=r.cooc_count,
150
  cooc_score=r.cooc_score,
151
  sources=r.sources,
 
 
152
  )
153
  for r in results
154
  ]
 
26
 
27
  from __future__ import annotations
28
 
29
+ import asyncio
30
+ from fastapi import FastAPI, HTTPException
31
  from pydantic import BaseModel, Field
32
 
33
  from core.engine import DanbooruTagger
 
77
  cooc_count: int
78
  cooc_score: float
79
  sources: list[str]
80
+ post_count: int = 0
81
+ wiki: str = ""
82
 
83
 
84
  class SearchOut(BaseModel):
 
107
  request = SearchRequest(**body.model_dump())
108
 
109
  # 并发安全的异步 search(信号量串行化 + 线程池执行)
110
+ try:
111
+ response: SearchResponse = await tagger.search_async(request)
112
+ except asyncio.TimeoutError:
113
+ raise HTTPException(status_code=503, detail="搜索超时(120s),请简化查询或稍后重试")
114
 
115
  # 计数:每次 API 搜索调用均计入搜索、成功、复制;访问不变
116
  await counter.increment()
 
155
  cooc_count=r.cooc_count,
156
  cooc_score=r.cooc_score,
157
  sources=r.sources,
158
+ post_count=r.post_count,
159
+ wiki=r.wiki,
160
  )
161
  for r in results
162
  ]
core/engine.py CHANGED
@@ -575,7 +575,7 @@ class DanbooruTagger:
575
  async with self._get_cpu_sem():
576
  return await asyncio.wait_for(
577
  asyncio.to_thread(self.search, request),
578
- timeout=60.0,
579
  )
580
 
581
  async def get_related_async(
@@ -1270,7 +1270,7 @@ class DanbooruTagger:
1270
  g: np.array(idxs, dtype=np.int64) for g, idxs in group_members.items()
1271
  }
1272
 
1273
- self._group_cn_names = data.get('group_cn_names', {})
1274
 
1275
  print(f'[Engine] Tag Group loaded, {len(self._tag_to_groups)} tags, '
1276
  f'{len(self._group_to_tags_idx)} groups, '
 
575
  async with self._get_cpu_sem():
576
  return await asyncio.wait_for(
577
  asyncio.to_thread(self.search, request),
578
+ timeout=120.0,
579
  )
580
 
581
  async def get_related_async(
 
1270
  g: np.array(idxs, dtype=np.int64) for g, idxs in group_members.items()
1271
  }
1272
 
1273
+ self._group_cn_names = data.get("group_cn_names", {})
1274
 
1275
  print(f'[Engine] Tag Group loaded, {len(self._tag_to_groups)} tags, '
1276
  f'{len(self._group_to_tags_idx)} groups, '
mcp_server.py CHANGED
@@ -11,8 +11,9 @@ MCP 服务层
11
  https://sakizuki-danboorusearch.hf.space/mcp/mcp
12
 
13
  支持的工具:
14
- search_tags 自然语言搜索标签
15
- get_related_tags 基于共现表查关联推荐
 
16
  """
17
 
18
  import json
@@ -27,17 +28,40 @@ import core.counter as counter
27
  import re
28
 
29
 
30
- # ── 过滤客户端断连产生的无害报错噪音 ──────────────────────────────────
31
  class _SuppressClientDisconnect(logging.Filter):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  def filter(self, record: logging.LogRecord) -> bool:
 
33
  exc = record.exc_info[1] if record.exc_info else None
34
- if isinstance(exc, (BrokenResourceError, ClosedResourceError)):
35
- return False # 丢弃该日志记录
 
 
 
 
 
36
  return True
37
 
38
 
39
  _disconnect_filter = _SuppressClientDisconnect()
40
  logging.getLogger("mcp.server.streamable_http").addFilter(_disconnect_filter)
 
41
  logging.getLogger("uvicorn.error").addFilter(_disconnect_filter)
42
 
43
 
@@ -129,7 +153,12 @@ Each result: tag, cn_name, category, final_score, count[, wiki if include_wiki=T
129
  group_mode=preset["group_mode"],
130
  max_per_group=preset["max_per_group"],
131
  )
132
- response = await tagger.search_async(request)
 
 
 
 
 
133
  # 计数:每次 MCP 搜索调用均计入搜索、成功、复制;访问不变
134
  await counter.increment()
135
  await counter.increment_success()
@@ -296,4 +325,195 @@ JSON array sorted by aggregated NPMI score (descending). Each result:
296
  "results": output,
297
  }
298
 
299
- return json.dumps(payload, ensure_ascii=False, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  https://sakizuki-danboorusearch.hf.space/mcp/mcp
12
 
13
  支持的工具:
14
+ search_tags 自然语言搜索标签
15
+ get_related_tags 基于共现表查关联推荐
16
+ get_anima_format 返回 Anima 模型 Hybrid 提示词格式规范
17
  """
18
 
19
  import json
 
28
  import re
29
 
30
 
31
+ # ── 过滤客户端断连/超时产生的无害报错噪音 ──────────────────────────────
32
  class _SuppressClientDisconnect(logging.Filter):
33
+ _SUPPRESSED: tuple = ()
34
+ _HAS_STARLETTE: bool = False
35
+
36
+ @classmethod
37
+ def _init_suppressed(cls):
38
+ if cls._SUPPRESSED:
39
+ return
40
+ types: list = [BrokenResourceError, ClosedResourceError, asyncio.CancelledError]
41
+ try:
42
+ from starlette.requests import ClientDisconnect
43
+ types.append(ClientDisconnect)
44
+ cls._HAS_STARLETTE = True
45
+ except ImportError:
46
+ pass
47
+ cls._SUPPRESSED = tuple(types)
48
+
49
  def filter(self, record: logging.LogRecord) -> bool:
50
+ self._init_suppressed()
51
  exc = record.exc_info[1] if record.exc_info else None
52
+ if isinstance(exc, self._SUPPRESSED):
53
+ return False
54
+ # 用类名字符串兜底(避免 starlette 版本差异导致 import 失败)
55
+ if exc is not None and not self._HAS_STARLETTE:
56
+ name = type(exc).__name__
57
+ if name in ('ClientDisconnect',):
58
+ return False
59
  return True
60
 
61
 
62
  _disconnect_filter = _SuppressClientDisconnect()
63
  logging.getLogger("mcp.server.streamable_http").addFilter(_disconnect_filter)
64
+ logging.getLogger("mcp.server").addFilter(_disconnect_filter)
65
  logging.getLogger("uvicorn.error").addFilter(_disconnect_filter)
66
 
67
 
 
153
  group_mode=preset["group_mode"],
154
  max_per_group=preset["max_per_group"],
155
  )
156
+ try:
157
+ response = await tagger.search_async(request)
158
+ except asyncio.TimeoutError:
159
+ return json.dumps({
160
+ "error": "搜索超时(120s),请简化查询或稍后重试",
161
+ }, ensure_ascii=False, indent=2)
162
  # 计数:每次 MCP 搜索调用均计入搜索、成功、复制;访问不变
163
  await counter.increment()
164
  await counter.increment_success()
 
325
  "results": output,
326
  }
327
 
328
+ return json.dumps(payload, ensure_ascii=False, indent=2)
329
+
330
+
331
+ # ── Anima 提示词格式说明 ─────────────────────────────────────────────────
332
+ _ANIMA_FORMAT_INSTRUCTION = """请严格按以下 Anima 混合提示词(Hybrid Prompt)规范,基于提供的标签和用户描述,输出最终结果。
333
+
334
+ # Anima Prompt Composer
335
+
336
+ ## Overview
337
+
338
+ 将已有的 Danbooru 风格标签数据整合为 Anima 模型的最优 Hybrid 提示词。该 Skill 假定调用方已经拥有充足的标签信息(通过 Tagger、Captioner 或用户输入),仅负责按 Anima 的格式规范与社区验证的最佳实践进行结构化组装。
339
+
340
+ ## 核心设计理念
341
+
342
+ Anima 是一个 2B 参数的文生图模型(CircleStone Labs × Comfy Org),基于 NVIDIA Cosmos-Predict2-2B,使用 Qwen 3 0.6B 文本编码器。它同时理解 Danbooru 标签和自然语言,但两者的行为有本质差异——标签掌控结构与精度,自然语言掌控氛围与构图。
343
+
344
+ 社区的共识结论:
345
+ - **纯标签提示词**:线条锐利、色彩平整、几乎没有解剖错误,但画面扁平,缺乏光影、氛围、构图的精确控制。
346
+ - **纯自然语言提示词**:细节丰富、光影动态、气氛到位,但超过 2~3 段后结构崩塌,手部最先出问题。
347
+ - **Hybrid 混合模式**:标签主导主体结构,自然语言补充环境与氛围,获得约 80% 的主体控制力加完整的氛围控制力。
348
+
349
+ 核心风险:自然语言的影响力 **远强于** 标签。当你用自然语言描述背景时,模型会忽略 `close-up`、`upper body` 等取景标签,生成广角镜头。解决方案是对取景标签使用权重语法。
350
+
351
+ ## 输出格式
352
+
353
+ ````markdown
354
+ ## Prompt
355
+ ```
356
+ [标签块:逗号分隔,单行]
357
+
358
+ [自然语言段落:2 到 3 句英文]
359
+ ```
360
+
361
+ ## 中文解释
362
+
363
+ [分点说明提示词设计逻辑,包���Prompt自然语言段落的完整翻译]
364
+ ````
365
+
366
+ **绝对禁止**在任何部分之外添加开场白、寒暄或总结。
367
+
368
+ ## 标签格式化规则
369
+
370
+ - 所有标签小写,下划线 `_` 替换为空格。**唯一例外**:`score_1` 到 `score_9` 保持下划线。
371
+ - 标签内括号用反斜杠转义:`momoko (momopoco)` → `momoko \\(momopoco\\)`
372
+ - 标签间用一个逗号加一个空格连接:`tag a, tag b, tag c`
373
+ - 不要编造不存在的标签。若不确定某标签是否存在,将该概念放入自然语言段落。
374
+ - Tag Dropout 机制意味着不需要塞入每一个相关标签——只保留最关键和区分性最强的。
375
+
376
+ ## 标签块结构规则
377
+
378
+ ### 官方推荐标签顺序
379
+ ```
380
+ [quality/meta/year/safety] → [1girl/1boy/1other] → [character] → [series] → [@artist] → [general tags]
381
+ ```
382
+
383
+ ### 单人物详细结构
384
+ ```
385
+ [quality/meta/safety], [1girl/1boy], [character name], [series], [@artist], [hair], [eyes], [clothing], [body/pose], [expression], [action], [background/atmosphere], [composition tags]
386
+ ```
387
+
388
+ ### 多人物详细结构(防串扰核心规则)
389
+ ```
390
+ [quality/meta/safety], [2girls / 1girl 1boy],
391
+ [character_A name], [series_A], [A hair], [A eyes], [A clothing], [A body], [A expression],
392
+ [character_B name], [series_B], [B hair], [B eyes], [B clothing], [B body], [B expression],
393
+ [shared pose/action], [background], [atmosphere], [composition]
394
+ ```
395
+
396
+ ## 标签体系速查
397
+
398
+ ### 质量标签(任选其一或混用)
399
+ - 人工评分系:`masterpiece`, `best quality`, `good quality`, `normal quality`, `low quality`, `worst quality`
400
+ - 美学评分系:`score_9`, `score_8`, `score_7`, `score_6` ... `score_1`(仅score标签保留下划线)
401
+
402
+ ### 年代标签
403
+ - 具体年份:`year 2025`, `year 2024` ...
404
+ - 时期:`newest` (2022-2023), `recent` (2019-2021), `mid` (2015-2018), `early` (2011-2014), `old` (2005-2010)
405
+
406
+ ### 元标签
407
+ `highres`, `absurdres`, `anime screenshot`, `jpeg artifacts`, `official art`
408
+
409
+ ### 安全分级
410
+ `safe`, `sensitive`, `nsfw`, `explicit`
411
+
412
+ ### 艺术家标签
413
+ **必须以 @ 开头**。没有 @ 前缀的风格几乎不生效。
414
+
415
+ 格式:`@nnn yryr`, `@big chungus`
416
+
417
+ ### 数据集标签(非动漫风格时的备选)
418
+ 在提示词最开头另起一行使用,可大幅改变风格倾向:
419
+ - `ye-pop`:LAION-POP 数据集风格,偏抽象/油画/概念艺术
420
+ - `deviantart`:DeviantArt 数据集风格,偏数字绘画/插画
421
+
422
+ ## 自然语言段落规则
423
+
424
+ 自然语言段落严格 2 到 3 句英文,仅用于标签难以精确表达的内容:
425
+
426
+ 1. **镜头取景**:angle、shot distance、framing (close-up, wide shot, dutch angle…)
427
+ 2. **光线**:方向、质感、色温 (rim light, volumetric god rays, warm key light…)
428
+ 3. **色彩调性**:palette、color grading (monochromatic indigo, vibrant cel-shaded…)
429
+ 4. **天气与环境**:rain、fog、dappled sunlight、underwater…
430
+ 5. **氛围**:somber、airy、tense、ethereal…
431
+ 6. **多角色空间关系与动作**:谁在左边、谁在干什么、互动方式
432
+
433
+ **关键禁忌**:
434
+ - 不要在自然语言中重复标签已覆盖的内容(发型、瞳色、服装等)。
435
+ - 不要写超过 3 段的自然语言——超过 2~3 段后画面结构会崩溃,手部最先出问题。
436
+ - 自然语言中不要使用隐喻或情绪化修辞,应使用客观、具体、视觉化的描述。
437
+
438
+ ## 默认前缀与默认值
439
+
440
+ **正向前缀**(无特殊要求时的默认值):
441
+ ```
442
+ masterpiece, best quality, score_7, safe,
443
+ ```
444
+
445
+ **取景默认**:若用户未指定,默认近景人物、人物面向观众。若用户有描述则以用户描述为准。
446
+
447
+ **模式默认**:采用 Hybrid 混合结构(标签 + 自然语言)。仅当用户明确要求纯标签或纯自然语言时才切换。
448
+
449
+ ## 权重语法
450
+
451
+ Anima 支持 Prompt Weighting,但需要的权重值 **高于 SDXL**:
452
+
453
+ - 正常强调:`(tag:2)` 起步
454
+ - 强强调:`(tag:3)` 到 `(tag:5)`
455
+ - 权重取值范围:2 ~ 5
456
+ - 若用户提供 1.2 等较小权重,**必须放大至 2~5 区间**
457
+ - 多角色区分性特征(如一个蓝发一个红发)使用权重:`(blue hair:1.3)`, `(red hair:1.3)`,Anima 的 Qwen 编码器支持但需要 1.3~2.0
458
+
459
+ ## Composition Tag 对抗自然语言漂移(关键规则)
460
+
461
+ 当 Hybrid 提示词中自然语言段落包含环境描述时,模型倾向于拉远镜头,忽略 `close-up`、`upper body`、`portrait` 等取景标签。必须采取以下对抗措施:
462
+
463
+ 1. **对取景标签使用强权重**:`(upper body:2)`, `(close-up:3)`
464
+ 2. **在自然语言首句中明确取景**:`The composition is a tight close-up portrait...`
465
+ 3. 如果仍然拉远,继续提高权重至 `(upper body:5)` 甚至 `(upper body:7)`
466
+
467
+ ## 多人物特征分离规则(Anima 最高风险项)
468
+
469
+ Anima 在多人场景中极易发生特征混淆。必须严格遵守:
470
+
471
+ 1. **角色属性按角色分组排列**。同一角色的发型、瞳色、服装、体型连续出现后再切换。严禁交叉排列(如 `blue hair, red hair, short hair, long hair`)。
472
+
473
+ 2. **自然语言中为每个角色写一句"外观锚定短语"**。格式:`CharacterName with [key features]...` 明确指出视觉归属。这比仅靠标签的防串扰效果强得多。
474
+
475
+ 3. **使用空间方位词分离角色**:left/right/foreground/background。
476
+
477
+ 4. **为易混淆特征使用权重**:`(blue hair:2)`, `(red hair:2)`。
478
+
479
+ 5. **角色外观在标签块中充分描述**。官方文档明确指出:先命名角色,再描述其外观。仅列出角色名而不描述外观会让模型困惑。
480
+
481
+ 6. **自然语言中不重复标签内容**——自然语言补充空间关系、互动动作、光影氛围、构图取景。
482
+
483
+ ## 安全标签使用规则
484
+
485
+ - 在提示 prefix 中始终包含安全分级标签(safe / sensitive / nsfw / explicit)。
486
+ - 描绘现有角色时,**禁止使用 score_8、score_9 等过强标签**,以免过拟合导致角色特征丢失。使用 `score_7` 作为上限。
487
+
488
+ ## 中文解释撰写规则
489
+
490
+ - 采用分点结构,每点对应一个设计决策。
491
+ - 解释覆盖:为何选择当前提示词架构、关键标签的作用、自然语言各句的功能。
492
+ - 多人物时**必须**解释角色分组策略。
493
+ - 必须包含自然语言部分的完整中文翻译。
494
+ - 语言中立、客观、技术化。不使用感叹号、表情符号或情绪化措辞。
495
+ - 避免冗长背景介绍,只解释本次提示词中实际出现的元素。
496
+
497
+ """
498
+
499
+
500
+ @mcp.tool()
501
+ async def get_anima_format() -> str:
502
+ """
503
+ 返回 Anima 文生图模型的 Hybrid 混合提示词格式规范。
504
+
505
+ 当用户提到「Anima 提示词」「Anima 格式」「Anima Prompt」「Anima 模型」等关键词时,
506
+ 应在搜索标签完成、最终输出前调用此工具,以获取完整的提示词组装规范。
507
+
508
+ ## 适用场景
509
+
510
+ - 用户明确要求输出 Anima 模型的提示词
511
+ - 用户提到 anima、Anima 等关键词
512
+ - 需要将标签转换为 Anima 的 Hybrid 混合格式
513
+
514
+ ## Returns
515
+
516
+ 包含完整 Anima 提示词格式规范的 Markdown 文本,涵盖标签格式化规则、
517
+ 自然语言段落规则、权重语法、多人物防串扰规则等。
518
+ """
519
+ return _ANIMA_FORMAT_INSTRUCTION