Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Auto-sync from GitHub Actions
Browse files- core/engine.py +124 -53
- ui_nicegui.py +6 -2
core/engine.py
CHANGED
|
@@ -92,7 +92,24 @@ STOP_WORDS: frozenset[str] = frozenset({
|
|
| 92 |
'十分', '非常', '特别', '比较',
|
| 93 |
'图片', '画面', '图像',
|
| 94 |
'位于', '处于',
|
| 95 |
-
'许多', '大量', '各种', '所有', '其他', '其它'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
})
|
| 97 |
|
| 98 |
CAT_MAP: dict[str, str] = {
|
|
@@ -241,6 +258,7 @@ class DanbooruTagger:
|
|
| 241 |
self._setup_jieba_from_memory()
|
| 242 |
self._load_cooc()
|
| 243 |
self._name_to_idx = {n: i for i, n in enumerate(self.df['name'])}
|
|
|
|
| 244 |
self._rebuild_arrays_from_df()
|
| 245 |
self._normalize_embeddings()
|
| 246 |
self._load_groups()
|
|
@@ -631,6 +649,7 @@ class DanbooruTagger:
|
|
| 631 |
|
| 632 |
self.max_log_count = float(np.log1p(self.df['post_count'].max()))
|
| 633 |
self._name_to_idx = {n: i for i, n in enumerate(self.df['name'])}
|
|
|
|
| 634 |
self._rebuild_arrays_from_df()
|
| 635 |
self._load_groups()
|
| 636 |
self._normalize_embeddings()
|
|
@@ -753,67 +772,119 @@ class DanbooruTagger:
|
|
| 753 |
return {'英文': 1.3, '中文核心词': 1.0, '中文扩展词': 0.8, '释义': 0.6}
|
| 754 |
return {'英文': 1.0, '中文核心词': 1.0, '中文扩展词': 1.0, '释义': 1.0}
|
| 755 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
def _smart_split(self, text: str) -> tuple[list[str], list[str]]:
|
| 757 |
-
"""将查询文本拆分为关键词列表,同时返回
|
| 758 |
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
-
|
| 766 |
-
|
|
|
|
|
|
|
|
|
|
| 767 |
|
| 768 |
Returns:
|
| 769 |
-
(tokens,
|
| 770 |
-
- tokens: 处理后的关键词列表
|
| 771 |
-
-
|
| 772 |
"""
|
| 773 |
-
|
| 774 |
-
if not
|
| 775 |
return [], []
|
|
|
|
| 776 |
|
| 777 |
tokens: list[str] = []
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
cleaned = re.sub(r'[,()\[\]{}:]', ' ', chunk)
|
| 788 |
-
for part in cleaned.split():
|
| 789 |
-
try:
|
| 790 |
-
float(part)
|
| 791 |
-
except ValueError:
|
| 792 |
-
tokens.append(part)
|
| 793 |
-
return tokens, segments
|
| 794 |
-
|
| 795 |
-
# 多片段 → 每个分隔的片段按长度决定是否原子保留
|
| 796 |
-
for segment in segments:
|
| 797 |
-
# 纯 CJK 片段
|
| 798 |
-
if re.match(r'^[一-龥]+$', segment):
|
| 799 |
-
if len(segment) <= _ATOMIC_CJK_MAX_LEN:
|
| 800 |
-
tokens.append(segment) # 短 → 原子概念
|
| 801 |
-
else:
|
| 802 |
-
tokens.extend(jieba.cut(segment)) # 长 → jieba 切分
|
| 803 |
continue
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
return tokens, segments
|
| 818 |
|
| 819 |
# ── 关联推荐 ──────────────────────────────────────────────────────────
|
|
|
|
| 92 |
'十分', '非常', '特别', '比较',
|
| 93 |
'图片', '画面', '图像',
|
| 94 |
'位于', '处于',
|
| 95 |
+
'许多', '大量', '各种', '所有', '其他', '其它',
|
| 96 |
+
# ── 英文停用词 ──
|
| 97 |
+
'a', 'an', 'the',
|
| 98 |
+
'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'as', 'into',
|
| 99 |
+
'about', 'between', 'through', 'after', 'before', 'above', 'below',
|
| 100 |
+
'and', 'or', 'but', 'nor', 'so', 'yet',
|
| 101 |
+
'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
| 102 |
+
'do', 'does', 'did', 'done',
|
| 103 |
+
'have', 'has', 'had', 'having',
|
| 104 |
+
'will', 'would', 'shall', 'should', 'can', 'could', 'may', 'might', 'must',
|
| 105 |
+
'not', 'no', 'very', 'too', 'also', 'just', 'only', 'even', 'still',
|
| 106 |
+
'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he', 'him', 'his',
|
| 107 |
+
'she', 'her', 'it', 'its', 'they', 'them', 'their',
|
| 108 |
+
'this', 'that', 'these', 'those', 'which', 'who', 'whom', 'what',
|
| 109 |
+
'there', 'here', 'where', 'when', 'how', 'all', 'each', 'every',
|
| 110 |
+
'some', 'any', 'few', 'more', 'most', 'other', 'such',
|
| 111 |
+
'than', 'up', 'out', 'if', 'then', 'else', 'while', 'during',
|
| 112 |
+
'both', 'same', 'own', 'now',
|
| 113 |
})
|
| 114 |
|
| 115 |
CAT_MAP: dict[str, str] = {
|
|
|
|
| 258 |
self._setup_jieba_from_memory()
|
| 259 |
self._load_cooc()
|
| 260 |
self._name_to_idx = {n: i for i, n in enumerate(self.df['name'])}
|
| 261 |
+
self._tag_names_set: set[str] = set(self._name_to_idx.keys())
|
| 262 |
self._rebuild_arrays_from_df()
|
| 263 |
self._normalize_embeddings()
|
| 264 |
self._load_groups()
|
|
|
|
| 649 |
|
| 650 |
self.max_log_count = float(np.log1p(self.df['post_count'].max()))
|
| 651 |
self._name_to_idx = {n: i for i, n in enumerate(self.df['name'])}
|
| 652 |
+
self._tag_names_set = set(self._name_to_idx.keys())
|
| 653 |
self._rebuild_arrays_from_df()
|
| 654 |
self._load_groups()
|
| 655 |
self._normalize_embeddings()
|
|
|
|
| 772 |
return {'英文': 1.3, '中文核心词': 1.0, '中文扩展词': 0.8, '释义': 0.6}
|
| 773 |
return {'英文': 1.0, '中文核心词': 1.0, '中文扩展词': 1.0, '释义': 1.0}
|
| 774 |
|
| 775 |
+
# ── 英文分词辅助 ──────────────────────────────────────────────────────
|
| 776 |
+
|
| 777 |
+
_EN_MAX_COMPOUND = 4 # 复合标签最大单词数
|
| 778 |
+
|
| 779 |
+
def _tokenize_en_chunk(self, chunk: str) -> list[str]:
|
| 780 |
+
"""对一段英文文本做分词:清洗 → 按空格切分 → 过滤停用词/纯数 → 合并已知复合标签。"""
|
| 781 |
+
cleaned = re.sub(r'[,()\[\]{}:]', ' ', chunk)
|
| 782 |
+
raw = [p for p in cleaned.split() if p]
|
| 783 |
+
tag_set = getattr(self, '_tag_names_set', None)
|
| 784 |
+
tokens: list[str] = []
|
| 785 |
+
for part in raw:
|
| 786 |
+
low = part.lower()
|
| 787 |
+
# 已知标签直接保留(如用户输入了带下划线的 tag 名)
|
| 788 |
+
if tag_set and low in tag_set:
|
| 789 |
+
tokens.append(low)
|
| 790 |
+
continue
|
| 791 |
+
if low in STOP_WORDS:
|
| 792 |
+
continue
|
| 793 |
+
if part.isdigit(): # 仅过滤纯数字,保留 3d/2b 等含数字的词
|
| 794 |
+
continue
|
| 795 |
+
tokens.append(low)
|
| 796 |
+
if not tokens:
|
| 797 |
+
return []
|
| 798 |
+
return self._merge_compound_english(tokens)
|
| 799 |
+
|
| 800 |
+
def _merge_compound_english(self, tokens: list[str]) -> list[str]:
|
| 801 |
+
"""将相邻英文单词合并为已知的 Danbooru 下划线复合标签。
|
| 802 |
+
|
| 803 |
+
贪心最长匹配:优先 4-gram,依次递减到 bigram,匹配到即消耗。
|
| 804 |
+
例: ['beam', 'rifle', 'scope'] → 如果 'beam_rifle' 是标签则合并,
|
| 805 |
+
否则保留原样。
|
| 806 |
+
"""
|
| 807 |
+
tag_set = getattr(self, '_tag_names_set', None)
|
| 808 |
+
if tag_set is None or len(tokens) < 2:
|
| 809 |
+
return tokens
|
| 810 |
+
|
| 811 |
+
result: list[str] = []
|
| 812 |
+
i = 0
|
| 813 |
+
max_w = min(self._EN_MAX_COMPOUND, len(tokens))
|
| 814 |
+
while i < len(tokens):
|
| 815 |
+
merged = False
|
| 816 |
+
for w in range(max_w, 1, -1): # 4, 3, 2
|
| 817 |
+
if i + w > len(tokens):
|
| 818 |
+
continue
|
| 819 |
+
candidate = '_'.join(tokens[i:i + w])
|
| 820 |
+
if candidate in tag_set:
|
| 821 |
+
result.append(candidate)
|
| 822 |
+
i += w
|
| 823 |
+
merged = True
|
| 824 |
+
break
|
| 825 |
+
if not merged:
|
| 826 |
+
result.append(tokens[i])
|
| 827 |
+
i += 1
|
| 828 |
+
return result
|
| 829 |
+
|
| 830 |
+
# ── 查询切分 ──────────────────────────────────────────────────────────
|
| 831 |
+
|
| 832 |
def _smart_split(self, text: str) -> tuple[list[str], list[str]]:
|
| 833 |
+
"""将查询文本拆分为关键词列表,同时返回从句级片段。
|
| 834 |
|
| 835 |
+
先把文本切成交替的 CN-region(含 CJK 字符)与 EN-region(无 CJK);
|
| 836 |
+
中英文用各自的规则处理:
|
| 837 |
+
|
| 838 |
+
1. CN-region 按空格/CJK 标点切出"子句"(segments),中文自然语句里
|
| 839 |
+
这些符号是显式概念边界。子句的 token 切分策略由"整句是否含任何
|
| 840 |
+
分隔符"决定:
|
| 841 |
+
- 整句无任何分隔符 → 视作自然句,jieba 切分;
|
| 842 |
+
- 整句有分隔符 → 用户已标边界,每个短纯 CJK 子句原子保留,
|
| 843 |
+
超过 _ATOMIC_CJK_MAX_LEN 才走 jieba。
|
| 844 |
+
2. EN-region 仅走 _tokenize_en_chunk(停用词过滤 + 复合词合并),
|
| 845 |
+
不产出 segments——英文里空格是词内分隔而非概念边界。
|
| 846 |
|
| 847 |
Returns:
|
| 848 |
+
(tokens, segments):
|
| 849 |
+
- tokens: 处理后的关键词列表
|
| 850 |
+
- segments: CN-region 切出的子句片段;纯英文查询为空列表
|
| 851 |
"""
|
| 852 |
+
user_pieces = [s.strip() for s in re.split(r'[\s\n\r,、;。]+', text) if s.strip()]
|
| 853 |
+
if not user_pieces:
|
| 854 |
return [], []
|
| 855 |
+
has_boundary = len(user_pieces) > 1 # 整句是否含任何用户标注的概念边界
|
| 856 |
|
| 857 |
tokens: list[str] = []
|
| 858 |
+
segments: list[str] = []
|
| 859 |
+
|
| 860 |
+
# 把文本切成交替的 CN-region 与 EN-region。
|
| 861 |
+
# CN-region 允许内部以空格/CJK 标点连接相邻 CJK 块。
|
| 862 |
+
cjk_region = r'[一-龥]+(?:[\s\n\r,、;。]+[一-龥]+)*'
|
| 863 |
+
parts = re.split(f'({cjk_region})', text)
|
| 864 |
+
|
| 865 |
+
for part in parts:
|
| 866 |
+
if not part.strip():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
continue
|
| 868 |
+
|
| 869 |
+
if re.search(r'[一-龥]', part):
|
| 870 |
+
# CN region:产出子句 + tokens
|
| 871 |
+
cn_segs = [s.strip() for s in re.split(r'[\s\n\r,、;。]+', part) if s.strip()]
|
| 872 |
+
for seg in cn_segs:
|
| 873 |
+
segments.append(seg)
|
| 874 |
+
if has_boundary and re.match(r'^[一-龥]+$', seg) and len(seg) <= _ATOMIC_CJK_MAX_LEN:
|
| 875 |
+
tokens.append(seg) # 短 → 原子概念
|
| 876 |
+
else:
|
| 877 |
+
for chunk in re.split(r'([一-龥]+)', seg):
|
| 878 |
+
if not chunk.strip():
|
| 879 |
+
continue
|
| 880 |
+
if re.match(r'[一-龥]+', chunk):
|
| 881 |
+
tokens.extend(jieba.cut(chunk))
|
| 882 |
+
else:
|
| 883 |
+
tokens.extend(self._tokenize_en_chunk(chunk))
|
| 884 |
+
else:
|
| 885 |
+
# EN region:仅 tokenize,不产出子句
|
| 886 |
+
tokens.extend(self._tokenize_en_chunk(part))
|
| 887 |
+
|
| 888 |
return tokens, segments
|
| 889 |
|
| 890 |
# ── 关联推荐 ──────────────────────────────────────────────────────────
|
ui_nicegui.py
CHANGED
|
@@ -136,6 +136,7 @@ class DanbooruSearchUI:
|
|
| 136 |
|
| 137 |
self.full_table_data: list[dict] = []
|
| 138 |
self.current_segments: list[str] = [] # 从句级原始片段,用于区分 chip 颜色
|
|
|
|
| 139 |
self.current_query_str: str = ""
|
| 140 |
self.full_tags_str: str = ""
|
| 141 |
self.full_tags_str_sfw: str = ""
|
|
@@ -1096,6 +1097,7 @@ class DanbooruSearchUI:
|
|
| 1096 |
# ── 分词筛选 ──────────────────────────────────────────────────────────
|
| 1097 |
|
| 1098 |
def _filter_by_source(self, keyword: str):
|
|
|
|
| 1099 |
show_nsfw_val = self.input_nsfw.value
|
| 1100 |
if not keyword or keyword == 'ALL':
|
| 1101 |
filtered = self.full_table_data
|
|
@@ -1218,6 +1220,7 @@ class DanbooruSearchUI:
|
|
| 1218 |
self._refresh_related([], show_nsfw_val)
|
| 1219 |
|
| 1220 |
# 分词筛选 chips
|
|
|
|
| 1221 |
self.keywords_container.clear()
|
| 1222 |
with self.keywords_container:
|
| 1223 |
ui.label('分词筛选:').classes('text-sm text-gray-500 font-bold mr-2')
|
|
@@ -1475,7 +1478,7 @@ class DanbooruSearchUI:
|
|
| 1475 |
f'{group_cn} ({len(tags)} 个标签)',
|
| 1476 |
icon='label',
|
| 1477 |
).classes('w-full').props('dense'):
|
| 1478 |
-
with ui.element('div').classes('w-full grid grid-cols-
|
| 1479 |
for t in tags:
|
| 1480 |
tag = t['tag']
|
| 1481 |
cn_first = t['cn_name'].split(',')[0].strip() if t['cn_name'] else ''
|
|
@@ -1574,7 +1577,8 @@ class DanbooruSearchUI:
|
|
| 1574 |
def on_nsfw_toggle(self, e):
|
| 1575 |
show_nsfw_val = self.input_nsfw.value
|
| 1576 |
|
| 1577 |
-
|
|
|
|
| 1578 |
if not show_nsfw_val:
|
| 1579 |
self.result_table.selected = [r for r in self.result_table.selected if r.get('nsfw') != '1']
|
| 1580 |
self._update_selection_display(None)
|
|
|
|
| 136 |
|
| 137 |
self.full_table_data: list[dict] = []
|
| 138 |
self.current_segments: list[str] = [] # 从句级原始片段,用于区分 chip 颜色
|
| 139 |
+
self.current_filter_keyword: str = 'ALL' # 当前选中的分词筛选 keyword(NSFW 切换时复用)
|
| 140 |
self.current_query_str: str = ""
|
| 141 |
self.full_tags_str: str = ""
|
| 142 |
self.full_tags_str_sfw: str = ""
|
|
|
|
| 1097 |
# ── 分词筛选 ──────────────────────────────────────────────────────────
|
| 1098 |
|
| 1099 |
def _filter_by_source(self, keyword: str):
|
| 1100 |
+
self.current_filter_keyword = keyword if keyword else 'ALL'
|
| 1101 |
show_nsfw_val = self.input_nsfw.value
|
| 1102 |
if not keyword or keyword == 'ALL':
|
| 1103 |
filtered = self.full_table_data
|
|
|
|
| 1220 |
self._refresh_related([], show_nsfw_val)
|
| 1221 |
|
| 1222 |
# 分词筛选 chips
|
| 1223 |
+
self.current_filter_keyword = 'ALL' # 新搜索默认选中"全部"
|
| 1224 |
self.keywords_container.clear()
|
| 1225 |
with self.keywords_container:
|
| 1226 |
ui.label('分词筛选:').classes('text-sm text-gray-500 font-bold mr-2')
|
|
|
|
| 1478 |
f'{group_cn} ({len(tags)} 个标签)',
|
| 1479 |
icon='label',
|
| 1480 |
).classes('w-full').props('dense'):
|
| 1481 |
+
with ui.element('div').classes('w-full grid grid-cols-2 gap-1 p-1').style('max-height: 600px; overflow-y: auto;'):
|
| 1482 |
for t in tags:
|
| 1483 |
tag = t['tag']
|
| 1484 |
cn_first = t['cn_name'].split(',')[0].strip() if t['cn_name'] else ''
|
|
|
|
| 1577 |
def on_nsfw_toggle(self, e):
|
| 1578 |
show_nsfw_val = self.input_nsfw.value
|
| 1579 |
|
| 1580 |
+
# 复用当前分词筛选:同时套用新 NSFW 状态并保持 chip 选中态
|
| 1581 |
+
self._filter_by_source(self.current_filter_keyword)
|
| 1582 |
if not show_nsfw_val:
|
| 1583 |
self.result_table.selected = [r for r in self.result_table.selected if r.get('nsfw') != '1']
|
| 1584 |
self._update_selection_display(None)
|