Skip to content

Commit 09b180e

Browse files
authored
Merge pull request #852 from zanllp/feat/auto-tagging
feat: implement auto-tagging
2 parents 2e560e9 + d8ed522 commit 09b180e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+568
-124
lines changed

javascript/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Promise.resolve().then(async () => {
1313
<link rel="icon" href="/favicon.ico" />
1414
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
1515
<title>Infinite Image Browsing</title>
16-
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-a7581aec.js"></script>
16+
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-6c47ee34.js"></script>
1717
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-a44325c7.css">
1818
</head>
1919

scripts/iib/api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,10 @@ class AppFeSettingReq(BaseModel):
313313
async def app_fe_setting(req: AppFeSettingReq):
314314
conn = DataBase.get_conn()
315315
GlobalSetting.save_setting(conn, req.name, req.value)
316+
# 如果更新的是自动标签规则,重新加载
317+
if req.name == "auto_tag_rules":
318+
from scripts.iib.auto_tag import AutoTagMatcher
319+
AutoTagMatcher.reload_rules(conn)
316320

317321
class AppFeSettingDelReq(BaseModel):
318322
name: str

scripts/iib/auto_tag.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from typing import List, Dict, Any, Optional
2+
import re
3+
from scripts.iib.db.datamodel import GlobalSetting, Tag, ImageTag, DataBase
4+
from scripts.iib.logger import logger
5+
from scripts.iib.parsers.model import ImageGenerationParams
6+
7+
class AutoTagMatcher:
8+
_instance: Optional['AutoTagMatcher'] = None
9+
10+
def __init__(self, conn):
11+
self.conn = conn
12+
self.rules = self.load_rules()
13+
14+
@classmethod
15+
def get_instance(cls, conn) -> 'AutoTagMatcher':
16+
"""获取全局单例实例"""
17+
if cls._instance is None:
18+
cls._instance = cls(conn)
19+
else:
20+
# 更新连接
21+
cls._instance.conn = conn
22+
return cls._instance
23+
24+
@classmethod
25+
def reload_rules(cls, conn) -> None:
26+
"""重新加载规则"""
27+
if cls._instance is not None:
28+
cls._instance.conn = conn
29+
cls._instance.rules = cls._instance.load_rules()
30+
31+
def load_rules(self) -> List[Dict[str, Any]]:
32+
try:
33+
setting = GlobalSetting.get_setting(self.conn, "auto_tag_rules")
34+
print("Loaded auto tag rules:", setting)
35+
if setting:
36+
return setting
37+
except Exception as e:
38+
logger.error(f"Error loading auto tag rules: {e}")
39+
return []
40+
41+
def match(self, params: ImageGenerationParams, rule: Dict[str, Any]) -> bool:
42+
# params is ImageGenerationParams
43+
# rule structure: { "tag": "TagName", "filters": [...] }
44+
# filter structure: { "field": "pos_prompt", "operator": "contains", "value": "foo" }
45+
46+
filters = rule.get("filters", [])
47+
if not filters:
48+
return False
49+
50+
for filter in filters:
51+
field = filter.get("field")
52+
operator = filter.get("operator")
53+
value = filter.get("value")
54+
55+
target_value = ""
56+
if field == "pos_prompt":
57+
target_value = ",".join(params.pos_prompt) if isinstance(params.pos_prompt, list) else str(params.pos_prompt)
58+
elif field == "neg_prompt":
59+
# Assuming neg_prompt might be in meta or extra, but params usually has pos_prompt.
60+
# Let's check where negative prompt is usually stored.
61+
# In api.py: params = parse_generation_parameters(content)
62+
# parse_generation_parameters returns dict with "meta", "pos_prompt".
63+
# "meta" usually contains "Negative prompt".
64+
target_value = params.meta.get("Negative prompt", "")
65+
else:
66+
# Try to find in meta
67+
target_value = params.meta.get(field, "")
68+
if not target_value and field in params.extra:
69+
target_value = params.extra.get(field, "")
70+
71+
target_value = str(target_value)
72+
73+
if operator == "contains":
74+
if value.lower() not in target_value.lower():
75+
return False
76+
elif operator == "equals":
77+
if value.lower() != target_value.lower():
78+
return False
79+
elif operator == "regex":
80+
try:
81+
if not re.search(value, target_value, re.IGNORECASE):
82+
return False
83+
except:
84+
return False
85+
# Add more operators if needed
86+
87+
return True
88+
89+
def apply(self, img_id: int, params: Any):
90+
if not self.rules:
91+
return
92+
93+
for rule in self.rules:
94+
try:
95+
if self.match(params, rule):
96+
tag_name = rule.get("tag")
97+
if tag_name:
98+
tag = Tag.get_or_create(self.conn, tag_name, "custom")
99+
ImageTag(img_id, tag.id).save_or_ignore(self.conn)
100+
except Exception as e:
101+
logger.error(f"Error applying auto tag rule {rule}: {e}")

scripts/iib/db/datamodel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,7 @@ def get_cached_media_files(cls, conn, folder_path):
908908
else:
909909
return []
910910

911-
911+
# Global settings storage, also use as key-value store
912912
class GlobalSetting:
913913
@classmethod
914914
def create_table(cls, conn):

scripts/iib/db/update_image_data.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from scripts.iib.logger import logger
1818
from scripts.iib.parsers.index import parse_image_info
1919
from scripts.iib.plugin import plugin_inst_map
20+
from scripts.iib.auto_tag import AutoTagMatcher
2021

2122
# 定义一个函数来获取图片文件的EXIF数据
2223
def get_exif_data(file_path):
@@ -222,4 +223,6 @@ def build_single_img_idx(conn, file_path, is_rebuild, safe_save_img_tag):
222223
safe_save_img_tag(ImageTag(img.id, tag.id))
223224
for k in pos:
224225
tag = Tag.get_or_create(conn, k, "pos")
225-
safe_save_img_tag(ImageTag(img.id, tag.id))
226+
safe_save_img_tag(ImageTag(img.id, tag.id))
227+
228+
AutoTagMatcher.get_instance(conn).apply(img.id, parsed_params)

scripts/iib/parsers/comfyui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def parse(clz, img, file_path):
3232
params = get_comfyui_exif_data(img)
3333
info = comfyui_exif_data_to_str(params)
3434
except Exception as e:
35-
logger.error("parse comfyui image failed. prompt:")
35+
logger.error("parse comfyui image failed. prompt:", exc_info=e)
3636
logger.error(img.info.get("prompt"))
3737
return ImageGenerationInfo(
3838
params=ImageGenerationParams(

scripts/iib/tool.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -460,11 +460,20 @@ def get_comfyui_exif_data(img: Image):
460460
meta["Model"] = data[KSampler_entry["model"][0]]["inputs"].get("ckpt_name")
461461
meta["Source Identifier"] = "ComfyUI"
462462
def get_text_from_clip(idx: str) :
463-
inputs = data[idx]["inputs"]
464-
text = inputs["text"] if "text" in inputs else inputs["t5xxl"]
465-
if isinstance(text, list): # type:CLIPTextEncode (NSP) mode:Wildcards
466-
text = data[text[0]]["inputs"]["text"]
467-
return text.strip()
463+
try:
464+
inputs = data[idx]["inputs"]
465+
if "text" in inputs:
466+
text = inputs["text"]
467+
elif "t5xxl" in inputs:
468+
text = inputs["t5xxl"]
469+
else:
470+
return ""
471+
if isinstance(text, list): # type:CLIPTextEncode (NSP) mode:Wildcards
472+
text = data[text[0]]["inputs"]["text"]
473+
return text.strip()
474+
except Exception as e:
475+
print(e)
476+
return ""
468477

469478
in_node = data[str(KSampler_entry["positive"][0])]
470479
if in_node["class_type"] != "FluxGuidance":
Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)