chaiNNer Documentation

A node-based image processing GUI aimed at making chaining image processing tasks easy and customizable. Born as an AI upscaling application, chaiNNer has...

6,095 stars Python Python 11 files ยท ~18,357 tokens
RAW Doc

Technical Documentation: chaiNNer-org/chaiNNer

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

chaiNNer





A node-based image processing GUI aimed at making chaining image processing tasks easy and customizable. Born as an AI upscaling application, chaiNNer has grown into an extremely flexible and powerful programmatic image processing application.

ChaiNNer gives you a level of customization of your image processing workflow that very few others do. Not only do you have full control over your processing pipeline, you can do incredibly complex tasks just by connecting a few nodes together.

ChaiNNer is also cross-platform, meaning you can run it on Windows, MacOS, and Linux.

For help, suggestions, or just to hang out, you can join the chaiNNer Discord server

ChaiNNer is under active development. If you're knowledgeable in TypeScript, React, or Python, feel free to contribute to this project and help us continue to improve it.

Installation

Download the latest release from the Github releases page and run the installer best suited for your system. Simple as that.

You don't even need to have Python installed, as chaiNNer will download an isolated integrated Python build on startup. From there, you can install all the other dependencies via the Dependency Manager.

If you do wish to use your system Python installation still, you can turn the system Python setting on. However, it is much more recommended to use integrated Python. If you do wish to use your system Python, Python 3.10 or later is required (3.11+ recommended).

If you'd like to test the latest changes and tweaks, try out our nightly builds

How To Use

Basic Usage

While it might seem intimidating at first due to all the possible options, chaiNNer is pretty simple to use. For example, this is all you need to do in order to perform an upscale:

Before you get to this point though, you'll need to install one of the neural network frameworks from the dependency manager. You can access this via the button in the upper-right-hand corner. ChaiNNer offers support for PyTorch (with select model architectures), NCNN, ONNX, and TensorRT. For Nvidia users, PyTorch or TensorRT will be the preferred way to upscale. For AMD users, NCNN will be the preferred way to upscale (or PyTorch with ROCm on Linux).

All the other Python dependencies are automatically installed, and chaiNNer even carries its own integrated Python support so that you do not have to modify your existing Python configuration.

Then, all you have to do is drag and drop (or double click) node names in the selection panel to bring them into the editor. Then, drag from one node handle to another to connect the nodes. Each handle is color-coded to its specific type, and while connecting will show you only the compatible connections. This makes it very easy to know what to connect where.

Once you have a working chain set up in the editor, you can press the green "run" button in the top bar to run the chain you have made. You will see the connections between nodes become animated, and start to un-animate as they finish processing. You can stop or pause processing with the red "stop" and yellow "pause" buttons respectively.

Don't forget, there are plenty of non-upscaling tasks you can do with chaiNNer as well!

Tips & Tricks

To select multiple nodes, hold down shift and drag around all the nodes you want to be selected. You can also select an individual node by just clicking on it. When nodes are selected, you can press backspace or delete to delete them from the editor.

To perform batch processing on a folder of images, use the "Load Images" node. To process videos, use the "Load Video" node. It's important to note however that you cannot use both "Load Images" and "Load Video" nodes (or any two nodes that perform batch iteration) together in a chain. You can however combine the output (collector) nodes in the chain, for example using "Save Image" with "Load Video", and "Save Video" with "Load Images".

You can right-click in the editor viewport to show an inline nodes list to select from. You also can get this menu by dragging a connection out to the editor rather than making an actual connection, and it will show compatible nodes to automatically create a connection with.

Helpful Resources

Compatibility Notes

  • MacOS versions 10.x and below are not supported.

  • Windows versions 8.1 and below are also not supported.

  • Apple Silicon Macs are supported with PyTorch MPS acceleration. ONNX only supports the CPU Execution Provider, and NCNN may not work properly on some configurations.

  • Some NCNN users with non-Nvidia GPUs might get all-black outputs. I am not sure what to do to fix this as it appears to be due to the graphics driver crashing as a result of going out of memory. If this happens to you, try manually setting a tiling amount.

  • To use the Clipboard nodes, Linux users need to have xclip or, for wayland users, wl-copy installed.

GPU Support

Nvidia GPUs: Full support via PyTorch (CUDA), ONNX, and TensorRT. TensorRT offers the best performance for supported models.

AMD GPUs:

  • On Linux, AMD GPUs can use PyTorch via ROCm
  • NCNN is available on all platforms for AMD GPUs

Apple Silicon (M1/M2/M3): PyTorch MPS acceleration is supported.

Intel GPUs: NCNN inference is supported for Intel GPUs.

CPU: All frameworks support CPU-only mode as a fallback.

For NCNN, make sure to select which GPU you want to use in the settings. It might be defaulting to your integrated graphics!

Model Architecture Support

ChaiNNer currently supports a limited amount of neural network architectures. More architectures will be supported in the future.

PyTorch

As of v0.21.0, chaiNNer uses our new package called Spandrel to support Pytorch model architectures. For a list of what's supported, check out the list there.

NCNN

Single Image Super Resolution

  • Technically, almost any SR model should work assuming they follow a typical CNN-based SR structure. However, I have only tested with ESRGAN (and its variants) and with Waifu2x.

ONNX

Single Image Super Resolution

  • Similarly to NCNN, technically almost any SR model should work assuming they follow a typical CNN-based SR structure. However, I have only tested with ESRGAN.

Background Removal

TensorRT

TensorRT provides optimized inference for Nvidia GPUs. Models must be converted to TensorRT engine format for use. This offers the best performance on supported hardware.

Troubleshooting

For troubleshooting information, view the troubleshooting document.

Building chaiNNer Yourself

I provide pre-built versions of chaiNNer here on GitHub. However, if you would like to build chaiNNer yourself, simply run npm install (make sure that you have at least npm v7 installed) to install all the nodejs dependencies, and npm run make to build the application.

FAQ

For FAQ information, view the FAQ document.

Documentation

For in-depth documentation covering various aspects of ChaiNNer, including CLI usage, data representation, and a contributor's guide, kindly refer to our ChaiNNer Wiki.


Overview (`backend/tests/README.md`)

Backend Testing Guide

This directory contains comprehensive tests for the chaiNNer backend functionality.

Test Coverage

The test suite covers:

  • Chain execution (test_chain.py, test_executor.py)
  • Iterator system (test_iterator.py, test_generator_exception_handling.py)
    • Generator classes (from_list, from_range, from_iter)
    • Collector functionality
    • Exception handling during iteration
  • Event system (test_events.py)
  • Server configuration (test_server_config.py)
  • JSON serialization (test_chain_json.py)
  • Utilities (test_util.py, test_response.py, test_logger.py)

Running Tests

Prerequisites

Install test dependencies:

bash
pip install pytest pytest-asyncio pytest-cov

Run All Tests

From the repository root:

bash
python3 -m pytest backend/tests/

Run with Verbose Output

bash
python3 -m pytest backend/tests/ -v

Run Specific Test File

bash
python3 -m pytest backend/tests/test_iterator.py

Run Specific Test

bash
python3 -m pytest backend/tests/test_iterator.py::TestGeneratorFromList::test_basic_list_mapping

Code Coverage

Generate Coverage Report

Run tests with coverage:

bash
python3 -m pytest backend/tests/ --cov=backend/src --cov-report=term

Generate HTML Coverage Report

bash
python3 -m pytest backend/tests/ --cov=backend/src --cov-report=html

This creates an htmlcov/ directory with detailed coverage information. Open htmlcov/index.html in a browser to view.

Coverage Configuration

Coverage settings are configured in pyproject.toml:

  • Source directories are specified
  • Packages and node implementations are excluded (as they're tested via integration)
  • Common exclusion patterns are defined (abstract methods, type checking, etc.)

Current Coverage

As of the latest tests:

  • 105 test cases
  • 50.46% overall coverage of core backend code
  • Key areas covered:
    • api/iter.py: 100% (Generator and Collector)
    • chain/input.py: 100%
    • events.py: 97.87%
    • response.py: 96.15%
    • server_config.py: 100%

Areas with lower coverage (process.py, api modules) require more complex integration testing or involve server-specific code.

Test Structure

Test Classes

Tests are organized into classes by functionality:

python
class TestGeneratorFromList:
    """Test Generator.from_list functionality."""
    
    def test_basic_list_mapping(self):
        """Test basic list mapping with simple function."""
        # Test implementation

Fixtures

Common test fixtures are defined using pytest:

python
@pytest.fixture
def executor_setup(event_loop):
    """Create basic executor setup for tests."""
    # Setup code
    yield setup_data
    # Cleanup code

Async Tests

Async tests use the @pytest.mark.asyncio decorator:

python
@pytest.mark.asyncio
async def test_empty_chain_execution(self, executor_setup):
    """Test executing an empty chain."""
    # Async test implementation

Writing New Tests

When adding new tests:

  1. Follow existing naming conventions (test_*.py)
  2. Use descriptive test names that explain what is being tested
  3. Include docstrings explaining the test purpose
  4. Organize related tests into classes
  5. Use appropriate fixtures for setup/teardown
  6. Test both success and error cases
  7. Keep tests focused and independent

Example Test

python
def test_generator_basic_functionality(self):
    """Test that Generator produces correct output."""
    gen = Generator.from_list([1, 2, 3], lambda x, i: x * 2)
    results = list(gen.supplier())
    
    assert results == [2, 4, 6]
    assert gen.expected_length == 3

Continuous Integration

Tests should be run as part of CI/CD pipelines to ensure code quality. The test suite is designed to be fast and reliable for automated testing.

Contributing

When contributing new backend functionality:

  1. Write tests for new features
  2. Ensure existing tests still pass
  3. Aim to maintain or improve code coverage
  4. Document any new test patterns or fixtures

Overview (`backend/src/dependencies/whls/README.md`)

Bundled Wheels

This is where we can store wheel files that are to be bundled with chaiNNer, in order to avoid needing to download them.

Requirements

Bundled wheels must be

  1. Reasonably small (a few MB max)
  2. py3-none-any (compatible with any python version and device)
  3. License compatible (allows bundling)

Goals

  • Speed up initial start time by downloading the minimal number of wheel files from the internet
  • Not increase chaiNNer's bundle size too much

Structure

The whls folder shall contain individual folders, named according to the package name we use to install via pip. Inside the folder must be the .whl file as well as the project's license.


01 Contributing (`docs/01--Contributing.md`)

Contributing

Getting Started

  1. Fork the repository on GitHub
  2. Clone the forked repository to your local machine
  3. Install the dependencies of the frontend with npm ci
  4. Install the dev dependencies of the backend with pip install -r requirements.txt
  5. Install the dependencies of the backend with python ./backend/src/run.py --close-after-start --install-builtin-packages. The backend will install all of its dependencies with this command, including torch. This can take a while, so be patient.

ChaiNNer is an Electron application that uses Python for the backend. The frontend and backend communicate through a JSON API. The frontend is located in the src folder and the backend is located in the backend folder.

We use VSCode as our IDE of choice, and we strongly recommend you to do the same. We have included the .vscode folder in this repository, which contains some settings that make developing chaiNNer easier.

Running the Application

There are 2 way to run chaiNNer:

  1. Use npm run dev to run the application in development mode. This will run the frontend and backend in parallel. The frontend will be reloaded automatically when you make changes to the code. The backend will be reloaded automatically when you make changes to the code and restart the frontend.

    This also allows for debugging. You can attach a debugger to the backend by using the Attach Debugger to chaiNNer launch configuration in VSCode. This allows you to set breakpoints in backend code.

    Note: The backend will use your system python in this mode. This means that you might need to (re)install some dependencies using chaiNNer's dependency manager.

  2. Use npm start to run the application in production mode. The frontend will automatically start and manage the backend process. The frontend will be reloaded automatically when you make changes to the code. The backend will NOT be reloaded automatically, use npm run dev if you need this.

    NOTE: ChaiNNer will use its own python installation, the same that a regular installation of chaiNNer also uses.

Generally, use npm run dev when you want to develop the backend or the frontend, and use npm start you want to start chaiNNer in a more production-like mode.

Useful commands

The following commands will only work after you have installed the dependencies of the frontend and the backend, so be sure to follow Getting Started.

  • npm run dev & npm start - See Running the Application
  • npm run lint - Runs the linter on the frontend and backend code.
  • npm run lint:fix - Runs the linter on the frontend and backend code and fixes all fixable errors. This includes formatting, sorting imports, etc.
  • npm run test - Runs the tests.
  • npm run test:update - Runs the tests and updates any outdated snapshots.

Project structure

The project is split into 2 parts: the frontend and the backend. The frontend is located in the src folder and the backend is located in the backend folder.

Frontend

The frontend is an Electron application written in TypeScript. As such, the frontend is split into 3 parts:

  1. The code for the main Node.js electron process, which is located in src/main.
  2. The code for the renderer process, which is located in src/renderer. This is the web part of the application. We use React and Chakra UI for the frontend.
  3. The code for the shared code between the main and renderer process, which is located in src/common.

Backend

The backend is a Python application. The backend is roughly split into 3 parts:

  1. backend/src/packages contains the packages that contain chaiNNer's nodes. Most nodes are in the chaiNNer_standard package.
  2. backend/src/nodes contains logic shared between nodes. This includes common functionality for task such as upscaling, classes to define inputs and outputs, etc.
  3. The rest of backend/src contains the code that glues everything together. This includes the code that defines the JSON API, the code that manages the nodes, the node executor, etc.

Adding a new node

In this section, we will go through the process of adding a new node to an existing package. As an illustrative example, we will add the Gaussian Blur node.

Firstly, we need to determine where to put the file of the node. Nodes are organized into package, categories, and node groups.

  • A package is a collection of nodes with similar dependencies. E.g. all NCNN nodes are in the chaiNNer_ncnn package. All packages are located at backend/src/packages/.

    In our case, we will use OpenCV's cv2.GaussianBlur function to implement the Gaussian Blur node. So we will choose the chaiNNer_standard package, because it includes OpenCV as a dependencies.

  • Categories are used to structure nodes in the UI. Packages can contain multiple categories. E.g. the chaiNNer_standard package contains the Image and Utility categories. Categories are defined in the __init__.py file of the package.

    In our case, we will add the Gaussian Blur node to the Image (Filters) category.

  • Node groups are used to group nodes within a category. Categories can contain multiple node groups. E.g. the Image (Filters) category contains the Blur, Sharpen, and Noise node groups. Node groups are typically defined in /<category>/__init__.py.

    In our case, we will add the Gaussian Blur node to the Blur node group.

Note: In this example, we will add the new node to an existing category and node group, but you may need to create new categories and node groups for your specific node. This is rather simple, so just follow the structure of the existing categories and node groups.

With package, category, and node group clear, we can now create the file for our node. The file should be located at backend/src/packages//<category>/<node_group>/<node_name>.py. In our case, this is backend/src/packages/chaiNNer_standard/image_filter/blur/gaussian_blur.py.

Let's implement the node:

python
# gaussian_blur.py
import cv2
import numpy as np

def gaussian_blur_node(img: np.ndarray, radius: float) -> np.ndarray:
    return cv2.GaussianBlur(img, (0, 0), sigmaX=radius, sigmaY=radius)

The implementation is rather simple, we take an image and a radius as input, and return the blurred image as output.

Now let's register this function as node, so chaiNNer can use it. We need to import our node group, in this case the Blur node group, and use its register function as a decorator like this:

python
# gaussian_blur.py
import cv2
import numpy as np

from .. import blur_group

@blur_group.register(
    # TODO: Add metadata here
)
def gaussian_blur_node(img: np.ndarray, radius: float) -> np.ndarray:
    return cv2.GaussianBlur(img, (0, 0), sigmaX=radius, sigmaY=radius)

register requires that we supply metadata, so let's add it. We need a name, a description, an icon, and we need to declare all inputs and outputs.

python
# gaussian_blur.py
import cv2
import numpy as np

from nodes.properties.inputs import ImageInput, NumberInput
from nodes.properties.outputs import ImageOutput

from .. import blur_group


@blur_group.register(
    schema_id="chainner:image_filter:gaussian_blur",
    name="Gaussian Blur",
    description="Apply Gaussian blur to an image.",
    icon="MdBlurOn",
    inputs=[
        ImageInput(),
        NumberInput("Radius", minimum=0, default=2, precision=1),
    ],
    outputs=[
        ImageOutput(shape_as=0),
    ],
)
def gaussian_blur_node(img: np.ndarray, radius: float) -> np.ndarray:
    return cv2.GaussianBlur(img, (0, 0), sigmaX=radius, sigmaY=radius)

Let's go through the metadata:

  • schema_id is a unique identifier for the node. The schema ID currently doesn't have a defined format, but we typically use chainner:<category>:<node_name>. In our example, we use chainner:image_filter:gaussian_blur.
  • name, description, and icon are all displayed in the UI.
  • inputs declares all arguments of the python function. In our case, we have 2 inputs: img and radius. ImageInput and NumberInput are classes that define the type of the input. ImageInput is used for images, and NumberInput is used for numbers. NumberInput takes a few arguments. minimum and maximum define the range of the number, default defines the default value, and precision defines the number of decimal places.
  • outputs declares all return values of the python function. In our case, we return one image. ImageOutput is used for images. shape_as=0 means that the output image has the same size and number of channels as the input image.

For more information about metadata, see the Node Metadata docs.

And that's it. We have successfully implemented a new node. Now we can start chaiNNer and see our new node in action.

For more information about nodes, see the nodes documentation.


02 Data Representation (`docs/02--Data-Representation.md`)

Data representation

ChaiNNer has to deal with multiples types of data (images, text, numbers, etc.). To make data handling easier, ChaiNNer enforces that all node implementations follow certain conventions. These conventions are guaranteed by inputs and enforced by outputs.

Numbers

Depending on the precision of the inputs, chaiNNer will either use float (precision > 0) or int (precision = 0) to represent numbers.

This is guaranteed by all numeric inputs. Numeric outputs do not enforce any such thing and will accept both float and int.

Text

Text is represented as str. If a number is connected to a text input, it will be converted to str.

Images

Images are represented using numpy ndarray and are guaranteed to have the following:

  • They are float32 arrays.
  • All values are finite (no NaN, inf, -inf) and range between 0 and 1 (inclusive).
  • The shape of the array is either (height, width, channels) (with channels > 1) or (height, width). Grayscale/single-channel images are represented as 2D arrays.
  • Channels are in BGR and BGRA order.
  • The array is readonly. Writing to it will cause a runtime error.

Note: You may not assume that chaiNNer only supports images with 1, 3, or 4 channels. It is possible that chaiNNer will support images with more channels in the future. If your node relies on this assumption, you should use ImageInput's channels argument to enforce it. Simply add channels=[1, 3, 4].

ImageOutput will try to convert the output image to fit the above conventions. It will:

  1. Convert the image to float32 if it isn't already. Integer formats (e.g. uint8) will automatically be brought into 0 to 1 range.
  2. Clip values between 0 and 1 (inclusive).
  3. Convert single-channel 3D images to 2D.

Since channel order cannot be checked, you have to guarantee this yourself.

Colors

Colors are represented using chaiNNer's Color class.

The Color class is a wrapper around a tuple of floats that represent the channels of the color. Just like with images, the channels are in BGR and BGRA order. You can generally think of Color as a 1x1 image.

Models

ChaiNNer has types for models of each platform (PyTorch, NCNN, ONNX). Models will be represented using platform-specific classes. See their input and output classes for more information.


03 Nodes (`docs/03--Nodes.md`)

Nodes

Core concepts and idea

ChaiNNer is a visual programming language and development environment. Chains (.chn files) are programs and chaiNNer's UI is the editor for this programming language. The backend is the compiler and executor. Nodes are the functions of this language, they take inputs and produce outputs. Nodes are implemented as Python functions with extra metadata. This metadata is used to display the node in the UI and determine their behavior. E.g. type information is used to determine which connections are valid. Nodes are structured into categories and packages. ChaiNNer uses packages to manage the dependencies of nodes (e.g. pip packages), and categories to organize nodes in the UI.

Rules for nodes

While nodes can generally implement whatever functionality you want, there are some rules that nodes must follow:

  • Immutability
    Nodes are not allowed to modify their inputs. This is because chaiNNer will cache and reuse values for performance reasons. If a node modifies its input, this will break the cache and cause chaiNNer to produce incorrect results.

    This is especially important for images, which are passed around as numpy arrays. Numpy arrays are mutable, so you need to be careful when working with them. If you need to modify an input, make a copy of it first.

  • Determinism
    Nodes are assumed to be deterministic. This means that a node should always produce the same output for the same input. This is important for caching.

    If your node needs random numbers, either use a static seed or use a seed input. See the Random Number node for an example of this.

    We do plan to add support for non-deterministic nodes in the future, but this is not implemented yet. See #1677.

  • Side effects
    Nodes are assumed to be side effect free. This means that a node should not change the file system (e.g. creating, changing, or deleting files), write to clipboard, open programs/files, or otherwise change the state of the system.

    If your node does have side effects (e.g. saving a file), you must declare this in the node metadata by using side_effects=True.

Node metadata

Nodes have rich metadata. This includes a name, description, icon, and more. This metadata is used to display the node in the UI and determine their behavior. E.g. type information is used to determine which connections are valid.

Metadata is used to define a contract between a node, the rest of the backend, and the frontend. This contract is used to verify the correctness of the node implementation, optimize the node graph, and define frontend behavior.

  • Schema ID
    The schema ID of the node. This is used to identify the node in the JSON API. This must be unique across all packages. We recommend using the format chainner:<category>:<name> for chaiNNer nodes.

    Note that the chaiNNer team has moved around nodes in the past. As such, you may find nodes that do not follow the above format. We recommend using the format above for new nodes.

  • Name
    The name of the node. This is used to display the node in the UI.

    When choosing a name, make sure that it is descriptive and unique. If you are implementing a node that already exists in another package, make sure to use the same name. E.g. PyTorch and NCNN both have a Load Model node.

  • Description
    A description of the node. This is currently used in the node's tooltip in the node selector sidebar, as well as the Node Documentation modal. If given an array of strings, only the first string will populate the tooltip, while all strings will populate the modal.

  • Icon
    The icon of the node. Use the search bar on React Icons for which icons you want to use. Note that we only support icons from the Bs, Cg, Md, and Im families.

  • Inputs/Outputs
    The inputs and outputs of the node. They describe the name, behavior, and type of the inputs and outputs. See Input and Output for more information.

  • Side effects
    Whether the node has side effects. See Rules for nodes for more information.

Inputs and Outputs

Nodes must explicitly declare all their inputs and outputs as part of their metadata. This is done using the inputs and outputs properties. Each argument of the python function has a corresponding input, and the return value of the function has a corresponding output. See The anatomy of a node for an example.

The main purpose of explicitly declaring inputs and outputs is to provide more information about the node. E.g. the type information is used to determine which connections are valid, the minimum/maximum information is used to validate user inputs, and placeholder and defaults are used to improve the user experience. They also make guarantees about input data and enforce conventions.

The most common classes used to declare inputs are NumberInput, TextInput, and ImageInput. There is also SliderInput as an alternative to NumberInput. Many more inputs are available.

The most common classes used to declare outputs are ImageOutput, NumberOutput, and TextOutput. Many more outputs are available.

It is important to note that inputs and outputs have IDs. These IDs are used to identify the input/output in the JSON API. By default, IDs are implicitly assigned based on order. E.g. in the following example, A has the input ID 0, B has the input ID 1, and C has the output ID 0.

python
inputs=[
    ImageInput("A"),
    NumberInput("B"),
],
outputs=[
    ImageOutput("C"),
],

Reordering

Since IDs are used to identify inputs and outputs, reordering them is a breaking change. If you want to reorder inputs or outputs, assign them explicit IDs using the with_id function, and then reorder them.

python
# 1. Make IDs explicit
inputs=[
    ImageInput("A").with_id(0),
    NumberInput("B").with_id(1),
],
# 2. Reorder inputs
inputs=[
    NumberInput("B").with_id(1),
    ImageInput("A").with_id(0),
],

Adding a new input in between existing inputs is also a breaking change, because it counts as appending the new input and then moving it in between the existing inputs. To add a new input in between existing inputs, assign explicit IDs to all inputs and then move the new input into the correct position.

python
# 1. Add input
inputs=[
    ImageInput("A"),
    NumberInput("B"),
    TextInput("T"),
],
# 2. Make IDs explicit
inputs=[
    ImageInput("A").with_id(0),
    NumberInput("B").with_id(1),
    TextInput("T").with_id(2),
],
# 3. Move input
inputs=[
    ImageInput("A").with_id(0),
    TextInput("T").with_id(2),
    NumberInput("B").with_id(1),
],

The same principles also apply to outputs.

Input groups

Input groups are a way of grouping inputs together and giving them additional functionality. Groups can change the visual appearance of the inputs, add additional functionality, and more.

The following groups are currently supported:

  • Conditional
    Conditional groups are used to hide/show inputs based on the value of another input. This is useful for inputs that are only relevant in certain situations. E.g. the Save Image node uses conditional groups to hide JPEG-specific options when PNG is selected as the output format.

    We most commonly use EnumInputs or DropDownInputs as the input that controls the visibility of the group. This is because they are the easiest to work with.

    Example:

    python
    from enum import Enum
    from nodes.groups import if_enum_group
    from nodes.properties.inputs import EnumInput, NumberInput
    
    class Format(Enum):
        A = 1
        B = 2
        C = 3
    
    # inside @node_group.register(...)
    inputs = [
        EnumInput(Format, "Format", default=Format.A).with_id(0),
        if_enum_group(0, Format.B)(
            # this weight will only be shown if B is selected
            NumberInput("B weight", default=50, minimum=0, maximum=100),
        ),
    ]
  • Seed
    This group is a wrapper around the SeedInput to change its visual appearance for better useability.

    python
    from nodes.group import group
    from nodes.properties.inputs import SeedInput
    
    # inside @node_group.register(...)
    inputs = [
        seed_group(SeedInput()),
    ]
  • And more. See backend/src/nodes/groups.py for more groups.

Python type hints

We require that all node functions have type hints. This is simply for correctness and to make the code easier to understand. You must add type hints to all arguments and the return value of the function.

python
def blur_node(img: np.ndarray, radius: float) -> np.ndarray:
    ...

If you forget type hints or get them wrong, you will get a warning or an error when you try to register the node. The input and output metadata is used to validate your type hints. E.g. if you forget the | None for optional inputs, you will get a warning.

Types

All inputs and outputs have types. These types are automatically generated from the input/output class and their supplied arguments. E.g. NumberInput("foo", minimum=0, maximum=100) will have the type int(0..100) (read: an integer between 0 and 100 inclusive).

Types are expressed using a custom type system called Navi. Navi is not relative to Python's type hints or TypeScript types, it's a completely separate system.

More information about Navi can be found in the Navi documentation.

Type narrowing

While all inputs and outputs have automatically generated types, these types might not be precise enough. Especially outputs often have very broad types. E.g. ImageOutput has the type Image, which means "an image of any size". It could be a 1x1 image, or a 10000x10000 image. This is not very useful for the frontend.

To fix this, most output classes have a type argument, typically called output_type or *_type. This argument is used to narrow the type of the output. E.g. ImageOutput("foo", image_type="Image { width: 100, height: 100, channels: 3 }") narrows the type to "an RGB image of size 100x100".

Since output types often depend on inputs, you can reference inputs using special variables. E.g. Input0 is the type of the input with ID 0.

Let's take a look at the inputs and outputs of the Create Border node. This node adds a solid color border to an image. It has 2 inputs: the image and the width of the border.

python
inputs=[
        ImageInput(),
        NumberInput("Width", unit="px"),
    ],
    outputs=[
        ImageOutput(
            image_type="""
                Image {
                    width: Input0.width + 2 * Input1,
                    height: Input0.height + 2 * Input1,
                    channels: Input0.channels,
                }
            """,
        )
    ],

Documentation

Inputs and outputs both allow optional documentation to be added to add more information about it to the Node Documentation modal. You can add documentation to an input or output using the .with_docs() method, like so:

python
inputs=[
        ImageInput().with_docs(
            "An example image input.",
            "Each string passed in will be its own paragraph in the resulting docs page.",
            "And, it supports _markdown_ *formatting*.",
        ),
        NumberInput().with_docs(
            "Confusing inputs can be given a hint flag, which will show a tooltip icon on the input in the node itself.",
            "Use these when you think people would need an explanation about something regardless of if they have looked at the docs modal."
            hint=True,
        )
    ],

Broadcasts

Broadcasts are a way of sending data from the backend to the frontend. They are used for updating previews (e.g. image previews), enhancing error messages with useful information, and providing the type system with additional information. Output classes (e.g. ImageOutput) are responsible for generating broadcasts.

There are 2 types of broadcasts: data broadcasts and type broadcasts.

Note: Broadcasts are completely optional. Outputs can have both types of broadcast, only one, or none at all.

Note: Sending a broadcast does not mean that the frontend will acknowledge it. In fact, the frontend ignores most type broadcasts and only uses some data broadcasts (e.g. the ones from LargeImageOutput for preview images).

Data broadcasts

Data broadcasts are used to send data from the backend to the frontend. E.g. ImageOutput sends the dimensions of output images for better error messages, and LargeImageOutput sends the dimensions and image data.

Data broadcasts are implemented using the get_broadcast_data method on output classes that inherit from BaseOutput.

Type broadcasts

Type broadcasts are used to send additional type information from the backend to the frontend. While we try our hardest to encode as much information as possible in the type system, there are some things that are impossible to statically evaluate using the type system.

E.g. we cannot infer the dimensions of an image from a file path alone. So ImageOutput sends an Image type with the dimensions of the image.

Type broadcasts are implemented using the get_broadcast_type method on output classes that inherit from BaseOutput.

Packages

A package is a collection of nodes. ChaiNNer comes with a set of standard nodes (chaiNNer_standard), but you can also create your own packages.

Unfortunately, packages are still a bit of a work in progress. We plan to improve the package system in the future. Right now, they are just fancy directories that contain nodes.

Categories

Nodes are categorized. Categories are defined per package and follow a strict hierarchy. You can see this hierarchy in the node selector sidebar of chaiNNer.

At the top of the hierarchy are categories (e.g. Image Filter). Each category can have any number of subcategories, called node groups. Node groups then contain the actual nodes.

This gives us a clear path: package -> category -> node group -> node. E.g. the path of the Gaussian Blur node is chaiNNer_standard -> Image Filter -> Blur -> Gaussian Blur.

The anatomy of a node

This section will explain how nodes are implemented in chaiNNer. We will use the Opacity node as an example. This node takes an image and an opacity value as input, and outputs the image with the opacity applied. It basically makes the image more transparent.

Here's the full code of the node:

py
import numpy as np

import navi
from nodes.impl.pil_utils import convert_to_BGRA
from nodes.properties.inputs import ImageInput, SliderInput
from nodes.properties.outputs import ImageOutput
from nodes.utils.utils import get_h_w_c

from .. import adjustments_group


@adjustments_group.register(
    schema_id="chainner:image:opacity",
    name="Opacity",
    description="Adjusts the opacity of an image. The higher the opacity value, the more opaque the image is.",
    icon="MdOutlineOpacity",
    inputs=[
        ImageInput(),
        SliderInput(
            "Opacity",
            maximum=100,
            default=100,
            precision=1,
            controls_step=1,
            unit="%",
        ),
    ],
    outputs=[
        ImageOutput(size_as=0, channels=4)
    ],
)
def opacity_node(img: np.ndarray, opacity: float) -> np.ndarray:
    """Apply opacity adjustment to alpha channel"""

    # Convert inputs
    c = get_h_w_c(img)[2]
    if opacity == 100 and c == 4:
        return img
    imgout = convert_to_BGRA(img, c)
    opacity /= 100

    imgout[:, :, 3] *= opacity

    return imgout

We can see the code is roughly split into 2 parts: @adjustments_group.register(...) and def opacity_node.

The first part defines the node's metadata. Nodes are registered for a specific node group (see the section on categories). In this case, the node is registered for the adjustments_group node group. Name, description, and icon should be self-explanatory. The schema_id is a unique identifier for the node. Input and outputs define rich metadata about the arguments and return value of the node. E.g. The SliderInput contains a lot more information than opacity: float. See the section on inputs and outputs for more information.

The second part is the actual node implementation. The implementation is rather straightforward. The node takes an image and an opacity value as input. Aside from handling a special case, it just multiplies the alpha channel of the image with the opacity value. Importantly, the input image is not modified. Instead, the image is copied (convert_to_BGRA guarantees a copy) and the copy is modified and returned. See the nodes section as to why copying is necessary.

It should be noted that metadata is used to verify the correctness of the implementation. E.g. the type hints of img and opacity are checked against the input metadata. So you can rely upon the fact that opacity is a number between 0 and 100. It's similar for the return type annotation. E.g. in this example, ImageOutput will validate that the returned image has 4 channels (as declared by channels=4) and will throw an error otherwise.


04 Navi (`docs/04--Navi.md`)

ChaiNNer has its own type system called Navi. The type system has 2 main purposes:

  1. Static validation
    It checks that the connections between nodes make sense. E.g. connecting a string output to an image input will cause a type error. This is done before running the chain, live inside the graph editor.

    While this might seem very simple, because of the expressiveness of Navi, we can do some very interesting things. For example, we check number ranges, image dimension, model architectures, and even file types.

    Since we can statically determine the validity of connections, the type system is also used to suggest relevant nodes to the user. This takes the form of graying out invalid handles and showing only connectable nodes in the context node selector.

  2. Real time feedback
    Since the type system is evaluated and update in real time, users get instant feedback. The frontend uses types to show the value or information about the value a node will output. E.g. number and text outputs will show the value, while image outputs will show the dimensions of the image.

    This is especially useful for new users who are not familiar with chaiNNer's nodes. They don't have to guess the size of an image upscaled with a 4x model, but the type system calculates it for them. They don't have to guess what Text Append does, because they can see its output and play around with it in real time.

Introduction

Before we start, while Navi can be somewhat complex, it should also be easy to pick up. Navi intentionally uses TypeScript/Python-like syntax, and has similar concepts. While everything works on types, the operations should feel familiar.

For example, here's a function that pads a string to a certain width:

navi
def padRight(s: string, width: uint): string {
    let len = string::len(s);
    let fillLen = width - len;

    match fillLen {
        1.. => string::concat(s, string::repeat(" ", fillLen)),
        _ => s,
    }
}

This code should be understandable to most programmers. A few things to note:

  • Braces! Navi uses braces for blocks, just like TypeScript. And just like TypeScript, Navi is white-space insensitive.
  • Semicolons. Each let statement ends with a semicolon. This is required.
  • Function are defined using def. They require type annotations for all arguments.
  • There is no return. The last expression in a function is the return value. Here, it's the match expression.
  • Navi supports simple pattern matching using match. if is also supported, but match is more powerful.
    • Number ranges are defined using the .. syntax. E.g. 1..10 is the set of all numbers between 1 and 10, including 1 and 10, and 1.. is all numbers greater than or equal to 1.
    • match (and if) are expressions and can be used anywhere. E.g. let x = match foo { 1..10 => 1, _ => 0 }; is valid.
  • Variable are declared using let. This is similar to let in TypeScript, but the variables are immutable.
  • Navi does not support .name() call syntax (right now). String function have to be called by their full name.

But remember, Navi is a type system, so padRight will operate on types. Examples:

navi
let a = padRight("foo", 4);
// a is "foo "

// Multiple values
// (`|` is the type union operator)

let b = padRight("foo" | "food", 5);
// b is "foo  " | "food "
let c = padRight("foo", 1 | 2 | 3 | 4 | 5);
// c is "foo" | "foo " | "foo  "

Navi is a set-based type system. This means that every type is a set of values. For example, the type number is the set of all numbers, int is the set of all integers, string is the set of all strings, and so on.

Important types

  • number - the set of all numbers. This includes integers, floats, and the infinities, and NaN.
  • int - the set of all integers. This does not include the infinities, and NaN.
  • uint - the set of all unsigned integers. It's all ints โ‰ฅ 0.
  • string - the set of all strings.
  • bool - true | false.
  • null - this type is used by chaiNNer to present optional inputs. If an input is null, no value is given and the python function will be given None.
  • any - the set of all values. This is more of a placeholder type or theoretical construct. It's rarely used in practice.
  • never - the empty set. Type is typically used to present on error.
  • 0, 1, 2.5, -10, inf, -inf, nan - literal numeric types. These are used to represent specific numbers. E.g. 0 is the set containing only the number 0.
  • 0..100, 0..inf, -10..10 - numeric range types. These are used to represent a range of numbers. E.g. 0..100 is the set of all numbers (including non-integers) between 0 and 100, including 0 and 100.
  • int(0..100), int(0..inf), int(-10..10) - integer range types. These are used to represent a range of integers. E.g. int(0..100) is the set of all integers between 0 and 100, including 0 and 100. Note that integer ranges are equivalent to casting numeric ranges to int. E.g. int(0..100) is equivalent to int & 0..100.
  • "", "foo", "bar" - literal string types. These are used to represent specific strings. E.g. "" is the set containing only the empty string.

Type operations

Navi has a set of operations that can be used to create new types from existing types. These operations are:

  • & - Intersection
    This operation creates a new type that is the intersection of the two types. E.g. -100..100 & 0..200 is equivalent to 0..100, 0..10 & 20..30 is equivalent to never, any & T is equivalent to T, and never & T is equivalent to never.

    Note: intersection can also be thought of as asserting that a value is of a certain type. E.g. int & 0..100 is the set of all integers between 0 and 100, including 0 and 100. This is equivalent to casting 0..100 to int.

  • | - Union
    This operation creates a new type that is the union of the two types. E.g. -100..100 | 0..200 is equivalent to -100..200, 1 | 2 | 3 | 4 is equivalent to int(1..4), any | T is equivalent to any, and never | T is equivalent to T.

Structure types

In the set-theoretic sense, structure types are the cartesian product of their fields, but it's easier to think of them as C-like structs. Structure types allow us to define new types from existing types.

For example, this is how chaiNNer's Image type is defined:

navi
struct Image {
    width: uint,
    height: uint,
    channels: int(1..inf),
}

As we can see, Image has 3 fields: width and height are unsigned integers, and channels is an integer between 1 and infinity (= unsigned integer starting at 1).

It should be noted that the above code is the type definition of Image. To get a type, we need to instantiate it. This is done by providing values for the fields. For example, Image { width: uint, height: 100, channels: 3 } is the type of all images with a height of 100 and 3 channels.

Fields that are equivalent to their definitions can be omitted. For example, Image { height: 100, channels: 3 } is equivalent to Image { width: uint, height: 100, channels: 3 }. Taking this to the extreme, we can omit all fields to get the type of all images: Image {}. Since this is quite common for structure types, we can omit the braces and just write Image.

Set operations apply to structure types as well. For example, Image { width: 100 } & Image { height: 100 } is equivalent to Image { width: 100, height: 100 }, and Image { width: 100 } | Image { width: 200 } is equivalent to Image { width: 100 | 200 }.

Constants

It is possible to define structure types with no fields. These are called unit types or constants. They are a type that represent only a single value: themselves.

The best examples for constants are true and false. Here's how they are defined:

navi
struct true;
struct false;

Note: struct Name; is equivalent to struct Name {}.

true and false are unique types and are not assignable to each other, they are disjoint (true & false is equivalent to never).

As you might have guessed, true and false are used to define the bool type. The bool type is equivalent to true | false:

navi
let bool = true | false;

Type aliases

It's possible to give types names. This is done with the let keyword.

navi
let Nat0 = int(0..inf);
let Nat1 = int(1..inf);
let keywords = "enum" | "let" | "struct";

Type aliases are used to both shorten long type names. Since everything is a type in Navi, type aliases can also be thought of as immutable variables (similar to const in JavaScript).

It's important to note that the order of definition does not matter. This means that the following code is valid:

navi
let b = a | Foo { value: 2 };
let a = Foo { value 1 };
struct Foo { value: int }

Match expressions

Match expressions are a core feature of the language and can be thought of as the opposite of type unions. While type unions allow us to combine types, match expressions allow us to split types.

A match expression consists of a match value and a set of match arms. Match arms represent conditions that the match value must satisfy. If the match value satisfies a match arm, the match arm's body is evaluated.

Example:

navi
let x = int(0..10);
let y = match x {
    0..5 => "small",
    5..10 => "big",
};

The value of y will be "small" | "big". Since x intersects with both 0..5 and 5..10, both match arms are evaluated and their results are unioned together.

Exhaustiveness & the default match arm

It is important to note that match arms must be exhaustive. This means that every possible value of the match value must be covered by at least one match arm. If this is not the case, a runtime error will be thrown.

If necessary, a default match arm can be used to cover all remaining values. This is done by using the _ keyword as the match arm's condition.

Example:

navi
let x = /* some arbitrary type */;
let y = match x {
    0..5 => "small",
    5..10 => "big",
    _ => "unknown",
};

The order of match arms

Match arms are evaluated in order. This is important because match will remove (set difference) the match arm's pattern from the match value. So later match arms will see the reduced match value.

As an example, let's see how following the value of y changed for different values of x. We define 2 structs (Small and Big) as helpers to make it clear which values of x take which match arm.

navi
struct Small { value: any }
struct Big { value: any }

let y = match x {
    0..5 => Small { value: x },
    5..10 => Big { value: x },
    _ => x,
};

Let's try a few values of x:

Let x = 3: y will be Small { value: 3 }. This result should be intuitive. 3 is in the range 0..5, so the first match arm is evaluated. Since the first match arm completely covers the match value, no other match arms are evaluated, and the final value will be Small { value: 3 }.

Let x = 3 | 7: y will be Small { value: 3 } | Big { value: 7 }. This should also be fairly intuitive. 3 only matches the first match arm, and 7 only matches the second match arm. Both match arms are evaluated with 3 and 7 respectively, and their results are unioned together.

This brings us to an important property of match: match A | B { ... } is equivalent to match A { ... } | match B { ... }. This means that match distributes over unions.

Now, let's try x = 5: y will be Small { value: 5 }. This is where things get interesting. 5 matches both match arms, but only the first one will be evaluated. This is because the first match arm completely covers the match value, so the second match arm will never be evaluated. This is why y is Small { value: 5 } and not Small { value: 5 } | Big { value: 5 }.

Finally, let's try x = int(0..20): y will be Small { value: int(0..5) } | Big { value: int(6..10) } | int(11..20). Let's go through the match arms step by step:

  1. We arrive at the first match arm with a match value of int(0..20). The intersection of int(0..20) and 0..5 is int(0..5), so this is the value used to evaluate the first match arm. The result of this match arm is Small { value: int(0..5) }.
  2. We then remove 0..5 from the match value int(0..20) to get int(6..20). This will be the match value for the second match arm.
  3. We arrive at the second match arm with a match value of int(6..20). The intersection of int(6..20) and 5..10 is int(6..10), so this is the value used to evaluate the second match arm. The result of this match arm is Big { value: int(6..10) }.
  4. We then remove 5..10 from the match value int(6..20) to get int(11..20). This will be the match value for the third match arm.
  5. We arrive at the third match arm with a match value of int(11..20). Since the third match arm is the default match arm, it will be evaluated with the match value int(11..20). The result of this match arm is int(11..20).
  6. We union the results of all match arms together to get Small { value: int(0..5) } | Big { value: int(6..10) } | int(11..20).

Bindings

In the above example, we observed that match automatically narrows down the variable x inside each match arm. Navi is unable to do this automatically for more complex match values. E.g. struct fields, type expressions, function calls, etc. cannot be narrowed down automatically.

Example:

navi
let x = Image { width: 100 | 200 };
let y = match x.width {
    100 => "small",
    _ => x.width,  // x.width will still be 100 | 200
};
// y = "small" | 100 | 200

To work around this, we can use bindings. Bindings are used to directly assign the current match value of the match arm to a variable. This variable can then be used inside the match arm. This is done by using the as keyword.

Example:

navi
let x = Image { width: 100 | 200 };
let y = match x.width {
    100 => "small",
    _ as width => width,  // width will still be 200
};
// y = "small" | 200

`if`

Navi also supports if expression on bool values. This is simple syntactic sugar for match.

Example:

navi
let x: bool = someFunction();
let y = if x {
    "true"
} else {
    "false"
};

is equivalent to:

navi
let x: bool = someFunction();
let y = match x {
    true => "true",
    false => "false",
};

Note: else is required.

else if is also supported. Just like in other languages, if { ... } else if { ... } else { ... } is equivalent to if { ... } else { if { ... } else { ... } }.

Functions

Functions can be defined using the def keyword and called using a C-like call syntax. Functions are very similar to functions in other programming languages, but they have one important difference: All arguments are types.

Example:

navi
def unionInt(a: int, b: int): int {
    a | b
}

let x = unionInt(1, 2 | 3);
// x = 1 | 2 | 3

There are 2 syntaxes for functions: expression syntax and scope syntax.

  • Expression syntax: def unionInt(a: int, b: int): int = a | b;
  • Scope syntax: def unionInt(a: int, b: int): int { a | b }

The only difference is that scope syntax allows you to use multiple statements inside the function body. E.g. let statements, if statements, etc.

Example:

navi
def unionInt(a: int, b: int): int {
    let foo = a | b;
    foo
}

Note: return is not supported. Functions always return the result of the last expression in the function body.

Note: The return type annotation is optional. However, it is recommended to always use it. It makes the code easier to read and allows Navi to detect errors.

Predefined functions

Navi has some predefined functions that are a standard library of sorts. This includes functions for string operations (length, slice, concatenation), number operations (addition, multiplication, rounding, etc.), and more.

All full list of all predefined functions can be found here. Note that some of these functions are intrinsics. This means that they are not actually implemented using Navi, but are instead implemented in JavaScript/TypeScript.

Syntax sugar

Since arithmetic operations are commonly used in chaiNNer's Navi types, they get special syntax. You can use +, -, *, /, >, >=, <, <=, ==, and !=. They will behave as expected. E.g. 1 + 2 == 3.

Under the hood, they are syntactic sugar for predefined functions. E.g. a + b is equivalent to number::add(a, b).

Equality

Since there is syntax for == and !=, it is important to understand how equality works in Navi.

The result of equality is a bool type. Since types are sets of values, we define equality of 2 types $A$ and $B$ as such: ${ a = b : a \in A, b \in B }$ where $a = b$ return true or false depending on whether $a$ and $b$ are the same value. Type equality is the set of all pair-wise comparisons of the values in $A$ and $B$.

This means that equality has the following properties:

  • A == B is B == A.
  • (A | B) == C is (A == C) | (B == C).
  • (A & B) == C is (A == C) & (B == C).
  • A == never is never.
  • A == any is bool (true | false).

Let's look at a few examples to see this in action:

  • 1 == 1 is true.
  • 1 == 2 is false.
  • 1 | 2 == 1 is true | false which is bool.
  • 1 & 2 == 1 is true & false which is never.

Lastly, A != B is defined as not A == B.

Note: When comparing values, nan is equal to itself. So nan == nan is true.

Unsoundness

It is very important to note that Navi is unsound. This means that there are false statements that are accepted as true by Navi. But every true statement can be proven by Navi. This tradeoff is forced upon us by the very nature of mathematical logic.

However, this is not a problem in practice, quite the opposite. It means that we 100% know that chains that do not pass the type system are invalid. But chains that pass the type system are not necessarily valid. This is a very important distinction. We can use Navi to detect errors, but we cannot use it to prove correctness.


05 CLI (`docs/05--CLI.md`)

CLI

CLI mode lets you use ChaiNNer without a GUI. This is useful for running ChaiNNer on a server or in a Docker container.

IMPORTANT: CLI mode is experimental and may change in the future. It is not recommended to use it in production. All of its features are subject to change.

IMPORTANT: CLI mode currently has a bug that prevents you from seeing the console outputs of chainner.exe. This is a known issue and will be fixed in the future.

Introduction

In some cases, you may want to run ChaiNNer without the GUI. E.g. to programmatically run the same chain with different inputs. As such, CLI mode is a way for advanced users with programming experience to work around the limitations of GUI chaiNNer.

Conceptually, CLI mode simply opens a .chn chain file and runs it. It's the same as if you opened the chain in the GUI and pressed the "Run" button. Of course, CLI mode has a way to set the inputs of nodes within the chain before running it.

Usage

The basic command of CLI mode is this:

text
chainner.exe run "path/to/your-chain.chn"

This will run the chain as is, without changing any inputs. The exit code of the program will be 0 if the chain ran successfully, and non-zero if it failed.

If you want to change the inputs of nodes within the chain, you can specify a file with input overrides like so:

text
chainner.exe run "path/to/your-chain.chn" --override "path/to/your-input-overrides.json"

Input overrides

Input overrides are a JSON file that specifies the inputs of nodes within the chain. The format is as follows:

ts
{
    "inputs": {
        "<input override id>": string | number | null
    }
}

You can specify as many input overrides as you want.

To get the input override ID of a specific input, open the chain in the GUI and right-click the input. Then click "Copy Input Override ID". This will copy the input override ID to your clipboard.

Supported inputs

The following inputs support overrides:

  • Text input
  • Number input
  • File input
  • Directory input

Dropdown, checkbox, and generic inputs do not support overrides.

Override values

Overrides can be either a string, a number, or null.

The value of the override depends on the type of the input. E.g. use a number for a number input, and a string for a text input. Using strings to override number inputs will result in an error.

null is used to reset optional inputs. It will not reset inputs to their default value, but rather remove the input value. This is only useful for a few optional text inputs in chaiNNer.


06 Troubleshooting (`docs/06--Troubleshooting.md`)

Troubleshooting

After installing PyTorch, the nodes still do not show up

  • This issue is still a bit of a mystery to us, but you can try a couple of things.

  • Ensure you have at least 3GB of free RAM before starting chaiNNer.

  • Uninstall PyTorch, restart chaiNNer, and reinstall it.

PyTorch is failing to install from the dependency manager

  • Make sure you have nothing blocking pip requests

Windows

  • Run this command in a terminal: %appdata%/chaiNNer/python/python/python.exe -m pip install torch==1.10.2+cu113 --extra-index-url https://download.pytorch.org/whl/cu113

Linux

  • I'm not 100% sure what the command would be on linux, but it would essentially be the same thing except with the path to whatever the equivalent of appdata is. so linuxappdata.../chaiNNer/python/python/python3.9 -m pip install torch==1.10.2+cu113 --extra-index-url https://download.pytorch.org/whl/cu113

Has Nvidia GPU, PyTorch using CPU

  • Check the dependency manager and ensure it installed the +cu113 version. If it did not, and it says CUDA supported at the top, then uninstall and reinstall PyTorch.

  • Check to make sure you have the nvidia-smi command available to you. Open a terminal (command prompt, powershell, windows terminal) and type nvidia-smi. If it errors, reinstall your drivers and restart your computer.

  • If chaiNNer or a terminal still isn't able to use nvidia-smi, try adding your System32 folder to your PATH environment variable. Make sure to restart your computer after.

Has AMD/Intel GPU, PyTorch using CPU

  • PyTorch can only use Nvidia GPUs. In this case, you would want to try using NCNN as the processing framework instead. You can convert most supported PyTorch models to NCNN through chaiNNer (just be sure to also install ONNX as it is needed in the conversion process).

Checked logs and see `CUDA_INITIALIZE` errors

  • Ensure your drivers are up to date. Outdated drivers might not support the CUDA version that PyTorch and ONNX rely on.

  • Ensure your GPU isn't too old. Your GPU needs to be able to support CUDA 11.3. If it only supports CUDA 10 for example, you'll need to manually install an older PyTorch version to your system Python and set chaiNNer to use system Python. However, if you do this we cannot guarantee everything will work properly.

vkQueueSubmit error with NCNN

  • If you are using an auto-tiling mode, try setting a manual tile size. The lower the number the more likely it will work.

  • If you are on Windows, you can try using this fix suggested by one of the NCNN devs.

"ChaiNNer was unable to install its integrated Python environment" message or simply fails to start up

  • Ensure you have access to the internet and that you did not restrict chaiNNer from accessing the internet. ChaiNNer needs internet access in order to download its Python environment and the required dependencies.

  • If you do have internet access, try going to your %appdata%/chaiNNer (windows), .config/chaiNNer (linux), or Application Support/chaiNNer folder, and deleting the Python folder that is there. This will force chaiNNer to download Python again if the previous download was corrupt.

  • If this all does not work, you can force chaiNNer to use system Python. Right now this is not configurable via the UI without being able to start up first, so you need to go to your data folder mentioned above, go to /settings, and either find or create a use-system-python text file (with no extension) that just contains the word true. In order for this to work, you need to have Python installed to your system.

Integrated Python downloaded, but failed to start up after installing dependencies

  • If you are on an older macOS version, such as any version below 10.15, this is unavoidable and you currently cannot use chaiNNer. This is due to a dependency (opencv) that we simply cannot work around at this time.

07 FAQ (`docs/07--FAQ.md`)

Frequently Asked Questions

Can you add [feature request]?

  • Maybe! But most likely it will be quite a while before we're able to implement any suggestions. We have a large backlog of major things we want to get done before we start working on other major things. However, if it's a small request, we may be able to implement it sooner.

How can I batch upscale?

  • Any of the iterators.
  • No. This will be possible once we do our iterator rewrite. For now though, this isn't possible.

Can you add [random upscaling network] support?

  • Maybe. We implement various upscaling architectures in a different way from most other applicatons. Most other apps just make a wrapper around the official CLI version of whatever network it is. We actually integrate the architecture into our code, and do complex parsing to determine the parameters of the models. This results in a really seamless and straightforward user experience, with a downside of it taking a long time to implement each network.

What does the name mean?

  • chaiNNer is a play on the fact that you can "chain" different tasks together, with the NN in the name being a common abbreviation for Neural Networks. This is following the brilliant naming scheme of victorca25's machine learning tools (traiNNer, iNNfer, augmeNNt) which he granted me permission to use for this as well.

Why not just use Cupscale/IEU/CLI?

  • All of these tools are viable options, but as anyone who has used them before knows they can be limited in what they can do. Many features like chaining or interpolating models are hardcoded and provide little flexibility. Certain features that would be useful, like being able to use a separate model on the alpha layer of an image for example, just do not exist in Cupscale. Inversely, you can pretty much do whatever you want with chaiNNer provided there are nodes implemented. Whatever weird feature you want implemented, you can implement yourself by connecting nodes however you want. Cupscale also does not have other image processing abilities like chaiNNer does, such as adjusting contrast.

  • Cupscale and IEU are also seemingly no longer maintained at the moment, while chaiNNer is being actively worked on still.

Wouldn't this make it more difficult to do things?

  • In a way, yes. Similarly to how programming your own script to do this stuff is more difficult, chaiNNer will also be a bit more difficult than simply dragging and dropping an image and messing with some sliders and pressing an upscale button. However, this gives you a lot more flexibility in what you can do. The added complexity is really just connecting some dots together to do what you want. That doesn't sound that bad, right?

Home (`docs/Home.md`)

Welcome to the ChaiNNer Wiki !

This wiki serves as a comprehensive guide to ChaiNNer, a visual programming language and development environment for chaining image processing tasks together. Below are the key sections of the documentation:

Table of Contents

[[01--Contributing]] - Overview of how to contribute to the ChaiNNer project, with setup instructions and project structure.

[[02--Data-Representation]] - Details on how ChaiNNer handles various data types such as images, text, and numbers, along with conventions for node implementations.

[[03--Nodes]] - Information about nodes in ChaiNNer, covering their structure, rules, and methods for implementation.

[[04--Navi]] - Overview of the Navi type system used for static validation and real-time feedback in ChaiNNer.

[[05--CLI]] - Instructions for operating ChaiNNer in command-line interface mode, allowing for chain execution without a GUI.

[[06--Troubleshooting]] - Solutions to common issues encountered while using ChaiNNer, addressing installation and runtime problems.

[[07--FAQ]] - Answers to frequently asked questions about ChaiNNer, including feature requests and usage scenarios.

Additional Resources

For more information, check out the individual sections linked above. If you have any questions or need further assistance, feel free to reach out to the community in discord or open an issue on the GitHub repository.


Thank you for visiting the ChaiNNer wiki!


--- METRICS ---