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 untrusted user who does not match committer: 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
Example on Windows:
Windows (PowerShell):
```shell
```powershell
$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"
authors = [{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 = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
@ -57,6 +58,7 @@ ignore-paths="test/*"
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
strict = true
exclude = [ "test" ]
[tool.pytest.ini_options]

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import argparse
import logging
import pathlib
import sys
from typing import List
from typing import List, Optional, Sequence, Tuple
from .. import __version__
from ..service import SkyeWeave
@ -16,15 +16,15 @@ class CLIArguments(argparse.Namespace):
"""
Default Arguments when calling the CLI
"""
output: str
id: int
reset: bool
nocache: bool
filter: List[str]
timeout: int
quiet: bool
output: Optional[str]
id: Optional[int]
reset: Optional[bool]
nocache: Optional[bool]
filter: Optional[List[str]]
timeout: Optional[int]
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
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("--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(" Welcome to skyeweave, an expression sheet ")
print(" composer developed by Firq ")
@ -63,7 +63,7 @@ def validate_id(input_id: None | str) -> int:
return int(input_id)
def run():
def run() -> None:
args, _ = parse_arguments(sys.argv[1:])
if args.output:
PathConfig.OUTPUT = pathlib.Path(args.output)
@ -75,7 +75,7 @@ def run():
else:
__welcome()
input_id = validate_id(args.id)
input_id = validate_id(str(args.id))
if args.nocache:
cachepath = PathConfig.IMAGES / str(input_id)
@ -91,6 +91,7 @@ def run():
rmdir(PathConfig.IMAGES)
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.compose()

View file

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

View file

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

View file

@ -45,7 +45,7 @@ def fetch_config(chara_id: int) -> SpritesheetData:
LOGGER.debug(returndata)
return returndata
def fetch_mstsvtjson():
def fetch_mstsvtjson() -> None:
url = AtlasAPIConfig.MST_SVT_JSON
filelocation = PathConfig.IMAGES / "mstsvt.json"
@ -65,7 +65,7 @@ def fetch_mstsvtjson():
break
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}"
savefolder = tempfolder / str(imageid)

View file

@ -1,6 +1,6 @@
import logging
import pathlib
from typing import Dict, List, Optional, TypedDict
from typing import Dict, List, Optional, Tuple, TypedDict
from collections import Counter
from PIL import Image
@ -19,7 +19,7 @@ class SkyeWeave:
output_folder: pathlib.Path
image_folder: pathlib.Path
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):
_output_folder = output or PathConfig.OUTPUT
@ -42,20 +42,20 @@ class SkyeWeave:
self.output_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:
expfolder = fetch_expression_sheets(self.image_folder, char_id)
config = fetch_config(char_id)
self.chara_infos.update({char_id : { "folder": expfolder, "config": config}})
LOGGER.debug(self.chara_infos)
def compose(self):
def compose(self) -> None:
for key, val in self.chara_infos.items():
LOGGER.info(f"Processing sheet for {key}")
self.process_sprite(val["folder"], val["config"], self.output_folder)
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")
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")
@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]
LOGGER.debug(f"{height} | {facesize[1]} --> {rowcount}")
LOGGER.debug(f"{width} | {facesize[0]} --> {colcount}")
return rowcount, colcount
@staticmethod
def _gen_main_sprite(imagepath: pathlib.Path):
def _gen_main_sprite(imagepath: pathlib.Path) -> Image.Image:
image = Image.open(imagepath)
width, height = image.size
LOGGER.debug(f"Main sprite ({imagepath}): {width}:{height}")
@ -126,7 +126,7 @@ class SkyeWeave:
return idx + 1
@staticmethod
def _is_empty(img: Image.Image):
def _is_empty(img: Image.Image) -> bool:
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())

View file

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

View file

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

View file

@ -4,17 +4,17 @@ import sys
from ..config import LoggingConfig
def disable_tqdm():
def disable_tqdm() -> None:
from tqdm import tqdm
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":
disable_tqdm()
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_formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s')