mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-09 07:20:15 -03:00
Compare commits
6 Commits
47fe2d3783
...
v1.1.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e303ab316 | ||
|
|
e9e8c31ad1 | ||
|
|
703a6a4ea0 | ||
|
|
283730cf38 | ||
|
|
20417797e8 | ||
|
|
004c69b9ef |
@@ -11,8 +11,8 @@
|
|||||||
"Insomnia Art Designs",
|
"Insomnia Art Designs",
|
||||||
"2018cfh",
|
"2018cfh",
|
||||||
"Arlecchino Shion",
|
"Arlecchino Shion",
|
||||||
"Rob Williams",
|
|
||||||
"Charles Blakemore",
|
"Charles Blakemore",
|
||||||
|
"Rob Williams",
|
||||||
"W+K+White",
|
"W+K+White",
|
||||||
"$MetaSamsara",
|
"$MetaSamsara",
|
||||||
"wackop",
|
"wackop",
|
||||||
@@ -21,6 +21,8 @@
|
|||||||
"stone9k",
|
"stone9k",
|
||||||
"Rosenthal",
|
"Rosenthal",
|
||||||
"Francisco Tatis",
|
"Francisco Tatis",
|
||||||
|
"JongWon Han",
|
||||||
|
"FreelancerZ",
|
||||||
"Polymorphic Indeterminate",
|
"Polymorphic Indeterminate",
|
||||||
"Skalabananen",
|
"Skalabananen",
|
||||||
"Marc Whiffen",
|
"Marc Whiffen",
|
||||||
@@ -38,24 +40,32 @@
|
|||||||
"Estragon",
|
"Estragon",
|
||||||
"J\\B/ 8r0wns0n",
|
"J\\B/ 8r0wns0n",
|
||||||
"ClockDaemon",
|
"ClockDaemon",
|
||||||
|
"KD",
|
||||||
|
"Omnidex",
|
||||||
|
"Tyler Trebuchon",
|
||||||
|
"Release Cabrakan",
|
||||||
"Tobi_Swagg",
|
"Tobi_Swagg",
|
||||||
"SG",
|
"SG",
|
||||||
|
"James Dooley",
|
||||||
"zenbound",
|
"zenbound",
|
||||||
|
"Buzzard",
|
||||||
"jmack",
|
"jmack",
|
||||||
"Andrew Wilson",
|
"Andrew Wilson",
|
||||||
"Greybush",
|
"Greybush",
|
||||||
|
"Mark Corneglio",
|
||||||
"Ricky Carter",
|
"Ricky Carter",
|
||||||
"James Todd",
|
"James Todd",
|
||||||
"JongWon Han",
|
"Steven Pfeiffer",
|
||||||
"VantAI",
|
"VantAI",
|
||||||
"レプサイ",
|
"レプサイ",
|
||||||
|
"Lisster",
|
||||||
"Michael Wong",
|
"Michael Wong",
|
||||||
"runte3221",
|
"runte3221",
|
||||||
"Illrigger",
|
"Illrigger",
|
||||||
"Tom Corrigan",
|
"Tom Corrigan",
|
||||||
"JackieWang",
|
"JackieWang",
|
||||||
"FreelancerZ",
|
|
||||||
"fnkylove",
|
"fnkylove",
|
||||||
|
"Yushio",
|
||||||
"Vik71it",
|
"Vik71it",
|
||||||
"Echo",
|
"Echo",
|
||||||
"Lilleman",
|
"Lilleman",
|
||||||
@@ -65,50 +75,48 @@
|
|||||||
"Fraser Cross",
|
"Fraser Cross",
|
||||||
"Liam MacDougal",
|
"Liam MacDougal",
|
||||||
"Sterilized",
|
"Sterilized",
|
||||||
|
"BadassArabianMofo",
|
||||||
"JORGE+LUIZ+HUSSNI+MESSIAS",
|
"JORGE+LUIZ+HUSSNI+MESSIAS",
|
||||||
"quarz",
|
"quarz",
|
||||||
"Greg",
|
"Greg",
|
||||||
|
"jean jahren",
|
||||||
"JSST",
|
"JSST",
|
||||||
"Snaggwort",
|
"Snaggwort",
|
||||||
|
"lmsupporter",
|
||||||
"Takkan",
|
"Takkan",
|
||||||
"wfpearl",
|
"wfpearl",
|
||||||
"Matt+J",
|
"Matt+J",
|
||||||
"Baekdoosixt",
|
"Baekdoosixt",
|
||||||
"Jonathan Ross",
|
"Jonathan Ross",
|
||||||
"KD",
|
"Jack B Nimble",
|
||||||
"Omnidex",
|
|
||||||
"Nazono_hito",
|
"Nazono_hito",
|
||||||
"Melville Parrish",
|
"Melville Parrish",
|
||||||
"daniel dove",
|
"daniel dove",
|
||||||
"Lustre",
|
"Lustre",
|
||||||
"Tyler Trebuchon",
|
|
||||||
"Release Cabrakan",
|
|
||||||
"JW Sin",
|
"JW Sin",
|
||||||
"Alex",
|
"Alex",
|
||||||
"bh",
|
"bh",
|
||||||
"carozzz",
|
"carozzz",
|
||||||
"Marlon Daniels",
|
"Marlon Daniels",
|
||||||
"James Dooley",
|
"Starkselle",
|
||||||
"Buzzard",
|
|
||||||
"Aaron Bleuer",
|
"Aaron Bleuer",
|
||||||
"LacesOut!",
|
"LacesOut!",
|
||||||
|
"greebles",
|
||||||
"Adam Shaw",
|
"Adam Shaw",
|
||||||
"Mark Corneglio",
|
|
||||||
"SarcasticHashtag",
|
"SarcasticHashtag",
|
||||||
"Anthony Rizzo",
|
"Anthony Rizzo",
|
||||||
"iamresist",
|
"iamresist",
|
||||||
|
"M Postkasse",
|
||||||
"RedrockVP",
|
"RedrockVP",
|
||||||
"Wolffen",
|
"Wolffen",
|
||||||
"Wicked Choices by ASLPro3D",
|
"Wicked Choices by ASLPro3D",
|
||||||
"Jacob Hoehler",
|
"Jacob Hoehler",
|
||||||
"FinalyFree",
|
"FinalyFree",
|
||||||
"Weasyl",
|
"Weasyl",
|
||||||
"Steven Pfeiffer",
|
|
||||||
"Timmy",
|
"Timmy",
|
||||||
"Johnny",
|
"Johnny",
|
||||||
"Cory Paza",
|
"Cory Paza",
|
||||||
"Tak",
|
"Tak",
|
||||||
"Lisster",
|
|
||||||
"Big Red",
|
"Big Red",
|
||||||
"whudunit",
|
"whudunit",
|
||||||
"Luc Job",
|
"Luc Job",
|
||||||
@@ -116,37 +124,36 @@
|
|||||||
"Philip Hempel",
|
"Philip Hempel",
|
||||||
"corde",
|
"corde",
|
||||||
"nwalker94",
|
"nwalker94",
|
||||||
"Yushio",
|
|
||||||
"Bishoujoker",
|
"Bishoujoker",
|
||||||
|
"aai",
|
||||||
"Todd Keck",
|
"Todd Keck",
|
||||||
"Briton Heilbrun",
|
"Briton Heilbrun",
|
||||||
"Tori",
|
"Tori",
|
||||||
"wildnut",
|
"wildnut",
|
||||||
"Aleksander Wujczyk",
|
"Aleksander Wujczyk",
|
||||||
"AM Kuro",
|
"AM Kuro",
|
||||||
"BadassArabianMofo",
|
|
||||||
"Pascal Dahle",
|
"Pascal Dahle",
|
||||||
"Sangheili460",
|
"Sangheili460",
|
||||||
"MagnaInsomnia",
|
"MagnaInsomnia",
|
||||||
"Akira_HentAI",
|
"Akira_HentAI",
|
||||||
"Karl P.",
|
"Karl P.",
|
||||||
"lmsupporter",
|
|
||||||
"andrew.tappan",
|
"andrew.tappan",
|
||||||
"N/A",
|
"N/A",
|
||||||
|
"The Spawn",
|
||||||
"graysock",
|
"graysock",
|
||||||
"Greenmoustache",
|
"Greenmoustache",
|
||||||
"zounic",
|
"zounic",
|
||||||
"fancypants",
|
"fancypants",
|
||||||
"Eldithor",
|
"Digital",
|
||||||
"Jack B Nimble",
|
|
||||||
"JaxMax",
|
"JaxMax",
|
||||||
"contrite831",
|
"contrite831",
|
||||||
"Jwk0205",
|
"Jwk0205",
|
||||||
"Starkselle",
|
"Bro Xie",
|
||||||
|
"batblue",
|
||||||
|
"carey6409",
|
||||||
"Olive",
|
"Olive",
|
||||||
"greebles",
|
"太郎 ゲーム",
|
||||||
"Some Guy Named Barry",
|
"Some Guy Named Barry",
|
||||||
"M Postkasse",
|
|
||||||
"AELOX",
|
"AELOX",
|
||||||
"Gooohokrbe",
|
"Gooohokrbe",
|
||||||
"Nicfit23",
|
"Nicfit23",
|
||||||
@@ -162,12 +169,12 @@
|
|||||||
"Serge Bekenkamp",
|
"Serge Bekenkamp",
|
||||||
"Jimmy Ledbetter",
|
"Jimmy Ledbetter",
|
||||||
"LeoZero",
|
"LeoZero",
|
||||||
|
"Antonio Pontes",
|
||||||
|
"nahinahi9",
|
||||||
"Dustin Chen",
|
"Dustin Chen",
|
||||||
"dan",
|
"dan",
|
||||||
"aai",
|
|
||||||
"Mouthlessman",
|
"Mouthlessman",
|
||||||
"otaku fra",
|
"otaku fra",
|
||||||
"jean jahren",
|
|
||||||
"MiraiKuriyamaSy",
|
"MiraiKuriyamaSy",
|
||||||
"Ran C",
|
"Ran C",
|
||||||
"ViperC",
|
"ViperC",
|
||||||
@@ -176,7 +183,6 @@
|
|||||||
"Adam Taylor",
|
"Adam Taylor",
|
||||||
"AbstractAss",
|
"AbstractAss",
|
||||||
"Weird_With_A_Beard",
|
"Weird_With_A_Beard",
|
||||||
"The Spawn",
|
|
||||||
"Pozadine1",
|
"Pozadine1",
|
||||||
"Qarob",
|
"Qarob",
|
||||||
"AIGooner",
|
"AIGooner",
|
||||||
@@ -187,18 +193,16 @@
|
|||||||
"Hasturkun",
|
"Hasturkun",
|
||||||
"Jon Sandman",
|
"Jon Sandman",
|
||||||
"Ubivis",
|
"Ubivis",
|
||||||
|
"CloudValley",
|
||||||
|
"linnfrey",
|
||||||
"Jackthemind",
|
"Jackthemind",
|
||||||
|
"griffin+dahlberg",
|
||||||
"Joboshy",
|
"Joboshy",
|
||||||
"Digital",
|
|
||||||
"takyamtom",
|
"takyamtom",
|
||||||
"Bohemian Corporal",
|
"Bohemian Corporal",
|
||||||
"Dan",
|
"Dan",
|
||||||
"Bro Xie",
|
|
||||||
"yer fey",
|
"yer fey",
|
||||||
"batblue",
|
|
||||||
"Error_Rule34_Not_found",
|
"Error_Rule34_Not_found",
|
||||||
"carey6409",
|
|
||||||
"太郎 ゲーム",
|
|
||||||
"Roslynd",
|
"Roslynd",
|
||||||
"jinxedx",
|
"jinxedx",
|
||||||
"Neco28",
|
"Neco28",
|
||||||
@@ -210,22 +214,21 @@
|
|||||||
"Frank Nitty",
|
"Frank Nitty",
|
||||||
"Magic Noob",
|
"Magic Noob",
|
||||||
"DougPeterson",
|
"DougPeterson",
|
||||||
"Antonio Pontes",
|
|
||||||
"ApathyJones",
|
"ApathyJones",
|
||||||
|
"Jeff",
|
||||||
"Bruce",
|
"Bruce",
|
||||||
"Julian V",
|
|
||||||
"Steven Owens",
|
"Steven Owens",
|
||||||
"nahinahi9",
|
|
||||||
"Kevin John Duck",
|
"Kevin John Duck",
|
||||||
"Kevin Christopher",
|
"Kevin Christopher",
|
||||||
"Blackfish95",
|
"Blackfish95",
|
||||||
|
"dd",
|
||||||
"Paul Kroll",
|
"Paul Kroll",
|
||||||
"Bas Imagineer",
|
"Bas Imagineer",
|
||||||
"John Statham",
|
"John Statham",
|
||||||
"yuxz69",
|
"yuxz69",
|
||||||
"esthe",
|
"esthe",
|
||||||
|
"AlexDuKaNa",
|
||||||
"decoy",
|
"decoy",
|
||||||
"CloudValley",
|
|
||||||
"thesoftwaredruid",
|
"thesoftwaredruid",
|
||||||
"wundershark",
|
"wundershark",
|
||||||
"mr_dinosaur",
|
"mr_dinosaur",
|
||||||
@@ -233,15 +236,20 @@
|
|||||||
"Ray Wing",
|
"Ray Wing",
|
||||||
"Ranzitho",
|
"Ranzitho",
|
||||||
"Gus",
|
"Gus",
|
||||||
|
"地獄の禄",
|
||||||
"MJG",
|
"MJG",
|
||||||
"David LaVallee",
|
"David LaVallee",
|
||||||
"linnfrey",
|
|
||||||
"ae",
|
"ae",
|
||||||
"Tr4shP4nda",
|
"Tr4shP4nda",
|
||||||
"capn",
|
"capn",
|
||||||
"Joseph",
|
"Joseph",
|
||||||
|
"Mirko Katzula",
|
||||||
|
"dan",
|
||||||
|
"Piccio08",
|
||||||
|
"kumakichi",
|
||||||
|
"cppbel",
|
||||||
"IamAyam",
|
"IamAyam",
|
||||||
"skaterb949",
|
"jeaness",
|
||||||
"Brian M",
|
"Brian M",
|
||||||
"Josef Lanzl",
|
"Josef Lanzl",
|
||||||
"Nerezza",
|
"Nerezza",
|
||||||
@@ -261,37 +269,32 @@
|
|||||||
"Eris3D",
|
"Eris3D",
|
||||||
"Max Marklund",
|
"Max Marklund",
|
||||||
"m",
|
"m",
|
||||||
|
"Pierce McBride",
|
||||||
"Pronredn",
|
"Pronredn",
|
||||||
|
"Mikko Hemilä",
|
||||||
"Jamie Ogletree",
|
"Jamie Ogletree",
|
||||||
"a _",
|
"a _",
|
||||||
"Jeff",
|
|
||||||
"lh qwe",
|
"lh qwe",
|
||||||
"James Coleman",
|
"James Coleman",
|
||||||
|
"Martial",
|
||||||
"conner",
|
"conner",
|
||||||
"Ouro Boros",
|
"Ouro Boros",
|
||||||
"Chad Idk",
|
"Chad Idk",
|
||||||
"dd",
|
|
||||||
"Princess Bright Eyes",
|
"Princess Bright Eyes",
|
||||||
"Yuji Kaneko",
|
"Yuji Kaneko",
|
||||||
"Dušan Ryban",
|
"Dušan Ryban",
|
||||||
"Felipe dos Santos",
|
"Felipe dos Santos",
|
||||||
|
"Rops Alot",
|
||||||
"Sam",
|
"Sam",
|
||||||
"sjon kreutz",
|
"sjon kreutz",
|
||||||
"Ace Ventura",
|
"Ace Ventura",
|
||||||
"Douglas Gaspar",
|
"Douglas Gaspar",
|
||||||
"Metryman55",
|
"Metryman55",
|
||||||
"AlexDuKaNa",
|
|
||||||
"George",
|
"George",
|
||||||
"dw",
|
"dw",
|
||||||
"地獄の禄",
|
|
||||||
"Gamalonia",
|
"Gamalonia",
|
||||||
"WRL_SPR",
|
"WRL_SPR",
|
||||||
"momokai",
|
"momokai",
|
||||||
"Mirko Katzula",
|
|
||||||
"dan",
|
|
||||||
"Piccio08",
|
|
||||||
"kumakichi",
|
|
||||||
"cppbel",
|
|
||||||
"Moon Knight",
|
"Moon Knight",
|
||||||
"몽타주",
|
"몽타주",
|
||||||
"Kland",
|
"Kland",
|
||||||
@@ -313,12 +316,15 @@
|
|||||||
"kyoumei",
|
"kyoumei",
|
||||||
"RadStorm04",
|
"RadStorm04",
|
||||||
"JohnDoe42054",
|
"JohnDoe42054",
|
||||||
|
"BillyHill",
|
||||||
|
"emyth",
|
||||||
"gzmzmvp",
|
"gzmzmvp",
|
||||||
"Richard",
|
"Richard",
|
||||||
"奚明 刘",
|
"奚明 刘",
|
||||||
"Andrew",
|
"Andrew",
|
||||||
"Robert Wegemund",
|
"Robert Wegemund",
|
||||||
"Littlehuggy",
|
"Littlehuggy",
|
||||||
|
"Gregory Kozhemiak",
|
||||||
"준희 김",
|
"준희 김",
|
||||||
"Brian Buie",
|
"Brian Buie",
|
||||||
"Sadlip",
|
"Sadlip",
|
||||||
@@ -328,16 +334,16 @@
|
|||||||
"Tomohiro Baba",
|
"Tomohiro Baba",
|
||||||
"Mike Simone",
|
"Mike Simone",
|
||||||
"Noora",
|
"Noora",
|
||||||
"Pierce McBride",
|
|
||||||
"Joshua Gray",
|
"Joshua Gray",
|
||||||
"Mattssn",
|
"Mattssn",
|
||||||
"Mikko Hemilä",
|
"Morgandel",
|
||||||
"Jacob McDaniel",
|
"Jacob McDaniel",
|
||||||
|
"X",
|
||||||
|
"Sloan Steddy",
|
||||||
"Temikus",
|
"Temikus",
|
||||||
"Artokun",
|
"Artokun",
|
||||||
"Michael Taylor",
|
"Michael Taylor",
|
||||||
"Derek Baker",
|
"Derek Baker",
|
||||||
"Martial",
|
|
||||||
"Michael Anthony Scott",
|
"Michael Anthony Scott",
|
||||||
"Emil Andersson",
|
"Emil Andersson",
|
||||||
"Atilla Berke Pekduyar",
|
"Atilla Berke Pekduyar",
|
||||||
@@ -345,7 +351,6 @@
|
|||||||
"CryptoTraderJK",
|
"CryptoTraderJK",
|
||||||
"Decx _",
|
"Decx _",
|
||||||
"Davaitamin",
|
"Davaitamin",
|
||||||
"Rops Alot",
|
|
||||||
"tedcor",
|
"tedcor",
|
||||||
"Fotek Design",
|
"Fotek Design",
|
||||||
"四糸凜音",
|
"四糸凜音",
|
||||||
@@ -358,8 +363,6 @@
|
|||||||
"starbugx",
|
"starbugx",
|
||||||
"dc7431",
|
"dc7431",
|
||||||
"Crocket",
|
"Crocket",
|
||||||
"BillyHill",
|
|
||||||
"emyth",
|
|
||||||
"chriphost",
|
"chriphost",
|
||||||
"KitKatM",
|
"KitKatM",
|
||||||
"socrasteeze",
|
"socrasteeze",
|
||||||
@@ -383,35 +386,40 @@
|
|||||||
"g9p0o",
|
"g9p0o",
|
||||||
"TheHolySheep",
|
"TheHolySheep",
|
||||||
"Monte Won",
|
"Monte Won",
|
||||||
"Gregory Kozhemiak",
|
"SpringBootisTrash",
|
||||||
|
"carsten",
|
||||||
|
"ikok",
|
||||||
|
"Wolfe7D1",
|
||||||
"Draven T",
|
"Draven T",
|
||||||
"mrjuan",
|
|
||||||
"elleshar666",
|
"elleshar666",
|
||||||
|
"ACTUALLY_the_Real_Willem_Dafoe",
|
||||||
"Aquatic Coffee",
|
"Aquatic Coffee",
|
||||||
|
"Kauffy",
|
||||||
"John J Linehan",
|
"John J Linehan",
|
||||||
"ethanfel",
|
"ethanfel",
|
||||||
"Elliot E",
|
"Elliot E",
|
||||||
"Morgandel",
|
|
||||||
"Theerat Jiramate",
|
"Theerat Jiramate",
|
||||||
"Focuschannel",
|
"Focuschannel",
|
||||||
"Edward Kennedy",
|
"Edward Kennedy",
|
||||||
"Noah",
|
"Noah",
|
||||||
"X",
|
|
||||||
"Sloan Steddy",
|
|
||||||
"Vane Holzer",
|
"Vane Holzer",
|
||||||
|
"psytrax",
|
||||||
"hexxish",
|
"hexxish",
|
||||||
"Anthony Faxlandez",
|
"Anthony Faxlandez",
|
||||||
"battu",
|
"battu",
|
||||||
"notedfakes",
|
"notedfakes",
|
||||||
"Nathan",
|
"Nathan",
|
||||||
"NICHOLAS BAXLEY",
|
"NICHOLAS BAXLEY",
|
||||||
|
"Michael Scott",
|
||||||
"Pat Hen",
|
"Pat Hen",
|
||||||
"Xeeosat",
|
"Xeeosat",
|
||||||
"Saya",
|
"Saya",
|
||||||
"Ed Wang",
|
"Ed Wang",
|
||||||
"Jordan Shaw",
|
"Jordan Shaw",
|
||||||
|
"Wes Sims",
|
||||||
"g unit",
|
"g unit",
|
||||||
"Srdb",
|
"Srdb",
|
||||||
|
"Filippo Ferrari",
|
||||||
"JC",
|
"JC",
|
||||||
"Prompt Pirate",
|
"Prompt Pirate",
|
||||||
"uwutismxd",
|
"uwutismxd",
|
||||||
@@ -427,9 +435,8 @@
|
|||||||
"Pkrsky",
|
"Pkrsky",
|
||||||
"nanana",
|
"nanana",
|
||||||
"raf8osz",
|
"raf8osz",
|
||||||
"SpringBootisTrash",
|
"FeralOpticsAI",
|
||||||
"carsten",
|
"Pavlaki",
|
||||||
"ikok",
|
|
||||||
"Doug+Rintoul",
|
"Doug+Rintoul",
|
||||||
"Noor",
|
"Noor",
|
||||||
"Yorunai",
|
"Yorunai",
|
||||||
@@ -446,36 +453,34 @@
|
|||||||
"cocona",
|
"cocona",
|
||||||
"ElitaSSJ4",
|
"ElitaSSJ4",
|
||||||
"David Schenck",
|
"David Schenck",
|
||||||
"Wolfe7D1",
|
|
||||||
"blikkies",
|
"blikkies",
|
||||||
"Chris",
|
"Chris",
|
||||||
"Time Valentine",
|
"Time Valentine",
|
||||||
"Shock Shockor",
|
"Shock Shockor",
|
||||||
"ACTUALLY_the_Real_Willem_Dafoe",
|
|
||||||
"Михал Михалыч",
|
"Михал Михалыч",
|
||||||
"Matt",
|
"Matt",
|
||||||
"Goldwaters",
|
"Goldwaters",
|
||||||
"Kauffy",
|
|
||||||
"Zude",
|
"Zude",
|
||||||
|
"Frogmilk",
|
||||||
"SPJ",
|
"SPJ",
|
||||||
"Kyler",
|
"Kyler",
|
||||||
|
"Bryan Rutkowski",
|
||||||
"Justin Blaylock",
|
"Justin Blaylock",
|
||||||
"aRtFuL_DodGeR",
|
"aRtFuL_DodGeR",
|
||||||
"Nick Kage",
|
"Nick Kage",
|
||||||
"psytrax",
|
|
||||||
"Cyrus Fett",
|
"Cyrus Fett",
|
||||||
"Xenon Xue",
|
"Xenon Xue",
|
||||||
"Edward Ten Eyck",
|
"Edward Ten Eyck",
|
||||||
"Billy Gladky",
|
"Billy Gladky",
|
||||||
"Michael Scott",
|
|
||||||
"Probis",
|
"Probis",
|
||||||
"Solixer",
|
"Solixer",
|
||||||
"Wes Sims",
|
|
||||||
"ItsGeneralButtNaked",
|
"ItsGeneralButtNaked",
|
||||||
"Donor4115",
|
"Donor4115",
|
||||||
|
"jinksta187",
|
||||||
"Distortik",
|
"Distortik",
|
||||||
"Manu Thetug",
|
"Manu Thetug",
|
||||||
"Filippo Ferrari",
|
"Karlanx",
|
||||||
|
"operationancut",
|
||||||
"Youguang",
|
"Youguang",
|
||||||
"andrewzpong",
|
"andrewzpong",
|
||||||
"BossGame",
|
"BossGame",
|
||||||
@@ -486,11 +491,18 @@
|
|||||||
"Kevinj",
|
"Kevinj",
|
||||||
"Mitchell Robson",
|
"Mitchell Robson",
|
||||||
"POPPIN",
|
"POPPIN",
|
||||||
|
"YassineKhaled",
|
||||||
|
"Y",
|
||||||
|
"MatteKey",
|
||||||
|
"Flob",
|
||||||
|
"ShiroSenpai",
|
||||||
|
"Inkognito",
|
||||||
"G",
|
"G",
|
||||||
"Tan+Huynh",
|
"Tan+Huynh",
|
||||||
"Bob+Barker",
|
"Bob+Barker",
|
||||||
"D",
|
"D",
|
||||||
"Dark_Pest",
|
"Dark_Pest",
|
||||||
|
"Eldithor",
|
||||||
"Alex",
|
"Alex",
|
||||||
"Karru",
|
"Karru",
|
||||||
"ChaChanoKo",
|
"ChaChanoKo",
|
||||||
@@ -503,13 +515,10 @@
|
|||||||
"g",
|
"g",
|
||||||
"J",
|
"J",
|
||||||
"Alan+Cano",
|
"Alan+Cano",
|
||||||
"FeralOpticsAI",
|
|
||||||
"Pavlaki",
|
|
||||||
"BillyBoy84",
|
"BillyBoy84",
|
||||||
"Buecyb99",
|
"Buecyb99",
|
||||||
"Welkor",
|
"Welkor",
|
||||||
"John Martin",
|
"John Martin",
|
||||||
"Ink Temptation",
|
|
||||||
"JBsuede",
|
"JBsuede",
|
||||||
"moranqianlong",
|
"moranqianlong",
|
||||||
"Kalli Core",
|
"Kalli Core",
|
||||||
@@ -519,13 +528,12 @@
|
|||||||
"Dave Abraham",
|
"Dave Abraham",
|
||||||
"Joaquin Hierrezuelo",
|
"Joaquin Hierrezuelo",
|
||||||
"Locrospiel",
|
"Locrospiel",
|
||||||
"Frogmilk",
|
|
||||||
"Sean voets",
|
"Sean voets",
|
||||||
|
"Jarrid Lee",
|
||||||
"Kor",
|
"Kor",
|
||||||
"Joseph Hanson",
|
"Joseph Hanson",
|
||||||
"John Rednoulf",
|
"John Rednoulf",
|
||||||
"Kyron Mahan",
|
"Kyron Mahan",
|
||||||
"Bryan Rutkowski",
|
|
||||||
"Boba Smith",
|
"Boba Smith",
|
||||||
"TBitz33",
|
"TBitz33",
|
||||||
"Anonym dkjglfleeoeldldldlkf",
|
"Anonym dkjglfleeoeldldldlkf",
|
||||||
@@ -544,15 +552,12 @@
|
|||||||
"Pete Pain",
|
"Pete Pain",
|
||||||
"Jacob Winter",
|
"Jacob Winter",
|
||||||
"Ryan Presley Ng",
|
"Ryan Presley Ng",
|
||||||
"jinksta187",
|
|
||||||
"RHopkirk",
|
"RHopkirk",
|
||||||
"Andrew Wilkinson",
|
"Andrew Wilkinson",
|
||||||
"Karlanx",
|
|
||||||
"Lyavph",
|
"Lyavph",
|
||||||
"Maxim",
|
"Maxim",
|
||||||
"David",
|
"David",
|
||||||
"Meilo",
|
"Meilo",
|
||||||
"operationancut",
|
|
||||||
"shinonomeiro",
|
"shinonomeiro",
|
||||||
"Snille",
|
"Snille",
|
||||||
"MaartenAlbers",
|
"MaartenAlbers",
|
||||||
@@ -571,6 +576,8 @@
|
|||||||
"Scott",
|
"Scott",
|
||||||
"Muratoraccio",
|
"Muratoraccio",
|
||||||
"D",
|
"D",
|
||||||
|
"Akkas+Haque",
|
||||||
|
"Kachac",
|
||||||
"SAVEagleBasement",
|
"SAVEagleBasement",
|
||||||
"Kevin+Isom",
|
"Kevin+Isom",
|
||||||
"Rune+Osnes",
|
"Rune+Osnes",
|
||||||
@@ -600,12 +607,6 @@
|
|||||||
"Lev+Lanevskiy",
|
"Lev+Lanevskiy",
|
||||||
"low9",
|
"low9",
|
||||||
"Winged",
|
"Winged",
|
||||||
"YassineKhaled",
|
|
||||||
"Y",
|
|
||||||
"MatteKey",
|
|
||||||
"Flob",
|
|
||||||
"ShiroSenpai",
|
|
||||||
"Inkognito",
|
|
||||||
"Jacky+Ho",
|
"Jacky+Ho",
|
||||||
"generic404",
|
"generic404",
|
||||||
"abattoirblues",
|
"abattoirblues",
|
||||||
@@ -614,29 +615,34 @@
|
|||||||
"hayden",
|
"hayden",
|
||||||
"Obsidian.Studios",
|
"Obsidian.Studios",
|
||||||
"ahoystan",
|
"ahoystan",
|
||||||
|
"Zomba Mann",
|
||||||
"edk",
|
"edk",
|
||||||
"Tú Nguyễn Lý Hoàng",
|
"Tú Nguyễn Lý Hoàng",
|
||||||
"shira1011",
|
"shira1011",
|
||||||
|
"Neko Desco",
|
||||||
"Ben D",
|
"Ben D",
|
||||||
"G",
|
"G",
|
||||||
|
"Vinarus",
|
||||||
"ja s",
|
"ja s",
|
||||||
"Leslie Andrew Ridings",
|
"Leslie Andrew Ridings",
|
||||||
"Doug Mason",
|
"Doug Mason",
|
||||||
"scoreswazey",
|
"scoreswazey",
|
||||||
"Owen Gwosdz",
|
"Owen Gwosdz",
|
||||||
"Jarrid Lee",
|
|
||||||
"Poophead27 Blyat",
|
"Poophead27 Blyat",
|
||||||
"Mythspire",
|
"Mythspire",
|
||||||
"AZ Party Oasis",
|
"AZ Party Oasis",
|
||||||
"Devil Lude",
|
"Devil Lude",
|
||||||
"David Murcko",
|
"David Murcko",
|
||||||
|
"TheFusion",
|
||||||
"MR.Bear",
|
"MR.Bear",
|
||||||
"Jack Dole",
|
"Jack Dole",
|
||||||
"matt",
|
"matt",
|
||||||
"somethingtosay8",
|
"somethingtosay8",
|
||||||
|
"3zS4QNQ4",
|
||||||
"Terminuz",
|
"Terminuz",
|
||||||
"ivistorm",
|
"ivistorm",
|
||||||
"max blo",
|
"max blo",
|
||||||
|
"Ivan Imes",
|
||||||
"CptNeo",
|
"CptNeo",
|
||||||
"Jack Lawfield",
|
"Jack Lawfield",
|
||||||
"Borte",
|
"Borte",
|
||||||
@@ -645,7 +651,7 @@
|
|||||||
"Sage Himeros",
|
"Sage Himeros",
|
||||||
"Eric Ketchum",
|
"Eric Ketchum",
|
||||||
"Kevin Wallace",
|
"Kevin Wallace",
|
||||||
"David Spearing",
|
"Zeeble",
|
||||||
"ChicRic",
|
"ChicRic",
|
||||||
"Tigon",
|
"Tigon",
|
||||||
"BastardSama",
|
"BastardSama",
|
||||||
@@ -706,6 +712,9 @@
|
|||||||
"SelfishMedic",
|
"SelfishMedic",
|
||||||
"adderleighn",
|
"adderleighn",
|
||||||
"EnragedAntelope",
|
"EnragedAntelope",
|
||||||
|
"Somebody",
|
||||||
|
"Jasper",
|
||||||
|
"megameganck",
|
||||||
"thomasand01",
|
"thomasand01",
|
||||||
"Shiba+Sama",
|
"Shiba+Sama",
|
||||||
"miduzza",
|
"miduzza",
|
||||||
@@ -730,8 +739,6 @@
|
|||||||
"matter",
|
"matter",
|
||||||
"SRCRCOSS",
|
"SRCRCOSS",
|
||||||
"imer",
|
"imer",
|
||||||
"Akkas+Haque",
|
|
||||||
"Kachac",
|
|
||||||
"jcx29",
|
"jcx29",
|
||||||
"Drizzly",
|
"Drizzly",
|
||||||
"Nebuleux",
|
"Nebuleux",
|
||||||
@@ -751,24 +758,23 @@
|
|||||||
"KUJYAKU",
|
"KUJYAKU",
|
||||||
"Coeur+de+cochon",
|
"Coeur+de+cochon",
|
||||||
"han b",
|
"han b",
|
||||||
"Zomba Mann",
|
|
||||||
"Aquaneo",
|
"Aquaneo",
|
||||||
"Nico",
|
"Nico",
|
||||||
"Maximilian Krischan",
|
"Maximilian Krischan",
|
||||||
"Banana Joe",
|
"Banana Joe",
|
||||||
"proto merp",
|
"proto merp",
|
||||||
"_ G3n",
|
"_ G3n",
|
||||||
|
"Brandon Thomas",
|
||||||
"Donovan Jenkins",
|
"Donovan Jenkins",
|
||||||
"Hans Meier",
|
"Hans Meier",
|
||||||
"sicarius",
|
"sicarius",
|
||||||
"Michael Eid",
|
"Michael Eid",
|
||||||
"Wolf and Fox Legends",
|
"Wolf and Fox Legends",
|
||||||
"beersandbacon",
|
"beersandbacon",
|
||||||
"Neko Desco",
|
|
||||||
"Bob barker",
|
"Bob barker",
|
||||||
"Ninja Tom",
|
"Ninja Tom",
|
||||||
"karim ben brik",
|
"karim ben brik",
|
||||||
"Vinarus",
|
"Elemnt",
|
||||||
"Josh Snyder",
|
"Josh Snyder",
|
||||||
"Michael Zhu",
|
"Michael Zhu",
|
||||||
"Nemisu",
|
"Nemisu",
|
||||||
@@ -792,16 +798,15 @@
|
|||||||
"Forbidden Atelier",
|
"Forbidden Atelier",
|
||||||
"Thomas Sankowski",
|
"Thomas Sankowski",
|
||||||
"DrB",
|
"DrB",
|
||||||
|
"Nimhloth",
|
||||||
"Adictedtohumping",
|
"Adictedtohumping",
|
||||||
"Snorklebort",
|
"Snorklebort",
|
||||||
"vinter",
|
"vinter",
|
||||||
"Towelie",
|
"Towelie",
|
||||||
"TheFusion",
|
|
||||||
"Jean-françois SEMA",
|
"Jean-françois SEMA",
|
||||||
"3zS4QNQ4",
|
|
||||||
"Kurt",
|
"Kurt",
|
||||||
|
"Andrew Ly",
|
||||||
"Matt M.",
|
"Matt M.",
|
||||||
"Ivan Imes",
|
|
||||||
"J M",
|
"J M",
|
||||||
"Slacks",
|
"Slacks",
|
||||||
"Bouya shaka",
|
"Bouya shaka",
|
||||||
@@ -837,5 +842,5 @@
|
|||||||
"Somebody",
|
"Somebody",
|
||||||
"CK"
|
"CK"
|
||||||
],
|
],
|
||||||
"totalCount": 834
|
"totalCount": 839
|
||||||
}
|
}
|
||||||
@@ -514,11 +514,21 @@ class CivitaiApiMetadataParser(RecipeMetadataParser):
|
|||||||
|
|
||||||
result["loras"].append(lora_entry)
|
result["loras"].append(lora_entry)
|
||||||
|
|
||||||
# Process modelVersionIds from Civitai image API
|
# Process modelVersionIds from Civitai image API.
|
||||||
# These are model version IDs returned at root level when meta doesn't contain resources
|
# These are version IDs returned at root level of the API response.
|
||||||
if "modelVersionIds" in metadata and isinstance(
|
# When resources or civitaiResources are already present in metadata
|
||||||
metadata["modelVersionIds"], list
|
# (which they are when ?withMeta=true is passed), those sections have
|
||||||
|
# complete hash/type information — modelVersionIds is a fallback for
|
||||||
|
# when meta is null and only the flat ID list is available. Skipping
|
||||||
|
# it here avoids duplicates: the same file hash often resolves to
|
||||||
|
# different version IDs via hash lookup (resources) vs the original
|
||||||
|
# version ID in modelVersionIds, and both paths would create entries.
|
||||||
|
if (
|
||||||
|
"modelVersionIds" in metadata
|
||||||
|
and isinstance(metadata["modelVersionIds"], list)
|
||||||
|
and not result.get("loras")
|
||||||
):
|
):
|
||||||
|
|
||||||
for version_id in metadata["modelVersionIds"]:
|
for version_id in metadata["modelVersionIds"]:
|
||||||
version_id_str = str(version_id)
|
version_id_str = str(version_id)
|
||||||
|
|
||||||
@@ -526,6 +536,13 @@ class CivitaiApiMetadataParser(RecipeMetadataParser):
|
|||||||
if version_id_str in added_loras:
|
if version_id_str in added_loras:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Skip if this version ID is already the recipe's checkpoint
|
||||||
|
# (resolved earlier from embedded resources/Model hash,
|
||||||
|
# avoiding a duplicate CivitAI API call).
|
||||||
|
existing_model = result.get("model")
|
||||||
|
if existing_model and str(existing_model.get("id")) == version_id_str:
|
||||||
|
continue
|
||||||
|
|
||||||
# Initialize lora entry with version ID
|
# Initialize lora entry with version ID
|
||||||
lora_entry = {
|
lora_entry = {
|
||||||
"id": version_id,
|
"id": version_id,
|
||||||
@@ -559,9 +576,40 @@ class CivitaiApiMetadataParser(RecipeMetadataParser):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if populated_entry is None:
|
if populated_entry is None:
|
||||||
continue # Skip invalid LoRA types
|
# Not a LoRA — try as checkpoint (only if we
|
||||||
|
# don't already have one). Reuses the same
|
||||||
|
# civitai_info from the API call above so no
|
||||||
|
# extra query is made.
|
||||||
|
if result["model"] is None:
|
||||||
|
checkpoint_entry = {
|
||||||
|
"id": version_id,
|
||||||
|
"modelId": 0,
|
||||||
|
"name": "Unknown Model",
|
||||||
|
"version": "",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"existsLocally": False,
|
||||||
|
"localPath": None,
|
||||||
|
"file_name": "",
|
||||||
|
"hash": "",
|
||||||
|
"thumbnailUrl": (
|
||||||
|
"/loras_static/images/no-preview.png"
|
||||||
|
),
|
||||||
|
"baseModel": "",
|
||||||
|
"size": 0,
|
||||||
|
"downloadUrl": "",
|
||||||
|
"isDeleted": False,
|
||||||
|
}
|
||||||
|
cp_populated = await (
|
||||||
|
self.populate_checkpoint_from_civitai(
|
||||||
|
checkpoint_entry, civitai_info
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if cp_populated.get("modelId"):
|
||||||
|
result["model"] = cp_populated
|
||||||
|
continue # Not a LoRA, don't add to loras
|
||||||
|
|
||||||
lora_entry = populated_entry
|
lora_entry = populated_entry
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Error fetching Civitai info for model version {version_id}: {e}"
|
f"Error fetching Civitai info for model version {version_id}: {e}"
|
||||||
|
|||||||
@@ -414,9 +414,10 @@ class PromptServerProtocol(Protocol):
|
|||||||
"""Subset of PromptServer used by the handlers."""
|
"""Subset of PromptServer used by the handlers."""
|
||||||
|
|
||||||
instance: "PromptServerProtocol"
|
instance: "PromptServerProtocol"
|
||||||
|
sockets: dict # maps clientId (sid) → WebSocketResponse
|
||||||
|
|
||||||
def send_sync(
|
def send_sync(
|
||||||
self, event: str, payload: dict
|
self, event: str, payload: dict | None = None, sid: str | None = None
|
||||||
) -> None: # pragma: no cover - protocol
|
) -> None: # pragma: no cover - protocol
|
||||||
...
|
...
|
||||||
|
|
||||||
@@ -471,90 +472,154 @@ class BackupServiceProtocol(Protocol):
|
|||||||
|
|
||||||
|
|
||||||
class NodeRegistry:
|
class NodeRegistry:
|
||||||
"""Thread-safe registry for tracking LoRA nodes in active workflows."""
|
"""Thread-safe registry for tracking LoRA nodes across ComfyUI tabs.
|
||||||
|
|
||||||
|
Each connected ComfyUI browser tab (identified by its ``sid`` / ``clientId``)
|
||||||
|
registers its own set of workflow nodes. Queries merge all known tabs into
|
||||||
|
a single result so that the calling LM panel always sees *every* available
|
||||||
|
target node, regardless of which tab responded fastest.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
self._nodes: Dict[str, dict] = {}
|
# sid → {unique_id → node_info}
|
||||||
self._registry_updated = asyncio.Event()
|
self._tab_nodes: Dict[str, Dict[str, dict]] = {}
|
||||||
|
self._ready = asyncio.Event()
|
||||||
|
self._waiting_clients: set[str] = set()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pending_client_count(self) -> int:
|
||||||
|
"""Number of clients that have not yet responded in the current refresh cycle."""
|
||||||
|
return len(self._waiting_clients)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Helpers to build one node dict (extracted so it's reused for each tab)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@staticmethod
|
||||||
|
def _build_node_dict(node: dict) -> dict:
|
||||||
|
node_id = node["node_id"]
|
||||||
|
graph_id = str(node["graph_id"])
|
||||||
|
unique_id = f"{graph_id}:{node_id}"
|
||||||
|
node_type = node.get("type", "")
|
||||||
|
type_id = NODE_TYPES.get(node_type, 0)
|
||||||
|
bgcolor = node.get("bgcolor") or DEFAULT_NODE_COLOR
|
||||||
|
|
||||||
|
raw_capabilities = node.get("capabilities")
|
||||||
|
capabilities: dict = {}
|
||||||
|
if isinstance(raw_capabilities, dict):
|
||||||
|
capabilities = dict(raw_capabilities)
|
||||||
|
|
||||||
|
raw_widget_names: list | None = node.get("widget_names")
|
||||||
|
if not isinstance(raw_widget_names, list):
|
||||||
|
capability_widget_names = capabilities.get("widget_names")
|
||||||
|
raw_widget_names = (
|
||||||
|
capability_widget_names
|
||||||
|
if isinstance(capability_widget_names, list)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
widget_names: list[str] = []
|
||||||
|
if isinstance(raw_widget_names, list):
|
||||||
|
widget_names = [
|
||||||
|
str(widget_name)
|
||||||
|
for widget_name in raw_widget_names
|
||||||
|
if isinstance(widget_name, str) and widget_name
|
||||||
|
]
|
||||||
|
|
||||||
|
if widget_names:
|
||||||
|
capabilities["widget_names"] = widget_names
|
||||||
|
else:
|
||||||
|
capabilities.pop("widget_names", None)
|
||||||
|
|
||||||
|
if "supports_lora" in capabilities:
|
||||||
|
capabilities["supports_lora"] = bool(capabilities["supports_lora"])
|
||||||
|
|
||||||
|
comfy_class = node.get("comfy_class")
|
||||||
|
if not isinstance(comfy_class, str) or not comfy_class:
|
||||||
|
comfy_class = node_type if isinstance(node_type, str) else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": node_id,
|
||||||
|
"graph_id": graph_id,
|
||||||
|
"graph_name": node.get("graph_name"),
|
||||||
|
"unique_id": unique_id,
|
||||||
|
"bgcolor": bgcolor,
|
||||||
|
"title": node.get("title"),
|
||||||
|
"type": type_id,
|
||||||
|
"type_name": node_type,
|
||||||
|
"comfy_class": comfy_class,
|
||||||
|
"capabilities": capabilities,
|
||||||
|
"widget_names": widget_names,
|
||||||
|
"mode": node.get("mode"),
|
||||||
|
"marker_role": node.get("marker_role"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Public API
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
async def register_nodes(self, sid: str, nodes: list[dict]) -> None:
|
||||||
|
"""Register/replace the node list for a single ComfyUI tab (identified by *sid*)."""
|
||||||
|
tab_nodes: dict[str, dict] = {}
|
||||||
|
for node in nodes:
|
||||||
|
nd = self._build_node_dict(node)
|
||||||
|
tab_nodes[nd["unique_id"]] = nd
|
||||||
|
|
||||||
async def register_nodes(self, nodes: list[dict]) -> None:
|
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
self._nodes.clear()
|
self._tab_nodes[sid] = tab_nodes
|
||||||
for node in nodes:
|
self._waiting_clients.discard(sid)
|
||||||
node_id = node["node_id"]
|
if not self._waiting_clients:
|
||||||
graph_id = str(node["graph_id"])
|
self._ready.set()
|
||||||
unique_id = f"{graph_id}:{node_id}"
|
|
||||||
node_type = node.get("type", "")
|
|
||||||
type_id = NODE_TYPES.get(node_type, 0)
|
|
||||||
bgcolor = node.get("bgcolor") or DEFAULT_NODE_COLOR
|
|
||||||
raw_capabilities = node.get("capabilities")
|
|
||||||
capabilities: dict = {}
|
|
||||||
if isinstance(raw_capabilities, dict):
|
|
||||||
capabilities = dict(raw_capabilities)
|
|
||||||
|
|
||||||
raw_widget_names: list | None = node.get("widget_names")
|
logger.debug("Registered %s nodes from client %s", len(nodes), sid)
|
||||||
if not isinstance(raw_widget_names, list):
|
|
||||||
capability_widget_names = capabilities.get("widget_names")
|
|
||||||
raw_widget_names = (
|
|
||||||
capability_widget_names
|
|
||||||
if isinstance(capability_widget_names, list)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
widget_names: list[str] = []
|
def prepare_for_refresh(self, active_sids: list[str]) -> None:
|
||||||
if isinstance(raw_widget_names, list):
|
"""Set the list of client IDs we expect to hear from during the next refresh cycle."""
|
||||||
widget_names = [
|
self._ready.clear()
|
||||||
str(widget_name)
|
self._waiting_clients = set(active_sids)
|
||||||
for widget_name in raw_widget_names
|
|
||||||
if isinstance(widget_name, str) and widget_name
|
|
||||||
]
|
|
||||||
|
|
||||||
if widget_names:
|
async def wait_for_all(self, timeout: float = 2.0) -> bool:
|
||||||
capabilities["widget_names"] = widget_names
|
"""Block until every client in the current waiting set has responded
|
||||||
else:
|
(or *timeout* seconds elapse). Returns ``True`` if all responded."""
|
||||||
capabilities.pop("widget_names", None)
|
if not self._waiting_clients:
|
||||||
|
return True
|
||||||
if "supports_lora" in capabilities:
|
|
||||||
capabilities["supports_lora"] = bool(capabilities["supports_lora"])
|
|
||||||
|
|
||||||
comfy_class = node.get("comfy_class")
|
|
||||||
if not isinstance(comfy_class, str) or not comfy_class:
|
|
||||||
comfy_class = node_type if isinstance(node_type, str) else None
|
|
||||||
|
|
||||||
self._nodes[unique_id] = {
|
|
||||||
"id": node_id,
|
|
||||||
"graph_id": graph_id,
|
|
||||||
"graph_name": node.get("graph_name"),
|
|
||||||
"unique_id": unique_id,
|
|
||||||
"bgcolor": bgcolor,
|
|
||||||
"title": node.get("title"),
|
|
||||||
"type": type_id,
|
|
||||||
"type_name": node_type,
|
|
||||||
"comfy_class": comfy_class,
|
|
||||||
"capabilities": capabilities,
|
|
||||||
"widget_names": widget_names,
|
|
||||||
"mode": node.get("mode"),
|
|
||||||
"marker_role": node.get("marker_role"),
|
|
||||||
}
|
|
||||||
logger.debug("Registered %s nodes in registry", len(nodes))
|
|
||||||
self._registry_updated.set()
|
|
||||||
|
|
||||||
async def get_registry(self) -> dict:
|
|
||||||
async with self._lock:
|
|
||||||
return {
|
|
||||||
"nodes": dict(self._nodes),
|
|
||||||
"node_count": len(self._nodes),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def wait_for_update(self, timeout: float = 1.0) -> bool:
|
|
||||||
self._registry_updated.clear()
|
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(self._registry_updated.wait(), timeout=timeout)
|
await asyncio.wait_for(self._ready.wait(), timeout=timeout)
|
||||||
return True
|
return True
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def get_merged_registry(self, active_sids: set[str] | None = None) -> dict:
|
||||||
|
"""Return the union of all known tab nodes, pruning any tab that is no
|
||||||
|
longer connected."""
|
||||||
|
async with self._lock:
|
||||||
|
# Garbage-collect stale entries (disconnected tabs)
|
||||||
|
if active_sids is not None:
|
||||||
|
for sid in list(self._tab_nodes):
|
||||||
|
if sid not in active_sids:
|
||||||
|
del self._tab_nodes[sid]
|
||||||
|
|
||||||
|
merged: dict[str, dict] = {}
|
||||||
|
tab_info: dict[str, dict] = {}
|
||||||
|
for sid, nodes in self._tab_nodes.items():
|
||||||
|
tab_info[sid] = {
|
||||||
|
"node_count": len(nodes),
|
||||||
|
"graph_names": list(
|
||||||
|
{
|
||||||
|
n.get("graph_name")
|
||||||
|
for n in nodes.values()
|
||||||
|
if n.get("graph_name")
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
merged.update(nodes)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"nodes": merged,
|
||||||
|
"node_count": len(merged),
|
||||||
|
"tab_count": len(self._tab_nodes),
|
||||||
|
"tabs": tab_info,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class HealthCheckHandler:
|
class HealthCheckHandler:
|
||||||
async def health_check(self, request: web.Request) -> web.Response:
|
async def health_check(self, request: web.Request) -> web.Response:
|
||||||
@@ -2995,10 +3060,21 @@ class NodeRegistryHandler:
|
|||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
nodes = data.get("nodes", [])
|
nodes = data.get("nodes", [])
|
||||||
|
client_id = data.get("client_id")
|
||||||
if not isinstance(nodes, list):
|
if not isinstance(nodes, list):
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"success": False, "error": "nodes must be a list"}, status=400
|
{"success": False, "error": "nodes must be a list"}, status=400
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not isinstance(client_id, str) or not client_id:
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"error": "Missing client_id parameter",
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
for index, node in enumerate(nodes):
|
for index, node in enumerate(nodes):
|
||||||
if not isinstance(node, dict):
|
if not isinstance(node, dict):
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
@@ -3042,7 +3118,7 @@ class NodeRegistryHandler:
|
|||||||
else:
|
else:
|
||||||
node["graph_name"] = str(graph_name)
|
node["graph_name"] = str(graph_name)
|
||||||
|
|
||||||
await self._node_registry.register_nodes(nodes)
|
await self._node_registry.register_nodes(client_id, nodes)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -3066,9 +3142,15 @@ class NodeRegistryHandler:
|
|||||||
status=503,
|
status=503,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Snapshot of currently-connected ComfyUI tabs
|
||||||
|
active_sids = list(self._prompt_server.instance.sockets.keys())
|
||||||
|
self._node_registry.prepare_for_refresh(active_sids)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._prompt_server.instance.send_sync("lora_registry_refresh", {})
|
self._prompt_server.instance.send_sync("lora_registry_refresh", {})
|
||||||
logger.debug("Sent registry refresh request to frontend")
|
logger.debug(
|
||||||
|
"Sent registry refresh request (expecting %s clients)", len(active_sids)
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("Failed to send registry refresh message: %s", exc)
|
logger.error("Failed to send registry refresh message: %s", exc)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
@@ -3080,19 +3162,31 @@ class NodeRegistryHandler:
|
|||||||
status=500,
|
status=500,
|
||||||
)
|
)
|
||||||
|
|
||||||
registry_updated = await self._node_registry.wait_for_update(timeout=1.0)
|
if not await self._node_registry.wait_for_all(timeout=2.0):
|
||||||
if not registry_updated:
|
logger.warning(
|
||||||
logger.warning("Registry refresh timeout after 1 second")
|
"Registry refresh timeout after 2s (%s/%s clients responded)",
|
||||||
|
len(active_sids) - self._node_registry.pending_client_count,
|
||||||
|
len(active_sids),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Re-read current sockets after the wait: a tab may have connected
|
||||||
|
# while we were waiting, and we don't want to garbage-collect it.
|
||||||
|
current_sids = set(self._prompt_server.instance.sockets.keys())
|
||||||
|
registry_info = await self._node_registry.get_merged_registry(
|
||||||
|
active_sids=current_sids
|
||||||
|
)
|
||||||
|
|
||||||
|
if registry_info["node_count"] == 0:
|
||||||
|
logger.warning("No nodes registered after refresh")
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "Timeout Error",
|
"error": "Empty Registry",
|
||||||
"message": "Registry refresh timeout - ComfyUI frontend may not be responsive",
|
"message": "No workflow nodes found — ensure ComfyUI is open and the extension is loaded.",
|
||||||
},
|
},
|
||||||
status=408,
|
status=408,
|
||||||
)
|
)
|
||||||
|
|
||||||
registry_info = await self._node_registry.get_registry()
|
|
||||||
return web.json_response({"success": True, "data": registry_info})
|
return web.json_response({"success": True, "data": registry_info})
|
||||||
except Exception as exc: # pragma: no cover - defensive logging
|
except Exception as exc: # pragma: no cover - defensive logging
|
||||||
logger.error("Failed to get registry: %s", exc, exc_info=True)
|
logger.error("Failed to get registry: %s", exc, exc_info=True)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from ...utils.civitai_utils import (
|
|||||||
extract_civitai_image_id_from_cdn_url,
|
extract_civitai_image_id_from_cdn_url,
|
||||||
rewrite_preview_url,
|
rewrite_preview_url,
|
||||||
)
|
)
|
||||||
|
from ...utils.constants import NSFW_LEVELS
|
||||||
from ...utils.exif_utils import ExifUtils
|
from ...utils.exif_utils import ExifUtils
|
||||||
from ...recipes.merger import GenParamsMerger
|
from ...recipes.merger import GenParamsMerger
|
||||||
from ...recipes.enrichment import RecipeEnricher
|
from ...recipes.enrichment import RecipeEnricher
|
||||||
@@ -1120,6 +1121,13 @@ class RecipeManagementHandler:
|
|||||||
if parsed_embedded.get("base_model") and not metadata.get("base_model"):
|
if parsed_embedded.get("base_model") and not metadata.get("base_model"):
|
||||||
metadata["base_model"] = parsed_embedded["base_model"]
|
metadata["base_model"] = parsed_embedded["base_model"]
|
||||||
|
|
||||||
|
# Extract preview_nsfw_level from the CivitAI API response
|
||||||
|
# (injected into civitai_meta_raw by _download_remote_media).
|
||||||
|
if isinstance(civitai_meta_raw, dict):
|
||||||
|
bl = civitai_meta_raw.get("browsingLevel")
|
||||||
|
if isinstance(bl, int) and bl > 0:
|
||||||
|
metadata["preview_nsfw_level"] = bl
|
||||||
|
|
||||||
civitai_client = self._civitai_client_getter()
|
civitai_client = self._civitai_client_getter()
|
||||||
await RecipeEnricher.enrich_recipe(
|
await RecipeEnricher.enrich_recipe(
|
||||||
recipe=metadata,
|
recipe=metadata,
|
||||||
@@ -1515,8 +1523,31 @@ class RecipeManagementHandler:
|
|||||||
# CivitAI API returns modelVersionIds at the root level of
|
# CivitAI API returns modelVersionIds at the root level of
|
||||||
# the image response, NOT inside the meta object.
|
# the image response, NOT inside the meta object.
|
||||||
mvids = image_info.get("modelVersionIds")
|
mvids = image_info.get("modelVersionIds")
|
||||||
if mvids and isinstance(civitai_meta_raw, dict):
|
if mvids:
|
||||||
civitai_meta_raw["modelVersionIds"] = mvids
|
if isinstance(civitai_meta_raw, dict):
|
||||||
|
civitai_meta_raw["modelVersionIds"] = mvids
|
||||||
|
else:
|
||||||
|
# meta is null but modelVersionIds exists — create a
|
||||||
|
# minimal dict so downstream parsers can discover
|
||||||
|
# LoRAs and checkpoints from the API response.
|
||||||
|
civitai_meta_raw = {"modelVersionIds": mvids}
|
||||||
|
|
||||||
|
# Inject browsingLevel (canonical integer) so the recipe's
|
||||||
|
# preview_nsfw_level can be set, enabling proper NSFW blur
|
||||||
|
# of the preview image. Fall back to nsfwLevel (string)
|
||||||
|
# when browsingLevel is absent.
|
||||||
|
if isinstance(civitai_meta_raw, dict):
|
||||||
|
browsing_level = image_info.get("browsingLevel")
|
||||||
|
nsfw_level_str = image_info.get("nsfwLevel")
|
||||||
|
if isinstance(browsing_level, int) and browsing_level > 0:
|
||||||
|
civitai_meta_raw["browsingLevel"] = browsing_level
|
||||||
|
elif (
|
||||||
|
isinstance(nsfw_level_str, str)
|
||||||
|
and nsfw_level_str in NSFW_LEVELS
|
||||||
|
):
|
||||||
|
civitai_meta_raw["browsingLevel"] = NSFW_LEVELS[
|
||||||
|
nsfw_level_str
|
||||||
|
]
|
||||||
|
|
||||||
original_url = (
|
original_url = (
|
||||||
image_info.get("url") if civitai_image_id and image_info else None
|
image_info.get("url") if civitai_image_id and image_info else None
|
||||||
@@ -1796,6 +1827,13 @@ class RecipeManagementHandler:
|
|||||||
"source_path": image_url,
|
"source_path": image_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Extract preview_nsfw_level from the CivitAI API response
|
||||||
|
# (injected into civitai_meta_raw by _download_remote_media).
|
||||||
|
if isinstance(civitai_meta_raw, dict):
|
||||||
|
bl = civitai_meta_raw.get("browsingLevel")
|
||||||
|
if isinstance(bl, int) and bl > 0:
|
||||||
|
metadata["preview_nsfw_level"] = bl
|
||||||
|
|
||||||
if civitai_parsed:
|
if civitai_parsed:
|
||||||
civitai_loras = civitai_parsed.get("loras", [])
|
civitai_loras = civitai_parsed.get("loras", [])
|
||||||
if civitai_loras and not metadata.get("loras"):
|
if civitai_loras and not metadata.get("loras"):
|
||||||
|
|||||||
@@ -523,6 +523,10 @@ class BatchImportService:
|
|||||||
if payload.get("checkpoint"):
|
if payload.get("checkpoint"):
|
||||||
metadata["checkpoint"] = payload["checkpoint"]
|
metadata["checkpoint"] = payload["checkpoint"]
|
||||||
|
|
||||||
|
nsfw = payload.get("preview_nsfw_level")
|
||||||
|
if isinstance(nsfw, int) and nsfw > 0:
|
||||||
|
metadata["preview_nsfw_level"] = nsfw
|
||||||
|
|
||||||
image_bytes = None
|
image_bytes = None
|
||||||
image_base64 = payload.get("image_base64")
|
image_base64 = payload.get("image_base64")
|
||||||
|
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ class CivArchiveClient:
|
|||||||
|
|
||||||
if version_id is not None:
|
if version_id is not None:
|
||||||
raw_id = version_data.get("id")
|
raw_id = version_data.get("id")
|
||||||
if raw_id != version_id:
|
if raw_id is not None and str(raw_id) != str(version_id):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Requested version %s doesn't match default version %s for model %s",
|
"Requested version %s doesn't match default version %s for model %s",
|
||||||
version_id,
|
version_id,
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class CivitaiClient:
|
|||||||
self._MAX_CACHE_ENTRIES = 500
|
self._MAX_CACHE_ENTRIES = 500
|
||||||
|
|
||||||
def _build_image_info_url(self, image_id: str) -> str:
|
def _build_image_info_url(self, image_id: str) -> str:
|
||||||
return f"{self.base_url}/images?imageId={image_id}&nsfw=X"
|
return f"{self.base_url}/images?imageId={image_id}&nsfw=X&withMeta=true"
|
||||||
|
|
||||||
async def _make_request(
|
async def _make_request(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -1288,10 +1288,24 @@ class DownloadManager:
|
|||||||
"download_id": download_id,
|
"download_id": download_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if this checkpoint should be treated as a diffusion model based on baseModel
|
# Check if this checkpoint should be treated as a diffusion model
|
||||||
|
# Priority: (1) any file has type "UNet" or "Diffusion Model",
|
||||||
|
# (2) baseModel is in DIFFUSION_MODEL_BASE_MODELS
|
||||||
is_diffusion_model = False
|
is_diffusion_model = False
|
||||||
if model_type == "checkpoint":
|
if model_type == "checkpoint":
|
||||||
if base_model_value in DIFFUSION_MODEL_BASE_MODELS:
|
# Check file types first (more direct signal from CivitAI)
|
||||||
|
version_files = version_info.get("files", [])
|
||||||
|
for f in version_files:
|
||||||
|
f_type = f.get("type", "")
|
||||||
|
if f_type in ("UNet", "Diffusion Model"):
|
||||||
|
is_diffusion_model = True
|
||||||
|
logger.info(
|
||||||
|
f"File type '{f_type}' detected, routing checkpoint to unet folder"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Fallback to baseModel name check
|
||||||
|
if not is_diffusion_model and base_model_value in DIFFUSION_MODEL_BASE_MODELS:
|
||||||
is_diffusion_model = True
|
is_diffusion_model = True
|
||||||
logger.info(
|
logger.info(
|
||||||
f"baseModel '{base_model_value}' is a known diffusion model, routing to unet folder"
|
f"baseModel '{base_model_value}' is a known diffusion model, routing to unet folder"
|
||||||
@@ -1420,7 +1434,7 @@ class DownloadManager:
|
|||||||
f
|
f
|
||||||
for f in files
|
for f in files
|
||||||
if f.get("primary")
|
if f.get("primary")
|
||||||
and f.get("type") in ("Model", "Negative", "Diffusion Model")
|
and f.get("type") in ("Model", "Negative", "Diffusion Model", "UNet")
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -1451,7 +1465,7 @@ class DownloadManager:
|
|||||||
(
|
(
|
||||||
f
|
f
|
||||||
for f in files
|
for f in files
|
||||||
if f.get("primary") and f.get("type") in ("Model", "Negative", "Diffusion Model")
|
if f.get("primary") and f.get("type") in ("Model", "Negative", "Diffusion Model", "UNet")
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -146,11 +146,38 @@ class RecipeAnalysisService:
|
|||||||
):
|
):
|
||||||
metadata = metadata["meta"]
|
metadata = metadata["meta"]
|
||||||
|
|
||||||
# Include modelVersionIds from root level if available
|
# Include modelVersionIds from root level if available.
|
||||||
# Civitai API returns modelVersionIds at root level, not in meta
|
# CivitAI API returns modelVersionIds at root level, not in meta.
|
||||||
|
# When meta is null (None), create a minimal dict so downstream
|
||||||
|
# parsers can still discover LoRAs and checkpoints.
|
||||||
model_version_ids = image_info.get("modelVersionIds")
|
model_version_ids = image_info.get("modelVersionIds")
|
||||||
if model_version_ids and isinstance(metadata, dict):
|
if model_version_ids:
|
||||||
metadata["modelVersionIds"] = model_version_ids
|
if isinstance(metadata, dict):
|
||||||
|
metadata["modelVersionIds"] = model_version_ids
|
||||||
|
else:
|
||||||
|
metadata = {"modelVersionIds": model_version_ids}
|
||||||
|
|
||||||
|
# Inject browsingLevel (canonical integer) so the recipe's
|
||||||
|
# preview_nsfw_level can be set, enabling proper NSFW blur
|
||||||
|
# of the preview image. Fall back to nsfwLevel (string)
|
||||||
|
# when browsingLevel is absent.
|
||||||
|
if isinstance(metadata, dict):
|
||||||
|
browsing_level = image_info.get("browsingLevel")
|
||||||
|
nsfw_level_str = image_info.get("nsfwLevel")
|
||||||
|
if isinstance(browsing_level, int) and browsing_level > 0:
|
||||||
|
metadata["browsingLevel"] = browsing_level
|
||||||
|
elif (
|
||||||
|
isinstance(nsfw_level_str, str)
|
||||||
|
and nsfw_level_str
|
||||||
|
in (
|
||||||
|
"PG", "PG13", "R", "X", "XXX", "Blocked",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
from ...utils.constants import NSFW_LEVELS
|
||||||
|
|
||||||
|
metadata["browsingLevel"] = NSFW_LEVELS.get(
|
||||||
|
nsfw_level_str, 0
|
||||||
|
)
|
||||||
|
|
||||||
# Validate that metadata contains meaningful recipe fields
|
# Validate that metadata contains meaningful recipe fields
|
||||||
# If not, treat as None to trigger EXIF extraction from downloaded image
|
# If not, treat as None to trigger EXIF extraction from downloaded image
|
||||||
@@ -171,12 +198,19 @@ class RecipeAnalysisService:
|
|||||||
temp_path = self._create_temp_path(suffix=extension)
|
temp_path = self._create_temp_path(suffix=extension)
|
||||||
await self._download_image(url, temp_path)
|
await self._download_image(url, temp_path)
|
||||||
|
|
||||||
if metadata is None and not is_video:
|
# Always extract EXIF from the downloaded image for generation
|
||||||
metadata = await asyncio.to_thread(
|
# params (prompt, negative prompt, sampler, steps, etc.).
|
||||||
|
# Previously this was gated on ``metadata is None``, but that
|
||||||
|
# skipped EXIF entirely when API metadata (modelVersionIds,
|
||||||
|
# browsingLevel) is present, losing all generation parameters.
|
||||||
|
exif_metadata = None
|
||||||
|
if not is_video:
|
||||||
|
exif_metadata = await asyncio.to_thread(
|
||||||
self._exif_utils.extract_image_metadata, temp_path
|
self._exif_utils.extract_image_metadata, temp_path
|
||||||
)
|
)
|
||||||
|
|
||||||
if not metadata and civitai_image_id and image_info:
|
# Fallback: try the original (non-optimized) image for EXIF data
|
||||||
|
if not exif_metadata and civitai_image_id and image_info:
|
||||||
original_url = image_info.get("url")
|
original_url = image_info.get("url")
|
||||||
if original_url:
|
if original_url:
|
||||||
self._logger.debug(
|
self._logger.debug(
|
||||||
@@ -187,15 +221,38 @@ class RecipeAnalysisService:
|
|||||||
orig_temp_path = self._create_temp_path(suffix=".png")
|
orig_temp_path = self._create_temp_path(suffix=".png")
|
||||||
try:
|
try:
|
||||||
await self._download_image(original_url, orig_temp_path)
|
await self._download_image(original_url, orig_temp_path)
|
||||||
metadata = await asyncio.to_thread(
|
exif_metadata = await asyncio.to_thread(
|
||||||
self._exif_utils.extract_image_metadata,
|
self._exif_utils.extract_image_metadata,
|
||||||
orig_temp_path,
|
orig_temp_path,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self._safe_cleanup(orig_temp_path)
|
self._safe_cleanup(orig_temp_path)
|
||||||
|
|
||||||
|
# Parse EXIF data (typically a string like parameters/prompt/workflow)
|
||||||
|
# and API metadata (dict with modelVersionIds, browsingLevel) separately,
|
||||||
|
# then merge: API loras/checkpoint override, EXIF gen_params fill in gaps.
|
||||||
|
# This mirrors the two-pass approach in _do_import_from_url.
|
||||||
|
exif_parsed_result = None
|
||||||
|
if isinstance(exif_metadata, str):
|
||||||
|
exif_parser = self._recipe_parser_factory.create_parser(exif_metadata)
|
||||||
|
if exif_parser:
|
||||||
|
exif_data = await exif_parser.parse_metadata(
|
||||||
|
exif_metadata, recipe_scanner=recipe_scanner,
|
||||||
|
)
|
||||||
|
if exif_data and not exif_data.get("error"):
|
||||||
|
exif_parsed_result = exif_data
|
||||||
|
|
||||||
|
# Merge API metadata (dict) with EXIF data (if dict) for the
|
||||||
|
# CivitaiApiMetadataParser. If EXIF data is a string it was
|
||||||
|
# parsed above — don't try to merge a string into a dict.
|
||||||
|
merged = {}
|
||||||
|
if isinstance(exif_metadata, dict):
|
||||||
|
merged.update(exif_metadata)
|
||||||
|
if isinstance(metadata, dict):
|
||||||
|
merged.update(metadata)
|
||||||
|
|
||||||
result = await self._parse_metadata(
|
result = await self._parse_metadata(
|
||||||
metadata or {},
|
merged,
|
||||||
recipe_scanner=recipe_scanner,
|
recipe_scanner=recipe_scanner,
|
||||||
image_path=temp_path,
|
image_path=temp_path,
|
||||||
include_image_base64=True,
|
include_image_base64=True,
|
||||||
@@ -203,13 +260,23 @@ class RecipeAnalysisService:
|
|||||||
extension=extension,
|
extension=extension,
|
||||||
)
|
)
|
||||||
|
|
||||||
if civitai_image_id and image_info and not result.payload.get("error"):
|
# Merge EXIF string-parsed gen_params into the API result.
|
||||||
mvid = image_info.get("modelVersionId")
|
# API gen_params take priority (they come later via update).
|
||||||
if not mvid:
|
if exif_parsed_result and not result.payload.get("error"):
|
||||||
mvids = image_info.get("modelVersionIds")
|
exif_gp = exif_parsed_result.get("gen_params") or {}
|
||||||
if isinstance(mvids, list) and mvids:
|
result_gp = result.payload.get("gen_params") or {}
|
||||||
mvid = mvids[0]
|
merged_gp = {**exif_gp, **result_gp}
|
||||||
|
if merged_gp:
|
||||||
|
result.payload["gen_params"] = merged_gp
|
||||||
|
|
||||||
|
if civitai_image_id and image_info and not result.payload.get("error"):
|
||||||
|
# Use the metadata dict we built (may contain modelVersionIds
|
||||||
|
# and browsingLevel from the API root level). Do NOT pass
|
||||||
|
# image_info.get("meta") — it is null for images whose meta
|
||||||
|
# lives at the root level only. Also do NOT derive
|
||||||
|
# model_version_id from modelVersionIds[0] — that array mixes
|
||||||
|
# checkpoints, LoRAs, and other types without ordering
|
||||||
|
# guarantees; the parser already resolved them correctly.
|
||||||
recipe_for_enrich = {
|
recipe_for_enrich = {
|
||||||
"gen_params": result.payload.get("gen_params", {}),
|
"gen_params": result.payload.get("gen_params", {}),
|
||||||
"loras": result.payload.get("loras", []),
|
"loras": result.payload.get("loras", []),
|
||||||
@@ -222,8 +289,10 @@ class RecipeAnalysisService:
|
|||||||
recipe=recipe_for_enrich,
|
recipe=recipe_for_enrich,
|
||||||
civitai_client=civitai_client,
|
civitai_client=civitai_client,
|
||||||
request_params=None,
|
request_params=None,
|
||||||
prefetched_civitai_meta_raw=image_info.get("meta"),
|
prefetched_civitai_meta_raw=(
|
||||||
prefetched_model_version_id=mvid,
|
metadata if isinstance(metadata, dict) else None
|
||||||
|
),
|
||||||
|
prefetched_model_version_id=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
result.payload["gen_params"] = recipe_for_enrich["gen_params"]
|
result.payload["gen_params"] = recipe_for_enrich["gen_params"]
|
||||||
@@ -232,6 +301,12 @@ class RecipeAnalysisService:
|
|||||||
if recipe_for_enrich.get("base_model"):
|
if recipe_for_enrich.get("base_model"):
|
||||||
result.payload["base_model"] = recipe_for_enrich["base_model"]
|
result.payload["base_model"] = recipe_for_enrich["base_model"]
|
||||||
|
|
||||||
|
# Extract browsingLevel from our constructed metadata for NSFW blur
|
||||||
|
if isinstance(metadata, dict):
|
||||||
|
bl = metadata.get("browsingLevel")
|
||||||
|
if isinstance(bl, int) and bl > 0:
|
||||||
|
result.payload["preview_nsfw_level"] = bl
|
||||||
|
|
||||||
return result
|
return result
|
||||||
finally:
|
finally:
|
||||||
if temp_path:
|
if temp_path:
|
||||||
@@ -314,6 +389,10 @@ class RecipeAnalysisService:
|
|||||||
"prompt_type",
|
"prompt_type",
|
||||||
"positive",
|
"positive",
|
||||||
"negative",
|
"negative",
|
||||||
|
# modelVersionIds is injected at the root level by CivitAI's image
|
||||||
|
# API when meta is null. It carries the version IDs of ALL models
|
||||||
|
# (checkpoint + LoRAs) used to generate the image.
|
||||||
|
"modelVersionIds",
|
||||||
}
|
}
|
||||||
return any(field in metadata for field in recipe_fields)
|
return any(field in metadata for field in recipe_fields)
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ DIFFUSION_MODEL_BASE_MODELS = frozenset(
|
|||||||
"Qwen",
|
"Qwen",
|
||||||
"ZImageBase",
|
"ZImageBase",
|
||||||
"ZImageTurbo",
|
"ZImageTurbo",
|
||||||
|
# Krea 2 — loaded via UNETLoader in ComfyUI
|
||||||
|
"Krea 2",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-lora-manager"
|
name = "comfyui-lora-manager"
|
||||||
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
||||||
version = "1.1.5"
|
version = "1.1.6"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export class BulkManager {
|
|||||||
this.isMarqueeActive = false;
|
this.isMarqueeActive = false;
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
this.marqueeStart = { x: 0, y: 0 };
|
this.marqueeStart = { x: 0, y: 0 };
|
||||||
|
this.marqueeStartDoc = { x: 0, y: 0 }; // Marquee start in document coordinates
|
||||||
this.marqueeElement = null;
|
this.marqueeElement = null;
|
||||||
this.initialSelectedModels = new Set();
|
this.initialSelectedModels = new Set();
|
||||||
|
|
||||||
@@ -29,6 +30,11 @@ export class BulkManager {
|
|||||||
this.mouseDownTime = 0;
|
this.mouseDownTime = 0;
|
||||||
this.mouseDownPosition = { x: 0, y: 0 };
|
this.mouseDownPosition = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
// Auto-scroll properties for marquee
|
||||||
|
this.lastClientX = 0;
|
||||||
|
this.lastClientY = 0;
|
||||||
|
this.autoScrollRaf = null;
|
||||||
|
|
||||||
// Model type specific action configurations
|
// Model type specific action configurations
|
||||||
this.actionConfig = {
|
this.actionConfig = {
|
||||||
[MODEL_TYPES.LORA]: {
|
[MODEL_TYPES.LORA]: {
|
||||||
@@ -168,7 +174,10 @@ export class BulkManager {
|
|||||||
|
|
||||||
eventManager.addHandler('mousemove', 'bulkManager-marquee-move', (e) => {
|
eventManager.addHandler('mousemove', 'bulkManager-marquee-move', (e) => {
|
||||||
if (this.isMarqueeActive) {
|
if (this.isMarqueeActive) {
|
||||||
|
this.lastClientX = e.clientX;
|
||||||
|
this.lastClientY = e.clientY;
|
||||||
this.updateMarqueeSelection(e);
|
this.updateMarqueeSelection(e);
|
||||||
|
this.startAutoScroll();
|
||||||
} else if (this.mouseDownTime && !this.isDragging) {
|
} else if (this.mouseDownTime && !this.isDragging) {
|
||||||
// Check if we've moved enough to consider it a drag
|
// Check if we've moved enough to consider it a drag
|
||||||
const dx = e.clientX - this.mouseDownPosition.x;
|
const dx = e.clientX - this.mouseDownPosition.x;
|
||||||
@@ -237,6 +246,7 @@ export class BulkManager {
|
|||||||
* Clean up event handlers
|
* Clean up event handlers
|
||||||
*/
|
*/
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
this.stopAutoScroll();
|
||||||
eventManager.removeAllHandlersForSource('bulkManager-keyboard');
|
eventManager.removeAllHandlersForSource('bulkManager-keyboard');
|
||||||
eventManager.removeAllHandlersForSource('bulkManager-marquee-start');
|
eventManager.removeAllHandlersForSource('bulkManager-marquee-start');
|
||||||
eventManager.removeAllHandlersForSource('bulkManager-marquee-move');
|
eventManager.removeAllHandlersForSource('bulkManager-marquee-move');
|
||||||
@@ -1727,10 +1737,15 @@ export class BulkManager {
|
|||||||
* @param {boolean} isDragging - Whether this is triggered from a drag operation
|
* @param {boolean} isDragging - Whether this is triggered from a drag operation
|
||||||
*/
|
*/
|
||||||
startMarqueeSelection(e, isDragging = false) {
|
startMarqueeSelection(e, isDragging = false) {
|
||||||
// Store initial mouse position
|
// Store initial mouse position (viewport coordinates for visual element)
|
||||||
this.marqueeStart.x = this.mouseDownPosition.x;
|
this.marqueeStart.x = this.mouseDownPosition.x;
|
||||||
this.marqueeStart.y = this.mouseDownPosition.y;
|
this.marqueeStart.y = this.mouseDownPosition.y;
|
||||||
|
|
||||||
|
// Store initial mouse position in document coordinates (for logical selection)
|
||||||
|
const container = document.querySelector('.page-content');
|
||||||
|
this.marqueeStartDoc.x = this.mouseDownPosition.x + (container?.scrollLeft || 0);
|
||||||
|
this.marqueeStartDoc.y = this.mouseDownPosition.y + (container?.scrollTop || 0);
|
||||||
|
|
||||||
// Store initial selection state
|
// Store initial selection state
|
||||||
this.initialSelectedModels = new Set(state.selectedModels);
|
this.initialSelectedModels = new Set(state.selectedModels);
|
||||||
|
|
||||||
@@ -1776,46 +1791,67 @@ export class BulkManager {
|
|||||||
*/
|
*/
|
||||||
updateMarqueeSelection(e) {
|
updateMarqueeSelection(e) {
|
||||||
if (!this.marqueeElement) return;
|
if (!this.marqueeElement) return;
|
||||||
|
this.updateMarqueeSelectionFromPosition(e.clientX, e.clientY);
|
||||||
const currentX = e.clientX;
|
|
||||||
const currentY = e.clientY;
|
|
||||||
|
|
||||||
// Calculate rectangle bounds
|
|
||||||
const left = Math.min(this.marqueeStart.x, currentX);
|
|
||||||
const top = Math.min(this.marqueeStart.y, currentY);
|
|
||||||
const width = Math.abs(currentX - this.marqueeStart.x);
|
|
||||||
const height = Math.abs(currentY - this.marqueeStart.y);
|
|
||||||
|
|
||||||
// Update marquee element position and size
|
|
||||||
this.marqueeElement.style.left = left + 'px';
|
|
||||||
this.marqueeElement.style.top = top + 'px';
|
|
||||||
this.marqueeElement.style.width = width + 'px';
|
|
||||||
this.marqueeElement.style.height = height + 'px';
|
|
||||||
|
|
||||||
// Check which cards intersect with marquee
|
|
||||||
this.updateCardSelection(left, top, left + width, top + height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update card selection based on marquee bounds
|
* Update marquee from raw client coordinates (used by both mousemove and auto-scroll loop)
|
||||||
*/
|
*/
|
||||||
updateCardSelection(left, top, right, bottom) {
|
updateMarqueeSelectionFromPosition(clientX, clientY) {
|
||||||
const cards = document.querySelectorAll('.model-card');
|
if (!this.marqueeElement) return;
|
||||||
|
|
||||||
|
const container = document.querySelector('.page-content');
|
||||||
|
const scrollX = container?.scrollLeft || 0;
|
||||||
|
const scrollY = container?.scrollTop || 0;
|
||||||
|
|
||||||
|
// Current position in document coordinates
|
||||||
|
const currentDocX = clientX + scrollX;
|
||||||
|
const currentDocY = clientY + scrollY;
|
||||||
|
|
||||||
|
// Calculate marquee rectangle in document coordinates
|
||||||
|
const docLeft = Math.min(this.marqueeStartDoc.x, currentDocX);
|
||||||
|
const docTop = Math.min(this.marqueeStartDoc.y, currentDocY);
|
||||||
|
const docRight = Math.max(this.marqueeStartDoc.x, currentDocX);
|
||||||
|
const docBottom = Math.max(this.marqueeStartDoc.y, currentDocY);
|
||||||
|
|
||||||
|
// Update visual marquee element (position: fixed, so subtract scroll offset)
|
||||||
|
this.marqueeElement.style.left = (docLeft - scrollX) + 'px';
|
||||||
|
this.marqueeElement.style.top = (docTop - scrollY) + 'px';
|
||||||
|
this.marqueeElement.style.width = (docRight - docLeft) + 'px';
|
||||||
|
this.marqueeElement.style.height = (docBottom - docTop) + 'px';
|
||||||
|
|
||||||
|
// Check which cards intersect with marquee
|
||||||
|
this.updateCardSelection(docLeft, docTop, docRight, docBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update card selection based on marquee bounds (document coordinates).
|
||||||
|
* Uses dual detection: DOM cards for visible ones + VirtualScroller layout for off-screen cards.
|
||||||
|
*/
|
||||||
|
updateCardSelection(docLeft, docTop, docRight, docBottom) {
|
||||||
|
const vs = state.virtualScroller;
|
||||||
|
const container = document.querySelector('.page-content');
|
||||||
|
const scrollX = container?.scrollLeft || 0;
|
||||||
|
const scrollY = container?.scrollTop || 0;
|
||||||
const newSelection = new Set(this.initialSelectedModels);
|
const newSelection = new Set(this.initialSelectedModels);
|
||||||
|
const visibleFilepaths = new Set();
|
||||||
|
|
||||||
cards.forEach(card => {
|
// Step 1: Process visible DOM cards using getBoundingClientRect + scroll offset
|
||||||
const rect = card.getBoundingClientRect();
|
document.querySelectorAll('.model-card').forEach(card => {
|
||||||
|
|
||||||
// Check if card intersects with marquee rectangle
|
|
||||||
const intersects = !(rect.right < left ||
|
|
||||||
rect.left > right ||
|
|
||||||
rect.bottom < top ||
|
|
||||||
rect.top > bottom);
|
|
||||||
|
|
||||||
const filepath = card.dataset.filepath;
|
const filepath = card.dataset.filepath;
|
||||||
|
if (!filepath) return;
|
||||||
|
visibleFilepaths.add(filepath);
|
||||||
|
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const cardLeft = rect.left + scrollX;
|
||||||
|
const cardTop = rect.top + scrollY;
|
||||||
|
const cardRight = rect.right + scrollX;
|
||||||
|
const cardBottom = rect.bottom + scrollY;
|
||||||
|
|
||||||
|
const intersects = !(cardRight < docLeft || cardLeft > docRight ||
|
||||||
|
cardBottom < docTop || cardTop > docBottom);
|
||||||
|
|
||||||
if (intersects) {
|
if (intersects) {
|
||||||
// Add to selection if intersecting
|
|
||||||
newSelection.add(filepath);
|
newSelection.add(filepath);
|
||||||
card.classList.add('selected');
|
card.classList.add('selected');
|
||||||
|
|
||||||
@@ -1825,12 +1861,43 @@ export class BulkManager {
|
|||||||
this.updateMetadataCacheFromCard(filepath, card);
|
this.updateMetadataCacheFromCard(filepath, card);
|
||||||
}
|
}
|
||||||
} else if (!this.initialSelectedModels.has(filepath)) {
|
} else if (!this.initialSelectedModels.has(filepath)) {
|
||||||
// Remove from selection if not intersecting and wasn't initially selected
|
|
||||||
newSelection.delete(filepath);
|
newSelection.delete(filepath);
|
||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Step 2: Process off-screen cards via VirtualScroller layout calculation.
|
||||||
|
// Since VirtualScroller removes off-screen DOM elements, we compute
|
||||||
|
// each card's position from its index and the VS layout parameters.
|
||||||
|
if (vs?.gridElement && vs.items && vs.columnsCount > 0) {
|
||||||
|
const gridRect = vs.gridElement.getBoundingClientRect();
|
||||||
|
// Grid origin in scroll-container content coordinates
|
||||||
|
const originX = gridRect.left + scrollX;
|
||||||
|
const originY = gridRect.top + scrollY;
|
||||||
|
|
||||||
|
for (let i = 0; i < vs.items.length; i++) {
|
||||||
|
const filepath = vs.items[i]?.file_path;
|
||||||
|
if (!filepath || visibleFilepaths.has(filepath)) continue;
|
||||||
|
|
||||||
|
const row = Math.floor(i / vs.columnsCount);
|
||||||
|
const col = i % vs.columnsCount;
|
||||||
|
|
||||||
|
const cLeft = originX + col * (vs.itemWidth + vs.columnGap);
|
||||||
|
const cTop = originY + (vs.containerPaddingTop || 0) + row * (vs.itemHeight + (vs.rowGap || 0));
|
||||||
|
const cRight = cLeft + vs.itemWidth;
|
||||||
|
const cBottom = cTop + vs.itemHeight;
|
||||||
|
|
||||||
|
const intersects = !(cRight < docLeft || cLeft > docRight ||
|
||||||
|
cBottom < docTop || cTop > docBottom);
|
||||||
|
|
||||||
|
if (intersects) {
|
||||||
|
newSelection.add(filepath);
|
||||||
|
} else if (!this.initialSelectedModels.has(filepath)) {
|
||||||
|
newSelection.delete(filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update global selection state
|
// Update global selection state
|
||||||
state.selectedModels = newSelection;
|
state.selectedModels = newSelection;
|
||||||
|
|
||||||
@@ -1849,6 +1916,9 @@ export class BulkManager {
|
|||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
this.mouseDownTime = 0;
|
this.mouseDownTime = 0;
|
||||||
|
|
||||||
|
// Stop any active auto-scroll
|
||||||
|
this.stopAutoScroll();
|
||||||
|
|
||||||
// Update event manager state
|
// Update event manager state
|
||||||
eventManager.setState('marqueeActive', false);
|
eventManager.setState('marqueeActive', false);
|
||||||
|
|
||||||
@@ -1874,6 +1944,79 @@ export class BulkManager {
|
|||||||
// Clear initial selection state
|
// Clear initial selection state
|
||||||
this.initialSelectedModels.clear();
|
this.initialSelectedModels.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start auto-scroll loop when mouse approaches viewport edge during marquee
|
||||||
|
*/
|
||||||
|
startAutoScroll() {
|
||||||
|
if (this.autoScrollRaf) return;
|
||||||
|
this.autoScrollLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop auto-scroll loop
|
||||||
|
*/
|
||||||
|
stopAutoScroll() {
|
||||||
|
if (this.autoScrollRaf) {
|
||||||
|
cancelAnimationFrame(this.autoScrollRaf);
|
||||||
|
this.autoScrollRaf = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-scroll loop: scrolls the page when mouse is near viewport edges
|
||||||
|
* and re-evaluates marquee selection after each scroll.
|
||||||
|
*/
|
||||||
|
autoScrollLoop() {
|
||||||
|
if (!this.isMarqueeActive) {
|
||||||
|
this.autoScrollRaf = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.querySelector('.page-content');
|
||||||
|
if (!container) {
|
||||||
|
this.autoScrollRaf = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MARGIN = 30; // Px from edge to trigger scroll
|
||||||
|
const BASE_SPEED = 12; // Pixels per frame at edge boundary
|
||||||
|
const MAX_SPEED = 40; // Maximum scroll speed
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
let dx = 0;
|
||||||
|
let dy = 0;
|
||||||
|
|
||||||
|
// Vertical auto-scroll - speed increases the further the cursor is past the edge
|
||||||
|
if (this.lastClientY !== undefined) {
|
||||||
|
if (this.lastClientY < rect.top + MARGIN) {
|
||||||
|
const dist = Math.max(0, (rect.top + MARGIN) - this.lastClientY);
|
||||||
|
dy = -Math.min(BASE_SPEED + dist * 0.5, MAX_SPEED);
|
||||||
|
} else if (this.lastClientY > rect.bottom - MARGIN) {
|
||||||
|
const dist = Math.max(0, this.lastClientY - (rect.bottom - MARGIN));
|
||||||
|
dy = Math.min(BASE_SPEED + dist * 0.5, MAX_SPEED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal auto-scroll
|
||||||
|
if (this.lastClientX !== undefined) {
|
||||||
|
if (this.lastClientX < rect.left + MARGIN) {
|
||||||
|
const dist = Math.max(0, (rect.left + MARGIN) - this.lastClientX);
|
||||||
|
dx = -Math.min(BASE_SPEED + dist * 0.5, MAX_SPEED);
|
||||||
|
} else if (this.lastClientX > rect.right - MARGIN) {
|
||||||
|
const dist = Math.max(0, this.lastClientX - (rect.right - MARGIN));
|
||||||
|
dx = Math.min(BASE_SPEED + dist * 0.5, MAX_SPEED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dx !== 0 || dy !== 0) {
|
||||||
|
container.scrollBy(dx, dy);
|
||||||
|
// Re-evaluate marquee selection with the new scroll position
|
||||||
|
this.updateMarqueeSelectionFromPosition(this.lastClientX, this.lastClientY);
|
||||||
|
this.autoScrollRaf = requestAnimationFrame(() => this.autoScrollLoop());
|
||||||
|
} else {
|
||||||
|
this.autoScrollRaf = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bulkManager = new BulkManager();
|
export const bulkManager = new BulkManager();
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ export class DownloadManager {
|
|||||||
const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png';
|
const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png';
|
||||||
|
|
||||||
// Count model-type files per version
|
// Count model-type files per version
|
||||||
const modelFiles = (version.files || []).filter(f => f.type === 'Model');
|
const modelFiles = (version.files || []).filter(f => f.type === 'Model' || f.type === 'UNet' || f.type === 'Diffusion Model');
|
||||||
const primaryFile = modelFiles.find(f => f.primary) || modelFiles[0] || {};
|
const primaryFile = modelFiles.find(f => f.primary) || modelFiles[0] || {};
|
||||||
const fileSize = version.modelSizeKB ?
|
const fileSize = version.modelSizeKB ?
|
||||||
(version.modelSizeKB / 1024).toFixed(2) :
|
(version.modelSizeKB / 1024).toFixed(2) :
|
||||||
@@ -478,7 +478,7 @@ export class DownloadManager {
|
|||||||
if (!version) return;
|
if (!version) return;
|
||||||
|
|
||||||
this.currentVersion = version;
|
this.currentVersion = version;
|
||||||
const modelFiles = (version.files || []).filter(f => f.type === 'Model');
|
const modelFiles = (version.files || []).filter(f => f.type === 'Model' || f.type === 'UNet' || f.type === 'Diffusion Model');
|
||||||
|
|
||||||
document.getElementById('versionStep').style.display = 'none';
|
document.getElementById('versionStep').style.display = 'none';
|
||||||
document.getElementById('fileSelectionStep').style.display = 'block';
|
document.getElementById('fileSelectionStep').style.display = 'block';
|
||||||
@@ -534,7 +534,7 @@ export class DownloadManager {
|
|||||||
const version = this.currentVersion;
|
const version = this.currentVersion;
|
||||||
if (!version) return;
|
if (!version) return;
|
||||||
|
|
||||||
const modelFiles = (version.files || []).filter(f => f.type === 'Model');
|
const modelFiles = (version.files || []).filter(f => f.type === 'Model' || f.type === 'UNet' || f.type === 'Diffusion Model');
|
||||||
this.selectedFile = modelFiles.find(f => f.id.toString() === selectedRadio.value);
|
this.selectedFile = modelFiles.find(f => f.id.toString() === selectedRadio.value);
|
||||||
|
|
||||||
document.getElementById('fileSelectionStep').style.display = 'none';
|
document.getElementById('fileSelectionStep').style.display = 'none';
|
||||||
@@ -954,7 +954,7 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
if (!this.isBatchMode) {
|
if (!this.isBatchMode) {
|
||||||
const fileParams = this.selectedFile ? {
|
const fileParams = this.selectedFile ? {
|
||||||
type: 'Model',
|
type: this.selectedFile.type || 'Model',
|
||||||
format: this.selectedFile.metadata?.format || 'SafeTensor',
|
format: this.selectedFile.metadata?.format || 'SafeTensor',
|
||||||
size: this.selectedFile.metadata?.size || 'full',
|
size: this.selectedFile.metadata?.size || 'full',
|
||||||
fp: this.selectedFile.metadata?.fp,
|
fp: this.selectedFile.metadata?.fp,
|
||||||
|
|||||||
@@ -57,9 +57,16 @@ export class DownloadManager {
|
|||||||
base_model: this.importManager.recipeData.base_model || "",
|
base_model: this.importManager.recipeData.base_model || "",
|
||||||
loras: this.importManager.recipeData.loras || [],
|
loras: this.importManager.recipeData.loras || [],
|
||||||
gen_params: this.importManager.recipeData.gen_params || {},
|
gen_params: this.importManager.recipeData.gen_params || {},
|
||||||
raw_metadata: this.importManager.recipeData.raw_metadata || {}
|
raw_metadata: this.importManager.recipeData.raw_metadata || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Preserve preview_nsfw_level from analysis so the saved
|
||||||
|
// recipe applies the correct NSFW blur on the preview image.
|
||||||
|
const nsfwLevel = this.importManager.recipeData.preview_nsfw_level;
|
||||||
|
if (nsfwLevel !== undefined && nsfwLevel !== null) {
|
||||||
|
completeMetadata.preview_nsfw_level = nsfwLevel;
|
||||||
|
}
|
||||||
|
|
||||||
const checkpointMetadata =
|
const checkpointMetadata =
|
||||||
this.importManager.recipeData.checkpoint ||
|
this.importManager.recipeData.checkpoint ||
|
||||||
this.importManager.recipeData.model ||
|
this.importManager.recipeData.model ||
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ class FakePromptServer:
|
|||||||
sent = []
|
sent = []
|
||||||
|
|
||||||
class Instance:
|
class Instance:
|
||||||
def send_sync(self, event, payload):
|
sockets: dict = {}
|
||||||
|
|
||||||
|
def send_sync(self, event, payload, sid=None):
|
||||||
FakePromptServer.sent.append((event, payload))
|
FakePromptServer.sent.append((event, payload))
|
||||||
|
|
||||||
instance = Instance()
|
instance = Instance()
|
||||||
@@ -148,7 +150,8 @@ class TestNodeRegistryHandlerSnapshots:
|
|||||||
"type": "Lora Loader (LoraManager)",
|
"type": "Lora Loader (LoraManager)",
|
||||||
"title": "Test Loader",
|
"title": "Test Loader",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"client_id": "test-client-1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -167,7 +170,7 @@ class TestNodeRegistryHandlerSnapshots:
|
|||||||
standalone_mode=False,
|
standalone_mode=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
request = FakeRequest(json_data={"nodes": []})
|
request = FakeRequest(json_data={"nodes": [], "client_id": "test-client-1"})
|
||||||
response = await handler.register_nodes(request)
|
response = await handler.register_nodes(request)
|
||||||
payload = json.loads(response.text)
|
payload = json.loads(response.text)
|
||||||
|
|
||||||
|
|||||||
@@ -586,7 +586,9 @@ class FakePromptServer:
|
|||||||
sent = []
|
sent = []
|
||||||
|
|
||||||
class Instance:
|
class Instance:
|
||||||
def send_sync(self, event, payload):
|
sockets: dict = {}
|
||||||
|
|
||||||
|
def send_sync(self, event, payload, sid=None):
|
||||||
FakePromptServer.sent.append((event, payload))
|
FakePromptServer.sent.append((event, payload))
|
||||||
|
|
||||||
instance = Instance()
|
instance = Instance()
|
||||||
@@ -601,7 +603,12 @@ async def test_register_nodes_requires_graph_id():
|
|||||||
standalone_mode=False,
|
standalone_mode=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
request = FakeRequest(json_data={"nodes": [{"node_id": 1}]})
|
request = FakeRequest(
|
||||||
|
json_data={
|
||||||
|
"nodes": [{"node_id": 1}],
|
||||||
|
"client_id": "test-client-1",
|
||||||
|
}
|
||||||
|
)
|
||||||
response = await handler.register_nodes(request)
|
response = await handler.register_nodes(request)
|
||||||
payload = json.loads(response.text)
|
payload = json.loads(response.text)
|
||||||
|
|
||||||
@@ -629,7 +636,8 @@ async def test_register_nodes_stores_graph_identifier():
|
|||||||
"type": "Lora Loader (LoraManager)",
|
"type": "Lora Loader (LoraManager)",
|
||||||
"title": "Loader",
|
"title": "Loader",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"client_id": "test-client-1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -638,7 +646,7 @@ async def test_register_nodes_stores_graph_identifier():
|
|||||||
|
|
||||||
assert payload["success"] is True
|
assert payload["success"] is True
|
||||||
|
|
||||||
registry = await node_registry.get_registry()
|
registry = await node_registry.get_merged_registry()
|
||||||
assert registry["node_count"] == 1
|
assert registry["node_count"] == 1
|
||||||
stored_node = next(iter(registry["nodes"].values()))
|
stored_node = next(iter(registry["nodes"].values()))
|
||||||
assert stored_node["graph_id"] == "graph-123"
|
assert stored_node["graph_id"] == "graph-123"
|
||||||
@@ -664,7 +672,8 @@ async def test_register_nodes_defaults_graph_name_to_none():
|
|||||||
"type": "Lora Loader (LoraManager)",
|
"type": "Lora Loader (LoraManager)",
|
||||||
"title": "Root Loader",
|
"title": "Root Loader",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"client_id": "test-client-1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -673,7 +682,7 @@ async def test_register_nodes_defaults_graph_name_to_none():
|
|||||||
|
|
||||||
assert payload["success"] is True
|
assert payload["success"] is True
|
||||||
|
|
||||||
registry = await node_registry.get_registry()
|
registry = await node_registry.get_merged_registry()
|
||||||
stored_node = next(iter(registry["nodes"].values()))
|
stored_node = next(iter(registry["nodes"].values()))
|
||||||
assert stored_node["graph_name"] is None
|
assert stored_node["graph_name"] is None
|
||||||
|
|
||||||
@@ -700,7 +709,8 @@ async def test_register_nodes_includes_capabilities():
|
|||||||
"widget_names": ["ckpt_name", "", 42],
|
"widget_names": ["ckpt_name", "", 42],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"client_id": "test-client-1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -709,7 +719,7 @@ async def test_register_nodes_includes_capabilities():
|
|||||||
|
|
||||||
assert payload["success"] is True
|
assert payload["success"] is True
|
||||||
|
|
||||||
registry = await node_registry.get_registry()
|
registry = await node_registry.get_merged_registry()
|
||||||
stored_node = next(iter(registry["nodes"].values()))
|
stored_node = next(iter(registry["nodes"].values()))
|
||||||
assert stored_node["capabilities"] == {
|
assert stored_node["capabilities"] == {
|
||||||
"supports_lora": False,
|
"supports_lora": False,
|
||||||
@@ -724,7 +734,9 @@ async def test_update_node_widget_sends_payload():
|
|||||||
|
|
||||||
class RecordingPromptServer:
|
class RecordingPromptServer:
|
||||||
class Instance:
|
class Instance:
|
||||||
def send_sync(self, event, payload):
|
sockets: dict = {}
|
||||||
|
|
||||||
|
def send_sync(self, event, payload, sid=None):
|
||||||
send_calls.append((event, payload))
|
send_calls.append((event, payload))
|
||||||
|
|
||||||
instance = Instance()
|
instance = Instance()
|
||||||
@@ -768,7 +780,9 @@ async def test_update_lora_code_includes_graph_identifier():
|
|||||||
|
|
||||||
class RecordingPromptServer:
|
class RecordingPromptServer:
|
||||||
class Instance:
|
class Instance:
|
||||||
def send_sync(self, event, payload):
|
sockets: dict = {}
|
||||||
|
|
||||||
|
def send_sync(self, event, payload, sid=None):
|
||||||
send_calls.append((event, payload))
|
send_calls.append((event, payload))
|
||||||
|
|
||||||
instance = Instance()
|
instance = Instance()
|
||||||
|
|||||||
@@ -568,7 +568,7 @@ async def test_get_image_info_prefers_red_host_for_red_source(monkeypatch, downl
|
|||||||
|
|
||||||
assert result == {"id": 124950237, "name": "target"}
|
assert result == {"id": 124950237, "name": "target"}
|
||||||
assert requested_urls == [
|
assert requested_urls == [
|
||||||
"https://civitai.red/api/v1/images?imageId=124950237&nsfw=X"
|
"https://civitai.red/api/v1/images?imageId=124950237&nsfw=X&withMeta=true"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -589,7 +589,7 @@ async def test_get_image_info_uses_red_host_even_for_red_source(monkeypatch, dow
|
|||||||
|
|
||||||
assert result == {"id": 124950237, "name": "target"}
|
assert result == {"id": 124950237, "name": "target"}
|
||||||
assert requested_urls == [
|
assert requested_urls == [
|
||||||
"https://civitai.red/api/v1/images?imageId=124950237&nsfw=X",
|
"https://civitai.red/api/v1/images?imageId=124950237&nsfw=X&withMeta=true",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -610,7 +610,7 @@ async def test_get_image_info_does_not_fall_back_after_request_failure(monkeypat
|
|||||||
|
|
||||||
assert result is None
|
assert result is None
|
||||||
assert requested_urls == [
|
assert requested_urls == [
|
||||||
"https://civitai.red/api/v1/images?imageId=124950237&nsfw=X",
|
"https://civitai.red/api/v1/images?imageId=124950237&nsfw=X&withMeta=true",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,10 @@ app.registerExtension({
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ nodes: workflowNodes }),
|
body: JSON.stringify({
|
||||||
|
nodes: workflowNodes,
|
||||||
|
client_id: api.clientId ?? api.initialClientId ?? "",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user