From f3f4ec51d2994374544ddd6e6dbc61a0e87c06cc Mon Sep 17 00:00:00 2001 From: Firq Date: Wed, 16 Oct 2024 22:13:39 +0200 Subject: [PATCH] Rebranding --- .gitignore | 2 +- README.md | 57 ++++++++++---- atlasimagecomposer/__init__.py | 3 - atlasimagecomposer/cli/__init__.py | 0 atlasimagecomposer/config/__init__.py | 1 - pyproject.toml | 12 +-- {atlasimagecomposer => skyeweave}/.gitignore | 0 skyeweave/__init__.py | 3 + .../backend/__init__.py | 0 .../backend/atlas.py | 22 +++--- .../backend/compose.py | 20 ++--- skyeweave/cli/__init__.py | 1 + {atlasimagecomposer => skyeweave}/cli/cli.py | 75 ++++++++++--------- skyeweave/config/__init__.py | 1 + .../config/config.py | 12 +-- {atlasimagecomposer => skyeweave}/py.typed | 0 .../utils/__init__.py | 0 .../utils/disables.py | 1 + .../utils/filesystem.py | 1 - .../utils/logger.py | 8 +- 20 files changed, 128 insertions(+), 91 deletions(-) delete mode 100644 atlasimagecomposer/__init__.py delete mode 100644 atlasimagecomposer/cli/__init__.py delete mode 100644 atlasimagecomposer/config/__init__.py rename {atlasimagecomposer => skyeweave}/.gitignore (100%) create mode 100644 skyeweave/__init__.py rename {atlasimagecomposer => skyeweave}/backend/__init__.py (100%) rename {atlasimagecomposer => skyeweave}/backend/atlas.py (83%) rename {atlasimagecomposer => skyeweave}/backend/compose.py (87%) create mode 100644 skyeweave/cli/__init__.py rename {atlasimagecomposer => skyeweave}/cli/cli.py (57%) create mode 100644 skyeweave/config/__init__.py rename {atlasimagecomposer => skyeweave}/config/config.py (69%) rename {atlasimagecomposer => skyeweave}/py.typed (100%) rename {atlasimagecomposer => skyeweave}/utils/__init__.py (100%) rename {atlasimagecomposer => skyeweave}/utils/disables.py (78%) rename {atlasimagecomposer => skyeweave}/utils/filesystem.py (99%) rename {atlasimagecomposer => skyeweave}/utils/logger.py (66%) diff --git a/.gitignore b/.gitignore index f13dbf4..63f860a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Dev environment *venv/ .vscode/ -*out/ +*out*/ # python files __pycache__/ diff --git a/README.md b/README.md index 731d70c..77b91b4 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,54 @@ -# atlasimagecomposer +# skyeweave -A small CLI toolkit that allows you to easily download and compose any FGO expression sheets +Easily generate any FGO expression sheets from an id -## Installations +> Developed by [Firq](https://firq.dev/) and powered by the [AtlasAcademy API](https://atlasacademy.io/) -Install it using `pip` - You need to specify my extra index for it to be found +## Installation + +The CLI can be installed by using `pip`. Python >= 3.10 is required. ```shell -pip install --extra-index-url https://forgejo.neshweb.net/api/packages/Firq/pypi/simple/ atlasimagecomposer +pip install --extra-index-url https://forgejo.neshweb.net/api/packages/Firq/pypi/simple/ skyeweave ``` +Note: Specifying the additional index of `forgejo.neshweb.net` is necessary, as I don't host my packages on PyPI. Using `--extra-index-url` is necessary as the +specified depencencies are not hosted on that index. + ## Usage -Use it via the CLI command `atlasimagecomposer` +`skyeweave` is a CLI application, meaning it needs to be accessed via the commandline. + +The following options are available: + +- `--help` / `-h`: Shows helpful information about the other arguments +- `--version`: Shows the version number +- `--output`: Sets the output file path. This can be a relative path (`./out` or an absolute path `C:/files/out`) +- `--id`: Specify a servantId or charaId. This will skip the user prompt afterwards (useful in CI applications) +- `--filter`: Specify which spritesheets will actually be used. Useful if you want multiple spritesheets, but not all +- `--timeout`: Sets the timeout for any requests towards the Atlas Academy API. The default is 10 seconds +- `--no-cache`: Clear cache for this id, keeping all other files +- `--reset`: Delete any already downloaded assets +- `--quiet` / `-q`: Mute the output and hide progress bars + +Typical usage: ```plain -usage: atlasimagecomposer [-h] [--version] [--output OUTPUT] [--filter FILTER [FILTER ...]] [--timeout TIMEOUT] [--clear-cache] - -options: - -h, --help show this help message and exit - --version show program's version number and exit - --output OUTPUT Set the output location. This can be an absolute or relative path - --filter FILTER [FILTER ...] Specify one or more spritesheet ids that will be fetched - --timeout TIMEOUT Set the timeout for all requests towards AtlasAcademy, default is 10s - --clear-cache Clear cached assets before downloading files for a servant +skyeweave --output out --id 70 +``` + +This would generate the expressions for Scathach (Servant Id 70) in the folder out, using subfolders to better separate the outputs of multiple runs. + +## Issues / Support + +If there are any issues with `skyeweave`, feel free to reach out to me using the AtlasAcademy discord (`#dev-corner`) or create an issue in this repo. + +If you want to help me debug when an issue occurs, set the environment variable `SKYEWEAVE_STDOUT_LEVEL` to `debug` and send me a copy of the log + +Example on Windows: + +```shell +$env:SKYEWEAVE_STDOUT_LEVEL="debug" + +skyeweave --output out --id 70 > log.log ``` diff --git a/atlasimagecomposer/__init__.py b/atlasimagecomposer/__init__.py deleted file mode 100644 index cb9f729..0000000 --- a/atlasimagecomposer/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import importlib.metadata - -__version__ = importlib.metadata.version(__package__ or "atlasimagecomposer") diff --git a/atlasimagecomposer/cli/__init__.py b/atlasimagecomposer/cli/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/atlasimagecomposer/config/__init__.py b/atlasimagecomposer/config/__init__.py deleted file mode 100644 index 8071528..0000000 --- a/atlasimagecomposer/config/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .config import AtlasDefaults, ExpressionDefaults, Logging, Paths diff --git a/pyproject.toml b/pyproject.toml index a8a7b0f..62eadfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [project] -name = "atlasimagecomposer" -version = "0.1.0-c.5" +name = "skyeweave" +version = "0.1.0-c.1" requires-python = ">= 3.10" authors = [{name = "Firq", email = "firelp42@gmail.com"}] maintainers = [{name = "Firq", email = "firelp42@gmail.com"}] -description = "Package that enables people to quickly download and generate all potential spritesheet expressions with a single command" +description = "Easily generate any FGO expression sheets from an id" classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -30,11 +30,11 @@ typing = [ ] [project.scripts] -atlasimagecomposer = "atlasimagecomposer.cli.cli:run_cli" +skyeweave = "skyeweave.cli:run" [tool.setuptools.packages.find] where = ["."] -include = ["atlasimagecomposer*"] +include = ["skyeweave*"] [tool.setuptools.package-data] "*" = ["py.typed"] diff --git a/atlasimagecomposer/.gitignore b/skyeweave/.gitignore similarity index 100% rename from atlasimagecomposer/.gitignore rename to skyeweave/.gitignore diff --git a/skyeweave/__init__.py b/skyeweave/__init__.py new file mode 100644 index 0000000..19a18d9 --- /dev/null +++ b/skyeweave/__init__.py @@ -0,0 +1,3 @@ +import importlib.metadata + +__version__ = importlib.metadata.version(__package__ or "skyeweave") diff --git a/atlasimagecomposer/backend/__init__.py b/skyeweave/backend/__init__.py similarity index 100% rename from atlasimagecomposer/backend/__init__.py rename to skyeweave/backend/__init__.py diff --git a/atlasimagecomposer/backend/atlas.py b/skyeweave/backend/atlas.py similarity index 83% rename from atlasimagecomposer/backend/atlas.py rename to skyeweave/backend/atlas.py index 44736db..6afc754 100644 --- a/atlasimagecomposer/backend/atlas.py +++ b/skyeweave/backend/atlas.py @@ -2,9 +2,9 @@ import logging from typing import Annotated, List, NotRequired, Tuple, TypedDict import requests -from ..config import AtlasDefaults, Paths, ExpressionDefaults, Logging +from ..config import AtlasAPIConfig, PathConfig, ExpressionDefaults, LoggingConfig -LOGGER = logging.getLogger(Logging.NAME) +LOGGER = logging.getLogger(LoggingConfig.NAME) class SpritesheetData(TypedDict): facesize: Tuple[int, int] @@ -18,7 +18,7 @@ def fetch_config(chara_id: str) -> SpritesheetData: url = f"https://api.atlasacademy.io/raw/JP/svtScript?charaId={chara_id}" LOGGER.debug(f"Loading data for {url}") - response = requests.get(url, timeout=AtlasDefaults.TIMEOUT) + response = requests.get(url, timeout=AtlasAPIConfig.TIMEOUT) LOGGER.debug(f"{response.status_code} - {response.text}") if not response.ok: raise ValueError() @@ -42,8 +42,8 @@ def fetch_config(chara_id: str) -> SpritesheetData: return returndata def fetch_mstsvtjson(): - url = AtlasDefaults.MST_SVT_JSON - filelocation = Paths.IMAGES / "mstsvt.json" + url = AtlasAPIConfig.MST_SVT_JSON + filelocation = PathConfig.IMAGES / "mstsvt.json" if filelocation.exists(): LOGGER.info("Found cached asset for mstsvt.json") @@ -51,7 +51,7 @@ def fetch_mstsvtjson(): LOGGER.debug(f"Loading data for {url}") with open(filelocation, 'wb') as handle: - response = requests.get(url, stream=True, timeout=AtlasDefaults.TIMEOUT) + response = requests.get(url, stream=True, timeout=AtlasAPIConfig.TIMEOUT) status = response.status_code LOGGER.debug(f"{response.status_code} - {response.text}") if status != 200: @@ -62,9 +62,9 @@ def fetch_mstsvtjson(): handle.write(block) def fetch_expression_sheets(basefolder: str, imageid: str): - atlasurl_base = f"https://static.atlasacademy.io/{AtlasDefaults.REGION}/CharaFigure/{imageid}" + atlasurl_base = f"https://static.atlasacademy.io/{AtlasAPIConfig.REGION}/CharaFigure/{imageid}" - savefolder = Paths.IMAGES / basefolder / str(imageid) + savefolder = PathConfig.IMAGES / basefolder / str(imageid) if not savefolder.is_dir(): savefolder.mkdir(exist_ok=True, parents=True) @@ -89,7 +89,7 @@ def fetch_expression_sheets(basefolder: str, imageid: str): LOGGER.debug(f"Loading data for {atlasurl}") with open(filelocation, 'wb') as handle: - response = requests.get(atlasurl, stream=True, timeout=AtlasDefaults.TIMEOUT) + response = requests.get(atlasurl, stream=True, timeout=AtlasAPIConfig.TIMEOUT) status = response.status_code LOGGER.debug(f"{response.status_code} - {response.text}") if status != 200: @@ -107,10 +107,10 @@ def fetch_expression_sheets(basefolder: str, imageid: str): def fetch_data(servantid: int) -> List[str]: - atlasurl = f"https://api.atlasacademy.io/nice/{AtlasDefaults.REGION}/servant/{servantid}?lore=false&lang=en" + atlasurl = f"https://api.atlasacademy.io/nice/{AtlasAPIConfig.REGION}/servant/{servantid}?lore=false&lang=en" LOGGER.debug(f"Loading data for {atlasurl}") - response = requests.get(atlasurl, timeout=AtlasDefaults.TIMEOUT) + response = requests.get(atlasurl, timeout=AtlasAPIConfig.TIMEOUT) LOGGER.debug(f"{response.status_code}") if not response.ok: LOGGER.debug(f"{response.status_code} - {response.text}") diff --git a/atlasimagecomposer/backend/compose.py b/skyeweave/backend/compose.py similarity index 87% rename from atlasimagecomposer/backend/compose.py rename to skyeweave/backend/compose.py index 72726c2..c42bf69 100644 --- a/atlasimagecomposer/backend/compose.py +++ b/skyeweave/backend/compose.py @@ -6,21 +6,21 @@ from collections import Counter from PIL import Image from tqdm.contrib import itertools as tqdm_itertools -from ..config import Paths, Logging +from ..config import PathConfig, LoggingConfig from .atlas import SpritesheetData, fetch_data, fetch_expression_sheets, fetch_config -LOGGER = logging.getLogger(Logging.NAME) +LOGGER = logging.getLogger(LoggingConfig.NAME) def compose(input_id: int, filters: Optional[List[str]] = None): - Paths.IMAGES.mkdir(exist_ok=True) - Paths.OUTPUT.mkdir(exist_ok=True) + PathConfig.IMAGES.mkdir(exist_ok=True) + PathConfig.OUTPUT.mkdir(exist_ok=True) if input_id < 10000: chara_ids = fetch_data(input_id) - savefolder = Paths.OUTPUT / str(input_id) + savefolder = PathConfig.OUTPUT / str(input_id) else: LOGGER.info(f"Processing manually uploaded charaId {input_id}") - savefolder = Paths.OUTPUT / "manual" + savefolder = PathConfig.OUTPUT / "manual" chara_ids = [str(input_id)] if not savefolder.is_dir(): @@ -67,7 +67,7 @@ def process_sprite(images_folder: pathlib.Path, configdata: SpritesheetData, out if i.name == "0.png": initial_row = rowcount - 1 - for x, y in tqdm_itertools.product(range(initial_row, rowcount), range(0, colcount), ascii="-=", desc=f"[PROG] [{Logging.NAME}]", bar_format="{desc} {percentage:3.0f}% |{bar}|"): + for x, y in tqdm_itertools.product(range(initial_row, rowcount), range(0, colcount), ascii="-=", desc=f"[PROG] [{LoggingConfig.NAME}]", bar_format="{desc} {percentage:3.0f}% |{bar}|"): img = generate_sprite(main_sprite, expressions, x, y, configdata) if img is not None: image_idx = save_sprite(img, outputfolder, f"{images_folder.stem}", image_idx) @@ -107,8 +107,10 @@ def save_sprite(image: Image.Image, outputfolder: pathlib.Path, name: str, idx: return idx + 1 def is_empty(img: Image.Image): - data = Counter(img.crop((96, 96, 160, 160)).convert('LA').getdata()) + w, h = img.size + croparea = (w * 0.375, h * 0.375, w * 0.625, h * 0.625 ) + data = Counter(img.crop(croparea).convert('LA').getdata()) LOGGER.debug(f"Counts: {len(data)}") - if len(data) < 10: + if len(data) < 6: return True return False diff --git a/skyeweave/cli/__init__.py b/skyeweave/cli/__init__.py new file mode 100644 index 0000000..af3c5ad --- /dev/null +++ b/skyeweave/cli/__init__.py @@ -0,0 +1 @@ +from .cli import run diff --git a/atlasimagecomposer/cli/cli.py b/skyeweave/cli/cli.py similarity index 57% rename from atlasimagecomposer/cli/cli.py rename to skyeweave/cli/cli.py index abbdf65..7bf31ec 100644 --- a/atlasimagecomposer/cli/cli.py +++ b/skyeweave/cli/cli.py @@ -6,11 +6,11 @@ from typing import List from .. import __version__ from ..backend import compose -from ..config import Paths, AtlasDefaults, Logging +from ..config import AtlasAPIConfig, LoggingConfig, PathConfig from ..utils.filesystem import rmdir from ..utils.disables import disable_tqdm -LOGGER = logging.getLogger(Logging.NAME) +LOGGER = logging.getLogger(LoggingConfig.NAME) # pylint: disable=too-few-public-methods class Arguments(argparse.Namespace): @@ -18,8 +18,9 @@ class Arguments(argparse.Namespace): Default Arguments when calling the CLI """ output: str - id: str - cacheclear: bool + id: int + reset: bool + nocache: bool filter: List[str] timeout: int quiet: bool @@ -30,17 +31,18 @@ def parse_arguments(): Returns arguments and extra arguments separately """ parser = argparse.ArgumentParser( - prog="atlasimagecomposer", + prog="skyeweave", description="CLI tool to automatically generate expression sheets for servants", epilog="If there are any issues during execution, it helps to clear the cache from time to time") - parser.add_argument("--version", action="version", version=f"atlasimagecomposer {__version__}") + parser.add_argument("--version", action="version", version=f"skyeweave {__version__}") parser.add_argument("--output", action="store", type=str, default=None, dest="output", help="Set the output location. This can be an absolute or relative path") - parser.add_argument("--id", action="store", type=str, default=None, dest="id", help="Set the servantId/charaId - Skips user prompt when provided") - parser.add_argument("--filter", action="extend", nargs="+", dest="filter", help='Specify one or more spritesheet ids that will be fetched') - parser.add_argument("--timeout", action="store", type=int, default=None, dest="timeout", help="Set the timeout for all requests towards AtlasAcademy, default is 10s") - parser.add_argument("--clear-cache", action="store_true", default=False, dest="cacheclear", help="Clear cached assets before downloading files") - parser.add_argument("--quiet", "-q", action="store_true", default=False, dest="quiet", help="Disable logging output") + parser.add_argument("--id", action="store", type=int, default=None, dest="id", help="Set the servantId/charaId - Skips user prompt when provided") + parser.add_argument("--filter", action="extend", nargs="+", dest="filter", help='SSpecify which spritesheets will actually be used (one or more charaIds)') + parser.add_argument("--timeout", action="store", type=int, default=None, dest="timeout", help="Set the timeout for all requests towards AtlasAcademy (default: 10s)") + parser.add_argument("--no-cache", action="store_true", default=False, dest="nocache", help="Clear cache for this id, keeping all other files") + parser.add_argument("--reset", action="store_true", default=False, dest="reset", help="Delete any already downloaded assets") + parser.add_argument("--quiet", "-q", action="store_true", default=False, dest="quiet", help="Mute output and hide progress bars") args = Arguments() @@ -48,18 +50,30 @@ def parse_arguments(): return args, extra_args def welcome(): - print("-------------------------------------------------") - print(" Welcome to the FGO Sprite loader and composer ") - print(" developed by Firq ") - print("-------------------------------------------------") + print("-------------------------------------------") + print(" Welcome to skyeweave, an expression sheet ") + print(" composer developed by Firq ") + print("-------------------------------------------") -def run_cli(): +def validate_id(input_id: None | str) -> int: + input_id = input_id or input("Enter servantId or charaId: ") + + try: + if int(input_id) <= 0: + raise ValueError + except ValueError: + LOGGER.error("Servant ID has to be a valid integer above 0") + sys.exit(1) + + return int(input_id) + +def run(): args, _ = parse_arguments() if args.output: - Paths.OUTPUT = pathlib.Path(args.output) + PathConfig.OUTPUT = pathlib.Path(args.output) if args.timeout and args.timeout >= 0: - AtlasDefaults.TIMEOUT = args.timeout + AtlasAPIConfig.TIMEOUT = args.timeout if args.quiet: disable_tqdm() @@ -67,28 +81,21 @@ def run_cli(): else: welcome() - input_id = args.id - if not input_id: - input_id = input("Enter servantId/charaId: ") + input_id = validate_id(args.id) - try: - t = int(input_id) - if t <= 0: - raise ValueError - except ValueError: - LOGGER.error("Servant ID has to be a valid integer above 0") - sys.exit(1) - - input_id = int(input_id) - - if args.cacheclear: - cachepath = Paths.IMAGES / str(input_id) + if args.nocache: + cachepath = PathConfig.IMAGES / str(input_id) if input_id > 10000: - cachepath = Paths.IMAGES / "manual" / str(input_id) + cachepath = PathConfig.IMAGES / "manual" / str(input_id) if cachepath.exists(): rmdir(cachepath) LOGGER.info("Successfully cleared cached assets") else: LOGGER.info("No cache to clear was found, continuing") + if args.reset: + if PathConfig.IMAGES.exists(): + rmdir(PathConfig.IMAGES) + LOGGER.info("Successfully reset local storage") + compose(input_id, args.filter) diff --git a/skyeweave/config/__init__.py b/skyeweave/config/__init__.py new file mode 100644 index 0000000..48e9947 --- /dev/null +++ b/skyeweave/config/__init__.py @@ -0,0 +1 @@ +from .config import AtlasAPIConfig, ExpressionDefaults, LoggingConfig, PathConfig diff --git a/atlasimagecomposer/config/config.py b/skyeweave/config/config.py similarity index 69% rename from atlasimagecomposer/config/config.py rename to skyeweave/config/config.py index dbb5a08..c4bf065 100644 --- a/atlasimagecomposer/config/config.py +++ b/skyeweave/config/config.py @@ -2,21 +2,21 @@ import os import pathlib -class Logging: - _level = os.environ.get("AIC_STDOUT_LEVEL", "info") +class LoggingConfig: + _level = os.environ.get("SKYEWEAVE_STDOUT_LEVEL", "info") LEVEL = int(_level) if _level.isdigit() else _level.upper() - NAME = "atlasimagecomposer" + NAME = "skyeweave" -class Paths: +class PathConfig: _root = pathlib.Path(__file__).parents[1] IMAGES = _root / ".temp" - OUTPUT = _root / ".out" + OUTPUT = pathlib.Path.cwd() / "output" class ExpressionDefaults: FACESIZE = 256 SHEETSIZE = 1024 -class AtlasDefaults: +class AtlasAPIConfig: REGION = "JP" TIMEOUT = 10 MST_SVT_JSON = "https://git.atlasacademy.io/atlasacademy/fgo-game-data/raw/branch/JP/master/mstSvtScript.json" diff --git a/atlasimagecomposer/py.typed b/skyeweave/py.typed similarity index 100% rename from atlasimagecomposer/py.typed rename to skyeweave/py.typed diff --git a/atlasimagecomposer/utils/__init__.py b/skyeweave/utils/__init__.py similarity index 100% rename from atlasimagecomposer/utils/__init__.py rename to skyeweave/utils/__init__.py diff --git a/atlasimagecomposer/utils/disables.py b/skyeweave/utils/disables.py similarity index 78% rename from atlasimagecomposer/utils/disables.py rename to skyeweave/utils/disables.py index e9680a0..8a3cb2f 100644 --- a/atlasimagecomposer/utils/disables.py +++ b/skyeweave/utils/disables.py @@ -1,3 +1,4 @@ +# pylint: disable=import-outside-toplevel def disable_tqdm(): from tqdm import tqdm from functools import partialmethod diff --git a/atlasimagecomposer/utils/filesystem.py b/skyeweave/utils/filesystem.py similarity index 99% rename from atlasimagecomposer/utils/filesystem.py rename to skyeweave/utils/filesystem.py index d01b49c..752ef02 100644 --- a/atlasimagecomposer/utils/filesystem.py +++ b/skyeweave/utils/filesystem.py @@ -1,6 +1,5 @@ import pathlib - def rmdir(directory: pathlib.Path): """ Recursively deletes all files and folders in a given directory diff --git a/atlasimagecomposer/utils/logger.py b/skyeweave/utils/logger.py similarity index 66% rename from atlasimagecomposer/utils/logger.py rename to skyeweave/utils/logger.py index 752540f..45303f6 100644 --- a/atlasimagecomposer/utils/logger.py +++ b/skyeweave/utils/logger.py @@ -1,15 +1,15 @@ import logging import sys -from ..config import Logging +from ..config import LoggingConfig from .disables import disable_tqdm def init_logger(): - if Logging.LEVEL == "DEBUG": + if LoggingConfig.LEVEL == "DEBUG": disable_tqdm() - logger = logging.getLogger(Logging.NAME) - logger.setLevel(Logging.LEVEL) + logger = logging.getLogger(LoggingConfig.NAME) + logger.setLevel(LoggingConfig.LEVEL) handler = logging.StreamHandler(stream=sys.stdout) formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s') handler.setFormatter(formatter)