Tagging Anime Images in Lychee with DeepDanbooru
I wanted to create a collection of my favorite anime images (e.g. Pixiv artworks) on my private Lychee instance. Uploading the images was an easy task, but I was thinking that the browsing experience would be better if there was a method to filter the images by their characteristics. Fortunately, I was able to find a project on GitHub named DeepDanbooru.
Lychee supports adding tags to images. The plan was to tag the images using this project, and the search box in Lychee already supports searching images by tags. Using the Lychee API, the images will be fetched and passed into the AI model. Tags that are outputted will then be added to the corresponding images.
To use DeepDanbooru, we first clone the git repository. Install the Python dependencies using pip install -r requirements.txt. Then, we can download a pre-trained model from the Releases page. You may also train your own model by following the instructions in the repo's README. We then place the downloaded folder in the repo's root directory and rename it to model.
Next, create a script folder in which we will place our automation script. Open the folder. Copy the content below to a file named requirements.txt.
alive_progress
halo
titlecase
yachalkRun pip install -r requirements.txt. Copy the content below to a file named script.py.
import os
import requests
from alive_progress import alive_bar
from deepdanbooru.commands import evaluate_image
from deepdanbooru.project import load_model_from_project, load_tags_from_project
from halo import Halo
from io import BytesIO
from titlecase import titlecase
from yachalk import chalk
#
# Configuration
#
LYCHEE_HOST: str = "http://localhost"
"""
Host of the Lychee instance.
We recommend running this script on the same server hosting the Lychee instance for the best performance and the least network transfer.
Defaults to `http://localhost`.
"""
LYCHEE_TOKEN: str = ""
"""
API token for the Lychee instance.
"""
ALBUMS_LIST_RULE: str = "exclude" # "include" | "exclude"
"""
Rule for the albums list.
Defaults to `exclude`.
"""
ALBUMS_LIST: list[str] = []
"""
IDs of albums to include or exclude (depending on `ALBUMS_LIST_RULE`).
"""
MODEL_PATH: str = os.path.abspath("../model")
"""
Path to the DeepDanbooru model directory.
Defaults to `os.path.abspath("../model")`, which assumes that you have placed this script in a subfolder under the DeepDanbooru folder.
"""
TAG_THRESHOLD: float = 0.5
"""
Threshold for applying tags. Tags under this threshold will be filtered out.
Defaults to `0.5`.
"""
TAG_TAGGED: str = "Tagged by DeepDanbooru"
"""
Raw tag (not transformed by `CONVERT_TAG`) for marking photos that have been already processed.
Photos that already have this tag will be skipped.
Defaults to `Tagged by DeepDanbooru`.
"""
ADDITIONAL_TAGS: list[str] = ["Source: Pixiv"]
"""
Additional raw tags (not transformed by `CONVERT_TAG`) to be added to the photos.
"""
OVERRIDE_EXISTING_TAGS: bool = False
"""
Whether to override or append existing tags using new generated tags.
Defaults to `True`.
"""
def CONVERT_TAG(tag: str) -> str:
"""
Custom function to transform a tag before storing it in Lychee.
"""
if tag.startswith("rating:"):
return "Rating: " + titlecase(tag[7:])
converted_tag = titlecase(tag.replace("_", " "))
return converted_tag
#
# Constants
#
REPLACE: str = "\033[1A\033[2K"
#
# Main
#
# Load model and tags
spinner: Halo = Halo(text=chalk.gray("Loading model..."), color="grey").start()
dd_model = load_model_from_project(MODEL_PATH, compile_model=False)
spinner.succeed(chalk.green("Model loaded successfully."))
spinner: Halo = Halo(text=chalk.gray("Loading tags..."), color="grey").start()
dd_tags = load_tags_from_project(MODEL_PATH)
spinner.succeed(chalk.green("Tags loaded successfully."))
print()
# Define headers
headers: dict = {
"Accept": "application/json",
"Authorization": LYCHEE_TOKEN,
"Content-Type": "application/json",
}
# Fetch albums from Lychee
spinner: Halo = Halo(text=chalk.gray("Fetching albums..."), color="grey").start()
response: requests.Response = requests.post(
f"{LYCHEE_HOST}/api/Albums::get", json={}, headers=headers
)
all_albums: list[dict] = response.json()["albums"]
albums: list[dict] = [
album
for album in all_albums
if (
album["id"] in ALBUMS_LIST
if ALBUMS_LIST_RULE == "include"
else album["id"] not in ALBUMS_LIST
)
]
spinner.succeed(
f"Found {len(all_albums)} albums in total, {len(albums)} albums to process."
)
# Process each album
for album in albums:
def print_separator():
term_size = os.get_terminal_size()
print("=" * term_size.columns)
print()
print_separator()
print(
chalk.cyan.bold(f"ALBUM {chalk.underline(album['title'])} (ID: {album['id']})")
)
# Fetch photos from the album
spinner: Halo = Halo(text=chalk.gray("Fetching photos..."), color="grey").start()
response: requests.Response = requests.post(
f"{LYCHEE_HOST}/api/Album::get", json={"albumID": album["id"]}, headers=headers
)
photos: list[dict] = response.json()["photos"]
spinner.succeed(f"Found {len(photos)} photos.")
# Process each photo
with alive_bar(len(photos)) as bar:
for photo in photos:
# Skip if the photo has already been processed
if TAG_TAGGED in photo["tags"]:
print(
chalk.gray(
f"PHOTO {chalk.underline(photo['title'])} (ID: {photo['id']}) (skipped)"
)
)
bar()
continue
print(
chalk.cyan(
f"PHOTO {chalk.underline(photo['title'])} (ID: {photo['id']})"
)
)
# Download the image
spinner: Halo = Halo(
text=chalk.gray("Downloading image..."), color="grey"
).start()
image_data: bytes = requests.get(
f"{LYCHEE_HOST}/{photo['size_variants']['original']['url']}"
).content
spinner.stop()
# Process the image with DeepDanbooru
result: list[tuple[str, float]] = evaluate_image(
BytesIO(image_data), dd_model, dd_tags, TAG_THRESHOLD
)
# Convert tags
tags: list[str] = []
tags.extend(ADDITIONAL_TAGS)
tags.append(TAG_TAGGED)
tags.extend([CONVERT_TAG(tag) for tag, score in result])
print(chalk.blue(f"Tags: {', '.join(tags)}"))
# Upload tags
spinner: Halo = Halo(
text=chalk.gray("Uploading tags..."), color="grey"
).start()
response: requests.Response = requests.post(
f"{LYCHEE_HOST}/api/Photo::setTags",
json={
"photoIDs": [photo["id"]],
"tags": tags,
"shall_override": OVERRIDE_EXISTING_TAGS,
},
headers=headers,
)
if response.status_code == 204:
spinner.succeed(chalk.green("Uploaded successfully."))
else:
spinner.fail(chalk.red("Failed to upload."))
bar()
print_separator()
The script is self-explanatory. Configure the values according to your instance and your preferences. Run the script and see things happen.
