SAkizuki commited on
Commit
7043411
·
verified ·
1 Parent(s): 549b11b

Auto-sync from GitHub Actions

Browse files
Files changed (2) hide show
  1. core/engine.py +124 -53
  2. 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
- 1. 首先按分隔符切分——空格、换行、中文逗号/顿号/分号/句号均视为
761
- 用户显式指定的概念边界(中文自然语句不使用这些符号来分隔概念)。
762
- 2. 若只有一个片段无显式分隔),完全走原有 jieba 逻辑。
763
- 若有多片段,每个纯 CJK 片段按长度决定:
764
- - ≤ _ATOMIC_CJK_MAX_LEN 字 → 原子概念,直接保留
765
- - > _ATOMIC_CJK_MAX_LEN jieba 切分(长短语/短句)
766
- 混合文本(含点/英文)片段始终走有逻辑。
 
 
 
767
 
768
  Returns:
769
- (tokens, raw_segments):
770
- - tokens: 处理后的关键词列表(原子概念或 jieba 切分结果)
771
- - raw_segments: 分隔符分后原始片段(未经 jieba),用于多粒度查询
772
  """
773
- segments = [s.strip() for s in re.split(r'[\s\n\r,、;。]+', text) if s.strip()]
774
- if not segments:
775
  return [], []
 
776
 
777
  tokens: list[str] = []
778
- # 单一片段 无显式分隔,完全走原有 jieba 逻辑
779
- if len(segments) == 1:
780
- segment = segments[0]
781
- for chunk in re.split(r'([一-龥]+)', segment):
782
- if not chunk.strip():
783
- continue
784
- if re.match(r'[一-龥]+', chunk):
785
- tokens.extend(jieba.cut(chunk))
786
- else:
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
- # 混合文本 → 原有 jieba 切分逻辑
805
- for chunk in re.split(r'([一-龥]+)', segment):
806
- if not chunk.strip():
807
- continue
808
- if re.match(r'[一-龥]+', chunk):
809
- tokens.extend(jieba.cut(chunk))
810
- else:
811
- cleaned = re.sub(r'[,()\[\]{}:]', ' ', chunk)
812
- for part in cleaned.split():
813
- try:
814
- float(part)
815
- except ValueError:
816
- tokens.append(part)
 
 
 
 
 
 
 
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-3 gap-1 p-1').style('max-height: 600px; overflow-y: auto;'):
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
- self.result_table.rows = apply_nsfw_filter(self.full_table_data, show_nsfw_val)
 
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)