183 lines
6.1 KiB
Python
183 lines
6.1 KiB
Python
import os
|
|
import json
|
|
import urllib.request
|
|
import random
|
|
from io import BytesIO
|
|
import torch
|
|
import numpy as np
|
|
from PIL import Image
|
|
from aiohttp import web
|
|
import server
|
|
|
|
# Load the scraped characters
|
|
current_dir = os.path.dirname(os.path.realpath(__file__))
|
|
json_path = os.path.join(current_dir, "animadex_top_characters.json")
|
|
try:
|
|
with open(json_path, "r", encoding="utf-8") as f:
|
|
char_data = json.load(f)
|
|
except Exception as e:
|
|
print(f"[Animadex] Error loading data: {e}")
|
|
char_data = []
|
|
|
|
# Map character name + copyright to character dict
|
|
char_dict = {}
|
|
# Also build metadata for the sidebar
|
|
metadata_characters = []
|
|
copyright_counts = {}
|
|
|
|
for c in char_data:
|
|
display_name = f"{c.get('name', 'Unknown')} ({c.get('copyright_name', c.get('copyright', 'Unknown'))})"
|
|
c['_display_name'] = display_name
|
|
char_dict[display_name] = c
|
|
|
|
c_name = c.get('name', '')
|
|
copy_name = c.get('copyright_name') or c.get('copyright') or 'Unknown'
|
|
count = c.get('count', 0)
|
|
|
|
metadata_characters.append({
|
|
"name": c_name,
|
|
"display_name": display_name,
|
|
"copyright": copy_name,
|
|
"count": count
|
|
})
|
|
|
|
if copy_name not in copyright_counts:
|
|
copyright_counts[copy_name] = 0
|
|
copyright_counts[copy_name] += count
|
|
|
|
# Sort characters and copyrights for the sidebar by count descending
|
|
metadata_characters.sort(key=lambda x: x['count'], reverse=True)
|
|
metadata_copyrights = [{"name": k, "count": v} for k, v in copyright_counts.items()]
|
|
metadata_copyrights.sort(key=lambda x: x['count'], reverse=True)
|
|
|
|
char_names = list(char_dict.keys())
|
|
if not char_names:
|
|
char_names = ["No data loaded"]
|
|
|
|
|
|
# -------------------------------------------------------------
|
|
# API Routes for Frontend Gallery
|
|
# -------------------------------------------------------------
|
|
@server.PromptServer.instance.routes.get("/animadex/metadata")
|
|
async def get_metadata(request):
|
|
return web.json_response({
|
|
"characters": metadata_characters,
|
|
"copyrights": metadata_copyrights
|
|
})
|
|
|
|
|
|
@server.PromptServer.instance.routes.get("/animadex/search")
|
|
async def search_characters(request):
|
|
query = request.rel_url.query.get("q", "").lower()
|
|
copy_filter = request.rel_url.query.get("copyright", "").lower()
|
|
page = int(request.rel_url.query.get("page", "1"))
|
|
is_random = request.rel_url.query.get("random", "0") == "1"
|
|
favorites = request.rel_url.query.get("favorites", "").split(",")
|
|
fav_copies = request.rel_url.query.get("fav_copies", "").split(",")
|
|
fav_filter_only = request.rel_url.query.get("fav_only", "0") == "1"
|
|
exact_char = request.rel_url.query.get("exact", "")
|
|
|
|
page_size = 50
|
|
|
|
filtered = char_data
|
|
|
|
# 1. Filter by exact character if specified
|
|
if exact_char:
|
|
filtered = [c for c in filtered if c.get('_display_name') == exact_char]
|
|
|
|
# 2. Filter by favorites if toggled
|
|
if fav_filter_only:
|
|
filtered = [c for c in filtered if c.get('_display_name') in favorites]
|
|
|
|
# 2. Filter by copyright exact match (case insensitive)
|
|
if copy_filter:
|
|
filtered = [c for c in filtered if (c.get('copyright_name') or c.get('copyright') or '').lower() == copy_filter]
|
|
|
|
# 3. Filter by text search query
|
|
if query:
|
|
filtered = [
|
|
c for c in filtered
|
|
if query in c.get('name', '').lower()
|
|
or query in (c.get('copyright_name') or c.get('copyright') or '').lower()
|
|
or query in c.get('trigger', '').lower()
|
|
]
|
|
|
|
# 4. Randomize or sort
|
|
if is_random:
|
|
# We copy the list before shuffling so we don't modify the global reference
|
|
filtered = list(filtered)
|
|
random.shuffle(filtered)
|
|
|
|
# Pagination
|
|
total = len(filtered)
|
|
start_idx = (page - 1) * page_size
|
|
end_idx = start_idx + page_size
|
|
results = filtered[start_idx:end_idx]
|
|
|
|
return web.json_response({
|
|
"results": results,
|
|
"total": total,
|
|
"page": page,
|
|
"pages": (total + page_size - 1) // page_size if total > 0 else 1
|
|
})
|
|
|
|
# -------------------------------------------------------------
|
|
# Node Class
|
|
# -------------------------------------------------------------
|
|
class AnimadexCharacterNode:
|
|
"""
|
|
A ComfyUI custom node that provides a custom WebUI gallery of Animadex characters.
|
|
"""
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"character": (char_names,),
|
|
"use_high_res_image": (["false", "true"], {"default": "false"}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("image", "trigger_words", "prompt_tags", "trigger_and_tags")
|
|
FUNCTION = "get_character"
|
|
CATEGORY = "Animadex"
|
|
|
|
def get_character(self, character, use_high_res_image):
|
|
if character not in char_dict:
|
|
empty_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
|
return (empty_image, "", "", "")
|
|
|
|
c = char_dict[character]
|
|
trigger = c.get("trigger", "")
|
|
tags_list = c.get("tags", [])
|
|
tags = ", ".join(tags_list)
|
|
|
|
url = c.get("img_url") if use_high_res_image == "true" else c.get("thumb_url")
|
|
if not url:
|
|
url = c.get("thumb_url") or c.get("img_url")
|
|
|
|
try:
|
|
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
|
with urllib.request.urlopen(req) as response:
|
|
image_data = response.read()
|
|
i = Image.open(BytesIO(image_data))
|
|
i = i.convert("RGB")
|
|
image = np.array(i).astype(np.float32) / 255.0
|
|
image = torch.from_numpy(image)[None,]
|
|
except Exception as e:
|
|
print(f"[Animadex] Error downloading image from {url}: {e}")
|
|
image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
|
|
|
trigger_and_tags = trigger
|
|
if tags:
|
|
trigger_and_tags = f"{trigger}, {tags}" if trigger else tags
|
|
|
|
return (image, trigger, tags, trigger_and_tags)
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"AnimadexCharacterNode": AnimadexCharacterNode
|
|
}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"AnimadexCharacterNode": "Animadex Character Selector"
|
|
}
|