Repository: deepghs/imgutils (language: python)

Issue:
dev(narugo): add more processing method for tags



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/waifu2x.yml b/.github/workflows/waifu2x.yml
new file mode 100644
index 00000000000..d338ea47c89
--- /dev/null
+++ b/.github/workflows/waifu2x.yml
@@ -0,0 +1,44 @@
+name: Sync Waifu2x Models
+
+on:
+  #  push:
+  workflow_dispatch:
+  schedule:
+    - cron: '30 16 * * *'
+
+jobs:
+  sync:
+    name: Sync Waifu2x ONNX
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os:
+          - 'ubuntu-latest'
+        python-version:
+          - '3.8'
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 20
+      - name: Set up python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Set up python dependences
+        run: |
+          pip install --upgrade pip
+          pip install --upgrade flake8 setuptools wheel twine
+          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+          if [ -f requirements-build.txt ]; then pip install -r requirements-build.txt; fi
+          if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi
+          if [ -f requirements-test.txt ]; then pip install -r requirements-zoo.txt; fi
+          pip install --upgrade build
+      - name: Sync Models
+        env:
+          HF_TOKEN: ${{ secrets.HF_TOKEN }}
+          GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
+        run: |
+          python -m zoo.waifu2x sync
diff --git a/docs/source/api_doc/tagging/blacklist.rst b/docs/source/api_doc/tagging/blacklist.rst
new file mode 100644
index 00000000000..d7592061a13
--- /dev/null
+++ b/docs/source/api_doc/tagging/blacklist.rst
@@ -0,0 +1,22 @@
+imgutils.tagging.blacklist
+====================================
+
+.. currentmodule:: imgutils.tagging.blacklist
+
+.. automodule:: imgutils.tagging.blacklist
+
+
+is_blacklisted
+-------------------------------
+
+.. autofunction:: is_blacklisted
+
+
+
+drop_blacklisted_tags
+-------------------------------
+
+.. autofunction:: drop_blacklisted_tags
+
+
+
diff --git a/docs/source/api_doc/tagging/character.rst b/docs/source/api_doc/tagging/character.rst
new file mode 100644
index 00000000000..968fc50f755
--- /dev/null
+++ b/docs/source/api_doc/tagging/character.rst
@@ -0,0 +1,22 @@
+imgutils.tagging.character
+====================================
+
+.. currentmodule:: imgutils.tagging.character
+
+.. automodule:: imgutils.tagging.character
+
+
+is_basic_character_tag
+-------------------------------
+
+.. autofunction:: is_basic_character_tag
+
+
+
+drop_basic_character_tags
+-------------------------------
+
+.. autofunction:: drop_basic_character_tags
+
+
+
diff --git a/docs/source/api_doc/tagging/index.rst b/docs/source/api_doc/tagging/index.rst
index 989a0928135..723ecb9d971 100644
--- a/docs/source/api_doc/tagging/index.rst
+++ b/docs/source/api_doc/tagging/index.rst
@@ -14,3 +14,7 @@ imgutils.tagging
     deepdanbooru
     format
     overlap
+    blacklist
+    character
+    order
+
diff --git a/docs/source/api_doc/tagging/order.rst b/docs/source/api_doc/tagging/order.rst
new file mode 100644
index 00000000000..1dd0042c13a
--- /dev/null
+++ b/docs/source/api_doc/tagging/order.rst
@@ -0,0 +1,14 @@
+imgutils.tagging.order
+====================================
+
+.. currentmodule:: imgutils.tagging.order
+
+.. automodule:: imgutils.tagging.order
+
+
+sort_tags
+------------------------
+
+.. autofunction:: sort_tags
+
+
diff --git a/imgutils/config/meta.py b/imgutils/config/meta.py
index 2526cbe12c5..030ddc74ccb 100644
--- a/imgutils/config/meta.py
+++ b/imgutils/config/meta.py
@@ -7,7 +7,7 @@
 __TITLE__ = 'imgutils'
 
 #: Version of this project.
-__VERSION__ = '0.2.7'
+__VERSION__ = '0.2.8'
 
 #: Short description of the project, will be included in ``setup.py``.
 __DESCRIPTION__ = 'A convenient and user-friendly anime-style image data processing library that integrates ' \
diff --git a/imgutils/tagging/__init__.py b/imgutils/tagging/__init__.py
index 997e7e4f596..27c7adadb67 100644
--- a/imgutils/tagging/__init__.py
+++ b/imgutils/tagging/__init__.py
@@ -8,8 +8,11 @@
         :align: center
 
 """
+from .blacklist import is_blacklisted, drop_blacklisted_tags
+from .character import is_basic_character_tag, drop_basic_character_tags
 from .deepdanbooru import get_deepdanbooru_tags
 from .format import tags_to_text
 from .mldanbooru import get_mldanbooru_tags
+from .order import sort_tags
 from .overlap import drop_overlap_tags, drop_overlaps_for_dict
 from .wd14 import get_wd14_tags
diff --git a/imgutils/tagging/blacklist.py b/imgutils/tagging/blacklist.py
new file mode 100644
index 00000000000..df7e6df1c04
--- /dev/null
+++ b/imgutils/tagging/blacklist.py
@@ -0,0 +1,119 @@
+"""
+Overview:
+    Detect and drop some blacklisted tags, which are listed `here <https://huggingface.co/datasets/alea31415/tag_filtering/blob/main/blacklist_tags.txt>`_.
+"""
+from functools import lru_cache
+from typing import Union, List, Mapping, Set, Optional
+
+from huggingface_hub import hf_hub_download
+
+
+@lru_cache()
+def _load_online_blacklist() -> List[str]:
+    """
+    Load the online blacklist tags from the specified dataset repository.
+
+    :return: List of blacklisted tags.
+    :rtype: List[str]
+    """
+    with open(hf_hub_download(
+            'alea31415/tag_filtering',
+            'blacklist_tags.txt',
+            repo_type='dataset',
+    ), 'r') as f:
+        return [line.strip() for line in f if line.strip()]
+
+
+def _is_blacklisted(tag: str, blacklist: Set[str]):
+    """
+    Check if a tag is blacklisted.
+
+    :param tag: Tag to be checked.
+    :type tag: str
+    :param blacklist: Set of blacklisted tags.
+    :type blacklist: Set[str]
+    :return: True if the tag is blacklisted, False otherwise.
+    :rtype: bool
+    """
+    return (tag in blacklist or
+            tag.replace('_', ' ') in blacklist or
+            tag.replace(' ', '_') in blacklist)
+
+
+@lru_cache()
+def _online_blacklist_set() -> Set[str]:
+    """
+    Get the online blacklist as a set.
+
+    :return: Set of blacklisted tags.
+    :rtype: Set[str]
+    """
+    return set(_load_online_blacklist())
+
+
+def is_blacklisted(tags: str):
+    """
+    Check if any of the given tags are blacklisted.
+
+    :param tags: Tags to be checked.
+    :type tags: str
+    :return: True if any tag is blacklisted, False otherwise.
+    :rtype: bool
+
+    Examples::
+        >>> from imgutils.tagging import is_blacklisted
+        >>>
+        >>> is_blacklisted('cosplay')
+        True
+        >>> is_blacklisted('no_eyewear')
+        True
+        >>> is_blacklisted('no eyewear')  # span does not matter
+        True
+        >>> is_blacklisted('red_hair')
+        False
+    """
+    return _is_blacklisted(tags, _online_blacklist_set())
+
+
+def drop_blacklisted_tags(tags: Union[List[str], Mapping[str, float]],
+                          use_presets: bool = True, custom_blacklist: Optional[List[str]] = None) \
+        -> Union[List[str], Mapping[str, float]]:
+    """
+    Drop blacklisted tags from the given list or mapping of tags.
+
+    :param tags: List or mapping of tags to be filtered.
+    :type tags: Union[List[str], Mapping[str, float]]
+    :param use_presets: Whether to use the online blacklist presets, defaults to True.
+    :type use_presets: bool, optional
+    :param custom_blacklist: Custom blacklist to be used, defaults to None.
+    :type custom_blacklist: Optional[List[str]], optional
+    :return: Filtered list or mapping of tags without the blacklisted ones.
+    :rtype: Union[List[str], Mapping[str, float]]
+    :raises TypeError: If the input tags are neither a list nor a dictionary.
+
+    Examples::
+        >>> from imgutils.tagging import drop_blacklisted_tags
+        >>>
+        >>> drop_blacklisted_tags({
+        ...     'solo': 1.0, '1girl': 0.95,
+        ...     'cosplay': 0.7, 'no_eyewear': 0.6,
+        ... })
+        {'solo': 1.0, '1girl': 0.95}
+        >>> drop_blacklisted_tags(['solo', '1girl', 'cosplay', 'no_eyewear'])
+        ['solo', '1girl']
+    """
+    blacklist = []
+    if use_presets:
+        blacklist.extend(_load_online_blacklist())
+    blacklist.extend(custom_blacklist or [])
+
+    blacklist = set(tag.replace(' ', '_') for tag in blacklist)
+    blacklist_update = set(tag.replace('_', ' ') for tag in blacklist)
+    blacklist.update(blacklist_update)
+
+    if isinstance(tags, dict):
+        return {tag: value for tag, value in tags.items() if not _is_blacklisted(tag, blacklist)}
+    elif isinstance(tags, list):
+        return [tag for tag in tags if not _is_blacklisted(tag, blacklist)]
+    else:
+        raise TypeError(f"Unsupported types of tags, dict or list expected, but {tags!r} found.")
diff --git a/imgutils/tagging/character.py b/imgutils/tagging/character.py
new file mode 100644
index 00000000000..2c3a0064176
--- /dev/null
+++ b/imgutils/tagging/character.py
@@ -0,0 +1,161 @@
+"""
+Overview:
+    Detect and drop character-related basic tags.
+"""
+import re
+from typing import Union, List, Mapping
+
+from hbutils.string import singular_form, plural_form
+
+_CHAR_WHITELIST = [
+    'drill', 'pubic_hair', 'closed_eyes', 'half-closed_eyes', 'empty_eyes'
+]
+_CHAR_SUFFIXES = [
+    'eyes', 'skin', 'hair', 'bun', 'bangs', 'cut', 'sidelocks',
+    'twintails', 'braid', 'braids', 'afro', 'ahoge', 'drill',
+    'drills', 'bald', 'dreadlocks', 'side up', 'ponytail', 'updo',
+    'beard', 'mustache', 'pointy ears', 'ear', 'horn',
+]
+_CHAR_PREFIXES = [
+    'hair over', 'hair between'
+]
+
+
+def _split_to_words(text: str) -> List[str]:
+    """
+    Split a string into words and return them in lowercase.
+
+    :param text: The input text to split.
+    :type text: str
+    :return: List of lowercase words.
+    :rtype: List[str]
+    """
+    return [word.lower() for word in re.split(r'[\W_]+', text) if word]
+
+
+def _match_suffix(tag: str, suffix: str):
+    """
+    Check if a tag matches a given suffix.
+
+    :param tag: The tag to check.
+    :type tag: str
+    :param suffix: The suffix to match.
+    :type suffix: str
+    :return: True if the tag matches the suffix, False otherwise.
+    :rtype: bool
+    """
+    tag_words = _split_to_words(tag)
+    suffix_words = _split_to_words(suffix)
+    all_suffixes = [suffix_words]
+    all_suffixes.append([*suffix_words[:-1], singular_form(suffix_words[0])])
+    all_suffixes.append([*suffix_words[:-1], plural_form(suffix_words[0])])
+
+    for suf in all_suffixes:
+        if tag_words[-len(suf):] == suf:
+            return True
+
+    return False
+
+
+def _match_prefix(tag: str, prefix: str):
+    """
+    Check if a tag matches a given prefix.
+
+    :param tag: The tag to check.
+    :type tag: str
+    :param prefix: The prefix to match.
+    :type prefix: str
+    :return: True if the tag matches the prefix, False otherwise.
+    :rtype: bool
+    """
+    tag_words = _split_to_words(tag)
+    prefix_words = _split_to_words(prefix)
+    return tag_words[:len(prefix_words)] == prefix_words
+
+
+def _match_same(tag: str, expected: str):
+    """
+    Check if a tag matches another tag, considering singular and plural forms.
+
+    :param tag: The tag to check.
+    :type tag: str
+    :param expected: The expected tag.
+    :type expected: str
+    :return: True if the tag matches the expected tag, False otherwise.
+    :rtype: bool
+    """
+    a = _split_to_words(tag)
+    as_ = [a, [*a[:-1], singular_form(a[-1])], [*a[:-1], plural_form(a[-1])]]
+    as_ = set([tuple(item) for item in as_])
+
+    b = _split_to_words(expected)
+    bs_ = [b, [*b[:-1], singular_form(b[-1])], [*b[:-1], plural_form(b[-1])]]
+    bs_ = set([tuple(item) for item in bs_])
+
+    return bool(as_ & bs_)
+
+
+def is_basic_character_tag(tag: str) -> bool:
+    """
+    Check if a tag is a basic character tag by matching with predefined whitelisted and blacklisted patterns.
+
+    :param tag: The tag to check.
+    :type tag: str
+    :return: True if the tag is a basic character tag, False otherwise.
+    :rtype: bool
+
+    Examples::
+        >>> from imgutils.tagging import is_basic_character_tag
+        >>>
+        >>> is_basic_character_tag('red hair')
+        True
+        >>> is_basic_character_tag('red_hair')  # span doesn't matter
+        True
+        >>> is_basic_character_tag('cat ears')  # singular
+        True
+        >>> is_basic_character_tag('cat ear')  # plural
+        True
+        >>> is_basic_character_tag('chair')  # only whole word will be matched
+        False
+        >>> is_basic_character_tag('hear')  # only whole word will be matched
+        False
+        >>> is_basic_character_tag('dress')
+        False
+    """
+    if any(_match_same(tag, wl_tag) for wl_tag in _CHAR_WHITELIST):
+        return False
+    else:
+        return (any(_match_suffix(tag, suffix) for suffix in _CHAR_SUFFIXES)
+                or any(_match_prefix(tag, prefix) for prefix in _CHAR_PREFIXES))
+
+
+def drop_basic_character_tags(tags: Union[List[str], Mapping[str, float]]) -> Union[List[str], Mapping[str, float]]:
+    """
+    Drop basic character tags from the given list or mapping of tags.
+
+    :param tags: List or mapping of tags to be filtered.
+    :type tags: Union[List[str], Mapping[str, float]]
+    :return: Filtered list or mapping of tags without the basic character tags.
+    :rtype: Union[List[str], Mapping[str, float]]
+    :raises TypeError: If the input tags are neither a list nor a dictionary.
+
+    Examples::
+        >>> from imgutils.tagging import drop_basic_character_tags
+        >>>
+        >>> drop_basic_character_tags({
+        ...     '1girl': 1.0, 'solo': 0.95,
+        ...     'red_hair': 0.7, 'cat ears': 0.6,
+        ...     'chair': 0.86, 'hear': 0.72,
+        ... })
+        {'1girl': 1.0, 'solo': 0.95, 'chair': 0.86, 'hear': 0.72}
+        >>> drop_basic_character_tags([
+        ...     '1girl', 'solo', 'red_hair', 'cat ears', 'chair', 'hear'
+        ... ])
+        ['1girl', 'solo', 'chair', 'hear']
+    """
+    if isinstance(tags, dict):
+        return {tag: value for tag, value in tags.items() if not is_basic_character_tag(tag)}
+    elif isinstance(tags, list):
+        return [tag for tag in tags if not is_basic_character_tag(tag)]
+    else:
+        raise TypeError(f"Unsupported types of tags, dict or list expected, but {tags!r} found.")
diff --git a/imgutils/tagging/order.py b/imgutils/tagging/order.py
new file mode 100644
index 00000000000..bbab5847497
--- /dev/null
+++ b/imgutils/tagging/order.py
@@ -0,0 +1,90 @@
+import random
+import re
+from typing import Union, List, Mapping
+
+try:
+    from typing import Literal
+except (ImportError, ModuleNotFoundError):
+    from typing_extensions import Literal
+
+
+def sort_tags(tags: Union[List[str], Mapping[str, float]],
+              mode: Literal['original', 'shuffle', 'score'] = 'score') -> List[str]:
+    """
+    Sort the input list or mapping of tags by specified mode.
+
+    Tags can represent people counts (e.g., '1girl', '2boys'), and 'solo' tags.
+
+    :param tags: List or mapping of tags to be sorted.
+    :type tags: Union[List[str], Mapping[str, float]]
+    :param mode: The mode for sorting the tags. Options: 'original' (original order),
+                 'shuffle' (random shuffle), 'score' (sorted by score if available).
+    :type mode: Literal['original', 'shuffle', 'score']
+    :return: Sorted list of tags based on the specified mode.
+    :rtype: List[str]
+    :raises ValueError: If an unknown sort mode is provided.
+    :raises TypeError: If the input tags are of unsupported type or if mode is 'score'
+                       and the input is a list (as it does not have scores).
+
+    Examples:
+        Sorting tags in original order:
+
+        >>> from imgutils.tagging import sort_tags
+        >>>
+        >>> tags = ['1girls', 'solo', 'red_hair', 'cat ears']
+        >>> sort_tags(tags, mode='original')
+        ['solo', '1girls', 'red_hair', 'cat ears']
+        >>>
+        >>> tags = {'1girls': 0.9, 'solo': 0.95, 'red_hair': 1.0, 'cat_ears': 0.92}
+        >>> sort_tags(tags, mode='original')
+        ['solo', '1girls', 'red_hair', 'cat_ears']
+
+        Sorting tags by score (for a mapping of tags with scores):
+
+        >>> from imgutils.tagging import sort_tags
+        >>>
+        >>> tags = {'1girls': 0.9, 'solo': 0.95, 'red_hair': 1.0, 'cat_ears': 0.92}
+        >>> sort_tags(tags)
+        ['solo', '1girls', 'red_hair', 'cat_ears']
+
+        Shuffling tags (output is not unique)
+
+        >>> from imgutils.tagging import sort_tags
+        >>>
+        >>> tags = ['1girls', 'solo', 'red_hair', 'cat ears']
+        >>> sort_tags(tags, mode='shuffle')
+        ['solo', '1girls', 'red_hair', 'cat ears']
+        >>>
+        >>> tags = {'1girls': 0.9, 'solo': 0.95, 'red_hair': 1.0, 'cat_ears': 0.92}
+        >>> sort_tags(tags, mode='shuffle')
+        ['solo', '1girls', 'cat_ears', 'red_hair']
+    """
+    if mode not in {'original', 'shuffle', 'score'}:
+        raise ValueError(f'Unknown sort_mode, \'original\', '
+                         f'\'shuffle\' or \'score\' expected but {mode!r} found.')
+    npeople_tags = []
+    remaining_tags = []
+
+    if 'solo' in tags:
+        npeople_tags.append('solo')
+
+    for tag in tags:
+        if tag == 'solo':
+            continue
+        if re.fullmatch(r'^\d+\+?(boy|girl)s?$', tag):  # 1girl, 1boy, 2girls, 3boys, 9+girls
+            npeople_tags.append(tag)
+        else:
+            remaining_tags.append(tag)
+
+    if mode == 'score':
+        if isinstance(tags, dict):
+            remaining_tags = sorted(remaining_tags, key=lambda x: -tags[x])
+        else:
+            raise TypeError(f'Sort mode {mode!r} not supported for list, '
+                            f'for it do not have scores.')
+    elif mode == 'shuffle':
+        random.shuffle(remaining_tags)
+    else:
+        pass
+
+    return npeople_tags + remaining_tags
diff --git a/imgutils/upscale/__init__.py b/imgutils/upscale/__init__.py
new file mode 100644
index 00000000000..c9e8fe991b7
--- /dev/null
+++ b/imgutils/upscale/__init__.py
@@ -0,0 +1,1 @@
+from .waifu2x import upscale_image_by_waifu2x
diff --git a/imgutils/upscale/waifu2x.py b/imgutils/upscale/waifu2x.py
new file mode 100644
index 00000000000..d0ec4a73658
--- /dev/null
+++ b/imgutils/upscale/waifu2x.py
@@ -0,0 +1,133 @@
+import os
+import re
+from functools import lru_cache
+from typing import Optional, Mapping, Tuple
+
+import numpy as np
+from PIL import Image
+from huggingface_hub import hf_hub_download, HfFileSystem
+
+from ..data import ImageTyping, load_image
+from ..utils import open_onnx_model, area_batch_run
+
+_hf_fs = HfFileSystem()
+
+_REPOSITORY = 'deepghs/waifu2x_onnx'
+_FILENAME_PATTERN = re.compile(r'^(noise(?P<noise>\d+)_?)?(scale(?P<scale>\d+)x)?\.onnx$')
+
+
+@lru_cache()
+def _load_available_version() -> Mapping[Tuple[str, str, str], Mapping[Tuple[Optional[int], int], str]]:
+    """
+    Load available versions of the waifu2x model from the Hugging Face Hub.
+
+    :return: A mapping of available model versions.
+    :rtype: Mapping
+    """
+    records = {}
+    for file in _hf_fs.glob(f'{_REPOSITORY}/*/onnx_models/*/*/*.onnx'):
+        segments = os.path.relpath(file, _REPOSITORY).split('/')
+        assert len(segments) == 5 and segments[1] == 'onnx_models'
+        version, model, type_ = segments[0], segments[2], segments[3]
+        filename = segments[4]
+
+        key = (version, model, type_)
+        if key not in records:
+            records[key] = []
+        records[key].append(filename)
+
+    retval = {}
+    for key, filenames in records.items():
+        retval[key] = {}
+        for filename in filenames:
+            matching = _FILENAME_PATTERN.fullmatch(filename)
+            assert matching, f'Not matched, {filename!r}, key: {key!r}'
+            noise = int(matching.group('noise')) if matching.group('noise') else None
+            scale = int(matching.group('scale')) if matching.group('scale') else 1
+            retval[key][(noise, scale)] = filename
+
+    return retval
+
+
+@lru_cache()
+def _open_waifu2x_onnx_model(version: str, model: str, type_: str, noise: Optional[int], scale: int):
+    """
+    Open a specific version of the waifu2x ONNX model.
+
+    :param version: The model version.
+    :type version: str
+    :param model: The model type.
+    :type model: str
+    :param type_: The model usage type (e.g., 'art').
+    :type type_: str
+    :param noise: The noise level (None for no noise).
+    :type noise: Optional[int]
+    :param scale: The scaling factor.
+    :type scale: int
+    :return: The ONNX model.
+    """
+    _all_versions = _load_available_version()
+    if (version, model, type_) in _all_versions:
+        _all_k = _all_versions[(version, model, type_)]
+        if (noise, scale) in _all_k:
+            filename = _all_k[(noise, scale)]
+            return open_onnx_model(hf_hub_download(
+                f'deepghs/waifu2x_onnx',
+                f'{version}/onnx_models/{model}/{type_}/{filename}',
+            ))
+        else:
+            raise ValueError(f'Noise {noise!r} or scale {scale!r} may be invalid.')
+    else:
+        raise ValueError(f"Version {version!r} or model {model!r} or type_ {type_!r} may be invalid.")
+
+
+def _single_upscale_by_waifu2x(x, version: str = '20230504', model: str = 'swin_unet',
+                               type_: str = 'art', noise: Optional[int] = None, scale: int = 2):
+    ort = _open_waifu2x_onnx_model(version, model, type_, noise, scale)
+    # noinspection PyTypeChecker
+    x = np.pad(x, ((0, 0), (0, 0), (8, 8), (8, 8)), mode='reflect')
+    y, = ort.run(['y'], {'x': x})
+    return y
+
+
+def upscale_image_by_waifu2x(image: ImageTyping, scale: int = 2, noise: Optional[int] = None,
+                             version: str = '20230504', model: str = 'swin_unet', type_: str = 'art',
+                             tile_size: int = 64, tile_overlap: int = 8, silent: bool = False) -> Image.Image:
+    """
+    Upscale an image using the waifu2x model.
+
+    :param image: The input image.
+    :type image: ImageTyping
+    :param scale: The scaling factor. Default is 2.
+    :type scale: int
+    :param noise: The noise level. Default is None.
+    :type noise: Optional[int]
+    :param version: The model version. Default is '20230504'.
+    :type version: str
+    :param model: The model type. Default is 'swin_unet'.
+    :type model: str
+    :param type_: The model usage type (e.g., 'art'). Default is 'art'.
+    :type type_: str
+    :param tile_size: The size of processing tiles. Default is 64.
+    :type tile_size: int
+    :param tile_overlap: The overlap between tiles. Default is 8.
+    :type tile_overlap: int
+    :param silent: If True, the progress will not be displayed. Default is False.
+    :type silent: bool
+    :return: The upscaled image.
+    :rtype: Image.Image
+    """
+    image = load_image(image, mode='RGB', force_background='white')
+    input_ = np.array(image).astype(np.float32) / 255.0
+    input_ = input_.transpose((2, 0, 1))[None, ...]
+
+    def _method(ix):
+        return _single_upscale_by_waifu2x(ix, version, model, type_, noise, scale)
+
+    output_ = area_batch_run(
+        input_, _method,
+        scale=scale, tile_size=tile_size, tile_overlap=tile_overlap, silent=silent,
+        process_title='Waifu2x Upscale',
+    )
+    output_ = np.clip(output_, a_min=0.0, a_max=1.0)
+    return Image.fromarray((output_[0].transpose((1, 2, 0)) * 255).astype(np.int8), 'RGB')
diff --git a/imgutils/utils/__init__.py b/imgutils/utils/__init__.py
index 2800761193b..865c2667b8a 100644
--- a/imgutils/utils/__init__.py
+++ b/imgutils/utils/__init__.py
@@ -2,4 +2,6 @@
 Overview:
     Generic utilities for :mod:`imgutils`.
 """
+from .area import *
 from .onnxruntime import *
+from .tqdm_ import *
diff --git a/imgutils/utils/area.py b/imgutils/utils/area.py
new file mode 100644
index 00000000000..d7671f3ac1c
--- /dev/null
+++ b/imgutils/utils/area.py
@@ -0,0 +1,82 @@
+import math
+
+import numpy as np
+
+from .tqdm_ import tqdm
+
+__all__ = ['area_batch_run']
+
+
+def area_batch_run(origin_input: np.ndarray, func, scale: int = 1,
+                   tile_size: int = 512, tile_overlap: int = 16, batch_size: int = 4,
+                   input_channels: int = 3, output_channels: int = 3, silent: bool = False,
+                   process_title: str = 'Process Tiles', rebuild_title: str = 'Rebuild Tiles'):
+    """
+    Perform a batch execution of a given function on overlapping tiles of a large image.
+
+    This function divides the original input image into tiles, applies a given function to each tile,
+    and then reconstructs the image by combining the processed tiles.
+
+    :param origin_input: The original input image as a NumPy ndarray with shape (batch, channels, height, width).
+    :type origin_input: np.ndarray
+    :param func: The function to apply to each tile. It should accept a tile (np.ndarray)
+        as input and return the processed tile.
+    :type func: callable
+    :param scale: Scaling factor for output, defaults to 1.
+    :type scale: int, optional
+    :param tile_size: Size of the tiles, defaults to 512.
+    :type tile_size: int, optional
+    :param tile_overlap: Overlap between adjacent tiles, defaults to 16.
+    :type tile_overlap: int, optional
+    :param batch_size: Batch size for processing tiles, defaults to 4.
+    :type batch_size: int, optional
+    :param input_channels: Number of input channels, defaults to 3.
+    :type input_channels: int, optional
+    :param output_channels: Number of output channels, defaults to 3.
+    :type output_channels: int, optional
+    :param silent: If True, suppresses the progress bar output, defaults to False.
+    :type silent: bool, optional
+    :param process_title: Title for the processing progress bar, defaults to 'Process Tiles'.
+    :type process_title: str, optional
+    :param rebuild_title: Title for the rebuilding progress bar, defaults to 'Rebuild Tiles'.
+    :type rebuild_title: str, optional
+    :return: Processed image as a NumPy ndarray.
+    :rtype: np.ndarray
+    """
+    batch, channels, height, width = origin_input.shape
+    assert channels == input_channels, f'Input channels {input_channels!r} expected, but {channels!r} found.'
+
+    tile = min(tile_size, height, width)
+    stride = tile - tile_overlap
+    h_idx_list = list(range(0, height - tile, stride)) + [height - tile]
+    w_idx_list = list(range(0, width - tile, stride)) + [width - tile]
+    sum_ = np.zeros((batch, output_channels, height * scale, width * scale), dtype=origin_input.dtype)
+    weight = np.zeros_like(sum_, dtype=origin_input.dtype)
+
+    all_patch = []
+    all_idx = []
+
+    with tqdm(total=math.ceil(len(h_idx_list) * len(w_idx_list) / batch_size),
+              desc=process_title, silent=silent) as pbar:
+        for h_idx in h_idx_list:
+            for w_idx in w_idx_list:
+                in_patch = origin_input[..., h_idx: h_idx + tile, w_idx: w_idx + tile]
+                all_patch.append(in_patch)
+                all_idx.append((h_idx, w_idx))
+
+        results = []
+        for i in range(0, len(all_patch), batch_size):
+            input_ = np.concatenate(all_patch[i:i + batch_size])
+            output_ = func(input_)
+            for idx, (h_idx, w_idx) in enumerate(all_idx[i:i + batch_size]):
+                results.append((h_idx, w_idx, output_[idx]))
+            pbar.update()
+
+    for h_idx, w_idx, output_ in tqdm(results, desc=rebuild_title, silent=silent):
+        out_patch_mask = np.ones_like(output_)
+        h_min, h_max = h_idx * scale, (h_idx + tile) * scale
+        w_min, w_max = w_idx * scale, (w_idx + tile) * scale
+        sum_[..., h_min:h_max, w_min:w_max] += output_
+        weight[..., h_min:h_max, w_min:w_max] += out_patch_mask
+
+    return sum_ / weight
diff --git a/imgutils/utils/tqdm_.py b/imgutils/utils/tqdm_.py
new file mode 100644
index 00000000000..79b74953bcd
--- /dev/null
+++ b/imgutils/utils/tqdm_.py
@@ -0,0 +1,25 @@
+import io
+
+from tqdm.auto import tqdm as _origin_tqdm
+
+__all__ = ['tqdm']
+
+
+def tqdm(*args, silent: bool = False, **kwargs):
+    """
+    An enhanced version of tqdm (progress bar) with an option to silence the output.
+
+    This function modifies the behavior of tqdm to allow silencing the progress bar.
+
+    :param args: Positional arguments to be passed to tqdm.
+    :param silent: If True, the progress bar content will not be displayed.
+    :type silent: bool
+    :param kwargs: Additional keyword arguments to be passed to tqdm.
+    :return: tqdm progress bar.
+    :rtype: tqdm.std.tqdm
+    """
+    with io.StringIO() as sio:
+        if silent:
+            kwargs['file'] = sio
+
+        return _origin_tqdm(*args, **kwargs)
diff --git a/requirements-zoo.txt b/requirements-zoo.txt
index 340af0f1f4d..94714f287cc 100644
--- a/requirements-zoo.txt
+++ b/requirements-zoo.txt
@@ -21,4 +21,5 @@ ultralytics
 controlnet_aux
 lighttuner
 natsort
-tabulate
\ No newline at end of file
+tabulate
+hfmirror>=0.0.7
\ No newline at end of file
diff --git a/zoo/waifu2x/__init__.py b/zoo/waifu2x/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/zoo/waifu2x/__main__.py b/zoo/waifu2x/__main__.py
new file mode 100644
index 00000000000..e460f5190b0
--- /dev/null
+++ b/zoo/waifu2x/__main__.py
@@ -0,0 +1,31 @@
+from functools import partial
+
+import click
+from ditk import logging
+
+from .sync import sync_to_huggingface
+from ..utils import GLOBAL_CONTEXT_SETTINGS
+from ..utils import print_version as _origin_print_version
+
+print_version = partial(_origin_print_version, 'zoo.waifu2x')
+
+
+@click.group(context_settings={**GLOBAL_CONTEXT_SETTINGS})
+@click.option('-v', '--version', is_flag=True,
+              callback=print_version, expose_value=False, is_eager=True,
+              help="Utils with waifu2x models.")
+def cli():
+    pass  # pragma: no cover
+
+
+@cli.command('sync', help='Export feature extract model as onnx.',
+             context_settings={**GLOBAL_CONTEXT_SETTINGS})
+@click.option('--repository', '-r', 'repository', type=str, default='deepghs/waifu2x_onnx',
+              help='Repository to sync.', show_default=True)
+def sync(repository: str):
+    logging.try_init_root(logging.INFO)
+    sync_to_huggingface(repository)
+
+
+if __name__ == '__main__':
+    cli()
diff --git a/zoo/waifu2x/sync.py b/zoo/waifu2x/sync.py
new file mode 100644
index 00000000000..8785b618847
--- /dev/null
+++ b/zoo/waifu2x/sync.py
@@ -0,0 +1,50 @@
+import os
+import re
+import zipfile
+from contextlib import contextmanager
+
+from github import Github
+from hbutils.system import TemporaryDirectory
+from hfmirror.resource import LocalDirectoryResource
+from hfmirror.storage import HuggingfaceStorage
+from hfmirror.sync import SyncTask
+from hfmirror.utils import download_file
+from huggingface_hub import HfApi
+from tqdm.auto import tqdm
+
+MODEL_ASSET_PATTERN = re.compile(r'^waifu2x_onnx_models_(?P<version>[\s\S]*)\.zip$')
+
+
+@contextmanager
+def load_model_project():
+    github_client = Github(os.environ['GH_ACCESS_TOKEN'])
+    repo = github_client.get_repo('nagadomi/nunif')
+    release = repo.get_release('0.0.0')
+    with TemporaryDirectory() as ztd, TemporaryDirectory() as ptd:
+        for asset in tqdm(release.get_assets()):
+            matching = MODEL_ASSET_PATTERN.fullmatch(asset.name)
+            if not matching:
+                continue
+
+            version = matching.group('version')
+            url = asset.browser_download_url
+            zip_file = os.path.join(ztd, asset.name)
+            download_file(url, zip_file)
+
+            version_dir = os.path.join(ptd, version)
+            os.makedirs(version_dir, exist_ok=True)
+            with zipfile.ZipFile(zip_file, 'r') as zf:
+                zf.extractall(version_dir)
+
+        yield ptd
+
+
+def sync_to_huggingface(repository: str = 'deepghs/waifu2x_onnx'):
+    hf_client = HfApi(token=os.environ['HF_TOKEN'])
+    hf_client.create_repo(repository, repo_type='model', exist_ok=True)
+    storage = HuggingfaceStorage(repository, repo_type='model', hf_client=hf_client)
+
+    with load_model_project() as ptd:
+        resource = LocalDirectoryResource(ptd)
+        task = SyncTask(resource, storage)
+        task.sync()


Test patch:
diff --git a/test/tagging/conftest.py b/test/tagging/conftest.py
new file mode 100644
index 00000000000..3637f69b292
--- /dev/null
+++ b/test/tagging/conftest.py
@@ -0,0 +1,23 @@
+import pytest
+
+
+@pytest.fixture()
+def complex_dict_tags():
+    return {
+        '1girl': 0.998362123966217, 'solo': 0.9912548065185547, 'long_hair': 0.9401906728744507,
+        'breasts': 0.983635425567627, 'looking_at_viewer': 0.9146994352340698, 'blush': 0.8892400860786438,
+        'smile': 0.43393653631210327, 'bangs': 0.49712443351745605, 'large_breasts': 0.5196534395217896,
+        'navel': 0.9653235077857971, 'hair_between_eyes': 0.5786703824996948, 'very_long_hair': 0.8142435550689697,
+        'closed_mouth': 0.9369247555732727, 'nipples': 0.9660118222236633, 'purple_eyes': 0.9676010012626648,
+        'collarbone': 0.588348925113678, 'nude': 0.9496222734451294, 'red_hair': 0.9200156331062317,
+        'sweat': 0.8690457344055176, 'horns': 0.9711267948150635, 'pussy': 0.9868264198303223,
+        'spread_legs': 0.9603149890899658, 'armpits': 0.9024748802185059, 'stomach': 0.6723923087120056,
+        'arms_up': 0.9380699396133423, 'completely_nude': 0.9002960920333862, 'uncensored': 0.8612104058265686,
+        'pussy_juice': 0.6021570563316345, 'feet_out_of_frame': 0.39779460430145264, 'on_bed': 0.610720157623291,
+        'arms_behind_head': 0.44814401865005493, 'breasts_apart': 0.39798974990844727, 'clitoris': 0.5310801267623901,
+    }
+
+
+@pytest.fixture()
+def complex_list_tags(complex_dict_tags):
+    return list(complex_dict_tags.keys())
diff --git a/test/tagging/test_blacklist.py b/test/tagging/test_blacklist.py
new file mode 100644
index 00000000000..a908d248399
--- /dev/null
+++ b/test/tagging/test_blacklist.py
@@ -0,0 +1,39 @@
+import pytest
+
+from imgutils.tagging import is_blacklisted, drop_blacklisted_tags
+
+
+@pytest.mark.unittest
+class TestTaggingBlacklist:
+    def test_is_blacklisted(self):
+        assert is_blacklisted('alternate_costume')
+        assert is_blacklisted('cosplay')
+        assert not is_blacklisted('red_hair')
+        assert not is_blacklisted('solo')
+
+    def test_drop_blacklisted_tags(self, complex_dict_tags, complex_list_tags):
+        assert drop_blacklisted_tags(complex_dict_tags) == pytest.approx({
+            '1girl': 0.998362123966217, 'solo': 0.9912548065185547, 'long_hair': 0.9401906728744507,
+            'breasts': 0.983635425567627, 'looking_at_viewer': 0.9146994352340698, 'blush': 0.8892400860786438,
+            'smile': 0.43393653631210327, 'bangs': 0.49712443351745605, 'large_breasts': 0.5196534395217896,
+            'navel': 0.9653235077857971, 'hair_between_eyes': 0.5786703824996948, 'very_long_hair': 0.8142435550689697,
+            'closed_mouth': 0.9369247555732727, 'nipples': 0.9660118222236633, 'purple_eyes': 0.9676010012626648,
+            'collarbone': 0.588348925113678, 'nude': 0.9496222734451294, 'red_hair': 0.9200156331062317,
+            'sweat': 0.8690457344055176, 'horns': 0.9711267948150635, 'pussy': 0.9868264198303223,
+            'spread_legs': 0.9603149890899658, 'armpits': 0.9024748802185059, 'stomach': 0.6723923087120056,
+            'arms_up': 0.9380699396133423, 'completely_nude': 0.9002960920333862, 'uncensored': 0.8612104058265686,
+            'pussy_juice': 0.6021570563316345, 'on_bed': 0.610720157623291, 'arms_behind_head': 0.44814401865005493,
+            'breasts_apart': 0.39798974990844727, 'clitoris': 0.5310801267623901,
+        }, abs=1e-3)
+        assert drop_blacklisted_tags(complex_list_tags) == [
+            '1girl', 'solo', 'long_hair', 'breasts', 'looking_at_viewer', 'blush', 'smile', 'bangs', 'large_breasts',
+            'navel', 'hair_between_eyes', 'very_long_hair', 'closed_mouth', 'nipples', 'purple_eyes', 'collarbone',
+            'nude', 'red_hair', 'sweat', 'horns', 'pussy', 'spread_legs', 'armpits', 'stomach', 'arms_up',
+            'completely_nude', 'uncensored', 'pussy_juice', 'on_bed', 'arms_behind_head', 'breasts_apart', 'clitoris'
+        ]
+
+    def test_drop_blacklisted_tags_error(self):
+        with pytest.raises(TypeError):
+            drop_blacklisted_tags(123)
+        with pytest.raises(TypeError):
+            drop_blacklisted_tags(None)
diff --git a/test/tagging/test_character.py b/test/tagging/test_character.py
new file mode 100644
index 00000000000..5b017e46c89
--- /dev/null
+++ b/test/tagging/test_character.py
@@ -0,0 +1,50 @@
+import pytest
+
+from imgutils.tagging import is_basic_character_tag, drop_basic_character_tags
+
+
+@pytest.mark.unittest
+class TestTaggingCharacter:
+    def test_is_basic_character_tag(self):
+        assert is_basic_character_tag('red_hair')
+        assert is_basic_character_tag('red hair')
+        assert is_basic_character_tag('blue eyes')
+        assert is_basic_character_tag('blue eye')
+        assert is_basic_character_tag('sheep ear')
+        assert is_basic_character_tag('pointy ears')
+        assert is_basic_character_tag('hair bun')
+
+        assert is_basic_character_tag('hair over heads')
+        assert is_basic_character_tag('hair_between_breasts')
+
+        assert not is_basic_character_tag('chair')
+        assert not is_basic_character_tag('hear')
+        assert not is_basic_character_tag('drill')
+        assert not is_basic_character_tag('drills')
+        assert not is_basic_character_tag('pubic hair')
+
+    def test_drop_basic_character_tags(self, complex_dict_tags, complex_list_tags):
+        assert drop_basic_character_tags(complex_dict_tags) == pytest.approx({
+            '1girl': 0.998362123966217, 'solo': 0.9912548065185547, 'breasts': 0.983635425567627,
+            'looking_at_viewer': 0.9146994352340698, 'blush': 0.8892400860786438, 'smile': 0.43393653631210327,
+            'large_breasts': 0.5196534395217896, 'navel': 0.9653235077857971, 'closed_mouth': 0.9369247555732727,
+            'nipples': 0.9660118222236633, 'collarbone': 0.588348925113678, 'nude': 0.9496222734451294,
+            'sweat': 0.8690457344055176, 'pussy': 0.9868264198303223, 'spread_legs': 0.9603149890899658,
+            'armpits': 0.9024748802185059, 'stomach': 0.6723923087120056, 'arms_up': 0.9380699396133423,
+            'completely_nude': 0.9002960920333862, 'uncensored': 0.8612104058265686, 'pussy_juice': 0.6021570563316345,
+            'feet_out_of_frame': 0.39779460430145264, 'on_bed': 0.610720157623291,
+            'arms_behind_head': 0.44814401865005493, 'breasts_apart': 0.39798974990844727,
+            'clitoris': 0.5310801267623901
+        }, abs=1e-3)
+        assert drop_basic_character_tags(complex_list_tags) == [
+            '1girl', 'solo', 'breasts', 'looking_at_viewer', 'blush', 'smile', 'large_breasts', 'navel', 'closed_mouth',
+            'nipples', 'collarbone', 'nude', 'sweat', 'pussy', 'spread_legs', 'armpits', 'stomach', 'arms_up',
+            'completely_nude', 'uncensored', 'pussy_juice', 'feet_out_of_frame', 'on_bed', 'arms_behind_head',
+            'breasts_apart', 'clitoris'
+        ]
+
+    def test_drop_basic_character_tags_error(self):
+        with pytest.raises(TypeError):
+            drop_basic_character_tags(122)
+        with pytest.raises(TypeError):
+            drop_basic_character_tags(None)
diff --git a/test/tagging/test_order.py b/test/tagging/test_order.py
new file mode 100644
index 00000000000..d6feec435b7
--- /dev/null
+++ b/test/tagging/test_order.py
@@ -0,0 +1,63 @@
+import pytest
+
+from imgutils.tagging import sort_tags
+
+
+@pytest.mark.unittest
+class TestTaggingOrder:
+    def test_sort_tags(self, complex_dict_tags, complex_list_tags):
+        assert sort_tags(complex_dict_tags) == [
+            'solo', '1girl', 'pussy', 'breasts', 'horns', 'purple_eyes', 'nipples', 'navel', 'spread_legs', 'nude',
+            'long_hair', 'arms_up', 'closed_mouth', 'red_hair', 'looking_at_viewer', 'armpits', 'completely_nude',
+            'blush', 'sweat', 'uncensored', 'very_long_hair', 'stomach', 'on_bed', 'pussy_juice', 'collarbone',
+            'hair_between_eyes', 'clitoris', 'large_breasts', 'bangs', 'arms_behind_head', 'smile', 'breasts_apart',
+            'feet_out_of_frame'
+        ]
+        with pytest.raises(TypeError):
+            sort_tags(complex_list_tags)
+
+    def test_sort_tags_score(self, complex_dict_tags, complex_list_tags):
+        assert sort_tags(complex_dict_tags, mode='score') == [
+            'solo', '1girl', 'pussy', 'breasts', 'horns', 'purple_eyes', 'nipples', 'navel', 'spread_legs', 'nude',
+            'long_hair', 'arms_up', 'closed_mouth', 'red_hair', 'looking_at_viewer', 'armpits', 'completely_nude',
+            'blush', 'sweat', 'uncensored', 'very_long_hair', 'stomach', 'on_bed', 'pussy_juice', 'collarbone',
+            'hair_between_eyes', 'clitoris', 'large_breasts', 'bangs', 'arms_behind_head', 'smile', 'breasts_apart',
+            'feet_out_of_frame'
+        ]
+        with pytest.raises(TypeError):
+            sort_tags(complex_list_tags, mode='score')
+
+    def test_sort_tags_original(self, complex_dict_tags, complex_list_tags):
+        assert sort_tags(complex_dict_tags, mode='original') == [
+            'solo', '1girl', 'long_hair', 'breasts', 'looking_at_viewer', 'blush', 'smile', 'bangs', 'large_breasts',
+            'navel', 'hair_between_eyes', 'very_long_hair', 'closed_mouth', 'nipples', 'purple_eyes', 'collarbone',
+            'nude', 'red_hair', 'sweat', 'horns', 'pussy', 'spread_legs', 'armpits', 'stomach', 'arms_up',
+            'completely_nude', 'uncensored', 'pussy_juice', 'feet_out_of_frame', 'on_bed', 'arms_behind_head',
+            'breasts_apart', 'clitoris'
+        ]
+        assert sort_tags(complex_list_tags, mode='original') == [
+            'solo', '1girl', 'long_hair', 'breasts', 'looking_at_viewer', 'blush', 'smile', 'bangs', 'large_breasts',
+            'navel', 'hair_between_eyes', 'very_long_hair', 'closed_mouth', 'nipples', 'purple_eyes', 'collarbone',
+            'nude', 'red_hair', 'sweat', 'horns', 'pussy', 'spread_legs', 'armpits', 'stomach', 'arms_up',
+            'completely_nude', 'uncensored', 'pussy_juice', 'feet_out_of_frame', 'on_bed', 'arms_behind_head',
+            'breasts_apart', 'clitoris'
+        ]
+
+    def test_sort_tags_shuffle(self, complex_dict_tags, complex_list_tags):
+        tpls = []
+        for _ in range(10):
+            new_tpl = tuple(sort_tags(complex_dict_tags, mode='shuffle'))
+            assert new_tpl[:2] == ('solo', '1girl')
+            tpls.append(new_tpl)
+        assert len(set(tpls)) >= 9
+
+        tpls = []
+        for _ in range(10):
+            new_tpl = tuple(sort_tags(complex_list_tags, mode='shuffle'))
+            assert new_tpl[:2] == ('solo', '1girl')
+            tpls.append(new_tpl)
+        assert len(set(tpls)) >= 9
+
+    def test_sort_tags_invalid_mode(self, complex_dict_tags):
+        with pytest.raises(ValueError):
+            _ = sort_tags(complex_dict_tags, mode='invalid mode')
diff --git a/test/tagging/test_overlap.py b/test/tagging/test_overlap.py
index 6535fc7e840..8b0dbb17f0c 100644
--- a/test/tagging/test_overlap.py
+++ b/test/tagging/test_overlap.py
@@ -3,23 +3,6 @@
 from imgutils.tagging import drop_overlaps_for_dict, drop_overlap_tags
 
 
-@pytest.fixture()
-def complex_dict_tags():
-    return {
-        '1girl': 0.998362123966217, 'solo': 0.9912548065185547, 'long_hair': 0.9401906728744507,
-        'breasts': 0.983635425567627, 'looking_at_viewer': 0.9146994352340698, 'blush': 0.8892400860786438,
-        'smile': 0.43393653631210327, 'bangs': 0.49712443351745605, 'large_breasts': 0.5196534395217896,
-        'navel': 0.9653235077857971, 'hair_between_eyes': 0.5786703824996948, 'very_long_hair': 0.8142435550689697,
-        'closed_mouth': 0.9369247555732727, 'nipples': 0.9660118222236633, 'purple_eyes': 0.9676010012626648,
-        'collarbone': 0.588348925113678, 'nude': 0.9496222734451294, 'red_hair': 0.9200156331062317,
-        'sweat': 0.8690457344055176, 'horns': 0.9711267948150635, 'pussy': 0.9868264198303223,
-        'spread_legs': 0.9603149890899658, 'armpits': 0.9024748802185059, 'stomach': 0.6723923087120056,
-        'arms_up': 0.9380699396133423, 'completely_nude': 0.9002960920333862, 'uncensored': 0.8612104058265686,
-        'pussy_juice': 0.6021570563316345, 'feet_out_of_frame': 0.39779460430145264, 'on_bed': 0.610720157623291,
-        'arms_behind_head': 0.44814401865005493, 'breasts_apart': 0.39798974990844727, 'clitoris': 0.5310801267623901,
-    }
-
-
 @pytest.mark.unittest
 class TestTaggingOverlap:
     def test_drop_overlap_tags(self):