[
  {
    "path": ".editorconfig",
    "content": "# Remove the line below if you want to inherit .editorconfig settings from higher directories\nroot = true\n\n# C# files\n[*.cs]\n\n#### Core EditorConfig Options ####\n\n# Indentation and spacing\nindent_size = 4\nindent_style = space\ntab_width = 4\n\n# New line preferences\nend_of_line = crlf\ninsert_final_newline = false\n\n#### .NET Coding Conventions ####\n\n# Organize usings\ndotnet_separate_import_directive_groups = false\ndotnet_sort_system_directives_first = false\nfile_header_template = unset\n\n# this. and Me. preferences\ndotnet_style_qualification_for_event = false\ndotnet_style_qualification_for_field = false\ndotnet_style_qualification_for_method = false\ndotnet_style_qualification_for_property = false\n\n# Language keywords vs BCL types preferences\ndotnet_style_predefined_type_for_locals_parameters_members = true\ndotnet_style_predefined_type_for_member_access = true\n\n# Parentheses preferences\ndotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity\ndotnet_style_parentheses_in_other_binary_operators = always_for_clarity\ndotnet_style_parentheses_in_other_operators = never_if_unnecessary\ndotnet_style_parentheses_in_relational_binary_operators = always_for_clarity\n\n# Modifier preferences\ndotnet_style_require_accessibility_modifiers = for_non_interface_members\n\n# Expression-level preferences\ndotnet_style_coalesce_expression = true\ndotnet_style_collection_initializer = true\ndotnet_style_explicit_tuple_names = true\ndotnet_style_namespace_match_folder = true\ndotnet_style_null_propagation = true\ndotnet_style_object_initializer = true\ndotnet_style_operator_placement_when_wrapping = beginning_of_line\ndotnet_style_prefer_auto_properties = true\ndotnet_style_prefer_collection_expression = when_types_loosely_match\ndotnet_style_prefer_compound_assignment = true\ndotnet_style_prefer_conditional_expression_over_assignment = true\ndotnet_style_prefer_conditional_expression_over_return = true\ndotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed\ndotnet_style_prefer_inferred_anonymous_type_member_names = true\ndotnet_style_prefer_inferred_tuple_names = true\ndotnet_style_prefer_is_null_check_over_reference_equality_method = true\ndotnet_style_prefer_simplified_boolean_expressions = true\ndotnet_style_prefer_simplified_interpolation = true\n\n# Field preferences\ndotnet_style_readonly_field = true\n\n# Parameter preferences\ndotnet_code_quality_unused_parameters = all\n\n# Suppression preferences\ndotnet_remove_unnecessary_suppression_exclusions = none\n\n# New line preferences\ndotnet_style_allow_multiple_blank_lines_experimental = true\ndotnet_style_allow_statement_immediately_after_block_experimental = true\n\n#### C# Coding Conventions ####\n\n# var preferences\ncsharp_style_var_elsewhere = false\ncsharp_style_var_for_built_in_types = false\ncsharp_style_var_when_type_is_apparent = false\n\n# Expression-bodied members\ncsharp_style_expression_bodied_accessors = true\ncsharp_style_expression_bodied_constructors = false\ncsharp_style_expression_bodied_indexers = true\ncsharp_style_expression_bodied_lambdas = true\ncsharp_style_expression_bodied_local_functions = false\ncsharp_style_expression_bodied_methods = false\ncsharp_style_expression_bodied_operators = false\ncsharp_style_expression_bodied_properties = true\n\n# Pattern matching preferences\ncsharp_style_pattern_matching_over_as_with_null_check = true\ncsharp_style_pattern_matching_over_is_with_cast_check = true\ncsharp_style_prefer_extended_property_pattern = true\ncsharp_style_prefer_not_pattern = true\ncsharp_style_prefer_pattern_matching = true\ncsharp_style_prefer_switch_expression = true\n\n# Null-checking preferences\ncsharp_style_conditional_delegate_call = true\n\n# Modifier preferences\ncsharp_prefer_static_local_function = true\ncsharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async\ncsharp_style_prefer_readonly_struct = true\ncsharp_style_prefer_readonly_struct_member = true\n\n# Code-block preferences\ncsharp_prefer_braces = true\ncsharp_prefer_simple_using_statement = true\ncsharp_style_namespace_declarations = block_scoped\ncsharp_style_prefer_method_group_conversion = true\ncsharp_style_prefer_primary_constructors = true\ncsharp_style_prefer_top_level_statements = true\n\n# Expression-level preferences\ncsharp_prefer_simple_default_expression = true\ncsharp_style_deconstructed_variable_declaration = true\ncsharp_style_implicit_object_creation_when_type_is_apparent = true\ncsharp_style_inlined_variable_declaration = true\ncsharp_style_prefer_index_operator = true\ncsharp_style_prefer_local_over_anonymous_function = true\ncsharp_style_prefer_null_check_over_type_check = true\ncsharp_style_prefer_range_operator = true\ncsharp_style_prefer_tuple_swap = true\ncsharp_style_prefer_utf8_string_literals = true\ncsharp_style_throw_expression = true\ncsharp_style_unused_value_assignment_preference = discard_variable\ncsharp_style_unused_value_expression_statement_preference = discard_variable\n\n# 'using' directive preferences\ncsharp_using_directive_placement = outside_namespace\n\n# New line preferences\ncsharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true\ncsharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true\ncsharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true\ncsharp_style_allow_blank_lines_between_consecutive_braces_experimental = true\ncsharp_style_allow_embedded_statements_on_same_line_experimental = true\n\n#### C# Formatting Rules ####\n\n# New line preferences\ncsharp_new_line_before_catch = true\ncsharp_new_line_before_else = true\ncsharp_new_line_before_finally = true\ncsharp_new_line_before_members_in_anonymous_types = true\ncsharp_new_line_before_members_in_object_initializers = true\ncsharp_new_line_before_open_brace = all\ncsharp_new_line_between_query_expression_clauses = true\n\n# Indentation preferences\ncsharp_indent_block_contents = true\ncsharp_indent_braces = false\ncsharp_indent_case_contents = true\ncsharp_indent_case_contents_when_block = true\ncsharp_indent_labels = one_less_than_current\ncsharp_indent_switch_labels = true\n\n# Space preferences\ncsharp_space_after_cast = false\ncsharp_space_after_colon_in_inheritance_clause = true\ncsharp_space_after_comma = true\ncsharp_space_after_dot = false\ncsharp_space_after_keywords_in_control_flow_statements = true\ncsharp_space_after_semicolon_in_for_statement = true\ncsharp_space_around_binary_operators = before_and_after\ncsharp_space_around_declaration_statements = false\ncsharp_space_before_colon_in_inheritance_clause = true\ncsharp_space_before_comma = false\ncsharp_space_before_dot = false\ncsharp_space_before_open_square_brackets = false\ncsharp_space_before_semicolon_in_for_statement = false\ncsharp_space_between_empty_square_brackets = false\ncsharp_space_between_method_call_empty_parameter_list_parentheses = false\ncsharp_space_between_method_call_name_and_opening_parenthesis = false\ncsharp_space_between_method_call_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_empty_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_name_and_open_parenthesis = false\ncsharp_space_between_method_declaration_parameter_list_parentheses = false\ncsharp_space_between_parentheses = false\ncsharp_space_between_square_brackets = false\n\n# Wrapping preferences\ncsharp_preserve_single_line_blocks = true\ncsharp_preserve_single_line_statements = true\n\n#### Naming styles ####\n\n# Naming rules\n\ndotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion\ndotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface\ndotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i\n\ndotnet_naming_rule.types_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.types_should_be_pascal_case.symbols = types\ndotnet_naming_rule.types_should_be_pascal_case.style = pascal_case\n\ndotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members\ndotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case\n\n# Symbol specifications\n\ndotnet_naming_symbols.interface.applicable_kinds = interface\ndotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.interface.required_modifiers = \n\ndotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum\ndotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.types.required_modifiers = \n\ndotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method\ndotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.non_field_members.required_modifiers = \n\n# Naming styles\n\ndotnet_naming_style.pascal_case.required_prefix = \ndotnet_naming_style.pascal_case.required_suffix = \ndotnet_naming_style.pascal_case.word_separator = \ndotnet_naming_style.pascal_case.capitalization = pascal_case\n\ndotnet_naming_style.begins_with_i.required_prefix = I\ndotnet_naming_style.begins_with_i.required_suffix = \ndotnet_naming_style.begins_with_i.word_separator = \ndotnet_naming_style.begins_with_i.capitalization = pascal_case\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy to GitHub Releases\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version number for the release'\n        required: true\n        default: ''\n\njobs:\n  deploy-to-github-releases:\n    runs-on: windows-latest\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n      - name: Install .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: 10.0.x\n      - name: Publish Application\n        run: dotnet publish MangaJaNaiConverterGui/MangaJaNaiConverterGui.csproj -c Release -o publish -r win-x64\n      - name: Create Velopack Release\n        run: |\n          dotnet tool install -g vpk --prerelease\n          vpk download github --repoUrl https://github.com/the-database/MangaJaNaiConverterGui\n          vpk pack -u MangaJaNaiConverterGui -v ${{ github.event.inputs.version }} -p publish  -i ./MangaJaNaiConverterGui/assets/logo.ico -e MangaJaNaiConverterGui.exe\n          vpk upload github --repoUrl https://github.com/the-database/MangaJaNaiConverterGui --releaseName \"${{ github.event.inputs.version }}\" --tag ${{ github.event.inputs.version }} --token ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# ASP.NET Scaffolding\nScaffoldingReadMe.txt\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.tlog\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*.json\ncoverage*.xml\ncoverage*.info\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio 6 auto-generated project file (contains which files were open etc.)\n*.vbp\n\n# Visual Studio 6 workspace and project file (working project files containing files to include in project)\n*.dsw\n*.dsp\n\n# Visual Studio 6 technical files\n*.ncb\n*.aps\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# Visual Studio History (VSHistory) files\n.vshistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Fody - auto-generated XML schema\nFodyWeavers.xsd\n\n# VS Code files for those working on multiple tools\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n*.code-workspace\n\n# Local History for Visual Studio Code\n.history/\n\n# Windows Installer files from build outputs\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# JetBrains Rider\n*.sln.iml\n\nMangaJaNaiConverterGui/chaiNNer/python/\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "MangaJaNaiConverterGui/App.axaml",
    "content": "<Application xmlns=\"https://github.com/avaloniaui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             x:Class=\"MangaJaNaiConverterGui.App\"\n             xmlns:local=\"using:MangaJaNaiConverterGui\"\n             RequestedThemeVariant=\"Default\"\n             xmlns:materialIcons=\"clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia\"\n             xmlns:sty=\"using:FluentAvalonia.Styling\">\n             <!-- \"Default\" ThemeVariant follows system theme variant. \"Dark\" or \"Light\" are other available options. -->\n\n    <Application.DataTemplates>\n        <local:ViewLocator/>\n    </Application.DataTemplates>\n  \n    <Application.Styles>\n      <materialIcons:MaterialIconStyles />\n      <sty:FluentAvaloniaTheme />\n    </Application.Styles>\n</Application>"
  },
  {
    "path": "MangaJaNaiConverterGui/App.axaml.cs",
    "content": "using Autofac;\nusing Avalonia;\nusing Avalonia.Markup.Xaml;\nusing MangaJaNaiConverterGui.Services;\nusing MangaJaNaiConverterGui.ViewModels;\nusing MangaJaNaiConverterGui.Views;\nusing ReactiveUI;\nusing Splat;\nusing Splat.Autofac;\nusing System.IO;\nusing ReactiveUI;\nusing ReactiveUI.Avalonia;\nusing Splat;\nusing Splat.Autofac;\n\nnamespace MangaJaNaiConverterGui\n{\n    public partial class App : Application\n    {\n        public override void Initialize()\n        {\n            AvaloniaXamlLoader.Load(this);\n        }\n\n        public override void OnFrameworkInitializationCompleted()\n        {\n            // Create a new Autofac container builder.\n            var builder = new ContainerBuilder();\n            builder.RegisterType<MainWindowViewModel>().AsSelf();\n            builder.RegisterType<PythonService>().As<IPythonService>().SingleInstance();\n            builder.RegisterType<UpdateManagerService>().As<IUpdateManagerService>().SingleInstance();\n            builder.RegisterType<SuspensionDriverService>().As<ISuspensionDriverService>().SingleInstance();\n            // etc.\n            // Register the Adapter to Splat.\n            // Creates and sets the Autofac resolver as the Locator.\n            var autofacResolver = builder.UseAutofacDependencyResolver();\n\n            // Register the resolver in Autofac so it can be later resolved.\n            builder.RegisterInstance(autofacResolver);\n\n            // Initialize ReactiveUI components.\n            autofacResolver.InitializeReactiveUI();\n\n            var container = builder.Build();\n\n            autofacResolver.SetLifetimeScope(container);\n\n            //var vm = container.Resolve<MainWindowViewModel>();\n            var umService = container.Resolve<IUpdateManagerService>();\n\n            if (umService.IsInstalled)\n            {\n                if (!Directory.Exists(Program.InstalledAppStateFolder))\n                {\n                    Directory.CreateDirectory(Program.InstalledAppStateFolder);\n                }\n\n                if (!File.Exists(Program.InstalledAppStatePath))\n                {\n                    File.Copy(Program.InstalledAppStateFilename, Program.InstalledAppStatePath);\n                }\n            }\n\n            var suspension = new AutoSuspendHelper(ApplicationLifetime);\n            RxApp.SuspensionHost.CreateNewAppState = () => new MainWindowViewModel();\n            RxApp.SuspensionHost.SetupDefaultSuspendResume(container.Resolve<ISuspensionDriverService>().SuspensionDriver);\n            suspension.OnFrameworkInitializationCompleted();\n\n            // Load the saved view model state.\n            var state = RxApp.SuspensionHost.GetAppState<MainWindowViewModel>();\n\n            foreach (var wf in state.Workflows)\n            {\n                wf.Vm = state;\n\n                foreach (var chain in wf.Chains)\n                {\n                    chain.Vm = state;\n                }\n            }\n\n            state.CurrentWorkflow?.Validate();\n\n            new MainWindow { DataContext = state }.Show();\n            base.OnFrameworkInitializationCompleted();\n        }\n    }\n}"
  },
  {
    "path": "MangaJaNaiConverterGui/Drivers/NewtonsoftJsonSuspensionDriver.cs",
    "content": "﻿using Newtonsoft.Json;\nusing ReactiveUI;\nusing System;\nusing System.IO;\nusing System.Reactive;\nusing System.Reactive.Linq;\n\nnamespace MangaJaNaiConverterGui.Drivers\n{\n    public class NewtonsoftJsonSuspensionDriver : ISuspensionDriver\n    {\n        private readonly string _file;\n        public static readonly JsonSerializerSettings Settings = new()\n        {\n            TypeNameHandling = TypeNameHandling.All,\n            Formatting = Formatting.Indented,\n        };\n\n        public NewtonsoftJsonSuspensionDriver(string file) => _file = file;\n\n        public IObservable<Unit> InvalidateState()\n        {\n            if (File.Exists(_file))\n                File.Delete(_file);\n            return Observable.Return(Unit.Default);\n        }\n\n        public IObservable<object> LoadState()\n        {\n            var lines = File.ReadAllText(_file);\n            var state = JsonConvert.DeserializeObject<object>(lines, Settings)!;\n            return Observable.Return(state);\n        }\n\n        public IObservable<Unit> SaveState(object state)\n        {\n            var lines = JsonConvert.SerializeObject(state, Settings);\n            File.WriteAllText(_file, lines);\n            return Observable.Return(Unit.Default);\n        }\n    }\n}"
  },
  {
    "path": "MangaJaNaiConverterGui/MangaJaNaiConverterGui.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <!--<PublishTrimmed>true</PublishTrimmed>\n    <PublishAot>true</PublishAot>\n    <IsTrimmable>true</IsTrimmable>\n    <EnableTrimAnalyzer>true</EnableTrimAnalyzer>\n    <EnableAotAnalyzer>true</EnableAotAnalyzer>\n    <EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>\n    <TrimmerSingleWarn>false</TrimmerSingleWarn>-->\n    <OutputType>WinExe</OutputType>\n    <TargetFramework>net10.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <BuiltInComInteropSupport>true</BuiltInComInteropSupport>\n    <ApplicationManifest>app.manifest</ApplicationManifest>\n    <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>\n    <Version>1.0.0</Version>\n    <PackageIcon>logo.png</PackageIcon>\n    <ApplicationIcon>Assets\\logo.ico</ApplicationIcon>\n    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Models\\\" />\n    <AvaloniaResource Include=\"Assets\\**\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Remove=\"appstate - Copy.json\" />\n    <None Remove=\"Assets\\logo.ico\" />\n  </ItemGroup>\n\n\n  <ItemGroup>\n    <PackageReference Include=\"Avalonia\" Version=\"11.3.9\" />\n    <PackageReference Include=\"Avalonia.Desktop\" Version=\"11.3.9\" />\n    <PackageReference Include=\"Avalonia.Themes.Fluent\" Version=\"11.3.9\" />\n    <PackageReference Include=\"Avalonia.Fonts.Inter\" Version=\"11.3.9\" />\n    <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->\n    <PackageReference Condition=\"'$(Configuration)' == 'Debug'\" Include=\"Avalonia.Diagnostics\" Version=\"11.3.9\" />\n    <PackageReference Include=\"FluentAvaloniaUI\" Version=\"2.4.1\" />\n    <PackageReference Include=\"HyperText.Avalonia\" Version=\"2.0.0\" />\n    <PackageReference Include=\"Material.Icons.Avalonia\" Version=\"2.4.1\" />\n    <PackageReference Include=\"Newtonsoft.Json\" Version=\"13.0.4\" />\n    <PackageReference Include=\"ReactiveUI.Avalonia\" Version=\"11.3.8\" />\n    <PackageReference Include=\"SevenZipExtractor\" Version=\"1.0.19\" />\n    <PackageReference Include=\"SharpZipLib\" Version=\"1.4.2\" />\n    <PackageReference Include=\"Splat.Autofac\" Version=\"17.1.1\" />\n    <PackageReference Include=\"Velopack\" Version=\"0.0.1298\" />\n  </ItemGroup>\n\n\n  <ItemGroup>\n    <None Update=\"appstate2.json\">\n      <CopyToOutputDirectory>Always</CopyToOutputDirectory>\n    </None>\n    <Content Include=\"backend\\**\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </Content>\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "MangaJaNaiConverterGui/Program.cs",
    "content": "﻿using Avalonia;\nusing ReactiveUI.Avalonia;\nusing System;\nusing System.IO;\nusing Velopack;\n\nnamespace MangaJaNaiConverterGui\n{\n    internal class Program\n    {\n        public static bool WasFirstRun { get; private set; }\n\n        public static readonly string InstalledAppStateFolder = Path.Combine(\n            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),\n            \"MangaJaNaiConverterGui\"\n        );\n\n        public static readonly string InstalledAppStateFilename = \"appstate2.json\";\n        public static readonly string InstalledAppStatePath = Path.Combine(InstalledAppStateFolder, InstalledAppStateFilename);\n\n        // Initialization code. Don't use any Avalonia, third-party APIs or any\n        // SynchronizationContext-reliant code before AppMain is called: things aren't initialized\n        // yet and stuff might break.\n        [STAThread]\n        public static void Main(string[] args)\n        {\n            VelopackApp.Build()\n                .OnBeforeUninstallFastCallback((v) =>\n                {\n                    // On uninstall, remove Python and models from app data\n                    var pythonDir = Path.Combine(InstalledAppStateFolder, \"python\");\n                    var modelsDir = Path.Combine(InstalledAppStateFolder, \"models\");\n                    if (Directory.Exists(pythonDir))\n                    {\n                        Directory.Delete(pythonDir, true);\n                    }\n                    if (Directory.Exists(modelsDir))\n                    {\n                        Directory.Delete(modelsDir, true);\n                    }\n                })\n                .OnFirstRun(_ =>\n                {\n                    WasFirstRun = true;\n                })\n                .Run();\n            BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);\n        }\n\n        // Avalonia configuration, don't remove; also used by visual designer.\n        public static AppBuilder BuildAvaloniaApp()\n            => AppBuilder.Configure<App>()\n                .UsePlatformDetect()\n                .WithInterFont()\n                .LogToTrace()\n                .UseReactiveUI();\n    }\n}"
  },
  {
    "path": "MangaJaNaiConverterGui/Services/Downloader.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\n\nnamespace MangaJaNaiConverterGui.Services\n{\n    public class Downloader\n    {\n        public delegate void ProgressChanged(double percentage);\n\n        public static async Task DownloadFileAsync(string url, string destinationFilePath, ProgressChanged progressChanged)\n        {\n            using HttpClient client = new();\n            using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);\n\n            response.EnsureSuccessStatusCode();\n\n            long totalBytes = response.Content.Headers.ContentLength ?? -1L;\n            using Stream contentStream = await response.Content.ReadAsStreamAsync(), fileStream = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);\n\n            var totalRead = 0L;\n            var buffer = new byte[8192];\n            int read;\n\n            while ((read = await contentStream.ReadAsync(buffer)) > 0)\n            {\n                await fileStream.WriteAsync(buffer.AsMemory(0, read));\n                totalRead += read;\n\n                if (totalBytes != -1)\n                {\n                    double percentage = Math.Round((double)totalRead / totalBytes * 100, 0);\n                    progressChanged?.Invoke(percentage);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MangaJaNaiConverterGui/Services/ETACalculator.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing ProgressItem = System.Collections.Generic.KeyValuePair<long, float>;\n\n\nnamespace MangaJaNaiConverterGui.Services\n{\n    public interface IETACalculator\n    {\n        /// <summary> Clears all collected data.\n        /// </summary>\n        void Reset();\n\n        /// <summary> Updates the current progress.\n        /// </summary>\n        /// <param name=\"progress\">The current level of completion.\n        /// Must be between 0.0 and 1.0 (inclusively).</param>\n        void Update(float progress);\n\n        /// <summary> Returns True when there is enough data to calculate the ETA.\n        /// Returns False if the ETA is still calculating.\n        /// </summary>\n        bool ETAIsAvailable { get; }\n\n        /// <summary> Calculates the Estimated Time of Arrival (Completion)\n        /// </summary>\n        DateTime ETA { get; }\n\n        /// <summary> Calculates the Estimated Time Remaining.\n        /// </summary>\n        TimeSpan ETR { get; }\n    }\n\n    /// <summary> Calculates the \"Estimated Time of Arrival\"\n    /// (or more accurately, \"Estimated Time of Completion\"),\n    /// based on a \"rolling average\" of progress over time.\n    /// </summary>\n    public class ETACalculator : IETACalculator\n    {\n        /// <summary>\n        /// </summary>\n        /// <param name=\"minimumData\">\n        /// The minimum number of data points required before ETA can be calculated.\n        /// </param>\n        /// <param name=\"maximumDuration\">\n        /// Determines how many seconds of data will be used to calculate the ETA.\n        /// </param>\n        public ETACalculator(int minimumData, double maximumDuration)\n        {\n            this.minimumData = minimumData;\n            maximumTicks = (long)(maximumDuration * Stopwatch.Frequency);\n            queue = new Queue<ProgressItem>(minimumData * 2);\n            timer = Stopwatch.StartNew();\n        }\n\n        private int minimumData;\n        private long maximumTicks;\n        private readonly Stopwatch timer;\n        private readonly Queue<ProgressItem> queue;\n\n        private ProgressItem current;\n        private ProgressItem oldest;\n\n        public void Reset()\n        {\n            queue.Clear();\n\n            timer.Reset();\n            timer.Start();\n        }\n\n        private void ClearExpired()\n        {\n            var expired = timer.ElapsedTicks - maximumTicks;\n            while (queue.Count > minimumData && queue.Peek().Key < expired)\n            {\n                oldest = queue.Dequeue();\n            }\n        }\n\n        /// <summary> Adds the current progress to the calculation of ETA.\n        /// </summary>\n        /// <param name=\"progress\">The current level of completion.\n        /// Must be between 0.0 and 1.0 (inclusively).</param>\n        public void Update(float progress)\n        {\n            // If progress hasn't changed, ignore:\n            if (current.Value == progress)\n            {\n                return;\n            }\n\n            // Clear space for this item:\n            ClearExpired();\n\n            // Queue this item:\n            long currentTicks = timer.ElapsedTicks;\n            current = new ProgressItem(currentTicks, progress);\n            queue.Enqueue(current);\n\n            // See if its the first item:\n            if (queue.Count == 1)\n            {\n                oldest = current;\n            }\n        }\n\n        /// <summary> Calculates the Estimated Time Remaining\n        /// </summary>\n        public TimeSpan ETR\n        {\n            get\n            {\n                // Create local copies of the oldest & current,\n                // so that another thread can update them without locking:\n                var oldest = this.oldest;\n                var current = this.current;\n\n                // Make sure we have enough items:\n                if (queue.Count < minimumData || oldest.Value == current.Value)\n                {\n                    return TimeSpan.MaxValue;\n                }\n\n                // Calculate the estimated finished time:\n                double finishedInTicks = (1.0d - current.Value) * (current.Key - oldest.Key) / (current.Value - oldest.Value);\n\n                return TimeSpan.FromSeconds(finishedInTicks / Stopwatch.Frequency);\n            }\n        }\n\n        /// <summary> Calculates the Estimated Time of Arrival (Completion)\n        /// </summary>\n        public DateTime ETA\n        {\n            get\n            {\n                return DateTime.Now.Add(ETR);\n            }\n        }\n\n        /// <summary> Returns True when there is enough data to calculate the ETA.\n        /// Returns False if the ETA is still calculating.\n        /// </summary>\n        public bool ETAIsAvailable\n        {\n            get\n            {\n                // Make sure we have enough items:\n                return queue.Count >= minimumData && oldest.Value != current.Value;\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "MangaJaNaiConverterGui/Services/IPythonService.cs",
    "content": "﻿using Avalonia.Collections;\nusing System;\nusing System.Threading.Tasks;\n\nnamespace MangaJaNaiConverterGui.Services\n{\n    public interface IPythonService\n    {\n        bool IsPythonInstalled();\n\n        Task<bool> IsPythonUpdated();\n        Task<bool> IsBackendUpdated();\n        bool AreModelsInstalled();\n        string BackendUrl { get; }\n        string BackendDirectory { get; }\n        string LogsDirectory { get; }\n        string PythonDirectory { get; }\n        string ModelsDirectory { get; }\n        string PythonPath { get; }\n        string AppStateFolder { get; }\n        string AppStatePath { get; }\n        string AppStateFilename { get; }\n        string InstallUpdatePythonDependenciesCommand { get; }\n        string PythonBackendVersionPath { get; }\n        Version BackendVersion { get; }\n        void ExtractTgz(string gzArchiveName, string destFolder);\n        void ExtractZip(string archivePath, string outFolder, ProgressChanged progressChanged);\n        void Extract7z(string archivePath, string outFolder);\n        void AddPythonPth(string destFolder);\n        AvaloniaList<string> AllModels { get; }\n    }\n}\n"
  },
  {
    "path": "MangaJaNaiConverterGui/Services/ISuspensionDriverService.cs",
    "content": "﻿using ReactiveUI;\n\nnamespace MangaJaNaiConverterGui.Services\n{\n    public interface ISuspensionDriverService\n    {\n        ISuspensionDriver SuspensionDriver { get; }\n    }\n}\n"
  },
  {
    "path": "MangaJaNaiConverterGui/Services/IUpdateManagerService.cs",
    "content": "﻿using System;\nusing System.Threading.Tasks;\nusing Velopack;\n\nnamespace MangaJaNaiConverterGui.Services\n{\n    public interface IUpdateManagerService\n    {\n        bool IsInstalled { get; }\n        bool IsPortable { get; }\n        string AppVersion { get; }\n        bool IsUpdatePendingRestart { get; }\n        void ApplyUpdatesAndRestart(UpdateInfo update);\n        Task<UpdateInfo?> CheckForUpdatesAsync();\n        Task DownloadUpdatesAsync(UpdateInfo update, Action<int>? progress = null);\n    }\n}\n"
  },
  {
    "path": "MangaJaNaiConverterGui/Services/PythonService.cs",
    "content": "﻿using Avalonia.Collections;\nusing ICSharpCode.SharpZipLib.Core;\nusing ICSharpCode.SharpZipLib.GZip;\nusing ICSharpCode.SharpZipLib.Tar;\nusing ICSharpCode.SharpZipLib.Zip;\nusing SevenZipExtractor;\nusing Splat;\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace MangaJaNaiConverterGui.Services\n{\n    public delegate void ProgressChanged(double percentage);\n\n    // https://github.com/chaiNNer-org/chaiNNer/blob/main/src/main/python/integratedPython.ts\n    public class PythonService : IPythonService\n    {\n        private readonly IUpdateManagerService _updateManagerService;\n\n        public static readonly Dictionary<string, PythonDownload> PYTHON_DOWNLOADS = new()\n        {\n            {\n                \"win32\",\n                new PythonDownload\n                {\n                    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\",\n                    Path = \"python/python.exe\",\n                    Version = \"3.13.9\",\n                    Filename = \"Python.tar.gz\"\n                }\n            },\n        };\n\n        public Version BackendVersion => new Version(1, 5, 0);\n\n        public string BackendUrl => $\"https://github.com/the-database/MangaJaNaiConverterGui-backend/releases/download/{BackendVersion}/mangajanaiconvertergui-backend-{BackendVersion}.7z\";\n\n        public PythonService(IUpdateManagerService? updateManagerService = null)\n        {\n            _updateManagerService = updateManagerService ?? Locator.Current.GetService<IUpdateManagerService>()!;\n        }\n\n        public string BackendDirectory => (_updateManagerService?.IsInstalled ?? false) ? Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @\"MangaJaNaiConverterGui\") : Path.GetFullPath(@\".\\backend\");\n\n        public string LogsDirectory => Path.Combine(BackendDirectory, \"logs\");\n        public string ModelsDirectory => Path.Combine(BackendDirectory, \"models\");\n        public string PythonDirectory => Path.Combine(BackendDirectory, \"python\");\n        public string PythonBackendVersionPath => Path.Combine(PythonDirectory, \"Version.txt\");\n        public string PythonPath => Path.GetFullPath(Path.Join(PythonDirectory, PYTHON_DOWNLOADS[\"win32\"].Path));\n\n        public string AppStateFolder => ((_updateManagerService?.IsInstalled ?? false) && !(_updateManagerService?.IsPortable ?? false)) ? Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @\"MangaJaNaiConverterGui\") : Path.GetFullPath(@\".\");\n\n        public string AppStateFilename => \"appstate2.json\";\n        public string AppStatePath => Path.Join(AppStateFolder, AppStateFilename);\n\n        public bool IsPythonInstalled() => File.Exists(PythonPath);\n\n        public async Task<bool> IsPythonUpdated()\n        {\n            var relPythonPath = @\".\\python\\python\\python.exe\";\n\n            var cmd = $@\"{relPythonPath} -V\";\n\n            // Create a new process to run the CMD command\n            using (var process = new Process())\n            {\n                process.StartInfo.FileName = \"cmd.exe\";\n                process.StartInfo.Arguments = @$\"/C {cmd}\";\n                process.StartInfo.RedirectStandardOutput = true;\n                process.StartInfo.RedirectStandardError = true;\n                process.StartInfo.UseShellExecute = false;\n                process.StartInfo.CreateNoWindow = true;\n                process.StartInfo.StandardOutputEncoding = Encoding.UTF8;\n                process.StartInfo.StandardErrorEncoding = Encoding.UTF8;\n                process.StartInfo.WorkingDirectory = BackendDirectory;\n\n                Version? result = null;\n\n                // Create a StreamWriter to write the output to a log file\n                try\n                {\n                    process.ErrorDataReceived += (sender, e) =>\n                    {\n                        if (!string.IsNullOrEmpty(e.Data))\n                        {\n                            // ignore\n                        }\n                    };\n\n                    process.OutputDataReceived += (sender, e) =>\n                    {\n                        if (!string.IsNullOrEmpty(e.Data))\n                        {\n                            result = new Version(e.Data.Replace(\"Python \", \"\"));\n                        }\n                    };\n\n                    process.Start();\n                    process.BeginOutputReadLine();\n                    process.BeginErrorReadLine(); // Start asynchronous reading of the output\n                    await process.WaitForExitAsync();\n                }\n                catch (IOException) { }\n\n                if (result == null || result.CompareTo(new Version(PYTHON_DOWNLOADS[\"win32\"].Version)) < 0)\n                {\n                    return false;\n                }\n            }\n\n            return true;\n        }\n\n        public async Task<bool> IsBackendUpdated()\n        {\n            if (File.Exists(PythonBackendVersionPath))\n            {\n                var currentVersion = new Version(await File.ReadAllTextAsync(PythonBackendVersionPath));\n\n                return currentVersion.CompareTo(BackendVersion) >= 0;\n            }\n\n            return false;\n        }\n\n        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\"));\n\n        public class PythonDownload\n        {\n            public string Url { get; set; }\n            public string Version { get; set; }\n            public string Path { get; set; }\n            public string Filename { get; set; }\n        }\n\n        public void ExtractTgz(string gzArchiveName, string destFolder)\n        {\n            Stream inStream = File.OpenRead(gzArchiveName);\n            Stream gzipStream = new GZipInputStream(inStream);\n\n            TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream, Encoding.UTF8);\n            tarArchive.ExtractContents(destFolder);\n            tarArchive.Close();\n\n            gzipStream.Close();\n            inStream.Close();\n        }\n\n        public void ExtractZip(string archivePath, string outFolder, ProgressChanged progressChanged)\n        {\n\n            using (var fsInput = File.OpenRead(archivePath))\n            using (var zf = new ZipFile(fsInput))\n            {\n\n                for (var i = 0; i < zf.Count; i++)\n                {\n                    ZipEntry zipEntry = zf[i];\n\n                    if (!zipEntry.IsFile)\n                    {\n                        // Ignore directories\n                        continue;\n                    }\n                    String entryFileName = zipEntry.Name;\n                    // to remove the folder from the entry:\n                    //entryFileName = Path.GetFileName(entryFileName);\n                    // Optionally match entrynames against a selection list here\n                    // to skip as desired.\n                    // The unpacked length is available in the zipEntry.Size property.\n\n                    // Manipulate the output filename here as desired.\n                    var fullZipToPath = Path.Combine(outFolder, entryFileName);\n                    var directoryName = Path.GetDirectoryName(fullZipToPath);\n                    if (directoryName.Length > 0)\n                    {\n                        Directory.CreateDirectory(directoryName);\n                    }\n\n                    // 4K is optimum\n                    var buffer = new byte[4096];\n\n                    // Unzip file in buffered chunks. This is just as fast as unpacking\n                    // to a buffer the full size of the file, but does not waste memory.\n                    // The \"using\" will close the stream even if an exception occurs.\n                    using (var zipStream = zf.GetInputStream(zipEntry))\n                    using (Stream fsOutput = File.Create(fullZipToPath))\n                    {\n                        StreamUtils.Copy(zipStream, fsOutput, buffer);\n                    }\n\n                    var percentage = Math.Round((double)i / zf.Count * 100, 0);\n                    progressChanged?.Invoke(percentage);\n                }\n            }\n        }\n\n        public void Extract7z(string archiveName, string outFolder)\n        {\n            using ArchiveFile archiveFile = new(archiveName);\n            archiveFile.Extract(outFolder);\n        }\n\n        public void AddPythonPth(string destFolder)\n        {\n            string[] lines = { \"python313.zip\", \"DLLs\", \"Lib\", \".\", \"Lib/site-packages\" };\n            var filename = \"python313._pth\";\n\n            using var outputFile = new StreamWriter(Path.Combine(destFolder, filename));\n\n            foreach (string line in lines)\n                outputFile.WriteLine(line);\n        }\n\n        public string InstallUpdatePythonDependenciesCommand\n        {\n            get\n            {\n                var relPythonPath = @\".\\python\\python\\python.exe\";\n\n                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\";\n            }\n        }\n\n        private AvaloniaList<string>? _allModels;\n\n        public AvaloniaList<string> AllModels\n        {\n            get\n            {\n                if (_allModels == null)\n                {\n\n                    try\n                    {\n                        var models = new AvaloniaList<string>(Directory.GetFiles(ModelsDirectory).Where(filename =>\n                            Path.GetExtension(filename).Equals(\".pth\", StringComparison.CurrentCultureIgnoreCase) ||\n                            Path.GetExtension(filename).Equals(\".pt\", StringComparison.CurrentCultureIgnoreCase) ||\n                            Path.GetExtension(filename).Equals(\".ckpt\", StringComparison.CurrentCultureIgnoreCase) ||\n                            Path.GetExtension(filename).Equals(\".safetensors\", StringComparison.CurrentCultureIgnoreCase)\n                        )\n                        .Select(filename => Path.GetFileName(filename))\n                        .Order().ToList());\n\n                        models.Add(\"No Model\");\n\n                        Debug.WriteLine($\"GetAllModels: {models.Count}\");\n\n                        _allModels = models;\n                    }\n                    catch (DirectoryNotFoundException)\n                    {\n                        Debug.WriteLine($\"GetAllModels: DirectoryNotFoundException\");\n                        return [];\n                    }\n                }\n\n                return _allModels;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MangaJaNaiConverterGui/Services/SuspensionDriverService.cs",
    "content": "﻿using MangaJaNaiConverterGui.Drivers;\nusing ReactiveUI;\n\nnamespace MangaJaNaiConverterGui.Services\n{\n    public class SuspensionDriverService(IPythonService pythonService) : ISuspensionDriverService\n    {\n        private readonly ISuspensionDriver _driver = new NewtonsoftJsonSuspensionDriver(pythonService.AppStatePath);\n        public ISuspensionDriver SuspensionDriver => _driver;\n    }\n}\n"
  },
  {
    "path": "MangaJaNaiConverterGui/Services/UpdateManagerService.cs",
    "content": "﻿using System;\nusing System.Threading.Tasks;\nusing Velopack;\nusing Velopack.Sources;\n\nnamespace MangaJaNaiConverterGui.Services\n{\n    public class UpdateManagerService : IUpdateManagerService\n    {\n        private readonly UpdateManager _um;\n\n        public UpdateManagerService()\n        {\n            _um = new UpdateManager(new GithubSource(\"https://github.com/the-database/MangaJaNaiConverterGui\", null, false));\n        }\n\n        public string AppVersion { get => _um?.CurrentVersion?.ToString() ?? \"\"; }\n\n        public bool IsInstalled { get => _um.IsInstalled; }\n        public bool IsPortable { get => _um.IsPortable; }\n\n        public bool IsUpdatePendingRestart { get => _um.IsUpdatePendingRestart; }\n\n        public void ApplyUpdatesAndRestart(UpdateInfo update)\n        {\n            _um.ApplyUpdatesAndRestart(update);\n        }\n\n        public async Task<UpdateInfo?> CheckForUpdatesAsync()\n        {\n            return await _um.CheckForUpdatesAsync();\n        }\n\n        public Task DownloadUpdatesAsync(UpdateInfo update, Action<int>? progress = null)\n        {\n            return _um.DownloadUpdatesAsync(update, progress);\n        }\n    }\n}\n"
  },
  {
    "path": "MangaJaNaiConverterGui/ViewLocator.cs",
    "content": "using Avalonia.Controls;\nusing Avalonia.Controls.Templates;\nusing MangaJaNaiConverterGui.ViewModels;\nusing System;\n\nnamespace MangaJaNaiConverterGui\n{\n    public class ViewLocator : IDataTemplate\n    {\n        public Control Build(object data)\n        {\n            var name = data.GetType().FullName!.Replace(\"ViewModel\", \"View\");\n            var type = Type.GetType(name);\n\n            if (type != null)\n            {\n                return (Control)Activator.CreateInstance(type)!;\n            }\n\n            return new TextBlock { Text = \"Not Found: \" + name };\n        }\n\n        public bool Match(object data)\n        {\n            return data is ViewModelBase;\n        }\n    }\n}"
  },
  {
    "path": "MangaJaNaiConverterGui/ViewModels/MainWindowViewModel.cs",
    "content": "﻿using Avalonia.Collections;\nusing Avalonia.Threading;\nusing MangaJaNaiConverterGui.Drivers;\nusing MangaJaNaiConverterGui.Services;\nusing Newtonsoft.Json;\nusing ReactiveUI;\nusing Splat;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Reactive.Linq;\nusing System.Runtime.Serialization;\nusing System.Text;\nusing System.Text.Json.Serialization;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Velopack;\nusing File = System.IO.File;\nusing Path = System.IO.Path;\n\nnamespace MangaJaNaiConverterGui.ViewModels\n{\n    [DataContract]\n    public class MainWindowViewModel : ViewModelBase\n    {\n        public static readonly List<string> IMAGE_EXTENSIONS = [\".png\", \".jpg\", \".jpeg\", \".webp\", \".bmp\", \".avif\"];\n        public static readonly List<string> ARCHIVE_EXTENSIONS = [\".zip\", \".cbz\", \".rar\", \".cbr\"];\n\n        private readonly DispatcherTimer _timer = new();\n        private static readonly HttpClient client = new();\n\n        private UpdateInfo? _update = null;\n\n        private readonly IPythonService _pythonService;\n        private readonly IUpdateManagerService _updateManagerService;\n        private readonly ISuspensionDriverService _suspensionDriverService;\n\n        public MainWindowViewModel(IPythonService? pythonService = null, IUpdateManagerService? updateManagerService = null, ISuspensionDriverService? suspensionDriverService = null)\n        {\n            _pythonService = pythonService ?? Locator.Current.GetService<IPythonService>()!;\n            _updateManagerService = updateManagerService ?? Locator.Current.GetService<IUpdateManagerService>()!;\n            _suspensionDriverService = suspensionDriverService ?? Locator.Current.GetService<ISuspensionDriverService>()!;\n\n            var g1 = this.WhenAnyValue\n            (\n                x => x.SelectedWorkflowIndex\n            ).Subscribe(x =>\n            {\n                CurrentWorkflow?.Validate();\n            });\n\n            _timer.Interval = TimeSpan.FromSeconds(1);\n            _timer.Tick += _timer_Tick;\n\n            ShowDialog = new Interaction<MainWindowViewModel, MainWindowViewModel?>();\n\n            CheckAndDoBackup();\n            CheckForUpdates();\n        }\n\n        private string[] _commonResolutions = [\n\"0x0\",\n\"0x1250\",\n\"0x1251\",\n\"0x1350\",\n\"0x1351\",\n\"0x1450\",\n\"0x1451\",\n\"0x1550\",\n\"0x1551\",\n\"0x1760\",\n\"0x1761\",\n\"0x1984\",\n\"0x1985\",];\n\n        private static readonly string DEFAULT_WORKFLOW = \"\"\"\n{\n  \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n  \"WorkflowName\": \"Upscale Manga (Default)\",\n  \"WorkflowIndex\": 0,\n  \"SelectedTabIndex\": 0,\n  \"InputFilePath\": \"\",\n  \"InputFolderPath\": \"\",\n  \"OutputFilename\": \"%filename%-mangajanai\",\n  \"OutputFolderPath\": \"\",\n  \"OverwriteExistingFiles\": false,\n  \"UpscaleImages\": true,\n  \"UpscaleArchives\": true,\n  \"ResizeHeightAfterUpscale\": 2160,\n  \"ResizeWidthAfterUpscale\": 3840,\n  \"WebpSelected\": true,\n  \"AvifSelected\": false,\n  \"PngSelected\": false,\n  \"JpegSelected\": false,\n  \"UseLosslessCompression\": false,\n  \"LossyCompressionQuality\": 80,\n  \"ShowLossySettings\": true,\n  \"ModeScaleSelected\": true,\n  \"UpscaleScaleFactor\": 4,\n  \"ModeWidthSelected\": false,\n  \"ModeHeightSelected\": false,\n  \"ModeFitToDisplaySelected\": false,\n  \"DisplayDevice\": \"Kobo Elipsa 2E (2023)\",\n  \"DisplayDeviceWidth\": 1404,\n  \"DisplayDeviceHeight\": 1872,\n  \"DisplayPortraitSelected\": true,\n  \"ShowAdvancedSettings\": false,\n  \"GrayscaleDetectionThreshold\": 12,\n  \"Chains\": {\n    \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n    \"$values\": [\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"1\",\n        \"MinResolution\": \"0x0\",\n        \"MaxResolution\": \"0x0\",\n        \"IsGrayscale\": false,\n        \"IsColor\": true,\n        \"MinScaleFactor\": 0,\n        \"MaxScaleFactor\": 2,\n        \"ModelFilePath\": \"2x_IllustrationJaNai_V3denoise_FDAT_M_unshuffle_30k_fp16.safetensors\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": false,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"2\",\n        \"MinResolution\": \"0x0\",\n        \"MaxResolution\": \"0x0\",\n        \"IsGrayscale\": false,\n        \"IsColor\": true,\n        \"MinScaleFactor\": 2,\n        \"MaxScaleFactor\": 0,\n        \"ModelFilePath\": \"4x_IllustrationJaNai_V3denoise_FDAT_M_47k_fp16.safetensors\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": false,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"3\",\n        \"MinResolution\": \"0x0\",\n        \"MaxResolution\": \"0x1250\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 0,\n        \"MaxScaleFactor\": 2,\n        \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"4\",\n        \"MinResolution\": \"0x0\",\n        \"MaxResolution\": \"0x1250\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 2,\n        \"MaxScaleFactor\": 0,\n        \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"5\",\n        \"MinResolution\": \"0x1251\",\n        \"MaxResolution\": \"0x1350\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 0,\n        \"MaxScaleFactor\": 2,\n        \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"6\",\n        \"MinResolution\": \"0x1251\",\n        \"MaxResolution\": \"0x1350\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 2,\n        \"MaxScaleFactor\": 0,\n        \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"7\",\n        \"MinResolution\": \"0x1351\",\n        \"MaxResolution\": \"0x1450\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 0,\n        \"MaxScaleFactor\": 2,\n        \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"8\",\n        \"MinResolution\": \"0x1351\",\n        \"MaxResolution\": \"0x1450\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 2,\n        \"MaxScaleFactor\": 0,\n        \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"9\",\n        \"MinResolution\": \"0x1451\",\n        \"MaxResolution\": \"0x1550\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 0,\n        \"MaxScaleFactor\": 2,\n        \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"10\",\n        \"MinResolution\": \"0x1451\",\n        \"MaxResolution\": \"0x1550\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 2,\n        \"MaxScaleFactor\": 0,\n        \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"11\",\n        \"MinResolution\": \"0x1551\",\n        \"MaxResolution\": \"0x1760\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 0,\n        \"MaxScaleFactor\": 2,\n        \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"12\",\n        \"MinResolution\": \"0x1551\",\n        \"MaxResolution\": \"0x1760\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 2,\n        \"MaxScaleFactor\": 0,\n        \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"13\",\n        \"MinResolution\": \"0x1761\",\n        \"MaxResolution\": \"0x1984\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 0,\n        \"MaxScaleFactor\": 2,\n        \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"14\",\n        \"MinResolution\": \"0x1761\",\n        \"MaxResolution\": \"0x1984\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 2,\n        \"MaxScaleFactor\": 0,\n        \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"15\",\n        \"MinResolution\": \"0x1985\",\n        \"MaxResolution\": \"0x0\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 0,\n        \"MaxScaleFactor\": 2,\n        \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n        \"ChainNumber\": \"16\",\n        \"MinResolution\": \"0x1985\",\n        \"MaxResolution\": \"0x0\",\n        \"IsGrayscale\": true,\n        \"IsColor\": false,\n        \"MinScaleFactor\": 2,\n        \"MaxScaleFactor\": 0,\n        \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n        \"ModelTileSize\": \"Auto (Estimate)\",\n        \"AutoAdjustLevels\": true,\n        \"ResizeHeightBeforeUpscale\": 0,\n        \"ResizeWidthBeforeUpscale\": 0,\n        \"ResizeFactorBeforeUpscale\": 100.0\n      }\n    ]\n  }\n}\n\"\"\";\n\n        public string[] CommonResolutions\n        {\n            get => _commonResolutions;\n            set => this.RaiseAndSetIfChanged(ref _commonResolutions, value);\n        }\n\n        public Interaction<MainWindowViewModel, MainWindowViewModel?> ShowDialog { get; }\n\n        private void _timer_Tick(object? sender, EventArgs e)\n        {\n            ElapsedTime = ElapsedTime.Add(TimeSpan.FromSeconds(1));\n        }\n\n        private CancellationTokenSource? _cancellationTokenSource;\n        private Process? _runningProcess = null;\n        private readonly IETACalculator _archiveEtaCalculator = new ETACalculator(2, 3.0);\n        private readonly IETACalculator _totalEtaCalculator = new ETACalculator(2, 3.0);\n\n        public TimeSpan ArchiveEtr => _archiveEtaCalculator.ETAIsAvailable ? _archiveEtaCalculator.ETR : TimeSpan.FromSeconds(0);\n        public string ArchiveEta => _archiveEtaCalculator.ETAIsAvailable ? _archiveEtaCalculator.ETA.ToString(\"t\") : \"please wait\";\n\n        public TimeSpan TotalEtr => _totalEtaCalculator.ETAIsAvailable ? _totalEtaCalculator.ETR : ArchiveEtr + (ElapsedTime + ArchiveEtr) * (ProgressTotalFiles - (ProgressCurrentFile + 1));\n\n        public string TotalEta => _totalEtaCalculator.ETAIsAvailable ? _totalEtaCalculator.ETA.ToString(\"t\") : _archiveEtaCalculator.ETAIsAvailable ? DateTime.Now.Add(TotalEtr).ToString(\"t\") : \"please wait\";\n\n        public bool IsInstalled => _updateManagerService.IsInstalled;\n        [DataMember]\n        public string ModelsDirectory => _pythonService.ModelsDirectory;\n\n        private bool _showCheckUpdateButton = true;\n        public bool ShowCheckUpdateButton\n        {\n            get => _showCheckUpdateButton;\n            set => this.RaiseAndSetIfChanged(ref _showCheckUpdateButton, value);\n        }\n\n        private bool _showDownloadButton = false;\n        public bool ShowDownloadButton\n        {\n            get => _showDownloadButton;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _showDownloadButton, value);\n                this.RaisePropertyChanged(nameof(ShowCheckUpdateButton));\n            }\n        }\n\n        private bool _showApplyButton = false;\n        public bool ShowApplyButton\n        {\n            get => _showApplyButton;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _showApplyButton, value);\n                this.RaisePropertyChanged(nameof(ShowCheckUpdateButton));\n            }\n        }\n\n        public string AppVersion => _updateManagerService.AppVersion;\n\n        private string _updateStatusText = string.Empty;\n        public string UpdateStatusText\n        {\n            get => _updateStatusText;\n            set => this.RaiseAndSetIfChanged(ref _updateStatusText, value);\n        }\n\n        private string[] _tileSizes = [\n            \"Auto (Estimate)\",\n            \"Maximum\",\n            \"No Tiling\",\n            \"128\",\n            \"192\",\n            \"256\",\n            \"384\",\n            \"512\",\n            \"768\",\n            \"1024\",\n            \"2048\",\n            \"4096\"];\n\n        public string[] TileSizes\n        {\n            get => _tileSizes;\n            set => this.RaiseAndSetIfChanged(ref _tileSizes, value);\n        }\n\n        private string[] _deviceList = [];\n\n        public string[] DeviceList\n        {\n            get => _deviceList;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _deviceList, value);\n                this.RaisePropertyChanged(nameof(SelectedDeviceIndex));\n            }\n        }\n\n        private string _pythonPipList = string.Empty;\n        public string PythonPipList\n        {\n            get => _pythonPipList;\n            set => this.RaiseAndSetIfChanged(ref _pythonPipList, value);\n        }\n\n        private AvaloniaDictionary<string, ReaderDevice> _displayDeviceMap = [];\n        [DataMember]\n        public AvaloniaDictionary<string, ReaderDevice> DisplayDeviceMap\n        {\n            get => _displayDeviceMap;\n            set => this.RaiseAndSetIfChanged(ref _displayDeviceMap, value);\n        }\n\n        private bool _autoUpdate;\n        [DataMember]\n        public bool AutoUpdateEnabled\n        {\n            get => _autoUpdate;\n            set => this.RaiseAndSetIfChanged(ref _autoUpdate, value);\n        }\n\n        private int _selectedDeviceIndex;\n        [DataMember]\n        public int SelectedDeviceIndex\n        {\n            get => _selectedDeviceIndex;\n            set => this.RaiseAndSetIfChanged(ref _selectedDeviceIndex, value);\n        }\n\n        private bool _useCpu;\n        [DataMember]\n        public bool UseCpu\n        {\n            get => _useCpu;\n            set => this.RaiseAndSetIfChanged(ref _useCpu, value);\n        }\n\n        private bool _useFp16;\n        [DataMember]\n        public bool UseFp16\n        {\n            get => _useFp16;\n            set => this.RaiseAndSetIfChanged(ref _useFp16, value);\n        }\n\n\n        private bool _upscaling = false;\n        [IgnoreDataMember]\n        public bool Upscaling\n        {\n            get => _upscaling;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _upscaling, value);\n                this.RaisePropertyChanged(nameof(UpscaleEnabled));\n                this.RaisePropertyChanged(nameof(LeftStatus));\n            }\n        }\n\n        private string _validationText = string.Empty;\n        public string ValidationText\n        {\n            get => _validationText;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _validationText, value);\n                this.RaisePropertyChanged(nameof(LeftStatus));\n            }\n        }\n\n        private string _backendSetupMainStatus = string.Empty;\n        public string BackendSetupMainStatus\n        {\n            get => this._backendSetupMainStatus;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _backendSetupMainStatus, value);\n            }\n        }\n\n        public string BackendSetupSubStatusText => string.Join(\"\\n\", BackendSetupSubStatusQueue);\n\n        private static readonly int BACKEND_SETUP_SUB_STATUS_QUEUE_CAPACITY = 50;\n\n        private ConcurrentQueue<string> _backendSetupSubStatusQueue = new();\n        public ConcurrentQueue<string> BackendSetupSubStatusQueue\n        {\n            get => this._backendSetupSubStatusQueue;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _backendSetupSubStatusQueue, value);\n                this.RaisePropertyChanged(nameof(BackendSetupSubStatusText));\n            }\n        }\n\n        public string ConsoleText => string.Join(\"\\n\", ConsoleQueue);\n\n        private static readonly int CONSOLE_QUEUE_CAPACITY = 1000;\n\n        private ConcurrentQueue<string> _consoleQueue = new();\n        public ConcurrentQueue<string> ConsoleQueue\n        {\n            get => this._consoleQueue;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _consoleQueue, value);\n                this.RaisePropertyChanged(nameof(ConsoleText));\n            }\n        }\n\n        private bool _showConsole = false;\n        public bool ShowConsole\n        {\n            get => _showConsole;\n            set => this.RaiseAndSetIfChanged(ref _showConsole, value);\n        }\n\n        private bool _showAppSettings = false;\n        public bool RequestShowAppSettings\n        {\n            get => _showAppSettings;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _showAppSettings, value);\n                this.RaisePropertyChanged(nameof(ShowAppSettings));\n                this.RaisePropertyChanged(nameof(ShowMainForm));\n            }\n        }\n\n        public string PythonPath => _pythonService.PythonPath;\n\n        private bool _isExtractingBackend = true;\n        public bool IsExtractingBackend\n        {\n            get => _isExtractingBackend;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _isExtractingBackend, value);\n                this.RaisePropertyChanged(nameof(RequestShowAppSettings));\n                this.RaisePropertyChanged(nameof(ShowMainForm));\n            }\n        }\n\n        public bool ShowAppSettings => RequestShowAppSettings && !IsExtractingBackend;\n\n        public bool ShowMainForm => !RequestShowAppSettings && !IsExtractingBackend;\n\n        private bool _showEstimates = false;\n        public bool ShowEstimates\n        {\n            get => _showEstimates;\n            set => this.RaiseAndSetIfChanged(ref _showEstimates, value);\n        }\n\n        private string _inputStatusText = string.Empty;\n        public string InputStatusText\n        {\n            get => _inputStatusText;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _inputStatusText, value);\n                this.RaisePropertyChanged(nameof(LeftStatus));\n            }\n        }\n\n        public string LeftStatus => !CurrentWorkflow.Valid ? ValidationText.Replace(\"\\n\", \" \") : $\"{InputStatusText} selected for upscaling.\";\n\n        private int _progressCurrentFile = 0;\n        public int ProgressCurrentFile\n        {\n            get => _progressCurrentFile;\n            set => this.RaiseAndSetIfChanged(ref _progressCurrentFile, value);\n        }\n\n        private int _progressTotalFiles = 0;\n        public int ProgressTotalFiles\n        {\n            get => _progressTotalFiles;\n            set => this.RaiseAndSetIfChanged(ref _progressTotalFiles, value);\n        }\n\n        private int _progressCurrentFileInCurrentArchive = 0;\n        public int ProgressCurrentFileInArchive\n        {\n            get => _progressCurrentFileInCurrentArchive;\n            set => this.RaiseAndSetIfChanged(ref _progressCurrentFileInCurrentArchive, value);\n        }\n\n        private int _progressTotalFilesInCurrentArchive = 0;\n        public int ProgressTotalFilesInCurrentArchive\n        {\n            get => _progressTotalFilesInCurrentArchive;\n            set => this.RaiseAndSetIfChanged(ref _progressTotalFilesInCurrentArchive, value);\n        }\n\n        private bool _showArchiveProgressBar = false;\n        public bool ShowArchiveProgressBar\n        {\n            get => _showArchiveProgressBar;\n            set => this.RaiseAndSetIfChanged(ref _showArchiveProgressBar, value);\n        }\n\n        public bool UpscaleEnabled => CurrentWorkflow.Valid && !Upscaling;\n\n        private TimeSpan _elapsedTime = TimeSpan.FromSeconds(0);\n        public TimeSpan ElapsedTime\n        {\n            get => _elapsedTime;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _elapsedTime, value);\n            }\n        }\n\n        private AvaloniaList<UpscaleWorkflow>? _workflows;\n        [DataMember]\n        public AvaloniaList<UpscaleWorkflow>? Workflows\n        {\n            get => _workflows;\n            set => this.RaiseAndSetIfChanged(ref _workflows, value);\n        }\n\n        public AvaloniaList<UpscaleWorkflow> CustomWorkflows => new(Workflows.Skip(1).ToList());\n\n        private int _selectedWorkflowIndex = 0;\n        [DataMember]\n        public int SelectedWorkflowIndex\n        {\n            get => _selectedWorkflowIndex;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _selectedWorkflowIndex, value);\n                this.RaisePropertyChanged(nameof(CurrentWorkflow));\n                this.RaisePropertyChanged(nameof(CurrentWorkflow.ActiveWorkflow));\n            }\n        }\n\n        public UpscaleWorkflow? CurrentWorkflow\n        {\n            get => Workflows?[SelectedWorkflowIndex];\n            set\n            {\n                if (Workflows != null)\n                {\n                    Workflows[SelectedWorkflowIndex] = value;\n                    this.RaisePropertyChanged(nameof(CurrentWorkflow));\n                    this.RaisePropertyChanged(nameof(CustomWorkflows));\n                }\n            }\n        }\n\n        public void HandleWorkflowSelected(int workflowIndex)\n        {\n            SelectedWorkflowIndex = workflowIndex;\n            RequestShowAppSettings = false;\n        }\n\n        public void HandleAppSettingsSelected()\n        {\n            RequestShowAppSettings = true;\n        }\n\n\n\n\n        public async Task RunUpscale()\n        {\n            _cancellationTokenSource = new CancellationTokenSource();\n            var ct = _cancellationTokenSource.Token;\n\n            var task = Task.Run(async () =>\n            {\n                await _suspensionDriverService.SuspensionDriver.SaveState(this);\n                ElapsedTime = TimeSpan.FromSeconds(0);\n                ShowEstimates = true;\n                _archiveEtaCalculator.Reset();\n                _totalEtaCalculator.Reset();\n                ct.ThrowIfCancellationRequested();\n                ConsoleQueueClear();\n                Upscaling = true;\n                ProgressCurrentFile = 0;\n                ProgressCurrentFileInArchive = 0;\n                ShowArchiveProgressBar = false;\n\n                var cmd = $@\".\\python\\python\\python.exe \"\"{Path.GetFullPath(@\".\\backend\\src\\run_upscale.py\")}\"\" --settings \"\"{_pythonService.AppStatePath}\"\"\";\n                ConsoleQueueEnqueue($\"Upscaling with command: {cmd}\");\n                await RunCommand($@\" /C {cmd}\");\n\n                CurrentWorkflow.Valid = true;\n            }, ct);\n\n            try\n            {\n                _timer.Start();\n                await task;\n                _timer.Stop();\n                CurrentWorkflow.Validate();\n            }\n            catch (OperationCanceledException e)\n            {\n                _timer.Stop();\n                Console.WriteLine($\"{nameof(OperationCanceledException)} thrown with message: {e.Message}\");\n                Upscaling = false;\n            }\n            finally\n            {\n                _timer.Stop();\n                _cancellationTokenSource.Dispose();\n                Upscaling = false;\n            }\n        }\n\n        public void CancelUpscale()\n        {\n            try\n            {\n                _cancellationTokenSource?.Cancel();\n                if (_runningProcess != null && !_runningProcess.HasExited)\n                {\n                    // Kill the process\n                    _runningProcess.Kill(true);\n                    _runningProcess = null; // Clear the reference to the terminated process\n                }\n                CurrentWorkflow.Validate();\n            }\n            catch { }\n        }\n\n\n        public void CheckInputs()\n        {\n            if (CurrentWorkflow.Valid && !Upscaling)\n            {\n                var overwriteText = CurrentWorkflow.OverwriteExistingFiles ? \"overwritten\" : \"skipped\";\n\n                // input file\n                if (CurrentWorkflow.SelectedTabIndex == 0)\n                {\n                    StringBuilder status = new();\n                    var skipFiles = 0;\n\n\n\n                    if (IMAGE_EXTENSIONS.Any(x => CurrentWorkflow.InputFilePath.ToLower().EndsWith(x)))\n                    {\n                        var outputFilePath = Path.Join(\n                                                    Path.GetFullPath(CurrentWorkflow.OutputFolderPath),\n                                                    CurrentWorkflow.OutputFilename.Replace(\"%filename%\", Path.GetFileNameWithoutExtension(CurrentWorkflow.InputFilePath))) + $\".{CurrentWorkflow.ImageFormat}\";\n                        if (File.Exists(outputFilePath))\n                        {\n                            status.Append($\" (1 image already exists and will be {overwriteText})\");\n                            if (!CurrentWorkflow.OverwriteExistingFiles)\n                            {\n                                skipFiles++;\n                            }\n                        }\n                    }\n                    else if (ARCHIVE_EXTENSIONS.Any(x => CurrentWorkflow.InputFilePath.ToLower().EndsWith(x)))\n                    {\n                        var outputFilePath = Path.Join(Path.GetFullPath(CurrentWorkflow.OutputFolderPath),\n                            CurrentWorkflow.OutputFilename.Replace(\"%filename%\", Path.GetFileNameWithoutExtension(CurrentWorkflow.InputFilePath))) + \".cbz\";\n\n                        if (File.Exists(outputFilePath))\n                        {\n                            status.Append($\" (1 archive already exists and will be {overwriteText})\");\n                            if (!CurrentWorkflow.OverwriteExistingFiles)\n                            {\n                                skipFiles++;\n                            }\n                        }\n                    }\n                    else\n                    {\n                        // TODO ???\n                    }\n\n                    var s = skipFiles > 0 ? \"s\" : \"\";\n                    if (IMAGE_EXTENSIONS.Any(x => CurrentWorkflow.InputFilePath.ToLower().EndsWith(x)))\n                    {\n                        status.Insert(0, $\"{1 - skipFiles} image{s}\");\n                    }\n                    else if (ARCHIVE_EXTENSIONS.Any(x => CurrentWorkflow.InputFilePath.ToLower().EndsWith(x)))\n                    {\n                        status.Insert(0, $\"{1 - skipFiles} archive{s}\");\n                    }\n                    else\n                    {\n                        status.Insert(0, \"0 files\");\n                    }\n\n                    InputStatusText = status.ToString();\n                    ProgressCurrentFile = 0;\n                    ProgressTotalFiles = 1 - skipFiles;\n                    ProgressCurrentFileInArchive = 0;\n                    ProgressTotalFilesInCurrentArchive = 0;\n                    ShowArchiveProgressBar = false;\n                }\n                else  // input folder\n                {\n                    List<string> statuses = new();\n                    var existImageCount = 0;\n                    var existArchiveCount = 0;\n                    var totalFileCount = 0;\n\n                    if (CurrentWorkflow.UpscaleImages)\n                    {\n                        var images = Directory.EnumerateFiles(CurrentWorkflow.InputFolderPath, \"*.*\", SearchOption.AllDirectories)\n                            .Where(file => IMAGE_EXTENSIONS.Any(ext => file.ToLower().EndsWith(ext)));\n                        var imagesCount = 0;\n\n                        foreach (var inputImagePath in images)\n                        {\n                            var outputImagePath = Path.Join(\n                                                        Path.GetFullPath(CurrentWorkflow.OutputFolderPath),\n                                                        CurrentWorkflow.OutputFilename.Replace(\"%filename%\", Path.GetFileNameWithoutExtension(inputImagePath))) + $\"{CurrentWorkflow.ImageFormat}\";\n                            // if out file exists, exist count ++\n                            // if overwrite image OR out file doesn't exist, count image++\n                            var fileExists = File.Exists(outputImagePath);\n\n                            if (fileExists)\n                            {\n                                existImageCount++;\n                            }\n\n                            if (!fileExists || CurrentWorkflow.OverwriteExistingFiles)\n                            {\n                                imagesCount++;\n                            }\n                        }\n\n                        var imageS = imagesCount == 1 ? \"\" : \"s\";\n                        var existImageS = existImageCount == 1 ? \"\" : \"s\";\n\n                        statuses.Add($\"{imagesCount} image{imageS} ({existImageCount} image{existImageS} already exist and will be {overwriteText})\");\n                        totalFileCount += imagesCount;\n                    }\n                    if (CurrentWorkflow.UpscaleArchives)\n                    {\n                        var archives = Directory.EnumerateFiles(CurrentWorkflow.InputFolderPath, \"*.*\", SearchOption.AllDirectories)\n                            .Where(file => ARCHIVE_EXTENSIONS.Any(ext => file.ToLower().EndsWith(ext)));\n                        var archivesCount = 0;\n\n                        foreach (var inputArchivePath in archives)\n                        {\n                            var outputArchivePath = Path.Join(\n                                                            Path.GetFullPath(CurrentWorkflow.OutputFolderPath),\n                                                            CurrentWorkflow.OutputFilename.Replace(\"%filename%\", Path.GetFileNameWithoutExtension(inputArchivePath))) + \".cbz\";\n                            var fileExists = File.Exists(outputArchivePath);\n\n                            if (fileExists)\n                            {\n                                existArchiveCount++;\n                            }\n\n                            if (!fileExists || CurrentWorkflow.OverwriteExistingFiles)\n                            {\n                                archivesCount++;\n                            }\n                        }\n\n                        var archiveS = archivesCount == 1 ? \"\" : \"s\";\n                        var existArchiveS = existArchiveCount == 1 ? \"\" : \"s\";\n                        statuses.Add($\"{archivesCount} archive{archiveS} ({existArchiveCount} archive{existArchiveS} already exist and will be {overwriteText})\");\n                        totalFileCount += archivesCount;\n                    }\n\n                    if (!CurrentWorkflow.UpscaleArchives && !CurrentWorkflow.UpscaleImages)\n                    {\n                        InputStatusText = \"0 files\";\n                    }\n                    else\n                    {\n                        InputStatusText = $\"{string.Join(\" and \", statuses)}\";\n                    }\n\n                    ProgressCurrentFile = 0;\n                    ProgressTotalFiles = totalFileCount;\n                    ProgressCurrentFileInArchive = 0;\n                    ProgressTotalFilesInCurrentArchive = 0;\n                    ShowArchiveProgressBar = false;\n\n                }\n            }\n        }\n\n        public void AddChain()\n        {\n            CurrentWorkflow?.Chains.Add(new UpscaleChain\n            {\n                Vm = this,\n            });\n            UpdateChainHeaders();\n        }\n\n        public void DeleteChain(UpscaleChain chain)\n        {\n            try\n            {\n                CurrentWorkflow.Chains.Remove(chain);\n            }\n            catch (ArgumentOutOfRangeException)\n            {\n\n            }\n\n            UpdateChainHeaders();\n        }\n\n        public void UpdateChainHeaders()\n        {\n            for (var i = 0; i < CurrentWorkflow.Chains.Count; i++)\n            {\n                CurrentWorkflow.Chains[i].ChainNumber = (i + 1).ToString();\n            }\n        }\n\n\n\n        public async Task RunCommand(string command)\n        {\n            // Create a new process to run the CMD command\n            using (var process = new Process())\n            {\n                _runningProcess = process;\n                process.StartInfo.FileName = \"cmd.exe\";\n                process.StartInfo.Arguments = command;\n                process.StartInfo.RedirectStandardOutput = true;\n                process.StartInfo.RedirectStandardError = true;\n                process.StartInfo.UseShellExecute = false;\n                process.StartInfo.CreateNoWindow = true;\n                process.StartInfo.WorkingDirectory = _pythonService.BackendDirectory;\n                process.StartInfo.StandardOutputEncoding = Encoding.UTF8;\n                process.StartInfo.StandardErrorEncoding = Encoding.UTF8;\n\n                // Create a StreamWriter to write the output to a log file\n                using (var outputFile = new StreamWriter(Path.Combine(_pythonService.LogsDirectory, \"upscale.log\"), append: false))\n                {\n                    process.ErrorDataReceived += (sender, e) =>\n                    {\n                        if (!string.IsNullOrEmpty(e.Data))\n                        {\n                            outputFile.WriteLine(e.Data); // Write the output to the log file\n                            ConsoleQueueEnqueue(e.Data);\n                        }\n                    };\n\n                    process.OutputDataReceived += (sender, e) =>\n                    {\n                        if (!string.IsNullOrEmpty(e.Data))\n                        {\n                            if (e.Data.StartsWith(\"PROGRESS=\"))\n                            {\n                                if (e.Data.Contains(\"_zip_image\"))\n                                {\n                                    ShowArchiveProgressBar = true;\n                                    ProgressCurrentFileInArchive++;\n                                    UpdateEtas();\n                                }\n                                else\n                                {\n                                    ProgressCurrentFile++;\n                                    UpdateEtas();\n\n                                }\n                            }\n                            else if (e.Data.StartsWith(\"TOTALZIP=\"))\n                            {\n                                if (int.TryParse(e.Data.Replace(\"TOTALZIP=\", \"\"), out var total))\n                                {\n                                    ShowArchiveProgressBar = true;\n                                    ProgressCurrentFileInArchive = 0;\n                                    ProgressTotalFilesInCurrentArchive = total;\n                                    UpdateEtas();\n                                }\n                            }\n                            else\n                            {\n                                outputFile.WriteLine(e.Data); // Write the output to the log file\n                                ConsoleQueueEnqueue(e.Data);\n                                Debug.WriteLine(e.Data);\n                            }\n                        }\n                    };\n\n                    process.Start();\n                    process.BeginOutputReadLine();\n                    process.BeginErrorReadLine(); // Start asynchronous reading of the output\n                    await process.WaitForExitAsync();\n                }\n\n            }\n        }\n\n        public async Task<DeviceResponse?> InitializeDeviceList()\n        {\n            if (!File.Exists(@\".\\backend\\src\\device_list.py\"))\n            {\n                return null;\n            }\n\n            // Create a new process to run the CMD command\n            using (var process = new Process())\n            {\n                _runningProcess = process;\n                process.StartInfo.FileName = \"cmd.exe\";\n                process.StartInfo.Arguments = @$\"/C .\\python\\python\\python.exe {Path.GetFullPath(@\".\\backend\\src\\device_list.py\")}\";\n                process.StartInfo.RedirectStandardOutput = true;\n                process.StartInfo.RedirectStandardError = true;\n                process.StartInfo.UseShellExecute = false;\n                process.StartInfo.CreateNoWindow = true;\n                process.StartInfo.WorkingDirectory = _pythonService.BackendDirectory;\n                process.StartInfo.StandardOutputEncoding = Encoding.UTF8;\n                process.StartInfo.StandardErrorEncoding = Encoding.UTF8;\n\n                var result = string.Empty;\n\n                // Create a StreamWriter to write the output to a log file\n                try\n                {\n                    using var outputFile = new StreamWriter(Path.Combine(_pythonService.LogsDirectory, \"upscale.log\"), append: false);\n                    process.ErrorDataReceived += (sender, e) =>\n                    {\n                        if (!string.IsNullOrEmpty(e.Data))\n                        {\n                            //outputFile.WriteLine(e.Data); // Write the output to the log file\n                            //ConsoleQueueEnqueue(e.Data);\n                            Debug.WriteLine(e.Data);\n                        }\n                    };\n\n                    process.OutputDataReceived += (sender, e) =>\n                    {\n                        if (!string.IsNullOrEmpty(e.Data))\n                        {\n                            result = e.Data;\n                            Debug.WriteLine(e.Data);\n                        }\n                    };\n\n                    process.Start();\n                    process.BeginOutputReadLine();\n                    process.BeginErrorReadLine(); // Start asynchronous reading of the output\n                    await process.WaitForExitAsync();\n\n                    if (!string.IsNullOrEmpty(result))\n                    {\n                        return JsonConvert.DeserializeObject<DeviceResponse>(result);\n                    }\n                }\n                catch (IOException) { }\n            }\n\n            return null;\n        }\n\n        public async Task<string> RunPythonPipList()\n        {\n            List<string> result = [];\n\n            // Create a new process to run the CMD command\n            using (var process = new Process())\n            {\n                _runningProcess = process;\n                process.StartInfo.FileName = \"cmd.exe\";\n                process.StartInfo.Arguments = @$\"/C .\\python\\python\\python.exe -m pip list\";\n                process.StartInfo.RedirectStandardOutput = true;\n                process.StartInfo.RedirectStandardError = true;\n                process.StartInfo.UseShellExecute = false;\n                process.StartInfo.CreateNoWindow = true;\n                process.StartInfo.WorkingDirectory = _pythonService.BackendDirectory;\n                process.StartInfo.StandardOutputEncoding = Encoding.UTF8;\n                process.StartInfo.StandardErrorEncoding = Encoding.UTF8;\n\n                // Create a StreamWriter to write the output to a log file\n                try\n                {\n                    process.ErrorDataReceived += (sender, e) =>\n                    {\n                        if (!string.IsNullOrEmpty(e.Data))\n                        {\n                            //outputFile.WriteLine(e.Data); // Write the output to the log file\n                            //ConsoleQueueEnqueue(e.Data);\n                            Debug.WriteLine(e.Data);\n                        }\n                    };\n\n                    process.OutputDataReceived += (sender, e) =>\n                    {\n                        if (!string.IsNullOrEmpty(e.Data))\n                        {\n                            result.Add(e.Data);\n                        }\n                    };\n\n                    process.Start();\n                    process.BeginOutputReadLine();\n                    process.BeginErrorReadLine(); // Start asynchronous reading of the output\n                    await process.WaitForExitAsync();\n                }\n                catch (IOException) { }\n            }\n\n            return string.Join(\"\\n\", result);\n        }\n\n        public async void ShowSettingsDialog()\n        {\n            var result = await ShowDialog.Handle(this);\n        }\n\n        private void UpdateEtas()\n        {\n            if (ProgressTotalFilesInCurrentArchive > 0)\n            {\n                _archiveEtaCalculator.Update(ProgressCurrentFileInArchive / (float)ProgressTotalFilesInCurrentArchive);\n            }\n\n            if (ProgressTotalFiles > 0)\n            {\n                _totalEtaCalculator.Update(ProgressCurrentFile / (float)ProgressTotalFiles);\n            }\n\n            this.RaisePropertyChanged(nameof(ArchiveEtr));\n            this.RaisePropertyChanged(nameof(ArchiveEta));\n            this.RaisePropertyChanged(nameof(TotalEtr));\n            this.RaisePropertyChanged(nameof(TotalEta));\n        }\n\n        private void ConsoleQueueClear()\n        {\n            ConsoleQueue.Clear();\n            this.RaisePropertyChanged(nameof(ConsoleText));\n        }\n\n        private void ConsoleQueueEnqueue(string value)\n        {\n            while (ConsoleQueue.Count > CONSOLE_QUEUE_CAPACITY)\n            {\n                ConsoleQueue.TryDequeue(out var _);\n            }\n            ConsoleQueue.Enqueue(value);\n            this.RaisePropertyChanged(nameof(ConsoleText));\n        }\n\n        private void BackendSetupSubStatusQueueEnqueue(string value)\n        {\n            while (BackendSetupSubStatusQueue.Count > BACKEND_SETUP_SUB_STATUS_QUEUE_CAPACITY)\n            {\n                BackendSetupSubStatusQueue.TryDequeue(out var _);\n            }\n            BackendSetupSubStatusQueue.Enqueue(value);\n            this.RaisePropertyChanged(nameof(BackendSetupSubStatusText));\n        }\n\n        public void ReadWorkflowFileToCurrentWorkflow(string fullPath)\n        {\n            if (!File.Exists(fullPath))\n            {\n                return;\n            }\n\n            var lines = File.ReadAllText(fullPath);\n            var workflow = JsonConvert.DeserializeObject<UpscaleWorkflow>(lines, NewtonsoftJsonSuspensionDriver.Settings);\n            if (workflow != null && CurrentWorkflow != null)\n            {\n                workflow.WorkflowIndex = CurrentWorkflow.WorkflowIndex;\n                workflow.Vm = CurrentWorkflow.Vm;\n                CurrentWorkflow = workflow;\n            }\n        }\n\n        public void WriteCurrentWorkflowToFile(string fullPath)\n        {\n            var lines = JsonConvert.SerializeObject(CurrentWorkflow, NewtonsoftJsonSuspensionDriver.Settings);\n            File.WriteAllText(fullPath, lines);\n        }\n\n        public async Task CheckAndExtractBackend()\n        {\n            await Task.Run(async () =>\n            {\n                IsExtractingBackend = true;\n\n                if (!Directory.Exists(_pythonService.LogsDirectory))\n                {\n                    Directory.CreateDirectory(_pythonService.LogsDirectory);\n                }\n\n                if (!_pythonService.AreModelsInstalled())\n                {\n                    await DownloadModels();\n                }\n\n                if (!_pythonService.IsPythonInstalled() || !(await _pythonService.IsBackendUpdated()))\n                {\n                    // Download Python tgz\n                    BackendSetupMainStatus = \"Downloading Python Backend...\";\n                    var downloadUrl = _pythonService.BackendUrl;\n                    var targetPath = Path.Join(_pythonService.PythonDirectory, \"backend.7z\");\n                    if (Directory.Exists(_pythonService.PythonDirectory))\n                    {\n                        Directory.Delete(_pythonService.PythonDirectory, true);\n                    }\n                    Directory.CreateDirectory(_pythonService.PythonDirectory);\n                    await Downloader.DownloadFileAsync(downloadUrl, targetPath, (progress) =>\n                    {\n                        BackendSetupMainStatus = $\"Downloading Python Backend ({progress}%)...\";\n                    });\n\n                    // Extract Python 7z\n                    BackendSetupMainStatus = \"Extracting Python Backend...\";\n                    _pythonService.Extract7z(targetPath, _pythonService.PythonDirectory);\n\n                    Directory.Move(Path.Combine(_pythonService.PythonDirectory, \"backend\", \"python\"), Path.Combine(_pythonService.PythonDirectory, \"python\"));\n\n                    using (StreamWriter sw = File.CreateText(_pythonService.PythonBackendVersionPath))\n                    {\n                        sw.WriteLine(_pythonService.BackendVersion);\n                    }\n\n                    Directory.Delete(Path.Combine(_pythonService.PythonDirectory, \"backend\"));\n                    File.Delete(targetPath);\n                }\n\n                IsExtractingBackend = false;\n            });\n\n            var deviceResponse = await InitializeDeviceList();\n            if (deviceResponse != null)\n            {\n                DeviceList = [.. deviceResponse.AllDevices.Select(d => d.Name)];\n                SelectedDeviceIndex = deviceResponse.BestDevice;\n            }\n            else\n            {\n                SelectedDeviceIndex = 1; // default to first non cpu device\n            }\n\n            PythonPipList = await RunPythonPipList();\n        }\n\n        public async Task ReinstallBackend()\n        {\n            if (Directory.Exists(_pythonService.ModelsDirectory))\n            {\n                Directory.Delete(_pythonService.ModelsDirectory, true);\n            }\n\n            if (Directory.Exists(_pythonService.PythonDirectory))\n            {\n                Directory.Delete(_pythonService.PythonDirectory, true);\n            }\n\n            await CheckAndExtractBackend();\n        }\n\n        public async Task DownloadModels()\n        {\n            BackendSetupMainStatus = \"Downloading MangaJaNai Models...\";\n            var download = \"https://github.com/the-database/mangajanai/releases/download/1.0.0/MangaJaNai_V1_ModelsOnly.zip\";\n            var targetPath = Path.Join(_pythonService.ModelsDirectory, \"mangajanai.zip\");\n            Directory.CreateDirectory(_pythonService.ModelsDirectory);\n            await Downloader.DownloadFileAsync(download, targetPath, (progress) =>\n            {\n                BackendSetupMainStatus = $\"Downloading MangaJaNai Models ({progress}%)...\";\n            });\n\n            BackendSetupMainStatus = \"Extracting MangaJaNai Models...\";\n            _pythonService.ExtractZip(targetPath, _pythonService.ModelsDirectory, (double progress) =>\n            {\n                BackendSetupMainStatus = $\"Extracting MangaJaNai Models ({progress}%)...\";\n            });\n            File.Delete(targetPath);\n\n            BackendSetupMainStatus = \"Downloading IllustrationJaNai V3denoise Models...\";\n            download = \"https://github.com/the-database/MangaJaNai/releases/download/3.0.0/IllustrationJaNai_V3denoise.zip\";\n            targetPath = Path.Join(_pythonService.ModelsDirectory, \"illustrationjanai.zip\");\n            await Downloader.DownloadFileAsync(download, targetPath, (progress) =>\n            {\n                BackendSetupMainStatus = $\"Downloading IllustrationJaNai V3denoise Models ({progress}%)...\";\n            });\n\n            BackendSetupMainStatus = \"Extracting IllustrationJaNai V3denoise Models...\";\n            _pythonService.ExtractZip(targetPath, _pythonService.ModelsDirectory, (double progress) =>\n            {\n                BackendSetupMainStatus = $\"Extracting IllustrationJaNai V3denoise Models ({progress}%)...\";\n            });\n            File.Delete(targetPath);\n\n            BackendSetupMainStatus = \"Downloading IllustrationJaNai V3detail Models...\";\n            download = \"https://github.com/the-database/MangaJaNai/releases/download/3.0.0/IllustrationJaNai_V3detail.zip\";\n            targetPath = Path.Join(_pythonService.ModelsDirectory, \"illustrationjanai.zip\");\n            await Downloader.DownloadFileAsync(download, targetPath, (progress) =>\n            {\n                BackendSetupMainStatus = $\"Downloading IllustrationJaNai V3detail Models ({progress}%)...\";\n            });\n\n            BackendSetupMainStatus = \"Extracting IllustrationJaNai V3detail Models...\";\n            _pythonService.ExtractZip(targetPath, _pythonService.ModelsDirectory, (double progress) =>\n            {\n                BackendSetupMainStatus = $\"Extracting IllustrationJaNai V3detail Models ({progress}%)...\";\n            });\n            File.Delete(targetPath);\n        }\n\n\n\n        public async Task<string[]> InstallUpdatePythonDependencies()\n        {\n            var cmd = _pythonService.InstallUpdatePythonDependenciesCommand;\n            Debug.WriteLine(cmd);\n\n            // Create a new process to run the CMD command\n            using (var process = new Process())\n            {\n                process.StartInfo.FileName = \"cmd.exe\";\n                process.StartInfo.Arguments = @$\"/C {cmd}\";\n                process.StartInfo.RedirectStandardOutput = true;\n                process.StartInfo.RedirectStandardError = true;\n                process.StartInfo.UseShellExecute = false;\n                process.StartInfo.CreateNoWindow = true;\n                process.StartInfo.StandardOutputEncoding = Encoding.UTF8;\n                process.StartInfo.StandardErrorEncoding = Encoding.UTF8;\n                process.StartInfo.WorkingDirectory = _pythonService.BackendDirectory;\n\n                var result = string.Empty;\n                using var outputFile = new StreamWriter(Path.Combine(_pythonService.LogsDirectory, \"install.log\"));\n                outputFile.WriteLine($\"Working Directory: {process.StartInfo.WorkingDirectory}\");\n                outputFile.WriteLine($\"Run Command: {cmd}\");\n                // Create a StreamWriter to write the output to a log file\n                try\n                {\n                    //using var outputFile = new StreamWriter(\"error.log\", append: true);\n                    process.ErrorDataReceived += (sender, e) =>\n                    {\n                        if (!string.IsNullOrEmpty(e.Data))\n                        {\n                            //Debug.WriteLine($\"STDERR = {e.Data}\");\n                            outputFile.WriteLine(e.Data);\n                            BackendSetupSubStatusQueueEnqueue(e.Data);\n                        }\n                    };\n\n                    process.OutputDataReceived += (sender, e) =>\n                    {\n                        if (!string.IsNullOrEmpty(e.Data))\n                        {\n                            result = e.Data;\n                            outputFile.WriteLine(e.Data);\n                            //Debug.WriteLine($\"STDOUT = {e.Data}\");\n                            BackendSetupSubStatusQueueEnqueue(e.Data);\n                        }\n                    };\n\n                    process.Start();\n                    process.BeginOutputReadLine();\n                    process.BeginErrorReadLine(); // Start asynchronous reading of the output\n                    await process.WaitForExitAsync();\n                }\n                catch (IOException) { }\n            }\n\n            return [];\n        }\n\n        public void CheckAndDoBackup()\n        {\n            Task.Run(() =>\n            {\n                try\n                {\n                    if (!File.Exists(_pythonService.AppStatePath))\n                        return;\n\n                    var files = Directory.EnumerateFiles(_pythonService.AppStateFolder)\n                        .Where(f =>\n                        {\n                            var name = Path.GetFileName(f);\n                            return name.StartsWith(\"autobackup_\") &&\n                                   name.EndsWith(_pythonService.AppStateFilename);\n                        })\n                        .OrderByDescending(f => f)\n                        .ToList();\n\n                    var latestBackup = files.FirstOrDefault();\n                    if (latestBackup is not null &&\n                        FilesAreEqual(_pythonService.AppStatePath, latestBackup))\n                    {\n                        return;\n                    }\n\n                    var backupName =\n                        $\"autobackup_{DateTime.Now:yyyyMMdd-HHmmss}_{_pythonService.AppStateFilename}\";\n                    var backupPath = Path.Combine(_pythonService.AppStateFolder, backupName);\n\n                    File.Copy(_pythonService.AppStatePath, backupPath);\n\n                    files.Insert(0, backupPath);\n\n                    const int maxBackups = 10;\n                    if (files.Count > maxBackups)\n                    {\n                        foreach (var old in files.Skip(maxBackups))\n                        {\n                            try { File.Delete(old); }\n                            catch { }\n                        }\n                    }\n                }\n                catch\n                {\n                }\n            });\n        }\n\n        private static bool FilesAreEqual(string path1, string path2)\n        {\n            var info1 = new FileInfo(path1);\n            var info2 = new FileInfo(path2);\n            if (info1.Length != info2.Length)\n                return false;\n\n            var bytes1 = File.ReadAllBytes(path1);\n            var bytes2 = File.ReadAllBytes(path2);\n\n            return bytes1.AsSpan().SequenceEqual(bytes2);\n        }\n\n        public void ResetCurrentWorkflow()\n        {\n            if (CurrentWorkflow != null)\n            {\n                var workflow = JsonConvert.DeserializeObject<UpscaleWorkflow>(DEFAULT_WORKFLOW, NewtonsoftJsonSuspensionDriver.Settings);\n                var workflowIndex = CurrentWorkflow.WorkflowIndex;\n                var workflowName = $\"Custom Workflow {workflowIndex}\";\n\n                if (workflow != null)\n                {\n                    var defaultWorkflow = new UpscaleWorkflow\n                    {\n                        Vm = this,\n                        WorkflowIndex = workflowIndex,\n                        WorkflowName = workflowName,\n                        Chains = workflow.Chains\n                    };\n\n                    foreach (var chain in defaultWorkflow.Chains)\n                    {\n                        chain.Vm = this;\n                    }\n\n                    CurrentWorkflow = defaultWorkflow;\n                }\n            }\n        }\n\n        public async Task<IEnumerable<object>> PopulateDevicesAsync(string? searchText, CancellationToken cancellationToken)\n        {\n            try\n            {\n                var requestUrl = $\"https://animejan.ai/mangajanai/api/search?q={Uri.EscapeDataString(searchText?.Trim() ?? \"\")}&p=0&s=4\";\n                if (string.IsNullOrWhiteSpace(searchText))\n                {\n                    requestUrl = $\"https://animejan.ai/mangajanai/api/top\";\n                }\n                var response = await client.GetStringAsync(requestUrl, cancellationToken);\n                var devices = JsonConvert.DeserializeObject<List<ReaderDevice>>(response, NewtonsoftJsonSuspensionDriver.Settings);\n                if (devices != null)\n                {\n                    foreach (var device in devices)\n                    {\n                        DisplayDeviceMap[device.ToString()] = device;\n\n                    }\n                    return devices.ToList();\n                }\n            }\n            catch (Exception ex)\n            {\n                Debug.WriteLine(ex);\n            }\n\n            return [];\n        }\n\n        public async Task CheckForUpdates()\n        {\n            try\n            {\n                if (_updateManagerService.IsInstalled)\n                {\n                    await Task.Run(async () =>\n                    {\n                        _update = await _updateManagerService.CheckForUpdatesAsync().ConfigureAwait(true);\n                    });\n\n                    UpdateStatus();\n\n                    if (AutoUpdateEnabled)\n                    {\n                        await DownloadUpdate();\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                UpdateStatusText = $\"Check for update failed: {ex.Message}\";\n            }\n        }\n\n        public async Task DownloadUpdate()\n        {\n            try\n            {\n                if (_update != null)\n                {\n                    ShowDownloadButton = false;\n                    await _updateManagerService.DownloadUpdatesAsync(_update, Progress).ConfigureAwait(true);\n                    UpdateStatus();\n                }\n            }\n            catch\n            {\n\n            }\n        }\n\n        public void ApplyUpdate()\n        {\n            if (_update != null)\n            {\n                ShowApplyButton = false;\n                _updateManagerService.ApplyUpdatesAndRestart(_update);\n            }\n        }\n\n        private void UpdateStatus()\n        {\n            ShowDownloadButton = false;\n            ShowApplyButton = false;\n            ShowCheckUpdateButton = true;\n\n            if (_update != null)\n            {\n                UpdateStatusText = $\"Update is available: {_update.TargetFullRelease.Version}\";\n                ShowDownloadButton = true;\n                ShowCheckUpdateButton = false;\n\n                if (_updateManagerService.IsUpdatePendingRestart)\n                {\n                    UpdateStatusText = $\"Update ready, pending restart to install version: {_update.TargetFullRelease.Version}\";\n                    ShowDownloadButton = false;\n                    ShowApplyButton = true;\n                    ShowCheckUpdateButton = false;\n                }\n                else\n                {\n                }\n            }\n            else\n            {\n                UpdateStatusText = \"No updates found\";\n            }\n        }\n\n        private void Progress(int percent)\n        {\n            UpdateStatusText = $\"Downloading update {_update?.TargetFullRelease.Version} ({percent}%)...\";\n        }\n\n\n        public async void OpenModelsDirectory()\n        {\n            await Task.Run(() =>\n            {\n                Process.Start(\"explorer.exe\", _pythonService.ModelsDirectory);\n            });\n        }\n    }\n\n    [DataContract]\n    public class UpscaleWorkflow : ReactiveObject\n    {\n        public UpscaleWorkflow()\n        {\n            var g1 = this.WhenAnyValue\n            (\n                x => x.InputFilePath,\n                x => x.OutputFilename,\n                x => x.InputFolderPath,\n                x => x.OutputFolderPath,\n                x => x.SelectedTabIndex,\n                x => x.DisplayDevice,\n                x => x.DisplayPortraitSelected\n            );\n\n            var g2 = this.WhenAnyValue\n            (\n                x => x.UpscaleImages,\n                x => x.UpscaleArchives,\n                x => x.OverwriteExistingFiles,\n                x => x.WebpSelected,\n                x => x.PngSelected,\n                x => x.JpegSelected,\n                x => x.AvifSelected\n            );\n\n            var g3 = this.WhenAnyValue\n            (\n                x => x.ModeFitToDisplaySelected,\n                x => x.ModeHeightSelected,\n                x => x.ModeWidthSelected,\n                x => x.ResizeHeightAfterUpscale,\n                x => x.ResizeWidthAfterUpscale\n            );\n\n            g1.CombineLatest(g2).CombineLatest(g3).Subscribe(x =>\n            {\n                Validate();\n            });\n\n            this.WhenAnyValue(x => x.Vm).Subscribe(x =>\n            {\n                sub?.Dispose();\n                sub = Vm.WhenAnyValue(\n                    x => x.SelectedWorkflowIndex,\n                    x => x.RequestShowAppSettings\n                    ).Subscribe(x =>\n                    {\n                        this.RaisePropertyChanged(nameof(ActiveWorkflow));\n                        Vm?.RaisePropertyChanged(\"Workflows\");\n                    });\n            });\n\n            this.WhenAnyValue(x => x.InputFilePath).Subscribe(x =>\n            {\n                if (string.IsNullOrWhiteSpace(OutputFolderPath) && !string.IsNullOrWhiteSpace(InputFilePath))\n                {\n                    try\n                    {\n                        OutputFolderPath = Directory.GetParent(InputFilePath)?.ToString() ?? \"\";\n                    }\n                    catch (Exception)\n                    {\n\n                    }\n                }\n            });\n\n            this.WhenAnyValue(x => x.InputFolderPath).Subscribe(x =>\n            {\n                if (string.IsNullOrWhiteSpace(OutputFolderPath) && !string.IsNullOrWhiteSpace(InputFolderPath))\n                {\n                    try\n                    {\n                        OutputFolderPath = $\"{InputFolderPath} mangajanai\";\n                    }\n                    catch (Exception)\n                    {\n\n                    }\n                }\n            });\n        }\n\n        private IDisposable? sub;\n\n        private MainWindowViewModel? _vm;\n        public MainWindowViewModel? Vm\n        {\n            get => _vm;\n            set => this.RaiseAndSetIfChanged(ref _vm, value);\n        }\n\n        private string _workflowName;\n        [DataMember]\n        public string WorkflowName\n        {\n            get => _workflowName;\n            set => this.RaiseAndSetIfChanged(ref _workflowName, value);\n        }\n\n        private int _workflowIndex;\n        [DataMember]\n        public int WorkflowIndex\n        {\n            get => _workflowIndex;\n            set => this.RaiseAndSetIfChanged(ref _workflowIndex, value);\n\n        }\n\n        public string WorkflowIcon => $\"Numeric{WorkflowIndex}Circle\";\n\n        public bool ActiveWorkflow\n        {\n            get\n            {\n                Debug.WriteLine($\"ActiveWorkflow {WorkflowIndex} == {Vm?.SelectedWorkflowIndex}; {Vm == null}\");\n                return WorkflowIndex == Vm?.SelectedWorkflowIndex && (!Vm?.ShowAppSettings ?? false);\n            }\n\n        }\n\n        public bool IsDefaultWorkflow => WorkflowIndex == 0;\n\n        private int _selectedTabIndex;\n        [DataMember]\n        public int SelectedTabIndex\n        {\n            get => _selectedTabIndex;\n            set\n            {\n                if (_selectedTabIndex != value)\n                {\n                    this.RaiseAndSetIfChanged(ref _selectedTabIndex, value);\n                    Vm?.RaisePropertyChanged(nameof(Vm.InputStatusText));  // TODO\n                }\n            }\n        }\n\n        private string _inputFilePath = string.Empty;\n        [DataMember]\n        public string InputFilePath\n        {\n            get => _inputFilePath;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _inputFilePath, value);\n                Vm?.RaisePropertyChanged(nameof(Vm.InputStatusText));  // TODO\n            }\n        }\n\n        private string _inputFolderPath = string.Empty;\n        [DataMember]\n        public string InputFolderPath\n        {\n            get => _inputFolderPath;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _inputFolderPath, value);\n                Vm?.RaisePropertyChanged(nameof(Vm.InputStatusText)); // TODO\n            }\n        }\n\n        private string _outputFilename = \"%filename%-mangajanai\";\n        [DataMember]\n        public string OutputFilename\n        {\n            get => _outputFilename;\n            set => this.RaiseAndSetIfChanged(ref _outputFilename, value);\n        }\n\n        private string _outputFolderPath = string.Empty;\n        [DataMember]\n        public string OutputFolderPath\n        {\n            get => _outputFolderPath;\n            set => this.RaiseAndSetIfChanged(ref _outputFolderPath, value);\n        }\n\n        private bool _overwriteExistingFiles = false;\n        [DataMember]\n        public bool OverwriteExistingFiles\n        {\n            get => _overwriteExistingFiles;\n            set => this.RaiseAndSetIfChanged(ref _overwriteExistingFiles, value);\n        }\n\n        private bool _upscaleImages = false;\n        [DataMember]\n        public bool UpscaleImages\n        {\n            get => _upscaleImages;\n            set => this.RaiseAndSetIfChanged(ref _upscaleImages, value);\n        }\n\n        private bool _upscaleArchives = true;\n        [DataMember]\n        public bool UpscaleArchives\n        {\n            get => _upscaleArchives;\n            set => this.RaiseAndSetIfChanged(ref _upscaleArchives, value);\n        }\n\n        private int? _resizeHeightAfterUpscale = 2160;\n        [DataMember]\n        public int? ResizeHeightAfterUpscale\n        {\n            get => _resizeHeightAfterUpscale;\n            set => this.RaiseAndSetIfChanged(ref _resizeHeightAfterUpscale, value ?? 2160);\n        }\n\n        private int? _resizeWidthAfterUpscale = 3840;\n        [DataMember]\n        public int? ResizeWidthAfterUpscale\n        {\n            get => _resizeWidthAfterUpscale;\n            set => this.RaiseAndSetIfChanged(ref _resizeWidthAfterUpscale, value ?? 3840);\n        }\n\n        private bool _webpSelected = true;\n        [DataMember]\n        public bool WebpSelected\n        {\n            get => _webpSelected;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _webpSelected, value);\n                this.RaisePropertyChanged(nameof(ShowUseLosslessCompression));\n                this.RaisePropertyChanged(nameof(ShowLossyCompressionQuality));\n            }\n        }\n\n        private bool _avifSelected = false;\n        [DataMember]\n        public bool AvifSelected\n        {\n            get => _avifSelected;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _avifSelected, value);\n                this.RaisePropertyChanged(nameof(ShowLossyCompressionQuality));\n                this.RaisePropertyChanged(nameof(ShowUseLosslessCompression));\n            }\n        }\n\n        private bool _pngSelected = false;\n        [DataMember]\n        public bool PngSelected\n        {\n            get => _pngSelected;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _pngSelected, value);\n            }\n        }\n\n        private bool _jpegSelected = false;\n        [DataMember]\n        public bool JpegSelected\n        {\n            get => _jpegSelected;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _jpegSelected, value);\n                this.RaisePropertyChanged(nameof(ShowLossyCompressionQuality));\n            }\n        }\n\n        public string ImageFormat => WebpSelected ? \"webp\" : PngSelected ? \"png\" : AvifSelected ? \"avif\" : \"jpg\";\n\n        public bool ShowUseLosslessCompression => WebpSelected;\n\n        private bool _useLosslessCompression = false;\n        [DataMember]\n        public bool UseLosslessCompression\n        {\n            get => _useLosslessCompression;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _useLosslessCompression, value);\n                this.RaisePropertyChanged(nameof(ShowLossyCompressionQuality));\n            }\n        }\n\n        public bool ShowLossyCompressionQuality => JpegSelected || (WebpSelected && !UseLosslessCompression) || AvifSelected;\n\n        private int? _lossyCompressionQuality = 80;\n        [DataMember]\n        public int? LossyCompressionQuality\n        {\n            get => _lossyCompressionQuality;\n            set => this.RaiseAndSetIfChanged(ref _lossyCompressionQuality, value ?? 80);\n        }\n\n        private bool _showLossySettings = true;\n        [DataMember]\n        public bool ShowLossySettings\n        {\n            get => _showLossySettings;\n            set => this.RaiseAndSetIfChanged(ref _showLossySettings, value);\n        }\n\n        private bool _modeScaleSelected = true;\n        [DataMember]\n        public bool ModeScaleSelected\n        {\n            get => _modeScaleSelected;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _modeScaleSelected, value);\n            }\n        }\n\n        private int _upscaleScaleFactor = 4;\n        [DataMember]\n        public int UpscaleScaleFactor\n        {\n            get => _upscaleScaleFactor;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _upscaleScaleFactor, value);\n                this.RaisePropertyChanged(nameof(Is1x));\n                this.RaisePropertyChanged(nameof(Is2x));\n                this.RaisePropertyChanged(nameof(Is3x));\n                this.RaisePropertyChanged(nameof(Is4x));\n            }\n        }\n\n        public bool Is1x => UpscaleScaleFactor == 1;\n        public bool Is2x => UpscaleScaleFactor == 2;\n        public bool Is3x => UpscaleScaleFactor == 3;\n        public bool Is4x => UpscaleScaleFactor == 4;\n\n\n        public void SetUpscaleScaleFactor(int scaleFactor)\n        {\n            UpscaleScaleFactor = scaleFactor;\n        }\n\n        private bool _modeWidthSelected = false;\n        [DataMember]\n        public bool ModeWidthSelected\n        {\n            get => _modeWidthSelected;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _modeWidthSelected, value);\n            }\n        }\n\n        private bool _modeHeightSelected = false;\n        [DataMember]\n        public bool ModeHeightSelected\n        {\n            get => _modeHeightSelected;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _modeHeightSelected, value);\n            }\n        }\n\n        private bool _modeFitToDisplaySelected = false;\n        [DataMember]\n        public bool ModeFitToDisplaySelected\n        {\n            get => _modeFitToDisplaySelected;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _modeFitToDisplaySelected, value);\n            }\n        }\n\n        private string _displayDevice;\n        [DataMember]\n        public string DisplayDevice\n        {\n            get => _displayDevice;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _displayDevice, value);\n                this.RaisePropertyChanged(nameof(DisplayDeviceWidth));\n                this.RaisePropertyChanged(nameof(DisplayDeviceHeight));\n            }\n        }\n\n        [DataMember]\n        public int DisplayDeviceWidth\n        {\n            get\n            {\n                if (Vm != null && DisplayDevice != null)\n                {\n                    Vm.DisplayDeviceMap.TryGetValue(DisplayDevice, out var displayDevice);\n                    if (displayDevice != null)\n                    {\n                        return DisplayPortraitSelected ? displayDevice.Width : displayDevice.Height;\n                    }\n                }\n\n                return 0;\n            }\n        }\n\n        [DataMember]\n        public int DisplayDeviceHeight\n        {\n            get\n            {\n                if (Vm != null && DisplayDevice != null)\n                {\n                    Vm.DisplayDeviceMap.TryGetValue(DisplayDevice, out var displayDevice);\n                    if (displayDevice != null)\n                    {\n                        return DisplayPortraitSelected ? displayDevice.Height : displayDevice.Width;\n                    }\n                }\n\n                return 0;\n            }\n        }\n\n        private bool _displayPortraitSelected = true;\n        [DataMember]\n        public bool DisplayPortraitSelected\n        {\n            get => _displayPortraitSelected;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _displayPortraitSelected, value);\n                this.RaisePropertyChanged(nameof(DisplayDeviceWidth));\n                this.RaisePropertyChanged(nameof(DisplayDeviceHeight));\n            }\n        }\n\n        private bool _showAdvancedSettings = false;\n        [DataMember]\n        public bool ShowAdvancedSettings\n        {\n            get => _showAdvancedSettings;\n            set => this.RaiseAndSetIfChanged(ref _showAdvancedSettings, value);\n        }\n\n        private int _grayscaleDetectionThreshold = 12;\n        [DataMember]\n        public int GrayscaleDetectionThreshold\n        {\n            get => _grayscaleDetectionThreshold;\n            set => this.RaiseAndSetIfChanged(ref _grayscaleDetectionThreshold, value);\n        }\n\n        private AvaloniaList<UpscaleChain> _chains;\n        [DataMember]\n        public AvaloniaList<UpscaleChain> Chains\n        {\n            get => _chains;\n            set => this.RaiseAndSetIfChanged(ref _chains, value);\n        }\n\n        private bool _valid = false;\n        [IgnoreDataMember]\n        public bool Valid\n        {\n            get => _valid;\n            set\n            {\n                this.RaiseAndSetIfChanged(ref _valid, value);\n                if (Vm != null)\n                {\n                    Vm.RaisePropertyChanged(nameof(Vm.UpscaleEnabled));  // TODO\n                    Vm.RaisePropertyChanged(nameof(Vm.LeftStatus));  // TODO\n                }\n            }\n        }\n\n        public void SetWebpSelected()\n        {\n            WebpSelected = true;\n            PngSelected = false;\n            JpegSelected = false;\n            AvifSelected = false;\n        }\n\n        public void SetPngSelected()\n        {\n            PngSelected = true;\n            WebpSelected = false;\n            JpegSelected = false;\n            AvifSelected = false;\n        }\n\n        public void SetJpegSelected()\n        {\n            JpegSelected = true;\n            WebpSelected = false;\n            PngSelected = false;\n            AvifSelected = false;\n        }\n\n        public void SetAvifSelected()\n        {\n            AvifSelected = true;\n            JpegSelected = false;\n            WebpSelected = false;\n            PngSelected = false;\n        }\n\n        public void SetModeScaleSelected()\n        {\n            ModeScaleSelected = true;\n            ModeWidthSelected = false;\n            ModeHeightSelected = false;\n            ModeFitToDisplaySelected = false;\n        }\n\n        public void SetModeWidthSelected()\n        {\n            ModeWidthSelected = true;\n            ModeScaleSelected = false;\n            ModeHeightSelected = false;\n            ModeFitToDisplaySelected = false;\n        }\n\n        public void SetModeHeightSelected()\n        {\n            ModeHeightSelected = true;\n            ModeScaleSelected = false;\n            ModeWidthSelected = false;\n            ModeFitToDisplaySelected = false;\n        }\n\n        public void SetModeFitToDisplaySelected()\n        {\n            ModeFitToDisplaySelected = true;\n            ModeHeightSelected = false;\n            ModeWidthSelected = false;\n            ModeScaleSelected = false;\n        }\n\n        public void Validate()\n        {\n            var valid = true;\n            var validationText = new List<string>();\n            if (SelectedTabIndex == 0)\n            {\n\n                if (string.IsNullOrWhiteSpace(InputFilePath))\n                {\n                    valid = false;\n                    validationText.Add(\"Input File is required.\");\n                }\n                else if (!File.Exists(InputFilePath))\n                {\n                    valid = false;\n                    validationText.Add(\"Input File does not exist.\");\n                }\n\n            }\n            else\n            {\n                if (string.IsNullOrWhiteSpace(InputFolderPath))\n                {\n                    valid = false;\n                    validationText.Add(\"Input Folder is required.\");\n                }\n                else if (!Directory.Exists(InputFolderPath))\n                {\n                    valid = false;\n                    validationText.Add(\"Input Folder does not exist.\");\n                }\n            }\n\n            if (string.IsNullOrWhiteSpace(OutputFilename))\n            {\n                valid = false;\n                validationText.Add(\"Output Filename is required.\");\n            }\n\n            if (string.IsNullOrWhiteSpace(OutputFolderPath))\n            {\n                valid = false;\n                validationText.Add(\"Output Folder is required.\");\n            }\n\n            if (ModeHeightSelected && ResizeHeightAfterUpscale == 0)\n            {\n                valid = false;\n                validationText.Add(\"Output Height is invalid. Enter a height larger than 0.\");\n            }\n\n            if (ModeWidthSelected && ResizeWidthAfterUpscale == 0)\n            {\n                valid = false;\n                validationText.Add(\"Output Width is invalid. Enter a width larger than 0.\");\n            }\n\n            if (ModeFitToDisplaySelected && (DisplayDeviceWidth == 0 || DisplayDeviceHeight == 0))\n            {\n                valid = false;\n                validationText.Add(\"Tablet Device or Display is invalid. Please make a selection from the list of options.\");\n            }\n\n            Valid = valid;\n\n            if (Vm != null)\n            {\n                // TODO\n                Vm.CheckInputs();\n                if (Vm?.ProgressTotalFiles == 0)\n                {\n                    Valid = false;\n                    validationText.Add($\"{Vm?.InputStatusText} selected for upscaling. At least one file must be selected.\");\n                }\n                Vm.ValidationText = string.Join(\"\\n\", validationText);\n            }\n        }\n    }\n\n    [DataContract]\n    public class UpscaleChain : ReactiveObject\n    {\n        IPythonService _pythonService;\n\n        public UpscaleChain(IPythonService? pythonService = null)\n        {\n            _pythonService = pythonService ?? Locator.Current.GetService<IPythonService>()!;\n\n            this.WhenAnyValue(x => x.Vm).Subscribe(x =>\n            {\n                sub?.Dispose();\n                sub = Vm.WhenAnyValue(\n                    x => x.IsExtractingBackend\n                    ).Subscribe(x =>\n                    {\n                        this.RaisePropertyChanged(nameof(AllModels));\n                        this.RaisePropertyChanged(nameof(ModelFilePath));\n                    });\n            });\n\n            this.RaisePropertyChanged(nameof(AllModels));\n            this.RaisePropertyChanged(nameof(ModelFilePath));\n        }\n\n        private IDisposable? sub;\n\n        private MainWindowViewModel? _vm;\n        public MainWindowViewModel? Vm\n        {\n            get => _vm;\n            set => this.RaiseAndSetIfChanged(ref _vm, value);\n        }\n\n        private string _chainNumber = string.Empty;\n        [DataMember]\n        public string ChainNumber\n        {\n            get => _chainNumber;\n            set => this.RaiseAndSetIfChanged(ref _chainNumber, value);\n        }\n\n        private string _minResolution = \"0x0\";\n        [DataMember]\n        public string MinResolution\n        {\n            get => _minResolution;\n            set => this.RaiseAndSetIfChanged(ref _minResolution, value);\n        }\n\n        private string _maxResolution = \"0x0\";\n        [DataMember]\n        public string MaxResolution\n        {\n            get => _maxResolution;\n            set => this.RaiseAndSetIfChanged(ref _maxResolution, value);\n        }\n\n        private bool _isGrayscale = false;\n        [DataMember]\n        public bool IsGrayscale\n        {\n            get => _isGrayscale;\n            set => this.RaiseAndSetIfChanged(ref _isGrayscale, value);\n        }\n\n        private bool _isColor = false;\n        [DataMember]\n        public bool IsColor\n        {\n            get => _isColor;\n            set => this.RaiseAndSetIfChanged(ref _isColor, value);\n        }\n\n        private int? _minScaleFactor = 0;\n        [DataMember]\n        public int? MinScaleFactor\n        {\n            get => _minScaleFactor;\n            set => this.RaiseAndSetIfChanged(ref _minScaleFactor, value ?? 0);\n        }\n\n        private int? _maxScaleFactor = 0;\n        [DataMember]\n        public int? MaxScaleFactor\n        {\n            get => _maxScaleFactor;\n            set => this.RaiseAndSetIfChanged(ref _maxScaleFactor, value ?? 0);\n        }\n\n        private string _modelFilePath = string.Empty;\n        [DataMember]\n        public string ModelFilePath\n        {\n            get => _modelFilePath;\n            set => this.RaiseAndSetIfChanged(ref _modelFilePath, value);\n        }\n\n        private string _modelTileSize = \"Auto (Estimate)\";\n        [DataMember]\n        public string ModelTileSize\n        {\n            get => _modelTileSize;\n            set => this.RaiseAndSetIfChanged(ref _modelTileSize, value);\n        }\n\n        private bool _autoAdjustLevels = false;\n        [DataMember]\n        public bool AutoAdjustLevels\n        {\n            get => _autoAdjustLevels;\n            set => this.RaiseAndSetIfChanged(ref _autoAdjustLevels, value);\n        }\n\n        private int? _resizeHeightBeforeUpscale = 0;\n        [DataMember]\n        public int? ResizeHeightBeforeUpscale\n        {\n            get => _resizeHeightBeforeUpscale;\n            set => this.RaiseAndSetIfChanged(ref _resizeHeightBeforeUpscale, value ?? 0);\n        }\n\n        private int? _resizeWidthBeforeUpscale = 0;\n        [DataMember]\n        public int? ResizeWidthBeforeUpscale\n        {\n            get => _resizeWidthBeforeUpscale;\n            set => this.RaiseAndSetIfChanged(ref _resizeWidthBeforeUpscale, value ?? 0);\n        }\n\n        private double? _resizeFactorBeforeUpscale = 100;\n        [DataMember]\n        public double? ResizeFactorBeforeUpscale\n        {\n            get => _resizeFactorBeforeUpscale;\n            set => this.RaiseAndSetIfChanged(ref _resizeFactorBeforeUpscale, value ?? 100);\n        }\n\n        public AvaloniaList<string> AllModels => _pythonService.AllModels;\n\n        private string[] _tileSizes = [\n    \"Auto (Estimate)\",\n            \"Maximum\",\n            \"No Tiling\",\n            \"128\",\n            \"192\",\n            \"256\",\n            \"384\",\n            \"512\",\n            \"768\",\n            \"1024\",\n            \"2048\",\n            \"4096\"];\n\n        public string[] TileSizes\n        {\n            get => _tileSizes;\n            set => this.RaiseAndSetIfChanged(ref _tileSizes, value);\n        }\n    }\n\n    // TODO refactor into separate file\n    public class ReaderDevice\n    {\n        public string Name { get; set; } = default!;\n        public string Brand { get; set; } = default!;\n        public string Year { get; set; } = default!;\n        public int Width { get; set; } = default!;\n        public int Height { get; set; } = default!;\n\n        public override string ToString()\n        {\n            List<string> parts = [];\n\n            if (!string.IsNullOrWhiteSpace(Brand))\n            {\n                parts.Add(Brand);\n            }\n\n            if (!string.IsNullOrWhiteSpace(Name))\n            {\n                parts.Add(Name);\n            }\n\n            if (!string.IsNullOrWhiteSpace(Year))\n            {\n                parts.Add($\"({Year})\");\n            }\n\n            return string.Join(\" \", parts);\n        }\n    }\n\n    public class DeviceResponse\n    {\n        [JsonProperty(\"all_devices\")]\n        public List<AcceleratorDevice> AllDevices { get; set; } = [];\n\n        [JsonProperty(\"best_device\")]\n        public int BestDevice { get; set; }\n    }\n\n    public class AcceleratorDevice\n    {\n        [JsonProperty(\"type\")]\n        public string Type { get; set; } = string.Empty;\n\n        [JsonProperty(\"index\")]\n        public int Index { get; set; }\n\n        [JsonProperty(\"name\")]\n        public string Name { get; set; } = string.Empty;\n\n        [JsonProperty(\"device_string\")]\n        public string DeviceString { get; set; } = string.Empty;\n\n        [JsonProperty(\"supports_fp16\")]\n        public bool SupportsFp16 { get; set; }\n\n        [JsonProperty(\"supports_bf16\")]\n        public bool SupportsBf16 { get; set; }\n\n        [JsonProperty(\"memory_total\")]\n        public long? MemoryTotal { get; set; }\n\n        [JsonProperty(\"memory_free\")]\n        public long? MemoryFree { get; set; }\n    }\n}"
  },
  {
    "path": "MangaJaNaiConverterGui/ViewModels/ViewModelBase.cs",
    "content": "﻿using ReactiveUI;\n\nnamespace MangaJaNaiConverterGui.ViewModels\n{\n    //[DataContract]\n    public class ViewModelBase : ReactiveObject\n    {\n        //private bool _autoUpdate;\n        //[DataMember]\n        //public bool AutoUpdateEnabled\n        //{\n        //    get => _autoUpdate;\n        //    set => this.RaiseAndSetIfChanged(ref _autoUpdate, value);\n        //}\n    }\n}"
  },
  {
    "path": "MangaJaNaiConverterGui/Views/MainWindow.axaml",
    "content": "<Window xmlns=\"https://github.com/avaloniaui\"\n        xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n        xmlns:vm=\"using:MangaJaNaiConverterGui.ViewModels\"\n        xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n        xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n        mc:Ignorable=\"d\" \n        xmlns:materialIcons=\"clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia\"\n        xmlns:hypertext=\"clr-namespace:HyperText.Avalonia.Controls;assembly=HyperText.Avalonia\"\n        xmlns:ui=\"using:FluentAvalonia.UI.Controls\"\n        xmlns:uip=\"using:FluentAvalonia.UI.Controls.Primitives\"\n        x:Class=\"MangaJaNaiConverterGui.Views.MainWindow\"\n        x:DataType=\"vm:MainWindowViewModel\"\n        Icon=\"/Assets/logo.ico\"\n        Title=\"MangaJaNaiConverterGui\">\n\n  <Window.Styles>\n    <Style Selector=\"TabControl[TabStripPlacement=Top]\">\n      <!-- Override styled behaviour -->\n      <Setter Property=\"Padding\" Value=\"0\"/>\n    </Style>\n    <Style Selector=\"TextBlock\">\n      <Setter Property=\"VerticalAlignment\" Value=\"Center\"/>\n    </Style>\n    <Style Selector=\"TextBox\">\n      <Setter Property=\"VerticalAlignment\" Value=\"Center\"/>\n    </Style>\n    <Style Selector=\"Border.border\">\n      \n      <Setter Property=\"Margin\" Value=\"0,10,0,0\" />\n      <Setter Property=\"CornerRadius\" Value=\"5\" />\n      <Setter Property=\"BorderBrush\" Value=\"#33888888\" />\n      <Setter Property=\"BorderThickness\" Value=\"1\" />\n      <Setter Property=\"Padding\" Value=\"10\" />\n    </Style>\n    <Style Selector=\"Button.active\">\n      <Setter Property=\"Background\" Value=\"{DynamicResource SystemAccentColor }\" />\n      <Style Selector=\"^:pointerover\">\n        <Style Selector=\"^ /template/ ContentPresenter#PART_ContentPresenter\">\n          <Setter Property=\"Background\" Value=\"{DynamicResource SystemAccentColor}\" />\n          <Setter Property=\"BorderBrush\" Value=\"{DynamicResource SystemAccentColor}\" />\n        </Style>\n      </Style>\n    </Style>\n  </Window.Styles>\n  \n    <Design.DataContext>\n        <!-- This only sets the DataContext for the previewer in an IDE,\n             to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->\n        <vm:MainWindowViewModel/>\n    </Design.DataContext>\n\n  <Grid>\n    <!-- Main Content -->\n    <Grid ColumnDefinitions=\"300,*\" RowDefinitions=\"*\" IsVisible=\"{Binding !IsExtractingBackend}\">\n      <DockPanel Grid.Column=\"0\" Background=\"#33000000\">\n\n        <StackPanel DockPanel.Dock=\"Bottom\">\n          <TextBlock Margin=\"0\">\n\n          </TextBlock>\n          <Button FontSize=\"13\" Padding=\"10\" Classes.active=\"{Binding ShowAppSettings}\" Width=\"300\" HorizontalContentAlignment=\"Left\" Command=\"{Binding HandleAppSettingsSelected}\">\n            <StackPanel Orientation=\"Horizontal\">\n              <materialIcons:MaterialIcon Kind=\"Gear\" VerticalAlignment=\"Center\" />\n              <TextBlock Margin=\"5,0,5,0\" VerticalAlignment=\"Center\">App Settings</TextBlock>\n            </StackPanel>\n          </Button>\n        </StackPanel>\n\n        <StackPanel>\n          <TextBlock Margin=\"10\">\n            Default Workflows\n          </TextBlock>\n\n          <Button FontSize=\"13\" Padding=\"10\" Width=\"300\" HorizontalContentAlignment=\"Left\" Command=\"{Binding HandleWorkflowSelected}\" CommandParameter=\"0\"\n                  Classes.active=\"{Binding Workflows[0].ActiveWorkflow}\">\n            <StackPanel Orientation=\"Horizontal\">\n              <materialIcons:MaterialIcon Kind=\"Book\" VerticalAlignment=\"Center\" />\n              <TextBlock Margin=\"5,0,5,0\" VerticalAlignment=\"Center\" Text=\"{Binding Workflows[0].WorkflowName}\" />\n            </StackPanel>\n          </Button>\n\n          <StackPanel Orientation=\"Horizontal\" Margin=\"10\" ToolTip.Tip=\"Custom workflows provide convenient access to save and load preset settings for upscaling.\" ToolTip.ShowDelay=\"200\">\n            <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">\n              Custom Workflows\n            </TextBlock>\n            <materialIcons:MaterialIcon Kind=\"QuestionMarkCircle\" VerticalAlignment=\"Center\" Opacity=\"0.5\" />\n          </StackPanel>\n\n          <ItemsControl ItemsSource=\"{Binding CustomWorkflows}\">\n            <ItemsControl.ItemTemplate>\n              <DataTemplate x:DataType=\"vm:UpscaleWorkflow\">\n                <StackPanel>\n                  <Button FontSize=\"13\" Padding=\"10\" Width=\"300\" HorizontalContentAlignment=\"Left\"\n                          ClickMode=\"Press\"\n                          Classes.active=\"{Binding ActiveWorkflow}\"\n                          Command=\"{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).HandleWorkflowSelected}\"\n                          CommandParameter=\"{Binding WorkflowIndex}\">\n                    <StackPanel Orientation=\"Horizontal\">\n                      <materialIcons:MaterialIcon Kind=\"{Binding WorkflowIcon}\" VerticalAlignment=\"Center\" />\n                      <TextBlock Margin=\"5,0,5,0\" VerticalAlignment=\"Center\" Text=\"{Binding WorkflowName}\" />\n                    </StackPanel>\n                  </Button>\n                </StackPanel>\n              </DataTemplate>\n            </ItemsControl.ItemTemplate>\n          </ItemsControl>\n        </StackPanel>\n\n      </DockPanel>\n\n      <DockPanel IsVisible=\"{Binding ShowMainForm}\" Grid.Column=\"1\">\n\n        <StackPanel DockPanel.Dock=\"Bottom\">\n          <StackPanel Orientation=\"Horizontal\">\n            <Button Margin=\"20,10,0,10\" FontWeight=\"Bold\" Background=\"Green\" IsEnabled=\"{Binding UpscaleEnabled}\" Command=\"{Binding RunUpscale}\">\n              <StackPanel Orientation=\"Horizontal\">\n                <materialIcons:MaterialIcon Kind=\"PlayCircle\" />\n                <TextBlock Margin=\"5,0,0,0\">Upscale</TextBlock>\n              </StackPanel>\n\n            </Button>\n            <Button Margin=\"20,10,0,10\" FontWeight=\"Bold\" Background=\"Red\" IsEnabled=\"{Binding Upscaling}\" Command=\"{Binding CancelUpscale}\">\n              <StackPanel Orientation=\"Horizontal\">\n                <materialIcons:MaterialIcon Kind=\"StopCircle\" />\n                <TextBlock Margin=\"5,0,0,0\">Cancel</TextBlock>\n              </StackPanel>\n            </Button>\n          </StackPanel>\n\n          <StackPanel IsVisible=\"{Binding ShowConsole}\" >\n            <DockPanel>\n              <TextBlock DockPanel.Dock=\"Left\" Margin=\"20,10,0,0\" FontWeight=\"Bold\" Text=\"Console\"></TextBlock>\n              <ToggleButton DockPanel.Dock=\"Right\" Margin=\"0,0,20,0\" IsChecked=\"{Binding !ShowConsole}\">\n                <materialIcons:MaterialIcon Kind=\"Close\" />\n              </ToggleButton>\n              <Rectangle/>\n            </DockPanel>\n\n            <ScrollViewer Margin=\"0,10,0,0\" Background=\"#111111\" Height=\"450\" HorizontalAlignment=\"Stretch\" HorizontalScrollBarVisibility=\"Auto\" Foreground=\"Gray\"  PropertyChanged=\"ConsoleScrollViewer_PropertyChanged\">\n              <SelectableTextBlock Margin=\"20\" Text=\"{Binding ConsoleText}\" FontFamily=\"Consolas\" PropertyChanged=\"ConsoleTextBlock_PropertyChanged\" />\n            </ScrollViewer>\n          </StackPanel>\n\n          <DockPanel Margin=\"0\" Height=\"30\" DockPanel.Dock=\"Bottom\" HorizontalAlignment=\"Stretch\">\n\n            <TextBlock Margin=\"10,10,10,0\" DockPanel.Dock=\"Left\" FontSize=\"10\" Text=\"{Binding LeftStatus}\" VerticalAlignment=\"Center\" />\n            <StackPanel DockPanel.Dock=\"Right\" Orientation=\"Horizontal\">\n\n              <StackPanel Orientation=\"Horizontal\" IsVisible=\"{Binding ShowEstimates}\">\n                <TextBlock TextAlignment=\"Center\" Width=\"140\" FontSize=\"10\" Margin=\"5,10,10,0\" Text=\"{Binding ElapsedTime, StringFormat={}Elapsed Time: {0}}\" />\n                <StackPanel Orientation=\"Horizontal\" IsVisible=\"{Binding ShowArchiveProgressBar}\">\n                  <TextBlock TextAlignment=\"Center\" Width=\"220\" FontSize=\"10\" Margin=\"5,10,10,0\" Text=\"{Binding ArchiveEtr, StringFormat=Remaining Time (Current Archive): {0:hh\\\\:mm\\\\:ss}}\" />\n                </StackPanel>\n                <TextBlock TextAlignment=\"Center\" Width=\"180\" FontSize=\"10\" Margin=\"5,10,10,0\" Text=\"{Binding TotalEtr, StringFormat=Remaining Time (Total): {0:hh\\\\:mm\\\\:ss}}\" />\n                <TextBlock TextAlignment=\"Center\" Width=\"180\" FontSize=\"10\" Margin=\"5,10,10,0\" Text=\"{Binding TotalEta, StringFormat={}Estimated Finish Time: {0}}\" />\n              </StackPanel>\n\n              <!-- progress within current archive -->\n              <ProgressBar  Margin=\"5,0,0,0\" Height=\"20\"\n                            Minimum=\"0\"\n                            Maximum=\"{Binding ProgressTotalFilesInCurrentArchive}\"\n                            Value=\"{Binding ProgressCurrentFileInArchive}\"\n                            ProgressTextFormat=\"{}{0:0} / {3:0} images in current archive\"\n                            FontSize=\"10\"\n                            ShowProgressText=\"True\"\n                            MinWidth=\"50\"\n                            Width=\"50\"\n                            MaxWidth=\"50\"\n                            IsVisible=\"{Binding ShowArchiveProgressBar}\" />\n\n              <!-- total progress across all files -->\n              <ProgressBar  Margin=\"5,0,5,0\" Height=\"20\"\n                            Minimum=\"0\"\n                            Maximum=\"{Binding ProgressTotalFiles}\"\n                            Value=\"{Binding ProgressCurrentFile}\"\n                            ProgressTextFormat=\"{}{0:0} / {3:0} total files\"\n                            FontSize=\"10\"\n                            MinWidth=\"50\"\n                            Width=\"50\"\n                            MaxWidth=\"50\"\n                            ShowProgressText=\"True\"/>\n\n              <ToggleButton IsChecked=\"{Binding ShowConsole}\" FontSize=\"10\" Margin=\"5,0,5,0\">\n                <StackPanel Orientation=\"Horizontal\">\n                  <materialIcons:MaterialIcon Kind=\"Console\" VerticalAlignment=\"Center\" />\n                  <TextBlock Margin=\"5,0,5,0\" VerticalAlignment=\"Center\">Console</TextBlock>\n                </StackPanel>\n              </ToggleButton>\n            </StackPanel>\n\n            <Rectangle />\n          </DockPanel>\n        </StackPanel>\n\n        <ScrollViewer HorizontalScrollBarVisibility=\"Auto\" VerticalScrollBarVisibility=\"Visible\">\n          <!-- Main Form -->\n          <StackPanel Margin=\"20\">\n            <Grid>\n              <StackPanel Orientation=\"Horizontal\" Margin=\"0,10,0,10\" Grid.Column=\"0\">\n                <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Workflow Name</TextBlock>\n                <TextBox Width=\"500\" Margin=\"0,0,5,0\" Text=\"{Binding CurrentWorkflow.WorkflowName}\"/>\n              </StackPanel>\n\n              <StackPanel Grid.Column=\"1\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Top\">\n                <StackPanel Margin=\"0,10,0,0\" Orientation=\"Horizontal\" HorizontalAlignment=\"Right\">\n\n                  <Button Margin=\"10,0,10,0\" Click=\"ImportCurrentWorkflowButtonClick\">\n                    <StackPanel Orientation=\"Horizontal\">\n                      <materialIcons:MaterialIcon Kind=\"Import\" VerticalAlignment=\"Center\" />\n                      <TextBlock Margin=\"10,0,10,0\" VerticalAlignment=\"Center\">Import Workflow</TextBlock>\n                    </StackPanel>\n                  </Button>\n                  <Button Margin=\"10,0,10,0\" Click=\"ExportCurrentWorkflowButtonClick\">\n                    <StackPanel Orientation=\"Horizontal\">\n                      <materialIcons:MaterialIcon Kind=\"Export\" VerticalAlignment=\"Center\" />\n                      <TextBlock Margin=\"10,0,10,0\" VerticalAlignment=\"Center\">Export Workflow</TextBlock>\n                    </StackPanel>\n                  </Button>\n                  <Button Margin=\"10,0,0,0\" Click=\"ResetWorkflow\">\n                    <StackPanel Orientation=\"Horizontal\">\n                      <materialIcons:MaterialIcon Kind=\"Refresh\" VerticalAlignment=\"Center\" />\n                      <TextBlock Margin=\"10,0,10,0\" VerticalAlignment=\"Center\">Reset Workflow</TextBlock>\n                    </StackPanel>\n                  </Button>\n                </StackPanel>\n              </StackPanel>\n\n            </Grid>\n            <DockPanel>\n              <TextBlock DockPanel.Dock=\"Left\" FontWeight=\"Bold\" Text=\"Input and Output\"></TextBlock>\n              <CheckBox DockPanel.Dock=\"Right\" IsChecked=\"{Binding CurrentWorkflow.ShowAdvancedSettings}\" Content=\"Show Advanced Settings\" />\n              <Rectangle />\n            </DockPanel>\n\n            <!--Input and Output -->\n            <Border Classes=\"border\"\n                    IsEnabled=\"{Binding !Upscaling}\">\n              <StackPanel>\n                <TabControl SelectedIndex=\"{Binding CurrentWorkflow.SelectedTabIndex}\" Margin=\"0\">\n                  <TabItem VerticalContentAlignment=\"Center\" FontSize=\"16\" Margin=\"0\">\n                    <TabItem.Header>\n                      <StackPanel Orientation=\"Horizontal\">\n                        <materialIcons:MaterialIcon Kind=\"File\" />\n                        <TextBlock Margin=\"5,0,5,0\">Single File Upscale</TextBlock>\n                      </StackPanel>\n                    </TabItem.Header>\n                    <Border Classes=\"border\">\n                      <StackPanel>\n                        <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                          <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Input File</TextBlock>\n                          <TextBox x:Name=\"InputFileNameTextBox\" Margin=\"0,0,5,0\" Text=\"{Binding CurrentWorkflow.InputFilePath}\" IsReadOnly=\"False\" Width=\"600\" DragDrop.AllowDrop=\"True\"/>\n                          <Button Content=\"Select File\" Click=\"OpenInputFileButtonClick\" />\n                          <TextBlock Foreground=\"Gray\" Width=\"500\" TextWrapping=\"WrapWithOverflow\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">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.</TextBlock>\n                        </StackPanel>\n\n                      </StackPanel>\n                    </Border>\n\n                  </TabItem>\n                  <TabItem VerticalAlignment=\"Center\" FontSize=\"16\">\n                    <TabItem.Header>\n                      <StackPanel Orientation=\"Horizontal\">\n                        <materialIcons:MaterialIcon Kind=\"Folder\" />\n                        <TextBlock Margin=\"5,0,5,0\">Batch Folder Upscale</TextBlock>\n                      </StackPanel>\n                    </TabItem.Header>\n                    <StackPanel>\n\n                      <Border Classes=\"border\">\n                        <StackPanel>\n                          <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                            <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Input Folder</TextBlock>\n                            <TextBox x:Name=\"InputFolderNameTextBox\" Margin=\"0,0,5,0\" Text=\"{Binding CurrentWorkflow.InputFolderPath}\" IsReadOnly=\"False\" Width=\"600\" DragDrop.AllowDrop=\"True\" />\n                            <Button Content=\"Select Folder\" Click=\"OpenInputFolderButtonClick\" />\n                            <TextBlock Foreground=\"Gray\" Width=\"500\" TextWrapping=\"WrapWithOverflow\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                              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.\n                            </TextBlock>\n                          </StackPanel>\n\n                          <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,10\">\n                            <CheckBox Margin=\"0,0,5,0\" IsChecked=\"{Binding CurrentWorkflow.UpscaleArchives}\">Upscale Archives</CheckBox>\n                            <CheckBox IsChecked=\"{Binding CurrentWorkflow.UpscaleImages}\">Upscale Images</CheckBox>\n                            <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                              Whether to upscale Image files (*.png, *.jpg, *.jpeg, *.webp, *.bmp) and/or Archive files (*.zip, *.cbz, *.rar, *.cbr) in the selected Input Folder.\n                            </TextBlock>\n                          </StackPanel>\n                        </StackPanel>\n                      </Border>\n\n                    </StackPanel>\n                  </TabItem>\n                </TabControl>\n\n                <Border Classes=\"border\">\n                  <StackPanel>\n                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Output Folder</TextBlock>\n                      <TextBox x:Name=\"OutputFolderNameTextBox\" Classes=\"clearButton\" Margin=\"0,0,5,0\" Text=\"{Binding CurrentWorkflow.OutputFolderPath}\" IsReadOnly=\"False\" Width=\"600\" DragDrop.AllowDrop=\"True\" />\n                      <Button Content=\"Select Folder\" Click=\"OpenOutputFolderButtonClick\" />\n                      <TextBlock Foreground=\"Gray\" Width=\"500\" TextWrapping=\"WrapWithOverflow\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                        Path of the folder to save the upscaled image(s) or archive(s).\n                      </TextBlock>\n                    </StackPanel>\n\n                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,10\">\n                      <TextBlock Margin=\"0,0,5,0\">Output Filename</TextBlock>\n                      <TextBox Text=\"{Binding CurrentWorkflow.OutputFilename}\" Margin=\"0,0,5,0\" IsReadOnly=\"False\" Width=\"600\" DragDrop.AllowDrop=\"True\" />\n                      <SelectableTextBlock Foreground=\"Gray\" Width=\"500\" TextWrapping=\"WrapWithOverflow\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                        The filename of the upscaled image(s) or archive(s), without the file extension. <Run FontFamily=\"Consolas\">%filename%</Run> 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.\n                      </SelectableTextBlock>\n                    </StackPanel>\n\n                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,0\">\n                      <CheckBox Margin=\"0,0,5,0\" VerticalAlignment=\"Center\" IsChecked=\"{Binding CurrentWorkflow.OverwriteExistingFiles}\">Allow Files in Output Path to be Overwritten</CheckBox>\n                      <TextBlock Width=\"600\" TextWrapping=\"WrapWithOverflow\" Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">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.</TextBlock>\n                    </StackPanel>\n                  </StackPanel>\n                </Border>\n\n\n                <Border Classes=\"border\">\n                  <StackPanel>\n                    <StackPanel Orientation=\"Horizontal\" VerticalAlignment=\"Center\" Margin=\"10,10,0,10\">\n                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Output Image Format</TextBlock>\n                      <ToggleButton IsChecked=\"{Binding CurrentWorkflow.WebpSelected}\" Content=\"WebP\" Command=\"{Binding CurrentWorkflow.SetWebpSelected}\" />\n                      <ToggleButton IsChecked=\"{Binding CurrentWorkflow.AvifSelected}\" Content=\"AVIF\" Command=\"{Binding CurrentWorkflow.SetAvifSelected}\" />\n                      <ToggleButton IsChecked=\"{Binding CurrentWorkflow.PngSelected}\" Content=\"PNG\" Command=\"{Binding CurrentWorkflow.SetPngSelected}\" />\n                      <ToggleButton IsChecked=\"{Binding CurrentWorkflow.JpegSelected}\" Content=\"JPEG\" Command=\"{Binding CurrentWorkflow.SetJpegSelected}\" />\n                      <TextBlock Foreground=\"Gray\" Width=\"950\" TextWrapping=\"WrapWithOverflow\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"40,0,0,0\" xml:space=\"preserve\"><Bold>WebP</Bold>: Modern format recommended for good quality and efficient filesize compression, with good compatibility on modern devices. Supports lossless and lossy compression. \n<Bold>AVIF</Bold>: Modern format with better lossy compression efficiency than WebP, but not as widely supported and slower to save and load compared to WebP.\n<Bold>PNG</Bold>: Lossless compressed format with excellent compatibility, but worse compression efficiency than WebP and AVIF. \n<Bold>JPEG</Bold>: Lossy compressed format with excellent compatibility, but worse compression efficiency than WebP and AVIF. \n            </TextBlock>\n                    </StackPanel>\n\n                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,10\" IsVisible=\"{Binding CurrentWorkflow.ShowUseLosslessCompression}\">\n                      <CheckBox Margin=\"0,0,5,0\" IsChecked=\"{Binding CurrentWorkflow.UseLosslessCompression}\">Use Lossless Compression</CheckBox>\n                      <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                        Use lossless compression. Usually not recommended due to producing images with much larger filesize with little visual benefit.\n                      </TextBlock>\n                    </StackPanel>\n\n                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,10\" IsVisible=\"{Binding CurrentWorkflow.ShowLossyCompressionQuality}\">\n                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Lossy Compression Quality</TextBlock>\n                      <NumericUpDown Margin=\"0,0,5,0\" Value=\"{Binding CurrentWorkflow.LossyCompressionQuality}\" Increment=\"1\" Minimum=\"0\" Maximum=\"100\" Width=\"120\"  />\n                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">%</TextBlock>\n                      <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                        Quality level for compression. Note that a quality level of 100 is still lossy.\n                      </TextBlock>\n                    </StackPanel>\n                  </StackPanel>\n                </Border>\n\n              </StackPanel>\n            </Border>\n\n            <TextBlock Margin=\"0,40,0,0\" FontWeight=\"Bold\" Text=\"Upscaling\"></TextBlock>\n            <Border Classes=\"border\" IsEnabled=\"{Binding !Upscaling}\">\n\n              <StackPanel>\n\n                <StackPanel Orientation=\"Horizontal\" VerticalAlignment=\"Center\" Margin=\"10,10,0,10\">\n                  <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Upscale Mode</TextBlock>\n                  <ToggleButton IsChecked=\"{Binding CurrentWorkflow.ModeScaleSelected}\" Content=\"Scale\" Command=\"{Binding CurrentWorkflow.SetModeScaleSelected}\" />\n                  <ToggleButton IsChecked=\"{Binding CurrentWorkflow.ModeWidthSelected}\" Content=\"Width\" Command=\"{Binding CurrentWorkflow.SetModeWidthSelected}\" />\n                  <ToggleButton IsChecked=\"{Binding CurrentWorkflow.ModeHeightSelected}\" Content=\"Height\" Command=\"{Binding CurrentWorkflow.SetModeHeightSelected}\" />\n                  <ToggleButton IsChecked=\"{Binding CurrentWorkflow.ModeFitToDisplaySelected}\" Content=\"Fit to Display\" Command=\"{Binding CurrentWorkflow.SetModeFitToDisplaySelected}\" />\n                  <TextBlock Foreground=\"Gray\" Width=\"900\" TextWrapping=\"WrapWithOverflow\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"40,0,0,0\" xml:space=\"preserve\"><Bold>Scale</Bold>: 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.\n<Bold>Width</Bold>: All images will be upscaled to the specified width, while maintaining aspect ratio of the image. \n<Bold>Height</Bold>: All images will be upscaled to the specified height, while maintaining aspect ratio of the image. \n<Bold>Fit to Display</Bold>: All images will be upscaled to fit exactly within the specified display device. \n            </TextBlock>\n                </StackPanel>\n\n                <StackPanel Orientation=\"Horizontal\" VerticalAlignment=\"Center\" Margin=\"10,0,0,10\" IsVisible=\"{Binding CurrentWorkflow.ModeScaleSelected}\">\n                  <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Scale Factor</TextBlock>\n                  <ToggleButton IsChecked=\"{Binding CurrentWorkflow.Is1x}\" Content=\"1x\" Command=\"{Binding CurrentWorkflow.SetUpscaleScaleFactor}\" CommandParameter=\"1\" />\n                  <ToggleButton IsChecked=\"{Binding CurrentWorkflow.Is2x}\" Content=\"2x\" Command=\"{Binding CurrentWorkflow.SetUpscaleScaleFactor}\" CommandParameter=\"2\" />\n                  <ToggleButton IsChecked=\"{Binding CurrentWorkflow.Is3x}\" Content=\"3x\" Command=\"{Binding CurrentWorkflow.SetUpscaleScaleFactor}\" CommandParameter=\"3\" />\n                  <ToggleButton IsChecked=\"{Binding CurrentWorkflow.Is4x}\" Content=\"4x\" Command=\"{Binding CurrentWorkflow.SetUpscaleScaleFactor}\" CommandParameter=\"4\" />\n                  <TextBlock Foreground=\"Gray\" Width=\"900\" TextWrapping=\"WrapWithOverflow\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"40,0,0,0\">\n                    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.\n                  </TextBlock>\n                </StackPanel>\n\n                <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,10\" IsVisible=\"{Binding CurrentWorkflow.ModeHeightSelected}\">\n                  <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Output Height</TextBlock>\n                  <NumericUpDown Margin=\"0,0,5,0\" Value=\"{Binding CurrentWorkflow.ResizeHeightAfterUpscale}\" AllowSpin=\"False\" ShowButtonSpinner=\"False\" Minimum=\"1\" />\n                  <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">px</TextBlock>\n                  <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\" Width=\"900\" TextWrapping=\"WrapWithOverflow\">\n                    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.\n                  </TextBlock>\n                </StackPanel>\n                <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,10\" IsVisible=\"{Binding CurrentWorkflow.ModeWidthSelected}\">\n                  <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Output Width</TextBlock>\n                  <NumericUpDown Margin=\"0,0,5,0\" Value=\"{Binding CurrentWorkflow.ResizeWidthAfterUpscale}\" AllowSpin=\"False\" ShowButtonSpinner=\"False\" Minimum=\"1\" />\n                  <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">px</TextBlock>\n                  <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\" Width=\"900\" TextWrapping=\"WrapWithOverflow\">\n                    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.\n                  </TextBlock>\n                </StackPanel>\n\n                <!--Fit to Device Mode-->\n                <StackPanel IsVisible=\"{Binding CurrentWorkflow.ModeFitToDisplaySelected}\">\n                  <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,10\">\n                    <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Tablet Device or Display</TextBlock>\n                    <AutoCompleteBox Classes=\"clearButton\" Margin=\"0,0,5,0\" AsyncPopulator=\"{Binding PopulateDevicesAsync}\" FilterMode=\"None\" Width=\"400\" Text=\"{Binding CurrentWorkflow.DisplayDevice}\" Tapped=\"HandleDevicesTapped\" GotFocus=\"HandleDevicesGotFocus\" MinimumPrefixLength=\"0\"  />\n                    <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\" Width=\"900\" TextWrapping=\"WrapWithOverflow\">\n                      The name of the tablet or display device. Start typing to show more options.\n                    </TextBlock>\n                  </StackPanel>\n                  <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                    <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Display Orientation</TextBlock>\n                    <ToggleButton IsChecked=\"{Binding CurrentWorkflow.DisplayPortraitSelected}\" Content=\"Portrait\"  />\n                    <ToggleButton IsChecked=\"{Binding !CurrentWorkflow.DisplayPortraitSelected}\" Content=\"Landscape\" />\n                    <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\" Width=\"900\" TextWrapping=\"WrapWithOverflow\">\n                      Whether the display will be used in portrait/vertical mode or landscape/horizontal mode.\n                    </TextBlock>\n                  </StackPanel>\n                  <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                    <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Display Resolution</TextBlock>\n                    <TextBlock Margin=\"5,5,5,0\" FontFamily=\"Consolas\"  VerticalAlignment=\"Center\">\n                      <Run Text=\"{Binding CurrentWorkflow.DisplayDeviceWidth}\"/>\n                      <Run Text=\"px\" />\n                      <Run Text=\"×\" />\n                      <Run Text=\"{Binding CurrentWorkflow.DisplayDeviceHeight}\" />\n                      <Run Text=\"px\" />\n                    </TextBlock>\n                    <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\" Width=\"900\" TextWrapping=\"WrapWithOverflow\">\n                      Actual resolution of the selected display with the selected orientation, width × height.\n                    </TextBlock>\n                  </StackPanel>\n                </StackPanel>\n              </StackPanel>\n            </Border>\n\n            <StackPanel IsVisible=\"{Binding CurrentWorkflow.ShowAdvancedSettings}\" IsEnabled=\"{Binding !Upscaling}\">\n              <StackPanel Margin=\"0,40,0,0\" VerticalAlignment=\"Center\" Orientation=\"Horizontal\" ToolTip.Tip=\"Advanced settings allow you to select different settings to use depending on the image selected for upscaling.&#013;&#013;A chain is a set of conditions and the desired settings to apply when those conditions are met. When upscaling each image, if that image meets the activation conditions of a chain, then that chain's settings are applied to an image. If the image meets the activation conditions of multiple chains, only the settings of the highest matching chain in the list are applied. If the image doesn't meet the activation condition of any chain, then no processing or upscaling is applied to that image.&#013;&#013;The default settings use one upscaling model for color images, IllustrationJaNai, and several different MangaJaNai models for grayscale images. Each MangaJaNai model is optimized to work on images with a different height, ranging from 1200p to 2048p, so the chains are used to select the most suitable MangaJaNai model based on the resolution of the image.\">\n                <TextBlock Margin=\"0,0,5,0\" FontWeight=\"Bold\" Text=\"Advanced Settings\"/>\n                <materialIcons:MaterialIcon Kind=\"QuestionMarkCircle\" Opacity=\"0.5\" VerticalAlignment=\"Center\" />\n              </StackPanel>\n\n              <Border Classes=\"border\">\n                <StackPanel>\n                  <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,0\">\n                    <TextBlock Margin=\"0,0,5,0\" Text=\"Grayscale Detection Threshold\" VerticalAlignment=\"Center\" />\n                    <Slider VerticalAlignment=\"Center\" Value=\"{Binding CurrentWorkflow.GrayscaleDetectionThreshold}\" Minimum=\"0\" Maximum=\"24\" Width=\"500\" TickFrequency=\"1\" IsSnapToTickEnabled=\"True\" />\n                    <TextBlock VerticalAlignment=\"Center\" Width=\"40\" Margin=\"10,10,0,10\" Text=\"{Binding CurrentWorkflow.GrayscaleDetectionThreshold}\" HorizontalAlignment=\"Center\"/>\n                    <TextBlock Width=\"650\" TextWrapping=\"WrapWithOverflow\" Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">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.</TextBlock>\n                  </StackPanel>\n\n                </StackPanel>\n              </Border>\n\n              <Border Classes=\"border\">\n                <StackPanel>\n                  <ItemsControl ItemsSource=\"{Binding CurrentWorkflow.Chains}\">\n                    <ItemsControl.ItemTemplate>\n                      <DataTemplate x:DataType=\"vm:UpscaleChain\">\n                        <StackPanel>\n                          <TextBlock Margin=\"10,10,0,10\" FontWeight=\"Bold\" Text=\"{Binding ChainNumber, StringFormat=Chain {0}}\"  />\n                          <Border Classes=\"border\" Margin=\"0,0,0,20\">\n                            <Grid>\n                              <StackPanel Grid.Column=\"0\">\n                                <TextBlock Margin=\"10,10,0,5\" Text=\"Activation Condition\" />\n                                <Border Classes=\"border\">\n                                  <StackPanel>\n                                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\" VerticalAlignment=\"Center\">\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Resolution Range</TextBlock>\n                                      <ui:FAComboBox IsEditable=\"True\" Width=\"120\"  MaxDropDownHeight=\"600\" Text=\"{Binding MinResolution, Mode=TwoWay}\"\n                                                     VerticalAlignment=\"Center\"\n                                                     ItemsSource=\"{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).CommonResolutions}\" />\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">px</TextBlock>\n\n                                      <TextBlock Margin=\"10,0,20,0\" VerticalAlignment=\"Center\"> - </TextBlock>\n                                      <ui:FAComboBox IsEditable=\"True\" Width=\"120\"  MaxDropDownHeight=\"600\" Text=\"{Binding MaxResolution, Mode=TwoWay}\"\n                                                     VerticalAlignment=\"Center\"\n                                                     ItemsSource=\"{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).CommonResolutions}\" />\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">px</TextBlock>\n                                      <TextBlock Foreground=\"Gray\" Width=\"800\" TextWrapping=\"WrapWithOverflow\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">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.</TextBlock>\n                                    </StackPanel>\n\n                                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Scaling Factor Range</TextBlock>\n                                      <NumericUpDown Margin=\"0,0,5,0\" Value=\"{Binding MinScaleFactor}\" Minimum=\"0\" Maximum=\"999\" Width=\"80\" AllowSpin=\"False\" ShowButtonSpinner=\"False\" />\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">x</TextBlock>\n\n                                      <TextBlock Margin=\"10,0,20,0\" VerticalAlignment=\"Center\"> - </TextBlock>\n                                      <NumericUpDown Margin=\"0,0,5,0\" Value=\"{Binding MaxScaleFactor}\" Minimum=\"0\" Maximum=\"999\" Width=\"80\" AllowSpin=\"False\" ShowButtonSpinner=\"False\" />\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">x</TextBlock>\n                                      <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">Range of necessary scaling factor to activate this chain. A maximum scaling factor of 0 means no maximum limit.</TextBlock>\n                                    </StackPanel>\n\n                                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                                      <CheckBox Margin=\"0,0,5,0\" VerticalAlignment=\"Center\" IsChecked=\"{Binding IsColor}\">Is Color Image</CheckBox>\n                                      <CheckBox Margin=\"0,0,5,0\" VerticalAlignment=\"Center\" IsChecked=\"{Binding IsGrayscale}\">Is Grayscale Image</CheckBox>\n                                      <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">Whether the image is color and/or grayscale. Images that appear grayscale but have faint color due to JPEG artifacts are still considered grayscale.</TextBlock>\n                                    </StackPanel>\n\n                                  </StackPanel>\n                                </Border>\n\n                                <TextBlock Margin=\"10,20,0,5\" Text=\"Upscale Settings\" />\n                                <Border Classes=\"border\">\n                                  <StackPanel>\n\n                                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,0\">\n                                      <CheckBox Margin=\"0,0,5,0\" VerticalAlignment=\"Center\" IsChecked=\"{Binding AutoAdjustLevels}\">Auto Adjust Levels on Grayscale Images</CheckBox>\n                                      <TextBlock Width=\"800\" TextWrapping=\"WrapWithOverflow\" Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">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. </TextBlock>\n                                    </StackPanel>\n                                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Resize Height Before Upscale</TextBlock>\n                                      <NumericUpDown Margin=\"0,0,5,0\" VerticalAlignment=\"Center\" Value=\"{Binding ResizeHeightBeforeUpscale}\" Minimum=\"0\" AllowSpin=\"False\" ShowButtonSpinner=\"False\" />\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">px</TextBlock>\n                                      <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                                        Resize each image to this height before upscaling, set to 0 to disable.\n                                      </TextBlock>\n                                    </StackPanel>\n                                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,10\">\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Resize Width Before Upscale</TextBlock>\n                                      <NumericUpDown Margin=\"0,0,5,0\" Value=\"{Binding ResizeWidthBeforeUpscale}\" Minimum=\"0\" AllowSpin=\"False\" ShowButtonSpinner=\"False\" />\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">px</TextBlock>\n                                      <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                                        Resize each image to this width before upscaling, set to 0 to disable.\n                                      </TextBlock>\n                                    </StackPanel>\n                                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,10\">\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Resize Factor Before Upscale</TextBlock>\n                                      <NumericUpDown Margin=\"0,0,5,0\" VerticalAlignment=\"Center\" Value=\"{Binding ResizeFactorBeforeUpscale}\" Minimum=\"0\" AllowSpin=\"False\" ShowButtonSpinner=\"False\" />\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">%</TextBlock>\n                                      <TextBlock Foreground=\"Gray\" Width=\"800\" TextWrapping=\"WrapWithOverflow\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                                        Resize each image by this factor before upscaling. This setting is ignored if Resize Height Before Upscale is specified.\n                                      </TextBlock>\n                                    </StackPanel>\n\n                                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Model</TextBlock>\n                                      <ComboBox ItemsSource=\"{Binding AllModels}\" SelectedValue=\"{Binding ModelFilePath}\" Width=\"400\" />\n                                      <Button Margin=\"5,0,0,0\" Command=\"{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).OpenModelsDirectory}\">\n                                        <StackPanel Orientation=\"Horizontal\">\n                                          <materialIcons:MaterialIcon Kind=\"Launch\" />\n                                          <TextBlock Margin=\"5,0,0,0\" Text=\"Open Models Directory\" />\n                                        </StackPanel>\n                                      </Button>\n                                      <TextBlock Width=\"500\" TextWrapping=\"WrapWithOverflow\" Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">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.</TextBlock>\n                                    </StackPanel>\n\n                                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,10\">\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Model Tile Size</TextBlock>\n                                      <ui:FAComboBox IsEnabled=\"True\" IsEditable=\"True\" Width=\"160\"  MaxDropDownHeight=\"600\" Text=\"{Binding ModelTileSize, Mode=TwoWay}\"\n                                                     VerticalAlignment=\"Center\"\n                                                     ItemsSource=\"{Binding TileSizes}\" />\n                                      <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">px</TextBlock>\n                                      <TextBlock Width=\"700\" TextWrapping=\"WrapWithOverflow\" Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">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.</TextBlock>\n                                    </StackPanel>\n\n                                  </StackPanel>\n\n                                </Border>\n\n                              </StackPanel>\n                              <Button Grid.Column=\"1\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Top\"\n        IsVisible=\"{Binding !$parent[ItemsControl].((vm:MainWindowViewModel)DataContext).CurrentWorkflow.IsDefaultWorkflow}\"\n        Command=\"{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).DeleteChain}\"\n        CommandParameter=\"{Binding}\">\n                                <StackPanel Orientation=\"Horizontal\">\n                                  <materialIcons:MaterialIcon Kind=\"MinusCircle\" />\n                                  <TextBlock Margin=\"5,0,0,0\" Text=\"{Binding ChainNumber, StringFormat=Remove Chain {0}}\" />\n                                </StackPanel>\n                              </Button>\n                            </Grid>\n                          </Border>\n\n                        </StackPanel>\n                      </DataTemplate>\n                    </ItemsControl.ItemTemplate>\n                  </ItemsControl>\n                  <StackPanel Orientation=\"Horizontal\" IsVisible=\"{Binding !CurrentWorkflow.IsDefaultWorkflow}\">\n                    <Button Margin=\"10,20,0,20\" Command=\"{Binding AddChain}\">\n                      <StackPanel Orientation=\"Horizontal\">\n                        <materialIcons:MaterialIcon Kind=\"PlusCircle\" />\n                        <TextBlock Margin=\"5,0,0,0\">Add Chain</TextBlock>\n                      </StackPanel>\n                    </Button>\n                    <TextBlock Foreground=\"Gray\" Width=\"700\" TextWrapping=\"WrapWithOverflow\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                      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.\n                      This allows you to specify different upscale models for different types of images.\n                    </TextBlock>\n                  </StackPanel>\n                </StackPanel>\n              </Border>\n            </StackPanel>\n\n\n          </StackPanel>\n        </ScrollViewer>\n\n      </DockPanel>\n\n\n      <!-- Settings Overlay -->\n      <ScrollViewer IsVisible=\"{Binding ShowAppSettings}\" HorizontalAlignment=\"Stretch\" VerticalAlignment=\"Stretch\" Margin=\"0,0,0,30\" Grid.Column=\"1\">\n      <StackPanel >\n        <StackPanel HorizontalAlignment=\"Stretch\" VerticalAlignment=\"Stretch\" Margin=\"20\">\n          <DockPanel>\n            <TextBlock DockPanel.Dock=\"Left\" FontWeight=\"Bold\" Text=\"App Settings\"></TextBlock>\n          </DockPanel>\n\n          <!-- App Settings -->\n          <Border Classes=\"border\">\n            <StackPanel>\n              <StackPanel IsVisible=\"{Binding IsInstalled}\">\n                <StackPanel>\n                  <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                    <CheckBox IsChecked=\"{Binding AutoUpdateEnabled}\">Auto Update</CheckBox>\n                    <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                      Whether to automatically check for and install app updates.\n                    </TextBlock>\n                  </StackPanel>\n\n                  <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                    <TextBlock Text=\"Current Version\" VerticalAlignment=\"Center\"></TextBlock>\n                    <TextBlock VerticalAlignment=\"Center\" FontFamily=\"Consolas\" Text=\"{Binding AppVersion}\" Margin=\"20,0,0,0\"></TextBlock>\n                    <hypertext:Hyperlink VerticalAlignment=\"Center\" Margin=\"20,0,0,0\" Url=\"https://github.com/the-database/MangaJaNaiConverterGui/releases\"/>\n                  </StackPanel>\n\n                  <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                    <TextBlock VerticalAlignment=\"Center\" Text=\"{Binding UpdateStatusText}\" Margin=\"0,0,0,0\"></TextBlock>\n                  </StackPanel>\n\n                  <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\" IsVisible=\"{Binding ShowCheckUpdateButton}\">\n                    <Button Command=\"{Binding CheckForUpdates}\">Check for Updates Now</Button>\n                  </StackPanel>\n\n                  <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\" IsVisible=\"{Binding ShowDownloadButton}\">\n                    <Button Command=\"{Binding DownloadUpdate}\">Download Update</Button>\n                  </StackPanel>\n\n                  <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\" IsVisible=\"{Binding ShowApplyButton}\">\n                    <Button Command=\"{Binding ApplyUpdate}\">Restart to Update MangaJaNaiConverterGui</Button>\n                  </StackPanel>\n                </StackPanel>\n              </StackPanel>\n              <StackPanel IsVisible=\"{Binding !IsInstalled}\">\n                <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                  <TextBlock>App is not installed; auto update settings unavailable.</TextBlock>\n                </StackPanel>\n              </StackPanel>\n            </StackPanel>\n          </Border>\n\n          <!-- Upscale Settings -->\n          <DockPanel Margin=\"0,10,0,0\">\n            <TextBlock DockPanel.Dock=\"Left\" FontWeight=\"Bold\" Text=\"Upscale Settings\"></TextBlock>\n          </DockPanel>\n          <Border Classes=\"border\">\n            <StackPanel>\n              <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                <TextBlock Margin=\"0,0,5,0\" VerticalAlignment=\"Center\">Device</TextBlock>\n                <ComboBox ItemsSource=\"{Binding DeviceList}\" SelectedIndex=\"{Binding SelectedDeviceIndex, Mode=TwoWay}\"  />\n                <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                  Which device to use for upscaling with PyTorch. CPU is much slower than GPU and should be avoided unless no GPU is available.\n                </TextBlock>\n              </StackPanel>\n\n              <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                <CheckBox IsChecked=\"{Binding UseFp16}\">FP16 Mode</CheckBox>\n                <TextBlock Foreground=\"Gray\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                  Runs PyTorch upscaling in FP16 mode for less VRAM usage and speedup on RTX GPUs.\n                </TextBlock>\n              </StackPanel>\n            </StackPanel>\n          </Border>\n\n          <!-- Python Environment -->\n          <DockPanel Margin=\"0,10,0,0\">\n            <TextBlock DockPanel.Dock=\"Left\" FontWeight=\"Bold\" Text=\"Python Environment\"></TextBlock>\n          </DockPanel>\n          <Border Classes=\"border\">\n            <StackPanel>\n              <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                <SelectableTextBlock FontFamily=\"Consolas\" Margin=\"0,0,5,0\" VerticalAlignment=\"Center\" Text=\"{Binding PythonPath}\" />\n              </StackPanel>\n\n              <ScrollViewer Margin=\"0,10,0,0\" Background=\"#111111\" Height=\"450\" HorizontalScrollBarVisibility=\"Auto\" Foreground=\"Gray\"  PropertyChanged=\"ConsoleScrollViewer_PropertyChanged\">\n                <SelectableTextBlock Margin=\"20\" Text=\"{Binding PythonPipList}\" FontFamily=\"Consolas\" PropertyChanged=\"ConsoleTextBlock_PropertyChanged\" />\n              </ScrollViewer>\n\n              <StackPanel Orientation=\"Horizontal\" Margin=\"10,10,0,10\">\n                <Button FontWeight=\"Bold\" Background=\"Red\" Content=\"Reinstall Python Environment\" Click=\"ReinstallBackendClick\" />\n                <TextBlock Foreground=\"Gray\" Width=\"400\" TextWrapping=\"WrapWithOverflow\" FontSize=\"12\" VerticalAlignment=\"Center\" Margin=\"20,0,0,0\">\n                  Reinstall the Python environment. Try this if you are having any problems getting upscales to work. This process may take several minutes.\n                </TextBlock>\n              </StackPanel>\n\n            </StackPanel>\n          </Border>\n\n          <Border Classes=\"border\">\n            <ToggleButton DockPanel.Dock=\"Right\" Margin=\"10,10,0,10\" IsChecked=\"{Binding !RequestShowAppSettings}\">\n              <StackPanel Orientation=\"Horizontal\">\n                <materialIcons:MaterialIcon Kind=\"ArrowBackCircle\" />\n                <TextBlock Margin=\"5,0,0,0\">Return</TextBlock>\n              </StackPanel>\n            </ToggleButton>\n          </Border>\n        </StackPanel>\n      </StackPanel>\n      </ScrollViewer>\n    </Grid>\n\n    <Grid IsVisible=\"{Binding IsExtractingBackend}\" HorizontalAlignment=\"Stretch\" VerticalAlignment=\"Stretch\">\n      <StackPanel>\n        <TextBlock HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\" Text=\"{Binding BackendSetupMainStatus}\" />\n        <ScrollViewer Margin=\"0,10,0,0\" Background=\"#111111\" Height=\"450\" Width=\"1200\" HorizontalScrollBarVisibility=\"Auto\" Foreground=\"Gray\"  PropertyChanged=\"ConsoleScrollViewer_PropertyChanged\">\n          <SelectableTextBlock Margin=\"20\" Text=\"{Binding BackendSetupSubStatusText}\" FontFamily=\"Consolas\" PropertyChanged=\"ConsoleTextBlock_PropertyChanged\" />\n        </ScrollViewer>\n      </StackPanel>\n    </Grid>\n  </Grid>\n\n</Window>\n"
  },
  {
    "path": "MangaJaNaiConverterGui/Views/MainWindow.axaml.cs",
    "content": "using Avalonia;\nusing Avalonia.Controls;\nusing Avalonia.Input;\nusing Avalonia.Interactivity;\nusing Avalonia.Layout;\nusing Avalonia.Markup.Xaml;\nusing Avalonia.Media;\nusing Avalonia.Platform.Storage;\nusing FluentAvalonia.UI.Controls;\nusing FluentAvalonia.UI.Windowing;\nusing MangaJaNaiConverterGui.ViewModels;\nusing Material.Icons.Avalonia;\nusing System;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\n\nnamespace MangaJaNaiConverterGui.Views\n{\n    public partial class MainWindow : AppWindow\n    {\n        private bool _autoScrollConsole = true;\n        private bool _userWantsToQuit = false;\n\n        public MainWindow()\n        {\n            AvaloniaXamlLoader.Load(this);\n\n            Resized += MainWindow_Resized;\n            Closing += MainWindow_Closing;\n            Opened += MainWindow_Opened;\n\n            var inputFileNameTextBox = this.FindControl<TextBox>(\"InputFileNameTextBox\");\n            var outputFileNameTextBox = this.FindControl<TextBox>(\"OutputFileNameTextBox\");\n            var inputFolderNameTextBox = this.FindControl<TextBox>(\"InputFolderNameTextBox\");\n            var outputFolderNameTextBox = this.FindControl<TextBox>(\"OutputFolderNameTextBox\");\n            var grayscaleModelFilePathTextBox = this.FindControl<TextBox>(\"GrayscaleModelFilePathTextBox\");\n            var colorModelFilePathTextBox = this.FindControl<TextBox>(\"ColorModelFilePathTextBox\");\n\n            inputFileNameTextBox?.AddHandler(DragDrop.DropEvent, SetInputFilePath);\n            inputFolderNameTextBox?.AddHandler(DragDrop.DropEvent, SetInputFolderPath);\n            outputFolderNameTextBox?.AddHandler(DragDrop.DropEvent, SetOutputFolderPath);\n        }\n\n        private async void MainWindow_Opened(object? sender, EventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n                vm.CheckAndExtractBackend();\n            }\n        }\n\n        private async void MainWindow_Closing(object? sender, WindowClosingEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n                // Show confirmation dialog\n                if (!_userWantsToQuit && vm.Upscaling)\n                {\n                    // Cancel close to show dialog\n                    e.Cancel = true;\n\n                    _userWantsToQuit = await ShowConfirmationDialog(\"Cancel unfinished upscales?\", \"If you exit now, all unfinished upscales will be canceled. Are you sure you want to exit?\");\n\n                    // Close if the user confirmed\n                    if (_userWantsToQuit)\n                    {\n                        vm.CancelUpscale();\n                        Close();\n                    }\n                }\n                else\n                {\n                }\n            }\n        }\n\n        private void ConsoleScrollViewer_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)\n        {\n            if (e.Property.Name == \"Offset\" && sender is ScrollViewer consoleScrollViewer)\n            {\n                if (e.NewValue is Vector newVector)\n                {\n                    _autoScrollConsole = newVector.Y == consoleScrollViewer?.ScrollBarMaximum.Y;\n                }\n            }\n\n        }\n\n        private void ConsoleTextBlock_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)\n        {\n            if (e.Property.Name == \"Text\" && sender is TextBlock textBlock)\n            {\n                if (textBlock.Parent is ScrollViewer consoleScrollViewer)\n                {\n                    if (consoleScrollViewer != null)\n                    {\n                        if (_autoScrollConsole)\n                        {\n                            consoleScrollViewer.ScrollToEnd();\n                        }\n                    }\n                }\n            }\n        }\n\n        private void MainWindow_Resized(object? sender, WindowResizedEventArgs e)\n        {\n            // Set the ScrollViewer width based on the new parent window's width\n            var consoleScrollViewer = this.FindControl<ScrollViewer>(\"ConsoleScrollViewer\");\n            if (consoleScrollViewer != null)\n            {\n                consoleScrollViewer.Width = Width - 340; // Adjust the width as needed\n            }\n        }\n\n        public void SetInputFilePath(object? sender, DragEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n                var files = e.Data.GetFiles().ToList();\n\n\n                if (files.Count > 0)\n                {\n                    var filePath = files[0].TryGetLocalPath();\n                    if (File.Exists(filePath))\n                    {\n                        vm.CurrentWorkflow.InputFilePath = filePath;\n                    }\n                }\n            }\n        }\n\n        public void SetInputFolderPath(object? sender, DragEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n                var files = e.Data.GetFiles().ToList();\n\n\n                if (files.Count > 0)\n                {\n                    var filePath = files[0].TryGetLocalPath();\n                    if (Directory.Exists(filePath))\n                    {\n                        vm.CurrentWorkflow.InputFolderPath = filePath;\n                    }\n                }\n            }\n        }\n\n        public void SetOutputFolderPath(object? sender, DragEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n                var files = e.Data.GetFiles().ToList();\n\n\n                if (files.Count > 0)\n                {\n                    var filePath = files[0].TryGetLocalPath();\n                    if (Directory.Exists(filePath))\n                    {\n                        vm.CurrentWorkflow.OutputFolderPath = filePath;\n                    }\n                }\n            }\n        }\n\n        private async void OpenInputFileButtonClick(object? sender, RoutedEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n                // Get top level from the current control. Alternatively, you can use Window reference instead.\n                var topLevel = TopLevel.GetTopLevel(this);\n\n                var storageProvider = topLevel.StorageProvider;\n\n                IStorageFolder? suggestedStartLocation = null;\n\n                var inputFolder = Path.GetDirectoryName(vm.CurrentWorkflow.InputFilePath);\n\n                if (Directory.Exists(inputFolder))\n                {\n                    suggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(new Uri(inputFolder));\n                }\n\n                // Start async operation to open the dialog.\n                var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions\n                {\n                    Title = \"Open Image or Archive File\",\n                    AllowMultiple = false,\n                    FileTypeFilter = new FilePickerFileType[]\n                    {\n                        new(\"Image or Archive File\") { Patterns = MainWindowViewModel.IMAGE_EXTENSIONS.Concat(MainWindowViewModel.ARCHIVE_EXTENSIONS).Select(x => $\"*{x}\").ToArray(),\n                            MimeTypes = new[] { \"*/*\" } }, FilePickerFileTypes.All,\n                    },\n                    SuggestedStartLocation = suggestedStartLocation,\n                });\n\n                if (files.Count >= 1)\n                {\n                    vm.CurrentWorkflow.InputFilePath = files[0].TryGetLocalPath() ?? \"\";\n                }\n            }\n        }\n\n        private async void OpenInputFolderButtonClick(object? sender, RoutedEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n                // Get top level from the current control. Alternatively, you can use Window reference instead.\n                var topLevel = GetTopLevel(this);\n\n                var storageProvider = topLevel.StorageProvider;\n\n                IStorageFolder? suggestedStartLocation = null;\n\n                if (Directory.Exists(vm.CurrentWorkflow.InputFolderPath))\n                {\n                    suggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(new Uri(Path.GetFullPath(vm.CurrentWorkflow.InputFolderPath)));\n                }\n\n                // Start async operation to open the dialog.\n                var files = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions\n                {\n                    Title = \"Open Folder\",\n                    AllowMultiple = false,\n                    SuggestedStartLocation = suggestedStartLocation\n                });\n\n                if (files.Count >= 1)\n                {\n                    vm.CurrentWorkflow.InputFolderPath = files[0].TryGetLocalPath() ?? \"\";\n                }\n            }\n        }\n\n        private async void OpenOutputFolderButtonClick(object? sender, RoutedEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n                // Get top level from the current control. Alternatively, you can use Window reference instead.\n                var topLevel = GetTopLevel(this);\n\n                var storageProvider = topLevel.StorageProvider;\n\n                IStorageFolder? suggestedStartLocation = null;\n\n                if (Directory.Exists(vm.CurrentWorkflow.OutputFolderPath))\n                {\n                    suggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(new Uri(Path.GetFullPath(vm.CurrentWorkflow.OutputFolderPath)));\n                }\n\n                // Start async operation to open the dialog.\n                var files = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions\n                {\n                    Title = \"Open Folder\",\n                    AllowMultiple = false,\n                    SuggestedStartLocation = suggestedStartLocation\n                });\n\n                if (files.Count >= 1)\n                {\n                    vm.CurrentWorkflow.OutputFolderPath = files[0].TryGetLocalPath() ?? \"\";\n                }\n            }\n        }\n\n        private async void ImportCurrentWorkflowButtonClick(object? sender, RoutedEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n                // Get top level from the current control. Alternatively, you can use Window reference instead.\n                var topLevel = GetTopLevel(this);\n\n                // Start async operation to open the dialog.\n                var storageProvider = topLevel.StorageProvider;\n\n                var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions\n                {\n                    Title = \"Import Workflow File\",\n                    AllowMultiple = false,\n                    FileTypeFilter = [new(\"MangaJaNai Workflow File\") { Patterns = [\"*.mwf\"], MimeTypes = [\"*/*\"] }, FilePickerFileTypes.All],\n                    SuggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)),\n                });\n\n                if (files.Count >= 1)\n                {\n\n                    var inPath = files[0].TryGetLocalPath();\n\n                    if (inPath != null)\n                    {\n                        var td = new TaskDialog\n                        {\n                            Title = \"Confirm Workflow Import\",\n                            ShowProgressBar = false,\n                            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\" +\n                            inPath,\n                            Buttons =\n                        {\n                            TaskDialogButton.OKButton,\n                            TaskDialogButton.CancelButton\n                        }\n                        };\n\n\n                        td.Closing += async (s, e) =>\n                        {\n                            if ((TaskDialogStandardResult)e.Result == TaskDialogStandardResult.OK)\n                            {\n                                var deferral = e.GetDeferral();\n\n                                td.SetProgressBarState(0, TaskDialogProgressState.Indeterminate);\n                                td.ShowProgressBar = true;\n\n                                await Task.Run(() =>\n                                {\n                                    vm.ReadWorkflowFileToCurrentWorkflow(inPath);\n                                });\n\n                                deferral.Complete();\n                            }\n                        };\n\n                        td.XamlRoot = VisualRoot as Visual;\n                        _ = await td.ShowAsync();\n                    }\n                }\n            }\n        }\n\n        private async void ResetWorkflow(object? sender, RoutedEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n\n                var td = new TaskDialog\n                {\n                    Title = \"Confirm Workflow Reset\",\n                    ShowProgressBar = false,\n                    Content = $\"The current workflow's settings will be reset to the default settings. Any unsaved settings for the current workflow will be lost.\",\n                    Buttons =\n                {\n                    TaskDialogButton.OKButton,\n                    TaskDialogButton.CancelButton\n                }\n                };\n\n\n                td.Closing += async (s, e) =>\n                {\n                    if ((TaskDialogStandardResult)e.Result == TaskDialogStandardResult.OK)\n                    {\n                        var deferral = e.GetDeferral();\n\n                        td.SetProgressBarState(0, TaskDialogProgressState.Indeterminate);\n                        td.ShowProgressBar = true;\n\n                        await Task.Run(() =>\n                        {\n                            vm.ResetCurrentWorkflow();\n                        });\n\n                        deferral.Complete();\n                    }\n                };\n\n                td.XamlRoot = VisualRoot as Visual;\n                _ = await td.ShowAsync();\n\n            }\n        }\n\n        private async void ExportCurrentWorkflowButtonClick(object? sender, RoutedEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n                // Get top level from the current control. Alternatively, you can use Window reference instead.\n                var topLevel = GetTopLevel(this);\n\n                var storageProvider = topLevel.StorageProvider;\n\n                // Start async operation to open the dialog.\n                var file = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions\n                {\n                    Title = \"Export Current Profile Conf File\",\n                    DefaultExtension = \"conf\",\n                    FileTypeChoices =\n                    [\n                    new(\"MangaJaNai Workflow File (*.mwf)\") { Patterns = [\"*.mwf\"] },\n                    ],\n                    SuggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)),\n                    SuggestedFileName = vm.CurrentWorkflow?.WorkflowName,\n                });\n\n                if (file is not null)\n                {\n                    var outPath = file.TryGetLocalPath();\n\n                    if (outPath != null)\n                    {\n                        vm.WriteCurrentWorkflowToFile(outPath);\n                    }\n                }\n            }\n        }\n\n        private async void ReinstallBackendClick(object? sender, RoutedEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm)\n            {\n                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?\");\n\n                if (confirm)\n                {\n                    await vm.ReinstallBackend();\n                }\n            }\n        }\n\n        private async Task<bool> ShowConfirmationDialog(string title, string message)\n        {\n            var dialog = new Window\n            {\n                Title = title,\n                Width = 480,\n                Height = 200,\n                WindowStartupLocation = WindowStartupLocation.CenterOwner,\n                //Icon = Icon, // TODO\n                CanResize = false,\n                ShowInTaskbar = false\n            };\n\n            var textBlock = new TextBlock\n            {\n                Text = message,\n                Margin = new Thickness(20),\n                TextWrapping = TextWrapping.Wrap,\n                VerticalAlignment = VerticalAlignment.Center,\n                Width = 380,\n            };\n\n            var materialIcon = new MaterialIcon\n            {\n                Kind = Material.Icons.MaterialIconKind.QuestionMarkCircleOutline,\n                Width = 48,\n                Height = 48,\n            };\n\n            var textPanel = new StackPanel\n            {\n                Orientation = Orientation.Horizontal,\n                Margin = new Thickness(20),\n                Children = { materialIcon, textBlock },\n            };\n\n            var yesButton = new Button\n            {\n                Content = \"Yes\",\n                Width = 100,\n                HorizontalAlignment = HorizontalAlignment.Center,\n                HorizontalContentAlignment = HorizontalAlignment.Center,\n                VerticalAlignment = VerticalAlignment.Center,\n                VerticalContentAlignment = VerticalAlignment.Center,\n                Margin = new Thickness(0, 0, 10, 0)\n            };\n            yesButton.Click += (sender, e) => dialog.Close(true);\n\n            var noButton = new Button\n            {\n                Content = \"No\",\n                Width = 100,\n                HorizontalAlignment = HorizontalAlignment.Center,\n                HorizontalContentAlignment = HorizontalAlignment.Center,\n                VerticalAlignment = VerticalAlignment.Center,\n                VerticalContentAlignment = VerticalAlignment.Center,\n                Margin = new Thickness(0, 0, 0, 0)\n            };\n            noButton.Click += (sender, e) => dialog.Close(false);\n\n            var buttonPanel = new StackPanel\n            {\n                Orientation = Orientation.Horizontal,\n                Children = { yesButton, noButton },\n                HorizontalAlignment = HorizontalAlignment.Right,\n                Margin = new Thickness(20, 0, 20, 20)\n            };\n\n            var mainPanel = new StackPanel\n            {\n                Children = { textPanel, buttonPanel }\n            };\n\n            dialog.Content = mainPanel;\n            var result = await dialog.ShowDialog<bool?>(this);\n\n            return result ?? false;\n        }\n\n        public void HandleDevicesGotFocus(object? sender, GotFocusEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm && sender is AutoCompleteBox cb)\n            {\n                if (vm.CurrentWorkflow != null && e.NavigationMethod != NavigationMethod.Unspecified && e.Source is TextBox)\n                {\n                    cb.IsDropDownOpen = true;\n                }\n            }\n        }\n\n        private void HandleDevicesTapped(object? sender, Avalonia.Input.TappedEventArgs e)\n        {\n            if (DataContext is MainWindowViewModel vm && sender is AutoCompleteBox cb)\n            {\n                if (vm.CurrentWorkflow != null)\n                {\n                    cb.IsDropDownOpen = true;\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "MangaJaNaiConverterGui/app.manifest",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com:asm.v1\">\n  <!-- This manifest is used on Windows only.\n       Don't remove it as it might cause problems with window transparency and embeded controls.\n       For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->\n  <assemblyIdentity version=\"1.0.0.0\" name=\"MangaJaNaiConverterGui.Desktop\"/>\n\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- A list of the Windows versions that this application has been tested on\n           and is designed to work with. Uncomment the appropriate elements\n           and Windows will automatically select the most compatible environment. -->\n\n      <!-- Windows 10 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\" />\n    </application>\n  </compatibility>\n</assembly>\n"
  },
  {
    "path": "MangaJaNaiConverterGui/appstate2.json",
    "content": "{\n  \"$type\": \"MangaJaNaiConverterGui.ViewModels.MainWindowViewModel, MangaJaNaiConverterGui\",\n  \"DisplayDeviceMap\": {\n    \"$type\": \"Avalonia.Collections.AvaloniaDictionary`2[[System.String, System.Private.CoreLib],[MangaJaNaiConverterGui.ViewModels.ReaderDevice, MangaJaNaiConverterGui]], Avalonia.Base\"\n  },\n  \"AutoUpdateEnabled\": true,\n  \"SelectedDeviceIndex\": 0,\n  \"UseCpu\": false,\n  \"UseFp16\": true,\n  \"Workflows\": {\n    \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui]], Avalonia.Base\",\n    \"$values\": [\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Upscale Manga (Default)\",\n        \"WorkflowIndex\": 0,\n        \"SelectedTabIndex\": 0,\n        \"InputFilePath\": \"\",\n        \"InputFolderPath\": \"\",\n        \"OutputFilename\": \"%filename%-mangajanai\",\n        \"OutputFolderPath\": \"\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": true,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": true,\n        \"UpscaleScaleFactor\": 4,\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": false,\n        \"DisplayDevice\": \"Kobo Elipsa 2E (2023)\",\n        \"DisplayDeviceWidth\": 1404,\n        \"DisplayDeviceHeight\": 1872,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_IllustrationJaNai_V3denoise_FDAT_M_unshuffle_30k_fp16.safetensors\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V3denoise_FDAT_M_47k_fp16.safetensors\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"16\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Custom Workflow 1\",\n        \"WorkflowIndex\": 1,\n        \"SelectedTabIndex\": 0,\n        \"InputFilePath\": \"\",\n        \"InputFolderPath\": \"\",\n        \"OutputFilename\": \"%filename%-mangajanai\",\n        \"OutputFolderPath\": \"\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": false,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": true,\n        \"UpscaleScaleFactor\": 4,\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": false,\n        \"DisplayDevice\": null,\n        \"DisplayDeviceWidth\": 0,\n        \"DisplayDeviceHeight\": 0,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V1_ESRGAN_135k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Custom Workflow 2\",\n        \"WorkflowIndex\": 2,\n        \"SelectedTabIndex\": 0,\n        \"InputFilePath\": \"\",\n        \"InputFolderPath\": \"\",\n        \"OutputFilename\": \"%filename%-mangajanai\",\n        \"OutputFolderPath\": \"\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": false,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": true,\n        \"UpscaleScaleFactor\": 4,\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": false,\n        \"DisplayDevice\": null,\n        \"DisplayDeviceWidth\": 0,\n        \"DisplayDeviceHeight\": 0,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V1_ESRGAN_135k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Custom Workflow 3\",\n        \"WorkflowIndex\": 3,\n        \"SelectedTabIndex\": 0,\n        \"InputFilePath\": \"\",\n        \"InputFolderPath\": \"\",\n        \"OutputFilename\": \"%filename%-mangajanai\",\n        \"OutputFolderPath\": \"\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": false,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": true,\n        \"UpscaleScaleFactor\": 4,\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": false,\n        \"DisplayDevice\": null,\n        \"DisplayDeviceWidth\": 0,\n        \"DisplayDeviceHeight\": 0,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V1_ESRGAN_135k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Custom Workflow 4\",\n        \"WorkflowIndex\": 4,\n        \"SelectedTabIndex\": 0,\n        \"InputFilePath\": \"\",\n        \"InputFolderPath\": \"\",\n        \"OutputFilename\": \"%filename%-mangajanai\",\n        \"OutputFolderPath\": \"\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": false,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": true,\n        \"UpscaleScaleFactor\": 4,\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": false,\n        \"DisplayDevice\": null,\n        \"DisplayDeviceWidth\": 0,\n        \"DisplayDeviceHeight\": 0,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V1_ESRGAN_135k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Custom Workflow 5\",\n        \"WorkflowIndex\": 5,\n        \"SelectedTabIndex\": 0,\n        \"InputFilePath\": \"\",\n        \"InputFolderPath\": \"\",\n        \"OutputFilename\": \"%filename%-mangajanai\",\n        \"OutputFolderPath\": \"\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": false,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": true,\n        \"UpscaleScaleFactor\": 4,\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": false,\n        \"DisplayDevice\": null,\n        \"DisplayDeviceWidth\": 0,\n        \"DisplayDeviceHeight\": 0,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V1_ESRGAN_135k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Custom Workflow 6\",\n        \"WorkflowIndex\": 6,\n        \"SelectedTabIndex\": 0,\n        \"InputFilePath\": \"\",\n        \"InputFolderPath\": \"\",\n        \"OutputFilename\": \"%filename%-mangajanai\",\n        \"OutputFolderPath\": \"\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": false,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": true,\n        \"UpscaleScaleFactor\": 4,\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": false,\n        \"DisplayDevice\": null,\n        \"DisplayDeviceWidth\": 0,\n        \"DisplayDeviceHeight\": 0,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V1_ESRGAN_135k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Custom Workflow 7\",\n        \"WorkflowIndex\": 7,\n        \"SelectedTabIndex\": 1,\n        \"InputFilePath\": \"\",\n        \"InputFolderPath\": \"\",\n        \"OutputFilename\": \"%filename%-mangajanai\",\n        \"OutputFolderPath\": \"\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": true,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": false,\n        \"UpscaleScaleFactor\": 4,\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": true,\n        \"DisplayDevice\": \"ONYX Boox Max Lumi 2 (2021)\",\n        \"DisplayDeviceWidth\": 1650,\n        \"DisplayDeviceHeight\": 2200,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V1_ESRGAN_135k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Custom Workflow 8\",\n        \"WorkflowIndex\": 8,\n        \"SelectedTabIndex\": 0,\n        \"InputFilePath\": \"\",\n        \"InputFolderPath\": \"\",\n        \"OutputFilename\": \"%filename%-mangajanai\",\n        \"OutputFolderPath\": \"\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": true,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": false,\n        \"UpscaleScaleFactor\": 4,\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": true,\n        \"DisplayDevice\": \"Samsung Galaxy Tab S9 Ultra (2023)\",\n        \"DisplayDeviceWidth\": 1848,\n        \"DisplayDeviceHeight\": 2950,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V1_ESRGAN_135k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Custom Workflow 9\",\n        \"WorkflowIndex\": 9,\n        \"SelectedTabIndex\": 0,\n        \"InputFilePath\": \"\",\n        \"InputFolderPath\": \"\",\n        \"OutputFilename\": \"%filename%-mangajanai\",\n        \"OutputFolderPath\": \"\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": true,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": true,\n        \"UpscaleScaleFactor\": 4,\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": false,\n        \"DisplayDevice\": null,\n        \"DisplayDeviceWidth\": 0,\n        \"DisplayDeviceHeight\": 0,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V1_ESRGAN_135k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      },\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Custom Workflow 10\",\n        \"WorkflowIndex\": 10,\n        \"SelectedTabIndex\": 0,\n        \"InputFilePath\": \"\",\n        \"InputFolderPath\": \"\",\n        \"OutputFilename\": \"%filename%-mangajanai\",\n        \"OutputFolderPath\": \"\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": true,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": true,\n        \"UpscaleScaleFactor\": 4,\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": false,\n        \"DisplayDevice\": null,\n        \"DisplayDeviceWidth\": 0,\n        \"DisplayDeviceHeight\": 0,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V1_ESRGAN_135k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      }\n    ]\n  },\n  \"SelectedWorkflowIndex\": 0\n}"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/resources/default_cli_configuration.json",
    "content": "{\n  \"$type\": \"MangaJaNaiConverterGui.ViewModels.MainWindowViewModel, MangaJaNaiConverterGui\",\n  \"AutoUpdateEnabled\": true,\n  \"SelectedDeviceIndex\": \">>CONTROLLED_BY_CLI<<\",\n  \"UseCpu\": false,\n  \"UseFp16\": true,\n  \"ModelsDirectory\": \">>CONTROLLED_BY_CLI<<\",\n  \"Workflows\": {\n    \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui]], Avalonia.Base\",\n    \"$values\": [\n      {\n        \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleWorkflow, MangaJaNaiConverterGui\",\n        \"WorkflowName\": \"Upscale Manga (Default)\",\n        \"WorkflowIndex\": 0,\n        \"SelectedTabIndex\": \">>CONTROLLED_BY_CL<<\",\n        \"GrayscaleDetectionThreshold\": 12,\n        \"InputFilePath\": \">>CONTROLLED_BY_CL<<\",\n        \"InputFolderPath\": \">>CONTROLLED_BY_CL<<\",\n        \"OutputFilename\": \"%filename%\",\n        \"OutputFolderPath\": \">>CONTROLLED_BY_CL<<\",\n        \"OverwriteExistingFiles\": false,\n        \"UpscaleImages\": true,\n        \"UpscaleArchives\": true,\n        \"ResizeHeightAfterUpscale\": 2160,\n        \"ResizeWidthAfterUpscale\": 3840,\n        \"WebpSelected\": true,\n        \"AvifSelected\": false,\n        \"PngSelected\": false,\n        \"JpegSelected\": false,\n        \"UseLosslessCompression\": false,\n        \"LossyCompressionQuality\": 80,\n        \"ShowLossySettings\": true,\n        \"ModeScaleSelected\": true,\n        \"UpscaleScaleFactor\": \">>CONTROLLED_BY_CL<<\",\n        \"ModeWidthSelected\": false,\n        \"ModeHeightSelected\": false,\n        \"ModeFitToDisplaySelected\": false,\n        \"DisplayDevice\": \"Kobo Elipsa 2E (2023)\",\n        \"DisplayDeviceWidth\": 1404,\n        \"DisplayDeviceHeight\": 1872,\n        \"DisplayPortraitSelected\": true,\n        \"ShowAdvancedSettings\": false,\n        \"Chains\": {\n          \"$type\": \"Avalonia.Collections.AvaloniaList`1[[MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui]], Avalonia.Base\",\n          \"$values\": [\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"1\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": false,\n              \"IsColor\": true,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_IllustrationJaNai_V1_ESRGAN_135k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": false,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"2\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"3\",\n              \"MinResolution\": \"0x0\",\n              \"MaxResolution\": \"0x1250\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1200p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"4\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"5\",\n              \"MinResolution\": \"0x1251\",\n              \"MaxResolution\": \"0x1350\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1300p_V1_ESRGAN_75k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"6\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1400p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"7\",\n              \"MinResolution\": \"0x1351\",\n              \"MaxResolution\": \"0x1450\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1400p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"8\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1500p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"9\",\n              \"MinResolution\": \"0x1451\",\n              \"MaxResolution\": \"0x1550\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1500p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"10\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1600p_V1_ESRGAN_90k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"11\",\n              \"MinResolution\": \"0x1551\",\n              \"MaxResolution\": \"0x1760\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1600p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"12\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_1920p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"13\",\n              \"MinResolution\": \"0x1761\",\n              \"MaxResolution\": \"0x1984\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_1920p_V1_ESRGAN_105k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"14\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 0,\n              \"MaxScaleFactor\": 2,\n              \"ModelFilePath\": \"2x_MangaJaNai_2048p_V1_ESRGAN_95k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            },\n            {\n              \"$type\": \"MangaJaNaiConverterGui.ViewModels.UpscaleChain, MangaJaNaiConverterGui\",\n              \"ChainNumber\": \"15\",\n              \"MinResolution\": \"0x1985\",\n              \"MaxResolution\": \"0x0\",\n              \"IsGrayscale\": true,\n              \"IsColor\": false,\n              \"MinScaleFactor\": 2,\n              \"MaxScaleFactor\": 0,\n              \"ModelFilePath\": \"4x_MangaJaNai_2048p_V1_ESRGAN_70k.pth\",\n              \"ModelTileSize\": \"Auto (Estimate)\",\n              \"AutoAdjustLevels\": true,\n              \"ResizeHeightBeforeUpscale\": 0,\n              \"ResizeWidthBeforeUpscale\": 0,\n              \"ResizeFactorBeforeUpscale\": 100.0\n            }\n          ]\n        }\n      }\n    ]\n  },\n  \"SelectedWorkflowIndex\": 0\n}\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/.pre-commit-config.yaml",
    "content": "- repo: https://github.com/astral-sh/ruff-pre-commit\n  # Ruff version.\n  rev: v0.5.1\n  hooks:\n    # Run the linter.\n    - id: ruff\n      args: [ --fix ]\n    # Run the formatter.\n    - id: ruff-format\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/README.md",
    "content": "# Info\nMangaJaNaiConverterGui is a convenient GUI windows tool, but in the backend it operates by running some python scripts in CLI.\n\nThis README is for those interested in running only the CLI on a linux systems or WSL.\n\n# Setup\n## Setup virtual environment and download dependencies\nNavigate 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:\n```commandline\npython -m venv venv_mangajanai\nsource venv_mangajanai/bin/activate\npip install MangaJaNaiConverterGui/backend/src/\n```\n\nBy 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/). \n\n## Download models\nScripts 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.\n\nYou can extract them from the release .exe file of the version you wish to use.\n\n\n# Usage\nYou can use this tool in two ways. As a simple CLI or as advanced json-controlled utility.\n\n## Simple CLI\n\n## Execution\n\nScript should be run from `MangaJaNaiConverterGui/backend/src/` directory. Example usage:\n```bash\n# Show detailed help\npython run_upscale.py -h\n\n# Upscale single file with factor 4\npython run_upscale.py -f \"/my/dir/myFile.jpg\" -u 4\n\n# Upscale whole directory into a custom output directory\npython run_upscale.py -d \"/my/input/dir/\" -o \"/my/output/dir/\"\n```\n\n## Advanced Utility\n\nScript uses a settings file generated by the GUI to control its behavior. You need to build it manually to use the CLI. \nThe default settings can be found in [MangaJaNaiConverterGui/appstate2.json](MangaJaNaiConverterGui/appstate2.json). Just copy it and modify only what you need. \nIt's a long file, but you only need to worry about a few root keys and first workflow `Upscale Manga (Default)`\n\n### Important Root Keys\n- **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)  \n- **UseCPU** - true/false\n- **UseFp16** - true/false\n\n### Important Workflows Keys\n- **SelectedTabIndex** - Choose if you want to upscale a single file or a whole folder \n  - 0 - file\n  - 1 - folder\n- **InputFilePath** - absolute file path. Used when **SelectedTabIndex** = 0\n- **InputFolderPath** - absolute folder path. Used when **SelectedTabIndex** = 1\n- **OutputFilename** - Name of generated filenames. Keep `%filename%` to leave the same name\n- **OutputFolderPath** - absolute folder path.\n- **OverwriteExistingFiles** - true/false\n- **UpscaleImages** - true/false - needs to be true for upscale to work\n- **WebpSelected/AvifSelected/PngSelected/JpegSelected** - true/false. Only one should be true. Selects output filetype.\n- **UpscaleScaleFactor** - 1/2/3/4 - How much you want to upscale which controls which models will be used\n- There are a bunch of other options if you want to dig deeper into it, but setting these should be enough for basic usage\n\n### Execution\nIn `MangaJaNaiConverterGui/backend/src/` directory run:\n```bash\npython run_upscale.py --settings \"/path/to/your/file/appstate2.json\"\n```\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/__init__.py",
    "content": ""
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/accelerator_detection.py",
    "content": "\"\"\"\nComprehensive accelerator detection for PyTorch backend.\nSupports all available PyTorch accelerators in PyTorch 2.7+\n\"\"\"\nfrom __future__ import annotations\n\nimport warnings\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom functools import cached_property\nfrom typing import Any, Optional, Sequence, override\n\nimport torch\nfrom sanic.log import logger\n\n\nclass AcceleratorType(Enum):\n    \"\"\"Supported accelerator types\"\"\"\n    CPU = \"cpu\"\n    CUDA = \"cuda\"\n    ROCM = \"rocm\"  # AMD GPUs using ROCm\n    MPS = \"mps\"    # Apple Metal Performance Shaders\n    XPU = \"xpu\"    # Intel GPUs\n\n\n@dataclass(frozen=True)\nclass AcceleratorDevice:\n    \"\"\"Information about an accelerator device\"\"\"\n    type: AcceleratorType\n    index: int\n    name: str\n    memory_total: Optional[int] = None\n    memory_free: Optional[int] = None\n    supports_fp16: bool = False\n    supports_bf16: bool = False\n    device_string: str = \"\"\n\n    def __post_init__(self):\n        if not self.device_string:\n            if self.type == AcceleratorType.CPU:\n                object.__setattr__(self, \"device_string\", \"cpu\")\n            else:\n                object.__setattr__(self, \"device_string\", f\"{self.type.value}:{self.index}\")\n\n    @property\n    def torch_device(self) -> torch.device:\n        \"\"\"Get the corresponding torch.device\"\"\"\n        return torch.device(self.device_string)\n\n    @override\n    def __eq__(self, other: object) -> bool:\n        if isinstance(other, AcceleratorDevice):\n            return self.device_string == other.device_string\n        return NotImplemented\n\n    @override\n    def __hash__(self) -> int:\n        # only needed if you ever put Device in sets / dict keys\n        return hash(self.device_string)\n\n\nclass AcceleratorDetector:\n    \"\"\"Detects and manages available accelerators\"\"\"\n\n    def __init__(self):\n        self._devices: Optional[list[AcceleratorDevice]] = None\n\n    @cached_property\n    def available_devices(self) -> list[AcceleratorDevice]:\n        \"\"\"Get all available accelerator devices\"\"\"\n        if self._devices is None:\n            self._devices = self._detect_all_devices()\n        return self._devices\n\n    def _detect_all_devices(self) -> list[AcceleratorDevice]:\n        \"\"\"Detect all available accelerator devices\"\"\"\n        devices = []\n\n        # Always add CPU\n        devices.append(AcceleratorDevice(\n            type=AcceleratorType.CPU,\n            index=0,\n            name=\"CPU\",\n            supports_fp16=False,  # PyTorch 2.7+ doesn't support FP16 on CPU\n            supports_bf16=True,   # CPU supports bfloat16\n        ))\n\n        # Detect CUDA devices\n        devices.extend(self._detect_cuda_devices())\n\n        # Detect ROCm devices (ROCm uses CUDA API)\n        devices.extend(self._detect_rocm_devices())\n\n        # Detect Apple MPS\n        devices.extend(self._detect_mps_devices())\n\n        # Detect Intel XPU\n        devices.extend(self._detect_xpu_devices())\n\n        return devices\n\n    def _detect_cuda_devices(self) -> list[AcceleratorDevice]:\n        \"\"\"Detect NVIDIA CUDA devices\"\"\"\n        devices = []\n        try:\n            if torch.cuda.is_available():\n                for i in range(torch.cuda.device_count()):\n                    try:\n                        device_props = torch.cuda.get_device_properties(i)\n                        memory_info = torch.cuda.mem_get_info(i)\n                        \n                        # Determine FP16 support based on architecture\n                        supports_fp16 = self._cuda_supports_fp16(device_props, i)\n                        supports_bf16 = self._cuda_supports_bf16(device_props)\n\n                        devices.append(AcceleratorDevice(\n                            type=AcceleratorType.CUDA,\n                            index=i,\n                            name=device_props.name,\n                            memory_total=device_props.total_memory,\n                            memory_free=memory_info[0],\n                            supports_fp16=supports_fp16,\n                            supports_bf16=supports_bf16,\n                        ))\n                        logger.info(f\"Detected CUDA device {i}: {device_props.name}\")\n                    except Exception as e:\n                        logger.warning(f\"Failed to get info for CUDA device {i}: {e}\")\n        except Exception as e:\n            logger.info(f\"CUDA not available: {e}\")\n        \n        return devices\n\n    def _detect_rocm_devices(self) -> list[AcceleratorDevice]:\n        \"\"\"Detect AMD ROCm devices\"\"\"\n        devices = []\n        try:\n            # ROCm devices appear as CUDA devices due to HIP/CUDA compatibility\n            # Check if we're actually running on ROCm\n            if hasattr(torch.version, 'hip') and torch.version.hip is not None:\n                # This is ROCm, re-categorize CUDA devices as ROCm\n                if torch.cuda.is_available():\n                    for i in range(torch.cuda.device_count()):\n                        try:\n                            device_props = torch.cuda.get_device_properties(i)\n                            memory_info = torch.cuda.mem_get_info(i)\n                            \n                            devices.append(AcceleratorDevice(\n                                type=AcceleratorType.ROCM,\n                                index=i,\n                                name=device_props.name,\n                                memory_total=device_props.total_memory,\n                                memory_free=memory_info[0],\n                                supports_fp16=True,  # Most modern AMD GPUs support FP16\n                                supports_bf16=True,  # Modern AMD GPUs support bfloat16\n                                device_string=f\"cuda:{i}\",  # ROCm uses cuda device string\n                            ))\n                            logger.info(f\"Detected ROCm device {i}: {device_props.name}\")\n                        except Exception as e:\n                            logger.warning(f\"Failed to get info for ROCm device {i}: {e}\")\n        except Exception as e:\n            logger.debug(f\"ROCm detection failed: {e}\")\n        \n        return devices\n\n    def _detect_mps_devices(self) -> list[AcceleratorDevice]:\n        \"\"\"Detect Apple Metal Performance Shaders devices\"\"\"\n        devices = []\n        try:\n            if (hasattr(torch, 'backends') and \n                hasattr(torch.backends, 'mps') and \n                torch.backends.mps.is_built() and \n                torch.backends.mps.is_available()):\n                \n                devices.append(AcceleratorDevice(\n                    type=AcceleratorType.MPS,\n                    index=0,\n                    name=\"Apple Metal GPU\",\n                    supports_fp16=True,   # MPS supports FP16\n                    supports_bf16=False,  # MPS doesn't support bfloat16 yet\n                    device_string=\"mps\",\n                ))\n                logger.info(\"Detected Apple MPS device\")\n        except Exception as e:\n            logger.debug(f\"MPS detection failed: {e}\")\n        \n        return devices\n\n    def _detect_xpu_devices(self) -> list[AcceleratorDevice]:\n        \"\"\"Detect Intel XPU devices\"\"\"\n        devices = []\n        try:\n            if hasattr(torch, 'xpu') and torch.xpu.is_available():\n                device_count = torch.xpu.device_count()\n                for i in range(device_count):\n                    try:\n                        device_name = torch.xpu.get_device_name(i)\n                        # Try to get memory info if available\n                        memory_info = None\n                        try:\n                            memory_info = torch.xpu.mem_get_info(i)\n                        except Exception:\n                            pass\n\n                        devices.append(AcceleratorDevice(\n                            type=AcceleratorType.XPU,\n                            index=i,\n                            name=device_name,\n                            memory_total=memory_info[1] if memory_info else None,\n                            memory_free=memory_info[0] if memory_info else None,\n                            supports_fp16=True,   # Intel XPU supports FP16\n                            supports_bf16=True,   # Intel XPU supports bfloat16\n                        ))\n                        logger.info(f\"Detected Intel XPU device {i}: {device_name}\")\n                    except Exception as e:\n                        logger.warning(f\"Failed to get info for XPU device {i}: {e}\")\n        except Exception as e:\n            logger.debug(f\"XPU detection failed: {e}\")\n        \n        return devices\n\n    def _cuda_supports_fp16(self, device_props: Any, device_index: int) -> bool:\n        \"\"\"Check if CUDA device supports FP16\"\"\"\n        try:\n            # Check compute capability\n            major, minor = device_props.major, device_props.minor\n            compute_capability = major * 10 + minor\n            \n            # FP16 is supported on:\n            # - Volta (7.0+) and newer architectures\n            # - Some Turing cards (RTX series, not GTX 16xx)\n            if compute_capability >= 70:  # Volta and newer\n                return True\n            elif compute_capability >= 75:  # Turing\n                # For Turing, check if it's RTX (supports FP16) or GTX 16xx (doesn't)\n                return \"RTX\" in device_props.name\n            else:\n                return False\n        except Exception:\n            # Fallback: try to actually use FP16\n            try:\n                test_tensor = torch.tensor([1.0], dtype=torch.float16, device=f\"cuda:{device_index}\")\n                return True\n            except Exception:\n                return False\n\n    def _cuda_supports_bf16(self, device_props: Any) -> bool:\n        \"\"\"Check if CUDA device supports bfloat16\"\"\"\n        try:\n            # bfloat16 is supported on Ampere (8.0+) and newer\n            major, minor = device_props.major, device_props.minor\n            compute_capability = major * 10 + minor\n            return compute_capability >= 80\n        except Exception:\n            return False\n\n    def get_devices_by_type(self, accelerator_type: AcceleratorType) -> list[AcceleratorDevice]:\n        \"\"\"Get all devices of a specific type\"\"\"\n        return [device for device in self.available_devices if device.type == accelerator_type]\n\n    def get_best_device(self, prefer_gpu: bool = True) -> AcceleratorDevice:\n        \"\"\"Get the best available device\"\"\"\n        if not prefer_gpu:\n            return self.get_cpu_device()\n\n        # Priority order: CUDA > XPU > MPS > ROCm > CPU\n        for device_type in [AcceleratorType.CUDA, AcceleratorType.XPU, AcceleratorType.MPS, \n                           AcceleratorType.ROCM]:\n            devices = self.get_devices_by_type(device_type)\n            if devices:\n                return devices[0]  # Return the first device of this type\n\n        return self.get_cpu_device()\n\n    def get_cpu_device(self) -> AcceleratorDevice:\n        \"\"\"Get the CPU device\"\"\"\n        cpu_devices = self.get_devices_by_type(AcceleratorType.CPU)\n        return cpu_devices[0] if cpu_devices else AcceleratorDevice(\n            type=AcceleratorType.CPU, index=0, name=\"CPU\"\n        )\n\n    def get_device_by_index(self, device_type: AcceleratorType, index: int) -> Optional[AcceleratorDevice]:\n        \"\"\"Get a specific device by type and index\"\"\"\n        devices = self.get_devices_by_type(device_type)\n        for device in devices:\n            if device.index == index:\n                return device\n        return None\n\n\ndef get_autocast_device_type(device: torch.device) -> str:\n    \"\"\"Get the correct device type string for torch.autocast\"\"\"\n    device_type = device.type\n    \n    # Map device types to autocast device types\n    if device_type in [\"cuda\", \"rocm\"]:  # ROCm uses cuda device type\n        return \"cuda\"\n    elif device_type == \"xpu\":\n        return \"xpu\"\n    elif device_type == \"mps\":\n        # MPS doesn't support autocast yet, use cpu\n        return \"cpu\"\n    else:\n        return \"cpu\"\n\n\ndef is_device_type_supported_for_autocast(device: torch.device) -> bool:\n    \"\"\"Check if device type supports autocast\"\"\"\n    device_type = device.type\n    return device_type in [\"cuda\", \"xpu\"]\n\n\n# Global detector instance\n_detector: Optional[AcceleratorDetector] = None\n\n\ndef get_accelerator_detector() -> AcceleratorDetector:\n    \"\"\"Get the global accelerator detector instance\"\"\"\n    global _detector\n    if _detector is None:\n        _detector = AcceleratorDetector()\n    return _detector\n\n\ndef get_available_devices() -> list[AcceleratorDevice]:\n    \"\"\"Convenience function to get all available devices\"\"\"\n    return get_accelerator_detector().available_devices\n\n\ndef get_best_device(prefer_gpu: bool = True) -> AcceleratorDevice:\n    \"\"\"Convenience function to get the best available device\"\"\"\n    return get_accelerator_detector().get_best_device(prefer_gpu)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/__init__.py",
    "content": "from .api import *\nfrom .group import *\nfrom .input import *\nfrom .iter import *\nfrom .lazy import *\nfrom .node_context import *\nfrom .node_data import *\nfrom .output import *\nfrom .settings import *\nfrom .types import *\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/api.py",
    "content": "from __future__ import annotations\n\nimport importlib\nimport os\nfrom collections.abc import Awaitable, Callable, Iterable\nfrom dataclasses import asdict, dataclass, field\nfrom typing import (\n    Any,\n    TypeVar,\n)\n\nfrom sanic.log import logger\n\nfrom .group import Group, GroupId, NestedGroup, NestedIdGroup\nfrom .input import BaseInput\nfrom .node_check import (\n    NAME_CHECK_LEVEL,\n    TYPE_CHECK_LEVEL,\n    CheckFailedError,\n    CheckLevel,\n    check_naming_conventions,\n    check_schema_types,\n)\nfrom .node_data import (\n    IteratorInputInfo,\n    IteratorOutputInfo,\n    KeyInfo,\n    NodeData,\n    SpecialSuggestion,\n)\nfrom .output import BaseOutput\nfrom .settings import Setting\nfrom .types import FeatureId, InputId, NodeId, NodeKind, OutputId, RunFn\n\nKB = 1024**1\nMB = 1024**2\nGB = 1024**3\n\n\ndef _process_inputs(base_inputs: Iterable[BaseInput | NestedGroup]):\n    inputs: list[BaseInput] = []\n    groups: list[NestedIdGroup] = []\n\n    def add_inputs(\n        current: Iterable[BaseInput | NestedGroup],\n    ) -> list[InputId | NestedIdGroup]:\n        layout: list[InputId | NestedIdGroup] = []\n\n        for x in current:\n            if isinstance(x, Group):\n                if x.info.id == -1:\n                    x.info.id = GroupId(len(groups))\n                g: NestedIdGroup = Group(x.info, [])\n                groups.append(g)\n                layout.append(g)\n                g.items.extend(add_inputs(x.items))  # type: ignore\n            else:\n                if x.id == -1:\n                    x.id = InputId(len(inputs))\n                layout.append(x.id)\n                inputs.append(x)\n\n        return layout\n\n    return inputs, add_inputs(base_inputs)\n\n\ndef _process_outputs(base_outputs: Iterable[BaseOutput]):\n    outputs: list[BaseOutput] = []\n    for i, output_value in enumerate(base_outputs):\n        if output_value.id == -1:\n            output_value.id = OutputId(i)\n        outputs.append(output_value)\n    return outputs\n\n\nT = TypeVar(\"T\", bound=RunFn)\nS = TypeVar(\"S\")\n\n\n@dataclass\nclass NodeGroup:\n    category: Category\n    id: str\n    name: str\n    order: list[str | NodeId] = field(default_factory=list)\n    nodes: list[NodeData] = field(default_factory=list)\n\n    def add_node(self, node: NodeData) -> None:\n        logger.debug(f\"Added {node.schema_id}\")\n        self.nodes.append(node)\n\n    def to_dict(self):\n        return {\n            \"id\": self.id,\n            \"category\": self.category.id,\n            \"name\": self.name,\n            \"order\": self.order,\n        }\n\n    def register(\n        self,\n        schema_id: str,\n        *,\n        name: str,\n        description: str | list[str],\n        inputs: list[BaseInput | NestedGroup],\n        outputs: list[BaseOutput],\n        icon: str = \"BsQuestionCircleFill\",\n        kind: NodeKind = \"regularNode\",\n        side_effects: bool = False,\n        deprecated: bool = False,\n        decorators: list[Callable] | None = None,\n        see_also: list[str] | str | None = None,\n        features: list[FeatureId] | FeatureId | None = None,\n        limited_to_8bpc: bool | str = False,\n        iterator_inputs: list[IteratorInputInfo] | IteratorInputInfo | None = None,\n        iterator_outputs: list[IteratorOutputInfo] | IteratorOutputInfo | None = None,\n        node_context: bool = False,\n        key_info: KeyInfo | None = None,\n        suggestions: list[SpecialSuggestion] | None = None,\n    ):\n        if not isinstance(description, str):\n            description = \"\\n\\n\".join(description)\n\n        if limited_to_8bpc:\n            description += \"\\n\\n#### Limited color depth\\n\\n\"\n            if isinstance(limited_to_8bpc, str):\n                description += f\" {limited_to_8bpc}\"\n            else:\n                description += (\n                    \"This node will internally convert input images to 8 bits/channel.\"\n                    \" This is generally only a problem if you intend to save the output with 16 bits/channel or higher.\"\n                )\n\n        def to_list(x: list[S] | S | None) -> list[S]:\n            if x is None:\n                return []\n            if isinstance(x, list):\n                return x\n            return [x]\n\n        see_also = to_list(see_also)\n        features = to_list(features)\n\n        iterator_inputs = to_list(iterator_inputs)\n        iterator_outputs = to_list(iterator_outputs)\n\n        if kind == \"generator\":  # Generator\n            assert len(iterator_inputs) == 0 and len(iterator_outputs) == 1\n        elif kind == \"collector\":\n            assert len(iterator_inputs) == 1 and len(iterator_outputs) == 0\n        else:\n            assert len(iterator_inputs) == 0 and len(iterator_outputs) == 0\n\n        def run_check(level: CheckLevel, run: Callable[[bool], None]) -> None:\n            if level == CheckLevel.NONE:\n                return\n\n            try:\n                run(level == CheckLevel.FIX)\n            except CheckFailedError as e:\n                full_error_message = f\"Error in {schema_id}: {e}\"\n                if level == CheckLevel.ERROR:\n                    raise CheckFailedError(full_error_message)  # noqa: B904\n                logger.warning(full_error_message)\n\n        def inner_wrapper(wrapped_func: T) -> T:\n            p_inputs, group_layout = _process_inputs(inputs)\n            p_outputs = _process_outputs(outputs)\n\n            original_fn = wrapped_func\n\n            if decorators is not None:\n                for decorator in decorators:\n                    wrapped_func = decorator(wrapped_func)\n\n            node = NodeData(\n                schema_id=schema_id,\n                name=name,\n                description=description,\n                see_also=see_also,\n                icon=icon,\n                kind=kind,\n                inputs=p_inputs,\n                group_layout=group_layout,\n                outputs=p_outputs,\n                iterable_inputs=iterator_inputs,\n                iterable_outputs=iterator_outputs,\n                key_info=key_info,\n                suggestions=suggestions or [],\n                side_effects=side_effects,\n                deprecated=deprecated,\n                node_context=node_context,\n                features=features,\n                run=wrapped_func,\n            )\n\n            run_check(\n                TYPE_CHECK_LEVEL,\n                lambda _: check_schema_types(original_fn, node),\n            )\n            run_check(\n                NAME_CHECK_LEVEL,\n                lambda fix: check_naming_conventions(original_fn, name, fix),\n            )\n\n            self.add_node(node)\n            return wrapped_func\n\n        return inner_wrapper\n\n\n@dataclass\nclass Category:\n    package: Package\n    id: str\n    name: str\n    description: str\n    icon: str = \"BsQuestionCircleFill\"\n    color: str = \"#777777\"\n    install_hint: str | None = None\n    node_groups: list[NodeGroup] = field(default_factory=list)\n\n    def add_node_group(self, name: str) -> NodeGroup:\n        result = NodeGroup(\n            category=self,\n            id=self.id + \"/\" + name.lower(),\n            name=name,\n        )\n        self.node_groups.append(result)\n        return result\n\n    def to_dict(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"description\": self.description,\n            \"icon\": self.icon,\n            \"color\": self.color,\n            \"installHint\": self.install_hint,\n            \"groups\": [g.to_dict() for g in self.node_groups],\n        }\n\n\n@dataclass\nclass Dependency:\n    display_name: str\n    pypi_name: str\n    version: str\n    size_estimate: int | float\n    auto_update: bool = True\n    extra_index_url: str | None = None\n\n    import_name: str | None = None\n\n    def to_dict(self):\n        return {\n            \"displayName\": self.display_name,\n            \"pypiName\": self.pypi_name,\n            \"version\": self.version,\n            \"sizeEstimate\": int(self.size_estimate),\n            \"autoUpdate\": self.auto_update,\n            \"findLink\": self.extra_index_url,\n        }\n\n    @staticmethod\n    def from_dict(data: dict[str, Any]) -> Dependency:\n        return Dependency(\n            display_name=data[\"displayName\"],\n            pypi_name=data[\"pypiName\"],\n            version=data[\"version\"],\n            size_estimate=data[\"sizeEstimate\"],\n            auto_update=data[\"autoUpdate\"],\n            extra_index_url=data[\"findLink\"],\n        )\n\n\n@dataclass\nclass Feature:\n    id: str\n    name: str\n    description: str\n    behavior: FeatureBehavior | None = None\n\n    def add_behavior(self, check: Callable[[], Awaitable[FeatureState]]) -> FeatureId:\n        if self.behavior is not None:\n            raise ValueError(\"Behavior already set\")\n\n        self.behavior = FeatureBehavior(check=check)\n        return FeatureId(self.id)\n\n    def to_dict(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"description\": self.description,\n        }\n\n    @staticmethod\n    def from_dict(data: dict[str, Any]) -> Feature:\n        return Feature(\n            id=data[\"id\"],\n            name=data[\"name\"],\n            description=data[\"description\"],\n        )\n\n\n@dataclass\nclass FeatureBehavior:\n    check: Callable[[], Awaitable[FeatureState]]\n\n\n@dataclass(frozen=True)\nclass FeatureState:\n    is_enabled: bool\n    details: str | None = None\n\n    @staticmethod\n    def enabled(details: str | None = None) -> FeatureState:\n        return FeatureState(is_enabled=True, details=details)\n\n    @staticmethod\n    def disabled(details: str | None = None) -> FeatureState:\n        return FeatureState(is_enabled=False, details=details)\n\n\n@dataclass\nclass Package:\n    where: str\n    id: str\n    name: str\n    description: str\n    icon: str\n    color: str\n    dependencies: list[Dependency] = field(default_factory=list)\n    categories: list[Category] = field(default_factory=list)\n    features: list[Feature] = field(default_factory=list)\n    settings: list[Setting] = field(default_factory=list)\n\n    def add_category(\n        self,\n        name: str,\n        description: str,\n        icon: str,\n        color: str,\n        install_hint: str | None = None,\n    ) -> Category:\n        result = Category(\n            package=self,\n            id=name.lower(),\n            name=name,\n            description=description,\n            icon=icon,\n            color=color,\n            install_hint=install_hint,\n        )\n        self.categories.append(result)\n        return result\n\n    def add_dependency(self, dependency: Dependency) -> None:\n        self.dependencies.append(dependency)\n\n    def add_setting(self, setting: Setting) -> None:\n        self.settings.append(setting)\n\n    def add_feature(\n        self,\n        id: str,  # pylint: disable=redefined-builtin\n        name: str,\n        description: str,\n    ) -> Feature:\n        if any(f.id == id for f in self.features):\n            raise ValueError(f\"Duplicate feature id: {id}\")\n\n        feature = Feature(id=id, name=name, description=description)\n        self.features.append(feature)\n        return feature\n\n    def to_dict(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"description\": self.description,\n            \"icon\": self.icon,\n            \"color\": self.color,\n            \"dependencies\": [d.to_dict() for d in self.dependencies],\n            \"features\": [f.to_dict() for f in self.features],\n            \"settings\": [asdict(x) for x in self.settings],\n        }\n\n    @staticmethod\n    def from_dict(data: dict[str, Any]) -> Package:\n        \"\"\"This is really only for dependency purposes, so it's not feature-complete\"\"\"\n        return Package(\n            where=data.get(\"where\", \"unknown\"),\n            id=data[\"id\"],\n            name=data[\"name\"],\n            description=data[\"description\"],\n            icon=data[\"icon\"],\n            color=data[\"color\"],\n            dependencies=[Dependency.from_dict(d) for d in data[\"dependencies\"]],\n            categories=[],\n            features=[Feature.from_dict(f) for f in data[\"features\"]],\n            settings=[],\n        )\n\n\ndef _iter_py_files(directory: str):\n    for root, _, files in os.walk(directory):\n        for file in files:\n            if file.endswith(\".py\"):\n                yield os.path.join(root, file)\n\n\n@dataclass\nclass LoadErrorInfo:\n    module: str\n    file: str\n    error: Exception\n\n\nclass PackageRegistry:\n    def __init__(self) -> None:\n        self.packages: dict[str, Package] = {}\n        self.categories: list[Category] = []\n        self.nodes: dict[str, tuple[NodeData, NodeGroup]] = {}\n\n    def get_node(self, schema_id: str) -> NodeData:\n        return self.nodes[schema_id][0]\n\n    def get_package(self, schema_id: str) -> Package:\n        return self.nodes[schema_id][1].category.package\n\n    def add(self, package: Package) -> Package:\n        # assert package.where not in self.packages\n        self.packages[package.where] = package\n        return package\n\n    def load_nodes(self, current_file: str) -> list[LoadErrorInfo]:\n        load_error: list[LoadErrorInfo] = []\n        failed_checks: list[CheckFailedError] = []\n\n        for package in list(self.packages.values()):\n            for file_path in _iter_py_files(os.path.dirname(package.where)):\n                _, name = os.path.split(file_path)\n\n                if not name.startswith(\"_\"):\n                    module = os.path.relpath(file_path, os.path.dirname(current_file))\n                    module = module.replace(\"/\", \".\").replace(\"\\\\\", \".\")[: -len(\".py\")]\n                    try:\n                        importlib.import_module(module, package=None)\n                    except CheckFailedError as e:\n                        logger.error(e)\n                        failed_checks.append(e)\n                    except Exception as e:\n                        load_error.append(LoadErrorInfo(module, file_path, e))\n\n        if len(failed_checks) > 0:\n            raise RuntimeError(f\"Checks failed in {len(failed_checks)} node(s)\")\n\n        self._refresh_nodes()\n\n        return load_error\n\n    def _refresh_nodes(self) -> None:\n        self.nodes = {}\n        self.categories = []\n\n        for package in self.packages.values():\n            self.categories.extend(package.categories)\n            for category in package.categories:\n                for sub in category.node_groups:\n                    for node in sub.nodes:\n                        if node.schema_id in self.nodes:\n                            # print warning\n                            pass\n                        self.nodes[node.schema_id] = node, sub\n\n\nregistry = PackageRegistry()\n\n\ndef add_package(\n    where: str,\n    id: str,  # pylint: disable=redefined-builtin\n    name: str,\n    description: str,\n    dependencies: list[Dependency] | None = None,\n    icon: str = \"BsQuestionCircleFill\",\n    color: str = \"#777777\",\n) -> Package:\n    return registry.add(\n        Package(\n            where=where,\n            id=id,\n            name=name,\n            description=description,\n            icon=icon,\n            color=color,\n            dependencies=dependencies or [],\n        )\n    )\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/group.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, Generic, NewType, TypeVar, Union\n\nfrom .input import BaseInput\nfrom .types import InputId\n\nT = TypeVar(\"T\")\n\n\nGroupId = NewType(\"GroupId\", int)\n\n\nclass GroupInfo:\n    def __init__(\n        self,\n        group_id: GroupId,\n        kind: str,\n        options: dict[str, Any] | None = None,\n    ) -> None:\n        self.id: GroupId = group_id\n        self.kind: str = kind\n        self.options: dict[str, Any] = {} if options is None else options\n\n\nclass Group(Generic[T]):\n    def __init__(self, info: GroupInfo, items: list[T]) -> None:\n        self.info: GroupInfo = info\n        self.items: list[T] = items\n\n    def to_dict(self):\n        return {\n            \"id\": self.info.id,\n            \"kind\": self.info.kind,\n            \"options\": self.info.options,\n            \"items\": [i.to_dict() if isinstance(i, Group) else i for i in self.items],\n        }\n\n\nNestedGroup = Group[Union[BaseInput, \"NestedGroup\"]]\nNestedIdGroup = Group[Union[InputId, \"NestedIdGroup\"]]\n\n\n# pylint: disable-next=redefined-builtin\ndef group(kind: str, options: dict[str, Any] | None = None, id: int = -1):\n    info = GroupInfo(GroupId(id), kind, options)\n\n    def ret(*items: BaseInput | NestedGroup) -> NestedGroup:\n        return Group(info, list(items))\n\n    return ret\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/input.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Mapping\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Any, Generic, Literal, Optional, TypedDict, TypeVar, Union\n\nimport navi\n\nfrom .types import InputId, OutputId\n\nInputKind = Literal[\n    \"number\",\n    \"slider\",\n    \"dropdown\",\n    \"text\",\n    \"directory\",\n    \"file\",\n    \"color\",\n    \"generic\",\n    \"static\",\n]\n\n\n@dataclass\nclass InputConversion:\n    \"\"\"\n    An input conversion can be used to convert the assigned type of an input.\n    This is useful to model the changes `enforce` makes to values.\n\n    `type` is used to declare which type is intended to be converted by this\n    conversion. `convert` is the expression that does the actual conversion. It\n    will be given a special parameter called `Input` that will be the value to\n    convert. The `Input` parameter is guaranteed to be a non-empty sub type of\n    `type`.\n\n    Example:\n    To convert all numbers to string, use this conversions:\n    ```\n    InputConversion(\"number\", \"toString(Input)\")\n    ```\n    \"\"\"\n\n    type: navi.ExpressionJson\n    convert: navi.ExpressionJson\n\n    def to_dict(self):\n        return {\n            \"type\": self.type,\n            \"convert\": self.convert,\n        }\n\n\n@dataclass\nclass IOFusion:\n    output_id: OutputId\n\n\nclass LiteralErrorValue(TypedDict):\n    type: Literal[\"literal\"]\n    value: str | int | float | None\n\n\nclass FormattedErrorValue(TypedDict):\n    type: Literal[\"formatted\"]\n    formatString: str\n\n\nclass PendingErrorValue(TypedDict):\n    type: Literal[\"pending\"]\n\n\nclass UnknownErrorValue(TypedDict):\n    type: Literal[\"unknown\"]\n    typeName: str\n    typeModule: str\n\n\nErrorValue = Union[\n    LiteralErrorValue, FormattedErrorValue, UnknownErrorValue, PendingErrorValue\n]\n\nT = TypeVar(\"T\")\n\n\nclass BaseInput(Generic[T]):\n    def __init__(\n        self,\n        input_type: navi.ExpressionJson,\n        label: str,\n        kind: InputKind = \"generic\",\n        has_handle: bool = True,\n        associated_type: Any = None,\n    ) -> None:\n        self.input_type: navi.ExpressionJson = input_type\n        self.input_conversions: list[InputConversion] = []\n        self.input_adapt: navi.ExpressionJson | None = None\n        self.type_definitions: str | None = None\n        self.kind: InputKind = kind\n        self.label: str = label\n        self.optional: bool = False\n        self.has_handle: bool = has_handle\n        self.id: InputId = InputId(-1)\n        self.associated_type: Any = associated_type\n\n        self.fused: IOFusion | None = None\n        self.lazy: bool = False\n\n        # Optional documentation\n        self.description: str | None = None\n        self.hint: bool = False\n        self.should_suggest: bool = False\n\n    # This is the method that should be created by each input\n    def enforce(self, value: object) -> T:\n        \"\"\"Enforce the input type\"\"\"\n        return value  # type: ignore\n\n    # This is the method that should be called by the processing code\n    def enforce_(self, value: object | None) -> T | None:\n        if self.optional and value is None:\n            return None\n        assert value is not None, (\n            f\"Expected value to exist, \"\n            f\"but does not exist for {self.kind} input with type {self.input_type} and label {self.label}\"\n        )\n        return self.enforce(value)\n\n    def get_error_value(self, value: object) -> ErrorValue:\n        if isinstance(value, Enum):\n            # unwrap enum\n            value = value.value\n\n        if isinstance(value, bool):\n            # bools need to be 0 or 1\n            return {\"type\": \"literal\", \"value\": int(value)}\n\n        if isinstance(value, int | float | str) or value is None:\n            return {\"type\": \"literal\", \"value\": value}\n\n        if isinstance(value, Path):\n            return {\"type\": \"literal\", \"value\": str(value)}\n\n        return {\n            \"type\": \"unknown\",\n            \"typeName\": type(value).__qualname__,\n            \"typeModule\": type(value).__module__,\n        }\n\n    def to_dict(self) -> Mapping[str, Any]:\n        actual_type = [self.input_type, \"null\"] if self.optional else self.input_type\n        return {\n            \"id\": self.id,\n            \"type\": actual_type,\n            \"conversions\": [c.to_dict() for c in self.input_conversions],\n            \"adapt\": self.input_adapt,\n            \"typeDefinitions\": self.type_definitions,\n            \"kind\": self.kind,\n            \"label\": self.label,\n            \"optional\": self.optional,\n            \"hasHandle\": self.has_handle,\n            \"description\": self.description,\n            \"hint\": self.hint,\n            \"suggest\": self.should_suggest,\n            \"fused\": {\n                \"outputId\": self.fused.output_id,\n            }\n            if self.fused\n            else None,\n        }\n\n    def with_id(self, input_id: InputId | int):\n        self.id = InputId(input_id)\n        return self\n\n    def with_docs(self, *description: str, hint: bool = False):\n        self.description = \"\\n\\n\".join(description)\n        self.hint = hint\n        return self\n\n    def suggest(self):\n        self.should_suggest = True\n        return self\n\n    def make_optional(self):\n        self.optional = True\n        if self.associated_type is not None:\n            associated_type = self.associated_type\n            self.associated_type = Optional[associated_type]\n        return self\n\n    def make_lazy(self):\n        self.lazy = True\n        return self\n\n    def make_fused(self, with_output: OutputId | int = 0):\n        self.fused = IOFusion(output_id=OutputId(with_output))\n        return self\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/iter.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable, Iterable\nfrom dataclasses import dataclass\nfrom typing import Generic, TypeVar\n\nI = TypeVar(\"I\")\nL = TypeVar(\"L\")\n\n\n@dataclass\nclass Generator(Generic[I]):\n    supplier: Callable[[], Iterable[I | Exception]]\n    expected_length: int\n    fail_fast: bool = True\n    metadata: object | None = None\n\n    def with_fail_fast(self, fail_fast: bool):\n        self.fail_fast = fail_fast\n        return self\n\n    def with_metadata(self, metadata: object):\n        self.metadata = metadata\n        return self\n\n    @staticmethod\n    def from_iter(\n        supplier: Callable[[], Iterable[I | Exception]], expected_length: int\n    ) -> Generator[I]:\n        return Generator(supplier, expected_length)\n\n    @staticmethod\n    def from_list(l: list[L], map_fn: Callable[[L, int], I]) -> Generator[I]:\n        \"\"\"\n        Creates a new generator from a list that is mapped using the given\n        function. The iterable will be equivalent to `map(map_fn, l)`.\n        \"\"\"\n\n        def supplier():\n            for i, x in enumerate(l):\n                try:\n                    yield map_fn(x, i)\n                except Exception as e:\n                    yield e\n\n        return Generator(supplier, len(l))\n\n    @staticmethod\n    def from_range(count: int, map_fn: Callable[[int], I]) -> Generator[I]:\n        \"\"\"\n        Creates a new generator the given number of items where each item is\n        lazily evaluated. The iterable will be equivalent to `map(map_fn, range(count))`.\n        \"\"\"\n        assert count >= 0\n\n        def supplier():\n            for i in range(count):\n                try:\n                    yield map_fn(i)\n                except Exception as e:\n                    yield e\n\n        return Generator(supplier, count)\n\n\nN = TypeVar(\"N\")\nR = TypeVar(\"R\")\n\n\n@dataclass\nclass Collector(Generic[N, R]):\n    on_iterate: Callable[[N], None]\n    on_complete: Callable[[], R]\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/lazy.py",
    "content": "from __future__ import annotations\n\nimport time\nfrom asyncio import AbstractEventLoop\nfrom collections.abc import Callable, Coroutine\nfrom typing import Any, Generic, TypeVar\n\nT = TypeVar(\"T\")\n\n\nclass _Result(Generic[T]):\n    \"\"\"Either an okay value of T or an error value.\"\"\"\n\n    def __init__(self, value: T | None, error: Exception | None) -> None:\n        self.value = value\n        self.error = error\n\n    def result(self) -> T:\n        \"\"\"Returns the value if it is okay, otherwise raises the error.\"\"\"\n        if self.error is not None:\n            raise self.error\n        return self.value  # type: ignore\n\n    @property\n    def is_ok(self) -> bool:\n        \"\"\"Returns True if the result is okay, otherwise False.\"\"\"\n        return self.error is None\n\n    @staticmethod\n    def ok(value: T) -> _Result[T]:\n        return _Result(value, None)\n\n    @staticmethod\n    def err(error: Exception) -> _Result[T]:\n        return _Result(None, error)\n\n\ndef _to_result(fn: Callable[[], T]) -> Callable[[], _Result[T]]:\n    def wrapper() -> _Result[T]:\n        try:\n            return _Result.ok(fn())\n        except Exception as e:\n            return _Result.err(e)\n\n    return wrapper\n\n\nclass Lazy(Generic[T]):\n    def __init__(self, factory: Callable[[], T]) -> None:\n        self._factory = _to_result(factory)\n        self._value: _Result[T] | None = None\n        self._evaluating = False\n        self._eval_time = 0\n\n    @staticmethod\n    def ready(value: T) -> Lazy[T]:\n        lazy = Lazy(lambda: value)\n        lazy._value = _Result.ok(value)  # noqa: SLF001\n        return lazy\n\n    @staticmethod\n    def from_coroutine(\n        coroutine: Coroutine[Any, Any, T], loop: AbstractEventLoop\n    ) -> Lazy[T]:\n        def supplier() -> T:\n            task = loop.create_task(coroutine)\n\n            while not task.done():\n                if task.cancelled():\n                    raise ValueError(\"Task was cancelled\")\n                time.sleep(0.001)\n\n            return task.result()\n\n        return Lazy(supplier)\n\n    @property\n    def has_value(self) -> bool:\n        \"\"\"Returns True if the value has been computed, otherwise False.\"\"\"\n        return self._value is not None and self._value.is_ok\n\n    @property\n    def has_error(self) -> bool:\n        \"\"\"Returns True if the value has been computed and it errored instead, otherwise False.\"\"\"\n        return self._value is not None and not self._value.is_ok\n\n    @property\n    def evaluation_time(self) -> float:\n        \"\"\"The time in seconds that it took to evaluate the value. If the value is not computed, returns 0.\"\"\"\n        return self._eval_time\n\n    @property\n    def value(self) -> T:\n        if self._value is None:\n            if self._evaluating:\n                # wait for the value to be computed\n                while self._value is None and self._evaluating:\n                    time.sleep(0.001)\n                if self._value is None:\n                    raise ValueError(\"Value was not computed\")\n            else:\n                self._evaluating = True\n                try:\n                    start = time.time()\n                    self._value = self._factory()\n                    self._eval_time = time.time() - start\n                finally:\n                    self._evaluating = False\n\n        return self._value.result()\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/node_check.py",
    "content": "from __future__ import annotations\n\nimport ast\nimport inspect\nimport os\nimport pathlib\nfrom collections import OrderedDict\nfrom collections.abc import Callable\nfrom enum import Enum\nfrom typing import Any, NewType, Union, cast, get_args\n\nfrom .node_context import NodeContext\nfrom .node_data import NodeData\n\n_Ty = NewType(\"_Ty\", object)\n\n\nclass CheckFailedError(Exception):\n    pass\n\n\nclass CheckLevel(Enum):\n    NONE = \"none\"\n    WARN = \"warn\"\n    FIX = \"fix\"\n    ERROR = \"error\"\n\n    @staticmethod\n    def parse(s: str) -> CheckLevel:\n        s = s.strip().lower()\n        if s == CheckLevel.NONE.value:\n            return CheckLevel.NONE\n        elif s == CheckLevel.WARN.value:\n            return CheckLevel.WARN\n        elif s == CheckLevel.FIX.value:\n            return CheckLevel.FIX\n        elif s == CheckLevel.ERROR.value:\n            return CheckLevel.ERROR\n        else:\n            raise ValueError(f\"Invalid check level: {s}\")\n\n\ndef _get_check_level(name: str, default: CheckLevel) -> CheckLevel:\n    try:\n        s = os.environ.get(name, default.value)\n        return CheckLevel.parse(s)\n    except Exception:\n        return default\n\n\nCHECK_LEVEL = _get_check_level(\"CHECK_LEVEL\", CheckLevel.NONE)\nNAME_CHECK_LEVEL = _get_check_level(\"NAME_CHECK_LEVEL\", CHECK_LEVEL)\nTYPE_CHECK_LEVEL = _get_check_level(\"TYPE_CHECK_LEVEL\", CHECK_LEVEL)\n\n\nclass TypeTransformer(ast.NodeTransformer):\n    def visit_BinOp(self, node: ast.BinOp):  # noqa\n        if isinstance(node.op, ast.BitOr):\n            return ast.Subscript(\n                value=ast.Name(id=\"Union\", ctx=ast.Load()),\n                slice=ast.Index(\n                    value=ast.Tuple(\n                        elts=[\n                            self.visit(node.left),\n                            self.visit(node.right),\n                        ],\n                        ctx=ast.Load(),\n                    ),\n                    ctx=ast.Load(),\n                ),\n                ctx=ast.Load(),\n            )\n        return super().visit_BinOp(node)\n\n    def visit_Subscript(self, node: ast.Subscript):  # noqa\n        if isinstance(node.value, ast.Name) and node.value.id == \"tuple\":\n            return ast.Subscript(\n                value=ast.Name(id=\"Tuple\", ctx=ast.Load()),\n                slice=node.slice,\n                ctx=ast.Load(),\n            )\n        return super().visit_Subscript(node)\n\n\ndef compile_type_string(s: str, filename: str = \"<string>\"):\n    tree = ast.parse(s, filename, \"eval\")\n    new_tree = ast.fix_missing_locations(TypeTransformer().visit(tree))\n    return compile(new_tree, filename, \"eval\")\n\n\ndef eval_type(t: str | _Ty, __globals: dict[str, Any], /):\n    if not isinstance(t, str):\n        return t\n\n    # `compile_type_string` adds `Union`, so we need it in scope\n    local_scope = {\n        \"Union\": Union,\n        \"Tuple\": tuple,\n    }\n\n    try:\n        # pylint: disable=eval-used\n        return _Ty(eval(compile_type_string(t), __globals, local_scope))\n    except Exception as e:\n        raise ValueError(f\"Unable to evaluate type '{t}': {e}\") from e\n\n\ndef union_types(types: list[_Ty]) -> _Ty:\n    assert len(types) > 0\n    t: Any = types[0]\n    for t2 in types[1:]:\n        t = Union[t, cast(Any, t2)]\n    return t\n\n\ndef union_to_set(t: _Ty) -> set[_Ty]:\n    s = str(t)\n    if s.startswith(\"typing.Union[\"):\n        return set(get_args(t))\n    elif s.startswith(\"typing.Optional[\"):\n        return {*union_to_set(get_args(t)[0]), _Ty(type(None))}\n    else:\n        return {t}\n\n\ndef is_subset_of(a: _Ty, b: _Ty) -> bool:\n    if a == b:\n        return True\n\n    return union_to_set(a).issubset(union_to_set(b))\n\n\ndef is_tuple(t: _Ty) -> bool:\n    s = str(t)\n    return s.startswith((\"typing.Tuple[\", \"tuple[\"))\n\n\nclass FailedToParse:\n    pass\n\n\ndef get_type_annotations(fn: Callable) -> dict[str, _Ty | FailedToParse]:\n    \"\"\"Get the annotations for a function, with support for Python 3.8+\"\"\"\n    ann = getattr(fn, \"__annotations__\", None)\n\n    if ann is None:\n        return {}\n\n    type_annotations: dict[str, _Ty | FailedToParse] = {}\n    for k, v in ann.items():\n        try:\n            type_annotations[k] = eval_type(v, fn.__globals__)\n        except Exception:\n            type_annotations[k] = FailedToParse()\n    return type_annotations\n\n\ndef validate_return_type(return_type: _Ty, node: NodeData) -> None:\n    outputs = node.outputs\n\n    if len(outputs) == 0:\n        if return_type is not None and return_type is not type(None):  # type: ignore\n            raise CheckFailedError(\n                \"Return type should be 'None' because there are no outputs\"\n            )\n    elif len(outputs) == 1:\n        o = outputs[0]\n        if o.associated_type is not None and not is_subset_of(\n            return_type, o.associated_type\n        ):\n            raise CheckFailedError(\n                f\"Return type '{return_type}' must be a subset of '{o.associated_type}'\"\n            )\n    else:\n        if not is_tuple(return_type):\n            raise CheckFailedError(\n                f\"Return type '{return_type}' must be a tuple because there are multiple outputs\"\n            )\n\n        return_args = get_args(return_type)\n        if len(return_args) != len(outputs):\n            raise CheckFailedError(\n                f\"Return type '{return_type}' must have the same number of arguments as there are outputs\"\n            )\n\n        for o, return_arg in zip(outputs, return_args, strict=False):\n            if o.associated_type is not None and not is_subset_of(\n                return_arg, o.associated_type\n            ):\n                raise CheckFailedError(\n                    f\"Return type of {o.label} '{return_arg}' must be a subset of '{o.associated_type}'\"\n                )\n\n\ndef check_schema_types(\n    wrapped_func: Callable,\n    node: NodeData,\n) -> None:\n    \"\"\"\n    Runtime validation for the number of inputs/outputs compared to the type args\n    \"\"\"\n\n    if node.kind != \"regularNode\":\n        return\n\n    ann = OrderedDict(get_type_annotations(wrapped_func))\n\n    # check return type\n    if \"return\" in ann:\n        return_type = ann.pop(\"return\")\n        if not isinstance(return_type, FailedToParse):\n            validate_return_type(return_type, node)\n\n    # check arguments\n    arg_spec = inspect.getfullargspec(wrapped_func)\n    for arg in arg_spec.args:\n        if arg not in ann:\n            raise CheckFailedError(f\"Missing type annotation for '{arg}'\")\n\n    if node.node_context:\n        first = arg_spec.args[0]\n        if first != \"context\":\n            raise CheckFailedError(\n                f\"Expected the first parameter to be 'context: NodeContext' but found '{first}'.\"\n            )\n        context_type = ann.pop(first)\n        if context_type != NodeContext:  # type: ignore\n            raise CheckFailedError(\n                f\"Expected type of 'context' to be 'api.NodeContext' but found '{context_type}'\"\n            )\n\n    # check inputs\n    inputs = node.inputs\n\n    if arg_spec.varargs is not None:\n        if arg_spec.varargs not in ann:\n            raise CheckFailedError(f\"Missing type annotation for '{arg_spec.varargs}'\")\n        va_type = ann.pop(arg_spec.varargs)\n\n        # split inputs by varargs and non-varargs\n        varargs_inputs = inputs[len(ann) :]\n        inputs = inputs[: len(ann)]\n\n        total: list[_Ty] | None = []\n        for i in varargs_inputs:\n            associated_type = i.associated_type\n\n            if associated_type is not None and not isinstance(va_type, FailedToParse):\n                if not is_subset_of(associated_type, va_type):\n                    raise CheckFailedError(\n                        f\"Input type of {i.label} '{associated_type}' is not assignable to varargs type '{va_type}'\"\n                    )\n\n            # append to total\n            if associated_type is not None:\n                if total is not None:\n                    total.append(associated_type)\n            else:\n                total = None\n\n        if total is not None and not isinstance(va_type, FailedToParse):\n            total_type = union_types(total)\n            if total_type != va_type:\n                raise CheckFailedError(\n                    f\"Varargs type '{va_type}' should be equal to the union of all arguments '{total_type}'\"\n                )\n\n    if len(ann) != len(inputs):\n        raise CheckFailedError(\n            f\"Number of inputs and arguments don't match: {len(ann)=} != {len(inputs)=}\"\n        )\n    for (a_name, a_type), i in zip(ann.items(), inputs, strict=False):\n        associated_type = i.associated_type\n        if (\n            associated_type is not None\n            and not isinstance(a_type, FailedToParse)\n            and a_type != associated_type\n        ):\n            raise CheckFailedError(\n                f\"Expected type of {i.label} ({a_name}) to be '{associated_type}' but found '{a_type}'\"\n            )\n\n\ndef check_naming_conventions(\n    wrapped_func: Callable,\n    name: str,\n    fix: bool,\n) -> None:\n    expected_name = (\n        name.lower()\n        .replace(\" \", \"_\")\n        .replace(\"-\", \"_\")\n        .replace(\"(\", \"\")\n        .replace(\")\", \"\")\n        .replace(\"&\", \"and\")\n    )\n\n    func_name = wrapped_func.__name__\n    file_path = pathlib.Path(inspect.getfile(wrapped_func))\n    file_name = file_path.stem\n\n    # check function name\n    if func_name != expected_name + \"_node\":\n        if not fix:\n            raise CheckFailedError(\n                f\"Function name is '{func_name}', but it should be '{expected_name}_node'\"\n            )\n\n        fixed_code = file_path.read_text(encoding=\"utf-8\").replace(\n            f\"def {func_name}(\", f\"def {expected_name}_node(\"\n        )\n        file_path.write_text(fixed_code, encoding=\"utf-8\")\n\n    # check file name\n    if file_name != expected_name:\n        if not fix:\n            raise CheckFailedError(\n                f\"File name is '{file_name}.py', but it should be '{expected_name}.py'\"\n            )\n\n        os.rename(file_path, file_path.with_name(expected_name + \".py\"))\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/node_context.py",
    "content": "import time\nfrom abc import ABC, abstractmethod\nfrom collections.abc import Callable\nfrom pathlib import Path\nfrom typing import Literal\n\nfrom .settings import SettingsParser\n\n\nclass Aborted(Exception):\n    pass\n\n\nclass Progress(ABC):\n    @property\n    @abstractmethod\n    def aborted(self) -> bool:\n        \"\"\"\n        Returns whether the current operation was aborted.\n        \"\"\"\n\n    @property\n    @abstractmethod\n    def paused(self) -> bool:\n        \"\"\"\n        Returns whether the current operation was paused.\n        \"\"\"\n\n    def check_aborted(self) -> None:\n        \"\"\"\n        Raises an `Aborted` exception if the current operation was aborted. Does nothing otherwise.\n        \"\"\"\n\n        if self.aborted:\n            raise Aborted()\n\n    def suspend(self) -> None:\n        \"\"\"\n        If the operation was aborted, this method will throw an `Aborted` exception.\n        If the operation is paused, this method will wait until the operation is resumed or aborted.\n        \"\"\"\n\n        while True:\n            self.check_aborted()\n            if not self.paused:\n                break\n            time.sleep(0.1)\n\n    @abstractmethod\n    def set_progress(self, progress: float) -> None:\n        \"\"\"\n        Sets the progress of the current node execution. `progress` must be a value between 0 and 1.\n\n        Raises an `Aborted` exception if the current operation was aborted.\n        \"\"\"\n\n    def sub_progress(self, offset: float, length: float) -> \"Progress\":\n        \"\"\"\n        Returns a new `NodeProgress` object that represents a sub-progress of the current operation.\n\n        The progress range of the sub-progress is defined by `offset` and `length`. `offset` must be a value between 0\n        and 1, and `length` must be a positive value such that `offset + length <= 1`.\n\n        The real progress of the sub-progress is calculated as `offset + progress * length`, where `progress` is the\n        progress value passed to `set_progress` of the sub-progress.\n        \"\"\"\n        return _SubProgress(self, offset, length)\n\n    @staticmethod\n    def noop_progress() -> \"Progress\":\n        \"\"\"\n        Returns a `Progress` object that does nothing. It is never paused or aborted and does not report any progress.\n        \"\"\"\n        return _NoopProgress()\n\n\nclass _NoopProgress(Progress):\n    @property\n    def aborted(self) -> Literal[False]:\n        return False\n\n    @property\n    def paused(self) -> Literal[False]:\n        return False\n\n    def check_aborted(self) -> None:\n        pass\n\n    def suspend(self) -> None:\n        pass\n\n    def set_progress(self, progress: float) -> None:\n        pass\n\n    def sub_progress(self, offset: float, length: float) -> \"Progress\":\n        return _NoopProgress()\n\n\nclass _SubProgress(Progress):\n    def __init__(self, parent: Progress, offset: float, length: float) -> None:\n        self._parent = parent\n        self._offset = offset\n        self._length = length\n\n    @property\n    def aborted(self) -> bool:\n        return self._parent.aborted\n\n    @property\n    def paused(self) -> bool:\n        return self._parent.paused\n\n    def check_aborted(self) -> None:\n        self._parent.check_aborted()\n\n    def suspend(self) -> None:\n        self._parent.suspend()\n\n    def set_progress(self, progress: float) -> None:\n        self._parent.set_progress(self._offset + progress * self._length)\n\n    def sub_progress(self, offset: float, length: float) -> \"_SubProgress\":\n        return _SubProgress(\n            self._parent,\n            offset=self._offset + offset * self._length,\n            length=length * self._length,\n        )\n\n\nclass NodeContext(Progress, ABC):\n    \"\"\"\n    The execution context of the current node.\n    \"\"\"\n\n    @property\n    @abstractmethod\n    def settings(self) -> SettingsParser:\n        \"\"\"\n        Returns the settings of the current node execution.\n        \"\"\"\n\n    @property\n    @abstractmethod\n    def storage_dir(self) -> Path:\n        \"\"\"\n        The path of a directory where nodes can store files.\n\n        This directory persists between node executions, and its contents are shared between different nodes.\n        \"\"\"\n\n    @abstractmethod\n    def add_cleanup(\n        self, fn: Callable[[], None], after: Literal[\"node\", \"chain\"] = \"chain\"\n    ) -> None:\n        \"\"\"\n        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).\n\n        Registering the same function (object) twice will only result in the function being called once.\n        \"\"\"\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/node_data.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable, Mapping\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom typing import Any, Generic, Protocol, TypeVar\n\nimport navi\n\nfrom .group import NestedIdGroup\nfrom .input import BaseInput\nfrom .iter import Generator\nfrom .output import BaseOutput\nfrom .types import (\n    FeatureId,\n    InputId,\n    IterInputId,\n    IterOutputId,\n    NodeKind,\n    OutputId,\n    RunFn,\n)\n\n\nclass IteratorInputInfo:\n    def __init__(\n        self,\n        inputs: int | InputId | list[int] | list[InputId] | list[int | InputId],\n        length_type: navi.ExpressionJson = \"uint\",\n    ) -> None:\n        self.id: IterInputId = IterInputId(0)\n        self.inputs: list[InputId] = (\n            [InputId(x) for x in inputs]\n            if isinstance(inputs, list)\n            else [InputId(inputs)]\n        )\n        self.length_type: navi.ExpressionJson = length_type\n\n    def to_dict(self):\n        return {\n            \"id\": self.id,\n            \"inputs\": self.inputs,\n            \"sequenceType\": navi.named(\"Sequence\", {\"length\": self.length_type}),\n        }\n\n\nM_co = TypeVar(\"M_co\", covariant=True)\n\n\nclass AnyConstructor(Protocol, Generic[M_co]):\n    def __call__(self, *args: Any, **kwargs: Any) -> M_co: ...\n\n\nclass IteratorOutputInfo:\n    def __init__(\n        self,\n        outputs: int | OutputId | list[int] | list[OutputId] | list[int | OutputId],\n        length_type: navi.ExpressionJson = \"uint\",\n    ) -> None:\n        self.id: IterOutputId = IterOutputId(0)\n        self.outputs: list[OutputId] = (\n            [OutputId(x) for x in outputs]\n            if isinstance(outputs, list)\n            else [OutputId(outputs)]\n        )\n        self.length_type: navi.ExpressionJson = length_type\n\n        self._metadata_constructor: Any | None = None\n        self._item_types_fn: (\n            Callable[[Any], Mapping[OutputId, navi.ExpressionJson]] | None\n        ) = None\n\n    def with_item_types(\n        self,\n        class_: AnyConstructor[M_co],\n        fn: Callable[[M_co], Mapping[OutputId, navi.ExpressionJson]],\n    ):\n        self._metadata_constructor = class_\n        self._item_types_fn = fn\n        return self\n\n    def to_dict(self):\n        return {\n            \"id\": self.id,\n            \"outputs\": self.outputs,\n            \"sequenceType\": navi.named(\"Sequence\", {\"length\": self.length_type}),\n        }\n\n    def get_broadcast_sequence_type(self, generator: Generator) -> navi.ExpressionJson:\n        return navi.named(\"Sequence\", {\"length\": generator.expected_length})\n\n    def get_broadcast_item_types(\n        self, generator: Generator\n    ) -> Mapping[OutputId, navi.ExpressionJson]:\n        if self._item_types_fn is not None and self._metadata_constructor is not None:\n            metadata = generator.metadata\n            if isinstance(metadata, self._metadata_constructor):\n                return self._item_types_fn(metadata)\n        return {}\n\n\nclass KeyInfo:\n    def __init__(self, data: dict[str, Any]) -> None:\n        self._data = data\n\n    @staticmethod\n    def enum(enum_input: InputId | int) -> KeyInfo:\n        return KeyInfo({\"kind\": \"enum\", \"inputId\": enum_input})\n\n    @staticmethod\n    def number(number_input: InputId | int) -> KeyInfo:\n        return KeyInfo({\"kind\": \"number\", \"inputId\": number_input})\n\n    @staticmethod\n    def type(expression: navi.ExpressionJson) -> KeyInfo:\n        return KeyInfo({\"kind\": \"type\", \"expression\": expression})\n\n    def to_dict(self):\n        return self._data\n\n\nclass SpecialSuggestion:\n    \"\"\"\n    A special suggestion in chaiNNer's context node selector.\n\n    A suggestion consists of 3 parts:\n    1.  The search query to match. The query may optionally contain a pattern at the end\n        to supply a value to an input. E.g. `+{2}` will match the search query \"+123\"\n        and \"123\" will be parsed for the input with ID 2.\n    2.  The name of the suggestion. This is the text that will be displayed in the\n        suggestion list.\n    3.  The input values to supply to the node. This is a mapping of input IDs to the\n        values to supply to them. Values that aren't defined here will be left as\n        default values.\n    \"\"\"\n\n    def __init__(\n        self,\n        query: str,\n        *,\n        name: str | None = None,\n        inputs: Mapping[InputId | int, Any] = {},\n    ) -> None:\n        self.query, self.parse_input = SpecialSuggestion._parse_query(query)\n        self.name = name\n        self.inputs: dict[InputId, Any] = {InputId(k): v for k, v in inputs.items()}\n\n    @staticmethod\n    def _parse_query(query: str) -> tuple[str, InputId | None]:\n        # e.g. \"+{2}\"\n        if \"{\" in query:\n            query, input_id = query.split(\"{\")\n            input_id = int(input_id[:-1])\n            return query, InputId(input_id)\n        return query, None\n\n    def to_dict(self):\n        def convert_value(value: Any) -> Any:\n            if isinstance(value, bool):\n                return int(value)\n            if isinstance(value, Enum):\n                return value.value\n            return value\n\n        return {\n            \"query\": self.query,\n            \"name\": self.name,\n            \"parseInput\": self.parse_input,\n            \"inputs\": {k: convert_value(v) for k, v in self.inputs.items()},\n        }\n\n\n@dataclass(frozen=True)\nclass NodeData:\n    schema_id: str\n    description: str\n    see_also: list[str]\n    name: str\n    icon: str\n    kind: NodeKind\n\n    inputs: list[BaseInput]\n    outputs: list[BaseOutput]\n    group_layout: list[InputId | NestedIdGroup]\n\n    iterable_inputs: list[IteratorInputInfo]\n    iterable_outputs: list[IteratorOutputInfo]\n\n    key_info: KeyInfo | None\n    suggestions: list[SpecialSuggestion]\n\n    side_effects: bool\n    deprecated: bool\n    node_context: bool\n    features: list[FeatureId]\n\n    run: RunFn\n\n    @property\n    def single_iterable_input(self) -> IteratorInputInfo:\n        assert len(self.iterable_inputs) == 1\n        return self.iterable_inputs[0]\n\n    @property\n    def single_iterable_output(self) -> IteratorOutputInfo:\n        assert len(self.iterable_outputs) == 1\n        return self.iterable_outputs[0]\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/output.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Mapping\nfrom typing import Any, Generic, Literal, TypeVar\n\nimport navi\n\nfrom .types import InputId, OutputId\n\nOutputKind = Literal[\"large-image\", \"tagged\", \"generic\"]\nBroadcastData = Mapping[str, object]\n\nT = TypeVar(\"T\")\n\n\nclass BaseOutput(Generic[T]):\n    def __init__(\n        self,\n        output_type: navi.ExpressionJson,\n        label: str,\n        kind: OutputKind = \"generic\",\n        has_handle: bool = True,\n        associated_type: Any = None,\n    ) -> None:\n        self.output_type: navi.ExpressionJson = output_type\n        self.label: str = label\n        self.id: OutputId = OutputId(-1)\n        self.never_reason: str | None = None\n        self.kind: OutputKind = kind\n        self.has_handle: bool = has_handle\n        self.passthrough_of: InputId | None = None\n\n        self.associated_type: Any = associated_type\n\n        # Optional documentation\n        self.description: str | None = None\n        self.should_suggest: bool = False\n\n    def to_dict(self):\n        return {\n            \"id\": self.id,\n            \"type\": self.output_type,\n            \"label\": self.label,\n            \"neverReason\": self.never_reason,\n            \"kind\": self.kind,\n            \"hasHandle\": self.has_handle,\n            \"passthroughOf\": self.passthrough_of,\n            \"description\": self.description,\n            \"suggest\": self.should_suggest,\n        }\n\n    def with_id(self, output_id: OutputId | int):\n        self.id = OutputId(output_id)\n        return self\n\n    def with_never_reason(self, reason: str):\n        self.never_reason = reason\n        return self\n\n    def with_docs(self, *description: str):\n        self.description = \"\\n\\n\".join(description)\n        return self\n\n    def suggest(self):\n        self.should_suggest = True\n        return self\n\n    def as_passthrough_of(self, input_id: InputId | int):\n        self.passthrough_of = InputId(input_id)\n        return self\n\n    def get_broadcast_data(self, _value: T) -> BroadcastData | None:\n        return None\n\n    def get_broadcast_type(self, _value: T) -> navi.ExpressionJson | None:\n        return None\n\n    def enforce(self, value: object) -> T:\n        assert value is not None\n        return value  # type: ignore\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/settings.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import TypedDict, Union\n\nfrom sanic.log import logger\n\nSettingsJson = dict[str, object]\nJsonExecutionOptions = dict[str, SettingsJson]\n\n\nclass ExecutionOptions:\n    def __init__(\n        self,\n        backend_settings: JsonExecutionOptions,\n    ) -> None:\n        self.__settings = backend_settings\n        self.__parsers: dict[str, SettingsParser] = {}\n\n        logger.info(f\"Execution options: {self.__settings}\")\n\n    @staticmethod\n    def parse(json: JsonExecutionOptions) -> ExecutionOptions:\n        return ExecutionOptions(backend_settings=json)\n\n    def get_package_settings_json(self, package_id: str) -> SettingsJson:\n        return self.__settings.get(package_id, {})\n\n    def get_package_settings(self, package_id: str) -> SettingsParser:\n        parser = self.__parsers.get(package_id)\n        if parser is None:\n            parser = SettingsParser(self.get_package_settings_json(package_id))\n            self.__parsers[package_id] = parser\n        return parser\n\n\nclass SettingsParser:\n    def __init__(self, raw: SettingsJson) -> None:\n        self.__settings = raw\n\n    def get_bool(self, key: str, default: bool) -> bool:\n        value = self.__settings.get(key, default)\n        if isinstance(value, bool):\n            return value\n        raise ValueError(f\"Invalid bool value for {key}: {value}\")\n\n    def get_int(self, key: str, default: int, parse_str: bool = False) -> int:\n        value = self.__settings.get(key, default)\n        if parse_str and isinstance(value, str):\n            return int(value)\n        if isinstance(value, int) and not isinstance(value, bool):\n            return value\n        raise ValueError(f\"Invalid str value for {key}: {value}\")\n\n    def get_str(self, key: str, default: str) -> str:\n        value = self.__settings.get(key, default)\n        if isinstance(value, str):\n            return value\n        raise ValueError(f\"Invalid str value for {key}: {value}\")\n\n    def get_cache_location(self, key: str) -> str | None:\n        value = self.__settings.get(key)\n        if isinstance(value, str) or value is None:\n            return value or None\n        raise ValueError(f\"Invalid cache location value for {key}: {value}\")\n\n\n@dataclass\nclass ToggleSetting:\n    label: str\n    key: str\n    description: str\n    default: bool = False\n    disabled: bool = False\n    type: str = \"toggle\"\n\n\nclass DropdownOption(TypedDict):\n    label: str\n    value: str\n\n\n@dataclass\nclass DropdownSetting:\n    label: str\n    key: str\n    description: str\n    options: list[DropdownOption]\n    default: str\n    disabled: bool = False\n    type: str = \"dropdown\"\n\n\n@dataclass\nclass NumberSetting:\n    label: str\n    key: str\n    description: str\n    min: float\n    max: float\n    default: float = 0\n    disabled: bool = False\n    type: str = \"number\"\n\n\n@dataclass\nclass CacheSetting:\n    label: str\n    key: str\n    description: str\n    directory: str\n    default: str = \"\"\n    disabled: bool = False\n    type: str = \"cache\"\n\n\nSetting = Union[ToggleSetting, DropdownSetting, NumberSetting, CacheSetting]\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/api/types.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom typing import Any, Literal, NewType\n\nNodeId = NewType(\"NodeId\", str)\nInputId = NewType(\"InputId\", int)\nOutputId = NewType(\"OutputId\", int)\nIterInputId = NewType(\"IterInputId\", int)\nIterOutputId = NewType(\"IterOutputId\", int)\nFeatureId = NewType(\"FeatureId\", str)\n\n\nRunFn = Callable[..., Any]\n\nNodeKind = Literal[\"regularNode\", \"generator\", \"collector\"]\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/device_list.py",
    "content": "import json\nimport os\nimport sys\n\nsys.path.append(os.path.normpath(os.path.dirname(os.path.abspath(__file__))))\nfrom accelerator_detection import get_accelerator_detector\n\n# Get all available accelerator devices\ndetector = get_accelerator_detector()\nall_devices = detector.available_devices\nbest_device = detector.get_best_device()\n\ndevice_list = []\nfor device in all_devices:\n    device_info = {\n        \"type\": device.type.value,\n        \"index\": device.index,\n        \"name\": device.name,\n        \"device_string\": device.device_string,\n        \"supports_fp16\": device.supports_fp16,\n        \"supports_bf16\": device.supports_bf16,\n    }\n    if device.memory_total:\n        device_info[\"memory_total\"] = device.memory_total\n    if device.memory_free:\n        device_info[\"memory_free\"] = device.memory_free\n\n    device_list.append(device_info)\n\nprint(json.dumps({\"all_devices\": device_list, \"best_device\": all_devices.index(best_device)}))"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/gpu.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable, Sequence\nfrom dataclasses import dataclass\nfrom functools import cached_property\n\nimport pynvml as nv\nfrom sanic.log import logger\n\n_FP16_ARCH_ABILITY_MAP = {\n    nv.NVML_DEVICE_ARCH_KEPLER: False,\n    nv.NVML_DEVICE_ARCH_MAXWELL: False,\n    nv.NVML_DEVICE_ARCH_PASCAL: False,\n    nv.NVML_DEVICE_ARCH_VOLTA: True,\n    nv.NVML_DEVICE_ARCH_TURING: True,\n    nv.NVML_DEVICE_ARCH_AMPERE: True,\n    nv.NVML_DEVICE_ARCH_ADA: True,\n    nv.NVML_DEVICE_ARCH_HOPPER: True,\n    nv.NVML_DEVICE_ARCH_UNKNOWN: False,\n}\n\n\n@dataclass\nclass MemoryUsage:\n    total: int\n    used: int\n    free: int\n\n\n@dataclass(frozen=True)\nclass NvDevice:\n    index: int\n    handle: nv.c_nvmlDevice_t\n    name: str\n\n    @staticmethod\n    def from_index(index: int) -> NvDevice:\n        handle = nv.nvmlDeviceGetHandleByIndex(index)\n\n        return NvDevice(\n            index=index,\n            handle=handle,\n            name=nv.nvmlDeviceGetName(handle),\n        )\n\n    @cached_property\n    def architecture(self) -> int:\n        # We catch and ignore errors to support older drivers that don't have nvmlDeviceGetArchitecture\n        try:\n            return nv.nvmlDeviceGetArchitecture(self.handle)\n        except Exception:\n            return nv.NVML_DEVICE_ARCH_UNKNOWN\n\n    @property\n    def supports_fp16(self):\n        arch = self.architecture\n\n        # This generation also contains the GTX 1600 cards, which do not support FP16.\n        if arch == nv.NVML_DEVICE_ARCH_TURING:\n            return \"RTX\" in self.name\n\n        # Future proofing. We can be reasonably sure that future architectures will support FP16.\n        return _FP16_ARCH_ABILITY_MAP.get(arch, arch > nv.NVML_DEVICE_ARCH_HOPPER)\n\n    def get_current_vram_usage(self) -> MemoryUsage:\n        info = nv.nvmlDeviceGetMemoryInfo(self.handle)\n        return MemoryUsage(info.total, info.used, info.free)  # type: ignore\n\n\nclass NvInfo:\n    def __init__(\n        self, devices: Sequence[NvDevice], clean_up: Callable[[], None]\n    ) -> None:\n        self.__devices: Sequence[NvDevice] = devices\n        self.__clean_up = clean_up\n\n    @staticmethod\n    def unavailable():\n        return NvInfo([], lambda: None)\n\n    def __del__(self) -> None:\n        self.__clean_up()\n\n    @property\n    def devices(self) -> Sequence[NvDevice]:\n        return self.__devices\n\n    @property\n    def is_available(self):\n        return len(self.devices) > 0\n\n    @property\n    def all_support_fp16(self) -> bool:\n        return all(gpu.supports_fp16 for gpu in self.devices)\n\n\ndef _try_nvml_init() -> bool | None:\n    try:\n        nv.nvmlInit()\n        return True\n    except Exception as e:\n        if isinstance(e, nv.NVMLError):\n            logger.info(\"No Nvidia GPU found, or invalid driver installed.\")\n        else:\n            logger.info(\n                f\"Unknown error occurred when trying to initialize Nvidia GPU: {e}\"\n            )\n        return False\n\n\ndef _try_nvml_shutdown() -> None:\n    try:\n        nv.nvmlShutdown()\n    except Exception:\n        logger.warn(\"Failed to shut down Nvidia GPU.\", exc_info=True)\n\n\ndef _get_nvidia_info() -> NvInfo:\n    if not _try_nvml_init():\n        return NvInfo.unavailable()\n\n    try:\n        device_count = nv.nvmlDeviceGetCount()\n        devices = [NvDevice.from_index(i) for i in range(device_count)]\n        return NvInfo(devices, _try_nvml_shutdown)\n    except Exception as e:\n        logger.info(f\"Unknown error occurred when trying to initialize Nvidia GPU: {e}\")\n        _try_nvml_shutdown()\n        return NvInfo.unavailable()\n\n\nnvidia = _get_nvidia_info()\n\n\n__all__ = [\"MemoryUsage\", \"NvDevice\", \"NvInfo\", \"nvidia\"]\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/navi.py",
    "content": "from __future__ import annotations\n\nimport math\nfrom typing import Literal, TypedDict, Union\n\nNumberJson = Union[int, float, Literal[\"inf\"], Literal[\"-inf\"], Literal[\"NaN\"]]\n\n\ndef to_number_json(n: int | float) -> NumberJson:\n    if math.isnan(n):\n        return \"NaN\"\n    if n == float(\"inf\"):\n        return \"inf\"\n    if n == float(\"-inf\"):\n        return \"-inf\"\n    return n\n\n\ndef from_number_json(n: NumberJson) -> int | float:\n    if n == \"NaN\":\n        return float(\"nan\")\n    if n == \"inf\":\n        return float(\"inf\")\n    if n == \"-inf\":\n        return float(\"-inf\")\n    return n\n\n\nExpressionJson = Union[\n    str,\n    int,\n    bool,\n    \"NumericLiteralTypeJson\",\n    \"IntervalTypeJson\",\n    \"IntIntervalTypeJson\",\n    \"StringLiteralTypeJson\",\n    \"UnionExpressionJson\",\n    \"IntersectionExpressionJson\",\n    \"NamedExpressionJson\",\n    \"FieldAccessExpressionJson\",\n    \"FunctionCallExpressionJson\",\n    \"MatchExpressionJson\",\n    list[\"ExpressionJson\"],\n    list[int],\n    list[str],\n]\n\n\nclass NumericLiteralTypeJson(TypedDict):\n    type: Literal[\"numeric-literal\"]\n    value: NumberJson\n\n\nclass IntervalTypeJson(TypedDict):\n    type: Literal[\"interval\"]\n    min: NumberJson\n    max: NumberJson\n\n\nclass IntIntervalTypeJson(TypedDict):\n    type: Literal[\"int-interval\"]\n    min: NumberJson\n    max: NumberJson\n\n\nclass StringLiteralTypeJson(TypedDict):\n    type: Literal[\"string-literal\"]\n    value: str\n\n\nclass UnionExpressionJson(TypedDict):\n    type: Literal[\"union\"]\n    items: list[ExpressionJson]\n\n\nclass IntersectionExpressionJson(TypedDict):\n    type: Literal[\"intersection\"]\n    items: list[ExpressionJson]\n\n\nclass NamedExpressionJson(TypedDict):\n    type: Literal[\"named\"]\n    name: str\n    fields: dict[str, ExpressionJson] | None\n\n\nclass FieldAccessExpressionJson(TypedDict):\n    type: Literal[\"field-access\"]\n    of: ExpressionJson\n    field: str\n\n\nclass FunctionCallExpressionJson(TypedDict):\n    type: Literal[\"function-call\"]\n    name: str\n    args: list[ExpressionJson]\n\n\nclass MatchArmJson(TypedDict):\n    pattern: ExpressionJson\n    binding: str | None\n    to: ExpressionJson\n\n\nclass MatchExpressionJson(TypedDict):\n    type: Literal[\"match\"]\n    of: ExpressionJson\n    arms: list[MatchArmJson]\n\n\ndef literal(value: str | (int | float)) -> ExpressionJson:\n    if isinstance(value, str):\n        return {\n            \"type\": \"string-literal\",\n            \"value\": value,\n        }\n    return {\n        \"type\": \"numeric-literal\",\n        \"value\": to_number_json(value),\n    }\n\n\ndef interval(\n    min_value: int | (float | None) = None,\n    max_value: int | (float | None) = None,\n) -> ExpressionJson:\n    return {\n        \"type\": \"interval\",\n        \"min\": to_number_json(min_value if min_value is not None else float(\"-inf\")),\n        \"max\": to_number_json(max_value if max_value is not None else float(\"inf\")),\n    }\n\n\ndef int_interval(\n    min_value: int | (float | None) = None,\n    max_value: int | (float | None) = None,\n) -> ExpressionJson:\n    return {\n        \"type\": \"int-interval\",\n        \"min\": to_number_json(min_value if min_value is not None else float(\"-inf\")),\n        \"max\": to_number_json(max_value if max_value is not None else float(\"inf\")),\n    }\n\n\ndef union(*items: ExpressionJson) -> ExpressionJson:\n    return {\"type\": \"union\", \"items\": list(items)}\n\n\ndef intersect(*items: ExpressionJson) -> ExpressionJson:\n    return {\"type\": \"intersection\", \"items\": list(items)}\n\n\ndef intersect_with_error(*items: ExpressionJson) -> ExpressionJson:\n    return union(intersect(*items), *[intersect(\"Error\", item) for item in items])\n\n\ndef named(name: str, fields: dict[str, ExpressionJson] | None = None) -> ExpressionJson:\n    return {\"type\": \"named\", \"name\": name, \"fields\": fields}\n\n\ndef field(of: ExpressionJson, field_name: str) -> ExpressionJson:\n    return {\"type\": \"field-access\", \"of\": of, \"field\": field_name}\n\n\ndef fn(name: str, *args: ExpressionJson) -> ExpressionJson:\n    return {\"type\": \"function-call\", \"name\": name, \"args\": list(args)}\n\n\ndef match(\n    of: ExpressionJson,\n    *args: tuple[ExpressionJson, str | None, ExpressionJson],\n    default: ExpressionJson | None = None,\n) -> ExpressionJson:\n    arms: list[MatchArmJson] = []\n    for pattern, binding, to in args:\n        arms.append({\"pattern\": pattern, \"binding\": binding, \"to\": to})\n    if default is not None:\n        arms.append({\"pattern\": \"any\", \"binding\": None, \"to\": default})\n    return {\"type\": \"match\", \"of\": of, \"arms\": arms}\n\n\ndef Image(  # noqa: N802\n    width: ExpressionJson | None = None,\n    height: ExpressionJson | None = None,\n    channels: ExpressionJson | None = None,\n    width_as: ExpressionJson | None = None,\n    height_as: ExpressionJson | None = None,\n    channels_as: ExpressionJson | None = None,\n    size_as: ExpressionJson | None = None,\n) -> ExpressionJson:\n    fields: dict[str, ExpressionJson] = {}\n    if width is not None:\n        fields[\"width\"] = width\n    if height is not None:\n        fields[\"height\"] = height\n    if channels is not None:\n        fields[\"channels\"] = channels\n    if width_as is not None:\n        fields[\"width\"] = field(width_as, \"width\")\n    if height_as is not None:\n        fields[\"height\"] = field(height_as, \"height\")\n    if channels_as is not None:\n        fields[\"channels\"] = field(channels_as, \"channels\")\n    if size_as is not None:\n        fields[\"width\"] = field(size_as, \"width\")\n        fields[\"height\"] = field(size_as, \"height\")\n    return named(\"Image\", fields)\n\n\ndef Color(  # noqa: N802\n    channels: ExpressionJson | None = None,\n    channels_as: ExpressionJson | None = None,\n) -> ExpressionJson:\n    fields: dict[str, ExpressionJson] = {}\n    if channels is not None:\n        fields[\"channels\"] = channels\n    if channels_as is not None:\n        fields[\"channels\"] = field(channels_as, \"channels\")\n    return named(\"Color\", fields)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/__init__.py",
    "content": ""
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/condition.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom enum import Enum\nfrom typing import Literal, TypedDict, Union\n\nimport navi\n\nfrom api import InputId\n\nInputValue = Union[int, str]\nEnumValues = Union[\n    InputValue,\n    Enum,\n    Iterable[str],\n    Iterable[int],\n    Iterable[Enum],\n]\n\nConditionJson = Union[\n    \"_AndConditionJson\",\n    \"_OrConditionJson\",\n    \"_NotConditionJson\",\n    \"_EnumConditionJson\",\n    \"_TypeConditionJson\",\n]\n\n\nclass _AndConditionJson(TypedDict):\n    kind: Literal[\"and\"]\n    items: list[ConditionJson]\n\n\nclass _OrConditionJson(TypedDict):\n    kind: Literal[\"or\"]\n    items: list[ConditionJson]\n\n\nclass _NotConditionJson(TypedDict):\n    kind: Literal[\"not\"]\n    condition: ConditionJson\n\n\nclass _EnumConditionJson(TypedDict):\n    kind: Literal[\"enum\"]\n    enum: InputId\n    values: list[str | int]\n\n\nclass _TypeConditionJson(TypedDict):\n    kind: Literal[\"type\"]\n    input: InputId\n    condition: navi.ExpressionJson\n    ifNotConnected: bool\n\n\nclass Condition:\n    def __init__(self, value: ConditionJson) -> None:\n        self._value: ConditionJson = value\n\n    def to_json(self):\n        return self._value\n\n    def __and__(self, other: Condition) -> Condition:\n        return Condition({\"kind\": \"and\", \"items\": [self._value, other._value]})\n\n    def __or__(self, other: Condition) -> Condition:\n        return Condition({\"kind\": \"or\", \"items\": [self._value, other._value]})\n\n    def __invert__(self) -> Condition:\n        return Condition({\"kind\": \"not\", \"condition\": self._value})\n\n    @staticmethod\n    def enum(enum: int, values: EnumValues) -> Condition:\n        \"\"\"\n        A condition to check whether a certain dropdown/enum input has a certain value.\n        \"\"\"\n\n        v: list[str | int] = []\n\n        def convert(value: int | str | Enum) -> None:\n            if isinstance(value, int | str):\n                v.append(value)\n            else:\n                enum_value = value.value\n                assert isinstance(enum_value, int | str)\n                v.append(enum_value)\n\n        if isinstance(values, int | str | Enum):\n            convert(values)\n        else:\n            for value in values:\n                convert(value)\n\n        return Condition(\n            {\n                \"kind\": \"enum\",\n                \"enum\": InputId(enum),\n                \"values\": v,\n            }\n        )\n\n    @staticmethod\n    def bool(input_id: int, value: bool) -> Condition:\n        \"\"\"\n        A condition to check whether a certain bool input has a certain value.\n        \"\"\"\n        return Condition(\n            {\n                \"kind\": \"enum\",\n                \"enum\": InputId(input_id),\n                \"values\": [int(value)],\n            }\n        )\n\n    @staticmethod\n    def type(\n        input_id: int,\n        condition: navi.ExpressionJson,\n        if_not_connected: bool = False,\n    ) -> Condition:\n        \"\"\"\n        A condition to check whether a certain input is compatible a certain type.\n        Here \"compatible\" is defined as overlapping.\n        \"\"\"\n        return Condition(\n            {\n                \"kind\": \"type\",\n                \"input\": InputId(input_id),\n                \"condition\": condition,\n                \"ifNotConnected\": if_not_connected,\n            }\n        )\n\n    @staticmethod\n    def const(value: bool) -> Condition:\n        if value:\n            return Condition({\"kind\": \"and\", \"items\": []})\n        return Condition({\"kind\": \"or\", \"items\": []})\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/group.py",
    "content": "from typing import Any, Generic, NewType, TypeVar, Union\n\nfrom base_types import InputId\nfrom nodes.base_input import BaseInput\n\nT = TypeVar(\"T\")\n\n\nGroupId = NewType(\"GroupId\", int)\n\n\nclass GroupInfo:\n    def __init__(\n        self,\n        group_id: GroupId,\n        kind: str,\n        options: dict[str, Any] | None = None,\n    ) -> None:\n        self.id: GroupId = group_id\n        self.kind: str = kind\n        self.options: dict[str, Any] = {} if options is None else options\n\n\nclass Group(Generic[T]):\n    def __init__(self, info: GroupInfo, items: list[T]) -> None:\n        self.info: GroupInfo = info\n        self.items: list[T] = items\n\n    def toDict(self):\n        return {\n            \"id\": self.info.id,\n            \"kind\": self.info.kind,\n            \"options\": self.info.options,\n            \"items\": [i.toDict() if isinstance(i, Group) else i for i in self.items],\n        }\n\n\nNestedGroup = Group[Union[BaseInput, \"NestedGroup\"]]\nNestedIdGroup = Group[Union[InputId, \"NestedIdGroup\"]]\n\n\n# pylint: disable-next=redefined-builtin\ndef group(kind: str, options: dict[str, Any] | None = None, id: int = -1):\n    info = GroupInfo(GroupId(id), kind, options)\n\n    def ret(*items: BaseInput | NestedGroup) -> NestedGroup:\n        return Group(info, list(items))\n\n    return ret\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/groups.py",
    "content": "from __future__ import annotations\n\nfrom typing import Union\n\nfrom api import BaseInput, NestedGroup, group\n\nfrom .condition import Condition, EnumValues, InputValue\n\nRawEnumValues = Union[\n    InputValue, list[str], list[int], tuple[str, ...], tuple[int, ...]\n]\n\n\ndef if_group(condition: Condition):\n    return group(\"conditional\", {\"condition\": condition.to_json()})\n\n\ndef if_enum_group(enum: int, condition: EnumValues):\n    return if_group(Condition.enum(enum, condition))\n\n\ndef required(condition: Condition | None = None):\n    \"\"\"\n    Given generic inputs (meaning of kind \"generic\") that are optional, this group marks them as\n    being required under the given condition. If no condition is given, `True` will be used.\n\n    In addition to the given condition, if the require group is nested within conditional group\n    (`if_group` and derivatives), then the conditions of all ancestor conditional groups must also\n    be met.\n\n    Note that this group only guarantees **best effort**. It cannot guarantee that the optional\n    input is going to have a value if the condition is met. You must always check `None`.\n\n    Example:\n    ```py\n    if_group(someCondition)(\n        required()(\n            GenericInput(\"Foo\").make_optional(),\n        )\n    )\n    ```\n\n    In this example, the input \"Foo\" is required if and only if the input is visible (by virtue of\n    the parent conditional group).\n    \"\"\"\n\n    if condition is None:\n        condition = Condition.const(True)\n    return group(\"required\", {\"condition\": condition.to_json()})\n\n\ndef seed_group(seed_input: BaseInput):\n    \"\"\"\n    This groups is a wrapper around the `SeedInput`. It changes its visual appearance and adds a\n    little button for users to click on to generate a new seed.\n\n    All `SeedInput`s must be wrapped in this group.\n\n    Example:\n    ```py\n    seed_group(SeedInput())\n    ```\n    \"\"\"\n    return group(\"seed\")(seed_input)\n\n\ndef optional_list_group(*inputs: BaseInput | NestedGroup):\n    \"\"\"\n    This groups wraps around optional inputs and displays them as a list.\n\n    This can be used to create nodes that have a variable number of inputs. The user will initially\n    see no inputs, but can add as many inputs as the group contains. While not true varargs, this\n    can be used to create a similar effect.\n\n    See the Text Append node for an example.\n    \"\"\"\n    return group(\"optional-list\")(*inputs)\n\n\ndef linked_inputs_group(*inputs: BaseInput):\n    \"\"\"\n    This group wraps around inputs of the same type. It ensures that all inputs have the same\n    value.\n\n    \"The same type\" here not only refers to the Navi type of those inputs. All possible values\n    from all inputs must also be valid values for all other inputs. This typically necessitates\n    that the inputs are of the same class and use the same parameters.\n    \"\"\"\n    return group(\"linked-inputs\")(*inputs)\n\n\ndef ncnn_file_inputs_group(param_input: BaseInput, bin_input: BaseInput):\n    \"\"\"\n    This group wraps around 2 .param and .bin file inputs and synchronizes them in the UI.\n    \"\"\"\n    return group(\"ncnn-file-inputs\")(param_input, bin_input)\n\n\ndef from_to_dropdowns_group(from_dd: BaseInput, to_dd: BaseInput):\n    \"\"\"\n    This group wraps around 2 dropdown inputs that will be displayed as\n    `[From] -> [To]` in the UI.\n    \"\"\"\n    return group(\"from-to-dropdowns\")(from_dd, to_dd)\n\n\ndef icon_set_group(label: str):\n    \"\"\"\n    This group causes the given boolean inputs to be displayed as a set of icons instead of\n    checkboxes. The icons are specified by the `icons` parameter.\n    \"\"\"\n    return group(\"icon-set\", {\"label\": label})\n\n\ndef menu_icon_row_group():\n    \"\"\"\n    This group displays multiple icon-only inputs and groups as a row of icons. Only a few inputs\n    and groups are supported.\n\n    This group is intended to be used in the \"Text As Image\" node.\n    \"\"\"\n    return group(\"menu-icon-row\")\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/__init__.py",
    "content": ""
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/blend.py",
    "content": "from enum import Enum\n\nimport cv2\nimport numpy as np\n\nfrom ..utils.utils import get_h_w_c\nfrom .image_utils import as_target_channels, normalize, to_uint8\n\n\nclass BlendMode(Enum):\n    NORMAL = 0\n    DARKEN = 2\n    MULTIPLY = 1\n    COLOR_BURN = 5\n    LINEAR_BURN = 22\n    LIGHTEN = 3\n    SCREEN = 12\n    COLOR_DODGE = 6\n    ADD = 4\n    OVERLAY = 9\n    SOFT_LIGHT = 17\n    HARD_LIGHT = 18\n    VIVID_LIGHT = 19\n    LINEAR_LIGHT = 20\n    PIN_LIGHT = 21\n    REFLECT = 7\n    GLOW = 8\n    DIFFERENCE = 10\n    EXCLUSION = 16\n    NEGATION = 11\n    SUBTRACT = 14\n    DIVIDE = 15\n    XOR = 13\n\n\n__normalized = {\n    BlendMode.NORMAL: True,\n    BlendMode.MULTIPLY: True,\n    BlendMode.DARKEN: True,\n    BlendMode.LIGHTEN: True,\n    BlendMode.ADD: False,\n    BlendMode.COLOR_BURN: False,\n    BlendMode.COLOR_DODGE: False,\n    BlendMode.REFLECT: False,\n    BlendMode.GLOW: False,\n    BlendMode.OVERLAY: True,\n    BlendMode.DIFFERENCE: True,\n    BlendMode.NEGATION: True,\n    BlendMode.SCREEN: True,\n    BlendMode.XOR: True,\n    BlendMode.SUBTRACT: False,\n    BlendMode.DIVIDE: False,\n    BlendMode.EXCLUSION: True,\n    BlendMode.SOFT_LIGHT: True,\n    BlendMode.HARD_LIGHT: True,\n    BlendMode.VIVID_LIGHT: False,\n    BlendMode.LINEAR_LIGHT: False,\n    BlendMode.PIN_LIGHT: True,\n    BlendMode.LINEAR_BURN: False,\n}\n\n\ndef blend_mode_normalized(blend_mode: BlendMode) -> bool:\n    \"\"\"\n    Returns whether the given blend mode is guaranteed to produce normalized results (value between 0 and 1).\n    \"\"\"\n    return __normalized.get(blend_mode, False)\n\n\nclass ImageBlender:\n    \"\"\"Class for compositing images using different blending modes.\"\"\"\n\n    def __init__(self) -> None:\n        self.modes = {\n            BlendMode.NORMAL: self.__normal,\n            BlendMode.MULTIPLY: self.__multiply,\n            BlendMode.DARKEN: self.__darken,\n            BlendMode.LIGHTEN: self.__lighten,\n            BlendMode.ADD: self.__add,\n            BlendMode.COLOR_BURN: self.__color_burn,\n            BlendMode.COLOR_DODGE: self.__color_dodge,\n            BlendMode.REFLECT: self.__reflect,\n            BlendMode.GLOW: self.__glow,\n            BlendMode.OVERLAY: self.__overlay,\n            BlendMode.DIFFERENCE: self.__difference,\n            BlendMode.NEGATION: self.__negation,\n            BlendMode.SCREEN: self.__screen,\n            BlendMode.XOR: self.__xor,\n            BlendMode.SUBTRACT: self.__subtract,\n            BlendMode.DIVIDE: self.__divide,\n            BlendMode.EXCLUSION: self.__exclusion,\n            BlendMode.SOFT_LIGHT: self.__soft_light,\n            BlendMode.HARD_LIGHT: self.__hard_light,\n            BlendMode.VIVID_LIGHT: self.__vivid_light,\n            BlendMode.LINEAR_LIGHT: self.__linear_light,\n            BlendMode.PIN_LIGHT: self.__pin_light,\n            BlendMode.LINEAR_BURN: self.__linear_burn,\n        }\n\n    def apply_blend(\n        self, a: np.ndarray, b: np.ndarray, blend_mode: BlendMode\n    ) -> np.ndarray:\n        return self.modes[blend_mode](a, b)\n\n    def __normal(self, a: np.ndarray, _: np.ndarray) -> np.ndarray:\n        return a\n\n    def __multiply(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return a * b\n\n    def __darken(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return np.minimum(a, b)\n\n    def __lighten(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return np.maximum(a, b)\n\n    def __add(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return a + b\n\n    def __color_burn(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return np.where(\n            a == 0, 0, np.maximum(0, (1 - ((1 - b) / np.maximum(0.0001, a))))\n        )\n\n    def __color_dodge(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return np.where(a == 1, 1, np.minimum(1, b / np.maximum(0.0001, (1 - a))))\n\n    def __reflect(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return np.where(a == 1, 1, np.minimum(1, b * b / np.maximum(0.0001, 1 - a)))\n\n    def __glow(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return np.where(b == 1, 1, np.minimum(1, a * a / np.maximum(0.0001, 1 - b)))\n\n    def __overlay(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return np.where(b < 0.5, 2 * b * a, 1 - 2 * (1 - b) * (1 - a))\n\n    def __difference(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return np.asarray(cv2.absdiff(a, b))\n\n    def __negation(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return 1 - cv2.absdiff(1 - b, a)  # type: ignore\n\n    def __screen(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return a + b - (a * b)  # type: ignore\n\n    def __xor(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return normalize(\n            np.bitwise_xor(to_uint8(a, normalized=True), to_uint8(b, normalized=True))\n        )\n\n    def __subtract(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return b - a\n\n    def __divide(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return b / np.maximum(0.0001, a)\n\n    def __exclusion(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return a * (1 - b) + b * (1 - a)\n\n    def __soft_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        l = 2 * b * a + np.square(b) * (1 - 2 * a)\n        h = np.sqrt(b) * (2 * a - 1) + 2 * b * (1 - a)\n        return np.where(a <= 0.5, l, h)\n\n    def __hard_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return np.where(a <= 0.5, 2 * a * b, 1 - 2 * (1 - a) * (1 - b))\n\n    def __vivid_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return np.where(\n            a <= 0.5, self.__color_burn(2 * a, b), self.__color_dodge(2 * (a - 0.5), b)\n        )\n\n    def __linear_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return b + 2 * a - 1\n\n    def __pin_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        x = 2 * a\n        y = x - 1\n        return np.where(b < y, y, np.where(b > x, x, b))\n\n    def __linear_burn(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:\n        return a + b - 1\n\n\ndef blend_images(overlay: np.ndarray, base: np.ndarray, blend_mode: BlendMode):\n    \"\"\"\n    Changes the given image to the background overlayed with the image.\n\n    The 2 given images must be the same size and their values must be between 0 and 1.\n\n    The returned image is guaranteed to have values between 0 and 1.\n\n    If the 2 given images have a different number of channels, then the returned image\n    will have maximum of the two.\n\n    Only grayscale, RGB, and RGBA images are supported.\n    \"\"\"\n    o_shape = get_h_w_c(overlay)\n    b_shape = get_h_w_c(base)\n\n    assert (\n        o_shape[:2] == b_shape[:2]\n    ), \"The overlay and the base image must have the same size\"\n\n    def assert_sane(c: int, name: str) -> None:\n        sane = c in (1, 3, 4)\n        assert sane, f\"The {name} has to be a grayscale, RGB, or RGBA image\"\n\n    o_channels = o_shape[2]\n    b_channels = b_shape[2]\n\n    assert_sane(o_channels, \"overlay layer\")\n    assert_sane(b_channels, \"base layer\")\n\n    blender = ImageBlender()\n    target_c = max(o_channels, b_channels)\n    needs_clipping = not blend_mode_normalized(blend_mode)\n\n    if target_c == 4 and b_channels < 4:\n        base = as_target_channels(base, 3)\n\n        # The general algorithm below can be optimized because we know that b_a is 1\n        o_a = np.dstack((overlay[:, :, 3],) * 3)\n        o_rgb = overlay[:, :, :3]\n\n        blend_rgb = blender.apply_blend(o_rgb, base, blend_mode)\n        final_rgb = o_a * blend_rgb + (1 - o_a) * base  # type: ignore\n        if needs_clipping:\n            final_rgb = np.clip(final_rgb, 0, 1)\n\n        return as_target_channels(final_rgb, 4)\n\n    overlay = as_target_channels(overlay, target_c)\n    base = as_target_channels(base, target_c)\n\n    if target_c in (1, 3):\n        # We don't need to do any alpha blending, so the images can blended directly\n        result = blender.apply_blend(overlay, base, blend_mode)\n        if needs_clipping:\n            result = np.clip(result, 0, 1)\n        return result\n\n    # do the alpha blending for RGBA\n    o_a = overlay[:, :, 3]\n    b_a = base[:, :, 3]\n    o_rgb = overlay[:, :, :3]\n    b_rgb = base[:, :, :3]\n\n    final_a = 1 - (1 - o_a) * (1 - b_a)\n\n    blend_strength = o_a * b_a\n    o_strength = o_a - blend_strength  # type: ignore\n    b_strength = b_a - blend_strength  # type: ignore\n\n    blend_rgb = blender.apply_blend(o_rgb, b_rgb, blend_mode)\n\n    final_rgb = (\n        (np.dstack((o_strength,) * 3) * o_rgb)\n        + (np.dstack((b_strength,) * 3) * b_rgb)\n        + (np.dstack((blend_strength,) * 3) * blend_rgb)\n    )\n    final_rgb /= np.maximum(np.dstack((final_a,) * 3), 0.0001)  # type: ignore\n    final_rgb = np.clip(final_rgb, 0, 1)\n\n    result = np.concatenate([final_rgb, np.expand_dims(final_a, axis=2)], axis=2)\n    if needs_clipping:\n        result = np.clip(result, 0, 1)\n    return result\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/color/__init__.py",
    "content": ""
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/color/color.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom collections.abc import Iterable\nfrom typing import Literal, TypedDict, Union, cast\n\nimport numpy as np\nfrom nodes.utils.utils import get_h_w_c\n\nFloatLike = Union[np.floating, float]\n\n\ndef _norm(n: FloatLike) -> float:\n    return max(0, min(float(n), 1))\n\n\nColorJsonKind = Literal[\"grayscale\", \"rgb\", \"rgba\"]\n\n\nclass ColorJson(TypedDict):\n    kind: ColorJsonKind\n    values: list[float]\n\n\nclass Color:\n    def __init__(self, value: tuple[float, ...]) -> None:\n        assert len(value) >= 1\n        self.value: tuple[float, ...] = value\n\n    @property\n    def channels(self) -> int:\n        return len(self.value)\n\n    @staticmethod\n    def gray(gray: FloatLike) -> Color:\n        return Color((_norm(gray),))\n\n    @staticmethod\n    def bgr(value: Iterable[FloatLike]) -> Color:\n        t = tuple(map(_norm, value))\n        assert len(t) == 3\n        return Color(t)\n\n    @staticmethod\n    def bgra(value: Iterable[FloatLike]) -> Color:\n        t = tuple(map(_norm, value))\n        assert len(t) == 4\n        return Color(t)\n\n    @staticmethod\n    def from_1x1_image(img: np.ndarray) -> Color:\n        h, w, c = get_h_w_c(img)\n        assert h == w == 1\n\n        if c == 1:\n            return Color.gray(img.flat[0])\n        elif c == 3:\n            return Color.bgr(img.flat)\n        elif c == 4:\n            return Color.bgra(img.flat)\n        else:\n            raise AssertionError(\"Only grayscale, RGB, and RGBA colors are supported.\")\n\n    @staticmethod\n    def from_json(color_json: ColorJson | str) -> Color:\n        if isinstance(color_json, str):\n            color_json = cast(ColorJson, json.loads(color_json))\n        kind = color_json[\"kind\"]\n        values = color_json[\"values\"]\n\n        if kind == \"grayscale\":\n            assert len(values) == 1\n            return Color.gray(values[0])\n        elif kind == \"rgb\":\n            assert len(values) == 3\n            return Color.bgr([values[2], values[1], values[0]])\n        elif kind == \"rgba\":\n            assert len(values) == 4\n            return Color.bgra([values[2], values[1], values[0], values[3]])\n        else:\n            raise AssertionError(f\"Unknown color kind {kind}\")\n\n    def to_1x1_image(self) -> np.ndarray:\n        return self.to_image(1, 1)\n\n    def to_image(self, width: int, height: int) -> np.ndarray:\n        v = self.value\n        if len(v) == 1:\n            return np.full((height, width), v[0], dtype=np.float32)\n        else:\n            return np.full((height, width, len(v)), v, dtype=np.float32)\n\n    def to_json(self) -> ColorJson:\n        values = list(self.value)\n        kind: ColorJsonKind\n        if len(values) == 1:\n            kind = \"grayscale\"\n        elif len(values) == 3:\n            kind = \"rgb\"\n            values = [values[2], values[1], values[0]]\n        elif len(values) == 4:\n            kind = \"rgba\"\n            values = [values[2], values[1], values[0], values[3]]\n        else:\n            raise AssertionError(f\"Colors with {len(values)} are not supported.\")\n\n        return {\"kind\": kind, \"values\": values}\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/color/convert.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable, Iterable\nfrom dataclasses import dataclass, field\nfrom typing import Generic, TypeVar\n\nimport numpy as np\nfrom sanic.log import logger\n\nfrom .convert_data import color_spaces, color_spaces_or_detectors, conversions\nfrom .convert_model import (\n    ColorSpace,\n    ColorSpaceDetector,\n    Conversion,\n    assert_input_channels,\n    assert_output_channels,\n)\n\n\ndef color_space_from_id(id_: int) -> ColorSpace:\n    for c in color_spaces:\n        if c.id == id_:\n            return c\n    raise ValueError(f\"There is no color space with the id {id_}.\")\n\n\ndef color_space_or_detector_from_id(id_: int) -> ColorSpace | ColorSpaceDetector:\n    for c in color_spaces_or_detectors:\n        if c.id == id_:\n            return c\n    raise ValueError(f\"There is no color space with the id {id_}.\")\n\n\nT = TypeVar(\"T\")\n\n\n@dataclass(order=True)\nclass __ProcessingItem(Generic[T]):  # noqa: N801\n    cost: int\n    path: list[T] = field(compare=False)\n\n\ndef get_shortest_path(\n    start: T,\n    is_destination: Callable[[T], bool],\n    get_next: Callable[[T], Iterable[tuple[int, T]]],\n) -> list[T] | None:\n    \"\"\"A simple implementation of Dijkstra's\"\"\"\n\n    processed: set[T] = set()\n    front: dict[T, __ProcessingItem] = {\n        start: __ProcessingItem(cost=0, path=[start]),\n    }\n\n    while len(front) > 0:\n        best = None\n        for x in front.values():\n            if best is None:\n                best = x\n            elif x.cost < best.cost:\n                best = x\n        assert best is not None\n\n        current = best.path[-1]\n        del front[current]\n        processed.add(current)\n\n        if is_destination(current):\n            return best.path\n\n        for cost, to in get_next(current):\n            total_cost = best.cost + cost\n            old = front.get(to, None)\n            if old is None:\n                if to not in processed:\n                    new_path = best.path.copy()\n                    new_path.append(to)\n                    front[to] = __ProcessingItem(cost=total_cost, path=new_path)\n            elif old.cost > total_cost:\n                old.cost = total_cost\n                old.path.clear()\n                old.path.extend(best.path)\n                old.path.append(to)\n\n\n__conversions_map: dict[ColorSpace, list[Conversion]] = {}\nfor conversion in conversions:\n    l = __conversions_map.get(conversion.input, [])\n    if len(l) == 0:\n        __conversions_map[conversion.input] = l\n    l.append(conversion)\n\n\ndef convert(\n    img: np.ndarray,\n    input_: ColorSpace | ColorSpaceDetector,\n    output: ColorSpace,\n) -> np.ndarray:\n    if isinstance(input_, ColorSpaceDetector):\n        input_ = input_.detect(img)\n\n    assert_input_channels(img, input_, output)\n\n    if input_ == output:\n        return img\n\n    path = get_shortest_path(\n        input_,\n        is_destination=lambda i: i == output,\n        get_next=lambda i: [(c.cost, c.output) for c in __conversions_map.get(i, [])],\n    )\n\n    if path is None:\n        raise ValueError(f\"Conversion {input_.name} -> {output.name} is not possible.\")\n\n    logger.debug(f\"Converting color using the path {' -> '.join(x.name for x in path)}\")\n\n    for i in range(1, len(path)):\n        curr_in = path[i - 1]\n        curr_out = path[i]\n\n        conv = None\n        for c in __conversions_map.get(curr_in, []):\n            if c.output == curr_out:\n                conv = c\n                break\n        assert conv is not None\n\n        img = conv.convert(img)\n\n    assert_output_channels(img, input_, output)\n    return img\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/color/convert_data.py",
    "content": "from __future__ import annotations\n\nimport math\n\nimport cv2\nimport numpy as np\n\nfrom ...utils.utils import get_h_w_c\nfrom .convert_model import ColorSpace, ColorSpaceDetector, Conversion\n\nGRAY = ColorSpace(0, \"Gray\", 1)\nRGB = ColorSpace(1, \"RGB\", 3)\nRGBA = ColorSpace(2, \"RGBA\", 4)\nYUV = ColorSpace(3, \"YUV\", 3)\nHSV = ColorSpace(4, \"HSV\", 3)\nHSL = ColorSpace(5, \"HSL\", 3)\nCMYK = ColorSpace(6, \"CMYK\", 4)\nYUVA = ColorSpace(7, \"YUVA\", 4)\nHSVA = ColorSpace(8, \"HSVA\", 4)\nHSLA = ColorSpace(9, \"HSLA\", 4)\nLAB = ColorSpace(10, \"L*a*b*\", 3)\nLABA = ColorSpace(11, \"L*a*b*A\", 4)\nLCH = ColorSpace(12, \"L*C*h°\", 3)\nLCHA = ColorSpace(13, \"L*C*h°A\", 4)\n\nRGB_LIKE = ColorSpaceDetector(1000, \"RGB\", [GRAY, RGB, RGBA])\nYUV_LIKE = ColorSpaceDetector(1001, \"YUV\", [YUV, YUVA])\nHSV_LIKE = ColorSpaceDetector(1002, \"HSV\", [HSV, HSVA])\nHSL_LIKE = ColorSpaceDetector(1003, \"HSL\", [HSL, HSLA])\nLAB_LIKE = ColorSpaceDetector(1004, \"L*a*b*\", [LAB, LABA])\nLCH_LIKE = ColorSpaceDetector(1005, \"L*C*h°\", [LCH, LCHA])\n\nALPHA_PAIRS: dict[ColorSpace, ColorSpace] = {\n    RGB: RGBA,\n    YUV: YUVA,\n    HSV: HSVA,\n    HSL: HSLA,\n    LAB: LABA,\n    LCH: LCHA,\n}\n\n\ndef is_alpha_partner(c: ColorSpace) -> bool:\n    \"\"\"\n    Whether this color space is returned by `get_alpha_partner` for some input.\n    \"\"\"\n    return c in ALPHA_PAIRS.values()\n\n\ndef get_alpha_partner(c: ColorSpace) -> ColorSpace | None:\n    \"\"\"\n    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.\n\n    E.g. RGB will return RGBA and RGBA will return None.\n    \"\"\"\n    return ALPHA_PAIRS.get(c, None)\n\n\ncolor_spaces: list[ColorSpace] = [\n    RGB,\n    RGBA,\n    GRAY,\n    YUV,\n    YUVA,\n    HSV,\n    HSVA,\n    HSL,\n    HSLA,\n    CMYK,\n    LAB,\n    LABA,\n    LCH,\n    LCHA,\n]\ncolor_spaces_or_detectors: list[ColorSpace | ColorSpaceDetector] = [\n    RGB_LIKE,\n    GRAY,\n    YUV_LIKE,\n    HSV_LIKE,\n    HSL_LIKE,\n    CMYK,\n    LAB_LIKE,\n    LCH_LIKE,\n]\n\n\ndef __rev3(image: np.ndarray) -> np.ndarray:\n    c = get_h_w_c(image)[2]\n    assert c == 3, \"Expected a 3-channel image\"\n    return np.stack([image[:, :, 2], image[:, :, 1], image[:, :, 0]], axis=2)\n\n\ndef __rgb_to_hsv(img: np.ndarray) -> np.ndarray:\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)\n    img[:, :, 0] /= 360  # type: ignore\n    return __rev3(img)\n\n\ndef __hsv_to_rgb(img: np.ndarray) -> np.ndarray:\n    img = __rev3(img)\n    img[:, :, 0] *= 360\n    return cv2.cvtColor(img, cv2.COLOR_HSV2BGR)\n\n\ndef __rgb_to_hsl(img: np.ndarray) -> np.ndarray:\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2HLS)\n    h = img[:, :, 0] / 360  # type: ignore\n    l = img[:, :, 1]\n    s = img[:, :, 2]\n    return cv2.merge((l, s, h))\n\n\ndef __hsl_to_rgb(img: np.ndarray) -> np.ndarray:\n    h = img[:, :, 2] * 360\n    s = img[:, :, 1]\n    l = img[:, :, 0]\n    return cv2.cvtColor(cv2.merge((h, l, s)), cv2.COLOR_HLS2BGR)\n\n\ndef __hsv_to_hsl(img: np.ndarray) -> np.ndarray:\n    # the S and HSV and HSL are different, only the H is the same\n    h = img[:, :, 2]\n    hls = cv2.cvtColor(__hsv_to_rgb(img), cv2.COLOR_BGR2HLS)\n    l = hls[:, :, 1]\n    s = hls[:, :, 2]\n    return cv2.merge((l, s, h))\n\n\ndef __hsl_to_hsv(img: np.ndarray) -> np.ndarray:\n    # the S and HSV and HSL are different, only the H is the same\n    h = img[:, :, 2]\n    hsv = cv2.cvtColor(__hsl_to_rgb(img), cv2.COLOR_BGR2HSV)\n    s = hsv[:, :, 1]\n    v = hsv[:, :, 2]\n    return cv2.merge((v, s, h))\n\n\ndef __rgb_to_cmyk(img: np.ndarray) -> np.ndarray:\n    b, g, r = img[:, :, 0], img[:, :, 1], img[:, :, 2]\n    maximum = np.max(img, axis=2)\n    soft_max = np.maximum(maximum, 0.001)\n    c = 1 - r / soft_max\n    m = 1 - g / soft_max\n    y = 1 - b / soft_max\n    k = 1 - maximum\n    return cv2.merge((y, m, c, k))\n\n\ndef __cmyk_to_rgb(img: np.ndarray) -> np.ndarray:\n    y, m, c, k = img[:, :, 0], img[:, :, 1], img[:, :, 2], img[:, :, 3]\n    maximum = 1 - k\n    r = (1 - c) * maximum\n    g = (1 - m) * maximum\n    b = (1 - y) * maximum\n    return cv2.merge((b, g, r))\n\n\ndef __rgb_to_lab(img: np.ndarray) -> np.ndarray:\n    # 0≤L≤100 , -127≤a≤127, -127≤b≤127\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)\n    l = img[:, :, 0] / 100  # type: ignore\n    a = (img[:, :, 1] + 127) / 254  # type: ignore\n    b = (img[:, :, 2] + 127) / 254  # type: ignore\n    return cv2.merge((b, a, l))\n\n\ndef __lab_to_rgb(img: np.ndarray) -> np.ndarray:\n    # 0≤L≤100 , -127≤a≤127, -127≤b≤127\n    l = img[:, :, 2] * 100\n    a = img[:, :, 1] * 254 - 127\n    b = img[:, :, 0] * 254 - 127\n    return cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR)\n\n\ndef __lab_to_lch(img: np.ndarray) -> np.ndarray:\n    l = img[:, :, 2]\n    # a and b must be centered at 0\n    a = img[:, :, 1] - 0.5\n    b = img[:, :, 0] - 0.5\n\n    c = np.hypot(a, b)\n    h = np.arctan2(b, a)\n\n    # normalize C and h to [0,1]\n    #\n    # This is quite simple for h, but not so much for C. The problem is that\n    # the maximum value of C depends on h. This is because a*,b* in [-1,1] form\n    # a square of possible value around the origin. C nd h are simple polar\n    # coordinates where h is the angel and C is the distance from origin. Since\n    # the corners of a square are farther away from the origin than the mid\n    # point of the sides of the a square, the maximum value of C depends on the\n    # angle. E.g. for h=0, the maximum C value is 0.5 and for h=45°, it's\n    # sqrt(0.5).\n    #\n    # One strategy would be simple use the maximum value for all possible h\n    # values (which is sqrt(0.5)), but this has the problem that it is now\n    # possible to create C,h value pairs that create invalid a*,b* values.\n    # Ideally, all possible values C,h values should map to valid a*,b* values.\n    #\n    # To solve this problem, we calculate the maximum C value for the current\n    # h angle and use that to normalize C. `Cmax` for a,b in [-1,1] is defined\n    # as follows: `Cmax = 1/max(abs(cos(h)), abs(sin(h)))`. Since, we use a,b\n    # in [-0.5,0.5], we just have to divide that value by 2.\n    c_max = 0.5 / np.maximum(np.abs(np.sin(h)), np.abs(np.cos(h)))\n    c = c / c_max\n    h = h / (math.pi * 2) + 0.5\n\n    return cv2.merge((h, c, l))\n\n\ndef __lch_to_lab(img: np.ndarray) -> np.ndarray:\n    l = img[:, :, 2]\n\n    # undo the c and h [0,1] normalization\n    h = (img[:, :, 0] - 0.5) * (math.pi * 2)\n    sin_h = np.sin(h)\n    cos_h = np.cos(h)\n    c_max = 0.5 / np.maximum(np.abs(sin_h), np.abs(cos_h))\n    c = img[:, :, 1] * c_max\n\n    a = c * cos_h + 0.5\n    b = c * sin_h + 0.5\n    return cv2.merge((b, a, l))\n\n\n# The conversion loses one channel of information (e.g. the alpha channel, or a color channel)\n__CHANNEL_LOST = 1000\n# The conversion loses hue/chroma information in certain edge cases\n__CHROMA_LOST = 100\n\n\nconversions: list[Conversion] = [\n    # RGB and grayscale\n    Conversion(\n        direction=(RGB, GRAY),\n        convert=lambda i: cv2.cvtColor(i, cv2.COLOR_BGR2GRAY),\n        cost=__CHANNEL_LOST * 2,\n    ),\n    Conversion(\n        direction=(GRAY, RGB),\n        convert=lambda i: cv2.cvtColor(i, cv2.COLOR_GRAY2BGR),\n    ),\n    Conversion(\n        direction=(RGBA, GRAY),\n        convert=lambda i: cv2.cvtColor(i, cv2.COLOR_BGRA2GRAY),\n        cost=__CHANNEL_LOST * 3,\n    ),\n    Conversion(\n        direction=(GRAY, RGBA),\n        convert=lambda i: cv2.cvtColor(i, cv2.COLOR_GRAY2BGRA),\n    ),\n    # YUV\n    Conversion(\n        direction=(RGB, YUV),\n        convert=lambda i: __rev3(cv2.cvtColor(i, cv2.COLOR_BGR2YUV)),\n    ),\n    Conversion(\n        direction=(YUV, RGB),\n        convert=lambda i: np.clip(cv2.cvtColor(__rev3(i), cv2.COLOR_YUV2BGR), 0, 1),\n        cost=__CHROMA_LOST,\n    ),\n    # HSV/HSL\n    Conversion(\n        direction=(RGB, HSV),\n        convert=__rgb_to_hsv,\n    ),\n    Conversion(\n        direction=(HSV, RGB),\n        convert=__hsv_to_rgb,\n        cost=__CHROMA_LOST,\n    ),\n    Conversion(\n        direction=(RGB, HSL),\n        convert=__rgb_to_hsl,\n    ),\n    Conversion(\n        direction=(HSL, RGB),\n        convert=__hsl_to_rgb,\n        cost=__CHROMA_LOST,\n    ),\n    Conversion(\n        direction=(HSV, HSL),\n        convert=__hsv_to_hsl,\n    ),\n    Conversion(\n        direction=(HSL, HSV),\n        convert=__hsl_to_hsv,\n    ),\n    # CMYK\n    Conversion(\n        direction=(RGB, CMYK),\n        convert=__rgb_to_cmyk,\n    ),\n    Conversion(\n        direction=(CMYK, RGB),\n        convert=__cmyk_to_rgb,\n        cost=__CHROMA_LOST,\n    ),\n    # LAB\n    Conversion(\n        direction=(RGB, LAB),\n        convert=__rgb_to_lab,\n    ),\n    Conversion(\n        direction=(LAB, RGB),\n        convert=__lab_to_rgb,\n    ),\n    # LCH\n    Conversion(\n        direction=(LAB, LCH),\n        convert=__lab_to_lch,\n    ),\n    Conversion(\n        direction=(LCH, LAB),\n        convert=__lch_to_lab,\n        cost=__CHROMA_LOST,\n    ),\n]\n\n\n# Add conversions that can be generated because only alpha is different\nfor dir_3, dir_4 in ALPHA_PAIRS.items():\n    assert dir_3.channels == 3\n    assert dir_4.channels == 4\n\n    # Add and remove the alpha channel\n    conversions.append(\n        Conversion(\n            direction=(dir_3, dir_4),\n            convert=lambda i: cv2.cvtColor(i, cv2.COLOR_BGR2BGRA),\n        )\n    )\n    conversions.append(\n        Conversion(\n            direction=(dir_4, dir_3),\n            convert=lambda i: cv2.cvtColor(i, cv2.COLOR_BGRA2BGR),\n            cost=__CHANNEL_LOST,\n        )\n    )\n\n__ALPHA_3_to_4 = dict(ALPHA_PAIRS)\nfor conv in list(conversions):\n    in_4 = __ALPHA_3_to_4.get(conv.input)\n    out_4 = __ALPHA_3_to_4.get(conv.output)\n\n    if in_4 is not None and out_4 is not None:\n        # if we have a conversion X -> Y, then we can generate XA -> YA\n        # e.g. RGBA -> HSVA can be generated from RGB -> HSV\n\n        def create_convert(old_conv: Conversion):\n            def convert(img: np.ndarray) -> np.ndarray:\n                color = img[:, :, :3]\n                alpha = img[:, :, 3]\n                return np.dstack((old_conv.convert(color), alpha))\n\n            return convert\n\n        conversions.append(\n            Conversion(\n                direction=(in_4, out_4),\n                convert=create_convert(conv),\n                cost=conv.cost,\n            )\n        )\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/color/convert_model.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable, Iterable\n\nimport numpy as np\n\nfrom ...utils.format import format_image_with_channels\nfrom ...utils.utils import get_h_w_c\n\n\nclass ColorSpace:\n    def __init__(self, id_: int, name: str, channels: int) -> None:\n        assert 0 <= id_ and id_ < 256\n        self.id = id_\n        self.name = name\n        self.channels = channels\n\n\nclass ColorSpaceDetector:\n    def __init__(self, id_: int, name: str, color_spaces: Iterable[ColorSpace]) -> None:\n        assert 1000 <= id_ and id_ < 2000\n        self.id = id_\n        self.name = name\n        self.channel_map: dict[int, ColorSpace] = {}\n        for cs in color_spaces:\n            assert cs.channels not in self.channel_map\n            self.channel_map[cs.channels] = cs\n        self.channels = list(self.channel_map.keys())\n\n    def detect(self, image: np.ndarray) -> ColorSpace:\n        c = get_h_w_c(image)[2]\n        cs = self.channel_map.get(c, None)\n        if cs is not None:\n            return cs\n\n        raise ValueError(\n            f\"Expected the input image for {self.name}\"\n            f\" to be {format_image_with_channels(self.channels)}\"\n            f\" but found {format_image_with_channels([c])}.\"\n        )\n\n\ndef assert_input_channels(\n    img: np.ndarray, input_: ColorSpace, output: ColorSpace\n) -> None:\n    c = get_h_w_c(img)[2]\n    if c != input_.channels:\n        raise ValueError(\n            f\"Expected the input image for a {input_.name} -> {output.name} conversion\"\n            f\" to be {format_image_with_channels([input_.channels])}\"\n            f\" but found {format_image_with_channels([c])}.\"\n        )\n\n\ndef assert_output_channels(\n    result: np.ndarray, input_: ColorSpace, output: ColorSpace\n) -> None:\n    c = get_h_w_c(result)[2]\n    if c != output.channels:\n        raise ValueError(\n            f\"Expected the output image for a {input_.name} -> {output.name} conversion\"\n            f\" to be {format_image_with_channels([output.channels])}\"\n            f\" but found {format_image_with_channels([c])}.\"\n            f\" This is an internal implementation error.\"\n            f\" Please report this as a bug.\"\n        )\n\n\nConvertFn = Callable[[np.ndarray], np.ndarray]\n\n\nclass Conversion:\n    def __init__(\n        self,\n        direction: tuple[ColorSpace, ColorSpace],\n        convert: ConvertFn,\n        cost: int = 1,\n    ) -> None:\n        input_, output = direction\n        assert input_ != output\n        self.input: ColorSpace = input_\n        self.output: ColorSpace = output\n        self.__convert = convert\n        assert cost >= 1\n        self.cost: int = cost\n\n    def convert(self, img: np.ndarray) -> np.ndarray:\n        assert_input_channels(img, self.input, self.output)\n        result = self.__convert(img)\n        assert_output_channels(result, self.input, self.output)\n        return result\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/image_formats.py",
    "content": "def get_opencv_formats():\n    return [\n        # Bitmaps\n        \".bmp\",\n        \".dib\",\n        # JPEG\n        \".jpg\",\n        \".jpeg\",\n        \".jpe\",\n        \".jp2\",\n        # PNG, WebP, Tiff\n        \".png\",\n        \".webp\",\n        \".tif\",\n        \".tiff\",\n        # Portable image format\n        \".pbm\",\n        \".pgm\",\n        \".ppm\",\n        \".pxm\",\n        \".pnm\",\n        # Sun Rasters\n        \".sr\",\n        \".ras\",\n        # OpenEXR\n        \".exr\",\n        # Radiance HDR\n        \".hdr\",\n        \".pic\",\n    ]\n\n\ndef get_pil_formats():\n    return [\n        # Bitmaps\n        \".bmp\",\n        \".dib\",\n        \".xbm\",\n        # DDS\n        \".dds\",\n        # EPS\n        \".eps\",\n        # GIF\n        # \".gif\",\n        # Icons\n        \".icns\",\n        \".ico\",\n        # JPEG\n        \".jpg\",\n        \".jpeg\",\n        \".jfif\",\n        \".jp2\",\n        \".jpx\",\n        # Randoms\n        \".msp\",\n        \".pcx\",\n        \".sgi\",\n        # PNG, WebP, TIFF\n        \".png\",\n        \".webp\",\n        \".tiff\",\n        # APNG\n        # \".apng\",\n        # Portable image format\n        \".pbm\",\n        \".pgm\",\n        \".ppm\",\n        \".pnm\",\n        # TGA\n        \".tga\",\n        # AVIF,\n        \".avif\",\n    ]\n\n\ndef get_available_image_formats():\n    all_formats = [*get_opencv_formats(), *get_pil_formats()]\n    return sorted(set(all_formats))\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/image_op.py",
    "content": "from collections.abc import Callable\nfrom typing import Concatenate\n\nimport numpy as np\nfrom typing_extensions import ParamSpec\n\nImageOp = Callable[[np.ndarray], np.ndarray]\n\"\"\"\nAn image processing operation that takes an image and produces a new image.\n\nThe given image is guaranteed to *not* be modified.\n\"\"\"\n\n\ndef clipped(op: ImageOp) -> ImageOp:\n    \"\"\"\n    Ensures that all values in the returned image are between 0 and 1.\n    \"\"\"\n    return lambda i: np.clip(op(i), 0, 1)\n\n\nP = ParamSpec(\"P\")\n\n\ndef to_op(fn: Callable[Concatenate[np.ndarray, P], np.ndarray]) -> Callable[P, ImageOp]:\n    \"\"\"\n    Applies a form of currying to convert the given function into a constructor for an image operation.\n\n    Example: Simple resize method could be defined as follows: `resize(np.ndarray, Size2D) -> np.ndarray`.\n    It takes an image and its new size and returns the resized image.\n    If we want to convert it to an image operation, we have to create a function with the following signature: `resize_op(Size2D) -> ImageOp`.\n    The implementation of this function would be rather simple, it would simply take all arguments of `resize` except for the image like this:\n    ```py\n    def resize_op(size: Size2D) -> ImageOp:\n        return lambda img: resize(img, size)\n    ```\n    `to_op` does exactly this transformation, but for any number of arguments.\n\n    Note: This only works if the input image is the first argument of the given function.\n    \"\"\"\n\n    def p(*args: P.args, **kwargs: P.kwargs) -> ImageOp:\n        return lambda i: fn(i, *args, **kwargs)\n\n    return p\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/image_utils.py",
    "content": "from __future__ import annotations\n\nimport itertools\nimport math\nfrom enum import Enum\nfrom pathlib import Path\n\nimport cv2\nimport numpy as np\n\nfrom ..utils.utils import Padding, get_h_w_c, split_file_path\nfrom .color.color import Color\n\nMAX_VALUES_BY_DTYPE = {\n    np.dtype(\"int8\").name: 127,\n    np.dtype(\"uint8\").name: 255,\n    np.dtype(\"int16\").name: 32767,\n    np.dtype(\"uint16\").name: 65535,\n    np.dtype(\"int32\").name: 2147483647,\n    np.dtype(\"uint32\").name: 4294967295,\n    np.dtype(\"int64\").name: 9223372036854775807,\n    np.dtype(\"uint64\").name: 18446744073709551615,\n    np.dtype(\"float32\").name: 1.0,\n    np.dtype(\"float64\").name: 1.0,\n}\n\n\nclass FillColor(Enum):\n    AUTO = -1\n    BLACK = 0\n    TRANSPARENT = 1\n\n    def get_color(self, channels: int):\n        \"\"\"Select how to fill negative space that results from rotation\"\"\"\n\n        if self == FillColor.AUTO:\n            fill_color = (0,) * channels\n        elif self == FillColor.BLACK:\n            fill_color = (0,) * channels if channels < 4 else (0, 0, 0, 1)\n        else:\n            fill_color = (0, 0, 0, 0)\n\n        return fill_color\n\n\nclass FlipAxis(Enum):\n    HORIZONTAL = 1\n    VERTICAL = 0\n    BOTH = -1\n    NONE = 2\n\n    def flip(self, img: np.ndarray) -> np.ndarray:\n        if self == FlipAxis.NONE:\n            return img\n        return cv2.flip(img, self.value)\n\n\nclass BorderType(Enum):\n    REFLECT_MIRROR = 4\n    WRAP = 3\n    REPLICATE = 1\n    BLACK = 0\n    WHITE = 6\n    TRANSPARENT = 5\n    CUSTOM_COLOR = 7\n\n\nclass NormalMapType(Enum):\n    DIRECTX = \"DirectX\"\n    OPENGL = \"OpenGL\"\n    OCTAHEDRAL = \"Octahedral\"\n\n\ndef convert_to_bgra(img: np.ndarray, in_c: int) -> np.ndarray:\n    assert in_c in (1, 3, 4), f\"Number of channels ({in_c}) unexpected\"\n    if in_c == 1:\n        img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGRA)\n    elif in_c == 3:\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)\n\n    return img.copy()\n\n\ndef _get_iinfo(img: np.ndarray) -> np.iinfo | None:\n    try:\n        return np.iinfo(img.dtype)\n    except Exception:\n        return None\n\n\ndef normalize(img: np.ndarray) -> np.ndarray:\n    if img.dtype != np.float32:\n        info = _get_iinfo(img)\n        img = img.astype(np.float32)\n\n        if info is not None:\n            img /= info.max\n            if info.min == 0:\n                # we don't need to clip\n                return img\n\n        # we own `img`, so it's okay to write to it\n        return np.clip(img, 0, 1, out=img)\n\n    return np.clip(img, 0, 1)\n\n\ndef to_uint8(img: np.ndarray, normalized: bool = False) -> np.ndarray:\n    \"\"\"\n    Returns a new uint8 image with the given image data.\n\n    If `normalized` is `False`, then the image will be normalized before being converted to uint8.\n    \"\"\"\n    if img.dtype == np.uint8:\n        return img.copy()\n\n    if not normalized or img.dtype != np.float32:\n        img = normalize(img)\n\n    return (img * 255).round().astype(np.uint8)\n\n\ndef to_uint16(img: np.ndarray, normalized: bool = False) -> np.ndarray:\n    \"\"\"\n    Returns a new uint16 image with the given image data.\n\n    If `normalized` is `False`, then the image will be normalized before being converted to uint16.\n    \"\"\"\n    if img.dtype == np.uint16:\n        return img.copy()\n\n    if not normalized or img.dtype != np.float32:\n        img = normalize(img)\n\n    return (img * 65535).round().astype(np.uint16)\n\n\nclass ShiftFill(Enum):\n    AUTO = -1\n    BLACK = 0\n    TRANSPARENT = 1\n    WRAP = 2\n\n    def to_fill_color(self) -> FillColor:\n        if self == ShiftFill.AUTO:\n            return FillColor.AUTO\n        elif self == ShiftFill.BLACK:\n            return FillColor.BLACK\n        elif self == ShiftFill.TRANSPARENT:\n            return FillColor.TRANSPARENT\n        raise ValueError(f\"Cannot get color for {self}\")\n\n\ndef shift(\n    img: np.ndarray, amount_x: int, amount_y: int, shift_fill: ShiftFill\n) -> np.ndarray:\n    h, w, c = get_h_w_c(img)\n\n    if shift_fill == ShiftFill.WRAP:\n        amount_x %= w\n        amount_y %= h\n\n        if amount_x != 0:\n            img = np.roll(img, amount_x, axis=1)\n        if amount_y != 0:\n            img = np.roll(img, amount_y, axis=0)\n\n        return img\n\n    fill = shift_fill.to_fill_color()\n    if fill == FillColor.TRANSPARENT:\n        img = convert_to_bgra(img, c)\n    fill_color = fill.get_color(c)\n\n    h, w, _ = get_h_w_c(img)\n    translation_matrix = np.asarray(\n        [[1, 0, amount_x], [0, 1, amount_y]], dtype=np.float32\n    )\n    img = cv2.warpAffine(\n        img,\n        translation_matrix,\n        (w, h),\n        borderMode=cv2.BORDER_CONSTANT,\n        borderValue=fill_color,\n    )\n\n    return img\n\n\ndef as_2d_grayscale(img: np.ndarray) -> np.ndarray:\n    \"\"\"Given a grayscale image, this returns an image with 2 dimensions (image.ndim == 2).\"\"\"\n    if img.ndim == 2:\n        return img\n    if img.ndim == 3 and img.shape[2] == 1:\n        return img[:, :, 0]\n    raise AssertionError(f\"Invalid image shape {img.shape}\")\n\n\ndef as_3d(img: np.ndarray) -> np.ndarray:\n    \"\"\"Given a grayscale image, this returns an image with 3 dimensions (image.ndim == 3).\"\"\"\n    if img.ndim == 2:\n        return np.expand_dims(img.copy(), axis=2)\n    return img\n\n\ndef as_target_channels(\n    img: np.ndarray, target_c: int, narrowing: bool = False\n) -> np.ndarray:\n    \"\"\"\n    Given a number of target channels (either 1, 3, or 4), this convert the given image\n    to an image with that many channels. If the given image already has the correct\n    number of channels, it will be returned as is.\n\n    Narrowing conversions are only supported if narrowing is True.\n    \"\"\"\n    c = get_h_w_c(img)[2]\n\n    if c == target_c == 1:\n        return as_2d_grayscale(img)\n    if c == target_c:\n        return img\n\n    if not narrowing:\n        assert (\n            c < target_c\n        ), f\"Narrowing is false, image channels ({c}) must be less than target channels ({target_c})\"\n\n    if c == 1:\n        if target_c == 3:\n            return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)\n        if target_c == 4:\n            return cv2.cvtColor(img, cv2.COLOR_GRAY2BGRA)\n\n    if c == 3:\n        if target_c == 1:\n            return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n        if target_c == 4:\n            return cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)\n\n    if c == 4:\n        if target_c == 1:\n            return cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)\n        if target_c == 3:\n            return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)\n\n    raise ValueError(f\"Unable to convert {c} channel image to {target_c} channel image\")\n\n\ndef create_border(\n    img: np.ndarray,\n    border_type: BorderType,\n    border: Padding,\n    color: Color | None = None,\n) -> np.ndarray:\n    \"\"\"\n    Returns a new image with a specified border.\n    \"\"\"\n\n    if border.empty:\n        return img\n\n    _, _, c = get_h_w_c(img)\n    if c == 4 and border_type == BorderType.BLACK:\n        value = (0.0, 0.0, 0.0, 1.0)\n    else:\n        value = (0.0,)\n\n    cv_border_type: int = border_type.value\n    if border_type == BorderType.TRANSPARENT:\n        cv_border_type = cv2.BORDER_CONSTANT\n        value = (0.0,)\n        img = as_target_channels(img, 4)\n    elif border_type == BorderType.WHITE:\n        cv_border_type = cv2.BORDER_CONSTANT\n        value = (1.0,) * c\n    elif border_type == BorderType.CUSTOM_COLOR:\n        assert (\n            color is not None\n        ), \"Creating a border with a custom color requires supplying a custom color.\"\n\n        # widen image or color to make them compatible\n        if color.channels > c:\n            img = as_target_channels(img, color.channels)\n        elif c > color.channels:\n            color = Color.from_1x1_image(as_target_channels(color.to_1x1_image(), c))\n\n        cv_border_type = cv2.BORDER_CONSTANT\n        value = color.value\n\n    return cv2.copyMakeBorder(\n        img,\n        top=border.top,\n        left=border.left,\n        right=border.right,\n        bottom=border.bottom,\n        borderType=cv_border_type,\n        value=value,\n    )\n\n\ndef calculate_ssim(\n    img1: np.ndarray,\n    img2: np.ndarray,\n) -> float:\n    \"\"\"Calculates mean localized Structural Similarity Index (SSIM)\n    between two images.\"\"\"\n\n    c1 = 0.01**2\n    c2 = 0.03**2\n\n    kernel = cv2.getGaussianKernel(11, 1.5)\n    window = np.outer(kernel, kernel.transpose())  # type: ignore\n\n    mu1 = cv2.filter2D(img1, -1, window)[5:-5, 5:-5]\n    mu2 = cv2.filter2D(img2, -1, window)[5:-5, 5:-5]\n    mu1_sq = np.power(mu1, 2)\n    mu2_sq = np.power(mu2, 2)\n    mu1_mu2 = np.multiply(mu1, mu2)\n    sigma1_sq = cv2.filter2D(img1**2, -1, window)[5:-5, 5:-5] - mu1_sq\n    sigma2_sq = cv2.filter2D(img2**2, -1, window)[5:-5, 5:-5] - mu2_sq\n    sigma12 = cv2.filter2D(img1 * img2, -1, window)[5:-5, 5:-5] - mu1_mu2\n\n    ssim_map = ((2 * mu1_mu2 + c1) * (2 * sigma12 + c2)) / (\n        (mu1_sq + mu2_sq + c1) * (sigma1_sq + sigma2_sq + c2)\n    )\n\n    return float(np.mean(ssim_map))\n\n\ndef cv_save_image(path: Path | str, img: np.ndarray, params: list[int]) -> None:\n    \"\"\"\n    A light wrapper around `cv2.imwrite` to support non-ASCII paths.\n    \"\"\"\n\n    # We can't actually use `cv2.imwrite`, because it:\n    # 1. Doesn't support non-ASCII paths\n    # 2. Silently fails without doing anything if the path is invalid\n\n    _, _, extension = split_file_path(path)\n    _, buf_img = cv2.imencode(f\".{extension}\", img, params)\n    with open(path, \"wb\") as outf:\n        outf.write(buf_img)  # type: ignore\n\n\ndef cartesian_product(arrays: list[np.ndarray]) -> np.ndarray:\n    \"\"\"\n    Returns the cartesian product of the given arrays. Good for initializing coordinates, for example.\n\n    This is cartesian_product_transpose_pp from this following SO post by Paul Panzer:\n    https://stackoverflow.com/questions/11144513/cartesian-product-of-x-and-y-array-points-into-single-array-of-2d-points/49445693#49445693\n    \"\"\"\n    la = len(arrays)\n    dtype = np.result_type(*arrays)\n    arr = np.empty((la, *map(len, arrays)), dtype=dtype)\n    idx = slice(None), *itertools.repeat(None, la)\n    for i, a in enumerate(arrays):\n        arr[i, ...] = a[idx[: la - i]]\n    return arr.reshape(la, -1).T\n\n\ndef fast_gaussian_blur(\n    img: np.ndarray,\n    sigma_x: float,\n    sigma_y: float | None = None,\n) -> np.ndarray:\n    \"\"\"\n    Computes a channel-wise gaussian blur of the given image using a fast approximation.\n\n    The maximum error of the approximation is guaranteed to be less than 0.1%.\n    In addition to that, the error is guaranteed to be smoothly distributed across the image.\n    There are no sudden spikes in error anywhere.\n\n    Specifically, the method is implemented by downsampling the image, blurring the downsampled\n    image, and then upsampling the blurred image. This is much faster than blurring the full image.\n    Unfortunately, OpenCV's `resize` method has unfortunate artifacts when upscaling, so we\n    apply a small gaussian blur to the image after upscaling to smooth out the artifacts. This\n    single step almost doubles the runtime of the method, but it is still much faster than\n    blurring the full image.\n    \"\"\"\n    if sigma_y is None:\n        sigma_y = sigma_x\n    if sigma_x == 0 or sigma_y == 0:\n        return img.copy()\n\n    h, w, _ = get_h_w_c(img)\n\n    def get_scale_factor(sigma: float) -> float:\n        if sigma < 11:\n            return 1\n        if sigma < 15:\n            return 1.25\n        if sigma < 20:\n            return 1.5\n        if sigma < 25:\n            return 2\n        if sigma < 30:\n            return 2.5\n        if sigma < 50:\n            return 3\n        if sigma < 100:\n            return 4\n        if sigma < 200:\n            return 6\n        return 8\n\n    def get_sizing(size: int, sigma: float, f: float) -> tuple[int, float, float]:\n        \"\"\"\n        Return the size of the downsampled image, the sigma of the downsampled gaussian blur,\n        and the sigma of the upscaled gaussian blur.\n        \"\"\"\n        if f <= 1:\n            # just use simple gaussian, the error is too large otherwise\n            return size, 0, sigma\n\n        size_down = math.ceil(size / f)\n        f = size / size_down\n        sigma_up = f\n        sigma_down = math.sqrt(sigma**2 - sigma_up**2) / f\n        return size_down, sigma_down, sigma_up\n\n    # Handling different sigma values for x and y is difficult, so we take the easy way out\n    # and just use the smaller one. There are potentially better ways of combining them, but\n    # this is good enough for now.\n    scale_factor = min(get_scale_factor(sigma_x), get_scale_factor(sigma_y))\n    h_down, y_down_sigma, y_up_sigma = get_sizing(h, sigma_y, scale_factor)\n    w_down, x_down_sigma, x_up_sigma = get_sizing(w, sigma_x, scale_factor)\n\n    if h != h_down or w != w_down:\n        # downsampled gaussian blur\n        img = cv2.resize(img, (w_down, h_down), interpolation=cv2.INTER_AREA)\n        img = cv2.GaussianBlur(\n            img,\n            (0, 0),\n            sigmaX=x_down_sigma,\n            sigmaY=y_down_sigma,\n            borderType=cv2.BORDER_REFLECT,\n        )\n        img = cv2.resize(img, (w, h), interpolation=cv2.INTER_LINEAR)\n\n    if x_up_sigma != 0 or y_up_sigma != 0:\n        # post blur to smooth out artifacts\n        img = cv2.GaussianBlur(\n            img,\n            (0, 0),\n            sigmaX=x_up_sigma,\n            sigmaY=y_up_sigma,\n            borderType=cv2.BORDER_REFLECT,\n        )\n\n    return img\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/__init__.py",
    "content": ""
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/auto_split.py",
    "content": "from __future__ import annotations\n\nimport gc\nfrom collections.abc import Callable\n\nimport numpy as np\nimport onnxruntime as ort\nfrom nodes.impl.onnx.model import SizeReq\n\nfrom ..upscale.auto_split import Tiler, auto_split\n\n\ndef _into_batched_form(img: np.ndarray, change_shape: bool) -> np.ndarray:\n    shape_size = len(img.shape)\n    if shape_size == 3:\n        if change_shape:\n            # (H, W, C) -> (1, H, W, C)\n            return img[np.newaxis, :]\n        else:\n            # (H, W, C) -> (1, C, H, W)\n            return img.transpose((2, 0, 1))[np.newaxis, :]\n    elif shape_size == 2:\n        if change_shape:\n            # (H, W) -> (1, H, W, 1)\n            return img[np.newaxis, :, np.newaxis]\n        else:\n            # (H, W) -> (1, 1, H, W)\n            return img[np.newaxis, np.newaxis, :]\n    else:\n        raise ValueError(\"Unsupported input tensor shape\")\n\n\ndef _into_standard_image_form(img: np.ndarray, change_shape: bool) -> np.ndarray:\n    shape_size = len(img.shape)\n    if shape_size == 4:\n        if change_shape:\n            # (1, H, W, C) -> (H, W, C)\n            return img.squeeze(0)\n        else:\n            # (1, C, H, W) -> (H, W, C)\n            return img.squeeze(0).transpose(1, 2, 0)\n    elif shape_size == 3:\n        if change_shape:\n            # (H, W, C) -> (H, W, C)\n            return img\n        else:\n            # (C, H, W) -> (H, W, C)\n            return img.transpose(1, 2, 0)\n    elif shape_size == 2:\n        # (H, W)\n        return img\n    else:\n        raise ValueError(\"Unsupported output tensor shape\")\n\n\ndef _flip_r_b_channels(img: np.ndarray) -> np.ndarray:\n    shape_size = len(img.shape)\n    if shape_size != 3:\n        return img\n    if img.shape[2] == 3:\n        # (H, W, C) RGB -> BGR\n        return np.flip(img, 2)\n    elif img.shape[2] == 4:\n        # (H, W, C) RGBA -> BGRA\n        return np.dstack((img[:, :, 2], img[:, :, 1], img[:, :, 0], img[:, :, 3]))\n    return img\n\n\ndef _pad(\n    img: np.ndarray, req: SizeReq\n) -> tuple[np.ndarray, Callable[[np.ndarray], np.ndarray]]:\n    w = img.shape[1]\n    h = img.shape[0]\n\n    pad_w, pad_h = req.get_padding(w, h)\n\n    def remove_padding(i: np.ndarray) -> np.ndarray:\n        new_w = i.shape[1]\n        new_h = i.shape[0]\n        scale_w = new_w // (w + pad_w)\n        scale_h = new_h // (h + pad_h)\n        new_pad_w = int(pad_w * scale_w)\n        new_pad_h = int(pad_h * scale_h)\n        return i[: new_h - new_pad_h, : new_w - new_pad_w]\n\n    if pad_w or pad_h:\n        paddings = [(0, pad_h), (0, pad_w)]\n        if len(img.shape) == 3:\n            paddings.append((0, 0))\n        img = np.pad(img, paddings, \"reflect\")\n\n        return img, remove_padding\n    else:\n        return img, lambda i: i\n\n\ndef onnx_auto_split(\n    img: np.ndarray,\n    session: ort.InferenceSession,\n    change_shape: bool,\n    tiler: Tiler,\n    size_req: SizeReq | None = None,\n) -> np.ndarray:\n    input_name = session.get_inputs()[0].name\n    output_name = session.get_outputs()[0].name\n\n    is_fp16_model = session.get_inputs()[0].type == \"tensor(float16)\"\n\n    def upscale(img: np.ndarray, _: object):\n        try:\n            lr_img = img.astype(np.float16) if is_fp16_model else img\n            lr_img, remove_pad = _pad(lr_img, size_req or SizeReq())\n            lr_img = _flip_r_b_channels(lr_img)\n            lr_img = _into_batched_form(lr_img, change_shape)\n\n            output: np.ndarray = session.run([output_name], {input_name: lr_img})[0]\n\n            output = _into_standard_image_form(output, change_shape)\n            output = _flip_r_b_channels(output)\n            output = remove_pad(output)\n            return output.astype(np.float32)\n        except Exception as e:\n            if \"ONNXRuntimeError\" in str(e) and (\n                \"allocate memory\" in str(e)\n                or \"out of memory\" in str(e)\n                or \"cudaMalloc\" in str(e)\n            ):\n                raise RuntimeError(  # noqa: B904\n                    \"A VRAM out-of-memory error has occurred. Please try using a more extreme tiling mode.\"\n                )\n            else:\n                # Re-raise the exception if not an OOM error\n                raise\n\n    try:\n        return auto_split(img, upscale, tiler)\n    finally:\n        gc.collect()\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/load.py",
    "content": "from __future__ import annotations\n\nimport onnx\nimport onnx.inliner\nimport re2\nfrom sanic.log import logger\n\nfrom .model import OnnxGeneric, OnnxInfo, OnnxModel, OnnxRemBg, SizeReq\nfrom .utils import (\n    ModelShapeInference,\n    get_opset,\n    get_tensor_fp_datatype,\n)\n\nre2_options = re2.Options()\nre2_options.dot_nl = True\nre2_options.encoding = re2.Options.Encoding.LATIN1\n\nU2NET_STANDARD = re2.compile(b\"1959.+1960.+1961.+1962.+1963.+1964.+1965\", re2_options)\nU2NET_CLOTH = re2.compile(\n    b\"output.+d1.+Concat_1876.+Concat_1896.+Concat_1916.+Concat_1936.+Concat_1956\",\n    re2_options,\n)\nU2NET_SILUETA = re2.compile(b\"1808.+1827.+1828.+2296.+1831.+1850.+1958\", re2_options)\nU2NET_ISNET = re2.compile(\n    b\"/stage1/rebnconvin/conv_s1/Conv.+/stage1/rebnconvin/relu_s1/Relu\", re2_options\n)\n\n\ndef _detect_size_req(infer: ModelShapeInference):\n    if infer.fixed_input_width is not None and infer.fixed_input_height is not None:\n        shape = infer.infer_shape((infer.fixed_input_width, infer.fixed_input_height))\n        return SizeReq(), shape\n\n    for size in [16, 64, 256, 512]:\n        try:\n            shape = infer.infer_shape((size, size))\n            out_h, out_w, _ = shape[1]\n            if out_h is not None and out_w is not None:\n                return SizeReq(multiple_of=size), shape\n        except Exception:\n            logger.error(f\"Failed to infer shape for size {size}\", exc_info=True)\n\n    return SizeReq(), None\n\n\ndef load_onnx_model(model_or_bytes: onnx.ModelProto | bytes) -> OnnxModel:\n    if isinstance(model_or_bytes, onnx.ModelProto):\n        model = model_or_bytes\n        model_as_bytes = model.SerializeToString()\n    else:\n        model_as_bytes = model_or_bytes\n        model = onnx.load_model_from_string(model_or_bytes)\n\n    info = OnnxInfo(\n        opset=get_opset(model),\n        dtype=get_tensor_fp_datatype(model),\n    )\n\n    if (\n        U2NET_STANDARD.search(model_as_bytes[-1000:]) is not None\n        or U2NET_SILUETA.search(model_as_bytes[-600:]) is not None\n        or U2NET_ISNET.search(model_as_bytes[:10000]) is not None\n    ):\n        info.scale_width = 1\n        info.scale_height = 1\n        return OnnxRemBg(model_as_bytes, info)\n    elif U2NET_CLOTH.search(model_as_bytes[-1000:]) is not None:\n        info.scale_width = 1\n        info.scale_height = 3\n        return OnnxRemBg(model_as_bytes, info)\n    else:\n        try:\n            infer = ModelShapeInference(model)\n            info.fixed_input_width = infer.fixed_input_width\n            info.fixed_input_height = infer.fixed_input_height\n            info.input_channels = infer.input_channels\n            info.output_channels = infer.output_channels\n\n            size_req, shape_result = _detect_size_req(infer)\n            info.size_req = size_req\n\n            if shape_result:\n                i_hwc, o_hwc = shape_result\n                i_h, i_w, _i_c = i_hwc\n                o_h, o_w, o_c = o_hwc\n\n                def get_scale(i: int | None, o: int | None) -> int | None:\n                    if i is None or o is None:\n                        return None\n                    if o % i != 0:\n                        return None\n                    return o // i\n\n                info.scale_width = get_scale(i_w, o_w)\n                info.scale_height = get_scale(i_h, o_h)\n                info.output_channels = o_c\n        except Exception:\n            pass\n\n        return OnnxGeneric(model_as_bytes, info)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/model.py",
    "content": "# This class defines an interface.\n# It is important that is does not contain types that depend on ONNX.\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom typing import Final, Literal, Union\n\nOnnxSubType = Literal[\"Generic\", \"RemBg\"]\n\n\ndef _ceil_div(a: int, b: int):\n    return -(a // -b)\n\n\n@dataclass(init=False)\nclass SizeReq:\n    minimum: int\n    multiple_of: int\n\n    def __init__(self, minimum: int = 1, multiple_of: int = 1) -> None:\n        if minimum < 1:\n            raise ValueError(\"minimum must be at least 1\")\n        if multiple_of < 1:\n            raise ValueError(\"multiple_of must be at least 1\")\n\n        self.minimum = _ceil_div(minimum, multiple_of) * multiple_of\n        self.multiple_of = multiple_of\n\n    def get_padding(self, width: int, height: int) -> tuple[int, int]:\n        \"\"\"\n        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.\n        \"\"\"\n\n        def ceil_modulo(x: int, mod: int) -> int:\n            if x % mod == 0:\n                return x\n            return (x // mod + 1) * mod\n\n        w: int = max(self.minimum, width)\n        h: int = max(self.minimum, height)\n\n        w = ceil_modulo(w, self.multiple_of)\n        h = ceil_modulo(h, self.multiple_of)\n\n        return w - width, h - height\n\n\n@dataclass\nclass OnnxInfo:\n    opset: int\n    dtype: str\n\n    scale_width: int | None = None\n    scale_height: int | None = None\n\n    fixed_input_width: int | None = None\n    fixed_input_height: int | None = None\n\n    input_channels: int | None = None\n    output_channels: int | None = None\n\n    size_req: SizeReq = field(default_factory=SizeReq)\n\n\nclass OnnxGeneric:\n    def __init__(self, model_as_bytes: bytes, info: OnnxInfo) -> None:\n        self.bytes: bytes = model_as_bytes\n        self.sub_type: Final[Literal[\"Generic\"]] = \"Generic\"\n        self.info: OnnxInfo = info\n\n\nclass OnnxRemBg:\n    def __init__(\n        self,\n        model_as_bytes: bytes,\n        info: OnnxInfo,\n    ) -> None:\n        self.bytes: bytes = model_as_bytes\n        self.sub_type: Final[Literal[\"RemBg\"]] = \"RemBg\"\n        self.info: OnnxInfo = info\n\n\nOnnxModels = (OnnxGeneric, OnnxRemBg)\nOnnxModel = Union[OnnxGeneric, OnnxRemBg]\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/np_tensor_utils.py",
    "content": "from __future__ import annotations\n\nimport numpy as np\n\nfrom ..image_utils import MAX_VALUES_BY_DTYPE, as_3d\n\n\ndef np_denorm(x: np.ndarray, min_max: tuple[float, float] = (-1.0, 1.0)) -> np.ndarray:\n    \"\"\"Denormalize from [-1,1] range to [0,1]\n    formula: xi' = (xi - mu)/sigma\n    Example: \"out = (x + 1.0) / 2.0\" for denorm\n        range (-1,1) to (0,1)\n    for use with proper act in Generator output (ie. tanh)\n    \"\"\"\n    out = (x - min_max[0]) / (min_max[1] - min_max[0])\n    return np.clip(out, 0, 1)\n\n\ndef np_norm(x: np.ndarray) -> np.ndarray:\n    \"\"\"Normalize (z-norm) from [0,1] range to [-1,1]\"\"\"\n    out = (x - 0.5) * 2.0\n    return np.clip(out, -1, 1)\n\n\ndef np_bgr_to_rgb(img: np.ndarray) -> np.ndarray:\n    out: np.ndarray = img[::-1, ...]\n    return out\n\n\ndef np_rgb_to_bgr(img: np.ndarray) -> np.ndarray:\n    # same operation as bgr_to_rgb(), flip image channels\n    return np_bgr_to_rgb(img)\n\n\ndef np_bgra_to_rgba(img: np.ndarray) -> np.ndarray:\n    out: np.ndarray = img[[2, 1, 0, 3], ...]  # type: ignore\n    return out\n\n\ndef np_rgba_to_bgra(img: np.ndarray) -> np.ndarray:\n    # same operation as bgra_to_rgba(), flip image channels\n    return np_bgra_to_rgba(img)\n\n\ndef np2nptensor(\n    img: np.ndarray,\n    bgr2rgb: bool = True,\n    data_range: float = 1.0,\n    normalize: bool = False,\n    change_range: bool = True,\n    add_batch: bool = True,\n) -> np.ndarray:\n    \"\"\"Converts a numpy image array into a numpy Tensor array.\n    Parameters:\n        img (numpy array): the input image numpy array\n        add_batch (bool): choose if new tensor needs batch dimension added\n    \"\"\"\n    # check how many channels the image has, then condition. ie. RGB, RGBA, Gray\n    # if bgr2rgb:\n    #     img = img[\n    #         :, :, [2, 1, 0]\n    #     ]  # BGR to RGB -> in numpy, if using OpenCV, else not needed. Only if image has colors.\n    if change_range:\n        dtype = img.dtype\n        maxval = MAX_VALUES_BY_DTYPE.get(dtype.name, 1.0)\n        t_dtype = np.dtype(\"float32\")\n        img = img.astype(t_dtype) / maxval  # ie: uint8 = /255\n    # \"HWC to CHW\" and \"numpy to tensor\"\n    img = np.ascontiguousarray(np.transpose(as_3d(img), (2, 0, 1))).astype(np.float32)\n    if bgr2rgb:\n        # BGR to RGB -> in tensor, if using OpenCV, else not needed. Only if image has colors.)\n        if (\n            img.shape[0] % 3 == 0\n        ):  # RGB or MultixRGB (3xRGB, 5xRGB, etc. For video tensors.)\n            img = np_bgr_to_rgb(img)\n        elif img.shape[0] == 4:  # RGBA\n            img = np_bgra_to_rgba(img)\n    if add_batch:\n        img = np.expand_dims(\n            img, axis=0\n        )  # Add fake batch dimension = 1 . squeeze() will remove the dimensions of size 1\n    if normalize:\n        img = np_norm(img)\n    return img\n\n\ndef nptensor2np(\n    img: np.ndarray,\n    rgb2bgr: bool = True,\n    remove_batch: bool = True,\n    data_range: float = 255,\n    denormalize: bool = False,\n    change_range: bool = True,\n    imtype: type = np.uint8,\n) -> np.ndarray:\n    \"\"\"Converts a Tensor array into a numpy image array.\n    Parameters:\n        img (tensor): the input image tensor array\n            4D(B,(3/1),H,W), 3D(C,H,W), or 2D(H,W), any range, RGB channel order\n        remove_batch (bool): choose if tensor of shape BCHW needs to be squeezed\n        denormalize (bool): Used to denormalize from [-1,1] range back to [0,1]\n        imtype (type): the desired type of the converted numpy array (np.uint8\n            default)\n    Output:\n        img (np array): 3D(H,W,C) or 2D(H,W), [0,255], np.uint8 (default)\n    \"\"\"\n    n_dim = img.ndim\n\n    img = img.astype(np.float32)\n\n    if n_dim in (4, 3):\n        # if n_dim == 4, has to convert to 3 dimensions\n        if n_dim == 4 and remove_batch:\n            # remove a fake batch dimension\n            img = img.squeeze(0)\n\n        if img.shape[0] == 3 and rgb2bgr:  # RGB\n            # RGB to BGR -> in tensor, if using OpenCV, else not needed. Only if image has colors.\n            img_np = np_rgb_to_bgr(img)\n        elif img.shape[0] == 4 and rgb2bgr:  # RGBA\n            # RGBA to BGRA -> in tensor, if using OpenCV, else not needed. Only if image has colors.\n            img_np = np_rgba_to_bgra(img)\n        else:\n            img_np = img\n        img_np = np.transpose(img_np, (1, 2, 0))  # CHW to HWC\n    elif n_dim == 2:\n        img_np = img\n    else:\n        raise TypeError(\n            f\"Only support 4D, 3D and 2D tensor. But received with dimension: {n_dim:d}\"\n        )\n\n    # if rgb2bgr:\n    # img_np = img_np[[2, 1, 0], :, :] #RGB to BGR -> in numpy, if using OpenCV, else not needed. Only if image has colors.\n    # TODO: Check: could denormalize in the begining in tensor form instead\n    if denormalize:\n        img_np = np_denorm(img_np)  # denormalize if needed\n    if change_range:\n        img_np = np.clip(\n            data_range * img_np,\n            0,\n            data_range,\n        ).round()  # np.clip to the data_range\n\n    # has to be in range (0,255) before changing to np.uint8, else np.float32\n    return img_np.astype(imtype)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/onnx_to_ncnn.py",
    "content": "# ruff: noqa: N806\nfrom __future__ import annotations\n\nimport numpy as np\nimport onnx.numpy_helper as onph\nfrom google.protobuf.internal.containers import (\n    RepeatedCompositeFieldContainer,\n    RepeatedScalarFieldContainer,\n)\nfrom onnx.onnx_pb import AttributeProto, GraphProto, ModelProto, NodeProto, TensorProto\nfrom sanic.log import logger\n\nfrom ..ncnn.model import (\n    DTYPE_FP16,\n    DTYPE_FP32,\n    BinaryOpTypes,\n    EltwiseOpTypes,\n    GruDirectionFlags,\n    InterpResizeTypes,\n    NcnnLayer,\n    NcnnModel,\n    NormalizeEpsModes,\n    PaddingTypes,\n    PadModes,\n    PermuteOrderTypes,\n    ReductionOpTypes,\n    UnaryOpTypes,\n)\nfrom ..ncnn.optimizer import NcnnOptimizer\nfrom .tensorproto_utils import (\n    APT,\n    FLOAT32_MAX,\n    get_node_attr_af,\n    get_node_attr_ai,\n    get_node_attr_f,\n    get_node_attr_from_input_af,\n    get_node_attr_from_input_ai,\n    get_node_attr_from_input_f,\n    get_node_attr_i,\n    get_node_attr_s,\n    get_node_attr_tensor,\n    get_tensor_proto_data_size,\n    set_node_attr_ai,\n)\n\nUOT = UnaryOpTypes\nBOT = BinaryOpTypes\nEOT = EltwiseOpTypes\nGRU = GruDirectionFlags\nIRT = InterpResizeTypes\nNEM = NormalizeEpsModes\nPAM = PadModes\nPAT = PaddingTypes\nPOT = PermuteOrderTypes\nROT = ReductionOpTypes\n\n\nclass Onnx2NcnnConverter:\n    def __init__(self, onnx_model: ModelProto) -> None:\n        self.onnx_graph: GraphProto = onnx_model.graph\n        self.mutable_graph_nodes: list[NodeProto] = list(self.onnx_graph.node)\n        self.node_count: int = len(self.onnx_graph.node)\n        self.weights: dict[str, TensorProto] = {\n            initializer.name: initializer for initializer in self.onnx_graph.initializer\n        }\n\n        self.producers: dict[str, None] = {i.name: None for i in self.onnx_graph.input}\n        self.node_reference: dict[str, int] = {}\n        self.blob_names: dict[str, None] = {}\n\n    @staticmethod\n    def add_weight(\n        layer: NcnnLayer,\n        weight_name: str,\n        data: float | int | np.ndarray | TensorProto,\n        quantize_tag: bytes = b\"\",\n    ) -> int:\n        if isinstance(data, TensorProto):\n            data = onph.to_array(data)\n\n        return layer.add_weight(weight_name, data, quantize_tag)\n\n    @staticmethod\n    def clear_container(\n        container: RepeatedCompositeFieldContainer | RepeatedScalarFieldContainer,\n    ) -> None:\n        for _ in range(len(container)):\n            container.pop()\n\n    def swap_nodes(self, a: int, b: int) -> None:\n        self.mutable_graph_nodes[a], self.mutable_graph_nodes[b] = (\n            self.mutable_graph_nodes[b],\n            self.mutable_graph_nodes[a],\n        )\n\n    def fuse_rewrite_gather(self) -> None:\n        for gather in self.mutable_graph_nodes:\n            if gather.op_type == \"Gather\":\n                indices = get_node_attr_from_input_ai(self.weights[gather.input[1]])\n                if len(indices) == 1:\n                    # Reconstruct node connections\n                    self.node_reference[gather.input[1]] -= 1\n                    origin_inp = gather.input[0]\n                    gather.ClearField(\"input\")\n                    gather.input.append(origin_inp)\n\n                    # Update axis, starts and ends\n                    axis = get_node_attr_i(gather, \"axis\", 1)\n                    gather.op_type = \"Crop\"\n                    gather.ClearField(\"attribute\")\n\n                    index = indices[0]\n                    set_node_attr_ai(gather, \"starts\", np.array([index], np.int32))\n                    set_node_attr_ai(gather, \"ends\", np.array([index + 1], np.int32))\n                    set_node_attr_ai(gather, \"axis\", np.array([axis], np.int32))\n\n    def fuse_weight_reshape(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n            if node.op_type == \"Reshape\":\n                if node.input[0] in self.weights:\n                    self.weights[node.output[0]] = self.weights[node.input[0]]\n                    if len(node.input) == 1:\n                        shape = get_node_attr_ai(node, \"shape\")\n                    elif len(node.input) == 2:\n                        shape = get_node_attr_from_input_ai(self.weights[node.input[1]])\n                    else:\n                        shape = np.empty(0, np.int64)\n\n                    self.clear_container(self.weights[node.output[0]].dims)\n                    for dim in shape:\n                        self.weights[node.output[0]].dims.append(dim)\n\n                    node.op_type = \"noop_reducedncnn\"\n\n                    self.node_reference[node.input[0]] -= 1\n                    if len(node.input) == 2:\n                        self.node_reference[node.input[1]] -= 1\n\n                    reduced_node_count[0] += 1\n                    i += 1  # noqa\n\n    def fuse_weight_transpose(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n            if node.op_type == \"Transpose\":\n                if (\n                    node.input[0] in self.weights\n                    and len(self.weights[node.input[0]].dims) == 2\n                ):\n                    perm = get_node_attr_ai(node, \"perm\")\n                    if perm.size != 2 or perm[0] != 1 or perm[1] != 0:\n                        continue\n\n                    self.weights[node.output[0]] = self.weights[node.input[0]]\n\n                    # Permute weight\n                    B = self.weights[node.output[0]]\n\n                    h, w = B.dims[:2]\n\n                    permuted_data = onph.to_array(B).T\n\n                    B.dims[:2] = (w, h)\n\n                    if B.raw_data:\n                        B.raw_data = permuted_data.tobytes()\n                    else:\n                        self.clear_container(B.float_data)\n                        B.float_data.extend(permuted_data)\n\n                    # Reduce\n                    node.op_type = \"noop_reducednccn\"\n                    self.node_reference[node.input[0]] -= 1\n\n                    reduced_node_count[0] += 1\n                    i += 1  # noqa\n\n    def fuse_shufflechannel(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # ShuffleChannel <= Reshape - Transpose - Reshape\n            # ShuffleChannel <= Reshape - Transpose - Constant - Reshape\n            if node.op_type == \"Reshape\":\n                if self.node_reference[node.output[0]] != 1:\n                    continue\n\n                if len(node.input) == 1:\n                    shape = get_node_attr_ai(node, \"shape\")\n                else:\n                    # Skip weight reshape\n                    if node.input[1] not in self.weights:\n                        continue\n                    shape = get_node_attr_from_input_ai(self.weights[node.input[1]])\n\n                # 1 groups channels_per_group, height, width\n                # reverse style = channels_per_group, groups, height * width\n                if (shape.size not in (5, 3)) or (shape.size == 5 and shape[0] != 1):\n                    continue\n                if i + 2 >= self.node_count:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n\n                if node3.op_type == \"Constant\":\n                    if i + 3 >= self.node_count:\n                        continue\n                    node3 = self.mutable_graph_nodes[i + 3]\n                if (node2.op_type != \"Transpose\" or node3.op_type != \"Reshape\") or (\n                    self.node_reference[node2.output[0]] != 1\n                ):\n                    continue\n\n                # 0 2 1 3 4\n                # reverse style = 1 0 2\n                perm = get_node_attr_ai(node2, \"perm\")\n                if perm.size not in (5, 3):\n                    continue\n                if perm.size == 5 and (\n                    perm[0] != 0\n                    or perm[1] != 2\n                    or perm[2] != 1\n                    or perm[3] != 3\n                    or perm[4] != 4\n                ):\n                    continue\n                if perm.size == 3 and (perm[0] != 1 or perm[1] != 0 or perm[2] != 2):\n                    continue\n\n                if len(node3.input) == 1:\n                    shape3 = get_node_attr_ai(node3, \"shape\")\n                else:\n                    if node3.input[1] not in self.weights:\n                        continue\n                    shape3 = get_node_attr_from_input_ai(self.weights[node3.input[1]])\n\n                # 1, -1, height, width\n                # reverse style = group, -1, channels_per_group, height, width\n                if shape3.size not in (4, 5):\n                    continue\n                if shape3.size == 4 and (\n                    shape3[0] != 1\n                    or (shape3[1] != -1 and shape3[1] != shape[1] * shape[2])\n                ):\n                    continue\n                if shape3.size == 5 and (\n                    shape3[0] != shape[1]\n                    or shape3[2] != shape[0]\n                    or shape3[3] * shape3[4] != shape[2]\n                ):\n                    continue\n\n                # Reduce\n                node.op_type = \"noop_reducedncnn\"\n                node2.op_type = \"noop_reducedncnn\"\n\n                if len(node.input) == 2:\n                    self.node_reference[node.input[1]] -= 1\n                self.node_reference[node.output[0]] -= 1\n                self.node_reference[node2.output[0]] -= 1\n                if len(node3.input) == 2:\n                    self.node_reference[node3.input[1]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n\n                node3.op_type = \"ShuffleChannel\"\n                node3.input[0] = node.input[0]\n\n                attr_group = AttributeProto(name=\"group\", i=shape[1], type=APT.INT)\n                node3.attribute.append(attr_group)\n\n                attr_reverse = AttributeProto(\n                    name=\"reverse\", i=int(shape.size == 3), type=APT.INT\n                )\n                node3.attribute.append(attr_reverse)\n\n                reduced_node_count[0] += 2\n                i += 2  # noqa\n\n    def fuse_shufflechannel_split(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # Split <= ShuffleChannel(reverse type) - Gather(0) - Gather(1)\n            if node.op_type == \"ShuffleChannel\":\n                # reverse = 1\n                reverse = get_node_attr_i(node, \"reverse\")\n                if reverse != 1 or (i + 2 >= self.node_count):\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n\n                if node2.op_type != \"Gather\" or node3.op_type != \"Gather\":\n                    continue\n                if node2.input[0] != node.output[0] or node3.input[0] != node.output[0]:\n                    continue\n\n                # axis = 0 or indices = 0\n                gather2_axis = get_node_attr_i(node2, \"axis\")\n                if gather2_axis != 0 or node2.input[1] not in self.weights:\n                    continue\n\n                gather2_indices = get_node_attr_from_input_ai(\n                    self.weights[node2.input[1]]\n                )\n                if gather2_indices.size != 1 or gather2_indices[0] != 0:\n                    continue\n\n                # axis = 0 or indices = 1\n                gather3_axis = get_node_attr_i(node3, \"axis\")\n                if gather3_axis != 0 or node3.input[1] not in self.weights:\n                    continue\n\n                gather3_indices = get_node_attr_from_input_ai(\n                    self.weights[node3.input[1]]\n                )\n                if gather3_indices.size != 1 or gather2_indices[0] != 1:\n                    continue\n\n                # reduce\n                node2.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.output[0]] -= 2\n                self.node_reference[node2.input[1]] -= 1\n                self.node_reference[node3.input[1]] -= 1\n\n                node3.op_type = \"Split\"\n                node3.ClearField(\"input\")\n                node3.input.append(node.output[0])\n                node3.output.append(node3.output[0])\n                node3.output[0] = node2.output[0]\n\n                node3.ClearField(\"attribute\")\n                attr_axis = AttributeProto(name=\"axis\", i=1, type=APT.INT)\n                node3.attribute.append(attr_axis)\n\n                reduced_node_count[0] += 1\n                i += 1  # noqa\n\n    def fuse_hardswish(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # HardSwish <= Add(+3) - Clip(0, 6) - Mul(X, ) - Div( / 6)\n            # HardSwish <= Add(+3) - Clip(0, 6) - Mul(X, ) - Mul(*(1 / 6))\n            # HardSwish <= Add(+3) - Clip(0, 6) - Mul(X, ) - Constant - Div( / 6)\n            # HardSwish <= Add(+3) - Clip(0, 6) - Mul(X, ) - Constant - Mul(*(1 / 6))\n            # out = x * F.relu6(x + 3, inplace=True) / 6\n            if node.op_type == \"Add\":\n                if (\n                    self.node_reference[node.output[0]] != 1\n                    or i + 3 >= self.node_count\n                    or node.input[1] not in self.weights\n                ):\n                    continue\n\n                add_three = self.weights[node.input[1]]\n                if (\n                    len(add_three.dims) != 0\n                    or get_tensor_proto_data_size(add_three, add_three.data_type) != 1\n                ):\n                    continue\n\n                constant_add_three = get_node_attr_from_input_f(add_three)\n                if constant_add_three != 3:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n                node4 = self.mutable_graph_nodes[i + 3]\n\n                if node4.op_type == \"Constant\":\n                    if i + 4 >= self.node_count:\n                        continue\n                    node4 = self.mutable_graph_nodes[i + 4]\n                if (\n                    node2.op_type != \"Clip\"\n                    or node3.op_type != \"Mul\"\n                    or (node4.op_type not in (\"Div\", \"Mul\"))\n                ):\n                    continue\n                if self.node_reference[node2.output[0]] != 1:\n                    continue\n\n                if len(node2.input) == 1:\n                    relu6_min = get_node_attr_f(node2, \"min\", -FLOAT32_MAX)\n                    relu6_max = get_node_attr_f(node2, \"max\", FLOAT32_MAX)\n                else:\n                    min_tp = self.weights[node2.input[1]]\n                    max_tp = self.weights[node2.input[2]]\n                    relu6_min = get_node_attr_from_input_f(min_tp)\n                    relu6_max = get_node_attr_from_input_f(max_tp)\n\n                if relu6_min != 0 or relu6_max != 6:\n                    continue\n                if self.node_reference[node3.output[0]] != 1:\n                    continue\n                if node3.input[0] != node.input[0] or node3.input[1] != node2.output[0]:\n                    continue\n                if node4.input[1] not in self.weights:\n                    continue\n\n                div_six = self.weights[node4.input[1]]\n                if (\n                    len(div_six.dims) != 0\n                    or get_tensor_proto_data_size(div_six, div_six.data_type) != 1\n                ):\n                    continue\n\n                constant_div_six = get_node_attr_from_input_f(div_six)\n                if (node4.op_type == \"Div\" and constant_div_six != 6) or (\n                    node4.op_type == \"Mul\" and constant_div_six != 1 / 6\n                ):\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n                node2.op_type = \"noop_reducedncnn\"\n                node3.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.input[0]] -= 1\n                self.node_reference[node.input[1]] -= 1\n                self.node_reference[node.output[0]] -= 1\n                if len(node2.input) == 3:\n                    self.node_reference[node2.input[1]] -= 1\n                    self.node_reference[node2.input[2]] -= 1\n                self.node_reference[node2.output[0]] -= 1\n                self.node_reference[node3.output[0]] -= 1\n                self.node_reference[node4.input[1]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n                self.blob_names.pop(node3.output[0], None)\n\n                node4.op_type = \"HardSwish\"\n                node4.ClearField(\"input\")\n                node4.input.append(node.input[0])\n\n                attr_alpha = AttributeProto(name=\"alpha\", f=1 / 6, type=APT.FLOAT)\n                node4.attribute.append(attr_alpha)\n\n                attr_beta = AttributeProto(name=\"beta\", f=0.5, type=APT.FLOAT)\n                node4.attribute.append(attr_beta)\n\n                reduced_node_count[0] += 3\n                i += 3  # noqa\n\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # HardSwish <= HardSigmoid - Mul\n            # out = x * hsigmoid(x)\n            if node.op_type == \"HardSigmoid\":\n                if self.node_reference[node.output[0]] != 1:\n                    continue\n\n                alpha = get_node_attr_f(node, \"alpha\", 0.2)\n                beta = get_node_attr_f(node, \"beta\", 0.5)\n\n                if i + 1 >= self.node_count:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n\n                if node2.op_type != \"Mul\":\n                    continue\n                if node2.input[0] != node.input[0] or node2.input[1] != node.output[0]:\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.input[0]] -= 1\n                self.node_reference[node.output[0]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n\n                node2.op_type = \"HardSwish\"\n                node2.ClearField(\"input\")\n                node2.input.append(node.input[0])\n\n                attr_alpha = AttributeProto(name=\"alpha\", f=alpha, type=APT.FLOAT)\n                node2.attribute.append(attr_alpha)\n\n                attr_beta = AttributeProto(name=\"beta\", f=beta, type=APT.FLOAT)\n                node2.attribute.append(attr_beta)\n\n                reduced_node_count[0] += 1\n                i += 1  # noqa\n\n    def fuse_hardsigmoid(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # HardSigmoid <= Add(+3) - Clip(0, 6) - Div( / 6)\n            # HardSigmoid <= Add(+3) - Clip(0, 6) - Mul(*(1 / 6))\n            # HardSigmoid <= Add(+3) - Clip(0, 6) - Constant - Div( / 6)\n            # HardSigmoid <= Add(+3) - Clip(0, 6) - Constant - Mul(*(1 / 6))\n            # out = F.relu6(x + 3, inplace=True) / 6\n            if node.op_type == \"Add\":\n                if (\n                    self.node_reference[node.output[0]] != 1\n                    or i + 2 >= self.node_count\n                    or node.input[1] not in self.weights\n                ):\n                    continue\n\n                add_three = self.weights[node.input[1]]\n                if (\n                    len(add_three.dims) != 0\n                    or get_tensor_proto_data_size(add_three, add_three.data_type) != 1\n                ):\n                    continue\n\n                constant_add_three = self.weights[node.input[1]]\n                if constant_add_three != 3:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n\n                if node3.op_type == \"Constant\":\n                    if i + 3 >= self.node_count:\n                        continue\n                    node3 = self.mutable_graph_nodes[i + 3]\n\n                if node2.op_type != \"Clip\" or (node3.op_type not in (\"Div\", \"Mul\")):\n                    continue\n\n                if self.node_reference[node2.output[0]] != 1:\n                    continue\n\n                if len(node2.input) == 1:\n                    relu6_min = get_node_attr_f(node2, \"min\", -FLOAT32_MAX)\n                    relu6_max = get_node_attr_f(node2, \"max\", FLOAT32_MAX)\n                else:\n                    min_tp = self.weights[node2.input[1]]\n                    max_tp = self.weights[node2.input[2]]\n                    relu6_min = get_node_attr_from_input_f(min_tp)\n                    relu6_max = get_node_attr_from_input_f(max_tp)\n\n                if relu6_min != 0 or relu6_max != 6:\n                    continue\n                if node3.input[1] not in self.weights:\n                    continue\n\n                div_six = self.weights[node3.input[1]]\n                if (\n                    len(div_six.dims) != 0\n                    or get_tensor_proto_data_size(div_six, div_six.data_type) != 1\n                ):\n                    continue\n\n                constant_div_six = get_node_attr_from_input_f(div_six)\n                if (node3.op_type == \"Div\" and constant_div_six != 6) or (\n                    node3.op_type == \"Mul\" and constant_div_six != 1 / 6\n                ):\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n                node2.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.input[1]] -= 1\n                self.node_reference[node.output[0]] -= 1\n                if len(node2.input) == 3:\n                    self.node_reference[node2.input[1]] -= 1\n                    self.node_reference[node2.input[2]] -= 1\n                self.node_reference[node2.output[0]] -= 1\n                self.node_reference[node3.input[1]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n\n                node3.op_type = \"HardSigmoid\"\n                node3.ClearField(\"input\")\n                node3.input.append(node.input[0])\n\n                attr_alpha = AttributeProto(name=\"alpha\", f=1 / 6, type=APT.FLOAT)\n                node3.attribute.append(attr_alpha)\n\n                attr_beta = AttributeProto(name=\"beta\", f=0.5, type=APT.FLOAT)\n                node3.attribute.append(attr_beta)\n\n                reduced_node_count[0] += 2\n                i += 2  # noqa\n\n    def fuse_swish(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # Swish <= Sigmoid - Mul\n            # x * torch.sigmoid(x)\n            if node.op_type == \"Sigmoid\":\n                if self.node_reference[node.output[0]] != 1 or i + 1 >= self.node_count:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n\n                if node2.op_type != \"Mul\":\n                    continue\n                if node2.input[0] != node.input[0] or node2.input[1] != node.output[0]:\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.input[0]] -= 1\n                self.node_reference[node.output[0]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n\n                node2.op_type = \"Swish\"\n                node2.ClearField(\"input\")\n                node2.input.append(node.input[0])\n\n                reduced_node_count[0] += 1\n                i += 1  # noqa\n\n    def fuse_batchnorm1d_squeeze_unsqueeze(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # BatchNormalization <= Unsqueeze - BatchNormalization - Squeeze\n            if node.op_type == \"Unsqueeze\":\n                if self.node_reference[node.output[0]] != 1 or i + 2 >= self.node_count:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n\n                if node2.op_type != \"BatchNormalization\" or node3.op_type != \"Squeeze\":\n                    continue\n                if self.node_reference[node2.output[0]] != 1:\n                    continue\n                if (\n                    node2.input[0] != node.output[0]\n                    or node3.input[0] != node2.output[0]\n                ):\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n                node3.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.output[0]] -= 1\n                self.node_reference[node2.output[0]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n\n                node2.input[0] = node.input[0]\n                node2.output[0] = node3.output[0]\n\n                reduced_node_count[0] += 2\n                i += 2  # noqa\n\n    def fuse_unsqueeze_prelu(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # PReLU <= Unsqueeze - PReLU\n            if node.op_type == \"Unsqueeze\":\n                # check weight\n                if node.input[0] not in self.weights:\n                    continue\n\n                B = self.weights[node.input[0]]\n                if len(B.dims) != 1:\n                    continue\n                if self.node_reference[node.output[0]] != 1:\n                    continue\n\n                # axes = (1, 2)\n                axes = get_node_attr_ai(node, \"axes\")\n                if axes.size != 2 or axes[0] != 1 or axes[1] != 2:\n                    continue\n                if i + 1 >= self.node_count:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n\n                if node2.op_type != \"PRelu\" or node2.input[1] != node.output[0]:\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.output[0]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n\n                node2.input[1] = node.input[0]\n\n                reduced_node_count[0] += 1\n                i += 1  # noqa\n\n    def fuse_normalize(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # Normalize <= X - ReduceL2 - Clip - Expand - Div\n            # Normalize <= X - ReduceL2 - Clip - Shape - Expand - Div\n            if node.op_type == \"ReduceL2\":\n                if self.node_reference[node.output[0]] != 1:\n                    continue\n\n                # axes = (1)\n                axes = get_node_attr_ai(node, \"axes\")\n                if len(axes) != 1 or axes[0] != 1 or i + 3 >= self.node_count:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n                node4 = self.mutable_graph_nodes[i + 3]\n\n                has_shape_node = node3.op_type == \"Shape\"\n                node_shape = NodeProto()\n                if has_shape_node:\n                    if i + 4 >= self.node_count:\n                        continue\n\n                    node_shape = node3\n                    node3 = self.mutable_graph_nodes[i + 3]\n                    node4 = self.mutable_graph_nodes[i + 4]\n\n                if (\n                    node2.op_type != \"Clip\"\n                    or node3.op_type != \"Expand\"\n                    or node4.op_type != \"Div\"\n                ):\n                    continue\n                if (\n                    self.node_reference[node2.output[0]] != 1\n                    or self.node_reference[node3.output[0]] != 1\n                ):\n                    continue\n                if (\n                    node2.input[0] != node.output[0]\n                    or node3.input[0] != node2.output[0]\n                    or node4.input[0] != node.input[0]\n                    or node4.input[1] != node3.output[0]\n                ):\n                    continue\n\n                if has_shape_node and (\n                    node_shape.input[0] != node.input[0]\n                    or node3.input[1] != node_shape.output[0]\n                ):\n                    continue\n\n                # +eps\n                if len(node2.input) == 1:\n                    clip_min = get_node_attr_f(node2, \"min\", -FLOAT32_MAX)\n                else:\n                    min_tp = self.weights[node2.input[1]]\n                    clip_min = get_node_attr_from_input_f(min_tp)\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n                node2.op_type = \"noop_reducedncnn\"\n                if has_shape_node:\n                    node_shape.op_type = \"noop_reducedncnn\"\n                node3.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.input[0]] -= 2 if has_shape_node else 1\n                self.node_reference[node.output[0]] -= 1\n                self.node_reference[node2.output[0]] -= 1\n                if has_shape_node:\n                    self.node_reference[node_shape.output[0]] -= 1\n                self.node_reference[node3.output[0]] -= 1\n                if len(node3.input) == 2:\n                    self.node_reference[node3.input[1]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n                if has_shape_node:\n                    self.blob_names.pop(node_shape.output[0], None)\n                self.blob_names.pop(node3.output[0], None)\n\n                node4.op_type = \"Normalize\"\n                node4.ClearField(\"input\")\n                node4.input.append(node.input[0])\n\n                attr_alpha = AttributeProto(name=\"eps\", f=clip_min, type=APT.FLOAT)\n                node4.attribute.append(attr_alpha)\n\n                reduced_node_count[0] += 4 if has_shape_node else 3\n                i += 4 if has_shape_node else 3  # noqa\n\n    def fuse_groupnorm(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # GroupNorm <= X - Reshape - InstanceNormalization - Reshape - Mul - Add\n            if node.op_type == \"Reshape\":\n                if self.node_reference[node.output[0]] != 1:\n                    continue\n\n                if len(node.input) == 1:\n                    shape = get_node_attr_ai(node, \"shape\")\n                else:\n                    # Skip weight reshape\n                    if node.input[1] not in self.weights:\n                        continue\n\n                    shape = get_node_attr_from_input_ai(self.weights[node.input[1]])\n\n                # 0, group, -1\n                if (\n                    shape.size != 3\n                    or shape[0] != 0\n                    or shape[2] != -1\n                    or i + 4 >= self.node_count\n                ):\n                    continue\n\n                groups = shape[1]\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n                node4 = self.mutable_graph_nodes[i + 3]\n                node5 = self.mutable_graph_nodes[i + 4]\n\n                if (\n                    node2.op_type != \"InstanceNormalization\"\n                    or node3.op_type != \"Reshape\"\n                    or node4.op_type != \"Mul\"\n                    or node5.op_type != \"Add\"\n                ):\n                    continue\n                if (\n                    self.node_reference[node2.output[0]] != 1\n                    or self.node_reference[node3.output[0]] != 1\n                    or self.node_reference[node4.output[0]] != 1\n                ):\n                    continue\n                if (\n                    node2.input[0] != node.output[0]\n                    or node3.input[0] != node2.output[0]\n                    or node4.input[0] != node3.output[0]\n                    or node5.input[0] != node4.output[0]\n                ):\n                    continue\n\n                # InstanceNormalization S=1 B=0\n                S = get_node_attr_from_input_af(self.weights[node2.input[1]])\n                B = get_node_attr_from_input_af(self.weights[node2.input[2]])\n                if S.size != groups or B.size != groups:\n                    continue\n                if np.any(S != 1) or np.any(B != 0):\n                    continue\n\n                if len(node3.input) == 1:\n                    shape2 = get_node_attr_ai(node3, \"shape\")\n                else:\n                    # Skip weight reshape\n                    if node3.input[1] not in self.weights:\n                        continue\n\n                    shape2 = get_node_attr_from_input_ai(self.weights[node3.input[1]])\n\n                # 1, channels, w, h\n                if shape2.size != 4 or shape2[0] != 1:\n                    continue\n\n                channels = shape2[1]\n\n                # affine\n                affine_S = get_node_attr_from_input_af(self.weights[node4.input[1]])\n                affine_B = get_node_attr_from_input_af(self.weights[node5.input[1]])\n                if channels not in (affine_S.size, affine_B.size):\n                    continue  # only per-channel affine allowed\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n                node2.op_type = \"noop_reducedncnn\"\n                node3.op_type = \"noop_reducedncnn\"\n                node4.op_type = \"noop_reducedncnn\"\n\n                if len(node.input) == 2:\n                    self.node_reference[node.input[1]] -= 1\n                self.node_reference[node.output[0]] -= 1\n                self.node_reference[node2.input[1]] -= 1\n                self.node_reference[node2.input[2]] -= 1\n                self.node_reference[node2.output[0]] -= 1\n                if len(node3.input) == 2:\n                    self.node_reference[node3.input[1]] -= 1\n                self.node_reference[node3.output[0]] -= 1\n                self.node_reference[node4.output[0]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n                self.blob_names.pop(node3.output[0], None)\n                self.blob_names.pop(node4.output[0], None)\n\n                affine_scale = node4.input[1]\n                affine_bias = node5.input[1]\n\n                node5.op_type = \"GroupNorm\"\n                node5.ClearField(\"input\")\n                node5.input.append(node.input[0])\n                node5.input.append(affine_scale)\n                node5.input.append(affine_bias)\n\n                attr_groups = AttributeProto(name=\"groups\", i=groups, type=APT.INT)\n                node5.attribute.append(attr_groups)\n\n                attr_channels = AttributeProto(\n                    name=\"channels\", i=channels, type=APT.INT\n                )\n                node5.attribute.append(attr_channels)\n\n                # +eps\n                eps = get_node_attr_f(node2, \"epsilon\", 0.00001)\n                attr_eps = AttributeProto(name=\"epsilon\", f=eps, type=APT.FLOAT)\n                node5.attribute.append(attr_eps)\n\n                attr_affine = AttributeProto(name=\"affine\", i=1, type=APT.INT)\n                node5.attribute.append(attr_affine)\n\n                reduced_node_count[0] += 4\n                i += 4  # noqa\n\n    def fuse_layernorm(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # LayerNorm <= X - ReduceMean - Sub - Pow - ReduceMean - Add - Sqrt - Div\n            # LayerNorm <= X - ReduceMean - Sub - Pow - ReduceMean - Add - Sqrt - Div - Mul - Add\n            if node.op_type == \"ReduceMean\":\n                if self.node_reference[node.output[0]] != 1:\n                    continue\n\n                axes = get_node_attr_ai(node, \"axes\")\n\n                # -1\n                # -2 -1\n                if axes.size not in (1, 2):\n                    continue\n                if (axes.size == 1 and axes[0] != -1) or (\n                    axes.size == 2 and (axes[0] != -2 or axes[1] != -1)\n                ):\n                    continue\n                if i + 6 >= self.node_count:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n                node4 = self.mutable_graph_nodes[i + 3]\n                node5 = self.mutable_graph_nodes[i + 4]\n                node6 = self.mutable_graph_nodes[i + 5]\n                node7 = self.mutable_graph_nodes[i + 6]\n\n                if node2.op_type != \"Sub\" or node3.op_type != \"Pow\":\n                    continue\n                if (\n                    self.node_reference[node2.output[0]] != 2\n                    or self.node_reference[node3.output[0]] != 1\n                    or self.node_reference[node4.output[0]] != 1\n                    or self.node_reference[node5.output[0]] != 1\n                    or self.node_reference[node6.output[0]] != 1\n                ):\n                    continue\n                if (\n                    node2.input[0] != node.output[0]\n                    or node2.input[1] != node.output[0]\n                    or node3.input[0] != node2.output[0]\n                    or node4.input[0] != node3.output[0]\n                    or node5.input[0] != node4.output[0]\n                    or node6.input[0] != node5.output[0]\n                    or node7.input[0] != node2.output[0]\n                    or node7.input[1] != node6.output[0]\n                ):\n                    continue\n                if node3.input[1] not in self.weights:\n                    continue\n\n                pow_two = self.weights[node3.input[1]]\n                if (\n                    len(pow_two.dims) != 0\n                    or get_tensor_proto_data_size(pow_two, pow_two.data_type) != 1\n                ):\n                    continue\n\n                constant_pow_two = get_node_attr_from_input_f(pow_two)\n                if constant_pow_two != 2:\n                    continue\n\n                axes4 = get_node_attr_ai(node4, \"axes\")\n\n                # -1\n                # -2 -1\n                if axes4.size != axes.size:\n                    continue\n                if (axes.size == 1 and axes[4] != -1) or (\n                    axes.size == 2 and (axes4[0] != -2 or axes4[1] != -1)\n                ):\n                    continue\n                if node5.input[1] not in self.weights:\n                    continue\n\n                add_eps = self.weights[node5.input[1]]\n                if (\n                    len(add_eps.dims) != 0\n                    or get_tensor_proto_data_size(add_eps, add_eps.data_type) != 1\n                ):\n                    continue\n\n                eps = get_node_attr_from_input_f(add_eps)\n\n                affine = 0\n                while i + 8 < self.node_count:\n                    node8 = self.mutable_graph_nodes[i + 7]\n                    node9 = self.mutable_graph_nodes[i + 8]\n\n                    if node8.op_type != \"Mul\" or node9.op_type != \"Add\":\n                        break\n                    if (\n                        self.node_reference[node7.output[0]] != 1\n                        or self.node_reference[node8.output[0]] != 1\n                    ):\n                        break\n                    if (\n                        node8.input[0] != node7.output[0]\n                        or node9.input[0] != node8.output[0]\n                    ):\n                        break\n\n                    # affine\n                    affine_S = get_node_attr_from_input_af(self.weights[node8.input[1]])\n                    affine_B = get_node_attr_from_input_af(self.weights[node9.input[1]])\n                    if affine_S.size != affine_B.size:\n                        break\n\n                    affine = 1\n                    break\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n                node2.op_type = \"noop_reducedncnn\"\n                node3.op_type = \"noop_reducedncnn\"\n                node4.op_type = \"noop_reducedncnn\"\n                node5.op_type = \"noop_reducedncnn\"\n                node6.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node2.input[0]] -= 1\n                self.node_reference[node2.input[1]] -= 1\n                self.node_reference[node3.input[0]] -= 1\n                self.node_reference[node3.input[1]] -= 1\n                self.node_reference[node4.input[0]] -= 1\n                self.node_reference[node5.input[0]] -= 1\n                self.node_reference[node5.input[1]] -= 1\n                self.node_reference[node6.input[0]] -= 1\n                self.node_reference[node7.input[0]] -= 1\n                self.node_reference[node7.input[1]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n                self.blob_names.pop(node3.output[0], None)\n                self.blob_names.pop(node4.output[0], None)\n                self.blob_names.pop(node5.output[0], None)\n                self.blob_names.pop(node6.output[0], None)\n\n                attr_eps = AttributeProto(name=\"epsilon\", f=eps, type=APT.FLOAT)\n                attr_affine = AttributeProto(name=\"affine\", i=affine, type=APT.INT)\n                if affine == 0:\n                    node7.op_type = \"LayerNorm\"\n                    node7.ClearField(\"input\")\n                    node7.input.append(node.input[0])\n\n                    node7.attribute.append(attr_eps)\n                    node7.attribute.append(attr_affine)\n\n                    reduced_node_count[0] += 6\n                    i += 6  # noqa\n                else:\n                    # This is probably unnecessary on their part, but I'm paranoid\n                    node8 = self.mutable_graph_nodes[i + 7]\n                    node9 = self.mutable_graph_nodes[i + 8]\n\n                    node7.op_type = \"noop_reducedncnn\"\n                    node8.op_type = \"noop_reducedncnn\"\n\n                    self.node_reference[node8.input[0]] -= 1\n                    self.node_reference[node9.input[0]] -= 1\n\n                    self.blob_names.pop(node7.output[0], None)\n                    self.blob_names.pop(node8.output[0], None)\n\n                    affine_scale = node8.input[1]\n                    affine_bias = node9.input[1]\n\n                    node9.op_type = \"LayerNorm\"\n                    node9.ClearField(\"input\")\n                    node9.input.append(node.input[0])\n                    node9.input.append(affine_scale)\n                    node9.input.append(affine_bias)\n\n                    node9.attribute.append(attr_eps)\n                    node9.attribute.append(attr_affine)\n\n                    reduced_node_count[0] += 8\n                    i += 8  # noqa\n\n    def fuse_flatten(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # Flatten <= X - Shape - Gather - Constant - Unsqueeze - Unsqueeze - Concat - Reshape\n            if node.op_type == \"Shape\":\n                if self.node_reference[node.output[0]] != 1:\n                    continue\n                if i + 6 >= self.node_count:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n                node4 = self.mutable_graph_nodes[i + 3]\n                node5 = self.mutable_graph_nodes[i + 4]\n                node6 = self.mutable_graph_nodes[i + 5]\n                node7 = self.mutable_graph_nodes[i + 6]\n\n                if (\n                    node2.op_type != \"Gather\"\n                    or node3.op_type != \"Constant\"\n                    or node4.op_type != \"Unsqueeze\"\n                    or node5.op_type != \"Unsqueeze\"\n                    or node6.op_type != \"Concat\"\n                    or node7.op_type != \"Reshape\"\n                ):\n                    continue\n                if (\n                    self.node_reference[node2.output[0]] != 1\n                    or self.node_reference[node4.output[0]] != 1\n                    or self.node_reference[node5.output[0]] != 1\n                    or self.node_reference[node6.output[0]] != 1\n                ):\n                    continue\n                if (\n                    node2.input[0] != node.output[0]\n                    or node4.input[0] != node2.output[0]\n                    or node5.input[0] != node3.output[0]\n                    or node6.input[0] != node4.output[0]\n                    or node6.input[1] != node5.output[0]\n                    or node7.input[0] != node.input[0]\n                    or node7.input[1] != node6.output[0]\n                ):\n                    continue\n\n                # axis = 0\n                gather_axis = get_node_attr_i(node2, \"axis\")\n                if gather_axis != 0:\n                    continue\n\n                # indices = 0\n                if node2.input[1] not in self.weights:\n                    continue\n\n                gather_indices = get_node_attr_from_input_ai(\n                    self.weights[node2.input[1]]\n                )\n                if gather_indices.size != 1 or gather_indices[0] != 0:\n                    continue\n\n                # axes = (0)\n                unsqueeze_axes = get_node_attr_ai(node4, \"axes\")\n                if unsqueeze_axes.size != 1 or unsqueeze_axes[0] != 0:\n                    continue\n                unsqueeze_axes2 = get_node_attr_ai(node5, \"axes\")\n                if unsqueeze_axes2.size != 1 or unsqueeze_axes2[0] != 0:\n                    continue\n\n                # data = -1\n                if node5.input[0] not in self.weights:\n                    continue\n\n                unsqueeze2_data = get_node_attr_from_input_ai(\n                    self.weights[node5.input[0]]\n                )\n                if unsqueeze2_data.size != 1 or unsqueeze2_data[0] != -1:\n                    continue\n\n                # axis = 0\n                concat_axis = get_node_attr_i(node6, \"axis\")\n                if concat_axis != 0:\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n                node2.op_type = \"noop_reducedncnn\"\n                node4.op_type = \"noop_reducedncnn\"\n                node5.op_type = \"noop_reducedncnn\"\n                node6.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.input[0]] -= 1\n                self.node_reference[node.output[0]] -= 1\n                self.node_reference[node2.input[1]] -= 1\n                self.node_reference[node2.output[0]] -= 1\n                self.node_reference[node4.output[0]] -= 1\n                self.node_reference[node5.input[0]] -= 1\n                self.node_reference[node5.output[0]] -= 1\n                self.node_reference[node.output[0]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n                self.blob_names.pop(node4.output[0], None)\n                self.blob_names.pop(node5.output[0], None)\n                self.blob_names.pop(node6.output[0], None)\n\n                node7.op_type = \"Flatten\"\n                node7.ClearField(\"input\")\n                node7.input.append(node.input[0])\n\n                reduced_node_count[0] += 5\n                i += 5  # noqa\n\n    def fuse_pixelshuffle(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # PixelShuffle <= Reshape - Transpose - Reshape\n            # PixelShuffle <= Reshape - Transpose - Constant - Reshape\n            if node.op_type == \"Reshape\":\n                if self.node_reference[node.output[0]] != 1:\n                    continue\n\n                if len(node.input) == 1:\n                    shape = get_node_attr_ai(node, \"shape\")\n                else:\n                    # skip weight reshape\n                    if node.input[1] not in self.weights:\n                        continue\n\n                    shape = get_node_attr_from_input_ai(self.weights[node.input[1]])\n\n                # -1, 3, upscale_factor, upscale_factor, height, width\n                if (\n                    shape.size != 6\n                    or (shape[0] != 1 and shape[0] != -1)\n                    or shape[2] != shape[3]\n                    or i + 2 >= self.node_count\n                ):\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n\n                if node3.op_type == \"Constant\":\n                    if i + 3 >= self.node_count:\n                        continue\n\n                    node3 = self.mutable_graph_nodes[i + 3]\n\n                if node2.op_type != \"Transpose\" or node3.op_type != \"Reshape\":\n                    continue\n                if self.node_reference[node2.output[0]] != 1:\n                    continue\n\n                # 0 1 4 2 5 3\n                perm = get_node_attr_ai(node2, \"perm\")\n                if (\n                    perm.size != 6\n                    or perm[0] != 0\n                    or perm[1] != 1\n                    or perm[2] != 4\n                    or perm[3] != 2\n                    or perm[4] != 5\n                    or perm[5] != 3\n                ):\n                    continue\n\n                if len(node3.input) == 1:\n                    shape3 = get_node_attr_ai(node3, \"shape\")\n                else:\n                    if node3.input[1] not in self.weights:\n                        continue\n\n                    shape3 = get_node_attr_from_input_ai(self.weights[node3.input[1]])\n\n                # -1, 3, height, width\n                if (\n                    shape3.size != 4\n                    or (shape3[0] != 1 and shape3[0] != -1)\n                    or shape3[1] != shape[1]\n                    or shape3[2] != shape[2] * shape[4]\n                    or shape3[3] != shape[3] * shape[5]\n                ):\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n                node2.op_type = \"noop_reducedncnn\"\n\n                if len(node.input) == 2:\n                    self.node_reference[node.input[1]] -= 1\n                self.node_reference[node.output[0]] -= 1\n                self.node_reference[node2.output[0]] -= 1\n                if len(node3.input) == 2:\n                    self.node_reference[node3.input[1]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n\n                node3.op_type = \"PixelShuffle\"\n                node3.input[0] = node.input[0]\n\n                attr_group = AttributeProto(\n                    name=\"scale_factor\", i=shape[2], type=APT.INT\n                )\n                node3.attribute.append(attr_group)\n\n                reduced_node_count[0] += 2\n                i += 2  # noqa\n\n    def fuse_reorg(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # PixelShuffle <= Reshape - Transpose - Reshape\n            # PixelShuffle <= Reshape - Transpose - Constant - Reshape\n            if node.op_type == \"Reshape\":\n                if self.node_reference[node.output[0]] != 1:\n                    continue\n\n                if len(node.input) == 1:\n                    shape = get_node_attr_ai(node, \"shape\")\n                else:\n                    if node.input[1] not in self.weights:\n                        continue\n\n                    shape = get_node_attr_from_input_ai(self.weights[node.input[1]])\n\n                # -1, 3, out_height, block_size, out_width, block_size\n                if (\n                    shape.size != 6\n                    or (shape[0] != 1 and shape[0] != -1)\n                    or shape[3] != shape[5]\n                    or i + 2 >= self.node_count\n                ):\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n\n                if node3.op_type == \"Constant\":\n                    if i + 3 >= self.node_count:\n                        continue\n\n                    node3 = self.mutable_graph_nodes[i + 3]\n\n                if node2.op_type != \"Transpose\" or node3.op_type != \"Reshape\":\n                    continue\n                if self.node_reference[node2.output[0]] != 1:\n                    continue\n\n                # 0 1 3 5 2 4\n                perm = get_node_attr_ai(node2, \"perm\")\n                if (\n                    perm.size != 6\n                    or perm[0] != 0\n                    or perm[1] != 1\n                    or perm[2] != 3\n                    or perm[3] != 5\n                    or perm[4] != 2\n                    or perm[5] != 4\n                ):\n                    continue\n\n                if len(node3.input) == 1:\n                    shape3 = get_node_attr_ai(node3, \"shape\")\n                else:\n                    if node3.input[1] not in self.weights:\n                        continue\n\n                    shape3 = get_node_attr_from_input_ai(self.weights[node3.input[1]])\n\n                # -1, out_channels, out_height, out_width\n                if (\n                    shape3.size != 4\n                    or (shape3[0] != 1 and shape3[0] != -1)\n                    or shape3[1] != shape[1] * shape[3] * shape[5]\n                    or shape3[2] != shape[2]\n                    or shape3[3] != shape[4]\n                ):\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n                node2.op_type = \"noop_reducedncnn\"\n\n                if len(node.input) == 2:\n                    self.node_reference[node.input[1]] -= 1\n                self.node_reference[node.output[0]] -= 1\n                self.node_reference[node2.output[0]] -= 1\n                if len(node3.input) == 2:\n                    self.node_reference[node3.input[1]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n\n                node3.op_type = \"Reorg\"\n                node3.input[0] = node.input[0]\n\n                attr_group = AttributeProto(name=\"stride\", i=shape[3], type=APT.INT)\n                node3.attribute.append(attr_group)\n\n                reduced_node_count[0] += 2\n                i += 2  # noqa\n\n    def fuse_expand_broadcast(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # Add/Sub/Mul/Div/Min/Max <= Expand - Add/Sub/Mul/Div/Min/Max\n            if node.op_type == \"Expand\":\n                if self.node_reference[node.output[0]] != 1 or i + 1 >= self.node_count:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n\n                if node2.op_type not in [\"Add\", \"Sub\", \"Mul\", \"Div\", \"Min\", \"Max\"]:\n                    continue\n                if (\n                    node2.input[1] != node.output[0]\n                    and node2.input[0] != node.output[0]\n                ):\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.output[0]] -= 1\n                if len(node.input) == 2:\n                    self.node_reference[node.input[1]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n\n                if node2.input[0] == node.output[0]:\n                    node2.input[0] = node.input[0]\n                else:\n                    node2.input[1] = node.input[0]\n\n                reduced_node_count[0] += 1\n                i += 1  # noqa\n\n    def fuse_lstm_gru_rnn(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # LSTM(bi) <= LSTM(bi) - Transpose - Reshape - Transpose\n            if node.op_type in [\"LSTM\", \"GRU\", \"RNN\"]:\n                if self.node_reference[node.output[0]] != 1 or i + 2 >= self.node_count:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n\n                if node2.op_type != \"Transpose\" or node3.op_type != \"Reshape\":\n                    continue\n                if self.node_reference[node2.output[0]] != 1:\n                    continue\n                if (\n                    node2.input[0] != node.output[0]\n                    or node3.input[0] != node2.output[0]\n                ):\n                    continue\n\n                direction = get_node_attr_s(node, \"direction\")\n                if direction != \"bidirectional\":\n                    continue\n\n                # 0 2 1 3\n                perm = get_node_attr_ai(node2, \"perm\")\n                if (\n                    perm.size != 4\n                    or perm[0] != 0\n                    or perm[1] != 2\n                    or perm[2] != 1\n                    or perm[3] != 3\n                ):\n                    continue\n\n                if len(node3.input) == 1:\n                    shape = get_node_attr_ai(node3, \"shape\")\n                else:\n                    if node3.input[1] not in self.weights:\n                        continue\n\n                    shape = get_node_attr_from_input_ai(self.weights[node3.input[1]])\n\n                # 0 0 -1\n                if shape.size != 3 or shape[0] != 0 or shape[1] != 0 or shape[2] != -1:\n                    continue\n\n                # reduce\n                node2.op_type = \"noop_reducedncnn\"\n                node3.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.output[0]] -= 1\n                self.node_reference[node2.output[0]] -= 1\n                if len(node3.input) == 2:\n                    self.node_reference[node3.input[1]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n\n                node.output[0] = node3.output[0]\n\n                reduced_node_count[0] += 2\n                i += 2  # noqa\n\n                if i + 1 < self.node_count:\n                    if self.node_reference[node3.output[0]] != 1:\n                        continue\n\n                    node4 = self.mutable_graph_nodes[i + 1]\n\n                    if node4.op_type != \"Transpose\":\n                        continue\n                    if node4.input[0] != node.output[0]:\n                        continue\n\n                    # 1 0 2\n                    perm4 = get_node_attr_ai(node4, \"perm\")\n                    if (\n                        perm4.size != 3\n                        or perm4[0] != 1\n                        or perm4[1] != 0\n                        or perm4[2] != 2\n                    ):\n                        continue\n\n                    # reduce\n                    node4.op_type = \"noop_reducedncnn\"\n\n                    self.node_reference[node.output[0]] -= 1\n\n                    self.blob_names.pop(node.output[0], None)\n\n                    node.output[0] = node4.output[0]\n\n                    reduced_node_count[0] += 1\n                    i += 1  # noqa\n\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # LSTM(uni) <= LSTM(uni) - Squeeze - Transpose\n            if node.op_type in [\"LSTM\", \"GRU\", \"RNN\"]:\n                if self.node_reference[node.output[0]] != 1 or i + 1 >= self.node_count:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n\n                if node2.op_type != \"Squeeze\":\n                    continue\n                if node2.input[0] != node.output[0]:\n                    continue\n\n                direction = get_node_attr_s(node, \"direction\")\n                if direction == \"bidirectional\":\n                    continue\n\n                axes = get_node_attr_ai(node2, \"axes\")\n                if axes.size != 1 or axes[0] != 1:\n                    continue\n\n                # reduce\n                node2.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.output[0]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n\n                node.output[0] = node2.output[0]\n\n                reduced_node_count[0] += 1\n                i += 1  # noqa\n\n                if i + 1 < self.node_count:\n                    if self.node_reference[node2.output[0]] != 1:\n                        continue\n\n                    node3 = self.mutable_graph_nodes[i + 1]\n\n                    if node3.op_type != \"Transpose\":\n                        continue\n\n                    if node3.input[0] != node.output[0]:\n                        continue\n\n                    # 1 0 2\n                    perm4 = get_node_attr_ai(node3, \"perm\")\n                    if (\n                        perm4.size != 3\n                        or perm4[0] != 1\n                        or perm4[1] != 0\n                        or perm4[2] != 2\n                    ):\n                        continue\n\n                    # reduce\n                    node3.op_type = \"noop_reducedncnn\"\n\n                    self.node_reference[node.output[0]] -= 1\n\n                    self.blob_names.pop(node.output[0], None)\n\n                    node.output[0] = node3.output[0]\n\n                    reduced_node_count[0] += 1\n                    i += 1  # noqa\n\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # LSTM <= Transpose - LSTM\n            if node.op_type == \"Transpose\":\n                if self.node_reference[node.output[0]] != 1:\n                    continue\n\n                # 1 0 2\n                perm = get_node_attr_ai(node, \"perm\")\n                if perm.size != 3 or perm[0] != 1 or perm[1] != 0 or perm[2] != 2:\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n\n                if node2.op_type not in [\"LSTM\", \"GRU\", \"RNN\"]:\n                    continue\n                if node2.input[0] != node.output[0]:\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node.output[0]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n\n                node2.input[0] = node.input[0]\n\n                reduced_node_count[0] += 1\n                i += 1  # noqa\n\n    def fuse_multiheadattention(self, reduced_node_count: list[int]) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # MultiHeadAttention <= MatMul(q) - Add\n            #                      - MatMul(k) - Add\n            #                      - MatMul(v) - Add\n            #                      - Mul\n            #                      - Reshape - Transpose\n            #                      - Reshape - Reshape - Transpose - Transpose\n            #                      - Gemm - Softmax - Gemm - Transpose - Reshape - MatMul - Add\n            if node.op_type == \"MatMul\":\n                if (\n                    self.node_reference[node.output[0]] != 1\n                    or i + 19 >= self.node_count\n                ):\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n                node4 = self.mutable_graph_nodes[i + 3]\n                node5 = self.mutable_graph_nodes[i + 4]\n                node6 = self.mutable_graph_nodes[i + 5]\n                node7 = self.mutable_graph_nodes[i + 6]\n                node8 = self.mutable_graph_nodes[i + 7]\n                node9 = self.mutable_graph_nodes[i + 8]\n                node10 = self.mutable_graph_nodes[i + 9]\n                node11 = self.mutable_graph_nodes[i + 10]\n                node12 = self.mutable_graph_nodes[i + 11]\n                node13 = self.mutable_graph_nodes[i + 12]\n                node14 = self.mutable_graph_nodes[i + 13]\n                node15 = self.mutable_graph_nodes[i + 14]\n                node16 = self.mutable_graph_nodes[i + 15]\n                node17 = self.mutable_graph_nodes[i + 16]\n                node18 = self.mutable_graph_nodes[i + 17]\n                node19 = self.mutable_graph_nodes[i + 18]\n                node20 = self.mutable_graph_nodes[i + 19]\n\n                if (\n                    node2.op_type != \"Add\"\n                    or node3.op_type != \"MatMul\"\n                    or node4.op_type != \"Add\"\n                    or node5.op_type != \"MatMul\"\n                    or node6.op_type != \"Add\"\n                    or node7.op_type != \"Mul\"\n                    or node8.op_type != \"Reshape\"\n                    or node9.op_type != \"Transpose\"\n                    or node10.op_type != \"Reshape\"\n                    or node11.op_type != \"Reshape\"\n                    or node12.op_type != \"Transpose\"\n                    or node13.op_type != \"Transpose\"\n                    or node14.op_type != \"MatMul\"\n                    or node15.op_type != \"Softmax\"\n                    or node16.op_type != \"MatMul\"\n                    or node17.op_type != \"Transpose\"\n                    or node18.op_type != \"Reshape\"\n                    or node19.op_type != \"MatMul\"\n                    or node20.op_type != \"Add\"\n                ):\n                    continue\n                if (\n                    self.node_reference[node2.output[0]] != 1\n                    or self.node_reference[node3.output[0]] != 1\n                    or self.node_reference[node4.output[0]] != 1\n                    or self.node_reference[node5.output[0]] != 1\n                    or self.node_reference[node6.output[0]] != 1\n                    or self.node_reference[node7.output[0]] != 1\n                    or self.node_reference[node8.output[0]] != 1\n                    or self.node_reference[node9.output[0]] != 1\n                    or self.node_reference[node10.output[0]] != 1\n                    or self.node_reference[node11.output[0]] != 1\n                    or self.node_reference[node12.output[0]] != 1\n                    or self.node_reference[node13.output[0]] != 1\n                    or self.node_reference[node14.output[0]] != 1\n                    or self.node_reference[node15.output[0]] != 1\n                    or self.node_reference[node16.output[0]] != 1\n                    or self.node_reference[node17.output[0]] != 1\n                    or self.node_reference[node18.output[0]] != 1\n                    or self.node_reference[node19.output[0]] != 1\n                ):\n                    continue\n                if (\n                    node2.input[0] != node.output[0]\n                    or node4.input[0] != node3.output[0]\n                    or node6.input[0] != node5.output[0]\n                    or node7.input[0] != node2.output[0]\n                    or node8.input[0] != node7.output[0]\n                    or node9.input[0] != node8.output[0]\n                    or node10.input[0] != node4.output[0]\n                    or node11.input[0] != node6.output[0]\n                    or node12.input[0] != node11.output[0]\n                    or node13.input[0] != node10.output[0]\n                    or node14.input[0] != node9.output[0]\n                    or node14.input[1] != node13.output[0]\n                    or node15.input[0] != node14.output[0]\n                    or node16.input[0] != node15.output[0]\n                    or node16.input[1] != node12.output[0]\n                    or node17.input[0] != node16.output[0]\n                    or node18.input[0] != node17.output[0]\n                    or node19.input[0] != node18.output[0]\n                    or node20.input[0] != node19.output[0]\n                ):\n                    continue\n\n                q_B = get_node_attr_from_input_af(self.weights[node2.input[1]])\n                k_B = get_node_attr_from_input_af(self.weights[node4.input[1]])\n                v_B = get_node_attr_from_input_af(self.weights[node6.input[1]])\n                o_B = get_node_attr_from_input_af(self.weights[node20.input[1]])\n\n                if q_B.size != k_B.size or q_B.size != v_B.size or q_B.size != o_B.size:\n                    continue\n\n                embed_dim = q_B.size\n\n                # 1 0 2\n                perm9 = get_node_attr_ai(node9, \"perm\")\n                perm12 = get_node_attr_ai(node12, \"perm\")\n                if perm9.size != 3 or perm9[0] != 1 or perm9[1] != 0 or perm9[2] != 2:\n                    continue\n                if (\n                    perm12.size != 3\n                    or perm12[0] != 1\n                    or perm12[1] != 0\n                    or perm12[2] != 2\n                ):\n                    continue\n\n                # 1 2 0\n                perm13 = get_node_attr_ai(node13, \"perm\")\n                if (\n                    perm13.size != 3\n                    or perm13[0] != 1\n                    or perm13[1] != 2\n                    or perm13[2] != 0\n                ):\n                    continue\n\n                # 1 0 2\n                perm17 = get_node_attr_ai(node17, \"perm\")\n                if (\n                    perm17.size != 3\n                    or perm17[0] != 1\n                    or perm17[1] != 0\n                    or perm17[2] != 2\n                ):\n                    continue\n\n                softmax_axis = get_node_attr_i(node15, \"axis\")\n                if softmax_axis != 2:\n                    continue\n\n                # 1/-1 seqlen * num_heads, embed_dim / num_heads\n                if len(node8.input) == 1:\n                    shape8 = get_node_attr_ai(node8, \"shape\")\n                else:\n                    if node8.input[1] not in self.weights:\n                        continue\n\n                    shape8 = get_node_attr_from_input_ai(self.weights[node8.input[1]])\n                if len(node10.input) == 1:\n                    shape10 = get_node_attr_ai(node10, \"shape\")\n                else:\n                    if node10.input[1] not in self.weights:\n                        continue\n\n                    shape10 = get_node_attr_from_input_ai(self.weights[node10.input[1]])\n                if len(node11.input) == 1:\n                    shape11 = get_node_attr_ai(node11, \"shape\")\n                else:\n                    if node11.input[1] not in self.weights:\n                        continue\n\n                    shape11 = get_node_attr_from_input_ai(self.weights[node11.input[1]])\n\n                if shape8.size != 3 or shape10.size != 3 or shape11.size != 3:\n                    continue\n                if (\n                    shape8[1] != shape10[1]\n                    or shape8[1] != shape11[1]\n                    or shape8[2] != shape10[2]\n                    or shape8[2] != shape11[2]\n                ):\n                    continue\n\n                num_heads = embed_dim / shape8[2]\n\n                if len(node18.input) == 1:\n                    shape18 = get_node_attr_ai(node18, \"shape\")\n                else:\n                    if node18.input[1] not in self.weights:\n                        continue\n\n                    shape18 = get_node_attr_from_input_ai(self.weights[node18.input[1]])\n\n                if (\n                    shape18.size != 3\n                    or shape18[2] != embed_dim\n                    or shape18[1] * num_heads != shape8[1]\n                ):\n                    continue\n\n                node.op_type = \"noop_reducedncnn\"\n                node2.op_type = \"noop_reducedncnn\"\n                node3.op_type = \"noop_reducedncnn\"\n                node4.op_type = \"noop_reducedncnn\"\n                node5.op_type = \"noop_reducedncnn\"\n                node6.op_type = \"noop_reducedncnn\"\n                node7.op_type = \"noop_reducedncnn\"\n                node8.op_type = \"noop_reducedncnn\"\n                node9.op_type = \"noop_reducedncnn\"\n                node10.op_type = \"noop_reducedncnn\"\n                node11.op_type = \"noop_reducedncnn\"\n                node12.op_type = \"noop_reducedncnn\"\n                node13.op_type = \"noop_reducedncnn\"\n                node14.op_type = \"noop_reducedncnn\"\n                node15.op_type = \"noop_reducedncnn\"\n                node16.op_type = \"noop_reducedncnn\"\n                node17.op_type = \"noop_reducedncnn\"\n                node18.op_type = \"noop_reducedncnn\"\n                node19.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node2.input[0]] -= 1\n                self.node_reference[node4.input[0]] -= 1\n                self.node_reference[node6.input[0]] -= 1\n                self.node_reference[node7.input[0]] -= 1\n                self.node_reference[node7.input[1]] -= 1\n                self.node_reference[node8.input[0]] -= 1\n                if len(node8.input) == 2:\n                    self.node_reference[node8.input[1]] -= 1\n                self.node_reference[node9.input[0]] -= 1\n                self.node_reference[node10.input[0]] -= 1\n                if len(node10.input) == 2:\n                    self.node_reference[node10.input[1]] -= 1\n                self.node_reference[node11.input[0]] -= 1\n                if len(node11.input) == 2:\n                    self.node_reference[node11.input[1]] -= 1\n                self.node_reference[node12.input[0]] -= 1\n                self.node_reference[node13.input[0]] -= 1\n                self.node_reference[node14.input[0]] -= 1\n                self.node_reference[node14.input[1]] -= 1\n                self.node_reference[node15.input[0]] -= 1\n                self.node_reference[node16.input[0]] -= 1\n                self.node_reference[node16.input[1]] -= 1\n                self.node_reference[node17.input[0]] -= 1\n                self.node_reference[node18.input[0]] -= 1\n                if len(node18.input) == 2:\n                    self.node_reference[node18.input[1]] -= 1\n                self.node_reference[node19.input[0]] -= 1\n                self.node_reference[node20.input[0]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n                self.blob_names.pop(node3.output[0], None)\n                self.blob_names.pop(node4.output[0], None)\n                self.blob_names.pop(node5.output[0], None)\n                self.blob_names.pop(node6.output[0], None)\n                self.blob_names.pop(node7.output[0], None)\n                self.blob_names.pop(node8.output[0], None)\n                self.blob_names.pop(node9.output[0], None)\n                self.blob_names.pop(node10.output[0], None)\n                self.blob_names.pop(node11.output[0], None)\n                self.blob_names.pop(node12.output[0], None)\n                self.blob_names.pop(node13.output[0], None)\n                self.blob_names.pop(node14.output[0], None)\n                self.blob_names.pop(node15.output[0], None)\n                self.blob_names.pop(node16.output[0], None)\n                self.blob_names.pop(node17.output[0], None)\n                self.blob_names.pop(node18.output[0], None)\n                self.blob_names.pop(node19.output[0], None)\n\n                qw = node.input[1]\n                qb = node2.input[1]\n                kw = node3.input[1]\n                kb = node4.input[1]\n                vw = node5.input[1]\n                vb = node6.input[1]\n                ow = node19.input[1]\n                ob = node20.input[1]\n\n                node20.op_type = \"MultiHeadAttention\"\n                node20.ClearField(\"input\")\n                node20.input.append(node.input[0])\n                node20.input.append(node3.input[0])\n                node20.input.append(node5.input[0])\n                node20.input.append(qw)\n                node20.input.append(qb)\n                node20.input.append(kw)\n                node20.input.append(kb)\n                node20.input.append(vw)\n                node20.input.append(vb)\n                node20.input.append(ow)\n                node20.input.append(ob)\n\n                attr_embed_dim = AttributeProto(\n                    name=\"embed_dim\", i=embed_dim, type=APT.INT\n                )\n                node20.attribute.append(attr_embed_dim)\n\n                attr_num_heads = AttributeProto(\n                    name=\"num_heads\", i=num_heads, type=APT.INT\n                )\n                node20.attribute.append(attr_num_heads)\n\n                reduced_node_count[0] += 19\n                i += 19  # noqa\n\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # MultiHeadAttention <= MatMul(qkv) - Add - Split\n            #                      - Mul\n            #                      - Reshape - Transpose\n            #                      - Reshape - Reshape - Transpose - Transpose\n            #                      - Gemm - Softmax - Gemm - Transpose - Reshape - MatMul - Add\n            if node.op_type == \"MatMul\":\n                if (\n                    self.node_reference[node.output[0]] != 1\n                    or i + 16 >= self.node_count\n                ):\n                    continue\n\n                node2 = self.mutable_graph_nodes[i + 1]\n                node3 = self.mutable_graph_nodes[i + 2]\n                node4 = self.mutable_graph_nodes[i + 3]\n                node5 = self.mutable_graph_nodes[i + 4]\n                node6 = self.mutable_graph_nodes[i + 5]\n                node7 = self.mutable_graph_nodes[i + 6]\n                node8 = self.mutable_graph_nodes[i + 7]\n                node9 = self.mutable_graph_nodes[i + 8]\n                node10 = self.mutable_graph_nodes[i + 9]\n                node11 = self.mutable_graph_nodes[i + 10]\n                node12 = self.mutable_graph_nodes[i + 11]\n                node13 = self.mutable_graph_nodes[i + 12]\n                node14 = self.mutable_graph_nodes[i + 13]\n                node15 = self.mutable_graph_nodes[i + 14]\n                node16 = self.mutable_graph_nodes[i + 15]\n                node17 = self.mutable_graph_nodes[i + 16]\n\n                if (\n                    node2.op_type != \"Add\"\n                    or node3.op_type != \"Split\"\n                    or node4.op_type != \"Mul\"\n                    or node5.op_type != \"Reshape\"\n                    or node6.op_type != \"Transpose\"\n                    or node7.op_type != \"Reshape\"\n                    or node8.op_type != \"Reshape\"\n                    or node9.op_type != \"Transpose\"\n                    or node10.op_type != \"Transpose\"\n                    or node11.op_type != \"MatMul\"\n                    or node12.op_type != \"Softmax\"\n                    or node13.op_type != \"MatMul\"\n                    or node14.op_type != \"Transpose\"\n                    or node15.op_type != \"Reshape\"\n                    or node16.op_type != \"MatMul\"\n                    or node17.op_type != \"Add\"\n                ):\n                    continue\n                if (\n                    self.node_reference[node2.output[0]] != 1\n                    or self.node_reference[node3.output[0]] != 1\n                    or self.node_reference[node3.output[1]] != 1\n                    or self.node_reference[node3.output[2]] != 1\n                    or self.node_reference[node4.output[0]] != 1\n                    or self.node_reference[node5.output[0]] != 1\n                    or self.node_reference[node6.output[0]] != 1\n                    or self.node_reference[node7.output[0]] != 1\n                    or self.node_reference[node8.output[0]] != 1\n                    or self.node_reference[node9.output[0]] != 1\n                    or self.node_reference[node10.output[0]] != 1\n                    or self.node_reference[node11.output[0]] != 1\n                    or self.node_reference[node12.output[0]] != 1\n                    or self.node_reference[node13.output[0]] != 1\n                    or self.node_reference[node14.output[0]] != 1\n                    or self.node_reference[node15.output[0]] != 1\n                    or self.node_reference[node16.output[0]] != 1\n                ):\n                    continue\n                if (\n                    node2.input[0] != node.output[0]\n                    or node3.input[0] != node2.output[0]\n                    or node4.input[0] != node3.output[0]\n                    or node5.input[0] != node4.output[0]\n                    or node6.input[0] != node5.output[0]\n                    or node7.input[0] != node3.output[1]\n                    or node8.input[0] != node3.output[2]\n                    or node9.input[0] != node8.output[0]\n                    or node10.input[0] != node7.output[0]\n                    or node11.input[0] != node6.output[0]\n                    or node11.input[1] != node10.output[0]\n                    or node12.input[0] != node11.output[0]\n                    or node13.input[0] != node12.output[0]\n                    or node13.input[1] != node9.output[0]\n                    or node14.input[0] != node13.output[0]\n                    or node15.input[0] != node14.output[0]\n                    or node16.input[0] != node15.output[0]\n                    or node17.input[0] != node16.output[0]\n                ):\n                    continue\n\n                qkv_B = get_node_attr_from_input_af(self.weights[node2.input[1]])\n                o_B = get_node_attr_from_input_af(self.weights[node17.input[1]])\n\n                if qkv_B.size != o_B.size * 3:\n                    continue\n\n                embed_dim = o_B.size\n\n                # 1 0 2\n                perm6 = get_node_attr_ai(node6, \"perm\")\n                perm9 = get_node_attr_ai(node9, \"perm\")\n                if perm6.size != 3 or perm6[0] != 1 or perm6[1] != 0 or perm6[2] != 2:\n                    continue\n                if perm9.size != 3 or perm9[0] != 1 or perm9[1] != 0 or perm9[2] != 2:\n                    continue\n\n                # 1 2 0\n                perm10 = get_node_attr_ai(node10, \"perm\")\n                if (\n                    perm10.size != 3\n                    or perm10[0] != 1\n                    or perm10[1] != 2\n                    or perm10[2] != 0\n                ):\n                    continue\n\n                # 1 0 2\n                perm14 = get_node_attr_ai(node14, \"perm\")\n                if (\n                    perm14.size != 3\n                    or perm14[0] != 1\n                    or perm14[1] != 0\n                    or perm14[2] != 2\n                ):\n                    continue\n\n                softmax_axis = get_node_attr_i(node12, \"axis\")\n                if softmax_axis != 2:\n                    continue\n\n                # 1/-1, seqlen * num_heads, embed_dim / num_heads\n                if len(node5.input) == 1:\n                    shape5 = get_node_attr_ai(node5, \"shape\")\n                else:\n                    if node5.input[1] not in self.weights:\n                        continue\n\n                    shape5 = get_node_attr_from_input_ai(self.weights[node5.input[1]])\n                if len(node7.input) == 1:\n                    shape7 = get_node_attr_ai(node7, \"shape\")\n                else:\n                    if node7.input[1] not in self.weights:\n                        continue\n\n                    shape7 = get_node_attr_from_input_ai(self.weights[node7.input[1]])\n                if len(node8.input) == 1:\n                    shape8 = get_node_attr_ai(node8, \"shape\")\n                else:\n                    if node8.input[1] not in self.weights:\n                        continue\n\n                    shape8 = get_node_attr_from_input_ai(self.weights[node8.input[1]])\n\n                if (\n                    shape5[1] != shape7[1]\n                    or shape5[1] != shape8[1]\n                    or shape5[2] != shape7[2]\n                    or shape5[2] != shape8[2]\n                ):\n                    continue\n\n                num_heads = embed_dim / shape5[2]\n\n                # 1, seqlen, embed_dim\n                if len(node15.input) == 1:\n                    shape15 = get_node_attr_ai(node15, \"shape\")\n                else:\n                    if node15.input[1] not in self.weights:\n                        continue\n\n                    shape15 = get_node_attr_from_input_ai(self.weights[node15.input[1]])\n\n                if (\n                    shape15.size != 3\n                    or shape15[2] != embed_dim\n                    or shape15[1] * num_heads != shape8[1]\n                ):\n                    continue\n\n                # reduce\n                node.op_type = \"noop_reducedncnn\"\n                node2.op_type = \"noop_reducedncnn\"\n                node3.op_type = \"noop_reducedncnn\"\n                node4.op_type = \"noop_reducedncnn\"\n                node5.op_type = \"noop_reducedncnn\"\n                node6.op_type = \"noop_reducedncnn\"\n                node7.op_type = \"noop_reducedncnn\"\n                node8.op_type = \"noop_reducedncnn\"\n                node9.op_type = \"noop_reducedncnn\"\n                node10.op_type = \"noop_reducedncnn\"\n                node11.op_type = \"noop_reducedncnn\"\n                node12.op_type = \"noop_reducedncnn\"\n                node13.op_type = \"noop_reducedncnn\"\n                node14.op_type = \"noop_reducedncnn\"\n                node15.op_type = \"noop_reducedncnn\"\n                node16.op_type = \"noop_reducedncnn\"\n\n                self.node_reference[node2.input[0]] -= 1\n                self.node_reference[node3.input[0]] -= 1\n                self.node_reference[node4.input[0]] -= 1\n                self.node_reference[node4.input[1]] -= 1\n                self.node_reference[node5.input[0]] -= 1\n                if len(node5.input) == 2:\n                    self.node_reference[node5.input[1]] -= 1\n                self.node_reference[node6.input[0]] -= 1\n                self.node_reference[node7.input[0]] -= 1\n                if len(node7.input) == 2:\n                    self.node_reference[node7.input[1]] -= 1\n                self.node_reference[node8.input[0]] -= 1\n                if len(node8.input) == 2:\n                    self.node_reference[node8.input[1]] -= 1\n                self.node_reference[node9.input[0]] -= 1\n                self.node_reference[node10.input[0]] -= 1\n                self.node_reference[node11.input[0]] -= 1\n                self.node_reference[node11.input[1]] -= 1\n                self.node_reference[node12.input[0]] -= 1\n                self.node_reference[node13.input[0]] -= 1\n                self.node_reference[node13.input[1]] -= 1\n                self.node_reference[node14.input[0]] -= 1\n                self.node_reference[node15.input[0]] -= 1\n                if len(node15.input) == 2:\n                    self.node_reference[node15.input[1]] -= 1\n                self.node_reference[node16.input[0]] -= 1\n                self.node_reference[node17.input[0]] -= 1\n\n                self.blob_names.pop(node.output[0], None)\n                self.blob_names.pop(node2.output[0], None)\n                self.blob_names.pop(node3.output[0], None)\n                self.blob_names.pop(node3.output[1], None)\n                self.blob_names.pop(node3.output[2], None)\n                self.blob_names.pop(node4.output[0], None)\n                self.blob_names.pop(node5.output[0], None)\n                self.blob_names.pop(node6.output[0], None)\n                self.blob_names.pop(node7.output[0], None)\n                self.blob_names.pop(node8.output[0], None)\n                self.blob_names.pop(node9.output[0], None)\n                self.blob_names.pop(node10.output[0], None)\n                self.blob_names.pop(node11.output[0], None)\n                self.blob_names.pop(node12.output[0], None)\n                self.blob_names.pop(node13.output[0], None)\n                self.blob_names.pop(node14.output[0], None)\n                self.blob_names.pop(node15.output[0], None)\n                self.blob_names.pop(node16.output[0], None)\n\n                qkvw = node.input[1]\n                qkvb = node2.input[1]\n                ow = node16.input[1]\n                ob = node17.input[1]\n\n                node17.op_type = \"MultiHeadAttention\"\n                node17.ClearField(\"input\")\n                node17.input.append(node.input[0])\n                node17.input.append(qkvw)\n                node17.input.append(qkvb)\n                node17.input.append(ow)\n                node17.input.append(ob)\n\n                attr_embed_dim = AttributeProto(\n                    name=\"embed_dim\", i=embed_dim, type=APT.INT\n                )\n                node17.attribute.append(attr_embed_dim)\n\n                attr_num_heads = AttributeProto(\n                    name=\"num_heads\", i=num_heads, type=APT.INT\n                )\n                node17.attribute.append(attr_num_heads)\n\n                reduced_node_count[0] += 16\n                i += 16  # noqa\n\n    def fuse_binaryop_with_scalar(self) -> None:\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # Add/Sub/Mul/Div/Min/Max/Pow(a, x)\n            if node.op_type in [\"Add\", \"Sub\", \"Mul\", \"Div\", \"Min\", \"Max\", \"Pow\"]:\n                if node.input[0] not in self.weights:\n                    continue\n\n                scalar_b = self.weights[node.input[0]]\n                if (\n                    len(scalar_b.dims) != 0\n                    or get_tensor_proto_data_size(scalar_b, scalar_b.data_type) != 1\n                ):\n                    continue\n\n                if node.op_type == \"Sub\":\n                    node.op_type = \"RSub\"\n                elif node.op_type == \"Div\":\n                    node.op_type = \"RDiv\"\n\n                b = get_node_attr_from_input_f(scalar_b)\n\n                self.node_reference[node.input[0]] -= 1\n\n                node_input = node.input[1]\n                node.ClearField(\"input\")\n                node.input.append(node_input)\n\n                attr_with_scalar = AttributeProto(name=\"with_scalar\", i=1, type=APT.INT)\n                node.attribute.append(attr_with_scalar)\n\n                attr_b = AttributeProto(name=\"b\", f=b, type=APT.FLOAT)\n                node.attribute.append(attr_b)\n\n        for i in range(self.node_count):\n            node = self.mutable_graph_nodes[i]\n\n            # Add/Sub/Mul/Div/Min/Max/Pow(x, b)\n            if node.op_type in [\"Add\", \"Sub\", \"Mul\", \"Div\", \"Min\", \"Max\", \"Pow\"]:\n                if node.input[1] not in self.weights:\n                    continue\n\n                scalar_b = self.weights[node.input[1]]\n                if (\n                    len(scalar_b.dims) != 0\n                    or get_tensor_proto_data_size(scalar_b, scalar_b.data_type) != 1\n                ):\n                    continue\n\n                b = get_node_attr_from_input_f(scalar_b)\n\n                self.node_reference[node.input[1]] -= 1\n\n                node_input = node.input[0]\n                node.ClearField(\"input\")\n                node.input.append(node_input)\n\n                attr_with_scalar = AttributeProto(name=\"with_scalar\", i=1, type=APT.INT)\n                node.attribute.append(attr_with_scalar)\n\n                attr_b = AttributeProto(name=\"b\", f=b, type=APT.FLOAT)\n                node.attribute.append(attr_b)\n\n    def convert(self, is_fp16: bool = False, include_mem_data: bool = True):\n        if is_fp16:\n            logger.debug(\"NCNN mode: fp16\")\n        else:\n            logger.debug(\"NCNN mode: fp32\")\n\n        # Topological sort\n        i = 0\n        while i < self.node_count:\n            node = self.mutable_graph_nodes[i]\n            swapnode = False\n            missing_input_name = None\n            for input_name in node.input:\n                if (\n                    input_name\n                    and input_name not in self.producers\n                    and input_name not in self.weights\n                ):\n                    swapnode = True\n                    missing_input_name = input_name\n                    break\n\n            # If nothing missing, add outputs to producers and continue\n            # to next node\n            if not swapnode:\n                for output_name in node.output:\n                    if output_name:\n                        self.producers[output_name] = None\n\n                i += 1\n                continue\n\n            # find node that produces missing_input_name\n            swap_j = 0\n            for j, nodeq in enumerate(self.mutable_graph_nodes, i + 1):\n                swap_j = j\n                found = False\n                for output_name in nodeq.output:\n                    if output_name == missing_input_name:\n                        found = True\n                        break\n\n                if found:\n                    break\n            else:\n                raise RuntimeError(\n                    f\"Cannot find node that produces {missing_input_name}, \"\n                    f\"which is required by node {i} ({node.name}).\"\n                )\n\n            self.swap_nodes(i, swap_j)\n\n        # global definition line\n        # [layer count][blob count]\n        for node in self.onnx_graph.node:\n            op = node.op_type\n            if not node.name:\n                node.name = node.output[0]\n\n            if op == \"Constant\":\n                self.weights[node.output[0]] = get_node_attr_tensor(node, \"value\")\n\n            for input_name in node.input:\n                self.blob_names[input_name] = None\n\n                if input_name not in self.node_reference:\n                    self.node_reference[input_name] = 1\n                else:\n                    self.node_reference[input_name] += 1\n\n            if op == \"Dropout\":\n                output_name = node.output[0]\n                self.blob_names[output_name] = None\n                self.node_reference[output_name] = 0\n                continue\n\n            for output_name in node.output:\n                self.blob_names[output_name] = None\n                self.node_reference[output_name] = 0\n\n        # include Input node\n        input_node_count = 0\n        for graph_input in self.onnx_graph.input:\n            input_name = graph_input.name\n\n            # check weight\n            if input_name not in self.weights:\n                self.blob_names[input_name] = None\n                input_node_count += 1\n\n        # op chain fusion\n        reduced_node_count = [0]\n        self.fuse_weight_reshape(reduced_node_count)\n        self.fuse_weight_transpose(reduced_node_count)\n        self.fuse_shufflechannel(reduced_node_count)\n        self.fuse_shufflechannel_split(reduced_node_count)\n        self.fuse_hardsigmoid(reduced_node_count)\n        self.fuse_hardswish(reduced_node_count)\n        self.fuse_swish(reduced_node_count)\n        self.fuse_batchnorm1d_squeeze_unsqueeze(reduced_node_count)\n        self.fuse_unsqueeze_prelu(reduced_node_count)\n        self.fuse_normalize(reduced_node_count)\n        self.fuse_groupnorm(reduced_node_count)\n        self.fuse_layernorm(reduced_node_count)\n        self.fuse_flatten(reduced_node_count)\n        self.fuse_pixelshuffle(reduced_node_count)\n        self.fuse_reorg(reduced_node_count)\n        self.fuse_expand_broadcast(reduced_node_count)\n        self.fuse_lstm_gru_rnn(reduced_node_count)\n        self.fuse_multiheadattention(reduced_node_count)\n        self.fuse_binaryop_with_scalar()\n        self.fuse_rewrite_gather()\n\n        # reduce common const weight node_reference\n        for node in self.onnx_graph.node:\n            op = node.op_type\n            if op == \"BatchNormalization\":\n                self.node_reference[node.input[1]] -= 1\n                self.node_reference[node.input[2]] -= 1\n                self.node_reference[node.input[3]] -= 1\n                self.node_reference[node.input[4]] -= 1\n            elif op == \"BiasGelu\":\n                self.node_reference[node.input[1]] -= 1\n            elif op == \"Clip\":\n                if len(node.input) == 3:\n                    self.node_reference[node.input[1]] -= 1\n                    self.node_reference[node.input[2]] -= 1\n            elif op == \"Conv\":\n                self.node_reference[node.input[1]] -= 1\n                if len(node.input) == 3:\n                    self.node_reference[node.input[2]] -= 1\n            elif op == \"ConvTranspose\":\n                self.node_reference[node.input[1]] -= 1\n                if len(node.input) == 3:\n                    self.node_reference[node.input[2]] -= 1\n            elif op == \"EmbedLayerNormalization\":\n                self.node_reference[node.input[1]] -= 1\n                self.node_reference[node.input[2]] -= 1\n                self.node_reference[node.input[3]] -= 1\n                self.node_reference[node.input[4]] -= 1\n                self.node_reference[node.input[5]] -= 1\n                self.node_reference[node.input[6]] -= 1\n            elif op == \"Gemm\":\n                alpha = get_node_attr_f(node, \"alpha\", 1)\n                beta = get_node_attr_f(node, \"beta\", 1)\n                transA = get_node_attr_i(node, \"transA\", 0)\n                transB = get_node_attr_i(node, \"transB\", 0)\n\n                if alpha == 1 and beta == 1 and transA == 0 and transB == 1:\n                    # InnerProduct-like A * B + C\n                    self.node_reference[node.input[1]] -= 1\n                    self.node_reference[node.input[2]] -= 1\n            elif op == \"GroupNorm\":\n                affine = get_node_attr_i(node, \"affine\", 1)\n                if affine:\n                    self.node_reference[node.input[1]] -= 1\n                    self.node_reference[node.input[2]] -= 1\n            elif op == \"GRU\":\n                for gru_input in node.input:\n                    self.node_reference[gru_input] -= 1\n            elif op == \"InstanceNormalization\":\n                self.node_reference[node.input[1]] -= 1\n                self.node_reference[node.input[2]] -= 1\n            elif op == \"LayerNorm\":\n                affine = get_node_attr_i(node, \"affine\", 1)\n                if affine:\n                    self.node_reference[node.input[1]] -= 1\n                    self.node_reference[node.input[2]] -= 1\n            elif op == \"LSTM\":\n                for lstm_input in node.input:\n                    self.node_reference[lstm_input] -= 1\n            elif op == \"MatMul\":\n                if (\n                    node.input[1] in self.weights\n                    and len(self.weights[node.input[1]].dims) == 2\n                ):\n                    # InnerProduct\n                    self.node_reference[node.input[1]] -= 1\n            elif op == \"MultiHeadAttention\":\n                if len(node.input) == 5:\n                    self.node_reference[node.input[1]] -= 1\n                    self.node_reference[node.input[2]] -= 1\n                    self.node_reference[node.input[3]] -= 1\n                    self.node_reference[node.input[4]] -= 1\n                else:\n                    self.node_reference[node.input[3]] -= 1\n                    self.node_reference[node.input[4]] -= 1\n                    self.node_reference[node.input[5]] -= 1\n                    self.node_reference[node.input[6]] -= 1\n                    self.node_reference[node.input[7]] -= 1\n                    self.node_reference[node.input[8]] -= 1\n                    self.node_reference[node.input[9]] -= 1\n                    self.node_reference[node.input[10]] -= 1\n            elif op == \"Pad\":\n                if len(node.input) >= 2:\n                    self.node_reference[node.input[1]] -= 1\n            elif op == \"PRelu\":\n                self.node_reference[node.input[1]] -= 1\n            elif op == \"Reshape\":\n                if len(node.input) >= 2:\n                    self.node_reference[node.input[1]] -= 1\n            elif op == \"Resize\":\n                if len(node.input) == 2:\n                    # opset 10\n                    self.node_reference[node.input[1]] -= 1\n                else:\n                    # opset 11+\n                    self.node_reference[node.input[1]] -= 1\n                    self.node_reference[node.input[2]] -= 1\n                    if len(node.input) >= 4:\n                        self.node_reference[node.input[3]] -= 1\n            elif op == \"RNN\":\n                for rnn_input in node.input:\n                    self.node_reference[rnn_input] -= 1\n            elif op == \"SkipLayerNormalization\":\n                self.node_reference[node.input[2]] -= 1\n                self.node_reference[node.input[3]] -= 1\n                self.node_reference[node.input[4]] -= 1\n            elif op == \"Slice\":\n                if len(node.input) >= 2:\n                    self.node_reference[node.input[1]] -= 1\n                    self.node_reference[node.input[2]] -= 1\n                    if len(node.input) >= 4:\n                        self.node_reference[node.input[3]] -= 1\n                    if len(node.input) >= 5:\n                        self.node_reference[node.input[4]] -= 1\n            elif op == \"Upsample\":\n                if len(node.input) >= 2:\n                    self.node_reference[node.input[1]] -= 1\n            elif op in (\"adaptive_avg_pool2d\", \"adaptive_max_pool2d\"):\n                if len(node.input) >= 2:\n                    self.node_reference[node.input[1]] -= 1\n\n        # count all weight node with zero reference\n        zero_reference_weight_node_count = 0\n        for input_name in self.weights.keys():\n            # there may be some weight nodes in initializer but none of the graph nodes use them\n            # add them to blob_names so we could get proper blob count later\n            self.blob_names[input_name] = None\n\n            refcount = self.node_reference[input_name]\n            if refcount == 0:\n                zero_reference_weight_node_count += 1\n\n        # we always treat constant nodes as weights or binaryop_weights\n        # do not count it twice for layer_count\n        constant_node_count_moved_to_weight = 0\n        for node in self.onnx_graph.node:\n            if node.op_type == \"Constant\":\n                constant_node_count_moved_to_weight += 1\n\n        # some ops may have anonymous input\n        # LSTM sequence_lens\n        self.blob_names.pop(\"\", None)\n        self.node_reference.pop(\"\", None)\n\n        # remove node_reference entries with references equal to one\n        split_layer_count = 0\n        splitncnn_blob_count = 0\n\n        # split node reference\n        split_node_reference = {}\n        for ref, count in self.node_reference.items():\n            if count > 1:\n                split_layer_count += 1\n                splitncnn_blob_count += count\n                split_node_reference[ref] = count\n\n        ncnn_node_count = (\n            self.node_count\n            - constant_node_count_moved_to_weight\n            + len(self.weights)\n            - zero_reference_weight_node_count\n            - reduced_node_count[0]\n            + input_node_count\n            + split_layer_count\n        )\n        ncnn_blob_count = (\n            len(self.blob_names)\n            - zero_reference_weight_node_count\n            + splitncnn_blob_count\n        )\n        ncnn_model = NcnnModel(ncnn_node_count, ncnn_blob_count)\n        logger.debug(\n            f\"Node count: {ncnn_model.node_count}, Blob count: {ncnn_model.blob_count}\"\n        )\n\n        bin_length = 0\n        for i, graph_input in enumerate(self.onnx_graph.input):\n            input_name = graph_input.name\n\n            # Make sure input is not in weights\n            if input_name not in self.weights:\n                ncnn_model.add_layer(\n                    NcnnLayer(\"Input\", input_name, 0, 1, outputs=[input_name])\n                )\n\n                refcount = self.node_reference[input_name]\n                if refcount > 1:\n                    layer_input_list = [\n                        f\"{input_name}_splitncnn_{j}\" for j in range(refcount)\n                    ]\n                    ncnn_model.add_layer(\n                        NcnnLayer(\n                            \"Split\",\n                            f\"splitncnn_input{i}\",\n                            1,\n                            refcount,\n                            [input_name],\n                            layer_input_list,\n                        )\n                    )\n\n        # place MemoryData next if it is being included\n        internal_split = 0\n        if include_mem_data:\n            for input_name, M in self.weights.items():\n                refcount = self.node_reference[input_name]\n                if refcount != 0:\n                    layer = NcnnLayer(\"MemoryData\", input_name, 0, 1, [input_name])\n\n                    M_dims_size = len(M.dims)\n                    if M_dims_size == 0:\n                        layer.add_param(0, get_tensor_proto_data_size(M, M.data_type))\n                    elif M_dims_size == 1:\n                        layer.add_param(0, M.dims[0])\n                    elif M_dims_size == 2:\n                        layer.add_param(0, M.dims[1])\n                        if M.dims[0] != 1:\n                            layer.add_param(1, M.dims[0])\n                    elif M_dims_size == 3:\n                        layer.add_param(0, M.dims[2])\n                        layer.add_param(1, M.dims[1])\n                        if M.dims[0] != 1:\n                            layer.add_param(2, M.dims[0])\n                    elif M_dims_size == 4:\n                        layer.add_param(0, M.dims[3])\n                        layer.add_param(1, M.dims[2])\n                        layer.add_param(2, M.dims[1])\n\n                    bin_length += self.add_weight(layer, \"MemoryData\", M)\n\n                    ncnn_model.add_layer(layer)\n\n                    if refcount > 1:\n                        layer_output_list = [\n                            f\"{input_name}_splitncnn_{i}\" for i in range(refcount)\n                        ]\n                        ncnn_model.add_layer(\n                            NcnnLayer(\n                                \"Split\",\n                                f\"splitncnn_{internal_split}\",\n                                1,\n                                refcount,\n                                [input_name],\n                                layer_output_list,\n                            )\n                        )\n\n                        internal_split += 1\n\n        for node in self.onnx_graph.node:\n            op = node.op_type\n\n            if op == \"noop_reducedncnn\":\n                continue\n\n            name = node.name\n            if not name:\n                name = node.output[0]\n\n            input_size = len(node.input)\n            output_size = len(node.output)\n\n            for input_name in node.input:\n                # check weight\n                if not input_name or (\n                    input_name in self.weights and self.node_reference[input_name] == 0\n                ):\n                    input_size -= 1\n\n            layer = NcnnLayer()\n            if op in [\n                \"Abs\",\n                \"Acos\",\n                \"Asin\",\n                \"Atan\",\n                \"Ceil\",\n                \"Cos\",\n                \"Exp\",\n                \"Floor\",\n                \"Log\",\n                \"Neg\",\n                \"Reciprocal\",\n                \"Sin\",\n                \"Sqrt\",\n                \"Tan\",\n                \"Tanh\",\n            ]:\n                layer.op_type = \"UnaryOp\"\n            elif op in [\n                \"Add\",\n                \"Div\",\n                \"Max\",\n                \"Min\",\n                \"Mul\",\n                \"Pow\",\n                \"RDiv\",\n                \"RSub\",\n                \"Sub\",\n            ]:\n                layer.op_type = \"BinaryOp\"\n            elif op in (\"AveragePool\", \"MaxPool\"):\n                kernel_shape = get_node_attr_ai(node, \"kernel_shape\")\n                if kernel_shape.size == 1:\n                    layer.op_type = \"Pooling1D\"\n                else:\n                    layer.op_type = \"Pooling\"\n            elif op == \"BatchNormalization\":\n                layer.op_type = \"BatchNorm\"\n            elif op == \"BiasGelu\":\n                layer.op_type = \"BiasGelu\"\n            elif op == \"Clip\":\n                layer.op_type = \"Clip\"\n            elif op == \"Concat\":\n                layer.op_type = \"Concat\"\n            elif op == \"Constant\":\n                continue\n            elif op == \"Conv\":\n                kernel_shape = get_node_attr_ai(node, \"kernel_shape\")\n                if kernel_shape.size == 1:\n                    layer.op_type = \"Convolution1D\"\n                else:\n                    group = get_node_attr_i(node, \"group\", 1)\n                    if group > 1:\n                        layer.op_type = \"ConvolutionDepthWise\"\n                    else:\n                        layer.op_type = \"Convolution\"\n            elif op == \"ConvTranspose\":\n                group = get_node_attr_i(node, \"group\", 1)\n                if group > 1:\n                    layer.op_type = \"DeconvolutionDepthWise\"\n                else:\n                    layer.op_type = \"Deconvolution\"\n            elif op in (\"Crop\", \"Slice\"):\n                layer.op_type = \"Crop\"\n            elif op in (\"DepthToSpace\", \"PixelShuffle\"):\n                layer.op_type = \"PixelShuffle\"\n            elif op == \"Dropout\":\n                layer.op_type = \"Dropout\"\n                output_size = 1\n            elif op == \"Elu\":\n                layer.op_type = \"ELU\"\n            elif op == \"EmbedLayerNormalization\":\n                layer.op_type = \"EmbedLayerNormalization\"\n            elif op == \"Flatten\":\n                layer.op_type = \"Flatten\"\n            elif op == \"Gelu\":\n                layer.op_type = \"GELU\"\n            elif op == \"Gemm\":\n                alpha = get_node_attr_f(node, \"alpha\", 1)\n                beta = get_node_attr_f(node, \"beta\", 1)\n                transA = get_node_attr_i(node, \"transA\", 0)\n                transB = get_node_attr_i(node, \"transB\", 0)\n\n                if alpha == 1 and beta == 1 and transA == 0 and transB == 1:\n                    # InnerProduct-like A * B + C\n                    layer.op_type = \"InnerProduct\"\n                else:\n                    layer.op_type = \"Gemm\"\n            elif op in [\n                \"GlobalAveragePool\",\n                \"GlobalMaxPool\",\n                \"adaptive_avg_pool2d\",\n                \"adaptive_max_pool2d\",\n            ]:\n                layer.op_type = \"Pooling\"\n            elif op == \"GroupNorm\":\n                layer.op_type = \"GroupNorm\"\n            elif op == \"GRU\":\n                layer.op_type = \"GRU\"\n            elif op == \"HardSigmoid\":\n                layer.op_type = \"HardSigmoid\"\n            elif op == \"HardSwish\":\n                layer.op_type = \"HardSwish\"\n            elif op == \"ImageScaler\":\n                layer.op_type = \"Scale\"\n            elif op == \"InstanceNormalization\":\n                layer.op_type = \"InstanceNorm\"\n            elif op == \"LayerNorm\":\n                layer.op_type = \"LayerNorm\"\n            elif op in (\"LeakyRelu\", \"Relu\"):\n                layer.op_type = \"ReLU\"\n            elif op == \"LRN\":\n                layer.op_type = \"LRN\"\n            elif op == \"LSTM\":\n                layer.op_type = \"LSTM\"\n            elif op == \"MatMul\":\n                if (\n                    node.input[1] in self.weights\n                    and len(self.weights[node.input[1]].dims) == 2\n                ):\n                    layer.op_type = \"InnerProduct\"\n                else:\n                    layer.op_type = \"Gemm\"\n            elif op == \"MultiHeadAttention\":\n                layer.op_type = \"MultiHeadAttention\"\n            elif op == \"Normalize\":\n                layer.op_type = \"Normalize\"\n            elif op == \"Pad\":\n                layer.op_type = \"Padding\"\n            elif op == \"PRelu\":\n                layer.op_type = \"PReLU\"\n            elif op in [\n                \"ReduceMax\",\n                \"ReduceMin\",\n                \"ReduceMean\",\n                \"ReduceProd\",\n                \"ReduceSum\",\n                \"ReduceSumSquare\",\n                \"ReduceL1\",\n                \"ReduceL2\",\n                \"ReduceLogSum\",\n                \"ReduceLogSumExp\",\n            ]:\n                layer.op_type = \"Reduction\"\n            elif op == \"Reorg\":\n                layer.op_type = \"Reorg\"\n            elif op == \"Reshape\":\n                layer.op_type = \"Reshape\"\n            elif op == \"RNN\":\n                layer.op_type = \"RNN\"\n            elif op == \"ShuffleChannel\":\n                layer.op_type = \"ShuffleChannel\"\n            elif op == \"Sigmoid\":\n                layer.op_type = \"Sigmoid\"\n            elif op == \"SkipLayerNormalization\":\n                layer.op_type = \"SkipLayerNormalization\"\n            elif op == \"Softmax\":\n                layer.op_type = \"Softmax\"\n            elif op == \"Softplus\":\n                layer.op_type = \"Softplus\"\n            elif op == \"Split\":\n                layer.op_type = \"Slice\"\n            elif op == \"Squeeze\":\n                layer.op_type = \"Squeeze\"\n            elif op == \"Sum\":\n                layer.op_type = \"Eltwise\"\n            elif op == \"Swish\":\n                layer.op_type = \"Swish\"\n            elif op == \"Transpose\":\n                layer.op_type = \"Permute\"\n            elif op in (\"Upsample\", \"Resize\"):\n                layer.op_type = \"Interp\"\n            elif op == \"Unsqueeze\":\n                layer.op_type = \"ExpandDims\"\n            else:\n                error_msg = f\"{op} not currently supported by NCNN.\"\n                raise ValueError(error_msg)\n\n            layer.name = name\n            layer.num_inputs = input_size\n            layer.num_outputs = output_size\n            layer.params.set_op(layer.op_type)\n\n            for input_name in node.input:\n                # check weight\n                if input_name and not (\n                    input_name in self.weights and self.node_reference[input_name] == 0\n                ):\n                    if input_name in split_node_reference:\n                        refidx = split_node_reference[input_name] - 1\n                        split_node_reference[input_name] = refidx\n                        input_name = f\"{input_name}_splitncnn_{refidx}\"  # noqa\n\n                    layer.inputs.append(input_name)\n\n            for o in range(output_size):\n                layer.outputs.append(node.output[o])\n\n            if op == \"Abs\":\n                layer.add_param(0, UOT.ABS)\n            elif op == \"Acos\":\n                layer.add_param(0, UOT.ACOS)\n            elif layer.op_type == \"BinaryOp\":\n                if op == \"Add\":\n                    layer.add_param(0, BOT.ADD)\n                elif op == \"Div\":\n                    layer.add_param(0, BOT.DIV)\n                elif op == \"Max\":\n                    layer.add_param(0, BOT.MAX)\n                elif op == \"Min\":\n                    layer.add_param(0, BOT.MIN)\n                elif op == \"Mul\":\n                    layer.add_param(0, BOT.MUL)\n                elif op == \"Pow\":\n                    layer.add_param(0, BOT.POW)\n                elif op == \"RDiv\":\n                    layer.add_param(0, BOT.RDIV)\n                elif op == \"RSub\":\n                    layer.add_param(0, BOT.RSUB)\n                elif op == \"Sub\":\n                    layer.add_param(0, BOT.SUB)\n\n                with_scalar = get_node_attr_i(node, \"with_scalar\", 0)\n                b = get_node_attr_f(node, \"b\", 0)\n                if with_scalar:\n                    layer.add_param(1, with_scalar)\n                    layer.add_param(2, b)\n            elif op == \"Asin\":\n                layer.add_param(0, UOT.ASIN)\n            elif op == \"Atan\":\n                layer.add_param(0, UOT.ATAN)\n            elif op in (\"AveragePool\", \"MaxPool\"):\n                auto_pad = get_node_attr_s(node, \"auto_pad\")\n                ceil_mode = get_node_attr_i(node, \"ceil_mode\", 0)\n                kernel_shape = get_node_attr_ai(node, \"kernel_shape\")\n                strides = get_node_attr_ai(node, \"strides\")\n                pads = get_node_attr_ai(node, \"pads\")\n\n                pool = int(op == \"AveragePool\")\n\n                if ceil_mode == 1:\n                    pad_mode = PAM.FULL\n                elif auto_pad == \"SAME_UPPER\":\n                    pad_mode = PAM.SAMEUPPER\n                elif auto_pad == \"SAME_LOWER\":\n                    pad_mode = PAM.SAMELOWER\n                else:\n                    pad_mode = PAM.VALID\n\n                layer.add_param(0, pool)\n\n                if kernel_shape.size == 1:\n                    layer.add_param(1, int(kernel_shape[0]))\n                elif kernel_shape.size == 2:\n                    layer.add_param(1, int(kernel_shape[1]))\n                    layer.add_param(11, int(kernel_shape[0]))\n\n                if strides.size == 1:\n                    layer.add_param(2, int(strides[0]))\n                elif strides.size == 2:\n                    layer.add_param(2, int(strides[1]))\n                    layer.add_param(12, int(strides[0]))\n\n                if pads.size == 1:\n                    layer.add_param(3, int(pads[0]))\n                elif pads.size == 2:\n                    layer.add_param(3, int(pads[1]))\n                    layer.add_param(13, int(pads[0]))\n                elif pads.size == 4:\n                    layer.add_param(3, int(pads[1]))\n                    layer.add_param(13, int(pads[0]))\n                    layer.add_param(14, int(pads[3]))\n                    layer.add_param(15, int(pads[2]))\n\n                layer.add_param(5, pad_mode)\n\n                if pool:\n                    avgpool_count_include_pad = get_node_attr_i(\n                        node, \"count_include_pad\", 0\n                    )\n                    layer.add_param(6, avgpool_count_include_pad)\n            elif op == \"BatchNormalization\":\n                epsilon = get_node_attr_f(node, \"epsilon\", 0.00001)\n                scale = self.weights[node.input[1]]\n                B = self.weights[node.input[2]]\n                mean = self.weights[node.input[3]]\n                var = self.weights[node.input[4]]\n                channels = get_tensor_proto_data_size(scale, scale.data_type)\n\n                layer.add_param(0, channels)\n\n                bin_length += self.add_weight(layer, \"slope\", scale)\n                bin_length += self.add_weight(layer, \"mean\", mean)\n\n                # apply epsilon to var\n                v = onph.to_array(var)\n                ve = np.array([v[i] + epsilon for i in range(channels)], np.float32)\n                bin_length += self.add_weight(layer, \"variance\", ve)\n                bin_length += self.add_weight(layer, \"bias\", B)\n            elif op == \"BiasGelu\":\n                B = self.weights[node.input[1]]\n\n                layer.add_param(0, get_tensor_proto_data_size(B, B.data_type))\n\n                bin_length += self.add_weight(layer, \"bias\", B)\n            elif op == \"Ceil\":\n                layer.add_param(0, UOT.CEIL)\n            elif op == \"Clip\":\n                if len(node.input) == 1:\n                    minimum = get_node_attr_f(node, \"min\", -FLOAT32_MAX)\n                    maximum = get_node_attr_f(node, \"max\", FLOAT32_MAX)\n                else:\n                    minimum = (\n                        get_node_attr_from_input_f(self.weights[node.input[1]])\n                        if node.input[1] in self.weights\n                        else -FLOAT32_MAX\n                    )\n                    maximum = (\n                        get_node_attr_from_input_f(self.weights[node.input[2]])\n                        if node.input[2] in self.weights\n                        else FLOAT32_MAX\n                    )\n\n                layer.add_param(0, minimum)\n                layer.add_param(1, maximum)\n            elif op == \"Concat\":\n                axis = get_node_attr_i(node, \"axis\", 1)\n                layer.add_param(0, axis - 1 if axis > 0 else axis)\n            elif op == \"Constant\":\n                logger.error(\"Code should not have reached inside Constant.\")\n            elif op == \"Conv\":\n                W = self.weights[node.input[1]]\n\n                num_filter = W.dims[0]\n                has_bias = int(len(node.input) == 3)\n\n                auto_pad = get_node_attr_s(node, \"auto_pad\")\n                kernel_shape = get_node_attr_ai(node, \"kernel_shape\")\n                dilations = get_node_attr_ai(node, \"dilations\")\n                strides = get_node_attr_ai(node, \"strides\")\n                pads = get_node_attr_ai(node, \"pads\")\n                group = get_node_attr_i(node, \"group\", 1)\n\n                layer.add_param(0, num_filter)\n\n                if kernel_shape.size == 1:\n                    layer.add_param(1, int(kernel_shape[0]))\n                elif kernel_shape.size == 2:\n                    layer.add_param(1, int(kernel_shape[1]))\n                    layer.add_param(11, int(kernel_shape[0]))\n\n                if dilations.size == 1:\n                    layer.add_param(2, int(dilations[0]))\n                elif dilations.size == 2:\n                    layer.add_param(2, int(dilations[1]))\n                    layer.add_param(12, int(dilations[0]))\n\n                if strides.size == 1:\n                    layer.add_param(3, int(strides[0]))\n                elif strides.size == 2:\n                    layer.add_param(3, int(strides[1]))\n                    layer.add_param(13, int(strides[0]))\n\n                if auto_pad == \"SAME_UPPER\":\n                    layer.add_param(4, -233)\n                elif auto_pad == \"SAME_LOWER\":\n                    layer.add_param(4, -234)\n                elif pads.size == 1:\n                    layer.add_param(4, int(pads[0]))\n                elif pads.size == 2:\n                    layer.add_param(4, int(pads[1]))\n                    layer.add_param(14, int(pads[0]))\n                elif pads.size == 4:\n                    layer.add_param(4, int(pads[1]))\n                    layer.add_param(14, int(pads[0]))\n                    layer.add_param(15, int(pads[3]))\n                    layer.add_param(16, int(pads[2]))\n\n                layer.add_param(5, has_bias)\n\n                layer.add_param(6, get_tensor_proto_data_size(W, W.data_type))\n\n                if group > 1:\n                    layer.add_param(7, int(group))\n\n                quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32\n                bin_length += self.add_weight(layer, \"weight\", W, quantize_tag)\n\n                if has_bias:\n                    B = self.weights[node.input[2]]\n                    bin_length += self.add_weight(layer, \"bias\", B)\n            elif op == \"ConvTranspose\":\n                W = self.weights[node.input[1]]\n\n                has_bias = int(len(node.input) == 3)\n\n                auto_pad = get_node_attr_s(node, \"auto_pad\")\n                kernel_shape = get_node_attr_ai(node, \"kernel_shape\")\n                dilations = get_node_attr_ai(node, \"dilations\")\n                strides = get_node_attr_ai(node, \"strides\")\n                output_padding = get_node_attr_ai(node, \"output_padding\")\n                output_shape = get_node_attr_ai(node, \"output_shape\")\n                pads = get_node_attr_ai(node, \"pads\")\n                group = get_node_attr_i(node, \"group\", 1)\n                num_filter = W.dims[1] * group\n\n                layer.add_param(0, num_filter)\n\n                if kernel_shape.size == 1:\n                    layer.add_param(1, int(kernel_shape[0]))\n                elif kernel_shape.size == 2:\n                    layer.add_param(1, int(kernel_shape[1]))\n                    layer.add_param(11, int(kernel_shape[0]))\n\n                if dilations.size == 1:\n                    layer.add_param(2, int(dilations[0]))\n                elif dilations.size == 2:\n                    layer.add_param(2, int(dilations[1]))\n                    layer.add_param(12, int(dilations[0]))\n\n                if strides.size == 1:\n                    layer.add_param(3, int(strides[0]))\n                elif strides.size == 2:\n                    layer.add_param(3, int(strides[1]))\n                    layer.add_param(13, int(strides[0]))\n\n                if auto_pad == \"SAME_UPPER\":\n                    layer.add_param(4, -233)\n                elif auto_pad == \"SAME_LOWER\":\n                    layer.add_param(4, -234)\n                elif pads.size == 1:\n                    layer.add_param(4, int(pads[0]))\n                elif pads.size == 2:\n                    layer.add_param(4, int(pads[1]))\n                    layer.add_param(14, int(pads[0]))\n                elif pads.size == 4:\n                    layer.add_param(4, int(pads[1]))\n                    layer.add_param(14, int(pads[0]))\n                    layer.add_param(15, int(pads[3]))\n                    layer.add_param(16, int(pads[2]))\n\n                if output_padding.size == 1:\n                    layer.add_param(18, int(output_padding[0]))\n                elif output_padding.size == 2:\n                    layer.add_param(18, int(output_padding[1]))\n                    layer.add_param(19, int(output_padding[0]))\n\n                if output_shape.size == 1:\n                    layer.add_param(20, int(output_shape[0]))\n                elif output_shape == 2:\n                    layer.add_param(20, int(output_shape[1]))\n                    layer.add_param(21, int(output_shape[0]))\n\n                layer.add_param(5, has_bias)\n\n                weight_data_size = get_tensor_proto_data_size(W, W.data_type)\n                layer.add_param(6, weight_data_size)\n\n                if group > 1:\n                    layer.add_param(7, group)\n\n                quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32\n                weight_data = onph.to_array(W)\n                bin_length += self.add_weight(\n                    layer, \"weight\", weight_data.swapaxes(0, 1), quantize_tag\n                )\n\n                if has_bias:\n                    B = self.weights[node.input[2]]\n                    bin_length += self.add_weight(layer, \"bias\", B)\n            elif op == \"Cos\":\n                layer.add_param(0, UOT.COS)\n            elif op == \"Crop\":\n                starts = get_node_attr_ai(node, \"starts\")\n                layer.add_param(9, [starts.size, *starts])\n\n                ends = get_node_attr_ai(node, \"ends\")\n                layer.add_param(10, [ends.size, *ends])\n\n                axes = get_node_attr_ai(node, \"axis\")\n                layer.add_param(11, [axes.size, *axes])\n            elif op == \"DepthToSpace\":\n                # pixelshuffle\n                scale_factor = get_node_attr_i(node, \"blocksize\", 1)\n                mode = get_node_attr_s(node, \"mode\")\n                layer.add_param(0, scale_factor)\n                if mode == \"CRD\":\n                    layer.add_param(1, 0)\n                elif mode == \"DCR\":\n                    layer.add_param(1, 1)\n            elif op == \"Dropout\":\n                pass\n            elif op == \"Elu\":\n                alpha = get_node_attr_f(node, \"alpha\", 1)\n                layer.add_param(0, alpha)\n            elif op == \"EmbedLayerNormalization\":\n                logger.error(f\"No NCNN documentation for {op} yet, will not function\")\n                words = self.weights[node.input[2]]\n                positions = self.weights[node.input[3]]\n                W = self.weights[node.input[5]]\n                B = self.weights[node.input[6]]\n\n                layer.add_param(0, get_tensor_proto_data_size(B, B.data_type))\n                layer.add_param(1, get_tensor_proto_data_size(words, words.data_type))\n                layer.add_param(\n                    2, get_tensor_proto_data_size(positions, positions.data_type)\n                )\n\n                quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32\n                bin_length += self.add_weight(layer, \"words\", words, DTYPE_FP32)\n                bin_length += self.add_weight(layer, \"positions\", positions, DTYPE_FP32)\n                bin_length += self.add_weight(layer, \"weight\", W, quantize_tag)\n                bin_length += self.add_weight(layer, \"bias\", B)\n            elif op == \"Exp\":\n                layer.add_param(0, UOT.EXP)\n            elif op == \"Flatten\":\n                axis = get_node_attr_i(node, \"axis\", 1)\n                if axis != 1:\n                    raise ValueError(f\"Unsupported Flatten axis {axis}.\")\n            elif op == \"Floor\":\n                layer.add_param(0, UOT.FLOOR)\n            elif op == \"Gelu\":\n                layer.add_param(0, 1)\n            elif op == \"Gemm\":\n                alpha = get_node_attr_f(node, \"alpha\", 1)\n                beta = get_node_attr_f(node, \"beta\", 1)\n                transA = get_node_attr_i(node, \"transA\", 0)\n                transB = get_node_attr_i(node, \"transB\", 0)\n\n                if alpha == 1 and beta == 1 and transA == 0 and transB == 1:\n                    # InnerProduct-like A * B * C\n                    B = self.weights[node.input[1]]\n                    C = self.weights[node.input[2]]\n\n                    layer.add_param(0, get_tensor_proto_data_size(C, C.data_type))\n                    layer.add_param(1, 1)\n                    layer.add_param(2, get_tensor_proto_data_size(B, B.data_type))\n\n                    quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32\n                    bin_length += self.add_weight(layer, \"B\", B, quantize_tag)\n                    bin_length += self.add_weight(layer, \"C\", C)\n                else:\n                    # gemm\n                    layer.add_param(0, alpha)\n                    layer.add_param(1, beta)\n                    layer.add_param(2, transA)\n                    layer.add_param(3, transB)\n            elif op in (\"GlobalAveragePool\", \"GlobalMaxPool\"):\n                layer.add_param(0, int(op == \"GlobalAveragePool\"))\n                layer.add_param(4, 1)\n            elif op in (\"adaptive_avg_pool2d\", \"adaptive_max_pool2d\"):\n                out_shape_tp = self.weights[node.input[1]]\n                out_shape = get_node_attr_from_input_ai(out_shape_tp)\n\n                layer.add_param(0, int(op == \"adaptive_avg_pool2d\"))\n                layer.add_param(7, 1)\n                if out_shape.size == 1:\n                    layer.add_param(8, int(out_shape[0]))\n                elif out_shape.size == 2:\n                    layer.add_param(8, int(out_shape[1]))  # out_w\n                    layer.add_param(18, int(out_shape[0]))  # out_h\n            elif op == \"GroupNorm\":\n                groups = get_node_attr_i(node, \"groups\", 1)\n                channels = get_node_attr_i(node, \"channels\", 1)\n                eps = get_node_attr_f(node, \"epsilon\", 0.00001)\n                affine = get_node_attr_i(node, \"affine\", 1)\n\n                if affine:\n                    # discard affine-less S=1 B=0\n                    affine_S = get_node_attr_from_input_af(self.weights[node.input[1]])\n                    affine_B = get_node_attr_from_input_af(self.weights[node.input[2]])\n                    if (\n                        affine_S.size == 1\n                        and affine_S[0] == 1\n                        and affine_B.size == 1\n                        and affine_B[0] == 0\n                    ):\n                        affine = 0\n                    elif np.any(affine_S[:channels] != 1) or np.any(\n                        affine_B[:channels] != 0\n                    ):\n                        affine = 1\n                    else:\n                        affine = 0\n\n                layer.add_param(0, groups)\n                layer.add_param(1, channels)\n                layer.add_param(2, eps)\n                layer.add_param(3, affine)\n                if affine:\n                    scale = self.weights[node.input[1]]\n                    B = self.weights[node.input[2]]\n\n                    bin_length += self.add_weight(layer, \"scale\", scale)\n                    bin_length += self.add_weight(layer, \"bias\", B)\n            elif op == \"GRU\":\n                # W = self.weights[node.input[1]]\n                # R = self.weights[node.input[2]]\n                # B = self.weights[node.input[3]]\n\n                # hidden_size = get_node_attr_i(node, \"hidden_size\", 0)\n                # direction = get_node_attr_s(node, \"direction\")\n\n                # if direction == \"forward\":\n                #    direction_type = GRU.FORWARD\n                # elif direction == \"reverse\":\n                #    direction_type = GRU.REVERSE\n                # elif direction == \"bidirectional\":\n                #    direction_type = GRU.BIDIRECTIONAL\n\n                # weight_data_size = get_tensor_proto_data_size(W)\n\n                # layer.add_param(0, hidden_size)\n                # layer.add_param(1, weight_data_size)\n                # layer.add_param(2, direction_type)\n\n                # num_directions = 2 if direction_type == GRU.BIDIRECTIONAL else 1\n\n                # reorder num_directions-URN-hidden_size to num_directions-RUN-hidden_size\n                # quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32\n\n                # logger.error(\n                #    \"Not sure GRU weight reordering is accurate, \"\n                #    \"docs and code comments appear to give different shape orders\"\n                # )\n\n                # W_array = onph.to_array(W)\n                # W_array = np.stack(\n                #    (W_array[:, 1, :], W_array[:, 0, :], W_array[:, 2, :]), axis=1\n                # )\n                # bin_length += self.add_weight(layer, W_array, \"weight_xc_data\", quantize_tag, is_fp16)\n\n                # reduce U and R bias except N\n                # reorder num_directions-URN-hidden to num_directions-RUN-hidden\n                # B_array = onph.to_array(B)\n\n                # bias_data_size_g = B_array.size / 6 / num_directions\n                # for i in range(bias_data_size_g)[1:]:\n                #    pass\n                raise RuntimeError(\n                    \"GRU not implemented yet, please report issue with model used\"\n                )\n            elif op in (\"HardSigmoid\", \"Hard Swish\"):\n                alpha = get_node_attr_f(node, \"alpha\", 0.2)\n                beta = get_node_attr_f(node, \"beta\", 0.5)\n\n                layer.add_param(0, alpha)\n                layer.add_param(1, beta)\n            elif op == \"ImageScaler\":\n                bias = get_node_attr_af(node, \"bias\")\n                scale = get_node_attr_f(node, \"scale\", 1)\n                channels = bias.size\n\n                layer.add_param(0, channels)\n                layer.add_param(1, 1)\n\n                bin_length += self.add_weight(layer, \"scale\", np.array((scale,) * 3))\n                bin_length += self.add_weight(layer, \"bias\", bias)\n            elif op == \"InstanceNormalization\":\n                eps = get_node_attr_f(node, \"epsilon\", 0.00001)\n\n                # Discard affine-less S=1 B=0\n                affine_S = get_node_attr_from_input_af(self.weights[node.input[1]])\n                affine_B = get_node_attr_from_input_af(self.weights[node.input[2]])\n                channels = affine_S.size\n\n                if np.any(affine_S[:channels] != 1) or np.any(affine_B[:channels] != 0):\n                    affine = 1\n                else:\n                    affine = 0\n\n                layer.add_param(0, channels)\n                layer.add_param(1, eps)\n                layer.add_param(2, affine)\n                if affine:\n                    scale = self.weights[node.input[1]]\n                    B = self.weights[node.input[2]]\n\n                    bin_length += self.add_weight(layer, \"scale\", scale)\n                    bin_length += self.add_weight(layer, \"bias\", B)\n            elif op == \"LayerNorm\":\n                eps = get_node_attr_f(node, \"epsilon\", 0.00001)\n                affine = get_node_attr_i(node, \"affine\", 1)\n\n                if affine:\n                    # discard affine-less S=1 B=0\n                    affine_S = get_node_attr_from_input_af(self.weights[node.input[1]])\n                    affine_B = get_node_attr_from_input_af(self.weights[node.input[2]])\n                    affine_size = affine_S.size\n\n                    if np.any(affine_S[:affine_size] != 1) or np.any(\n                        affine_B[:affine_size]\n                    ):\n                        affine = 1\n                    else:\n                        affine = 0\n\n                    if affine:\n                        layer.add_param(0, affine_size)\n\n                layer.add_param(1, eps)\n                layer.add_param(2, affine)\n\n                if affine:\n                    scale = self.weights[node.input[1]]\n                    B = self.weights[node.input[2]]\n\n                    bin_length += self.add_weight(layer, \"scale\", scale)\n                    bin_length += self.add_weight(layer, \"bias\", B)\n            elif op == \"LeakyRelu\":\n                alpha = get_node_attr_f(node, \"alpha\", 0.01)\n                layer.add_param(0, alpha)\n            elif op == \"Log\":\n                layer.add_param(0, UOT.LOG)\n            elif op == \"LRN\":\n                layer.add_param(0, 0)\n                layer.add_param(1, get_node_attr_i(node, \"size\", 1))\n                layer.add_param(2, get_node_attr_f(node, \"alpha\", 1))\n                layer.add_param(3, get_node_attr_f(node, \"beta\", 0.5))\n                layer.add_param(4, get_node_attr_f(node, \"bias\", 1))\n            elif op == \"LSTM\":\n                # W = self.weights[node.input[1]]\n                # R = self.weights[node.input[2]]\n                # B = self.weights[node.input[3]]\n\n                # hidden_size = get_node_attr_i(node, \"hidden_size\", 0)\n                # direction = get_node_attr_s(node, \"direction\")\n\n                # if direction == \"forward\":\n                #    direction_type = GRU.FORWARD\n                # elif direction == \"reverse\":\n                #    direction_type = GRU.REVERSE\n                # elif direction  == \"bidirectional\":\n                #    direction_type = GRU.BIDIRECTIONAL\n                raise RuntimeError(\n                    \"LSTM not implemented yet, please report issue with model used\"\n                )\n            elif op == \"MatMul\":\n                if node.input[1] in self.weights:\n                    # InnerProduct\n                    B = self.weights[node.input[1]]\n                    weight_data_size = get_tensor_proto_data_size(B, B.data_type)\n                    num_output = B.dims[-1]\n\n                    layer.add_param(0, num_output)\n                    layer.add_param(1, 0)\n                    layer.add_param(2, weight_data_size)\n\n                    B_array = onph.to_array(B)\n                    bin_length += self.add_weight(layer, \"bias\", B_array.T, DTYPE_FP32)\n                # There is a dead else here, not sure if this was incomplete code\n            elif op == \"MultiHeadAttention\":\n                # embed_dim = get_node_attr_i(node, \"embed_dim\", 0)\n                # num_heads = get_node_attr_i(node, \"num_heads\", 0)\n\n                # layer.add_param(0, embed_dim)\n                # layer.add_param(1, num_heads)\n\n                # if len(node.input) == 5:\n                #    qkvw = self.weights[node.input[1]]\n                #    qkvb = self.weights[node.input[2]]\n                #    ow = self.weights[node.input[3]]\n                #    ob = self.weights[node.input[4]]\n\n                #    weight_data_size = get_tensor_proto_data_size(ow)\n\n                #    layer.add_param(2, weight_data_size)\n\n                #    quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32\n                raise RuntimeError(\n                    \"MultiHeadAttention not implemented, please report issue with model used\"\n                )\n            elif op == \"Neg\":\n                layer.add_param(0, UOT.NEG)\n            elif op == \"Normalize\":\n                eps = get_node_attr_f(node, \"eps\", 0)\n\n                layer.add_param(1, 1)  # channel_shared\n                layer.add_param(2, eps)\n                layer.add_param(3, 1)  # scale_data_size\n                layer.add_param(9, NEM.PYTORCH)\n\n                bin_length += self.add_weight(layer, \"scale\", 1)\n            elif op == \"Pad\":\n                mode = get_node_attr_s(node, \"mode\")\n                value = get_node_attr_f(node, \"value\", 0)\n\n                if len(node.input) == 1:\n                    pads = get_node_attr_ai(node, \"pads\")\n                else:\n                    pads = get_node_attr_from_input_ai(self.weights[node.input[1]])\n\n                if mode == \"edge\":\n                    ptype = PAT.REPLICATE\n                elif mode == \"reflect\":\n                    ptype = PAT.REFLECT\n                else:\n                    ptype = PAT.CONSTANT\n\n                pad_size = pads.size\n                top = bottom = front = behind = 0\n                if pad_size == 8:\n                    # NCHW\n                    top = pads[2]\n                    bottom = pads[6]\n                    left = pads[3]\n                    right = pads[7]\n                    front = pads[1]\n                    behind = pads[5]\n                elif pad_size == 6:\n                    # NHW\n                    top = pads[1]\n                    bottom = pads[4]\n                    left = pads[2]\n                    right = pads[5]\n                else:\n                    # NW\n                    left = pads[1]\n                    right = pads[3]\n\n                layer.add_param(0, int(top))\n                layer.add_param(1, int(bottom))\n                layer.add_param(2, int(left))\n                layer.add_param(3, int(right))\n                layer.add_param(4, int(ptype))\n                layer.add_param(5, int(value))\n                layer.add_param(7, int(front))\n                layer.add_param(8, int(behind))\n            elif op == \"PixelShuffle\":\n                layer.add_param(0, get_node_attr_i(node, \"scale_factor\", 1))\n            elif op == \"PRelu\":\n                slope = self.weights[node.input[1]]\n                num_slope = get_tensor_proto_data_size(slope, slope.data_type)\n\n                layer.add_param(0, num_slope)\n\n                bin_length += self.add_weight(layer, \"slope\", slope)\n            elif op == \"Reciprocal\":\n                layer.add_param(0, UOT.RECIPROCAL)\n            elif op in [\n                \"ReduceMax\",\n                \"ReduceMin\",\n                \"ReduceMean\",\n                \"ReduceProd\",\n                \"ReduceSum\",\n                \"ReduceSumSquare\",\n                \"ReduceL1\",\n                \"ReduceL2\",\n                \"ReduceLogSum\",\n                \"ReduceLogSumExp\",\n            ]:\n                if op == \"ReduceSum\":\n                    op_type = ROT.SUM\n                elif op == \"ReduceSumSquare\":\n                    op_type = ROT.SUMSQ\n                elif op == \"ReduceMean\":\n                    op_type = ROT.MEAN\n                elif op == \"ReduceMax\":\n                    op_type = ROT.MAX\n                elif op == \"ReduceMin\":\n                    op_type = ROT.MIN\n                elif op == \"ReduceProd\":\n                    op_type = ROT.PROD\n                elif op == \"ReduceL1\":\n                    op_type = ROT.L1\n                elif op == \"ReduceL2\":\n                    op_type = ROT.L2\n                elif op == \"ReduceLogSum\":\n                    op_type = ROT.LOGSUM\n                elif op == \"ReduceLogSumExp\":\n                    op_type = ROT.LOGSUMEXP\n                else:\n                    op_type = -233\n\n                layer.add_param(0, op_type)\n\n                axes = get_node_attr_ai(node, \"axes\")\n                keepdims = get_node_attr_i(node, \"keepdims\", 1)\n\n                if axes.size > 0:\n                    # if axes set, reduce according to axes\n                    layer.add_param(1, 0)\n\n                    for axis in axes:\n                        if axis == 0 or axis > 4 or axis < -3:\n                            raise ValueError(f\"Unsupported axis {axis} in Reduction\")\n                    layer.add_param(\n                        3,\n                        [axes.size, *[a - 1 if a > 0 else a for a in axes]],\n                    )\n                else:\n                    # if axes not set, reduce all axes by default\n                    layer.add_param(1, 1)\n\n                layer.add_param(4, keepdims)\n                logger.error(\"No NCNN documentation for Reduction param 5\")\n                layer.add_param(5, 1)\n            elif op == \"Reorg\":\n                layer.add_param(0, get_node_attr_i(node, \"stride\", 1))\n            elif op == \"Reshape\":\n                if len(node.input) == 1:\n                    shape = get_node_attr_ai(node, \"shape\")\n                else:\n                    shape = get_node_attr_from_input_ai(self.weights[node.input[1]])\n\n                shape_size = shape.size\n                if shape_size == 1:\n                    logger.error(\"Should never reach shape.size == 1 in Reshape\")\n                    layer.add_param(0, int(shape[0]))\n                elif shape_size == 2:\n                    layer.add_param(0, int(shape[1]))\n                elif shape_size == 3:\n                    layer.add_param(0, int(shape[2]))\n                    layer.add_param(1, int(shape[1]))\n                elif shape_size == 4:\n                    layer.add_param(0, int(shape[3]))\n                    layer.add_param(1, int(shape[2]))\n                    layer.add_param(2, int(shape[1]))\n                elif shape_size == 5:\n                    layer.add_param(0, int(shape[3] * shape[3]))\n                    layer.add_param(1, int(shape[2]))\n                    layer.add_param(2, int(shape[1]))\n            elif op == \"Resize\":\n                mode = get_node_attr_s(node, \"mode\")\n                align = get_node_attr_s(node, \"coordinate_transformation_mode\")\n\n                if len(node.input) == 2:\n                    # opset 10\n                    scales = get_node_attr_from_input_af(self.weights[node.input[1]])\n                    sizes = np.empty(0, np.int32)\n                else:\n                    # opset 11+\n                    scales = get_node_attr_from_input_af(self.weights[node.input[2]])\n                    if len(node.input) >= 4:\n                        sizes = get_node_attr_from_input_ai(self.weights[node.input[3]])\n                    else:\n                        sizes = np.empty(0, np.int32)\n\n                if mode == \"linear\":\n                    resize_type = IRT.BILINEAR\n                elif mode == \"cubic\":\n                    resize_type = IRT.BICUBIC\n                else:\n                    resize_type = IRT.NEAREST\n\n                if scales.size == 0 and sizes.size == 0:\n                    raise ValueError(\n                        \"Unsupported Resize scales and sizes are all empty.\"\n                    )\n\n                if scales.size == 2:\n                    h_scale = 1\n                    w_scale = scales[1]\n                elif scales.size == 3:\n                    h_scale = scales[1]\n                    w_scale = scales[2]\n                elif scales.size == 4:\n                    if scales[1] != 1:\n                        raise TypeError(f\"Unsupported Resize scales {scales}.\")\n                    h_scale = scales[2]\n                    w_scale = scales[3]\n                else:\n                    h_scale = 1\n                    w_scale = 1\n\n                if sizes.size == 2:\n                    output_height = 0\n                    output_width = sizes[1]\n                elif sizes.size == 3:\n                    output_height = sizes[1]\n                    output_width = sizes[2]\n                elif sizes.size == 4:\n                    output_height = sizes[2]\n                    output_width = sizes[3]\n                else:\n                    output_height = 0\n                    output_width = 0\n\n                align_corner = int(align == \"align_corners\")\n\n                layer.add_param(0, resize_type)\n                layer.add_param(1, float(h_scale))\n                layer.add_param(2, float(w_scale))\n                layer.add_param(3, int(output_height))\n                layer.add_param(4, int(output_width))\n                layer.add_param(6, align_corner)\n            elif op == \"RNN\":\n                W = self.weights[node.input[1]]\n                R = self.weights[node.input[2]]\n                B = self.weights[node.input[3]]\n\n                hidden_size = get_node_attr_i(node, \"hidden_size\", 0)\n                direction = get_node_attr_s(node, \"direction\")\n\n                if direction == \"reverse\":\n                    direction_type = GRU.REVERSE\n                elif direction == \"bidirectional\":\n                    direction_type = GRU.BIDIRECTIONAL\n                else:\n                    direction_type = GRU.FORWARD\n\n                weight_data_size = get_tensor_proto_data_size(W, W.data_type)\n\n                layer.add_param(0, hidden_size)\n                layer.add_param(1, weight_data_size)\n                layer.add_param(2, direction_type)\n\n                quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32\n                bin_length += self.add_weight(layer, \"weight\", W, quantize_tag)\n\n                # reduce xc and hc bias\n                reduced_B = np.sum(onph.to_array(B), 1)\n                bin_length += self.add_weight(layer, \"bias\", reduced_B, quantize_tag)\n\n                bin_length += self.add_weight(layer, \"R\", R, quantize_tag)\n            elif op == \"ShuffleChannel\":\n                layer.add_param(0, get_node_attr_i(node, \"group\", 1))\n                layer.add_param(1, get_node_attr_i(node, \"reverse\", 0))\n            elif op == \"Sigmoid\":\n                pass\n            elif op == \"Sin\":\n                layer.add_param(0, UOT.SIN)\n            elif op == \"SkipLayerNormalization\":\n                logger.error(f\"No NCNN documentation for {op} yet, will not function\")\n                W = self.weights[node.input[2]]\n                B = self.weights[node.input[3]]\n                B2 = self.weights[node.input[4]]\n\n                layer.add_param(0, get_tensor_proto_data_size(B, B.data_type))\n\n                quantize_tag = DTYPE_FP16 if is_fp16 else DTYPE_FP32\n                bin_length += self.add_weight(layer, \"weight\", W, quantize_tag)\n                bin_length += self.add_weight(layer, \"bias1\", B, DTYPE_FP32)\n                bin_length += self.add_weight(layer, \"bias2\", B2, DTYPE_FP32)\n            elif op == \"Slice\":\n                input_size = len(node.input)\n                if input_size == 1:\n                    starts = get_node_attr_ai(node, \"starts\")\n                    ends = get_node_attr_ai(node, \"ends\")\n                    axes = get_node_attr_ai(node, \"axes\")\n                    steps = get_node_attr_ai(node, \"steps\")\n                else:\n                    starts = get_node_attr_from_input_ai(self.weights[node.input[1]])\n                    ends = get_node_attr_from_input_ai(self.weights[node.input[2]])\n                    if input_size >= 4:\n                        axes = get_node_attr_from_input_ai(self.weights[node.input[3]])\n                    else:\n                        axes = np.empty(0, np.int32)\n                    if input_size >= 5:\n                        steps = get_node_attr_from_input_ai(self.weights[node.input[4]])\n                    else:\n                        steps = np.empty(0, np.int32)\n\n                assert np.all(steps != 1), f\"Unsupported Slice step {steps}\"\n\n                # Filter out N-dim axis\n                if axes.size:\n                    for i, axis in enumerate(axes):\n                        if axis == 0:\n                            np.delete(starts, i)\n                            np.delete(ends, i)\n                            np.delete(axes, i)\n                            break\n\n                layer.add_param(9, [starts.size, *list(starts)])\n                layer.add_param(10, [ends.size, *list(ends)])\n                if axes.size:\n                    assert np.all(\n                        axes != 0 and axes <= 3 and axes >= -3\n                    ), f\"Unsupported Slice axes {axes}\"\n                    layer.add_param(\n                        11, [axes.size, *[a - 1 if a > 0 else a for a in axes]]\n                    )\n            elif op == \"Softmax\":\n                axis = get_node_attr_i(node, \"axis\", 1)\n                layer.add_param(0, axis - 1)\n                layer.add_param(1, 1)\n            elif op == \"Split\":\n                axis = get_node_attr_i(node, \"axis\", 0)\n                splits = get_node_attr_ai(node, \"split\")\n\n                assert axis >= 1, f\"Unsupported axis {axis} in Split\"\n\n                if splits.size:\n                    layer.add_param(0, [output_size, *list(splits[:-1]), -233])\n                else:\n                    layer.add_param(\n                        0, [output_size, *[-233 for _ in range(output_size)]]\n                    )\n                layer.add_param(1, axis - 1)\n            elif op == \"Sqrt\":\n                layer.add_param(0, UOT.SQRT)\n            elif op == \"Squeeze\":\n                axes = get_node_attr_ai(node, \"axes\")\n\n                if axes.size:\n                    assert np.all(\n                        axes != 0 and axes <= 4 and axes >= -3\n                    ), f\"Unsupported Squeeze axes {axes}\"\n\n                    layer.add_param(\n                        3, [axes.size, *[a - 1 if a > 0 else a for a in axes]]\n                    )\n                else:\n                    layer.add_param(0, 1)\n                    layer.add_param(1, 1)\n                    layer.add_param(2, 1)\n            elif op == \"Sum\":\n                layer.add_param(0, EOT.SUM)\n            elif op == \"Swish\":\n                pass\n            elif op == \"Tan\":\n                layer.add_param(0, UOT.TAN)\n            elif op == \"Tanh\":\n                layer.add_param(0, UOT.TANH)\n            elif op == \"Transpose\":\n                perm = get_node_attr_ai(node, \"perm\")\n                if perm.size == 3:\n                    if (perm[1] == 1 and perm[2] == 2) or (\n                        perm[0] == 1 and perm[1] == 0 and perm[2] == 2\n                    ):\n                        layer.add_param(0, POT.WH_WHC_WHDC)\n                    elif (perm[1] == 2 and perm[2] == 1) or (\n                        perm[0] == 2 and perm[1] == 0 and perm[2] == 1\n                    ):\n                        layer.add_param(0, POT.HW_HWC_HWDC)\n                elif perm.size == 4:\n                    if perm[1] == 1 and perm[2] == 2 and perm[3] == 3:\n                        layer.add_param(0, POT.WH_WHC_WHDC)\n                    elif perm[1] == 1 and perm[2] == 3 and perm[3] == 2:\n                        layer.add_param(0, POT.HW_HWC_HWDC)\n                    elif perm[1] == 2 and perm[2] == 1 and perm[3] == 3:\n                        layer.add_param(0, POT.WCH_WDHC)\n                    elif perm[1] == 2 and perm[2] == 3 and perm[3] == 1:\n                        layer.add_param(0, POT.CWH_DWHC)\n                    elif perm[1] == 3 and perm[2] == 1 and perm[3] == 2:\n                        layer.add_param(0, POT.HCW_HDWC)\n                    elif perm[1] == 3 and perm[2] == 2 and perm[3] == 1:\n                        layer.add_param(0, POT.CHW_DHWC)\n                elif perm.size == 5:\n                    if perm[1] == 1 and perm[2] == 2 and perm[3] == 3 and perm[4] == 4:\n                        layer.add_param(0, POT.WH_WHC_WHDC)\n                    elif (\n                        perm[1] == 1 and perm[2] == 3 and perm[3] == 4 and perm[4] == 2\n                    ):\n                        layer.add_param(0, POT.HW_HWC_HWDC)\n                    elif (\n                        perm[1] == 2 and perm[2] == 1 and perm[3] == 3 and perm[4] == 4\n                    ):\n                        layer.add_param(0, POT.WCH_WDHC)\n                    elif (\n                        perm[1] == 2 and perm[2] == 3 and perm[3] == 4 and perm[4] == 1\n                    ):\n                        layer.add_param(0, POT.CWH_DWHC)\n                    elif (\n                        perm[1] == 3 and perm[2] == 4 and perm[3] == 1 and perm[4] == 2\n                    ):\n                        layer.add_param(0, POT.HCW_HDWC)\n                    elif (\n                        perm[1] == 3 and perm[2] == 4 and perm[3] == 2 and perm[4] == 1\n                    ):\n                        layer.add_param(0, POT.CHW_DHWC)\n                    else:\n                        error_msg = f\"Unsupported Transpose type {perm}\"\n                        raise ValueError(error_msg)\n            elif op == \"Upsample\":\n                mode = get_node_attr_s(node, \"mode\")\n                align = get_node_attr_s(node, \"coordinate_transformation_mode\")\n\n                if len(node.input) == 1:\n                    scales = get_node_attr_af(node, \"scales\")\n                else:\n                    scales = get_node_attr_from_input_af(self.weights[node.input[1]])\n\n                if mode in (\"bilinear\", \"linear\"):\n                    resize_type = IRT.BILINEAR\n                elif mode == \"trilinear\":\n                    raise ValueError(\"Upsample does not support trilinear mode\")\n                else:\n                    resize_type = IRT.NEAREST\n\n                if scales.size == 2:\n                    h_scale = 1\n                    w_scale = scales[1]\n                elif scales.size == 3:\n                    h_scale = scales[1]\n                    w_scale = scales[2]\n                elif scales.size == 4:\n                    h_scale = scales[2]\n                    w_scale = scales[3]\n\n                    if scales[1] != 1:\n                        error_msg = f\"Unsupported Upsample scales {scales}\"\n                        raise ValueError(error_msg)\n                else:\n                    error_msg = f\"Unsupported Upsample scales {scales}\"\n                    raise ValueError(error_msg)\n\n                align_corner = int(align == \"align_corners\")\n\n                layer.add_param(0, resize_type)\n                layer.add_param(1, float(h_scale))\n                layer.add_param(2, float(w_scale))\n                layer.add_param(6, align_corner)\n            elif op == \"Unsqueeze\":\n                axes = get_node_attr_ai(node, \"axes\")\n\n                assert (\n                    np.all(axes != 0) and np.all(axes <= 4) and np.all(axes >= -4)\n                ), f\"Unsupported axes {axes} in Unsqueeze\"\n\n                layer.add_param(\n                    3, [axes.size, *[axis - 1 if axis > 0 else axis for axis in axes]]\n                )\n            else:\n                # NCNN TODO: op specific param\n                # This is presumably to catch anything they haven't written an op for yet\n                for attr in node.attribute:\n                    if attr.type == 1:\n                        error_msg = f\"Op {op} does not exist yet; {attr.name}={attr.f}\"\n                    elif attr.type == 2:\n                        error_msg = f\"Op {op} does not exist yet; {attr.name}={attr.i}\"\n                    elif attr.type == 3:\n                        error_msg = f\"Op {op} does not exist yet; {attr.name}={attr.s}\"\n                    else:\n                        error_msg = (\n                            f\"Op {op} does not exist yet; {attr.name}={attr.type}\"\n                        )\n\n                    raise ValueError(error_msg)\n\n            ncnn_model.add_layer(layer)\n\n            for o in range(output_size):\n                output_name = node.output[o]\n                if output_name in self.node_reference:\n                    refcount = self.node_reference[output_name]\n                    if refcount > 1:\n                        ncnn_model.add_layer(\n                            NcnnLayer(\n                                \"Split\",\n                                f\"splitncnn_{internal_split}\",\n                                1,\n                                refcount,\n                                [output_name],\n                                [\n                                    f\"{output_name}_splitncnn_{j}\"\n                                    for j in range(refcount)\n                                ],\n                            )\n                        )\n\n                        internal_split += 1\n\n        ncnn_model.bin_length = bin_length\n        NcnnOptimizer(ncnn_model).optimize()\n\n        return ncnn_model\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/session.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, Union\nfrom weakref import WeakKeyDictionary\n\nimport onnxruntime as ort\n\nfrom .model import OnnxModel\nfrom .utils import OnnxParsedTensorShape, parse_onnx_shape\n\nProviderDesc = Union[str, tuple[str, dict[Any, Any]]]\n\n\ndef create_inference_session(\n    model: OnnxModel,\n    gpu_index: int,\n    execution_provider: str,\n    should_tensorrt_fp16: bool = False,\n    tensorrt_cache_path: str | None = None,\n) -> ort.InferenceSession:\n    tensorrt: ProviderDesc = (\n        \"TensorrtExecutionProvider\",\n        {\n            \"device_id\": gpu_index,\n            \"trt_engine_cache_enable\": tensorrt_cache_path is not None,\n            \"trt_engine_cache_path\": tensorrt_cache_path,\n            \"trt_fp16_enable\": should_tensorrt_fp16,\n        },\n    )\n    cuda: ProviderDesc = (\n        \"CUDAExecutionProvider\",\n        {\n            \"device_id\": gpu_index,\n        },\n    )\n    cpu: ProviderDesc = \"CPUExecutionProvider\"\n\n    if execution_provider == \"TensorrtExecutionProvider\":\n        providers = [tensorrt, cuda, cpu]\n    elif execution_provider == \"CUDAExecutionProvider\":\n        providers = [cuda, cpu]\n    else:\n        providers = [execution_provider, cpu]\n\n    session = ort.InferenceSession(\n        model.bytes,\n        providers=providers,\n    )\n    return session\n\n\n__session_cache: WeakKeyDictionary[OnnxModel, ort.InferenceSession] = (\n    WeakKeyDictionary()\n)\n\n\ndef get_onnx_session(\n    model: OnnxModel,\n    gpu_index: int,\n    execution_provider: str,\n    should_tensorrt_fp16: bool,\n    tensorrt_cache_path: str | None = None,\n) -> ort.InferenceSession:\n    cached = __session_cache.get(model)\n    if cached is None:\n        cached = create_inference_session(\n            model,\n            gpu_index,\n            execution_provider,\n            should_tensorrt_fp16,\n            tensorrt_cache_path,\n        )\n        __session_cache[model] = cached\n    return cached\n\n\ndef get_input_shape(session: ort.InferenceSession) -> OnnxParsedTensorShape:\n    \"\"\"\n    Returns the input shape, input channels, input width (optional), and input height (optional).\n    \"\"\"\n\n    return parse_onnx_shape(session.get_inputs()[0].shape)\n\n\ndef get_output_shape(session: ort.InferenceSession) -> OnnxParsedTensorShape:\n    \"\"\"\n    Returns the output shape, output channels, output width (optional), and output height (optional).\n    \"\"\"\n\n    return parse_onnx_shape(session.get_outputs()[0].shape)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/tensorproto_utils.py",
    "content": "from sys import float_info\n\nimport numpy as np\nfrom onnx import numpy_helper as onph\nfrom onnx.onnx_pb import AttributeProto, NodeProto, TensorProto\nfrom sanic.log import logger\n\nINT64_MIN, INT64_MAX = np.iinfo(np.int64).min, np.iinfo(np.int64).max\nFLOAT32_MAX = float_info.max\n\nAPT = AttributeProto\nTPT = TensorProto\n\n\ndef get_node_attr_ai(node: NodeProto, key: str) -> np.ndarray:\n    for attr in node.attribute:\n        if attr.name == key:\n            return np.array(\n                [max(min(i, INT64_MAX), INT64_MIN) for i in attr.ints], np.int64\n            )\n\n    return np.empty(0, np.int32)\n\n\ndef set_node_attr_ai(node: NodeProto, key: str, value: np.ndarray) -> None:\n    attr_group = AttributeProto(name=key, floats=value, type=APT.INTS)\n    node.attribute.append(attr_group)\n\n\ndef get_node_attr_af(node: NodeProto, key: str) -> np.ndarray:\n    for attr in node.attribute:\n        if attr.name == key:\n            return np.array(list(attr.floats), np.float32)\n\n    return np.empty(0, np.float32)\n\n\ndef get_node_attr_i(node: NodeProto, key: str, default: int = 0) -> int:\n    for attr in node.attribute:\n        if attr.name == key:\n            return max(min(attr.i, INT64_MAX), INT64_MIN)\n\n    return default\n\n\ndef get_node_attr_f(node: NodeProto, key: str, default: float = 0) -> float:\n    for attr in node.attribute:\n        if attr.name == key:\n            return attr.f\n\n    return default\n\n\ndef get_node_attr_s(node: NodeProto, key: str, default: str = \"\"):\n    for attr in node.attribute:\n        if attr.name == key:\n            return attr.s.decode(\"ascii\")\n\n    return default\n\n\ndef get_node_attr_tensor(node: NodeProto, key: str) -> TensorProto:\n    for attr in node.attribute:\n        if attr.name == key:\n            return attr.t\n\n    return TensorProto()\n\n\ndef get_node_attr_from_input_f(tp: TensorProto) -> float:\n    shape_data = onph.to_array(tp)\n\n    if tp.data_type in (TPT.FLOAT, TPT.FLOAT16, TPT.DOUBLE, TPT.INT32):\n        f = shape_data.item(0)\n    elif tp.data_type == TPT.INT64:\n        f = max(min(shape_data.item(0), INT64_MAX), INT64_MIN)\n    else:\n        raise TypeError(f\"Unknown data type {tp.data_type}\")\n\n    return f\n\n\ndef get_node_attr_from_input_ai(tp: TensorProto) -> np.ndarray:\n    if tp.data_type in (TPT.INT32, TPT.INT64):\n        shape_data = onph.to_array(tp)\n        if shape_data.size == 1:\n            shape_data = np.array([shape_data.item(0)], shape_data.dtype)\n        return np.array(\n            [\n                max(min(val, INT64_MAX), INT64_MIN)\n                if tp.data_type == TPT.INT64\n                else val\n                for val in shape_data\n            ],\n            shape_data.dtype,\n        )\n    else:\n        logger.error(f\"Unknown data type {tp.data_type}\")\n\n    return np.empty(0, np.int32)\n\n\ndef get_node_attr_from_input_af(tp: TensorProto) -> np.ndarray:\n    if tp.data_type in (TPT.FLOAT, TPT.FLOAT16, TPT.DOUBLE):\n        shape_data = onph.to_array(tp)\n        return np.array(list(shape_data), shape_data.dtype)\n    else:\n        logger.error(f\"Unknown data type {tp.data_type}\")\n\n    return np.empty(0, np.float32)\n\n\ndef get_tensor_proto_data_size(tp: TensorProto, fpmode: int = TPT.FLOAT) -> int:\n    if tp.raw_data:\n        if fpmode == TPT.FLOAT16:\n            return len(tp.raw_data) // 2\n        return len(tp.raw_data) // 4\n    elif tp.data_type in (TPT.FLOAT, TPT.FLOAT16):\n        return len(tp.float_data)\n\n    return 0\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/update_model_dims.py",
    "content": "# Copyright (c) ONNX Project Contributors\n# SPDX-License-Identifier: Apache-2.0\n#\n# Include in chaiNNer with the following changes:\n# - Remove `dim_proto.dim_value != dim` for constant dimensions\n# - Improved type hints\nfrom __future__ import annotations\n\nfrom collections.abc import Sequence\n\nimport onnx.checker\nfrom onnx import ModelProto, ValueInfoProto\n\n\ndef update_inputs_outputs_dims(\n    model: ModelProto,\n    input_dims: dict[str, Sequence[str | int]],\n    output_dims: dict[str, Sequence[str | int]],\n) -> ModelProto:\n    \"\"\"This function updates the dimension sizes of the model's inputs and outputs to the values\n    provided in input_dims and output_dims. if the dim value provided is negative, a unique dim_param\n    will be set for that dimension.\n\n    Example. if we have the following shape for inputs and outputs:\n\n    * shape(input_1) = ('b', 3, 'w', 'h')\n    * shape(input_2) = ('b', 4)\n    * shape(output)  = ('b', 'd', 5)\n\n    The parameters can be provided as:\n\n    ::\n\n        input_dims = {\n            \"input_1\": ['b', 3, 'w', 'h'],\n            \"input_2\": ['b', 4],\n        }\n        output_dims = {\n            \"output\": ['b', -1, 5]\n        }\n\n    Putting it together:\n\n    ::\n\n        model = onnx.load('model.onnx')\n        updated_model = update_inputs_outputs_dims(model, input_dims, output_dims)\n        onnx.save(updated_model, 'model.onnx')\n    \"\"\"\n    dim_param_set: set[str] = set()\n\n    def init_dim_param_set(\n        dim_param_set: set[str], value_infos: list[ValueInfoProto]\n    ) -> None:\n        for info in value_infos:\n            shape = info.type.tensor_type.shape\n            for dim in shape.dim:\n                if dim.HasField(\"dim_param\"):\n                    dim_param_set.add(dim.dim_param)  # type: ignore\n\n    init_dim_param_set(dim_param_set, model.graph.input)  # type: ignore\n    init_dim_param_set(dim_param_set, model.graph.output)  # type: ignore\n    init_dim_param_set(dim_param_set, model.graph.value_info)  # type: ignore\n\n    def update_dim(tensor: ValueInfoProto, dim: str | int, j: int, name: str) -> None:\n        dim_proto = tensor.type.tensor_type.shape.dim[j]\n        if isinstance(dim, int):\n            if dim >= 0:\n                dim_proto.dim_value = dim\n            else:\n                generated_dim_param = name + \"_\" + str(j)\n                if generated_dim_param in dim_param_set:\n                    raise ValueError(\n                        f\"Unable to generate unique dim_param for axis {j} of {name}. Please manually provide a dim_param value.\"\n                    )\n                dim_proto.dim_param = generated_dim_param\n        else:\n            dim_proto.dim_param = dim\n\n    for input_ in model.graph.input:\n        input_name = input_.name\n        input_dim_arr = input_dims[input_name]\n        for j, dim in enumerate(input_dim_arr):\n            update_dim(input_, dim, j, input_name)\n\n    for output in model.graph.output:\n        output_name = output.name\n        output_dim_arr = output_dims[output_name]\n        for j, dim in enumerate(output_dim_arr):\n            update_dim(output, dim, j, output_name)\n\n    onnx.checker.check_model(model)\n    return model\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/onnx/utils.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, Literal\n\nimport onnx\nfrom onnx.onnx_pb import ModelProto, ValueInfoProto\n\nfrom .update_model_dims import update_inputs_outputs_dims\n\nOnnxTensorFormat = Literal[\"BCHW\", \"BHWC\"]\nOnnxTensorShape = tuple[\n    int | str,\n    int | str,\n    int | str,\n    int | str,\n]\nOnnxParsedTensorShape = tuple[\n    OnnxTensorFormat,\n    int,\n    int | None,\n    int | None,\n]\n\"\"\"\nThe elements are:\n- The tensor format (BCHW or BHWC).\n- The number of channels.\n- The width (optional).\n- The height (optional).\n\"\"\"\n\n\ndef _as_int(value: object) -> int | None:\n    if isinstance(value, int):\n        return value\n    return None\n\n\ndef _or_else(value: int | None, default: int) -> int:\n    return value if value is not None else default\n\n\ndef parse_onnx_shape(shape: OnnxTensorShape) -> OnnxParsedTensorShape:\n    if isinstance(shape[1], int) and shape[1] <= 4:\n        return \"BCHW\", shape[1], _as_int(shape[3]), _as_int(shape[2])\n    elif isinstance(shape[3], int) and shape[3] <= 4:\n        return \"BHWC\", shape[3], _as_int(shape[2]), _as_int(shape[1])\n    else:\n        return \"BCHW\", 3, _as_int(shape[3]), _as_int(shape[2])\n\n\ndef to_onnx_tensor_shape(\n    tensor: onnx.TypeProto.Tensor,\n) -> OnnxTensorShape:\n    shape = tuple(\n        dim.dim_param if dim.HasField(\"dim_param\") else dim.dim_value\n        for dim in tensor.shape.dim\n    )\n    if len(shape) != 4:\n        raise ValueError(f\"Expected 4 dimensions, got {len(shape)}\")\n    return shape\n\n\ndef is_tensor_input(input: ValueInfoProto) -> bool:\n    return input.type.HasField(\"tensor_type\")\n\n\ndef is_image_to_image(model: ModelProto) -> bool:\n    \"\"\"\n    Returns whether the model is an image to image model (single image input -> single image output).\n    \"\"\"\n    if len(model.graph.input) != 1 or len(model.graph.output) != 1:\n        return False\n\n    i = model.graph.input[0]\n    o = model.graph.output[0]\n    return is_tensor_input(i) and is_tensor_input(o)\n\n\nclass ModelShapeInference:\n    def __init__(self, model: ModelProto) -> None:\n        self.model = model\n\n        i = model.graph.input[0]\n        o = model.graph.output[0]\n\n        if not is_tensor_input(i) or not is_tensor_input(o):\n            raise ValueError(\"Expected tensor inputs and outputs\")\n\n        # modify model to have a fixed input size\n        self.input_shape = to_onnx_tensor_shape(i.type.tensor_type)\n        self.output_shape = to_onnx_tensor_shape(o.type.tensor_type)\n\n        parsed_input_shape = parse_onnx_shape(self.input_shape)\n        self.tensor_format = parsed_input_shape[0]\n        self.input_channels = parsed_input_shape[1]\n        self.fixed_input_width = parsed_input_shape[2]\n        self.fixed_input_height = parsed_input_shape[3]\n\n        self.output_channels = _as_int(\n            self.output_shape[1]\n            if self.tensor_format == \"BCHW\"\n            else self.output_shape[3]\n        )\n\n    def infer_shape(\n        self, input_size: tuple[int, int]\n    ) -> tuple[\n        tuple[int | None, int | None, int | None],\n        tuple[int | None, int | None, int | None],\n    ]:\n        \"\"\"\n        input_shape: The size of the input tensors as width, height.\n\n        return: The shapes of the input and output tensors in HWC format.\n\n        **This will mutate the model.**\n        \"\"\"\n        b = _or_else(_as_int(self.input_shape[0]), 1)\n        c = self.input_channels\n        h = _or_else(self.fixed_input_height, input_size[1])\n        w = _or_else(self.fixed_input_width, input_size[0])\n\n        new_inputs: list[Any]\n        if self.tensor_format == \"BCHW\":\n            new_inputs = [b, c, h, w]\n        elif self.tensor_format == \"BHWC\":\n            new_inputs = [b, h, w, c]\n        else:\n            raise ValueError(f\"Unknown tensor format: {self.tensor_format}\")\n\n        i = self.model.graph.input[0]\n        o = self.model.graph.output[0]\n\n        update_inputs_outputs_dims(\n            self.model,\n            {i.name: new_inputs},\n            {o.name: list(self.output_shape)},\n        )\n\n        # infer the output shape using the fixed input size\n        inferred_model = onnx.shape_inference.infer_shapes(self.model, strict_mode=True)\n        i = inferred_model.graph.input[0]\n        o = inferred_model.graph.output[0]\n\n        input_shape = to_onnx_tensor_shape(i.type.tensor_type)\n        output_shape = to_onnx_tensor_shape(o.type.tensor_type)\n\n        # output in HWC format\n        input_shape = input_shape[1:]\n        output_shape = output_shape[1:]\n\n        if self.tensor_format == \"BCHW\":\n            input_shape = input_shape[::-1]\n            output_shape = output_shape[::-1]\n\n        input_shape = tuple(_as_int(dim) for dim in input_shape)\n        output_shape = tuple(_as_int(dim) for dim in output_shape)\n\n        assert len(input_shape) == 3\n        assert len(output_shape) == 3\n\n        return input_shape[0:3], output_shape\n\n\ndef get_tensor_fp_datatype(model: ModelProto) -> str:\n    for item in [*model.graph.input, *model.graph.output]:\n        if item.type.HasField(\"tensor_type\"):\n            tensor = item.type.tensor_type\n            if tensor.elem_type == onnx.TensorProto.FLOAT16:\n                return \"fp16\"\n            if tensor.elem_type == onnx.TensorProto.FLOAT:\n                return \"fp32\"\n            if tensor.elem_type == onnx.TensorProto.DOUBLE:\n                return \"fp64\"\n            if tensor.elem_type == onnx.TensorProto.BFLOAT16:\n                return \"bf16\"\n    return \"fp32\"\n\n\ndef get_opset(model: onnx.ModelProto) -> int:\n    for opset in model.opset_import:\n        if opset.domain == \"\":\n            return opset.version\n    return -1\n\n\ndef safely_optimize_onnx_model(model_proto: ModelProto) -> ModelProto:\n    \"\"\"\n    Optimizes the model using onnxoptimizer. If onnxoptimizer is not installed, the model is returned as is.\n    \"\"\"\n    try:\n        import onnxoptimizer\n\n        passes = onnxoptimizer.get_fuse_and_elimination_passes()\n        model_proto = onnxoptimizer.optimize(model_proto, passes)\n    except Exception:\n        pass\n    return model_proto\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/pil_utils.py",
    "content": "from __future__ import annotations\n\nfrom enum import Enum\n\nimport numpy as np\nfrom PIL import Image\n\nfrom ..utils.utils import get_h_w_c\nfrom .image_utils import FillColor, convert_to_bgra, normalize, to_uint8\n\n\nclass InterpolationMethod(Enum):\n    AUTO = -1\n    NEAREST = 0\n    LANCZOS = 1\n    LINEAR = 2\n    CUBIC = 3\n    BOX = 4\n\n\nclass RotationInterpolationMethod(Enum):\n    CUBIC = InterpolationMethod.CUBIC.value\n    LINEAR = InterpolationMethod.LINEAR.value\n    NEAREST = InterpolationMethod.NEAREST.value\n\n    @property\n    def interpolation_method(self) -> InterpolationMethod:\n        return InterpolationMethod(self.value)\n\n\nINTERPOLATION_METHODS_MAP = {\n    InterpolationMethod.NEAREST: Image.NEAREST,\n    InterpolationMethod.BOX: Image.BOX,\n    InterpolationMethod.LINEAR: Image.BILINEAR,\n    InterpolationMethod.CUBIC: Image.BICUBIC,\n    InterpolationMethod.LANCZOS: Image.LANCZOS,\n}\n\n\nclass RotateSizeChange(Enum):\n    EXPAND = 1\n    CROP = 0\n\n\ndef resize(\n    img: np.ndarray, out_dims: tuple[int, int], interpolation: InterpolationMethod\n) -> np.ndarray:\n    \"\"\"Perform PIL resize\"\"\"\n\n    if interpolation == InterpolationMethod.AUTO:\n        # automatically chose a method that works\n        new_w, new_h = out_dims\n        old_h, old_w, _ = get_h_w_c(img)\n        if new_w > old_w or new_h > old_h:\n            interpolation = InterpolationMethod.LANCZOS\n        else:\n            interpolation = InterpolationMethod.BOX\n\n    resample = INTERPOLATION_METHODS_MAP[interpolation]\n\n    pimg = Image.fromarray(to_uint8(img, normalized=True))\n    pimg = pimg.resize(out_dims, resample=resample)  # type: ignore\n    return normalize(np.array(pimg))\n\n\ndef rotate(\n    img: np.ndarray,\n    angle: float,\n    interpolation: RotationInterpolationMethod,\n    expand: RotateSizeChange,\n    fill: FillColor,\n) -> np.ndarray:\n    \"\"\"Perform PIL rotate\"\"\"\n\n    c = get_h_w_c(img)[2]\n    if fill == FillColor.TRANSPARENT:\n        img = convert_to_bgra(img, c)\n    fill_color = tuple([x * 255 for x in fill.get_color(c)])\n\n    resample = INTERPOLATION_METHODS_MAP[interpolation.interpolation_method]\n\n    pimg = Image.fromarray(to_uint8(img, normalized=True))\n    pimg = pimg.rotate(\n        angle,\n        resample=resample,  # type: ignore\n        expand=bool(expand.value),\n        fillcolor=fill_color,  # type: ignore\n    )\n    return normalize(np.array(pimg))\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/__init__.py",
    "content": ""
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/auto_split.py",
    "content": "from __future__ import annotations\n\nimport gc\n\nimport numpy as np\nimport torch\nfrom accelerator_detection import get_autocast_device_type, is_device_type_supported_for_autocast\nfrom nodes.utils.utils import get_h_w_c\nfrom spandrel import ImageModelDescriptor\n\nfrom api import Progress\n\nfrom ..upscale.auto_split import Split, Tiler, auto_split\nfrom .utils import safe_accelerator_cache_empty\n\n\ndef _into_standard_image_form(t: torch.Tensor) -> torch.Tensor:\n    if len(t.shape) == 2:\n        # (H, W)\n        return t\n    elif len(t.shape) == 3:\n        # (C, H, W) -> (H, W, C)\n        return t.permute(1, 2, 0)\n    elif len(t.shape) == 4:\n        # (1, C, H, W) -> (H, W, C)\n        return t.squeeze(0).permute(1, 2, 0)\n    else:\n        raise ValueError(\"Unsupported output tensor shape\")\n\n\ndef _into_batched_form(t: torch.Tensor) -> torch.Tensor:\n    if len(t.shape) == 2:\n        # (H, W) -> (1, 1, H, W)\n        return t.unsqueeze(0).unsqueeze(0)\n    elif len(t.shape) == 3:\n        # (H, W, C) -> (1, C, H, W)\n        return t.permute(2, 0, 1).unsqueeze(0)\n    else:\n        raise ValueError(\"Unsupported input tensor shape\")\n\n\ndef _rgb_to_bgr(t: torch.Tensor) -> torch.Tensor:\n    if len(t.shape) == 3 and t.shape[2] == 3:\n        # (H, W, C) RGB -> BGR\n        return t.flip(2)\n    elif len(t.shape) == 3 and t.shape[2] == 4:\n        # (H, W, C) RGBA -> BGRA\n        return torch.cat((t[:, :, 2:3], t[:, :, 1:2], t[:, :, 0:1], t[:, :, 3:4]), 2)\n    else:\n        return t\n\n\ndef _into_tensor(\n    img: np.ndarray, device: torch.device, dtype: torch.dtype\n) -> torch.Tensor:\n    img = np.ascontiguousarray(img)\n    writeable = img.flags.writeable\n    try:\n        if not writeable and device == torch.device(\"cpu\"):\n            img = np.copy(img)\n        else:\n            # since we are going to copy the image to the GPU, we can skip the copy here\n            try:\n                img.flags.writeable = True\n            except Exception:\n                # Some arrays cannot be made writeable, and we need to copy them\n                img = np.copy(img)\n        if device == torch.device(\"cpu\"):\n            input_tensor = (\n                torch.from_numpy(img).to(device, dtype, non_blocking=True)\n            )\n        else:\n            input_tensor = (\n                torch.from_numpy(img).pin_memory().to(device, dtype, non_blocking=True)\n            )\n        return input_tensor\n    finally:\n        img.flags.writeable = writeable\n\n\n@torch.inference_mode()\ndef pytorch_auto_split(\n    img: np.ndarray,\n    model: ImageModelDescriptor[torch.nn.Module],\n    device: torch.device,\n    use_fp16: bool,\n    tiler: Tiler,\n    progress: Progress,\n) -> np.ndarray:\n    dtype = torch.float32\n    if use_fp16:\n        if model.supports_half:\n            dtype = torch.float16\n        elif torch.cuda.is_bf16_supported():\n            dtype = torch.bfloat16\n    # print(\"dtype\", dtype, use_fp16, flush=True)\n    if model.dtype != dtype or model.device != device:\n        # print(\"move model\", flush=True)\n        model = model.to(device, dtype, memory_format=torch.channels_last)\n\n    def upscale(img: np.ndarray, _: object):\n        progress.check_aborted()\n        if progress.paused:\n            # clear resources before pausing\n            gc.collect()\n            safe_cuda_cache_empty()\n            progress.suspend()\n\n        input_tensor = None\n        try:\n            _, _, input_channels = get_h_w_c(img)\n            # convert to tensor\n            input_tensor = _into_tensor(img, device, dtype)\n            # expand grayscale tensor to match model input channels\n            if input_channels == 1 and model.input_channels > 1:\n                input_tensor = input_tensor.repeat(1, 1, model.input_channels)\n            input_tensor = _into_batched_form(input_tensor)\n            input_tensor = input_tensor.to(\n                memory_format=torch.channels_last\n            )  # TODO refactor\n            # inference with accelerator-aware autocast\n            autocast_device_type = get_autocast_device_type(device)\n            autocast_enabled = is_device_type_supported_for_autocast(device) and use_fp16\n            \n            with torch.autocast(device_type=autocast_device_type, dtype=dtype, enabled=autocast_enabled):\n                output_tensor = model(input_tensor)\n\n            # convert back to numpy\n            output_tensor = _into_standard_image_form(output_tensor)\n            if input_channels == 1:\n                output_tensor = output_tensor[:, :, 0].unsqueeze(-1)\n            # print(\"out dtype\", output_tensor.dtype, flush=True)\n            # result = output_tensor.detach().cpu().detach().float().numpy()\n            result = output_tensor.detach().cpu().detach()\n            if result.dtype == torch.bfloat16:\n                result = result.float()\n            result = result.numpy()\n\n            return result\n        except RuntimeError as e:\n            # Check to see if its actually an out of memory error\n            if \"allocate\" in str(e) or \"CUDA\" in str(e) or \"out of memory\" in str(e).lower():\n                # Collect garbage (clear memory)\n                if input_tensor is not None:\n                    try:\n                        input_tensor.detach().cpu()\n                    except Exception:\n                        pass\n                    del input_tensor\n                gc.collect()\n                safe_accelerator_cache_empty(device)\n                return Split()\n            else:\n                # Re-raise the exception if not an OOM error\n                raise\n\n    return auto_split(img, upscale, tiler)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/convert_to_onnx_impl.py",
    "content": "from io import BytesIO\n\nimport torch\nfrom spandrel import ImageModelDescriptor, ModelDescriptor\nfrom spandrel.architectures.CRAFT import CRAFT\nfrom spandrel.architectures.SAFMN import SAFMN\nfrom spandrel.architectures.SCUNet import SCUNet\n\n\ndef is_onnx_supported(model: ModelDescriptor) -> bool:\n    return not isinstance(model.model, SCUNet | SAFMN | CRAFT)\n\n\ndef convert_to_onnx_impl(\n    model: ModelDescriptor,\n    device: torch.device,\n    use_half: bool = False,\n    input_name: str = \"input\",\n    output_name: str = \"output\",\n    opset_version: int = 14,\n) -> bytes:\n    # https://github.com/onnx/onnx/issues/654\n    dynamic_axes = {\n        input_name: {0: \"batch_size\", 2: \"height\", 3: \"width\"},\n        output_name: {0: \"batch_size\", 2: \"height\", 3: \"width\"},\n    }\n    size = 3\n    size += model.size_requirements.get_padding(size, size)[0]\n    dummy_input = torch.rand(1, model.input_channels, size, size)\n    dummy_input = dummy_input.to(device)\n\n    if use_half:\n        if not model.supports_half:\n            raise ValueError(\n                f\"Model of arch {model.architecture} does not support half precision.\"\n            )\n        model.half()\n        dummy_input = dummy_input.half()\n    else:\n        model.float()\n        dummy_input = dummy_input.float()\n\n    m = model.model\n\n    if isinstance(model, ImageModelDescriptor):\n\n        class FakeModel(torch.nn.Module):\n            def __init__(self, model: ImageModelDescriptor) -> None:\n                super().__init__()\n                self.model = model\n\n            def forward(self, x: torch.Tensor):\n                return self.model(x)\n\n        m = FakeModel(model)\n\n    with BytesIO() as f:\n        torch.onnx.export(\n            m,\n            dummy_input,\n            f,\n            opset_version=opset_version,\n            verbose=False,\n            input_names=[input_name],\n            output_names=[output_name],\n            dynamic_axes=dynamic_axes,\n            do_constant_folding=True,\n        )\n        f.seek(0)\n        return f.read()\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/pix_transform/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Riccardo de Lutio\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/pix_transform/auto_split.py",
    "content": "from __future__ import annotations\n\nimport gc\n\nimport numpy as np\nimport torch\n\nfrom ....utils.utils import Region, Size, get_h_w_c\nfrom ...image_op import to_op\nfrom ...upscale.auto_split import Split, auto_split\nfrom ...upscale.grayscale import SplitMode, grayscale_split\nfrom ...upscale.passthrough import passthrough_single_color\nfrom ...upscale.tiler import Tiler\nfrom ..utils import safe_accelerator_cache_empty\nfrom .pix_transform import Params, pix_transform\n\n\nclass _PixTiler(Tiler):\n    def __init__(self, max_tile_size: int = 2048) -> None:\n        self.max_tile_size: int = max_tile_size\n\n    def allow_smaller_tile_size(self) -> bool:\n        return False\n\n    def starting_tile_size(self, width: int, height: int, channels: int) -> Size:\n        square = min(width, height, self.max_tile_size)\n        return square, square\n\n    def split(self, tile_size: Size) -> Size:\n        # half the tile size plus a bit extra to account for overlap\n        size = tile_size[0] // 2 + tile_size[0] // 8\n        if size < 16:\n            raise ValueError(\"Cannot split any further.\")\n        return size, size\n\n\ndef _as_3d(img: np.ndarray) -> np.ndarray:\n    if img.ndim == 3:\n        return img\n    return np.expand_dims(img, axis=2)\n\n\ndef pix_transform_auto_split(\n    source: np.ndarray,\n    guide: np.ndarray,\n    device: torch.device,\n    params: Params,\n    split_mode: SplitMode = SplitMode.LAB,\n) -> np.ndarray:\n    \"\"\"\n    Automatically splits the source and guide image into segments that can be processed by PixTransform.\n\n    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.\n    \"\"\"\n\n    s_w, s_h, _ = get_h_w_c(source)\n    g_w, g_h, _ = get_h_w_c(guide)\n\n    assert (\n        g_h > s_h and g_w > s_w\n    ), \"The guide image mus be larger than the source image.\"\n    assert (\n        g_w / s_w == g_w // s_w and g_w / s_w == g_h / s_h\n    ), \"The size of the guide image must be an integer multiple of the size of the source image (e.g. 2x, 3x, 4x, ...).\"\n\n    tiler = _PixTiler()\n    scale = g_w // s_w\n\n    def upscale(tile: np.ndarray, region: Region):\n        try:\n            tile_guide = region.scale(scale).read_from(guide)\n            pix_op = to_op(pix_transform)(\n                guide_img=np.transpose(_as_3d(tile_guide), (2, 0, 1)),\n                device=device,\n                params=params,\n            )\n            # passthrough single colors to speed up alpha channels\n            pass_op = to_op(passthrough_single_color)(scale, pix_op)\n\n            return grayscale_split(tile, pass_op, split_mode)\n        except RuntimeError as e:\n            # Check to see if its actually an out of memory error\n            if \"allocate\" in str(e) or \"CUDA\" in str(e) or \"out of memory\" in str(e).lower():\n                # Collect garbage (clear memory)\n                gc.collect()\n                safe_accelerator_cache_empty(device)\n                # Retry once with smaller tiles\n                return Split()\n            else:\n                # Re-raise the exception if not an OOM error\n                raise\n\n    return auto_split(source, upscale, tiler)\n\n\ndef _run_pix_transform():\n    with torch.no_grad():\n        safe_accelerator_cache_empty(device)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/pix_transform/pix_transform.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Literal\n\nimport numpy as np\nimport torch\nimport torch.utils.data\nfrom torch import optim\n\nfrom .pix_transform_net import PixTransformNet\n\n\n@dataclass\nclass Params:\n    spatial_features_input: bool = True\n    # spatial color head\n    weights_regularizer: tuple[float, float, float] | None = (0.0001, 0.001, 0.001)\n    loss: Literal[\"mse\", \"l1\"] = \"l1\"\n    lr: float = 0.001\n    batch_size: int = 32\n    iteration: int = 32 * 1024\n\n\ndef pix_transform(\n    source_img: np.ndarray,\n    guide_img: np.ndarray,\n    device: torch.device,\n    params: Params,\n) -> np.ndarray:\n    if len(guide_img.shape) < 3:\n        guide_img = np.expand_dims(guide_img, 0)\n\n    _n_channels, hr_height, hr_width = guide_img.shape\n\n    source_img = source_img.squeeze()\n    lr_height, lr_width = source_img.shape\n\n    assert hr_height == hr_width\n    assert lr_height == lr_width\n    assert hr_height % lr_height == 0\n\n    d = hr_height // lr_height\n    m = lr_height\n    _n = hr_height\n\n    # normalize guide and source\n    guide_img = (\n        guide_img - np.mean(guide_img, axis=(1, 2), keepdims=True)\n    ) / np.maximum(0.0001, np.std(guide_img, axis=(1, 2), keepdims=True))\n\n    source_img_mean = np.mean(source_img)\n    source_img_std = np.std(source_img)\n    source_img = (source_img - source_img_mean) / np.maximum(0.0001, source_img_std)\n\n    if params.spatial_features_input:\n        x = np.linspace(-0.5, 0.5, hr_width)\n        x_grid, y_grid = np.meshgrid(x, x, indexing=\"ij\")\n\n        x_grid = np.expand_dims(x_grid, axis=0)\n        y_grid = np.expand_dims(y_grid, axis=0)\n\n        guide_img = np.concatenate([guide_img, x_grid, y_grid], axis=0)\n\n    #### prepare_patches #########################################################################\n    # guide_patches is M^2 x C x D x D\n    # source_pixels is M^2 x 1\n\n    guide_tensor = torch.from_numpy(guide_img).float().to(device)\n    source_tensor = torch.from_numpy(source_img).float().to(device)\n\n    guide_patches = torch.zeros((m * m, guide_tensor.shape[0], d, d)).to(device)\n    source_pixels = torch.zeros((m * m, 1)).to(device)\n    for i in range(m):\n        for j in range(m):\n            guide_patches[j + i * m, :, :, :] = guide_tensor[\n                :, i * d : (i + 1) * d, j * d : (j + 1) * d\n            ]\n            source_pixels[j + i * m] = source_tensor[i : (i + 1), j : (j + 1)]\n\n    train_data = torch.utils.data.TensorDataset(guide_patches, source_pixels)\n    train_loader = torch.utils.data.DataLoader(\n        train_data, batch_size=params.batch_size, shuffle=True\n    )\n    ###############################################################################################\n\n    #### setup network ############################################################################\n    mynet = (\n        PixTransformNet(\n            channels_in=guide_tensor.shape[0],\n            weights_regularizer=params.weights_regularizer,\n        )\n        .train()\n        .to(device)\n    )\n    optimizer = optim.Adam(mynet.params_with_regularizer, lr=params.lr)\n    if params.loss == \"mse\":\n        myloss = torch.nn.MSELoss()\n    elif params.loss == \"l1\":\n        myloss = torch.nn.L1Loss()\n    else:\n        raise AssertionError(\"unknown loss!\")\n    ###############################################################################################\n\n    epochs = params.batch_size * params.iteration // (m * m)\n    for _epoch in range(epochs):\n        for x, y in train_loader:\n            optimizer.zero_grad()\n\n            y_pred = mynet(x)\n            y_mean_pred = torch.mean(y_pred, dim=[2, 3])\n\n            source_patch_consistency = myloss(y_mean_pred, y)\n\n            source_patch_consistency.backward()\n            optimizer.step()\n\n    # compute final prediction, un-normalize, and back to numpy\n    mynet.eval()\n    predicted_target_img = mynet(guide_tensor.unsqueeze(0)).squeeze()\n    predicted_target_img = source_img_mean + source_img_std * predicted_target_img\n    predicted_target_img = predicted_target_img.cpu().detach().squeeze().numpy()\n\n    return predicted_target_img\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/pix_transform/pix_transform_net.py",
    "content": "from __future__ import annotations\n\nfrom torch import nn\n\n\nclass PixTransformNet(nn.Module):\n    def __init__(\n        self,\n        channels_in: int = 5,\n        kernel_size: int = 1,\n        weights_regularizer: tuple[float, float, float] | None = None,\n    ) -> None:\n        super().__init__()\n\n        self.channels_in = channels_in\n\n        self.spatial_net = nn.Sequential(\n            nn.Conv2d(2, 32, (1, 1), padding=0),\n            nn.ReLU(),\n            nn.Conv2d(\n                32, 2048, (kernel_size, kernel_size), padding=(kernel_size - 1) // 2\n            ),\n        )\n        self.color_net = nn.Sequential(\n            nn.Conv2d(channels_in - 2, 32, (1, 1), padding=0),\n            nn.ReLU(),\n            nn.Conv2d(\n                32, 2048, (kernel_size, kernel_size), padding=(kernel_size - 1) // 2\n            ),\n        )\n        self.head_net = nn.Sequential(\n            nn.ReLU(),\n            nn.Conv2d(\n                2048, 32, (kernel_size, kernel_size), padding=(kernel_size - 1) // 2\n            ),\n            nn.ReLU(),\n            nn.Conv2d(32, 1, (1, 1), padding=0),\n        )\n\n        if weights_regularizer is None:\n            reg_spatial = 0.0001\n            reg_color = 0.001\n            reg_head = 0.0001\n        else:\n            reg_spatial = weights_regularizer[0]\n            reg_color = weights_regularizer[1]\n            reg_head = weights_regularizer[2]\n\n        self.params_with_regularizer = []\n        self.params_with_regularizer += [\n            {\"params\": self.spatial_net.parameters(), \"weight_decay\": reg_spatial}\n        ]\n        self.params_with_regularizer += [\n            {\"params\": self.color_net.parameters(), \"weight_decay\": reg_color}\n        ]\n        self.params_with_regularizer += [\n            {\"params\": self.head_net.parameters(), \"weight_decay\": reg_head}\n        ]\n\n    def forward(self, input_):  # noqa: ANN001\n        input_spatial = input_[:, self.channels_in - 2 :, :, :]\n        input_color = input_[:, 0 : self.channels_in - 2, :, :]\n\n        merged_features = self.spatial_net(input_spatial) + self.color_net(input_color)\n\n        return self.head_net(merged_features)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/rife/IFNet_HDv3_v4_14_align.py",
    "content": "# type: ignore\n# Original Rife Frame Interpolation by hzwer\n# https://github.com/megvii-research/ECCV2022-RIFE\n# https://github.com/hzwer/Practical-RIFE\n\n# Modifications to use Rife for Image Alignment by tepete/pifroggi ('Enhance Everything!' Discord Server)\n\n# Additional helpful github issues\n# https://github.com/megvii-research/ECCV2022-RIFE/issues/278\n# https://github.com/megvii-research/ECCV2022-RIFE/issues/344\n\nimport torch\nimport torch.nn.functional as F  # noqa: N812\nfrom torch import nn\nfrom torchvision import transforms\n\nfrom .warplayer import warp\n\n\ndef conv(in_planes, out_planes, kernel_size=3, stride=1, padding=1, dilation=1):  # noqa: ANN001\n    return nn.Sequential(\n        nn.Conv2d(\n            in_planes,\n            out_planes,\n            kernel_size=kernel_size,\n            stride=stride,\n            padding=padding,\n            dilation=dilation,\n            bias=True,\n        ),\n        nn.LeakyReLU(0.2, True),\n    )\n\n\ndef conv_bn(in_planes, out_planes, kernel_size=3, stride=1, padding=1, dilation=1):  # noqa: ANN001\n    return nn.Sequential(\n        nn.Conv2d(\n            in_planes,\n            out_planes,\n            kernel_size=kernel_size,\n            stride=stride,\n            padding=padding,\n            dilation=dilation,\n            bias=False,\n        ),\n        nn.BatchNorm2d(out_planes),\n        nn.LeakyReLU(0.2, True),\n    )\n\n\nclass Head(nn.Module):\n    def __init__(self) -> None:\n        super().__init__()\n        self.cnn0 = nn.Conv2d(3, 32, 3, 2, 1)\n        self.cnn1 = nn.Conv2d(32, 32, 3, 1, 1)\n        self.cnn2 = nn.Conv2d(32, 32, 3, 1, 1)\n        self.cnn3 = nn.ConvTranspose2d(32, 8, 4, 2, 1)\n        self.relu = nn.LeakyReLU(0.2, True)\n\n    def forward(self, x, feat=False):  # noqa: ANN001\n        x0 = self.cnn0(x)\n        x = self.relu(x0)\n        x1 = self.cnn1(x)\n        x = self.relu(x1)\n        x2 = self.cnn2(x)\n        x = self.relu(x2)\n        x3 = self.cnn3(x)\n        if feat:\n            return [x0, x1, x2, x3]\n        return x3\n\n\nclass ResConv(nn.Module):\n    def __init__(self, c, dilation=1) -> None:  # noqa: ANN001\n        super().__init__()\n        self.conv = nn.Conv2d(c, c, 3, 1, dilation, dilation=dilation, groups=1)\n        self.beta = nn.Parameter(torch.ones((1, c, 1, 1)), requires_grad=True)\n        self.relu = nn.LeakyReLU(0.2, True)\n\n    def forward(self, x):  # noqa: ANN001\n        return self.relu(self.conv(x) * self.beta + x)\n\n\nclass IFBlock(nn.Module):\n    def __init__(self, in_planes, c=64) -> None:  # noqa: ANN001\n        super().__init__()\n        self.conv0 = nn.Sequential(\n            conv(in_planes, c // 2, 3, 2, 1),\n            conv(c // 2, c, 3, 2, 1),\n        )\n        self.convblock = nn.Sequential(\n            ResConv(c),\n            ResConv(c),\n            ResConv(c),\n            ResConv(c),\n            ResConv(c),\n            ResConv(c),\n            ResConv(c),\n            ResConv(c),\n        )\n        self.lastconv = nn.Sequential(\n            nn.ConvTranspose2d(c, 4 * 6, 4, 2, 1), nn.PixelShuffle(2)\n        )\n\n    def forward(self, x, flow=None, scale=1):  # noqa: ANN001\n        x = F.interpolate(\n            x, scale_factor=1.0 / scale, mode=\"bilinear\", align_corners=False\n        )\n        if flow is not None:\n            flow = (\n                F.interpolate(\n                    flow, scale_factor=1.0 / scale, mode=\"bilinear\", align_corners=False\n                )\n                * 1.0\n                / scale\n            )\n            x = torch.cat((x, flow), 1)\n        feat = self.conv0(x)\n        feat = self.convblock(feat)\n        tmp = self.lastconv(feat)\n        tmp = F.interpolate(\n            tmp, scale_factor=scale, mode=\"bilinear\", align_corners=False\n        )\n        flow = tmp[:, :4] * scale\n        mask = tmp[:, 4:5]\n        return flow, mask\n\n\nclass IFNet(nn.Module):\n    def __init__(self) -> None:\n        super().__init__()\n        self.block0 = IFBlock(7 + 16, c=192)\n        self.block1 = IFBlock(8 + 4 + 16, c=128)\n        self.block2 = IFBlock(8 + 4 + 16, c=96)\n        self.block3 = IFBlock(8 + 4 + 16, c=64)\n        self.encode = Head()\n\n    def align_images(\n        self,\n        img0,  # noqa: ANN001\n        img1,  # noqa: ANN001\n        timestep,  # noqa: ANN001\n        scale_list,  # noqa: ANN001\n        blur_strength,  # noqa: ANN001\n        ensemble,  # noqa: ANN001\n        device,  # noqa: ANN001\n    ):\n        # optional blur\n        if blur_strength is not None and blur_strength > 0:\n            blur = transforms.GaussianBlur(\n                kernel_size=(5, 5), sigma=(blur_strength, blur_strength)\n            )\n            img0_blurred = blur(img0)\n            img1_blurred = blur(img1)\n        else:\n            img0_blurred = img0\n            img1_blurred = img1\n\n        f0 = self.encode(img0_blurred[:, :3])\n        f1 = self.encode(img1_blurred[:, :3])\n        flow_list = []\n        mask_list = []\n        flow = None\n        mask = None\n        block = [self.block0, self.block1, self.block2, self.block3]\n        for i in range(4):\n            if flow is None:\n                flow, mask = block[i](\n                    torch.cat(\n                        (img0_blurred[:, :3], img1_blurred[:, :3], f0, f1, timestep), 1\n                    ),\n                    None,\n                    scale=scale_list[i],\n                )\n                if ensemble:\n                    f_, m_ = block[i](\n                        torch.cat(\n                            (\n                                img1_blurred[:, :3],\n                                img0_blurred[:, :3],\n                                f1,\n                                f0,\n                                1 - timestep,\n                            ),\n                            1,\n                        ),\n                        None,\n                        scale=scale_list[i],\n                    )\n                    flow = (flow + torch.cat((f_[:, 2:4], f_[:, :2]), 1)) / 2\n                    mask = (mask + (-m_)) / 2\n            else:\n                wf0 = warp(f0, flow[:, :2], device)\n                wf1 = warp(f1, flow[:, 2:4], device)\n                fd, m0 = block[i](\n                    torch.cat(\n                        (\n                            img0_blurred[:, :3],\n                            img1_blurred[:, :3],\n                            wf0,\n                            wf1,\n                            timestep,\n                            mask,\n                        ),\n                        1,\n                    ),\n                    flow,\n                    scale=scale_list[i],\n                )\n                if ensemble:\n                    f_, m_ = block[i](\n                        torch.cat(\n                            (\n                                img1_blurred[:, :3],\n                                img0_blurred[:, :3],\n                                wf1,\n                                wf0,\n                                1 - timestep,\n                                -mask,\n                            ),\n                            1,\n                        ),\n                        torch.cat((flow[:, 2:4], flow[:, :2]), 1),\n                        scale=scale_list[i],\n                    )\n                    fd = (fd + torch.cat((f_[:, 2:4], f_[:, :2]), 1)) / 2\n                    mask = (m0 + (-m_)) / 2\n                else:\n                    mask = m0\n                flow = flow + fd\n            mask_list.append(mask)\n            flow_list.append(flow)\n\n        # apply warp to original image\n        aligned_img0 = warp(img0, flow_list[-1][:, :2], device)\n\n        # add clamp here instead of in warplayer script, as it changes the output there\n        aligned_img0 = aligned_img0.clamp(min=0.0, max=1.0)\n        return aligned_img0, flow_list[-1]\n\n    def forward(\n        self,\n        x,  # noqa: ANN001\n        timestep=1,  # noqa: ANN001\n        training=False,  # noqa: ANN001\n        fastmode=True,  # noqa: ANN001\n        ensemble=True,  # noqa: ANN001\n        num_iterations=1,  # noqa: ANN001\n        multiplier=0.5,  # noqa: ANN001\n        blur_strength=0,  # noqa: ANN001\n        device=\"cuda\",  # noqa: ANN001\n    ):\n        if not training:\n            channel = x.shape[1] // 2\n            img0 = x[:, :channel]\n            img1 = x[:, channel:]\n\n        scale_list = [multiplier * 8, multiplier * 4, multiplier * 2, multiplier]\n\n        if not torch.is_tensor(timestep):\n            timestep = (x[:, :1].clone() * 0 + 1) * timestep\n        else:\n            timestep = timestep.repeat(1, 1, img0.shape[2], img0.shape[3])  # type: ignore\n\n        for _iteration in range(num_iterations):\n            aligned_img0, flow = self.align_images(\n                img0, img1, timestep, scale_list, blur_strength, ensemble, device\n            )\n            img0 = aligned_img0  # use the aligned image as img0 for the next iteration\n\n        return aligned_img0, flow\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/rife/warplayer.py",
    "content": "# type: ignore\nimport torch\n\nbackwarp_tenGrid = {}  # noqa: N816\n\n\ndef warp(tenInput, tenFlow, device):  # noqa: ANN001, N803\n    k = (str(tenFlow.device), str(tenFlow.size()))\n    if k not in backwarp_tenGrid:\n        tenHorizontal = (  # noqa: N806\n            torch.linspace(-1.0, 1.0, tenFlow.shape[3], device=device)\n            .view(1, 1, 1, tenFlow.shape[3])\n            .expand(tenFlow.shape[0], -1, tenFlow.shape[2], -1)\n        )\n        tenVertical = (  # noqa: N806\n            torch.linspace(-1.0, 1.0, tenFlow.shape[2], device=device)\n            .view(1, 1, tenFlow.shape[2], 1)\n            .expand(tenFlow.shape[0], -1, -1, tenFlow.shape[3])\n        )\n        backwarp_tenGrid[k] = torch.cat([tenHorizontal, tenVertical], 1).to(device)\n\n    tenFlow = torch.cat(  # noqa: N806\n        [\n            tenFlow[:, 0:1, :, :] / ((tenInput.shape[3] - 1.0) / 2.0),\n            tenFlow[:, 1:2, :, :] / ((tenInput.shape[2] - 1.0) / 2.0),\n        ],\n        1,\n    )\n\n    g = (backwarp_tenGrid[k] + tenFlow).permute(0, 2, 3, 1)\n    tenOutput = torch.nn.functional.grid_sample(\n        input=tenInput,\n        grid=g,\n        mode=\"bicubic\",\n        padding_mode=\"border\",\n        align_corners=True,\n    )\n    return tenOutput\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/pytorch/utils.py",
    "content": "from __future__ import annotations\n\nimport numpy as np\nimport torch\nfrom torch import Tensor\n\nfrom ..image_utils import as_3d\nfrom ..onnx.np_tensor_utils import MAX_VALUES_BY_DTYPE, np_denorm\n\n\ndef bgr_to_rgb(image: Tensor) -> Tensor:\n    # flip image channels\n    # https://github.com/pytorch/pytorch/issues/229\n    out: Tensor = image.flip(-3)\n    # RGB to BGR #may be faster:\n    # out: Tensor = image[[2, 1, 0], :, :]\n    return out\n\n\ndef rgb_to_bgr(image: Tensor) -> Tensor:\n    # same operation as bgr_to_rgb(), flip image channels\n    return bgr_to_rgb(image)\n\n\ndef bgra_to_rgba(image: Tensor) -> Tensor:\n    out: Tensor = image[[2, 1, 0, 3], :, :]\n    return out\n\n\ndef rgba_to_bgra(image: Tensor) -> Tensor:\n    # same operation as bgra_to_rgba(), flip image channels\n    return bgra_to_rgba(image)\n\n\ndef norm(x: Tensor):\n    \"\"\"Normalize (z-norm) from [0,1] range to [-1,1]\"\"\"\n    out = (x - 0.5) * 2.0\n    return out.clamp(-1, 1)\n\n\ndef np2tensor(\n    img: np.ndarray,\n    bgr2rgb: bool = True,\n    data_range: float = 1.0,\n    normalize: bool = False,\n    change_range: bool = True,\n    add_batch: bool = True,\n) -> Tensor:\n    \"\"\"Converts a numpy image array into a Tensor array.\n    Parameters:\n        img (numpy array): the input image numpy array\n        add_batch (bool): choose if new tensor needs batch dimension added\n    \"\"\"\n\n    # check how many channels the image has, then condition. ie. RGB, RGBA, Gray\n    # if bgr2rgb:\n    #     img = img[\n    #         :, :, [2, 1, 0]\n    #     ]  # BGR to RGB -> in numpy, if using OpenCV, else not needed. Only if image has colors.\n    if change_range:\n        dtype = img.dtype\n        maxval = MAX_VALUES_BY_DTYPE.get(dtype.name, 1.0)\n        t_dtype = np.dtype(\"float32\")\n        img = img.astype(t_dtype) / maxval  # ie: uint8 = /255\n    # \"HWC to CHW\" and \"numpy to tensor\"\n    tensor = torch.from_numpy(\n        np.ascontiguousarray(np.transpose(as_3d(img), (2, 0, 1)))\n    ).float()\n    if bgr2rgb:\n        # BGR to RGB -> in tensor, if using OpenCV, else not needed. Only if image has colors.)\n        if tensor.shape[0] % 3 == 0:\n            # RGB or MultixRGB (3xRGB, 5xRGB, etc. For video tensors.)\n            tensor = bgr_to_rgb(tensor)\n        elif tensor.shape[0] == 4:\n            # RGBA\n            tensor = bgra_to_rgba(tensor)\n    if add_batch:\n        # Add fake batch dimension = 1 . squeeze() will remove the dimensions of size 1\n        tensor.unsqueeze_(0)\n    if normalize:\n        tensor = norm(tensor)\n    return tensor\n\n\ndef tensor2np(\n    img: Tensor,\n    rgb2bgr: bool = True,\n    remove_batch: bool = True,\n    data_range: float = 255,\n    denormalize: bool = False,\n    change_range: bool = True,\n    imtype: type = np.uint8,\n) -> np.ndarray:\n    \"\"\"Converts a Tensor array into a numpy image array.\n    Parameters:\n        img (tensor): the input image tensor array\n            4D(B,(3/1),H,W), 3D(C,H,W), or 2D(H,W), any range, RGB channel order\n        remove_batch (bool): choose if tensor of shape BCHW needs to be squeezed\n        denormalize (bool): Used to denormalize from [-1,1] range back to [0,1]\n        imtype (type): the desired type of the converted numpy array (np.uint8\n            default)\n    Output:\n        img (np array): 3D(H,W,C) or 2D(H,W), [0,255], np.uint8 (default)\n    \"\"\"\n    n_dim = img.dim()\n\n    # TODO: Check: could denormalize here in tensor form instead, but end result is the same\n\n    img = img.float().cpu()\n\n    img_np: np.ndarray\n\n    if n_dim in (4, 3):\n        # if n_dim == 4, has to convert to 3 dimensions\n        if n_dim == 4 and remove_batch:\n            # remove a fake batch dimension\n            img = img.squeeze(dim=0)\n\n        if img.shape[0] == 3 and rgb2bgr:  # RGB\n            # RGB to BGR -> in tensor, if using OpenCV, else not needed. Only if image has colors.\n            img_np = rgb_to_bgr(img).numpy()\n        elif img.shape[0] == 4 and rgb2bgr:  # RGBA\n            # RGBA to BGRA -> in tensor, if using OpenCV, else not needed. Only if image has colors.\n            img_np = rgba_to_bgra(img).numpy()\n        else:\n            img_np = img.numpy()\n        img_np = np.transpose(img_np, (1, 2, 0))  # CHW to HWC\n    elif n_dim == 2:\n        img_np = img.numpy()\n    else:\n        raise TypeError(\n            f\"Only support 4D, 3D and 2D tensor. But received with dimension: {n_dim:d}\"\n        )\n\n    # if rgb2bgr:\n    # img_np = img_np[[2, 1, 0], :, :] #RGB to BGR -> in numpy, if using OpenCV, else not needed. Only if image has colors.\n    # TODO: Check: could denormalize in the begining in tensor form instead\n    if denormalize:\n        img_np = np_denorm(img_np)  # denormalize if needed\n    if change_range:\n        img_np = np.clip(\n            data_range * img_np, 0, data_range\n        ).round()  # np.clip to the data_range\n\n    # has to be in range (0,255) before changing to np.uint8, else np.float32\n    return img_np.astype(imtype)\n\n\ndef safe_cuda_cache_empty() -> None:\n    \"\"\"\n    Empties the CUDA cache if CUDA is available. Hopefully without causing any errors.\n    \n    DEPRECATED: Use safe_accelerator_cache_empty() instead for better accelerator support.\n    \"\"\"\n    try:\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n    except Exception:\n        pass\n\n\ndef safe_accelerator_cache_empty(device: torch.device) -> None:\n    \"\"\"\n    Empties the accelerator cache for the given device type. \n    Supports CUDA, ROCm, XPU, MPS, and other accelerators.\n    \"\"\"\n    try:\n        device_type = device.type\n        \n        if device_type in [\"cuda\", \"rocm\"]:  # ROCm uses CUDA API\n            if torch.cuda.is_available():\n                torch.cuda.empty_cache()\n        elif device_type == \"xpu\":\n            if hasattr(torch, 'xpu') and torch.xpu.is_available():\n                torch.xpu.empty_cache()\n        elif device_type == \"mps\":\n            if (hasattr(torch, 'backends') and \n                hasattr(torch.backends, 'mps') and \n                torch.backends.mps.is_available()):\n                # MPS doesn't have an explicit empty_cache, but we can try\n                # to trigger garbage collection\n                import gc\n                gc.collect()\n        # For other device types, we just do garbage collection\n        # since they typically don't have specific cache clearing APIs\n        else:\n            import gc\n            gc.collect()\n    except Exception:\n        # Fallback to garbage collection if anything fails\n        import gc\n        gc.collect()\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/resize.py",
    "content": "from __future__ import annotations\n\nfrom enum import Enum\n\nimport numpy as np\nfrom chainner_ext import ResizeFilter as NativeResizeFilter\nfrom chainner_ext import resize as native_resize\n\nfrom ..utils.utils import get_h_w_c\n\n\nclass ResizeFilter(Enum):\n    AUTO = -1\n    NEAREST = 0\n    BOX = 4\n    LINEAR = 2\n    CATROM = 3\n    LANCZOS = 1\n\n    HERMITE = 5\n    MITCHELL = 6\n    BSPLINE = 7\n    HAMMING = 8\n    HANN = 9\n    LAGRANGE = 10\n    GAUSS = 11\n\n\n_FILTER_MAP: dict[ResizeFilter, NativeResizeFilter] = {\n    ResizeFilter.NEAREST: NativeResizeFilter.Nearest,\n    ResizeFilter.BOX: NativeResizeFilter.Box,\n    ResizeFilter.LINEAR: NativeResizeFilter.Linear,\n    ResizeFilter.CATROM: NativeResizeFilter.CubicCatrom,\n    ResizeFilter.LANCZOS: NativeResizeFilter.Lanczos,\n    ResizeFilter.HERMITE: NativeResizeFilter.Hermite,\n    ResizeFilter.MITCHELL: NativeResizeFilter.CubicMitchell,\n    ResizeFilter.BSPLINE: NativeResizeFilter.CubicBSpline,\n    ResizeFilter.HAMMING: NativeResizeFilter.Hamming,\n    ResizeFilter.HANN: NativeResizeFilter.Hann,\n    ResizeFilter.LAGRANGE: NativeResizeFilter.Lagrange,\n    ResizeFilter.GAUSS: NativeResizeFilter.Gauss,\n}\n\n\ndef resize(\n    img: np.ndarray,\n    out_dims: tuple[int, int],\n    filter: ResizeFilter,\n    separate_alpha: bool = False,\n    gamma_correction: bool = False,\n) -> np.ndarray:\n    h, w, c = get_h_w_c(img)\n    new_w, new_h = out_dims\n\n    # check memory\n    GB: int = 2**30  # noqa: N806\n    MAX_MEMORY = 16 * GB  # noqa: N806\n    new_memory = new_w * new_h * c * 4\n    if new_memory > MAX_MEMORY:\n        raise RuntimeError(\n            f\"Resize would require {round(new_memory / GB, 3)} GB of memory, but only {MAX_MEMORY//GB} GB are allowed.\"\n        )\n\n    if filter == ResizeFilter.AUTO:\n        # automatically chose a method that works\n        if new_w > w or new_h > h:\n            filter = ResizeFilter.LANCZOS\n        else:\n            filter = ResizeFilter.BOX\n\n    if (w, h) == out_dims and (filter in (ResizeFilter.NEAREST, ResizeFilter.BOX)):\n        # no resize needed\n        return img.copy()\n\n    if filter == ResizeFilter.NEAREST:\n        # we don't need premultiplied alpha for NN\n        separate_alpha = True\n\n    native_filter = _FILTER_MAP[filter]\n\n    if not separate_alpha and c == 4:\n        # pre-multiply alpha\n        img = img.copy()\n        img[:, :, 0] *= img[..., 3]\n        img[:, :, 1] *= img[..., 3]\n        img[:, :, 2] *= img[..., 3]\n\n    img = native_resize(img, out_dims, native_filter, gamma_correction)\n    # native_resize guarantees that the output is float32 in the range [0, 1]\n    # so no need to normalize\n\n    if not separate_alpha and c == 4:\n        # undo pre-multiply alpha\n        alpha_r = 1 / np.maximum(img[..., 3], 0.0001)\n        img[:, :, 0] *= alpha_r\n        img[:, :, 1] *= alpha_r\n        img[:, :, 2] *= alpha_r\n        np.minimum(img, 1, out=img)\n\n    return img\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/__init__.py",
    "content": ""
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/auto_split.py",
    "content": "from __future__ import annotations\n\nimport math\nfrom collections.abc import Callable\n\nimport numpy as np\nfrom sanic.log import logger\n\nfrom ...utils.utils import Region, Size, get_h_w_c\nfrom .exact_split import exact_split\nfrom .tile_blending import BlendDirection, TileBlender, TileOverlap, half_sin_blend_fn\nfrom .tiler import Tiler\n\n\nclass Split:\n    pass\n\n\nSplitImageOp = Callable[[np.ndarray, Region], np.ndarray | Split]\n\n\ndef auto_split(\n    img: np.ndarray,\n    upscale: SplitImageOp,\n    tiler: Tiler,\n    overlap: int = 16,\n) -> np.ndarray:\n    \"\"\"\n    Splits the image into tiles according to the given tiler.\n\n    This method only changes the size of the given image, the tiles passed into the upscale function will have same number of channels.\n\n    The region passed into the upscale function is the region of the current tile.\n    The size of the region is guaranteed to be the same as the size of the given tile.\n\n    ## Padding\n\n    If the given tiler allows smaller tile sizes, then it is guaranteed that no padding will be added.\n    Otherwise, no padding is only guaranteed if the starting tile size is not larger than the size of the given image.\n    \"\"\"\n\n    h, w, c = get_h_w_c(img)\n    split = _max_split if tiler.allow_smaller_tile_size() else _exact_split\n\n    return split(\n        img,\n        upscale=upscale,\n        starting_tile_size=tiler.starting_tile_size(w, h, c),\n        split_tile_size=tiler.split,\n        overlap=overlap,\n    )\n\n\nclass _SplitEx(Exception):\n    pass\n\n\ndef _exact_split(\n    img: np.ndarray,\n    upscale: SplitImageOp,\n    starting_tile_size: Size,\n    split_tile_size: Callable[[Size], Size],\n    overlap: int,\n) -> np.ndarray:\n    h, w, c = get_h_w_c(img)\n    logger.debug(\n        f\"Exact size split image ({w}x{h}px @ {c}) with exact tile size {starting_tile_size[0]}x{starting_tile_size[1]}px.\"\n    )\n\n    def no_split_upscale(i: np.ndarray, r: Region) -> np.ndarray:\n        result = upscale(i, r)\n        if isinstance(result, Split):\n            raise _SplitEx\n        return result\n\n    MAX_ITER = 20  # noqa: N806\n\n    for _ in range(MAX_ITER):\n        try:\n            max_overlap = min(*starting_tile_size) // 4\n            return exact_split(\n                img=img,\n                exact_size=starting_tile_size,\n                upscale=no_split_upscale,\n                overlap=min(max_overlap, overlap),\n            )\n        except _SplitEx:\n            starting_tile_size = split_tile_size(starting_tile_size)\n\n    raise ValueError(f\"Aborting after {MAX_ITER} splits. Unable to upscale image.\")\n\n\ndef _max_split(\n    img: np.ndarray,\n    upscale: SplitImageOp,\n    starting_tile_size: Size,\n    split_tile_size: Callable[[Size], Size],\n    overlap: int,\n) -> np.ndarray:\n    \"\"\"\n    Splits the image into tiles with at most the given tile size.\n\n    If the upscale method requests a split, then the tile size will be lowered.\n    \"\"\"\n\n    h, w, c = get_h_w_c(img)\n\n    img_region = Region(0, 0, w, h)\n\n    max_tile_size = starting_tile_size\n    logger.debug(\n        f\"Auto split image ({w}x{h}px @ {c}) with initial tile size {max_tile_size}.\"\n    )\n\n    if w <= max_tile_size[0] and h <= max_tile_size[1]:\n        # the image might be small enough so that we don't have to split at all\n        upscale_result = upscale(img, img_region)\n        if not isinstance(upscale_result, Split):\n            return upscale_result\n\n        # the image was too large\n        max_tile_size = split_tile_size(max_tile_size)\n\n        logger.warn(\n            f\"Unable to upscale the whole image at once. Reduced tile size to {max_tile_size}.\"\n        )\n\n    # The upscale method is allowed to request splits at any time.\n    # When a split occurs, we have to \"restart\" the loop and\n    # this variable allow us to split the already processed tiles.\n    start_y = 0\n\n    # To allocate the result image, we need to know the upscale factor first,\n    # and we only get to know this factor after the first successful upscale.\n    result: TileBlender | None = None\n    scale: int = 0\n    out_channels: int = 0\n\n    restart = True\n    while restart:\n        restart = False\n\n        # This is a bit complex.\n        # We don't actually use the current tile size to partition the image.\n        # If we did, then tile_size=1024 and w=1200 would result in very uneven tiles.\n        # Instead, we use tile_size to calculate how many tiles we get in the x and y direction\n        # and then calculate the optimal tile size for the x and y direction using the counts.\n        # This yields optimal tile sizes which should prevent unnecessary splitting.\n        tile_count_x = math.ceil(w / max_tile_size[0])\n        tile_count_y = math.ceil(h / max_tile_size[1])\n        tile_size_x = math.ceil(w / tile_count_x)\n        tile_size_y = math.ceil(h / tile_count_y)\n\n        logger.debug(\n            f\"Currently {tile_count_x}x{tile_count_y} tiles each {tile_size_x}x{tile_size_y}px.\"\n        )\n\n        prev_row_result: TileBlender | None = None\n\n        for y in range(tile_count_y):\n            if y < start_y:\n                continue\n\n            row_result: TileBlender | None = None\n            row_overlap: TileOverlap | None = None\n\n            for x in range(tile_count_x):\n                tile = Region(\n                    x * tile_size_x, y * tile_size_y, tile_size_x, tile_size_y\n                ).intersect(img_region)\n                pad = img_region.child_padding(tile).min(overlap)\n                padded_tile = tile.add_padding(pad)\n\n                upscale_result = upscale(padded_tile.read_from(img), padded_tile)\n\n                if isinstance(upscale_result, Split):\n                    max_tile_size = split_tile_size(max_tile_size)\n\n                    new_tile_count_y = math.ceil(h / max_tile_size[1])\n                    new_tile_size_y = math.ceil(h / new_tile_count_y)\n                    start_y = (y * tile_size_x) // new_tile_size_y\n\n                    logger.debug(\n                        f\"Split occurred. New tile size is {max_tile_size}. Starting at row {start_y}.\"\n                    )\n\n                    # reset result\n                    if result is not None:\n                        # we already added at least one row, so we have to set the offset back\n                        result.offset = start_y * new_tile_size_y\n\n                    restart = True\n                    break\n\n                # figure out by how much the image was upscaled by\n                up_h, up_w, up_c = get_h_w_c(upscale_result)\n                current_scale = up_h // padded_tile.height\n                assert current_scale > 0\n                assert padded_tile.height * current_scale == up_h\n                assert padded_tile.width * current_scale == up_w\n\n                if row_result is None:\n                    # allocate the result image\n                    scale = current_scale\n                    out_channels = up_c\n                    row_result = TileBlender(\n                        width=w * scale,\n                        height=padded_tile.height * scale,\n                        channels=out_channels,\n                        direction=BlendDirection.X,\n                        blend_fn=half_sin_blend_fn,\n                        _prev=prev_row_result,\n                    )\n                    prev_row_result = row_result\n                    row_overlap = TileOverlap(pad.top * scale, pad.bottom * scale)\n\n                assert current_scale == scale\n\n                # add to row\n                row_result.add_tile(\n                    upscale_result, TileOverlap(pad.left * scale, pad.right * scale)\n                )\n\n            if restart:\n                break\n\n            assert row_result is not None\n            assert row_overlap is not None\n\n            if result is None:\n                result = TileBlender(\n                    width=w * scale,\n                    height=h * scale,\n                    channels=out_channels,\n                    direction=BlendDirection.Y,\n                    blend_fn=half_sin_blend_fn,\n                )\n\n            # add row\n            result.add_tile(row_result.get_result(), row_overlap)\n\n    assert result is not None\n    return result.get_result()\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/auto_split_tiles.py",
    "content": "from collections.abc import Callable\nfrom typing import NewType\n\nimport numpy as np\nfrom sanic.log import logger\n\nfrom ...utils.utils import get_h_w_c\nfrom .tiler import MaxTileSize, NoTiling, Tiler\n\nGB_AMT = 1024**3\n\n\ndef estimate_tile_size(\n    budget: int,\n    model_size: int,\n    img: np.ndarray,\n    img_element_size: int = 4,\n) -> int:\n    h, w, c = get_h_w_c(img)\n    img_bytes = h * w * c * img_element_size\n    mem_required_estimation = (model_size / (1024 * 52)) * img_bytes\n\n    tile_pixels = w * h * budget / mem_required_estimation\n    # the largest power-of-2 tile_size such that tile_size**2 < tile_pixels\n    tile_size = 2 ** (int(tile_pixels**0.5).bit_length() - 1)\n    # tile_size = int(tile_pixels**0.5) // 16 * 16\n\n    required_mem = f\"{mem_required_estimation/GB_AMT:.2f}\"\n    budget_mem = f\"{budget/GB_AMT:.2f}\"\n    logger.debug(\n        f\"Estimating memory required: {required_mem} GB, {budget_mem} GB free.\"\n        f\" Estimated tile size: {tile_size}, tile_pixels = {tile_pixels}\",\n    )\n\n    return tile_size\n\n\nTileSize = NewType(\"TileSize\", int)\nESTIMATE = TileSize(0)\nNO_TILING = TileSize(-1)\nMAX_TILE_SIZE = TileSize(-2)\nCUSTOM = TileSize(-3)\nTILE_SIZE_256 = TileSize(256)\n\n\ndef parse_tile_size_input(tile_size: TileSize, estimate: Callable[[], Tiler]) -> Tiler:\n    if tile_size == 0:\n        return estimate()\n    if tile_size == -1:\n        return NoTiling()\n    if tile_size == -2:\n        return MaxTileSize()\n\n    assert tile_size > 0\n    return MaxTileSize(tile_size)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/basic_upscale.py",
    "content": "import math\nfrom dataclasses import dataclass\nfrom enum import Enum\n\nimport numpy as np\nfrom nodes.impl.image_op import ImageOp\nfrom nodes.impl.image_utils import BorderType\nfrom nodes.impl.resize import ResizeFilter, resize\nfrom nodes.utils.utils import get_h_w_c\n\nfrom .convenient_upscale import convenient_upscale\n\n\n@dataclass\nclass UpscaleInfo:\n    in_nc: int\n    out_nc: int\n    scale: int\n\n    @property\n    def supports_custom_scale(self) -> bool:\n        return self.scale != 1 and self.in_nc == self.out_nc\n\n\nclass PaddingType(Enum):\n    NONE = 0\n    REFLECT_MIRROR = 1\n    WRAP = 2\n    REPLICATE = 3\n\n    def to_border_type(self) -> BorderType:\n        if self == PaddingType.NONE:\n            raise ValueError(\n                \"PaddingType.NONE does not have a corresponding BorderType\"\n            )\n        elif self == PaddingType.REFLECT_MIRROR:\n            return BorderType.REFLECT_MIRROR\n        elif self == PaddingType.WRAP:\n            return BorderType.WRAP\n        elif self == PaddingType.REPLICATE:\n            return BorderType.REPLICATE\n\n        raise ValueError(f\"Unknown padding type: {self}\")\n\n\nPAD_SIZE = 16\n\n\ndef _custom_scale_upscale(\n    img: np.ndarray,\n    upscale: ImageOp,\n    natural_scale: int,\n    custom_scale: int,\n    separate_alpha: bool,\n) -> np.ndarray:\n    if custom_scale == natural_scale:\n        return upscale(img)\n\n    # number of iterations we need to do to reach the desired scale\n    # e.g. if the model is 2x and the desired scale is 13x, we need to do 4 iterations\n    iterations = max(1, math.ceil(math.log(custom_scale, natural_scale)))\n    org_h, org_w, _ = get_h_w_c(img)\n    for _ in range(iterations):\n        img = upscale(img)\n\n    # resize, if necessary\n    target_size = (\n        org_w * custom_scale,\n        org_h * custom_scale,\n    )\n    h, w, _ = get_h_w_c(img)\n    if (w, h) != target_size:\n        img = resize(\n            img,\n            target_size,\n            ResizeFilter.BOX,\n            separate_alpha=separate_alpha,\n        )\n\n    return img\n\n\ndef basic_upscale(\n    img: np.ndarray,\n    upscale: ImageOp,\n    upscale_info: UpscaleInfo,\n    scale: int,\n    separate_alpha: bool,\n    clip: bool = True,\n):\n    def inner_upscale(img: np.ndarray) -> np.ndarray:\n        return convenient_upscale(\n            img,\n            upscale_info.in_nc,\n            upscale_info.out_nc,\n            upscale,\n            separate_alpha,\n            clip=clip,\n        )\n\n    if not upscale_info.supports_custom_scale and scale != upscale_info.scale:\n        raise ValueError(\n            f\"Upscale info does not support custom scale: {upscale_info}, scale: {scale}\"\n        )\n\n    img = _custom_scale_upscale(\n        img,\n        inner_upscale,\n        natural_scale=upscale_info.scale,\n        custom_scale=scale,\n        separate_alpha=separate_alpha,\n    )\n\n    return img\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/convenient_upscale.py",
    "content": "from __future__ import annotations\n\nimport numpy as np\n\nfrom ...utils.utils import get_h_w_c\nfrom ..image_op import ImageOp, clipped\nfrom ..image_utils import as_target_channels\n\n\ndef with_black_and_white_backgrounds(img: np.ndarray) -> tuple[np.ndarray, np.ndarray]:\n    c = get_h_w_c(img)[2]\n    assert c == 4\n\n    black = np.copy(img[:, :, :3])\n    white = np.copy(img[:, :, :3])\n    for c in range(3):\n        black[:, :, c] *= img[:, :, 3]\n        white[:, :, c] = (white[:, :, c] - 1) * img[:, :, 3] + 1\n\n    return black, white\n\n\ndef denoise_and_flatten_alpha(img: np.ndarray) -> np.ndarray:\n    alpha_min = np.min(img, axis=2)\n    alpha_max = np.max(img, axis=2)\n    alpha_mean = np.mean(img, axis=2)\n    alpha = alpha_max * alpha_mean + alpha_min * (1 - alpha_mean)\n    return alpha.clip(0, 1)\n\n\ndef convenient_upscale(\n    img: np.ndarray,\n    model_in_nc: int,\n    model_out_nc: int,\n    upscale: ImageOp,\n    separate_alpha: bool = False,\n    clip: bool = True,\n) -> np.ndarray:\n    \"\"\"\n    Upscales the given image in an intuitive/convenient way.\n\n    This method guarantees that the `upscale` function will be called with an image with\n    `model_in_nc` number of channels.\n\n    Additionally, guarantees that the number of channels of the output image will match\n    that of the input image in cases where `model_in_nc` == `model_out_nc`, and match\n    `model_out_nc` otherwise.\n    \"\"\"\n    in_img_c = get_h_w_c(img)[2]\n\n    if clip:\n        upscale = clipped(upscale)\n\n    if model_in_nc != model_out_nc:\n        return upscale(as_target_channels(img, model_in_nc, True))\n\n    if in_img_c == model_in_nc:\n        return upscale(img)\n\n    if in_img_c == 4:\n        # Ignore alpha if single-color or not being replaced\n        unique = np.unique(img[:, :, 3])\n        if len(unique) == 1:\n            rgb = as_target_channels(\n                upscale(as_target_channels(img[:, :, :3], model_in_nc, True)), 3, True\n            )\n            unique_alpha = np.full(rgb.shape[:-1], unique[0], np.float32)\n            return np.dstack((rgb, unique_alpha))\n\n        if separate_alpha:\n            # Upscale the RGB channels and alpha channel separately\n            rgb = as_target_channels(\n                upscale(as_target_channels(img[:, :, :3], model_in_nc, True)), 3, True\n            )\n            alpha = denoise_and_flatten_alpha(\n                upscale(as_target_channels(img[:, :, 3], model_in_nc, True))\n            )\n            return np.dstack((rgb, alpha))\n        else:\n            # Transparency hack (white/black background difference alpha)\n            black, white = with_black_and_white_backgrounds(img)\n            black_up = as_target_channels(\n                upscale(as_target_channels(black, model_in_nc, True)), 3, True\n            )\n            white_up = as_target_channels(\n                upscale(as_target_channels(white, model_in_nc, True)), 3, True\n            )\n\n            # Interpolate between the alpha values to get a more defined alpha\n            alpha_candidates = 1 - (white_up - black_up)  #  type: ignore\n            alpha = denoise_and_flatten_alpha(alpha_candidates)\n\n            return np.dstack((black_up, alpha))\n\n    # skip all conversions for grayscale to improve performance by reducing the amount of data that needs to be copied\n    # instead we do the color conversions on the tensors after they're already on the gpu\n    if in_img_c == 1:\n        if img.ndim == 2:\n            img = np.expand_dims(img, axis=-1)\n        return upscale(img)\n\n    return as_target_channels(\n        upscale(as_target_channels(img, model_in_nc, True)), in_img_c, True\n    )\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/custom_scale.py",
    "content": "import math\n\nimport numpy as np\nfrom nodes.impl.image_op import ImageOp\nfrom nodes.impl.resize import ResizeFilter, resize\nfrom nodes.utils.utils import get_h_w_c\n\n\ndef custom_scale_upscale(\n    img: np.ndarray,\n    upscale: ImageOp,\n    natural_scale: int,\n    custom_scale: int,\n    separate_alpha: bool,\n) -> np.ndarray:\n    if custom_scale == natural_scale:\n        return upscale(img)\n\n    # number of iterations we need to do to reach the desired scale\n    # e.g. if the model is 2x and the desired scale is 13x, we need to do 4 iterations\n    iterations = max(1, math.ceil(math.log(custom_scale, natural_scale)))\n    org_h, org_w, _ = get_h_w_c(img)\n    for _ in range(iterations):\n        img = upscale(img)\n\n    # resize, if necessary\n    target_size = (\n        org_w * custom_scale,\n        org_h * custom_scale,\n    )\n    h, w, _ = get_h_w_c(img)\n    if (w, h) != target_size:\n        img = resize(\n            img,\n            target_size,\n            ResizeFilter.BOX,\n            separate_alpha=separate_alpha,\n        )\n\n    return img\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/exact_split.py",
    "content": "from __future__ import annotations\n\nimport math\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\n\nimport numpy as np\nfrom sanic.log import logger\n\nfrom ...utils.utils import Padding, Region, Size, get_h_w_c\nfrom ..image_utils import BorderType, create_border\nfrom .tile_blending import BlendDirection, TileBlender, TileOverlap, half_sin_blend_fn\n\n\ndef _pad_image(img: np.ndarray, min_size: Size):\n    h, w, _ = get_h_w_c(img)\n\n    min_w, min_h = min_size\n    x = max(0, min_w - w) / 2\n    y = max(0, min_h - h) / 2\n\n    padding = Padding(math.floor(y), math.floor(x), math.ceil(y), math.ceil(x))\n\n    return create_border(img, BorderType.REFLECT_MIRROR, padding), padding\n\n\n@dataclass\nclass _Segment:\n    start: int\n    end: int\n    start_padding: int\n    end_padding: int\n\n    @property\n    def length(self) -> int:\n        return self.end - self.start\n\n    @property\n    def padded_length(self) -> int:\n        return self.end + self.end_padding - (self.start - self.start_padding)\n\n\ndef _exact_split_into_segments(length: int, exact: int, overlap: int) -> list[_Segment]:\n    \"\"\"\n    Splits the given length into segments of `exact` (padded) length.\n    Segments will overlap into each other with at least the given overlap.\n    \"\"\"\n    if length == exact:\n        # trivial\n        return [_Segment(0, exact, 0, 0)]\n\n    assert length > exact\n    assert exact > overlap * 2\n\n    result: list[_Segment] = []\n\n    def add(s: _Segment) -> None:\n        assert s.padded_length == exact\n        result.append(s)\n\n    # The current strategy is to go from left to right and to align segments\n    # such that we use the least overlap possible. The last segment will then\n    # be the smallest with potentially a lot of overlap.\n    # While this is easy to implement, it's actually not ideal. Ideally, we\n    # would want for the overlap to be distributed evenly between segments.\n    # However, this is complex to implement and the current method also works.\n\n    # we know that the first segment looks like this\n    add(_Segment(0, exact - overlap, 0, overlap))\n\n    while result[-1].end < length:\n        start_padding = overlap\n        start = result[-1].end\n        end = start + exact - overlap * 2\n        end_padding = overlap\n\n        if end + end_padding >= length:\n            # last segment\n            end_padding = 0\n            end = length\n            start_padding = exact - (end - start)\n\n        add(_Segment(start, end, start_padding, end_padding))\n\n    return result\n\n\ndef _exact_split_into_regions(\n    w: int,\n    h: int,\n    exact_w: int,\n    exact_h: int,\n    overlap: int,\n) -> list[list[tuple[Region, Padding]]]:\n    \"\"\"\n    Returns a list of disjoint regions along with padding.\n    Each region plus its padding is guaranteed to have the given exact size.\n    The padding (if not zero) is guaranteed to be at least the given overlap value.\n    \"\"\"\n\n    # we can split x and y independently from each other and then combine the results\n    x_segments = _exact_split_into_segments(w, exact_w, overlap)\n    y_segments = _exact_split_into_segments(h, exact_h, overlap)\n\n    logger.info(\n        f\"Image is split into {len(x_segments)}x{len(y_segments)} tiles each exactly {exact_w}x{exact_h}px.\"\n    )\n\n    result: list[list[tuple[Region, Padding]]] = []\n    for y in y_segments:\n        row: list[tuple[Region, Padding]] = []\n        for x in x_segments:\n            row.append(\n                (\n                    Region(x.start, y.start, x.length, y.length),\n                    Padding(\n                        y.start_padding, x.end_padding, y.end_padding, x.start_padding\n                    ),\n                )\n            )\n        result.append(row)\n    return result\n\n\ndef _exact_split_without_padding(\n    img: np.ndarray,\n    exact_size: Size,\n    upscale: Callable[[np.ndarray, Region], np.ndarray],\n    overlap: int,\n) -> np.ndarray:\n    h, w, _ = get_h_w_c(img)\n    exact_w, exact_h = exact_size\n    assert w >= exact_w and h >= exact_h\n\n    if (w, h) == exact_size:\n        return upscale(img, Region(0, 0, w, h))\n\n    # To allocate the result image, we need to know the upscale factor first,\n    # and we only get to know this factor after the first successful upscale.\n    result: TileBlender | None = None\n    scale: int = 0\n    out_channels: int = 0\n\n    regions = _exact_split_into_regions(w, h, exact_w, exact_h, overlap)\n    for row in regions:\n        row_result: TileBlender | None = None\n        row_overlap: TileOverlap | None = None\n\n        for tile, pad in row:\n            padded_tile = tile.add_padding(pad)\n            assert padded_tile.size == exact_size\n\n            upscale_result = upscale(padded_tile.read_from(img), padded_tile)\n\n            # figure out by how much the image was upscaled by\n            up_h, up_w, up_c = get_h_w_c(upscale_result)\n            current_scale = up_h // exact_h\n            assert current_scale > 0\n            assert exact_h * current_scale == up_h\n            assert exact_w * current_scale == up_w\n\n            if row_result is None:\n                # allocate the result image\n                scale = current_scale\n                out_channels = up_c\n                row_result = TileBlender(\n                    width=w * scale,\n                    height=exact_h * scale,\n                    channels=out_channels,\n                    direction=BlendDirection.X,\n                    blend_fn=half_sin_blend_fn,\n                )\n                row_overlap = TileOverlap(pad.top * scale, pad.bottom * scale)\n\n            assert current_scale == scale\n\n            row_result.add_tile(\n                upscale_result, TileOverlap(pad.left * scale, pad.right * scale)\n            )\n\n        assert row_result is not None\n        assert row_overlap is not None\n        if result is None:\n            result = TileBlender(\n                width=w * scale,\n                height=h * scale,\n                channels=out_channels,\n                direction=BlendDirection.Y,\n                blend_fn=half_sin_blend_fn,\n            )\n\n        result.add_tile(row_result.get_result(), row_overlap)\n\n    assert result is not None\n\n    # remove initially added padding\n    return result.get_result()\n\n\ndef exact_split(\n    img: np.ndarray,\n    exact_size: Size,\n    upscale: Callable[[np.ndarray, Region], np.ndarray],\n    overlap: int = 16,\n) -> np.ndarray:\n    \"\"\"\n    Splits the image into tiles with exactly the given tile size.\n\n    If the image is smaller than the given size, then it will be padded.\n    \"\"\"\n\n    # ensure that the image is at least as large as the given size\n    img, base_padding = _pad_image(img, exact_size)\n    h, w, _ = get_h_w_c(img)\n\n    result = _exact_split_without_padding(img, exact_size, upscale, overlap)\n    scale = get_h_w_c(result)[0] // h\n\n    if base_padding.empty:\n        return result\n\n    # remove initially added padding\n    return (\n        Region(0, 0, w, h).remove_padding(base_padding).scale(scale).read_from(result)\n    )\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/grayscale.py",
    "content": "from __future__ import annotations\n\nfrom enum import Enum\n\nimport numpy as np\n\nfrom ..color.convert import convert\nfrom ..color.convert_data import LAB, RGB\nfrom ..image_op import ImageOp\n\n\nclass SplitMode(Enum):\n    RGB = 1\n    LAB = 2\n\n    def split(self, img: np.ndarray) -> list[np.ndarray]:\n        if img.ndim == 2:\n            return [img]\n\n        assert img.ndim == 3\n        c = img.shape[2]\n\n        if c == 1:\n            return [img[:, :, 0]]\n\n        if self == SplitMode.RGB:\n            return [img[:, :, channel] for channel in range(c)]\n        elif self == SplitMode.LAB:\n            if c < 3:\n                return [img[:, :, channel] for channel in range(c)]\n            lab = convert(img[:, :, 0:3], RGB, LAB)\n            remaining_channels = [img[:, :, channel] for channel in range(3, c)]\n            return [\n                lab[:, :, 0],\n                lab[:, :, 1],\n                lab[:, :, 2],\n                *remaining_channels,\n            ]\n        else:\n            raise AssertionError()\n\n    def combine(self, channels: list[np.ndarray]) -> np.ndarray:\n        l = len(channels)\n        assert l > 0\n\n        if l == 1:\n            return channels[0]\n\n        if self == SplitMode.RGB:\n            return np.dstack(channels)\n        elif self == SplitMode.LAB:\n            if l < 3:\n                return np.dstack(channels)\n            rgb = convert(np.dstack(channels[0:3]), LAB, RGB)\n            if l == 3:\n                return rgb\n            return np.dstack([rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2], *channels[3:]])\n        else:\n            raise AssertionError()\n\n\ndef grayscale_split(\n    img: np.ndarray, process: ImageOp, mode: SplitMode = SplitMode.RGB\n) -> np.ndarray:\n    \"\"\"\n    This function guarantees that the given image operation method will be called with 2D single-channel images.\n    The images passed into the operation are guaranteed to have the same size as the given image.\n    \"\"\"\n\n    input_channels = mode.split(img)\n    output_channels: list[np.ndarray] = []\n    for channel in input_channels:\n        output_channels.append(process(channel))\n\n    return mode.combine(output_channels)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/passthrough.py",
    "content": "import numpy as np\n\nfrom ...utils.utils import get_h_w_c\nfrom ..image_op import ImageOp\n\n\ndef passthrough_single_color(img: np.ndarray, scale: int, op: ImageOp) -> np.ndarray:\n    \"\"\"\n    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.\n    Obviously, this optimization is only correct if `op` doesn't change the color of single-color images.\n\n    To make this a transparent optimization, it is important that `scale` is correct.\n    `scale` must be the same factor by which `op` changes the dimension of the image.\n    \"\"\"\n\n    h, w, c = get_h_w_c(img)\n\n    if c == 1:\n        unique_list = np.unique(img)\n        if len(unique_list) == 1:\n            return np.full((h * scale, w * scale), unique_list[0], np.float32)\n    else:\n        unique_values = []\n        is_unique = True\n        for channel in range(c):\n            unique_list = np.unique(img[:, :, channel])\n            if len(unique_list) == 1:\n                unique_values.append(unique_list[0])\n            else:\n                is_unique = False\n                break\n\n        if is_unique:\n            channels = [\n                np.full((h * scale, w * scale), unique_values[channel], np.float32)\n                for channel in range(c)\n            ]\n            return np.dstack(channels)\n\n    return op(img)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/tile_blending.py",
    "content": "from __future__ import annotations\n\nimport math\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom enum import Enum\n\nimport numpy as np\nfrom nodes.utils.utils import get_h_w_c\n\n\ndef sin_blend_fn(x: np.ndarray) -> np.ndarray:\n    return (np.sin(x * math.pi - math.pi / 2) + 1) / 2\n\n\ndef half_sin_blend_fn(i: np.ndarray) -> np.ndarray:\n    # only use half the overlap\n    i = np.clip(i * 2 - 0.5, 0, 1)\n    return sin_blend_fn(i)\n\n\nclass BlendDirection(Enum):\n    X = 0\n    Y = 1\n\n\n@dataclass(frozen=True)\nclass TileOverlap:\n    start: int\n    end: int\n\n    @property\n    def total(self) -> int:\n        return self.start + self.end\n\n\ndef _fast_mix(a: np.ndarray, b: np.ndarray, blend: np.ndarray) -> np.ndarray:\n    \"\"\"\n    Returns `a * (1 - blend) + b * blend`\n    \"\"\"\n    # a * (1 - blend) + b * blend\n    # a - a * blend + b * blend\n    r = b * blend\n    r += a\n    r -= a * blend  # type: ignore\n    return r\n\n\nclass TileBlender:\n    def __init__(\n        self,\n        width: int,\n        height: int,\n        channels: int,\n        direction: BlendDirection,\n        blend_fn: Callable[[np.ndarray], np.ndarray] = sin_blend_fn,\n        _prev: TileBlender | None = None,\n    ) -> None:\n        self.direction: BlendDirection = direction\n        self.blend_fn: Callable[[np.ndarray], np.ndarray] = blend_fn\n        self.offset: int = 0\n        self.last_end_overlap: int = 0\n        self._last_blend: np.ndarray | None = None\n\n        if (\n            _prev is not None\n            and _prev.direction == direction\n            and _prev.width == width\n            and _prev.height == height\n            and _prev.channels == channels\n        ):\n            if _prev.blend_fn == blend_fn:\n                # reuse blend\n                self._last_blend = _prev._last_blend  # noqa: SLF001\n            result = _prev.result\n        else:\n            result = np.zeros((height, width, channels), dtype=np.float32)\n        self.result: np.ndarray = result\n\n    @property\n    def width(self) -> int:\n        return self.result.shape[1]\n\n    @property\n    def height(self) -> int:\n        return self.result.shape[0]\n\n    @property\n    def channels(self) -> int:\n        return self.result.shape[2]\n\n    def _get_blend(self, blend_size: int) -> np.ndarray:\n        if self.direction == BlendDirection.X:\n            if self._last_blend is not None and self._last_blend.shape[1] == blend_size:\n                return self._last_blend\n\n            blend = self.blend_fn(\n                np.arange(blend_size, dtype=np.float32) / (blend_size - 1)\n            )\n            blend = blend.reshape((1, blend_size, 1))\n            blend = np.repeat(blend, repeats=self.height, axis=0)\n            blend = np.repeat(blend, repeats=self.channels, axis=2)\n        else:\n            if self._last_blend is not None and self._last_blend.shape[0] == blend_size:\n                return self._last_blend\n\n            blend = self.blend_fn(\n                np.arange(blend_size, dtype=np.float32) / (blend_size - 1)\n            )\n            blend = blend.reshape((blend_size, 1, 1))\n            blend = np.repeat(blend, repeats=self.width, axis=1)\n            blend = np.repeat(blend, repeats=self.channels, axis=2)\n\n        self._last_blend = blend\n        return blend\n\n    def add_tile(self, tile: np.ndarray, overlap: TileOverlap) -> None:\n        h, w, c = get_h_w_c(tile)\n        assert c == self.channels\n        o = overlap\n\n        if self.direction == BlendDirection.X:\n            assert h == self.height\n            assert w > o.total\n\n            if self.offset == 0:\n                # the first tile is copied in as is\n                self.result[:, :w, ...] = tile\n\n                assert o.start == 0\n                self.offset += w - o.end\n                self.last_end_overlap = o.end\n\n            else:\n                assert self.offset < self.width, \"All tiles were filled in already\"\n\n                if self.last_end_overlap < o.start:\n                    # we can't use all the overlap of the current tile, so we have to cut it off\n                    diff = o.start - self.last_end_overlap\n                    tile = tile[:, diff:, ...]\n                    h, w, c = get_h_w_c(tile)\n                    o = TileOverlap(self.last_end_overlap, o.end)\n\n                # copy over the part that doesn't need blending (yet)\n                self.result[\n                    :, self.offset + o.start : self.offset + w - o.start, ...\n                ] = tile[:, o.start * 2 :, ...]\n\n                # blend the overlapping part\n                blend_size = o.start * 2\n                blend = self._get_blend(blend_size)\n\n                left = self.result[\n                    :, self.offset - o.start : self.offset + o.start, ...\n                ]\n                right = tile[:, :blend_size, ...]\n\n                self.result[:, self.offset - o.start : self.offset + o.start, ...] = (\n                    _fast_mix(left, right, blend)\n                )\n\n                self.offset += w - o.total\n                self.last_end_overlap = o.end\n        else:\n            assert w == self.width\n            assert h > o.total\n\n            if self.offset == 0:\n                # the first tile is copied in as is\n                self.result[:h, :, ...] = tile\n\n                assert o.start == 0\n                self.offset += h - o.end\n                self.last_end_overlap = o.end\n\n            else:\n                assert self.offset < self.height, \"All tiles were filled in already\"\n\n                if self.last_end_overlap < o.start:\n                    # we can't use all the overlap of the current tile, so we have to cut it off\n                    diff = o.start - self.last_end_overlap\n                    tile = tile[diff:, :, ...]\n                    h, w, c = get_h_w_c(tile)\n                    o = TileOverlap(self.last_end_overlap, o.end)\n\n                # copy over the part that doesn't need blending\n                self.result[\n                    self.offset + o.start : self.offset + h - o.start, :, ...\n                ] = tile[o.start * 2 :, :, ...]\n\n                # blend the overlapping part\n                blend_size = o.start * 2\n                blend = self._get_blend(blend_size)\n\n                left = self.result[\n                    self.offset - o.start : self.offset + o.start, :, ...\n                ]\n                right = tile[: o.start * 2, :, ...]\n\n                self.result[self.offset - o.start : self.offset + o.start, :, ...] = (\n                    _fast_mix(left, right, blend)\n                )\n\n                self.offset += h - o.total\n                self.last_end_overlap = o.end\n\n    def get_result(self) -> np.ndarray:\n        if self.direction == BlendDirection.X:\n            assert self.offset == self.width\n        else:\n            assert self.offset == self.height\n\n        return self.result\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/impl/upscale/tiler.py",
    "content": "from abc import ABC, abstractmethod\n\nfrom ...utils.utils import Size\n\n\nclass Tiler(ABC):\n    @abstractmethod\n    def allow_smaller_tile_size(self) -> bool:\n        \"\"\"\n        Whether the split implementation may use tile sizes smaller than the ones returned by this tiler.\n\n        If False, then the split implementation guarantees that the all tiles are of exactly the size returned by this tiler.\n        If the image is smaller than the returned tile size, then it has to be padded to reach the given size.\n        \"\"\"\n\n    @abstractmethod\n    def starting_tile_size(self, width: int, height: int, channels: int) -> Size:\n        \"\"\"\n        The starting tile size is the first tile size that will be used.\n\n        We generally prefer square tile sizes, but any tile size may be used.\n        \"\"\"\n\n    def split(self, tile_size: Size) -> Size:\n        w, h = tile_size\n        assert w >= 16 and h >= 16\n        return max(16, w // 2), max(16, h // 2)\n\n\nclass NoTiling(Tiler):\n    def allow_smaller_tile_size(self) -> bool:\n        return True\n\n    def starting_tile_size(self, width: int, height: int, channels: int) -> Size:\n        size = max(width, height)\n        # we prefer square tiles\n        return size, size\n\n    def split(self, tile_size: Size) -> Size:\n        raise ValueError(\"Image cannot be upscale with No Tiling mode.\")\n\n\nclass MaxTileSize(Tiler):\n    def __init__(self, tile_size: int = 2**31) -> None:\n        self.tile_size: int = tile_size\n\n    def allow_smaller_tile_size(self) -> bool:\n        return True\n\n    def starting_tile_size(self, width: int, height: int, channels: int) -> Size:\n        # Tile size a lot larger than the image don't make sense.\n        # So we use the minimum of the image dimensions and the given tile size.\n        max_tile_size = max(width + 10, height + 10)\n        size = min(self.tile_size, max_tile_size)\n        return size, size\n\n\nclass ExactTileSize(Tiler):\n    def __init__(self, exact_size: Size) -> None:\n        self.exact_size = exact_size\n\n    def allow_smaller_tile_size(self) -> bool:\n        return False\n\n    def starting_tile_size(self, width: int, height: int, channels: int) -> Size:\n        return self.exact_size\n\n    def split(self, tile_size: Size) -> Size:\n        raise ValueError(\n            f\"Splits are not supported for exact size ({self.exact_size[0]}x{self.exact_size[1]}px) splitting.\"\n            f\" This typically means that your machine does not have enough VRAM to run the current model.\"\n        )\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/node_cache.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport hashlib\nimport os\nimport tempfile\nimport time\nfrom collections.abc import Iterable\nfrom enum import Enum\nfrom typing import NewType\n\nimport numpy as np\nfrom sanic.log import logger\n\nfrom api import RunFn\n\nCACHE_MAX_BYTES = int(os.environ.get(\"CACHE_MAX_BYTES\", 1024**3))  # default 1 GiB\nCACHE_REGISTRY: list[NodeOutputCache] = []\n\n\nclass CachedNumpyArray:\n    def __init__(self, arr: np.ndarray) -> None:\n        self.file = tempfile.TemporaryFile()\n        self.file.write(arr.tobytes())\n\n        self.shape = arr.shape\n        self.dtype = arr.dtype\n\n    def value(self) -> np.ndarray:\n        self.file.seek(0)\n        return np.frombuffer(self.file.read(), dtype=self.dtype).reshape(self.shape)\n\n\nCacheKey = NewType(\"CacheKey\", tuple)\n\n\nclass NodeOutputCache:\n    def __init__(self) -> None:\n        self._data: dict[CacheKey, list] = {}\n        self._bytes: dict[CacheKey, int] = {}\n        self._access_time: dict[CacheKey, float] = {}\n\n        CACHE_REGISTRY.append(self)\n\n    @staticmethod\n    def _args_to_key(args: Iterable[object]) -> CacheKey:\n        key = []\n        for arg in args:\n            if isinstance(arg, int | float | bool | str | bytes):\n                key.append(arg)\n            elif arg is None:\n                key.append(None)\n            elif isinstance(arg, Enum):\n                key.append(arg.value)\n            elif isinstance(arg, np.ndarray):\n                key.append(tuple(arg.shape))\n                key.append(arg.dtype.str)\n                key.append(hashlib.sha256(arg.tobytes()).digest())\n            elif hasattr(arg, \"cache_key_func\"):\n                key.append(arg.__class__.__name__)\n                key.append(arg.cache_key_func())  # type: ignore\n            else:\n                raise RuntimeError(f\"Unexpected argument type {arg.__class__.__name__}\")\n        return CacheKey(tuple(key))\n\n    @staticmethod\n    def _estimate_bytes(output: list[object]) -> int:\n        size = 0\n        for out in output:\n            if isinstance(out, np.ndarray):\n                size += out.nbytes\n            else:\n                # any other type but numpy arrays is probably negligible, but here's an overestimate to handle\n                # pathological cases where someone has a pipeline with a million math nodes\n                size += 1024  # 1 KiB\n        return size\n\n    def empty(self) -> bool:\n        return len(self._data) == 0\n\n    def oldest(self) -> tuple[CacheKey, float]:\n        return min(self._access_time.items(), key=lambda x: x[1])\n\n    def size(self):\n        return sum(self._bytes.values())\n\n    @staticmethod\n    def _enforce_limits() -> None:\n        while True:\n            total_bytes = sum([cache.size() for cache in CACHE_REGISTRY])\n            logger.debug(\n                f\"Cache size: {total_bytes} ({100*total_bytes/CACHE_MAX_BYTES:0.1f}% of limit)\"\n            )\n            if total_bytes <= CACHE_MAX_BYTES:\n                return\n            logger.debug(\"Dropping oldest cache key\")\n\n            oldest_keys = [\n                (cache, cache.oldest()) for cache in CACHE_REGISTRY if not cache.empty()\n            ]\n\n            cache, (key, _) = min(oldest_keys, key=lambda x: x[1][1])\n            cache.drop(key)\n\n    @staticmethod\n    def _write_arrays_to_disk(output: list) -> list:\n        return [\n            CachedNumpyArray(item) if isinstance(item, np.ndarray) else item\n            for item in output\n        ]\n\n    @staticmethod\n    def _read_arrays_from_disk(output: list) -> list:\n        return [\n            item.value() if isinstance(item, CachedNumpyArray) else item\n            for item in output\n        ]\n\n    @staticmethod\n    def _output_to_list(output: object) -> list[object]:\n        if isinstance(output, list):\n            return output\n        elif isinstance(output, tuple):\n            return list(output)\n        else:\n            return [output]\n\n    @staticmethod\n    def _list_to_output(output: list[object]):\n        if len(output) == 1:\n            return output[0]\n        return output\n\n    def get(self, args: Iterable[object]) -> object | None:\n        key = self._args_to_key(args)\n        if key in self._data:\n            logger.debug(\"Cache hit\")\n            self._access_time[key] = time.time()\n            return self._list_to_output(self._read_arrays_from_disk(self._data[key]))\n        logger.debug(\"Cache miss\")\n        return None\n\n    def put(self, args: Iterable[object], output: object) -> None:\n        key = self._args_to_key(args)\n        self._data[key] = self._write_arrays_to_disk(self._output_to_list(output))\n        self._bytes[key] = self._estimate_bytes(self._output_to_list(output))\n        self._access_time[key] = time.time()\n        self._enforce_limits()\n\n    def drop(self, key: CacheKey) -> None:\n        del self._data[key]\n        del self._bytes[key]\n        del self._access_time[key]\n\n\ndef cached(run: RunFn):\n    cache = NodeOutputCache()\n\n    @functools.wraps(run)\n    def _run(*args: object):\n        out = cache.get(args)\n        if out is not None:\n            return out\n        output = run(*args)\n        cache.put(args, output)\n        return output\n\n    return _run\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/__init__.py",
    "content": ""
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/__init__.py",
    "content": "from .file_inputs import *\nfrom .generic_inputs import *\nfrom .image_dropdown_inputs import *\nfrom .numeric_inputs import *\nfrom .numpy_inputs import *\n\ntry:\n    from .ncnn_inputs import *\nexcept Exception:\n    pass\ntry:\n    from .onnx_inputs import *\nexcept Exception:\n    pass\ntry:\n    from .pytorch_inputs import *\nexcept Exception:\n    pass\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/__system_inputs.py",
    "content": "from __future__ import annotations\n\nimport math\nfrom typing import Literal\n\nfrom navi import ExpressionJson\n\nfrom api import BaseInput\n\n\nclass StaticValueInput(BaseInput):\n    def __init__(\n        self,\n        label: str,\n        py_type: type = str,\n        navi_type: ExpressionJson = \"string\",\n        value: Literal[\"execution_number\"] = \"execution_number\",\n    ) -> None:\n        super().__init__(navi_type, label, kind=\"static\", has_handle=False)\n\n        self.associated_type = py_type\n        self.value = value\n\n    def to_dict(self):\n        return {\n            **super().to_dict(),\n            \"value\": self.value,\n        }\n\n    def enforce(self, value: object):\n        return_value = value\n        if not isinstance(value, self.associated_type):\n            return_value = self.associated_type(value)\n\n        if isinstance(value, float | int) and math.isnan(value):\n            raise ValueError(\"NaN is not a valid number\")\n\n        return return_value\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/file_inputs.py",
    "content": "from __future__ import annotations\n\nimport re\nfrom pathlib import Path\nfrom typing import Literal, Union\n\nimport navi\n\nfrom api import BaseInput\n\n# pylint: disable=relative-beyond-top-level\nfrom ...impl.image_formats import get_available_image_formats\nfrom .generic_inputs import TextInput\nfrom .label import LabelStyle\n\nFileInputKind = Union[\n    Literal[\"bin\"],\n    Literal[\"image\"],\n    Literal[\"onnx\"],\n    Literal[\"param\"],\n    Literal[\"pt\"],\n    Literal[\"pth\"],\n    Literal[\"video\"],\n]\n\n\nclass FileInput(BaseInput):\n    \"\"\"Input for submitting a local file\"\"\"\n\n    def __init__(\n        self,\n        label: str,\n        file_kind: FileInputKind,\n        filetypes: list[str],\n        has_handle: bool = False,\n        primary_input: bool = False,\n    ) -> None:\n        super().__init__(\n            navi.named(\"File\", {\"kind\": navi.literal(file_kind)}),\n            label,\n            kind=\"file\",\n            has_handle=has_handle,\n        )\n        self.filetypes = filetypes\n        self.file_kind = file_kind\n        self.primary_input = primary_input\n\n        self.input_adapt = \"\"\"\n            match Input {\n                string as path => File { path },\n                _ => never\n            }\n        \"\"\"\n\n        self.associated_type = Path\n\n    def to_dict(self):\n        return {\n            **super().to_dict(),\n            \"filetypes\": self.filetypes,\n            \"fileKind\": self.file_kind,\n            \"primaryInput\": self.primary_input,\n        }\n\n    def enforce(self, value: object) -> Path:\n        if isinstance(value, str):\n            value = Path(value)\n        assert isinstance(value, Path)\n        assert value.exists(), f\"File {value} does not exist\"\n        assert value.is_file(), f\"The path {value} is not a file\"\n        return value\n\n\ndef ImageFileInput(primary_input: bool = False) -> FileInput:\n    \"\"\"Input for submitting a local image file\"\"\"\n    return FileInput(\n        label=\"Image File\",\n        file_kind=\"image\",\n        filetypes=get_available_image_formats(),\n        has_handle=False,\n        primary_input=primary_input,\n    )\n\n\ndef VideoFileInput(primary_input: bool = False) -> FileInput:\n    \"\"\"Input for submitting a local video file\"\"\"\n    return FileInput(\n        label=\"Video File\",\n        file_kind=\"video\",\n        filetypes=[\n            \".mp4\",\n            \".h264\",\n            \".hevc\",\n            \".webm\",\n            \".avi\",\n            \".gif\",\n            \".mov\",\n            \".mkv\",\n            \".flv\",\n            \".m4v\",\n            \".avs\",\n        ],\n        has_handle=False,\n        primary_input=primary_input,\n    )\n\n\ndef PthFileInput(primary_input: bool = False) -> FileInput:\n    \"\"\"Input for submitting a local .pth file\"\"\"\n    return FileInput(\n        label=\"Model\",\n        file_kind=\"pth\",\n        filetypes=[\".pt\", \".pth\", \".ckpt\", \".safetensors\"],\n        primary_input=primary_input,\n    )\n\n\nclass DirectoryInput(BaseInput[Path]):\n    \"\"\"Input for submitting a local directory\"\"\"\n\n    def __init__(\n        self,\n        label: str = \"Directory\",\n        has_handle: bool = True,\n        must_exist: bool = True,\n        label_style: LabelStyle = \"default\",\n    ) -> None:\n        super().__init__(\"Directory\", label, kind=\"directory\", has_handle=has_handle)\n\n        self.input_adapt = \"\"\"\n            match Input {\n                string as path => Directory { path },\n                _ => never\n            }\n        \"\"\"\n\n        self.must_exist: bool = must_exist\n        self.label_style: LabelStyle = label_style\n\n        self.associated_type = Path\n\n    def to_dict(self):\n        return {\n            **super().to_dict(),\n            \"labelStyle\": self.label_style,\n        }\n\n    def enforce(self, value: object) -> Path:\n        if isinstance(value, str):\n            value = Path(value)\n        assert isinstance(value, Path)\n\n        if self.must_exist:\n            assert value.exists(), f\"Directory {value} does not exist\"\n\n        return value\n\n\ndef BinFileInput(primary_input: bool = False) -> FileInput:\n    \"\"\"Input for submitting a local .bin file\"\"\"\n    return FileInput(\n        label=\"NCNN Bin File\",\n        file_kind=\"bin\",\n        filetypes=[\".bin\"],\n        primary_input=primary_input,\n    )\n\n\ndef ParamFileInput(primary_input: bool = False) -> FileInput:\n    \"\"\"Input for submitting a local .param file\"\"\"\n    return FileInput(\n        label=\"NCNN Param File\",\n        file_kind=\"param\",\n        filetypes=[\".param\"],\n        primary_input=primary_input,\n    )\n\n\ndef OnnxFileInput(primary_input: bool = False) -> FileInput:\n    \"\"\"Input for submitting a local .onnx file\"\"\"\n    return FileInput(\n        label=\"ONNX Model File\",\n        file_kind=\"onnx\",\n        filetypes=[\".onnx\"],\n        primary_input=primary_input,\n    )\n\n\n_INVALID_PATH_CHARS = re.compile(r'[<>:\"|?*\\x00-\\x1F]')\n\n\ndef _is_abs_path(path: str) -> bool:\n    return path.startswith((\"/\", \"\\\\\")) or Path(path).is_absolute()\n\n\nclass RelativePathInput(TextInput):\n    def __init__(\n        self,\n        label: str,\n        has_handle: bool = True,\n        placeholder: str | None = None,\n        allow_numbers: bool = True,\n        default: str | None = None,\n        label_style: LabelStyle = \"default\",\n    ) -> None:\n        super().__init__(\n            label,\n            has_handle=has_handle,\n            min_length=1,\n            max_length=None,\n            placeholder=placeholder,\n            multiline=False,\n            allow_numbers=allow_numbers,\n            default=default,\n            label_style=label_style,\n            allow_empty_string=False,\n            invalid_pattern=_INVALID_PATH_CHARS.pattern,\n        )\n\n    def enforce(self, value: object) -> str:\n        value = super().enforce(value)\n\n        if _is_abs_path(value):\n            raise ValueError(f\"Absolute paths are not allowed for input {self.label}.\")\n\n        invalid = _INVALID_PATH_CHARS.search(value)\n        if invalid is not None:\n            raise ValueError(f\"Invalid character '{invalid.group()}' in {self.label}.\")\n\n        return value\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/generic_inputs.py",
    "content": "from __future__ import annotations\n\nimport json\nimport re\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom typing import Any, Literal, Never, NotRequired, TypedDict, TypeVar\n\nimport navi\nimport numpy as np\nfrom sanic.log import logger\n\nfrom api import BaseInput, InputConversion, group\n\nfrom ...condition import Condition, ConditionJson\nfrom ...impl.blend import BlendMode\nfrom ...impl.color.color import Color\nfrom ...impl.image_utils import FillColor\nfrom ...impl.upscale.auto_split_tiles import (\n    CUSTOM,\n    ESTIMATE,\n    MAX_TILE_SIZE,\n    NO_TILING,\n    TileSize,\n)\nfrom ...utils.format import format_color_with_channels\nfrom ...utils.seed import Seed\nfrom ...utils.utils import (\n    join_pascal_case,\n    join_space_case,\n    split_pascal_case,\n    split_snake_case,\n)\nfrom .label import LabelStyle\nfrom .numeric_inputs import NumberInput\n\n\nclass DropDownOption(TypedDict):\n    option: str\n    icon: NotRequired[str | None]\n    value: str | int\n    type: NotRequired[navi.ExpressionJson]\n    condition: NotRequired[ConditionJson | None]\n\n\nDropDownStyle = Literal[\"dropdown\", \"checkbox\", \"tabs\", \"icons\", \"anchor\"]\n\"\"\"\nThis specified the preferred style in which the frontend may display the dropdown.\n\n- `dropdown`: This is the default style. The dropdown will simply be displayed as a dropdown.\n- `checkbox`: If the dropdown has 2 options, then it will be displayed as a checkbox.\n  The first option will be interpreted as the yes/true option while the second option will be interpreted as the no/false option.\n- `tabs`: The options are displayed as tab list. The label of the input itself will *not* be displayed.\n- `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.\n- `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.\n\"\"\"\n\n\n@dataclass\nclass DropDownGroup:\n    label: str | None\n    start_at: str | int | Enum\n\n    @staticmethod\n    def divider(start_at: str | int | Enum):\n        return DropDownGroup(None, start_at)\n\n    def to_dict(self):\n        start_at = self.start_at\n        if isinstance(start_at, Enum):\n            start_at = start_at.value\n        return {\"label\": self.label, \"startAt\": start_at}\n\n\nT = TypeVar(\"T\")\n\n\nclass DropDownInput(BaseInput[T]):\n    \"\"\"Input for a dropdown\"\"\"\n\n    def __init__(\n        self,\n        input_type: navi.ExpressionJson,\n        label: str,\n        options: list[DropDownOption],\n        default_value: str | int | None = None,\n        preferred_style: DropDownStyle = \"dropdown\",\n        label_style: LabelStyle = \"default\",\n        groups: list[DropDownGroup] | None = None,\n        associated_type: Any = None,\n    ) -> None:\n        super().__init__(input_type, label, kind=\"dropdown\", has_handle=False)\n        self.options = options\n        self.accepted_values = {o[\"value\"] for o in self.options}\n        self.default = (\n            default_value if default_value is not None else options[0][\"value\"]\n        )\n        self.preferred_style: DropDownStyle = preferred_style\n        self.label_style: LabelStyle = label_style\n        self.groups: list[DropDownGroup] = groups or []\n\n        if self.default not in self.accepted_values:\n            logger.error(\n                f\"Invalid default value {self.default} in {label} dropdown. Using first value instead.\"\n            )\n            self.default = options[0][\"value\"]\n\n        self.associated_type = (\n            associated_type if associated_type is not None else type(self.default)\n        )\n\n    def to_dict(self):\n        return {\n            **super().to_dict(),\n            \"options\": self.options,\n            \"def\": self.default,\n            \"preferredStyle\": self.preferred_style,\n            \"labelStyle\": self.label_style,\n            \"groups\": [c.to_dict() for c in self.groups],\n        }\n\n    def make_optional(self) -> Never:\n        raise ValueError(\"DropDownInput cannot be made optional\")\n\n    def enforce(self, value: object) -> T:\n        assert value in self.accepted_values, f\"{value} is not a valid option\"\n        return value  # type: ignore\n\n    def wrap_with_conditional_group(self):\n        \"\"\"\n        Adds a conditional group around the dropdown input according to the conditions of its options.\n\n        Note: Calling this method is only valid if all options have a condition.\n        \"\"\"\n\n        conditions: list[ConditionJson] = []\n        for option in self.options:\n            c = option.get(\"condition\")\n            if c is None:\n                raise ValueError(\n                    f\"wrap_with_conditional is unnecessary, because the {option['option']} option has no condition.\"\n                )\n            conditions.append(c)\n\n        condition: ConditionJson = {\"kind\": \"or\", \"items\": conditions}\n\n        return group(\"conditional\", {\"condition\": condition})(self)\n\n\nclass _BoolEnumInput(DropDownInput[bool]):\n    def __init__(\n        self, label: str, *, default: bool = True, icon: str | None = None\n    ) -> None:\n        super().__init__(\n            input_type=\"bool\",\n            label=label,\n            default_value=int(default),\n            options=[\n                {\n                    \"option\": \"Yes\",\n                    \"value\": int(True),  # 1\n                    \"type\": \"true\",\n                    \"icon\": icon,\n                },\n                {\n                    \"option\": \"No\",\n                    \"value\": int(False),  # 0\n                    \"type\": \"false\",\n                },\n            ],\n            preferred_style=\"checkbox\",\n        )\n        self.associated_type = bool\n\n    def enforce(self, value: object) -> bool:\n        value = super().enforce(value)\n        return bool(value)\n\n\nclass _BoolGenericInput(BaseInput[bool]):\n    def __init__(self, label: str) -> None:\n        super().__init__(input_type=\"bool\", label=label)\n        self.associated_type = bool\n\n    def enforce(self, value: object) -> bool:\n        if isinstance(value, bool):\n            return value\n        if isinstance(value, int):\n            return bool(value)\n\n        raise ValueError(\n            f\"The value of input '{self.label}' should have been either True or False.\"\n        )\n\n\ndef BoolInput(\n    label: str,\n    *,\n    default: bool = True,\n    icon: str | None = None,\n    has_handle: bool = False,\n):\n    if has_handle:\n        return _BoolGenericInput(label)\n    return _BoolEnumInput(label, default=default, icon=icon)\n\n\nE = TypeVar(\"E\", bound=Enum)\n\n\nclass EnumInput(DropDownInput[E]):\n    \"\"\"\n    This adapts a python Enum into a chaiNNer dropdown input.\n\n    ### Features\n\n    All variants of the enum will be converted into typed dropdown options.\n    The dropdown will be fully typed and bring its own type definitions.\n    Option labels can be (partially) overridden using `option_labels`.\n\n    By default, the input label, type names, and option labels will all be generated from the enum name and variant names.\n    All of those defaults can be overridden.\n\n    Options will be ordered by declaration order in the python enum definition.\n\n    ### Requirements\n\n    The value of each variant has to be either `str` or `int`.\n    Other types are not permitted.\n    \"\"\"\n\n    def __init__(\n        self,\n        enum: type[E],\n        label: str | None = None,\n        *,\n        default: E | None = None,\n        type_name: str | None = None,\n        option_labels: dict[E, str] | None = None,\n        extra_definitions: str | None = None,\n        preferred_style: DropDownStyle = \"dropdown\",\n        label_style: LabelStyle = \"default\",\n        categories: list[DropDownGroup] | None = None,\n        conditions: dict[E, Condition] | None = None,\n        icons: dict[E, str] | None = None,\n    ) -> None:\n        if type_name is None:\n            type_name = enum.__name__\n        if label is None:\n            label = join_space_case(split_pascal_case(type_name))\n        if option_labels is None:\n            option_labels = {}\n        if conditions is None:\n            conditions = {}\n        if icons is None:\n            icons = {}\n\n        options: list[DropDownOption] = []\n        variant_types: list[str] = []\n        for variant in enum:\n            value = variant.value\n            assert isinstance(value, int | str)\n\n            variant_type = EnumInput.get_variant_type(variant, type_name)\n            option_label = option_labels.get(\n                variant, join_space_case(split_snake_case(variant.name))\n            )\n            condition = conditions.get(variant)\n            if condition is not None:\n                condition = condition.to_json()\n\n            variant_types.append(variant_type)\n\n            options.append(\n                {\n                    \"option\": option_label,\n                    \"value\": value,\n                    \"type\": variant_type,\n                    \"condition\": condition,\n                    \"icon\": icons.get(variant),\n                }\n            )\n\n        super().__init__(\n            input_type=type_name,\n            label=label,\n            options=options,\n            default_value=default.value if default is not None else None,\n            preferred_style=preferred_style,\n            label_style=label_style,\n            groups=categories,\n        )\n\n        self.type_definitions = (\n            f\"let {type_name} = {' | '.join(variant_types)};\\n\"\n            + \"\\n\".join([f\"struct {t};\" for t in variant_types])\n            + (extra_definitions or \"\")\n        )\n        self.type_name: str = type_name\n        self.enum = enum\n\n        self.associated_type = enum\n\n    @staticmethod\n    def get_variant_type(variant: Enum, type_name: str | None = None) -> str:\n        \"\"\"\n        Returns the full type name of a variant of an enum.\n        \"\"\"\n\n        enum = variant.__class__\n        if type_name is None:\n            type_name = enum.__name__\n\n        assert (\n            re.match(r\"^[a-zA-Z_][a-zA-Z0-9_]*$\", variant.name) is not None\n        ), f\"Expected the name of {enum.__name__}.{variant.name} to be snake case.\"\n\n        return f\"{type_name}::{join_pascal_case(split_snake_case(variant.name))}\"\n\n    def enforce(self, value: object) -> E:\n        value = super().enforce(value)\n        return self.enum(value)\n\n\nclass TextInput(BaseInput[str]):\n    \"\"\"Input for arbitrary text\"\"\"\n\n    def __init__(\n        self,\n        label: str,\n        *,\n        has_handle: bool = True,\n        min_length: int = 0,\n        max_length: int | None = None,\n        placeholder: str | None = None,\n        multiline: bool = False,\n        allow_numbers: bool = True,\n        default: str | None = None,\n        label_style: LabelStyle = \"default\",\n        allow_empty_string: bool = False,\n        invalid_pattern: str | None = None,\n    ) -> None:\n        super().__init__(\n            input_type=\"string\" if min_length == 0 else 'invStrSet(\"\")',\n            label=label,\n            has_handle=has_handle,\n            kind=\"text\",\n        )\n        self.min_length = min_length\n        self.max_length = max_length\n        self.placeholder = placeholder\n        self.default = default\n        self.multiline = multiline\n        self.label_style: LabelStyle = label_style\n        self.allow_empty_string = allow_empty_string\n        self.invalid_pattern = invalid_pattern\n\n        if default is not None:\n            assert default != \"\" or allow_empty_string\n            assert min_length <= len(default)\n            assert max_length is None or len(default) < max_length\n\n        self.associated_type = str\n\n        if allow_numbers:\n            self.input_conversions = [InputConversion(\"number\", \"toString(Input)\")]\n\n    def enforce(self, value: object) -> str:\n        if isinstance(value, float) and int(value) == value:\n            # stringify integers values\n            value = str(int(value))\n        else:\n            value = str(value)\n\n        # enforce length range\n        if self.max_length is not None and len(value) > self.max_length:\n            value = value[: self.max_length]\n        if len(value) < self.min_length:\n            raise ValueError(\n                f\"Text value of input '{self.label}' must be at least {self.min_length} characters long,\"\n                f\" but found {len(value)} ('{value}').\"\n            )\n\n        return value\n\n    def to_dict(self):\n        return {\n            **super().to_dict(),\n            \"minLength\": self.min_length,\n            \"maxLength\": self.max_length,\n            \"placeholder\": self.placeholder,\n            \"multiline\": self.multiline,\n            \"def\": self.default,\n            \"labelStyle\": self.label_style,\n            \"allowEmptyString\": self.allow_empty_string,\n            \"invalidPattern\": self.invalid_pattern,\n        }\n\n\nclass ClipboardInput(BaseInput):\n    \"\"\"Input for pasting from clipboard\"\"\"\n\n    def __init__(self, label: str = \"Clipboard input\") -> None:\n        super().__init__([\"Image\", \"string\", \"number\"], label, kind=\"text\")\n        self.input_conversions = [InputConversion(\"Image\", '\"<Image>\"')]\n\n        self.label_style: LabelStyle = \"hidden\"\n\n    def enforce(self, value: object):\n        if isinstance(value, np.ndarray):\n            return value\n\n        if isinstance(value, float) and int(value) == value:\n            # stringify integers values\n            return str(int(value))\n\n        return str(value)\n\n    def to_dict(self):\n        return {\n            **super().to_dict(),\n            \"labelStyle\": self.label_style,\n        }\n\n\nclass AnyInput(BaseInput[object]):\n    def __init__(self, label: str) -> None:\n        super().__init__(input_type=\"any\", label=label)\n        self.associated_type = object\n\n    def enforce_(self, value: object):\n        # The behavior for optional inputs and None makes sense for all inputs except this one.\n        return value\n\n\nclass SeedInput(NumberInput):\n    def __init__(self, label: str = \"Seed\", *, has_handle: bool = True) -> None:\n        super().__init__(\n            label=label,\n            min=None,\n            max=None,\n            precision=0,\n            default=0,\n            label_style=\"default\",\n        )\n        self.has_handle = has_handle\n\n        self.input_type = \"Seed | int\"\n        self.input_conversions = [InputConversion(\"int\", \"Seed\")]\n        self.input_adapt = \"\"\"\n            match Input {\n                int => Seed,\n                _ => never\n            }\n        \"\"\"\n\n        self.associated_type = Seed\n\n    def enforce(self, value: object) -> Seed:  # type: ignore\n        if isinstance(value, Seed):\n            return value\n        if isinstance(value, int | float | str):\n            return Seed(int(value))\n        raise ValueError(f\"Cannot convert {value} to Seed\")\n\n    def make_optional(self) -> Never:\n        raise ValueError(\"SeedInput cannot be made optional\")\n\n\nclass ColorInput(BaseInput[Color]):\n    def __init__(\n        self,\n        label: str = \"Color\",\n        *,\n        default: Color | None = None,\n        channels: int | list[int] | None = None,\n    ) -> None:\n        super().__init__(\n            input_type=navi.Color(channels=channels),\n            label=label,\n            has_handle=True,\n            kind=\"color\",\n        )\n\n        self.input_adapt = \"\"\"\n            match Input {\n                string => parseColorJson(Input),\n                _ => never\n            }\n        \"\"\"\n\n        self.channels: list[int] | None = (\n            [channels] if isinstance(channels, int) else channels\n        )\n\n        if self.channels is None:\n            if default is None:\n                default = Color.bgr((0.5, 0.5, 0.5))\n        else:\n            assert len(self.channels) >= 0\n            if default is None:\n                if 3 in self.channels:\n                    default = Color.bgr((0.5, 0.5, 0.5))\n                elif 4 in self.channels:\n                    default = Color.bgra((0.5, 0.5, 0.5, 1))\n                elif 1 in self.channels:\n                    default = Color.gray(0.5)\n                else:\n                    raise ValueError(\"Cannot find default color value\")\n            else:\n                assert (\n                    default.channels in self.channels\n                ), \"The default color is not accepted.\"\n\n        self.default: Color = default\n\n        self.associated_type = Color\n\n    def enforce(self, value: object) -> Color:\n        if isinstance(value, str):\n            # decode color JSON strings from the frontend\n            value = Color.from_json(json.loads(value))\n\n        assert isinstance(value, Color)\n\n        if self.channels is not None and value.channels not in self.channels:\n            expected = format_color_with_channels(self.channels, plural=True)\n            actual = format_color_with_channels([value.channels])\n            raise ValueError(\n                f\"The input {self.label} only supports {expected} but was given {actual}.\"\n            )\n\n        return value\n\n    def to_dict(self):\n        return {\n            **super().to_dict(),\n            \"def\": json.dumps(self.default.to_json()),\n            \"channels\": self.channels,\n        }\n\n    def make_optional(self) -> Never:\n        raise ValueError(\"ColorInput cannot be made optional\")\n\n\ndef BlendModeDropdown() -> DropDownInput:\n    \"\"\"Blending Mode option dropdown\"\"\"\n    return EnumInput(\n        BlendMode,\n        option_labels={\n            BlendMode.ADD: \"Linear Dodge (Add)\",\n        },\n        categories=[\n            DropDownGroup.divider(start_at=BlendMode.DARKEN),\n            DropDownGroup.divider(start_at=BlendMode.LIGHTEN),\n            DropDownGroup.divider(start_at=BlendMode.OVERLAY),\n            DropDownGroup.divider(start_at=BlendMode.DIFFERENCE),\n        ],\n    )\n\n\ndef FillColorDropdown() -> DropDownInput:\n    return EnumInput(\n        FillColor,\n        label=\"Negative Space Fill\",\n        default=FillColor.AUTO,\n        extra_definitions=\"\"\"\n            def FillColor::getOutputChannels(fill: FillColor, channels: uint) {\n                match fill {\n                    FillColor::Transparent => 4,\n                    _ => channels\n                }\n            }\n        \"\"\",\n    )\n\n\ndef TileSizeDropdown(\n    label: str = \"Tile Size\", *, estimate: bool = True, default: TileSize | None = None\n) -> DropDownInput:\n    options = []\n    if estimate:\n        options.append({\"option\": \"Auto (estimate)\", \"value\": ESTIMATE})\n\n    options.append({\"option\": \"Maximum\", \"value\": MAX_TILE_SIZE})\n    options.append({\"option\": \"No Tiling\", \"value\": NO_TILING})\n\n    for size in [128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096]:\n        options.append({\"option\": str(size), \"value\": size})\n\n    options.append({\"option\": \"Custom\", \"value\": CUSTOM})\n\n    return DropDownInput(\n        input_type=\"TileSize\",\n        label=label,\n        options=options,\n        associated_type=TileSize,\n        default_value=default,\n    )\n\n\nclass AudioStreamInput(BaseInput):\n    def __init__(self, label: str = \"Audio Stream\") -> None:\n        super().__init__(\"AudioStream\", label, kind=\"generic\")\n\n\nclass OrderEnum(Enum):\n    ROW_MAJOR = 0\n    COLUMN_MAJOR = 1\n\n\ndef RowOrderDropdown() -> DropDownInput:\n    return EnumInput(\n        OrderEnum,\n        label=\"Order\",\n        default=OrderEnum.ROW_MAJOR,\n    )\n\n\nclass Anchor(Enum):\n    TOP_LEFT = \"top_left\"\n    TOP = \"top_centered\"\n    TOP_RIGHT = \"top_right\"\n    LEFT = \"centered_left\"\n    CENTER = \"centered\"\n    RIGHT = \"centered_right\"\n    BOTTOM_LEFT = \"bottom_left\"\n    BOTTOM = \"bottom_centered\"\n    BOTTOM_RIGHT = \"bottom_right\"\n\n\ndef AnchorInput(label: str = \"Anchor\", icon: str = \"BsFillImageFill\") -> DropDownInput:\n    return EnumInput(\n        Anchor,\n        label=label,\n        label_style=\"inline\",\n        option_labels={\n            Anchor.TOP_LEFT: \"Top Left\",\n            Anchor.TOP: \"Top\",\n            Anchor.TOP_RIGHT: \"Top Right\",\n            Anchor.LEFT: \"Left\",\n            Anchor.CENTER: \"Center\",\n            Anchor.RIGHT: \"Right\",\n            Anchor.BOTTOM_LEFT: \"Bottom Left\",\n            Anchor.BOTTOM: \"Bottom\",\n            Anchor.BOTTOM_RIGHT: \"Bottom Right\",\n        },\n        icons={\n            Anchor.TOP_LEFT: icon,\n            Anchor.TOP: icon,\n            Anchor.TOP_RIGHT: icon,\n            Anchor.LEFT: icon,\n            Anchor.CENTER: icon,\n            Anchor.RIGHT: icon,\n            Anchor.BOTTOM_LEFT: icon,\n            Anchor.BOTTOM: icon,\n            Anchor.BOTTOM_RIGHT: icon,\n        },\n        preferred_style=\"anchor\",\n        default=Anchor.CENTER,\n    )\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/image_dropdown_inputs.py",
    "content": "import navi\n\nfrom ...impl.color.convert_data import (\n    color_spaces,\n    color_spaces_or_detectors,\n    get_alpha_partner,\n    is_alpha_partner,\n)\n\n# pylint: disable=relative-beyond-top-level\nfrom ...impl.image_utils import BorderType\nfrom ...impl.pil_utils import RotationInterpolationMethod\nfrom ...impl.resize import ResizeFilter\nfrom .generic_inputs import DropDownGroup, DropDownInput, EnumInput\n\n\ndef ColorSpaceDetectorInput(label: str = \"Color Space\") -> DropDownInput:\n    return DropDownInput(\n        input_type=\"ColorSpace\",\n        label=label,\n        options=[\n            {\n                \"option\": c.name,\n                \"value\": c.id,\n                \"type\": navi.named(\"ColorSpace\", {\"channels\": c.channels}),\n            }\n            for c in color_spaces_or_detectors\n        ],\n    )\n\n\ndef ColorSpaceInput(label: str = \"Color Space\") -> DropDownInput:\n    return DropDownInput(\n        input_type=\"ColorSpace\",\n        label=label,\n        options=[\n            {\n                \"option\": c.name,\n                \"value\": c.id,\n                \"type\": navi.named(\n                    \"ColorSpace\",\n                    {\n                        \"channels\": c.channels,\n                        \"supportsAlpha\": get_alpha_partner(c) is not None,\n                    },\n                ),\n            }\n            for c in color_spaces\n            if not is_alpha_partner(c)\n        ],\n    )\n\n\ndef ResizeFilterInput() -> DropDownInput:\n    return EnumInput(\n        ResizeFilter,\n        label=\"Interpolation Method\",\n        categories=[\n            DropDownGroup(\"Basic\", start_at=ResizeFilter.AUTO),\n            DropDownGroup(\"Advanced\", start_at=ResizeFilter.HERMITE),\n        ],\n        option_labels={\n            ResizeFilter.NEAREST: \"Nearest Neighbor\",\n            ResizeFilter.BOX: \"Area (Box)\",\n            ResizeFilter.CATROM: \"Cubic\",\n            ResizeFilter.BSPLINE: \"B-Spline\",\n        },\n    )\n\n\ndef RotateInterpolationInput() -> DropDownInput:\n    return EnumInput(\n        RotationInterpolationMethod,\n        label=\"Interpolation Method\",\n        option_labels={\n            RotationInterpolationMethod.NEAREST: \"Nearest Neighbor\",\n        },\n    )\n\n\ndef BorderInput() -> DropDownInput:\n    return EnumInput(\n        BorderType,\n        default=BorderType.REFLECT_MIRROR,\n        option_labels={\n            BorderType.REFLECT_MIRROR: \"Reflect (Mirror)\",\n            BorderType.WRAP: \"Wrap (Tile)\",\n            BorderType.REPLICATE: \"Replicate Edges\",\n        },\n        extra_definitions=\"\"\"\n            def BorderType::getOutputChannels(type: BorderType, channels: uint, color: Color | null): uint {\n                match type {\n                    BorderType::Transparent => 4,\n                    BorderType::CustomColor => match color {\n                        Color => max(color.channels, channels),\n                        null => never,\n                    },\n                    _ => channels\n                }\n            }\n        \"\"\",\n    )\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/label.py",
    "content": "from typing import Literal\n\nLabelStyle = Literal[\"default\", \"hidden\", \"inline\"]\n\n\ndef get_default_label_style(label: str) -> LabelStyle:\n    return \"inline\" if len(label) <= 8 else \"default\"\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/ncnn_inputs.py",
    "content": "from api import BaseInput\n\nfrom ...impl.ncnn.model import NcnnModelWrapper\n\n\nclass NcnnModelInput(BaseInput):\n    \"\"\"Input for NcnnModel\"\"\"\n\n    def __init__(self, label: str = \"Model\") -> None:\n        super().__init__(\"NcnnNetwork\", label)\n        self.associated_type = NcnnModelWrapper\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/numeric_inputs.py",
    "content": "from __future__ import annotations\n\nimport math\nfrom typing import Literal, Never, Union\n\nimport navi\n\nfrom api import BaseInput, InputConversion, InputKind\n\nfrom ...utils.utils import round_half_up\nfrom .label import LabelStyle, get_default_label_style\n\nPrecision = Union[int, Literal[\"unlimited\"]]\n\n\ndef _get_step(precision: Precision) -> float:\n    if precision == \"unlimited\":\n        return 1\n    return 10**-precision\n\n\ndef _is_int(precision: Precision) -> bool:\n    return precision == 0\n\n\ndef clamp_number(\n    value: float | int,\n    precision: Precision,\n    min_value: float | int | None,\n    max_value: float | int | None,\n) -> float | int:\n    # Convert proper number type\n    if precision != \"unlimited\":\n        value = round_half_up(value) if precision == 0 else round(value, precision)\n\n    # Clamp to max and min, correcting for max/min not aligning with offset + n * step\n    if max_value is not None:\n        value = min(value, max_value)\n    if min_value is not None:\n        value = max(value, min_value)\n\n    # guarantee integers\n    if _is_int(precision):\n        return int(value)\n    else:\n        return float(value)\n\n\ndef get_number_type(\n    min_value: float | int | None,\n    max_value: float | int | None,\n    precision: Precision,\n) -> navi.ExpressionJson:\n    if not _is_int(precision):\n        # step is not an integer\n        return navi.interval(min_value, max_value)\n    return navi.int_interval(min_value, max_value)\n\n\nclass NumberInput(BaseInput):\n    \"\"\"Input a number\"\"\"\n\n    def __init__(\n        self,\n        label: str,\n        *,\n        precision: Precision = 0,\n        step: float | int | None = None,\n        default: float | int = 0,\n        min: float | int | None = 0,\n        max: float | int | None = None,\n        unit: str | None = None,\n        note_expression: str | None = None,\n        kind: InputKind = \"number\",\n        hide_trailing_zeros: bool = True,\n        label_style: LabelStyle | None = None,\n        has_handle: bool = True,\n    ) -> None:\n        super().__init__(\"number\", label, kind=kind, has_handle=has_handle)\n        self.precision: int | Literal[\"unlimited\"] = precision\n        # controls_step is for increment/decrement arrows.\n        self.step: float | int = step if step is not None else _get_step(precision)\n        self.default = default\n        self.min = min\n        self.max = max\n        self.unit = unit\n        self.note_expression = note_expression\n        self.hide_trailing_zeros = hide_trailing_zeros\n        self.label_style: LabelStyle = label_style or get_default_label_style(label)\n\n        self.associated_type = float if not _is_int(precision) else int\n\n        self.input_type = get_number_type(self.min, self.max, self.precision)\n        if self.precision == 0:\n            self.input_conversions = [InputConversion(\"number\", \"round(Input)\")]\n\n    def to_dict(self):\n        return {\n            **super().to_dict(),\n            \"min\": self.min,\n            \"max\": self.max,\n            \"noteExpression\": self.note_expression,\n            \"def\": self.default,\n            \"precision\": 100 if self.precision == \"unlimited\" else self.precision,\n            \"controlsStep\": self.step,\n            \"unit\": self.unit,\n            \"hideTrailingZeros\": self.hide_trailing_zeros,\n            \"labelStyle\": self.label_style,\n            \"hasHandle\": self.has_handle,\n        }\n\n    def make_optional(self) -> Never:\n        raise ValueError(\"NumberInput and SliderInput cannot be made optional\")\n\n    def enforce(self, value: object):\n        assert isinstance(value, int | float)\n\n        if math.isnan(value):\n            raise ValueError(\"NaN is not a valid number\")\n\n        return clamp_number(value, self.precision, self.min, self.max)\n\n\nclass SliderInput(NumberInput):\n    \"\"\"Input for integer number via slider\"\"\"\n\n    def __init__(\n        self,\n        label: str,\n        *,\n        precision: Precision = 0,\n        step: float | int | None = None,\n        slider_step: float | int | None = None,\n        min: float | int = 0,\n        max: float | int = 100,\n        default: float | int = 50,\n        unit: str | None = None,\n        note_expression: str | None = None,\n        ends: tuple[str | None, str | None] = (None, None),\n        hide_trailing_zeros: bool = False,\n        gradient: list[str] | None = None,\n        scale: Literal[\"linear\", \"log\", \"log-offset\", \"sqrt\"] = \"linear\",\n        has_handle: bool = True,\n    ) -> None:\n        super().__init__(\n            label,\n            precision=precision,\n            step=step,\n            default=default,\n            min=min,\n            max=max,\n            unit=unit,\n            note_expression=note_expression,\n            kind=\"slider\",\n            hide_trailing_zeros=hide_trailing_zeros,\n            has_handle=has_handle,\n        )\n        self.ends = ends\n        self.slider_step = (\n            slider_step\n            if slider_step is not None\n            else (step if step is not None else _get_step(precision))\n        )\n        self.gradient = gradient\n        self.scale = scale\n\n    def to_dict(self):\n        return {\n            **super().to_dict(),\n            \"ends\": self.ends,\n            \"sliderStep\": self.slider_step,\n            \"gradient\": self.gradient,\n            \"scale\": self.scale,\n        }\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/numpy_inputs.py",
    "content": "# pylint: disable=relative-beyond-top-level\nfrom __future__ import annotations\n\nfrom typing import Union\n\nimport navi\nimport numpy as np\n\nfrom api import BaseInput, ErrorValue\n\nfrom ...impl.color.color import Color\nfrom ...utils.format import format_color_with_channels, format_image_with_channels\nfrom ...utils.utils import get_h_w_c\n\n\nclass AudioInput(BaseInput):\n    \"\"\"Input a 1D Audio NumPy array\"\"\"\n\n    def __init__(self, label: str = \"Audio\") -> None:\n        super().__init__(\"Audio\", label)\n\n\nclass ImageInput(BaseInput):\n    \"\"\"Input a 2D Image NumPy array\"\"\"\n\n    def __init__(\n        self,\n        label: str = \"Image\",\n        *,\n        image_type: navi.ExpressionJson = \"Image | Color\",\n        channels: int | list[int] | None = None,\n        allow_colors: bool = False,\n    ) -> None:\n        base_type = [navi.Image(channels=channels)]\n        if allow_colors:\n            base_type.append(navi.Color(channels=channels))\n        image_type = navi.intersect(image_type, base_type)\n        super().__init__(image_type, label)\n\n        self.channels: list[int] | None = (\n            [channels] if isinstance(channels, int) else channels\n        )\n        self.allow_colors: bool = allow_colors\n\n        self.associated_type = np.ndarray\n\n        if self.allow_colors:\n            self.associated_type = Union[np.ndarray, Color]\n\n    def enforce(self, value: object):\n        if isinstance(value, Color):\n            if not self.allow_colors:\n                raise ValueError(\n                    f\"The input {self.label} does not accept colors, but was connected with one.\"\n                )\n\n            if self.channels is not None and value.channels not in self.channels:\n                expected = format_color_with_channels(self.channels, plural=True)\n                actual = format_color_with_channels([value.channels])\n                raise ValueError(\n                    f\"The input {self.label} only supports {expected} but was given {actual}.\"\n                )\n\n            return value\n\n        assert isinstance(value, np.ndarray)\n        _, _, c = get_h_w_c(value)\n\n        if self.channels is not None and c not in self.channels:\n            expected = format_image_with_channels(self.channels, plural=True)\n            actual = format_image_with_channels([c])\n            raise ValueError(\n                f\"The input {self.label} only supports {expected} but was given {actual}.\"\n            )\n\n        assert value.dtype == np.float32, \"Expected the input image to be normalized.\"\n\n        if c == 1 and value.ndim == 3:\n            value = value[:, :, 0]\n\n        return value\n\n    def get_error_value(self, value: object) -> ErrorValue:\n        def get_channels(channel: int) -> str:\n            if channel == 1:\n                return \"Grayscale\"\n            if channel == 3:\n                return \"RGB\"\n            if channel == 4:\n                return \"RGBA\"\n            return f\"{channel}-channel\"\n\n        if isinstance(value, Color):\n            return {\n                \"type\": \"formatted\",\n                \"formatString\": f\"{get_channels(value.channels)} Color\",\n            }\n        elif isinstance(value, np.ndarray):\n            h, w, c = get_h_w_c(value)\n            return {\n                \"type\": \"formatted\",\n                \"formatString\": f\"{get_channels(c)} Image {w}x{h}\",\n            }\n        else:\n            return super().get_error_value(value)\n\n\nclass VideoInput(BaseInput):\n    \"\"\"Input a 3D Video NumPy array\"\"\"\n\n    def __init__(self, label: str = \"Video\") -> None:\n        super().__init__(\"Video\", label)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/onnx_inputs.py",
    "content": "import navi\n\nfrom api import BaseInput\n\nfrom ...impl.onnx.model import OnnxGeneric, OnnxModel, OnnxModels, OnnxRemBg\nfrom .generic_inputs import DropDownInput\n\n\nclass OnnxModelInput(BaseInput):\n    \"\"\"Input for onnx model\"\"\"\n\n    def __init__(\n        self, label: str = \"Model\", input_type: navi.ExpressionJson = \"OnnxModel\"\n    ) -> None:\n        super().__init__(input_type, label)\n        self.associated_type = OnnxModel\n\n\nclass OnnxGenericModelInput(OnnxModelInput):\n    \"\"\"ONNX model input for things that aren't background removal\"\"\"\n\n    def __init__(\n        self, label: str = \"Model\", input_type: navi.ExpressionJson = \"OnnxModel\"\n    ) -> None:\n        super().__init__(label, navi.intersect(input_type, \"OnnxGenericModel\"))\n        self.associated_type = OnnxGeneric\n\n    def enforce(self, value: object):\n        assert isinstance(value, OnnxModels)\n        assert value.sub_type == \"Generic\", \"Expected a non-rembg model\"\n        return value\n\n\nclass OnnxRemBgModelInput(OnnxModelInput):\n    \"\"\"ONNX model input for background removal\"\"\"\n\n    def __init__(\n        self, label: str = \"Model\", input_type: navi.ExpressionJson = \"OnnxModel\"\n    ) -> None:\n        super().__init__(label, navi.intersect(input_type, \"OnnxRemBgModel\"))\n        self.associated_type = OnnxRemBg\n\n    def enforce(self, value: object):\n        assert isinstance(value, OnnxModels)\n        assert value.sub_type == \"RemBg\", \"Expected a rembg model\"\n        return value\n\n\ndef OnnxFpDropdown() -> DropDownInput:\n    return DropDownInput(\n        input_type=\"FpMode\",\n        label=\"Data Type\",\n        options=[\n            {\n                \"option\": \"fp32\",\n                \"value\": 0,\n                \"type\": \"FpMode::fp32\",\n            },\n            {\n                \"option\": \"fp16\",\n                \"value\": 1,\n                \"type\": \"FpMode::fp16\",\n            },\n        ],\n    )\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/inputs/pytorch_inputs.py",
    "content": "from __future__ import annotations\n\ntry:\n    import spandrel\n    from spandrel import Purpose\nexcept Exception:\n    spandrel = None\n\nimport navi\n\nfrom api import BaseInput\n\n\ndef _model_with_purpose(purpose: set[Purpose]):\n    sub_type = \" | \".join('\"' + p + '\"' for p in purpose)\n    return \"PyTorchModel { subType: \" + sub_type + \" }\"\n\n\nclass ModelInput(BaseInput):\n    \"\"\"Input a loaded model\"\"\"\n\n    def __init__(\n        self,\n        label: str = \"Model\",\n        input_type: navi.ExpressionJson = \"PyTorchModel\",\n    ) -> None:\n        super().__init__(input_type, label)\n        if spandrel is not None:\n            self.associated_type = spandrel.ModelDescriptor\n\n    def enforce(self, value: object):\n        if spandrel is not None:\n            assert isinstance(\n                value,\n                spandrel.ImageModelDescriptor | spandrel.MaskedImageModelDescriptor,\n            ), \"Expected a supported PyTorch model.\"\n        return value\n\n\nclass SrModelInput(ModelInput):\n    def __init__(\n        self,\n        label: str = \"Model\",\n        input_type: navi.ExpressionJson = \"PyTorchModel\",\n    ) -> None:\n        self.purpose: set[Purpose] = {\"SR\", \"Restoration\"}\n\n        super().__init__(\n            label,\n            navi.intersect(input_type, _model_with_purpose(self.purpose)),\n        )\n        if spandrel is not None:\n            self.associated_type = spandrel.ImageModelDescriptor\n\n    def enforce(self, value: object):\n        if spandrel is not None:\n            assert isinstance(\n                value, spandrel.ImageModelDescriptor\n            ), \"Expected a supported single image PyTorch model.\"\n            assert (\n                value.purpose in self.purpose\n            ), \"Expected a Super-Resolution or Restoration model.\"\n        return value\n\n\nclass FaceModelInput(ModelInput):\n    def __init__(\n        self, label: str = \"Model\", input_type: navi.ExpressionJson = \"PyTorchModel\"\n    ) -> None:\n        self.purpose: set[Purpose] = {\"FaceSR\"}\n\n        super().__init__(\n            label,\n            navi.intersect(input_type, _model_with_purpose(self.purpose)),\n        )\n        if spandrel is not None:\n            self.associated_type = spandrel.ImageModelDescriptor\n\n    def enforce(self, value: object):\n        if spandrel is not None:\n            assert isinstance(\n                value, spandrel.ImageModelDescriptor\n            ), \"Expected a supported single image PyTorch model.\"\n            assert (\n                value.purpose in self.purpose\n            ), \"Expected a Face Super-Resolution model.\"\n        return value\n\n\nclass InpaintModelInput(ModelInput):\n    def __init__(\n        self, label: str = \"Model\", input_type: navi.ExpressionJson = \"PyTorchModel\"\n    ) -> None:\n        self.purpose: set[Purpose] = {\"Inpainting\"}\n\n        super().__init__(\n            label,\n            navi.intersect(input_type, _model_with_purpose(self.purpose)),\n        )\n        if spandrel is not None:\n            self.associated_type = spandrel.MaskedImageModelDescriptor\n\n    def enforce(self, value: object):\n        if spandrel is not None:\n            assert isinstance(\n                value, spandrel.MaskedImageModelDescriptor\n            ), \"Expected a supported masked-image PyTorch model.\"\n            assert value.purpose in self.purpose, \"Expected an Inpainting model.\"\n        return value\n\n\nclass TorchScriptInput(BaseInput):\n    \"\"\"Input a JIT traced model\"\"\"\n\n    def __init__(self, label: str = \"Traced Model\") -> None:\n        super().__init__(\"PyTorchScript\", label)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/__init__.py",
    "content": "from .file_outputs import *\nfrom .generic_outputs import *\nfrom .numpy_outputs import *\n\ntry:\n    from .ncnn_outputs import *\nexcept Exception:\n    pass\ntry:\n    from .onnx_outputs import *\nexcept Exception:\n    pass\ntry:\n    from .pytorch_outputs import *\nexcept Exception:\n    pass\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/file_outputs.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import Path\n\nimport navi\n\nfrom api import BaseOutput\n\n\nclass DirectoryOutput(BaseOutput[Path]):\n    \"\"\"Output for saving to a directory\"\"\"\n\n    def __init__(\n        self,\n        label: str = \"Directory\",\n        of_input: int | None = None,\n        output_type: str = \"Directory\",\n    ) -> None:\n        directory_type = (\n            \"Directory\"\n            if of_input is None\n            else f\"splitFilePath(Input{of_input}.path).dir\"\n        )\n        directory_type = navi.intersect_with_error(directory_type, output_type)\n        super().__init__(directory_type, label, associated_type=Path)\n\n    def get_broadcast_type(self, value: Path):\n        return navi.named(\"Directory\", {\"path\": navi.literal(str(value))})\n\n    def enforce(self, value: object) -> Path:\n        assert isinstance(value, Path)\n        return value\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/generic_outputs.py",
    "content": "from __future__ import annotations\n\nfrom typing import Union\n\nimport navi\n\nfrom api import BaseOutput\n\nfrom ...impl.color.color import Color\nfrom ...utils.format import format_color_with_channels\nfrom ...utils.seed import Seed\n\n\nclass NumberOutput(BaseOutput[Union[int, float]]):\n    def __init__(\n        self,\n        label: str,\n        output_type: navi.ExpressionJson = \"number\",\n    ) -> None:\n        super().__init__(\n            navi.intersect_with_error(\"number\", output_type),\n            label,\n            associated_type=Union[int, float],\n        )\n\n    def get_broadcast_type(self, value: int | float):\n        return navi.literal(value)\n\n    def enforce(self, value: object) -> int | float:\n        assert isinstance(value, int | float)\n        return value\n\n\nclass TextOutput(BaseOutput):\n    def __init__(\n        self,\n        label: str,\n        output_type: navi.ExpressionJson = \"string\",\n    ) -> None:\n        super().__init__(navi.intersect_with_error(\"string\", output_type), label)\n\n    def get_broadcast_type(self, value: str):\n        return navi.literal(value)\n\n    def enforce(self, value: object) -> str:\n        assert isinstance(value, str)\n        return value\n\n\ndef FileNameOutput(label: str = \"Name\", of_input: int | None = None):\n    output_type = (\n        \"string\"\n        if of_input is None\n        else f\"splitFilePath(Input{of_input}.path).basename\"\n    )\n\n    return TextOutput(label=label, output_type=output_type)\n\n\nclass SeedOutput(BaseOutput):\n    def __init__(self, label: str = \"Seed\") -> None:\n        super().__init__(output_type=\"Seed\", label=label, kind=\"generic\")\n\n    def enforce(self, value: object) -> Seed:\n        assert isinstance(value, Seed)\n        return value\n\n\nclass ColorOutput(BaseOutput):\n    def __init__(\n        self,\n        label: str = \"Color\",\n        color_type: navi.ExpressionJson = \"Color\",\n        channels: int | None = None,\n    ) -> None:\n        super().__init__(\n            output_type=navi.intersect_with_error(\n                color_type, navi.Color(channels=channels)\n            ),\n            label=label,\n            kind=\"generic\",\n        )\n\n        self.channels = channels\n\n    def enforce(self, value: object) -> Color:\n        assert isinstance(value, Color)\n\n        if self.channels is not None and value.channels != self.channels:\n            expected = format_color_with_channels([self.channels])\n            actual = format_color_with_channels([value.channels])\n            raise ValueError(\n                f\"The output {self.label} was supposed to return {expected} but actually returned {actual}.\"\n                f\" This is a bug in the implementation of the node.\"\n                f\" Please report this bug.\"\n            )\n\n        return value\n\n\nclass BoolOutput(BaseOutput):\n    def __init__(\n        self,\n        label: str = \"Logical\",\n        *,\n        output_type: navi.ExpressionJson = \"bool\",\n    ) -> None:\n        super().__init__(\n            output_type=navi.intersect_with_error(\"bool\", output_type),\n            label=label,\n            kind=\"generic\",\n        )\n\n\nclass AudioStreamOutput(BaseOutput):\n    def __init__(self, label: str = \"Audio Stream\") -> None:\n        super().__init__(\n            output_type=\"AudioStream\",\n            label=label,\n            kind=\"generic\",\n        )\n\n\nclass AnyOutput(BaseOutput):\n    def __init__(\n        self, label: str = \"Any\", output_type: navi.ExpressionJson = \"Any\"\n    ) -> None:\n        super().__init__(\n            output_type=output_type,\n            label=label,\n            kind=\"generic\",\n        )\n\n    def enforce(self, value: object) -> object:\n        return value\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/ncnn_outputs.py",
    "content": "import navi\n\nfrom api import BaseOutput, OutputKind\n\nfrom ...impl.ncnn.model import NcnnModelWrapper\nfrom ...utils.format import format_channel_numbers\n\n\nclass NcnnModelOutput(BaseOutput):\n    def __init__(\n        self,\n        model_type: navi.ExpressionJson = \"NcnnNetwork\",\n        label: str = \"Model\",\n        kind: OutputKind = \"generic\",\n    ) -> None:\n        super().__init__(model_type, label, kind=kind, associated_type=NcnnModelWrapper)\n\n    def get_broadcast_data(self, value: NcnnModelWrapper):\n        return {\n            \"tags\": [\n                format_channel_numbers(value.in_nc, value.out_nc),\n                f\"{value.nf}nf\",\n                value.fp,\n            ]\n        }\n\n    def get_broadcast_type(self, value: NcnnModelWrapper):\n        return navi.named(\n            \"NcnnNetwork\",\n            {\n                \"scale\": value.scale,\n                \"inputChannels\": value.in_nc,\n                \"outputChannels\": value.out_nc,\n                \"nf\": value.nf,\n                \"fp\": navi.literal(value.fp),\n            },\n        )\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/numpy_outputs.py",
    "content": "from __future__ import annotations\n\nimport base64\n\nimport cv2\nimport navi\nimport numpy as np\n\nfrom api import BaseOutput, BroadcastData, InputId, OutputKind\n\nfrom ...impl.image_utils import normalize, to_uint8\nfrom ...impl.resize import ResizeFilter, resize\nfrom ...utils.format import format_image_with_channels\nfrom ...utils.utils import get_h_w_c, round_half_up\n\n\nclass NumPyOutput(BaseOutput[np.ndarray]):\n    \"\"\"Output a NumPy array\"\"\"\n\n    def __init__(\n        self,\n        output_type: navi.ExpressionJson,\n        label: str,\n        kind: OutputKind = \"generic\",\n        has_handle: bool = True,\n    ) -> None:\n        super().__init__(\n            output_type,\n            label,\n            kind=kind,\n            has_handle=has_handle,\n            associated_type=np.ndarray,\n        )\n\n    def enforce(self, value: object) -> np.ndarray:\n        assert isinstance(value, np.ndarray)\n        return value\n\n\ndef AudioOutput():\n    \"\"\"Output a 1D Audio NumPy array\"\"\"\n    return NumPyOutput(\"Audio\", \"Audio\")\n\n\nclass ImageOutput(NumPyOutput):\n    def __init__(\n        self,\n        label: str = \"Image\",\n        *,\n        image_type: navi.ExpressionJson = \"Image\",\n        kind: OutputKind = \"generic\",\n        has_handle: bool = True,\n        channels: int | None = None,\n        shape_as: int | InputId | None = None,\n        size_as: int | InputId | None = None,\n        assume_normalized: bool = False,\n    ) -> None:\n        # narrow down type\n        if channels is not None:\n            image_type = navi.intersect_with_error(\n                image_type, navi.Image(channels=channels)\n            )\n        if shape_as is not None:\n            image_type = navi.intersect_with_error(image_type, f\"Input{shape_as}\")\n        if size_as is not None:\n            image_type = navi.intersect_with_error(\n                image_type, navi.Image(size_as=f\"Input{size_as}\")\n            )\n\n        super().__init__(image_type, label, kind=kind, has_handle=has_handle)\n\n        self.channels: int | None = channels\n        self.assume_normalized: bool = assume_normalized\n\n        if shape_as is not None:\n            self.as_passthrough_of(shape_as)\n\n    def get_broadcast_data(self, value: np.ndarray) -> BroadcastData:\n        h, w, c = get_h_w_c(value)\n        return {\n            \"height\": h,\n            \"width\": w,\n            \"channels\": c,\n        }\n\n    def get_broadcast_type(self, value: np.ndarray):\n        h, w, c = get_h_w_c(value)\n        return navi.Image(width=w, height=h, channels=c)\n\n    def enforce(self, value: object) -> np.ndarray:\n        assert isinstance(value, np.ndarray)\n\n        h, w, c = get_h_w_c(value)\n\n        if h == 0 or w == 0:\n            raise ValueError(\n                f\"The output {self.label} returned an empty image (w={w} h={h}).\"\n                f\" This is a bug in the implementation of the node.\"\n                f\" Please report this bug.\"\n            )\n\n        if self.channels is not None and c != self.channels:\n            expected = format_image_with_channels([self.channels])\n            actual = format_image_with_channels([c])\n            raise ValueError(\n                f\"The output {self.label} was supposed to return {expected} but actually returned {actual}.\"\n                f\" This is a bug in the implementation of the node.\"\n                f\" Please report this bug.\"\n            )\n\n        # flatting 3D single-channel images to 2D\n        if c == 1 and value.ndim == 3:\n            value = value[:, :, 0]\n\n        if not self.assume_normalized:\n            value = normalize(value)\n\n        assert value.dtype == np.float32, (\n            f\"The output {self.label} did not return a normalized image.\"\n            f\" This is a bug in the implementation of the node.\"\n            f\" Please report this bug.\"\n            f\"\\n\\nTo the author of this node: Either use `normalize` or remove `assume_normalized=True` from this output.\"\n        )\n\n        # make image readonly\n        value.setflags(write=False)\n\n        return value\n\n\ndef preview_encode(\n    img: np.ndarray,\n    target_size: int = 512,\n    grace: float = 1.2,\n    lossless: bool = False,\n) -> tuple[str, np.ndarray]:\n    \"\"\"\n    resize the image, so the preview loads faster and doesn't lag the UI\n    512 was chosen as the default target because a 512x512 RGBA 8bit PNG is at most 1MB in size\n    \"\"\"\n    h, w, c = get_h_w_c(img)\n\n    max_size = target_size * grace\n    if w > max_size or h > max_size:\n        f = max(w / target_size, h / target_size)\n        t = (max(1, round_half_up(w / f)), max(1, round_half_up(h / f)))\n        img = resize(img, t, ResizeFilter.BOX)\n\n    image_format = \"png\" if c > 3 or lossless else \"jpg\"\n\n    _, encoded_img = cv2.imencode(f\".{image_format}\", to_uint8(img, normalized=True))  # type: ignore\n    base64_img = base64.b64encode(encoded_img).decode(\"utf8\")  # type: ignore\n\n    return f\"data:image/{image_format};base64,{base64_img}\", img\n\n\nclass LargeImageOutput(ImageOutput):\n    def __init__(\n        self,\n        label: str = \"Image\",\n        image_type: navi.ExpressionJson = \"Image\",\n        kind: OutputKind = \"large-image\",\n        has_handle: bool = True,\n        assume_normalized: bool = False,\n    ) -> None:\n        super().__init__(\n            label,\n            image_type=image_type,\n            kind=kind,\n            has_handle=has_handle,\n            assume_normalized=assume_normalized,\n        )\n\n    def get_broadcast_data(self, value: np.ndarray):\n        img = value\n        h, w, c = get_h_w_c(img)\n        image_size = max(h, w)\n\n        preview_sizes = [2048, 1024, 512, 256]\n        preview_size_grace = 1.2\n\n        start_index = len(preview_sizes) - 1\n        for i, size in enumerate(preview_sizes):\n            if size <= image_size and image_size <= size * preview_size_grace:\n                # this preview size will perfectly fit the image\n                start_index = i\n                break\n            if image_size > size:\n                # the image size is larger than the preview size, so try to pick the previous size\n                start_index = max(0, i - 1)\n                break\n\n        previews = []\n\n        # Encode for multiple scales. Use the preceding scale to save time encoding the smaller sizes.\n        last_encoded = img\n        for size in preview_sizes[start_index:]:\n            largest_preview = size == preview_sizes[start_index]\n            url, last_encoded = preview_encode(\n                last_encoded,\n                target_size=size,\n                grace=preview_size_grace,\n                lossless=largest_preview,\n            )\n            le_h, le_w, _ = get_h_w_c(last_encoded)\n            previews.append({\"width\": le_w, \"height\": le_h, \"url\": url})\n\n        return {\n            \"previews\": previews,\n            \"height\": h,\n            \"width\": w,\n            \"channels\": c,\n        }\n\n\ndef VideoOutput():\n    \"\"\"Output a 3D Video NumPy array\"\"\"\n    return NumPyOutput(\"Video\", \"Video\")\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/onnx_outputs.py",
    "content": "from __future__ import annotations\n\nimport navi\nfrom nodes.utils.format import format_channel_numbers\n\nfrom api import BaseOutput, OutputKind\n\nfrom ...impl.onnx.model import OnnxModel\n\n\nclass OnnxModelOutput(BaseOutput):\n    \"\"\"Output for onnx model\"\"\"\n\n    def __init__(\n        self,\n        model_type: navi.ExpressionJson = \"OnnxModel\",\n        label: str = \"Model\",\n        kind: OutputKind = \"generic\",\n    ) -> None:\n        super().__init__(model_type, label, kind=kind, associated_type=OnnxModel)\n\n    def get_broadcast_data(self, value: OnnxModel):\n        i = value.info\n\n        tags: list[str] = []\n        if i.input_channels is not None and i.output_channels is not None:\n            tags.append(format_channel_numbers(i.input_channels, i.output_channels))\n\n        tags.append(f\"opset{i.opset}\")\n        tags.append(i.dtype)\n\n        return {\"tags\": tags}\n\n    def get_broadcast_type(self, value: OnnxModel):\n        fields = {\n            \"subType\": navi.literal(value.sub_type),\n        }\n\n        i = value.info\n        if i.scale_width is not None:\n            fields[\"scaleWidth\"] = i.scale_width\n        if i.scale_height is not None:\n            fields[\"scaleHeight\"] = i.scale_height\n        if i.input_channels is not None:\n            fields[\"inputChannels\"] = i.input_channels\n        if i.output_channels is not None:\n            fields[\"outputChannels\"] = i.output_channels\n\n        return navi.named(\"OnnxModel\", fields)\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/properties/outputs/pytorch_outputs.py",
    "content": "from __future__ import annotations\n\nimport navi\nfrom spandrel import ModelDescriptor, ModelTiling\n\nfrom api import BaseOutput, OutputKind\n\nfrom ...utils.format import format_channel_numbers\n\n\nclass ModelOutput(BaseOutput):\n    def __init__(\n        self,\n        model_type: navi.ExpressionJson = \"PyTorchModel\",\n        label: str = \"Model\",\n        kind: OutputKind = \"generic\",\n    ) -> None:\n        super().__init__(model_type, label, kind=kind, associated_type=ModelDescriptor)\n\n    def get_broadcast_data(self, value: ModelDescriptor) -> dict[str, list[str]]:\n        return {\n            \"tags\": [\n                value.architecture.name,\n                format_channel_numbers(value.input_channels, value.output_channels),\n                *value.tags,\n            ]\n        }\n\n    def get_broadcast_type(self, value: ModelDescriptor):\n        tiling_map: dict[ModelTiling, str] = {\n            ModelTiling.SUPPORTED: \"ModelTiling::Supported\",\n            ModelTiling.DISCOURAGED: \"ModelTiling::Discouraged\",\n            ModelTiling.INTERNAL: \"ModelTiling::Internal\",\n        }\n\n        return navi.named(\n            \"PyTorchModel\",\n            {\n                \"scale\": value.scale,\n                \"inputChannels\": value.input_channels,\n                \"outputChannels\": value.output_channels,\n                \"arch\": navi.literal(value.architecture.name),\n                \"subType\": navi.literal(value.purpose),\n                \"size\": navi.literal(\"x\".join(value.tags)),\n                \"tiling\": tiling_map[value.tiling],\n            },\n        )\n\n\ndef TorchScriptOutput():\n    \"\"\"Output a JIT traced model\"\"\"\n    return BaseOutput(\"PyTorchScript\", \"Traced Model\")\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/utils/__init__.py",
    "content": ""
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/utils/format.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable, Iterable\nfrom typing import Literal, TypeVar\n\nT = TypeVar(\"T\")\nConj = Literal[\"and\", \"or\"]\n\n\ndef join_english(\n    items: Iterable[T],\n    to_str: Callable[[T], str] = str,\n    conj: Conj = \"and\",\n) -> str:\n    s = list(map(to_str, items))\n\n    l = len(s)\n    assert l > 0\n\n    if l == 1:\n        return s[0]\n    if l == 2:\n        return f\"{s[0]} {conj} {s[1]}\"\n    return \", \".join(s[:-1]) + f\", {conj} \" + s[-1]\n\n\ndef format_image_with_channels(\n    channels: list[int],\n    conj: Conj = \"and\",\n    plural: bool = False,\n) -> str:\n    assert len(channels) > 0\n\n    named = {1: \"grayscale\", 3: \"RGB\", 4: \"RGBA\"}\n    if all(x in named for x in channels):\n        if plural:\n            return join_english(channels, lambda c: named[c], conj=conj) + \" images\"\n        else:\n            return (\n                \"a \" + join_english(channels, lambda c: named[c], conj=conj) + \" image\"\n            )\n\n    if plural:\n        return f\"images with {join_english(channels, conj=conj)} channel(s)\"\n    else:\n        return f\"an image with {join_english(channels, conj=conj)} channel(s)\"\n\n\ndef format_color_with_channels(\n    channels: list[int],\n    conj: Conj = \"and\",\n    plural: bool = False,\n) -> str:\n    assert len(channels) > 0\n\n    named = {1: \"grayscale\", 3: \"RGB\", 4: \"RGBA\"}\n    if all(x in named for x in channels):\n        if plural:\n            return join_english(channels, lambda c: named[c], conj=conj) + \" colors\"\n        else:\n            return (\n                \"a \" + join_english(channels, lambda c: named[c], conj=conj) + \" color\"\n            )\n\n    if plural:\n        return f\"color with {join_english(channels, conj=conj)} channel(s)\"\n    else:\n        return f\"a color with {join_english(channels, conj=conj)} channel(s)\"\n\n\n_CHANNEL_NUMBER_NAME = {1: \"GRAY\", 3: \"RGB\", 4: \"RGBA\"}\n\n\ndef format_channel_numbers(input_channels: int, output_channels: int) -> str:\n    i = _CHANNEL_NUMBER_NAME.get(input_channels, str(input_channels))\n    o = _CHANNEL_NUMBER_NAME.get(output_channels, str(output_channels))\n    return f\"{i}🠚{o}\"\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/utils/seed.py",
    "content": "from dataclasses import dataclass\nfrom random import Random\n\n_U32_MAX = 4294967296\n\n\n@dataclass(frozen=True)\nclass Seed:\n    value: int\n    \"\"\"\n    The value of the seed. This value may be signed and generally have any range.\n    \"\"\"\n\n    @staticmethod\n    def from_bytes(b: bytes):\n        return Seed(Random(b).randint(0, _U32_MAX - 1))\n\n    def to_range(self, a: int, b: int) -> int:\n        \"\"\"\n        Returns the value of the seed within the given range [a,b] both ends inclusive.\n\n        If the current seed is not within the given range, a value within the range will be derived from the current seed.\n        \"\"\"\n        if a <= self.value <= b:\n            return self.value\n        return Random(self.value).randint(a, b)\n\n    def to_u32(self) -> int:\n        \"\"\"\n        Returns the value of the seed as a 32bit unsigned integer.\n        \"\"\"\n        return self.to_range(0, _U32_MAX - 1)\n\n    def cache_key_func(self):\n        return self.value\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/nodes/utils/utils.py",
    "content": "# From https://github.com/victorca25/iNNfer/blob/main/utils/utils.py\nfrom __future__ import annotations\n\nimport math\nimport os\nimport re\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nimport numpy as np\nfrom sanic.log import logger\n\nSize = tuple[int, int]\n\"\"\"\nThe width and height (in that order) of an image.\n\"\"\"\n\nNUMBERS = re.compile(r\"(\\d+)\")\n\nALPHABET = [*\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"]\n\n\ndef round_half_up(number: float | int) -> int:\n    \"\"\"\n    Python's `round` method implements round-half-to-even rounding which is very unintuitive.\n    This function implements round-half-up rounding.\n\n    Round half up is consistent with JavaScript's `Math.round`.\n\n    https://en.wikipedia.org/wiki/Rounding#Rounding_to_the_nearest_integer\n    \"\"\"\n    return math.floor(number + 0.5)\n\n\ndef get_h_w_c(image: np.ndarray) -> tuple[int, int, int]:\n    \"\"\"Returns the height, width, and number of channels.\"\"\"\n    h, w = image.shape[:2]\n    c = 1 if image.ndim == 2 else image.shape[2]\n    return h, w, c\n\n\ndef alphanumeric_sort(value: str) -> list[str | int]:\n    \"\"\"Key function to sort strings containing numbers by proper\n    numerical order.\"\"\"\n\n    lcase_value = value.upper()\n    parts = NUMBERS.split(lcase_value)\n    parts[1::2] = map(int, parts[1::2])\n    return parts  # type: ignore\n\n\n__SPLIT_SNAKE_CASE = re.compile(r\"(\\d+|_+)\")\n__SPLIT_PASCAL_CASE = re.compile(r\"(\\d+)|(?<=[a-z])(?=[A-Z])\")\n\n\ndef split_snake_case(s: str) -> list[str]:\n    \"\"\"Splits a snake case identifier into its parts. E.g. `SNAKE_CASE` -> [`snake`, `case`]\"\"\"\n    return [\n        x.lower() for x in __SPLIT_SNAKE_CASE.split(s) if x and not x.startswith(\"_\")\n    ]\n\n\ndef split_pascal_case(s: str) -> list[str]:\n    \"\"\"Splits a snake case identifier into its parts. E.g. `SNAKE_CASE` -> [`snake`, `case`]\"\"\"\n    return [\n        x.lower() for x in __SPLIT_PASCAL_CASE.split(s) if x and not x.startswith(\"_\")\n    ]\n\n\ndef join_pascal_case(words: list[str]) -> str:\n    return \"\".join([x.capitalize() for x in words])\n\n\n__ABBREVIATIONS = {\"rgb\", \"rgba\"}\n\n\ndef smart_capitalize(word: str) -> str:\n    if word in __ABBREVIATIONS:\n        return word.upper()\n    return word.capitalize()\n\n\ndef join_space_case(words: list[str]) -> str:\n    return \" \".join([smart_capitalize(x) for x in words])\n\n\ndef split_file_path(path: Path | str) -> tuple[Path, str, str]:\n    \"\"\"\n    Returns the base directory, file name, and extension of the given file path.\n    \"\"\"\n    base, ext = os.path.splitext(path)\n    dirname, basename = os.path.split(base)\n    return Path(dirname), basename, ext\n\n\ndef walk_error_handler(exception_instance: Exception) -> None:\n    logger.warning(\n        f\"Exception occurred during walk: {exception_instance} Continuing...\"\n    )\n\n\ndef list_all_files_sorted(\n    directory: Path, ext_filter: list[str] | None = None\n) -> list[Path]:\n    just_files: list[Path] = []\n    for root, dirs, files in os.walk(\n        directory, topdown=True, onerror=walk_error_handler\n    ):\n        dirs.sort(key=alphanumeric_sort)\n        for name in sorted(files, key=alphanumeric_sort):\n            filepath = os.path.join(root, name)\n            _base, ext = os.path.splitext(filepath)\n            if ext_filter is None or ext.lower() in ext_filter:\n                just_files.append(Path(filepath))\n    return just_files\n\n\n@dataclass(frozen=True)\nclass Padding:\n    top: int\n    right: int\n    bottom: int\n    left: int\n\n    @staticmethod\n    def all(value: int) -> Padding:\n        return Padding(value, value, value, value)\n\n    @staticmethod\n    def to(value: Padding | int) -> Padding:\n        if isinstance(value, int):\n            return Padding.all(value)\n        return value\n\n    @property\n    def horizontal(self) -> int:\n        return self.left + self.right\n\n    @property\n    def vertical(self) -> int:\n        return self.top + self.bottom\n\n    @property\n    def empty(self) -> bool:\n        return self.top == 0 and self.right == 0 and self.bottom == 0 and self.left == 0\n\n    def scale(self, factor: int) -> Padding:\n        return Padding(\n            self.top * factor,\n            self.right * factor,\n            self.bottom * factor,\n            self.left * factor,\n        )\n\n    def min(self, other: Padding | int) -> Padding:\n        other = Padding.to(other)\n        return Padding(\n            min(self.top, other.top),\n            min(self.right, other.right),\n            min(self.bottom, other.bottom),\n            min(self.left, other.left),\n        )\n\n    def remove_from(self, image: np.ndarray) -> np.ndarray:\n        h, w, _ = get_h_w_c(image)\n\n        return image[\n            self.top : (h - self.bottom),\n            self.left : (w - self.right),\n            ...,\n        ]\n\n\n@dataclass(frozen=True)\nclass Region:\n    x: int\n    y: int\n    width: int\n    height: int\n\n    @property\n    def size(self) -> Size:\n        return self.width, self.height\n\n    def scale(self, factor: int) -> Region:\n        return Region(\n            self.x * factor,\n            self.y * factor,\n            self.width * factor,\n            self.height * factor,\n        )\n\n    def intersect(self, other: Region) -> Region:\n        x = max(self.x, other.x)\n        y = max(self.y, other.y)\n        width = min(self.x + self.width, other.x + other.width) - x\n        height = min(self.y + self.height, other.y + other.height) - y\n        return Region(x, y, width, height)\n\n    def add_padding(self, pad: Padding) -> Region:\n        return Region(\n            x=self.x - pad.left,\n            y=self.y - pad.top,\n            width=self.width + pad.horizontal,\n            height=self.height + pad.vertical,\n        )\n\n    def remove_padding(self, pad: Padding) -> Region:\n        return self.add_padding(pad.scale(-1))\n\n    def child_padding(self, child: Region) -> Padding:\n        \"\"\"\n        Returns the padding `p` such that `child.add_padding(p) == self`.\n        \"\"\"\n        left = child.x - self.x\n        top = child.y - self.y\n        right = self.width - child.width - left\n        bottom = self.height - child.height - top\n        return Padding(top, right, bottom, left)\n\n    def read_from(self, image: np.ndarray) -> np.ndarray:\n        h, w, _ = get_h_w_c(image)\n        if (w, h) == self.size:\n            return image\n\n        return image[\n            self.y : (self.y + self.height),\n            self.x : (self.x + self.width),\n            ...,\n        ]\n\n    def write_into(self, lhs: np.ndarray, rhs: np.ndarray) -> None:\n        h, w, c = get_h_w_c(rhs)\n        assert (w, h) == self.size\n        assert c == get_h_w_c(lhs)[2]\n\n        if c == 1:\n            if lhs.ndim == 2 and rhs.ndim == 3:\n                rhs = rhs[:, :, 0]\n            if lhs.ndim == 3 and rhs.ndim == 2:\n                rhs = np.expand_dims(rhs, axis=2)\n\n        lhs[\n            self.y : (self.y + self.height),\n            self.x : (self.x + self.width),\n            ...,\n        ] = rhs\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/packages/chaiNNer_pytorch/__init__.py",
    "content": "import os\n\nfrom accelerator_detection import AcceleratorType, get_accelerator_detector\nfrom gpu import nvidia\nfrom sanic.log import logger\nfrom system import is_arm_mac\n\nfrom api import GB, KB, MB, Dependency, add_package\n\n# Get available accelerators\ndetector = get_accelerator_detector()\navailable_devices = detector.available_devices\ngpu_devices = [d for d in available_devices if d.type != AcceleratorType.CPU]\n\n# Build description based on available accelerators\naccelerator_names = []\nif any(d.type == AcceleratorType.CUDA for d in gpu_devices):\n    accelerator_names.append(\"NVIDIA CUDA\")\nif any(d.type == AcceleratorType.ROCM for d in gpu_devices):\n    accelerator_names.append(\"AMD ROCm\")\nif any(d.type == AcceleratorType.XPU for d in gpu_devices):\n    accelerator_names.append(\"Intel XPU\")\nif any(d.type == AcceleratorType.MPS for d in gpu_devices):\n    accelerator_names.append(\"Apple MPS\")\n\ngeneral = \"PyTorch uses .pth models to upscale images.\"\n\nif is_arm_mac:\n    os.environ[\"PYTORCH_ENABLE_MPS_FALLBACK\"] = \"1\"\n    package_description = f\"{general} Optimized for Apple Silicon with MPS acceleration.\"\n    inst_hint = f\"{general} It is the most widely-used upscaling architecture and supports Apple Silicon acceleration.\"\nelif accelerator_names:\n    accelerator_list = \", \".join(accelerator_names)\n    package_description = f\"{general} Supports hardware acceleration with: {accelerator_list}.\"\n    inst_hint = f\"{general} It is the most widely-used upscaling architecture and supports multiple accelerators including {accelerator_list}.\"\nelse:\n    package_description = f\"{general} Running on CPU (no hardware accelerators detected).\"\n    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).\"\n\n\ndef get_pytorch():\n    if is_arm_mac:\n        return [\n            Dependency(\n                display_name=\"PyTorch\",\n                pypi_name=\"torch\",\n                version=\"2.1.2\",\n                size_estimate=55.8 * MB,\n                auto_update=False,\n            ),\n            Dependency(\n                display_name=\"TorchVision\",\n                pypi_name=\"torchvision\",\n                version=\"0.16.2\",\n                size_estimate=1.3 * MB,\n                auto_update=False,\n            ),\n        ]\n    else:\n        return [\n            Dependency(\n                display_name=\"PyTorch\",\n                pypi_name=\"torch\",\n                version=\"2.1.2+cu121\" if nvidia.is_available else \"2.1.2\",\n                size_estimate=2 * GB if nvidia.is_available else 140 * MB,\n                extra_index_url=(\n                    \"https://download.pytorch.org/whl/cu121\"\n                    if nvidia.is_available\n                    else \"https://download.pytorch.org/whl/cpu\"\n                ),\n                auto_update=False,\n            ),\n            Dependency(\n                display_name=\"TorchVision\",\n                pypi_name=\"torchvision\",\n                version=\"0.16.2+cu121\" if nvidia.is_available else \"0.16.2\",\n                size_estimate=2 * MB if nvidia.is_available else 800 * KB,\n                extra_index_url=(\n                    \"https://download.pytorch.org/whl/cu121\"\n                    if nvidia.is_available\n                    else \"https://download.pytorch.org/whl/cpu\"\n                ),\n                auto_update=False,\n            ),\n        ]\n\n\npackage = add_package(\n    __file__,\n    id=\"chaiNNer_pytorch\",\n    name=\"PyTorch\",\n    description=package_description,\n    dependencies=[\n        *get_pytorch(),\n        Dependency(\n            display_name=\"FaceXLib\",\n            pypi_name=\"facexlib\",\n            version=\"0.3.0\",\n            size_estimate=59.6 * KB,\n        ),\n        Dependency(\n            display_name=\"Einops\",\n            pypi_name=\"einops\",\n            version=\"0.6.1\",\n            size_estimate=42.2 * KB,\n        ),\n        Dependency(\n            display_name=\"safetensors\",\n            pypi_name=\"safetensors\",\n            version=\"0.4.0\",\n            size_estimate=1 * MB,\n        ),\n        Dependency(\n            display_name=\"Spandrel\",\n            pypi_name=\"spandrel\",\n            version=\"0.3.4\",\n            size_estimate=264 * KB,\n        ),\n        Dependency(\n            display_name=\"Spandrel extra architectures\",\n            pypi_name=\"spandrel_extra_arches\",\n            version=\"0.1.1\",\n            size_estimate=83 * KB,\n        ),\n    ],\n    icon=\"PyTorch\",\n    color=\"#DD6B20\",\n)\n\npytorch_category = package.add_category(\n    name=\"PyTorch\",\n    description=\"Nodes for using the PyTorch Neural Network Framework with images.\",\n    icon=\"PyTorch\",\n    color=\"#DD6B20\",\n    install_hint=inst_hint,\n)\n\nlogger.debug(f\"Loaded package {package.name}\")\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/packages/chaiNNer_pytorch/pytorch/__init__.py",
    "content": "from .. import pytorch_category\n\nio_group = pytorch_category.add_node_group(\"Input & Output\")\nprocessing_group = pytorch_category.add_node_group(\"Processing\")\nrestoration_group = pytorch_category.add_node_group(\"Restoration\")\nbatch_processing_group = pytorch_category.add_node_group(\"Batch Processing\")\nutility_group = pytorch_category.add_node_group(\"Utility\")\n\nprocessing_group.order = [\n    \"chainner:pytorch:upscale_image\",\n    \"chainner:pytorch:inpaint\",\n]\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/packages/chaiNNer_pytorch/pytorch/io/load_model.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom pathlib import Path\n\nimport torch\nfrom nodes.properties.inputs import PthFileInput\nfrom nodes.properties.outputs import DirectoryOutput, FileNameOutput, ModelOutput\nfrom nodes.utils.utils import split_file_path\nfrom sanic.log import logger\nfrom spandrel import MAIN_REGISTRY, ModelDescriptor, ModelLoader\nfrom spandrel_extra_arches import EXTRA_REGISTRY\n\nfrom api import NodeContext\n\nfrom ...settings import get_settings\nfrom .. import io_group\n\nMAIN_REGISTRY.add(*EXTRA_REGISTRY)\n\n\ndef parse_ckpt_state_dict(checkpoint: dict):\n    state_dict = {}\n    for i, j in checkpoint.items():\n        if \"netG.\" in i:\n            key = i.replace(\"netG.\", \"\")\n            state_dict[key] = j\n        elif \"module.\" in i:\n            key = i.replace(\"module.\", \"\")\n            state_dict[key] = j\n    return state_dict\n\n\n@io_group.register(\n    schema_id=\"chainner:pytorch:load_model\",\n    name=\"Load Model\",\n    description=[\n        (\n            \"Load PyTorch state dict (.pth), TorchScript (.pt), or Checkpoint (.ckpt) files into an\"\n            \" auto-detected supported model architecture.\"\n        ),\n        (\n            \"- For Super-Resolution, we support most variations of the RRDB\"\n            \" architecture (ESRGAN, Real-ESRGAN, RealSR, BSRGAN, SPSR), Real-ESRGAN's\"\n            \" SRVGG architecture, Swift-SRGAN, SwinIR, Swin2SR, HAT, Omni-SR, SRFormer, and DAT.\"\n        ),\n        (\n            \"- For Face-Restoration, we support GFPGAN (1.2, 1.3, 1.4), RestoreFormer,\"\n            \" and CodeFormer.\"\n        ),\n        \"- For Inpainting, we support LaMa and MAT.\",\n        (\n            \"Links to the official models can be found in [chaiNNer's\"\n            \" README](https://github.com/chaiNNer-org/chaiNNer#pytorch), and\"\n            \" community-trained models on [OpenModelDB](https://openmodeldb.info/).\"\n        ),\n    ],\n    icon=\"PyTorch\",\n    inputs=[PthFileInput(primary_input=True)],\n    outputs=[\n        ModelOutput(kind=\"tagged\").suggest(),\n        DirectoryOutput(\"Directory\", of_input=0).with_id(2),\n        FileNameOutput(\"Name\", of_input=0).with_id(1),\n    ],\n    node_context=True,\n    see_also=[\n        \"chainner:pytorch:load_models\",\n    ],\n    side_effects=True,\n)\ndef load_model_node(\n    context: NodeContext, path: Path\n) -> tuple[ModelDescriptor, Path, str]:\n    assert os.path.exists(path), f\"Model file at location {path} does not exist\"\n\n    assert os.path.isfile(path), f\"Path {path} is not a file\"\n\n    exec_options = get_settings(context)\n    pytorch_device = exec_options.device\n\n    try:\n        logger.debug(f\"Reading state dict from path: {path}\")\n\n        model_descriptor = ModelLoader(pytorch_device).load_from_file(path)\n\n        for _, v in model_descriptor.model.named_parameters():\n            v.requires_grad = False\n        model_descriptor.model.eval()\n        model_descriptor = model_descriptor.to(pytorch_device)\n        # if should_use_fp16:\n        #     model_descriptor.model.half()\n        # else:\n        #     model_descriptor.model.float()\n        if exec_options.use_fp16:\n            if model_descriptor.supports_half:\n                model_descriptor.model.half()\n            elif torch.cuda.is_bf16_supported():\n                model_descriptor.model.bfloat16()\n            else:\n                model_descriptor.model.float()\n        else:\n            model_descriptor.model.float()\n    except Exception as e:\n        raise ValueError(\n            f\"Model {os.path.basename(path)} is unsupported by chaiNNer. Please try\"\n            \" another.\"\n        ) from e\n\n    dirname, basename, _ = split_file_path(path)\n    return model_descriptor, dirname, basename\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/packages/chaiNNer_pytorch/pytorch/processing/upscale_image.py",
    "content": "from __future__ import annotations\n\nimport weakref\n\nimport numpy as np\nimport psutil\nimport torch\nfrom accelerator_detection import AcceleratorType\nfrom nodes.groups import Condition, if_enum_group, if_group\nfrom nodes.impl.pytorch.auto_split import pytorch_auto_split\nfrom nodes.impl.pytorch.utils import safe_accelerator_cache_empty\nfrom nodes.impl.upscale.auto_split_tiles import (\n    CUSTOM,\n    NO_TILING,\n    TILE_SIZE_256,\n    TileSize,\n    estimate_tile_size,\n    parse_tile_size_input,\n)\nfrom nodes.impl.upscale.basic_upscale import UpscaleInfo, basic_upscale\nfrom nodes.impl.upscale.tiler import MaxTileSize\nfrom nodes.properties.inputs import (\n    BoolInput,\n    ImageInput,\n    NumberInput,\n    SrModelInput,\n    TileSizeDropdown,\n)\nfrom nodes.properties.outputs import ImageOutput\nfrom sanic.log import logger\nfrom spandrel import ImageModelDescriptor, ModelTiling\n\nfrom api import KeyInfo, NodeContext, Progress\n\nfrom ...settings import PyTorchSettings, get_settings\nfrom .. import processing_group\n\nMODEL_BYTES_CACHE = weakref.WeakKeyDictionary()\n\n\ndef upscale(\n    img: np.ndarray,\n    model: ImageModelDescriptor,\n    tile_size: TileSize,\n    options: PyTorchSettings,\n    progress: Progress,\n):\n    with torch.no_grad():\n        # Borrowed from iNNfer\n        logger.debug(\"Upscaling image\")\n\n        # TODO: use bfloat16 if RTX\n        use_fp16 = options.use_fp16  # and model.supports_half\n        device = options.device\n\n        if model.tiling == ModelTiling.INTERNAL:\n            # disable tiling if the model already does it internally\n            tile_size = NO_TILING\n\n        def estimate():\n            model_bytes = MODEL_BYTES_CACHE.get(model)\n            if model_bytes is None:\n                model_bytes = sum(p.numel() * 4 for p in model.model.parameters())\n                MODEL_BYTES_CACHE[model] = model_bytes\n\n            device_type = device.type\n            accelerator_device = options.accelerator_device\n\n            # Memory estimation for different accelerator types\n            if device_type in [\"cuda\", \"rocm\"]:  # CUDA/ROCm\n                if options.use_fp16:\n                    model_bytes = model_bytes // 2\n                try:\n                    mem_info: tuple[int, int] = torch.cuda.mem_get_info(device)  # type: ignore\n                    _free, total = mem_info\n                    # only use 75% of the total memory\n                    total = int(total * 0.75)\n                    if options.budget_limit > 0:\n                        total = min(options.budget_limit * 1024**3, total)\n                    # Estimate using 80% of the value to be more conservative\n                    budget = int(total * 0.8)\n\n                    return MaxTileSize(\n                        estimate_tile_size(\n                            budget,\n                            model_bytes,\n                            img,\n                            2 if use_fp16 else 4,\n                        )\n                    )\n                except Exception:\n                    # Fallback if memory info fails\n                    return MaxTileSize()\n            elif device_type == \"xpu\":  # Intel XPU\n                if options.use_fp16:\n                    model_bytes = model_bytes // 2\n                try:\n                    if hasattr(torch.xpu, 'mem_get_info'):\n                        mem_info = torch.xpu.mem_get_info(device)\n                        _free, total = mem_info\n                        total = int(total * 0.75)\n                        if options.budget_limit > 0:\n                            total = min(options.budget_limit * 1024**3, total)\n                        budget = int(total * 0.8)\n                        return MaxTileSize(\n                            estimate_tile_size(\n                                budget,\n                                model_bytes,\n                                img,\n                                2 if use_fp16 else 4,\n                            )\n                        )\n                except Exception:\n                    pass\n                # Fallback for XPU without memory info\n                return MaxTileSize()\n            elif device_type == \"mps\":  # Apple MPS\n                # MPS doesn't have direct memory querying, use conservative estimation\n                # Assume 8GB unified memory with 50% available for inference\n                estimated_budget = 4 * 1024**3  # 4GB conservative estimate\n                if options.budget_limit > 0:\n                    estimated_budget = min(options.budget_limit * 1024**3, estimated_budget)\n                budget = int(estimated_budget * 0.8)\n                return MaxTileSize(\n                    estimate_tile_size(\n                        budget,\n                        model_bytes,\n                        img,\n                        2 if use_fp16 else 4,\n                    )\n                )\n            elif device_type == \"cpu\":\n                free = psutil.virtual_memory().available\n                if options.budget_limit > 0:\n                    free = min(options.budget_limit * 1024**3, free)\n                budget = int(free * 0.8)\n                return MaxTileSize(\n                    estimate_tile_size(\n                        budget,\n                        model_bytes,\n                        img,\n                        4,  # CPU always uses FP32\n                    )\n                )\n            else:\n                # For other device types, use conservative estimation\n                estimated_budget = 2 * 1024**3  # 2GB conservative estimate\n                if options.budget_limit > 0:\n                    estimated_budget = min(options.budget_limit * 1024**3, estimated_budget)\n                budget = int(estimated_budget * 0.8)\n                return MaxTileSize(\n                    estimate_tile_size(\n                        budget,\n                        model_bytes,\n                        img,\n                        2 if use_fp16 else 4,\n                    )\n                )\n\n        img_out = pytorch_auto_split(\n            img,\n            model=model,\n            device=device,\n            use_fp16=use_fp16,\n            tiler=parse_tile_size_input(tile_size, estimate),\n            progress=progress,\n        )\n        logger.debug(\"Done upscaling\")\n\n        return img_out\n\n\n@processing_group.register(\n    schema_id=\"chainner:pytorch:upscale_image\",\n    name=\"Upscale Image\",\n    description=(\n        \"Upscales an image using a PyTorch Super-Resolution model. Select a\"\n        \" manual number of tiles if you are having issues with the automatic mode. \"\n    ),\n    icon=\"PyTorch\",\n    inputs=[\n        ImageInput().with_id(1),\n        SrModelInput().with_id(0),\n        if_group(\n            Condition.type(0, \"PyTorchModel { scale: int(2..) }\", if_not_connected=True)\n            & (\n                Condition.type(\n                    0,\n                    \"PyTorchModel { inputChannels: 1, outputChannels: 1 }\",\n                    if_not_connected=True,\n                )\n                | Condition.type(\n                    0, \"PyTorchModel { inputChannels: 3, outputChannels: 3 }\"\n                )\n                | Condition.type(\n                    0, \"PyTorchModel { inputChannels: 4, outputChannels: 4 }\"\n                )\n            )\n        )(\n            BoolInput(\"Custom Scale\", default=False)\n            .with_id(4)\n            .with_docs(\n                \"If enabled, the scale factor can be manually set. This makes it possible to e.g. upscale 4x with a 2x model.\",\n                \"Custom scales are **not** supported for 1x models and colorization models.\",\n                \"Under the hood, this will repeatedly apply the model to the image, effectively upscaling by the given factor.\"\n                \" E.g. if the model is 2x and the desired scale is 4x, the model will be applied 2 times.\"\n                \" If the desired scale cannot be reached exactly, the image will be downscaled to the desired scale after upscaling.\"\n                \" 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.\",\n                \"If the desired scale is less than the model's scale, the image will be downscaled to the desired scale after upscaling.\",\n                hint=True,\n            ),\n            if_group(Condition.bool(4, True))(\n                NumberInput(\n                    \"Scale\", default=4, min=1, max=32, label_style=\"hidden\"\n                ).with_id(5),\n            ),\n        ),\n        if_group(\n            Condition.type(\n                0,\n                \"PyTorchModel { tiling: ModelTiling::Supported | ModelTiling::Discouraged } \",\n                if_not_connected=True,\n            )\n        )(\n            TileSizeDropdown()\n            .with_id(2)\n            .with_docs(\n                \"Tiled upscaling is used to allow large images to be upscaled without\"\n                \" hitting memory limits.\",\n                \"This works by splitting the image into tiles (with overlap), upscaling\"\n                \" each tile individually, and seamlessly recombining them.\",\n                \"Generally it's recommended to use the largest tile size possible for\"\n                \" best performance (with the ideal scenario being no tiling at all),\"\n                \" but depending on the model and image size, this may not be possible.\",\n                \"If you are having issues with the automatic mode, you can manually\"\n                \" select a tile size. Sometimes, a manually selected tile size may be\"\n                \" faster than what the automatic mode picks.\",\n                hint=True,\n            ),\n            if_enum_group(2, CUSTOM)(\n                NumberInput(\n                    \"Custom Tile Size\",\n                    min=1,\n                    max=None,\n                    default=TILE_SIZE_256,\n                    unit=\"px\",\n                ).with_id(6),\n            ),\n        ),\n        if_group(\n            Condition.type(1, \"Image { channels: 4 } \")\n            & (\n                Condition.type(\n                    0, \"PyTorchModel { inputChannels: 1, outputChannels: 1 }\"\n                )\n                | Condition.type(\n                    0, \"PyTorchModel { inputChannels: 3, outputChannels: 3 }\"\n                )\n            )\n        )(\n            BoolInput(\"Separate Alpha\", default=False)\n            .with_id(3)\n            .with_docs(\n                \"Upscale alpha separately from color. Enabling this option will cause the alpha of\"\n                \" the upscaled image to be less noisy and more accurate to the alpha of the original\"\n                \" image, but the image may suffer from dark borders near transparency edges\"\n                \" (transition from fully transparent to fully opaque).\",\n                \"Whether enabling this option will improve the upscaled image depends on the original\"\n                \" image. We generally recommend this option for images with smooth transitions between\"\n                \" transparent and opaque regions.\",\n            )\n        ),\n    ],\n    outputs=[\n        ImageOutput(\n            \"Image\",\n            image_type=\"\"\"\n                let img = Input1;\n                let model = Input0;\n                let useCustomScale = Input4;\n                let customScale = Input5;\n\n                let singleUpscale = convenientUpscale(model, img);\n\n                if useCustomScale and model.scale >= 2 and model.inputChannels == model.outputChannels {\n                    Image {\n                        width: img.width * customScale,\n                        height: img.height * customScale,\n                        channels: singleUpscale.channels,\n                    }\n                } else {\n                    singleUpscale\n                }\n            \"\"\",\n            assume_normalized=True,  # pytorch_auto_split already does clipping internally\n        )\n    ],\n    key_info=KeyInfo.type(\n        \"\"\"\n        let model = Input0;\n        let useCustomScale = Input4;\n        let customScale = Input5;\n\n        let singleUpscale = convenientUpscale(model, img);\n\n        let scale = if useCustomScale and model.scale >= 2 and model.inputChannels == model.outputChannels {\n            customScale\n        } else {\n            model.scale\n        };\n\n        string::concat(toString(scale), \"x\")\n        \"\"\"\n    ),\n    node_context=True,\n)\ndef upscale_image_node(\n    context: NodeContext,\n    img: np.ndarray,\n    model: ImageModelDescriptor,\n    use_custom_scale: bool,\n    custom_scale: int,\n    tile_size: TileSize,\n    custom_tile_size: int,\n    separate_alpha: bool,\n) -> np.ndarray:\n    exec_options = get_settings(context)\n\n    context.add_cleanup(\n        lambda: safe_accelerator_cache_empty(exec_options.device),\n        after=\"node\" if exec_options.force_cache_wipe else \"chain\",\n    )\n\n    info = UpscaleInfo(\n        in_nc=model.input_channels, out_nc=model.output_channels, scale=model.scale\n    )\n    if not use_custom_scale or not info.supports_custom_scale:\n        custom_scale = model.scale\n\n    return basic_upscale(\n        img,\n        lambda i: upscale(\n            i,\n            model,\n            TileSize(custom_tile_size) if tile_size == CUSTOM else tile_size,\n            exec_options,\n            context,\n        ),\n        upscale_info=info,\n        scale=custom_scale,\n        separate_alpha=separate_alpha,\n        clip=False,  # pytorch_auto_split already does clipping internally\n    )\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/packages/chaiNNer_pytorch/settings.py",
    "content": "from dataclasses import dataclass\n\nimport torch\nfrom accelerator_detection import AcceleratorType, get_accelerator_detector\nfrom gpu import nvidia\nfrom sanic.log import logger\nfrom system import is_arm_mac\n\nfrom api import DropdownSetting, NodeContext, NumberSetting, ToggleSetting\n\nfrom . import package\n\n# Get all available accelerator devices\ndetector = get_accelerator_detector()\nall_devices = detector.available_devices\n\n# Create device options, excluding CPU for the dropdown\ngpu_devices = [device for device in all_devices if device.type != AcceleratorType.CPU]\n\nif gpu_devices:\n    package.add_setting(\n        DropdownSetting(\n            label=\"Accelerator Device\",\n            key=\"accelerator_device_index\",\n            description=(\n                \"Which accelerator device to use for PyTorch. This includes NVIDIA CUDA, \"\n                \"AMD ROCm, Intel XPU, Apple MPS, and other supported accelerators.\"\n            ),\n            options=[{\n                \"label\": f\"{device.name} ({device.type.value.upper()}:{device.index})\", \n                \"value\": str(i)\n            } for i, device in enumerate(gpu_devices)],\n            default=\"0\",\n        )\n    )\n\n# Legacy GPU index setting for backward compatibility (CUDA only)\nif not is_arm_mac:\n    cuda_devices = detector.get_devices_by_type(AcceleratorType.CUDA)\n    if cuda_devices:\n        package.add_setting(\n            DropdownSetting(\n                label=\"CUDA GPU (Legacy)\",\n                key=\"gpu_index\",\n                description=(\n                    \"Which CUDA GPU to use for PyTorch. This setting is deprecated - \"\n                    \"use 'Accelerator Device' instead for full accelerator support.\"\n                ),\n                options=[{\"label\": device.name, \"value\": str(device.index)} for device in cuda_devices],\n                default=\"0\",\n            )\n        )\n\npackage.add_setting(\n    ToggleSetting(\n        label=\"Use CPU Mode\",\n        key=\"use_cpu\",\n        description=(\n            \"Use CPU for PyTorch instead of accelerator devices. This is much slower \"\n            \"and not recommended unless you have no compatible accelerator.\"\n        ),\n        default=False,\n    ),\n)\n\n# Determine default FP16 setting based on available devices\nshould_fp16 = False\ngpu_devices = [device for device in all_devices if device.type != AcceleratorType.CPU]\nif gpu_devices:\n    # Enable FP16 by default if any GPU supports it\n    should_fp16 = any(device.supports_fp16 for device in gpu_devices)\nelif nvidia.is_available:\n    should_fp16 = nvidia.all_support_fp16\nelse:\n    should_fp16 = is_arm_mac\n\npackage.add_setting(\n    ToggleSetting(\n        label=\"Use FP16 Mode\",\n        key=\"use_fp16\",\n        description=(\n            \"Runs PyTorch in half-precision (FP16) mode for reduced memory usage. \"\n            \"Automatically falls back to bfloat16 or FP32 when FP16 is not supported. \"\n            \"Falls back to full-precision (FP32) mode when CPU mode is selected.\"\n        ),\n        default=should_fp16,\n    ),\n)\n\npackage.add_setting(\n    NumberSetting(\n        label=\"Memory Budget Limit (GiB)\",\n        key=\"budget_limit\",\n        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.\",\n        default=0,\n        min=0,\n        max=1024**2,\n    )\n)\n\n# Add cache wipe setting for accelerator types that support it\nhas_accelerator_with_cache = any(\n    device.type in [AcceleratorType.CUDA, AcceleratorType.ROCM, AcceleratorType.XPU] \n    for device in all_devices\n)\n\nif has_accelerator_with_cache:\n    package.add_setting(\n        ToggleSetting(\n            label=\"Force Accelerator Cache Wipe (not recommended)\",\n            key=\"force_cache_wipe\",\n            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.\",\n            default=False,\n        )\n    )\n\n\n@dataclass(frozen=True)\nclass PyTorchSettings:\n    use_cpu: bool\n    use_fp16: bool\n    gpu_index: int  # Legacy CUDA index\n    accelerator_device_index: int  # New unified accelerator index\n    budget_limit: int\n    force_cache_wipe: bool = False\n\n    # PyTorch 2.7 does not support FP16 when using CPU\n    def __post_init__(self):\n        if self.use_cpu and self.use_fp16:\n            object.__setattr__(self, \"use_fp16\", False)\n            logger.info(\"Falling back to FP32 mode for CPU.\")\n\n    @property\n    def device(self) -> torch.device:\n        \"\"\"Get the appropriate torch device\"\"\"\n        detector = get_accelerator_detector()\n        \n        # CPU override\n        if self.use_cpu:\n            return torch.device(\"cpu\")\n        \n        # Try to use the new accelerator device index first\n        gpu_devices = [device for device in detector.available_devices if device.type != AcceleratorType.CPU]\n        \n        if gpu_devices and 0 <= self.accelerator_device_index < len(gpu_devices):\n            selected_device = gpu_devices[self.accelerator_device_index]\n            return selected_device.torch_device\n        \n        # Fallback to legacy CUDA device selection for backward compatibility\n        cuda_devices = detector.get_devices_by_type(AcceleratorType.CUDA)\n        if cuda_devices and 0 <= self.gpu_index < len(cuda_devices):\n            return torch.device(f\"cuda:{self.gpu_index}\")\n        \n        # Fallback to best available device\n        best_device = detector.get_best_device(prefer_gpu=True)\n        if best_device.type != AcceleratorType.CPU:\n            return best_device.torch_device\n        \n        # Final fallback to CPU\n        return torch.device(\"cpu\")\n\n    @property\n    def accelerator_device(self) -> 'AcceleratorDevice':\n        \"\"\"Get the selected accelerator device info\"\"\"\n        detector = get_accelerator_detector()\n        \n        if self.use_cpu:\n            return detector.get_cpu_device()\n        \n        # Try to use the new accelerator device index first\n        gpu_devices = [device for device in detector.available_devices if device.type != AcceleratorType.CPU]\n        \n        if gpu_devices and 0 <= self.accelerator_device_index < len(gpu_devices):\n            return gpu_devices[self.accelerator_device_index]\n        \n        # Fallback to legacy CUDA device selection\n        cuda_devices = detector.get_devices_by_type(AcceleratorType.CUDA)\n        if cuda_devices and 0 <= self.gpu_index < len(cuda_devices):\n            return cuda_devices[self.gpu_index]\n        \n        # Fallback to best available device\n        return detector.get_best_device(prefer_gpu=True)\n\n\ndef get_settings(context: NodeContext) -> PyTorchSettings:\n    settings = context.settings\n\n    return PyTorchSettings(\n        use_cpu=settings.get_bool(\"use_cpu\", False),\n        use_fp16=settings.get_bool(\"use_fp16\", False),\n        gpu_index=settings.get_int(\"gpu_index\", 0, parse_str=True),\n        accelerator_device_index=settings.get_int(\"accelerator_device_index\", 0, parse_str=True),\n        budget_limit=settings.get_int(\"budget_limit\", 0, parse_str=True),\n        force_cache_wipe=settings.get_bool(\"force_cache_wipe\", False),\n    )\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/progress_controller.py",
    "content": "import asyncio\nimport time\nfrom abc import ABC, abstractmethod\n\n\nclass Aborted(Exception):\n    pass\n\n\nclass ProgressToken(ABC):\n    @property\n    @abstractmethod\n    def paused(self) -> bool:\n        pass\n\n    @property\n    @abstractmethod\n    def aborted(self) -> bool:\n        pass\n\n    @abstractmethod\n    async def suspend(self) -> None:\n        \"\"\"\n        If the operation was aborted, this method will throw an `Aborted` exception.\n        If the operation is paused, this method will wait until the operation is resumed or aborted.\n        \"\"\"\n\n\nclass ProgressController(ProgressToken):\n    def __init__(self) -> None:\n        self.__paused: bool = False\n        self.__aborted: bool = False\n\n        self.time_paused: float = 0\n        \"\"\"\n        The amount of time spend paused in seconds.\n\n        Only time spend during `suspend` is counted.\n        \"\"\"\n\n    @property\n    def paused(self) -> bool:\n        return self.__paused\n\n    @property\n    def aborted(self) -> bool:\n        return self.__aborted\n\n    def pause(self) -> None:\n        self.__paused = True\n\n    def resume(self) -> None:\n        self.__paused = False\n\n    def abort(self) -> None:\n        self.__aborted = True\n\n    async def suspend(self) -> None:\n        if self.aborted:\n            raise Aborted()\n\n        if self.paused:\n            start = time.monotonic()\n            try:\n                while self.paused:\n                    await asyncio.sleep(0.1)\n                    if self.aborted:\n                        raise Aborted()\n            finally:\n                self.time_paused += time.monotonic() - start\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/pyproject.toml",
    "content": "[project]\nname = \"mangajanaiconvertergui\"\ndynamic = [\"version\"]\ndependencies = [\n  \"chainner_ext==0.3.10\",\n  \"numpy==2.2.5\",\n  \"opencv-python==4.11.0.86\",\n  \"packaging==25.0\",\n  \"psutil==6.0.0\",\n  \"pynvml==11.5.3\",\n  \"pyvips==3.0.0\",\n  \"pyvips-binary==8.16.1\",\n  \"rarfile==4.2\",\n  \"sanic==24.6.0\",\n  \"spandrel_extra_arches==0.2.0\",\n  \"spandrel==0.4.1\",\n  \"torch==2.9.1\",\n  \"torchvision==0.24.1\",\n]\nauthors = [{name = \"the-database\"}]\ndescription = \"Upscaling manga images and archives with PyTorch models.\"\nreadme = \"README.md\"\nlicense = {file = \"LICENSE.txt\"}\nkeywords = []\n\n[project.optional-dependencies]\ndev = [\"ruff\", \"pyright\", \"pytest\"]\n\n[project.urls]\nRepository = \"https://github.com/the-database/MangaJaNaiConverterGui.git\"\n\n[tool.setuptools.packages.find]\nwhere = [\".\"]  # list of folders that contain the packages ([\".\"] by default)\ninclude = [\"*\"]  # package names should match these glob patterns ([\"*\"] by default)\nexclude = []  # exclude packages matching these glob patterns (empty by default)\nnamespaces = false  # to disable scanning PEP 420 namespaces (true by default)\n\n[tool.ruff]\n# Same as Black.\nline-length = 88\nindent-width = 4\n\nsrc = [\"*\"]\n\nunsafe-fixes = true\n\n[tool.ruff.lint]\n# Add the `line-too-long` rule to the enforced rule set.\nextend-select = [\n    \"UP\", # pyupgrade\n    \"E\",  # pycodestyle\n    \"W\",  # pycodestyle\n    \"F\",  # pyflakes\n    \"I\",  # isort\n    \"N\",  # pep8-naming\n    # \"ANN\", # flake8-annotations\n    \"ANN001\",\n    \"ANN002\",\n    \"ANN201\",\n    \"ANN202\",\n    \"ANN204\",\n    \"ANN205\",\n    \"ANN206\",\n    # \"ASYNC\", # flake8-async\n    \"PL\",  # pylint\n    \"RUF\", # ruff\n    \"B\",   # flake8-bugbear\n    # \"A\",   # flake8-builtins\n    # \"COM\", # flake8-commas\n    \"C4\",  # flake8-comprehensions\n    \"FA\",  # flake8-future-annotations\n    \"ISC\", # flake8-implicit-str-concat\n    \"ICN\", # flake8-import-conventions\n    \"G\",   # flake8-logging-format\n    # \"INP\", # flake8-implicit-namespaces\n    \"PIE\", # flake8-pie\n    # \"PYI\", # flake8-pyi\n    \"Q\", # flake8-quotes\n    # \"RET\", # flake8-return\n    \"SLF\", # flake8-self\n    # \"SIM\", # flake8-simplify\n    # \"TCH\", # flake8-tidy-imports\n    \"NPY\", # NumPy-specific rules\n    \"NPY201\", # numpy2-deprecation\n]\nignore = [\n    \"E501\",    # Line too long\n    \"PLR2004\", # Magic value\n    \"PLR0911\", # Too many return statements\n    \"PLR0912\", # Too many branches\n    \"PLR0913\", # Too many arguments\n    \"PLR0915\", # Too many statements,\n    \"E741\",    # Ambiguous variable name,\n    \"E712\",    # true-false-comparison, has false positives because of numpy's operator overloading\n    \"F821\",    # Undefined name -- this one is weird, it seems like it has false positives on closures and other context changes\n    \"F403\",    # 'from module import *' used; unable to detect undefined names\n    \"PLW0603\", # Using the global statement\n    \"N999\",    # Invalid module name (which triggers for chaiNNer)\n    \"N818\",    # Exception name should end in Error\n    \"ISC001\",  # Implicit string concatenation, conflicts with formatter\n]\n\n[tool.ruff.format]\n# Like Black, use double quotes for strings.\nquote-style = \"double\"\n\n# Like Black, indent with spaces, rather than tabs.\nindent-style = \"space\"\n\n# Like Black, respect magic trailing commas.\nskip-magic-trailing-comma = false\n\n# Like Black, automatically detect the appropriate line ending.\nline-ending = \"auto\"\n\n[tool.uv.pip]\nextra-index-url = [\"https://download.pytorch.org/whl/cu121\"]"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/pyrightconfig.json",
    "content": "{\n\t\"include\": [\n\t\t// \"traiNNer\"\n\t],\n\t\"exclude\": [\n\t\t\"**/__pycache__\"\n\t],\n\t\"ignore\": [],\n\t\"typeCheckingMode\": \"standard\",\n\t\"useLibraryCodeForTypes\": true,\n\t\"strictListInference\": true,\n\t\"strictDictionaryInference\": true,\n\t\"strictSetInference\": true,\n\t\"reportDuplicateImport\": \"warning\",\n\t\"reportImportCycles\": \"error\",\n\t\"reportIncompatibleVariableOverride\": \"error\",\n\t\"reportIncompatibleMethodOverride\": \"error\",\n\t\"reportOverlappingOverload\": \"error\",\n\t\"reportPrivateImportUsage\": \"error\",\n\t\"reportUninitializedInstanceVariable\": \"error\",\n\t\"reportUnnecessaryCast\": \"error\",\n\t\"reportUnnecessaryComparison\": \"error\",\n\t\"reportUnnecessaryContains\": \"error\",\n\t\"reportUnnecessaryIsInstance\": \"error\",\n\t\"reportUnusedClass\": \"warning\",\n\t\"reportUnusedFunction\": \"warning\",\n\t\"reportUnusedImport\": \"warning\",\n\t\"reportUnusedVariable\": \"warning\",\n}\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/run_upscale.py",
    "content": "import argparse\nimport ctypes\nimport io\nimport json\nimport os\nimport platform\nimport sys\nimport time\nfrom collections.abc import Callable\nfrom io import BytesIO\nfrom pathlib import Path\nfrom queue import Queue\nfrom multiprocessing import Queue as MPQueue, Process\nfrom threading import Thread\nfrom typing import Any, Literal\nfrom zipfile import ZipFile, ZIP_DEFLATED\n\nimport cv2\nimport numpy as np\nimport pyvips\nimport rarfile\nfrom chainner_ext import ResizeFilter, resize\nfrom cv2.typing import MatLike\nfrom PIL import Image, ImageCms, ImageFilter\nfrom PIL.Image import Image as ImageType\nfrom PIL.ImageCms import ImageCmsProfile\nfrom rarfile import RarFile\nfrom spandrel import ImageModelDescriptor, ModelDescriptor\n\nsys.path.append(os.path.normpath(os.path.dirname(os.path.abspath(__file__))))\n\nimport spandrel_custom\nfrom nodes.impl.image_utils import normalize, to_uint8, to_uint16\nfrom nodes.impl.upscale.auto_split_tiles import (\n    ESTIMATE,\n    MAX_TILE_SIZE,\n    NO_TILING,\n    TileSize,\n)\nfrom nodes.utils.utils import get_h_w_c\nfrom packages.chaiNNer_pytorch.pytorch.io.load_model import load_model_node\nfrom packages.chaiNNer_pytorch.pytorch.processing.upscale_image import (\n    upscale_image_node,\n)\nfrom progress_controller import ProgressController, ProgressToken\n\nfrom api import (\n    NodeContext,\n    SettingsParser,\n)\n\n\nclass _ExecutorNodeContext(NodeContext):\n    def __init__(\n        self, progress: ProgressToken, settings: SettingsParser, storage_dir: Path\n    ) -> None:\n        super().__init__()\n\n        self.progress = progress\n        self.__settings = settings\n        self._storage_dir = storage_dir\n\n        self.chain_cleanup_fns: set[Callable[[], None]] = set()\n        self.node_cleanup_fns: set[Callable[[], None]] = set()\n\n    @property\n    def aborted(self) -> bool:\n        return self.progress.aborted\n\n    @property\n    def paused(self) -> bool:\n        time.sleep(0.001)\n        return self.progress.paused\n\n    def set_progress(self, progress: float) -> None:\n        self.check_aborted()\n\n        # TODO: send progress event\n\n    @property\n    def settings(self) -> SettingsParser:\n        \"\"\"\n        Returns the settings of the current node execution.\n        \"\"\"\n        return self.__settings\n\n    @property\n    def storage_dir(self) -> Path:\n        return self._storage_dir\n\n    def add_cleanup(\n        self, fn: Callable[[], None], after: Literal[\"node\", \"chain\"] = \"chain\"\n    ) -> None:\n        if after == \"chain\":\n            self.chain_cleanup_fns.add(fn)\n        elif after == \"node\":\n            self.node_cleanup_fns.add(fn)\n        else:\n            raise ValueError(f\"Unknown cleanup type: {after}\")\n\n\ndef get_tile_size(tile_size_str: str) -> TileSize:\n    if tile_size_str == \"Auto (Estimate)\":\n        return ESTIMATE\n    elif tile_size_str == \"Maximum\":\n        return MAX_TILE_SIZE\n    elif tile_size_str == \"No Tiling\":\n        return NO_TILING\n    elif tile_size_str.isdecimal():\n        return TileSize(int(tile_size_str))\n\n    return ESTIMATE\n\n\n\"\"\"\nlanczos downscale without color conversion, for pre-upscale\ndownscale and final color downscale\n\"\"\"\n\n\ndef standard_resize(image: np.ndarray, new_size: tuple[int, int]) -> np.ndarray:\n    new_image = image.astype(np.float32) / 255.0\n    new_image = resize(new_image, new_size, ResizeFilter.Lanczos, False)\n    new_image = (new_image * 255).round().astype(np.uint8)\n\n    _, _, c = get_h_w_c(image)\n\n    if c == 1 and new_image.ndim == 3:\n        new_image = np.squeeze(new_image, axis=-1)\n\n    return new_image\n\n\n\"\"\"\nfinal downscale for grayscale images only\n\"\"\"\n\n\ndef dotgain20_resize(image: np.ndarray, new_size: tuple[int, int]) -> np.ndarray:\n    h, _, c = get_h_w_c(image)\n    size_ratio = h / new_size[1]\n    blur_size = (1 / size_ratio - 1) / 3.5\n    if blur_size >= 0.1:\n        blur_size = min(blur_size, 250)\n\n    pil_image = Image.fromarray(image, mode=\"L\")\n    pil_image = pil_image.filter(ImageFilter.GaussianBlur(radius=blur_size))\n    pil_image = ImageCms.applyTransform(pil_image, dotgain20togamma1transform, False)\n\n    new_image = np.array(pil_image)\n    new_image = new_image.astype(np.float32) / 255.0\n    new_image = resize(new_image, new_size, ResizeFilter.CubicCatrom, False)\n    new_image = (new_image * 255).round().astype(np.uint8)\n\n    pil_image = Image.fromarray(new_image[:, :, 0], mode=\"L\")\n    pil_image = ImageCms.applyTransform(pil_image, gamma1todotgain20transform, False)\n    return np.array(pil_image)\n\n\ndef image_resize(\n    image: np.ndarray, new_size: tuple[int, int], is_grayscale: bool\n) -> np.ndarray:\n    if is_grayscale:\n        return dotgain20_resize(image, new_size)\n\n    return standard_resize(image, new_size)\n\n\ndef get_system_codepage() -> Any:\n    return None if not is_windows else ctypes.windll.kernel32.GetConsoleOutputCP()\n\n\ndef enhance_contrast(image: np.ndarray) -> MatLike:\n    image_p = Image.fromarray(image).convert(\"L\")\n\n    # Calculate the histogram\n    hist = image_p.histogram()\n    # print(hist)\n\n    # Find the global maximum peak in the range 0-30 for the black level\n    new_black_level = 0\n    global_max_black = hist[0]\n\n    for i in range(1, 31):\n        if hist[i] > global_max_black:\n            global_max_black = hist[i]\n            new_black_level = i\n        # elif hist[i] < global_max_black:\n        #     break\n\n    # Continue searching at 31 and later for the black level\n    continuous_count = 0\n    for i in range(31, 256):\n        if hist[i] > global_max_black:\n            continuous_count = 0\n            global_max_black = hist[i]\n            new_black_level = i\n        elif hist[i] < global_max_black:\n            continuous_count += 1\n            if continuous_count > 1:\n                break\n\n    # Find the global maximum peak in the range 255-225 for the white level\n    new_white_level = 255\n    global_max_white = hist[255]\n\n    for i in range(254, 224, -1):\n        if hist[i] > global_max_white:\n            global_max_white = hist[i]\n            new_white_level = i\n        # elif hist[i] < global_max_white:\n        #     break\n\n    # Continue searching at 224 and below for the white level\n    continuous_count = 0\n    for i in range(223, -1, -1):\n        if hist[i] > global_max_white:\n            continuous_count = 0\n            global_max_white = hist[i]\n            new_white_level = i\n        elif hist[i] < global_max_white:\n            continuous_count += 1\n            if continuous_count > 1:\n                break\n\n    print(\n        f\"Auto adjusted levels: new black level = {new_black_level}; new white level = {new_white_level}\",\n        flush=True,\n    )\n\n    image_array = np.array(image_p).astype(\"float32\")\n    image_array = np.maximum(image_array - new_black_level, 0) / (\n        new_white_level - new_black_level\n    )\n    return np.clip(image_array, 0, 1)\n\n\ndef _read_image(img_stream: bytes, filename: str) -> np.ndarray:\n    return _read_vips(img_stream)\n\n\ndef _read_image_from_path(path: str) -> np.ndarray:\n    return pyvips.Image.new_from_file(path, access=\"sequential\", fail=True).icc_transform(\"srgb\").numpy()\n\n\ndef _read_vips(img_stream: bytes) -> np.ndarray:\n    return pyvips.Image.new_from_buffer(img_stream, \"\", access=\"sequential\").icc_transform(\"srgb\").numpy()\n\n\ndef cv_image_is_grayscale(image: np.ndarray, user_threshold: float) -> bool:\n    _, _, c = get_h_w_c(image)\n\n    if c == 1:\n        return True\n\n    b, g, r = cv2.split(image[:, :, :3])\n\n    ignore_threshold = user_threshold\n\n    # getting differences between (b,g), (r,g), (b,r) channel pixels\n    r_g = cv2.subtract(cv2.absdiff(r, g), ignore_threshold)  # type: ignore\n    r_b = cv2.subtract(cv2.absdiff(r, b), ignore_threshold)  # type: ignore\n    g_b = cv2.subtract(cv2.absdiff(g, b), ignore_threshold)  # type: ignore\n\n    # create masks to identify pure black and pure white pixels\n    pure_black_mask = np.logical_and.reduce((r == 0, g == 0, b == 0))\n    pure_white_mask = np.logical_and.reduce((r == 255, g == 255, b == 255))\n\n    # combine masks to exclude both pure black and pure white pixels\n    exclude_mask = np.logical_or(pure_black_mask, pure_white_mask)\n\n    # exclude pure black and pure white pixels from diff_sum and image size calculation\n    diff_sum = np.sum(np.where(exclude_mask, 0, r_g + r_b + g_b))\n    size_without_black_and_white = np.sum(~exclude_mask) * 3\n\n    # if the entire image is pure black or pure white, return False\n    if size_without_black_and_white == 0:\n        return False\n\n    # finding ratio of diff_sum with respect to size of image without pure black and pure white pixels\n    ratio = diff_sum / size_without_black_and_white\n\n    return ratio <= user_threshold / 12\n\n\ndef convert_image_to_grayscale(image: np.ndarray) -> np.ndarray:\n    channels = get_h_w_c(image)[2]\n    if channels == 3:\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    elif channels == 4:\n        image = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)\n\n    return image\n\n\ndef get_chain_for_image(\n    image: np.ndarray,\n    target_scale: float | None,\n    target_width: int,\n    target_height: int,\n    chains: list[dict[str, Any]],\n    grayscale_detection_threshold: int,\n) -> tuple[dict[str, Any], bool, int, int] | tuple[None, None, int, int]:\n    original_height, original_width, _ = get_h_w_c(image)\n\n    if target_width != 0 and target_height != 0:\n        target_scale = min(\n            target_height / original_height, target_width / original_width\n        )\n    if target_height != 0:\n        target_scale = target_height / original_height\n    elif target_width != 0:\n        target_scale = target_width / original_width\n\n    assert target_scale is not None\n\n    is_grayscale = cv_image_is_grayscale(image, grayscale_detection_threshold)\n\n    for chain in chains:\n        if should_chain_activate_for_image(\n            original_width, original_height, is_grayscale, target_scale, chain\n        ):\n            print(\"Matched Chain:\", chain, flush=True)\n            return chain, is_grayscale, original_width, original_height\n\n    return None, None, original_width, original_height\n\n\ndef should_chain_activate_for_image(\n    original_width: int,\n    original_height: int,\n    is_grayscale: bool,\n    target_scale: float,\n    chain: dict[str, Any],\n) -> bool:\n    min_width, min_height = (int(x) for x in chain[\"MinResolution\"].split(\"x\"))\n    max_width, max_height = (int(x) for x in chain[\"MaxResolution\"].split(\"x\"))\n\n    # resolution tests\n    if min_width != 0 and min_width > original_width:\n        return False\n    if min_height != 0 and min_height > original_height:\n        return False\n    if max_width != 0 and max_width < original_width:\n        return False\n    if max_height != 0 and max_height < original_height:\n        return False\n\n    # color / grayscale tests\n    if is_grayscale and not chain[\"IsGrayscale\"]:\n        return False\n    if not is_grayscale and not chain[\"IsColor\"]:\n        return False\n\n    # scale tests\n    if chain[\"MaxScaleFactor\"] != 0 and target_scale > chain[\"MaxScaleFactor\"]:\n        return False\n    if chain[\"MinScaleFactor\"] != 0 and target_scale < chain[\"MinScaleFactor\"]:\n        return False\n\n    return True\n\n\ndef ai_upscale_image(\n    image: np.ndarray, model_tile_size: TileSize, model: ImageModelDescriptor | None\n) -> np.ndarray:\n    if model is not None:\n        result = upscale_image_node(\n            context,\n            image,\n            model,\n            False,\n            0,\n            model_tile_size,\n            256,\n            False,\n        )\n\n        _, _, c = get_h_w_c(image)\n\n        if c == 1 and result.ndim == 3:\n            result = np.squeeze(result, axis=-1)\n\n        return result\n\n    return image\n\n\ndef postprocess_image(image: np.ndarray) -> np.ndarray:\n    # print(f\"postprocess_image\")\n    return to_uint8(image, normalized=True)\n\n\ndef final_target_resize(\n    image: np.ndarray,\n    target_scale: float,\n    target_width: int,\n    target_height: int,\n    original_width: int,\n    original_height: int,\n    is_grayscale: bool,\n) -> np.ndarray:\n    # fit to dimensions\n    if target_height != 0 and target_width != 0:\n        h, w, _ = get_h_w_c(image)\n        # determine whether to fit to height or width\n        if target_height / original_height < target_width / original_width:\n            target_width = 0\n        else:\n            target_height = 0\n\n    # resize height, keep proportional width\n    if target_height != 0:\n        h, w, _ = get_h_w_c(image)\n        if h != target_height:\n            return image_resize(\n                image, (round(w * target_height / h), target_height), is_grayscale\n            )\n    # resize width, keep proportional height\n    elif target_width != 0:\n        h, w, _ = get_h_w_c(image)\n        if w != target_width:\n            return image_resize(\n                image, (target_width, round(h * target_width / w)), is_grayscale\n            )\n    else:\n        h, w, _ = get_h_w_c(image)\n        new_target_height = round(original_height * target_scale)\n        if h != new_target_height:\n            return image_resize(\n                image,\n                (round(w * new_target_height / h), new_target_height),\n                is_grayscale,\n            )\n\n    return image\n\n\ndef save_image_zip(\n    image: np.ndarray,\n    file_name: str,\n    output_zip: ZipFile,\n    image_format: str,\n    lossy_compression_quality: int,\n    use_lossless_compression: bool,\n    original_width: int,\n    original_height: int,\n    target_scale: float,\n    target_width: int,\n    target_height: int,\n    is_grayscale: bool,\n) -> None:\n    print(f\"save image to zip: {file_name}\", flush=True)\n\n    image = to_uint8(image, normalized=True)\n\n    image = final_target_resize(\n        image,\n        target_scale,\n        target_width,\n        target_height,\n        original_width,\n        original_height,\n        is_grayscale,\n    )\n\n    # Convert the resized image back to bytes\n    args = {\"Q\": int(lossy_compression_quality)}\n    if image_format in {\"webp\"}:\n        args[\"lossless\"] = use_lossless_compression\n    buf_img = pyvips.Image.new_from_array(image).write_to_buffer(f\".{image_format}\", **args)\n    output_buffer = io.BytesIO(buf_img)  # type: ignore\n\n    upscaled_image_data = output_buffer.getvalue()\n\n    # Add the resized image to the output zip\n    output_zip.writestr(file_name, upscaled_image_data)\n\n\ndef save_image(\n    image: np.ndarray,\n    output_file_path: str,\n    image_format: str,\n    lossy_compression_quality: int,\n    use_lossless_compression: bool,\n    original_width: int,\n    original_height: int,\n    target_scale: float,\n    target_width: int,\n    target_height: int,\n    is_grayscale: bool,\n) -> None:\n    print(f\"save image: {output_file_path}\", flush=True)\n\n    image = to_uint8(image, normalized=True)\n\n    image = final_target_resize(\n        image,\n        target_scale,\n        target_width,\n        target_height,\n        original_width,\n        original_height,\n        is_grayscale,\n    )\n\n    args = {\"Q\": int(lossy_compression_quality)}\n    if image_format in {\"webp\"}:\n        args[\"lossless\"] = use_lossless_compression\n    pyvips.Image.new_from_array(image).write_to_file(output_file_path, **args)\n\n\ndef preprocess_worker_archive(\n    upscale_queue: Queue,\n    input_archive_path: str,\n    output_archive_path: str,\n    target_scale: float | None,\n    target_width: int,\n    target_height: int,\n    chains: list[dict[str, Any]],\n    loaded_models: dict[str, ModelDescriptor],\n    grayscale_detection_threshold: int,\n) -> None:\n    \"\"\"\n    given a zip or rar path, read images out of the archive, apply auto levels, add the image to upscale queue\n    \"\"\"\n\n    if input_archive_path.endswith(ZIP_EXTENSIONS):\n        with ZipFile(input_archive_path, \"r\") as input_zip:\n            preprocess_worker_archive_file(\n                upscale_queue,\n                input_zip,\n                output_archive_path,\n                target_scale,\n                target_width,\n                target_height,\n                chains,\n                loaded_models,\n                grayscale_detection_threshold,\n            )\n    elif input_archive_path.endswith(RAR_EXTENSIONS):\n        with rarfile.RarFile(input_archive_path, \"r\") as input_rar:\n            preprocess_worker_archive_file(\n                upscale_queue,\n                input_rar,\n                output_archive_path,\n                target_scale,\n                target_width,\n                target_height,\n                chains,\n                loaded_models,\n                grayscale_detection_threshold,\n            )\n\n\ndef preprocess_worker_archive_file(\n    upscale_queue: Queue,\n    input_archive: RarFile | ZipFile,\n    output_archive_path: str,\n    target_scale: float | None,\n    target_width: int,\n    target_height: int,\n    chains: list[dict[str, Any]],\n    loaded_models: dict[str, ModelDescriptor],\n    grayscale_detection_threshold: int,\n) -> None:\n    \"\"\"\n    given an input zip or rar archive, read images out of the archive, apply auto levels, add the image to upscale queue\n    \"\"\"\n    os.makedirs(os.path.dirname(output_archive_path), exist_ok=True)\n    namelist = input_archive.namelist()\n    print(f\"TOTALZIP={len(namelist)}\", flush=True)\n    for filename in namelist:\n        decoded_filename = filename\n        image_data = None\n        try:\n            decoded_filename = decoded_filename.encode(\"cp437\").decode(\n                f\"cp{system_codepage}\"\n            )\n        except:  # noqa: E722\n            pass\n\n        # Open the file inside the input zip\n        try:\n            with input_archive.open(filename) as file_in_archive:\n                # Read the image data\n\n                image_data = file_in_archive.read()\n\n                # image_bytes = io.BytesIO(image_data)\n                image = _read_image(image_data, filename)\n                print(\"read image\", filename, flush=True)\n                chain, is_grayscale, original_width, original_height = (\n                    get_chain_for_image(\n                        image,\n                        target_scale,\n                        target_width,\n                        target_height,\n                        chains,\n                        grayscale_detection_threshold,\n                    )\n                )\n\n                if is_grayscale:\n                    image = convert_image_to_grayscale(image)\n\n                model = None\n                tile_size_str = \"\"\n                if chain is not None:\n                    resize_width_before_upscale = chain[\"ResizeWidthBeforeUpscale\"]\n                    resize_height_before_upscale = chain[\"ResizeHeightBeforeUpscale\"]\n                    resize_factor_before_upscale = chain[\"ResizeFactorBeforeUpscale\"]\n\n                    # resize width and height, distorting image\n                    if (\n                        resize_height_before_upscale != 0\n                        and resize_width_before_upscale != 0\n                    ):\n                        h, w, _ = get_h_w_c(image)\n                        image = standard_resize(\n                            image,\n                            (resize_width_before_upscale, resize_height_before_upscale),\n                        )\n                    # resize height, keep proportional width\n                    elif resize_height_before_upscale != 0:\n                        h, w, _ = get_h_w_c(image)\n                        image = standard_resize(\n                            image,\n                            (\n                                round(w * resize_height_before_upscale / h),\n                                resize_height_before_upscale,\n                            ),\n                        )\n                    # resize width, keep proportional height\n                    elif resize_width_before_upscale != 0:\n                        h, w, _ = get_h_w_c(image)\n                        image = standard_resize(\n                            image,\n                            (\n                                resize_width_before_upscale,\n                                round(h * resize_width_before_upscale / w),\n                            ),\n                        )\n                    elif resize_factor_before_upscale != 100:\n                        h, w, _ = get_h_w_c(image)\n                        image = standard_resize(\n                            image,\n                            (\n                                round(w * resize_factor_before_upscale / 100),\n                                round(h * resize_factor_before_upscale / 100),\n                            ),\n                        )\n\n                    if is_grayscale and chain[\"AutoAdjustLevels\"]:\n                        image = enhance_contrast(image)\n                    else:\n                        image = normalize(image)\n\n                    model_abs_path = get_model_abs_path(chain[\"ModelFilePath\"])\n\n                    if model_abs_path in loaded_models:\n                        model = loaded_models[model_abs_path]\n\n                    elif os.path.exists(model_abs_path):\n                        model, _, _ = load_model_node(context, Path(model_abs_path))\n                        loaded_models[model_abs_path] = model\n\n                    tile_size_str = chain[\"ModelTileSize\"]\n                else:\n                    image = normalize(image)\n\n                # image = np.ascontiguousarray(image)\n                upscale_queue.put(\n                    (\n                        image,\n                        decoded_filename,\n                        True,\n                        is_grayscale,\n                        original_width,\n                        original_height,\n                        get_tile_size(tile_size_str),\n                        model,\n                    )\n                )\n        except Exception as e:\n            print(\n                f\"could not read as image, copying file to zip instead of upscaling: {decoded_filename}, {e}\",\n                flush=True,\n            )\n            upscale_queue.put(\n                (image_data, decoded_filename, False, False, None, None, None, None)\n            )\n        #     pass\n    upscale_queue.put(UPSCALE_SENTINEL)\n\n    # print(\"preprocess_worker_archive exiting\")\n\n\ndef preprocess_worker_folder(\n    upscale_queue: Queue,\n    input_folder_path: str,\n    output_folder_path: str,\n    output_filename: str,\n    upscale_images: bool,\n    upscale_archives: bool,\n    overwrite_existing_files: bool,\n    image_format: str,\n    lossy_compression_quality: int,\n    use_lossless_compression: bool,\n    target_scale: float | None,\n    target_width: int,\n    target_height: int,\n    chains: list[dict[str, Any]],\n    loaded_models: dict[str, ModelDescriptor],\n    grayscale_detection_threshold: int,\n) -> None:\n    \"\"\"\n    given a folder path, recursively iterate the folder\n    \"\"\"\n    print(\n        f\"preprocess_worker_folder entering {input_folder_path} {output_folder_path} {output_filename}\",\n        flush=True,\n    )\n    for root, _dirs, files in os.walk(input_folder_path):\n        for filename in files:\n            # for output file, create dirs if necessary, or skip if file exists and overwrite not enabled\n            input_file_base = Path(filename).stem\n            filename_rel = os.path.relpath(\n                os.path.join(root, filename), input_folder_path\n            )\n            output_filename_rel = os.path.join(\n                os.path.dirname(filename_rel),\n                output_filename.replace(\"%filename%\", input_file_base),\n            )\n            output_file_path = Path(\n                os.path.join(output_folder_path, output_filename_rel)\n            )\n\n            if filename.lower().endswith(IMAGE_EXTENSIONS):  # TODO if image\n                if upscale_images:\n                    output_file_path = str(\n                        Path(f\"{output_file_path}.{image_format}\")\n                    ).replace(\"%filename%\", input_file_base)\n\n                    if not overwrite_existing_files and os.path.isfile(\n                        output_file_path\n                    ):\n                        print(f\"file exists, skip: {output_file_path}\", flush=True)\n                        continue\n\n                    os.makedirs(os.path.dirname(output_file_path), exist_ok=True)\n                    image = _read_image_from_path(os.path.join(root, filename))\n\n                    chain, is_grayscale, original_width, original_height = (\n                        get_chain_for_image(\n                            image,\n                            target_scale,\n                            target_width,\n                            target_height,\n                            chains,\n                            grayscale_detection_threshold,\n                        )\n                    )\n\n                    if is_grayscale:\n                        image = convert_image_to_grayscale(image)\n\n                    model = None\n                    tile_size_str = \"\"\n                    if chain is not None:\n                        resize_width_before_upscale = chain[\"ResizeWidthBeforeUpscale\"]\n                        resize_height_before_upscale = chain[\n                            \"ResizeHeightBeforeUpscale\"\n                        ]\n                        resize_factor_before_upscale = chain[\n                            \"ResizeFactorBeforeUpscale\"\n                        ]\n\n                        # resize width and height, distorting image\n                        if (\n                            resize_height_before_upscale != 0\n                            and resize_width_before_upscale != 0\n                        ):\n                            h, w, _ = get_h_w_c(image)\n                            image = standard_resize(\n                                image,\n                                (\n                                    resize_width_before_upscale,\n                                    resize_height_before_upscale,\n                                ),\n                            )\n                        # resize height, keep proportional width\n                        elif resize_height_before_upscale != 0:\n                            h, w, _ = get_h_w_c(image)\n                            image = standard_resize(\n                                image,\n                                (\n                                    round(w * resize_height_before_upscale / h),\n                                    resize_height_before_upscale,\n                                ),\n                            )\n                        # resize width, keep proportional height\n                        elif resize_width_before_upscale != 0:\n                            h, w, _ = get_h_w_c(image)\n                            image = standard_resize(\n                                image,\n                                (\n                                    resize_width_before_upscale,\n                                    round(h * resize_width_before_upscale / w),\n                                ),\n                            )\n                        elif resize_factor_before_upscale != 100:\n                            h, w, _ = get_h_w_c(image)\n                            image = standard_resize(\n                                image,\n                                (\n                                    round(w * resize_factor_before_upscale / 100),\n                                    round(h * resize_factor_before_upscale / 100),\n                                ),\n                            )\n\n                        if is_grayscale and chain[\"AutoAdjustLevels\"]:\n                            image = enhance_contrast(image)\n                        else:\n                            image = normalize(image)\n\n                        model_abs_path = get_model_abs_path(chain[\"ModelFilePath\"])\n\n                        if model_abs_path in loaded_models:\n                            model = loaded_models[model_abs_path]\n\n                        elif os.path.exists(model_abs_path):\n                            model, _, _ = load_model_node(context, Path(model_abs_path))\n                            loaded_models[model_abs_path] = model\n                        tile_size_str = chain[\"ModelTileSize\"]\n                    else:\n                        image = normalize(image)\n\n                    # image = np.ascontiguousarray(image)\n\n                    upscale_queue.put(\n                        (\n                            image,\n                            output_filename_rel,\n                            True,\n                            is_grayscale,\n                            original_width,\n                            original_height,\n                            get_tile_size(tile_size_str),\n                            model,\n                        )\n                    )\n            elif filename.lower().endswith(ARCHIVE_EXTENSIONS):\n                if upscale_archives:\n                    output_file_path = f\"{output_file_path}.cbz\"\n                    if not overwrite_existing_files and os.path.isfile(\n                        output_file_path\n                    ):\n                        print(f\"file exists, skip: {output_file_path}\", flush=True)\n                        continue\n                    os.makedirs(os.path.dirname(output_file_path), exist_ok=True)\n\n                    upscale_archive_file(\n                        os.path.join(root, filename),\n                        output_file_path,\n                        image_format,\n                        lossy_compression_quality,\n                        use_lossless_compression,\n                        target_scale,\n                        target_width,\n                        target_height,\n                        chains,\n                        loaded_models,\n                        grayscale_detection_threshold,\n                    )  # TODO custom output extension\n    upscale_queue.put(UPSCALE_SENTINEL)\n    # print(\"preprocess_worker_folder exiting\")\n\n\ndef preprocess_worker_image(\n    upscale_queue: Queue,\n    input_image_path: str,\n    output_image_path: str,\n    overwrite_existing_files: bool,\n    target_scale: float | None,\n    target_width: int,\n    target_height: int,\n    chains: list[dict[str, Any]],\n    loaded_models: dict[str, ModelDescriptor],\n    grayscale_detection_threshold: int,\n) -> None:\n    \"\"\"\n    given an image path, apply auto levels and add to upscale queue\n    \"\"\"\n    if input_image_path.lower().endswith(IMAGE_EXTENSIONS):\n        if not overwrite_existing_files and os.path.isfile(output_image_path):\n            print(f\"file exists, skip: {output_image_path}\", flush=True)\n            return\n\n        os.makedirs(os.path.dirname(output_image_path), exist_ok=True)\n        # with Image.open(input_image_path) as img:\n        image = _read_image_from_path(input_image_path)\n\n        chain, is_grayscale, original_width, original_height = get_chain_for_image(\n            image,\n            target_scale,\n            target_width,\n            target_height,\n            chains,\n            grayscale_detection_threshold,\n        )\n\n        if is_grayscale:\n            image = convert_image_to_grayscale(image)\n\n        model = None\n        tile_size_str = \"\"\n        if chain is not None:\n            resize_width_before_upscale = chain[\"ResizeWidthBeforeUpscale\"]\n            resize_height_before_upscale = chain[\"ResizeHeightBeforeUpscale\"]\n            resize_factor_before_upscale = chain[\"ResizeFactorBeforeUpscale\"]\n\n            # resize width and height, distorting image\n            if resize_height_before_upscale != 0 and resize_width_before_upscale != 0:\n                h, w, _ = get_h_w_c(image)\n                image = standard_resize(\n                    image, (resize_width_before_upscale, resize_height_before_upscale)\n                )\n            # resize height, keep proportional width\n            elif resize_height_before_upscale != 0:\n                h, w, _ = get_h_w_c(image)\n                image = standard_resize(\n                    image,\n                    (\n                        round(w * resize_height_before_upscale / h),\n                        resize_height_before_upscale,\n                    ),\n                )\n            # resize width, keep proportional height\n            elif resize_width_before_upscale != 0:\n                h, w, _ = get_h_w_c(image)\n                image = standard_resize(\n                    image,\n                    (\n                        resize_width_before_upscale,\n                        round(h * resize_width_before_upscale / w),\n                    ),\n                )\n            elif resize_factor_before_upscale != 100:\n                h, w, _ = get_h_w_c(image)\n                image = standard_resize(\n                    image,\n                    (\n                        round(w * resize_factor_before_upscale / 100),\n                        round(h * resize_factor_before_upscale / 100),\n                    ),\n                )\n\n            if is_grayscale and chain[\"AutoAdjustLevels\"]:\n                image = enhance_contrast(image)\n            else:\n                image = normalize(image)\n\n            if chain[\"ModelFilePath\"] == \"No Model\":\n                pass\n            else:\n                model_abs_path = get_model_abs_path(chain[\"ModelFilePath\"])\n\n                if not os.path.exists(model_abs_path):\n                    raise FileNotFoundError(model_abs_path)\n\n                if model_abs_path in loaded_models:\n                    model = loaded_models[model_abs_path]\n\n                elif os.path.exists(model_abs_path):\n                    model, _, _ = load_model_node(context, Path(model_abs_path))\n                    loaded_models[model_abs_path] = model\n                tile_size_str = chain[\"ModelTileSize\"]\n        else:\n            print(\"No chain!!!!!!!\")\n            image = normalize(image)\n\n        # image = np.ascontiguousarray(image)\n\n        upscale_queue.put(\n            (\n                image,\n                None,\n                True,\n                is_grayscale,\n                original_width,\n                original_height,\n                get_tile_size(tile_size_str),\n                model,\n            )\n        )\n    upscale_queue.put(UPSCALE_SENTINEL)\n\n\ndef upscale_worker(upscale_queue: Queue, postprocess_queue: Queue) -> None:\n    \"\"\"\n    wait for upscale queue, for each queue entry, upscale image and add result to postprocess queue\n    \"\"\"\n    # print(\"upscale_worker entering\")\n    while True:\n        (\n            image,\n            file_name,\n            is_image,\n            is_grayscale,\n            original_width,\n            original_height,\n            model_tile_size,\n            model,\n        ) = upscale_queue.get()\n        if image is None:\n            break\n\n        if is_image:\n            image = ai_upscale_image(image, model_tile_size, model)\n\n            # convert back to grayscale\n            if is_grayscale:\n                image = convert_image_to_grayscale(image)\n\n        postprocess_queue.put(\n            (image, file_name, is_image, is_grayscale, original_width, original_height)\n        )\n    postprocess_queue.put(POSTPROCESS_SENTINEL)\n    # print(\"upscale_worker exiting\")\n\n\ndef postprocess_worker_zip(\n    postprocess_queue: Queue,\n    output_zip_path: str,\n    image_format: str,\n    lossy_compression_quality: int,\n    use_lossless_compression: bool,\n    target_scale: float,\n    target_width: int,\n    target_height: int,\n) -> None:\n    \"\"\"\n    wait for postprocess queue, for each queue entry, save the image to the zip file\n    \"\"\"\n    # print(\"postprocess_worker_zip entering\")\n    with ZipFile(output_zip_path, \"w\", ZIP_DEFLATED) as output_zip:\n        while True:\n            (\n                image,\n                file_name,\n                is_image,\n                is_grayscale,\n                original_width,\n                original_height,\n            ) = postprocess_queue.get()\n            if image is None:\n                break\n            if is_image:\n                # image = postprocess_image(image)\n                save_image_zip(\n                    image,\n                    str(Path(file_name).with_suffix(f\".{image_format}\")),\n                    output_zip,\n                    image_format,\n                    lossy_compression_quality,\n                    use_lossless_compression,\n                    original_width,\n                    original_height,\n                    target_scale,\n                    target_width,\n                    target_height,\n                    is_grayscale,\n                )\n            else:  # copy file\n                output_zip.writestr(file_name, image)\n            print(\"PROGRESS=postprocess_worker_zip_image\", flush=True)\n        print(\"PROGRESS=postprocess_worker_zip_archive\", flush=True)\n\n\ndef postprocess_worker_folder(\n    postprocess_queue: Queue,\n    output_folder_path: str,\n    image_format: str,\n    lossy_compression_quality: int,\n    use_lossless_compression: bool,\n    target_scale: float,\n    target_width: int,\n    target_height: int,\n) -> None:\n    \"\"\"\n    wait for postprocess queue, for each queue entry, save the image to the output folder\n    \"\"\"\n    # print(\"postprocess_worker_folder entering\")\n    while True:\n        image, file_name, _, is_grayscale, original_width, original_height = (\n            postprocess_queue.get()\n        )\n        if image is None:\n            break\n        image = postprocess_image(image)\n        save_image(\n            image,\n            os.path.join(output_folder_path, str(Path(f\"{file_name}.{image_format}\"))),\n            image_format,\n            lossy_compression_quality,\n            use_lossless_compression,\n            original_width,\n            original_height,\n            target_scale,\n            target_width,\n            target_height,\n            is_grayscale,\n        )\n        print(\"PROGRESS=postprocess_worker_folder\", flush=True)\n\n    # print(\"postprocess_worker_folder exiting\")\n\n\ndef postprocess_worker_image(\n    postprocess_queue: Queue,\n    output_file_path: str,\n    image_format: str,\n    lossy_compression_quality: int,\n    use_lossless_compression: bool,\n    target_scale: float,\n    target_width: int,\n    target_height: int,\n) -> None:\n    \"\"\"\n    wait for postprocess queue, for each queue entry, save the image to the output file path\n    \"\"\"\n    while True:\n        image, _, _, is_grayscale, original_width, original_height = (\n            postprocess_queue.get()\n        )\n        if image is None:\n            break\n        # image = postprocess_image(image)\n\n        save_image(\n            image,\n            output_file_path,\n            image_format,\n            lossy_compression_quality,\n            use_lossless_compression,\n            original_width,\n            original_height,\n            target_scale,\n            target_width,\n            target_height,\n            is_grayscale,\n        )\n        print(\"PROGRESS=postprocess_worker_image\", flush=True)\n\n\ndef upscale_archive_file(\n    input_zip_path: str,\n    output_zip_path: str,\n    image_format: str,\n    lossy_compression_quality: int,\n    use_lossless_compression: bool,\n    target_scale: float | None,\n    target_width: int,\n    target_height: int,\n    chains: list[dict[str, Any]],\n    loaded_models: dict[str, ModelDescriptor],\n    grayscale_detection_threshold: int,\n) -> None:\n    # TODO accept multiple paths to reuse simple queues?\n\n    upscale_queue = Queue(maxsize=1)\n    postprocess_queue = MPQueue(maxsize=1)\n\n    # start preprocess zip process\n    preprocess_process = Thread(\n        target=preprocess_worker_archive,\n        args=(\n            upscale_queue,\n            input_zip_path,\n            output_zip_path,\n            target_scale,\n            target_width,\n            target_height,\n            chains,\n            loaded_models,\n            grayscale_detection_threshold,\n        ),\n    )\n    preprocess_process.start()\n\n    # start upscale process\n    upscale_process = Thread(\n        target=upscale_worker, args=(upscale_queue, postprocess_queue)\n    )\n    upscale_process.start()\n\n    # start postprocess zip process\n    postprocess_process = Process(\n        target=postprocess_worker_zip,\n        args=(\n            postprocess_queue,\n            output_zip_path,\n            image_format,\n            lossy_compression_quality,\n            use_lossless_compression,\n            target_scale,\n            target_width,\n            target_height,\n        ),\n    )\n    postprocess_process.start()\n\n    # wait for all processes\n    preprocess_process.join()\n    upscale_process.join()\n    postprocess_process.join()\n\n\ndef upscale_image_file(\n    input_image_path: str,\n    output_image_path: str,\n    overwrite_existing_files: bool,\n    image_format: str,\n    lossy_compression_quality: int,\n    use_lossless_compression: bool,\n    target_scale: float | None,\n    target_width: int,\n    target_height: int,\n    chains: list[dict[str, Any]],\n    loaded_models: dict[str, ModelDescriptor],\n    grayscale_detection_threshold: int,\n) -> None:\n    upscale_queue = Queue(maxsize=1)\n    postprocess_queue = MPQueue(maxsize=1)\n\n    # start preprocess image process\n    preprocess_process = Thread(\n        target=preprocess_worker_image,\n        args=(\n            upscale_queue,\n            input_image_path,\n            output_image_path,\n            overwrite_existing_files,\n            target_scale,\n            target_width,\n            target_height,\n            chains,\n            loaded_models,\n            grayscale_detection_threshold,\n        ),\n    )\n    preprocess_process.start()\n\n    # start upscale process\n    upscale_process = Thread(\n        target=upscale_worker, args=(upscale_queue, postprocess_queue)\n    )\n    upscale_process.start()\n\n    # start postprocess image process\n    postprocess_process = Process(\n        target=postprocess_worker_image,\n        args=(\n            postprocess_queue,\n            output_image_path,\n            image_format,\n            lossy_compression_quality,\n            use_lossless_compression,\n            target_scale,\n            target_width,\n            target_height,\n        ),\n    )\n    postprocess_process.start()\n\n    # wait for all processes\n    preprocess_process.join()\n    upscale_process.join()\n    postprocess_process.join()\n\n\ndef upscale_file(\n    input_file_path: str,\n    output_folder_path: str,\n    output_filename: str,\n    overwrite_existing_files: bool,\n    image_format: str,\n    lossy_compression_quality: int,\n    use_lossless_compression: bool,\n    target_scale: float | None,\n    target_width: int,\n    target_height: int,\n    chains: list[dict[str, Any]],\n    loaded_models: dict[str, ModelDescriptor],\n    grayscale_detection_threshold: int,\n) -> None:\n    input_file_base = Path(input_file_path).stem\n\n    if input_file_path.lower().endswith(ARCHIVE_EXTENSIONS):\n        output_file_path = str(\n            Path(\n                f\"{os.path.join(output_folder_path,output_filename.replace('%filename%', input_file_base))}.cbz\"\n            )\n        )\n        print(\"output_file_path\", output_file_path, flush=True)\n        if not overwrite_existing_files and os.path.isfile(output_file_path):\n            print(f\"file exists, skip: {output_file_path}\", flush=True)\n            return\n\n        upscale_archive_file(\n            input_file_path,\n            output_file_path,\n            image_format,\n            lossy_compression_quality,\n            use_lossless_compression,\n            target_scale,\n            target_width,\n            target_height,\n            chains,\n            loaded_models,\n            grayscale_detection_threshold,\n        )\n\n    elif input_file_path.lower().endswith(IMAGE_EXTENSIONS):\n        output_file_path = str(\n            Path(\n                f\"{os.path.join(output_folder_path,output_filename.replace('%filename%', input_file_base))}.{image_format}\"\n            )\n        )\n        if not overwrite_existing_files and os.path.isfile(output_file_path):\n            print(f\"file exists, skip: {output_file_path}\", flush=True)\n            return\n\n        upscale_image_file(\n            input_file_path,\n            output_file_path,\n            overwrite_existing_files,\n            image_format,\n            lossy_compression_quality,\n            use_lossless_compression,\n            target_scale,\n            target_width,\n            target_height,\n            chains,\n            loaded_models,\n            grayscale_detection_threshold,\n        )\n\n\ndef upscale_folder(\n    input_folder_path: str,\n    output_folder_path: str,\n    output_filename: str,\n    upscale_images: bool,\n    upscale_archives: bool,\n    overwrite_existing_files: bool,\n    image_format: str,\n    lossy_compression_quality: int,\n    use_lossless_compression: bool,\n    target_scale: float | None,\n    target_width: int,\n    target_height: int,\n    chains: list[dict[str, Any]],\n    loaded_models: dict[str, ModelDescriptor],\n    grayscale_detection_threshold: int,\n) -> None:\n    # print(\"upscale_folder: entering\")\n\n    # preprocess_queue = Queue(maxsize=1)\n    upscale_queue = Queue(maxsize=1)\n    postprocess_queue = MPQueue(maxsize=1)\n\n    # start preprocess folder process\n    preprocess_process = Thread(\n        target=preprocess_worker_folder,\n        args=(\n            upscale_queue,\n            input_folder_path,\n            output_folder_path,\n            output_filename,\n            upscale_images,\n            upscale_archives,\n            overwrite_existing_files,\n            image_format,\n            lossy_compression_quality,\n            use_lossless_compression,\n            target_scale,\n            target_width,\n            target_height,\n            chains,\n            loaded_models,\n            grayscale_detection_threshold,\n        ),\n    )\n    preprocess_process.start()\n\n    # start upscale process\n    upscale_process = Thread(\n        target=upscale_worker, args=(upscale_queue, postprocess_queue)\n    )\n    upscale_process.start()\n\n    # start postprocess folder process\n    postprocess_process = Process(\n        target=postprocess_worker_folder,\n        args=(\n            postprocess_queue,\n            output_folder_path,\n            image_format,\n            lossy_compression_quality,\n            use_lossless_compression,\n            target_scale,\n            target_width,\n            target_height,\n        ),\n    )\n    postprocess_process.start()\n\n    # wait for all processes\n    preprocess_process.join()\n    upscale_process.join()\n    postprocess_process.join()\n\n\ncurrent_file_directory = os.path.dirname(os.path.abspath(__file__))\n\n\ndef get_model_abs_path(chain_model_file_path: str) -> str:\n    return os.path.abspath(os.path.join(models_directory, chain_model_file_path))\n\n\ndef get_gamma_icc_profile() -> ImageCmsProfile:\n    profile_path = os.path.join(\n        current_file_directory, \"../ImageMagick/Custom Gray Gamma 1.0.icc\"\n    )\n    return ImageCms.getOpenProfile(profile_path)\n\n\ndef get_dot20_icc_profile() -> ImageCmsProfile:\n    profile_path = os.path.join(\n        current_file_directory, \"../ImageMagick/Dot Gain 20%.icc\"\n    )\n    return ImageCms.getOpenProfile(profile_path)\n\n\ndef parse_settings_from_cli():\n    parser = argparse.ArgumentParser(prog=\"python run_upscale.py\",\n                                     description=\"By default, used by MangaJaNaiConverterGui as an internal tool. \"\n                                                 \"Alternative options made available to make it easier to skip the GUI \"\n                                                 \"and run upscaling jobs directly from CLI.\")\n\n    execution_type_group = parser.add_mutually_exclusive_group(required=True)\n    execution_type_group.add_argument(\"--settings\",\n                                      help=\"Default behaviour, based on provided appstate configuration. \"\n                                           \"For advanced usage.\")\n    execution_type_group.add_argument(\"-f\", \"--file-path\",\n                                      help=\"Upscale single file\")\n    execution_type_group.add_argument(\"-d\", \"--folder-path\",\n                                      help=\"Upscale whole directory\")\n\n    parser.add_argument(\"-o\", \"--output-folder-path\",\n                        default=os.path.join(\".\", \"out\"),\n                        help=\"Output directory for upscaled files. Default: ./out\")\n    parser.add_argument(\"-m\", \"--models-directory-path\",\n                        default=os.path.join(\"..\", \"models\"),\n                        help=\"Directory with models used for upscaling. \"\n                             \"Supports only models bundled with MangaJaNaiConvertedGui. \"\n                             \"Default: MangaJaNaiConverterGui/chaiNNer/models/\")\n    parser.add_argument(\"-u\", \"--upscale-factor\",\n                        type=int,\n                        choices=[1, 2, 3, 4],\n                        default=2,\n                        help=\"Used for calculating which model will be used. Default: 2\")\n    parser.add_argument(\"--device-index\",\n                        type=int,\n                        default=0,\n                        help=\"Device used to run upscaling jobs in case more than one is available. Default: 0\")\n\n    args = parser.parse_args()\n\n    return parse_auto_settings(args) if args.settings else parse_manual_settings(args)\n\n\ndef parse_auto_settings(args):\n    with open(args.settings, encoding=\"utf-8\") as f:\n        json_settings = json.load(f)\n\n    return json_settings\n\n\ndef parse_manual_settings(args):\n    default_file_path = os.path.join(\"..\", \"resources\", \"default_cli_configuration.json\")\n    with open(default_file_path, \"r\") as default_file:\n        default_json = json.load(default_file)\n\n    default_json[\"SelectedDeviceIndex\"] = int(args.device_index)\n    default_json[\"ModelsDirectory\"] = args.models_directory_path\n\n    default_json[\"Workflows\"][\"$values\"][0][\"OutputFolderPath\"] = args.output_folder_path\n    default_json[\"Workflows\"][\"$values\"][0][\"SelectedDeviceIndex\"] = args.device_index\n    default_json[\"Workflows\"][\"$values\"][0][\"UpscaleScaleFactor\"] = args.upscale_factor\n    if args.file_path:\n        default_json[\"Workflows\"][\"$values\"][0][\"SelectedTabIndex\"] = 0\n        default_json[\"Workflows\"][\"$values\"][0][\"InputFilePath\"] = args.file_path\n    elif args.folder_path:\n        default_json[\"Workflows\"][\"$values\"][0][\"SelectedTabIndex\"] = 1\n        default_json[\"Workflows\"][\"$values\"][0][\"InputFolderPath\"] = args.folder_path\n\n    return default_json\n\n\nis_windows = platform.system() == \"win32\"\nsys.stdout.reconfigure(encoding=\"utf-8\")  # type: ignore\n\nsettings = parse_settings_from_cli()\n\nworkflow = settings[\"Workflows\"][\"$values\"][settings[\"SelectedWorkflowIndex\"]]\nmodels_directory = settings[\"ModelsDirectory\"]\n\nUPSCALE_SENTINEL = (None, None, None, None, None, None, None, None)\nPOSTPROCESS_SENTINEL = (None, None, None, None, None, None)\nCV2_IMAGE_EXTENSIONS = (\".png\", \".jpg\", \".jpeg\", \".webp\", \".bmp\")\nIMAGE_EXTENSIONS = (*CV2_IMAGE_EXTENSIONS, \".avif\")\nZIP_EXTENSIONS = (\".zip\", \".cbz\")\nRAR_EXTENSIONS = (\".rar\", \".cbr\")\nARCHIVE_EXTENSIONS = ZIP_EXTENSIONS + RAR_EXTENSIONS\nloaded_models = {}\nsystem_codepage = get_system_codepage()\n\nsettings_parser = SettingsParser(\n    {\n        \"use_cpu\": settings[\"SelectedDeviceIndex\"] == 0,\n        \"use_fp16\": settings[\"UseFp16\"],\n        \"accelerator_device_index\": settings[\"SelectedDeviceIndex\"],\n        \"budget_limit\": 0,\n    }\n)\n\nprint(\"settings\", settings_parser.get_int(\"accelerator_device_index\", 0), flush=True)\n\ncontext = _ExecutorNodeContext(ProgressController(), settings_parser, Path())\n\ngamma1icc = get_gamma_icc_profile()\ndotgain20icc = get_dot20_icc_profile()\n\ndotgain20togamma1transform = ImageCms.buildTransformFromOpenProfiles(\n    dotgain20icc, gamma1icc, \"L\", \"L\"\n)\ngamma1todotgain20transform = ImageCms.buildTransformFromOpenProfiles(\n    gamma1icc, dotgain20icc, \"L\", \"L\"\n)\n\nif __name__ == \"__main__\":\n    spandrel_custom.install()\n    # gc.disable() #TODO!!!!!!!!!!!!\n    # Record the start time\n    start_time = time.time()\n\n    image_format = None\n    if workflow[\"WebpSelected\"]:\n        image_format = \"webp\"\n    elif workflow[\"PngSelected\"]:\n        image_format = \"png\"\n    elif workflow[\"AvifSelected\"]:\n        image_format = \"avif\"\n    else:\n        image_format = \"jpeg\"\n\n    target_scale: float | None = None\n    target_width = 0\n    target_height = 0\n\n    grayscale_detection_threshold = workflow[\"GrayscaleDetectionThreshold\"]\n\n    if workflow[\"ModeScaleSelected\"]:\n        target_scale = workflow[\"UpscaleScaleFactor\"]\n    elif workflow[\"ModeWidthSelected\"]:\n        target_width = workflow[\"ResizeWidthAfterUpscale\"]\n    elif workflow[\"ModeHeightSelected\"]:\n        target_height = workflow[\"ResizeHeightAfterUpscale\"]\n    else:\n        target_width = workflow[\"DisplayDeviceWidth\"]\n        target_height = workflow[\"DisplayDeviceHeight\"]\n\n    if workflow[\"SelectedTabIndex\"] == 1:\n        upscale_folder(\n            workflow[\"InputFolderPath\"],\n            workflow[\"OutputFolderPath\"],\n            workflow[\"OutputFilename\"],\n            workflow[\"UpscaleImages\"],\n            workflow[\"UpscaleArchives\"],\n            workflow[\"OverwriteExistingFiles\"],\n            image_format,\n            workflow[\"LossyCompressionQuality\"],\n            workflow[\"UseLosslessCompression\"],\n            target_scale,\n            target_width,\n            target_height,\n            workflow[\"Chains\"][\"$values\"],\n            loaded_models,\n            grayscale_detection_threshold,\n        )\n    elif workflow[\"SelectedTabIndex\"] == 0:\n        upscale_file(\n            workflow[\"InputFilePath\"],\n            workflow[\"OutputFolderPath\"],\n            workflow[\"OutputFilename\"],\n            workflow[\"OverwriteExistingFiles\"],\n            image_format,\n            workflow[\"LossyCompressionQuality\"],\n            workflow[\"UseLosslessCompression\"],\n            target_scale,\n            target_width,\n            target_height,\n            workflow[\"Chains\"][\"$values\"],\n            loaded_models,\n            grayscale_detection_threshold,\n        )\n\n    # # Record the end time\n    end_time = time.time()\n\n    # # Calculate the elapsed time\n    elapsed_time = end_time - start_time\n\n    # Print the elapsed time\n    print(f\"Elapsed time: {elapsed_time:.2f} seconds\")\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/spandrel_custom/__init__.py",
    "content": "from spandrel import (\n    MAIN_REGISTRY,\n    ArchRegistry,\n    ArchSupport,\n)\n\nfrom .architectures import FDAT\n\nCUSTOM_REGISTRY = ArchRegistry()\n\nCUSTOM_REGISTRY.add(\n    ArchSupport.from_architecture(FDAT.FDATArch()),\n)\n\ndef install(*, ignore_duplicates: bool = False) -> list[ArchSupport]:\n    \"\"\"\n    Try to install the extra architectures into the main registry.\n\n    If `ignore_duplicates` is True, the function will not raise an error\n    if the installation fails due to any of the architectures having already\n    been installed (but they won't be replaced by ones from this package).\n    \"\"\"\n    return MAIN_REGISTRY.add(*CUSTOM_REGISTRY, ignore_duplicates=ignore_duplicates)\n\n\n__all__ = [\n    \"CUSTOM_REGISTRY\",\n    \"install\",\n]"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/spandrel_custom/architectures/FDAT/__arch/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Vaibhav Bhat\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/spandrel_custom/architectures/FDAT/__arch/fdat.py",
    "content": "# https://github.com/stinkybread/fdat/blob/main/fdat.py\nfrom __future__ import annotations\n\nimport math\nfrom typing import Literal\n\nimport numpy as np\nimport torch\nimport torch.nn.functional as F  # noqa: N812\nfrom einops import rearrange\nfrom torch import Tensor, nn\nfrom torch.nn.init import trunc_normal_\nfrom torch.nn.modules.module import _IncompatibleKeys  # type: ignore\n\nfrom spandrel.__helpers.model_descriptor import StateDict\nfrom spandrel.util import store_hyperparameters\nfrom spandrel.util.timm import DropPath\n\nSampleMods = Literal[\n    \"conv\",\n    \"pixelshuffledirect\",\n    \"pixelshuffle\",\n    \"nearest+conv\",\n    \"dysample\",\n]\n\nSampleMods3 = Literal[SampleMods, \"transpose+conv\", \"lda\", \"pa_up\"]\n\n\nclass DySample(nn.Module):\n    \"\"\"Adapted from 'Learning to Upsample by Learning to Sample':\n    https://arxiv.org/abs/2308.15085\n    https://github.com/tiny-smart/dysample\n    \"\"\"\n\n    def __init__(\n        self,\n        in_channels: int = 64,\n        out_ch: int = 3,\n        scale: int = 2,\n        groups: int = 4,\n        end_convolution: bool = True,\n        end_kernel=1,\n    ) -> None:\n        super().__init__()\n\n        if in_channels <= groups or in_channels % groups != 0:\n            msg = \"Incorrect in_channels and groups values.\"\n            raise ValueError(msg)\n\n        out_channels = 2 * groups * scale**2\n        self.scale = scale\n        self.groups = groups\n        self.end_convolution = end_convolution\n        if end_convolution:\n            self.end_conv = nn.Conv2d(\n                in_channels, out_ch, end_kernel, 1, end_kernel // 2\n            )\n        self.offset = nn.Conv2d(in_channels, out_channels, 1)\n        self.scope = nn.Conv2d(in_channels, out_channels, 1, bias=False)\n        if self.training:\n            nn.init.trunc_normal_(self.offset.weight, std=0.02)\n            nn.init.constant_(self.scope.weight, val=0)\n\n        self.register_buffer(\"init_pos\", self._init_pos())\n\n    def _init_pos(self) -> Tensor:\n        h = torch.arange((-self.scale + 1) / 2, (self.scale - 1) / 2 + 1) / self.scale\n        return (\n            torch.stack(torch.meshgrid([h, h], indexing=\"ij\"))\n            .transpose(1, 2)\n            .repeat(1, self.groups, 1)\n            .reshape(1, -1, 1, 1)\n        )\n\n    def forward(self, x: Tensor) -> Tensor:\n        offset = self.offset(x) * self.scope(x).sigmoid() * 0.5 + self.init_pos\n        B, _, H, W = offset.shape\n        offset = offset.view(B, 2, -1, H, W)\n        coords_h = torch.arange(H) + 0.5\n        coords_w = torch.arange(W) + 0.5\n\n        coords = (\n            torch.stack(torch.meshgrid([coords_w, coords_h], indexing=\"ij\"))\n            .transpose(1, 2)\n            .unsqueeze(1)\n            .unsqueeze(0)\n            .type(x.dtype)\n            .to(x.device, non_blocking=True)\n        )\n        normalizer = torch.tensor(\n            [W, H], dtype=x.dtype, device=x.device, pin_memory=True\n        ).view(1, 2, 1, 1, 1)\n        coords = 2 * (coords + offset) / normalizer - 1\n\n        coords = (\n            F.pixel_shuffle(coords.reshape(B, -1, H, W), self.scale)\n            .view(B, 2, -1, self.scale * H, self.scale * W)\n            .permute(0, 2, 3, 4, 1)\n            .contiguous()\n            .flatten(0, 1)\n        )\n        output = F.grid_sample(\n            x.reshape(B * self.groups, -1, H, W),\n            coords,\n            mode=\"bilinear\",\n            align_corners=False,\n            padding_mode=\"border\",\n        ).view(B, -1, self.scale * H, self.scale * W)\n\n        if self.end_convolution:\n            output = self.end_conv(output)\n\n        return output\n\n\nclass LayerNorm(nn.Module):\n    def __init__(self, dim: int = 64, eps: float = 1e-6) -> None:\n        super().__init__()\n        self.weight = nn.Parameter(torch.ones(dim))\n        self.bias = nn.Parameter(torch.zeros(dim))\n        self.eps = eps\n        self.dim = (dim,)\n\n    def forward(self, x):\n        if x.is_contiguous(memory_format=torch.channels_last):\n            return F.layer_norm(\n                x.permute(0, 2, 3, 1), self.dim, self.weight, self.bias, self.eps\n            ).permute(0, 3, 1, 2)\n        u = x.mean(1, keepdim=True)\n        s = (x - u).pow(2).mean(1, keepdim=True)\n        x = (x - u) / torch.sqrt(s + self.eps)\n        return self.weight[:, None, None] * x + self.bias[:, None, None]\n\n\nclass LDA_AQU(nn.Module):\n    def __init__(\n        self,\n        in_channels=48,\n        reduction_factor=4,\n        nh=1,\n        scale_factor=2.0,\n        k_e=3,\n        k_u=3,\n        n_groups=2,\n        range_factor=11,\n        rpb=True,\n    ) -> None:\n        super().__init__()\n        self.k_u = k_u\n        self.num_head = nh\n        self.scale_factor = scale_factor\n        self.n_groups = n_groups\n        self.offset_range_factor = range_factor\n\n        self.attn_dim = in_channels // (reduction_factor * self.num_head)\n        self.scale = self.attn_dim**-0.5\n        self.rpb = rpb\n        self.hidden_dim = in_channels // reduction_factor\n        self.proj_q = nn.Conv2d(\n            in_channels, self.hidden_dim, kernel_size=1, stride=1, padding=0, bias=False\n        )\n\n        self.proj_k = nn.Conv2d(\n            in_channels, self.hidden_dim, kernel_size=1, stride=1, padding=0, bias=False\n        )\n\n        self.group_channel = in_channels // (reduction_factor * self.n_groups)\n        self.conv_offset = nn.Sequential(\n            nn.Conv2d(\n                self.group_channel,\n                self.group_channel,\n                3,\n                1,\n                1,\n                groups=self.group_channel,\n                bias=False,\n            ),\n            LayerNorm(self.group_channel),\n            nn.SiLU(),\n            nn.Conv2d(self.group_channel, 2 * k_u**2, k_e, 1, k_e // 2),\n        )\n        self.layer_norm = LayerNorm(in_channels)\n\n        self.pad = int((self.k_u - 1) / 2)\n        base = np.arange(-self.pad, self.pad + 1).astype(np.float32)\n        base_y = np.repeat(base, self.k_u)\n        base_x = np.tile(base, self.k_u)\n        base_offset = np.stack([base_y, base_x], axis=1).flatten()\n        base_offset = torch.tensor(base_offset).view(1, -1, 1, 1)\n        self.register_buffer(\"base_offset\", base_offset, persistent=False)\n\n        if self.rpb:\n            self.relative_position_bias_table = nn.Parameter(\n                torch.zeros(\n                    1, self.num_head, 1, self.k_u**2, self.hidden_dim // self.num_head\n                )\n            )\n            nn.init.trunc_normal_(self.relative_position_bias_table, std=0.02)\n\n    def init_weights(self) -> None:\n        for m in self.modules():\n            if isinstance(m, nn.Conv2d):\n                nn.init.xavier_uniform(m)\n            elif isinstance(m, nn.LayerNorm):\n                nn.init.constant_(m.bias, 0)\n                nn.init.constant_(m.weight, 1.0)\n        nn.init.constant_(self.conv_offset[-1].weight, 0)\n        nn.init.constant_(self.conv_offset[-1].bias, 0)\n\n    def get_offset(self, offset, Hout, Wout):\n        B, _, _, _ = offset.shape\n        device = offset.device\n        row_indices = torch.arange(Hout, device=device)\n        col_indices = torch.arange(Wout, device=device)\n        row_indices, col_indices = torch.meshgrid(row_indices, col_indices)\n        index_tensor = torch.stack((row_indices, col_indices), dim=-1).view(\n            1, Hout, Wout, 2\n        )\n        offset = rearrange(\n            offset, \"b (kh kw d) h w -> b kh h kw w d\", kh=self.k_u, kw=self.k_u\n        )\n        offset = offset + index_tensor.view(1, 1, Hout, 1, Wout, 2)\n        offset = offset.contiguous().view(B, self.k_u * Hout, self.k_u * Wout, 2)\n\n        offset[..., 0] = 2 * offset[..., 0] / (Hout - 1) - 1\n        offset[..., 1] = 2 * offset[..., 1] / (Wout - 1) - 1\n        offset = offset.flip(-1)\n        return offset\n\n    def extract_feats(self, x, offset, ks=3):\n        out = nn.functional.grid_sample(\n            x,\n            offset,\n            mode=\"bilinear\",\n            padding_mode=\"zeros\",\n            align_corners=True,\n        )\n        out = rearrange(out, \"b c (ksh h) (ksw w) -> b (ksh ksw) c h w\", ksh=ks, ksw=ks)\n        return out\n\n    def forward(self, x):\n        B, C, H, W = x.shape\n        out_H, out_W = int(H * self.scale_factor), int(W * self.scale_factor)\n        v = x\n        x = self.layer_norm(x)\n        q = self.proj_q(x)\n        k = self.proj_k(x)\n\n        q = torch.nn.functional.interpolate(\n            q, (out_H, out_W), mode=\"bilinear\", align_corners=True\n        )\n        q_off = q.view(B * self.n_groups, -1, out_H, out_W)\n        pred_offset = self.conv_offset(q_off)\n        offset = pred_offset.tanh().mul(self.offset_range_factor) + self.base_offset.to(\n            x.dtype\n        )\n\n        k = k.view(B * self.n_groups, self.hidden_dim // self.n_groups, H, W)\n        v = v.view(B * self.n_groups, C // self.n_groups, H, W)\n        offset = self.get_offset(offset, out_H, out_W)\n        k = self.extract_feats(k, offset=offset)\n        v = self.extract_feats(v, offset=offset)\n\n        q = rearrange(q, \"b (nh c) h w -> b nh (h w) () c\", nh=self.num_head)\n        k = rearrange(k, \"(b g) n c h w -> b (h w) n (g c)\", g=self.n_groups)\n        v = rearrange(v, \"(b g) n c h w -> b (h w) n (g c)\", g=self.n_groups)\n        k = rearrange(k, \"b n1 n (nh c) -> b nh n1 n c\", nh=self.num_head)\n        v = rearrange(v, \"b n1 n (nh c) -> b nh n1 n c\", nh=self.num_head)\n\n        if self.rpb:\n            k = k + self.relative_position_bias_table\n\n        q = q * self.scale\n        attn = q @ k.transpose(-1, -2)\n        attn = attn.softmax(dim=-1)\n        out = attn @ v\n\n        out = rearrange(out, \"b nh (h w) t c -> b (nh c) (t h) w\", h=out_H)\n        return out\n\n\nclass PA(nn.Module):\n    def __init__(self, dim) -> None:\n        super().__init__()\n        self.conv = nn.Sequential(nn.Conv2d(dim, dim, 1), nn.Sigmoid())\n\n    def forward(self, x):\n        return x.mul(self.conv(x))\n\n\nclass UniUpsampleV3(nn.Sequential):\n    def __init__(\n        self,\n        upsample: SampleMods3 = \"pa_up\",\n        scale: int = 2,\n        in_dim: int = 48,\n        out_dim: int = 3,\n        mid_dim: int = 48,\n        group: int = 4,  # Only DySample\n        dysample_end_kernel=1,  # needed only for compatibility with version 2\n    ) -> None:\n        m = []\n\n        if scale == 1 or upsample == \"conv\":\n            m.append(nn.Conv2d(in_dim, out_dim, 3, 1, 1))\n        elif upsample == \"pixelshuffledirect\":\n            m.extend(\n                [nn.Conv2d(in_dim, out_dim * scale**2, 3, 1, 1), nn.PixelShuffle(scale)]\n            )\n        elif upsample == \"pixelshuffle\":\n            m.extend([nn.Conv2d(in_dim, mid_dim, 3, 1, 1), nn.LeakyReLU(inplace=True)])\n            if (scale & (scale - 1)) == 0:  # scale = 2^n\n                for _ in range(int(math.log2(scale))):\n                    m.extend(\n                        [nn.Conv2d(mid_dim, 4 * mid_dim, 3, 1, 1), nn.PixelShuffle(2)]\n                    )\n            elif scale == 3:\n                m.extend([nn.Conv2d(mid_dim, 9 * mid_dim, 3, 1, 1), nn.PixelShuffle(3)])\n            else:\n                raise ValueError(\n                    f\"scale {scale} is not supported. Supported scales: 2^n and 3.\"\n                )\n            m.append(nn.Conv2d(mid_dim, out_dim, 3, 1, 1))\n        elif upsample == \"nearest+conv\":\n            if (scale & (scale - 1)) == 0:\n                for _ in range(int(math.log2(scale))):\n                    m.extend(\n                        (\n                            nn.Conv2d(in_dim, in_dim, 3, 1, 1),\n                            nn.Upsample(scale_factor=2),\n                            nn.LeakyReLU(negative_slope=0.2, inplace=True),\n                        )\n                    )\n                m.extend(\n                    (\n                        nn.Conv2d(in_dim, in_dim, 3, 1, 1),\n                        nn.LeakyReLU(negative_slope=0.2, inplace=True),\n                    )\n                )\n            elif scale == 3:\n                m.extend(\n                    (\n                        nn.Conv2d(in_dim, in_dim, 3, 1, 1),\n                        nn.Upsample(scale_factor=scale),\n                        nn.LeakyReLU(negative_slope=0.2, inplace=True),\n                        nn.Conv2d(in_dim, in_dim, 3, 1, 1),\n                        nn.LeakyReLU(negative_slope=0.2, inplace=True),\n                    )\n                )\n            else:\n                raise ValueError(\n                    f\"scale {scale} is not supported. Supported scales: 2^n and 3.\"\n                )\n            m.append(nn.Conv2d(in_dim, out_dim, 3, 1, 1))\n        elif upsample == \"dysample\":\n            if mid_dim != in_dim:\n                m.extend(\n                    [nn.Conv2d(in_dim, mid_dim, 3, 1, 1), nn.LeakyReLU(inplace=True)]\n                )\n            m.append(\n                DySample(mid_dim, out_dim, scale, group, end_kernel=dysample_end_kernel)\n            )\n            # m.append(nn.Conv2d(mid_dim, out_dim, dysample_end_kernel, 1, dysample_end_kernel//2)) # kernel 1 causes chromatic artifacts\n        elif upsample == \"transpose+conv\":\n            if scale == 2:\n                m.append(nn.ConvTranspose2d(in_dim, out_dim, 4, 2, 1))\n            elif scale == 3:\n                m.append(nn.ConvTranspose2d(in_dim, out_dim, 3, 3, 0))\n            elif scale == 4:\n                m.extend(\n                    [\n                        nn.ConvTranspose2d(in_dim, in_dim, 4, 2, 1),\n                        nn.GELU(),\n                        nn.ConvTranspose2d(in_dim, out_dim, 4, 2, 1),\n                    ]\n                )\n            else:\n                raise ValueError(\n                    f\"scale {scale} is not supported. Supported scales: 2, 3, 4\"\n                )\n            m.append(nn.Conv2d(out_dim, out_dim, 3, 1, 1))\n        elif upsample == \"lda\":\n            if mid_dim != in_dim:\n                m.extend(\n                    [nn.Conv2d(in_dim, mid_dim, 3, 1, 1), nn.LeakyReLU(inplace=True)]\n                )\n            m.append(LDA_AQU(mid_dim, scale_factor=scale))\n            m.append(nn.Conv2d(mid_dim, out_dim, 3, 1, 1))\n        elif upsample == \"pa_up\":\n            if (scale & (scale - 1)) == 0:\n                for _ in range(int(math.log2(scale))):\n                    m.extend(\n                        [\n                            nn.Upsample(scale_factor=2),\n                            nn.Conv2d(in_dim, mid_dim, 3, 1, 1),\n                            PA(mid_dim),\n                            nn.LeakyReLU(negative_slope=0.2, inplace=True),\n                            nn.Conv2d(mid_dim, mid_dim, 3, 1, 1),\n                            nn.LeakyReLU(negative_slope=0.2, inplace=True),\n                        ]\n                    )\n                    in_dim = mid_dim\n            elif scale == 3:\n                m.extend(\n                    [\n                        nn.Upsample(scale_factor=3),\n                        nn.Conv2d(in_dim, mid_dim, 3, 1, 1),\n                        PA(mid_dim),\n                        nn.LeakyReLU(negative_slope=0.2, inplace=True),\n                        nn.Conv2d(mid_dim, mid_dim, 3, 1, 1),\n                        nn.LeakyReLU(negative_slope=0.2, inplace=True),\n                    ]\n                )\n            else:\n                raise ValueError(\n                    f\"scale {scale} is not supported. Supported scales: 2^n and 3.\"\n                )\n            m.append(nn.Conv2d(mid_dim, out_dim, 3, 1, 1))\n        else:\n            raise ValueError(\n                f\"An invalid Upsample was selected. Please choose one of {SampleMods}\"\n            )\n        super().__init__(*m)\n\n        self.register_buffer(\n            \"MetaUpsample\",\n            torch.tensor(\n                [\n                    3,  # Block version, if you change something, please number from the end so that you can distinguish between authorized changes and third parties\n                    list(SampleMods3.__args__).index(upsample),  # UpSample method index\n                    scale,\n                    in_dim,\n                    out_dim,\n                    mid_dim,\n                    group,\n                ],\n                dtype=torch.uint8,\n            ),\n        )\n\n\n# --- fdat Components ---\nclass FastSpatialWindowAttention(nn.Module):\n    def __init__(self, dim, window_size=8, num_heads=4, qkv_bias=False) -> None:\n        super().__init__()\n        self.dim, self.ws, self.nh = dim, window_size, num_heads\n        self.scale = (dim // num_heads) ** -0.5\n        self.qkv, self.proj = (\n            nn.Linear(dim, dim * 3, bias=qkv_bias),\n            nn.Linear(dim, dim),\n        )\n        self.bias = nn.Parameter(\n            torch.zeros(num_heads, window_size * window_size, window_size * window_size)\n        )\n        trunc_normal_(self.bias, std=0.02)\n\n    def forward(self, x, H, W):\n        B, L, C = x.shape\n        pad_r, pad_b = (\n            (self.ws - W % self.ws) % self.ws,\n            (self.ws - H % self.ws) % self.ws,\n        )\n        if pad_r > 0 or pad_b > 0:\n            x = F.pad(x.view(B, H, W, C), (0, 0, 0, pad_r, 0, pad_b)).view(B, -1, C)\n\n        H_pad, W_pad = H + pad_b, W + pad_r\n        x = (\n            x.view(B, H_pad // self.ws, self.ws, W_pad // self.ws, self.ws, C)\n            .permute(0, 1, 3, 2, 4, 5)\n            .contiguous()\n            .view(-1, self.ws * self.ws, C)\n        )\n        qkv = (\n            self.qkv(x)\n            .view(-1, self.ws * self.ws, 3, self.nh, C // self.nh)\n            .permute(2, 0, 3, 1, 4)\n        )\n        q, k, v = qkv.unbind(0)\n        attn = (q * self.scale @ k.transpose(-2, -1)) + self.bias\n        x = (\n            (F.softmax(attn, dim=-1) @ v)\n            .transpose(1, 2)\n            .reshape(-1, self.ws * self.ws, C)\n        )\n        x = (\n            self.proj(x)\n            .view(B, H_pad // self.ws, W_pad // self.ws, self.ws, self.ws, C)\n            .permute(0, 1, 3, 2, 4, 5)\n            .contiguous()\n            .view(B, H_pad, W_pad, C)\n        )\n        if pad_r > 0 or pad_b > 0:\n            x = x[:, :H, :W, :].contiguous()\n        return x.view(B, L, C)\n\n\nclass FastChannelAttention(nn.Module):\n    def __init__(self, dim, num_heads=4, qkv_bias=False) -> None:\n        super().__init__()\n        self.nh = num_heads\n        self.temp = nn.Parameter(torch.ones(num_heads, 1, 1))\n        self.qkv, self.proj = (\n            nn.Linear(dim, dim * 3, bias=qkv_bias),\n            nn.Linear(dim, dim),\n        )\n\n    def forward(self, x, H, W):  # H, W are unused but kept for API consistency\n        B, N, C = x.shape\n        qkv = self.qkv(x).view(B, N, 3, self.nh, C // self.nh).permute(2, 0, 3, 1, 4)\n        q, k, v = qkv.unbind(0)\n        q, k = (\n            F.normalize(q.transpose(-2, -1), dim=-1),\n            F.normalize(k.transpose(-2, -1), dim=-1),\n        )\n        attn = F.softmax((q @ k.transpose(-2, -1)) * self.temp, dim=-1)\n        return self.proj(\n            (attn @ v.transpose(-2, -1)).permute(0, 3, 1, 2).reshape(B, N, C)\n        )\n\n\nclass SimplifiedAIM(nn.Module):\n    def __init__(self, dim, reduction_ratio=8) -> None:\n        super().__init__()\n        self.sg = nn.Sequential(nn.Conv2d(dim, 1, 1, bias=False), nn.Sigmoid())\n        self.cg = nn.Sequential(\n            nn.AdaptiveAvgPool2d(1),\n            nn.Conv2d(dim, dim // reduction_ratio, 1, bias=False),\n            nn.GELU(),\n            nn.Conv2d(dim // reduction_ratio, dim, 1, bias=False),\n            nn.Sigmoid(),\n        )\n\n    def forward(self, attn_feat, conv_feat, interaction_type, H, W):\n        B, L, C = attn_feat.shape\n        if interaction_type == \"spatial_modulates_channel\":\n            sm = (\n                self.sg(attn_feat.transpose(1, 2).view(B, C, H, W))\n                .view(B, 1, L)\n                .transpose(1, 2)\n            )\n            return attn_feat + (conv_feat * sm)\n        else:\n            cm = (\n                self.cg(conv_feat.transpose(1, 2).view(B, C, H, W))\n                .view(B, C, 1)\n                .transpose(1, 2)\n            )\n            return (attn_feat * cm) + conv_feat\n\n\nclass SimplifiedFFN(nn.Module):\n    def __init__(self, dim, expansion_ratio=2.0, drop=0.0) -> None:\n        super().__init__()\n        hd = int(dim * expansion_ratio)\n        self.fc1, self.act, self.fc2 = (\n            nn.Linear(dim, hd, False),\n            nn.GELU(),\n            nn.Linear(hd, dim, False),\n        )\n        self.drop = nn.Dropout(drop)\n        self.smix = nn.Conv2d(hd, hd, 3, 1, 1, groups=hd, bias=False)\n\n    def forward(self, x, H, W):\n        B, L, _C = x.shape\n        x = self.drop(self.act(self.fc1(x)))\n        x_s = (\n            self.smix(x.transpose(1, 2).view(B, x.shape[-1], H, W))\n            .view(B, x.shape[-1], L)\n            .transpose(1, 2)\n        )\n        return self.drop(self.fc2(x_s))\n\n\nclass SimplifiedDATBlock(nn.Module):\n    def __init__(self, dim, nh, ws, ffn_exp, aim_re, btype, dp, qkv_b=False) -> None:\n        super().__init__()\n        self.btype = btype\n        self.n1, self.n2 = nn.LayerNorm(dim), nn.LayerNorm(dim)\n        self.attn = (\n            FastSpatialWindowAttention(dim, ws, nh, qkv_b)\n            if btype == \"spatial\"\n            else FastChannelAttention(dim, nh, qkv_b)\n        )\n        self.conv = nn.Sequential(\n            nn.Conv2d(dim, dim, 3, 1, 1, groups=dim, bias=False), nn.GELU()\n        )\n        self.inter = SimplifiedAIM(dim, aim_re)\n        self.dp = DropPath(dp) if dp > 0.0 else nn.Identity()\n        self.ffn = SimplifiedFFN(dim, ffn_exp)\n\n    def _conv_fwd(self, x, H, W):\n        B, L, C = x.shape\n        return (\n            self.conv(x.transpose(1, 2).view(B, C, H, W)).view(B, C, L).transpose(1, 2)\n        )\n\n    def forward(self, x, H, W):\n        n1 = self.n1(x)\n        itype = (\n            \"channel_modulates_spatial\"\n            if self.btype == \"spatial\"\n            else \"spatial_modulates_channel\"\n        )\n        fused = self.inter(self.attn(n1, H, W), self._conv_fwd(n1, H, W), itype, H, W)\n        x = x + self.dp(fused)\n        x = x + self.dp(self.ffn(self.n2(x), H, W))\n        return x\n\n\nclass SimplifiedResidualGroup(nn.Module):\n    def __init__(self, dim, depth, nh, ws, ffn_exp, aim_re, pattern, dp_rates) -> None:\n        super().__init__()\n        self.blocks = nn.ModuleList(\n            [\n                SimplifiedDATBlock(\n                    dim, nh, ws, ffn_exp, aim_re, pattern[i % len(pattern)], dp_rates[i]\n                )\n                for i in range(depth)\n            ]\n        )\n        self.conv = nn.Conv2d(dim, dim, 3, 1, 1, bias=False)\n\n    def forward(self, x: Tensor) -> Tensor:\n        B, C, H, W = x.shape\n        x_seq = x.view(B, C, H * W).transpose(1, 2).contiguous()\n        for block in self.blocks:\n            x_seq = block(x_seq, H, W)\n        return self.conv(x_seq.transpose(1, 2).view(B, C, H, W)) + x\n\n\n@store_hyperparameters()\nclass FDAT(nn.Module):\n    hyperparameters = {}\n\n    def __init__(\n        self,\n        *,\n        num_in_ch: int = 3,\n        num_out_ch: int = 3,\n        scale: int = 4,\n        embed_dim: int = 120,\n        num_groups: int = 4,\n        depth_per_group: int = 3,\n        num_heads: int = 4,\n        window_size: int = 8,\n        ffn_expansion_ratio: float = 2.0,\n        aim_reduction_ratio: int = 8,\n        group_block_pattern: list[str] | None = None,\n        drop_path_rate: float = 0.1,\n        mid_dim: int = 64,\n        upsampler_type: SampleMods3 = \"transpose+conv\",\n        img_range: float = 1.0,\n        unshuffle_mod: bool = False,\n    ) -> None:\n        if group_block_pattern is None:\n            group_block_pattern = [\"spatial\", \"channel\"]\n        super().__init__()\n        self.img_range, self.upscale = img_range, scale\n        self.mean = torch.zeros(1, 1, 1, 1)\n        self.pad = 0\n        if unshuffle_mod and scale < 3:\n            unshuffle = 4 // scale\n            scale = 4\n            self.conv_first = nn.Sequential(\n                nn.PixelUnshuffle(unshuffle),\n                nn.Conv2d(num_in_ch * unshuffle**2, embed_dim, 3, 1, 1, bias=True),\n            )\n            self.pad = unshuffle\n        else:\n            self.conv_first = nn.Conv2d(num_in_ch, embed_dim, 3, 1, 1, bias=True)\n        ad = depth_per_group * len(group_block_pattern)\n        td = num_groups * ad\n        dpr = [x.item() for x in torch.linspace(0, drop_path_rate, td)]\n\n        self.groups = nn.Sequential(\n            *[\n                SimplifiedResidualGroup(\n                    embed_dim,\n                    ad,\n                    num_heads,\n                    window_size,\n                    ffn_expansion_ratio,\n                    aim_reduction_ratio,\n                    group_block_pattern,\n                    dpr[i * ad : (i + 1) * ad],\n                )\n                for i in range(num_groups)\n            ]\n        )\n\n        self.conv_after = nn.Conv2d(embed_dim, embed_dim, 3, 1, 1, bias=False)\n        self.upsampler = UniUpsampleV3(\n            upsampler_type, scale, embed_dim, num_out_ch, mid_dim, 4\n        )\n        self.apply(self._init_weights)\n\n    def load_state_dict(\n        self,\n        state_dict: StateDict,\n        *args,  # noqa: ANN002\n        **kwargs,\n    ) -> _IncompatibleKeys:\n        state_dict[\"upsampler.MetaUpsample\"] = self.upsampler.MetaUpsample\n        return super().load_state_dict(state_dict, *args, **kwargs)\n\n    def _init_weights(self, m: nn.Module) -> None:\n        if isinstance(m, nn.Linear):\n            trunc_normal_(m.weight, std=0.02)\n            if m.bias is not None:\n                nn.init.constant_(m.bias, 0)\n        elif isinstance(m, nn.Conv2d):\n            trunc_normal_(m.weight, std=0.02)\n            if m.bias is not None:\n                nn.init.constant_(m.bias, 0)\n        elif isinstance(m, nn.LayerNorm | nn.GroupNorm):\n            if hasattr(m, \"bias\") and m.bias is not None:\n                nn.init.constant_(m.bias, 0)\n            if hasattr(m, \"weight\") and m.weight is not None:\n                nn.init.constant_(m.weight, 1.0)\n\n    def check_img_size(self, x: Tensor, h: int, w: int) -> Tensor:\n        if self.pad == 0:\n            return x\n        mod_pad_h = (self.pad - h % self.pad) % self.pad\n        mod_pad_w = (self.pad - w % self.pad) % self.pad\n        return F.pad(x, (0, mod_pad_w, 0, mod_pad_h), \"reflect\")\n\n    def forward(self, x: Tensor) -> Tensor:\n        _b, _c, h, w = x.shape\n        x = self.check_img_size(x, h, w)\n        x_shallow = self.conv_first(x)\n        x_deep = self.groups(x_shallow)\n        x_deep = self.conv_after(x_deep)\n        x_out = self.upsampler(x_deep + x_shallow)\n        return x_out[:, :, : h * self.upscale, : w * self.upscale]\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/spandrel_custom/architectures/FDAT/__init__.py",
    "content": "import math\n\nfrom spandrel.util import KeyCondition, get_seq_len\n\nfrom spandrel.__helpers.model_descriptor import Architecture, ImageModelDescriptor, StateDict\nfrom .__arch.fdat import FDAT, SampleMods3\n\n\nclass FDATArch(Architecture[FDAT]):\n    def __init__(self):\n        super().__init__(\n            id=\"FDAT\",\n            detect=KeyCondition.has_any(\n                KeyCondition.has_all(\n                    \"conv_first.weight\",\n                    \"groups.0.blocks.0.attn.bias\",\n                    \"groups.0.blocks.0.inter.cg.1.weight\",\n                    \"groups.0.blocks.0.ffn.fc1.weight\",\n                    \"groups.0.blocks.0.n1.weight\",\n                    \"upsampler.MetaUpsample\",\n                ),\n                KeyCondition.has_all(\n                    \"conv_first.1.weight\",\n                    \"groups.0.blocks.0.attn.bias\",\n                    \"groups.0.blocks.0.inter.cg.1.weight\",\n                    \"groups.0.blocks.0.ffn.fc1.weight\",\n                    \"groups.0.blocks.0.n1.weight\",\n                    \"upsampler.MetaUpsample\",\n                ),\n            ),\n        )\n\n    def load(self, state_dict: StateDict) -> ImageModelDescriptor[FDAT]:\n        _, upsampler_index, scale, embed_dim, num_out_ch, mid_dim, _ = state_dict[\n            \"upsampler.MetaUpsample\"\n        ].tolist()\n        upsampler_type = list(SampleMods3.__args__)[upsampler_index]\n\n        if \"conv_first.1.weight\" in state_dict:\n            num_in_ch = num_out_ch\n            scale = 4 // (\n                math.isqrt(state_dict[\"conv_first.1.weight\"].shape[1] // num_in_ch)\n            )\n            unshuffle_mod = True\n        else:\n            unshuffle_mod = False\n            num_in_ch = state_dict[\"conv_first.weight\"].shape[1]\n\n        num_groups = get_seq_len(state_dict, \"groups\")\n        group_block_pattern = [\"spatial\", \"channel\"]\n        depth_per_group = get_seq_len(state_dict, \"groups.0.blocks\") // len(\n            group_block_pattern\n        )\n        num_heads = state_dict[\"groups.0.blocks.0.attn.bias\"].shape[0]\n        window_size = math.isqrt(state_dict[\"groups.0.blocks.0.attn.bias\"].shape[2])\n        ffn_expansion_ratio = float(\n            state_dict[\"groups.0.blocks.0.ffn.fc1.weight\"].shape[0] / embed_dim\n        )\n        aim_reduction_ratio = (\n            embed_dim // state_dict[\"groups.0.blocks.0.inter.cg.1.weight\"].shape[0]\n        )\n\n        img_range = 1.0\n\n        model = FDAT(\n            num_in_ch=num_in_ch,\n            num_out_ch=num_out_ch,\n            scale=scale,\n            embed_dim=embed_dim,\n            num_groups=num_groups,\n            depth_per_group=depth_per_group,\n            num_heads=num_heads,\n            window_size=window_size,\n            ffn_expansion_ratio=ffn_expansion_ratio,\n            aim_reduction_ratio=aim_reduction_ratio,\n            group_block_pattern=None,\n            upsampler_type=upsampler_type,\n            mid_dim=mid_dim,\n            img_range=img_range,\n            unshuffle_mod=unshuffle_mod,\n        )\n\n        sizes = {96: \"tiny\", 108: \"light\", 120: \"medium\", 180: \"large\"}\n        size_tag = None\n\n        if embed_dim in sizes:\n            size_tag = sizes[embed_dim]\n            if num_groups == 6:\n                size_tag = \"xl\"\n\n        tags = [\n            f\"{embed_dim}dim\",\n            upsampler_type,\n        ]\n\n        if size_tag:\n            tags.append(size_tag)\n\n        if unshuffle_mod:\n            tags.append(\"unshuffle\")\n\n        return ImageModelDescriptor(\n            model,\n            state_dict,\n            architecture=self,\n            purpose=\"Restoration\" if scale == 1 else \"SR\",\n            tags=tags,\n            supports_half=True,\n            supports_bfloat16=True,\n            scale=scale,\n            input_channels=num_in_ch,\n            output_channels=num_out_ch,\n        )\n\n\n__all__ = [\"FDATArch\", \"FDAT\"]\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/system.py",
    "content": "import platform\nimport sys\n\nis_mac = sys.platform == \"darwin\"\nis_arm_mac = is_mac and platform.machine() == \"arm64\"\nis_windows = sys.platform == \"win32\"\nis_linux = sys.platform == \"linux\"\n"
  },
  {
    "path": "MangaJaNaiConverterGui/backend/src/test_accelerators.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nTest script for the new accelerator detection system.\n\"\"\"\n\nimport sys\nimport torch\nfrom accelerator_detection import get_accelerator_detector, AcceleratorType\n\ndef test_accelerator_detection():\n    \"\"\"Test the accelerator detection system\"\"\"\n    print(\"=== PyTorch Accelerator Detection Test ===\\n\")\n    \n    # Get detector\n    detector = get_accelerator_detector()\n    \n    # Show PyTorch version\n    print(f\"PyTorch Version: {torch.__version__}\")\n    if hasattr(torch.version, 'hip') and torch.version.hip:\n        print(f\"ROCm Version: {torch.version.hip}\")\n    print()\n    \n    # Get all devices\n    all_devices = detector.available_devices\n    \n    print(f\"Detected {len(all_devices)} device(s):\\n\")\n    \n    for i, device in enumerate(all_devices):\n        print(f\"Device {i}: {device.name}\")\n        print(f\"  Type: {device.type.value.upper()}\")\n        print(f\"  Index: {device.index}\")\n        print(f\"  Device String: {device.device_string}\")\n        print(f\"  Torch Device: {device.torch_device}\")\n        print(f\"  FP16 Support: {device.supports_fp16}\")\n        print(f\"  BF16 Support: {device.supports_bf16}\")\n        \n        if device.memory_total:\n            total_gb = device.memory_total / (1024**3)\n            print(f\"  Total Memory: {total_gb:.2f} GB\")\n        \n        if device.memory_free:\n            free_gb = device.memory_free / (1024**3)\n            print(f\"  Free Memory: {free_gb:.2f} GB\")\n        \n        print()\n    \n    # Test device selection\n    print(\"=== Device Selection Tests ===\\n\")\n    \n    best_device = detector.get_best_device()\n    print(f\"Best Device: {best_device.name} ({best_device.type.value})\")\n    \n    cpu_device = detector.get_cpu_device()\n    print(f\"CPU Device: {cpu_device.name}\")\n    \n    # Test by type\n    for device_type in AcceleratorType:\n        devices = detector.get_devices_by_type(device_type)\n        if devices:\n            print(f\"{device_type.value.upper()} devices: {len(devices)}\")\n            for device in devices:\n                print(f\"  - {device.name}\")\n    \n    print(\"\\n=== Simple Tensor Test ===\\n\")\n    \n    # Test with best device\n    try:\n        test_device = best_device.torch_device\n        print(f\"Testing tensor creation on {test_device}\")\n        \n        # Create a simple tensor\n        x = torch.tensor([1.0, 2.0, 3.0]).to(test_device)\n        y = torch.tensor([4.0, 5.0, 6.0]).to(test_device)\n        z = x + y\n        \n        print(f\"Tensor computation successful: {z.cpu().tolist()}\")\n        \n        # Test autocast if supported\n        from accelerator_detection import get_autocast_device_type, is_device_type_supported_for_autocast\n        \n        autocast_device_type = get_autocast_device_type(test_device)\n        autocast_supported = is_device_type_supported_for_autocast(test_device)\n        \n        print(f\"Autocast device type: {autocast_device_type}\")\n        print(f\"Autocast supported: {autocast_supported}\")\n        \n        if autocast_supported:\n            with torch.autocast(device_type=autocast_device_type, dtype=torch.float16, enabled=True):\n                z_autocast = x * y\n                print(f\"Autocast computation successful: {z_autocast.cpu().tolist()}\")\n        \n    except Exception as e:\n        print(f\"Tensor test failed: {e}\")\n    \n    print(\"\\n=== Test Completed ===\")\n\n\nif __name__ == \"__main__\":\n    test_accelerator_detection()\n"
  },
  {
    "path": "MangaJaNaiConverterGui.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.7.34031.279\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"MangaJaNaiConverterGui\", \"MangaJaNaiConverterGui\\MangaJaNaiConverterGui.csproj\", \"{A0F428F0-2B21-415A-B6F0-23D52FB3300E}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Solution Items\", \"Solution Items\", \"{9FA41E85-6A01-4BB3-90FB-36C1A91C1D52}\"\n\tProjectSection(SolutionItems) = preProject\n\t\t.editorconfig = .editorconfig\n\tEndProjectSection\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{A0F428F0-2B21-415A-B6F0-23D52FB3300E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{A0F428F0-2B21-415A-B6F0-23D52FB3300E}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{A0F428F0-2B21-415A-B6F0-23D52FB3300E}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{A0F428F0-2B21-415A-B6F0-23D52FB3300E}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {922771E1-4DB5-4BA6-8485-66F1D46AEC92}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "README.md",
    "content": "# <img src=\"logo.png\" width=\"24\"></img> MangaJaNaiConverterGui\n[![Discord](https://img.shields.io/discord/1121653618173546546?label=Discord&logo=Discord&logoColor=white)](https://discord.gg/EeFfZUBvxj)\n\n## Overview\nThis 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. \n\n![image](https://github.com/the-database/MangaJaNaiConverterGui/assets/25811902/89095677-5b1f-46c9-9a1d-3d9df80cefe8)\n\n\n## Instructions\nSimply 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. \n\n### Important Note for NVIDIA Users\n\nFirst, 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**. \n\n![image](https://github.com/the-database/MangaJaNaiConverterGui/assets/25811902/3ad7392e-0de1-4eea-be59-a7b26935f08a)\n\n### For Linux users\nCheck out [this README](MangaJaNaiConverterGui/backend/src/README.md).\n\n\n## Resources\n- [OpenModelDB](https://openmodeldb.info/): Repository of AI upscaling models.\n\n## Related Projects\n- [MangaJaNai](https://github.com/the-database/mangajanai): Main repository for manga upscaling models.\n- [VideoJaNai](https://github.com/the-database/VideoJaNai): Windows GUI for video upscaling with extremely fast performance.\n- [traiNNer-redux](https://github.com/the-database/traiNNer-redux): Software for training upscaling models.\n\n## Acknowledgments \n- [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.\n"
  },
  {
    "path": "pack.bat",
    "content": "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"
  }
]