Whyx-PROmpTea / src /tag_searcher.py
ArtShumov's picture
feat(prod): rewrite pipeline + NoobAI + ensemble tagger (3xWD14+DeepDanbooru-ready) + 1000 artists + negative templates + history ext + scoring config
e6404d0
Raw
History Blame
8.18 kB
import functools
import json
import os
_COOCCURRENCE_PATH = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"data", "tag_cooccurrence.json",
)
_COOCURRENCE_DATA = None
DANBOORU_TAG_API = "https://danbooru.donmai.us/tags.json"
_REQUEST_TIMEOUT = 8
# relation weights: curated/semantic connections beat co-category noise.
_RELATION_BONUS = {
"character_tag": 3.0,
"common_pair": 2.0,
"synonym": 1.5,
"modifier": 1.2,
"co_category": 1.0,
}
def _load_cooccurrence() -> dict:
global _COOCURRENCE_DATA
if _COOCURRENCE_DATA is not None:
return _COOCURRENCE_DATA
try:
with open(_COOCCURRENCE_PATH, "r", encoding="utf-8") as f:
_COOCURRENCE_DATA = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
_COOCURRENCE_DATA = {"cooccurrence": {}}
return _COOCURRENCE_DATA
def reload_cooccurrence():
global _COOCURRENCE_DATA
_COOCURRENCE_DATA = None
_load_cooccurrence()
def search_local_tags(query: str, warehouse, limit: int = 30) -> list[dict]:
q = query.lower().strip()
if not q:
return []
results = []
seen = set()
for cat_name in warehouse.pools:
pool = warehouse.get_pool(cat_name)
if pool is None:
continue
for tag_dict in pool.get_all_tag_dicts():
tag = tag_dict.get("tag", "")
tl = tag.lower()
if q in tl and tl not in seen:
# Rank: exact match > starts-with > contains; shorter tags win ties.
if tl == q:
rank = 0
elif tl.startswith(q):
rank = 1
else:
rank = 2
results.append({
"tag": tag,
"category": cat_name,
"rating": tag_dict.get("rating", "sfw"),
"source": "local",
"_rank": (rank, len(tl)),
})
seen.add(tl)
results.sort(key=lambda x: x["_rank"])
out = []
for r in results[:limit]:
r.pop("_rank", None)
out.append(r)
return out
@functools.lru_cache(maxsize=512)
def search_danbooru_tags(query: str, limit: int = 20) -> list[dict]:
import urllib.request
import urllib.parse
import urllib.error
q = query.lower().strip()
if not q:
return []
url = f"{DANBOORU_TAG_API}?search[name_or_name_matches]={urllib.parse.quote(q)}&limit={limit}"
try:
req = urllib.request.Request(url, headers={"User-Agent": "Whyx-PROmpTea/1.0"})
with urllib.request.urlopen(req, timeout=_REQUEST_TIMEOUT) as resp:
data = json.loads(resp.read().decode("utf-8"))
results = []
for item in data:
results.append({
"tag": item.get("name", ""),
"category": item.get("category", 0),
"post_count": item.get("post_count", 0),
"source": "danbooru",
})
results.sort(key=lambda x: -x.get("post_count", 0))
return results[:limit]
except (urllib.error.URLError, urllib.error.HTTPError, OSError, json.JSONDecodeError):
return []
def search_tags(query: str, warehouse, limit: int = 30) -> dict:
local = search_local_tags(query, warehouse, limit)
web = search_danbooru_tags(query, max(5, limit // 2))
return {
"local": local,
"web": web,
}
def get_cooccurrence_tags(tag: str, limit: int = 10) -> list[dict]:
"""Co-occurring tags for ``tag``, strongest first.
Related entries are scored by base weight × relation bonus so that
curated/semantic links (synonym, common_pair, character_tag) outrank raw
co-category noise.
"""
co = _load_cooccurrence().get("cooccurrence", {})
tl = tag.lower().strip()
related = co.get(tl, [])
scored = sorted(
related,
key=lambda r: -(
float(r.get("weight", 1.0)) * _RELATION_BONUS.get(r.get("relation", "co_category"), 1.0)
),
)
return scored[:limit]
HUMAN_SUBJECT_TAGS = frozenset({
"1girl", "1boy", "solo", "female", "male", "human", "person",
})
SUBJECT_COUNT_CONFLICTS = {
"1girl": ["2girls", "3girls", "multiple_girls", "1boy", "2boys", "3boys",
"multiple_boys", "male_focus", "no_humans", "group"],
"2girls": ["1girl", "solo", "no_humans"],
"3girls": ["1girl", "solo", "no_humans"],
"1boy": ["2girls", "3girls", "multiple_girls", "female_focus", "1girl",
"no_humans"],
"2boys": ["1boy", "solo", "no_humans"],
"3boys": ["1boy", "solo", "no_humans"],
"solo": ["2girls", "3girls", "multiple_girls", "2boys", "3boys", "group",
"multiple_boys"],
"multiple_girls": ["1girl", "solo"],
"multiple_boys": ["1boy", "solo"],
"no_humans": ["1girl", "2girls", "3girls", "1boy", "2boys", "3boys",
"solo", "group", "male_focus", "female_focus"],
"male_focus": ["1girl", "female_focus"],
"female_focus": ["1boy", "male_focus"],
"group": ["1girl", "1boy", "solo"],
}
# Canonicalize to spaced form: the pipeline works in spaced canonical tags
# (e.g. "multiple girls", "male focus"), so the conflict keys/values must
# match. Build a normalized copy that replaces underscores with spaces.
SUBJECT_COUNT_CONFLICTS = {
k.replace("_", " "): [v.replace("_", " ") for v in vals]
for k, vals in SUBJECT_COUNT_CONFLICTS.items()
}
def has_human_subject(user_tags: list[str]) -> bool:
if not user_tags:
return False
return bool({t.lower().strip() for t in user_tags} & HUMAN_SUBJECT_TAGS)
def filter_subject_conflicts(tags: list[str], user_tags: list[str] | None = None) -> list[str]:
protect_src = user_tags if user_tags is not None else tags
protect_lower = {t.lower().strip().replace("_", " ") for t in protect_src}
blocked = set()
for tag in protect_lower:
if tag in SUBJECT_COUNT_CONFLICTS:
blocked.update(SUBJECT_COUNT_CONFLICTS[tag])
if not blocked:
return tags
return [t for t in tags if t.lower().strip().replace("_", " ") not in blocked]
def enrich_prompt_tags(
parsed,
warehouse,
max_tags: int = 5,
user_tags: list[str] | None = None,
) -> list[str]:
enrich_tags = []
context_sources = []
if parsed.subject:
context_sources.append(parsed.subject)
if parsed.character:
context_sources.append(parsed.character)
if parsed.series:
context_sources.append(parsed.series)
if parsed.general_tags:
context_sources.extend(parsed.general_tags[:5])
seen = set()
for src in context_sources:
src_lower = src.lower().strip()
if src_lower in seen:
continue
seen.add(src_lower)
related = get_cooccurrence_tags(src, limit=5)
for item in related:
tag = item.get("tag", "")
if tag.lower() not in seen:
enrich_tags.append(tag)
seen.add(tag.lower())
enrich_tags = enrich_tags[:max_tags]
if len(enrich_tags) < max_tags:
web_results = search_danbooru_tags(context_sources[0] if context_sources else "", limit=10)
for item in web_results:
tag = item.get("tag", "")
if tag.lower() not in seen:
enrich_tags.append(tag)
seen.add(tag.lower())
if len(enrich_tags) >= max_tags:
break
enrich_tags = filter_subject_conflicts(enrich_tags, user_tags)
return enrich_tags[:max_tags]
def build_COOCURRENCE_DATA(warehouse) -> dict:
co = {}
for cat_name in warehouse.pools:
pool = warehouse.get_pool(cat_name)
if pool is None:
continue
tags_in_cat = pool.get_all_tags(max_rating="explicit")
for tag in tags_in_cat:
tl = tag.lower().strip()
if tl not in co:
co[tl] = []
related = [t for t in tags_in_cat if t.lower().strip() != tl]
for rt in related[:15]:
co[tl].append({"tag": rt, "relation": "co_category", "weight": 1.0})
return {"cooccurrence": co}