Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Auto-sync from GitHub Actions
Browse files- .github/workflows/sync_to_hf.yml +9 -9
- api_fastapi.py +11 -28
- 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 |
-
#
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1598 |
-
"""Normalize user-entered artist names toward Danbooru
|
| 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
|
| 1643 |
-
normalized = self.
|
| 1644 |
compact_query = self._compact_artist_key(normalized)
|
| 1645 |
if not compact_query:
|
| 1646 |
return []
|
| 1647 |
|
| 1648 |
-
|
| 1649 |
for artist in sorted(self._artist_top_tags.keys()):
|
| 1650 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 1661 |
-
semantic_score=
|
| 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 |
-
|
| 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.
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 {
|