Merge pull request #54 from space-nuko/a1111-parser

A1111 parser
This commit is contained in:
space-nuko
2023-05-20 11:02:54 -05:00
committed by GitHub
30 changed files with 1960 additions and 282 deletions

View File

@@ -33,7 +33,7 @@
"svelte-check": "^3.2.0", "svelte-check": "^3.2.0",
"svelte-dnd-action": "^0.9.22", "svelte-dnd-action": "^0.9.22",
"typescript": "^5.0.3", "typescript": "^5.0.3",
"vite": "^4.3.1", "vite": "^4.3.8",
"vite-plugin-glsl": "^1.1.2", "vite-plugin-glsl": "^1.1.2",
"vite-plugin-static-copy": "^0.14.0", "vite-plugin-static-copy": "^0.14.0",
"vite-plugin-svelte-console-remover": "^1.0.10", "vite-plugin-svelte-console-remover": "^1.0.10",
@@ -65,6 +65,7 @@
"@litegraph-ts/tsconfig": "workspace:*", "@litegraph-ts/tsconfig": "workspace:*",
"@sveltejs/vite-plugin-svelte": "^2.1.1", "@sveltejs/vite-plugin-svelte": "^2.1.1",
"@tsconfig/svelte": "^4.0.1", "@tsconfig/svelte": "^4.0.1",
"@zerodevx/svelte-json-view": "^1.0.5",
"events": "^3.3.0", "events": "^3.3.0",
"framework7": "^8.0.3", "framework7": "^8.0.3",
"framework7-svelte": "^8.0.3", "framework7-svelte": "^8.0.3",
@@ -80,6 +81,7 @@
"tailwindcss": "^3.3.1", "tailwindcss": "^3.3.1",
"typed-emitter": "github:andywer/typed-emitter", "typed-emitter": "github:andywer/typed-emitter",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"vite-plugin-full-reload": "^1.0.5" "vite-plugin-full-reload": "^1.0.5",
"zod": "^3.21.4"
} }
} }

196
pnpm-lock.yaml generated
View File

@@ -69,10 +69,13 @@ importers:
version: link:litegraph/packages/tsconfig version: link:litegraph/packages/tsconfig
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1(svelte@3.58.0)(vite@4.3.1) version: 2.1.1(svelte@3.58.0)(vite@4.3.8)
'@tsconfig/svelte': '@tsconfig/svelte':
specifier: ^4.0.1 specifier: ^4.0.1
version: 4.0.1 version: 4.0.1
'@zerodevx/svelte-json-view':
specifier: ^1.0.5
version: 1.0.5(svelte@3.58.0)
events: events:
specifier: ^3.3.0 specifier: ^3.3.0
version: 3.3.0 version: 3.3.0
@@ -120,7 +123,10 @@ importers:
version: 9.0.0 version: 9.0.0
vite-plugin-full-reload: vite-plugin-full-reload:
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5(vite@4.3.1) version: 1.0.5(vite@4.3.8)
zod:
specifier: ^3.21.4
version: 3.21.4
devDependencies: devDependencies:
'@floating-ui/core': '@floating-ui/core':
specifier: ^1.2.6 specifier: ^1.2.6
@@ -168,20 +174,20 @@ importers:
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.0.3 version: 5.0.3
vite: vite:
specifier: ^4.3.1 specifier: ^4.3.8
version: 4.3.1(sass@1.61.0) version: 4.3.8(sass@1.61.0)
vite-plugin-glsl: vite-plugin-glsl:
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2(vite@4.3.1) version: 1.1.2(vite@4.3.8)
vite-plugin-static-copy: vite-plugin-static-copy:
specifier: ^0.14.0 specifier: ^0.14.0
version: 0.14.0(vite@4.3.1) version: 0.14.0(vite@4.3.8)
vite-plugin-svelte-console-remover: vite-plugin-svelte-console-remover:
specifier: ^1.0.10 specifier: ^1.0.10
version: 1.0.10(sass@1.61.0) version: 1.0.10(sass@1.61.0)
vite-tsconfig-paths: vite-tsconfig-paths:
specifier: ^4.0.8 specifier: ^4.0.8
version: 4.0.8(typescript@5.0.3)(vite@4.3.1) version: 4.0.8(typescript@5.0.3)(vite@4.3.8)
vitest: vitest:
specifier: ^0.27.3 specifier: ^0.27.3
version: 0.27.3(happy-dom@9.18.3)(jsdom@22.0.0)(sass@1.61.0) version: 0.27.3(happy-dom@9.18.3)(jsdom@22.0.0)(sass@1.61.0)
@@ -3038,7 +3044,7 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@sveltejs/vite-plugin-svelte@2.1.1(svelte@3.58.0)(vite@4.3.1): /@sveltejs/vite-plugin-svelte@2.1.1(svelte@3.58.0)(vite@4.3.8):
resolution: {integrity: sha512-7YeBDt4us0FiIMNsVXxyaP4Hwyn2/v9x3oqStkHU3ZdIc5O22pGwUwH33wUqYo+7Itdmo8zxJ45Qvfm3H7UUjQ==} resolution: {integrity: sha512-7YeBDt4us0FiIMNsVXxyaP4Hwyn2/v9x3oqStkHU3ZdIc5O22pGwUwH33wUqYo+7Itdmo8zxJ45Qvfm3H7UUjQ==}
engines: {node: ^14.18.0 || >= 16} engines: {node: ^14.18.0 || >= 16}
peerDependencies: peerDependencies:
@@ -3051,8 +3057,8 @@ packages:
magic-string: 0.30.0 magic-string: 0.30.0
svelte: 3.58.0 svelte: 3.58.0
svelte-hmr: 0.15.1(svelte@3.58.0) svelte-hmr: 0.15.1(svelte@3.58.0)
vite: 4.3.1(sass@1.61.0) vite: 4.3.8(sass@1.61.0)
vitefu: 0.2.4(vite@4.3.1) vitefu: 0.2.4(vite@4.3.8)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: false dev: false
@@ -3060,7 +3066,7 @@ packages:
/@swc/helpers@0.4.14: /@swc/helpers@0.4.14:
resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==} resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==}
dependencies: dependencies:
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/@tootallnate/once@2.0.0: /@tootallnate/once@2.0.0:
@@ -3126,10 +3132,14 @@ packages:
/@types/chai-subset@1.3.3: /@types/chai-subset@1.3.3:
resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==}
dependencies: dependencies:
'@types/chai': 4.3.4 '@types/chai': 4.3.5
/@types/chai@4.3.4: /@types/chai@4.3.4:
resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==}
dev: false
/@types/chai@4.3.5:
resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==}
/@types/concat-stream@1.6.1: /@types/concat-stream@1.6.1:
resolution: {integrity: sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==} resolution: {integrity: sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==}
@@ -3368,6 +3378,14 @@ packages:
pretty-format: 27.5.1 pretty-format: 27.5.1
dev: false dev: false
/@zerodevx/svelte-json-view@1.0.5(svelte@3.58.0):
resolution: {integrity: sha512-oQDI9v0dJEte6PYVDVjLOjU58AOoWLYRXjghKggFpZXrglWJJqoMeDe14Jrd0cs6NPcPogT/aR/LtkuW2Z1GkQ==}
peerDependencies:
svelte: ^3.55.1
dependencies:
svelte: 3.58.0
dev: false
/@zerodevx/svelte-toast@0.9.3(svelte@3.58.0): /@zerodevx/svelte-toast@0.9.3(svelte@3.58.0):
resolution: {integrity: sha512-VPKWR4A9y01fyXRscu9HiTj7tV2hFrpRKZvGwMmaPXfHIXR1D9+NNsz0HXcQ7qZ0C5UaHS3n9uNtPtIcAXT7RQ==} resolution: {integrity: sha512-VPKWR4A9y01fyXRscu9HiTj7tV2hFrpRKZvGwMmaPXfHIXR1D9+NNsz0HXcQ7qZ0C5UaHS3n9uNtPtIcAXT7RQ==}
peerDependencies: peerDependencies:
@@ -3510,7 +3528,7 @@ packages:
engines: {node: '>=16.1.0'} engines: {node: '>=16.1.0'}
dependencies: dependencies:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/autoprefixer@10.4.2(postcss@8.4.21): /autoprefixer@10.4.2(postcss@8.4.21):
@@ -3682,7 +3700,7 @@ packages:
dependencies: dependencies:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
fast-unique-numbers: 8.0.0 fast-unique-numbers: 8.0.0
tslib: 2.5.0 tslib: 2.5.1
worker-factory: 7.0.0 worker-factory: 7.0.0
dev: false dev: false
@@ -3691,8 +3709,8 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
dependencies: dependencies:
caniuse-lite: 1.0.30001481 caniuse-lite: 1.0.30001488
electron-to-chromium: 1.4.371 electron-to-chromium: 1.4.400
node-releases: 2.0.10 node-releases: 2.0.10
update-browserslist-db: 1.0.11(browserslist@4.21.5) update-browserslist-db: 1.0.11(browserslist@4.21.5)
@@ -3753,6 +3771,10 @@ packages:
/caniuse-lite@1.0.30001481: /caniuse-lite@1.0.30001481:
resolution: {integrity: sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==} resolution: {integrity: sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==}
dev: true
/caniuse-lite@1.0.30001488:
resolution: {integrity: sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==}
/case@1.6.3: /case@1.6.3:
resolution: {integrity: sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==} resolution: {integrity: sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==}
@@ -3953,7 +3975,7 @@ packages:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
dashify: 2.0.0 dashify: 2.0.0
indefinite-article: 0.0.2 indefinite-article: 0.0.2
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/compilerr@11.0.0: /compilerr@11.0.0:
@@ -3963,7 +3985,7 @@ packages:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
dashify: 2.0.0 dashify: 2.0.0
indefinite-article: 0.0.2 indefinite-article: 0.0.2
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/concat-map@0.0.1: /concat-map@0.0.1:
@@ -4449,8 +4471,8 @@ packages:
sigmund: 1.0.1 sigmund: 1.0.1
dev: false dev: false
/electron-to-chromium@1.4.371: /electron-to-chromium@1.4.400:
resolution: {integrity: sha512-jlBzY4tFcJaiUjzhRTCWAqRvTO/fWzjA3Bls0mykzGZ7zvcMP7h05W6UcgzfT9Ca1SW2xyKDOFRyI0pQeRNZGw==} resolution: {integrity: sha512-Lsvf7cvwbIxCfB8VqbnVtEsjGi3+48ejDiQZfWo5gkT+1vQ2DHQI5pl0nUvPD6z1IQk6JgFeMC5ZQJqVhalEHg==}
/emittery@0.13.1: /emittery@0.13.1:
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
@@ -4950,14 +4972,14 @@ packages:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
broker-factory: 3.0.76 broker-factory: 3.0.76
extendable-media-recorder-wav-encoder-worker: 8.0.77 extendable-media-recorder-wav-encoder-worker: 8.0.77
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/extendable-media-recorder-wav-encoder-worker@8.0.77: /extendable-media-recorder-wav-encoder-worker@8.0.77:
resolution: {integrity: sha512-9g9Q7fhOxPY7RALHVTK9Wjnc8RPYjJ9XCBP1TaNtDraIAFxvhBRax9QUOmFqHM2MvRM6hQhNav7jn23yy6tcVQ==} resolution: {integrity: sha512-9g9Q7fhOxPY7RALHVTK9Wjnc8RPYjJ9XCBP1TaNtDraIAFxvhBRax9QUOmFqHM2MvRM6hQhNav7jn23yy6tcVQ==}
dependencies: dependencies:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
tslib: 2.5.0 tslib: 2.5.1
worker-factory: 7.0.0 worker-factory: 7.0.0
dev: false dev: false
@@ -5020,7 +5042,7 @@ packages:
engines: {node: '>=14.15.4'} engines: {node: '>=14.15.4'}
dependencies: dependencies:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/fast-unique-numbers@8.0.0: /fast-unique-numbers@8.0.0:
@@ -5028,7 +5050,7 @@ packages:
engines: {node: '>=16.1.0'} engines: {node: '>=16.1.0'}
dependencies: dependencies:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/fastq@1.15.0: /fastq@1.15.0:
@@ -6624,7 +6646,7 @@ packages:
broker-factory: 3.0.76 broker-factory: 3.0.76
fast-unique-numbers: 8.0.0 fast-unique-numbers: 8.0.0
media-encoder-host-worker: 9.1.1 media-encoder-host-worker: 9.1.1
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/media-encoder-host-worker@9.1.1: /media-encoder-host-worker@9.1.1:
@@ -6632,7 +6654,7 @@ packages:
dependencies: dependencies:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
extendable-media-recorder-wav-encoder-broker: 7.0.78 extendable-media-recorder-wav-encoder-broker: 7.0.78
tslib: 2.5.0 tslib: 2.5.1
worker-factory: 7.0.0 worker-factory: 7.0.0
dev: false dev: false
@@ -6642,7 +6664,7 @@ packages:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
media-encoder-host-broker: 7.0.79 media-encoder-host-broker: 7.0.79
media-encoder-host-worker: 9.1.1 media-encoder-host-worker: 9.1.1
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/merge-stream@2.0.0: /merge-stream@2.0.0:
@@ -6760,7 +6782,7 @@ packages:
engines: {node: '>=12.20.1'} engines: {node: '>=12.20.1'}
dependencies: dependencies:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/murmurhash-js@1.0.0: /murmurhash-js@1.0.0:
@@ -7176,6 +7198,14 @@ packages:
picocolors: 1.0.0 picocolors: 1.0.0
source-map-js: 1.0.2 source-map-js: 1.0.2
/postcss@8.4.23:
resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.6
picocolors: 1.0.0
source-map-js: 1.0.2
/posthtml-parser@0.10.2: /posthtml-parser@0.10.2:
resolution: {integrity: sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg==} resolution: {integrity: sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -7360,7 +7390,7 @@ packages:
resolution: {integrity: sha512-oiiS2sp6eMxkvjt13yetSYUJvnAxBZk60mIxz0Vf/2lDWa/4svCyMLHIDzYKbHahkISd0UYyqLS9dI7xDlUOCA==} resolution: {integrity: sha512-oiiS2sp6eMxkvjt13yetSYUJvnAxBZk60mIxz0Vf/2lDWa/4svCyMLHIDzYKbHahkISd0UYyqLS9dI7xDlUOCA==}
dependencies: dependencies:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/recorder-audio-worklet@5.1.39: /recorder-audio-worklet@5.1.39:
@@ -7372,7 +7402,7 @@ packages:
recorder-audio-worklet-processor: 4.2.21 recorder-audio-worklet-processor: 4.2.21
standardized-audio-context: 25.3.45 standardized-audio-context: 25.3.45
subscribable-things: 2.1.14 subscribable-things: 2.1.14
tslib: 2.5.0 tslib: 2.5.1
worker-factory: 6.0.76 worker-factory: 6.0.76
dev: false dev: false
@@ -7513,11 +7543,11 @@ packages:
resolution: {integrity: sha512-ASEq9atUw7lualXB+knvgtvwkCEvGWV2gDD/8qnASzBkzEARZck9JAyxmY8OS6Nc1pCPEgDTKNcx+YqqYfzArw==} resolution: {integrity: sha512-ASEq9atUw7lualXB+knvgtvwkCEvGWV2gDD/8qnASzBkzEARZck9JAyxmY8OS6Nc1pCPEgDTKNcx+YqqYfzArw==}
dev: false dev: false
/rxjs@7.8.0: /rxjs@7.8.1:
resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
requiresBuild: true requiresBuild: true
dependencies: dependencies:
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
optional: true optional: true
@@ -7755,7 +7785,7 @@ packages:
dependencies: dependencies:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
automation-events: 6.0.0 automation-events: 6.0.0
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/std-env@3.3.3: /std-env@3.3.3:
@@ -7839,7 +7869,7 @@ packages:
dependencies: dependencies:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
rxjs-interop: 2.0.0 rxjs-interop: 2.0.0
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/sucrase@3.32.0: /sucrase@3.32.0:
@@ -8314,6 +8344,11 @@ packages:
/tinybench@2.4.0: /tinybench@2.4.0:
resolution: {integrity: sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==} resolution: {integrity: sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==}
dev: false
/tinybench@2.5.0:
resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==}
dev: true
/tinypool@0.3.1: /tinypool@0.3.1:
resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==} resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==}
@@ -8420,6 +8455,10 @@ packages:
/tslib@2.5.0: /tslib@2.5.0:
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
/tslib@2.5.1:
resolution: {integrity: sha512-KaI6gPil5m9vF7DKaoXxx1ia9fxS4qG5YveErRRVknPDXXriu5M8h48YRjB6h5ZUOKuAKlSJYb0GaDe8I39fRw==}
dev: false
/tsutils@3.21.0(typescript@5.0.3): /tsutils@3.21.0(typescript@5.0.3):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -8597,7 +8636,7 @@ packages:
fast-json-patch: 3.1.1 fast-json-patch: 3.1.1
json-stringify-pretty-compact: 3.0.0 json-stringify-pretty-compact: 3.0.0
semver: 7.4.0 semver: 7.4.0
tslib: 2.5.0 tslib: 2.5.1
vega: 5.22.1 vega: 5.22.1
vega-interpreter: 1.0.5 vega-interpreter: 1.0.5
vega-lite: 0.6.7 vega-lite: 0.6.7
@@ -8975,7 +9014,7 @@ packages:
picocolors: 1.0.0 picocolors: 1.0.0
source-map: 0.6.1 source-map: 0.6.1
source-map-support: 0.5.21 source-map-support: 0.5.21
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0) vite: 4.3.8(@types/node@18.16.0)(sass@1.61.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- less - less
@@ -8996,7 +9035,7 @@ packages:
mlly: 1.2.1 mlly: 1.2.1
pathe: 1.1.0 pathe: 1.1.0
picocolors: 1.0.0 picocolors: 1.0.0
vite: 4.3.1(@types/node@18.16.0) vite: 4.3.8(@types/node@18.16.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- less - less
@@ -9082,29 +9121,29 @@ packages:
- supports-color - supports-color
dev: false dev: false
/vite-plugin-full-reload@1.0.5(vite@4.3.1): /vite-plugin-full-reload@1.0.5(vite@4.3.8):
resolution: {integrity: sha512-kVZFDFWr0DxiHn6MuDVTQf7gnWIdETGlZh0hvTiMXzRN80vgF4PKbONSq8U1d0WtHsKaFODTQgJeakLacoPZEQ==} resolution: {integrity: sha512-kVZFDFWr0DxiHn6MuDVTQf7gnWIdETGlZh0hvTiMXzRN80vgF4PKbONSq8U1d0WtHsKaFODTQgJeakLacoPZEQ==}
peerDependencies: peerDependencies:
vite: ^2 || ^3 || ^4 vite: ^2 || ^3 || ^4
dependencies: dependencies:
picocolors: 1.0.0 picocolors: 1.0.0
picomatch: 2.3.1 picomatch: 2.3.1
vite: 4.3.1(sass@1.61.0) vite: 4.3.8(sass@1.61.0)
dev: false dev: false
/vite-plugin-glsl@1.1.2(vite@4.3.1): /vite-plugin-glsl@1.1.2(vite@4.3.8):
resolution: {integrity: sha512-zmXsfc1vn2MlYve9t3FAoWuhLyoCkNS1TuQL+TkXZL7tGmBjRErp10eNYxcse5tK9oUC5MyJpNc4ElpQnx8DoA==} resolution: {integrity: sha512-zmXsfc1vn2MlYve9t3FAoWuhLyoCkNS1TuQL+TkXZL7tGmBjRErp10eNYxcse5tK9oUC5MyJpNc4ElpQnx8DoA==}
engines: {node: '>= 16.15.1', npm: '>= 8.11.0'} engines: {node: '>= 16.15.1', npm: '>= 8.11.0'}
peerDependencies: peerDependencies:
vite: ^3.0.0 || ^4.0.0 vite: ^3.0.0 || ^4.0.0
dependencies: dependencies:
'@rollup/pluginutils': 5.0.2 '@rollup/pluginutils': 5.0.2
vite: 4.3.1(sass@1.61.0) vite: 4.3.8(sass@1.61.0)
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
dev: true dev: true
/vite-plugin-static-copy@0.14.0(vite@4.3.1): /vite-plugin-static-copy@0.14.0(vite@4.3.8):
resolution: {integrity: sha512-RMFmb4czomcrsbQBiUZs9HcDGN3kxGvF+OrtkfTVocp12CuoUCuJQhcY26RK35A6KS4WasGzEwcYZqHMjkAvVw==} resolution: {integrity: sha512-RMFmb4czomcrsbQBiUZs9HcDGN3kxGvF+OrtkfTVocp12CuoUCuJQhcY26RK35A6KS4WasGzEwcYZqHMjkAvVw==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies: peerDependencies:
@@ -9114,7 +9153,7 @@ packages:
fast-glob: 3.2.12 fast-glob: 3.2.12
fs-extra: 11.1.1 fs-extra: 11.1.1
picocolors: 1.0.0 picocolors: 1.0.0
vite: 4.3.1(sass@1.61.0) vite: 4.3.8(sass@1.61.0)
dev: true dev: true
/vite-plugin-svelte-console-remover@1.0.10(sass@1.61.0): /vite-plugin-svelte-console-remover@1.0.10(sass@1.61.0):
@@ -9130,7 +9169,7 @@ packages:
- supports-color - supports-color
dev: true dev: true
/vite-tsconfig-paths@4.0.8(typescript@5.0.3)(vite@4.3.1): /vite-tsconfig-paths@4.0.8(typescript@5.0.3)(vite@4.3.8):
resolution: {integrity: sha512-p04zH+Ey+NT78571x0pdX7nVRIJSlmKVvYryFglSWOK3Hc72eDL0+JJfbyQiugaIBApJkaEqbBQvqpsFZOSVGg==} resolution: {integrity: sha512-p04zH+Ey+NT78571x0pdX7nVRIJSlmKVvYryFglSWOK3Hc72eDL0+JJfbyQiugaIBApJkaEqbBQvqpsFZOSVGg==}
peerDependencies: peerDependencies:
vite: '*' vite: '*'
@@ -9141,7 +9180,7 @@ packages:
debug: 4.3.4 debug: 4.3.4
globrex: 0.1.2 globrex: 0.1.2
tsconfck: 2.1.1(typescript@5.0.3) tsconfck: 2.1.1(typescript@5.0.3)
vite: 4.3.1(sass@1.61.0) vite: 4.3.8(sass@1.61.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- typescript - typescript
@@ -9293,8 +9332,8 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: false dev: false
/vite@4.3.1(@types/node@18.16.0)(sass@1.61.0): /vite@4.3.8(@types/node@18.16.0):
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==} resolution: {integrity: sha512-uYB8PwN7hbMrf4j1xzGDk/lqjsZvCDbt/JC5dyfxc19Pg8kRm14LinK/uq+HSLNswZEoKmweGdtpbnxRtrAXiQ==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -9320,15 +9359,48 @@ packages:
dependencies: dependencies:
'@types/node': 18.16.0 '@types/node': 18.16.0
esbuild: 0.17.18 esbuild: 0.17.18
postcss: 8.4.21 postcss: 8.4.23
rollup: 3.21.0
optionalDependencies:
fsevents: 2.3.2
dev: false
/vite@4.3.8(@types/node@18.16.0)(sass@1.61.0):
resolution: {integrity: sha512-uYB8PwN7hbMrf4j1xzGDk/lqjsZvCDbt/JC5dyfxc19Pg8kRm14LinK/uq+HSLNswZEoKmweGdtpbnxRtrAXiQ==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
less: '*'
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
dependencies:
'@types/node': 18.16.0
esbuild: 0.17.18
postcss: 8.4.23
rollup: 3.21.0 rollup: 3.21.0
sass: 1.61.0 sass: 1.61.0
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/vite@4.3.1(sass@1.61.0): /vite@4.3.8(sass@1.61.0):
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==} resolution: {integrity: sha512-uYB8PwN7hbMrf4j1xzGDk/lqjsZvCDbt/JC5dyfxc19Pg8kRm14LinK/uq+HSLNswZEoKmweGdtpbnxRtrAXiQ==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -9353,7 +9425,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
esbuild: 0.17.18 esbuild: 0.17.18
postcss: 8.4.21 postcss: 8.4.23
rollup: 3.21.0 rollup: 3.21.0
sass: 1.61.0 sass: 1.61.0
optionalDependencies: optionalDependencies:
@@ -9368,7 +9440,7 @@ packages:
optional: true optional: true
dev: true dev: true
/vitefu@0.2.4(vite@4.3.1): /vitefu@0.2.4(vite@4.3.8):
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
peerDependencies: peerDependencies:
vite: ^3.0.0 || ^4.0.0 vite: ^3.0.0 || ^4.0.0
@@ -9376,7 +9448,7 @@ packages:
vite: vite:
optional: true optional: true
dependencies: dependencies:
vite: 4.3.1(sass@1.61.0) vite: 4.3.8(sass@1.61.0)
dev: false dev: false
/vitest@0.27.3(happy-dom@9.18.3)(jsdom@22.0.0)(sass@1.61.0): /vitest@0.27.3(happy-dom@9.18.3)(jsdom@22.0.0)(sass@1.61.0):
@@ -9401,7 +9473,7 @@ packages:
jsdom: jsdom:
optional: true optional: true
dependencies: dependencies:
'@types/chai': 4.3.4 '@types/chai': 4.3.5
'@types/chai-subset': 1.3.3 '@types/chai-subset': 1.3.3
'@types/node': 18.16.0 '@types/node': 18.16.0
acorn: 8.8.2 acorn: 8.8.2
@@ -9416,10 +9488,10 @@ packages:
source-map: 0.6.1 source-map: 0.6.1
std-env: 3.3.3 std-env: 3.3.3
strip-literal: 1.0.1 strip-literal: 1.0.1
tinybench: 2.4.0 tinybench: 2.5.0
tinypool: 0.3.1 tinypool: 0.3.1
tinyspy: 1.1.1 tinyspy: 1.1.1
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0) vite: 4.3.8(@types/node@18.16.0)(sass@1.61.0)
vite-node: 0.27.3(@types/node@18.16.0)(sass@1.61.0) vite-node: 0.27.3(@types/node@18.16.0)(sass@1.61.0)
why-is-node-running: 2.2.2 why-is-node-running: 2.2.2
transitivePeerDependencies: transitivePeerDependencies:
@@ -9625,7 +9697,7 @@ packages:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
compilerr: 10.0.2 compilerr: 10.0.2
fast-unique-numbers: 7.0.2 fast-unique-numbers: 7.0.2
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/worker-factory@7.0.0: /worker-factory@7.0.0:
@@ -9634,7 +9706,7 @@ packages:
'@babel/runtime': 7.21.0 '@babel/runtime': 7.21.0
compilerr: 11.0.0 compilerr: 11.0.0
fast-unique-numbers: 8.0.0 fast-unique-numbers: 8.0.0
tslib: 2.5.0 tslib: 2.5.1
dev: false dev: false
/wrap-ansi@7.0.0: /wrap-ansi@7.0.0:
@@ -9752,10 +9824,14 @@ packages:
commander: 9.5.0 commander: 9.5.0
dev: false dev: false
/zod@3.21.4:
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
dev: false
github.com/andywer/typed-emitter/9a139b6fa0ec6b0db6141b5b756b784e4f7ef4e4: github.com/andywer/typed-emitter/9a139b6fa0ec6b0db6141b5b756b784e4f7ef4e4:
resolution: {tarball: https://codeload.github.com/andywer/typed-emitter/tar.gz/9a139b6fa0ec6b0db6141b5b756b784e4f7ef4e4} resolution: {tarball: https://codeload.github.com/andywer/typed-emitter/tar.gz/9a139b6fa0ec6b0db6141b5b756b784e4f7ef4e4}
name: typed-emitter name: typed-emitter
version: 2.1.0 version: 2.1.0
optionalDependencies: optionalDependencies:
rxjs: 7.8.0 rxjs: 7.8.1
dev: false dev: false

View File

@@ -0,0 +1,263 @@
import { z, type ZodTypeAny } from "zod"
/*
* This metadata can be attached to each entry in a group to assist in
* identifying the correct nodes to apply it to.
*
* As an example, positive and negative conditioning are deployed as two
* separate nodes in ComfyUI. This makes bundling them into a { positive,
* negative } entry difficult as either one can be missing. So instead they're
* tagged like
*
* {
* conditioning: [
* { text: "masterpiece", "$meta": { types: ["positive"] } },
* { text: "worst quality", "$meta": { types: ["negative"] } },
* ]
* }
*
* The reasoning is the "types" information isn't required to reinstantiate
* the node, it's only semantic information describing how the node is used in
* the encompassing workflow. When the prompt is loaded the workflow can be
* searched for a node with the compatible type to attach the information to.
*/
const GroupMetadata = z.object({
types: z.array(z.string()).nonempty().optional()
})
export type ComfyBoxStdGroupMetadata = z.infer<typeof GroupMetadata>
const group = (obj: Record<string, any>): ZodTypeAny => {
const meta = z.object({ "$meta": GroupMetadata.optional() })
return z.object(obj).and(meta)
}
const ModelHashes = z.object({
a1111_shorthash: z.string().optional(),
sha256: z.string().optional(),
}).refine(({ a1111_shorthash, sha256 }) =>
a1111_shorthash !== undefined || sha256 !== undefined,
{ message: "At least one model hash must be specified" })
const GroupConditioning = group({
text: z.string(),
})
export type ComfyBoxStdGroupConditioning = z.infer<typeof GroupConditioning>
const GroupCheckpoint = group({
model_name: z.string().optional(),
model_hashes: ModelHashes.optional(),
}).refine(({ model_name, model_hashes }) =>
model_name !== undefined || model_hashes !== undefined,
{ message: "Must include either model name or model hash" }
)
export type ComfyBoxStdGroupCheckpoint = z.infer<typeof GroupCheckpoint>
const GroupVAE = group({
model_name: z.string().optional(),
model_hashes: ModelHashes.optional(),
type: z.enum(["internal", "external"])
}).refine(({ model_name, model_hashes }) =>
model_name !== undefined || model_hashes !== undefined,
{ message: "Must include either model name or model hashes" }
)
export type ComfyBoxStdGroupVAE = z.infer<typeof GroupVAE>
const GroupKSampler = group({
cfg_scale: z.number(),
seed: z.number(),
steps: z.number(),
sampler_name: z.string(),
scheduler: z.string(),
denoise: z.number().default(1.0),
type: z.enum(["empty", "image", "upscale"]).optional()
})
export type ComfyBoxStdGroupKSampler = z.infer<typeof GroupKSampler>
const GroupLatentImage = group({
width: z.number(),
height: z.number(),
mask_blur: z.number().optional(),
batch_count: z.number().default(1).optional(),
batch_pos: z.number().default(0).optional()
})
export type ComfyBoxStdGroupLatentImage = z.infer<typeof GroupLatentImage>
const GroupLatentUpscale = group({
width: z.number(),
height: z.number(),
upscale_method: z.string().optional(),
upscale_by: z.number().optional(),
crop: z.string().optional()
})
export type ComfyBoxStdGroupLatentUpscale = z.infer<typeof GroupLatentUpscale>
const GroupSDUpscale = group({
upscaler: z.string(),
overlap: z.number(),
})
export type ComfyBoxStdGroupSDUpscale = z.infer<typeof GroupSDUpscale>
const GroupSelfAttentionGuidance = group({
guidance_scale: z.number(),
mask_threshold: z.number(),
})
export type ComfyBoxStdGroupSelfAttentionGuidance = z.infer<typeof GroupSelfAttentionGuidance>
const GroupHypernetwork = group({
model_name: z.string(),
model_hashes: ModelHashes.optional(),
strength: z.number()
})
export type ComfyBoxStdGroupHypernetwork = z.infer<typeof GroupHypernetwork>
const LoRAModelHashes = z.object({
addnet_shorthash: z.string().optional(),
addnet_shorthash_legacy: z.string().optional(),
sha256: z.string().optional(),
}).refine(({ addnet_shorthash, addnet_shorthash_legacy, sha256 }) =>
addnet_shorthash !== undefined || addnet_shorthash_legacy !== undefined || sha256 !== undefined,
{ message: "At least one model hash must be specified" })
const GroupLoRA = group({
model_name: z.string(),
module_name: z.string().optional(),
model_hashes: LoRAModelHashes.optional(),
strength_unet: z.number(),
strength_tenc: z.number()
})
export type ComfyBoxStdGroupLoRA = z.infer<typeof GroupLoRA>
const GroupControlNet = group({
model: z.string(),
model_hashes: ModelHashes.optional(),
strength: z.number(),
})
export type ComfyBoxStdGroupControlNet = z.infer<typeof GroupControlNet>
const GroupCLIP = group({
clip_skip: z.number().optional()
})
export type ComfyBoxStdGroupCLIP = z.infer<typeof GroupCLIP>
const GroupDynamicThresholding = group({
mimic_scale: z.number(),
threshold_percentile: z.number(),
mimic_mode: z.string(),
mimic_scale_minimum: z.number(),
cfg_mode: z.string(),
cfg_scale_minimum: z.number()
})
export type ComfyBoxStdGroupDynamicThresholding = z.infer<typeof GroupDynamicThresholding>
const GroupAestheticEmbedding = group({
model_name: z.string(),
lr: z.number(),
slerp: z.boolean(),
slerp_angle: z.number().optional(),
steps: z.number(),
text: z.string(),
text_negative: z.boolean(),
weight: z.number(),
})
export type ComfyBoxStdGroupAestheticEmbedding = z.infer<typeof GroupAestheticEmbedding>
const GroupDDetailer = group({
positive_prompt: z.string(),
negative_prompt: z.string(),
bitwise: z.string(),
model: z.string().optional(),
model_hashes: ModelHashes.optional(),
conf: z.number(),
mask_blur: z.number(),
denoise: z.number(),
dilation: z.number(),
offset_x: z.number(),
offset_y: z.number(),
preprocess: z.boolean(),
inpaint_full: z.boolean(),
inpaint_padding: z.number(),
cfg: z.number()
})
export type ComfyBoxStdGroupDDetailer = z.infer<typeof GroupDDetailer>
const groupArray = (entry: ZodTypeAny) => {
return z.optional(z.array(entry).nonempty());
}
const Parameters = z.object({
conditioning: groupArray(GroupConditioning),
checkpoint: groupArray(GroupCheckpoint),
vae: groupArray(GroupVAE),
k_sampler: groupArray(GroupKSampler),
clip: groupArray(GroupCLIP),
latent_image: groupArray(GroupLatentImage),
latent_upscale: groupArray(GroupLatentUpscale),
sd_upscale: groupArray(GroupSDUpscale),
hypernetwork: groupArray(GroupHypernetwork),
lora: groupArray(GroupLoRA),
control_net: groupArray(GroupControlNet),
dynamic_thresholding: groupArray(GroupDynamicThresholding),
aesthetic_embedding: groupArray(GroupAestheticEmbedding),
self_attention_guidance: groupArray(GroupSelfAttentionGuidance),
ddetailer: groupArray(GroupDDetailer)
}).partial()
export type ComfyBoxStdParameters = z.infer<typeof Parameters>
const ComfyBoxExtraData = z.object({
workflows: z.array(z.string())
})
const A1111ExtraData = z.object({
params: z.any()
})
const ExtraData = z.object({
comfybox: ComfyBoxExtraData.optional(),
a1111: A1111ExtraData.optional()
})
const Metadata = z.object({
created_with: z.string(),
author: z.string().optional(),
app_version: z.string().optional(),
commit_hash: z.string().optional(),
extra_data: ExtraData
})
const ComfyBoxStdPrompt = z.object({
version: z.number(),
metadata: Metadata,
parameters: Parameters
})
export default ComfyBoxStdPrompt
/*
* A standardized Stable Diffusion parameter format that should be used with an
* encompassing workflow. Aims to encompass an arbitrary number of parameter
* counts and types, so that most ComfyUI workflows can have parts of their
* prompts transferred between each other.
*
* This format does *not* describe how the information should be used in the
* underlying workflow, i.e. it does not specify the structure of a ComfyUI
* execution graph. It only gives hints via tagged input types on each input
* entry as to where the data should be inserted. To recreate a ComfyBox
* workflow with the exact state of the UI intact, the `SerializedAppState` type
* should be used instead. It suffices to embed data of that type in the output
* PNGs for recreating their workflows. This type is meant as an interchange
* format *between* workflows so their inputs can be copied to and from each
* other in a sane-enough manner. (In ComfyBox, copying workflow outputs like
* images to other workflows is handled separately, since this type does not
* retain the actual image data.)
*
* In contrast with a serialized workflow, which is concerned with the
* connections between nodes and the state of the frontend's UI, this format
* concerns itself with the exact values that the execution backend receives,
* after the data in the UI have finished processing.
*
* (Take for example a "scale by" slider that adjusts the width and height of an
* img2img input image of 512 x 512 resolution by 2x. The backend will only
* "see" width 1024 and height 1024, even though the only parameter exposed from
* the frontend was the scale of 2.)
*/
export type ComfyBoxStdPrompt = z.infer<typeof ComfyBoxStdPrompt>

View File

@@ -0,0 +1,55 @@
import type { ComfyBoxStdGroupLoRA, ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
import type { SerializedPrompt, SerializedPromptInputs } from "./components/ComfyApp";
export type ComfyPromptConverter = (stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs, nodeID: ComfyNodeID) => void;
function LoraLoader(stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs) {
const params = stdPrompt.parameters
const lora: ComfyBoxStdGroupLoRA = {
model_name: inputs["lora_name"],
strength_unet: inputs["strength_model"],
strength_tenc: inputs["strength_clip"]
}
if (params.lora)
params.lora.push(lora)
else
params.lora = [lora]
}
const ALL_CONVERTERS: Record<string, ComfyPromptConverter> = {
LoraLoader
}
const COMMIT_HASH: string = __GIT_COMMIT_HASH__;
export default class ComfyBoxStdPromptSerializer {
serialize(prompt: SerializedPrompt): ComfyBoxStdPrompt {
const stdPrompt: ComfyBoxStdPrompt = {
version: 1,
metadata: {
created_with: "ComfyBox",
commit_hash: COMMIT_HASH,
extra_data: {
comfybox: {
}
}
},
parameters: {}
}
for (const [nodeID, inputs] of Object.entries(prompt.output)) {
const classType = inputs.class_type
const converter = ALL_CONVERTERS[classType]
if (converter) {
converter(stdPrompt, inputs.inputs, nodeID)
}
else {
console.warn("No StdPrompt type converter for comfy class!", classType)
}
}
return stdPrompt
}
}

View File

@@ -98,7 +98,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
} }
if (color) { if (color) {
this.drawNodeOutline(node, ctx, size, fgColor, bgColor, color, thickness) this.drawNodeOutline(node, ctx, size, mouseOver, fgColor, bgColor, color, thickness)
} }
if (isRunningNode && state.progress) { if (isRunningNode && state.progress) {
@@ -108,27 +108,36 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
} }
} }
private drawNodeOutline(node: LGraphNode, ctx: CanvasRenderingContext2D, size: Vector2, fgColor: string, bgColor: string, outlineColor: string, outlineThickness: number) { private drawNodeOutline(node: LGraphNode, ctx: CanvasRenderingContext2D, size: Vector2, mouseOver: boolean, fgColor: string, bgColor: string, outlineColor: string, outlineThickness: number) {
const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE; const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE;
var render_title = true;
if (node.titleMode == TitleMode.TRANSPARENT_TITLE || node.titleMode == TitleMode.NO_TITLE) {
render_title = false;
} else if (node.titleMode == TitleMode.AUTOHIDE_TITLE && mouseOver) {
render_title = true;
}
const titleHeight = render_title ? LiteGraph.NODE_TITLE_HEIGHT : 0;
ctx.lineWidth = outlineThickness; ctx.lineWidth = outlineThickness;
ctx.globalAlpha = 0.8; ctx.globalAlpha = 0.8;
ctx.beginPath(); ctx.beginPath();
if (shape == BuiltInSlotShape.BOX_SHAPE) if (shape == BuiltInSlotShape.BOX_SHAPE)
ctx.rect(-6, -6 + LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT); ctx.rect(-6, -6 + titleHeight, 12 + size[0] + 1, 12 + size[1] + titleHeight);
else if (shape == BuiltInSlotShape.ROUND_SHAPE || (shape == BuiltInSlotShape.CARD_SHAPE && node.flags.collapsed)) else if (shape == BuiltInSlotShape.ROUND_SHAPE || (shape == BuiltInSlotShape.CARD_SHAPE && node.flags.collapsed))
ctx.roundRect( ctx.roundRect(
-6, -6,
-6 - LiteGraph.NODE_TITLE_HEIGHT, -6 - titleHeight,
12 + size[0] + 1, 12 + size[0] + 1,
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, 12 + size[1] + titleHeight,
this.round_radius * 2 this.round_radius * 2
); );
else if (shape == BuiltInSlotShape.CARD_SHAPE) else if (shape == BuiltInSlotShape.CARD_SHAPE)
ctx.roundRect( ctx.roundRect(
-6, -6,
-6 + LiteGraph.NODE_TITLE_HEIGHT, -6 + titleHeight,
12 + size[0] + 1, 12 + size[0] + 1,
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, 12 + size[1] + titleHeight,
this.round_radius * 2, this.round_radius * 2,
2 2
); );

View File

@@ -1,4 +1,4 @@
import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutputs } from "./components/ComfyApp"; import type { Progress, SerializedPrompt, SerializedPromptInputsForNode, SerializedPromptInputsAll, SerializedPromptOutputs } from "./components/ComfyApp";
import type TypedEmitter from "typed-emitter"; import type TypedEmitter from "typed-emitter";
import EventEmitter from "events"; import EventEmitter from "events";
import type { ComfyImageLocation } from "$lib/utils"; import type { ComfyImageLocation } from "$lib/utils";

View File

@@ -0,0 +1,114 @@
<script lang="ts">
import ComfyBoxStdPrompt from "$lib/ComfyBoxStdPrompt";
import type { A1111ParsedInfotext } from "$lib/parseA1111";
import { Block, BlockTitle } from "@gradio/atoms";
import { TextBox } from "@gradio/form";
import { JsonView } from '@zerodevx/svelte-json-view'
import type { A1111PromptAndInfo } from "./ComfyApp";
import { StaticImage } from "./gradio/image";
export let prompt: A1111PromptAndInfo | null = null;
let json: any = {}
let a1111: A1111ParsedInfotext | null = null
let infotext: string = ""
let image: string | null = null;
$: if (prompt) {
infotext = prompt.infotext;
a1111 = prompt.parsedInfotext;
json = prompt.stdPrompt;
image = URL.createObjectURL(prompt.imageFile);
}
else {
infotext = ""
a1111 = null;
json = {}
image = null;
}
</script>
{#if prompt != null}
<div class="a1111-prompt-display">
<div class="prompt-container">
<Block>
<TextBox label="Infotext" show_label={true} value={infotext} lines={5} max_lines={20}/>
</Block>
<div class="scroll-container">
{#if a1111}
{#if Object.keys(a1111.extraParams).length > 0}
<Block>
<BlockTitle>Unused Parameters</BlockTitle>
<div class="json">
<JsonView json={a1111.extraParams} />
</div>
</Block>
{/if}
{/if}
<Block>
<BlockTitle>Converted Prompt</BlockTitle>
<div class="json">
<JsonView {json} />
</div>
</Block>
</div>
</div>
<Block>
<StaticImage show_label={false} label="Image" value={image} />
</Block>
</div>
{/if}
<style lang="scss">
.a1111-prompt-display {
width: 70vw;
height: 70vh;
color: none;
--jsonPaddingLeft: 1rem;
--jsonBorderLeft: 1px dotted var(--neutral-600);
--jsonBracketColor: currentcolor;
--jsonBracketHoverBackground: var(--neutral-100);
--jsonSeparatorColor: currentcolor;
--jsonKeyColor: var(--body-text-color);
--jsonValColor: var(--body-text-color-subdued);
--jsonValStringColor: var(--color-green-500);
--jsonValNumberColor: var(--color-blue-500);
--jsonValBooleanColor: var(--color-red-500);
display: flex;
flex-wrap: nowrap;
overflow-y: none;
flex-direction: row;
.prompt-container {
flex-direction: column;
overflow: auto;
.json {
font-family: monospace;
}
.scroll-container {
position: relative;
flex: 1 1 0%;
height: 100%;
}
}
:global(> .block) {
background: var(--panel-background-fill);
}
.accordion {
background: var(--panel-background-fill);
:global(> .block .block) {
background: var(--panel-background-fill);
}
}
}
</style>

View File

@@ -25,7 +25,7 @@
export let isMobile: boolean = false; export let isMobile: boolean = false;
let isOpen: Writable<boolean> | null = null; let isOpen: Writable<boolean> | null = null;
let children: IDragItem[] | null = null; let children: IDragItem[] = [];
const flipDurationMs = 100; const flipDurationMs = 100;
let selectedIndex: number = 0; let selectedIndex: number = 0;
@@ -61,7 +61,7 @@
} }
</script> </script>
{#if container && Array.isArray(children)} {#if container}
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)} {@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}" <div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.containerVariant === "hidden"} class:hide-block={container.attrs.containerVariant === "hidden"}

View File

@@ -24,7 +24,7 @@
export let isMobile: boolean = false; export let isMobile: boolean = false;
let attrsChanged: Writable<number> | null = null; let attrsChanged: Writable<number> | null = null;
let children: IDragItem[] | null = null; let children: IDragItem[] = [];
const flipDurationMs = 100; const flipDurationMs = 100;
$: if (container) { $: if (container) {
@@ -35,12 +35,12 @@
} }
else { else {
container = null; container = null;
children = null; children = [];
attrsChanged = null; attrsChanged = null;
} }
} }
else { else {
children = null; children = [];
attrsChanged = null attrsChanged = null
} }
@@ -55,7 +55,7 @@
}; };
</script> </script>
{#if container && Array.isArray(children)} {#if container}
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)} {@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}" <div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.containerVariant === "hidden"} class:hide-block={container.attrs.containerVariant === "hidden"}

View File

@@ -5,13 +5,12 @@
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import { BlockTitle } from "@gradio/atoms"; import { BlockTitle } from "@gradio/atoms";
import ComfyUIPane from "./ComfyUIPane.svelte"; import ComfyUIPane from "./ComfyUIPane.svelte";
import ComfyApp, { type SerializedAppState } from "./ComfyApp"; import ComfyApp, { type A1111PromptAndInfo, type SerializedAppState } from "./ComfyApp";
import { Checkbox, TextBox } from "@gradio/form" import { Checkbox, TextBox } from "@gradio/form"
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import layoutState from "$lib/stores/layoutState"; import layoutState from "$lib/stores/layoutState";
import selectionState from "$lib/stores/selectionState"; import selectionState from "$lib/stores/selectionState";
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
import type { ComfyAPIStatus } from "$lib/api";
import { SvelteToast, toast } from '@zerodevx/svelte-toast' import { SvelteToast, toast } from '@zerodevx/svelte-toast'
import { LGraph } from "@litegraph-ts/core"; import { LGraph } from "@litegraph-ts/core";
@@ -20,14 +19,18 @@
import ComfyProperties from "./ComfyProperties.svelte"; import ComfyProperties from "./ComfyProperties.svelte";
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte"; import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte";
import ComfyGraphView from "./ComfyGraphView.svelte"; import ComfyGraphView from "./ComfyGraphView.svelte";
import { download, jsonToJsObject } from "$lib/utils"; import { download, jsonToJsObject } from "$lib/utils";
import notify from "$lib/notify"; import notify from "$lib/notify";
import Modal from "./Modal.svelte";
import ComfyBoxStdPrompt from "$lib/ComfyBoxStdPrompt";
import A1111PromptDisplay from "./A1111PromptDisplay.svelte";
import type { A1111ParsedInfotext } from "$lib/parseA1111";
export let app: ComfyApp = undefined; export let app: ComfyApp = undefined;
let queue: ComfyQueue = undefined; let alreadySetup: Writable<boolean> = writable(false);
let a1111Prompt: Writable<A1111PromptAndInfo | null> = writable(null);
let mainElem: HTMLDivElement; let mainElem: HTMLDivElement;
let uiPane: ComfyUIPane = undefined;
let props: ComfyProperties = undefined; let props: ComfyProperties = undefined;
let containerElem: HTMLDivElement; let containerElem: HTMLDivElement;
let resizeTimeout: NodeJS.Timeout | null; let resizeTimeout: NodeJS.Timeout | null;
@@ -44,6 +47,11 @@
} }
} }
$: if(app) {
alreadySetup = app.alreadySetup;
a1111Prompt = app.a1111Prompt;
}
function refreshView(event?: Event) { function refreshView(event?: Event) {
clearTimeout(resizeTimeout); clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(app.resizeCanvas.bind(app), 250); resizeTimeout = setTimeout(app.resizeCanvas.bind(app), 250);
@@ -188,6 +196,10 @@
else { else {
document.getElementById("app-root").classList.remove("dark") document.getElementById("app-root").classList.remove("dark")
} }
let showModal: boolean = false;
$: showModal = $a1111Prompt != null
</script> </script>
<svelte:head> <svelte:head>
@@ -196,8 +208,19 @@
{/if} {/if}
</svelte:head> </svelte:head>
<Modal bind:showModal on:close={() => ($a1111Prompt = null)}>
<div slot="header" class="prompt-modal-header">
<h1 style="padding-bottom: 1rem;">A1111 Prompt Details</h1>
</div>
<A1111PromptDisplay prompt={$a1111Prompt} />
<div slot="buttons" let:closeDialog>
<Button variant="secondary" on:click={closeDialog}>
Close
</Button>
</div>
</Modal>
<div id="main" class:dark={uiTheme === "gradio-dark"}> <div id="main" class:dark={uiTheme === "gradio-dark"}>
<div id="dropzone" class="dropzone"></div>
<div id="container" bind:this={containerElem}> <div id="container" bind:this={containerElem}>
<Splitpanes theme="comfy" on:resize={refreshView}> <Splitpanes theme="comfy" on:resize={refreshView}>
<Pane bind:size={propsSidebarSize}> <Pane bind:size={propsSidebarSize}>
@@ -208,7 +231,7 @@
<Pane> <Pane>
<Splitpanes theme="comfy" on:resize={refreshView} horizontal="{true}"> <Splitpanes theme="comfy" on:resize={refreshView} horizontal="{true}">
<Pane> <Pane>
<ComfyUIPane bind:this={uiPane} {app} /> <ComfyUIPane {app} />
</Pane> </Pane>
<Pane bind:size={graphSize}> <Pane bind:size={graphSize}>
<ComfyGraphView {app} transitioning={graphTransitioning} /> <ComfyGraphView {app} transitioning={graphTransitioning} />
@@ -217,7 +240,7 @@
</Pane> </Pane>
<Pane bind:size={queueSidebarSize}> <Pane bind:size={queueSidebarSize}>
<div class="sidebar-wrapper pane-wrapper"> <div class="sidebar-wrapper pane-wrapper">
<ComfyQueue bind:this={queue} /> <ComfyQueue {app} />
</div> </div>
</Pane> </Pane>
</Splitpanes> </Splitpanes>
@@ -225,35 +248,35 @@
<div id="bottombar"> <div id="bottombar">
<div class="left"> <div class="left">
{#if $layoutState.attrs.queuePromptButtonName != ""} {#if $layoutState.attrs.queuePromptButtonName != ""}
<Button variant="primary" on:click={queuePrompt}> <Button variant="primary" disabled={!$alreadySetup} on:click={queuePrompt}>
{$layoutState.attrs.queuePromptButtonName} {$layoutState.attrs.queuePromptButtonName}
</Button> </Button>
{/if} {/if}
<Button variant="secondary" on:click={toggleGraph}> <Button variant="secondary" disabled={!$alreadySetup} on:click={toggleGraph}>
Toggle Graph Toggle Graph
</Button> </Button>
<Button variant="secondary" on:click={toggleProps}> <Button variant="secondary" disabled={!$alreadySetup} on:click={toggleProps}>
Toggle Props Toggle Props
</Button> </Button>
<Button variant="secondary" on:click={toggleQueue}> <Button variant="secondary" disabled={!$alreadySetup} on:click={toggleQueue}>
Toggle Queue Toggle Queue
</Button> </Button>
<Button variant="secondary" on:click={doSave}> <Button variant="secondary" disabled={!$alreadySetup} on:click={doSave}>
Save Save
</Button> </Button>
<Button variant="secondary" on:click={doSaveLocal}> <Button variant="secondary" disabled={!$alreadySetup} on:click={doSaveLocal}>
Save Local Save Local
</Button> </Button>
<Button variant="secondary" on:click={doLoad}> <Button variant="secondary" disabled={!$alreadySetup} on:click={doLoad}>
Load Load
</Button> </Button>
<Button variant="secondary" on:click={doClear}> <Button variant="secondary" disabled={!$alreadySetup} on:click={doClear}>
Clear Clear
</Button> </Button>
<Button variant="secondary" on:click={doLoadDefault}> <Button variant="secondary" disabled={!$alreadySetup} on:click={doLoadDefault}>
Load Default Load Default
</Button> </Button>
<Button variant="secondary" on:click={doRefreshCombos}> <Button variant="secondary" disabled={!$alreadySetup} on:click={doRefreshCombos}>
🔄 🔄
</Button> </Button>
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/> <!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
@@ -325,19 +348,6 @@
height: 100%; height: 100%;
} }
.dropzone {
box-sizing: border-box;
display: none;
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 99999;
background: #60a7dc80;
border: 4px dashed #60a7dc;
}
:global(html, body) { :global(html, body) {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@@ -26,7 +26,7 @@ import layoutState from "$lib/stores/layoutState";
import { toast } from '@zerodevx/svelte-toast' import { toast } from '@zerodevx/svelte-toast'
import ComfyGraph from "$lib/ComfyGraph"; import ComfyGraph from "$lib/ComfyGraph";
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import { get } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import { tick } from "svelte"; import { tick } from "svelte";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import { download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils"; import { download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils";
@@ -37,6 +37,10 @@ import type { ComfyExecutionResult } from "$lib/utils";
import ComfyPromptSerializer, { UpstreamNodeLocator, isActiveBackendNode } from "./ComfyPromptSerializer"; import ComfyPromptSerializer, { UpstreamNodeLocator, isActiveBackendNode } from "./ComfyPromptSerializer";
import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef"; import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef";
import { ComfyComboNode } from "$lib/nodes/widgets"; import { ComfyComboNode } from "$lib/nodes/widgets";
import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111";
import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt";
import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer";
export const COMFYBOX_SERIAL_VERSION = 1; export const COMFYBOX_SERIAL_VERSION = 1;
@@ -45,32 +49,68 @@ if (typeof window !== "undefined") {
nodes.ComfyReroute.setDefaultTextVisibility(!!localStorage["Comfy.ComfyReroute.DefaultVisibility"]); nodes.ComfyReroute.setDefaultTextVisibility(!!localStorage["Comfy.ComfyReroute.DefaultVisibility"]);
} }
type QueueItem = { num: number, batchCount: number } /*
* Queued prompt that hasn't been sent to the backend yet.
* TODO: Assumes the currently active graph will be serialized, needs to change
* for multiple loaded workflow support
*/
type QueueItem = {
num: number,
batchCount: number
}
export type A1111PromptAndInfo = {
infotext: string,
parsedInfotext: A1111ParsedInfotext,
stdPrompt: ComfyBoxStdPrompt,
imageFile: File
}
/*
* Represents a single workflow that can be loaded into the program from JSON.
*/
export type SerializedAppState = { export type SerializedAppState = {
/** Program identifier, should always be "ComfyBox" */
createdBy: "ComfyBox", createdBy: "ComfyBox",
/** Serial version, should be incremented on breaking changes */
version: number, version: number,
/** Commit hash if found */
commitHash?: string,
/** Graph state */
workflow: SerializedLGraph, workflow: SerializedLGraph,
/** UI state */
layout: SerializedLayoutState, layout: SerializedLayoutState,
/** Position/offset of the canvas at the time of saving */
canvas: SerializedGraphCanvasState canvas: SerializedGraphCanvasState
} }
/** [link origin, link index] | value */ /** [link_origin, link_slot_index] | input_value */
export type SerializedPromptInput = [ComfyNodeID, number] | any export type SerializedPromptInput = [ComfyNodeID, number] | any
export type SerializedPromptInputs = { export type SerializedPromptInputs = Record<string, SerializedPromptInput>;
/*
* A single node in the prompt and its input values.
*/
export type SerializedPromptInputsForNode = {
/* property name -> value or link */ /* property name -> value or link */
inputs: Record<string, SerializedPromptInput>, inputs: SerializedPromptInputs,
class_type: string class_type: string
} }
export type SerializedPromptInputsAll = Record<ComfyNodeID, SerializedPromptInputs> /*
* All nodes in the graph and their input values.
*/
export type SerializedPromptInputsAll = Record<ComfyNodeID, SerializedPromptInputsForNode>
export type SerializedPrompt = { export type SerializedPrompt = {
workflow: SerializedLGraph, workflow: SerializedLGraph,
output: SerializedPromptInputsAll output: SerializedPromptInputsAll
} }
/*
* Outputs for each node.
*/
export type SerializedPromptOutputs = Record<ComfyNodeID, ComfyExecutionResult> export type SerializedPromptOutputs = Record<ComfyNodeID, ComfyExecutionResult>
export type Progress = { export type Progress = {
@@ -78,6 +118,10 @@ export type Progress = {
max: number max: number
} }
/*
* A combo node and the backend node that will send an updated config over, for
* refreshing lists of model files
*/
type BackendComboNode = { type BackendComboNode = {
comboNode: ComfyComboNode, comboNode: ComfyComboNode,
comfyInput: IComfyInputSlot, comfyInput: IComfyInputSlot,
@@ -95,20 +139,24 @@ export default class ComfyApp {
nodeOutputs: Record<string, any> = {}; nodeOutputs: Record<string, any> = {};
shiftDown: boolean = false; shiftDown: boolean = false;
ctrlDown: boolean = false;
selectedGroupMoving: boolean = false; selectedGroupMoving: boolean = false;
alreadySetup: Writable<boolean> = writable(false);
a1111Prompt: Writable<A1111PromptAndInfo | null> = writable(null);
private queueItems: QueueItem[] = []; private queueItems: QueueItem[] = [];
private processingQueue: boolean = false; private processingQueue: boolean = false;
private alreadySetup = false;
private promptSerializer: ComfyPromptSerializer; private promptSerializer: ComfyPromptSerializer;
private stdPromptSerializer: ComfyBoxStdPromptSerializer;
constructor() { constructor() {
this.api = new ComfyAPI(); this.api = new ComfyAPI();
this.promptSerializer = new ComfyPromptSerializer(); this.promptSerializer = new ComfyPromptSerializer();
this.stdPromptSerializer = new ComfyBoxStdPromptSerializer();
} }
async setup(): Promise<void> { async setup(): Promise<void> {
if (this.alreadySetup) { if (get(this.alreadySetup)) {
console.error("Already setup") console.error("Already setup")
return; return;
} }
@@ -151,7 +199,6 @@ export default class ComfyApp {
// setInterval(this.saveStateToLocalStorage.bind(this), 1000); // setInterval(this.saveStateToLocalStorage.bind(this), 1000);
this.addApiUpdateHandlers(); this.addApiUpdateHandlers();
this.addDropHandler();
this.addPasteHandler(); this.addPasteHandler();
this.addKeyboardHandler(); this.addKeyboardHandler();
@@ -165,7 +212,7 @@ export default class ComfyApp {
this.requestPermissions(); this.requestPermissions();
this.alreadySetup = true; this.alreadySetup.set(true);
return Promise.resolve(); return Promise.resolve();
} }
@@ -286,70 +333,28 @@ export default class ComfyApp {
} }
} }
private showDropZone() {
if (this.dropZone)
this.dropZone.style.display = "block";
}
private hideDropZone() {
if (this.dropZone)
this.dropZone.style.display = "none";
}
private allowDrag(event: DragEvent) {
if (event.dataTransfer.items?.length > 0) {
event.dataTransfer.dropEffect = 'copy';
this.showDropZone();
event.preventDefault();
}
}
private async handleDrop(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
this.hideDropZone();
if (event.dataTransfer.files.length > 0) {
await this.handleFile(event.dataTransfer.files[0]);
}
}
private addDropHandler() {
// this.dropZone = document.getElementById("dropzone");
// if (this.dropZone) {
// window.addEventListener('dragenter', this.allowDrag.bind(this));
// this.dropZone.addEventListener('dragover', this.allowDrag.bind(this));
// this.dropZone.addEventListener('dragleave', this.hideDropZone.bind(this));
// this.dropZone.addEventListener('drop', this.handleDrop.bind(this));
// }
// else {
// console.warn("No dropzone detected (probably on mobile).")
// }
}
/** /**
* Adds a handler on paste that extracts and loads workflows from pasted JSON data * Adds a handler on paste that extracts and loads workflows from pasted JSON data
*/ */
private addPasteHandler() { private addPasteHandler() {
// document.addEventListener("paste", (e) => { document.addEventListener("paste", (e) => {
// let data = (e.clipboardData || (window as any).clipboardData).getData("text/plain"); let data = (e.clipboardData || (window as any).clipboardData).getData("text/plain");
// let workflow; let workflow;
// try { try {
// data = data.slice(data.indexOf("{")); data = data.slice(data.indexOf("{"));
// workflow = JSON.parse(data); workflow = JSON.parse(data);
// } catch (err) { } catch (err) {
// try { try {
// data = data.slice(data.indexOf("workflow\n")); data = data.slice(data.indexOf("workflow\n"));
// data = data.slice(data.indexOf("{")); data = data.slice(data.indexOf("{"));
// workflow = JSON.parse(data); workflow = JSON.parse(data);
// } catch (error) { } } catch (error) { }
// } }
// if (workflow && workflow.version && workflow.nodes && workflow.extra) { if (workflow && workflow.version && workflow.nodes && workflow.extra) {
// this.loadGraphData(workflow); this.loadGraphData(workflow);
// } }
// }); });
} }
/** /**
@@ -406,6 +411,7 @@ export default class ComfyApp {
private addKeyboardHandler() { private addKeyboardHandler() {
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
this.shiftDown = e.shiftKey; this.shiftDown = e.shiftKey;
this.ctrlDown = e.ctrlKey;
// Queue prompt using ctrl or command + enter // Queue prompt using ctrl or command + enter
if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.keyCode === 13 || e.keyCode === 10)) { if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.keyCode === 13 || e.keyCode === 10)) {
@@ -414,6 +420,7 @@ export default class ComfyApp {
}); });
window.addEventListener("keyup", (e) => { window.addEventListener("keyup", (e) => {
this.shiftDown = e.shiftKey; this.shiftDown = e.shiftKey;
this.ctrlDown = e.ctrlKey;
}); });
} }
@@ -561,7 +568,9 @@ export default class ComfyApp {
} }
if (get(layoutState).attrs.queuePromptButtonRunWorkflow) { if (get(layoutState).attrs.queuePromptButtonRunWorkflow) {
this.queuePrompt(0, 1); // Hold control to queue at the front
const num = this.ctrlDown ? -1 : 0;
this.queuePrompt(num, 1);
} }
} }
@@ -645,6 +654,9 @@ export default class ComfyApp {
console.debug(graphToGraphVis(this.lGraph)) console.debug(graphToGraphVis(this.lGraph))
console.debug(promptToGraphVis(p)) console.debug(promptToGraphVis(p))
const stdPrompt = this.stdPromptSerializer.serialize(p);
console.warn("STD", stdPrompt);
const extraData: ComfyBoxPromptExtraData = { const extraData: ComfyBoxPromptExtraData = {
extra_pnginfo: { extra_pnginfo: {
workflow: p.workflow, workflow: p.workflow,
@@ -709,8 +721,18 @@ export default class ComfyApp {
if (pngInfo.comfyBoxConfig) { if (pngInfo.comfyBoxConfig) {
this.deserialize(JSON.parse(pngInfo.comfyBoxConfig)); this.deserialize(JSON.parse(pngInfo.comfyBoxConfig));
} else if (pngInfo.parameters) { } else if (pngInfo.parameters) {
throw "TODO A111 import!" const parsed = parseA1111(pngInfo.parameters)
// importA1111(this.lGraph, pngInfo.parameters, this.api); if ("error" in parsed) {
notify(`Couldn't parse webui prompt: ${parsed.error}`, { type: "error" })
return;
}
const converted = convertA1111ToStdPrompt(parsed)
this.a1111Prompt.set({
infotext: pngInfo.parameters,
parsedInfotext: parsed,
stdPrompt: converted,
imageFile: file
})
} }
else { else {
console.error("No metadata found in image file.", pngInfo) console.error("No metadata found in image file.", pngInfo)
@@ -854,5 +876,6 @@ export default class ComfyApp {
*/ */
clean() { clean() {
this.nodeOutputs = {}; this.nodeOutputs = {};
this.a1111Prompt.set(null);
} }
} }

View File

@@ -2,7 +2,7 @@ import type ComfyGraph from "$lib/ComfyGraph";
import type { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; import type { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode"; import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import { GraphInput, GraphOutput, LGraph, LGraphNode, LLink, NodeMode, Subgraph, type SlotIndex } from "@litegraph-ts/core"; import { GraphInput, GraphOutput, LGraph, LGraphNode, LLink, NodeMode, Subgraph, type SlotIndex } from "@litegraph-ts/core";
import type { SerializedPrompt, SerializedPromptInput, SerializedPromptInputs, SerializedPromptInputsAll } from "./ComfyApp"; import type { SerializedPrompt, SerializedPromptInput, SerializedPromptInputsForNode, SerializedPromptInputsAll, SerializedPromptInputs } from "./ComfyApp";
import type IComfyInputSlot from "$lib/IComfyInputSlot"; import type IComfyInputSlot from "$lib/IComfyInputSlot";
function hasTag(node: LGraphNode, tag: string): boolean { function hasTag(node: LGraphNode, tag: string): boolean {
@@ -113,7 +113,7 @@ export class UpstreamNodeLocator {
} }
// If there are non-target nodes between us and another // If there are non-target nodes between us and another
// backend node, we have to traverse them first. This // target node, we have to traverse them first. This
// behavior is dependent on the type of node. Reroute nodes // behavior is dependent on the type of node. Reroute nodes
// will simply follow their single input, while branching // will simply follow their single input, while branching
// nodes have conditional logic that determines which link // nodes have conditional logic that determines which link
@@ -150,7 +150,7 @@ export class UpstreamNodeLocator {
} }
export default class ComfyPromptSerializer { export default class ComfyPromptSerializer {
serializeInputValues(node: ComfyBackendNode): Record<string, SerializedPromptInput> { serializeInputValues(node: ComfyBackendNode): SerializedPromptInputs {
// Store input values passed by frontend-only nodes // Store input values passed by frontend-only nodes
if (!node.inputs) { if (!node.inputs) {
return {} return {}

View File

@@ -11,7 +11,10 @@
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import type ComfyApp from "./ComfyApp"; import type ComfyApp from "./ComfyApp";
import { tick } from "svelte"; import { tick } from "svelte";
import Modal from "./Modal.svelte"; import Modal from "./Modal.svelte";
import DropZone from "./DropZone.svelte";
export let app: ComfyApp;
let queuePending: Writable<QueueEntry[]> | null = null; let queuePending: Writable<QueueEntry[]> | null = null;
let queueRunning: Writable<QueueEntry[]> | null = null; let queueRunning: Writable<QueueEntry[]> | null = null;
@@ -197,6 +200,7 @@
</Modal> </Modal>
<div class="queue"> <div class="queue">
<DropZone {app} />
<div class="queue-entries {mode}-mode" bind:this={queueList}> <div class="queue-entries {mode}-mode" bind:this={queueList}>
{#if _entries.length > 0} {#if _entries.length > 0}
{#each _entries as entry} {#each _entries as entry}

View File

@@ -0,0 +1,78 @@
<script lang="ts">
import { writable, type Writable } from "svelte/store";
import type ComfyApp from "./ComfyApp";
export let app: ComfyApp;
let a1111Prompt: Writable<any | null> = writable(null);
let dropZone: HTMLDivElement | null = null;
let disabled = false;
$: a1111Prompt = app.a1111Prompt;
$: disabled = a1111Prompt && $a1111Prompt;
$: if (disabled) {
hideDropZone();
}
function showDropZone() {
if (dropZone && !disabled)
dropZone.style.display = "block";
}
function hideDropZone() {
if (dropZone)
dropZone.style.display = "none";
}
function allowDrag(event: DragEvent) {
if (disabled)
return
if (event.dataTransfer != null && event.dataTransfer.items?.length > 0) {
event.dataTransfer.dropEffect = 'copy';
showDropZone();
event.preventDefault();
}
}
async function handleDrop(event: DragEvent) {
if (disabled)
return
event.preventDefault();
event.stopPropagation();
hideDropZone();
if (event.dataTransfer != null && event.dataTransfer.files.length > 0) {
await app.handleFile(event.dataTransfer.files[0]);
}
}
</script>
<svelte:window on:dragenter={showDropZone} />
{#if !disabled}
<div id="dropzone"
class="dropzone"
bind:this={dropZone}
on:dragover={allowDrag}
on:dragleave={hideDropZone}
on:drop={handleDrop}
/>
{/if}
<style lang="scss">
.dropzone {
box-sizing: border-box;
display: none;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 99999;
background: #60a7dc80;
border: 4px dashed #60a7dc;
}
</style>

View File

@@ -94,5 +94,6 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: var(--spacing-sm); gap: var(--spacing-sm);
padding-top: 0.5em;
} }
</style> </style>

View File

@@ -25,7 +25,7 @@
export let isMobile: boolean = false; export let isMobile: boolean = false;
let attrsChanged: Writable<boolean> | null = null; let attrsChanged: Writable<boolean> | null = null;
let children: IDragItem[] | null = null; let children: IDragItem[] = [];
const flipDurationMs = 100; const flipDurationMs = 100;
let selectedIndex: number = 0; let selectedIndex: number = 0;
@@ -35,7 +35,7 @@
attrsChanged = container.attrsChanged attrsChanged = container.attrsChanged
} }
else { else {
children = null; children = [];
attrsChanged = null attrsChanged = null
} }
@@ -68,7 +68,7 @@
} }
</script> </script>
{#if container && Array.isArray(children)} {#if container}
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)} {@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}" <div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.containerVariant === "hidden"} class:hide-block={container.attrs.containerVariant === "hidden"}

View File

@@ -0,0 +1,377 @@
import type { ComfyBoxStdGroupAestheticEmbedding, ComfyBoxStdGroupCheckpoint, ComfyBoxStdGroupDDetailer, ComfyBoxStdGroupDynamicThresholding, ComfyBoxStdGroupHypernetwork, ComfyBoxStdGroupKSampler, ComfyBoxStdGroupLatentImage, ComfyBoxStdGroupLatentUpscale, ComfyBoxStdGroupLoRA, ComfyBoxStdGroupSelfAttentionGuidance, ComfyBoxStdParameters, ComfyBoxStdPrompt } from "./ComfyBoxStdPrompt";
import type { A1111ParsedInfotext } from "./parseA1111";
function getSamplerAndScheduler(a1111Sampler: string): [string, string] {
let name = a1111Sampler.toLowerCase().replace("++", "pp").replaceAll(" ", "_");
let scheduler = "normal";
if (name.includes("karras")) {
name = name.replace("karras", "").replace(/_+$/, "");
scheduler = "karras";
} else {
scheduler = "normal"
}
return [name, scheduler]
}
const reAddNetModelName = /^([^(]+)\((.+)\)$/;
function parseAddNetModelNameAndHash(name: string | null): [string | undefined, string | undefined] {
if (!name)
return [undefined, undefined]
const match = name.match(reAddNetModelName);
if (match != null) {
return [match[1], match[2]]
}
return [undefined, undefined]
}
const reDDetailerModelName = /(.+)\s\[(.+)\]/;
function parseDDetailerModelNameAndHash(name: string | null): [string | undefined, string | undefined] {
if (!name || name === "None")
return [undefined, undefined]
// bbox\mmdet_anime-face_yolov3.pth [51e1af4a]
const match = name.match(reDDetailerModelName);
if (match != null) {
return [match[1], match[2]]
}
return [undefined, undefined]
}
export default function convertA1111ToStdPrompt(infotext: A1111ParsedInfotext): ComfyBoxStdPrompt {
const popOpt = (name: string): string | undefined => {
const v = infotext.extraParams[name];
delete infotext.extraParams[name];
return v;
}
const parameters: ComfyBoxStdParameters = {}
parameters.conditioning = [
{
"$meta": {
types: ["positive"]
},
text: infotext.positive,
},
{
"$meta": {
types: ["negative"]
},
text: infotext.negative,
}
]
const hrUp = popOpt("hires upscale");
const hrSz = popOpt("hires resize");
let hrScaleBy = hrUp ? parseFloat(hrUp) : undefined;
let hrMethod = popOpt("hires upscaler");
let hrSteps = popOpt("hires steps");
let hrWidth = undefined
let hrHeight = undefined
if (hrSz) {
[hrWidth, hrHeight] = hrSz.split(hrSz).map(parseInt);
}
const latent_image: ComfyBoxStdGroupLatentImage = {
width: infotext.width,
height: infotext.height,
// type: "empty", // detect txt2img???
batch_count: infotext.batchSize,
batch_pos: infotext.batchPos,
}
const maskBlur = popOpt("mask blur")
if (maskBlur != null)
latent_image.mask_blur = parseFloat(maskBlur)
parameters.latent_image = [latent_image];
if (hrMethod != null) {
let uw, uh;
if (hrScaleBy) {
uw = infotext.width * hrScaleBy;
uh = infotext.height * hrScaleBy;
} else {
if (hrWidth == null || hrHeight == null)
throw new Error("Highres prompt didn't have width/height!")
uw = +hrWidth;
uh = +hrHeight;
}
const hr: ComfyBoxStdGroupLatentUpscale = {
width: uw,
height: uh,
upscale_by: hrScaleBy,
upscale_method: hrMethod
}
parameters.latent_upscale = [hr];
}
const [sampler_name, scheduler] = getSamplerAndScheduler(infotext.sampler)
const k_sampler: ComfyBoxStdGroupKSampler = {
steps: infotext.steps,
seed: infotext.seed,
cfg_scale: infotext.cfgScale,
denoise: hrMethod != null ? 1.0 : infotext.denoise || 1.0, // detect img2img???
sampler_name,
scheduler,
}
parameters.k_sampler = [k_sampler];
if (hrMethod != null) {
const k_sampler_hr: ComfyBoxStdGroupKSampler = {
"$meta": {
types: ["upscale"]
},
steps: hrSteps != null ? parseInt(hrSteps) : infotext.steps,
seed: infotext.seed,
cfg_scale: infotext.cfgScale,
denoise: infotext.denoise || 1.0,
sampler_name,
scheduler,
}
parameters.k_sampler.push(k_sampler_hr)
}
if (infotext.modelHash || infotext.modelName) {
const checkpoint: ComfyBoxStdGroupCheckpoint = {
model_name: infotext.modelName,
model_hashes: {
a1111_shorthash: infotext.modelHash
}
}
parameters.checkpoint = [checkpoint]
}
if ("clip skip" in infotext.extraParams) {
const clipSkip = popOpt("clip skip")
parameters.clip = [{
clip_skip: parseInt(clipSkip)
}]
}
if ("sd upscale upscaler" in infotext.extraParams) {
const sdUpscaleUpscaler = popOpt("sd upscale upscaler")
const sdUpscaleOverlap = popOpt("sd upscale overlap") || "64"
parameters.sd_upscale = [{
upscaler: sdUpscaleUpscaler,
overlap: parseInt(sdUpscaleOverlap)
}]
}
if ("aesthetic embedding" in infotext.extraParams) {
const slerp = popOpt("aesthetic slerp") === "True"
const aesthetic_embedding: ComfyBoxStdGroupAestheticEmbedding = {
model_name: popOpt("aesthetic embedding"),
lr: parseFloat(popOpt("aesthetic lr")),
slerp,
slerp_angle: parseFloat(popOpt("aesthetic slerp angle")),
steps: parseInt(popOpt("aesthetic steps")),
text: popOpt("aesthetic text"),
text_negative: popOpt("aesthetic text negative") === "True",
weight: parseFloat(popOpt("aesthetic weight")),
}
parameters.aesthetic_embedding = [aesthetic_embedding]
}
if ("dynamic thresholding enabled" in infotext.extraParams) {
const dtEnabled = popOpt("dynamic thresholding enabled")
if (dtEnabled === "True") {
const dynamic_thresholding: ComfyBoxStdGroupDynamicThresholding = {
mimic_scale: parseInt(popOpt("mimic scale")),
threshold_percentile: parseFloat(popOpt("threshold percentile")),
mimic_mode: popOpt("mimic mode"),
mimic_scale_minimum: parseFloat(popOpt("mimic scale minimum")),
cfg_mode: popOpt("cfg mode"),
cfg_scale_minimum: parseFloat(popOpt("cfg scale minimum")),
}
parameters.dynamic_thresholding = [dynamic_thresholding]
}
}
if ("sag guidance scale" in infotext.extraParams) {
const self_attention_guidance: ComfyBoxStdGroupSelfAttentionGuidance = {
guidance_scale: parseFloat(popOpt("sag guidance scale")),
mask_threshold: parseFloat(popOpt("sag mask threshold")),
}
parameters.self_attention_guidance = [self_attention_guidance]
}
if ("ddetailer prompt" in infotext.extraParams) {
const positive_prompt = popOpt("ddetailer prompt")
const negative_prompt = popOpt("ddetailer neg prompt")
const bitwise = popOpt("ddetailer bitwise")
const denoise = parseFloat(popOpt("ddetailer denoising"))
const inpaint_full = popOpt("ddetailer inpaint full") === "True"
const inpaint_padding = parseInt(popOpt("ddetailer inpaint padding"))
const mask_blur = parseFloat(popOpt("ddetailer mask blur"))
const cfg = parseFloat(popOpt("ddetailer cfg"))
const preprocess_b = popOpt("ddetailer preprocess b") === "True"
const [model_a, model_a_shorthash] = parseDDetailerModelNameAndHash(popOpt("ddetailer model a"))
const [model_b, model_b_shorthash] = parseDDetailerModelNameAndHash(popOpt("ddetailer model b"))
const ddetailer_a: ComfyBoxStdGroupDDetailer = {
positive_prompt,
negative_prompt,
bitwise,
denoise,
inpaint_full,
inpaint_padding,
mask_blur,
cfg,
model: model_a,
model_hashes: model_a_shorthash ? {
a1111_shorthash: model_a_shorthash
} : undefined,
preprocess: !preprocess_b,
conf: parseFloat(popOpt("ddetailer conf a")),
dilation: parseFloat(popOpt("ddetailer dilation a")),
offset_x: parseFloat(popOpt("ddetailer offset x a")),
offset_y: parseFloat(popOpt("ddetailer offset y a")),
}
const ddetailer_b: ComfyBoxStdGroupDDetailer = {
positive_prompt,
negative_prompt,
bitwise,
denoise,
inpaint_full,
inpaint_padding,
mask_blur,
cfg,
model: model_b,
model_hashes: model_b_shorthash ? {
a1111_shorthash: model_b_shorthash
} : undefined,
preprocess: preprocess_b,
conf: parseFloat(popOpt("ddetailer conf b")),
dilation: parseFloat(popOpt("ddetailer dilation b")),
offset_x: parseFloat(popOpt("ddetailer offset x b")),
offset_y: parseFloat(popOpt("ddetailer offset y b")),
}
parameters.ddetailer = [ddetailer_a, ddetailer_b]
}
// TODO ControlNet
for (const [extraNetworkType, extraNetworks] of Object.entries(infotext.extraNetworks)) {
for (const extraNetworkParams of extraNetworks) {
let strength;
switch (extraNetworkType.toLowerCase()) {
case "lora":
case "locon":
case "lyco":
strength = parseFloat(extraNetworkParams.items[1]);
const lora: ComfyBoxStdGroupLoRA = {
module_name: extraNetworkType.toLowerCase(),
model_name: extraNetworkParams.items[0],
strength_unet: strength,
strength_tenc: strength,
}
if (parameters.lora)
parameters.lora.push(lora)
else
parameters.lora = [lora]
break;
case "hypernet":
strength = parseFloat(extraNetworkParams.items[1]);
const hypernetwork: ComfyBoxStdGroupHypernetwork = {
model_name: extraNetworkParams.items[0],
strength
}
if (parameters.hypernetwork)
parameters.hypernetwork.push(hypernetwork)
else
parameters.hypernetwork = [hypernetwork]
break;
default:
break;
}
}
delete infotext.extraNetworks[extraNetworkType]
}
let index = 1;
let found = infotext.extraParams[`addnet module ${index}`]
while (`addnet module ${index}` in infotext.extraParams) {
popOpt("addnet enabled")
const moduleName = popOpt(`addnet module ${index}`)
const modelName = popOpt(`addnet model ${index}`);
const weight = popOpt(`addnet weight ${index}`);
let weightA = popOpt(`addnet weight a ${index}`);
let weightB = popOpt(`addnet weight b ${index}`);
if (weightA == null || weightB == null) {
// linked weights before addnet version update
weightA = weight;
weightB = weight;
}
if (moduleName == null || modelName == null || weightA == null || weightB == null) {
throw new Error(`Error parsing addnet model params: ${moduleName} ${modelName} ${weightA} ${weightB}`)
}
if (moduleName !== "LoRA") {
throw new Error("Unknown AddNet model type " + moduleName)
}
const [name, hash] = parseAddNetModelNameAndHash(modelName);
if (name == null || hash == null) {
throw new Error("Error parsing addnet model name: " + JSON.stringify(modelName));
}
let shorthash = undefined
let shorthash_legacy = undefined
if (hash.length > 8) {
// new method using safetensors hash
shorthash = hash
}
else {
// old hash using webui's 0x10000 hashing method
shorthash_legacy = hash
}
const lora: ComfyBoxStdGroupLoRA = {
model_name: name,
module_name: moduleName,
model_hashes: {
addnet_shorthash: shorthash,
addnet_shorthash_legacy: shorthash_legacy
},
strength_unet: parseFloat(weightA),
strength_tenc: parseFloat(weightB),
}
if (parameters.lora)
parameters.lora.push(lora)
else
parameters.lora = [lora]
index += 1;
found = infotext.extraParams[`addnet model ${index}`]
}
let app_version = popOpt("version")
const extra_data: Record<string, any> = {};
if (Object.keys(infotext.extraParams).length > 0) {
extra_data.a1111 = { params: infotext.extraParams }
}
const prompt: ComfyBoxStdPrompt = {
version: 1,
metadata: {
created_with: "stable-diffusion-webui",
app_version,
extra_data
},
parameters
}
console.warn("Unhandled A1111 parameters:", infotext.extraParams, infotext.extraNetworks)
return prompt
}

View File

@@ -312,7 +312,9 @@ export class ComfyExecuteSubgraphAction extends ComfyGraphNode {
if (!app) if (!app)
return; return;
app.queuePrompt(0, 1, tag); // Hold control to queue at the front
const num = app.ctrlDown ? -1 : 0;
app.queuePrompt(num, 1, tag);
} }
} }

View File

@@ -40,13 +40,13 @@ export default class ComfyComboNode extends ComfyWidgetNode<string> {
// Wait until the initial graph load for combo to be valid. // Wait until the initial graph load for combo to be valid.
firstLoad: Writable<boolean>; firstLoad: Writable<boolean>;
lightUp: Writable<boolean>; lightUp: Writable<boolean>;
valuesForCombo: Writable<any[] | null>; // Changed when the combo box has values. valuesForCombo: Writable<any[]>; // Changed when the combo box has values.
constructor(name?: string) { constructor(name?: string) {
super(name, "A") super(name, "A")
this.firstLoad = writable(false) this.firstLoad = writable(false)
this.lightUp = writable(true) this.lightUp = writable(true)
this.valuesForCombo = writable(null) this.valuesForCombo = writable([])
} }
override onPropertyChanged(property: any, value: any) { override onPropertyChanged(property: any, value: any) {

166
src/lib/parseA1111.ts Normal file
View File

@@ -0,0 +1,166 @@
interface ExtraNetworkParams {
items: string[];
}
export type A1111ParsedInfotext = {
positive: string,
negative: string,
steps: number,
cfgScale: number,
width: number,
height: number,
modelHash?: string,
modelName?: string,
batchSize?: number,
batchPos?: number,
sampler: string,
seed: number,
denoise?: number,
extraNetworks: Record<string, ExtraNetworkParams[]>
extraParams: Record<string, any>
}
export type A1111ParsingError = {
error: string
}
const reExtraNetworks = /<(\w+):([^>]+)>/g;
const reParam = /\s*([\w ]+):\s*("(?:\\"[^,]|\\"|\\|[^\"])+"|[^,]*)(?:,|$)/g;
function parseExtraNetworks(prompt: string): [string, Record<string, ExtraNetworkParams[]>] {
const res: Record<string, ExtraNetworkParams[]> = {};
function found(_match: string, modelType: string, args: string): string {
if (!res[modelType]) {
res[modelType] = [];
}
res[modelType].push({ items: args.split(":") });
return "";
}
prompt = prompt.replace(reExtraNetworks, found);
return [prompt, res];
}
type A1111ParamHandler = string | ((prompt: A1111ParsedInfotext, value: string) => void);
const wrapFloat = (name: string): ((p: A1111ParsedInfotext, v: string) => void) => {
return (p, v) => {
p[name] = parseFloat(v);
}
}
const wrapInt = (name: string): A1111ParamHandler => {
return (p, v) => {
p[name] = parseInt(v);
}
}
const handlers: Record<string, A1111ParamHandler> = {
steps: wrapInt("steps"),
"cfg scale": wrapFloat("cfgScale"),
"size": (p, v) => {
const [widthStr, heightStr] = v.split("x")
p.width = parseInt(widthStr);
p.height = parseInt(heightStr);
},
"model hash": "modelHash",
model: "modelName",
"batch size": wrapInt("batchSize"),
"batch pos": wrapInt("batchPos"),
sampler: "sampler",
seed: wrapInt("seed"),
"denoising strength": wrapFloat("denoise")
}
/*
* Parses AUTOMATIC1111/stable-diffusion-webui format infotext into their raw parameters.
*
* Format is as follows:
* - Prompt text immediately starts at the start of the file, ending
* on the first line starting with "Negative prompt:" or "Steps:"
* - "Negative prompt:" is optional and might be omitted
* - Following "Steps:" are various sort-of-comma-separated values.
* Random characters can completely break parsing. Here be dragons.
*/
export default function parseA1111(infotext: string): A1111ParsedInfotext | A1111ParsingError {
let doneWithPrompt = false;
let positive_ = ""
let negative = ""
const lines = infotext.trim().split("\n")
let lastLineIdx = lines.findIndex(l => l.trim().indexOf("Steps: ") !== -1)
if (lastLineIdx === -1) {
return { error: "Steps: line not found" }
}
for (let index = 0; index < lastLineIdx; index++) {
let line = lines[index].trim()
if (line.startsWith("Negative prompt:")) {
doneWithPrompt = true;
line = line.substring(16).trim();
}
if (doneWithPrompt) {
const addNewLine = negative != ""
negative += (addNewLine ? "\n" : "") + line
}
else {
const addNewLine = positive_ != ""
positive_ += (addNewLine ? "\n" : "") + line
}
}
// webui doesn't apply extra networks in the negative prompt
let [positive, extraNetworks] = parseExtraNetworks(positive_)
const extraParams: Record<string, string> = {}
let result: A1111ParsedInfotext = {
positive,
negative,
// defaults taken from webui
width: 512,
height: 512,
steps: 20,
cfgScale: 7.0,
seed: -1,
sampler: "Euler",
extraNetworks,
extraParams
}
for (let index = lastLineIdx; index < lines.length; index++) {
const line = lines[index];
for (let [_, key, value] of line.matchAll(reParam)) {
key = key.toLowerCase()
if (value[0] === '"' && value[value.length - 1] === '""')
value = value.substring(1, value.length - 1)
const handler = handlers[key]
if (handler != null) {
if (value != null) {
if (typeof handler === "function") {
handler(result, value)
}
else {
(result as any)[handler] = value
}
}
}
else {
extraParams[key] = value
}
}
}
return result;
}

View File

@@ -39,8 +39,10 @@ export type LayoutAttributes = {
queuePromptButtonName: string, queuePromptButtonName: string,
/* /*
* If true, clicking the "Queue Prompt" button will run the default subgraph. * If true, clicking the "Queue Prompt" button will run the default
* Set this to false if you need special behavior before running any subgraphs. * subgraph. Set this to false if you need special behavior before running
* any subgraphs, and instead use the `onDefaultQueueAction` event of the
* Comfy.QueueEvents node.
*/ */
queuePromptButtonRunWorkflow: boolean, queuePromptButtonRunWorkflow: boolean,
} }
@@ -84,6 +86,9 @@ export type LayoutState = {
*/ */
attrs: LayoutAttributes attrs: LayoutAttributes
/*
* Increment to force Svelte to re-render the props panel
*/
refreshPropsPanel: Writable<number> refreshPropsPanel: Writable<number>
} }
@@ -102,7 +107,7 @@ export type Attributes = {
title: string, title: string,
/* /*
* List of classes to apply to the component. * List of CSS classes to apply to the component.
*/ */
classes: string, classes: string,
@@ -204,22 +209,22 @@ export type AttributesSpec = {
values?: string[], values?: string[],
/* /*
* If `type` is "number", step for the slider * If `type` is "number", step for the slider that edits this attribute
*/ */
step?: number, step?: number,
/* /*
* If `type` is "number", min for the slider * If `type` is "number", min for the slider that edits this attribute
*/ */
min?: number, min?: number,
/* /*
* If `type` is "number", max for the slider * If `type` is "number", max for the slider that edits this attribute
*/ */
max?: number, max?: number,
/* /*
* If `type` is "string", display as a textarea. * If `type` is "string", display as a textarea instead of an input.
*/ */
multiline?: boolean, multiline?: boolean,
@@ -863,8 +868,9 @@ function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
let prevWidget = state.allItemsByNode[node.id] let prevWidget = state.allItemsByNode[node.id]
if (prevWidget == null) { if (prevWidget == null) {
// If a subgraph was cloned, try looking for the original widget node corresponding to the new widget node being added. // If a subgraph was cloned, try looking for the original widget node corresponding to the new widget node being added.
// `node` is the new ComfyWidgetNode instance to copy attrs to. // `node` is the new ComfyWidgetNode instance to copy layout attrs to.
// `options.cloneData` should contain the results of Subgraph.clone(), called "subgraphNewIDMapping". // `options.cloneData` should contain the results of Subgraph.clone(), which is named "subgraphNewIDMapping" in an
// entry of the `forNode` Record.
// `options.cloneData` is attached to the onNodeAdded options if a node is added to a graph after being // `options.cloneData` is attached to the onNodeAdded options if a node is added to a graph after being
// selection-cloned or pasted, as they both call clone() internally. // selection-cloned or pasted, as they both call clone() internally.
const cloneData = options.cloneData.forNode[options.prevNodeID] const cloneData = options.cloneData.forNode[options.prevNodeID]
@@ -879,7 +885,7 @@ function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
if (nodeIDInLayoutState) { if (nodeIDInLayoutState) {
// Gottem. // Gottem.
prevWidget = state.allItemsByNode[nodeIDInLayoutState] prevWidget = state.allItemsByNode[nodeIDInLayoutState]
console.warn("FOUND CLONED SUBGRAPH NODE", node.id, "=>", nodeIDInLayoutState, prevWidget) // console.warn("FOUND CLONED SUBGRAPH NODE", node.id, "=>", nodeIDInLayoutState, prevWidget)
} }
} }
} }

View File

@@ -4,10 +4,6 @@ import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
import notify from "$lib/notify"; import notify from "$lib/notify";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
export type QueueItem = {
name: string
}
export type QueueEntryStatus = "success" | "error" | "interrupted" | "all_cached" | "unknown"; export type QueueEntryStatus = "success" | "error" | "interrupted" | "all_cached" | "unknown";
type QueueStateOps = { type QueueStateOps = {
@@ -23,8 +19,13 @@ type QueueStateOps = {
onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => void onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => void
} }
/*
* Single job that the backend keeps track of.
*/
export type QueueEntry = { export type QueueEntry = {
/* Data preserved on page refresh */ /*** Data preserved on page refresh ***/
/** Priority of the prompt. -1 means to queue at the front. */
number: number, number: number,
queuedAt?: Date, queuedAt?: Date,
finishedAt?: Date, finishedAt?: Date,
@@ -33,23 +34,34 @@ export type QueueEntry = {
extraData: ComfyBoxPromptExtraData, extraData: ComfyBoxPromptExtraData,
goodOutputs: ComfyNodeID[], goodOutputs: ComfyNodeID[],
/* Data not sent by ComfyUI's API, lost on page refresh */ /*** Data not sent by ComfyUI's API, lost on page refresh ***/
/* Prompt outputs, collected while the prompt is still executing */ /* Prompt outputs, collected while the prompt is still executing */
outputs: SerializedPromptOutputs, outputs: SerializedPromptOutputs,
/* Nodes of the workflow that have finished running so far. */
/* Nodes in of the workflow that have finished running so far. */
nodesRan: Set<ComfyNodeID>, nodesRan: Set<ComfyNodeID>,
/* Nodes of the workflow the backend reported as cached. */
cachedNodes: Set<ComfyNodeID> cachedNodes: Set<ComfyNodeID>
} }
/*
* Represents a queue entry that has finished executing (suceeded or failed) and
* has been moved to the history.
*/
export type CompletedQueueEntry = { export type CompletedQueueEntry = {
/** Corresponding entry in the queue, for the prompt/extra data */
entry: QueueEntry, entry: QueueEntry,
/** The result of this prompt, success/failed/cached */
status: QueueEntryStatus, status: QueueEntryStatus,
/** Message to display in the frontend */
message?: string, message?: string,
/** Detailed error/stacktrace, perhaps inspectible with a popup */
error?: string, error?: string,
} }
/*
* Keeps track of queued and completed (history) prompts.
*/
export type QueueState = { export type QueueState = {
queueRunning: Writable<QueueEntry[]>, queueRunning: Writable<QueueEntry[]>,
queuePending: Writable<QueueEntry[]>, queuePending: Writable<QueueEntry[]>,
@@ -57,6 +69,11 @@ export type QueueState = {
queueRemaining: number | "X" | null; queueRemaining: number | "X" | null;
runningNodeID: ComfyNodeID | null; runningNodeID: ComfyNodeID | null;
progress: Progress | null, progress: Progress | null,
/**
* If true, user pressed the "Interrupt" button in the frontend. Disable the
* button and wait until the next prompt starts running to re-enable it
* again
*/
isInterrupting: boolean isInterrupting: boolean
} }
type WritableQueueStateStore = Writable<QueueState> & QueueStateOps; type WritableQueueStateStore = Writable<QueueState> & QueueStateOps;
@@ -159,6 +176,7 @@ function moveToCompleted(index: number, queue: Writable<QueueEntry[]>, status: Q
return qc return qc
}) })
state.isInterrupting = false;
store.set(state) store.set(state)
} }

View File

@@ -11,10 +11,10 @@
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
let node: ComfyComboNode | null = null; let node: ComfyComboNode | null = null;
let nodeValue: Writable<string> | null = null; let nodeValue: Writable<string> = writable("");
let propsChanged: Writable<number> | null = null; let propsChanged: Writable<number> | null = null;
let lightUp: Writable<boolean> = writable(false); let lightUp: Writable<boolean> = writable(false);
let valuesForCombo: Writable<any[]> | null = null; let valuesForCombo: Writable<any[]> = writable([])
let lastConfigured: any = null; let lastConfigured: any = null;
let option: any = null; let option: any = null;
@@ -132,77 +132,69 @@
</script> </script>
<div class="wrapper comfy-combo" class:mobile={isMobile} class:updated={$lightUp}> <div class="wrapper comfy-combo" class:mobile={isMobile} class:updated={$lightUp}>
{#key $valuesForCombo} <label>
{#if node !== null && nodeValue !== null} {#if widget.attrs.title !== ""}
{#if $valuesForCombo == null} <BlockTitle show_label={true}>
<span>Loading...</span> {widget.attrs.title}
{:else} <span class="count-text">({$valuesForCombo.length})</span>
<label> </BlockTitle>
{#if widget.attrs.title !== ""}
<BlockTitle show_label={true}>
{widget.attrs.title}
<span class="count-text">({$valuesForCombo.length})</span>
</BlockTitle>
{/if}
<Select
value={$nodeValue}
bind:justValue={option}
bind:hoverItemIndex
bind:filterText
bind:listOpen
bind:input
items={$valuesForCombo}
disabled={isDisabled(widget)}
clearable={false}
showChevron={true}
listAutoWidth={true}
inputAttributes={{ autocomplete: 'off' }}
on:change
on:focus={onFocus}
on:hoverItem={(e) => handleHover(e.detail)}
on:select={(e) => handleSelect(e.detail.index)}
on:blur
on:filter={onFilter}>
<div class="comfy-select-list" slot="list" let:filteredItems>
{#if filteredItems.length > 0}
{@const itemSize = isMobile ? 50 : 25}
{@const itemsToShow = isMobile ? 10 : 30}
<VirtualList
items={filteredItems}
width="100%"
height={Math.min(filteredItems.length, itemsToShow) * itemSize}
itemCount={filteredItems.length}
{itemSize}
overscanCount={5}
scrollToIndex={hoverItemIndex}>
<div slot="item"
class="comfy-select-item"
class:mobile={isMobile}
let:index={i}
let:style
{style}
class:active={activeIndex === filteredItems[i].index}
class:hover={hoverItemIndex === i}
on:click={() => handleSelect(filteredItems[i].index)}
on:focus={() => handleHover(i)}
on:mouseover={() => handleHover(i)}>
{@const item = filteredItems[i]}
<span class="comfy-select-label">
{item.label}
</span>
</div>
</VirtualList>
{:else}
<div class="comfy-empty-list">
<span>(No items)</span>
</div>
{/if}
</div>
</Select>
</label>
{/if}
{/if} {/if}
{/key} <Select
value={$valuesForCombo.length === 0 ? "(Loading...)" : $nodeValue}
bind:justValue={option}
bind:hoverItemIndex
bind:filterText
bind:listOpen
bind:input
items={$valuesForCombo}
disabled={$valuesForCombo.length === 0 || isDisabled(widget)}
clearable={false}
showChevron={true}
listAutoWidth={true}
inputAttributes={{ autocomplete: 'off' }}
on:change
on:focus={onFocus}
on:hoverItem={(e) => handleHover(e.detail)}
on:select={(e) => handleSelect(e.detail.index)}
on:blur
on:filter={onFilter}>
<div class="comfy-select-list" slot="list" let:filteredItems>
{#if filteredItems.length > 0}
{@const itemSize = isMobile ? 50 : 25}
{@const itemsToShow = isMobile ? 10 : 30}
<VirtualList
items={filteredItems}
width="100%"
height={Math.min(filteredItems.length, itemsToShow) * itemSize}
itemCount={filteredItems.length}
{itemSize}
overscanCount={5}
scrollToIndex={hoverItemIndex}>
<div slot="item"
class="comfy-select-item"
class:mobile={isMobile}
let:index={i}
let:style
{style}
class:active={activeIndex === filteredItems[i].index}
class:hover={hoverItemIndex === i}
on:click={() => handleSelect(filteredItems[i].index)}
on:focus={() => handleHover(i)}
on:mouseover={() => handleHover(i)}>
{@const item = filteredItems[i]}
<span class="comfy-select-label">
{item.label}
</span>
</div>
</VirtualList>
{:else}
<div class="comfy-empty-list">
<span>(No items)</span>
</div>
{/if}
</div>
</Select>
</label>
</div> </div>
<style lang="scss"> <style lang="scss">
@@ -257,6 +249,15 @@
--item-background-hover: var(--comfy-dropdown-item-background-hover); --item-background-hover: var(--comfy-dropdown-item-background-hover);
--item-color-active: var(--comfy-dropdown-item-color-active); --item-color-active: var(--comfy-dropdown-item-color-active);
--item-background-active: var(--comfy-dropdown-item-background-active); --item-background-active: var(--comfy-dropdown-item-background-active);
--disabled-color: var(--comfy-disabled-textbox-text-color);
--disabled-border-color: var(--comfy-disabled-textbox-border-color);
--disabled-background: var(--comfy-disabled-textbox-background-fill);
}
:global(.svelte-select.disabled) {
--input-color: var(--comfy-disabled-textbox-text-color) !important;
--selected-item-color: var(--comfy-disabled-textbox-text-color) !important;
} }
:global(.svelte-select-list) { :global(.svelte-select-list) {

View File

@@ -0,0 +1,308 @@
import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt";
import { expect } from 'vitest';
import UnitTest from "./UnitTest";
import type { A1111ParsedInfotext } from "$lib/parseA1111";
import ComfyBoxStdPrompt from "$lib/ComfyBoxStdPrompt";
export default class convertA1111ToStdPromptTests extends UnitTest {
test__convertsBasic() {
const infotext: A1111ParsedInfotext = {
positive: "highest quality, masterpiece, best quality, masterpiece, asuka langley sitting cross legged on a chair",
negative: "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts,signature, watermark, username, blurry, artist name",
height: 512,
width: 512,
modelHash: "925997e9",
cfgScale: 12,
sampler: "Euler",
seed: 2870305590,
steps: 28,
extraNetworks: {},
extraParams: {
"clip skip": "2",
"aesthetic embedding": "Belle",
"aesthetic lr": "0.0005",
"aesthetic slerp": "False",
"aesthetic slerp angle": "0.1",
"aesthetic steps": "15",
"aesthetic text": "",
"aesthetic text negative": "False",
"aesthetic weight": "0.9",
},
}
const converted = convertA1111ToStdPrompt(infotext);
expect(converted).toEqual({
version: 1,
metadata: {
created_with: "stable-diffusion-webui",
extra_data: {}
},
parameters: {
checkpoint: [{
model_hashes: {
a1111_shorthash: "925997e9",
}
}],
conditioning: [{
"$meta": {
types: ["positive"]
},
text: "highest quality, masterpiece, best quality, masterpiece, asuka langley sitting cross legged on a chair",
}, {
"$meta": {
types: ["negative"]
},
text: "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts,signature, watermark, username, blurry, artist name"
}],
clip: [{
clip_skip: 2,
}],
k_sampler: [{
cfg_scale: 12,
denoise: 1,
sampler_name: "euler",
scheduler: "normal",
seed: 2870305590,
steps: 28
}],
latent_image: [{
width: 512,
height: 512,
}],
aesthetic_embedding: [{
lr: 0.0005,
model_name: "Belle",
text: "",
text_negative: false,
slerp: false,
slerp_angle: 0.1,
steps: 15,
weight: 0.9
}]
}
})
expect(() => ComfyBoxStdPrompt.parse(converted)).not.toThrow();
}
test__convertsExtraNetworks() {
const infotext: A1111ParsedInfotext = {
positive: "dreamlike fantasy landscape where everything is a shade of pink,\n dog ",
negative: "(worst quality:1.4), (low quality:1.4) , (monochrome:1.1)",
width: 640,
height: 512,
modelHash: "0f0eaaa61e",
modelName: "pastelmix-better-vae-fp16",
cfgScale: 12,
sampler: "DPM++ 2M Karras",
seed: 2416682767,
steps: 40,
denoise: 0.55,
extraNetworks: {
hypernet: [
{ items: ["zxcfc", "0.5", "baz", "quux"], },
],
lora: [
{ items: ["asdfg", "0.8", "foo", "bar"] },
],
},
extraParams: {
"clip skip": "2",
"ensd": "31337",
"hires steps": "20",
"hires upscale": "2",
"hires upscaler": "Latent",
},
}
const converted = convertA1111ToStdPrompt(infotext);
expect(converted).toEqual({
version: 1,
metadata: {
created_with: "stable-diffusion-webui",
extra_data: {
a1111: {
params: {
"ensd": "31337"
}
}
}
},
parameters: {
checkpoint: [{
model_name: "pastelmix-better-vae-fp16",
model_hashes: {
a1111_shorthash: "0f0eaaa61e",
}
}],
conditioning: [{
"$meta": {
types: ["positive"]
},
text: "dreamlike fantasy landscape where everything is a shade of pink,\n dog ",
}, {
"$meta": {
types: ["negative"]
},
text: "(worst quality:1.4), (low quality:1.4) , (monochrome:1.1)"
}],
clip: [{
clip_skip: 2,
}],
hypernetwork: [{
model_name: "zxcfc",
strength: 0.5,
}],
lora: [{
model_name: "asdfg",
module_name: "lora",
strength_unet: 0.8,
strength_tenc: 0.8,
}],
k_sampler: [{
cfg_scale: 12,
denoise: 1,
sampler_name: "dpmpp_2m",
scheduler: "karras",
seed: 2416682767,
steps: 40
}, {
"$meta": {
types: ["upscale"]
},
cfg_scale: 12,
denoise: 0.55,
sampler_name: "dpmpp_2m",
scheduler: "karras",
seed: 2416682767,
steps: 20
}],
latent_image: [{
width: 640,
height: 512,
}],
latent_upscale: [{
width: 1280,
height: 1024,
upscale_by: 2,
upscale_method: "Latent"
}]
}
})
expect(() => ComfyBoxStdPrompt.parse(converted)).not.toThrow();
}
test__convertsAdditionalNetworks() {
const infotext: A1111ParsedInfotext = {
positive: "1girl, pink hair",
negative: "(worst quality, low quality:1.4)",
width: 512,
height: 768,
modelHash: "0873291ac5",
modelName: "AbyssOrangeMix2_nsfw",
cfgScale: 6,
sampler: "DPM++ SDE Karras",
seed: 780207036,
steps: 20,
denoise: 0.2,
extraNetworks: {},
extraParams: {
"addnet enabled": "True",
"addnet model 1": "ElysiaV3-000002(6d3eb064dcc1)",
"addnet model 2": "elfmorie2(a34cd9a8c3cc)",
"addnet module 1": "LoRA",
"addnet module 2": "LoRA",
"addnet weight a 1": "0.9",
"addnet weight a 2": "1",
"addnet weight b 1": "0.7",
"addnet weight b 2": "0.8",
"ensd": "31337",
"mask blur": "1",
"sd upscale overlap": "64",
"sd upscale upscaler": "4x_Valar_v1",
// XXX: just make sure it doesn't fall over for now
// this prompt format I swear...
"template": "1girl",
"negative template": "(worst quality",
}
}
const converted = convertA1111ToStdPrompt(infotext)
expect(converted).toEqual({
version: 1,
metadata: {
created_with: "stable-diffusion-webui",
extra_data: {
a1111: {
params: {
"ensd": "31337",
// TODO
"template": "1girl",
"negative template": "(worst quality",
}
}
}
},
parameters: {
checkpoint: [{
model_name: "AbyssOrangeMix2_nsfw",
model_hashes: {
a1111_shorthash: "0873291ac5",
}
}],
conditioning: [{
"$meta": {
types: ["positive"]
},
text: "1girl, pink hair",
}, {
"$meta": {
types: ["negative"]
},
text: "(worst quality, low quality:1.4)",
}],
lora: [{
module_name: "LoRA",
model_name: "ElysiaV3-000002",
model_hashes: {
addnet_shorthash: "6d3eb064dcc1"
},
strength_unet: 0.9,
strength_tenc: 0.7,
},
{
module_name: "LoRA",
model_name: "elfmorie2",
model_hashes: {
addnet_shorthash: "a34cd9a8c3cc"
},
strength_unet: 1,
strength_tenc: 0.8,
}],
k_sampler: [{
cfg_scale: 6,
denoise: 0.2,
sampler_name: "dpmpp_sde",
scheduler: "karras",
seed: 780207036,
steps: 20
}],
latent_image: [{
width: 512,
height: 768,
mask_blur: 1
}],
sd_upscale: [{
upscaler: "4x_Valar_v1",
overlap: 64
}]
}
})
expect(() => ComfyBoxStdPrompt.parse(converted)).not.toThrow();
}
}

View File

@@ -0,0 +1,158 @@
import parseA1111 from "$lib/parseA1111";
import { expect } from 'vitest';
import UnitTest from "./UnitTest";
export default class parseA1111Tests extends UnitTest {
test__parsesBasic() {
const infotext = `
highest quality, masterpiece, best quality, masterpiece, asuka langley sitting cross legged on a chair
Negative prompt: lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts,signature, watermark, username, blurry, artist name
Size: 512x512, Seed: 2870305590, Steps: 28, Sampler: Euler, CFG scale: 12, Clip skip: 2, Model hash: 925997e9, Aesthetic LR: 0.0005, Aesthetic text: , Aesthetic slerp: False, Aesthetic steps: 15, Aesthetic weight: 0.9, Aesthetic embedding: Belle, Aesthetic slerp angle: 0.1, Aesthetic text negative: False
`
const parsed = parseA1111(infotext);
expect(parsed).toEqual({
positive: "highest quality, masterpiece, best quality, masterpiece, asuka langley sitting cross legged on a chair",
negative: "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts,signature, watermark, username, blurry, artist name",
height: 512,
width: 512,
modelHash: "925997e9",
cfgScale: 12,
sampler: "Euler",
seed: 2870305590,
steps: 28,
extraNetworks: {},
extraParams: {
"clip skip": "2",
"aesthetic embedding": "Belle",
"aesthetic lr": "0.0005",
"aesthetic slerp": "False",
"aesthetic slerp angle": "0.1",
"aesthetic steps": "15",
"aesthetic text": "",
"aesthetic text negative": "False",
"aesthetic weight": "0.9",
},
})
}
test__parsesExtraNetworks() {
const infotext = `
dreamlike fantasy landscape where everything is a shade of pink,
<lora:asdfg:1:foo:bar> dog <hypernet:0.5:baz:quux>
Negative prompt: (worst quality:1.4), (low quality:1.4) , (monochrome:1.1)
Steps: 40, Sampler: DPM++ 2M Karras, CFG scale: 12, Seed: 2416682767, Size: 640x512, Model hash: 0f0eaaa61e, Model: pastelmix-better-vae-fp16, Denoising strength: 0.55, Clip skip: 2, ENSD: 31337, Hires upscale: 2, Hires steps: 20, Hires upscaler: Latent
`
const parsed = parseA1111(infotext);
expect(parsed).toEqual({
positive: "dreamlike fantasy landscape where everything is a shade of pink,\n dog ",
negative: "(worst quality:1.4), (low quality:1.4) , (monochrome:1.1)",
width: 640,
height: 512,
modelHash: "0f0eaaa61e",
modelName: "pastelmix-better-vae-fp16",
cfgScale: 12,
sampler: "DPM++ 2M Karras",
seed: 2416682767,
steps: 40,
denoise: 0.55,
extraNetworks: {
hypernet: [
{ items: ["0.5", "baz", "quux"], },
],
lora: [
{ items: ["asdfg", "1", "foo", "bar"] },
],
},
extraParams: {
"clip skip": "2",
"ensd": "31337",
"hires steps": "20",
"hires upscale": "2",
"hires upscaler": "Latent",
},
})
}
test__parsesXYZGrid() {
const infotext = `
1girl
Negative prompt: (worst quality, low quality:1.4)
Steps: 20, Sampler: DPM++ SDE Karras, CFG scale: 5, Seed: 1964718363, Size: 512x512, Model hash: 736a6f43c2, Denoising strength: 0.5, Clip skip: 2, Hires upscale: 1.75, Hires steps: 14, Hires upscaler: Latent (nearest-exact), Script: X/Y/Z plot, X Type: Prompt S/R, X Values: "<lora:cru5rb:0.5> , <lora:cru5rb:0.6>,<lora:cru5rb:0.7>, <lora:cru5rb:0.8> ,<lora:cru5rb:0.9> , <lora:cru5rb:1>,"
`
const parsed = parseA1111(infotext);
expect(parsed).toEqual({
positive: "1girl",
negative: "(worst quality, low quality:1.4)",
width: 512,
height: 512,
modelHash: "736a6f43c2",
cfgScale: 5,
sampler: "DPM++ SDE Karras",
seed: 1964718363,
steps: 20,
denoise: 0.5,
extraNetworks: {},
extraParams: {
"clip skip": "2",
"hires steps": "14",
"hires upscale": "1.75",
"hires upscaler": "Latent (nearest-exact)",
"script": "X/Y/Z plot",
"x type": "Prompt S/R",
"x values": '"<lora:cru5rb:0.5> , <lora:cru5rb:0.6>,<lora:cru5rb:0.7>, <lora:cru5rb:0.8> ,<lora:cru5rb:0.9> , <lora:cru5rb:1>,"',
},
})
}
test__parsesDynamicPromptsTemplates() {
const infotext = `
1girl, pink hair
Negative prompt: (worst quality, low quality:1.4)
Steps: 20, Sampler: DPM++ SDE Karras, CFG scale: 6, Seed: 780207036, Size: 512x768, Model hash: 0873291ac5, Model: AbyssOrangeMix2_nsfw, Denoising strength: 0.2, ENSD: 31337, Mask blur: 1, SD upscale overlap: 64, SD upscale upscaler: 4x_Valar_v1, AddNet Enabled: True, AddNet Module 1: LoRA, AddNet Model 1: ElysiaV3-000002(6d3eb064dcc1), AddNet Weight A 1: 0.9, AddNet Weight B 1: 0.9, AddNet Module 2: LoRA, AddNet Model 2: elfmorie2(a34cd9a8c3cc), AddNet Weight A 2: 1, AddNet Weight B 2: 1
Template: 1girl, __haircolor__
Negative Template: (worst quality, low quality:1.4), __badprompt__
`
const parsed = parseA1111(infotext);
expect(parsed).toEqual({
positive: "1girl, pink hair",
negative: "(worst quality, low quality:1.4)",
width: 512,
height: 768,
modelHash: "0873291ac5",
modelName: "AbyssOrangeMix2_nsfw",
cfgScale: 6,
sampler: "DPM++ SDE Karras",
seed: 780207036,
steps: 20,
denoise: 0.2,
extraNetworks: {},
extraParams: {
"addnet enabled": "True",
"addnet model 1": "ElysiaV3-000002(6d3eb064dcc1)",
"addnet model 2": "elfmorie2(a34cd9a8c3cc)",
"addnet module 1": "LoRA",
"addnet module 2": "LoRA",
"addnet weight a 1": "0.9",
"addnet weight a 2": "1",
"addnet weight b 1": "0.9",
"addnet weight b 2": "1",
"ensd": "31337",
"low quality": "1.4)",
"mask blur": "1",
"sd upscale overlap": "64",
"sd upscale upscaler": "4x_Valar_v1",
// XXX: just make sure it doesn't fall over for now
// this prompt format I swear...
"template": "1girl",
"negative template": "(worst quality",
},
})
}
}

View File

@@ -1,2 +1,4 @@
export { default as ComfyPromptSerializerTests } from "./ComfyPromptSerializerTests" export { default as ComfyPromptSerializerTests } from "./ComfyPromptSerializerTests"
export { default as ComfyGraphTests } from "./ComfyGraphTests" export { default as ComfyGraphTests } from "./ComfyGraphTests"
export { default as parseA1111Tests } from "./parseA1111Tests"
export { default as convertA1111ToStdPromptTests } from "./convertA1111ToStdPromptTests"

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare const __GIT_COMMIT_HASH__: string

View File

@@ -7,6 +7,7 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"strict": false,
"baseUrl": "./src", "baseUrl": "./src",
"paths": { "paths": {
"$lib": ["lib"], "$lib": ["lib"],

View File

@@ -11,6 +11,9 @@ const isProduction = process.env.NODE_ENV === "production";
console.log("Production build: " + isProduction) console.log("Production build: " + isProduction)
export default defineConfig({ export default defineConfig({
define: {
"__GIT_COMMIT_HASH__": '"test"'
},
clearScreen: false, clearScreen: false,
base: "./", base: "./",
plugins: [ plugins: [