Jon-Becker / nft-generator-py

This tool allows users to generate sets of unique images and metadata using weighted layer collections.
MIT License
301 stars 127 forks source link

Invalid layer weight: '10.0'. Expected type: <class 'int'> #56

Closed crush0x closed 9 months ago

crush0x commented 10 months ago

Hi,

I've just started playing around with nft-generator-py, great work! I'm currently running into an error when trying to validate my config file, which is as the title of the issue suggests: "Invalid layer weight: '10.0'. Expected type: <class 'int'>"

All of the weights are being generated as floating point, even if there are only 5 traits in a particular folder (e.g. weights are then: [20.0, 20.0, 20.0, 20.0, 20.0], instead of [20, 20, 20, 20, 20], and the validation stage seems to consider this an error:

raise ConfigValidationError(
src.common.exceptions.ConfigValidationError: config["layers"][0]["weights"][0]: Invalid layer weight: '10.0'. Expected type: <class 'int'>

Is there any known way to avoid this issue?

Jon-Becker commented 10 months ago

This should be covered by the above PR, i'll take a look and make a release.

crush0x commented 10 months ago

This should be covered by the above PR, i'll take a look and make a release.

nice one, I'll check that out cheers

crush0x commented 10 months ago

just tried adding that line weight.append(int((100 / len(item_list[i])))) into config.py, and it gives me a ZeroDivisionError btw

crush0x commented 9 months ago

I believe the ZeroDivisionError is happening because with some trait folders in my collection there are for example 150 traits (I can probably solve this by further separating my layers), so the weight is a floating point less than 1, and any float less than 1 is converted to 0 by Python's int method.

We could use the math library and specifically the math.ceil method, although since weights need to add up to 100 that could pose a different problem, and would break with trait folders with more than 100 items.

Perhaps the simplest fix here is just to specify that trait folders cannot hold more than 100 items, and keep the new int conversion, although, thinking about it, if the weights must add up to 100 and the int method rounds down, then this will fail on quite a lot of folder sizes, unless I'm missing something.

crush0x commented 9 months ago

Here's updated code that will handle trait folders of more than 100, while also ensuring that the weight folder will always equal 100. This ensures that no matter what the trait folder size is, the outputted weights will always be an array of integers.

It does this by calculating the floating points first (before rounding), keeping track of the sum of the rounded weights (and the difference from 100), and then distributing back the difference, so the final weight adds up to 100.

This will still of course cause any trait folder that contains more than 100 items to leave some weights as equal to zero (but avoids the ZeroDivisionError). If this or something like this works for you, I'd also add a caveat in the README / docs that trait folders shouldn't contain more than 100 items.

import json
import os

from src.utils.io import list_full_dir, list_name
from src.utils.logger import get_logger

def generate_config(trait_dir: str, output: str, verbose: int) -> None:
    logger = get_logger(verbose)
    layerlist = list_name(f"{trait_dir}/*")
    path_list = list_full_dir(f"{trait_dir}/")
    item_list = [list_name(items + "/*") for items in path_list]

    weightlist = [] * len(layerlist)

    for i in range(len(item_list)):
        float_weights = [(100 / len(item_list[i])) for _ in range(len(item_list[i]))]
        int_weights = [int(w) for w in float_weights]

        # Find the difference between the sum of int_weights and 100
        diff = 100 - sum(int_weights)

        # Distribute the difference back to make the sum of weights 100
        for j in range(diff):
            int_weights[j] += 1

        weightlist.append(int_weights)

        for j in range(len(item_list[i])):
            item_list[i][j] = item_list[i][j].split(".")[0]

    # generate json blob
    finalized_layers = []
    for x in range(len(layerlist)):
        layer = {
            "name": layerlist[x],
            "values": item_list[x],
            "trait_path": path_list[x],
            "filename": item_list[x],
            "weights": weightlist[x],
        }
        finalized_layers.append(layer)

    config = {
        "layers": finalized_layers,
        "incompatibilities": [],
        "baseURI": "TODO",
        "name": "TODO",
        "description": "TODO",
    }

    # ensure the directory exists for the output file
    try:
        os.makedirs(os.path.dirname(output), exist_ok=True)
    except OSError:
        pass

    with open(output, "w") as outfile:
        json.dump(config, outfile, indent=4)

    logger.info(f"Generated config file at {output}")
    logger.warning(
        "You'll need to manually update the baseURI, name, and description fields."
    )
faea726 commented 9 months ago

Here's updated code that will handle trait folders of more than 100, while also ensuring that the weight folder will always equal 100. This ensures that no matter what the trait folder size is, the outputted weights will always be an array of integers.

It does this by calculating the floating points first (before rounding), keeping track of the sum of the rounded weights (and the difference from 100), and then distributing back the difference, so the final weight adds up to 100.

If this is more than 100 traits, int number will not qualified anymore. You should try to improve the generate function instead of build_config.

crush0x commented 9 months ago

@faea726 yeah I mean I initially just wanted to use an open-source NFT generator rather than write one