Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78e0d6f096 |
+1
-4
@@ -2,7 +2,4 @@ __pycache__
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
*.onnx
|
*.onnx
|
||||||
wheels/
|
wheels/
|
||||||
node_modules/
|
node_modules/
|
||||||
compose.yaml
|
|
||||||
comfy_mtb.wsb
|
|
||||||
Dockerfile
|
|
||||||
-402
@@ -1,402 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
This is an automated changelog based on the commits in this repository.
|
|
||||||
|
|
||||||
Check the notes in the [releases](https://github.com/melMass/comfy_mtb/releases) for more information.
|
|
||||||
## [main] - 2024-03-07
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- 🐛 font fallback ([9fccdee](https://github.com/melMass/comfy_mtb/commit/9fccdee82d721e88c64d2292c209fec869524dd2))
|
|
||||||
- ✨ optional inputs of colored image ([cd32f26](https://github.com/melMass/comfy_mtb/commit/cd32f26b167088d6b489e43b260c187ea5e4d223)) by [@ScottNealon](https://github.com/ScottNealon) in [#147](https://github.com/melMass/comfy_mtb/pull/147)
|
|
||||||
- 📝 adds a way to not load the imagefeed ([501c330](https://github.com/melMass/comfy_mtb/commit/501c3301056b2851555cccd75ab3ff15b1ab8e0c))
|
|
||||||
- 🐛 colored image mask input ([30c4311](https://github.com/melMass/comfy_mtb/commit/30c4311b69f6481a34f968cb67a9b5ce5d2e9fda))
|
|
||||||
- 🐛 handle font cache errors ([c43a661](https://github.com/melMass/comfy_mtb/commit/c43a661ba31dcd7720b4f32d8e96760e6191fbd9))
|
|
||||||
- 💄 register the COLOR type even for external extensions ([12b134a](https://github.com/melMass/comfy_mtb/commit/12b134ab4c937c192aaf4a3667d9885dd4fe43ca))
|
|
||||||
- ✨ mask crop output ([59a361a](https://github.com/melMass/comfy_mtb/commit/59a361af5870b8ffc984c6680dd3282d3553dcf9))
|
|
||||||
- 🚑️ thread font loading ([e4da832](https://github.com/melMass/comfy_mtb/commit/e4da832b99bd640b72c31b67178a3168e3238fa0))
|
|
||||||
- 📦 changed way of creating bbox from mask ([14ee9e2](https://github.com/melMass/comfy_mtb/commit/14ee9e23c009ab55fa3b2fc6ec60fb683c46d57d)) by [@Yurchikian](https://github.com/Yurchikian) in [#124](https://github.com/melMass/comfy_mtb/pull/124)
|
|
||||||
- ✨ expose invert of bboxfrommask ([53cb503](https://github.com/melMass/comfy_mtb/commit/53cb503866da6d83b47eaeb8073039ace2ae0a95))
|
|
||||||
- ✨ less strict csv parsing ([d5c4c5f](https://github.com/melMass/comfy_mtb/commit/d5c4c5f2649ecdb4bf7b517c5b33bbf8df753047))
|
|
||||||
- 🐛 fit number regression ([c8658df](https://github.com/melMass/comfy_mtb/commit/c8658dfbdd3a0ca8c3e88cd1adfddc55c7444045))
|
|
||||||
- 🐛 remove uneeded installs ([4e07450](https://github.com/melMass/comfy_mtb/commit/4e07450bcabb0105b5610e52f7d4692ea07f9c1d))
|
|
||||||
- 🐛 import issue ([255ac03](https://github.com/melMass/comfy_mtb/commit/255ac036bab1d776301857843d0e7a85e9a9dcb8))
|
|
||||||
- 🐛 wrong output for bbox ([8d12b59](https://github.com/melMass/comfy_mtb/commit/8d12b59844958fbc696d01d51162f97262664ae9))
|
|
||||||
- 🚑️ fallback when symlink detection fails ([278f22c](https://github.com/melMass/comfy_mtb/commit/278f22c2093b6eca63d2d00f7936774918707e4e))
|
|
||||||
- ✨ handle malformed styles.csv ([e6f6502](https://github.com/melMass/comfy_mtb/commit/e6f65026735770df8aced4a3acb75550ff1c84da))
|
|
||||||
- 🐛 encoding ([5af2840](https://github.com/melMass/comfy_mtb/commit/5af284067c65042bcdfff04a5d5a2360bf9e4af7))
|
|
||||||
- ⚡️ add the cli deps ([bb90e04](https://github.com/melMass/comfy_mtb/commit/bb90e0415f6a1ececbf468815dc0f5959d9a34e8))
|
|
||||||
- 🚑️ check for symlink ([25b933c](https://github.com/melMass/comfy_mtb/commit/25b933c698b250a411549d2600fae49bec225b7a))
|
|
||||||
- 🚑️ remove problematic dependencies ([5dfea51](https://github.com/melMass/comfy_mtb/commit/5dfea51dd8db2a4829e559eadeda22374b51c8a4))
|
|
||||||
- 🐛 batch support ([f1ff9fc](https://github.com/melMass/comfy_mtb/commit/f1ff9fc7c4684ad673c3178df3b8142dcf0b16ac))
|
|
||||||
- 🐛 automatically disable tiling if seamless is on ([4605f74](https://github.com/melMass/comfy_mtb/commit/4605f74f370d4d221ab1d50f21b72910fa6909c7))
|
|
||||||
- 🐛 debug node ([dc500b7](https://github.com/melMass/comfy_mtb/commit/dc500b788e885205f017956da6a71a677f822941))
|
|
||||||
- ⚡️ hack to handle prompt validation ([d49b257](https://github.com/melMass/comfy_mtb/commit/d49b2578c247dcba9b09b374d99f5cc45cac172d))
|
|
||||||
- ✨ deepbump update ([87b245c](https://github.com/melMass/comfy_mtb/commit/87b245c6a6895490e3612b235879fa90b62dea2b))
|
|
||||||
- 👷 user folder_paths to retrieve comfy root ([38df58a](https://github.com/melMass/comfy_mtb/commit/38df58a78c363ef2657011893d4d811676b1c664))
|
|
||||||
- 🐛 typo ([90aee83](https://github.com/melMass/comfy_mtb/commit/90aee83797a863cf4797cdbe187f949061cbd176))
|
|
||||||
- 🐛 do not resolve symlink for "here" ([a50b11b](https://github.com/melMass/comfy_mtb/commit/a50b11bdaa66f4e805811b1676c937ade11318c2))
|
|
||||||
- ✏️ use Union to allow support for <3.10 ([88a2779](https://github.com/melMass/comfy_mtb/commit/88a277968745ac990406b14d300a8ada9c575b11)) by [@M1kep](https://github.com/M1kep) in [#91](https://github.com/melMass/comfy_mtb/pull/91)
|
|
||||||
- ⚡️ simplify widgets cleanup ([cdd098e](https://github.com/melMass/comfy_mtb/commit/cdd098e10258401402b8023c9143532cfa4a1745))
|
|
||||||
- ✨ don't assume the install was ran ([cc43654](https://github.com/melMass/comfy_mtb/commit/cc43654af2987bc8860557caa99cde91e8309b21))
|
|
||||||
- 🐛 install ([616b2bf](https://github.com/melMass/comfy_mtb/commit/616b2bfc6c629cef1d30cb0d717bd805c3a086aa))
|
|
||||||
- 🐛 properly escape paths ([22cac9b](https://github.com/melMass/comfy_mtb/commit/22cac9b2d95910197941b73e7548735470bd3b17))
|
|
||||||
- 🐛 use relative paths in JS ([e2773ff](https://github.com/melMass/comfy_mtb/commit/e2773ff22e43e7756ad618344a03d661a576cf35))
|
|
||||||
- 💄 BatchFromHistory when "listening" ([3b07984](https://github.com/melMass/comfy_mtb/commit/3b07984716402fbbf5da41020bf73befd52e7ebf))
|
|
||||||
- ✨ save gif widget removal ([fe8f519](https://github.com/melMass/comfy_mtb/commit/fe8f519f8860b0610d8cafcd9b843b4171c2b3d4))
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- 📝 add changelog ([0d817bf](https://github.com/melMass/comfy_mtb/commit/0d817bf326b4a22e2221264a414af50c3b7048b9))
|
|
||||||
- 📄 add note+ screenshot ([90d9636](https://github.com/melMass/comfy_mtb/commit/90d96366c8b7637b55d1b4f88cb9aca217c1414b))
|
|
||||||
- 📝 add cover image ([6b993b8](https://github.com/melMass/comfy_mtb/commit/6b993b84071bbb80ba1b8bd63576f31e35d05590))
|
|
||||||
- 📝 fix image size ([3e8c2fe](https://github.com/melMass/comfy_mtb/commit/3e8c2fe789925e7017c2f8c8d9164c139588aba4))
|
|
||||||
- 📝 add image ([3e93ea6](https://github.com/melMass/comfy_mtb/commit/3e93ea6f2c73353891b1a3f6223b5730bc69df37))
|
|
||||||
- 📝 explain optional nodes ([cea0b08](https://github.com/melMass/comfy_mtb/commit/cea0b08eb044756ab1b408f630435095b8969d36))
|
|
||||||
- 📝 add the example previews from the wiki ([8f90986](https://github.com/melMass/comfy_mtb/commit/8f909864bfaa9f2d0fbdcf3942eacb9d78ee8fb8))
|
|
||||||
- 📝 update node list ([4917e31](https://github.com/melMass/comfy_mtb/commit/4917e31c427c74d28c830fd7b2423cab393ba0f8))
|
|
||||||
- 📝 add some deprecation warnings and recommendations ([e11df9d](https://github.com/melMass/comfy_mtb/commit/e11df9d45c81d93f4334841de036b4aa3364375a))
|
|
||||||
- 📝 add a reference to SlickComfy for colab ([bb35098](https://github.com/melMass/comfy_mtb/commit/bb35098c656b0b2d30909b83df0a3b65c5975f78))
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- ✨ add "To Device" ([c28181f](https://github.com/melMass/comfy_mtb/commit/c28181f1615d2e183767aa76cc2350934330e546))
|
|
||||||
- ✨ add note+ example ([90f3bc2](https://github.com/melMass/comfy_mtb/commit/90f3bc2d953b299ea34e9e3a925f1a824b488855))
|
|
||||||
- 💄 node+ improvements ([4b29395](https://github.com/melMass/comfy_mtb/commit/4b29395000254382882c0d1be115b2ed80cd7c99))
|
|
||||||
- 📝 add note plus ([605c8db](https://github.com/melMass/comfy_mtb/commit/605c8db320e1531c6347f6888606fa50d8eb268b))
|
|
||||||
- 🚧 add playlist nodes ([cf96572](https://github.com/melMass/comfy_mtb/commit/cf965727e8e7064328704d88cd0410c61f1e686e))
|
|
||||||
- 🚨 add missing node ([16c1a59](https://github.com/melMass/comfy_mtb/commit/16c1a59312b1d9841f5f8a814eff93a1ddf04edb))
|
|
||||||
- ✨ Math Expression node ([142624e](https://github.com/melMass/comfy_mtb/commit/142624eea616a5622387b1b641c02605455ee6f1))
|
|
||||||
- 🚀 add optional inputs to colored image ([049983d](https://github.com/melMass/comfy_mtb/commit/049983dbe2dbce6b772908468c4042d2bfde5eb2))
|
|
||||||
- ✨ Add support for extra_model_paths.yaml ([d7b8ac8](https://github.com/melMass/comfy_mtb/commit/d7b8ac8e0c98b0d7a2e21889d35aad9f6b093560))
|
|
||||||
- ✨ add batch shake ([af94203](https://github.com/melMass/comfy_mtb/commit/af94203d1b461d934ca1c44211ca0f71a5d05d48))
|
|
||||||
- ✨ enhance concat images ([a798eb0](https://github.com/melMass/comfy_mtb/commit/a798eb07d0d891cfbd47013b442ef2fa3d7cc5bc))
|
|
||||||
- 💄 add a few more batch nodes ([c1d42de](https://github.com/melMass/comfy_mtb/commit/c1d42de0fcde86d2a167fb4b5e781ee987814da2))
|
|
||||||
- ✨ Batch node utilities ([cef5023](https://github.com/melMass/comfy_mtb/commit/cef5023efc17366a2e937ef43944de3587707fac))
|
|
||||||
- 🚨 Image Stack node (horizontal and vertical stack) ([bb3277d](https://github.com/melMass/comfy_mtb/commit/bb3277d85f4ca21735cb1f5237cb1430db88c183))
|
|
||||||
- 🚀 add seamless model hack ([21acc87](https://github.com/melMass/comfy_mtb/commit/21acc87ff0a84b7588f4b5aae0aeb5ae94bbbfbe))
|
|
||||||
- 🔧 debug handle a few more types ([638498c](https://github.com/melMass/comfy_mtb/commit/638498c6b47c2b2cab82f76aec1f3d46df67f263))
|
|
||||||
- 🎨 Add an editor for the styles loader ([2faa2f2](https://github.com/melMass/comfy_mtb/commit/2faa2f2a148a4dbf5525e4945f688a239f244546))
|
|
||||||
- ✨ add a static assets path ([6a00d1d](https://github.com/melMass/comfy_mtb/commit/6a00d1da5a8a5fa47af1bf1ab5d3cd206c599841))
|
|
||||||
- ✨ add Interpolate Clip Sequential ([a71c273](https://github.com/melMass/comfy_mtb/commit/a71c273baf450ad7e2a7e032451f015d3be3e9e9))
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- 🧹 applied some linting ([fe49312](https://github.com/melMass/comfy_mtb/commit/fe49312cbef03c6540304448fa88aa7a88391efa))
|
|
||||||
- 📝 header links not parsed ([514c0d2](https://github.com/melMass/comfy_mtb/commit/514c0d2eda9990435eb18258d4bbd1aa137feb3d))
|
|
||||||
- 📝 hardcode links in changelog ([915b744](https://github.com/melMass/comfy_mtb/commit/915b7444a9db83f349d83b636304af0d276f529f))
|
|
||||||
- 🔖 local updates ([6c5e5d3](https://github.com/melMass/comfy_mtb/commit/6c5e5d36379bdab223b4503e42b7956b55a82ab0))
|
|
||||||
- 📝 update node list ([dd27f99](https://github.com/melMass/comfy_mtb/commit/dd27f990c72fa94aff205eb314a8ea360f57479e))
|
|
||||||
- ✨ update node_list ([537a0d8](https://github.com/melMass/comfy_mtb/commit/537a0d8108d0caa3ab2daeafd1d25d680214ef26))
|
|
||||||
- ✨ local stuff ([9afad1a](https://github.com/melMass/comfy_mtb/commit/9afad1a1680073006d946be10f8c97b75ddfe253))
|
|
||||||
- 📝 fix update issue template ([da290db](https://github.com/melMass/comfy_mtb/commit/da290dbcf2952a56be9334f7bf9dc4d8fa64a21d))
|
|
||||||
- 📝 update issue template ([b949bb4](https://github.com/melMass/comfy_mtb/commit/b949bb406bc1929634600465ea389eaedefe6e6f))
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- ⚡️ small local fixes ([bcac665](https://github.com/melMass/comfy_mtb/commit/bcac66508d2e788cc437da289d1ccede19465b8c))
|
|
||||||
- 🗑️ remove unused code in install script ([5b75436](https://github.com/melMass/comfy_mtb/commit/5b75436610c6312adf47c6baa3e9fe9cc7d56dcf))
|
|
||||||
|
|
||||||
### Merge
|
|
||||||
|
|
||||||
- 🔀 pull request #109 from melMass/dev/0.2.0 ([87e301d](https://github.com/melMass/comfy_mtb/commit/87e301d120a542d5aabe544bec10d38dbd19b2f6)) in [#109](https://github.com/melMass/comfy_mtb/pull/109)
|
|
||||||
- 🔀 pull request #86 from melMass/feature/styles-editor ([cbdb816](https://github.com/melMass/comfy_mtb/commit/cbdb816164900061ddaa1671f4287763d0b79ee1)) in [#86](https://github.com/melMass/comfy_mtb/pull/86)
|
|
||||||
|
|
||||||
### Wip
|
|
||||||
|
|
||||||
- 🚧 add text template node ([af2175a](https://github.com/melMass/comfy_mtb/commit/af2175a1fc0c2fb29ef3493f242fe45ec6fcabac))
|
|
||||||
|
|
||||||
## New Contributors
|
|
||||||
* [@ScottNealon](https://github.com/ScottNealon) made their first contribution in [#147](https://github.com/melMass/comfy_mtb/pull/147)
|
|
||||||
* [@Yurchikian](https://github.com/Yurchikian) made their first contribution in [#124](https://github.com/melMass/comfy_mtb/pull/124)
|
|
||||||
* [@M1kep](https://github.com/M1kep) made their first contribution in [#91](https://github.com/melMass/comfy_mtb/pull/91)
|
|
||||||
## [0.1.4] - 2023-08-12
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- 🚀 pending fixes ([ea5d73d](https://github.com/melMass/comfy_mtb/commit/ea5d73d48cfa4046f48a52609cff7f754d8364ed))
|
|
||||||
- 🚑️ image resize infinite loop ([30d6cfe](https://github.com/melMass/comfy_mtb/commit/30d6cfe81292d0f7702544b3c2cbad1820c4a926))
|
|
||||||
- ✨ update example files ([610afe0](https://github.com/melMass/comfy_mtb/commit/610afe031f21d737b2fd5128e4be7100b6666181))
|
|
||||||
- 🐛 simplify install steps ([4fc84d6](https://github.com/melMass/comfy_mtb/commit/4fc84d615dd0f546442c3537f00c52366db4ca9b))
|
|
||||||
- ✨ refactor ([8523392](https://github.com/melMass/comfy_mtb/commit/8523392df74c586dc940841ddbb5069943b16f7d))
|
|
||||||
- 🐛 debug rgba ([40560f8](https://github.com/melMass/comfy_mtb/commit/40560f8154d3ddeabf708be4d111370648d466ac))
|
|
||||||
- 🎨 rename fun to generate ([e7f72f9](https://github.com/melMass/comfy_mtb/commit/e7f72f9825da58254e3084b4ba91f76e6cf2cf5f))
|
|
||||||
- ✨ refactor existing ([1144466](https://github.com/melMass/comfy_mtb/commit/11444662b9198861b62aff06a08b9c9ea01dd8bd))
|
|
||||||
- ⚡️ move getbatchfromhistory to graphutils ([2eccba4](https://github.com/melMass/comfy_mtb/commit/2eccba4e33b21d1d080cb2f415f76a93488120f0))
|
|
||||||
- 🚧 wip dependency installer UI ([630b492](https://github.com/melMass/comfy_mtb/commit/630b492347f75d7308b31a000061b41d7dfa4a10))
|
|
||||||
- 🐛 image feed zorder ([0fb2d4d](https://github.com/melMass/comfy_mtb/commit/0fb2d4da90a7e65f82b3f9c8942a68e360456cf7))
|
|
||||||
- ⬇️ download_antelopev2 ([4dd5321](https://github.com/melMass/comfy_mtb/commit/4dd532185223a1fa5978446e7bb75d32d77ebdb5))
|
|
||||||
- 🚑️ frontend pushed too early ([91f60d4](https://github.com/melMass/comfy_mtb/commit/91f60d4c463c474ac10e868e8e73e13fa019856b))
|
|
||||||
- 🚑️ missing input ([84ac8ac](https://github.com/melMass/comfy_mtb/commit/84ac8ac852aeb962029bfd8369fe5ed59a203977))
|
|
||||||
- 🐛 shell command bug ([3d5075f](https://github.com/melMass/comfy_mtb/commit/3d5075fea2e219a179271c9810017c7e38bff6cc))
|
|
||||||
- 🚑️ remove pipe mode from the install.py ([b854a30](https://github.com/melMass/comfy_mtb/commit/b854a302ce4708d2ad2dac249860308dbdcae5a6))
|
|
||||||
- ⚡️ colab install ([36d8e6b](https://github.com/melMass/comfy_mtb/commit/36d8e6bdb06edab72ccfb686266d2e644a9f028c))
|
|
||||||
- 🚑️ install typo ([ffa1a87](https://github.com/melMass/comfy_mtb/commit/ffa1a87b9184df5a3699a6118714b39d359bde4d))
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- 📝 link the actual action instead of badge ([098d74a](https://github.com/melMass/comfy_mtb/commit/098d74a3cd8449d836569a074995e20d775c6728))
|
|
||||||
- 📝 add action badge ([e74314b](https://github.com/melMass/comfy_mtb/commit/e74314b04eb218c140482ccf704b61af06db3f4d))
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- 💫 export to prores -> export with ffmpeg ([a4d99d9](https://github.com/melMass/comfy_mtb/commit/a4d99d966b1207191243a9749385b998d1a9c6b1))
|
|
||||||
- 🔥 add any to string & refactor ([dbdb872](https://github.com/melMass/comfy_mtb/commit/dbdb872b74e18c16feb44bd037abc3aafbb4700f))
|
|
||||||
- ✨ add UI for interpolate clip sequential ([5ec5511](https://github.com/melMass/comfy_mtb/commit/5ec551143302b2a94ca82e477f684ecee23f1459))
|
|
||||||
- ✨ add portable reqs ([3f14b16](https://github.com/melMass/comfy_mtb/commit/3f14b1676d28f5ffa1f47fda00b9bc244951045c))
|
|
||||||
- ✨ add border extension ([fb64484](https://github.com/melMass/comfy_mtb/commit/fb644847ca434123e8e8e4991d33949fd31e3cbe))
|
|
||||||
- ✨ use PIL for gif saving ([2bc7ae8](https://github.com/melMass/comfy_mtb/commit/2bc7ae88bf4cdfa575d11233c0e6f7b07f9dfd23))
|
|
||||||
- 🎨 update node list ([a54d7d5](https://github.com/melMass/comfy_mtb/commit/a54d7d5346c272898dd4e67c65495de7325ab3a0))
|
|
||||||
- ✨ install fix ([512de60](https://github.com/melMass/comfy_mtb/commit/512de6023e55f2cc47516bf44436efe22157273f)) in [#41](https://github.com/melMass/comfy_mtb/pull/41)
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- 💄 encoding ([49c64c7](https://github.com/melMass/comfy_mtb/commit/49c64c74eb3e99f456b563bbd79e3fe47a85c70d))
|
|
||||||
- 🚀 only fetch controlnet_preprocessor deps ([414beb9](https://github.com/melMass/comfy_mtb/commit/414beb99a1f9bf719eca6ac139c9b2ccdfd6d743))
|
|
||||||
- 🚀 add controlnetpreprocessors to tests ([63b3aec](https://github.com/melMass/comfy_mtb/commit/63b3aece2ba05adc2b655afeb41e3d47e7887b33))
|
|
||||||
- ✨ remove unused input ([d4f791d](https://github.com/melMass/comfy_mtb/commit/d4f791d7a14ba9cb8abd7c95ba70b081fee5fb7c))
|
|
||||||
- ✨ use the same cwd as manager ([2ff0467](https://github.com/melMass/comfy_mtb/commit/2ff04672daff773d52e1552dca1bf616bc32daa6))
|
|
||||||
- 🎨 no brace glob ([bbfcb62](https://github.com/melMass/comfy_mtb/commit/bbfcb62c398de39058bcb6e18161425059d53e8e))
|
|
||||||
- 🎨 extract txt ([a22fd01](https://github.com/melMass/comfy_mtb/commit/a22fd01d664276e4cd833ae1326feeece1d1deaf))
|
|
||||||
- 🎨 also push wheels_order to releases ([8e5b776](https://github.com/melMass/comfy_mtb/commit/8e5b7765cc0c6730bd5517ccfd56e817ea39bd3a))
|
|
||||||
- 🚧 more info for bug reports ([3dadc11](https://github.com/melMass/comfy_mtb/commit/3dadc119f44fca1029ec4b349d71ce99fb20a4b6))
|
|
||||||
- ✨ individual wheels ([346ff64](https://github.com/melMass/comfy_mtb/commit/346ff649d50c9f0286ad2243938406fefb62853b))
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- 🚧 tidy ([4f30829](https://github.com/melMass/comfy_mtb/commit/4f30829e06c41b3685644bfe7bece07e0bcfb70e))
|
|
||||||
- ♻️ get batch from history ([13d255a](https://github.com/melMass/comfy_mtb/commit/13d255a730b08c4903647875350b9b3dcd61b4a6))
|
|
||||||
|
|
||||||
### Revert
|
|
||||||
|
|
||||||
- 💄 use BOOLEAN instead of BOOL ([cfb3b23](https://github.com/melMass/comfy_mtb/commit/cfb3b237cf64b512414a17f71e6d89c3355aa8ef))
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
- 🧪 remove sha input ([c5bbe83](https://github.com/melMass/comfy_mtb/commit/c5bbe83008bb194cbd6ad5e3dc70cb3850b18985))
|
|
||||||
- 🧪 ci for comfy embedded ([7b3afca](https://github.com/melMass/comfy_mtb/commit/7b3afca8179760e35e8a6fbf742080dee13e4fc7))
|
|
||||||
|
|
||||||
### Merge
|
|
||||||
|
|
||||||
- 🔀 pull request #50 from melMass/dev/august-refactor ([2ecd470](https://github.com/melMass/comfy_mtb/commit/2ecd4700d77c0727e6b5d2124e0a6ebd48ec96ed)) in [#50](https://github.com/melMass/comfy_mtb/pull/50)
|
|
||||||
|
|
||||||
## [0.1.3] - 2023-07-29
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- 🔥 manage pip from install only, remove requirements.txt ([247fbfb](https://github.com/melMass/comfy_mtb/commit/247fbfbc216b8259d607e0699d5b990b6a06ca71)) in [#38](https://github.com/melMass/comfy_mtb/pull/38)
|
|
||||||
- 🎨 use image ratio for imagefeed ([f5cd56c](https://github.com/melMass/comfy_mtb/commit/f5cd56ce861c8c0a931744ae6cf2b96e9c8bca06))
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- 📝 update imagefeed preview ([cbcacbe](https://github.com/melMass/comfy_mtb/commit/cbcacbe3c92ebb5f74d046b83504c3723710f130))
|
|
||||||
- 📝 fix typo and add more details ([7c020ba](https://github.com/melMass/comfy_mtb/commit/7c020bab288aa7d17dc937b5f102319d43c3ebb3))
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- ✨ use wheel order if present ([9b24edd](https://github.com/melMass/comfy_mtb/commit/9b24eddd9c51004af08d7ac6ff2b6473dd3ee161))
|
|
||||||
- ✨ store order of install for wheels ([5053142](https://github.com/melMass/comfy_mtb/commit/505314294f02e7c19ac95e4d0ed37fd397a54b46))
|
|
||||||
|
|
||||||
## [0.1.2] - 2023-07-28
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- ✨ various small things ([0e311cf](https://github.com/melMass/comfy_mtb/commit/0e311cf2c64cf2b4861d4cc612a3409390e3039a))
|
|
||||||
- 📝 last release ([889f08c](https://github.com/melMass/comfy_mtb/commit/889f08c08b721be8fdb4e4d7eacc47169d5692d6)) in [#36](https://github.com/melMass/comfy_mtb/pull/36)
|
|
||||||
- 📝 narrow requirements ([5d661b2](https://github.com/melMass/comfy_mtb/commit/5d661b2509fecf3940c3c0fab25b16ec0eae7a2d))
|
|
||||||
- ✨ Separate FaceAnalysis model loading ([d143e83](https://github.com/melMass/comfy_mtb/commit/d143e83dba3bffa16e1b98d7ad1e9cf92dc94db2))
|
|
||||||
- ⚡️ update examples to match wiki ([3dfe98c](https://github.com/melMass/comfy_mtb/commit/3dfe98c7957df48723380de85e1242a424ec23de))
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- 📝 add readme for web extensions features ([be162a2](https://github.com/melMass/comfy_mtb/commit/be162a20477258627fa0d742c97a478bd085ff4f))
|
|
||||||
- 📝 link to the proper lang instructions ([232cf89](https://github.com/melMass/comfy_mtb/commit/232cf8966cc20291b60c68f487dfd37bf6aa4dfa)) in [#33](https://github.com/melMass/comfy_mtb/pull/33)
|
|
||||||
- 📝 update readmes ([96a0618](https://github.com/melMass/comfy_mtb/commit/96a0618c5990a8559a9e2dd17c868d3465b8ca90))
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- 🎉 bump version ([9e751a2](https://github.com/melMass/comfy_mtb/commit/9e751a242f4e9afee3dc5c871c414b29b9706ff6))
|
|
||||||
- 👷 remove stale example ([c237737](https://github.com/melMass/comfy_mtb/commit/c2377374201fc34b107c8b7db1cdeb2f483d1e18))
|
|
||||||
- 🐛 fix size ([c0cc557](https://github.com/melMass/comfy_mtb/commit/c0cc5572d8c727568eca8a3d0f116a1f540c31ff))
|
|
||||||
|
|
||||||
## [0.1.1] - 2023-07-24
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- 🎨 improve a bit the HTML response of endpoints ([50d51c7](https://github.com/melMass/comfy_mtb/commit/50d51c70d04e49e9df524975c171288c0fc0b20f))
|
|
||||||
- 🐛 caching issues ([55c9736](https://github.com/melMass/comfy_mtb/commit/55c9736a9b2ca036926be4b06406121bfb9ebad2))
|
|
||||||
- 🔥 remove notice ([abf1e82](https://github.com/melMass/comfy_mtb/commit/abf1e82adb9fac8cd70d5c409baad55309ef6fe1))
|
|
||||||
- 🔥 use BOOL everywhere ([a393793](https://github.com/melMass/comfy_mtb/commit/a393793cfa93721eac46295723076a1dda940dcd))
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- 📝 added lang links ([bbdac97](https://github.com/melMass/comfy_mtb/commit/bbdac97e49af4e90d22eeec3f63b96ecc126ffcf))
|
|
||||||
- 📝 add comfyforum example ([10d0503](https://github.com/melMass/comfy_mtb/commit/10d05031b1791ab3534cf838be6eb75df638dfb6))
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- 🚧 jupyter seems to require an __init__ there ([9a4eda3](https://github.com/melMass/comfy_mtb/commit/9a4eda3ef573bf382c13515f67ae8a415bf61abd))
|
|
||||||
- ⚡️ use notify ([a2ecc11](https://github.com/melMass/comfy_mtb/commit/a2ecc11ebde79c2403959bf09c258f3a2465894a))
|
|
||||||
- ✨ first version of Notify ([7e9c97e](https://github.com/melMass/comfy_mtb/commit/7e9c97ecb48672b25e5ed17b9b35dba9208ac311))
|
|
||||||
- ⚡️ add an "actions" endpoint ([3de160a](https://github.com/melMass/comfy_mtb/commit/3de160af25b516c02aaa8cc32baec16e9ef358fb))
|
|
||||||
- ✨ add Unsplash Image node ([8d3cc39](https://github.com/melMass/comfy_mtb/commit/8d3cc39b72dff1b5eb61bf7e2e395753c138ec8a))
|
|
||||||
- ✨ add back Save Tensors ([7142b28](https://github.com/melMass/comfy_mtb/commit/7142b284adc7fba9a1bdafd1a52621bfc168bde1))
|
|
||||||
- ✨ add TransformImage node ([11128ff](https://github.com/melMass/comfy_mtb/commit/11128ff85a7e0b4a54f405548969c2478da26df6))
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- 🚀 bump version ([cf86552](https://github.com/melMass/comfy_mtb/commit/cf865529ab64b350cd7af964b41160e7d130d12d))
|
|
||||||
- 🚀 Remove large files from release ([3b9190a](https://github.com/melMass/comfy_mtb/commit/3b9190a69b002b8933c097fd6655bb4fe07264d2))
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- ✨ cleaned up frontend code a bit ([3801a44](https://github.com/melMass/comfy_mtb/commit/3801a443bc1e89c70fdb35ce0b1724d86fa22928))
|
|
||||||
- ⚡️ remove empty inits ([21729b2](https://github.com/melMass/comfy_mtb/commit/21729b2784a50fcaf24a63ac283bdae475a53ce7))
|
|
||||||
|
|
||||||
### Merge
|
|
||||||
|
|
||||||
- 🔀 pull request #32 from melMass/dev/next ([8695cd3](https://github.com/melMass/comfy_mtb/commit/8695cd3f1b6d27b5cd6c616ed1215ea2f25c5304)) in [#32](https://github.com/melMass/comfy_mtb/pull/32)
|
|
||||||
|
|
||||||
## [0.1.0] - 2023-07-22
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- 🔥 properly match built wheels ([119b4d6](https://github.com/melMass/comfy_mtb/commit/119b4d6e16c2a90db1664ccaac748507feb73ea0)) in [#30](https://github.com/melMass/comfy_mtb/pull/30)
|
|
||||||
- ✨ also try to copy web if symlink fails ([0df55de](https://github.com/melMass/comfy_mtb/commit/0df55def29fb992751010f6b8a707699f230ff37))
|
|
||||||
- ✨ install process tested in comfy-manager (embed, colab) ([b40730d](https://github.com/melMass/comfy_mtb/commit/b40730ddbc3f8e3e7d5a17e9e9e4526ff37977fd))
|
|
||||||
- 🚀 try to support remote install too ([3c66de2](https://github.com/melMass/comfy_mtb/commit/3c66de2500a89efd2d2e3af88fc58429af725789))
|
|
||||||
- 💄 save gif issues ([7335003](https://github.com/melMass/comfy_mtb/commit/7335003346e83666c5dee631b8e6b15586d871e7))
|
|
||||||
- 🚑️ always use latest for now ([fccf313](https://github.com/melMass/comfy_mtb/commit/fccf31348994ab6e344a1ab00a8f9998309f9319))
|
|
||||||
- 🐛 install logic ([7e301e2](https://github.com/melMass/comfy_mtb/commit/7e301e2a067d41cba9b8ef357496dd1df94e4cdd))
|
|
||||||
- 🎉 remove tests & add missing docs ([4e6b877](https://github.com/melMass/comfy_mtb/commit/4e6b87719989aa144946c5c9a43b9398c20bf11e))
|
|
||||||
- ⚡️ update node_list ([c794d6a](https://github.com/melMass/comfy_mtb/commit/c794d6a071778220d654b526d2edfddcc79752fc))
|
|
||||||
- 🚑️ set debug level from endpoint ([18402e3](https://github.com/melMass/comfy_mtb/commit/18402e3be1ab47e10109cfd2dff18863a1ee56f7))
|
|
||||||
- 🐛 add base64 prefix to outputs ([0950f99](https://github.com/melMass/comfy_mtb/commit/0950f9914c9bbed7c89f3de33a967cb76f9d0bbb))
|
|
||||||
- 🎨 refactor and add Gif preview on node ([c2e8379](https://github.com/melMass/comfy_mtb/commit/c2e83794faeb8da708c98908882e38b2a42827bd))
|
|
||||||
- ✨ Various widgets issues ([27500ca](https://github.com/melMass/comfy_mtb/commit/27500ca432d686774b991045b7cffc58c0b67faf))
|
|
||||||
- 🔥 deprecate some nodes and fix image list ([9aa934f](https://github.com/melMass/comfy_mtb/commit/9aa934f70ff6adf91efb26aa8e5cb21ec575196a))
|
|
||||||
- 🐛 crop nodes ([67d3783](https://github.com/melMass/comfy_mtb/commit/67d3783ac9186da6bba4b7dc7e8dc3d5db5a1b0f))
|
|
||||||
- 🐛 tensor2pil ([8a59508](https://github.com/melMass/comfy_mtb/commit/8a59508ff91d6b2d9ca287ef1c054ec5755337a4))
|
|
||||||
- ⚡️ a few missing __doc__ ([ab09cca](https://github.com/melMass/comfy_mtb/commit/ab09ccadd905bebbf1b7b2d992e96e36fc60d68a))
|
|
||||||
- ⚡️ from tensor2np always returning a list ([6168b3a](https://github.com/melMass/comfy_mtb/commit/6168b3a2ac38b5eebed3daf9e52df5742abf6813))
|
|
||||||
- 🚑️ TF by default fills vram ([c225da5](https://github.com/melMass/comfy_mtb/commit/c225da5f298acb4cb2b39022382543c0c966d428))
|
|
||||||
- ✨ leftovers ([da3e6f4](https://github.com/melMass/comfy_mtb/commit/da3e6f47c6073e73cf9d3a3cd23ba5ccbe1fedce))
|
|
||||||
- ✨ handle non fork gdown in model dll ([95797e8](https://github.com/melMass/comfy_mtb/commit/95797e823e12e62ae8758753f60c34afbe19ec90))
|
|
||||||
- ✨ properly add the submodules ([00510ed](https://github.com/melMass/comfy_mtb/commit/00510ed0b8583dd64518daa67d963582f4f029d3))
|
|
||||||
- 📌 remove sad talker for now ([1622cbc](https://github.com/melMass/comfy_mtb/commit/1622cbcb9d51ddd0e1a8b4d87ba47b99327163eb))
|
|
||||||
- 🎨 narrow requirements ([1a92ef7](https://github.com/melMass/comfy_mtb/commit/1a92ef734dd4271efc875856038f9e3b6b9ded6c))
|
|
||||||
- 🚀 use the comfy util to handle graph interruption ([9752f3e](https://github.com/melMass/comfy_mtb/commit/9752f3e9dec9aa59cfa809aa14f0151594c03858))
|
|
||||||
- 🔥 much faster (using GPU) on windows ([2f455aa](https://github.com/melMass/comfy_mtb/commit/2f455aaca55c0a044735c768b295d077b2f5b8d6))
|
|
||||||
- 🐛 uint8 to uint16 ([be5a655](https://github.com/melMass/comfy_mtb/commit/be5a655cfaba1794f7b09c82d65de42e2b031720))
|
|
||||||
- ✨ add missing requirements ([b779bc3](https://github.com/melMass/comfy_mtb/commit/b779bc39ac19f779aeb73c98b916671d1d16806f))
|
|
||||||
- 📝 don't propagate base logs ([7fd99c2](https://github.com/melMass/comfy_mtb/commit/7fd99c25c4e50566def5c5166a9d9059b1febfa6))
|
|
||||||
- 🐛 bg upscaler in gfpgan ([fee48ad](https://github.com/melMass/comfy_mtb/commit/fee48adff3d66960cb17836f3f4efbfd0c8740c4))
|
|
||||||
- 📝 separate debug / info better ([e24863d](https://github.com/melMass/comfy_mtb/commit/e24863d1f9f63f367a2b392e6228ffa42927b71b))
|
|
||||||
- 🔥 change log level of the base logger ([7538c2c](https://github.com/melMass/comfy_mtb/commit/7538c2c4bad8390a32226dc0a5a6ef978b00d201))
|
|
||||||
- ✨ handle externs dynamicly ([6ef308a](https://github.com/melMass/comfy_mtb/commit/6ef308a87062c91e2d7249c05d96c6fb76e5a6c4))
|
|
||||||
- 🐛 separate faceswap model load ([8e267c0](https://github.com/melMass/comfy_mtb/commit/8e267c0204ce5abe8e113fd401234d49f377646a))
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- 📝 fold each comfy mode ([3c3c438](https://github.com/melMass/comfy_mtb/commit/3c3c4380bd1a3f0eed5216b835e076c26fce2f88))
|
|
||||||
- 📝 add more description to examples ([46eab5c](https://github.com/melMass/comfy_mtb/commit/46eab5ca2f0e04d872d87c849b11551fd219bdb9))
|
|
||||||
- 📝 add model notice ([cbe67ed](https://github.com/melMass/comfy_mtb/commit/cbe67edd4befb7260be01fa09af8448e5bcf5680))
|
|
||||||
- 📝 add preview for examples ([b5176ca](https://github.com/melMass/comfy_mtb/commit/b5176ca0ee489ada52b6632f68b794b4f709d5ba))
|
|
||||||
- 📝 add jp and cn (using deep translation) ([da559b9](https://github.com/melMass/comfy_mtb/commit/da559b9eaf135a49c0ab9bfa45573baf0c18dfb2))
|
|
||||||
- 📝 update readme ([b0fb522](https://github.com/melMass/comfy_mtb/commit/b0fb5222cb19e4004533d3367863be5c9ce8e72b)) in [#15](https://github.com/melMass/comfy_mtb/pull/15)
|
|
||||||
- 📝 update README.md ([f8dc768](https://github.com/melMass/comfy_mtb/commit/f8dc768635a2d21f6ff81b42c418724c432159bf))
|
|
||||||
- 📝 updated instructions ([c3b9fd4](https://github.com/melMass/comfy_mtb/commit/c3b9fd4afedbb46748aef17b40e167a4cfad65f5))
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- ✨ update install instructions ([7be37db](https://github.com/melMass/comfy_mtb/commit/7be37dbbfac45e8038f94ced8a2fa8ec2b06fb34))
|
|
||||||
- 🚀 add install script ([dad3966](https://github.com/melMass/comfy_mtb/commit/dad3966ba219c1998e4fc7f6e641864fb0e7c3e8))
|
|
||||||
- 🚧 add my CLIs ([44eaae5](https://github.com/melMass/comfy_mtb/commit/44eaae5c79f4dbec344053d945e7275be5c3c0a5))
|
|
||||||
- ✨comfy_widget shared utils ([91bb95d](https://github.com/melMass/comfy_mtb/commit/91bb95da914468de533b040324700c7f9707e4fb))
|
|
||||||
- 🚀 debug node ([b27b8ef](https://github.com/melMass/comfy_mtb/commit/b27b8ef91fe7335b1df3766547edaf4b9625ae4d))
|
|
||||||
- ✨ add FitNumber node ([aa551eb](https://github.com/melMass/comfy_mtb/commit/aa551ebe57801c69010815119fe21e19a858780c))
|
|
||||||
- 🔥 add API endpoints ([95afbdb](https://github.com/melMass/comfy_mtb/commit/95afbdbf76e66897e632252d876384ada9acf153))
|
|
||||||
- ✨ categorize ([d2b3962](https://github.com/melMass/comfy_mtb/commit/d2b396236a10fe620ebebabd5a22c36159921913))
|
|
||||||
- 🚀 add a few examples ([b9c1d3d](https://github.com/melMass/comfy_mtb/commit/b9c1d3df7a1460fe9ffa84f6f9ea0cfb5409de1a))
|
|
||||||
- ✨ added a way to export the node list ([5f5297f](https://github.com/melMass/comfy_mtb/commit/5f5297f80debc77f3fda2f0d37b3acff8419140d))
|
|
||||||
- ✨ WIP batch from history ([cde7293](https://github.com/melMass/comfy_mtb/commit/cde72938d5ffd09179f5974676e12d4599a8d6ff))
|
|
||||||
- ✨ extract node names using ast ([38f6147](https://github.com/melMass/comfy_mtb/commit/38f61473bc23b4c5d4efc5048d54c059565a6fa0))
|
|
||||||
- 🔥 add batch support for load image sequence ([3faadc4](https://github.com/melMass/comfy_mtb/commit/3faadc4b8a5049cb8c264b8a3d50565adec405f1))
|
|
||||||
- 🎨 add support for image.size(0) == 0 ([629e2b5](https://github.com/melMass/comfy_mtb/commit/629e2b5f5fbebe4e79e8b7a4cff2de6017e79225))
|
|
||||||
- ✨ image feed ([99eb5ae](https://github.com/melMass/comfy_mtb/commit/99eb5ae0c7413f6ab1f24cfc8337c9b1b2d9824c))
|
|
||||||
- ✨ FILM interpolation nodes ([e04e77e](https://github.com/melMass/comfy_mtb/commit/e04e77eb097735ec1369dec51238cdcc5abe39b7))
|
|
||||||
- ✨ add an headless option for model downloads ([217e8a1](https://github.com/melMass/comfy_mtb/commit/217e8a1546d06b97250d99612ac6bdb5ce89e155))
|
|
||||||
- 🐛 support batch count > 1 for restore face ([8ef48a0](https://github.com/melMass/comfy_mtb/commit/8ef48a013a8d6b832b8c0c7dcabc1b78c27ff207))
|
|
||||||
- 🚧 wrapper for GFPGAN bg upscaler ([88cdcc6](https://github.com/melMass/comfy_mtb/commit/88cdcc6a87dae452924e8915eccdadc69d7d136e))
|
|
||||||
- ✨ add GFPGAN (FaceRestore) ([3a6e545](https://github.com/melMass/comfy_mtb/commit/3a6e5450502f3b1d7c505178fc9ba337cd95c39e))
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- ✨ before categorize ([0cc54e5](https://github.com/melMass/comfy_mtb/commit/0cc54e58ec86c28354cae37e14f39e831c13ea02))
|
|
||||||
- ✨ add more issue templates ([710a638](https://github.com/melMass/comfy_mtb/commit/710a638a8187ef08254478f684307dccdebcded2)) in [#25](https://github.com/melMass/comfy_mtb/pull/25)
|
|
||||||
- ✨ add bug report template ([f927bc7](https://github.com/melMass/comfy_mtb/commit/f927bc7c9a82951e6df4763433732f20ea87e9cb))
|
|
||||||
- 🍻 create FUNDING.yml ([f634fe0](https://github.com/melMass/comfy_mtb/commit/f634fe0e6b2db28138e4bd7932fbfc8606a0f033))
|
|
||||||
- 🍻 add bmc to readme ([cd1b603](https://github.com/melMass/comfy_mtb/commit/cd1b603565464fe98a718e1fbaa8c7cd84057576))
|
|
||||||
- 📝 extra files from another branch ([b78be8f](https://github.com/melMass/comfy_mtb/commit/b78be8fd3cd36666fd94a3ab08eca11cce526043))
|
|
||||||
- 🚀 push leftovers ([4c41fe7](https://github.com/melMass/comfy_mtb/commit/4c41fe7af9f8e16d895eb06223349e1294dd4698))
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- ♻️ removes a few nodes, moved other around ([4d8ddac](https://github.com/melMass/comfy_mtb/commit/4d8ddaca320ce483640d030618e70730b3453df2))
|
|
||||||
- ♻️ remove test ([68c250e](https://github.com/melMass/comfy_mtb/commit/68c250e890dacae9f627b0d266ad6dcab0fa0c8b))
|
|
||||||
- 🚧 remove color_widget ([e480d07](https://github.com/melMass/comfy_mtb/commit/e480d071171cffa789930620f1e7ccc76473bf93))
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
- 🔧 pipe detection ([ee17d57](https://github.com/melMass/comfy_mtb/commit/ee17d57c3d6d71fda1a5acc2cf85f936c525bc87))
|
|
||||||
|
|
||||||
### Install
|
|
||||||
|
|
||||||
- 🚧 handle symlink errors ([d982b69](https://github.com/melMass/comfy_mtb/commit/d982b69a58c05ccead9c49370764beaa4549992a))
|
|
||||||
|
|
||||||
### Merge
|
|
||||||
|
|
||||||
- 🔀 pull request #22 from melMass/dev/next-release ([c34de0a](https://github.com/melMass/comfy_mtb/commit/c34de0ab351b2c95d7fa4fab4487155bee6bfa3a)) in [#22](https://github.com/melMass/comfy_mtb/pull/22)
|
|
||||||
- 🎉 pull request #11 from dev/frame_interpolation ([1e28606](https://github.com/melMass/comfy_mtb/commit/1e28606427bcc8d895b87eaa6cd4147ab6d9a11f)) in [#11](https://github.com/melMass/comfy_mtb/pull/11)
|
|
||||||
- 🎉 pull request #8 from dev/small-fixes ([7585624](https://github.com/melMass/comfy_mtb/commit/7585624de5895eb34c6a520d4dab18b47e64b6ca)) in [#8](https://github.com/melMass/comfy_mtb/pull/8)
|
|
||||||
|
|
||||||
## [0.0.1] - 2023-06-28
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- 🤦 add missing file ([e2c4561](https://github.com/melMass/comfy_mtb/commit/e2c456147c260b4e9d583662e3bb9d6d9a019a5e))
|
|
||||||
- ✨ small edits ([bcf55ca](https://github.com/melMass/comfy_mtb/commit/bcf55ca9a3a07067be3319182501f7b635e5d2ba))
|
|
||||||
- ⚡️ add support for batch in roop ([2dae020](https://github.com/melMass/comfy_mtb/commit/2dae02056a11ddfe1f84ee040818028177e404b5))
|
|
||||||
- 🔥 various preparing for the first tag ([793784a](https://github.com/melMass/comfy_mtb/commit/793784a5fd08e8a70d670fc8edbc3bb5b6e13e67))
|
|
||||||
- 🐛 various bugs ([afd0843](https://github.com/melMass/comfy_mtb/commit/afd08431458e3bbb14a25c84a87408113edf5db5))
|
|
||||||
- ⚡️ add missing controls to QRCode ([7e86b0e](https://github.com/melMass/comfy_mtb/commit/7e86b0ed4d300021517f6c5cf28a45012497b5c5))
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- 📝 add rembg screenshot ([9a2d523](https://github.com/melMass/comfy_mtb/commit/9a2d52325f87ecf6342ef4897da919006755b9db))
|
|
||||||
- 📝 add a few screenshots ([e162336](https://github.com/melMass/comfy_mtb/commit/e162336cd366d39cd4b96f05b3c9c68eecec3dc4))
|
|
||||||
- 📝 update readme ([7f3070d](https://github.com/melMass/comfy_mtb/commit/7f3070debbc3330da50ff845621ce299894cf862))
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- 💄 faceswap node using roop ([966a14b](https://github.com/melMass/comfy_mtb/commit/966a14b40d88f4fccfb2eaa5ff9b222f0eedd7cb))
|
|
||||||
- ✨ sync local changes ([647bf9e](https://github.com/melMass/comfy_mtb/commit/647bf9e94195c279a620c74c2253471b9c4b90f7))
|
|
||||||
- ✨ bbox from alpha ([37abf8a](https://github.com/melMass/comfy_mtb/commit/37abf8aad12f4711c6d82c6be4be6fa3578e7af5))
|
|
||||||
- ✨ a111 like style loader ([f59b68e](https://github.com/melMass/comfy_mtb/commit/f59b68e3ad92841a4d189d8dddf7b41e915c9b4e))
|
|
||||||
- ✨ add a color type and widget ([9a2e986](https://github.com/melMass/comfy_mtb/commit/9a2e986327c34227a707beab6d9929b0a05e41e6))
|
|
||||||
- ✨ add a few nodes ([811443b](https://github.com/melMass/comfy_mtb/commit/811443b92161815db1cdff81898e8834dcd6fbfa))
|
|
||||||
- ✨ add SadTalker as a submodule ([3fb8716](https://github.com/melMass/comfy_mtb/commit/3fb871651b12bce62d8e911bd3884f417f80c937))
|
|
||||||
- 🚨 push local changes ([6cac344](https://github.com/melMass/comfy_mtb/commit/6cac344f6fb15ebb902acee70ee71edc585ec4bc))
|
|
||||||
- ⚡️ initial commit ([1ae3bbc](https://github.com/melMass/comfy_mtb/commit/1ae3bbc89ae6e0d2e8c61122485bd0df837e17c2))
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- 🚀 add gh action ([572b4d5](https://github.com/melMass/comfy_mtb/commit/572b4d52bce1398660d4d7ca0c5c48c11e0128e3)) in [#4](https://github.com/melMass/comfy_mtb/pull/4)
|
|
||||||
|
|
||||||
[main]: https://github.com/melMass/comfy_mtb/compare/v0.1.4..main
|
|
||||||
[0.1.4]: https://github.com/melMass/comfy_mtb/compare/v0.1.3..v0.1.4
|
|
||||||
[0.1.3]: https://github.com/melMass/comfy_mtb/compare/v0.1.2..v0.1.3
|
|
||||||
[0.1.2]: https://github.com/melMass/comfy_mtb/compare/v0.1.1..v0.1.2
|
|
||||||
[0.1.1]: https://github.com/melMass/comfy_mtb/compare/v0.1.0..v0.1.1
|
|
||||||
[0.1.0]: https://github.com/melMass/comfy_mtb/compare/v0.0.1..v0.1.0
|
|
||||||
|
|
||||||
@@ -322,6 +322,14 @@ if hasattr(PromptServer, "instance"):
|
|||||||
|
|
||||||
return await endpoint.do_action(request)
|
return await endpoint.do_action(request)
|
||||||
|
|
||||||
|
@PromptServer.instance.routes.get("/mtb/audio")
|
||||||
|
async def get_audio(request):
|
||||||
|
from . import endpoint
|
||||||
|
|
||||||
|
reload(endpoint)
|
||||||
|
|
||||||
|
return await endpoint.get_audio(request)
|
||||||
|
|
||||||
|
|
||||||
# - WAS Dictionary
|
# - WAS Dictionary
|
||||||
MANIFEST = {
|
MANIFEST = {
|
||||||
|
|||||||
-83
@@ -1,83 +0,0 @@
|
|||||||
[changelog]
|
|
||||||
header = """
|
|
||||||
# Changelog\n
|
|
||||||
This is an automated changelog based on the commits in this repository.
|
|
||||||
|
|
||||||
Check the notes in the [releases](https://github.com/melMass/comfy_mtb/releases) for more information.
|
|
||||||
"""
|
|
||||||
# https://keats.github.io/tera/docs/#introduction
|
|
||||||
body = """
|
|
||||||
{% if version -%}\
|
|
||||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
|
||||||
{% else %}\
|
|
||||||
## [Unreleased]
|
|
||||||
{% endif -%}\
|
|
||||||
|
|
||||||
{% for group, commits in commits | group_by(attribute="group") %}
|
|
||||||
### {{ group | upper_first }}
|
|
||||||
{% for commit in commits %}
|
|
||||||
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first | trim }} ([{{ commit.id | truncate(length=7, end="") }}](<REPO>/commit/{{ commit.id }}))\
|
|
||||||
{% if commit.github.username and commit.github.username != remote.github.owner %} by [@{{ commit.github.username }}](https://github.com/{{ commit.github.username }}){%- endif -%}
|
|
||||||
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}](<REPO>/pull/{{ commit.github.pr_number }}){%- endif -%}
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
|
||||||
## New Contributors
|
|
||||||
{%- endif -%}
|
|
||||||
|
|
||||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
|
||||||
* [@{{ contributor.username }}](https://github.com/{{ contributor.username }}) made their first contribution in [#{{ contributor.pr_number }}](<REPO>/pull/{{ contributor.pr_number }})\
|
|
||||||
{%- endfor %}\n
|
|
||||||
"""
|
|
||||||
footer = """
|
|
||||||
{% for release in releases -%}
|
|
||||||
{% if release.version -%}
|
|
||||||
{% if release.previous.version -%}
|
|
||||||
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
|
||||||
<REPO>/compare/{{ release.previous.version }}..{{ release.version }}
|
|
||||||
{% endif -%}
|
|
||||||
{% else -%}
|
|
||||||
[unreleased]: <REPO>/compare/{{ release.previous.version }}..HEAD
|
|
||||||
{% endif -%}
|
|
||||||
{% endfor %}
|
|
||||||
"""
|
|
||||||
trim = true
|
|
||||||
postprocessors = [
|
|
||||||
{ pattern = '<REPO>', replace = "https://github.com/melMass/comfy_mtb" }, # replace repository URL
|
|
||||||
]
|
|
||||||
|
|
||||||
[git]
|
|
||||||
# https://www.conventionalcommits.org
|
|
||||||
conventional_commits = true
|
|
||||||
filter_unconventional = true
|
|
||||||
split_commits = false
|
|
||||||
commit_preprocessors = [
|
|
||||||
# { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))" }, # replace issue numbers
|
|
||||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
|
||||||
]
|
|
||||||
commit_parsers = [
|
|
||||||
{ message = "^feat", group = "Features" },
|
|
||||||
{ message = "^fix", group = "Bug Fixes" },
|
|
||||||
{ message = "^doc", group = "Documentation" },
|
|
||||||
{ message = "^perf", group = "Performance" },
|
|
||||||
{ message = "^refactor", group = "Refactor" },
|
|
||||||
{ message = "^style", group = "Styling" },
|
|
||||||
{ message = "^test", group = "Testing" },
|
|
||||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
|
||||||
{ message = "^chore\\(deps\\)", skip = true },
|
|
||||||
{ message = "^chore\\(pr\\)", skip = true },
|
|
||||||
{ message = "^chore\\(pull\\)", skip = true },
|
|
||||||
{ message = "^chore|ci", group = "Miscellaneous Tasks" },
|
|
||||||
{ body = ".*security", group = "Security" },
|
|
||||||
{ message = "^revert", group = "Revert" },
|
|
||||||
]
|
|
||||||
protect_breaking_commits = false
|
|
||||||
filter_commits = false
|
|
||||||
tag_pattern = "v[0-9].*"
|
|
||||||
topo_order = false
|
|
||||||
sort_commits = "newest"
|
|
||||||
|
|
||||||
[remote.github]
|
|
||||||
owner = "melMass"
|
|
||||||
repo = "comfy_mtb"
|
|
||||||
+55
-4
@@ -3,7 +3,16 @@ import csv
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .log import mklog
|
from .log import mklog
|
||||||
from .utils import backup_file, here, import_install, reqs_map, run_command, styles_dir
|
from .utils import (
|
||||||
|
audioInputDir,
|
||||||
|
backup_file,
|
||||||
|
comfy_dir,
|
||||||
|
here,
|
||||||
|
import_install,
|
||||||
|
reqs_map,
|
||||||
|
run_command,
|
||||||
|
styles_dir,
|
||||||
|
)
|
||||||
|
|
||||||
endlog = mklog("mtb endpoint")
|
endlog = mklog("mtb endpoint")
|
||||||
|
|
||||||
@@ -15,6 +24,29 @@ from pathlib import Path
|
|||||||
import_install("requirements")
|
import_install("requirements")
|
||||||
|
|
||||||
|
|
||||||
|
def ACTIONS_loadAudio(args):
|
||||||
|
if not audioInputDir.exists():
|
||||||
|
audioInputDir.mkdir()
|
||||||
|
|
||||||
|
endlog.debug(f"Received Load Audio request for {args}")
|
||||||
|
|
||||||
|
if not args.file:
|
||||||
|
return web.Response(status=400)
|
||||||
|
|
||||||
|
filename = args.filename
|
||||||
|
if not filename:
|
||||||
|
return web.Response(status=400)
|
||||||
|
|
||||||
|
target = audioInputDir / filename
|
||||||
|
if target.exists():
|
||||||
|
target.unlink()
|
||||||
|
|
||||||
|
with target.open("wb") as f:
|
||||||
|
f.write(args.file.read())
|
||||||
|
|
||||||
|
return {"name": filename}
|
||||||
|
|
||||||
|
|
||||||
def ACTIONS_installDependency(dependency_names=None):
|
def ACTIONS_installDependency(dependency_names=None):
|
||||||
if dependency_names is None:
|
if dependency_names is None:
|
||||||
return {"error": "No dependency name provided"}
|
return {"error": "No dependency name provided"}
|
||||||
@@ -88,7 +120,7 @@ def ACTIONS_saveStyle(data):
|
|||||||
|
|
||||||
async def do_action(request) -> web.Response:
|
async def do_action(request) -> web.Response:
|
||||||
endlog.debug("Init action request")
|
endlog.debug("Init action request")
|
||||||
request_data = await request.json()
|
request_data = await request.post()
|
||||||
name = request_data.get("name")
|
name = request_data.get("name")
|
||||||
args = request_data.get("args")
|
args = request_data.get("args")
|
||||||
|
|
||||||
@@ -100,14 +132,33 @@ async def do_action(request) -> web.Response:
|
|||||||
if callable(method):
|
if callable(method):
|
||||||
result = method(args) if args else method()
|
result = method(args) if args else method()
|
||||||
endlog.debug(f"Action result: {result}")
|
endlog.debug(f"Action result: {result}")
|
||||||
return web.json_response({"result": result})
|
return web.json_response({"result": result}, status=200)
|
||||||
|
|
||||||
available_methods = [
|
available_methods = [
|
||||||
attr[len("ACTIONS_") :] for attr in globals() if attr.startswith("ACTIONS_")
|
attr[len("ACTIONS_") :] for attr in globals() if attr.startswith("ACTIONS_")
|
||||||
]
|
]
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"error": "Invalid method name.", "available_methods": available_methods}
|
{"error": "Invalid method name.", "available_methods": available_methods},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_audio(request):
|
||||||
|
name = request.rel_url.query.get("filename")
|
||||||
|
if not name:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "No filename provided as url query."}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
target = audioInputDir / name
|
||||||
|
if not target.exists():
|
||||||
|
return web.json_response(
|
||||||
|
{"error": f"File {name} (in {audioInputDir}) not found..."}, status=404
|
||||||
|
)
|
||||||
|
|
||||||
|
return web.FileResponse(
|
||||||
|
target, headers={"Content-Disposition": f'filename="{name}"'}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{"last_node_id":9,"last_link_id":0,"nodes":[{"id":9,"type":"Note Plus (mtb)","pos":[332, 139, 0, 0, 0, 0, 0, 0, 0, 0],"size":[573.4446126650389, 1292.5298919072263],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[],"title":"Note+ (mtb)","properties":{},"widgets_values":["# Note+ Demo\n\n# Images \nyou can resize them (see showdown syntax)\n\n\n\n# iFrame (embeds)\n<iframe src=\"https://www.youtube.com/embed/tgbNymZ7vqY\">\n</iframe>\n\n# Headings\n\n# h1 Heading:smile:\n\n## h2 Heading\n\n### h3 Heading\n\n#### h4 Heading\n\n##### h5 Heading\n\n###### h6 Heading\n\n# Tables\n\nColons can be used to align columns.\n\n| Tables|Are|Cool |\n| ------------- |:-----------:| ----:|\n| col 3 is| right-aligned | $1600 |\n| col 2 is| centered| $12 |\n| zebra stripes | are neat|$1 |\n\nEmphasis, aka italics, with _asterisks_ or _underscores_.\n\nStrong emphasis, aka bold, with **asterisks** or **underscores**.\n\nCombined emphasis with **asterisks and _underscores_**.\n\nStrikethrough uses two tildes. ~~Scratch this.~~\n\n**This is bold text**\n\n**This is bold text**\n\n_This is italic text_\n\n_This is italic text_\n\n~~Strikethrough~~\n\n1. First ordered list item\n2. Another item\n\n- Unordered sub-list.\n\n1. Actual numbers don't matter, just that it's a number\n1. Ordered sub-list\n1. And another item.\n1.\n\n- [x] Finish my changes\n- [] Push my commits to GitHub\n- [] Open a pull request\n- [x] mentions:@melmass, #refs, [links](), **formatting**, and <del>tags</del> supported\n- [x] list syntax required (any unordered or ordered list supported)\n- [x] this is a complete item\n- [] this is an incomplete item\n","markdown","*{\ncolor:whitesmoke;\n}\n\nh1{\ncolor:cyan;\n}\nh2{\ncolor:yellow;\n}\nh3{\ncolor:pink;\n}\n\nstrong{\ncolor:red;\n}"],"color":"#223","bgcolor":"#335","shape":1}],"links":[],"groups":[],"config":{},"extra":{},"version":0.4}
|
|
||||||
+1
-2
@@ -42,14 +42,13 @@
|
|||||||
"Mask To Image (mtb)": "Converts a mask (alpha) to an RGB image with a color and background",
|
"Mask To Image (mtb)": "Converts a mask (alpha) to an RGB image with a color and background",
|
||||||
"Math Expression (mtb)": "Node to evaluate a simple math expression string",
|
"Math Expression (mtb)": "Node to evaluate a simple math expression string",
|
||||||
"Model Patch Seamless (mtb)": "Uses the stable diffusion 'hack' to infer seamless images by setting the model layers padding mode to circular (experimental)",
|
"Model Patch Seamless (mtb)": "Uses the stable diffusion 'hack' to infer seamless images by setting the model layers padding mode to circular (experimental)",
|
||||||
"Pick From Batch (mtb)": "Pick a specific number of images from a batch, either from the start or end.",
|
|
||||||
"Qr Code (mtb)": "Basic QR Code generator",
|
"Qr Code (mtb)": "Basic QR Code generator",
|
||||||
"Restore Face (mtb)": "Uses GFPGan to restore faces",
|
"Restore Face (mtb)": "Uses GFPGan to restore faces",
|
||||||
"Save Gif (mtb)": "Save the images from the batch as a GIF",
|
"Save Gif (mtb)": "Save the images from the batch as a GIF",
|
||||||
"Save Image Grid (mtb)": "Save all the images in the input batch as a grid of images.",
|
"Save Image Grid (mtb)": "Save all the images in the input batch as a grid of images.",
|
||||||
"Save Image Sequence (mtb)": "Save an image sequence to a folder. The current frame is used to determine which image to save.\n\n This is merely a wrapper around the `save_images` function with formatting for the output folder and filename.\n ",
|
"Save Image Sequence (mtb)": "Save an image sequence to a folder. The current frame is used to determine which image to save.\n\n This is merely a wrapper around the `save_images` function with formatting for the output folder and filename.\n ",
|
||||||
"Save Tensors (mtb)": "Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy",
|
"Save Tensors (mtb)": "Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy",
|
||||||
"Sharpen (mtb)": "Sharpens an image using a Gaussian kernel.",
|
"Sharpen (mtb)": "Sharpens an image using a Gaussian kernel.",
|
||||||
"Smart Step (mtb)": "Utils to control the steps start/stop of the KAdvancedSampler in percentage",
|
"Smart Step (mtb)": "Utils to control the steps start/stop of the KAdvancedSampler in percentage",
|
||||||
"Stack Images (mtb)": "Stack the input images horizontally or vertically",
|
"Stack Images (mtb)": "Stack the input images horizontally or vertically",
|
||||||
"String Replace (mtb)": "Basic string replacement",
|
"String Replace (mtb)": "Basic string replacement",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
"""MTB Nodes module."""
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ from ..log import log
|
|||||||
|
|
||||||
|
|
||||||
class AnimationBuilder:
|
class AnimationBuilder:
|
||||||
"""Simple maths for animation."""
|
"""Convenient way to manage basic animation maths at the core of many of my workflows"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
|
|||||||
+64
-164
@@ -1,6 +1,7 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
import torchaudio
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -9,6 +10,11 @@ from ..log import log
|
|||||||
from ..utils import apply_easing, pil2tensor
|
from ..utils import apply_easing, pil2tensor
|
||||||
from .transform import TransformImage
|
from .transform import TransformImage
|
||||||
|
|
||||||
|
try:
|
||||||
|
import librosa
|
||||||
|
except ImportError:
|
||||||
|
log.warning("librosa not installed. Batch Audio features will not be available.")
|
||||||
|
|
||||||
|
|
||||||
def hex_to_rgb(hex_color, bgr=False):
|
def hex_to_rgb(hex_color, bgr=False):
|
||||||
hex_color = hex_color.lstrip("#")
|
hex_color = hex_color.lstrip("#")
|
||||||
@@ -92,9 +98,7 @@ class BatchShape:
|
|||||||
res = []
|
res = []
|
||||||
for x in range(count):
|
for x in range(count):
|
||||||
# Initialize an image canvas
|
# Initialize an image canvas
|
||||||
canvas = np.full(
|
canvas = np.full((image_height, image_width, 3), bg_color, dtype=np.uint8)
|
||||||
(image_height, image_width, 3), bg_color, dtype=np.uint8
|
|
||||||
)
|
|
||||||
mask = np.zeros((image_height, image_width), dtype=np.uint8)
|
mask = np.zeros((image_height, image_width), dtype=np.uint8)
|
||||||
|
|
||||||
# Compute the center point of the shape
|
# Compute the center point of the shape
|
||||||
@@ -124,9 +128,7 @@ class BatchShape:
|
|||||||
# Apply shading effects to a separate shading canvas
|
# Apply shading effects to a separate shading canvas
|
||||||
shading = np.zeros_like(canvas, dtype=np.float32)
|
shading = np.zeros_like(canvas, dtype=np.float32)
|
||||||
shading[:, :, 0] = shadex * np.linspace(0, 1, image_width)
|
shading[:, :, 0] = shadex * np.linspace(0, 1, image_width)
|
||||||
shading[:, :, 1] = shadey * np.linspace(
|
shading[:, :, 1] = shadey * np.linspace(0, 1, image_height).reshape(-1, 1)
|
||||||
0, 1, image_height
|
|
||||||
).reshape(-1, 1)
|
|
||||||
shading_canvas = cv2.addWeighted(
|
shading_canvas = cv2.addWeighted(
|
||||||
canvas.astype(np.float32), 1, shading, 1, 0
|
canvas.astype(np.float32), 1, shading, 1, 0
|
||||||
).astype(np.uint8)
|
).astype(np.uint8)
|
||||||
@@ -159,9 +161,7 @@ class BatchFloatFill:
|
|||||||
def fill_floats(self, floats, direction, value, count):
|
def fill_floats(self, floats, direction, value, count):
|
||||||
size = len(floats)
|
size = len(floats)
|
||||||
if size > count:
|
if size > count:
|
||||||
raise ValueError(
|
raise ValueError(f"Size ({size}) is less then target count ({count})")
|
||||||
f"Size ({size}) is less then target count ({count})"
|
|
||||||
)
|
|
||||||
|
|
||||||
rem = count - size
|
rem = count - size
|
||||||
if direction == "tail":
|
if direction == "tail":
|
||||||
@@ -264,10 +264,7 @@ class BatchMerge:
|
|||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"fusion_mode": (
|
"fusion_mode": (["add", "multiply", "average"], {"default": "average"}),
|
||||||
["add", "multiply", "average"],
|
|
||||||
{"default": "average"},
|
|
||||||
),
|
|
||||||
"fill": (["head", "tail"], {"default": "tail"}),
|
"fill": (["head", "tail"], {"default": "tail"}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,9 +282,7 @@ class BatchMerge:
|
|||||||
frame_count = img.shape[0]
|
frame_count = img.shape[0]
|
||||||
if frame_count < max_frames:
|
if frame_count < max_frames:
|
||||||
fill_frame = img[0] if fill == "head" else img[-1]
|
fill_frame = img[0] if fill == "head" else img[-1]
|
||||||
fill_frames = fill_frame.repeat(
|
fill_frames = fill_frame.repeat(max_frames - frame_count, 1, 1, 1)
|
||||||
max_frames - frame_count, 1, 1, 1
|
|
||||||
)
|
|
||||||
adjusted_batch = (
|
adjusted_batch = (
|
||||||
torch.cat((fill_frames, img), dim=0)
|
torch.cat((fill_frames, img), dim=0)
|
||||||
if fill == "head"
|
if fill == "head"
|
||||||
@@ -360,12 +355,9 @@ class Batch2dTransform:
|
|||||||
shear=None,
|
shear=None,
|
||||||
):
|
):
|
||||||
if all(
|
if all(
|
||||||
self.get_num_elements(param) <= 0
|
self.get_num_elements(param) <= 0 for param in [x, y, zoom, angle, shear]
|
||||||
for param in [x, y, zoom, angle, shear]
|
|
||||||
):
|
):
|
||||||
raise ValueError(
|
raise ValueError("At least one transform parameter must be provided")
|
||||||
"At least one transform parameter must be provided"
|
|
||||||
)
|
|
||||||
|
|
||||||
keyframes = {"x": [], "y": [], "zoom": [], "angle": [], "shear": []}
|
keyframes = {"x": [], "y": [], "zoom": [], "angle": [], "shear": []}
|
||||||
|
|
||||||
@@ -408,138 +400,6 @@ class Batch2dTransform:
|
|||||||
return (torch.cat(res, dim=0),)
|
return (torch.cat(res, dim=0),)
|
||||||
|
|
||||||
|
|
||||||
class PlotBatchFloat:
|
|
||||||
"""Plot floats"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(cls):
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"width": ("INT", {"default": 768}),
|
|
||||||
"height": ("INT", {"default": 768}),
|
|
||||||
"point_size": ("INT", {"default": 4}),
|
|
||||||
"seed": ("INT", {"default": 1}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
RETURN_NAMES = ("plot",)
|
|
||||||
FUNCTION = "plot"
|
|
||||||
CATEGORY = "mtb/batch"
|
|
||||||
|
|
||||||
def plot(self, width, height, point_size, seed, **kwargs):
|
|
||||||
import matplotlib
|
|
||||||
|
|
||||||
matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(width / 100, height / 100), dpi=100)
|
|
||||||
fig.set_edgecolor("black")
|
|
||||||
fig.patch.set_facecolor("#2e2e2e")
|
|
||||||
# Setting background color and grid
|
|
||||||
ax.set_facecolor("#2e2e2e") # Dark gray background
|
|
||||||
ax.grid(color="gray", linestyle="-", linewidth=0.5, alpha=0.5)
|
|
||||||
|
|
||||||
# Finding global min and max across all lists for scaling the plot
|
|
||||||
global_min = min(min(values) for values in kwargs.values())
|
|
||||||
global_max = max(max(values) for values in kwargs.values())
|
|
||||||
|
|
||||||
# Color cycle to ensure each plot has a distinct color
|
|
||||||
colormap = plt.cm.get_cmap("viridis", len(kwargs))
|
|
||||||
color_normalization_factor = (
|
|
||||||
0.5 if len(kwargs) == 1 else (len(kwargs) - 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Plotting each list with a unique color
|
|
||||||
for i, (label, values) in enumerate(kwargs.items()):
|
|
||||||
color_value = i / color_normalization_factor
|
|
||||||
ax.plot(values, label=label, color=colormap(color_value))
|
|
||||||
|
|
||||||
ax.set_ylim(global_min, global_max) # Scaling the y-axis
|
|
||||||
ax.legend(
|
|
||||||
title="Legend",
|
|
||||||
title_fontsize="large",
|
|
||||||
fontsize="medium",
|
|
||||||
edgecolor="black",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Setting labels and title
|
|
||||||
ax.set_xlabel("Time", fontsize="large", color="white")
|
|
||||||
ax.set_ylabel("Value", fontsize="large", color="white")
|
|
||||||
ax.set_title(
|
|
||||||
"Plot of Values over Time", fontsize="x-large", color="white"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Adjusting tick colors to be visible on dark background
|
|
||||||
ax.tick_params(colors="white")
|
|
||||||
|
|
||||||
# Changing color of the axes border
|
|
||||||
for _, spine in ax.spines.items():
|
|
||||||
spine.set_edgecolor("white")
|
|
||||||
|
|
||||||
# Rendering the plot into a NumPy array
|
|
||||||
buf = BytesIO()
|
|
||||||
plt.savefig(buf, format="png", bbox_inches="tight")
|
|
||||||
buf.seek(0)
|
|
||||||
image = Image.open(buf)
|
|
||||||
plt.close(fig) # Closing the figure to free up memory
|
|
||||||
|
|
||||||
return (pil2tensor(image),)
|
|
||||||
|
|
||||||
def draw_point(self, image, point, color, point_size):
|
|
||||||
x, y = point
|
|
||||||
y = image.shape[0] - 1 - y # Invert Y-coordinate
|
|
||||||
half_size = point_size // 2
|
|
||||||
x_start, x_end = (
|
|
||||||
max(0, x - half_size),
|
|
||||||
min(image.shape[1], x + half_size + 1),
|
|
||||||
)
|
|
||||||
y_start, y_end = (
|
|
||||||
max(0, y - half_size),
|
|
||||||
min(image.shape[0], y + half_size + 1),
|
|
||||||
)
|
|
||||||
image[y_start:y_end, x_start:x_end] = color
|
|
||||||
|
|
||||||
def draw_line(self, image, start, end, color):
|
|
||||||
x1, y1 = start
|
|
||||||
x2, y2 = end
|
|
||||||
|
|
||||||
# Invert Y-coordinate
|
|
||||||
y1 = image.shape[0] - 1 - y1
|
|
||||||
y2 = image.shape[0] - 1 - y2
|
|
||||||
|
|
||||||
dx = x2 - x1
|
|
||||||
dy = y2 - y1
|
|
||||||
is_steep = abs(dy) > abs(dx)
|
|
||||||
if is_steep:
|
|
||||||
x1, y1 = y1, x1
|
|
||||||
x2, y2 = y2, x2
|
|
||||||
swapped = False
|
|
||||||
if x1 > x2:
|
|
||||||
x1, x2 = x2, x1
|
|
||||||
y1, y2 = y2, y1
|
|
||||||
swapped = True
|
|
||||||
dx = x2 - x1
|
|
||||||
dy = y2 - y1
|
|
||||||
error = int(dx / 2.0)
|
|
||||||
y = y1
|
|
||||||
ystep = None
|
|
||||||
if y1 < y2:
|
|
||||||
ystep = 1
|
|
||||||
else:
|
|
||||||
ystep = -1
|
|
||||||
for x in range(x1, x2 + 1):
|
|
||||||
coord = (y, x) if is_steep else (x, y)
|
|
||||||
image[coord] = color
|
|
||||||
error -= abs(dy)
|
|
||||||
if error < 0:
|
|
||||||
y += ystep
|
|
||||||
error += dx
|
|
||||||
if swapped:
|
|
||||||
image[(x1, y1)] = color
|
|
||||||
image[(x2, y2)] = color
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_INTERPOLANT = lambda t: t * t * t * (t * (t * 6 - 15) + 10)
|
DEFAULT_INTERPOLANT = lambda t: t * t * t * (t * (t * 6 - 15) + 10)
|
||||||
|
|
||||||
|
|
||||||
@@ -595,9 +455,7 @@ class BatchShake:
|
|||||||
delta = (res[0] / shape[0], res[1] / shape[1])
|
delta = (res[0] / shape[0], res[1] / shape[1])
|
||||||
d = (shape[0] // res[0], shape[1] // res[1])
|
d = (shape[0] // res[0], shape[1] // res[1])
|
||||||
grid = (
|
grid = (
|
||||||
np.mgrid[0 : res[0] : delta[0], 0 : res[1] : delta[1]].transpose(
|
np.mgrid[0 : res[0] : delta[0], 0 : res[1] : delta[1]].transpose(1, 2, 0)
|
||||||
1, 2, 0
|
|
||||||
)
|
|
||||||
% 1
|
% 1
|
||||||
)
|
)
|
||||||
# Gradients
|
# Gradients
|
||||||
@@ -616,9 +474,7 @@ class BatchShake:
|
|||||||
n00 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1])) * g00, 2)
|
n00 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1])) * g00, 2)
|
||||||
n10 = np.sum(np.dstack((grid[:, :, 0] - 1, grid[:, :, 1])) * g10, 2)
|
n10 = np.sum(np.dstack((grid[:, :, 0] - 1, grid[:, :, 1])) * g10, 2)
|
||||||
n01 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1] - 1)) * g01, 2)
|
n01 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1] - 1)) * g01, 2)
|
||||||
n11 = np.sum(
|
n11 = np.sum(np.dstack((grid[:, :, 0] - 1, grid[:, :, 1] - 1)) * g11, 2)
|
||||||
np.dstack((grid[:, :, 0] - 1, grid[:, :, 1] - 1)) * g11, 2
|
|
||||||
)
|
|
||||||
# Interpolation
|
# Interpolation
|
||||||
t = interpolant(grid)
|
t = interpolant(grid)
|
||||||
n0 = n00 * (1 - t[:, :, 0]) + t[:, :, 0] * n10
|
n0 = n00 * (1 - t[:, :, 0]) + t[:, :, 0] * n10
|
||||||
@@ -666,10 +522,7 @@ class BatchShake:
|
|||||||
amplitude = 1
|
amplitude = 1
|
||||||
for _ in range(octaves):
|
for _ in range(octaves):
|
||||||
noise += amplitude * self.generate_perlin_noise_2d(
|
noise += amplitude * self.generate_perlin_noise_2d(
|
||||||
shape,
|
shape, (frequency * res[0], frequency * res[1]), tileable, interpolant
|
||||||
(frequency * res[0], frequency * res[1]),
|
|
||||||
tileable,
|
|
||||||
interpolant,
|
|
||||||
)
|
)
|
||||||
frequency *= lacunarity
|
frequency *= lacunarity
|
||||||
amplitude *= persistence
|
amplitude *= persistence
|
||||||
@@ -763,14 +616,61 @@ class BatchShake:
|
|||||||
return (shaken_images, x_translations, y_translations, rotations)
|
return (shaken_images, x_translations, y_translations, rotations)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchFloatsFromSound:
|
||||||
|
"""Extracts a list of floats based on audio frequency band peaks."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"audio": ("AUDIO",),
|
||||||
|
"sensitivity": ("FLOAT", {"default": 1.0}),
|
||||||
|
"low_freq": ("FLOAT", {"default": 100.0}),
|
||||||
|
"high_freq": ("FLOAT", {"default": 2000.0}),
|
||||||
|
"hop_length": ("INT", {"default": 512}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("FLOATS",)
|
||||||
|
RETURN_NAMES = ("float_data",)
|
||||||
|
FUNCTION = "process_audio"
|
||||||
|
CATEGORY = "mtb/audio"
|
||||||
|
|
||||||
|
def process_audio(
|
||||||
|
self,
|
||||||
|
audio,
|
||||||
|
sensitivity=1.0,
|
||||||
|
low_freq=100,
|
||||||
|
high_freq=2000,
|
||||||
|
hop_length=512,
|
||||||
|
):
|
||||||
|
# audio_data, _ = librosa.load(audio_file_path, sr=sample_rate)
|
||||||
|
|
||||||
|
# audio_data_tensor = audio.squeeze(1) # Remove the channel dimension if present
|
||||||
|
# audio_tensor = audio_data_tensor.float()
|
||||||
|
audio_data = audio.to(device=torchaudio.transforms.Spectrogram().window.device)
|
||||||
|
|
||||||
|
hop_length = 512
|
||||||
|
stft = torchaudio.transforms.Spectrogram()(audio_data)
|
||||||
|
freqs = torchaudio.transforms.FrequencyMasking(low_freq, high_freq)(stft)
|
||||||
|
band_energy = torch.sum(freqs, dim=1)
|
||||||
|
|
||||||
|
min_val = torch.min(band_energy)
|
||||||
|
max_val = torch.max(band_energy)
|
||||||
|
normalized_peaks = (band_energy - min_val) / (max_val - min_val)
|
||||||
|
scaled_peaks = normalized_peaks * sensitivity
|
||||||
|
|
||||||
|
return (scaled_peaks.tolist(),)
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [
|
__nodes__ = [
|
||||||
BatchFloat,
|
BatchFloat,
|
||||||
Batch2dTransform,
|
Batch2dTransform,
|
||||||
|
BatchFloatsFromSound,
|
||||||
BatchShape,
|
BatchShape,
|
||||||
BatchMake,
|
BatchMake,
|
||||||
BatchFloatAssemble,
|
BatchFloatAssemble,
|
||||||
BatchFloatFill,
|
BatchFloatFill,
|
||||||
BatchMerge,
|
BatchMerge,
|
||||||
BatchShake,
|
BatchShake,
|
||||||
PlotBatchFloat,
|
|
||||||
]
|
]
|
||||||
|
|||||||
+6
-13
@@ -1,4 +1,5 @@
|
|||||||
import csv, shutil
|
import csv
|
||||||
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import folder_paths
|
import folder_paths
|
||||||
@@ -156,21 +157,13 @@ class StylesLoader:
|
|||||||
for file in files:
|
for file in files:
|
||||||
with open(file, "r", encoding="utf8") as f:
|
with open(file, "r", encoding="utf8") as f:
|
||||||
parsed = csv.reader(f)
|
parsed = csv.reader(f)
|
||||||
for i, row in enumerate(parsed):
|
for row in parsed:
|
||||||
log.debug(f"Adding style {row[0]}")
|
log.debug(f"Adding style {row[0]}")
|
||||||
try:
|
try:
|
||||||
name, positive, negative = (row + [None] * 3)[:3]
|
cls.options[row[0]] = (row[1], row[2])
|
||||||
positive = positive or ""
|
except Exception:
|
||||||
negative = negative or ""
|
|
||||||
if name is not None:
|
|
||||||
cls.options[name] = (positive, negative)
|
|
||||||
else:
|
|
||||||
# Handle the case where 'name' is None
|
|
||||||
log.warning(f"Missing 'name' in row {i}.")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(
|
log.warning(
|
||||||
f"There was an error while parsing {file}, make sure it respects A1111 format, i.e 3 columns name, positive, negative:\n{e}"
|
f"There was an error while parsing {file}, make sure it respects A1111 format, i.e 3 columns name, positive, negative"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
+7
-11
@@ -44,7 +44,6 @@ class BboxFromMask:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"mask": ("MASK",),
|
"mask": ("MASK",),
|
||||||
"invert": ("BOOLEAN", {"default": False}),
|
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"image": ("IMAGE",),
|
"image": ("IMAGE",),
|
||||||
@@ -62,7 +61,7 @@ class BboxFromMask:
|
|||||||
FUNCTION = "extract_bounding_box"
|
FUNCTION = "extract_bounding_box"
|
||||||
CATEGORY = "mtb/crop"
|
CATEGORY = "mtb/crop"
|
||||||
|
|
||||||
def extract_bounding_box(self, mask: torch.Tensor, invert: bool, image=None):
|
def extract_bounding_box(self, mask: torch.Tensor, image=None):
|
||||||
# if image != None:
|
# if image != None:
|
||||||
# if mask.size(0) != image.size(0):
|
# if mask.size(0) != image.size(0):
|
||||||
# if mask.size(0) != 1:
|
# if mask.size(0) != 1:
|
||||||
@@ -74,8 +73,9 @@ class BboxFromMask:
|
|||||||
# f"Batch count mismatch for mask and image, it can either be 1 mask for X images, or X masks for X images (mask: {mask.shape} | image: {image.shape})"
|
# f"Batch count mismatch for mask and image, it can either be 1 mask for X images, or X masks for X images (mask: {mask.shape} | image: {image.shape})"
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
_mask = tensor2pil(1.0 - mask)[0]
|
||||||
|
|
||||||
# we invert it
|
# we invert it
|
||||||
_mask = tensor2pil(1.0 - mask)[0] if invert else tensor2pil(mask)[0]
|
|
||||||
alpha_channel = np.array(_mask)
|
alpha_channel = np.array(_mask)
|
||||||
|
|
||||||
non_zero_indices = np.nonzero(alpha_channel)
|
non_zero_indices = np.nonzero(alpha_channel)
|
||||||
@@ -141,23 +141,19 @@ class Crop:
|
|||||||
self, image: torch.Tensor, mask=None, x=0, y=0, width=256, height=256, bbox=None
|
self, image: torch.Tensor, mask=None, x=0, y=0, width=256, height=256, bbox=None
|
||||||
):
|
):
|
||||||
image = image.numpy()
|
image = image.numpy()
|
||||||
if mask is not None:
|
if mask:
|
||||||
mask = mask.numpy()
|
mask = mask.numpy()
|
||||||
|
|
||||||
if bbox is not None:
|
if bbox != None:
|
||||||
x, y, width, height = bbox
|
x, y, width, height = bbox
|
||||||
|
|
||||||
cropped_image = image[:, y : y + height, x : x + width, :]
|
cropped_image = image[:, y : y + height, x : x + width, :]
|
||||||
cropped_mask = None
|
cropped_mask = mask[y : y + height, x : x + width] if mask != None else None
|
||||||
if mask is not None:
|
|
||||||
cropped_mask = (
|
|
||||||
mask[:, y : y + height, x : x + width] if mask is not None else None
|
|
||||||
)
|
|
||||||
crop_data = (x, y, width, height)
|
crop_data = (x, y, width, height)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
torch.from_numpy(cropped_image),
|
torch.from_numpy(cropped_image),
|
||||||
torch.from_numpy(cropped_mask) if cropped_mask is not None else None,
|
torch.from_numpy(cropped_mask) if mask != None else None,
|
||||||
crop_data,
|
crop_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
def deserialize_curve(curve):
|
|
||||||
if isinstance(curve, str):
|
|
||||||
curve = json.loads(curve)
|
|
||||||
return curve
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_curve(curve):
|
|
||||||
if not isinstance(curve, str):
|
|
||||||
curve = json.dumps(curve)
|
|
||||||
return curve
|
|
||||||
|
|
||||||
|
|
||||||
class MTB_Curve:
|
|
||||||
"""A basic FLOAT_CURVE input node."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(cls):
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"curve": ("FLOAT_CURVE",),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("FLOAT_CURVE",)
|
|
||||||
FUNCTION = "do_curve"
|
|
||||||
|
|
||||||
CATEGORY = "mtb/curve"
|
|
||||||
|
|
||||||
def do_curve(self, curve):
|
|
||||||
return (curve,)
|
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [MTB_Curve]
|
|
||||||
+9
-18
@@ -39,14 +39,11 @@ def process_list(anything):
|
|||||||
and isinstance(first_element[0], torch.Tensor)
|
and isinstance(first_element[0], torch.Tensor)
|
||||||
):
|
):
|
||||||
text.append(
|
text.append(
|
||||||
"List of List of Tensors: "
|
f"List of List of Tensors: {first_element[0].shape} (x{len(anything)})"
|
||||||
f"{first_element[0].shape} (x{len(anything)})"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif isinstance(first_element, torch.Tensor):
|
elif isinstance(first_element, torch.Tensor):
|
||||||
text.append(
|
text.append(f"List of Tensors: {first_element.shape} (x{len(anything)})")
|
||||||
f"List of Tensors: {first_element.shape} (x{len(anything)})"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"text": text}
|
return {"text": text}
|
||||||
|
|
||||||
@@ -54,9 +51,7 @@ def process_list(anything):
|
|||||||
def process_dict(anything):
|
def process_dict(anything):
|
||||||
text = []
|
text = []
|
||||||
if "samples" in anything:
|
if "samples" in anything:
|
||||||
is_empty = (
|
is_empty = "(empty)" if torch.count_nonzero(anything["samples"]) == 0 else ""
|
||||||
"(empty)" if torch.count_nonzero(anything["samples"]) == 0 else ""
|
|
||||||
)
|
|
||||||
text.append(f"Latent Samples: {anything['samples'].shape} {is_empty}")
|
text.append(f"Latent Samples: {anything['samples'].shape} {is_empty}")
|
||||||
|
|
||||||
return {"text": text}
|
return {"text": text}
|
||||||
@@ -74,10 +69,7 @@ def process_text(anything):
|
|||||||
|
|
||||||
|
|
||||||
class Debug:
|
class Debug:
|
||||||
"""Experimental node to debug any Comfy values.
|
"""Experimental node to debug any Comfy values, support for more types and widgets is planned"""
|
||||||
|
|
||||||
support for more types and widgets is planned.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -111,15 +103,15 @@ class Debug:
|
|||||||
|
|
||||||
for ui_key, ui_value in processed_data.items():
|
for ui_key, ui_value in processed_data.items():
|
||||||
output["ui"][ui_key].extend(ui_value)
|
output["ui"][ui_key].extend(ui_value)
|
||||||
|
# log.debug(
|
||||||
|
# f"Processed input {k}, found {len(processed_data.get('b64_images', []))} images and {len(processed_data.get('text', []))} text items."
|
||||||
|
# )
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
class SaveTensors:
|
class SaveTensors:
|
||||||
"""Save torch tensors (image, mask or latent) to disk.
|
"""Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy"""
|
||||||
|
|
||||||
useful to debug things outside comfy.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.output_dir = folder_paths.get_output_directory()
|
self.output_dir = folder_paths.get_output_directory()
|
||||||
@@ -174,8 +166,7 @@ class SaveTensors:
|
|||||||
torch.save(latent, full_output_folder / latent_file)
|
torch.save(latent, full_output_folder / latent_file)
|
||||||
# pickle.dump(latent, open(full_output_folder/ latent_file, "wb"))
|
# pickle.dump(latent, open(full_output_folder/ latent_file, "wb"))
|
||||||
|
|
||||||
# np.save(full_output_folder / latent_file,
|
# np.save(full_output_folder/ latent_file, latent[""].cpu().numpy())
|
||||||
# latent[""].cpu().numpy())
|
|
||||||
|
|
||||||
return f"{filename_prefix}_{counter:05}"
|
return f"{filename_prefix}_{counter:05}"
|
||||||
|
|
||||||
|
|||||||
+35
-70
@@ -8,13 +8,7 @@ from PIL import Image
|
|||||||
|
|
||||||
from ..errors import ModelNotFound
|
from ..errors import ModelNotFound
|
||||||
from ..log import mklog
|
from ..log import mklog
|
||||||
from ..utils import (
|
from ..utils import get_model_path, tensor2pil, tiles_infer, tiles_merge, tiles_split
|
||||||
get_model_path,
|
|
||||||
tensor2pil,
|
|
||||||
tiles_infer,
|
|
||||||
tiles_merge,
|
|
||||||
tiles_split,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Disable MS telemetry
|
# Disable MS telemetry
|
||||||
ort.disable_telemetry_events()
|
ort.disable_telemetry_events()
|
||||||
@@ -22,13 +16,9 @@ log = mklog(__name__)
|
|||||||
|
|
||||||
|
|
||||||
# - COLOR to NORMALS
|
# - COLOR to NORMALS
|
||||||
def color_to_normals(
|
def color_to_normals(color_img, overlap, progress_callback, save_temp=False):
|
||||||
color_img, overlap, progress_callback, *, save_temp=False
|
"""Computes a normal map from the given color map. 'color_img' must be a numpy array
|
||||||
):
|
in C,H,W format (with C as RGB). 'overlap' must be one of 'SMALL', 'MEDIUM', 'LARGE'.
|
||||||
"""Compute a normal map from the given color map.
|
|
||||||
|
|
||||||
'color_img' must be a numpy array in C,H,W format (with C as RGB).
|
|
||||||
'overlap' must be one of 'SMALL', 'MEDIUM', 'LARGE'.
|
|
||||||
"""
|
"""
|
||||||
temp_dir = Path(tempfile.mkdtemp()) if save_temp else None
|
temp_dir = Path(tempfile.mkdtemp()) if save_temp else None
|
||||||
|
|
||||||
@@ -41,8 +31,7 @@ def color_to_normals(
|
|||||||
)
|
)
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
"Converting color image to grayscale by taking "
|
f"Converting color image to grayscale by taking the mean over color channels: {img.shape}"
|
||||||
f"the mean over color channels: {img.shape}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Split image in tiles
|
# Split image in tiles
|
||||||
@@ -73,15 +62,13 @@ def color_to_normals(
|
|||||||
|
|
||||||
# Predict normal map for each tile
|
# Predict normal map for each tile
|
||||||
log.debug("DeepBump Color → Normals : generating")
|
log.debug("DeepBump Color → Normals : generating")
|
||||||
pred_tiles = tiles_infer(
|
pred_tiles = tiles_infer(tiles, ort_session, progress_callback=progress_callback)
|
||||||
tiles, ort_session, progress_callback=progress_callback
|
|
||||||
)
|
|
||||||
|
|
||||||
if temp_dir:
|
if temp_dir:
|
||||||
for i, pred_tile in enumerate(pred_tiles):
|
for i, pred_tile in enumerate(pred_tiles):
|
||||||
Image.fromarray(
|
Image.fromarray((pred_tile.transpose(1, 2, 0) * 255).astype(np.uint8)).save(
|
||||||
(pred_tile.transpose(1, 2, 0) * 255).astype(np.uint8)
|
temp_dir / f"pred_tile_{i}.png"
|
||||||
).save(temp_dir / f"pred_tile_{i}.png")
|
)
|
||||||
|
|
||||||
# Merge tiles
|
# Merge tiles
|
||||||
log.debug("DeepBump Color → Normals : merging")
|
log.debug("DeepBump Color → Normals : merging")
|
||||||
@@ -93,17 +80,17 @@ def color_to_normals(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if temp_dir:
|
if temp_dir:
|
||||||
Image.fromarray(
|
Image.fromarray((pred_img.transpose(1, 2, 0) * 255).astype(np.uint8)).save(
|
||||||
(pred_img.transpose(1, 2, 0) * 255).astype(np.uint8)
|
temp_dir / "merged_img.png"
|
||||||
).save(temp_dir / "merged_img.png")
|
)
|
||||||
|
|
||||||
# Normalize each pixel to unit vector
|
# Normalize each pixel to unit vector
|
||||||
pred_img = normalize(pred_img)
|
pred_img = normalize(pred_img)
|
||||||
|
|
||||||
if temp_dir:
|
if temp_dir:
|
||||||
Image.fromarray(
|
Image.fromarray((pred_img.transpose(1, 2, 0) * 255).astype(np.uint8)).save(
|
||||||
(pred_img.transpose(1, 2, 0) * 255).astype(np.uint8)
|
temp_dir / "final_img.png"
|
||||||
).save(temp_dir / "final_img.png")
|
)
|
||||||
|
|
||||||
log.debug(f"Debug images saved in {temp_dir}")
|
log.debug(f"Debug images saved in {temp_dir}")
|
||||||
|
|
||||||
@@ -112,47 +99,40 @@ def color_to_normals(
|
|||||||
|
|
||||||
# - NORMALS to CURVATURE
|
# - NORMALS to CURVATURE
|
||||||
def conv_1d(array, kernel_1d):
|
def conv_1d(array, kernel_1d):
|
||||||
"""Perform row by row 1D convolutions.
|
"""Performs row by row 1D convolutions of the given 2D image with the given 1D kernel."""
|
||||||
|
|
||||||
of the given 2D image with the given 1D kernel.
|
|
||||||
"""
|
|
||||||
# Input kernel length must be odd
|
# Input kernel length must be odd
|
||||||
k_l = len(kernel_1d)
|
k_l = len(kernel_1d)
|
||||||
|
|
||||||
assert k_l % 2 != 0
|
assert k_l % 2 != 0
|
||||||
# Convolution is repeat-padded
|
# Convolution is repeat-padded
|
||||||
extended = np.pad(array, k_l // 2, mode="wrap")
|
extended = np.pad(array, k_l // 2, mode="wrap")
|
||||||
# Output has same size as input (padded, valid-mode convolution)
|
# Output has same size as input (padded, valid-mode convolution)
|
||||||
output = np.empty(array.shape)
|
output = np.empty(array.shape)
|
||||||
for i in range(array.shape[0]):
|
for i in range(array.shape[0]):
|
||||||
output[i] = np.convolve(
|
output[i] = np.convolve(extended[i + (k_l // 2)], kernel_1d, mode="valid")
|
||||||
extended[i + (k_l // 2)], kernel_1d, mode="valid"
|
|
||||||
)
|
|
||||||
|
|
||||||
return output * -1
|
return output * -1
|
||||||
|
|
||||||
|
|
||||||
def gaussian_kernel(length, sigma):
|
def gaussian_kernel(length, sigma):
|
||||||
"""Return a 1D gaussian kernel of size 'length'."""
|
"""Returns a 1D gaussian kernel of size 'length'."""
|
||||||
|
|
||||||
space = np.linspace(-(length - 1) / 2, (length - 1) / 2, length)
|
space = np.linspace(-(length - 1) / 2, (length - 1) / 2, length)
|
||||||
kernel = np.exp(-0.5 * np.square(space) / np.square(sigma))
|
kernel = np.exp(-0.5 * np.square(space) / np.square(sigma))
|
||||||
return kernel / np.sum(kernel)
|
return kernel / np.sum(kernel)
|
||||||
|
|
||||||
|
|
||||||
def normalize(np_array):
|
def normalize(np_array):
|
||||||
"""Normalize all elements of the given numpy array to [0,1]."""
|
"""Normalize all elements of the given numpy array to [0,1]"""
|
||||||
return (np_array - np.min(np_array)) / (
|
|
||||||
np.max(np_array) - np.min(np_array)
|
return (np_array - np.min(np_array)) / (np.max(np_array) - np.min(np_array))
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def normals_to_curvature(normals_img, blur_radius, progress_callback):
|
def normals_to_curvature(normals_img, blur_radius, progress_callback):
|
||||||
"""Compute a curvature map from the given normal map.
|
"""Computes a curvature map from the given normal map. 'normals_img' must be a numpy array
|
||||||
|
in C,H,W format (with C as RGB). 'blur_radius' must be one of 'SMALLEST', 'SMALLER', 'SMALL',
|
||||||
|
'MEDIUM', 'LARGE', 'LARGER', 'LARGEST'."""
|
||||||
|
|
||||||
'normals_img' must be a numpy array in C,H,W format (with C as RGB).
|
|
||||||
'blur_radius' must be one of:
|
|
||||||
'SMALLEST', 'SMALLER', 'SMALL', 'MEDIUM', 'LARGE', 'LARGER', 'LARGEST'.
|
|
||||||
"""
|
|
||||||
# Convolutions on normal map red & green channels
|
# Convolutions on normal map red & green channels
|
||||||
if progress_callback is not None:
|
if progress_callback is not None:
|
||||||
progress_callback(0, 4)
|
progress_callback(0, 4)
|
||||||
@@ -177,12 +157,8 @@ def normals_to_curvature(normals_img, blur_radius, progress_callback):
|
|||||||
"LARGER": 1 / 8,
|
"LARGER": 1 / 8,
|
||||||
"LARGEST": 1 / 4,
|
"LARGEST": 1 / 4,
|
||||||
}
|
}
|
||||||
if blur_radius not in blur_factors:
|
assert blur_radius in blur_factors
|
||||||
raise ValueError(f"{blur_radius} not found in {blur_factors}")
|
blur_radius_px = int(np.mean(normals_img.shape[1:3]) * blur_factors[blur_radius])
|
||||||
|
|
||||||
blur_radius_px = int(
|
|
||||||
np.mean(normals_img.shape[1:3]) * blur_factors[blur_radius]
|
|
||||||
)
|
|
||||||
|
|
||||||
# If blur radius too small, do not blur
|
# If blur radius too small, do not blur
|
||||||
if blur_radius_px < 2:
|
if blur_radius_px < 2:
|
||||||
@@ -219,9 +195,8 @@ def normals_to_grad(normals_img):
|
|||||||
|
|
||||||
def copy_flip(grad_x, grad_y):
|
def copy_flip(grad_x, grad_y):
|
||||||
"""Concat 4 flipped copies of input gradients (makes them wrap).
|
"""Concat 4 flipped copies of input gradients (makes them wrap).
|
||||||
|
Output is twice bigger in both dimensions."""
|
||||||
|
|
||||||
Output is twice bigger in both dimensions.
|
|
||||||
"""
|
|
||||||
grad_x_top = np.hstack([grad_x, -np.flip(grad_x, axis=1)])
|
grad_x_top = np.hstack([grad_x, -np.flip(grad_x, axis=1)])
|
||||||
grad_x_bottom = np.hstack([np.flip(grad_x, axis=0), -np.flip(grad_x)])
|
grad_x_bottom = np.hstack([np.flip(grad_x, axis=0), -np.flip(grad_x)])
|
||||||
new_grad_x = np.vstack([grad_x_top, grad_x_bottom])
|
new_grad_x = np.vstack([grad_x_top, grad_x_bottom])
|
||||||
@@ -235,6 +210,7 @@ def copy_flip(grad_x, grad_y):
|
|||||||
|
|
||||||
def frankot_chellappa(grad_x, grad_y, progress_callback=None):
|
def frankot_chellappa(grad_x, grad_y, progress_callback=None):
|
||||||
"""Frankot-Chellappa depth-from-gradient algorithm."""
|
"""Frankot-Chellappa depth-from-gradient algorithm."""
|
||||||
|
|
||||||
if progress_callback is not None:
|
if progress_callback is not None:
|
||||||
progress_callback(0, 3)
|
progress_callback(0, 3)
|
||||||
|
|
||||||
@@ -274,8 +250,8 @@ def frankot_chellappa(grad_x, grad_y, progress_callback=None):
|
|||||||
def normals_to_height(normals_img, seamless, progress_callback):
|
def normals_to_height(normals_img, seamless, progress_callback):
|
||||||
"""Computes a height map from the given normal map. 'normals_img' must be a numpy array
|
"""Computes a height map from the given normal map. 'normals_img' must be a numpy array
|
||||||
in C,H,W format (with C as RGB). 'seamless' is a bool that should indicates if 'normals_img'
|
in C,H,W format (with C as RGB). 'seamless' is a bool that should indicates if 'normals_img'
|
||||||
is seamless.
|
is seamless."""
|
||||||
"""
|
|
||||||
# Flip height axis
|
# Flip height axis
|
||||||
flip_img = np.flip(normals_img, axis=1)
|
flip_img = np.flip(normals_img, axis=1)
|
||||||
|
|
||||||
@@ -289,9 +265,7 @@ def normals_to_height(normals_img, seamless, progress_callback):
|
|||||||
grad_x, grad_y = copy_flip(grad_x, grad_y)
|
grad_x, grad_y = copy_flip(grad_x, grad_y)
|
||||||
|
|
||||||
# Compute height
|
# Compute height
|
||||||
pred_img = frankot_chellappa(
|
pred_img = frankot_chellappa(-grad_x, grad_y, progress_callback=progress_callback)
|
||||||
-grad_x, grad_y, progress_callback=progress_callback
|
|
||||||
)
|
|
||||||
|
|
||||||
# Cut to valid part if gradients were expanded
|
# Cut to valid part if gradients were expanded
|
||||||
if not seamless:
|
if not seamless:
|
||||||
@@ -312,11 +286,7 @@ class DeepBump:
|
|||||||
"required": {
|
"required": {
|
||||||
"image": ("IMAGE",),
|
"image": ("IMAGE",),
|
||||||
"mode": (
|
"mode": (
|
||||||
[
|
["Color to Normals", "Normals to Curvature", "Normals to Height"],
|
||||||
"Color to Normals",
|
|
||||||
"Normals to Curvature",
|
|
||||||
"Normals to Height",
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
"color_to_normals_overlap": (["SMALL", "MEDIUM", "LARGE"],),
|
"color_to_normals_overlap": (["SMALL", "MEDIUM", "LARGE"],),
|
||||||
"normals_to_curvature_blur_radius": (
|
"normals_to_curvature_blur_radius": (
|
||||||
@@ -341,7 +311,6 @@ class DeepBump:
|
|||||||
|
|
||||||
def apply(
|
def apply(
|
||||||
self,
|
self,
|
||||||
*,
|
|
||||||
image,
|
image,
|
||||||
mode="Color to Normals",
|
mode="Color to Normals",
|
||||||
color_to_normals_overlap="SMALL",
|
color_to_normals_overlap="SMALL",
|
||||||
@@ -360,17 +329,13 @@ class DeepBump:
|
|||||||
|
|
||||||
# Apply processing
|
# Apply processing
|
||||||
if mode == "Color to Normals":
|
if mode == "Color to Normals":
|
||||||
out_img = color_to_normals(
|
out_img = color_to_normals(in_img, color_to_normals_overlap, None)
|
||||||
in_img, color_to_normals_overlap, None
|
|
||||||
)
|
|
||||||
if mode == "Normals to Curvature":
|
if mode == "Normals to Curvature":
|
||||||
out_img = normals_to_curvature(
|
out_img = normals_to_curvature(
|
||||||
in_img, normals_to_curvature_blur_radius, None
|
in_img, normals_to_curvature_blur_radius, None
|
||||||
)
|
)
|
||||||
if mode == "Normals to Height":
|
if mode == "Normals to Height":
|
||||||
out_img = normals_to_height(
|
out_img = normals_to_height(in_img, normals_to_height_seamless, None)
|
||||||
in_img, normals_to_height_seamless, None
|
|
||||||
)
|
|
||||||
|
|
||||||
if out_img is not None:
|
if out_img is not None:
|
||||||
log.debug(f"Output image shape: {out_img.shape}")
|
log.debug(f"Output image shape: {out_img.shape}")
|
||||||
|
|||||||
+12
-29
@@ -40,12 +40,12 @@ class LoadFaceEnhanceModel:
|
|||||||
log.warning("Face restoration models not found.")
|
log.warning("Face restoration models not found.")
|
||||||
return []
|
return []
|
||||||
if not fr_models_path.exists():
|
if not fr_models_path.exists():
|
||||||
# log.warning(
|
log.warning(
|
||||||
# f"No Face Restore checkpoints found at {fr_models_path} (if you've used mtb before these checkpoints were saved in upscale_models before)"
|
f"No Face Restore checkpoints found at {fr_models_path} (if you've used mtb before these checkpoints were saved in upscale_models before)"
|
||||||
# )
|
)
|
||||||
# log.warning(
|
log.warning(
|
||||||
# "For now we fallback to upscale_models but this will be removed in a future version"
|
"For now we fallback to upscale_models but this will be removed in a future version"
|
||||||
# )
|
)
|
||||||
if um_models_path.exists():
|
if um_models_path.exists():
|
||||||
return [
|
return [
|
||||||
x
|
x
|
||||||
@@ -98,9 +98,7 @@ class LoadFaceEnhanceModel:
|
|||||||
(fr_root if fr_root.exists() else um_root) / model_name
|
(fr_root if fr_root.exists() else um_root) / model_name
|
||||||
).as_posix(),
|
).as_posix(),
|
||||||
upscale=upscale,
|
upscale=upscale,
|
||||||
arch="clean"
|
arch="clean" if basic else "RestoreFormer", # or original for v1.0 only
|
||||||
if basic
|
|
||||||
else "RestoreFormer", # or original for v1.0 only
|
|
||||||
channel_multiplier=2, # 1 for v1.0 only
|
channel_multiplier=2, # 1 for v1.0 only
|
||||||
bg_upsampler=bg_upsampler,
|
bg_upsampler=bg_upsampler,
|
||||||
)
|
)
|
||||||
@@ -124,11 +122,7 @@ class BGUpscaleWrapper:
|
|||||||
imgt = imgt.movedim(-1, -3).to(device)
|
imgt = imgt.movedim(-1, -3).to(device)
|
||||||
|
|
||||||
steps = imgt.shape[0] * comfy.utils.get_tiled_scale_steps(
|
steps = imgt.shape[0] * comfy.utils.get_tiled_scale_steps(
|
||||||
imgt.shape[3],
|
imgt.shape[3], imgt.shape[2], tile_x=tile, tile_y=tile, overlap=overlap
|
||||||
imgt.shape[2],
|
|
||||||
tile_x=tile,
|
|
||||||
tile_y=tile,
|
|
||||||
overlap=overlap,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
log.debug(f"Steps: {steps}")
|
log.debug(f"Steps: {steps}")
|
||||||
@@ -205,14 +199,10 @@ class RestoreFace:
|
|||||||
log.warning(f"Weight value has no effect for now. (value: {weight})")
|
log.warning(f"Weight value has no effect for now. (value: {weight})")
|
||||||
|
|
||||||
if save_tmp_steps:
|
if save_tmp_steps:
|
||||||
self.save_intermediate_images(
|
self.save_intermediate_images(cropped_faces, restored_faces, height, width)
|
||||||
cropped_faces, restored_faces, height, width
|
|
||||||
)
|
|
||||||
output = None
|
output = None
|
||||||
if restored_img is not None:
|
if restored_img is not None:
|
||||||
output = Image.fromarray(
|
output = Image.fromarray(cv2.cvtColor(restored_img, cv2.COLOR_BGR2RGB))
|
||||||
cv2.cvtColor(restored_img, cv2.COLOR_BGR2RGB)
|
|
||||||
)
|
|
||||||
# imwrite(restored_img, save_restore_path)
|
# imwrite(restored_img, save_restore_path)
|
||||||
|
|
||||||
return pil2tensor(output)
|
return pil2tensor(output)
|
||||||
@@ -228,12 +218,7 @@ class RestoreFace:
|
|||||||
) -> Tuple[torch.Tensor]:
|
) -> Tuple[torch.Tensor]:
|
||||||
out = [
|
out = [
|
||||||
self.do_restore(
|
self.do_restore(
|
||||||
image[i],
|
image[i], model, aligned, only_center_face, weight, save_tmp_steps
|
||||||
model,
|
|
||||||
aligned,
|
|
||||||
only_center_face,
|
|
||||||
weight,
|
|
||||||
save_tmp_steps,
|
|
||||||
)
|
)
|
||||||
for i in range(image.size(0))
|
for i in range(image.size(0))
|
||||||
]
|
]
|
||||||
@@ -255,9 +240,7 @@ class RestoreFace:
|
|||||||
|
|
||||||
return os.path.join(full_output_folder, file)
|
return os.path.join(full_output_folder, file)
|
||||||
|
|
||||||
def save_intermediate_images(
|
def save_intermediate_images(self, cropped_faces, restored_faces, height, width):
|
||||||
self, cropped_faces, restored_faces, height, width
|
|
||||||
):
|
|
||||||
for idx, (cropped_face, restored_face) in enumerate(
|
for idx, (cropped_face, restored_face) in enumerate(
|
||||||
zip(cropped_faces, restored_faces)
|
zip(cropped_faces, restored_faces)
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import torch
|
|
||||||
|
|
||||||
|
|
||||||
class MTB_FilterZ:
|
|
||||||
"""Filters an image based on a depth map"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(cls):
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"image": ("IMAGE",),
|
|
||||||
"depth": ("IMAGE",),
|
|
||||||
"to_black": ("BOOLEAN", {"default": True}),
|
|
||||||
"threshold": (
|
|
||||||
"FLOAT",
|
|
||||||
{"default": 0.5, "step": 0.01, "min": 0.0, "max": 1.0},
|
|
||||||
),
|
|
||||||
"invert": ("BOOLEAN", {"default": True}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
FUNCTION = "filter"
|
|
||||||
CATEGORY = "mtb/filters"
|
|
||||||
|
|
||||||
def filter(
|
|
||||||
self,
|
|
||||||
image: torch.Tensor,
|
|
||||||
depth: torch.Tensor,
|
|
||||||
to_black,
|
|
||||||
threshold: float,
|
|
||||||
invert,
|
|
||||||
):
|
|
||||||
# Normalize depth map to be in range [0, 1]
|
|
||||||
depth_normalized = (depth - depth.min()) / (depth.max() - depth.min())
|
|
||||||
|
|
||||||
# Calculate the difference from the threshold
|
|
||||||
diff_from_threshold = torch.abs(depth_normalized - threshold)
|
|
||||||
|
|
||||||
out_img = None
|
|
||||||
|
|
||||||
if to_black:
|
|
||||||
if invert:
|
|
||||||
soft_mask = diff_from_threshold >= threshold
|
|
||||||
else:
|
|
||||||
soft_mask = diff_from_threshold <= threshold
|
|
||||||
|
|
||||||
out_img = image.clone()
|
|
||||||
out_img[soft_mask] = 0
|
|
||||||
return (out_img,)
|
|
||||||
else:
|
|
||||||
alpha_channel = 1 - diff_from_threshold / threshold
|
|
||||||
alpha_channel = torch.clamp(alpha_channel, 0, 1)
|
|
||||||
|
|
||||||
if invert:
|
|
||||||
# Invert the alpha channel
|
|
||||||
alpha_channel = 1 - alpha_channel
|
|
||||||
|
|
||||||
# Ensure alpha_channel has the correct shape
|
|
||||||
# It should have the shape [batch_size, height, width, 1]
|
|
||||||
alpha_channel = alpha_channel.unsqueeze(-1)
|
|
||||||
|
|
||||||
# Combine RGB channels with alpha channel
|
|
||||||
out_img = torch.cat((image, alpha_channel), dim=-1)
|
|
||||||
|
|
||||||
return (out_img,)
|
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [MTB_FilterZ]
|
|
||||||
+48
-126
@@ -1,8 +1,9 @@
|
|||||||
import qrcode
|
import qrcode
|
||||||
|
from ..utils import pil2tensor
|
||||||
|
from ..utils import comfy_dir
|
||||||
|
from typing import cast
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ..log import log
|
from ..log import log
|
||||||
from ..utils import comfy_dir, font_path, pil2tensor
|
|
||||||
|
|
||||||
# class MtbExamples:
|
# class MtbExamples:
|
||||||
# """MTB Example Images"""
|
# """MTB Example Images"""
|
||||||
@@ -59,18 +60,9 @@ class UnsplashImage:
|
|||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"width": (
|
"width": ("INT", {"default": 512, "max": 8096, "min": 0, "step": 1}),
|
||||||
"INT",
|
"height": ("INT", {"default": 512, "max": 8096, "min": 0, "step": 1}),
|
||||||
{"default": 512, "max": 8096, "min": 0, "step": 1},
|
"random_seed": ("INT", {"default": 0, "max": 1e5, "min": 0, "step": 1}),
|
||||||
),
|
|
||||||
"height": (
|
|
||||||
"INT",
|
|
||||||
{"default": 512, "max": 8096, "min": 0, "step": 1},
|
|
||||||
),
|
|
||||||
"random_seed": (
|
|
||||||
"INT",
|
|
||||||
{"default": 0, "max": 1e5, "min": 0, "step": 1},
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"keyword": ("STRING", {"default": "nature"}),
|
"keyword": ("STRING", {"default": "nature"}),
|
||||||
@@ -82,9 +74,8 @@ class UnsplashImage:
|
|||||||
CATEGORY = "mtb/generate"
|
CATEGORY = "mtb/generate"
|
||||||
|
|
||||||
def do_unsplash_image(self, width, height, random_seed, keyword=None):
|
def do_unsplash_image(self, width, height, random_seed, keyword=None):
|
||||||
import io
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import io
|
||||||
|
|
||||||
base_url = "https://source.unsplash.com/random/"
|
base_url = "https://source.unsplash.com/random/"
|
||||||
|
|
||||||
@@ -130,14 +121,8 @@ class QrCode:
|
|||||||
{"default": 256, "max": 8096, "min": 0, "step": 1},
|
{"default": 256, "max": 8096, "min": 0, "step": 1},
|
||||||
),
|
),
|
||||||
"error_correct": (("L", "M", "Q", "H"), {"default": "L"}),
|
"error_correct": (("L", "M", "Q", "H"), {"default": "L"}),
|
||||||
"box_size": (
|
"box_size": ("INT", {"default": 10, "max": 8096, "min": 0, "step": 1}),
|
||||||
"INT",
|
"border": ("INT", {"default": 4, "max": 8096, "min": 0, "step": 1}),
|
||||||
{"default": 10, "max": 8096, "min": 0, "step": 1},
|
|
||||||
),
|
|
||||||
"border": (
|
|
||||||
"INT",
|
|
||||||
{"default": 4, "max": 8096, "min": 0, "step": 1},
|
|
||||||
),
|
|
||||||
"invert": (("BOOLEAN",), {"default": False}),
|
"invert": (("BOOLEAN",), {"default": False}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,9 +131,7 @@ class QrCode:
|
|||||||
FUNCTION = "do_qr"
|
FUNCTION = "do_qr"
|
||||||
CATEGORY = "mtb/generate"
|
CATEGORY = "mtb/generate"
|
||||||
|
|
||||||
def do_qr(
|
def do_qr(self, url, width, height, error_correct, box_size, border, invert):
|
||||||
self, url, width, height, error_correct, box_size, border, invert
|
|
||||||
):
|
|
||||||
log.warning(
|
log.warning(
|
||||||
"This node will soon be deprecated, there are much better alternatives like https://github.com/coreyryanhanson/comfy-qr"
|
"This node will soon be deprecated, there are much better alternatives like https://github.com/coreyryanhanson/comfy-qr"
|
||||||
)
|
)
|
||||||
@@ -173,9 +156,7 @@ class QrCode:
|
|||||||
back_color = (255, 255, 255) if invert else (0, 0, 0)
|
back_color = (255, 255, 255) if invert else (0, 0, 0)
|
||||||
fill_color = (0, 0, 0) if invert else (255, 255, 255)
|
fill_color = (0, 0, 0) if invert else (255, 255, 255)
|
||||||
|
|
||||||
code = img = qr.make_image(
|
code = img = qr.make_image(back_color=back_color, fill_color=fill_color)
|
||||||
back_color=back_color, fill_color=fill_color
|
|
||||||
)
|
|
||||||
|
|
||||||
# that we now resize without filtering
|
# that we now resize without filtering
|
||||||
code = code.resize((width, height), Image.NEAREST)
|
code = code.resize((width, height), Image.NEAREST)
|
||||||
@@ -190,11 +171,9 @@ def bbox_dim(bbox):
|
|||||||
return width, height
|
return width, height
|
||||||
|
|
||||||
|
|
||||||
# TODO: Auto install the base font to ComfyUI/fonts
|
|
||||||
|
|
||||||
|
|
||||||
class TextToImage:
|
class TextToImage:
|
||||||
"""Utils to convert text to image using a font.
|
"""Utils to convert text to image using a font
|
||||||
|
|
||||||
|
|
||||||
The tool looks for any .ttf file in the Comfy folder hierarchy.
|
The tool looks for any .ttf file in the Comfy folder hierarchy.
|
||||||
"""
|
"""
|
||||||
@@ -202,27 +181,27 @@ class TextToImage:
|
|||||||
fonts = {}
|
fonts = {}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# - This is executed when the graph is executed,
|
# - This is executed when the graph is executed, we could conditionaly reload fonts there
|
||||||
# - we could conditionaly reload fonts there
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def CACHE_FONTS(cls):
|
def CACHE_FONTS(cls):
|
||||||
font_extensions = ["*.ttf", "*.otf", "*.woff", "*.woff2", "*.eot"]
|
font_extensions = ["*.ttf", "*.otf", "*.woff", "*.woff2", "*.eot"]
|
||||||
fonts = [font_path]
|
fonts = []
|
||||||
|
|
||||||
for extension in font_extensions:
|
for extension in font_extensions:
|
||||||
try:
|
fonts.extend(comfy_dir.glob(f"**/{extension}"))
|
||||||
if comfy_dir.exists():
|
|
||||||
fonts.extend(comfy_dir.glob(f"fonts/**/{extension}"))
|
if not fonts:
|
||||||
else:
|
log.warn(
|
||||||
log.warn(f"Directory {comfy_dir} does not exist.")
|
"> No fonts found in the comfy folder, place at least one font file somewhere in ComfyUI's hierarchy"
|
||||||
except Exception as e:
|
)
|
||||||
log.error(f"Error during font caching: {e}")
|
else:
|
||||||
|
log.debug(f"> Found {len(fonts)} fonts")
|
||||||
|
|
||||||
for font in fonts:
|
for font in fonts:
|
||||||
log.debug(f"Adding font {font}")
|
log.debug(f"Adding font {font}")
|
||||||
TextToImage.fonts[font.stem] = font.as_posix()
|
cls.fonts[font.stem] = font.as_posix()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -237,15 +216,13 @@ class TextToImage:
|
|||||||
{"default": "Hello world!"},
|
{"default": "Hello world!"},
|
||||||
),
|
),
|
||||||
"font": ((sorted(cls.fonts.keys())),),
|
"font": ((sorted(cls.fonts.keys())),),
|
||||||
"wrap": ("BOOLEAN", {"default": True}),
|
"wrap": (
|
||||||
"trim": ("BOOLEAN", {"default": True}),
|
"INT",
|
||||||
"line_height": (
|
{"default": 120, "min": 0, "max": 8096, "step": 1},
|
||||||
"FLOAT",
|
|
||||||
{"default": 1.0, "min": 0, "step": 0.1},
|
|
||||||
),
|
),
|
||||||
"font_size": (
|
"font_size": (
|
||||||
"INT",
|
"INT",
|
||||||
{"default": 32, "min": 1, "max": 2500, "step": 1},
|
{"default": 12, "min": 1, "max": 2500, "step": 1},
|
||||||
),
|
),
|
||||||
"width": (
|
"width": (
|
||||||
"INT",
|
"INT",
|
||||||
@@ -255,6 +232,7 @@ class TextToImage:
|
|||||||
"INT",
|
"INT",
|
||||||
{"default": 512, "min": 1, "max": 8096, "step": 1},
|
{"default": 512, "min": 1, "max": 8096, "step": 1},
|
||||||
),
|
),
|
||||||
|
# "position": (["INT"], {"default": 0, "min": 0, "max": 100, "step": 1}),
|
||||||
"color": (
|
"color": (
|
||||||
"COLOR",
|
"COLOR",
|
||||||
{"default": "black"},
|
{"default": "black"},
|
||||||
@@ -263,20 +241,6 @@ class TextToImage:
|
|||||||
"COLOR",
|
"COLOR",
|
||||||
{"default": "white"},
|
{"default": "white"},
|
||||||
),
|
),
|
||||||
"h_align": (("left", "center", "right"), {"default": "left"}),
|
|
||||||
"v_align": (("top", "center", "bottom"), {"default": "top"}),
|
|
||||||
"h_offset": (
|
|
||||||
"INT",
|
|
||||||
{"default": 0, "min": 0, "max": 8096, "step": 1},
|
|
||||||
),
|
|
||||||
"v_offset": (
|
|
||||||
"INT",
|
|
||||||
{"default": 0, "min": 0, "max": 8096, "step": 1},
|
|
||||||
),
|
|
||||||
"h_coverage": (
|
|
||||||
"INT",
|
|
||||||
{"default": 100, "min": 1, "max": 100, "step": 1},
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,79 +250,37 @@ class TextToImage:
|
|||||||
CATEGORY = "mtb/generate"
|
CATEGORY = "mtb/generate"
|
||||||
|
|
||||||
def text_to_image(
|
def text_to_image(
|
||||||
self,
|
self, text, font, wrap, font_size, width, height, color, background
|
||||||
text: str,
|
|
||||||
font,
|
|
||||||
wrap,
|
|
||||||
trim,
|
|
||||||
line_height,
|
|
||||||
font_size,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
color,
|
|
||||||
background,
|
|
||||||
h_align="left",
|
|
||||||
v_align="top",
|
|
||||||
h_offset=0,
|
|
||||||
v_offset=0,
|
|
||||||
h_coverage=100,
|
|
||||||
):
|
):
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
font = self.fonts[font]
|
||||||
|
font = cast(ImageFont.FreeTypeFont, ImageFont.truetype(font, font_size))
|
||||||
font_path = self.fonts[font]
|
if wrap == 0:
|
||||||
|
wrap = width / font_size
|
||||||
text = (
|
lines = textwrap.wrap(text, width=wrap)
|
||||||
text.encode("ascii", "ignore").decode().strip() if trim else text
|
|
||||||
)
|
|
||||||
# Handle word wrapping
|
|
||||||
if wrap:
|
|
||||||
wrap_width = (((width / 100) * h_coverage) / font_size) * 2
|
|
||||||
lines = textwrap.wrap(text, width=wrap_width)
|
|
||||||
else:
|
|
||||||
lines = [text]
|
|
||||||
font = ImageFont.truetype(font_path, size=font_size)
|
|
||||||
log.debug(f"Lines: {lines}")
|
log.debug(f"Lines: {lines}")
|
||||||
img = Image.new("RGBA", (width, height), background)
|
line_height = bbox_dim(font.getbbox("hg"))[1]
|
||||||
|
img_height = height # line_height * len(lines)
|
||||||
|
img_width = width # max(font.getsize(line)[0] for line in lines)
|
||||||
|
|
||||||
|
img = Image.new("RGBA", (img_width, img_height), background)
|
||||||
draw = ImageDraw.Draw(img)
|
draw = ImageDraw.Draw(img)
|
||||||
|
y_text = 0
|
||||||
line_height_px = line_height * font_size
|
# - bbox is [left, upper, right, lower]
|
||||||
|
|
||||||
# Vertical alignment
|
|
||||||
if v_align == "top":
|
|
||||||
y_text = v_offset
|
|
||||||
elif v_align == "center":
|
|
||||||
y_text = ((height - (line_height_px * len(lines))) // 2) + v_offset
|
|
||||||
else: # bottom
|
|
||||||
y_text = (height - (line_height_px * len(lines))) - v_offset
|
|
||||||
|
|
||||||
def get_width(line):
|
|
||||||
if hasattr(font, "getsize"):
|
|
||||||
return font.getsize(line)[0]
|
|
||||||
else:
|
|
||||||
return font.getlength(line)
|
|
||||||
|
|
||||||
# Draw each line of text
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line_width = get_width(line)
|
width, height = bbox_dim(font.getbbox(line))
|
||||||
# Horizontal alignment
|
draw.text((0, y_text), line, color, font=font)
|
||||||
if h_align == "left":
|
y_text += height
|
||||||
x_text = h_offset
|
|
||||||
elif h_align == "center":
|
|
||||||
x_text = ((width - line_width) // 2) + h_offset
|
|
||||||
else: # right
|
|
||||||
x_text = (width - line_width) - h_offset
|
|
||||||
|
|
||||||
draw.text((x_text, y_text), line, fill=color, font=font)
|
|
||||||
y_text += line_height_px
|
|
||||||
|
|
||||||
|
# img.save(os.path.join(folder_paths.base_path, f'{str(uuid.uuid4())}.png'))
|
||||||
return (pil2tensor(img),)
|
return (pil2tensor(img),)
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [
|
__nodes__ = [
|
||||||
QrCode,
|
QrCode,
|
||||||
UnsplashImage,
|
UnsplashImage,
|
||||||
TextToImage,
|
TextToImage
|
||||||
# MtbExamples,
|
# MtbExamples,
|
||||||
]
|
]
|
||||||
|
|||||||
+12
-93
@@ -1,5 +1,7 @@
|
|||||||
from typing import Optional
|
import io
|
||||||
import io, json, urllib.parse, urllib.request
|
import json
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
@@ -23,82 +25,10 @@ def get_image(filename, subfolder, folder_type):
|
|||||||
return io.BytesIO(response.read())
|
return io.BytesIO(response.read())
|
||||||
|
|
||||||
|
|
||||||
class MTB_ToDevice:
|
|
||||||
"""Send a image or mask tensor to the given device."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(cls):
|
|
||||||
devices = ["cpu"]
|
|
||||||
if torch.backends.mps.is_available():
|
|
||||||
devices.append("mps")
|
|
||||||
if torch.cuda.is_available():
|
|
||||||
for i in range(torch.cuda.device_count()):
|
|
||||||
devices.append(f"cuda{i}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"ignore_errors": ("BOOLEAN", {"default": False}),
|
|
||||||
"device": (devices, {"default": "cpu"}),
|
|
||||||
},
|
|
||||||
"optional": {
|
|
||||||
"image": ("IMAGE",),
|
|
||||||
"mask": ("MASK",),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE", "MASK")
|
|
||||||
RETURN_NAMES = ("images", "masks")
|
|
||||||
CATEGORY = "mtb/utils"
|
|
||||||
FUNCTION = "to_device"
|
|
||||||
|
|
||||||
def to_device(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
ignore_errors=False,
|
|
||||||
device="cuda",
|
|
||||||
image: Optional[torch.Tensor] = None,
|
|
||||||
mask: Optional[torch.Tensor] = None,
|
|
||||||
):
|
|
||||||
if not ignore_errors and image is None and mask is None:
|
|
||||||
raise ValueError(
|
|
||||||
"You must either provide an image or a mask,"
|
|
||||||
" use ignore_error to passthrough"
|
|
||||||
)
|
|
||||||
if image is not None:
|
|
||||||
image = image.to(device)
|
|
||||||
if mask is not None:
|
|
||||||
mask = mask.to(device)
|
|
||||||
return (image, mask)
|
|
||||||
|
|
||||||
|
|
||||||
# class MTB_ApplyTextTemplate:
|
|
||||||
class MTB_ApplyTextTemplate:
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(cls):
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"template": ("STRING", {"default": "", "multiline": True}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("STRING",)
|
|
||||||
RETURN_NAMES = ("string",)
|
|
||||||
CATEGORY = "mtb/utils"
|
|
||||||
FUNCTION = "execute"
|
|
||||||
|
|
||||||
def execute(self, *, template: str, **kwargs):
|
|
||||||
res = f"{template}"
|
|
||||||
for k, v in kwargs.items():
|
|
||||||
res = res.replace(f"{{{k}}}", f"{v}")
|
|
||||||
|
|
||||||
return (res,)
|
|
||||||
|
|
||||||
|
|
||||||
class GetBatchFromHistory:
|
class GetBatchFromHistory:
|
||||||
"""Very experimental node to load images from the history of the server.
|
"""Very experimental node to load images from the history of the server.
|
||||||
|
|
||||||
Queue items without output are ignored in the count.
|
Queue items without output are ignored in the count."""
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -121,7 +51,6 @@ class GetBatchFromHistory:
|
|||||||
|
|
||||||
def load_from_history(
|
def load_from_history(
|
||||||
self,
|
self,
|
||||||
*,
|
|
||||||
enable=True,
|
enable=True,
|
||||||
count=0,
|
count=0,
|
||||||
offset=0,
|
offset=0,
|
||||||
@@ -159,9 +88,7 @@ class GetBatchFromHistory:
|
|||||||
if "images" in node_output:
|
if "images" in node_output:
|
||||||
for image in node_output["images"]:
|
for image in node_output["images"]:
|
||||||
image_data = get_image(
|
image_data = get_image(
|
||||||
image["filename"],
|
image["filename"], image["subfolder"], image["type"]
|
||||||
image["subfolder"],
|
|
||||||
image["type"],
|
|
||||||
)
|
)
|
||||||
output_images.append(image_data)
|
output_images.append(image_data)
|
||||||
|
|
||||||
@@ -184,7 +111,7 @@ class GetBatchFromHistory:
|
|||||||
|
|
||||||
|
|
||||||
class AnyToString:
|
class AnyToString:
|
||||||
"""Tries to take any input and convert it to a string."""
|
"""Tries to take any input and convert it to a string"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -204,14 +131,10 @@ class AnyToString:
|
|||||||
elif isinstance(input, Image.Image):
|
elif isinstance(input, Image.Image):
|
||||||
return (f"PIL Image of size {input.size} and mode {input.mode}",)
|
return (f"PIL Image of size {input.size} and mode {input.mode}",)
|
||||||
elif isinstance(input, np.ndarray):
|
elif isinstance(input, np.ndarray):
|
||||||
return (
|
return (f"Numpy array of shape {input.shape} and dtype {input.dtype}",)
|
||||||
f"Numpy array of shape {input.shape} and dtype {input.dtype}",
|
|
||||||
)
|
|
||||||
|
|
||||||
elif isinstance(input, dict):
|
elif isinstance(input, dict):
|
||||||
return (
|
return (f"Dictionary of {len(input)} items, with keys {input.keys()}",)
|
||||||
f"Dictionary of {len(input)} items, with keys {input.keys()}",
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.debug(f"Falling back to string conversion of {input}")
|
log.debug(f"Falling back to string conversion of {input}")
|
||||||
@@ -219,7 +142,7 @@ class AnyToString:
|
|||||||
|
|
||||||
|
|
||||||
class StringReplace:
|
class StringReplace:
|
||||||
"""Basic string replacement."""
|
"""Basic string replacement"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -262,9 +185,7 @@ class MTB_MathExpression:
|
|||||||
RETURN_TYPES = ("FLOAT", "INT")
|
RETURN_TYPES = ("FLOAT", "INT")
|
||||||
RETURN_NAMES = ("result (float)", "result (int)")
|
RETURN_NAMES = ("result (float)", "result (int)")
|
||||||
CATEGORY = "mtb/math"
|
CATEGORY = "mtb/math"
|
||||||
DESCRIPTION = (
|
DESCRIPTION = "evaluate a simple math expression string (!! Fallsback to eval)"
|
||||||
"evaluate a simple math expression string (!! Fallsback to eval)"
|
|
||||||
)
|
|
||||||
|
|
||||||
def eval_expression(self, expression, **kwargs):
|
def eval_expression(self, expression, **kwargs):
|
||||||
import math
|
import math
|
||||||
@@ -369,7 +290,7 @@ class FitNumber:
|
|||||||
|
|
||||||
|
|
||||||
class ConcatImages:
|
class ConcatImages:
|
||||||
"""Add images to batch."""
|
"""Add images to batch"""
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
RETURN_TYPES = ("IMAGE",)
|
||||||
FUNCTION = "concatenate_tensors"
|
FUNCTION = "concatenate_tensors"
|
||||||
@@ -402,6 +323,4 @@ __nodes__ = [
|
|||||||
AnyToString,
|
AnyToString,
|
||||||
ConcatImages,
|
ConcatImages,
|
||||||
MTB_MathExpression,
|
MTB_MathExpression,
|
||||||
MTB_ToDevice,
|
|
||||||
MTB_ApplyTextTemplate,
|
|
||||||
]
|
]
|
||||||
|
|||||||
+40
-108
@@ -3,11 +3,12 @@ import json
|
|||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import cv2
|
||||||
import folder_paths
|
import folder_paths
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image
|
||||||
from PIL.PngImagePlugin import PngInfo
|
from PIL.PngImagePlugin import PngInfo
|
||||||
from skimage.filters import gaussian
|
from skimage.filters import gaussian
|
||||||
from skimage.util import compare_images
|
from skimage.util import compare_images
|
||||||
@@ -21,9 +22,7 @@ from ..utils import pil2tensor, tensor2np, tensor2pil
|
|||||||
# log.warning("cv2.ximgproc.guidedFilter not found, use opencv-contrib-python")
|
# log.warning("cv2.ximgproc.guidedFilter not found, use opencv-contrib-python")
|
||||||
|
|
||||||
|
|
||||||
def gaussian_kernel(
|
def gaussian_kernel(kernel_size: int, sigma_x: float, sigma_y: float, device=None):
|
||||||
kernel_size: int, sigma_x: float, sigma_y: float, device=None
|
|
||||||
):
|
|
||||||
x, y = torch.meshgrid(
|
x, y = torch.meshgrid(
|
||||||
torch.linspace(-1, 1, kernel_size, device=device),
|
torch.linspace(-1, 1, kernel_size, device=device),
|
||||||
torch.linspace(-1, 1, kernel_size, device=device),
|
torch.linspace(-1, 1, kernel_size, device=device),
|
||||||
@@ -116,9 +115,7 @@ class ColorCorrect:
|
|||||||
return pil2tensor(out)
|
return pil2tensor(out)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def hsv_adjustment_tensor_not_working(
|
def hsv_adjustment_tensor_not_working(image: torch.Tensor, hue, saturation, value):
|
||||||
image: torch.Tensor, hue, saturation, value
|
|
||||||
):
|
|
||||||
"""Abandonning for now"""
|
"""Abandonning for now"""
|
||||||
image = image.squeeze(0).permute(2, 0, 1)
|
image = image.squeeze(0).permute(2, 0, 1)
|
||||||
|
|
||||||
@@ -254,7 +251,6 @@ class LoadImageFromUrl_:
|
|||||||
def load(self, url):
|
def load(self, url):
|
||||||
# get the image from the url
|
# get the image from the url
|
||||||
image = Image.open(requests.get(url, stream=True).raw)
|
image = Image.open(requests.get(url, stream=True).raw)
|
||||||
image = ImageOps.exif_transpose(image)
|
|
||||||
return (pil2tensor(image),)
|
return (pil2tensor(image),)
|
||||||
|
|
||||||
|
|
||||||
@@ -348,16 +344,11 @@ class Sharpen_:
|
|||||||
(sharpen_radius, sharpen_radius, sharpen_radius, sharpen_radius),
|
(sharpen_radius, sharpen_radius, sharpen_radius, sharpen_radius),
|
||||||
"reflect",
|
"reflect",
|
||||||
)
|
)
|
||||||
sharpened = F.conv2d(
|
sharpened = F.conv2d(tensor_image, kernel, padding=center, groups=channels)
|
||||||
tensor_image, kernel, padding=center, groups=channels
|
|
||||||
)
|
|
||||||
|
|
||||||
# Remove padding
|
# Remove padding
|
||||||
sharpened = sharpened[
|
sharpened = sharpened[
|
||||||
:,
|
:, :, sharpen_radius:-sharpen_radius, sharpen_radius:-sharpen_radius
|
||||||
:,
|
|
||||||
sharpen_radius:-sharpen_radius,
|
|
||||||
sharpen_radius:-sharpen_radius,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
sharpened = sharpened.permute(0, 2, 3, 1)
|
sharpened = sharpened.permute(0, 2, 3, 1)
|
||||||
@@ -417,9 +408,7 @@ class MaskToImage:
|
|||||||
for m in masks:
|
for m in masks:
|
||||||
_mask = Image.fromarray(m).convert("L")
|
_mask = Image.fromarray(m).convert("L")
|
||||||
|
|
||||||
log.debug(
|
log.debug(f"Converted mask to PIL Image format, size: {_mask.size}")
|
||||||
f"Converted mask to PIL Image format, size: {_mask.size}"
|
|
||||||
)
|
|
||||||
|
|
||||||
image = Image.new("RGBA", _mask.size, color=color)
|
image = Image.new("RGBA", _mask.size, color=color)
|
||||||
# apply the mask
|
# apply the mask
|
||||||
@@ -436,11 +425,8 @@ class MaskToImage:
|
|||||||
return (pil2tensor(images),)
|
return (pil2tensor(images),)
|
||||||
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
class ColoredImage:
|
class ColoredImage:
|
||||||
"""Constant color image of given size."""
|
"""Constant color image of given size"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
pass
|
pass
|
||||||
@@ -465,87 +451,42 @@ class ColoredImage:
|
|||||||
|
|
||||||
FUNCTION = "render_img"
|
FUNCTION = "render_img"
|
||||||
|
|
||||||
def resize_and_crop(self, img, target_size):
|
|
||||||
# Calculate scaling factors for both dimensions
|
|
||||||
scale_x = target_size[0] / img.width
|
|
||||||
scale_y = target_size[1] / img.height
|
|
||||||
|
|
||||||
# Use the smaller scaling factor to maintain aspect ratio
|
|
||||||
scale = max(scale_x, scale_y)
|
|
||||||
|
|
||||||
# Resize the image based on calculated scale
|
|
||||||
new_size = (int(img.width * scale), int(img.height * scale))
|
|
||||||
img = img.resize(new_size, Image.LANCZOS)
|
|
||||||
|
|
||||||
# Calculate cropping coordinates
|
|
||||||
left = (img.width - target_size[0]) / 2
|
|
||||||
top = (img.height - target_size[1]) / 2
|
|
||||||
right = (img.width + target_size[0]) / 2
|
|
||||||
bottom = (img.height + target_size[1]) / 2
|
|
||||||
|
|
||||||
# Crop and return the image
|
|
||||||
return img.crop((left, top, right, bottom))
|
|
||||||
|
|
||||||
def resize_and_crop_thumbnails(self, img, target_size):
|
|
||||||
img.thumbnail(target_size, Image.LANCZOS)
|
|
||||||
left = (img.width - target_size[0]) / 2
|
|
||||||
top = (img.height - target_size[1]) / 2
|
|
||||||
right = (img.width + target_size[0]) / 2
|
|
||||||
bottom = (img.height + target_size[1]) / 2
|
|
||||||
return img.crop((left, top, right, bottom))
|
|
||||||
|
|
||||||
def render_img(
|
def render_img(
|
||||||
self,
|
self, color, width, height, foreground_image=None, foreground_mask=None
|
||||||
color,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
foreground_image: Optional[torch.Tensor] = None,
|
|
||||||
foreground_mask: Optional[torch.Tensor] = None,
|
|
||||||
):
|
):
|
||||||
image = Image.new("RGBA", (width, height), color=color)
|
image = Image.new("RGBA", (width, height), color=color)
|
||||||
output = []
|
output = []
|
||||||
if foreground_image is not None:
|
if foreground_image is not None:
|
||||||
fg_images = tensor2pil(foreground_image)
|
if foreground_mask is None:
|
||||||
fg_masks = [None] * len(
|
fg_images = tensor2pil(foreground_image)
|
||||||
fg_images
|
for img in fg_images:
|
||||||
) # Default to None for each foreground image
|
if image.size != img.size:
|
||||||
|
|
||||||
if foreground_mask is not None:
|
|
||||||
if foreground_image.size()[0] != foreground_mask.size()[0]:
|
|
||||||
raise ValueError(
|
|
||||||
"Foreground image and mask must have same batch size"
|
|
||||||
)
|
|
||||||
fg_masks = tensor2pil(foreground_mask.unsqueeze(-1))
|
|
||||||
|
|
||||||
for fg_image, fg_mask in zip(fg_images, fg_masks):
|
|
||||||
# Resize and crop if dimensions mismatch
|
|
||||||
if fg_image.size != image.size:
|
|
||||||
fg_image = self.resize_and_crop(fg_image, image.size)
|
|
||||||
if fg_mask:
|
|
||||||
fg_mask = self.resize_and_crop(fg_mask, image.size)
|
|
||||||
|
|
||||||
if fg_mask:
|
|
||||||
output.append(
|
|
||||||
Image.composite(
|
|
||||||
fg_image.convert("RGBA"),
|
|
||||||
image,
|
|
||||||
fg_mask,
|
|
||||||
).convert("RGB")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if fg_image.mode != "RGBA":
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Foreground image must be in 'RGBA' mode "
|
f"Dimension mismatch: image {image.size}, img {img.size}"
|
||||||
f"when no mask is provided, got {fg_image.mode}"
|
|
||||||
)
|
)
|
||||||
output.append(
|
|
||||||
Image.alpha_composite(image, fg_image).convert("RGB")
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
if img.mode != "RGBA":
|
||||||
if foreground_mask is not None:
|
raise ValueError(
|
||||||
log.warn("Mask ignored because no foreground image is given")
|
f"Foreground image must be in 'RGBA' mode when no mask is provided, got {img.mode}"
|
||||||
output.append(image.convert("RGB"))
|
)
|
||||||
|
|
||||||
|
output.append(Image.alpha_composite(image, img).convert("RGB"))
|
||||||
|
|
||||||
|
elif foreground_image.size[0] != foreground_mask.size[0]:
|
||||||
|
raise ValueError("Foreground image and mask must have same batch size")
|
||||||
|
else:
|
||||||
|
fg_images = tensor2pil(foreground_image)
|
||||||
|
fg_masks = tensor2pil(foreground_mask)
|
||||||
|
output.extend(
|
||||||
|
Image.composite(
|
||||||
|
fg_image.convert("RGBA"),
|
||||||
|
image,
|
||||||
|
fg_mask,
|
||||||
|
).convert("RGB")
|
||||||
|
for fg_image, fg_mask in zip(fg_images, fg_masks)
|
||||||
|
)
|
||||||
|
elif foreground_mask is not None:
|
||||||
|
log.warn("Mask ignored because no foreground image is given")
|
||||||
|
|
||||||
output = pil2tensor(output)
|
output = pil2tensor(output)
|
||||||
|
|
||||||
@@ -636,9 +577,7 @@ class ImageResizeFactor:
|
|||||||
):
|
):
|
||||||
# Check if the tensor has the correct dimension
|
# Check if the tensor has the correct dimension
|
||||||
if len(image.shape) not in [3, 4]: # HxWxC or BxHxWxC
|
if len(image.shape) not in [3, 4]: # HxWxC or BxHxWxC
|
||||||
raise ValueError(
|
raise ValueError("Expected image tensor of shape (H, W, C) or (B, H, W, C)")
|
||||||
"Expected image tensor of shape (H, W, C) or (B, H, W, C)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Transpose to CxHxW or BxCxHxW for PyTorch
|
# Transpose to CxHxW or BxCxHxW for PyTorch
|
||||||
if len(image.shape) == 3:
|
if len(image.shape) == 3:
|
||||||
@@ -750,10 +689,7 @@ class SaveImageGrid_:
|
|||||||
subfolder,
|
subfolder,
|
||||||
filename_prefix,
|
filename_prefix,
|
||||||
) = folder_paths.get_save_image_path(
|
) = folder_paths.get_save_image_path(
|
||||||
filename_prefix,
|
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]
|
||||||
self.output_dir,
|
|
||||||
images[0].shape[1],
|
|
||||||
images[0].shape[0],
|
|
||||||
)
|
)
|
||||||
image_list = []
|
image_list = []
|
||||||
batch_counter = counter
|
batch_counter = counter
|
||||||
@@ -783,14 +719,10 @@ class SaveImageGrid_:
|
|||||||
file = f"{filename}_{counter:05}_.png"
|
file = f"{filename}_{counter:05}_.png"
|
||||||
grid = self.create_image_grid(image_list)
|
grid = self.create_image_grid(image_list)
|
||||||
grid.save(
|
grid.save(
|
||||||
os.path.join(full_output_folder, file),
|
os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4
|
||||||
pnginfo=metadata,
|
|
||||||
compress_level=4,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
results = [
|
results = [{"filename": file, "subfolder": subfolder, "type": self.type}]
|
||||||
{"filename": file, "subfolder": subfolder, "type": self.type}
|
|
||||||
]
|
|
||||||
return {"ui": {"images": results}}
|
return {"ui": {"images": results}}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+12
-80
@@ -1,9 +1,10 @@
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from ..log import log
|
from ..log import log
|
||||||
|
|
||||||
|
|
||||||
class StackImages:
|
class StackImages:
|
||||||
"""Stack the input images horizontally or vertically."""
|
"""Stack the input images horizontally or vertically"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -19,91 +20,22 @@ class StackImages:
|
|||||||
|
|
||||||
tensors = list(kwargs.values())
|
tensors = list(kwargs.values())
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Stacking {len(tensors)} tensors "
|
f"Stacking {len(tensors)} tensors {'vertically' if vertical else 'horizontally'}"
|
||||||
f"{'vertically' if vertical else 'horizontally'}"
|
|
||||||
)
|
)
|
||||||
|
log.debug(list(kwargs.keys()))
|
||||||
|
|
||||||
normalized_tensors = [
|
ref_shape = tensors[0].shape
|
||||||
self.normalize_to_rgba(tensor) for tensor in tensors
|
for tensor in tensors[1:]:
|
||||||
]
|
if tensor.shape[1:] != ref_shape[1:]:
|
||||||
|
|
||||||
if vertical:
|
|
||||||
width = normalized_tensors[0].shape[2]
|
|
||||||
if any(tensor.shape[2] != width for tensor in normalized_tensors):
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"All tensors must have the same width "
|
"All tensors must have the same dimensions except for the stacking dimension."
|
||||||
"for vertical stacking."
|
|
||||||
)
|
)
|
||||||
dim = 1
|
|
||||||
else:
|
|
||||||
height = normalized_tensors[0].shape[1]
|
|
||||||
if any(tensor.shape[1] != height for tensor in normalized_tensors):
|
|
||||||
raise ValueError(
|
|
||||||
"All tensors must have the same height "
|
|
||||||
"for horizontal stacking."
|
|
||||||
)
|
|
||||||
dim = 2
|
|
||||||
|
|
||||||
stacked_tensor = torch.cat(normalized_tensors, dim=dim)
|
dim = 1 if vertical else 2
|
||||||
|
|
||||||
|
stacked_tensor = torch.cat(tensors, dim=dim)
|
||||||
|
|
||||||
return (stacked_tensor,)
|
return (stacked_tensor,)
|
||||||
|
|
||||||
def normalize_to_rgba(self, tensor):
|
|
||||||
"""Normalize tensor to have 4 channels (RGBA)."""
|
|
||||||
_, _, _, channels = tensor.shape
|
|
||||||
# already RGBA
|
|
||||||
if channels == 4:
|
|
||||||
return tensor
|
|
||||||
# RGB to RGBA
|
|
||||||
elif channels == 3:
|
|
||||||
alpha_channel = torch.ones(
|
|
||||||
tensor.shape[:-1] + (1,), device=tensor.device
|
|
||||||
) # Add an alpha channel
|
|
||||||
return torch.cat((tensor, alpha_channel), dim=-1)
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
"Tensor has an unsupported number of channels: "
|
|
||||||
"expected 3 (RGB) or 4 (RGBA)."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
__nodes__ = [StackImages]
|
||||||
class PickFromBatch:
|
|
||||||
"""Pick a specific number of images from a batch.
|
|
||||||
|
|
||||||
either from the start or end.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(cls):
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"image": ("IMAGE",),
|
|
||||||
"from_direction": (["end", "start"], {"default": "start"}),
|
|
||||||
"count": ("INT", {"default": 1}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
FUNCTION = "pick_from_batch"
|
|
||||||
CATEGORY = "mtb/image utils"
|
|
||||||
|
|
||||||
def pick_from_batch(self, image, from_direction, count):
|
|
||||||
batch_size = image.size(0)
|
|
||||||
|
|
||||||
# Limit count to the available number of images in the batch
|
|
||||||
count = min(count, batch_size)
|
|
||||||
if count < batch_size:
|
|
||||||
log.warning(
|
|
||||||
f"Requested {count} images, "
|
|
||||||
f"but only {batch_size} are available."
|
|
||||||
)
|
|
||||||
|
|
||||||
if from_direction == "end":
|
|
||||||
selected_tensors = image[-count:]
|
|
||||||
else:
|
|
||||||
selected_tensors = image[:count]
|
|
||||||
|
|
||||||
return (selected_tensors,)
|
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [StackImages, PickFromBatch]
|
|
||||||
|
|||||||
+68
-213
@@ -8,112 +8,42 @@ import comfy.model_management as model_management
|
|||||||
import folder_paths
|
import folder_paths
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
|
from comfy.model_management import get_torch_device
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ..log import log
|
from ..log import log
|
||||||
from ..utils import PIL_FILTER_MAP, output_dir, session_id, tensor2np
|
from ..utils import PIL_FILTER_MAP, audioInputDir, tensor2np
|
||||||
|
|
||||||
|
try:
|
||||||
|
import librosa
|
||||||
|
except ImportError:
|
||||||
|
log.warning("librosa not installed. I/O Audio features will not be available.")
|
||||||
|
|
||||||
|
|
||||||
def get_playlist_path(playlist_name: str, persistant_playlist=False):
|
class LoadAudio_:
|
||||||
if persistant_playlist:
|
"""Load an audio file from the input folder (supports upload)"""
|
||||||
return output_dir / "playlists" / f"{playlist_name}.json"
|
|
||||||
|
|
||||||
return output_dir / "playlists" / session_id / f"{playlist_name}.json"
|
|
||||||
|
|
||||||
|
|
||||||
class ReadPlaylist:
|
|
||||||
"""Read a playlist"""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"enable": ("BOOLEAN", {"default": True}),
|
"audio": ("AUDIO_UPLOAD",),
|
||||||
"persistant_playlist": ("BOOLEAN", {"default": False}),
|
"sample_rate": ("INT", {"default": 44100}),
|
||||||
"playlist_name": (
|
|
||||||
"STRING",
|
|
||||||
{"default": "playlist_{index:04d}"},
|
|
||||||
),
|
|
||||||
"index": ("INT", {"default": 0, "min": 0}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("PLAYLIST",)
|
RETURN_TYPES = ("AUDIO",)
|
||||||
FUNCTION = "read_playlist"
|
RETURN_NAMES = ("audio",)
|
||||||
CATEGORY = "mtb/IO"
|
FUNCTION = "load_audio"
|
||||||
|
CATEGORY = "mtb/audio"
|
||||||
|
|
||||||
def read_playlist(
|
def load_audio(self, audio: str, sample_rate: int):
|
||||||
self,
|
log.debug(f"Audio file: {audio}")
|
||||||
enable: bool,
|
audio_file_path = audioInputDir / audio
|
||||||
persistant_playlist: bool,
|
log.debug(f"Loading audio file: {audio_file_path}")
|
||||||
playlist_name: str,
|
audio_data, _ = librosa.load(audio_file_path.as_posix(), sr=sample_rate)
|
||||||
index: int,
|
audio_tensor = torch.from_numpy(audio_data).to(get_torch_device())
|
||||||
):
|
return (audio_tensor.unsqueeze(0).float(),)
|
||||||
playlist_name = playlist_name.format(index=index)
|
|
||||||
playlist_path = get_playlist_path(playlist_name, persistant_playlist)
|
|
||||||
if not enable:
|
|
||||||
return (None,)
|
|
||||||
|
|
||||||
if not playlist_path.exists():
|
|
||||||
log.warning(f"Playlist {playlist_path} does not exist, skipping")
|
|
||||||
return (None,)
|
|
||||||
|
|
||||||
log.debug(f"Reading playlist {playlist_path}")
|
|
||||||
return (json.loads(playlist_path.read_text(encoding="utf-8")),)
|
|
||||||
|
|
||||||
|
|
||||||
class AddToPlaylist:
|
|
||||||
"""Add a video to the playlist"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(cls):
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"relative_paths": ("BOOLEAN", {"default": False}),
|
|
||||||
"persistant_playlist": ("BOOLEAN", {"default": False}),
|
|
||||||
"playlist_name": (
|
|
||||||
"STRING",
|
|
||||||
{"default": "playlist_{index:04d}"},
|
|
||||||
),
|
|
||||||
"index": ("INT", {"default": 0, "min": 0}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ()
|
|
||||||
OUTPUT_NODE = True
|
|
||||||
FUNCTION = "add_to_playlist"
|
|
||||||
CATEGORY = "mtb/IO"
|
|
||||||
|
|
||||||
def add_to_playlist(
|
|
||||||
self,
|
|
||||||
relative_paths: bool,
|
|
||||||
persistant_playlist: bool,
|
|
||||||
playlist_name: str,
|
|
||||||
index: int,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
playlist_name = playlist_name.format(index=index)
|
|
||||||
playlist_path = get_playlist_path(playlist_name, persistant_playlist)
|
|
||||||
|
|
||||||
if not playlist_path.parent.exists():
|
|
||||||
playlist_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
playlist = []
|
|
||||||
if not playlist_path.exists():
|
|
||||||
playlist_path.write_text("[]")
|
|
||||||
else:
|
|
||||||
playlist = json.loads(playlist_path.read_text())
|
|
||||||
log.debug(f"Playlist {playlist_path} has {len(playlist)} items")
|
|
||||||
for video in kwargs.values():
|
|
||||||
if relative_paths:
|
|
||||||
video = Path(video).relative_to(output_dir).as_posix()
|
|
||||||
|
|
||||||
log.debug(f"Adding {video} to playlist")
|
|
||||||
playlist.append(video)
|
|
||||||
|
|
||||||
log.debug(f"Writing playlist {playlist_path}")
|
|
||||||
playlist_path.write_text(json.dumps(playlist), encoding="utf-8")
|
|
||||||
return ()
|
|
||||||
|
|
||||||
|
|
||||||
class ExportWithFfmpeg:
|
class ExportWithFfmpeg:
|
||||||
@@ -122,22 +52,18 @@ class ExportWithFfmpeg:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"optional": {
|
|
||||||
"images": ("IMAGE",),
|
|
||||||
"playlist": ("PLAYLIST",),
|
|
||||||
},
|
|
||||||
"required": {
|
"required": {
|
||||||
|
"images": ("IMAGE",),
|
||||||
|
# "frames": ("FRAMES",),
|
||||||
"fps": ("FLOAT", {"default": 24, "min": 1}),
|
"fps": ("FLOAT", {"default": 24, "min": 1}),
|
||||||
"prefix": ("STRING", {"default": "export"}),
|
"prefix": ("STRING", {"default": "export"}),
|
||||||
"format": (
|
"format": (["mov", "mp4", "mkv", "avi"], {"default": "mov"}),
|
||||||
["mov", "mp4", "mkv", "gif", "avi"],
|
|
||||||
{"default": "mov"},
|
|
||||||
),
|
|
||||||
"codec": (
|
"codec": (
|
||||||
["prores_ks", "libx264", "libx265", "gif"],
|
["prores_ks", "libx264", "libx265"],
|
||||||
{"default": "prores_ks"},
|
{"default": "prores_ks"},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("VIDEO",)
|
RETURN_TYPES = ("VIDEO",)
|
||||||
@@ -147,99 +73,52 @@ class ExportWithFfmpeg:
|
|||||||
|
|
||||||
def export_prores(
|
def export_prores(
|
||||||
self,
|
self,
|
||||||
|
images: torch.Tensor,
|
||||||
fps: float,
|
fps: float,
|
||||||
prefix: str,
|
prefix: str,
|
||||||
format: str,
|
format: str,
|
||||||
codec: str,
|
codec: str,
|
||||||
images: Optional[torch.Tensor] = None,
|
prompt=None,
|
||||||
playlist: Optional[List[str]] = None,
|
extra_pnginfo=None,
|
||||||
):
|
):
|
||||||
|
metadata = {}
|
||||||
|
if images.size(0) == 0:
|
||||||
|
return ("",)
|
||||||
|
|
||||||
|
if extra_pnginfo is not None:
|
||||||
|
metadata["extra"] = {}
|
||||||
|
for x in extra_pnginfo:
|
||||||
|
metadata["extra"][x] = json.dumps(extra_pnginfo[x])
|
||||||
|
|
||||||
|
if prompt is not None:
|
||||||
|
metadata["prompt"] = json.dumps(prompt)
|
||||||
|
|
||||||
|
output_dir = Path(folder_paths.get_output_directory())
|
||||||
pix_fmt = "rgb48le" if codec == "prores_ks" else "yuv420p"
|
pix_fmt = "rgb48le" if codec == "prores_ks" else "yuv420p"
|
||||||
file_ext = format
|
file_ext = format
|
||||||
file_id = f"{prefix}_{uuid.uuid4()}.{file_ext}"
|
file_id = f"{prefix}_{uuid.uuid4()}.{file_ext}"
|
||||||
|
|
||||||
if playlist is not None and images is not None:
|
log.debug(f"Exporting to {output_dir / file_id}")
|
||||||
log.info(f"Exporting to {output_dir / file_id}")
|
|
||||||
|
|
||||||
if playlist is not None:
|
|
||||||
if len(playlist) == 0:
|
|
||||||
log.debug("Playlist is empty, skipping")
|
|
||||||
return ("",)
|
|
||||||
|
|
||||||
temp_playlist_path = (
|
|
||||||
output_dir / f"temp_playlist_{uuid.uuid4()}.txt"
|
|
||||||
)
|
|
||||||
log.debug(
|
|
||||||
f"Create a temporary file to list the videos for concatenation to {temp_playlist_path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(temp_playlist_path, "w") as f:
|
|
||||||
for video_path in playlist:
|
|
||||||
f.write(f"file '{video_path}'\n")
|
|
||||||
|
|
||||||
out_path = (output_dir / file_id).as_posix()
|
|
||||||
|
|
||||||
# Prepare the FFmpeg command for concatenating videos from the playlist
|
|
||||||
command = [
|
|
||||||
"ffmpeg",
|
|
||||||
"-f",
|
|
||||||
"concat",
|
|
||||||
"-safe",
|
|
||||||
"0",
|
|
||||||
"-i",
|
|
||||||
temp_playlist_path.as_posix(),
|
|
||||||
"-c",
|
|
||||||
"copy",
|
|
||||||
"-y",
|
|
||||||
out_path,
|
|
||||||
]
|
|
||||||
log.debug(f"Executing {command}")
|
|
||||||
subprocess.run(command)
|
|
||||||
|
|
||||||
temp_playlist_path.unlink()
|
|
||||||
|
|
||||||
return (out_path,)
|
|
||||||
|
|
||||||
if (
|
|
||||||
images is None or images.size(0) == 0
|
|
||||||
): # the is None check is just for the type checker
|
|
||||||
return ("",)
|
|
||||||
|
|
||||||
frames = tensor2np(images)
|
frames = tensor2np(images)
|
||||||
log.debug(f"Frames type {type(frames[0])}")
|
log.debug(f"Frames type {type(frames[0])}")
|
||||||
log.debug(f"Exporting {len(frames)} frames")
|
log.debug(f"Exporting {len(frames)} frames")
|
||||||
|
|
||||||
if codec == "gif":
|
frames = [frame.astype(np.uint16) * 257 for frame in frames]
|
||||||
out_path = (output_dir / file_id).as_posix()
|
|
||||||
command = [
|
|
||||||
"ffmpeg",
|
|
||||||
"-f",
|
|
||||||
"image2pipe",
|
|
||||||
"-vcodec",
|
|
||||||
"png",
|
|
||||||
"-r",
|
|
||||||
str(fps),
|
|
||||||
"-i",
|
|
||||||
"-",
|
|
||||||
"-vcodec",
|
|
||||||
"gif",
|
|
||||||
"-y",
|
|
||||||
out_path,
|
|
||||||
]
|
|
||||||
process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
|
||||||
for frame in frames:
|
|
||||||
model_management.throw_exception_if_processing_interrupted()
|
|
||||||
Image.fromarray(frame).save(process.stdin, "PNG")
|
|
||||||
|
|
||||||
process.stdin.close()
|
|
||||||
process.wait()
|
|
||||||
else:
|
|
||||||
frames = [frame.astype(np.uint16) * 257 for frame in frames]
|
|
||||||
|
|
||||||
height, width, _ = frames[0].shape
|
height, width, _ = frames[0].shape
|
||||||
|
|
||||||
out_path = (output_dir / file_id).as_posix()
|
out_path = (output_dir / file_id).as_posix()
|
||||||
|
|
||||||
|
metadata_cmd = []
|
||||||
|
|
||||||
|
if metadata:
|
||||||
|
for k, v in metadata.items():
|
||||||
|
metadata_cmd += [
|
||||||
|
"-metadata:s:v",
|
||||||
|
f"{k}='{v if isinstance(v,str) else json.dumps(v)}'",
|
||||||
|
]
|
||||||
|
|
||||||
# Prepare the FFmpeg command
|
# Prepare the FFmpeg command
|
||||||
command = [
|
command = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
@@ -258,6 +137,7 @@ class ExportWithFfmpeg:
|
|||||||
"-",
|
"-",
|
||||||
"-c:v",
|
"-c:v",
|
||||||
codec,
|
codec,
|
||||||
|
*metadata_cmd,
|
||||||
"-r",
|
"-r",
|
||||||
str(fps),
|
str(fps),
|
||||||
"-y",
|
"-y",
|
||||||
@@ -319,8 +199,9 @@ class SaveGif:
|
|||||||
"resize_by": ("FLOAT", {"default": 1.0, "min": 0.1}),
|
"resize_by": ("FLOAT", {"default": 1.0, "min": 0.1}),
|
||||||
"optimize": ("BOOLEAN", {"default": False}),
|
"optimize": ("BOOLEAN", {"default": False}),
|
||||||
"pingpong": ("BOOLEAN", {"default": False}),
|
"pingpong": ("BOOLEAN", {"default": False}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
"resample_filter": (list(PIL_FILTER_MAP.keys()),),
|
"resample_filter": (list(PIL_FILTER_MAP.keys()),),
|
||||||
"use_ffmpeg": ("BOOLEAN", {"default": False}),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +218,6 @@ class SaveGif:
|
|||||||
optimize=False,
|
optimize=False,
|
||||||
pingpong=False,
|
pingpong=False,
|
||||||
resample_filter=None,
|
resample_filter=None,
|
||||||
use_ffmpeg=False,
|
|
||||||
):
|
):
|
||||||
if image.size(0) == 0:
|
if image.size(0) == 0:
|
||||||
return ("",)
|
return ("",)
|
||||||
@@ -356,43 +236,18 @@ class SaveGif:
|
|||||||
ruuid = ruuid.hex[:10]
|
ruuid = ruuid.hex[:10]
|
||||||
out_path = f"{folder_paths.output_directory}/{ruuid}.gif"
|
out_path = f"{folder_paths.output_directory}/{ruuid}.gif"
|
||||||
|
|
||||||
if use_ffmpeg:
|
# Create the GIF from PIL images
|
||||||
# Use FFmpeg to create the GIF from PIL images
|
pil_images[0].save(
|
||||||
command = [
|
out_path,
|
||||||
"ffmpeg",
|
save_all=True,
|
||||||
"-f",
|
append_images=pil_images[1:],
|
||||||
"image2pipe",
|
optimize=optimize,
|
||||||
"-vcodec",
|
duration=int(1000 / fps),
|
||||||
"png",
|
loop=0,
|
||||||
"-r",
|
)
|
||||||
str(fps),
|
|
||||||
"-i",
|
|
||||||
"-",
|
|
||||||
"-vcodec",
|
|
||||||
"gif",
|
|
||||||
"-y",
|
|
||||||
out_path,
|
|
||||||
]
|
|
||||||
process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
|
||||||
for image in pil_images:
|
|
||||||
model_management.throw_exception_if_processing_interrupted()
|
|
||||||
image.save(process.stdin, "PNG")
|
|
||||||
process.stdin.close()
|
|
||||||
process.wait()
|
|
||||||
|
|
||||||
else:
|
results = [{"filename": f"{ruuid}.gif", "subfolder": "", "type": "output"}]
|
||||||
pil_images[0].save(
|
|
||||||
out_path,
|
|
||||||
save_all=True,
|
|
||||||
append_images=pil_images[1:],
|
|
||||||
optimize=optimize,
|
|
||||||
duration=int(1000 / fps),
|
|
||||||
loop=0,
|
|
||||||
)
|
|
||||||
results = [
|
|
||||||
{"filename": f"{ruuid}.gif", "subfolder": "", "type": "output"}
|
|
||||||
]
|
|
||||||
return {"ui": {"gif": results}}
|
return {"ui": {"gif": results}}
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [SaveGif, ExportWithFfmpeg, AddToPlaylist, ReadPlaylist]
|
__nodes__ = [SaveGif, ExportWithFfmpeg, LoadAudio_]
|
||||||
|
|||||||
+10
-15
@@ -1,11 +1,13 @@
|
|||||||
import hashlib, json, os, re
|
import os
|
||||||
from pathlib import Path
|
import re
|
||||||
|
|
||||||
import folder_paths
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
import hashlib
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
from PIL.PngImagePlugin import PngInfo
|
from PIL.PngImagePlugin import PngInfo
|
||||||
|
import folder_paths
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
from ..log import log
|
from ..log import log
|
||||||
|
|
||||||
@@ -171,10 +173,7 @@ class SaveImageSequence:
|
|||||||
"required": {
|
"required": {
|
||||||
"images": ("IMAGE",),
|
"images": ("IMAGE",),
|
||||||
"filename_prefix": ("STRING", {"default": "Sequence"}),
|
"filename_prefix": ("STRING", {"default": "Sequence"}),
|
||||||
"current_frame": (
|
"current_frame": ("INT", {"default": 0, "min": 0, "max": 9999999}),
|
||||||
"INT",
|
|
||||||
{"default": 0, "min": 0, "max": 9999999},
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||||
}
|
}
|
||||||
@@ -221,14 +220,10 @@ class SaveImageSequence:
|
|||||||
resolved_path = Path(self.output_dir) / filename_prefix
|
resolved_path = Path(self.output_dir) / filename_prefix
|
||||||
resolved_path.mkdir(parents=True, exist_ok=True)
|
resolved_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
resolved_img = (
|
resolved_img = resolved_path / f"{filename_prefix}_{current_frame:05}.png"
|
||||||
resolved_path / f"{filename_prefix}_{current_frame:05}.png"
|
|
||||||
)
|
|
||||||
|
|
||||||
output_image = images[0].cpu().numpy()
|
output_image = images[0].cpu().numpy()
|
||||||
img = Image.fromarray(
|
img = Image.fromarray(np.clip(output_image * 255.0, 0, 255).astype(np.uint8))
|
||||||
np.clip(output_image * 255.0, 0, 255).astype(np.uint8)
|
|
||||||
)
|
|
||||||
metadata = PngInfo()
|
metadata = PngInfo()
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
metadata.add_text("prompt", json.dumps(prompt))
|
metadata.add_text("prompt", json.dumps(prompt))
|
||||||
|
|||||||
-114
@@ -1,114 +0,0 @@
|
|||||||
[tool.poetry]
|
|
||||||
name = "comfy-mtb"
|
|
||||||
version = "0.4.0"
|
|
||||||
description = "Animation oriented nodes pack for ComfyUI."
|
|
||||||
license = "MIT"
|
|
||||||
readme = "README.md"
|
|
||||||
repository = "https://github.com/melMass/comfy_mtb"
|
|
||||||
authors = ["Mel Massadian"]
|
|
||||||
packages = [{ include = "comfy-mtb" }]
|
|
||||||
classifiers = [
|
|
||||||
"License :: OSI Approved :: MIT License",
|
|
||||||
"Operating System :: OS Independent",
|
|
||||||
"Programming Language :: Python",
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: 3.11",
|
|
||||||
"Intended Audience :: Developers",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.poetry.urls]
|
|
||||||
"Bug Tracker" = "https://github.com/melMass/comfy_mtb/issues"
|
|
||||||
"Changelog" = "https://github.com/melMass/comfy_mtb/releases"
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "^3.10"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
black = { extras = ["jupyter"], version = "^23.7.0" }
|
|
||||||
codespell = "^2.2.5"
|
|
||||||
mypy = "^1.5.1"
|
|
||||||
pre-commit = "^3.3.3"
|
|
||||||
pytest = "^7.4.0"
|
|
||||||
pytest-cov = "^4.1.0"
|
|
||||||
pytest-random-order = "^1.1.0"
|
|
||||||
ruff = "^0.0.285"
|
|
||||||
|
|
||||||
[tool.poetry.group.docs]
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
[tool.poetry.group.docs.dependencies]
|
|
||||||
docutils = "0.17.1"
|
|
||||||
jupyter-book = "^0.15.1"
|
|
||||||
sphinx-autobuild = "^2021.3.14"
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
|
||||||
log_level = "DEBUG"
|
|
||||||
log_cli = true
|
|
||||||
markers = [
|
|
||||||
"wip: tests that aren't fully finished yet",
|
|
||||||
"heavy: marks tests as heavy (deselect with '-m \"not heavy\"')",
|
|
||||||
|
|
||||||
]
|
|
||||||
filterwarnings = ["ignore::UserWarning", 'ignore::DeprecationWarning']
|
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
profile = "black"
|
|
||||||
line_length = 88
|
|
||||||
auto_identify_namespace_packages = false
|
|
||||||
# NOTE:
|
|
||||||
# pyright doesn't like implicit namespace + single line (related to https://github.com/microsoft/pyright/issues/2882?) but it's horible so I'll live with it
|
|
||||||
force_single_line = false
|
|
||||||
known_first_party = ["mtb"]
|
|
||||||
extend_skip = ["archives"]
|
|
||||||
combine_straight_imports = true
|
|
||||||
|
|
||||||
[tool.coverage.run]
|
|
||||||
parallel = true
|
|
||||||
source = ["docs", "tests", "comfy-mtb"]
|
|
||||||
|
|
||||||
[tool.coverage.report]
|
|
||||||
fail_under = 90
|
|
||||||
show_missing = true
|
|
||||||
|
|
||||||
[tool.coverage.html]
|
|
||||||
show_contexts = true
|
|
||||||
|
|
||||||
[tool.ruff]
|
|
||||||
line-length = 79
|
|
||||||
select = ["A", "B", "C", "D", "E", "F", "FBT", "I", "N", "S", "SIM", "UP", "W"]
|
|
||||||
# NOTE:
|
|
||||||
# D102 - undocumented-public-method (noisy)
|
|
||||||
# D103 - undocumented-public-function (noisy)
|
|
||||||
# D100 - undocumented-public-module (noisy)
|
|
||||||
# N802 - invalid-function-name (forced by comfy's arch)
|
|
||||||
ignore = ["D103", "D102", "D100", "N802"]
|
|
||||||
# exclude auto generated file
|
|
||||||
extend-exclude = ["./docs/conf.py"]
|
|
||||||
|
|
||||||
[tool.ruff.per-file-ignores]
|
|
||||||
# imported but unused
|
|
||||||
"__init__.py" = ["F401"]
|
|
||||||
# use of assert detected
|
|
||||||
"tests/*" = ["S101"]
|
|
||||||
|
|
||||||
[tool.ruff.pydocstyle]
|
|
||||||
convention = "numpy"
|
|
||||||
|
|
||||||
[tool.mypy]
|
|
||||||
pretty = true
|
|
||||||
ignore_missing_imports = true
|
|
||||||
# exclude auto generated file
|
|
||||||
exclude = ["docs/conf.py"]
|
|
||||||
|
|
||||||
[tool.codespell]
|
|
||||||
# exclude auto generated file
|
|
||||||
skip = "./docs/conf.py,poetry.lock"
|
|
||||||
check-filenames = true
|
|
||||||
|
|
||||||
[tool.poetry-version-plugin]
|
|
||||||
source = "git-tag"
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
+2
-2
@@ -6,5 +6,5 @@ rembg
|
|||||||
imageio_ffmpeg
|
imageio_ffmpeg
|
||||||
rich
|
rich
|
||||||
rich_argparse
|
rich_argparse
|
||||||
matplotlib
|
librosa
|
||||||
pillow
|
torchaudio
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
$env.GITHUB_TOKEN = (gh auth token)
|
|
||||||
git cliff --tag main | save -f CHANGELOG.md
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# NOTE: this file must be used in all test_* files to setup the test env.
|
|
||||||
|
|
||||||
import importlib
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def setup_test():
|
|
||||||
mod_path = Path(__file__).parent.parent
|
|
||||||
|
|
||||||
# add custom_nodes to path
|
|
||||||
sys.path.append(mod_path.parent.as_posix())
|
|
||||||
print(f"Appended: {mod_path.parent.as_posix()}")
|
|
||||||
|
|
||||||
# add comfy root to path
|
|
||||||
sys.path.append(mod_path.parent.parent.as_posix())
|
|
||||||
print(f"Appended: {mod_path.parent.parent.as_posix()}")
|
|
||||||
|
|
||||||
# import mtb
|
|
||||||
module = importlib.import_module(mod_path.name)
|
|
||||||
|
|
||||||
# add the module to globals
|
|
||||||
globals()[mod_path.name] = module
|
|
||||||
|
|
||||||
# set the logging level for third-party libraries
|
|
||||||
logging.getLogger("xformers").setLevel(logging.WARNING)
|
|
||||||
logging.getLogger("tensorflow").setLevel(logging.ERROR)
|
|
||||||
logging.getLogger("h5py._conv").setLevel(logging.ERROR)
|
|
||||||
logging.getLogger("numexpr.utils").setLevel(logging.ERROR)
|
|
||||||
logging.getLogger("matplotlib").setLevel(logging.ERROR)
|
|
||||||
logging.getLogger("torchaudio._extension").setLevel(logging.ERROR)
|
|
||||||
logging.getLogger("torch.utils.tensorboard").setLevel(logging.ERROR)
|
|
||||||
logging.getLogger("torchvision.transforms.functional_tensor").setLevel(
|
|
||||||
logging.ERROR
|
|
||||||
)
|
|
||||||
logging.getLogger("basicsr.metrics.niqe").setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from setup_test import setup_test
|
|
||||||
|
|
||||||
setup_test()
|
|
||||||
|
|
||||||
from comfy_mtb_utils.nodes.graph_utils import FitNumber
|
|
||||||
from comfy_mtb_utils.utils import apply_easing
|
|
||||||
|
|
||||||
|
|
||||||
# - apply_easing
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"value, easing_type, expected",
|
|
||||||
[
|
|
||||||
(0.5, "Linear", 0.5),
|
|
||||||
(0, "Linear", 0),
|
|
||||||
(1, "Linear", 1),
|
|
||||||
(0.5, "Sine In", 0.2928932188134524),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_apply_easing(value: float, easing_type: str, expected: float):
|
|
||||||
assert apply_easing(value, easing_type) == pytest.approx(
|
|
||||||
expected
|
|
||||||
), "Easing function did not return expected value"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"val, easing_type",
|
|
||||||
[
|
|
||||||
(0.5, "NonExistentEasing"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_apply_easing_error_handling(val, easing_type):
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
apply_easing(val, easing_type)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"val, easing_type, expected",
|
|
||||||
[
|
|
||||||
(0, "Linear", 0),
|
|
||||||
(1, "Linear", 1),
|
|
||||||
(0.5, "Linear", 0.5),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_apply_easing_corner_cases(val, easing_type, expected):
|
|
||||||
assert apply_easing(val, easing_type) == expected
|
|
||||||
|
|
||||||
|
|
||||||
# - FitNumber
|
|
||||||
@pytest.fixture
|
|
||||||
def fit_number_instance():
|
|
||||||
return FitNumber()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"value, clamp, source_min, source_max, target_min, target_max, easing, expected",
|
|
||||||
[
|
|
||||||
(0.5, False, 0, 1, 0, 1, "Linear", (0.5,)),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_fit_number_set_range(
|
|
||||||
fit_number_instance,
|
|
||||||
value,
|
|
||||||
clamp,
|
|
||||||
source_min,
|
|
||||||
source_max,
|
|
||||||
target_min,
|
|
||||||
target_max,
|
|
||||||
easing,
|
|
||||||
expected,
|
|
||||||
):
|
|
||||||
assert (
|
|
||||||
fit_number_instance.set_range(
|
|
||||||
value=value,
|
|
||||||
clamp=clamp,
|
|
||||||
source_min=source_min,
|
|
||||||
source_max=source_max,
|
|
||||||
target_min=target_min,
|
|
||||||
target_max=target_max,
|
|
||||||
easing=easing,
|
|
||||||
)
|
|
||||||
== expected
|
|
||||||
), "Set range method did not return expected value"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"value, clamp, source_min, source_max, target_min, target_max, easing, expected",
|
|
||||||
[
|
|
||||||
(0.5, False, 0, 1, 0, 1, "Linear", (0.5,)),
|
|
||||||
(0.4, False, 1.0, 0.4, 0.4, 0.7, "Linear", (0.7,)),
|
|
||||||
# Add more test cases covering various scenarios, edge cases, and easing types
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_fit_number_inverted_ranges(
|
|
||||||
fit_number_instance,
|
|
||||||
value,
|
|
||||||
clamp,
|
|
||||||
source_min,
|
|
||||||
source_max,
|
|
||||||
target_min,
|
|
||||||
target_max,
|
|
||||||
easing,
|
|
||||||
expected,
|
|
||||||
):
|
|
||||||
assert (
|
|
||||||
fit_number_instance.set_range(
|
|
||||||
value=value,
|
|
||||||
clamp=clamp,
|
|
||||||
source_min=source_min,
|
|
||||||
source_max=source_max,
|
|
||||||
target_min=target_min,
|
|
||||||
target_max=target_max,
|
|
||||||
easing=easing,
|
|
||||||
)
|
|
||||||
== expected
|
|
||||||
), "Set range method did not return expected value"
|
|
||||||
@@ -86,9 +86,7 @@ def get_server_info():
|
|||||||
base_url = args.listen
|
base_url = args.listen
|
||||||
if base_url == "0.0.0.0":
|
if base_url == "0.0.0.0":
|
||||||
log.debug("Server set to 0.0.0.0, we will try to resolve the host IP")
|
log.debug("Server set to 0.0.0.0, we will try to resolve the host IP")
|
||||||
base_url = ip_checker.get_working_ip(
|
base_url = ip_checker.get_working_ip(f"http://{{}}:{args.port}/history")
|
||||||
f"http://{{}}:{args.port}/history"
|
|
||||||
)
|
|
||||||
log.debug(f"Setting ip to {base_url}")
|
log.debug(f"Setting ip to {base_url}")
|
||||||
return (base_url, args.port)
|
return (base_url, args.port)
|
||||||
|
|
||||||
@@ -168,9 +166,7 @@ def run_command(cmd, ignored_lines_start=None):
|
|||||||
try:
|
try:
|
||||||
_run_command(shell_cmd, ignored_lines_start)
|
_run_command(shell_cmd, ignored_lines_start)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(
|
print(f"Command failed with return code: {e.returncode}", file=sys.stderr)
|
||||||
f"Command failed with return code: {e.returncode}", file=sys.stderr
|
|
||||||
)
|
|
||||||
print(e.stderr.strip(), file=sys.stderr)
|
print(e.stderr.strip(), file=sys.stderr)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
@@ -218,13 +214,7 @@ def import_install(package_name):
|
|||||||
|
|
||||||
except Exception: # (ImportError, ModuleNotFoundError):
|
except Exception: # (ImportError, ModuleNotFoundError):
|
||||||
run_command(
|
run_command(
|
||||||
[
|
[Path(sys.executable).as_posix(), "-m", "pip", "install", package_spec]
|
||||||
Path(sys.executable).as_posix(),
|
|
||||||
"-m",
|
|
||||||
"pip",
|
|
||||||
"install",
|
|
||||||
package_spec,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
importlib.import_module(package_name)
|
importlib.import_module(package_name)
|
||||||
|
|
||||||
@@ -248,11 +238,11 @@ here = Path(__file__).parent.absolute()
|
|||||||
# - Construct the absolute path to the ComfyUI directory
|
# - Construct the absolute path to the ComfyUI directory
|
||||||
comfy_dir = Path(folder_paths.base_path)
|
comfy_dir = Path(folder_paths.base_path)
|
||||||
models_dir = Path(folder_paths.models_dir)
|
models_dir = Path(folder_paths.models_dir)
|
||||||
output_dir = Path(folder_paths.output_directory)
|
|
||||||
styles_dir = comfy_dir / "styles"
|
styles_dir = comfy_dir / "styles"
|
||||||
session_id = str(uuid.uuid4())
|
audioInputDir = comfy_dir / "input" / "audio"
|
||||||
|
|
||||||
# - Construct the path to the font file
|
# - Construct the path to the font file
|
||||||
font_path = here / "data" / "font.ttf"
|
font_path = here / "font.ttf"
|
||||||
|
|
||||||
# - Add extern folder to path
|
# - Add extern folder to path
|
||||||
extern_root = here / "extern"
|
extern_root = here / "extern"
|
||||||
@@ -263,7 +253,7 @@ for pth in extern_root.iterdir():
|
|||||||
|
|
||||||
# - Add the ComfyUI directory and custom nodes path to the sys.path list
|
# - Add the ComfyUI directory and custom nodes path to the sys.path list
|
||||||
add_path(comfy_dir)
|
add_path(comfy_dir)
|
||||||
add_path(comfy_dir / "custom_nodes")
|
add_path((comfy_dir / "custom_nodes"))
|
||||||
|
|
||||||
PIL_FILTER_MAP = {
|
PIL_FILTER_MAP = {
|
||||||
"nearest": Image.Resampling.NEAREST,
|
"nearest": Image.Resampling.NEAREST,
|
||||||
@@ -287,9 +277,7 @@ def tensor2pil(image: torch.Tensor) -> List[Image.Image]:
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
Image.fromarray(
|
Image.fromarray(
|
||||||
np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(
|
np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
|
||||||
np.uint8
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -298,9 +286,7 @@ def pil2tensor(image: Union[Image.Image, List[Image.Image]]) -> torch.Tensor:
|
|||||||
if isinstance(image, list):
|
if isinstance(image, list):
|
||||||
return torch.cat([pil2tensor(img) for img in image], dim=0)
|
return torch.cat([pil2tensor(img) for img in image], dim=0)
|
||||||
|
|
||||||
return torch.from_numpy(
|
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
||||||
np.array(image).astype(np.float32) / 255.0
|
|
||||||
).unsqueeze(0)
|
|
||||||
|
|
||||||
|
|
||||||
def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor:
|
def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor:
|
||||||
@@ -318,31 +304,23 @@ def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]:
|
|||||||
out.extend(tensor2np(tensor[i]))
|
out.extend(tensor2np(tensor[i]))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
return [
|
return [np.clip(255.0 * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)]
|
||||||
np.clip(255.0 * tensor.cpu().numpy().squeeze(), 0, 255).astype(
|
|
||||||
np.uint8
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def pad(img, left, right, top, bottom):
|
def pad(img, left, right, top, bottom):
|
||||||
pad_width = np.array(((0, 0), (top, bottom), (left, right)))
|
pad_width = np.array(((0, 0), (top, bottom), (left, right)))
|
||||||
print(
|
print(f"pad_width: {pad_width}, shape: {pad_width.shape}") # Debugging line
|
||||||
f"pad_width: {pad_width}, shape: {pad_width.shape}"
|
|
||||||
) # Debugging line
|
|
||||||
return np.pad(img, pad_width, mode="wrap")
|
return np.pad(img, pad_width, mode="wrap")
|
||||||
|
|
||||||
|
|
||||||
def tiles_infer(tiles, ort_session, progress_callback=None):
|
def tiles_infer(tiles, ort_session, progress_callback=None):
|
||||||
"""Infer each tile with the given model. progress_callback will be called with
|
"""Infer each tile with the given model. progress_callback will be called with
|
||||||
arguments : current tile idx and total tiles amount (used to show progress on
|
arguments : current tile idx and total tiles amount (used to show progress on
|
||||||
cursor in Blender).
|
cursor in Blender)."""
|
||||||
"""
|
|
||||||
out_channels = 3 # normal map RGB channels
|
out_channels = 3 # normal map RGB channels
|
||||||
tiles_nb = tiles.shape[0]
|
tiles_nb = tiles.shape[0]
|
||||||
pred_tiles = np.empty(
|
pred_tiles = np.empty((tiles_nb, out_channels, tiles.shape[2], tiles.shape[3]))
|
||||||
(tiles_nb, out_channels, tiles.shape[2], tiles.shape[3])
|
|
||||||
)
|
|
||||||
|
|
||||||
for i in range(tiles_nb):
|
for i in range(tiles_nb):
|
||||||
if progress_callback != None:
|
if progress_callback != None:
|
||||||
@@ -356,6 +334,7 @@ def tiles_infer(tiles, ort_session, progress_callback=None):
|
|||||||
|
|
||||||
def generate_mask(tile_size, stride_size):
|
def generate_mask(tile_size, stride_size):
|
||||||
"""Generates a pyramidal-like mask. Used for mixing overlapping predicted tiles."""
|
"""Generates a pyramidal-like mask. Used for mixing overlapping predicted tiles."""
|
||||||
|
|
||||||
tile_h, tile_w = tile_size
|
tile_h, tile_w = tile_size
|
||||||
stride_h, stride_w = stride_size
|
stride_h, stride_w = stride_size
|
||||||
ramp_h = tile_h - stride_h
|
ramp_h = tile_h - stride_h
|
||||||
@@ -394,8 +373,8 @@ def generate_mask(tile_size, stride_size):
|
|||||||
|
|
||||||
def corner_mask(side_length):
|
def corner_mask(side_length):
|
||||||
"""Generates the corner part of the pyramidal-like mask.
|
"""Generates the corner part of the pyramidal-like mask.
|
||||||
Currently, only for square shapes.
|
Currently, only for square shapes."""
|
||||||
"""
|
|
||||||
corner = np.zeros([side_length, side_length])
|
corner = np.zeros([side_length, side_length])
|
||||||
|
|
||||||
for h in range(0, side_length):
|
for h in range(0, side_length):
|
||||||
@@ -431,8 +410,8 @@ def scaling_mask(side_length):
|
|||||||
|
|
||||||
def tiles_merge(tiles, stride_size, img_size, paddings):
|
def tiles_merge(tiles, stride_size, img_size, paddings):
|
||||||
"""Merges the list of tiles into one image. img_size is the original size, before
|
"""Merges the list of tiles into one image. img_size is the original size, before
|
||||||
padding.
|
padding."""
|
||||||
"""
|
|
||||||
_, tile_h, tile_w = tiles[0].shape
|
_, tile_h, tile_w = tiles[0].shape
|
||||||
pad_left, pad_right, pad_top, pad_bottom = paddings
|
pad_left, pad_right, pad_top, pad_bottom = paddings
|
||||||
height = img_size[1] + pad_top + pad_bottom
|
height = img_size[1] + pad_top + pad_bottom
|
||||||
@@ -465,8 +444,7 @@ def tiles_merge(tiles, stride_size, img_size, paddings):
|
|||||||
|
|
||||||
def tiles_split(img, tile_size, stride_size):
|
def tiles_split(img, tile_size, stride_size):
|
||||||
"""Returns list of tiles from the given image and the padding used to fit the tiles
|
"""Returns list of tiles from the given image and the padding used to fit the tiles
|
||||||
in it. Input image must have dimension C,H,W.
|
in it. Input image must have dimension C,H,W."""
|
||||||
"""
|
|
||||||
log.debug(f"Splitting img: tile {tile_size}, stride {stride_size} ")
|
log.debug(f"Splitting img: tile {tile_size}, stride {stride_size} ")
|
||||||
tile_h, tile_w = tile_size
|
tile_h, tile_w = tile_size
|
||||||
stride_h, stride_w = stride_size
|
stride_h, stride_w = stride_size
|
||||||
@@ -523,9 +501,7 @@ def tiles_split(img, tile_size, stride_size):
|
|||||||
|
|
||||||
# region MODEL Utilities
|
# region MODEL Utilities
|
||||||
def download_antelopev2():
|
def download_antelopev2():
|
||||||
antelopev2_url = (
|
antelopev2_url = "https://drive.google.com/uc?id=18wEUfMNohBJ4K3Ly5wpTejPfDzp-8fI8"
|
||||||
"https://drive.google.com/uc?id=18wEUfMNohBJ4K3Ly5wpTejPfDzp-8fI8"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import gdown
|
import gdown
|
||||||
@@ -636,10 +612,7 @@ def apply_easing(value, easing_type):
|
|||||||
return 1
|
return 1
|
||||||
p = 0.3
|
p = 0.3
|
||||||
s = p / 4
|
s = p / 4
|
||||||
return -(
|
return -(math.pow(2, 10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p))
|
||||||
math.pow(2, 10 * (t - 1))
|
|
||||||
* math.sin((t - 1 - s) * (2 * math.pi) / p)
|
|
||||||
)
|
|
||||||
|
|
||||||
def easeOutElastic(t):
|
def easeOutElastic(t):
|
||||||
if t == 0:
|
if t == 0:
|
||||||
@@ -660,13 +633,10 @@ def apply_easing(value, easing_type):
|
|||||||
t = t * 2
|
t = t * 2
|
||||||
if t < 1:
|
if t < 1:
|
||||||
return -0.5 * (
|
return -0.5 * (
|
||||||
math.pow(2, 10 * (t - 1))
|
math.pow(2, 10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p)
|
||||||
* math.sin((t - 1 - s) * (2 * math.pi) / p)
|
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
0.5
|
0.5 * math.pow(2, -10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p)
|
||||||
* math.pow(2, -10 * (t - 1))
|
|
||||||
* math.sin((t - 1 - s) * (2 * math.pi) / p)
|
|
||||||
+ 1
|
+ 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ data otherwise:
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
**note +**
|
|
||||||
A basic HTML note mainly to add better looking notes/instructions for workflow makers:
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## Standalone
|
## Standalone
|
||||||
These scripts can be taken and placed independently of `comfy_mtb` or any other files, mimicking what pythongosss did for their
|
These scripts can be taken and placed independently of `comfy_mtb` or any other files, mimicking what pythongosss did for their
|
||||||
|
|||||||
+5
-142
@@ -9,67 +9,6 @@
|
|||||||
|
|
||||||
import { app } from '../../scripts/app.js'
|
import { app } from '../../scripts/app.js'
|
||||||
|
|
||||||
// - crude uuid
|
|
||||||
export function makeUUID() {
|
|
||||||
let dt = new Date().getTime()
|
|
||||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
||||||
const r = (dt + Math.random() * 16) % 16 | 0
|
|
||||||
dt = Math.floor(dt / 16)
|
|
||||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
|
||||||
})
|
|
||||||
return uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
//- local storage manager
|
|
||||||
export class LocalStorageManager {
|
|
||||||
constructor(namespace) {
|
|
||||||
this.namespace = namespace
|
|
||||||
}
|
|
||||||
|
|
||||||
_namespacedKey(key) {
|
|
||||||
return `${this.namespace}:${key}`
|
|
||||||
}
|
|
||||||
|
|
||||||
set(key, value) {
|
|
||||||
const serializedValue = JSON.stringify(value)
|
|
||||||
localStorage.setItem(this._namespacedKey(key), serializedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
get(key, default_val = null) {
|
|
||||||
const value = localStorage.getItem(this._namespacedKey(key))
|
|
||||||
return value ? JSON.parse(value) : default_val
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(key) {
|
|
||||||
localStorage.removeItem(this._namespacedKey(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
Object.keys(localStorage)
|
|
||||||
.filter((k) => k.startsWith(this.namespace + ':'))
|
|
||||||
.forEach((k) => localStorage.removeItem(k))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// - log utilities
|
|
||||||
|
|
||||||
function createLogger(emoji, color, consoleMethod = 'log') {
|
|
||||||
return function (message, ...args) {
|
|
||||||
if (window.MTB?.DEBUG) {
|
|
||||||
console[consoleMethod](
|
|
||||||
`%c${emoji} ${message}`,
|
|
||||||
`color: ${color};`,
|
|
||||||
...args,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const infoLogger = createLogger('ℹ️', 'yellow')
|
|
||||||
export const warnLogger = createLogger('⚠️', 'orange', 'warn')
|
|
||||||
export const errorLogger = createLogger('🔥', 'red', 'error')
|
|
||||||
export const successLogger = createLogger('✅', 'green')
|
|
||||||
|
|
||||||
export const log = (...args) => {
|
export const log = (...args) => {
|
||||||
if (window.MTB?.DEBUG) {
|
if (window.MTB?.DEBUG) {
|
||||||
console.debug(...args)
|
console.debug(...args)
|
||||||
@@ -109,14 +48,14 @@ export function offsetDOMWidget(
|
|||||||
node,
|
node,
|
||||||
widgetWidth,
|
widgetWidth,
|
||||||
widgetY,
|
widgetY,
|
||||||
height,
|
height
|
||||||
) {
|
) {
|
||||||
const margin = 10
|
const margin = 10
|
||||||
const elRect = ctx.canvas.getBoundingClientRect()
|
const elRect = ctx.canvas.getBoundingClientRect()
|
||||||
const transform = new DOMMatrix()
|
const transform = new DOMMatrix()
|
||||||
.scaleSelf(
|
.scaleSelf(
|
||||||
elRect.width / ctx.canvas.width,
|
elRect.width / ctx.canvas.width,
|
||||||
elRect.height / ctx.canvas.height,
|
elRect.height / ctx.canvas.height
|
||||||
)
|
)
|
||||||
.multiplySelf(ctx.getTransform())
|
.multiplySelf(ctx.getTransform())
|
||||||
.translateSelf(margin, margin + widgetY)
|
.translateSelf(margin, margin + widgetY)
|
||||||
@@ -166,7 +105,7 @@ export const setupDynamicConnections = (nodeType, prefix, inputType) => {
|
|||||||
type,
|
type,
|
||||||
index,
|
index,
|
||||||
connected,
|
connected,
|
||||||
link_info,
|
link_info
|
||||||
) {
|
) {
|
||||||
const r = onConnectionsChange
|
const r = onConnectionsChange
|
||||||
? onConnectionsChange.apply(this, arguments)
|
? onConnectionsChange.apply(this, arguments)
|
||||||
@@ -180,11 +119,8 @@ export const dynamic_connection = (
|
|||||||
connected,
|
connected,
|
||||||
connectionPrefix = 'input_',
|
connectionPrefix = 'input_',
|
||||||
connectionType = 'PSDLAYER',
|
connectionType = 'PSDLAYER',
|
||||||
nameArray = [],
|
nameArray = []
|
||||||
) => {
|
) => {
|
||||||
if (!node.inputs[index].name.startsWith(connectionPrefix)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// remove all non connected inputs
|
// remove all non connected inputs
|
||||||
if (!connected && node.inputs.length > 1) {
|
if (!connected && node.inputs.length > 1) {
|
||||||
log(`Removing input ${index} (${node.inputs[index].name})`)
|
log(`Removing input ${index} (${node.inputs[index].name})`)
|
||||||
@@ -220,25 +156,6 @@ export const dynamic_connection = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateTotalChildrenHeight(parentElement) {
|
|
||||||
let totalHeight = 0
|
|
||||||
|
|
||||||
for (const child of parentElement.children) {
|
|
||||||
const style = window.getComputedStyle(child)
|
|
||||||
|
|
||||||
// Get height as an integer (without 'px')
|
|
||||||
const height = parseInt(style.height, 10)
|
|
||||||
|
|
||||||
// Get vertical margin as integers
|
|
||||||
const marginTop = parseInt(style.marginTop, 10)
|
|
||||||
const marginBottom = parseInt(style.marginBottom, 10)
|
|
||||||
|
|
||||||
// Sum up height and vertical margins
|
|
||||||
totalHeight += height + marginTop + marginBottom
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalHeight
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Appends a callback to the extra menu options of a given node type.
|
* Appends a callback to the extra menu options of a given node type.
|
||||||
* @param {*} nodeType
|
* @param {*} nodeType
|
||||||
@@ -409,48 +326,11 @@ function getBrightness(rgbObj) {
|
|||||||
(parseInt(rgbObj[0]) * 299 +
|
(parseInt(rgbObj[0]) * 299 +
|
||||||
parseInt(rgbObj[1]) * 587 +
|
parseInt(rgbObj[1]) * 587 +
|
||||||
parseInt(rgbObj[2]) * 114) /
|
parseInt(rgbObj[2]) * 114) /
|
||||||
1000,
|
1000
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
//- HTML / CSS UTILS
|
//- HTML / CSS UTILS
|
||||||
export const loadScript = (
|
|
||||||
FILE_URL,
|
|
||||||
async = true,
|
|
||||||
type = 'text/javascript',
|
|
||||||
) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
// Check if the script already exists
|
|
||||||
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
|
|
||||||
if (existingScript) {
|
|
||||||
resolve({ status: true, message: 'Script already loaded' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const scriptEle = document.createElement('script')
|
|
||||||
scriptEle.type = type
|
|
||||||
scriptEle.async = async
|
|
||||||
scriptEle.src = FILE_URL
|
|
||||||
|
|
||||||
scriptEle.addEventListener('load', (ev) => {
|
|
||||||
resolve({ status: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
scriptEle.addEventListener('error', (ev) => {
|
|
||||||
reject({
|
|
||||||
status: false,
|
|
||||||
message: `Failed to load the script ${FILE_URL}`,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
document.body.appendChild(scriptEle)
|
|
||||||
} catch (error) {
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defineClass(className, classStyles) {
|
export function defineClass(className, classStyles) {
|
||||||
const styleSheets = document.styleSheets
|
const styleSheets = document.styleSheets
|
||||||
|
|
||||||
@@ -483,20 +363,3 @@ export function defineClass(className, classStyles) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Prefixes the node title with '[DEPRECATED]' and log the deprecation reason to the console.*/
|
|
||||||
export const addDeprecation = (nodeType, reason) => {
|
|
||||||
const title = nodeType.title
|
|
||||||
nodeType.title = '[DEPRECATED] ' + title
|
|
||||||
// console.log(nodeType)
|
|
||||||
|
|
||||||
const styles = {
|
|
||||||
title: 'font-size:1.3em;font-weight:900;color:yellow; background: black',
|
|
||||||
reason: 'font-size:1.2em',
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
`%c⚠️ ${title} is deprecated:%c ${reason}`,
|
|
||||||
styles.title,
|
|
||||||
styles.reason,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,187 +0,0 @@
|
|||||||
|
|
||||||
import { app } from '../../scripts/app.js'
|
|
||||||
|
|
||||||
function B0(t) { return (1 - t) ** 3 / 6; }
|
|
||||||
function B1(t) { return (3 * t ** 3 - 6 * t ** 2 + 4) / 6; }
|
|
||||||
function B2(t) { return (-3 * t ** 3 + 3 * t ** 2 + 3 * t + 1) / 6; }
|
|
||||||
function B3(t) { return t ** 3 / 6; }
|
|
||||||
|
|
||||||
class CurveWidget {
|
|
||||||
constructor(inputName, defaultValue) {
|
|
||||||
this.name = inputName || "Curve";
|
|
||||||
this._value = defaultValue || [{ x: 0, y: 0 }, { x: 1, y: 1 }];
|
|
||||||
this.type = "FLOAT_CURVE";
|
|
||||||
this.selectedPointIndex = null;
|
|
||||||
this.resize
|
|
||||||
}
|
|
||||||
|
|
||||||
drawBSpline(ctx, width, height, posY) {
|
|
||||||
const n = this._value.length - 1;
|
|
||||||
const numSegments = n - 2;
|
|
||||||
const numPoints = this._value.length;
|
|
||||||
if (numPoints < 4) {
|
|
||||||
this.drawLinear(ctx, width, height, posY);
|
|
||||||
} else {
|
|
||||||
for (let j = 0; j <= numSegments; j++) {
|
|
||||||
for (let t = 0; t <= 1; t += 0.01) {
|
|
||||||
let pt = this.getBSplinePoint(j, t);
|
|
||||||
let x = pt.x * width;
|
|
||||||
let y = posY + height - pt.y * height;
|
|
||||||
|
|
||||||
if (t === 0) ctx.moveTo(x, y);
|
|
||||||
else ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drawLinear(ctx, width, height, posY) {
|
|
||||||
for (let i = 0; i < this._value.length - 1; i++) {
|
|
||||||
let p1 = this._value[i];
|
|
||||||
let p2 = this._value[i + 1];
|
|
||||||
ctx.moveTo(p1.x * width, posY + height - p1.y * height);
|
|
||||||
ctx.lineTo(p2.x * width, posY + height - p2.y * height);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
getBSplinePoint(i, t) {
|
|
||||||
// Control points for this segment
|
|
||||||
const p0 = this._value[i];
|
|
||||||
const p1 = this._value[i + 1];
|
|
||||||
const p2 = this._value[i + 2];
|
|
||||||
const p3 = this._value[i + 3];
|
|
||||||
|
|
||||||
const x = B0(t) * p0.x + B1(t) * p1.x + B2(t) * p2.x + B3(t) * p3.x;
|
|
||||||
const y = B0(t) * p0.y + B1(t) * p1.y + B2(t) * p2.y + B3(t) * p3.y;
|
|
||||||
|
|
||||||
return { x, y };
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(ctx, node, width, posY, height) {
|
|
||||||
const [cw, ch] = this.computeSize(width)
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.fillStyle = "#000";
|
|
||||||
//ctx.fillRect(0, posY, cw, ch);
|
|
||||||
ctx.strokeStyle = "#fff";
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
|
|
||||||
// normalized coordinates -> canvas coordinates
|
|
||||||
for (let i = 0; i < this._value.length - 1; i++) {
|
|
||||||
let p1 = this._value[i];
|
|
||||||
let p2 = this._value[i + 1];
|
|
||||||
ctx.moveTo(p1.x * cw, posY + ch - p1.y * ch);
|
|
||||||
ctx.lineTo(p2.x * cw, posY + ch - p2.y * ch);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
// this.drawBSpline(ctx, width, height, posY);
|
|
||||||
|
|
||||||
// points
|
|
||||||
this._value.forEach(point => {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(point.x * cw, posY + ch - point.y * ch, 5, 0, 2 * Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mouse(event, pos, node) {
|
|
||||||
// console.debug(event.type, pos, node)
|
|
||||||
let x = pos[0] - node.pos[0]
|
|
||||||
let y = pos[1] - node.pos[1]
|
|
||||||
let width = node.size[0]
|
|
||||||
const height = 300; // TODO: compute
|
|
||||||
const posY = node.pos[1];
|
|
||||||
|
|
||||||
const localPos = { x: pos[0], y: pos[1] - LiteGraph.NODE_WIDGET_HEIGHT };
|
|
||||||
|
|
||||||
if (event.type === LiteGraph.pointerevents_method + "down") {
|
|
||||||
console.debug("Checking if a point was clicked");
|
|
||||||
const clickedPointIndex = this.detectPoint(localPos, width, height);
|
|
||||||
if (clickedPointIndex !== null) {
|
|
||||||
this.selectedPointIndex = clickedPointIndex;
|
|
||||||
} else {
|
|
||||||
this.addPoint(localPos, width, height);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else if (event.type === LiteGraph.pointerevents_method + "move" && this.selectedPointIndex !== null) {
|
|
||||||
this.movePoint(this.selectedPointIndex, localPos, width, height);
|
|
||||||
return true;
|
|
||||||
} else if (event.type === LiteGraph.pointerevents_method + "up" && this.selectedPointIndex !== null) {
|
|
||||||
this.selectedPointIndex = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
detectPoint(localPos, width, height) {
|
|
||||||
const threshold = 20; // TODO: extract
|
|
||||||
for (let i = 0; i < this._value.length; i++) {
|
|
||||||
const p = this._value[i];
|
|
||||||
const px = p.x * width;
|
|
||||||
const py = height - p.y * height;
|
|
||||||
if (Math.abs(localPos.x - px) < threshold && Math.abs(localPos.y - py) < threshold) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
addPoint(localPos, width, height) {
|
|
||||||
// add a new point based on click position
|
|
||||||
const normalizedPoint = { x: localPos.x / width, y: 1 - localPos.y / height };
|
|
||||||
this._value.push(normalizedPoint);
|
|
||||||
this._value.sort((a, b) => a.x - b.x);
|
|
||||||
this.value = JSON.stringify(this._value);
|
|
||||||
}
|
|
||||||
|
|
||||||
movePoint(index, localPos, width, height) {
|
|
||||||
const point = this._value[index];
|
|
||||||
point.x = Math.max(0, Math.min(1, localPos.x / width));
|
|
||||||
point.y = Math.max(0, Math.min(1, 1 - localPos.y / height));
|
|
||||||
|
|
||||||
this._value[index] = point;
|
|
||||||
this.value = JSON.stringify(this._value);
|
|
||||||
}
|
|
||||||
|
|
||||||
computeSize(width) {
|
|
||||||
return [width, 300];
|
|
||||||
}
|
|
||||||
|
|
||||||
configure(data) {
|
|
||||||
console.log(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
value() {
|
|
||||||
console.debug('Returning value', this._value)
|
|
||||||
return this._value
|
|
||||||
}
|
|
||||||
setValue(value) {
|
|
||||||
console.debug('Setting value', value)
|
|
||||||
this._value = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.registerExtension({
|
|
||||||
name: 'mtb.curves',
|
|
||||||
getCustomWidgets: function () {
|
|
||||||
return {
|
|
||||||
FLOAT_CURVE: (node, inputName, inputData, app) => {
|
|
||||||
console.debug('Registering float curve widget');
|
|
||||||
|
|
||||||
return {
|
|
||||||
widget: node.addCustomWidget(
|
|
||||||
new CurveWidget(inputName, inputData[1]?.default)
|
|
||||||
),
|
|
||||||
minWidth: 150,
|
|
||||||
minHeight: 30,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
})
|
|
||||||
+4
-5
@@ -41,7 +41,7 @@ app.registerExtension({
|
|||||||
type,
|
type,
|
||||||
index,
|
index,
|
||||||
connected,
|
connected,
|
||||||
link_info,
|
link_info
|
||||||
) {
|
) {
|
||||||
const r = onConnectionsChange
|
const r = onConnectionsChange
|
||||||
? onConnectionsChange.apply(this, arguments)
|
? onConnectionsChange.apply(this, arguments)
|
||||||
@@ -52,7 +52,7 @@ app.registerExtension({
|
|||||||
//- infer type
|
//- infer type
|
||||||
if (link_info) {
|
if (link_info) {
|
||||||
const fromNode = this.graph._nodes.find(
|
const fromNode = this.graph._nodes.find(
|
||||||
(otherNode) => otherNode.id == link_info.origin_id,
|
(otherNode) => otherNode.id == link_info.origin_id
|
||||||
)
|
)
|
||||||
const type = fromNode.outputs[link_info.origin_slot].type
|
const type = fromNode.outputs[link_info.origin_slot].type
|
||||||
this.inputs[index].type = type
|
this.inputs[index].type = type
|
||||||
@@ -63,7 +63,6 @@ app.registerExtension({
|
|||||||
this.inputs[index].type = '*'
|
this.inputs[index].type = '*'
|
||||||
this.inputs[index].label = `anything_${index + 1}`
|
this.inputs[index].label = `anything_${index + 1}`
|
||||||
}
|
}
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onExecuted = nodeType.prototype.onExecuted
|
const onExecuted = nodeType.prototype.onExecuted
|
||||||
@@ -87,7 +86,7 @@ app.registerExtension({
|
|||||||
if (message.text) {
|
if (message.text) {
|
||||||
for (const txt of message.text) {
|
for (const txt of message.text) {
|
||||||
const w = this.addCustomWidget(
|
const w = this.addCustomWidget(
|
||||||
MtbWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, escapeHtml(txt)),
|
MtbWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, escapeHtml(txt))
|
||||||
)
|
)
|
||||||
w.parent = this
|
w.parent = this
|
||||||
widgetI++
|
widgetI++
|
||||||
@@ -96,7 +95,7 @@ app.registerExtension({
|
|||||||
if (message.b64_images) {
|
if (message.b64_images) {
|
||||||
for (const img of message.b64_images) {
|
for (const img of message.b64_images) {
|
||||||
const w = this.addCustomWidget(
|
const w = this.addCustomWidget(
|
||||||
MtbWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img),
|
MtbWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img)
|
||||||
)
|
)
|
||||||
w.parent = this
|
w.parent = this
|
||||||
widgetI++
|
widgetI++
|
||||||
|
|||||||
Vendored
-3
File diff suppressed because one or more lines are too long
Vendored
-3
File diff suppressed because one or more lines are too long
+5
-24
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
import { api } from '../../scripts/api.js'
|
import { api } from '../../scripts/api.js'
|
||||||
import { app } from '../../scripts/app.js'
|
import { app } from '../../scripts/app.js'
|
||||||
import { LocalStorageManager } from "./comfy_shared.js"
|
|
||||||
const styles = {
|
const styles = {
|
||||||
lighbox: {
|
lighbox: {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
@@ -53,31 +53,11 @@ let currentImageIndex = 0
|
|||||||
const imageUrls = []
|
const imageUrls = []
|
||||||
|
|
||||||
let image_menu = null
|
let image_menu = null
|
||||||
const storage = new LocalStorageManager('mtb');
|
let activated = true
|
||||||
|
|
||||||
let activated = storage.get("image_feed", true)
|
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: 'mtb.ImageFeed',
|
name: 'mtb.ImageFeed',
|
||||||
setup: () => {
|
|
||||||
app.ui.settings.addSetting({
|
|
||||||
id: 'mtb.imageFeed.enabled',
|
|
||||||
name: '[⚡mtb] Enable image feed',
|
|
||||||
type: 'boolean',
|
|
||||||
defaultValue: true,
|
|
||||||
attrs: {
|
|
||||||
style: {
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async onChange(value) {
|
|
||||||
storage.set("image_feed", value)
|
|
||||||
activated = value
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
init: async () => {
|
init: async () => {
|
||||||
if (!activated) { return }
|
|
||||||
const pythongossFeed = app.extensions.find(
|
const pythongossFeed = app.extensions.find(
|
||||||
(e) => e.name == 'pysssss.ImageFeed'
|
(e) => e.name == 'pysssss.ImageFeed'
|
||||||
)
|
)
|
||||||
@@ -250,8 +230,9 @@ app.registerExtension({
|
|||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
})
|
})
|
||||||
|
|
||||||
img.src = `/view?filename=${encodeURIComponent(src.filename)}&type=${src.type
|
img.src = `/view?filename=${encodeURIComponent(src.filename)}&type=${
|
||||||
}&subfolder=${encodeURIComponent(src.subfolder)}`
|
src.type
|
||||||
|
}&subfolder=${encodeURIComponent(src.subfolder)}`
|
||||||
|
|
||||||
imageUrls.push(img.src)
|
imageUrls.push(img.src)
|
||||||
|
|
||||||
|
|||||||
+214
-60
@@ -7,8 +7,6 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO: Use the builtin addDOMWidget everywhere appropriate
|
|
||||||
|
|
||||||
import { app } from '../../scripts/app.js'
|
import { app } from '../../scripts/app.js'
|
||||||
import { api } from '../../scripts/api.js'
|
import { api } from '../../scripts/api.js'
|
||||||
|
|
||||||
@@ -16,13 +14,7 @@ import parseCss from './extern/parse-css.js'
|
|||||||
import * as shared from './comfy_shared.js'
|
import * as shared from './comfy_shared.js'
|
||||||
import { log } from './comfy_shared.js'
|
import { log } from './comfy_shared.js'
|
||||||
|
|
||||||
// NOTE: new widget types registered by MTB Widgets
|
const newTypes = [, /*'BOOL'*/ 'COLOR', 'BBOX', 'AUDIO_UPLOAD']
|
||||||
const newTypes = [, /*'BOOL'*/ 'COLOR', 'BBOX']
|
|
||||||
|
|
||||||
const deprecated_nodes = {
|
|
||||||
// 'Animation Builder':
|
|
||||||
// 'Kept to avoid breaking older script but replaced by TimeEngine',
|
|
||||||
}
|
|
||||||
|
|
||||||
const withFont = (ctx, font, cb) => {
|
const withFont = (ctx, font, cb) => {
|
||||||
const oldFont = ctx.font
|
const oldFont = ctx.font
|
||||||
@@ -49,10 +41,58 @@ const calculateTextDimensions = (ctx, value, width, fontSize = 16) => {
|
|||||||
const textHeight = (lines.length + 1) * fontSize
|
const textHeight = (lines.length + 1) * fontSize
|
||||||
const maxLineWidth = lines.reduce(
|
const maxLineWidth = lines.reduce(
|
||||||
(maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width),
|
(maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width),
|
||||||
0,
|
0
|
||||||
)
|
)
|
||||||
return { textHeight, maxLineWidth }
|
return { textHeight, maxLineWidth }
|
||||||
}
|
}
|
||||||
|
function addPlaybackWidget(node, name, url) {
|
||||||
|
let isTick = true
|
||||||
|
const audio = new Audio(url)
|
||||||
|
const slider = node.addWidget(
|
||||||
|
'slider',
|
||||||
|
'loading',
|
||||||
|
0,
|
||||||
|
(v) => {
|
||||||
|
if (!isTick) {
|
||||||
|
audio.currentTime = v
|
||||||
|
}
|
||||||
|
isTick = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 0,
|
||||||
|
max: 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const button = node.addWidget('button', `Play ${name}`, 'play', () => {
|
||||||
|
try {
|
||||||
|
if (audio.paused) {
|
||||||
|
audio.play()
|
||||||
|
button.name = `Pause ${name}`
|
||||||
|
} else {
|
||||||
|
audio.pause()
|
||||||
|
button.name = `Play ${name}`
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(error)
|
||||||
|
}
|
||||||
|
app.canvas.setDirty(true)
|
||||||
|
})
|
||||||
|
audio.addEventListener('timeupdate', () => {
|
||||||
|
isTick = true
|
||||||
|
slider.value = audio.currentTime
|
||||||
|
app.canvas.setDirty(true)
|
||||||
|
})
|
||||||
|
audio.addEventListener('ended', () => {
|
||||||
|
button.name = `Play ${name}`
|
||||||
|
app.canvas.setDirty(true)
|
||||||
|
})
|
||||||
|
audio.addEventListener('loadedmetadata', () => {
|
||||||
|
slider.options.max = audio.duration
|
||||||
|
slider.name = `(${audio.duration})`
|
||||||
|
app.canvas.setDirty(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const MtbWidgets = {
|
export const MtbWidgets = {
|
||||||
BBOX: (key, val) => {
|
BBOX: (key, val) => {
|
||||||
@@ -112,7 +152,7 @@ export const MtbWidgets = {
|
|||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
this.label || this.name,
|
this.label || this.name,
|
||||||
margin * 2 + 5,
|
margin * 2 + 5,
|
||||||
currentY + H * 0.7,
|
currentY + H * 0.7
|
||||||
)
|
)
|
||||||
ctx.fillStyle = text_color
|
ctx.fillStyle = text_color
|
||||||
ctx.textAlign = 'right'
|
ctx.textAlign = 'right'
|
||||||
@@ -121,10 +161,10 @@ export const MtbWidgets = {
|
|||||||
Number(this.value).toFixed(
|
Number(this.value).toFixed(
|
||||||
this.options?.precision !== undefined
|
this.options?.precision !== undefined
|
||||||
? this.options.precision
|
? this.options.precision
|
||||||
: 3,
|
: 3
|
||||||
),
|
),
|
||||||
widget_width - margin * 2 - 20,
|
widget_width - margin * 2 - 20,
|
||||||
currentY + H * 0.7,
|
currentY + H * 0.7
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,7 +246,7 @@ export const MtbWidgets = {
|
|||||||
this.value = Number(v)
|
this.value = Number(v)
|
||||||
shared.inner_value_change(this, this.value, event)
|
shared.inner_value_change(this, this.value, event)
|
||||||
}.bind(w),
|
}.bind(w),
|
||||||
event,
|
event
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,7 +256,7 @@ export const MtbWidgets = {
|
|||||||
function () {
|
function () {
|
||||||
shared.inner_value_change(this, this.value, event)
|
shared.inner_value_change(this, this.value, event)
|
||||||
}.bind(this),
|
}.bind(this),
|
||||||
20,
|
20
|
||||||
)
|
)
|
||||||
|
|
||||||
app.canvas.setDirty(true)
|
app.canvas.setDirty(true)
|
||||||
@@ -265,7 +305,7 @@ export const MtbWidgets = {
|
|||||||
border,
|
border,
|
||||||
widgetY + border,
|
widgetY + border,
|
||||||
widgetWidth - border * 2,
|
widgetWidth - border * 2,
|
||||||
height - border * 2,
|
height - border * 2
|
||||||
)
|
)
|
||||||
const color = parseCss(this.value.default || this.value)
|
const color = parseCss(this.value.default || this.value)
|
||||||
if (!color) {
|
if (!color) {
|
||||||
@@ -368,7 +408,7 @@ export const MtbWidgets = {
|
|||||||
})
|
})
|
||||||
const widgetWidth = Math.max(
|
const widgetWidth = Math.max(
|
||||||
width || this.width || 32,
|
width || this.width || 32,
|
||||||
dimensions.maxLineWidth,
|
dimensions.maxLineWidth
|
||||||
)
|
)
|
||||||
const widgetHeight = dimensions.textHeight * 1.5
|
const widgetHeight = dimensions.textHeight * 1.5
|
||||||
return [widgetWidth, widgetHeight]
|
return [widgetWidth, widgetHeight]
|
||||||
@@ -392,7 +432,7 @@ export const MtbWidgets = {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: ${fontSize}px;
|
font-size: ${fontSize}px;
|
||||||
color: var(--input-text);
|
color: var(--input-text);
|
||||||
line-height: 1em;
|
line-height: 0;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
`
|
`
|
||||||
w.value = val
|
w.value = val
|
||||||
@@ -400,6 +440,119 @@ export const MtbWidgets = {
|
|||||||
|
|
||||||
return w
|
return w
|
||||||
},
|
},
|
||||||
|
|
||||||
|
AUDIO_UPLOAD: function (name, val) {
|
||||||
|
const w = {
|
||||||
|
name,
|
||||||
|
type: 'audio_upload',
|
||||||
|
value: val,
|
||||||
|
draw: function (ctx, node, widgetWidth, widgetY, height) {
|
||||||
|
const [cw, ch] = this.computeSize(widgetWidth)
|
||||||
|
shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
|
||||||
|
},
|
||||||
|
computeSize: function (width) {
|
||||||
|
if (width) {
|
||||||
|
return [width, 64]
|
||||||
|
}
|
||||||
|
return [128, 128]
|
||||||
|
},
|
||||||
|
onRemoved: function () {
|
||||||
|
if (this.inputEl) {
|
||||||
|
this.inputEl.remove()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFile = async (file, node) => {
|
||||||
|
try {
|
||||||
|
const body = new FormData()
|
||||||
|
body.append('name', 'loadAudio')
|
||||||
|
body.append('args', file)
|
||||||
|
const loadAudio = await api.fetchApi('/mtb/actions', {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loadAudio.status === 200) {
|
||||||
|
const { result } = await loadAudio.json()
|
||||||
|
console.log('received from server', result)
|
||||||
|
console.log(
|
||||||
|
`Getting file /mtb/audio?filename=${encodeURIComponent(
|
||||||
|
result.name
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
|
||||||
|
w.value = result.name
|
||||||
|
addPlaybackWidget(
|
||||||
|
node,
|
||||||
|
result.name,
|
||||||
|
`/mtb/audio?filename=${encodeURIComponent(result.name)}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
alert(loadAudio.status + ' -' + loadAudio.statusText)
|
||||||
|
}
|
||||||
|
// if (resp.status === 200) {
|
||||||
|
// const { name } = await resp.json()
|
||||||
|
// pathWidget.value = name
|
||||||
|
// addPlaybackWidget(
|
||||||
|
// node,
|
||||||
|
// name,
|
||||||
|
// `/samplediffusion/audio?filename=${encodeURIComponent(name)}`
|
||||||
|
// )
|
||||||
|
// } else {
|
||||||
|
// alert(resp.status + ' - ' + resp.statusText)
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
alert(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.inputEl = document.createElement('div')
|
||||||
|
const hidden_input = document.createElement('input')
|
||||||
|
const label = document.createElement('label')
|
||||||
|
|
||||||
|
const uniqueId = 'input_' + Date.now()
|
||||||
|
Object.assign(hidden_input, {
|
||||||
|
type: 'file',
|
||||||
|
accept: 'audio/mpeg,audio/wav,audio/x-wav',
|
||||||
|
id: uniqueId,
|
||||||
|
style: `
|
||||||
|
width: 0.1px;
|
||||||
|
height: 0.1px;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
`,
|
||||||
|
onchange: async () => {
|
||||||
|
if (hidden_input.files.length) {
|
||||||
|
console.log(hidden_input.files[0])
|
||||||
|
await uploadFile(hidden_input.files[0], this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.assign(label, {
|
||||||
|
htmlFor: uniqueId,
|
||||||
|
})
|
||||||
|
label.textContent = 'Upload Audio File'
|
||||||
|
label.style = `
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: monospace;
|
||||||
|
padding:0.5em;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: white;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
display: inline-block;
|
||||||
|
`
|
||||||
|
document.body.appendChild(w.inputEl)
|
||||||
|
w.inputEl.appendChild(hidden_input)
|
||||||
|
w.inputEl.appendChild(label)
|
||||||
|
return w
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -425,7 +578,7 @@ const mtb_widgets = {
|
|||||||
setup: () => {
|
setup: () => {
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id: 'mtb.Debug.enabled',
|
id: 'mtb.Debug.enabled',
|
||||||
name: '[⚡mtb] Enable Debug (py and js)',
|
name: '[mtb] Enable Debug (py and js)',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
|
|
||||||
@@ -466,7 +619,7 @@ const mtb_widgets = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
widget: node.addCustomWidget(
|
widget: node.addCustomWidget(
|
||||||
MtbWidgets.BOOL(inputName, inputData[1]?.default || false),
|
MtbWidgets.BOOL(inputName, inputData[1]?.default || false)
|
||||||
),
|
),
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
minHeight: 30,
|
minHeight: 30,
|
||||||
@@ -477,12 +630,26 @@ const mtb_widgets = {
|
|||||||
console.debug('Registering color')
|
console.debug('Registering color')
|
||||||
return {
|
return {
|
||||||
widget: node.addCustomWidget(
|
widget: node.addCustomWidget(
|
||||||
MtbWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000'),
|
MtbWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000')
|
||||||
),
|
),
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
minHeight: 30,
|
minHeight: 30,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
AUDIO_UPLOAD: (node, inputName, inputData, app) => {
|
||||||
|
console.debug('Registering audio')
|
||||||
|
return {
|
||||||
|
widget: node.addCustomWidget(
|
||||||
|
MtbWidgets.AUDIO_UPLOAD.bind(node)(
|
||||||
|
inputName,
|
||||||
|
inputData[1]?.default || ''
|
||||||
|
)
|
||||||
|
),
|
||||||
|
minWidth: 150,
|
||||||
|
minHeight: 30,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// BBOX: (node, inputName, inputData, app) => {
|
// BBOX: (node, inputName, inputData, app) => {
|
||||||
// console.debug("Registering bbox")
|
// console.debug("Registering bbox")
|
||||||
// return {
|
// return {
|
||||||
@@ -502,6 +669,10 @@ const mtb_widgets = {
|
|||||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
// const rinputs = nodeData.input?.required
|
// const rinputs = nodeData.input?.required
|
||||||
|
|
||||||
|
if (!nodeData.name.endsWith('(mtb)')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let has_custom = false
|
let has_custom = false
|
||||||
if (nodeData.input && nodeData.input.required) {
|
if (nodeData.input && nodeData.input.required) {
|
||||||
for (const i of Object.keys(nodeData.input.required)) {
|
for (const i of Object.keys(nodeData.input.required)) {
|
||||||
@@ -569,14 +740,6 @@ const mtb_widgets = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nodeData.name.endsWith('(mtb)')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const deprecation = deprecated_nodes[nodeData.name.replace(' (mtb)', '')]
|
|
||||||
|
|
||||||
if (deprecation) {
|
|
||||||
shared.addDeprecation(nodeType, deprecation)
|
|
||||||
}
|
|
||||||
//- Extending Python Nodes
|
//- Extending Python Nodes
|
||||||
switch (nodeData.name) {
|
switch (nodeData.name) {
|
||||||
case 'Psd Save (mtb)': {
|
case 'Psd Save (mtb)': {
|
||||||
@@ -585,7 +748,7 @@ const mtb_widgets = {
|
|||||||
type,
|
type,
|
||||||
index,
|
index,
|
||||||
connected,
|
connected,
|
||||||
link_info,
|
link_info
|
||||||
) {
|
) {
|
||||||
const r = onConnectionsChange
|
const r = onConnectionsChange
|
||||||
? onConnectionsChange.apply(this, arguments)
|
? onConnectionsChange.apply(this, arguments)
|
||||||
@@ -603,7 +766,7 @@ const mtb_widgets = {
|
|||||||
? onNodeCreated.apply(this, arguments)
|
? onNodeCreated.apply(this, arguments)
|
||||||
: undefined
|
: undefined
|
||||||
const internal_count = this.widgets.find(
|
const internal_count = this.widgets.find(
|
||||||
(w) => w.name === 'internal_count',
|
(w) => w.name === 'internal_count'
|
||||||
)
|
)
|
||||||
shared.hideWidgetForGood(this, internal_count)
|
shared.hideWidgetForGood(this, internal_count)
|
||||||
internal_count.afterQueued = function () {
|
internal_count.afterQueued = function () {
|
||||||
@@ -643,24 +806,24 @@ const mtb_widgets = {
|
|||||||
imgURLs = imgURLs.concat(
|
imgURLs = imgURLs.concat(
|
||||||
message.gif.map((params) => {
|
message.gif.map((params) => {
|
||||||
return api.apiURL(
|
return api.apiURL(
|
||||||
'/view?' + new URLSearchParams(params).toString(),
|
'/view?' + new URLSearchParams(params).toString()
|
||||||
)
|
)
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (message.apng) {
|
if (message.apng) {
|
||||||
imgURLs = imgURLs.concat(
|
imgURLs = imgURLs.concat(
|
||||||
message.apng.map((params) => {
|
message.apng.map((params) => {
|
||||||
return api.apiURL(
|
return api.apiURL(
|
||||||
'/view?' + new URLSearchParams(params).toString(),
|
'/view?' + new URLSearchParams(params).toString()
|
||||||
)
|
)
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
let i = 0
|
let i = 0
|
||||||
for (const img of imgURLs) {
|
for (const img of imgURLs) {
|
||||||
const w = this.addCustomWidget(
|
const w = this.addCustomWidget(
|
||||||
MtbWidgets.DEBUG_IMG(`${prefix}_${i}`, img),
|
MtbWidgets.DEBUG_IMG(`${prefix}_${i}`, img)
|
||||||
)
|
)
|
||||||
w.parent = this
|
w.parent = this
|
||||||
i++
|
i++
|
||||||
@@ -688,12 +851,12 @@ const mtb_widgets = {
|
|||||||
this.changeMode(LiteGraph.ALWAYS)
|
this.changeMode(LiteGraph.ALWAYS)
|
||||||
|
|
||||||
const raw_iteration = this.widgets.find(
|
const raw_iteration = this.widgets.find(
|
||||||
(w) => w.name === 'raw_iteration',
|
(w) => w.name === 'raw_iteration'
|
||||||
)
|
)
|
||||||
const raw_loop = this.widgets.find((w) => w.name === 'raw_loop')
|
const raw_loop = this.widgets.find((w) => w.name === 'raw_loop')
|
||||||
|
|
||||||
const total_frames = this.widgets.find(
|
const total_frames = this.widgets.find(
|
||||||
(w) => w.name === 'total_frames',
|
(w) => w.name === 'total_frames'
|
||||||
)
|
)
|
||||||
const loop_count = this.widgets.find((w) => w.name === 'loop_count')
|
const loop_count = this.widgets.find((w) => w.name === 'loop_count')
|
||||||
|
|
||||||
@@ -703,12 +866,12 @@ const mtb_widgets = {
|
|||||||
raw_iteration._value = 0
|
raw_iteration._value = 0
|
||||||
|
|
||||||
const value_preview = this.addCustomWidget(
|
const value_preview = this.addCustomWidget(
|
||||||
MtbWidgets['DEBUG_STRING']('value_preview', 'Idle'),
|
MtbWidgets['DEBUG_STRING']('value_preview', 'Idle')
|
||||||
)
|
)
|
||||||
value_preview.parent = this
|
value_preview.parent = this
|
||||||
|
|
||||||
const loop_preview = this.addCustomWidget(
|
const loop_preview = this.addCustomWidget(
|
||||||
MtbWidgets['DEBUG_STRING']('loop_preview', 'Iteration: Idle'),
|
MtbWidgets['DEBUG_STRING']('loop_preview', 'Iteration: Idle')
|
||||||
)
|
)
|
||||||
loop_preview.parent = this
|
loop_preview.parent = this
|
||||||
|
|
||||||
@@ -726,7 +889,7 @@ const mtb_widgets = {
|
|||||||
'button',
|
'button',
|
||||||
`Reset`,
|
`Reset`,
|
||||||
'reset',
|
'reset',
|
||||||
onReset,
|
onReset
|
||||||
)
|
)
|
||||||
|
|
||||||
const run_button = this.addWidget('button', `Queue`, 'queue', () => {
|
const run_button = this.addWidget('button', `Queue`, 'queue', () => {
|
||||||
@@ -736,7 +899,7 @@ const mtb_widgets = {
|
|||||||
`Started a queue of ${total_frames.value} frames (for ${
|
`Started a queue of ${total_frames.value} frames (for ${
|
||||||
loop_count.value
|
loop_count.value
|
||||||
} loop, so ${total_frames.value * loop_count.value})`,
|
} loop, so ${total_frames.value * loop_count.value})`,
|
||||||
5000,
|
5000
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -773,7 +936,7 @@ const mtb_widgets = {
|
|||||||
type,
|
type,
|
||||||
index,
|
index,
|
||||||
connected,
|
connected,
|
||||||
link_info,
|
link_info
|
||||||
) {
|
) {
|
||||||
const r = onConnectionsChange
|
const r = onConnectionsChange
|
||||||
? onConnectionsChange.apply(this, arguments)
|
? onConnectionsChange.apply(this, arguments)
|
||||||
@@ -794,7 +957,7 @@ const mtb_widgets = {
|
|||||||
const input = this.addInput(
|
const input = this.addInput(
|
||||||
`replacement_${this.widgets.length}`,
|
`replacement_${this.widgets.length}`,
|
||||||
'STRING',
|
'STRING',
|
||||||
'',
|
''
|
||||||
)
|
)
|
||||||
console.log(input)
|
console.log(input)
|
||||||
this.addWidget('STRING', `replacement_${this.widgets.length}`, '')
|
this.addWidget('STRING', `replacement_${this.widgets.length}`, '')
|
||||||
@@ -811,7 +974,7 @@ const mtb_widgets = {
|
|||||||
'remove',
|
'remove',
|
||||||
function (value, widget, node) {
|
function (value, widget, node) {
|
||||||
console.log(`Button clicked: ${value}`, widget, node)
|
console.log(`Button clicked: ${value}`, widget, node)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
@@ -852,7 +1015,7 @@ const mtb_widgets = {
|
|||||||
if (style && style.length >= 1) {
|
if (style && style.length >= 1) {
|
||||||
if (style[0]) {
|
if (style[0]) {
|
||||||
window.MTB?.notify?.(
|
window.MTB?.notify?.(
|
||||||
`Extracted positive from ${this.widgets[0].value}`,
|
`Extracted positive from ${this.widgets[0].value}`
|
||||||
)
|
)
|
||||||
const tn = LiteGraph.createNode('Text box')
|
const tn = LiteGraph.createNode('Text box')
|
||||||
app.graph.add(tn)
|
app.graph.add(tn)
|
||||||
@@ -860,7 +1023,7 @@ const mtb_widgets = {
|
|||||||
tn.widgets[0].value = style[0]
|
tn.widgets[0].value = style[0]
|
||||||
} else {
|
} else {
|
||||||
window.MTB?.notify?.(
|
window.MTB?.notify?.(
|
||||||
`No positive to extract for ${this.widgets[0].value}`,
|
`No positive to extract for ${this.widgets[0].value}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -873,7 +1036,7 @@ const mtb_widgets = {
|
|||||||
if (style && style.length >= 2) {
|
if (style && style.length >= 2) {
|
||||||
if (style[1]) {
|
if (style[1]) {
|
||||||
window.MTB?.notify?.(
|
window.MTB?.notify?.(
|
||||||
`Extracted negative from ${this.widgets[0].value}`,
|
`Extracted negative from ${this.widgets[0].value}`
|
||||||
)
|
)
|
||||||
const tn = LiteGraph.createNode('Text box')
|
const tn = LiteGraph.createNode('Text box')
|
||||||
app.graph.add(tn)
|
app.graph.add(tn)
|
||||||
@@ -881,7 +1044,7 @@ const mtb_widgets = {
|
|||||||
tn.widgets[0].value = style[1]
|
tn.widgets[0].value = style[1]
|
||||||
} else {
|
} else {
|
||||||
window.MTB.notify(
|
window.MTB.notify(
|
||||||
`No negative to extract for ${this.widgets[0].value}`,
|
`No negative to extract for ${this.widgets[0].value}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -893,14 +1056,6 @@ const mtb_widgets = {
|
|||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'Apply Text Template (mtb)': {
|
|
||||||
shared.setupDynamicConnections(nodeType, 'var', '*')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'Add To Playlist (mtb)': {
|
|
||||||
shared.setupDynamicConnections(nodeType, 'video', 'VIDEO')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'Stack Images (mtb)':
|
case 'Stack Images (mtb)':
|
||||||
case 'Concat Images (mtb)': {
|
case 'Concat Images (mtb)': {
|
||||||
shared.setupDynamicConnections(nodeType, 'image', 'IMAGE')
|
shared.setupDynamicConnections(nodeType, 'image', 'IMAGE')
|
||||||
@@ -917,7 +1072,6 @@ const mtb_widgets = {
|
|||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// TODO: remove this, recommend pythongoss's version that is much better
|
|
||||||
case 'Math Expression (mtb)': {
|
case 'Math Expression (mtb)': {
|
||||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
@@ -933,7 +1087,7 @@ const mtb_widgets = {
|
|||||||
type,
|
type,
|
||||||
index,
|
index,
|
||||||
connected,
|
connected,
|
||||||
link_info,
|
link_info
|
||||||
) {
|
) {
|
||||||
const r = onConnectionsChange
|
const r = onConnectionsChange
|
||||||
? onConnectionsChange.apply(this, arguments)
|
? onConnectionsChange.apply(this, arguments)
|
||||||
@@ -947,7 +1101,7 @@ const mtb_widgets = {
|
|||||||
//- infer type
|
//- infer type
|
||||||
if (link_info) {
|
if (link_info) {
|
||||||
const fromNode = this.graph._nodes.find(
|
const fromNode = this.graph._nodes.find(
|
||||||
(otherNode) => otherNode.id == link_info.origin_id,
|
(otherNode) => otherNode.id == link_info.origin_id
|
||||||
)
|
)
|
||||||
const type = fromNode.outputs[link_info.origin_slot].type
|
const type = fromNode.outputs[link_info.origin_slot].type
|
||||||
this.inputs[index].type = type
|
this.inputs[index].type = type
|
||||||
|
|||||||
@@ -1,697 +0,0 @@
|
|||||||
import { app } from '../../scripts/app.js'
|
|
||||||
import * as shared from './comfy_shared.js'
|
|
||||||
import { infoLogger, successLogger, errorLogger } from './comfy_shared.js'
|
|
||||||
|
|
||||||
const DEFAULT_CSS = ''
|
|
||||||
const DEFAULT_HTML = `<p style='color:red;font-family:monospace'>
|
|
||||||
Note+
|
|
||||||
</p>`
|
|
||||||
const DEFAULT_MD = '## Note+'
|
|
||||||
const DEFAULT_MODE = 'markdown'
|
|
||||||
const DEFAULT_THEME = 'one_dark'
|
|
||||||
|
|
||||||
const CSS_RESET = `
|
|
||||||
* {
|
|
||||||
font-family: monospace;
|
|
||||||
line-height: 1.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
p, ul, ol, dl, blockquote {
|
|
||||||
margin: 0.3em;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ul, ol {
|
|
||||||
|
|
||||||
padding-left: 1em;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
pointer-events: all;
|
|
||||||
color: cyan;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
padding: 1em 0;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border:none;
|
|
||||||
pointer-events:all;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
border-left: 4px solid #ccc;
|
|
||||||
padding-left: 1em;
|
|
||||||
margin-left: 0;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre, code {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 1px solid #000;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
border-left: 1px solid #000;
|
|
||||||
border-right: 1px solid #000;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
border: 1px solid #000;
|
|
||||||
|
|
||||||
background-color: rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
const themes = [
|
|
||||||
'ambiance',
|
|
||||||
'chaos',
|
|
||||||
'chrome',
|
|
||||||
'cloud9_day',
|
|
||||||
'cloud9_night',
|
|
||||||
'cloud9_night_low_color',
|
|
||||||
'cloud_editor',
|
|
||||||
'cloud_editor_dark',
|
|
||||||
'clouds',
|
|
||||||
'clouds_midnight',
|
|
||||||
'cobalt',
|
|
||||||
'crimson_editor',
|
|
||||||
'dawn',
|
|
||||||
'dracula',
|
|
||||||
'dreamweaver',
|
|
||||||
'eclipse',
|
|
||||||
'github',
|
|
||||||
'github_dark',
|
|
||||||
'gob',
|
|
||||||
'gruvbox',
|
|
||||||
'gruvbox_dark_hard',
|
|
||||||
'gruvbox_light_hard',
|
|
||||||
'idle_fingers',
|
|
||||||
'iplastic',
|
|
||||||
'katzenmilch',
|
|
||||||
'kr_theme',
|
|
||||||
'kuroir',
|
|
||||||
'merbivore',
|
|
||||||
'merbivore_soft',
|
|
||||||
'mono_industrial',
|
|
||||||
'monokai',
|
|
||||||
'nord_dark',
|
|
||||||
'one_dark',
|
|
||||||
'pastel_on_dark',
|
|
||||||
'solarized_dark',
|
|
||||||
'solarized_light',
|
|
||||||
'sqlserver',
|
|
||||||
'terminal',
|
|
||||||
'textmate',
|
|
||||||
'tomorrow',
|
|
||||||
'tomorrow_night',
|
|
||||||
'tomorrow_night_blue',
|
|
||||||
'tomorrow_night_bright',
|
|
||||||
'tomorrow_night_eighties',
|
|
||||||
'twilight',
|
|
||||||
'vibrant_ink',
|
|
||||||
'vscode',
|
|
||||||
]
|
|
||||||
class NotePlus extends LiteGraph.LGraphNode {
|
|
||||||
title = 'Note+ (mtb)'
|
|
||||||
category = 'mtb/utils'
|
|
||||||
|
|
||||||
// same values as the comfy note
|
|
||||||
color = LGraphCanvas.node_colors.yellow.color
|
|
||||||
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
|
||||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
this.uuid = shared.makeUUID()
|
|
||||||
|
|
||||||
infoLogger('Constructing Note+ instance')
|
|
||||||
// - litegraph settings
|
|
||||||
this.collapsable = true
|
|
||||||
this.isVirtualNode = true
|
|
||||||
this.shape = LiteGraph.BOX_SHAPE
|
|
||||||
this.serialize_widgets = true
|
|
||||||
|
|
||||||
// - default values, serialization is done through widgets
|
|
||||||
this._raw_html = DEFAULT_MODE === 'html' ? DEFAULT_HTML : DEFAULT_MD
|
|
||||||
|
|
||||||
// - mardown converter
|
|
||||||
this.markdownConverter = new showdown.Converter({
|
|
||||||
tables: true,
|
|
||||||
strikethrough: true,
|
|
||||||
emoji: true,
|
|
||||||
ghCodeBlocks: true,
|
|
||||||
tasklists: true,
|
|
||||||
ghMentions: true,
|
|
||||||
smoothLivePreview: true,
|
|
||||||
simplifiedAutoLink: true,
|
|
||||||
parseImgDimensions: true,
|
|
||||||
openLinksInNewWindow: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// - state
|
|
||||||
this.live = true
|
|
||||||
this.calculated_height = 0
|
|
||||||
|
|
||||||
// - add widgets
|
|
||||||
const inner = document.createElement('div')
|
|
||||||
inner.style.margin = '0'
|
|
||||||
inner.style.padding = '0'
|
|
||||||
inner.style.pointerEvents = 'none'
|
|
||||||
this.html_widget = this.addDOMWidget('HTML', 'html', inner, {
|
|
||||||
setValue: (val) => {
|
|
||||||
this._raw_html = val
|
|
||||||
},
|
|
||||||
getValue: () => this._raw_html,
|
|
||||||
getMinHeight: () => this.calculated_height, // (the edit button),
|
|
||||||
hideOnZoom: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setupSerializationWidgets()
|
|
||||||
this.setupDialog()
|
|
||||||
this.loadAceEditor()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {CanvasRenderingContext2D} ctx
|
|
||||||
* @param {LGraphCanvas} graphcanvas
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
|
|
||||||
onDrawForeground(ctx, _graphcanvas) {
|
|
||||||
if (this.flags.collapsed) return
|
|
||||||
|
|
||||||
// Define the size and position of the icon
|
|
||||||
const iconSize = 14 // Size of the icon
|
|
||||||
const iconMargin = 8 // Margin from the edges
|
|
||||||
const x = this.size[0] - iconSize - iconMargin
|
|
||||||
const y = iconMargin * 1.5
|
|
||||||
|
|
||||||
// Create a new Path2D object from SVG path data
|
|
||||||
const pencilPath = new Path2D(
|
|
||||||
'M21.28 6.4l-9.54 9.54c-.95.95-3.77 1.39-4.4.76-.63-.63-.2-3.45.75-4.4l9.55-9.55a2.58 2.58 0 1 1 3.64 3.65z',
|
|
||||||
)
|
|
||||||
const folderPath = new Path2D(
|
|
||||||
'M11 4H6a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h11c2.21 0 3-1.8 3-4v-5',
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw the paths
|
|
||||||
ctx.save()
|
|
||||||
ctx.translate(x, y) // Position the icon on the canvas
|
|
||||||
ctx.scale(iconSize / 32, iconSize / 32) // Scale the icon to the desired size
|
|
||||||
ctx.strokeStyle = 'rgba(255,255,255,0.3)'
|
|
||||||
|
|
||||||
ctx.lineCap = 'round'
|
|
||||||
ctx.lineJoin = 'round'
|
|
||||||
|
|
||||||
ctx.lineWidth = 2.4
|
|
||||||
ctx.stroke(pencilPath)
|
|
||||||
ctx.stroke(folderPath)
|
|
||||||
ctx.restore()
|
|
||||||
}
|
|
||||||
onMouseDown(_e, localPos, _graphcanvas) {
|
|
||||||
// Check if the click is within the pencil icon bounds
|
|
||||||
const iconSize = 14
|
|
||||||
const iconMargin = 8
|
|
||||||
const iconX = this.size[0] - iconSize - iconMargin
|
|
||||||
const iconY = iconMargin * 1.5
|
|
||||||
|
|
||||||
if (
|
|
||||||
localPos[0] > iconX &&
|
|
||||||
localPos[0] < iconX + iconSize &&
|
|
||||||
localPos[1] > iconY &&
|
|
||||||
localPos[1] < iconY + iconSize
|
|
||||||
) {
|
|
||||||
// Pencil icon was clicked, open the editor
|
|
||||||
this.openEditorDialog()
|
|
||||||
return true // Return true to indicate the event was handled
|
|
||||||
}
|
|
||||||
|
|
||||||
return false // Return false to let the event propagate
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSerializationWidgets() {
|
|
||||||
infoLogger('Setup Serializing widgets')
|
|
||||||
|
|
||||||
this.edit_mode_widget = this.addWidget(
|
|
||||||
'combo',
|
|
||||||
'Mode',
|
|
||||||
DEFAULT_MODE,
|
|
||||||
(me) => successLogger('Updating edit_mode', me),
|
|
||||||
{
|
|
||||||
values: ['html', 'markdown', 'raw'],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
this.css_widget = this.addWidget('text', 'CSS', DEFAULT_CSS, (val) => {
|
|
||||||
successLogger(`Updating css ${val}`)
|
|
||||||
})
|
|
||||||
this.theme_widget = this.addWidget(
|
|
||||||
'text',
|
|
||||||
'Theme',
|
|
||||||
DEFAULT_THEME,
|
|
||||||
(val) => {
|
|
||||||
successLogger(`Setting theme ${val}`)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
shared.hideWidgetForGood(this, this.edit_mode_widget)
|
|
||||||
shared.hideWidgetForGood(this, this.css_widget)
|
|
||||||
shared.hideWidgetForGood(this, this.theme_widget)
|
|
||||||
}
|
|
||||||
setupDialog() {
|
|
||||||
infoLogger('Setup dialog')
|
|
||||||
// this.addWidget('button', 'Edit', 'Edit', this.openEditorDialog.bind(this))
|
|
||||||
|
|
||||||
this.dialog = new app.ui.dialog.constructor()
|
|
||||||
this.dialog.element.classList.add('comfy-settings')
|
|
||||||
|
|
||||||
const closeButton = this.dialog.element.querySelector('button')
|
|
||||||
closeButton.textContent = 'CANCEL'
|
|
||||||
const saveButton = document.createElement('button')
|
|
||||||
saveButton.textContent = 'SAVE'
|
|
||||||
saveButton.onclick = () => {
|
|
||||||
this.closeEditorDialog(true)
|
|
||||||
}
|
|
||||||
closeButton.onclick = () => {
|
|
||||||
this.closeEditorDialog(false)
|
|
||||||
}
|
|
||||||
closeButton.before(saveButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
teardownEditors() {
|
|
||||||
this.css_editor.destroy()
|
|
||||||
this.css_editor.container.remove()
|
|
||||||
|
|
||||||
this.html_editor.destroy()
|
|
||||||
this.html_editor.container.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
closeEditorDialog(accept) {
|
|
||||||
infoLogger('Closing editor dialog', accept)
|
|
||||||
if (accept) {
|
|
||||||
this.updateHTML(this.html_editor.getValue())
|
|
||||||
this.updateCSS(this.css_editor.getValue())
|
|
||||||
}
|
|
||||||
this.teardownEditors()
|
|
||||||
this.dialog.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
openEditorDialog() {
|
|
||||||
infoLogger(`Current edit mode ${this.edit_mode_widget.value}`)
|
|
||||||
const container = document.createElement('div')
|
|
||||||
|
|
||||||
Object.assign(container.style, {
|
|
||||||
display: 'flex',
|
|
||||||
gap: '10px',
|
|
||||||
flexDirection: 'column',
|
|
||||||
})
|
|
||||||
|
|
||||||
const editorsContainer = document.createElement('div')
|
|
||||||
Object.assign(editorsContainer.style, {
|
|
||||||
display: 'flex',
|
|
||||||
gap: '10px',
|
|
||||||
flexDirection: 'row',
|
|
||||||
})
|
|
||||||
|
|
||||||
container.append(editorsContainer)
|
|
||||||
|
|
||||||
this.dialog.show('')
|
|
||||||
this.dialog.textElement.append(container)
|
|
||||||
|
|
||||||
const aceHTML = document.createElement('div')
|
|
||||||
aceHTML.id = 'noteplus-html-editor'
|
|
||||||
Object.assign(aceHTML.style, {
|
|
||||||
width: '300px',
|
|
||||||
height: '300px',
|
|
||||||
// backgroundColor: 'rgb(30,30,30)',
|
|
||||||
// color: 'whitesmoke',
|
|
||||||
})
|
|
||||||
|
|
||||||
editorsContainer.append(aceHTML)
|
|
||||||
|
|
||||||
const aceCSS = document.createElement('div')
|
|
||||||
aceCSS.id = 'noteplus-css-editor'
|
|
||||||
Object.assign(aceCSS.style, {
|
|
||||||
width: '300px',
|
|
||||||
height: '300px',
|
|
||||||
// backgroundColor: 'rgb(30,30,30)',
|
|
||||||
// color: 'whitesmoke',
|
|
||||||
})
|
|
||||||
|
|
||||||
editorsContainer.append(aceCSS)
|
|
||||||
|
|
||||||
const live_edit = document.createElement('input')
|
|
||||||
live_edit.type = 'checkbox'
|
|
||||||
live_edit.checked = this.live
|
|
||||||
live_edit.onchange = () => {
|
|
||||||
this.live = live_edit.checked
|
|
||||||
}
|
|
||||||
|
|
||||||
//- "Dynamic" elements
|
|
||||||
const firstButton = this.dialog.element.querySelector('button')
|
|
||||||
const syncUI = () => {
|
|
||||||
let convert_to_html =
|
|
||||||
this.dialog.element.querySelector('#convert-to-html')
|
|
||||||
if (this.edit_mode_widget.value === 'markdown') {
|
|
||||||
if (convert_to_html == null) {
|
|
||||||
convert_to_html = document.createElement('button')
|
|
||||||
convert_to_html.textContent = 'Convert to HTML (NO UNDO!)'
|
|
||||||
convert_to_html.id = 'convert-to-html'
|
|
||||||
convert_to_html.onclick = () => {
|
|
||||||
const select_mode = this.dialog.element.querySelector('#edit_mode')
|
|
||||||
|
|
||||||
const md = this.html_editor.getValue()
|
|
||||||
this.edit_mode_widget.value = 'html'
|
|
||||||
select_mode.value = 'html'
|
|
||||||
const html = this.markdownConverter.makeHtml(md)
|
|
||||||
this.html_widget.value = html
|
|
||||||
this.html_editor.setValue(html)
|
|
||||||
this.html_editor.session.setMode('ace/mode/html')
|
|
||||||
this.updateHTML(this.html_widget.value)
|
|
||||||
|
|
||||||
convert_to_html.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
firstButton.before(convert_to_html)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (convert_to_html != null) {
|
|
||||||
convert_to_html.remove()
|
|
||||||
convert_to_html = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select_mode.value = this.edit_mode_widget.value
|
|
||||||
}
|
|
||||||
//- combobox
|
|
||||||
let theme_select = this.dialog.element.querySelector('#theme_select')
|
|
||||||
if (!theme_select) {
|
|
||||||
infoLogger('Creating combobox for select')
|
|
||||||
theme_select = document.createElement('select')
|
|
||||||
theme_select.name = 'theme'
|
|
||||||
theme_select.id = 'theme_select'
|
|
||||||
|
|
||||||
const addOption = (label) => {
|
|
||||||
const option = document.createElement('option')
|
|
||||||
option.value = label
|
|
||||||
option.textContent = label
|
|
||||||
theme_select.append(option)
|
|
||||||
}
|
|
||||||
for (const t of themes) {
|
|
||||||
addOption(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
theme_select.addEventListener('change', (event) => {
|
|
||||||
const val = event.target.value
|
|
||||||
this.setTheme(val)
|
|
||||||
})
|
|
||||||
|
|
||||||
container.prepend(theme_select)
|
|
||||||
}
|
|
||||||
|
|
||||||
theme_select.value = this.theme_widget.value
|
|
||||||
|
|
||||||
let select_mode = this.dialog.element.querySelector('#edit_mode')
|
|
||||||
|
|
||||||
if (!select_mode) {
|
|
||||||
infoLogger('Creating combobox for select')
|
|
||||||
select_mode = document.createElement('select')
|
|
||||||
select_mode.name = 'mode'
|
|
||||||
select_mode.id = 'edit_mode'
|
|
||||||
|
|
||||||
const addOption = (label) => {
|
|
||||||
const option = document.createElement('option')
|
|
||||||
option.value = label
|
|
||||||
option.textContent = label
|
|
||||||
select_mode.append(option)
|
|
||||||
}
|
|
||||||
addOption('markdown')
|
|
||||||
addOption('html')
|
|
||||||
|
|
||||||
select_mode.addEventListener('change', (event) => {
|
|
||||||
const val = event.target.value
|
|
||||||
this.edit_mode_widget.value = val
|
|
||||||
if (this.html_editor) {
|
|
||||||
this.html_editor.session.setMode(`ace/mode/${val}`)
|
|
||||||
this.updateHTML(this.html_editor.getValue())
|
|
||||||
|
|
||||||
syncUI()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
container.append(select_mode)
|
|
||||||
}
|
|
||||||
select_mode.value = this.edit_mode_widget.value
|
|
||||||
|
|
||||||
syncUI()
|
|
||||||
|
|
||||||
const live_edit_label = document.createElement('label')
|
|
||||||
live_edit_label.textContent = 'Live Edit'
|
|
||||||
|
|
||||||
// add a tooltip
|
|
||||||
live_edit_label.title =
|
|
||||||
'When this is on, the editor will update the note+ whenever you change the text.'
|
|
||||||
|
|
||||||
live_edit_label.append(live_edit)
|
|
||||||
|
|
||||||
// select_mode.before(live_edit_label)
|
|
||||||
container.append(live_edit_label)
|
|
||||||
|
|
||||||
this.setupEditors()
|
|
||||||
}
|
|
||||||
loadAceEditor() {
|
|
||||||
shared
|
|
||||||
.loadScript(
|
|
||||||
'https://cdn.jsdelivr.net/npm/ace-builds@1.16.0/src-min-noconflict/ace.min.js',
|
|
||||||
)
|
|
||||||
.catch((e) => {
|
|
||||||
errorLogger(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onCreate() {
|
|
||||||
errorLogger('NotePlus onCreate')
|
|
||||||
}
|
|
||||||
configure(info) {
|
|
||||||
super.configure(info)
|
|
||||||
infoLogger('Restoring serialized values', info)
|
|
||||||
// - update view from serialzed data
|
|
||||||
this.html_widget.element.id = `note-plus-${this.uuid}`
|
|
||||||
this.setMode(this.edit_mode_widget.value)
|
|
||||||
this.setTheme(this.theme_widget.value)
|
|
||||||
this.updateHTML(this.html_widget.value)
|
|
||||||
this.updateCSS(this.css_widget.value)
|
|
||||||
this.setSize(info.size)
|
|
||||||
}
|
|
||||||
onNodeCreated() {
|
|
||||||
infoLogger('Node created', this.uuid)
|
|
||||||
this.html_widget.element.id = `note-plus-${this.uuid}`
|
|
||||||
this.setMode(this.edit_mode_widget.value)
|
|
||||||
this.setTheme(this.theme_widget.value)
|
|
||||||
this.updateHTML(this.html_widget.value) // widget is populated here since we called super
|
|
||||||
this.updateCSS(this.css_widget.value)
|
|
||||||
}
|
|
||||||
onRemoved() {
|
|
||||||
infoLogger('Node removed', this.uuid)
|
|
||||||
}
|
|
||||||
getExtraMenuOptions() {
|
|
||||||
const options = []
|
|
||||||
// {
|
|
||||||
// content: string;
|
|
||||||
// callback?: ContextMenuEventListener;
|
|
||||||
// /** Used as innerHTML for extra child element */
|
|
||||||
// title?: string;
|
|
||||||
// disabled?: boolean;
|
|
||||||
// has_submenu?: boolean;
|
|
||||||
// submenu?: {
|
|
||||||
// options: ContextMenuItem[];
|
|
||||||
// } & IContextMenuOptions;
|
|
||||||
// className?: string;
|
|
||||||
// }
|
|
||||||
options.push({
|
|
||||||
content: `Set to ${
|
|
||||||
this.edit_mode_widget.value === 'html' ? 'markdown' : 'html'
|
|
||||||
}`,
|
|
||||||
callback: () => {
|
|
||||||
this.edit_mode_widget.value =
|
|
||||||
this.edit_mode_widget.value === 'html' ? 'markdown' : 'html'
|
|
||||||
this.updateHTML(this.html_widget.value)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupEditor(editor) {
|
|
||||||
this.setTheme(this.theme_widget.value)
|
|
||||||
|
|
||||||
editor.setShowPrintMargin(false)
|
|
||||||
editor.session.setUseWrapMode(true)
|
|
||||||
editor.renderer.setShowGutter(false)
|
|
||||||
editor.session.setTabSize(4)
|
|
||||||
editor.session.setUseSoftTabs(true)
|
|
||||||
editor.setFontSize(14)
|
|
||||||
editor.setReadOnly(false)
|
|
||||||
editor.setHighlightActiveLine(false)
|
|
||||||
editor.setShowFoldWidgets(true)
|
|
||||||
return editor
|
|
||||||
}
|
|
||||||
|
|
||||||
setTheme(theme) {
|
|
||||||
this.theme_widget.value = theme
|
|
||||||
if (this.html_editor) {
|
|
||||||
this.html_editor.setTheme(`ace/theme/${theme}`)
|
|
||||||
}
|
|
||||||
if (this.css_editor) {
|
|
||||||
this.css_editor.setTheme(`ace/theme/${theme}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setMode(mode) {
|
|
||||||
this.edit_mode_widget.value = mode
|
|
||||||
if (this.html_editor) {
|
|
||||||
this.html_editor.session.setMode(`ace/mode/${mode}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateHTML(this.html_widget.value)
|
|
||||||
}
|
|
||||||
setupEditors() {
|
|
||||||
infoLogger('NotePlus setupEditor')
|
|
||||||
|
|
||||||
this.html_editor = ace.edit('noteplus-html-editor')
|
|
||||||
|
|
||||||
this.css_editor = ace.edit('noteplus-css-editor')
|
|
||||||
this.css_editor.session.setMode('ace/mode/css')
|
|
||||||
|
|
||||||
this.setMode(DEFAULT_MODE)
|
|
||||||
|
|
||||||
this._setupEditor(this.html_editor)
|
|
||||||
this._setupEditor(this.css_editor)
|
|
||||||
|
|
||||||
this.css_editor.session.on('change', (_delta) => {
|
|
||||||
// delta.start, delta.end, delta.lines, delta.action
|
|
||||||
if (this.live) {
|
|
||||||
this.updateCSS(this.css_editor.getValue())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.html_editor.session.on('change', (_delta) => {
|
|
||||||
// delta.start, delta.end, delta.lines, delta.action
|
|
||||||
if (this.live) {
|
|
||||||
this.updateHTML(this.html_editor.getValue())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.html_editor.setValue(this.html_widget.value)
|
|
||||||
this.css_editor.setValue(this.css_widget.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
scopeCss(css, scopeId) {
|
|
||||||
return css
|
|
||||||
.split('}')
|
|
||||||
.map((rule) => {
|
|
||||||
if (rule.trim() === '') {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
const scopedRule = rule
|
|
||||||
.split('{')
|
|
||||||
.map((segment, index) => {
|
|
||||||
if (index === 0) {
|
|
||||||
return `#${scopeId} ${segment.trim()}`
|
|
||||||
}
|
|
||||||
return `{${segment.trim()}`
|
|
||||||
})
|
|
||||||
.join(' ')
|
|
||||||
return `${scopedRule}}`
|
|
||||||
})
|
|
||||||
.join('\n')
|
|
||||||
}
|
|
||||||
getCssDom() {
|
|
||||||
const styleTagId = `note-plus-stylesheet-${this.uuid}`
|
|
||||||
|
|
||||||
let styleTag = document.head.querySelector(`#${styleTagId}`)
|
|
||||||
|
|
||||||
if (!styleTag) {
|
|
||||||
styleTag = document.createElement('style')
|
|
||||||
styleTag.type = 'text/css'
|
|
||||||
styleTag.id = styleTagId
|
|
||||||
document.head.appendChild(styleTag)
|
|
||||||
infoLogger(`Creating note-plus-stylesheet-${this.uuid}`, styleTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
return styleTag
|
|
||||||
}
|
|
||||||
calculateHeight() {
|
|
||||||
this.calculated_height = shared.calculateTotalChildrenHeight(
|
|
||||||
this.html_widget.element,
|
|
||||||
)
|
|
||||||
this.setDirtyCanvas(true, true)
|
|
||||||
}
|
|
||||||
updateCSS(css) {
|
|
||||||
infoLogger('NotePlus updateCSS')
|
|
||||||
// this.html_widget.element.style = css
|
|
||||||
const scopedCss = this.scopeCss(
|
|
||||||
`${CSS_RESET}\n${css}`,
|
|
||||||
`note-plus-${this.uuid}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
const cssDom = this.getCssDom()
|
|
||||||
cssDom.innerHTML = scopedCss
|
|
||||||
|
|
||||||
this.css_widget.value = css
|
|
||||||
this.calculateHeight()
|
|
||||||
infoLogger('NotePlus updateCSS', this.calculated_height)
|
|
||||||
// this.setSize(this.computeSize())
|
|
||||||
}
|
|
||||||
|
|
||||||
updateHTML(val) {
|
|
||||||
const cleanHTML = DOMPurify.sanitize(val, { ADD_TAGS: ['iframe'] })
|
|
||||||
this.html_widget.value = cleanHTML
|
|
||||||
|
|
||||||
// update our widget preview
|
|
||||||
if (this.edit_mode_widget.value === 'html') {
|
|
||||||
this.html_widget.element.innerHTML = cleanHTML
|
|
||||||
} else if (this.edit_mode_widget.value === 'markdown') {
|
|
||||||
this.html_widget.element.innerHTML =
|
|
||||||
this.markdownConverter.makeHtml(cleanHTML)
|
|
||||||
}
|
|
||||||
this.calculateHeight()
|
|
||||||
// this.setSize(this.computeSize())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.registerExtension({
|
|
||||||
name: 'mtb.noteplus',
|
|
||||||
|
|
||||||
registerCustomNodes() {
|
|
||||||
LiteGraph.registerNodeType('Note Plus (mtb)', NotePlus)
|
|
||||||
|
|
||||||
NotePlus.title_mode = LiteGraph.NO_TITLE
|
|
||||||
},
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user