Repository: the-database/MangaJaNaiConverterGui
Branch: main
Commit: e63e7843ba45
Files: 136
Total size: 971.0 KB
Directory structure:
gitextract_o6l0qlog/
├── .editorconfig
├── .github/
│ └── workflows/
│ └── deploy.yml
├── .gitignore
├── LICENSE
├── MangaJaNaiConverterGui/
│ ├── App.axaml
│ ├── App.axaml.cs
│ ├── Drivers/
│ │ └── NewtonsoftJsonSuspensionDriver.cs
│ ├── MangaJaNaiConverterGui.csproj
│ ├── Program.cs
│ ├── Services/
│ │ ├── Downloader.cs
│ │ ├── ETACalculator.cs
│ │ ├── IPythonService.cs
│ │ ├── ISuspensionDriverService.cs
│ │ ├── IUpdateManagerService.cs
│ │ ├── PythonService.cs
│ │ ├── SuspensionDriverService.cs
│ │ └── UpdateManagerService.cs
│ ├── ViewLocator.cs
│ ├── ViewModels/
│ │ ├── MainWindowViewModel.cs
│ │ └── ViewModelBase.cs
│ ├── Views/
│ │ ├── MainWindow.axaml
│ │ └── MainWindow.axaml.cs
│ ├── app.manifest
│ ├── appstate2.json
│ └── backend/
│ ├── ImageMagick/
│ │ ├── Custom Gray Gamma 1.0.icc
│ │ ├── Custom RGB Gamma 1.0.icc
│ │ └── Dot Gain 20%.icc
│ ├── resources/
│ │ └── default_cli_configuration.json
│ └── src/
│ ├── .pre-commit-config.yaml
│ ├── README.md
│ ├── __init__.py
│ ├── accelerator_detection.py
│ ├── 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
│ ├── device_list.py
│ ├── gpu.py
│ ├── navi.py
│ ├── nodes/
│ │ ├── __init__.py
│ │ ├── condition.py
│ │ ├── group.py
│ │ ├── groups.py
│ │ ├── impl/
│ │ │ ├── __init__.py
│ │ │ ├── blend.py
│ │ │ ├── color/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── color.py
│ │ │ │ ├── convert.py
│ │ │ │ ├── convert_data.py
│ │ │ │ └── convert_model.py
│ │ │ ├── image_formats.py
│ │ │ ├── image_op.py
│ │ │ ├── image_utils.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
│ │ │ │ │ └── warplayer.py
│ │ │ │ └── utils.py
│ │ │ ├── resize.py
│ │ │ └── upscale/
│ │ │ ├── __init__.py
│ │ │ ├── auto_split.py
│ │ │ ├── auto_split_tiles.py
│ │ │ ├── basic_upscale.py
│ │ │ ├── convenient_upscale.py
│ │ │ ├── custom_scale.py
│ │ │ ├── exact_split.py
│ │ │ ├── grayscale.py
│ │ │ ├── passthrough.py
│ │ │ ├── tile_blending.py
│ │ │ └── tiler.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
│ │ ├── format.py
│ │ ├── seed.py
│ │ └── utils.py
│ ├── packages/
│ │ └── chaiNNer_pytorch/
│ │ ├── __init__.py
│ │ ├── pytorch/
│ │ │ ├── __init__.py
│ │ │ ├── io/
│ │ │ │ └── load_model.py
│ │ │ └── processing/
│ │ │ └── upscale_image.py
│ │ └── settings.py
│ ├── progress_controller.py
│ ├── pyproject.toml
│ ├── pyrightconfig.json
│ ├── run_upscale.py
│ ├── spandrel_custom/
│ │ ├── __init__.py
│ │ └── architectures/
│ │ └── FDAT/
│ │ ├── __arch/
│ │ │ ├── LICENSE
│ │ │ └── fdat.py
│ │ └── __init__.py
│ ├── system.py
│ └── test_accelerators.py
├── MangaJaNaiConverterGui.sln
├── README.md
└── pack.bat
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Expression-level preferences
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Field preferences
dotnet_style_readonly_field = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
# New line preferences
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = true
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = false
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = false
# Expression-bodied members
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Modifier preferences
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
# Code-block preferences
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
csharp_style_namespace_declarations = block_scoped
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_primary_constructors = true
csharp_style_prefer_top_level_statements = true
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace
# New line preferences
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
================================================
FILE: .github/workflows/deploy.yml
================================================
name: Deploy to GitHub Releases
on:
workflow_dispatch:
inputs:
version:
description: 'Version number for the release'
required: true
default: ''
jobs:
deploy-to-github-releases:
runs-on: windows-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Publish Application
run: dotnet publish MangaJaNaiConverterGui/MangaJaNaiConverterGui.csproj -c Release -o publish -r win-x64
- name: Create Velopack Release
run: |
dotnet tool install -g vpk --prerelease
vpk download github --repoUrl https://github.com/the-database/MangaJaNaiConverterGui
vpk pack -u MangaJaNaiConverterGui -v ${{ github.event.inputs.version }} -p publish -i ./MangaJaNaiConverterGui/assets/logo.ico -e MangaJaNaiConverterGui.exe
vpk upload github --repoUrl https://github.com/the-database/MangaJaNaiConverterGui --releaseName "${{ github.event.inputs.version }}" --tag ${{ github.event.inputs.version }} --token ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
MangaJaNaiConverterGui/chaiNNer/python/
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
================================================
FILE: MangaJaNaiConverterGui/App.axaml
================================================
================================================
FILE: MangaJaNaiConverterGui/App.axaml.cs
================================================
using Autofac;
using Avalonia;
using Avalonia.Markup.Xaml;
using MangaJaNaiConverterGui.Services;
using MangaJaNaiConverterGui.ViewModels;
using MangaJaNaiConverterGui.Views;
using ReactiveUI;
using Splat;
using Splat.Autofac;
using System.IO;
using ReactiveUI;
using ReactiveUI.Avalonia;
using Splat;
using Splat.Autofac;
namespace MangaJaNaiConverterGui
{
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
// Create a new Autofac container builder.
var builder = new ContainerBuilder();
builder.RegisterType().AsSelf();
builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().SingleInstance();
// etc.
// Register the Adapter to Splat.
// Creates and sets the Autofac resolver as the Locator.
var autofacResolver = builder.UseAutofacDependencyResolver();
// Register the resolver in Autofac so it can be later resolved.
builder.RegisterInstance(autofacResolver);
// Initialize ReactiveUI components.
autofacResolver.InitializeReactiveUI();
var container = builder.Build();
autofacResolver.SetLifetimeScope(container);
//var vm = container.Resolve();
var umService = container.Resolve();
if (umService.IsInstalled)
{
if (!Directory.Exists(Program.InstalledAppStateFolder))
{
Directory.CreateDirectory(Program.InstalledAppStateFolder);
}
if (!File.Exists(Program.InstalledAppStatePath))
{
File.Copy(Program.InstalledAppStateFilename, Program.InstalledAppStatePath);
}
}
var suspension = new AutoSuspendHelper(ApplicationLifetime);
RxApp.SuspensionHost.CreateNewAppState = () => new MainWindowViewModel();
RxApp.SuspensionHost.SetupDefaultSuspendResume(container.Resolve().SuspensionDriver);
suspension.OnFrameworkInitializationCompleted();
// Load the saved view model state.
var state = RxApp.SuspensionHost.GetAppState();
foreach (var wf in state.Workflows)
{
wf.Vm = state;
foreach (var chain in wf.Chains)
{
chain.Vm = state;
}
}
state.CurrentWorkflow?.Validate();
new MainWindow { DataContext = state }.Show();
base.OnFrameworkInitializationCompleted();
}
}
}
================================================
FILE: MangaJaNaiConverterGui/Drivers/NewtonsoftJsonSuspensionDriver.cs
================================================
using Newtonsoft.Json;
using ReactiveUI;
using System;
using System.IO;
using System.Reactive;
using System.Reactive.Linq;
namespace MangaJaNaiConverterGui.Drivers
{
public class NewtonsoftJsonSuspensionDriver : ISuspensionDriver
{
private readonly string _file;
public static readonly JsonSerializerSettings Settings = new()
{
TypeNameHandling = TypeNameHandling.All,
Formatting = Formatting.Indented,
};
public NewtonsoftJsonSuspensionDriver(string file) => _file = file;
public IObservable InvalidateState()
{
if (File.Exists(_file))
File.Delete(_file);
return Observable.Return(Unit.Default);
}
public IObservable LoadState()
{
var lines = File.ReadAllText(_file);
var state = JsonConvert.DeserializeObject(lines, Settings)!;
return Observable.Return(state);
}
public IObservable SaveState(object state)
{
var lines = JsonConvert.SerializeObject(state, Settings);
File.WriteAllText(_file, lines);
return Observable.Return(Unit.Default);
}
}
}
================================================
FILE: MangaJaNaiConverterGui/MangaJaNaiConverterGui.csproj
================================================
WinExe
net10.0
enable
true
app.manifest
true
1.0.0
logo.png
Assets\logo.ico
true
Always
PreserveNewest
================================================
FILE: MangaJaNaiConverterGui/Program.cs
================================================
using Avalonia;
using ReactiveUI.Avalonia;
using System;
using System.IO;
using Velopack;
namespace MangaJaNaiConverterGui
{
internal class Program
{
public static bool WasFirstRun { get; private set; }
public static readonly string InstalledAppStateFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MangaJaNaiConverterGui"
);
public static readonly string InstalledAppStateFilename = "appstate2.json";
public static readonly string InstalledAppStatePath = Path.Combine(InstalledAppStateFolder, InstalledAppStateFilename);
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
VelopackApp.Build()
.OnBeforeUninstallFastCallback((v) =>
{
// On uninstall, remove Python and models from app data
var pythonDir = Path.Combine(InstalledAppStateFolder, "python");
var modelsDir = Path.Combine(InstalledAppStateFolder, "models");
if (Directory.Exists(pythonDir))
{
Directory.Delete(pythonDir, true);
}
if (Directory.Exists(modelsDir))
{
Directory.Delete(modelsDir, true);
}
})
.OnFirstRun(_ =>
{
WasFirstRun = true;
})
.Run();
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
.UseReactiveUI();
}
}
================================================
FILE: MangaJaNaiConverterGui/Services/Downloader.cs
================================================
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace MangaJaNaiConverterGui.Services
{
public class Downloader
{
public delegate void ProgressChanged(double percentage);
public static async Task DownloadFileAsync(string url, string destinationFilePath, ProgressChanged progressChanged)
{
using HttpClient client = new();
using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
long totalBytes = response.Content.Headers.ContentLength ?? -1L;
using Stream contentStream = await response.Content.ReadAsStreamAsync(), fileStream = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
var totalRead = 0L;
var buffer = new byte[8192];
int read;
while ((read = await contentStream.ReadAsync(buffer)) > 0)
{
await fileStream.WriteAsync(buffer.AsMemory(0, read));
totalRead += read;
if (totalBytes != -1)
{
double percentage = Math.Round((double)totalRead / totalBytes * 100, 0);
progressChanged?.Invoke(percentage);
}
}
}
}
}
================================================
FILE: MangaJaNaiConverterGui/Services/ETACalculator.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using ProgressItem = System.Collections.Generic.KeyValuePair;
namespace MangaJaNaiConverterGui.Services
{
public interface IETACalculator
{
/// Clears all collected data.
///
void Reset();
/// Updates the current progress.
///
/// The current level of completion.
/// Must be between 0.0 and 1.0 (inclusively).
void Update(float progress);
/// Returns True when there is enough data to calculate the ETA.
/// Returns False if the ETA is still calculating.
///
bool ETAIsAvailable { get; }
/// Calculates the Estimated Time of Arrival (Completion)
///
DateTime ETA { get; }
/// Calculates the Estimated Time Remaining.
///
TimeSpan ETR { get; }
}
/// Calculates the "Estimated Time of Arrival"
/// (or more accurately, "Estimated Time of Completion"),
/// based on a "rolling average" of progress over time.
///
public class ETACalculator : IETACalculator
{
///
///
///
/// The minimum number of data points required before ETA can be calculated.
///
///
/// Determines how many seconds of data will be used to calculate the ETA.
///
public ETACalculator(int minimumData, double maximumDuration)
{
this.minimumData = minimumData;
maximumTicks = (long)(maximumDuration * Stopwatch.Frequency);
queue = new Queue(minimumData * 2);
timer = Stopwatch.StartNew();
}
private int minimumData;
private long maximumTicks;
private readonly Stopwatch timer;
private readonly Queue queue;
private ProgressItem current;
private ProgressItem oldest;
public void Reset()
{
queue.Clear();
timer.Reset();
timer.Start();
}
private void ClearExpired()
{
var expired = timer.ElapsedTicks - maximumTicks;
while (queue.Count > minimumData && queue.Peek().Key < expired)
{
oldest = queue.Dequeue();
}
}
/// Adds the current progress to the calculation of ETA.
///
/// The current level of completion.
/// Must be between 0.0 and 1.0 (inclusively).
public void Update(float progress)
{
// If progress hasn't changed, ignore:
if (current.Value == progress)
{
return;
}
// Clear space for this item:
ClearExpired();
// Queue this item:
long currentTicks = timer.ElapsedTicks;
current = new ProgressItem(currentTicks, progress);
queue.Enqueue(current);
// See if its the first item:
if (queue.Count == 1)
{
oldest = current;
}
}
/// Calculates the Estimated Time Remaining
///
public TimeSpan ETR
{
get
{
// Create local copies of the oldest & current,
// so that another thread can update them without locking:
var oldest = this.oldest;
var current = this.current;
// Make sure we have enough items:
if (queue.Count < minimumData || oldest.Value == current.Value)
{
return TimeSpan.MaxValue;
}
// Calculate the estimated finished time:
double finishedInTicks = (1.0d - current.Value) * (current.Key - oldest.Key) / (current.Value - oldest.Value);
return TimeSpan.FromSeconds(finishedInTicks / Stopwatch.Frequency);
}
}
/// Calculates the Estimated Time of Arrival (Completion)
///
public DateTime ETA
{
get
{
return DateTime.Now.Add(ETR);
}
}
/// Returns True when there is enough data to calculate the ETA.
/// Returns False if the ETA is still calculating.
///
public bool ETAIsAvailable
{
get
{
// Make sure we have enough items:
return queue.Count >= minimumData && oldest.Value != current.Value;
}
}
}
}
================================================
FILE: MangaJaNaiConverterGui/Services/IPythonService.cs
================================================
using Avalonia.Collections;
using System;
using System.Threading.Tasks;
namespace MangaJaNaiConverterGui.Services
{
public interface IPythonService
{
bool IsPythonInstalled();
Task IsPythonUpdated();
Task IsBackendUpdated();
bool AreModelsInstalled();
string BackendUrl { get; }
string BackendDirectory { get; }
string LogsDirectory { get; }
string PythonDirectory { get; }
string ModelsDirectory { get; }
string PythonPath { get; }
string AppStateFolder { get; }
string AppStatePath { get; }
string AppStateFilename { get; }
string InstallUpdatePythonDependenciesCommand { get; }
string PythonBackendVersionPath { get; }
Version BackendVersion { get; }
void ExtractTgz(string gzArchiveName, string destFolder);
void ExtractZip(string archivePath, string outFolder, ProgressChanged progressChanged);
void Extract7z(string archivePath, string outFolder);
void AddPythonPth(string destFolder);
AvaloniaList AllModels { get; }
}
}
================================================
FILE: MangaJaNaiConverterGui/Services/ISuspensionDriverService.cs
================================================
using ReactiveUI;
namespace MangaJaNaiConverterGui.Services
{
public interface ISuspensionDriverService
{
ISuspensionDriver SuspensionDriver { get; }
}
}
================================================
FILE: MangaJaNaiConverterGui/Services/IUpdateManagerService.cs
================================================
using System;
using System.Threading.Tasks;
using Velopack;
namespace MangaJaNaiConverterGui.Services
{
public interface IUpdateManagerService
{
bool IsInstalled { get; }
bool IsPortable { get; }
string AppVersion { get; }
bool IsUpdatePendingRestart { get; }
void ApplyUpdatesAndRestart(UpdateInfo update);
Task CheckForUpdatesAsync();
Task DownloadUpdatesAsync(UpdateInfo update, Action? progress = null);
}
}
================================================
FILE: MangaJaNaiConverterGui/Services/PythonService.cs
================================================
using Avalonia.Collections;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip;
using SevenZipExtractor;
using Splat;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MangaJaNaiConverterGui.Services
{
public delegate void ProgressChanged(double percentage);
// https://github.com/chaiNNer-org/chaiNNer/blob/main/src/main/python/integratedPython.ts
public class PythonService : IPythonService
{
private readonly IUpdateManagerService _updateManagerService;
public static readonly Dictionary PYTHON_DOWNLOADS = new()
{
{
"win32",
new PythonDownload
{
Url = "https://github.com/astral-sh/python-build-standalone/releases/download/20251120/cpython-3.13.9+20251120-x86_64-pc-windows-msvc-install_only.tar.gz",
Path = "python/python.exe",
Version = "3.13.9",
Filename = "Python.tar.gz"
}
},
};
public Version BackendVersion => new Version(1, 5, 0);
public string BackendUrl => $"https://github.com/the-database/MangaJaNaiConverterGui-backend/releases/download/{BackendVersion}/mangajanaiconvertergui-backend-{BackendVersion}.7z";
public PythonService(IUpdateManagerService? updateManagerService = null)
{
_updateManagerService = updateManagerService ?? Locator.Current.GetService()!;
}
public string BackendDirectory => (_updateManagerService?.IsInstalled ?? false) ? Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"MangaJaNaiConverterGui") : Path.GetFullPath(@".\backend");
public string LogsDirectory => Path.Combine(BackendDirectory, "logs");
public string ModelsDirectory => Path.Combine(BackendDirectory, "models");
public string PythonDirectory => Path.Combine(BackendDirectory, "python");
public string PythonBackendVersionPath => Path.Combine(PythonDirectory, "Version.txt");
public string PythonPath => Path.GetFullPath(Path.Join(PythonDirectory, PYTHON_DOWNLOADS["win32"].Path));
public string AppStateFolder => ((_updateManagerService?.IsInstalled ?? false) && !(_updateManagerService?.IsPortable ?? false)) ? Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"MangaJaNaiConverterGui") : Path.GetFullPath(@".");
public string AppStateFilename => "appstate2.json";
public string AppStatePath => Path.Join(AppStateFolder, AppStateFilename);
public bool IsPythonInstalled() => File.Exists(PythonPath);
public async Task IsPythonUpdated()
{
var relPythonPath = @".\python\python\python.exe";
var cmd = $@"{relPythonPath} -V";
// Create a new process to run the CMD command
using (var process = new Process())
{
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = @$"/C {cmd}";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
process.StartInfo.WorkingDirectory = BackendDirectory;
Version? result = null;
// Create a StreamWriter to write the output to a log file
try
{
process.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
// ignore
}
};
process.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
result = new Version(e.Data.Replace("Python ", ""));
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine(); // Start asynchronous reading of the output
await process.WaitForExitAsync();
}
catch (IOException) { }
if (result == null || result.CompareTo(new Version(PYTHON_DOWNLOADS["win32"].Version)) < 0)
{
return false;
}
}
return true;
}
public async Task IsBackendUpdated()
{
if (File.Exists(PythonBackendVersionPath))
{
var currentVersion = new Version(await File.ReadAllTextAsync(PythonBackendVersionPath));
return currentVersion.CompareTo(BackendVersion) >= 0;
}
return false;
}
public bool AreModelsInstalled() => Directory.Exists(ModelsDirectory) && Directory.GetFiles(ModelsDirectory).Length > 0 && Directory.GetFiles(ModelsDirectory).Any(x => x.Contains("2x_IllustrationJaNai_V3denoise_FDAT_M_unshuffle_30k_fp16"));
public class PythonDownload
{
public string Url { get; set; }
public string Version { get; set; }
public string Path { get; set; }
public string Filename { get; set; }
}
public void ExtractTgz(string gzArchiveName, string destFolder)
{
Stream inStream = File.OpenRead(gzArchiveName);
Stream gzipStream = new GZipInputStream(inStream);
TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream, Encoding.UTF8);
tarArchive.ExtractContents(destFolder);
tarArchive.Close();
gzipStream.Close();
inStream.Close();
}
public void ExtractZip(string archivePath, string outFolder, ProgressChanged progressChanged)
{
using (var fsInput = File.OpenRead(archivePath))
using (var zf = new ZipFile(fsInput))
{
for (var i = 0; i < zf.Count; i++)
{
ZipEntry zipEntry = zf[i];
if (!zipEntry.IsFile)
{
// Ignore directories
continue;
}
String entryFileName = zipEntry.Name;
// to remove the folder from the entry:
//entryFileName = Path.GetFileName(entryFileName);
// Optionally match entrynames against a selection list here
// to skip as desired.
// The unpacked length is available in the zipEntry.Size property.
// Manipulate the output filename here as desired.
var fullZipToPath = Path.Combine(outFolder, entryFileName);
var directoryName = Path.GetDirectoryName(fullZipToPath);
if (directoryName.Length > 0)
{
Directory.CreateDirectory(directoryName);
}
// 4K is optimum
var buffer = new byte[4096];
// Unzip file in buffered chunks. This is just as fast as unpacking
// to a buffer the full size of the file, but does not waste memory.
// The "using" will close the stream even if an exception occurs.
using (var zipStream = zf.GetInputStream(zipEntry))
using (Stream fsOutput = File.Create(fullZipToPath))
{
StreamUtils.Copy(zipStream, fsOutput, buffer);
}
var percentage = Math.Round((double)i / zf.Count * 100, 0);
progressChanged?.Invoke(percentage);
}
}
}
public void Extract7z(string archiveName, string outFolder)
{
using ArchiveFile archiveFile = new(archiveName);
archiveFile.Extract(outFolder);
}
public void AddPythonPth(string destFolder)
{
string[] lines = { "python313.zip", "DLLs", "Lib", ".", "Lib/site-packages" };
var filename = "python313._pth";
using var outputFile = new StreamWriter(Path.Combine(destFolder, filename));
foreach (string line in lines)
outputFile.WriteLine(line);
}
public string InstallUpdatePythonDependenciesCommand
{
get
{
var relPythonPath = @".\python\python\python.exe";
return $@"{relPythonPath} -m pip install -U pip wheel --no-warn-script-location && {relPythonPath} -m pip install torch==2.9.1 torchvision --index-url https://download.pytorch.org/whl/cu128 --no-warn-script-location && {relPythonPath} -m pip install ""{Path.GetFullPath(@".\backend\src")}"" --no-warn-script-location";
}
}
private AvaloniaList? _allModels;
public AvaloniaList AllModels
{
get
{
if (_allModels == null)
{
try
{
var models = new AvaloniaList(Directory.GetFiles(ModelsDirectory).Where(filename =>
Path.GetExtension(filename).Equals(".pth", StringComparison.CurrentCultureIgnoreCase) ||
Path.GetExtension(filename).Equals(".pt", StringComparison.CurrentCultureIgnoreCase) ||
Path.GetExtension(filename).Equals(".ckpt", StringComparison.CurrentCultureIgnoreCase) ||
Path.GetExtension(filename).Equals(".safetensors", StringComparison.CurrentCultureIgnoreCase)
)
.Select(filename => Path.GetFileName(filename))
.Order().ToList());
models.Add("No Model");
Debug.WriteLine($"GetAllModels: {models.Count}");
_allModels = models;
}
catch (DirectoryNotFoundException)
{
Debug.WriteLine($"GetAllModels: DirectoryNotFoundException");
return [];
}
}
return _allModels;
}
}
}
}
================================================
FILE: MangaJaNaiConverterGui/Services/SuspensionDriverService.cs
================================================
using MangaJaNaiConverterGui.Drivers;
using ReactiveUI;
namespace MangaJaNaiConverterGui.Services
{
public class SuspensionDriverService(IPythonService pythonService) : ISuspensionDriverService
{
private readonly ISuspensionDriver _driver = new NewtonsoftJsonSuspensionDriver(pythonService.AppStatePath);
public ISuspensionDriver SuspensionDriver => _driver;
}
}
================================================
FILE: MangaJaNaiConverterGui/Services/UpdateManagerService.cs
================================================
using System;
using System.Threading.Tasks;
using Velopack;
using Velopack.Sources;
namespace MangaJaNaiConverterGui.Services
{
public class UpdateManagerService : IUpdateManagerService
{
private readonly UpdateManager _um;
public UpdateManagerService()
{
_um = new UpdateManager(new GithubSource("https://github.com/the-database/MangaJaNaiConverterGui", null, false));
}
public string AppVersion { get => _um?.CurrentVersion?.ToString() ?? ""; }
public bool IsInstalled { get => _um.IsInstalled; }
public bool IsPortable { get => _um.IsPortable; }
public bool IsUpdatePendingRestart { get => _um.IsUpdatePendingRestart; }
public void ApplyUpdatesAndRestart(UpdateInfo update)
{
_um.ApplyUpdatesAndRestart(update);
}
public async Task CheckForUpdatesAsync()
{
return await _um.CheckForUpdatesAsync();
}
public Task DownloadUpdatesAsync(UpdateInfo update, Action? progress = null)
{
return _um.DownloadUpdatesAsync(update, progress);
}
}
}
================================================
FILE: MangaJaNaiConverterGui/ViewLocator.cs
================================================
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using MangaJaNaiConverterGui.ViewModels;
using System;
namespace MangaJaNaiConverterGui
{
public class ViewLocator : IDataTemplate
{
public Control Build(object data)
{
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object data)
{
return data is ViewModelBase;
}
}
}
================================================
FILE: MangaJaNaiConverterGui/ViewModels/MainWindowViewModel.cs
================================================
using Avalonia.Collections;
using Avalonia.Threading;
using MangaJaNaiConverterGui.Drivers;
using MangaJaNaiConverterGui.Services;
using Newtonsoft.Json;
using ReactiveUI;
using Splat;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reactive.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Velopack;
using File = System.IO.File;
using Path = System.IO.Path;
namespace MangaJaNaiConverterGui.ViewModels
{
[DataContract]
public class MainWindowViewModel : ViewModelBase
{
public static readonly List IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".bmp", ".avif"];
public static readonly List ARCHIVE_EXTENSIONS = [".zip", ".cbz", ".rar", ".cbr"];
private readonly DispatcherTimer _timer = new();
private static readonly HttpClient client = new();
private UpdateInfo? _update = null;
private readonly IPythonService _pythonService;
private readonly IUpdateManagerService _updateManagerService;
private readonly ISuspensionDriverService _suspensionDriverService;
public MainWindowViewModel(IPythonService? pythonService = null, IUpdateManagerService? updateManagerService = null, ISuspensionDriverService? suspensionDriverService = null)
{
_pythonService = pythonService ?? Locator.Current.GetService()!;
_updateManagerService = updateManagerService ?? Locator.Current.GetService()!;
_suspensionDriverService = suspensionDriverService ?? Locator.Current.GetService()!;
var g1 = this.WhenAnyValue
(
x => x.SelectedWorkflowIndex
).Subscribe(x =>
{
CurrentWorkflow?.Validate();
});
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += _timer_Tick;
ShowDialog = new Interaction();
CheckAndDoBackup();
CheckForUpdates();
}
private string[] _commonResolutions = [
"0x0",
"0x1250",
"0x1251",
"0x1350",
"0x1351",
"0x1450",
"0x1451",
"0x1550",
"0x1551",
"0x1760",
"0x1761",
"0x1984",
"0x1985",];
private static readonly string DEFAULT_WORKFLOW = """
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Upscale Manga (Default)",
"WorkflowIndex": 0,
"SelectedTabIndex": 0,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": true,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": true,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": false,
"DisplayDevice": "Kobo Elipsa 2E (2023)",
"DisplayDeviceWidth": 1404,
"DisplayDeviceHeight": 1872,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"GrayscaleDetectionThreshold": 12,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_IllustrationJaNai_V3denoise_FDAT_M_unshuffle_30k_fp16.safetensors",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V3denoise_FDAT_M_47k_fp16.safetensors",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "16",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
}
""";
public string[] CommonResolutions
{
get => _commonResolutions;
set => this.RaiseAndSetIfChanged(ref _commonResolutions, value);
}
public Interaction ShowDialog { get; }
private void _timer_Tick(object? sender, EventArgs e)
{
ElapsedTime = ElapsedTime.Add(TimeSpan.FromSeconds(1));
}
private CancellationTokenSource? _cancellationTokenSource;
private Process? _runningProcess = null;
private readonly IETACalculator _archiveEtaCalculator = new ETACalculator(2, 3.0);
private readonly IETACalculator _totalEtaCalculator = new ETACalculator(2, 3.0);
public TimeSpan ArchiveEtr => _archiveEtaCalculator.ETAIsAvailable ? _archiveEtaCalculator.ETR : TimeSpan.FromSeconds(0);
public string ArchiveEta => _archiveEtaCalculator.ETAIsAvailable ? _archiveEtaCalculator.ETA.ToString("t") : "please wait";
public TimeSpan TotalEtr => _totalEtaCalculator.ETAIsAvailable ? _totalEtaCalculator.ETR : ArchiveEtr + (ElapsedTime + ArchiveEtr) * (ProgressTotalFiles - (ProgressCurrentFile + 1));
public string TotalEta => _totalEtaCalculator.ETAIsAvailable ? _totalEtaCalculator.ETA.ToString("t") : _archiveEtaCalculator.ETAIsAvailable ? DateTime.Now.Add(TotalEtr).ToString("t") : "please wait";
public bool IsInstalled => _updateManagerService.IsInstalled;
[DataMember]
public string ModelsDirectory => _pythonService.ModelsDirectory;
private bool _showCheckUpdateButton = true;
public bool ShowCheckUpdateButton
{
get => _showCheckUpdateButton;
set => this.RaiseAndSetIfChanged(ref _showCheckUpdateButton, value);
}
private bool _showDownloadButton = false;
public bool ShowDownloadButton
{
get => _showDownloadButton;
set
{
this.RaiseAndSetIfChanged(ref _showDownloadButton, value);
this.RaisePropertyChanged(nameof(ShowCheckUpdateButton));
}
}
private bool _showApplyButton = false;
public bool ShowApplyButton
{
get => _showApplyButton;
set
{
this.RaiseAndSetIfChanged(ref _showApplyButton, value);
this.RaisePropertyChanged(nameof(ShowCheckUpdateButton));
}
}
public string AppVersion => _updateManagerService.AppVersion;
private string _updateStatusText = string.Empty;
public string UpdateStatusText
{
get => _updateStatusText;
set => this.RaiseAndSetIfChanged(ref _updateStatusText, value);
}
private string[] _tileSizes = [
"Auto (Estimate)",
"Maximum",
"No Tiling",
"128",
"192",
"256",
"384",
"512",
"768",
"1024",
"2048",
"4096"];
public string[] TileSizes
{
get => _tileSizes;
set => this.RaiseAndSetIfChanged(ref _tileSizes, value);
}
private string[] _deviceList = [];
public string[] DeviceList
{
get => _deviceList;
set
{
this.RaiseAndSetIfChanged(ref _deviceList, value);
this.RaisePropertyChanged(nameof(SelectedDeviceIndex));
}
}
private string _pythonPipList = string.Empty;
public string PythonPipList
{
get => _pythonPipList;
set => this.RaiseAndSetIfChanged(ref _pythonPipList, value);
}
private AvaloniaDictionary _displayDeviceMap = [];
[DataMember]
public AvaloniaDictionary DisplayDeviceMap
{
get => _displayDeviceMap;
set => this.RaiseAndSetIfChanged(ref _displayDeviceMap, value);
}
private bool _autoUpdate;
[DataMember]
public bool AutoUpdateEnabled
{
get => _autoUpdate;
set => this.RaiseAndSetIfChanged(ref _autoUpdate, value);
}
private int _selectedDeviceIndex;
[DataMember]
public int SelectedDeviceIndex
{
get => _selectedDeviceIndex;
set => this.RaiseAndSetIfChanged(ref _selectedDeviceIndex, value);
}
private bool _useCpu;
[DataMember]
public bool UseCpu
{
get => _useCpu;
set => this.RaiseAndSetIfChanged(ref _useCpu, value);
}
private bool _useFp16;
[DataMember]
public bool UseFp16
{
get => _useFp16;
set => this.RaiseAndSetIfChanged(ref _useFp16, value);
}
private bool _upscaling = false;
[IgnoreDataMember]
public bool Upscaling
{
get => _upscaling;
set
{
this.RaiseAndSetIfChanged(ref _upscaling, value);
this.RaisePropertyChanged(nameof(UpscaleEnabled));
this.RaisePropertyChanged(nameof(LeftStatus));
}
}
private string _validationText = string.Empty;
public string ValidationText
{
get => _validationText;
set
{
this.RaiseAndSetIfChanged(ref _validationText, value);
this.RaisePropertyChanged(nameof(LeftStatus));
}
}
private string _backendSetupMainStatus = string.Empty;
public string BackendSetupMainStatus
{
get => this._backendSetupMainStatus;
set
{
this.RaiseAndSetIfChanged(ref _backendSetupMainStatus, value);
}
}
public string BackendSetupSubStatusText => string.Join("\n", BackendSetupSubStatusQueue);
private static readonly int BACKEND_SETUP_SUB_STATUS_QUEUE_CAPACITY = 50;
private ConcurrentQueue _backendSetupSubStatusQueue = new();
public ConcurrentQueue BackendSetupSubStatusQueue
{
get => this._backendSetupSubStatusQueue;
set
{
this.RaiseAndSetIfChanged(ref _backendSetupSubStatusQueue, value);
this.RaisePropertyChanged(nameof(BackendSetupSubStatusText));
}
}
public string ConsoleText => string.Join("\n", ConsoleQueue);
private static readonly int CONSOLE_QUEUE_CAPACITY = 1000;
private ConcurrentQueue _consoleQueue = new();
public ConcurrentQueue ConsoleQueue
{
get => this._consoleQueue;
set
{
this.RaiseAndSetIfChanged(ref _consoleQueue, value);
this.RaisePropertyChanged(nameof(ConsoleText));
}
}
private bool _showConsole = false;
public bool ShowConsole
{
get => _showConsole;
set => this.RaiseAndSetIfChanged(ref _showConsole, value);
}
private bool _showAppSettings = false;
public bool RequestShowAppSettings
{
get => _showAppSettings;
set
{
this.RaiseAndSetIfChanged(ref _showAppSettings, value);
this.RaisePropertyChanged(nameof(ShowAppSettings));
this.RaisePropertyChanged(nameof(ShowMainForm));
}
}
public string PythonPath => _pythonService.PythonPath;
private bool _isExtractingBackend = true;
public bool IsExtractingBackend
{
get => _isExtractingBackend;
set
{
this.RaiseAndSetIfChanged(ref _isExtractingBackend, value);
this.RaisePropertyChanged(nameof(RequestShowAppSettings));
this.RaisePropertyChanged(nameof(ShowMainForm));
}
}
public bool ShowAppSettings => RequestShowAppSettings && !IsExtractingBackend;
public bool ShowMainForm => !RequestShowAppSettings && !IsExtractingBackend;
private bool _showEstimates = false;
public bool ShowEstimates
{
get => _showEstimates;
set => this.RaiseAndSetIfChanged(ref _showEstimates, value);
}
private string _inputStatusText = string.Empty;
public string InputStatusText
{
get => _inputStatusText;
set
{
this.RaiseAndSetIfChanged(ref _inputStatusText, value);
this.RaisePropertyChanged(nameof(LeftStatus));
}
}
public string LeftStatus => !CurrentWorkflow.Valid ? ValidationText.Replace("\n", " ") : $"{InputStatusText} selected for upscaling.";
private int _progressCurrentFile = 0;
public int ProgressCurrentFile
{
get => _progressCurrentFile;
set => this.RaiseAndSetIfChanged(ref _progressCurrentFile, value);
}
private int _progressTotalFiles = 0;
public int ProgressTotalFiles
{
get => _progressTotalFiles;
set => this.RaiseAndSetIfChanged(ref _progressTotalFiles, value);
}
private int _progressCurrentFileInCurrentArchive = 0;
public int ProgressCurrentFileInArchive
{
get => _progressCurrentFileInCurrentArchive;
set => this.RaiseAndSetIfChanged(ref _progressCurrentFileInCurrentArchive, value);
}
private int _progressTotalFilesInCurrentArchive = 0;
public int ProgressTotalFilesInCurrentArchive
{
get => _progressTotalFilesInCurrentArchive;
set => this.RaiseAndSetIfChanged(ref _progressTotalFilesInCurrentArchive, value);
}
private bool _showArchiveProgressBar = false;
public bool ShowArchiveProgressBar
{
get => _showArchiveProgressBar;
set => this.RaiseAndSetIfChanged(ref _showArchiveProgressBar, value);
}
public bool UpscaleEnabled => CurrentWorkflow.Valid && !Upscaling;
private TimeSpan _elapsedTime = TimeSpan.FromSeconds(0);
public TimeSpan ElapsedTime
{
get => _elapsedTime;
set
{
this.RaiseAndSetIfChanged(ref _elapsedTime, value);
}
}
private AvaloniaList? _workflows;
[DataMember]
public AvaloniaList? Workflows
{
get => _workflows;
set => this.RaiseAndSetIfChanged(ref _workflows, value);
}
public AvaloniaList CustomWorkflows => new(Workflows.Skip(1).ToList());
private int _selectedWorkflowIndex = 0;
[DataMember]
public int SelectedWorkflowIndex
{
get => _selectedWorkflowIndex;
set
{
this.RaiseAndSetIfChanged(ref _selectedWorkflowIndex, value);
this.RaisePropertyChanged(nameof(CurrentWorkflow));
this.RaisePropertyChanged(nameof(CurrentWorkflow.ActiveWorkflow));
}
}
public UpscaleWorkflow? CurrentWorkflow
{
get => Workflows?[SelectedWorkflowIndex];
set
{
if (Workflows != null)
{
Workflows[SelectedWorkflowIndex] = value;
this.RaisePropertyChanged(nameof(CurrentWorkflow));
this.RaisePropertyChanged(nameof(CustomWorkflows));
}
}
}
public void HandleWorkflowSelected(int workflowIndex)
{
SelectedWorkflowIndex = workflowIndex;
RequestShowAppSettings = false;
}
public void HandleAppSettingsSelected()
{
RequestShowAppSettings = true;
}
public async Task RunUpscale()
{
_cancellationTokenSource = new CancellationTokenSource();
var ct = _cancellationTokenSource.Token;
var task = Task.Run(async () =>
{
await _suspensionDriverService.SuspensionDriver.SaveState(this);
ElapsedTime = TimeSpan.FromSeconds(0);
ShowEstimates = true;
_archiveEtaCalculator.Reset();
_totalEtaCalculator.Reset();
ct.ThrowIfCancellationRequested();
ConsoleQueueClear();
Upscaling = true;
ProgressCurrentFile = 0;
ProgressCurrentFileInArchive = 0;
ShowArchiveProgressBar = false;
var cmd = $@".\python\python\python.exe ""{Path.GetFullPath(@".\backend\src\run_upscale.py")}"" --settings ""{_pythonService.AppStatePath}""";
ConsoleQueueEnqueue($"Upscaling with command: {cmd}");
await RunCommand($@" /C {cmd}");
CurrentWorkflow.Valid = true;
}, ct);
try
{
_timer.Start();
await task;
_timer.Stop();
CurrentWorkflow.Validate();
}
catch (OperationCanceledException e)
{
_timer.Stop();
Console.WriteLine($"{nameof(OperationCanceledException)} thrown with message: {e.Message}");
Upscaling = false;
}
finally
{
_timer.Stop();
_cancellationTokenSource.Dispose();
Upscaling = false;
}
}
public void CancelUpscale()
{
try
{
_cancellationTokenSource?.Cancel();
if (_runningProcess != null && !_runningProcess.HasExited)
{
// Kill the process
_runningProcess.Kill(true);
_runningProcess = null; // Clear the reference to the terminated process
}
CurrentWorkflow.Validate();
}
catch { }
}
public void CheckInputs()
{
if (CurrentWorkflow.Valid && !Upscaling)
{
var overwriteText = CurrentWorkflow.OverwriteExistingFiles ? "overwritten" : "skipped";
// input file
if (CurrentWorkflow.SelectedTabIndex == 0)
{
StringBuilder status = new();
var skipFiles = 0;
if (IMAGE_EXTENSIONS.Any(x => CurrentWorkflow.InputFilePath.ToLower().EndsWith(x)))
{
var outputFilePath = Path.Join(
Path.GetFullPath(CurrentWorkflow.OutputFolderPath),
CurrentWorkflow.OutputFilename.Replace("%filename%", Path.GetFileNameWithoutExtension(CurrentWorkflow.InputFilePath))) + $".{CurrentWorkflow.ImageFormat}";
if (File.Exists(outputFilePath))
{
status.Append($" (1 image already exists and will be {overwriteText})");
if (!CurrentWorkflow.OverwriteExistingFiles)
{
skipFiles++;
}
}
}
else if (ARCHIVE_EXTENSIONS.Any(x => CurrentWorkflow.InputFilePath.ToLower().EndsWith(x)))
{
var outputFilePath = Path.Join(Path.GetFullPath(CurrentWorkflow.OutputFolderPath),
CurrentWorkflow.OutputFilename.Replace("%filename%", Path.GetFileNameWithoutExtension(CurrentWorkflow.InputFilePath))) + ".cbz";
if (File.Exists(outputFilePath))
{
status.Append($" (1 archive already exists and will be {overwriteText})");
if (!CurrentWorkflow.OverwriteExistingFiles)
{
skipFiles++;
}
}
}
else
{
// TODO ???
}
var s = skipFiles > 0 ? "s" : "";
if (IMAGE_EXTENSIONS.Any(x => CurrentWorkflow.InputFilePath.ToLower().EndsWith(x)))
{
status.Insert(0, $"{1 - skipFiles} image{s}");
}
else if (ARCHIVE_EXTENSIONS.Any(x => CurrentWorkflow.InputFilePath.ToLower().EndsWith(x)))
{
status.Insert(0, $"{1 - skipFiles} archive{s}");
}
else
{
status.Insert(0, "0 files");
}
InputStatusText = status.ToString();
ProgressCurrentFile = 0;
ProgressTotalFiles = 1 - skipFiles;
ProgressCurrentFileInArchive = 0;
ProgressTotalFilesInCurrentArchive = 0;
ShowArchiveProgressBar = false;
}
else // input folder
{
List statuses = new();
var existImageCount = 0;
var existArchiveCount = 0;
var totalFileCount = 0;
if (CurrentWorkflow.UpscaleImages)
{
var images = Directory.EnumerateFiles(CurrentWorkflow.InputFolderPath, "*.*", SearchOption.AllDirectories)
.Where(file => IMAGE_EXTENSIONS.Any(ext => file.ToLower().EndsWith(ext)));
var imagesCount = 0;
foreach (var inputImagePath in images)
{
var outputImagePath = Path.Join(
Path.GetFullPath(CurrentWorkflow.OutputFolderPath),
CurrentWorkflow.OutputFilename.Replace("%filename%", Path.GetFileNameWithoutExtension(inputImagePath))) + $"{CurrentWorkflow.ImageFormat}";
// if out file exists, exist count ++
// if overwrite image OR out file doesn't exist, count image++
var fileExists = File.Exists(outputImagePath);
if (fileExists)
{
existImageCount++;
}
if (!fileExists || CurrentWorkflow.OverwriteExistingFiles)
{
imagesCount++;
}
}
var imageS = imagesCount == 1 ? "" : "s";
var existImageS = existImageCount == 1 ? "" : "s";
statuses.Add($"{imagesCount} image{imageS} ({existImageCount} image{existImageS} already exist and will be {overwriteText})");
totalFileCount += imagesCount;
}
if (CurrentWorkflow.UpscaleArchives)
{
var archives = Directory.EnumerateFiles(CurrentWorkflow.InputFolderPath, "*.*", SearchOption.AllDirectories)
.Where(file => ARCHIVE_EXTENSIONS.Any(ext => file.ToLower().EndsWith(ext)));
var archivesCount = 0;
foreach (var inputArchivePath in archives)
{
var outputArchivePath = Path.Join(
Path.GetFullPath(CurrentWorkflow.OutputFolderPath),
CurrentWorkflow.OutputFilename.Replace("%filename%", Path.GetFileNameWithoutExtension(inputArchivePath))) + ".cbz";
var fileExists = File.Exists(outputArchivePath);
if (fileExists)
{
existArchiveCount++;
}
if (!fileExists || CurrentWorkflow.OverwriteExistingFiles)
{
archivesCount++;
}
}
var archiveS = archivesCount == 1 ? "" : "s";
var existArchiveS = existArchiveCount == 1 ? "" : "s";
statuses.Add($"{archivesCount} archive{archiveS} ({existArchiveCount} archive{existArchiveS} already exist and will be {overwriteText})");
totalFileCount += archivesCount;
}
if (!CurrentWorkflow.UpscaleArchives && !CurrentWorkflow.UpscaleImages)
{
InputStatusText = "0 files";
}
else
{
InputStatusText = $"{string.Join(" and ", statuses)}";
}
ProgressCurrentFile = 0;
ProgressTotalFiles = totalFileCount;
ProgressCurrentFileInArchive = 0;
ProgressTotalFilesInCurrentArchive = 0;
ShowArchiveProgressBar = false;
}
}
}
public void AddChain()
{
CurrentWorkflow?.Chains.Add(new UpscaleChain
{
Vm = this,
});
UpdateChainHeaders();
}
public void DeleteChain(UpscaleChain chain)
{
try
{
CurrentWorkflow.Chains.Remove(chain);
}
catch (ArgumentOutOfRangeException)
{
}
UpdateChainHeaders();
}
public void UpdateChainHeaders()
{
for (var i = 0; i < CurrentWorkflow.Chains.Count; i++)
{
CurrentWorkflow.Chains[i].ChainNumber = (i + 1).ToString();
}
}
public async Task RunCommand(string command)
{
// Create a new process to run the CMD command
using (var process = new Process())
{
_runningProcess = process;
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = command;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.WorkingDirectory = _pythonService.BackendDirectory;
process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
// Create a StreamWriter to write the output to a log file
using (var outputFile = new StreamWriter(Path.Combine(_pythonService.LogsDirectory, "upscale.log"), append: false))
{
process.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
outputFile.WriteLine(e.Data); // Write the output to the log file
ConsoleQueueEnqueue(e.Data);
}
};
process.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
if (e.Data.StartsWith("PROGRESS="))
{
if (e.Data.Contains("_zip_image"))
{
ShowArchiveProgressBar = true;
ProgressCurrentFileInArchive++;
UpdateEtas();
}
else
{
ProgressCurrentFile++;
UpdateEtas();
}
}
else if (e.Data.StartsWith("TOTALZIP="))
{
if (int.TryParse(e.Data.Replace("TOTALZIP=", ""), out var total))
{
ShowArchiveProgressBar = true;
ProgressCurrentFileInArchive = 0;
ProgressTotalFilesInCurrentArchive = total;
UpdateEtas();
}
}
else
{
outputFile.WriteLine(e.Data); // Write the output to the log file
ConsoleQueueEnqueue(e.Data);
Debug.WriteLine(e.Data);
}
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine(); // Start asynchronous reading of the output
await process.WaitForExitAsync();
}
}
}
public async Task InitializeDeviceList()
{
if (!File.Exists(@".\backend\src\device_list.py"))
{
return null;
}
// Create a new process to run the CMD command
using (var process = new Process())
{
_runningProcess = process;
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = @$"/C .\python\python\python.exe {Path.GetFullPath(@".\backend\src\device_list.py")}";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.WorkingDirectory = _pythonService.BackendDirectory;
process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
var result = string.Empty;
// Create a StreamWriter to write the output to a log file
try
{
using var outputFile = new StreamWriter(Path.Combine(_pythonService.LogsDirectory, "upscale.log"), append: false);
process.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
//outputFile.WriteLine(e.Data); // Write the output to the log file
//ConsoleQueueEnqueue(e.Data);
Debug.WriteLine(e.Data);
}
};
process.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
result = e.Data;
Debug.WriteLine(e.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine(); // Start asynchronous reading of the output
await process.WaitForExitAsync();
if (!string.IsNullOrEmpty(result))
{
return JsonConvert.DeserializeObject(result);
}
}
catch (IOException) { }
}
return null;
}
public async Task RunPythonPipList()
{
List result = [];
// Create a new process to run the CMD command
using (var process = new Process())
{
_runningProcess = process;
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = @$"/C .\python\python\python.exe -m pip list";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.WorkingDirectory = _pythonService.BackendDirectory;
process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
// Create a StreamWriter to write the output to a log file
try
{
process.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
//outputFile.WriteLine(e.Data); // Write the output to the log file
//ConsoleQueueEnqueue(e.Data);
Debug.WriteLine(e.Data);
}
};
process.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
result.Add(e.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine(); // Start asynchronous reading of the output
await process.WaitForExitAsync();
}
catch (IOException) { }
}
return string.Join("\n", result);
}
public async void ShowSettingsDialog()
{
var result = await ShowDialog.Handle(this);
}
private void UpdateEtas()
{
if (ProgressTotalFilesInCurrentArchive > 0)
{
_archiveEtaCalculator.Update(ProgressCurrentFileInArchive / (float)ProgressTotalFilesInCurrentArchive);
}
if (ProgressTotalFiles > 0)
{
_totalEtaCalculator.Update(ProgressCurrentFile / (float)ProgressTotalFiles);
}
this.RaisePropertyChanged(nameof(ArchiveEtr));
this.RaisePropertyChanged(nameof(ArchiveEta));
this.RaisePropertyChanged(nameof(TotalEtr));
this.RaisePropertyChanged(nameof(TotalEta));
}
private void ConsoleQueueClear()
{
ConsoleQueue.Clear();
this.RaisePropertyChanged(nameof(ConsoleText));
}
private void ConsoleQueueEnqueue(string value)
{
while (ConsoleQueue.Count > CONSOLE_QUEUE_CAPACITY)
{
ConsoleQueue.TryDequeue(out var _);
}
ConsoleQueue.Enqueue(value);
this.RaisePropertyChanged(nameof(ConsoleText));
}
private void BackendSetupSubStatusQueueEnqueue(string value)
{
while (BackendSetupSubStatusQueue.Count > BACKEND_SETUP_SUB_STATUS_QUEUE_CAPACITY)
{
BackendSetupSubStatusQueue.TryDequeue(out var _);
}
BackendSetupSubStatusQueue.Enqueue(value);
this.RaisePropertyChanged(nameof(BackendSetupSubStatusText));
}
public void ReadWorkflowFileToCurrentWorkflow(string fullPath)
{
if (!File.Exists(fullPath))
{
return;
}
var lines = File.ReadAllText(fullPath);
var workflow = JsonConvert.DeserializeObject(lines, NewtonsoftJsonSuspensionDriver.Settings);
if (workflow != null && CurrentWorkflow != null)
{
workflow.WorkflowIndex = CurrentWorkflow.WorkflowIndex;
workflow.Vm = CurrentWorkflow.Vm;
CurrentWorkflow = workflow;
}
}
public void WriteCurrentWorkflowToFile(string fullPath)
{
var lines = JsonConvert.SerializeObject(CurrentWorkflow, NewtonsoftJsonSuspensionDriver.Settings);
File.WriteAllText(fullPath, lines);
}
public async Task CheckAndExtractBackend()
{
await Task.Run(async () =>
{
IsExtractingBackend = true;
if (!Directory.Exists(_pythonService.LogsDirectory))
{
Directory.CreateDirectory(_pythonService.LogsDirectory);
}
if (!_pythonService.AreModelsInstalled())
{
await DownloadModels();
}
if (!_pythonService.IsPythonInstalled() || !(await _pythonService.IsBackendUpdated()))
{
// Download Python tgz
BackendSetupMainStatus = "Downloading Python Backend...";
var downloadUrl = _pythonService.BackendUrl;
var targetPath = Path.Join(_pythonService.PythonDirectory, "backend.7z");
if (Directory.Exists(_pythonService.PythonDirectory))
{
Directory.Delete(_pythonService.PythonDirectory, true);
}
Directory.CreateDirectory(_pythonService.PythonDirectory);
await Downloader.DownloadFileAsync(downloadUrl, targetPath, (progress) =>
{
BackendSetupMainStatus = $"Downloading Python Backend ({progress}%)...";
});
// Extract Python 7z
BackendSetupMainStatus = "Extracting Python Backend...";
_pythonService.Extract7z(targetPath, _pythonService.PythonDirectory);
Directory.Move(Path.Combine(_pythonService.PythonDirectory, "backend", "python"), Path.Combine(_pythonService.PythonDirectory, "python"));
using (StreamWriter sw = File.CreateText(_pythonService.PythonBackendVersionPath))
{
sw.WriteLine(_pythonService.BackendVersion);
}
Directory.Delete(Path.Combine(_pythonService.PythonDirectory, "backend"));
File.Delete(targetPath);
}
IsExtractingBackend = false;
});
var deviceResponse = await InitializeDeviceList();
if (deviceResponse != null)
{
DeviceList = [.. deviceResponse.AllDevices.Select(d => d.Name)];
SelectedDeviceIndex = deviceResponse.BestDevice;
}
else
{
SelectedDeviceIndex = 1; // default to first non cpu device
}
PythonPipList = await RunPythonPipList();
}
public async Task ReinstallBackend()
{
if (Directory.Exists(_pythonService.ModelsDirectory))
{
Directory.Delete(_pythonService.ModelsDirectory, true);
}
if (Directory.Exists(_pythonService.PythonDirectory))
{
Directory.Delete(_pythonService.PythonDirectory, true);
}
await CheckAndExtractBackend();
}
public async Task DownloadModels()
{
BackendSetupMainStatus = "Downloading MangaJaNai Models...";
var download = "https://github.com/the-database/mangajanai/releases/download/1.0.0/MangaJaNai_V1_ModelsOnly.zip";
var targetPath = Path.Join(_pythonService.ModelsDirectory, "mangajanai.zip");
Directory.CreateDirectory(_pythonService.ModelsDirectory);
await Downloader.DownloadFileAsync(download, targetPath, (progress) =>
{
BackendSetupMainStatus = $"Downloading MangaJaNai Models ({progress}%)...";
});
BackendSetupMainStatus = "Extracting MangaJaNai Models...";
_pythonService.ExtractZip(targetPath, _pythonService.ModelsDirectory, (double progress) =>
{
BackendSetupMainStatus = $"Extracting MangaJaNai Models ({progress}%)...";
});
File.Delete(targetPath);
BackendSetupMainStatus = "Downloading IllustrationJaNai V3denoise Models...";
download = "https://github.com/the-database/MangaJaNai/releases/download/3.0.0/IllustrationJaNai_V3denoise.zip";
targetPath = Path.Join(_pythonService.ModelsDirectory, "illustrationjanai.zip");
await Downloader.DownloadFileAsync(download, targetPath, (progress) =>
{
BackendSetupMainStatus = $"Downloading IllustrationJaNai V3denoise Models ({progress}%)...";
});
BackendSetupMainStatus = "Extracting IllustrationJaNai V3denoise Models...";
_pythonService.ExtractZip(targetPath, _pythonService.ModelsDirectory, (double progress) =>
{
BackendSetupMainStatus = $"Extracting IllustrationJaNai V3denoise Models ({progress}%)...";
});
File.Delete(targetPath);
BackendSetupMainStatus = "Downloading IllustrationJaNai V3detail Models...";
download = "https://github.com/the-database/MangaJaNai/releases/download/3.0.0/IllustrationJaNai_V3detail.zip";
targetPath = Path.Join(_pythonService.ModelsDirectory, "illustrationjanai.zip");
await Downloader.DownloadFileAsync(download, targetPath, (progress) =>
{
BackendSetupMainStatus = $"Downloading IllustrationJaNai V3detail Models ({progress}%)...";
});
BackendSetupMainStatus = "Extracting IllustrationJaNai V3detail Models...";
_pythonService.ExtractZip(targetPath, _pythonService.ModelsDirectory, (double progress) =>
{
BackendSetupMainStatus = $"Extracting IllustrationJaNai V3detail Models ({progress}%)...";
});
File.Delete(targetPath);
}
public async Task InstallUpdatePythonDependencies()
{
var cmd = _pythonService.InstallUpdatePythonDependenciesCommand;
Debug.WriteLine(cmd);
// Create a new process to run the CMD command
using (var process = new Process())
{
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = @$"/C {cmd}";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
process.StartInfo.WorkingDirectory = _pythonService.BackendDirectory;
var result = string.Empty;
using var outputFile = new StreamWriter(Path.Combine(_pythonService.LogsDirectory, "install.log"));
outputFile.WriteLine($"Working Directory: {process.StartInfo.WorkingDirectory}");
outputFile.WriteLine($"Run Command: {cmd}");
// Create a StreamWriter to write the output to a log file
try
{
//using var outputFile = new StreamWriter("error.log", append: true);
process.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
//Debug.WriteLine($"STDERR = {e.Data}");
outputFile.WriteLine(e.Data);
BackendSetupSubStatusQueueEnqueue(e.Data);
}
};
process.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
result = e.Data;
outputFile.WriteLine(e.Data);
//Debug.WriteLine($"STDOUT = {e.Data}");
BackendSetupSubStatusQueueEnqueue(e.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine(); // Start asynchronous reading of the output
await process.WaitForExitAsync();
}
catch (IOException) { }
}
return [];
}
public void CheckAndDoBackup()
{
Task.Run(() =>
{
try
{
if (!File.Exists(_pythonService.AppStatePath))
return;
var files = Directory.EnumerateFiles(_pythonService.AppStateFolder)
.Where(f =>
{
var name = Path.GetFileName(f);
return name.StartsWith("autobackup_") &&
name.EndsWith(_pythonService.AppStateFilename);
})
.OrderByDescending(f => f)
.ToList();
var latestBackup = files.FirstOrDefault();
if (latestBackup is not null &&
FilesAreEqual(_pythonService.AppStatePath, latestBackup))
{
return;
}
var backupName =
$"autobackup_{DateTime.Now:yyyyMMdd-HHmmss}_{_pythonService.AppStateFilename}";
var backupPath = Path.Combine(_pythonService.AppStateFolder, backupName);
File.Copy(_pythonService.AppStatePath, backupPath);
files.Insert(0, backupPath);
const int maxBackups = 10;
if (files.Count > maxBackups)
{
foreach (var old in files.Skip(maxBackups))
{
try { File.Delete(old); }
catch { }
}
}
}
catch
{
}
});
}
private static bool FilesAreEqual(string path1, string path2)
{
var info1 = new FileInfo(path1);
var info2 = new FileInfo(path2);
if (info1.Length != info2.Length)
return false;
var bytes1 = File.ReadAllBytes(path1);
var bytes2 = File.ReadAllBytes(path2);
return bytes1.AsSpan().SequenceEqual(bytes2);
}
public void ResetCurrentWorkflow()
{
if (CurrentWorkflow != null)
{
var workflow = JsonConvert.DeserializeObject(DEFAULT_WORKFLOW, NewtonsoftJsonSuspensionDriver.Settings);
var workflowIndex = CurrentWorkflow.WorkflowIndex;
var workflowName = $"Custom Workflow {workflowIndex}";
if (workflow != null)
{
var defaultWorkflow = new UpscaleWorkflow
{
Vm = this,
WorkflowIndex = workflowIndex,
WorkflowName = workflowName,
Chains = workflow.Chains
};
foreach (var chain in defaultWorkflow.Chains)
{
chain.Vm = this;
}
CurrentWorkflow = defaultWorkflow;
}
}
}
public async Task> PopulateDevicesAsync(string? searchText, CancellationToken cancellationToken)
{
try
{
var requestUrl = $"https://animejan.ai/mangajanai/api/search?q={Uri.EscapeDataString(searchText?.Trim() ?? "")}&p=0&s=4";
if (string.IsNullOrWhiteSpace(searchText))
{
requestUrl = $"https://animejan.ai/mangajanai/api/top";
}
var response = await client.GetStringAsync(requestUrl, cancellationToken);
var devices = JsonConvert.DeserializeObject>(response, NewtonsoftJsonSuspensionDriver.Settings);
if (devices != null)
{
foreach (var device in devices)
{
DisplayDeviceMap[device.ToString()] = device;
}
return devices.ToList();
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
return [];
}
public async Task CheckForUpdates()
{
try
{
if (_updateManagerService.IsInstalled)
{
await Task.Run(async () =>
{
_update = await _updateManagerService.CheckForUpdatesAsync().ConfigureAwait(true);
});
UpdateStatus();
if (AutoUpdateEnabled)
{
await DownloadUpdate();
}
}
}
catch (Exception ex)
{
UpdateStatusText = $"Check for update failed: {ex.Message}";
}
}
public async Task DownloadUpdate()
{
try
{
if (_update != null)
{
ShowDownloadButton = false;
await _updateManagerService.DownloadUpdatesAsync(_update, Progress).ConfigureAwait(true);
UpdateStatus();
}
}
catch
{
}
}
public void ApplyUpdate()
{
if (_update != null)
{
ShowApplyButton = false;
_updateManagerService.ApplyUpdatesAndRestart(_update);
}
}
private void UpdateStatus()
{
ShowDownloadButton = false;
ShowApplyButton = false;
ShowCheckUpdateButton = true;
if (_update != null)
{
UpdateStatusText = $"Update is available: {_update.TargetFullRelease.Version}";
ShowDownloadButton = true;
ShowCheckUpdateButton = false;
if (_updateManagerService.IsUpdatePendingRestart)
{
UpdateStatusText = $"Update ready, pending restart to install version: {_update.TargetFullRelease.Version}";
ShowDownloadButton = false;
ShowApplyButton = true;
ShowCheckUpdateButton = false;
}
else
{
}
}
else
{
UpdateStatusText = "No updates found";
}
}
private void Progress(int percent)
{
UpdateStatusText = $"Downloading update {_update?.TargetFullRelease.Version} ({percent}%)...";
}
public async void OpenModelsDirectory()
{
await Task.Run(() =>
{
Process.Start("explorer.exe", _pythonService.ModelsDirectory);
});
}
}
[DataContract]
public class UpscaleWorkflow : ReactiveObject
{
public UpscaleWorkflow()
{
var g1 = this.WhenAnyValue
(
x => x.InputFilePath,
x => x.OutputFilename,
x => x.InputFolderPath,
x => x.OutputFolderPath,
x => x.SelectedTabIndex,
x => x.DisplayDevice,
x => x.DisplayPortraitSelected
);
var g2 = this.WhenAnyValue
(
x => x.UpscaleImages,
x => x.UpscaleArchives,
x => x.OverwriteExistingFiles,
x => x.WebpSelected,
x => x.PngSelected,
x => x.JpegSelected,
x => x.AvifSelected
);
var g3 = this.WhenAnyValue
(
x => x.ModeFitToDisplaySelected,
x => x.ModeHeightSelected,
x => x.ModeWidthSelected,
x => x.ResizeHeightAfterUpscale,
x => x.ResizeWidthAfterUpscale
);
g1.CombineLatest(g2).CombineLatest(g3).Subscribe(x =>
{
Validate();
});
this.WhenAnyValue(x => x.Vm).Subscribe(x =>
{
sub?.Dispose();
sub = Vm.WhenAnyValue(
x => x.SelectedWorkflowIndex,
x => x.RequestShowAppSettings
).Subscribe(x =>
{
this.RaisePropertyChanged(nameof(ActiveWorkflow));
Vm?.RaisePropertyChanged("Workflows");
});
});
this.WhenAnyValue(x => x.InputFilePath).Subscribe(x =>
{
if (string.IsNullOrWhiteSpace(OutputFolderPath) && !string.IsNullOrWhiteSpace(InputFilePath))
{
try
{
OutputFolderPath = Directory.GetParent(InputFilePath)?.ToString() ?? "";
}
catch (Exception)
{
}
}
});
this.WhenAnyValue(x => x.InputFolderPath).Subscribe(x =>
{
if (string.IsNullOrWhiteSpace(OutputFolderPath) && !string.IsNullOrWhiteSpace(InputFolderPath))
{
try
{
OutputFolderPath = $"{InputFolderPath} mangajanai";
}
catch (Exception)
{
}
}
});
}
private IDisposable? sub;
private MainWindowViewModel? _vm;
public MainWindowViewModel? Vm
{
get => _vm;
set => this.RaiseAndSetIfChanged(ref _vm, value);
}
private string _workflowName;
[DataMember]
public string WorkflowName
{
get => _workflowName;
set => this.RaiseAndSetIfChanged(ref _workflowName, value);
}
private int _workflowIndex;
[DataMember]
public int WorkflowIndex
{
get => _workflowIndex;
set => this.RaiseAndSetIfChanged(ref _workflowIndex, value);
}
public string WorkflowIcon => $"Numeric{WorkflowIndex}Circle";
public bool ActiveWorkflow
{
get
{
Debug.WriteLine($"ActiveWorkflow {WorkflowIndex} == {Vm?.SelectedWorkflowIndex}; {Vm == null}");
return WorkflowIndex == Vm?.SelectedWorkflowIndex && (!Vm?.ShowAppSettings ?? false);
}
}
public bool IsDefaultWorkflow => WorkflowIndex == 0;
private int _selectedTabIndex;
[DataMember]
public int SelectedTabIndex
{
get => _selectedTabIndex;
set
{
if (_selectedTabIndex != value)
{
this.RaiseAndSetIfChanged(ref _selectedTabIndex, value);
Vm?.RaisePropertyChanged(nameof(Vm.InputStatusText)); // TODO
}
}
}
private string _inputFilePath = string.Empty;
[DataMember]
public string InputFilePath
{
get => _inputFilePath;
set
{
this.RaiseAndSetIfChanged(ref _inputFilePath, value);
Vm?.RaisePropertyChanged(nameof(Vm.InputStatusText)); // TODO
}
}
private string _inputFolderPath = string.Empty;
[DataMember]
public string InputFolderPath
{
get => _inputFolderPath;
set
{
this.RaiseAndSetIfChanged(ref _inputFolderPath, value);
Vm?.RaisePropertyChanged(nameof(Vm.InputStatusText)); // TODO
}
}
private string _outputFilename = "%filename%-mangajanai";
[DataMember]
public string OutputFilename
{
get => _outputFilename;
set => this.RaiseAndSetIfChanged(ref _outputFilename, value);
}
private string _outputFolderPath = string.Empty;
[DataMember]
public string OutputFolderPath
{
get => _outputFolderPath;
set => this.RaiseAndSetIfChanged(ref _outputFolderPath, value);
}
private bool _overwriteExistingFiles = false;
[DataMember]
public bool OverwriteExistingFiles
{
get => _overwriteExistingFiles;
set => this.RaiseAndSetIfChanged(ref _overwriteExistingFiles, value);
}
private bool _upscaleImages = false;
[DataMember]
public bool UpscaleImages
{
get => _upscaleImages;
set => this.RaiseAndSetIfChanged(ref _upscaleImages, value);
}
private bool _upscaleArchives = true;
[DataMember]
public bool UpscaleArchives
{
get => _upscaleArchives;
set => this.RaiseAndSetIfChanged(ref _upscaleArchives, value);
}
private int? _resizeHeightAfterUpscale = 2160;
[DataMember]
public int? ResizeHeightAfterUpscale
{
get => _resizeHeightAfterUpscale;
set => this.RaiseAndSetIfChanged(ref _resizeHeightAfterUpscale, value ?? 2160);
}
private int? _resizeWidthAfterUpscale = 3840;
[DataMember]
public int? ResizeWidthAfterUpscale
{
get => _resizeWidthAfterUpscale;
set => this.RaiseAndSetIfChanged(ref _resizeWidthAfterUpscale, value ?? 3840);
}
private bool _webpSelected = true;
[DataMember]
public bool WebpSelected
{
get => _webpSelected;
set
{
this.RaiseAndSetIfChanged(ref _webpSelected, value);
this.RaisePropertyChanged(nameof(ShowUseLosslessCompression));
this.RaisePropertyChanged(nameof(ShowLossyCompressionQuality));
}
}
private bool _avifSelected = false;
[DataMember]
public bool AvifSelected
{
get => _avifSelected;
set
{
this.RaiseAndSetIfChanged(ref _avifSelected, value);
this.RaisePropertyChanged(nameof(ShowLossyCompressionQuality));
this.RaisePropertyChanged(nameof(ShowUseLosslessCompression));
}
}
private bool _pngSelected = false;
[DataMember]
public bool PngSelected
{
get => _pngSelected;
set
{
this.RaiseAndSetIfChanged(ref _pngSelected, value);
}
}
private bool _jpegSelected = false;
[DataMember]
public bool JpegSelected
{
get => _jpegSelected;
set
{
this.RaiseAndSetIfChanged(ref _jpegSelected, value);
this.RaisePropertyChanged(nameof(ShowLossyCompressionQuality));
}
}
public string ImageFormat => WebpSelected ? "webp" : PngSelected ? "png" : AvifSelected ? "avif" : "jpg";
public bool ShowUseLosslessCompression => WebpSelected;
private bool _useLosslessCompression = false;
[DataMember]
public bool UseLosslessCompression
{
get => _useLosslessCompression;
set
{
this.RaiseAndSetIfChanged(ref _useLosslessCompression, value);
this.RaisePropertyChanged(nameof(ShowLossyCompressionQuality));
}
}
public bool ShowLossyCompressionQuality => JpegSelected || (WebpSelected && !UseLosslessCompression) || AvifSelected;
private int? _lossyCompressionQuality = 80;
[DataMember]
public int? LossyCompressionQuality
{
get => _lossyCompressionQuality;
set => this.RaiseAndSetIfChanged(ref _lossyCompressionQuality, value ?? 80);
}
private bool _showLossySettings = true;
[DataMember]
public bool ShowLossySettings
{
get => _showLossySettings;
set => this.RaiseAndSetIfChanged(ref _showLossySettings, value);
}
private bool _modeScaleSelected = true;
[DataMember]
public bool ModeScaleSelected
{
get => _modeScaleSelected;
set
{
this.RaiseAndSetIfChanged(ref _modeScaleSelected, value);
}
}
private int _upscaleScaleFactor = 4;
[DataMember]
public int UpscaleScaleFactor
{
get => _upscaleScaleFactor;
set
{
this.RaiseAndSetIfChanged(ref _upscaleScaleFactor, value);
this.RaisePropertyChanged(nameof(Is1x));
this.RaisePropertyChanged(nameof(Is2x));
this.RaisePropertyChanged(nameof(Is3x));
this.RaisePropertyChanged(nameof(Is4x));
}
}
public bool Is1x => UpscaleScaleFactor == 1;
public bool Is2x => UpscaleScaleFactor == 2;
public bool Is3x => UpscaleScaleFactor == 3;
public bool Is4x => UpscaleScaleFactor == 4;
public void SetUpscaleScaleFactor(int scaleFactor)
{
UpscaleScaleFactor = scaleFactor;
}
private bool _modeWidthSelected = false;
[DataMember]
public bool ModeWidthSelected
{
get => _modeWidthSelected;
set
{
this.RaiseAndSetIfChanged(ref _modeWidthSelected, value);
}
}
private bool _modeHeightSelected = false;
[DataMember]
public bool ModeHeightSelected
{
get => _modeHeightSelected;
set
{
this.RaiseAndSetIfChanged(ref _modeHeightSelected, value);
}
}
private bool _modeFitToDisplaySelected = false;
[DataMember]
public bool ModeFitToDisplaySelected
{
get => _modeFitToDisplaySelected;
set
{
this.RaiseAndSetIfChanged(ref _modeFitToDisplaySelected, value);
}
}
private string _displayDevice;
[DataMember]
public string DisplayDevice
{
get => _displayDevice;
set
{
this.RaiseAndSetIfChanged(ref _displayDevice, value);
this.RaisePropertyChanged(nameof(DisplayDeviceWidth));
this.RaisePropertyChanged(nameof(DisplayDeviceHeight));
}
}
[DataMember]
public int DisplayDeviceWidth
{
get
{
if (Vm != null && DisplayDevice != null)
{
Vm.DisplayDeviceMap.TryGetValue(DisplayDevice, out var displayDevice);
if (displayDevice != null)
{
return DisplayPortraitSelected ? displayDevice.Width : displayDevice.Height;
}
}
return 0;
}
}
[DataMember]
public int DisplayDeviceHeight
{
get
{
if (Vm != null && DisplayDevice != null)
{
Vm.DisplayDeviceMap.TryGetValue(DisplayDevice, out var displayDevice);
if (displayDevice != null)
{
return DisplayPortraitSelected ? displayDevice.Height : displayDevice.Width;
}
}
return 0;
}
}
private bool _displayPortraitSelected = true;
[DataMember]
public bool DisplayPortraitSelected
{
get => _displayPortraitSelected;
set
{
this.RaiseAndSetIfChanged(ref _displayPortraitSelected, value);
this.RaisePropertyChanged(nameof(DisplayDeviceWidth));
this.RaisePropertyChanged(nameof(DisplayDeviceHeight));
}
}
private bool _showAdvancedSettings = false;
[DataMember]
public bool ShowAdvancedSettings
{
get => _showAdvancedSettings;
set => this.RaiseAndSetIfChanged(ref _showAdvancedSettings, value);
}
private int _grayscaleDetectionThreshold = 12;
[DataMember]
public int GrayscaleDetectionThreshold
{
get => _grayscaleDetectionThreshold;
set => this.RaiseAndSetIfChanged(ref _grayscaleDetectionThreshold, value);
}
private AvaloniaList _chains;
[DataMember]
public AvaloniaList Chains
{
get => _chains;
set => this.RaiseAndSetIfChanged(ref _chains, value);
}
private bool _valid = false;
[IgnoreDataMember]
public bool Valid
{
get => _valid;
set
{
this.RaiseAndSetIfChanged(ref _valid, value);
if (Vm != null)
{
Vm.RaisePropertyChanged(nameof(Vm.UpscaleEnabled)); // TODO
Vm.RaisePropertyChanged(nameof(Vm.LeftStatus)); // TODO
}
}
}
public void SetWebpSelected()
{
WebpSelected = true;
PngSelected = false;
JpegSelected = false;
AvifSelected = false;
}
public void SetPngSelected()
{
PngSelected = true;
WebpSelected = false;
JpegSelected = false;
AvifSelected = false;
}
public void SetJpegSelected()
{
JpegSelected = true;
WebpSelected = false;
PngSelected = false;
AvifSelected = false;
}
public void SetAvifSelected()
{
AvifSelected = true;
JpegSelected = false;
WebpSelected = false;
PngSelected = false;
}
public void SetModeScaleSelected()
{
ModeScaleSelected = true;
ModeWidthSelected = false;
ModeHeightSelected = false;
ModeFitToDisplaySelected = false;
}
public void SetModeWidthSelected()
{
ModeWidthSelected = true;
ModeScaleSelected = false;
ModeHeightSelected = false;
ModeFitToDisplaySelected = false;
}
public void SetModeHeightSelected()
{
ModeHeightSelected = true;
ModeScaleSelected = false;
ModeWidthSelected = false;
ModeFitToDisplaySelected = false;
}
public void SetModeFitToDisplaySelected()
{
ModeFitToDisplaySelected = true;
ModeHeightSelected = false;
ModeWidthSelected = false;
ModeScaleSelected = false;
}
public void Validate()
{
var valid = true;
var validationText = new List();
if (SelectedTabIndex == 0)
{
if (string.IsNullOrWhiteSpace(InputFilePath))
{
valid = false;
validationText.Add("Input File is required.");
}
else if (!File.Exists(InputFilePath))
{
valid = false;
validationText.Add("Input File does not exist.");
}
}
else
{
if (string.IsNullOrWhiteSpace(InputFolderPath))
{
valid = false;
validationText.Add("Input Folder is required.");
}
else if (!Directory.Exists(InputFolderPath))
{
valid = false;
validationText.Add("Input Folder does not exist.");
}
}
if (string.IsNullOrWhiteSpace(OutputFilename))
{
valid = false;
validationText.Add("Output Filename is required.");
}
if (string.IsNullOrWhiteSpace(OutputFolderPath))
{
valid = false;
validationText.Add("Output Folder is required.");
}
if (ModeHeightSelected && ResizeHeightAfterUpscale == 0)
{
valid = false;
validationText.Add("Output Height is invalid. Enter a height larger than 0.");
}
if (ModeWidthSelected && ResizeWidthAfterUpscale == 0)
{
valid = false;
validationText.Add("Output Width is invalid. Enter a width larger than 0.");
}
if (ModeFitToDisplaySelected && (DisplayDeviceWidth == 0 || DisplayDeviceHeight == 0))
{
valid = false;
validationText.Add("Tablet Device or Display is invalid. Please make a selection from the list of options.");
}
Valid = valid;
if (Vm != null)
{
// TODO
Vm.CheckInputs();
if (Vm?.ProgressTotalFiles == 0)
{
Valid = false;
validationText.Add($"{Vm?.InputStatusText} selected for upscaling. At least one file must be selected.");
}
Vm.ValidationText = string.Join("\n", validationText);
}
}
}
[DataContract]
public class UpscaleChain : ReactiveObject
{
IPythonService _pythonService;
public UpscaleChain(IPythonService? pythonService = null)
{
_pythonService = pythonService ?? Locator.Current.GetService()!;
this.WhenAnyValue(x => x.Vm).Subscribe(x =>
{
sub?.Dispose();
sub = Vm.WhenAnyValue(
x => x.IsExtractingBackend
).Subscribe(x =>
{
this.RaisePropertyChanged(nameof(AllModels));
this.RaisePropertyChanged(nameof(ModelFilePath));
});
});
this.RaisePropertyChanged(nameof(AllModels));
this.RaisePropertyChanged(nameof(ModelFilePath));
}
private IDisposable? sub;
private MainWindowViewModel? _vm;
public MainWindowViewModel? Vm
{
get => _vm;
set => this.RaiseAndSetIfChanged(ref _vm, value);
}
private string _chainNumber = string.Empty;
[DataMember]
public string ChainNumber
{
get => _chainNumber;
set => this.RaiseAndSetIfChanged(ref _chainNumber, value);
}
private string _minResolution = "0x0";
[DataMember]
public string MinResolution
{
get => _minResolution;
set => this.RaiseAndSetIfChanged(ref _minResolution, value);
}
private string _maxResolution = "0x0";
[DataMember]
public string MaxResolution
{
get => _maxResolution;
set => this.RaiseAndSetIfChanged(ref _maxResolution, value);
}
private bool _isGrayscale = false;
[DataMember]
public bool IsGrayscale
{
get => _isGrayscale;
set => this.RaiseAndSetIfChanged(ref _isGrayscale, value);
}
private bool _isColor = false;
[DataMember]
public bool IsColor
{
get => _isColor;
set => this.RaiseAndSetIfChanged(ref _isColor, value);
}
private int? _minScaleFactor = 0;
[DataMember]
public int? MinScaleFactor
{
get => _minScaleFactor;
set => this.RaiseAndSetIfChanged(ref _minScaleFactor, value ?? 0);
}
private int? _maxScaleFactor = 0;
[DataMember]
public int? MaxScaleFactor
{
get => _maxScaleFactor;
set => this.RaiseAndSetIfChanged(ref _maxScaleFactor, value ?? 0);
}
private string _modelFilePath = string.Empty;
[DataMember]
public string ModelFilePath
{
get => _modelFilePath;
set => this.RaiseAndSetIfChanged(ref _modelFilePath, value);
}
private string _modelTileSize = "Auto (Estimate)";
[DataMember]
public string ModelTileSize
{
get => _modelTileSize;
set => this.RaiseAndSetIfChanged(ref _modelTileSize, value);
}
private bool _autoAdjustLevels = false;
[DataMember]
public bool AutoAdjustLevels
{
get => _autoAdjustLevels;
set => this.RaiseAndSetIfChanged(ref _autoAdjustLevels, value);
}
private int? _resizeHeightBeforeUpscale = 0;
[DataMember]
public int? ResizeHeightBeforeUpscale
{
get => _resizeHeightBeforeUpscale;
set => this.RaiseAndSetIfChanged(ref _resizeHeightBeforeUpscale, value ?? 0);
}
private int? _resizeWidthBeforeUpscale = 0;
[DataMember]
public int? ResizeWidthBeforeUpscale
{
get => _resizeWidthBeforeUpscale;
set => this.RaiseAndSetIfChanged(ref _resizeWidthBeforeUpscale, value ?? 0);
}
private double? _resizeFactorBeforeUpscale = 100;
[DataMember]
public double? ResizeFactorBeforeUpscale
{
get => _resizeFactorBeforeUpscale;
set => this.RaiseAndSetIfChanged(ref _resizeFactorBeforeUpscale, value ?? 100);
}
public AvaloniaList AllModels => _pythonService.AllModels;
private string[] _tileSizes = [
"Auto (Estimate)",
"Maximum",
"No Tiling",
"128",
"192",
"256",
"384",
"512",
"768",
"1024",
"2048",
"4096"];
public string[] TileSizes
{
get => _tileSizes;
set => this.RaiseAndSetIfChanged(ref _tileSizes, value);
}
}
// TODO refactor into separate file
public class ReaderDevice
{
public string Name { get; set; } = default!;
public string Brand { get; set; } = default!;
public string Year { get; set; } = default!;
public int Width { get; set; } = default!;
public int Height { get; set; } = default!;
public override string ToString()
{
List parts = [];
if (!string.IsNullOrWhiteSpace(Brand))
{
parts.Add(Brand);
}
if (!string.IsNullOrWhiteSpace(Name))
{
parts.Add(Name);
}
if (!string.IsNullOrWhiteSpace(Year))
{
parts.Add($"({Year})");
}
return string.Join(" ", parts);
}
}
public class DeviceResponse
{
[JsonProperty("all_devices")]
public List AllDevices { get; set; } = [];
[JsonProperty("best_device")]
public int BestDevice { get; set; }
}
public class AcceleratorDevice
{
[JsonProperty("type")]
public string Type { get; set; } = string.Empty;
[JsonProperty("index")]
public int Index { get; set; }
[JsonProperty("name")]
public string Name { get; set; } = string.Empty;
[JsonProperty("device_string")]
public string DeviceString { get; set; } = string.Empty;
[JsonProperty("supports_fp16")]
public bool SupportsFp16 { get; set; }
[JsonProperty("supports_bf16")]
public bool SupportsBf16 { get; set; }
[JsonProperty("memory_total")]
public long? MemoryTotal { get; set; }
[JsonProperty("memory_free")]
public long? MemoryFree { get; set; }
}
}
================================================
FILE: MangaJaNaiConverterGui/ViewModels/ViewModelBase.cs
================================================
using ReactiveUI;
namespace MangaJaNaiConverterGui.ViewModels
{
//[DataContract]
public class ViewModelBase : ReactiveObject
{
//private bool _autoUpdate;
//[DataMember]
//public bool AutoUpdateEnabled
//{
// get => _autoUpdate;
// set => this.RaiseAndSetIfChanged(ref _autoUpdate, value);
//}
}
}
================================================
FILE: MangaJaNaiConverterGui/Views/MainWindow.axaml
================================================
App Settings
Default Workflows
Custom Workflows
Upscale
Cancel
Console
Workflow Name
Import Workflow
Export Workflow
Reset Workflow
Single File Upscale
Input File
Path of the image or archive file (such as zip or cbz) to upscale. If an archive file is selected, each image in the archive will be upscaled and saved to a new archive.
Batch Folder Upscale
Input Folder
Path of the folder to upscale. The folder and all of its subfolders will be scanned and images and/or archives will be upscaled depending on the selection below.
Upscale Archives
Upscale Images
Whether to upscale Image files (*.png, *.jpg, *.jpeg, *.webp, *.bmp) and/or Archive files (*.zip, *.cbz, *.rar, *.cbr) in the selected Input Folder.
Output Folder
Path of the folder to save the upscaled image(s) or archive(s).
Output Filename
The filename of the upscaled image(s) or archive(s), without the file extension. %filename% is the input filename without extension. Archives will be output with .cbz extension; images will be output with the extension of the image format selected below.
Allow Files in Output Path to be Overwritten
If unchecked, upscaling will be skipped for files that already exist in the output path. If checked, any files that already exist in the output path will be overwritten without warning. Use with caution.
Output Image Format
WebP : Modern format recommended for good quality and efficient filesize compression, with good compatibility on modern devices. Supports lossless and lossy compression.
AVIF : Modern format with better lossy compression efficiency than WebP, but not as widely supported and slower to save and load compared to WebP.
PNG : Lossless compressed format with excellent compatibility, but worse compression efficiency than WebP and AVIF.
JPEG : Lossy compressed format with excellent compatibility, but worse compression efficiency than WebP and AVIF.
Use Lossless Compression
Use lossless compression. Usually not recommended due to producing images with much larger filesize with little visual benefit.
Lossy Compression Quality
%
Quality level for compression. Note that a quality level of 100 is still lossy.
Upscale Mode
Scale : All images will be upscaled by the specified scaling factor. For example, a factor of 2x will double the width and height of each image.
Width : All images will be upscaled to the specified width, while maintaining aspect ratio of the image.
Height : All images will be upscaled to the specified height, while maintaining aspect ratio of the image.
Fit to Display : All images will be upscaled to fit exactly within the specified display device.
Scale Factor
All images will be upscaled by the specified scaling factor. For example, a factor of 2x will double the width and height of each image.
Output Height
px
All images will be upscaled to this height. For reading on tablets, this can be used to set the image height to the resolution of your tablet's display.
Output Width
px
All images will be upscaled to this width. For reading on tablets, this can be used to set the image width to the resolution of your tablet's display.
Tablet Device or Display
The name of the tablet or display device. Start typing to show more options.
Display Orientation
Whether the display will be used in portrait/vertical mode or landscape/horizontal mode.
Display Resolution
Actual resolution of the selected display with the selected orientation, width × height.
The threshold for which an image is considered grayscale. The default value of 12 considers images with slight color as grayscale, because some grayscale images have slight color due to artifacts. Set the threshold to 0 to consider only strictly grayscale images as grayscale, and increase the threshold to consider images with more color as grayscale.
Resolution Range
px
-
px
Range of image resolutions to activate this chain. Select a common resolution from the drop down or type a custom resolution. Larger resolutions require more processing power to upscale. A dimension value of 0 means any value for that dimension, for example 0x1250 means any width, and 1250px tall. A maximum resolution of 0x0 means no resolution limit.
Scaling Factor Range
x
-
x
Range of necessary scaling factor to activate this chain. A maximum scaling factor of 0 means no maximum limit.
Is Color Image
Is Grayscale Image
Whether the image is color and/or grayscale. Images that appear grayscale but have faint color due to JPEG artifacts are still considered grayscale.
Auto Adjust Levels on Grayscale Images
If checked, automatically increase the contrast of all grayscale images if necessary. For best results with the MangaJaNai grayscale model, this setting is recommend when upscaling images which appear to be faded. This will have no effect on color images or grayscale images with sufficient contrast.
Resize Height Before Upscale
px
Resize each image to this height before upscaling, set to 0 to disable.
Resize Width Before Upscale
px
Resize each image to this width before upscaling, set to 0 to disable.
Resize Factor Before Upscale
%
Resize each image by this factor before upscaling. This setting is ignored if Resize Height Before Upscale is specified.
Model
The upscaling model to run. To choose from more models, add PyTorch (*.pth) model files to the models directory. Select No Model to skip running any upscaling model for this chain.
Model Tile Size
px
Tile size to use when upscaling images with the selected model. The image is cut into tiles in order to upscale without running into the VRAM limits of your GPU. Larger is better when the GPU has enough VRAM to support it. The auto setting estimates the largest tile size which can be used based on available VRAM and is recommended for most users.
Add Chain
A chain is a a set of upscale settings which can be activated based on conditions such as the image resolution and whether the image is color or grayscale.
This allows you to specify different upscale models for different types of images.
Auto Update
Whether to automatically check for and install app updates.
Check for Updates Now
Download Update
Restart to Update MangaJaNaiConverterGui
App is not installed; auto update settings unavailable.
Device
Which device to use for upscaling with PyTorch. CPU is much slower than GPU and should be avoided unless no GPU is available.
FP16 Mode
Runs PyTorch upscaling in FP16 mode for less VRAM usage and speedup on RTX GPUs.
Reinstall the Python environment. Try this if you are having any problems getting upscales to work. This process may take several minutes.
Return
================================================
FILE: MangaJaNaiConverterGui/Views/MainWindow.axaml.cs
================================================
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Windowing;
using MangaJaNaiConverterGui.ViewModels;
using Material.Icons.Avalonia;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace MangaJaNaiConverterGui.Views
{
public partial class MainWindow : AppWindow
{
private bool _autoScrollConsole = true;
private bool _userWantsToQuit = false;
public MainWindow()
{
AvaloniaXamlLoader.Load(this);
Resized += MainWindow_Resized;
Closing += MainWindow_Closing;
Opened += MainWindow_Opened;
var inputFileNameTextBox = this.FindControl("InputFileNameTextBox");
var outputFileNameTextBox = this.FindControl("OutputFileNameTextBox");
var inputFolderNameTextBox = this.FindControl("InputFolderNameTextBox");
var outputFolderNameTextBox = this.FindControl("OutputFolderNameTextBox");
var grayscaleModelFilePathTextBox = this.FindControl("GrayscaleModelFilePathTextBox");
var colorModelFilePathTextBox = this.FindControl("ColorModelFilePathTextBox");
inputFileNameTextBox?.AddHandler(DragDrop.DropEvent, SetInputFilePath);
inputFolderNameTextBox?.AddHandler(DragDrop.DropEvent, SetInputFolderPath);
outputFolderNameTextBox?.AddHandler(DragDrop.DropEvent, SetOutputFolderPath);
}
private async void MainWindow_Opened(object? sender, EventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
vm.CheckAndExtractBackend();
}
}
private async void MainWindow_Closing(object? sender, WindowClosingEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
// Show confirmation dialog
if (!_userWantsToQuit && vm.Upscaling)
{
// Cancel close to show dialog
e.Cancel = true;
_userWantsToQuit = await ShowConfirmationDialog("Cancel unfinished upscales?", "If you exit now, all unfinished upscales will be canceled. Are you sure you want to exit?");
// Close if the user confirmed
if (_userWantsToQuit)
{
vm.CancelUpscale();
Close();
}
}
else
{
}
}
}
private void ConsoleScrollViewer_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property.Name == "Offset" && sender is ScrollViewer consoleScrollViewer)
{
if (e.NewValue is Vector newVector)
{
_autoScrollConsole = newVector.Y == consoleScrollViewer?.ScrollBarMaximum.Y;
}
}
}
private void ConsoleTextBlock_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property.Name == "Text" && sender is TextBlock textBlock)
{
if (textBlock.Parent is ScrollViewer consoleScrollViewer)
{
if (consoleScrollViewer != null)
{
if (_autoScrollConsole)
{
consoleScrollViewer.ScrollToEnd();
}
}
}
}
}
private void MainWindow_Resized(object? sender, WindowResizedEventArgs e)
{
// Set the ScrollViewer width based on the new parent window's width
var consoleScrollViewer = this.FindControl("ConsoleScrollViewer");
if (consoleScrollViewer != null)
{
consoleScrollViewer.Width = Width - 340; // Adjust the width as needed
}
}
public void SetInputFilePath(object? sender, DragEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
var files = e.Data.GetFiles().ToList();
if (files.Count > 0)
{
var filePath = files[0].TryGetLocalPath();
if (File.Exists(filePath))
{
vm.CurrentWorkflow.InputFilePath = filePath;
}
}
}
}
public void SetInputFolderPath(object? sender, DragEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
var files = e.Data.GetFiles().ToList();
if (files.Count > 0)
{
var filePath = files[0].TryGetLocalPath();
if (Directory.Exists(filePath))
{
vm.CurrentWorkflow.InputFolderPath = filePath;
}
}
}
}
public void SetOutputFolderPath(object? sender, DragEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
var files = e.Data.GetFiles().ToList();
if (files.Count > 0)
{
var filePath = files[0].TryGetLocalPath();
if (Directory.Exists(filePath))
{
vm.CurrentWorkflow.OutputFolderPath = filePath;
}
}
}
}
private async void OpenInputFileButtonClick(object? sender, RoutedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
// Get top level from the current control. Alternatively, you can use Window reference instead.
var topLevel = TopLevel.GetTopLevel(this);
var storageProvider = topLevel.StorageProvider;
IStorageFolder? suggestedStartLocation = null;
var inputFolder = Path.GetDirectoryName(vm.CurrentWorkflow.InputFilePath);
if (Directory.Exists(inputFolder))
{
suggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(new Uri(inputFolder));
}
// Start async operation to open the dialog.
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Open Image or Archive File",
AllowMultiple = false,
FileTypeFilter = new FilePickerFileType[]
{
new("Image or Archive File") { Patterns = MainWindowViewModel.IMAGE_EXTENSIONS.Concat(MainWindowViewModel.ARCHIVE_EXTENSIONS).Select(x => $"*{x}").ToArray(),
MimeTypes = new[] { "*/*" } }, FilePickerFileTypes.All,
},
SuggestedStartLocation = suggestedStartLocation,
});
if (files.Count >= 1)
{
vm.CurrentWorkflow.InputFilePath = files[0].TryGetLocalPath() ?? "";
}
}
}
private async void OpenInputFolderButtonClick(object? sender, RoutedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
// Get top level from the current control. Alternatively, you can use Window reference instead.
var topLevel = GetTopLevel(this);
var storageProvider = topLevel.StorageProvider;
IStorageFolder? suggestedStartLocation = null;
if (Directory.Exists(vm.CurrentWorkflow.InputFolderPath))
{
suggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(new Uri(Path.GetFullPath(vm.CurrentWorkflow.InputFolderPath)));
}
// Start async operation to open the dialog.
var files = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Open Folder",
AllowMultiple = false,
SuggestedStartLocation = suggestedStartLocation
});
if (files.Count >= 1)
{
vm.CurrentWorkflow.InputFolderPath = files[0].TryGetLocalPath() ?? "";
}
}
}
private async void OpenOutputFolderButtonClick(object? sender, RoutedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
// Get top level from the current control. Alternatively, you can use Window reference instead.
var topLevel = GetTopLevel(this);
var storageProvider = topLevel.StorageProvider;
IStorageFolder? suggestedStartLocation = null;
if (Directory.Exists(vm.CurrentWorkflow.OutputFolderPath))
{
suggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(new Uri(Path.GetFullPath(vm.CurrentWorkflow.OutputFolderPath)));
}
// Start async operation to open the dialog.
var files = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Open Folder",
AllowMultiple = false,
SuggestedStartLocation = suggestedStartLocation
});
if (files.Count >= 1)
{
vm.CurrentWorkflow.OutputFolderPath = files[0].TryGetLocalPath() ?? "";
}
}
}
private async void ImportCurrentWorkflowButtonClick(object? sender, RoutedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
// Get top level from the current control. Alternatively, you can use Window reference instead.
var topLevel = GetTopLevel(this);
// Start async operation to open the dialog.
var storageProvider = topLevel.StorageProvider;
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Import Workflow File",
AllowMultiple = false,
FileTypeFilter = [new("MangaJaNai Workflow File") { Patterns = ["*.mwf"], MimeTypes = ["*/*"] }, FilePickerFileTypes.All],
SuggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)),
});
if (files.Count >= 1)
{
var inPath = files[0].TryGetLocalPath();
if (inPath != null)
{
var td = new TaskDialog
{
Title = "Confirm Workflow Import",
ShowProgressBar = false,
Content = $"The following workflow file will be imported to the current workflow {vm.CurrentWorkflow?.WorkflowName}. All configuration settings for the current profile {vm.CurrentWorkflow?.WorkflowName} will be overwritten.\n\n" +
inPath,
Buttons =
{
TaskDialogButton.OKButton,
TaskDialogButton.CancelButton
}
};
td.Closing += async (s, e) =>
{
if ((TaskDialogStandardResult)e.Result == TaskDialogStandardResult.OK)
{
var deferral = e.GetDeferral();
td.SetProgressBarState(0, TaskDialogProgressState.Indeterminate);
td.ShowProgressBar = true;
await Task.Run(() =>
{
vm.ReadWorkflowFileToCurrentWorkflow(inPath);
});
deferral.Complete();
}
};
td.XamlRoot = VisualRoot as Visual;
_ = await td.ShowAsync();
}
}
}
}
private async void ResetWorkflow(object? sender, RoutedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
var td = new TaskDialog
{
Title = "Confirm Workflow Reset",
ShowProgressBar = false,
Content = $"The current workflow's settings will be reset to the default settings. Any unsaved settings for the current workflow will be lost.",
Buttons =
{
TaskDialogButton.OKButton,
TaskDialogButton.CancelButton
}
};
td.Closing += async (s, e) =>
{
if ((TaskDialogStandardResult)e.Result == TaskDialogStandardResult.OK)
{
var deferral = e.GetDeferral();
td.SetProgressBarState(0, TaskDialogProgressState.Indeterminate);
td.ShowProgressBar = true;
await Task.Run(() =>
{
vm.ResetCurrentWorkflow();
});
deferral.Complete();
}
};
td.XamlRoot = VisualRoot as Visual;
_ = await td.ShowAsync();
}
}
private async void ExportCurrentWorkflowButtonClick(object? sender, RoutedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
// Get top level from the current control. Alternatively, you can use Window reference instead.
var topLevel = GetTopLevel(this);
var storageProvider = topLevel.StorageProvider;
// Start async operation to open the dialog.
var file = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Export Current Profile Conf File",
DefaultExtension = "conf",
FileTypeChoices =
[
new("MangaJaNai Workflow File (*.mwf)") { Patterns = ["*.mwf"] },
],
SuggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)),
SuggestedFileName = vm.CurrentWorkflow?.WorkflowName,
});
if (file is not null)
{
var outPath = file.TryGetLocalPath();
if (outPath != null)
{
vm.WriteCurrentWorkflowToFile(outPath);
}
}
}
}
private async void ReinstallBackendClick(object? sender, RoutedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
var confirm = await ShowConfirmationDialog("Reinstall Python Backend", "The existing Python backend will be removed and then reinstalled. Your workflow settings will be preserved. This process will take several minutes. Proceed?");
if (confirm)
{
await vm.ReinstallBackend();
}
}
}
private async Task ShowConfirmationDialog(string title, string message)
{
var dialog = new Window
{
Title = title,
Width = 480,
Height = 200,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
//Icon = Icon, // TODO
CanResize = false,
ShowInTaskbar = false
};
var textBlock = new TextBlock
{
Text = message,
Margin = new Thickness(20),
TextWrapping = TextWrapping.Wrap,
VerticalAlignment = VerticalAlignment.Center,
Width = 380,
};
var materialIcon = new MaterialIcon
{
Kind = Material.Icons.MaterialIconKind.QuestionMarkCircleOutline,
Width = 48,
Height = 48,
};
var textPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(20),
Children = { materialIcon, textBlock },
};
var yesButton = new Button
{
Content = "Yes",
Width = 100,
HorizontalAlignment = HorizontalAlignment.Center,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 10, 0)
};
yesButton.Click += (sender, e) => dialog.Close(true);
var noButton = new Button
{
Content = "No",
Width = 100,
HorizontalAlignment = HorizontalAlignment.Center,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 0, 0)
};
noButton.Click += (sender, e) => dialog.Close(false);
var buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
Children = { yesButton, noButton },
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(20, 0, 20, 20)
};
var mainPanel = new StackPanel
{
Children = { textPanel, buttonPanel }
};
dialog.Content = mainPanel;
var result = await dialog.ShowDialog(this);
return result ?? false;
}
public void HandleDevicesGotFocus(object? sender, GotFocusEventArgs e)
{
if (DataContext is MainWindowViewModel vm && sender is AutoCompleteBox cb)
{
if (vm.CurrentWorkflow != null && e.NavigationMethod != NavigationMethod.Unspecified && e.Source is TextBox)
{
cb.IsDropDownOpen = true;
}
}
}
private void HandleDevicesTapped(object? sender, Avalonia.Input.TappedEventArgs e)
{
if (DataContext is MainWindowViewModel vm && sender is AutoCompleteBox cb)
{
if (vm.CurrentWorkflow != null)
{
cb.IsDropDownOpen = true;
}
}
}
}
}
================================================
FILE: MangaJaNaiConverterGui/app.manifest
================================================
================================================
FILE: MangaJaNaiConverterGui/appstate2.json
================================================
{
"$type": "MangaJaNaiConverterGui.ViewModels.MainWindowViewModel, MangaJaNaiConverterGui",
"DisplayDeviceMap": {
"$type": "Avalonia.Collections.AvaloniaDictionary`2[[System.String, System.Private.CoreLib],[MangaJaNaiConverterGui.ViewModels.ReaderDevice, MangaJaNaiConverterGui]], Avalonia.Base"
},
"AutoUpdateEnabled": true,
"SelectedDeviceIndex": 0,
"UseCpu": false,
"UseFp16": true,
"Workflows": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Upscale Manga (Default)",
"WorkflowIndex": 0,
"SelectedTabIndex": 0,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": true,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": true,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": false,
"DisplayDevice": "Kobo Elipsa 2E (2023)",
"DisplayDeviceWidth": 1404,
"DisplayDeviceHeight": 1872,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_IllustrationJaNai_V3denoise_FDAT_M_unshuffle_30k_fp16.safetensors",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V3denoise_FDAT_M_47k_fp16.safetensors",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "16",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Custom Workflow 1",
"WorkflowIndex": 1,
"SelectedTabIndex": 0,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": false,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": true,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": false,
"DisplayDevice": null,
"DisplayDeviceWidth": 0,
"DisplayDeviceHeight": 0,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V1_ESRGAN_135k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Custom Workflow 2",
"WorkflowIndex": 2,
"SelectedTabIndex": 0,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": false,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": true,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": false,
"DisplayDevice": null,
"DisplayDeviceWidth": 0,
"DisplayDeviceHeight": 0,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V1_ESRGAN_135k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Custom Workflow 3",
"WorkflowIndex": 3,
"SelectedTabIndex": 0,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": false,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": true,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": false,
"DisplayDevice": null,
"DisplayDeviceWidth": 0,
"DisplayDeviceHeight": 0,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V1_ESRGAN_135k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Custom Workflow 4",
"WorkflowIndex": 4,
"SelectedTabIndex": 0,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": false,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": true,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": false,
"DisplayDevice": null,
"DisplayDeviceWidth": 0,
"DisplayDeviceHeight": 0,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V1_ESRGAN_135k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Custom Workflow 5",
"WorkflowIndex": 5,
"SelectedTabIndex": 0,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": false,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": true,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": false,
"DisplayDevice": null,
"DisplayDeviceWidth": 0,
"DisplayDeviceHeight": 0,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V1_ESRGAN_135k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Custom Workflow 6",
"WorkflowIndex": 6,
"SelectedTabIndex": 0,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": false,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": true,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": false,
"DisplayDevice": null,
"DisplayDeviceWidth": 0,
"DisplayDeviceHeight": 0,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V1_ESRGAN_135k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Custom Workflow 7",
"WorkflowIndex": 7,
"SelectedTabIndex": 1,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": true,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": false,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": true,
"DisplayDevice": "ONYX Boox Max Lumi 2 (2021)",
"DisplayDeviceWidth": 1650,
"DisplayDeviceHeight": 2200,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V1_ESRGAN_135k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Custom Workflow 8",
"WorkflowIndex": 8,
"SelectedTabIndex": 0,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": true,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": false,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": true,
"DisplayDevice": "Samsung Galaxy Tab S9 Ultra (2023)",
"DisplayDeviceWidth": 1848,
"DisplayDeviceHeight": 2950,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V1_ESRGAN_135k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Custom Workflow 9",
"WorkflowIndex": 9,
"SelectedTabIndex": 0,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": true,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": true,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": false,
"DisplayDevice": null,
"DisplayDeviceWidth": 0,
"DisplayDeviceHeight": 0,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V1_ESRGAN_135k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Custom Workflow 10",
"WorkflowIndex": 10,
"SelectedTabIndex": 0,
"InputFilePath": "",
"InputFolderPath": "",
"OutputFilename": "%filename%-mangajanai",
"OutputFolderPath": "",
"OverwriteExistingFiles": false,
"UpscaleImages": true,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": true,
"UpscaleScaleFactor": 4,
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": false,
"DisplayDevice": null,
"DisplayDeviceWidth": 0,
"DisplayDeviceHeight": 0,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V1_ESRGAN_135k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
}
]
},
"SelectedWorkflowIndex": 0
}
================================================
FILE: MangaJaNaiConverterGui/backend/resources/default_cli_configuration.json
================================================
{
"$type": "MangaJaNaiConverterGui.ViewModels.MainWindowViewModel, MangaJaNaiConverterGui",
"AutoUpdateEnabled": true,
"SelectedDeviceIndex": ">>CONTROLLED_BY_CLI<<",
"UseCpu": false,
"UseFp16": true,
"ModelsDirectory": ">>CONTROLLED_BY_CLI<<",
"Workflows": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui",
"WorkflowName": "Upscale Manga (Default)",
"WorkflowIndex": 0,
"SelectedTabIndex": ">>CONTROLLED_BY_CL<<",
"GrayscaleDetectionThreshold": 12,
"InputFilePath": ">>CONTROLLED_BY_CL<<",
"InputFolderPath": ">>CONTROLLED_BY_CL<<",
"OutputFilename": "%filename%",
"OutputFolderPath": ">>CONTROLLED_BY_CL<<",
"OverwriteExistingFiles": false,
"UpscaleImages": true,
"UpscaleArchives": true,
"ResizeHeightAfterUpscale": 2160,
"ResizeWidthAfterUpscale": 3840,
"WebpSelected": true,
"AvifSelected": false,
"PngSelected": false,
"JpegSelected": false,
"UseLosslessCompression": false,
"LossyCompressionQuality": 80,
"ShowLossySettings": true,
"ModeScaleSelected": true,
"UpscaleScaleFactor": ">>CONTROLLED_BY_CL<<",
"ModeWidthSelected": false,
"ModeHeightSelected": false,
"ModeFitToDisplaySelected": false,
"DisplayDevice": "Kobo Elipsa 2E (2023)",
"DisplayDeviceWidth": 1404,
"DisplayDeviceHeight": 1872,
"DisplayPortraitSelected": true,
"ShowAdvancedSettings": false,
"Chains": {
"$type": "Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base",
"$values": [
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "1",
"MinResolution": "0x0",
"MaxResolution": "0x0",
"IsGrayscale": false,
"IsColor": true,
"MinScaleFactor": 0,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_IllustrationJaNai_V1_ESRGAN_135k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": false,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "2",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "3",
"MinResolution": "0x0",
"MaxResolution": "0x1250",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "4",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "5",
"MinResolution": "0x1251",
"MaxResolution": "0x1350",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "6",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "7",
"MinResolution": "0x1351",
"MaxResolution": "0x1450",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "8",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "9",
"MinResolution": "0x1451",
"MaxResolution": "0x1550",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "10",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "11",
"MinResolution": "0x1551",
"MaxResolution": "0x1760",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "12",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "13",
"MinResolution": "0x1761",
"MaxResolution": "0x1984",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "14",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 0,
"MaxScaleFactor": 2,
"ModelFilePath": "2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
},
{
"$type": "MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui",
"ChainNumber": "15",
"MinResolution": "0x1985",
"MaxResolution": "0x0",
"IsGrayscale": true,
"IsColor": false,
"MinScaleFactor": 2,
"MaxScaleFactor": 0,
"ModelFilePath": "4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth",
"ModelTileSize": "Auto (Estimate)",
"AutoAdjustLevels": true,
"ResizeHeightBeforeUpscale": 0,
"ResizeWidthBeforeUpscale": 0,
"ResizeFactorBeforeUpscale": 100.0
}
]
}
}
]
},
"SelectedWorkflowIndex": 0
}
================================================
FILE: MangaJaNaiConverterGui/backend/src/.pre-commit-config.yaml
================================================
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.5.1
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format
================================================
FILE: MangaJaNaiConverterGui/backend/src/README.md
================================================
# Info
MangaJaNaiConverterGui is a convenient GUI windows tool, but in the backend it operates by running some python scripts in CLI.
This README is for those interested in running only the CLI on a linux systems or WSL.
# Setup
## Setup virtual environment and download dependencies
Navigate to root fo the project and execute these commands. This script will create a new python virtual environment, switch to it and download all required dependencies:
```commandline
python -m venv venv_mangajanai
source venv_mangajanai/bin/activate
pip install MangaJaNaiConverterGui/backend/src/
```
By default, it uses pytorch for CUDA 12.1 compute platform. If you wish to change it, install proper version based on instruction from their [official website](https://pytorch.org/get-started/locally/).
## Download models
Scripts expect all MangaJaNai models to be available in order to select the best one for each processed file. When running it as CLI on linux, put them in `MangaJaNaiConverterGui/backend/models` folder.
You can extract them from the release .exe file of the version you wish to use.
# Usage
You can use this tool in two ways. As a simple CLI or as advanced json-controlled utility.
## Simple CLI
## Execution
Script should be run from `MangaJaNaiConverterGui/backend/src/` directory. Example usage:
```bash
# Show detailed help
python run_upscale.py -h
# Upscale single file with factor 4
python run_upscale.py -f "/my/dir/myFile.jpg" -u 4
# Upscale whole directory into a custom output directory
python run_upscale.py -d "/my/input/dir/" -o "/my/output/dir/"
```
## Advanced Utility
Script uses a settings file generated by the GUI to control its behavior. You need to build it manually to use the CLI.
The default settings can be found in [MangaJaNaiConverterGui/appstate2.json](MangaJaNaiConverterGui/appstate2.json). Just copy it and modify only what you need.
It's a long file, but you only need to worry about a few root keys and first workflow `Upscale Manga (Default)`
### Important Root Keys
- **SelectedDeviceIndex** - Controls which GPU should run upscaling jobs. If default doesn't work for you, check your devices by running [device_list.py](MangaJaNaiConverterGui/chaiNNer/backend/src/device_list.py)
- **UseCPU** - true/false
- **UseFp16** - true/false
### Important Workflows Keys
- **SelectedTabIndex** - Choose if you want to upscale a single file or a whole folder
- 0 - file
- 1 - folder
- **InputFilePath** - absolute file path. Used when **SelectedTabIndex** = 0
- **InputFolderPath** - absolute folder path. Used when **SelectedTabIndex** = 1
- **OutputFilename** - Name of generated filenames. Keep `%filename%` to leave the same name
- **OutputFolderPath** - absolute folder path.
- **OverwriteExistingFiles** - true/false
- **UpscaleImages** - true/false - needs to be true for upscale to work
- **WebpSelected/AvifSelected/PngSelected/JpegSelected** - true/false. Only one should be true. Selects output filetype.
- **UpscaleScaleFactor** - 1/2/3/4 - How much you want to upscale which controls which models will be used
- There are a bunch of other options if you want to dig deeper into it, but setting these should be enough for basic usage
### Execution
In `MangaJaNaiConverterGui/backend/src/` directory run:
```bash
python run_upscale.py --settings "/path/to/your/file/appstate2.json"
```
================================================
FILE: MangaJaNaiConverterGui/backend/src/__init__.py
================================================
================================================
FILE: MangaJaNaiConverterGui/backend/src/accelerator_detection.py
================================================
"""
Comprehensive accelerator detection for PyTorch backend.
Supports all available PyTorch accelerators in PyTorch 2.7+
"""
from __future__ import annotations
import warnings
from dataclasses import dataclass
from enum import Enum
from functools import cached_property
from typing import Any, Optional, Sequence, override
import torch
from sanic.log import logger
class AcceleratorType(Enum):
"""Supported accelerator types"""
CPU = "cpu"
CUDA = "cuda"
ROCM = "rocm" # AMD GPUs using ROCm
MPS = "mps" # Apple Metal Performance Shaders
XPU = "xpu" # Intel GPUs
@dataclass(frozen=True)
class AcceleratorDevice:
"""Information about an accelerator device"""
type: AcceleratorType
index: int
name: str
memory_total: Optional[int] = None
memory_free: Optional[int] = None
supports_fp16: bool = False
supports_bf16: bool = False
device_string: str = ""
def __post_init__(self):
if not self.device_string:
if self.type == AcceleratorType.CPU:
object.__setattr__(self, "device_string", "cpu")
else:
object.__setattr__(self, "device_string", f"{self.type.value}:{self.index}")
@property
def torch_device(self) -> torch.device:
"""Get the corresponding torch.device"""
return torch.device(self.device_string)
@override
def __eq__(self, other: object) -> bool:
if isinstance(other, AcceleratorDevice):
return self.device_string == other.device_string
return NotImplemented
@override
def __hash__(self) -> int:
# only needed if you ever put Device in sets / dict keys
return hash(self.device_string)
class AcceleratorDetector:
"""Detects and manages available accelerators"""
def __init__(self):
self._devices: Optional[list[AcceleratorDevice]] = None
@cached_property
def available_devices(self) -> list[AcceleratorDevice]:
"""Get all available accelerator devices"""
if self._devices is None:
self._devices = self._detect_all_devices()
return self._devices
def _detect_all_devices(self) -> list[AcceleratorDevice]:
"""Detect all available accelerator devices"""
devices = []
# Always add CPU
devices.append(AcceleratorDevice(
type=AcceleratorType.CPU,
index=0,
name="CPU",
supports_fp16=False, # PyTorch 2.7+ doesn't support FP16 on CPU
supports_bf16=True, # CPU supports bfloat16
))
# Detect CUDA devices
devices.extend(self._detect_cuda_devices())
# Detect ROCm devices (ROCm uses CUDA API)
devices.extend(self._detect_rocm_devices())
# Detect Apple MPS
devices.extend(self._detect_mps_devices())
# Detect Intel XPU
devices.extend(self._detect_xpu_devices())
return devices
def _detect_cuda_devices(self) -> list[AcceleratorDevice]:
"""Detect NVIDIA CUDA devices"""
devices = []
try:
if torch.cuda.is_available():
for i in range(torch.cuda.device_count()):
try:
device_props = torch.cuda.get_device_properties(i)
memory_info = torch.cuda.mem_get_info(i)
# Determine FP16 support based on architecture
supports_fp16 = self._cuda_supports_fp16(device_props, i)
supports_bf16 = self._cuda_supports_bf16(device_props)
devices.append(AcceleratorDevice(
type=AcceleratorType.CUDA,
index=i,
name=device_props.name,
memory_total=device_props.total_memory,
memory_free=memory_info[0],
supports_fp16=supports_fp16,
supports_bf16=supports_bf16,
))
logger.info(f"Detected CUDA device {i}: {device_props.name}")
except Exception as e:
logger.warning(f"Failed to get info for CUDA device {i}: {e}")
except Exception as e:
logger.info(f"CUDA not available: {e}")
return devices
def _detect_rocm_devices(self) -> list[AcceleratorDevice]:
"""Detect AMD ROCm devices"""
devices = []
try:
# ROCm devices appear as CUDA devices due to HIP/CUDA compatibility
# Check if we're actually running on ROCm
if hasattr(torch.version, 'hip') and torch.version.hip is not None:
# This is ROCm, re-categorize CUDA devices as ROCm
if torch.cuda.is_available():
for i in range(torch.cuda.device_count()):
try:
device_props = torch.cuda.get_device_properties(i)
memory_info = torch.cuda.mem_get_info(i)
devices.append(AcceleratorDevice(
type=AcceleratorType.ROCM,
index=i,
name=device_props.name,
memory_total=device_props.total_memory,
memory_free=memory_info[0],
supports_fp16=True, # Most modern AMD GPUs support FP16
supports_bf16=True, # Modern AMD GPUs support bfloat16
device_string=f"cuda:{i}", # ROCm uses cuda device string
))
logger.info(f"Detected ROCm device {i}: {device_props.name}")
except Exception as e:
logger.warning(f"Failed to get info for ROCm device {i}: {e}")
except Exception as e:
logger.debug(f"ROCm detection failed: {e}")
return devices
def _detect_mps_devices(self) -> list[AcceleratorDevice]:
"""Detect Apple Metal Performance Shaders devices"""
devices = []
try:
if (hasattr(torch, 'backends') and
hasattr(torch.backends, 'mps') and
torch.backends.mps.is_built() and
torch.backends.mps.is_available()):
devices.append(AcceleratorDevice(
type=AcceleratorType.MPS,
index=0,
name="Apple Metal GPU",
supports_fp16=True, # MPS supports FP16
supports_bf16=False, # MPS doesn't support bfloat16 yet
device_string="mps",
))
logger.info("Detected Apple MPS device")
except Exception as e:
logger.debug(f"MPS detection failed: {e}")
return devices
def _detect_xpu_devices(self) -> list[AcceleratorDevice]:
"""Detect Intel XPU devices"""
devices = []
try:
if hasattr(torch, 'xpu') and torch.xpu.is_available():
device_count = torch.xpu.device_count()
for i in range(device_count):
try:
device_name = torch.xpu.get_device_name(i)
# Try to get memory info if available
memory_info = None
try:
memory_info = torch.xpu.mem_get_info(i)
except Exception:
pass
devices.append(AcceleratorDevice(
type=AcceleratorType.XPU,
index=i,
name=device_name,
memory_total=memory_info[1] if memory_info else None,
memory_free=memory_info[0] if memory_info else None,
supports_fp16=True, # Intel XPU supports FP16
supports_bf16=True, # Intel XPU supports bfloat16
))
logger.info(f"Detected Intel XPU device {i}: {device_name}")
except Exception as e:
logger.warning(f"Failed to get info for XPU device {i}: {e}")
except Exception as e:
logger.debug(f"XPU detection failed: {e}")
return devices
def _cuda_supports_fp16(self, device_props: Any, device_index: int) -> bool:
"""Check if CUDA device supports FP16"""
try:
# Check compute capability
major, minor = device_props.major, device_props.minor
compute_capability = major * 10 + minor
# FP16 is supported on:
# - Volta (7.0+) and newer architectures
# - Some Turing cards (RTX series, not GTX 16xx)
if compute_capability >= 70: # Volta and newer
return True
elif compute_capability >= 75: # Turing
# For Turing, check if it's RTX (supports FP16) or GTX 16xx (doesn't)
return "RTX" in device_props.name
else:
return False
except Exception:
# Fallback: try to actually use FP16
try:
test_tensor = torch.tensor([1.0], dtype=torch.float16, device=f"cuda:{device_index}")
return True
except Exception:
return False
def _cuda_supports_bf16(self, device_props: Any) -> bool:
"""Check if CUDA device supports bfloat16"""
try:
# bfloat16 is supported on Ampere (8.0+) and newer
major, minor = device_props.major, device_props.minor
compute_capability = major * 10 + minor
return compute_capability >= 80
except Exception:
return False
def get_devices_by_type(self, accelerator_type: AcceleratorType) -> list[AcceleratorDevice]:
"""Get all devices of a specific type"""
return [device for device in self.available_devices if device.type == accelerator_type]
def get_best_device(self, prefer_gpu: bool = True) -> AcceleratorDevice:
"""Get the best available device"""
if not prefer_gpu:
return self.get_cpu_device()
# Priority order: CUDA > XPU > MPS > ROCm > CPU
for device_type in [AcceleratorType.CUDA, AcceleratorType.XPU, AcceleratorType.MPS,
AcceleratorType.ROCM]:
devices = self.get_devices_by_type(device_type)
if devices:
return devices[0] # Return the first device of this type
return self.get_cpu_device()
def get_cpu_device(self) -> AcceleratorDevice:
"""Get the CPU device"""
cpu_devices = self.get_devices_by_type(AcceleratorType.CPU)
return cpu_devices[0] if cpu_devices else AcceleratorDevice(
type=AcceleratorType.CPU, index=0, name="CPU"
)
def get_device_by_index(self, device_type: AcceleratorType, index: int) -> Optional[AcceleratorDevice]:
"""Get a specific device by type and index"""
devices = self.get_devices_by_type(device_type)
for device in devices:
if device.index == index:
return device
return None
def get_autocast_device_type(device: torch.device) -> str:
"""Get the correct device type string for torch.autocast"""
device_type = device.type
# Map device types to autocast device types
if device_type in ["cuda", "rocm"]: # ROCm uses cuda device type
return "cuda"
elif device_type == "xpu":
return "xpu"
elif device_type == "mps":
# MPS doesn't support autocast yet, use cpu
return "cpu"
else:
return "cpu"
def is_device_type_supported_for_autocast(device: torch.device) -> bool:
"""Check if device type supports autocast"""
device_type = device.type
return device_type in ["cuda", "xpu"]
# Global detector instance
_detector: Optional[AcceleratorDetector] = None
def get_accelerator_detector() -> AcceleratorDetector:
"""Get the global accelerator detector instance"""
global _detector
if _detector is None:
_detector = AcceleratorDetector()
return _detector
def get_available_devices() -> list[AcceleratorDevice]:
"""Convenience function to get all available devices"""
return get_accelerator_detector().available_devices
def get_best_device(prefer_gpu: bool = True) -> AcceleratorDevice:
"""Convenience function to get the best available device"""
return get_accelerator_detector().get_best_device(prefer_gpu)
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/__init__.py
================================================
from .api import *
from .group import *
from .input import *
from .iter import *
from .lazy import *
from .node_context import *
from .node_data import *
from .output import *
from .settings import *
from .types import *
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/api.py
================================================
from __future__ import annotations
import importlib
import os
from collections.abc import Awaitable, Callable, Iterable
from dataclasses import asdict, dataclass, field
from typing import (
Any,
TypeVar,
)
from sanic.log import logger
from .group import Group, GroupId, NestedGroup, NestedIdGroup
from .input import BaseInput
from .node_check import (
NAME_CHECK_LEVEL,
TYPE_CHECK_LEVEL,
CheckFailedError,
CheckLevel,
check_naming_conventions,
check_schema_types,
)
from .node_data import (
IteratorInputInfo,
IteratorOutputInfo,
KeyInfo,
NodeData,
SpecialSuggestion,
)
from .output import BaseOutput
from .settings import Setting
from .types import FeatureId, InputId, NodeId, NodeKind, OutputId, RunFn
KB = 1024**1
MB = 1024**2
GB = 1024**3
def _process_inputs(base_inputs: Iterable[BaseInput | NestedGroup]):
inputs: list[BaseInput] = []
groups: list[NestedIdGroup] = []
def add_inputs(
current: Iterable[BaseInput | NestedGroup],
) -> list[InputId | NestedIdGroup]:
layout: list[InputId | NestedIdGroup] = []
for x in current:
if isinstance(x, Group):
if x.info.id == -1:
x.info.id = GroupId(len(groups))
g: NestedIdGroup = Group(x.info, [])
groups.append(g)
layout.append(g)
g.items.extend(add_inputs(x.items)) # type: ignore
else:
if x.id == -1:
x.id = InputId(len(inputs))
layout.append(x.id)
inputs.append(x)
return layout
return inputs, add_inputs(base_inputs)
def _process_outputs(base_outputs: Iterable[BaseOutput]):
outputs: list[BaseOutput] = []
for i, output_value in enumerate(base_outputs):
if output_value.id == -1:
output_value.id = OutputId(i)
outputs.append(output_value)
return outputs
T = TypeVar("T", bound=RunFn)
S = TypeVar("S")
@dataclass
class NodeGroup:
category: Category
id: str
name: str
order: list[str | NodeId] = field(default_factory=list)
nodes: list[NodeData] = field(default_factory=list)
def add_node(self, node: NodeData) -> None:
logger.debug(f"Added {node.schema_id}")
self.nodes.append(node)
def to_dict(self):
return {
"id": self.id,
"category": self.category.id,
"name": self.name,
"order": self.order,
}
def register(
self,
schema_id: str,
*,
name: str,
description: str | list[str],
inputs: list[BaseInput | NestedGroup],
outputs: list[BaseOutput],
icon: str = "BsQuestionCircleFill",
kind: NodeKind = "regularNode",
side_effects: bool = False,
deprecated: bool = False,
decorators: list[Callable] | None = None,
see_also: list[str] | str | None = None,
features: list[FeatureId] | FeatureId | None = None,
limited_to_8bpc: bool | str = False,
iterator_inputs: list[IteratorInputInfo] | IteratorInputInfo | None = None,
iterator_outputs: list[IteratorOutputInfo] | IteratorOutputInfo | None = None,
node_context: bool = False,
key_info: KeyInfo | None = None,
suggestions: list[SpecialSuggestion] | None = None,
):
if not isinstance(description, str):
description = "\n\n".join(description)
if limited_to_8bpc:
description += "\n\n#### Limited color depth\n\n"
if isinstance(limited_to_8bpc, str):
description += f" {limited_to_8bpc}"
else:
description += (
"This node will internally convert input images to 8 bits/channel."
" This is generally only a problem if you intend to save the output with 16 bits/channel or higher."
)
def to_list(x: list[S] | S | None) -> list[S]:
if x is None:
return []
if isinstance(x, list):
return x
return [x]
see_also = to_list(see_also)
features = to_list(features)
iterator_inputs = to_list(iterator_inputs)
iterator_outputs = to_list(iterator_outputs)
if kind == "generator": # Generator
assert len(iterator_inputs) == 0 and len(iterator_outputs) == 1
elif kind == "collector":
assert len(iterator_inputs) == 1 and len(iterator_outputs) == 0
else:
assert len(iterator_inputs) == 0 and len(iterator_outputs) == 0
def run_check(level: CheckLevel, run: Callable[[bool], None]) -> None:
if level == CheckLevel.NONE:
return
try:
run(level == CheckLevel.FIX)
except CheckFailedError as e:
full_error_message = f"Error in {schema_id}: {e}"
if level == CheckLevel.ERROR:
raise CheckFailedError(full_error_message) # noqa: B904
logger.warning(full_error_message)
def inner_wrapper(wrapped_func: T) -> T:
p_inputs, group_layout = _process_inputs(inputs)
p_outputs = _process_outputs(outputs)
original_fn = wrapped_func
if decorators is not None:
for decorator in decorators:
wrapped_func = decorator(wrapped_func)
node = NodeData(
schema_id=schema_id,
name=name,
description=description,
see_also=see_also,
icon=icon,
kind=kind,
inputs=p_inputs,
group_layout=group_layout,
outputs=p_outputs,
iterable_inputs=iterator_inputs,
iterable_outputs=iterator_outputs,
key_info=key_info,
suggestions=suggestions or [],
side_effects=side_effects,
deprecated=deprecated,
node_context=node_context,
features=features,
run=wrapped_func,
)
run_check(
TYPE_CHECK_LEVEL,
lambda _: check_schema_types(original_fn, node),
)
run_check(
NAME_CHECK_LEVEL,
lambda fix: check_naming_conventions(original_fn, name, fix),
)
self.add_node(node)
return wrapped_func
return inner_wrapper
@dataclass
class Category:
package: Package
id: str
name: str
description: str
icon: str = "BsQuestionCircleFill"
color: str = "#777777"
install_hint: str | None = None
node_groups: list[NodeGroup] = field(default_factory=list)
def add_node_group(self, name: str) -> NodeGroup:
result = NodeGroup(
category=self,
id=self.id + "/" + name.lower(),
name=name,
)
self.node_groups.append(result)
return result
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"description": self.description,
"icon": self.icon,
"color": self.color,
"installHint": self.install_hint,
"groups": [g.to_dict() for g in self.node_groups],
}
@dataclass
class Dependency:
display_name: str
pypi_name: str
version: str
size_estimate: int | float
auto_update: bool = True
extra_index_url: str | None = None
import_name: str | None = None
def to_dict(self):
return {
"displayName": self.display_name,
"pypiName": self.pypi_name,
"version": self.version,
"sizeEstimate": int(self.size_estimate),
"autoUpdate": self.auto_update,
"findLink": self.extra_index_url,
}
@staticmethod
def from_dict(data: dict[str, Any]) -> Dependency:
return Dependency(
display_name=data["displayName"],
pypi_name=data["pypiName"],
version=data["version"],
size_estimate=data["sizeEstimate"],
auto_update=data["autoUpdate"],
extra_index_url=data["findLink"],
)
@dataclass
class Feature:
id: str
name: str
description: str
behavior: FeatureBehavior | None = None
def add_behavior(self, check: Callable[[], Awaitable[FeatureState]]) -> FeatureId:
if self.behavior is not None:
raise ValueError("Behavior already set")
self.behavior = FeatureBehavior(check=check)
return FeatureId(self.id)
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"description": self.description,
}
@staticmethod
def from_dict(data: dict[str, Any]) -> Feature:
return Feature(
id=data["id"],
name=data["name"],
description=data["description"],
)
@dataclass
class FeatureBehavior:
check: Callable[[], Awaitable[FeatureState]]
@dataclass(frozen=True)
class FeatureState:
is_enabled: bool
details: str | None = None
@staticmethod
def enabled(details: str | None = None) -> FeatureState:
return FeatureState(is_enabled=True, details=details)
@staticmethod
def disabled(details: str | None = None) -> FeatureState:
return FeatureState(is_enabled=False, details=details)
@dataclass
class Package:
where: str
id: str
name: str
description: str
icon: str
color: str
dependencies: list[Dependency] = field(default_factory=list)
categories: list[Category] = field(default_factory=list)
features: list[Feature] = field(default_factory=list)
settings: list[Setting] = field(default_factory=list)
def add_category(
self,
name: str,
description: str,
icon: str,
color: str,
install_hint: str | None = None,
) -> Category:
result = Category(
package=self,
id=name.lower(),
name=name,
description=description,
icon=icon,
color=color,
install_hint=install_hint,
)
self.categories.append(result)
return result
def add_dependency(self, dependency: Dependency) -> None:
self.dependencies.append(dependency)
def add_setting(self, setting: Setting) -> None:
self.settings.append(setting)
def add_feature(
self,
id: str, # pylint: disable=redefined-builtin
name: str,
description: str,
) -> Feature:
if any(f.id == id for f in self.features):
raise ValueError(f"Duplicate feature id: {id}")
feature = Feature(id=id, name=name, description=description)
self.features.append(feature)
return feature
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"description": self.description,
"icon": self.icon,
"color": self.color,
"dependencies": [d.to_dict() for d in self.dependencies],
"features": [f.to_dict() for f in self.features],
"settings": [asdict(x) for x in self.settings],
}
@staticmethod
def from_dict(data: dict[str, Any]) -> Package:
"""This is really only for dependency purposes, so it's not feature-complete"""
return Package(
where=data.get("where", "unknown"),
id=data["id"],
name=data["name"],
description=data["description"],
icon=data["icon"],
color=data["color"],
dependencies=[Dependency.from_dict(d) for d in data["dependencies"]],
categories=[],
features=[Feature.from_dict(f) for f in data["features"]],
settings=[],
)
def _iter_py_files(directory: str):
for root, _, files in os.walk(directory):
for file in files:
if file.endswith(".py"):
yield os.path.join(root, file)
@dataclass
class LoadErrorInfo:
module: str
file: str
error: Exception
class PackageRegistry:
def __init__(self) -> None:
self.packages: dict[str, Package] = {}
self.categories: list[Category] = []
self.nodes: dict[str, tuple[NodeData, NodeGroup]] = {}
def get_node(self, schema_id: str) -> NodeData:
return self.nodes[schema_id][0]
def get_package(self, schema_id: str) -> Package:
return self.nodes[schema_id][1].category.package
def add(self, package: Package) -> Package:
# assert package.where not in self.packages
self.packages[package.where] = package
return package
def load_nodes(self, current_file: str) -> list[LoadErrorInfo]:
load_error: list[LoadErrorInfo] = []
failed_checks: list[CheckFailedError] = []
for package in list(self.packages.values()):
for file_path in _iter_py_files(os.path.dirname(package.where)):
_, name = os.path.split(file_path)
if not name.startswith("_"):
module = os.path.relpath(file_path, os.path.dirname(current_file))
module = module.replace("/", ".").replace("\\", ".")[: -len(".py")]
try:
importlib.import_module(module, package=None)
except CheckFailedError as e:
logger.error(e)
failed_checks.append(e)
except Exception as e:
load_error.append(LoadErrorInfo(module, file_path, e))
if len(failed_checks) > 0:
raise RuntimeError(f"Checks failed in {len(failed_checks)} node(s)")
self._refresh_nodes()
return load_error
def _refresh_nodes(self) -> None:
self.nodes = {}
self.categories = []
for package in self.packages.values():
self.categories.extend(package.categories)
for category in package.categories:
for sub in category.node_groups:
for node in sub.nodes:
if node.schema_id in self.nodes:
# print warning
pass
self.nodes[node.schema_id] = node, sub
registry = PackageRegistry()
def add_package(
where: str,
id: str, # pylint: disable=redefined-builtin
name: str,
description: str,
dependencies: list[Dependency] | None = None,
icon: str = "BsQuestionCircleFill",
color: str = "#777777",
) -> Package:
return registry.add(
Package(
where=where,
id=id,
name=name,
description=description,
icon=icon,
color=color,
dependencies=dependencies or [],
)
)
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/group.py
================================================
from __future__ import annotations
from typing import Any, Generic, NewType, TypeVar, Union
from .input import BaseInput
from .types import InputId
T = TypeVar("T")
GroupId = NewType("GroupId", int)
class GroupInfo:
def __init__(
self,
group_id: GroupId,
kind: str,
options: dict[str, Any] | None = None,
) -> None:
self.id: GroupId = group_id
self.kind: str = kind
self.options: dict[str, Any] = {} if options is None else options
class Group(Generic[T]):
def __init__(self, info: GroupInfo, items: list[T]) -> None:
self.info: GroupInfo = info
self.items: list[T] = items
def to_dict(self):
return {
"id": self.info.id,
"kind": self.info.kind,
"options": self.info.options,
"items": [i.to_dict() if isinstance(i, Group) else i for i in self.items],
}
NestedGroup = Group[Union[BaseInput, "NestedGroup"]]
NestedIdGroup = Group[Union[InputId, "NestedIdGroup"]]
# pylint: disable-next=redefined-builtin
def group(kind: str, options: dict[str, Any] | None = None, id: int = -1):
info = GroupInfo(GroupId(id), kind, options)
def ret(*items: BaseInput | NestedGroup) -> NestedGroup:
return Group(info, list(items))
return ret
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/input.py
================================================
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any, Generic, Literal, Optional, TypedDict, TypeVar, Union
import navi
from .types import InputId, OutputId
InputKind = Literal[
"number",
"slider",
"dropdown",
"text",
"directory",
"file",
"color",
"generic",
"static",
]
@dataclass
class InputConversion:
"""
An input conversion can be used to convert the assigned type of an input.
This is useful to model the changes `enforce` makes to values.
`type` is used to declare which type is intended to be converted by this
conversion. `convert` is the expression that does the actual conversion. It
will be given a special parameter called `Input` that will be the value to
convert. The `Input` parameter is guaranteed to be a non-empty sub type of
`type`.
Example:
To convert all numbers to string, use this conversions:
```
InputConversion("number", "toString(Input)")
```
"""
type: navi.ExpressionJson
convert: navi.ExpressionJson
def to_dict(self):
return {
"type": self.type,
"convert": self.convert,
}
@dataclass
class IOFusion:
output_id: OutputId
class LiteralErrorValue(TypedDict):
type: Literal["literal"]
value: str | int | float | None
class FormattedErrorValue(TypedDict):
type: Literal["formatted"]
formatString: str
class PendingErrorValue(TypedDict):
type: Literal["pending"]
class UnknownErrorValue(TypedDict):
type: Literal["unknown"]
typeName: str
typeModule: str
ErrorValue = Union[
LiteralErrorValue, FormattedErrorValue, UnknownErrorValue, PendingErrorValue
]
T = TypeVar("T")
class BaseInput(Generic[T]):
def __init__(
self,
input_type: navi.ExpressionJson,
label: str,
kind: InputKind = "generic",
has_handle: bool = True,
associated_type: Any = None,
) -> None:
self.input_type: navi.ExpressionJson = input_type
self.input_conversions: list[InputConversion] = []
self.input_adapt: navi.ExpressionJson | None = None
self.type_definitions: str | None = None
self.kind: InputKind = kind
self.label: str = label
self.optional: bool = False
self.has_handle: bool = has_handle
self.id: InputId = InputId(-1)
self.associated_type: Any = associated_type
self.fused: IOFusion | None = None
self.lazy: bool = False
# Optional documentation
self.description: str | None = None
self.hint: bool = False
self.should_suggest: bool = False
# This is the method that should be created by each input
def enforce(self, value: object) -> T:
"""Enforce the input type"""
return value # type: ignore
# This is the method that should be called by the processing code
def enforce_(self, value: object | None) -> T | None:
if self.optional and value is None:
return None
assert value is not None, (
f"Expected value to exist, "
f"but does not exist for {self.kind} input with type {self.input_type} and label {self.label}"
)
return self.enforce(value)
def get_error_value(self, value: object) -> ErrorValue:
if isinstance(value, Enum):
# unwrap enum
value = value.value
if isinstance(value, bool):
# bools need to be 0 or 1
return {"type": "literal", "value": int(value)}
if isinstance(value, int | float | str) or value is None:
return {"type": "literal", "value": value}
if isinstance(value, Path):
return {"type": "literal", "value": str(value)}
return {
"type": "unknown",
"typeName": type(value).__qualname__,
"typeModule": type(value).__module__,
}
def to_dict(self) -> Mapping[str, Any]:
actual_type = [self.input_type, "null"] if self.optional else self.input_type
return {
"id": self.id,
"type": actual_type,
"conversions": [c.to_dict() for c in self.input_conversions],
"adapt": self.input_adapt,
"typeDefinitions": self.type_definitions,
"kind": self.kind,
"label": self.label,
"optional": self.optional,
"hasHandle": self.has_handle,
"description": self.description,
"hint": self.hint,
"suggest": self.should_suggest,
"fused": {
"outputId": self.fused.output_id,
}
if self.fused
else None,
}
def with_id(self, input_id: InputId | int):
self.id = InputId(input_id)
return self
def with_docs(self, *description: str, hint: bool = False):
self.description = "\n\n".join(description)
self.hint = hint
return self
def suggest(self):
self.should_suggest = True
return self
def make_optional(self):
self.optional = True
if self.associated_type is not None:
associated_type = self.associated_type
self.associated_type = Optional[associated_type]
return self
def make_lazy(self):
self.lazy = True
return self
def make_fused(self, with_output: OutputId | int = 0):
self.fused = IOFusion(output_id=OutputId(with_output))
return self
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/iter.py
================================================
from __future__ import annotations
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from typing import Generic, TypeVar
I = TypeVar("I")
L = TypeVar("L")
@dataclass
class Generator(Generic[I]):
supplier: Callable[[], Iterable[I | Exception]]
expected_length: int
fail_fast: bool = True
metadata: object | None = None
def with_fail_fast(self, fail_fast: bool):
self.fail_fast = fail_fast
return self
def with_metadata(self, metadata: object):
self.metadata = metadata
return self
@staticmethod
def from_iter(
supplier: Callable[[], Iterable[I | Exception]], expected_length: int
) -> Generator[I]:
return Generator(supplier, expected_length)
@staticmethod
def from_list(l: list[L], map_fn: Callable[[L, int], I]) -> Generator[I]:
"""
Creates a new generator from a list that is mapped using the given
function. The iterable will be equivalent to `map(map_fn, l)`.
"""
def supplier():
for i, x in enumerate(l):
try:
yield map_fn(x, i)
except Exception as e:
yield e
return Generator(supplier, len(l))
@staticmethod
def from_range(count: int, map_fn: Callable[[int], I]) -> Generator[I]:
"""
Creates a new generator the given number of items where each item is
lazily evaluated. The iterable will be equivalent to `map(map_fn, range(count))`.
"""
assert count >= 0
def supplier():
for i in range(count):
try:
yield map_fn(i)
except Exception as e:
yield e
return Generator(supplier, count)
N = TypeVar("N")
R = TypeVar("R")
@dataclass
class Collector(Generic[N, R]):
on_iterate: Callable[[N], None]
on_complete: Callable[[], R]
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/lazy.py
================================================
from __future__ import annotations
import time
from asyncio import AbstractEventLoop
from collections.abc import Callable, Coroutine
from typing import Any, Generic, TypeVar
T = TypeVar("T")
class _Result(Generic[T]):
"""Either an okay value of T or an error value."""
def __init__(self, value: T | None, error: Exception | None) -> None:
self.value = value
self.error = error
def result(self) -> T:
"""Returns the value if it is okay, otherwise raises the error."""
if self.error is not None:
raise self.error
return self.value # type: ignore
@property
def is_ok(self) -> bool:
"""Returns True if the result is okay, otherwise False."""
return self.error is None
@staticmethod
def ok(value: T) -> _Result[T]:
return _Result(value, None)
@staticmethod
def err(error: Exception) -> _Result[T]:
return _Result(None, error)
def _to_result(fn: Callable[[], T]) -> Callable[[], _Result[T]]:
def wrapper() -> _Result[T]:
try:
return _Result.ok(fn())
except Exception as e:
return _Result.err(e)
return wrapper
class Lazy(Generic[T]):
def __init__(self, factory: Callable[[], T]) -> None:
self._factory = _to_result(factory)
self._value: _Result[T] | None = None
self._evaluating = False
self._eval_time = 0
@staticmethod
def ready(value: T) -> Lazy[T]:
lazy = Lazy(lambda: value)
lazy._value = _Result.ok(value) # noqa: SLF001
return lazy
@staticmethod
def from_coroutine(
coroutine: Coroutine[Any, Any, T], loop: AbstractEventLoop
) -> Lazy[T]:
def supplier() -> T:
task = loop.create_task(coroutine)
while not task.done():
if task.cancelled():
raise ValueError("Task was cancelled")
time.sleep(0.001)
return task.result()
return Lazy(supplier)
@property
def has_value(self) -> bool:
"""Returns True if the value has been computed, otherwise False."""
return self._value is not None and self._value.is_ok
@property
def has_error(self) -> bool:
"""Returns True if the value has been computed and it errored instead, otherwise False."""
return self._value is not None and not self._value.is_ok
@property
def evaluation_time(self) -> float:
"""The time in seconds that it took to evaluate the value. If the value is not computed, returns 0."""
return self._eval_time
@property
def value(self) -> T:
if self._value is None:
if self._evaluating:
# wait for the value to be computed
while self._value is None and self._evaluating:
time.sleep(0.001)
if self._value is None:
raise ValueError("Value was not computed")
else:
self._evaluating = True
try:
start = time.time()
self._value = self._factory()
self._eval_time = time.time() - start
finally:
self._evaluating = False
return self._value.result()
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/node_check.py
================================================
from __future__ import annotations
import ast
import inspect
import os
import pathlib
from collections import OrderedDict
from collections.abc import Callable
from enum import Enum
from typing import Any, NewType, Union, cast, get_args
from .node_context import NodeContext
from .node_data import NodeData
_Ty = NewType("_Ty", object)
class CheckFailedError(Exception):
pass
class CheckLevel(Enum):
NONE = "none"
WARN = "warn"
FIX = "fix"
ERROR = "error"
@staticmethod
def parse(s: str) -> CheckLevel:
s = s.strip().lower()
if s == CheckLevel.NONE.value:
return CheckLevel.NONE
elif s == CheckLevel.WARN.value:
return CheckLevel.WARN
elif s == CheckLevel.FIX.value:
return CheckLevel.FIX
elif s == CheckLevel.ERROR.value:
return CheckLevel.ERROR
else:
raise ValueError(f"Invalid check level: {s}")
def _get_check_level(name: str, default: CheckLevel) -> CheckLevel:
try:
s = os.environ.get(name, default.value)
return CheckLevel.parse(s)
except Exception:
return default
CHECK_LEVEL = _get_check_level("CHECK_LEVEL", CheckLevel.NONE)
NAME_CHECK_LEVEL = _get_check_level("NAME_CHECK_LEVEL", CHECK_LEVEL)
TYPE_CHECK_LEVEL = _get_check_level("TYPE_CHECK_LEVEL", CHECK_LEVEL)
class TypeTransformer(ast.NodeTransformer):
def visit_BinOp(self, node: ast.BinOp): # noqa
if isinstance(node.op, ast.BitOr):
return ast.Subscript(
value=ast.Name(id="Union", ctx=ast.Load()),
slice=ast.Index(
value=ast.Tuple(
elts=[
self.visit(node.left),
self.visit(node.right),
],
ctx=ast.Load(),
),
ctx=ast.Load(),
),
ctx=ast.Load(),
)
return super().visit_BinOp(node)
def visit_Subscript(self, node: ast.Subscript): # noqa
if isinstance(node.value, ast.Name) and node.value.id == "tuple":
return ast.Subscript(
value=ast.Name(id="Tuple", ctx=ast.Load()),
slice=node.slice,
ctx=ast.Load(),
)
return super().visit_Subscript(node)
def compile_type_string(s: str, filename: str = ""):
tree = ast.parse(s, filename, "eval")
new_tree = ast.fix_missing_locations(TypeTransformer().visit(tree))
return compile(new_tree, filename, "eval")
def eval_type(t: str | _Ty, __globals: dict[str, Any], /):
if not isinstance(t, str):
return t
# `compile_type_string` adds `Union`, so we need it in scope
local_scope = {
"Union": Union,
"Tuple": tuple,
}
try:
# pylint: disable=eval-used
return _Ty(eval(compile_type_string(t), __globals, local_scope))
except Exception as e:
raise ValueError(f"Unable to evaluate type '{t}': {e}") from e
def union_types(types: list[_Ty]) -> _Ty:
assert len(types) > 0
t: Any = types[0]
for t2 in types[1:]:
t = Union[t, cast(Any, t2)]
return t
def union_to_set(t: _Ty) -> set[_Ty]:
s = str(t)
if s.startswith("typing.Union["):
return set(get_args(t))
elif s.startswith("typing.Optional["):
return {*union_to_set(get_args(t)[0]), _Ty(type(None))}
else:
return {t}
def is_subset_of(a: _Ty, b: _Ty) -> bool:
if a == b:
return True
return union_to_set(a).issubset(union_to_set(b))
def is_tuple(t: _Ty) -> bool:
s = str(t)
return s.startswith(("typing.Tuple[", "tuple["))
class FailedToParse:
pass
def get_type_annotations(fn: Callable) -> dict[str, _Ty | FailedToParse]:
"""Get the annotations for a function, with support for Python 3.8+"""
ann = getattr(fn, "__annotations__", None)
if ann is None:
return {}
type_annotations: dict[str, _Ty | FailedToParse] = {}
for k, v in ann.items():
try:
type_annotations[k] = eval_type(v, fn.__globals__)
except Exception:
type_annotations[k] = FailedToParse()
return type_annotations
def validate_return_type(return_type: _Ty, node: NodeData) -> None:
outputs = node.outputs
if len(outputs) == 0:
if return_type is not None and return_type is not type(None): # type: ignore
raise CheckFailedError(
"Return type should be 'None' because there are no outputs"
)
elif len(outputs) == 1:
o = outputs[0]
if o.associated_type is not None and not is_subset_of(
return_type, o.associated_type
):
raise CheckFailedError(
f"Return type '{return_type}' must be a subset of '{o.associated_type}'"
)
else:
if not is_tuple(return_type):
raise CheckFailedError(
f"Return type '{return_type}' must be a tuple because there are multiple outputs"
)
return_args = get_args(return_type)
if len(return_args) != len(outputs):
raise CheckFailedError(
f"Return type '{return_type}' must have the same number of arguments as there are outputs"
)
for o, return_arg in zip(outputs, return_args, strict=False):
if o.associated_type is not None and not is_subset_of(
return_arg, o.associated_type
):
raise CheckFailedError(
f"Return type of {o.label} '{return_arg}' must be a subset of '{o.associated_type}'"
)
def check_schema_types(
wrapped_func: Callable,
node: NodeData,
) -> None:
"""
Runtime validation for the number of inputs/outputs compared to the type args
"""
if node.kind != "regularNode":
return
ann = OrderedDict(get_type_annotations(wrapped_func))
# check return type
if "return" in ann:
return_type = ann.pop("return")
if not isinstance(return_type, FailedToParse):
validate_return_type(return_type, node)
# check arguments
arg_spec = inspect.getfullargspec(wrapped_func)
for arg in arg_spec.args:
if arg not in ann:
raise CheckFailedError(f"Missing type annotation for '{arg}'")
if node.node_context:
first = arg_spec.args[0]
if first != "context":
raise CheckFailedError(
f"Expected the first parameter to be 'context: NodeContext' but found '{first}'."
)
context_type = ann.pop(first)
if context_type != NodeContext: # type: ignore
raise CheckFailedError(
f"Expected type of 'context' to be 'api.NodeContext' but found '{context_type}'"
)
# check inputs
inputs = node.inputs
if arg_spec.varargs is not None:
if arg_spec.varargs not in ann:
raise CheckFailedError(f"Missing type annotation for '{arg_spec.varargs}'")
va_type = ann.pop(arg_spec.varargs)
# split inputs by varargs and non-varargs
varargs_inputs = inputs[len(ann) :]
inputs = inputs[: len(ann)]
total: list[_Ty] | None = []
for i in varargs_inputs:
associated_type = i.associated_type
if associated_type is not None and not isinstance(va_type, FailedToParse):
if not is_subset_of(associated_type, va_type):
raise CheckFailedError(
f"Input type of {i.label} '{associated_type}' is not assignable to varargs type '{va_type}'"
)
# append to total
if associated_type is not None:
if total is not None:
total.append(associated_type)
else:
total = None
if total is not None and not isinstance(va_type, FailedToParse):
total_type = union_types(total)
if total_type != va_type:
raise CheckFailedError(
f"Varargs type '{va_type}' should be equal to the union of all arguments '{total_type}'"
)
if len(ann) != len(inputs):
raise CheckFailedError(
f"Number of inputs and arguments don't match: {len(ann)=} != {len(inputs)=}"
)
for (a_name, a_type), i in zip(ann.items(), inputs, strict=False):
associated_type = i.associated_type
if (
associated_type is not None
and not isinstance(a_type, FailedToParse)
and a_type != associated_type
):
raise CheckFailedError(
f"Expected type of {i.label} ({a_name}) to be '{associated_type}' but found '{a_type}'"
)
def check_naming_conventions(
wrapped_func: Callable,
name: str,
fix: bool,
) -> None:
expected_name = (
name.lower()
.replace(" ", "_")
.replace("-", "_")
.replace("(", "")
.replace(")", "")
.replace("&", "and")
)
func_name = wrapped_func.__name__
file_path = pathlib.Path(inspect.getfile(wrapped_func))
file_name = file_path.stem
# check function name
if func_name != expected_name + "_node":
if not fix:
raise CheckFailedError(
f"Function name is '{func_name}', but it should be '{expected_name}_node'"
)
fixed_code = file_path.read_text(encoding="utf-8").replace(
f"def {func_name}(", f"def {expected_name}_node("
)
file_path.write_text(fixed_code, encoding="utf-8")
# check file name
if file_name != expected_name:
if not fix:
raise CheckFailedError(
f"File name is '{file_name}.py', but it should be '{expected_name}.py'"
)
os.rename(file_path, file_path.with_name(expected_name + ".py"))
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/node_context.py
================================================
import time
from abc import ABC, abstractmethod
from collections.abc import Callable
from pathlib import Path
from typing import Literal
from .settings import SettingsParser
class Aborted(Exception):
pass
class Progress(ABC):
@property
@abstractmethod
def aborted(self) -> bool:
"""
Returns whether the current operation was aborted.
"""
@property
@abstractmethod
def paused(self) -> bool:
"""
Returns whether the current operation was paused.
"""
def check_aborted(self) -> None:
"""
Raises an `Aborted` exception if the current operation was aborted. Does nothing otherwise.
"""
if self.aborted:
raise Aborted()
def suspend(self) -> None:
"""
If the operation was aborted, this method will throw an `Aborted` exception.
If the operation is paused, this method will wait until the operation is resumed or aborted.
"""
while True:
self.check_aborted()
if not self.paused:
break
time.sleep(0.1)
@abstractmethod
def set_progress(self, progress: float) -> None:
"""
Sets the progress of the current node execution. `progress` must be a value between 0 and 1.
Raises an `Aborted` exception if the current operation was aborted.
"""
def sub_progress(self, offset: float, length: float) -> "Progress":
"""
Returns a new `NodeProgress` object that represents a sub-progress of the current operation.
The progress range of the sub-progress is defined by `offset` and `length`. `offset` must be a value between 0
and 1, and `length` must be a positive value such that `offset + length <= 1`.
The real progress of the sub-progress is calculated as `offset + progress * length`, where `progress` is the
progress value passed to `set_progress` of the sub-progress.
"""
return _SubProgress(self, offset, length)
@staticmethod
def noop_progress() -> "Progress":
"""
Returns a `Progress` object that does nothing. It is never paused or aborted and does not report any progress.
"""
return _NoopProgress()
class _NoopProgress(Progress):
@property
def aborted(self) -> Literal[False]:
return False
@property
def paused(self) -> Literal[False]:
return False
def check_aborted(self) -> None:
pass
def suspend(self) -> None:
pass
def set_progress(self, progress: float) -> None:
pass
def sub_progress(self, offset: float, length: float) -> "Progress":
return _NoopProgress()
class _SubProgress(Progress):
def __init__(self, parent: Progress, offset: float, length: float) -> None:
self._parent = parent
self._offset = offset
self._length = length
@property
def aborted(self) -> bool:
return self._parent.aborted
@property
def paused(self) -> bool:
return self._parent.paused
def check_aborted(self) -> None:
self._parent.check_aborted()
def suspend(self) -> None:
self._parent.suspend()
def set_progress(self, progress: float) -> None:
self._parent.set_progress(self._offset + progress * self._length)
def sub_progress(self, offset: float, length: float) -> "_SubProgress":
return _SubProgress(
self._parent,
offset=self._offset + offset * self._length,
length=length * self._length,
)
class NodeContext(Progress, ABC):
"""
The execution context of the current node.
"""
@property
@abstractmethod
def settings(self) -> SettingsParser:
"""
Returns the settings of the current node execution.
"""
@property
@abstractmethod
def storage_dir(self) -> Path:
"""
The path of a directory where nodes can store files.
This directory persists between node executions, and its contents are shared between different nodes.
"""
@abstractmethod
def add_cleanup(
self, fn: Callable[[], None], after: Literal["node", "chain"] = "chain"
) -> None:
"""
Registers a function that will be called when the chain execution is finished (if set to chain mode) or after node execution is finished (node mode).
Registering the same function (object) twice will only result in the function being called once.
"""
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/node_data.py
================================================
from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from enum import Enum
from typing import Any, Generic, Protocol, TypeVar
import navi
from .group import NestedIdGroup
from .input import BaseInput
from .iter import Generator
from .output import BaseOutput
from .types import (
FeatureId,
InputId,
IterInputId,
IterOutputId,
NodeKind,
OutputId,
RunFn,
)
class IteratorInputInfo:
def __init__(
self,
inputs: int | InputId | list[int] | list[InputId] | list[int | InputId],
length_type: navi.ExpressionJson = "uint",
) -> None:
self.id: IterInputId = IterInputId(0)
self.inputs: list[InputId] = (
[InputId(x) for x in inputs]
if isinstance(inputs, list)
else [InputId(inputs)]
)
self.length_type: navi.ExpressionJson = length_type
def to_dict(self):
return {
"id": self.id,
"inputs": self.inputs,
"sequenceType": navi.named("Sequence", {"length": self.length_type}),
}
M_co = TypeVar("M_co", covariant=True)
class AnyConstructor(Protocol, Generic[M_co]):
def __call__(self, *args: Any, **kwargs: Any) -> M_co: ...
class IteratorOutputInfo:
def __init__(
self,
outputs: int | OutputId | list[int] | list[OutputId] | list[int | OutputId],
length_type: navi.ExpressionJson = "uint",
) -> None:
self.id: IterOutputId = IterOutputId(0)
self.outputs: list[OutputId] = (
[OutputId(x) for x in outputs]
if isinstance(outputs, list)
else [OutputId(outputs)]
)
self.length_type: navi.ExpressionJson = length_type
self._metadata_constructor: Any | None = None
self._item_types_fn: (
Callable[[Any], Mapping[OutputId, navi.ExpressionJson]] | None
) = None
def with_item_types(
self,
class_: AnyConstructor[M_co],
fn: Callable[[M_co], Mapping[OutputId, navi.ExpressionJson]],
):
self._metadata_constructor = class_
self._item_types_fn = fn
return self
def to_dict(self):
return {
"id": self.id,
"outputs": self.outputs,
"sequenceType": navi.named("Sequence", {"length": self.length_type}),
}
def get_broadcast_sequence_type(self, generator: Generator) -> navi.ExpressionJson:
return navi.named("Sequence", {"length": generator.expected_length})
def get_broadcast_item_types(
self, generator: Generator
) -> Mapping[OutputId, navi.ExpressionJson]:
if self._item_types_fn is not None and self._metadata_constructor is not None:
metadata = generator.metadata
if isinstance(metadata, self._metadata_constructor):
return self._item_types_fn(metadata)
return {}
class KeyInfo:
def __init__(self, data: dict[str, Any]) -> None:
self._data = data
@staticmethod
def enum(enum_input: InputId | int) -> KeyInfo:
return KeyInfo({"kind": "enum", "inputId": enum_input})
@staticmethod
def number(number_input: InputId | int) -> KeyInfo:
return KeyInfo({"kind": "number", "inputId": number_input})
@staticmethod
def type(expression: navi.ExpressionJson) -> KeyInfo:
return KeyInfo({"kind": "type", "expression": expression})
def to_dict(self):
return self._data
class SpecialSuggestion:
"""
A special suggestion in chaiNNer's context node selector.
A suggestion consists of 3 parts:
1. The search query to match. The query may optionally contain a pattern at the end
to supply a value to an input. E.g. `+{2}` will match the search query "+123"
and "123" will be parsed for the input with ID 2.
2. The name of the suggestion. This is the text that will be displayed in the
suggestion list.
3. The input values to supply to the node. This is a mapping of input IDs to the
values to supply to them. Values that aren't defined here will be left as
default values.
"""
def __init__(
self,
query: str,
*,
name: str | None = None,
inputs: Mapping[InputId | int, Any] = {},
) -> None:
self.query, self.parse_input = SpecialSuggestion._parse_query(query)
self.name = name
self.inputs: dict[InputId, Any] = {InputId(k): v for k, v in inputs.items()}
@staticmethod
def _parse_query(query: str) -> tuple[str, InputId | None]:
# e.g. "+{2}"
if "{" in query:
query, input_id = query.split("{")
input_id = int(input_id[:-1])
return query, InputId(input_id)
return query, None
def to_dict(self):
def convert_value(value: Any) -> Any:
if isinstance(value, bool):
return int(value)
if isinstance(value, Enum):
return value.value
return value
return {
"query": self.query,
"name": self.name,
"parseInput": self.parse_input,
"inputs": {k: convert_value(v) for k, v in self.inputs.items()},
}
@dataclass(frozen=True)
class NodeData:
schema_id: str
description: str
see_also: list[str]
name: str
icon: str
kind: NodeKind
inputs: list[BaseInput]
outputs: list[BaseOutput]
group_layout: list[InputId | NestedIdGroup]
iterable_inputs: list[IteratorInputInfo]
iterable_outputs: list[IteratorOutputInfo]
key_info: KeyInfo | None
suggestions: list[SpecialSuggestion]
side_effects: bool
deprecated: bool
node_context: bool
features: list[FeatureId]
run: RunFn
@property
def single_iterable_input(self) -> IteratorInputInfo:
assert len(self.iterable_inputs) == 1
return self.iterable_inputs[0]
@property
def single_iterable_output(self) -> IteratorOutputInfo:
assert len(self.iterable_outputs) == 1
return self.iterable_outputs[0]
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/output.py
================================================
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, Generic, Literal, TypeVar
import navi
from .types import InputId, OutputId
OutputKind = Literal["large-image", "tagged", "generic"]
BroadcastData = Mapping[str, object]
T = TypeVar("T")
class BaseOutput(Generic[T]):
def __init__(
self,
output_type: navi.ExpressionJson,
label: str,
kind: OutputKind = "generic",
has_handle: bool = True,
associated_type: Any = None,
) -> None:
self.output_type: navi.ExpressionJson = output_type
self.label: str = label
self.id: OutputId = OutputId(-1)
self.never_reason: str | None = None
self.kind: OutputKind = kind
self.has_handle: bool = has_handle
self.passthrough_of: InputId | None = None
self.associated_type: Any = associated_type
# Optional documentation
self.description: str | None = None
self.should_suggest: bool = False
def to_dict(self):
return {
"id": self.id,
"type": self.output_type,
"label": self.label,
"neverReason": self.never_reason,
"kind": self.kind,
"hasHandle": self.has_handle,
"passthroughOf": self.passthrough_of,
"description": self.description,
"suggest": self.should_suggest,
}
def with_id(self, output_id: OutputId | int):
self.id = OutputId(output_id)
return self
def with_never_reason(self, reason: str):
self.never_reason = reason
return self
def with_docs(self, *description: str):
self.description = "\n\n".join(description)
return self
def suggest(self):
self.should_suggest = True
return self
def as_passthrough_of(self, input_id: InputId | int):
self.passthrough_of = InputId(input_id)
return self
def get_broadcast_data(self, _value: T) -> BroadcastData | None:
return None
def get_broadcast_type(self, _value: T) -> navi.ExpressionJson | None:
return None
def enforce(self, value: object) -> T:
assert value is not None
return value # type: ignore
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/settings.py
================================================
from __future__ import annotations
from dataclasses import dataclass
from typing import TypedDict, Union
from sanic.log import logger
SettingsJson = dict[str, object]
JsonExecutionOptions = dict[str, SettingsJson]
class ExecutionOptions:
def __init__(
self,
backend_settings: JsonExecutionOptions,
) -> None:
self.__settings = backend_settings
self.__parsers: dict[str, SettingsParser] = {}
logger.info(f"Execution options: {self.__settings}")
@staticmethod
def parse(json: JsonExecutionOptions) -> ExecutionOptions:
return ExecutionOptions(backend_settings=json)
def get_package_settings_json(self, package_id: str) -> SettingsJson:
return self.__settings.get(package_id, {})
def get_package_settings(self, package_id: str) -> SettingsParser:
parser = self.__parsers.get(package_id)
if parser is None:
parser = SettingsParser(self.get_package_settings_json(package_id))
self.__parsers[package_id] = parser
return parser
class SettingsParser:
def __init__(self, raw: SettingsJson) -> None:
self.__settings = raw
def get_bool(self, key: str, default: bool) -> bool:
value = self.__settings.get(key, default)
if isinstance(value, bool):
return value
raise ValueError(f"Invalid bool value for {key}: {value}")
def get_int(self, key: str, default: int, parse_str: bool = False) -> int:
value = self.__settings.get(key, default)
if parse_str and isinstance(value, str):
return int(value)
if isinstance(value, int) and not isinstance(value, bool):
return value
raise ValueError(f"Invalid str value for {key}: {value}")
def get_str(self, key: str, default: str) -> str:
value = self.__settings.get(key, default)
if isinstance(value, str):
return value
raise ValueError(f"Invalid str value for {key}: {value}")
def get_cache_location(self, key: str) -> str | None:
value = self.__settings.get(key)
if isinstance(value, str) or value is None:
return value or None
raise ValueError(f"Invalid cache location value for {key}: {value}")
@dataclass
class ToggleSetting:
label: str
key: str
description: str
default: bool = False
disabled: bool = False
type: str = "toggle"
class DropdownOption(TypedDict):
label: str
value: str
@dataclass
class DropdownSetting:
label: str
key: str
description: str
options: list[DropdownOption]
default: str
disabled: bool = False
type: str = "dropdown"
@dataclass
class NumberSetting:
label: str
key: str
description: str
min: float
max: float
default: float = 0
disabled: bool = False
type: str = "number"
@dataclass
class CacheSetting:
label: str
key: str
description: str
directory: str
default: str = ""
disabled: bool = False
type: str = "cache"
Setting = Union[ToggleSetting, DropdownSetting, NumberSetting, CacheSetting]
================================================
FILE: MangaJaNaiConverterGui/backend/src/api/types.py
================================================
from __future__ import annotations
from collections.abc import Callable
from typing import Any, Literal, NewType
NodeId = NewType("NodeId", str)
InputId = NewType("InputId", int)
OutputId = NewType("OutputId", int)
IterInputId = NewType("IterInputId", int)
IterOutputId = NewType("IterOutputId", int)
FeatureId = NewType("FeatureId", str)
RunFn = Callable[..., Any]
NodeKind = Literal["regularNode", "generator", "collector"]
================================================
FILE: MangaJaNaiConverterGui/backend/src/device_list.py
================================================
import json
import os
import sys
sys.path.append(os.path.normpath(os.path.dirname(os.path.abspath(__file__))))
from accelerator_detection import get_accelerator_detector
# Get all available accelerator devices
detector = get_accelerator_detector()
all_devices = detector.available_devices
best_device = detector.get_best_device()
device_list = []
for device in all_devices:
device_info = {
"type": device.type.value,
"index": device.index,
"name": device.name,
"device_string": device.device_string,
"supports_fp16": device.supports_fp16,
"supports_bf16": device.supports_bf16,
}
if device.memory_total:
device_info["memory_total"] = device.memory_total
if device.memory_free:
device_info["memory_free"] = device.memory_free
device_list.append(device_info)
print(json.dumps({"all_devices": device_list, "best_device": all_devices.index(best_device)}))
================================================
FILE: MangaJaNaiConverterGui/backend/src/gpu.py
================================================
from __future__ import annotations
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from functools import cached_property
import pynvml as nv
from sanic.log import logger
_FP16_ARCH_ABILITY_MAP = {
nv.NVML_DEVICE_ARCH_KEPLER: False,
nv.NVML_DEVICE_ARCH_MAXWELL: False,
nv.NVML_DEVICE_ARCH_PASCAL: False,
nv.NVML_DEVICE_ARCH_VOLTA: True,
nv.NVML_DEVICE_ARCH_TURING: True,
nv.NVML_DEVICE_ARCH_AMPERE: True,
nv.NVML_DEVICE_ARCH_ADA: True,
nv.NVML_DEVICE_ARCH_HOPPER: True,
nv.NVML_DEVICE_ARCH_UNKNOWN: False,
}
@dataclass
class MemoryUsage:
total: int
used: int
free: int
@dataclass(frozen=True)
class NvDevice:
index: int
handle: nv.c_nvmlDevice_t
name: str
@staticmethod
def from_index(index: int) -> NvDevice:
handle = nv.nvmlDeviceGetHandleByIndex(index)
return NvDevice(
index=index,
handle=handle,
name=nv.nvmlDeviceGetName(handle),
)
@cached_property
def architecture(self) -> int:
# We catch and ignore errors to support older drivers that don't have nvmlDeviceGetArchitecture
try:
return nv.nvmlDeviceGetArchitecture(self.handle)
except Exception:
return nv.NVML_DEVICE_ARCH_UNKNOWN
@property
def supports_fp16(self):
arch = self.architecture
# This generation also contains the GTX 1600 cards, which do not support FP16.
if arch == nv.NVML_DEVICE_ARCH_TURING:
return "RTX" in self.name
# Future proofing. We can be reasonably sure that future architectures will support FP16.
return _FP16_ARCH_ABILITY_MAP.get(arch, arch > nv.NVML_DEVICE_ARCH_HOPPER)
def get_current_vram_usage(self) -> MemoryUsage:
info = nv.nvmlDeviceGetMemoryInfo(self.handle)
return MemoryUsage(info.total, info.used, info.free) # type: ignore
class NvInfo:
def __init__(
self, devices: Sequence[NvDevice], clean_up: Callable[[], None]
) -> None:
self.__devices: Sequence[NvDevice] = devices
self.__clean_up = clean_up
@staticmethod
def unavailable():
return NvInfo([], lambda: None)
def __del__(self) -> None:
self.__clean_up()
@property
def devices(self) -> Sequence[NvDevice]:
return self.__devices
@property
def is_available(self):
return len(self.devices) > 0
@property
def all_support_fp16(self) -> bool:
return all(gpu.supports_fp16 for gpu in self.devices)
def _try_nvml_init() -> bool | None:
try:
nv.nvmlInit()
return True
except Exception as e:
if isinstance(e, nv.NVMLError):
logger.info("No Nvidia GPU found, or invalid driver installed.")
else:
logger.info(
f"Unknown error occurred when trying to initialize Nvidia GPU: {e}"
)
return False
def _try_nvml_shutdown() -> None:
try:
nv.nvmlShutdown()
except Exception:
logger.warn("Failed to shut down Nvidia GPU.", exc_info=True)
def _get_nvidia_info() -> NvInfo:
if not _try_nvml_init():
return NvInfo.unavailable()
try:
device_count = nv.nvmlDeviceGetCount()
devices = [NvDevice.from_index(i) for i in range(device_count)]
return NvInfo(devices, _try_nvml_shutdown)
except Exception as e:
logger.info(f"Unknown error occurred when trying to initialize Nvidia GPU: {e}")
_try_nvml_shutdown()
return NvInfo.unavailable()
nvidia = _get_nvidia_info()
__all__ = ["MemoryUsage", "NvDevice", "NvInfo", "nvidia"]
================================================
FILE: MangaJaNaiConverterGui/backend/src/navi.py
================================================
from __future__ import annotations
import math
from typing import Literal, TypedDict, Union
NumberJson = Union[int, float, Literal["inf"], Literal["-inf"], Literal["NaN"]]
def to_number_json(n: int | float) -> NumberJson:
if math.isnan(n):
return "NaN"
if n == float("inf"):
return "inf"
if n == float("-inf"):
return "-inf"
return n
def from_number_json(n: NumberJson) -> int | float:
if n == "NaN":
return float("nan")
if n == "inf":
return float("inf")
if n == "-inf":
return float("-inf")
return n
ExpressionJson = Union[
str,
int,
bool,
"NumericLiteralTypeJson",
"IntervalTypeJson",
"IntIntervalTypeJson",
"StringLiteralTypeJson",
"UnionExpressionJson",
"IntersectionExpressionJson",
"NamedExpressionJson",
"FieldAccessExpressionJson",
"FunctionCallExpressionJson",
"MatchExpressionJson",
list["ExpressionJson"],
list[int],
list[str],
]
class NumericLiteralTypeJson(TypedDict):
type: Literal["numeric-literal"]
value: NumberJson
class IntervalTypeJson(TypedDict):
type: Literal["interval"]
min: NumberJson
max: NumberJson
class IntIntervalTypeJson(TypedDict):
type: Literal["int-interval"]
min: NumberJson
max: NumberJson
class StringLiteralTypeJson(TypedDict):
type: Literal["string-literal"]
value: str
class UnionExpressionJson(TypedDict):
type: Literal["union"]
items: list[ExpressionJson]
class IntersectionExpressionJson(TypedDict):
type: Literal["intersection"]
items: list[ExpressionJson]
class NamedExpressionJson(TypedDict):
type: Literal["named"]
name: str
fields: dict[str, ExpressionJson] | None
class FieldAccessExpressionJson(TypedDict):
type: Literal["field-access"]
of: ExpressionJson
field: str
class FunctionCallExpressionJson(TypedDict):
type: Literal["function-call"]
name: str
args: list[ExpressionJson]
class MatchArmJson(TypedDict):
pattern: ExpressionJson
binding: str | None
to: ExpressionJson
class MatchExpressionJson(TypedDict):
type: Literal["match"]
of: ExpressionJson
arms: list[MatchArmJson]
def literal(value: str | (int | float)) -> ExpressionJson:
if isinstance(value, str):
return {
"type": "string-literal",
"value": value,
}
return {
"type": "numeric-literal",
"value": to_number_json(value),
}
def interval(
min_value: int | (float | None) = None,
max_value: int | (float | None) = None,
) -> ExpressionJson:
return {
"type": "interval",
"min": to_number_json(min_value if min_value is not None else float("-inf")),
"max": to_number_json(max_value if max_value is not None else float("inf")),
}
def int_interval(
min_value: int | (float | None) = None,
max_value: int | (float | None) = None,
) -> ExpressionJson:
return {
"type": "int-interval",
"min": to_number_json(min_value if min_value is not None else float("-inf")),
"max": to_number_json(max_value if max_value is not None else float("inf")),
}
def union(*items: ExpressionJson) -> ExpressionJson:
return {"type": "union", "items": list(items)}
def intersect(*items: ExpressionJson) -> ExpressionJson:
return {"type": "intersection", "items": list(items)}
def intersect_with_error(*items: ExpressionJson) -> ExpressionJson:
return union(intersect(*items), *[intersect("Error", item) for item in items])
def named(name: str, fields: dict[str, ExpressionJson] | None = None) -> ExpressionJson:
return {"type": "named", "name": name, "fields": fields}
def field(of: ExpressionJson, field_name: str) -> ExpressionJson:
return {"type": "field-access", "of": of, "field": field_name}
def fn(name: str, *args: ExpressionJson) -> ExpressionJson:
return {"type": "function-call", "name": name, "args": list(args)}
def match(
of: ExpressionJson,
*args: tuple[ExpressionJson, str | None, ExpressionJson],
default: ExpressionJson | None = None,
) -> ExpressionJson:
arms: list[MatchArmJson] = []
for pattern, binding, to in args:
arms.append({"pattern": pattern, "binding": binding, "to": to})
if default is not None:
arms.append({"pattern": "any", "binding": None, "to": default})
return {"type": "match", "of": of, "arms": arms}
def Image( # noqa: N802
width: ExpressionJson | None = None,
height: ExpressionJson | None = None,
channels: ExpressionJson | None = None,
width_as: ExpressionJson | None = None,
height_as: ExpressionJson | None = None,
channels_as: ExpressionJson | None = None,
size_as: ExpressionJson | None = None,
) -> ExpressionJson:
fields: dict[str, ExpressionJson] = {}
if width is not None:
fields["width"] = width
if height is not None:
fields["height"] = height
if channels is not None:
fields["channels"] = channels
if width_as is not None:
fields["width"] = field(width_as, "width")
if height_as is not None:
fields["height"] = field(height_as, "height")
if channels_as is not None:
fields["channels"] = field(channels_as, "channels")
if size_as is not None:
fields["width"] = field(size_as, "width")
fields["height"] = field(size_as, "height")
return named("Image", fields)
def Color( # noqa: N802
channels: ExpressionJson | None = None,
channels_as: ExpressionJson | None = None,
) -> ExpressionJson:
fields: dict[str, ExpressionJson] = {}
if channels is not None:
fields["channels"] = channels
if channels_as is not None:
fields["channels"] = field(channels_as, "channels")
return named("Color", fields)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/__init__.py
================================================
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/condition.py
================================================
from __future__ import annotations
from collections.abc import Iterable
from enum import Enum
from typing import Literal, TypedDict, Union
import navi
from api import InputId
InputValue = Union[int, str]
EnumValues = Union[
InputValue,
Enum,
Iterable[str],
Iterable[int],
Iterable[Enum],
]
ConditionJson = Union[
"_AndConditionJson",
"_OrConditionJson",
"_NotConditionJson",
"_EnumConditionJson",
"_TypeConditionJson",
]
class _AndConditionJson(TypedDict):
kind: Literal["and"]
items: list[ConditionJson]
class _OrConditionJson(TypedDict):
kind: Literal["or"]
items: list[ConditionJson]
class _NotConditionJson(TypedDict):
kind: Literal["not"]
condition: ConditionJson
class _EnumConditionJson(TypedDict):
kind: Literal["enum"]
enum: InputId
values: list[str | int]
class _TypeConditionJson(TypedDict):
kind: Literal["type"]
input: InputId
condition: navi.ExpressionJson
ifNotConnected: bool
class Condition:
def __init__(self, value: ConditionJson) -> None:
self._value: ConditionJson = value
def to_json(self):
return self._value
def __and__(self, other: Condition) -> Condition:
return Condition({"kind": "and", "items": [self._value, other._value]})
def __or__(self, other: Condition) -> Condition:
return Condition({"kind": "or", "items": [self._value, other._value]})
def __invert__(self) -> Condition:
return Condition({"kind": "not", "condition": self._value})
@staticmethod
def enum(enum: int, values: EnumValues) -> Condition:
"""
A condition to check whether a certain dropdown/enum input has a certain value.
"""
v: list[str | int] = []
def convert(value: int | str | Enum) -> None:
if isinstance(value, int | str):
v.append(value)
else:
enum_value = value.value
assert isinstance(enum_value, int | str)
v.append(enum_value)
if isinstance(values, int | str | Enum):
convert(values)
else:
for value in values:
convert(value)
return Condition(
{
"kind": "enum",
"enum": InputId(enum),
"values": v,
}
)
@staticmethod
def bool(input_id: int, value: bool) -> Condition:
"""
A condition to check whether a certain bool input has a certain value.
"""
return Condition(
{
"kind": "enum",
"enum": InputId(input_id),
"values": [int(value)],
}
)
@staticmethod
def type(
input_id: int,
condition: navi.ExpressionJson,
if_not_connected: bool = False,
) -> Condition:
"""
A condition to check whether a certain input is compatible a certain type.
Here "compatible" is defined as overlapping.
"""
return Condition(
{
"kind": "type",
"input": InputId(input_id),
"condition": condition,
"ifNotConnected": if_not_connected,
}
)
@staticmethod
def const(value: bool) -> Condition:
if value:
return Condition({"kind": "and", "items": []})
return Condition({"kind": "or", "items": []})
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/group.py
================================================
from typing import Any, Generic, NewType, TypeVar, Union
from base_types import InputId
from nodes.base_input import BaseInput
T = TypeVar("T")
GroupId = NewType("GroupId", int)
class GroupInfo:
def __init__(
self,
group_id: GroupId,
kind: str,
options: dict[str, Any] | None = None,
) -> None:
self.id: GroupId = group_id
self.kind: str = kind
self.options: dict[str, Any] = {} if options is None else options
class Group(Generic[T]):
def __init__(self, info: GroupInfo, items: list[T]) -> None:
self.info: GroupInfo = info
self.items: list[T] = items
def toDict(self):
return {
"id": self.info.id,
"kind": self.info.kind,
"options": self.info.options,
"items": [i.toDict() if isinstance(i, Group) else i for i in self.items],
}
NestedGroup = Group[Union[BaseInput, "NestedGroup"]]
NestedIdGroup = Group[Union[InputId, "NestedIdGroup"]]
# pylint: disable-next=redefined-builtin
def group(kind: str, options: dict[str, Any] | None = None, id: int = -1):
info = GroupInfo(GroupId(id), kind, options)
def ret(*items: BaseInput | NestedGroup) -> NestedGroup:
return Group(info, list(items))
return ret
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/groups.py
================================================
from __future__ import annotations
from typing import Union
from api import BaseInput, NestedGroup, group
from .condition import Condition, EnumValues, InputValue
RawEnumValues = Union[
InputValue, list[str], list[int], tuple[str, ...], tuple[int, ...]
]
def if_group(condition: Condition):
return group("conditional", {"condition": condition.to_json()})
def if_enum_group(enum: int, condition: EnumValues):
return if_group(Condition.enum(enum, condition))
def required(condition: Condition | None = None):
"""
Given generic inputs (meaning of kind "generic") that are optional, this group marks them as
being required under the given condition. If no condition is given, `True` will be used.
In addition to the given condition, if the require group is nested within conditional group
(`if_group` and derivatives), then the conditions of all ancestor conditional groups must also
be met.
Note that this group only guarantees **best effort**. It cannot guarantee that the optional
input is going to have a value if the condition is met. You must always check `None`.
Example:
```py
if_group(someCondition)(
required()(
GenericInput("Foo").make_optional(),
)
)
```
In this example, the input "Foo" is required if and only if the input is visible (by virtue of
the parent conditional group).
"""
if condition is None:
condition = Condition.const(True)
return group("required", {"condition": condition.to_json()})
def seed_group(seed_input: BaseInput):
"""
This groups is a wrapper around the `SeedInput`. It changes its visual appearance and adds a
little button for users to click on to generate a new seed.
All `SeedInput`s must be wrapped in this group.
Example:
```py
seed_group(SeedInput())
```
"""
return group("seed")(seed_input)
def optional_list_group(*inputs: BaseInput | NestedGroup):
"""
This groups wraps around optional inputs and displays them as a list.
This can be used to create nodes that have a variable number of inputs. The user will initially
see no inputs, but can add as many inputs as the group contains. While not true varargs, this
can be used to create a similar effect.
See the Text Append node for an example.
"""
return group("optional-list")(*inputs)
def linked_inputs_group(*inputs: BaseInput):
"""
This group wraps around inputs of the same type. It ensures that all inputs have the same
value.
"The same type" here not only refers to the Navi type of those inputs. All possible values
from all inputs must also be valid values for all other inputs. This typically necessitates
that the inputs are of the same class and use the same parameters.
"""
return group("linked-inputs")(*inputs)
def ncnn_file_inputs_group(param_input: BaseInput, bin_input: BaseInput):
"""
This group wraps around 2 .param and .bin file inputs and synchronizes them in the UI.
"""
return group("ncnn-file-inputs")(param_input, bin_input)
def from_to_dropdowns_group(from_dd: BaseInput, to_dd: BaseInput):
"""
This group wraps around 2 dropdown inputs that will be displayed as
`[From] -> [To]` in the UI.
"""
return group("from-to-dropdowns")(from_dd, to_dd)
def icon_set_group(label: str):
"""
This group causes the given boolean inputs to be displayed as a set of icons instead of
checkboxes. The icons are specified by the `icons` parameter.
"""
return group("icon-set", {"label": label})
def menu_icon_row_group():
"""
This group displays multiple icon-only inputs and groups as a row of icons. Only a few inputs
and groups are supported.
This group is intended to be used in the "Text As Image" node.
"""
return group("menu-icon-row")
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/__init__.py
================================================
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/blend.py
================================================
from enum import Enum
import cv2
import numpy as np
from ..utils.utils import get_h_w_c
from .image_utils import as_target_channels, normalize, to_uint8
class BlendMode(Enum):
NORMAL = 0
DARKEN = 2
MULTIPLY = 1
COLOR_BURN = 5
LINEAR_BURN = 22
LIGHTEN = 3
SCREEN = 12
COLOR_DODGE = 6
ADD = 4
OVERLAY = 9
SOFT_LIGHT = 17
HARD_LIGHT = 18
VIVID_LIGHT = 19
LINEAR_LIGHT = 20
PIN_LIGHT = 21
REFLECT = 7
GLOW = 8
DIFFERENCE = 10
EXCLUSION = 16
NEGATION = 11
SUBTRACT = 14
DIVIDE = 15
XOR = 13
__normalized = {
BlendMode.NORMAL: True,
BlendMode.MULTIPLY: True,
BlendMode.DARKEN: True,
BlendMode.LIGHTEN: True,
BlendMode.ADD: False,
BlendMode.COLOR_BURN: False,
BlendMode.COLOR_DODGE: False,
BlendMode.REFLECT: False,
BlendMode.GLOW: False,
BlendMode.OVERLAY: True,
BlendMode.DIFFERENCE: True,
BlendMode.NEGATION: True,
BlendMode.SCREEN: True,
BlendMode.XOR: True,
BlendMode.SUBTRACT: False,
BlendMode.DIVIDE: False,
BlendMode.EXCLUSION: True,
BlendMode.SOFT_LIGHT: True,
BlendMode.HARD_LIGHT: True,
BlendMode.VIVID_LIGHT: False,
BlendMode.LINEAR_LIGHT: False,
BlendMode.PIN_LIGHT: True,
BlendMode.LINEAR_BURN: False,
}
def blend_mode_normalized(blend_mode: BlendMode) -> bool:
"""
Returns whether the given blend mode is guaranteed to produce normalized results (value between 0 and 1).
"""
return __normalized.get(blend_mode, False)
class ImageBlender:
"""Class for compositing images using different blending modes."""
def __init__(self) -> None:
self.modes = {
BlendMode.NORMAL: self.__normal,
BlendMode.MULTIPLY: self.__multiply,
BlendMode.DARKEN: self.__darken,
BlendMode.LIGHTEN: self.__lighten,
BlendMode.ADD: self.__add,
BlendMode.COLOR_BURN: self.__color_burn,
BlendMode.COLOR_DODGE: self.__color_dodge,
BlendMode.REFLECT: self.__reflect,
BlendMode.GLOW: self.__glow,
BlendMode.OVERLAY: self.__overlay,
BlendMode.DIFFERENCE: self.__difference,
BlendMode.NEGATION: self.__negation,
BlendMode.SCREEN: self.__screen,
BlendMode.XOR: self.__xor,
BlendMode.SUBTRACT: self.__subtract,
BlendMode.DIVIDE: self.__divide,
BlendMode.EXCLUSION: self.__exclusion,
BlendMode.SOFT_LIGHT: self.__soft_light,
BlendMode.HARD_LIGHT: self.__hard_light,
BlendMode.VIVID_LIGHT: self.__vivid_light,
BlendMode.LINEAR_LIGHT: self.__linear_light,
BlendMode.PIN_LIGHT: self.__pin_light,
BlendMode.LINEAR_BURN: self.__linear_burn,
}
def apply_blend(
self, a: np.ndarray, b: np.ndarray, blend_mode: BlendMode
) -> np.ndarray:
return self.modes[blend_mode](a, b)
def __normal(self, a: np.ndarray, _: np.ndarray) -> np.ndarray:
return a
def __multiply(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a * b
def __darken(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.minimum(a, b)
def __lighten(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.maximum(a, b)
def __add(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a + b
def __color_burn(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(
a == 0, 0, np.maximum(0, (1 - ((1 - b) / np.maximum(0.0001, a))))
)
def __color_dodge(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(a == 1, 1, np.minimum(1, b / np.maximum(0.0001, (1 - a))))
def __reflect(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(a == 1, 1, np.minimum(1, b * b / np.maximum(0.0001, 1 - a)))
def __glow(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(b == 1, 1, np.minimum(1, a * a / np.maximum(0.0001, 1 - b)))
def __overlay(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(b < 0.5, 2 * b * a, 1 - 2 * (1 - b) * (1 - a))
def __difference(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.asarray(cv2.absdiff(a, b))
def __negation(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return 1 - cv2.absdiff(1 - b, a) # type: ignore
def __screen(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a + b - (a * b) # type: ignore
def __xor(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return normalize(
np.bitwise_xor(to_uint8(a, normalized=True), to_uint8(b, normalized=True))
)
def __subtract(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return b - a
def __divide(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return b / np.maximum(0.0001, a)
def __exclusion(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a * (1 - b) + b * (1 - a)
def __soft_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
l = 2 * b * a + np.square(b) * (1 - 2 * a)
h = np.sqrt(b) * (2 * a - 1) + 2 * b * (1 - a)
return np.where(a <= 0.5, l, h)
def __hard_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(a <= 0.5, 2 * a * b, 1 - 2 * (1 - a) * (1 - b))
def __vivid_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(
a <= 0.5, self.__color_burn(2 * a, b), self.__color_dodge(2 * (a - 0.5), b)
)
def __linear_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return b + 2 * a - 1
def __pin_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
x = 2 * a
y = x - 1
return np.where(b < y, y, np.where(b > x, x, b))
def __linear_burn(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a + b - 1
def blend_images(overlay: np.ndarray, base: np.ndarray, blend_mode: BlendMode):
"""
Changes the given image to the background overlayed with the image.
The 2 given images must be the same size and their values must be between 0 and 1.
The returned image is guaranteed to have values between 0 and 1.
If the 2 given images have a different number of channels, then the returned image
will have maximum of the two.
Only grayscale, RGB, and RGBA images are supported.
"""
o_shape = get_h_w_c(overlay)
b_shape = get_h_w_c(base)
assert (
o_shape[:2] == b_shape[:2]
), "The overlay and the base image must have the same size"
def assert_sane(c: int, name: str) -> None:
sane = c in (1, 3, 4)
assert sane, f"The {name} has to be a grayscale, RGB, or RGBA image"
o_channels = o_shape[2]
b_channels = b_shape[2]
assert_sane(o_channels, "overlay layer")
assert_sane(b_channels, "base layer")
blender = ImageBlender()
target_c = max(o_channels, b_channels)
needs_clipping = not blend_mode_normalized(blend_mode)
if target_c == 4 and b_channels < 4:
base = as_target_channels(base, 3)
# The general algorithm below can be optimized because we know that b_a is 1
o_a = np.dstack((overlay[:, :, 3],) * 3)
o_rgb = overlay[:, :, :3]
blend_rgb = blender.apply_blend(o_rgb, base, blend_mode)
final_rgb = o_a * blend_rgb + (1 - o_a) * base # type: ignore
if needs_clipping:
final_rgb = np.clip(final_rgb, 0, 1)
return as_target_channels(final_rgb, 4)
overlay = as_target_channels(overlay, target_c)
base = as_target_channels(base, target_c)
if target_c in (1, 3):
# We don't need to do any alpha blending, so the images can blended directly
result = blender.apply_blend(overlay, base, blend_mode)
if needs_clipping:
result = np.clip(result, 0, 1)
return result
# do the alpha blending for RGBA
o_a = overlay[:, :, 3]
b_a = base[:, :, 3]
o_rgb = overlay[:, :, :3]
b_rgb = base[:, :, :3]
final_a = 1 - (1 - o_a) * (1 - b_a)
blend_strength = o_a * b_a
o_strength = o_a - blend_strength # type: ignore
b_strength = b_a - blend_strength # type: ignore
blend_rgb = blender.apply_blend(o_rgb, b_rgb, blend_mode)
final_rgb = (
(np.dstack((o_strength,) * 3) * o_rgb)
+ (np.dstack((b_strength,) * 3) * b_rgb)
+ (np.dstack((blend_strength,) * 3) * blend_rgb)
)
final_rgb /= np.maximum(np.dstack((final_a,) * 3), 0.0001) # type: ignore
final_rgb = np.clip(final_rgb, 0, 1)
result = np.concatenate([final_rgb, np.expand_dims(final_a, axis=2)], axis=2)
if needs_clipping:
result = np.clip(result, 0, 1)
return result
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/color/__init__.py
================================================
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/color/color.py
================================================
from __future__ import annotations
import json
from collections.abc import Iterable
from typing import Literal, TypedDict, Union, cast
import numpy as np
from nodes.utils.utils import get_h_w_c
FloatLike = Union[np.floating, float]
def _norm(n: FloatLike) -> float:
return max(0, min(float(n), 1))
ColorJsonKind = Literal["grayscale", "rgb", "rgba"]
class ColorJson(TypedDict):
kind: ColorJsonKind
values: list[float]
class Color:
def __init__(self, value: tuple[float, ...]) -> None:
assert len(value) >= 1
self.value: tuple[float, ...] = value
@property
def channels(self) -> int:
return len(self.value)
@staticmethod
def gray(gray: FloatLike) -> Color:
return Color((_norm(gray),))
@staticmethod
def bgr(value: Iterable[FloatLike]) -> Color:
t = tuple(map(_norm, value))
assert len(t) == 3
return Color(t)
@staticmethod
def bgra(value: Iterable[FloatLike]) -> Color:
t = tuple(map(_norm, value))
assert len(t) == 4
return Color(t)
@staticmethod
def from_1x1_image(img: np.ndarray) -> Color:
h, w, c = get_h_w_c(img)
assert h == w == 1
if c == 1:
return Color.gray(img.flat[0])
elif c == 3:
return Color.bgr(img.flat)
elif c == 4:
return Color.bgra(img.flat)
else:
raise AssertionError("Only grayscale, RGB, and RGBA colors are supported.")
@staticmethod
def from_json(color_json: ColorJson | str) -> Color:
if isinstance(color_json, str):
color_json = cast(ColorJson, json.loads(color_json))
kind = color_json["kind"]
values = color_json["values"]
if kind == "grayscale":
assert len(values) == 1
return Color.gray(values[0])
elif kind == "rgb":
assert len(values) == 3
return Color.bgr([values[2], values[1], values[0]])
elif kind == "rgba":
assert len(values) == 4
return Color.bgra([values[2], values[1], values[0], values[3]])
else:
raise AssertionError(f"Unknown color kind {kind}")
def to_1x1_image(self) -> np.ndarray:
return self.to_image(1, 1)
def to_image(self, width: int, height: int) -> np.ndarray:
v = self.value
if len(v) == 1:
return np.full((height, width), v[0], dtype=np.float32)
else:
return np.full((height, width, len(v)), v, dtype=np.float32)
def to_json(self) -> ColorJson:
values = list(self.value)
kind: ColorJsonKind
if len(values) == 1:
kind = "grayscale"
elif len(values) == 3:
kind = "rgb"
values = [values[2], values[1], values[0]]
elif len(values) == 4:
kind = "rgba"
values = [values[2], values[1], values[0], values[3]]
else:
raise AssertionError(f"Colors with {len(values)} are not supported.")
return {"kind": kind, "values": values}
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/color/convert.py
================================================
from __future__ import annotations
from collections.abc import Callable, Iterable
from dataclasses import dataclass, field
from typing import Generic, TypeVar
import numpy as np
from sanic.log import logger
from .convert_data import color_spaces, color_spaces_or_detectors, conversions
from .convert_model import (
ColorSpace,
ColorSpaceDetector,
Conversion,
assert_input_channels,
assert_output_channels,
)
def color_space_from_id(id_: int) -> ColorSpace:
for c in color_spaces:
if c.id == id_:
return c
raise ValueError(f"There is no color space with the id {id_}.")
def color_space_or_detector_from_id(id_: int) -> ColorSpace | ColorSpaceDetector:
for c in color_spaces_or_detectors:
if c.id == id_:
return c
raise ValueError(f"There is no color space with the id {id_}.")
T = TypeVar("T")
@dataclass(order=True)
class __ProcessingItem(Generic[T]): # noqa: N801
cost: int
path: list[T] = field(compare=False)
def get_shortest_path(
start: T,
is_destination: Callable[[T], bool],
get_next: Callable[[T], Iterable[tuple[int, T]]],
) -> list[T] | None:
"""A simple implementation of Dijkstra's"""
processed: set[T] = set()
front: dict[T, __ProcessingItem] = {
start: __ProcessingItem(cost=0, path=[start]),
}
while len(front) > 0:
best = None
for x in front.values():
if best is None:
best = x
elif x.cost < best.cost:
best = x
assert best is not None
current = best.path[-1]
del front[current]
processed.add(current)
if is_destination(current):
return best.path
for cost, to in get_next(current):
total_cost = best.cost + cost
old = front.get(to, None)
if old is None:
if to not in processed:
new_path = best.path.copy()
new_path.append(to)
front[to] = __ProcessingItem(cost=total_cost, path=new_path)
elif old.cost > total_cost:
old.cost = total_cost
old.path.clear()
old.path.extend(best.path)
old.path.append(to)
__conversions_map: dict[ColorSpace, list[Conversion]] = {}
for conversion in conversions:
l = __conversions_map.get(conversion.input, [])
if len(l) == 0:
__conversions_map[conversion.input] = l
l.append(conversion)
def convert(
img: np.ndarray,
input_: ColorSpace | ColorSpaceDetector,
output: ColorSpace,
) -> np.ndarray:
if isinstance(input_, ColorSpaceDetector):
input_ = input_.detect(img)
assert_input_channels(img, input_, output)
if input_ == output:
return img
path = get_shortest_path(
input_,
is_destination=lambda i: i == output,
get_next=lambda i: [(c.cost, c.output) for c in __conversions_map.get(i, [])],
)
if path is None:
raise ValueError(f"Conversion {input_.name} -> {output.name} is not possible.")
logger.debug(f"Converting color using the path {' -> '.join(x.name for x in path)}")
for i in range(1, len(path)):
curr_in = path[i - 1]
curr_out = path[i]
conv = None
for c in __conversions_map.get(curr_in, []):
if c.output == curr_out:
conv = c
break
assert conv is not None
img = conv.convert(img)
assert_output_channels(img, input_, output)
return img
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/color/convert_data.py
================================================
from __future__ import annotations
import math
import cv2
import numpy as np
from ...utils.utils import get_h_w_c
from .convert_model import ColorSpace, ColorSpaceDetector, Conversion
GRAY = ColorSpace(0, "Gray", 1)
RGB = ColorSpace(1, "RGB", 3)
RGBA = ColorSpace(2, "RGBA", 4)
YUV = ColorSpace(3, "YUV", 3)
HSV = ColorSpace(4, "HSV", 3)
HSL = ColorSpace(5, "HSL", 3)
CMYK = ColorSpace(6, "CMYK", 4)
YUVA = ColorSpace(7, "YUVA", 4)
HSVA = ColorSpace(8, "HSVA", 4)
HSLA = ColorSpace(9, "HSLA", 4)
LAB = ColorSpace(10, "L*a*b*", 3)
LABA = ColorSpace(11, "L*a*b*A", 4)
LCH = ColorSpace(12, "L*C*h°", 3)
LCHA = ColorSpace(13, "L*C*h°A", 4)
RGB_LIKE = ColorSpaceDetector(1000, "RGB", [GRAY, RGB, RGBA])
YUV_LIKE = ColorSpaceDetector(1001, "YUV", [YUV, YUVA])
HSV_LIKE = ColorSpaceDetector(1002, "HSV", [HSV, HSVA])
HSL_LIKE = ColorSpaceDetector(1003, "HSL", [HSL, HSLA])
LAB_LIKE = ColorSpaceDetector(1004, "L*a*b*", [LAB, LABA])
LCH_LIKE = ColorSpaceDetector(1005, "L*C*h°", [LCH, LCHA])
ALPHA_PAIRS: dict[ColorSpace, ColorSpace] = {
RGB: RGBA,
YUV: YUVA,
HSV: HSVA,
HSL: HSLA,
LAB: LABA,
LCH: LCHA,
}
def is_alpha_partner(c: ColorSpace) -> bool:
"""
Whether this color space is returned by `get_alpha_partner` for some input.
"""
return c in ALPHA_PAIRS.values()
def get_alpha_partner(c: ColorSpace) -> ColorSpace | None:
"""
If the given color does NOT have an alpha channel and there exists another color space that this equivalent to the given one but does have an alpha, then the color space with an alpha channel will be returned, `None` otherwise.
E.g. RGB will return RGBA and RGBA will return None.
"""
return ALPHA_PAIRS.get(c, None)
color_spaces: list[ColorSpace] = [
RGB,
RGBA,
GRAY,
YUV,
YUVA,
HSV,
HSVA,
HSL,
HSLA,
CMYK,
LAB,
LABA,
LCH,
LCHA,
]
color_spaces_or_detectors: list[ColorSpace | ColorSpaceDetector] = [
RGB_LIKE,
GRAY,
YUV_LIKE,
HSV_LIKE,
HSL_LIKE,
CMYK,
LAB_LIKE,
LCH_LIKE,
]
def __rev3(image: np.ndarray) -> np.ndarray:
c = get_h_w_c(image)[2]
assert c == 3, "Expected a 3-channel image"
return np.stack([image[:, :, 2], image[:, :, 1], image[:, :, 0]], axis=2)
def __rgb_to_hsv(img: np.ndarray) -> np.ndarray:
img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
img[:, :, 0] /= 360 # type: ignore
return __rev3(img)
def __hsv_to_rgb(img: np.ndarray) -> np.ndarray:
img = __rev3(img)
img[:, :, 0] *= 360
return cv2.cvtColor(img, cv2.COLOR_HSV2BGR)
def __rgb_to_hsl(img: np.ndarray) -> np.ndarray:
img = cv2.cvtColor(img, cv2.COLOR_BGR2HLS)
h = img[:, :, 0] / 360 # type: ignore
l = img[:, :, 1]
s = img[:, :, 2]
return cv2.merge((l, s, h))
def __hsl_to_rgb(img: np.ndarray) -> np.ndarray:
h = img[:, :, 2] * 360
s = img[:, :, 1]
l = img[:, :, 0]
return cv2.cvtColor(cv2.merge((h, l, s)), cv2.COLOR_HLS2BGR)
def __hsv_to_hsl(img: np.ndarray) -> np.ndarray:
# the S and HSV and HSL are different, only the H is the same
h = img[:, :, 2]
hls = cv2.cvtColor(__hsv_to_rgb(img), cv2.COLOR_BGR2HLS)
l = hls[:, :, 1]
s = hls[:, :, 2]
return cv2.merge((l, s, h))
def __hsl_to_hsv(img: np.ndarray) -> np.ndarray:
# the S and HSV and HSL are different, only the H is the same
h = img[:, :, 2]
hsv = cv2.cvtColor(__hsl_to_rgb(img), cv2.COLOR_BGR2HSV)
s = hsv[:, :, 1]
v = hsv[:, :, 2]
return cv2.merge((v, s, h))
def __rgb_to_cmyk(img: np.ndarray) -> np.ndarray:
b, g, r = img[:, :, 0], img[:, :, 1], img[:, :, 2]
maximum = np.max(img, axis=2)
soft_max = np.maximum(maximum, 0.001)
c = 1 - r / soft_max
m = 1 - g / soft_max
y = 1 - b / soft_max
k = 1 - maximum
return cv2.merge((y, m, c, k))
def __cmyk_to_rgb(img: np.ndarray) -> np.ndarray:
y, m, c, k = img[:, :, 0], img[:, :, 1], img[:, :, 2], img[:, :, 3]
maximum = 1 - k
r = (1 - c) * maximum
g = (1 - m) * maximum
b = (1 - y) * maximum
return cv2.merge((b, g, r))
def __rgb_to_lab(img: np.ndarray) -> np.ndarray:
# 0≤L≤100 , -127≤a≤127, -127≤b≤127
img = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
l = img[:, :, 0] / 100 # type: ignore
a = (img[:, :, 1] + 127) / 254 # type: ignore
b = (img[:, :, 2] + 127) / 254 # type: ignore
return cv2.merge((b, a, l))
def __lab_to_rgb(img: np.ndarray) -> np.ndarray:
# 0≤L≤100 , -127≤a≤127, -127≤b≤127
l = img[:, :, 2] * 100
a = img[:, :, 1] * 254 - 127
b = img[:, :, 0] * 254 - 127
return cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR)
def __lab_to_lch(img: np.ndarray) -> np.ndarray:
l = img[:, :, 2]
# a and b must be centered at 0
a = img[:, :, 1] - 0.5
b = img[:, :, 0] - 0.5
c = np.hypot(a, b)
h = np.arctan2(b, a)
# normalize C and h to [0,1]
#
# This is quite simple for h, but not so much for C. The problem is that
# the maximum value of C depends on h. This is because a*,b* in [-1,1] form
# a square of possible value around the origin. C nd h are simple polar
# coordinates where h is the angel and C is the distance from origin. Since
# the corners of a square are farther away from the origin than the mid
# point of the sides of the a square, the maximum value of C depends on the
# angle. E.g. for h=0, the maximum C value is 0.5 and for h=45°, it's
# sqrt(0.5).
#
# One strategy would be simple use the maximum value for all possible h
# values (which is sqrt(0.5)), but this has the problem that it is now
# possible to create C,h value pairs that create invalid a*,b* values.
# Ideally, all possible values C,h values should map to valid a*,b* values.
#
# To solve this problem, we calculate the maximum C value for the current
# h angle and use that to normalize C. `Cmax` for a,b in [-1,1] is defined
# as follows: `Cmax = 1/max(abs(cos(h)), abs(sin(h)))`. Since, we use a,b
# in [-0.5,0.5], we just have to divide that value by 2.
c_max = 0.5 / np.maximum(np.abs(np.sin(h)), np.abs(np.cos(h)))
c = c / c_max
h = h / (math.pi * 2) + 0.5
return cv2.merge((h, c, l))
def __lch_to_lab(img: np.ndarray) -> np.ndarray:
l = img[:, :, 2]
# undo the c and h [0,1] normalization
h = (img[:, :, 0] - 0.5) * (math.pi * 2)
sin_h = np.sin(h)
cos_h = np.cos(h)
c_max = 0.5 / np.maximum(np.abs(sin_h), np.abs(cos_h))
c = img[:, :, 1] * c_max
a = c * cos_h + 0.5
b = c * sin_h + 0.5
return cv2.merge((b, a, l))
# The conversion loses one channel of information (e.g. the alpha channel, or a color channel)
__CHANNEL_LOST = 1000
# The conversion loses hue/chroma information in certain edge cases
__CHROMA_LOST = 100
conversions: list[Conversion] = [
# RGB and grayscale
Conversion(
direction=(RGB, GRAY),
convert=lambda i: cv2.cvtColor(i, cv2.COLOR_BGR2GRAY),
cost=__CHANNEL_LOST * 2,
),
Conversion(
direction=(GRAY, RGB),
convert=lambda i: cv2.cvtColor(i, cv2.COLOR_GRAY2BGR),
),
Conversion(
direction=(RGBA, GRAY),
convert=lambda i: cv2.cvtColor(i, cv2.COLOR_BGRA2GRAY),
cost=__CHANNEL_LOST * 3,
),
Conversion(
direction=(GRAY, RGBA),
convert=lambda i: cv2.cvtColor(i, cv2.COLOR_GRAY2BGRA),
),
# YUV
Conversion(
direction=(RGB, YUV),
convert=lambda i: __rev3(cv2.cvtColor(i, cv2.COLOR_BGR2YUV)),
),
Conversion(
direction=(YUV, RGB),
convert=lambda i: np.clip(cv2.cvtColor(__rev3(i), cv2.COLOR_YUV2BGR), 0, 1),
cost=__CHROMA_LOST,
),
# HSV/HSL
Conversion(
direction=(RGB, HSV),
convert=__rgb_to_hsv,
),
Conversion(
direction=(HSV, RGB),
convert=__hsv_to_rgb,
cost=__CHROMA_LOST,
),
Conversion(
direction=(RGB, HSL),
convert=__rgb_to_hsl,
),
Conversion(
direction=(HSL, RGB),
convert=__hsl_to_rgb,
cost=__CHROMA_LOST,
),
Conversion(
direction=(HSV, HSL),
convert=__hsv_to_hsl,
),
Conversion(
direction=(HSL, HSV),
convert=__hsl_to_hsv,
),
# CMYK
Conversion(
direction=(RGB, CMYK),
convert=__rgb_to_cmyk,
),
Conversion(
direction=(CMYK, RGB),
convert=__cmyk_to_rgb,
cost=__CHROMA_LOST,
),
# LAB
Conversion(
direction=(RGB, LAB),
convert=__rgb_to_lab,
),
Conversion(
direction=(LAB, RGB),
convert=__lab_to_rgb,
),
# LCH
Conversion(
direction=(LAB, LCH),
convert=__lab_to_lch,
),
Conversion(
direction=(LCH, LAB),
convert=__lch_to_lab,
cost=__CHROMA_LOST,
),
]
# Add conversions that can be generated because only alpha is different
for dir_3, dir_4 in ALPHA_PAIRS.items():
assert dir_3.channels == 3
assert dir_4.channels == 4
# Add and remove the alpha channel
conversions.append(
Conversion(
direction=(dir_3, dir_4),
convert=lambda i: cv2.cvtColor(i, cv2.COLOR_BGR2BGRA),
)
)
conversions.append(
Conversion(
direction=(dir_4, dir_3),
convert=lambda i: cv2.cvtColor(i, cv2.COLOR_BGRA2BGR),
cost=__CHANNEL_LOST,
)
)
__ALPHA_3_to_4 = dict(ALPHA_PAIRS)
for conv in list(conversions):
in_4 = __ALPHA_3_to_4.get(conv.input)
out_4 = __ALPHA_3_to_4.get(conv.output)
if in_4 is not None and out_4 is not None:
# if we have a conversion X -> Y, then we can generate XA -> YA
# e.g. RGBA -> HSVA can be generated from RGB -> HSV
def create_convert(old_conv: Conversion):
def convert(img: np.ndarray) -> np.ndarray:
color = img[:, :, :3]
alpha = img[:, :, 3]
return np.dstack((old_conv.convert(color), alpha))
return convert
conversions.append(
Conversion(
direction=(in_4, out_4),
convert=create_convert(conv),
cost=conv.cost,
)
)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/color/convert_model.py
================================================
from __future__ import annotations
from collections.abc import Callable, Iterable
import numpy as np
from ...utils.format import format_image_with_channels
from ...utils.utils import get_h_w_c
class ColorSpace:
def __init__(self, id_: int, name: str, channels: int) -> None:
assert 0 <= id_ and id_ < 256
self.id = id_
self.name = name
self.channels = channels
class ColorSpaceDetector:
def __init__(self, id_: int, name: str, color_spaces: Iterable[ColorSpace]) -> None:
assert 1000 <= id_ and id_ < 2000
self.id = id_
self.name = name
self.channel_map: dict[int, ColorSpace] = {}
for cs in color_spaces:
assert cs.channels not in self.channel_map
self.channel_map[cs.channels] = cs
self.channels = list(self.channel_map.keys())
def detect(self, image: np.ndarray) -> ColorSpace:
c = get_h_w_c(image)[2]
cs = self.channel_map.get(c, None)
if cs is not None:
return cs
raise ValueError(
f"Expected the input image for {self.name}"
f" to be {format_image_with_channels(self.channels)}"
f" but found {format_image_with_channels([c])}."
)
def assert_input_channels(
img: np.ndarray, input_: ColorSpace, output: ColorSpace
) -> None:
c = get_h_w_c(img)[2]
if c != input_.channels:
raise ValueError(
f"Expected the input image for a {input_.name} -> {output.name} conversion"
f" to be {format_image_with_channels([input_.channels])}"
f" but found {format_image_with_channels([c])}."
)
def assert_output_channels(
result: np.ndarray, input_: ColorSpace, output: ColorSpace
) -> None:
c = get_h_w_c(result)[2]
if c != output.channels:
raise ValueError(
f"Expected the output image for a {input_.name} -> {output.name} conversion"
f" to be {format_image_with_channels([output.channels])}"
f" but found {format_image_with_channels([c])}."
f" This is an internal implementation error."
f" Please report this as a bug."
)
ConvertFn = Callable[[np.ndarray], np.ndarray]
class Conversion:
def __init__(
self,
direction: tuple[ColorSpace, ColorSpace],
convert: ConvertFn,
cost: int = 1,
) -> None:
input_, output = direction
assert input_ != output
self.input: ColorSpace = input_
self.output: ColorSpace = output
self.__convert = convert
assert cost >= 1
self.cost: int = cost
def convert(self, img: np.ndarray) -> np.ndarray:
assert_input_channels(img, self.input, self.output)
result = self.__convert(img)
assert_output_channels(result, self.input, self.output)
return result
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/image_formats.py
================================================
def get_opencv_formats():
return [
# Bitmaps
".bmp",
".dib",
# JPEG
".jpg",
".jpeg",
".jpe",
".jp2",
# PNG, WebP, Tiff
".png",
".webp",
".tif",
".tiff",
# Portable image format
".pbm",
".pgm",
".ppm",
".pxm",
".pnm",
# Sun Rasters
".sr",
".ras",
# OpenEXR
".exr",
# Radiance HDR
".hdr",
".pic",
]
def get_pil_formats():
return [
# Bitmaps
".bmp",
".dib",
".xbm",
# DDS
".dds",
# EPS
".eps",
# GIF
# ".gif",
# Icons
".icns",
".ico",
# JPEG
".jpg",
".jpeg",
".jfif",
".jp2",
".jpx",
# Randoms
".msp",
".pcx",
".sgi",
# PNG, WebP, TIFF
".png",
".webp",
".tiff",
# APNG
# ".apng",
# Portable image format
".pbm",
".pgm",
".ppm",
".pnm",
# TGA
".tga",
# AVIF,
".avif",
]
def get_available_image_formats():
all_formats = [*get_opencv_formats(), *get_pil_formats()]
return sorted(set(all_formats))
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/image_op.py
================================================
from collections.abc import Callable
from typing import Concatenate
import numpy as np
from typing_extensions import ParamSpec
ImageOp = Callable[[np.ndarray], np.ndarray]
"""
An image processing operation that takes an image and produces a new image.
The given image is guaranteed to *not* be modified.
"""
def clipped(op: ImageOp) -> ImageOp:
"""
Ensures that all values in the returned image are between 0 and 1.
"""
return lambda i: np.clip(op(i), 0, 1)
P = ParamSpec("P")
def to_op(fn: Callable[Concatenate[np.ndarray, P], np.ndarray]) -> Callable[P, ImageOp]:
"""
Applies a form of currying to convert the given function into a constructor for an image operation.
Example: Simple resize method could be defined as follows: `resize(np.ndarray, Size2D) -> np.ndarray`.
It takes an image and its new size and returns the resized image.
If we want to convert it to an image operation, we have to create a function with the following signature: `resize_op(Size2D) -> ImageOp`.
The implementation of this function would be rather simple, it would simply take all arguments of `resize` except for the image like this:
```py
def resize_op(size: Size2D) -> ImageOp:
return lambda img: resize(img, size)
```
`to_op` does exactly this transformation, but for any number of arguments.
Note: This only works if the input image is the first argument of the given function.
"""
def p(*args: P.args, **kwargs: P.kwargs) -> ImageOp:
return lambda i: fn(i, *args, **kwargs)
return p
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/image_utils.py
================================================
from __future__ import annotations
import itertools
import math
from enum import Enum
from pathlib import Path
import cv2
import numpy as np
from ..utils.utils import Padding, get_h_w_c, split_file_path
from .color.color import Color
MAX_VALUES_BY_DTYPE = {
np.dtype("int8").name: 127,
np.dtype("uint8").name: 255,
np.dtype("int16").name: 32767,
np.dtype("uint16").name: 65535,
np.dtype("int32").name: 2147483647,
np.dtype("uint32").name: 4294967295,
np.dtype("int64").name: 9223372036854775807,
np.dtype("uint64").name: 18446744073709551615,
np.dtype("float32").name: 1.0,
np.dtype("float64").name: 1.0,
}
class FillColor(Enum):
AUTO = -1
BLACK = 0
TRANSPARENT = 1
def get_color(self, channels: int):
"""Select how to fill negative space that results from rotation"""
if self == FillColor.AUTO:
fill_color = (0,) * channels
elif self == FillColor.BLACK:
fill_color = (0,) * channels if channels < 4 else (0, 0, 0, 1)
else:
fill_color = (0, 0, 0, 0)
return fill_color
class FlipAxis(Enum):
HORIZONTAL = 1
VERTICAL = 0
BOTH = -1
NONE = 2
def flip(self, img: np.ndarray) -> np.ndarray:
if self == FlipAxis.NONE:
return img
return cv2.flip(img, self.value)
class BorderType(Enum):
REFLECT_MIRROR = 4
WRAP = 3
REPLICATE = 1
BLACK = 0
WHITE = 6
TRANSPARENT = 5
CUSTOM_COLOR = 7
class NormalMapType(Enum):
DIRECTX = "DirectX"
OPENGL = "OpenGL"
OCTAHEDRAL = "Octahedral"
def convert_to_bgra(img: np.ndarray, in_c: int) -> np.ndarray:
assert in_c in (1, 3, 4), f"Number of channels ({in_c}) unexpected"
if in_c == 1:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGRA)
elif in_c == 3:
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
return img.copy()
def _get_iinfo(img: np.ndarray) -> np.iinfo | None:
try:
return np.iinfo(img.dtype)
except Exception:
return None
def normalize(img: np.ndarray) -> np.ndarray:
if img.dtype != np.float32:
info = _get_iinfo(img)
img = img.astype(np.float32)
if info is not None:
img /= info.max
if info.min == 0:
# we don't need to clip
return img
# we own `img`, so it's okay to write to it
return np.clip(img, 0, 1, out=img)
return np.clip(img, 0, 1)
def to_uint8(img: np.ndarray, normalized: bool = False) -> np.ndarray:
"""
Returns a new uint8 image with the given image data.
If `normalized` is `False`, then the image will be normalized before being converted to uint8.
"""
if img.dtype == np.uint8:
return img.copy()
if not normalized or img.dtype != np.float32:
img = normalize(img)
return (img * 255).round().astype(np.uint8)
def to_uint16(img: np.ndarray, normalized: bool = False) -> np.ndarray:
"""
Returns a new uint16 image with the given image data.
If `normalized` is `False`, then the image will be normalized before being converted to uint16.
"""
if img.dtype == np.uint16:
return img.copy()
if not normalized or img.dtype != np.float32:
img = normalize(img)
return (img * 65535).round().astype(np.uint16)
class ShiftFill(Enum):
AUTO = -1
BLACK = 0
TRANSPARENT = 1
WRAP = 2
def to_fill_color(self) -> FillColor:
if self == ShiftFill.AUTO:
return FillColor.AUTO
elif self == ShiftFill.BLACK:
return FillColor.BLACK
elif self == ShiftFill.TRANSPARENT:
return FillColor.TRANSPARENT
raise ValueError(f"Cannot get color for {self}")
def shift(
img: np.ndarray, amount_x: int, amount_y: int, shift_fill: ShiftFill
) -> np.ndarray:
h, w, c = get_h_w_c(img)
if shift_fill == ShiftFill.WRAP:
amount_x %= w
amount_y %= h
if amount_x != 0:
img = np.roll(img, amount_x, axis=1)
if amount_y != 0:
img = np.roll(img, amount_y, axis=0)
return img
fill = shift_fill.to_fill_color()
if fill == FillColor.TRANSPARENT:
img = convert_to_bgra(img, c)
fill_color = fill.get_color(c)
h, w, _ = get_h_w_c(img)
translation_matrix = np.asarray(
[[1, 0, amount_x], [0, 1, amount_y]], dtype=np.float32
)
img = cv2.warpAffine(
img,
translation_matrix,
(w, h),
borderMode=cv2.BORDER_CONSTANT,
borderValue=fill_color,
)
return img
def as_2d_grayscale(img: np.ndarray) -> np.ndarray:
"""Given a grayscale image, this returns an image with 2 dimensions (image.ndim == 2)."""
if img.ndim == 2:
return img
if img.ndim == 3 and img.shape[2] == 1:
return img[:, :, 0]
raise AssertionError(f"Invalid image shape {img.shape}")
def as_3d(img: np.ndarray) -> np.ndarray:
"""Given a grayscale image, this returns an image with 3 dimensions (image.ndim == 3)."""
if img.ndim == 2:
return np.expand_dims(img.copy(), axis=2)
return img
def as_target_channels(
img: np.ndarray, target_c: int, narrowing: bool = False
) -> np.ndarray:
"""
Given a number of target channels (either 1, 3, or 4), this convert the given image
to an image with that many channels. If the given image already has the correct
number of channels, it will be returned as is.
Narrowing conversions are only supported if narrowing is True.
"""
c = get_h_w_c(img)[2]
if c == target_c == 1:
return as_2d_grayscale(img)
if c == target_c:
return img
if not narrowing:
assert (
c < target_c
), f"Narrowing is false, image channels ({c}) must be less than target channels ({target_c})"
if c == 1:
if target_c == 3:
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
if target_c == 4:
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGRA)
if c == 3:
if target_c == 1:
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
if target_c == 4:
return cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
if c == 4:
if target_c == 1:
return cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)
if target_c == 3:
return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
raise ValueError(f"Unable to convert {c} channel image to {target_c} channel image")
def create_border(
img: np.ndarray,
border_type: BorderType,
border: Padding,
color: Color | None = None,
) -> np.ndarray:
"""
Returns a new image with a specified border.
"""
if border.empty:
return img
_, _, c = get_h_w_c(img)
if c == 4 and border_type == BorderType.BLACK:
value = (0.0, 0.0, 0.0, 1.0)
else:
value = (0.0,)
cv_border_type: int = border_type.value
if border_type == BorderType.TRANSPARENT:
cv_border_type = cv2.BORDER_CONSTANT
value = (0.0,)
img = as_target_channels(img, 4)
elif border_type == BorderType.WHITE:
cv_border_type = cv2.BORDER_CONSTANT
value = (1.0,) * c
elif border_type == BorderType.CUSTOM_COLOR:
assert (
color is not None
), "Creating a border with a custom color requires supplying a custom color."
# widen image or color to make them compatible
if color.channels > c:
img = as_target_channels(img, color.channels)
elif c > color.channels:
color = Color.from_1x1_image(as_target_channels(color.to_1x1_image(), c))
cv_border_type = cv2.BORDER_CONSTANT
value = color.value
return cv2.copyMakeBorder(
img,
top=border.top,
left=border.left,
right=border.right,
bottom=border.bottom,
borderType=cv_border_type,
value=value,
)
def calculate_ssim(
img1: np.ndarray,
img2: np.ndarray,
) -> float:
"""Calculates mean localized Structural Similarity Index (SSIM)
between two images."""
c1 = 0.01**2
c2 = 0.03**2
kernel = cv2.getGaussianKernel(11, 1.5)
window = np.outer(kernel, kernel.transpose()) # type: ignore
mu1 = cv2.filter2D(img1, -1, window)[5:-5, 5:-5]
mu2 = cv2.filter2D(img2, -1, window)[5:-5, 5:-5]
mu1_sq = np.power(mu1, 2)
mu2_sq = np.power(mu2, 2)
mu1_mu2 = np.multiply(mu1, mu2)
sigma1_sq = cv2.filter2D(img1**2, -1, window)[5:-5, 5:-5] - mu1_sq
sigma2_sq = cv2.filter2D(img2**2, -1, window)[5:-5, 5:-5] - mu2_sq
sigma12 = cv2.filter2D(img1 * img2, -1, window)[5:-5, 5:-5] - mu1_mu2
ssim_map = ((2 * mu1_mu2 + c1) * (2 * sigma12 + c2)) / (
(mu1_sq + mu2_sq + c1) * (sigma1_sq + sigma2_sq + c2)
)
return float(np.mean(ssim_map))
def cv_save_image(path: Path | str, img: np.ndarray, params: list[int]) -> None:
"""
A light wrapper around `cv2.imwrite` to support non-ASCII paths.
"""
# We can't actually use `cv2.imwrite`, because it:
# 1. Doesn't support non-ASCII paths
# 2. Silently fails without doing anything if the path is invalid
_, _, extension = split_file_path(path)
_, buf_img = cv2.imencode(f".{extension}", img, params)
with open(path, "wb") as outf:
outf.write(buf_img) # type: ignore
def cartesian_product(arrays: list[np.ndarray]) -> np.ndarray:
"""
Returns the cartesian product of the given arrays. Good for initializing coordinates, for example.
This is cartesian_product_transpose_pp from this following SO post by Paul Panzer:
https://stackoverflow.com/questions/11144513/cartesian-product-of-x-and-y-array-points-into-single-array-of-2d-points/49445693#49445693
"""
la = len(arrays)
dtype = np.result_type(*arrays)
arr = np.empty((la, *map(len, arrays)), dtype=dtype)
idx = slice(None), *itertools.repeat(None, la)
for i, a in enumerate(arrays):
arr[i, ...] = a[idx[: la - i]]
return arr.reshape(la, -1).T
def fast_gaussian_blur(
img: np.ndarray,
sigma_x: float,
sigma_y: float | None = None,
) -> np.ndarray:
"""
Computes a channel-wise gaussian blur of the given image using a fast approximation.
The maximum error of the approximation is guaranteed to be less than 0.1%.
In addition to that, the error is guaranteed to be smoothly distributed across the image.
There are no sudden spikes in error anywhere.
Specifically, the method is implemented by downsampling the image, blurring the downsampled
image, and then upsampling the blurred image. This is much faster than blurring the full image.
Unfortunately, OpenCV's `resize` method has unfortunate artifacts when upscaling, so we
apply a small gaussian blur to the image after upscaling to smooth out the artifacts. This
single step almost doubles the runtime of the method, but it is still much faster than
blurring the full image.
"""
if sigma_y is None:
sigma_y = sigma_x
if sigma_x == 0 or sigma_y == 0:
return img.copy()
h, w, _ = get_h_w_c(img)
def get_scale_factor(sigma: float) -> float:
if sigma < 11:
return 1
if sigma < 15:
return 1.25
if sigma < 20:
return 1.5
if sigma < 25:
return 2
if sigma < 30:
return 2.5
if sigma < 50:
return 3
if sigma < 100:
return 4
if sigma < 200:
return 6
return 8
def get_sizing(size: int, sigma: float, f: float) -> tuple[int, float, float]:
"""
Return the size of the downsampled image, the sigma of the downsampled gaussian blur,
and the sigma of the upscaled gaussian blur.
"""
if f <= 1:
# just use simple gaussian, the error is too large otherwise
return size, 0, sigma
size_down = math.ceil(size / f)
f = size / size_down
sigma_up = f
sigma_down = math.sqrt(sigma**2 - sigma_up**2) / f
return size_down, sigma_down, sigma_up
# Handling different sigma values for x and y is difficult, so we take the easy way out
# and just use the smaller one. There are potentially better ways of combining them, but
# this is good enough for now.
scale_factor = min(get_scale_factor(sigma_x), get_scale_factor(sigma_y))
h_down, y_down_sigma, y_up_sigma = get_sizing(h, sigma_y, scale_factor)
w_down, x_down_sigma, x_up_sigma = get_sizing(w, sigma_x, scale_factor)
if h != h_down or w != w_down:
# downsampled gaussian blur
img = cv2.resize(img, (w_down, h_down), interpolation=cv2.INTER_AREA)
img = cv2.GaussianBlur(
img,
(0, 0),
sigmaX=x_down_sigma,
sigmaY=y_down_sigma,
borderType=cv2.BORDER_REFLECT,
)
img = cv2.resize(img, (w, h), interpolation=cv2.INTER_LINEAR)
if x_up_sigma != 0 or y_up_sigma != 0:
# post blur to smooth out artifacts
img = cv2.GaussianBlur(
img,
(0, 0),
sigmaX=x_up_sigma,
sigmaY=y_up_sigma,
borderType=cv2.BORDER_REFLECT,
)
return img
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/__init__.py
================================================
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/auto_split.py
================================================
from __future__ import annotations
import gc
from collections.abc import Callable
import numpy as np
import onnxruntime as ort
from nodes.impl.onnx.model import SizeReq
from ..upscale.auto_split import Tiler, auto_split
def _into_batched_form(img: np.ndarray, change_shape: bool) -> np.ndarray:
shape_size = len(img.shape)
if shape_size == 3:
if change_shape:
# (H, W, C) -> (1, H, W, C)
return img[np.newaxis, :]
else:
# (H, W, C) -> (1, C, H, W)
return img.transpose((2, 0, 1))[np.newaxis, :]
elif shape_size == 2:
if change_shape:
# (H, W) -> (1, H, W, 1)
return img[np.newaxis, :, np.newaxis]
else:
# (H, W) -> (1, 1, H, W)
return img[np.newaxis, np.newaxis, :]
else:
raise ValueError("Unsupported input tensor shape")
def _into_standard_image_form(img: np.ndarray, change_shape: bool) -> np.ndarray:
shape_size = len(img.shape)
if shape_size == 4:
if change_shape:
# (1, H, W, C) -> (H, W, C)
return img.squeeze(0)
else:
# (1, C, H, W) -> (H, W, C)
return img.squeeze(0).transpose(1, 2, 0)
elif shape_size == 3:
if change_shape:
# (H, W, C) -> (H, W, C)
return img
else:
# (C, H, W) -> (H, W, C)
return img.transpose(1, 2, 0)
elif shape_size == 2:
# (H, W)
return img
else:
raise ValueError("Unsupported output tensor shape")
def _flip_r_b_channels(img: np.ndarray) -> np.ndarray:
shape_size = len(img.shape)
if shape_size != 3:
return img
if img.shape[2] == 3:
# (H, W, C) RGB -> BGR
return np.flip(img, 2)
elif img.shape[2] == 4:
# (H, W, C) RGBA -> BGRA
return np.dstack((img[:, :, 2], img[:, :, 1], img[:, :, 0], img[:, :, 3]))
return img
def _pad(
img: np.ndarray, req: SizeReq
) -> tuple[np.ndarray, Callable[[np.ndarray], np.ndarray]]:
w = img.shape[1]
h = img.shape[0]
pad_w, pad_h = req.get_padding(w, h)
def remove_padding(i: np.ndarray) -> np.ndarray:
new_w = i.shape[1]
new_h = i.shape[0]
scale_w = new_w // (w + pad_w)
scale_h = new_h // (h + pad_h)
new_pad_w = int(pad_w * scale_w)
new_pad_h = int(pad_h * scale_h)
return i[: new_h - new_pad_h, : new_w - new_pad_w]
if pad_w or pad_h:
paddings = [(0, pad_h), (0, pad_w)]
if len(img.shape) == 3:
paddings.append((0, 0))
img = np.pad(img, paddings, "reflect")
return img, remove_padding
else:
return img, lambda i: i
def onnx_auto_split(
img: np.ndarray,
session: ort.InferenceSession,
change_shape: bool,
tiler: Tiler,
size_req: SizeReq | None = None,
) -> np.ndarray:
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
is_fp16_model = session.get_inputs()[0].type == "tensor(float16)"
def upscale(img: np.ndarray, _: object):
try:
lr_img = img.astype(np.float16) if is_fp16_model else img
lr_img, remove_pad = _pad(lr_img, size_req or SizeReq())
lr_img = _flip_r_b_channels(lr_img)
lr_img = _into_batched_form(lr_img, change_shape)
output: np.ndarray = session.run([output_name], {input_name: lr_img})[0]
output = _into_standard_image_form(output, change_shape)
output = _flip_r_b_channels(output)
output = remove_pad(output)
return output.astype(np.float32)
except Exception as e:
if "ONNXRuntimeError" in str(e) and (
"allocate memory" in str(e)
or "out of memory" in str(e)
or "cudaMalloc" in str(e)
):
raise RuntimeError( # noqa: B904
"A VRAM out-of-memory error has occurred. Please try using a more extreme tiling mode."
)
else:
# Re-raise the exception if not an OOM error
raise
try:
return auto_split(img, upscale, tiler)
finally:
gc.collect()
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/load.py
================================================
from __future__ import annotations
import onnx
import onnx.inliner
import re2
from sanic.log import logger
from .model import OnnxGeneric, OnnxInfo, OnnxModel, OnnxRemBg, SizeReq
from .utils import (
ModelShapeInference,
get_opset,
get_tensor_fp_datatype,
)
re2_options = re2.Options()
re2_options.dot_nl = True
re2_options.encoding = re2.Options.Encoding.LATIN1
U2NET_STANDARD = re2.compile(b"1959.+1960.+1961.+1962.+1963.+1964.+1965", re2_options)
U2NET_CLOTH = re2.compile(
b"output.+d1.+Concat_1876.+Concat_1896.+Concat_1916.+Concat_1936.+Concat_1956",
re2_options,
)
U2NET_SILUETA = re2.compile(b"1808.+1827.+1828.+2296.+1831.+1850.+1958", re2_options)
U2NET_ISNET = re2.compile(
b"/stage1/rebnconvin/conv_s1/Conv.+/stage1/rebnconvin/relu_s1/Relu", re2_options
)
def _detect_size_req(infer: ModelShapeInference):
if infer.fixed_input_width is not None and infer.fixed_input_height is not None:
shape = infer.infer_shape((infer.fixed_input_width, infer.fixed_input_height))
return SizeReq(), shape
for size in [16, 64, 256, 512]:
try:
shape = infer.infer_shape((size, size))
out_h, out_w, _ = shape[1]
if out_h is not None and out_w is not None:
return SizeReq(multiple_of=size), shape
except Exception:
logger.error(f"Failed to infer shape for size {size}", exc_info=True)
return SizeReq(), None
def load_onnx_model(model_or_bytes: onnx.ModelProto | bytes) -> OnnxModel:
if isinstance(model_or_bytes, onnx.ModelProto):
model = model_or_bytes
model_as_bytes = model.SerializeToString()
else:
model_as_bytes = model_or_bytes
model = onnx.load_model_from_string(model_or_bytes)
info = OnnxInfo(
opset=get_opset(model),
dtype=get_tensor_fp_datatype(model),
)
if (
U2NET_STANDARD.search(model_as_bytes[-1000:]) is not None
or U2NET_SILUETA.search(model_as_bytes[-600:]) is not None
or U2NET_ISNET.search(model_as_bytes[:10000]) is not None
):
info.scale_width = 1
info.scale_height = 1
return OnnxRemBg(model_as_bytes, info)
elif U2NET_CLOTH.search(model_as_bytes[-1000:]) is not None:
info.scale_width = 1
info.scale_height = 3
return OnnxRemBg(model_as_bytes, info)
else:
try:
infer = ModelShapeInference(model)
info.fixed_input_width = infer.fixed_input_width
info.fixed_input_height = infer.fixed_input_height
info.input_channels = infer.input_channels
info.output_channels = infer.output_channels
size_req, shape_result = _detect_size_req(infer)
info.size_req = size_req
if shape_result:
i_hwc, o_hwc = shape_result
i_h, i_w, _i_c = i_hwc
o_h, o_w, o_c = o_hwc
def get_scale(i: int | None, o: int | None) -> int | None:
if i is None or o is None:
return None
if o % i != 0:
return None
return o // i
info.scale_width = get_scale(i_w, o_w)
info.scale_height = get_scale(i_h, o_h)
info.output_channels = o_c
except Exception:
pass
return OnnxGeneric(model_as_bytes, info)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/model.py
================================================
# This class defines an interface.
# It is important that is does not contain types that depend on ONNX.
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Final, Literal, Union
OnnxSubType = Literal["Generic", "RemBg"]
def _ceil_div(a: int, b: int):
return -(a // -b)
@dataclass(init=False)
class SizeReq:
minimum: int
multiple_of: int
def __init__(self, minimum: int = 1, multiple_of: int = 1) -> None:
if minimum < 1:
raise ValueError("minimum must be at least 1")
if multiple_of < 1:
raise ValueError("multiple_of must be at least 1")
self.minimum = _ceil_div(minimum, multiple_of) * multiple_of
self.multiple_of = multiple_of
def get_padding(self, width: int, height: int) -> tuple[int, int]:
"""
Given an image size, this returns the minimum amount of padding necessary to satisfy the size requirements. The returned padding is in the format `(pad_width, pad_height)` and is guaranteed to be non-negative.
"""
def ceil_modulo(x: int, mod: int) -> int:
if x % mod == 0:
return x
return (x // mod + 1) * mod
w: int = max(self.minimum, width)
h: int = max(self.minimum, height)
w = ceil_modulo(w, self.multiple_of)
h = ceil_modulo(h, self.multiple_of)
return w - width, h - height
@dataclass
class OnnxInfo:
opset: int
dtype: str
scale_width: int | None = None
scale_height: int | None = None
fixed_input_width: int | None = None
fixed_input_height: int | None = None
input_channels: int | None = None
output_channels: int | None = None
size_req: SizeReq = field(default_factory=SizeReq)
class OnnxGeneric:
def __init__(self, model_as_bytes: bytes, info: OnnxInfo) -> None:
self.bytes: bytes = model_as_bytes
self.sub_type: Final[Literal["Generic"]] = "Generic"
self.info: OnnxInfo = info
class OnnxRemBg:
def __init__(
self,
model_as_bytes: bytes,
info: OnnxInfo,
) -> None:
self.bytes: bytes = model_as_bytes
self.sub_type: Final[Literal["RemBg"]] = "RemBg"
self.info: OnnxInfo = info
OnnxModels = (OnnxGeneric, OnnxRemBg)
OnnxModel = Union[OnnxGeneric, OnnxRemBg]
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/np_tensor_utils.py
================================================
from __future__ import annotations
import numpy as np
from ..image_utils import MAX_VALUES_BY_DTYPE, as_3d
def np_denorm(x: np.ndarray, min_max: tuple[float, float] = (-1.0, 1.0)) -> np.ndarray:
"""Denormalize from [-1,1] range to [0,1]
formula: xi' = (xi - mu)/sigma
Example: "out = (x + 1.0) / 2.0" for denorm
range (-1,1) to (0,1)
for use with proper act in Generator output (ie. tanh)
"""
out = (x - min_max[0]) / (min_max[1] - min_max[0])
return np.clip(out, 0, 1)
def np_norm(x: np.ndarray) -> np.ndarray:
"""Normalize (z-norm) from [0,1] range to [-1,1]"""
out = (x - 0.5) * 2.0
return np.clip(out, -1, 1)
def np_bgr_to_rgb(img: np.ndarray) -> np.ndarray:
out: np.ndarray = img[::-1, ...]
return out
def np_rgb_to_bgr(img: np.ndarray) -> np.ndarray:
# same operation as bgr_to_rgb(), flip image channels
return np_bgr_to_rgb(img)
def np_bgra_to_rgba(img: np.ndarray) -> np.ndarray:
out: np.ndarray = img[[2, 1, 0, 3], ...] # type: ignore
return out
def np_rgba_to_bgra(img: np.ndarray) -> np.ndarray:
# same operation as bgra_to_rgba(), flip image channels
return np_bgra_to_rgba(img)
def np2nptensor(
img: np.ndarray,
bgr2rgb: bool = True,
data_range: float = 1.0,
normalize: bool = False,
change_range: bool = True,
add_batch: bool = True,
) -> np.ndarray:
"""Converts a numpy image array into a numpy Tensor array.
Parameters:
img (numpy array): the input image numpy array
add_batch (bool): choose if new tensor needs batch dimension added
"""
# check how many channels the image has, then condition. ie. RGB, RGBA, Gray
# if bgr2rgb:
# img = img[
# :, :, [2, 1, 0]
# ] # BGR to RGB -> in numpy, if using OpenCV, else not needed. Only if image has colors.
if change_range:
dtype = img.dtype
maxval = MAX_VALUES_BY_DTYPE.get(dtype.name, 1.0)
t_dtype = np.dtype("float32")
img = img.astype(t_dtype) / maxval # ie: uint8 = /255
# "HWC to CHW" and "numpy to tensor"
img = np.ascontiguousarray(np.transpose(as_3d(img), (2, 0, 1))).astype(np.float32)
if bgr2rgb:
# BGR to RGB -> in tensor, if using OpenCV, else not needed. Only if image has colors.)
if (
img.shape[0] % 3 == 0
): # RGB or MultixRGB (3xRGB, 5xRGB, etc. For video tensors.)
img = np_bgr_to_rgb(img)
elif img.shape[0] == 4: # RGBA
img = np_bgra_to_rgba(img)
if add_batch:
img = np.expand_dims(
img, axis=0
) # Add fake batch dimension = 1 . squeeze() will remove the dimensions of size 1
if normalize:
img = np_norm(img)
return img
def nptensor2np(
img: np.ndarray,
rgb2bgr: bool = True,
remove_batch: bool = True,
data_range: float = 255,
denormalize: bool = False,
change_range: bool = True,
imtype: type = np.uint8,
) -> np.ndarray:
"""Converts a Tensor array into a numpy image array.
Parameters:
img (tensor): the input image tensor array
4D(B,(3/1),H,W), 3D(C,H,W), or 2D(H,W), any range, RGB channel order
remove_batch (bool): choose if tensor of shape BCHW needs to be squeezed
denormalize (bool): Used to denormalize from [-1,1] range back to [0,1]
imtype (type): the desired type of the converted numpy array (np.uint8
default)
Output:
img (np array): 3D(H,W,C) or 2D(H,W), [0,255], np.uint8 (default)
"""
n_dim = img.ndim
img = img.astype(np.float32)
if n_dim in (4, 3):
# if n_dim == 4, has to convert to 3 dimensions
if n_dim == 4 and remove_batch:
# remove a fake batch dimension
img = img.squeeze(0)
if img.shape[0] == 3 and rgb2bgr: # RGB
# RGB to BGR -> in tensor, if using OpenCV, else not needed. Only if image has colors.
img_np = np_rgb_to_bgr(img)
elif img.shape[0] == 4 and rgb2bgr: # RGBA
# RGBA to BGRA -> in tensor, if using OpenCV, else not needed. Only if image has colors.
img_np = np_rgba_to_bgra(img)
else:
img_np = img
img_np = np.transpose(img_np, (1, 2, 0)) # CHW to HWC
elif n_dim == 2:
img_np = img
else:
raise TypeError(
f"Only support 4D, 3D and 2D tensor. But received with dimension: {n_dim:d}"
)
# if rgb2bgr:
# img_np = img_np[[2, 1, 0], :, :] #RGB to BGR -> in numpy, if using OpenCV, else not needed. Only if image has colors.
# TODO: Check: could denormalize in the begining in tensor form instead
if denormalize:
img_np = np_denorm(img_np) # denormalize if needed
if change_range:
img_np = np.clip(
data_range * img_np,
0,
data_range,
).round() # np.clip to the data_range
# has to be in range (0,255) before changing to np.uint8, else np.float32
return img_np.astype(imtype)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/onnx_to_ncnn.py
================================================
# ruff: noqa: N806
from __future__ import annotations
import numpy as np
import onnx.numpy_helper as onph
from google.protobuf.internal.containers import (
RepeatedCompositeFieldContainer,
RepeatedScalarFieldContainer,
)
from onnx.onnx_pb import AttributeProto, GraphProto, ModelProto, NodeProto, TensorProto
from sanic.log import logger
from ..ncnn.model import (
DTYPE_FP16,
DTYPE_FP32,
BinaryOpTypes,
EltwiseOpTypes,
GruDirectionFlags,
InterpResizeTypes,
NcnnLayer,
NcnnModel,
NormalizeEpsModes,
PaddingTypes,
PadModes,
PermuteOrderTypes,
ReductionOpTypes,
UnaryOpTypes,
)
from ..ncnn.optimizer import NcnnOptimizer
from .tensorproto_utils import (
APT,
FLOAT32_MAX,
get_node_attr_af,
get_node_attr_ai,
get_node_attr_f,
get_node_attr_from_input_af,
get_node_attr_from_input_ai,
get_node_attr_from_input_f,
get_node_attr_i,
get_node_attr_s,
get_node_attr_tensor,
get_tensor_proto_data_size,
set_node_attr_ai,
)
UOT = UnaryOpTypes
BOT = BinaryOpTypes
EOT = EltwiseOpTypes
GRU = GruDirectionFlags
IRT = InterpResizeTypes
NEM = NormalizeEpsModes
PAM = PadModes
PAT = PaddingTypes
POT = PermuteOrderTypes
ROT = ReductionOpTypes
class Onnx2NcnnConverter:
def __init__(self, onnx_model: ModelProto) -> None:
self.onnx_graph: GraphProto = onnx_model.graph
self.mutable_graph_nodes: list[NodeProto] = list(self.onnx_graph.node)
self.node_count: int = len(self.onnx_graph.node)
self.weights: dict[str, TensorProto] = {
initializer.name: initializer for initializer in self.onnx_graph.initializer
}
self.producers: dict[str, None] = {i.name: None for i in self.onnx_graph.input}
self.node_reference: dict[str, int] = {}
self.blob_names: dict[str, None] = {}
@staticmethod
def add_weight(
layer: NcnnLayer,
weight_name: str,
data: float | int | np.ndarray | TensorProto,
quantize_tag: bytes = b"",
) -> int:
if isinstance(data, TensorProto):
data = onph.to_array(data)
return layer.add_weight(weight_name, data, quantize_tag)
@staticmethod
def clear_container(
container: RepeatedCompositeFieldContainer | RepeatedScalarFieldContainer,
) -> None:
for _ in range(len(container)):
container.pop()
def swap_nodes(self, a: int, b: int) -> None:
self.mutable_graph_nodes[a], self.mutable_graph_nodes[b] = (
self.mutable_graph_nodes[b],
self.mutable_graph_nodes[a],
)
def fuse_rewrite_gather(self) -> None:
for gather in self.mutable_graph_nodes:
if gather.op_type == "Gather":
indices = get_node_attr_from_input_ai(self.weights[gather.input[1]])
if len(indices) == 1:
# Reconstruct node connections
self.node_reference[gather.input[1]] -= 1
origin_inp = gather.input[0]
gather.ClearField("input")
gather.input.append(origin_inp)
# Update axis, starts and ends
axis = get_node_attr_i(gather, "axis", 1)
gather.op_type = "Crop"
gather.ClearField("attribute")
index = indices[0]
set_node_attr_ai(gather, "starts", np.array([index], np.int32))
set_node_attr_ai(gather, "ends", np.array([index + 1], np.int32))
set_node_attr_ai(gather, "axis", np.array([axis], np.int32))
def fuse_weight_reshape(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
if node.op_type == "Reshape":
if node.input[0] in self.weights:
self.weights[node.output[0]] = self.weights[node.input[0]]
if len(node.input) == 1:
shape = get_node_attr_ai(node, "shape")
elif len(node.input) == 2:
shape = get_node_attr_from_input_ai(self.weights[node.input[1]])
else:
shape = np.empty(0, np.int64)
self.clear_container(self.weights[node.output[0]].dims)
for dim in shape:
self.weights[node.output[0]].dims.append(dim)
node.op_type = "noop_reducedncnn"
self.node_reference[node.input[0]] -= 1
if len(node.input) == 2:
self.node_reference[node.input[1]] -= 1
reduced_node_count[0] += 1
i += 1 # noqa
def fuse_weight_transpose(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
if node.op_type == "Transpose":
if (
node.input[0] in self.weights
and len(self.weights[node.input[0]].dims) == 2
):
perm = get_node_attr_ai(node, "perm")
if perm.size != 2 or perm[0] != 1 or perm[1] != 0:
continue
self.weights[node.output[0]] = self.weights[node.input[0]]
# Permute weight
B = self.weights[node.output[0]]
h, w = B.dims[:2]
permuted_data = onph.to_array(B).T
B.dims[:2] = (w, h)
if B.raw_data:
B.raw_data = permuted_data.tobytes()
else:
self.clear_container(B.float_data)
B.float_data.extend(permuted_data)
# Reduce
node.op_type = "noop_reducednccn"
self.node_reference[node.input[0]] -= 1
reduced_node_count[0] += 1
i += 1 # noqa
def fuse_shufflechannel(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# ShuffleChannel <= Reshape - Transpose - Reshape
# ShuffleChannel <= Reshape - Transpose - Constant - Reshape
if node.op_type == "Reshape":
if self.node_reference[node.output[0]] != 1:
continue
if len(node.input) == 1:
shape = get_node_attr_ai(node, "shape")
else:
# Skip weight reshape
if node.input[1] not in self.weights:
continue
shape = get_node_attr_from_input_ai(self.weights[node.input[1]])
# 1 groups channels_per_group, height, width
# reverse style = channels_per_group, groups, height * width
if (shape.size not in (5, 3)) or (shape.size == 5 and shape[0] != 1):
continue
if i + 2 >= self.node_count:
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
if node3.op_type == "Constant":
if i + 3 >= self.node_count:
continue
node3 = self.mutable_graph_nodes[i + 3]
if (node2.op_type != "Transpose" or node3.op_type != "Reshape") or (
self.node_reference[node2.output[0]] != 1
):
continue
# 0 2 1 3 4
# reverse style = 1 0 2
perm = get_node_attr_ai(node2, "perm")
if perm.size not in (5, 3):
continue
if perm.size == 5 and (
perm[0] != 0
or perm[1] != 2
or perm[2] != 1
or perm[3] != 3
or perm[4] != 4
):
continue
if perm.size == 3 and (perm[0] != 1 or perm[1] != 0 or perm[2] != 2):
continue
if len(node3.input) == 1:
shape3 = get_node_attr_ai(node3, "shape")
else:
if node3.input[1] not in self.weights:
continue
shape3 = get_node_attr_from_input_ai(self.weights[node3.input[1]])
# 1, -1, height, width
# reverse style = group, -1, channels_per_group, height, width
if shape3.size not in (4, 5):
continue
if shape3.size == 4 and (
shape3[0] != 1
or (shape3[1] != -1 and shape3[1] != shape[1] * shape[2])
):
continue
if shape3.size == 5 and (
shape3[0] != shape[1]
or shape3[2] != shape[0]
or shape3[3] * shape3[4] != shape[2]
):
continue
# Reduce
node.op_type = "noop_reducedncnn"
node2.op_type = "noop_reducedncnn"
if len(node.input) == 2:
self.node_reference[node.input[1]] -= 1
self.node_reference[node.output[0]] -= 1
self.node_reference[node2.output[0]] -= 1
if len(node3.input) == 2:
self.node_reference[node3.input[1]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
node3.op_type = "ShuffleChannel"
node3.input[0] = node.input[0]
attr_group = AttributeProto(name="group", i=shape[1], type=APT.INT)
node3.attribute.append(attr_group)
attr_reverse = AttributeProto(
name="reverse", i=int(shape.size == 3), type=APT.INT
)
node3.attribute.append(attr_reverse)
reduced_node_count[0] += 2
i += 2 # noqa
def fuse_shufflechannel_split(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# Split <= ShuffleChannel(reverse type) - Gather(0) - Gather(1)
if node.op_type == "ShuffleChannel":
# reverse = 1
reverse = get_node_attr_i(node, "reverse")
if reverse != 1 or (i + 2 >= self.node_count):
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
if node2.op_type != "Gather" or node3.op_type != "Gather":
continue
if node2.input[0] != node.output[0] or node3.input[0] != node.output[0]:
continue
# axis = 0 or indices = 0
gather2_axis = get_node_attr_i(node2, "axis")
if gather2_axis != 0 or node2.input[1] not in self.weights:
continue
gather2_indices = get_node_attr_from_input_ai(
self.weights[node2.input[1]]
)
if gather2_indices.size != 1 or gather2_indices[0] != 0:
continue
# axis = 0 or indices = 1
gather3_axis = get_node_attr_i(node3, "axis")
if gather3_axis != 0 or node3.input[1] not in self.weights:
continue
gather3_indices = get_node_attr_from_input_ai(
self.weights[node3.input[1]]
)
if gather3_indices.size != 1 or gather2_indices[0] != 1:
continue
# reduce
node2.op_type = "noop_reducedncnn"
self.node_reference[node.output[0]] -= 2
self.node_reference[node2.input[1]] -= 1
self.node_reference[node3.input[1]] -= 1
node3.op_type = "Split"
node3.ClearField("input")
node3.input.append(node.output[0])
node3.output.append(node3.output[0])
node3.output[0] = node2.output[0]
node3.ClearField("attribute")
attr_axis = AttributeProto(name="axis", i=1, type=APT.INT)
node3.attribute.append(attr_axis)
reduced_node_count[0] += 1
i += 1 # noqa
def fuse_hardswish(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# HardSwish <= Add(+3) - Clip(0, 6) - Mul(X, ) - Div( / 6)
# HardSwish <= Add(+3) - Clip(0, 6) - Mul(X, ) - Mul(*(1 / 6))
# HardSwish <= Add(+3) - Clip(0, 6) - Mul(X, ) - Constant - Div( / 6)
# HardSwish <= Add(+3) - Clip(0, 6) - Mul(X, ) - Constant - Mul(*(1 / 6))
# out = x * F.relu6(x + 3, inplace=True) / 6
if node.op_type == "Add":
if (
self.node_reference[node.output[0]] != 1
or i + 3 >= self.node_count
or node.input[1] not in self.weights
):
continue
add_three = self.weights[node.input[1]]
if (
len(add_three.dims) != 0
or get_tensor_proto_data_size(add_three, add_three.data_type) != 1
):
continue
constant_add_three = get_node_attr_from_input_f(add_three)
if constant_add_three != 3:
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
node4 = self.mutable_graph_nodes[i + 3]
if node4.op_type == "Constant":
if i + 4 >= self.node_count:
continue
node4 = self.mutable_graph_nodes[i + 4]
if (
node2.op_type != "Clip"
or node3.op_type != "Mul"
or (node4.op_type not in ("Div", "Mul"))
):
continue
if self.node_reference[node2.output[0]] != 1:
continue
if len(node2.input) == 1:
relu6_min = get_node_attr_f(node2, "min", -FLOAT32_MAX)
relu6_max = get_node_attr_f(node2, "max", FLOAT32_MAX)
else:
min_tp = self.weights[node2.input[1]]
max_tp = self.weights[node2.input[2]]
relu6_min = get_node_attr_from_input_f(min_tp)
relu6_max = get_node_attr_from_input_f(max_tp)
if relu6_min != 0 or relu6_max != 6:
continue
if self.node_reference[node3.output[0]] != 1:
continue
if node3.input[0] != node.input[0] or node3.input[1] != node2.output[0]:
continue
if node4.input[1] not in self.weights:
continue
div_six = self.weights[node4.input[1]]
if (
len(div_six.dims) != 0
or get_tensor_proto_data_size(div_six, div_six.data_type) != 1
):
continue
constant_div_six = get_node_attr_from_input_f(div_six)
if (node4.op_type == "Div" and constant_div_six != 6) or (
node4.op_type == "Mul" and constant_div_six != 1 / 6
):
continue
# reduce
node.op_type = "noop_reducedncnn"
node2.op_type = "noop_reducedncnn"
node3.op_type = "noop_reducedncnn"
self.node_reference[node.input[0]] -= 1
self.node_reference[node.input[1]] -= 1
self.node_reference[node.output[0]] -= 1
if len(node2.input) == 3:
self.node_reference[node2.input[1]] -= 1
self.node_reference[node2.input[2]] -= 1
self.node_reference[node2.output[0]] -= 1
self.node_reference[node3.output[0]] -= 1
self.node_reference[node4.input[1]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
self.blob_names.pop(node3.output[0], None)
node4.op_type = "HardSwish"
node4.ClearField("input")
node4.input.append(node.input[0])
attr_alpha = AttributeProto(name="alpha", f=1 / 6, type=APT.FLOAT)
node4.attribute.append(attr_alpha)
attr_beta = AttributeProto(name="beta", f=0.5, type=APT.FLOAT)
node4.attribute.append(attr_beta)
reduced_node_count[0] += 3
i += 3 # noqa
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# HardSwish <= HardSigmoid - Mul
# out = x * hsigmoid(x)
if node.op_type == "HardSigmoid":
if self.node_reference[node.output[0]] != 1:
continue
alpha = get_node_attr_f(node, "alpha", 0.2)
beta = get_node_attr_f(node, "beta", 0.5)
if i + 1 >= self.node_count:
continue
node2 = self.mutable_graph_nodes[i + 1]
if node2.op_type != "Mul":
continue
if node2.input[0] != node.input[0] or node2.input[1] != node.output[0]:
continue
# reduce
node.op_type = "noop_reducedncnn"
self.node_reference[node.input[0]] -= 1
self.node_reference[node.output[0]] -= 1
self.blob_names.pop(node.output[0], None)
node2.op_type = "HardSwish"
node2.ClearField("input")
node2.input.append(node.input[0])
attr_alpha = AttributeProto(name="alpha", f=alpha, type=APT.FLOAT)
node2.attribute.append(attr_alpha)
attr_beta = AttributeProto(name="beta", f=beta, type=APT.FLOAT)
node2.attribute.append(attr_beta)
reduced_node_count[0] += 1
i += 1 # noqa
def fuse_hardsigmoid(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# HardSigmoid <= Add(+3) - Clip(0, 6) - Div( / 6)
# HardSigmoid <= Add(+3) - Clip(0, 6) - Mul(*(1 / 6))
# HardSigmoid <= Add(+3) - Clip(0, 6) - Constant - Div( / 6)
# HardSigmoid <= Add(+3) - Clip(0, 6) - Constant - Mul(*(1 / 6))
# out = F.relu6(x + 3, inplace=True) / 6
if node.op_type == "Add":
if (
self.node_reference[node.output[0]] != 1
or i + 2 >= self.node_count
or node.input[1] not in self.weights
):
continue
add_three = self.weights[node.input[1]]
if (
len(add_three.dims) != 0
or get_tensor_proto_data_size(add_three, add_three.data_type) != 1
):
continue
constant_add_three = self.weights[node.input[1]]
if constant_add_three != 3:
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
if node3.op_type == "Constant":
if i + 3 >= self.node_count:
continue
node3 = self.mutable_graph_nodes[i + 3]
if node2.op_type != "Clip" or (node3.op_type not in ("Div", "Mul")):
continue
if self.node_reference[node2.output[0]] != 1:
continue
if len(node2.input) == 1:
relu6_min = get_node_attr_f(node2, "min", -FLOAT32_MAX)
relu6_max = get_node_attr_f(node2, "max", FLOAT32_MAX)
else:
min_tp = self.weights[node2.input[1]]
max_tp = self.weights[node2.input[2]]
relu6_min = get_node_attr_from_input_f(min_tp)
relu6_max = get_node_attr_from_input_f(max_tp)
if relu6_min != 0 or relu6_max != 6:
continue
if node3.input[1] not in self.weights:
continue
div_six = self.weights[node3.input[1]]
if (
len(div_six.dims) != 0
or get_tensor_proto_data_size(div_six, div_six.data_type) != 1
):
continue
constant_div_six = get_node_attr_from_input_f(div_six)
if (node3.op_type == "Div" and constant_div_six != 6) or (
node3.op_type == "Mul" and constant_div_six != 1 / 6
):
continue
# reduce
node.op_type = "noop_reducedncnn"
node2.op_type = "noop_reducedncnn"
self.node_reference[node.input[1]] -= 1
self.node_reference[node.output[0]] -= 1
if len(node2.input) == 3:
self.node_reference[node2.input[1]] -= 1
self.node_reference[node2.input[2]] -= 1
self.node_reference[node2.output[0]] -= 1
self.node_reference[node3.input[1]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
node3.op_type = "HardSigmoid"
node3.ClearField("input")
node3.input.append(node.input[0])
attr_alpha = AttributeProto(name="alpha", f=1 / 6, type=APT.FLOAT)
node3.attribute.append(attr_alpha)
attr_beta = AttributeProto(name="beta", f=0.5, type=APT.FLOAT)
node3.attribute.append(attr_beta)
reduced_node_count[0] += 2
i += 2 # noqa
def fuse_swish(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# Swish <= Sigmoid - Mul
# x * torch.sigmoid(x)
if node.op_type == "Sigmoid":
if self.node_reference[node.output[0]] != 1 or i + 1 >= self.node_count:
continue
node2 = self.mutable_graph_nodes[i + 1]
if node2.op_type != "Mul":
continue
if node2.input[0] != node.input[0] or node2.input[1] != node.output[0]:
continue
# reduce
node.op_type = "noop_reducedncnn"
self.node_reference[node.input[0]] -= 1
self.node_reference[node.output[0]] -= 1
self.blob_names.pop(node.output[0], None)
node2.op_type = "Swish"
node2.ClearField("input")
node2.input.append(node.input[0])
reduced_node_count[0] += 1
i += 1 # noqa
def fuse_batchnorm1d_squeeze_unsqueeze(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# BatchNormalization <= Unsqueeze - BatchNormalization - Squeeze
if node.op_type == "Unsqueeze":
if self.node_reference[node.output[0]] != 1 or i + 2 >= self.node_count:
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
if node2.op_type != "BatchNormalization" or node3.op_type != "Squeeze":
continue
if self.node_reference[node2.output[0]] != 1:
continue
if (
node2.input[0] != node.output[0]
or node3.input[0] != node2.output[0]
):
continue
# reduce
node.op_type = "noop_reducedncnn"
node3.op_type = "noop_reducedncnn"
self.node_reference[node.output[0]] -= 1
self.node_reference[node2.output[0]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
node2.input[0] = node.input[0]
node2.output[0] = node3.output[0]
reduced_node_count[0] += 2
i += 2 # noqa
def fuse_unsqueeze_prelu(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# PReLU <= Unsqueeze - PReLU
if node.op_type == "Unsqueeze":
# check weight
if node.input[0] not in self.weights:
continue
B = self.weights[node.input[0]]
if len(B.dims) != 1:
continue
if self.node_reference[node.output[0]] != 1:
continue
# axes = (1, 2)
axes = get_node_attr_ai(node, "axes")
if axes.size != 2 or axes[0] != 1 or axes[1] != 2:
continue
if i + 1 >= self.node_count:
continue
node2 = self.mutable_graph_nodes[i + 1]
if node2.op_type != "PRelu" or node2.input[1] != node.output[0]:
continue
# reduce
node.op_type = "noop_reducedncnn"
self.node_reference[node.output[0]] -= 1
self.blob_names.pop(node.output[0], None)
node2.input[1] = node.input[0]
reduced_node_count[0] += 1
i += 1 # noqa
def fuse_normalize(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# Normalize <= X - ReduceL2 - Clip - Expand - Div
# Normalize <= X - ReduceL2 - Clip - Shape - Expand - Div
if node.op_type == "ReduceL2":
if self.node_reference[node.output[0]] != 1:
continue
# axes = (1)
axes = get_node_attr_ai(node, "axes")
if len(axes) != 1 or axes[0] != 1 or i + 3 >= self.node_count:
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
node4 = self.mutable_graph_nodes[i + 3]
has_shape_node = node3.op_type == "Shape"
node_shape = NodeProto()
if has_shape_node:
if i + 4 >= self.node_count:
continue
node_shape = node3
node3 = self.mutable_graph_nodes[i + 3]
node4 = self.mutable_graph_nodes[i + 4]
if (
node2.op_type != "Clip"
or node3.op_type != "Expand"
or node4.op_type != "Div"
):
continue
if (
self.node_reference[node2.output[0]] != 1
or self.node_reference[node3.output[0]] != 1
):
continue
if (
node2.input[0] != node.output[0]
or node3.input[0] != node2.output[0]
or node4.input[0] != node.input[0]
or node4.input[1] != node3.output[0]
):
continue
if has_shape_node and (
node_shape.input[0] != node.input[0]
or node3.input[1] != node_shape.output[0]
):
continue
# +eps
if len(node2.input) == 1:
clip_min = get_node_attr_f(node2, "min", -FLOAT32_MAX)
else:
min_tp = self.weights[node2.input[1]]
clip_min = get_node_attr_from_input_f(min_tp)
# reduce
node.op_type = "noop_reducedncnn"
node2.op_type = "noop_reducedncnn"
if has_shape_node:
node_shape.op_type = "noop_reducedncnn"
node3.op_type = "noop_reducedncnn"
self.node_reference[node.input[0]] -= 2 if has_shape_node else 1
self.node_reference[node.output[0]] -= 1
self.node_reference[node2.output[0]] -= 1
if has_shape_node:
self.node_reference[node_shape.output[0]] -= 1
self.node_reference[node3.output[0]] -= 1
if len(node3.input) == 2:
self.node_reference[node3.input[1]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
if has_shape_node:
self.blob_names.pop(node_shape.output[0], None)
self.blob_names.pop(node3.output[0], None)
node4.op_type = "Normalize"
node4.ClearField("input")
node4.input.append(node.input[0])
attr_alpha = AttributeProto(name="eps", f=clip_min, type=APT.FLOAT)
node4.attribute.append(attr_alpha)
reduced_node_count[0] += 4 if has_shape_node else 3
i += 4 if has_shape_node else 3 # noqa
def fuse_groupnorm(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# GroupNorm <= X - Reshape - InstanceNormalization - Reshape - Mul - Add
if node.op_type == "Reshape":
if self.node_reference[node.output[0]] != 1:
continue
if len(node.input) == 1:
shape = get_node_attr_ai(node, "shape")
else:
# Skip weight reshape
if node.input[1] not in self.weights:
continue
shape = get_node_attr_from_input_ai(self.weights[node.input[1]])
# 0, group, -1
if (
shape.size != 3
or shape[0] != 0
or shape[2] != -1
or i + 4 >= self.node_count
):
continue
groups = shape[1]
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
node4 = self.mutable_graph_nodes[i + 3]
node5 = self.mutable_graph_nodes[i + 4]
if (
node2.op_type != "InstanceNormalization"
or node3.op_type != "Reshape"
or node4.op_type != "Mul"
or node5.op_type != "Add"
):
continue
if (
self.node_reference[node2.output[0]] != 1
or self.node_reference[node3.output[0]] != 1
or self.node_reference[node4.output[0]] != 1
):
continue
if (
node2.input[0] != node.output[0]
or node3.input[0] != node2.output[0]
or node4.input[0] != node3.output[0]
or node5.input[0] != node4.output[0]
):
continue
# InstanceNormalization S=1 B=0
S = get_node_attr_from_input_af(self.weights[node2.input[1]])
B = get_node_attr_from_input_af(self.weights[node2.input[2]])
if S.size != groups or B.size != groups:
continue
if np.any(S != 1) or np.any(B != 0):
continue
if len(node3.input) == 1:
shape2 = get_node_attr_ai(node3, "shape")
else:
# Skip weight reshape
if node3.input[1] not in self.weights:
continue
shape2 = get_node_attr_from_input_ai(self.weights[node3.input[1]])
# 1, channels, w, h
if shape2.size != 4 or shape2[0] != 1:
continue
channels = shape2[1]
# affine
affine_S = get_node_attr_from_input_af(self.weights[node4.input[1]])
affine_B = get_node_attr_from_input_af(self.weights[node5.input[1]])
if channels not in (affine_S.size, affine_B.size):
continue # only per-channel affine allowed
# reduce
node.op_type = "noop_reducedncnn"
node2.op_type = "noop_reducedncnn"
node3.op_type = "noop_reducedncnn"
node4.op_type = "noop_reducedncnn"
if len(node.input) == 2:
self.node_reference[node.input[1]] -= 1
self.node_reference[node.output[0]] -= 1
self.node_reference[node2.input[1]] -= 1
self.node_reference[node2.input[2]] -= 1
self.node_reference[node2.output[0]] -= 1
if len(node3.input) == 2:
self.node_reference[node3.input[1]] -= 1
self.node_reference[node3.output[0]] -= 1
self.node_reference[node4.output[0]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
self.blob_names.pop(node3.output[0], None)
self.blob_names.pop(node4.output[0], None)
affine_scale = node4.input[1]
affine_bias = node5.input[1]
node5.op_type = "GroupNorm"
node5.ClearField("input")
node5.input.append(node.input[0])
node5.input.append(affine_scale)
node5.input.append(affine_bias)
attr_groups = AttributeProto(name="groups", i=groups, type=APT.INT)
node5.attribute.append(attr_groups)
attr_channels = AttributeProto(
name="channels", i=channels, type=APT.INT
)
node5.attribute.append(attr_channels)
# +eps
eps = get_node_attr_f(node2, "epsilon", 0.00001)
attr_eps = AttributeProto(name="epsilon", f=eps, type=APT.FLOAT)
node5.attribute.append(attr_eps)
attr_affine = AttributeProto(name="affine", i=1, type=APT.INT)
node5.attribute.append(attr_affine)
reduced_node_count[0] += 4
i += 4 # noqa
def fuse_layernorm(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# LayerNorm <= X - ReduceMean - Sub - Pow - ReduceMean - Add - Sqrt - Div
# LayerNorm <= X - ReduceMean - Sub - Pow - ReduceMean - Add - Sqrt - Div - Mul - Add
if node.op_type == "ReduceMean":
if self.node_reference[node.output[0]] != 1:
continue
axes = get_node_attr_ai(node, "axes")
# -1
# -2 -1
if axes.size not in (1, 2):
continue
if (axes.size == 1 and axes[0] != -1) or (
axes.size == 2 and (axes[0] != -2 or axes[1] != -1)
):
continue
if i + 6 >= self.node_count:
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
node4 = self.mutable_graph_nodes[i + 3]
node5 = self.mutable_graph_nodes[i + 4]
node6 = self.mutable_graph_nodes[i + 5]
node7 = self.mutable_graph_nodes[i + 6]
if node2.op_type != "Sub" or node3.op_type != "Pow":
continue
if (
self.node_reference[node2.output[0]] != 2
or self.node_reference[node3.output[0]] != 1
or self.node_reference[node4.output[0]] != 1
or self.node_reference[node5.output[0]] != 1
or self.node_reference[node6.output[0]] != 1
):
continue
if (
node2.input[0] != node.output[0]
or node2.input[1] != node.output[0]
or node3.input[0] != node2.output[0]
or node4.input[0] != node3.output[0]
or node5.input[0] != node4.output[0]
or node6.input[0] != node5.output[0]
or node7.input[0] != node2.output[0]
or node7.input[1] != node6.output[0]
):
continue
if node3.input[1] not in self.weights:
continue
pow_two = self.weights[node3.input[1]]
if (
len(pow_two.dims) != 0
or get_tensor_proto_data_size(pow_two, pow_two.data_type) != 1
):
continue
constant_pow_two = get_node_attr_from_input_f(pow_two)
if constant_pow_two != 2:
continue
axes4 = get_node_attr_ai(node4, "axes")
# -1
# -2 -1
if axes4.size != axes.size:
continue
if (axes.size == 1 and axes[4] != -1) or (
axes.size == 2 and (axes4[0] != -2 or axes4[1] != -1)
):
continue
if node5.input[1] not in self.weights:
continue
add_eps = self.weights[node5.input[1]]
if (
len(add_eps.dims) != 0
or get_tensor_proto_data_size(add_eps, add_eps.data_type) != 1
):
continue
eps = get_node_attr_from_input_f(add_eps)
affine = 0
while i + 8 < self.node_count:
node8 = self.mutable_graph_nodes[i + 7]
node9 = self.mutable_graph_nodes[i + 8]
if node8.op_type != "Mul" or node9.op_type != "Add":
break
if (
self.node_reference[node7.output[0]] != 1
or self.node_reference[node8.output[0]] != 1
):
break
if (
node8.input[0] != node7.output[0]
or node9.input[0] != node8.output[0]
):
break
# affine
affine_S = get_node_attr_from_input_af(self.weights[node8.input[1]])
affine_B = get_node_attr_from_input_af(self.weights[node9.input[1]])
if affine_S.size != affine_B.size:
break
affine = 1
break
# reduce
node.op_type = "noop_reducedncnn"
node2.op_type = "noop_reducedncnn"
node3.op_type = "noop_reducedncnn"
node4.op_type = "noop_reducedncnn"
node5.op_type = "noop_reducedncnn"
node6.op_type = "noop_reducedncnn"
self.node_reference[node2.input[0]] -= 1
self.node_reference[node2.input[1]] -= 1
self.node_reference[node3.input[0]] -= 1
self.node_reference[node3.input[1]] -= 1
self.node_reference[node4.input[0]] -= 1
self.node_reference[node5.input[0]] -= 1
self.node_reference[node5.input[1]] -= 1
self.node_reference[node6.input[0]] -= 1
self.node_reference[node7.input[0]] -= 1
self.node_reference[node7.input[1]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
self.blob_names.pop(node3.output[0], None)
self.blob_names.pop(node4.output[0], None)
self.blob_names.pop(node5.output[0], None)
self.blob_names.pop(node6.output[0], None)
attr_eps = AttributeProto(name="epsilon", f=eps, type=APT.FLOAT)
attr_affine = AttributeProto(name="affine", i=affine, type=APT.INT)
if affine == 0:
node7.op_type = "LayerNorm"
node7.ClearField("input")
node7.input.append(node.input[0])
node7.attribute.append(attr_eps)
node7.attribute.append(attr_affine)
reduced_node_count[0] += 6
i += 6 # noqa
else:
# This is probably unnecessary on their part, but I'm paranoid
node8 = self.mutable_graph_nodes[i + 7]
node9 = self.mutable_graph_nodes[i + 8]
node7.op_type = "noop_reducedncnn"
node8.op_type = "noop_reducedncnn"
self.node_reference[node8.input[0]] -= 1
self.node_reference[node9.input[0]] -= 1
self.blob_names.pop(node7.output[0], None)
self.blob_names.pop(node8.output[0], None)
affine_scale = node8.input[1]
affine_bias = node9.input[1]
node9.op_type = "LayerNorm"
node9.ClearField("input")
node9.input.append(node.input[0])
node9.input.append(affine_scale)
node9.input.append(affine_bias)
node9.attribute.append(attr_eps)
node9.attribute.append(attr_affine)
reduced_node_count[0] += 8
i += 8 # noqa
def fuse_flatten(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# Flatten <= X - Shape - Gather - Constant - Unsqueeze - Unsqueeze - Concat - Reshape
if node.op_type == "Shape":
if self.node_reference[node.output[0]] != 1:
continue
if i + 6 >= self.node_count:
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
node4 = self.mutable_graph_nodes[i + 3]
node5 = self.mutable_graph_nodes[i + 4]
node6 = self.mutable_graph_nodes[i + 5]
node7 = self.mutable_graph_nodes[i + 6]
if (
node2.op_type != "Gather"
or node3.op_type != "Constant"
or node4.op_type != "Unsqueeze"
or node5.op_type != "Unsqueeze"
or node6.op_type != "Concat"
or node7.op_type != "Reshape"
):
continue
if (
self.node_reference[node2.output[0]] != 1
or self.node_reference[node4.output[0]] != 1
or self.node_reference[node5.output[0]] != 1
or self.node_reference[node6.output[0]] != 1
):
continue
if (
node2.input[0] != node.output[0]
or node4.input[0] != node2.output[0]
or node5.input[0] != node3.output[0]
or node6.input[0] != node4.output[0]
or node6.input[1] != node5.output[0]
or node7.input[0] != node.input[0]
or node7.input[1] != node6.output[0]
):
continue
# axis = 0
gather_axis = get_node_attr_i(node2, "axis")
if gather_axis != 0:
continue
# indices = 0
if node2.input[1] not in self.weights:
continue
gather_indices = get_node_attr_from_input_ai(
self.weights[node2.input[1]]
)
if gather_indices.size != 1 or gather_indices[0] != 0:
continue
# axes = (0)
unsqueeze_axes = get_node_attr_ai(node4, "axes")
if unsqueeze_axes.size != 1 or unsqueeze_axes[0] != 0:
continue
unsqueeze_axes2 = get_node_attr_ai(node5, "axes")
if unsqueeze_axes2.size != 1 or unsqueeze_axes2[0] != 0:
continue
# data = -1
if node5.input[0] not in self.weights:
continue
unsqueeze2_data = get_node_attr_from_input_ai(
self.weights[node5.input[0]]
)
if unsqueeze2_data.size != 1 or unsqueeze2_data[0] != -1:
continue
# axis = 0
concat_axis = get_node_attr_i(node6, "axis")
if concat_axis != 0:
continue
# reduce
node.op_type = "noop_reducedncnn"
node2.op_type = "noop_reducedncnn"
node4.op_type = "noop_reducedncnn"
node5.op_type = "noop_reducedncnn"
node6.op_type = "noop_reducedncnn"
self.node_reference[node.input[0]] -= 1
self.node_reference[node.output[0]] -= 1
self.node_reference[node2.input[1]] -= 1
self.node_reference[node2.output[0]] -= 1
self.node_reference[node4.output[0]] -= 1
self.node_reference[node5.input[0]] -= 1
self.node_reference[node5.output[0]] -= 1
self.node_reference[node.output[0]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
self.blob_names.pop(node4.output[0], None)
self.blob_names.pop(node5.output[0], None)
self.blob_names.pop(node6.output[0], None)
node7.op_type = "Flatten"
node7.ClearField("input")
node7.input.append(node.input[0])
reduced_node_count[0] += 5
i += 5 # noqa
def fuse_pixelshuffle(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# PixelShuffle <= Reshape - Transpose - Reshape
# PixelShuffle <= Reshape - Transpose - Constant - Reshape
if node.op_type == "Reshape":
if self.node_reference[node.output[0]] != 1:
continue
if len(node.input) == 1:
shape = get_node_attr_ai(node, "shape")
else:
# skip weight reshape
if node.input[1] not in self.weights:
continue
shape = get_node_attr_from_input_ai(self.weights[node.input[1]])
# -1, 3, upscale_factor, upscale_factor, height, width
if (
shape.size != 6
or (shape[0] != 1 and shape[0] != -1)
or shape[2] != shape[3]
or i + 2 >= self.node_count
):
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
if node3.op_type == "Constant":
if i + 3 >= self.node_count:
continue
node3 = self.mutable_graph_nodes[i + 3]
if node2.op_type != "Transpose" or node3.op_type != "Reshape":
continue
if self.node_reference[node2.output[0]] != 1:
continue
# 0 1 4 2 5 3
perm = get_node_attr_ai(node2, "perm")
if (
perm.size != 6
or perm[0] != 0
or perm[1] != 1
or perm[2] != 4
or perm[3] != 2
or perm[4] != 5
or perm[5] != 3
):
continue
if len(node3.input) == 1:
shape3 = get_node_attr_ai(node3, "shape")
else:
if node3.input[1] not in self.weights:
continue
shape3 = get_node_attr_from_input_ai(self.weights[node3.input[1]])
# -1, 3, height, width
if (
shape3.size != 4
or (shape3[0] != 1 and shape3[0] != -1)
or shape3[1] != shape[1]
or shape3[2] != shape[2] * shape[4]
or shape3[3] != shape[3] * shape[5]
):
continue
# reduce
node.op_type = "noop_reducedncnn"
node2.op_type = "noop_reducedncnn"
if len(node.input) == 2:
self.node_reference[node.input[1]] -= 1
self.node_reference[node.output[0]] -= 1
self.node_reference[node2.output[0]] -= 1
if len(node3.input) == 2:
self.node_reference[node3.input[1]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
node3.op_type = "PixelShuffle"
node3.input[0] = node.input[0]
attr_group = AttributeProto(
name="scale_factor", i=shape[2], type=APT.INT
)
node3.attribute.append(attr_group)
reduced_node_count[0] += 2
i += 2 # noqa
def fuse_reorg(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# PixelShuffle <= Reshape - Transpose - Reshape
# PixelShuffle <= Reshape - Transpose - Constant - Reshape
if node.op_type == "Reshape":
if self.node_reference[node.output[0]] != 1:
continue
if len(node.input) == 1:
shape = get_node_attr_ai(node, "shape")
else:
if node.input[1] not in self.weights:
continue
shape = get_node_attr_from_input_ai(self.weights[node.input[1]])
# -1, 3, out_height, block_size, out_width, block_size
if (
shape.size != 6
or (shape[0] != 1 and shape[0] != -1)
or shape[3] != shape[5]
or i + 2 >= self.node_count
):
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
if node3.op_type == "Constant":
if i + 3 >= self.node_count:
continue
node3 = self.mutable_graph_nodes[i + 3]
if node2.op_type != "Transpose" or node3.op_type != "Reshape":
continue
if self.node_reference[node2.output[0]] != 1:
continue
# 0 1 3 5 2 4
perm = get_node_attr_ai(node2, "perm")
if (
perm.size != 6
or perm[0] != 0
or perm[1] != 1
or perm[2] != 3
or perm[3] != 5
or perm[4] != 2
or perm[5] != 4
):
continue
if len(node3.input) == 1:
shape3 = get_node_attr_ai(node3, "shape")
else:
if node3.input[1] not in self.weights:
continue
shape3 = get_node_attr_from_input_ai(self.weights[node3.input[1]])
# -1, out_channels, out_height, out_width
if (
shape3.size != 4
or (shape3[0] != 1 and shape3[0] != -1)
or shape3[1] != shape[1] * shape[3] * shape[5]
or shape3[2] != shape[2]
or shape3[3] != shape[4]
):
continue
# reduce
node.op_type = "noop_reducedncnn"
node2.op_type = "noop_reducedncnn"
if len(node.input) == 2:
self.node_reference[node.input[1]] -= 1
self.node_reference[node.output[0]] -= 1
self.node_reference[node2.output[0]] -= 1
if len(node3.input) == 2:
self.node_reference[node3.input[1]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
node3.op_type = "Reorg"
node3.input[0] = node.input[0]
attr_group = AttributeProto(name="stride", i=shape[3], type=APT.INT)
node3.attribute.append(attr_group)
reduced_node_count[0] += 2
i += 2 # noqa
def fuse_expand_broadcast(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# Add/Sub/Mul/Div/Min/Max <= Expand - Add/Sub/Mul/Div/Min/Max
if node.op_type == "Expand":
if self.node_reference[node.output[0]] != 1 or i + 1 >= self.node_count:
continue
node2 = self.mutable_graph_nodes[i + 1]
if node2.op_type not in ["Add", "Sub", "Mul", "Div", "Min", "Max"]:
continue
if (
node2.input[1] != node.output[0]
and node2.input[0] != node.output[0]
):
continue
# reduce
node.op_type = "noop_reducedncnn"
self.node_reference[node.output[0]] -= 1
if len(node.input) == 2:
self.node_reference[node.input[1]] -= 1
self.blob_names.pop(node.output[0], None)
if node2.input[0] == node.output[0]:
node2.input[0] = node.input[0]
else:
node2.input[1] = node.input[0]
reduced_node_count[0] += 1
i += 1 # noqa
def fuse_lstm_gru_rnn(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# LSTM(bi) <= LSTM(bi) - Transpose - Reshape - Transpose
if node.op_type in ["LSTM", "GRU", "RNN"]:
if self.node_reference[node.output[0]] != 1 or i + 2 >= self.node_count:
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
if node2.op_type != "Transpose" or node3.op_type != "Reshape":
continue
if self.node_reference[node2.output[0]] != 1:
continue
if (
node2.input[0] != node.output[0]
or node3.input[0] != node2.output[0]
):
continue
direction = get_node_attr_s(node, "direction")
if direction != "bidirectional":
continue
# 0 2 1 3
perm = get_node_attr_ai(node2, "perm")
if (
perm.size != 4
or perm[0] != 0
or perm[1] != 2
or perm[2] != 1
or perm[3] != 3
):
continue
if len(node3.input) == 1:
shape = get_node_attr_ai(node3, "shape")
else:
if node3.input[1] not in self.weights:
continue
shape = get_node_attr_from_input_ai(self.weights[node3.input[1]])
# 0 0 -1
if shape.size != 3 or shape[0] != 0 or shape[1] != 0 or shape[2] != -1:
continue
# reduce
node2.op_type = "noop_reducedncnn"
node3.op_type = "noop_reducedncnn"
self.node_reference[node.output[0]] -= 1
self.node_reference[node2.output[0]] -= 1
if len(node3.input) == 2:
self.node_reference[node3.input[1]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
node.output[0] = node3.output[0]
reduced_node_count[0] += 2
i += 2 # noqa
if i + 1 < self.node_count:
if self.node_reference[node3.output[0]] != 1:
continue
node4 = self.mutable_graph_nodes[i + 1]
if node4.op_type != "Transpose":
continue
if node4.input[0] != node.output[0]:
continue
# 1 0 2
perm4 = get_node_attr_ai(node4, "perm")
if (
perm4.size != 3
or perm4[0] != 1
or perm4[1] != 0
or perm4[2] != 2
):
continue
# reduce
node4.op_type = "noop_reducedncnn"
self.node_reference[node.output[0]] -= 1
self.blob_names.pop(node.output[0], None)
node.output[0] = node4.output[0]
reduced_node_count[0] += 1
i += 1 # noqa
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# LSTM(uni) <= LSTM(uni) - Squeeze - Transpose
if node.op_type in ["LSTM", "GRU", "RNN"]:
if self.node_reference[node.output[0]] != 1 or i + 1 >= self.node_count:
continue
node2 = self.mutable_graph_nodes[i + 1]
if node2.op_type != "Squeeze":
continue
if node2.input[0] != node.output[0]:
continue
direction = get_node_attr_s(node, "direction")
if direction == "bidirectional":
continue
axes = get_node_attr_ai(node2, "axes")
if axes.size != 1 or axes[0] != 1:
continue
# reduce
node2.op_type = "noop_reducedncnn"
self.node_reference[node.output[0]] -= 1
self.blob_names.pop(node.output[0], None)
node.output[0] = node2.output[0]
reduced_node_count[0] += 1
i += 1 # noqa
if i + 1 < self.node_count:
if self.node_reference[node2.output[0]] != 1:
continue
node3 = self.mutable_graph_nodes[i + 1]
if node3.op_type != "Transpose":
continue
if node3.input[0] != node.output[0]:
continue
# 1 0 2
perm4 = get_node_attr_ai(node3, "perm")
if (
perm4.size != 3
or perm4[0] != 1
or perm4[1] != 0
or perm4[2] != 2
):
continue
# reduce
node3.op_type = "noop_reducedncnn"
self.node_reference[node.output[0]] -= 1
self.blob_names.pop(node.output[0], None)
node.output[0] = node3.output[0]
reduced_node_count[0] += 1
i += 1 # noqa
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# LSTM <= Transpose - LSTM
if node.op_type == "Transpose":
if self.node_reference[node.output[0]] != 1:
continue
# 1 0 2
perm = get_node_attr_ai(node, "perm")
if perm.size != 3 or perm[0] != 1 or perm[1] != 0 or perm[2] != 2:
continue
node2 = self.mutable_graph_nodes[i + 1]
if node2.op_type not in ["LSTM", "GRU", "RNN"]:
continue
if node2.input[0] != node.output[0]:
continue
# reduce
node.op_type = "noop_reducedncnn"
self.node_reference[node.output[0]] -= 1
self.blob_names.pop(node.output[0], None)
node2.input[0] = node.input[0]
reduced_node_count[0] += 1
i += 1 # noqa
def fuse_multiheadattention(self, reduced_node_count: list[int]) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# MultiHeadAttention <= MatMul(q) - Add
# - MatMul(k) - Add
# - MatMul(v) - Add
# - Mul
# - Reshape - Transpose
# - Reshape - Reshape - Transpose - Transpose
# - Gemm - Softmax - Gemm - Transpose - Reshape - MatMul - Add
if node.op_type == "MatMul":
if (
self.node_reference[node.output[0]] != 1
or i + 19 >= self.node_count
):
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
node4 = self.mutable_graph_nodes[i + 3]
node5 = self.mutable_graph_nodes[i + 4]
node6 = self.mutable_graph_nodes[i + 5]
node7 = self.mutable_graph_nodes[i + 6]
node8 = self.mutable_graph_nodes[i + 7]
node9 = self.mutable_graph_nodes[i + 8]
node10 = self.mutable_graph_nodes[i + 9]
node11 = self.mutable_graph_nodes[i + 10]
node12 = self.mutable_graph_nodes[i + 11]
node13 = self.mutable_graph_nodes[i + 12]
node14 = self.mutable_graph_nodes[i + 13]
node15 = self.mutable_graph_nodes[i + 14]
node16 = self.mutable_graph_nodes[i + 15]
node17 = self.mutable_graph_nodes[i + 16]
node18 = self.mutable_graph_nodes[i + 17]
node19 = self.mutable_graph_nodes[i + 18]
node20 = self.mutable_graph_nodes[i + 19]
if (
node2.op_type != "Add"
or node3.op_type != "MatMul"
or node4.op_type != "Add"
or node5.op_type != "MatMul"
or node6.op_type != "Add"
or node7.op_type != "Mul"
or node8.op_type != "Reshape"
or node9.op_type != "Transpose"
or node10.op_type != "Reshape"
or node11.op_type != "Reshape"
or node12.op_type != "Transpose"
or node13.op_type != "Transpose"
or node14.op_type != "MatMul"
or node15.op_type != "Softmax"
or node16.op_type != "MatMul"
or node17.op_type != "Transpose"
or node18.op_type != "Reshape"
or node19.op_type != "MatMul"
or node20.op_type != "Add"
):
continue
if (
self.node_reference[node2.output[0]] != 1
or self.node_reference[node3.output[0]] != 1
or self.node_reference[node4.output[0]] != 1
or self.node_reference[node5.output[0]] != 1
or self.node_reference[node6.output[0]] != 1
or self.node_reference[node7.output[0]] != 1
or self.node_reference[node8.output[0]] != 1
or self.node_reference[node9.output[0]] != 1
or self.node_reference[node10.output[0]] != 1
or self.node_reference[node11.output[0]] != 1
or self.node_reference[node12.output[0]] != 1
or self.node_reference[node13.output[0]] != 1
or self.node_reference[node14.output[0]] != 1
or self.node_reference[node15.output[0]] != 1
or self.node_reference[node16.output[0]] != 1
or self.node_reference[node17.output[0]] != 1
or self.node_reference[node18.output[0]] != 1
or self.node_reference[node19.output[0]] != 1
):
continue
if (
node2.input[0] != node.output[0]
or node4.input[0] != node3.output[0]
or node6.input[0] != node5.output[0]
or node7.input[0] != node2.output[0]
or node8.input[0] != node7.output[0]
or node9.input[0] != node8.output[0]
or node10.input[0] != node4.output[0]
or node11.input[0] != node6.output[0]
or node12.input[0] != node11.output[0]
or node13.input[0] != node10.output[0]
or node14.input[0] != node9.output[0]
or node14.input[1] != node13.output[0]
or node15.input[0] != node14.output[0]
or node16.input[0] != node15.output[0]
or node16.input[1] != node12.output[0]
or node17.input[0] != node16.output[0]
or node18.input[0] != node17.output[0]
or node19.input[0] != node18.output[0]
or node20.input[0] != node19.output[0]
):
continue
q_B = get_node_attr_from_input_af(self.weights[node2.input[1]])
k_B = get_node_attr_from_input_af(self.weights[node4.input[1]])
v_B = get_node_attr_from_input_af(self.weights[node6.input[1]])
o_B = get_node_attr_from_input_af(self.weights[node20.input[1]])
if q_B.size != k_B.size or q_B.size != v_B.size or q_B.size != o_B.size:
continue
embed_dim = q_B.size
# 1 0 2
perm9 = get_node_attr_ai(node9, "perm")
perm12 = get_node_attr_ai(node12, "perm")
if perm9.size != 3 or perm9[0] != 1 or perm9[1] != 0 or perm9[2] != 2:
continue
if (
perm12.size != 3
or perm12[0] != 1
or perm12[1] != 0
or perm12[2] != 2
):
continue
# 1 2 0
perm13 = get_node_attr_ai(node13, "perm")
if (
perm13.size != 3
or perm13[0] != 1
or perm13[1] != 2
or perm13[2] != 0
):
continue
# 1 0 2
perm17 = get_node_attr_ai(node17, "perm")
if (
perm17.size != 3
or perm17[0] != 1
or perm17[1] != 0
or perm17[2] != 2
):
continue
softmax_axis = get_node_attr_i(node15, "axis")
if softmax_axis != 2:
continue
# 1/-1 seqlen * num_heads, embed_dim / num_heads
if len(node8.input) == 1:
shape8 = get_node_attr_ai(node8, "shape")
else:
if node8.input[1] not in self.weights:
continue
shape8 = get_node_attr_from_input_ai(self.weights[node8.input[1]])
if len(node10.input) == 1:
shape10 = get_node_attr_ai(node10, "shape")
else:
if node10.input[1] not in self.weights:
continue
shape10 = get_node_attr_from_input_ai(self.weights[node10.input[1]])
if len(node11.input) == 1:
shape11 = get_node_attr_ai(node11, "shape")
else:
if node11.input[1] not in self.weights:
continue
shape11 = get_node_attr_from_input_ai(self.weights[node11.input[1]])
if shape8.size != 3 or shape10.size != 3 or shape11.size != 3:
continue
if (
shape8[1] != shape10[1]
or shape8[1] != shape11[1]
or shape8[2] != shape10[2]
or shape8[2] != shape11[2]
):
continue
num_heads = embed_dim / shape8[2]
if len(node18.input) == 1:
shape18 = get_node_attr_ai(node18, "shape")
else:
if node18.input[1] not in self.weights:
continue
shape18 = get_node_attr_from_input_ai(self.weights[node18.input[1]])
if (
shape18.size != 3
or shape18[2] != embed_dim
or shape18[1] * num_heads != shape8[1]
):
continue
node.op_type = "noop_reducedncnn"
node2.op_type = "noop_reducedncnn"
node3.op_type = "noop_reducedncnn"
node4.op_type = "noop_reducedncnn"
node5.op_type = "noop_reducedncnn"
node6.op_type = "noop_reducedncnn"
node7.op_type = "noop_reducedncnn"
node8.op_type = "noop_reducedncnn"
node9.op_type = "noop_reducedncnn"
node10.op_type = "noop_reducedncnn"
node11.op_type = "noop_reducedncnn"
node12.op_type = "noop_reducedncnn"
node13.op_type = "noop_reducedncnn"
node14.op_type = "noop_reducedncnn"
node15.op_type = "noop_reducedncnn"
node16.op_type = "noop_reducedncnn"
node17.op_type = "noop_reducedncnn"
node18.op_type = "noop_reducedncnn"
node19.op_type = "noop_reducedncnn"
self.node_reference[node2.input[0]] -= 1
self.node_reference[node4.input[0]] -= 1
self.node_reference[node6.input[0]] -= 1
self.node_reference[node7.input[0]] -= 1
self.node_reference[node7.input[1]] -= 1
self.node_reference[node8.input[0]] -= 1
if len(node8.input) == 2:
self.node_reference[node8.input[1]] -= 1
self.node_reference[node9.input[0]] -= 1
self.node_reference[node10.input[0]] -= 1
if len(node10.input) == 2:
self.node_reference[node10.input[1]] -= 1
self.node_reference[node11.input[0]] -= 1
if len(node11.input) == 2:
self.node_reference[node11.input[1]] -= 1
self.node_reference[node12.input[0]] -= 1
self.node_reference[node13.input[0]] -= 1
self.node_reference[node14.input[0]] -= 1
self.node_reference[node14.input[1]] -= 1
self.node_reference[node15.input[0]] -= 1
self.node_reference[node16.input[0]] -= 1
self.node_reference[node16.input[1]] -= 1
self.node_reference[node17.input[0]] -= 1
self.node_reference[node18.input[0]] -= 1
if len(node18.input) == 2:
self.node_reference[node18.input[1]] -= 1
self.node_reference[node19.input[0]] -= 1
self.node_reference[node20.input[0]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
self.blob_names.pop(node3.output[0], None)
self.blob_names.pop(node4.output[0], None)
self.blob_names.pop(node5.output[0], None)
self.blob_names.pop(node6.output[0], None)
self.blob_names.pop(node7.output[0], None)
self.blob_names.pop(node8.output[0], None)
self.blob_names.pop(node9.output[0], None)
self.blob_names.pop(node10.output[0], None)
self.blob_names.pop(node11.output[0], None)
self.blob_names.pop(node12.output[0], None)
self.blob_names.pop(node13.output[0], None)
self.blob_names.pop(node14.output[0], None)
self.blob_names.pop(node15.output[0], None)
self.blob_names.pop(node16.output[0], None)
self.blob_names.pop(node17.output[0], None)
self.blob_names.pop(node18.output[0], None)
self.blob_names.pop(node19.output[0], None)
qw = node.input[1]
qb = node2.input[1]
kw = node3.input[1]
kb = node4.input[1]
vw = node5.input[1]
vb = node6.input[1]
ow = node19.input[1]
ob = node20.input[1]
node20.op_type = "MultiHeadAttention"
node20.ClearField("input")
node20.input.append(node.input[0])
node20.input.append(node3.input[0])
node20.input.append(node5.input[0])
node20.input.append(qw)
node20.input.append(qb)
node20.input.append(kw)
node20.input.append(kb)
node20.input.append(vw)
node20.input.append(vb)
node20.input.append(ow)
node20.input.append(ob)
attr_embed_dim = AttributeProto(
name="embed_dim", i=embed_dim, type=APT.INT
)
node20.attribute.append(attr_embed_dim)
attr_num_heads = AttributeProto(
name="num_heads", i=num_heads, type=APT.INT
)
node20.attribute.append(attr_num_heads)
reduced_node_count[0] += 19
i += 19 # noqa
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# MultiHeadAttention <= MatMul(qkv) - Add - Split
# - Mul
# - Reshape - Transpose
# - Reshape - Reshape - Transpose - Transpose
# - Gemm - Softmax - Gemm - Transpose - Reshape - MatMul - Add
if node.op_type == "MatMul":
if (
self.node_reference[node.output[0]] != 1
or i + 16 >= self.node_count
):
continue
node2 = self.mutable_graph_nodes[i + 1]
node3 = self.mutable_graph_nodes[i + 2]
node4 = self.mutable_graph_nodes[i + 3]
node5 = self.mutable_graph_nodes[i + 4]
node6 = self.mutable_graph_nodes[i + 5]
node7 = self.mutable_graph_nodes[i + 6]
node8 = self.mutable_graph_nodes[i + 7]
node9 = self.mutable_graph_nodes[i + 8]
node10 = self.mutable_graph_nodes[i + 9]
node11 = self.mutable_graph_nodes[i + 10]
node12 = self.mutable_graph_nodes[i + 11]
node13 = self.mutable_graph_nodes[i + 12]
node14 = self.mutable_graph_nodes[i + 13]
node15 = self.mutable_graph_nodes[i + 14]
node16 = self.mutable_graph_nodes[i + 15]
node17 = self.mutable_graph_nodes[i + 16]
if (
node2.op_type != "Add"
or node3.op_type != "Split"
or node4.op_type != "Mul"
or node5.op_type != "Reshape"
or node6.op_type != "Transpose"
or node7.op_type != "Reshape"
or node8.op_type != "Reshape"
or node9.op_type != "Transpose"
or node10.op_type != "Transpose"
or node11.op_type != "MatMul"
or node12.op_type != "Softmax"
or node13.op_type != "MatMul"
or node14.op_type != "Transpose"
or node15.op_type != "Reshape"
or node16.op_type != "MatMul"
or node17.op_type != "Add"
):
continue
if (
self.node_reference[node2.output[0]] != 1
or self.node_reference[node3.output[0]] != 1
or self.node_reference[node3.output[1]] != 1
or self.node_reference[node3.output[2]] != 1
or self.node_reference[node4.output[0]] != 1
or self.node_reference[node5.output[0]] != 1
or self.node_reference[node6.output[0]] != 1
or self.node_reference[node7.output[0]] != 1
or self.node_reference[node8.output[0]] != 1
or self.node_reference[node9.output[0]] != 1
or self.node_reference[node10.output[0]] != 1
or self.node_reference[node11.output[0]] != 1
or self.node_reference[node12.output[0]] != 1
or self.node_reference[node13.output[0]] != 1
or self.node_reference[node14.output[0]] != 1
or self.node_reference[node15.output[0]] != 1
or self.node_reference[node16.output[0]] != 1
):
continue
if (
node2.input[0] != node.output[0]
or node3.input[0] != node2.output[0]
or node4.input[0] != node3.output[0]
or node5.input[0] != node4.output[0]
or node6.input[0] != node5.output[0]
or node7.input[0] != node3.output[1]
or node8.input[0] != node3.output[2]
or node9.input[0] != node8.output[0]
or node10.input[0] != node7.output[0]
or node11.input[0] != node6.output[0]
or node11.input[1] != node10.output[0]
or node12.input[0] != node11.output[0]
or node13.input[0] != node12.output[0]
or node13.input[1] != node9.output[0]
or node14.input[0] != node13.output[0]
or node15.input[0] != node14.output[0]
or node16.input[0] != node15.output[0]
or node17.input[0] != node16.output[0]
):
continue
qkv_B = get_node_attr_from_input_af(self.weights[node2.input[1]])
o_B = get_node_attr_from_input_af(self.weights[node17.input[1]])
if qkv_B.size != o_B.size * 3:
continue
embed_dim = o_B.size
# 1 0 2
perm6 = get_node_attr_ai(node6, "perm")
perm9 = get_node_attr_ai(node9, "perm")
if perm6.size != 3 or perm6[0] != 1 or perm6[1] != 0 or perm6[2] != 2:
continue
if perm9.size != 3 or perm9[0] != 1 or perm9[1] != 0 or perm9[2] != 2:
continue
# 1 2 0
perm10 = get_node_attr_ai(node10, "perm")
if (
perm10.size != 3
or perm10[0] != 1
or perm10[1] != 2
or perm10[2] != 0
):
continue
# 1 0 2
perm14 = get_node_attr_ai(node14, "perm")
if (
perm14.size != 3
or perm14[0] != 1
or perm14[1] != 0
or perm14[2] != 2
):
continue
softmax_axis = get_node_attr_i(node12, "axis")
if softmax_axis != 2:
continue
# 1/-1, seqlen * num_heads, embed_dim / num_heads
if len(node5.input) == 1:
shape5 = get_node_attr_ai(node5, "shape")
else:
if node5.input[1] not in self.weights:
continue
shape5 = get_node_attr_from_input_ai(self.weights[node5.input[1]])
if len(node7.input) == 1:
shape7 = get_node_attr_ai(node7, "shape")
else:
if node7.input[1] not in self.weights:
continue
shape7 = get_node_attr_from_input_ai(self.weights[node7.input[1]])
if len(node8.input) == 1:
shape8 = get_node_attr_ai(node8, "shape")
else:
if node8.input[1] not in self.weights:
continue
shape8 = get_node_attr_from_input_ai(self.weights[node8.input[1]])
if (
shape5[1] != shape7[1]
or shape5[1] != shape8[1]
or shape5[2] != shape7[2]
or shape5[2] != shape8[2]
):
continue
num_heads = embed_dim / shape5[2]
# 1, seqlen, embed_dim
if len(node15.input) == 1:
shape15 = get_node_attr_ai(node15, "shape")
else:
if node15.input[1] not in self.weights:
continue
shape15 = get_node_attr_from_input_ai(self.weights[node15.input[1]])
if (
shape15.size != 3
or shape15[2] != embed_dim
or shape15[1] * num_heads != shape8[1]
):
continue
# reduce
node.op_type = "noop_reducedncnn"
node2.op_type = "noop_reducedncnn"
node3.op_type = "noop_reducedncnn"
node4.op_type = "noop_reducedncnn"
node5.op_type = "noop_reducedncnn"
node6.op_type = "noop_reducedncnn"
node7.op_type = "noop_reducedncnn"
node8.op_type = "noop_reducedncnn"
node9.op_type = "noop_reducedncnn"
node10.op_type = "noop_reducedncnn"
node11.op_type = "noop_reducedncnn"
node12.op_type = "noop_reducedncnn"
node13.op_type = "noop_reducedncnn"
node14.op_type = "noop_reducedncnn"
node15.op_type = "noop_reducedncnn"
node16.op_type = "noop_reducedncnn"
self.node_reference[node2.input[0]] -= 1
self.node_reference[node3.input[0]] -= 1
self.node_reference[node4.input[0]] -= 1
self.node_reference[node4.input[1]] -= 1
self.node_reference[node5.input[0]] -= 1
if len(node5.input) == 2:
self.node_reference[node5.input[1]] -= 1
self.node_reference[node6.input[0]] -= 1
self.node_reference[node7.input[0]] -= 1
if len(node7.input) == 2:
self.node_reference[node7.input[1]] -= 1
self.node_reference[node8.input[0]] -= 1
if len(node8.input) == 2:
self.node_reference[node8.input[1]] -= 1
self.node_reference[node9.input[0]] -= 1
self.node_reference[node10.input[0]] -= 1
self.node_reference[node11.input[0]] -= 1
self.node_reference[node11.input[1]] -= 1
self.node_reference[node12.input[0]] -= 1
self.node_reference[node13.input[0]] -= 1
self.node_reference[node13.input[1]] -= 1
self.node_reference[node14.input[0]] -= 1
self.node_reference[node15.input[0]] -= 1
if len(node15.input) == 2:
self.node_reference[node15.input[1]] -= 1
self.node_reference[node16.input[0]] -= 1
self.node_reference[node17.input[0]] -= 1
self.blob_names.pop(node.output[0], None)
self.blob_names.pop(node2.output[0], None)
self.blob_names.pop(node3.output[0], None)
self.blob_names.pop(node3.output[1], None)
self.blob_names.pop(node3.output[2], None)
self.blob_names.pop(node4.output[0], None)
self.blob_names.pop(node5.output[0], None)
self.blob_names.pop(node6.output[0], None)
self.blob_names.pop(node7.output[0], None)
self.blob_names.pop(node8.output[0], None)
self.blob_names.pop(node9.output[0], None)
self.blob_names.pop(node10.output[0], None)
self.blob_names.pop(node11.output[0], None)
self.blob_names.pop(node12.output[0], None)
self.blob_names.pop(node13.output[0], None)
self.blob_names.pop(node14.output[0], None)
self.blob_names.pop(node15.output[0], None)
self.blob_names.pop(node16.output[0], None)
qkvw = node.input[1]
qkvb = node2.input[1]
ow = node16.input[1]
ob = node17.input[1]
node17.op_type = "MultiHeadAttention"
node17.ClearField("input")
node17.input.append(node.input[0])
node17.input.append(qkvw)
node17.input.append(qkvb)
node17.input.append(ow)
node17.input.append(ob)
attr_embed_dim = AttributeProto(
name="embed_dim", i=embed_dim, type=APT.INT
)
node17.attribute.append(attr_embed_dim)
attr_num_heads = AttributeProto(
name="num_heads", i=num_heads, type=APT.INT
)
node17.attribute.append(attr_num_heads)
reduced_node_count[0] += 16
i += 16 # noqa
def fuse_binaryop_with_scalar(self) -> None:
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# Add/Sub/Mul/Div/Min/Max/Pow(a, x)
if node.op_type in ["Add", "Sub", "Mul", "Div", "Min", "Max", "Pow"]:
if node.input[0] not in self.weights:
continue
scalar_b = self.weights[node.input[0]]
if (
len(scalar_b.dims) != 0
or get_tensor_proto_data_size(scalar_b, scalar_b.data_type) != 1
):
continue
if node.op_type == "Sub":
node.op_type = "RSub"
elif node.op_type == "Div":
node.op_type = "RDiv"
b = get_node_attr_from_input_f(scalar_b)
self.node_reference[node.input[0]] -= 1
node_input = node.input[1]
node.ClearField("input")
node.input.append(node_input)
attr_with_scalar = AttributeProto(name="with_scalar", i=1, type=APT.INT)
node.attribute.append(attr_with_scalar)
attr_b = AttributeProto(name="b", f=b, type=APT.FLOAT)
node.attribute.append(attr_b)
for i in range(self.node_count):
node = self.mutable_graph_nodes[i]
# Add/Sub/Mul/Div/Min/Max/Pow(x, b)
if node.op_type in ["Add", "Sub", "Mul", "Div", "Min", "Max", "Pow"]:
if node.input[1] not in self.weights:
continue
scalar_b = self.weights[node.input[1]]
if (
len(scalar_b.dims) != 0
or get_tensor_proto_data_size(scalar_b, scalar_b.data_type) != 1
):
continue
b = get_node_attr_from_input_f(scalar_b)
self.node_reference[node.input[1]] -= 1
node_input = node.input[0]
node.ClearField("input")
node.input.append(node_input)
attr_with_scalar = AttributeProto(name="with_scalar", i=1, type=APT.INT)
node.attribute.append(attr_with_scalar)
attr_b = AttributeProto(name="b", f=b, type=APT.FLOAT)
node.attribute.append(attr_b)
def convert(self, is_fp16: bool = False, include_mem_data: bool = True):
if is_fp16:
logger.debug("NCNN mode: fp16")
else:
logger.debug("NCNN mode: fp32")
# Topological sort
i = 0
while i < self.node_count:
node = self.mutable_graph_nodes[i]
swapnode = False
missing_input_name = None
for input_name in node.input:
if (
input_name
and input_name not in self.producers
and input_name not in self.weights
):
swapnode = True
missing_input_name = input_name
break
# If nothing missing, add outputs to producers and continue
# to next node
if not swapnode:
for output_name in node.output:
if output_name:
self.producers[output_name] = None
i += 1
continue
# find node that produces missing_input_name
swap_j = 0
for j, nodeq in enumerate(self.mutable_graph_nodes, i + 1):
swap_j = j
found = False
for output_name in nodeq.output:
if output_name == missing_input_name:
found = True
break
if found:
break
else:
raise RuntimeError(
f"Cannot find node that produces {missing_input_name}, "
f"which is required by node {i} ({node.name})."
)
self.swap_nodes(i, swap_j)
# global definition line
# [layer count][blob count]
for node in self.onnx_graph.node:
op = node.op_type
if not node.name:
node.name = node.output[0]
if op == "Constant":
self.weights[node.output[0]] = get_node_attr_tensor(node, "value")
for input_name in node.input:
self.blob_names[input_name] = None
if input_name not in self.node_reference:
self.node_reference[input_name] = 1
else:
self.node_reference[input_name] += 1
if op == "Dropout":
output_name = node.output[0]
self.blob_names[output_name] = None
self.node_reference[output_name] = 0
continue
for output_name in node.output:
self.blob_names[output_name] = None
self.node_reference[output_name] = 0
# include Input node
input_node_count = 0
for graph_input in self.onnx_graph.input:
input_name = graph_input.name
# check weight
if input_name not in self.weights:
self.blob_names[input_name] = None
input_node_count += 1
# op chain fusion
reduced_node_count = [0]
self.fuse_weight_reshape(reduced_node_count)
self.fuse_weight_transpose(reduced_node_count)
self.fuse_shufflechannel(reduced_node_count)
self.fuse_shufflechannel_split(reduced_node_count)
self.fuse_hardsigmoid(reduced_node_count)
self.fuse_hardswish(reduced_node_count)
self.fuse_swish(reduced_node_count)
self.fuse_batchnorm1d_squeeze_unsqueeze(reduced_node_count)
self.fuse_unsqueeze_prelu(reduced_node_count)
self.fuse_normalize(reduced_node_count)
self.fuse_groupnorm(reduced_node_count)
self.fuse_layernorm(reduced_node_count)
self.fuse_flatten(reduced_node_count)
self.fuse_pixelshuffle(reduced_node_count)
self.fuse_reorg(reduced_node_count)
self.fuse_expand_broadcast(reduced_node_count)
self.fuse_lstm_gru_rnn(reduced_node_count)
self.fuse_multiheadattention(reduced_node_count)
self.fuse_binaryop_with_scalar()
self.fuse_rewrite_gather()
# reduce common const weight node_reference
for node in self.onnx_graph.node:
op = node.op_type
if op == "BatchNormalization":
self.node_reference[node.input[1]] -= 1
self.node_reference[node.input[2]] -= 1
self.node_reference[node.input[3]] -= 1
self.node_reference[node.input[4]] -= 1
elif op == "BiasGelu":
self.node_reference[node.input[1]] -= 1
elif op == "Clip":
if len(node.input) == 3:
self.node_reference[node.input[1]] -= 1
self.node_reference[node.input[2]] -= 1
elif op == "Conv":
self.node_reference[node.input[1]] -= 1
if len(node.input) == 3:
self.node_reference[node.input[2]] -= 1
elif op == "ConvTranspose":
self.node_reference[node.input[1]] -= 1
if len(node.input) == 3:
self.node_reference[node.input[2]] -= 1
elif op == "EmbedLayerNormalization":
self.node_reference[node.input[1]] -= 1
self.node_reference[node.input[2]] -= 1
self.node_reference[node.input[3]] -= 1
self.node_reference[node.input[4]] -= 1
self.node_reference[node.input[5]] -= 1
self.node_reference[node.input[6]] -= 1
elif op == "Gemm":
alpha = get_node_attr_f(node, "alpha", 1)
beta = get_node_attr_f(node, "beta", 1)
transA = get_node_attr_i(node, "transA", 0)
transB = get_node_attr_i(node, "transB", 0)
if alpha == 1 and beta == 1 and transA == 0 and transB == 1:
# InnerProduct-like A * B + C
self.node_reference[node.input[1]] -= 1
self.node_reference[node.input[2]] -= 1
elif op == "GroupNorm":
affine = get_node_attr_i(node, "affine", 1)
if affine:
self.node_reference[node.input[1]] -= 1
self.node_reference[node.input[2]] -= 1
elif op == "GRU":
for gru_input in node.input:
self.node_reference[gru_input] -= 1
elif op == "InstanceNormalization":
self.node_reference[node.input[1]] -= 1
self.node_reference[node.input[2]] -= 1
elif op == "LayerNorm":
affine = get_node_attr_i(node, "affine", 1)
if affine:
self.node_reference[node.input[1]] -= 1
self.node_reference[node.input[2]] -= 1
elif op == "LSTM":
for lstm_input in node.input:
self.node_reference[lstm_input] -= 1
elif op == "MatMul":
if (
node.input[1] in self.weights
and len(self.weights[node.input[1]].dims) == 2
):
# InnerProduct
self.node_reference[node.input[1]] -= 1
elif op == "MultiHeadAttention":
if len(node.input) == 5:
self.node_reference[node.input[1]] -= 1
self.node_reference[node.input[2]] -= 1
self.node_reference[node.input[3]] -= 1
self.node_reference[node.input[4]] -= 1
else:
self.node_reference[node.input[3]] -= 1
self.node_reference[node.input[4]] -= 1
self.node_reference[node.input[5]] -= 1
self.node_reference[node.input[6]] -= 1
self.node_reference[node.input[7]] -= 1
self.node_reference[node.input[8]] -= 1
self.node_reference[node.input[9]] -= 1
self.node_reference[node.input[10]] -= 1
elif op == "Pad":
if len(node.input) >= 2:
self.node_reference[node.input[1]] -= 1
elif op == "PRelu":
self.node_reference[node.input[1]] -= 1
elif op == "Reshape":
if len(node.input) >= 2:
self.node_reference[node.input[1]] -= 1
elif op == "Resize":
if len(node.input) == 2:
# opset 10
self.node_reference[node.input[1]] -= 1
else:
# opset 11+
self.node_reference[node.input[1]] -= 1
self.node_reference[node.input[2]] -= 1
if len(node.input) >= 4:
self.node_reference[node.input[3]] -= 1
elif op == "RNN":
for rnn_input in node.input:
self.node_reference[rnn_input] -= 1
elif op == "SkipLayerNormalization":
self.node_reference[node.input[2]] -= 1
self.node_reference[node.input[3]] -= 1
self.node_reference[node.input[4]] -= 1
elif op == "Slice":
if len(node.input) >= 2:
self.node_reference[node.input[1]] -= 1
self.node_reference[node.input[2]] -= 1
if len(node.input) >= 4:
self.node_reference[node.input[3]] -= 1
if len(node.input) >= 5:
self.node_reference[node.input[4]] -= 1
elif op == "Upsample":
if len(node.input) >= 2:
self.node_reference[node.input[1]] -= 1
elif op in ("adaptive_avg_pool2d", "adaptive_max_pool2d"):
if len(node.input) >= 2:
self.node_reference[node.input[1]] -= 1
# count all weight node with zero reference
zero_reference_weight_node_count = 0
for input_name in self.weights.keys():
# there may be some weight nodes in initializer but none of the graph nodes use them
# add them to blob_names so we could get proper blob count later
self.blob_names[input_name] = None
refcount = self.node_reference[input_name]
if refcount == 0:
zero_reference_weight_node_count += 1
# we always treat constant nodes as weights or binaryop_weights
# do not count it twice for layer_count
constant_node_count_moved_to_weight = 0
for node in self.onnx_graph.node:
if node.op_type == "Constant":
constant_node_count_moved_to_weight += 1
# some ops may have anonymous input
# LSTM sequence_lens
self.blob_names.pop("", None)
self.node_reference.pop("", None)
# remove node_reference entries with references equal to one
split_layer_count = 0
splitncnn_blob_count = 0
# split node reference
split_node_reference = {}
for ref, count in self.node_reference.items():
if count > 1:
split_layer_count += 1
splitncnn_blob_count += count
split_node_reference[ref] = count
ncnn_node_count = (
self.node_count
- constant_node_count_moved_to_weight
+ len(self.weights)
- zero_reference_weight_node_count
- reduced_node_count[0]
+ input_node_count
+ split_layer_count
)
ncnn_blob_count = (
len(self.blob_names)
- zero_reference_weight_node_count
+ splitncnn_blob_count
)
ncnn_model = NcnnModel(ncnn_node_count, ncnn_blob_count)
logger.debug(
f"Node count: {ncnn_model.node_count}, Blob count: {ncnn_model.blob_count}"
)
bin_length = 0
for i, graph_input in enumerate(self.onnx_graph.input):
input_name = graph_input.name
# Make sure input is not in weights
if input_name not in self.weights:
ncnn_model.add_layer(
NcnnLayer("Input", input_name, 0, 1, outputs=[input_name])
)
refcount = self.node_reference[input_name]
if refcount > 1:
layer_input_list = [
f"{input_name}_splitncnn_{j}" for j in range(refcount)
]
ncnn_model.add_layer(
NcnnLayer(
"Split",
f"splitncnn_input{i}",
1,
refcount,
[input_name],
layer_input_list,
)
)
# place MemoryData next if it is being included
internal_split = 0
if include_mem_data:
for input_name, M in self.weights.items():
refcount = self.node_reference[input_name]
if refcount != 0:
layer = NcnnLayer("MemoryData", input_name, 0, 1, [input_name])
M_dims_size = len(M.dims)
if M_dims_size == 0:
layer.add_param(0, get_tensor_proto_data_size(M, M.data_type))
elif M_dims_size == 1:
layer.add_param(0, M.dims[0])
elif M_dims_size == 2:
layer.add_param(0, M.dims[1])
if M.dims[0] != 1:
layer.add_param(1, M.dims[0])
elif M_dims_size == 3:
layer.add_param(0, M.dims[2])
layer.add_param(1, M.dims[1])
if M.dims[0] != 1:
layer.add_param(2, M.dims[0])
elif M_dims_size == 4:
layer.add_param(0, M.dims[3])
layer.add_param(1, M.dims[2])
layer.add_param(2, M.dims[1])
bin_length += self.add_weight(layer, "MemoryData", M)
ncnn_model.add_layer(layer)
if refcount > 1:
layer_output_list = [
f"{input_name}_splitncnn_{i}" for i in range(refcount)
]
ncnn_model.add_layer(
NcnnLayer(
"Split",
f"splitncnn_{internal_split}",
1,
refcount,
[input_name],
layer_output_list,
)
)
internal_split += 1
for node in self.onnx_graph.node:
op = node.op_type
if op == "noop_reducedncnn":
continue
name = node.name
if not name:
name = node.output[0]
input_size = len(node.input)
output_size = len(node.output)
for input_name in node.input:
# check weight
if not input_name or (
input_name in self.weights and self.node_reference[input_name] == 0
):
input_size -= 1
layer = NcnnLayer()
if op in [
"Abs",
"Acos",
"Asin",
"Atan",
"Ceil",
"Cos",
"Exp",
"Floor",
"Log",
"Neg",
"Reciprocal",
"Sin",
"Sqrt",
"Tan",
"Tanh",
]:
layer.op_type = "UnaryOp"
elif op in [
"Add",
"Div",
"Max",
"Min",
"Mul",
"Pow",
"RDiv",
"RSub",
"Sub",
]:
layer.op_type = "BinaryOp"
elif op in ("AveragePool", "MaxPool"):
kernel_shape = get_node_attr_ai(node, "kernel_shape")
if kernel_shape.size == 1:
layer.op_type = "Pooling1D"
else:
layer.op_type = "Pooling"
elif op == "BatchNormalization":
layer.op_type = "BatchNorm"
elif op == "BiasGelu":
layer.op_type = "BiasGelu"
elif op == "Clip":
layer.op_type = "Clip"
elif op == "Concat":
layer.op_type = "Concat"
elif op == "Constant":
continue
elif op == "Conv":
kernel_shape = get_node_attr_ai(node, "kernel_shape")
if kernel_shape.size == 1:
layer.op_type = "Convolution1D"
else:
group = get_node_attr_i(node, "group", 1)
if group > 1:
layer.op_type = "ConvolutionDepthWise"
else:
layer.op_type = "Convolution"
elif op == "ConvTranspose":
group = get_node_attr_i(node, "group", 1)
if group > 1:
layer.op_type = "DeconvolutionDepthWise"
else:
layer.op_type = "Deconvolution"
elif op in ("Crop", "Slice"):
layer.op_type = "Crop"
elif op in ("DepthToSpace", "PixelShuffle"):
layer.op_type = "PixelShuffle"
elif op == "Dropout":
layer.op_type = "Dropout"
output_size = 1
elif op == "Elu":
layer.op_type = "ELU"
elif op == "EmbedLayerNormalization":
layer.op_type = "EmbedLayerNormalization"
elif op == "Flatten":
layer.op_type = "Flatten"
elif op == "Gelu":
layer.op_type = "GELU"
elif op == "Gemm":
alpha = get_node_attr_f(node, "alpha", 1)
beta = get_node_attr_f(node, "beta", 1)
transA = get_node_attr_i(node, "transA", 0)
transB = get_node_attr_i(node, "transB", 0)
if alpha == 1 and beta == 1 and transA == 0 and transB == 1:
# InnerProduct-like A * B + C
layer.op_type = "InnerProduct"
else:
layer.op_type = "Gemm"
elif op in [
"GlobalAveragePool",
"GlobalMaxPool",
"adaptive_avg_pool2d",
"adaptive_max_pool2d",
]:
layer.op_type = "Pooling"
elif op == "GroupNorm":
layer.op_type = "GroupNorm"
elif op == "GRU":
layer.op_type = "GRU"
elif op == "HardSigmoid":
layer.op_type = "HardSigmoid"
elif op == "HardSwish":
layer.op_type = "HardSwish"
elif op == "ImageScaler":
layer.op_type = "Scale"
elif op == "InstanceNormalization":
layer.op_type = "InstanceNorm"
elif op == "LayerNorm":
layer.op_type = "LayerNorm"
elif op in ("LeakyRelu", "Relu"):
layer.op_type = "ReLU"
elif op == "LRN":
layer.op_type = "LRN"
elif op == "LSTM":
layer.op_type = "LSTM"
elif op == "MatMul":
if (
node.input[1] in self.weights
and len(self.weights[node.input[1]].dims) == 2
):
layer.op_type = "InnerProduct"
else:
layer.op_type = "Gemm"
elif op == "MultiHeadAttention":
layer.op_type = "MultiHeadAttention"
elif op == "Normalize":
layer.op_type = "Normalize"
elif op == "Pad":
layer.op_type = "Padding"
elif op == "PRelu":
layer.op_type = "PReLU"
elif op in [
"ReduceMax",
"ReduceMin",
"ReduceMean",
"ReduceProd",
"ReduceSum",
"ReduceSumSquare",
"ReduceL1",
"ReduceL2",
"ReduceLogSum",
"ReduceLogSumExp",
]:
layer.op_type = "Reduction"
elif op == "Reorg":
layer.op_type = "Reorg"
elif op == "Reshape":
layer.op_type = "Reshape"
elif op == "RNN":
layer.op_type = "RNN"
elif op == "ShuffleChannel":
layer.op_type = "ShuffleChannel"
elif op == "Sigmoid":
layer.op_type = "Sigmoid"
elif op == "SkipLayerNormalization":
layer.op_type = "SkipLayerNormalization"
elif op == "Softmax":
layer.op_type = "Softmax"
elif op == "Softplus":
layer.op_type = "Softplus"
elif op == "Split":
layer.op_type = "Slice"
elif op == "Squeeze":
layer.op_type = "Squeeze"
elif op == "Sum":
layer.op_type = "Eltwise"
elif op == "Swish":
layer.op_type = "Swish"
elif op == "Transpose":
layer.op_type = "Permute"
elif op in ("Upsample", "Resize"):
layer.op_type = "Interp"
elif op == "Unsqueeze":
layer.op_type = "ExpandDims"
else:
error_msg = f"{op} not currently supported by NCNN."
raise ValueError(error_msg)
layer.name = name
layer.num_inputs = input_size
layer.num_outputs = output_size
layer.params.set_op(layer.op_type)
for input_name in node.input:
# check weight
if input_name and not (
input_name in self.weights and self.node_reference[input_name] == 0
):
if input_name in split_node_reference:
refidx = split_node_reference[input_name] - 1
split_node_reference[input_name] = refidx
input_name = f"{input_name}_splitncnn_{refidx}" # noqa
layer.inputs.append(input_name)
for o in range(output_size):
layer.outputs.append(node.output[o])
if op == "Abs":
layer.add_param(0, UOT.ABS)
elif op == "Acos":
layer.add_param(0, UOT.ACOS)
elif layer.op_type == "BinaryOp":
if op == "Add":
layer.add_param(0, BOT.ADD)
elif op == "Div":
layer.add_param(0, BOT.DIV)
elif op == "Max":
layer.add_param(0, BOT.MAX)
elif op == "Min":
layer.add_param(0, BOT.MIN)
elif op == "Mul":
layer.add_param(0, BOT.MUL)
elif op == "Pow":
layer.add_param(0, BOT.POW)
elif op == "RDiv":
layer.add_param(0, BOT.RDIV)
elif op == "RSub":
layer.add_param(0, BOT.RSUB)
elif op == "Sub":
layer.add_param(0, BOT.SUB)
with_scalar = get_node_attr_i(node, "with_scalar", 0)
b = get_node_attr_f(node, "b", 0)
if with_scalar:
layer.add_param(1, with_scalar)
layer.add_param(2, b)
elif op == "Asin":
layer.add_param(0, UOT.ASIN)
elif op == "Atan":
layer.add_param(0, UOT.ATAN)
elif op in ("AveragePool", "MaxPool"):
auto_pad = get_node_attr_s(node, "auto_pad")
ceil_mode = get_node_attr_i(node, "ceil_mode", 0)
kernel_shape = get_node_attr_ai(node, "kernel_shape")
strides = get_node_attr_ai(node, "strides")
pads = get_node_attr_ai(node, "pads")
pool = int(op == "AveragePool")
if ceil_mode == 1:
pad_mode = PAM.FULL
elif auto_pad == "SAME_UPPER":
pad_mode = PAM.SAMEUPPER
elif auto_pad == "SAME_LOWER":
pad_mode = PAM.SAMELOWER
else:
pad_mode = PAM.VALID
layer.add_param(0, pool)
if kernel_shape.size == 1:
layer.add_param(1, int(kernel_shape[0]))
elif kernel_shape.size == 2:
layer.add_param(1, int(kernel_shape[1]))
layer.add_param(11, int(kernel_shape[0]))
if strides.size == 1:
layer.add_param(2, int(strides[0]))
elif strides.size == 2:
layer.add_param(2, int(strides[1]))
layer.add_param(12, int(strides[0]))
if pads.size == 1:
layer.add_param(3, int(pads[0]))
elif pads.size == 2:
layer.add_param(3, int(pads[1]))
layer.add_param(13, int(pads[0]))
elif pads.size == 4:
layer.add_param(3, int(pads[1]))
layer.add_param(13, int(pads[0]))
layer.add_param(14, int(pads[3]))
layer.add_param(15, int(pads[2]))
layer.add_param(5, pad_mode)
if pool:
avgpool_count_include_pad = get_node_attr_i(
node, "count_include_pad", 0
)
layer.add_param(6, avgpool_count_include_pad)
elif op == "BatchNormalization":
epsilon = get_node_attr_f(node, "epsilon", 0.00001)
scale = self.weights[node.input[1]]
B = self.weights[node.input[2]]
mean = self.weights[node.input[3]]
var = self.weights[node.input[4]]
channels = get_tensor_proto_data_size(scale, scale.data_type)
layer.add_param(0, channels)
bin_length += self.add_weight(layer, "slope", scale)
bin_length += self.add_weight(layer, "mean", mean)
# apply epsilon to var
v = onph.to_array(var)
ve = np.array([v[i] + epsilon for i in range(channels)], np.float32)
bin_length += self.add_weight(layer, "variance", ve)
bin_length += self.add_weight(layer, "bias", B)
elif op == "BiasGelu":
B = self.weights[node.input[1]]
layer.add_param(0, get_tensor_proto_data_size(B, B.data_type))
bin_length += self.add_weight(layer, "bias", B)
elif op == "Ceil":
layer.add_param(0, UOT.CEIL)
elif op == "Clip":
if len(node.input) == 1:
minimum = get_node_attr_f(node, "min", -FLOAT32_MAX)
maximum = get_node_attr_f(node, "max", FLOAT32_MAX)
else:
minimum = (
get_node_attr_from_input_f(self.weights[node.input[1]])
if node.input[1] in self.weights
else -FLOAT32_MAX
)
maximum = (
get_node_attr_from_input_f(self.weights[node.input[2]])
if node.input[2] in self.weights
else FLOAT32_MAX
)
layer.add_param(0, minimum)
layer.add_param(1, maximum)
elif op == "Concat":
axis = get_node_attr_i(node, "axis", 1)
layer.add_param(0, axis - 1 if axis > 0 else axis)
elif op == "Constant":
logger.error("Code should not have reached inside Constant.")
elif op == "Conv":
W = self.weights[node.input[1]]
num_filter = W.dims[0]
has_bias = int(len(node.input) == 3)
auto_pad = get_node_attr_s(node, "auto_pad")
kernel_shape = get_node_attr_ai(node, "kernel_shape")
dilations = get_node_attr_ai(node, "dilations")
strides = get_node_attr_ai(node, "strides")
pads = get_node_attr_ai(node, "pads")
group = get_node_attr_i(node, "group", 1)
layer.add_param(0, num_filter)
if kernel_shape.size == 1:
layer.add_param(1, int(kernel_shape[0]))
elif kernel_shape.size == 2:
layer.add_param(1, int(kernel_shape[1]))
layer.add_param(11, int(kernel_shape[0]))
if dilations.size == 1:
layer.add_param(2, int(dilations[0]))
elif dilations.size == 2:
layer.add_param(2, int(dilations[1]))
layer.add_param(12, int(dilations[0]))
if strides.size == 1:
layer.add_param(3, int(strides[0]))
elif strides.size == 2:
layer.add_param(3, int(strides[1]))
layer.add_param(13, int(strides[0]))
if auto_pad == "SAME_UPPER":
layer.add_param(4, -233)
elif auto_pad == "SAME_LOWER":
layer.add_param(4, -234)
elif pads.size == 1:
layer.add_param(4, int(pads[0]))
elif pads.size == 2:
layer.add_param(4, int(pads[1]))
layer.add_param(14, int(pads[0]))
elif pads.size == 4:
layer.add_param(4, int(pads[1]))
layer.add_param(14, int(pads[0]))
layer.add_param(15, int(pads[3]))
layer.add_param(16, int(pads[2]))
layer.add_param(5, has_bias)
layer.add_param(6, get_tensor_proto_data_size(W, W.data_type))
if group > 1:
layer.add_param(7, int(group))
quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32
bin_length += self.add_weight(layer, "weight", W, quantize_tag)
if has_bias:
B = self.weights[node.input[2]]
bin_length += self.add_weight(layer, "bias", B)
elif op == "ConvTranspose":
W = self.weights[node.input[1]]
has_bias = int(len(node.input) == 3)
auto_pad = get_node_attr_s(node, "auto_pad")
kernel_shape = get_node_attr_ai(node, "kernel_shape")
dilations = get_node_attr_ai(node, "dilations")
strides = get_node_attr_ai(node, "strides")
output_padding = get_node_attr_ai(node, "output_padding")
output_shape = get_node_attr_ai(node, "output_shape")
pads = get_node_attr_ai(node, "pads")
group = get_node_attr_i(node, "group", 1)
num_filter = W.dims[1] * group
layer.add_param(0, num_filter)
if kernel_shape.size == 1:
layer.add_param(1, int(kernel_shape[0]))
elif kernel_shape.size == 2:
layer.add_param(1, int(kernel_shape[1]))
layer.add_param(11, int(kernel_shape[0]))
if dilations.size == 1:
layer.add_param(2, int(dilations[0]))
elif dilations.size == 2:
layer.add_param(2, int(dilations[1]))
layer.add_param(12, int(dilations[0]))
if strides.size == 1:
layer.add_param(3, int(strides[0]))
elif strides.size == 2:
layer.add_param(3, int(strides[1]))
layer.add_param(13, int(strides[0]))
if auto_pad == "SAME_UPPER":
layer.add_param(4, -233)
elif auto_pad == "SAME_LOWER":
layer.add_param(4, -234)
elif pads.size == 1:
layer.add_param(4, int(pads[0]))
elif pads.size == 2:
layer.add_param(4, int(pads[1]))
layer.add_param(14, int(pads[0]))
elif pads.size == 4:
layer.add_param(4, int(pads[1]))
layer.add_param(14, int(pads[0]))
layer.add_param(15, int(pads[3]))
layer.add_param(16, int(pads[2]))
if output_padding.size == 1:
layer.add_param(18, int(output_padding[0]))
elif output_padding.size == 2:
layer.add_param(18, int(output_padding[1]))
layer.add_param(19, int(output_padding[0]))
if output_shape.size == 1:
layer.add_param(20, int(output_shape[0]))
elif output_shape == 2:
layer.add_param(20, int(output_shape[1]))
layer.add_param(21, int(output_shape[0]))
layer.add_param(5, has_bias)
weight_data_size = get_tensor_proto_data_size(W, W.data_type)
layer.add_param(6, weight_data_size)
if group > 1:
layer.add_param(7, group)
quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32
weight_data = onph.to_array(W)
bin_length += self.add_weight(
layer, "weight", weight_data.swapaxes(0, 1), quantize_tag
)
if has_bias:
B = self.weights[node.input[2]]
bin_length += self.add_weight(layer, "bias", B)
elif op == "Cos":
layer.add_param(0, UOT.COS)
elif op == "Crop":
starts = get_node_attr_ai(node, "starts")
layer.add_param(9, [starts.size, *starts])
ends = get_node_attr_ai(node, "ends")
layer.add_param(10, [ends.size, *ends])
axes = get_node_attr_ai(node, "axis")
layer.add_param(11, [axes.size, *axes])
elif op == "DepthToSpace":
# pixelshuffle
scale_factor = get_node_attr_i(node, "blocksize", 1)
mode = get_node_attr_s(node, "mode")
layer.add_param(0, scale_factor)
if mode == "CRD":
layer.add_param(1, 0)
elif mode == "DCR":
layer.add_param(1, 1)
elif op == "Dropout":
pass
elif op == "Elu":
alpha = get_node_attr_f(node, "alpha", 1)
layer.add_param(0, alpha)
elif op == "EmbedLayerNormalization":
logger.error(f"No NCNN documentation for {op} yet, will not function")
words = self.weights[node.input[2]]
positions = self.weights[node.input[3]]
W = self.weights[node.input[5]]
B = self.weights[node.input[6]]
layer.add_param(0, get_tensor_proto_data_size(B, B.data_type))
layer.add_param(1, get_tensor_proto_data_size(words, words.data_type))
layer.add_param(
2, get_tensor_proto_data_size(positions, positions.data_type)
)
quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32
bin_length += self.add_weight(layer, "words", words, DTYPE_FP32)
bin_length += self.add_weight(layer, "positions", positions, DTYPE_FP32)
bin_length += self.add_weight(layer, "weight", W, quantize_tag)
bin_length += self.add_weight(layer, "bias", B)
elif op == "Exp":
layer.add_param(0, UOT.EXP)
elif op == "Flatten":
axis = get_node_attr_i(node, "axis", 1)
if axis != 1:
raise ValueError(f"Unsupported Flatten axis {axis}.")
elif op == "Floor":
layer.add_param(0, UOT.FLOOR)
elif op == "Gelu":
layer.add_param(0, 1)
elif op == "Gemm":
alpha = get_node_attr_f(node, "alpha", 1)
beta = get_node_attr_f(node, "beta", 1)
transA = get_node_attr_i(node, "transA", 0)
transB = get_node_attr_i(node, "transB", 0)
if alpha == 1 and beta == 1 and transA == 0 and transB == 1:
# InnerProduct-like A * B * C
B = self.weights[node.input[1]]
C = self.weights[node.input[2]]
layer.add_param(0, get_tensor_proto_data_size(C, C.data_type))
layer.add_param(1, 1)
layer.add_param(2, get_tensor_proto_data_size(B, B.data_type))
quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32
bin_length += self.add_weight(layer, "B", B, quantize_tag)
bin_length += self.add_weight(layer, "C", C)
else:
# gemm
layer.add_param(0, alpha)
layer.add_param(1, beta)
layer.add_param(2, transA)
layer.add_param(3, transB)
elif op in ("GlobalAveragePool", "GlobalMaxPool"):
layer.add_param(0, int(op == "GlobalAveragePool"))
layer.add_param(4, 1)
elif op in ("adaptive_avg_pool2d", "adaptive_max_pool2d"):
out_shape_tp = self.weights[node.input[1]]
out_shape = get_node_attr_from_input_ai(out_shape_tp)
layer.add_param(0, int(op == "adaptive_avg_pool2d"))
layer.add_param(7, 1)
if out_shape.size == 1:
layer.add_param(8, int(out_shape[0]))
elif out_shape.size == 2:
layer.add_param(8, int(out_shape[1])) # out_w
layer.add_param(18, int(out_shape[0])) # out_h
elif op == "GroupNorm":
groups = get_node_attr_i(node, "groups", 1)
channels = get_node_attr_i(node, "channels", 1)
eps = get_node_attr_f(node, "epsilon", 0.00001)
affine = get_node_attr_i(node, "affine", 1)
if affine:
# discard affine-less S=1 B=0
affine_S = get_node_attr_from_input_af(self.weights[node.input[1]])
affine_B = get_node_attr_from_input_af(self.weights[node.input[2]])
if (
affine_S.size == 1
and affine_S[0] == 1
and affine_B.size == 1
and affine_B[0] == 0
):
affine = 0
elif np.any(affine_S[:channels] != 1) or np.any(
affine_B[:channels] != 0
):
affine = 1
else:
affine = 0
layer.add_param(0, groups)
layer.add_param(1, channels)
layer.add_param(2, eps)
layer.add_param(3, affine)
if affine:
scale = self.weights[node.input[1]]
B = self.weights[node.input[2]]
bin_length += self.add_weight(layer, "scale", scale)
bin_length += self.add_weight(layer, "bias", B)
elif op == "GRU":
# W = self.weights[node.input[1]]
# R = self.weights[node.input[2]]
# B = self.weights[node.input[3]]
# hidden_size = get_node_attr_i(node, "hidden_size", 0)
# direction = get_node_attr_s(node, "direction")
# if direction == "forward":
# direction_type = GRU.FORWARD
# elif direction == "reverse":
# direction_type = GRU.REVERSE
# elif direction == "bidirectional":
# direction_type = GRU.BIDIRECTIONAL
# weight_data_size = get_tensor_proto_data_size(W)
# layer.add_param(0, hidden_size)
# layer.add_param(1, weight_data_size)
# layer.add_param(2, direction_type)
# num_directions = 2 if direction_type == GRU.BIDIRECTIONAL else 1
# reorder num_directions-URN-hidden_size to num_directions-RUN-hidden_size
# quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32
# logger.error(
# "Not sure GRU weight reordering is accurate, "
# "docs and code comments appear to give different shape orders"
# )
# W_array = onph.to_array(W)
# W_array = np.stack(
# (W_array[:, 1, :], W_array[:, 0, :], W_array[:, 2, :]), axis=1
# )
# bin_length += self.add_weight(layer, W_array, "weight_xc_data", quantize_tag, is_fp16)
# reduce U and R bias except N
# reorder num_directions-URN-hidden to num_directions-RUN-hidden
# B_array = onph.to_array(B)
# bias_data_size_g = B_array.size / 6 / num_directions
# for i in range(bias_data_size_g)[1:]:
# pass
raise RuntimeError(
"GRU not implemented yet, please report issue with model used"
)
elif op in ("HardSigmoid", "Hard Swish"):
alpha = get_node_attr_f(node, "alpha", 0.2)
beta = get_node_attr_f(node, "beta", 0.5)
layer.add_param(0, alpha)
layer.add_param(1, beta)
elif op == "ImageScaler":
bias = get_node_attr_af(node, "bias")
scale = get_node_attr_f(node, "scale", 1)
channels = bias.size
layer.add_param(0, channels)
layer.add_param(1, 1)
bin_length += self.add_weight(layer, "scale", np.array((scale,) * 3))
bin_length += self.add_weight(layer, "bias", bias)
elif op == "InstanceNormalization":
eps = get_node_attr_f(node, "epsilon", 0.00001)
# Discard affine-less S=1 B=0
affine_S = get_node_attr_from_input_af(self.weights[node.input[1]])
affine_B = get_node_attr_from_input_af(self.weights[node.input[2]])
channels = affine_S.size
if np.any(affine_S[:channels] != 1) or np.any(affine_B[:channels] != 0):
affine = 1
else:
affine = 0
layer.add_param(0, channels)
layer.add_param(1, eps)
layer.add_param(2, affine)
if affine:
scale = self.weights[node.input[1]]
B = self.weights[node.input[2]]
bin_length += self.add_weight(layer, "scale", scale)
bin_length += self.add_weight(layer, "bias", B)
elif op == "LayerNorm":
eps = get_node_attr_f(node, "epsilon", 0.00001)
affine = get_node_attr_i(node, "affine", 1)
if affine:
# discard affine-less S=1 B=0
affine_S = get_node_attr_from_input_af(self.weights[node.input[1]])
affine_B = get_node_attr_from_input_af(self.weights[node.input[2]])
affine_size = affine_S.size
if np.any(affine_S[:affine_size] != 1) or np.any(
affine_B[:affine_size]
):
affine = 1
else:
affine = 0
if affine:
layer.add_param(0, affine_size)
layer.add_param(1, eps)
layer.add_param(2, affine)
if affine:
scale = self.weights[node.input[1]]
B = self.weights[node.input[2]]
bin_length += self.add_weight(layer, "scale", scale)
bin_length += self.add_weight(layer, "bias", B)
elif op == "LeakyRelu":
alpha = get_node_attr_f(node, "alpha", 0.01)
layer.add_param(0, alpha)
elif op == "Log":
layer.add_param(0, UOT.LOG)
elif op == "LRN":
layer.add_param(0, 0)
layer.add_param(1, get_node_attr_i(node, "size", 1))
layer.add_param(2, get_node_attr_f(node, "alpha", 1))
layer.add_param(3, get_node_attr_f(node, "beta", 0.5))
layer.add_param(4, get_node_attr_f(node, "bias", 1))
elif op == "LSTM":
# W = self.weights[node.input[1]]
# R = self.weights[node.input[2]]
# B = self.weights[node.input[3]]
# hidden_size = get_node_attr_i(node, "hidden_size", 0)
# direction = get_node_attr_s(node, "direction")
# if direction == "forward":
# direction_type = GRU.FORWARD
# elif direction == "reverse":
# direction_type = GRU.REVERSE
# elif direction == "bidirectional":
# direction_type = GRU.BIDIRECTIONAL
raise RuntimeError(
"LSTM not implemented yet, please report issue with model used"
)
elif op == "MatMul":
if node.input[1] in self.weights:
# InnerProduct
B = self.weights[node.input[1]]
weight_data_size = get_tensor_proto_data_size(B, B.data_type)
num_output = B.dims[-1]
layer.add_param(0, num_output)
layer.add_param(1, 0)
layer.add_param(2, weight_data_size)
B_array = onph.to_array(B)
bin_length += self.add_weight(layer, "bias", B_array.T, DTYPE_FP32)
# There is a dead else here, not sure if this was incomplete code
elif op == "MultiHeadAttention":
# embed_dim = get_node_attr_i(node, "embed_dim", 0)
# num_heads = get_node_attr_i(node, "num_heads", 0)
# layer.add_param(0, embed_dim)
# layer.add_param(1, num_heads)
# if len(node.input) == 5:
# qkvw = self.weights[node.input[1]]
# qkvb = self.weights[node.input[2]]
# ow = self.weights[node.input[3]]
# ob = self.weights[node.input[4]]
# weight_data_size = get_tensor_proto_data_size(ow)
# layer.add_param(2, weight_data_size)
# quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32
raise RuntimeError(
"MultiHeadAttention not implemented, please report issue with model used"
)
elif op == "Neg":
layer.add_param(0, UOT.NEG)
elif op == "Normalize":
eps = get_node_attr_f(node, "eps", 0)
layer.add_param(1, 1) # channel_shared
layer.add_param(2, eps)
layer.add_param(3, 1) # scale_data_size
layer.add_param(9, NEM.PYTORCH)
bin_length += self.add_weight(layer, "scale", 1)
elif op == "Pad":
mode = get_node_attr_s(node, "mode")
value = get_node_attr_f(node, "value", 0)
if len(node.input) == 1:
pads = get_node_attr_ai(node, "pads")
else:
pads = get_node_attr_from_input_ai(self.weights[node.input[1]])
if mode == "edge":
ptype = PAT.REPLICATE
elif mode == "reflect":
ptype = PAT.REFLECT
else:
ptype = PAT.CONSTANT
pad_size = pads.size
top = bottom = front = behind = 0
if pad_size == 8:
# NCHW
top = pads[2]
bottom = pads[6]
left = pads[3]
right = pads[7]
front = pads[1]
behind = pads[5]
elif pad_size == 6:
# NHW
top = pads[1]
bottom = pads[4]
left = pads[2]
right = pads[5]
else:
# NW
left = pads[1]
right = pads[3]
layer.add_param(0, int(top))
layer.add_param(1, int(bottom))
layer.add_param(2, int(left))
layer.add_param(3, int(right))
layer.add_param(4, int(ptype))
layer.add_param(5, int(value))
layer.add_param(7, int(front))
layer.add_param(8, int(behind))
elif op == "PixelShuffle":
layer.add_param(0, get_node_attr_i(node, "scale_factor", 1))
elif op == "PRelu":
slope = self.weights[node.input[1]]
num_slope = get_tensor_proto_data_size(slope, slope.data_type)
layer.add_param(0, num_slope)
bin_length += self.add_weight(layer, "slope", slope)
elif op == "Reciprocal":
layer.add_param(0, UOT.RECIPROCAL)
elif op in [
"ReduceMax",
"ReduceMin",
"ReduceMean",
"ReduceProd",
"ReduceSum",
"ReduceSumSquare",
"ReduceL1",
"ReduceL2",
"ReduceLogSum",
"ReduceLogSumExp",
]:
if op == "ReduceSum":
op_type = ROT.SUM
elif op == "ReduceSumSquare":
op_type = ROT.SUMSQ
elif op == "ReduceMean":
op_type = ROT.MEAN
elif op == "ReduceMax":
op_type = ROT.MAX
elif op == "ReduceMin":
op_type = ROT.MIN
elif op == "ReduceProd":
op_type = ROT.PROD
elif op == "ReduceL1":
op_type = ROT.L1
elif op == "ReduceL2":
op_type = ROT.L2
elif op == "ReduceLogSum":
op_type = ROT.LOGSUM
elif op == "ReduceLogSumExp":
op_type = ROT.LOGSUMEXP
else:
op_type = -233
layer.add_param(0, op_type)
axes = get_node_attr_ai(node, "axes")
keepdims = get_node_attr_i(node, "keepdims", 1)
if axes.size > 0:
# if axes set, reduce according to axes
layer.add_param(1, 0)
for axis in axes:
if axis == 0 or axis > 4 or axis < -3:
raise ValueError(f"Unsupported axis {axis} in Reduction")
layer.add_param(
3,
[axes.size, *[a - 1 if a > 0 else a for a in axes]],
)
else:
# if axes not set, reduce all axes by default
layer.add_param(1, 1)
layer.add_param(4, keepdims)
logger.error("No NCNN documentation for Reduction param 5")
layer.add_param(5, 1)
elif op == "Reorg":
layer.add_param(0, get_node_attr_i(node, "stride", 1))
elif op == "Reshape":
if len(node.input) == 1:
shape = get_node_attr_ai(node, "shape")
else:
shape = get_node_attr_from_input_ai(self.weights[node.input[1]])
shape_size = shape.size
if shape_size == 1:
logger.error("Should never reach shape.size == 1 in Reshape")
layer.add_param(0, int(shape[0]))
elif shape_size == 2:
layer.add_param(0, int(shape[1]))
elif shape_size == 3:
layer.add_param(0, int(shape[2]))
layer.add_param(1, int(shape[1]))
elif shape_size == 4:
layer.add_param(0, int(shape[3]))
layer.add_param(1, int(shape[2]))
layer.add_param(2, int(shape[1]))
elif shape_size == 5:
layer.add_param(0, int(shape[3] * shape[3]))
layer.add_param(1, int(shape[2]))
layer.add_param(2, int(shape[1]))
elif op == "Resize":
mode = get_node_attr_s(node, "mode")
align = get_node_attr_s(node, "coordinate_transformation_mode")
if len(node.input) == 2:
# opset 10
scales = get_node_attr_from_input_af(self.weights[node.input[1]])
sizes = np.empty(0, np.int32)
else:
# opset 11+
scales = get_node_attr_from_input_af(self.weights[node.input[2]])
if len(node.input) >= 4:
sizes = get_node_attr_from_input_ai(self.weights[node.input[3]])
else:
sizes = np.empty(0, np.int32)
if mode == "linear":
resize_type = IRT.BILINEAR
elif mode == "cubic":
resize_type = IRT.BICUBIC
else:
resize_type = IRT.NEAREST
if scales.size == 0 and sizes.size == 0:
raise ValueError(
"Unsupported Resize scales and sizes are all empty."
)
if scales.size == 2:
h_scale = 1
w_scale = scales[1]
elif scales.size == 3:
h_scale = scales[1]
w_scale = scales[2]
elif scales.size == 4:
if scales[1] != 1:
raise TypeError(f"Unsupported Resize scales {scales}.")
h_scale = scales[2]
w_scale = scales[3]
else:
h_scale = 1
w_scale = 1
if sizes.size == 2:
output_height = 0
output_width = sizes[1]
elif sizes.size == 3:
output_height = sizes[1]
output_width = sizes[2]
elif sizes.size == 4:
output_height = sizes[2]
output_width = sizes[3]
else:
output_height = 0
output_width = 0
align_corner = int(align == "align_corners")
layer.add_param(0, resize_type)
layer.add_param(1, float(h_scale))
layer.add_param(2, float(w_scale))
layer.add_param(3, int(output_height))
layer.add_param(4, int(output_width))
layer.add_param(6, align_corner)
elif op == "RNN":
W = self.weights[node.input[1]]
R = self.weights[node.input[2]]
B = self.weights[node.input[3]]
hidden_size = get_node_attr_i(node, "hidden_size", 0)
direction = get_node_attr_s(node, "direction")
if direction == "reverse":
direction_type = GRU.REVERSE
elif direction == "bidirectional":
direction_type = GRU.BIDIRECTIONAL
else:
direction_type = GRU.FORWARD
weight_data_size = get_tensor_proto_data_size(W, W.data_type)
layer.add_param(0, hidden_size)
layer.add_param(1, weight_data_size)
layer.add_param(2, direction_type)
quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32
bin_length += self.add_weight(layer, "weight", W, quantize_tag)
# reduce xc and hc bias
reduced_B = np.sum(onph.to_array(B), 1)
bin_length += self.add_weight(layer, "bias", reduced_B, quantize_tag)
bin_length += self.add_weight(layer, "R", R, quantize_tag)
elif op == "ShuffleChannel":
layer.add_param(0, get_node_attr_i(node, "group", 1))
layer.add_param(1, get_node_attr_i(node, "reverse", 0))
elif op == "Sigmoid":
pass
elif op == "Sin":
layer.add_param(0, UOT.SIN)
elif op == "SkipLayerNormalization":
logger.error(f"No NCNN documentation for {op} yet, will not function")
W = self.weights[node.input[2]]
B = self.weights[node.input[3]]
B2 = self.weights[node.input[4]]
layer.add_param(0, get_tensor_proto_data_size(B, B.data_type))
quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32
bin_length += self.add_weight(layer, "weight", W, quantize_tag)
bin_length += self.add_weight(layer, "bias1", B, DTYPE_FP32)
bin_length += self.add_weight(layer, "bias2", B2, DTYPE_FP32)
elif op == "Slice":
input_size = len(node.input)
if input_size == 1:
starts = get_node_attr_ai(node, "starts")
ends = get_node_attr_ai(node, "ends")
axes = get_node_attr_ai(node, "axes")
steps = get_node_attr_ai(node, "steps")
else:
starts = get_node_attr_from_input_ai(self.weights[node.input[1]])
ends = get_node_attr_from_input_ai(self.weights[node.input[2]])
if input_size >= 4:
axes = get_node_attr_from_input_ai(self.weights[node.input[3]])
else:
axes = np.empty(0, np.int32)
if input_size >= 5:
steps = get_node_attr_from_input_ai(self.weights[node.input[4]])
else:
steps = np.empty(0, np.int32)
assert np.all(steps != 1), f"Unsupported Slice step {steps}"
# Filter out N-dim axis
if axes.size:
for i, axis in enumerate(axes):
if axis == 0:
np.delete(starts, i)
np.delete(ends, i)
np.delete(axes, i)
break
layer.add_param(9, [starts.size, *list(starts)])
layer.add_param(10, [ends.size, *list(ends)])
if axes.size:
assert np.all(
axes != 0 and axes <= 3 and axes >= -3
), f"Unsupported Slice axes {axes}"
layer.add_param(
11, [axes.size, *[a - 1 if a > 0 else a for a in axes]]
)
elif op == "Softmax":
axis = get_node_attr_i(node, "axis", 1)
layer.add_param(0, axis - 1)
layer.add_param(1, 1)
elif op == "Split":
axis = get_node_attr_i(node, "axis", 0)
splits = get_node_attr_ai(node, "split")
assert axis >= 1, f"Unsupported axis {axis} in Split"
if splits.size:
layer.add_param(0, [output_size, *list(splits[:-1]), -233])
else:
layer.add_param(
0, [output_size, *[-233 for _ in range(output_size)]]
)
layer.add_param(1, axis - 1)
elif op == "Sqrt":
layer.add_param(0, UOT.SQRT)
elif op == "Squeeze":
axes = get_node_attr_ai(node, "axes")
if axes.size:
assert np.all(
axes != 0 and axes <= 4 and axes >= -3
), f"Unsupported Squeeze axes {axes}"
layer.add_param(
3, [axes.size, *[a - 1 if a > 0 else a for a in axes]]
)
else:
layer.add_param(0, 1)
layer.add_param(1, 1)
layer.add_param(2, 1)
elif op == "Sum":
layer.add_param(0, EOT.SUM)
elif op == "Swish":
pass
elif op == "Tan":
layer.add_param(0, UOT.TAN)
elif op == "Tanh":
layer.add_param(0, UOT.TANH)
elif op == "Transpose":
perm = get_node_attr_ai(node, "perm")
if perm.size == 3:
if (perm[1] == 1 and perm[2] == 2) or (
perm[0] == 1 and perm[1] == 0 and perm[2] == 2
):
layer.add_param(0, POT.WH_WHC_WHDC)
elif (perm[1] == 2 and perm[2] == 1) or (
perm[0] == 2 and perm[1] == 0 and perm[2] == 1
):
layer.add_param(0, POT.HW_HWC_HWDC)
elif perm.size == 4:
if perm[1] == 1 and perm[2] == 2 and perm[3] == 3:
layer.add_param(0, POT.WH_WHC_WHDC)
elif perm[1] == 1 and perm[2] == 3 and perm[3] == 2:
layer.add_param(0, POT.HW_HWC_HWDC)
elif perm[1] == 2 and perm[2] == 1 and perm[3] == 3:
layer.add_param(0, POT.WCH_WDHC)
elif perm[1] == 2 and perm[2] == 3 and perm[3] == 1:
layer.add_param(0, POT.CWH_DWHC)
elif perm[1] == 3 and perm[2] == 1 and perm[3] == 2:
layer.add_param(0, POT.HCW_HDWC)
elif perm[1] == 3 and perm[2] == 2 and perm[3] == 1:
layer.add_param(0, POT.CHW_DHWC)
elif perm.size == 5:
if perm[1] == 1 and perm[2] == 2 and perm[3] == 3 and perm[4] == 4:
layer.add_param(0, POT.WH_WHC_WHDC)
elif (
perm[1] == 1 and perm[2] == 3 and perm[3] == 4 and perm[4] == 2
):
layer.add_param(0, POT.HW_HWC_HWDC)
elif (
perm[1] == 2 and perm[2] == 1 and perm[3] == 3 and perm[4] == 4
):
layer.add_param(0, POT.WCH_WDHC)
elif (
perm[1] == 2 and perm[2] == 3 and perm[3] == 4 and perm[4] == 1
):
layer.add_param(0, POT.CWH_DWHC)
elif (
perm[1] == 3 and perm[2] == 4 and perm[3] == 1 and perm[4] == 2
):
layer.add_param(0, POT.HCW_HDWC)
elif (
perm[1] == 3 and perm[2] == 4 and perm[3] == 2 and perm[4] == 1
):
layer.add_param(0, POT.CHW_DHWC)
else:
error_msg = f"Unsupported Transpose type {perm}"
raise ValueError(error_msg)
elif op == "Upsample":
mode = get_node_attr_s(node, "mode")
align = get_node_attr_s(node, "coordinate_transformation_mode")
if len(node.input) == 1:
scales = get_node_attr_af(node, "scales")
else:
scales = get_node_attr_from_input_af(self.weights[node.input[1]])
if mode in ("bilinear", "linear"):
resize_type = IRT.BILINEAR
elif mode == "trilinear":
raise ValueError("Upsample does not support trilinear mode")
else:
resize_type = IRT.NEAREST
if scales.size == 2:
h_scale = 1
w_scale = scales[1]
elif scales.size == 3:
h_scale = scales[1]
w_scale = scales[2]
elif scales.size == 4:
h_scale = scales[2]
w_scale = scales[3]
if scales[1] != 1:
error_msg = f"Unsupported Upsample scales {scales}"
raise ValueError(error_msg)
else:
error_msg = f"Unsupported Upsample scales {scales}"
raise ValueError(error_msg)
align_corner = int(align == "align_corners")
layer.add_param(0, resize_type)
layer.add_param(1, float(h_scale))
layer.add_param(2, float(w_scale))
layer.add_param(6, align_corner)
elif op == "Unsqueeze":
axes = get_node_attr_ai(node, "axes")
assert (
np.all(axes != 0) and np.all(axes <= 4) and np.all(axes >= -4)
), f"Unsupported axes {axes} in Unsqueeze"
layer.add_param(
3, [axes.size, *[axis - 1 if axis > 0 else axis for axis in axes]]
)
else:
# NCNN TODO: op specific param
# This is presumably to catch anything they haven't written an op for yet
for attr in node.attribute:
if attr.type == 1:
error_msg = f"Op {op} does not exist yet; {attr.name}={attr.f}"
elif attr.type == 2:
error_msg = f"Op {op} does not exist yet; {attr.name}={attr.i}"
elif attr.type == 3:
error_msg = f"Op {op} does not exist yet; {attr.name}={attr.s}"
else:
error_msg = (
f"Op {op} does not exist yet; {attr.name}={attr.type}"
)
raise ValueError(error_msg)
ncnn_model.add_layer(layer)
for o in range(output_size):
output_name = node.output[o]
if output_name in self.node_reference:
refcount = self.node_reference[output_name]
if refcount > 1:
ncnn_model.add_layer(
NcnnLayer(
"Split",
f"splitncnn_{internal_split}",
1,
refcount,
[output_name],
[
f"{output_name}_splitncnn_{j}"
for j in range(refcount)
],
)
)
internal_split += 1
ncnn_model.bin_length = bin_length
NcnnOptimizer(ncnn_model).optimize()
return ncnn_model
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/session.py
================================================
from __future__ import annotations
from typing import Any, Union
from weakref import WeakKeyDictionary
import onnxruntime as ort
from .model import OnnxModel
from .utils import OnnxParsedTensorShape, parse_onnx_shape
ProviderDesc = Union[str, tuple[str, dict[Any, Any]]]
def create_inference_session(
model: OnnxModel,
gpu_index: int,
execution_provider: str,
should_tensorrt_fp16: bool = False,
tensorrt_cache_path: str | None = None,
) -> ort.InferenceSession:
tensorrt: ProviderDesc = (
"TensorrtExecutionProvider",
{
"device_id": gpu_index,
"trt_engine_cache_enable": tensorrt_cache_path is not None,
"trt_engine_cache_path": tensorrt_cache_path,
"trt_fp16_enable": should_tensorrt_fp16,
},
)
cuda: ProviderDesc = (
"CUDAExecutionProvider",
{
"device_id": gpu_index,
},
)
cpu: ProviderDesc = "CPUExecutionProvider"
if execution_provider == "TensorrtExecutionProvider":
providers = [tensorrt, cuda, cpu]
elif execution_provider == "CUDAExecutionProvider":
providers = [cuda, cpu]
else:
providers = [execution_provider, cpu]
session = ort.InferenceSession(
model.bytes,
providers=providers,
)
return session
__session_cache: WeakKeyDictionary[OnnxModel, ort.InferenceSession] = (
WeakKeyDictionary()
)
def get_onnx_session(
model: OnnxModel,
gpu_index: int,
execution_provider: str,
should_tensorrt_fp16: bool,
tensorrt_cache_path: str | None = None,
) -> ort.InferenceSession:
cached = __session_cache.get(model)
if cached is None:
cached = create_inference_session(
model,
gpu_index,
execution_provider,
should_tensorrt_fp16,
tensorrt_cache_path,
)
__session_cache[model] = cached
return cached
def get_input_shape(session: ort.InferenceSession) -> OnnxParsedTensorShape:
"""
Returns the input shape, input channels, input width (optional), and input height (optional).
"""
return parse_onnx_shape(session.get_inputs()[0].shape)
def get_output_shape(session: ort.InferenceSession) -> OnnxParsedTensorShape:
"""
Returns the output shape, output channels, output width (optional), and output height (optional).
"""
return parse_onnx_shape(session.get_outputs()[0].shape)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/tensorproto_utils.py
================================================
from sys import float_info
import numpy as np
from onnx import numpy_helper as onph
from onnx.onnx_pb import AttributeProto, NodeProto, TensorProto
from sanic.log import logger
INT64_MIN, INT64_MAX = np.iinfo(np.int64).min, np.iinfo(np.int64).max
FLOAT32_MAX = float_info.max
APT = AttributeProto
TPT = TensorProto
def get_node_attr_ai(node: NodeProto, key: str) -> np.ndarray:
for attr in node.attribute:
if attr.name == key:
return np.array(
[max(min(i, INT64_MAX), INT64_MIN) for i in attr.ints], np.int64
)
return np.empty(0, np.int32)
def set_node_attr_ai(node: NodeProto, key: str, value: np.ndarray) -> None:
attr_group = AttributeProto(name=key, floats=value, type=APT.INTS)
node.attribute.append(attr_group)
def get_node_attr_af(node: NodeProto, key: str) -> np.ndarray:
for attr in node.attribute:
if attr.name == key:
return np.array(list(attr.floats), np.float32)
return np.empty(0, np.float32)
def get_node_attr_i(node: NodeProto, key: str, default: int = 0) -> int:
for attr in node.attribute:
if attr.name == key:
return max(min(attr.i, INT64_MAX), INT64_MIN)
return default
def get_node_attr_f(node: NodeProto, key: str, default: float = 0) -> float:
for attr in node.attribute:
if attr.name == key:
return attr.f
return default
def get_node_attr_s(node: NodeProto, key: str, default: str = ""):
for attr in node.attribute:
if attr.name == key:
return attr.s.decode("ascii")
return default
def get_node_attr_tensor(node: NodeProto, key: str) -> TensorProto:
for attr in node.attribute:
if attr.name == key:
return attr.t
return TensorProto()
def get_node_attr_from_input_f(tp: TensorProto) -> float:
shape_data = onph.to_array(tp)
if tp.data_type in (TPT.FLOAT, TPT.FLOAT16, TPT.DOUBLE, TPT.INT32):
f = shape_data.item(0)
elif tp.data_type == TPT.INT64:
f = max(min(shape_data.item(0), INT64_MAX), INT64_MIN)
else:
raise TypeError(f"Unknown data type {tp.data_type}")
return f
def get_node_attr_from_input_ai(tp: TensorProto) -> np.ndarray:
if tp.data_type in (TPT.INT32, TPT.INT64):
shape_data = onph.to_array(tp)
if shape_data.size == 1:
shape_data = np.array([shape_data.item(0)], shape_data.dtype)
return np.array(
[
max(min(val, INT64_MAX), INT64_MIN)
if tp.data_type == TPT.INT64
else val
for val in shape_data
],
shape_data.dtype,
)
else:
logger.error(f"Unknown data type {tp.data_type}")
return np.empty(0, np.int32)
def get_node_attr_from_input_af(tp: TensorProto) -> np.ndarray:
if tp.data_type in (TPT.FLOAT, TPT.FLOAT16, TPT.DOUBLE):
shape_data = onph.to_array(tp)
return np.array(list(shape_data), shape_data.dtype)
else:
logger.error(f"Unknown data type {tp.data_type}")
return np.empty(0, np.float32)
def get_tensor_proto_data_size(tp: TensorProto, fpmode: int = TPT.FLOAT) -> int:
if tp.raw_data:
if fpmode == TPT.FLOAT16:
return len(tp.raw_data) // 2
return len(tp.raw_data) // 4
elif tp.data_type in (TPT.FLOAT, TPT.FLOAT16):
return len(tp.float_data)
return 0
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/update_model_dims.py
================================================
# Copyright (c) ONNX Project Contributors
# SPDX-License-Identifier: Apache-2.0
#
# Include in chaiNNer with the following changes:
# - Remove `dim_proto.dim_value != dim` for constant dimensions
# - Improved type hints
from __future__ import annotations
from collections.abc import Sequence
import onnx.checker
from onnx import ModelProto, ValueInfoProto
def update_inputs_outputs_dims(
model: ModelProto,
input_dims: dict[str, Sequence[str | int]],
output_dims: dict[str, Sequence[str | int]],
) -> ModelProto:
"""This function updates the dimension sizes of the model's inputs and outputs to the values
provided in input_dims and output_dims. if the dim value provided is negative, a unique dim_param
will be set for that dimension.
Example. if we have the following shape for inputs and outputs:
* shape(input_1) = ('b', 3, 'w', 'h')
* shape(input_2) = ('b', 4)
* shape(output) = ('b', 'd', 5)
The parameters can be provided as:
::
input_dims = {
"input_1": ['b', 3, 'w', 'h'],
"input_2": ['b', 4],
}
output_dims = {
"output": ['b', -1, 5]
}
Putting it together:
::
model = onnx.load('model.onnx')
updated_model = update_inputs_outputs_dims(model, input_dims, output_dims)
onnx.save(updated_model, 'model.onnx')
"""
dim_param_set: set[str] = set()
def init_dim_param_set(
dim_param_set: set[str], value_infos: list[ValueInfoProto]
) -> None:
for info in value_infos:
shape = info.type.tensor_type.shape
for dim in shape.dim:
if dim.HasField("dim_param"):
dim_param_set.add(dim.dim_param) # type: ignore
init_dim_param_set(dim_param_set, model.graph.input) # type: ignore
init_dim_param_set(dim_param_set, model.graph.output) # type: ignore
init_dim_param_set(dim_param_set, model.graph.value_info) # type: ignore
def update_dim(tensor: ValueInfoProto, dim: str | int, j: int, name: str) -> None:
dim_proto = tensor.type.tensor_type.shape.dim[j]
if isinstance(dim, int):
if dim >= 0:
dim_proto.dim_value = dim
else:
generated_dim_param = name + "_" + str(j)
if generated_dim_param in dim_param_set:
raise ValueError(
f"Unable to generate unique dim_param for axis {j} of {name}. Please manually provide a dim_param value."
)
dim_proto.dim_param = generated_dim_param
else:
dim_proto.dim_param = dim
for input_ in model.graph.input:
input_name = input_.name
input_dim_arr = input_dims[input_name]
for j, dim in enumerate(input_dim_arr):
update_dim(input_, dim, j, input_name)
for output in model.graph.output:
output_name = output.name
output_dim_arr = output_dims[output_name]
for j, dim in enumerate(output_dim_arr):
update_dim(output, dim, j, output_name)
onnx.checker.check_model(model)
return model
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/utils.py
================================================
from __future__ import annotations
from typing import Any, Literal
import onnx
from onnx.onnx_pb import ModelProto, ValueInfoProto
from .update_model_dims import update_inputs_outputs_dims
OnnxTensorFormat = Literal["BCHW", "BHWC"]
OnnxTensorShape = tuple[
int | str,
int | str,
int | str,
int | str,
]
OnnxParsedTensorShape = tuple[
OnnxTensorFormat,
int,
int | None,
int | None,
]
"""
The elements are:
- The tensor format (BCHW or BHWC).
- The number of channels.
- The width (optional).
- The height (optional).
"""
def _as_int(value: object) -> int | None:
if isinstance(value, int):
return value
return None
def _or_else(value: int | None, default: int) -> int:
return value if value is not None else default
def parse_onnx_shape(shape: OnnxTensorShape) -> OnnxParsedTensorShape:
if isinstance(shape[1], int) and shape[1] <= 4:
return "BCHW", shape[1], _as_int(shape[3]), _as_int(shape[2])
elif isinstance(shape[3], int) and shape[3] <= 4:
return "BHWC", shape[3], _as_int(shape[2]), _as_int(shape[1])
else:
return "BCHW", 3, _as_int(shape[3]), _as_int(shape[2])
def to_onnx_tensor_shape(
tensor: onnx.TypeProto.Tensor,
) -> OnnxTensorShape:
shape = tuple(
dim.dim_param if dim.HasField("dim_param") else dim.dim_value
for dim in tensor.shape.dim
)
if len(shape) != 4:
raise ValueError(f"Expected 4 dimensions, got {len(shape)}")
return shape
def is_tensor_input(input: ValueInfoProto) -> bool:
return input.type.HasField("tensor_type")
def is_image_to_image(model: ModelProto) -> bool:
"""
Returns whether the model is an image to image model (single image input -> single image output).
"""
if len(model.graph.input) != 1 or len(model.graph.output) != 1:
return False
i = model.graph.input[0]
o = model.graph.output[0]
return is_tensor_input(i) and is_tensor_input(o)
class ModelShapeInference:
def __init__(self, model: ModelProto) -> None:
self.model = model
i = model.graph.input[0]
o = model.graph.output[0]
if not is_tensor_input(i) or not is_tensor_input(o):
raise ValueError("Expected tensor inputs and outputs")
# modify model to have a fixed input size
self.input_shape = to_onnx_tensor_shape(i.type.tensor_type)
self.output_shape = to_onnx_tensor_shape(o.type.tensor_type)
parsed_input_shape = parse_onnx_shape(self.input_shape)
self.tensor_format = parsed_input_shape[0]
self.input_channels = parsed_input_shape[1]
self.fixed_input_width = parsed_input_shape[2]
self.fixed_input_height = parsed_input_shape[3]
self.output_channels = _as_int(
self.output_shape[1]
if self.tensor_format == "BCHW"
else self.output_shape[3]
)
def infer_shape(
self, input_size: tuple[int, int]
) -> tuple[
tuple[int | None, int | None, int | None],
tuple[int | None, int | None, int | None],
]:
"""
input_shape: The size of the input tensors as width, height.
return: The shapes of the input and output tensors in HWC format.
**This will mutate the model.**
"""
b = _or_else(_as_int(self.input_shape[0]), 1)
c = self.input_channels
h = _or_else(self.fixed_input_height, input_size[1])
w = _or_else(self.fixed_input_width, input_size[0])
new_inputs: list[Any]
if self.tensor_format == "BCHW":
new_inputs = [b, c, h, w]
elif self.tensor_format == "BHWC":
new_inputs = [b, h, w, c]
else:
raise ValueError(f"Unknown tensor format: {self.tensor_format}")
i = self.model.graph.input[0]
o = self.model.graph.output[0]
update_inputs_outputs_dims(
self.model,
{i.name: new_inputs},
{o.name: list(self.output_shape)},
)
# infer the output shape using the fixed input size
inferred_model = onnx.shape_inference.infer_shapes(self.model, strict_mode=True)
i = inferred_model.graph.input[0]
o = inferred_model.graph.output[0]
input_shape = to_onnx_tensor_shape(i.type.tensor_type)
output_shape = to_onnx_tensor_shape(o.type.tensor_type)
# output in HWC format
input_shape = input_shape[1:]
output_shape = output_shape[1:]
if self.tensor_format == "BCHW":
input_shape = input_shape[::-1]
output_shape = output_shape[::-1]
input_shape = tuple(_as_int(dim) for dim in input_shape)
output_shape = tuple(_as_int(dim) for dim in output_shape)
assert len(input_shape) == 3
assert len(output_shape) == 3
return input_shape[0:3], output_shape
def get_tensor_fp_datatype(model: ModelProto) -> str:
for item in [*model.graph.input, *model.graph.output]:
if item.type.HasField("tensor_type"):
tensor = item.type.tensor_type
if tensor.elem_type == onnx.TensorProto.FLOAT16:
return "fp16"
if tensor.elem_type == onnx.TensorProto.FLOAT:
return "fp32"
if tensor.elem_type == onnx.TensorProto.DOUBLE:
return "fp64"
if tensor.elem_type == onnx.TensorProto.BFLOAT16:
return "bf16"
return "fp32"
def get_opset(model: onnx.ModelProto) -> int:
for opset in model.opset_import:
if opset.domain == "":
return opset.version
return -1
def safely_optimize_onnx_model(model_proto: ModelProto) -> ModelProto:
"""
Optimizes the model using onnxoptimizer. If onnxoptimizer is not installed, the model is returned as is.
"""
try:
import onnxoptimizer
passes = onnxoptimizer.get_fuse_and_elimination_passes()
model_proto = onnxoptimizer.optimize(model_proto, passes)
except Exception:
pass
return model_proto
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/pil_utils.py
================================================
from __future__ import annotations
from enum import Enum
import numpy as np
from PIL import Image
from ..utils.utils import get_h_w_c
from .image_utils import FillColor, convert_to_bgra, normalize, to_uint8
class InterpolationMethod(Enum):
AUTO = -1
NEAREST = 0
LANCZOS = 1
LINEAR = 2
CUBIC = 3
BOX = 4
class RotationInterpolationMethod(Enum):
CUBIC = InterpolationMethod.CUBIC.value
LINEAR = InterpolationMethod.LINEAR.value
NEAREST = InterpolationMethod.NEAREST.value
@property
def interpolation_method(self) -> InterpolationMethod:
return InterpolationMethod(self.value)
INTERPOLATION_METHODS_MAP = {
InterpolationMethod.NEAREST: Image.NEAREST,
InterpolationMethod.BOX: Image.BOX,
InterpolationMethod.LINEAR: Image.BILINEAR,
InterpolationMethod.CUBIC: Image.BICUBIC,
InterpolationMethod.LANCZOS: Image.LANCZOS,
}
class RotateSizeChange(Enum):
EXPAND = 1
CROP = 0
def resize(
img: np.ndarray, out_dims: tuple[int, int], interpolation: InterpolationMethod
) -> np.ndarray:
"""Perform PIL resize"""
if interpolation == InterpolationMethod.AUTO:
# automatically chose a method that works
new_w, new_h = out_dims
old_h, old_w, _ = get_h_w_c(img)
if new_w > old_w or new_h > old_h:
interpolation = InterpolationMethod.LANCZOS
else:
interpolation = InterpolationMethod.BOX
resample = INTERPOLATION_METHODS_MAP[interpolation]
pimg = Image.fromarray(to_uint8(img, normalized=True))
pimg = pimg.resize(out_dims, resample=resample) # type: ignore
return normalize(np.array(pimg))
def rotate(
img: np.ndarray,
angle: float,
interpolation: RotationInterpolationMethod,
expand: RotateSizeChange,
fill: FillColor,
) -> np.ndarray:
"""Perform PIL rotate"""
c = get_h_w_c(img)[2]
if fill == FillColor.TRANSPARENT:
img = convert_to_bgra(img, c)
fill_color = tuple([x * 255 for x in fill.get_color(c)])
resample = INTERPOLATION_METHODS_MAP[interpolation.interpolation_method]
pimg = Image.fromarray(to_uint8(img, normalized=True))
pimg = pimg.rotate(
angle,
resample=resample, # type: ignore
expand=bool(expand.value),
fillcolor=fill_color, # type: ignore
)
return normalize(np.array(pimg))
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/__init__.py
================================================
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/auto_split.py
================================================
from __future__ import annotations
import gc
import numpy as np
import torch
from accelerator_detection import get_autocast_device_type, is_device_type_supported_for_autocast
from nodes.utils.utils import get_h_w_c
from spandrel import ImageModelDescriptor
from api import Progress
from ..upscale.auto_split import Split, Tiler, auto_split
from .utils import safe_accelerator_cache_empty
def _into_standard_image_form(t: torch.Tensor) -> torch.Tensor:
if len(t.shape) == 2:
# (H, W)
return t
elif len(t.shape) == 3:
# (C, H, W) -> (H, W, C)
return t.permute(1, 2, 0)
elif len(t.shape) == 4:
# (1, C, H, W) -> (H, W, C)
return t.squeeze(0).permute(1, 2, 0)
else:
raise ValueError("Unsupported output tensor shape")
def _into_batched_form(t: torch.Tensor) -> torch.Tensor:
if len(t.shape) == 2:
# (H, W) -> (1, 1, H, W)
return t.unsqueeze(0).unsqueeze(0)
elif len(t.shape) == 3:
# (H, W, C) -> (1, C, H, W)
return t.permute(2, 0, 1).unsqueeze(0)
else:
raise ValueError("Unsupported input tensor shape")
def _rgb_to_bgr(t: torch.Tensor) -> torch.Tensor:
if len(t.shape) == 3 and t.shape[2] == 3:
# (H, W, C) RGB -> BGR
return t.flip(2)
elif len(t.shape) == 3 and t.shape[2] == 4:
# (H, W, C) RGBA -> BGRA
return torch.cat((t[:, :, 2:3], t[:, :, 1:2], t[:, :, 0:1], t[:, :, 3:4]), 2)
else:
return t
def _into_tensor(
img: np.ndarray, device: torch.device, dtype: torch.dtype
) -> torch.Tensor:
img = np.ascontiguousarray(img)
writeable = img.flags.writeable
try:
if not writeable and device == torch.device("cpu"):
img = np.copy(img)
else:
# since we are going to copy the image to the GPU, we can skip the copy here
try:
img.flags.writeable = True
except Exception:
# Some arrays cannot be made writeable, and we need to copy them
img = np.copy(img)
if device == torch.device("cpu"):
input_tensor = (
torch.from_numpy(img).to(device, dtype, non_blocking=True)
)
else:
input_tensor = (
torch.from_numpy(img).pin_memory().to(device, dtype, non_blocking=True)
)
return input_tensor
finally:
img.flags.writeable = writeable
@torch.inference_mode()
def pytorch_auto_split(
img: np.ndarray,
model: ImageModelDescriptor[torch.nn.Module],
device: torch.device,
use_fp16: bool,
tiler: Tiler,
progress: Progress,
) -> np.ndarray:
dtype = torch.float32
if use_fp16:
if model.supports_half:
dtype = torch.float16
elif torch.cuda.is_bf16_supported():
dtype = torch.bfloat16
# print("dtype", dtype, use_fp16, flush=True)
if model.dtype != dtype or model.device != device:
# print("move model", flush=True)
model = model.to(device, dtype, memory_format=torch.channels_last)
def upscale(img: np.ndarray, _: object):
progress.check_aborted()
if progress.paused:
# clear resources before pausing
gc.collect()
safe_cuda_cache_empty()
progress.suspend()
input_tensor = None
try:
_, _, input_channels = get_h_w_c(img)
# convert to tensor
input_tensor = _into_tensor(img, device, dtype)
# expand grayscale tensor to match model input channels
if input_channels == 1 and model.input_channels > 1:
input_tensor = input_tensor.repeat(1, 1, model.input_channels)
input_tensor = _into_batched_form(input_tensor)
input_tensor = input_tensor.to(
memory_format=torch.channels_last
) # TODO refactor
# inference with accelerator-aware autocast
autocast_device_type = get_autocast_device_type(device)
autocast_enabled = is_device_type_supported_for_autocast(device) and use_fp16
with torch.autocast(device_type=autocast_device_type, dtype=dtype, enabled=autocast_enabled):
output_tensor = model(input_tensor)
# convert back to numpy
output_tensor = _into_standard_image_form(output_tensor)
if input_channels == 1:
output_tensor = output_tensor[:, :, 0].unsqueeze(-1)
# print("out dtype", output_tensor.dtype, flush=True)
# result = output_tensor.detach().cpu().detach().float().numpy()
result = output_tensor.detach().cpu().detach()
if result.dtype == torch.bfloat16:
result = result.float()
result = result.numpy()
return result
except RuntimeError as e:
# Check to see if its actually an out of memory error
if "allocate" in str(e) or "CUDA" in str(e) or "out of memory" in str(e).lower():
# Collect garbage (clear memory)
if input_tensor is not None:
try:
input_tensor.detach().cpu()
except Exception:
pass
del input_tensor
gc.collect()
safe_accelerator_cache_empty(device)
return Split()
else:
# Re-raise the exception if not an OOM error
raise
return auto_split(img, upscale, tiler)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/convert_to_onnx_impl.py
================================================
from io import BytesIO
import torch
from spandrel import ImageModelDescriptor, ModelDescriptor
from spandrel.architectures.CRAFT import CRAFT
from spandrel.architectures.SAFMN import SAFMN
from spandrel.architectures.SCUNet import SCUNet
def is_onnx_supported(model: ModelDescriptor) -> bool:
return not isinstance(model.model, SCUNet | SAFMN | CRAFT)
def convert_to_onnx_impl(
model: ModelDescriptor,
device: torch.device,
use_half: bool = False,
input_name: str = "input",
output_name: str = "output",
opset_version: int = 14,
) -> bytes:
# https://github.com/onnx/onnx/issues/654
dynamic_axes = {
input_name: {0: "batch_size", 2: "height", 3: "width"},
output_name: {0: "batch_size", 2: "height", 3: "width"},
}
size = 3
size += model.size_requirements.get_padding(size, size)[0]
dummy_input = torch.rand(1, model.input_channels, size, size)
dummy_input = dummy_input.to(device)
if use_half:
if not model.supports_half:
raise ValueError(
f"Model of arch {model.architecture} does not support half precision."
)
model.half()
dummy_input = dummy_input.half()
else:
model.float()
dummy_input = dummy_input.float()
m = model.model
if isinstance(model, ImageModelDescriptor):
class FakeModel(torch.nn.Module):
def __init__(self, model: ImageModelDescriptor) -> None:
super().__init__()
self.model = model
def forward(self, x: torch.Tensor):
return self.model(x)
m = FakeModel(model)
with BytesIO() as f:
torch.onnx.export(
m,
dummy_input,
f,
opset_version=opset_version,
verbose=False,
input_names=[input_name],
output_names=[output_name],
dynamic_axes=dynamic_axes,
do_constant_folding=True,
)
f.seek(0)
return f.read()
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/pix_transform/LICENSE
================================================
MIT License
Copyright (c) 2019 Riccardo de Lutio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/pix_transform/auto_split.py
================================================
from __future__ import annotations
import gc
import numpy as np
import torch
from ....utils.utils import Region, Size, get_h_w_c
from ...image_op import to_op
from ...upscale.auto_split import Split, auto_split
from ...upscale.grayscale import SplitMode, grayscale_split
from ...upscale.passthrough import passthrough_single_color
from ...upscale.tiler import Tiler
from ..utils import safe_accelerator_cache_empty
from .pix_transform import Params, pix_transform
class _PixTiler(Tiler):
def __init__(self, max_tile_size: int = 2048) -> None:
self.max_tile_size: int = max_tile_size
def allow_smaller_tile_size(self) -> bool:
return False
def starting_tile_size(self, width: int, height: int, channels: int) -> Size:
square = min(width, height, self.max_tile_size)
return square, square
def split(self, tile_size: Size) -> Size:
# half the tile size plus a bit extra to account for overlap
size = tile_size[0] // 2 + tile_size[0] // 8
if size < 16:
raise ValueError("Cannot split any further.")
return size, size
def _as_3d(img: np.ndarray) -> np.ndarray:
if img.ndim == 3:
return img
return np.expand_dims(img, axis=2)
def pix_transform_auto_split(
source: np.ndarray,
guide: np.ndarray,
device: torch.device,
params: Params,
split_mode: SplitMode = SplitMode.LAB,
) -> np.ndarray:
"""
Automatically splits the source and guide image into segments that can be processed by PixTransform.
The source and guide image may have any number of channels and any size, also long as the size of the guide image is a whole number (greater than 1) multiple of the size of the source image.
"""
s_w, s_h, _ = get_h_w_c(source)
g_w, g_h, _ = get_h_w_c(guide)
assert (
g_h > s_h and g_w > s_w
), "The guide image mus be larger than the source image."
assert (
g_w / s_w == g_w // s_w and g_w / s_w == g_h / s_h
), "The size of the guide image must be an integer multiple of the size of the source image (e.g. 2x, 3x, 4x, ...)."
tiler = _PixTiler()
scale = g_w // s_w
def upscale(tile: np.ndarray, region: Region):
try:
tile_guide = region.scale(scale).read_from(guide)
pix_op = to_op(pix_transform)(
guide_img=np.transpose(_as_3d(tile_guide), (2, 0, 1)),
device=device,
params=params,
)
# passthrough single colors to speed up alpha channels
pass_op = to_op(passthrough_single_color)(scale, pix_op)
return grayscale_split(tile, pass_op, split_mode)
except RuntimeError as e:
# Check to see if its actually an out of memory error
if "allocate" in str(e) or "CUDA" in str(e) or "out of memory" in str(e).lower():
# Collect garbage (clear memory)
gc.collect()
safe_accelerator_cache_empty(device)
# Retry once with smaller tiles
return Split()
else:
# Re-raise the exception if not an OOM error
raise
return auto_split(source, upscale, tiler)
def _run_pix_transform():
with torch.no_grad():
safe_accelerator_cache_empty(device)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/pix_transform/pix_transform.py
================================================
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
import numpy as np
import torch
import torch.utils.data
from torch import optim
from .pix_transform_net import PixTransformNet
@dataclass
class Params:
spatial_features_input: bool = True
# spatial color head
weights_regularizer: tuple[float, float, float] | None = (0.0001, 0.001, 0.001)
loss: Literal["mse", "l1"] = "l1"
lr: float = 0.001
batch_size: int = 32
iteration: int = 32 * 1024
def pix_transform(
source_img: np.ndarray,
guide_img: np.ndarray,
device: torch.device,
params: Params,
) -> np.ndarray:
if len(guide_img.shape) < 3:
guide_img = np.expand_dims(guide_img, 0)
_n_channels, hr_height, hr_width = guide_img.shape
source_img = source_img.squeeze()
lr_height, lr_width = source_img.shape
assert hr_height == hr_width
assert lr_height == lr_width
assert hr_height % lr_height == 0
d = hr_height // lr_height
m = lr_height
_n = hr_height
# normalize guide and source
guide_img = (
guide_img - np.mean(guide_img, axis=(1, 2), keepdims=True)
) / np.maximum(0.0001, np.std(guide_img, axis=(1, 2), keepdims=True))
source_img_mean = np.mean(source_img)
source_img_std = np.std(source_img)
source_img = (source_img - source_img_mean) / np.maximum(0.0001, source_img_std)
if params.spatial_features_input:
x = np.linspace(-0.5, 0.5, hr_width)
x_grid, y_grid = np.meshgrid(x, x, indexing="ij")
x_grid = np.expand_dims(x_grid, axis=0)
y_grid = np.expand_dims(y_grid, axis=0)
guide_img = np.concatenate([guide_img, x_grid, y_grid], axis=0)
#### prepare_patches #########################################################################
# guide_patches is M^2 x C x D x D
# source_pixels is M^2 x 1
guide_tensor = torch.from_numpy(guide_img).float().to(device)
source_tensor = torch.from_numpy(source_img).float().to(device)
guide_patches = torch.zeros((m * m, guide_tensor.shape[0], d, d)).to(device)
source_pixels = torch.zeros((m * m, 1)).to(device)
for i in range(m):
for j in range(m):
guide_patches[j + i * m, :, :, :] = guide_tensor[
:, i * d : (i + 1) * d, j * d : (j + 1) * d
]
source_pixels[j + i * m] = source_tensor[i : (i + 1), j : (j + 1)]
train_data = torch.utils.data.TensorDataset(guide_patches, source_pixels)
train_loader = torch.utils.data.DataLoader(
train_data, batch_size=params.batch_size, shuffle=True
)
###############################################################################################
#### setup network ############################################################################
mynet = (
PixTransformNet(
channels_in=guide_tensor.shape[0],
weights_regularizer=params.weights_regularizer,
)
.train()
.to(device)
)
optimizer = optim.Adam(mynet.params_with_regularizer, lr=params.lr)
if params.loss == "mse":
myloss = torch.nn.MSELoss()
elif params.loss == "l1":
myloss = torch.nn.L1Loss()
else:
raise AssertionError("unknown loss!")
###############################################################################################
epochs = params.batch_size * params.iteration // (m * m)
for _epoch in range(epochs):
for x, y in train_loader:
optimizer.zero_grad()
y_pred = mynet(x)
y_mean_pred = torch.mean(y_pred, dim=[2, 3])
source_patch_consistency = myloss(y_mean_pred, y)
source_patch_consistency.backward()
optimizer.step()
# compute final prediction, un-normalize, and back to numpy
mynet.eval()
predicted_target_img = mynet(guide_tensor.unsqueeze(0)).squeeze()
predicted_target_img = source_img_mean + source_img_std * predicted_target_img
predicted_target_img = predicted_target_img.cpu().detach().squeeze().numpy()
return predicted_target_img
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/pix_transform/pix_transform_net.py
================================================
from __future__ import annotations
from torch import nn
class PixTransformNet(nn.Module):
def __init__(
self,
channels_in: int = 5,
kernel_size: int = 1,
weights_regularizer: tuple[float, float, float] | None = None,
) -> None:
super().__init__()
self.channels_in = channels_in
self.spatial_net = nn.Sequential(
nn.Conv2d(2, 32, (1, 1), padding=0),
nn.ReLU(),
nn.Conv2d(
32, 2048, (kernel_size, kernel_size), padding=(kernel_size - 1) // 2
),
)
self.color_net = nn.Sequential(
nn.Conv2d(channels_in - 2, 32, (1, 1), padding=0),
nn.ReLU(),
nn.Conv2d(
32, 2048, (kernel_size, kernel_size), padding=(kernel_size - 1) // 2
),
)
self.head_net = nn.Sequential(
nn.ReLU(),
nn.Conv2d(
2048, 32, (kernel_size, kernel_size), padding=(kernel_size - 1) // 2
),
nn.ReLU(),
nn.Conv2d(32, 1, (1, 1), padding=0),
)
if weights_regularizer is None:
reg_spatial = 0.0001
reg_color = 0.001
reg_head = 0.0001
else:
reg_spatial = weights_regularizer[0]
reg_color = weights_regularizer[1]
reg_head = weights_regularizer[2]
self.params_with_regularizer = []
self.params_with_regularizer += [
{"params": self.spatial_net.parameters(), "weight_decay": reg_spatial}
]
self.params_with_regularizer += [
{"params": self.color_net.parameters(), "weight_decay": reg_color}
]
self.params_with_regularizer += [
{"params": self.head_net.parameters(), "weight_decay": reg_head}
]
def forward(self, input_): # noqa: ANN001
input_spatial = input_[:, self.channels_in - 2 :, :, :]
input_color = input_[:, 0 : self.channels_in - 2, :, :]
merged_features = self.spatial_net(input_spatial) + self.color_net(input_color)
return self.head_net(merged_features)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/rife/IFNet_HDv3_v4_14_align.py
================================================
# type: ignore
# Original Rife Frame Interpolation by hzwer
# https://github.com/megvii-research/ECCV2022-RIFE
# https://github.com/hzwer/Practical-RIFE
# Modifications to use Rife for Image Alignment by tepete/pifroggi ('Enhance Everything!' Discord Server)
# Additional helpful github issues
# https://github.com/megvii-research/ECCV2022-RIFE/issues/278
# https://github.com/megvii-research/ECCV2022-RIFE/issues/344
import torch
import torch.nn.functional as F # noqa: N812
from torch import nn
from torchvision import transforms
from .warplayer import warp
def conv(in_planes, out_planes, kernel_size=3, stride=1, padding=1, dilation=1): # noqa: ANN001
return nn.Sequential(
nn.Conv2d(
in_planes,
out_planes,
kernel_size=kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
bias=True,
),
nn.LeakyReLU(0.2, True),
)
def conv_bn(in_planes, out_planes, kernel_size=3, stride=1, padding=1, dilation=1): # noqa: ANN001
return nn.Sequential(
nn.Conv2d(
in_planes,
out_planes,
kernel_size=kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
bias=False,
),
nn.BatchNorm2d(out_planes),
nn.LeakyReLU(0.2, True),
)
class Head(nn.Module):
def __init__(self) -> None:
super().__init__()
self.cnn0 = nn.Conv2d(3, 32, 3, 2, 1)
self.cnn1 = nn.Conv2d(32, 32, 3, 1, 1)
self.cnn2 = nn.Conv2d(32, 32, 3, 1, 1)
self.cnn3 = nn.ConvTranspose2d(32, 8, 4, 2, 1)
self.relu = nn.LeakyReLU(0.2, True)
def forward(self, x, feat=False): # noqa: ANN001
x0 = self.cnn0(x)
x = self.relu(x0)
x1 = self.cnn1(x)
x = self.relu(x1)
x2 = self.cnn2(x)
x = self.relu(x2)
x3 = self.cnn3(x)
if feat:
return [x0, x1, x2, x3]
return x3
class ResConv(nn.Module):
def __init__(self, c, dilation=1) -> None: # noqa: ANN001
super().__init__()
self.conv = nn.Conv2d(c, c, 3, 1, dilation, dilation=dilation, groups=1)
self.beta = nn.Parameter(torch.ones((1, c, 1, 1)), requires_grad=True)
self.relu = nn.LeakyReLU(0.2, True)
def forward(self, x): # noqa: ANN001
return self.relu(self.conv(x) * self.beta + x)
class IFBlock(nn.Module):
def __init__(self, in_planes, c=64) -> None: # noqa: ANN001
super().__init__()
self.conv0 = nn.Sequential(
conv(in_planes, c // 2, 3, 2, 1),
conv(c // 2, c, 3, 2, 1),
)
self.convblock = nn.Sequential(
ResConv(c),
ResConv(c),
ResConv(c),
ResConv(c),
ResConv(c),
ResConv(c),
ResConv(c),
ResConv(c),
)
self.lastconv = nn.Sequential(
nn.ConvTranspose2d(c, 4 * 6, 4, 2, 1), nn.PixelShuffle(2)
)
def forward(self, x, flow=None, scale=1): # noqa: ANN001
x = F.interpolate(
x, scale_factor=1.0 / scale, mode="bilinear", align_corners=False
)
if flow is not None:
flow = (
F.interpolate(
flow, scale_factor=1.0 / scale, mode="bilinear", align_corners=False
)
* 1.0
/ scale
)
x = torch.cat((x, flow), 1)
feat = self.conv0(x)
feat = self.convblock(feat)
tmp = self.lastconv(feat)
tmp = F.interpolate(
tmp, scale_factor=scale, mode="bilinear", align_corners=False
)
flow = tmp[:, :4] * scale
mask = tmp[:, 4:5]
return flow, mask
class IFNet(nn.Module):
def __init__(self) -> None:
super().__init__()
self.block0 = IFBlock(7 + 16, c=192)
self.block1 = IFBlock(8 + 4 + 16, c=128)
self.block2 = IFBlock(8 + 4 + 16, c=96)
self.block3 = IFBlock(8 + 4 + 16, c=64)
self.encode = Head()
def align_images(
self,
img0, # noqa: ANN001
img1, # noqa: ANN001
timestep, # noqa: ANN001
scale_list, # noqa: ANN001
blur_strength, # noqa: ANN001
ensemble, # noqa: ANN001
device, # noqa: ANN001
):
# optional blur
if blur_strength is not None and blur_strength > 0:
blur = transforms.GaussianBlur(
kernel_size=(5, 5), sigma=(blur_strength, blur_strength)
)
img0_blurred = blur(img0)
img1_blurred = blur(img1)
else:
img0_blurred = img0
img1_blurred = img1
f0 = self.encode(img0_blurred[:, :3])
f1 = self.encode(img1_blurred[:, :3])
flow_list = []
mask_list = []
flow = None
mask = None
block = [self.block0, self.block1, self.block2, self.block3]
for i in range(4):
if flow is None:
flow, mask = block[i](
torch.cat(
(img0_blurred[:, :3], img1_blurred[:, :3], f0, f1, timestep), 1
),
None,
scale=scale_list[i],
)
if ensemble:
f_, m_ = block[i](
torch.cat(
(
img1_blurred[:, :3],
img0_blurred[:, :3],
f1,
f0,
1 - timestep,
),
1,
),
None,
scale=scale_list[i],
)
flow = (flow + torch.cat((f_[:, 2:4], f_[:, :2]), 1)) / 2
mask = (mask + (-m_)) / 2
else:
wf0 = warp(f0, flow[:, :2], device)
wf1 = warp(f1, flow[:, 2:4], device)
fd, m0 = block[i](
torch.cat(
(
img0_blurred[:, :3],
img1_blurred[:, :3],
wf0,
wf1,
timestep,
mask,
),
1,
),
flow,
scale=scale_list[i],
)
if ensemble:
f_, m_ = block[i](
torch.cat(
(
img1_blurred[:, :3],
img0_blurred[:, :3],
wf1,
wf0,
1 - timestep,
-mask,
),
1,
),
torch.cat((flow[:, 2:4], flow[:, :2]), 1),
scale=scale_list[i],
)
fd = (fd + torch.cat((f_[:, 2:4], f_[:, :2]), 1)) / 2
mask = (m0 + (-m_)) / 2
else:
mask = m0
flow = flow + fd
mask_list.append(mask)
flow_list.append(flow)
# apply warp to original image
aligned_img0 = warp(img0, flow_list[-1][:, :2], device)
# add clamp here instead of in warplayer script, as it changes the output there
aligned_img0 = aligned_img0.clamp(min=0.0, max=1.0)
return aligned_img0, flow_list[-1]
def forward(
self,
x, # noqa: ANN001
timestep=1, # noqa: ANN001
training=False, # noqa: ANN001
fastmode=True, # noqa: ANN001
ensemble=True, # noqa: ANN001
num_iterations=1, # noqa: ANN001
multiplier=0.5, # noqa: ANN001
blur_strength=0, # noqa: ANN001
device="cuda", # noqa: ANN001
):
if not training:
channel = x.shape[1] // 2
img0 = x[:, :channel]
img1 = x[:, channel:]
scale_list = [multiplier * 8, multiplier * 4, multiplier * 2, multiplier]
if not torch.is_tensor(timestep):
timestep = (x[:, :1].clone() * 0 + 1) * timestep
else:
timestep = timestep.repeat(1, 1, img0.shape[2], img0.shape[3]) # type: ignore
for _iteration in range(num_iterations):
aligned_img0, flow = self.align_images(
img0, img1, timestep, scale_list, blur_strength, ensemble, device
)
img0 = aligned_img0 # use the aligned image as img0 for the next iteration
return aligned_img0, flow
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/rife/warplayer.py
================================================
# type: ignore
import torch
backwarp_tenGrid = {} # noqa: N816
def warp(tenInput, tenFlow, device): # noqa: ANN001, N803
k = (str(tenFlow.device), str(tenFlow.size()))
if k not in backwarp_tenGrid:
tenHorizontal = ( # noqa: N806
torch.linspace(-1.0, 1.0, tenFlow.shape[3], device=device)
.view(1, 1, 1, tenFlow.shape[3])
.expand(tenFlow.shape[0], -1, tenFlow.shape[2], -1)
)
tenVertical = ( # noqa: N806
torch.linspace(-1.0, 1.0, tenFlow.shape[2], device=device)
.view(1, 1, tenFlow.shape[2], 1)
.expand(tenFlow.shape[0], -1, -1, tenFlow.shape[3])
)
backwarp_tenGrid[k] = torch.cat([tenHorizontal, tenVertical], 1).to(device)
tenFlow = torch.cat( # noqa: N806
[
tenFlow[:, 0:1, :, :] / ((tenInput.shape[3] - 1.0) / 2.0),
tenFlow[:, 1:2, :, :] / ((tenInput.shape[2] - 1.0) / 2.0),
],
1,
)
g = (backwarp_tenGrid[k] + tenFlow).permute(0, 2, 3, 1)
tenOutput = torch.nn.functional.grid_sample(
input=tenInput,
grid=g,
mode="bicubic",
padding_mode="border",
align_corners=True,
)
return tenOutput
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/utils.py
================================================
from __future__ import annotations
import numpy as np
import torch
from torch import Tensor
from ..image_utils import as_3d
from ..onnx.np_tensor_utils import MAX_VALUES_BY_DTYPE, np_denorm
def bgr_to_rgb(image: Tensor) -> Tensor:
# flip image channels
# https://github.com/pytorch/pytorch/issues/229
out: Tensor = image.flip(-3)
# RGB to BGR #may be faster:
# out: Tensor = image[[2, 1, 0], :, :]
return out
def rgb_to_bgr(image: Tensor) -> Tensor:
# same operation as bgr_to_rgb(), flip image channels
return bgr_to_rgb(image)
def bgra_to_rgba(image: Tensor) -> Tensor:
out: Tensor = image[[2, 1, 0, 3], :, :]
return out
def rgba_to_bgra(image: Tensor) -> Tensor:
# same operation as bgra_to_rgba(), flip image channels
return bgra_to_rgba(image)
def norm(x: Tensor):
"""Normalize (z-norm) from [0,1] range to [-1,1]"""
out = (x - 0.5) * 2.0
return out.clamp(-1, 1)
def np2tensor(
img: np.ndarray,
bgr2rgb: bool = True,
data_range: float = 1.0,
normalize: bool = False,
change_range: bool = True,
add_batch: bool = True,
) -> Tensor:
"""Converts a numpy image array into a Tensor array.
Parameters:
img (numpy array): the input image numpy array
add_batch (bool): choose if new tensor needs batch dimension added
"""
# check how many channels the image has, then condition. ie. RGB, RGBA, Gray
# if bgr2rgb:
# img = img[
# :, :, [2, 1, 0]
# ] # BGR to RGB -> in numpy, if using OpenCV, else not needed. Only if image has colors.
if change_range:
dtype = img.dtype
maxval = MAX_VALUES_BY_DTYPE.get(dtype.name, 1.0)
t_dtype = np.dtype("float32")
img = img.astype(t_dtype) / maxval # ie: uint8 = /255
# "HWC to CHW" and "numpy to tensor"
tensor = torch.from_numpy(
np.ascontiguousarray(np.transpose(as_3d(img), (2, 0, 1)))
).float()
if bgr2rgb:
# BGR to RGB -> in tensor, if using OpenCV, else not needed. Only if image has colors.)
if tensor.shape[0] % 3 == 0:
# RGB or MultixRGB (3xRGB, 5xRGB, etc. For video tensors.)
tensor = bgr_to_rgb(tensor)
elif tensor.shape[0] == 4:
# RGBA
tensor = bgra_to_rgba(tensor)
if add_batch:
# Add fake batch dimension = 1 . squeeze() will remove the dimensions of size 1
tensor.unsqueeze_(0)
if normalize:
tensor = norm(tensor)
return tensor
def tensor2np(
img: Tensor,
rgb2bgr: bool = True,
remove_batch: bool = True,
data_range: float = 255,
denormalize: bool = False,
change_range: bool = True,
imtype: type = np.uint8,
) -> np.ndarray:
"""Converts a Tensor array into a numpy image array.
Parameters:
img (tensor): the input image tensor array
4D(B,(3/1),H,W), 3D(C,H,W), or 2D(H,W), any range, RGB channel order
remove_batch (bool): choose if tensor of shape BCHW needs to be squeezed
denormalize (bool): Used to denormalize from [-1,1] range back to [0,1]
imtype (type): the desired type of the converted numpy array (np.uint8
default)
Output:
img (np array): 3D(H,W,C) or 2D(H,W), [0,255], np.uint8 (default)
"""
n_dim = img.dim()
# TODO: Check: could denormalize here in tensor form instead, but end result is the same
img = img.float().cpu()
img_np: np.ndarray
if n_dim in (4, 3):
# if n_dim == 4, has to convert to 3 dimensions
if n_dim == 4 and remove_batch:
# remove a fake batch dimension
img = img.squeeze(dim=0)
if img.shape[0] == 3 and rgb2bgr: # RGB
# RGB to BGR -> in tensor, if using OpenCV, else not needed. Only if image has colors.
img_np = rgb_to_bgr(img).numpy()
elif img.shape[0] == 4 and rgb2bgr: # RGBA
# RGBA to BGRA -> in tensor, if using OpenCV, else not needed. Only if image has colors.
img_np = rgba_to_bgra(img).numpy()
else:
img_np = img.numpy()
img_np = np.transpose(img_np, (1, 2, 0)) # CHW to HWC
elif n_dim == 2:
img_np = img.numpy()
else:
raise TypeError(
f"Only support 4D, 3D and 2D tensor. But received with dimension: {n_dim:d}"
)
# if rgb2bgr:
# img_np = img_np[[2, 1, 0], :, :] #RGB to BGR -> in numpy, if using OpenCV, else not needed. Only if image has colors.
# TODO: Check: could denormalize in the begining in tensor form instead
if denormalize:
img_np = np_denorm(img_np) # denormalize if needed
if change_range:
img_np = np.clip(
data_range * img_np, 0, data_range
).round() # np.clip to the data_range
# has to be in range (0,255) before changing to np.uint8, else np.float32
return img_np.astype(imtype)
def safe_cuda_cache_empty() -> None:
"""
Empties the CUDA cache if CUDA is available. Hopefully without causing any errors.
DEPRECATED: Use safe_accelerator_cache_empty() instead for better accelerator support.
"""
try:
if torch.cuda.is_available():
torch.cuda.empty_cache()
except Exception:
pass
def safe_accelerator_cache_empty(device: torch.device) -> None:
"""
Empties the accelerator cache for the given device type.
Supports CUDA, ROCm, XPU, MPS, and other accelerators.
"""
try:
device_type = device.type
if device_type in ["cuda", "rocm"]: # ROCm uses CUDA API
if torch.cuda.is_available():
torch.cuda.empty_cache()
elif device_type == "xpu":
if hasattr(torch, 'xpu') and torch.xpu.is_available():
torch.xpu.empty_cache()
elif device_type == "mps":
if (hasattr(torch, 'backends') and
hasattr(torch.backends, 'mps') and
torch.backends.mps.is_available()):
# MPS doesn't have an explicit empty_cache, but we can try
# to trigger garbage collection
import gc
gc.collect()
# For other device types, we just do garbage collection
# since they typically don't have specific cache clearing APIs
else:
import gc
gc.collect()
except Exception:
# Fallback to garbage collection if anything fails
import gc
gc.collect()
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/resize.py
================================================
from __future__ import annotations
from enum import Enum
import numpy as np
from chainner_ext import ResizeFilter as NativeResizeFilter
from chainner_ext import resize as native_resize
from ..utils.utils import get_h_w_c
class ResizeFilter(Enum):
AUTO = -1
NEAREST = 0
BOX = 4
LINEAR = 2
CATROM = 3
LANCZOS = 1
HERMITE = 5
MITCHELL = 6
BSPLINE = 7
HAMMING = 8
HANN = 9
LAGRANGE = 10
GAUSS = 11
_FILTER_MAP: dict[ResizeFilter, NativeResizeFilter] = {
ResizeFilter.NEAREST: NativeResizeFilter.Nearest,
ResizeFilter.BOX: NativeResizeFilter.Box,
ResizeFilter.LINEAR: NativeResizeFilter.Linear,
ResizeFilter.CATROM: NativeResizeFilter.CubicCatrom,
ResizeFilter.LANCZOS: NativeResizeFilter.Lanczos,
ResizeFilter.HERMITE: NativeResizeFilter.Hermite,
ResizeFilter.MITCHELL: NativeResizeFilter.CubicMitchell,
ResizeFilter.BSPLINE: NativeResizeFilter.CubicBSpline,
ResizeFilter.HAMMING: NativeResizeFilter.Hamming,
ResizeFilter.HANN: NativeResizeFilter.Hann,
ResizeFilter.LAGRANGE: NativeResizeFilter.Lagrange,
ResizeFilter.GAUSS: NativeResizeFilter.Gauss,
}
def resize(
img: np.ndarray,
out_dims: tuple[int, int],
filter: ResizeFilter,
separate_alpha: bool = False,
gamma_correction: bool = False,
) -> np.ndarray:
h, w, c = get_h_w_c(img)
new_w, new_h = out_dims
# check memory
GB: int = 2**30 # noqa: N806
MAX_MEMORY = 16 * GB # noqa: N806
new_memory = new_w * new_h * c * 4
if new_memory > MAX_MEMORY:
raise RuntimeError(
f"Resize would require {round(new_memory / GB, 3)} GB of memory, but only {MAX_MEMORY//GB} GB are allowed."
)
if filter == ResizeFilter.AUTO:
# automatically chose a method that works
if new_w > w or new_h > h:
filter = ResizeFilter.LANCZOS
else:
filter = ResizeFilter.BOX
if (w, h) == out_dims and (filter in (ResizeFilter.NEAREST, ResizeFilter.BOX)):
# no resize needed
return img.copy()
if filter == ResizeFilter.NEAREST:
# we don't need premultiplied alpha for NN
separate_alpha = True
native_filter = _FILTER_MAP[filter]
if not separate_alpha and c == 4:
# pre-multiply alpha
img = img.copy()
img[:, :, 0] *= img[..., 3]
img[:, :, 1] *= img[..., 3]
img[:, :, 2] *= img[..., 3]
img = native_resize(img, out_dims, native_filter, gamma_correction)
# native_resize guarantees that the output is float32 in the range [0, 1]
# so no need to normalize
if not separate_alpha and c == 4:
# undo pre-multiply alpha
alpha_r = 1 / np.maximum(img[..., 3], 0.0001)
img[:, :, 0] *= alpha_r
img[:, :, 1] *= alpha_r
img[:, :, 2] *= alpha_r
np.minimum(img, 1, out=img)
return img
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/__init__.py
================================================
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/auto_split.py
================================================
from __future__ import annotations
import math
from collections.abc import Callable
import numpy as np
from sanic.log import logger
from ...utils.utils import Region, Size, get_h_w_c
from .exact_split import exact_split
from .tile_blending import BlendDirection, TileBlender, TileOverlap, half_sin_blend_fn
from .tiler import Tiler
class Split:
pass
SplitImageOp = Callable[[np.ndarray, Region], np.ndarray | Split]
def auto_split(
img: np.ndarray,
upscale: SplitImageOp,
tiler: Tiler,
overlap: int = 16,
) -> np.ndarray:
"""
Splits the image into tiles according to the given tiler.
This method only changes the size of the given image, the tiles passed into the upscale function will have same number of channels.
The region passed into the upscale function is the region of the current tile.
The size of the region is guaranteed to be the same as the size of the given tile.
## Padding
If the given tiler allows smaller tile sizes, then it is guaranteed that no padding will be added.
Otherwise, no padding is only guaranteed if the starting tile size is not larger than the size of the given image.
"""
h, w, c = get_h_w_c(img)
split = _max_split if tiler.allow_smaller_tile_size() else _exact_split
return split(
img,
upscale=upscale,
starting_tile_size=tiler.starting_tile_size(w, h, c),
split_tile_size=tiler.split,
overlap=overlap,
)
class _SplitEx(Exception):
pass
def _exact_split(
img: np.ndarray,
upscale: SplitImageOp,
starting_tile_size: Size,
split_tile_size: Callable[[Size], Size],
overlap: int,
) -> np.ndarray:
h, w, c = get_h_w_c(img)
logger.debug(
f"Exact size split image ({w}x{h}px @ {c}) with exact tile size {starting_tile_size[0]}x{starting_tile_size[1]}px."
)
def no_split_upscale(i: np.ndarray, r: Region) -> np.ndarray:
result = upscale(i, r)
if isinstance(result, Split):
raise _SplitEx
return result
MAX_ITER = 20 # noqa: N806
for _ in range(MAX_ITER):
try:
max_overlap = min(*starting_tile_size) // 4
return exact_split(
img=img,
exact_size=starting_tile_size,
upscale=no_split_upscale,
overlap=min(max_overlap, overlap),
)
except _SplitEx:
starting_tile_size = split_tile_size(starting_tile_size)
raise ValueError(f"Aborting after {MAX_ITER} splits. Unable to upscale image.")
def _max_split(
img: np.ndarray,
upscale: SplitImageOp,
starting_tile_size: Size,
split_tile_size: Callable[[Size], Size],
overlap: int,
) -> np.ndarray:
"""
Splits the image into tiles with at most the given tile size.
If the upscale method requests a split, then the tile size will be lowered.
"""
h, w, c = get_h_w_c(img)
img_region = Region(0, 0, w, h)
max_tile_size = starting_tile_size
logger.debug(
f"Auto split image ({w}x{h}px @ {c}) with initial tile size {max_tile_size}."
)
if w <= max_tile_size[0] and h <= max_tile_size[1]:
# the image might be small enough so that we don't have to split at all
upscale_result = upscale(img, img_region)
if not isinstance(upscale_result, Split):
return upscale_result
# the image was too large
max_tile_size = split_tile_size(max_tile_size)
logger.warn(
f"Unable to upscale the whole image at once. Reduced tile size to {max_tile_size}."
)
# The upscale method is allowed to request splits at any time.
# When a split occurs, we have to "restart" the loop and
# this variable allow us to split the already processed tiles.
start_y = 0
# To allocate the result image, we need to know the upscale factor first,
# and we only get to know this factor after the first successful upscale.
result: TileBlender | None = None
scale: int = 0
out_channels: int = 0
restart = True
while restart:
restart = False
# This is a bit complex.
# We don't actually use the current tile size to partition the image.
# If we did, then tile_size=1024 and w=1200 would result in very uneven tiles.
# Instead, we use tile_size to calculate how many tiles we get in the x and y direction
# and then calculate the optimal tile size for the x and y direction using the counts.
# This yields optimal tile sizes which should prevent unnecessary splitting.
tile_count_x = math.ceil(w / max_tile_size[0])
tile_count_y = math.ceil(h / max_tile_size[1])
tile_size_x = math.ceil(w / tile_count_x)
tile_size_y = math.ceil(h / tile_count_y)
logger.debug(
f"Currently {tile_count_x}x{tile_count_y} tiles each {tile_size_x}x{tile_size_y}px."
)
prev_row_result: TileBlender | None = None
for y in range(tile_count_y):
if y < start_y:
continue
row_result: TileBlender | None = None
row_overlap: TileOverlap | None = None
for x in range(tile_count_x):
tile = Region(
x * tile_size_x, y * tile_size_y, tile_size_x, tile_size_y
).intersect(img_region)
pad = img_region.child_padding(tile).min(overlap)
padded_tile = tile.add_padding(pad)
upscale_result = upscale(padded_tile.read_from(img), padded_tile)
if isinstance(upscale_result, Split):
max_tile_size = split_tile_size(max_tile_size)
new_tile_count_y = math.ceil(h / max_tile_size[1])
new_tile_size_y = math.ceil(h / new_tile_count_y)
start_y = (y * tile_size_x) // new_tile_size_y
logger.debug(
f"Split occurred. New tile size is {max_tile_size}. Starting at row {start_y}."
)
# reset result
if result is not None:
# we already added at least one row, so we have to set the offset back
result.offset = start_y * new_tile_size_y
restart = True
break
# figure out by how much the image was upscaled by
up_h, up_w, up_c = get_h_w_c(upscale_result)
current_scale = up_h // padded_tile.height
assert current_scale > 0
assert padded_tile.height * current_scale == up_h
assert padded_tile.width * current_scale == up_w
if row_result is None:
# allocate the result image
scale = current_scale
out_channels = up_c
row_result = TileBlender(
width=w * scale,
height=padded_tile.height * scale,
channels=out_channels,
direction=BlendDirection.X,
blend_fn=half_sin_blend_fn,
_prev=prev_row_result,
)
prev_row_result = row_result
row_overlap = TileOverlap(pad.top * scale, pad.bottom * scale)
assert current_scale == scale
# add to row
row_result.add_tile(
upscale_result, TileOverlap(pad.left * scale, pad.right * scale)
)
if restart:
break
assert row_result is not None
assert row_overlap is not None
if result is None:
result = TileBlender(
width=w * scale,
height=h * scale,
channels=out_channels,
direction=BlendDirection.Y,
blend_fn=half_sin_blend_fn,
)
# add row
result.add_tile(row_result.get_result(), row_overlap)
assert result is not None
return result.get_result()
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/auto_split_tiles.py
================================================
from collections.abc import Callable
from typing import NewType
import numpy as np
from sanic.log import logger
from ...utils.utils import get_h_w_c
from .tiler import MaxTileSize, NoTiling, Tiler
GB_AMT = 1024**3
def estimate_tile_size(
budget: int,
model_size: int,
img: np.ndarray,
img_element_size: int = 4,
) -> int:
h, w, c = get_h_w_c(img)
img_bytes = h * w * c * img_element_size
mem_required_estimation = (model_size / (1024 * 52)) * img_bytes
tile_pixels = w * h * budget / mem_required_estimation
# the largest power-of-2 tile_size such that tile_size**2 < tile_pixels
tile_size = 2 ** (int(tile_pixels**0.5).bit_length() - 1)
# tile_size = int(tile_pixels**0.5) // 16 * 16
required_mem = f"{mem_required_estimation/GB_AMT:.2f}"
budget_mem = f"{budget/GB_AMT:.2f}"
logger.debug(
f"Estimating memory required: {required_mem} GB, {budget_mem} GB free."
f" Estimated tile size: {tile_size}, tile_pixels = {tile_pixels}",
)
return tile_size
TileSize = NewType("TileSize", int)
ESTIMATE = TileSize(0)
NO_TILING = TileSize(-1)
MAX_TILE_SIZE = TileSize(-2)
CUSTOM = TileSize(-3)
TILE_SIZE_256 = TileSize(256)
def parse_tile_size_input(tile_size: TileSize, estimate: Callable[[], Tiler]) -> Tiler:
if tile_size == 0:
return estimate()
if tile_size == -1:
return NoTiling()
if tile_size == -2:
return MaxTileSize()
assert tile_size > 0
return MaxTileSize(tile_size)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/basic_upscale.py
================================================
import math
from dataclasses import dataclass
from enum import Enum
import numpy as np
from nodes.impl.image_op import ImageOp
from nodes.impl.image_utils import BorderType
from nodes.impl.resize import ResizeFilter, resize
from nodes.utils.utils import get_h_w_c
from .convenient_upscale import convenient_upscale
@dataclass
class UpscaleInfo:
in_nc: int
out_nc: int
scale: int
@property
def supports_custom_scale(self) -> bool:
return self.scale != 1 and self.in_nc == self.out_nc
class PaddingType(Enum):
NONE = 0
REFLECT_MIRROR = 1
WRAP = 2
REPLICATE = 3
def to_border_type(self) -> BorderType:
if self == PaddingType.NONE:
raise ValueError(
"PaddingType.NONE does not have a corresponding BorderType"
)
elif self == PaddingType.REFLECT_MIRROR:
return BorderType.REFLECT_MIRROR
elif self == PaddingType.WRAP:
return BorderType.WRAP
elif self == PaddingType.REPLICATE:
return BorderType.REPLICATE
raise ValueError(f"Unknown padding type: {self}")
PAD_SIZE = 16
def _custom_scale_upscale(
img: np.ndarray,
upscale: ImageOp,
natural_scale: int,
custom_scale: int,
separate_alpha: bool,
) -> np.ndarray:
if custom_scale == natural_scale:
return upscale(img)
# number of iterations we need to do to reach the desired scale
# e.g. if the model is 2x and the desired scale is 13x, we need to do 4 iterations
iterations = max(1, math.ceil(math.log(custom_scale, natural_scale)))
org_h, org_w, _ = get_h_w_c(img)
for _ in range(iterations):
img = upscale(img)
# resize, if necessary
target_size = (
org_w * custom_scale,
org_h * custom_scale,
)
h, w, _ = get_h_w_c(img)
if (w, h) != target_size:
img = resize(
img,
target_size,
ResizeFilter.BOX,
separate_alpha=separate_alpha,
)
return img
def basic_upscale(
img: np.ndarray,
upscale: ImageOp,
upscale_info: UpscaleInfo,
scale: int,
separate_alpha: bool,
clip: bool = True,
):
def inner_upscale(img: np.ndarray) -> np.ndarray:
return convenient_upscale(
img,
upscale_info.in_nc,
upscale_info.out_nc,
upscale,
separate_alpha,
clip=clip,
)
if not upscale_info.supports_custom_scale and scale != upscale_info.scale:
raise ValueError(
f"Upscale info does not support custom scale: {upscale_info}, scale: {scale}"
)
img = _custom_scale_upscale(
img,
inner_upscale,
natural_scale=upscale_info.scale,
custom_scale=scale,
separate_alpha=separate_alpha,
)
return img
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/convenient_upscale.py
================================================
from __future__ import annotations
import numpy as np
from ...utils.utils import get_h_w_c
from ..image_op import ImageOp, clipped
from ..image_utils import as_target_channels
def with_black_and_white_backgrounds(img: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
c = get_h_w_c(img)[2]
assert c == 4
black = np.copy(img[:, :, :3])
white = np.copy(img[:, :, :3])
for c in range(3):
black[:, :, c] *= img[:, :, 3]
white[:, :, c] = (white[:, :, c] - 1) * img[:, :, 3] + 1
return black, white
def denoise_and_flatten_alpha(img: np.ndarray) -> np.ndarray:
alpha_min = np.min(img, axis=2)
alpha_max = np.max(img, axis=2)
alpha_mean = np.mean(img, axis=2)
alpha = alpha_max * alpha_mean + alpha_min * (1 - alpha_mean)
return alpha.clip(0, 1)
def convenient_upscale(
img: np.ndarray,
model_in_nc: int,
model_out_nc: int,
upscale: ImageOp,
separate_alpha: bool = False,
clip: bool = True,
) -> np.ndarray:
"""
Upscales the given image in an intuitive/convenient way.
This method guarantees that the `upscale` function will be called with an image with
`model_in_nc` number of channels.
Additionally, guarantees that the number of channels of the output image will match
that of the input image in cases where `model_in_nc` == `model_out_nc`, and match
`model_out_nc` otherwise.
"""
in_img_c = get_h_w_c(img)[2]
if clip:
upscale = clipped(upscale)
if model_in_nc != model_out_nc:
return upscale(as_target_channels(img, model_in_nc, True))
if in_img_c == model_in_nc:
return upscale(img)
if in_img_c == 4:
# Ignore alpha if single-color or not being replaced
unique = np.unique(img[:, :, 3])
if len(unique) == 1:
rgb = as_target_channels(
upscale(as_target_channels(img[:, :, :3], model_in_nc, True)), 3, True
)
unique_alpha = np.full(rgb.shape[:-1], unique[0], np.float32)
return np.dstack((rgb, unique_alpha))
if separate_alpha:
# Upscale the RGB channels and alpha channel separately
rgb = as_target_channels(
upscale(as_target_channels(img[:, :, :3], model_in_nc, True)), 3, True
)
alpha = denoise_and_flatten_alpha(
upscale(as_target_channels(img[:, :, 3], model_in_nc, True))
)
return np.dstack((rgb, alpha))
else:
# Transparency hack (white/black background difference alpha)
black, white = with_black_and_white_backgrounds(img)
black_up = as_target_channels(
upscale(as_target_channels(black, model_in_nc, True)), 3, True
)
white_up = as_target_channels(
upscale(as_target_channels(white, model_in_nc, True)), 3, True
)
# Interpolate between the alpha values to get a more defined alpha
alpha_candidates = 1 - (white_up - black_up) # type: ignore
alpha = denoise_and_flatten_alpha(alpha_candidates)
return np.dstack((black_up, alpha))
# skip all conversions for grayscale to improve performance by reducing the amount of data that needs to be copied
# instead we do the color conversions on the tensors after they're already on the gpu
if in_img_c == 1:
if img.ndim == 2:
img = np.expand_dims(img, axis=-1)
return upscale(img)
return as_target_channels(
upscale(as_target_channels(img, model_in_nc, True)), in_img_c, True
)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/custom_scale.py
================================================
import math
import numpy as np
from nodes.impl.image_op import ImageOp
from nodes.impl.resize import ResizeFilter, resize
from nodes.utils.utils import get_h_w_c
def custom_scale_upscale(
img: np.ndarray,
upscale: ImageOp,
natural_scale: int,
custom_scale: int,
separate_alpha: bool,
) -> np.ndarray:
if custom_scale == natural_scale:
return upscale(img)
# number of iterations we need to do to reach the desired scale
# e.g. if the model is 2x and the desired scale is 13x, we need to do 4 iterations
iterations = max(1, math.ceil(math.log(custom_scale, natural_scale)))
org_h, org_w, _ = get_h_w_c(img)
for _ in range(iterations):
img = upscale(img)
# resize, if necessary
target_size = (
org_w * custom_scale,
org_h * custom_scale,
)
h, w, _ = get_h_w_c(img)
if (w, h) != target_size:
img = resize(
img,
target_size,
ResizeFilter.BOX,
separate_alpha=separate_alpha,
)
return img
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/exact_split.py
================================================
from __future__ import annotations
import math
from collections.abc import Callable
from dataclasses import dataclass
import numpy as np
from sanic.log import logger
from ...utils.utils import Padding, Region, Size, get_h_w_c
from ..image_utils import BorderType, create_border
from .tile_blending import BlendDirection, TileBlender, TileOverlap, half_sin_blend_fn
def _pad_image(img: np.ndarray, min_size: Size):
h, w, _ = get_h_w_c(img)
min_w, min_h = min_size
x = max(0, min_w - w) / 2
y = max(0, min_h - h) / 2
padding = Padding(math.floor(y), math.floor(x), math.ceil(y), math.ceil(x))
return create_border(img, BorderType.REFLECT_MIRROR, padding), padding
@dataclass
class _Segment:
start: int
end: int
start_padding: int
end_padding: int
@property
def length(self) -> int:
return self.end - self.start
@property
def padded_length(self) -> int:
return self.end + self.end_padding - (self.start - self.start_padding)
def _exact_split_into_segments(length: int, exact: int, overlap: int) -> list[_Segment]:
"""
Splits the given length into segments of `exact` (padded) length.
Segments will overlap into each other with at least the given overlap.
"""
if length == exact:
# trivial
return [_Segment(0, exact, 0, 0)]
assert length > exact
assert exact > overlap * 2
result: list[_Segment] = []
def add(s: _Segment) -> None:
assert s.padded_length == exact
result.append(s)
# The current strategy is to go from left to right and to align segments
# such that we use the least overlap possible. The last segment will then
# be the smallest with potentially a lot of overlap.
# While this is easy to implement, it's actually not ideal. Ideally, we
# would want for the overlap to be distributed evenly between segments.
# However, this is complex to implement and the current method also works.
# we know that the first segment looks like this
add(_Segment(0, exact - overlap, 0, overlap))
while result[-1].end < length:
start_padding = overlap
start = result[-1].end
end = start + exact - overlap * 2
end_padding = overlap
if end + end_padding >= length:
# last segment
end_padding = 0
end = length
start_padding = exact - (end - start)
add(_Segment(start, end, start_padding, end_padding))
return result
def _exact_split_into_regions(
w: int,
h: int,
exact_w: int,
exact_h: int,
overlap: int,
) -> list[list[tuple[Region, Padding]]]:
"""
Returns a list of disjoint regions along with padding.
Each region plus its padding is guaranteed to have the given exact size.
The padding (if not zero) is guaranteed to be at least the given overlap value.
"""
# we can split x and y independently from each other and then combine the results
x_segments = _exact_split_into_segments(w, exact_w, overlap)
y_segments = _exact_split_into_segments(h, exact_h, overlap)
logger.info(
f"Image is split into {len(x_segments)}x{len(y_segments)} tiles each exactly {exact_w}x{exact_h}px."
)
result: list[list[tuple[Region, Padding]]] = []
for y in y_segments:
row: list[tuple[Region, Padding]] = []
for x in x_segments:
row.append(
(
Region(x.start, y.start, x.length, y.length),
Padding(
y.start_padding, x.end_padding, y.end_padding, x.start_padding
),
)
)
result.append(row)
return result
def _exact_split_without_padding(
img: np.ndarray,
exact_size: Size,
upscale: Callable[[np.ndarray, Region], np.ndarray],
overlap: int,
) -> np.ndarray:
h, w, _ = get_h_w_c(img)
exact_w, exact_h = exact_size
assert w >= exact_w and h >= exact_h
if (w, h) == exact_size:
return upscale(img, Region(0, 0, w, h))
# To allocate the result image, we need to know the upscale factor first,
# and we only get to know this factor after the first successful upscale.
result: TileBlender | None = None
scale: int = 0
out_channels: int = 0
regions = _exact_split_into_regions(w, h, exact_w, exact_h, overlap)
for row in regions:
row_result: TileBlender | None = None
row_overlap: TileOverlap | None = None
for tile, pad in row:
padded_tile = tile.add_padding(pad)
assert padded_tile.size == exact_size
upscale_result = upscale(padded_tile.read_from(img), padded_tile)
# figure out by how much the image was upscaled by
up_h, up_w, up_c = get_h_w_c(upscale_result)
current_scale = up_h // exact_h
assert current_scale > 0
assert exact_h * current_scale == up_h
assert exact_w * current_scale == up_w
if row_result is None:
# allocate the result image
scale = current_scale
out_channels = up_c
row_result = TileBlender(
width=w * scale,
height=exact_h * scale,
channels=out_channels,
direction=BlendDirection.X,
blend_fn=half_sin_blend_fn,
)
row_overlap = TileOverlap(pad.top * scale, pad.bottom * scale)
assert current_scale == scale
row_result.add_tile(
upscale_result, TileOverlap(pad.left * scale, pad.right * scale)
)
assert row_result is not None
assert row_overlap is not None
if result is None:
result = TileBlender(
width=w * scale,
height=h * scale,
channels=out_channels,
direction=BlendDirection.Y,
blend_fn=half_sin_blend_fn,
)
result.add_tile(row_result.get_result(), row_overlap)
assert result is not None
# remove initially added padding
return result.get_result()
def exact_split(
img: np.ndarray,
exact_size: Size,
upscale: Callable[[np.ndarray, Region], np.ndarray],
overlap: int = 16,
) -> np.ndarray:
"""
Splits the image into tiles with exactly the given tile size.
If the image is smaller than the given size, then it will be padded.
"""
# ensure that the image is at least as large as the given size
img, base_padding = _pad_image(img, exact_size)
h, w, _ = get_h_w_c(img)
result = _exact_split_without_padding(img, exact_size, upscale, overlap)
scale = get_h_w_c(result)[0] // h
if base_padding.empty:
return result
# remove initially added padding
return (
Region(0, 0, w, h).remove_padding(base_padding).scale(scale).read_from(result)
)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/grayscale.py
================================================
from __future__ import annotations
from enum import Enum
import numpy as np
from ..color.convert import convert
from ..color.convert_data import LAB, RGB
from ..image_op import ImageOp
class SplitMode(Enum):
RGB = 1
LAB = 2
def split(self, img: np.ndarray) -> list[np.ndarray]:
if img.ndim == 2:
return [img]
assert img.ndim == 3
c = img.shape[2]
if c == 1:
return [img[:, :, 0]]
if self == SplitMode.RGB:
return [img[:, :, channel] for channel in range(c)]
elif self == SplitMode.LAB:
if c < 3:
return [img[:, :, channel] for channel in range(c)]
lab = convert(img[:, :, 0:3], RGB, LAB)
remaining_channels = [img[:, :, channel] for channel in range(3, c)]
return [
lab[:, :, 0],
lab[:, :, 1],
lab[:, :, 2],
*remaining_channels,
]
else:
raise AssertionError()
def combine(self, channels: list[np.ndarray]) -> np.ndarray:
l = len(channels)
assert l > 0
if l == 1:
return channels[0]
if self == SplitMode.RGB:
return np.dstack(channels)
elif self == SplitMode.LAB:
if l < 3:
return np.dstack(channels)
rgb = convert(np.dstack(channels[0:3]), LAB, RGB)
if l == 3:
return rgb
return np.dstack([rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2], *channels[3:]])
else:
raise AssertionError()
def grayscale_split(
img: np.ndarray, process: ImageOp, mode: SplitMode = SplitMode.RGB
) -> np.ndarray:
"""
This function guarantees that the given image operation method will be called with 2D single-channel images.
The images passed into the operation are guaranteed to have the same size as the given image.
"""
input_channels = mode.split(img)
output_channels: list[np.ndarray] = []
for channel in input_channels:
output_channels.append(process(channel))
return mode.combine(output_channels)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/passthrough.py
================================================
import numpy as np
from ...utils.utils import get_h_w_c
from ..image_op import ImageOp
def passthrough_single_color(img: np.ndarray, scale: int, op: ImageOp) -> np.ndarray:
"""
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.
Obviously, this optimization is only correct if `op` doesn't change the color of single-color images.
To make this a transparent optimization, it is important that `scale` is correct.
`scale` must be the same factor by which `op` changes the dimension of the image.
"""
h, w, c = get_h_w_c(img)
if c == 1:
unique_list = np.unique(img)
if len(unique_list) == 1:
return np.full((h * scale, w * scale), unique_list[0], np.float32)
else:
unique_values = []
is_unique = True
for channel in range(c):
unique_list = np.unique(img[:, :, channel])
if len(unique_list) == 1:
unique_values.append(unique_list[0])
else:
is_unique = False
break
if is_unique:
channels = [
np.full((h * scale, w * scale), unique_values[channel], np.float32)
for channel in range(c)
]
return np.dstack(channels)
return op(img)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/tile_blending.py
================================================
from __future__ import annotations
import math
from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
import numpy as np
from nodes.utils.utils import get_h_w_c
def sin_blend_fn(x: np.ndarray) -> np.ndarray:
return (np.sin(x * math.pi - math.pi / 2) + 1) / 2
def half_sin_blend_fn(i: np.ndarray) -> np.ndarray:
# only use half the overlap
i = np.clip(i * 2 - 0.5, 0, 1)
return sin_blend_fn(i)
class BlendDirection(Enum):
X = 0
Y = 1
@dataclass(frozen=True)
class TileOverlap:
start: int
end: int
@property
def total(self) -> int:
return self.start + self.end
def _fast_mix(a: np.ndarray, b: np.ndarray, blend: np.ndarray) -> np.ndarray:
"""
Returns `a * (1 - blend) + b * blend`
"""
# a * (1 - blend) + b * blend
# a - a * blend + b * blend
r = b * blend
r += a
r -= a * blend # type: ignore
return r
class TileBlender:
def __init__(
self,
width: int,
height: int,
channels: int,
direction: BlendDirection,
blend_fn: Callable[[np.ndarray], np.ndarray] = sin_blend_fn,
_prev: TileBlender | None = None,
) -> None:
self.direction: BlendDirection = direction
self.blend_fn: Callable[[np.ndarray], np.ndarray] = blend_fn
self.offset: int = 0
self.last_end_overlap: int = 0
self._last_blend: np.ndarray | None = None
if (
_prev is not None
and _prev.direction == direction
and _prev.width == width
and _prev.height == height
and _prev.channels == channels
):
if _prev.blend_fn == blend_fn:
# reuse blend
self._last_blend = _prev._last_blend # noqa: SLF001
result = _prev.result
else:
result = np.zeros((height, width, channels), dtype=np.float32)
self.result: np.ndarray = result
@property
def width(self) -> int:
return self.result.shape[1]
@property
def height(self) -> int:
return self.result.shape[0]
@property
def channels(self) -> int:
return self.result.shape[2]
def _get_blend(self, blend_size: int) -> np.ndarray:
if self.direction == BlendDirection.X:
if self._last_blend is not None and self._last_blend.shape[1] == blend_size:
return self._last_blend
blend = self.blend_fn(
np.arange(blend_size, dtype=np.float32) / (blend_size - 1)
)
blend = blend.reshape((1, blend_size, 1))
blend = np.repeat(blend, repeats=self.height, axis=0)
blend = np.repeat(blend, repeats=self.channels, axis=2)
else:
if self._last_blend is not None and self._last_blend.shape[0] == blend_size:
return self._last_blend
blend = self.blend_fn(
np.arange(blend_size, dtype=np.float32) / (blend_size - 1)
)
blend = blend.reshape((blend_size, 1, 1))
blend = np.repeat(blend, repeats=self.width, axis=1)
blend = np.repeat(blend, repeats=self.channels, axis=2)
self._last_blend = blend
return blend
def add_tile(self, tile: np.ndarray, overlap: TileOverlap) -> None:
h, w, c = get_h_w_c(tile)
assert c == self.channels
o = overlap
if self.direction == BlendDirection.X:
assert h == self.height
assert w > o.total
if self.offset == 0:
# the first tile is copied in as is
self.result[:, :w, ...] = tile
assert o.start == 0
self.offset += w - o.end
self.last_end_overlap = o.end
else:
assert self.offset < self.width, "All tiles were filled in already"
if self.last_end_overlap < o.start:
# we can't use all the overlap of the current tile, so we have to cut it off
diff = o.start - self.last_end_overlap
tile = tile[:, diff:, ...]
h, w, c = get_h_w_c(tile)
o = TileOverlap(self.last_end_overlap, o.end)
# copy over the part that doesn't need blending (yet)
self.result[
:, self.offset + o.start : self.offset + w - o.start, ...
] = tile[:, o.start * 2 :, ...]
# blend the overlapping part
blend_size = o.start * 2
blend = self._get_blend(blend_size)
left = self.result[
:, self.offset - o.start : self.offset + o.start, ...
]
right = tile[:, :blend_size, ...]
self.result[:, self.offset - o.start : self.offset + o.start, ...] = (
_fast_mix(left, right, blend)
)
self.offset += w - o.total
self.last_end_overlap = o.end
else:
assert w == self.width
assert h > o.total
if self.offset == 0:
# the first tile is copied in as is
self.result[:h, :, ...] = tile
assert o.start == 0
self.offset += h - o.end
self.last_end_overlap = o.end
else:
assert self.offset < self.height, "All tiles were filled in already"
if self.last_end_overlap < o.start:
# we can't use all the overlap of the current tile, so we have to cut it off
diff = o.start - self.last_end_overlap
tile = tile[diff:, :, ...]
h, w, c = get_h_w_c(tile)
o = TileOverlap(self.last_end_overlap, o.end)
# copy over the part that doesn't need blending
self.result[
self.offset + o.start : self.offset + h - o.start, :, ...
] = tile[o.start * 2 :, :, ...]
# blend the overlapping part
blend_size = o.start * 2
blend = self._get_blend(blend_size)
left = self.result[
self.offset - o.start : self.offset + o.start, :, ...
]
right = tile[: o.start * 2, :, ...]
self.result[self.offset - o.start : self.offset + o.start, :, ...] = (
_fast_mix(left, right, blend)
)
self.offset += h - o.total
self.last_end_overlap = o.end
def get_result(self) -> np.ndarray:
if self.direction == BlendDirection.X:
assert self.offset == self.width
else:
assert self.offset == self.height
return self.result
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/tiler.py
================================================
from abc import ABC, abstractmethod
from ...utils.utils import Size
class Tiler(ABC):
@abstractmethod
def allow_smaller_tile_size(self) -> bool:
"""
Whether the split implementation may use tile sizes smaller than the ones returned by this tiler.
If False, then the split implementation guarantees that the all tiles are of exactly the size returned by this tiler.
If the image is smaller than the returned tile size, then it has to be padded to reach the given size.
"""
@abstractmethod
def starting_tile_size(self, width: int, height: int, channels: int) -> Size:
"""
The starting tile size is the first tile size that will be used.
We generally prefer square tile sizes, but any tile size may be used.
"""
def split(self, tile_size: Size) -> Size:
w, h = tile_size
assert w >= 16 and h >= 16
return max(16, w // 2), max(16, h // 2)
class NoTiling(Tiler):
def allow_smaller_tile_size(self) -> bool:
return True
def starting_tile_size(self, width: int, height: int, channels: int) -> Size:
size = max(width, height)
# we prefer square tiles
return size, size
def split(self, tile_size: Size) -> Size:
raise ValueError("Image cannot be upscale with No Tiling mode.")
class MaxTileSize(Tiler):
def __init__(self, tile_size: int = 2**31) -> None:
self.tile_size: int = tile_size
def allow_smaller_tile_size(self) -> bool:
return True
def starting_tile_size(self, width: int, height: int, channels: int) -> Size:
# Tile size a lot larger than the image don't make sense.
# So we use the minimum of the image dimensions and the given tile size.
max_tile_size = max(width + 10, height + 10)
size = min(self.tile_size, max_tile_size)
return size, size
class ExactTileSize(Tiler):
def __init__(self, exact_size: Size) -> None:
self.exact_size = exact_size
def allow_smaller_tile_size(self) -> bool:
return False
def starting_tile_size(self, width: int, height: int, channels: int) -> Size:
return self.exact_size
def split(self, tile_size: Size) -> Size:
raise ValueError(
f"Splits are not supported for exact size ({self.exact_size[0]}x{self.exact_size[1]}px) splitting."
f" This typically means that your machine does not have enough VRAM to run the current model."
)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/node_cache.py
================================================
from __future__ import annotations
import functools
import hashlib
import os
import tempfile
import time
from collections.abc import Iterable
from enum import Enum
from typing import NewType
import numpy as np
from sanic.log import logger
from api import RunFn
CACHE_MAX_BYTES = int(os.environ.get("CACHE_MAX_BYTES", 1024**3)) # default 1 GiB
CACHE_REGISTRY: list[NodeOutputCache] = []
class CachedNumpyArray:
def __init__(self, arr: np.ndarray) -> None:
self.file = tempfile.TemporaryFile()
self.file.write(arr.tobytes())
self.shape = arr.shape
self.dtype = arr.dtype
def value(self) -> np.ndarray:
self.file.seek(0)
return np.frombuffer(self.file.read(), dtype=self.dtype).reshape(self.shape)
CacheKey = NewType("CacheKey", tuple)
class NodeOutputCache:
def __init__(self) -> None:
self._data: dict[CacheKey, list] = {}
self._bytes: dict[CacheKey, int] = {}
self._access_time: dict[CacheKey, float] = {}
CACHE_REGISTRY.append(self)
@staticmethod
def _args_to_key(args: Iterable[object]) -> CacheKey:
key = []
for arg in args:
if isinstance(arg, int | float | bool | str | bytes):
key.append(arg)
elif arg is None:
key.append(None)
elif isinstance(arg, Enum):
key.append(arg.value)
elif isinstance(arg, np.ndarray):
key.append(tuple(arg.shape))
key.append(arg.dtype.str)
key.append(hashlib.sha256(arg.tobytes()).digest())
elif hasattr(arg, "cache_key_func"):
key.append(arg.__class__.__name__)
key.append(arg.cache_key_func()) # type: ignore
else:
raise RuntimeError(f"Unexpected argument type {arg.__class__.__name__}")
return CacheKey(tuple(key))
@staticmethod
def _estimate_bytes(output: list[object]) -> int:
size = 0
for out in output:
if isinstance(out, np.ndarray):
size += out.nbytes
else:
# any other type but numpy arrays is probably negligible, but here's an overestimate to handle
# pathological cases where someone has a pipeline with a million math nodes
size += 1024 # 1 KiB
return size
def empty(self) -> bool:
return len(self._data) == 0
def oldest(self) -> tuple[CacheKey, float]:
return min(self._access_time.items(), key=lambda x: x[1])
def size(self):
return sum(self._bytes.values())
@staticmethod
def _enforce_limits() -> None:
while True:
total_bytes = sum([cache.size() for cache in CACHE_REGISTRY])
logger.debug(
f"Cache size: {total_bytes} ({100*total_bytes/CACHE_MAX_BYTES:0.1f}% of limit)"
)
if total_bytes <= CACHE_MAX_BYTES:
return
logger.debug("Dropping oldest cache key")
oldest_keys = [
(cache, cache.oldest()) for cache in CACHE_REGISTRY if not cache.empty()
]
cache, (key, _) = min(oldest_keys, key=lambda x: x[1][1])
cache.drop(key)
@staticmethod
def _write_arrays_to_disk(output: list) -> list:
return [
CachedNumpyArray(item) if isinstance(item, np.ndarray) else item
for item in output
]
@staticmethod
def _read_arrays_from_disk(output: list) -> list:
return [
item.value() if isinstance(item, CachedNumpyArray) else item
for item in output
]
@staticmethod
def _output_to_list(output: object) -> list[object]:
if isinstance(output, list):
return output
elif isinstance(output, tuple):
return list(output)
else:
return [output]
@staticmethod
def _list_to_output(output: list[object]):
if len(output) == 1:
return output[0]
return output
def get(self, args: Iterable[object]) -> object | None:
key = self._args_to_key(args)
if key in self._data:
logger.debug("Cache hit")
self._access_time[key] = time.time()
return self._list_to_output(self._read_arrays_from_disk(self._data[key]))
logger.debug("Cache miss")
return None
def put(self, args: Iterable[object], output: object) -> None:
key = self._args_to_key(args)
self._data[key] = self._write_arrays_to_disk(self._output_to_list(output))
self._bytes[key] = self._estimate_bytes(self._output_to_list(output))
self._access_time[key] = time.time()
self._enforce_limits()
def drop(self, key: CacheKey) -> None:
del self._data[key]
del self._bytes[key]
del self._access_time[key]
def cached(run: RunFn):
cache = NodeOutputCache()
@functools.wraps(run)
def _run(*args: object):
out = cache.get(args)
if out is not None:
return out
output = run(*args)
cache.put(args, output)
return output
return _run
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/__init__.py
================================================
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/__init__.py
================================================
from .file_inputs import *
from .generic_inputs import *
from .image_dropdown_inputs import *
from .numeric_inputs import *
from .numpy_inputs import *
try:
from .ncnn_inputs import *
except Exception:
pass
try:
from .onnx_inputs import *
except Exception:
pass
try:
from .pytorch_inputs import *
except Exception:
pass
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/__system_inputs.py
================================================
from __future__ import annotations
import math
from typing import Literal
from navi import ExpressionJson
from api import BaseInput
class StaticValueInput(BaseInput):
def __init__(
self,
label: str,
py_type: type = str,
navi_type: ExpressionJson = "string",
value: Literal["execution_number"] = "execution_number",
) -> None:
super().__init__(navi_type, label, kind="static", has_handle=False)
self.associated_type = py_type
self.value = value
def to_dict(self):
return {
**super().to_dict(),
"value": self.value,
}
def enforce(self, value: object):
return_value = value
if not isinstance(value, self.associated_type):
return_value = self.associated_type(value)
if isinstance(value, float | int) and math.isnan(value):
raise ValueError("NaN is not a valid number")
return return_value
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/file_inputs.py
================================================
from __future__ import annotations
import re
from pathlib import Path
from typing import Literal, Union
import navi
from api import BaseInput
# pylint: disable=relative-beyond-top-level
from ...impl.image_formats import get_available_image_formats
from .generic_inputs import TextInput
from .label import LabelStyle
FileInputKind = Union[
Literal["bin"],
Literal["image"],
Literal["onnx"],
Literal["param"],
Literal["pt"],
Literal["pth"],
Literal["video"],
]
class FileInput(BaseInput):
"""Input for submitting a local file"""
def __init__(
self,
label: str,
file_kind: FileInputKind,
filetypes: list[str],
has_handle: bool = False,
primary_input: bool = False,
) -> None:
super().__init__(
navi.named("File", {"kind": navi.literal(file_kind)}),
label,
kind="file",
has_handle=has_handle,
)
self.filetypes = filetypes
self.file_kind = file_kind
self.primary_input = primary_input
self.input_adapt = """
match Input {
string as path => File { path },
_ => never
}
"""
self.associated_type = Path
def to_dict(self):
return {
**super().to_dict(),
"filetypes": self.filetypes,
"fileKind": self.file_kind,
"primaryInput": self.primary_input,
}
def enforce(self, value: object) -> Path:
if isinstance(value, str):
value = Path(value)
assert isinstance(value, Path)
assert value.exists(), f"File {value} does not exist"
assert value.is_file(), f"The path {value} is not a file"
return value
def ImageFileInput(primary_input: bool = False) -> FileInput:
"""Input for submitting a local image file"""
return FileInput(
label="Image File",
file_kind="image",
filetypes=get_available_image_formats(),
has_handle=False,
primary_input=primary_input,
)
def VideoFileInput(primary_input: bool = False) -> FileInput:
"""Input for submitting a local video file"""
return FileInput(
label="Video File",
file_kind="video",
filetypes=[
".mp4",
".h264",
".hevc",
".webm",
".avi",
".gif",
".mov",
".mkv",
".flv",
".m4v",
".avs",
],
has_handle=False,
primary_input=primary_input,
)
def PthFileInput(primary_input: bool = False) -> FileInput:
"""Input for submitting a local .pth file"""
return FileInput(
label="Model",
file_kind="pth",
filetypes=[".pt", ".pth", ".ckpt", ".safetensors"],
primary_input=primary_input,
)
class DirectoryInput(BaseInput[Path]):
"""Input for submitting a local directory"""
def __init__(
self,
label: str = "Directory",
has_handle: bool = True,
must_exist: bool = True,
label_style: LabelStyle = "default",
) -> None:
super().__init__("Directory", label, kind="directory", has_handle=has_handle)
self.input_adapt = """
match Input {
string as path => Directory { path },
_ => never
}
"""
self.must_exist: bool = must_exist
self.label_style: LabelStyle = label_style
self.associated_type = Path
def to_dict(self):
return {
**super().to_dict(),
"labelStyle": self.label_style,
}
def enforce(self, value: object) -> Path:
if isinstance(value, str):
value = Path(value)
assert isinstance(value, Path)
if self.must_exist:
assert value.exists(), f"Directory {value} does not exist"
return value
def BinFileInput(primary_input: bool = False) -> FileInput:
"""Input for submitting a local .bin file"""
return FileInput(
label="NCNN Bin File",
file_kind="bin",
filetypes=[".bin"],
primary_input=primary_input,
)
def ParamFileInput(primary_input: bool = False) -> FileInput:
"""Input for submitting a local .param file"""
return FileInput(
label="NCNN Param File",
file_kind="param",
filetypes=[".param"],
primary_input=primary_input,
)
def OnnxFileInput(primary_input: bool = False) -> FileInput:
"""Input for submitting a local .onnx file"""
return FileInput(
label="ONNX Model File",
file_kind="onnx",
filetypes=[".onnx"],
primary_input=primary_input,
)
_INVALID_PATH_CHARS = re.compile(r'[<>:"|?*\x00-\x1F]')
def _is_abs_path(path: str) -> bool:
return path.startswith(("/", "\\")) or Path(path).is_absolute()
class RelativePathInput(TextInput):
def __init__(
self,
label: str,
has_handle: bool = True,
placeholder: str | None = None,
allow_numbers: bool = True,
default: str | None = None,
label_style: LabelStyle = "default",
) -> None:
super().__init__(
label,
has_handle=has_handle,
min_length=1,
max_length=None,
placeholder=placeholder,
multiline=False,
allow_numbers=allow_numbers,
default=default,
label_style=label_style,
allow_empty_string=False,
invalid_pattern=_INVALID_PATH_CHARS.pattern,
)
def enforce(self, value: object) -> str:
value = super().enforce(value)
if _is_abs_path(value):
raise ValueError(f"Absolute paths are not allowed for input {self.label}.")
invalid = _INVALID_PATH_CHARS.search(value)
if invalid is not None:
raise ValueError(f"Invalid character '{invalid.group()}' in {self.label}.")
return value
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/generic_inputs.py
================================================
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from enum import Enum
from typing import Any, Literal, Never, NotRequired, TypedDict, TypeVar
import navi
import numpy as np
from sanic.log import logger
from api import BaseInput, InputConversion, group
from ...condition import Condition, ConditionJson
from ...impl.blend import BlendMode
from ...impl.color.color import Color
from ...impl.image_utils import FillColor
from ...impl.upscale.auto_split_tiles import (
CUSTOM,
ESTIMATE,
MAX_TILE_SIZE,
NO_TILING,
TileSize,
)
from ...utils.format import format_color_with_channels
from ...utils.seed import Seed
from ...utils.utils import (
join_pascal_case,
join_space_case,
split_pascal_case,
split_snake_case,
)
from .label import LabelStyle
from .numeric_inputs import NumberInput
class DropDownOption(TypedDict):
option: str
icon: NotRequired[str | None]
value: str | int
type: NotRequired[navi.ExpressionJson]
condition: NotRequired[ConditionJson | None]
DropDownStyle = Literal["dropdown", "checkbox", "tabs", "icons", "anchor"]
"""
This specified the preferred style in which the frontend may display the dropdown.
- `dropdown`: This is the default style. The dropdown will simply be displayed as a dropdown.
- `checkbox`: If the dropdown has 2 options, then it will be displayed as a checkbox.
The first option will be interpreted as the yes/true option while the second option will be interpreted as the no/false option.
- `tabs`: The options are displayed as tab list. The label of the input itself will *not* be displayed.
- `icons`: The options are displayed as a list of icons. This is only available if all options have icons. Labels are still required for all options.
- `anchor`: The options are displayed as a 3x3 grid where the user is allowed to select one of 9 anchor positions. This only works for dropdowns with 9 options.
"""
@dataclass
class DropDownGroup:
label: str | None
start_at: str | int | Enum
@staticmethod
def divider(start_at: str | int | Enum):
return DropDownGroup(None, start_at)
def to_dict(self):
start_at = self.start_at
if isinstance(start_at, Enum):
start_at = start_at.value
return {"label": self.label, "startAt": start_at}
T = TypeVar("T")
class DropDownInput(BaseInput[T]):
"""Input for a dropdown"""
def __init__(
self,
input_type: navi.ExpressionJson,
label: str,
options: list[DropDownOption],
default_value: str | int | None = None,
preferred_style: DropDownStyle = "dropdown",
label_style: LabelStyle = "default",
groups: list[DropDownGroup] | None = None,
associated_type: Any = None,
) -> None:
super().__init__(input_type, label, kind="dropdown", has_handle=False)
self.options = options
self.accepted_values = {o["value"] for o in self.options}
self.default = (
default_value if default_value is not None else options[0]["value"]
)
self.preferred_style: DropDownStyle = preferred_style
self.label_style: LabelStyle = label_style
self.groups: list[DropDownGroup] = groups or []
if self.default not in self.accepted_values:
logger.error(
f"Invalid default value {self.default} in {label} dropdown. Using first value instead."
)
self.default = options[0]["value"]
self.associated_type = (
associated_type if associated_type is not None else type(self.default)
)
def to_dict(self):
return {
**super().to_dict(),
"options": self.options,
"def": self.default,
"preferredStyle": self.preferred_style,
"labelStyle": self.label_style,
"groups": [c.to_dict() for c in self.groups],
}
def make_optional(self) -> Never:
raise ValueError("DropDownInput cannot be made optional")
def enforce(self, value: object) -> T:
assert value in self.accepted_values, f"{value} is not a valid option"
return value # type: ignore
def wrap_with_conditional_group(self):
"""
Adds a conditional group around the dropdown input according to the conditions of its options.
Note: Calling this method is only valid if all options have a condition.
"""
conditions: list[ConditionJson] = []
for option in self.options:
c = option.get("condition")
if c is None:
raise ValueError(
f"wrap_with_conditional is unnecessary, because the {option['option']} option has no condition."
)
conditions.append(c)
condition: ConditionJson = {"kind": "or", "items": conditions}
return group("conditional", {"condition": condition})(self)
class _BoolEnumInput(DropDownInput[bool]):
def __init__(
self, label: str, *, default: bool = True, icon: str | None = None
) -> None:
super().__init__(
input_type="bool",
label=label,
default_value=int(default),
options=[
{
"option": "Yes",
"value": int(True), # 1
"type": "true",
"icon": icon,
},
{
"option": "No",
"value": int(False), # 0
"type": "false",
},
],
preferred_style="checkbox",
)
self.associated_type = bool
def enforce(self, value: object) -> bool:
value = super().enforce(value)
return bool(value)
class _BoolGenericInput(BaseInput[bool]):
def __init__(self, label: str) -> None:
super().__init__(input_type="bool", label=label)
self.associated_type = bool
def enforce(self, value: object) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, int):
return bool(value)
raise ValueError(
f"The value of input '{self.label}' should have been either True or False."
)
def BoolInput(
label: str,
*,
default: bool = True,
icon: str | None = None,
has_handle: bool = False,
):
if has_handle:
return _BoolGenericInput(label)
return _BoolEnumInput(label, default=default, icon=icon)
E = TypeVar("E", bound=Enum)
class EnumInput(DropDownInput[E]):
"""
This adapts a python Enum into a chaiNNer dropdown input.
### Features
All variants of the enum will be converted into typed dropdown options.
The dropdown will be fully typed and bring its own type definitions.
Option labels can be (partially) overridden using `option_labels`.
By default, the input label, type names, and option labels will all be generated from the enum name and variant names.
All of those defaults can be overridden.
Options will be ordered by declaration order in the python enum definition.
### Requirements
The value of each variant has to be either `str` or `int`.
Other types are not permitted.
"""
def __init__(
self,
enum: type[E],
label: str | None = None,
*,
default: E | None = None,
type_name: str | None = None,
option_labels: dict[E, str] | None = None,
extra_definitions: str | None = None,
preferred_style: DropDownStyle = "dropdown",
label_style: LabelStyle = "default",
categories: list[DropDownGroup] | None = None,
conditions: dict[E, Condition] | None = None,
icons: dict[E, str] | None = None,
) -> None:
if type_name is None:
type_name = enum.__name__
if label is None:
label = join_space_case(split_pascal_case(type_name))
if option_labels is None:
option_labels = {}
if conditions is None:
conditions = {}
if icons is None:
icons = {}
options: list[DropDownOption] = []
variant_types: list[str] = []
for variant in enum:
value = variant.value
assert isinstance(value, int | str)
variant_type = EnumInput.get_variant_type(variant, type_name)
option_label = option_labels.get(
variant, join_space_case(split_snake_case(variant.name))
)
condition = conditions.get(variant)
if condition is not None:
condition = condition.to_json()
variant_types.append(variant_type)
options.append(
{
"option": option_label,
"value": value,
"type": variant_type,
"condition": condition,
"icon": icons.get(variant),
}
)
super().__init__(
input_type=type_name,
label=label,
options=options,
default_value=default.value if default is not None else None,
preferred_style=preferred_style,
label_style=label_style,
groups=categories,
)
self.type_definitions = (
f"let {type_name} = {' | '.join(variant_types)};\n"
+ "\n".join([f"struct {t};" for t in variant_types])
+ (extra_definitions or "")
)
self.type_name: str = type_name
self.enum = enum
self.associated_type = enum
@staticmethod
def get_variant_type(variant: Enum, type_name: str | None = None) -> str:
"""
Returns the full type name of a variant of an enum.
"""
enum = variant.__class__
if type_name is None:
type_name = enum.__name__
assert (
re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", variant.name) is not None
), f"Expected the name of {enum.__name__}.{variant.name} to be snake case."
return f"{type_name}::{join_pascal_case(split_snake_case(variant.name))}"
def enforce(self, value: object) -> E:
value = super().enforce(value)
return self.enum(value)
class TextInput(BaseInput[str]):
"""Input for arbitrary text"""
def __init__(
self,
label: str,
*,
has_handle: bool = True,
min_length: int = 0,
max_length: int | None = None,
placeholder: str | None = None,
multiline: bool = False,
allow_numbers: bool = True,
default: str | None = None,
label_style: LabelStyle = "default",
allow_empty_string: bool = False,
invalid_pattern: str | None = None,
) -> None:
super().__init__(
input_type="string" if min_length == 0 else 'invStrSet("")',
label=label,
has_handle=has_handle,
kind="text",
)
self.min_length = min_length
self.max_length = max_length
self.placeholder = placeholder
self.default = default
self.multiline = multiline
self.label_style: LabelStyle = label_style
self.allow_empty_string = allow_empty_string
self.invalid_pattern = invalid_pattern
if default is not None:
assert default != "" or allow_empty_string
assert min_length <= len(default)
assert max_length is None or len(default) < max_length
self.associated_type = str
if allow_numbers:
self.input_conversions = [InputConversion("number", "toString(Input)")]
def enforce(self, value: object) -> str:
if isinstance(value, float) and int(value) == value:
# stringify integers values
value = str(int(value))
else:
value = str(value)
# enforce length range
if self.max_length is not None and len(value) > self.max_length:
value = value[: self.max_length]
if len(value) < self.min_length:
raise ValueError(
f"Text value of input '{self.label}' must be at least {self.min_length} characters long,"
f" but found {len(value)} ('{value}')."
)
return value
def to_dict(self):
return {
**super().to_dict(),
"minLength": self.min_length,
"maxLength": self.max_length,
"placeholder": self.placeholder,
"multiline": self.multiline,
"def": self.default,
"labelStyle": self.label_style,
"allowEmptyString": self.allow_empty_string,
"invalidPattern": self.invalid_pattern,
}
class ClipboardInput(BaseInput):
"""Input for pasting from clipboard"""
def __init__(self, label: str = "Clipboard input") -> None:
super().__init__(["Image", "string", "number"], label, kind="text")
self.input_conversions = [InputConversion("Image", '""')]
self.label_style: LabelStyle = "hidden"
def enforce(self, value: object):
if isinstance(value, np.ndarray):
return value
if isinstance(value, float) and int(value) == value:
# stringify integers values
return str(int(value))
return str(value)
def to_dict(self):
return {
**super().to_dict(),
"labelStyle": self.label_style,
}
class AnyInput(BaseInput[object]):
def __init__(self, label: str) -> None:
super().__init__(input_type="any", label=label)
self.associated_type = object
def enforce_(self, value: object):
# The behavior for optional inputs and None makes sense for all inputs except this one.
return value
class SeedInput(NumberInput):
def __init__(self, label: str = "Seed", *, has_handle: bool = True) -> None:
super().__init__(
label=label,
min=None,
max=None,
precision=0,
default=0,
label_style="default",
)
self.has_handle = has_handle
self.input_type = "Seed | int"
self.input_conversions = [InputConversion("int", "Seed")]
self.input_adapt = """
match Input {
int => Seed,
_ => never
}
"""
self.associated_type = Seed
def enforce(self, value: object) -> Seed: # type: ignore
if isinstance(value, Seed):
return value
if isinstance(value, int | float | str):
return Seed(int(value))
raise ValueError(f"Cannot convert {value} to Seed")
def make_optional(self) -> Never:
raise ValueError("SeedInput cannot be made optional")
class ColorInput(BaseInput[Color]):
def __init__(
self,
label: str = "Color",
*,
default: Color | None = None,
channels: int | list[int] | None = None,
) -> None:
super().__init__(
input_type=navi.Color(channels=channels),
label=label,
has_handle=True,
kind="color",
)
self.input_adapt = """
match Input {
string => parseColorJson(Input),
_ => never
}
"""
self.channels: list[int] | None = (
[channels] if isinstance(channels, int) else channels
)
if self.channels is None:
if default is None:
default = Color.bgr((0.5, 0.5, 0.5))
else:
assert len(self.channels) >= 0
if default is None:
if 3 in self.channels:
default = Color.bgr((0.5, 0.5, 0.5))
elif 4 in self.channels:
default = Color.bgra((0.5, 0.5, 0.5, 1))
elif 1 in self.channels:
default = Color.gray(0.5)
else:
raise ValueError("Cannot find default color value")
else:
assert (
default.channels in self.channels
), "The default color is not accepted."
self.default: Color = default
self.associated_type = Color
def enforce(self, value: object) -> Color:
if isinstance(value, str):
# decode color JSON strings from the frontend
value = Color.from_json(json.loads(value))
assert isinstance(value, Color)
if self.channels is not None and value.channels not in self.channels:
expected = format_color_with_channels(self.channels, plural=True)
actual = format_color_with_channels([value.channels])
raise ValueError(
f"The input {self.label} only supports {expected} but was given {actual}."
)
return value
def to_dict(self):
return {
**super().to_dict(),
"def": json.dumps(self.default.to_json()),
"channels": self.channels,
}
def make_optional(self) -> Never:
raise ValueError("ColorInput cannot be made optional")
def BlendModeDropdown() -> DropDownInput:
"""Blending Mode option dropdown"""
return EnumInput(
BlendMode,
option_labels={
BlendMode.ADD: "Linear Dodge (Add)",
},
categories=[
DropDownGroup.divider(start_at=BlendMode.DARKEN),
DropDownGroup.divider(start_at=BlendMode.LIGHTEN),
DropDownGroup.divider(start_at=BlendMode.OVERLAY),
DropDownGroup.divider(start_at=BlendMode.DIFFERENCE),
],
)
def FillColorDropdown() -> DropDownInput:
return EnumInput(
FillColor,
label="Negative Space Fill",
default=FillColor.AUTO,
extra_definitions="""
def FillColor::getOutputChannels(fill: FillColor, channels: uint) {
match fill {
FillColor::Transparent => 4,
_ => channels
}
}
""",
)
def TileSizeDropdown(
label: str = "Tile Size", *, estimate: bool = True, default: TileSize | None = None
) -> DropDownInput:
options = []
if estimate:
options.append({"option": "Auto (estimate)", "value": ESTIMATE})
options.append({"option": "Maximum", "value": MAX_TILE_SIZE})
options.append({"option": "No Tiling", "value": NO_TILING})
for size in [128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096]:
options.append({"option": str(size), "value": size})
options.append({"option": "Custom", "value": CUSTOM})
return DropDownInput(
input_type="TileSize",
label=label,
options=options,
associated_type=TileSize,
default_value=default,
)
class AudioStreamInput(BaseInput):
def __init__(self, label: str = "Audio Stream") -> None:
super().__init__("AudioStream", label, kind="generic")
class OrderEnum(Enum):
ROW_MAJOR = 0
COLUMN_MAJOR = 1
def RowOrderDropdown() -> DropDownInput:
return EnumInput(
OrderEnum,
label="Order",
default=OrderEnum.ROW_MAJOR,
)
class Anchor(Enum):
TOP_LEFT = "top_left"
TOP = "top_centered"
TOP_RIGHT = "top_right"
LEFT = "centered_left"
CENTER = "centered"
RIGHT = "centered_right"
BOTTOM_LEFT = "bottom_left"
BOTTOM = "bottom_centered"
BOTTOM_RIGHT = "bottom_right"
def AnchorInput(label: str = "Anchor", icon: str = "BsFillImageFill") -> DropDownInput:
return EnumInput(
Anchor,
label=label,
label_style="inline",
option_labels={
Anchor.TOP_LEFT: "Top Left",
Anchor.TOP: "Top",
Anchor.TOP_RIGHT: "Top Right",
Anchor.LEFT: "Left",
Anchor.CENTER: "Center",
Anchor.RIGHT: "Right",
Anchor.BOTTOM_LEFT: "Bottom Left",
Anchor.BOTTOM: "Bottom",
Anchor.BOTTOM_RIGHT: "Bottom Right",
},
icons={
Anchor.TOP_LEFT: icon,
Anchor.TOP: icon,
Anchor.TOP_RIGHT: icon,
Anchor.LEFT: icon,
Anchor.CENTER: icon,
Anchor.RIGHT: icon,
Anchor.BOTTOM_LEFT: icon,
Anchor.BOTTOM: icon,
Anchor.BOTTOM_RIGHT: icon,
},
preferred_style="anchor",
default=Anchor.CENTER,
)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/image_dropdown_inputs.py
================================================
import navi
from ...impl.color.convert_data import (
color_spaces,
color_spaces_or_detectors,
get_alpha_partner,
is_alpha_partner,
)
# pylint: disable=relative-beyond-top-level
from ...impl.image_utils import BorderType
from ...impl.pil_utils import RotationInterpolationMethod
from ...impl.resize import ResizeFilter
from .generic_inputs import DropDownGroup, DropDownInput, EnumInput
def ColorSpaceDetectorInput(label: str = "Color Space") -> DropDownInput:
return DropDownInput(
input_type="ColorSpace",
label=label,
options=[
{
"option": c.name,
"value": c.id,
"type": navi.named("ColorSpace", {"channels": c.channels}),
}
for c in color_spaces_or_detectors
],
)
def ColorSpaceInput(label: str = "Color Space") -> DropDownInput:
return DropDownInput(
input_type="ColorSpace",
label=label,
options=[
{
"option": c.name,
"value": c.id,
"type": navi.named(
"ColorSpace",
{
"channels": c.channels,
"supportsAlpha": get_alpha_partner(c) is not None,
},
),
}
for c in color_spaces
if not is_alpha_partner(c)
],
)
def ResizeFilterInput() -> DropDownInput:
return EnumInput(
ResizeFilter,
label="Interpolation Method",
categories=[
DropDownGroup("Basic", start_at=ResizeFilter.AUTO),
DropDownGroup("Advanced", start_at=ResizeFilter.HERMITE),
],
option_labels={
ResizeFilter.NEAREST: "Nearest Neighbor",
ResizeFilter.BOX: "Area (Box)",
ResizeFilter.CATROM: "Cubic",
ResizeFilter.BSPLINE: "B-Spline",
},
)
def RotateInterpolationInput() -> DropDownInput:
return EnumInput(
RotationInterpolationMethod,
label="Interpolation Method",
option_labels={
RotationInterpolationMethod.NEAREST: "Nearest Neighbor",
},
)
def BorderInput() -> DropDownInput:
return EnumInput(
BorderType,
default=BorderType.REFLECT_MIRROR,
option_labels={
BorderType.REFLECT_MIRROR: "Reflect (Mirror)",
BorderType.WRAP: "Wrap (Tile)",
BorderType.REPLICATE: "Replicate Edges",
},
extra_definitions="""
def BorderType::getOutputChannels(type: BorderType, channels: uint, color: Color | null): uint {
match type {
BorderType::Transparent => 4,
BorderType::CustomColor => match color {
Color => max(color.channels, channels),
null => never,
},
_ => channels
}
}
""",
)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/label.py
================================================
from typing import Literal
LabelStyle = Literal["default", "hidden", "inline"]
def get_default_label_style(label: str) -> LabelStyle:
return "inline" if len(label) <= 8 else "default"
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/ncnn_inputs.py
================================================
from api import BaseInput
from ...impl.ncnn.model import NcnnModelWrapper
class NcnnModelInput(BaseInput):
"""Input for NcnnModel"""
def __init__(self, label: str = "Model") -> None:
super().__init__("NcnnNetwork", label)
self.associated_type = NcnnModelWrapper
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/numeric_inputs.py
================================================
from __future__ import annotations
import math
from typing import Literal, Never, Union
import navi
from api import BaseInput, InputConversion, InputKind
from ...utils.utils import round_half_up
from .label import LabelStyle, get_default_label_style
Precision = Union[int, Literal["unlimited"]]
def _get_step(precision: Precision) -> float:
if precision == "unlimited":
return 1
return 10**-precision
def _is_int(precision: Precision) -> bool:
return precision == 0
def clamp_number(
value: float | int,
precision: Precision,
min_value: float | int | None,
max_value: float | int | None,
) -> float | int:
# Convert proper number type
if precision != "unlimited":
value = round_half_up(value) if precision == 0 else round(value, precision)
# Clamp to max and min, correcting for max/min not aligning with offset + n * step
if max_value is not None:
value = min(value, max_value)
if min_value is not None:
value = max(value, min_value)
# guarantee integers
if _is_int(precision):
return int(value)
else:
return float(value)
def get_number_type(
min_value: float | int | None,
max_value: float | int | None,
precision: Precision,
) -> navi.ExpressionJson:
if not _is_int(precision):
# step is not an integer
return navi.interval(min_value, max_value)
return navi.int_interval(min_value, max_value)
class NumberInput(BaseInput):
"""Input a number"""
def __init__(
self,
label: str,
*,
precision: Precision = 0,
step: float | int | None = None,
default: float | int = 0,
min: float | int | None = 0,
max: float | int | None = None,
unit: str | None = None,
note_expression: str | None = None,
kind: InputKind = "number",
hide_trailing_zeros: bool = True,
label_style: LabelStyle | None = None,
has_handle: bool = True,
) -> None:
super().__init__("number", label, kind=kind, has_handle=has_handle)
self.precision: int | Literal["unlimited"] = precision
# controls_step is for increment/decrement arrows.
self.step: float | int = step if step is not None else _get_step(precision)
self.default = default
self.min = min
self.max = max
self.unit = unit
self.note_expression = note_expression
self.hide_trailing_zeros = hide_trailing_zeros
self.label_style: LabelStyle = label_style or get_default_label_style(label)
self.associated_type = float if not _is_int(precision) else int
self.input_type = get_number_type(self.min, self.max, self.precision)
if self.precision == 0:
self.input_conversions = [InputConversion("number", "round(Input)")]
def to_dict(self):
return {
**super().to_dict(),
"min": self.min,
"max": self.max,
"noteExpression": self.note_expression,
"def": self.default,
"precision": 100 if self.precision == "unlimited" else self.precision,
"controlsStep": self.step,
"unit": self.unit,
"hideTrailingZeros": self.hide_trailing_zeros,
"labelStyle": self.label_style,
"hasHandle": self.has_handle,
}
def make_optional(self) -> Never:
raise ValueError("NumberInput and SliderInput cannot be made optional")
def enforce(self, value: object):
assert isinstance(value, int | float)
if math.isnan(value):
raise ValueError("NaN is not a valid number")
return clamp_number(value, self.precision, self.min, self.max)
class SliderInput(NumberInput):
"""Input for integer number via slider"""
def __init__(
self,
label: str,
*,
precision: Precision = 0,
step: float | int | None = None,
slider_step: float | int | None = None,
min: float | int = 0,
max: float | int = 100,
default: float | int = 50,
unit: str | None = None,
note_expression: str | None = None,
ends: tuple[str | None, str | None] = (None, None),
hide_trailing_zeros: bool = False,
gradient: list[str] | None = None,
scale: Literal["linear", "log", "log-offset", "sqrt"] = "linear",
has_handle: bool = True,
) -> None:
super().__init__(
label,
precision=precision,
step=step,
default=default,
min=min,
max=max,
unit=unit,
note_expression=note_expression,
kind="slider",
hide_trailing_zeros=hide_trailing_zeros,
has_handle=has_handle,
)
self.ends = ends
self.slider_step = (
slider_step
if slider_step is not None
else (step if step is not None else _get_step(precision))
)
self.gradient = gradient
self.scale = scale
def to_dict(self):
return {
**super().to_dict(),
"ends": self.ends,
"sliderStep": self.slider_step,
"gradient": self.gradient,
"scale": self.scale,
}
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/numpy_inputs.py
================================================
# pylint: disable=relative-beyond-top-level
from __future__ import annotations
from typing import Union
import navi
import numpy as np
from api import BaseInput, ErrorValue
from ...impl.color.color import Color
from ...utils.format import format_color_with_channels, format_image_with_channels
from ...utils.utils import get_h_w_c
class AudioInput(BaseInput):
"""Input a 1D Audio NumPy array"""
def __init__(self, label: str = "Audio") -> None:
super().__init__("Audio", label)
class ImageInput(BaseInput):
"""Input a 2D Image NumPy array"""
def __init__(
self,
label: str = "Image",
*,
image_type: navi.ExpressionJson = "Image | Color",
channels: int | list[int] | None = None,
allow_colors: bool = False,
) -> None:
base_type = [navi.Image(channels=channels)]
if allow_colors:
base_type.append(navi.Color(channels=channels))
image_type = navi.intersect(image_type, base_type)
super().__init__(image_type, label)
self.channels: list[int] | None = (
[channels] if isinstance(channels, int) else channels
)
self.allow_colors: bool = allow_colors
self.associated_type = np.ndarray
if self.allow_colors:
self.associated_type = Union[np.ndarray, Color]
def enforce(self, value: object):
if isinstance(value, Color):
if not self.allow_colors:
raise ValueError(
f"The input {self.label} does not accept colors, but was connected with one."
)
if self.channels is not None and value.channels not in self.channels:
expected = format_color_with_channels(self.channels, plural=True)
actual = format_color_with_channels([value.channels])
raise ValueError(
f"The input {self.label} only supports {expected} but was given {actual}."
)
return value
assert isinstance(value, np.ndarray)
_, _, c = get_h_w_c(value)
if self.channels is not None and c not in self.channels:
expected = format_image_with_channels(self.channels, plural=True)
actual = format_image_with_channels([c])
raise ValueError(
f"The input {self.label} only supports {expected} but was given {actual}."
)
assert value.dtype == np.float32, "Expected the input image to be normalized."
if c == 1 and value.ndim == 3:
value = value[:, :, 0]
return value
def get_error_value(self, value: object) -> ErrorValue:
def get_channels(channel: int) -> str:
if channel == 1:
return "Grayscale"
if channel == 3:
return "RGB"
if channel == 4:
return "RGBA"
return f"{channel}-channel"
if isinstance(value, Color):
return {
"type": "formatted",
"formatString": f"{get_channels(value.channels)} Color",
}
elif isinstance(value, np.ndarray):
h, w, c = get_h_w_c(value)
return {
"type": "formatted",
"formatString": f"{get_channels(c)} Image {w}x{h}",
}
else:
return super().get_error_value(value)
class VideoInput(BaseInput):
"""Input a 3D Video NumPy array"""
def __init__(self, label: str = "Video") -> None:
super().__init__("Video", label)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/onnx_inputs.py
================================================
import navi
from api import BaseInput
from ...impl.onnx.model import OnnxGeneric, OnnxModel, OnnxModels, OnnxRemBg
from .generic_inputs import DropDownInput
class OnnxModelInput(BaseInput):
"""Input for onnx model"""
def __init__(
self, label: str = "Model", input_type: navi.ExpressionJson = "OnnxModel"
) -> None:
super().__init__(input_type, label)
self.associated_type = OnnxModel
class OnnxGenericModelInput(OnnxModelInput):
"""ONNX model input for things that aren't background removal"""
def __init__(
self, label: str = "Model", input_type: navi.ExpressionJson = "OnnxModel"
) -> None:
super().__init__(label, navi.intersect(input_type, "OnnxGenericModel"))
self.associated_type = OnnxGeneric
def enforce(self, value: object):
assert isinstance(value, OnnxModels)
assert value.sub_type == "Generic", "Expected a non-rembg model"
return value
class OnnxRemBgModelInput(OnnxModelInput):
"""ONNX model input for background removal"""
def __init__(
self, label: str = "Model", input_type: navi.ExpressionJson = "OnnxModel"
) -> None:
super().__init__(label, navi.intersect(input_type, "OnnxRemBgModel"))
self.associated_type = OnnxRemBg
def enforce(self, value: object):
assert isinstance(value, OnnxModels)
assert value.sub_type == "RemBg", "Expected a rembg model"
return value
def OnnxFpDropdown() -> DropDownInput:
return DropDownInput(
input_type="FpMode",
label="Data Type",
options=[
{
"option": "fp32",
"value": 0,
"type": "FpMode::fp32",
},
{
"option": "fp16",
"value": 1,
"type": "FpMode::fp16",
},
],
)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/pytorch_inputs.py
================================================
from __future__ import annotations
try:
import spandrel
from spandrel import Purpose
except Exception:
spandrel = None
import navi
from api import BaseInput
def _model_with_purpose(purpose: set[Purpose]):
sub_type = " | ".join('"' + p + '"' for p in purpose)
return "PyTorchModel { subType: " + sub_type + " }"
class ModelInput(BaseInput):
"""Input a loaded model"""
def __init__(
self,
label: str = "Model",
input_type: navi.ExpressionJson = "PyTorchModel",
) -> None:
super().__init__(input_type, label)
if spandrel is not None:
self.associated_type = spandrel.ModelDescriptor
def enforce(self, value: object):
if spandrel is not None:
assert isinstance(
value,
spandrel.ImageModelDescriptor | spandrel.MaskedImageModelDescriptor,
), "Expected a supported PyTorch model."
return value
class SrModelInput(ModelInput):
def __init__(
self,
label: str = "Model",
input_type: navi.ExpressionJson = "PyTorchModel",
) -> None:
self.purpose: set[Purpose] = {"SR", "Restoration"}
super().__init__(
label,
navi.intersect(input_type, _model_with_purpose(self.purpose)),
)
if spandrel is not None:
self.associated_type = spandrel.ImageModelDescriptor
def enforce(self, value: object):
if spandrel is not None:
assert isinstance(
value, spandrel.ImageModelDescriptor
), "Expected a supported single image PyTorch model."
assert (
value.purpose in self.purpose
), "Expected a Super-Resolution or Restoration model."
return value
class FaceModelInput(ModelInput):
def __init__(
self, label: str = "Model", input_type: navi.ExpressionJson = "PyTorchModel"
) -> None:
self.purpose: set[Purpose] = {"FaceSR"}
super().__init__(
label,
navi.intersect(input_type, _model_with_purpose(self.purpose)),
)
if spandrel is not None:
self.associated_type = spandrel.ImageModelDescriptor
def enforce(self, value: object):
if spandrel is not None:
assert isinstance(
value, spandrel.ImageModelDescriptor
), "Expected a supported single image PyTorch model."
assert (
value.purpose in self.purpose
), "Expected a Face Super-Resolution model."
return value
class InpaintModelInput(ModelInput):
def __init__(
self, label: str = "Model", input_type: navi.ExpressionJson = "PyTorchModel"
) -> None:
self.purpose: set[Purpose] = {"Inpainting"}
super().__init__(
label,
navi.intersect(input_type, _model_with_purpose(self.purpose)),
)
if spandrel is not None:
self.associated_type = spandrel.MaskedImageModelDescriptor
def enforce(self, value: object):
if spandrel is not None:
assert isinstance(
value, spandrel.MaskedImageModelDescriptor
), "Expected a supported masked-image PyTorch model."
assert value.purpose in self.purpose, "Expected an Inpainting model."
return value
class TorchScriptInput(BaseInput):
"""Input a JIT traced model"""
def __init__(self, label: str = "Traced Model") -> None:
super().__init__("PyTorchScript", label)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/__init__.py
================================================
from .file_outputs import *
from .generic_outputs import *
from .numpy_outputs import *
try:
from .ncnn_outputs import *
except Exception:
pass
try:
from .onnx_outputs import *
except Exception:
pass
try:
from .pytorch_outputs import *
except Exception:
pass
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/file_outputs.py
================================================
from __future__ import annotations
from pathlib import Path
import navi
from api import BaseOutput
class DirectoryOutput(BaseOutput[Path]):
"""Output for saving to a directory"""
def __init__(
self,
label: str = "Directory",
of_input: int | None = None,
output_type: str = "Directory",
) -> None:
directory_type = (
"Directory"
if of_input is None
else f"splitFilePath(Input{of_input}.path).dir"
)
directory_type = navi.intersect_with_error(directory_type, output_type)
super().__init__(directory_type, label, associated_type=Path)
def get_broadcast_type(self, value: Path):
return navi.named("Directory", {"path": navi.literal(str(value))})
def enforce(self, value: object) -> Path:
assert isinstance(value, Path)
return value
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/generic_outputs.py
================================================
from __future__ import annotations
from typing import Union
import navi
from api import BaseOutput
from ...impl.color.color import Color
from ...utils.format import format_color_with_channels
from ...utils.seed import Seed
class NumberOutput(BaseOutput[Union[int, float]]):
def __init__(
self,
label: str,
output_type: navi.ExpressionJson = "number",
) -> None:
super().__init__(
navi.intersect_with_error("number", output_type),
label,
associated_type=Union[int, float],
)
def get_broadcast_type(self, value: int | float):
return navi.literal(value)
def enforce(self, value: object) -> int | float:
assert isinstance(value, int | float)
return value
class TextOutput(BaseOutput):
def __init__(
self,
label: str,
output_type: navi.ExpressionJson = "string",
) -> None:
super().__init__(navi.intersect_with_error("string", output_type), label)
def get_broadcast_type(self, value: str):
return navi.literal(value)
def enforce(self, value: object) -> str:
assert isinstance(value, str)
return value
def FileNameOutput(label: str = "Name", of_input: int | None = None):
output_type = (
"string"
if of_input is None
else f"splitFilePath(Input{of_input}.path).basename"
)
return TextOutput(label=label, output_type=output_type)
class SeedOutput(BaseOutput):
def __init__(self, label: str = "Seed") -> None:
super().__init__(output_type="Seed", label=label, kind="generic")
def enforce(self, value: object) -> Seed:
assert isinstance(value, Seed)
return value
class ColorOutput(BaseOutput):
def __init__(
self,
label: str = "Color",
color_type: navi.ExpressionJson = "Color",
channels: int | None = None,
) -> None:
super().__init__(
output_type=navi.intersect_with_error(
color_type, navi.Color(channels=channels)
),
label=label,
kind="generic",
)
self.channels = channels
def enforce(self, value: object) -> Color:
assert isinstance(value, Color)
if self.channels is not None and value.channels != self.channels:
expected = format_color_with_channels([self.channels])
actual = format_color_with_channels([value.channels])
raise ValueError(
f"The output {self.label} was supposed to return {expected} but actually returned {actual}."
f" This is a bug in the implementation of the node."
f" Please report this bug."
)
return value
class BoolOutput(BaseOutput):
def __init__(
self,
label: str = "Logical",
*,
output_type: navi.ExpressionJson = "bool",
) -> None:
super().__init__(
output_type=navi.intersect_with_error("bool", output_type),
label=label,
kind="generic",
)
class AudioStreamOutput(BaseOutput):
def __init__(self, label: str = "Audio Stream") -> None:
super().__init__(
output_type="AudioStream",
label=label,
kind="generic",
)
class AnyOutput(BaseOutput):
def __init__(
self, label: str = "Any", output_type: navi.ExpressionJson = "Any"
) -> None:
super().__init__(
output_type=output_type,
label=label,
kind="generic",
)
def enforce(self, value: object) -> object:
return value
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/ncnn_outputs.py
================================================
import navi
from api import BaseOutput, OutputKind
from ...impl.ncnn.model import NcnnModelWrapper
from ...utils.format import format_channel_numbers
class NcnnModelOutput(BaseOutput):
def __init__(
self,
model_type: navi.ExpressionJson = "NcnnNetwork",
label: str = "Model",
kind: OutputKind = "generic",
) -> None:
super().__init__(model_type, label, kind=kind, associated_type=NcnnModelWrapper)
def get_broadcast_data(self, value: NcnnModelWrapper):
return {
"tags": [
format_channel_numbers(value.in_nc, value.out_nc),
f"{value.nf}nf",
value.fp,
]
}
def get_broadcast_type(self, value: NcnnModelWrapper):
return navi.named(
"NcnnNetwork",
{
"scale": value.scale,
"inputChannels": value.in_nc,
"outputChannels": value.out_nc,
"nf": value.nf,
"fp": navi.literal(value.fp),
},
)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/numpy_outputs.py
================================================
from __future__ import annotations
import base64
import cv2
import navi
import numpy as np
from api import BaseOutput, BroadcastData, InputId, OutputKind
from ...impl.image_utils import normalize, to_uint8
from ...impl.resize import ResizeFilter, resize
from ...utils.format import format_image_with_channels
from ...utils.utils import get_h_w_c, round_half_up
class NumPyOutput(BaseOutput[np.ndarray]):
"""Output a NumPy array"""
def __init__(
self,
output_type: navi.ExpressionJson,
label: str,
kind: OutputKind = "generic",
has_handle: bool = True,
) -> None:
super().__init__(
output_type,
label,
kind=kind,
has_handle=has_handle,
associated_type=np.ndarray,
)
def enforce(self, value: object) -> np.ndarray:
assert isinstance(value, np.ndarray)
return value
def AudioOutput():
"""Output a 1D Audio NumPy array"""
return NumPyOutput("Audio", "Audio")
class ImageOutput(NumPyOutput):
def __init__(
self,
label: str = "Image",
*,
image_type: navi.ExpressionJson = "Image",
kind: OutputKind = "generic",
has_handle: bool = True,
channels: int | None = None,
shape_as: int | InputId | None = None,
size_as: int | InputId | None = None,
assume_normalized: bool = False,
) -> None:
# narrow down type
if channels is not None:
image_type = navi.intersect_with_error(
image_type, navi.Image(channels=channels)
)
if shape_as is not None:
image_type = navi.intersect_with_error(image_type, f"Input{shape_as}")
if size_as is not None:
image_type = navi.intersect_with_error(
image_type, navi.Image(size_as=f"Input{size_as}")
)
super().__init__(image_type, label, kind=kind, has_handle=has_handle)
self.channels: int | None = channels
self.assume_normalized: bool = assume_normalized
if shape_as is not None:
self.as_passthrough_of(shape_as)
def get_broadcast_data(self, value: np.ndarray) -> BroadcastData:
h, w, c = get_h_w_c(value)
return {
"height": h,
"width": w,
"channels": c,
}
def get_broadcast_type(self, value: np.ndarray):
h, w, c = get_h_w_c(value)
return navi.Image(width=w, height=h, channels=c)
def enforce(self, value: object) -> np.ndarray:
assert isinstance(value, np.ndarray)
h, w, c = get_h_w_c(value)
if h == 0 or w == 0:
raise ValueError(
f"The output {self.label} returned an empty image (w={w} h={h})."
f" This is a bug in the implementation of the node."
f" Please report this bug."
)
if self.channels is not None and c != self.channels:
expected = format_image_with_channels([self.channels])
actual = format_image_with_channels([c])
raise ValueError(
f"The output {self.label} was supposed to return {expected} but actually returned {actual}."
f" This is a bug in the implementation of the node."
f" Please report this bug."
)
# flatting 3D single-channel images to 2D
if c == 1 and value.ndim == 3:
value = value[:, :, 0]
if not self.assume_normalized:
value = normalize(value)
assert value.dtype == np.float32, (
f"The output {self.label} did not return a normalized image."
f" This is a bug in the implementation of the node."
f" Please report this bug."
f"\n\nTo the author of this node: Either use `normalize` or remove `assume_normalized=True` from this output."
)
# make image readonly
value.setflags(write=False)
return value
def preview_encode(
img: np.ndarray,
target_size: int = 512,
grace: float = 1.2,
lossless: bool = False,
) -> tuple[str, np.ndarray]:
"""
resize the image, so the preview loads faster and doesn't lag the UI
512 was chosen as the default target because a 512x512 RGBA 8bit PNG is at most 1MB in size
"""
h, w, c = get_h_w_c(img)
max_size = target_size * grace
if w > max_size or h > max_size:
f = max(w / target_size, h / target_size)
t = (max(1, round_half_up(w / f)), max(1, round_half_up(h / f)))
img = resize(img, t, ResizeFilter.BOX)
image_format = "png" if c > 3 or lossless else "jpg"
_, encoded_img = cv2.imencode(f".{image_format}", to_uint8(img, normalized=True)) # type: ignore
base64_img = base64.b64encode(encoded_img).decode("utf8") # type: ignore
return f"data:image/{image_format};base64,{base64_img}", img
class LargeImageOutput(ImageOutput):
def __init__(
self,
label: str = "Image",
image_type: navi.ExpressionJson = "Image",
kind: OutputKind = "large-image",
has_handle: bool = True,
assume_normalized: bool = False,
) -> None:
super().__init__(
label,
image_type=image_type,
kind=kind,
has_handle=has_handle,
assume_normalized=assume_normalized,
)
def get_broadcast_data(self, value: np.ndarray):
img = value
h, w, c = get_h_w_c(img)
image_size = max(h, w)
preview_sizes = [2048, 1024, 512, 256]
preview_size_grace = 1.2
start_index = len(preview_sizes) - 1
for i, size in enumerate(preview_sizes):
if size <= image_size and image_size <= size * preview_size_grace:
# this preview size will perfectly fit the image
start_index = i
break
if image_size > size:
# the image size is larger than the preview size, so try to pick the previous size
start_index = max(0, i - 1)
break
previews = []
# Encode for multiple scales. Use the preceding scale to save time encoding the smaller sizes.
last_encoded = img
for size in preview_sizes[start_index:]:
largest_preview = size == preview_sizes[start_index]
url, last_encoded = preview_encode(
last_encoded,
target_size=size,
grace=preview_size_grace,
lossless=largest_preview,
)
le_h, le_w, _ = get_h_w_c(last_encoded)
previews.append({"width": le_w, "height": le_h, "url": url})
return {
"previews": previews,
"height": h,
"width": w,
"channels": c,
}
def VideoOutput():
"""Output a 3D Video NumPy array"""
return NumPyOutput("Video", "Video")
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/onnx_outputs.py
================================================
from __future__ import annotations
import navi
from nodes.utils.format import format_channel_numbers
from api import BaseOutput, OutputKind
from ...impl.onnx.model import OnnxModel
class OnnxModelOutput(BaseOutput):
"""Output for onnx model"""
def __init__(
self,
model_type: navi.ExpressionJson = "OnnxModel",
label: str = "Model",
kind: OutputKind = "generic",
) -> None:
super().__init__(model_type, label, kind=kind, associated_type=OnnxModel)
def get_broadcast_data(self, value: OnnxModel):
i = value.info
tags: list[str] = []
if i.input_channels is not None and i.output_channels is not None:
tags.append(format_channel_numbers(i.input_channels, i.output_channels))
tags.append(f"opset{i.opset}")
tags.append(i.dtype)
return {"tags": tags}
def get_broadcast_type(self, value: OnnxModel):
fields = {
"subType": navi.literal(value.sub_type),
}
i = value.info
if i.scale_width is not None:
fields["scaleWidth"] = i.scale_width
if i.scale_height is not None:
fields["scaleHeight"] = i.scale_height
if i.input_channels is not None:
fields["inputChannels"] = i.input_channels
if i.output_channels is not None:
fields["outputChannels"] = i.output_channels
return navi.named("OnnxModel", fields)
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/pytorch_outputs.py
================================================
from __future__ import annotations
import navi
from spandrel import ModelDescriptor, ModelTiling
from api import BaseOutput, OutputKind
from ...utils.format import format_channel_numbers
class ModelOutput(BaseOutput):
def __init__(
self,
model_type: navi.ExpressionJson = "PyTorchModel",
label: str = "Model",
kind: OutputKind = "generic",
) -> None:
super().__init__(model_type, label, kind=kind, associated_type=ModelDescriptor)
def get_broadcast_data(self, value: ModelDescriptor) -> dict[str, list[str]]:
return {
"tags": [
value.architecture.name,
format_channel_numbers(value.input_channels, value.output_channels),
*value.tags,
]
}
def get_broadcast_type(self, value: ModelDescriptor):
tiling_map: dict[ModelTiling, str] = {
ModelTiling.SUPPORTED: "ModelTiling::Supported",
ModelTiling.DISCOURAGED: "ModelTiling::Discouraged",
ModelTiling.INTERNAL: "ModelTiling::Internal",
}
return navi.named(
"PyTorchModel",
{
"scale": value.scale,
"inputChannels": value.input_channels,
"outputChannels": value.output_channels,
"arch": navi.literal(value.architecture.name),
"subType": navi.literal(value.purpose),
"size": navi.literal("x".join(value.tags)),
"tiling": tiling_map[value.tiling],
},
)
def TorchScriptOutput():
"""Output a JIT traced model"""
return BaseOutput("PyTorchScript", "Traced Model")
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/utils/__init__.py
================================================
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/utils/format.py
================================================
from __future__ import annotations
from collections.abc import Callable, Iterable
from typing import Literal, TypeVar
T = TypeVar("T")
Conj = Literal["and", "or"]
def join_english(
items: Iterable[T],
to_str: Callable[[T], str] = str,
conj: Conj = "and",
) -> str:
s = list(map(to_str, items))
l = len(s)
assert l > 0
if l == 1:
return s[0]
if l == 2:
return f"{s[0]} {conj} {s[1]}"
return ", ".join(s[:-1]) + f", {conj} " + s[-1]
def format_image_with_channels(
channels: list[int],
conj: Conj = "and",
plural: bool = False,
) -> str:
assert len(channels) > 0
named = {1: "grayscale", 3: "RGB", 4: "RGBA"}
if all(x in named for x in channels):
if plural:
return join_english(channels, lambda c: named[c], conj=conj) + " images"
else:
return (
"a " + join_english(channels, lambda c: named[c], conj=conj) + " image"
)
if plural:
return f"images with {join_english(channels, conj=conj)} channel(s)"
else:
return f"an image with {join_english(channels, conj=conj)} channel(s)"
def format_color_with_channels(
channels: list[int],
conj: Conj = "and",
plural: bool = False,
) -> str:
assert len(channels) > 0
named = {1: "grayscale", 3: "RGB", 4: "RGBA"}
if all(x in named for x in channels):
if plural:
return join_english(channels, lambda c: named[c], conj=conj) + " colors"
else:
return (
"a " + join_english(channels, lambda c: named[c], conj=conj) + " color"
)
if plural:
return f"color with {join_english(channels, conj=conj)} channel(s)"
else:
return f"a color with {join_english(channels, conj=conj)} channel(s)"
_CHANNEL_NUMBER_NAME = {1: "GRAY", 3: "RGB", 4: "RGBA"}
def format_channel_numbers(input_channels: int, output_channels: int) -> str:
i = _CHANNEL_NUMBER_NAME.get(input_channels, str(input_channels))
o = _CHANNEL_NUMBER_NAME.get(output_channels, str(output_channels))
return f"{i}🠚{o}"
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/utils/seed.py
================================================
from dataclasses import dataclass
from random import Random
_U32_MAX = 4294967296
@dataclass(frozen=True)
class Seed:
value: int
"""
The value of the seed. This value may be signed and generally have any range.
"""
@staticmethod
def from_bytes(b: bytes):
return Seed(Random(b).randint(0, _U32_MAX - 1))
def to_range(self, a: int, b: int) -> int:
"""
Returns the value of the seed within the given range [a,b] both ends inclusive.
If the current seed is not within the given range, a value within the range will be derived from the current seed.
"""
if a <= self.value <= b:
return self.value
return Random(self.value).randint(a, b)
def to_u32(self) -> int:
"""
Returns the value of the seed as a 32bit unsigned integer.
"""
return self.to_range(0, _U32_MAX - 1)
def cache_key_func(self):
return self.value
================================================
FILE: MangaJaNaiConverterGui/backend/src/nodes/utils/utils.py
================================================
# From https://github.com/victorca25/iNNfer/blob/main/utils/utils.py
from __future__ import annotations
import math
import os
import re
from dataclasses import dataclass
from pathlib import Path
import numpy as np
from sanic.log import logger
Size = tuple[int, int]
"""
The width and height (in that order) of an image.
"""
NUMBERS = re.compile(r"(\d+)")
ALPHABET = [*"ABCDEFGHIJKLMNOPQRSTUVWXYZ"]
def round_half_up(number: float | int) -> int:
"""
Python's `round` method implements round-half-to-even rounding which is very unintuitive.
This function implements round-half-up rounding.
Round half up is consistent with JavaScript's `Math.round`.
https://en.wikipedia.org/wiki/Rounding#Rounding_to_the_nearest_integer
"""
return math.floor(number + 0.5)
def get_h_w_c(image: np.ndarray) -> tuple[int, int, int]:
"""Returns the height, width, and number of channels."""
h, w = image.shape[:2]
c = 1 if image.ndim == 2 else image.shape[2]
return h, w, c
def alphanumeric_sort(value: str) -> list[str | int]:
"""Key function to sort strings containing numbers by proper
numerical order."""
lcase_value = value.upper()
parts = NUMBERS.split(lcase_value)
parts[1::2] = map(int, parts[1::2])
return parts # type: ignore
__SPLIT_SNAKE_CASE = re.compile(r"(\d+|_+)")
__SPLIT_PASCAL_CASE = re.compile(r"(\d+)|(?<=[a-z])(?=[A-Z])")
def split_snake_case(s: str) -> list[str]:
"""Splits a snake case identifier into its parts. E.g. `SNAKE_CASE` -> [`snake`, `case`]"""
return [
x.lower() for x in __SPLIT_SNAKE_CASE.split(s) if x and not x.startswith("_")
]
def split_pascal_case(s: str) -> list[str]:
"""Splits a snake case identifier into its parts. E.g. `SNAKE_CASE` -> [`snake`, `case`]"""
return [
x.lower() for x in __SPLIT_PASCAL_CASE.split(s) if x and not x.startswith("_")
]
def join_pascal_case(words: list[str]) -> str:
return "".join([x.capitalize() for x in words])
__ABBREVIATIONS = {"rgb", "rgba"}
def smart_capitalize(word: str) -> str:
if word in __ABBREVIATIONS:
return word.upper()
return word.capitalize()
def join_space_case(words: list[str]) -> str:
return " ".join([smart_capitalize(x) for x in words])
def split_file_path(path: Path | str) -> tuple[Path, str, str]:
"""
Returns the base directory, file name, and extension of the given file path.
"""
base, ext = os.path.splitext(path)
dirname, basename = os.path.split(base)
return Path(dirname), basename, ext
def walk_error_handler(exception_instance: Exception) -> None:
logger.warning(
f"Exception occurred during walk: {exception_instance} Continuing..."
)
def list_all_files_sorted(
directory: Path, ext_filter: list[str] | None = None
) -> list[Path]:
just_files: list[Path] = []
for root, dirs, files in os.walk(
directory, topdown=True, onerror=walk_error_handler
):
dirs.sort(key=alphanumeric_sort)
for name in sorted(files, key=alphanumeric_sort):
filepath = os.path.join(root, name)
_base, ext = os.path.splitext(filepath)
if ext_filter is None or ext.lower() in ext_filter:
just_files.append(Path(filepath))
return just_files
@dataclass(frozen=True)
class Padding:
top: int
right: int
bottom: int
left: int
@staticmethod
def all(value: int) -> Padding:
return Padding(value, value, value, value)
@staticmethod
def to(value: Padding | int) -> Padding:
if isinstance(value, int):
return Padding.all(value)
return value
@property
def horizontal(self) -> int:
return self.left + self.right
@property
def vertical(self) -> int:
return self.top + self.bottom
@property
def empty(self) -> bool:
return self.top == 0 and self.right == 0 and self.bottom == 0 and self.left == 0
def scale(self, factor: int) -> Padding:
return Padding(
self.top * factor,
self.right * factor,
self.bottom * factor,
self.left * factor,
)
def min(self, other: Padding | int) -> Padding:
other = Padding.to(other)
return Padding(
min(self.top, other.top),
min(self.right, other.right),
min(self.bottom, other.bottom),
min(self.left, other.left),
)
def remove_from(self, image: np.ndarray) -> np.ndarray:
h, w, _ = get_h_w_c(image)
return image[
self.top : (h - self.bottom),
self.left : (w - self.right),
...,
]
@dataclass(frozen=True)
class Region:
x: int
y: int
width: int
height: int
@property
def size(self) -> Size:
return self.width, self.height
def scale(self, factor: int) -> Region:
return Region(
self.x * factor,
self.y * factor,
self.width * factor,
self.height * factor,
)
def intersect(self, other: Region) -> Region:
x = max(self.x, other.x)
y = max(self.y, other.y)
width = min(self.x + self.width, other.x + other.width) - x
height = min(self.y + self.height, other.y + other.height) - y
return Region(x, y, width, height)
def add_padding(self, pad: Padding) -> Region:
return Region(
x=self.x - pad.left,
y=self.y - pad.top,
width=self.width + pad.horizontal,
height=self.height + pad.vertical,
)
def remove_padding(self, pad: Padding) -> Region:
return self.add_padding(pad.scale(-1))
def child_padding(self, child: Region) -> Padding:
"""
Returns the padding `p` such that `child.add_padding(p) == self`.
"""
left = child.x - self.x
top = child.y - self.y
right = self.width - child.width - left
bottom = self.height - child.height - top
return Padding(top, right, bottom, left)
def read_from(self, image: np.ndarray) -> np.ndarray:
h, w, _ = get_h_w_c(image)
if (w, h) == self.size:
return image
return image[
self.y : (self.y + self.height),
self.x : (self.x + self.width),
...,
]
def write_into(self, lhs: np.ndarray, rhs: np.ndarray) -> None:
h, w, c = get_h_w_c(rhs)
assert (w, h) == self.size
assert c == get_h_w_c(lhs)[2]
if c == 1:
if lhs.ndim == 2 and rhs.ndim == 3:
rhs = rhs[:, :, 0]
if lhs.ndim == 3 and rhs.ndim == 2:
rhs = np.expand_dims(rhs, axis=2)
lhs[
self.y : (self.y + self.height),
self.x : (self.x + self.width),
...,
] = rhs
================================================
FILE: MangaJaNaiConverterGui/backend/src/packages/chaiNNer_pytorch/__init__.py
================================================
import os
from accelerator_detection import AcceleratorType, get_accelerator_detector
from gpu import nvidia
from sanic.log import logger
from system import is_arm_mac
from api import GB, KB, MB, Dependency, add_package
# Get available accelerators
detector = get_accelerator_detector()
available_devices = detector.available_devices
gpu_devices = [d for d in available_devices if d.type != AcceleratorType.CPU]
# Build description based on available accelerators
accelerator_names = []
if any(d.type == AcceleratorType.CUDA for d in gpu_devices):
accelerator_names.append("NVIDIA CUDA")
if any(d.type == AcceleratorType.ROCM for d in gpu_devices):
accelerator_names.append("AMD ROCm")
if any(d.type == AcceleratorType.XPU for d in gpu_devices):
accelerator_names.append("Intel XPU")
if any(d.type == AcceleratorType.MPS for d in gpu_devices):
accelerator_names.append("Apple MPS")
general = "PyTorch uses .pth models to upscale images."
if is_arm_mac:
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
package_description = f"{general} Optimized for Apple Silicon with MPS acceleration."
inst_hint = f"{general} It is the most widely-used upscaling architecture and supports Apple Silicon acceleration."
elif accelerator_names:
accelerator_list = ", ".join(accelerator_names)
package_description = f"{general} Supports hardware acceleration with: {accelerator_list}."
inst_hint = f"{general} It is the most widely-used upscaling architecture and supports multiple accelerators including {accelerator_list}."
else:
package_description = f"{general} Running on CPU (no hardware accelerators detected)."
inst_hint = f"{general} It is the most widely-used upscaling architecture. No hardware accelerators were detected, so it will run on CPU (which is slow)."
def get_pytorch():
if is_arm_mac:
return [
Dependency(
display_name="PyTorch",
pypi_name="torch",
version="2.1.2",
size_estimate=55.8 * MB,
auto_update=False,
),
Dependency(
display_name="TorchVision",
pypi_name="torchvision",
version="0.16.2",
size_estimate=1.3 * MB,
auto_update=False,
),
]
else:
return [
Dependency(
display_name="PyTorch",
pypi_name="torch",
version="2.1.2+cu121" if nvidia.is_available else "2.1.2",
size_estimate=2 * GB if nvidia.is_available else 140 * MB,
extra_index_url=(
"https://download.pytorch.org/whl/cu121"
if nvidia.is_available
else "https://download.pytorch.org/whl/cpu"
),
auto_update=False,
),
Dependency(
display_name="TorchVision",
pypi_name="torchvision",
version="0.16.2+cu121" if nvidia.is_available else "0.16.2",
size_estimate=2 * MB if nvidia.is_available else 800 * KB,
extra_index_url=(
"https://download.pytorch.org/whl/cu121"
if nvidia.is_available
else "https://download.pytorch.org/whl/cpu"
),
auto_update=False,
),
]
package = add_package(
__file__,
id="chaiNNer_pytorch",
name="PyTorch",
description=package_description,
dependencies=[
*get_pytorch(),
Dependency(
display_name="FaceXLib",
pypi_name="facexlib",
version="0.3.0",
size_estimate=59.6 * KB,
),
Dependency(
display_name="Einops",
pypi_name="einops",
version="0.6.1",
size_estimate=42.2 * KB,
),
Dependency(
display_name="safetensors",
pypi_name="safetensors",
version="0.4.0",
size_estimate=1 * MB,
),
Dependency(
display_name="Spandrel",
pypi_name="spandrel",
version="0.3.4",
size_estimate=264 * KB,
),
Dependency(
display_name="Spandrel extra architectures",
pypi_name="spandrel_extra_arches",
version="0.1.1",
size_estimate=83 * KB,
),
],
icon="PyTorch",
color="#DD6B20",
)
pytorch_category = package.add_category(
name="PyTorch",
description="Nodes for using the PyTorch Neural Network Framework with images.",
icon="PyTorch",
color="#DD6B20",
install_hint=inst_hint,
)
logger.debug(f"Loaded package {package.name}")
================================================
FILE: MangaJaNaiConverterGui/backend/src/packages/chaiNNer_pytorch/pytorch/__init__.py
================================================
from .. import pytorch_category
io_group = pytorch_category.add_node_group("Input & Output")
processing_group = pytorch_category.add_node_group("Processing")
restoration_group = pytorch_category.add_node_group("Restoration")
batch_processing_group = pytorch_category.add_node_group("Batch Processing")
utility_group = pytorch_category.add_node_group("Utility")
processing_group.order = [
"chainner:pytorch:upscale_image",
"chainner:pytorch:inpaint",
]
================================================
FILE: MangaJaNaiConverterGui/backend/src/packages/chaiNNer_pytorch/pytorch/io/load_model.py
================================================
from __future__ import annotations
import os
from pathlib import Path
import torch
from nodes.properties.inputs import PthFileInput
from nodes.properties.outputs import DirectoryOutput, FileNameOutput, ModelOutput
from nodes.utils.utils import split_file_path
from sanic.log import logger
from spandrel import MAIN_REGISTRY, ModelDescriptor, ModelLoader
from spandrel_extra_arches import EXTRA_REGISTRY
from api import NodeContext
from ...settings import get_settings
from .. import io_group
MAIN_REGISTRY.add(*EXTRA_REGISTRY)
def parse_ckpt_state_dict(checkpoint: dict):
state_dict = {}
for i, j in checkpoint.items():
if "netG." in i:
key = i.replace("netG.", "")
state_dict[key] = j
elif "module." in i:
key = i.replace("module.", "")
state_dict[key] = j
return state_dict
@io_group.register(
schema_id="chainner:pytorch:load_model",
name="Load Model",
description=[
(
"Load PyTorch state dict (.pth), TorchScript (.pt), or Checkpoint (.ckpt) files into an"
" auto-detected supported model architecture."
),
(
"- For Super-Resolution, we support most variations of the RRDB"
" architecture (ESRGAN, Real-ESRGAN, RealSR, BSRGAN, SPSR), Real-ESRGAN's"
" SRVGG architecture, Swift-SRGAN, SwinIR, Swin2SR, HAT, Omni-SR, SRFormer, and DAT."
),
(
"- For Face-Restoration, we support GFPGAN (1.2, 1.3, 1.4), RestoreFormer,"
" and CodeFormer."
),
"- For Inpainting, we support LaMa and MAT.",
(
"Links to the official models can be found in [chaiNNer's"
" README](https://github.com/chaiNNer-org/chaiNNer#pytorch), and"
" community-trained models on [OpenModelDB](https://openmodeldb.info/)."
),
],
icon="PyTorch",
inputs=[PthFileInput(primary_input=True)],
outputs=[
ModelOutput(kind="tagged").suggest(),
DirectoryOutput("Directory", of_input=0).with_id(2),
FileNameOutput("Name", of_input=0).with_id(1),
],
node_context=True,
see_also=[
"chainner:pytorch:load_models",
],
side_effects=True,
)
def load_model_node(
context: NodeContext, path: Path
) -> tuple[ModelDescriptor, Path, str]:
assert os.path.exists(path), f"Model file at location {path} does not exist"
assert os.path.isfile(path), f"Path {path} is not a file"
exec_options = get_settings(context)
pytorch_device = exec_options.device
try:
logger.debug(f"Reading state dict from path: {path}")
model_descriptor = ModelLoader(pytorch_device).load_from_file(path)
for _, v in model_descriptor.model.named_parameters():
v.requires_grad = False
model_descriptor.model.eval()
model_descriptor = model_descriptor.to(pytorch_device)
# if should_use_fp16:
# model_descriptor.model.half()
# else:
# model_descriptor.model.float()
if exec_options.use_fp16:
if model_descriptor.supports_half:
model_descriptor.model.half()
elif torch.cuda.is_bf16_supported():
model_descriptor.model.bfloat16()
else:
model_descriptor.model.float()
else:
model_descriptor.model.float()
except Exception as e:
raise ValueError(
f"Model {os.path.basename(path)} is unsupported by chaiNNer. Please try"
" another."
) from e
dirname, basename, _ = split_file_path(path)
return model_descriptor, dirname, basename
================================================
FILE: MangaJaNaiConverterGui/backend/src/packages/chaiNNer_pytorch/pytorch/processing/upscale_image.py
================================================
from __future__ import annotations
import weakref
import numpy as np
import psutil
import torch
from accelerator_detection import AcceleratorType
from nodes.groups import Condition, if_enum_group, if_group
from nodes.impl.pytorch.auto_split import pytorch_auto_split
from nodes.impl.pytorch.utils import safe_accelerator_cache_empty
from nodes.impl.upscale.auto_split_tiles import (
CUSTOM,
NO_TILING,
TILE_SIZE_256,
TileSize,
estimate_tile_size,
parse_tile_size_input,
)
from nodes.impl.upscale.basic_upscale import UpscaleInfo, basic_upscale
from nodes.impl.upscale.tiler import MaxTileSize
from nodes.properties.inputs import (
BoolInput,
ImageInput,
NumberInput,
SrModelInput,
TileSizeDropdown,
)
from nodes.properties.outputs import ImageOutput
from sanic.log import logger
from spandrel import ImageModelDescriptor, ModelTiling
from api import KeyInfo, NodeContext, Progress
from ...settings import PyTorchSettings, get_settings
from .. import processing_group
MODEL_BYTES_CACHE = weakref.WeakKeyDictionary()
def upscale(
img: np.ndarray,
model: ImageModelDescriptor,
tile_size: TileSize,
options: PyTorchSettings,
progress: Progress,
):
with torch.no_grad():
# Borrowed from iNNfer
logger.debug("Upscaling image")
# TODO: use bfloat16 if RTX
use_fp16 = options.use_fp16 # and model.supports_half
device = options.device
if model.tiling == ModelTiling.INTERNAL:
# disable tiling if the model already does it internally
tile_size = NO_TILING
def estimate():
model_bytes = MODEL_BYTES_CACHE.get(model)
if model_bytes is None:
model_bytes = sum(p.numel() * 4 for p in model.model.parameters())
MODEL_BYTES_CACHE[model] = model_bytes
device_type = device.type
accelerator_device = options.accelerator_device
# Memory estimation for different accelerator types
if device_type in ["cuda", "rocm"]: # CUDA/ROCm
if options.use_fp16:
model_bytes = model_bytes // 2
try:
mem_info: tuple[int, int] = torch.cuda.mem_get_info(device) # type: ignore
_free, total = mem_info
# only use 75% of the total memory
total = int(total * 0.75)
if options.budget_limit > 0:
total = min(options.budget_limit * 1024**3, total)
# Estimate using 80% of the value to be more conservative
budget = int(total * 0.8)
return MaxTileSize(
estimate_tile_size(
budget,
model_bytes,
img,
2 if use_fp16 else 4,
)
)
except Exception:
# Fallback if memory info fails
return MaxTileSize()
elif device_type == "xpu": # Intel XPU
if options.use_fp16:
model_bytes = model_bytes // 2
try:
if hasattr(torch.xpu, 'mem_get_info'):
mem_info = torch.xpu.mem_get_info(device)
_free, total = mem_info
total = int(total * 0.75)
if options.budget_limit > 0:
total = min(options.budget_limit * 1024**3, total)
budget = int(total * 0.8)
return MaxTileSize(
estimate_tile_size(
budget,
model_bytes,
img,
2 if use_fp16 else 4,
)
)
except Exception:
pass
# Fallback for XPU without memory info
return MaxTileSize()
elif device_type == "mps": # Apple MPS
# MPS doesn't have direct memory querying, use conservative estimation
# Assume 8GB unified memory with 50% available for inference
estimated_budget = 4 * 1024**3 # 4GB conservative estimate
if options.budget_limit > 0:
estimated_budget = min(options.budget_limit * 1024**3, estimated_budget)
budget = int(estimated_budget * 0.8)
return MaxTileSize(
estimate_tile_size(
budget,
model_bytes,
img,
2 if use_fp16 else 4,
)
)
elif device_type == "cpu":
free = psutil.virtual_memory().available
if options.budget_limit > 0:
free = min(options.budget_limit * 1024**3, free)
budget = int(free * 0.8)
return MaxTileSize(
estimate_tile_size(
budget,
model_bytes,
img,
4, # CPU always uses FP32
)
)
else:
# For other device types, use conservative estimation
estimated_budget = 2 * 1024**3 # 2GB conservative estimate
if options.budget_limit > 0:
estimated_budget = min(options.budget_limit * 1024**3, estimated_budget)
budget = int(estimated_budget * 0.8)
return MaxTileSize(
estimate_tile_size(
budget,
model_bytes,
img,
2 if use_fp16 else 4,
)
)
img_out = pytorch_auto_split(
img,
model=model,
device=device,
use_fp16=use_fp16,
tiler=parse_tile_size_input(tile_size, estimate),
progress=progress,
)
logger.debug("Done upscaling")
return img_out
@processing_group.register(
schema_id="chainner:pytorch:upscale_image",
name="Upscale Image",
description=(
"Upscales an image using a PyTorch Super-Resolution model. Select a"
" manual number of tiles if you are having issues with the automatic mode. "
),
icon="PyTorch",
inputs=[
ImageInput().with_id(1),
SrModelInput().with_id(0),
if_group(
Condition.type(0, "PyTorchModel { scale: int(2..) }", if_not_connected=True)
& (
Condition.type(
0,
"PyTorchModel { inputChannels: 1, outputChannels: 1 }",
if_not_connected=True,
)
| Condition.type(
0, "PyTorchModel { inputChannels: 3, outputChannels: 3 }"
)
| Condition.type(
0, "PyTorchModel { inputChannels: 4, outputChannels: 4 }"
)
)
)(
BoolInput("Custom Scale", default=False)
.with_id(4)
.with_docs(
"If enabled, the scale factor can be manually set. This makes it possible to e.g. upscale 4x with a 2x model.",
"Custom scales are **not** supported for 1x models and colorization models.",
"Under the hood, this will repeatedly apply the model to the image, effectively upscaling by the given factor."
" E.g. if the model is 2x and the desired scale is 4x, the model will be applied 2 times."
" If the desired scale cannot be reached exactly, the image will be downscaled to the desired scale after upscaling."
" E.g. if the model is 2x and the desired scale is 6x, the model will be applied 3 times (8x) and the image will be downscaled to 6x.",
"If the desired scale is less than the model's scale, the image will be downscaled to the desired scale after upscaling.",
hint=True,
),
if_group(Condition.bool(4, True))(
NumberInput(
"Scale", default=4, min=1, max=32, label_style="hidden"
).with_id(5),
),
),
if_group(
Condition.type(
0,
"PyTorchModel { tiling: ModelTiling::Supported | ModelTiling::Discouraged } ",
if_not_connected=True,
)
)(
TileSizeDropdown()
.with_id(2)
.with_docs(
"Tiled upscaling is used to allow large images to be upscaled without"
" hitting memory limits.",
"This works by splitting the image into tiles (with overlap), upscaling"
" each tile individually, and seamlessly recombining them.",
"Generally it's recommended to use the largest tile size possible for"
" best performance (with the ideal scenario being no tiling at all),"
" but depending on the model and image size, this may not be possible.",
"If you are having issues with the automatic mode, you can manually"
" select a tile size. Sometimes, a manually selected tile size may be"
" faster than what the automatic mode picks.",
hint=True,
),
if_enum_group(2, CUSTOM)(
NumberInput(
"Custom Tile Size",
min=1,
max=None,
default=TILE_SIZE_256,
unit="px",
).with_id(6),
),
),
if_group(
Condition.type(1, "Image { channels: 4 } ")
& (
Condition.type(
0, "PyTorchModel { inputChannels: 1, outputChannels: 1 }"
)
| Condition.type(
0, "PyTorchModel { inputChannels: 3, outputChannels: 3 }"
)
)
)(
BoolInput("Separate Alpha", default=False)
.with_id(3)
.with_docs(
"Upscale alpha separately from color. Enabling this option will cause the alpha of"
" the upscaled image to be less noisy and more accurate to the alpha of the original"
" image, but the image may suffer from dark borders near transparency edges"
" (transition from fully transparent to fully opaque).",
"Whether enabling this option will improve the upscaled image depends on the original"
" image. We generally recommend this option for images with smooth transitions between"
" transparent and opaque regions.",
)
),
],
outputs=[
ImageOutput(
"Image",
image_type="""
let img = Input1;
let model = Input0;
let useCustomScale = Input4;
let customScale = Input5;
let singleUpscale = convenientUpscale(model, img);
if useCustomScale and model.scale >= 2 and model.inputChannels == model.outputChannels {
Image {
width: img.width * customScale,
height: img.height * customScale,
channels: singleUpscale.channels,
}
} else {
singleUpscale
}
""",
assume_normalized=True, # pytorch_auto_split already does clipping internally
)
],
key_info=KeyInfo.type(
"""
let model = Input0;
let useCustomScale = Input4;
let customScale = Input5;
let singleUpscale = convenientUpscale(model, img);
let scale = if useCustomScale and model.scale >= 2 and model.inputChannels == model.outputChannels {
customScale
} else {
model.scale
};
string::concat(toString(scale), "x")
"""
),
node_context=True,
)
def upscale_image_node(
context: NodeContext,
img: np.ndarray,
model: ImageModelDescriptor,
use_custom_scale: bool,
custom_scale: int,
tile_size: TileSize,
custom_tile_size: int,
separate_alpha: bool,
) -> np.ndarray:
exec_options = get_settings(context)
context.add_cleanup(
lambda: safe_accelerator_cache_empty(exec_options.device),
after="node" if exec_options.force_cache_wipe else "chain",
)
info = UpscaleInfo(
in_nc=model.input_channels, out_nc=model.output_channels, scale=model.scale
)
if not use_custom_scale or not info.supports_custom_scale:
custom_scale = model.scale
return basic_upscale(
img,
lambda i: upscale(
i,
model,
TileSize(custom_tile_size) if tile_size == CUSTOM else tile_size,
exec_options,
context,
),
upscale_info=info,
scale=custom_scale,
separate_alpha=separate_alpha,
clip=False, # pytorch_auto_split already does clipping internally
)
================================================
FILE: MangaJaNaiConverterGui/backend/src/packages/chaiNNer_pytorch/settings.py
================================================
from dataclasses import dataclass
import torch
from accelerator_detection import AcceleratorType, get_accelerator_detector
from gpu import nvidia
from sanic.log import logger
from system import is_arm_mac
from api import DropdownSetting, NodeContext, NumberSetting, ToggleSetting
from . import package
# Get all available accelerator devices
detector = get_accelerator_detector()
all_devices = detector.available_devices
# Create device options, excluding CPU for the dropdown
gpu_devices = [device for device in all_devices if device.type != AcceleratorType.CPU]
if gpu_devices:
package.add_setting(
DropdownSetting(
label="Accelerator Device",
key="accelerator_device_index",
description=(
"Which accelerator device to use for PyTorch. This includes NVIDIA CUDA, "
"AMD ROCm, Intel XPU, Apple MPS, and other supported accelerators."
),
options=[{
"label": f"{device.name} ({device.type.value.upper()}:{device.index})",
"value": str(i)
} for i, device in enumerate(gpu_devices)],
default="0",
)
)
# Legacy GPU index setting for backward compatibility (CUDA only)
if not is_arm_mac:
cuda_devices = detector.get_devices_by_type(AcceleratorType.CUDA)
if cuda_devices:
package.add_setting(
DropdownSetting(
label="CUDA GPU (Legacy)",
key="gpu_index",
description=(
"Which CUDA GPU to use for PyTorch. This setting is deprecated - "
"use 'Accelerator Device' instead for full accelerator support."
),
options=[{"label": device.name, "value": str(device.index)} for device in cuda_devices],
default="0",
)
)
package.add_setting(
ToggleSetting(
label="Use CPU Mode",
key="use_cpu",
description=(
"Use CPU for PyTorch instead of accelerator devices. This is much slower "
"and not recommended unless you have no compatible accelerator."
),
default=False,
),
)
# Determine default FP16 setting based on available devices
should_fp16 = False
gpu_devices = [device for device in all_devices if device.type != AcceleratorType.CPU]
if gpu_devices:
# Enable FP16 by default if any GPU supports it
should_fp16 = any(device.supports_fp16 for device in gpu_devices)
elif nvidia.is_available:
should_fp16 = nvidia.all_support_fp16
else:
should_fp16 = is_arm_mac
package.add_setting(
ToggleSetting(
label="Use FP16 Mode",
key="use_fp16",
description=(
"Runs PyTorch in half-precision (FP16) mode for reduced memory usage. "
"Automatically falls back to bfloat16 or FP32 when FP16 is not supported. "
"Falls back to full-precision (FP32) mode when CPU mode is selected."
),
default=should_fp16,
),
)
package.add_setting(
NumberSetting(
label="Memory Budget Limit (GiB)",
key="budget_limit",
description="Maximum memory (VRAM if GPU, RAM if CPU) to use for PyTorch inference. 0 means no limit. Memory usage measurement is not completely accurate yet; you may need to significantly adjust this budget limit via trial-and-error if it's not having the effect you want.",
default=0,
min=0,
max=1024**2,
)
)
# Add cache wipe setting for accelerator types that support it
has_accelerator_with_cache = any(
device.type in [AcceleratorType.CUDA, AcceleratorType.ROCM, AcceleratorType.XPU]
for device in all_devices
)
if has_accelerator_with_cache:
package.add_setting(
ToggleSetting(
label="Force Accelerator Cache Wipe (not recommended)",
key="force_cache_wipe",
description="Clears PyTorch's accelerator cache after each inference. This is NOT recommended, as it interferes with how PyTorch is intended to work and can significantly slow down inference time. Only enable this if you're experiencing issues with memory allocation.",
default=False,
)
)
@dataclass(frozen=True)
class PyTorchSettings:
use_cpu: bool
use_fp16: bool
gpu_index: int # Legacy CUDA index
accelerator_device_index: int # New unified accelerator index
budget_limit: int
force_cache_wipe: bool = False
# PyTorch 2.7 does not support FP16 when using CPU
def __post_init__(self):
if self.use_cpu and self.use_fp16:
object.__setattr__(self, "use_fp16", False)
logger.info("Falling back to FP32 mode for CPU.")
@property
def device(self) -> torch.device:
"""Get the appropriate torch device"""
detector = get_accelerator_detector()
# CPU override
if self.use_cpu:
return torch.device("cpu")
# Try to use the new accelerator device index first
gpu_devices = [device for device in detector.available_devices if device.type != AcceleratorType.CPU]
if gpu_devices and 0 <= self.accelerator_device_index < len(gpu_devices):
selected_device = gpu_devices[self.accelerator_device_index]
return selected_device.torch_device
# Fallback to legacy CUDA device selection for backward compatibility
cuda_devices = detector.get_devices_by_type(AcceleratorType.CUDA)
if cuda_devices and 0 <= self.gpu_index < len(cuda_devices):
return torch.device(f"cuda:{self.gpu_index}")
# Fallback to best available device
best_device = detector.get_best_device(prefer_gpu=True)
if best_device.type != AcceleratorType.CPU:
return best_device.torch_device
# Final fallback to CPU
return torch.device("cpu")
@property
def accelerator_device(self) -> 'AcceleratorDevice':
"""Get the selected accelerator device info"""
detector = get_accelerator_detector()
if self.use_cpu:
return detector.get_cpu_device()
# Try to use the new accelerator device index first
gpu_devices = [device for device in detector.available_devices if device.type != AcceleratorType.CPU]
if gpu_devices and 0 <= self.accelerator_device_index < len(gpu_devices):
return gpu_devices[self.accelerator_device_index]
# Fallback to legacy CUDA device selection
cuda_devices = detector.get_devices_by_type(AcceleratorType.CUDA)
if cuda_devices and 0 <= self.gpu_index < len(cuda_devices):
return cuda_devices[self.gpu_index]
# Fallback to best available device
return detector.get_best_device(prefer_gpu=True)
def get_settings(context: NodeContext) -> PyTorchSettings:
settings = context.settings
return PyTorchSettings(
use_cpu=settings.get_bool("use_cpu", False),
use_fp16=settings.get_bool("use_fp16", False),
gpu_index=settings.get_int("gpu_index", 0, parse_str=True),
accelerator_device_index=settings.get_int("accelerator_device_index", 0, parse_str=True),
budget_limit=settings.get_int("budget_limit", 0, parse_str=True),
force_cache_wipe=settings.get_bool("force_cache_wipe", False),
)
================================================
FILE: MangaJaNaiConverterGui/backend/src/progress_controller.py
================================================
import asyncio
import time
from abc import ABC, abstractmethod
class Aborted(Exception):
pass
class ProgressToken(ABC):
@property
@abstractmethod
def paused(self) -> bool:
pass
@property
@abstractmethod
def aborted(self) -> bool:
pass
@abstractmethod
async def suspend(self) -> None:
"""
If the operation was aborted, this method will throw an `Aborted` exception.
If the operation is paused, this method will wait until the operation is resumed or aborted.
"""
class ProgressController(ProgressToken):
def __init__(self) -> None:
self.__paused: bool = False
self.__aborted: bool = False
self.time_paused: float = 0
"""
The amount of time spend paused in seconds.
Only time spend during `suspend` is counted.
"""
@property
def paused(self) -> bool:
return self.__paused
@property
def aborted(self) -> bool:
return self.__aborted
def pause(self) -> None:
self.__paused = True
def resume(self) -> None:
self.__paused = False
def abort(self) -> None:
self.__aborted = True
async def suspend(self) -> None:
if self.aborted:
raise Aborted()
if self.paused:
start = time.monotonic()
try:
while self.paused:
await asyncio.sleep(0.1)
if self.aborted:
raise Aborted()
finally:
self.time_paused += time.monotonic() - start
================================================
FILE: MangaJaNaiConverterGui/backend/src/pyproject.toml
================================================
[project]
name = "mangajanaiconvertergui"
dynamic = ["version"]
dependencies = [
"chainner_ext==0.3.10",
"numpy==2.2.5",
"opencv-python==4.11.0.86",
"packaging==25.0",
"psutil==6.0.0",
"pynvml==11.5.3",
"pyvips==3.0.0",
"pyvips-binary==8.16.1",
"rarfile==4.2",
"sanic==24.6.0",
"spandrel_extra_arches==0.2.0",
"spandrel==0.4.1",
"torch==2.9.1",
"torchvision==0.24.1",
]
authors = [{name = "the-database"}]
description = "Upscaling manga images and archives with PyTorch models."
readme = "README.md"
license = {file = "LICENSE.txt"}
keywords = []
[project.optional-dependencies]
dev = ["ruff", "pyright", "pytest"]
[project.urls]
Repository = "https://github.com/the-database/MangaJaNaiConverterGui.git"
[tool.setuptools.packages.find]
where = ["."] # list of folders that contain the packages (["."] by default)
include = ["*"] # package names should match these glob patterns (["*"] by default)
exclude = [] # exclude packages matching these glob patterns (empty by default)
namespaces = false # to disable scanning PEP 420 namespaces (true by default)
[tool.ruff]
# Same as Black.
line-length = 88
indent-width = 4
src = ["*"]
unsafe-fixes = true
[tool.ruff.lint]
# Add the `line-too-long` rule to the enforced rule set.
extend-select = [
"UP", # pyupgrade
"E", # pycodestyle
"W", # pycodestyle
"F", # pyflakes
"I", # isort
"N", # pep8-naming
# "ANN", # flake8-annotations
"ANN001",
"ANN002",
"ANN201",
"ANN202",
"ANN204",
"ANN205",
"ANN206",
# "ASYNC", # flake8-async
"PL", # pylint
"RUF", # ruff
"B", # flake8-bugbear
# "A", # flake8-builtins
# "COM", # flake8-commas
"C4", # flake8-comprehensions
"FA", # flake8-future-annotations
"ISC", # flake8-implicit-str-concat
"ICN", # flake8-import-conventions
"G", # flake8-logging-format
# "INP", # flake8-implicit-namespaces
"PIE", # flake8-pie
# "PYI", # flake8-pyi
"Q", # flake8-quotes
# "RET", # flake8-return
"SLF", # flake8-self
# "SIM", # flake8-simplify
# "TCH", # flake8-tidy-imports
"NPY", # NumPy-specific rules
"NPY201", # numpy2-deprecation
]
ignore = [
"E501", # Line too long
"PLR2004", # Magic value
"PLR0911", # Too many return statements
"PLR0912", # Too many branches
"PLR0913", # Too many arguments
"PLR0915", # Too many statements,
"E741", # Ambiguous variable name,
"E712", # true-false-comparison, has false positives because of numpy's operator overloading
"F821", # Undefined name -- this one is weird, it seems like it has false positives on closures and other context changes
"F403", # 'from module import *' used; unable to detect undefined names
"PLW0603", # Using the global statement
"N999", # Invalid module name (which triggers for chaiNNer)
"N818", # Exception name should end in Error
"ISC001", # Implicit string concatenation, conflicts with formatter
]
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
[tool.uv.pip]
extra-index-url = ["https://download.pytorch.org/whl/cu121"]
================================================
FILE: MangaJaNaiConverterGui/backend/src/pyrightconfig.json
================================================
{
"include": [
// "traiNNer"
],
"exclude": [
"**/__pycache__"
],
"ignore": [],
"typeCheckingMode": "standard",
"useLibraryCodeForTypes": true,
"strictListInference": true,
"strictDictionaryInference": true,
"strictSetInference": true,
"reportDuplicateImport": "warning",
"reportImportCycles": "error",
"reportIncompatibleVariableOverride": "error",
"reportIncompatibleMethodOverride": "error",
"reportOverlappingOverload": "error",
"reportPrivateImportUsage": "error",
"reportUninitializedInstanceVariable": "error",
"reportUnnecessaryCast": "error",
"reportUnnecessaryComparison": "error",
"reportUnnecessaryContains": "error",
"reportUnnecessaryIsInstance": "error",
"reportUnusedClass": "warning",
"reportUnusedFunction": "warning",
"reportUnusedImport": "warning",
"reportUnusedVariable": "warning",
}
================================================
FILE: MangaJaNaiConverterGui/backend/src/run_upscale.py
================================================
import argparse
import ctypes
import io
import json
import os
import platform
import sys
import time
from collections.abc import Callable
from io import BytesIO
from pathlib import Path
from queue import Queue
from multiprocessing import Queue as MPQueue, Process
from threading import Thread
from typing import Any, Literal
from zipfile import ZipFile, ZIP_DEFLATED
import cv2
import numpy as np
import pyvips
import rarfile
from chainner_ext import ResizeFilter, resize
from cv2.typing import MatLike
from PIL import Image, ImageCms, ImageFilter
from PIL.Image import Image as ImageType
from PIL.ImageCms import ImageCmsProfile
from rarfile import RarFile
from spandrel import ImageModelDescriptor, ModelDescriptor
sys.path.append(os.path.normpath(os.path.dirname(os.path.abspath(__file__))))
import spandrel_custom
from nodes.impl.image_utils import normalize, to_uint8, to_uint16
from nodes.impl.upscale.auto_split_tiles import (
ESTIMATE,
MAX_TILE_SIZE,
NO_TILING,
TileSize,
)
from nodes.utils.utils import get_h_w_c
from packages.chaiNNer_pytorch.pytorch.io.load_model import load_model_node
from packages.chaiNNer_pytorch.pytorch.processing.upscale_image import (
upscale_image_node,
)
from progress_controller import ProgressController, ProgressToken
from api import (
NodeContext,
SettingsParser,
)
class _ExecutorNodeContext(NodeContext):
def __init__(
self, progress: ProgressToken, settings: SettingsParser, storage_dir: Path
) -> None:
super().__init__()
self.progress = progress
self.__settings = settings
self._storage_dir = storage_dir
self.chain_cleanup_fns: set[Callable[[], None]] = set()
self.node_cleanup_fns: set[Callable[[], None]] = set()
@property
def aborted(self) -> bool:
return self.progress.aborted
@property
def paused(self) -> bool:
time.sleep(0.001)
return self.progress.paused
def set_progress(self, progress: float) -> None:
self.check_aborted()
# TODO: send progress event
@property
def settings(self) -> SettingsParser:
"""
Returns the settings of the current node execution.
"""
return self.__settings
@property
def storage_dir(self) -> Path:
return self._storage_dir
def add_cleanup(
self, fn: Callable[[], None], after: Literal["node", "chain"] = "chain"
) -> None:
if after == "chain":
self.chain_cleanup_fns.add(fn)
elif after == "node":
self.node_cleanup_fns.add(fn)
else:
raise ValueError(f"Unknown cleanup type: {after}")
def get_tile_size(tile_size_str: str) -> TileSize:
if tile_size_str == "Auto (Estimate)":
return ESTIMATE
elif tile_size_str == "Maximum":
return MAX_TILE_SIZE
elif tile_size_str == "No Tiling":
return NO_TILING
elif tile_size_str.isdecimal():
return TileSize(int(tile_size_str))
return ESTIMATE
"""
lanczos downscale without color conversion, for pre-upscale
downscale and final color downscale
"""
def standard_resize(image: np.ndarray, new_size: tuple[int, int]) -> np.ndarray:
new_image = image.astype(np.float32) / 255.0
new_image = resize(new_image, new_size, ResizeFilter.Lanczos, False)
new_image = (new_image * 255).round().astype(np.uint8)
_, _, c = get_h_w_c(image)
if c == 1 and new_image.ndim == 3:
new_image = np.squeeze(new_image, axis=-1)
return new_image
"""
final downscale for grayscale images only
"""
def dotgain20_resize(image: np.ndarray, new_size: tuple[int, int]) -> np.ndarray:
h, _, c = get_h_w_c(image)
size_ratio = h / new_size[1]
blur_size = (1 / size_ratio - 1) / 3.5
if blur_size >= 0.1:
blur_size = min(blur_size, 250)
pil_image = Image.fromarray(image, mode="L")
pil_image = pil_image.filter(ImageFilter.GaussianBlur(radius=blur_size))
pil_image = ImageCms.applyTransform(pil_image, dotgain20togamma1transform, False)
new_image = np.array(pil_image)
new_image = new_image.astype(np.float32) / 255.0
new_image = resize(new_image, new_size, ResizeFilter.CubicCatrom, False)
new_image = (new_image * 255).round().astype(np.uint8)
pil_image = Image.fromarray(new_image[:, :, 0], mode="L")
pil_image = ImageCms.applyTransform(pil_image, gamma1todotgain20transform, False)
return np.array(pil_image)
def image_resize(
image: np.ndarray, new_size: tuple[int, int], is_grayscale: bool
) -> np.ndarray:
if is_grayscale:
return dotgain20_resize(image, new_size)
return standard_resize(image, new_size)
def get_system_codepage() -> Any:
return None if not is_windows else ctypes.windll.kernel32.GetConsoleOutputCP()
def enhance_contrast(image: np.ndarray) -> MatLike:
image_p = Image.fromarray(image).convert("L")
# Calculate the histogram
hist = image_p.histogram()
# print(hist)
# Find the global maximum peak in the range 0-30 for the black level
new_black_level = 0
global_max_black = hist[0]
for i in range(1, 31):
if hist[i] > global_max_black:
global_max_black = hist[i]
new_black_level = i
# elif hist[i] < global_max_black:
# break
# Continue searching at 31 and later for the black level
continuous_count = 0
for i in range(31, 256):
if hist[i] > global_max_black:
continuous_count = 0
global_max_black = hist[i]
new_black_level = i
elif hist[i] < global_max_black:
continuous_count += 1
if continuous_count > 1:
break
# Find the global maximum peak in the range 255-225 for the white level
new_white_level = 255
global_max_white = hist[255]
for i in range(254, 224, -1):
if hist[i] > global_max_white:
global_max_white = hist[i]
new_white_level = i
# elif hist[i] < global_max_white:
# break
# Continue searching at 224 and below for the white level
continuous_count = 0
for i in range(223, -1, -1):
if hist[i] > global_max_white:
continuous_count = 0
global_max_white = hist[i]
new_white_level = i
elif hist[i] < global_max_white:
continuous_count += 1
if continuous_count > 1:
break
print(
f"Auto adjusted levels: new black level = {new_black_level}; new white level = {new_white_level}",
flush=True,
)
image_array = np.array(image_p).astype("float32")
image_array = np.maximum(image_array - new_black_level, 0) / (
new_white_level - new_black_level
)
return np.clip(image_array, 0, 1)
def _read_image(img_stream: bytes, filename: str) -> np.ndarray:
return _read_vips(img_stream)
def _read_image_from_path(path: str) -> np.ndarray:
return pyvips.Image.new_from_file(path, access="sequential", fail=True).icc_transform("srgb").numpy()
def _read_vips(img_stream: bytes) -> np.ndarray:
return pyvips.Image.new_from_buffer(img_stream, "", access="sequential").icc_transform("srgb").numpy()
def cv_image_is_grayscale(image: np.ndarray, user_threshold: float) -> bool:
_, _, c = get_h_w_c(image)
if c == 1:
return True
b, g, r = cv2.split(image[:, :, :3])
ignore_threshold = user_threshold
# getting differences between (b,g), (r,g), (b,r) channel pixels
r_g = cv2.subtract(cv2.absdiff(r, g), ignore_threshold) # type: ignore
r_b = cv2.subtract(cv2.absdiff(r, b), ignore_threshold) # type: ignore
g_b = cv2.subtract(cv2.absdiff(g, b), ignore_threshold) # type: ignore
# create masks to identify pure black and pure white pixels
pure_black_mask = np.logical_and.reduce((r == 0, g == 0, b == 0))
pure_white_mask = np.logical_and.reduce((r == 255, g == 255, b == 255))
# combine masks to exclude both pure black and pure white pixels
exclude_mask = np.logical_or(pure_black_mask, pure_white_mask)
# exclude pure black and pure white pixels from diff_sum and image size calculation
diff_sum = np.sum(np.where(exclude_mask, 0, r_g + r_b + g_b))
size_without_black_and_white = np.sum(~exclude_mask) * 3
# if the entire image is pure black or pure white, return False
if size_without_black_and_white == 0:
return False
# finding ratio of diff_sum with respect to size of image without pure black and pure white pixels
ratio = diff_sum / size_without_black_and_white
return ratio <= user_threshold / 12
def convert_image_to_grayscale(image: np.ndarray) -> np.ndarray:
channels = get_h_w_c(image)[2]
if channels == 3:
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
elif channels == 4:
image = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)
return image
def get_chain_for_image(
image: np.ndarray,
target_scale: float | None,
target_width: int,
target_height: int,
chains: list[dict[str, Any]],
grayscale_detection_threshold: int,
) -> tuple[dict[str, Any], bool, int, int] | tuple[None, None, int, int]:
original_height, original_width, _ = get_h_w_c(image)
if target_width != 0 and target_height != 0:
target_scale = min(
target_height / original_height, target_width / original_width
)
if target_height != 0:
target_scale = target_height / original_height
elif target_width != 0:
target_scale = target_width / original_width
assert target_scale is not None
is_grayscale = cv_image_is_grayscale(image, grayscale_detection_threshold)
for chain in chains:
if should_chain_activate_for_image(
original_width, original_height, is_grayscale, target_scale, chain
):
print("Matched Chain:", chain, flush=True)
return chain, is_grayscale, original_width, original_height
return None, None, original_width, original_height
def should_chain_activate_for_image(
original_width: int,
original_height: int,
is_grayscale: bool,
target_scale: float,
chain: dict[str, Any],
) -> bool:
min_width, min_height = (int(x) for x in chain["MinResolution"].split("x"))
max_width, max_height = (int(x) for x in chain["MaxResolution"].split("x"))
# resolution tests
if min_width != 0 and min_width > original_width:
return False
if min_height != 0 and min_height > original_height:
return False
if max_width != 0 and max_width < original_width:
return False
if max_height != 0 and max_height < original_height:
return False
# color / grayscale tests
if is_grayscale and not chain["IsGrayscale"]:
return False
if not is_grayscale and not chain["IsColor"]:
return False
# scale tests
if chain["MaxScaleFactor"] != 0 and target_scale > chain["MaxScaleFactor"]:
return False
if chain["MinScaleFactor"] != 0 and target_scale < chain["MinScaleFactor"]:
return False
return True
def ai_upscale_image(
image: np.ndarray, model_tile_size: TileSize, model: ImageModelDescriptor | None
) -> np.ndarray:
if model is not None:
result = upscale_image_node(
context,
image,
model,
False,
0,
model_tile_size,
256,
False,
)
_, _, c = get_h_w_c(image)
if c == 1 and result.ndim == 3:
result = np.squeeze(result, axis=-1)
return result
return image
def postprocess_image(image: np.ndarray) -> np.ndarray:
# print(f"postprocess_image")
return to_uint8(image, normalized=True)
def final_target_resize(
image: np.ndarray,
target_scale: float,
target_width: int,
target_height: int,
original_width: int,
original_height: int,
is_grayscale: bool,
) -> np.ndarray:
# fit to dimensions
if target_height != 0 and target_width != 0:
h, w, _ = get_h_w_c(image)
# determine whether to fit to height or width
if target_height / original_height < target_width / original_width:
target_width = 0
else:
target_height = 0
# resize height, keep proportional width
if target_height != 0:
h, w, _ = get_h_w_c(image)
if h != target_height:
return image_resize(
image, (round(w * target_height / h), target_height), is_grayscale
)
# resize width, keep proportional height
elif target_width != 0:
h, w, _ = get_h_w_c(image)
if w != target_width:
return image_resize(
image, (target_width, round(h * target_width / w)), is_grayscale
)
else:
h, w, _ = get_h_w_c(image)
new_target_height = round(original_height * target_scale)
if h != new_target_height:
return image_resize(
image,
(round(w * new_target_height / h), new_target_height),
is_grayscale,
)
return image
def save_image_zip(
image: np.ndarray,
file_name: str,
output_zip: ZipFile,
image_format: str,
lossy_compression_quality: int,
use_lossless_compression: bool,
original_width: int,
original_height: int,
target_scale: float,
target_width: int,
target_height: int,
is_grayscale: bool,
) -> None:
print(f"save image to zip: {file_name}", flush=True)
image = to_uint8(image, normalized=True)
image = final_target_resize(
image,
target_scale,
target_width,
target_height,
original_width,
original_height,
is_grayscale,
)
# Convert the resized image back to bytes
args = {"Q": int(lossy_compression_quality)}
if image_format in {"webp"}:
args["lossless"] = use_lossless_compression
buf_img = pyvips.Image.new_from_array(image).write_to_buffer(f".{image_format}", **args)
output_buffer = io.BytesIO(buf_img) # type: ignore
upscaled_image_data = output_buffer.getvalue()
# Add the resized image to the output zip
output_zip.writestr(file_name, upscaled_image_data)
def save_image(
image: np.ndarray,
output_file_path: str,
image_format: str,
lossy_compression_quality: int,
use_lossless_compression: bool,
original_width: int,
original_height: int,
target_scale: float,
target_width: int,
target_height: int,
is_grayscale: bool,
) -> None:
print(f"save image: {output_file_path}", flush=True)
image = to_uint8(image, normalized=True)
image = final_target_resize(
image,
target_scale,
target_width,
target_height,
original_width,
original_height,
is_grayscale,
)
args = {"Q": int(lossy_compression_quality)}
if image_format in {"webp"}:
args["lossless"] = use_lossless_compression
pyvips.Image.new_from_array(image).write_to_file(output_file_path, **args)
def preprocess_worker_archive(
upscale_queue: Queue,
input_archive_path: str,
output_archive_path: str,
target_scale: float | None,
target_width: int,
target_height: int,
chains: list[dict[str, Any]],
loaded_models: dict[str, ModelDescriptor],
grayscale_detection_threshold: int,
) -> None:
"""
given a zip or rar path, read images out of the archive, apply auto levels, add the image to upscale queue
"""
if input_archive_path.endswith(ZIP_EXTENSIONS):
with ZipFile(input_archive_path, "r") as input_zip:
preprocess_worker_archive_file(
upscale_queue,
input_zip,
output_archive_path,
target_scale,
target_width,
target_height,
chains,
loaded_models,
grayscale_detection_threshold,
)
elif input_archive_path.endswith(RAR_EXTENSIONS):
with rarfile.RarFile(input_archive_path, "r") as input_rar:
preprocess_worker_archive_file(
upscale_queue,
input_rar,
output_archive_path,
target_scale,
target_width,
target_height,
chains,
loaded_models,
grayscale_detection_threshold,
)
def preprocess_worker_archive_file(
upscale_queue: Queue,
input_archive: RarFile | ZipFile,
output_archive_path: str,
target_scale: float | None,
target_width: int,
target_height: int,
chains: list[dict[str, Any]],
loaded_models: dict[str, ModelDescriptor],
grayscale_detection_threshold: int,
) -> None:
"""
given an input zip or rar archive, read images out of the archive, apply auto levels, add the image to upscale queue
"""
os.makedirs(os.path.dirname(output_archive_path), exist_ok=True)
namelist = input_archive.namelist()
print(f"TOTALZIP={len(namelist)}", flush=True)
for filename in namelist:
decoded_filename = filename
image_data = None
try:
decoded_filename = decoded_filename.encode("cp437").decode(
f"cp{system_codepage}"
)
except: # noqa: E722
pass
# Open the file inside the input zip
try:
with input_archive.open(filename) as file_in_archive:
# Read the image data
image_data = file_in_archive.read()
# image_bytes = io.BytesIO(image_data)
image = _read_image(image_data, filename)
print("read image", filename, flush=True)
chain, is_grayscale, original_width, original_height = (
get_chain_for_image(
image,
target_scale,
target_width,
target_height,
chains,
grayscale_detection_threshold,
)
)
if is_grayscale:
image = convert_image_to_grayscale(image)
model = None
tile_size_str = ""
if chain is not None:
resize_width_before_upscale = chain["ResizeWidthBeforeUpscale"]
resize_height_before_upscale = chain["ResizeHeightBeforeUpscale"]
resize_factor_before_upscale = chain["ResizeFactorBeforeUpscale"]
# resize width and height, distorting image
if (
resize_height_before_upscale != 0
and resize_width_before_upscale != 0
):
h, w, _ = get_h_w_c(image)
image = standard_resize(
image,
(resize_width_before_upscale, resize_height_before_upscale),
)
# resize height, keep proportional width
elif resize_height_before_upscale != 0:
h, w, _ = get_h_w_c(image)
image = standard_resize(
image,
(
round(w * resize_height_before_upscale / h),
resize_height_before_upscale,
),
)
# resize width, keep proportional height
elif resize_width_before_upscale != 0:
h, w, _ = get_h_w_c(image)
image = standard_resize(
image,
(
resize_width_before_upscale,
round(h * resize_width_before_upscale / w),
),
)
elif resize_factor_before_upscale != 100:
h, w, _ = get_h_w_c(image)
image = standard_resize(
image,
(
round(w * resize_factor_before_upscale / 100),
round(h * resize_factor_before_upscale / 100),
),
)
if is_grayscale and chain["AutoAdjustLevels"]:
image = enhance_contrast(image)
else:
image = normalize(image)
model_abs_path = get_model_abs_path(chain["ModelFilePath"])
if model_abs_path in loaded_models:
model = loaded_models[model_abs_path]
elif os.path.exists(model_abs_path):
model, _, _ = load_model_node(context, Path(model_abs_path))
loaded_models[model_abs_path] = model
tile_size_str = chain["ModelTileSize"]
else:
image = normalize(image)
# image = np.ascontiguousarray(image)
upscale_queue.put(
(
image,
decoded_filename,
True,
is_grayscale,
original_width,
original_height,
get_tile_size(tile_size_str),
model,
)
)
except Exception as e:
print(
f"could not read as image, copying file to zip instead of upscaling: {decoded_filename}, {e}",
flush=True,
)
upscale_queue.put(
(image_data, decoded_filename, False, False, None, None, None, None)
)
# pass
upscale_queue.put(UPSCALE_SENTINEL)
# print("preprocess_worker_archive exiting")
def preprocess_worker_folder(
upscale_queue: Queue,
input_folder_path: str,
output_folder_path: str,
output_filename: str,
upscale_images: bool,
upscale_archives: bool,
overwrite_existing_files: bool,
image_format: str,
lossy_compression_quality: int,
use_lossless_compression: bool,
target_scale: float | None,
target_width: int,
target_height: int,
chains: list[dict[str, Any]],
loaded_models: dict[str, ModelDescriptor],
grayscale_detection_threshold: int,
) -> None:
"""
given a folder path, recursively iterate the folder
"""
print(
f"preprocess_worker_folder entering {input_folder_path} {output_folder_path} {output_filename}",
flush=True,
)
for root, _dirs, files in os.walk(input_folder_path):
for filename in files:
# for output file, create dirs if necessary, or skip if file exists and overwrite not enabled
input_file_base = Path(filename).stem
filename_rel = os.path.relpath(
os.path.join(root, filename), input_folder_path
)
output_filename_rel = os.path.join(
os.path.dirname(filename_rel),
output_filename.replace("%filename%", input_file_base),
)
output_file_path = Path(
os.path.join(output_folder_path, output_filename_rel)
)
if filename.lower().endswith(IMAGE_EXTENSIONS): # TODO if image
if upscale_images:
output_file_path = str(
Path(f"{output_file_path}.{image_format}")
).replace("%filename%", input_file_base)
if not overwrite_existing_files and os.path.isfile(
output_file_path
):
print(f"file exists, skip: {output_file_path}", flush=True)
continue
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
image = _read_image_from_path(os.path.join(root, filename))
chain, is_grayscale, original_width, original_height = (
get_chain_for_image(
image,
target_scale,
target_width,
target_height,
chains,
grayscale_detection_threshold,
)
)
if is_grayscale:
image = convert_image_to_grayscale(image)
model = None
tile_size_str = ""
if chain is not None:
resize_width_before_upscale = chain["ResizeWidthBeforeUpscale"]
resize_height_before_upscale = chain[
"ResizeHeightBeforeUpscale"
]
resize_factor_before_upscale = chain[
"ResizeFactorBeforeUpscale"
]
# resize width and height, distorting image
if (
resize_height_before_upscale != 0
and resize_width_before_upscale != 0
):
h, w, _ = get_h_w_c(image)
image = standard_resize(
image,
(
resize_width_before_upscale,
resize_height_before_upscale,
),
)
# resize height, keep proportional width
elif resize_height_before_upscale != 0:
h, w, _ = get_h_w_c(image)
image = standard_resize(
image,
(
round(w * resize_height_before_upscale / h),
resize_height_before_upscale,
),
)
# resize width, keep proportional height
elif resize_width_before_upscale != 0:
h, w, _ = get_h_w_c(image)
image = standard_resize(
image,
(
resize_width_before_upscale,
round(h * resize_width_before_upscale / w),
),
)
elif resize_factor_before_upscale != 100:
h, w, _ = get_h_w_c(image)
image = standard_resize(
image,
(
round(w * resize_factor_before_upscale / 100),
round(h * resize_factor_before_upscale / 100),
),
)
if is_grayscale and chain["AutoAdjustLevels"]:
image = enhance_contrast(image)
else:
image = normalize(image)
model_abs_path = get_model_abs_path(chain["ModelFilePath"])
if model_abs_path in loaded_models:
model = loaded_models[model_abs_path]
elif os.path.exists(model_abs_path):
model, _, _ = load_model_node(context, Path(model_abs_path))
loaded_models[model_abs_path] = model
tile_size_str = chain["ModelTileSize"]
else:
image = normalize(image)
# image = np.ascontiguousarray(image)
upscale_queue.put(
(
image,
output_filename_rel,
True,
is_grayscale,
original_width,
original_height,
get_tile_size(tile_size_str),
model,
)
)
elif filename.lower().endswith(ARCHIVE_EXTENSIONS):
if upscale_archives:
output_file_path = f"{output_file_path}.cbz"
if not overwrite_existing_files and os.path.isfile(
output_file_path
):
print(f"file exists, skip: {output_file_path}", flush=True)
continue
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
upscale_archive_file(
os.path.join(root, filename),
output_file_path,
image_format,
lossy_compression_quality,
use_lossless_compression,
target_scale,
target_width,
target_height,
chains,
loaded_models,
grayscale_detection_threshold,
) # TODO custom output extension
upscale_queue.put(UPSCALE_SENTINEL)
# print("preprocess_worker_folder exiting")
def preprocess_worker_image(
upscale_queue: Queue,
input_image_path: str,
output_image_path: str,
overwrite_existing_files: bool,
target_scale: float | None,
target_width: int,
target_height: int,
chains: list[dict[str, Any]],
loaded_models: dict[str, ModelDescriptor],
grayscale_detection_threshold: int,
) -> None:
"""
given an image path, apply auto levels and add to upscale queue
"""
if input_image_path.lower().endswith(IMAGE_EXTENSIONS):
if not overwrite_existing_files and os.path.isfile(output_image_path):
print(f"file exists, skip: {output_image_path}", flush=True)
return
os.makedirs(os.path.dirname(output_image_path), exist_ok=True)
# with Image.open(input_image_path) as img:
image = _read_image_from_path(input_image_path)
chain, is_grayscale, original_width, original_height = get_chain_for_image(
image,
target_scale,
target_width,
target_height,
chains,
grayscale_detection_threshold,
)
if is_grayscale:
image = convert_image_to_grayscale(image)
model = None
tile_size_str = ""
if chain is not None:
resize_width_before_upscale = chain["ResizeWidthBeforeUpscale"]
resize_height_before_upscale = chain["ResizeHeightBeforeUpscale"]
resize_factor_before_upscale = chain["ResizeFactorBeforeUpscale"]
# resize width and height, distorting image
if resize_height_before_upscale != 0 and resize_width_before_upscale != 0:
h, w, _ = get_h_w_c(image)
image = standard_resize(
image, (resize_width_before_upscale, resize_height_before_upscale)
)
# resize height, keep proportional width
elif resize_height_before_upscale != 0:
h, w, _ = get_h_w_c(image)
image = standard_resize(
image,
(
round(w * resize_height_before_upscale / h),
resize_height_before_upscale,
),
)
# resize width, keep proportional height
elif resize_width_before_upscale != 0:
h, w, _ = get_h_w_c(image)
image = standard_resize(
image,
(
resize_width_before_upscale,
round(h * resize_width_before_upscale / w),
),
)
elif resize_factor_before_upscale != 100:
h, w, _ = get_h_w_c(image)
image = standard_resize(
image,
(
round(w * resize_factor_before_upscale / 100),
round(h * resize_factor_before_upscale / 100),
),
)
if is_grayscale and chain["AutoAdjustLevels"]:
image = enhance_contrast(image)
else:
image = normalize(image)
if chain["ModelFilePath"] == "No Model":
pass
else:
model_abs_path = get_model_abs_path(chain["ModelFilePath"])
if not os.path.exists(model_abs_path):
raise FileNotFoundError(model_abs_path)
if model_abs_path in loaded_models:
model = loaded_models[model_abs_path]
elif os.path.exists(model_abs_path):
model, _, _ = load_model_node(context, Path(model_abs_path))
loaded_models[model_abs_path] = model
tile_size_str = chain["ModelTileSize"]
else:
print("No chain!!!!!!!")
image = normalize(image)
# image = np.ascontiguousarray(image)
upscale_queue.put(
(
image,
None,
True,
is_grayscale,
original_width,
original_height,
get_tile_size(tile_size_str),
model,
)
)
upscale_queue.put(UPSCALE_SENTINEL)
def upscale_worker(upscale_queue: Queue, postprocess_queue: Queue) -> None:
"""
wait for upscale queue, for each queue entry, upscale image and add result to postprocess queue
"""
# print("upscale_worker entering")
while True:
(
image,
file_name,
is_image,
is_grayscale,
original_width,
original_height,
model_tile_size,
model,
) = upscale_queue.get()
if image is None:
break
if is_image:
image = ai_upscale_image(image, model_tile_size, model)
# convert back to grayscale
if is_grayscale:
image = convert_image_to_grayscale(image)
postprocess_queue.put(
(image, file_name, is_image, is_grayscale, original_width, original_height)
)
postprocess_queue.put(POSTPROCESS_SENTINEL)
# print("upscale_worker exiting")
def postprocess_worker_zip(
postprocess_queue: Queue,
output_zip_path: str,
image_format: str,
lossy_compression_quality: int,
use_lossless_compression: bool,
target_scale: float,
target_width: int,
target_height: int,
) -> None:
"""
wait for postprocess queue, for each queue entry, save the image to the zip file
"""
# print("postprocess_worker_zip entering")
with ZipFile(output_zip_path, "w", ZIP_DEFLATED) as output_zip:
while True:
(
image,
file_name,
is_image,
is_grayscale,
original_width,
original_height,
) = postprocess_queue.get()
if image is None:
break
if is_image:
# image = postprocess_image(image)
save_image_zip(
image,
str(Path(file_name).with_suffix(f".{image_format}")),
output_zip,
image_format,
lossy_compression_quality,
use_lossless_compression,
original_width,
original_height,
target_scale,
target_width,
target_height,
is_grayscale,
)
else: # copy file
output_zip.writestr(file_name, image)
print("PROGRESS=postprocess_worker_zip_image", flush=True)
print("PROGRESS=postprocess_worker_zip_archive", flush=True)
def postprocess_worker_folder(
postprocess_queue: Queue,
output_folder_path: str,
image_format: str,
lossy_compression_quality: int,
use_lossless_compression: bool,
target_scale: float,
target_width: int,
target_height: int,
) -> None:
"""
wait for postprocess queue, for each queue entry, save the image to the output folder
"""
# print("postprocess_worker_folder entering")
while True:
image, file_name, _, is_grayscale, original_width, original_height = (
postprocess_queue.get()
)
if image is None:
break
image = postprocess_image(image)
save_image(
image,
os.path.join(output_folder_path, str(Path(f"{file_name}.{image_format}"))),
image_format,
lossy_compression_quality,
use_lossless_compression,
original_width,
original_height,
target_scale,
target_width,
target_height,
is_grayscale,
)
print("PROGRESS=postprocess_worker_folder", flush=True)
# print("postprocess_worker_folder exiting")
def postprocess_worker_image(
postprocess_queue: Queue,
output_file_path: str,
image_format: str,
lossy_compression_quality: int,
use_lossless_compression: bool,
target_scale: float,
target_width: int,
target_height: int,
) -> None:
"""
wait for postprocess queue, for each queue entry, save the image to the output file path
"""
while True:
image, _, _, is_grayscale, original_width, original_height = (
postprocess_queue.get()
)
if image is None:
break
# image = postprocess_image(image)
save_image(
image,
output_file_path,
image_format,
lossy_compression_quality,
use_lossless_compression,
original_width,
original_height,
target_scale,
target_width,
target_height,
is_grayscale,
)
print("PROGRESS=postprocess_worker_image", flush=True)
def upscale_archive_file(
input_zip_path: str,
output_zip_path: str,
image_format: str,
lossy_compression_quality: int,
use_lossless_compression: bool,
target_scale: float | None,
target_width: int,
target_height: int,
chains: list[dict[str, Any]],
loaded_models: dict[str, ModelDescriptor],
grayscale_detection_threshold: int,
) -> None:
# TODO accept multiple paths to reuse simple queues?
upscale_queue = Queue(maxsize=1)
postprocess_queue = MPQueue(maxsize=1)
# start preprocess zip process
preprocess_process = Thread(
target=preprocess_worker_archive,
args=(
upscale_queue,
input_zip_path,
output_zip_path,
target_scale,
target_width,
target_height,
chains,
loaded_models,
grayscale_detection_threshold,
),
)
preprocess_process.start()
# start upscale process
upscale_process = Thread(
target=upscale_worker, args=(upscale_queue, postprocess_queue)
)
upscale_process.start()
# start postprocess zip process
postprocess_process = Process(
target=postprocess_worker_zip,
args=(
postprocess_queue,
output_zip_path,
image_format,
lossy_compression_quality,
use_lossless_compression,
target_scale,
target_width,
target_height,
),
)
postprocess_process.start()
# wait for all processes
preprocess_process.join()
upscale_process.join()
postprocess_process.join()
def upscale_image_file(
input_image_path: str,
output_image_path: str,
overwrite_existing_files: bool,
image_format: str,
lossy_compression_quality: int,
use_lossless_compression: bool,
target_scale: float | None,
target_width: int,
target_height: int,
chains: list[dict[str, Any]],
loaded_models: dict[str, ModelDescriptor],
grayscale_detection_threshold: int,
) -> None:
upscale_queue = Queue(maxsize=1)
postprocess_queue = MPQueue(maxsize=1)
# start preprocess image process
preprocess_process = Thread(
target=preprocess_worker_image,
args=(
upscale_queue,
input_image_path,
output_image_path,
overwrite_existing_files,
target_scale,
target_width,
target_height,
chains,
loaded_models,
grayscale_detection_threshold,
),
)
preprocess_process.start()
# start upscale process
upscale_process = Thread(
target=upscale_worker, args=(upscale_queue, postprocess_queue)
)
upscale_process.start()
# start postprocess image process
postprocess_process = Process(
target=postprocess_worker_image,
args=(
postprocess_queue,
output_image_path,
image_format,
lossy_compression_quality,
use_lossless_compression,
target_scale,
target_width,
target_height,
),
)
postprocess_process.start()
# wait for all processes
preprocess_process.join()
upscale_process.join()
postprocess_process.join()
def upscale_file(
input_file_path: str,
output_folder_path: str,
output_filename: str,
overwrite_existing_files: bool,
image_format: str,
lossy_compression_quality: int,
use_lossless_compression: bool,
target_scale: float | None,
target_width: int,
target_height: int,
chains: list[dict[str, Any]],
loaded_models: dict[str, ModelDescriptor],
grayscale_detection_threshold: int,
) -> None:
input_file_base = Path(input_file_path).stem
if input_file_path.lower().endswith(ARCHIVE_EXTENSIONS):
output_file_path = str(
Path(
f"{os.path.join(output_folder_path,output_filename.replace('%filename%', input_file_base))}.cbz"
)
)
print("output_file_path", output_file_path, flush=True)
if not overwrite_existing_files and os.path.isfile(output_file_path):
print(f"file exists, skip: {output_file_path}", flush=True)
return
upscale_archive_file(
input_file_path,
output_file_path,
image_format,
lossy_compression_quality,
use_lossless_compression,
target_scale,
target_width,
target_height,
chains,
loaded_models,
grayscale_detection_threshold,
)
elif input_file_path.lower().endswith(IMAGE_EXTENSIONS):
output_file_path = str(
Path(
f"{os.path.join(output_folder_path,output_filename.replace('%filename%', input_file_base))}.{image_format}"
)
)
if not overwrite_existing_files and os.path.isfile(output_file_path):
print(f"file exists, skip: {output_file_path}", flush=True)
return
upscale_image_file(
input_file_path,
output_file_path,
overwrite_existing_files,
image_format,
lossy_compression_quality,
use_lossless_compression,
target_scale,
target_width,
target_height,
chains,
loaded_models,
grayscale_detection_threshold,
)
def upscale_folder(
input_folder_path: str,
output_folder_path: str,
output_filename: str,
upscale_images: bool,
upscale_archives: bool,
overwrite_existing_files: bool,
image_format: str,
lossy_compression_quality: int,
use_lossless_compression: bool,
target_scale: float | None,
target_width: int,
target_height: int,
chains: list[dict[str, Any]],
loaded_models: dict[str, ModelDescriptor],
grayscale_detection_threshold: int,
) -> None:
# print("upscale_folder: entering")
# preprocess_queue = Queue(maxsize=1)
upscale_queue = Queue(maxsize=1)
postprocess_queue = MPQueue(maxsize=1)
# start preprocess folder process
preprocess_process = Thread(
target=preprocess_worker_folder,
args=(
upscale_queue,
input_folder_path,
output_folder_path,
output_filename,
upscale_images,
upscale_archives,
overwrite_existing_files,
image_format,
lossy_compression_quality,
use_lossless_compression,
target_scale,
target_width,
target_height,
chains,
loaded_models,
grayscale_detection_threshold,
),
)
preprocess_process.start()
# start upscale process
upscale_process = Thread(
target=upscale_worker, args=(upscale_queue, postprocess_queue)
)
upscale_process.start()
# start postprocess folder process
postprocess_process = Process(
target=postprocess_worker_folder,
args=(
postprocess_queue,
output_folder_path,
image_format,
lossy_compression_quality,
use_lossless_compression,
target_scale,
target_width,
target_height,
),
)
postprocess_process.start()
# wait for all processes
preprocess_process.join()
upscale_process.join()
postprocess_process.join()
current_file_directory = os.path.dirname(os.path.abspath(__file__))
def get_model_abs_path(chain_model_file_path: str) -> str:
return os.path.abspath(os.path.join(models_directory, chain_model_file_path))
def get_gamma_icc_profile() -> ImageCmsProfile:
profile_path = os.path.join(
current_file_directory, "../ImageMagick/Custom Gray Gamma 1.0.icc"
)
return ImageCms.getOpenProfile(profile_path)
def get_dot20_icc_profile() -> ImageCmsProfile:
profile_path = os.path.join(
current_file_directory, "../ImageMagick/Dot Gain 20%.icc"
)
return ImageCms.getOpenProfile(profile_path)
def parse_settings_from_cli():
parser = argparse.ArgumentParser(prog="python run_upscale.py",
description="By default, used by MangaJaNaiConverterGui as an internal tool. "
"Alternative options made available to make it easier to skip the GUI "
"and run upscaling jobs directly from CLI.")
execution_type_group = parser.add_mutually_exclusive_group(required=True)
execution_type_group.add_argument("--settings",
help="Default behaviour, based on provided appstate configuration. "
"For advanced usage.")
execution_type_group.add_argument("-f", "--file-path",
help="Upscale single file")
execution_type_group.add_argument("-d", "--folder-path",
help="Upscale whole directory")
parser.add_argument("-o", "--output-folder-path",
default=os.path.join(".", "out"),
help="Output directory for upscaled files. Default: ./out")
parser.add_argument("-m", "--models-directory-path",
default=os.path.join("..", "models"),
help="Directory with models used for upscaling. "
"Supports only models bundled with MangaJaNaiConvertedGui. "
"Default: MangaJaNaiConverterGui/chaiNNer/models/")
parser.add_argument("-u", "--upscale-factor",
type=int,
choices=[1, 2, 3, 4],
default=2,
help="Used for calculating which model will be used. Default: 2")
parser.add_argument("--device-index",
type=int,
default=0,
help="Device used to run upscaling jobs in case more than one is available. Default: 0")
args = parser.parse_args()
return parse_auto_settings(args) if args.settings else parse_manual_settings(args)
def parse_auto_settings(args):
with open(args.settings, encoding="utf-8") as f:
json_settings = json.load(f)
return json_settings
def parse_manual_settings(args):
default_file_path = os.path.join("..", "resources", "default_cli_configuration.json")
with open(default_file_path, "r") as default_file:
default_json = json.load(default_file)
default_json["SelectedDeviceIndex"] = int(args.device_index)
default_json["ModelsDirectory"] = args.models_directory_path
default_json["Workflows"]["$values"][0]["OutputFolderPath"] = args.output_folder_path
default_json["Workflows"]["$values"][0]["SelectedDeviceIndex"] = args.device_index
default_json["Workflows"]["$values"][0]["UpscaleScaleFactor"] = args.upscale_factor
if args.file_path:
default_json["Workflows"]["$values"][0]["SelectedTabIndex"] = 0
default_json["Workflows"]["$values"][0]["InputFilePath"] = args.file_path
elif args.folder_path:
default_json["Workflows"]["$values"][0]["SelectedTabIndex"] = 1
default_json["Workflows"]["$values"][0]["InputFolderPath"] = args.folder_path
return default_json
is_windows = platform.system() == "win32"
sys.stdout.reconfigure(encoding="utf-8") # type: ignore
settings = parse_settings_from_cli()
workflow = settings["Workflows"]["$values"][settings["SelectedWorkflowIndex"]]
models_directory = settings["ModelsDirectory"]
UPSCALE_SENTINEL = (None, None, None, None, None, None, None, None)
POSTPROCESS_SENTINEL = (None, None, None, None, None, None)
CV2_IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".webp", ".bmp")
IMAGE_EXTENSIONS = (*CV2_IMAGE_EXTENSIONS, ".avif")
ZIP_EXTENSIONS = (".zip", ".cbz")
RAR_EXTENSIONS = (".rar", ".cbr")
ARCHIVE_EXTENSIONS = ZIP_EXTENSIONS + RAR_EXTENSIONS
loaded_models = {}
system_codepage = get_system_codepage()
settings_parser = SettingsParser(
{
"use_cpu": settings["SelectedDeviceIndex"] == 0,
"use_fp16": settings["UseFp16"],
"accelerator_device_index": settings["SelectedDeviceIndex"],
"budget_limit": 0,
}
)
print("settings", settings_parser.get_int("accelerator_device_index", 0), flush=True)
context = _ExecutorNodeContext(ProgressController(), settings_parser, Path())
gamma1icc = get_gamma_icc_profile()
dotgain20icc = get_dot20_icc_profile()
dotgain20togamma1transform = ImageCms.buildTransformFromOpenProfiles(
dotgain20icc, gamma1icc, "L", "L"
)
gamma1todotgain20transform = ImageCms.buildTransformFromOpenProfiles(
gamma1icc, dotgain20icc, "L", "L"
)
if __name__ == "__main__":
spandrel_custom.install()
# gc.disable() #TODO!!!!!!!!!!!!
# Record the start time
start_time = time.time()
image_format = None
if workflow["WebpSelected"]:
image_format = "webp"
elif workflow["PngSelected"]:
image_format = "png"
elif workflow["AvifSelected"]:
image_format = "avif"
else:
image_format = "jpeg"
target_scale: float | None = None
target_width = 0
target_height = 0
grayscale_detection_threshold = workflow["GrayscaleDetectionThreshold"]
if workflow["ModeScaleSelected"]:
target_scale = workflow["UpscaleScaleFactor"]
elif workflow["ModeWidthSelected"]:
target_width = workflow["ResizeWidthAfterUpscale"]
elif workflow["ModeHeightSelected"]:
target_height = workflow["ResizeHeightAfterUpscale"]
else:
target_width = workflow["DisplayDeviceWidth"]
target_height = workflow["DisplayDeviceHeight"]
if workflow["SelectedTabIndex"] == 1:
upscale_folder(
workflow["InputFolderPath"],
workflow["OutputFolderPath"],
workflow["OutputFilename"],
workflow["UpscaleImages"],
workflow["UpscaleArchives"],
workflow["OverwriteExistingFiles"],
image_format,
workflow["LossyCompressionQuality"],
workflow["UseLosslessCompression"],
target_scale,
target_width,
target_height,
workflow["Chains"]["$values"],
loaded_models,
grayscale_detection_threshold,
)
elif workflow["SelectedTabIndex"] == 0:
upscale_file(
workflow["InputFilePath"],
workflow["OutputFolderPath"],
workflow["OutputFilename"],
workflow["OverwriteExistingFiles"],
image_format,
workflow["LossyCompressionQuality"],
workflow["UseLosslessCompression"],
target_scale,
target_width,
target_height,
workflow["Chains"]["$values"],
loaded_models,
grayscale_detection_threshold,
)
# # Record the end time
end_time = time.time()
# # Calculate the elapsed time
elapsed_time = end_time - start_time
# Print the elapsed time
print(f"Elapsed time: {elapsed_time:.2f} seconds")
================================================
FILE: MangaJaNaiConverterGui/backend/src/spandrel_custom/__init__.py
================================================
from spandrel import (
MAIN_REGISTRY,
ArchRegistry,
ArchSupport,
)
from .architectures import FDAT
CUSTOM_REGISTRY = ArchRegistry()
CUSTOM_REGISTRY.add(
ArchSupport.from_architecture(FDAT.FDATArch()),
)
def install(*, ignore_duplicates: bool = False) -> list[ArchSupport]:
"""
Try to install the extra architectures into the main registry.
If `ignore_duplicates` is True, the function will not raise an error
if the installation fails due to any of the architectures having already
been installed (but they won't be replaced by ones from this package).
"""
return MAIN_REGISTRY.add(*CUSTOM_REGISTRY, ignore_duplicates=ignore_duplicates)
__all__ = [
"CUSTOM_REGISTRY",
"install",
]
================================================
FILE: MangaJaNaiConverterGui/backend/src/spandrel_custom/architectures/FDAT/__arch/LICENSE
================================================
MIT License
Copyright (c) 2025 Vaibhav Bhat
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: MangaJaNaiConverterGui/backend/src/spandrel_custom/architectures/FDAT/__arch/fdat.py
================================================
# https://github.com/stinkybread/fdat/blob/main/fdat.py
from __future__ import annotations
import math
from typing import Literal
import numpy as np
import torch
import torch.nn.functional as F # noqa: N812
from einops import rearrange
from torch import Tensor, nn
from torch.nn.init import trunc_normal_
from torch.nn.modules.module import _IncompatibleKeys # type: ignore
from spandrel.__helpers.model_descriptor import StateDict
from spandrel.util import store_hyperparameters
from spandrel.util.timm import DropPath
SampleMods = Literal[
"conv",
"pixelshuffledirect",
"pixelshuffle",
"nearest+conv",
"dysample",
]
SampleMods3 = Literal[SampleMods, "transpose+conv", "lda", "pa_up"]
class DySample(nn.Module):
"""Adapted from 'Learning to Upsample by Learning to Sample':
https://arxiv.org/abs/2308.15085
https://github.com/tiny-smart/dysample
"""
def __init__(
self,
in_channels: int = 64,
out_ch: int = 3,
scale: int = 2,
groups: int = 4,
end_convolution: bool = True,
end_kernel=1,
) -> None:
super().__init__()
if in_channels <= groups or in_channels % groups != 0:
msg = "Incorrect in_channels and groups values."
raise ValueError(msg)
out_channels = 2 * groups * scale**2
self.scale = scale
self.groups = groups
self.end_convolution = end_convolution
if end_convolution:
self.end_conv = nn.Conv2d(
in_channels, out_ch, end_kernel, 1, end_kernel // 2
)
self.offset = nn.Conv2d(in_channels, out_channels, 1)
self.scope = nn.Conv2d(in_channels, out_channels, 1, bias=False)
if self.training:
nn.init.trunc_normal_(self.offset.weight, std=0.02)
nn.init.constant_(self.scope.weight, val=0)
self.register_buffer("init_pos", self._init_pos())
def _init_pos(self) -> Tensor:
h = torch.arange((-self.scale + 1) / 2, (self.scale - 1) / 2 + 1) / self.scale
return (
torch.stack(torch.meshgrid([h, h], indexing="ij"))
.transpose(1, 2)
.repeat(1, self.groups, 1)
.reshape(1, -1, 1, 1)
)
def forward(self, x: Tensor) -> Tensor:
offset = self.offset(x) * self.scope(x).sigmoid() * 0.5 + self.init_pos
B, _, H, W = offset.shape
offset = offset.view(B, 2, -1, H, W)
coords_h = torch.arange(H) + 0.5
coords_w = torch.arange(W) + 0.5
coords = (
torch.stack(torch.meshgrid([coords_w, coords_h], indexing="ij"))
.transpose(1, 2)
.unsqueeze(1)
.unsqueeze(0)
.type(x.dtype)
.to(x.device, non_blocking=True)
)
normalizer = torch.tensor(
[W, H], dtype=x.dtype, device=x.device, pin_memory=True
).view(1, 2, 1, 1, 1)
coords = 2 * (coords + offset) / normalizer - 1
coords = (
F.pixel_shuffle(coords.reshape(B, -1, H, W), self.scale)
.view(B, 2, -1, self.scale * H, self.scale * W)
.permute(0, 2, 3, 4, 1)
.contiguous()
.flatten(0, 1)
)
output = F.grid_sample(
x.reshape(B * self.groups, -1, H, W),
coords,
mode="bilinear",
align_corners=False,
padding_mode="border",
).view(B, -1, self.scale * H, self.scale * W)
if self.end_convolution:
output = self.end_conv(output)
return output
class LayerNorm(nn.Module):
def __init__(self, dim: int = 64, eps: float = 1e-6) -> None:
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.bias = nn.Parameter(torch.zeros(dim))
self.eps = eps
self.dim = (dim,)
def forward(self, x):
if x.is_contiguous(memory_format=torch.channels_last):
return F.layer_norm(
x.permute(0, 2, 3, 1), self.dim, self.weight, self.bias, self.eps
).permute(0, 3, 1, 2)
u = x.mean(1, keepdim=True)
s = (x - u).pow(2).mean(1, keepdim=True)
x = (x - u) / torch.sqrt(s + self.eps)
return self.weight[:, None, None] * x + self.bias[:, None, None]
class LDA_AQU(nn.Module):
def __init__(
self,
in_channels=48,
reduction_factor=4,
nh=1,
scale_factor=2.0,
k_e=3,
k_u=3,
n_groups=2,
range_factor=11,
rpb=True,
) -> None:
super().__init__()
self.k_u = k_u
self.num_head = nh
self.scale_factor = scale_factor
self.n_groups = n_groups
self.offset_range_factor = range_factor
self.attn_dim = in_channels // (reduction_factor * self.num_head)
self.scale = self.attn_dim**-0.5
self.rpb = rpb
self.hidden_dim = in_channels // reduction_factor
self.proj_q = nn.Conv2d(
in_channels, self.hidden_dim, kernel_size=1, stride=1, padding=0, bias=False
)
self.proj_k = nn.Conv2d(
in_channels, self.hidden_dim, kernel_size=1, stride=1, padding=0, bias=False
)
self.group_channel = in_channels // (reduction_factor * self.n_groups)
self.conv_offset = nn.Sequential(
nn.Conv2d(
self.group_channel,
self.group_channel,
3,
1,
1,
groups=self.group_channel,
bias=False,
),
LayerNorm(self.group_channel),
nn.SiLU(),
nn.Conv2d(self.group_channel, 2 * k_u**2, k_e, 1, k_e // 2),
)
self.layer_norm = LayerNorm(in_channels)
self.pad = int((self.k_u - 1) / 2)
base = np.arange(-self.pad, self.pad + 1).astype(np.float32)
base_y = np.repeat(base, self.k_u)
base_x = np.tile(base, self.k_u)
base_offset = np.stack([base_y, base_x], axis=1).flatten()
base_offset = torch.tensor(base_offset).view(1, -1, 1, 1)
self.register_buffer("base_offset", base_offset, persistent=False)
if self.rpb:
self.relative_position_bias_table = nn.Parameter(
torch.zeros(
1, self.num_head, 1, self.k_u**2, self.hidden_dim // self.num_head
)
)
nn.init.trunc_normal_(self.relative_position_bias_table, std=0.02)
def init_weights(self) -> None:
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.xavier_uniform(m)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
nn.init.constant_(self.conv_offset[-1].weight, 0)
nn.init.constant_(self.conv_offset[-1].bias, 0)
def get_offset(self, offset, Hout, Wout):
B, _, _, _ = offset.shape
device = offset.device
row_indices = torch.arange(Hout, device=device)
col_indices = torch.arange(Wout, device=device)
row_indices, col_indices = torch.meshgrid(row_indices, col_indices)
index_tensor = torch.stack((row_indices, col_indices), dim=-1).view(
1, Hout, Wout, 2
)
offset = rearrange(
offset, "b (kh kw d) h w -> b kh h kw w d", kh=self.k_u, kw=self.k_u
)
offset = offset + index_tensor.view(1, 1, Hout, 1, Wout, 2)
offset = offset.contiguous().view(B, self.k_u * Hout, self.k_u * Wout, 2)
offset[..., 0] = 2 * offset[..., 0] / (Hout - 1) - 1
offset[..., 1] = 2 * offset[..., 1] / (Wout - 1) - 1
offset = offset.flip(-1)
return offset
def extract_feats(self, x, offset, ks=3):
out = nn.functional.grid_sample(
x,
offset,
mode="bilinear",
padding_mode="zeros",
align_corners=True,
)
out = rearrange(out, "b c (ksh h) (ksw w) -> b (ksh ksw) c h w", ksh=ks, ksw=ks)
return out
def forward(self, x):
B, C, H, W = x.shape
out_H, out_W = int(H * self.scale_factor), int(W * self.scale_factor)
v = x
x = self.layer_norm(x)
q = self.proj_q(x)
k = self.proj_k(x)
q = torch.nn.functional.interpolate(
q, (out_H, out_W), mode="bilinear", align_corners=True
)
q_off = q.view(B * self.n_groups, -1, out_H, out_W)
pred_offset = self.conv_offset(q_off)
offset = pred_offset.tanh().mul(self.offset_range_factor) + self.base_offset.to(
x.dtype
)
k = k.view(B * self.n_groups, self.hidden_dim // self.n_groups, H, W)
v = v.view(B * self.n_groups, C // self.n_groups, H, W)
offset = self.get_offset(offset, out_H, out_W)
k = self.extract_feats(k, offset=offset)
v = self.extract_feats(v, offset=offset)
q = rearrange(q, "b (nh c) h w -> b nh (h w) () c", nh=self.num_head)
k = rearrange(k, "(b g) n c h w -> b (h w) n (g c)", g=self.n_groups)
v = rearrange(v, "(b g) n c h w -> b (h w) n (g c)", g=self.n_groups)
k = rearrange(k, "b n1 n (nh c) -> b nh n1 n c", nh=self.num_head)
v = rearrange(v, "b n1 n (nh c) -> b nh n1 n c", nh=self.num_head)
if self.rpb:
k = k + self.relative_position_bias_table
q = q * self.scale
attn = q @ k.transpose(-1, -2)
attn = attn.softmax(dim=-1)
out = attn @ v
out = rearrange(out, "b nh (h w) t c -> b (nh c) (t h) w", h=out_H)
return out
class PA(nn.Module):
def __init__(self, dim) -> None:
super().__init__()
self.conv = nn.Sequential(nn.Conv2d(dim, dim, 1), nn.Sigmoid())
def forward(self, x):
return x.mul(self.conv(x))
class UniUpsampleV3(nn.Sequential):
def __init__(
self,
upsample: SampleMods3 = "pa_up",
scale: int = 2,
in_dim: int = 48,
out_dim: int = 3,
mid_dim: int = 48,
group: int = 4, # Only DySample
dysample_end_kernel=1, # needed only for compatibility with version 2
) -> None:
m = []
if scale == 1 or upsample == "conv":
m.append(nn.Conv2d(in_dim, out_dim, 3, 1, 1))
elif upsample == "pixelshuffledirect":
m.extend(
[nn.Conv2d(in_dim, out_dim * scale**2, 3, 1, 1), nn.PixelShuffle(scale)]
)
elif upsample == "pixelshuffle":
m.extend([nn.Conv2d(in_dim, mid_dim, 3, 1, 1), nn.LeakyReLU(inplace=True)])
if (scale & (scale - 1)) == 0: # scale = 2^n
for _ in range(int(math.log2(scale))):
m.extend(
[nn.Conv2d(mid_dim, 4 * mid_dim, 3, 1, 1), nn.PixelShuffle(2)]
)
elif scale == 3:
m.extend([nn.Conv2d(mid_dim, 9 * mid_dim, 3, 1, 1), nn.PixelShuffle(3)])
else:
raise ValueError(
f"scale {scale} is not supported. Supported scales: 2^n and 3."
)
m.append(nn.Conv2d(mid_dim, out_dim, 3, 1, 1))
elif upsample == "nearest+conv":
if (scale & (scale - 1)) == 0:
for _ in range(int(math.log2(scale))):
m.extend(
(
nn.Conv2d(in_dim, in_dim, 3, 1, 1),
nn.Upsample(scale_factor=2),
nn.LeakyReLU(negative_slope=0.2, inplace=True),
)
)
m.extend(
(
nn.Conv2d(in_dim, in_dim, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.2, inplace=True),
)
)
elif scale == 3:
m.extend(
(
nn.Conv2d(in_dim, in_dim, 3, 1, 1),
nn.Upsample(scale_factor=scale),
nn.LeakyReLU(negative_slope=0.2, inplace=True),
nn.Conv2d(in_dim, in_dim, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.2, inplace=True),
)
)
else:
raise ValueError(
f"scale {scale} is not supported. Supported scales: 2^n and 3."
)
m.append(nn.Conv2d(in_dim, out_dim, 3, 1, 1))
elif upsample == "dysample":
if mid_dim != in_dim:
m.extend(
[nn.Conv2d(in_dim, mid_dim, 3, 1, 1), nn.LeakyReLU(inplace=True)]
)
m.append(
DySample(mid_dim, out_dim, scale, group, end_kernel=dysample_end_kernel)
)
# m.append(nn.Conv2d(mid_dim, out_dim, dysample_end_kernel, 1, dysample_end_kernel//2)) # kernel 1 causes chromatic artifacts
elif upsample == "transpose+conv":
if scale == 2:
m.append(nn.ConvTranspose2d(in_dim, out_dim, 4, 2, 1))
elif scale == 3:
m.append(nn.ConvTranspose2d(in_dim, out_dim, 3, 3, 0))
elif scale == 4:
m.extend(
[
nn.ConvTranspose2d(in_dim, in_dim, 4, 2, 1),
nn.GELU(),
nn.ConvTranspose2d(in_dim, out_dim, 4, 2, 1),
]
)
else:
raise ValueError(
f"scale {scale} is not supported. Supported scales: 2, 3, 4"
)
m.append(nn.Conv2d(out_dim, out_dim, 3, 1, 1))
elif upsample == "lda":
if mid_dim != in_dim:
m.extend(
[nn.Conv2d(in_dim, mid_dim, 3, 1, 1), nn.LeakyReLU(inplace=True)]
)
m.append(LDA_AQU(mid_dim, scale_factor=scale))
m.append(nn.Conv2d(mid_dim, out_dim, 3, 1, 1))
elif upsample == "pa_up":
if (scale & (scale - 1)) == 0:
for _ in range(int(math.log2(scale))):
m.extend(
[
nn.Upsample(scale_factor=2),
nn.Conv2d(in_dim, mid_dim, 3, 1, 1),
PA(mid_dim),
nn.LeakyReLU(negative_slope=0.2, inplace=True),
nn.Conv2d(mid_dim, mid_dim, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.2, inplace=True),
]
)
in_dim = mid_dim
elif scale == 3:
m.extend(
[
nn.Upsample(scale_factor=3),
nn.Conv2d(in_dim, mid_dim, 3, 1, 1),
PA(mid_dim),
nn.LeakyReLU(negative_slope=0.2, inplace=True),
nn.Conv2d(mid_dim, mid_dim, 3, 1, 1),
nn.LeakyReLU(negative_slope=0.2, inplace=True),
]
)
else:
raise ValueError(
f"scale {scale} is not supported. Supported scales: 2^n and 3."
)
m.append(nn.Conv2d(mid_dim, out_dim, 3, 1, 1))
else:
raise ValueError(
f"An invalid Upsample was selected. Please choose one of {SampleMods}"
)
super().__init__(*m)
self.register_buffer(
"MetaUpsample",
torch.tensor(
[
3, # Block version, if you change something, please number from the end so that you can distinguish between authorized changes and third parties
list(SampleMods3.__args__).index(upsample), # UpSample method index
scale,
in_dim,
out_dim,
mid_dim,
group,
],
dtype=torch.uint8,
),
)
# --- fdat Components ---
class FastSpatialWindowAttention(nn.Module):
def __init__(self, dim, window_size=8, num_heads=4, qkv_bias=False) -> None:
super().__init__()
self.dim, self.ws, self.nh = dim, window_size, num_heads
self.scale = (dim // num_heads) ** -0.5
self.qkv, self.proj = (
nn.Linear(dim, dim * 3, bias=qkv_bias),
nn.Linear(dim, dim),
)
self.bias = nn.Parameter(
torch.zeros(num_heads, window_size * window_size, window_size * window_size)
)
trunc_normal_(self.bias, std=0.02)
def forward(self, x, H, W):
B, L, C = x.shape
pad_r, pad_b = (
(self.ws - W % self.ws) % self.ws,
(self.ws - H % self.ws) % self.ws,
)
if pad_r > 0 or pad_b > 0:
x = F.pad(x.view(B, H, W, C), (0, 0, 0, pad_r, 0, pad_b)).view(B, -1, C)
H_pad, W_pad = H + pad_b, W + pad_r
x = (
x.view(B, H_pad // self.ws, self.ws, W_pad // self.ws, self.ws, C)
.permute(0, 1, 3, 2, 4, 5)
.contiguous()
.view(-1, self.ws * self.ws, C)
)
qkv = (
self.qkv(x)
.view(-1, self.ws * self.ws, 3, self.nh, C // self.nh)
.permute(2, 0, 3, 1, 4)
)
q, k, v = qkv.unbind(0)
attn = (q * self.scale @ k.transpose(-2, -1)) + self.bias
x = (
(F.softmax(attn, dim=-1) @ v)
.transpose(1, 2)
.reshape(-1, self.ws * self.ws, C)
)
x = (
self.proj(x)
.view(B, H_pad // self.ws, W_pad // self.ws, self.ws, self.ws, C)
.permute(0, 1, 3, 2, 4, 5)
.contiguous()
.view(B, H_pad, W_pad, C)
)
if pad_r > 0 or pad_b > 0:
x = x[:, :H, :W, :].contiguous()
return x.view(B, L, C)
class FastChannelAttention(nn.Module):
def __init__(self, dim, num_heads=4, qkv_bias=False) -> None:
super().__init__()
self.nh = num_heads
self.temp = nn.Parameter(torch.ones(num_heads, 1, 1))
self.qkv, self.proj = (
nn.Linear(dim, dim * 3, bias=qkv_bias),
nn.Linear(dim, dim),
)
def forward(self, x, H, W): # H, W are unused but kept for API consistency
B, N, C = x.shape
qkv = self.qkv(x).view(B, N, 3, self.nh, C // self.nh).permute(2, 0, 3, 1, 4)
q, k, v = qkv.unbind(0)
q, k = (
F.normalize(q.transpose(-2, -1), dim=-1),
F.normalize(k.transpose(-2, -1), dim=-1),
)
attn = F.softmax((q @ k.transpose(-2, -1)) * self.temp, dim=-1)
return self.proj(
(attn @ v.transpose(-2, -1)).permute(0, 3, 1, 2).reshape(B, N, C)
)
class SimplifiedAIM(nn.Module):
def __init__(self, dim, reduction_ratio=8) -> None:
super().__init__()
self.sg = nn.Sequential(nn.Conv2d(dim, 1, 1, bias=False), nn.Sigmoid())
self.cg = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Conv2d(dim, dim // reduction_ratio, 1, bias=False),
nn.GELU(),
nn.Conv2d(dim // reduction_ratio, dim, 1, bias=False),
nn.Sigmoid(),
)
def forward(self, attn_feat, conv_feat, interaction_type, H, W):
B, L, C = attn_feat.shape
if interaction_type == "spatial_modulates_channel":
sm = (
self.sg(attn_feat.transpose(1, 2).view(B, C, H, W))
.view(B, 1, L)
.transpose(1, 2)
)
return attn_feat + (conv_feat * sm)
else:
cm = (
self.cg(conv_feat.transpose(1, 2).view(B, C, H, W))
.view(B, C, 1)
.transpose(1, 2)
)
return (attn_feat * cm) + conv_feat
class SimplifiedFFN(nn.Module):
def __init__(self, dim, expansion_ratio=2.0, drop=0.0) -> None:
super().__init__()
hd = int(dim * expansion_ratio)
self.fc1, self.act, self.fc2 = (
nn.Linear(dim, hd, False),
nn.GELU(),
nn.Linear(hd, dim, False),
)
self.drop = nn.Dropout(drop)
self.smix = nn.Conv2d(hd, hd, 3, 1, 1, groups=hd, bias=False)
def forward(self, x, H, W):
B, L, _C = x.shape
x = self.drop(self.act(self.fc1(x)))
x_s = (
self.smix(x.transpose(1, 2).view(B, x.shape[-1], H, W))
.view(B, x.shape[-1], L)
.transpose(1, 2)
)
return self.drop(self.fc2(x_s))
class SimplifiedDATBlock(nn.Module):
def __init__(self, dim, nh, ws, ffn_exp, aim_re, btype, dp, qkv_b=False) -> None:
super().__init__()
self.btype = btype
self.n1, self.n2 = nn.LayerNorm(dim), nn.LayerNorm(dim)
self.attn = (
FastSpatialWindowAttention(dim, ws, nh, qkv_b)
if btype == "spatial"
else FastChannelAttention(dim, nh, qkv_b)
)
self.conv = nn.Sequential(
nn.Conv2d(dim, dim, 3, 1, 1, groups=dim, bias=False), nn.GELU()
)
self.inter = SimplifiedAIM(dim, aim_re)
self.dp = DropPath(dp) if dp > 0.0 else nn.Identity()
self.ffn = SimplifiedFFN(dim, ffn_exp)
def _conv_fwd(self, x, H, W):
B, L, C = x.shape
return (
self.conv(x.transpose(1, 2).view(B, C, H, W)).view(B, C, L).transpose(1, 2)
)
def forward(self, x, H, W):
n1 = self.n1(x)
itype = (
"channel_modulates_spatial"
if self.btype == "spatial"
else "spatial_modulates_channel"
)
fused = self.inter(self.attn(n1, H, W), self._conv_fwd(n1, H, W), itype, H, W)
x = x + self.dp(fused)
x = x + self.dp(self.ffn(self.n2(x), H, W))
return x
class SimplifiedResidualGroup(nn.Module):
def __init__(self, dim, depth, nh, ws, ffn_exp, aim_re, pattern, dp_rates) -> None:
super().__init__()
self.blocks = nn.ModuleList(
[
SimplifiedDATBlock(
dim, nh, ws, ffn_exp, aim_re, pattern[i % len(pattern)], dp_rates[i]
)
for i in range(depth)
]
)
self.conv = nn.Conv2d(dim, dim, 3, 1, 1, bias=False)
def forward(self, x: Tensor) -> Tensor:
B, C, H, W = x.shape
x_seq = x.view(B, C, H * W).transpose(1, 2).contiguous()
for block in self.blocks:
x_seq = block(x_seq, H, W)
return self.conv(x_seq.transpose(1, 2).view(B, C, H, W)) + x
@store_hyperparameters()
class FDAT(nn.Module):
hyperparameters = {}
def __init__(
self,
*,
num_in_ch: int = 3,
num_out_ch: int = 3,
scale: int = 4,
embed_dim: int = 120,
num_groups: int = 4,
depth_per_group: int = 3,
num_heads: int = 4,
window_size: int = 8,
ffn_expansion_ratio: float = 2.0,
aim_reduction_ratio: int = 8,
group_block_pattern: list[str] | None = None,
drop_path_rate: float = 0.1,
mid_dim: int = 64,
upsampler_type: SampleMods3 = "transpose+conv",
img_range: float = 1.0,
unshuffle_mod: bool = False,
) -> None:
if group_block_pattern is None:
group_block_pattern = ["spatial", "channel"]
super().__init__()
self.img_range, self.upscale = img_range, scale
self.mean = torch.zeros(1, 1, 1, 1)
self.pad = 0
if unshuffle_mod and scale < 3:
unshuffle = 4 // scale
scale = 4
self.conv_first = nn.Sequential(
nn.PixelUnshuffle(unshuffle),
nn.Conv2d(num_in_ch * unshuffle**2, embed_dim, 3, 1, 1, bias=True),
)
self.pad = unshuffle
else:
self.conv_first = nn.Conv2d(num_in_ch, embed_dim, 3, 1, 1, bias=True)
ad = depth_per_group * len(group_block_pattern)
td = num_groups * ad
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, td)]
self.groups = nn.Sequential(
*[
SimplifiedResidualGroup(
embed_dim,
ad,
num_heads,
window_size,
ffn_expansion_ratio,
aim_reduction_ratio,
group_block_pattern,
dpr[i * ad : (i + 1) * ad],
)
for i in range(num_groups)
]
)
self.conv_after = nn.Conv2d(embed_dim, embed_dim, 3, 1, 1, bias=False)
self.upsampler = UniUpsampleV3(
upsampler_type, scale, embed_dim, num_out_ch, mid_dim, 4
)
self.apply(self._init_weights)
def load_state_dict(
self,
state_dict: StateDict,
*args, # noqa: ANN002
**kwargs,
) -> _IncompatibleKeys:
state_dict["upsampler.MetaUpsample"] = self.upsampler.MetaUpsample
return super().load_state_dict(state_dict, *args, **kwargs)
def _init_weights(self, m: nn.Module) -> None:
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=0.02)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Conv2d):
trunc_normal_(m.weight, std=0.02)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm | nn.GroupNorm):
if hasattr(m, "bias") and m.bias is not None:
nn.init.constant_(m.bias, 0)
if hasattr(m, "weight") and m.weight is not None:
nn.init.constant_(m.weight, 1.0)
def check_img_size(self, x: Tensor, h: int, w: int) -> Tensor:
if self.pad == 0:
return x
mod_pad_h = (self.pad - h % self.pad) % self.pad
mod_pad_w = (self.pad - w % self.pad) % self.pad
return F.pad(x, (0, mod_pad_w, 0, mod_pad_h), "reflect")
def forward(self, x: Tensor) -> Tensor:
_b, _c, h, w = x.shape
x = self.check_img_size(x, h, w)
x_shallow = self.conv_first(x)
x_deep = self.groups(x_shallow)
x_deep = self.conv_after(x_deep)
x_out = self.upsampler(x_deep + x_shallow)
return x_out[:, :, : h * self.upscale, : w * self.upscale]
================================================
FILE: MangaJaNaiConverterGui/backend/src/spandrel_custom/architectures/FDAT/__init__.py
================================================
import math
from spandrel.util import KeyCondition, get_seq_len
from spandrel.__helpers.model_descriptor import Architecture, ImageModelDescriptor, StateDict
from .__arch.fdat import FDAT, SampleMods3
class FDATArch(Architecture[FDAT]):
def __init__(self):
super().__init__(
id="FDAT",
detect=KeyCondition.has_any(
KeyCondition.has_all(
"conv_first.weight",
"groups.0.blocks.0.attn.bias",
"groups.0.blocks.0.inter.cg.1.weight",
"groups.0.blocks.0.ffn.fc1.weight",
"groups.0.blocks.0.n1.weight",
"upsampler.MetaUpsample",
),
KeyCondition.has_all(
"conv_first.1.weight",
"groups.0.blocks.0.attn.bias",
"groups.0.blocks.0.inter.cg.1.weight",
"groups.0.blocks.0.ffn.fc1.weight",
"groups.0.blocks.0.n1.weight",
"upsampler.MetaUpsample",
),
),
)
def load(self, state_dict: StateDict) -> ImageModelDescriptor[FDAT]:
_, upsampler_index, scale, embed_dim, num_out_ch, mid_dim, _ = state_dict[
"upsampler.MetaUpsample"
].tolist()
upsampler_type = list(SampleMods3.__args__)[upsampler_index]
if "conv_first.1.weight" in state_dict:
num_in_ch = num_out_ch
scale = 4 // (
math.isqrt(state_dict["conv_first.1.weight"].shape[1] // num_in_ch)
)
unshuffle_mod = True
else:
unshuffle_mod = False
num_in_ch = state_dict["conv_first.weight"].shape[1]
num_groups = get_seq_len(state_dict, "groups")
group_block_pattern = ["spatial", "channel"]
depth_per_group = get_seq_len(state_dict, "groups.0.blocks") // len(
group_block_pattern
)
num_heads = state_dict["groups.0.blocks.0.attn.bias"].shape[0]
window_size = math.isqrt(state_dict["groups.0.blocks.0.attn.bias"].shape[2])
ffn_expansion_ratio = float(
state_dict["groups.0.blocks.0.ffn.fc1.weight"].shape[0] / embed_dim
)
aim_reduction_ratio = (
embed_dim // state_dict["groups.0.blocks.0.inter.cg.1.weight"].shape[0]
)
img_range = 1.0
model = FDAT(
num_in_ch=num_in_ch,
num_out_ch=num_out_ch,
scale=scale,
embed_dim=embed_dim,
num_groups=num_groups,
depth_per_group=depth_per_group,
num_heads=num_heads,
window_size=window_size,
ffn_expansion_ratio=ffn_expansion_ratio,
aim_reduction_ratio=aim_reduction_ratio,
group_block_pattern=None,
upsampler_type=upsampler_type,
mid_dim=mid_dim,
img_range=img_range,
unshuffle_mod=unshuffle_mod,
)
sizes = {96: "tiny", 108: "light", 120: "medium", 180: "large"}
size_tag = None
if embed_dim in sizes:
size_tag = sizes[embed_dim]
if num_groups == 6:
size_tag = "xl"
tags = [
f"{embed_dim}dim",
upsampler_type,
]
if size_tag:
tags.append(size_tag)
if unshuffle_mod:
tags.append("unshuffle")
return ImageModelDescriptor(
model,
state_dict,
architecture=self,
purpose="Restoration" if scale == 1 else "SR",
tags=tags,
supports_half=True,
supports_bfloat16=True,
scale=scale,
input_channels=num_in_ch,
output_channels=num_out_ch,
)
__all__ = ["FDATArch", "FDAT"]
================================================
FILE: MangaJaNaiConverterGui/backend/src/system.py
================================================
import platform
import sys
is_mac = sys.platform == "darwin"
is_arm_mac = is_mac and platform.machine() == "arm64"
is_windows = sys.platform == "win32"
is_linux = sys.platform == "linux"
================================================
FILE: MangaJaNaiConverterGui/backend/src/test_accelerators.py
================================================
#!/usr/bin/env python3
"""
Test script for the new accelerator detection system.
"""
import sys
import torch
from accelerator_detection import get_accelerator_detector, AcceleratorType
def test_accelerator_detection():
"""Test the accelerator detection system"""
print("=== PyTorch Accelerator Detection Test ===\n")
# Get detector
detector = get_accelerator_detector()
# Show PyTorch version
print(f"PyTorch Version: {torch.__version__}")
if hasattr(torch.version, 'hip') and torch.version.hip:
print(f"ROCm Version: {torch.version.hip}")
print()
# Get all devices
all_devices = detector.available_devices
print(f"Detected {len(all_devices)} device(s):\n")
for i, device in enumerate(all_devices):
print(f"Device {i}: {device.name}")
print(f" Type: {device.type.value.upper()}")
print(f" Index: {device.index}")
print(f" Device String: {device.device_string}")
print(f" Torch Device: {device.torch_device}")
print(f" FP16 Support: {device.supports_fp16}")
print(f" BF16 Support: {device.supports_bf16}")
if device.memory_total:
total_gb = device.memory_total / (1024**3)
print(f" Total Memory: {total_gb:.2f} GB")
if device.memory_free:
free_gb = device.memory_free / (1024**3)
print(f" Free Memory: {free_gb:.2f} GB")
print()
# Test device selection
print("=== Device Selection Tests ===\n")
best_device = detector.get_best_device()
print(f"Best Device: {best_device.name} ({best_device.type.value})")
cpu_device = detector.get_cpu_device()
print(f"CPU Device: {cpu_device.name}")
# Test by type
for device_type in AcceleratorType:
devices = detector.get_devices_by_type(device_type)
if devices:
print(f"{device_type.value.upper()} devices: {len(devices)}")
for device in devices:
print(f" - {device.name}")
print("\n=== Simple Tensor Test ===\n")
# Test with best device
try:
test_device = best_device.torch_device
print(f"Testing tensor creation on {test_device}")
# Create a simple tensor
x = torch.tensor([1.0, 2.0, 3.0]).to(test_device)
y = torch.tensor([4.0, 5.0, 6.0]).to(test_device)
z = x + y
print(f"Tensor computation successful: {z.cpu().tolist()}")
# Test autocast if supported
from accelerator_detection import get_autocast_device_type, is_device_type_supported_for_autocast
autocast_device_type = get_autocast_device_type(test_device)
autocast_supported = is_device_type_supported_for_autocast(test_device)
print(f"Autocast device type: {autocast_device_type}")
print(f"Autocast supported: {autocast_supported}")
if autocast_supported:
with torch.autocast(device_type=autocast_device_type, dtype=torch.float16, enabled=True):
z_autocast = x * y
print(f"Autocast computation successful: {z_autocast.cpu().tolist()}")
except Exception as e:
print(f"Tensor test failed: {e}")
print("\n=== Test Completed ===")
if __name__ == "__main__":
test_accelerator_detection()
================================================
FILE: MangaJaNaiConverterGui.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34031.279
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MangaJaNaiConverterGui", "MangaJaNaiConverterGui\MangaJaNaiConverterGui.csproj", "{A0F428F0-2B21-415A-B6F0-23D52FB3300E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9FA41E85-6A01-4BB3-90FB-36C1A91C1D52}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A0F428F0-2B21-415A-B6F0-23D52FB3300E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0F428F0-2B21-415A-B6F0-23D52FB3300E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0F428F0-2B21-415A-B6F0-23D52FB3300E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0F428F0-2B21-415A-B6F0-23D52FB3300E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {922771E1-4DB5-4BA6-8485-66F1D46AEC92}
EndGlobalSection
EndGlobal
================================================
FILE: README.md
================================================
# MangaJaNaiConverterGui
[](https://discord.gg/EeFfZUBvxj)
## Overview
This project provides a Windows GUI for upscaling manga images and archives with PyTorch models. It includes a set of models optimized for upscaling manga with Japanese and English text, but many other models are also supported. It also utilizes linear light downscaling to minimize halftone artifacts when downscaling.

## Instructions
Simply download [MangaJaNaiConverterGui-win-Setup.exe](https://github.com/the-database/MangaJaNaiConverterGui/releases/latest/download/MangaJaNaiConverterGui-win-Setup.exe) (if you want to install the app) or [MangaJaNaiConverterGui-win-Portable.zip](https://github.com/the-database/MangaJaNaiConverterGui/releases/latest/download/MangaJaNaiConverterGui-win-Portable.zip) (if you want a portable version of the app that isn't installed) from the [latest release](https://github.com/the-database/MangaJaNaiConverterGui/releases). Select your input file or folder, choose your upscale settings, and click Upscale.
### Important Note for NVIDIA Users
First, please ensure that you are running the latest NVIDIA drivers. To avoid major slowdowns while upscaling, open NVIDIA Control Panel and set the **CUDA - Sysmem Fallback Policy** setting to **Prefer No Sysmem Fallback**.

### For Linux users
Check out [this README](MangaJaNaiConverterGui/backend/src/README.md).
## Resources
- [OpenModelDB](https://openmodeldb.info/): Repository of AI upscaling models.
## Related Projects
- [MangaJaNai](https://github.com/the-database/mangajanai): Main repository for manga upscaling models.
- [VideoJaNai](https://github.com/the-database/VideoJaNai): Windows GUI for video upscaling with extremely fast performance.
- [traiNNer-redux](https://github.com/the-database/traiNNer-redux): Software for training upscaling models.
## Acknowledgments
- [chaiNNer](https://github.com/chaiNNer-org/chaiNNer): General purpose tool for AI upscaling. This project uses a modified version of its backend for running upscaling models.
================================================
FILE: pack.bat
================================================
vpk pack -u MangaJaNaiConverterGui -v 1.1.3 -p ".\MangaJaNaiConverterGui\bin\Release\net8.0\publish\win-x64" -i ./MangaJaNaiConverterGui/assets/logo.ico -e MangaJaNaiConverterGui.exe