SAkizuki commited on
Commit
ed91b89
·
verified ·
1 Parent(s): 65b0b93

Auto-sync from GitHub Actions

Browse files
Files changed (3) hide show
  1. .github/workflows/sync_to_hf.yml +9 -9
  2. api_fastapi.py +11 -28
  3. core/engine.py +36 -42
.github/workflows/sync_to_hf.yml CHANGED
@@ -56,14 +56,14 @@ jobs:
56
  export GIT_LFS_SKIP_SMUDGE=1
57
  git clone https://oauth2:${MS_TOKEN}@www.modelscope.cn/studios/SAkizuki/DanbooruSearchOnline.git ms_repo
58
 
59
- # 拷贝根目录下的 .py 文件、依赖文件及 README.md
60
- cp *.py ms_repo/ 2>/dev/null || true
61
- cp requirements.txt ms_repo/ 2>/dev/null || true
62
- cp README.md ms_repo/ 2>/dev/null || true
63
-
64
- # 拷贝 core 目录下的 .py 文件
65
- mkdir -p ms_repo/core
66
- cp core/*.py ms_repo/core/ 2>/dev/null || true
67
 
68
  cd ms_repo
69
  git add .
@@ -74,4 +74,4 @@ jobs:
74
  echo "MS sync done"
75
  else
76
  echo "No code changes to sync to MS."
77
- fi
 
56
  export GIT_LFS_SKIP_SMUDGE=1
57
  git clone https://oauth2:${MS_TOKEN}@www.modelscope.cn/studios/SAkizuki/DanbooruSearchOnline.git ms_repo
58
 
59
+ # 同步仓库代码及配置,同时保留 ModelScope 中的模型和数据文件
60
+ rsync -av \
61
+ --exclude='.git/' \
62
+ --exclude='.github/' \
63
+ --exclude='ms_repo/' \
64
+ --exclude='origin_database/' \
65
+ --exclude='tags_embedding/' \
66
+ ./ ms_repo/
67
 
68
  cd ms_repo
69
  git add .
 
74
  echo "MS sync done"
75
  else
76
  echo "No code changes to sync to MS."
77
+ fi
api_fastapi.py CHANGED
@@ -133,37 +133,20 @@ class ArtistOut(BaseModel):
133
 
134
 
135
  async def _correct_tags(tagger: DanbooruTagger, tags: list[str]) -> tuple[list[str], list[str], dict[str, str]]:
136
- valid_tags: list[str] = []
 
137
  invalid_tags: list[str] = []
138
- for tag in tags:
139
- if tag in tagger._name_to_idx:
140
- valid_tags.append(tag)
141
- else:
142
- invalid_tags.append(tag)
143
-
144
  corrections: dict[str, str] = {}
145
- for bad_tag in invalid_tags:
146
- try:
147
- request = SearchRequest(
148
- query=bad_tag,
149
- top_k=5,
150
- limit=5,
151
- popularity_weight=0.15,
152
- use_segmentation=False,
153
- target_layers=['英文'],
154
- )
155
- response = await tagger.search_async(request)
156
- if response.results:
157
- corrections[bad_tag] = response.results[0].tag
158
- except Exception:
159
- pass
160
 
161
- corrected_tags: list[str] = []
162
- for tag in tags:
163
- if tag in valid_tags:
164
- corrected_tags.append(tag)
165
- elif tag in corrections:
166
- corrected_tags.append(corrections[tag])
 
 
 
167
 
168
  return corrected_tags, invalid_tags, corrections
169
 
 
133
 
134
 
135
  async def _correct_tags(tagger: DanbooruTagger, tags: list[str]) -> tuple[list[str], list[str], dict[str, str]]:
136
+ """Resolve input tags through the deterministic canonical-tag resolver."""
137
+ corrected_tags: list[str] = []
138
  invalid_tags: list[str] = []
 
 
 
 
 
 
139
  corrections: dict[str, str] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
+ for raw_tag in tags:
142
+ resolved = tagger.resolve_tag_name(raw_tag)
143
+ canonical = resolved.get("tag")
144
+ if canonical:
145
+ corrected_tags.append(canonical)
146
+ if canonical != raw_tag:
147
+ corrections[raw_tag] = canonical
148
+ else:
149
+ invalid_tags.append(raw_tag)
150
 
151
  return corrected_tags, invalid_tags, corrections
152
 
core/engine.py CHANGED
@@ -173,6 +173,8 @@ SCHEMA_VERSION = 4 # 升级此值将自动触发全量重建,用于破坏性
173
 
174
  # 用户显式分隔后,纯 CJK 片段超过此长度仍用 jieba 切分(避免长句被当作原子概念)
175
  _ATOMIC_CJK_MAX_LEN = 7
 
 
176
 
177
  # 四路 embedding 层配置: (层名, tensor 属性名, DataFrame 列名)
178
  _LAYER_SPEC: list[tuple[str, str, str]] = [
@@ -1594,8 +1596,8 @@ class DanbooruTagger:
1594
  return result
1595
 
1596
  @staticmethod
1597
- def _normalize_artist_name(name: str) -> str:
1598
- """Normalize user-entered artist names toward Danbooru tag form."""
1599
  text = str(name or "").strip().lower()
1600
  if text.startswith("@"):
1601
  text = text[1:].strip()
@@ -1607,58 +1609,46 @@ class DanbooruTagger:
1607
  def _compact_artist_key(name: str) -> str:
1608
  return re.sub(r"[\W_]+", "", str(name or "").lower())
1609
 
1610
- @staticmethod
1611
- def _edit_distance_at_most_one(left: str, right: str) -> bool:
1612
- if left == right:
1613
- return True
1614
- if abs(len(left) - len(right)) > 1:
1615
- return False
1616
-
1617
- if len(left) == len(right):
1618
- mismatches = 0
1619
- for a, b in zip(left, right):
1620
- if a != b:
1621
- mismatches += 1
1622
- if mismatches > 1:
1623
- return False
1624
- return True
1625
-
1626
- short, long = (left, right) if len(left) < len(right) else (right, left)
1627
- i = j = edits = 0
1628
- while i < len(short) and j < len(long):
1629
- if short[i] == long[j]:
1630
- i += 1
1631
- j += 1
1632
- continue
1633
- edits += 1
1634
- if edits > 1:
1635
- return False
1636
- j += 1
1637
- return True
1638
-
1639
  def search_artist_rows(
1640
  self, query: str, limit: int = 20, show_nsfw: bool = True,
1641
  ) -> list[TagResult]:
1642
- """Return artist rows whose normalized name is within edit distance 1."""
1643
- normalized = self._normalize_artist_name(query)
1644
  compact_query = self._compact_artist_key(normalized)
1645
  if not compact_query:
1646
  return []
1647
 
1648
- matches: list[TagResult] = []
1649
  for artist in sorted(self._artist_top_tags.keys()):
1650
- if not self._edit_distance_at_most_one(compact_query, self._compact_artist_key(artist)):
 
 
 
 
 
1651
  continue
 
 
 
 
 
 
 
 
 
 
 
1652
  top_tags = self.get_artist_top_tags(
1653
  [artist], top_n=10, show_nsfw=show_nsfw,
1654
  ).get(artist, [])
 
1655
  matches.append(TagResult(
1656
  tag=artist,
1657
  cn_name="画师标签",
1658
  category="Artist",
1659
  nsfw="0",
1660
- final_score=1.0,
1661
- semantic_score=1.0,
1662
  count=int(self._artist_post_count.get(artist, 0)),
1663
  source=query,
1664
  layer="artist",
@@ -1666,13 +1656,12 @@ class DanbooruTagger:
1666
  artist_top_tags=top_tags,
1667
  ))
1668
 
1669
- matches.sort(key=lambda r: (r.count, r.tag), reverse=True)
1670
- return matches[:limit]
1671
 
1672
  def resolve_artist_name(self, artist_name: str) -> dict[str, Any]:
1673
  """Resolve a user-entered artist name to the artist co-occurrence index."""
1674
  artists = set(self._artist_top_tags.keys())
1675
- normalized = self._normalize_artist_name(artist_name)
1676
 
1677
  if artist_name in artists:
1678
  return {
@@ -1706,7 +1695,12 @@ class DanbooruTagger:
1706
  "candidates": sorted(compact_matches)[:10],
1707
  }
1708
 
1709
- close = difflib.get_close_matches(normalized, sorted(artists), n=5, cutoff=0.78)
 
 
 
 
 
1710
  if len(close) == 1:
1711
  return {
1712
  "artist": close[0],
@@ -1722,7 +1716,7 @@ class DanbooruTagger:
1722
  def resolve_tag_name(self, tag_name: str) -> dict[str, Any]:
1723
  """Resolve a user-entered tag name to the canonical tag index without semantic search."""
1724
  tags = set(self._name_to_idx.keys())
1725
- normalized = self._normalize_artist_name(tag_name)
1726
 
1727
  if tag_name in tags:
1728
  return {
 
173
 
174
  # 用户显式分隔后,纯 CJK 片段超过此长度仍用 jieba 切分(避免长句被当作原子概念)
175
  _ATOMIC_CJK_MAX_LEN = 7
176
+ _ARTIST_FUZZY_CUTOFF = 0.78
177
+ _ARTIST_SEARCH_MAX_RESULTS = 3
178
 
179
  # 四路 embedding 层配置: (层名, tensor 属性名, DataFrame 列名)
180
  _LAYER_SPEC: list[tuple[str, str, str]] = [
 
1596
  return result
1597
 
1598
  @staticmethod
1599
+ def _normalize_danbooru_name(name: str) -> str:
1600
+ """Normalize user-entered tag or artist names toward Danbooru form."""
1601
  text = str(name or "").strip().lower()
1602
  if text.startswith("@"):
1603
  text = text[1:].strip()
 
1609
  def _compact_artist_key(name: str) -> str:
1610
  return re.sub(r"[\W_]+", "", str(name or "").lower())
1611
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1612
  def search_artist_rows(
1613
  self, query: str, limit: int = 20, show_nsfw: bool = True,
1614
  ) -> list[TagResult]:
1615
+ """Return up to three artist rows ranked by compact-name similarity."""
1616
+ normalized = self._normalize_danbooru_name(query)
1617
  compact_query = self._compact_artist_key(normalized)
1618
  if not compact_query:
1619
  return []
1620
 
1621
+ ranked_matches: list[tuple[float, int, str]] = []
1622
  for artist in sorted(self._artist_top_tags.keys()):
1623
+ similarity = difflib.SequenceMatcher(
1624
+ None,
1625
+ compact_query,
1626
+ self._compact_artist_key(artist),
1627
+ ).ratio()
1628
+ if similarity < _ARTIST_FUZZY_CUTOFF:
1629
  continue
1630
+ ranked_matches.append((
1631
+ float(similarity),
1632
+ int(self._artist_post_count.get(artist, 0)),
1633
+ artist,
1634
+ ))
1635
+
1636
+ ranked_matches.sort(key=lambda item: (-item[0], -item[1], item[2]))
1637
+ max_results = max(0, min(int(limit), _ARTIST_SEARCH_MAX_RESULTS))
1638
+
1639
+ matches: list[TagResult] = []
1640
+ for similarity, _post_count, artist in ranked_matches[:max_results]:
1641
  top_tags = self.get_artist_top_tags(
1642
  [artist], top_n=10, show_nsfw=show_nsfw,
1643
  ).get(artist, [])
1644
+ score = round(similarity, 4)
1645
  matches.append(TagResult(
1646
  tag=artist,
1647
  cn_name="画师标签",
1648
  category="Artist",
1649
  nsfw="0",
1650
+ final_score=score,
1651
+ semantic_score=score,
1652
  count=int(self._artist_post_count.get(artist, 0)),
1653
  source=query,
1654
  layer="artist",
 
1656
  artist_top_tags=top_tags,
1657
  ))
1658
 
1659
+ return matches
 
1660
 
1661
  def resolve_artist_name(self, artist_name: str) -> dict[str, Any]:
1662
  """Resolve a user-entered artist name to the artist co-occurrence index."""
1663
  artists = set(self._artist_top_tags.keys())
1664
+ normalized = self._normalize_danbooru_name(artist_name)
1665
 
1666
  if artist_name in artists:
1667
  return {
 
1695
  "candidates": sorted(compact_matches)[:10],
1696
  }
1697
 
1698
+ close = difflib.get_close_matches(
1699
+ normalized,
1700
+ sorted(artists),
1701
+ n=5,
1702
+ cutoff=_ARTIST_FUZZY_CUTOFF,
1703
+ )
1704
  if len(close) == 1:
1705
  return {
1706
  "artist": close[0],
 
1716
  def resolve_tag_name(self, tag_name: str) -> dict[str, Any]:
1717
  """Resolve a user-entered tag name to the canonical tag index without semantic search."""
1718
  tags = set(self._name_to_idx.keys())
1719
+ normalized = self._normalize_danbooru_name(tag_name)
1720
 
1721
  if tag_name in tags:
1722
  return {