bilegentile's picture
Upload folder using huggingface_hub
c19ca42 verified
from __future__ import annotations
import json
import re
from enum import Enum
from typing import Dict, Generic, List, Literal, Tuple, Type, TypedDict, TypeVar, Union
import numpy as np
from nodes.log import logger
import navi
from nodes.base_input import BaseInput, InputConversion
from ...impl.blend import BlendMode
from ...impl.color.color import Color
from ...impl.dds.format import DDSFormat
from ...impl.image_utils import FillColor, normalize
from ...impl.upscale.auto_split_tiles import TileSize
from ...utils.format import format_color_with_channels
from ...utils.seed import Seed
from ...utils.utils import (
join_pascal_case,
join_space_case,
split_pascal_case,
split_snake_case,
)
from .numeric_inputs import NumberInput
class UntypedOption(TypedDict):
option: str
value: str | int
class TypedOption(TypedDict):
option: str
value: str | int
type: navi.ExpressionJson
DropDownOption = Union[UntypedOption, TypedOption]
DropDownStyle = Literal["dropdown", "checkbox"]
"""
This specified the preferred style in which the frontend may display the dropdown.
- `dropdown`: This is the default style. The dropdown will simply be displayed as a dropdown.
- `checkbox`: If the dropdown has 2 options, then it will be displayed as a checkbox.
The first option will be interpreted as the yes/true option while the second option will be interpreted as the no/false option.
"""
class DropDownInput(BaseInput):
"""Input for a dropdown"""
def __init__(
self,
input_type: navi.ExpressionJson,
label: str,
options: List[DropDownOption],
default_value: str | int | None = None,
preferred_style: DropDownStyle = "dropdown",
associated_type: Union[Type, None] = None,
):
super().__init__(input_type, label, kind="dropdown", has_handle=False)
self.options = options
self.accepted_values = {o["value"] for o in self.options}
self.default = (
default_value if default_value is not None else options[0]["value"]
)
self.preferred_style: DropDownStyle = preferred_style
if self.default not in self.accepted_values:
logger.error(f"chaiNNer: invalid default value {self.default} in {label} dropdown. Using first value instead.")
self.default = options[0]["value"]
self.associated_type = (
associated_type if associated_type is not None else type(self.default)
)
def toDict(self):
return {
**super().toDict(),
"options": self.options,
"def": self.default,
"preferredStyle": self.preferred_style,
}
def make_optional(self):
raise ValueError("DropDownInput cannot be made optional")
def enforce(self, value):
assert value in self.accepted_values, f"{value} is not a valid option"
return value
class BoolInput(DropDownInput):
def __init__(self, label: str, default: bool = True):
super().__init__(
input_type="bool",
label=label,
default_value=int(default),
options=[
{
"option": "Yes",
"value": int(True), # 1
"type": "true",
},
{
"option": "No",
"value": int(False), # 0
"type": "false",
},
],
preferred_style="checkbox",
)
self.associated_type = bool
def enforce(self, value) -> bool:
value = super().enforce(value)
return bool(value)
T = TypeVar("T", bound=Enum)
class EnumInput(Generic[T], DropDownInput):
"""
This adapts a python Enum into a chaiNNer dropdown input.
### Features
All variants of the enum will be converted into typed dropdown options.
The dropdown will be fully typed and bring its own type definitions.
Option labels can be (partially) overridden using `option_labels`.
By default, the input label, type names, and option labels will all be generated from the enum name and variant names.
All of those defaults can be overridden.
Options will be ordered by declaration order in the python enum definition.
### Requirements
The value of each variant has to be either `str` or `int`.
Other types are not permitted.
"""
def __init__(
self,
enum: Type[T],
label: str | None = None,
default: T | None = None,
type_name: str | None = None,
option_labels: Dict[T, str] | None = None,
extra_definitions: str | None = None,
):
if type_name is None:
type_name = enum.__name__
if label is None:
label = join_space_case(split_pascal_case(type_name))
if option_labels is None:
option_labels = {}
options: List[DropDownOption] = []
variant_types: List[str] = []
for variant in enum:
value = variant.value
assert isinstance(value, (int, str))
assert (
re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", variant.name) is not None
), f"Expected the name of {enum.__name__}.{variant.name} to be snake case."
name = split_snake_case(variant.name)
variant_type = f"{type_name}::{join_pascal_case(name)}"
option_label = option_labels.get(variant, join_space_case(name))
variant_types.append(variant_type)
options.append(
{"option": option_label, "value": value, "type": variant_type}
)
super().__init__(
input_type=type_name,
label=label,
options=options,
default_value=default.value if default is not None else None,
)
self.type_definitions = (
f"let {type_name} = {' | '.join(variant_types)};\n"
+ "\n".join([f"struct {t};" for t in variant_types])
+ (extra_definitions or "")
)
self.type_name: str = type_name
self.enum = enum
self.associated_type = enum
def enforce(self, value) -> T:
value = super().enforce(value)
return self.enum(value)
class TextInput(BaseInput):
"""Input for arbitrary text"""
def __init__(
self,
label: str,
has_handle=True,
min_length: int = 0,
max_length: Union[int, None] = None,
placeholder: Union[str, None] = None,
multiline: bool = False,
allow_numbers: bool = True,
default: Union[str, None] = None,
hide_label: bool = False,
allow_empty_string: bool = False,
):
super().__init__(
input_type="string" if min_length == 0 else 'invStrSet("")',
label=label,
has_handle=has_handle,
kind="text",
)
self.min_length = min_length
self.max_length = max_length
self.placeholder = placeholder
self.default = default
self.multiline = multiline
self.hide_label = hide_label
self.allow_empty_string = allow_empty_string
if default is not None:
assert default != ""
assert min_length < len(default)
assert max_length is None or len(default) < max_length
self.associated_type = str
if allow_numbers:
self.input_conversions = [InputConversion("number", "toString(Input)")]
def enforce(self, value) -> str:
if isinstance(value, float) and int(value) == value:
# stringify integers values
value = str(int(value))
else:
value = str(value)
# enforce length range
if self.max_length is not None and len(value) > self.max_length:
value = value[: self.max_length]
if len(value) < self.min_length:
raise ValueError(
f"Text value of input '{self.label}' must be at least {self.min_length} characters long,"
f" but found {len(value)} ('{value}')."
)
return value
def toDict(self):
return {
**super().toDict(),
"minLength": self.min_length,
"maxLength": self.max_length,
"placeholder": self.placeholder,
"multiline": self.multiline,
"def": self.default,
"hideLabel": self.hide_label,
"allowEmptyString": self.allow_empty_string,
}
class ClipboardInput(BaseInput):
"""Input for pasting from clipboard"""
def __init__(self, label: str = "Clipboard input"):
super().__init__(["Image", "string", "number"], label, kind="text")
self.input_conversions = [InputConversion("Image", '"<Image>"')]
def enforce(self, value):
if isinstance(value, np.ndarray):
return normalize(value)
if isinstance(value, float) and int(value) == value:
# stringify integers values
return str(int(value))
return str(value)
class AnyInput(BaseInput):
def __init__(self, label: str):
super().__init__(input_type="any", label=label)
self.associated_type = object
def enforce_(self, value):
# The behavior for optional inputs and None makes sense for all inputs except this one.
return value
class SeedInput(NumberInput):
def __init__(self, label: str = "Seed", has_handle: bool = True):
super().__init__(
label=label,
minimum=None,
maximum=None,
precision=0,
default=0,
)
self.has_handle = has_handle
self.input_type = "Seed | int"
self.input_conversions = [InputConversion("int", "Seed")]
self.input_adapt = """
match Input {
int => Seed,
_ => never
}
"""
self.associated_type = Seed
def enforce(self, value) -> Seed:
if isinstance(value, Seed):
return value
return Seed(int(value))
def make_optional(self):
raise ValueError("SeedInput cannot be made optional")
class ColorInput(BaseInput):
def __init__(
self,
label: str = "Color",
default: Color | None = None,
channels: int | List[int] | None = None,
):
super().__init__(
input_type=navi.Color(channels=channels),
label=label,
has_handle=True,
kind="color",
)
self.input_adapt = """
match Input {
string => parseColorJson(Input),
_ => never
}
"""
self.channels: List[int] | None = (
[channels] if isinstance(channels, int) else channels
)
if self.channels is None:
if default is None:
default = Color.bgr((0.5, 0.5, 0.5))
else:
assert len(self.channels) >= 0
if default is None:
if 3 in self.channels:
default = Color.bgr((0.5, 0.5, 0.5))
elif 4 in self.channels:
default = Color.bgra((0.5, 0.5, 0.5, 1))
elif 1 in self.channels:
default = Color.gray(0.5)
else:
raise ValueError("Cannot find default color value")
else:
assert (
default.channels in self.channels
), "The default color is not accepted."
self.default: Color = default
self.associated_type = Color
def enforce(self, value) -> Color:
if isinstance(value, str):
# decode color JSON strings from the frontend
value = Color.from_json(json.loads(value))
assert isinstance(value, Color)
if self.channels is not None and value.channels not in self.channels:
expected = format_color_with_channels(self.channels, plural=True)
actual = format_color_with_channels([value.channels])
raise ValueError(
f"The input {self.label} only supports {expected} but was given {actual}."
)
return value
def toDict(self):
return {
**super().toDict(),
"def": json.dumps(self.default.to_json()),
"channels": self.channels,
}
def make_optional(self):
raise ValueError("ColorInput cannot be made optional")
def IteratorInput():
"""Input for showing that an iterator automatically handles the input"""
return BaseInput("IteratorAuto", "Auto (Iterator)", has_handle=False)
class VideoContainer(Enum):
MKV = "mkv"
MP4 = "mp4"
MOV = "mov"
WEBM = "webm"
AVI = "avi"
GIF = "gif"
NONE = "none"
VIDEO_CONTAINERS = {
VideoContainer.MKV: "mkv",
VideoContainer.MP4: "mp4",
VideoContainer.MOV: "mov",
VideoContainer.WEBM: "WebM",
VideoContainer.AVI: "avi",
VideoContainer.GIF: "GIF",
VideoContainer.NONE: "None",
}
VIDEO_NONE_CONTAINERS: List[VideoContainer] = [VideoContainer.NONE, VideoContainer.GIF]
def VideoNoneContainerDropdown() -> DropDownInput:
return DropDownInput(
input_type="VideoContainer",
label="Container",
options=[
{"option": VIDEO_CONTAINERS[vc], "value": vc.value}
for vc in VIDEO_NONE_CONTAINERS
],
associated_type=VideoContainer,
)
VIDEO_FFV1_CONTAINERS: List[VideoContainer] = [VideoContainer.MKV]
def VideoFfv1ContainerDropdown() -> DropDownInput:
return DropDownInput(
input_type="VideoContainer",
label="Container",
options=[
{"option": VIDEO_CONTAINERS[vc], "value": vc.value}
for vc in VIDEO_FFV1_CONTAINERS
],
associated_type=VideoContainer,
)
VIDEO_VP9_CONTAINERS: List[VideoContainer] = [
VideoContainer.WEBM,
VideoContainer.MP4,
VideoContainer.MKV,
]
def VideoVp9ContainerDropdown() -> DropDownInput:
return DropDownInput(
input_type="VideoContainer",
label="Container",
options=[
{"option": VIDEO_CONTAINERS[vc], "value": vc.value}
for vc in VIDEO_VP9_CONTAINERS
],
associated_type=VideoContainer,
)
VIDEO_H264_CONTAINERS: List[VideoContainer] = [
VideoContainer.MKV,
VideoContainer.MP4,
VideoContainer.MOV,
VideoContainer.AVI,
]
def VideoH264ContainerDropdown() -> DropDownInput:
return DropDownInput(
input_type="VideoContainer",
label="Container",
options=[
{"option": VIDEO_CONTAINERS[vc], "value": vc.value}
for vc in VIDEO_H264_CONTAINERS
],
associated_type=VideoContainer,
)
VIDEO_H265_CONTAINERS: List[VideoContainer] = [
VideoContainer.MKV,
VideoContainer.MP4,
VideoContainer.MOV,
]
def VideoH265ContainerDropdown() -> DropDownInput:
return DropDownInput(
input_type="VideoContainer",
label="Container",
options=[
{"option": VIDEO_CONTAINERS[vc], "value": vc.value}
for vc in VIDEO_H265_CONTAINERS
],
associated_type=VideoContainer,
)
class VideoEncoder(Enum):
H264 = "libx264"
H265 = "libx265"
VP9 = "libvpx-vp9"
FFV1 = "ffv1"
NONE = "none"
VIDEO_ENCODER_LABELS = {
VideoEncoder.H264: "H.264 (AVC)",
VideoEncoder.H265: "H.265 (HEVC)",
VideoEncoder.VP9: "VP9",
VideoEncoder.FFV1: "FFV1",
VideoEncoder.NONE: "None",
}
def VideoEncoderDropdown() -> DropDownInput:
return DropDownInput(
input_type="VideoEncoder",
label="Encoder",
options=[
{"option": label, "value": vc.value}
for vc, label in VIDEO_ENCODER_LABELS.items()
],
default_value=VideoEncoder.H264.value,
associated_type=VideoEncoder,
)
def VideoPresetDropdown() -> DropDownInput:
"""Video Type option dropdown"""
return DropDownInput(
input_type="VideoPreset",
label="Preset",
options=[
{"option": "ultrafast", "value": "ultrafast"},
{"option": "superfast", "value": "superfast"},
{"option": "veryfast", "value": "veryfast"},
{"option": "fast", "value": "fast"},
{"option": "medium", "value": "medium"},
{"option": "slow", "value": "slow"},
{"option": "slower", "value": "slower"},
{"option": "veryslow", "value": "veryslow"},
],
)
def BlendModeDropdown() -> DropDownInput:
"""Blending Mode option dropdown"""
return EnumInput(
BlendMode,
option_labels={
BlendMode.ADD: "Linear Dodge (Add)",
},
)
def FillColorDropdown() -> DropDownInput:
return EnumInput(
FillColor,
label="Negative Space Fill",
default=FillColor.AUTO,
extra_definitions="""
def FillColor::getOutputChannels(fill: FillColor, channels: uint) {
match fill {
FillColor::Transparent => 4,
_ => channels
}
}
""",
)
def TileSizeDropdown(
label="Tile Size", estimate=True, default: TileSize | None = None
) -> DropDownInput:
options = []
if estimate:
options.append({"option": "Auto (estimate)", "value": 0})
options.append({"option": "Maximum", "value": -2})
options.append({"option": "No Tiling", "value": -1})
for size in [128, 192, 256, 384, 512, 768, 1024, 2048, 4096]:
options.append({"option": str(size), "value": size})
return DropDownInput(
input_type="TileSize",
label=label,
options=options,
associated_type=TileSize,
default_value=default,
)
SUPPORTED_DDS_FORMATS: List[Tuple[DDSFormat, str]] = [
("BC1_UNORM_SRGB", "BC1 (4bpp, sRGB, 1-bit Alpha)"),
("BC1_UNORM", "BC1 (4bpp, Linear, 1-bit Alpha)"),
("BC3_UNORM_SRGB", "BC3 (8bpp, sRGB, 8-bit Alpha)"),
("BC3_UNORM", "BC3 (8bpp, Linear, 8-bit Alpha)"),
("BC4_UNORM", "BC4 (4bpp, Grayscale)"),
("BC5_UNORM", "BC5 (8bpp, Unsigned, 2-channel normal)"),
("BC5_SNORM", "BC5 (8bpp, Signed, 2-channel normal)"),
("BC7_UNORM_SRGB", "BC7 (8bpp, sRGB, 8-bit Alpha)"),
("BC7_UNORM", "BC7 (8bpp, Linear, 8-bit Alpha)"),
("DXT1", "DXT1 (4bpp, Linear, 1-bit Alpha, Legacy)"),
("DXT3", "DXT3 (8bpp, Linear, 4-bit Alpha, Legacy)"),
("DXT5", "DXT5 (8bpp, Linear, 8-bit Alpha, Legacy)"),
("R8G8B8A8_UNORM_SRGB", "RGBA (32bpp, sRGB, 8-bit Alpha)"),
("R8G8B8A8_UNORM", "RGBA (32bpp, Linear, 8-bit Alpha)"),
("B8G8R8A8_UNORM_SRGB", "BGRA (32bpp, sRGB, 8-bit Alpha)"),
("B8G8R8A8_UNORM", "BGRA (32bpp, Linear, 8-bit Alpha)"),
("B5G5R5A1_UNORM", "BGRA (16bpp, Linear, 1-bit Alpha)"),
("B5G6R5_UNORM", "BGR (16bpp, Linear)"),
("B8G8R8X8_UNORM_SRGB", "BGRX (32bpp, sRGB)"),
("B8G8R8X8_UNORM", "BGRX (32bpp, Linear)"),
("R8G8_UNORM", "RG (16bpp, Linear)"),
("R8_UNORM", "R (8bpp, Linear)"),
]
def DdsFormatDropdown() -> DropDownInput:
return DropDownInput(
input_type="DdsFormat",
label="DDS Format",
options=[{"option": title, "value": f} for f, title in SUPPORTED_DDS_FORMATS],
associated_type=DDSFormat,
)
def DdsMipMapsDropdown() -> DropDownInput:
return DropDownInput(
input_type="DdsMipMaps",
label="Generate Mip Maps",
preferred_style="checkbox",
options=[
# these are not boolean values, see dds.py for more info
{"option": "Yes", "value": 0},
{"option": "No", "value": 1},
],
)