mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 16:36:45 -03:00
Compare commits
13 Commits
14cb7fec47
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6dd6938b0 | ||
|
|
727d0ef043 | ||
|
|
9344d86332 | ||
|
|
d36b16c213 | ||
|
|
33a7f07558 | ||
|
|
4f599aeced | ||
|
|
30db8c3d1d | ||
|
|
05636712f0 | ||
|
|
d8e5fe1247 | ||
|
|
3e9210394a | ||
|
|
4dd2c0526f | ||
|
|
9bdb337962 | ||
|
|
f93baf5fc0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,6 +15,7 @@ model_cache/
|
|||||||
# agent
|
# agent
|
||||||
.opencode/
|
.opencode/
|
||||||
.claude/
|
.claude/
|
||||||
|
.codex
|
||||||
|
|
||||||
# Vue widgets development cache (but keep build output)
|
# Vue widgets development cache (but keep build output)
|
||||||
vue-widgets/node_modules/
|
vue-widgets/node_modules/
|
||||||
|
|||||||
@@ -9,17 +9,17 @@
|
|||||||
"Insomnia Art Designs",
|
"Insomnia Art Designs",
|
||||||
"megakirbs",
|
"megakirbs",
|
||||||
"Brennok",
|
"Brennok",
|
||||||
"wackop",
|
|
||||||
"2018cfh",
|
"2018cfh",
|
||||||
|
"W+K+White",
|
||||||
|
"wackop",
|
||||||
"Takkan",
|
"Takkan",
|
||||||
"stone9k",
|
"Carl G.",
|
||||||
"$MetaSamsara",
|
"$MetaSamsara",
|
||||||
"itismyelement",
|
"itismyelement",
|
||||||
"onesecondinosaur",
|
"onesecondinosaur",
|
||||||
"Carl G.",
|
"stone9k",
|
||||||
"Rosenthal",
|
"Rosenthal",
|
||||||
"Francisco Tatis",
|
"Francisco Tatis",
|
||||||
"Tobi_Swagg",
|
|
||||||
"Andrew Wilson",
|
"Andrew Wilson",
|
||||||
"Greybush",
|
"Greybush",
|
||||||
"Gooohokrbe",
|
"Gooohokrbe",
|
||||||
@@ -29,18 +29,16 @@
|
|||||||
"VantAI",
|
"VantAI",
|
||||||
"runte3221",
|
"runte3221",
|
||||||
"FreelancerZ",
|
"FreelancerZ",
|
||||||
"Julian V",
|
|
||||||
"Edgar Tejeda",
|
"Edgar Tejeda",
|
||||||
"Birdy",
|
|
||||||
"Liam MacDougal",
|
"Liam MacDougal",
|
||||||
"Fraser Cross",
|
"Fraser Cross",
|
||||||
"Polymorphic Indeterminate",
|
"Polymorphic Indeterminate",
|
||||||
|
"Birdy",
|
||||||
"Marc Whiffen",
|
"Marc Whiffen",
|
||||||
"Kiba",
|
|
||||||
"Jorge Hussni",
|
"Jorge Hussni",
|
||||||
"Reno Lam",
|
"Kiba",
|
||||||
"Skalabananen",
|
"Skalabananen",
|
||||||
"esthe",
|
"Reno Lam",
|
||||||
"sig",
|
"sig",
|
||||||
"Christian Byrne",
|
"Christian Byrne",
|
||||||
"DM",
|
"DM",
|
||||||
@@ -49,24 +47,22 @@
|
|||||||
"J\\B/ 8r0wns0n",
|
"J\\B/ 8r0wns0n",
|
||||||
"Snaggwort",
|
"Snaggwort",
|
||||||
"Arlecchino Shion",
|
"Arlecchino Shion",
|
||||||
|
"Charles Blakemore",
|
||||||
|
"Rob Williams",
|
||||||
"ClockDaemon",
|
"ClockDaemon",
|
||||||
"KD",
|
"KD",
|
||||||
"Omnidex",
|
"Omnidex",
|
||||||
"Tyler Trebuchon",
|
"Tyler Trebuchon",
|
||||||
"Release Cabrakan",
|
"Release Cabrakan",
|
||||||
"confiscated Zyra",
|
"Tobi_Swagg",
|
||||||
"SG",
|
"SG",
|
||||||
"carozzz",
|
"carozzz",
|
||||||
"James Dooley",
|
"James Dooley",
|
||||||
"zenbound",
|
"zenbound",
|
||||||
"Buzzard",
|
"Buzzard",
|
||||||
"jmack",
|
"jmack",
|
||||||
"Adam Shaw",
|
|
||||||
"Tee Gee",
|
|
||||||
"Mark Corneglio",
|
"Mark Corneglio",
|
||||||
"SarcasticHashtag",
|
"SarcasticHashtag",
|
||||||
"Anthony Rizzo",
|
|
||||||
"tarek helmi",
|
|
||||||
"Cosmosis",
|
"Cosmosis",
|
||||||
"iamresist",
|
"iamresist",
|
||||||
"RedrockVP",
|
"RedrockVP",
|
||||||
@@ -75,45 +71,34 @@
|
|||||||
"James Todd",
|
"James Todd",
|
||||||
"Steven Pfeiffer",
|
"Steven Pfeiffer",
|
||||||
"Tim",
|
"Tim",
|
||||||
"Timmy",
|
|
||||||
"Johnny",
|
|
||||||
"Lisster",
|
"Lisster",
|
||||||
"Michael Wong",
|
"Michael Wong",
|
||||||
"Illrigger",
|
"Illrigger",
|
||||||
"whudunit",
|
|
||||||
"Tom Corrigan",
|
"Tom Corrigan",
|
||||||
"JackieWang",
|
"JackieWang",
|
||||||
"fnkylove",
|
"fnkylove",
|
||||||
|
"Julian V",
|
||||||
"Steven Owens",
|
"Steven Owens",
|
||||||
"Yushio",
|
"Yushio",
|
||||||
"Vik71it",
|
"Vik71it",
|
||||||
"lh qwe",
|
|
||||||
"Echo",
|
"Echo",
|
||||||
"Lilleman",
|
"Lilleman",
|
||||||
"Robert Stacey",
|
"Robert Stacey",
|
||||||
"PM",
|
"PM",
|
||||||
"Todd Keck",
|
"Todd Keck",
|
||||||
"Briton Heilbrun",
|
|
||||||
"Mozzel",
|
"Mozzel",
|
||||||
"Gingko Biloba",
|
"Gingko Biloba",
|
||||||
"Felipe dos Santos",
|
|
||||||
"Penfore",
|
|
||||||
"BadassArabianMofo",
|
|
||||||
"Sterilized",
|
"Sterilized",
|
||||||
|
"BadassArabianMofo",
|
||||||
"Pascal Dahle",
|
"Pascal Dahle",
|
||||||
"Markus",
|
|
||||||
"quarz",
|
"quarz",
|
||||||
"Greg",
|
"Greg",
|
||||||
"Douglas Gaspar",
|
"Penfore",
|
||||||
"JSST",
|
"JSST",
|
||||||
"AlexDuKaNa",
|
"esthe",
|
||||||
"George",
|
|
||||||
"lmsupporter",
|
"lmsupporter",
|
||||||
"Phil",
|
|
||||||
"Charles Blakemore",
|
|
||||||
"IamAyam",
|
"IamAyam",
|
||||||
"wfpearl",
|
"wfpearl",
|
||||||
"Rob Williams",
|
|
||||||
"Baekdoosixt",
|
"Baekdoosixt",
|
||||||
"Jonathan Ross",
|
"Jonathan Ross",
|
||||||
"Jack B Nimble",
|
"Jack B Nimble",
|
||||||
@@ -125,127 +110,118 @@
|
|||||||
"contrite831",
|
"contrite831",
|
||||||
"Alex",
|
"Alex",
|
||||||
"bh",
|
"bh",
|
||||||
|
"confiscated Zyra",
|
||||||
"Marlon Daniels",
|
"Marlon Daniels",
|
||||||
"Starkselle",
|
"Starkselle",
|
||||||
"Aaron Bleuer",
|
"Aaron Bleuer",
|
||||||
"LacesOut!",
|
"LacesOut!",
|
||||||
"Graham Colehour",
|
"greebles",
|
||||||
|
"Adam Shaw",
|
||||||
|
"Tee Gee",
|
||||||
|
"Anthony Rizzo",
|
||||||
|
"tarek helmi",
|
||||||
"M Postkasse",
|
"M Postkasse",
|
||||||
"Tomohiro Baba",
|
|
||||||
"David Ortega",
|
|
||||||
"ASLPro3D",
|
"ASLPro3D",
|
||||||
"Jacob Hoehler",
|
"Jacob Hoehler",
|
||||||
"FinalyFree",
|
"FinalyFree",
|
||||||
"Weasyl",
|
"Weasyl",
|
||||||
"Lex Song",
|
"Timmy",
|
||||||
|
"Johnny",
|
||||||
"Cory Paza",
|
"Cory Paza",
|
||||||
"Tak",
|
"Tak",
|
||||||
"Gonzalo Andre Allendes Lopez",
|
"Gonzalo Andre Allendes Lopez",
|
||||||
"Zach Gonser",
|
"Zach Gonser",
|
||||||
"Big Red",
|
"Big Red",
|
||||||
"Jimmy Ledbetter",
|
"whudunit",
|
||||||
"Luc Job",
|
"Luc Job",
|
||||||
"dl0901dm",
|
"dl0901dm",
|
||||||
"Philip Hempel",
|
"Philip Hempel",
|
||||||
"corde",
|
"corde",
|
||||||
"Nick Walker",
|
"Nick Walker",
|
||||||
|
"lh qwe",
|
||||||
"Bishoujoker",
|
"Bishoujoker",
|
||||||
"conner",
|
"conner",
|
||||||
"aai",
|
"aai",
|
||||||
"Yaboi",
|
"Briton Heilbrun",
|
||||||
"Tori",
|
"Tori",
|
||||||
"wildnut",
|
"wildnut",
|
||||||
"Princess Bright Eyes",
|
"Princess Bright Eyes",
|
||||||
"Damon Cunliffe",
|
|
||||||
"CryptoTraderJK",
|
|
||||||
"Davaitamin",
|
|
||||||
"AbstractAss",
|
"AbstractAss",
|
||||||
|
"Felipe dos Santos",
|
||||||
"ViperC",
|
"ViperC",
|
||||||
|
"jean jahren",
|
||||||
"Aleksander Wujczyk",
|
"Aleksander Wujczyk",
|
||||||
"AM Kuro",
|
"AM Kuro",
|
||||||
"jean jahren",
|
"Markus",
|
||||||
"Ran C",
|
|
||||||
"tedcor",
|
|
||||||
"S Sang",
|
"S Sang",
|
||||||
"MagnaInsomnia",
|
|
||||||
"Akira_HentAI",
|
|
||||||
"Karl P.",
|
"Karl P.",
|
||||||
|
"Akira_HentAI",
|
||||||
|
"MagnaInsomnia",
|
||||||
"Gordon Cole",
|
"Gordon Cole",
|
||||||
"yuxz69",
|
"yuxz69",
|
||||||
"MadSpin",
|
"Douglas Gaspar",
|
||||||
|
"AlexDuKaNa",
|
||||||
|
"George",
|
||||||
"andrew.tappan",
|
"andrew.tappan",
|
||||||
"dw",
|
"dw",
|
||||||
"N/A",
|
"N/A",
|
||||||
"The Spawn",
|
"The Spawn",
|
||||||
|
"Phil",
|
||||||
"graysock",
|
"graysock",
|
||||||
"Greenmoustache",
|
"Greenmoustache",
|
||||||
"zounic",
|
"zounic",
|
||||||
"Gamalonia",
|
|
||||||
"fancypants",
|
"fancypants",
|
||||||
"Vir",
|
|
||||||
"Joboshy",
|
|
||||||
"Digital",
|
"Digital",
|
||||||
"JaxMax",
|
"JaxMax",
|
||||||
"takyamtom",
|
"takyamtom",
|
||||||
"Bohemian Corporal",
|
|
||||||
"奚明 刘",
|
"奚明 刘",
|
||||||
"Dan",
|
|
||||||
"Seth Christensen",
|
|
||||||
"Jwk0205",
|
"Jwk0205",
|
||||||
"Bro Xie",
|
"Bro Xie",
|
||||||
"Draven T",
|
"준희 김",
|
||||||
"yer fey",
|
|
||||||
"batblue",
|
"batblue",
|
||||||
"carey6409",
|
"carey6409",
|
||||||
"Olive",
|
"Olive",
|
||||||
"太郎 ゲーム",
|
"太郎 ゲーム",
|
||||||
"Some Guy Named Barry",
|
"Some Guy Named Barry",
|
||||||
"jinxedx",
|
|
||||||
"Aquatic Coffee",
|
|
||||||
"Max Marklund",
|
"Max Marklund",
|
||||||
|
"Tomohiro Baba",
|
||||||
|
"David Ortega",
|
||||||
"AELOX",
|
"AELOX",
|
||||||
"Dankin",
|
|
||||||
"Nicfit23",
|
"Nicfit23",
|
||||||
"Noora",
|
"Noora",
|
||||||
"ethanfel",
|
|
||||||
"wamekukyouzin",
|
"wamekukyouzin",
|
||||||
"drum matthieu",
|
"drum matthieu",
|
||||||
"Dogmaster",
|
"Dogmaster",
|
||||||
"Matt Wenzel",
|
"Matt Wenzel",
|
||||||
"Mattssn",
|
"Mattssn",
|
||||||
"Frank Nitty",
|
"Lex Song",
|
||||||
"John Saveas",
|
"John Saveas",
|
||||||
"Focuschannel",
|
|
||||||
"Christopher Michel",
|
"Christopher Michel",
|
||||||
"Serge Bekenkamp",
|
"Serge Bekenkamp",
|
||||||
|
"Jimmy Ledbetter",
|
||||||
"LeoZero",
|
"LeoZero",
|
||||||
"Antonio Pontes",
|
"Antonio Pontes",
|
||||||
"ApathyJones",
|
"ApathyJones",
|
||||||
"nahinahi9",
|
"nahinahi9",
|
||||||
"Anthony Faxlandez",
|
|
||||||
"Dustin Chen",
|
"Dustin Chen",
|
||||||
"dan",
|
"dan",
|
||||||
"Blackfish95",
|
"Yaboi",
|
||||||
"Mouthlessman",
|
"Mouthlessman",
|
||||||
"Steam Steam",
|
"Steam Steam",
|
||||||
"Paul Kroll",
|
"Damon Cunliffe",
|
||||||
|
"CryptoTraderJK",
|
||||||
|
"Davaitamin",
|
||||||
"otaku fra",
|
"otaku fra",
|
||||||
"semicolon drainpipe",
|
"Ran C",
|
||||||
"Thesharingbrother",
|
"tedcor",
|
||||||
"Fotek Design",
|
"Fotek Design",
|
||||||
"Bas Imagineer",
|
|
||||||
"Pat Hen",
|
|
||||||
"ResidentDeviant",
|
|
||||||
"Adam Taylor",
|
"Adam Taylor",
|
||||||
"JC",
|
|
||||||
"Weird_With_A_Beard",
|
"Weird_With_A_Beard",
|
||||||
"Prompt Pirate",
|
"MadSpin",
|
||||||
"Pozadine1",
|
"Pozadine1",
|
||||||
"uwutismxd",
|
|
||||||
"Qarob",
|
"Qarob",
|
||||||
"AIGooner",
|
"AIGooner",
|
||||||
"inbijiburu",
|
"inbijiburu",
|
||||||
"decoy",
|
|
||||||
"Luc",
|
"Luc",
|
||||||
"ProtonPrince",
|
"ProtonPrince",
|
||||||
"DiffDuck",
|
"DiffDuck",
|
||||||
@@ -258,53 +234,54 @@
|
|||||||
"thesoftwaredruid",
|
"thesoftwaredruid",
|
||||||
"wundershark",
|
"wundershark",
|
||||||
"mr_dinosaur",
|
"mr_dinosaur",
|
||||||
"Tyrswood",
|
|
||||||
"linnfrey",
|
"linnfrey",
|
||||||
"zenobeus",
|
"Gamalonia",
|
||||||
"Jackthemind",
|
"Vir",
|
||||||
"Stryker",
|
|
||||||
"Pkrsky",
|
"Pkrsky",
|
||||||
"raf8osz",
|
"Joboshy",
|
||||||
"blikkies",
|
"Bohemian Corporal",
|
||||||
|
"Dan",
|
||||||
"Josef Lanzl",
|
"Josef Lanzl",
|
||||||
|
"Seth Christensen",
|
||||||
"Griffin Dahlberg",
|
"Griffin Dahlberg",
|
||||||
"준희 김",
|
"Draven T",
|
||||||
|
"yer fey",
|
||||||
"Error_Rule34_Not_found",
|
"Error_Rule34_Not_found",
|
||||||
"Gerald Welly",
|
"Gerald Welly",
|
||||||
"Shock Shockor",
|
|
||||||
"Roslynd",
|
"Roslynd",
|
||||||
"Geolog",
|
"Geolog",
|
||||||
"Goldwaters",
|
"jinxedx",
|
||||||
"Neco28",
|
"Neco28",
|
||||||
"Zude",
|
"Aquatic Coffee",
|
||||||
|
"Dankin",
|
||||||
|
"ethanfel",
|
||||||
"Cristian Vazquez",
|
"Cristian Vazquez",
|
||||||
"Kyler",
|
"Frank Nitty",
|
||||||
"Magic Noob",
|
"Magic Noob",
|
||||||
"aRtFuL_DodGeR",
|
"Focuschannel",
|
||||||
"X",
|
|
||||||
"DougPeterson",
|
"DougPeterson",
|
||||||
"Jeff",
|
"Jeff",
|
||||||
"Bruce",
|
"Bruce",
|
||||||
"CrimsonDX",
|
|
||||||
"Kevin John Duck",
|
"Kevin John Duck",
|
||||||
|
"Anthony Faxlandez",
|
||||||
"Kevin Christopher",
|
"Kevin Christopher",
|
||||||
"Ouro Boros",
|
"Ouro Boros",
|
||||||
"DarkSunset",
|
"Blackfish95",
|
||||||
"dd",
|
"dd",
|
||||||
"Billy Gladky",
|
"Paul Kroll",
|
||||||
"Probis",
|
"MiraiKuriyamaSy",
|
||||||
"shrshpp",
|
"semicolon drainpipe",
|
||||||
"Dušan Ryban",
|
"Thesharingbrother",
|
||||||
"ItsGeneralButtNaked",
|
"Bas Imagineer",
|
||||||
"sjon kreutz",
|
"Pat Hen",
|
||||||
"Nimess",
|
|
||||||
"John Statham",
|
"John Statham",
|
||||||
"Youguang",
|
"ResidentDeviant",
|
||||||
"Nihongasuki",
|
"Nihongasuki",
|
||||||
"Metryman55",
|
"JC",
|
||||||
"andrewzpong",
|
"Prompt Pirate",
|
||||||
"FrxzenSnxw",
|
"uwutismxd",
|
||||||
"BossGame",
|
"decoy",
|
||||||
|
"Tyrswood",
|
||||||
"Ray Wing",
|
"Ray Wing",
|
||||||
"Ranzitho",
|
"Ranzitho",
|
||||||
"Gus",
|
"Gus",
|
||||||
@@ -316,7 +293,6 @@
|
|||||||
"WRL_SPR",
|
"WRL_SPR",
|
||||||
"capn",
|
"capn",
|
||||||
"Joseph",
|
"Joseph",
|
||||||
"lrdchs",
|
|
||||||
"Mirko Katzula",
|
"Mirko Katzula",
|
||||||
"dan",
|
"dan",
|
||||||
"Piccio08",
|
"Piccio08",
|
||||||
@@ -326,51 +302,135 @@
|
|||||||
"Moon Knight",
|
"Moon Knight",
|
||||||
"몽타주",
|
"몽타주",
|
||||||
"Kland",
|
"Kland",
|
||||||
"Hailshem",
|
"zenobeus",
|
||||||
|
"Jackthemind",
|
||||||
"ryoma",
|
"ryoma",
|
||||||
"John Martin",
|
"Stryker",
|
||||||
|
"raf8osz",
|
||||||
|
"ElitaSSJ4",
|
||||||
|
"blikkies",
|
||||||
"Chris",
|
"Chris",
|
||||||
"Brian M",
|
"Brian M",
|
||||||
"Nerezza",
|
"Nerezza",
|
||||||
"sanborondon",
|
"sanborondon",
|
||||||
"moranqianlong",
|
|
||||||
"Taylor Funk",
|
"Taylor Funk",
|
||||||
"aezin",
|
"aezin",
|
||||||
"Thought2Form",
|
"Thought2Form",
|
||||||
"jcay015",
|
"jcay015",
|
||||||
"Kevin Picco",
|
"Kevin Picco",
|
||||||
"Erik Lopez",
|
"Erik Lopez",
|
||||||
|
"Shock Shockor",
|
||||||
"Mateo Curić",
|
"Mateo Curić",
|
||||||
"Haru Yotu",
|
"Goldwaters",
|
||||||
|
"Zude",
|
||||||
"Eris3D",
|
"Eris3D",
|
||||||
"m",
|
"m",
|
||||||
"Pierce McBride",
|
"Pierce McBride",
|
||||||
"Joshua Gray",
|
"Joshua Gray",
|
||||||
|
"Kyler",
|
||||||
"Mikko Hemilä",
|
"Mikko Hemilä",
|
||||||
"Matura Arbeit",
|
"aRtFuL_DodGeR",
|
||||||
"Jamie Ogletree",
|
"Jamie Ogletree",
|
||||||
"TBitz33",
|
|
||||||
"Emil Bernhoff",
|
|
||||||
"a _",
|
"a _",
|
||||||
"SendingRavens",
|
|
||||||
"James Coleman",
|
"James Coleman",
|
||||||
|
"CrimsonDX",
|
||||||
"Martial",
|
"Martial",
|
||||||
"battu",
|
"battu",
|
||||||
"Emil Andersson",
|
"Emil Andersson",
|
||||||
"Chad Idk",
|
"Chad Idk",
|
||||||
"Michael Docherty",
|
"DarkSunset",
|
||||||
|
"Billy Gladky",
|
||||||
"Yuji Kaneko",
|
"Yuji Kaneko",
|
||||||
|
"Probis",
|
||||||
|
"Dušan Ryban",
|
||||||
|
"ItsGeneralButtNaked",
|
||||||
|
"Jordan Shaw",
|
||||||
|
"Rops Alot",
|
||||||
|
"Sam",
|
||||||
|
"sjon kreutz",
|
||||||
|
"Nimess",
|
||||||
|
"SRDB",
|
||||||
|
"Ace Ventura",
|
||||||
|
"g unit",
|
||||||
|
"Youguang",
|
||||||
|
"Metryman55",
|
||||||
|
"andrewzpong",
|
||||||
|
"FrxzenSnxw",
|
||||||
|
"BossGame",
|
||||||
|
"lrdchs",
|
||||||
|
"momokai",
|
||||||
|
"Hailshem",
|
||||||
|
"kudari",
|
||||||
|
"Naomi Hale Danchi",
|
||||||
|
"dc7431",
|
||||||
|
"ken",
|
||||||
|
"Inversity",
|
||||||
|
"AIVORY3D",
|
||||||
|
"epicgamer0020690",
|
||||||
|
"Joshua Porrata",
|
||||||
|
"keemun",
|
||||||
|
"SuBu",
|
||||||
|
"RedPIXel",
|
||||||
|
"Kevinj",
|
||||||
|
"Wind",
|
||||||
|
"Nexus",
|
||||||
|
"Ramneek“Guy”Ashok",
|
||||||
|
"squid_actually",
|
||||||
|
"Nat_20",
|
||||||
|
"Edward Weeks",
|
||||||
|
"kyoumei",
|
||||||
|
"RadStorm04",
|
||||||
|
"JohnDoe42054",
|
||||||
|
"BillyHill",
|
||||||
|
"emyth",
|
||||||
|
"chriphost",
|
||||||
|
"KitKatM",
|
||||||
|
"socrasteeze",
|
||||||
|
"ResidentDeviant",
|
||||||
|
"gzmzmvp",
|
||||||
|
"Welkor",
|
||||||
|
"John Martin",
|
||||||
|
"Richard",
|
||||||
|
"Andrew",
|
||||||
|
"Robert Wegemund",
|
||||||
|
"Littlehuggy",
|
||||||
|
"moranqianlong",
|
||||||
|
"Gregory Kozhemiak",
|
||||||
|
"mrjuan",
|
||||||
|
"Brian Buie",
|
||||||
|
"Sadlip",
|
||||||
|
"Haru Yotu",
|
||||||
|
"Eric Whitney",
|
||||||
|
"Joey Callahan",
|
||||||
|
"Ivan Tadic",
|
||||||
|
"Mike Simone",
|
||||||
|
"Morgandel",
|
||||||
|
"Kyron Mahan",
|
||||||
|
"Matura Arbeit",
|
||||||
|
"Noah",
|
||||||
|
"Jacob McDaniel",
|
||||||
|
"X",
|
||||||
|
"Sloan Steddy",
|
||||||
|
"TBitz33",
|
||||||
|
"Anonym dkjglfleeoeldldldlkf",
|
||||||
|
"Temikus",
|
||||||
|
"Artokun",
|
||||||
|
"Michael Taylor",
|
||||||
|
"SendingRavens",
|
||||||
|
"Derek Baker",
|
||||||
|
"Michael Anthony Scott",
|
||||||
|
"Atilla Berke Pekduyar",
|
||||||
|
"Michael Docherty",
|
||||||
|
"Nathan",
|
||||||
|
"Decx _",
|
||||||
|
"Paul Hartsuyker",
|
||||||
"elitassj",
|
"elitassj",
|
||||||
"Jacob Winter",
|
"Jacob Winter",
|
||||||
"Jordan Shaw",
|
"Distortik",
|
||||||
"Sam",
|
|
||||||
"Rops Alot",
|
|
||||||
"SRDB",
|
|
||||||
"g unit",
|
|
||||||
"Ace Ventura",
|
|
||||||
"David",
|
"David",
|
||||||
"Meilo",
|
"Meilo",
|
||||||
"Pen Bouryoung",
|
"Pen Bouryoung",
|
||||||
|
"四糸凜音",
|
||||||
"shinonomeiro",
|
"shinonomeiro",
|
||||||
"Snille",
|
"Snille",
|
||||||
"MaartenAlbers",
|
"MaartenAlbers",
|
||||||
@@ -378,101 +438,104 @@
|
|||||||
"xybrightsummer",
|
"xybrightsummer",
|
||||||
"jreedatchison",
|
"jreedatchison",
|
||||||
"PhilW",
|
"PhilW",
|
||||||
"momokai",
|
"Tree Tagger",
|
||||||
"Janik",
|
"Janik",
|
||||||
"kudari",
|
|
||||||
"Naomi Hale Danchi",
|
|
||||||
"dc7431",
|
|
||||||
"ken",
|
|
||||||
"Inversity",
|
|
||||||
"Crocket",
|
"Crocket",
|
||||||
"AIVORY3D",
|
|
||||||
"epicgamer0020690",
|
|
||||||
"Joshua Porrata",
|
|
||||||
"Cruel",
|
"Cruel",
|
||||||
"keemun",
|
|
||||||
"SuBu",
|
|
||||||
"RedPIXel",
|
|
||||||
"MRBlack",
|
"MRBlack",
|
||||||
"Kevinj",
|
|
||||||
"Wind",
|
|
||||||
"Nexus",
|
|
||||||
"Mitchell Robson",
|
"Mitchell Robson",
|
||||||
"Ramneek“Guy”Ashok",
|
|
||||||
"squid_actually",
|
|
||||||
"Nat_20",
|
|
||||||
"Kiyoe",
|
"Kiyoe",
|
||||||
"Edward Weeks",
|
|
||||||
"kyoumei",
|
|
||||||
"RadStorm04",
|
|
||||||
"JohnDoe42054",
|
|
||||||
"BillyHill",
|
|
||||||
"humptynutz",
|
"humptynutz",
|
||||||
"emyth",
|
|
||||||
"michael.isaza",
|
"michael.isaza",
|
||||||
"Kalnei",
|
"Kalnei",
|
||||||
"chriphost",
|
"Whitepinetrader",
|
||||||
"KitKatM",
|
"OrganicArtifact",
|
||||||
"socrasteeze",
|
|
||||||
"ResidentDeviant",
|
|
||||||
"Scott",
|
"Scott",
|
||||||
"gzmzmvp",
|
"MudkipMedkitz",
|
||||||
"Welkor",
|
"deanbrian",
|
||||||
|
"POPPIN",
|
||||||
|
"Alex Wortman",
|
||||||
|
"Cody",
|
||||||
|
"Raku",
|
||||||
|
"smart.edge5178",
|
||||||
|
"emadsultan",
|
||||||
|
"InformedViewz",
|
||||||
|
"CHKeeho80",
|
||||||
|
"Bubbafett",
|
||||||
|
"leaf",
|
||||||
|
"Menard",
|
||||||
|
"Skyfire83",
|
||||||
|
"Adam Rinehart",
|
||||||
|
"D",
|
||||||
|
"Pitpe11",
|
||||||
|
"TheD1rtyD03",
|
||||||
|
"moonpetal",
|
||||||
|
"SomeDude",
|
||||||
|
"g9p0o",
|
||||||
|
"nanana",
|
||||||
|
"TheHolySheep",
|
||||||
|
"Monte Won",
|
||||||
|
"SpringBootisTrash",
|
||||||
|
"carsten",
|
||||||
|
"ikok",
|
||||||
|
"Buecyb99",
|
||||||
|
"4IXplr0r3r",
|
||||||
|
"dfklsjfkljslfjd",
|
||||||
"hayden",
|
"hayden",
|
||||||
"Richard",
|
|
||||||
"ahoystan",
|
"ahoystan",
|
||||||
"Leland Saunders",
|
"Leland Saunders",
|
||||||
"Andrew",
|
"Wolfe7D1",
|
||||||
|
"Ink Temptation",
|
||||||
"Bob Barker",
|
"Bob Barker",
|
||||||
"Robert Wegemund",
|
"edk",
|
||||||
"Littlehuggy",
|
"Kalli Core",
|
||||||
"Gregory Kozhemiak",
|
|
||||||
"mrjuan",
|
|
||||||
"Aeternyx",
|
"Aeternyx",
|
||||||
"Brian Buie",
|
"elleshar666",
|
||||||
"YOU SINWOO",
|
"YOU SINWOO",
|
||||||
"Sadlip",
|
|
||||||
"ja s",
|
"ja s",
|
||||||
"Eric Whitney",
|
|
||||||
"Doug Mason",
|
"Doug Mason",
|
||||||
"Joey Callahan",
|
"Kauffy",
|
||||||
"Ivan Tadic",
|
|
||||||
"y2Rxy7FdXzWo",
|
|
||||||
"Jeremy Townsend",
|
"Jeremy Townsend",
|
||||||
"Mike Simone",
|
"EpicElric",
|
||||||
"Sean voets",
|
"Sean voets",
|
||||||
"Owen Gwosdz",
|
"Owen Gwosdz",
|
||||||
"Morgandel",
|
"John J Linehan",
|
||||||
|
"Elliot E",
|
||||||
"Thomas Wanner",
|
"Thomas Wanner",
|
||||||
"Kyron Mahan",
|
|
||||||
"Theerat Jiramate",
|
"Theerat Jiramate",
|
||||||
"Noah",
|
"Edward Kennedy",
|
||||||
"Jacob McDaniel",
|
"Justin Blaylock",
|
||||||
|
"Devil Lude",
|
||||||
|
"Nick Kage",
|
||||||
"kevin stoddard",
|
"kevin stoddard",
|
||||||
"Sloan Steddy",
|
|
||||||
"Jack Dole",
|
"Jack Dole",
|
||||||
|
"Vane Holzer",
|
||||||
|
"psytrax",
|
||||||
"Ezokewn",
|
"Ezokewn",
|
||||||
"Temikus",
|
"hexxish",
|
||||||
"Artokun",
|
"CptNeo",
|
||||||
"Michael Taylor",
|
"notedfakes",
|
||||||
"Derek Baker",
|
|
||||||
"Michael Anthony Scott",
|
|
||||||
"Atilla Berke Pekduyar",
|
|
||||||
"Maso",
|
"Maso",
|
||||||
"Nathan",
|
"Eric Ketchum",
|
||||||
"Decx _",
|
"NICHOLAS BAXLEY",
|
||||||
|
"Michael Scott",
|
||||||
"Kevin Wallace",
|
"Kevin Wallace",
|
||||||
"Matheus Couto",
|
"Matheus Couto",
|
||||||
"Paul Hartsuyker",
|
"Saya",
|
||||||
"ChicRic",
|
"ChicRic",
|
||||||
"mercur",
|
"mercur",
|
||||||
"J C",
|
"J C",
|
||||||
"Distortik",
|
"Ed Wang",
|
||||||
|
"Ryan Presley Ng",
|
||||||
|
"Wes Sims",
|
||||||
|
"Donor4115",
|
||||||
"Yves Poezevara",
|
"Yves Poezevara",
|
||||||
"Teriak47",
|
"Teriak47",
|
||||||
"Just me",
|
"Just me",
|
||||||
"Raf Stahelin",
|
"Raf Stahelin",
|
||||||
"Вячеслав Маринин",
|
"Вячеслав Маринин",
|
||||||
|
"Lyavph",
|
||||||
|
"Filippo Ferrari",
|
||||||
"Cola Matthew",
|
"Cola Matthew",
|
||||||
"OniNoKen",
|
"OniNoKen",
|
||||||
"Iain Wisely",
|
"Iain Wisely",
|
||||||
@@ -505,117 +568,100 @@
|
|||||||
"RevyHiep",
|
"RevyHiep",
|
||||||
"Captain_Swag",
|
"Captain_Swag",
|
||||||
"obkircher",
|
"obkircher",
|
||||||
"Tree Tagger",
|
|
||||||
"gwyar",
|
"gwyar",
|
||||||
"D",
|
"D",
|
||||||
"edgecase",
|
"edgecase",
|
||||||
"Neoxena",
|
"Neoxena",
|
||||||
"mrmhalo",
|
"mrmhalo",
|
||||||
"dg",
|
"dg",
|
||||||
"Whitepinetrader",
|
|
||||||
"Maarten Harms",
|
"Maarten Harms",
|
||||||
"OrganicArtifact",
|
|
||||||
"四糸凜音",
|
|
||||||
"MudkipMedkitz",
|
|
||||||
"Israel",
|
"Israel",
|
||||||
"deanbrian",
|
|
||||||
"POPPIN",
|
|
||||||
"Muratoraccio",
|
"Muratoraccio",
|
||||||
"SelfishMedic",
|
"SelfishMedic",
|
||||||
"Ginnie",
|
"Ginnie",
|
||||||
"Alex Wortman",
|
|
||||||
"Cody",
|
|
||||||
"adderleighn",
|
"adderleighn",
|
||||||
"Raku",
|
|
||||||
"smart.edge5178",
|
|
||||||
"emadsultan",
|
|
||||||
"InformedViewz",
|
|
||||||
"CHKeeho80",
|
|
||||||
"Bubbafett",
|
|
||||||
"leaf",
|
|
||||||
"Menard",
|
|
||||||
"Skyfire83",
|
|
||||||
"Adam Rinehart",
|
|
||||||
"D",
|
|
||||||
"Pitpe11",
|
|
||||||
"TheD1rtyD03",
|
|
||||||
"EnragedAntelope",
|
"EnragedAntelope",
|
||||||
"moonpetal",
|
"Alan+Cano",
|
||||||
"SomeDude",
|
"FeralOpticsAI",
|
||||||
"g9p0o",
|
"Pavlaki",
|
||||||
"nanana",
|
"generic404",
|
||||||
"TheHolySheep",
|
"Mateusz+Kosela",
|
||||||
"Monte Won",
|
"Doug+Rintoul",
|
||||||
"SpringBootisTrash",
|
"Noor",
|
||||||
"carsten",
|
"Yorunai",
|
||||||
"ikok",
|
"Bula",
|
||||||
"Buecyb99",
|
"quantenmecha",
|
||||||
"4IXplr0r3r",
|
"abattoirblues",
|
||||||
|
"Jason+Nash",
|
||||||
|
"BillyBoy84",
|
||||||
|
"DarkRoast",
|
||||||
|
"zounik",
|
||||||
|
"letzte",
|
||||||
|
"Nasty+Hobbit",
|
||||||
|
"SgtFluffles",
|
||||||
|
"lrdchs2",
|
||||||
|
"Duk3+Rand0m",
|
||||||
|
"KUJYAKU",
|
||||||
|
"NathenChoi",
|
||||||
|
"Thomas+Reck",
|
||||||
|
"Larses",
|
||||||
|
"cocona",
|
||||||
"Coeur+de+cochon",
|
"Coeur+de+cochon",
|
||||||
"David Schenck",
|
"David Schenck",
|
||||||
"han b",
|
"han b",
|
||||||
"Nico",
|
"Nico",
|
||||||
"Wolfe7D1",
|
|
||||||
"Banana Joe",
|
"Banana Joe",
|
||||||
"_ G3n",
|
"_ G3n",
|
||||||
"Donovan Jenkins",
|
"Donovan Jenkins",
|
||||||
"Ink Temptation",
|
"JBsuede",
|
||||||
"edk",
|
|
||||||
"Michael Eid",
|
"Michael Eid",
|
||||||
"beersandbacon",
|
"beersandbacon",
|
||||||
"Maximilian Pyko",
|
"Maximilian Pyko",
|
||||||
"Invis",
|
"Invis",
|
||||||
"Kalli Core",
|
|
||||||
"Justin Houston",
|
"Justin Houston",
|
||||||
|
"Time Valentine",
|
||||||
"james",
|
"james",
|
||||||
"elleshar666",
|
|
||||||
"OrochiNights",
|
"OrochiNights",
|
||||||
"Michael Zhu",
|
"Michael Zhu",
|
||||||
"ACTUALLY_the_Real_Willem_Dafoe",
|
"ACTUALLY_the_Real_Willem_Dafoe",
|
||||||
"gonzalo",
|
"gonzalo",
|
||||||
"Seraphy",
|
"Seraphy",
|
||||||
|
"Михал Михалыч",
|
||||||
"雨の心 落",
|
"雨の心 落",
|
||||||
|
"Matt",
|
||||||
"AllTimeNoobie",
|
"AllTimeNoobie",
|
||||||
"jumpd",
|
"jumpd",
|
||||||
"John C",
|
"John C",
|
||||||
"Kauffy",
|
|
||||||
"Rim",
|
"Rim",
|
||||||
"Dismem",
|
"Dismem",
|
||||||
"EpicElric",
|
"Frogmilk",
|
||||||
"John J Linehan",
|
"SPJ",
|
||||||
"Xan Dionysus",
|
"Xan Dionysus",
|
||||||
"Nathan lee",
|
"Nathan lee",
|
||||||
"Mewtora",
|
"Mewtora",
|
||||||
"Elliot E",
|
|
||||||
"Middo",
|
"Middo",
|
||||||
"Forbidden Atelier",
|
"Forbidden Atelier",
|
||||||
"Edward Kennedy",
|
"Bryan Rutkowski",
|
||||||
"Justin Blaylock",
|
|
||||||
"Adictedtohumping",
|
"Adictedtohumping",
|
||||||
"Devil Lude",
|
|
||||||
"Nick Kage",
|
|
||||||
"Towelie",
|
"Towelie",
|
||||||
"Vane Holzer",
|
|
||||||
"psytrax",
|
|
||||||
"Cyrus Fett",
|
"Cyrus Fett",
|
||||||
"Jean-françois SEMA",
|
"Jean-françois SEMA",
|
||||||
"Kurt",
|
"Kurt",
|
||||||
"hexxish",
|
"max blo",
|
||||||
"giani kidd",
|
"Xenon Xue",
|
||||||
"CptNeo",
|
"JackJohnnyJim",
|
||||||
"notedfakes",
|
"Edward Ten Eyck",
|
||||||
"Chase Kwon",
|
"Chase Kwon",
|
||||||
|
"Inyoshu",
|
||||||
"Goober719",
|
"Goober719",
|
||||||
"Eric Ketchum",
|
|
||||||
"Chad Barnes",
|
"Chad Barnes",
|
||||||
"NICHOLAS BAXLEY",
|
|
||||||
"Michael Scott",
|
|
||||||
"James Ming",
|
"James Ming",
|
||||||
"vanditking",
|
"vanditking",
|
||||||
"kripitonga",
|
"kripitonga",
|
||||||
"Rizzi",
|
"Rizzi",
|
||||||
"nimin",
|
"nimin",
|
||||||
"OMAR LUCIANO",
|
"OMAR LUCIANO",
|
||||||
|
"hannibal",
|
||||||
"Jo+Example",
|
"Jo+Example",
|
||||||
"BrentBertram",
|
"BrentBertram",
|
||||||
"eumelzocker",
|
"eumelzocker",
|
||||||
@@ -623,5 +669,5 @@
|
|||||||
"L C",
|
"L C",
|
||||||
"Dude"
|
"Dude"
|
||||||
],
|
],
|
||||||
"totalCount": 620
|
"totalCount": 666
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -341,6 +341,10 @@
|
|||||||
"saveFailed": "Ausgeschlossene Basismodelle konnten nicht gespeichert werden: {message}"
|
"saveFailed": "Ausgeschlossene Basismodelle konnten nicht gespeichert werden: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipPreviouslyDownloadedModelVersions": {
|
||||||
|
"label": "Bereits heruntergeladene Modellversionen überspringen",
|
||||||
|
"help": "Wenn aktiviert, überspringt LoRA Manager den Download einer Modellversion, wenn der Download-Verlaufsdienst diese spezifische Version als bereits heruntergeladen erfasst hat. Gilt für alle Download-Abläufe."
|
||||||
|
},
|
||||||
"layoutSettings": {
|
"layoutSettings": {
|
||||||
"displayDensity": "Anzeige-Dichte",
|
"displayDensity": "Anzeige-Dichte",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
@@ -393,8 +397,8 @@
|
|||||||
},
|
},
|
||||||
"extraFolderPaths": {
|
"extraFolderPaths": {
|
||||||
"title": "Zusätzliche Ordnerpfade",
|
"title": "Zusätzliche Ordnerpfade",
|
||||||
"help": "Fügen Sie zusätzliche Modellordner außerhalb der Standardpfade von ComfyUI hinzu. Diese Pfade werden separat gespeichert und zusammen mit den Standardordnern gescannt.",
|
"description": "Zusätzliche Modellstammverzeichnisse, die ausschließlich für LoRA Manager gelten. Laden Sie Modelle von Speicherorten außerhalb der Standardordner von ComfyUI – ideal für große Bibliotheken, die ComfyUI sonst verlangsamen würden.",
|
||||||
"description": "Konfigurieren Sie zusätzliche Ordner zum Scannen von Modellen. Diese Pfade sind spezifisch für LoRA Manager und werden mit den Standardpfaden von ComfyUI zusammengeführt.",
|
"restartRequired": "Requires restart to take effect",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "LoRA-Pfade",
|
"lora": "LoRA-Pfade",
|
||||||
"checkpoint": "Checkpoint-Pfade",
|
"checkpoint": "Checkpoint-Pfade",
|
||||||
@@ -402,7 +406,7 @@
|
|||||||
"embedding": "Embedding-Pfade"
|
"embedding": "Embedding-Pfade"
|
||||||
},
|
},
|
||||||
"pathPlaceholder": "/pfad/zu/extra/modellen",
|
"pathPlaceholder": "/pfad/zu/extra/modellen",
|
||||||
"saveSuccess": "Zusätzliche Ordnerpfade aktualisiert.",
|
"saveSuccess": "Zusätzliche Ordnerpfade aktualisiert. Neustart erforderlich, um Änderungen anzuwenden.",
|
||||||
"saveError": "Fehler beim Aktualisieren der zusätzlichen Ordnerpfade: {message}",
|
"saveError": "Fehler beim Aktualisieren der zusätzlichen Ordnerpfade: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "Dieser Pfad ist bereits konfiguriert"
|
"duplicatePath": "Dieser Pfad ist bereits konfiguriert"
|
||||||
@@ -827,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"moveToOtherTypeFolder": "In {otherType}-Ordner verschieben",
|
"moveToOtherTypeFolder": "In {otherType}-Ordner verschieben",
|
||||||
"sendToWorkflow": "[TODO: Translate] Send to Workflow"
|
"sendToWorkflow": "An Workflow senden"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embeddings": {
|
"embeddings": {
|
||||||
|
|||||||
@@ -325,7 +325,7 @@
|
|||||||
},
|
},
|
||||||
"downloadSkipBaseModels": {
|
"downloadSkipBaseModels": {
|
||||||
"label": "Skip downloads for base models",
|
"label": "Skip downloads for base models",
|
||||||
"help": "When a model version uses one of these base models, LoRA Manager will skip the download before any file transfer starts. Applies to all download flows. Only supported base models can be selected here.",
|
"help": "When enabled, versions using the selected base models will be skipped.",
|
||||||
"searchPlaceholder": "Filter base models...",
|
"searchPlaceholder": "Filter base models...",
|
||||||
"empty": "No base models match the current search.",
|
"empty": "No base models match the current search.",
|
||||||
"summary": {
|
"summary": {
|
||||||
@@ -341,6 +341,10 @@
|
|||||||
"saveFailed": "Unable to save excluded base models: {message}"
|
"saveFailed": "Unable to save excluded base models: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipPreviouslyDownloadedModelVersions": {
|
||||||
|
"label": "Skip previously downloaded model versions",
|
||||||
|
"help": "When enabled, versions downloaded before will be skipped."
|
||||||
|
},
|
||||||
"layoutSettings": {
|
"layoutSettings": {
|
||||||
"displayDensity": "Display Density",
|
"displayDensity": "Display Density",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
@@ -393,8 +397,8 @@
|
|||||||
},
|
},
|
||||||
"extraFolderPaths": {
|
"extraFolderPaths": {
|
||||||
"title": "Extra Folder Paths",
|
"title": "Extra Folder Paths",
|
||||||
"help": "Add additional model folders outside of ComfyUI's standard paths. These paths are stored separately and scanned alongside the default folders.",
|
"description": "Additional model root paths exclusive to LoRA Manager. Load models from locations outside ComfyUI's standard folders—ideal for large libraries that would otherwise slow down ComfyUI.",
|
||||||
"description": "Configure additional folders to scan for models. These paths are specific to LoRA Manager and will be merged with ComfyUI's default paths.",
|
"restartRequired": "Requires restart to take effect",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "LoRA Paths",
|
"lora": "LoRA Paths",
|
||||||
"checkpoint": "Checkpoint Paths",
|
"checkpoint": "Checkpoint Paths",
|
||||||
@@ -402,7 +406,7 @@
|
|||||||
"embedding": "Embedding Paths"
|
"embedding": "Embedding Paths"
|
||||||
},
|
},
|
||||||
"pathPlaceholder": "/path/to/extra/models",
|
"pathPlaceholder": "/path/to/extra/models",
|
||||||
"saveSuccess": "Extra folder paths updated.",
|
"saveSuccess": "Extra folder paths updated. Restart required to apply changes.",
|
||||||
"saveError": "Failed to update extra folder paths: {message}",
|
"saveError": "Failed to update extra folder paths: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "This path is already configured"
|
"duplicatePath": "This path is already configured"
|
||||||
|
|||||||
@@ -341,6 +341,10 @@
|
|||||||
"saveFailed": "No se pudieron guardar los modelos base excluidos: {message}"
|
"saveFailed": "No se pudieron guardar los modelos base excluidos: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipPreviouslyDownloadedModelVersions": {
|
||||||
|
"label": "Omitir versiones de modelos previamente descargadas",
|
||||||
|
"help": "Cuando está habilitado, LoRA Manager omitirá la descarga de una versión de modelo si el servicio de historial de descargas registra esa versión exacta como ya descargada. Aplica a todos los flujos de descarga."
|
||||||
|
},
|
||||||
"layoutSettings": {
|
"layoutSettings": {
|
||||||
"displayDensity": "Densidad de visualización",
|
"displayDensity": "Densidad de visualización",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
@@ -393,8 +397,8 @@
|
|||||||
},
|
},
|
||||||
"extraFolderPaths": {
|
"extraFolderPaths": {
|
||||||
"title": "Rutas de carpetas adicionales",
|
"title": "Rutas de carpetas adicionales",
|
||||||
"help": "Agregue carpetas de modelos adicionales fuera de las rutas estándar de ComfyUI. Estas rutas se almacenan por separado y se escanean junto con las carpetas predeterminadas.",
|
"description": "Rutas raíz de modelos adicionales exclusivas para LoRA Manager. Cargue modelos desde ubicaciones fuera de las carpetas estándar de ComfyUI, ideal para bibliotecas grandes que de otro modo ralentizarían ComfyUI.",
|
||||||
"description": "Configure carpetas adicionales para escanear modelos. Estas rutas son específicas de LoRA Manager y se fusionarán con las rutas predeterminadas de ComfyUI.",
|
"restartRequired": "Requires restart to take effect",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "Rutas de LoRA",
|
"lora": "Rutas de LoRA",
|
||||||
"checkpoint": "Rutas de Checkpoint",
|
"checkpoint": "Rutas de Checkpoint",
|
||||||
@@ -402,7 +406,7 @@
|
|||||||
"embedding": "Rutas de Embedding"
|
"embedding": "Rutas de Embedding"
|
||||||
},
|
},
|
||||||
"pathPlaceholder": "/ruta/a/modelos/extra",
|
"pathPlaceholder": "/ruta/a/modelos/extra",
|
||||||
"saveSuccess": "Rutas de carpetas adicionales actualizadas.",
|
"saveSuccess": "Rutas de carpetas adicionales actualizadas. Se requiere reinicio para aplicar los cambios.",
|
||||||
"saveError": "Error al actualizar las rutas de carpetas adicionales: {message}",
|
"saveError": "Error al actualizar las rutas de carpetas adicionales: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "Esta ruta ya está configurada"
|
"duplicatePath": "Esta ruta ya está configurada"
|
||||||
@@ -827,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"moveToOtherTypeFolder": "Mover a la carpeta {otherType}",
|
"moveToOtherTypeFolder": "Mover a la carpeta {otherType}",
|
||||||
"sendToWorkflow": "[TODO: Translate] Send to Workflow"
|
"sendToWorkflow": "Enviar al flujo de trabajo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embeddings": {
|
"embeddings": {
|
||||||
|
|||||||
@@ -341,6 +341,10 @@
|
|||||||
"saveFailed": "Impossible d’enregistrer les modèles de base exclus : {message}"
|
"saveFailed": "Impossible d’enregistrer les modèles de base exclus : {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipPreviouslyDownloadedModelVersions": {
|
||||||
|
"label": "Ignorer les versions de modèles précédemment téléchargées",
|
||||||
|
"help": "Lorsque activé, LoRA Manager ignorera le téléchargement d'une version de modèle si le service d'historique des téléchargements enregistre cette version exacte comme déjà téléchargée. S'applique à tous les flux de téléchargement."
|
||||||
|
},
|
||||||
"layoutSettings": {
|
"layoutSettings": {
|
||||||
"displayDensity": "Densité d'affichage",
|
"displayDensity": "Densité d'affichage",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
@@ -393,8 +397,8 @@
|
|||||||
},
|
},
|
||||||
"extraFolderPaths": {
|
"extraFolderPaths": {
|
||||||
"title": "Chemins de dossiers supplémentaires",
|
"title": "Chemins de dossiers supplémentaires",
|
||||||
"help": "Ajoutez des dossiers de modèles supplémentaires en dehors des chemins standard de ComfyUI. Ces chemins sont stockés séparément et analysés aux côtés des dossiers par défaut.",
|
"description": "Chemins racine de modèles supplémentaires exclusifs à LoRA Manager. Chargez des modèles depuis des emplacements en dehors des dossiers standard de ComfyUI, idéal pour les grandes bibliothèques qui ralentiraient autrement ComfyUI.",
|
||||||
"description": "Configurez des dossiers supplémentaires pour l'analyse de modèles. Ces chemins sont spécifiques à LoRA Manager et seront fusionnés avec les chemins par défaut de ComfyUI.",
|
"restartRequired": "Requires restart to take effect",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "Chemins LoRA",
|
"lora": "Chemins LoRA",
|
||||||
"checkpoint": "Chemins Checkpoint",
|
"checkpoint": "Chemins Checkpoint",
|
||||||
@@ -402,7 +406,7 @@
|
|||||||
"embedding": "Chemins Embedding"
|
"embedding": "Chemins Embedding"
|
||||||
},
|
},
|
||||||
"pathPlaceholder": "/chemin/vers/modèles/supplémentaires",
|
"pathPlaceholder": "/chemin/vers/modèles/supplémentaires",
|
||||||
"saveSuccess": "Chemins de dossiers supplémentaires mis à jour.",
|
"saveSuccess": "Chemins de dossiers supplémentaires mis à jour. Redémarrage requis pour appliquer les changements.",
|
||||||
"saveError": "Échec de la mise à jour des chemins de dossiers supplémentaires: {message}",
|
"saveError": "Échec de la mise à jour des chemins de dossiers supplémentaires: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "Ce chemin est déjà configuré"
|
"duplicatePath": "Ce chemin est déjà configuré"
|
||||||
@@ -827,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"moveToOtherTypeFolder": "Déplacer vers le dossier {otherType}",
|
"moveToOtherTypeFolder": "Déplacer vers le dossier {otherType}",
|
||||||
"sendToWorkflow": "[TODO: Translate] Send to Workflow"
|
"sendToWorkflow": "Envoyer vers le workflow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embeddings": {
|
"embeddings": {
|
||||||
|
|||||||
@@ -341,6 +341,10 @@
|
|||||||
"saveFailed": "לא ניתן לשמור את מודלי הבסיס המוחרגים: {message}"
|
"saveFailed": "לא ניתן לשמור את מודלי הבסיס המוחרגים: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipPreviouslyDownloadedModelVersions": {
|
||||||
|
"label": "דלג על גרסאות מודלים שהורדו בעבר",
|
||||||
|
"help": "כאשר מופעל, LoRA Manager ידלג על הורדת גרסת מודל אם שירות היסטוריית ההורדות רושם את הגרסה המדויקת הזו ככבר שהורדה. חל על כל תהליכי ההורדה."
|
||||||
|
},
|
||||||
"layoutSettings": {
|
"layoutSettings": {
|
||||||
"displayDensity": "צפיפות תצוגה",
|
"displayDensity": "צפיפות תצוגה",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
@@ -393,8 +397,8 @@
|
|||||||
},
|
},
|
||||||
"extraFolderPaths": {
|
"extraFolderPaths": {
|
||||||
"title": "נתיבי תיקיות נוספים",
|
"title": "נתיבי תיקיות נוספים",
|
||||||
"help": "הוסף תיקיות מודלים נוספות מחוץ לנתיבים הסטנדרטיים של ComfyUI. נתיבים אלה נשמרים בנפרד ונסרקים לצד תיקיות ברירת המחדל.",
|
"description": "נתיבי שורש מודלים נוספים בלעדיים ל-LoRA Manager. טען מודלים ממיקומים מחוץ לתיקיות הסטנדרטיות של ComfyUI - אידיאלי לספריות גדולות שאחרת יאטו את ComfyUI.",
|
||||||
"description": "הגדר תיקיות נוספות לסריקת מודלים. נתיבים אלה ספציפיים ל-LoRA Manager וימוזגו עם נתיבי ברירת המחדל של ComfyUI.",
|
"restartRequired": "Requires restart to take effect",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "נתיבי LoRA",
|
"lora": "נתיבי LoRA",
|
||||||
"checkpoint": "נתיבי Checkpoint",
|
"checkpoint": "נתיבי Checkpoint",
|
||||||
@@ -402,7 +406,7 @@
|
|||||||
"embedding": "נתיבי Embedding"
|
"embedding": "נתיבי Embedding"
|
||||||
},
|
},
|
||||||
"pathPlaceholder": "/נתיב/למודלים/נוספים",
|
"pathPlaceholder": "/נתיב/למודלים/נוספים",
|
||||||
"saveSuccess": "נתיבי תיקיות נוספים עודכנו.",
|
"saveSuccess": "נתיבי תיקיות נוספים עודכנו. נדרשת הפעלה מחדש כדי להחיל את השינויים.",
|
||||||
"saveError": "נכשל בעדכון נתיבי תיקיות נוספים: {message}",
|
"saveError": "נכשל בעדכון נתיבי תיקיות נוספים: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "נתיב זה כבר מוגדר"
|
"duplicatePath": "נתיב זה כבר מוגדר"
|
||||||
@@ -827,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"moveToOtherTypeFolder": "העבר לתיקיית {otherType}",
|
"moveToOtherTypeFolder": "העבר לתיקיית {otherType}",
|
||||||
"sendToWorkflow": "[TODO: Translate] Send to Workflow"
|
"sendToWorkflow": "שלח ל-workflow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embeddings": {
|
"embeddings": {
|
||||||
|
|||||||
@@ -341,6 +341,10 @@
|
|||||||
"saveFailed": "除外するベースモデルを保存できませんでした: {message}"
|
"saveFailed": "除外するベースモデルを保存できませんでした: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipPreviouslyDownloadedModelVersions": {
|
||||||
|
"label": "以前にダウンロードしたモデルバージョンをスキップ",
|
||||||
|
"help": "有効にすると、ダウンロード履歴サービスがそのバージョンが既にダウンロード済みと記録している場合、LoRA Managerはそのモデルバージョンのダウンロードをスキップします。すべてのダウンロードフローに適用されます。"
|
||||||
|
},
|
||||||
"layoutSettings": {
|
"layoutSettings": {
|
||||||
"displayDensity": "表示密度",
|
"displayDensity": "表示密度",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
@@ -393,8 +397,8 @@
|
|||||||
},
|
},
|
||||||
"extraFolderPaths": {
|
"extraFolderPaths": {
|
||||||
"title": "追加フォルダーパス",
|
"title": "追加フォルダーパス",
|
||||||
"help": "ComfyUIの標準パスの外部に追加のモデルフォルダを追加します。これらのパスは別々に保存され、デフォルトのフォルダと一緒にスキャンされます。",
|
"description": "LoRA Manager専用の追加モデルルートパス。ComfyUIの標準フォルダー外の場所からモデルを読み込みます。ComfyUIの動作を低下させる可能性のある大規模ライブラリに最適です。",
|
||||||
"description": "モデルをスキャンするための追加フォルダを設定します。これらのパスはLoRA Manager固有であり、ComfyUIのデフォルトパスとマージされます。",
|
"restartRequired": "Requires restart to take effect",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "LoRAパス",
|
"lora": "LoRAパス",
|
||||||
"checkpoint": "Checkpointパス",
|
"checkpoint": "Checkpointパス",
|
||||||
@@ -402,7 +406,7 @@
|
|||||||
"embedding": "Embeddingパス"
|
"embedding": "Embeddingパス"
|
||||||
},
|
},
|
||||||
"pathPlaceholder": "/追加モデルへのパス",
|
"pathPlaceholder": "/追加モデルへのパス",
|
||||||
"saveSuccess": "追加フォルダーパスを更新しました。",
|
"saveSuccess": "追加フォルダーパスを更新しました。変更を適用するには再起動が必要です。",
|
||||||
"saveError": "追加フォルダーパスの更新に失敗しました: {message}",
|
"saveError": "追加フォルダーパスの更新に失敗しました: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "このパスはすでに設定されています"
|
"duplicatePath": "このパスはすでに設定されています"
|
||||||
@@ -827,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"moveToOtherTypeFolder": "{otherType} フォルダに移動",
|
"moveToOtherTypeFolder": "{otherType} フォルダに移動",
|
||||||
"sendToWorkflow": "[TODO: Translate] Send to Workflow"
|
"sendToWorkflow": "ワークフローに送信"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embeddings": {
|
"embeddings": {
|
||||||
|
|||||||
@@ -341,6 +341,10 @@
|
|||||||
"saveFailed": "제외된 기본 모델을 저장할 수 없습니다: {message}"
|
"saveFailed": "제외된 기본 모델을 저장할 수 없습니다: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipPreviouslyDownloadedModelVersions": {
|
||||||
|
"label": "이전에 다운로드한 모델 버전 건너뛰기",
|
||||||
|
"help": "활성화하면 다운로드 기록 서비스가 해당 버전이 이미 다운로드되었음을 기록한 경우 LoRA Manager는 해당 모델 버전 다운로드를 건너뜁니다. 모든 다운로드 플로우에 적용됩니다."
|
||||||
|
},
|
||||||
"layoutSettings": {
|
"layoutSettings": {
|
||||||
"displayDensity": "표시 밀도",
|
"displayDensity": "표시 밀도",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
@@ -393,8 +397,8 @@
|
|||||||
},
|
},
|
||||||
"extraFolderPaths": {
|
"extraFolderPaths": {
|
||||||
"title": "추가 폴다 경로",
|
"title": "추가 폴다 경로",
|
||||||
"help": "ComfyUI의 표준 경로 외부에 추가 모델 폴드를 추가하세요. 이러한 경로는 별도로 저장되며 기본 폴와 함께 스캔됩니다.",
|
"description": "LoRA Manager 전용 추가 모델 루트 경로입니다. ComfyUI의 표준 폴더 외부 위치에서 모델을 로드하여 대규모 라이브러리로 인한 성능 저하를 방지합니다.",
|
||||||
"description": "모델을 스캔하기 위한 추가 폴를 설정하세요. 이러한 경로는 LoRA Manager 특유의 것이며 ComfyUI의 기본 경로와 병합됩니다.",
|
"restartRequired": "Requires restart to take effect",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "LoRA 경로",
|
"lora": "LoRA 경로",
|
||||||
"checkpoint": "Checkpoint 경로",
|
"checkpoint": "Checkpoint 경로",
|
||||||
@@ -402,7 +406,7 @@
|
|||||||
"embedding": "Embedding 경로"
|
"embedding": "Embedding 경로"
|
||||||
},
|
},
|
||||||
"pathPlaceholder": "/추가/모델/경로",
|
"pathPlaceholder": "/추가/모델/경로",
|
||||||
"saveSuccess": "추가 폴다 경로가 업데이트되었습니다.",
|
"saveSuccess": "추가 폴다 경로가 업데이트되었습니다. 변경 사항을 적용하려면 재시작이 필요합니다.",
|
||||||
"saveError": "추가 폴다 경로 업데이트 실패: {message}",
|
"saveError": "추가 폴다 경로 업데이트 실패: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "이 경로는 이미 구성되어 있습니다"
|
"duplicatePath": "이 경로는 이미 구성되어 있습니다"
|
||||||
@@ -827,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"moveToOtherTypeFolder": "{otherType} 폴더로 이동",
|
"moveToOtherTypeFolder": "{otherType} 폴더로 이동",
|
||||||
"sendToWorkflow": "[TODO: Translate] Send to Workflow"
|
"sendToWorkflow": "워크플로우로 전송"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embeddings": {
|
"embeddings": {
|
||||||
|
|||||||
@@ -341,6 +341,10 @@
|
|||||||
"saveFailed": "Не удалось сохранить исключённые базовые модели: {message}"
|
"saveFailed": "Не удалось сохранить исключённые базовые модели: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipPreviouslyDownloadedModelVersions": {
|
||||||
|
"label": "Пропускать ранее загруженные версии моделей",
|
||||||
|
"help": "Если включено, LoRA Manager будет пропускать загрузку версии модели, если сервис истории загрузок записал, что эта конкретная версия уже загружена. Применяется ко всем потокам загрузки."
|
||||||
|
},
|
||||||
"layoutSettings": {
|
"layoutSettings": {
|
||||||
"displayDensity": "Плотность отображения",
|
"displayDensity": "Плотность отображения",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
@@ -393,8 +397,8 @@
|
|||||||
},
|
},
|
||||||
"extraFolderPaths": {
|
"extraFolderPaths": {
|
||||||
"title": "Дополнительные пути к папкам",
|
"title": "Дополнительные пути к папкам",
|
||||||
"help": "Добавьте дополнительные папки моделей за пределами стандартных путей ComfyUI. Эти пути хранятся отдельно и сканируются вместе с папками по умолчанию.",
|
"description": "Дополнительные корневые пути моделей, эксклюзивные для LoRA Manager. Загружайте модели из расположений за пределами стандартных папок ComfyUI — идеально подходит для больших библиотек, которые иначе замедлили бы ComfyUI.",
|
||||||
"description": "Настройте дополнительные папки для сканирования моделей. Эти пути специфичны для LoRA Manager и будут объединены с путями по умолчанию ComfyUI.",
|
"restartRequired": "Requires restart to take effect",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "Пути LoRA",
|
"lora": "Пути LoRA",
|
||||||
"checkpoint": "Пути Checkpoint",
|
"checkpoint": "Пути Checkpoint",
|
||||||
@@ -402,7 +406,7 @@
|
|||||||
"embedding": "Пути Embedding"
|
"embedding": "Пути Embedding"
|
||||||
},
|
},
|
||||||
"pathPlaceholder": "/путь/к/дополнительным/моделям",
|
"pathPlaceholder": "/путь/к/дополнительным/моделям",
|
||||||
"saveSuccess": "Дополнительные пути к папкам обновлены.",
|
"saveSuccess": "Дополнительные пути к папкам обновлены. Требуется перезапуск для применения изменений.",
|
||||||
"saveError": "Не удалось обновить дополнительные пути к папкам: {message}",
|
"saveError": "Не удалось обновить дополнительные пути к папкам: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "Этот путь уже настроен"
|
"duplicatePath": "Этот путь уже настроен"
|
||||||
@@ -827,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"moveToOtherTypeFolder": "Переместить в папку {otherType}",
|
"moveToOtherTypeFolder": "Переместить в папку {otherType}",
|
||||||
"sendToWorkflow": "[TODO: Translate] Send to Workflow"
|
"sendToWorkflow": "Отправить в workflow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embeddings": {
|
"embeddings": {
|
||||||
|
|||||||
@@ -341,6 +341,10 @@
|
|||||||
"saveFailed": "无法保存已排除的基础模型:{message}"
|
"saveFailed": "无法保存已排除的基础模型:{message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipPreviouslyDownloadedModelVersions": {
|
||||||
|
"label": "跳过已下载的模型版本",
|
||||||
|
"help": "启用后,如果下载历史服务记录显示该版本已下载,LoRA Manager 将跳过下载该模型版本。适用于所有下载流程。"
|
||||||
|
},
|
||||||
"layoutSettings": {
|
"layoutSettings": {
|
||||||
"displayDensity": "显示密度",
|
"displayDensity": "显示密度",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
@@ -393,8 +397,8 @@
|
|||||||
},
|
},
|
||||||
"extraFolderPaths": {
|
"extraFolderPaths": {
|
||||||
"title": "额外文件夹路径",
|
"title": "额外文件夹路径",
|
||||||
"help": "在 ComfyUI 的标准路径之外添加额外的模型文件夹。这些路径单独存储,并与默认文件夹一起扫描。",
|
"description": "LoRA Manager 专属的额外模型根目录。从 ComfyUI 标准文件夹之外的位置加载模型,特别适合管理大型模型库,避免影响 ComfyUI 性能。",
|
||||||
"description": "配置额外的文件夹以扫描模型。这些路径是 LoRA Manager 特有的,将与 ComfyUI 的默认路径合并。",
|
"restartRequired": "需要重启才能生效",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "LoRA 路径",
|
"lora": "LoRA 路径",
|
||||||
"checkpoint": "Checkpoint 路径",
|
"checkpoint": "Checkpoint 路径",
|
||||||
@@ -402,7 +406,7 @@
|
|||||||
"embedding": "Embedding 路径"
|
"embedding": "Embedding 路径"
|
||||||
},
|
},
|
||||||
"pathPlaceholder": "/额外/模型/路径",
|
"pathPlaceholder": "/额外/模型/路径",
|
||||||
"saveSuccess": "额外文件夹路径已更新。",
|
"saveSuccess": "额外文件夹路径已更新,需要重启才能生效。",
|
||||||
"saveError": "更新额外文件夹路径失败:{message}",
|
"saveError": "更新额外文件夹路径失败:{message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "此路径已配置"
|
"duplicatePath": "此路径已配置"
|
||||||
@@ -827,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"moveToOtherTypeFolder": "移动到 {otherType} 文件夹",
|
"moveToOtherTypeFolder": "移动到 {otherType} 文件夹",
|
||||||
"sendToWorkflow": "[TODO: Translate] Send to Workflow"
|
"sendToWorkflow": "发送到工作流"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embeddings": {
|
"embeddings": {
|
||||||
|
|||||||
@@ -341,6 +341,10 @@
|
|||||||
"saveFailed": "無法儲存已排除的基礎模型:{message}"
|
"saveFailed": "無法儲存已排除的基礎模型:{message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipPreviouslyDownloadedModelVersions": {
|
||||||
|
"label": "跳過已下載的模型版本",
|
||||||
|
"help": "啟用後,如果下載歷史服務記錄顯示該版本已下載,LoRA Manager 將跳過下載該模型版本。適用於所有下載流程。"
|
||||||
|
},
|
||||||
"layoutSettings": {
|
"layoutSettings": {
|
||||||
"displayDensity": "顯示密度",
|
"displayDensity": "顯示密度",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
@@ -393,8 +397,8 @@
|
|||||||
},
|
},
|
||||||
"extraFolderPaths": {
|
"extraFolderPaths": {
|
||||||
"title": "額外資料夾路徑",
|
"title": "額外資料夾路徑",
|
||||||
"help": "在 ComfyUI 的標準路徑之外新增額外的模型資料夾。這些路徑單獨儲存,並與預設資料夾一起掃描。",
|
"description": "LoRA Manager 專屬的額外模型根目錄。從 ComfyUI 標準資料夾之外的位置載入模型,特別適合管理大型模型庫,避免影響 ComfyUI 效能。",
|
||||||
"description": "設定額外的資料夾以掃描模型。這些路徑是 LoRA Manager 特有的,將與 ComfyUI 的預設路徑合併。",
|
"restartRequired": "Requires restart to take effect",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "LoRA 路徑",
|
"lora": "LoRA 路徑",
|
||||||
"checkpoint": "Checkpoint 路徑",
|
"checkpoint": "Checkpoint 路徑",
|
||||||
@@ -402,7 +406,7 @@
|
|||||||
"embedding": "Embedding 路徑"
|
"embedding": "Embedding 路徑"
|
||||||
},
|
},
|
||||||
"pathPlaceholder": "/額外/模型/路徑",
|
"pathPlaceholder": "/額外/模型/路徑",
|
||||||
"saveSuccess": "額外資料夾路徑已更新。",
|
"saveSuccess": "額外資料夾路徑已更新,需要重啟才能生效。",
|
||||||
"saveError": "更新額外資料夾路徑失敗:{message}",
|
"saveError": "更新額外資料夾路徑失敗:{message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "此路徑已設定"
|
"duplicatePath": "此路徑已設定"
|
||||||
@@ -827,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"moveToOtherTypeFolder": "移動到 {otherType} 資料夾",
|
"moveToOtherTypeFolder": "移動到 {otherType} 資料夾",
|
||||||
"sendToWorkflow": "[TODO: Translate] Send to Workflow"
|
"sendToWorkflow": "傳送到工作流"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embeddings": {
|
"embeddings": {
|
||||||
|
|||||||
61
py/config.py
61
py/config.py
@@ -25,6 +25,31 @@ standalone_mode = (
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_valid_default_root(
|
||||||
|
current: str, primary_paths: List[str], name: str
|
||||||
|
) -> str:
|
||||||
|
"""Return a valid default root from the current primary path set."""
|
||||||
|
|
||||||
|
valid_paths = [path for path in primary_paths if isinstance(path, str) and path.strip()]
|
||||||
|
if not valid_paths:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if current in valid_paths:
|
||||||
|
return current
|
||||||
|
|
||||||
|
if current:
|
||||||
|
logger.info(
|
||||||
|
"Repaired stale %s from '%s' to '%s'",
|
||||||
|
name,
|
||||||
|
current,
|
||||||
|
valid_paths[0],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info("Auto-setting %s to '%s'", name, valid_paths[0])
|
||||||
|
|
||||||
|
return valid_paths[0]
|
||||||
|
|
||||||
|
|
||||||
def _normalize_folder_paths_for_comparison(
|
def _normalize_folder_paths_for_comparison(
|
||||||
folder_paths: Mapping[str, Iterable[str]],
|
folder_paths: Mapping[str, Iterable[str]],
|
||||||
) -> Dict[str, Set[str]]:
|
) -> Dict[str, Set[str]]:
|
||||||
@@ -197,25 +222,23 @@ class Config:
|
|||||||
"Failed to rename legacy 'default' library: %s", rename_error
|
"Failed to rename legacy 'default' library: %s", rename_error
|
||||||
)
|
)
|
||||||
|
|
||||||
default_lora_root = comfy_library.get("default_lora_root", "")
|
default_lora_root = _resolve_valid_default_root(
|
||||||
if not default_lora_root and len(self.loras_roots) == 1:
|
comfy_library.get("default_lora_root", ""),
|
||||||
default_lora_root = self.loras_roots[0]
|
list(self.loras_roots or []),
|
||||||
|
"default_lora_root",
|
||||||
|
)
|
||||||
|
|
||||||
default_checkpoint_root = comfy_library.get("default_checkpoint_root", "")
|
default_checkpoint_root = _resolve_valid_default_root(
|
||||||
if (
|
comfy_library.get("default_checkpoint_root", ""),
|
||||||
not default_checkpoint_root
|
list(self.checkpoints_roots or []),
|
||||||
and self.checkpoints_roots
|
"default_checkpoint_root",
|
||||||
and len(self.checkpoints_roots) == 1
|
)
|
||||||
):
|
|
||||||
default_checkpoint_root = self.checkpoints_roots[0]
|
|
||||||
|
|
||||||
default_embedding_root = comfy_library.get("default_embedding_root", "")
|
default_embedding_root = _resolve_valid_default_root(
|
||||||
if (
|
comfy_library.get("default_embedding_root", ""),
|
||||||
not default_embedding_root
|
list(self.embeddings_roots or []),
|
||||||
and self.embeddings_roots
|
"default_embedding_root",
|
||||||
and len(self.embeddings_roots) == 1
|
)
|
||||||
):
|
|
||||||
default_embedding_root = self.embeddings_roots[0]
|
|
||||||
|
|
||||||
metadata = dict(comfy_library.get("metadata", {}))
|
metadata = dict(comfy_library.get("metadata", {}))
|
||||||
metadata.setdefault("display_name", "ComfyUI")
|
metadata.setdefault("display_name", "ComfyUI")
|
||||||
@@ -706,7 +729,9 @@ class Config:
|
|||||||
return unique_paths
|
return unique_paths
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_path_for_comparison(path: str, *, resolve_realpath: bool = False) -> str:
|
def _normalize_path_for_comparison(
|
||||||
|
path: str, *, resolve_realpath: bool = False
|
||||||
|
) -> str:
|
||||||
"""Normalize a path for equality checks across platforms."""
|
"""Normalize a path for equality checks across platforms."""
|
||||||
candidate = os.path.realpath(path) if resolve_realpath else path
|
candidate = os.path.realpath(path) if resolve_realpath else path
|
||||||
return os.path.normcase(os.path.normpath(candidate)).replace(os.sep, "/")
|
return os.path.normcase(os.path.normpath(candidate)).replace(os.sep, "/")
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ from typing import Awaitable, Callable, Dict, List
|
|||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
# Use wildcard for CivitAI to support their CDN subdomains (e.g., image-b2.civitai.com)
|
||||||
|
# Security note: This is acceptable because:
|
||||||
|
# 1. CSP img-src only controls image/video loading, not script execution
|
||||||
|
# 2. All *.civitai.com subdomains are controlled by Civitai
|
||||||
|
# 3. Explicit domain list would require constant updates as Civitai adds CDN nodes
|
||||||
REMOTE_MEDIA_SOURCES = (
|
REMOTE_MEDIA_SOURCES = (
|
||||||
"https://image.civitai.com",
|
"https://*.civitai.com",
|
||||||
"https://img.genur.art",
|
"https://img.genur.art",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def relax_csp_for_remote_media(
|
async def relax_csp_for_remote_media(
|
||||||
request: web.Request, handler: Callable[[web.Request], Awaitable[web.StreamResponse]]
|
request: web.Request,
|
||||||
|
handler: Callable[[web.Request], Awaitable[web.StreamResponse]],
|
||||||
) -> web.StreamResponse:
|
) -> web.StreamResponse:
|
||||||
"""Allow LoRA Manager media previews to load from trusted remote domains.
|
"""Allow LoRA Manager media previews to load from trusted remote domains.
|
||||||
|
|
||||||
@@ -43,7 +49,9 @@ async def relax_csp_for_remote_media(
|
|||||||
directive_order.append(name)
|
directive_order.append(name)
|
||||||
directives[name] = values
|
directives[name] = values
|
||||||
|
|
||||||
def merge_sources(name: str, sources: List[str], defaults: List[str] | None = None) -> None:
|
def merge_sources(
|
||||||
|
name: str, sources: List[str], defaults: List[str] | None = None
|
||||||
|
) -> None:
|
||||||
existing = directives.get(name, list(defaults or []))
|
existing = directives.get(name, list(defaults or []))
|
||||||
|
|
||||||
for source in sources:
|
for source in sources:
|
||||||
|
|||||||
@@ -751,6 +751,7 @@ class ServiceRegistryAdapter:
|
|||||||
get_lora_scanner: Callable[[], Awaitable]
|
get_lora_scanner: Callable[[], Awaitable]
|
||||||
get_checkpoint_scanner: Callable[[], Awaitable]
|
get_checkpoint_scanner: Callable[[], Awaitable]
|
||||||
get_embedding_scanner: Callable[[], Awaitable]
|
get_embedding_scanner: Callable[[], Awaitable]
|
||||||
|
get_downloaded_version_history_service: Callable[[], Awaitable]
|
||||||
|
|
||||||
|
|
||||||
class ModelLibraryHandler:
|
class ModelLibraryHandler:
|
||||||
@@ -764,6 +765,41 @@ class ModelLibraryHandler:
|
|||||||
self._service_registry = service_registry
|
self._service_registry = service_registry
|
||||||
self._metadata_provider_factory = metadata_provider_factory
|
self._metadata_provider_factory = metadata_provider_factory
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_model_type(model_type: str | None) -> str | None:
|
||||||
|
if not isinstance(model_type, str):
|
||||||
|
return None
|
||||||
|
normalized = model_type.strip().lower()
|
||||||
|
if normalized in {"lora", "locon", "dora"}:
|
||||||
|
return "lora"
|
||||||
|
if normalized == "checkpoint":
|
||||||
|
return "checkpoint"
|
||||||
|
if normalized in {"embedding", "textualinversion"}:
|
||||||
|
return "embedding"
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _get_scanner_for_type(self, model_type: str | None):
|
||||||
|
normalized_type = self._normalize_model_type(model_type)
|
||||||
|
if normalized_type == "lora":
|
||||||
|
return normalized_type, await self._service_registry.get_lora_scanner()
|
||||||
|
if normalized_type == "checkpoint":
|
||||||
|
return normalized_type, await self._service_registry.get_checkpoint_scanner()
|
||||||
|
if normalized_type == "embedding":
|
||||||
|
return normalized_type, await self._service_registry.get_embedding_scanner()
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
async def _get_download_history_service(self):
|
||||||
|
return await self._service_registry.get_downloaded_version_history_service()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _with_downloaded_flag(versions: list[dict]) -> list[dict]:
|
||||||
|
enriched: list[dict] = []
|
||||||
|
for version in versions:
|
||||||
|
entry = dict(version)
|
||||||
|
entry.setdefault("hasBeenDownloaded", True)
|
||||||
|
enriched.append(entry)
|
||||||
|
return enriched
|
||||||
|
|
||||||
async def check_model_exists(self, request: web.Request) -> web.Response:
|
async def check_model_exists(self, request: web.Request) -> web.Response:
|
||||||
try:
|
try:
|
||||||
model_id_str = request.query.get("modelId")
|
model_id_str = request.query.get("modelId")
|
||||||
@@ -819,11 +855,30 @@ class ModelLibraryHandler:
|
|||||||
exists = True
|
exists = True
|
||||||
model_type = "embedding"
|
model_type = "embedding"
|
||||||
|
|
||||||
|
history_service = await self._get_download_history_service()
|
||||||
|
has_been_downloaded = False
|
||||||
|
history_type = model_type
|
||||||
|
if history_type:
|
||||||
|
has_been_downloaded = await history_service.has_been_downloaded(
|
||||||
|
history_type,
|
||||||
|
model_version_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for candidate_type in ("lora", "checkpoint", "embedding"):
|
||||||
|
if await history_service.has_been_downloaded(
|
||||||
|
candidate_type,
|
||||||
|
model_version_id,
|
||||||
|
):
|
||||||
|
has_been_downloaded = True
|
||||||
|
history_type = candidate_type
|
||||||
|
break
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"exists": exists,
|
"exists": exists,
|
||||||
"modelType": model_type if exists else None,
|
"modelType": model_type if exists else history_type,
|
||||||
|
"hasBeenDownloaded": has_been_downloaded,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -841,23 +896,166 @@ class ModelLibraryHandler:
|
|||||||
|
|
||||||
model_type = None
|
model_type = None
|
||||||
versions = []
|
versions = []
|
||||||
|
downloaded_version_ids = []
|
||||||
|
history_service = await self._get_download_history_service()
|
||||||
if lora_versions:
|
if lora_versions:
|
||||||
model_type = "lora"
|
model_type = "lora"
|
||||||
versions = lora_versions
|
versions = self._with_downloaded_flag(lora_versions)
|
||||||
|
downloaded_version_ids = await history_service.get_downloaded_version_ids(
|
||||||
|
model_type,
|
||||||
|
model_id,
|
||||||
|
)
|
||||||
elif checkpoint_versions:
|
elif checkpoint_versions:
|
||||||
model_type = "checkpoint"
|
model_type = "checkpoint"
|
||||||
versions = checkpoint_versions
|
versions = self._with_downloaded_flag(checkpoint_versions)
|
||||||
|
downloaded_version_ids = await history_service.get_downloaded_version_ids(
|
||||||
|
model_type,
|
||||||
|
model_id,
|
||||||
|
)
|
||||||
elif embedding_versions:
|
elif embedding_versions:
|
||||||
model_type = "embedding"
|
model_type = "embedding"
|
||||||
versions = embedding_versions
|
versions = self._with_downloaded_flag(embedding_versions)
|
||||||
|
downloaded_version_ids = await history_service.get_downloaded_version_ids(
|
||||||
|
model_type,
|
||||||
|
model_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for candidate_type in ("lora", "checkpoint", "embedding"):
|
||||||
|
candidate_downloaded_version_ids = (
|
||||||
|
await history_service.get_downloaded_version_ids(
|
||||||
|
candidate_type,
|
||||||
|
model_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if candidate_downloaded_version_ids:
|
||||||
|
model_type = candidate_type
|
||||||
|
downloaded_version_ids = candidate_downloaded_version_ids
|
||||||
|
break
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"success": True, "modelType": model_type, "versions": versions}
|
{
|
||||||
|
"success": True,
|
||||||
|
"modelType": model_type,
|
||||||
|
"versions": versions,
|
||||||
|
"downloadedVersionIds": downloaded_version_ids,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
except Exception as exc: # pragma: no cover - defensive logging
|
except Exception as exc: # pragma: no cover - defensive logging
|
||||||
logger.error("Failed to check model existence: %s", exc, exc_info=True)
|
logger.error("Failed to check model existence: %s", exc, exc_info=True)
|
||||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def get_model_version_download_status(
|
||||||
|
self, request: web.Request
|
||||||
|
) -> web.Response:
|
||||||
|
try:
|
||||||
|
model_type, _ = await self._get_scanner_for_type(request.query.get("modelType"))
|
||||||
|
if not model_type:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "Parameter modelType is required"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
model_version_id_str = request.query.get("modelVersionId")
|
||||||
|
if not model_version_id_str:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "Missing required parameter: modelVersionId"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
model_version_id = int(model_version_id_str)
|
||||||
|
except ValueError:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "Parameter modelVersionId must be an integer"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
history_service = await self._get_download_history_service()
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"modelType": model_type,
|
||||||
|
"modelVersionId": model_version_id,
|
||||||
|
"hasBeenDownloaded": await history_service.has_been_downloaded(
|
||||||
|
model_type,
|
||||||
|
model_version_id,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as exc: # pragma: no cover - defensive logging
|
||||||
|
logger.error(
|
||||||
|
"Failed to get model version download status: %s",
|
||||||
|
exc,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def set_model_version_download_status(
|
||||||
|
self, request: web.Request
|
||||||
|
) -> web.Response:
|
||||||
|
try:
|
||||||
|
if request.method == "GET":
|
||||||
|
data = request.query
|
||||||
|
else:
|
||||||
|
data = await request.json()
|
||||||
|
model_type, _ = await self._get_scanner_for_type(data.get("modelType"))
|
||||||
|
if not model_type:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "Parameter modelType is required"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
model_version_id = int(data.get("modelVersionId"))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "Parameter modelVersionId must be an integer"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
downloaded = data.get("downloaded")
|
||||||
|
if isinstance(downloaded, str):
|
||||||
|
normalized_downloaded = downloaded.strip().lower()
|
||||||
|
if normalized_downloaded in {"true", "1"}:
|
||||||
|
downloaded = True
|
||||||
|
elif normalized_downloaded in {"false", "0"}:
|
||||||
|
downloaded = False
|
||||||
|
|
||||||
|
if not isinstance(downloaded, bool):
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "Parameter downloaded must be a boolean"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
history_service = await self._get_download_history_service()
|
||||||
|
if downloaded:
|
||||||
|
model_id = data.get("modelId")
|
||||||
|
file_path = data.get("filePath")
|
||||||
|
await history_service.mark_downloaded(
|
||||||
|
model_type,
|
||||||
|
model_version_id,
|
||||||
|
model_id=model_id,
|
||||||
|
source="manual",
|
||||||
|
file_path=file_path if isinstance(file_path, str) else None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await history_service.mark_not_downloaded(model_type, model_version_id)
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"modelType": model_type,
|
||||||
|
"modelVersionId": model_version_id,
|
||||||
|
"hasBeenDownloaded": downloaded,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as exc: # pragma: no cover - defensive logging
|
||||||
|
logger.error(
|
||||||
|
"Failed to set model version download status: %s",
|
||||||
|
exc,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
async def get_model_versions_status(self, request: web.Request) -> web.Response:
|
async def get_model_versions_status(self, request: web.Request) -> web.Response:
|
||||||
try:
|
try:
|
||||||
model_id_str = request.query.get("modelId")
|
model_id_str = request.query.get("modelId")
|
||||||
@@ -896,18 +1094,8 @@ class ModelLibraryHandler:
|
|||||||
model_name = response.get("name", "")
|
model_name = response.get("name", "")
|
||||||
model_type = response.get("type", "").lower()
|
model_type = response.get("type", "").lower()
|
||||||
|
|
||||||
scanner = None
|
normalized_type, scanner = await self._get_scanner_for_type(model_type)
|
||||||
normalized_type = None
|
if not normalized_type:
|
||||||
if model_type in {"lora", "locon", "dora"}:
|
|
||||||
scanner = await self._service_registry.get_lora_scanner()
|
|
||||||
normalized_type = "lora"
|
|
||||||
elif model_type == "checkpoint":
|
|
||||||
scanner = await self._service_registry.get_checkpoint_scanner()
|
|
||||||
normalized_type = "checkpoint"
|
|
||||||
elif model_type == "textualinversion":
|
|
||||||
scanner = await self._service_registry.get_embedding_scanner()
|
|
||||||
normalized_type = "embedding"
|
|
||||||
else:
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"success": False,
|
"success": False,
|
||||||
@@ -925,8 +1113,14 @@ class ModelLibraryHandler:
|
|||||||
status=503,
|
status=503,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
history_service = await self._get_download_history_service()
|
||||||
local_versions = await scanner.get_model_versions_by_id(model_id)
|
local_versions = await scanner.get_model_versions_by_id(model_id)
|
||||||
local_version_ids = {version["versionId"] for version in local_versions}
|
local_version_ids = {version["versionId"] for version in local_versions}
|
||||||
|
downloaded_version_ids = await history_service.get_downloaded_version_ids(
|
||||||
|
normalized_type,
|
||||||
|
model_id,
|
||||||
|
)
|
||||||
|
downloaded_version_id_set = set(downloaded_version_ids)
|
||||||
|
|
||||||
enriched_versions = []
|
enriched_versions = []
|
||||||
for version in versions:
|
for version in versions:
|
||||||
@@ -939,6 +1133,7 @@ class ModelLibraryHandler:
|
|||||||
if version.get("images")
|
if version.get("images")
|
||||||
else None,
|
else None,
|
||||||
"inLibrary": version_id in local_version_ids,
|
"inLibrary": version_id in local_version_ids,
|
||||||
|
"hasBeenDownloaded": version_id in downloaded_version_id_set,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1007,6 +1202,33 @@ class ModelLibraryHandler:
|
|||||||
}
|
}
|
||||||
|
|
||||||
versions: list[dict] = []
|
versions: list[dict] = []
|
||||||
|
history_service = await self._get_download_history_service()
|
||||||
|
model_ids: list[int] = []
|
||||||
|
for model in models:
|
||||||
|
try:
|
||||||
|
model_ids.append(int(model.get("id")))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
lora_downloaded = await history_service.get_downloaded_version_ids_bulk(
|
||||||
|
"lora",
|
||||||
|
model_ids,
|
||||||
|
)
|
||||||
|
checkpoint_downloaded = await history_service.get_downloaded_version_ids_bulk(
|
||||||
|
"checkpoint",
|
||||||
|
model_ids,
|
||||||
|
)
|
||||||
|
embedding_downloaded = await history_service.get_downloaded_version_ids_bulk(
|
||||||
|
"embedding",
|
||||||
|
model_ids,
|
||||||
|
)
|
||||||
|
downloaded_version_map: Dict[str, Dict[int, set[int]]] = {
|
||||||
|
"lora": lora_downloaded,
|
||||||
|
"locon": lora_downloaded,
|
||||||
|
"dora": lora_downloaded,
|
||||||
|
"checkpoint": checkpoint_downloaded,
|
||||||
|
"textualinversion": embedding_downloaded,
|
||||||
|
}
|
||||||
for model in models:
|
for model in models:
|
||||||
if not isinstance(model, dict):
|
if not isinstance(model, dict):
|
||||||
continue
|
continue
|
||||||
@@ -1061,6 +1283,8 @@ class ModelLibraryHandler:
|
|||||||
in_library = await scanner.check_model_version_exists(
|
in_library = await scanner.check_model_version_exists(
|
||||||
version_id_int
|
version_id_int
|
||||||
)
|
)
|
||||||
|
downloaded_versions = downloaded_version_map.get(model_type, {})
|
||||||
|
downloaded_version_ids = downloaded_versions.get(model_id_int, set())
|
||||||
|
|
||||||
versions.append(
|
versions.append(
|
||||||
{
|
{
|
||||||
@@ -1073,6 +1297,7 @@ class ModelLibraryHandler:
|
|||||||
"baseModel": version.get("baseModel"),
|
"baseModel": version.get("baseModel"),
|
||||||
"thumbnailUrl": thumbnail_url,
|
"thumbnailUrl": thumbnail_url,
|
||||||
"inLibrary": in_library,
|
"inLibrary": in_library,
|
||||||
|
"hasBeenDownloaded": version_id_int in downloaded_version_ids,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1655,6 +1880,8 @@ class MiscHandlerSet:
|
|||||||
"update_node_widget": self.node_registry.update_node_widget,
|
"update_node_widget": self.node_registry.update_node_widget,
|
||||||
"get_registry": self.node_registry.get_registry,
|
"get_registry": self.node_registry.get_registry,
|
||||||
"check_model_exists": self.model_library.check_model_exists,
|
"check_model_exists": self.model_library.check_model_exists,
|
||||||
|
"get_model_version_download_status": self.model_library.get_model_version_download_status,
|
||||||
|
"set_model_version_download_status": self.model_library.set_model_version_download_status,
|
||||||
"get_civitai_user_models": self.model_library.get_civitai_user_models,
|
"get_civitai_user_models": self.model_library.get_civitai_user_models,
|
||||||
"download_metadata_archive": self.metadata_archive.download_metadata_archive,
|
"download_metadata_archive": self.metadata_archive.download_metadata_archive,
|
||||||
"remove_metadata_archive": self.metadata_archive.remove_metadata_archive,
|
"remove_metadata_archive": self.metadata_archive.remove_metadata_archive,
|
||||||
@@ -1679,4 +1906,5 @@ def build_service_registry_adapter() -> ServiceRegistryAdapter:
|
|||||||
get_lora_scanner=ServiceRegistry.get_lora_scanner,
|
get_lora_scanner=ServiceRegistry.get_lora_scanner,
|
||||||
get_checkpoint_scanner=ServiceRegistry.get_checkpoint_scanner,
|
get_checkpoint_scanner=ServiceRegistry.get_checkpoint_scanner,
|
||||||
get_embedding_scanner=ServiceRegistry.get_embedding_scanner,
|
get_embedding_scanner=ServiceRegistry.get_embedding_scanner,
|
||||||
|
get_downloaded_version_history_service=ServiceRegistry.get_downloaded_version_history_service,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,6 +37,21 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
|||||||
RouteDefinition("POST", "/api/lm/update-node-widget", "update_node_widget"),
|
RouteDefinition("POST", "/api/lm/update-node-widget", "update_node_widget"),
|
||||||
RouteDefinition("GET", "/api/lm/get-registry", "get_registry"),
|
RouteDefinition("GET", "/api/lm/get-registry", "get_registry"),
|
||||||
RouteDefinition("GET", "/api/lm/check-model-exists", "check_model_exists"),
|
RouteDefinition("GET", "/api/lm/check-model-exists", "check_model_exists"),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET",
|
||||||
|
"/api/lm/model-version-download-status",
|
||||||
|
"get_model_version_download_status",
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"POST",
|
||||||
|
"/api/lm/model-version-download-status",
|
||||||
|
"set_model_version_download_status",
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET",
|
||||||
|
"/api/lm/set-model-version-download-status",
|
||||||
|
"set_model_version_download_status",
|
||||||
|
),
|
||||||
RouteDefinition("GET", "/api/lm/civitai/user-models", "get_civitai_user_models"),
|
RouteDefinition("GET", "/api/lm/civitai/user-models", "get_civitai_user_models"),
|
||||||
RouteDefinition(
|
RouteDefinition(
|
||||||
"POST", "/api/lm/download-metadata-archive", "download_metadata_archive"
|
"POST", "/api/lm/download-metadata-archive", "download_metadata_archive"
|
||||||
|
|||||||
@@ -64,6 +64,19 @@ class DownloadManager:
|
|||||||
"""Get the checkpoint scanner from registry"""
|
"""Get the checkpoint scanner from registry"""
|
||||||
return await ServiceRegistry.get_checkpoint_scanner()
|
return await ServiceRegistry.get_checkpoint_scanner()
|
||||||
|
|
||||||
|
async def _has_been_downloaded(self, model_type: str, model_version_id: int) -> bool:
|
||||||
|
try:
|
||||||
|
history_service = await ServiceRegistry.get_downloaded_version_history_service()
|
||||||
|
return await history_service.has_been_downloaded(model_type, model_version_id)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(
|
||||||
|
"Failed to read download history for %s version %s: %s",
|
||||||
|
model_type,
|
||||||
|
model_version_id,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
async def download_from_civitai(
|
async def download_from_civitai(
|
||||||
self,
|
self,
|
||||||
model_id: int = None,
|
model_id: int = None,
|
||||||
@@ -355,6 +368,57 @@ class DownloadManager:
|
|||||||
"error": f'Model type "{model_type_from_info}" is not supported for download',
|
"error": f'Model type "{model_type_from_info}" is not supported for download',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolved_version_id = model_version_id
|
||||||
|
raw_version_id = version_info.get("id")
|
||||||
|
if resolved_version_id is None and raw_version_id is not None:
|
||||||
|
try:
|
||||||
|
resolved_version_id = int(raw_version_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
resolved_version_id = None
|
||||||
|
|
||||||
|
if (
|
||||||
|
get_settings_manager().get_skip_previously_downloaded_model_versions()
|
||||||
|
and resolved_version_id is not None
|
||||||
|
and await self._has_been_downloaded(model_type, resolved_version_id)
|
||||||
|
):
|
||||||
|
file_name = ""
|
||||||
|
files = version_info.get("files")
|
||||||
|
if isinstance(files, list):
|
||||||
|
primary_file = next(
|
||||||
|
(
|
||||||
|
file_info
|
||||||
|
for file_info in files
|
||||||
|
if isinstance(file_info, dict) and file_info.get("primary")
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
selected_file = primary_file
|
||||||
|
if selected_file is None:
|
||||||
|
selected_file = next(
|
||||||
|
(file_info for file_info in files if isinstance(file_info, dict)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if isinstance(selected_file, dict):
|
||||||
|
raw_file_name = selected_file.get("name", "")
|
||||||
|
if isinstance(raw_file_name, str):
|
||||||
|
file_name = raw_file_name.strip()
|
||||||
|
|
||||||
|
message = (
|
||||||
|
f"Skipped download for '{file_name or version_info.get('name') or f'model_version:{resolved_version_id}'}' "
|
||||||
|
f"because version {resolved_version_id} was already downloaded before"
|
||||||
|
)
|
||||||
|
logger.info(message)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"skipped": True,
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "previously_downloaded_version",
|
||||||
|
"message": message,
|
||||||
|
"model_version_id": resolved_version_id,
|
||||||
|
"file_name": file_name,
|
||||||
|
"download_id": download_id,
|
||||||
|
}
|
||||||
|
|
||||||
excluded_base_models = get_settings_manager().get_download_skip_base_models()
|
excluded_base_models = get_settings_manager().get_download_skip_base_models()
|
||||||
base_model_value = version_info.get("baseModel", "")
|
base_model_value = version_info.get("baseModel", "")
|
||||||
if (
|
if (
|
||||||
@@ -640,6 +704,13 @@ class DownloadManager:
|
|||||||
or version_info.get("modelId")
|
or version_info.get("modelId")
|
||||||
or (version_info.get("model") or {}).get("id")
|
or (version_info.get("model") or {}).get("id")
|
||||||
)
|
)
|
||||||
|
await self._record_downloaded_version_history(
|
||||||
|
model_type,
|
||||||
|
resolved_model_id,
|
||||||
|
version_info,
|
||||||
|
model_version_id,
|
||||||
|
save_path,
|
||||||
|
)
|
||||||
await self._sync_downloaded_version(
|
await self._sync_downloaded_version(
|
||||||
model_type,
|
model_type,
|
||||||
resolved_model_id,
|
resolved_model_id,
|
||||||
@@ -669,6 +740,55 @@ class DownloadManager:
|
|||||||
}
|
}
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
async def _record_downloaded_version_history(
|
||||||
|
self,
|
||||||
|
model_type: str,
|
||||||
|
model_id_value,
|
||||||
|
version_info: Dict,
|
||||||
|
fallback_version_id=None,
|
||||||
|
file_path: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
history_service = await ServiceRegistry.get_downloaded_version_history_service()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(
|
||||||
|
"Skipping download history sync; failed to acquire history service: %s",
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if history_service is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
resolved_model_id = model_id_value
|
||||||
|
if resolved_model_id is None:
|
||||||
|
resolved_model_id = version_info.get("modelId")
|
||||||
|
if resolved_model_id is None:
|
||||||
|
model_info = version_info.get("model")
|
||||||
|
if isinstance(model_info, dict):
|
||||||
|
resolved_model_id = model_info.get("id")
|
||||||
|
|
||||||
|
version_id = version_info.get("id")
|
||||||
|
if version_id is None:
|
||||||
|
version_id = fallback_version_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
await history_service.mark_downloaded(
|
||||||
|
model_type,
|
||||||
|
int(version_id),
|
||||||
|
model_id=int(resolved_model_id) if resolved_model_id is not None else None,
|
||||||
|
source="download",
|
||||||
|
file_path=file_path,
|
||||||
|
)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
logger.debug(
|
||||||
|
"Skipping download history sync; invalid identifiers model=%s version=%s",
|
||||||
|
resolved_model_id,
|
||||||
|
version_id,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("Failed to sync download history for %s: %s", model_type, exc)
|
||||||
|
|
||||||
async def _sync_downloaded_version(
|
async def _sync_downloaded_version(
|
||||||
self,
|
self,
|
||||||
model_type: str,
|
model_type: str,
|
||||||
|
|||||||
313
py/services/downloaded_version_history_service.py
Normal file
313
py/services/downloaded_version_history_service.py
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
from typing import Iterable, Mapping, Optional, Sequence
|
||||||
|
|
||||||
|
from ..utils.cache_paths import get_cache_base_dir
|
||||||
|
from .settings_manager import get_settings_manager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_model_type(model_type: str | None) -> Optional[str]:
|
||||||
|
if not isinstance(model_type, str):
|
||||||
|
return None
|
||||||
|
normalized = model_type.strip().lower()
|
||||||
|
if normalized in {"lora", "locon", "dora"}:
|
||||||
|
return "lora"
|
||||||
|
if normalized == "checkpoint":
|
||||||
|
return "checkpoint"
|
||||||
|
if normalized in {"embedding", "textualinversion"}:
|
||||||
|
return "embedding"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_int(value) -> Optional[int]:
|
||||||
|
try:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_database_path() -> str:
|
||||||
|
base_dir = get_cache_base_dir(create=True)
|
||||||
|
history_dir = os.path.join(base_dir, "download_history")
|
||||||
|
os.makedirs(history_dir, exist_ok=True)
|
||||||
|
return os.path.join(history_dir, "downloaded_versions.sqlite")
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadedVersionHistoryService:
|
||||||
|
_SCHEMA = """
|
||||||
|
CREATE TABLE IF NOT EXISTS downloaded_model_versions (
|
||||||
|
model_type TEXT NOT NULL,
|
||||||
|
version_id INTEGER NOT NULL,
|
||||||
|
model_id INTEGER,
|
||||||
|
first_seen_at REAL NOT NULL,
|
||||||
|
last_seen_at REAL NOT NULL,
|
||||||
|
source TEXT NOT NULL,
|
||||||
|
last_file_path TEXT,
|
||||||
|
last_library_name TEXT,
|
||||||
|
is_deleted_override INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (model_type, version_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_downloaded_model_versions_model
|
||||||
|
ON downloaded_model_versions(model_type, model_id);
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, db_path: str | None = None, *, settings_manager=None) -> None:
|
||||||
|
self._db_path = db_path or _resolve_database_path()
|
||||||
|
self._settings = settings_manager or get_settings_manager()
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
self._schema_initialized = False
|
||||||
|
self._ensure_directory()
|
||||||
|
self._initialize_schema()
|
||||||
|
|
||||||
|
def _ensure_directory(self) -> None:
|
||||||
|
directory = os.path.dirname(self._db_path)
|
||||||
|
if directory:
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
|
||||||
|
def _connect(self) -> sqlite3.Connection:
|
||||||
|
conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def _initialize_schema(self) -> None:
|
||||||
|
if self._schema_initialized:
|
||||||
|
return
|
||||||
|
with self._connect() as conn:
|
||||||
|
conn.executescript(self._SCHEMA)
|
||||||
|
conn.commit()
|
||||||
|
self._schema_initialized = True
|
||||||
|
|
||||||
|
def get_database_path(self) -> str:
|
||||||
|
return self._db_path
|
||||||
|
|
||||||
|
def _get_active_library_name(self) -> str | None:
|
||||||
|
try:
|
||||||
|
value = self._settings.get_active_library_name()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return value or None
|
||||||
|
|
||||||
|
async def mark_downloaded(
|
||||||
|
self,
|
||||||
|
model_type: str,
|
||||||
|
version_id: int,
|
||||||
|
*,
|
||||||
|
model_id: int | None = None,
|
||||||
|
source: str = "manual",
|
||||||
|
file_path: str | None = None,
|
||||||
|
library_name: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
normalized_type = _normalize_model_type(model_type)
|
||||||
|
normalized_version_id = _normalize_int(version_id)
|
||||||
|
normalized_model_id = _normalize_int(model_id)
|
||||||
|
if normalized_type is None or normalized_version_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
active_library_name = library_name or self._get_active_library_name()
|
||||||
|
timestamp = time.time()
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
with self._connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO downloaded_model_versions (
|
||||||
|
model_type, version_id, model_id, first_seen_at, last_seen_at,
|
||||||
|
source, last_file_path, last_library_name, is_deleted_override
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
|
||||||
|
ON CONFLICT(model_type, version_id) DO UPDATE SET
|
||||||
|
model_id = COALESCE(excluded.model_id, downloaded_model_versions.model_id),
|
||||||
|
last_seen_at = excluded.last_seen_at,
|
||||||
|
source = excluded.source,
|
||||||
|
last_file_path = COALESCE(excluded.last_file_path, downloaded_model_versions.last_file_path),
|
||||||
|
last_library_name = COALESCE(excluded.last_library_name, downloaded_model_versions.last_library_name),
|
||||||
|
is_deleted_override = 0
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
normalized_type,
|
||||||
|
normalized_version_id,
|
||||||
|
normalized_model_id,
|
||||||
|
timestamp,
|
||||||
|
timestamp,
|
||||||
|
source,
|
||||||
|
file_path,
|
||||||
|
active_library_name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
async def mark_downloaded_bulk(
|
||||||
|
self,
|
||||||
|
model_type: str,
|
||||||
|
records: Sequence[Mapping[str, object]],
|
||||||
|
*,
|
||||||
|
source: str = "scan",
|
||||||
|
library_name: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
normalized_type = _normalize_model_type(model_type)
|
||||||
|
if normalized_type is None or not records:
|
||||||
|
return
|
||||||
|
|
||||||
|
timestamp = time.time()
|
||||||
|
active_library_name = library_name or self._get_active_library_name()
|
||||||
|
payload: list[tuple[object, ...]] = []
|
||||||
|
for record in records:
|
||||||
|
version_id = _normalize_int(record.get("version_id"))
|
||||||
|
if version_id is None:
|
||||||
|
continue
|
||||||
|
payload.append(
|
||||||
|
(
|
||||||
|
normalized_type,
|
||||||
|
version_id,
|
||||||
|
_normalize_int(record.get("model_id")),
|
||||||
|
timestamp,
|
||||||
|
timestamp,
|
||||||
|
source,
|
||||||
|
record.get("file_path"),
|
||||||
|
active_library_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not payload:
|
||||||
|
return
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
with self._connect() as conn:
|
||||||
|
conn.executemany(
|
||||||
|
"""
|
||||||
|
INSERT INTO downloaded_model_versions (
|
||||||
|
model_type, version_id, model_id, first_seen_at, last_seen_at,
|
||||||
|
source, last_file_path, last_library_name, is_deleted_override
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
|
||||||
|
ON CONFLICT(model_type, version_id) DO UPDATE SET
|
||||||
|
model_id = COALESCE(excluded.model_id, downloaded_model_versions.model_id),
|
||||||
|
last_seen_at = excluded.last_seen_at,
|
||||||
|
source = excluded.source,
|
||||||
|
last_file_path = COALESCE(excluded.last_file_path, downloaded_model_versions.last_file_path),
|
||||||
|
last_library_name = COALESCE(excluded.last_library_name, downloaded_model_versions.last_library_name),
|
||||||
|
is_deleted_override = 0
|
||||||
|
""",
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
async def mark_not_downloaded(self, model_type: str, version_id: int) -> None:
|
||||||
|
normalized_type = _normalize_model_type(model_type)
|
||||||
|
normalized_version_id = _normalize_int(version_id)
|
||||||
|
if normalized_type is None or normalized_version_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
timestamp = time.time()
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
with self._connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO downloaded_model_versions (
|
||||||
|
model_type, version_id, model_id, first_seen_at, last_seen_at,
|
||||||
|
source, last_file_path, last_library_name, is_deleted_override
|
||||||
|
) VALUES (?, ?, NULL, ?, ?, 'manual', NULL, ?, 1)
|
||||||
|
ON CONFLICT(model_type, version_id) DO UPDATE SET
|
||||||
|
last_seen_at = excluded.last_seen_at,
|
||||||
|
source = excluded.source,
|
||||||
|
last_library_name = COALESCE(excluded.last_library_name, downloaded_model_versions.last_library_name),
|
||||||
|
is_deleted_override = 1
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
normalized_type,
|
||||||
|
normalized_version_id,
|
||||||
|
timestamp,
|
||||||
|
timestamp,
|
||||||
|
self._get_active_library_name(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
async def has_been_downloaded(self, model_type: str, version_id: int) -> bool:
|
||||||
|
normalized_type = _normalize_model_type(model_type)
|
||||||
|
normalized_version_id = _normalize_int(version_id)
|
||||||
|
if normalized_type is None or normalized_version_id is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
with self._connect() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT is_deleted_override
|
||||||
|
FROM downloaded_model_versions
|
||||||
|
WHERE model_type = ? AND version_id = ?
|
||||||
|
""",
|
||||||
|
(normalized_type, normalized_version_id),
|
||||||
|
).fetchone()
|
||||||
|
return bool(row) and not bool(row["is_deleted_override"])
|
||||||
|
|
||||||
|
async def get_downloaded_version_ids(
|
||||||
|
self, model_type: str, model_id: int
|
||||||
|
) -> list[int]:
|
||||||
|
normalized_type = _normalize_model_type(model_type)
|
||||||
|
normalized_model_id = _normalize_int(model_id)
|
||||||
|
if normalized_type is None or normalized_model_id is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
with self._connect() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT version_id
|
||||||
|
FROM downloaded_model_versions
|
||||||
|
WHERE model_type = ? AND model_id = ? AND is_deleted_override = 0
|
||||||
|
ORDER BY version_id ASC
|
||||||
|
""",
|
||||||
|
(normalized_type, normalized_model_id),
|
||||||
|
).fetchall()
|
||||||
|
return [int(row["version_id"]) for row in rows]
|
||||||
|
|
||||||
|
async def get_downloaded_version_ids_bulk(
|
||||||
|
self, model_type: str, model_ids: Iterable[int]
|
||||||
|
) -> dict[int, set[int]]:
|
||||||
|
normalized_type = _normalize_model_type(model_type)
|
||||||
|
if normalized_type is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
normalized_model_ids = sorted(
|
||||||
|
{
|
||||||
|
value
|
||||||
|
for value in (_normalize_int(model_id) for model_id in model_ids)
|
||||||
|
if value is not None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not normalized_model_ids:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
placeholders = ", ".join(["?"] * len(normalized_model_ids))
|
||||||
|
params: list[object] = [normalized_type, *normalized_model_ids]
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
with self._connect() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
f"""
|
||||||
|
SELECT model_id, version_id
|
||||||
|
FROM downloaded_model_versions
|
||||||
|
WHERE model_type = ?
|
||||||
|
AND model_id IN ({placeholders})
|
||||||
|
AND is_deleted_override = 0
|
||||||
|
""",
|
||||||
|
params,
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
result: dict[int, set[int]] = {}
|
||||||
|
for row in rows:
|
||||||
|
model_id = _normalize_int(row["model_id"])
|
||||||
|
version_id = _normalize_int(row["version_id"])
|
||||||
|
if model_id is None or version_id is None:
|
||||||
|
continue
|
||||||
|
result.setdefault(model_id, set()).add(version_id)
|
||||||
|
return result
|
||||||
@@ -411,6 +411,7 @@ class ModelScanner:
|
|||||||
if scan_result:
|
if scan_result:
|
||||||
await self._apply_scan_result(scan_result)
|
await self._apply_scan_result(scan_result)
|
||||||
await self._save_persistent_cache(scan_result)
|
await self._save_persistent_cache(scan_result)
|
||||||
|
await self._sync_download_history(scan_result.raw_data, source='scan')
|
||||||
|
|
||||||
# Send final progress update
|
# Send final progress update
|
||||||
await ws_manager.broadcast_init_progress({
|
await ws_manager.broadcast_init_progress({
|
||||||
@@ -516,6 +517,7 @@ class ModelScanner:
|
|||||||
)
|
)
|
||||||
|
|
||||||
await self._apply_scan_result(scan_result)
|
await self._apply_scan_result(scan_result)
|
||||||
|
await self._sync_download_history(adjusted_raw_data, source='scan')
|
||||||
|
|
||||||
await ws_manager.broadcast_init_progress({
|
await ws_manager.broadcast_init_progress({
|
||||||
'stage': 'loading_cache',
|
'stage': 'loading_cache',
|
||||||
@@ -576,6 +578,7 @@ class ModelScanner:
|
|||||||
excluded_models=list(self._excluded_models)
|
excluded_models=list(self._excluded_models)
|
||||||
)
|
)
|
||||||
await self._save_persistent_cache(snapshot)
|
await self._save_persistent_cache(snapshot)
|
||||||
|
await self._sync_download_history(snapshot.raw_data, source='scan')
|
||||||
def _count_model_files(self) -> int:
|
def _count_model_files(self) -> int:
|
||||||
"""Count all model files with supported extensions in all roots
|
"""Count all model files with supported extensions in all roots
|
||||||
|
|
||||||
@@ -704,6 +707,7 @@ class ModelScanner:
|
|||||||
scan_result = await self._gather_model_data()
|
scan_result = await self._gather_model_data()
|
||||||
await self._apply_scan_result(scan_result)
|
await self._apply_scan_result(scan_result)
|
||||||
await self._save_persistent_cache(scan_result)
|
await self._save_persistent_cache(scan_result)
|
||||||
|
await self._sync_download_history(scan_result.raw_data, source='scan')
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{self.model_type.capitalize()} Scanner: Cache initialization completed in {time.time() - start_time:.2f} seconds, "
|
f"{self.model_type.capitalize()} Scanner: Cache initialization completed in {time.time() - start_time:.2f} seconds, "
|
||||||
@@ -1101,6 +1105,49 @@ class ModelScanner:
|
|||||||
|
|
||||||
await self._cache.resort()
|
await self._cache.resort()
|
||||||
|
|
||||||
|
async def _sync_download_history(
|
||||||
|
self,
|
||||||
|
raw_data: List[Mapping[str, Any]],
|
||||||
|
*,
|
||||||
|
source: str,
|
||||||
|
) -> None:
|
||||||
|
records: List[Dict[str, Any]] = []
|
||||||
|
for item in raw_data or []:
|
||||||
|
if not isinstance(item, Mapping):
|
||||||
|
continue
|
||||||
|
civitai = item.get('civitai')
|
||||||
|
if not isinstance(civitai, Mapping):
|
||||||
|
continue
|
||||||
|
|
||||||
|
version_id = civitai.get('id')
|
||||||
|
if version_id in (None, ''):
|
||||||
|
continue
|
||||||
|
|
||||||
|
records.append(
|
||||||
|
{
|
||||||
|
'version_id': version_id,
|
||||||
|
'model_id': civitai.get('modelId'),
|
||||||
|
'file_path': item.get('file_path'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
history_service = await ServiceRegistry.get_downloaded_version_history_service()
|
||||||
|
await history_service.mark_downloaded_bulk(
|
||||||
|
self.model_type,
|
||||||
|
records,
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(
|
||||||
|
"%s Scanner: Failed to sync download history: %s",
|
||||||
|
self.model_type.capitalize(),
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
|
||||||
async def _gather_model_data(
|
async def _gather_model_data(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -167,6 +167,28 @@ class ServiceRegistry:
|
|||||||
logger.debug(f"Created and registered {service_name}")
|
logger.debug(f"Created and registered {service_name}")
|
||||||
return service
|
return service
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_downloaded_version_history_service(cls):
|
||||||
|
"""Get or create the downloaded-version history service."""
|
||||||
|
|
||||||
|
service_name = "downloaded_version_history_service"
|
||||||
|
|
||||||
|
if service_name in cls._services:
|
||||||
|
return cls._services[service_name]
|
||||||
|
|
||||||
|
async with cls._get_lock(service_name):
|
||||||
|
if service_name in cls._services:
|
||||||
|
return cls._services[service_name]
|
||||||
|
|
||||||
|
from .downloaded_version_history_service import (
|
||||||
|
DownloadedVersionHistoryService,
|
||||||
|
)
|
||||||
|
|
||||||
|
service = DownloadedVersionHistoryService()
|
||||||
|
cls._services[service_name] = service
|
||||||
|
logger.debug(f"Created and registered {service_name}")
|
||||||
|
return service
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_civarchive_client(cls):
|
async def get_civarchive_client(cls):
|
||||||
"""Get or create CivArchive client instance"""
|
"""Get or create CivArchive client instance"""
|
||||||
|
|||||||
@@ -7,7 +7,17 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Awaitable, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Awaitable,
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Mapping,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Tuple,
|
||||||
|
)
|
||||||
|
|
||||||
from platformdirs import user_config_dir
|
from platformdirs import user_config_dir
|
||||||
|
|
||||||
@@ -17,7 +27,11 @@ from ..utils.constants import (
|
|||||||
SUPPORTED_DOWNLOAD_SKIP_BASE_MODELS,
|
SUPPORTED_DOWNLOAD_SKIP_BASE_MODELS,
|
||||||
)
|
)
|
||||||
from ..utils.preview_selection import VALID_MATURE_BLUR_LEVELS
|
from ..utils.preview_selection import VALID_MATURE_BLUR_LEVELS
|
||||||
from ..utils.settings_paths import APP_NAME, ensure_settings_file, get_legacy_settings_path
|
from ..utils.settings_paths import (
|
||||||
|
APP_NAME,
|
||||||
|
ensure_settings_file,
|
||||||
|
get_legacy_settings_path,
|
||||||
|
)
|
||||||
from ..utils.tag_priorities import (
|
from ..utils.tag_priorities import (
|
||||||
PriorityTagEntry,
|
PriorityTagEntry,
|
||||||
collect_canonical_tags,
|
collect_canonical_tags,
|
||||||
@@ -77,6 +91,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
|
|||||||
"update_flag_strategy": "same_base",
|
"update_flag_strategy": "same_base",
|
||||||
"auto_organize_exclusions": [],
|
"auto_organize_exclusions": [],
|
||||||
"metadata_refresh_skip_paths": [],
|
"metadata_refresh_skip_paths": [],
|
||||||
|
"skip_previously_downloaded_model_versions": False,
|
||||||
"download_skip_base_models": [],
|
"download_skip_base_models": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +109,9 @@ class SettingsManager:
|
|||||||
self._template_payload_cache_loaded = False
|
self._template_payload_cache_loaded = False
|
||||||
self._original_disk_payload: Optional[Dict[str, Any]] = None
|
self._original_disk_payload: Optional[Dict[str, Any]] = None
|
||||||
self._preserve_disk_template = False
|
self._preserve_disk_template = False
|
||||||
self._template_path = Path(__file__).resolve().parents[2] / "settings.json.example"
|
self._template_path = (
|
||||||
|
Path(__file__).resolve().parents[2] / "settings.json.example"
|
||||||
|
)
|
||||||
self.settings = self._load_settings()
|
self.settings = self._load_settings()
|
||||||
self._migrate_setting_keys()
|
self._migrate_setting_keys()
|
||||||
self._ensure_default_settings()
|
self._ensure_default_settings()
|
||||||
@@ -120,7 +137,7 @@ class SettingsManager:
|
|||||||
"""Load settings from file"""
|
"""Load settings from file"""
|
||||||
if os.path.exists(self.settings_file):
|
if os.path.exists(self.settings_file):
|
||||||
try:
|
try:
|
||||||
with open(self.settings_file, 'r', encoding='utf-8') as f:
|
with open(self.settings_file, "r", encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
self._original_disk_payload = copy.deepcopy(data)
|
self._original_disk_payload = copy.deepcopy(data)
|
||||||
@@ -198,7 +215,9 @@ class SettingsManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
logger.debug("settings.json.example is not a JSON object; ignoring template")
|
logger.debug(
|
||||||
|
"settings.json.example is not a JSON object; ignoring template"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self._template_payload_cache = copy.deepcopy(data)
|
self._template_payload_cache = copy.deepcopy(data)
|
||||||
@@ -274,7 +293,9 @@ class SettingsManager:
|
|||||||
normalized_skip_paths = self.normalize_metadata_refresh_skip_paths(
|
normalized_skip_paths = self.normalize_metadata_refresh_skip_paths(
|
||||||
self.settings.get("metadata_refresh_skip_paths")
|
self.settings.get("metadata_refresh_skip_paths")
|
||||||
)
|
)
|
||||||
if normalized_skip_paths != self.settings.get("metadata_refresh_skip_paths"):
|
if normalized_skip_paths != self.settings.get(
|
||||||
|
"metadata_refresh_skip_paths"
|
||||||
|
):
|
||||||
self.settings["metadata_refresh_skip_paths"] = normalized_skip_paths
|
self.settings["metadata_refresh_skip_paths"] = normalized_skip_paths
|
||||||
updated_existing = True
|
updated_existing = True
|
||||||
else:
|
else:
|
||||||
@@ -288,14 +309,16 @@ class SettingsManager:
|
|||||||
if normalized_skip_base_models != self.settings.get(
|
if normalized_skip_base_models != self.settings.get(
|
||||||
"download_skip_base_models"
|
"download_skip_base_models"
|
||||||
):
|
):
|
||||||
self.settings["download_skip_base_models"] = (
|
self.settings["download_skip_base_models"] = normalized_skip_base_models
|
||||||
normalized_skip_base_models
|
|
||||||
)
|
|
||||||
updated_existing = True
|
updated_existing = True
|
||||||
else:
|
else:
|
||||||
self.settings["download_skip_base_models"] = []
|
self.settings["download_skip_base_models"] = []
|
||||||
inserted_defaults = True
|
inserted_defaults = True
|
||||||
|
|
||||||
|
if "skip_previously_downloaded_model_versions" not in self.settings:
|
||||||
|
self.settings["skip_previously_downloaded_model_versions"] = False
|
||||||
|
inserted_defaults = True
|
||||||
|
|
||||||
had_mature_level = "mature_blur_level" in self.settings
|
had_mature_level = "mature_blur_level" in self.settings
|
||||||
raw_mature_level = self.settings.get("mature_blur_level")
|
raw_mature_level = self.settings.get("mature_blur_level")
|
||||||
normalized_mature_level = self.normalize_mature_blur_level(raw_mature_level)
|
normalized_mature_level = self.normalize_mature_blur_level(raw_mature_level)
|
||||||
@@ -330,19 +353,19 @@ class SettingsManager:
|
|||||||
raw_top_level_paths = self.settings.get("folder_paths", {})
|
raw_top_level_paths = self.settings.get("folder_paths", {})
|
||||||
normalized_top_level_paths: Dict[str, List[str]] = {}
|
normalized_top_level_paths: Dict[str, List[str]] = {}
|
||||||
if isinstance(raw_top_level_paths, Mapping):
|
if isinstance(raw_top_level_paths, Mapping):
|
||||||
normalized_top_level_paths = self._normalize_folder_paths(raw_top_level_paths)
|
normalized_top_level_paths = self._normalize_folder_paths(
|
||||||
|
raw_top_level_paths
|
||||||
|
)
|
||||||
if normalized_top_level_paths != raw_top_level_paths:
|
if normalized_top_level_paths != raw_top_level_paths:
|
||||||
self.settings["folder_paths"] = copy.deepcopy(normalized_top_level_paths)
|
self.settings["folder_paths"] = copy.deepcopy(
|
||||||
|
normalized_top_level_paths
|
||||||
|
)
|
||||||
|
|
||||||
top_level_has_paths = self._has_configured_paths(normalized_top_level_paths)
|
top_level_has_paths = self._has_configured_paths(normalized_top_level_paths)
|
||||||
|
|
||||||
needs_library_bootstrap = not isinstance(libraries, dict) or not libraries
|
needs_library_bootstrap = not isinstance(libraries, dict) or not libraries
|
||||||
|
|
||||||
if (
|
if not needs_library_bootstrap and top_level_has_paths and len(libraries) == 1:
|
||||||
not needs_library_bootstrap
|
|
||||||
and top_level_has_paths
|
|
||||||
and len(libraries) == 1
|
|
||||||
):
|
|
||||||
only_library_payload = next(iter(libraries.values()))
|
only_library_payload = next(iter(libraries.values()))
|
||||||
if isinstance(only_library_payload, Mapping):
|
if isinstance(only_library_payload, Mapping):
|
||||||
folder_payload = only_library_payload.get("folder_paths")
|
folder_payload = only_library_payload.get("folder_paths")
|
||||||
@@ -354,7 +377,9 @@ class SettingsManager:
|
|||||||
library_payload = self._build_library_payload(
|
library_payload = self._build_library_payload(
|
||||||
folder_paths=normalized_top_level_paths,
|
folder_paths=normalized_top_level_paths,
|
||||||
default_lora_root=self.settings.get("default_lora_root", ""),
|
default_lora_root=self.settings.get("default_lora_root", ""),
|
||||||
default_checkpoint_root=self.settings.get("default_checkpoint_root", ""),
|
default_checkpoint_root=self.settings.get(
|
||||||
|
"default_checkpoint_root", ""
|
||||||
|
),
|
||||||
default_unet_root=self.settings.get("default_unet_root", ""),
|
default_unet_root=self.settings.get("default_unet_root", ""),
|
||||||
default_embedding_root=self.settings.get("default_embedding_root", ""),
|
default_embedding_root=self.settings.get("default_embedding_root", ""),
|
||||||
)
|
)
|
||||||
@@ -376,7 +401,11 @@ class SettingsManager:
|
|||||||
|
|
||||||
if target_name:
|
if target_name:
|
||||||
candidate_payload = libraries.get(target_name)
|
candidate_payload = libraries.get(target_name)
|
||||||
if isinstance(candidate_payload, Mapping) and not self._has_configured_paths(candidate_payload.get("folder_paths")):
|
if isinstance(
|
||||||
|
candidate_payload, Mapping
|
||||||
|
) and not self._has_configured_paths(
|
||||||
|
candidate_payload.get("folder_paths")
|
||||||
|
):
|
||||||
seed_library_name = target_name
|
seed_library_name = target_name
|
||||||
|
|
||||||
sanitized_libraries: Dict[str, Dict[str, Any]] = {}
|
sanitized_libraries: Dict[str, Dict[str, Any]] = {}
|
||||||
@@ -435,11 +464,17 @@ class SettingsManager:
|
|||||||
active_library = libraries.get(active_name, {})
|
active_library = libraries.get(active_name, {})
|
||||||
folder_paths = copy.deepcopy(active_library.get("folder_paths", {}))
|
folder_paths = copy.deepcopy(active_library.get("folder_paths", {}))
|
||||||
self.settings["folder_paths"] = folder_paths
|
self.settings["folder_paths"] = folder_paths
|
||||||
self.settings["extra_folder_paths"] = copy.deepcopy(active_library.get("extra_folder_paths", {}))
|
self.settings["extra_folder_paths"] = copy.deepcopy(
|
||||||
|
active_library.get("extra_folder_paths", {})
|
||||||
|
)
|
||||||
self.settings["default_lora_root"] = active_library.get("default_lora_root", "")
|
self.settings["default_lora_root"] = active_library.get("default_lora_root", "")
|
||||||
self.settings["default_checkpoint_root"] = active_library.get("default_checkpoint_root", "")
|
self.settings["default_checkpoint_root"] = active_library.get(
|
||||||
|
"default_checkpoint_root", ""
|
||||||
|
)
|
||||||
self.settings["default_unet_root"] = active_library.get("default_unet_root", "")
|
self.settings["default_unet_root"] = active_library.get("default_unet_root", "")
|
||||||
self.settings["default_embedding_root"] = active_library.get("default_embedding_root", "")
|
self.settings["default_embedding_root"] = active_library.get(
|
||||||
|
"default_embedding_root", ""
|
||||||
|
)
|
||||||
|
|
||||||
if save:
|
if save:
|
||||||
self._save_settings()
|
self._save_settings()
|
||||||
@@ -468,7 +503,9 @@ class SettingsManager:
|
|||||||
payload.setdefault("folder_paths", {})
|
payload.setdefault("folder_paths", {})
|
||||||
|
|
||||||
if extra_folder_paths is not None:
|
if extra_folder_paths is not None:
|
||||||
payload["extra_folder_paths"] = self._normalize_folder_paths(extra_folder_paths)
|
payload["extra_folder_paths"] = self._normalize_folder_paths(
|
||||||
|
extra_folder_paths
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
payload.setdefault("extra_folder_paths", {})
|
payload.setdefault("extra_folder_paths", {})
|
||||||
|
|
||||||
@@ -577,7 +614,9 @@ class SettingsManager:
|
|||||||
}
|
}
|
||||||
overlap = existing.intersection(new_paths.keys())
|
overlap = existing.intersection(new_paths.keys())
|
||||||
if overlap:
|
if overlap:
|
||||||
collisions = ", ".join(sorted(new_paths[value] for value in overlap))
|
collisions = ", ".join(
|
||||||
|
sorted(new_paths[value] for value in overlap)
|
||||||
|
)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Folder path(s) {collisions} already assigned to library '{other_name}'"
|
f"Folder path(s) {collisions} already assigned to library '{other_name}'"
|
||||||
)
|
)
|
||||||
@@ -612,19 +651,31 @@ class SettingsManager:
|
|||||||
library["extra_folder_paths"] = normalized_extra_paths
|
library["extra_folder_paths"] = normalized_extra_paths
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
if default_lora_root is not None and library.get("default_lora_root") != default_lora_root:
|
if (
|
||||||
|
default_lora_root is not None
|
||||||
|
and library.get("default_lora_root") != default_lora_root
|
||||||
|
):
|
||||||
library["default_lora_root"] = default_lora_root
|
library["default_lora_root"] = default_lora_root
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
if default_checkpoint_root is not None and library.get("default_checkpoint_root") != default_checkpoint_root:
|
if (
|
||||||
|
default_checkpoint_root is not None
|
||||||
|
and library.get("default_checkpoint_root") != default_checkpoint_root
|
||||||
|
):
|
||||||
library["default_checkpoint_root"] = default_checkpoint_root
|
library["default_checkpoint_root"] = default_checkpoint_root
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
if default_unet_root is not None and library.get("default_unet_root") != default_unet_root:
|
if (
|
||||||
|
default_unet_root is not None
|
||||||
|
and library.get("default_unet_root") != default_unet_root
|
||||||
|
):
|
||||||
library["default_unet_root"] = default_unet_root
|
library["default_unet_root"] = default_unet_root
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
if default_embedding_root is not None and library.get("default_embedding_root") != default_embedding_root:
|
if (
|
||||||
|
default_embedding_root is not None
|
||||||
|
and library.get("default_embedding_root") != default_embedding_root
|
||||||
|
):
|
||||||
library["default_embedding_root"] = default_embedding_root
|
library["default_embedding_root"] = default_embedding_root
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
@@ -637,16 +688,16 @@ class SettingsManager:
|
|||||||
def _migrate_setting_keys(self) -> None:
|
def _migrate_setting_keys(self) -> None:
|
||||||
"""Migrate legacy camelCase setting keys to snake_case"""
|
"""Migrate legacy camelCase setting keys to snake_case"""
|
||||||
key_migrations = {
|
key_migrations = {
|
||||||
'optimizeExampleImages': 'optimize_example_images',
|
"optimizeExampleImages": "optimize_example_images",
|
||||||
'autoDownloadExampleImages': 'auto_download_example_images',
|
"autoDownloadExampleImages": "auto_download_example_images",
|
||||||
'blurMatureContent': 'blur_mature_content',
|
"blurMatureContent": "blur_mature_content",
|
||||||
'matureBlurLevel': 'mature_blur_level',
|
"matureBlurLevel": "mature_blur_level",
|
||||||
'autoplayOnHover': 'autoplay_on_hover',
|
"autoplayOnHover": "autoplay_on_hover",
|
||||||
'displayDensity': 'display_density',
|
"displayDensity": "display_density",
|
||||||
'cardInfoDisplay': 'card_info_display',
|
"cardInfoDisplay": "card_info_display",
|
||||||
'includeTriggerWords': 'include_trigger_words',
|
"includeTriggerWords": "include_trigger_words",
|
||||||
'compactMode': 'compact_mode',
|
"compactMode": "compact_mode",
|
||||||
'modelCardFooterAction': 'model_card_footer_action',
|
"modelCardFooterAction": "model_card_footer_action",
|
||||||
}
|
}
|
||||||
|
|
||||||
updated = False
|
updated = False
|
||||||
@@ -663,65 +714,77 @@ class SettingsManager:
|
|||||||
|
|
||||||
def _migrate_download_path_template(self):
|
def _migrate_download_path_template(self):
|
||||||
"""Migrate old download_path_template to new download_path_templates"""
|
"""Migrate old download_path_template to new download_path_templates"""
|
||||||
old_template = self.settings.get('download_path_template')
|
old_template = self.settings.get("download_path_template")
|
||||||
templates = self.settings.get('download_path_templates')
|
templates = self.settings.get("download_path_templates")
|
||||||
|
|
||||||
# If old template exists and new templates don't exist, migrate
|
# If old template exists and new templates don't exist, migrate
|
||||||
if old_template is not None and not templates:
|
if old_template is not None and not templates:
|
||||||
logger.info("Migrating download_path_template to download_path_templates")
|
logger.info("Migrating download_path_template to download_path_templates")
|
||||||
self.settings['download_path_templates'] = {
|
self.settings["download_path_templates"] = {
|
||||||
'lora': old_template,
|
"lora": old_template,
|
||||||
'checkpoint': old_template,
|
"checkpoint": old_template,
|
||||||
'embedding': old_template
|
"embedding": old_template,
|
||||||
}
|
}
|
||||||
# Remove old setting
|
# Remove old setting
|
||||||
del self.settings['download_path_template']
|
del self.settings["download_path_template"]
|
||||||
self._save_settings()
|
self._save_settings()
|
||||||
logger.info("Migration completed")
|
logger.info("Migration completed")
|
||||||
|
|
||||||
def _auto_set_default_roots(self):
|
def _auto_set_default_roots(self):
|
||||||
"""Auto set default root paths when the current default is unset or not among the options.
|
"""Ensure default root paths always point at a current valid root.
|
||||||
|
|
||||||
For single-path cases, always use that path.
|
Empty or stale defaults are repaired to the first configured root.
|
||||||
For multi-path cases, only set if current default is empty or invalid.
|
Skips auto-setting when the settings file matches the template
|
||||||
|
(user hasn't customized yet).
|
||||||
"""
|
"""
|
||||||
folder_paths = self.settings.get('folder_paths', {})
|
# Skip auto-setting if the user hasn't customized settings yet (template preserved)
|
||||||
|
if self._preserve_disk_template:
|
||||||
|
return
|
||||||
|
|
||||||
|
folder_paths = self.settings.get("folder_paths", {})
|
||||||
updated = False
|
updated = False
|
||||||
# loras
|
|
||||||
loras = folder_paths.get('loras', [])
|
def _check_and_auto_set(key: str, setting_key: str) -> bool:
|
||||||
if isinstance(loras, list) and len(loras) == 1:
|
"""Repair default roots when empty or no longer present."""
|
||||||
current_lora_root = self.settings.get('default_lora_root')
|
current = self.settings.get(setting_key, "")
|
||||||
if current_lora_root not in loras:
|
candidates = folder_paths.get(key, [])
|
||||||
self.settings['default_lora_root'] = loras[0]
|
if not isinstance(candidates, list) or not candidates:
|
||||||
updated = True
|
return False
|
||||||
# checkpoints
|
|
||||||
checkpoints = folder_paths.get('checkpoints', [])
|
# Filter valid path strings
|
||||||
if isinstance(checkpoints, list) and len(checkpoints) == 1:
|
valid_paths = [p for p in candidates if isinstance(p, str) and p.strip()]
|
||||||
current_checkpoint_root = self.settings.get('default_checkpoint_root')
|
if not valid_paths:
|
||||||
if current_checkpoint_root not in checkpoints:
|
return False
|
||||||
self.settings['default_checkpoint_root'] = checkpoints[0]
|
|
||||||
updated = True
|
if current in valid_paths:
|
||||||
# unet (diffusion models) - auto-set if empty or invalid
|
return False
|
||||||
unet_paths = folder_paths.get('unet', [])
|
|
||||||
if isinstance(unet_paths, list) and len(unet_paths) >= 1:
|
self.settings[setting_key] = valid_paths[0]
|
||||||
current_unet_root = self.settings.get('default_unet_root')
|
if current:
|
||||||
# Set to first path if current is empty or not in the valid paths
|
logger.info(
|
||||||
if not current_unet_root or current_unet_root not in unet_paths:
|
"Repaired stale %s from '%s' to '%s'",
|
||||||
self.settings['default_unet_root'] = unet_paths[0]
|
setting_key,
|
||||||
updated = True
|
current,
|
||||||
# embeddings
|
valid_paths[0],
|
||||||
embeddings = folder_paths.get('embeddings', [])
|
)
|
||||||
if isinstance(embeddings, list) and len(embeddings) == 1:
|
else:
|
||||||
current_embedding_root = self.settings.get('default_embedding_root')
|
logger.info("Auto-set %s to '%s'", setting_key, valid_paths[0])
|
||||||
if current_embedding_root not in embeddings:
|
return True
|
||||||
self.settings['default_embedding_root'] = embeddings[0]
|
|
||||||
updated = True
|
# Process all model types
|
||||||
|
updated = _check_and_auto_set("loras", "default_lora_root") or updated
|
||||||
|
updated = (
|
||||||
|
_check_and_auto_set("checkpoints", "default_checkpoint_root") or updated
|
||||||
|
)
|
||||||
|
updated = _check_and_auto_set("unet", "default_unet_root") or updated
|
||||||
|
updated = _check_and_auto_set("embeddings", "default_embedding_root") or updated
|
||||||
|
|
||||||
if updated:
|
if updated:
|
||||||
self._update_active_library_entry(
|
self._update_active_library_entry(
|
||||||
default_lora_root=self.settings.get('default_lora_root'),
|
default_lora_root=self.settings.get("default_lora_root"),
|
||||||
default_checkpoint_root=self.settings.get('default_checkpoint_root'),
|
default_checkpoint_root=self.settings.get("default_checkpoint_root"),
|
||||||
default_unet_root=self.settings.get('default_unet_root'),
|
default_unet_root=self.settings.get("default_unet_root"),
|
||||||
default_embedding_root=self.settings.get('default_embedding_root'),
|
default_embedding_root=self.settings.get("default_embedding_root"),
|
||||||
)
|
)
|
||||||
if self._bootstrap_reason == "missing":
|
if self._bootstrap_reason == "missing":
|
||||||
self._needs_initial_save = True
|
self._needs_initial_save = True
|
||||||
@@ -730,11 +793,11 @@ class SettingsManager:
|
|||||||
|
|
||||||
def _check_environment_variables(self) -> None:
|
def _check_environment_variables(self) -> None:
|
||||||
"""Check for environment variables and update settings if needed"""
|
"""Check for environment variables and update settings if needed"""
|
||||||
env_api_key = os.environ.get('CIVITAI_API_KEY')
|
env_api_key = os.environ.get("CIVITAI_API_KEY")
|
||||||
if env_api_key: # Check if the environment variable exists and is not empty
|
if env_api_key: # Check if the environment variable exists and is not empty
|
||||||
logger.info("Found CIVITAI_API_KEY environment variable")
|
logger.info("Found CIVITAI_API_KEY environment variable")
|
||||||
# Always use the environment variable if it exists
|
# Always use the environment variable if it exists
|
||||||
self.settings['civitai_api_key'] = env_api_key
|
self.settings["civitai_api_key"] = env_api_key
|
||||||
self._save_settings()
|
self._save_settings()
|
||||||
|
|
||||||
def _default_settings_actions(self) -> List[Dict[str, Any]]:
|
def _default_settings_actions(self) -> List[Dict[str, Any]]:
|
||||||
@@ -799,7 +862,9 @@ class SettingsManager:
|
|||||||
disk_value = self._original_disk_payload.get(key)
|
disk_value = self._original_disk_payload.get(key)
|
||||||
default_value = defaults.get(key)
|
default_value = defaults.get(key)
|
||||||
# Compare using JSON serialization for complex objects
|
# Compare using JSON serialization for complex objects
|
||||||
if json.dumps(disk_value, sort_keys=True, default=str) == json.dumps(default_value, sort_keys=True, default=str):
|
if json.dumps(disk_value, sort_keys=True, default=str) == json.dumps(
|
||||||
|
default_value, sort_keys=True, default=str
|
||||||
|
):
|
||||||
default_value_keys.add(key)
|
default_value_keys.add(key)
|
||||||
|
|
||||||
# Only cleanup if there are "many" default keys (indicating a bloated file)
|
# Only cleanup if there are "many" default keys (indicating a bloated file)
|
||||||
@@ -807,7 +872,7 @@ class SettingsManager:
|
|||||||
if len(default_value_keys) >= DEFAULT_KEYS_CLEANUP_THRESHOLD:
|
if len(default_value_keys) >= DEFAULT_KEYS_CLEANUP_THRESHOLD:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Cleaning up %d default value(s) from settings.json to keep it minimal",
|
"Cleaning up %d default value(s) from settings.json to keep it minimal",
|
||||||
len(default_value_keys)
|
len(default_value_keys),
|
||||||
)
|
)
|
||||||
self._save_settings()
|
self._save_settings()
|
||||||
# Update original payload to match what we just saved
|
# Update original payload to match what we just saved
|
||||||
@@ -817,8 +882,8 @@ class SettingsManager:
|
|||||||
if not self._standalone_mode:
|
if not self._standalone_mode:
|
||||||
return
|
return
|
||||||
|
|
||||||
folder_paths = self.settings.get('folder_paths', {}) or {}
|
folder_paths = self.settings.get("folder_paths", {}) or {}
|
||||||
monitored_keys = ('loras', 'checkpoints', 'embeddings')
|
monitored_keys = ("loras", "checkpoints", "embeddings")
|
||||||
|
|
||||||
has_valid_paths = False
|
has_valid_paths = False
|
||||||
for key in monitored_keys:
|
for key in monitored_keys:
|
||||||
@@ -829,7 +894,10 @@ class SettingsManager:
|
|||||||
iterator = list(raw_paths)
|
iterator = list(raw_paths)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
continue
|
continue
|
||||||
if any(isinstance(path, str) and path and os.path.exists(path) for path in iterator):
|
if any(
|
||||||
|
isinstance(path, str) and path and os.path.exists(path)
|
||||||
|
for path in iterator
|
||||||
|
):
|
||||||
has_valid_paths = True
|
has_valid_paths = True
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -860,13 +928,13 @@ class SettingsManager:
|
|||||||
def _get_default_settings(self) -> Dict[str, Any]:
|
def _get_default_settings(self) -> Dict[str, Any]:
|
||||||
"""Return default settings"""
|
"""Return default settings"""
|
||||||
defaults = copy.deepcopy(DEFAULT_SETTINGS)
|
defaults = copy.deepcopy(DEFAULT_SETTINGS)
|
||||||
defaults['base_model_path_mappings'] = {}
|
defaults["base_model_path_mappings"] = {}
|
||||||
defaults['download_path_templates'] = {}
|
defaults["download_path_templates"] = {}
|
||||||
defaults['priority_tags'] = DEFAULT_PRIORITY_TAG_CONFIG.copy()
|
defaults["priority_tags"] = DEFAULT_PRIORITY_TAG_CONFIG.copy()
|
||||||
defaults.setdefault('folder_paths', {})
|
defaults.setdefault("folder_paths", {})
|
||||||
defaults.setdefault('extra_folder_paths', {})
|
defaults.setdefault("extra_folder_paths", {})
|
||||||
defaults['auto_organize_exclusions'] = []
|
defaults["auto_organize_exclusions"] = []
|
||||||
defaults['metadata_refresh_skip_paths'] = []
|
defaults["metadata_refresh_skip_paths"] = []
|
||||||
|
|
||||||
library_name = defaults.get("active_library") or "default"
|
library_name = defaults.get("active_library") or "default"
|
||||||
default_library = self._build_library_payload(
|
default_library = self._build_library_payload(
|
||||||
@@ -876,8 +944,8 @@ class SettingsManager:
|
|||||||
default_checkpoint_root=defaults.get("default_checkpoint_root"),
|
default_checkpoint_root=defaults.get("default_checkpoint_root"),
|
||||||
default_embedding_root=defaults.get("default_embedding_root"),
|
default_embedding_root=defaults.get("default_embedding_root"),
|
||||||
)
|
)
|
||||||
defaults['libraries'] = {library_name: default_library}
|
defaults["libraries"] = {library_name: default_library}
|
||||||
defaults['active_library'] = library_name
|
defaults["active_library"] = library_name
|
||||||
return defaults
|
return defaults
|
||||||
|
|
||||||
def _normalize_priority_tag_config(self, value: Any) -> Dict[str, str]:
|
def _normalize_priority_tag_config(self, value: Any) -> Dict[str, str]:
|
||||||
@@ -908,7 +976,9 @@ class SettingsManager:
|
|||||||
candidates: Iterable[str] = (
|
candidates: Iterable[str] = (
|
||||||
value.replace("\n", ",").replace(";", ",").split(",")
|
value.replace("\n", ",").replace(";", ",").split(",")
|
||||||
)
|
)
|
||||||
elif isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray, str)):
|
elif isinstance(value, Sequence) and not isinstance(
|
||||||
|
value, (bytes, bytearray, str)
|
||||||
|
):
|
||||||
candidates = value
|
candidates = value
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
@@ -954,7 +1024,9 @@ class SettingsManager:
|
|||||||
candidates: Iterable[str] = (
|
candidates: Iterable[str] = (
|
||||||
value.replace("\n", ",").replace(";", ",").split(",")
|
value.replace("\n", ",").replace(";", ",").split(",")
|
||||||
)
|
)
|
||||||
elif isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray, str)):
|
elif isinstance(value, Sequence) and not isinstance(
|
||||||
|
value, (bytes, bytearray, str)
|
||||||
|
):
|
||||||
candidates = value
|
candidates = value
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
@@ -1023,6 +1095,17 @@ class SettingsManager:
|
|||||||
self._save_settings()
|
self._save_settings()
|
||||||
return base_models
|
return base_models
|
||||||
|
|
||||||
|
def get_skip_previously_downloaded_model_versions(self) -> bool:
|
||||||
|
value = self.settings.get("skip_previously_downloaded_model_versions", False)
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
normalized = False
|
||||||
|
if isinstance(value, str):
|
||||||
|
normalized = value.strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
self.settings["skip_previously_downloaded_model_versions"] = normalized
|
||||||
|
self._save_settings()
|
||||||
|
return normalized
|
||||||
|
|
||||||
def get_extra_folder_paths(self) -> Dict[str, List[str]]:
|
def get_extra_folder_paths(self) -> Dict[str, List[str]]:
|
||||||
"""Get extra folder paths for the active library.
|
"""Get extra folder paths for the active library.
|
||||||
|
|
||||||
@@ -1060,7 +1143,9 @@ class SettingsManager:
|
|||||||
continue
|
continue
|
||||||
normalized = os.path.normcase(os.path.normpath(stripped))
|
normalized = os.path.normcase(os.path.normpath(stripped))
|
||||||
if os.path.exists(stripped):
|
if os.path.exists(stripped):
|
||||||
normalized = os.path.normcase(os.path.normpath(os.path.realpath(stripped)))
|
normalized = os.path.normcase(
|
||||||
|
os.path.normpath(os.path.realpath(stripped))
|
||||||
|
)
|
||||||
primary_real_paths.add(normalized)
|
primary_real_paths.add(normalized)
|
||||||
|
|
||||||
primary_symlink_targets = set()
|
primary_symlink_targets = set()
|
||||||
@@ -1096,8 +1181,13 @@ class SettingsManager:
|
|||||||
continue
|
continue
|
||||||
normalized = os.path.normcase(os.path.normpath(stripped))
|
normalized = os.path.normcase(os.path.normpath(stripped))
|
||||||
if os.path.exists(stripped):
|
if os.path.exists(stripped):
|
||||||
normalized = os.path.normcase(os.path.normpath(os.path.realpath(stripped)))
|
normalized = os.path.normcase(
|
||||||
if normalized in primary_real_paths or normalized in primary_symlink_targets:
|
os.path.normpath(os.path.realpath(stripped))
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
normalized in primary_real_paths
|
||||||
|
or normalized in primary_symlink_targets
|
||||||
|
):
|
||||||
overlapping_paths.append(stripped)
|
overlapping_paths.append(stripped)
|
||||||
|
|
||||||
if overlapping_paths:
|
if overlapping_paths:
|
||||||
@@ -1161,19 +1251,19 @@ class SettingsManager:
|
|||||||
if key == "use_portable_settings" and isinstance(value, bool):
|
if key == "use_portable_settings" and isinstance(value, bool):
|
||||||
portable_switch_pending = True
|
portable_switch_pending = True
|
||||||
self._prepare_portable_switch(value)
|
self._prepare_portable_switch(value)
|
||||||
if key == 'folder_paths' and isinstance(value, Mapping):
|
if key == "folder_paths" and isinstance(value, Mapping):
|
||||||
self._update_active_library_entry(folder_paths=value) # type: ignore[arg-type]
|
self._update_active_library_entry(folder_paths=value) # type: ignore[arg-type]
|
||||||
elif key == 'extra_folder_paths' and isinstance(value, Mapping):
|
elif key == "extra_folder_paths" and isinstance(value, Mapping):
|
||||||
self._update_active_library_entry(extra_folder_paths=value) # type: ignore[arg-type]
|
self._update_active_library_entry(extra_folder_paths=value) # type: ignore[arg-type]
|
||||||
elif key == 'default_lora_root':
|
elif key == "default_lora_root":
|
||||||
self._update_active_library_entry(default_lora_root=str(value))
|
self._update_active_library_entry(default_lora_root=str(value))
|
||||||
elif key == 'default_checkpoint_root':
|
elif key == "default_checkpoint_root":
|
||||||
self._update_active_library_entry(default_checkpoint_root=str(value))
|
self._update_active_library_entry(default_checkpoint_root=str(value))
|
||||||
elif key == 'default_unet_root':
|
elif key == "default_unet_root":
|
||||||
self._update_active_library_entry(default_unet_root=str(value))
|
self._update_active_library_entry(default_unet_root=str(value))
|
||||||
elif key == 'default_embedding_root':
|
elif key == "default_embedding_root":
|
||||||
self._update_active_library_entry(default_embedding_root=str(value))
|
self._update_active_library_entry(default_embedding_root=str(value))
|
||||||
elif key == 'model_name_display':
|
elif key == "model_name_display":
|
||||||
self._notify_model_name_display_change(value)
|
self._notify_model_name_display_change(value)
|
||||||
self._save_settings()
|
self._save_settings()
|
||||||
if portable_switch_pending:
|
if portable_switch_pending:
|
||||||
@@ -1249,10 +1339,9 @@ class SettingsManager:
|
|||||||
|
|
||||||
source_cache_dir = os.path.join(source_dir, "model_cache")
|
source_cache_dir = os.path.join(source_dir, "model_cache")
|
||||||
target_cache_dir = os.path.join(target_dir, "model_cache")
|
target_cache_dir = os.path.join(target_dir, "model_cache")
|
||||||
if (
|
if os.path.isdir(source_cache_dir) and os.path.abspath(
|
||||||
os.path.isdir(source_cache_dir)
|
source_cache_dir
|
||||||
and os.path.abspath(source_cache_dir) != os.path.abspath(target_cache_dir)
|
) != os.path.abspath(target_cache_dir):
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
shutil.copytree(
|
shutil.copytree(
|
||||||
source_cache_dir,
|
source_cache_dir,
|
||||||
@@ -1270,10 +1359,9 @@ class SettingsManager:
|
|||||||
|
|
||||||
source_cache_file = os.path.join(source_dir, "model_cache.sqlite")
|
source_cache_file = os.path.join(source_dir, "model_cache.sqlite")
|
||||||
target_cache_file = os.path.join(target_dir, "model_cache.sqlite")
|
target_cache_file = os.path.join(target_dir, "model_cache.sqlite")
|
||||||
if (
|
if os.path.isfile(source_cache_file) and os.path.abspath(
|
||||||
os.path.isfile(source_cache_file)
|
source_cache_file
|
||||||
and os.path.abspath(source_cache_file) != os.path.abspath(target_cache_file)
|
) != os.path.abspath(target_cache_file):
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
shutil.copy2(source_cache_file, target_cache_file)
|
shutil.copy2(source_cache_file, target_cache_file)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -1299,7 +1387,9 @@ class SettingsManager:
|
|||||||
try:
|
try:
|
||||||
os.makedirs(config_dir, exist_ok=True)
|
os.makedirs(config_dir, exist_ok=True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to create user config directory %s: %s", config_dir, exc)
|
logger.warning(
|
||||||
|
"Failed to create user config directory %s: %s", config_dir, exc
|
||||||
|
)
|
||||||
|
|
||||||
return config_dir
|
return config_dir
|
||||||
|
|
||||||
@@ -1359,7 +1449,9 @@ class SettingsManager:
|
|||||||
try:
|
try:
|
||||||
asyncio.run(coroutine)
|
asyncio.run(coroutine)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
logger.debug("Skipping name display update due to missing event loop")
|
logger.debug(
|
||||||
|
"Skipping name display update due to missing event loop"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if loop is not None and target_loop is loop:
|
if loop is not None and target_loop is loop:
|
||||||
@@ -1382,7 +1474,7 @@ class SettingsManager:
|
|||||||
"""Save settings to file"""
|
"""Save settings to file"""
|
||||||
try:
|
try:
|
||||||
payload = self._serialize_settings_for_disk()
|
payload = self._serialize_settings_for_disk()
|
||||||
with open(self.settings_file, 'w', encoding='utf-8') as f:
|
with open(self.settings_file, "w", encoding="utf-8") as f:
|
||||||
json.dump(payload, f, indent=2)
|
json.dump(payload, f, indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error saving settings: {e}")
|
logger.error(f"Error saving settings: {e}")
|
||||||
@@ -1423,7 +1515,9 @@ class SettingsManager:
|
|||||||
minimal[key] = copy.deepcopy(value)
|
minimal[key] = copy.deepcopy(value)
|
||||||
# Complex objects need deep comparison
|
# Complex objects need deep comparison
|
||||||
elif isinstance(value, (dict, list)) and default_value is not None:
|
elif isinstance(value, (dict, list)) and default_value is not None:
|
||||||
if json.dumps(value, sort_keys=True, default=str) != json.dumps(default_value, sort_keys=True, default=str):
|
if json.dumps(value, sort_keys=True, default=str) != json.dumps(
|
||||||
|
default_value, sort_keys=True, default=str
|
||||||
|
):
|
||||||
minimal[key] = copy.deepcopy(value)
|
minimal[key] = copy.deepcopy(value)
|
||||||
# Simple values use direct comparison
|
# Simple values use direct comparison
|
||||||
elif value != default_value:
|
elif value != default_value:
|
||||||
@@ -1500,9 +1594,15 @@ class SettingsManager:
|
|||||||
existing = libraries.get(name, {})
|
existing = libraries.get(name, {})
|
||||||
|
|
||||||
payload = self._build_library_payload(
|
payload = self._build_library_payload(
|
||||||
folder_paths=folder_paths if folder_paths is not None else existing.get("folder_paths"),
|
folder_paths=folder_paths
|
||||||
extra_folder_paths=extra_folder_paths if extra_folder_paths is not None else existing.get("extra_folder_paths"),
|
if folder_paths is not None
|
||||||
default_lora_root=default_lora_root if default_lora_root is not None else existing.get("default_lora_root"),
|
else existing.get("folder_paths"),
|
||||||
|
extra_folder_paths=extra_folder_paths
|
||||||
|
if extra_folder_paths is not None
|
||||||
|
else existing.get("extra_folder_paths"),
|
||||||
|
default_lora_root=default_lora_root
|
||||||
|
if default_lora_root is not None
|
||||||
|
else existing.get("default_lora_root"),
|
||||||
default_checkpoint_root=(
|
default_checkpoint_root=(
|
||||||
default_checkpoint_root
|
default_checkpoint_root
|
||||||
if default_checkpoint_root is not None
|
if default_checkpoint_root is not None
|
||||||
@@ -1662,7 +1762,9 @@ class SettingsManager:
|
|||||||
if service and hasattr(service, "on_library_changed"):
|
if service and hasattr(service, "on_library_changed"):
|
||||||
try:
|
try:
|
||||||
service.on_library_changed()
|
service.on_library_changed()
|
||||||
except Exception as service_exc: # pragma: no cover - defensive logging
|
except (
|
||||||
|
Exception
|
||||||
|
) as service_exc: # pragma: no cover - defensive logging
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Service %s failed to handle library change: %s",
|
"Service %s failed to handle library change: %s",
|
||||||
service_name,
|
service_name,
|
||||||
@@ -1680,7 +1782,7 @@ class SettingsManager:
|
|||||||
Returns:
|
Returns:
|
||||||
Template string for the model type, defaults to '{base_model}/{first_tag}'
|
Template string for the model type, defaults to '{base_model}/{first_tag}'
|
||||||
"""
|
"""
|
||||||
templates = self.settings.get('download_path_templates', {})
|
templates = self.settings.get("download_path_templates", {})
|
||||||
|
|
||||||
# Handle edge case where templates might be stored as JSON string
|
# Handle edge case where templates might be stored as JSON string
|
||||||
if isinstance(templates, str):
|
if isinstance(templates, str):
|
||||||
@@ -1689,36 +1791,40 @@ class SettingsManager:
|
|||||||
parsed_templates = json.loads(templates)
|
parsed_templates = json.loads(templates)
|
||||||
if isinstance(parsed_templates, dict):
|
if isinstance(parsed_templates, dict):
|
||||||
# Update settings with parsed dictionary
|
# Update settings with parsed dictionary
|
||||||
self.settings['download_path_templates'] = parsed_templates
|
self.settings["download_path_templates"] = parsed_templates
|
||||||
self._save_settings()
|
self._save_settings()
|
||||||
templates = parsed_templates
|
templates = parsed_templates
|
||||||
logger.info("Successfully parsed download_path_templates from JSON string")
|
logger.info(
|
||||||
|
"Successfully parsed download_path_templates from JSON string"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Parsed JSON is not a dictionary")
|
raise ValueError("Parsed JSON is not a dictionary")
|
||||||
except (json.JSONDecodeError, ValueError) as e:
|
except (json.JSONDecodeError, ValueError) as e:
|
||||||
# If parsing fails, set default values
|
# If parsing fails, set default values
|
||||||
logger.warning(f"Failed to parse download_path_templates JSON string: {e}. Setting default values.")
|
logger.warning(
|
||||||
default_template = '{base_model}/{first_tag}'
|
f"Failed to parse download_path_templates JSON string: {e}. Setting default values."
|
||||||
|
)
|
||||||
|
default_template = "{base_model}/{first_tag}"
|
||||||
templates = {
|
templates = {
|
||||||
'lora': default_template,
|
"lora": default_template,
|
||||||
'checkpoint': default_template,
|
"checkpoint": default_template,
|
||||||
'embedding': default_template
|
"embedding": default_template,
|
||||||
}
|
}
|
||||||
self.settings['download_path_templates'] = templates
|
self.settings["download_path_templates"] = templates
|
||||||
self._save_settings()
|
self._save_settings()
|
||||||
|
|
||||||
# Ensure templates is a dictionary
|
# Ensure templates is a dictionary
|
||||||
if not isinstance(templates, dict):
|
if not isinstance(templates, dict):
|
||||||
default_template = '{base_model}/{first_tag}'
|
default_template = "{base_model}/{first_tag}"
|
||||||
templates = {
|
templates = {
|
||||||
'lora': default_template,
|
"lora": default_template,
|
||||||
'checkpoint': default_template,
|
"checkpoint": default_template,
|
||||||
'embedding': default_template
|
"embedding": default_template,
|
||||||
}
|
}
|
||||||
self.settings['download_path_templates'] = templates
|
self.settings["download_path_templates"] = templates
|
||||||
self._save_settings()
|
self._save_settings()
|
||||||
|
|
||||||
return templates.get(model_type, '{base_model}/{first_tag}')
|
return templates.get(model_type, "{base_model}/{first_tag}")
|
||||||
|
|
||||||
|
|
||||||
_SETTINGS_MANAGER: Optional["SettingsManager"] = None
|
_SETTINGS_MANAGER: Optional["SettingsManager"] = None
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ def _normalize_commercial_values(value: Any) -> Sequence[str]:
|
|||||||
|
|
||||||
def _split_aggregate(value_str: str) -> list[str]:
|
def _split_aggregate(value_str: str) -> list[str]:
|
||||||
stripped = value_str.strip()
|
stripped = value_str.strip()
|
||||||
looks_aggregate = "," in stripped or (stripped.startswith("{") and stripped.endswith("}"))
|
looks_aggregate = "," in stripped or (
|
||||||
|
stripped.startswith("{") and stripped.endswith("}")
|
||||||
|
)
|
||||||
if not looks_aggregate:
|
if not looks_aggregate:
|
||||||
return [value_str]
|
return [value_str]
|
||||||
|
|
||||||
@@ -141,14 +143,18 @@ def build_license_flags(payload: Mapping[str, Any] | None) -> int:
|
|||||||
return flags
|
return flags
|
||||||
|
|
||||||
|
|
||||||
def resolve_license_info(model_data: Mapping[str, Any] | None) -> tuple[Dict[str, Any], int]:
|
def resolve_license_info(
|
||||||
|
model_data: Mapping[str, Any] | None,
|
||||||
|
) -> tuple[Dict[str, Any], int]:
|
||||||
"""Return normalized license payload and its encoded bitset."""
|
"""Return normalized license payload and its encoded bitset."""
|
||||||
|
|
||||||
payload = resolve_license_payload(model_data)
|
payload = resolve_license_payload(model_data)
|
||||||
return payload, build_license_flags(payload)
|
return payload, build_license_flags(payload)
|
||||||
|
|
||||||
|
|
||||||
def rewrite_preview_url(source_url: str | None, media_type: str | None = None) -> tuple[str | None, bool]:
|
def rewrite_preview_url(
|
||||||
|
source_url: str | None, media_type: str | None = None
|
||||||
|
) -> tuple[str | None, bool]:
|
||||||
"""Rewrite Civitai preview URLs to use optimized renditions.
|
"""Rewrite Civitai preview URLs to use optimized renditions.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -168,7 +174,12 @@ def rewrite_preview_url(source_url: str | None, media_type: str | None = None) -
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return source_url, False
|
return source_url, False
|
||||||
|
|
||||||
if parsed.netloc.lower() != "image.civitai.com":
|
hostname = parsed.hostname
|
||||||
|
if hostname is None:
|
||||||
|
return source_url, False
|
||||||
|
|
||||||
|
hostname = hostname.lower()
|
||||||
|
if hostname == "civitai.com" or not hostname.endswith(".civitai.com"):
|
||||||
return source_url, False
|
return source_url, False
|
||||||
|
|
||||||
replacement = "/width=450,optimized=true"
|
replacement = "/width=450,optimized=true"
|
||||||
|
|||||||
@@ -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.0.0"
|
version = "1.0.2"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ export class SettingsManager {
|
|||||||
backendSettings?.metadata_refresh_skip_paths ?? defaults.metadata_refresh_skip_paths
|
backendSettings?.metadata_refresh_skip_paths ?? defaults.metadata_refresh_skip_paths
|
||||||
);
|
);
|
||||||
|
|
||||||
|
merged.skip_previously_downloaded_model_versions =
|
||||||
|
backendSettings?.skip_previously_downloaded_model_versions
|
||||||
|
?? defaults.skip_previously_downloaded_model_versions;
|
||||||
|
|
||||||
merged.download_skip_base_models = this.normalizeDownloadSkipBaseModels(
|
merged.download_skip_base_models = this.normalizeDownloadSkipBaseModels(
|
||||||
backendSettings?.download_skip_base_models ?? defaults.download_skip_base_models
|
backendSettings?.download_skip_base_models ?? defaults.download_skip_base_models
|
||||||
);
|
);
|
||||||
@@ -836,6 +840,12 @@ export class SettingsManager {
|
|||||||
hideEarlyAccessUpdatesCheckbox.checked = state.global.settings.hide_early_access_updates || false;
|
hideEarlyAccessUpdatesCheckbox.checked = state.global.settings.hide_early_access_updates || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const skipPreviouslyDownloadedModelVersionsCheckbox = document.getElementById('skipPreviouslyDownloadedModelVersions');
|
||||||
|
if (skipPreviouslyDownloadedModelVersionsCheckbox) {
|
||||||
|
skipPreviouslyDownloadedModelVersionsCheckbox.checked =
|
||||||
|
state.global.settings.skip_previously_downloaded_model_versions || false;
|
||||||
|
}
|
||||||
|
|
||||||
// Set optimize example images setting
|
// Set optimize example images setting
|
||||||
const optimizeExampleImagesCheckbox = document.getElementById('optimizeExampleImages');
|
const optimizeExampleImagesCheckbox = document.getElementById('optimizeExampleImages');
|
||||||
if (optimizeExampleImagesCheckbox) {
|
if (optimizeExampleImagesCheckbox) {
|
||||||
@@ -1246,10 +1256,7 @@ export class SettingsManager {
|
|||||||
throw new Error('No LoRA roots found');
|
throw new Error('No LoRA roots found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing options except the first one (No Default)
|
|
||||||
const noDefaultOption = defaultLoraRootSelect.querySelector('option[value=""]');
|
|
||||||
defaultLoraRootSelect.innerHTML = '';
|
defaultLoraRootSelect.innerHTML = '';
|
||||||
defaultLoraRootSelect.appendChild(noDefaultOption);
|
|
||||||
|
|
||||||
// Add options for each root
|
// Add options for each root
|
||||||
data.roots.forEach(root => {
|
data.roots.forEach(root => {
|
||||||
@@ -1259,9 +1266,8 @@ export class SettingsManager {
|
|||||||
defaultLoraRootSelect.appendChild(option);
|
defaultLoraRootSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set selected value from settings
|
|
||||||
const defaultRoot = state.global.settings.default_lora_root || '';
|
const defaultRoot = state.global.settings.default_lora_root || '';
|
||||||
defaultLoraRootSelect.value = defaultRoot;
|
defaultLoraRootSelect.value = data.roots.includes(defaultRoot) ? defaultRoot : data.roots[0];
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading LoRA roots:', error);
|
console.error('Error loading LoRA roots:', error);
|
||||||
@@ -1285,10 +1291,7 @@ export class SettingsManager {
|
|||||||
throw new Error('No checkpoint roots found');
|
throw new Error('No checkpoint roots found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing options except first one (No Default)
|
|
||||||
const noDefaultOption = defaultCheckpointRootSelect.querySelector('option[value=""]');
|
|
||||||
defaultCheckpointRootSelect.innerHTML = '';
|
defaultCheckpointRootSelect.innerHTML = '';
|
||||||
defaultCheckpointRootSelect.appendChild(noDefaultOption);
|
|
||||||
|
|
||||||
// Add options for each root
|
// Add options for each root
|
||||||
data.roots.forEach(root => {
|
data.roots.forEach(root => {
|
||||||
@@ -1298,9 +1301,8 @@ export class SettingsManager {
|
|||||||
defaultCheckpointRootSelect.appendChild(option);
|
defaultCheckpointRootSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set selected value from settings
|
|
||||||
const defaultRoot = state.global.settings.default_checkpoint_root || '';
|
const defaultRoot = state.global.settings.default_checkpoint_root || '';
|
||||||
defaultCheckpointRootSelect.value = defaultRoot;
|
defaultCheckpointRootSelect.value = data.roots.includes(defaultRoot) ? defaultRoot : data.roots[0];
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading checkpoint roots:', error);
|
console.error('Error loading checkpoint roots:', error);
|
||||||
@@ -1324,10 +1326,7 @@ export class SettingsManager {
|
|||||||
throw new Error('No diffusion model roots found');
|
throw new Error('No diffusion model roots found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing options except first one (No Default)
|
|
||||||
const noDefaultOption = defaultUnetRootSelect.querySelector('option[value=""]');
|
|
||||||
defaultUnetRootSelect.innerHTML = '';
|
defaultUnetRootSelect.innerHTML = '';
|
||||||
defaultUnetRootSelect.appendChild(noDefaultOption);
|
|
||||||
|
|
||||||
// Add options for each root
|
// Add options for each root
|
||||||
data.roots.forEach(root => {
|
data.roots.forEach(root => {
|
||||||
@@ -1337,9 +1336,8 @@ export class SettingsManager {
|
|||||||
defaultUnetRootSelect.appendChild(option);
|
defaultUnetRootSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set selected value from settings
|
|
||||||
const defaultRoot = state.global.settings.default_unet_root || '';
|
const defaultRoot = state.global.settings.default_unet_root || '';
|
||||||
defaultUnetRootSelect.value = defaultRoot;
|
defaultUnetRootSelect.value = data.roots.includes(defaultRoot) ? defaultRoot : data.roots[0];
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading diffusion model roots:', error);
|
console.error('Error loading diffusion model roots:', error);
|
||||||
@@ -1363,10 +1361,7 @@ export class SettingsManager {
|
|||||||
throw new Error('No embedding roots found');
|
throw new Error('No embedding roots found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing options except first one (No Default)
|
|
||||||
const noDefaultOption = defaultEmbeddingRootSelect.querySelector('option[value=""]');
|
|
||||||
defaultEmbeddingRootSelect.innerHTML = '';
|
defaultEmbeddingRootSelect.innerHTML = '';
|
||||||
defaultEmbeddingRootSelect.appendChild(noDefaultOption);
|
|
||||||
|
|
||||||
// Add options for each root
|
// Add options for each root
|
||||||
data.roots.forEach(root => {
|
data.roots.forEach(root => {
|
||||||
@@ -1376,9 +1371,8 @@ export class SettingsManager {
|
|||||||
defaultEmbeddingRootSelect.appendChild(option);
|
defaultEmbeddingRootSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set selected value from settings
|
|
||||||
const defaultRoot = state.global.settings.default_embedding_root || '';
|
const defaultRoot = state.global.settings.default_embedding_root || '';
|
||||||
defaultEmbeddingRootSelect.value = defaultRoot;
|
defaultEmbeddingRootSelect.value = data.roots.includes(defaultRoot) ? defaultRoot : data.roots[0];
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading embedding roots:', error);
|
console.error('Error loading embedding roots:', error);
|
||||||
@@ -1477,7 +1471,7 @@ export class SettingsManager {
|
|||||||
try {
|
try {
|
||||||
// Save to backend - this triggers path validation
|
// Save to backend - this triggers path validation
|
||||||
await this.saveSetting('extra_folder_paths', extraFolderPaths);
|
await this.saveSetting('extra_folder_paths', extraFolderPaths);
|
||||||
showToast('toast.settings.settingsUpdated', { setting: 'Extra Folder Paths' }, 'success');
|
showToast('settings.extraFolderPaths.saveSuccess', {}, 'success');
|
||||||
|
|
||||||
// Add empty row if no valid paths exist for the changed type
|
// Add empty row if no valid paths exist for the changed type
|
||||||
const container = document.getElementById(`extraFolderPaths-${changedModelType}`);
|
const container = document.getElementById(`extraFolderPaths-${changedModelType}`);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
|
|||||||
hide_early_access_updates: false,
|
hide_early_access_updates: false,
|
||||||
auto_organize_exclusions: [],
|
auto_organize_exclusions: [],
|
||||||
metadata_refresh_skip_paths: [],
|
metadata_refresh_skip_paths: [],
|
||||||
|
skip_previously_downloaded_model_versions: false,
|
||||||
download_skip_base_models: [],
|
download_skip_base_models: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ export function rewriteCivitaiUrl(sourceUrl, mediaType = null, mode = Optimizati
|
|||||||
try {
|
try {
|
||||||
const url = new URL(sourceUrl);
|
const url = new URL(sourceUrl);
|
||||||
|
|
||||||
// Check if it's a CivitAI image domain
|
// Check if it's a CivitAI CDN domain (supports all subdomains like image-b2.civitai.com)
|
||||||
if (url.hostname.toLowerCase() !== 'image.civitai.com') {
|
const hostname = url.hostname.toLowerCase();
|
||||||
|
if (hostname === 'civitai.com' || !hostname.endsWith('.civitai.com')) {
|
||||||
return [sourceUrl, false];
|
return [sourceUrl, false];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +113,8 @@ export function isCivitaiUrl(url) {
|
|||||||
if (!url) return false;
|
if (!url) return false;
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
return parsed.hostname.toLowerCase() === 'image.civitai.com';
|
const hostname = parsed.hostname.toLowerCase();
|
||||||
|
return hostname.endsWith('.civitai.com') && hostname !== 'civitai.com';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,9 +484,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-control select-control">
|
<div class="setting-control select-control">
|
||||||
<select id="defaultLoraRoot" onchange="settingsManager.saveSelectSetting('defaultLoraRoot', 'default_lora_root')">
|
<select id="defaultLoraRoot" onchange="settingsManager.saveSelectSetting('defaultLoraRoot', 'default_lora_root')"></select>
|
||||||
<option value="">{{ t('settings.folderSettings.noDefault') }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -500,9 +498,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-control select-control">
|
<div class="setting-control select-control">
|
||||||
<select id="defaultCheckpointRoot" onchange="settingsManager.saveSelectSetting('defaultCheckpointRoot', 'default_checkpoint_root')">
|
<select id="defaultCheckpointRoot" onchange="settingsManager.saveSelectSetting('defaultCheckpointRoot', 'default_checkpoint_root')"></select>
|
||||||
<option value="">{{ t('settings.folderSettings.noDefault') }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -516,9 +512,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-control select-control">
|
<div class="setting-control select-control">
|
||||||
<select id="defaultUnetRoot" onchange="settingsManager.saveSelectSetting('defaultUnetRoot', 'default_unet_root')">
|
<select id="defaultUnetRoot" onchange="settingsManager.saveSelectSetting('defaultUnetRoot', 'default_unet_root')"></select>
|
||||||
<option value="">{{ t('settings.folderSettings.noDefault') }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -532,9 +526,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-control select-control">
|
<div class="setting-control select-control">
|
||||||
<select id="defaultEmbeddingRoot" onchange="settingsManager.saveSelectSetting('defaultEmbeddingRoot', 'default_embedding_root')">
|
<select id="defaultEmbeddingRoot" onchange="settingsManager.saveSelectSetting('defaultEmbeddingRoot', 'default_embedding_root')"></select>
|
||||||
<option value="">{{ t('settings.folderSettings.noDefault') }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -545,7 +537,7 @@
|
|||||||
<div class="settings-subsection-header">
|
<div class="settings-subsection-header">
|
||||||
<h4>
|
<h4>
|
||||||
{{ t('settings.extraFolderPaths.title') }}
|
{{ t('settings.extraFolderPaths.title') }}
|
||||||
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.extraFolderPaths.help') }}"></i>
|
<i class="fas fa-sync-alt restart-required-icon" title="{{ t('settings.extraFolderPaths.restartRequired') }}"></i>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
@@ -743,6 +735,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info">
|
||||||
|
<label for="skipPreviouslyDownloadedModelVersions">
|
||||||
|
{{ t('settings.skipPreviouslyDownloadedModelVersions.label') }}
|
||||||
|
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.skipPreviouslyDownloadedModelVersions.help') }}"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="skipPreviouslyDownloadedModelVersions"
|
||||||
|
onchange="settingsManager.saveToggleSetting('skipPreviouslyDownloadedModelVersions', 'skip_previously_downloaded_model_versions')">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
|
|||||||
@@ -131,6 +131,102 @@ def test_save_paths_logs_warning_when_upsert_fails(
|
|||||||
assert "Failed to save folder paths: boom" in caplog.text
|
assert "Failed to save folder paths: boom" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_paths_repairs_empty_default_roots(monkeypatch: pytest.MonkeyPatch, tmp_path):
|
||||||
|
folder_paths = _setup_config_environment(monkeypatch, tmp_path)
|
||||||
|
|
||||||
|
class FakeSettingsService:
|
||||||
|
def get_libraries(self):
|
||||||
|
return {
|
||||||
|
"comfyui": {
|
||||||
|
"folder_paths": {key: list(value) for key, value in folder_paths.items()},
|
||||||
|
"default_lora_root": "",
|
||||||
|
"default_checkpoint_root": "",
|
||||||
|
"default_embedding_root": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def rename_library(self, *_):
|
||||||
|
raise AssertionError("rename_library should not be invoked")
|
||||||
|
|
||||||
|
def upsert_library(self, name: str, **payload):
|
||||||
|
self.name = name
|
||||||
|
self.payload = payload
|
||||||
|
|
||||||
|
fake_settings = FakeSettingsService()
|
||||||
|
monkeypatch.setattr(settings_manager_module, "settings", fake_settings)
|
||||||
|
|
||||||
|
config_module.Config()
|
||||||
|
|
||||||
|
assert fake_settings.name == "comfyui"
|
||||||
|
assert fake_settings.payload["default_lora_root"] == folder_paths["loras"][0].replace("\\", "/")
|
||||||
|
assert fake_settings.payload["default_checkpoint_root"] == folder_paths["checkpoints"][0].replace("\\", "/")
|
||||||
|
assert fake_settings.payload["default_embedding_root"] == folder_paths["embeddings"][0].replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_paths_repairs_stale_default_roots(monkeypatch: pytest.MonkeyPatch, tmp_path):
|
||||||
|
folder_paths = _setup_config_environment(monkeypatch, tmp_path)
|
||||||
|
|
||||||
|
class FakeSettingsService:
|
||||||
|
def get_libraries(self):
|
||||||
|
return {
|
||||||
|
"comfyui": {
|
||||||
|
"folder_paths": {key: list(value) for key, value in folder_paths.items()},
|
||||||
|
"default_lora_root": "/stale/loras",
|
||||||
|
"default_checkpoint_root": "/stale/checkpoints",
|
||||||
|
"default_embedding_root": "/stale/embeddings",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def rename_library(self, *_):
|
||||||
|
raise AssertionError("rename_library should not be invoked")
|
||||||
|
|
||||||
|
def upsert_library(self, name: str, **payload):
|
||||||
|
self.name = name
|
||||||
|
self.payload = payload
|
||||||
|
|
||||||
|
fake_settings = FakeSettingsService()
|
||||||
|
monkeypatch.setattr(settings_manager_module, "settings", fake_settings)
|
||||||
|
|
||||||
|
config_module.Config()
|
||||||
|
|
||||||
|
assert fake_settings.name == "comfyui"
|
||||||
|
assert fake_settings.payload["default_lora_root"] == folder_paths["loras"][0].replace("\\", "/")
|
||||||
|
assert fake_settings.payload["default_checkpoint_root"] == folder_paths["checkpoints"][0].replace("\\", "/")
|
||||||
|
assert fake_settings.payload["default_embedding_root"] == folder_paths["embeddings"][0].replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_paths_keeps_valid_default_roots(monkeypatch: pytest.MonkeyPatch, tmp_path):
|
||||||
|
folder_paths = _setup_config_environment(monkeypatch, tmp_path)
|
||||||
|
|
||||||
|
class FakeSettingsService:
|
||||||
|
def get_libraries(self):
|
||||||
|
return {
|
||||||
|
"comfyui": {
|
||||||
|
"folder_paths": {key: list(value) for key, value in folder_paths.items()},
|
||||||
|
"default_lora_root": folder_paths["loras"][0],
|
||||||
|
"default_checkpoint_root": folder_paths["checkpoints"][0],
|
||||||
|
"default_embedding_root": folder_paths["embeddings"][0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def rename_library(self, *_):
|
||||||
|
raise AssertionError("rename_library should not be invoked")
|
||||||
|
|
||||||
|
def upsert_library(self, name: str, **payload):
|
||||||
|
self.name = name
|
||||||
|
self.payload = payload
|
||||||
|
|
||||||
|
fake_settings = FakeSettingsService()
|
||||||
|
monkeypatch.setattr(settings_manager_module, "settings", fake_settings)
|
||||||
|
|
||||||
|
config_module.Config()
|
||||||
|
|
||||||
|
assert fake_settings.name == "comfyui"
|
||||||
|
assert fake_settings.payload["default_lora_root"] == folder_paths["loras"][0].replace("\\", "/")
|
||||||
|
assert fake_settings.payload["default_checkpoint_root"] == folder_paths["checkpoints"][0].replace("\\", "/")
|
||||||
|
assert fake_settings.payload["default_embedding_root"] == folder_paths["embeddings"][0].replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
def test_save_paths_removes_template_default_library(monkeypatch, tmp_path):
|
def test_save_paths_removes_template_default_library(monkeypatch, tmp_path):
|
||||||
folder_paths = _setup_config_environment(monkeypatch, tmp_path)
|
folder_paths = _setup_config_environment(monkeypatch, tmp_path)
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ vi.mock('../../../static/js/state/index.js', () => {
|
|||||||
},
|
},
|
||||||
createDefaultSettings: () => ({
|
createDefaultSettings: () => ({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
|
skip_previously_downloaded_model_versions: false,
|
||||||
download_skip_base_models: [],
|
download_skip_base_models: [],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -117,6 +118,7 @@ describe('SettingsManager download skip base models UI', () => {
|
|||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
state.global.settings = {
|
state.global.settings = {
|
||||||
|
skip_previously_downloaded_model_versions: false,
|
||||||
download_skip_base_models: [],
|
download_skip_base_models: [],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -150,4 +152,31 @@ describe('SettingsManager download skip base models UI', () => {
|
|||||||
expect(document.querySelectorAll('#downloadSkipBaseModelsContainer input')).toHaveLength(0);
|
expect(document.querySelectorAll('#downloadSkipBaseModelsContainer input')).toHaveLength(0);
|
||||||
expect(document.getElementById('downloadSkipBaseModelsEmpty').hidden).toBe(false);
|
expect(document.getElementById('downloadSkipBaseModelsEmpty').hidden).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('initializes the previously-downloaded-version toggle from settings', () => {
|
||||||
|
document.body.innerHTML = '<input id="skipPreviouslyDownloadedModelVersions" type="checkbox" />';
|
||||||
|
state.global.settings.skip_previously_downloaded_model_versions = true;
|
||||||
|
const manager = createManager();
|
||||||
|
|
||||||
|
manager.loadSettingsToUI();
|
||||||
|
|
||||||
|
expect(document.getElementById('skipPreviouslyDownloadedModelVersions').checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saves the previously-downloaded-version toggle with the expected setting key', async () => {
|
||||||
|
document.body.innerHTML = '<input id="skipPreviouslyDownloadedModelVersions" type="checkbox" checked />';
|
||||||
|
const manager = createManager();
|
||||||
|
manager.saveSetting = vi.fn().mockResolvedValue();
|
||||||
|
manager.applyFrontendSettings = vi.fn();
|
||||||
|
|
||||||
|
await manager.saveToggleSetting(
|
||||||
|
'skipPreviouslyDownloadedModelVersions',
|
||||||
|
'skip_previously_downloaded_model_versions',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.saveSetting).toHaveBeenCalledWith(
|
||||||
|
'skip_previously_downloaded_model_versions',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,6 +94,37 @@ describe('civitaiUtils', () => {
|
|||||||
expect(wasRewritten).toBe(false);
|
expect(wasRewritten).toBe(false);
|
||||||
expect(rewritten).toBe('not-a-valid-url');
|
expect(rewritten).toBe('not-a-valid-url');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should rewrite URLs from CivitAI CDN subdomains', () => {
|
||||||
|
const originalUrl = 'https://image-b2.civitai.com/file/civitai-media-cache/original=true/sample.png';
|
||||||
|
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL);
|
||||||
|
|
||||||
|
expect(wasRewritten).toBe(true);
|
||||||
|
expect(rewritten).toBe('https://image-b2.civitai.com/file/civitai-media-cache/width=450,optimized=true/sample.png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle URLs with explicit port numbers', () => {
|
||||||
|
const originalUrl = 'https://image.civitai.com:443/checkpoints/original=true/test.png';
|
||||||
|
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL);
|
||||||
|
|
||||||
|
expect(wasRewritten).toBe(true);
|
||||||
|
// JavaScript URL.toString() removes default HTTPS port (443)
|
||||||
|
expect(rewritten).toBe('https://image.civitai.com/checkpoints/width=450,optimized=true/test.png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle case-insensitive hostnames', () => {
|
||||||
|
const testCases = [
|
||||||
|
'https://IMAGE.CIVITAI.COM/original=true/test.png',
|
||||||
|
'https://Image.Civitai.Com/original=true/test.png',
|
||||||
|
'https://image-b2.CIVITAI.com/original=true/test.png',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const url of testCases) {
|
||||||
|
const [rewritten, wasRewritten] = rewriteCivitaiUrl(url, 'image', OptimizationMode.THUMBNAIL);
|
||||||
|
expect(wasRewritten).toBe(true);
|
||||||
|
expect(rewritten).toContain('width=450,optimized=true');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getOptimizedUrl', () => {
|
describe('getOptimizedUrl', () => {
|
||||||
@@ -157,6 +188,23 @@ describe('civitaiUtils', () => {
|
|||||||
expect(isCivitaiUrl('https://image.civitai.com/')).toBe(true);
|
expect(isCivitaiUrl('https://image.civitai.com/')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return true for CivitAI CDN subdomains', () => {
|
||||||
|
expect(isCivitaiUrl('https://image-b2.civitai.com/file/test.png')).toBe(true);
|
||||||
|
expect(isCivitaiUrl('https://image-b3.civitai.com/test.jpg')).toBe(true);
|
||||||
|
expect(isCivitaiUrl('https://cdn.civitai.com/test.png')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for CivitAI URLs with explicit ports', () => {
|
||||||
|
expect(isCivitaiUrl('https://image.civitai.com:443/test.png')).toBe(true);
|
||||||
|
expect(isCivitaiUrl('https://image-b2.civitai.com:443/file/test.jpg')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle case-insensitive hostnames', () => {
|
||||||
|
expect(isCivitaiUrl('https://IMAGE.CIVITAI.COM/test.png')).toBe(true);
|
||||||
|
expect(isCivitaiUrl('https://Image.Civitai.Com/test.png')).toBe(true);
|
||||||
|
expect(isCivitaiUrl('https://image-b2.CIVITAI.com/test.png')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return false for non-CivitAI URLs', () => {
|
it('should return false for non-CivitAI URLs', () => {
|
||||||
expect(isCivitaiUrl('https://example.com/image.jpg')).toBe(false);
|
expect(isCivitaiUrl('https://example.com/image.jpg')).toBe(false);
|
||||||
expect(isCivitaiUrl('https://civitai.com/image.jpg')).toBe(false);
|
expect(isCivitaiUrl('https://civitai.com/image.jpg')).toBe(false);
|
||||||
|
|||||||
151
tests/frontend/utils/loraChainTraversal.test.js
Normal file
151
tests/frontend/utils/loraChainTraversal.test.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const { APP_MODULE, UTILS_MODULE } = vi.hoisted(() => ({
|
||||||
|
APP_MODULE: new URL("../../../scripts/app.js", import.meta.url).pathname,
|
||||||
|
UTILS_MODULE: new URL("../../../web/comfyui/utils.js", import.meta.url).pathname,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(APP_MODULE, () => ({
|
||||||
|
app: {
|
||||||
|
graph: null,
|
||||||
|
registerExtension: vi.fn(),
|
||||||
|
ui: {
|
||||||
|
settings: {
|
||||||
|
getSettingValue: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("LoRA chain traversal", () => {
|
||||||
|
let collectActiveLorasFromChain;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
({ collectActiveLorasFromChain } = await import(UTILS_MODULE));
|
||||||
|
});
|
||||||
|
|
||||||
|
function createGraph(nodes, links) {
|
||||||
|
const graph = {
|
||||||
|
_nodes: nodes,
|
||||||
|
links,
|
||||||
|
getNodeById(id) {
|
||||||
|
return nodes.find((node) => node.id === id) ?? null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
node.graph = graph;
|
||||||
|
});
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("aggregates active LoRAs through a combiner with multiple LORA_STACK inputs", () => {
|
||||||
|
const randomizerA = {
|
||||||
|
id: 1,
|
||||||
|
comfyClass: "Lora Randomizer (LoraManager)",
|
||||||
|
mode: 0,
|
||||||
|
widgets: [
|
||||||
|
{
|
||||||
|
name: "loras",
|
||||||
|
value: [
|
||||||
|
{ name: "Alpha", active: true },
|
||||||
|
{ name: "Ignored", active: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
};
|
||||||
|
const randomizerB = {
|
||||||
|
id: 2,
|
||||||
|
comfyClass: "Lora Randomizer (LoraManager)",
|
||||||
|
mode: 0,
|
||||||
|
widgets: [
|
||||||
|
{
|
||||||
|
name: "loras",
|
||||||
|
value: [{ name: "Beta", active: true }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
};
|
||||||
|
const combiner = {
|
||||||
|
id: 3,
|
||||||
|
comfyClass: "Lora Stack Combiner (LoraManager)",
|
||||||
|
mode: 0,
|
||||||
|
widgets: [],
|
||||||
|
inputs: [
|
||||||
|
{ name: "lora_stack_a", type: "LORA_STACK", link: 11 },
|
||||||
|
{ name: "lora_stack_b", type: "LORA_STACK", link: 12 },
|
||||||
|
],
|
||||||
|
outputs: [],
|
||||||
|
};
|
||||||
|
const loader = {
|
||||||
|
id: 4,
|
||||||
|
comfyClass: "Lora Loader (LoraManager)",
|
||||||
|
mode: 0,
|
||||||
|
widgets: [],
|
||||||
|
inputs: [{ name: "lora_stack", type: "LORA_STACK", link: 13 }],
|
||||||
|
outputs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
createGraph(
|
||||||
|
[randomizerA, randomizerB, combiner, loader],
|
||||||
|
{
|
||||||
|
11: { origin_id: 1, target_id: 3 },
|
||||||
|
12: { origin_id: 2, target_id: 3 },
|
||||||
|
13: { origin_id: 3, target_id: 4 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = collectActiveLorasFromChain(loader);
|
||||||
|
|
||||||
|
expect([...result]).toEqual(["Alpha", "Beta"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stops propagation when the combiner is inactive", () => {
|
||||||
|
const randomizer = {
|
||||||
|
id: 1,
|
||||||
|
comfyClass: "Lora Randomizer (LoraManager)",
|
||||||
|
mode: 0,
|
||||||
|
widgets: [
|
||||||
|
{
|
||||||
|
name: "loras",
|
||||||
|
value: [{ name: "Alpha", active: true }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
};
|
||||||
|
const combiner = {
|
||||||
|
id: 2,
|
||||||
|
comfyClass: "Lora Stack Combiner (LoraManager)",
|
||||||
|
mode: 2,
|
||||||
|
widgets: [],
|
||||||
|
inputs: [{ name: "lora_stack_a", type: "LORA_STACK", link: 21 }],
|
||||||
|
outputs: [],
|
||||||
|
};
|
||||||
|
const loader = {
|
||||||
|
id: 3,
|
||||||
|
comfyClass: "Lora Loader (LoraManager)",
|
||||||
|
mode: 0,
|
||||||
|
widgets: [],
|
||||||
|
inputs: [{ name: "lora_stack", type: "LORA_STACK", link: 22 }],
|
||||||
|
outputs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
createGraph(
|
||||||
|
[randomizer, combiner, loader],
|
||||||
|
{
|
||||||
|
21: { origin_id: 1, target_id: 2 },
|
||||||
|
22: { origin_id: 2, target_id: 3 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = collectActiveLorasFromChain(loader);
|
||||||
|
|
||||||
|
expect(result.size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,10 @@ import pytest
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.test_utils import make_mocked_request
|
from aiohttp.test_utils import make_mocked_request
|
||||||
|
|
||||||
from py.middleware.csp_middleware import REMOTE_MEDIA_SOURCES, relax_csp_for_remote_media
|
from py.middleware.csp_middleware import (
|
||||||
|
REMOTE_MEDIA_SOURCES,
|
||||||
|
relax_csp_for_remote_media,
|
||||||
|
)
|
||||||
|
|
||||||
DEFAULT_CSP = (
|
DEFAULT_CSP = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
@@ -40,7 +43,9 @@ async def _invoke_middleware(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_relax_csp_appends_remote_sources_and_preserves_existing_directives() -> None:
|
async def test_relax_csp_appends_remote_sources_and_preserves_existing_directives() -> (
|
||||||
|
None
|
||||||
|
):
|
||||||
response = await _invoke_middleware("/some-path", web.Response())
|
response = await _invoke_middleware("/some-path", web.Response())
|
||||||
header_value = response.headers.get("Content-Security-Policy")
|
header_value = response.headers.get("Content-Security-Policy")
|
||||||
assert header_value is not None
|
assert header_value is not None
|
||||||
@@ -48,16 +53,17 @@ async def test_relax_csp_appends_remote_sources_and_preserves_existing_directive
|
|||||||
directives = _parse_directives(header_value)
|
directives = _parse_directives(header_value)
|
||||||
|
|
||||||
# Existing directives remain intact
|
# Existing directives remain intact
|
||||||
assert directives["script-src"] == ["'self'", "'unsafe-inline'", "'unsafe-eval'", "blob:"]
|
assert directives["script-src"] == [
|
||||||
|
"'self'",
|
||||||
|
"'unsafe-inline'",
|
||||||
|
"'unsafe-eval'",
|
||||||
|
"blob:",
|
||||||
|
]
|
||||||
assert directives["img-src"][:3] == ["'self'", "data:", "blob:"]
|
assert directives["img-src"][:3] == ["'self'", "data:", "blob:"]
|
||||||
|
|
||||||
# Remote media hosts are added once to the relevant directives
|
# Remote media hosts are added once to the relevant directives
|
||||||
for source in REMOTE_MEDIA_SOURCES:
|
for source in REMOTE_MEDIA_SOURCES:
|
||||||
assert source in directives["img-src"]
|
assert source in directives["img-src"]
|
||||||
|
|
||||||
assert "media-src" in directives
|
|
||||||
assert directives["media-src"][0] == "'self'"
|
|
||||||
for source in REMOTE_MEDIA_SOURCES:
|
|
||||||
assert source in directives["media-src"]
|
assert source in directives["media-src"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# serializer version: 1
|
# serializer version: 1
|
||||||
# name: TestModelLibraryHandlerSnapshots.test_check_model_exists_empty_response
|
# name: TestModelLibraryHandlerSnapshots.test_check_model_exists_empty_response
|
||||||
dict({
|
dict({
|
||||||
|
'downloadedVersionIds': list([
|
||||||
|
]),
|
||||||
'modelType': None,
|
'modelType': None,
|
||||||
'success': True,
|
'success': True,
|
||||||
'versions': list([
|
'versions': list([
|
||||||
|
|||||||
@@ -66,6 +66,27 @@ class FakePromptServer:
|
|||||||
instance = Instance()
|
instance = Instance()
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDownloadHistoryService:
|
||||||
|
async def has_been_downloaded(self, _model_type, _version_id):
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_downloaded_version_ids(self, _model_type, _model_id):
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_downloaded_version_ids_bulk(self, _model_type, _model_ids):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def mark_downloaded(self, *_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def mark_not_downloaded(self, *_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def fake_download_history_service_factory():
|
||||||
|
return FakeDownloadHistoryService()
|
||||||
|
|
||||||
|
|
||||||
class TestSettingsHandlerSnapshots:
|
class TestSettingsHandlerSnapshots:
|
||||||
"""Snapshot tests for SettingsHandler responses."""
|
"""Snapshot tests for SettingsHandler responses."""
|
||||||
|
|
||||||
@@ -223,6 +244,7 @@ class TestModelLibraryHandlerSnapshots:
|
|||||||
get_lora_scanner=scanner_factory,
|
get_lora_scanner=scanner_factory,
|
||||||
get_checkpoint_scanner=scanner_factory,
|
get_checkpoint_scanner=scanner_factory,
|
||||||
get_embedding_scanner=scanner_factory,
|
get_embedding_scanner=scanner_factory,
|
||||||
|
get_downloaded_version_history_service=fake_download_history_service_factory,
|
||||||
),
|
),
|
||||||
metadata_provider_factory=lambda: None,
|
metadata_provider_factory=lambda: None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ from py.routes.misc_routes import MiscRoutes
|
|||||||
|
|
||||||
|
|
||||||
class FakeRequest:
|
class FakeRequest:
|
||||||
def __init__(self, *, json_data=None, query=None):
|
def __init__(self, *, json_data=None, query=None, method="POST"):
|
||||||
self._json_data = json_data or {}
|
self._json_data = json_data or {}
|
||||||
self.query = query or {}
|
self.query = query or {}
|
||||||
|
self.method = method
|
||||||
|
|
||||||
async def json(self):
|
async def json(self):
|
||||||
return self._json_data
|
return self._json_data
|
||||||
@@ -438,6 +439,46 @@ async def fake_metadata_archive_manager_factory():
|
|||||||
return FakeMetadataArchiveManager()
|
return FakeMetadataArchiveManager()
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDownloadHistoryService:
|
||||||
|
def __init__(self, downloaded_by_type=None):
|
||||||
|
self.downloaded_by_type = downloaded_by_type or {}
|
||||||
|
self.marked_downloaded: list[tuple] = []
|
||||||
|
self.marked_not_downloaded: list[tuple] = []
|
||||||
|
|
||||||
|
async def has_been_downloaded(self, model_type, version_id):
|
||||||
|
return version_id in self.downloaded_by_type.get(model_type, set())
|
||||||
|
|
||||||
|
async def get_downloaded_version_ids(self, model_type, model_id):
|
||||||
|
entries = self.downloaded_by_type.get(model_type, {})
|
||||||
|
if isinstance(entries, dict):
|
||||||
|
return sorted(entries.get(model_id, set()))
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_downloaded_version_ids_bulk(self, model_type, model_ids):
|
||||||
|
entries = self.downloaded_by_type.get(model_type, {})
|
||||||
|
if not isinstance(entries, dict):
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
model_id: set(entries.get(model_id, set()))
|
||||||
|
for model_id in model_ids
|
||||||
|
if model_id in entries
|
||||||
|
}
|
||||||
|
|
||||||
|
async def mark_downloaded(
|
||||||
|
self, model_type, version_id, *, model_id=None, source="manual", file_path=None
|
||||||
|
):
|
||||||
|
self.marked_downloaded.append(
|
||||||
|
(model_type, version_id, model_id, source, file_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def mark_not_downloaded(self, model_type, version_id):
|
||||||
|
self.marked_not_downloaded.append((model_type, version_id))
|
||||||
|
|
||||||
|
|
||||||
|
async def fake_download_history_service_factory():
|
||||||
|
return FakeDownloadHistoryService()
|
||||||
|
|
||||||
|
|
||||||
class RecordingRegistrar:
|
class RecordingRegistrar:
|
||||||
def __init__(self, _app):
|
def __init__(self, _app):
|
||||||
self.registered_mapping = None
|
self.registered_mapping = None
|
||||||
@@ -452,6 +493,7 @@ async def test_misc_routes_bind_produces_expected_handlers():
|
|||||||
get_lora_scanner=fake_scanner_factory,
|
get_lora_scanner=fake_scanner_factory,
|
||||||
get_checkpoint_scanner=fake_scanner_factory,
|
get_checkpoint_scanner=fake_scanner_factory,
|
||||||
get_embedding_scanner=fake_scanner_factory,
|
get_embedding_scanner=fake_scanner_factory,
|
||||||
|
get_downloaded_version_history_service=fake_download_history_service_factory,
|
||||||
)
|
)
|
||||||
|
|
||||||
recorded_registrars = []
|
recorded_registrars = []
|
||||||
@@ -578,6 +620,7 @@ async def test_get_civitai_user_models_marks_library_versions():
|
|||||||
get_lora_scanner=lora_factory,
|
get_lora_scanner=lora_factory,
|
||||||
get_checkpoint_scanner=checkpoint_factory,
|
get_checkpoint_scanner=checkpoint_factory,
|
||||||
get_embedding_scanner=embedding_factory,
|
get_embedding_scanner=embedding_factory,
|
||||||
|
get_downloaded_version_history_service=lambda: fake_download_history_service_factory(),
|
||||||
),
|
),
|
||||||
metadata_provider_factory=provider_factory,
|
metadata_provider_factory=provider_factory,
|
||||||
)
|
)
|
||||||
@@ -600,6 +643,7 @@ async def test_get_civitai_user_models_marks_library_versions():
|
|||||||
"baseModel": "Flux.1",
|
"baseModel": "Flux.1",
|
||||||
"thumbnailUrl": "http://example.com/a1.jpg",
|
"thumbnailUrl": "http://example.com/a1.jpg",
|
||||||
"inLibrary": False,
|
"inLibrary": False,
|
||||||
|
"hasBeenDownloaded": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"modelId": 1,
|
"modelId": 1,
|
||||||
@@ -611,6 +655,7 @@ async def test_get_civitai_user_models_marks_library_versions():
|
|||||||
"baseModel": "Flux.1",
|
"baseModel": "Flux.1",
|
||||||
"thumbnailUrl": "http://example.com/a2.jpg",
|
"thumbnailUrl": "http://example.com/a2.jpg",
|
||||||
"inLibrary": True,
|
"inLibrary": True,
|
||||||
|
"hasBeenDownloaded": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"modelId": 2,
|
"modelId": 2,
|
||||||
@@ -622,6 +667,7 @@ async def test_get_civitai_user_models_marks_library_versions():
|
|||||||
"baseModel": None,
|
"baseModel": None,
|
||||||
"thumbnailUrl": "http://example.com/e1.jpg",
|
"thumbnailUrl": "http://example.com/e1.jpg",
|
||||||
"inLibrary": False,
|
"inLibrary": False,
|
||||||
|
"hasBeenDownloaded": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"modelId": 2,
|
"modelId": 2,
|
||||||
@@ -633,6 +679,7 @@ async def test_get_civitai_user_models_marks_library_versions():
|
|||||||
"baseModel": None,
|
"baseModel": None,
|
||||||
"thumbnailUrl": None,
|
"thumbnailUrl": None,
|
||||||
"inLibrary": True,
|
"inLibrary": True,
|
||||||
|
"hasBeenDownloaded": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"modelId": 3,
|
"modelId": 3,
|
||||||
@@ -644,6 +691,7 @@ async def test_get_civitai_user_models_marks_library_versions():
|
|||||||
"baseModel": "SDXL",
|
"baseModel": "SDXL",
|
||||||
"thumbnailUrl": None,
|
"thumbnailUrl": None,
|
||||||
"inLibrary": False,
|
"inLibrary": False,
|
||||||
|
"hasBeenDownloaded": False,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -692,6 +740,7 @@ async def test_get_civitai_user_models_rewrites_civitai_previews():
|
|||||||
get_lora_scanner=fake_scanner_factory,
|
get_lora_scanner=fake_scanner_factory,
|
||||||
get_checkpoint_scanner=fake_scanner_factory,
|
get_checkpoint_scanner=fake_scanner_factory,
|
||||||
get_embedding_scanner=fake_scanner_factory,
|
get_embedding_scanner=fake_scanner_factory,
|
||||||
|
get_downloaded_version_history_service=fake_download_history_service_factory,
|
||||||
),
|
),
|
||||||
metadata_provider_factory=provider_factory,
|
metadata_provider_factory=provider_factory,
|
||||||
)
|
)
|
||||||
@@ -727,6 +776,7 @@ async def test_get_civitai_user_models_requires_username():
|
|||||||
get_lora_scanner=fake_scanner_factory,
|
get_lora_scanner=fake_scanner_factory,
|
||||||
get_checkpoint_scanner=fake_scanner_factory,
|
get_checkpoint_scanner=fake_scanner_factory,
|
||||||
get_embedding_scanner=fake_scanner_factory,
|
get_embedding_scanner=fake_scanner_factory,
|
||||||
|
get_downloaded_version_history_service=fake_download_history_service_factory,
|
||||||
),
|
),
|
||||||
metadata_provider_factory=provider_factory,
|
metadata_provider_factory=provider_factory,
|
||||||
)
|
)
|
||||||
@@ -760,6 +810,7 @@ def test_ensure_handler_mapping_caches_result():
|
|||||||
get_lora_scanner=fake_scanner_factory,
|
get_lora_scanner=fake_scanner_factory,
|
||||||
get_checkpoint_scanner=fake_scanner_factory,
|
get_checkpoint_scanner=fake_scanner_factory,
|
||||||
get_embedding_scanner=fake_scanner_factory,
|
get_embedding_scanner=fake_scanner_factory,
|
||||||
|
get_downloaded_version_history_service=fake_download_history_service_factory,
|
||||||
),
|
),
|
||||||
metadata_provider_factory=fake_metadata_provider_factory,
|
metadata_provider_factory=fake_metadata_provider_factory,
|
||||||
metadata_archive_manager_factory=fake_metadata_archive_manager_factory,
|
metadata_archive_manager_factory=fake_metadata_archive_manager_factory,
|
||||||
@@ -802,6 +853,7 @@ async def test_check_model_exists_returns_local_versions():
|
|||||||
get_lora_scanner=lora_factory,
|
get_lora_scanner=lora_factory,
|
||||||
get_checkpoint_scanner=checkpoint_factory,
|
get_checkpoint_scanner=checkpoint_factory,
|
||||||
get_embedding_scanner=embedding_factory,
|
get_embedding_scanner=embedding_factory,
|
||||||
|
get_downloaded_version_history_service=fake_download_history_service_factory,
|
||||||
),
|
),
|
||||||
metadata_provider_factory=fake_metadata_provider_factory,
|
metadata_provider_factory=fake_metadata_provider_factory,
|
||||||
)
|
)
|
||||||
@@ -811,10 +863,139 @@ async def test_check_model_exists_returns_local_versions():
|
|||||||
|
|
||||||
assert payload["success"] is True
|
assert payload["success"] is True
|
||||||
assert payload["modelType"] == "lora"
|
assert payload["modelType"] == "lora"
|
||||||
assert payload["versions"] == versions
|
assert payload["versions"] == [
|
||||||
|
{"versionId": 11, "name": "v1", "fileName": "model-one", "hasBeenDownloaded": True},
|
||||||
|
{"versionId": 12, "name": "v2", "fileName": "model-two", "hasBeenDownloaded": True},
|
||||||
|
]
|
||||||
assert lora_scanner.version_calls == [5]
|
assert lora_scanner.version_calls == [5]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_model_exists_model_id_only_does_not_call_metadata_provider():
|
||||||
|
async def metadata_provider_factory():
|
||||||
|
raise AssertionError("metadata provider should not be called for modelId-only checks")
|
||||||
|
|
||||||
|
handler = ModelLibraryHandler(
|
||||||
|
ServiceRegistryAdapter(
|
||||||
|
get_lora_scanner=fake_scanner_factory,
|
||||||
|
get_checkpoint_scanner=fake_scanner_factory,
|
||||||
|
get_embedding_scanner=fake_scanner_factory,
|
||||||
|
get_downloaded_version_history_service=fake_download_history_service_factory,
|
||||||
|
),
|
||||||
|
metadata_provider_factory=metadata_provider_factory,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await handler.check_model_exists(FakeRequest(query={"modelId": "5"}))
|
||||||
|
payload = json.loads(response.text)
|
||||||
|
|
||||||
|
assert payload == {
|
||||||
|
"success": True,
|
||||||
|
"modelType": None,
|
||||||
|
"versions": [],
|
||||||
|
"downloadedVersionIds": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_model_exists_returns_download_history_when_file_missing():
|
||||||
|
history_service = FakeDownloadHistoryService({"checkpoint": {999}})
|
||||||
|
|
||||||
|
async def history_factory():
|
||||||
|
return history_service
|
||||||
|
|
||||||
|
handler = ModelLibraryHandler(
|
||||||
|
ServiceRegistryAdapter(
|
||||||
|
get_lora_scanner=fake_scanner_factory,
|
||||||
|
get_checkpoint_scanner=fake_scanner_factory,
|
||||||
|
get_embedding_scanner=fake_scanner_factory,
|
||||||
|
get_downloaded_version_history_service=history_factory,
|
||||||
|
),
|
||||||
|
metadata_provider_factory=fake_metadata_provider_factory,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await handler.check_model_exists(
|
||||||
|
FakeRequest(query={"modelId": "5", "modelVersionId": "999"})
|
||||||
|
)
|
||||||
|
payload = json.loads(response.text)
|
||||||
|
|
||||||
|
assert payload == {
|
||||||
|
"success": True,
|
||||||
|
"exists": False,
|
||||||
|
"modelType": "checkpoint",
|
||||||
|
"hasBeenDownloaded": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_model_version_download_status_endpoints():
|
||||||
|
history_service = FakeDownloadHistoryService({"lora": {123}})
|
||||||
|
|
||||||
|
async def history_factory():
|
||||||
|
return history_service
|
||||||
|
|
||||||
|
handler = ModelLibraryHandler(
|
||||||
|
ServiceRegistryAdapter(
|
||||||
|
get_lora_scanner=fake_scanner_factory,
|
||||||
|
get_checkpoint_scanner=fake_scanner_factory,
|
||||||
|
get_embedding_scanner=fake_scanner_factory,
|
||||||
|
get_downloaded_version_history_service=history_factory,
|
||||||
|
),
|
||||||
|
metadata_provider_factory=fake_metadata_provider_factory,
|
||||||
|
)
|
||||||
|
|
||||||
|
get_response = await handler.get_model_version_download_status(
|
||||||
|
FakeRequest(query={"modelType": "lora", "modelVersionId": "123"})
|
||||||
|
)
|
||||||
|
get_payload = json.loads(get_response.text)
|
||||||
|
assert get_payload == {
|
||||||
|
"success": True,
|
||||||
|
"modelType": "lora",
|
||||||
|
"modelVersionId": 123,
|
||||||
|
"hasBeenDownloaded": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
set_response = await handler.set_model_version_download_status(
|
||||||
|
FakeRequest(
|
||||||
|
json_data={
|
||||||
|
"modelType": "checkpoint",
|
||||||
|
"modelVersionId": 456,
|
||||||
|
"modelId": 78,
|
||||||
|
"downloaded": True,
|
||||||
|
"filePath": "/tmp/model.safetensors",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
set_payload = json.loads(set_response.text)
|
||||||
|
assert set_payload == {
|
||||||
|
"success": True,
|
||||||
|
"modelType": "checkpoint",
|
||||||
|
"modelVersionId": 456,
|
||||||
|
"hasBeenDownloaded": True,
|
||||||
|
}
|
||||||
|
assert history_service.marked_downloaded == [
|
||||||
|
("checkpoint", 456, 78, "manual", "/tmp/model.safetensors")
|
||||||
|
]
|
||||||
|
|
||||||
|
set_get_response = await handler.set_model_version_download_status(
|
||||||
|
FakeRequest(
|
||||||
|
method="GET",
|
||||||
|
query={
|
||||||
|
"modelType": "embedding",
|
||||||
|
"modelVersionId": "789",
|
||||||
|
"modelId": "12",
|
||||||
|
"downloaded": "false",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
set_get_payload = json.loads(set_get_response.text)
|
||||||
|
assert set_get_payload == {
|
||||||
|
"success": True,
|
||||||
|
"modelType": "embedding",
|
||||||
|
"modelVersionId": 789,
|
||||||
|
"hasBeenDownloaded": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_create_handler_set_uses_provided_dependencies():
|
def test_create_handler_set_uses_provided_dependencies():
|
||||||
recorded_handlers: list[dict] = []
|
recorded_handlers: list[dict] = []
|
||||||
|
|
||||||
@@ -845,6 +1026,7 @@ def test_create_handler_set_uses_provided_dependencies():
|
|||||||
get_lora_scanner=fake_scanner_factory,
|
get_lora_scanner=fake_scanner_factory,
|
||||||
get_checkpoint_scanner=fake_scanner_factory,
|
get_checkpoint_scanner=fake_scanner_factory,
|
||||||
get_embedding_scanner=fake_scanner_factory,
|
get_embedding_scanner=fake_scanner_factory,
|
||||||
|
get_downloaded_version_history_service=fake_download_history_service_factory,
|
||||||
),
|
),
|
||||||
metadata_provider_factory=fake_metadata_provider_factory,
|
metadata_provider_factory=fake_metadata_provider_factory,
|
||||||
metadata_archive_manager_factory=fake_metadata_archive_manager_factory,
|
metadata_archive_manager_factory=fake_metadata_archive_manager_factory,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ def isolate_settings(monkeypatch, tmp_path):
|
|||||||
"embedding": "{base_model}/{first_tag}",
|
"embedding": "{base_model}/{first_tag}",
|
||||||
},
|
},
|
||||||
"base_model_path_mappings": {"BaseModel": "MappedModel"},
|
"base_model_path_mappings": {"BaseModel": "MappedModel"},
|
||||||
|
"skip_previously_downloaded_model_versions": False,
|
||||||
"download_skip_base_models": [],
|
"download_skip_base_models": [],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -454,7 +455,7 @@ async def test_download_skips_excluded_base_model(monkeypatch, scanners, metadat
|
|||||||
|
|
||||||
metadata_provider.get_model_version = AsyncMock(
|
metadata_provider.get_model_version = AsyncMock(
|
||||||
return_value={
|
return_value={
|
||||||
"id": 42,
|
"id": 99,
|
||||||
"model": {"type": "LoRA", "tags": ["fantasy"]},
|
"model": {"type": "LoRA", "tags": ["fantasy"]},
|
||||||
"baseModel": "SDXL 1.0",
|
"baseModel": "SDXL 1.0",
|
||||||
"creator": {"username": "Author"},
|
"creator": {"username": "Author"},
|
||||||
@@ -490,3 +491,104 @@ async def test_download_skips_excluded_base_model(monkeypatch, scanners, metadat
|
|||||||
assert "file.safetensors" in result["message"]
|
assert "file.safetensors" in result["message"]
|
||||||
execute_download.assert_not_called()
|
execute_download.assert_not_called()
|
||||||
assert manager._active_downloads[result["download_id"]]["status"] == "skipped"
|
assert manager._active_downloads[result["download_id"]]["status"] == "skipped"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_skips_previously_downloaded_version(monkeypatch, scanners, metadata_provider):
|
||||||
|
manager = DownloadManager()
|
||||||
|
get_settings_manager().settings["skip_previously_downloaded_model_versions"] = True
|
||||||
|
|
||||||
|
metadata_provider.get_model_version = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"id": 42,
|
||||||
|
"model": {"type": "LoRA", "tags": ["fantasy"]},
|
||||||
|
"baseModel": "SDXL 1.0",
|
||||||
|
"creator": {"username": "Author"},
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"type": "Model",
|
||||||
|
"primary": True,
|
||||||
|
"downloadUrl": "https://example.invalid/file.safetensors",
|
||||||
|
"name": "file.safetensors",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
history_service = AsyncMock()
|
||||||
|
history_service.has_been_downloaded = AsyncMock(return_value=True)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
ServiceRegistry,
|
||||||
|
"get_downloaded_version_history_service",
|
||||||
|
AsyncMock(return_value=history_service),
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_download = AsyncMock()
|
||||||
|
monkeypatch.setattr(
|
||||||
|
DownloadManager, "_execute_download", execute_download, raising=False
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await manager.download_from_civitai(
|
||||||
|
model_version_id=99,
|
||||||
|
use_default_paths=True,
|
||||||
|
progress_callback=None,
|
||||||
|
source=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["skipped"] is True
|
||||||
|
assert result["status"] == "skipped"
|
||||||
|
assert result["reason"] == "previously_downloaded_version"
|
||||||
|
assert result["model_version_id"] == 99
|
||||||
|
assert result["file_name"] == "file.safetensors"
|
||||||
|
history_service.has_been_downloaded.assert_awaited_once_with("lora", 99)
|
||||||
|
execute_download.assert_not_called()
|
||||||
|
assert manager._active_downloads[result["download_id"]]["status"] == "skipped"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_proceeds_when_history_skip_disabled(monkeypatch, scanners, metadata_provider):
|
||||||
|
manager = DownloadManager()
|
||||||
|
get_settings_manager().settings["skip_previously_downloaded_model_versions"] = False
|
||||||
|
|
||||||
|
metadata_provider.get_model_version = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"id": 42,
|
||||||
|
"model": {"type": "LoRA", "tags": ["fantasy"]},
|
||||||
|
"baseModel": "SDXL 1.0",
|
||||||
|
"creator": {"username": "Author"},
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"type": "Model",
|
||||||
|
"primary": True,
|
||||||
|
"downloadUrl": "https://example.invalid/file.safetensors",
|
||||||
|
"name": "file.safetensors",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
history_service = AsyncMock()
|
||||||
|
history_service.has_been_downloaded = AsyncMock(return_value=True)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
ServiceRegistry,
|
||||||
|
"get_downloaded_version_history_service",
|
||||||
|
AsyncMock(return_value=history_service),
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_download = AsyncMock(return_value={"success": True, "download_id": "done"})
|
||||||
|
monkeypatch.setattr(
|
||||||
|
DownloadManager, "_execute_download", execute_download, raising=False
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await manager.download_from_civitai(
|
||||||
|
model_version_id=99,
|
||||||
|
use_default_paths=True,
|
||||||
|
progress_callback=None,
|
||||||
|
source=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result.get("skipped") is not True
|
||||||
|
history_service.has_been_downloaded.assert_not_called()
|
||||||
|
execute_download.assert_awaited_once()
|
||||||
|
|||||||
70
tests/services/test_downloaded_version_history_service.py
Normal file
70
tests/services/test_downloaded_version_history_service.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from py.services.downloaded_version_history_service import (
|
||||||
|
DownloadedVersionHistoryService,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DummySettings:
|
||||||
|
def get_active_library_name(self) -> str:
|
||||||
|
return "alpha"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_history_roundtrip_and_manual_override(tmp_path: Path) -> None:
|
||||||
|
db_path = tmp_path / "download-history.sqlite"
|
||||||
|
service = DownloadedVersionHistoryService(
|
||||||
|
str(db_path),
|
||||||
|
settings_manager=DummySettings(),
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.mark_downloaded(
|
||||||
|
"lora",
|
||||||
|
101,
|
||||||
|
model_id=11,
|
||||||
|
source="scan",
|
||||||
|
file_path="/models/a.safetensors",
|
||||||
|
)
|
||||||
|
assert await service.has_been_downloaded("lora", 101) is True
|
||||||
|
assert await service.get_downloaded_version_ids("lora", 11) == [101]
|
||||||
|
|
||||||
|
await service.mark_not_downloaded("lora", 101)
|
||||||
|
assert await service.has_been_downloaded("lora", 101) is False
|
||||||
|
assert await service.get_downloaded_version_ids("lora", 11) == []
|
||||||
|
|
||||||
|
await service.mark_downloaded(
|
||||||
|
"lora",
|
||||||
|
101,
|
||||||
|
model_id=11,
|
||||||
|
source="download",
|
||||||
|
file_path="/models/a.safetensors",
|
||||||
|
)
|
||||||
|
assert await service.has_been_downloaded("lora", 101) is True
|
||||||
|
assert await service.get_downloaded_version_ids("lora", 11) == [101]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_history_bulk_lookup(tmp_path: Path) -> None:
|
||||||
|
db_path = tmp_path / "download-history.sqlite"
|
||||||
|
service = DownloadedVersionHistoryService(
|
||||||
|
str(db_path),
|
||||||
|
settings_manager=DummySettings(),
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.mark_downloaded_bulk(
|
||||||
|
"checkpoint",
|
||||||
|
[
|
||||||
|
{"model_id": 5, "version_id": 501, "file_path": "/m/one.safetensors"},
|
||||||
|
{"model_id": 5, "version_id": 502, "file_path": "/m/two.safetensors"},
|
||||||
|
{"model_id": 6, "version_id": 601, "file_path": "/m/three.safetensors"},
|
||||||
|
],
|
||||||
|
source="scan",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert await service.get_downloaded_version_ids("checkpoint", 5) == [501, 502]
|
||||||
|
assert await service.get_downloaded_version_ids_bulk("checkpoint", [5, 6, 7]) == {
|
||||||
|
5: {501, 502},
|
||||||
|
6: {601},
|
||||||
|
}
|
||||||
@@ -17,7 +17,9 @@ def test_portable_settings_use_project_root(tmp_path, monkeypatch):
|
|||||||
from importlib import reload
|
from importlib import reload
|
||||||
|
|
||||||
settings_paths_module = reload(settings_paths)
|
settings_paths_module = reload(settings_paths)
|
||||||
monkeypatch.setattr(settings_paths_module, "get_project_root", lambda: str(tmp_path))
|
monkeypatch.setattr(
|
||||||
|
settings_paths_module, "get_project_root", lambda: str(tmp_path)
|
||||||
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
settings_paths_module,
|
settings_paths_module,
|
||||||
"user_config_dir",
|
"user_config_dir",
|
||||||
@@ -25,7 +27,9 @@ def test_portable_settings_use_project_root(tmp_path, monkeypatch):
|
|||||||
)
|
)
|
||||||
|
|
||||||
portable_settings = {"use_portable_settings": True}
|
portable_settings = {"use_portable_settings": True}
|
||||||
(tmp_path / "settings.json").write_text(json.dumps(portable_settings), encoding="utf-8")
|
(tmp_path / "settings.json").write_text(
|
||||||
|
json.dumps(portable_settings), encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
config_dir = settings_paths_module.get_settings_dir(create=True)
|
config_dir = settings_paths_module.get_settings_dir(create=True)
|
||||||
assert config_dir == str(tmp_path)
|
assert config_dir == str(tmp_path)
|
||||||
@@ -74,7 +78,9 @@ def test_initial_save_persists_minimal_template(tmp_path, monkeypatch):
|
|||||||
self._seed_template = copy.deepcopy(template)
|
self._seed_template = copy.deepcopy(template)
|
||||||
return copy.deepcopy(template)
|
return copy.deepcopy(template)
|
||||||
|
|
||||||
monkeypatch.setattr(SettingsManager, "_load_settings_template", fake_template_loader)
|
monkeypatch.setattr(
|
||||||
|
SettingsManager, "_load_settings_template", fake_template_loader
|
||||||
|
)
|
||||||
|
|
||||||
manager = SettingsManager()
|
manager = SettingsManager()
|
||||||
|
|
||||||
@@ -118,7 +124,10 @@ def test_existing_folder_paths_seed_default_library(tmp_path, monkeypatch):
|
|||||||
assert "default" in libraries
|
assert "default" in libraries
|
||||||
assert libraries["default"]["folder_paths"]["loras"] == [str(lora_dir)]
|
assert libraries["default"]["folder_paths"]["loras"] == [str(lora_dir)]
|
||||||
assert libraries["default"]["folder_paths"]["checkpoints"] == [str(checkpoint_dir)]
|
assert libraries["default"]["folder_paths"]["checkpoints"] == [str(checkpoint_dir)]
|
||||||
assert libraries["default"]["folder_paths"]["unet"] == [str(diffusion_dir), str(unet_dir)]
|
assert libraries["default"]["folder_paths"]["unet"] == [
|
||||||
|
str(diffusion_dir),
|
||||||
|
str(unet_dir),
|
||||||
|
]
|
||||||
assert libraries["default"]["folder_paths"]["embeddings"] == [str(embedding_dir)]
|
assert libraries["default"]["folder_paths"]["embeddings"] == [str(embedding_dir)]
|
||||||
|
|
||||||
assert manager.get_startup_messages() == []
|
assert manager.get_startup_messages() == []
|
||||||
@@ -138,7 +147,9 @@ def test_environment_variable_overrides_settings(tmp_path, monkeypatch):
|
|||||||
assert mgr.get("civitai_api_key") == "secret"
|
assert mgr.get("civitai_api_key") == "secret"
|
||||||
|
|
||||||
|
|
||||||
def _create_manager_with_settings(tmp_path, monkeypatch, initial_settings, *, save_spy=None):
|
def _create_manager_with_settings(
|
||||||
|
tmp_path, monkeypatch, initial_settings, *, save_spy=None
|
||||||
|
):
|
||||||
"""Helper to instantiate SettingsManager with predefined settings."""
|
"""Helper to instantiate SettingsManager with predefined settings."""
|
||||||
|
|
||||||
fake_settings_path = tmp_path / "settings.json"
|
fake_settings_path = tmp_path / "settings.json"
|
||||||
@@ -203,7 +214,9 @@ def test_switch_to_portable_mode_copies_cache(tmp_path, monkeypatch):
|
|||||||
assert manager.settings_file == str(project_root / "settings.json")
|
assert manager.settings_file == str(project_root / "settings.json")
|
||||||
marker_copy = project_root / "model_cache" / "user_marker.txt"
|
marker_copy = project_root / "model_cache" / "user_marker.txt"
|
||||||
assert marker_copy.read_text(encoding="utf-8") == "user_marker.txt"
|
assert marker_copy.read_text(encoding="utf-8") == "user_marker.txt"
|
||||||
assert (project_root / "model_cache.sqlite").read_text(encoding="utf-8") == "user_db"
|
assert (project_root / "model_cache.sqlite").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
) == "user_db"
|
||||||
assert user_settings.exists()
|
assert user_settings.exists()
|
||||||
|
|
||||||
|
|
||||||
@@ -216,13 +229,17 @@ def test_switching_back_to_user_config_moves_cache(tmp_path, monkeypatch):
|
|||||||
|
|
||||||
project_cache_dir = project_root / "model_cache"
|
project_cache_dir = project_root / "model_cache"
|
||||||
project_cache_dir.mkdir(exist_ok=True)
|
project_cache_dir.mkdir(exist_ok=True)
|
||||||
(project_cache_dir / "project_marker.txt").write_text("project_marker", encoding="utf-8")
|
(project_cache_dir / "project_marker.txt").write_text(
|
||||||
|
"project_marker", encoding="utf-8"
|
||||||
|
)
|
||||||
(project_root / "model_cache.sqlite").write_text("project_db", encoding="utf-8")
|
(project_root / "model_cache.sqlite").write_text("project_db", encoding="utf-8")
|
||||||
|
|
||||||
manager.set("use_portable_settings", False)
|
manager.set("use_portable_settings", False)
|
||||||
|
|
||||||
assert manager.settings_file == str(user_settings)
|
assert manager.settings_file == str(user_settings)
|
||||||
assert (user_dir / "model_cache" / "project_marker.txt").read_text(encoding="utf-8") == "project_marker"
|
assert (user_dir / "model_cache" / "project_marker.txt").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
) == "project_marker"
|
||||||
assert (user_dir / "model_cache.sqlite").read_text(encoding="utf-8") == "project_db"
|
assert (user_dir / "model_cache.sqlite").read_text(encoding="utf-8") == "project_db"
|
||||||
|
|
||||||
|
|
||||||
@@ -242,10 +259,19 @@ def test_download_path_template_invalid_json(manager):
|
|||||||
template = manager.get_download_path_template("checkpoint")
|
template = manager.get_download_path_template("checkpoint")
|
||||||
|
|
||||||
assert template == "{base_model}/{first_tag}"
|
assert template == "{base_model}/{first_tag}"
|
||||||
assert manager.settings["download_path_templates"]["lora"] == "{base_model}/{first_tag}"
|
assert (
|
||||||
|
manager.settings["download_path_templates"]["lora"]
|
||||||
|
== "{base_model}/{first_tag}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_auto_set_default_roots(manager):
|
def test_auto_set_default_roots(manager):
|
||||||
|
# Clear any previously auto-set values to test fresh behavior
|
||||||
|
manager.settings["default_lora_root"] = ""
|
||||||
|
manager.settings["default_checkpoint_root"] = ""
|
||||||
|
manager.settings["default_embedding_root"] = ""
|
||||||
|
manager.settings["default_unet_root"] = ""
|
||||||
|
|
||||||
manager.settings["folder_paths"] = {
|
manager.settings["folder_paths"] = {
|
||||||
"loras": ["/loras"],
|
"loras": ["/loras"],
|
||||||
"checkpoints": ["/checkpoints"],
|
"checkpoints": ["/checkpoints"],
|
||||||
@@ -259,6 +285,48 @@ def test_auto_set_default_roots(manager):
|
|||||||
assert manager.get("default_embedding_root") == "/embeddings"
|
assert manager.get("default_embedding_root") == "/embeddings"
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_set_default_roots_repairs_stale_values(manager):
|
||||||
|
manager.settings["default_lora_root"] = "/stale-lora"
|
||||||
|
manager.settings["default_checkpoint_root"] = "/stale-checkpoint"
|
||||||
|
manager.settings["default_embedding_root"] = "/stale-embedding"
|
||||||
|
manager.settings["default_unet_root"] = "/stale-unet"
|
||||||
|
|
||||||
|
manager.settings["folder_paths"] = {
|
||||||
|
"loras": ["/loras"],
|
||||||
|
"checkpoints": ["/checkpoints"],
|
||||||
|
"unet": ["/unet"],
|
||||||
|
"embeddings": ["/embeddings"],
|
||||||
|
}
|
||||||
|
|
||||||
|
manager._auto_set_default_roots()
|
||||||
|
|
||||||
|
assert manager.get("default_lora_root") == "/loras"
|
||||||
|
assert manager.get("default_checkpoint_root") == "/checkpoints"
|
||||||
|
assert manager.get("default_unet_root") == "/unet"
|
||||||
|
assert manager.get("default_embedding_root") == "/embeddings"
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_set_default_roots_keeps_valid_values(manager):
|
||||||
|
manager.settings["default_lora_root"] = "/loras"
|
||||||
|
manager.settings["default_checkpoint_root"] = "/checkpoints"
|
||||||
|
manager.settings["default_embedding_root"] = "/embeddings"
|
||||||
|
manager.settings["default_unet_root"] = "/unet"
|
||||||
|
|
||||||
|
manager.settings["folder_paths"] = {
|
||||||
|
"loras": ["/loras", "/other-loras"],
|
||||||
|
"checkpoints": ["/checkpoints"],
|
||||||
|
"unet": ["/unet", "/other-unet"],
|
||||||
|
"embeddings": ["/embeddings"],
|
||||||
|
}
|
||||||
|
|
||||||
|
manager._auto_set_default_roots()
|
||||||
|
|
||||||
|
assert manager.get("default_lora_root") == "/loras"
|
||||||
|
assert manager.get("default_checkpoint_root") == "/checkpoints"
|
||||||
|
assert manager.get("default_unet_root") == "/unet"
|
||||||
|
assert manager.get("default_embedding_root") == "/embeddings"
|
||||||
|
|
||||||
|
|
||||||
def test_delete_setting(manager):
|
def test_delete_setting(manager):
|
||||||
manager.set("example", 1)
|
manager.set("example", 1)
|
||||||
manager.delete("example")
|
manager.delete("example")
|
||||||
@@ -293,7 +361,14 @@ def test_invalid_mature_blur_level_is_normalized_to_r(tmp_path, monkeypatch):
|
|||||||
|
|
||||||
def test_model_name_display_setting_notifies_scanners(tmp_path, monkeypatch):
|
def test_model_name_display_setting_notifies_scanners(tmp_path, monkeypatch):
|
||||||
initial = {
|
initial = {
|
||||||
"libraries": {"default": {"folder_paths": {}, "default_lora_root": "", "default_checkpoint_root": "", "default_embedding_root": ""}},
|
"libraries": {
|
||||||
|
"default": {
|
||||||
|
"folder_paths": {},
|
||||||
|
"default_lora_root": "",
|
||||||
|
"default_checkpoint_root": "",
|
||||||
|
"default_embedding_root": "",
|
||||||
|
}
|
||||||
|
},
|
||||||
"active_library": "default",
|
"active_library": "default",
|
||||||
"model_name_display": "model_name",
|
"model_name_display": "model_name",
|
||||||
}
|
}
|
||||||
@@ -315,6 +390,7 @@ def test_model_name_display_setting_notifies_scanners(tmp_path, monkeypatch):
|
|||||||
|
|
||||||
dispatched_loops = []
|
dispatched_loops = []
|
||||||
futures = []
|
futures = []
|
||||||
|
|
||||||
def tracking_run_coroutine_threadsafe(coro, target_loop):
|
def tracking_run_coroutine_threadsafe(coro, target_loop):
|
||||||
dispatched_loops.append(target_loop)
|
dispatched_loops.append(target_loop)
|
||||||
future = Future()
|
future = Future()
|
||||||
@@ -335,7 +411,9 @@ def test_model_name_display_setting_notifies_scanners(tmp_path, monkeypatch):
|
|||||||
"get_service_sync",
|
"get_service_sync",
|
||||||
classmethod(fake_get_service_sync),
|
classmethod(fake_get_service_sync),
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(asyncio, "run_coroutine_threadsafe", tracking_run_coroutine_threadsafe)
|
monkeypatch.setattr(
|
||||||
|
asyncio, "run_coroutine_threadsafe", tracking_run_coroutine_threadsafe
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manager.set("model_name_display", "file_name")
|
manager.set("model_name_display", "file_name")
|
||||||
@@ -354,12 +432,14 @@ def test_migrates_legacy_settings_file(tmp_path, monkeypatch):
|
|||||||
legacy_root = tmp_path / "legacy"
|
legacy_root = tmp_path / "legacy"
|
||||||
legacy_root.mkdir()
|
legacy_root.mkdir()
|
||||||
legacy_file = legacy_root / "settings.json"
|
legacy_file = legacy_root / "settings.json"
|
||||||
legacy_file.write_text("{\"value\": 1}", encoding="utf-8")
|
legacy_file.write_text('{"value": 1}', encoding="utf-8")
|
||||||
|
|
||||||
target_dir = tmp_path / "config"
|
target_dir = tmp_path / "config"
|
||||||
|
|
||||||
monkeypatch.setattr(settings_paths, "get_project_root", lambda: str(legacy_root))
|
monkeypatch.setattr(settings_paths, "get_project_root", lambda: str(legacy_root))
|
||||||
monkeypatch.setattr(settings_paths, "user_config_dir", lambda *_, **__: str(target_dir))
|
monkeypatch.setattr(
|
||||||
|
settings_paths, "user_config_dir", lambda *_, **__: str(target_dir)
|
||||||
|
)
|
||||||
|
|
||||||
migrated_path = settings_paths.ensure_settings_file()
|
migrated_path = settings_paths.ensure_settings_file()
|
||||||
|
|
||||||
@@ -380,7 +460,9 @@ def test_uses_portable_settings_file_when_enabled(tmp_path, monkeypatch):
|
|||||||
user_dir = tmp_path / "user"
|
user_dir = tmp_path / "user"
|
||||||
|
|
||||||
monkeypatch.setattr(settings_paths, "get_project_root", lambda: str(repo_root))
|
monkeypatch.setattr(settings_paths, "get_project_root", lambda: str(repo_root))
|
||||||
monkeypatch.setattr(settings_paths, "user_config_dir", lambda *_, **__: str(user_dir))
|
monkeypatch.setattr(
|
||||||
|
settings_paths, "user_config_dir", lambda *_, **__: str(user_dir)
|
||||||
|
)
|
||||||
|
|
||||||
resolved = settings_paths.ensure_settings_file()
|
resolved = settings_paths.ensure_settings_file()
|
||||||
|
|
||||||
@@ -393,7 +475,9 @@ def test_migrate_creates_default_library(manager):
|
|||||||
libraries = manager.get_libraries()
|
libraries = manager.get_libraries()
|
||||||
assert "default" in libraries
|
assert "default" in libraries
|
||||||
assert manager.get_active_library_name() == "default"
|
assert manager.get_active_library_name() == "default"
|
||||||
assert libraries["default"].get("folder_paths", {}) == manager.settings.get("folder_paths", {})
|
assert libraries["default"].get("folder_paths", {}) == manager.settings.get(
|
||||||
|
"folder_paths", {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_sanitizes_legacy_libraries(tmp_path, monkeypatch):
|
def test_migrate_sanitizes_legacy_libraries(tmp_path, monkeypatch):
|
||||||
@@ -464,12 +548,21 @@ def test_refresh_environment_variables_updates_stored_value(tmp_path, monkeypatc
|
|||||||
|
|
||||||
initial = {
|
initial = {
|
||||||
"civitai_api_key": "stale",
|
"civitai_api_key": "stale",
|
||||||
"libraries": {"default": {"folder_paths": {}, "default_lora_root": "", "default_checkpoint_root": "", "default_embedding_root": ""}},
|
"libraries": {
|
||||||
|
"default": {
|
||||||
|
"folder_paths": {},
|
||||||
|
"default_lora_root": "",
|
||||||
|
"default_checkpoint_root": "",
|
||||||
|
"default_embedding_root": "",
|
||||||
|
}
|
||||||
|
},
|
||||||
"active_library": "default",
|
"active_library": "default",
|
||||||
}
|
}
|
||||||
|
|
||||||
monkeypatch.setenv("CIVITAI_API_KEY", "from-init")
|
monkeypatch.setenv("CIVITAI_API_KEY", "from-init")
|
||||||
manager = _create_manager_with_settings(tmp_path, monkeypatch, initial, save_spy=save_spy)
|
manager = _create_manager_with_settings(
|
||||||
|
tmp_path, monkeypatch, initial, save_spy=save_spy
|
||||||
|
)
|
||||||
|
|
||||||
assert calls[-1] == "from-init"
|
assert calls[-1] == "from-init"
|
||||||
|
|
||||||
@@ -590,7 +683,9 @@ def test_extra_paths_validation_no_overlap_with_other_libraries(manager, tmp_pat
|
|||||||
manager.update_extra_folder_paths({"loras": [str(lora_dir1)]})
|
manager.update_extra_folder_paths({"loras": [str(lora_dir1)]})
|
||||||
|
|
||||||
|
|
||||||
def test_extra_paths_validation_no_overlap_with_active_primary_lora_root(manager, tmp_path):
|
def test_extra_paths_validation_no_overlap_with_active_primary_lora_root(
|
||||||
|
manager, tmp_path
|
||||||
|
):
|
||||||
"""Test that extra LoRA paths cannot overlap the active library primary LoRA roots."""
|
"""Test that extra LoRA paths cannot overlap the active library primary LoRA roots."""
|
||||||
real_lora_dir = tmp_path / "loras_real"
|
real_lora_dir = tmp_path / "loras_real"
|
||||||
real_lora_dir.mkdir()
|
real_lora_dir.mkdir()
|
||||||
@@ -603,7 +698,9 @@ def test_extra_paths_validation_no_overlap_with_active_primary_lora_root(manager
|
|||||||
activate=True,
|
activate=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="overlap with the active library's primary LoRA roots"):
|
with pytest.raises(
|
||||||
|
ValueError, match="overlap with the active library's primary LoRA roots"
|
||||||
|
):
|
||||||
manager.update_extra_folder_paths({"loras": [str(real_lora_dir)]})
|
manager.update_extra_folder_paths({"loras": [str(real_lora_dir)]})
|
||||||
|
|
||||||
|
|
||||||
@@ -627,7 +724,10 @@ def test_extra_paths_validation_no_overlap_with_active_primary_lora_root_case_in
|
|||||||
original_normcase = settings_manager_module.os.path.normcase
|
original_normcase = settings_manager_module.os.path.normcase
|
||||||
|
|
||||||
def fake_exists(path):
|
def fake_exists(path):
|
||||||
if isinstance(path, str) and path.lower() in {str(lora_link).lower(), str(real_lora_dir).lower()}:
|
if isinstance(path, str) and path.lower() in {
|
||||||
|
str(lora_link).lower(),
|
||||||
|
str(real_lora_dir).lower(),
|
||||||
|
}:
|
||||||
return True
|
return True
|
||||||
return original_exists(path)
|
return original_exists(path)
|
||||||
|
|
||||||
@@ -638,13 +738,21 @@ def test_extra_paths_validation_no_overlap_with_active_primary_lora_root_case_in
|
|||||||
|
|
||||||
monkeypatch.setattr(settings_manager_module.os.path, "exists", fake_exists)
|
monkeypatch.setattr(settings_manager_module.os.path, "exists", fake_exists)
|
||||||
monkeypatch.setattr(settings_manager_module.os.path, "realpath", fake_realpath)
|
monkeypatch.setattr(settings_manager_module.os.path, "realpath", fake_realpath)
|
||||||
monkeypatch.setattr(settings_manager_module.os.path, "normcase", lambda value: original_normcase(value).lower())
|
monkeypatch.setattr(
|
||||||
|
settings_manager_module.os.path,
|
||||||
|
"normcase",
|
||||||
|
lambda value: original_normcase(value).lower(),
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="overlap with the active library's primary LoRA roots"):
|
with pytest.raises(
|
||||||
|
ValueError, match="overlap with the active library's primary LoRA roots"
|
||||||
|
):
|
||||||
manager.update_extra_folder_paths({"loras": [str(real_lora_dir).upper()]})
|
manager.update_extra_folder_paths({"loras": [str(real_lora_dir).upper()]})
|
||||||
|
|
||||||
|
|
||||||
def test_extra_paths_validation_allows_missing_non_overlapping_lora_root(manager, tmp_path):
|
def test_extra_paths_validation_allows_missing_non_overlapping_lora_root(
|
||||||
|
manager, tmp_path
|
||||||
|
):
|
||||||
"""Missing non-overlapping extra LoRA paths should not be rejected."""
|
"""Missing non-overlapping extra LoRA paths should not be rejected."""
|
||||||
lora_dir = tmp_path / "loras"
|
lora_dir = tmp_path / "loras"
|
||||||
lora_dir.mkdir()
|
lora_dir.mkdir()
|
||||||
@@ -662,7 +770,9 @@ def test_extra_paths_validation_allows_missing_non_overlapping_lora_root(manager
|
|||||||
assert extra_paths["loras"] == [str(missing_extra)]
|
assert extra_paths["loras"] == [str(missing_extra)]
|
||||||
|
|
||||||
|
|
||||||
def test_extra_paths_validation_rejects_primary_root_first_level_symlink_target(manager, tmp_path):
|
def test_extra_paths_validation_rejects_primary_root_first_level_symlink_target(
|
||||||
|
manager, tmp_path
|
||||||
|
):
|
||||||
"""Extra LoRA paths should be rejected when already reachable via a first-level symlink under the primary root."""
|
"""Extra LoRA paths should be rejected when already reachable via a first-level symlink under the primary root."""
|
||||||
lora_dir = tmp_path / "loras"
|
lora_dir = tmp_path / "loras"
|
||||||
lora_dir.mkdir()
|
lora_dir.mkdir()
|
||||||
@@ -677,7 +787,9 @@ def test_extra_paths_validation_rejects_primary_root_first_level_symlink_target(
|
|||||||
activate=True,
|
activate=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="overlap with the active library's primary LoRA roots"):
|
with pytest.raises(
|
||||||
|
ValueError, match="overlap with the active library's primary LoRA roots"
|
||||||
|
):
|
||||||
manager.update_extra_folder_paths({"loras": [str(external_dir)]})
|
manager.update_extra_folder_paths({"loras": [str(external_dir)]})
|
||||||
|
|
||||||
|
|
||||||
@@ -698,7 +810,6 @@ def test_delete_library_switches_active(manager, tmp_path):
|
|||||||
assert manager.get_active_library_name() == "default"
|
assert manager.get_active_library_name() == "default"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_download_skip_base_models_are_normalized(manager):
|
def test_download_skip_base_models_are_normalized(manager):
|
||||||
manager.settings["download_skip_base_models"] = [
|
manager.settings["download_skip_base_models"] = [
|
||||||
"SDXL 1.0",
|
"SDXL 1.0",
|
||||||
@@ -715,9 +826,17 @@ def test_download_skip_base_models_are_normalized(manager):
|
|||||||
|
|
||||||
|
|
||||||
def test_setting_download_skip_base_models_normalizes_string_input(manager):
|
def test_setting_download_skip_base_models_normalizes_string_input(manager):
|
||||||
manager.set(
|
manager.set("download_skip_base_models", "SDXL 1.0, Pony; Invalid\nSDXL 1.0")
|
||||||
"download_skip_base_models",
|
|
||||||
"SDXL 1.0, Pony; Invalid\nSDXL 1.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert manager.get("download_skip_base_models") == ["SDXL 1.0", "Pony"]
|
assert manager.get("download_skip_base_models") == ["SDXL 1.0", "Pony"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_skip_previously_downloaded_model_versions_defaults_false(manager):
|
||||||
|
assert manager.get_skip_previously_downloaded_model_versions() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_skip_previously_downloaded_model_versions_coerces_string_input(manager):
|
||||||
|
manager.settings["skip_previously_downloaded_model_versions"] = "true"
|
||||||
|
|
||||||
|
assert manager.get_skip_previously_downloaded_model_versions() is True
|
||||||
|
assert manager.settings["skip_previously_downloaded_model_versions"] is True
|
||||||
|
|||||||
144
tests/utils/test_civitai_utils_rewrite.py
Normal file
144
tests/utils/test_civitai_utils_rewrite.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
"""Tests for CivitAI URL utilities."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from py.utils.civitai_utils import rewrite_preview_url
|
||||||
|
|
||||||
|
|
||||||
|
class TestRewritePreviewUrl:
|
||||||
|
"""Test cases for rewrite_preview_url function."""
|
||||||
|
|
||||||
|
def test_handles_none_input(self):
|
||||||
|
"""Should return (None, False) for None input."""
|
||||||
|
result, was_rewritten = rewrite_preview_url(None)
|
||||||
|
assert result is None
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_handles_empty_string(self):
|
||||||
|
"""Should return (empty_string, False) for empty input."""
|
||||||
|
result, was_rewritten = rewrite_preview_url("")
|
||||||
|
assert result == ""
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_handles_invalid_url(self):
|
||||||
|
"""Should return original URL and False for invalid URLs."""
|
||||||
|
invalid_url = "not-a-valid-url"
|
||||||
|
result, was_rewritten = rewrite_preview_url(invalid_url)
|
||||||
|
assert result == invalid_url
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_handles_url_without_scheme(self):
|
||||||
|
"""Should return original URL and False for URLs without scheme."""
|
||||||
|
url = "image.civitai.com/something"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url)
|
||||||
|
assert result == url
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_returns_false_for_non_civitai_domains(self):
|
||||||
|
"""Should not rewrite URLs from other domains."""
|
||||||
|
url = "https://example.com/image.jpg"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url)
|
||||||
|
assert result == url
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_returns_false_for_main_civitai_domain(self):
|
||||||
|
"""Should not rewrite URLs from main civitai.com domain."""
|
||||||
|
url = "https://civitai.com/images/123"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url)
|
||||||
|
assert result == url
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_rewrites_image_civitai_com_urls(self):
|
||||||
|
"""Should rewrite URLs from image.civitai.com."""
|
||||||
|
url = "https://image.civitai.com/checkpoints/original=true"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert (
|
||||||
|
result == "https://image.civitai.com/checkpoints/width=450,optimized=true"
|
||||||
|
)
|
||||||
|
assert was_rewritten is True
|
||||||
|
|
||||||
|
def test_rewrites_subdomain_civitai_urls(self):
|
||||||
|
"""Should rewrite URLs from CivitAI CDN subdomains like image-b2.civitai.com."""
|
||||||
|
url = "https://image-b2.civitai.com/file/civitai-media-cache/original=true/sample.png"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert (
|
||||||
|
result
|
||||||
|
== "https://image-b2.civitai.com/file/civitai-media-cache/width=450,optimized=true/sample.png"
|
||||||
|
)
|
||||||
|
assert was_rewritten is True
|
||||||
|
|
||||||
|
def test_rewrites_multiple_subdomains(self):
|
||||||
|
"""Should rewrite URLs from various CivitAI subdomains."""
|
||||||
|
test_cases = [
|
||||||
|
"https://image-b3.civitai.com/original=true/test.jpg",
|
||||||
|
"https://cdn.civitai.com/original=true/test.png",
|
||||||
|
]
|
||||||
|
for url in test_cases:
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert was_rewritten is True
|
||||||
|
assert "width=450,optimized=true" in result
|
||||||
|
|
||||||
|
def test_handles_urls_with_explicit_port(self):
|
||||||
|
"""Should correctly handle URLs with explicit port numbers."""
|
||||||
|
url = "https://image.civitai.com:443/checkpoints/original=true"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert was_rewritten is True
|
||||||
|
assert "width=450,optimized=true" in result
|
||||||
|
# Port is preserved in the URL (this is acceptable behavior)
|
||||||
|
assert ":443" in result
|
||||||
|
|
||||||
|
def test_rewrites_video_urls_with_transcode(self):
|
||||||
|
"""Should rewrite video URLs with transcode parameter."""
|
||||||
|
url = "https://image.civitai.com/videos/original=true/sample.mp4"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "video")
|
||||||
|
assert (
|
||||||
|
result
|
||||||
|
== "https://image.civitai.com/videos/transcode=true,width=450,optimized=true/sample.mp4"
|
||||||
|
)
|
||||||
|
assert was_rewritten is True
|
||||||
|
|
||||||
|
def test_video_rewrite_uses_case_insensitive_type(self):
|
||||||
|
"""Should handle video type case-insensitively."""
|
||||||
|
url = "https://image.civitai.com/original=true/test.mp4"
|
||||||
|
result1, was1 = rewrite_preview_url(url, "VIDEO")
|
||||||
|
result2, was2 = rewrite_preview_url(url, "Video")
|
||||||
|
assert was1 is True
|
||||||
|
assert was2 is True
|
||||||
|
assert "transcode=true" in result1
|
||||||
|
assert "transcode=true" in result2
|
||||||
|
|
||||||
|
def test_returns_original_when_no_original_true_in_path(self):
|
||||||
|
"""Should not rewrite URLs that don't contain /original=true."""
|
||||||
|
url = "https://image.civitai.com/checkpoints/optimized=true"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url)
|
||||||
|
assert result == url
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_preserves_path_structure_after_rewrite(self):
|
||||||
|
"""Should maintain path structure after rewriting."""
|
||||||
|
url = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.png"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert was_rewritten is True
|
||||||
|
assert result.startswith(
|
||||||
|
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/"
|
||||||
|
)
|
||||||
|
assert result.endswith("/12345.png")
|
||||||
|
|
||||||
|
def test_defaults_to_image_mode_when_media_type_is_none(self):
|
||||||
|
"""Should use image optimization when media_type is None."""
|
||||||
|
url = "https://image.civitai.com/original=true/test.png"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, None)
|
||||||
|
assert was_rewritten is True
|
||||||
|
assert "transcode=true" not in result
|
||||||
|
assert "width=450,optimized=true" in result
|
||||||
|
|
||||||
|
def test_case_insensitive_hostname_matching(self):
|
||||||
|
"""Should handle case-insensitive hostname matching."""
|
||||||
|
test_cases = [
|
||||||
|
"https://IMAGE.CIVITAI.COM/original=true/test.png",
|
||||||
|
"https://Image.Civitai.Com/original=true/test.png",
|
||||||
|
"https://image-b2.CIVITAI.com/original=true/test.png",
|
||||||
|
]
|
||||||
|
for url in test_cases:
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert was_rewritten is True, f"Failed for URL: {url}"
|
||||||
@@ -9,7 +9,7 @@ import type { LoraPoolConfig, RandomizerConfig, CyclerConfig } from './composabl
|
|||||||
import {
|
import {
|
||||||
setupModeChangeHandler,
|
setupModeChangeHandler,
|
||||||
createModeChangeCallback,
|
createModeChangeCallback,
|
||||||
LORA_PROVIDER_NODE_TYPES
|
LORA_CHAIN_NODE_TYPES
|
||||||
} from './mode-change-handler'
|
} from './mode-change-handler'
|
||||||
|
|
||||||
const LORA_POOL_WIDGET_MIN_WIDTH = 500
|
const LORA_POOL_WIDGET_MIN_WIDTH = 500
|
||||||
@@ -755,8 +755,8 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register mode change handlers for LoRA provider nodes
|
// Register mode change handlers for LORA_STACK chain nodes
|
||||||
if (LORA_PROVIDER_NODE_TYPES.includes(comfyClass)) {
|
if (LORA_CHAIN_NODE_TYPES.includes(comfyClass)) {
|
||||||
const originalOnNodeCreated = nodeType.prototype.onNodeCreated
|
const originalOnNodeCreated = nodeType.prototype.onNodeCreated
|
||||||
|
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
|||||||
@@ -18,7 +18,22 @@ export const LORA_PROVIDER_NODE_TYPES = [
|
|||||||
"Lora Cycler (LoraManager)",
|
"Lora Cycler (LoraManager)",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nodes that do not own LoRA state themselves, but merge or forward LORA_STACK
|
||||||
|
* inputs so downstream trigger-word updates must traverse through them.
|
||||||
|
*/
|
||||||
|
export const LORA_STACK_AGGREGATOR_NODE_TYPES = [
|
||||||
|
"Lora Stack Combiner (LoraManager)",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const LORA_CHAIN_NODE_TYPES = [
|
||||||
|
...LORA_PROVIDER_NODE_TYPES,
|
||||||
|
...LORA_STACK_AGGREGATOR_NODE_TYPES,
|
||||||
|
] as const;
|
||||||
|
|
||||||
export type LoraProviderNodeType = typeof LORA_PROVIDER_NODE_TYPES[number];
|
export type LoraProviderNodeType = typeof LORA_PROVIDER_NODE_TYPES[number];
|
||||||
|
export type LoraStackAggregatorNodeType = typeof LORA_STACK_AGGREGATOR_NODE_TYPES[number];
|
||||||
|
export type LoraChainNodeType = typeof LORA_CHAIN_NODE_TYPES[number];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a node class is a LoRA provider node.
|
* Check if a node class is a LoRA provider node.
|
||||||
@@ -27,6 +42,16 @@ export function isLoraProviderNode(comfyClass: string): comfyClass is LoraProvid
|
|||||||
return LORA_PROVIDER_NODE_TYPES.includes(comfyClass as LoraProviderNodeType);
|
return LORA_PROVIDER_NODE_TYPES.includes(comfyClass as LoraProviderNodeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isLoraStackAggregatorNode(
|
||||||
|
comfyClass: string
|
||||||
|
): comfyClass is LoraStackAggregatorNodeType {
|
||||||
|
return LORA_STACK_AGGREGATOR_NODE_TYPES.includes(comfyClass as LoraStackAggregatorNodeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLoraChainNode(comfyClass: string): comfyClass is LoraChainNodeType {
|
||||||
|
return LORA_CHAIN_NODE_TYPES.includes(comfyClass as LoraChainNodeType);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract active LoRA filenames from a node based on its type.
|
* Extract active LoRA filenames from a node based on its type.
|
||||||
*
|
*
|
||||||
@@ -40,6 +65,10 @@ export function getActiveLorasFromNodeByType(node: any): Set<string> {
|
|||||||
return extractFromCyclerConfig(node);
|
return extractFromCyclerConfig(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLoraStackAggregatorNode(comfyClass)) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
|
||||||
// Default: use lorasWidget (works for Stacker and Randomizer)
|
// Default: use lorasWidget (works for Stacker and Randomizer)
|
||||||
return extractFromLorasWidget(node);
|
return extractFromLorasWidget(node);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,27 @@ export const LORA_PROVIDER_NODE_TYPES = [
|
|||||||
"Lora Cycler (LoraManager)",
|
"Lora Cycler (LoraManager)",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const LORA_STACK_AGGREGATOR_NODE_TYPES = [
|
||||||
|
"Lora Stack Combiner (LoraManager)",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const LORA_CHAIN_NODE_TYPES = [
|
||||||
|
...LORA_PROVIDER_NODE_TYPES,
|
||||||
|
...LORA_STACK_AGGREGATOR_NODE_TYPES,
|
||||||
|
];
|
||||||
|
|
||||||
export function isLoraProviderNode(comfyClass) {
|
export function isLoraProviderNode(comfyClass) {
|
||||||
return LORA_PROVIDER_NODE_TYPES.includes(comfyClass);
|
return LORA_PROVIDER_NODE_TYPES.includes(comfyClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isLoraStackAggregatorNode(comfyClass) {
|
||||||
|
return LORA_STACK_AGGREGATOR_NODE_TYPES.includes(comfyClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLoraChainNode(comfyClass) {
|
||||||
|
return LORA_CHAIN_NODE_TYPES.includes(comfyClass);
|
||||||
|
}
|
||||||
|
|
||||||
function isMapLike(collection) {
|
function isMapLike(collection) {
|
||||||
return collection && typeof collection.entries === "function" && typeof collection.values === "function";
|
return collection && typeof collection.entries === "function" && typeof collection.values === "function";
|
||||||
}
|
}
|
||||||
@@ -245,16 +262,20 @@ export function hideWidgetForGood(node, widget, suffix = "") {
|
|||||||
// Update pattern to match both formats: <lora:name:model_strength> or <lora:name:model_strength:clip_strength>
|
// Update pattern to match both formats: <lora:name:model_strength> or <lora:name:model_strength:clip_strength>
|
||||||
export const LORA_PATTERN = /<lora:([^:]+):([-\d\.]+)(?::([-\d\.]+))?>/g;
|
export const LORA_PATTERN = /<lora:([^:]+):([-\d\.]+)(?::([-\d\.]+))?>/g;
|
||||||
|
|
||||||
// Get connected Lora Stacker nodes that feed into the current node
|
function isLoraStackInput(input) {
|
||||||
export function getConnectedInputStackers(node) {
|
return input?.type === "LORA_STACK";
|
||||||
const connectedStackers = [];
|
}
|
||||||
|
|
||||||
|
// Get connected LORA_STACK chain nodes that feed into the current node
|
||||||
|
export function getConnectedInputLoraChainNodes(node) {
|
||||||
|
const connectedNodes = [];
|
||||||
|
|
||||||
if (!node?.inputs) {
|
if (!node?.inputs) {
|
||||||
return connectedStackers;
|
return connectedNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const input of node.inputs) {
|
for (const input of node.inputs) {
|
||||||
if (input.name !== "lora_stack" || !input.link) {
|
if (!isLoraStackInput(input) || !input.link) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,12 +285,12 @@ export function getConnectedInputStackers(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sourceNode = node.graph?.getNodeById?.(link.origin_id);
|
const sourceNode = node.graph?.getNodeById?.(link.origin_id);
|
||||||
if (sourceNode && isLoraProviderNode(sourceNode.comfyClass)) {
|
if (sourceNode && isLoraChainNode(sourceNode.comfyClass)) {
|
||||||
connectedStackers.push(sourceNode);
|
connectedNodes.push(sourceNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return connectedStackers;
|
return connectedNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get connected TriggerWord Toggle nodes that receive output from the current node
|
// Get connected TriggerWord Toggle nodes that receive output from the current node
|
||||||
@@ -314,6 +335,11 @@ export function getActiveLorasFromNode(node) {
|
|||||||
return activeLoraNames;
|
return activeLoraNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aggregator nodes do not own LoRA state directly; they only forward upstream stacks.
|
||||||
|
if (isLoraStackAggregatorNode(node.comfyClass)) {
|
||||||
|
return activeLoraNames;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Lora Stacker and Lora Randomizer (lorasWidget)
|
// Handle Lora Stacker and Lora Randomizer (lorasWidget)
|
||||||
let lorasWidget = node.lorasWidget;
|
let lorasWidget = node.lorasWidget;
|
||||||
if (!lorasWidget && node.widgets) {
|
if (!lorasWidget && node.widgets) {
|
||||||
@@ -348,14 +374,18 @@ export function collectActiveLorasFromChain(node, visited = new Set()) {
|
|||||||
// Mode 2 is Never, Mode 4 is Bypass
|
// Mode 2 is Never, Mode 4 is Bypass
|
||||||
const isNodeActive = node.mode === undefined || node.mode === 0 || node.mode === 3;
|
const isNodeActive = node.mode === undefined || node.mode === 0 || node.mode === 3;
|
||||||
|
|
||||||
// Get active loras from current node only if node is active
|
if (!isNodeActive) {
|
||||||
const allActiveLoraNames = isNodeActive ? getActiveLorasFromNode(node) : new Set();
|
return new Set();
|
||||||
|
}
|
||||||
|
|
||||||
// Get connected input stackers and collect their active loras
|
// Get active loras from current node only if node is active
|
||||||
const inputStackers = getConnectedInputStackers(node);
|
const allActiveLoraNames = getActiveLorasFromNode(node);
|
||||||
for (const stacker of inputStackers) {
|
|
||||||
const stackerLoras = collectActiveLorasFromChain(stacker, visited);
|
// Get connected input LORA_STACK chain nodes and collect their active loras
|
||||||
stackerLoras.forEach(name => allActiveLoraNames.add(name));
|
const inputChainNodes = getConnectedInputLoraChainNodes(node);
|
||||||
|
for (const chainNode of inputChainNodes) {
|
||||||
|
const upstreamLoras = collectActiveLorasFromChain(chainNode, visited);
|
||||||
|
upstreamLoras.forEach(name => allActiveLoraNames.add(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
return allActiveLoraNames;
|
return allActiveLoraNames;
|
||||||
@@ -819,8 +849,8 @@ export function updateDownstreamLoaders(startNode, visited = new Set()) {
|
|||||||
collectActiveLorasFromChain(targetNode);
|
collectActiveLorasFromChain(targetNode);
|
||||||
updateConnectedTriggerWords(targetNode, allActiveLoraNames);
|
updateConnectedTriggerWords(targetNode, allActiveLoraNames);
|
||||||
}
|
}
|
||||||
// If target is another LoRA provider node, recursively check its outputs
|
// If target is another LORA_STACK chain node, recursively check its outputs
|
||||||
else if (targetNode && isLoraProviderNode(targetNode.comfyClass)) {
|
else if (targetNode && isLoraChainNode(targetNode.comfyClass)) {
|
||||||
updateDownstreamLoaders(targetNode, visited);
|
updateDownstreamLoaders(targetNode, visited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14938,11 +14938,24 @@ const LORA_PROVIDER_NODE_TYPES$1 = [
|
|||||||
"Lora Randomizer (LoraManager)",
|
"Lora Randomizer (LoraManager)",
|
||||||
"Lora Cycler (LoraManager)"
|
"Lora Cycler (LoraManager)"
|
||||||
];
|
];
|
||||||
|
const LORA_STACK_AGGREGATOR_NODE_TYPES$1 = [
|
||||||
|
"Lora Stack Combiner (LoraManager)"
|
||||||
|
];
|
||||||
|
const LORA_CHAIN_NODE_TYPES$1 = [
|
||||||
|
...LORA_PROVIDER_NODE_TYPES$1,
|
||||||
|
...LORA_STACK_AGGREGATOR_NODE_TYPES$1
|
||||||
|
];
|
||||||
|
function isLoraStackAggregatorNode$1(comfyClass) {
|
||||||
|
return LORA_STACK_AGGREGATOR_NODE_TYPES$1.includes(comfyClass);
|
||||||
|
}
|
||||||
function getActiveLorasFromNodeByType(node) {
|
function getActiveLorasFromNodeByType(node) {
|
||||||
const comfyClass = node == null ? void 0 : node.comfyClass;
|
const comfyClass = node == null ? void 0 : node.comfyClass;
|
||||||
if (comfyClass === "Lora Cycler (LoraManager)") {
|
if (comfyClass === "Lora Cycler (LoraManager)") {
|
||||||
return extractFromCyclerConfig(node);
|
return extractFromCyclerConfig(node);
|
||||||
}
|
}
|
||||||
|
if (isLoraStackAggregatorNode$1(comfyClass)) {
|
||||||
|
return /* @__PURE__ */ new Set();
|
||||||
|
}
|
||||||
return extractFromLorasWidget(node);
|
return extractFromLorasWidget(node);
|
||||||
}
|
}
|
||||||
function extractFromLorasWidget(node) {
|
function extractFromLorasWidget(node) {
|
||||||
@@ -15002,8 +15015,18 @@ const LORA_PROVIDER_NODE_TYPES = [
|
|||||||
"Lora Randomizer (LoraManager)",
|
"Lora Randomizer (LoraManager)",
|
||||||
"Lora Cycler (LoraManager)"
|
"Lora Cycler (LoraManager)"
|
||||||
];
|
];
|
||||||
function isLoraProviderNode(comfyClass) {
|
const LORA_STACK_AGGREGATOR_NODE_TYPES = [
|
||||||
return LORA_PROVIDER_NODE_TYPES.includes(comfyClass);
|
"Lora Stack Combiner (LoraManager)"
|
||||||
|
];
|
||||||
|
const LORA_CHAIN_NODE_TYPES = [
|
||||||
|
...LORA_PROVIDER_NODE_TYPES,
|
||||||
|
...LORA_STACK_AGGREGATOR_NODE_TYPES
|
||||||
|
];
|
||||||
|
function isLoraStackAggregatorNode(comfyClass) {
|
||||||
|
return LORA_STACK_AGGREGATOR_NODE_TYPES.includes(comfyClass);
|
||||||
|
}
|
||||||
|
function isLoraChainNode(comfyClass) {
|
||||||
|
return LORA_CHAIN_NODE_TYPES.includes(comfyClass);
|
||||||
}
|
}
|
||||||
function isMapLike(collection) {
|
function isMapLike(collection) {
|
||||||
return collection && typeof collection.entries === "function" && typeof collection.values === "function";
|
return collection && typeof collection.entries === "function" && typeof collection.values === "function";
|
||||||
@@ -15041,14 +15064,17 @@ function getLinkFromGraph(graph, linkId) {
|
|||||||
}
|
}
|
||||||
return graph.links[linkId] || null;
|
return graph.links[linkId] || null;
|
||||||
}
|
}
|
||||||
function getConnectedInputStackers(node) {
|
function isLoraStackInput(input) {
|
||||||
|
return (input == null ? void 0 : input.type) === "LORA_STACK";
|
||||||
|
}
|
||||||
|
function getConnectedInputLoraChainNodes(node) {
|
||||||
var _a2, _b;
|
var _a2, _b;
|
||||||
const connectedStackers = [];
|
const connectedNodes = [];
|
||||||
if (!(node == null ? void 0 : node.inputs)) {
|
if (!(node == null ? void 0 : node.inputs)) {
|
||||||
return connectedStackers;
|
return connectedNodes;
|
||||||
}
|
}
|
||||||
for (const input of node.inputs) {
|
for (const input of node.inputs) {
|
||||||
if (input.name !== "lora_stack" || !input.link) {
|
if (!isLoraStackInput(input) || !input.link) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const link = getLinkFromGraph(node.graph, input.link);
|
const link = getLinkFromGraph(node.graph, input.link);
|
||||||
@@ -15056,11 +15082,11 @@ function getConnectedInputStackers(node) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const sourceNode = (_b = (_a2 = node.graph) == null ? void 0 : _a2.getNodeById) == null ? void 0 : _b.call(_a2, link.origin_id);
|
const sourceNode = (_b = (_a2 = node.graph) == null ? void 0 : _a2.getNodeById) == null ? void 0 : _b.call(_a2, link.origin_id);
|
||||||
if (sourceNode && isLoraProviderNode(sourceNode.comfyClass)) {
|
if (sourceNode && isLoraChainNode(sourceNode.comfyClass)) {
|
||||||
connectedStackers.push(sourceNode);
|
connectedNodes.push(sourceNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return connectedStackers;
|
return connectedNodes;
|
||||||
}
|
}
|
||||||
function getConnectedTriggerToggleNodes(node) {
|
function getConnectedTriggerToggleNodes(node) {
|
||||||
var _a2, _b, _c;
|
var _a2, _b, _c;
|
||||||
@@ -15095,6 +15121,9 @@ function getActiveLorasFromNode(node) {
|
|||||||
}
|
}
|
||||||
return activeLoraNames;
|
return activeLoraNames;
|
||||||
}
|
}
|
||||||
|
if (isLoraStackAggregatorNode(node.comfyClass)) {
|
||||||
|
return activeLoraNames;
|
||||||
|
}
|
||||||
let lorasWidget = node.lorasWidget;
|
let lorasWidget = node.lorasWidget;
|
||||||
if (!lorasWidget && node.widgets) {
|
if (!lorasWidget && node.widgets) {
|
||||||
lorasWidget = node.widgets.find((w2) => w2.name === "loras");
|
lorasWidget = node.widgets.find((w2) => w2.name === "loras");
|
||||||
@@ -15118,11 +15147,14 @@ function collectActiveLorasFromChain(node, visited = /* @__PURE__ */ new Set())
|
|||||||
}
|
}
|
||||||
visited.add(nodeKey);
|
visited.add(nodeKey);
|
||||||
const isNodeActive2 = node.mode === void 0 || node.mode === 0 || node.mode === 3;
|
const isNodeActive2 = node.mode === void 0 || node.mode === 0 || node.mode === 3;
|
||||||
const allActiveLoraNames = isNodeActive2 ? getActiveLorasFromNode(node) : /* @__PURE__ */ new Set();
|
if (!isNodeActive2) {
|
||||||
const inputStackers = getConnectedInputStackers(node);
|
return /* @__PURE__ */ new Set();
|
||||||
for (const stacker of inputStackers) {
|
}
|
||||||
const stackerLoras = collectActiveLorasFromChain(stacker, visited);
|
const allActiveLoraNames = getActiveLorasFromNode(node);
|
||||||
stackerLoras.forEach((name) => allActiveLoraNames.add(name));
|
const inputChainNodes = getConnectedInputLoraChainNodes(node);
|
||||||
|
for (const chainNode of inputChainNodes) {
|
||||||
|
const upstreamLoras = collectActiveLorasFromChain(chainNode, visited);
|
||||||
|
upstreamLoras.forEach((name) => allActiveLoraNames.add(name));
|
||||||
}
|
}
|
||||||
return allActiveLoraNames;
|
return allActiveLoraNames;
|
||||||
}
|
}
|
||||||
@@ -15191,7 +15223,7 @@ function updateDownstreamLoaders(startNode, visited = /* @__PURE__ */ new Set())
|
|||||||
if (targetNode && targetNode.comfyClass === "Lora Loader (LoraManager)") {
|
if (targetNode && targetNode.comfyClass === "Lora Loader (LoraManager)") {
|
||||||
const allActiveLoraNames = collectActiveLorasFromChain(targetNode);
|
const allActiveLoraNames = collectActiveLorasFromChain(targetNode);
|
||||||
updateConnectedTriggerWords(targetNode, allActiveLoraNames);
|
updateConnectedTriggerWords(targetNode, allActiveLoraNames);
|
||||||
} else if (targetNode && isLoraProviderNode(targetNode.comfyClass)) {
|
} else if (targetNode && isLoraChainNode(targetNode.comfyClass)) {
|
||||||
updateDownstreamLoaders(targetNode, visited);
|
updateDownstreamLoaders(targetNode, visited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15784,7 +15816,7 @@ app$1.registerExtension({
|
|||||||
return originalConfigure == null ? void 0 : originalConfigure.apply(this, arguments);
|
return originalConfigure == null ? void 0 : originalConfigure.apply(this, arguments);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (LORA_PROVIDER_NODE_TYPES$1.includes(comfyClass)) {
|
if (LORA_CHAIN_NODE_TYPES$1.includes(comfyClass)) {
|
||||||
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
nodeType.prototype.onNodeCreated = function() {
|
nodeType.prototype.onNodeCreated = function() {
|
||||||
originalOnNodeCreated == null ? void 0 : originalOnNodeCreated.apply(this, arguments);
|
originalOnNodeCreated == null ? void 0 : originalOnNodeCreated.apply(this, arguments);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user