From 4c68e124430fed48c764b8bf19a968a9cd5d1bfa Mon Sep 17 00:00:00 2001 From: AtomAI-GR Date: Sat, 2 May 2026 18:20:30 +0300 Subject: [PATCH] Initial release --- README.md | 84 +++++++++ __init__.py | 15 ++ __pycache__/__init__.cpython-312.pyc | Bin 0 -> 684 bytes .../realistic_touch_node.cpython-312.pyc | Bin 0 -> 11203 bytes .../detail_enhance.cpython-312.pyc | Bin 0 -> 4201 bytes .../detail_enhance_node.cpython-312.pyc | Bin 0 -> 4206 bytes .../dynamic_lora_node.cpython-312.pyc | Bin 0 -> 3420 bytes nodes/__pycache__/lora_loader.cpython-312.pyc | Bin 0 -> 3150 bytes .../lora_loader_pro.cpython-312.pyc | Bin 0 -> 3404 bytes nodes/__pycache__/lora_stack.cpython-312.pyc | Bin 0 -> 4870 bytes .../multi_lora_stack_node.cpython-312.pyc | Bin 0 -> 4924 bytes .../realistic_touch_node.cpython-312.pyc | Bin 0 -> 11201 bytes .../simple_lora_node.cpython-312.pyc | Bin 0 -> 3173 bytes nodes/detail_enhance.py | 78 +++++++++ nodes/lora_loader.py | 74 ++++++++ nodes/lora_loader_pro.py | 81 +++++++++ nodes/lora_stack.py | 165 ++++++++++++++++++ web/js/dynamic_lora_node.js | 138 +++++++++++++++ web/js/simple_lora_node.js | 84 +++++++++ 19 files changed, 719 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 __pycache__/realistic_touch_node.cpython-312.pyc create mode 100644 nodes/__pycache__/detail_enhance.cpython-312.pyc create mode 100644 nodes/__pycache__/detail_enhance_node.cpython-312.pyc create mode 100644 nodes/__pycache__/dynamic_lora_node.cpython-312.pyc create mode 100644 nodes/__pycache__/lora_loader.cpython-312.pyc create mode 100644 nodes/__pycache__/lora_loader_pro.cpython-312.pyc create mode 100644 nodes/__pycache__/lora_stack.cpython-312.pyc create mode 100644 nodes/__pycache__/multi_lora_stack_node.cpython-312.pyc create mode 100644 nodes/__pycache__/realistic_touch_node.cpython-312.pyc create mode 100644 nodes/__pycache__/simple_lora_node.cpython-312.pyc create mode 100644 nodes/detail_enhance.py create mode 100644 nodes/lora_loader.py create mode 100644 nodes/lora_loader_pro.py create mode 100644 nodes/lora_stack.py create mode 100644 web/js/dynamic_lora_node.js create mode 100644 web/js/simple_lora_node.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..396f280 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +
+ +# Atom_AI โ€” Custom ComfyUI Nodes + +**A clean, practical node pack for LoRA management and image enhancement.** + +![ComfyUI](https://img.shields.io/badge/ComfyUI-compatible-brightgreen) +![Python](https://img.shields.io/badge/Python-3.10+-blue) +![License](https://img.shields.io/badge/License-MIT-yellow) + +
+ +--- + +## Nodes + +### ๐Ÿ” LoRA Loader +The simplest way to load multiple LoRAs in one node. +Add or remove slots with **+** / **โˆ’** buttons. Each slot has a strength slider and an enable/disable toggle. + +### โš™๏ธ LoRA Loader Pro +Same as LoRA Loader but with separate **model strength** and **clip strength** per LoRA. +For advanced users who need fine control. + +### ๐Ÿ”— LoRA Stack +A chainable single LoRA slot. Connect multiple entries in sequence to build a stack of any size. + +### ๐Ÿ“ฆ LoRA Stack Apply +Takes the final stack and applies all LoRAs to model and clip in one pass. + +### ๐Ÿ” LoRA Stack Info +Displays the contents of a LoRA stack as text โ€” useful for debugging. + +### โœจ Detail Enhance +Post-processing node that sharpens and enhances detail in generated images. +Controls: **Sharpen**, **Detail**, **Contrast**, **Clarity** + +--- + +## Installation + +**Option A โ€” ComfyUI Manager** *(recommended)* +Search for `Atom_AI` in the Manager and install directly. + +**Option B โ€” Manual** +```bash +cd ComfyUI/custom_nodes +git clone https://github.com/AtomAI-GR/Atom_AI +``` +Restart ComfyUI. + +--- + +## Usage + +### Basic LoRA workflow +``` +Load Checkpoint + โ”œโ”€โ”€ MODEL โ”€โ”€โ†’ LoRA Loader โ”€โ”€โ†’ model โ”€โ”€โ†’ KSampler + โ””โ”€โ”€ CLIP โ”€โ”€โ†’ LoRA Loader โ”€โ”€โ†’ clip โ”€โ”€โ†’ CLIP Text Encode +``` + +### With Detail Enhance +``` +VAE Decode โ”€โ”€โ†’ IMAGE โ”€โ”€โ†’ Detail Enhance โ”€โ”€โ†’ Preview Image +``` + +--- + +## Compatibility + +Works with all major model architectures: **SD 1.5 ยท SDXL ยท Flux ยท HiDream** + +--- + +## License + +MIT โ€” free to use, modify, and share. + +--- + +
+Made with โค๏ธ by Atom_AI +
diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..b092ff9 --- /dev/null +++ b/__init__.py @@ -0,0 +1,15 @@ +from .nodes.detail_enhance import NODE_CLASS_MAPPINGS as ENHANCE_NODES +from .nodes.detail_enhance import NODE_DISPLAY_NAME_MAPPINGS as ENHANCE_NAMES +from .nodes.lora_stack import NODE_CLASS_MAPPINGS as LORA_NODES +from .nodes.lora_stack import NODE_DISPLAY_NAME_MAPPINGS as LORA_NAMES +from .nodes.lora_loader_pro import NODE_CLASS_MAPPINGS as DYN_NODES +from .nodes.lora_loader_pro import NODE_DISPLAY_NAME_MAPPINGS as DYN_NAMES +from .nodes.lora_loader import NODE_CLASS_MAPPINGS as SIMPLE_NODES +from .nodes.lora_loader import NODE_DISPLAY_NAME_MAPPINGS as SIMPLE_NAMES + +NODE_CLASS_MAPPINGS = {**ENHANCE_NODES, **LORA_NODES, **DYN_NODES, **SIMPLE_NODES} +NODE_DISPLAY_NAME_MAPPINGS = {**ENHANCE_NAMES, **LORA_NAMES, **DYN_NAMES, **SIMPLE_NAMES} + +WEB_DIRECTORY = "./web" + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26da47d148a7eae23642face178a64b4a67ea8b4 GIT binary patch literal 684 zcmZXQO^ee&7{{M!n!aQk?XqiyVn0AVG&fJ8WYg$I(ljP3k|+$Z6E&DNOPV6}q@QB< zD8ho?Jnqq>;H5_i1QtDc6BhQcC!eI*RdC?>=Rd!BUS{%56a~O_WAiL{$N~J!Hy5i? zn$trx2f%=l5T?Yfkla_$b8fZpB;=9TO3ov%m%M?zS@H&Qq2wFLTP5E>E)od9uN)D- zhS9`m9TArxtD>)C2pA$Vj&bgl7r?3?hxbcm}csrXpkp+n75mA$d$82xs-J&Tw>A22`h zf{^;Nm)>mR7Zs_f8oRQgs5GBpmkll%j@h>Kdnxw7lxF6f>YeW`7Mcgi6>2> z>GS0xkd*xUNRuXuc@j-&F+h@YD{B&^!7NB99qcd98@HzsTZI0dxPxu-zdwJ1@|z=s zWN2qJ literal 0 HcmV?d00001 diff --git a/__pycache__/realistic_touch_node.cpython-312.pyc b/__pycache__/realistic_touch_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df5ff3057c9cacf5568d4fa013b34fbe4b6d87af GIT binary patch literal 11203 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 literal 0 HcmV?d00001 diff --git a/nodes/__pycache__/detail_enhance.cpython-312.pyc b/nodes/__pycache__/detail_enhance.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f51be556b5e11989d3ddcb012e830580ee8b3242 GIT binary patch literal 4201 zcmb_fU2Gf25#HnR$RjC|(n;2jZT-uZ7}$~}#g6S7QE2MNN@ZHHE{5G`0jR)G#Nf;D6l z?E-TfJua*av7$q;m5rS+wwH}v0(%>UD&`zMr&cv6Dtt6n*gTjN#I)P5B;#TKXpipJ zBMN-J=pPyKQJP0Id=$b_{VVK+afB*!+6_Dnzug%iaEg>*5omH2f=#dstl;>BnIbbz z!38tmOL3+GblflKctGwJyk$C-pyMo?srtkgr6!P%3u-JDZ=|AF6fg@rYs~Q2CI93! zu+aW9P+$-$Fg0&YfBX??PX{!c9E%Df_A#1W5U2Q5Ows6gGy#Oasaa)3oCQHto{jNy z|1|7v4-}ZKfr6R86n8KU@0g-T1JnI_%MJK!8e^PYr98vqSuqio6)YyEl^M+~=um_) zFGN$a<}D11ayF7oD43TO%^r#III7G&LI4UKiODhnpym7XJp!-rJ&|O5YAzM+36TEE zXip?16GRgLuH0jSYm5qe6maaGor9Yh9KSLdp1d|bG?8w6>2`FNiCwU%{3(zHv~5G~ zx{ZeRjU&f4n%ma;*X51F&1)BS7`lhsM)Z;DZ5Fk^wnub~h}wwgrB&TT)Imf|M~bUj z)~R>TS+>TUP_Bw=?fKu6mBI@$1z&&&j;!oa#CE`QS8vB`<*GB#00UpedQR!k7)Y+tId4ba+pT)L*KXvzy;-)`z^VL> z6JE#ZAjBJqvi;(#bZYbV3?oqQy@3$!P{=yHNhDVHw2a+6%l%?1!+(S*6M}B5@{H>N9qw(i}-OCRgg8 z6ibhJ$15zcKiHBp3oG!XYMW4n_Q{SR7i(PBa?#?fGOs| zMsv2*m-*FHnl(A|q|6>ruoY1F<`rN3gU?Q|=`jxF$Gj)w2Ky}e;xg+krZ=S?^gQH@ z%DitYNllK}F+C@|P>xcEsa5JttL7TPsh>UUv;WDOdb$>vnRmYeA@}}v1c!-`zz|!~ z{{tr^kz}@~m7;Bmly0sdJ^&gniv9`qdow=3-#W zb!bKY*43t~bg5j|T6K;)p65=g+{x^z;T$(YbPle(^R26CqqbpLT9O_d%D0|TThIKq z@mcGoTS>4&voV_%6VWX~TwO+0BEe;c|cC81jsC8!+ zhd1jEFW*|awfc*P@85ray>~s5>pYw9^sAlzuiC!u`m!t6`Qu#O81aO=_s$1D+T?1M zyOz3E`X5|>DBYKyalPw9DtBfvu)!VB$%=io<0H=#?z7rQwd>XE@~;{`zy5{zR8&s{ za=t;ee(?9ypU!_X{Dr5na<8cb0d;kFR7fg z7}(_MmV1_ZR{I~GzJGenzjou7Z>|T__BZlv18UpASC+5YFWFq%XpVauysWHQ_AYth z?X2v`dXHw=qxzc}^tm-Q98U0YF&x&M;cz@Dq+*1x2#0T^_*g;02KSAv1b(Mv(i1%DD`IW8(QNddQzOIRT*yb_HRru2{@S6IJD zm_)nok8!!k2l8#lm!uIc}S9P^=qseZ{%HUABSgG+w&$oWho->a8 zP5e_ZOwx*c8pt+HQPg*+;yc9s6`fGgiGN#%D5`S@5xPBYLCt;H{=sbHP!?5hH1%ft N1KGww6&=*S>3_%_n3Dhi literal 0 HcmV?d00001 diff --git a/nodes/__pycache__/detail_enhance_node.cpython-312.pyc b/nodes/__pycache__/detail_enhance_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..106f83058c401728eafbecc67219d5dd37372091 GIT binary patch literal 4206 zcmb_fU2Gf25#HnR$RjC|@=4Z@ZT-uZ7}$~}#g6S7Rv7BXN@QBG*5omH2f=#dstl;>RnItn# z!38tmOL3+GblflKctGwJyk$C-pyMo?+4rd}N{u5Q7t~lRUQb1_C}0+L)|jEui~flz zV4?k|zrY|=V5;Ao`s8EMp7d)rITjT}>|->$AWrhBn4;0~XaWd-L$k_?I0J&HJQL$* z|7qCU>Mt;x{RK0BDehnz-Zn)K`=|Q!mh1P~G{!i)N_m>cGh!kvD_BfSDbt!;(4h!p zUWle-&0822DK=Tj7-2$)h-H~K`ayAw14v_xx zNOvS96GRgLuH0>cYm5qe6mZm4x@KnKdIrZXPlP9~jt!2d8(z91U1eey>@0ro=C%-hxN3_L%SwCnfu&%i994BXUu z;H2`q3}e#F$dZWuu4c*vOPYbz@Y~%;1en=DvhS86q>$^XH%ZS;=E&$D75tf_6cSl_ zlRPF@>NAw73DH0YF-aZI{|m&&!WEfGX-py&2WYL4Sae3S!8Mwl5j9IkYdyc7|W2^jHRnBuF?>Vb_&OW{L<&`H_a-N|Ddc#|N z=jQF3%W}@!mSx*;3z&NWJ>sp7-WR|uA*G0#WadnmH(LgoXR{YRim8SqTahg?=1PG| z!&L_q9HwY5v`Q#QMYK$P1n-AwO~=ScA)CoJb+CMP+zF~0qaQyer>NN{V@Y1=>(v+~ zi6hgRlfV-;fOMo1^7WJ`rbU2?j>f0(QCRWOOgoDM*|kto|MzDlPyXU{MK_5K?O;dX_r)0;$Mbyv&S&9mGqrZS8v1&i=rzGSf& zb0xE{uvo+nELsK2`vc(PZ!&g?B*0zm6_H9mrcLk6evVc4nMNeePzNx@ zJlJ5)mijWk8cVY#XP%VV0}8eR3g5osi+}Xl2{t{(p$3`rWZYn%C0|@-z1j4p)PtUf zoKczgttF|+5j&>mgcr(DYB#k?y=m24BRKW5hkf=wSyNBf0yA^&S0Lox+m7Hc5fT_; zOZtD{gd~#8cC}KpO_KV}6~y~N!^nh`W;2Q)C~p{nJXE-i2)hZX0aB=^e%Jq;7fF(L-1k;VoJr4lC5sX4|;t(6dES+h;*H8&<1MXMO*Q?eZ86K};*SkJ!rII+<~ z2+2P9HA0RMLOucefM~P~rJ-de3XkJ88J~nHpBJh`F*b=wRA3Sin3NLu03jq2@F5@? z4ezFAzee^F1s+nMamVu936(pMJvo%)hKbIB<#)exHLlmxFG-8i{e$_I(`w7< z-!?pJxtOaNTNvJ`tzWvacw>crj6Y95Oh2nVQ<}4v=FYFzHm=mEwZ4TR!qu#HgB7** z^uo|a?V+Wci#Jz(`QU?lAFTDPMRFZy@*RG)!~b>bH=SQ~<~rWV)s7NRxV!Iu_>&E; zdZ}}8e(7 zX>j85rJzxx7e_(=g~4%+9l9J0OpII%YA*PvAj@%4nNAA0nOwpOS>csvq%ft247tMk zMZzT7b$^V@MILWV2nj0rGLTzn-P<^Se!Z$;essO+2oP`G{P|6$a@$hRICp4d+qXry zt&_;cZCfl%JDKCQ2nWTwG1pt1>)m1NnGRz|vQOG$R4L7a#ve_e6ZgV2K1_&S9noVW zqef_w2-c%r_fV?}UK|(<2S)wlw4U@wu%65J05A0dv z$j{-QfnkzX1 P+ZV_-45;XU{!RY_peUO_ literal 0 HcmV?d00001 diff --git a/nodes/__pycache__/dynamic_lora_node.cpython-312.pyc b/nodes/__pycache__/dynamic_lora_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a30d59f54836cc7ee186afa3c1925af671507f0 GIT binary patch literal 3420 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 literal 0 HcmV?d00001 diff --git a/nodes/__pycache__/lora_stack.cpython-312.pyc b/nodes/__pycache__/lora_stack.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd628d09fd1b5dc4bda9cd36ab282d43ed1951d7 GIT binary patch literal 4870 zcmb7HUrZdw8K1q~z5l>rz&^mh8WQ6J42~gnYS(ejfNg4WSlrQ+>z~u+W)GGex0l_$ zC|nN-wXSrjDwV3OOcbIZC6$O=RXqAJ>Qkh?9Ca&SH+gW{zC`N75z84ccfR@N_xsHs8ydU>N^|qS)!&{XEy6)%KLeF}gc8c7N!-42agI9f zlDNY`ZW7AhB~-ZYu%D0f%qN1+S@Q|Z=K`O*<`WfO5fqVnHu<=ddZ~~4S>3h5g&ID` z!6Hd}NZayo!TX4#TV9M_izM zT%-a7SB$$9H+9}6_i+d)agT>k4|U(=;@%Zk$ZMUzZ=Tj>!%_Uu$&9J5Q`{%YKC{0}+HJ3TWSPDEqj@!KJ|JB*{{mMLXHOxBVB zCWWzN2@-?K(FCc^I>k{)Tgp>`iquJ6n_>!L^)-(hBI8v#X!C6fkdzH7M;kXCkUUM0 zJU6Ci!m$N-1C+|OgUT}>RVyzo{HXQ!H$ThYy=FO$vAU@j(>A-&R8L+w79R$&ZC zRah0O2cWCo2nLectwZ2dU7?m+%bKc|k<*aJ7$FEOpG+HeI_qxmSiY&qow-;dHh*U_ zn(zL4CJY~GKMfs>B`|B`OE>8mdPggDjg%IX zSZ7Q9L#W&%1yT1M;z2>I?)?n6KvtavLGLKQ(!X{I;?q`eE@98QQs4@X0&JR1zS@@~ z1!sz5h@p${bcokG;i6*z^e%KbECKhh<&?8oB|}3lSX3pQs(VcD#tyhPmdG;S!2OOp z3kr+B-VBa>=TqWdP4f%zfN>4X8rgG_XGeZ5tjGTxkan&Y#m{^v)>pP}{OQy{+28z& z>9?lKy@Tc63+3LCJ(tkY_$3hF{q~&w=tYB*H@*qxF4=SNqPyZH%{^*lv6Aqwl^QsH16=Dsb%tSiG*iBUob(2$KFOKe925K;;EG9o6Bl9Yw$ z*`TO5z{iJpOMH1|VshFN#;2$5Sk7o{b}Dic-8Lc!3<$g|y%K6Br6F3%)MlMy7>SvT z>A!!11?3R^HhC43-5Jr5i2y5}w=Wnx8&r55iC4#^vZ3Z8*3N)Af+cLV^F7CAZU^ls zh|FLooAD}6(r~KW-dAq#Eq9-XoCOr%ow)Oz2RlEB>{~4e1?>B?fczjzfbY^|RVeU= zh+CLCQhXEyiHXm!QmAd{cR-}P%P(-NVu7C^KllkjXaH_(hv!yZP;;`H8)`0A^OzVd zR+f%}&w-D+3$Ch|S@jl#CB)LYvgE6Cz-_@xJ-_qTditOz=5AeCYN~U|k4diKoT;V_iwAwCV{cnerE=*s({Gk& zw>RPN2~*RPi%iT}4ue{)%*l8)rRg@A8v0orMOf0*OvtbI!%bb9TOxb0<;8skf-s{P zmWT>x#S$1jiv|t_Y@cAe&?asalLs7*deOiYM!>HY8w9U_w~laG9Q9%Lscb*b!!yP| z!K{%_+#R1awU=AlH^xe5MoTSYCI8stV9y8q*0_is-#g}6?m2)2(u6T15`jQat5~ovj2!;teqACE_p^0jw2Mp9iDE1I3NcZ?D9Z6 zj-?t!;EU{1;9(T8AlQTgbr;m#NGa3V1KFGD7VZYyiStEZ1@N4OR+|v8O=vMyyYo*@Nohzm0Yo)+TMe)<% znXQ3s`C;(F7ew?lzICe@F6OF|xTSTyZ+&({+<0v_I8Zt@SRvd{f4P6CbpGmY|BDZd z(%ErvO>pSjPVDwyeUL1jyB7?to5>S{=+eF%`Wt9iC6%X`$9uH|ZztZ$VzdVdO;}k9L^0#w%bdJjZdL5YH#1 z literal 0 HcmV?d00001 diff --git a/nodes/__pycache__/multi_lora_stack_node.cpython-312.pyc b/nodes/__pycache__/multi_lora_stack_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abdecb7572944a9dacfde11de53f50b6893945b2 GIT binary patch literal 4924 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 eK!2Wan53oBNP1oA{;F*N literal 0 HcmV?d00001 diff --git a/nodes/__pycache__/realistic_touch_node.cpython-312.pyc b/nodes/__pycache__/realistic_touch_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89ebe1b73c8ad0e56914d3f5032216c85a6de462 GIT binary patch literal 11201 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 literal 0 HcmV?d00001 diff --git a/nodes/__pycache__/simple_lora_node.cpython-312.pyc b/nodes/__pycache__/simple_lora_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86d887bf04fba1e6d9724e8c08b8b863032e62c1 GIT binary patch literal 3173 zcmaJ@O>7&-6`uX&@<$T&M^k^+%61(waHUjI?Ao;)2ofa|k!eAc5QwRp}4@JDTKLO2=kBF+WNSNc~wj^aTn&X0DozP35nlOW;9t7sZqKg2jAWZ5ZI4d zWGtq}LMXGZtv;rI%s$%Z=V25>AudYzsGe1cpy@`!46($Q(lx9a%5uWGZj$Cj)lyPv zP0c2jR7Fdh)*sLw)%0Rw5GSm}U{YU7t>n^!EH7&-4t!?v!0%umjR^|nUJ%GSdKnX#srw&%&qfKCO=Lq15mK6-u)z6T zQZtEo<7UEGG);=BEE_c3i6;_M#xIXWr@|BYo*$wz@RHpI;-(MCDtgw`wl@DeyDW9p z9Cp}57f3jv+YKEWx|qcr4RJL_!)j6 zdG_6FlVG|FtEogzvxs{MXj)5W)evViQ?V8x#N!0Gk}>tH0r!^bpy-&?sQp8FBhi`p zIc08PCLGHjf7K~p@}0GVuyz$yMbvPt(s8oVaiY@uTBY}7rSJ5vz`fp5MO<&|uFKWY zSmoS)PZhb{o*EML;?^5A#zEp^0DK*}54r1ASJ>w2E-dhis32grAlm!{y7n)G3NG7~ zp~xO-6xn)mDM%K*Jx==(Y4AZ^`d}}&MFZ;pt#V=M2dg}`#}I4@^Edjz56qpI)lk=x zp^iM#u-iV+QSjQ{)qpKzs0)uY?6IX!5cWPez=A<{eq&KTJ1s+l@JP!XJ2C-7+l!?? z`s!8}e1|kp{~l?Xt|JpLv>BVVeYP7A6KdRFf)@)+f&CH78yw3UVMWs?JoY~j>IgH) z7IAYP@F|FQ{>>w+r@oub*JTO~xWyLXmV9WAwvQ?;7piFaY=RoO2kd{0k?o|GNC{;-w`$p-MAp9 zv%gAcX)GVQQZ!i*o>mEisw0i{e5W|sYq4fd^)2FpQu3ar;yHr?mZRBTmcPEgF6SF$ zxj(C0a!Su-aR}0q+f1Zry-45#u@B|E55(%qNAujkQ0fH}`i5TPyvxbMSkCjZo|5x! zCpq_`+ka+>B;LLx%0 zRkN4~PQfEW4pNLsny4nLae@j8CRJ*jqJ6is4=vXFU0RiiVChyuBjR+;Hkcc{Sp1Z2z{zzhkG<=mIP40Nt%?q*7z+krF1sj7qm z!J?L0D08@QDEI9u@tp9N3t1c$bQLyylrnb;1F6(g0V zV7W=I_&ay}eOvy%(&XmI=IO@`P_%FXAB z;@>Y!tOYhs-R&v0{l4SVj?D|(eP=&!KKt3}B468nuH1B@ES>o8x1-44@f{c zjgfNS#d7QD=f2S@2U9OhngxH`K5-uX<$Pn9Lc<~lPb`J_ z6y^O~LfhxK6b0)^n3l%J=E74~XBUWXHas^!8?6<)gHd$sN;pQO$@%E`T;yt$c#~Sf zG?!HCI>Z}&lNmB4G+vw{>Qu8@4AI1HP(t4e=4Bwa&@Rgg5`;!`@9O1B+u-VSrEM69 zzi0LGSAu_+WrXfM4vEcG+5_Ch=3Uk!c=qRLFW4uXtZhP7=eu9==z4em6?HA-uH9U( zb43T78*01(%ZOB)F|iSXq4UagOeL*RcyTG?(_^uia%F601|D27(&kuEYPcM*{4uNl-k bTppP$_e_;hYvuS*d1SWS6Wc 0: + blurred = pil.filter(ImageFilter.GaussianBlur(radius=detail_radius)) + img_arr = np.array(pil, dtype=np.float32) + blur_arr = np.array(blurred, dtype=np.float32) + detail = img_arr - blur_arr + result = img_arr + detail * detail_strength + pil = Image.fromarray(result.clip(0, 255).astype(np.uint8)) + + if clarity > 0: + arr = np.array(pil, dtype=np.float32) / 255.0 + mid = 0.5 + arr = arr + clarity * 0.15 * np.sin((arr - mid) * np.pi) * (1 - np.abs(arr - mid) * 2).clip(0) + pil = Image.fromarray((arr.clip(0, 1) * 255).astype(np.uint8)) + + return (self._to_tensor(pil),) + + +NODE_CLASS_MAPPINGS = {"DetailEnhanceNode": DetailEnhanceNode} +NODE_DISPLAY_NAME_MAPPINGS = {"DetailEnhanceNode": "Detail Enhance"} diff --git a/nodes/lora_loader.py b/nodes/lora_loader.py new file mode 100644 index 0000000..391fa9e --- /dev/null +++ b/nodes/lora_loader.py @@ -0,0 +1,74 @@ +import folder_paths +import comfy.utils +import comfy.sd +import json + +def get_lora_list(): + return ["None"] + folder_paths.get_filename_list("loras") + + +class AtomSimpleLoRANode: + """ + Simple LoRA Loader โ€” clean UI, no clip str., strength 0-2. + """ + + CATEGORY = "Atom_AI/LoRA" + RETURN_TYPES = ("MODEL", "CLIP", "STRING") + RETURN_NAMES = ("model", "clip", "stack_info") + FUNCTION = "apply_loras" + + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("nan") + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "clip": ("CLIP",), + "loras_json": ("STRING", { + "default": "[]", + "multiline": False, + }), + } + } + + def apply_loras(self, model, clip, loras_json): + try: + entries = json.loads(loras_json) + except Exception: + return (model, clip, "Error: invalid loras_json") + + info_lines = [] + loaded = 0 + + for entry in entries: + name = entry.get("name", "None") + s_mod = float(entry.get("strength_model", 1.0)) + enabled = bool(entry.get("enabled", True)) + + if not enabled or name == "None": + continue + + lora_path = folder_paths.get_full_path("loras", name) + if lora_path is None: + info_lines.append(f" โœ— {name} (not found)") + continue + + lora_data = comfy.utils.load_torch_file(lora_path, safe_load=True) + # clip strength fixed at 1.0 + model, clip = comfy.sd.load_lora_for_models( + model, clip, lora_data, s_mod, 1.0 + ) + info_lines.append(f" โœ“ {name} (strength={s_mod:.2f})") + loaded += 1 + + total = len(entries) + header = f"Loaded {loaded} of {total} LoRA(s):" + info = header + "\n" + "\n".join(info_lines) if info_lines else header + return (model, clip, info) + + +NODE_CLASS_MAPPINGS = {"AtomSimpleLoRANode": AtomSimpleLoRANode} +NODE_DISPLAY_NAME_MAPPINGS = {"AtomSimpleLoRANode": "LoRA Loader"} diff --git a/nodes/lora_loader_pro.py b/nodes/lora_loader_pro.py new file mode 100644 index 0000000..674b752 --- /dev/null +++ b/nodes/lora_loader_pro.py @@ -0,0 +1,81 @@ +import folder_paths +import comfy.utils +import comfy.sd + +def get_lora_list(): + return ["None"] + folder_paths.get_filename_list("loras") + + +class AtomDynamicLoRANode: + """ + Dynamic LoRA Loader โ€” slots managed entirely by JS. + Python receives lora_count + flat arrays of name/strength_model/strength_clip/enabled. + """ + + CATEGORY = "Atom_AI/LoRA" + RETURN_TYPES = ("MODEL", "CLIP", "STRING") + RETURN_NAMES = ("model", "clip", "stack_info") + FUNCTION = "apply_loras" + + # Tell ComfyUI this node has dynamic widgets โ€” skip validation + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("nan") + + @classmethod + def INPUT_TYPES(cls): + loras = get_lora_list() + return { + "required": { + "model": ("MODEL",), + "clip": ("CLIP",), + # JS writes all LoRA data as a JSON string โ€” one input only + "loras_json": ("STRING", { + "default": "[]", + "multiline": False, + }), + } + } + + def apply_loras(self, model, clip, loras_json): + import json + + try: + entries = json.loads(loras_json) + except Exception: + return (model, clip, "Error: invalid loras_json") + + info_lines = [] + loaded = 0 + + for entry in entries: + name = entry.get("name", "None") + s_mod = float(entry.get("strength_model", 1.0)) + s_clip = float(entry.get("strength_clip", 1.0)) + enabled = bool(entry.get("enabled", True)) + + if not enabled or name == "None": + continue + + lora_path = folder_paths.get_full_path("loras", name) + if lora_path is None: + info_lines.append(f" โœ— {name} (not found)") + continue + + lora_data = comfy.utils.load_torch_file(lora_path, safe_load=True) + model, clip = comfy.sd.load_lora_for_models( + model, clip, lora_data, s_mod, s_clip + ) + info_lines.append( + f" โœ“ {name}\n model={s_mod:.2f} clip={s_clip:.2f}" + ) + loaded += 1 + + total = len(entries) + header = f"Loaded {loaded} of {total} LoRA(s):" + info = header + "\n" + "\n".join(info_lines) if info_lines else header + return (model, clip, info) + + +NODE_CLASS_MAPPINGS = {"AtomDynamicLoRANode": AtomDynamicLoRANode} +NODE_DISPLAY_NAME_MAPPINGS = {"AtomDynamicLoRANode": "LoRA Loader Pro"} diff --git a/nodes/lora_stack.py b/nodes/lora_stack.py new file mode 100644 index 0000000..97a94a5 --- /dev/null +++ b/nodes/lora_stack.py @@ -0,0 +1,165 @@ +import os +import folder_paths + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Helper: get all available LoRA filenames +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +def get_lora_list(): + loras = folder_paths.get_filename_list("loras") + return ["None"] + loras + + +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +# NODE 1 โ€” LoRA Stack Entry +# One slot: pick a LoRA + strength + toggle. +# Chains into a growing stack list. +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +class AtomLoRAStackEntry: + """ + A single chainable LoRA slot. + Connect multiple entries in sequence to build a stack of any size. + """ + + CATEGORY = "Atom_AI/LoRA" + RETURN_TYPES = ("LORA_STACK",) + RETURN_NAMES = ("lora_stack",) + FUNCTION = "add_lora" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "lora_name": (get_lora_list(),), + "strength_model": ("FLOAT", { + "default": 1.0, "min": -10.0, "max": 10.0, + "step": 0.01, "display": "slider" + }), + "strength_clip": ("FLOAT", { + "default": 1.0, "min": -10.0, "max": 10.0, + "step": 0.01, "display": "slider" + }), + "enabled": ("BOOLEAN", {"default": True}), + }, + "optional": { + "lora_stack": ("LORA_STACK",), # chain input from previous entry + } + } + + def add_lora(self, lora_name, strength_model, strength_clip, enabled, + lora_stack=None): + # Start from existing stack or fresh list + stack = list(lora_stack) if lora_stack else [] + + if enabled and lora_name != "None": + stack.append({ + "lora_name": lora_name, + "strength_model": strength_model, + "strength_clip": strength_clip, + }) + + return (stack,) + + +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +# NODE 2 โ€” LoRA Stack Loader +# Takes the final stack list, applies every +# LoRA in sequence to model + clip. +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +class AtomLoRAStackLoader: + """ + Applies a LORA_STACK (built from AtomLoRAStackEntry nodes) to + a model and clip in one pass. + """ + + CATEGORY = "Atom_AI/LoRA" + RETURN_TYPES = ("MODEL", "CLIP", "STRING") + RETURN_NAMES = ("model", "clip", "stack_info") + FUNCTION = "load_stack" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "clip": ("CLIP",), + "lora_stack": ("LORA_STACK",), + } + } + + def load_stack(self, model, clip, lora_stack): + import comfy.utils + + if not lora_stack: + return (model, clip, "No LoRAs loaded.") + + info_lines = [f"Loaded {len(lora_stack)} LoRA(s):"] + + for entry in lora_stack: + name = entry["lora_name"] + s_mod = entry["strength_model"] + s_clip = entry["strength_clip"] + + lora_path = folder_paths.get_full_path("loras", name) + lora_data = comfy.utils.load_torch_file(lora_path, safe_load=True) + + model, clip = comfy.sd.load_lora_for_models( + model, clip, lora_data, s_mod, s_clip + ) + + info_lines.append(f" โœ“ {name} (model={s_mod:.2f}, clip={s_clip:.2f})") + + return (model, clip, "\n".join(info_lines)) + + +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +# NODE 3 โ€” LoRA Stack Inspector (bonus) +# Shows what's in the stack without applying. +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +class AtomLoRAStackInspector: + """ + Displays the contents of a LORA_STACK as a human-readable string. + Useful for debugging complex chains. + """ + + CATEGORY = "Atom_AI/LoRA" + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("stack_info",) + FUNCTION = "inspect" + OUTPUT_NODE = True # allows display in UI + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "lora_stack": ("LORA_STACK",), + } + } + + def inspect(self, lora_stack): + if not lora_stack: + return ("Stack is empty.",) + + lines = [f"Stack contains {len(lora_stack)} LoRA(s):\n"] + for i, entry in enumerate(lora_stack, 1): + lines.append( + f"{i}. {entry['lora_name']}\n" + f" model strength : {entry['strength_model']:.3f}\n" + f" clip strength : {entry['strength_clip']:.3f}" + ) + return ("\n".join(lines),) + + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# ComfyUI registration +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +NODE_CLASS_MAPPINGS = { + "AtomLoRAStackEntry": AtomLoRAStackEntry, + "AtomLoRAStackLoader": AtomLoRAStackLoader, + "AtomLoRAStackInspector": AtomLoRAStackInspector, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "AtomLoRAStackEntry": "LoRA Stack", + "AtomLoRAStackLoader": "LoRA Stack Apply", + "AtomLoRAStackInspector": "LoRA Stack Info", +} diff --git a/web/js/dynamic_lora_node.js b/web/js/dynamic_lora_node.js new file mode 100644 index 0000000..2da3a5b --- /dev/null +++ b/web/js/dynamic_lora_node.js @@ -0,0 +1,138 @@ +import { app } from "/scripts/app.js"; + +const DEFAULT_SLOTS = 1; + +function serializeSlots(node) { + const entries = (node._loraSlots ?? []).map(s => ({ + name: s.nameW.value, + strength_model: s.smW.value, + strength_clip: s.scW.value, + enabled: s.enW.value, + })); + const jw = node.widgets?.find(w => w.name === "loras_json"); + if (jw) jw.value = JSON.stringify(entries); +} + +function addSlot(node, def = {}) { + const loras = node._loraNames ?? ["None"]; + + const nameW = node.addWidget("combo", "lora", def.name ?? "None", + () => serializeSlots(node), { values: loras }); + + const smW = node.addWidget("number", "model str.", def.strength_model ?? 1.0, + () => serializeSlots(node), { min: -10, max: 10, step: 0.1, precision: 2 }); + + const scW = node.addWidget("number", "clip str.", def.strength_clip ?? 1.0, + () => serializeSlots(node), { min: -10, max: 10, step: 0.1, precision: 2 }); + + const enW = node.addWidget("toggle", "enabled", def.enabled ?? true, + () => serializeSlots(node)); + + node._loraSlots = node._loraSlots ?? []; + node._loraSlots.push({ nameW, smW, scW, enW }); + + serializeSlots(node); + node.setSize([node.size[0], node.computeSize()[1]]); + node.graph?.setDirtyCanvas(true, true); +} + +function removeSlot(node) { + if (!node._loraSlots?.length) return; + const s = node._loraSlots.pop(); + for (const w of [s.nameW, s.smW, s.scW, s.enW]) { + const i = node.widgets.indexOf(w); + if (i !== -1) node.widgets.splice(i, 1); + } + serializeSlots(node); + node.setSize([node.size[0], node.computeSize()[1]]); + node.graph?.setDirtyCanvas(true, true); +} + +function init(node) { + if (node._atomDynInit) return; + node._atomDynInit = true; + node._loraSlots = []; + + // Hide loras_json + const jw = node.widgets?.find(w => w.name === "loras_json"); + if (jw) { + jw.hidden = true; + jw.computeSize = () => [0, -4]; + } + + node.addWidget("button", "+ Add LoRA", null, () => addSlot(node)); + node.addWidget("button", "โˆ’ Remove LoRA", null, () => removeSlot(node)); + + // Restore from saved JSON or add defaults + let restored = false; + if (jw?.value && jw.value !== "[]") { + try { + const entries = JSON.parse(jw.value); + if (entries.length > 0) { + entries.forEach(e => addSlot(node, e)); + restored = true; + } + } catch {} + } + if (!restored) { + for (let i = 0; i < DEFAULT_SLOTS; i++) addSlot(node); + } +} + +// Fetch lora list directly from ComfyUI's /object_info endpoint +async function fetchLoraNames() { + try { + const r = await fetch("/object_info"); + const all = await r.json(); + + // Try every known lora loader node name + const candidates = [ + "LoraLoader", + "LoRALoader", + "Load LoRA", + "LoraLoaderModelOnly", + ]; + + for (const name of candidates) { + const def = all[name]; + if (!def) continue; + // Search all required inputs for a list that looks like lora filenames + for (const [, val] of Object.entries(def.input?.required ?? {})) { + if (Array.isArray(val[0]) && val[0].some(v => + typeof v === "string" && (v.endsWith(".safetensors") || v.endsWith(".pt") || v.endsWith(".ckpt")) + )) { + return ["None", ...val[0]]; + } + } + } + + // Last resort: scan ALL node defs for a safetensors list + for (const def of Object.values(all)) { + for (const [, val] of Object.entries(def.input?.required ?? {})) { + if (Array.isArray(val[0]) && val[0].length > 0 && + val[0].some(v => typeof v === "string" && v.includes("lora"))) { + return ["None", ...val[0]]; + } + } + } + } catch (e) { + console.warn("[Atom_AI] Could not fetch lora list:", e); + } + return ["None"]; +} + +app.registerExtension({ + name: "Atom_AI.DynamicLoRANode", + + async setup() { + // Fetch and cache lora list at startup + app._atomLoraNames = await fetchLoraNames(); + console.log("[Atom_AI] LoRA list loaded:", app._atomLoraNames.length, "entries"); + }, + + nodeCreated(node) { + if (node.comfyClass !== "AtomDynamicLoRANode") return; + node._loraNames = app._atomLoraNames ?? ["None"]; + init(node); + }, +}); diff --git a/web/js/simple_lora_node.js b/web/js/simple_lora_node.js new file mode 100644 index 0000000..45d0b3c --- /dev/null +++ b/web/js/simple_lora_node.js @@ -0,0 +1,84 @@ +import { app } from "/scripts/app.js"; + +const DEFAULT_SLOTS = 2; + +function serializeSlots(node) { + const entries = (node._loraSlots ?? []).map(s => ({ + name: s.nameW.value, + strength_model: s.smW.value, + enabled: s.enW.value, + })); + const jw = node.widgets?.find(w => w.name === "loras_json"); + if (jw) jw.value = JSON.stringify(entries); +} + +function addSlot(node, def = {}) { + const loras = node._loraNames ?? ["None"]; + + const nameW = node.addWidget("combo", "lora", def.name ?? "None", + () => serializeSlots(node), { values: loras }); + + const smW = node.addWidget("number", "strength", def.strength_model ?? 1.0, + () => serializeSlots(node), { min: 0.0, max: 2.0, step: 0.1, precision: 2 }); + + const enW = node.addWidget("toggle", "enabled", def.enabled ?? true, + () => serializeSlots(node)); + + node._loraSlots = node._loraSlots ?? []; + node._loraSlots.push({ nameW, smW, enW }); + + serializeSlots(node); + node.setSize([node.size[0], node.computeSize()[1]]); + node.graph?.setDirtyCanvas(true, true); +} + +function removeSlot(node) { + if (!node._loraSlots?.length) return; + const s = node._loraSlots.pop(); + for (const w of [s.nameW, s.smW, s.enW]) { + const i = node.widgets.indexOf(w); + if (i !== -1) node.widgets.splice(i, 1); + } + serializeSlots(node); + node.setSize([node.size[0], node.computeSize()[1]]); + node.graph?.setDirtyCanvas(true, true); +} + +function init(node) { + if (node._atomSimpleInit) return; + node._atomSimpleInit = true; + node._loraSlots = []; + + const jw = node.widgets?.find(w => w.name === "loras_json"); + if (jw) { + jw.hidden = true; + jw.computeSize = () => [0, -4]; + } + + node.addWidget("button", "+ Add LoRA", null, () => addSlot(node)); + node.addWidget("button", "โˆ’ Remove LoRA", null, () => removeSlot(node)); + + let restored = false; + if (jw?.value && jw.value !== "[]") { + try { + const entries = JSON.parse(jw.value); + if (entries.length > 0) { + entries.forEach(e => addSlot(node, e)); + restored = true; + } + } catch {} + } + if (!restored) { + for (let i = 0; i < DEFAULT_SLOTS; i++) addSlot(node); + } +} + +app.registerExtension({ + name: "Atom_AI.SimpleLoRANode", + + nodeCreated(node) { + if (node.comfyClass !== "AtomSimpleLoRANode") return; + node._loraNames = app._atomLoraNames ?? ["None"]; + init(node); + }, +});