Spaces:
Running
Running
Upload 14 files
Browse files- .gitattributes +3 -0
- data/__init__.py +1 -0
- data/autocomplete/__init__.py +1 -0
- data/autocomplete/__pycache__/__init__.cpython-312.pyc +0 -0
- data/autocomplete/__pycache__/artist_dictionary.cpython-312.pyc +3 -0
- data/autocomplete/__pycache__/danbooru_character.cpython-312.pyc +3 -0
- data/autocomplete/__pycache__/result_dupl.cpython-312.pyc +3 -0
- data/autocomplete/artist_dictionary.py +0 -0
- data/autocomplete/danbooru_character.py +0 -0
- data/autocomplete/result_dupl.py +0 -0
- data/character_store.py +146 -0
- data/characteristic_list.txt +881 -0
- data/danbooru_character.py +0 -0
- data/partition_loader.py +281 -0
- data/tag_store.py +319 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
data/autocomplete/__pycache__/artist_dictionary.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
data/autocomplete/__pycache__/danbooru_character.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
data/autocomplete/__pycache__/result_dupl.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
data/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# NAIA-WEB Data Package
|
data/autocomplete/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Autocomplete data package
|
data/autocomplete/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (142 Bytes). View file
|
|
|
data/autocomplete/__pycache__/artist_dictionary.cpython-312.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5efe0ffa80b1cd2ddfa8b16e052fcf4997402fbfd6ef67b060f7e153d4060f4a
|
| 3 |
+
size 3033064
|
data/autocomplete/__pycache__/danbooru_character.cpython-312.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e968d93f060cfc751be6adf7a02ac4d93681234f9cbb5b9ea1fe9e47087a56f3
|
| 3 |
+
size 7657682
|
data/autocomplete/__pycache__/result_dupl.cpython-312.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f66435f4d1479811739d84f300a199b7480cc09540a70610610e05fff21d7766
|
| 3 |
+
size 2999924
|
data/autocomplete/artist_dictionary.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/autocomplete/danbooru_character.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/autocomplete/result_dupl.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/character_store.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Character Store for NAIA-WEB
|
| 3 |
+
Provides character search functionality using danbooru_character data
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import List, Tuple, Optional, Set
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class CharacterInfo:
|
| 13 |
+
"""Character information"""
|
| 14 |
+
name: str
|
| 15 |
+
tags: str
|
| 16 |
+
count: int
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class CharacterStore:
|
| 20 |
+
"""
|
| 21 |
+
Character data store for search functionality.
|
| 22 |
+
Loads data from danbooru_character.py on first access.
|
| 23 |
+
Filters tags using characteristic_list.txt.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
_instance = None
|
| 27 |
+
_loaded = False
|
| 28 |
+
_characters: dict = {}
|
| 29 |
+
_counts: dict = {}
|
| 30 |
+
_characteristic_set: Set[str] = set()
|
| 31 |
+
|
| 32 |
+
def __new__(cls):
|
| 33 |
+
if cls._instance is None:
|
| 34 |
+
cls._instance = super().__new__(cls)
|
| 35 |
+
return cls._instance
|
| 36 |
+
|
| 37 |
+
def __init__(self):
|
| 38 |
+
if not CharacterStore._loaded:
|
| 39 |
+
self._load_data()
|
| 40 |
+
|
| 41 |
+
def _load_characteristic_list(self) -> Set[str]:
|
| 42 |
+
"""Load characteristic list from file"""
|
| 43 |
+
characteristic_set = set()
|
| 44 |
+
try:
|
| 45 |
+
list_path = Path(__file__).parent / "characteristic_list.txt"
|
| 46 |
+
if list_path.exists():
|
| 47 |
+
with open(list_path, 'r', encoding='utf-8') as f:
|
| 48 |
+
for line in f:
|
| 49 |
+
tag = line.strip().lower()
|
| 50 |
+
if tag:
|
| 51 |
+
characteristic_set.add(tag)
|
| 52 |
+
print(f"NAIA-WEB: Loaded {len(characteristic_set)} characteristic tags")
|
| 53 |
+
else:
|
| 54 |
+
print(f"NAIA-WEB: characteristic_list.txt not found at {list_path}")
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(f"NAIA-WEB: Failed to load characteristic list: {e}")
|
| 57 |
+
return characteristic_set
|
| 58 |
+
|
| 59 |
+
def _filter_tags(self, tags_str: str) -> str:
|
| 60 |
+
"""Filter tags to only include characteristic tags"""
|
| 61 |
+
if not CharacterStore._characteristic_set:
|
| 62 |
+
return tags_str
|
| 63 |
+
|
| 64 |
+
tags = [t.strip() for t in tags_str.split(',')]
|
| 65 |
+
filtered = [t for t in tags if t.lower() in CharacterStore._characteristic_set]
|
| 66 |
+
return ', '.join(filtered)
|
| 67 |
+
|
| 68 |
+
def _load_data(self):
|
| 69 |
+
"""Load character data from danbooru_character.py"""
|
| 70 |
+
try:
|
| 71 |
+
# Load characteristic list first
|
| 72 |
+
CharacterStore._characteristic_set = self._load_characteristic_list()
|
| 73 |
+
|
| 74 |
+
from data.danbooru_character import character_dict, character_dict_count
|
| 75 |
+
CharacterStore._characters = character_dict
|
| 76 |
+
CharacterStore._counts = character_dict_count
|
| 77 |
+
CharacterStore._loaded = True
|
| 78 |
+
print(f"NAIA-WEB: Loaded {len(character_dict)} characters")
|
| 79 |
+
except ImportError as e:
|
| 80 |
+
print(f"NAIA-WEB: Failed to load character data: {e}")
|
| 81 |
+
CharacterStore._characters = {}
|
| 82 |
+
CharacterStore._counts = {}
|
| 83 |
+
CharacterStore._loaded = True
|
| 84 |
+
|
| 85 |
+
def search(self, query: str, min_count: int = 20, limit: int = 50) -> List[CharacterInfo]:
|
| 86 |
+
"""
|
| 87 |
+
Search characters by name.
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
query: Search query (character name)
|
| 91 |
+
min_count: Minimum count threshold
|
| 92 |
+
limit: Maximum results to return
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
List of CharacterInfo sorted by count (descending)
|
| 96 |
+
"""
|
| 97 |
+
query = query.lower().strip()
|
| 98 |
+
results = []
|
| 99 |
+
|
| 100 |
+
for name, tags in CharacterStore._characters.items():
|
| 101 |
+
count = CharacterStore._counts.get(name, 0)
|
| 102 |
+
|
| 103 |
+
# Filter by minimum count
|
| 104 |
+
if count < min_count:
|
| 105 |
+
continue
|
| 106 |
+
|
| 107 |
+
# Filter by query
|
| 108 |
+
if query and query not in name.lower():
|
| 109 |
+
continue
|
| 110 |
+
|
| 111 |
+
results.append(CharacterInfo(
|
| 112 |
+
name=name,
|
| 113 |
+
tags=self._filter_tags(tags),
|
| 114 |
+
count=count
|
| 115 |
+
))
|
| 116 |
+
|
| 117 |
+
# Sort by count descending
|
| 118 |
+
results.sort(key=lambda x: -x.count)
|
| 119 |
+
|
| 120 |
+
return results[:limit]
|
| 121 |
+
|
| 122 |
+
def get_character(self, name: str) -> Optional[CharacterInfo]:
|
| 123 |
+
"""Get character info by exact name"""
|
| 124 |
+
name = name.lower()
|
| 125 |
+
if name in CharacterStore._characters:
|
| 126 |
+
return CharacterInfo(
|
| 127 |
+
name=name,
|
| 128 |
+
tags=self._filter_tags(CharacterStore._characters[name]),
|
| 129 |
+
count=CharacterStore._counts.get(name, 0)
|
| 130 |
+
)
|
| 131 |
+
return None
|
| 132 |
+
|
| 133 |
+
def get_popular_characters(self, limit: int = 50) -> List[CharacterInfo]:
|
| 134 |
+
"""Get most popular characters"""
|
| 135 |
+
return self.search("", min_count=50, limit=limit)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# Global instance
|
| 139 |
+
_store = None
|
| 140 |
+
|
| 141 |
+
def get_character_store() -> CharacterStore:
|
| 142 |
+
"""Get the global CharacterStore instance"""
|
| 143 |
+
global _store
|
| 144 |
+
if _store is None:
|
| 145 |
+
_store = CharacterStore()
|
| 146 |
+
return _store
|
data/characteristic_list.txt
ADDED
|
@@ -0,0 +1,881 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flat chest
|
| 2 |
+
small breasts
|
| 3 |
+
medium breasts
|
| 4 |
+
large breasts
|
| 5 |
+
huge breasts
|
| 6 |
+
aqua eyes
|
| 7 |
+
black eyes
|
| 8 |
+
blue eyes
|
| 9 |
+
brown eyes
|
| 10 |
+
green eyes
|
| 11 |
+
grey eyes
|
| 12 |
+
orange eyes
|
| 13 |
+
purple eyes
|
| 14 |
+
pink eyes
|
| 15 |
+
red eyes
|
| 16 |
+
white eyes
|
| 17 |
+
yellow eyes
|
| 18 |
+
amber eyes
|
| 19 |
+
heterochromia
|
| 20 |
+
multicolored eyes
|
| 21 |
+
aqua pupils
|
| 22 |
+
blue pupils
|
| 23 |
+
brown pupils
|
| 24 |
+
green pupils
|
| 25 |
+
grey pupils
|
| 26 |
+
orange pupils
|
| 27 |
+
pink pupils
|
| 28 |
+
purple pupils
|
| 29 |
+
red pupils
|
| 30 |
+
white pupils
|
| 31 |
+
yellow pupils
|
| 32 |
+
pointy ears
|
| 33 |
+
long pointy ears
|
| 34 |
+
aqua hair
|
| 35 |
+
black hair
|
| 36 |
+
blonde hair
|
| 37 |
+
blue hair
|
| 38 |
+
light blue hair
|
| 39 |
+
dark blue hair
|
| 40 |
+
brown hair
|
| 41 |
+
light brown hair
|
| 42 |
+
green hair
|
| 43 |
+
dark green hair
|
| 44 |
+
light green hair
|
| 45 |
+
grey hair
|
| 46 |
+
orange hair
|
| 47 |
+
pink hair
|
| 48 |
+
purple hair
|
| 49 |
+
light purple hair
|
| 50 |
+
red hair
|
| 51 |
+
white hair
|
| 52 |
+
multicolored hair
|
| 53 |
+
colored inner hair
|
| 54 |
+
colored tips
|
| 55 |
+
roots (hair)
|
| 56 |
+
gradient hair
|
| 57 |
+
print hair
|
| 58 |
+
rainbow hair
|
| 59 |
+
split-color hair
|
| 60 |
+
spotted hair
|
| 61 |
+
streaked hair
|
| 62 |
+
two-tone hair
|
| 63 |
+
very short hair
|
| 64 |
+
short hair
|
| 65 |
+
medium hair
|
| 66 |
+
long hair
|
| 67 |
+
very long hair
|
| 68 |
+
absurdly long hair
|
| 69 |
+
big hair
|
| 70 |
+
bald
|
| 71 |
+
bald girl
|
| 72 |
+
bob cut
|
| 73 |
+
inverted bob
|
| 74 |
+
bowl cut
|
| 75 |
+
buzz cut
|
| 76 |
+
chonmage
|
| 77 |
+
crew cut
|
| 78 |
+
flattop
|
| 79 |
+
okappa
|
| 80 |
+
pixie cut
|
| 81 |
+
undercut
|
| 82 |
+
flipped hair
|
| 83 |
+
wolf cut
|
| 84 |
+
cornrows
|
| 85 |
+
dreadlocks
|
| 86 |
+
hime cut
|
| 87 |
+
mullet
|
| 88 |
+
bow-shaped hair
|
| 89 |
+
braid
|
| 90 |
+
braided bangs
|
| 91 |
+
front braid
|
| 92 |
+
side braid
|
| 93 |
+
french braid
|
| 94 |
+
crown braid
|
| 95 |
+
single braid
|
| 96 |
+
multiple braids
|
| 97 |
+
twin braids
|
| 98 |
+
low twin braids
|
| 99 |
+
tri braids
|
| 100 |
+
quad braids
|
| 101 |
+
flower-shaped hair
|
| 102 |
+
hair bun
|
| 103 |
+
braided bun
|
| 104 |
+
single hair bun
|
| 105 |
+
double bun
|
| 106 |
+
cone hair bun
|
| 107 |
+
doughnut hair bun
|
| 108 |
+
heart hair bun
|
| 109 |
+
triple bun
|
| 110 |
+
hair rings
|
| 111 |
+
single hair ring
|
| 112 |
+
half updo
|
| 113 |
+
one side up
|
| 114 |
+
two side up
|
| 115 |
+
low-braided long hair
|
| 116 |
+
low-tied long hair
|
| 117 |
+
mizura
|
| 118 |
+
multi-tied hair
|
| 119 |
+
nihongami
|
| 120 |
+
ponytail
|
| 121 |
+
folded ponytail
|
| 122 |
+
front ponytail
|
| 123 |
+
high ponytail
|
| 124 |
+
short ponytail
|
| 125 |
+
side ponytail
|
| 126 |
+
split ponytail
|
| 127 |
+
star-shaped hair
|
| 128 |
+
topknot
|
| 129 |
+
twintails
|
| 130 |
+
low twintails
|
| 131 |
+
short twintails
|
| 132 |
+
uneven twintails
|
| 133 |
+
tri tails
|
| 134 |
+
quad tails
|
| 135 |
+
quin tails
|
| 136 |
+
twisted hair
|
| 137 |
+
afro
|
| 138 |
+
huge afro
|
| 139 |
+
beehive hairdo
|
| 140 |
+
crested hair
|
| 141 |
+
pompadour
|
| 142 |
+
quiff
|
| 143 |
+
shouten pegasus mix mori
|
| 144 |
+
curly hair
|
| 145 |
+
drill hair
|
| 146 |
+
twin drills
|
| 147 |
+
tri drills
|
| 148 |
+
hair flaps
|
| 149 |
+
messy hair
|
| 150 |
+
pointy hair
|
| 151 |
+
ringlets
|
| 152 |
+
spiked hair
|
| 153 |
+
straight hair
|
| 154 |
+
wavy hair
|
| 155 |
+
bangs
|
| 156 |
+
arched bangs
|
| 157 |
+
asymmetrical bangs
|
| 158 |
+
bangs pinned back
|
| 159 |
+
blunt bangs
|
| 160 |
+
crossed bangs
|
| 161 |
+
diagonal bangs
|
| 162 |
+
dyed bangs
|
| 163 |
+
fanged bangs
|
| 164 |
+
hair over eyes
|
| 165 |
+
hair over one eye
|
| 166 |
+
long bangs
|
| 167 |
+
parted bangs
|
| 168 |
+
curtained hair
|
| 169 |
+
ribbon bangs
|
| 170 |
+
short bangs
|
| 171 |
+
swept bangs
|
| 172 |
+
hair between eyes
|
| 173 |
+
hair intakes
|
| 174 |
+
single hair intake
|
| 175 |
+
sidelocks
|
| 176 |
+
asymmetrical sidelocks
|
| 177 |
+
drill sidelocks
|
| 178 |
+
low-tied sidelocks
|
| 179 |
+
sidelocks tied back
|
| 180 |
+
single sidelock
|
| 181 |
+
ahoge
|
| 182 |
+
heart ahoge
|
| 183 |
+
huge ahoge
|
| 184 |
+
antenna hair
|
| 185 |
+
heart antenna hair
|
| 186 |
+
comb over
|
| 187 |
+
hair pulled back
|
| 188 |
+
hair slicked back
|
| 189 |
+
mohawk
|
| 190 |
+
oseledets
|
| 191 |
+
lone nape hair
|
| 192 |
+
hair bikini
|
| 193 |
+
hair censor
|
| 194 |
+
hair in own mouth
|
| 195 |
+
hair over breasts
|
| 196 |
+
hair over one breast
|
| 197 |
+
hair over crotch
|
| 198 |
+
hair over shoulder
|
| 199 |
+
hair scarf
|
| 200 |
+
alternate hairstyle
|
| 201 |
+
hair down
|
| 202 |
+
hair up
|
| 203 |
+
asymmetrical hair
|
| 204 |
+
sidecut
|
| 205 |
+
blunt ends
|
| 206 |
+
dark skin
|
| 207 |
+
dark-skinned female
|
| 208 |
+
pale skin
|
| 209 |
+
sun tatoo
|
| 210 |
+
black skin
|
| 211 |
+
blue skin
|
| 212 |
+
green skin
|
| 213 |
+
grey skin
|
| 214 |
+
orange skin
|
| 215 |
+
pink skin
|
| 216 |
+
purple skin
|
| 217 |
+
red skin
|
| 218 |
+
white skin
|
| 219 |
+
yellow skin
|
| 220 |
+
colored skin
|
| 221 |
+
multiple tails
|
| 222 |
+
demon tail
|
| 223 |
+
dragon tail
|
| 224 |
+
ghost tail
|
| 225 |
+
pikachu tail
|
| 226 |
+
snake head tail
|
| 227 |
+
fiery tail
|
| 228 |
+
bear tail
|
| 229 |
+
rabbit tail
|
| 230 |
+
cat tail
|
| 231 |
+
cow tail
|
| 232 |
+
deer tail
|
| 233 |
+
dog tail
|
| 234 |
+
ermine tail
|
| 235 |
+
fox tail
|
| 236 |
+
horse tail
|
| 237 |
+
leopard tail
|
| 238 |
+
lion tail
|
| 239 |
+
monkey tail
|
| 240 |
+
mouse tail
|
| 241 |
+
pig tail
|
| 242 |
+
sheep tail
|
| 243 |
+
squirrel tail
|
| 244 |
+
tiger tail
|
| 245 |
+
wolf tail
|
| 246 |
+
crocodilian tail
|
| 247 |
+
fish tail
|
| 248 |
+
scorpion tail
|
| 249 |
+
snake tail
|
| 250 |
+
tadpole tail
|
| 251 |
+
animal ears
|
| 252 |
+
bat ears
|
| 253 |
+
bear ears
|
| 254 |
+
rabbit ears
|
| 255 |
+
cat ears
|
| 256 |
+
cow ears
|
| 257 |
+
deer ears
|
| 258 |
+
dog ears
|
| 259 |
+
ferret ears
|
| 260 |
+
fox ears
|
| 261 |
+
goat ears
|
| 262 |
+
horse ears
|
| 263 |
+
kemonomimi mode
|
| 264 |
+
lion ears
|
| 265 |
+
monkey ears
|
| 266 |
+
mouse ears
|
| 267 |
+
panda ears
|
| 268 |
+
pikachu ears
|
| 269 |
+
pig ears
|
| 270 |
+
raccoon ears
|
| 271 |
+
sheep ears
|
| 272 |
+
squirrel ears
|
| 273 |
+
tiger ears
|
| 274 |
+
wolf ears
|
| 275 |
+
fake animal ears
|
| 276 |
+
animal ear headphones
|
| 277 |
+
bear ear headphones
|
| 278 |
+
cat ear headphones
|
| 279 |
+
rabbit ear headphones
|
| 280 |
+
hair ears
|
| 281 |
+
robot ears
|
| 282 |
+
extra ears
|
| 283 |
+
ear piercing
|
| 284 |
+
ear protection
|
| 285 |
+
object behind ear
|
| 286 |
+
roots
|
| 287 |
+
alternate hair color
|
| 288 |
+
translucent hair
|
| 289 |
+
widow's peak
|
| 290 |
+
neckbeard
|
| 291 |
+
stroking beard
|
| 292 |
+
two-tone beard
|
| 293 |
+
braided beard
|
| 294 |
+
long beard
|
| 295 |
+
tied beard
|
| 296 |
+
very long beard
|
| 297 |
+
goatee
|
| 298 |
+
mustache
|
| 299 |
+
stubble
|
| 300 |
+
alternate facial hair
|
| 301 |
+
bearded girl
|
| 302 |
+
fake mustache
|
| 303 |
+
beard
|
| 304 |
+
toothbrush mustache
|
| 305 |
+
mustached girl
|
| 306 |
+
head fins
|
| 307 |
+
single head wing
|
| 308 |
+
wings
|
| 309 |
+
alternate wings
|
| 310 |
+
multiple wings
|
| 311 |
+
no wings
|
| 312 |
+
single wing
|
| 313 |
+
large wings
|
| 314 |
+
mini wings
|
| 315 |
+
angel wings
|
| 316 |
+
demon wings
|
| 317 |
+
dragon wings
|
| 318 |
+
fairy wings
|
| 319 |
+
insect wings
|
| 320 |
+
butterfly wings
|
| 321 |
+
dragonfly wings
|
| 322 |
+
ladybug wings
|
| 323 |
+
moth wings
|
| 324 |
+
bat wings
|
| 325 |
+
crystal wings
|
| 326 |
+
energy wings
|
| 327 |
+
fiery wings
|
| 328 |
+
ice wings
|
| 329 |
+
light hawk wings
|
| 330 |
+
liquid wings
|
| 331 |
+
artificial wings
|
| 332 |
+
fake wings
|
| 333 |
+
hair wings
|
| 334 |
+
mechanical wings
|
| 335 |
+
metal wings
|
| 336 |
+
plant wings
|
| 337 |
+
feathered wings
|
| 338 |
+
black wings
|
| 339 |
+
gradient wings
|
| 340 |
+
red wings
|
| 341 |
+
white wings
|
| 342 |
+
blue wings
|
| 343 |
+
green wings
|
| 344 |
+
brown wings
|
| 345 |
+
transparent wings
|
| 346 |
+
yellow wings
|
| 347 |
+
pink wings
|
| 348 |
+
rainbow wings
|
| 349 |
+
grey wings
|
| 350 |
+
ankle wings
|
| 351 |
+
detached wings
|
| 352 |
+
head wings
|
| 353 |
+
low wings
|
| 354 |
+
leg wings
|
| 355 |
+
wrist wings
|
| 356 |
+
wing ears
|
| 357 |
+
winged arms
|
| 358 |
+
winged bag
|
| 359 |
+
winged hat
|
| 360 |
+
winged helmet
|
| 361 |
+
winged footwear
|
| 362 |
+
asymmetrical wings
|
| 363 |
+
bloody wings
|
| 364 |
+
bowed wings
|
| 365 |
+
flapping
|
| 366 |
+
glowing wings
|
| 367 |
+
heart wings
|
| 368 |
+
torn wings
|
| 369 |
+
wing censor
|
| 370 |
+
grabbing another's wing
|
| 371 |
+
wing hug
|
| 372 |
+
wing umbrella
|
| 373 |
+
wingjob
|
| 374 |
+
wing ribbon
|
| 375 |
+
constricted pupils
|
| 376 |
+
dilated pupils
|
| 377 |
+
extra pupils
|
| 378 |
+
horizontal pupils
|
| 379 |
+
no pupils
|
| 380 |
+
slit pupils
|
| 381 |
+
symbol-shaped pupils
|
| 382 |
+
diamond-shaped pupils
|
| 383 |
+
flower-shaped pupils
|
| 384 |
+
heart-shaped pupils
|
| 385 |
+
star-shaped pupils
|
| 386 |
+
cross-shaped pupils
|
| 387 |
+
x-shaped pupils
|
| 388 |
+
mismatched pupils
|
| 389 |
+
blue sclera
|
| 390 |
+
black sclera
|
| 391 |
+
blank eyes
|
| 392 |
+
bloodshot eyes
|
| 393 |
+
green sclera
|
| 394 |
+
mismatched sclera
|
| 395 |
+
no sclera
|
| 396 |
+
orange sclera
|
| 397 |
+
red sclera
|
| 398 |
+
yellow sclera
|
| 399 |
+
bags under eyes
|
| 400 |
+
aegyo sal
|
| 401 |
+
bruised eye
|
| 402 |
+
flaming eyes
|
| 403 |
+
glowing eyes
|
| 404 |
+
glowing eye
|
| 405 |
+
missing eye
|
| 406 |
+
one-eyed
|
| 407 |
+
third eye
|
| 408 |
+
extra eyes
|
| 409 |
+
no eyes
|
| 410 |
+
covering own eyes
|
| 411 |
+
bandage over one eye
|
| 412 |
+
eyelashes
|
| 413 |
+
colored eyelashes
|
| 414 |
+
fake eyelashes
|
| 415 |
+
eyes visible through hair
|
| 416 |
+
makeup
|
| 417 |
+
eyeliner
|
| 418 |
+
eyeshadow
|
| 419 |
+
mascara
|
| 420 |
+
forehead mark
|
| 421 |
+
forehead
|
| 422 |
+
aqua lips
|
| 423 |
+
black lips
|
| 424 |
+
blue lips
|
| 425 |
+
grey lips
|
| 426 |
+
green lips
|
| 427 |
+
orange lips
|
| 428 |
+
pink lips
|
| 429 |
+
purple lips
|
| 430 |
+
red lips
|
| 431 |
+
shiny lips
|
| 432 |
+
yellow lips
|
| 433 |
+
tail
|
| 434 |
+
tail bell
|
| 435 |
+
tail bow
|
| 436 |
+
tail ornament
|
| 437 |
+
tail piercing
|
| 438 |
+
tail ribbon
|
| 439 |
+
tail ring
|
| 440 |
+
fake tail
|
| 441 |
+
heart tail
|
| 442 |
+
heart tail duo
|
| 443 |
+
holding another's tail
|
| 444 |
+
holding own tail
|
| 445 |
+
holding with tail
|
| 446 |
+
intertwined tails
|
| 447 |
+
panties around tail
|
| 448 |
+
prehensile tail
|
| 449 |
+
spiked tail
|
| 450 |
+
stiff tail
|
| 451 |
+
tail between legs
|
| 452 |
+
tail biting
|
| 453 |
+
tail censor
|
| 454 |
+
tail grab
|
| 455 |
+
tail fondling
|
| 456 |
+
tail pull
|
| 457 |
+
tail raised
|
| 458 |
+
tail stand
|
| 459 |
+
tail wagging
|
| 460 |
+
tail wrap
|
| 461 |
+
dark-skinned male
|
| 462 |
+
dark-skinned other
|
| 463 |
+
matching hairstyle
|
| 464 |
+
official alternate hairstyle
|
| 465 |
+
single side bun
|
| 466 |
+
side up bun
|
| 467 |
+
dog human
|
| 468 |
+
gorilla human
|
| 469 |
+
cat human
|
| 470 |
+
bear human
|
| 471 |
+
raccoon human
|
| 472 |
+
werewolf
|
| 473 |
+
squirrel human
|
| 474 |
+
pig human
|
| 475 |
+
horse human
|
| 476 |
+
bat human
|
| 477 |
+
deer human
|
| 478 |
+
lion human
|
| 479 |
+
cow human
|
| 480 |
+
sheep human
|
| 481 |
+
fox human
|
| 482 |
+
goat human
|
| 483 |
+
monkey human
|
| 484 |
+
ferret human
|
| 485 |
+
mouse human
|
| 486 |
+
rabbit human
|
| 487 |
+
panda human
|
| 488 |
+
tiger human
|
| 489 |
+
dog furry
|
| 490 |
+
gorilla furry
|
| 491 |
+
cat furry
|
| 492 |
+
bear furry
|
| 493 |
+
raccoon furry
|
| 494 |
+
wolf furry
|
| 495 |
+
squirrel furry
|
| 496 |
+
pig furry
|
| 497 |
+
horse furry
|
| 498 |
+
bat furry
|
| 499 |
+
deer furry
|
| 500 |
+
lion furry
|
| 501 |
+
cow furry
|
| 502 |
+
sheep furry
|
| 503 |
+
fox furry
|
| 504 |
+
goat furry
|
| 505 |
+
monkey furry
|
| 506 |
+
ferret furry
|
| 507 |
+
mouse furry
|
| 508 |
+
rabbit furry
|
| 509 |
+
panda furry
|
| 510 |
+
tiger furry
|
| 511 |
+
furry
|
| 512 |
+
yellow hair
|
| 513 |
+
sky blue hair
|
| 514 |
+
ombre
|
| 515 |
+
sombre
|
| 516 |
+
absurdly short hair
|
| 517 |
+
gray hair streak
|
| 518 |
+
white hair streak
|
| 519 |
+
brown hair streak
|
| 520 |
+
red hair streak
|
| 521 |
+
pink hair streak
|
| 522 |
+
orange hair streak
|
| 523 |
+
yellow hair streak
|
| 524 |
+
blonde hair streak
|
| 525 |
+
light green hair streak
|
| 526 |
+
green hair streak
|
| 527 |
+
sky blue hair streak
|
| 528 |
+
blue hair streak
|
| 529 |
+
purple hair streak
|
| 530 |
+
black hair streak
|
| 531 |
+
rainbow hair streak
|
| 532 |
+
short eyes
|
| 533 |
+
long eyes
|
| 534 |
+
short eyebrow
|
| 535 |
+
long eyebrow
|
| 536 |
+
slender eyebrow
|
| 537 |
+
heavy eyebrow
|
| 538 |
+
short eyelashes
|
| 539 |
+
long eyelashes
|
| 540 |
+
slender eyelashes
|
| 541 |
+
heavy eyelashes
|
| 542 |
+
gray eyes
|
| 543 |
+
golden eyes
|
| 544 |
+
light green eyes
|
| 545 |
+
sky blue eyes
|
| 546 |
+
odd eye
|
| 547 |
+
multicolored eye
|
| 548 |
+
spade-shaped pupils
|
| 549 |
+
clover-shaped pupils
|
| 550 |
+
blanked eyes
|
| 551 |
+
empty eyes
|
| 552 |
+
bloodshot pupils
|
| 553 |
+
flaming pupils
|
| 554 |
+
glowing pupils
|
| 555 |
+
halo
|
| 556 |
+
horns
|
| 557 |
+
horn bow
|
| 558 |
+
horn ornament
|
| 559 |
+
horn ribbon
|
| 560 |
+
horn ring
|
| 561 |
+
horned headwear
|
| 562 |
+
horned helmet
|
| 563 |
+
horned hood
|
| 564 |
+
horned mask
|
| 565 |
+
black horns
|
| 566 |
+
blue horns
|
| 567 |
+
brown horns
|
| 568 |
+
purple horns
|
| 569 |
+
red horns
|
| 570 |
+
white horns
|
| 571 |
+
single horn
|
| 572 |
+
multiple horns
|
| 573 |
+
asymmetrical horns
|
| 574 |
+
mismatched horns
|
| 575 |
+
uneven horns
|
| 576 |
+
broken horns
|
| 577 |
+
broken horn
|
| 578 |
+
cone horns
|
| 579 |
+
cow horns
|
| 580 |
+
curled horns
|
| 581 |
+
demon horns
|
| 582 |
+
dragon horns
|
| 583 |
+
fake horns
|
| 584 |
+
fiery horns
|
| 585 |
+
glowing horns
|
| 586 |
+
giraffe horns
|
| 587 |
+
goat horns
|
| 588 |
+
gradient horns
|
| 589 |
+
hair horns
|
| 590 |
+
huge horns
|
| 591 |
+
low horns
|
| 592 |
+
mechanical horns
|
| 593 |
+
sheep horns
|
| 594 |
+
tree horns
|
| 595 |
+
skin-covered horns
|
| 596 |
+
broken halo
|
| 597 |
+
compass rose halo
|
| 598 |
+
crescent halo
|
| 599 |
+
cruciform halo
|
| 600 |
+
dark halo
|
| 601 |
+
double halo
|
| 602 |
+
drawn halo
|
| 603 |
+
faded halo
|
| 604 |
+
flaming halo
|
| 605 |
+
fake halo
|
| 606 |
+
liquid halo
|
| 607 |
+
mechanical halo
|
| 608 |
+
melting halo
|
| 609 |
+
star halo
|
| 610 |
+
square halo
|
| 611 |
+
winged halo
|
| 612 |
+
animal ear fluff
|
| 613 |
+
fangs
|
| 614 |
+
horse girl
|
| 615 |
+
fang
|
| 616 |
+
abs
|
| 617 |
+
cat girl
|
| 618 |
+
sharp teeth
|
| 619 |
+
demon girl
|
| 620 |
+
robot
|
| 621 |
+
elf
|
| 622 |
+
monster girl
|
| 623 |
+
goblin
|
| 624 |
+
slime
|
| 625 |
+
dark elf
|
| 626 |
+
high elf
|
| 627 |
+
orc
|
| 628 |
+
monster
|
| 629 |
+
fairy
|
| 630 |
+
dragon
|
| 631 |
+
ghost
|
| 632 |
+
spirit
|
| 633 |
+
angel
|
| 634 |
+
devil
|
| 635 |
+
vampire
|
| 636 |
+
dog
|
| 637 |
+
gorilla
|
| 638 |
+
cat
|
| 639 |
+
bear
|
| 640 |
+
raccoon
|
| 641 |
+
wolf
|
| 642 |
+
squirrel
|
| 643 |
+
pig
|
| 644 |
+
horse
|
| 645 |
+
bat
|
| 646 |
+
deer
|
| 647 |
+
lion
|
| 648 |
+
cow
|
| 649 |
+
sheep
|
| 650 |
+
fox
|
| 651 |
+
goat
|
| 652 |
+
ferret
|
| 653 |
+
mouse
|
| 654 |
+
rabbit
|
| 655 |
+
panda
|
| 656 |
+
tiger
|
| 657 |
+
short hair with long locks
|
| 658 |
+
oni horns
|
| 659 |
+
bird wings
|
| 660 |
+
two tails
|
| 661 |
+
shark tail
|
| 662 |
+
raccoon tail
|
| 663 |
+
bird tail
|
| 664 |
+
striped tail
|
| 665 |
+
black tail
|
| 666 |
+
thorns
|
| 667 |
+
multicolored wings
|
| 668 |
+
purple wings
|
| 669 |
+
large tail
|
| 670 |
+
spread wings
|
| 671 |
+
flame-tipped tail
|
| 672 |
+
small horns
|
| 673 |
+
mechanical tail
|
| 674 |
+
striped horns
|
| 675 |
+
grey horns
|
| 676 |
+
horns through headwear
|
| 677 |
+
white tail
|
| 678 |
+
purple tail
|
| 679 |
+
lizard tail
|
| 680 |
+
multicolored horns
|
| 681 |
+
tapir tail
|
| 682 |
+
cetacean tail
|
| 683 |
+
brown tail
|
| 684 |
+
stuffed winged unicorn
|
| 685 |
+
glowing hair
|
| 686 |
+
jaguar tail
|
| 687 |
+
green tail
|
| 688 |
+
pink tail
|
| 689 |
+
orange tail
|
| 690 |
+
yellow tail
|
| 691 |
+
glowing tattoo
|
| 692 |
+
grey tail
|
| 693 |
+
blue tail
|
| 694 |
+
eevee tail
|
| 695 |
+
zebra tail
|
| 696 |
+
no horns
|
| 697 |
+
skunk tail
|
| 698 |
+
trimmed tail
|
| 699 |
+
elephant tail
|
| 700 |
+
leaf wings
|
| 701 |
+
aqua horns
|
| 702 |
+
very long tail
|
| 703 |
+
glowing feather
|
| 704 |
+
tamandua tail
|
| 705 |
+
party horn
|
| 706 |
+
extra tails
|
| 707 |
+
spiked horns
|
| 708 |
+
drawn tail
|
| 709 |
+
meerkat tail
|
| 710 |
+
airplane wing
|
| 711 |
+
panther tail
|
| 712 |
+
glowing hands
|
| 713 |
+
winged unicorn
|
| 714 |
+
tail insertion
|
| 715 |
+
skeletal wings
|
| 716 |
+
fur-tipped tail
|
| 717 |
+
antelope horns
|
| 718 |
+
glowing skin
|
| 719 |
+
cheetah tail
|
| 720 |
+
tasmanian devil tail
|
| 721 |
+
french horn
|
| 722 |
+
moose tail
|
| 723 |
+
hyena tail
|
| 724 |
+
panda tail
|
| 725 |
+
gold horns
|
| 726 |
+
giraffe tail
|
| 727 |
+
implied tail plug
|
| 728 |
+
horn flower
|
| 729 |
+
hugging another's tail
|
| 730 |
+
electric plug tail
|
| 731 |
+
chameleon tail
|
| 732 |
+
anteater tail
|
| 733 |
+
large horns
|
| 734 |
+
aqua wings
|
| 735 |
+
aardwolf tail
|
| 736 |
+
cattail
|
| 737 |
+
mismatched wings
|
| 738 |
+
beaver tail
|
| 739 |
+
tailjob
|
| 740 |
+
chipmunk tail
|
| 741 |
+
jackal tail
|
| 742 |
+
dinosaur tail
|
| 743 |
+
fuse tail
|
| 744 |
+
drawn wings
|
| 745 |
+
red panda tail
|
| 746 |
+
two-tone wings
|
| 747 |
+
yellow horns
|
| 748 |
+
no tail
|
| 749 |
+
red tail
|
| 750 |
+
hair on horn
|
| 751 |
+
otter tail
|
| 752 |
+
hugging tail
|
| 753 |
+
cable tail
|
| 754 |
+
green horns
|
| 755 |
+
multicolored tail
|
| 756 |
+
hedgehog tail
|
| 757 |
+
lifted by tail
|
| 758 |
+
pink horns
|
| 759 |
+
penguin tail
|
| 760 |
+
alpaca tail
|
| 761 |
+
orange horns
|
| 762 |
+
pegasus wings
|
| 763 |
+
long horns
|
| 764 |
+
weasel tail
|
| 765 |
+
pokemon tail
|
| 766 |
+
hugging own tail
|
| 767 |
+
orange wings
|
| 768 |
+
ox horns
|
| 769 |
+
goat tail
|
| 770 |
+
expressive tail
|
| 771 |
+
forked tail
|
| 772 |
+
hair behind ear
|
| 773 |
+
notched ear
|
| 774 |
+
hand on own ear
|
| 775 |
+
hand on another's ear
|
| 776 |
+
grabbing another's ear
|
| 777 |
+
blowing in ear
|
| 778 |
+
bandaid on ear
|
| 779 |
+
fox girl
|
| 780 |
+
magical girl
|
| 781 |
+
dragon girl
|
| 782 |
+
rabbit girl
|
| 783 |
+
wolf girl
|
| 784 |
+
dog girl
|
| 785 |
+
minigirl
|
| 786 |
+
cow girl
|
| 787 |
+
shark girl
|
| 788 |
+
mouse girl
|
| 789 |
+
tiger girl
|
| 790 |
+
arthropod girl
|
| 791 |
+
raccoon girl
|
| 792 |
+
lion girl
|
| 793 |
+
sheep girl
|
| 794 |
+
bird girl
|
| 795 |
+
slime girl
|
| 796 |
+
squirrel girl
|
| 797 |
+
fish girl
|
| 798 |
+
bear girl
|
| 799 |
+
spider girl
|
| 800 |
+
goat girl
|
| 801 |
+
plant girl
|
| 802 |
+
bat girl
|
| 803 |
+
robot girl
|
| 804 |
+
deer girl
|
| 805 |
+
leopard girl
|
| 806 |
+
frog girl
|
| 807 |
+
reptile girl
|
| 808 |
+
moth girl
|
| 809 |
+
monkey girl
|
| 810 |
+
owl girl
|
| 811 |
+
penguin girl
|
| 812 |
+
weasel girl
|
| 813 |
+
chipmunk girl
|
| 814 |
+
dolphin girl
|
| 815 |
+
orca girl
|
| 816 |
+
bee girl
|
| 817 |
+
hedgehog girl
|
| 818 |
+
panda girl
|
| 819 |
+
pig girl
|
| 820 |
+
jaguar girl
|
| 821 |
+
dinosaur girl
|
| 822 |
+
hyena girl
|
| 823 |
+
ox girl
|
| 824 |
+
reindeer girl
|
| 825 |
+
alpaca girl
|
| 826 |
+
prairie dog girl
|
| 827 |
+
red panda girl
|
| 828 |
+
otter girl
|
| 829 |
+
mantis girl
|
| 830 |
+
hamster girl
|
| 831 |
+
unicorn girl
|
| 832 |
+
mushroom girl
|
| 833 |
+
ferret girl
|
| 834 |
+
cheetah girl
|
| 835 |
+
whale girl
|
| 836 |
+
cockroach girl
|
| 837 |
+
zebra girl
|
| 838 |
+
slug girl
|
| 839 |
+
snake girl
|
| 840 |
+
ghost girl
|
| 841 |
+
capybara girl
|
| 842 |
+
donkey girl
|
| 843 |
+
scorpion girl
|
| 844 |
+
monster boy
|
| 845 |
+
cat boy
|
| 846 |
+
tomboy
|
| 847 |
+
demon boy
|
| 848 |
+
dog boy
|
| 849 |
+
fox boy
|
| 850 |
+
wolf boy
|
| 851 |
+
tiger boy
|
| 852 |
+
miniboy
|
| 853 |
+
dragon boy
|
| 854 |
+
cow boy
|
| 855 |
+
rabbit boy
|
| 856 |
+
lion boy
|
| 857 |
+
fish boy
|
| 858 |
+
bear boy
|
| 859 |
+
arthropod boy
|
| 860 |
+
game boy
|
| 861 |
+
bird boy
|
| 862 |
+
monkey boy
|
| 863 |
+
leopard boy
|
| 864 |
+
magical boy
|
| 865 |
+
goat boy
|
| 866 |
+
horse boy
|
| 867 |
+
jackal boy
|
| 868 |
+
girly boy
|
| 869 |
+
cowboy
|
| 870 |
+
reptile boy
|
| 871 |
+
jaguar boy
|
| 872 |
+
shark boy
|
| 873 |
+
octopus boy
|
| 874 |
+
boar boy
|
| 875 |
+
slime boy
|
| 876 |
+
harpy boy
|
| 877 |
+
mouse boy
|
| 878 |
+
cuntboy
|
| 879 |
+
sheep boy
|
| 880 |
+
deer boy
|
| 881 |
+
raccoon boy
|
data/danbooru_character.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/partition_loader.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
NAIA-WEB Partition Loader
|
| 3 |
+
TGP file loading for Quick Search functionality
|
| 4 |
+
|
| 5 |
+
Reference: NAIA2.0/ui/remote/quick_search_tab.py (145-255)
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import struct
|
| 9 |
+
import lzma
|
| 10 |
+
import pickle
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Dict, List, Set, Optional
|
| 13 |
+
from collections import Counter
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
import numpy as np
|
| 17 |
+
HAS_NUMPY = True
|
| 18 |
+
except ImportError:
|
| 19 |
+
HAS_NUMPY = False
|
| 20 |
+
np = None
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# Data directory
|
| 24 |
+
DATA_DIR = Path(__file__).parent / "quick_search"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class SinglePartitionStore:
|
| 28 |
+
"""
|
| 29 |
+
Single partition storage (inverted index based) - Quick Search lightweight version
|
| 30 |
+
|
| 31 |
+
Reference: NAIA2.0/ui/remote/quick_search_tab.py SinglePartitionStore class
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
MAGIC = b'TGP1'
|
| 35 |
+
VERSION = 1
|
| 36 |
+
|
| 37 |
+
def __init__(self):
|
| 38 |
+
self.num_events: int = 0
|
| 39 |
+
self._event_tag_indices = None
|
| 40 |
+
self._event_tag_indptr = None
|
| 41 |
+
self._event_counts = None
|
| 42 |
+
self._tag_to_events: Dict[int, object] = {}
|
| 43 |
+
self._loaded: bool = False
|
| 44 |
+
|
| 45 |
+
@classmethod
|
| 46 |
+
def load(cls, input_path: str) -> 'SinglePartitionStore':
|
| 47 |
+
"""Load partition file"""
|
| 48 |
+
if not HAS_NUMPY:
|
| 49 |
+
raise RuntimeError("NumPy is required")
|
| 50 |
+
|
| 51 |
+
store = cls()
|
| 52 |
+
|
| 53 |
+
with open(input_path, 'rb') as f:
|
| 54 |
+
magic = f.read(4)
|
| 55 |
+
if magic != cls.MAGIC:
|
| 56 |
+
raise ValueError(f"Invalid format: {magic}")
|
| 57 |
+
|
| 58 |
+
_ = struct.unpack('<H', f.read(2))[0] # version
|
| 59 |
+
compressed_len = struct.unpack('<I', f.read(4))[0]
|
| 60 |
+
compressed = f.read(compressed_len)
|
| 61 |
+
|
| 62 |
+
serialized = lzma.decompress(compressed)
|
| 63 |
+
data = pickle.loads(serialized)
|
| 64 |
+
|
| 65 |
+
store.num_events = data['num_events']
|
| 66 |
+
store._event_tag_indices = np.frombuffer(data['event_tag_indices'], dtype=np.uint16).copy()
|
| 67 |
+
store._event_tag_indptr = np.frombuffer(data['event_tag_indptr'], dtype=np.int32).copy()
|
| 68 |
+
store._event_counts = np.frombuffer(data['event_counts'], dtype=np.int32).copy()
|
| 69 |
+
|
| 70 |
+
store._tag_to_events = {
|
| 71 |
+
int(k): np.frombuffer(v, dtype=np.int32).copy()
|
| 72 |
+
for k, v in data['tag_to_events'].items()
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
store._loaded = True
|
| 76 |
+
return store
|
| 77 |
+
|
| 78 |
+
def filter_events(
|
| 79 |
+
self,
|
| 80 |
+
required_tags: Optional[List[str]] = None,
|
| 81 |
+
excluded_tags: Optional[List[str]] = None,
|
| 82 |
+
tag_to_id: Optional[Dict[str, int]] = None
|
| 83 |
+
):
|
| 84 |
+
"""Return event indices matching conditions"""
|
| 85 |
+
if not self._loaded or not HAS_NUMPY:
|
| 86 |
+
return np.array([], dtype=np.int32) if HAS_NUMPY else []
|
| 87 |
+
|
| 88 |
+
# Start with all events
|
| 89 |
+
candidates = set(range(self.num_events))
|
| 90 |
+
|
| 91 |
+
# Required tags
|
| 92 |
+
if required_tags and tag_to_id:
|
| 93 |
+
for tag in required_tags:
|
| 94 |
+
if tag in tag_to_id:
|
| 95 |
+
tag_id = tag_to_id[tag]
|
| 96 |
+
if tag_id in self._tag_to_events:
|
| 97 |
+
candidates &= set(self._tag_to_events[tag_id])
|
| 98 |
+
else:
|
| 99 |
+
return np.array([], dtype=np.int32)
|
| 100 |
+
else:
|
| 101 |
+
return np.array([], dtype=np.int32)
|
| 102 |
+
|
| 103 |
+
# Excluded tags
|
| 104 |
+
if excluded_tags and tag_to_id:
|
| 105 |
+
for tag in excluded_tags:
|
| 106 |
+
if tag in tag_to_id:
|
| 107 |
+
tag_id = tag_to_id[tag]
|
| 108 |
+
if tag_id in self._tag_to_events:
|
| 109 |
+
candidates -= set(self._tag_to_events[tag_id])
|
| 110 |
+
|
| 111 |
+
return np.array(sorted(candidates), dtype=np.int32)
|
| 112 |
+
|
| 113 |
+
def get_tag_counts(
|
| 114 |
+
self,
|
| 115 |
+
event_indices=None,
|
| 116 |
+
id_to_tag: Optional[Dict[int, str]] = None
|
| 117 |
+
) -> Counter:
|
| 118 |
+
"""Count events per tag"""
|
| 119 |
+
if not HAS_NUMPY or id_to_tag is None:
|
| 120 |
+
return Counter()
|
| 121 |
+
|
| 122 |
+
if event_indices is None or len(event_indices) == 0:
|
| 123 |
+
# Total tag counts
|
| 124 |
+
return Counter({
|
| 125 |
+
id_to_tag[tag_id]: len(events)
|
| 126 |
+
for tag_id, events in self._tag_to_events.items()
|
| 127 |
+
if tag_id in id_to_tag
|
| 128 |
+
})
|
| 129 |
+
|
| 130 |
+
event_set = set(event_indices)
|
| 131 |
+
return Counter({
|
| 132 |
+
id_to_tag[tag_id]: len(set(events) & event_set)
|
| 133 |
+
for tag_id, events in self._tag_to_events.items()
|
| 134 |
+
if tag_id in id_to_tag
|
| 135 |
+
})
|
| 136 |
+
|
| 137 |
+
def get_event_tags(
|
| 138 |
+
self,
|
| 139 |
+
event_idx: int,
|
| 140 |
+
id_to_tag: Optional[Dict[int, str]] = None
|
| 141 |
+
) -> Set[str]:
|
| 142 |
+
"""Return tags for an event"""
|
| 143 |
+
if not self._loaded or id_to_tag is None:
|
| 144 |
+
return set()
|
| 145 |
+
|
| 146 |
+
if event_idx < 0 or event_idx >= self.num_events:
|
| 147 |
+
return set()
|
| 148 |
+
|
| 149 |
+
start = self._event_tag_indptr[event_idx]
|
| 150 |
+
end = self._event_tag_indptr[event_idx + 1]
|
| 151 |
+
tag_ids = self._event_tag_indices[start:end]
|
| 152 |
+
|
| 153 |
+
return {id_to_tag[int(tid)] for tid in tag_ids if int(tid) in id_to_tag}
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class PartitionMetadata:
|
| 157 |
+
"""
|
| 158 |
+
Metadata for partition files
|
| 159 |
+
|
| 160 |
+
Reference: NAIA2.0/ui/remote/quick_search_tab.py metadata loading
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
MAGIC = b'TGPS'
|
| 164 |
+
|
| 165 |
+
def __init__(self):
|
| 166 |
+
self.tag_to_id: Dict[str, int] = {}
|
| 167 |
+
self.id_to_tag: Dict[int, str] = {}
|
| 168 |
+
self.tag_freq: Dict[str, int] = {}
|
| 169 |
+
self.partitions: Dict[str, Dict] = {}
|
| 170 |
+
self._loaded: bool = False
|
| 171 |
+
|
| 172 |
+
@classmethod
|
| 173 |
+
def load(cls, input_path: str) -> 'PartitionMetadata':
|
| 174 |
+
"""Load metadata file"""
|
| 175 |
+
meta = cls()
|
| 176 |
+
|
| 177 |
+
with open(input_path, 'rb') as f:
|
| 178 |
+
magic = f.read(4)
|
| 179 |
+
if magic != cls.MAGIC:
|
| 180 |
+
raise ValueError(f"Invalid metadata format: {magic}")
|
| 181 |
+
|
| 182 |
+
_ = struct.unpack('<H', f.read(2))[0] # version
|
| 183 |
+
compressed_len = struct.unpack('<I', f.read(4))[0]
|
| 184 |
+
compressed = f.read(compressed_len)
|
| 185 |
+
|
| 186 |
+
serialized = lzma.decompress(compressed)
|
| 187 |
+
data = pickle.loads(serialized)
|
| 188 |
+
|
| 189 |
+
meta.tag_to_id = data.get('tag_to_id', {})
|
| 190 |
+
meta.id_to_tag = data.get('id_to_tag', {})
|
| 191 |
+
meta.tag_freq = data.get('tag_freq', {})
|
| 192 |
+
meta.partitions = data.get('partitions', {})
|
| 193 |
+
meta._loaded = True
|
| 194 |
+
|
| 195 |
+
return meta
|
| 196 |
+
|
| 197 |
+
@property
|
| 198 |
+
def is_loaded(self) -> bool:
|
| 199 |
+
return self._loaded
|
| 200 |
+
|
| 201 |
+
def get_partition_names(self) -> List[str]:
|
| 202 |
+
"""Return list of available partition names"""
|
| 203 |
+
return list(self.partitions.keys())
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class PartitionManager:
|
| 207 |
+
"""
|
| 208 |
+
Manages partition loading and caching
|
| 209 |
+
"""
|
| 210 |
+
|
| 211 |
+
def __init__(self):
|
| 212 |
+
self._metadata: Optional[PartitionMetadata] = None
|
| 213 |
+
self._loaded_partitions: Dict[str, SinglePartitionStore] = {}
|
| 214 |
+
self._data_dir = DATA_DIR
|
| 215 |
+
|
| 216 |
+
def is_data_available(self) -> bool:
|
| 217 |
+
"""Check if partition data files are available"""
|
| 218 |
+
metadata_path = self._data_dir / "metadata.tgpm"
|
| 219 |
+
return metadata_path.exists()
|
| 220 |
+
|
| 221 |
+
def load_metadata(self) -> Optional[PartitionMetadata]:
|
| 222 |
+
"""Load partition metadata"""
|
| 223 |
+
if self._metadata is not None:
|
| 224 |
+
return self._metadata
|
| 225 |
+
|
| 226 |
+
metadata_path = self._data_dir / "metadata.tgpm"
|
| 227 |
+
if not metadata_path.exists():
|
| 228 |
+
return None
|
| 229 |
+
|
| 230 |
+
try:
|
| 231 |
+
self._metadata = PartitionMetadata.load(str(metadata_path))
|
| 232 |
+
return self._metadata
|
| 233 |
+
except Exception as e:
|
| 234 |
+
print(f"Error loading metadata: {e}")
|
| 235 |
+
return None
|
| 236 |
+
|
| 237 |
+
def get_metadata(self) -> Optional[PartitionMetadata]:
|
| 238 |
+
"""Get loaded metadata (load if needed)"""
|
| 239 |
+
if self._metadata is None:
|
| 240 |
+
self.load_metadata()
|
| 241 |
+
return self._metadata
|
| 242 |
+
|
| 243 |
+
def load_partition(self, partition_name: str) -> Optional[SinglePartitionStore]:
|
| 244 |
+
"""Load a specific partition"""
|
| 245 |
+
if partition_name in self._loaded_partitions:
|
| 246 |
+
return self._loaded_partitions[partition_name]
|
| 247 |
+
|
| 248 |
+
partition_path = self._data_dir / f"{partition_name}.tgp"
|
| 249 |
+
if not partition_path.exists():
|
| 250 |
+
print(f"Partition file not found: {partition_path}")
|
| 251 |
+
return None
|
| 252 |
+
|
| 253 |
+
try:
|
| 254 |
+
store = SinglePartitionStore.load(str(partition_path))
|
| 255 |
+
self._loaded_partitions[partition_name] = store
|
| 256 |
+
return store
|
| 257 |
+
except Exception as e:
|
| 258 |
+
print(f"Error loading partition {partition_name}: {e}")
|
| 259 |
+
return None
|
| 260 |
+
|
| 261 |
+
def unload_partition(self, partition_name: str):
|
| 262 |
+
"""Unload a partition to free memory"""
|
| 263 |
+
if partition_name in self._loaded_partitions:
|
| 264 |
+
del self._loaded_partitions[partition_name]
|
| 265 |
+
|
| 266 |
+
def unload_all(self):
|
| 267 |
+
"""Unload all partitions"""
|
| 268 |
+
self._loaded_partitions.clear()
|
| 269 |
+
|
| 270 |
+
def get_partition_filename(self, rating: str, person: str) -> str:
|
| 271 |
+
"""
|
| 272 |
+
Get partition filename from rating and person category
|
| 273 |
+
|
| 274 |
+
Args:
|
| 275 |
+
rating: 'g', 's', 'q', or 'e'
|
| 276 |
+
person: person category like '1girl_solo'
|
| 277 |
+
|
| 278 |
+
Returns:
|
| 279 |
+
Partition name like 'g_1girl_solo'
|
| 280 |
+
"""
|
| 281 |
+
return f"{rating}_{person}"
|
data/tag_store.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
NAIA-WEB Tag Store
|
| 3 |
+
High-level interface for tag search and Quick Search functionality
|
| 4 |
+
|
| 5 |
+
Reference: NAIA2.0/ui/remote/quick_search_tab.py
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import random
|
| 9 |
+
from typing import List, Optional, Set, Tuple
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from collections import Counter
|
| 12 |
+
|
| 13 |
+
from .partition_loader import PartitionManager, SinglePartitionStore, PartitionMetadata
|
| 14 |
+
from utils.constants import PERSON_AUTO_TAGS
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class TagInfo:
|
| 19 |
+
"""Information about a single tag"""
|
| 20 |
+
tag: str
|
| 21 |
+
count: int
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class QuickSearchResult:
|
| 26 |
+
"""Result of a quick search operation"""
|
| 27 |
+
success: bool
|
| 28 |
+
prompt: str = ""
|
| 29 |
+
tags: List[str] = None
|
| 30 |
+
event_count: int = 0
|
| 31 |
+
error_message: str = ""
|
| 32 |
+
|
| 33 |
+
def __post_init__(self):
|
| 34 |
+
if self.tags is None:
|
| 35 |
+
self.tags = []
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class TagStore:
|
| 39 |
+
"""
|
| 40 |
+
High-level interface for Quick Search functionality.
|
| 41 |
+
|
| 42 |
+
Provides:
|
| 43 |
+
- Partition loading and management
|
| 44 |
+
- Tag filtering by rating and person category
|
| 45 |
+
- Include/Exclude tag filtering
|
| 46 |
+
- Random event sampling for prompt generation
|
| 47 |
+
- Tag frequency information
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
def __init__(self):
|
| 51 |
+
self._manager = PartitionManager()
|
| 52 |
+
self._current_partition: Optional[SinglePartitionStore] = None
|
| 53 |
+
self._current_partition_name: str = ""
|
| 54 |
+
|
| 55 |
+
def is_available(self) -> bool:
|
| 56 |
+
"""Check if tag data is available"""
|
| 57 |
+
return self._manager.is_data_available()
|
| 58 |
+
|
| 59 |
+
def get_available_partitions(self) -> List[str]:
|
| 60 |
+
"""Get list of available partition names"""
|
| 61 |
+
metadata = self._manager.get_metadata()
|
| 62 |
+
if metadata is None:
|
| 63 |
+
return []
|
| 64 |
+
return metadata.get_partition_names()
|
| 65 |
+
|
| 66 |
+
def load_partition(self, rating: str, person: str) -> bool:
|
| 67 |
+
"""
|
| 68 |
+
Load partition for given rating and person category.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
rating: 'g', 's', 'q', or 'e'
|
| 72 |
+
person: Person category like '1girl_solo'
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
True if loaded successfully
|
| 76 |
+
"""
|
| 77 |
+
partition_name = self._manager.get_partition_filename(rating, person)
|
| 78 |
+
|
| 79 |
+
if partition_name == self._current_partition_name and self._current_partition is not None:
|
| 80 |
+
return True # Already loaded
|
| 81 |
+
|
| 82 |
+
# Unload previous partition to save memory
|
| 83 |
+
if self._current_partition_name:
|
| 84 |
+
self._manager.unload_partition(self._current_partition_name)
|
| 85 |
+
|
| 86 |
+
partition = self._manager.load_partition(partition_name)
|
| 87 |
+
if partition is None:
|
| 88 |
+
self._current_partition = None
|
| 89 |
+
self._current_partition_name = ""
|
| 90 |
+
return False
|
| 91 |
+
|
| 92 |
+
self._current_partition = partition
|
| 93 |
+
self._current_partition_name = partition_name
|
| 94 |
+
return True
|
| 95 |
+
|
| 96 |
+
def get_event_count(self) -> int:
|
| 97 |
+
"""Get number of events in current partition"""
|
| 98 |
+
if self._current_partition is None:
|
| 99 |
+
return 0
|
| 100 |
+
return self._current_partition.num_events
|
| 101 |
+
|
| 102 |
+
def get_filtered_event_count(
|
| 103 |
+
self,
|
| 104 |
+
include_tags: Optional[List[str]] = None,
|
| 105 |
+
exclude_tags: Optional[List[str]] = None
|
| 106 |
+
) -> int:
|
| 107 |
+
"""Get number of events matching filter criteria"""
|
| 108 |
+
if self._current_partition is None:
|
| 109 |
+
return 0
|
| 110 |
+
|
| 111 |
+
metadata = self._manager.get_metadata()
|
| 112 |
+
if metadata is None:
|
| 113 |
+
return 0
|
| 114 |
+
|
| 115 |
+
filtered = self._current_partition.filter_events(
|
| 116 |
+
required_tags=include_tags,
|
| 117 |
+
excluded_tags=exclude_tags,
|
| 118 |
+
tag_to_id=metadata.tag_to_id
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
return len(filtered)
|
| 122 |
+
|
| 123 |
+
def get_top_tags(
|
| 124 |
+
self,
|
| 125 |
+
include_tags: Optional[List[str]] = None,
|
| 126 |
+
exclude_tags: Optional[List[str]] = None,
|
| 127 |
+
limit: int = 50,
|
| 128 |
+
offset: int = 0
|
| 129 |
+
) -> List[TagInfo]:
|
| 130 |
+
"""
|
| 131 |
+
Get most frequent tags in current partition with filters applied.
|
| 132 |
+
|
| 133 |
+
Returns tags sorted by frequency (descending).
|
| 134 |
+
"""
|
| 135 |
+
if self._current_partition is None:
|
| 136 |
+
return []
|
| 137 |
+
|
| 138 |
+
metadata = self._manager.get_metadata()
|
| 139 |
+
if metadata is None:
|
| 140 |
+
return []
|
| 141 |
+
|
| 142 |
+
# Filter events
|
| 143 |
+
filtered_indices = self._current_partition.filter_events(
|
| 144 |
+
required_tags=include_tags,
|
| 145 |
+
excluded_tags=exclude_tags,
|
| 146 |
+
tag_to_id=metadata.tag_to_id
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
if len(filtered_indices) == 0:
|
| 150 |
+
return []
|
| 151 |
+
|
| 152 |
+
# Count tags in filtered events
|
| 153 |
+
tag_counts = self._current_partition.get_tag_counts(
|
| 154 |
+
event_indices=filtered_indices,
|
| 155 |
+
id_to_tag=metadata.id_to_tag
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
# Exclude already included/excluded tags from results
|
| 159 |
+
excluded_set = set(include_tags or []) | set(exclude_tags or [])
|
| 160 |
+
|
| 161 |
+
# Sort by count and return with pagination
|
| 162 |
+
sorted_tags = sorted(
|
| 163 |
+
[(tag, count) for tag, count in tag_counts.items() if tag not in excluded_set],
|
| 164 |
+
key=lambda x: x[1],
|
| 165 |
+
reverse=True
|
| 166 |
+
)[offset:offset + limit]
|
| 167 |
+
|
| 168 |
+
return [TagInfo(tag=tag, count=count) for tag, count in sorted_tags]
|
| 169 |
+
|
| 170 |
+
def get_total_tag_count(
|
| 171 |
+
self,
|
| 172 |
+
include_tags: Optional[List[str]] = None,
|
| 173 |
+
exclude_tags: Optional[List[str]] = None
|
| 174 |
+
) -> int:
|
| 175 |
+
"""Get total number of unique tags matching filter criteria"""
|
| 176 |
+
if self._current_partition is None:
|
| 177 |
+
return 0
|
| 178 |
+
|
| 179 |
+
metadata = self._manager.get_metadata()
|
| 180 |
+
if metadata is None:
|
| 181 |
+
return 0
|
| 182 |
+
|
| 183 |
+
# Filter events
|
| 184 |
+
filtered_indices = self._current_partition.filter_events(
|
| 185 |
+
required_tags=include_tags,
|
| 186 |
+
excluded_tags=exclude_tags,
|
| 187 |
+
tag_to_id=metadata.tag_to_id
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
if len(filtered_indices) == 0:
|
| 191 |
+
return 0
|
| 192 |
+
|
| 193 |
+
# Count tags in filtered events
|
| 194 |
+
tag_counts = self._current_partition.get_tag_counts(
|
| 195 |
+
event_indices=filtered_indices,
|
| 196 |
+
id_to_tag=metadata.id_to_tag
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
# Exclude already included/excluded tags
|
| 200 |
+
excluded_set = set(include_tags or []) | set(exclude_tags or [])
|
| 201 |
+
return len([tag for tag in tag_counts.keys() if tag not in excluded_set])
|
| 202 |
+
|
| 203 |
+
def generate_random_prompt(
|
| 204 |
+
self,
|
| 205 |
+
rating: str,
|
| 206 |
+
person: str,
|
| 207 |
+
include_tags: Optional[List[str]] = None,
|
| 208 |
+
exclude_tags: Optional[List[str]] = None
|
| 209 |
+
) -> QuickSearchResult:
|
| 210 |
+
"""
|
| 211 |
+
Generate a random prompt from the partition.
|
| 212 |
+
|
| 213 |
+
Args:
|
| 214 |
+
rating: Rating code ('g', 's', 'q', 'e')
|
| 215 |
+
person: Person category
|
| 216 |
+
include_tags: Tags that must be present
|
| 217 |
+
exclude_tags: Tags that must not be present
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
QuickSearchResult with generated prompt
|
| 221 |
+
"""
|
| 222 |
+
# Load partition
|
| 223 |
+
if not self.load_partition(rating, person):
|
| 224 |
+
return QuickSearchResult(
|
| 225 |
+
success=False,
|
| 226 |
+
error_message=f"Failed to load partition: {rating}_{person}"
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
metadata = self._manager.get_metadata()
|
| 230 |
+
if metadata is None:
|
| 231 |
+
return QuickSearchResult(
|
| 232 |
+
success=False,
|
| 233 |
+
error_message="Metadata not available"
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# Get auto-tags for person category
|
| 237 |
+
auto_tags = PERSON_AUTO_TAGS.get(person, [])
|
| 238 |
+
|
| 239 |
+
# Combine include tags with auto tags
|
| 240 |
+
all_include = list(set((include_tags or []) + auto_tags))
|
| 241 |
+
|
| 242 |
+
# Filter events
|
| 243 |
+
filtered_indices = self._current_partition.filter_events(
|
| 244 |
+
required_tags=all_include if all_include else None,
|
| 245 |
+
excluded_tags=exclude_tags,
|
| 246 |
+
tag_to_id=metadata.tag_to_id
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
if len(filtered_indices) == 0:
|
| 250 |
+
return QuickSearchResult(
|
| 251 |
+
success=False,
|
| 252 |
+
error_message="No events match the filter criteria",
|
| 253 |
+
event_count=0
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
# Select random event
|
| 257 |
+
random_idx = random.choice(filtered_indices)
|
| 258 |
+
|
| 259 |
+
# Get tags for this event
|
| 260 |
+
event_tags = self._current_partition.get_event_tags(
|
| 261 |
+
event_idx=int(random_idx),
|
| 262 |
+
id_to_tag=metadata.id_to_tag
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
if not event_tags:
|
| 266 |
+
return QuickSearchResult(
|
| 267 |
+
success=False,
|
| 268 |
+
error_message="Failed to get tags for selected event"
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
# Convert to list and create prompt
|
| 272 |
+
tags_list = sorted(list(event_tags))
|
| 273 |
+
prompt = ", ".join(tags_list)
|
| 274 |
+
|
| 275 |
+
return QuickSearchResult(
|
| 276 |
+
success=True,
|
| 277 |
+
prompt=prompt,
|
| 278 |
+
tags=tags_list,
|
| 279 |
+
event_count=len(filtered_indices)
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
def search_tags(
|
| 283 |
+
self,
|
| 284 |
+
query: str,
|
| 285 |
+
limit: int = 20
|
| 286 |
+
) -> List[TagInfo]:
|
| 287 |
+
"""
|
| 288 |
+
Search for tags matching query string.
|
| 289 |
+
|
| 290 |
+
Args:
|
| 291 |
+
query: Search query (partial match)
|
| 292 |
+
limit: Maximum results
|
| 293 |
+
|
| 294 |
+
Returns:
|
| 295 |
+
List of matching tags with frequencies
|
| 296 |
+
"""
|
| 297 |
+
metadata = self._manager.get_metadata()
|
| 298 |
+
if metadata is None or not query:
|
| 299 |
+
return []
|
| 300 |
+
|
| 301 |
+
query_lower = query.lower()
|
| 302 |
+
|
| 303 |
+
# Find matching tags
|
| 304 |
+
matches = []
|
| 305 |
+
for tag, freq in metadata.tag_freq.items():
|
| 306 |
+
if query_lower in tag.lower():
|
| 307 |
+
matches.append(TagInfo(tag=tag, count=freq))
|
| 308 |
+
|
| 309 |
+
# Sort by frequency and return top N
|
| 310 |
+
matches.sort(key=lambda x: x.count, reverse=True)
|
| 311 |
+
return matches[:limit]
|
| 312 |
+
|
| 313 |
+
def get_partition_info(self) -> dict:
|
| 314 |
+
"""Get information about current partition"""
|
| 315 |
+
return {
|
| 316 |
+
"partition_name": self._current_partition_name,
|
| 317 |
+
"event_count": self.get_event_count(),
|
| 318 |
+
"is_loaded": self._current_partition is not None
|
| 319 |
+
}
|