Spaces:
Running
Running
| """Enrich artist.json entries that have empty signature_tags. | |
| For each such artist, fetch a page of their Danbooru posts and count | |
| general-tag frequencies across that sample; keep the top N as signature_tags | |
| and re-guess the style bucket from them. | |
| Resumable: entries that already have signature_tags are skipped, so the | |
| script can be interrupted and re-run safely. | |
| """ | |
| import json | |
| import os | |
| import sys | |
| import time | |
| import urllib.parse | |
| import urllib.request | |
| from collections import Counter | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from scripts.fetch_top_artists import ( | |
| ARTIST_PATH, _atomic_write_json, _guess_style, _load_json, | |
| ) | |
| POSTS_URL = "https://danbooru.donmai.us/posts.json" | |
| DEFAULT_POST_PAGES = 2 # 2 pages x 50 posts = up to 100 posts per artist | |
| TOP_SIGNATURES = 5 | |
| # Filter ubiquitous noise tags out of signature extraction. | |
| _NOISE = { | |
| "1girl", "1boy", "solo", "highres", "absurdres", "commentary", "translation", | |
| "signed", "artist name", "artist request", "commentary request", "translated", | |
| "safe", "questionable", "nsfw", "explicit", "sensitive", | |
| "blush", "smile", "open mouth", "looking at viewer", "long hair", | |
| } | |
| # Ratings we allow to contribute tags (g = general, s = sensitive). | |
| _ALLOWED_POST_RATINGS = {"g", "s"} | |
| def _load_nsfw_like_tags() -> set[str]: | |
| """All pool tags rated nsfw/explicit, in spaced lower-case form.""" | |
| pools_dir = os.path.join( | |
| os.path.dirname(os.path.dirname(os.path.abspath(__file__))), | |
| "data", "tag_pools", | |
| ) | |
| bad: set[str] = set() | |
| import glob as _glob | |
| for path in _glob.glob(os.path.join(pools_dir, "*.json")): | |
| try: | |
| entries = _load_json(path) | |
| except Exception: | |
| continue | |
| if isinstance(entries, dict): # artist.json shape | |
| entries = entries.get("artists", []) | |
| for e in entries: | |
| if isinstance(e, dict) and e.get("rating") in ("nsfw", "explicit"): | |
| t = e.get("tag", "").lower().strip() | |
| if t: | |
| bad.add(t) | |
| return bad | |
| _NSFW_TAGS = None | |
| def _valid_signature(tag: str) -> bool: | |
| global _NSFW_TAGS | |
| if _NSFW_TAGS is None: | |
| _NSFW_TAGS = _load_nsfw_like_tags() | |
| if not tag or len(tag) < 3: | |
| return False | |
| if tag in _NOISE or tag in _NSFW_TAGS: | |
| return False | |
| # Reject symbol-heavy/emoticon tags (:3, :d, ...). | |
| if any(ch in tag for ch in ":;<>^-_"): | |
| return False | |
| return tag.isprintable() | |
| def _post_count_for(tag: str) -> int: | |
| q = urllib.parse.urlencode({"tags": tag.replace(" ", "_"), "limit": 0}) | |
| url = f"{POSTS_URL}?{q}" | |
| req = urllib.request.Request(url, headers={"User-Agent": "Whyx-PROptimizer/1.0"}) | |
| try: | |
| with urllib.request.urlopen(req, timeout=20) as r: | |
| return int(r.headers.get("X-Total-Count", "0") or 0) | |
| except Exception: | |
| return 0 | |
| def _top_tags_for(tag: str, pages: int = DEFAULT_POST_PAGES) -> list[str]: | |
| counts: Counter = Counter() | |
| for page in range(1, pages + 1): | |
| q = urllib.parse.urlencode({ | |
| "tags": tag.replace(" ", "_"), | |
| "limit": 50, | |
| "page": page, | |
| }) | |
| url = f"{POSTS_URL}?{q}" | |
| req = urllib.request.Request(url, headers={"User-Agent": "Whyx-PROptimizer/1.0"}) | |
| try: | |
| with urllib.request.urlopen(req, timeout=25) as r: | |
| posts = json.loads(r.read().decode("utf-8")) | |
| except Exception: | |
| break | |
| if not posts: | |
| break | |
| for post in posts: | |
| if post.get("rating") not in _ALLOWED_POST_RATINGS: | |
| continue | |
| tags = (post.get("tag_string_general") or "").split() | |
| for t in tags: | |
| t = t.replace("_", " ").strip().lower() | |
| if _valid_signature(t): | |
| counts[t] += 1 | |
| time.sleep(0.35) | |
| return [t for t, _ in counts.most_common(TOP_SIGNATURES)] | |
| def main(limit: int | None = None, throttle: float = 0.6): | |
| data = _load_json(ARTIST_PATH) | |
| artists = data["artists"] | |
| todo = [a for a in artists if not a.get("signature_tags")] | |
| if limit: | |
| todo = todo[:limit] | |
| total = len(todo) | |
| if total == 0: | |
| print("nothing to enrich") | |
| return | |
| print(f"enriching {total} artists...") | |
| changed = 0 | |
| for i, artist in enumerate(todo, 1): | |
| tag = artist["tag"] | |
| sig = _top_tags_for(tag) | |
| if sig: | |
| artist["signature_tags"] = sig | |
| if artist.get("style", "detailed") == "detailed": | |
| artist["style"] = _guess_style(sig) | |
| changed += 1 | |
| if i % 25 == 0 or i == total: | |
| print(f" {i}/{total} done, {changed} updated") | |
| time.sleep(throttle) | |
| _atomic_write_json(ARTIST_PATH, data) | |
| still_empty = sum(1 for a in artists if not a.get("signature_tags")) | |
| print(f"done: {changed} enriched, {still_empty} still empty") | |
| if __name__ == "__main__": | |
| import argparse | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--limit", type=int, default=None, help="max artists to enrich this run") | |
| ap.add_argument("--throttle", type=float, default=0.6, help="seconds between artists") | |
| args = ap.parse_args() | |
| main(args.limit, args.throttle) | |