V2 io.ComfyNode wrapper for deepghs/imgutils — anime image analysis, tagging, detection, pose estimation, and segmentation. 6 nodes: - ImgUtilsValidate — safety/NSFW/content classification (18 ops) - ImgUtilsDescribe — WD14/DeepDanbooru/OCR/detection (17 ops) - ImgUtilsTransform — edge detection/restoration/upscaling (11 ops) - ImgUtilsCompare — CCIP/LPIPS image comparison (3 ops) - ImgUtilsPose — DWPose keypoint detection + visualization - ImgUtilsSegment — isnetis character segmentation (mask + RGBA)
208 lines
7.0 KiB
Python
208 lines
7.0 KiB
Python
"""
|
|
Node 7: ImgUtilsTags — "Process tag strings"
|
|
|
|
Text-in/text-out tag processing using imgutils.tagging utilities.
|
|
Parses comma-separated or newline-separated tag strings (with optional scores),
|
|
applies filtering/sorting/formatting operations, and outputs processed tags.
|
|
|
|
Dropdown options (9):
|
|
drop_blacklisted -> imgutils.tagging.drop_blacklisted_tags()
|
|
drop_basic_character -> imgutils.tagging.drop_basic_character_tags()
|
|
drop_overlap -> imgutils.tagging.drop_overlap_tags()
|
|
sort_original -> imgutils.tagging.sort_tags(mode='original')
|
|
sort_score -> imgutils.tagging.sort_tags(mode='score')
|
|
sort_shuffle -> imgutils.tagging.sort_tags(mode='shuffle')
|
|
spaces_to_underscores -> imgutils.tagging.add_underline()
|
|
underscores_to_spaces -> imgutils.tagging.remove_underline()
|
|
format_text -> imgutils.tagging.tags_to_text()
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from comfy_api.latest import io
|
|
|
|
|
|
class ImgUtilsTags(io.ComfyNode):
|
|
"""Process and transform tag strings using imgutils.tagging utilities."""
|
|
|
|
DROPDOWN_OPTIONS = [
|
|
"drop_blacklisted",
|
|
"drop_basic_character",
|
|
"drop_overlap",
|
|
"sort_original",
|
|
"sort_score",
|
|
"sort_shuffle",
|
|
"spaces_to_underscores",
|
|
"underscores_to_spaces",
|
|
"format_text",
|
|
]
|
|
|
|
@classmethod
|
|
def define_schema(cls) -> io.Schema:
|
|
return io.Schema(
|
|
node_id="ImgUtilsTags",
|
|
display_name="Imgutils Tags",
|
|
category="imgutils/tagging",
|
|
description=(
|
|
"Process tag strings — filter blacklisted/basic-character/overlapping tags, "
|
|
"sort by original order or score, convert between underscore/space formats, "
|
|
"and format as training-ready text."
|
|
),
|
|
search_aliases=[
|
|
"tags", "tagging", "filter", "blacklist",
|
|
"sort", "format", "underscore", "danbooru",
|
|
"wd14", "clean", "deduplicate",
|
|
],
|
|
inputs=[
|
|
io.String.Input(
|
|
"tags",
|
|
multiline=True,
|
|
tooltip="Tags to process. Comma-separated or newline-separated. "
|
|
"Optionally include scores as 'tag: 0.95'.",
|
|
),
|
|
io.Combo.Input(
|
|
"operation",
|
|
options=cls.DROPDOWN_OPTIONS,
|
|
default="drop_blacklisted",
|
|
tooltip="Processing operation — filtering, sorting, format conversion, or text formatting.",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.String.Output(display_name="tags"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, tags, operation) -> io.NodeOutput:
|
|
"""
|
|
Parse tag string, apply the selected operation, and return processed output.
|
|
|
|
Args:
|
|
tags: input tag string (comma or newline separated, optional scores)
|
|
operation: dropdown selection
|
|
|
|
Returns:
|
|
NodeOutput with processed tag string
|
|
"""
|
|
parsed, is_dict = _parse_tags(tags)
|
|
|
|
if operation == "drop_blacklisted":
|
|
from imgutils.tagging import drop_blacklisted_tags
|
|
|
|
result = drop_blacklisted_tags(parsed)
|
|
return io.NodeOutput(_format_tags(result))
|
|
|
|
elif operation == "drop_basic_character":
|
|
from imgutils.tagging import drop_basic_character_tags
|
|
|
|
result = drop_basic_character_tags(parsed)
|
|
return io.NodeOutput(_format_tags(result))
|
|
|
|
elif operation == "drop_overlap":
|
|
from imgutils.tagging import drop_overlap_tags
|
|
|
|
result = drop_overlap_tags(parsed)
|
|
return io.NodeOutput(_format_tags(result))
|
|
|
|
elif operation == "sort_original":
|
|
from imgutils.tagging import sort_tags
|
|
|
|
result = sort_tags(parsed, mode="original")
|
|
return io.NodeOutput(_format_tags(result))
|
|
|
|
elif operation == "sort_score":
|
|
from imgutils.tagging import sort_tags
|
|
|
|
result = sort_tags(parsed, mode="score")
|
|
return io.NodeOutput(_format_tags(result))
|
|
|
|
elif operation == "sort_shuffle":
|
|
from imgutils.tagging import sort_tags
|
|
|
|
result = sort_tags(parsed, mode="shuffle")
|
|
return io.NodeOutput(_format_tags(result))
|
|
|
|
elif operation == "spaces_to_underscores":
|
|
from imgutils.tagging import add_underline
|
|
|
|
if is_dict:
|
|
result = {add_underline(k): v for k, v in parsed.items()}
|
|
else:
|
|
result = [add_underline(t) for t in parsed]
|
|
return io.NodeOutput(_format_tags(result))
|
|
|
|
elif operation == "underscores_to_spaces":
|
|
from imgutils.tagging import remove_underline
|
|
|
|
if is_dict:
|
|
result = {remove_underline(k): v for k, v in parsed.items()}
|
|
else:
|
|
result = [remove_underline(t) for t in parsed]
|
|
return io.NodeOutput(_format_tags(result))
|
|
|
|
elif operation == "format_text":
|
|
from imgutils.tagging import tags_to_text
|
|
|
|
if not is_dict:
|
|
# Convert list to dict with uniform scores for formatting
|
|
parsed = {t: 1.0 for t in parsed}
|
|
result = tags_to_text(parsed, use_spaces=False, use_escape=True, include_score=False)
|
|
return io.NodeOutput(result)
|
|
|
|
else:
|
|
return io.NodeOutput(tags)
|
|
|
|
|
|
def _parse_tags(text: str) -> tuple[list[str] | dict[str, float], bool]:
|
|
"""
|
|
Parse a tag string into either a list of strings or a dict of {tag: score}.
|
|
|
|
Detection logic:
|
|
- Split by comma or newline
|
|
- If tokens contain ':' followed by a parseable float → dict mode
|
|
- Otherwise → list mode
|
|
"""
|
|
# Split by comma or newline, strip whitespace, filter empty
|
|
raw_tokens = [t.strip() for t in text.replace("\n", ",").split(",")]
|
|
tokens = [t for t in raw_tokens if t]
|
|
|
|
if not tokens:
|
|
return [], False
|
|
|
|
# Check if tokens look like scored tags ("tag: 0.95")
|
|
scored = False
|
|
for token in tokens:
|
|
if ":" in token:
|
|
parts = token.rsplit(":", 1)
|
|
try:
|
|
float(parts[1].strip())
|
|
scored = True
|
|
break
|
|
except ValueError:
|
|
pass
|
|
|
|
if scored:
|
|
result = {}
|
|
for token in tokens:
|
|
if ":" in token:
|
|
parts = token.rsplit(":", 1)
|
|
try:
|
|
score = float(parts[1].strip())
|
|
tag = parts[0].strip()
|
|
result[tag] = score
|
|
except ValueError:
|
|
result[token.strip()] = 1.0
|
|
else:
|
|
result[token.strip()] = 1.0
|
|
return result, True
|
|
else:
|
|
return tokens, False
|
|
|
|
|
|
def _format_tags(tags) -> str:
|
|
"""Format parsed tags back to a string."""
|
|
if isinstance(tags, dict):
|
|
return ", ".join(f"{k}: {v:.4f}" for k, v in tags.items())
|
|
else:
|
|
return ", ".join(tags)
|