├── .envrc ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── other.md └── workflows │ ├── build-site.yml │ ├── lint-backend.yml │ ├── lint-frontend.yml │ ├── make.yml │ ├── release-test.yml │ ├── release.yml │ ├── test-backend.yml │ └── test-frontend.yml ├── .gitignore ├── .prettierrc.json ├── .stylelintrc.json ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── backend ├── src │ ├── api │ │ ├── __init__.py │ │ ├── api.py │ │ ├── group.py │ │ ├── input.py │ │ ├── iter.py │ │ ├── lazy.py │ │ ├── node_check.py │ │ ├── node_context.py │ │ ├── node_data.py │ │ ├── output.py │ │ ├── settings.py │ │ └── types.py │ ├── chain │ │ ├── cache.py │ │ ├── chain.py │ │ ├── input.py │ │ ├── json.py │ │ └── optimize.py │ ├── custom_types.py │ ├── dependencies │ │ ├── __init__.py │ │ ├── install_server_deps.py │ │ ├── store.py │ │ └── whls │ │ │ ├── README.md │ │ │ ├── Sanic-Cors │ │ │ ├── LICENSE │ │ │ └── Sanic_Cors-2.2.0-py2.py3-none-any.whl │ │ │ ├── aiofiles │ │ │ ├── LICENSE │ │ │ └── aiofiles-23.1.0-py3-none-any.whl │ │ │ ├── chainner-pip │ │ │ └── chainner_pip-23.2.0-py3-none-any.whl │ │ │ ├── html5tagger │ │ │ ├── LICENSE │ │ │ └── html5tagger-1.3.0-py3-none-any.whl │ │ │ ├── pynvml │ │ │ ├── LICENSE │ │ │ └── pynvml-11.5.0-py3-none-any.whl │ │ │ ├── sanic-routing │ │ │ ├── LICENSE │ │ │ └── sanic_routing-22.8.0-py3-none-any.whl │ │ │ ├── sanic │ │ │ ├── LICENSE │ │ │ └── sanic-23.3.0-py3-none-any.whl │ │ │ ├── tracerite │ │ │ ├── LICENSE │ │ │ └── tracerite-1.1.0-py3-none-any.whl │ │ │ └── typing_extensions │ │ │ ├── LICENSE │ │ │ └── typing_extensions-4.6.3-py3-none-any.whl │ ├── events.py │ ├── fonts │ │ ├── Apache License.txt │ │ ├── Roboto-Light.ttf │ │ └── Roboto │ │ │ ├── LICENSE.txt │ │ │ ├── Roboto-Bold.ttf │ │ │ ├── Roboto-BoldItalic.ttf │ │ │ ├── Roboto-Italic.ttf │ │ │ └── Roboto-Regular.ttf │ ├── gpu.py │ ├── navi.py │ ├── nodes │ │ ├── __init__.py │ │ ├── condition.py │ │ ├── groups.py │ │ ├── impl │ │ │ ├── __init__.py │ │ │ ├── blend.py │ │ │ ├── caption.py │ │ │ ├── cas.py │ │ │ ├── color │ │ │ │ ├── __init__.py │ │ │ │ ├── color.py │ │ │ │ ├── convert.py │ │ │ │ ├── convert_data.py │ │ │ │ └── convert_model.py │ │ │ ├── color_transfer │ │ │ │ ├── __init__.py │ │ │ │ ├── linear_histogram.py │ │ │ │ ├── mean_std.py │ │ │ │ └── principal_color.py │ │ │ ├── dds │ │ │ │ ├── __init__.py │ │ │ │ ├── format.py │ │ │ │ └── texconv.py │ │ │ ├── dithering │ │ │ │ ├── __init__.py │ │ │ │ ├── constants.py │ │ │ │ └── palette.py │ │ │ ├── ffmpeg.py │ │ │ ├── gradients.py │ │ │ ├── image_formats.py │ │ │ ├── image_op.py │ │ │ ├── image_utils.py │ │ │ ├── ncnn │ │ │ │ ├── __init__.py │ │ │ │ ├── auto_split.py │ │ │ │ ├── model.py │ │ │ │ ├── optimizer.py │ │ │ │ ├── param_schema.json │ │ │ │ └── session.py │ │ │ ├── noise.py │ │ │ ├── noise_functions │ │ │ │ ├── __init__.py │ │ │ │ ├── blue.py │ │ │ │ ├── noise_generator.py │ │ │ │ ├── simplex.py │ │ │ │ └── value.py │ │ │ ├── normals │ │ │ │ ├── __init__.py │ │ │ │ ├── addition.py │ │ │ │ ├── edge_filter.py │ │ │ │ ├── height.py │ │ │ │ └── util.py │ │ │ ├── onnx │ │ │ │ ├── __init__.py │ │ │ │ ├── auto_split.py │ │ │ │ ├── load.py │ │ │ │ ├── model.py │ │ │ │ ├── np_tensor_utils.py │ │ │ │ ├── onnx_to_ncnn.py │ │ │ │ ├── session.py │ │ │ │ ├── tensorproto_utils.py │ │ │ │ ├── update_model_dims.py │ │ │ │ └── utils.py │ │ │ ├── pil_utils.py │ │ │ ├── pytorch │ │ │ │ ├── __init__.py │ │ │ │ ├── auto_split.py │ │ │ │ ├── convert_to_onnx_impl.py │ │ │ │ ├── pix_transform │ │ │ │ │ ├── LICENSE │ │ │ │ │ ├── auto_split.py │ │ │ │ │ ├── pix_transform.py │ │ │ │ │ └── pix_transform_net.py │ │ │ │ ├── rife │ │ │ │ │ ├── IFNet_HDv3_v4_14_align.py │ │ │ │ │ ├── LICENSE │ │ │ │ │ └── warplayer.py │ │ │ │ ├── utils.py │ │ │ │ └── xfeat │ │ │ │ │ ├── LICENSE │ │ │ │ │ ├── xfeat_align.py │ │ │ │ │ └── xfeat_arch.py │ │ │ ├── rembg │ │ │ │ ├── LICENSE.md │ │ │ │ ├── __init__.py │ │ │ │ ├── bg.py │ │ │ │ ├── session_base.py │ │ │ │ ├── session_cloth.py │ │ │ │ ├── session_factory.py │ │ │ │ └── session_simple.py │ │ │ ├── resize.py │ │ │ ├── rust_regex.py │ │ │ ├── tile.py │ │ │ ├── upscale │ │ │ │ ├── __init__.py │ │ │ │ ├── auto_split.py │ │ │ │ ├── auto_split_tiles.py │ │ │ │ ├── basic_upscale.py │ │ │ │ ├── convenient_upscale.py │ │ │ │ ├── exact_split.py │ │ │ │ ├── grayscale.py │ │ │ │ ├── passthrough.py │ │ │ │ ├── tile_blending.py │ │ │ │ └── tiler.py │ │ │ └── video.py │ │ ├── node_cache.py │ │ ├── properties │ │ │ ├── __init__.py │ │ │ ├── inputs │ │ │ │ ├── __init__.py │ │ │ │ ├── __system_inputs.py │ │ │ │ ├── file_inputs.py │ │ │ │ ├── generic_inputs.py │ │ │ │ ├── image_dropdown_inputs.py │ │ │ │ ├── label.py │ │ │ │ ├── ncnn_inputs.py │ │ │ │ ├── numeric_inputs.py │ │ │ │ ├── numpy_inputs.py │ │ │ │ ├── onnx_inputs.py │ │ │ │ └── pytorch_inputs.py │ │ │ └── outputs │ │ │ │ ├── __init__.py │ │ │ │ ├── file_outputs.py │ │ │ │ ├── generic_outputs.py │ │ │ │ ├── ncnn_outputs.py │ │ │ │ ├── numpy_outputs.py │ │ │ │ ├── onnx_outputs.py │ │ │ │ └── pytorch_outputs.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ ├── checked_cast.py │ │ │ ├── format.py │ │ │ ├── replacement.py │ │ │ ├── seed.py │ │ │ ├── unpickler.py │ │ │ └── utils.py │ ├── packages │ │ ├── chaiNNer_external │ │ │ ├── __init__.py │ │ │ ├── external_stable_diffusion │ │ │ │ ├── __init__.py │ │ │ │ └── automatic1111 │ │ │ │ │ ├── clip_interrogate.py │ │ │ │ │ ├── image_to_image.py │ │ │ │ │ ├── inpaint.py │ │ │ │ │ ├── outpaint.py │ │ │ │ │ ├── text_to_image.py │ │ │ │ │ └── upscale.py │ │ │ ├── features.py │ │ │ ├── util.py │ │ │ └── web_ui.py │ │ ├── chaiNNer_ncnn │ │ │ ├── __init__.py │ │ │ ├── ncnn │ │ │ │ ├── __init__.py │ │ │ │ ├── batch_processing │ │ │ │ │ └── load_models.py │ │ │ │ ├── io │ │ │ │ │ ├── load_model.py │ │ │ │ │ └── save_model.py │ │ │ │ ├── processing │ │ │ │ │ └── upscale_image.py │ │ │ │ └── utility │ │ │ │ │ ├── get_model_scale.py │ │ │ │ │ └── interpolate_models.py │ │ │ └── settings.py │ │ ├── chaiNNer_onnx │ │ │ ├── __init__.py │ │ │ ├── onnx │ │ │ │ ├── __init__.py │ │ │ │ ├── batch_processing │ │ │ │ │ └── load_models.py │ │ │ │ ├── io │ │ │ │ │ ├── load_model.py │ │ │ │ │ └── save_model.py │ │ │ │ ├── processing │ │ │ │ │ ├── remove_background.py │ │ │ │ │ └── upscale_image.py │ │ │ │ └── utility │ │ │ │ │ ├── convert_to_ncnn.py │ │ │ │ │ ├── get_model_info.py │ │ │ │ │ ├── interpolate_models.py │ │ │ │ │ └── optimize_model.py │ │ │ └── settings.py │ │ ├── chaiNNer_pytorch │ │ │ ├── __init__.py │ │ │ ├── pytorch │ │ │ │ ├── __init__.py │ │ │ │ ├── io │ │ │ │ │ ├── load_model.py │ │ │ │ │ └── save_model.py │ │ │ │ ├── iteration │ │ │ │ │ └── load_models.py │ │ │ │ ├── processing │ │ │ │ │ ├── align_image_to_reference.py │ │ │ │ │ ├── guided_upscale.py │ │ │ │ │ ├── inpaint.py │ │ │ │ │ ├── upscale_image.py │ │ │ │ │ └── wavelet_color_fix.py │ │ │ │ ├── restoration │ │ │ │ │ └── upscale_face.py │ │ │ │ └── utility │ │ │ │ │ ├── convert_to_ncnn.py │ │ │ │ │ ├── convert_to_onnx.py │ │ │ │ │ ├── get_model_info.py │ │ │ │ │ └── interpolate_models.py │ │ │ └── settings.py │ │ └── chaiNNer_standard │ │ │ ├── __init__.py │ │ │ ├── image │ │ │ ├── __init__.py │ │ │ ├── batch_processing │ │ │ │ ├── load_images.py │ │ │ │ ├── merge_spritesheet.py │ │ │ │ └── split_spritesheet.py │ │ │ ├── create_images │ │ │ │ ├── create_checkerboard.py │ │ │ │ ├── create_color.py │ │ │ │ ├── create_colorwheel.py │ │ │ │ ├── create_gradient.py │ │ │ │ ├── create_noise.py │ │ │ │ └── text_as_image.py │ │ │ ├── io │ │ │ │ ├── load_image.py │ │ │ │ ├── save_image.py │ │ │ │ ├── view_image.py │ │ │ │ └── view_image_external.py │ │ │ └── video_frames │ │ │ │ ├── load_video.py │ │ │ │ └── save_video.py │ │ │ ├── image_adjustment │ │ │ ├── __init__.py │ │ │ ├── adjustments │ │ │ │ ├── brightness_and_contrast.py │ │ │ │ ├── clamp.py │ │ │ │ ├── color_levels.py │ │ │ │ ├── hue_and_saturation.py │ │ │ │ ├── invert_color.py │ │ │ │ ├── opacity.py │ │ │ │ └── stretch_contrast.py │ │ │ ├── arithmetic │ │ │ │ ├── add.py │ │ │ │ ├── divide.py │ │ │ │ ├── multiply.py │ │ │ │ └── premultiplied_alpha.py │ │ │ ├── gamma │ │ │ │ ├── gamma.py │ │ │ │ └── log_to_linear.py │ │ │ └── threshold │ │ │ │ ├── generate_threshold.py │ │ │ │ ├── threshold.py │ │ │ │ └── threshold_adaptive.py │ │ │ ├── image_channel │ │ │ ├── __init__.py │ │ │ ├── all │ │ │ │ ├── combine_rgba.py │ │ │ │ ├── merge_channels.py │ │ │ │ └── separate_rgba.py │ │ │ ├── misc │ │ │ │ ├── alpha_matting.py │ │ │ │ ├── chroma_key.py │ │ │ │ └── fill_alpha.py │ │ │ └── transparency │ │ │ │ ├── merge_transparency.py │ │ │ │ └── split_transparency.py │ │ │ ├── image_dimension │ │ │ ├── __init__.py │ │ │ ├── border │ │ │ │ └── pad.py │ │ │ ├── crop │ │ │ │ ├── crop.py │ │ │ │ ├── crop_border.py │ │ │ │ └── crop_to_content.py │ │ │ ├── resize │ │ │ │ ├── resize.py │ │ │ │ ├── resize_pixel_art.py │ │ │ │ └── resize_to_side.py │ │ │ └── utility │ │ │ │ ├── get_bounding_box.py │ │ │ │ └── get_dimensions.py │ │ │ ├── image_filter │ │ │ ├── __init__.py │ │ │ ├── blur │ │ │ │ ├── box_blur.py │ │ │ │ ├── gaussian_blur.py │ │ │ │ ├── lens_blur.py │ │ │ │ ├── median_blur.py │ │ │ │ └── surface_blur.py │ │ │ ├── correction │ │ │ │ ├── average_color_fix.py │ │ │ │ └── color_transfer.py │ │ │ ├── miscellaneous │ │ │ │ ├── canny_edge_detection.py │ │ │ │ ├── convolve.py │ │ │ │ ├── dilate.py │ │ │ │ ├── distance_transform.py │ │ │ │ ├── edge_detection.py │ │ │ │ ├── erode.py │ │ │ │ ├── high_pass.py │ │ │ │ └── pixelate.py │ │ │ ├── noise │ │ │ │ ├── add_noise.py │ │ │ │ └── denoise.py │ │ │ ├── quantize │ │ │ │ ├── dither.py │ │ │ │ ├── dither_palette.py │ │ │ │ └── quantize_to_reference.py │ │ │ └── sharpen │ │ │ │ ├── high_boost_filter.py │ │ │ │ └── unsharp_mask.py │ │ │ ├── image_utility │ │ │ ├── __init__.py │ │ │ ├── compositing │ │ │ │ ├── add_caption.py │ │ │ │ ├── blend_images.py │ │ │ │ ├── stack_images.py │ │ │ │ └── z_stack_images.py │ │ │ ├── miscellaneous │ │ │ │ ├── apply_palette.py │ │ │ │ ├── change_color_model.py │ │ │ │ ├── generate_hash.py │ │ │ │ ├── image_metrics.py │ │ │ │ ├── image_statistics.py │ │ │ │ ├── inpaint.py │ │ │ │ ├── palette_from_image.py │ │ │ │ └── pick_color.py │ │ │ └── modification │ │ │ │ ├── flip.py │ │ │ │ ├── rotate.py │ │ │ │ └── shift.py │ │ │ ├── material_textures │ │ │ ├── __init__.py │ │ │ ├── conversion │ │ │ │ ├── metal_to_specular.py │ │ │ │ └── specular_to_metal.py │ │ │ └── normal_map │ │ │ │ ├── add_normals.py │ │ │ │ ├── balance_normals.py │ │ │ │ ├── convert_normals.py │ │ │ │ ├── normal_map_generator.py │ │ │ │ ├── normalize_normals.py │ │ │ │ └── scale_normals.py │ │ │ └── utility │ │ │ ├── __init__.py │ │ │ ├── clipboard │ │ │ └── copy_to_clipboard.py │ │ │ ├── color │ │ │ ├── color.py │ │ │ ├── color_from.py │ │ │ └── separate_color.py │ │ │ ├── directory │ │ │ ├── directory_go_into.py │ │ │ ├── directory_go_up.py │ │ │ └── directory_to_text.py │ │ │ ├── math │ │ │ ├── accumulate.py │ │ │ ├── compare.py │ │ │ ├── logic_operation.py │ │ │ ├── math.py │ │ │ └── round.py │ │ │ ├── random │ │ │ ├── derive_seed.py │ │ │ └── random_number.py │ │ │ ├── text │ │ │ ├── note.py │ │ │ ├── regex_find.py │ │ │ ├── regex_replace.py │ │ │ ├── text_append.py │ │ │ ├── text_length.py │ │ │ ├── text_padding.py │ │ │ ├── text_pattern.py │ │ │ ├── text_replace.py │ │ │ └── text_slice.py │ │ │ └── value │ │ │ ├── conditional.py │ │ │ ├── directory.py │ │ │ ├── execution_number.py │ │ │ ├── number.py │ │ │ ├── parse_number.py │ │ │ ├── pass_through.py │ │ │ ├── percent.py │ │ │ ├── range.py │ │ │ ├── resolutions.py │ │ │ ├── switch.py │ │ │ └── text.py │ ├── process.py │ ├── progress_controller.py │ ├── response.py │ ├── run.py │ ├── server.py │ ├── server_config.py │ ├── server_host.py │ ├── server_process_helper.py │ ├── system.py │ ├── texconv │ │ ├── LICENSE │ │ └── texconv.exe │ └── util.py └── tests │ ├── test_dummy.py │ └── test_util.py ├── docs ├── CONTRIBUTING.md ├── FAQ.md ├── assets │ ├── banner.png │ ├── input-override.png │ ├── screenshot.png │ └── simple_screenshot.png ├── cli.md ├── data-representation.md ├── navi.md ├── nodes.md └── troubleshooting.md ├── forge.config.js ├── index.html ├── package-lock.json ├── package.json ├── patches └── @electron-forge+plugin-vite+7.4.0.patch ├── pyproject.toml ├── pyrightconfig.json ├── requirements.txt ├── src ├── common │ ├── 2d.ts │ ├── Backend.ts │ ├── CategoryMap.ts │ ├── IdSet.ts │ ├── PassthroughMap.ts │ ├── SchemaInputsMap.ts │ ├── SchemaMap.ts │ ├── Validity.ts │ ├── color-json-util.ts │ ├── common-types.ts │ ├── formatExecutionErrorMessage.ts │ ├── group-inputs.ts │ ├── i18n.ts │ ├── input-override-common.ts │ ├── links.ts │ ├── locales │ │ └── en │ │ │ └── translation.json │ ├── log.ts │ ├── migrations-legacy.js │ ├── migrations.ts │ ├── nodes │ │ ├── EdgeState.ts │ │ ├── TypeState.ts │ │ ├── checkFeatures.ts │ │ ├── checkNodeValidity.ts │ │ ├── condition.ts │ │ ├── connectedInputs.ts │ │ ├── disabled.ts │ │ ├── groupStacks.ts │ │ ├── inputCondition.ts │ │ ├── keyInfo.ts │ │ ├── lineage.ts │ │ ├── optimize.ts │ │ ├── parseFunctionDefinitions.ts │ │ ├── sideEffect.ts │ │ ├── sort.ts │ │ └── toBackendJson.ts │ ├── rust-regex.ts │ ├── safeIpc.ts │ ├── settings │ │ ├── migration.ts │ │ └── settings.ts │ ├── types │ │ ├── assign.ts │ │ ├── chainner-builtin.ts │ │ ├── chainner-scope.ts │ │ ├── explain.ts │ │ ├── function.ts │ │ ├── json.ts │ │ ├── mismatch.ts │ │ ├── pretty.ts │ │ └── util.ts │ ├── ui │ │ ├── error.ts │ │ ├── interrupt.ts │ │ └── progress.ts │ ├── util.ts │ └── version.ts ├── custom.d.ts ├── globals.d.ts ├── i18next.d.ts ├── main │ ├── SaveFile.ts │ ├── arguments.ts │ ├── backend │ │ ├── process.ts │ │ └── setup.ts │ ├── childProc.ts │ ├── cli │ │ ├── create.ts │ │ ├── exit.ts │ │ └── run.ts │ ├── env.ts │ ├── fileWatcher.ts │ ├── gui │ │ ├── create.ts │ │ ├── main-window.ts │ │ └── menu.ts │ ├── i18n.ts │ ├── input-override.ts │ ├── main.ts │ ├── platform.ts │ ├── preload.ts │ ├── python │ │ ├── checkPythonPaths.ts │ │ ├── integratedPython.ts │ │ └── version.ts │ ├── safeIpc.ts │ ├── setting-storage.ts │ ├── squirrel.ts │ ├── systemInfo.ts │ └── util.ts ├── public │ ├── Info.plist │ ├── dmg-background.png │ ├── fonts │ │ ├── Noto Emoji │ │ │ ├── NotoEmoji-VariableFont_wght.ttf │ │ │ └── OFL.txt │ │ ├── Open Sans │ │ │ ├── OFL.txt │ │ │ ├── OpenSans-Italic-VariableFont_wdth,wght.ttf │ │ │ └── OpenSans-VariableFont_wdth,wght.ttf │ │ └── Roboto Mono │ │ │ ├── LICENSE.txt │ │ │ └── RobotoMono-VariableFont_wght.ttf │ ├── icons │ │ ├── cross_platform │ │ │ ├── icon.icns │ │ │ ├── icon.ico │ │ │ └── icon.png │ │ ├── mac │ │ │ ├── file_chn.icns │ │ │ └── icon.icns │ │ ├── png │ │ │ ├── 1024x1024.png │ │ │ ├── 128x128.png │ │ │ ├── 16x16.png │ │ │ ├── 24x24.png │ │ │ ├── 256x256.png │ │ │ ├── 32x32.png │ │ │ ├── 48x48.png │ │ │ ├── 512x512.png │ │ │ └── 64x64.png │ │ └── win │ │ │ ├── icon.ico │ │ │ └── installing_loop.gif │ └── splash_imgs │ │ ├── background.png │ │ └── front.png └── renderer │ ├── app.tsx │ ├── colors.scss │ ├── components │ ├── CustomEdge │ │ ├── CustomEdge.scss │ │ └── CustomEdge.tsx │ ├── CustomIcons.tsx │ ├── DependencyManagerButton.tsx │ ├── Handle.tsx │ ├── Header │ │ ├── AppInfo.tsx │ │ ├── ExecutionButtons.tsx │ │ ├── Header.tsx │ │ └── KoFiButton.tsx │ ├── HistoryProvider.tsx │ ├── IfVisible.tsx │ ├── Markdown.tsx │ ├── NodeDocumentation │ │ ├── ConditionExplanation.tsx │ │ ├── DropDownOptions.tsx │ │ ├── HighlightContainer.tsx │ │ ├── NodeDocs.tsx │ │ ├── NodeDocumentationModal.tsx │ │ ├── NodeExample.tsx │ │ ├── NodesList.tsx │ │ └── SchemaLink.tsx │ ├── NodeSelectorPanel │ │ ├── FavoritesAccordionItem.tsx │ │ ├── NodeRepresentative.tsx │ │ ├── NodeSelectorPanel.tsx │ │ ├── RegularAccordionItem.tsx │ │ ├── SubcategoryHeading.tsx │ │ └── TextBox.tsx │ ├── PaneNodeSearchMenu.tsx │ ├── ReactFlowBox.tsx │ ├── SearchBar.tsx │ ├── SettingsModal.tsx │ ├── SystemStats.tsx │ ├── TypeTag.tsx │ ├── chaiNNerLogo.tsx │ ├── groups │ │ ├── ConditionalGroup.tsx │ │ ├── FromToDropdownsGroup.tsx │ │ ├── Group.tsx │ │ ├── IconSetGroup.tsx │ │ ├── LinkedInputsGroup.tsx │ │ ├── MenuIconRowGroup.tsx │ │ ├── NcnnFileInputsGroup.tsx │ │ ├── OptionalInputsGroup.tsx │ │ ├── RequiredGroup.tsx │ │ ├── SeedGroup.tsx │ │ ├── props.ts │ │ └── util.ts │ ├── inputs │ │ ├── ColorInput.tsx │ │ ├── DirectoryInput.tsx │ │ ├── DropDownInput.tsx │ │ ├── FileInput.tsx │ │ ├── GenericInput.tsx │ │ ├── InputContainer.tsx │ │ ├── NumberInput.tsx │ │ ├── SchemaInput.tsx │ │ ├── SliderInput.tsx │ │ ├── StaticValueInput.tsx │ │ ├── TextInput.tsx │ │ ├── elements │ │ │ ├── AdvanceNumberInput.tsx │ │ │ ├── AdvancedNumberInput.scss │ │ │ ├── AnchorSelector.tsx │ │ │ ├── Checkbox.scss │ │ │ ├── Checkbox.tsx │ │ │ ├── ColorBoxButton.tsx │ │ │ ├── ColorCompare.tsx │ │ │ ├── ColorKindSelector.tsx │ │ │ ├── ColorPicker.tsx │ │ │ ├── ColorSlider.tsx │ │ │ ├── Dropdown.tsx │ │ │ ├── IconList.tsx │ │ │ ├── RgbHexInput.tsx │ │ │ ├── StyledSlider.tsx │ │ │ └── TabList.tsx │ │ └── props.ts │ ├── node │ │ ├── BreakPoint.tsx │ │ ├── CollapsedHandles.tsx │ │ ├── Node.tsx │ │ ├── NodeBody.tsx │ │ ├── NodeFooter │ │ │ ├── DisableSwitch.tsx │ │ │ ├── NodeFooter.tsx │ │ │ ├── Timer.tsx │ │ │ └── ValidityIndicator.tsx │ │ ├── NodeHeader.tsx │ │ ├── NodeInputs.tsx │ │ ├── NodeOutputs.tsx │ │ └── special │ │ │ └── NoteNode.tsx │ ├── outputs │ │ ├── GenericOutput.tsx │ │ ├── LargeImageOutput.tsx │ │ ├── OutputContainer.tsx │ │ ├── TaggedOutput.tsx │ │ ├── elements │ │ │ └── ModelDataTags.tsx │ │ └── props.ts │ └── settings │ │ ├── SettingContainer.tsx │ │ ├── SettingItem.tsx │ │ ├── components.tsx │ │ └── props.ts │ ├── contexts │ ├── AlertBoxContext.tsx │ ├── BackendContext.tsx │ ├── CollapsedNodeContext.tsx │ ├── ContextMenuContext.tsx │ ├── DependencyContext.tsx │ ├── ExecutionContext.tsx │ ├── FakeExampleContext.tsx │ ├── GlobalNodeState.tsx │ ├── HotKeyContext.tsx │ ├── InputContext.tsx │ ├── NodeDocumentationContext.tsx │ └── SettingsContext.tsx │ ├── env.ts │ ├── global.scss │ ├── helpers │ ├── accentColors.ts │ ├── canConnect.ts │ ├── chainProgress.ts │ ├── color.ts │ ├── colorTools.ts │ ├── copyAndPaste.ts │ ├── dataTransfer.ts │ ├── github.ts │ ├── graphUtils.ts │ ├── naviHelpers.ts │ ├── nodeScreenshot.ts │ ├── nodeSearchFuncs.ts │ ├── nodeState.ts │ ├── reactFlowUtil.ts │ ├── sliderScale.ts │ └── types.ts │ ├── hooks │ ├── useAsyncEffect.ts │ ├── useAutomaticFeatures.ts │ ├── useBackendEventSource.ts │ ├── useBatchedCallback.ts │ ├── useChangeCounter.ts │ ├── useColorModels.ts │ ├── useContextMenu.ts │ ├── useDevicePixelRatio.ts │ ├── useDisabled.ts │ ├── useEdgeMenu.tsx │ ├── useEventBacklog.ts │ ├── useHotkeys.ts │ ├── useInputHashes.ts │ ├── useInputRefactor.tsx │ ├── useInterval.ts │ ├── useIpcRendererListener.ts │ ├── useIsCollapsedNode.ts │ ├── useLastDirectory.ts │ ├── useLastWindowSize.ts │ ├── useMemo.ts │ ├── useNodeFavorites.ts │ ├── useNodeMenu.scss │ ├── useNodeMenu.tsx │ ├── useNodesMenu.tsx │ ├── useOpenRecent.ts │ ├── useOutputDataStore.ts │ ├── usePaneNodeSearchMenu.tsx │ ├── usePassthrough.ts │ ├── usePrevious.ts │ ├── useRunNode.ts │ ├── useSessionStorage.ts │ ├── useSettings.ts │ ├── useSourceTypeColor.ts │ ├── useStored.ts │ ├── useThemeColor.ts │ ├── useTypeColor.ts │ ├── useTypeMap.ts │ ├── useValidDropDownValue.ts │ ├── useValidity.ts │ └── useWatchFiles.ts │ ├── i18n.ts │ ├── index.tsx │ ├── main.tsx │ ├── renderer.ts │ ├── safeIpc.ts │ └── theme.ts ├── tests ├── common │ ├── SaveFile.test.ts │ ├── __snapshots__ │ │ ├── SaveFile.test.ts.snap │ │ └── settings.test.ts.snap │ ├── chainner-scope.test.ts │ ├── settings.test.ts │ └── util.test.ts └── data │ ├── DiffusePBR.chn │ ├── add noise with seed edge.chn │ ├── big ol test.chn │ ├── blend-images.chn │ ├── box-median-blur.chn │ ├── canny-edge-detection.chn │ ├── color-transfer.chn │ ├── combine-rgba.chn │ ├── convert-onnx-update.chn │ ├── convert-to-ncnn.chn │ ├── copy-to-clipboard.chn │ ├── create-border-migration.chn │ ├── create-color-old.chn │ ├── create-edges.chn │ ├── crop-content.chn │ ├── crop.chn │ ├── empty-string-input-test.chn │ ├── fast-nlmeans.chn │ ├── gamma.chn │ ├── image-adjustments.chn │ ├── image-channels.chn │ ├── image-dim.chn │ ├── image-filters.chn │ ├── image-input-output.chn │ ├── image-iterator.chn │ ├── image-metrics.chn │ ├── image-utilities.chn │ ├── metal-specular.chn │ ├── model-scale.chn │ ├── ncnn.chn │ ├── normal-map-gen-invert.chn │ ├── normal-map-generator.chn │ ├── onnx-interpolate.chn │ ├── opacity.chn │ ├── pass-through.chn │ ├── pytorch-scunet.chn │ ├── pytorch.chn │ ├── resize-to-side.chn │ ├── rnd.chn │ ├── save video input.chn │ ├── save-image-webp-lossless.chn │ ├── text-as-image.chn │ ├── text-pattern.chn │ ├── unified-resize.chn │ ├── utilities.chn │ └── video-frame-iterator.chn ├── tsconfig.json └── vite ├── base.config.ts ├── forge-types.ts ├── main.config.ts ├── preload.config.ts └── renderer.config.ts /.envrc: -------------------------------------------------------------------------------- 1 | NODEVERSION=18 2 | nvmrc=~/.nvm/nvm.sh 3 | if [ -e $nvmrc ]; then 4 | source $nvmrc 5 | nvm use $NODEVERSION 6 | fi 7 | 8 | PATH_add node_modules/.bin 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=LF 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: jballentine 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve. 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | <!-- 11 | Before you make open an issue, please search for your problem using the search bar here: https://github.com/chaiNNer-org/chaiNNer/issues 12 | 13 | Many problems are reported to us multiple times, so please try to find your problem before opening a new issue. 14 | --> 15 | 16 | **Information:** 17 | 18 | - Chainner version: [e.g. 0.11.0] 19 | - OS: [e.g. Windows 10, macOS 10.13] 20 | 21 | **Description** 22 | A clear and concise description of what the bug is and how to reproduce it. 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Logs** 26 | Logs help us to find the cause of many problems. To provide your logs, open Chainner and click _Help_ > _Open logs folder_ in the menu. Create a zip file with `main.log` and `renderer.log`, and [attach the zip file](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/attaching-files) to this issue. 27 | 28 | Privacy information: Log files contain file paths of Chainner's application files and the files you processed with Chainner. These file paths may contain the name of your account (e.g. Users/michael/...). If your account is your real name, and you are not comfortable sharing, then do not upload your logs or replace all identifying text with something like "fake name". 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature or change to existing features. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Motivation** 11 | Please explain the problem you're having/why you propose this feature. Ex. I'm always frustrated when [...] 12 | 13 | **Description** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Alternatives** 17 | A clear and concise description of any alternative solutions or features you've considered, if applicable. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: A report or questions that doesn't fit in the other categories. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/workflows/build-site.yml: -------------------------------------------------------------------------------- 1 | name: Build chaiNNer.app 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | - edited 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build-site: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Trigger chaiNNer.app build + deploy 16 | uses: fjogeleit/http-request-action@v1 17 | with: 18 | url: 'https://api.github.com/repos/chaiNNer-org/chaiNNer-org.github.io/dispatches' 19 | method: 'POST' 20 | customHeaders: '{"Accept": "application/vnd.github+json"}' 21 | bearerToken: ${{ secrets.GH_TEST_TOKEN }} 22 | data: '{"event_type": "webhook"}' 23 | -------------------------------------------------------------------------------- /.github/workflows/lint-frontend.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Lint 4 | 5 | # Controls when the workflow will run 6 | on: 7 | pull_request: 8 | branches: ['*'] 9 | types: 10 | - opened 11 | - synchronize 12 | - closed 13 | paths-ignore: 14 | - 'backend/**' 15 | - 'requirements.txt' 16 | - '.pylintrc' 17 | - 'README.md' 18 | push: 19 | branches: [main] 20 | 21 | # Allows you to run this workflow manually from the Actions tab 22 | workflow_dispatch: 23 | 24 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 25 | jobs: 26 | frontend-lint: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v3 30 | - uses: actions/setup-node@v3 31 | with: 32 | node-version: 16 33 | cache: 'npm' 34 | - run: npm ci 35 | - name: eslint 36 | run: npm run lint:js-ci 37 | - name: typescript 38 | run: npx tsc 39 | -------------------------------------------------------------------------------- /.github/workflows/test-frontend.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | # Controls when the workflow will run 4 | on: 5 | pull_request: 6 | branches: ['*'] 7 | types: 8 | - opened 9 | - synchronize 10 | - closed 11 | paths-ignore: 12 | - 'backend/**' 13 | - 'requirements.txt' 14 | - '.pylintrc' 15 | - 'README.md' 16 | push: 17 | branches: [main] 18 | 19 | # Allows you to run this workflow manually from the Actions tab 20 | workflow_dispatch: 21 | 22 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 23 | jobs: 24 | frontend-tests: 25 | runs-on: ubuntu-latest 26 | strategy: 27 | matrix: 28 | node-version: [16, 18] 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: actions/setup-node@v3 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | cache: 'npm' 35 | - run: npm ci 36 | - run: npm run test:js 37 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "singleAttributePerLine": true, 5 | "tabWidth": 4, 6 | "overrides": [ 7 | { 8 | "files": ["package.json", "package-lock.json"], 9 | "options": { 10 | "tabWidth": 2 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard-scss", 4 | "stylelint-prettier/recommended", 5 | "stylelint-config-prettier-scss" 6 | ], 7 | "rules": { 8 | "custom-property-empty-line-before": null, 9 | "no-descending-specificity": null, 10 | "selector-class-pattern": "^[a-z]+(?:-[a-z]+)*(?:__[a-z]+(?:-[a-z]+)*)?quot; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.vscode-pylance", 4 | "streetsidesoftware.code-spell-checker", 5 | "dbaeumer.vscode-eslint", 6 | "charliermarsh.ruff", 7 | "esbenp.prettier-vscode" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Attach Debugger to chaiNNer", 9 | "type": "python", 10 | "request": "attach", 11 | "connect": { 12 | "host": "localhost", 13 | "port": 5678 14 | }, 15 | "justMyCode": true 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import * 2 | from .group import * 3 | from .input import * 4 | from .iter import * 5 | from .lazy import * 6 | from .node_context import * 7 | from .node_data import * 8 | from .output import * 9 | from .settings import * 10 | from .types import * 11 | -------------------------------------------------------------------------------- /backend/src/api/group.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Generic, NewType, TypeVar, Union 4 | 5 | from .input import BaseInput 6 | from .types import InputId 7 | 8 | T = TypeVar("T") 9 | 10 | 11 | GroupId = NewType("GroupId", int) 12 | 13 | 14 | class GroupInfo: 15 | def __init__( 16 | self, 17 | group_id: GroupId, 18 | kind: str, 19 | options: dict[str, Any] | None = None, 20 | ) -> None: 21 | self.id: GroupId = group_id 22 | self.kind: str = kind 23 | self.options: dict[str, Any] = {} if options is None else options 24 | 25 | 26 | class Group(Generic[T]): 27 | def __init__(self, info: GroupInfo, items: list[T]) -> None: 28 | self.info: GroupInfo = info 29 | self.items: list[T] = items 30 | 31 | def to_dict(self): 32 | return { 33 | "id": self.info.id, 34 | "kind": self.info.kind, 35 | "options": self.info.options, 36 | "items": [i.to_dict() if isinstance(i, Group) else i for i in self.items], 37 | } 38 | 39 | 40 | NestedGroup = Group[Union[BaseInput, "NestedGroup"]] 41 | NestedIdGroup = Group[Union[InputId, "NestedIdGroup"]] 42 | 43 | 44 | # pylint: disable-next=redefined-builtin 45 | def group(kind: str, options: dict[str, Any] | None = None, id: int = -1): 46 | info = GroupInfo(GroupId(id), kind, options) 47 | 48 | def ret(*items: BaseInput | NestedGroup) -> NestedGroup: 49 | return Group(info, list(items)) 50 | 51 | return ret 52 | -------------------------------------------------------------------------------- /backend/src/api/types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Callable, Literal, NewType 4 | 5 | NodeId = NewType("NodeId", str) 6 | InputId = NewType("InputId", int) 7 | OutputId = NewType("OutputId", int) 8 | IterInputId = NewType("IterInputId", int) 9 | IterOutputId = NewType("IterOutputId", int) 10 | FeatureId = NewType("FeatureId", str) 11 | 12 | 13 | RunFn = Callable[..., Any] 14 | 15 | NodeKind = Literal["regularNode", "generator", "collector"] 16 | -------------------------------------------------------------------------------- /backend/src/custom_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Awaitable, Callable, Union 4 | 5 | UpdateProgressFn = Callable[[str, float, Union[float, None]], Awaitable[None]] 6 | -------------------------------------------------------------------------------- /backend/src/dependencies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/dependencies/__init__.py -------------------------------------------------------------------------------- /backend/src/dependencies/whls/README.md: -------------------------------------------------------------------------------- 1 | # Bundled Wheels 2 | 3 | This is where we can store wheel files that are to be bundled with chaiNNer, in order to avoid needing to download them. 4 | 5 | ## Requirements 6 | 7 | Bundled wheels must be 8 | 9 | 1. Reasonably small (a few MB max) 10 | 2. py3-none-any (compatible with any python version and device) 11 | 3. License compatible (allows bundling) 12 | 13 | ## Goals 14 | 15 | - Speed up initial start time by downloading the minimal number of wheel files from the internet 16 | - Not increase chaiNNer's bundle size too much 17 | 18 | ## Structure 19 | 20 | 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. 21 | -------------------------------------------------------------------------------- /backend/src/dependencies/whls/Sanic-Cors/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present Sanic Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/src/dependencies/whls/Sanic-Cors/Sanic_Cors-2.2.0-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/dependencies/whls/Sanic-Cors/Sanic_Cors-2.2.0-py2.py3-none-any.whl -------------------------------------------------------------------------------- /backend/src/dependencies/whls/aiofiles/aiofiles-23.1.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/dependencies/whls/aiofiles/aiofiles-23.1.0-py3-none-any.whl -------------------------------------------------------------------------------- /backend/src/dependencies/whls/chainner-pip/chainner_pip-23.2.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/dependencies/whls/chainner-pip/chainner_pip-23.2.0-py3-none-any.whl -------------------------------------------------------------------------------- /backend/src/dependencies/whls/html5tagger/LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /backend/src/dependencies/whls/html5tagger/html5tagger-1.3.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/dependencies/whls/html5tagger/html5tagger-1.3.0-py3-none-any.whl -------------------------------------------------------------------------------- /backend/src/dependencies/whls/pynvml/pynvml-11.5.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/dependencies/whls/pynvml/pynvml-11.5.0-py3-none-any.whl -------------------------------------------------------------------------------- /backend/src/dependencies/whls/sanic-routing/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present Sanic Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/src/dependencies/whls/sanic-routing/sanic_routing-22.8.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/dependencies/whls/sanic-routing/sanic_routing-22.8.0-py3-none-any.whl -------------------------------------------------------------------------------- /backend/src/dependencies/whls/sanic/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present Sanic Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/src/dependencies/whls/sanic/sanic-23.3.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/dependencies/whls/sanic/sanic-23.3.0-py3-none-any.whl -------------------------------------------------------------------------------- /backend/src/dependencies/whls/tracerite/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present Sanic Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/src/dependencies/whls/tracerite/tracerite-1.1.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/dependencies/whls/tracerite/tracerite-1.1.0-py3-none-any.whl -------------------------------------------------------------------------------- /backend/src/dependencies/whls/typing_extensions/typing_extensions-4.6.3-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/dependencies/whls/typing_extensions/typing_extensions-4.6.3-py3-none-any.whl -------------------------------------------------------------------------------- /backend/src/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /backend/src/fonts/Roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/fonts/Roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /backend/src/fonts/Roboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/fonts/Roboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /backend/src/fonts/Roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/fonts/Roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /backend/src/fonts/Roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/fonts/Roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /backend/src/nodes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/impl/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/color/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/impl/color/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/color_transfer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/impl/color_transfer/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/dds/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/impl/dds/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/dithering/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/impl/dithering/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/ncnn/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/impl/ncnn/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/noise_functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/impl/noise_functions/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/noise_functions/noise_generator.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | import numpy as np 4 | 5 | 6 | class NoiseGenerator(ABC): 7 | @abstractmethod 8 | def evaluate(self, points: np.ndarray) -> np.ndarray: ... 9 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/normals/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/impl/normals/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/normals/height.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import numpy as np 4 | 5 | from ...utils.utils import get_h_w_c 6 | 7 | 8 | class HeightSource(Enum): 9 | AVERAGE_RGB = 0 10 | MAX_RGB = 1 11 | # 1 - ((1-r) * (1-g) * (1-b)) 12 | SCREEN_RGB = 2 13 | RED = 3 14 | GREEN = 4 15 | BLUE = 5 16 | ALPHA = 6 17 | 18 | 19 | def get_height_map(img: np.ndarray, source: HeightSource) -> np.ndarray: 20 | """ 21 | Converts the given color/grayscale image to a height map. 22 | """ 23 | h, w, c = get_h_w_c(img) 24 | 25 | assert c in (1, 3, 4), "Only grayscale, RGB, and RGBA images are supported" 26 | 27 | if source == HeightSource.ALPHA: 28 | if c < 4: 29 | return np.ones((h, w), dtype=np.float32) 30 | return img[:, :, 3] 31 | 32 | if c == 1: 33 | if source == HeightSource.SCREEN_RGB: 34 | x = 1 - img 35 | return 1 - x * x * x 36 | return img 37 | 38 | r = img[:, :, 2] 39 | g = img[:, :, 1] 40 | b = img[:, :, 0] 41 | 42 | if source == HeightSource.RED: 43 | return r 44 | elif source == HeightSource.GREEN: 45 | return g 46 | elif source == HeightSource.BLUE: 47 | return b 48 | elif source == HeightSource.MAX_RGB: 49 | return np.maximum(np.maximum(r, g), b) 50 | elif source == HeightSource.AVERAGE_RGB: 51 | return (r + g + b) / 3 52 | elif source == HeightSource.SCREEN_RGB: 53 | return 1 - ((1 - r) * (1 - g) * (1 - b)) 54 | else: 55 | raise AssertionError(f"Invalid height source {source}.") 56 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/onnx/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/impl/onnx/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/pytorch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/impl/pytorch/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/pytorch/pix_transform/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Riccardo de Lutio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/pytorch/rife/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Megvii Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/pytorch/rife/warplayer.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | import torch 3 | 4 | backwarp_tenGrid = {} # noqa: N816 5 | 6 | 7 | def warp(tenInput, tenFlow, device): # noqa: ANN001, N803 8 | k = (str(tenFlow.device), str(tenFlow.size())) 9 | if k not in backwarp_tenGrid: 10 | tenHorizontal = ( # noqa: N806 11 | torch.linspace(-1.0, 1.0, tenFlow.shape[3], device=device) 12 | .view(1, 1, 1, tenFlow.shape[3]) 13 | .expand(tenFlow.shape[0], -1, tenFlow.shape[2], -1) 14 | ) 15 | tenVertical = ( # noqa: N806 16 | torch.linspace(-1.0, 1.0, tenFlow.shape[2], device=device) 17 | .view(1, 1, tenFlow.shape[2], 1) 18 | .expand(tenFlow.shape[0], -1, -1, tenFlow.shape[3]) 19 | ) 20 | backwarp_tenGrid[k] = torch.cat([tenHorizontal, tenVertical], 1).to(device) 21 | 22 | tenFlow = torch.cat( # noqa: N806 23 | [ 24 | tenFlow[:, 0:1, :, :] / ((tenInput.shape[3] - 1.0) / 2.0), 25 | tenFlow[:, 1:2, :, :] / ((tenInput.shape[2] - 1.0) / 2.0), 26 | ], 27 | 1, 28 | ) 29 | 30 | g = (backwarp_tenGrid[k] + tenFlow).permute(0, 2, 3, 1) 31 | tenOutput = torch.nn.functional.grid_sample( 32 | input=tenInput, 33 | grid=g, 34 | mode="bicubic", 35 | padding_mode="border", 36 | align_corners=True, 37 | ) 38 | return tenOutput 39 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/rembg/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Daniel Gatis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/rembg/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Rembg code is modified from and copyright of Daniel Gatis, 3 | and can be found here: https://github.com/danielgatis/rembg 4 | """ 5 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/rembg/session_base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | 5 | import numpy as np 6 | import onnxruntime as ort 7 | 8 | from nodes.impl.resize import ResizeFilter, resize 9 | 10 | 11 | class BaseSession(ABC): 12 | def __init__( 13 | self, 14 | inner_session: ort.InferenceSession, 15 | mean: tuple[float, float, float], 16 | std: tuple[float, float, float], 17 | size: tuple[int, int], 18 | ): 19 | self.inner_session = inner_session 20 | self.mean = mean 21 | self.std = std 22 | self.size = size 23 | 24 | def normalize(self, img: np.ndarray) -> dict[str, np.ndarray]: 25 | img = resize(img, self.size, ResizeFilter.LANCZOS) 26 | 27 | tmp_img = np.zeros((img.shape[0], img.shape[1], 3)) 28 | tmp_img[:, :, 0] = (img[:, :, 0] - self.mean[0]) / self.std[0] 29 | tmp_img[:, :, 1] = (img[:, :, 1] - self.mean[1]) / self.std[1] 30 | tmp_img[:, :, 2] = (img[:, :, 2] - self.mean[2]) / self.std[2] 31 | 32 | tmp_img = tmp_img.transpose((2, 0, 1)) 33 | 34 | model_input_name = self.inner_session.get_inputs()[0].name 35 | 36 | return {model_input_name: np.expand_dims(tmp_img, 0).astype(np.float32)} 37 | 38 | @abstractmethod 39 | def predict(self, img: np.ndarray) -> list[np.ndarray]: 40 | pass 41 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/rembg/session_factory.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import onnxruntime as ort 4 | 5 | from ..onnx.session import get_input_shape 6 | from .session_base import BaseSession 7 | from .session_cloth import ClothSession 8 | from .session_simple import SimpleSession 9 | 10 | 11 | def new_session(session: ort.InferenceSession) -> BaseSession: 12 | session_class: type[BaseSession] 13 | 14 | input_width = get_input_shape(session)[2] 15 | 16 | # Using size to determine session type and norm parameters is fragile, 17 | # but at the moment I don't know a better way to detect architecture due 18 | # to the lack of consistency in naming and outputs across arches and repos. 19 | # It works right now because of the limited number of models supported, 20 | # but if that expands, it may become necessary to find an alternative. 21 | mean = (0.485, 0.456, 0.406) 22 | std = (0.229, 0.224, 0.225) 23 | size = (input_width, input_width) if input_width is not None else (320, 320) 24 | if input_width == 768: # U2NET cloth model 25 | session_class = ClothSession 26 | mean = (0.5, 0.5, 0.5) 27 | std = (0.5, 0.5, 0.5) 28 | else: 29 | session_class = SimpleSession 30 | if input_width == 1024: # ISNET 31 | mean = (0.5, 0.5, 0.5) 32 | std = (1, 1, 1) 33 | elif input_width == 512: # Models trained using anime-segmentation repo 34 | mean = (0, 0, 0) 35 | std = (1, 1, 1) 36 | 37 | return session_class(session, mean, std, size) 38 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/rembg/session_simple.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.impl.image_utils import normalize 6 | from nodes.impl.resize import ResizeFilter, resize 7 | from nodes.utils.utils import get_h_w_c 8 | 9 | from .session_base import BaseSession 10 | 11 | 12 | class SimpleSession(BaseSession): 13 | def predict(self, img: np.ndarray) -> list[np.ndarray]: 14 | h, w, _ = get_h_w_c(img) 15 | ort_outs = self.inner_session.run(None, self.normalize(img)) 16 | 17 | pred = ort_outs[0][:, 0, :, :] 18 | 19 | ma = np.max(pred) 20 | mi = np.min(pred) 21 | 22 | pred = (pred - mi) / (ma - mi) 23 | mask = normalize(np.squeeze(pred)) 24 | mask = np.squeeze(resize(mask, (w, h), ResizeFilter.LANCZOS)) 25 | 26 | return [mask] 27 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/rust_regex.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Protocol 4 | 5 | from chainner_ext import MatchGroup, RegexMatch, RustRegex 6 | 7 | 8 | class Range(Protocol): 9 | @property 10 | def start(self) -> int: ... 11 | @property 12 | def end(self) -> int: ... 13 | 14 | 15 | def get_range_text(text: str, range: Range) -> str: 16 | return text[range.start : range.end] 17 | 18 | 19 | def match_to_replacements_dict( 20 | regex: RustRegex, match: RegexMatch, text: str 21 | ) -> dict[str, str]: 22 | def get_group_text(group: MatchGroup | None) -> str: 23 | if group is None: 24 | return "" 25 | return get_range_text(text, group) 26 | 27 | replacements: dict[str, str] = {} 28 | for i in range(regex.groups + 1): 29 | replacements[str(i)] = get_group_text(match.get(i)) 30 | for name, i in regex.groupindex.items(): 31 | replacements[name] = get_group_text(match.get(i)) 32 | 33 | return replacements 34 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/tile.py: -------------------------------------------------------------------------------- 1 | import math 2 | from enum import Enum 3 | 4 | import cv2 5 | import numpy as np 6 | 7 | from ..utils.utils import get_h_w_c 8 | 9 | 10 | class TileMode(Enum): 11 | TILE = 0 12 | MIRROR = 1 13 | 14 | 15 | def tile_image(img: np.ndarray, width: int, height: int, mode: TileMode) -> np.ndarray: 16 | if mode == TileMode.TILE: 17 | # do nothing 18 | pass 19 | elif mode == TileMode.MIRROR: 20 | # flip the image to create a mirrored tile 21 | flip_x: np.ndarray = cv2.flip(img, 0) 22 | flip_y: np.ndarray = cv2.flip(img, 1) 23 | flip_xy: np.ndarray = cv2.flip(img, -1) 24 | 25 | img = cv2.vconcat( 26 | [ 27 | cv2.hconcat([img, flip_y]), # type: ignore 28 | cv2.hconcat([flip_x, flip_xy]), # type: ignore 29 | ] 30 | ) 31 | else: 32 | raise AssertionError(f"Invalid tile mode {mode}") 33 | 34 | h, w, _ = get_h_w_c(img) 35 | tile_w = math.ceil(width / w) 36 | tile_h = math.ceil(height / h) 37 | img = np.tile(img, (tile_h, tile_w) if img.ndim == 2 else (tile_h, tile_w, 1)) 38 | 39 | # crop to make sure the dimensions are correct 40 | return img[:height, :width] 41 | -------------------------------------------------------------------------------- /backend/src/nodes/impl/upscale/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/impl/upscale/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/impl/upscale/passthrough.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from ...utils.utils import get_h_w_c 4 | from ..image_op import ImageOp 5 | 6 | 7 | def passthrough_single_color(img: np.ndarray, scale: int, op: ImageOp) -> np.ndarray: 8 | """ 9 | If the given image is a single-color image, it will be scaled and returned as is instead of being processed by the given operation. 10 | Obviously, this optimization is only correct if `op` doesn't change the color of single-color images. 11 | 12 | To make this a transparent optimization, it is important that `scale` is correct. 13 | `scale` must be the same factor by which `op` changes the dimension of the image. 14 | """ 15 | 16 | h, w, c = get_h_w_c(img) 17 | 18 | if c == 1: 19 | unique_list = np.unique(img) 20 | if len(unique_list) == 1: 21 | return np.full((h * scale, w * scale), unique_list[0], np.float32) 22 | else: 23 | unique_values = [] 24 | is_unique = True 25 | for channel in range(c): 26 | unique_list = np.unique(img[:, :, channel]) 27 | if len(unique_list) == 1: 28 | unique_values.append(unique_list[0]) 29 | else: 30 | is_unique = False 31 | break 32 | 33 | if is_unique: 34 | channels = [ 35 | np.full((h * scale, w * scale), unique_values[channel], np.float32) 36 | for channel in range(c) 37 | ] 38 | return np.dstack(channels) 39 | 40 | return op(img) 41 | -------------------------------------------------------------------------------- /backend/src/nodes/properties/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/properties/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/properties/inputs/__init__.py: -------------------------------------------------------------------------------- 1 | from .file_inputs import * 2 | from .generic_inputs import * 3 | from .image_dropdown_inputs import * 4 | from .numeric_inputs import * 5 | from .numpy_inputs import * 6 | 7 | try: 8 | from .ncnn_inputs import * 9 | except Exception: 10 | pass 11 | try: 12 | from .onnx_inputs import * 13 | except Exception: 14 | pass 15 | try: 16 | from .pytorch_inputs import * 17 | except Exception: 18 | pass 19 | -------------------------------------------------------------------------------- /backend/src/nodes/properties/inputs/__system_inputs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | from typing import Literal 5 | 6 | from api import BaseInput 7 | from navi import ExpressionJson 8 | 9 | 10 | class StaticValueInput(BaseInput): 11 | def __init__( 12 | self, 13 | label: str, 14 | py_type: type = str, 15 | navi_type: ExpressionJson = "string", 16 | value: Literal["execution_number"] = "execution_number", 17 | ): 18 | super().__init__(navi_type, label, kind="static", has_handle=False) 19 | 20 | self.associated_type = py_type 21 | self.value = value 22 | 23 | def to_dict(self): 24 | return { 25 | **super().to_dict(), 26 | "value": self.value, 27 | } 28 | 29 | def enforce(self, value: object): 30 | return_value = value 31 | if not isinstance(value, self.associated_type): 32 | return_value = self.associated_type(value) 33 | 34 | if isinstance(value, (float, int)) and math.isnan(value): 35 | raise ValueError("NaN is not a valid number") 36 | 37 | return return_value 38 | -------------------------------------------------------------------------------- /backend/src/nodes/properties/inputs/label.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | LabelStyle = Literal["default", "hidden", "inline"] 4 | 5 | 6 | def get_default_label_style(label: str) -> LabelStyle: 7 | return "inline" if len(label) <= 8 else "default" 8 | -------------------------------------------------------------------------------- /backend/src/nodes/properties/inputs/ncnn_inputs.py: -------------------------------------------------------------------------------- 1 | from api import BaseInput 2 | 3 | from ...impl.ncnn.model import NcnnModelWrapper 4 | 5 | 6 | class NcnnModelInput(BaseInput): 7 | """Input for NcnnModel""" 8 | 9 | def __init__(self, label: str = "Model"): 10 | super().__init__("NcnnNetwork", label) 11 | self.associated_type = NcnnModelWrapper 12 | -------------------------------------------------------------------------------- /backend/src/nodes/properties/outputs/__init__.py: -------------------------------------------------------------------------------- 1 | from .file_outputs import * 2 | from .generic_outputs import * 3 | from .numpy_outputs import * 4 | 5 | try: 6 | from .ncnn_outputs import * 7 | except Exception: 8 | pass 9 | try: 10 | from .onnx_outputs import * 11 | except Exception: 12 | pass 13 | try: 14 | from .pytorch_outputs import * 15 | except Exception: 16 | pass 17 | -------------------------------------------------------------------------------- /backend/src/nodes/properties/outputs/file_outputs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | import navi 6 | from api import BaseOutput 7 | 8 | 9 | class DirectoryOutput(BaseOutput[Path]): 10 | """Output for saving to a directory""" 11 | 12 | def __init__( 13 | self, 14 | label: str = "Directory", 15 | of_input: int | None = None, 16 | output_type: str = "Directory", 17 | ): 18 | directory_type = ( 19 | "Directory" 20 | if of_input is None 21 | else f"splitFilePath(Input{of_input}.path).dir" 22 | ) 23 | directory_type = navi.intersect_with_error(directory_type, output_type) 24 | super().__init__(directory_type, label, associated_type=Path) 25 | 26 | def get_broadcast_type(self, value: Path): 27 | return navi.named("Directory", {"path": navi.literal(str(value))}) 28 | 29 | def enforce(self, value: object) -> Path: 30 | assert isinstance(value, Path) 31 | return value 32 | -------------------------------------------------------------------------------- /backend/src/nodes/properties/outputs/ncnn_outputs.py: -------------------------------------------------------------------------------- 1 | import navi 2 | from api import BaseOutput, OutputKind 3 | 4 | from ...impl.ncnn.model import NcnnModelWrapper 5 | from ...utils.format import format_channel_numbers 6 | 7 | 8 | class NcnnModelOutput(BaseOutput): 9 | def __init__( 10 | self, 11 | model_type: navi.ExpressionJson = "NcnnNetwork", 12 | label: str = "Model", 13 | kind: OutputKind = "generic", 14 | ): 15 | super().__init__(model_type, label, kind=kind, associated_type=NcnnModelWrapper) 16 | 17 | def get_broadcast_data(self, value: NcnnModelWrapper): 18 | return { 19 | "tags": [ 20 | format_channel_numbers(value.in_nc, value.out_nc), 21 | f"{value.nf}nf", 22 | value.fp, 23 | ] 24 | } 25 | 26 | def get_broadcast_type(self, value: NcnnModelWrapper): 27 | return navi.named( 28 | "NcnnNetwork", 29 | { 30 | "scale": value.scale, 31 | "inputChannels": value.in_nc, 32 | "outputChannels": value.out_nc, 33 | "nf": value.nf, 34 | "fp": navi.literal(value.fp), 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /backend/src/nodes/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/nodes/utils/__init__.py -------------------------------------------------------------------------------- /backend/src/nodes/utils/checked_cast.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TypeVar 4 | 5 | T = TypeVar("T") 6 | 7 | 8 | def checked_cast(t: type[T], value: object) -> T: 9 | assert isinstance(value, t), f"Value is {type(value)}, must be type {t}" 10 | return value 11 | -------------------------------------------------------------------------------- /backend/src/nodes/utils/seed.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from random import Random 3 | 4 | _U32_MAX = 4294967296 5 | 6 | 7 | @dataclass(frozen=True) 8 | class Seed: 9 | value: int 10 | """ 11 | The value of the seed. This value may be signed and generally have any range. 12 | """ 13 | 14 | @staticmethod 15 | def from_bytes(b: bytes): 16 | return Seed(Random(b).randint(0, _U32_MAX - 1)) 17 | 18 | def to_range(self, a: int, b: int) -> int: 19 | """ 20 | Returns the value of the seed within the given range [a,b] both ends inclusive. 21 | 22 | If the current seed is not within the given range, a value within the range will be derived from the current seed. 23 | """ 24 | if a <= self.value <= b: 25 | return self.value 26 | return Random(self.value).randint(a, b) 27 | 28 | def to_u32(self) -> int: 29 | """ 30 | Returns the value of the seed as a 32bit unsigned integer. 31 | """ 32 | return self.to_range(0, _U32_MAX - 1) 33 | 34 | def cache_key_func(self): 35 | return self.value 36 | -------------------------------------------------------------------------------- /backend/src/nodes/utils/unpickler.py: -------------------------------------------------------------------------------- 1 | # Safe unpickler to prevent arbitrary code execution 2 | import pickle 3 | from types import SimpleNamespace 4 | 5 | safe_list = { 6 | ("collections", "OrderedDict"), 7 | ("typing", "OrderedDict"), 8 | ("torch._utils", "_rebuild_tensor_v2"), 9 | ("torch", "BFloat16Storage"), 10 | ("torch", "FloatStorage"), 11 | ("torch", "HalfStorage"), 12 | ("torch", "IntStorage"), 13 | ("torch", "LongStorage"), 14 | ("torch", "DoubleStorage"), 15 | } 16 | 17 | 18 | class RestrictedUnpickler(pickle.Unpickler): 19 | def find_class(self, module: str, name: str): 20 | # Only allow required classes to load state dict 21 | if (module, name) not in safe_list: 22 | raise pickle.UnpicklingError(f"Global '{module}.{name}' is forbidden") 23 | return super().find_class(module, name) 24 | 25 | 26 | RestrictedUnpickle = SimpleNamespace( 27 | Unpickler=RestrictedUnpickler, 28 | __name__="pickle", 29 | load=lambda *args, **kwargs: RestrictedUnpickler(*args, **kwargs).load(), 30 | ) 31 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_external/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from sanic.log import logger 4 | 5 | from api import add_package 6 | 7 | package = add_package( 8 | __file__, 9 | id="chaiNNer_external", 10 | name="External", 11 | description="Interact with an external Stable Diffusion API", 12 | ) 13 | 14 | external_stable_diffusion_category = package.add_category( 15 | name="Stable Diffusion (External)", 16 | description="Interact with an external Stable Diffusion API", 17 | icon="FaPaintBrush", 18 | color="#9331CC", 19 | ) 20 | 21 | _FEATURE_DESCRIPTION = f""" 22 | ChaiNNer can connect to [AUTOMATIC1111's Stable Diffusion Web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) to run Stable Diffusion nodes. 23 | 24 | If you want to use the External Stable Diffusion nodes, run the Automatic1111 web UI with the `--api` flag, like so: 25 | 26 | ```bash 27 | ./webui.{"bat" if sys.platform == "win32" else "sh"} --api 28 | ``` 29 | 30 | To manually set where chaiNNer looks for the API, use the `STABLE_DIFFUSION_PROTOCOL`, `STABLE_DIFFUSION_HOST`, and `STABLE_DIFFUSION_PORT` environment variables. By default, `127.0.0.1` will be the host. If not specified, chaiNNer will try to auto-detect the protocol and port. 31 | """ 32 | 33 | 34 | web_ui_feature_descriptor = package.add_feature( 35 | id="webui", 36 | name="AUTOMATIC1111/stable-diffusion-webui", 37 | description=_FEATURE_DESCRIPTION, 38 | ) 39 | 40 | logger.debug(f"Loaded package {package.name}") 41 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_external/external_stable_diffusion/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import external_stable_diffusion_category 2 | 3 | auto1111_group = external_stable_diffusion_category.add_node_group("Automatic1111") 4 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_external/external_stable_diffusion/automatic1111/clip_interrogate.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.node_cache import cached 6 | from nodes.properties.inputs import ImageInput 7 | from nodes.properties.outputs import TextOutput 8 | 9 | from ...features import web_ui 10 | from ...util import encode_base64_image 11 | from ...web_ui import STABLE_DIFFUSION_INTERROGATE_PATH, get_api 12 | from .. import auto1111_group 13 | 14 | 15 | @auto1111_group.register( 16 | schema_id="chainner:external_stable_diffusion:interrograte", 17 | name="CLIP Interrogate", 18 | description="Use Automatic1111 to get a description of an image", 19 | icon="MdTextFields", 20 | inputs=[ 21 | ImageInput(), 22 | ], 23 | outputs=[ 24 | TextOutput("Text"), 25 | ], 26 | decorators=[cached], 27 | features=web_ui, 28 | ) 29 | def clip_interrogate_node(image: np.ndarray) -> str: 30 | request_data = { 31 | "image": encode_base64_image(image), 32 | } 33 | response = get_api().post( 34 | path=STABLE_DIFFUSION_INTERROGATE_PATH, json_data=request_data 35 | ) 36 | return response["caption"] 37 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_external/features.py: -------------------------------------------------------------------------------- 1 | from api import FeatureState 2 | from nodes.utils.format import join_english 3 | 4 | from . import web_ui_feature_descriptor 5 | from .web_ui import ApiConfig, get_verified_api 6 | 7 | 8 | async def check_connection() -> FeatureState: 9 | config = None 10 | try: 11 | config = ApiConfig.from_env() 12 | api = await get_verified_api() 13 | if api is not None: 14 | return FeatureState.enabled(f"Connected to {api.base_url}") 15 | else: 16 | url = config.host 17 | if len(config.protocol) == 1: 18 | url = f"{config.protocol[0]}://{url}" 19 | if len(config.port) == 1: 20 | url += f":{config.port[0]}" 21 | else: 22 | ports = join_english(config.port) 23 | url += f" on ports {ports}" 24 | 25 | return FeatureState.disabled( 26 | f"No stable diffusion API found. Could not connect to {url}." 27 | ) 28 | except Exception as e: 29 | return FeatureState.disabled(f"Could not connect to stable diffusion API: {e}") 30 | 31 | 32 | web_ui = web_ui_feature_descriptor.add_behavior(check=check_connection) 33 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_external/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import base64 4 | import io 5 | 6 | import cv2 7 | import numpy as np 8 | from PIL import Image 9 | 10 | from nodes.impl.image_utils import normalize, to_uint8 11 | from nodes.utils.utils import get_h_w_c 12 | 13 | 14 | def nearest_valid_size(width: int, height: int): 15 | return (width // 8) * 8, (height // 8) * 8 16 | 17 | 18 | def decode_base64_image(image_bytes: bytes | str) -> np.ndarray: 19 | image = Image.open(io.BytesIO(base64.b64decode(image_bytes))) 20 | image_nparray = np.array(image) 21 | _, _, c = get_h_w_c(image_nparray) 22 | if c == 3: 23 | image_nparray = cv2.cvtColor(image_nparray, cv2.COLOR_RGB2BGR) 24 | elif c == 4: 25 | image_nparray = cv2.cvtColor(image_nparray, cv2.COLOR_RGBA2BGRA) 26 | return normalize(image_nparray) 27 | 28 | 29 | def encode_base64_image(image_nparray: np.ndarray) -> str: 30 | image_nparray = to_uint8(image_nparray) 31 | _, _, c = get_h_w_c(image_nparray) 32 | if c == 1: 33 | # PIL supports grayscale images just fine, so we don't need to do any conversion 34 | pass 35 | elif c == 3: 36 | image_nparray = cv2.cvtColor(image_nparray, cv2.COLOR_BGR2RGB) 37 | elif c == 4: 38 | image_nparray = cv2.cvtColor(image_nparray, cv2.COLOR_BGRA2RGBA) 39 | else: 40 | raise RuntimeError 41 | with io.BytesIO() as buffer: 42 | with Image.fromarray(image_nparray) as image: 43 | image.save(buffer, format="PNG") 44 | return base64.b64encode(buffer.getvalue()).decode("utf-8") 45 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_ncnn/ncnn/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import ncnn_category 2 | 3 | io_group = ncnn_category.add_node_group("Input & Output") 4 | processing_group = ncnn_category.add_node_group("Processing") 5 | utility_group = ncnn_category.add_node_group("Utility") 6 | batch_processing_group = ncnn_category.add_node_group("Batch Processing") 7 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_ncnn/ncnn/io/save_model.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from sanic.log import logger 4 | 5 | from nodes.impl.ncnn.model import NcnnModelWrapper 6 | from nodes.properties.inputs import DirectoryInput, NcnnModelInput, RelativePathInput 7 | 8 | from .. import io_group 9 | 10 | 11 | @io_group.register( 12 | schema_id="chainner:ncnn:save_model", 13 | name="Save Model", 14 | description="Save an NCNN model to specified directory. It can also be saved in fp16 mode for smaller file size and faster processing.", 15 | icon="MdSave", 16 | inputs=[ 17 | NcnnModelInput(), 18 | DirectoryInput(must_exist=False), 19 | RelativePathInput("Param/Bin Name"), 20 | ], 21 | outputs=[], 22 | side_effects=True, 23 | ) 24 | def save_model_node(model: NcnnModelWrapper, directory: Path, name: str) -> None: 25 | full_bin_path = (directory / f"{name}.bin").resolve() 26 | full_param_path = (directory / f"{name}.param").resolve() 27 | 28 | logger.debug(f"Writing NCNN model to paths: {full_bin_path} {full_param_path}") 29 | 30 | full_bin_path.parent.mkdir(parents=True, exist_ok=True) 31 | model.model.write_bin(full_bin_path) 32 | model.model.write_param(full_param_path) 33 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_ncnn/ncnn/utility/get_model_scale.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from nodes.impl.ncnn.model import NcnnModelWrapper 4 | from nodes.properties.inputs import NcnnModelInput 5 | from nodes.properties.outputs import NumberOutput 6 | 7 | from .. import utility_group 8 | 9 | 10 | @utility_group.register( 11 | schema_id="chainner:ncnn:model_dim", 12 | name="Get Model Scale", 13 | description="""Returns the scale of an NCNN model.""", 14 | icon="BsRulers", 15 | inputs=[NcnnModelInput()], 16 | outputs=[NumberOutput("Scale", output_type="Input0.scale")], 17 | ) 18 | def get_model_scale_node(model: NcnnModelWrapper) -> int: 19 | return model.scale 20 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_onnx/onnx/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import onnx_category 2 | 3 | io_group = onnx_category.add_node_group("Input & Output") 4 | processing_group = onnx_category.add_node_group("Processing") 5 | utility_group = onnx_category.add_node_group("Utility") 6 | batch_processing_group = onnx_category.add_node_group("Batch Processing") 7 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_onnx/onnx/io/save_model.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from sanic.log import logger 6 | 7 | from nodes.impl.onnx.model import OnnxModel 8 | from nodes.properties.inputs import DirectoryInput, OnnxModelInput, RelativePathInput 9 | 10 | from .. import io_group 11 | 12 | 13 | @io_group.register( 14 | schema_id="chainner:onnx:save_model", 15 | name="Save Model", 16 | description="""Save ONNX model to file (.onnx).""", 17 | icon="MdSave", 18 | inputs=[ 19 | OnnxModelInput(), 20 | DirectoryInput(must_exist=False), 21 | RelativePathInput("Model Name"), 22 | ], 23 | outputs=[], 24 | side_effects=True, 25 | ) 26 | def save_model_node(model: OnnxModel, directory: Path, model_name: str) -> None: 27 | full_path = (directory / f"{model_name}.onnx").resolve() 28 | logger.debug(f"Writing file to path: {full_path}") 29 | full_path.parent.mkdir(parents=True, exist_ok=True) 30 | with open(full_path, "wb") as f: 31 | f.write(model.bytes) 32 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_onnx/onnx/utility/get_model_info.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from nodes.impl.onnx.model import OnnxModel 4 | from nodes.properties.inputs import OnnxModelInput 5 | from nodes.properties.outputs import NumberOutput, TextOutput 6 | 7 | from .. import utility_group 8 | 9 | 10 | @utility_group.register( 11 | schema_id="chainner:onnx:model_info", 12 | name="Get Model Info", 13 | description="""Returns the scale and purpose of a ONNX model.""", 14 | icon="ImInfo", 15 | inputs=[OnnxModelInput("ONNX Model")], 16 | outputs=[ 17 | NumberOutput( 18 | "Scale", 19 | output_type=""" 20 | if Input0.scaleWidth == Input0.scaleHeight { 21 | Input0.scaleHeight 22 | } else { 23 | 0 24 | } 25 | """, 26 | ), 27 | TextOutput("Purpose", output_type="Input0.subType"), 28 | ], 29 | ) 30 | def get_model_info_node(model: OnnxModel) -> tuple[int, str]: 31 | scale_width = model.info.scale_width 32 | scale_height = model.info.scale_height 33 | return ( 34 | scale_width if (scale_width is not None and scale_width == scale_height) else 0, 35 | model.sub_type, 36 | ) 37 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_onnx/onnx/utility/optimize_model.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import onnx 4 | 5 | from nodes.impl.onnx.load import load_onnx_model 6 | from nodes.impl.onnx.model import OnnxModel 7 | from nodes.impl.onnx.utils import safely_optimize_onnx_model 8 | from nodes.properties.inputs import OnnxModelInput 9 | from nodes.properties.outputs import OnnxModelOutput 10 | 11 | from .. import utility_group 12 | 13 | 14 | @utility_group.register( 15 | schema_id="chainner:onnx:optimize_model", 16 | name="Optimize Model", 17 | description="Optimize the give model. Optimizations may or may not improve performance.", 18 | icon="MdSpeed", 19 | inputs=[ 20 | OnnxModelInput(), 21 | ], 22 | outputs=[ 23 | OnnxModelOutput(), 24 | ], 25 | ) 26 | def optimize_model_node(model: OnnxModel) -> OnnxModel: 27 | model_proto = onnx.load_from_string(model.bytes) 28 | model_proto = safely_optimize_onnx_model(model_proto) 29 | return load_onnx_model(model_proto) 30 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_pytorch/pytorch/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import pytorch_category 2 | 3 | io_group = pytorch_category.add_node_group("Input & Output") 4 | processing_group = pytorch_category.add_node_group("Processing") 5 | restoration_group = pytorch_category.add_node_group("Restoration") 6 | batch_processing_group = pytorch_category.add_node_group("Batch Processing") 7 | utility_group = pytorch_category.add_node_group("Utility") 8 | 9 | processing_group.order = [ 10 | "chainner:pytorch:upscale_image", 11 | "chainner:pytorch:inpaint", 12 | ] 13 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_pytorch/pytorch/utility/get_model_info.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from spandrel import ModelDescriptor 4 | 5 | from nodes.properties.inputs import ModelInput 6 | from nodes.properties.outputs import NumberOutput, TextOutput 7 | 8 | from .. import utility_group 9 | 10 | 11 | @utility_group.register( 12 | schema_id="chainner:pytorch:model_dim", 13 | name="Get Model Info", 14 | description="""Returns the purpose, architecture and scale of a PyTorch model.""", 15 | icon="ImInfo", 16 | inputs=[ModelInput()], 17 | outputs=[ 18 | NumberOutput("Scale", output_type="Input0.scale"), 19 | TextOutput("Architecture", output_type="Input0.arch"), 20 | TextOutput("Purpose", output_type="Input0.subType"), 21 | ], 22 | ) 23 | def get_model_info_node(model: ModelDescriptor) -> tuple[int, str, str]: 24 | return (model.scale, model.architecture.name, model.purpose) 25 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import image_category 2 | 3 | io_group = image_category.add_node_group("Input & Output") 4 | batch_processing_group = image_category.add_node_group("Batch Processing") 5 | video_frames_group = image_category.add_node_group("Video Frames") 6 | create_images_group = image_category.add_node_group("Create Images") 7 | 8 | batch_processing_group.order = [ 9 | "chainner:image:load_images", 10 | "chainner:image:load_image_pairs", 11 | "chainner:image:split_spritesheet", 12 | "chainner:image:merge_spritesheet", 13 | ] 14 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image/create_images/create_color.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | import navi 6 | from nodes.impl.color.color import Color 7 | from nodes.properties.inputs import ColorInput, NumberInput 8 | from nodes.properties.outputs import ImageOutput 9 | 10 | from .. import create_images_group 11 | 12 | 13 | @create_images_group.register( 14 | schema_id="chainner:image:create_color", 15 | name="Create Color", 16 | description="Create an image of specified dimensions filled with the given color.", 17 | icon="MdFormatColorFill", 18 | inputs=[ 19 | ColorInput(), 20 | NumberInput("Width", min=1, unit="px", default=1), 21 | NumberInput("Height", min=1, unit="px", default=1), 22 | ], 23 | outputs=[ 24 | ImageOutput( 25 | image_type=navi.Image( 26 | width="Input1", 27 | height="Input2", 28 | channels="Input0.channels", 29 | ), 30 | assume_normalized=True, 31 | ) 32 | ], 33 | ) 34 | def create_color_node(color: Color, width: int, height: int) -> np.ndarray: 35 | return color.to_image(width, height) 36 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image/io/view_image.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.properties.inputs import ImageInput 6 | from nodes.properties.outputs import LargeImageOutput 7 | 8 | from .. import io_group 9 | 10 | 11 | @io_group.register( 12 | schema_id="chainner:image:view", 13 | name="View Image", 14 | description="See an inline preview of the image in the editor.", 15 | icon="BsEyeFill", 16 | inputs=[ImageInput()], 17 | outputs=[ 18 | LargeImageOutput( 19 | "Preview", 20 | image_type="Input0", 21 | has_handle=False, 22 | assume_normalized=True, 23 | ), 24 | ], 25 | side_effects=True, 26 | ) 27 | def view_image_node(img: np.ndarray): 28 | return img 29 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_adjustment/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import image_adjustments_category 2 | 3 | adjustments_group = image_adjustments_category.add_node_group("Adjustments") 4 | threshold_group = image_adjustments_category.add_node_group("Threshold") 5 | gamma_group = image_adjustments_category.add_node_group("Gamma") 6 | arithmetic_group = image_adjustments_category.add_node_group("Arithmetic") 7 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_adjustment/adjustments/clamp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.properties.inputs import ImageInput, SliderInput 6 | from nodes.properties.outputs import ImageOutput 7 | 8 | from .. import adjustments_group 9 | 10 | 11 | @adjustments_group.register( 12 | schema_id="chainner:image:clamp", 13 | name="Clamp", 14 | description="Clamps the values of an image.", 15 | icon="ImContrast", 16 | inputs=[ 17 | ImageInput(), 18 | SliderInput( 19 | "Minimum", 20 | min=0.0, 21 | max=1.0, 22 | default=0.0, 23 | precision=4, 24 | step=0.001, 25 | scale="log", 26 | ), 27 | SliderInput( 28 | "Maximum", 29 | min=0.0, 30 | max=1.0, 31 | default=1.0, 32 | precision=4, 33 | step=0.001, 34 | scale="log", 35 | ), 36 | ], 37 | outputs=[ 38 | ImageOutput(shape_as=0, assume_normalized=True), 39 | ], 40 | ) 41 | def clamp_node(img: np.ndarray, minimum: float, maximum: float) -> np.ndarray: 42 | if minimum <= 0 and maximum >= 1: 43 | return img 44 | return np.clip(img, minimum, maximum) 45 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_adjustment/adjustments/invert_color.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.properties.inputs import ImageInput 6 | from nodes.properties.outputs import ImageOutput 7 | from nodes.utils.utils import get_h_w_c 8 | 9 | from .. import adjustments_group 10 | 11 | 12 | @adjustments_group.register( 13 | schema_id="chainner:image:invert", 14 | name="Invert Color", 15 | description="Inverts all colors in an image.", 16 | icon="MdInvertColors", 17 | inputs=[ImageInput()], 18 | outputs=[ImageOutput(shape_as=0, assume_normalized=True)], 19 | ) 20 | def invert_color_node(img: np.ndarray) -> np.ndarray: 21 | c = get_h_w_c(img)[2] 22 | 23 | # invert the first 3 channels 24 | if c <= 3: 25 | return 1 - img 26 | 27 | img = img.copy() 28 | img[:, :, :3] = 1 - img[:, :, :3] 29 | return img 30 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_adjustment/adjustments/opacity.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from api import KeyInfo 6 | from nodes.impl.pil_utils import convert_to_bgra 7 | from nodes.properties.inputs import ImageInput, SliderInput 8 | from nodes.properties.outputs import ImageOutput 9 | from nodes.utils.utils import get_h_w_c 10 | 11 | from .. import adjustments_group 12 | 13 | 14 | @adjustments_group.register( 15 | schema_id="chainner:image:opacity", 16 | name="Opacity", 17 | description="Adjusts the opacity of an image. The higher the opacity value, the more opaque the image is.", 18 | icon="MdOutlineOpacity", 19 | inputs=[ 20 | ImageInput(), 21 | SliderInput("Opacity", max=100, default=100, precision=1, step=1, unit="%"), 22 | ], 23 | outputs=[ 24 | ImageOutput(size_as=0, channels=4, assume_normalized=True), 25 | ], 26 | key_info=KeyInfo.number(1), 27 | ) 28 | def opacity_node(img: np.ndarray, opacity: float) -> np.ndarray: 29 | # Convert inputs 30 | c = get_h_w_c(img)[2] 31 | if opacity == 100 and c == 4: 32 | return img 33 | imgout = convert_to_bgra(img, c) 34 | opacity /= 100 35 | 36 | imgout[:, :, 3] *= opacity 37 | 38 | return imgout 39 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_adjustment/arithmetic/add.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.properties.inputs import ImageInput, SliderInput 6 | from nodes.properties.outputs import ImageOutput 7 | 8 | from .. import arithmetic_group 9 | 10 | 11 | @arithmetic_group.register( 12 | schema_id="chainner:image:add", 13 | description="Add values to an image.", 14 | name="Add", 15 | icon="ImBrightnessContrast", 16 | inputs=[ 17 | ImageInput(), 18 | SliderInput( 19 | "Add", 20 | min=-100, 21 | max=100, 22 | default=0, 23 | precision=1, 24 | step=1, 25 | ), 26 | ], 27 | outputs=[ImageOutput(shape_as=0)], 28 | ) 29 | def add_node(img: np.ndarray, add: float) -> np.ndarray: 30 | if add == 0: 31 | return img 32 | 33 | img = img + (add / 100) 34 | 35 | return img 36 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_adjustment/arithmetic/divide.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.properties.inputs import ImageInput, SliderInput 6 | from nodes.properties.outputs import ImageOutput 7 | 8 | from .. import arithmetic_group 9 | 10 | 11 | @arithmetic_group.register( 12 | schema_id="chainner:image:divide", 13 | description="Divide all channels in an image by a value.", 14 | name="Divide", 15 | icon="ImBrightnessContrast", 16 | inputs=[ 17 | ImageInput(), 18 | SliderInput( 19 | "Divide", 20 | min=0.0001, 21 | max=4.0, 22 | default=1.0, 23 | precision=4, 24 | step=0.0001, 25 | scale="log", 26 | ), 27 | ], 28 | outputs=[ImageOutput(shape_as=0)], 29 | ) 30 | def divide_node(img: np.ndarray, divide: float) -> np.ndarray: 31 | if divide == 1.0: 32 | return img 33 | 34 | img = img * (1.0 / divide) 35 | 36 | return img 37 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_adjustment/arithmetic/multiply.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.properties.inputs import ImageInput, SliderInput 6 | from nodes.properties.outputs import ImageOutput 7 | 8 | from .. import arithmetic_group 9 | 10 | 11 | @arithmetic_group.register( 12 | schema_id="chainner:image:multiply", 13 | description="Multiply all channels in an image by a value.", 14 | name="Multiply", 15 | icon="ImBrightnessContrast", 16 | inputs=[ 17 | ImageInput(), 18 | SliderInput( 19 | "Multiply", 20 | min=0.0, 21 | max=4.0, 22 | default=1.0, 23 | precision=4, 24 | step=0.0001, 25 | scale="log", 26 | ), 27 | ], 28 | outputs=[ImageOutput(shape_as=0)], 29 | ) 30 | def multiply_node(img: np.ndarray, mult: float) -> np.ndarray: 31 | if mult == 1.0: 32 | return img 33 | 34 | img = img * mult 35 | 36 | return img 37 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_adjustment/gamma/gamma.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | from chainner_ext import fast_gamma 5 | 6 | from nodes.properties.inputs import BoolInput, ImageInput, SliderInput 7 | from nodes.properties.outputs import ImageOutput 8 | 9 | from .. import gamma_group 10 | 11 | 12 | @gamma_group.register( 13 | schema_id="chainner:image:gamma", 14 | name="Gamma", 15 | description="Adjusts the gamma of an image.", 16 | icon="ImBrightnessContrast", 17 | inputs=[ 18 | ImageInput(), 19 | SliderInput( 20 | "Gamma", 21 | min=0.01, 22 | max=100, 23 | default=1, 24 | precision=4, 25 | step=0.1, 26 | scale="log", 27 | ), 28 | BoolInput("Invert Gamma", default=False), 29 | ], 30 | outputs=[ImageOutput(shape_as=0, assume_normalized=True)], 31 | ) 32 | def gamma_node(img: np.ndarray, gamma: float, invert_gamma: bool) -> np.ndarray: 33 | if gamma == 1: 34 | # noop 35 | return img 36 | 37 | if invert_gamma: 38 | gamma = 1 / gamma 39 | 40 | return fast_gamma(img, gamma) 41 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_channel/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import image_channel_category 2 | 3 | all_group = image_channel_category.add_node_group("All") 4 | transparency_group = image_channel_category.add_node_group("Transparency") 5 | miscellaneous_group = image_channel_category.add_node_group("Miscellaneous") 6 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_channel/transparency/split_transparency.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.impl.image_utils import as_target_channels 6 | from nodes.properties.inputs import ImageInput 7 | from nodes.properties.outputs import ImageOutput 8 | from nodes.utils.utils import get_h_w_c 9 | 10 | from .. import transparency_group 11 | 12 | 13 | @transparency_group.register( 14 | schema_id="chainner:image:split_transparency", 15 | name="Split Transparency", 16 | description="Split image channels into RGB and Alpha (transparency) channels.", 17 | icon="MdCallSplit", 18 | inputs=[ImageInput(channels=[1, 3, 4])], 19 | outputs=[ 20 | ImageOutput("RGB", size_as=0, channels=3, assume_normalized=True), 21 | ImageOutput("Alpha", size_as=0, channels=1, assume_normalized=True), 22 | ], 23 | ) 24 | def split_transparency_node(img: np.ndarray) -> tuple[np.ndarray, np.ndarray]: 25 | c = get_h_w_c(img)[2] 26 | if c == 3: 27 | # Performance optimization: 28 | # Subsequent operations will be faster since the underlying array will 29 | # be contiguous in memory. The speed up can anything from nothing to 30 | # 5x faster depending on the operation. 31 | return img, np.ones(img.shape[:2], dtype=img.dtype) 32 | 33 | img = as_target_channels(img, 4) 34 | 35 | rgb = img[:, :, :3] 36 | alpha = img[:, :, 3] 37 | 38 | return rgb, alpha 39 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_dimension/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import image_dimensions_category 2 | 3 | padding_group = image_dimensions_category.add_node_group("Padding") 4 | crop_group = image_dimensions_category.add_node_group("Crop") 5 | resize_group = image_dimensions_category.add_node_group("Resize") 6 | utility_group = image_dimensions_category.add_node_group("Utility") 7 | 8 | resize_group.order = [ 9 | "chainner:image:resize", 10 | "chainner:image:resize_to_side", 11 | ] 12 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_dimension/utility/get_dimensions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.properties.inputs import ImageInput 6 | from nodes.properties.outputs import NumberOutput 7 | from nodes.utils.utils import get_h_w_c 8 | 9 | from .. import utility_group 10 | 11 | 12 | @utility_group.register( 13 | schema_id="chainner:image:get_dims", 14 | name="Get Dimensions", 15 | description=("Get the Height, Width, and number of Channels from an image."), 16 | icon="BsRulers", 17 | inputs=[ 18 | ImageInput(), 19 | ], 20 | outputs=[ 21 | NumberOutput("Width", output_type="Input0.width"), 22 | NumberOutput("Height", output_type="Input0.height"), 23 | NumberOutput("Channels", output_type="Input0.channels"), 24 | ], 25 | ) 26 | def get_dimensions_node( 27 | img: np.ndarray, 28 | ) -> tuple[int, int, int]: 29 | h, w, c = get_h_w_c(img) 30 | return w, h, c 31 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_filter/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import image_filter_category 2 | 3 | blur_group = image_filter_category.add_node_group("Blur") 4 | sharpen_group = image_filter_category.add_node_group("Sharpen") 5 | noise_group = image_filter_category.add_node_group("Noise") 6 | quantize_group = image_filter_category.add_node_group("Quantize") 7 | correction_group = image_filter_category.add_node_group("Correction") 8 | miscellaneous_group = image_filter_category.add_node_group("Miscellaneous") 9 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_filter/blur/gaussian_blur.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.groups import linked_inputs_group 6 | from nodes.impl.image_utils import fast_gaussian_blur 7 | from nodes.properties.inputs import ImageInput, SliderInput 8 | from nodes.properties.outputs import ImageOutput 9 | 10 | from .. import blur_group 11 | 12 | 13 | @blur_group.register( 14 | schema_id="chainner:image:gaussian_blur", 15 | name="Gaussian Blur", 16 | description="Apply Gaussian blur to an image.", 17 | icon="MdBlurOn", 18 | inputs=[ 19 | ImageInput(), 20 | linked_inputs_group( 21 | SliderInput( 22 | "Radius X", 23 | min=0, 24 | max=1000, 25 | default=1, 26 | precision=1, 27 | step=1, 28 | slider_step=0.1, 29 | scale="log", 30 | ), 31 | SliderInput( 32 | "Radius Y", 33 | min=0, 34 | max=1000, 35 | default=1, 36 | precision=1, 37 | step=1, 38 | slider_step=0.1, 39 | scale="log", 40 | ), 41 | ), 42 | ], 43 | outputs=[ImageOutput(shape_as=0)], 44 | ) 45 | def gaussian_blur_node( 46 | img: np.ndarray, 47 | sigma_x: float, 48 | sigma_y: float, 49 | ) -> np.ndarray: 50 | if sigma_x == 0 and sigma_y == 0: 51 | return img 52 | 53 | return fast_gaussian_blur(img, sigma_x, sigma_y) 54 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_filter/blur/median_blur.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import cv2 4 | import numpy as np 5 | 6 | from nodes.impl.image_utils import to_uint8 7 | from nodes.properties.inputs import ImageInput, SliderInput 8 | from nodes.properties.outputs import ImageOutput 9 | 10 | from .. import blur_group 11 | 12 | 13 | @blur_group.register( 14 | schema_id="chainner:image:median_blur", 15 | name="Median Blur", 16 | description="Apply median blur to an image.", 17 | icon="MdBlurOn", 18 | inputs=[ 19 | ImageInput(), 20 | SliderInput("Radius", min=0, max=1000, default=1, scale="log"), 21 | ], 22 | outputs=[ImageOutput(shape_as=0)], 23 | limited_to_8bpc=True, 24 | ) 25 | def median_blur_node( 26 | img: np.ndarray, 27 | radius: int, 28 | ) -> np.ndarray: 29 | if radius == 0: 30 | return img 31 | else: 32 | size = 2 * radius + 1 33 | if size <= 5: 34 | return cv2.medianBlur(img, size) 35 | else: # cv2 requires uint8 for kernel size > 5 36 | return cv2.medianBlur(to_uint8(img, normalized=True), size) 37 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_filter/miscellaneous/canny_edge_detection.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import cv2 4 | import numpy as np 5 | 6 | from nodes.impl.image_utils import to_uint8 7 | from nodes.properties.inputs import ImageInput, NumberInput 8 | from nodes.properties.outputs import ImageOutput 9 | 10 | from .. import miscellaneous_group 11 | 12 | 13 | @miscellaneous_group.register( 14 | schema_id="chainner:image:canny_edge_detection", 15 | name="Canny Edge Detection", 16 | description=( 17 | "Detect the edges of the input image and output as black and white image." 18 | ), 19 | icon="MdAutoFixHigh", 20 | inputs=[ 21 | ImageInput(), 22 | NumberInput("Lower Threshold", min=0, default=100), 23 | NumberInput("Upper Threshold", min=0, default=300), 24 | ], 25 | outputs=[ImageOutput(size_as=0, channels=1)], 26 | ) 27 | def canny_edge_detection_node( 28 | img: np.ndarray, 29 | t_lower: int, 30 | t_upper: int, 31 | ) -> np.ndarray: 32 | return cv2.Canny(to_uint8(img, normalized=True), t_lower, t_upper) 33 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_filter/miscellaneous/dilate.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | 5 | import cv2 6 | import numpy as np 7 | 8 | from nodes.properties.inputs import EnumInput, ImageInput, SliderInput 9 | from nodes.properties.outputs import ImageOutput 10 | 11 | from .. import miscellaneous_group 12 | 13 | 14 | class MorphShape(Enum): 15 | RECTANGLE = cv2.MORPH_RECT 16 | ELLIPSE = cv2.MORPH_ELLIPSE 17 | CROSS = cv2.MORPH_CROSS 18 | 19 | 20 | @miscellaneous_group.register( 21 | schema_id="chainner:image:dilate", 22 | name="Dilate", 23 | description="Dilate an image", 24 | icon="MdOutlineAutoFixHigh", 25 | inputs=[ 26 | ImageInput(), 27 | EnumInput( 28 | MorphShape, 29 | option_labels={ 30 | MorphShape.RECTANGLE: "Square", 31 | MorphShape.ELLIPSE: "Circle", 32 | MorphShape.CROSS: "Cross", 33 | }, 34 | ), 35 | SliderInput("Radius", min=0, max=1000, default=1, scale="log"), 36 | SliderInput("Iterations", min=0, max=1000, default=1, scale="log"), 37 | ], 38 | outputs=[ImageOutput(shape_as=0)], 39 | ) 40 | def dilate_node( 41 | img: np.ndarray, 42 | morph_shape: MorphShape, 43 | radius: int, 44 | iterations: int, 45 | ) -> np.ndarray: 46 | if radius == 0 or iterations == 0: 47 | return img 48 | 49 | size = 2 * radius + 1 50 | element = cv2.getStructuringElement(morph_shape.value, (size, size)) 51 | 52 | return cv2.dilate(img, element, iterations=iterations) 53 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_filter/miscellaneous/erode.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | 5 | import cv2 6 | import numpy as np 7 | 8 | from nodes.properties.inputs import EnumInput, ImageInput, SliderInput 9 | from nodes.properties.outputs import ImageOutput 10 | 11 | from .. import miscellaneous_group 12 | 13 | 14 | class MorphShape(Enum): 15 | RECTANGLE = cv2.MORPH_RECT 16 | ELLIPSE = cv2.MORPH_ELLIPSE 17 | CROSS = cv2.MORPH_CROSS 18 | 19 | 20 | @miscellaneous_group.register( 21 | schema_id="chainner:image:erode", 22 | name="Erode", 23 | description="Erode an image", 24 | icon="MdOutlineAutoFixHigh", 25 | inputs=[ 26 | ImageInput(), 27 | EnumInput( 28 | MorphShape, 29 | option_labels={ 30 | MorphShape.RECTANGLE: "Square", 31 | MorphShape.ELLIPSE: "Circle", 32 | MorphShape.CROSS: "Cross", 33 | }, 34 | ), 35 | SliderInput("Radius", min=0, max=1000, default=1, scale="log"), 36 | SliderInput("Iterations", min=0, max=1000, default=1, scale="log"), 37 | ], 38 | outputs=[ImageOutput(shape_as=0)], 39 | ) 40 | def erode_node( 41 | img: np.ndarray, 42 | morph_shape: MorphShape, 43 | radius: int, 44 | iterations: int, 45 | ) -> np.ndarray: 46 | if radius == 0 or iterations == 0: 47 | return img 48 | 49 | size = 2 * radius + 1 50 | element = cv2.getStructuringElement(morph_shape.value, (size, size)) 51 | 52 | return cv2.erode(img, element, iterations=iterations) 53 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_utility/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import image_utility_category 2 | 3 | modification_group = image_utility_category.add_node_group("Modification") 4 | compositing_group = image_utility_category.add_node_group("Compositing") 5 | miscellaneous_group = image_utility_category.add_node_group("Miscellaneous") 6 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_utility/compositing/add_caption.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.impl.caption import CaptionPosition, add_caption 6 | from nodes.properties.inputs import EnumInput, ImageInput, NumberInput, TextInput 7 | from nodes.properties.outputs import ImageOutput 8 | 9 | from .. import compositing_group 10 | 11 | 12 | @compositing_group.register( 13 | schema_id="chainner:image:caption", 14 | name="Add Caption", 15 | description="Add a caption to the top or bottom of an image.", 16 | icon="MdVideoLabel", 17 | inputs=[ 18 | ImageInput(), 19 | TextInput("Caption"), 20 | NumberInput("Size", min=20, default=42, unit="px"), 21 | EnumInput( 22 | CaptionPosition, 23 | "Position", 24 | default=CaptionPosition.BOTTOM, 25 | label_style="inline", 26 | ), 27 | ], 28 | outputs=[ 29 | ImageOutput( 30 | image_type=""" 31 | // this value is defined by `add_caption` 32 | let captionHeight = Input2; 33 | Image { 34 | width: Input0.width, 35 | height: Input0.height + captionHeight, 36 | channels: Input0.channels, 37 | } 38 | """, 39 | assume_normalized=True, 40 | ) 41 | ], 42 | ) 43 | def add_caption_node( 44 | img: np.ndarray, caption: str, size: int, position: CaptionPosition 45 | ) -> np.ndarray: 46 | return add_caption(img, caption, size, position) 47 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_utility/miscellaneous/generate_hash.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import base64 4 | import hashlib 5 | 6 | import numpy as np 7 | 8 | from nodes.impl.image_utils import to_uint8 9 | from nodes.properties.inputs import ImageInput, SliderInput 10 | from nodes.properties.outputs import TextOutput 11 | 12 | from .. import miscellaneous_group 13 | 14 | 15 | @miscellaneous_group.register( 16 | schema_id="chainner:image:generate_hash", 17 | name="Generate Hash", 18 | description="Generate a hash from an image using the BLAKE2b hashing algorithm.", 19 | icon="MdCalculate", 20 | inputs=[ 21 | ImageInput(), 22 | SliderInput("Digest Size (in bytes)", min=1, max=64, default=8).with_docs( 23 | "The digest size determines the length of the hash that is returned." 24 | ), 25 | ], 26 | outputs=[ 27 | TextOutput("Hex"), 28 | TextOutput("Base64"), 29 | ], 30 | ) 31 | def generate_hash_node(img: np.ndarray, size: int) -> tuple[str, str]: 32 | img = np.ascontiguousarray(to_uint8(img)) 33 | h = hashlib.blake2b(img, digest_size=size) # type: ignore 34 | return h.hexdigest(), base64.urlsafe_b64encode(h.digest()).decode("utf-8") 35 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/image_utility/modification/flip.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from api import KeyInfo 6 | from nodes.impl.image_utils import FlipAxis 7 | from nodes.properties.inputs import EnumInput, ImageInput 8 | from nodes.properties.outputs import ImageOutput 9 | 10 | from .. import modification_group 11 | 12 | 13 | @modification_group.register( 14 | schema_id="chainner:image:flip", 15 | name="Flip", 16 | description="Flip an image.", 17 | icon="MdFlip", 18 | inputs=[ 19 | ImageInput("Image"), 20 | EnumInput(FlipAxis), 21 | ], 22 | outputs=[ImageOutput(shape_as=0, assume_normalized=True)], 23 | key_info=KeyInfo.enum(1), 24 | ) 25 | def flip_node(img: np.ndarray, axis: FlipAxis) -> np.ndarray: 26 | return axis.flip(img) 27 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/material_textures/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import material_textures_category 2 | 3 | normal_map_group = material_textures_category.add_node_group("Normal Map") 4 | conversion_group = material_textures_category.add_node_group("Conversion") 5 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/material_textures/normal_map/balance_normals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | import navi 6 | from nodes.impl.normals.util import gr_to_xyz, normalize_normals, xyz_to_bgr 7 | from nodes.properties.inputs import ImageInput 8 | from nodes.properties.outputs import ImageOutput 9 | 10 | from .. import normal_map_group 11 | 12 | 13 | @normal_map_group.register( 14 | schema_id="chainner:image:balance_normals", 15 | name="Balance Normals", 16 | description=[ 17 | "This ensures that the average of all normals is pointing straight up. The input normal map is normalized before this operation is applied. The output normal map is guaranteed to be normalized.", 18 | ], 19 | icon="MdExpand", 20 | inputs=[ 21 | ImageInput("Normal Map", channels=[3, 4]), 22 | ], 23 | outputs=[ 24 | ImageOutput("Normal Map", image_type=navi.Image(size_as="Input0"), channels=3), 25 | ], 26 | ) 27 | def balance_normals_node(n: np.ndarray) -> np.ndarray: 28 | x, y, _ = gr_to_xyz(n) 29 | 30 | x -= np.mean(x) 31 | y -= np.mean(y) 32 | 33 | return xyz_to_bgr(normalize_normals(x, y)) 34 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/material_textures/normal_map/scale_normals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from nodes.impl.normals.addition import AdditionMethod, strengthen_normals 6 | from nodes.impl.normals.util import xyz_to_bgr 7 | from nodes.properties.inputs import EnumInput, ImageInput, SliderInput 8 | from nodes.properties.outputs import ImageOutput 9 | 10 | from .. import normal_map_group 11 | 12 | 13 | @normal_map_group.register( 14 | schema_id="chainner:image:strengthen_normals", 15 | name="Scale Normals", 16 | description=[ 17 | "Strengths or weakens the normals in the given normal map. Only the R and G channels of the input image will be used. The output normal map is guaranteed to be normalized.", 18 | "Conceptually, this node is equivalent to `chainner:image:add_normals` with the strength of the second normal map set to 0.", 19 | ], 20 | icon="MdExpand", 21 | inputs=[ 22 | ImageInput("Normal Map", channels=[3, 4]), 23 | SliderInput("Strength", max=400, default=100), 24 | EnumInput( 25 | AdditionMethod, 26 | label="Method", 27 | default=AdditionMethod.PARTIAL_DERIVATIVES, 28 | ), 29 | ], 30 | outputs=[ 31 | ImageOutput("Normal Map", size_as=0, channels=3), 32 | ], 33 | ) 34 | def scale_normals_node( 35 | n: np.ndarray, strength: int, method: AdditionMethod 36 | ) -> np.ndarray: 37 | return xyz_to_bgr(strengthen_normals(method, n, strength / 100)) 38 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import utility_category 2 | 3 | clipboard_group = utility_category.add_node_group("Clipboard") 4 | value_group = utility_category.add_node_group("Value") 5 | math_group = utility_category.add_node_group("Math") 6 | text_group = utility_category.add_node_group("Text") 7 | color_group = utility_category.add_node_group("Color") 8 | random_group = utility_category.add_node_group("Random") 9 | directory_group = utility_category.add_node_group("Directory") 10 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/clipboard/copy_to_clipboard.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | from chainner_ext import Clipboard 5 | 6 | from nodes.properties.inputs import ClipboardInput 7 | 8 | from .. import clipboard_group 9 | 10 | 11 | @clipboard_group.register( 12 | schema_id="chainner:utility:copy_to_clipboard", 13 | name="Copy To Clipboard", 14 | description="Copies the input to the clipboard.", 15 | icon="BsClipboard", 16 | inputs=[ 17 | ClipboardInput(), 18 | ], 19 | outputs=[], 20 | side_effects=True, 21 | limited_to_8bpc="The image will be copied to clipboard with 8 bits/channel.", 22 | ) 23 | def copy_to_clipboard_node(value: str | np.ndarray) -> None: 24 | if isinstance(value, np.ndarray): 25 | Clipboard.create_instance().write_image(value, "BGR") 26 | else: 27 | Clipboard.create_instance().write_text(value) 28 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/color/color.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from api import KeyInfo 4 | from nodes.impl.color.color import Color 5 | from nodes.properties.inputs import ColorInput 6 | from nodes.properties.outputs import ColorOutput 7 | 8 | from .. import color_group 9 | 10 | 11 | @color_group.register( 12 | schema_id="chainner:utility:color", 13 | name="Color", 14 | description="Outputs the given color.", 15 | icon="MdColorLens", 16 | inputs=[ 17 | ColorInput().make_fused(), 18 | ], 19 | outputs=[ 20 | ColorOutput(color_type="Input0").suggest(), 21 | ], 22 | key_info=KeyInfo.type( 23 | """ 24 | let channels = Input0.channels; 25 | match channels { 26 | 1 => "Gray", 27 | 3 => "RGB", 28 | 4 => "RGBA", 29 | _ => never 30 | } 31 | """ 32 | ), 33 | ) 34 | def color_node(color: Color) -> Color: 35 | return color 36 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/directory/directory_go_up.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from nodes.properties.inputs import DirectoryInput, NumberInput 6 | from nodes.properties.outputs import DirectoryOutput 7 | 8 | from .. import directory_group 9 | 10 | 11 | @directory_group.register( 12 | schema_id="chainner:utility:back_directory", 13 | name="Directory Go Up", 14 | description="Traverse up from a directory the specified number of times.", 15 | icon="BsFolder", 16 | inputs=[ 17 | DirectoryInput(must_exist=False, label_style="hidden"), 18 | NumberInput("Times", min=0, default=1, label_style="inline").with_docs( 19 | "How many times to go up.", 20 | "- 0 will return the given directory as is.", 21 | "- 1 will return the parent directory.", 22 | "- 2 will return the grandparent directory.", 23 | "- etc.", 24 | hint=True, 25 | ), 26 | ], 27 | outputs=[ 28 | DirectoryOutput( 29 | output_type="Directory { path: getParentDirectory(Input0.path, Input1) }", 30 | ), 31 | ], 32 | ) 33 | def directory_go_up_node(directory: Path, amt: int) -> Path: 34 | result = directory 35 | for _ in range(amt): 36 | result = result.parent 37 | return result 38 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/directory/directory_to_text.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from nodes.properties.inputs import DirectoryInput 6 | from nodes.properties.outputs import TextOutput 7 | 8 | from .. import directory_group 9 | 10 | 11 | @directory_group.register( 12 | schema_id="chainner:utility:directory_to_text", 13 | name="Directory to Text", 14 | description="Converts a directory path into text.", 15 | icon="BsFolder", 16 | inputs=[ 17 | DirectoryInput(must_exist=False, label_style="hidden"), 18 | ], 19 | outputs=[ 20 | TextOutput( 21 | "Dir Text", 22 | output_type="Input0.path", 23 | ), 24 | ], 25 | ) 26 | def directory_to_text_node(directory: Path) -> str: 27 | return str(directory) 28 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/random/random_number.py: -------------------------------------------------------------------------------- 1 | from random import Random 2 | 3 | from nodes.groups import seed_group 4 | from nodes.properties.inputs import NumberInput, SeedInput 5 | from nodes.properties.outputs import NumberOutput 6 | from nodes.utils.seed import Seed 7 | 8 | from .. import random_group 9 | 10 | 11 | @random_group.register( 12 | schema_id="chainner:utility:random_number", 13 | name="Random Number", 14 | description="Generate a random integer.", 15 | icon="MdCalculate", 16 | inputs=[ 17 | NumberInput("Min", min=None, max=None, default=0), 18 | NumberInput("Max", min=None, max=None, default=100), 19 | seed_group(SeedInput()), 20 | ], 21 | outputs=[ 22 | NumberOutput("Result", output_type="int & max(.., Input0) & min(.., Input1)") 23 | ], 24 | ) 25 | def random_number_node(min_val: int, max_val: int, seed: Seed) -> int: 26 | return Random(seed.value).randint(min_val, max_val) 27 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/text/note.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from nodes.properties.inputs import BoolInput, TextInput 4 | 5 | from .. import text_group 6 | 7 | 8 | # This node is a bit special as it has special handling by the frontend. Changes made here will not necessarily be reflected in the frontend. 9 | @text_group.register( 10 | schema_id="chainner:utility:note", 11 | name="Note", 12 | description="Make a sticky note for whatever notes or comments you want to leave in the chain. Supports markdown syntax", 13 | icon="MdOutlineStickyNote2", 14 | inputs=[ 15 | TextInput( 16 | "Note Text", 17 | multiline=True, 18 | has_handle=False, 19 | label_style="hidden", 20 | ).make_optional(), 21 | BoolInput("Display Markdown", default=False), 22 | ], 23 | outputs=[], 24 | ) 25 | def note_node(_text: str | None, display_markdown: bool) -> None: 26 | return 27 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/text/text_length.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from nodes.properties.inputs import TextInput 4 | from nodes.properties.outputs import NumberOutput 5 | 6 | from .. import text_group 7 | 8 | 9 | @text_group.register( 10 | schema_id="chainner:utility:text_length", 11 | name="Text Length", 12 | description="Returns the number characters in a string of text.", 13 | icon="MdTextFields", 14 | inputs=[ 15 | TextInput("Text"), 16 | ], 17 | outputs=[ 18 | NumberOutput("Length", output_type="string::len(Input0)"), 19 | ], 20 | ) 21 | def text_length_node(text: str) -> int: 22 | return len(text) 23 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/value/conditional.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from api import Lazy 4 | from nodes.properties.inputs import AnyInput, BoolInput 5 | from nodes.properties.outputs import BaseOutput 6 | 7 | from .. import value_group 8 | 9 | 10 | @value_group.register( 11 | schema_id="chainner:utility:conditional", 12 | name="Conditional", 13 | description="Allows you to pass in multiple inputs and then change which one passes through to the output.", 14 | icon="BsShuffle", 15 | inputs=[ 16 | BoolInput("Condition", default=True, has_handle=True).with_id(0), 17 | AnyInput(label="If True").with_id(1).make_lazy(), 18 | AnyInput(label="If False").with_id(2).make_lazy(), 19 | ], 20 | outputs=[ 21 | BaseOutput( 22 | output_type=""" 23 | if Input0 { Input1 } else { Input2 } 24 | """, 25 | label="Value", 26 | ).as_passthrough_of(1), 27 | ], 28 | see_also=["chainner:utility:switch"], 29 | ) 30 | def conditional_node( 31 | cond: bool, if_true: Lazy[object], if_false: Lazy[object] 32 | ) -> object: 33 | return if_true.value if cond else if_false.value 34 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/value/directory.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from nodes.properties.inputs import DirectoryInput 6 | from nodes.properties.outputs import DirectoryOutput 7 | 8 | from .. import value_group 9 | 10 | 11 | @value_group.register( 12 | schema_id="chainner:utility:directory", 13 | name="Directory", 14 | description="Outputs the given directory.", 15 | icon="BsFolder", 16 | inputs=[ 17 | DirectoryInput(must_exist=False, label_style="hidden").make_fused(), 18 | ], 19 | outputs=[ 20 | DirectoryOutput(output_type="Input0").suggest(), 21 | ], 22 | ) 23 | def directory_node(directory: Path) -> Path: 24 | return directory 25 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/value/execution_number.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from nodes.properties.inputs.__system_inputs import StaticValueInput 4 | from nodes.properties.outputs import NumberOutput 5 | 6 | from .. import value_group 7 | 8 | 9 | @value_group.register( 10 | schema_id="chainner:utility:execution_number", 11 | name="Execution Number", 12 | description="Get the current execution number of this session. Increments by 1 every time you press the run button.", 13 | icon="MdNumbers", 14 | inputs=[ 15 | StaticValueInput( 16 | label="Value", 17 | value="execution_number", 18 | navi_type="int(1..)", 19 | py_type=int, 20 | ).make_fused(), 21 | ], 22 | outputs=[ 23 | NumberOutput("Execution Number", output_type="Input0"), 24 | ], 25 | ) 26 | def execution_number_node(number: int) -> int: 27 | return number 28 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/value/number.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from api import KeyInfo 4 | from nodes.properties.inputs import NumberInput 5 | from nodes.properties.outputs import NumberOutput 6 | 7 | from .. import value_group 8 | 9 | 10 | @value_group.register( 11 | schema_id="chainner:utility:number", 12 | name="Number", 13 | description="Outputs the given number.", 14 | icon="MdCalculate", 15 | inputs=[ 16 | NumberInput( 17 | "Number", 18 | min=None, 19 | max=None, 20 | precision="unlimited", 21 | step=1, 22 | label_style="hidden", 23 | ).make_fused(), 24 | ], 25 | outputs=[ 26 | NumberOutput("Number", output_type="Input0").suggest(), 27 | ], 28 | key_info=KeyInfo.number(0), 29 | ) 30 | def number_node(number: float) -> float: 31 | return number 32 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/value/parse_number.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from nodes.properties.inputs import NumberInput, TextInput 4 | from nodes.properties.outputs import NumberOutput 5 | 6 | from .. import value_group 7 | 8 | 9 | @value_group.register( 10 | schema_id="chainner:utility:parse_number", 11 | name="Parse Number", 12 | description="Parses text to base-10.", 13 | icon="MdCalculate", 14 | inputs=[ 15 | TextInput("Text", label_style="inline"), 16 | NumberInput("Base", default=10, min=2, max=36), 17 | ], 18 | outputs=[ 19 | NumberOutput( 20 | "Value", 21 | output_type="int & number::parseInt(Input0, Input1)", 22 | ).with_never_reason("The given text cannot be parsed into a number."), 23 | ], 24 | ) 25 | def parse_number_node(text: str, base: int) -> int: 26 | return int(text, base) 27 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/value/percent.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from api import KeyInfo 4 | from nodes.properties.inputs import SliderInput 5 | from nodes.properties.outputs import NumberOutput 6 | 7 | from .. import value_group 8 | 9 | 10 | @value_group.register( 11 | schema_id="chainner:utility:percent", 12 | name="Percent", 13 | description="Outputs the given percent.", 14 | icon="MdCalculate", 15 | inputs=[ 16 | SliderInput("Percent", min=0, max=100, default=50, unit="%").make_fused(), 17 | ], 18 | outputs=[ 19 | NumberOutput("Percent", output_type="Input0"), 20 | ], 21 | key_info=KeyInfo.number(0), 22 | ) 23 | def percent_node(number: int) -> int: 24 | return number 25 | -------------------------------------------------------------------------------- /backend/src/packages/chaiNNer_standard/utility/value/text.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from nodes.properties.inputs import TextInput 4 | from nodes.properties.outputs import TextOutput 5 | 6 | from .. import value_group 7 | 8 | 9 | @value_group.register( 10 | schema_id="chainner:utility:text", 11 | name="Text", 12 | description="Outputs the given text.", 13 | icon="MdTextFields", 14 | inputs=[ 15 | TextInput( 16 | "Text", min_length=0, label_style="hidden", allow_empty_string=True 17 | ).make_fused(), 18 | ], 19 | outputs=[ 20 | TextOutput("Text", output_type="Input0").suggest(), 21 | ], 22 | ) 23 | def text_node(text: str) -> str: 24 | return text 25 | -------------------------------------------------------------------------------- /backend/src/response.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Literal, TypedDict 4 | 5 | from events import ExecutionErrorSource 6 | from process import NodeExecutionError 7 | 8 | 9 | class SuccessResponse(TypedDict): 10 | type: Literal["success"] 11 | 12 | 13 | class ErrorResponse(TypedDict): 14 | type: Literal["error"] 15 | message: str 16 | exception: str 17 | source: ExecutionErrorSource | None 18 | 19 | 20 | class NoExecutorResponse(TypedDict): 21 | type: Literal["no-executor"] 22 | 23 | 24 | class AlreadyRunningResponse(TypedDict): 25 | type: Literal["already-running"] 26 | message: str 27 | 28 | 29 | def success_response() -> SuccessResponse: 30 | return {"type": "success"} 31 | 32 | 33 | def error_response( 34 | message: str, 35 | exception: str | Exception, 36 | source: ExecutionErrorSource | None = None, 37 | ) -> ErrorResponse: 38 | if source is None and isinstance(exception, NodeExecutionError): 39 | source = { 40 | "nodeId": exception.node_id, 41 | "schemaId": exception.node_data.schema_id, 42 | "inputs": exception.inputs, 43 | } 44 | return { 45 | "type": "error", 46 | "message": message, 47 | "exception": str(exception), 48 | "source": source, 49 | } 50 | 51 | 52 | def no_executor_response() -> NoExecutorResponse: 53 | return {"type": "no-executor"} 54 | 55 | 56 | def already_running_response(message: str) -> AlreadyRunningResponse: 57 | return {"type": "already-running", "message": message} 58 | -------------------------------------------------------------------------------- /backend/src/run.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | # Install server dependencies. Can't start the server without them, but we don't want to install the other deps yet. 4 | importlib.import_module("dependencies.install_server_deps") 5 | 6 | # Start the host server 7 | server_host = importlib.import_module("server_host") 8 | server_host.main() 9 | -------------------------------------------------------------------------------- /backend/src/system.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import sys 3 | 4 | is_mac = sys.platform == "darwin" 5 | is_arm_mac = is_mac and platform.machine() == "arm64" 6 | is_windows = sys.platform == "win32" 7 | is_linux = sys.platform == "linux" 8 | -------------------------------------------------------------------------------- /backend/src/texconv/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2022 Microsoft Corp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies 13 | or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 17 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 18 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /backend/src/texconv/texconv.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/backend/src/texconv/texconv.exe -------------------------------------------------------------------------------- /backend/tests/test_dummy.py: -------------------------------------------------------------------------------- 1 | from navi import Image 2 | 3 | 4 | def test_dummy(): 5 | Image(1, 1, 3) 6 | -------------------------------------------------------------------------------- /docs/assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/docs/assets/banner.png -------------------------------------------------------------------------------- /docs/assets/input-override.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/docs/assets/input-override.png -------------------------------------------------------------------------------- /docs/assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/docs/assets/screenshot.png -------------------------------------------------------------------------------- /docs/assets/simple_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/docs/assets/simple_screenshot.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <title>chaiNNer</title> 6 | 7 | </head> 8 | <body> 9 | <div id="root"> 10 | 11 | </div> 12 | <script> 13 | if (global === undefined) { 14 | var global = window; 15 | } 16 | </script> 17 | <script type="module" src="./src/renderer/renderer.ts"></script> 18 | </body> 19 | </html> 20 | -------------------------------------------------------------------------------- /patches/@electron-forge+plugin-vite+7.4.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@electron-forge/plugin-vite/dist/util/package.js b/node_modules/@electron-forge/plugin-vite/dist/util/package.js 2 | index be2a9f9..cf61a90 100644 3 | --- a/node_modules/@electron-forge/plugin-vite/dist/util/package.js 4 | +++ b/node_modules/@electron-forge/plugin-vite/dist/util/package.js 5 | @@ -34,7 +34,10 @@ async function lookupNodeModulesPaths(root, paths = []) { 6 | } 7 | exports.lookupNodeModulesPaths = lookupNodeModulesPaths; 8 | async function resolveDependencies(root) { 9 | - const rootDependencies = Object.keys((await fs_extra_1.default.readJson(node_path_1.default.join(root, 'package.json'))).dependencies || {}); 10 | + // FIXME: spm patch to allow tree shaking with vite and forge 11 | + // 12 | + // const rootDependencies = Object.keys((await fs_extra_1.default.readJson(node_path_1.default.join(root, 'package.json'))).dependencies || {}); 13 | + const rootDependencies = []; 14 | const resolve = async (prePath, dependencies, collected = new Map()) => await Promise.all(dependencies.map(async (name) => { 15 | let curPath = prePath, depPath = null, packageJson = null; 16 | while (!packageJson && !isRootPath(curPath)) { 17 | -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "backend" 4 | ], 5 | "extraPaths": [ 6 | "backend/src" 7 | ], 8 | "exclude": [ 9 | "**/__pycache__" 10 | ], 11 | "ignore": [], 12 | "typeCheckingMode": "basic", 13 | "useLibraryCodeForTypes": true, 14 | "strictListInference": true, 15 | "strictDictionaryInference": true, 16 | "strictSetInference": true, 17 | "reportDuplicateImport": "warning", 18 | "reportImportCycles": "error", 19 | "reportIncompatibleVariableOverride": "error", 20 | "reportIncompatibleMethodOverride": "error", 21 | "reportOverlappingOverload": "error", 22 | "reportPrivateImportUsage": "error", 23 | "reportUninitializedInstanceVariable": "error", 24 | "reportUnnecessaryCast": "error", 25 | "reportUnnecessaryComparison": "error", 26 | "reportUnnecessaryContains": "error", 27 | "reportUnnecessaryIsInstance": "error", 28 | // "reportUnnecessaryTypeIgnoreComment": "warning", 29 | "reportUnusedClass": "warning", 30 | "reportUnusedFunction": "warning", 31 | "reportUnusedImport": "warning", 32 | "reportUnusedVariable": "warning", 33 | } 34 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ruff==0.4.0 2 | debugpy 3 | pyright==1.1.338 4 | pytest 5 | appdirs==1.4.4 6 | viztracer 7 | -------------------------------------------------------------------------------- /src/common/CategoryMap.ts: -------------------------------------------------------------------------------- 1 | import { Category, CategoryId, NodeGroup, NodeGroupId } from './common-types'; 2 | 3 | export class CategoryMap { 4 | /** 5 | * An ordered list of all categories supported by the backend. 6 | * 7 | * Some categories might be empty. 8 | */ 9 | readonly categories: readonly Category[]; 10 | 11 | private readonly lookup: ReadonlyMap<CategoryId, Category>; 12 | 13 | private readonly lookupGroup: ReadonlyMap<NodeGroupId, NodeGroup>; 14 | 15 | static readonly EMPTY: CategoryMap = new CategoryMap([]); 16 | 17 | constructor(categories: readonly Category[]) { 18 | // defensive copy 19 | this.categories = [...categories]; 20 | this.lookup = new Map(categories.map((c) => [c.id, c] as const)); 21 | this.lookupGroup = new Map( 22 | categories.flatMap((c) => c.groups).map((g) => [g.id, g] as const) 23 | ); 24 | } 25 | 26 | get(id: CategoryId): Category | undefined { 27 | return this.lookup.get(id); 28 | } 29 | 30 | getGroup(id: NodeGroupId): NodeGroup | undefined { 31 | return this.lookupGroup.get(id); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/common/Validity.ts: -------------------------------------------------------------------------------- 1 | export type Validity = 2 | | { readonly isValid: true } 3 | | { readonly isValid: false; readonly reason: string }; 4 | 5 | export const VALID: Validity = { isValid: true }; 6 | 7 | export const invalid = (reason: string): Validity => ({ isValid: false, reason }); 8 | 9 | export const bothValid = (a: Validity, b: Validity): Validity => { 10 | if (!a.isValid) return a; 11 | if (!b.isValid) return b; 12 | return VALID; 13 | }; 14 | -------------------------------------------------------------------------------- /src/common/i18n.ts: -------------------------------------------------------------------------------- 1 | import { InitOptions } from 'i18next'; 2 | import en from './locales/en/translation.json'; 3 | 4 | const resources = { 5 | en, 6 | }; 7 | 8 | export const DEFAULT_OPTIONS: InitOptions = { 9 | resources, 10 | lng: 'en', 11 | 12 | interpolation: { 13 | escapeValue: false, 14 | }, 15 | 16 | returnNull: false, 17 | }; 18 | -------------------------------------------------------------------------------- /src/common/input-override-common.ts: -------------------------------------------------------------------------------- 1 | import { InputId } from './common-types'; 2 | 3 | export type InputOverrideId = string & { readonly __input_override_id?: never }; 4 | 5 | export const createInputOverrideId = (nodeId: string, inputId: InputId): InputOverrideId => { 6 | if (nodeId.length !== 36) 7 | throw new Error('Expected node id to be a 36 character hexadecimal UUID.'); 8 | return `#${nodeId}:${inputId}`; 9 | }; 10 | -------------------------------------------------------------------------------- /src/common/links.ts: -------------------------------------------------------------------------------- 1 | export const links = { 2 | kofi: 'https://ko-fi.com/T6T46KTTW', 3 | }; 4 | -------------------------------------------------------------------------------- /src/common/nodes/checkFeatures.ts: -------------------------------------------------------------------------------- 1 | import { Feature, FeatureId, FeatureState } from '../common-types'; 2 | import { joinEnglish } from '../util'; 3 | import { VALID, Validity, invalid } from '../Validity'; 4 | 5 | export const checkFeatures = ( 6 | features: readonly FeatureId[], 7 | featureMap: ReadonlyMap<FeatureId, Feature>, 8 | featureStates: ReadonlyMap<FeatureId, FeatureState> 9 | ): Validity => { 10 | const nonEnabledFeatures = features.filter((f) => !featureStates.get(f)?.enabled); 11 | if (nonEnabledFeatures.length === 0) return VALID; 12 | 13 | const getName = (f: FeatureId) => featureMap.get(f)?.name ?? f; 14 | 15 | let prefix; 16 | if (nonEnabledFeatures.length === 1) { 17 | prefix = `The feature ${getName(nonEnabledFeatures[0])} is`; 18 | } else { 19 | prefix = `The features ${joinEnglish(nonEnabledFeatures.map(getName))} are`; 20 | } 21 | 22 | return invalid( 23 | `${prefix} required to run this node. See the dependency manager for more details.` 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/common/nodes/inputCondition.ts: -------------------------------------------------------------------------------- 1 | import { Condition, Group, InputId, NodeSchema } from '../common-types'; 2 | import { lazyKeyed } from '../util'; 3 | 4 | const analyseInputs = lazyKeyed((schema: NodeSchema): ReadonlyMap<InputId, Condition> => { 5 | const byInput = new Map<InputId, Condition>(); 6 | 7 | const conditionStack: Condition[] = []; 8 | const recurse = (inputs: readonly (InputId | Group)[]): void => { 9 | for (const i of inputs) { 10 | if (typeof i === 'object') { 11 | if (i.kind === 'conditional') { 12 | conditionStack.push(i.options.condition); 13 | recurse(i.items); 14 | conditionStack.pop(); 15 | } 16 | } else { 17 | byInput.set(i, { 18 | kind: 'and', 19 | items: [...conditionStack], 20 | }); 21 | } 22 | } 23 | }; 24 | 25 | recurse(schema.groupLayout); 26 | 27 | return byInput; 28 | }); 29 | 30 | export const getInputCondition = (schema: NodeSchema, inputId: InputId): Condition | undefined => { 31 | return analyseInputs(schema).get(inputId); 32 | }; 33 | -------------------------------------------------------------------------------- /src/common/nodes/sort.ts: -------------------------------------------------------------------------------- 1 | import { CategoryMap } from '../CategoryMap'; 2 | import { NodeGroup, NodeSchema } from '../common-types'; 3 | import { groupBy } from '../util'; 4 | 5 | const sortGroupNodes = (nodes: readonly NodeSchema[], group: NodeGroup): NodeSchema[] => { 6 | const ordered: NodeSchema[] = []; 7 | const unordered: NodeSchema[] = []; 8 | 9 | for (const n of nodes) { 10 | if (group.order.includes(n.schemaId)) { 11 | ordered.push(n); 12 | } else { 13 | unordered.push(n); 14 | } 15 | } 16 | 17 | ordered.sort((a, b) => group.order.indexOf(a.schemaId) - group.order.indexOf(b.schemaId)); 18 | 19 | unordered.sort((a, b) => { 20 | return a.name.localeCompare(b.name); 21 | }); 22 | 23 | return [...ordered, ...unordered]; 24 | }; 25 | 26 | export const sortNodes = (nodes: readonly NodeSchema[], categories: CategoryMap): NodeSchema[] => { 27 | const byGroup = groupBy(nodes, 'nodeGroup'); 28 | 29 | return categories.categories 30 | .flatMap((c) => c.groups) 31 | .flatMap((g) => sortGroupNodes(byGroup.get(g.id) ?? [], g)); 32 | }; 33 | -------------------------------------------------------------------------------- /src/common/rust-regex.ts: -------------------------------------------------------------------------------- 1 | import wasmUrl from 'rregex/lib/rregex.wasm?url'; 2 | import init, { RRegex as _rr } from 'rregex/lib/web'; 3 | import { log } from './log'; 4 | 5 | // This is not good, but I can't think of a better way. 6 | // We are racing loading the wasm module and using it. 7 | init(wasmUrl).catch(log.error); 8 | 9 | export class RRegex extends _rr {} 10 | -------------------------------------------------------------------------------- /src/common/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import { PackageSettings, SchemaId, WindowSize } from '../common-types'; 2 | 3 | export interface ChainnerSettings { 4 | useSystemPython: boolean; 5 | systemPythonLocation: string; 6 | 7 | // renderer 8 | theme: string; 9 | checkForUpdatesOnStartup: boolean; 10 | startupTemplate: string; 11 | animateChain: boolean; 12 | snapToGrid: boolean; 13 | snapToGridAmount: number; 14 | viewportExportPadding: number; 15 | showMinimap: boolean; 16 | 17 | experimentalFeatures: boolean; 18 | hardwareAcceleration: boolean; 19 | allowMultipleInstances: boolean; 20 | 21 | lastWindowSize: WindowSize; 22 | 23 | favoriteNodes: readonly SchemaId[]; 24 | 25 | packageSettings: PackageSettings; 26 | 27 | storage: Readonly<Record<string, unknown>>; 28 | } 29 | 30 | export const defaultSettings: Readonly<ChainnerSettings> = { 31 | useSystemPython: false, 32 | systemPythonLocation: '', 33 | 34 | // renderer 35 | theme: 'default-dark', 36 | checkForUpdatesOnStartup: true, 37 | startupTemplate: '', 38 | animateChain: true, 39 | snapToGrid: false, 40 | snapToGridAmount: 16, 41 | viewportExportPadding: 20, 42 | showMinimap: false, 43 | 44 | experimentalFeatures: false, 45 | hardwareAcceleration: false, 46 | allowMultipleInstances: false, 47 | 48 | lastWindowSize: { 49 | maximized: false, 50 | width: 1280, 51 | height: 720, 52 | }, 53 | 54 | favoriteNodes: [], 55 | 56 | packageSettings: {}, 57 | 58 | storage: {}, 59 | }; 60 | -------------------------------------------------------------------------------- /src/common/ui/error.ts: -------------------------------------------------------------------------------- 1 | import { InterruptRequest } from './interrupt'; 2 | 3 | export class CriticalError extends Error { 4 | interrupt: InterruptRequest; 5 | 6 | constructor(interrupt: Omit<InterruptRequest, 'type'>) { 7 | super(interrupt.message); 8 | this.interrupt = { type: 'critical error', ...interrupt }; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/common/ui/interrupt.ts: -------------------------------------------------------------------------------- 1 | interface ActionBase<T extends string> { 2 | readonly type: T; 3 | } 4 | 5 | interface OpenUrlAction extends ActionBase<'open-url'> { 6 | url: string; 7 | } 8 | interface RunAction extends ActionBase<'run'> { 9 | action: () => void; 10 | } 11 | 12 | export type Action = OpenUrlAction | RunAction; 13 | 14 | export interface InteractionOption { 15 | title: string; 16 | action: Action; 17 | } 18 | 19 | export type InterruptType = 'critical error' | 'warning'; 20 | 21 | export interface InterruptRequest { 22 | type: InterruptType; 23 | title?: string; 24 | message: string; 25 | options?: InteractionOption[]; 26 | } 27 | -------------------------------------------------------------------------------- /src/common/version.ts: -------------------------------------------------------------------------------- 1 | import semver from 'semver'; 2 | import { Version } from './common-types'; 3 | 4 | export const parse = (v: string): Version => { 5 | const version = semver.coerce(v); 6 | if (!version) { 7 | throw new SyntaxError(`Invalid version '${v}'`); 8 | } 9 | return version.version as Version; 10 | }; 11 | 12 | export const versionLt = (lhs: Version, rhs: Version): boolean => { 13 | try { 14 | return semver.lt(parse(lhs), parse(rhs)); 15 | } catch { 16 | return false; 17 | } 18 | }; 19 | 20 | export const versionLte = (lhs: Version, rhs: Version): boolean => { 21 | try { 22 | return semver.lte(parse(lhs), parse(rhs)); 23 | } catch { 24 | return false; 25 | } 26 | }; 27 | 28 | export const versionGt = (lhs: Version, rhs: Version): boolean => { 29 | try { 30 | return semver.gt(parse(lhs), parse(rhs)); 31 | } catch { 32 | return false; 33 | } 34 | }; 35 | 36 | export const versionGte = (lhs: Version, rhs: Version): boolean => { 37 | try { 38 | return semver.gte(parse(lhs), parse(rhs)); 39 | } catch { 40 | return false; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-default-export */ 2 | 3 | declare module '*.svg' { 4 | const content: string; 5 | export default content; 6 | } 7 | declare module '*.png' { 8 | const content: string; 9 | export default content; 10 | } 11 | declare module '*.gif' { 12 | const content: string; 13 | export default content; 14 | } 15 | declare module '*.jpg' { 16 | const content: string; 17 | export default content; 18 | } 19 | declare module '*.jpeg' { 20 | const content: string; 21 | export default content; 22 | } 23 | 24 | declare module 'rregex/lib/rregex.wasm?url' { 25 | const content: string; 26 | export default content; 27 | } 28 | 29 | declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string; 30 | declare const MAIN_WINDOW_VITE_NAME: string; 31 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-var 2 | declare var startupFile: string | null; 3 | 4 | interface Window { 5 | unsafeIpcRenderer: import('electron').IpcRenderer; 6 | } 7 | -------------------------------------------------------------------------------- /src/i18next.d.ts: -------------------------------------------------------------------------------- 1 | // https://www.i18next.com/overview/typescript#create-a-declaration-file 2 | // https://www.i18next.com/overview/typescript#custom-type-options 3 | 4 | import 'i18next'; 5 | 6 | declare module 'i18next' { 7 | // Extend CustomTypeOptions 8 | interface CustomTypeOptions { 9 | returnNull: false; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/childProc.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | export const promisifiedSpawn = async (command: string, args: string[]): Promise<string> => { 4 | return new Promise((resolve, reject) => { 5 | const proc = spawn(command, args); 6 | let stdout = ''; 7 | let stderr = ''; 8 | proc.stdout.on('data', (data) => { 9 | stdout += data; 10 | }); 11 | proc.stderr.on('data', (data) => { 12 | stderr += data; 13 | }); 14 | proc.on('error', (err) => { 15 | reject(err); 16 | }); 17 | proc.on('close', (code) => { 18 | if (code !== 0) { 19 | reject(new Error(stderr)); 20 | } else { 21 | resolve(stdout); 22 | } 23 | }); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/main/cli/create.ts: -------------------------------------------------------------------------------- 1 | import electronLog from 'electron-log'; 2 | import { app } from 'electron/main'; 3 | import { log } from '../../common/log'; 4 | import { Exit } from './exit'; 5 | 6 | const fatalErrorInMain = (error: unknown) => { 7 | log.error('Error in Main process'); 8 | log.error(error); 9 | app.exit(1); 10 | }; 11 | 12 | const setupErrorHandling = () => { 13 | electronLog.catchErrors({ 14 | showDialog: false, 15 | onError: fatalErrorInMain, 16 | }); 17 | 18 | process.on('uncaughtException', fatalErrorInMain); 19 | }; 20 | 21 | export const createCli = (command: () => Promise<void>) => { 22 | setupErrorHandling(); 23 | 24 | // we don't need hardware acceleration at all 25 | app.disableHardwareAcceleration(); 26 | 27 | command().then( 28 | () => { 29 | app.exit(0); 30 | }, 31 | (error) => { 32 | if (error instanceof Exit) { 33 | app.exit(error.exitCode); 34 | } else { 35 | log.error(error); 36 | app.exit(1); 37 | } 38 | } 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/main/cli/exit.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An error that signals that the CLI app should exit in error with a given non-zero exit code. 3 | */ 4 | export class Exit extends Error { 5 | exitCode: number; 6 | 7 | constructor(exitCode = 1) { 8 | super(`Exit with code ${exitCode}`); 9 | this.exitCode = exitCode; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/env.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | export const isMac = process.platform === 'darwin'; 4 | const cpuModel = os.cpus()[0]?.model || null; 5 | export const isArmMac: boolean = isMac && !!cpuModel && /Apple M\d/i.test(cpuModel); 6 | 7 | const env = { ...process.env }; 8 | delete env.PYTHONHOME; 9 | export const sanitizedEnv = env; 10 | -------------------------------------------------------------------------------- /src/main/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import backend from 'i18next-http-backend'; 3 | import { DEFAULT_OPTIONS } from '../common/i18n'; 4 | import { log } from '../common/log'; 5 | 6 | i18n.use(backend) 7 | .init({ ...DEFAULT_OPTIONS, saveMissing: true }) 8 | .catch(log.error); 9 | -------------------------------------------------------------------------------- /src/main/platform.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron/main'; 2 | import { existsSync } from 'fs'; 3 | import os from 'os'; 4 | import path from 'path'; 5 | import { lazy } from '../common/util'; 6 | 7 | export type SupportedPlatform = 'linux' | 'darwin' | 'win32'; 8 | 9 | export const getPlatform = (): SupportedPlatform => { 10 | const platform = os.platform(); 11 | switch (platform) { 12 | case 'win32': 13 | case 'linux': 14 | case 'darwin': 15 | return platform; 16 | default: 17 | throw new Error( 18 | `Unsupported platform: ${platform}. Please report this to us and we may add support.` 19 | ); 20 | } 21 | }; 22 | 23 | export const currentExecutableDir = path.dirname(app.getPath('exe')); 24 | 25 | export const getIsPortableSync = lazy((): boolean => { 26 | const isPortable = existsSync(path.join(currentExecutableDir, 'portable')); 27 | return isPortable; 28 | }); 29 | 30 | export const getRootDir = lazy((): string => { 31 | const isPortable = getIsPortableSync(); 32 | const rootDir = isPortable ? currentExecutableDir : app.getPath('userData'); 33 | return rootDir; 34 | }); 35 | 36 | export const getLogsFolder = lazy((): string => { 37 | return path.join(getRootDir(), 'logs'); 38 | }); 39 | 40 | export const getBackendStorageFolder = lazy((): string => { 41 | return path.join(getRootDir(), 'backend-storage'); 42 | }); 43 | 44 | export const installDir = getIsPortableSync() 45 | ? path.dirname(app.getPath('exe')) 46 | : path.join(path.dirname(app.getPath('exe')), '..'); 47 | -------------------------------------------------------------------------------- /src/main/python/checkPythonPaths.ts: -------------------------------------------------------------------------------- 1 | import { PythonInfo } from '../../common/common-types'; 2 | import { log } from '../../common/log'; 3 | import { getPythonVersion, isSupportedPythonVersion } from './version'; 4 | 5 | export const checkPythonPaths = async (pythonsToCheck: string[]): Promise<PythonInfo> => { 6 | for (const py of pythonsToCheck) { 7 | // eslint-disable-next-line no-await-in-loop 8 | const version = await getPythonVersion(py).catch(log.error); 9 | if (version && isSupportedPythonVersion(version)) { 10 | return { python: py, version }; 11 | } 12 | } 13 | 14 | throw new Error( 15 | `No Python binaries in [${pythonsToCheck.join(', ')}] found or supported version (3.8+)` 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/main/python/version.ts: -------------------------------------------------------------------------------- 1 | import { Version } from '../../common/common-types'; 2 | import { parse, versionGte } from '../../common/version'; 3 | import { promisifiedSpawn } from '../childProc'; 4 | 5 | export const getPythonVersion = async (python: string) => { 6 | const version = await promisifiedSpawn(python, ['--version']); 7 | return parse(version); 8 | }; 9 | 10 | export const isSupportedPythonVersion = (version: Version) => versionGte(version, '3.7.0'); 11 | -------------------------------------------------------------------------------- /src/main/systemInfo.ts: -------------------------------------------------------------------------------- 1 | import { cpu, graphics } from 'systeminformation'; 2 | import { lazy } from '../common/util'; 3 | 4 | export const getGpuInfo = lazy(() => graphics()); 5 | export const getCpuInfo = lazy(() => cpu()); 6 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | import { constants } from 'fs'; 2 | import fs from 'fs/promises'; 3 | 4 | export const checkFileExists = (file: string): Promise<boolean> => 5 | fs.access(file, constants.F_OK).then( 6 | () => true, 7 | () => false 8 | ); 9 | -------------------------------------------------------------------------------- /src/public/Info.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>LSMinimumSystemVersion</key> 6 | <string>11.0.0</string> 7 | <key>CFBundleDocumentTypes</key> 8 | <array> 9 | <dict> 10 | <key>CFBundleTypeExtensions</key> 11 | <array> 12 | <string>chn</string> 13 | <string>CHN</string> 14 | </array> 15 | <key>CFBundleTypeIconFile</key> 16 | <string>file_chn.icns</string> 17 | <key>CFBundleTypeName</key> 18 | <string>chaiNNer Chain File</string> 19 | <key>CFBundleTypeRole</key> 20 | <string>Editor</string> 21 | <key>LSHandlerRank</key> 22 | <string>Default</string> 23 | </dict> 24 | </array> 25 | </dict> 26 | </plist> 27 | -------------------------------------------------------------------------------- /src/public/dmg-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/dmg-background.png -------------------------------------------------------------------------------- /src/public/fonts/Noto Emoji/NotoEmoji-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/fonts/Noto Emoji/NotoEmoji-VariableFont_wght.ttf -------------------------------------------------------------------------------- /src/public/fonts/Open Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/fonts/Open Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf -------------------------------------------------------------------------------- /src/public/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf -------------------------------------------------------------------------------- /src/public/fonts/Roboto Mono/RobotoMono-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/fonts/Roboto Mono/RobotoMono-VariableFont_wght.ttf -------------------------------------------------------------------------------- /src/public/icons/cross_platform/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/cross_platform/icon.icns -------------------------------------------------------------------------------- /src/public/icons/cross_platform/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/cross_platform/icon.ico -------------------------------------------------------------------------------- /src/public/icons/cross_platform/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/cross_platform/icon.png -------------------------------------------------------------------------------- /src/public/icons/mac/file_chn.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/mac/file_chn.icns -------------------------------------------------------------------------------- /src/public/icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/mac/icon.icns -------------------------------------------------------------------------------- /src/public/icons/png/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/png/1024x1024.png -------------------------------------------------------------------------------- /src/public/icons/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/png/128x128.png -------------------------------------------------------------------------------- /src/public/icons/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/png/16x16.png -------------------------------------------------------------------------------- /src/public/icons/png/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/png/24x24.png -------------------------------------------------------------------------------- /src/public/icons/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/png/256x256.png -------------------------------------------------------------------------------- /src/public/icons/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/png/32x32.png -------------------------------------------------------------------------------- /src/public/icons/png/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/png/48x48.png -------------------------------------------------------------------------------- /src/public/icons/png/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/png/512x512.png -------------------------------------------------------------------------------- /src/public/icons/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/png/64x64.png -------------------------------------------------------------------------------- /src/public/icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/win/icon.ico -------------------------------------------------------------------------------- /src/public/icons/win/installing_loop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/icons/win/installing_loop.gif -------------------------------------------------------------------------------- /src/public/splash_imgs/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/splash_imgs/background.png -------------------------------------------------------------------------------- /src/public/splash_imgs/front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaiNNer-org/chaiNNer/f76a45de229ae9af8dbcced003f3a18ff10a4c81/src/public/splash_imgs/front.png -------------------------------------------------------------------------------- /src/renderer/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Center, HStack } from '@chakra-ui/react'; 2 | import { memo } from 'react'; 3 | import { DependencyManagerButton } from '../DependencyManagerButton'; 4 | import { NodeDocumentationButton } from '../NodeDocumentation/NodeDocumentationModal'; 5 | import { SettingsButton } from '../SettingsModal'; 6 | import { SystemStats } from '../SystemStats'; 7 | import { AppInfo } from './AppInfo'; 8 | import { ExecutionButtons } from './ExecutionButtons'; 9 | import { KoFiButton } from './KoFiButton'; 10 | 11 | export const Header = memo(() => { 12 | return ( 13 | <Box 14 | alignItems="center" 15 | bg="var(--header-bg)" 16 | borderRadius="lg" 17 | borderWidth="0" 18 | display="flex" 19 | gap={4} 20 | h="56px" 21 | px={2} 22 | w="full" 23 | > 24 | <Box> 25 | <AppInfo /> 26 | </Box> 27 | <Center flexGrow="1"> 28 | <ExecutionButtons /> 29 | </Center> 30 | <HStack> 31 | <SystemStats /> 32 | <NodeDocumentationButton /> 33 | <DependencyManagerButton /> 34 | <KoFiButton /> 35 | <SettingsButton /> 36 | </HStack> 37 | </Box> 38 | ); 39 | }); 40 | -------------------------------------------------------------------------------- /src/renderer/components/Header/KoFiButton.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Tooltip } from '@chakra-ui/react'; 2 | import { memo } from 'react'; 3 | import { SiKofi } from 'react-icons/si'; 4 | import { links } from '../../../common/links'; 5 | import { log } from '../../../common/log'; 6 | import { ipcRenderer } from '../../safeIpc'; 7 | 8 | export const KoFiButton = memo(() => { 9 | return ( 10 | <Tooltip 11 | closeOnClick 12 | closeOnMouseDown 13 | borderRadius={8} 14 | label="Support chaiNNer on Ko-fi" 15 | px={2} 16 | py={1} 17 | > 18 | <IconButton 19 | aria-label="Support chaiNNer" 20 | icon={<SiKofi />} 21 | size="md" 22 | variant="outline" 23 | onClick={() => { 24 | ipcRenderer.invoke('open-url', links.kofi).catch(() => { 25 | log.error('Failed to open Ko-fi url'); 26 | }); 27 | }} 28 | > 29 | Support chaiNNer 30 | </IconButton> 31 | </Tooltip> 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /src/renderer/components/IfVisible.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@chakra-ui/react'; 2 | import { memo } from 'react'; 3 | import { useInView } from 'react-intersection-observer'; 4 | 5 | export interface IfVisibleProps { 6 | height: string | number; 7 | visibleOffset?: number; 8 | forceVisible?: boolean; 9 | children: React.ReactNode; 10 | } 11 | export const IfVisible = memo( 12 | ({ height, visibleOffset = 200, forceVisible = false, children }: IfVisibleProps) => { 13 | const { ref, entry } = useInView({ 14 | rootMargin: `${visibleOffset}px 0px ${visibleOffset}px 0px`, 15 | }); 16 | 17 | const finalVisibility = forceVisible || (entry?.isIntersecting ?? false); 18 | 19 | return ( 20 | <Box 21 | height={typeof height === 'number' ? `${height}px` : height} 22 | ref={ref} 23 | style={{ contain: 'layout size' }} 24 | > 25 | {finalVisibility && ( 26 | <> 27 | <Box display="flex" /> 28 | {children} 29 | <Box display="flex" /> 30 | </> 31 | )} 32 | </Box> 33 | ); 34 | } 35 | ); 36 | -------------------------------------------------------------------------------- /src/renderer/components/NodeDocumentation/DropDownOptions.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { InputOption } from '../../../common/common-types'; 3 | import { TypeTag } from '../TypeTag'; 4 | import { SupportHighlighting } from './HighlightContainer'; 5 | 6 | interface DropDownOptionProps { 7 | option: InputOption; 8 | } 9 | const DropDownOption = memo(({ option }: DropDownOptionProps) => { 10 | return ( 11 | <TypeTag 12 | fontSize="small" 13 | height="auto" 14 | key={option.value} 15 | mt="-0.2rem" 16 | verticalAlign="middle" 17 | > 18 | <SupportHighlighting>{option.option}</SupportHighlighting> 19 | </TypeTag> 20 | ); 21 | }); 22 | 23 | interface DropDownOptionsProps { 24 | options: readonly InputOption[]; 25 | } 26 | export const DropDownOptions = memo(({ options }: DropDownOptionsProps) => { 27 | return ( 28 | <> 29 | {options.map((o) => ( 30 | <DropDownOption 31 | key={o.value} 32 | option={o} 33 | /> 34 | ))} 35 | </> 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /src/renderer/components/NodeDocumentation/SchemaLink.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from '@chakra-ui/react'; 2 | import { memo } from 'react'; 3 | import { useContext } from 'use-context-selector'; 4 | import { NodeSchema } from '../../../common/common-types'; 5 | import { NodeDocumentationContext } from '../../contexts/NodeDocumentationContext'; 6 | 7 | export const SchemaLink = memo(({ schema }: { schema: NodeSchema }) => { 8 | const { openNodeDocumentation } = useContext(NodeDocumentationContext); 9 | 10 | return ( 11 | <Text 12 | _hover={{ 13 | textDecoration: 'underline', 14 | }} 15 | as="i" 16 | backgroundColor="var(--bg-700)" 17 | borderRadius={4} 18 | color="var(--link-color)" 19 | cursor="pointer" 20 | fontWeight="bold" 21 | px={2} 22 | py={1} 23 | userSelect="text" 24 | whiteSpace="nowrap" 25 | onClick={() => openNodeDocumentation(schema.schemaId)} 26 | > 27 | {schema.name} 28 | </Text> 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /src/renderer/components/NodeSelectorPanel/SubcategoryHeading.tsx: -------------------------------------------------------------------------------- 1 | import { Divider, HStack, Text } from '@chakra-ui/react'; 2 | import { memo } from 'react'; 3 | import { NodeGroup } from '../../../common/common-types'; 4 | 5 | interface SubcategoryHeadingProps { 6 | group: NodeGroup; 7 | collapsed?: boolean; 8 | } 9 | 10 | export const SubcategoryHeading = memo(({ group, collapsed = false }: SubcategoryHeadingProps) => { 11 | return ( 12 | <HStack 13 | h={6} 14 | w="full" 15 | > 16 | {collapsed ? ( 17 | <Divider orientation="horizontal" /> 18 | ) : ( 19 | <> 20 | <Divider orientation="horizontal" /> 21 | <Text 22 | casing="uppercase" 23 | color="#71809699" 24 | fontSize="sm" 25 | py={0.5} 26 | whiteSpace="nowrap" 27 | > 28 | {group.name} 29 | </Text> 30 | <Divider orientation="horizontal" /> 31 | </> 32 | )} 33 | </HStack> 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /src/renderer/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { CloseIcon, SearchIcon } from '@chakra-ui/icons'; 2 | import { Input, InputGroup, InputLeftElement, InputRightElement } from '@chakra-ui/react'; 3 | import { ChangeEventHandler, memo } from 'react'; 4 | 5 | interface SearchBarProps { 6 | value: string; 7 | onChange: ChangeEventHandler<HTMLInputElement>; 8 | onClose: () => void; 9 | onClick: () => void; 10 | } 11 | 12 | export const SearchBar = memo(({ value, onChange, onClose, onClick }: SearchBarProps) => ( 13 | <InputGroup borderRadius={0}> 14 | <InputLeftElement 15 | color="var(--fg-300)" 16 | pointerEvents="none" 17 | > 18 | <SearchIcon /> 19 | </InputLeftElement> 20 | <Input 21 | borderRadius={0} 22 | placeholder="Search..." 23 | spellCheck={false} 24 | type="text" 25 | value={value} 26 | variant="filled" 27 | onChange={onChange} 28 | onClick={onClick} 29 | /> 30 | <InputRightElement 31 | _hover={{ color: 'var(--fg-000)' }} 32 | style={{ 33 | color: 'var(--fg-300)', 34 | cursor: 'pointer', 35 | display: value ? undefined : 'none', 36 | fontSize: '66%', 37 | }} 38 | onClick={onClose} 39 | > 40 | <CloseIcon /> 41 | </InputRightElement> 42 | </InputGroup> 43 | )); 44 | -------------------------------------------------------------------------------- /src/renderer/components/groups/RequiredGroup.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useMemo } from 'react'; 2 | import { GenericInput } from '../../../common/common-types'; 3 | import { getUniqueKey } from '../../../common/group-inputs'; 4 | import { getRequireCondition } from '../../../common/nodes/groupStacks'; 5 | import { GroupProps } from './props'; 6 | 7 | export const RequiredGroup = memo( 8 | ({ inputs, nodeState, group, ItemRenderer }: GroupProps<'required'>) => { 9 | const { schema, testCondition } = nodeState; 10 | 11 | const isRequired = useMemo(() => { 12 | const condition = getRequireCondition(schema, group); 13 | return testCondition(condition); 14 | }, [schema, group, testCondition]); 15 | 16 | const requiredInputs: GenericInput[] = useMemo(() => { 17 | return inputs.map((i) => ({ ...i, optional: false })); 18 | }, [inputs]); 19 | 20 | return ( 21 | <> 22 | {(isRequired ? requiredInputs : inputs).map((item) => ( 23 | <ItemRenderer 24 | item={item} 25 | key={getUniqueKey(item)} 26 | nodeState={nodeState} 27 | /> 28 | ))} 29 | </> 30 | ); 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /src/renderer/components/groups/props.ts: -------------------------------------------------------------------------------- 1 | import { Group, GroupKind, OfKind } from '../../../common/common-types'; 2 | import { GroupInputs, InputItem } from '../../../common/group-inputs'; 3 | import { NodeState } from '../../helpers/nodeState'; 4 | 5 | export type InputItemRenderer = (props: { 6 | item: InputItem; 7 | nodeState: NodeState; 8 | }) => JSX.Element | null; 9 | 10 | export interface GroupProps<Kind extends GroupKind> { 11 | group: OfKind<Group, Kind>; 12 | inputs: GroupInputs[Kind]; 13 | nodeState: NodeState; 14 | ItemRenderer: InputItemRenderer; 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/components/groups/util.ts: -------------------------------------------------------------------------------- 1 | import { Input } from '../../../common/common-types'; 2 | import { InputItem } from '../../../common/group-inputs'; 3 | 4 | export const someInput = (item: InputItem, condFn: (input: Input) => boolean): boolean => { 5 | if (item.kind !== 'group') return condFn(item); 6 | return item.inputs.some((i) => someInput(i, condFn)); 7 | }; 8 | -------------------------------------------------------------------------------- /src/renderer/components/inputs/elements/AdvancedNumberInput.scss: -------------------------------------------------------------------------------- 1 | .small-stepper svg { 2 | height: 0.75em; 3 | } 4 | -------------------------------------------------------------------------------- /src/renderer/components/inputs/elements/Checkbox.scss: -------------------------------------------------------------------------------- 1 | @use '../../../colors.scss'; 2 | 3 | .chainner-node-checkbox > .chakra-checkbox__control { 4 | transition-property: border-color; 5 | transition-duration: var(--chakra-transition-duration-normal); 6 | 7 | /* stylelint-disable-next-line no-duplicate-selectors */ 8 | & { 9 | border-color: inherit; 10 | background: var(--bg-700); 11 | 12 | &:hover { 13 | /* stylelint-disable-next-line custom-property-pattern */ 14 | border-color: var(--chakra-colors-whiteAlpha-400); 15 | background: var(--bg-700); 16 | } 17 | } 18 | 19 | &[data-checked] { 20 | border-color: rgba(255 255 255/50%); 21 | background: var(--bg-700); 22 | 23 | &:hover { 24 | border-color: rgba(255 255 255/50%); 25 | background: var(--bg-700); 26 | } 27 | } 28 | 29 | & svg { 30 | color: var(--chakra-colors-chakra-body-text); 31 | width: 1em; 32 | cursor: pointer; 33 | overflow: visible; 34 | stroke-width: 2.75 !important; 35 | 36 | polyline { 37 | cursor: pointer; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/components/inputs/elements/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Checkbox as ChakraCheckbox } from '@chakra-ui/react'; 2 | import { ReactNode, memo } from 'react'; 3 | import { DropDownInput, InputSchemaValue } from '../../../../common/common-types'; 4 | import './Checkbox.scss'; 5 | 6 | type ArrayItem<T> = T extends readonly (infer I)[] ? I : never; 7 | type Option = ArrayItem<DropDownInput['options']>; 8 | 9 | export interface CheckboxProps { 10 | value: InputSchemaValue; 11 | onChange: (value: InputSchemaValue) => void; 12 | isDisabled?: boolean; 13 | yes: Option; 14 | no: Option; 15 | label: string; 16 | afterText?: ReactNode; 17 | } 18 | 19 | export const Checkbox = memo( 20 | ({ value, onChange, isDisabled, yes, no, label, afterText }: CheckboxProps) => { 21 | return ( 22 | <ChakraCheckbox 23 | className="chainner-node-checkbox" 24 | colorScheme="gray" 25 | isChecked={value === yes.value} 26 | isDisabled={isDisabled} 27 | onChange={(e) => { 28 | const selected = e.target.checked ? yes : no; 29 | onChange(selected.value); 30 | }} 31 | > 32 | <Box 33 | as="span" 34 | fontSize="14px" 35 | verticalAlign="text-top" 36 | > 37 | {label} 38 | </Box> 39 | {afterText} 40 | </ChakraCheckbox> 41 | ); 42 | } 43 | ); 44 | -------------------------------------------------------------------------------- /src/renderer/components/inputs/elements/ColorKindSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonGroup } from '@chakra-ui/react'; 2 | import { memo, useMemo } from 'react'; 3 | import { ColorKind } from '../../../../common/common-types'; 4 | 5 | const KIND_ORDER: readonly ColorKind[] = ['grayscale', 'rgb', 'rgba']; 6 | const KIND_LABEL: Readonly<Record<ColorKind, string>> = { 7 | grayscale: 'Gray', 8 | rgb: 'RGB', 9 | rgba: 'RGBA', 10 | }; 11 | 12 | interface ColorKindSelectorProps { 13 | kinds: ReadonlySet<ColorKind>; 14 | current: ColorKind; 15 | onSelect: (kind: ColorKind) => void; 16 | } 17 | export const ColorKindSelector = memo(({ kinds, current, onSelect }: ColorKindSelectorProps) => { 18 | const kindArray = useMemo(() => { 19 | return [...kinds].sort((a, b) => KIND_ORDER.indexOf(a) - KIND_ORDER.indexOf(b)); 20 | }, [kinds]); 21 | 22 | return ( 23 | <ButtonGroup 24 | isAttached 25 | size="sm" 26 | w="full" 27 | > 28 | {kindArray.map((k) => { 29 | return ( 30 | <Button 31 | borderRadius="lg" 32 | key={k} 33 | variant={current === k ? 'solid' : 'ghost'} 34 | w="full" 35 | onClick={() => onSelect(k)} 36 | > 37 | {KIND_LABEL[k]} 38 | </Button> 39 | ); 40 | })} 41 | </ButtonGroup> 42 | ); 43 | }); 44 | -------------------------------------------------------------------------------- /src/renderer/components/inputs/props.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@chainner/navi'; 2 | import { 3 | Condition, 4 | Input, 5 | InputKind, 6 | OfKind, 7 | PartialBy, 8 | SchemaId, 9 | Size, 10 | } from '../../../common/common-types'; 11 | 12 | export interface InputProps<Kind extends InputKind, Value extends string | number = never> { 13 | readonly value: Value | undefined; 14 | readonly setValue: (input: Value) => void; 15 | readonly resetValue: () => void; 16 | readonly input: Omit<PartialBy<OfKind<Input, Kind>, 'id'>, 'type' | 'conversion'>; 17 | readonly definitionType: Type; 18 | readonly isLocked: boolean; 19 | readonly isConnected: boolean; 20 | readonly inputKey: string; 21 | readonly size: Readonly<Size> | undefined; 22 | readonly setSize: (size: Readonly<Size>) => void; 23 | readonly inputType: Type; 24 | readonly nodeId?: string; 25 | readonly nodeSchemaId?: SchemaId; 26 | readonly testCondition: (condition: Condition) => boolean; 27 | readonly sequenceType?: Type; 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/components/outputs/TaggedOutput.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { ModelDataTags } from './elements/ModelDataTags'; 3 | import { OutputProps } from './props'; 4 | 5 | interface TagData { 6 | tags?: readonly string[] | null; 7 | } 8 | 9 | export const TaggedOutput = memo(({ output, useOutputData, animated }: OutputProps) => { 10 | const { current } = useOutputData<TagData>(output.id); 11 | 12 | return ( 13 | <ModelDataTags 14 | loading={animated} 15 | tags={current?.tags || undefined} 16 | /> 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /src/renderer/components/outputs/props.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@chainner/navi'; 2 | import { NodeSchema, Output, OutputId, Size } from '../../../common/common-types'; 3 | 4 | export interface UseOutputData<T> { 5 | /** The current output data. Current here means most recent + up to date (= same input hash). */ 6 | readonly current: T | undefined; 7 | /** The most recent output data. */ 8 | readonly last: T | undefined; 9 | /** Whether the most recent output data ({@link last}) is not the current output data ({@link current}). */ 10 | readonly stale: boolean; 11 | } 12 | 13 | export interface OutputProps { 14 | readonly output: Output; 15 | readonly id: string; 16 | readonly schema: NodeSchema; 17 | readonly definitionType: Type; 18 | readonly type: Type; 19 | readonly useOutputData: <T>(outputId: OutputId) => UseOutputData<T>; 20 | readonly animated: boolean; 21 | readonly size: Readonly<Size> | undefined; 22 | readonly setSize: (size: Readonly<Size>) => void; 23 | readonly sequenceType?: Type; 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/components/settings/SettingContainer.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, HStack, Text, VStack } from '@chakra-ui/react'; 2 | import { PropsWithChildren, memo } from 'react'; 3 | 4 | interface ContainerProps { 5 | title: string; 6 | description: string; 7 | } 8 | 9 | export const SettingContainer = memo( 10 | ({ title, description, children }: PropsWithChildren<ContainerProps>) => { 11 | return ( 12 | <Flex 13 | align="center" 14 | w="full" 15 | > 16 | <VStack 17 | alignContent="left" 18 | alignItems="left" 19 | w="full" 20 | > 21 | <Text 22 | flex="1" 23 | textAlign="left" 24 | > 25 | {title} 26 | </Text> 27 | <Text 28 | flex="1" 29 | fontSize="xs" 30 | marginTop={0} 31 | textAlign="left" 32 | > 33 | {description} 34 | </Text> 35 | </VStack> 36 | <HStack>{children}</HStack> 37 | </Flex> 38 | ); 39 | } 40 | ); 41 | -------------------------------------------------------------------------------- /src/renderer/components/settings/SettingItem.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useEffect } from 'react'; 2 | import { Setting, SettingValue } from '../../../common/common-types'; 3 | import { SettingComponents } from './components'; 4 | import { SettingsProps } from './props'; 5 | 6 | interface SettingItemProps { 7 | setting: Setting; 8 | value: SettingValue | undefined; 9 | setValue: (value: SettingValue) => void; 10 | } 11 | 12 | export const SettingItem = memo(({ setting, value, setValue }: SettingItemProps) => { 13 | const settingIsUnset = value === undefined; 14 | useEffect(() => { 15 | if (settingIsUnset) { 16 | setValue(setting.default); 17 | } 18 | }, [setting, settingIsUnset, setValue]); 19 | 20 | if (value === undefined) { 21 | return null; 22 | } 23 | 24 | const Component = SettingComponents[setting.type] as ( 25 | props: SettingsProps<Setting['type']> 26 | ) => JSX.Element; 27 | 28 | return ( 29 | <Component 30 | setValue={setValue} 31 | setting={setting} 32 | value={value} 33 | /> 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /src/renderer/components/settings/props.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from '../../../common/common-types'; 2 | 3 | type OfType<S, Type extends string> = S extends { type: Type } ? S : never; 4 | 5 | export interface SettingsProps<T extends Setting['type']> { 6 | setting: Omit<OfType<Setting, T>, 'default' | 'type' | 'key'>; 7 | value: OfType<Setting, T>['default']; 8 | setValue: (value: OfType<Setting, T>['default']) => void; 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/contexts/CollapsedNodeContext.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@chakra-ui/react'; 2 | import React, { memo } from 'react'; 3 | import { createContext } from 'use-context-selector'; 4 | 5 | export const IsCollapsedContext = createContext<boolean>(false); 6 | 7 | export const CollapsedNode = memo(({ children }: React.PropsWithChildren<unknown>) => { 8 | return ( 9 | <Box 10 | display="none" 11 | style={{ contain: 'strict' }} 12 | > 13 | <IsCollapsedContext.Provider value>{children}</IsCollapsedContext.Provider> 14 | </Box> 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /src/renderer/contexts/FakeExampleContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { createContext } from 'use-context-selector'; 3 | import { useMemoObject } from '../hooks/useMemo'; 4 | 5 | interface FakeNodeContextState { 6 | isFake: boolean; 7 | } 8 | 9 | export const FakeNodeContext = createContext<Readonly<FakeNodeContextState>>({ 10 | isFake: false, 11 | }); 12 | 13 | export const FakeNodeProvider = memo( 14 | ({ children, isFake }: React.PropsWithChildren<FakeNodeContextState>) => { 15 | const value = useMemoObject<FakeNodeContextState>({ isFake }); 16 | 17 | return <FakeNodeContext.Provider value={value}>{children}</FakeNodeContext.Provider>; 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /src/renderer/contexts/HotKeyContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useState } from 'react'; 2 | import { createContext } from 'use-context-selector'; 3 | import { noop } from '../../common/util'; 4 | import { useMemoObject } from '../hooks/useMemo'; 5 | import { ipcRenderer } from '../safeIpc'; 6 | 7 | interface HotkeysContextState { 8 | hotkeysEnabled: boolean; 9 | setHotkeysEnabled: (value: boolean) => void; 10 | } 11 | 12 | export const HotkeysContext = createContext<Readonly<HotkeysContextState>>({ 13 | hotkeysEnabled: true, 14 | setHotkeysEnabled: noop, 15 | }); 16 | 17 | export const HotkeysProvider = memo(({ children }: React.PropsWithChildren<unknown>) => { 18 | const [enabled, setEnabled] = useState(true); 19 | 20 | const setHotkeysEnabled = useCallback( 21 | (value: boolean) => { 22 | setEnabled(value); 23 | ipcRenderer.send(value ? 'enable-menu' : 'disable-menu'); 24 | }, 25 | [setEnabled] 26 | ); 27 | 28 | const value = useMemoObject<HotkeysContextState>({ 29 | hotkeysEnabled: enabled, 30 | setHotkeysEnabled, 31 | }); 32 | 33 | return <HotkeysContext.Provider value={value}>{children}</HotkeysContext.Provider>; 34 | }); 35 | -------------------------------------------------------------------------------- /src/renderer/contexts/InputContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { createContext } from 'use-context-selector'; 3 | import { useMemoObject } from '../hooks/useMemo'; 4 | 5 | interface InputContextState { 6 | /** 7 | * Whether the input is inactive (unused) due to some other input value. 8 | * 9 | * Such inactive inputs are usually not rendered, but they have to be if 10 | * they have a connection. 11 | */ 12 | conditionallyInactive: boolean; 13 | } 14 | 15 | export const InputContext = createContext<Readonly<InputContextState>>({ 16 | conditionallyInactive: false, 17 | }); 18 | 19 | export const WithInputContext = memo( 20 | ({ conditionallyInactive, children }: React.PropsWithChildren<InputContextState>) => { 21 | const value = useMemoObject<InputContextState>({ 22 | conditionallyInactive, 23 | }); 24 | 25 | return <InputContext.Provider value={value}>{children}</InputContext.Provider>; 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /src/renderer/env.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export const getCacheLocation = (userDataPath: string, cacheKey: string) => { 4 | return path.join(userDataPath, '/cache/', cacheKey); 5 | }; 6 | -------------------------------------------------------------------------------- /src/renderer/helpers/colorTools.ts: -------------------------------------------------------------------------------- 1 | import { Color } from './color'; 2 | 3 | /** 4 | * Lightens (percentage > 0) or darkens (percentage < 0) the given hex color. 5 | */ 6 | export const shadeColor = (color: string, percent: number) => { 7 | return Color.fromHex(color) 8 | .channelWise((c) => Math.min(Math.round(c * (1 + percent / 100)), 255)) 9 | .hex(); 10 | }; 11 | 12 | export const interpolateColor = (color1: string, color2: string, factor = 0.5) => 13 | Color.fromHex(color1).lerpLinear(Color.fromHex(color2), factor).hex(); 14 | 15 | export const createConicGradient = (colors: readonly string[]): string => { 16 | if (colors.length === 1) return colors[0]; 17 | 18 | const handleColorString = colors 19 | .map((color, index) => { 20 | const percent = index / colors.length; 21 | const nextPercent = (index + 1) / colors.length; 22 | return `${color} ${percent * 100}% ${nextPercent * 100}%`; 23 | }) 24 | .join(', '); 25 | return `conic-gradient(from 90deg, ${handleColorString})`; 26 | }; 27 | -------------------------------------------------------------------------------- /src/renderer/helpers/naviHelpers.ts: -------------------------------------------------------------------------------- 1 | import { FunctionCallExpression, Type, evaluate } from '@chainner/navi'; 2 | import { getChainnerScope } from '../../common/types/chainner-scope'; 3 | 4 | export const typeToString = (type: Type): Type => { 5 | return evaluate(new FunctionCallExpression('toString', [type]), getChainnerScope()); 6 | }; 7 | -------------------------------------------------------------------------------- /src/renderer/helpers/types.ts: -------------------------------------------------------------------------------- 1 | export type SetState<T> = React.Dispatch<React.SetStateAction<T>>; 2 | export type GetSetState<T> = readonly [T, SetState<T>]; 3 | -------------------------------------------------------------------------------- /src/renderer/hooks/useAutomaticFeatures.ts: -------------------------------------------------------------------------------- 1 | import { getIncomers, useReactFlow } from 'reactflow'; 2 | import { useContext } from 'use-context-selector'; 3 | import { EdgeData, NodeData, SchemaId } from '../../common/common-types'; 4 | import { BackendContext } from '../contexts/BackendContext'; 5 | 6 | /** 7 | * Determines whether a node should use automatic ahead-of-time features, such as individually running the node or determining certain type features automatically. 8 | */ 9 | export const useAutomaticFeatures = (id: string, schemaId: SchemaId) => { 10 | const { schemata } = useContext(BackendContext); 11 | const schema = schemata.get(schemaId); 12 | 13 | const { getEdges, getNodes, getNode } = useReactFlow<NodeData, EdgeData>(); 14 | const thisNode = getNode(id); 15 | 16 | // A node should not use automatic features if it has incoming connections 17 | const hasIncomingConnections = 18 | thisNode && getIncomers(thisNode, getNodes(), getEdges()).length > 0; 19 | 20 | // Same if it has any static input values 21 | const hasStaticValueInput = schema.inputs.some((i) => i.kind === 'static'); 22 | // We should only use automatic features if the node has side effects 23 | const { hasSideEffects } = schema; 24 | 25 | return { 26 | isAutomatic: hasSideEffects && !hasIncomingConnections && !hasStaticValueInput, 27 | hasIncomingConnections, 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/renderer/hooks/useChangeCounter.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef, useState } from 'react'; 2 | import { SetState } from '../helpers/types'; 3 | 4 | export type ChangeCounter = number & { __changeCounter: true }; 5 | 6 | export const nextChangeCount = (count: number): number => (count + 1) % 1_000_000; 7 | 8 | export const useChangeCounter = () => { 9 | const [counter, setCounter] = useState(0); 10 | const counterRef = useRef(0); 11 | 12 | const change = useCallback(() => { 13 | // we have to wrap at some point, so I just arbitrarily chose 1 million 14 | setCounter((prev) => { 15 | const newValue = nextChangeCount(prev); 16 | counterRef.current = newValue; 17 | return newValue; 18 | }); 19 | }, [setCounter]); 20 | 21 | return [counter as ChangeCounter, change, counterRef] as const; 22 | }; 23 | 24 | export const wrapChanges = <T>(setter: SetState<T>, addChange: () => void): SetState<T> => { 25 | return (value) => { 26 | setter(value); 27 | addChange(); 28 | }; 29 | }; 30 | export const wrapRefChanges = <T>( 31 | setter: Readonly<React.MutableRefObject<SetState<T>>>, 32 | addChange: () => void 33 | ): SetState<T> => { 34 | return (value) => { 35 | setter.current(value); 36 | addChange(); 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/renderer/hooks/useDevicePixelRatio.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useDevicePixelRatio = (): number => { 4 | const [value, setValue] = useState(window.devicePixelRatio); 5 | 6 | useEffect(() => { 7 | const update = () => setValue(window.devicePixelRatio); 8 | const mediaMatcher = window.matchMedia(`screen and (resolution: ${value}dppx)`); 9 | mediaMatcher.addEventListener('change', update); 10 | 11 | return () => { 12 | mediaMatcher.removeEventListener('change', update); 13 | }; 14 | }, [value]); 15 | 16 | return value; 17 | }; 18 | -------------------------------------------------------------------------------- /src/renderer/hooks/useEventBacklog.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useRef } from 'react'; 2 | 3 | export interface EventBacklog<T> { 4 | readonly push: (event: T) => void; 5 | readonly processAll: () => void; 6 | } 7 | 8 | export interface BacklogOption<T> { 9 | process: (event: T[]) => void; 10 | interval: number; 11 | } 12 | 13 | export const useEventBacklog = <T>({ process, interval }: BacklogOption<T>): EventBacklog<T> => { 14 | const backlogRef = useRef<T[]>([]); 15 | const push = useCallback((event: T): void => { 16 | backlogRef.current.push(event); 17 | }, []); 18 | 19 | const processRef = useRef(process); 20 | useEffect(() => { 21 | processRef.current = process; 22 | }, [process]); 23 | 24 | const processAll = useCallback(() => { 25 | if (backlogRef.current.length > 0) { 26 | const backlog = backlogRef.current; 27 | backlogRef.current = []; 28 | processRef.current(backlog); 29 | } 30 | }, []); 31 | 32 | useEffect(() => { 33 | const timeout = setInterval(processAll, interval); 34 | return () => clearInterval(timeout); 35 | }, [processAll, interval]); 36 | 37 | return useMemo(() => ({ push, processAll }), [push, processAll]); 38 | }; 39 | -------------------------------------------------------------------------------- /src/renderer/hooks/useHotkeys.ts: -------------------------------------------------------------------------------- 1 | import { useHotkeys as useHotkeysImpl } from 'react-hotkeys-hook'; 2 | import { useContext } from 'use-context-selector'; 3 | import { noop } from '../../common/util'; 4 | import { HotkeysContext } from '../contexts/HotKeyContext'; 5 | 6 | export const useHotkeys = (keys: string, callback: () => void): void => { 7 | const { hotkeysEnabled } = useContext(HotkeysContext); 8 | 9 | const fn = hotkeysEnabled ? callback : noop; 10 | 11 | useHotkeysImpl(keys, fn, [fn]); 12 | }; 13 | -------------------------------------------------------------------------------- /src/renderer/hooks/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | /** 4 | * Executes the given effect indefinitely every `delay` ms. 5 | * 6 | * The interval gets reset every time the callback changes. 7 | */ 8 | export const useInterval = (callback: () => void, delay: number) => { 9 | useEffect(() => { 10 | const id = setInterval(callback, delay); 11 | return () => clearInterval(id); 12 | }, [delay, callback]); 13 | }; 14 | -------------------------------------------------------------------------------- /src/renderer/hooks/useIpcRendererListener.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { ChannelArgs, SendChannels } from '../../common/safeIpc'; 3 | import { ipcRenderer } from '../safeIpc'; 4 | // eslint-disable-next-line import/no-nodejs-modules 5 | import type { IpcRendererEvent } from 'electron/renderer'; 6 | 7 | export const useIpcRendererListener = <C extends keyof SendChannels>( 8 | channel: C, 9 | listener: (event: IpcRendererEvent, ...args: ChannelArgs<C>) => void 10 | ) => { 11 | useEffect(() => { 12 | ipcRenderer.on(channel, listener); 13 | return () => { 14 | ipcRenderer.removeListener(channel, listener); 15 | }; 16 | }, [channel, listener]); 17 | }; 18 | -------------------------------------------------------------------------------- /src/renderer/hooks/useIsCollapsedNode.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'use-context-selector'; 2 | import { IsCollapsedContext } from '../contexts/CollapsedNodeContext'; 3 | 4 | export const useIsCollapsedNode = (): boolean => { 5 | return useContext(IsCollapsedContext); 6 | }; 7 | -------------------------------------------------------------------------------- /src/renderer/hooks/useLastDirectory.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { EMPTY_OBJECT } from '../../common/util'; 3 | import { useStored } from './useStored'; 4 | 5 | export interface UseLastDirectory { 6 | readonly lastDirectory: string | undefined; 7 | readonly setLastDirectory: (dir: string) => void; 8 | } 9 | 10 | export const useLastDirectory = (key: string): UseLastDirectory => { 11 | const [lastDirectories, setLastDirectories] = useStored<Record<string, string | undefined>>( 12 | 'lastDirectories', 13 | EMPTY_OBJECT 14 | ); 15 | 16 | const setLastDirectory = useCallback( 17 | (dir: string) => { 18 | setLastDirectories((prev) => ({ ...prev, [key]: dir })); 19 | }, 20 | [setLastDirectories, key] 21 | ); 22 | 23 | return { 24 | lastDirectory: lastDirectories[key], 25 | setLastDirectory, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/renderer/hooks/useLastWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react'; 2 | import { debounce } from '../../common/util'; 3 | import { useIpcRendererListener } from './useIpcRendererListener'; 4 | import { useMutSetting } from './useSettings'; 5 | 6 | export const useLastWindowSize = () => { 7 | const [, setSize] = useMutSetting('lastWindowSize'); 8 | 9 | useEffect(() => { 10 | const listener = debounce(() => { 11 | setSize((prev) => { 12 | if (prev.maximized) return prev; 13 | return { 14 | maximized: false, 15 | width: window.outerWidth, 16 | height: window.outerHeight, 17 | }; 18 | }); 19 | }, 100); 20 | 21 | window.addEventListener('resize', listener); 22 | return () => window.removeEventListener('resize', listener); 23 | }, [setSize]); 24 | 25 | useIpcRendererListener( 26 | 'window-maximized-change', 27 | useCallback( 28 | (_, maximized) => { 29 | setSize((prev) => ({ ...prev, maximized })); 30 | }, 31 | [setSize] 32 | ) 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/renderer/hooks/useMemo.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | /** 4 | * A `useMemo` variant that compares the array items for reference equality. 5 | */ 6 | export const useMemoArray = <T extends readonly unknown[]>(array: T): T => 7 | // eslint-disable-next-line react-hooks/exhaustive-deps 8 | useMemo(() => array, array); 9 | 10 | /** 11 | * A `useMemo` variant that compares the object values (using `Object.values`) for reference 12 | * equality. 13 | */ 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | export const useMemoObject = <T extends Readonly<Record<string | number, any>>>( 16 | obj: T 17 | // eslint-disable-next-line react-hooks/exhaustive-deps 18 | ): Readonly<T> => useMemo(() => obj, Object.values(obj)); 19 | -------------------------------------------------------------------------------- /src/renderer/hooks/useNodeMenu.scss: -------------------------------------------------------------------------------- 1 | .useNodeMenu-child { 2 | display: none; 3 | } 4 | 5 | .useNodeMenu-container:hover + .useNodeMenu-child { 6 | display: block; 7 | } 8 | 9 | .useNodeMenu-child:hover { 10 | display: block; 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export const usePrevious = <T>(value: T): T | undefined => { 4 | const ref = useRef<T>(); 5 | useEffect(() => { 6 | ref.current = value; 7 | }); 8 | return ref.current; 9 | }; 10 | -------------------------------------------------------------------------------- /src/renderer/hooks/useSessionStorage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const getSessionStorageOrDefault = <T>(key: string, defaultValue: T): T => { 4 | const stored = sessionStorage.getItem(key); 5 | if (!stored) { 6 | return defaultValue; 7 | } 8 | return JSON.parse(stored) as T; 9 | }; 10 | 11 | export const useSessionStorage = <T>(key: string, defaultValue: T) => { 12 | const [value, setValue] = useState(() => getSessionStorageOrDefault(key, defaultValue)); 13 | 14 | useEffect(() => { 15 | sessionStorage.setItem(key, JSON.stringify(value)); 16 | }, [key, value]); 17 | 18 | return [value, setValue] as const; 19 | }; 20 | -------------------------------------------------------------------------------- /src/renderer/hooks/useSettings.ts: -------------------------------------------------------------------------------- 1 | import { SetStateAction, useCallback } from 'react'; 2 | import { useContext } from 'use-context-selector'; 3 | import { ChainnerSettings } from '../../common/settings/settings'; 4 | import { SettingsContext } from '../contexts/SettingsContext'; 5 | 6 | export const useSettings = () => { 7 | return useContext(SettingsContext).settings; 8 | }; 9 | 10 | export const useMutSetting = <K extends keyof ChainnerSettings>(key: K) => { 11 | const { settings, setSetting } = useContext(SettingsContext); 12 | 13 | const set = useCallback( 14 | (update: SetStateAction<ChainnerSettings[K]>) => { 15 | setSetting(key, update); 16 | }, 17 | [key, setSetting] 18 | ); 19 | 20 | return [settings[key], set] as const; 21 | }; 22 | -------------------------------------------------------------------------------- /src/renderer/hooks/useStored.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useCallback } from 'react'; 2 | import { useContext } from 'use-context-selector'; 3 | import { SettingsContext } from '../contexts/SettingsContext'; 4 | 5 | const getStored = <T>(storage: Record<string, unknown>, key: string, defaultValue: T): T => { 6 | return (storage[key] as T | undefined) ?? defaultValue; 7 | }; 8 | 9 | export const useStored = <T>( 10 | key: string, 11 | defaultValue: T 12 | ): readonly [T, Dispatch<SetStateAction<T>>] => { 13 | const { settings, setSetting } = useContext(SettingsContext); 14 | 15 | const setValue = useCallback( 16 | (value: SetStateAction<T>) => { 17 | setSetting('storage', (prev) => { 18 | const newValue = 19 | typeof value === 'function' 20 | ? (value as (prev: T) => T)(getStored(prev, key, defaultValue)) 21 | : value; 22 | return { ...prev, [key]: newValue }; 23 | }); 24 | }, 25 | [setSetting, key, defaultValue] 26 | ); 27 | 28 | return [getStored(settings.storage, key, defaultValue), setValue] as const; 29 | }; 30 | -------------------------------------------------------------------------------- /src/renderer/hooks/useThemeColor.ts: -------------------------------------------------------------------------------- 1 | import { useColorMode } from '@chakra-ui/react'; 2 | import { useMemo } from 'react'; 3 | import { lazy } from '../../common/util'; 4 | import { useSettings } from './useSettings'; 5 | 6 | const light = lazy(() => getComputedStyle(document.documentElement)); 7 | const dark = lazy(() => getComputedStyle(document.documentElement)); 8 | 9 | export const useThemeColor = (name: `--${string}`): string => { 10 | const { colorMode } = useColorMode(); 11 | const { theme } = useSettings(); 12 | return useMemo(() => { 13 | const styles = colorMode === 'dark' ? dark() : light(); 14 | return styles.getPropertyValue(name).trim(); 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | }, [colorMode, name, theme]); 17 | }; 18 | -------------------------------------------------------------------------------- /src/renderer/hooks/useTypeColor.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@chainner/navi'; 2 | import { useMemo } from 'react'; 3 | import { getTypeAccentColors } from '../helpers/accentColors'; 4 | import { useSettings } from './useSettings'; 5 | 6 | export const useTypeColor = (type: Type) => { 7 | const { theme } = useSettings(); 8 | return useMemo(() => getTypeAccentColors(type, theme), [type, theme]); 9 | }; 10 | -------------------------------------------------------------------------------- /src/renderer/hooks/useValidDropDownValue.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { DropDownInput, InputSchemaValue } from '../../common/common-types'; 3 | 4 | export const useValidDropDownValue = ( 5 | value: InputSchemaValue | undefined, 6 | setValue: (value: InputSchemaValue) => void, 7 | input: Pick<DropDownInput, 'options' | 'def'> 8 | ) => { 9 | let valid = value ?? input.def; 10 | if (input.options.every((o) => o.value !== valid)) { 11 | valid = input.def; 12 | } 13 | 14 | // reset to valid value 15 | const resetTo = valid !== value ? valid : undefined; 16 | useEffect(() => { 17 | if (resetTo !== undefined) { 18 | setValue(resetTo); 19 | } 20 | }, [resetTo, setValue]); 21 | 22 | return valid; 23 | }; 24 | -------------------------------------------------------------------------------- /src/renderer/hooks/useWatchFiles.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react'; 2 | import { log } from '../../common/log'; 3 | import { ipcRenderer } from '../safeIpc'; 4 | // eslint-disable-next-line import/no-nodejs-modules 5 | import type { IpcRendererEvent } from 'electron/renderer'; 6 | 7 | export const useWatchFiles = (files: readonly string[], onChange: () => void): void => { 8 | const cb = useCallback( 9 | (event: IpcRendererEvent, eventType: 'add' | 'change' | 'unlink', path: string) => { 10 | if (files.includes(path)) { 11 | onChange(); 12 | } 13 | }, 14 | [files, onChange] 15 | ); 16 | 17 | useEffect(() => { 18 | if (files.length === 0) return; 19 | 20 | ipcRenderer.invoke('watch-files', files).catch(log.error); 21 | ipcRenderer.on('file-changed', cb); 22 | 23 | return () => { 24 | ipcRenderer.invoke('unwatch-files', files).catch(log.error); 25 | ipcRenderer.removeListener('file-changed', cb); 26 | }; 27 | }, [cb, files]); 28 | }; 29 | -------------------------------------------------------------------------------- /src/renderer/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import { DEFAULT_OPTIONS } from '../common/i18n'; 4 | import { log } from '../common/log'; 5 | 6 | i18n.use(initReactI18next).init(DEFAULT_OPTIONS).catch(log.error); 7 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import electronLog from 'electron-log/renderer'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { QueryClient, QueryClientProvider } from 'react-query'; 4 | import { LEVEL_NAME, log } from '../common/log'; 5 | import { App } from './app'; 6 | 7 | electronLog.transports.ipc.level = 'info'; 8 | electronLog.transports.console.level = 'debug'; 9 | log.addTransport({ 10 | log: ({ level, message, additional }) => { 11 | electronLog[LEVEL_NAME[level]](message, ...additional); 12 | }, 13 | }); 14 | 15 | const queryClient = new QueryClient(); 16 | 17 | const container = document.getElementById('root'); 18 | const root = createRoot(container!); 19 | root.render( 20 | <QueryClientProvider client={queryClient}> 21 | <App /> 22 | </QueryClientProvider> 23 | ); 24 | -------------------------------------------------------------------------------- /src/renderer/renderer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file will automatically be loaded by webpack and run in the "renderer" context. 3 | * To learn more about the differences between the "main" and the "renderer" context in 4 | * Electron, visit: 5 | * 6 | * https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 7 | * 8 | * By default, Node.js integration in this file is disabled. When enabling Node.js integration 9 | * in a renderer process, please be aware of potential security implications. You can read 10 | * more about security risks here: 11 | * 12 | * https://electronjs.org/docs/tutorial/security 13 | * 14 | * To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration` 15 | * flag: 16 | * 17 | * ``` 18 | * // Create the browser window. 19 | * mainWindow = new BrowserWindow({ 20 | * width: 800, 21 | * height: 600, 22 | * webPreferences: { 23 | * nodeIntegration: true 24 | * } 25 | * }); 26 | * ``` 27 | */ 28 | 29 | import './global.scss'; 30 | import './index'; 31 | -------------------------------------------------------------------------------- /src/renderer/theme.ts: -------------------------------------------------------------------------------- 1 | import { Theme, extendTheme } from '@chakra-ui/react'; 2 | 3 | // This is the initial theme config on startup 4 | const dark = { 5 | initialColorMode: 'dark', 6 | useSystemColorMode: false, 7 | } as const; 8 | 9 | // TODO: This should be used later after reading the theme settings. 10 | // When light, change the theme to this values before displaying the 11 | // window. Need to figure out where and when to load the new theme. 12 | // Currently this does nothing. 13 | const light = { 14 | initialColorMode: 'light', 15 | useSystemColorMode: false, 16 | } as const; 17 | 18 | // TODO: This should be used later to dynamically change the theme, 19 | // when the OS changes from dark to light or vice versa. Need to 20 | // figure out where and when to load the new theme. Currently this 21 | // does nothing. 22 | 23 | const system = { 24 | initialColorMode: 'system', 25 | useSystemColorMode: true, 26 | } as const; 27 | 28 | const grays = [50, 100, 200, 300, 400, 500, 600, 650, 700, 750, 800, 850, 900]; 29 | const colors = { 30 | gray: Object.fromEntries(grays.map((v) => [v, `var(--theme-${v})`])), 31 | }; 32 | 33 | const fonts = { 34 | heading: `Open Sans, sans-serif`, 35 | body: `Open Sans, sans-serif`, 36 | monospace: `Roboto-Mono, monospace`, 37 | }; 38 | 39 | export const darktheme = extendTheme({ config: dark, colors, fonts } as const) as Theme; 40 | 41 | export const lighttheme = extendTheme({ config: light, colors, fonts } as const) as Theme; 42 | 43 | export const systemtheme = extendTheme({ config: system, colors, fonts } as const) as Theme; 44 | -------------------------------------------------------------------------------- /tests/common/SaveFile.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { expect, test } from 'vitest'; 4 | import { RawSaveFile, SaveFile } from '../../src/main/SaveFile'; 5 | 6 | const dataDir = path.join(__dirname, '..', 'data'); 7 | 8 | for (const file of fs.readdirSync(dataDir)) { 9 | const filePath = path.join(dataDir, file); 10 | 11 | test(`Read save file ${file}`, async () => { 12 | const parsed = await SaveFile.read(filePath); 13 | expect(parsed).toMatchSnapshot(); 14 | }); 15 | test(`Write save file ${file}`, async () => { 16 | const json = SaveFile.stringify(await SaveFile.read(filePath), '0.0.0-test'); 17 | const obj = JSON.parse(json) as RawSaveFile; 18 | delete obj.migration; 19 | delete obj.timestamp; 20 | expect(obj).toMatchSnapshot(); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /tests/common/chainner-scope.test.ts: -------------------------------------------------------------------------------- 1 | import { FunctionCallExpression, NamedExpression, evaluate } from '@chainner/navi'; 2 | import { test } from 'vitest'; 3 | import { getChainnerScope } from '../../src/common/types/chainner-scope'; 4 | import { assertNever } from '../../src/common/util'; 5 | 6 | test(`Chainner scope is correct`, () => { 7 | const scope = getChainnerScope(); 8 | 9 | for (const [name, def] of scope.entries()) { 10 | switch (def.type) { 11 | case 'parameter': 12 | break; 13 | case 'variable': 14 | case 'struct': 15 | evaluate(new NamedExpression(name), scope); 16 | break; 17 | case 'intrinsic-function': 18 | case 'function': 19 | evaluate( 20 | new FunctionCallExpression( 21 | name, 22 | def.definition.parameters.map((p) => p.type) 23 | ), 24 | scope 25 | ); 26 | break; 27 | default: 28 | return assertNever(def); 29 | } 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /tests/common/util.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | import { compareNumber, sameNumber } from '../../src/common/util'; 3 | 4 | test('sameNumber', () => { 5 | // true 6 | expect(sameNumber(0, 0)).toBe(true); 7 | expect(sameNumber(Infinity, Infinity)).toBe(true); 8 | expect(sameNumber(-Infinity, -Infinity)).toBe(true); 9 | expect(sameNumber(NaN, NaN)).toBe(true); 10 | 11 | // false 12 | expect(sameNumber(-2, 0)).toBe(false); 13 | }); 14 | 15 | test('compareNumber', () => { 16 | const numbers = [ 17 | 0, 18 | -0, 19 | 1, 20 | -1, 21 | 2, 22 | -2, 23 | Number.MAX_VALUE, 24 | Number.MIN_VALUE, 25 | Number.EPSILON, 26 | Number.MAX_SAFE_INTEGER, 27 | Number.MIN_SAFE_INTEGER, 28 | Infinity, 29 | -Infinity, 30 | NaN, 31 | ]; 32 | 33 | const a = [...numbers].sort(compareNumber); 34 | const b = [...numbers].reverse().sort(compareNumber); 35 | 36 | expect(a).toStrictEqual(b); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/data/box-median-blur.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.8.1","content":{"nodes":[{"data":{"schemaId":"chainner:image:blur","inputData":{"1":3,"2":7},"id":"13626da8-98e4-4fc6-91ef-6b0e8e875619"},"id":"13626da8-98e4-4fc6-91ef-6b0e8e875619","position":{"x":480,"y":400},"type":"regularNode","selected":false,"height":339,"width":257,"zIndex":50},{"data":{"schemaId":"chainner:image:median_blur","inputData":{"1":4},"id":"e7c3d964-716d-43d7-83b9-83825c35a7e2"},"id":"e7c3d964-716d-43d7-83b9-83825c35a7e2","position":{"x":768,"y":400},"type":"regularNode","selected":false,"height":257,"width":257,"zIndex":50}],"edges":[{"id":"e8a88e5a-6d1f-44a6-9e42-e5f1b633d564","sourceHandle":"13626da8-98e4-4fc6-91ef-6b0e8e875619-0","targetHandle":"e7c3d964-716d-43d7-83b9-83825c35a7e2-0","source":"13626da8-98e4-4fc6-91ef-6b0e8e875619","target":"e7c3d964-716d-43d7-83b9-83825c35a7e2","type":"main","animated":false,"data":{},"zIndex":49}],"viewport":{"x":0,"y":0,"zoom":1}},"timestamp":"2022-06-06T16:48:59.273Z","checksum":"cbc68bacb9e1af4e16846bb25502daa9","migration":8} -------------------------------------------------------------------------------- /tests/data/canny-edge-detection.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.11.6","content":{"nodes":[{"data":{"schemaId":"chainner:image:view","inputData":{},"id":"b82af4e7-689e-46a2-873f-d41bc570401e"},"id":"b82af4e7-689e-46a2-873f-d41bc570401e","position":{"x":645,"y":240},"type":"regularNode","selected":false,"height":371,"width":241,"zIndex":50},{"data":{"schemaId":"chainner:image:canny_edge_detection","inputData":{"1":100,"2":300},"id":"d6f6e8e6-3699-44eb-95c1-bb9bd951a32b"},"id":"d6f6e8e6-3699-44eb-95c1-bb9bd951a32b","position":{"x":330,"y":240},"type":"regularNode","selected":false,"height":327,"width":266,"zIndex":50},{"data":{"schemaId":"chainner:image:load","inputData":{},"id":"d708003f-ca9e-4178-a775-4f35735dae02"},"id":"d708003f-ca9e-4178-a775-4f35735dae02","position":{"x":30,"y":240},"type":"regularNode","selected":false,"height":487,"width":259,"zIndex":50}],"edges":[{"id":"1270dca4-eab0-4bb0-857a-6bd27bd1aa33","sourceHandle":"d708003f-ca9e-4178-a775-4f35735dae02-0","targetHandle":"d6f6e8e6-3699-44eb-95c1-bb9bd951a32b-0","source":"d708003f-ca9e-4178-a775-4f35735dae02","target":"d6f6e8e6-3699-44eb-95c1-bb9bd951a32b","type":"main","animated":false,"data":{},"zIndex":49},{"id":"77306abe-b237-4b21-abe9-5750fe43bd66","sourceHandle":"d6f6e8e6-3699-44eb-95c1-bb9bd951a32b-0","targetHandle":"b82af4e7-689e-46a2-873f-d41bc570401e-0","source":"d6f6e8e6-3699-44eb-95c1-bb9bd951a32b","target":"b82af4e7-689e-46a2-873f-d41bc570401e","type":"main","animated":false,"data":{},"zIndex":49}],"viewport":{"x":0,"y":0,"zoom":1}},"timestamp":"2022-08-31T04:55:31.338Z","checksum":"1c98b81bffb6983c881f27aea8bccabb","migration":15} -------------------------------------------------------------------------------- /tests/data/convert-to-ncnn.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.10.1","content":{"nodes":[{"data":{"schemaId":"chainner:onnx:load_model","inputData":{},"id":"056923aa-5161-4efb-b1a8-1a15a867ae26"},"id":"056923aa-5161-4efb-b1a8-1a15a867ae26","position":{"x":390,"y":345},"type":"regularNode","selected":false,"height":252,"width":256,"zIndex":50},{"data":{"schemaId":"chainner:onnx:convert_to_ncnn","inputData":{"1":0},"id":"3e1a29d5-06de-4ab7-ac98-480c88156a73"},"id":"3e1a29d5-06de-4ab7-ac98-480c88156a73","position":{"x":675,"y":345},"type":"regularNode","selected":false,"height":252,"width":242,"zIndex":50},{"data":{"schemaId":"chainner:ncnn:save_model","inputData":{},"id":"9256866c-414a-4bd7-8b66-804922ac8875"},"id":"9256866c-414a-4bd7-8b66-804922ac8875","position":{"x":945,"y":345},"type":"regularNode","selected":true,"height":270,"width":256,"zIndex":70}],"edges":[{"id":"1eaa9bee-3615-45de-8bb6-6b79230ff664","sourceHandle":"3e1a29d5-06de-4ab7-ac98-480c88156a73-0","targetHandle":"9256866c-414a-4bd7-8b66-804922ac8875-0","source":"3e1a29d5-06de-4ab7-ac98-480c88156a73","target":"9256866c-414a-4bd7-8b66-804922ac8875","type":"main","animated":false,"data":{},"zIndex":69},{"id":"c83b7ba6-3179-43ba-8c7f-9b44a2f8ca45","sourceHandle":"056923aa-5161-4efb-b1a8-1a15a867ae26-0","targetHandle":"3e1a29d5-06de-4ab7-ac98-480c88156a73-0","source":"056923aa-5161-4efb-b1a8-1a15a867ae26","target":"3e1a29d5-06de-4ab7-ac98-480c88156a73","type":"main","animated":false,"data":{},"zIndex":49}],"viewport":{"x":-113.11594255284149,"y":57.879643031246246,"zoom":0.6263322193120638}},"timestamp":"2022-08-13T02:58:43.342Z","checksum":"8b7a040d325bbda5f9fb8bca141054db","migration":15} -------------------------------------------------------------------------------- /tests/data/copy-to-clipboard.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.12.3","content":{"nodes":[{"data":{"schemaId":"chainner:image:load","inputData":{},"id":"5fab26d4-8aee-472d-a76a-c8360b7c879e"},"id":"5fab26d4-8aee-472d-a76a-c8360b7c879e","position":{"x":-409.9270007828063,"y":434.0347224631112},"type":"regularNode","selected":false,"height":488,"width":257,"zIndex":50},{"data":{"schemaId":"chainner:utility:copy_to_clipboard","inputData":{},"id":"bd03ae2b-2de9-4ce6-a80e-0deef9d77d32"},"id":"bd03ae2b-2de9-4ce6-a80e-0deef9d77d32","position":{"x":10.940350900119938,"y":667.0756396555195},"type":"regularNode","selected":true,"height":154,"width":242,"zIndex":70}],"edges":[{"id":"8ff6f8b5-f99f-4d35-9429-1c29850c0770","sourceHandle":"5fab26d4-8aee-472d-a76a-c8360b7c879e-2","targetHandle":"bd03ae2b-2de9-4ce6-a80e-0deef9d77d32-0","source":"5fab26d4-8aee-472d-a76a-c8360b7c879e","target":"bd03ae2b-2de9-4ce6-a80e-0deef9d77d32","type":"main","animated":false,"data":{},"zIndex":69}],"viewport":{"x":434.34962404436874,"y":-334.55802893290115,"zoom":0.8705505632961259}},"timestamp":"2022-09-11T18:50:46.480Z","checksum":"84ccdb64066a8d7bc69f225bffb19cb3","migration":16} -------------------------------------------------------------------------------- /tests/data/crop-content.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.9.2","content":{"nodes":[{"data":{"schemaId":"chainner:image:crop_content","inputData":{},"id":"561b20d5-ff13-4424-930e-352caeb48aff"},"id":"561b20d5-ff13-4424-930e-352caeb48aff","position":{"x":896,"y":528},"type":"regularNode","selected":true,"height":175,"width":241,"zIndex":70}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}},"timestamp":"2022-06-26T16:08:51.948Z","checksum":"e4ba10ded4459c6fcf627fc35f4fa611","migration":12} -------------------------------------------------------------------------------- /tests/data/image-adjustments.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.8.1","content":{"nodes":[{"data":{"schemaId":"chainner:image:threshold","inputData":{"1":69,"2":76,"3":"2"},"id":"23a197cf-248a-4c35-8aa4-5c9a32863d44"},"id":"23a197cf-248a-4c35-8aa4-5c9a32863d44","position":{"x":625.8070606398546,"y":592.9030240407762},"type":"regularNode","selected":false,"height":392,"width":242,"zIndex":50},{"data":{"schemaId":"chainner:image:brightness_and_contrast","inputData":{"1":24,"2":43},"id":"59b0e388-685e-4b7b-8ffe-c64bc98a350f"},"id":"59b0e388-685e-4b7b-8ffe-c64bc98a350f","position":{"x":571.3328481394495,"y":163.52957902171042},"type":"regularNode","selected":false,"height":310,"width":278,"zIndex":50},{"data":{"schemaId":"chainner:image:hue_and_saturation","inputData":{"1":-61,"2":68},"id":"5acd95ce-3be7-42d0-9388-2e681fe39d1c"},"id":"5acd95ce-3be7-42d0-9388-2e681fe39d1c","position":{"x":1034.6005807596155,"y":179.7658590325766},"type":"regularNode","selected":false,"height":310,"width":242,"zIndex":50},{"data":{"schemaId":"chainner:image:threshold_adaptive","inputData":{"1":90,"2":0,"3":0,"4":9,"5":2},"id":"75cbaa5e-7d31-4990-b46d-8087e550a729"},"id":"75cbaa5e-7d31-4990-b46d-8087e550a729","position":{"x":1007.135509649303,"y":537.8809142551769},"type":"regularNode","selected":false,"height":572,"width":262,"zIndex":50}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}},"timestamp":"2022-05-26T19:24:24.086Z","checksum":"5e390b4028ca1e3d9849869c97336359","migration":7} -------------------------------------------------------------------------------- /tests/data/image-metrics.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.8.1","content":{"nodes":[{"data":{"schemaId":"chainner:image:image_metrics","inputData":{},"id":"0a3ed516-9505-462d-9ada-c409da4c4d67"},"id":"0a3ed516-9505-462d-9ada-c409da4c4d67","position":{"x":624,"y":560},"type":"regularNode","selected":true,"height":295,"width":241,"zIndex":70}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}},"timestamp":"2022-06-07T02:21:44.292Z","checksum":"b32ee40fefb7b627c825ae653b232c0a","migration":8} -------------------------------------------------------------------------------- /tests/data/model-scale.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.9.2","content":{"nodes":[{"data":{"schemaId":"chainner:pytorch:load_model","inputData":{"0":"C:\\DS3TexUp\\Cupscale 1.39.0f1\\CupscaleData\\models\\ESRGAN\\4x-AnimeSharp.pth"},"id":"336d5195-2137-49c4-8efc-6aa1bd99c4c1"},"id":"336d5195-2137-49c4-8efc-6aa1bd99c4c1","position":{"x":-1,"y":219},"type":"regularNode","selected":false,"height":418,"width":517,"zIndex":50},{"data":{"schemaId":"chainner:pytorch:model_dim","inputData":{},"id":"66afd92a-138e-42b7-a5d3-a45737f31ed4"},"id":"66afd92a-138e-42b7-a5d3-a45737f31ed4","position":{"x":239.78031643091583,"y":713.9047452339597},"type":"regularNode","selected":true,"height":240,"width":503,"zIndex":70}],"edges":[{"id":"ef31ea17-2e21-402b-8366-7ebacc4fc084","sourceHandle":"336d5195-2137-49c4-8efc-6aa1bd99c4c1-0","targetHandle":"66afd92a-138e-42b7-a5d3-a45737f31ed4-0","source":"336d5195-2137-49c4-8efc-6aa1bd99c4c1","target":"66afd92a-138e-42b7-a5d3-a45737f31ed4","type":"main","animated":false,"data":{},"zIndex":69}],"viewport":{"x":0,"y":0,"zoom":1}},"timestamp":"2022-07-02T17:37:21.861Z","checksum":"94dcaecb771e45bf85981d3b74643183","migration":12} -------------------------------------------------------------------------------- /tests/data/normal-map-gen-invert.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.21.2","content":{"nodes":[{"data":{"schemaId":"chainner:image:normal_generator","inputData":{"1":0,"2":0,"3":0,"4":1,"5":"sobel","6":1,"7":"none","8":0.25,"9":0.5,"10":0.3,"11":0.25,"12":0.2,"13":0.15,"14":0.1,"15":0.1,"16":0,"17":0,"18":0},"id":"ac216866-3f16-46ed-812d-2898f062e479"},"id":"ac216866-3f16-46ed-812d-2898f062e479","position":{"x":608,"y":240},"type":"regularNode","selected":false,"height":440,"width":295}],"edges":[],"viewport":{"x":-392.7852907370326,"y":74.69533137641815,"zoom":1.1486983549970369}},"timestamp":"2024-02-09T11:26:33.293Z","checksum":"e0eba6b2e538e07bc74410795a989949","migration":40} -------------------------------------------------------------------------------- /tests/data/onnx-interpolate.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.11.0","content":{"nodes":[{"data":{"schemaId":"chainner:onnx:interpolate_models","inputData":{"2":67},"id":"fbb2100e-2568-4427-b730-642e92b633b0","isDisabled":false},"id":"fbb2100e-2568-4427-b730-642e92b633b0","position":{"x":384,"y":144},"type":"regularNode","selected":false,"height":381,"width":247,"zIndex":50}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}},"timestamp":"2022-08-19T02:33:40.330Z","checksum":"1826a1b882ec4c54636066932cdeafb1","migration":15} -------------------------------------------------------------------------------- /tests/data/opacity.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.9.0","content":{"nodes":[{"data":{"schemaId":"chainner:image:opacity","inputData":{"1":72},"id":"6c9e3fce-fd01-492b-8abc-a175b104041e"},"id":"6c9e3fce-fd01-492b-8abc-a175b104041e","position":{"x":480,"y":400},"type":"regularNode","selected":true,"height":241,"width":241,"zIndex":70}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}},"timestamp":"2022-06-16T19:30:08.129Z","checksum":"07014c123db5bd7ebbb1d5a50421555f","migration":11} -------------------------------------------------------------------------------- /tests/data/resize-to-side.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.12.1","content":{"nodes":[{"data":{"schemaId":"chainner:image:save","inputData":{"4":"png"},"id":"0a558c45-2c62-4d94-a006-f3e24a40d2f5"},"id":"0a558c45-2c62-4d94-a006-f3e24a40d2f5","position":{"x":1485,"y":480},"type":"regularNode","selected":false,"height":422,"width":265,"zIndex":50},{"data":{"schemaId":"chainner:image:resize_to_side","inputData":{"1":2160,"2":"width","3":-1},"id":"89ad8b89-a34e-4327-846d-4c66ab4006e8"},"id":"89ad8b89-a34e-4327-846d-4c66ab4006e8","position":{"x":990,"y":375},"type":"regularNode","selected":false,"height":404,"width":283,"zIndex":50},{"data":{"schemaId":"chainner:image:load","inputData":{},"id":"fc9844d5-e9be-4f7d-8e01-b17944fb6874"},"id":"fc9844d5-e9be-4f7d-8e01-b17944fb6874","position":{"x":615,"y":330},"type":"regularNode","selected":false,"height":488,"width":257,"zIndex":50}],"edges":[{"id":"3046d390-6349-410c-b348-c0761399f39b","sourceHandle":"89ad8b89-a34e-4327-846d-4c66ab4006e8-0","targetHandle":"0a558c45-2c62-4d94-a006-f3e24a40d2f5-0","source":"89ad8b89-a34e-4327-846d-4c66ab4006e8","target":"0a558c45-2c62-4d94-a006-f3e24a40d2f5","type":"main","animated":false,"data":{},"zIndex":49},{"id":"64cee5d9-9521-4c12-8db0-52dcc9b42ca5","sourceHandle":"fc9844d5-e9be-4f7d-8e01-b17944fb6874-0","targetHandle":"89ad8b89-a34e-4327-846d-4c66ab4006e8-0","source":"fc9844d5-e9be-4f7d-8e01-b17944fb6874","target":"89ad8b89-a34e-4327-846d-4c66ab4006e8","type":"main","animated":false,"data":{},"zIndex":49}],"viewport":{"x":0,"y":0,"zoom":1}},"timestamp":"2022-09-06T13:37:05.838Z","checksum":"e3674a348d6215fefb7cfecf90fb65a1","migration":16} -------------------------------------------------------------------------------- /tests/data/save video input.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.21.2","content":{"nodes":[{"data":{"schemaId":"chainner:image:save_video","inputData":{"2":"foo","3":"libx264","4":"mkv","5":"mkv","6":"mkv","7":"webm","8":"veryfast","9":36,"10":"auto","11":"auto","12":1,"13":"Same text","14":24},"inputHeight":{"2":80,"13":80},"nodeWidth":240,"id":"7cbc339b-88d0-4192-ab0f-99c37d9e97eb"},"id":"7cbc339b-88d0-4192-ab0f-99c37d9e97eb","position":{"x":288,"y":224},"type":"collector","selected":false,"height":560,"width":258},{"data":{"schemaId":"chainner:image:save_video","inputData":{"2":"foo","3":"libvpx-vp9","4":"mkv","5":"mkv","6":"mkv","7":"webm","8":"veryfast","9":41,"10":"auto","11":"auto","12":0,"13":"Sam","14":24},"inputHeight":{"2":80,"13":80},"nodeWidth":240,"id":"a84ab15c-c8a1-555c-912f-dbfb8aee8a93"},"id":"a84ab15c-c8a1-555c-912f-dbfb8aee8a93","position":{"x":864,"y":224},"type":"collector","selected":false,"height":432,"width":240},{"data":{"schemaId":"chainner:image:save_video","inputData":{"2":"foo","3":"libvpx-vp9","4":"mkv","5":"mkv","6":"mkv","7":"mp4","8":"veryfast","9":11,"10":"auto","11":"auto","12":1,"13":"Sam","14":24},"inputHeight":{"2":80,"13":80},"nodeWidth":240,"id":"cb19d737-a6ea-516f-88a0-0f74fce288c4"},"id":"cb19d737-a6ea-516f-88a0-0f74fce288c4","position":{"x":576,"y":224},"type":"collector","selected":false,"height":520,"width":258}],"edges":[],"viewport":{"x":-174,"y":-28,"zoom":1}},"timestamp":"2024-01-25T23:38:29.614Z","checksum":"abaed3aadc2b9b5593ca292af0cbd54a","migration":39} -------------------------------------------------------------------------------- /tests/data/save-image-webp-lossless.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.19.1","content":{"nodes":[{"data":{"schemaId":"chainner:image:save","inputData":{"4":"webp-lossless","5":27,"6":"BC1_UNORM_SRGB","7":0,"8":0,"9":0,"10":0,"11":2167057,"12":0,"13":0,"14":0},"inputSize":{"2":{"width":240,"height":80},"3":{"width":240,"height":80}},"id":"08995baf-c169-4679-8372-4a6e8ee0b920"},"id":"08995baf-c169-4679-8372-4a6e8ee0b920","position":{"x":528,"y":336},"type":"regularNode","selected":true,"height":324,"width":240,"zIndex":70}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}},"timestamp":"2023-08-12T17:06:45.210Z","checksum":"0194d4899f5d5593b1e996bf19ff74b5","migration":31} -------------------------------------------------------------------------------- /tests/data/utilities.chn: -------------------------------------------------------------------------------- 1 | {"version":"0.8.1","content":{"nodes":[{"data":{"schemaId":"chainner:utility:text_append","inputData":{"0":"-","1":"Foo","2":"Bar","3":"","4":""},"id":"18acc79d-abf9-46f1-9b4b-651836a9d3b1"},"id":"18acc79d-abf9-46f1-9b4b-651836a9d3b1","position":{"x":592.03125,"y":283},"type":"regularNode","selected":false,"height":548,"width":242,"zIndex":50},{"data":{"schemaId":"chainner:utility:math","inputData":{"0":1,"1":"mul","2":5},"id":"56909a51-acd0-4e3d-b515-b3cb34e725c5"},"id":"56909a51-acd0-4e3d-b515-b3cb34e725c5","position":{"x":95.03125,"y":377},"type":"regularNode","selected":true,"height":384,"width":257,"zIndex":70},{"data":{"schemaId":"chainner:utility:note","inputData":{"0":"Hi mom!"},"id":"e21f5bf4-8171-43ce-a483-5152c5b7022e"},"id":"e21f5bf4-8171-43ce-a483-5152c5b7022e","position":{"x":282.03125,"y":81},"type":"regularNode","selected":false,"height":202,"width":258,"zIndex":50}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}},"timestamp":"2022-05-27T11:19:23.559Z","checksum":"0b701a58ee2c5edf56936dbd708f9d92","migration":7} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "noImplicitAny": true, 5 | "lib": [ 6 | "ESNext", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "module": "commonjs", 11 | "target": "ESNext", 12 | "jsx": "react-jsx", 13 | "allowJs": true, 14 | "checkJs": false, 15 | "strict": true, 16 | "allowSyntheticDefaultImports": true, 17 | "skipLibCheck": true, 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "esModuleInterop": true, 21 | "outDir": "dist", 22 | }, 23 | "include": [ 24 | "scripts", 25 | "src", 26 | "tests", 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /vite/forge-types.ts: -------------------------------------------------------------------------------- 1 | export {}; // Make this a module 2 | 3 | declare global { 4 | // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite 5 | // plugin that tells the Electron app where to look for the Vite-bundled app code (depending on 6 | // whether you're running in development or production). 7 | const MAIN_WINDOW_VITE_DEV_SERVER_URL: string; 8 | const MAIN_WINDOW_VITE_NAME: string; 9 | 10 | namespace NodeJS { 11 | interface Process { 12 | // Used for hot reload after preload scripts. 13 | viteDevServers: Record<string, import('vite').ViteDevServer>; 14 | } 15 | } 16 | 17 | type VitePluginConfig = ConstructorParameters< 18 | typeof import('@electron-forge/plugin-vite').VitePlugin 19 | >[0]; 20 | 21 | interface VitePluginRuntimeKeys { 22 | VITE_DEV_SERVER_URL: `${string}_VITE_DEV_SERVER_URL`; 23 | VITE_NAME: `${string}_VITE_NAME`; 24 | } 25 | } 26 | 27 | declare module 'vite' { 28 | interface ConfigEnv<K extends keyof VitePluginConfig = keyof VitePluginConfig> { 29 | root: string; 30 | forgeConfig: VitePluginConfig; 31 | forgeConfigSelf: VitePluginConfig[K][number]; 32 | } 33 | } 34 | --------------------------------------------------------------------------------