Updated readme, fixed linting and type annotations

This commit is contained in:
Firq 2025-04-04 11:33:49 +02:00
parent acc05a99bb
commit b701a91083
Signed by: Firq
GPG key ID: 3ACC61C8CEC83C20
12 changed files with 84 additions and 33 deletions

View file

@ -60,10 +60,48 @@ If there are any issues with `skyeweave`, feel free to reach out to me using the
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 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: Windows (PowerShell):
```shell ```powershell
$env:SKYEWEAVE_STDOUT_LEVEL="debug" $env:SKYEWEAVE_STDOUT_LEVEL="debug"
skyeweave --output out --id 70 > log.log skyeweave --output out --id 70 2>&1 log.log
``` ```
Linux (Bash):
```bash
SKYEWEAVE_STDOUT_LEVEL="debug"
skyeweave --output out --id 70 2>&1 log.log
```
## Contributing
Feel free to reach out if you want to help to improve skyeweave. I really appreachiate it.
## FAQ
> Q: Why Python
A: Because it is the language I am the most familiar with, and I know my stuff. I also like not having to deal with too many restrictions when developing or the mess that is JS/TS.
> Q: Why a CLI script?
A: Because it felt like the appropriate solution for this problem. Writing a whole GUI application felt overkill, and for the most part having the tool just spittingt out the necessary files is more than enough.
> Q: Where is the executable? Why do I have to install Python?
A: ~~Because I said so!~~ In all honesty, I felt that it's too much work for too little return building executables for multiple systems, expecially with `pyinstaller` resulting in a large executable for what is actually happening. Installing Python is pretty straightforward, and I have attached guidance for setting up from scratch below. Also: See [this gem](https://www.reddit.com/r/github/comments/1at9br4) for another reason.
> Q: How did this come to be?
A: I am usually editing thumbnails for my videos with the expression sheets, and at some point I was just annoyed as the process of editing those for EVERY. SINGLE. SERVANT. IN. EVERY. THUMBNAIL.
## How to install Python?
1. Go to [the download page](https://www.python.org/downloads/) and download a current version of Python (must be version 3.10 or later for this package to work).
2. Install Python as you would any other program, make sure it gets added to PATH
3. Open a Terminal (cmd, poweshell on Windows; bash, sh, ... on Linux) and run `python --version` once to verify it installed correctly. There should be some output like `Python 3.11.9`
After this, you can continue with the instructions above. I highly recommend you check out of virtual environments (venv) work beofre installing, so that you don't pollute the global package installation directory.

View file

@ -4,13 +4,14 @@ version = "1.0.0-c.4"
requires-python = ">= 3.10" requires-python = ">= 3.10"
authors = [{name = "Firq", email = "firelp42@gmail.com"}] authors = [{name = "Firq", email = "firelp42@gmail.com"}]
maintainers = [{name = "Firq", email = "firelp42@gmail.com"}] maintainers = [{name = "Firq", email = "firelp42@gmail.com"}]
description = "Easily generate any FGO expression sheets from an id" description = "Helper script to easily generate experssions from FGO expression sheets"
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
] ]
dependencies = [ dependencies = [
@ -57,6 +58,7 @@ ignore-paths="test/*"
python_version = "3.11" python_version = "3.11"
warn_return_any = true warn_return_any = true
warn_unused_configs = true warn_unused_configs = true
strict = true
exclude = [ "test" ] exclude = [ "test" ]
[tool.pytest.ini_options] [tool.pytest.ini_options]

View file

@ -3,3 +3,5 @@ import importlib.metadata
__version__ = importlib.metadata.version(__package__ or "skyeweave") __version__ = importlib.metadata.version(__package__ or "skyeweave")
from .service import SkyeWeave from .service import SkyeWeave
__all__ = [ "SkyeWeave" ]

View file

@ -1 +1,3 @@
from .cli import run from .cli import run
__all__ = [ "run" ]

View file

@ -2,7 +2,7 @@ import argparse
import logging import logging
import pathlib import pathlib
import sys import sys
from typing import List from typing import List, Optional, Sequence, Tuple
from .. import __version__ from .. import __version__
from ..service import SkyeWeave from ..service import SkyeWeave
@ -16,15 +16,15 @@ class CLIArguments(argparse.Namespace):
""" """
Default Arguments when calling the CLI Default Arguments when calling the CLI
""" """
output: str output: Optional[str]
id: int id: Optional[int]
reset: bool reset: Optional[bool]
nocache: bool nocache: Optional[bool]
filter: List[str] filter: Optional[List[str]]
timeout: int timeout: Optional[int]
quiet: bool quiet: Optional[bool]
def parse_arguments(arguments): def parse_arguments(arguments: Sequence[str]) -> Tuple[CLIArguments, list[str]]:
""" """
Create a parser and parse the arguments of sys.argv based on the Arguments Namespace Create a parser and parse the arguments of sys.argv based on the Arguments Namespace
Returns arguments and extra arguments separately Returns arguments and extra arguments separately
@ -43,9 +43,9 @@ def parse_arguments(arguments):
parser.add_argument("--reset", action="store_true", default=False, dest="reset", help="Delete any already downloaded assets") 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") parser.add_argument("--quiet", "-q", action="store_true", default=False, dest="quiet", help="Mute output and hide progress bars")
return parser.parse_known_args(arguments, namespace=CLIArguments) return parser.parse_known_args(arguments, namespace=CLIArguments())
def __welcome(): def __welcome() -> None:
print("-------------------------------------------") print("-------------------------------------------")
print(" Welcome to skyeweave, an expression sheet ") print(" Welcome to skyeweave, an expression sheet ")
print(" composer developed by Firq ") print(" composer developed by Firq ")
@ -63,7 +63,7 @@ def validate_id(input_id: None | str) -> int:
return int(input_id) return int(input_id)
def run(): def run() -> None:
args, _ = parse_arguments(sys.argv[1:]) args, _ = parse_arguments(sys.argv[1:])
if args.output: if args.output:
PathConfig.OUTPUT = pathlib.Path(args.output) PathConfig.OUTPUT = pathlib.Path(args.output)
@ -75,7 +75,7 @@ def run():
else: else:
__welcome() __welcome()
input_id = validate_id(args.id) input_id = validate_id(str(args.id))
if args.nocache: if args.nocache:
cachepath = PathConfig.IMAGES / str(input_id) cachepath = PathConfig.IMAGES / str(input_id)
@ -91,6 +91,7 @@ def run():
rmdir(PathConfig.IMAGES) rmdir(PathConfig.IMAGES)
LOGGER.info("Successfully reset local storage") LOGGER.info("Successfully reset local storage")
weaver = SkyeWeave(input_id, args.filter) filters = [int(f) for f in args.filter] if args.filter is not None else None
weaver = SkyeWeave(input_id, filters=filters)
weaver.download() weaver.download()
weaver.compose() weaver.compose()

View file

@ -1 +1,3 @@
from .config import AtlasAPIConfig, ExpressionDefaults, LoggingConfig, PathConfig from .config import AtlasAPIConfig, ExpressionDefaults, LoggingConfig, PathConfig
__all__ = [ "AtlasAPIConfig", "ExpressionDefaults", "LoggingConfig", "PathConfig" ]

View file

@ -1 +1,3 @@
from .compose import SkyeWeave from .compose import SkyeWeave
__all__ = [ "SkyeWeave" ]

View file

@ -45,7 +45,7 @@ def fetch_config(chara_id: int) -> SpritesheetData:
LOGGER.debug(returndata) LOGGER.debug(returndata)
return returndata return returndata
def fetch_mstsvtjson(): def fetch_mstsvtjson() -> None:
url = AtlasAPIConfig.MST_SVT_JSON url = AtlasAPIConfig.MST_SVT_JSON
filelocation = PathConfig.IMAGES / "mstsvt.json" filelocation = PathConfig.IMAGES / "mstsvt.json"
@ -65,7 +65,7 @@ def fetch_mstsvtjson():
break break
handle.write(block) handle.write(block)
def fetch_expression_sheets(tempfolder: pathlib.Path, imageid: int): def fetch_expression_sheets(tempfolder: pathlib.Path, imageid: int) -> pathlib.Path:
atlasurl_base = f"https://static.atlasacademy.io/{AtlasAPIConfig.REGION}/CharaFigure/{imageid}" atlasurl_base = f"https://static.atlasacademy.io/{AtlasAPIConfig.REGION}/CharaFigure/{imageid}"
savefolder = tempfolder / str(imageid) savefolder = tempfolder / str(imageid)

View file

@ -1,6 +1,6 @@
import logging import logging
import pathlib import pathlib
from typing import Dict, List, Optional, TypedDict from typing import Dict, List, Optional, Tuple, TypedDict
from collections import Counter from collections import Counter
from PIL import Image from PIL import Image
@ -19,7 +19,7 @@ class SkyeWeave:
output_folder: pathlib.Path output_folder: pathlib.Path
image_folder: pathlib.Path image_folder: pathlib.Path
chara_ids: List[int] chara_ids: List[int]
chara_infos: Dict[str, CharaInfos] chara_infos: Dict[int, CharaInfos]
def __init__(self, input_id: int, filters: Optional[List[int]] = None, output: Optional[pathlib.Path] = None, assets: Optional[pathlib.Path] = None): def __init__(self, input_id: int, filters: Optional[List[int]] = None, output: Optional[pathlib.Path] = None, assets: Optional[pathlib.Path] = None):
_output_folder = output or PathConfig.OUTPUT _output_folder = output or PathConfig.OUTPUT
@ -42,20 +42,20 @@ class SkyeWeave:
self.output_folder.mkdir(parents=True, exist_ok=True) self.output_folder.mkdir(parents=True, exist_ok=True)
self.image_folder.mkdir(parents=True, exist_ok=True) self.image_folder.mkdir(parents=True, exist_ok=True)
def download(self): def download(self) -> None:
for char_id in self.chara_ids: for char_id in self.chara_ids:
expfolder = fetch_expression_sheets(self.image_folder, char_id) expfolder = fetch_expression_sheets(self.image_folder, char_id)
config = fetch_config(char_id) config = fetch_config(char_id)
self.chara_infos.update({char_id : { "folder": expfolder, "config": config}}) self.chara_infos.update({char_id : { "folder": expfolder, "config": config}})
LOGGER.debug(self.chara_infos) LOGGER.debug(self.chara_infos)
def compose(self): def compose(self) -> None:
for key, val in self.chara_infos.items(): for key, val in self.chara_infos.items():
LOGGER.info(f"Processing sheet for {key}") LOGGER.info(f"Processing sheet for {key}")
self.process_sprite(val["folder"], val["config"], self.output_folder) self.process_sprite(val["folder"], val["config"], self.output_folder)
LOGGER.info(f"Files have been saved at: {self.output_folder.absolute()}") LOGGER.info(f"Files have been saved at: {self.output_folder.absolute()}")
def process_sprite(self, images_folder: pathlib.Path, configdata: SpritesheetData, outputfolder: pathlib.Path): def process_sprite(self, images_folder: pathlib.Path, configdata: SpritesheetData, outputfolder: pathlib.Path) -> None:
main_sprite = self._gen_main_sprite(images_folder / "0.png") main_sprite = self._gen_main_sprite(images_folder / "0.png")
image_idx = self._save_sprite(main_sprite, outputfolder, f"{images_folder.stem}") image_idx = self._save_sprite(main_sprite, outputfolder, f"{images_folder.stem}")
@ -79,14 +79,14 @@ class SkyeWeave:
LOGGER.debug(f"{x}/{y} - {'Invalid' if img is None else 'Valid'} image") LOGGER.debug(f"{x}/{y} - {'Invalid' if img is None else 'Valid'} image")
@staticmethod @staticmethod
def _calculate_counts(width: int, height: int, facesize: tuple[int, int]): def _calculate_counts(width: int, height: int, facesize: tuple[int, int]) -> Tuple[int, int]:
rowcount, colcount = height // facesize[1], width // facesize[0] rowcount, colcount = height // facesize[1], width // facesize[0]
LOGGER.debug(f"{height} | {facesize[1]} --> {rowcount}") LOGGER.debug(f"{height} | {facesize[1]} --> {rowcount}")
LOGGER.debug(f"{width} | {facesize[0]} --> {colcount}") LOGGER.debug(f"{width} | {facesize[0]} --> {colcount}")
return rowcount, colcount return rowcount, colcount
@staticmethod @staticmethod
def _gen_main_sprite(imagepath: pathlib.Path): def _gen_main_sprite(imagepath: pathlib.Path) -> Image.Image:
image = Image.open(imagepath) image = Image.open(imagepath)
width, height = image.size width, height = image.size
LOGGER.debug(f"Main sprite ({imagepath}): {width}:{height}") LOGGER.debug(f"Main sprite ({imagepath}): {width}:{height}")
@ -126,7 +126,7 @@ class SkyeWeave:
return idx + 1 return idx + 1
@staticmethod @staticmethod
def _is_empty(img: Image.Image): def _is_empty(img: Image.Image) -> bool:
w, h = img.size w, h = img.size
croparea = (w * 0.375, h * 0.375, w * 0.625, h * 0.625 ) croparea = (w * 0.375, h * 0.375, w * 0.625, h * 0.625 )
data = Counter(img.crop(croparea).convert('LA').getdata()) data = Counter(img.crop(croparea).convert('LA').getdata())

View file

@ -1,2 +1,4 @@
from .filesystem import rmdir from .filesystem import rmdir
from .logger import LOGGER, disable_tqdm from .logger import LOGGER, disable_tqdm
__all__ = [ "rmdir", "LOGGER", "disable_tqdm" ]

View file

@ -1,6 +1,6 @@
import pathlib import pathlib
def rmdir(directory: pathlib.Path): def rmdir(directory: pathlib.Path) -> None:
""" """
Recursively deletes all files and folders in a given directory Recursively deletes all files and folders in a given directory

View file

@ -4,17 +4,17 @@ import sys
from ..config import LoggingConfig from ..config import LoggingConfig
def disable_tqdm(): def disable_tqdm() -> None:
from tqdm import tqdm from tqdm import tqdm
from functools import partialmethod from functools import partialmethod
tqdm.__init__ = partialmethod(tqdm.__init__, disable=True) tqdm.__init__ = partialmethod(tqdm.__init__, disable=True) # type: ignore[method-assign,assignment]
def __init_logger(): def __init_logger() -> logging.Logger:
if LoggingConfig.LEVEL == "DEBUG": if LoggingConfig.LEVEL == "DEBUG":
disable_tqdm() disable_tqdm()
default_logger = logging.getLogger(LoggingConfig.NAME) default_logger = logging.getLogger(LoggingConfig.NAME)
default_logger.setLevel(LoggingConfig.LEVEL) default_logger.setLevel(LoggingConfig.LEVEL) # type: ignore[arg-type]
default_handler = logging.StreamHandler(stream=sys.stdout) default_handler = logging.StreamHandler(stream=sys.stdout)
default_formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s') default_formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s')