File size: 28,837 Bytes
6c9a052
 
 
 
 
 
 
 
 
 
 
 
 
775d296
 
 
 
6c9a052
 
 
 
d98e7b3
 
6c9a052
 
 
 
 
 
 
 
bcff293
d98e7b3
bcff293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d98e7b3
bcff293
d98e7b3
bcff293
 
 
 
 
 
 
d98e7b3
 
 
 
 
bcff293
d98e7b3
 
 
6c9a052
 
 
 
 
 
 
 
 
54316b6
 
6c9a052
 
 
 
 
54316b6
6c9a052
 
 
54316b6
 
 
d632079
54316b6
d632079
54316b6
 
 
 
 
6c9a052
 
54316b6
6c9a052
 
 
 
 
 
54316b6
 
 
 
 
 
6c9a052
54316b6
6c9a052
54316b6
 
6c9a052
 
54316b6
6c9a052
 
 
54316b6
 
 
 
 
 
 
 
6c9a052
 
 
 
 
 
 
 
54316b6
6c9a052
 
 
 
 
54316b6
 
 
6c9a052
54316b6
6c9a052
54316b6
 
6c9a052
bcff293
 
 
 
 
 
6c9a052
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570a57c
6c9a052
 
 
 
 
54316b6
6c9a052
570a57c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c9a052
 
5bb3b2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d98e7b3
5bb3b2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d632079
5bb3b2d
 
6c9a052
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54316b6
5bb3b2d
 
 
 
 
 
 
 
 
 
bcff293
 
 
e1b4c00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bcff293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
775d296
bcff293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
775d296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
"""
mcp_server.py
─────────────
MCP 服务层

挂载方式(在 ui_nicegui.py 中):
    from mcp_server import mcp
    app.mount('/mcp', mcp.streamable_http_app())

接入地址:
    https://sakizuki-danboorusearch.hf.space/mcp/mcp

支持的工具:
    search_tags        自然语言搜索标签
    get_related_tags   基于共现表查关联推荐
    get_anima_format   返回 Anima 模型 Hybrid 提示词格式规范
    get_newbie_format  返回 NewBie 模型 XML 提示词格式规范
"""

import json
import asyncio
import logging
from anyio import BrokenResourceError, ClosedResourceError
from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings
from core.engine import DanbooruTagger
from core.models import SearchRequest
import core.counter as counter
import re


# ── 过滤客户端断连/超时产生的无害报错噪音 ──────────────────────────────
class _SuppressClientDisconnect(logging.Filter):
    _SUPPRESSED: tuple = ()
    _HAS_STARLETTE: bool = False

    @classmethod
    def _init_suppressed(cls):
        if cls._SUPPRESSED:
            return
        types: list = [BrokenResourceError, ClosedResourceError, asyncio.CancelledError]
        try:
            from starlette.requests import ClientDisconnect
            types.append(ClientDisconnect)
            cls._HAS_STARLETTE = True
        except ImportError:
            pass
        cls._SUPPRESSED = tuple(types)

    def filter(self, record: logging.LogRecord) -> bool:
        self._init_suppressed()
        exc = record.exc_info[1] if record.exc_info else None
        if isinstance(exc, self._SUPPRESSED):
            return False
        # 用类名字符串兜底(避免 starlette 版本差异导致 import 失败)
        if exc is not None and not self._HAS_STARLETTE:
            name = type(exc).__name__
            if name in ('ClientDisconnect',):
                return False
        return True


_disconnect_filter = _SuppressClientDisconnect()
logging.getLogger("mcp.server.streamable_http").addFilter(_disconnect_filter)
logging.getLogger("mcp.server").addFilter(_disconnect_filter)
logging.getLogger("uvicorn.error").addFilter(_disconnect_filter)


mcp = FastMCP(
    name="danbooru-searcher",
    transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False),
)


@mcp.tool()
async def search_tags(
    query: str,
    search_mode: str = "full_scene",
    category: str = "all",
    show_nsfw: bool = True,
    include_wiki: bool = False,
) -> str:
    """
Search Danbooru tags using natural language and return a ready-to-use prompt.
Only supported for general, copyright, and character tag searches; **artists and meta tags are not supported.**

## Args
- query: Natural language description (Chinese recommended).
- search_mode: Preset strategy. Pick the one that matches your intent.
    "full_scene"       — Full scene → prompt (e.g. "一个穿着白色水手服的少女在雨中奔跑")
    "concept_explore"  — Vague concept exploration, broad recall (e.g. "赛博朋克服装", "兔耳朵", "中国风汉服")
    "subject_describe" — Describe **one** subject to find matching tags (e.g. "EVA中蓝发的驾驶员", "两侧有开口,前方有拉绳的运动短裤")
    "precise_lookup"   — Precise lookup / spell fix (e.g. "selafuku", "thighhigh")
- HINT: In subject_describe mode, the tokenizer is disabled, and you can only describe one thing at a time. To search for multiple things at once, use concept_explore or full_scene.
- category: Filter to a specific tag category. Default "all".
    "all"       — All (通用 + 版权 + 人物)
    "general"   — Visual attributes, clothing, pose, background, etc.
    "character" — Named characters from any series
    "copyright" — Specific anime/game/franchise titles
- show_nsfw: Include NSFW tags. Default True.
- include_wiki: Append wiki description to each result. Default False.
    Set True when tags are unfamiliar and need disambiguation.

## Query writing guide

Use **spaces, newlines, Chinese commas (,), or Chinese dunhao (、)** to manually separate concepts.
Each delimiter-bounded segment ≤7 characters stays atomic — the engine respects your intent.

| Query style | Example |
|---|---|
| Concept list (spaces) | `运动社团 校队 比赛 运动会` |
| Concept list (dun hao) | `反乌托邦、赛博朋克、蒸汽朋克` |
| Natural sentence | `一个穿着白色水手服的少女在雨中奔跑` |
| Mixed | `运动社团 一个穿水手服的少女` |

## Workflow

After search_tags, pass selected tags to get_related_tags to discover complementary tags via co-occurrence.
Chain freely: search_tags → get_related_tags → get_related_tags → search_tags for multi-hop exploration.

## Returns

JSON with: prompt (comma-separated tags), keywords, results.
Each result: tag, cn_name, category, final_score, count[, wiki if include_wiki=True].
    """
    _SEARCH_MODE_PRESETS: dict[str, dict] = {
        "precise_lookup":   {"top_k": 10, "limit": 10, "popularity_weight": 0.15, "use_segmentation": False, "group_mode": "off",    "max_per_group": 2},
        "concept_explore":  {"top_k": 80, "limit": 80, "popularity_weight": 0.15, "use_segmentation": True,  "group_mode": "expand",  "max_per_group": 2},
        "subject_describe": {"top_k": 20, "limit": 20, "popularity_weight": 0.15, "use_segmentation": False, "group_mode": "off",    "max_per_group": 2},
        "full_scene":       {"top_k": 5,  "limit": 80, "popularity_weight": 0.15, "use_segmentation": True,  "group_mode": "diverse", "max_per_group": 2},
    }
    preset = _SEARCH_MODE_PRESETS.get(search_mode, _SEARCH_MODE_PRESETS["full_scene"])

    _CATEGORY_MAP: dict[str, list[str]] = {
        "all":       ["General", "Character", "Copyright", "Artist", "Meta"],
        "general":   ["General"],
        "character": ["Character"],
        "copyright": ["Copyright"],
    }
    target_categories = _CATEGORY_MAP.get(
        category,
        _CATEGORY_MAP["all"],
    )

    tagger = await DanbooruTagger.get_instance()
    request = SearchRequest(
        query=query,
        top_k=preset["top_k"],
        limit=preset["limit"],
        popularity_weight=preset["popularity_weight"],
        show_nsfw=show_nsfw,
        use_segmentation=preset["use_segmentation"],
        target_categories=target_categories,
        group_mode=preset["group_mode"],
        max_per_group=preset["max_per_group"],
    )
    try:
        response = await tagger.search_async(request)
    except asyncio.TimeoutError:
        return json.dumps({
            "error": "搜索超时(120s),请简化查询或稍后重试",
        }, ensure_ascii=False, indent=2)
    # 计数:每次 MCP 搜索调用均计入搜索、成功、复制;访问不变
    await counter.increment()
    await counter.increment_success()
    await counter.increment_copy()
    await counter.increment_mcp()

    results = []
    for r in response.results:
        if r.nsfw == '1' and not show_nsfw:
            continue
        item = {
            "tag":         r.tag,
            "cn_name":     r.cn_name,
            "category":    r.category,
            "final_score": r.final_score,
            "count":       r.count,
        }
        if include_wiki:
            item["wiki"] = r.wiki
        results.append(item)

    payload = {
        "prompt":   response.tags_sfw if not show_nsfw else response.tags_all,
        "keywords": response.keywords,
        "results":  results,
    }
    han_chars = re.findall(r'[\u4e00-\u9fff]', query)
    if len(query) > 0 and len(han_chars) / len(query) < 0.5:
        payload["hint"] = (
            "检测到英文查询,该搜索引擎对中文查询优化更好,如果搜索结果不合预期,推荐用中文重试"
        )
    return json.dumps(payload, ensure_ascii=False, indent=2)



@mcp.tool()
async def get_related_tags(
    tags: list[str],
    limit: int = 50,
    show_nsfw: bool = True,
    include_wiki: bool = False,
) -> str:
    """
Return co-occurrence-based tag recommendations for a given tag list (NPMI scoring).
Only supported for general, copyright, and character tag searches; **artists and meta tags are not supported.**

This tool surfaces tags that frequently appear alongside the seeds in
Danbooru, mixing categories (General / Character / Copyright) by design.

## Typical use cases

- Attribute → characters who have it
  e.g. ["fingerless_gloves"] → tifa_lockhart, cammy_white, bridget_(guilty_gear), ...
- Work → characters in it
  e.g. ["overlord_(maruyama)"] → shalltear_bloodfallen, ainz_ooal_gown, albedo_(overlord), ...
- Character → their visual attributes
  e.g. ["amiya_(arknights)"] → outfits, expressions, accessories
- Theme exploration
  e.g. ["fighter_jet"] → aircraft types, actions, backgrounds
- Multi-tag intersection
  e.g. ["maid", "twintails"] → tags specific to the combination, scored by summed NPMI

For within-category exploration (e.g. "more clothing tags like X"), use search_tags
with the `category` parameter instead.

## Workflow

Chain freely: search_tags → get_related_tags → get_related_tags → search_tags.
Each hop along the co-occurrence graph reveals tags unreachable by semantic search alone.

## Args

- tags: List of canonical Danbooru tag names (underscores, no spaces).
        e.g. ["white_serafuku", "sailor_collar"]
- limit: Max recommendations returned. Default 50.
- show_nsfw: Include NSFW tags. Default True.
- include_wiki: Append wiki description to each result. Default False.
        Set True when result tags are unfamiliar and need disambiguation.

## Returns

JSON array sorted by aggregated NPMI score (descending). Each result:
- tag, cn_name, category, count (post_count), cooc_score (normalized to [0,1])
- sources: seed tags that contributed to this score
- wiki: only if include_wiki=True
    """
    tagger = await DanbooruTagger.get_instance()

    # ── 检查标签是否存在,不存在则尝试 search_tags 纠错 ──────────────────
    valid_tags = []
    invalid_tags = []
    for t in tags:
        if t in tagger._name_to_idx:
            valid_tags.append(t)
        else:
            invalid_tags.append(t)

    corrections = {}
    if invalid_tags:
        for bad_tag in invalid_tags:
            try:
                req = SearchRequest(
                    query=bad_tag,
                    top_k=5,
                    limit=5,
                    popularity_weight=0.15,
                    use_segmentation=False,
                    target_layers=['英文']
                )
                resp = await tagger.search_async(req)
                if resp.results:
                    corrections[bad_tag] = resp.results[0].tag
            except Exception:
                pass

    if not valid_tags and not corrections:
        return json.dumps({
            "error": "所有传入的标签均不存在于标签表中",
            "invalid_tags": invalid_tags,
        }, ensure_ascii=False, indent=2)

    # 用纠错后的标签替换无效标签
    corrected_tags = []
    for t in tags:
        if t in valid_tags:
            corrected_tags.append(t)
        elif t in corrections:
            corrected_tags.append(corrections[t])

    results = await tagger.get_related_async(
        corrected_tags,
        set(corrected_tags),
        limit,
        show_nsfw,
    )
    # 计数:每次 MCP related 调用均计入搜索、成功、复制;访问不变
    await counter.increment()
    await counter.increment_success()
    await counter.increment_copy()
    await counter.increment_mcp()

    output = []
    for r in results:
        item = {
            "tag":        r.tag,
            "cn_name":    r.cn_name,
            "category":   r.category,
            "count":      r.post_count,
            "cooc_score": r.cooc_score,
            "sources":    r.sources,
        }
        if include_wiki:
            item["wiki"] = r.wiki
        output.append(item)

    payload = {"results": output}
    if corrections:
        correction_notes = [
            f"{bad}{good}" for bad, good in corrections.items()
        ]
        payload = {
            "correction_note": "标签拼写错误,已经纠错: " + ", ".join(correction_notes),
            "corrections": corrections,
            "results": output,
        }

    return json.dumps(payload, ensure_ascii=False, indent=2)


@mcp.tool()
async def get_artist_recommendations(
    tags: list[str],
    limit: int = 30,
    min_cooc: int = 3,
    show_nsfw: bool = True,
) -> str:
    """
    Recommend artists who are skilled at drawing the given tags, based on NPMI co-occurrence data.

    Given a list of Danbooru tags (e.g. character names, clothing, styles), this tool returns
    artists whose works frequently co-occur with those tags on Danbooru, ranked by aggregated
    NPMI score.

    ## Args
    - tags: List of canonical Danbooru tag names (underscores, no spaces).
            e.g. ["1girl", "blue_hair", "school_uniform"]
    - limit: Max artists returned. Default 30.
    - min_cooc: Minimum co-occurrence count per (tag, artist) pair to consider. Default 3.
    - show_nsfw: Include NSFW artist data. Default True.

    ## Returns

    JSON array sorted by NPMI score (descending). Each result:
    - artist: Danbooru artist tag name
    - score: Aggregated NPMI score (higher = stronger association)
    - cooc_count: Total co-occurrence count across all input tags
    - post_count: Artist's total post count on Danbooru
    - sources: Input tags that matched this artist
    - hit_count: Number of input tags that matched
    """
    tagger = await DanbooruTagger.get_instance()

    if not tags:
        return json.dumps({"error": "tags 列表不能为空"}, ensure_ascii=False, indent=2)

    # ── 检查标签是否存在,不存在则尝试 search_tags 纠错 ──────────────────
    valid_tags = []
    invalid_tags = []
    for t in tags:
        if t in tagger._name_to_idx:
            valid_tags.append(t)
        else:
            invalid_tags.append(t)

    corrections = {}
    if invalid_tags:
        for bad_tag in invalid_tags:
            try:
                req = SearchRequest(
                    query=bad_tag,
                    top_k=5,
                    limit=5,
                    popularity_weight=0.15,
                    use_segmentation=False,
                    target_layers=['英文']
                )
                resp = await tagger.search_async(req)
                if resp.results:
                    corrections[bad_tag] = resp.results[0].tag
            except Exception:
                pass

    if not valid_tags and not corrections:
        return json.dumps({
            "error": "所有传入的标签均不存在于标签表中",
            "invalid_tags": invalid_tags,
        }, ensure_ascii=False, indent=2)

    # 用纠错后的标签替换无效标签
    corrected_tags = []
    for t in tags:
        if t in valid_tags:
            corrected_tags.append(t)
        elif t in corrections:
            corrected_tags.append(corrections[t])

    results = await tagger.search_artists_by_tags_async(
        corrected_tags, limit=limit, min_cooc=min_cooc,
    )

    output = []
    for r in results:
        item = {
            "artist":     r.artist,
            "score":      round(r.score, 4),
            "cooc_count": r.cooc_count,
            "post_count": r.post_count,
            "sources":    r.sources,
            "hit_count":  r.hit_count,
        }
        output.append(item)

    # 计数
    await counter.increment()
    await counter.increment_success()
    await counter.increment_copy()
    await counter.increment_mcp()

    payload = {"results": output}
    if corrections:
        correction_notes = [
            f"{bad}{good}" for bad, good in corrections.items()
        ]
        payload = {
            "correction_note": "标签拼写错误,已经纠错: " + ", ".join(correction_notes),
            "corrections": corrections,
            "results": output,
        }

    return json.dumps(payload, ensure_ascii=False, indent=2)


# ── Anima 提示词格式说明 ─────────────────────────────────────────────────
_ANIMA_FORMAT_INSTRUCTION = """请严格按以下 Anima 混合提示词(Hybrid Prompt)规范,基于提供的标签和用户描述,输出最终结果。

# Anima Prompt Composer

## Overview

将已有的 Danbooru 风格标签数据整合为 Anima 模型的最优 Hybrid 提示词。该 Skill 假定调用方已经拥有充足的标签信息(通过 Tagger、Captioner 或用户输入),仅负责按 Anima 的格式规范与社区验证的最佳实践进行结构化组装。

## 核心设计理念

Anima 是一个 2B 参数的文生图模型(CircleStone Labs × Comfy Org),基于 NVIDIA Cosmos-Predict2-2B,使用 Qwen 3 0.6B 文本编码器。它同时理解 Danbooru 标签和自然语言,但两者的行为有本质差异——标签掌控结构与精度,自然语言掌控氛围与构图。

社区的共识结论:
- **纯标签提示词**:线条锐利、色彩平整、几乎没有解剖错误,但画面扁平,缺乏光影、氛围、构图的精确控制。
- **纯自然语言提示词**:细节丰富、光影动态、气氛到位,但超过 2~3 段后结构崩塌,手部最先出问题。
- **Hybrid 混合模式**:标签主导主体结构,自然语言补充环境与氛围,获得约 80% 的主体控制力加完整的氛围控制力。

核心风险:自然语言的影响力 **远强于** 标签。当你用自然语言描述背景时,模型会忽略 `close-up`、`upper body` 等取景标签,生成广角镜头。解决方案是对取景标签使用权重语法。

## 输出格式

````markdown
## Prompt
```
[标签块:逗号分隔,单行]

[自然语言段落:2 到 3 句英文]
```

## 中文解释

[分点说明提示词设计逻辑,包含Prompt自然语言段落的完整翻译]
````

**绝对禁止**在任何部分之外添加开场白、寒暄或总结。

## 标签格式化规则

- 所有标签小写,下划线 `_` 替换为空格。**唯一例外**:`score_1` 到 `score_9` 保持下划线。
- 标签内括号用反斜杠转义:`momoko (momopoco)` → `momoko \\(momopoco\\)`
- 标签间用一个逗号加一个空格连接:`tag a, tag b, tag c`
- 不要编造不存在的标签。若不确定某标签是否存在,将该概念放入自然语言段落。
- Tag Dropout 机制意味着不需要塞入每一个相关标签——只保留最关键和区分性最强的。

## 标签块结构规则

### 官方推荐标签顺序
```
[quality/meta/year/safety] → [1girl/1boy/1other] → [character] → [series] → [@artist] → [general tags]
```

### 单人物详细结构
```
[quality/meta/safety], [1girl/1boy], [character name], [series], [@artist], [hair], [eyes], [clothing], [body/pose], [expression], [action], [background/atmosphere], [composition tags]
```

### 多人物详细结构(防串扰核心规则)
```
[quality/meta/safety], [2girls / 1girl 1boy],
[character_A name], [series_A], [A hair], [A eyes], [A clothing], [A body], [A expression],
[character_B name], [series_B], [B hair], [B eyes], [B clothing], [B body], [B expression],
[shared pose/action], [background], [atmosphere], [composition]
```

## 标签体系速查

### 质量标签(任选其一或混用)
- 人工评分系:`masterpiece`, `best quality`, `good quality`, `normal quality`, `low quality`, `worst quality`
- 美学评分系:`score_9`, `score_8`, `score_7`, `score_6` ... `score_1`(仅score标签保留下划线)

### 年代标签
- 具体年份:`year 2025`, `year 2024` ...
- 时期:`newest` (2022-2023), `recent` (2019-2021), `mid` (2015-2018), `early` (2011-2014), `old` (2005-2010)

### 元标签
`highres`, `absurdres`, `anime screenshot`, `jpeg artifacts`, `official art`

### 安全分级
`safe`, `sensitive`, `nsfw`, `explicit`

### 艺术家标签
**必须以 @ 开头**。没有 @ 前缀的风格几乎不生效。

格式:`@nnn yryr`, `@big chungus`

### 数据集标签(非动漫风格时的备选)
在提示词最开头另起一行使用,可大幅改变风格倾向:
- `ye-pop`:LAION-POP 数据集风格,偏抽象/油画/概念艺术
- `deviantart`:DeviantArt 数据集风格,偏数字绘画/插画

## 自然语言段落规则

自然语言段落严格 2 到 3 句英文,仅用于标签难以精确表达的内容:

1. **镜头取景**:angle、shot distance、framing (close-up, wide shot, dutch angle…)
2. **光线**:方向、质感、色温 (rim light, volumetric god rays, warm key light…)
3. **色彩调性**:palette、color grading (monochromatic indigo, vibrant cel-shaded…)
4. **天气与环境**:rain、fog、dappled sunlight、underwater…
5. **氛围**:somber、airy、tense、ethereal…
6. **多角色空间关系与动作**:谁在左边、谁在干什么、互动方式

**关键禁忌**:
- 不要在自然语言中重复标签已覆盖的内容(发型、瞳色、服装等)。
- 不要写超过 3 段的自然语言——超过 2~3 段后画面结构会崩溃,手部最先出问题。
- 自然语言中不要使用隐喻或情绪化修辞,应使用客观、具体、视觉化的描述。

## 默认前缀与默认值

**正向前缀**(无特殊要求时的默认值):
```
masterpiece, best quality, score_7, safe,
```

**取景默认**:若用户未指定,默认近景人物、人物面向观众。若用户有描述则以用户描述为准。

**模式默认**:采用 Hybrid 混合结构(标签 + 自然语言)。仅当用户明确要求纯标签或纯自然语言时才切换。

## 权重语法

Anima 支持 Prompt Weighting,但需要的权重值 **高于 SDXL**:

- 正常强调:`(tag:2)` 起步
- 强强调:`(tag:3)` 到 `(tag:5)`
- 权重取值范围:2 ~ 5
- 若用户提供 1.2 等较小权重,**必须放大至 2~5 区间**
- 多角色区分性特征(如一个蓝发一个红发)使用权重:`(blue hair:2)`, `(red hair:2)`

## Composition Tag 对抗自然语言漂移(关键规则)

当 Hybrid 提示词中自然语言段落包含环境描述时,模型倾向于拉远镜头,忽略 `close-up`、`upper body`、`portrait` 等取景标签。必须采取以下对抗措施:

1. **对取景标签使用强权重**:`(upper body:2)`, `(close-up:3)`
2. **在自然语言首句中明确取景**:`The composition is a tight close-up portrait...`
3. 如果仍然拉远,继续提高权重至 `(upper body:5)` 甚至 `(upper body:7)`

## 多人物特征分离规则(Anima 最高风险项)

Anima 在多人场景中极易发生特征混淆。必须严格遵守:

1. **角色属性按角色分组排列**。同一角色的发型、瞳色、服装、体型连续出现后再切换。严禁交叉排列(如 `blue hair, red hair, short hair, long hair`)。

2. **自然语言中为每个角色写一句"外观锚定短语"**。格式:`CharacterName with [key features]...` 明确指出视觉归属。这比仅靠标签的防串扰效果强得多。

3. **使用空间方位词分离角色**:left/right/foreground/background。

4. **为易混淆特征使用权重**:`(blue hair:2)`, `(red hair:2)`。

5. **角色外观在标签块中充分描述**。官方文档明确指出:先命名角色,再描述其外观。仅列出角色名而不描述外观会让模型困惑。

6. **自然语言中不重复标签内容**——自然语言补充空间关系、互动动作、光影氛围、构图取景。

## 安全标签使用规则

- 在提示 prefix 中始终包含安全分级标签(safe / sensitive / nsfw / explicit)。
- 描绘现有角色时,**禁止使用 score_8、score_9 等过强标签**,以免过拟合导致角色特征丢失。使用 `score_7` 作为上限。

## 中文解释撰写规则

- 采用分点结构,每点对应一个设计决策。
- 解释覆盖:为何选择当前提示词架构、关键标签的作用、自然语言各句的功能。
- 多人物时**必须**解释角色分组策略。
- 必须包含自然语言部分的完整中文翻译。
- 语言中立、客观、技术化。不使用感叹号、表情符号或情绪化措辞。
- 避免冗长背景介绍,只解释本次提示词中实际出现的元素。

"""


@mcp.tool()
async def get_anima_format() -> str:
    """
    返回 Anima 文生图模型的 Hybrid 混合提示词格式规范。

    当用户提到「Anima 提示词」「Anima 格式」「Anima Prompt」「Anima 模型」等关键词时,
    应在搜索标签完成、最终输出前调用此工具,以获取完整的提示词组装规范。

    ## 适用场景

    - 用户明确要求输出 Anima 模型的提示词
    - 用户提到 anima、Anima 等关键词
    - 需要将标签转换为 Anima 的 Hybrid 混合格式

    ## Returns

    包含完整 Anima 提示词格式规范的 Markdown 文本,涵盖标签格式化规则、
    自然语言段落规则、权重语法、多人物防串扰规则等。
    """
    return _ANIMA_FORMAT_INSTRUCTION


# ── NewBie 提示词格式说明 ─────────────────────────────────────────────────
_NEWBIE_OUTPUT_FORMAT = """
## 输出格式要求

你的输出包括两部分:一个 XML 代码块和代码块外的中文翻译。

### 标签处理规则
- 标签内部的空格必须替换为下划线 `_`(如 `red eyes` → `red_eyes`)
- 标签名内的括号必须用反斜杠转义(如 `momoko (momopoco)` → `momoko_\\(momopoco\\)`)
- 权重括号(如 `(daito:1.2)`)保持原样,不转义
- 括号内包含多个独立标签时,拆解为独立标签

### XML 结构

```xml
<img>
 <character_1>
  <n>角色名</n>
  <gender>性别标签 (如 1girl)</gender>
  <appearance>外貌特征 (发色, 瞳色, 身体特征等)</appearance>
  <clothing>衣着 (具体服饰)</clothing>
  <expression>表情</expression>
  <action>动作</action>
  <position>位置</position>
 </character_1>

 <!-- 若有多个角色,按 character_2, character_3 顺延 -->

 <general_tags>
  <count>人数标签</count>
  <style>画风标签(若用户未指定,默认 anime_style,realistic_shading)</style>
  <background>背景标签</background>
  <atmosphere>画面情绪、氛围标签</atmosphere>
  <quality>very_aesthetic, masterpiece, no_text</quality>
  <resolution>max_high_resolution</resolution>
  <artist>画师标签</artist>
  <objects>各种物品(包括武器、饰品等)</objects>
  <other>其它标签</other>
 </general_tags>

 <caption>
  将所有标签串联为一段流畅、详细的英文场景描述。包含光线、情绪、角色和背景。
  不要在此处提及 style 或 quality 类词汇。
 </caption>
</img>
```

在 XML 代码块结束后,输出 `<caption>` 内容的中文翻译。

### 多人物规则(防特征混淆)

如果用户提到了多个人物,必须严格遵循以下规则:

1. **角色分组**:每个 character_N 块内连续排列该角色的所有专属属性(发型、瞳色、服装、体型、表情、动作),然后再切换到下一角色。
2. **外观标签充分**:每个角色至少 5 个角色特征标签。可使用 get_related_tags 获得更多特征。
3. **属性不交叉**:禁止将不同角色的同类属性交叉排列。不同角色的特征混淆是多人场景最常见的失败模式。
4. **空间锚定**:在 `<position>` 和 `<caption>` 中明确每个角色的空间位置(如"左侧"、"右侧"、"前景"等)。
5. **caption 角色锚定**:在 `<caption>` 中为每个角色写一句外观锚定短语,使用"[角色名] with [关键特征]"的句式,明确指出视觉归属。
"""


@mcp.tool()
async def get_newbie_format() -> str:
    """
    返回 NewBie 文生图模型的 XML 格式提示词规范。

    当用户提到「NewBie 提示词」「NewBie 格式」「NewBie Prompt」「NewBie 模型」等关键词时,
    应在搜索标签完成、最终输出前调用此工具,以获取完整的 XML 格式组装规范。

    ## 适用场景

    - 用户明确要求输出 NewBie 模型的提示词
    - 用户提到 newbie、NewBie 等关键词
    - 需要将标签转换为 NewBie 的 XML 格式

    ## Returns

    包含完整 NewBie 提示词格式规范的文本,涵盖 XML 结构、标签处理规则、多人物规则等。
    """
    return _NEWBIE_OUTPUT_FORMAT