mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-10 12:59:24 -03:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24e2909627 | ||
|
|
b768f1368f | ||
|
|
37ccd29fc0 | ||
|
|
7416080cfb | ||
|
|
26be187d42 | ||
|
|
d7caa1fa47 | ||
|
|
2629fcce23 | ||
|
|
438e7d07b9 | ||
|
|
e9932ea870 | ||
|
|
5dd8b96422 | ||
|
|
5e1cf68bbd | ||
|
|
1044fa3c83 | ||
|
|
397892bb7f | ||
|
|
f105500740 | ||
|
|
806555cf06 | ||
|
|
5cd7204101 | ||
|
|
3b602a3698 | ||
|
|
15dfaed462 | ||
|
|
0e51851025 | ||
|
|
0d0f4defca | ||
|
|
818fa34a48 | ||
|
|
78303b2a5e | ||
|
|
9ce56dd40c | ||
|
|
33e5f3d85d | ||
|
|
031d5e4f40 | ||
|
|
4ff5774e34 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ model_cache/
|
|||||||
.claude/
|
.claude/
|
||||||
.sisyphus/
|
.sisyphus/
|
||||||
.codex
|
.codex
|
||||||
|
.omo
|
||||||
|
|
||||||
# Vue widgets development cache (but keep build output)
|
# Vue widgets development cache (but keep build output)
|
||||||
vue-widgets/node_modules/
|
vue-widgets/node_modules/
|
||||||
|
|||||||
@@ -15,41 +15,60 @@
|
|||||||
"Phil",
|
"Phil",
|
||||||
"Carl G.",
|
"Carl G.",
|
||||||
"Arlecchino Shion",
|
"Arlecchino Shion",
|
||||||
"$MetaSamsara",
|
"Charles Blakemore",
|
||||||
"Rob Williams",
|
"Rob Williams",
|
||||||
|
"$MetaSamsara",
|
||||||
"stone9k",
|
"stone9k",
|
||||||
|
"Rosenthal",
|
||||||
|
"Francisco Tatis",
|
||||||
"runte3221",
|
"runte3221",
|
||||||
|
"Fraser Cross",
|
||||||
|
"Polymorphic Indeterminate",
|
||||||
|
"Marc Whiffen",
|
||||||
|
"Skalabananen",
|
||||||
|
"Birdy",
|
||||||
"Kiba",
|
"Kiba",
|
||||||
"Mozzel",
|
"Mozzel",
|
||||||
"itismyelement",
|
"itismyelement",
|
||||||
"Gingko Biloba",
|
"Gingko Biloba",
|
||||||
|
"Reno Lam",
|
||||||
"onesecondinosaur",
|
"onesecondinosaur",
|
||||||
|
"sig",
|
||||||
"Christian Byrne",
|
"Christian Byrne",
|
||||||
"DM",
|
"DM",
|
||||||
"Sen314",
|
"Sen314",
|
||||||
"Estragon",
|
"Estragon",
|
||||||
|
"J\\B/ 8r0wns0n",
|
||||||
"Takkan",
|
"Takkan",
|
||||||
"Charles Blakemore",
|
|
||||||
"Rosenthal",
|
|
||||||
"ClockDaemon",
|
"ClockDaemon",
|
||||||
"Francisco Tatis",
|
"KD",
|
||||||
|
"Omnidex",
|
||||||
|
"Release Cabrakan",
|
||||||
"Tobi_Swagg",
|
"Tobi_Swagg",
|
||||||
"SG",
|
"SG",
|
||||||
|
"James Dooley",
|
||||||
|
"zenbound",
|
||||||
"jmack",
|
"jmack",
|
||||||
"Andrew Wilson",
|
"Andrew Wilson",
|
||||||
"Greybush",
|
"Greybush",
|
||||||
|
"Mark Corneglio",
|
||||||
|
"SarcasticHashtag",
|
||||||
"iamresist",
|
"iamresist",
|
||||||
"Wolffen",
|
"Wolffen",
|
||||||
"Ricky Carter",
|
"Ricky Carter",
|
||||||
|
"James Todd",
|
||||||
"JongWon Han",
|
"JongWon Han",
|
||||||
"VantAI",
|
"VantAI",
|
||||||
"Tim",
|
"Tim",
|
||||||
|
"Lisster",
|
||||||
"Michael Wong",
|
"Michael Wong",
|
||||||
"Illrigger",
|
"Illrigger",
|
||||||
"Tom Corrigan",
|
"Tom Corrigan",
|
||||||
"JackieWang",
|
"JackieWang",
|
||||||
"FreelancerZ",
|
"FreelancerZ",
|
||||||
"fnkylove",
|
"fnkylove",
|
||||||
|
"Yushio",
|
||||||
|
"Vik71it",
|
||||||
"Echo",
|
"Echo",
|
||||||
"Lilleman",
|
"Lilleman",
|
||||||
"Robert Stacey",
|
"Robert Stacey",
|
||||||
@@ -58,59 +77,54 @@
|
|||||||
"Jorge Hussni",
|
"Jorge Hussni",
|
||||||
"Liam MacDougal",
|
"Liam MacDougal",
|
||||||
"Sterilized",
|
"Sterilized",
|
||||||
"Fraser Cross",
|
"BadassArabianMofo",
|
||||||
"Polymorphic Indeterminate",
|
|
||||||
"Marc Whiffen",
|
|
||||||
"Birdy",
|
|
||||||
"Skalabananen",
|
|
||||||
"quarz",
|
"quarz",
|
||||||
"Reno Lam",
|
"Greg",
|
||||||
"JSST",
|
"JSST",
|
||||||
"sig",
|
|
||||||
"J\\B/ 8r0wns0n",
|
|
||||||
"Snaggwort",
|
"Snaggwort",
|
||||||
|
"lmsupporter",
|
||||||
|
"wfpearl",
|
||||||
"Baekdoosixt",
|
"Baekdoosixt",
|
||||||
"Jonathan Ross",
|
"Jonathan Ross",
|
||||||
"KD",
|
"Jack B Nimble",
|
||||||
"Omnidex",
|
|
||||||
"Nazono_hito",
|
"Nazono_hito",
|
||||||
"Melville Parrish",
|
"Melville Parrish",
|
||||||
"daniel dove",
|
"daniel dove",
|
||||||
"Lustre",
|
"Lustre",
|
||||||
"Tyler Trebuchon",
|
"Tyler Trebuchon",
|
||||||
"Release Cabrakan",
|
|
||||||
"JW Sin",
|
"JW Sin",
|
||||||
"contrite831",
|
"contrite831",
|
||||||
"Alex",
|
"Alex",
|
||||||
|
"bh",
|
||||||
"carozzz",
|
"carozzz",
|
||||||
"Marlon Daniels",
|
"Marlon Daniels",
|
||||||
"James Dooley",
|
"Starkselle",
|
||||||
"zenbound",
|
|
||||||
"Buzzard",
|
"Buzzard",
|
||||||
|
"Aaron Bleuer",
|
||||||
|
"LacesOut!",
|
||||||
|
"greebles",
|
||||||
"Adam Shaw",
|
"Adam Shaw",
|
||||||
"Mark Corneglio",
|
|
||||||
"SarcasticHashtag",
|
|
||||||
"Anthony Rizzo",
|
"Anthony Rizzo",
|
||||||
"Gooohokrbe",
|
"Gooohokrbe",
|
||||||
"RedrockVP",
|
"RedrockVP",
|
||||||
"James Todd",
|
|
||||||
"ASLPro3D",
|
"ASLPro3D",
|
||||||
"OldBones",
|
"OldBones",
|
||||||
|
"Jacob Hoehler",
|
||||||
"FinalyFree",
|
"FinalyFree",
|
||||||
|
"Weasyl",
|
||||||
"Steven Pfeiffer",
|
"Steven Pfeiffer",
|
||||||
"Timmy",
|
"Timmy",
|
||||||
"Johnny",
|
"Johnny",
|
||||||
|
"Cory Paza",
|
||||||
"Tak",
|
"Tak",
|
||||||
"Lisster",
|
|
||||||
"Zach Gonser",
|
"Zach Gonser",
|
||||||
"Big Red",
|
"Big Red",
|
||||||
"whudunit",
|
"whudunit",
|
||||||
"Luc Job",
|
"Luc Job",
|
||||||
"dl0901dm",
|
"dl0901dm",
|
||||||
|
"Philip Hempel",
|
||||||
"corde",
|
"corde",
|
||||||
"Nick Walker",
|
"Nick Walker",
|
||||||
"Yushio",
|
|
||||||
"Vik71it",
|
|
||||||
"Bishoujoker",
|
"Bishoujoker",
|
||||||
"Todd Keck",
|
"Todd Keck",
|
||||||
"Briton Heilbrun",
|
"Briton Heilbrun",
|
||||||
@@ -119,47 +133,50 @@
|
|||||||
"jean jahren",
|
"jean jahren",
|
||||||
"Aleksander Wujczyk",
|
"Aleksander Wujczyk",
|
||||||
"AM Kuro",
|
"AM Kuro",
|
||||||
"BadassArabianMofo",
|
|
||||||
"Pascal Dahle",
|
"Pascal Dahle",
|
||||||
"Penfore",
|
"Penfore",
|
||||||
"Greg",
|
"Sangheili460",
|
||||||
"MagnaInsomnia",
|
"MagnaInsomnia",
|
||||||
|
"Karl P.",
|
||||||
"Akira_HentAI",
|
"Akira_HentAI",
|
||||||
"Gordon Cole",
|
"Gordon Cole",
|
||||||
"AbstractAss",
|
"AbstractAss",
|
||||||
"lmsupporter",
|
|
||||||
"andrew.tappan",
|
"andrew.tappan",
|
||||||
"N/A",
|
"N/A",
|
||||||
|
"The Spawn",
|
||||||
|
"graysock",
|
||||||
"Greenmoustache",
|
"Greenmoustache",
|
||||||
"zounic",
|
"zounic",
|
||||||
"wfpearl",
|
"fancypants",
|
||||||
"Eldithor",
|
"Eldithor",
|
||||||
"Jack B Nimble",
|
"Digital",
|
||||||
"JaxMax",
|
"JaxMax",
|
||||||
"bh",
|
"takyamtom",
|
||||||
"Jwk0205",
|
"Jwk0205",
|
||||||
"Starkselle",
|
"batblue",
|
||||||
|
"carey6409",
|
||||||
"Olive",
|
"Olive",
|
||||||
"Aaron Bleuer",
|
|
||||||
"LacesOut!",
|
|
||||||
"greebles",
|
|
||||||
"Some Guy Named Barry",
|
"Some Guy Named Barry",
|
||||||
"Cosmosis",
|
"Cosmosis",
|
||||||
"M Postkasse",
|
"M Postkasse",
|
||||||
|
"AELOX",
|
||||||
|
"Nicfit23",
|
||||||
"FloPro4Sho",
|
"FloPro4Sho",
|
||||||
"wamekukyouzin",
|
"wamekukyouzin",
|
||||||
"Jacob Hoehler",
|
"drum matthieu",
|
||||||
|
"Dogmaster",
|
||||||
"Matt Wenzel",
|
"Matt Wenzel",
|
||||||
"Weasyl",
|
|
||||||
"Lex Song",
|
"Lex Song",
|
||||||
"Cory Paza",
|
"Christopher Michel",
|
||||||
"Gonzalo Andre Allendes Lopez",
|
"Gonzalo Andre Allendes Lopez",
|
||||||
"Serge Bekenkamp",
|
"Serge Bekenkamp",
|
||||||
"Jimmy Ledbetter",
|
"Jimmy Ledbetter",
|
||||||
"Philip Hempel",
|
"LeoZero",
|
||||||
"ApathyJones",
|
"ApathyJones",
|
||||||
"Julian V",
|
"Julian V",
|
||||||
"Steven Owens",
|
"Steven Owens",
|
||||||
|
"nahinahi9",
|
||||||
|
"Dustin Chen",
|
||||||
"dan",
|
"dan",
|
||||||
"aai",
|
"aai",
|
||||||
"Mouthlessman",
|
"Mouthlessman",
|
||||||
@@ -167,32 +184,29 @@
|
|||||||
"ViperC",
|
"ViperC",
|
||||||
"Ran C",
|
"Ran C",
|
||||||
"MiraiKuriyamaSy",
|
"MiraiKuriyamaSy",
|
||||||
"Sangheili460",
|
|
||||||
"Karl P.",
|
|
||||||
"yuxz69",
|
"yuxz69",
|
||||||
"Adam Taylor",
|
"Adam Taylor",
|
||||||
"Weird_With_A_Beard",
|
"Weird_With_A_Beard",
|
||||||
"esthe",
|
"esthe",
|
||||||
"The Spawn",
|
|
||||||
"graysock",
|
|
||||||
"Pozadine1",
|
"Pozadine1",
|
||||||
"Qarob",
|
"Qarob",
|
||||||
"AIGooner",
|
"AIGooner",
|
||||||
"Luc",
|
"Luc",
|
||||||
"ProtonPrince",
|
"ProtonPrince",
|
||||||
"DiffDuck",
|
"DiffDuck",
|
||||||
"fancypants",
|
"elu3199",
|
||||||
|
"Hasturkun",
|
||||||
|
"Jon Sandman",
|
||||||
|
"Ubivis",
|
||||||
|
"CloudValley",
|
||||||
"IamAyam",
|
"IamAyam",
|
||||||
"Joboshy",
|
"Joboshy",
|
||||||
"Digital",
|
|
||||||
"takyamtom",
|
|
||||||
"Bohemian Corporal",
|
"Bohemian Corporal",
|
||||||
"Dan",
|
"Dan",
|
||||||
"confiscated Zyra",
|
"confiscated Zyra",
|
||||||
"Bro Xie",
|
"Bro Xie",
|
||||||
"yer fey",
|
"yer fey",
|
||||||
"batblue",
|
"Error_Rule34_Not_found",
|
||||||
"carey6409",
|
|
||||||
"太郎 ゲーム",
|
"太郎 ゲーム",
|
||||||
"Roslynd",
|
"Roslynd",
|
||||||
"Tee Gee",
|
"Tee Gee",
|
||||||
@@ -200,42 +214,31 @@
|
|||||||
"tarek helmi",
|
"tarek helmi",
|
||||||
"Neco28",
|
"Neco28",
|
||||||
"Max Marklund",
|
"Max Marklund",
|
||||||
"AELOX",
|
"David Ortega",
|
||||||
"Dankin",
|
"Dankin",
|
||||||
"Nicfit23",
|
|
||||||
"Cristian Vazquez",
|
"Cristian Vazquez",
|
||||||
"drum matthieu",
|
|
||||||
"Dogmaster",
|
|
||||||
"Frank Nitty",
|
"Frank Nitty",
|
||||||
"Magic Noob",
|
"Magic Noob",
|
||||||
"Pronredn",
|
"Pronredn",
|
||||||
"Christopher Michel",
|
|
||||||
"DougPeterson",
|
"DougPeterson",
|
||||||
"LeoZero",
|
|
||||||
"Antonio Pontes",
|
"Antonio Pontes",
|
||||||
"Bruce",
|
"Bruce",
|
||||||
"nahinahi9",
|
|
||||||
"lh qwe",
|
"lh qwe",
|
||||||
"Kevin John Duck",
|
"Kevin John Duck",
|
||||||
"conner",
|
"conner",
|
||||||
"Dustin Chen",
|
"Kevin Christopher",
|
||||||
"Blackfish95",
|
"Blackfish95",
|
||||||
|
"dd",
|
||||||
"Princess Bright Eyes",
|
"Princess Bright Eyes",
|
||||||
"Paul Kroll",
|
"Paul Kroll",
|
||||||
"Felipe dos Santos",
|
"Felipe dos Santos",
|
||||||
"Bas Imagineer",
|
"Bas Imagineer",
|
||||||
"Markus",
|
|
||||||
"John Statham",
|
"John Statham",
|
||||||
"Douglas Gaspar",
|
"Douglas Gaspar",
|
||||||
"AlexDuKaNa",
|
"AlexDuKaNa",
|
||||||
"George",
|
"George",
|
||||||
"dw",
|
"dw",
|
||||||
"decoy",
|
"decoy",
|
||||||
"elu3199",
|
|
||||||
"Hasturkun",
|
|
||||||
"Jon Sandman",
|
|
||||||
"Ubivis",
|
|
||||||
"CloudValley",
|
|
||||||
"thesoftwaredruid",
|
"thesoftwaredruid",
|
||||||
"wundershark",
|
"wundershark",
|
||||||
"mr_dinosaur",
|
"mr_dinosaur",
|
||||||
@@ -243,57 +246,62 @@
|
|||||||
"Ray Wing",
|
"Ray Wing",
|
||||||
"Ranzitho",
|
"Ranzitho",
|
||||||
"Gus",
|
"Gus",
|
||||||
|
"地獄の禄",
|
||||||
"MJG",
|
"MJG",
|
||||||
"David LaVallee",
|
"David LaVallee",
|
||||||
"linnfrey",
|
"linnfrey",
|
||||||
|
"ae",
|
||||||
|
"Tr4shP4nda",
|
||||||
|
"WRL_SPR",
|
||||||
|
"capn",
|
||||||
|
"Joseph",
|
||||||
|
"Mirko Katzula",
|
||||||
"奚明 刘",
|
"奚明 刘",
|
||||||
|
"Brian M",
|
||||||
"Josef Lanzl",
|
"Josef Lanzl",
|
||||||
"Nerezza",
|
"Nerezza",
|
||||||
"sanborondon",
|
"sanborondon",
|
||||||
"Griffin Dahlberg",
|
"Griffin Dahlberg",
|
||||||
"준희 김",
|
"준희 김",
|
||||||
"Error_Rule34_Not_found",
|
|
||||||
"Taylor Funk",
|
"Taylor Funk",
|
||||||
"aezin",
|
"aezin",
|
||||||
|
"Thought2Form",
|
||||||
"jcay015",
|
"jcay015",
|
||||||
"Gerald Welly",
|
"Gerald Welly",
|
||||||
|
"Kevin Picco",
|
||||||
"Erik Lopez",
|
"Erik Lopez",
|
||||||
"Mateo Curić",
|
"Mateo Curić",
|
||||||
"Geolog",
|
"Geolog",
|
||||||
"Eris3D",
|
"Eris3D",
|
||||||
"Tomohiro Baba",
|
"Tomohiro Baba",
|
||||||
"David Ortega",
|
"m",
|
||||||
"Noora",
|
"Noora",
|
||||||
|
"Pierce McBride",
|
||||||
"Mattssn",
|
"Mattssn",
|
||||||
|
"Jamie Ogletree",
|
||||||
"a _",
|
"a _",
|
||||||
"Jeff",
|
"Jeff",
|
||||||
"James Coleman",
|
"James Coleman",
|
||||||
"Kevin Christopher",
|
|
||||||
"Emil Andersson",
|
"Emil Andersson",
|
||||||
"Ouro Boros",
|
"Ouro Boros",
|
||||||
"Chad Idk",
|
"Chad Idk",
|
||||||
"dd",
|
|
||||||
"Steam Steam",
|
"Steam Steam",
|
||||||
"CryptoTraderJK",
|
"CryptoTraderJK",
|
||||||
|
"Yuji Kaneko",
|
||||||
"Davaitamin",
|
"Davaitamin",
|
||||||
"Dušan Ryban",
|
"Dušan Ryban",
|
||||||
|
"Rops Alot",
|
||||||
"tedcor",
|
"tedcor",
|
||||||
"Sam",
|
"Sam",
|
||||||
"Fotek Design",
|
"Fotek Design",
|
||||||
"sjon kreutz",
|
"sjon kreutz",
|
||||||
|
"Ace Ventura",
|
||||||
"MadSpin",
|
"MadSpin",
|
||||||
"Metryman55",
|
"Metryman55",
|
||||||
"inbijiburu",
|
"inbijiburu",
|
||||||
"Nick “Loadstone” D",
|
"Nick “Loadstone” D",
|
||||||
"地獄の禄",
|
|
||||||
"ae",
|
|
||||||
"Tr4shP4nda",
|
|
||||||
"Gamalonia",
|
"Gamalonia",
|
||||||
"WRL_SPR",
|
|
||||||
"capn",
|
|
||||||
"Joseph",
|
|
||||||
"momokai",
|
"momokai",
|
||||||
"Mirko Katzula",
|
|
||||||
"dan",
|
"dan",
|
||||||
"Piccio08",
|
"Piccio08",
|
||||||
"kumakichi",
|
"kumakichi",
|
||||||
@@ -306,59 +314,13 @@
|
|||||||
"kudari",
|
"kudari",
|
||||||
"Naomi Hale Danchi",
|
"Naomi Hale Danchi",
|
||||||
"dc7431",
|
"dc7431",
|
||||||
|
"ken",
|
||||||
"epicgamer0020690",
|
"epicgamer0020690",
|
||||||
"Joshua Porrata",
|
"Joshua Porrata",
|
||||||
|
"keemun",
|
||||||
"SuBu",
|
"SuBu",
|
||||||
"RedPIXel",
|
"RedPIXel",
|
||||||
"Vir",
|
|
||||||
"Richard",
|
|
||||||
"Andrew",
|
|
||||||
"Brian M",
|
|
||||||
"Robert Wegemund",
|
|
||||||
"Littlehuggy",
|
|
||||||
"Draven T",
|
|
||||||
"mrjuan",
|
|
||||||
"Brian Buie",
|
|
||||||
"Thought2Form",
|
|
||||||
"Kevin Picco",
|
|
||||||
"Sadlip",
|
|
||||||
"Aquatic Coffee",
|
|
||||||
"m",
|
|
||||||
"ethanfel",
|
|
||||||
"Pierce McBride",
|
|
||||||
"Joshua Gray",
|
|
||||||
"Focuschannel",
|
|
||||||
"Mikko Hemilä",
|
|
||||||
"Jacob McDaniel",
|
|
||||||
"Jamie Ogletree",
|
|
||||||
"Temikus",
|
|
||||||
"Artokun",
|
|
||||||
"Michael Taylor",
|
|
||||||
"Derek Baker",
|
|
||||||
"Martial",
|
|
||||||
"Anthony Faxlandez",
|
|
||||||
"battu",
|
|
||||||
"Michael Anthony Scott",
|
|
||||||
"Atilla Berke Pekduyar",
|
|
||||||
"Decx _",
|
|
||||||
"Yuji Kaneko",
|
|
||||||
"Pat Hen",
|
|
||||||
"Jordan Shaw",
|
|
||||||
"Rops Alot",
|
|
||||||
"Thesharingbrother",
|
|
||||||
"Ace Ventura",
|
|
||||||
"ResidentDeviant",
|
|
||||||
"四糸凜音",
|
|
||||||
"Nihongasuki",
|
|
||||||
"JC",
|
|
||||||
"Prompt Pirate",
|
|
||||||
"uwutismxd",
|
|
||||||
"zenobeus",
|
|
||||||
"ken",
|
|
||||||
"Crocket",
|
|
||||||
"keemun",
|
|
||||||
"Wind",
|
"Wind",
|
||||||
"Jackthemind",
|
|
||||||
"Nexus",
|
"Nexus",
|
||||||
"Ramneek“Guy”Ashok",
|
"Ramneek“Guy”Ashok",
|
||||||
"squid_actually",
|
"squid_actually",
|
||||||
@@ -369,6 +331,53 @@
|
|||||||
"JohnDoe42054",
|
"JohnDoe42054",
|
||||||
"BillyHill",
|
"BillyHill",
|
||||||
"emyth",
|
"emyth",
|
||||||
|
"Vir",
|
||||||
|
"gzmzmvp",
|
||||||
|
"Richard",
|
||||||
|
"Andrew",
|
||||||
|
"Robert Wegemund",
|
||||||
|
"Littlehuggy",
|
||||||
|
"Draven T",
|
||||||
|
"mrjuan",
|
||||||
|
"Brian Buie",
|
||||||
|
"Sadlip",
|
||||||
|
"Eric Whitney",
|
||||||
|
"Joey Callahan",
|
||||||
|
"Aquatic Coffee",
|
||||||
|
"Ivan Tadic",
|
||||||
|
"Mike Simone",
|
||||||
|
"ethanfel",
|
||||||
|
"Joshua Gray",
|
||||||
|
"Morgandel",
|
||||||
|
"Focuschannel",
|
||||||
|
"Mikko Hemilä",
|
||||||
|
"Noah",
|
||||||
|
"Jacob McDaniel",
|
||||||
|
"X",
|
||||||
|
"Sloan Steddy",
|
||||||
|
"Temikus",
|
||||||
|
"Artokun",
|
||||||
|
"Michael Taylor",
|
||||||
|
"Derek Baker",
|
||||||
|
"Martial",
|
||||||
|
"Anthony Faxlandez",
|
||||||
|
"battu",
|
||||||
|
"Michael Anthony Scott",
|
||||||
|
"Atilla Berke Pekduyar",
|
||||||
|
"Decx _",
|
||||||
|
"Pat Hen",
|
||||||
|
"Jordan Shaw",
|
||||||
|
"Thesharingbrother",
|
||||||
|
"ResidentDeviant",
|
||||||
|
"四糸凜音",
|
||||||
|
"Nihongasuki",
|
||||||
|
"JC",
|
||||||
|
"Prompt Pirate",
|
||||||
|
"uwutismxd",
|
||||||
|
"FrxzenSnxw",
|
||||||
|
"zenobeus",
|
||||||
|
"Crocket",
|
||||||
|
"Jackthemind",
|
||||||
"chriphost",
|
"chriphost",
|
||||||
"KitKatM",
|
"KitKatM",
|
||||||
"ryoma",
|
"ryoma",
|
||||||
@@ -388,33 +397,42 @@
|
|||||||
"Menard",
|
"Menard",
|
||||||
"Skyfire83",
|
"Skyfire83",
|
||||||
"Adam Rinehart",
|
"Adam Rinehart",
|
||||||
"gzmzmvp",
|
"Pitpe11",
|
||||||
|
"TheD1rtyD03",
|
||||||
|
"moonpetal",
|
||||||
|
"SomeDude",
|
||||||
|
"g9p0o",
|
||||||
|
"TheHolySheep",
|
||||||
"raf8osz",
|
"raf8osz",
|
||||||
|
"Monte Won",
|
||||||
|
"SpringBootisTrash",
|
||||||
|
"carsten",
|
||||||
|
"ikok",
|
||||||
"ElitaSSJ4",
|
"ElitaSSJ4",
|
||||||
|
"Wolfe7D1",
|
||||||
"blikkies",
|
"blikkies",
|
||||||
"Chris",
|
"Chris",
|
||||||
"Gregory Kozhemiak",
|
"Gregory Kozhemiak",
|
||||||
|
"elleshar666",
|
||||||
"Shock Shockor",
|
"Shock Shockor",
|
||||||
|
"ACTUALLY_the_Real_Willem_Dafoe",
|
||||||
"Goldwaters",
|
"Goldwaters",
|
||||||
"Eric Whitney",
|
|
||||||
"Joey Callahan",
|
|
||||||
"Zude",
|
"Zude",
|
||||||
"Ivan Tadic",
|
|
||||||
"Mike Simone",
|
|
||||||
"John J Linehan",
|
"John J Linehan",
|
||||||
"Kyler",
|
"Kyler",
|
||||||
"Elliot E",
|
"Elliot E",
|
||||||
"Morgandel",
|
|
||||||
"Theerat Jiramate",
|
"Theerat Jiramate",
|
||||||
|
"Edward Kennedy",
|
||||||
|
"Justin Blaylock",
|
||||||
"aRtFuL_DodGeR",
|
"aRtFuL_DodGeR",
|
||||||
"Noah",
|
"Vane Holzer",
|
||||||
"X",
|
|
||||||
"Sloan Steddy",
|
|
||||||
"hexxish",
|
"hexxish",
|
||||||
|
"notedfakes",
|
||||||
"DarkSunset",
|
"DarkSunset",
|
||||||
"Nathan",
|
"Nathan",
|
||||||
"Billy Gladky",
|
"Billy Gladky",
|
||||||
"NICHOLAS BAXLEY",
|
"NICHOLAS BAXLEY",
|
||||||
|
"Michael Scott",
|
||||||
"Probis",
|
"Probis",
|
||||||
"Ed Wang",
|
"Ed Wang",
|
||||||
"ItsGeneralButtNaked",
|
"ItsGeneralButtNaked",
|
||||||
@@ -424,7 +442,6 @@
|
|||||||
"Youguang",
|
"Youguang",
|
||||||
"Saya",
|
"Saya",
|
||||||
"andrewzpong",
|
"andrewzpong",
|
||||||
"FrxzenSnxw",
|
|
||||||
"BossGame",
|
"BossGame",
|
||||||
"lrdchs",
|
"lrdchs",
|
||||||
"Tree Tagger",
|
"Tree Tagger",
|
||||||
@@ -437,17 +454,12 @@
|
|||||||
"Ginnie",
|
"Ginnie",
|
||||||
"Raku",
|
"Raku",
|
||||||
"emadsultan",
|
"emadsultan",
|
||||||
"Pitpe11",
|
|
||||||
"TheD1rtyD03",
|
|
||||||
"moonpetal",
|
|
||||||
"SomeDude",
|
|
||||||
"g9p0o",
|
|
||||||
"Pkrsky",
|
"Pkrsky",
|
||||||
"TheHolySheep",
|
"nanana",
|
||||||
"Monte Won",
|
"Pavlaki",
|
||||||
"SpringBootisTrash",
|
"Doug+Rintoul",
|
||||||
"carsten",
|
"Noor",
|
||||||
"ikok",
|
"Yorunai",
|
||||||
"quantenmecha",
|
"quantenmecha",
|
||||||
"Jason+Nash",
|
"Jason+Nash",
|
||||||
"BillyBoy84",
|
"BillyBoy84",
|
||||||
@@ -465,31 +477,27 @@
|
|||||||
"Welkor",
|
"Welkor",
|
||||||
"David Schenck",
|
"David Schenck",
|
||||||
"John Martin",
|
"John Martin",
|
||||||
"Wolfe7D1",
|
|
||||||
"Ink Temptation",
|
"Ink Temptation",
|
||||||
"moranqianlong",
|
"moranqianlong",
|
||||||
"Kalli Core",
|
"Kalli Core",
|
||||||
"Time Valentine",
|
"Time Valentine",
|
||||||
"elleshar666",
|
|
||||||
"ACTUALLY_the_Real_Willem_Dafoe",
|
|
||||||
"Михал Михалыч",
|
"Михал Михалыч",
|
||||||
"Matt",
|
"Matt",
|
||||||
"Kauffy",
|
"Kauffy",
|
||||||
|
"Frogmilk",
|
||||||
|
"SPJ",
|
||||||
"Kyron Mahan",
|
"Kyron Mahan",
|
||||||
"Edward Kennedy",
|
"Bryan Rutkowski",
|
||||||
"Justin Blaylock",
|
|
||||||
"Nick Kage",
|
"Nick Kage",
|
||||||
"TBitz33",
|
"TBitz33",
|
||||||
"Anonym dkjglfleeoeldldldlkf",
|
"Anonym dkjglfleeoeldldldlkf",
|
||||||
"Vane Holzer",
|
|
||||||
"psytrax",
|
"psytrax",
|
||||||
"Cyrus Fett",
|
"Cyrus Fett",
|
||||||
"Ezokewn",
|
"Ezokewn",
|
||||||
"SendingRavens",
|
"SendingRavens",
|
||||||
"Xenon Xue",
|
"Xenon Xue",
|
||||||
"notedfakes",
|
"Edward Ten Eyck",
|
||||||
"Michael Docherty",
|
"Michael Docherty",
|
||||||
"Michael Scott",
|
|
||||||
"Paul Hartsuyker",
|
"Paul Hartsuyker",
|
||||||
"Henrique Faiolli",
|
"Henrique Faiolli",
|
||||||
"elitassj",
|
"elitassj",
|
||||||
@@ -497,10 +505,13 @@
|
|||||||
"Jacob Winter",
|
"Jacob Winter",
|
||||||
"Ryan Presley Ng",
|
"Ryan Presley Ng",
|
||||||
"Wes Sims",
|
"Wes Sims",
|
||||||
|
"jinksta187",
|
||||||
"Donor4115",
|
"Donor4115",
|
||||||
|
"Manu Thetug",
|
||||||
"Lyavph",
|
"Lyavph",
|
||||||
"David",
|
"David",
|
||||||
"Meilo",
|
"Meilo",
|
||||||
|
"operationancut",
|
||||||
"Filippo Ferrari",
|
"Filippo Ferrari",
|
||||||
"shinonomeiro",
|
"shinonomeiro",
|
||||||
"Snille",
|
"Snille",
|
||||||
@@ -509,6 +520,7 @@
|
|||||||
"xybrightsummer",
|
"xybrightsummer",
|
||||||
"jreedatchison",
|
"jreedatchison",
|
||||||
"PhilW",
|
"PhilW",
|
||||||
|
"Marcus thronico",
|
||||||
"Janik",
|
"Janik",
|
||||||
"Cruel",
|
"Cruel",
|
||||||
"MRBlack",
|
"MRBlack",
|
||||||
@@ -519,7 +531,13 @@
|
|||||||
"Scott",
|
"Scott",
|
||||||
"Muratoraccio",
|
"Muratoraccio",
|
||||||
"D",
|
"D",
|
||||||
"nanana",
|
"MatteKey",
|
||||||
|
"Flob",
|
||||||
|
"ShiroSenpai",
|
||||||
|
"Inkognito",
|
||||||
|
"G",
|
||||||
|
"Tan+Huynh",
|
||||||
|
"D",
|
||||||
"Dark_Pest",
|
"Dark_Pest",
|
||||||
"Alex",
|
"Alex",
|
||||||
"Jacky+Ho",
|
"Jacky+Ho",
|
||||||
@@ -535,11 +553,7 @@
|
|||||||
"sfasdfasfdsa",
|
"sfasdfasfdsa",
|
||||||
"Alan+Cano",
|
"Alan+Cano",
|
||||||
"FeralOpticsAI",
|
"FeralOpticsAI",
|
||||||
"Pavlaki",
|
|
||||||
"generic404",
|
"generic404",
|
||||||
"Doug+Rintoul",
|
|
||||||
"Noor",
|
|
||||||
"Yorunai",
|
|
||||||
"abattoirblues",
|
"abattoirblues",
|
||||||
"zounik",
|
"zounik",
|
||||||
"4IXplr0r3r",
|
"4IXplr0r3r",
|
||||||
@@ -553,18 +567,21 @@
|
|||||||
"ja s",
|
"ja s",
|
||||||
"Doug Mason",
|
"Doug Mason",
|
||||||
"Jeremy Townsend",
|
"Jeremy Townsend",
|
||||||
|
"Dave Abraham",
|
||||||
|
"Joaquin Hierrezuelo",
|
||||||
"Locrospiel",
|
"Locrospiel",
|
||||||
"Frogmilk",
|
|
||||||
"Sean voets",
|
"Sean voets",
|
||||||
"Owen Gwosdz",
|
"Owen Gwosdz",
|
||||||
"SPJ",
|
"Jarrid Lee",
|
||||||
"Kor",
|
"Kor",
|
||||||
"Joseph Hanson",
|
"Joseph Hanson",
|
||||||
"Bryan Rutkowski",
|
"John Rednoulf",
|
||||||
|
"Boba Smith",
|
||||||
"Devil Lude",
|
"Devil Lude",
|
||||||
"David Murcko",
|
"David Murcko",
|
||||||
"Jack Dole",
|
"Jack Dole",
|
||||||
"max blo",
|
"max blo",
|
||||||
|
"Sauv",
|
||||||
"Steven",
|
"Steven",
|
||||||
"CptNeo",
|
"CptNeo",
|
||||||
"JackJohnnyJim",
|
"JackJohnnyJim",
|
||||||
@@ -572,7 +589,6 @@
|
|||||||
"Dmitry Ryzhov",
|
"Dmitry Ryzhov",
|
||||||
"Khánh Đặng",
|
"Khánh Đặng",
|
||||||
"Maso",
|
"Maso",
|
||||||
"Edward Ten Eyck",
|
|
||||||
"Eric Ketchum",
|
"Eric Ketchum",
|
||||||
"Kevin Wallace",
|
"Kevin Wallace",
|
||||||
"Jimmy Borup",
|
"Jimmy Borup",
|
||||||
@@ -580,14 +596,11 @@
|
|||||||
"mercur",
|
"mercur",
|
||||||
"Pete Pain",
|
"Pete Pain",
|
||||||
"RHopkirk",
|
"RHopkirk",
|
||||||
"jinksta187",
|
|
||||||
"Andrew Wilkinson",
|
"Andrew Wilkinson",
|
||||||
"Yavizu3d",
|
"Yavizu3d",
|
||||||
"Maxim",
|
"Maxim",
|
||||||
"Manu Thetug",
|
|
||||||
"Karlanx",
|
"Karlanx",
|
||||||
"Yves Poezevara",
|
"Yves Poezevara",
|
||||||
"operationancut",
|
|
||||||
"Teriak47",
|
"Teriak47",
|
||||||
"Just me",
|
"Just me",
|
||||||
"Raf Stahelin",
|
"Raf Stahelin",
|
||||||
@@ -611,7 +624,6 @@
|
|||||||
"pixl",
|
"pixl",
|
||||||
"Robin",
|
"Robin",
|
||||||
"chahknoir",
|
"chahknoir",
|
||||||
"Marcus thronico",
|
|
||||||
"nd",
|
"nd",
|
||||||
"keno94d",
|
"keno94d",
|
||||||
"James Melzer",
|
"James Melzer",
|
||||||
@@ -635,6 +647,19 @@
|
|||||||
"SelfishMedic",
|
"SelfishMedic",
|
||||||
"adderleighn",
|
"adderleighn",
|
||||||
"EnragedAntelope",
|
"EnragedAntelope",
|
||||||
|
"Kachac",
|
||||||
|
"tyrant2811",
|
||||||
|
"Kevin",
|
||||||
|
"Rune+Osnes",
|
||||||
|
"jcx29",
|
||||||
|
"cloudghost",
|
||||||
|
"Yongkwan+Lee",
|
||||||
|
"PoorStudent",
|
||||||
|
"lucites",
|
||||||
|
"Alex+Zaw",
|
||||||
|
"Mobius2020",
|
||||||
|
"ExLightSaber",
|
||||||
|
"YaboiRay",
|
||||||
"Drizzly",
|
"Drizzly",
|
||||||
"Sildoren",
|
"Sildoren",
|
||||||
"Darvidous",
|
"Darvidous",
|
||||||
@@ -658,17 +683,10 @@
|
|||||||
"you+halo9",
|
"you+halo9",
|
||||||
"YassineKhaled",
|
"YassineKhaled",
|
||||||
"YK12",
|
"YK12",
|
||||||
"MatteKey",
|
|
||||||
"Flob",
|
|
||||||
"ShiroSenpai",
|
|
||||||
"Somebody",
|
"Somebody",
|
||||||
"Inkognito",
|
|
||||||
"Somebody",
|
"Somebody",
|
||||||
"Gramer+Gumbyte",
|
|
||||||
"Crescent~San",
|
"Crescent~San",
|
||||||
"Tan+Huynh",
|
|
||||||
"AiGirlTS",
|
"AiGirlTS",
|
||||||
"D",
|
|
||||||
"datasl4ve",
|
"datasl4ve",
|
||||||
"Somebody",
|
"Somebody",
|
||||||
"koopa990",
|
"koopa990",
|
||||||
@@ -677,20 +695,25 @@
|
|||||||
"Bula",
|
"Bula",
|
||||||
"KUJYAKU",
|
"KUJYAKU",
|
||||||
"Coeur+de+cochon",
|
"Coeur+de+cochon",
|
||||||
|
"Obsidian.Studios",
|
||||||
"han b",
|
"han b",
|
||||||
"Nico",
|
"Nico",
|
||||||
"Maximilian Krischan",
|
"Maximilian Krischan",
|
||||||
"Banana Joe",
|
"Banana Joe",
|
||||||
"_ G3n",
|
"_ G3n",
|
||||||
"Donovan Jenkins",
|
"Donovan Jenkins",
|
||||||
|
"Hans Meier",
|
||||||
"Tú Nguyễn Lý Hoàng",
|
"Tú Nguyễn Lý Hoàng",
|
||||||
"shira1011",
|
"shira1011",
|
||||||
"Michael Eid",
|
"Michael Eid",
|
||||||
"beersandbacon",
|
"beersandbacon",
|
||||||
|
"Neko Desco",
|
||||||
"Bob barker",
|
"Bob barker",
|
||||||
"Ben D",
|
"Ben D",
|
||||||
"G",
|
"G",
|
||||||
"Ronan Delevacq",
|
"Ronan Delevacq",
|
||||||
|
"karim ben brik",
|
||||||
|
"Vinarus",
|
||||||
"james",
|
"james",
|
||||||
"Michael Zhu",
|
"Michael Zhu",
|
||||||
"Nemisu",
|
"Nemisu",
|
||||||
@@ -701,30 +724,30 @@
|
|||||||
"jumpd",
|
"jumpd",
|
||||||
"John C",
|
"John C",
|
||||||
"Rim",
|
"Rim",
|
||||||
"Dave Abraham",
|
|
||||||
"Joaquin Hierrezuelo",
|
|
||||||
"Jairus Knudsen",
|
"Jairus Knudsen",
|
||||||
"Jarrid Lee",
|
|
||||||
"Poophead27 Blyat",
|
"Poophead27 Blyat",
|
||||||
"Xan Dionysus",
|
"Xan Dionysus",
|
||||||
"Nathan lee",
|
"Nathan lee",
|
||||||
|
"Lyle Liston",
|
||||||
"Middo",
|
"Middo",
|
||||||
"Forbidden Atelier",
|
"Forbidden Atelier",
|
||||||
"John Rednoulf",
|
"Thomas Sankowski",
|
||||||
"Spire",
|
"Spire",
|
||||||
"DrB",
|
"DrB",
|
||||||
"AZ Party Oasis",
|
"AZ Party Oasis",
|
||||||
"Adictedtohumping",
|
"Adictedtohumping",
|
||||||
"Boba Smith",
|
|
||||||
"Towelie",
|
"Towelie",
|
||||||
|
"Ryan Smith",
|
||||||
"MR.Bear",
|
"MR.Bear",
|
||||||
"matt",
|
"matt",
|
||||||
"dsffsdfsdfsdfsdfsdf",
|
"dsffsdfsdfsdfsdfsdf",
|
||||||
"somethingtosay8",
|
"somethingtosay8",
|
||||||
"Jean-françois SEMA",
|
"Jean-françois SEMA",
|
||||||
|
"Terminuz",
|
||||||
"Kurt",
|
"Kurt",
|
||||||
"ivistorm",
|
"ivistorm",
|
||||||
"Sauv",
|
"Faburizu",
|
||||||
|
"Jack Lawfield",
|
||||||
"jimyjomson",
|
"jimyjomson",
|
||||||
"Borte",
|
"Borte",
|
||||||
"Chase Kwon",
|
"Chase Kwon",
|
||||||
@@ -744,6 +767,7 @@
|
|||||||
"hannibal",
|
"hannibal",
|
||||||
"Jo+Example",
|
"Jo+Example",
|
||||||
"BrentBertram",
|
"BrentBertram",
|
||||||
|
"inusanorthcape",
|
||||||
"Tigon",
|
"Tigon",
|
||||||
"eumelzocker",
|
"eumelzocker",
|
||||||
"dxjaymz",
|
"dxjaymz",
|
||||||
@@ -752,5 +776,5 @@
|
|||||||
"Somebody",
|
"Somebody",
|
||||||
"CK"
|
"CK"
|
||||||
],
|
],
|
||||||
"totalCount": 749
|
"totalCount": 773
|
||||||
}
|
}
|
||||||
@@ -232,6 +232,8 @@
|
|||||||
"license": "Lizenz",
|
"license": "Lizenz",
|
||||||
"noCreditRequired": "Kein Credit erforderlich",
|
"noCreditRequired": "Kein Credit erforderlich",
|
||||||
"allowSellingGeneratedContent": "Verkauf erlaubt",
|
"allowSellingGeneratedContent": "Verkauf erlaubt",
|
||||||
|
"allowSellingGeneratedContentTooltip": "Verkauf generierter Bilder erlauben",
|
||||||
|
"noCreditRequiredTooltip": "Modell ohne Nennung des Erstellers verwenden",
|
||||||
"noTags": "Keine Tags",
|
"noTags": "Keine Tags",
|
||||||
"autoTags": "Auto-Tags",
|
"autoTags": "Auto-Tags",
|
||||||
"noBaseModelMatches": "Keine Basismodelle entsprechen der aktuellen Suche.",
|
"noBaseModelMatches": "Keine Basismodelle entsprechen der aktuellen Suche.",
|
||||||
@@ -267,10 +269,10 @@
|
|||||||
},
|
},
|
||||||
"downloadBackend": {
|
"downloadBackend": {
|
||||||
"label": "Download-Backend",
|
"label": "Download-Backend",
|
||||||
"help": "Wähle aus, wie Modelldateien heruntergeladen werden. Python verwendet den eingebauten Downloader. aria2 verwendet den experimentellen externen Downloader-Prozess.",
|
"help": "Wähle aus, wie Modelldateien heruntergeladen werden. Python verwendet den eingebauten Downloader. aria2 verwendet den empfohlenen externen Downloader-Prozess.",
|
||||||
"options": {
|
"options": {
|
||||||
"python": "Python (integriert)",
|
"python": "Python (integriert)",
|
||||||
"aria2": "aria2 (experimentell)"
|
"aria2": "aria2 (empfohlen)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aria2cPath": {
|
"aria2cPath": {
|
||||||
@@ -577,7 +579,13 @@
|
|||||||
},
|
},
|
||||||
"misc": {
|
"misc": {
|
||||||
"includeTriggerWords": "Trigger Words in LoRA-Syntax einschließen",
|
"includeTriggerWords": "Trigger Words in LoRA-Syntax einschließen",
|
||||||
"includeTriggerWordsHelp": "Trainierte Trigger Words beim Kopieren der LoRA-Syntax in die Zwischenablage einschließen"
|
"includeTriggerWordsHelp": "Trainierte Trigger Words beim Kopieren der LoRA-Syntax in die Zwischenablage einschließen",
|
||||||
|
"loraSyntaxFormat": "LoRA-Syntaxformat",
|
||||||
|
"loraSyntaxFormatHelp": "LoRA-Syntaxformat. Der vollständige Pfad enthält den Unterordnerpfad (<lora:style/anime/x:1.0>) für verlustfreie Modellauflösung. Legacy verwendet nur den Dateinamen (<lora:x:1.0>) — A1111-Konvention, kann bei doppelten Dateinamen in verschiedenen Ordnern zu Mehrdeutigkeiten führen.",
|
||||||
|
"loraSyntaxFormatOptions": {
|
||||||
|
"full": "Vollständiger Pfad (Unterordner/Name)",
|
||||||
|
"legacy": "Legacy A1111 (nur Name)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"metadataArchive": {
|
"metadataArchive": {
|
||||||
"enableArchiveDb": "Metadaten-Archiv-Datenbank aktivieren",
|
"enableArchiveDb": "Metadaten-Archiv-Datenbank aktivieren",
|
||||||
@@ -1172,6 +1180,7 @@
|
|||||||
"editModelName": "Modellname bearbeiten",
|
"editModelName": "Modellname bearbeiten",
|
||||||
"editFileName": "Dateiname bearbeiten",
|
"editFileName": "Dateiname bearbeiten",
|
||||||
"editBaseModel": "Basis-Modell bearbeiten",
|
"editBaseModel": "Basis-Modell bearbeiten",
|
||||||
|
"editVersionName": "Versionsname bearbeiten",
|
||||||
"viewOnCivitai": "Auf Civitai anzeigen",
|
"viewOnCivitai": "Auf Civitai anzeigen",
|
||||||
"viewOnCivitaiText": "Auf Civitai anzeigen",
|
"viewOnCivitaiText": "Auf Civitai anzeigen",
|
||||||
"viewCreatorProfile": "Ersteller-Profil anzeigen",
|
"viewCreatorProfile": "Ersteller-Profil anzeigen",
|
||||||
@@ -1921,9 +1930,32 @@
|
|||||||
"warning": "Handlungsbedarf",
|
"warning": "Handlungsbedarf",
|
||||||
"error": "Aktion erforderlich"
|
"error": "Aktion erforderlich"
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"civitai_api_key": {
|
||||||
|
"title": "Civitai API Key"
|
||||||
|
},
|
||||||
|
"cache_health": {
|
||||||
|
"title": "Model Cache Health"
|
||||||
|
},
|
||||||
|
"filename_conflicts": {
|
||||||
|
"title": "Duplicate Filename Conflicts"
|
||||||
|
},
|
||||||
|
"ui_version": {
|
||||||
|
"title": "UI Version"
|
||||||
|
}
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"runAgain": "Erneut ausführen",
|
"runAgain": "Erneut ausführen",
|
||||||
"exportBundle": "Paket exportieren"
|
"exportBundle": "Paket exportieren",
|
||||||
|
"open-settings": "Open Settings",
|
||||||
|
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||||
|
"repair-cache": "Rebuild Cache",
|
||||||
|
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||||
|
"reload-page": "Reload UI"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"conflicts": "Conflicts",
|
||||||
|
"version": "Version"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailed": "Diagnose konnte nicht geladen werden: {message}",
|
"loadFailed": "Diagnose konnte nicht geladen werden: {message}",
|
||||||
@@ -1935,6 +1967,15 @@
|
|||||||
"conflictsResolveFailed": "Auflösung der Dateinamenskonflikte fehlgeschlagen: {message}"
|
"conflictsResolveFailed": "Auflösung der Dateinamenskonflikte fehlgeschlagen: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"conflictConfirm": {
|
||||||
|
"title": "Dateinamenskonflikte auflösen",
|
||||||
|
"message": "Umbenennen durch Anhängen eines 4-stelligen Hashs an jeden doppelten Dateinamen.",
|
||||||
|
"note": "Dieser Vorgang benennt Dateien auf der Festplatte um. Modellreferenzen in vorhandenen Workflows müssen möglicherweise aktualisiert werden, wenn Sie das A1111-Syntaxformat verwenden.",
|
||||||
|
"detail": "Beispiel: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||||
|
"impact": "Benennt <strong>{count}</strong> Datei(en) in <strong>{groups}</strong> Duplikatgruppe(n) um",
|
||||||
|
"confirm": "Dateien umbenennen",
|
||||||
|
"cancel": "Abbrechen"
|
||||||
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
"versionMismatch": {
|
"versionMismatch": {
|
||||||
"title": "Anwendungs-Update erkannt",
|
"title": "Anwendungs-Update erkannt",
|
||||||
|
|||||||
@@ -232,6 +232,8 @@
|
|||||||
"license": "License",
|
"license": "License",
|
||||||
"noCreditRequired": "No Credit Required",
|
"noCreditRequired": "No Credit Required",
|
||||||
"allowSellingGeneratedContent": "Allow Selling",
|
"allowSellingGeneratedContent": "Allow Selling",
|
||||||
|
"allowSellingGeneratedContentTooltip": "Allow selling generated images",
|
||||||
|
"noCreditRequiredTooltip": "Use the model without crediting the creator",
|
||||||
"noTags": "No tags",
|
"noTags": "No tags",
|
||||||
"autoTags": "Auto Tags",
|
"autoTags": "Auto Tags",
|
||||||
"noBaseModelMatches": "No base models match the current search.",
|
"noBaseModelMatches": "No base models match the current search.",
|
||||||
@@ -267,10 +269,10 @@
|
|||||||
},
|
},
|
||||||
"downloadBackend": {
|
"downloadBackend": {
|
||||||
"label": "Download backend",
|
"label": "Download backend",
|
||||||
"help": "Choose how model files are downloaded. Python uses the built-in downloader. aria2 uses the experimental external downloader process.",
|
"help": "Choose how model files are downloaded. Python uses the built-in downloader. aria2 uses the recommended external downloader process.",
|
||||||
"options": {
|
"options": {
|
||||||
"python": "Python (built-in)",
|
"python": "Python (built-in)",
|
||||||
"aria2": "aria2 (experimental)"
|
"aria2": "aria2 (recommended)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aria2cPath": {
|
"aria2cPath": {
|
||||||
@@ -577,7 +579,13 @@
|
|||||||
},
|
},
|
||||||
"misc": {
|
"misc": {
|
||||||
"includeTriggerWords": "Include Trigger Words in LoRA Syntax",
|
"includeTriggerWords": "Include Trigger Words in LoRA Syntax",
|
||||||
"includeTriggerWordsHelp": "Include trained trigger words when copying LoRA syntax to clipboard"
|
"includeTriggerWordsHelp": "Include trained trigger words when copying LoRA syntax to clipboard",
|
||||||
|
"loraSyntaxFormat": "LoRA Syntax Format",
|
||||||
|
"loraSyntaxFormatHelp": "LoRA syntax format. Full includes subfolder path (<lora:style/anime/x:1.0>) for lossless model resolution. Legacy uses filename only (<lora:x:1.0>) — A1111 convention, may be ambiguous with duplicate filenames across folders.",
|
||||||
|
"loraSyntaxFormatOptions": {
|
||||||
|
"full": "Full path (subfolder/name)",
|
||||||
|
"legacy": "Legacy A1111 (name only)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"metadataArchive": {
|
"metadataArchive": {
|
||||||
"enableArchiveDb": "Enable Metadata Archive Database",
|
"enableArchiveDb": "Enable Metadata Archive Database",
|
||||||
@@ -1172,6 +1180,7 @@
|
|||||||
"editModelName": "Edit model name",
|
"editModelName": "Edit model name",
|
||||||
"editFileName": "Edit file name",
|
"editFileName": "Edit file name",
|
||||||
"editBaseModel": "Edit base model",
|
"editBaseModel": "Edit base model",
|
||||||
|
"editVersionName": "Edit version name",
|
||||||
"viewOnCivitai": "View on Civitai",
|
"viewOnCivitai": "View on Civitai",
|
||||||
"viewOnCivitaiText": "View on Civitai",
|
"viewOnCivitaiText": "View on Civitai",
|
||||||
"viewCreatorProfile": "View Creator Profile",
|
"viewCreatorProfile": "View Creator Profile",
|
||||||
@@ -1921,9 +1930,32 @@
|
|||||||
"warning": "Needs Attention",
|
"warning": "Needs Attention",
|
||||||
"error": "Action Required"
|
"error": "Action Required"
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"civitai_api_key": {
|
||||||
|
"title": "Civitai API Key"
|
||||||
|
},
|
||||||
|
"cache_health": {
|
||||||
|
"title": "Model Cache Health"
|
||||||
|
},
|
||||||
|
"filename_conflicts": {
|
||||||
|
"title": "Duplicate Filename Conflicts"
|
||||||
|
},
|
||||||
|
"ui_version": {
|
||||||
|
"title": "UI Version"
|
||||||
|
}
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"runAgain": "Run Again",
|
"runAgain": "Run Again",
|
||||||
"exportBundle": "Export Bundle"
|
"exportBundle": "Export Bundle",
|
||||||
|
"open-settings": "Open Settings",
|
||||||
|
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||||
|
"repair-cache": "Rebuild Cache",
|
||||||
|
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||||
|
"reload-page": "Reload UI"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"conflicts": "Conflicts",
|
||||||
|
"version": "Version"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailed": "Failed to load diagnostics: {message}",
|
"loadFailed": "Failed to load diagnostics: {message}",
|
||||||
@@ -1935,6 +1967,15 @@
|
|||||||
"conflictsResolveFailed": "Failed to resolve filename conflicts: {message}"
|
"conflictsResolveFailed": "Failed to resolve filename conflicts: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"conflictConfirm": {
|
||||||
|
"title": "Resolve Filename Conflicts",
|
||||||
|
"message": "Renaming by appending a 4-character hash to each duplicate filename.",
|
||||||
|
"note": "This operation renames files on disk. Model references in existing workflows may need updating if you use the A1111 syntax format.",
|
||||||
|
"detail": "Example: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||||
|
"impact": "Will rename <strong>{count}</strong> file(s) across <strong>{groups}</strong> duplicate group(s).",
|
||||||
|
"confirm": "Rename Files",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
"versionMismatch": {
|
"versionMismatch": {
|
||||||
"title": "Application Update Detected",
|
"title": "Application Update Detected",
|
||||||
|
|||||||
@@ -232,6 +232,8 @@
|
|||||||
"license": "Licencia",
|
"license": "Licencia",
|
||||||
"noCreditRequired": "Sin crédito requerido",
|
"noCreditRequired": "Sin crédito requerido",
|
||||||
"allowSellingGeneratedContent": "Venta permitida",
|
"allowSellingGeneratedContent": "Venta permitida",
|
||||||
|
"allowSellingGeneratedContentTooltip": "Permitir la venta de imágenes generadas",
|
||||||
|
"noCreditRequiredTooltip": "Usar el modelo sin atribuir al creador",
|
||||||
"noTags": "Sin etiquetas",
|
"noTags": "Sin etiquetas",
|
||||||
"autoTags": "Etiquetas automáticas",
|
"autoTags": "Etiquetas automáticas",
|
||||||
"noBaseModelMatches": "Ningún modelo base coincide con la búsqueda actual.",
|
"noBaseModelMatches": "Ningún modelo base coincide con la búsqueda actual.",
|
||||||
@@ -267,10 +269,10 @@
|
|||||||
},
|
},
|
||||||
"downloadBackend": {
|
"downloadBackend": {
|
||||||
"label": "Backend de descarga",
|
"label": "Backend de descarga",
|
||||||
"help": "Elige cómo se descargan los archivos del modelo. Python usa el descargador integrado. aria2 usa el proceso externo experimental de descarga.",
|
"help": "Elige cómo se descargan los archivos del modelo. Python usa el descargador integrado. aria2 usa el proceso externo recomendado de descarga.",
|
||||||
"options": {
|
"options": {
|
||||||
"python": "Python (integrado)",
|
"python": "Python (integrado)",
|
||||||
"aria2": "aria2 (experimental)"
|
"aria2": "aria2 (recomendado)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aria2cPath": {
|
"aria2cPath": {
|
||||||
@@ -577,7 +579,13 @@
|
|||||||
},
|
},
|
||||||
"misc": {
|
"misc": {
|
||||||
"includeTriggerWords": "Incluir palabras clave en la sintaxis de LoRA",
|
"includeTriggerWords": "Incluir palabras clave en la sintaxis de LoRA",
|
||||||
"includeTriggerWordsHelp": "Incluir palabras clave entrenadas al copiar la sintaxis de LoRA al portapapeles"
|
"includeTriggerWordsHelp": "Incluir palabras clave entrenadas al copiar la sintaxis de LoRA al portapapeles",
|
||||||
|
"loraSyntaxFormat": "Formato de sintaxis LoRA",
|
||||||
|
"loraSyntaxFormatHelp": "Formato de sintaxis LoRA. El formato completo incluye la ruta de la subcarpeta (<lora:style/anime/x:1.0>) para una resolución de modelo sin pérdidas. El formato heredado usa solo el nombre del archivo (<lora:x:1.0>) — convención A1111, puede ser ambiguo con nombres de archivo duplicados entre carpetas.",
|
||||||
|
"loraSyntaxFormatOptions": {
|
||||||
|
"full": "Ruta completa (subcarpeta/nombre)",
|
||||||
|
"legacy": "A1111 heredado (solo nombre)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"metadataArchive": {
|
"metadataArchive": {
|
||||||
"enableArchiveDb": "Habilitar base de datos de archivo de metadatos",
|
"enableArchiveDb": "Habilitar base de datos de archivo de metadatos",
|
||||||
@@ -1172,6 +1180,7 @@
|
|||||||
"editModelName": "Editar nombre del modelo",
|
"editModelName": "Editar nombre del modelo",
|
||||||
"editFileName": "Editar nombre de archivo",
|
"editFileName": "Editar nombre de archivo",
|
||||||
"editBaseModel": "Editar modelo base",
|
"editBaseModel": "Editar modelo base",
|
||||||
|
"editVersionName": "Editar nombre de versión",
|
||||||
"viewOnCivitai": "Ver en Civitai",
|
"viewOnCivitai": "Ver en Civitai",
|
||||||
"viewOnCivitaiText": "Ver en Civitai",
|
"viewOnCivitaiText": "Ver en Civitai",
|
||||||
"viewCreatorProfile": "Ver perfil del creador",
|
"viewCreatorProfile": "Ver perfil del creador",
|
||||||
@@ -1921,9 +1930,32 @@
|
|||||||
"warning": "Requiere atención",
|
"warning": "Requiere atención",
|
||||||
"error": "Se requiere acción"
|
"error": "Se requiere acción"
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"civitai_api_key": {
|
||||||
|
"title": "Civitai API Key"
|
||||||
|
},
|
||||||
|
"cache_health": {
|
||||||
|
"title": "Model Cache Health"
|
||||||
|
},
|
||||||
|
"filename_conflicts": {
|
||||||
|
"title": "Duplicate Filename Conflicts"
|
||||||
|
},
|
||||||
|
"ui_version": {
|
||||||
|
"title": "UI Version"
|
||||||
|
}
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"runAgain": "Ejecutar de nuevo",
|
"runAgain": "Ejecutar de nuevo",
|
||||||
"exportBundle": "Exportar paquete"
|
"exportBundle": "Exportar paquete",
|
||||||
|
"open-settings": "Open Settings",
|
||||||
|
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||||
|
"repair-cache": "Rebuild Cache",
|
||||||
|
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||||
|
"reload-page": "Reload UI"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"conflicts": "Conflicts",
|
||||||
|
"version": "Version"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailed": "Error al cargar los diagnósticos: {message}",
|
"loadFailed": "Error al cargar los diagnósticos: {message}",
|
||||||
@@ -1935,6 +1967,15 @@
|
|||||||
"conflictsResolveFailed": "Error al resolver conflictos de nombre de archivo: {message}"
|
"conflictsResolveFailed": "Error al resolver conflictos de nombre de archivo: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"conflictConfirm": {
|
||||||
|
"title": "Resolver conflictos de nombres de archivo",
|
||||||
|
"message": "Renombrar añadiendo un hash de 4 caracteres a cada nombre de archivo duplicado.",
|
||||||
|
"note": "Esta operación renombra archivos en el disco. Es posible que las referencias a modelos en flujos de trabajo existentes deban actualizarse si usas el formato de sintaxis A1111.",
|
||||||
|
"detail": "Ejemplo: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||||
|
"impact": "Renombrará <strong>{count}</strong> archivo(s) en <strong>{groups}</strong> grupo(s) de duplicados",
|
||||||
|
"confirm": "Renombrar archivos",
|
||||||
|
"cancel": "Cancelar"
|
||||||
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
"versionMismatch": {
|
"versionMismatch": {
|
||||||
"title": "Actualización de la aplicación detectada",
|
"title": "Actualización de la aplicación detectada",
|
||||||
|
|||||||
@@ -232,6 +232,8 @@
|
|||||||
"license": "Licence",
|
"license": "Licence",
|
||||||
"noCreditRequired": "Crédit non requis",
|
"noCreditRequired": "Crédit non requis",
|
||||||
"allowSellingGeneratedContent": "Vente autorisée",
|
"allowSellingGeneratedContent": "Vente autorisée",
|
||||||
|
"allowSellingGeneratedContentTooltip": "Autoriser la vente d\"images générées",
|
||||||
|
"noCreditRequiredTooltip": "Utiliser le modèle sans créditer le créateur",
|
||||||
"noTags": "Aucun tag",
|
"noTags": "Aucun tag",
|
||||||
"autoTags": "Auto-Tags",
|
"autoTags": "Auto-Tags",
|
||||||
"noBaseModelMatches": "Aucun modèle de base ne correspond à la recherche actuelle.",
|
"noBaseModelMatches": "Aucun modèle de base ne correspond à la recherche actuelle.",
|
||||||
@@ -267,10 +269,10 @@
|
|||||||
},
|
},
|
||||||
"downloadBackend": {
|
"downloadBackend": {
|
||||||
"label": "Moteur de téléchargement",
|
"label": "Moteur de téléchargement",
|
||||||
"help": "Choisissez comment les fichiers de modèles sont téléchargés. Python utilise le téléchargeur intégré. aria2 utilise le processus externe expérimental de téléchargement.",
|
"help": "Choisissez comment les fichiers de modèles sont téléchargés. Python utilise le téléchargeur intégré. aria2 utilise le processus externe recommandé de téléchargement.",
|
||||||
"options": {
|
"options": {
|
||||||
"python": "Python (intégré)",
|
"python": "Python (intégré)",
|
||||||
"aria2": "aria2 (expérimental)"
|
"aria2": "aria2 (recommandé)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aria2cPath": {
|
"aria2cPath": {
|
||||||
@@ -577,7 +579,13 @@
|
|||||||
},
|
},
|
||||||
"misc": {
|
"misc": {
|
||||||
"includeTriggerWords": "Inclure les mots-clés dans la syntaxe LoRA",
|
"includeTriggerWords": "Inclure les mots-clés dans la syntaxe LoRA",
|
||||||
"includeTriggerWordsHelp": "Inclure les mots-clés d'entraînement lors de la copie de la syntaxe LoRA dans le presse-papiers"
|
"includeTriggerWordsHelp": "Inclure les mots-clés d'entraînement lors de la copie de la syntaxe LoRA dans le presse-papiers",
|
||||||
|
"loraSyntaxFormat": "Format de syntaxe LoRA",
|
||||||
|
"loraSyntaxFormatHelp": "Format de syntaxe LoRA. Le format complet inclut le chemin du sous-dossier (<lora:style/anime/x:1.0>) pour une résolution de modèle sans perte. Le format hérité utilise uniquement le nom du fichier (<lora:x:1.0>) — convention A1111, peut être ambiguë en cas de noms de fichiers en double dans différents dossiers.",
|
||||||
|
"loraSyntaxFormatOptions": {
|
||||||
|
"full": "Chemin complet (sous-dossier/nom)",
|
||||||
|
"legacy": "A1111 hérité (nom uniquement)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"metadataArchive": {
|
"metadataArchive": {
|
||||||
"enableArchiveDb": "Activer la base de données d'archive des métadonnées",
|
"enableArchiveDb": "Activer la base de données d'archive des métadonnées",
|
||||||
@@ -1172,6 +1180,7 @@
|
|||||||
"editModelName": "Modifier le nom du modèle",
|
"editModelName": "Modifier le nom du modèle",
|
||||||
"editFileName": "Modifier le nom de fichier",
|
"editFileName": "Modifier le nom de fichier",
|
||||||
"editBaseModel": "Modifier le modèle de base",
|
"editBaseModel": "Modifier le modèle de base",
|
||||||
|
"editVersionName": "Modifier le nom de la version",
|
||||||
"viewOnCivitai": "Voir sur Civitai",
|
"viewOnCivitai": "Voir sur Civitai",
|
||||||
"viewOnCivitaiText": "Voir sur Civitai",
|
"viewOnCivitaiText": "Voir sur Civitai",
|
||||||
"viewCreatorProfile": "Voir le profil du créateur",
|
"viewCreatorProfile": "Voir le profil du créateur",
|
||||||
@@ -1921,9 +1930,32 @@
|
|||||||
"warning": "Nécessite une attention",
|
"warning": "Nécessite une attention",
|
||||||
"error": "Action requise"
|
"error": "Action requise"
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"civitai_api_key": {
|
||||||
|
"title": "Civitai API Key"
|
||||||
|
},
|
||||||
|
"cache_health": {
|
||||||
|
"title": "Model Cache Health"
|
||||||
|
},
|
||||||
|
"filename_conflicts": {
|
||||||
|
"title": "Duplicate Filename Conflicts"
|
||||||
|
},
|
||||||
|
"ui_version": {
|
||||||
|
"title": "UI Version"
|
||||||
|
}
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"runAgain": "Relancer",
|
"runAgain": "Relancer",
|
||||||
"exportBundle": "Exporter le lot"
|
"exportBundle": "Exporter le lot",
|
||||||
|
"open-settings": "Open Settings",
|
||||||
|
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||||
|
"repair-cache": "Rebuild Cache",
|
||||||
|
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||||
|
"reload-page": "Reload UI"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"conflicts": "Conflicts",
|
||||||
|
"version": "Version"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailed": "Échec du chargement des diagnostics : {message}",
|
"loadFailed": "Échec du chargement des diagnostics : {message}",
|
||||||
@@ -1935,6 +1967,15 @@
|
|||||||
"conflictsResolveFailed": "Échec de la résolution des conflits de nom de fichier : {message}"
|
"conflictsResolveFailed": "Échec de la résolution des conflits de nom de fichier : {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"conflictConfirm": {
|
||||||
|
"title": "Résoudre les conflits de noms de fichiers",
|
||||||
|
"message": "Renommer en ajoutant un hachage de 4 caractères à chaque nom de fichier en double.",
|
||||||
|
"note": "Cette opération renomme les fichiers sur le disque. Les références de modèle dans les workflows existants peuvent nécessiter une mise à jour si vous utilisez le format de syntaxe A1111.",
|
||||||
|
"detail": "Exemple : <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||||
|
"impact": "Renommera <strong>{count}</strong> fichier(s) dans <strong>{groups}</strong> groupe(s) de doublons",
|
||||||
|
"confirm": "Renommer les fichiers",
|
||||||
|
"cancel": "Annuler"
|
||||||
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
"versionMismatch": {
|
"versionMismatch": {
|
||||||
"title": "Mise à jour de l'application détectée",
|
"title": "Mise à jour de l'application détectée",
|
||||||
|
|||||||
@@ -232,6 +232,8 @@
|
|||||||
"license": "רישיון",
|
"license": "רישיון",
|
||||||
"noCreditRequired": "ללא קרדיט נדרש",
|
"noCreditRequired": "ללא קרדיט נדרש",
|
||||||
"allowSellingGeneratedContent": "אפשר מכירה",
|
"allowSellingGeneratedContent": "אפשר מכירה",
|
||||||
|
"allowSellingGeneratedContentTooltip": "אפשר מכירת תמונות שנוצרו",
|
||||||
|
"noCreditRequiredTooltip": "שימוש במודל ללא מתן קרדיט ליוצר",
|
||||||
"noTags": "ללא תגיות",
|
"noTags": "ללא תגיות",
|
||||||
"autoTags": "תגיות אוטומטיות",
|
"autoTags": "תגיות אוטומטיות",
|
||||||
"noBaseModelMatches": "אין מודלי בסיס התואמים לחיפוש הנוכחי.",
|
"noBaseModelMatches": "אין מודלי בסיס התואמים לחיפוש הנוכחי.",
|
||||||
@@ -267,10 +269,10 @@
|
|||||||
},
|
},
|
||||||
"downloadBackend": {
|
"downloadBackend": {
|
||||||
"label": "מנגנון הורדה",
|
"label": "מנגנון הורדה",
|
||||||
"help": "בחר כיצד יורדים קבצי המודל. Python משתמש במוריד המובנה. aria2 משתמש בתהליך הורדה חיצוני ניסיוני.",
|
"help": "בחר כיצד יורדים קבצי המודל. Python משתמש במוריד המובנה. aria2 משתמש בתהליך הורדה חיצוני מומלץ.",
|
||||||
"options": {
|
"options": {
|
||||||
"python": "Python (מובנה)",
|
"python": "Python (מובנה)",
|
||||||
"aria2": "aria2 (ניסיוני)"
|
"aria2": "aria2 (מומלץ)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aria2cPath": {
|
"aria2cPath": {
|
||||||
@@ -577,7 +579,13 @@
|
|||||||
},
|
},
|
||||||
"misc": {
|
"misc": {
|
||||||
"includeTriggerWords": "כלול מילות טריגר בתחביר LoRA",
|
"includeTriggerWords": "כלול מילות טריגר בתחביר LoRA",
|
||||||
"includeTriggerWordsHelp": "כלול מילות טריגר מאומנות בעת העתקת תחביר LoRA ללוח"
|
"includeTriggerWordsHelp": "כלול מילות טריגר מאומנות בעת העתקת תחביר LoRA ללוח",
|
||||||
|
"loraSyntaxFormat": "פורמט תחביר LoRA",
|
||||||
|
"loraSyntaxFormatHelp": "פורמט תחביר LoRA. נתיב מלא כולל תת-תיקייה (<lora:style/anime/x:1.0>) לפתרון מודל ללא אובדן. גרסה ישנה משתמשת בשם קובץ בלבד (<lora:x:1.0>) — מוסכמת A1111, עלולה להיות לא חד משמעית עם שמות קבצים כפולים בתיקיות שונות.",
|
||||||
|
"loraSyntaxFormatOptions": {
|
||||||
|
"full": "נתיב מלא (תת-תיקייה/שם)",
|
||||||
|
"legacy": "A1111 ישן (שם בלבד)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"metadataArchive": {
|
"metadataArchive": {
|
||||||
"enableArchiveDb": "הפעל מסד נתונים של ארכיון מטא-דאטה",
|
"enableArchiveDb": "הפעל מסד נתונים של ארכיון מטא-דאטה",
|
||||||
@@ -1172,6 +1180,7 @@
|
|||||||
"editModelName": "ערוך שם מודל",
|
"editModelName": "ערוך שם מודל",
|
||||||
"editFileName": "ערוך שם קובץ",
|
"editFileName": "ערוך שם קובץ",
|
||||||
"editBaseModel": "ערוך מודל בסיס",
|
"editBaseModel": "ערוך מודל בסיס",
|
||||||
|
"editVersionName": "ערוך שם גרסה",
|
||||||
"viewOnCivitai": "הצג ב-Civitai",
|
"viewOnCivitai": "הצג ב-Civitai",
|
||||||
"viewOnCivitaiText": "הצג ב-Civitai",
|
"viewOnCivitaiText": "הצג ב-Civitai",
|
||||||
"viewCreatorProfile": "הצג פרופיל יוצר",
|
"viewCreatorProfile": "הצג פרופיל יוצר",
|
||||||
@@ -1921,9 +1930,32 @@
|
|||||||
"warning": "דורש תשומת לב",
|
"warning": "דורש תשומת לב",
|
||||||
"error": "נדרשת פעולה"
|
"error": "נדרשת פעולה"
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"civitai_api_key": {
|
||||||
|
"title": "Civitai API Key"
|
||||||
|
},
|
||||||
|
"cache_health": {
|
||||||
|
"title": "Model Cache Health"
|
||||||
|
},
|
||||||
|
"filename_conflicts": {
|
||||||
|
"title": "Duplicate Filename Conflicts"
|
||||||
|
},
|
||||||
|
"ui_version": {
|
||||||
|
"title": "UI Version"
|
||||||
|
}
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"runAgain": "הפעל שוב",
|
"runAgain": "הפעל שוב",
|
||||||
"exportBundle": "ייצוא חבילה"
|
"exportBundle": "ייצוא חבילה",
|
||||||
|
"open-settings": "Open Settings",
|
||||||
|
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||||
|
"repair-cache": "Rebuild Cache",
|
||||||
|
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||||
|
"reload-page": "Reload UI"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"conflicts": "Conflicts",
|
||||||
|
"version": "Version"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailed": "טעינת האבחון נכשלה: {message}",
|
"loadFailed": "טעינת האבחון נכשלה: {message}",
|
||||||
@@ -1935,6 +1967,15 @@
|
|||||||
"conflictsResolveFailed": "פתרון התנגשויות שמות קבצים נכשל: {message}"
|
"conflictsResolveFailed": "פתרון התנגשויות שמות קבצים נכשל: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"conflictConfirm": {
|
||||||
|
"title": "פתור התנגשויות בשמות קבצים",
|
||||||
|
"message": "שינוי שם על ידי הוספת האש באורך 4 תווים לכל שם קובץ כפול.",
|
||||||
|
"note": "פעולה זו משנה שמות של קבצים בדיסק. ייתכן שיהיה צורך לעדכן הפניות למודלים בזרימות עבודה קיימות אם אתה משתמש בפורמט התחביר A1111.",
|
||||||
|
"detail": "דוגמה: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||||
|
"impact": "ישנה שם של <strong>{count}</strong> קבצים ב-<strong>{groups}</strong> קבוצות כפולות",
|
||||||
|
"confirm": "שנה שמות קבצים",
|
||||||
|
"cancel": "ביטול"
|
||||||
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
"versionMismatch": {
|
"versionMismatch": {
|
||||||
"title": "זוהה עדכון יישום",
|
"title": "זוהה עדכון יישום",
|
||||||
|
|||||||
@@ -232,6 +232,8 @@
|
|||||||
"license": "ライセンス",
|
"license": "ライセンス",
|
||||||
"noCreditRequired": "クレジット不要",
|
"noCreditRequired": "クレジット不要",
|
||||||
"allowSellingGeneratedContent": "販売許可",
|
"allowSellingGeneratedContent": "販売許可",
|
||||||
|
"allowSellingGeneratedContentTooltip": "生成した画像の販売を許可",
|
||||||
|
"noCreditRequiredTooltip": "クレジット表記なしでモデルを使用可能",
|
||||||
"noTags": "タグなし",
|
"noTags": "タグなし",
|
||||||
"autoTags": "自動タグ",
|
"autoTags": "自動タグ",
|
||||||
"noBaseModelMatches": "現在の検索に一致するベースモデルはありません。",
|
"noBaseModelMatches": "現在の検索に一致するベースモデルはありません。",
|
||||||
@@ -267,10 +269,10 @@
|
|||||||
},
|
},
|
||||||
"downloadBackend": {
|
"downloadBackend": {
|
||||||
"label": "ダウンロードバックエンド",
|
"label": "ダウンロードバックエンド",
|
||||||
"help": "モデルファイルのダウンロード方法を選択します。Python は内蔵ダウンローダーを使用し、aria2 は実験的な外部ダウンローダープロセスを使用します。",
|
"help": "モデルファイルのダウンロード方法を選択します。Python は内蔵ダウンローダーを使用し、aria2 は推奨の外部ダウンローダープロセスを使用します。",
|
||||||
"options": {
|
"options": {
|
||||||
"python": "Python(内蔵)",
|
"python": "Python(内蔵)",
|
||||||
"aria2": "aria2(実験的)"
|
"aria2": "aria2(推奨)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aria2cPath": {
|
"aria2cPath": {
|
||||||
@@ -577,7 +579,13 @@
|
|||||||
},
|
},
|
||||||
"misc": {
|
"misc": {
|
||||||
"includeTriggerWords": "LoRA構文にトリガーワードを含める",
|
"includeTriggerWords": "LoRA構文にトリガーワードを含める",
|
||||||
"includeTriggerWordsHelp": "LoRA構文をクリップボードにコピーする際、学習済みトリガーワードを含めます"
|
"includeTriggerWordsHelp": "LoRA構文をクリップボードにコピーする際、学習済みトリガーワードを含めます",
|
||||||
|
"loraSyntaxFormat": "LoRA構文形式",
|
||||||
|
"loraSyntaxFormatHelp": "LoRA構文形式。フルパスはサブフォルダパスを含み(<lora:style/anime/x:1.0>)、モデルをロスレスで解決します。レガシーはファイル名のみ(<lora:x:1.0>)— A1111規約ですが、フォルダ間でファイル名が重複する場合に曖昧になる可能性があります。",
|
||||||
|
"loraSyntaxFormatOptions": {
|
||||||
|
"full": "フルパス(サブフォルダ/名前)",
|
||||||
|
"legacy": "レガシーA1111(名前のみ)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"metadataArchive": {
|
"metadataArchive": {
|
||||||
"enableArchiveDb": "メタデータアーカイブデータベースを有効化",
|
"enableArchiveDb": "メタデータアーカイブデータベースを有効化",
|
||||||
@@ -1172,6 +1180,7 @@
|
|||||||
"editModelName": "モデル名を編集",
|
"editModelName": "モデル名を編集",
|
||||||
"editFileName": "ファイル名を編集",
|
"editFileName": "ファイル名を編集",
|
||||||
"editBaseModel": "ベースモデルを編集",
|
"editBaseModel": "ベースモデルを編集",
|
||||||
|
"editVersionName": "バージョン名を編集",
|
||||||
"viewOnCivitai": "Civitaiで表示",
|
"viewOnCivitai": "Civitaiで表示",
|
||||||
"viewOnCivitaiText": "Civitaiで表示",
|
"viewOnCivitaiText": "Civitaiで表示",
|
||||||
"viewCreatorProfile": "作成者プロフィールを表示",
|
"viewCreatorProfile": "作成者プロフィールを表示",
|
||||||
@@ -1921,9 +1930,32 @@
|
|||||||
"warning": "要注意",
|
"warning": "要注意",
|
||||||
"error": "対応が必要"
|
"error": "対応が必要"
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"civitai_api_key": {
|
||||||
|
"title": "Civitai API キー"
|
||||||
|
},
|
||||||
|
"cache_health": {
|
||||||
|
"title": "モデルキャッシュの健全性"
|
||||||
|
},
|
||||||
|
"filename_conflicts": {
|
||||||
|
"title": "ファイル名重複競合"
|
||||||
|
},
|
||||||
|
"ui_version": {
|
||||||
|
"title": "UI バージョン"
|
||||||
|
}
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"runAgain": "再実行",
|
"runAgain": "再実行",
|
||||||
"exportBundle": "パッケージをエクスポート"
|
"exportBundle": "パッケージをエクスポート",
|
||||||
|
"open-settings": "設定を開く",
|
||||||
|
"open-settings-syntax-format": "フルパス構文に切り替え",
|
||||||
|
"repair-cache": "キャッシュを再構築",
|
||||||
|
"resolve-filename-conflicts": "競合を解決",
|
||||||
|
"reload-page": "UI をリロード"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"conflicts": "競合",
|
||||||
|
"version": "バージョン"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailed": "診断の読み込みに失敗しました: {message}",
|
"loadFailed": "診断の読み込みに失敗しました: {message}",
|
||||||
@@ -1935,6 +1967,15 @@
|
|||||||
"conflictsResolveFailed": "ファイル名競合の解決に失敗しました: {message}"
|
"conflictsResolveFailed": "ファイル名競合の解決に失敗しました: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"conflictConfirm": {
|
||||||
|
"title": "ファイル名の競合を解決",
|
||||||
|
"message": "重複したファイル名に4文字のハッシュを追加してリネームします。",
|
||||||
|
"note": "この操作はディスク上のファイルをリネームします。A1111 構文形式を使用している場合、既存のワークフロー内のモデル参照を更新する必要があるかもしれません。",
|
||||||
|
"detail": "例:<code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||||
|
"impact": "<strong>{groups}</strong> 組の重複にわたって <strong>{count}</strong> 個のファイルをリネームします",
|
||||||
|
"confirm": "ファイルをリネーム",
|
||||||
|
"cancel": "キャンセル"
|
||||||
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
"versionMismatch": {
|
"versionMismatch": {
|
||||||
"title": "アプリケーション更新が検出されました",
|
"title": "アプリケーション更新が検出されました",
|
||||||
|
|||||||
@@ -232,6 +232,8 @@
|
|||||||
"license": "라이선스",
|
"license": "라이선스",
|
||||||
"noCreditRequired": "크레딧 표기 없음",
|
"noCreditRequired": "크레딧 표기 없음",
|
||||||
"allowSellingGeneratedContent": "판매 허용",
|
"allowSellingGeneratedContent": "판매 허용",
|
||||||
|
"allowSellingGeneratedContentTooltip": "생성된 이미지 판매 허용",
|
||||||
|
"noCreditRequiredTooltip": "크리에이터 저작자 표시 없이 모델 사용 가능",
|
||||||
"noTags": "태그 없음",
|
"noTags": "태그 없음",
|
||||||
"autoTags": "자동 태그",
|
"autoTags": "자동 태그",
|
||||||
"noBaseModelMatches": "현재 검색과 일치하는 베이스 모델이 없습니다.",
|
"noBaseModelMatches": "현재 검색과 일치하는 베이스 모델이 없습니다.",
|
||||||
@@ -267,10 +269,10 @@
|
|||||||
},
|
},
|
||||||
"downloadBackend": {
|
"downloadBackend": {
|
||||||
"label": "다운로드 백엔드",
|
"label": "다운로드 백엔드",
|
||||||
"help": "모델 파일을 다운로드하는 방식을 선택합니다. Python은 내장 다운로더를 사용하고, aria2는 실험적인 외부 다운로더 프로세스를 사용합니다.",
|
"help": "모델 파일을 다운로드하는 방식을 선택합니다. Python은 내장 다운로더를 사용하고, aria2는 권장되는 외부 다운로더 프로세스를 사용합니다.",
|
||||||
"options": {
|
"options": {
|
||||||
"python": "Python(내장)",
|
"python": "Python(내장)",
|
||||||
"aria2": "aria2(실험적)"
|
"aria2": "aria2(권장)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aria2cPath": {
|
"aria2cPath": {
|
||||||
@@ -577,7 +579,13 @@
|
|||||||
},
|
},
|
||||||
"misc": {
|
"misc": {
|
||||||
"includeTriggerWords": "LoRA 문법에 트리거 단어 포함",
|
"includeTriggerWords": "LoRA 문법에 트리거 단어 포함",
|
||||||
"includeTriggerWordsHelp": "LoRA 문법을 클립보드에 복사할 때 학습된 트리거 단어를 포함합니다"
|
"includeTriggerWordsHelp": "LoRA 문법을 클립보드에 복사할 때 학습된 트리거 단어를 포함합니다",
|
||||||
|
"loraSyntaxFormat": "LoRA 구문 형식",
|
||||||
|
"loraSyntaxFormatHelp": "LoRA 구문 형식. 전체 경로는 하위 폴더 경로(<lora:style/anime/x:1.0>)를 포함하여 손실 없는 모델 해상도를 제공합니다. 레거시는 파일 이름만(<lora:x:1.0>) 사용 — A1111 규칙이지만, 폴더 간 파일명 중복 시 모호할 수 있습니다.",
|
||||||
|
"loraSyntaxFormatOptions": {
|
||||||
|
"full": "전체 경로(하위 폴더/이름)",
|
||||||
|
"legacy": "레거시 A1111(이름만)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"metadataArchive": {
|
"metadataArchive": {
|
||||||
"enableArchiveDb": "메타데이터 아카이브 데이터베이스 활성화",
|
"enableArchiveDb": "메타데이터 아카이브 데이터베이스 활성화",
|
||||||
@@ -1172,6 +1180,7 @@
|
|||||||
"editModelName": "모델명 편집",
|
"editModelName": "모델명 편집",
|
||||||
"editFileName": "파일명 편집",
|
"editFileName": "파일명 편집",
|
||||||
"editBaseModel": "베이스 모델 편집",
|
"editBaseModel": "베이스 모델 편집",
|
||||||
|
"editVersionName": "버전명 편집",
|
||||||
"viewOnCivitai": "Civitai에서 보기",
|
"viewOnCivitai": "Civitai에서 보기",
|
||||||
"viewOnCivitaiText": "Civitai에서 보기",
|
"viewOnCivitaiText": "Civitai에서 보기",
|
||||||
"viewCreatorProfile": "제작자 프로필 보기",
|
"viewCreatorProfile": "제작자 프로필 보기",
|
||||||
@@ -1921,9 +1930,32 @@
|
|||||||
"warning": "주의 필요",
|
"warning": "주의 필요",
|
||||||
"error": "조치 필요"
|
"error": "조치 필요"
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"civitai_api_key": {
|
||||||
|
"title": "Civitai API 키"
|
||||||
|
},
|
||||||
|
"cache_health": {
|
||||||
|
"title": "모델 캐시 상태"
|
||||||
|
},
|
||||||
|
"filename_conflicts": {
|
||||||
|
"title": "파일명 중복 충돌"
|
||||||
|
},
|
||||||
|
"ui_version": {
|
||||||
|
"title": "UI 버전"
|
||||||
|
}
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"runAgain": "다시 실행",
|
"runAgain": "다시 실행",
|
||||||
"exportBundle": "번들 내보내기"
|
"exportBundle": "번들 내보내기",
|
||||||
|
"open-settings": "설정 열기",
|
||||||
|
"open-settings-syntax-format": "전체 경로 구문으로 전환",
|
||||||
|
"repair-cache": "캐시 재구축",
|
||||||
|
"resolve-filename-conflicts": "충돌 해결",
|
||||||
|
"reload-page": "UI 새로고침"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"conflicts": "충돌",
|
||||||
|
"version": "버전"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailed": "진단 로드 실패: {message}",
|
"loadFailed": "진단 로드 실패: {message}",
|
||||||
@@ -1935,6 +1967,15 @@
|
|||||||
"conflictsResolveFailed": "파일명 충돌 해결 실패: {message}"
|
"conflictsResolveFailed": "파일명 충돌 해결 실패: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"conflictConfirm": {
|
||||||
|
"title": "파일명 충돌 해결",
|
||||||
|
"message": "중복 파일명에 4자리 해시를 추가하여 이름을 변경합니다.",
|
||||||
|
"note": "이 작업은 디스크에 있는 파일의 이름을 변경합니다. A1111 구문 형식을 사용하는 경우 기존 워크플로우의 모델 참조를 업데이트해야 할 수 있습니다.",
|
||||||
|
"detail": "예시: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||||
|
"impact": "<strong>{groups}</strong>개 중복 그룹에서 <strong>{count}</strong>개 파일 이름을 변경합니다",
|
||||||
|
"confirm": "파일 이름 변경",
|
||||||
|
"cancel": "취소"
|
||||||
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
"versionMismatch": {
|
"versionMismatch": {
|
||||||
"title": "애플리케이션 업데이트 감지",
|
"title": "애플리케이션 업데이트 감지",
|
||||||
|
|||||||
@@ -232,6 +232,8 @@
|
|||||||
"license": "Лицензия",
|
"license": "Лицензия",
|
||||||
"noCreditRequired": "Без указания авторства",
|
"noCreditRequired": "Без указания авторства",
|
||||||
"allowSellingGeneratedContent": "Продажа разрешена",
|
"allowSellingGeneratedContent": "Продажа разрешена",
|
||||||
|
"allowSellingGeneratedContentTooltip": "Разрешить продажу сгенерированных изображений",
|
||||||
|
"noCreditRequiredTooltip": "Использование модели без указания автора",
|
||||||
"noTags": "Без тегов",
|
"noTags": "Без тегов",
|
||||||
"autoTags": "Авто-теги",
|
"autoTags": "Авто-теги",
|
||||||
"noBaseModelMatches": "Нет базовых моделей, соответствующих текущему поиску.",
|
"noBaseModelMatches": "Нет базовых моделей, соответствующих текущему поиску.",
|
||||||
@@ -267,10 +269,10 @@
|
|||||||
},
|
},
|
||||||
"downloadBackend": {
|
"downloadBackend": {
|
||||||
"label": "Бэкенд загрузки",
|
"label": "Бэкенд загрузки",
|
||||||
"help": "Выберите способ загрузки файлов моделей. Python использует встроенный загрузчик. aria2 использует экспериментальный внешний процесс загрузки.",
|
"help": "Выберите способ загрузки файлов моделей. Python использует встроенный загрузчик. aria2 использует рекомендуемый внешний процесс загрузки.",
|
||||||
"options": {
|
"options": {
|
||||||
"python": "Python (встроенный)",
|
"python": "Python (встроенный)",
|
||||||
"aria2": "aria2 (экспериментальный)"
|
"aria2": "aria2 (рекомендуемый)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aria2cPath": {
|
"aria2cPath": {
|
||||||
@@ -577,7 +579,13 @@
|
|||||||
},
|
},
|
||||||
"misc": {
|
"misc": {
|
||||||
"includeTriggerWords": "Включать триггерные слова в синтаксис LoRA",
|
"includeTriggerWords": "Включать триггерные слова в синтаксис LoRA",
|
||||||
"includeTriggerWordsHelp": "Включать обученные триггерные слова при копировании синтаксиса LoRA в буфер обмена"
|
"includeTriggerWordsHelp": "Включать обученные триггерные слова при копировании синтаксиса LoRA в буфер обмена",
|
||||||
|
"loraSyntaxFormat": "Формат синтаксиса LoRA",
|
||||||
|
"loraSyntaxFormatHelp": "Формат синтаксиса LoRA. Полный путь включает подпапку (<lora:style/anime/x:1.0>) для безпотерьного разрешения модели. Устаревший использует только имя файла (<lora:x:1.0>) — соглашение A1111, может быть неоднозначным при дублировании имён файлов в разных папках.",
|
||||||
|
"loraSyntaxFormatOptions": {
|
||||||
|
"full": "Полный путь (подпапка/имя)",
|
||||||
|
"legacy": "Устаревший A1111 (только имя)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"metadataArchive": {
|
"metadataArchive": {
|
||||||
"enableArchiveDb": "Включить архив метаданных",
|
"enableArchiveDb": "Включить архив метаданных",
|
||||||
@@ -1172,6 +1180,7 @@
|
|||||||
"editModelName": "Редактировать название модели",
|
"editModelName": "Редактировать название модели",
|
||||||
"editFileName": "Редактировать имя файла",
|
"editFileName": "Редактировать имя файла",
|
||||||
"editBaseModel": "Редактировать базовую модель",
|
"editBaseModel": "Редактировать базовую модель",
|
||||||
|
"editVersionName": "Редактировать название версии",
|
||||||
"viewOnCivitai": "Посмотреть на Civitai",
|
"viewOnCivitai": "Посмотреть на Civitai",
|
||||||
"viewOnCivitaiText": "Посмотреть на Civitai",
|
"viewOnCivitaiText": "Посмотреть на Civitai",
|
||||||
"viewCreatorProfile": "Посмотреть профиль создателя",
|
"viewCreatorProfile": "Посмотреть профиль создателя",
|
||||||
@@ -1921,9 +1930,32 @@
|
|||||||
"warning": "Требует внимания",
|
"warning": "Требует внимания",
|
||||||
"error": "Требуется действие"
|
"error": "Требуется действие"
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"civitai_api_key": {
|
||||||
|
"title": "Civitai API Key"
|
||||||
|
},
|
||||||
|
"cache_health": {
|
||||||
|
"title": "Model Cache Health"
|
||||||
|
},
|
||||||
|
"filename_conflicts": {
|
||||||
|
"title": "Duplicate Filename Conflicts"
|
||||||
|
},
|
||||||
|
"ui_version": {
|
||||||
|
"title": "UI Version"
|
||||||
|
}
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"runAgain": "Запустить снова",
|
"runAgain": "Запустить снова",
|
||||||
"exportBundle": "Экспортировать пакет"
|
"exportBundle": "Экспортировать пакет",
|
||||||
|
"open-settings": "Open Settings",
|
||||||
|
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||||
|
"repair-cache": "Rebuild Cache",
|
||||||
|
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||||
|
"reload-page": "Reload UI"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"conflicts": "Conflicts",
|
||||||
|
"version": "Version"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailed": "Не удалось загрузить диагностику: {message}",
|
"loadFailed": "Не удалось загрузить диагностику: {message}",
|
||||||
@@ -1935,6 +1967,15 @@
|
|||||||
"conflictsResolveFailed": "Не удалось разрешить конфликты имён файлов: {message}"
|
"conflictsResolveFailed": "Не удалось разрешить конфликты имён файлов: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"conflictConfirm": {
|
||||||
|
"title": "Разрешить конфликты имён файлов",
|
||||||
|
"message": "Переименование с добавлением 4-символьного хеша к каждому дублирующемуся имени файла.",
|
||||||
|
"note": "Эта операция переименовывает файлы на диске. Если вы используете синтаксис A1111, ссылки на модели в существующих рабочих процессах могут потребовать обновления.",
|
||||||
|
"detail": "Пример: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||||
|
"impact": "Будет переименовано <strong>{count}</strong> файл(ов) в <strong>{groups}</strong> группе(ах) дубликатов",
|
||||||
|
"confirm": "Переименовать файлы",
|
||||||
|
"cancel": "Отмена"
|
||||||
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
"versionMismatch": {
|
"versionMismatch": {
|
||||||
"title": "Обнаружено обновление приложения",
|
"title": "Обнаружено обновление приложения",
|
||||||
|
|||||||
@@ -232,6 +232,8 @@
|
|||||||
"license": "许可证",
|
"license": "许可证",
|
||||||
"noCreditRequired": "无需署名",
|
"noCreditRequired": "无需署名",
|
||||||
"allowSellingGeneratedContent": "允许销售",
|
"allowSellingGeneratedContent": "允许销售",
|
||||||
|
"allowSellingGeneratedContentTooltip": "允许出售生成的图片",
|
||||||
|
"noCreditRequiredTooltip": "使用模型时无需注明原作者",
|
||||||
"noTags": "无标签",
|
"noTags": "无标签",
|
||||||
"autoTags": "自动标签",
|
"autoTags": "自动标签",
|
||||||
"noBaseModelMatches": "没有基础模型符合当前搜索。",
|
"noBaseModelMatches": "没有基础模型符合当前搜索。",
|
||||||
@@ -267,10 +269,10 @@
|
|||||||
},
|
},
|
||||||
"downloadBackend": {
|
"downloadBackend": {
|
||||||
"label": "下载后端",
|
"label": "下载后端",
|
||||||
"help": "选择模型文件的下载方式。Python 使用内置下载器。aria2 使用实验性的外部下载进程。",
|
"help": "选择模型文件的下载方式。Python 使用内置下载器。aria2 使用推荐的外部下载进程。",
|
||||||
"options": {
|
"options": {
|
||||||
"python": "Python(内置)",
|
"python": "Python(内置)",
|
||||||
"aria2": "aria2(实验性)"
|
"aria2": "aria2(推荐)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aria2cPath": {
|
"aria2cPath": {
|
||||||
@@ -577,7 +579,13 @@
|
|||||||
},
|
},
|
||||||
"misc": {
|
"misc": {
|
||||||
"includeTriggerWords": "复制 LoRA 语法时包含触发词",
|
"includeTriggerWords": "复制 LoRA 语法时包含触发词",
|
||||||
"includeTriggerWordsHelp": "复制 LoRA 语法到剪贴板时包含训练触发词"
|
"includeTriggerWordsHelp": "复制 LoRA 语法到剪贴板时包含训练触发词",
|
||||||
|
"loraSyntaxFormat": "LoRA 语法格式",
|
||||||
|
"loraSyntaxFormatHelp": "LoRA 语法格式。完整路径(Full)包含子文件夹路径 (<lora:style/anime/x:1.0>),解析精确无歧义。旧版(Legacy)仅使用文件名 (<lora:x:1.0>)——A1111 原始约定,同名文件跨文件夹时可能产生歧义。",
|
||||||
|
"loraSyntaxFormatOptions": {
|
||||||
|
"full": "完整路径(子文件夹/名称)",
|
||||||
|
"legacy": "旧版 A1111(仅名称)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"metadataArchive": {
|
"metadataArchive": {
|
||||||
"enableArchiveDb": "启用元数据归档数据库",
|
"enableArchiveDb": "启用元数据归档数据库",
|
||||||
@@ -1172,6 +1180,7 @@
|
|||||||
"editModelName": "编辑模型名称",
|
"editModelName": "编辑模型名称",
|
||||||
"editFileName": "编辑文件名",
|
"editFileName": "编辑文件名",
|
||||||
"editBaseModel": "编辑基础模型",
|
"editBaseModel": "编辑基础模型",
|
||||||
|
"editVersionName": "编辑版本名称",
|
||||||
"viewOnCivitai": "在 Civitai 查看",
|
"viewOnCivitai": "在 Civitai 查看",
|
||||||
"viewOnCivitaiText": "在 Civitai 查看",
|
"viewOnCivitaiText": "在 Civitai 查看",
|
||||||
"viewCreatorProfile": "查看创作者主页",
|
"viewCreatorProfile": "查看创作者主页",
|
||||||
@@ -1921,9 +1930,32 @@
|
|||||||
"warning": "需要关注",
|
"warning": "需要关注",
|
||||||
"error": "需要处理"
|
"error": "需要处理"
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"civitai_api_key": {
|
||||||
|
"title": "Civitai API 密钥"
|
||||||
|
},
|
||||||
|
"cache_health": {
|
||||||
|
"title": "模型缓存健康状态"
|
||||||
|
},
|
||||||
|
"filename_conflicts": {
|
||||||
|
"title": "文件名重复冲突"
|
||||||
|
},
|
||||||
|
"ui_version": {
|
||||||
|
"title": "UI 版本"
|
||||||
|
}
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"runAgain": "重新检查",
|
"runAgain": "重新检查",
|
||||||
"exportBundle": "导出诊断包"
|
"exportBundle": "导出诊断包",
|
||||||
|
"open-settings": "打开设置",
|
||||||
|
"open-settings-syntax-format": "切换为完整路径语法",
|
||||||
|
"repair-cache": "重建缓存",
|
||||||
|
"resolve-filename-conflicts": "解决冲突",
|
||||||
|
"reload-page": "刷新 UI"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"conflicts": "冲突详情",
|
||||||
|
"version": "版本信息"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailed": "加载诊断结果失败:{message}",
|
"loadFailed": "加载诊断结果失败:{message}",
|
||||||
@@ -1935,6 +1967,15 @@
|
|||||||
"conflictsResolveFailed": "解决文件名冲突失败:{message}"
|
"conflictsResolveFailed": "解决文件名冲突失败:{message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"conflictConfirm": {
|
||||||
|
"title": "解决文件名冲突",
|
||||||
|
"message": "通过在每个重复文件名后附加 4 位哈希值来重命名文件。",
|
||||||
|
"note": "此操作会重命名磁盘上的文件。如果使用 A1111 语法格式,现有工作流中的模型引用可能需要更新。",
|
||||||
|
"detail": "示例:<code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||||
|
"impact": "将重命名 <strong>{count}</strong> 个文件(共 <strong>{groups}</strong> 组重复)",
|
||||||
|
"confirm": "重命名文件",
|
||||||
|
"cancel": "取消"
|
||||||
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
"versionMismatch": {
|
"versionMismatch": {
|
||||||
"title": "检测到应用更新",
|
"title": "检测到应用更新",
|
||||||
|
|||||||
@@ -232,6 +232,8 @@
|
|||||||
"license": "授權",
|
"license": "授權",
|
||||||
"noCreditRequired": "無需署名",
|
"noCreditRequired": "無需署名",
|
||||||
"allowSellingGeneratedContent": "允許銷售",
|
"allowSellingGeneratedContent": "允許銷售",
|
||||||
|
"allowSellingGeneratedContentTooltip": "允許出售生成的圖片",
|
||||||
|
"noCreditRequiredTooltip": "使用模型時無需註明原作者",
|
||||||
"noTags": "無標籤",
|
"noTags": "無標籤",
|
||||||
"autoTags": "自動標籤",
|
"autoTags": "自動標籤",
|
||||||
"noBaseModelMatches": "沒有基礎模型符合目前的搜尋。",
|
"noBaseModelMatches": "沒有基礎模型符合目前的搜尋。",
|
||||||
@@ -267,10 +269,10 @@
|
|||||||
},
|
},
|
||||||
"downloadBackend": {
|
"downloadBackend": {
|
||||||
"label": "下載後端",
|
"label": "下載後端",
|
||||||
"help": "選擇模型檔案的下載方式。Python 使用內建下載器。aria2 使用實驗性的外部下載程序。",
|
"help": "選擇模型檔案的下載方式。Python 使用內建下載器。aria2 使用推薦的外部下載程序。",
|
||||||
"options": {
|
"options": {
|
||||||
"python": "Python(內建)",
|
"python": "Python(內建)",
|
||||||
"aria2": "aria2(實驗性)"
|
"aria2": "aria2(推薦)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aria2cPath": {
|
"aria2cPath": {
|
||||||
@@ -577,7 +579,13 @@
|
|||||||
},
|
},
|
||||||
"misc": {
|
"misc": {
|
||||||
"includeTriggerWords": "在 LoRA 語法中包含觸發詞",
|
"includeTriggerWords": "在 LoRA 語法中包含觸發詞",
|
||||||
"includeTriggerWordsHelp": "複製 LoRA 語法到剪貼簿時包含訓練觸發詞"
|
"includeTriggerWordsHelp": "複製 LoRA 語法到剪貼簿時包含訓練觸發詞",
|
||||||
|
"loraSyntaxFormat": "LoRA 語法格式",
|
||||||
|
"loraSyntaxFormatHelp": "LoRA 語法格式。完整路徑(Full)包含子資料夾路徑 (<lora:style/anime/x:1.0>),解析精確無歧義。舊版(Legacy)僅使用檔名 (<lora:x:1.0>)——A1111 原始約定,同名檔案跨資料夾時可能產生歧義。",
|
||||||
|
"loraSyntaxFormatOptions": {
|
||||||
|
"full": "完整路徑(子資料夾/名稱)",
|
||||||
|
"legacy": "舊版 A1111(僅名稱)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"metadataArchive": {
|
"metadataArchive": {
|
||||||
"enableArchiveDb": "啟用中繼資料封存資料庫",
|
"enableArchiveDb": "啟用中繼資料封存資料庫",
|
||||||
@@ -1172,6 +1180,7 @@
|
|||||||
"editModelName": "編輯模型名稱",
|
"editModelName": "編輯模型名稱",
|
||||||
"editFileName": "編輯檔案名稱",
|
"editFileName": "編輯檔案名稱",
|
||||||
"editBaseModel": "編輯基礎模型",
|
"editBaseModel": "編輯基礎模型",
|
||||||
|
"editVersionName": "編輯版本名稱",
|
||||||
"viewOnCivitai": "在 Civitai 查看",
|
"viewOnCivitai": "在 Civitai 查看",
|
||||||
"viewOnCivitaiText": "在 Civitai 查看",
|
"viewOnCivitaiText": "在 Civitai 查看",
|
||||||
"viewCreatorProfile": "查看創作者個人檔案",
|
"viewCreatorProfile": "查看創作者個人檔案",
|
||||||
@@ -1921,9 +1930,32 @@
|
|||||||
"warning": "需要注意",
|
"warning": "需要注意",
|
||||||
"error": "需要處理"
|
"error": "需要處理"
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"civitai_api_key": {
|
||||||
|
"title": "Civitai API 金鑰"
|
||||||
|
},
|
||||||
|
"cache_health": {
|
||||||
|
"title": "模型快取健康狀態"
|
||||||
|
},
|
||||||
|
"filename_conflicts": {
|
||||||
|
"title": "檔案名稱重複衝突"
|
||||||
|
},
|
||||||
|
"ui_version": {
|
||||||
|
"title": "UI 版本"
|
||||||
|
}
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"runAgain": "重新執行",
|
"runAgain": "重新執行",
|
||||||
"exportBundle": "匯出套件"
|
"exportBundle": "匯出套件",
|
||||||
|
"open-settings": "開啟設定",
|
||||||
|
"open-settings-syntax-format": "切換為完整路徑語法",
|
||||||
|
"repair-cache": "重建快取",
|
||||||
|
"resolve-filename-conflicts": "解決衝突",
|
||||||
|
"reload-page": "重新載入 UI"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"conflicts": "衝突詳情",
|
||||||
|
"version": "版本"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"loadFailed": "載入診斷失敗:{message}",
|
"loadFailed": "載入診斷失敗:{message}",
|
||||||
@@ -1935,6 +1967,15 @@
|
|||||||
"conflictsResolveFailed": "解決檔案名稱衝突失敗:{message}"
|
"conflictsResolveFailed": "解決檔案名稱衝突失敗:{message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"conflictConfirm": {
|
||||||
|
"title": "解決檔案名稱衝突",
|
||||||
|
"message": "通過在每個重複檔案名稱後附加 4 位元哈希值來重新命名檔案。",
|
||||||
|
"note": "此操作會重新命名磁碟上的檔案。如果使用 A1111 語法格式,現有工作流程中的模型參考可能需要更新。",
|
||||||
|
"detail": "示例:<code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||||
|
"impact": "將重新命名 <strong>{count}</strong> 個檔案(共 <strong>{groups}</strong> 組重複)",
|
||||||
|
"confirm": "重新命名檔案",
|
||||||
|
"cancel": "取消"
|
||||||
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
"versionMismatch": {
|
"versionMismatch": {
|
||||||
"title": "偵測到應用程式更新",
|
"title": "偵測到應用程式更新",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from ..utils.utils import get_lora_info_absolute
|
|||||||
from .utils import (
|
from .utils import (
|
||||||
FlexibleOptionalInputType,
|
FlexibleOptionalInputType,
|
||||||
any_type,
|
any_type,
|
||||||
|
apply_lora_syntax_format,
|
||||||
detect_nunchaku_model_kind,
|
detect_nunchaku_model_kind,
|
||||||
extract_lora_name,
|
extract_lora_name,
|
||||||
get_loras_list,
|
get_loras_list,
|
||||||
@@ -52,7 +53,7 @@ def _collect_widget_entries(kwargs):
|
|||||||
for lora in get_loras_list(kwargs):
|
for lora in get_loras_list(kwargs):
|
||||||
if not lora.get("active", False):
|
if not lora.get("active", False):
|
||||||
continue
|
continue
|
||||||
lora_name = lora["name"]
|
lora_name = apply_lora_syntax_format(lora["name"])
|
||||||
model_strength = float(lora["strength"])
|
model_strength = float(lora["strength"])
|
||||||
clip_strength = float(lora.get("clipStrength", model_strength))
|
clip_strength = float(lora.get("clipStrength", model_strength))
|
||||||
lora_path, trigger_words = get_lora_info_absolute(lora_name)
|
lora_path, trigger_words = get_lora_info_absolute(lora_name)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from ..utils.utils import get_lora_info
|
from ..utils.utils import get_lora_info
|
||||||
from .utils import FlexibleOptionalInputType, any_type, extract_lora_name, get_loras_list
|
from .utils import FlexibleOptionalInputType, any_type, apply_lora_syntax_format, extract_lora_name, get_loras_list
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ class LoraStackerLM:
|
|||||||
if not lora.get('active', False):
|
if not lora.get('active', False):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
lora_name = lora['name']
|
lora_name = apply_lora_syntax_format(lora['name'])
|
||||||
model_strength = float(lora['strength'])
|
model_strength = float(lora['strength'])
|
||||||
# Get clip strength - use model strength as default if not specified
|
# Get clip strength - use model strength as default if not specified
|
||||||
clip_strength = float(lora.get('clipStrength', model_strength))
|
clip_strength = float(lora.get('clipStrength', model_strength))
|
||||||
|
|||||||
@@ -44,11 +44,29 @@ import folder_paths # type: ignore
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_lora_syntax_format():
|
||||||
|
try:
|
||||||
|
from ..services.settings_manager import get_settings_manager
|
||||||
|
return get_settings_manager().get("lora_syntax_format", "legacy")
|
||||||
|
except Exception:
|
||||||
|
return "legacy"
|
||||||
|
|
||||||
|
|
||||||
|
def apply_lora_syntax_format(name):
|
||||||
|
fmt = get_lora_syntax_format()
|
||||||
|
if fmt == "legacy":
|
||||||
|
return name.replace("\\", "/").rstrip("/").split("/")[-1]
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
def extract_lora_name(lora_path):
|
def extract_lora_name(lora_path):
|
||||||
"""Extract the lora name from a lora path (e.g., 'IL\\aorunIllstrious.safetensors' -> 'aorunIllstrious')"""
|
normalized = lora_path.replace("\\", "/")
|
||||||
# Get the basename without extension
|
basename = os.path.basename(normalized)
|
||||||
basename = os.path.basename(lora_path)
|
name_no_ext = os.path.splitext(basename)[0]
|
||||||
return os.path.splitext(basename)[0]
|
dirname = os.path.dirname(normalized)
|
||||||
|
if dirname and dirname not in (".", "/") and not normalized.startswith("/"):
|
||||||
|
return apply_lora_syntax_format(f"{dirname}/{name_no_ext}")
|
||||||
|
return apply_lora_syntax_format(name_no_ext)
|
||||||
|
|
||||||
|
|
||||||
def get_loras_list(kwargs):
|
def get_loras_list(kwargs):
|
||||||
|
|||||||
@@ -686,6 +686,9 @@ class DoctorHandler:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def resolve_filename_conflicts(self, request: web.Request) -> web.Response:
|
async def resolve_filename_conflicts(self, request: web.Request) -> web.Response:
|
||||||
|
if self._settings.get("lora_syntax_format", "legacy") == "full":
|
||||||
|
return web.json_response({"success": True, "renamed": [], "count": 0})
|
||||||
|
|
||||||
renamed: list[dict[str, Any]] = []
|
renamed: list[dict[str, Any]] = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -990,11 +993,29 @@ class DoctorHandler:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def _check_filename_conflicts(self) -> dict[str, Any]:
|
async def _check_filename_conflicts(self) -> dict[str, Any]:
|
||||||
|
# When full path syntax is active, duplicate filenames across subfolders
|
||||||
|
# are not ambiguous (<lora:subfolder/name:strength>), so skip the check.
|
||||||
|
if self._settings.get("lora_syntax_format", "legacy") == "full":
|
||||||
|
return {
|
||||||
|
"id": "filename_conflicts",
|
||||||
|
"title": "Duplicate Filename Conflicts",
|
||||||
|
"status": "ok",
|
||||||
|
"summary": "Full path syntax is active — duplicate filenames across folders are not ambiguous.",
|
||||||
|
"details": [],
|
||||||
|
"actions": [],
|
||||||
|
}
|
||||||
|
|
||||||
all_conflicts: list[dict[str, Any]] = []
|
all_conflicts: list[dict[str, Any]] = []
|
||||||
total_conflict_groups = 0
|
total_conflict_groups = 0
|
||||||
total_conflict_files = 0
|
total_conflict_files = 0
|
||||||
|
|
||||||
for model_type, label, factory in self._scanner_factories:
|
for model_type, label, factory in self._scanner_factories:
|
||||||
|
# Duplicate filename detection targets LoRAs which use basename-only
|
||||||
|
# syntax (<lora:name:strength>). Checkpoints/embeddings reference
|
||||||
|
# models via relative paths with extensions, so conflicts there would
|
||||||
|
# be false positives.
|
||||||
|
if model_type != "lora":
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
scanner = await factory()
|
scanner = await factory()
|
||||||
hash_index = getattr(scanner, "_hash_index", None)
|
hash_index = getattr(scanner, "_hash_index", None)
|
||||||
@@ -1042,12 +1063,22 @@ class DoctorHandler:
|
|||||||
"total_conflict_files": total_conflict_files,
|
"total_conflict_files": total_conflict_files,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
for conflict in all_conflicts:
|
|
||||||
|
# Show at most 5 conflict groups inline; note any remainder.
|
||||||
|
MAX_VISIBLE_CONFLICTS = 5
|
||||||
|
visible_conflicts = all_conflicts[:MAX_VISIBLE_CONFLICTS]
|
||||||
|
for conflict in visible_conflicts:
|
||||||
details.append(
|
details.append(
|
||||||
f"[{conflict['label']}] '{conflict['filename']}' "
|
f"'{conflict['filename']}' "
|
||||||
f"found in {len(conflict['paths'])} locations"
|
f"found in {len(conflict['paths'])} locations"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
hidden_count = len(all_conflicts) - MAX_VISIBLE_CONFLICTS
|
||||||
|
if hidden_count > 0:
|
||||||
|
details.append(
|
||||||
|
f"...and {hidden_count} more duplicate filename group(s)"
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": "filename_conflicts",
|
"id": "filename_conflicts",
|
||||||
"title": "Duplicate Filename Conflicts",
|
"title": "Duplicate Filename Conflicts",
|
||||||
@@ -1058,7 +1089,11 @@ class DoctorHandler:
|
|||||||
{
|
{
|
||||||
"id": "resolve-filename-conflicts",
|
"id": "resolve-filename-conflicts",
|
||||||
"label": "Resolve Conflicts",
|
"label": "Resolve Conflicts",
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"id": "open-settings-syntax-format",
|
||||||
|
"label": "Switch to Full Path Syntax",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -788,7 +788,7 @@ class ModelManagementHandler:
|
|||||||
|
|
||||||
metadata_updates = {k: v for k, v in data.items() if k != "file_path"}
|
metadata_updates = {k: v for k, v in data.items() if k != "file_path"}
|
||||||
|
|
||||||
await self._metadata_sync.save_metadata_updates(
|
updated_metadata = await self._metadata_sync.save_metadata_updates(
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
updates=metadata_updates,
|
updates=metadata_updates,
|
||||||
metadata_loader=self._metadata_sync.load_local_metadata,
|
metadata_loader=self._metadata_sync.load_local_metadata,
|
||||||
@@ -799,7 +799,12 @@ class ModelManagementHandler:
|
|||||||
cache = await self._service.scanner.get_cached_data()
|
cache = await self._service.scanner.get_cached_data()
|
||||||
await cache.resort()
|
await cache.resort()
|
||||||
|
|
||||||
return web.json_response({"success": True})
|
from ...services.auto_tag_service import extract_auto_tags
|
||||||
|
auto_tags = extract_auto_tags(updated_metadata)
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
{"success": True, "auto_tags": auto_tags}
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._logger.error("Error saving metadata: %s", exc, exc_info=True)
|
self._logger.error("Error saving metadata: %s", exc, exc_info=True)
|
||||||
return web.Response(text=str(exc), status=500)
|
return web.Response(text=str(exc), status=500)
|
||||||
@@ -816,14 +821,16 @@ class ModelManagementHandler:
|
|||||||
if not isinstance(new_tags, list):
|
if not isinstance(new_tags, list):
|
||||||
return web.Response(text="Tags must be a list", status=400)
|
return web.Response(text="Tags must be a list", status=400)
|
||||||
|
|
||||||
tags = await self._tag_update_service.add_tags(
|
tags, auto_tags = await self._tag_update_service.add_tags(
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
new_tags=new_tags,
|
new_tags=new_tags,
|
||||||
metadata_loader=self._metadata_sync.load_local_metadata,
|
metadata_loader=self._metadata_sync.load_local_metadata,
|
||||||
update_cache=self._service.scanner.update_single_model_cache,
|
update_cache=self._service.scanner.update_single_model_cache,
|
||||||
)
|
)
|
||||||
|
|
||||||
return web.json_response({"success": True, "tags": tags})
|
return web.json_response(
|
||||||
|
{"success": True, "tags": tags, "auto_tags": auto_tags}
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._logger.error("Error adding tags: %s", exc, exc_info=True)
|
self._logger.error("Error adding tags: %s", exc, exc_info=True)
|
||||||
return web.Response(text=str(exc), status=500)
|
return web.Response(text=str(exc), status=500)
|
||||||
@@ -1170,6 +1177,12 @@ class ModelQueryHandler:
|
|||||||
|
|
||||||
async def find_filename_conflicts(self, request: web.Request) -> web.Response:
|
async def find_filename_conflicts(self, request: web.Request) -> web.Response:
|
||||||
try:
|
try:
|
||||||
|
settings = get_settings_manager()
|
||||||
|
if settings.get("lora_syntax_format", "legacy") == "full":
|
||||||
|
return web.json_response(
|
||||||
|
{"success": True, "conflicts": [], "count": 0}
|
||||||
|
)
|
||||||
|
|
||||||
duplicates = self._service.find_duplicate_filenames()
|
duplicates = self._service.find_duplicate_filenames()
|
||||||
result = []
|
result = []
|
||||||
cache = await self._service.scanner.get_cached_data()
|
cache = await self._service.scanner.get_cached_data()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import mimetypes
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -12,6 +13,12 @@ from ...config import config as global_config
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_CHUNK_SIZE = 256 * 1024 # 256 KB
|
||||||
|
|
||||||
|
# Video file extensions that bypass native sendfile on Windows
|
||||||
|
# to avoid IOCP/ProactorEventLoop crashes during client disconnect.
|
||||||
|
_VIDEO_EXTENSIONS = frozenset({".mp4", ".webm", ".mov", ".avi", ".mkv"})
|
||||||
|
|
||||||
|
|
||||||
class PreviewHandler:
|
class PreviewHandler:
|
||||||
"""Serve preview assets for the active library at request time."""
|
"""Serve preview assets for the active library at request time."""
|
||||||
@@ -48,8 +55,51 @@ class PreviewHandler:
|
|||||||
logger.debug("Preview file not found at %s", str(resolved))
|
logger.debug("Preview file not found at %s", str(resolved))
|
||||||
raise web.HTTPNotFound(text="Preview file not found")
|
raise web.HTTPNotFound(text="Preview file not found")
|
||||||
|
|
||||||
|
# Video files: stream manually to avoid Windows native sendfile crash.
|
||||||
|
# aiohttp's FileResponse uses _sendfile_native on Windows (IOCP-based),
|
||||||
|
# which breaks when the client disconnects mid-transfer — this happens
|
||||||
|
# constantly when users scroll through a gallery of animated previews.
|
||||||
|
suffix = resolved.suffix.lower()
|
||||||
|
if suffix in _VIDEO_EXTENSIONS:
|
||||||
|
return await self._stream_file(request, resolved)
|
||||||
|
|
||||||
# aiohttp's FileResponse handles range requests and content headers for us.
|
# aiohttp's FileResponse handles range requests and content headers for us.
|
||||||
return web.FileResponse(path=resolved, chunk_size=256 * 1024)
|
return web.FileResponse(path=resolved, chunk_size=_CHUNK_SIZE)
|
||||||
|
|
||||||
|
async def _stream_file(
|
||||||
|
self, request: web.Request, path: Path
|
||||||
|
) -> web.StreamResponse:
|
||||||
|
"""Stream a file chunk-by-chunk, bypassing native sendfile.
|
||||||
|
|
||||||
|
This avoids the Windows IOCP ``_sendfile_native`` crash that occurs
|
||||||
|
when the client disconnects during a large file transfer.
|
||||||
|
"""
|
||||||
|
content_type, _ = mimetypes.guess_type(str(path))
|
||||||
|
if content_type is None:
|
||||||
|
content_type = "application/octet-stream"
|
||||||
|
|
||||||
|
file_size = path.stat().st_size
|
||||||
|
resp = web.StreamResponse()
|
||||||
|
resp.content_type = content_type
|
||||||
|
resp.content_length = file_size
|
||||||
|
|
||||||
|
await resp.prepare(request)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
while True:
|
||||||
|
chunk = f.read(_CHUNK_SIZE)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
await resp.write(chunk)
|
||||||
|
except (ConnectionResetError, ConnectionAbortedError):
|
||||||
|
# Client disconnected during streaming — expected when scrolling
|
||||||
|
# rapidly through a library with animated previews.
|
||||||
|
pass
|
||||||
|
except OSError as exc:
|
||||||
|
logger.debug("I/O error streaming preview %s: %s", path, exc)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["PreviewHandler"]
|
__all__ = ["PreviewHandler"]
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class Aria2Transfer:
|
|||||||
|
|
||||||
|
|
||||||
class Aria2Downloader:
|
class Aria2Downloader:
|
||||||
"""Manage an aria2 RPC daemon for experimental model downloads."""
|
"""Manage an aria2 RPC daemon for recommended model downloads."""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
_lock = asyncio.Lock()
|
_lock = asyncio.Lock()
|
||||||
|
|||||||
@@ -76,46 +76,64 @@ def _collect_sources(model_data: Dict) -> List[str]:
|
|||||||
def extract_auto_tags(model_data: Dict) -> List[str]:
|
def extract_auto_tags(model_data: Dict) -> List[str]:
|
||||||
"""Extract auto-detected tags from model metadata.
|
"""Extract auto-detected tags from model metadata.
|
||||||
|
|
||||||
Matches predefined patterns against filename, base_model, and
|
Uses a two-layer approach:
|
||||||
CivitAI version name. Returns a sorted, deduplicated list of tag labels.
|
Layer 1 — Regex-based detection against filename, base_model, and
|
||||||
|
CivitAI version name.
|
||||||
|
Layer 2 — Merge in any user-defined tags that overlap with known
|
||||||
|
auto-tag categories. This provides a manual fallback when
|
||||||
|
auto-detection fails (e.g. "I2V HN" or unlabeled models).
|
||||||
|
|
||||||
HIGH/LOW tags are only returned when the base_model indicates a Wan
|
HIGH/LOW tags are only returned when the base_model indicates a Wan
|
||||||
family model — no other model architecture uses this distinction.
|
family model — no other model architecture uses this distinction.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
model_data: Model metadata dict with keys:
|
model_data: Model metadata dict with keys:
|
||||||
file_name, base_model, civitai (with optional 'name' field).
|
file_name, base_model, civitai (with optional 'name' field),
|
||||||
|
tags (user-defined tag list, used as fallback).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Sorted list of unique auto-tag strings (e.g. ["I2V"]).
|
Sorted list of unique auto-tag strings (e.g. ["I2V"]).
|
||||||
"""
|
"""
|
||||||
sources = _collect_sources(model_data)
|
sources = _collect_sources(model_data)
|
||||||
if not sources:
|
|
||||||
return []
|
|
||||||
|
|
||||||
base_model = model_data.get("base_model", "")
|
base_model = model_data.get("base_model", "")
|
||||||
is_wan = "wan" in base_model.lower()
|
is_wan = "wan" in base_model.lower()
|
||||||
|
|
||||||
found: Set[str] = set()
|
found: Set[str] = set()
|
||||||
|
|
||||||
for label, pattern in AUTO_TAG_CATEGORIES.items():
|
# ── Layer 1: regex-based detection ────────────────────────────
|
||||||
# HIGH/LOW are Wan-specific — skip for non-Wan to avoid noise
|
if sources:
|
||||||
if label in ("HIGH", "LOW"):
|
for label, pattern in AUTO_TAG_CATEGORIES.items():
|
||||||
if not is_wan:
|
# HIGH/LOW are Wan-specific — skip for non-Wan to avoid noise
|
||||||
continue
|
if label in ("HIGH", "LOW"):
|
||||||
# Use case-insensitive character class + case-sensitive boundary,
|
if not is_wan:
|
||||||
# so "HighNoise" (camelCase) matches but "highlight" doesn't.
|
continue
|
||||||
# Boundary: not followed by lowercase letter (= word has ended).
|
# Use case-insensitive character class + case-sensitive boundary,
|
||||||
ci = "".join(f"[{c.lower()}{c.upper()}]" for c in label)
|
# so "HighNoise" (camelCase) matches but "highlight" doesn't.
|
||||||
if label == "LOW":
|
# Boundary: not followed by lowercase letter (= word has ended).
|
||||||
regex = re.compile(r"(?<![Ff])" + ci + r"(?![a-z])")
|
ci = "".join(f"[{c.lower()}{c.upper()}]" for c in label)
|
||||||
|
if label == "LOW":
|
||||||
|
regex = re.compile(r"(?<![Ff])" + ci + r"(?![a-z])")
|
||||||
|
else:
|
||||||
|
regex = re.compile(ci + r"(?![a-z])")
|
||||||
else:
|
else:
|
||||||
regex = re.compile(ci + r"(?![a-z])")
|
regex = re.compile(pattern, re.IGNORECASE)
|
||||||
else:
|
for source in sources:
|
||||||
regex = re.compile(pattern, re.IGNORECASE)
|
if regex.search(source):
|
||||||
for source in sources:
|
found.add(label)
|
||||||
if regex.search(source):
|
break
|
||||||
found.add(label)
|
|
||||||
break
|
# ── Layer 2: user-defined tags as manual fallback ─────────────
|
||||||
|
# When auto-detection fails (abbreviated names like "Hi"/"Lo",
|
||||||
|
# "I2V HN", or unlabeled models), users can add canonical tags
|
||||||
|
# (HIGH, LOW, I2V, etc.) to the model's regular tags for correct
|
||||||
|
# badge display and filtering. Matching is case-insensitive so
|
||||||
|
# "high"/"High"/"HIGH" all resolve to the canonical label.
|
||||||
|
user_tags = model_data.get("tags")
|
||||||
|
if user_tags:
|
||||||
|
label_map = {label.lower(): label for label in AUTO_TAG_CATEGORIES}
|
||||||
|
for t in user_tags:
|
||||||
|
canonical = label_map.get(t.lower())
|
||||||
|
if canonical:
|
||||||
|
found.add(canonical)
|
||||||
|
|
||||||
return sorted(found)
|
return sorted(found)
|
||||||
|
|||||||
@@ -870,22 +870,75 @@ class BaseModelService(ABC):
|
|||||||
"""Get the static preview URL for a model file"""
|
"""Get the static preview URL for a model file"""
|
||||||
cache = await self.scanner.get_cached_data()
|
cache = await self.scanner.get_cached_data()
|
||||||
|
|
||||||
|
name_normalized = model_name.replace("\\", "/")
|
||||||
|
name_no_ext = name_normalized
|
||||||
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
|
if name_no_ext.lower().endswith(ext):
|
||||||
|
name_no_ext = name_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
|
||||||
|
has_path = "/" in name_no_ext
|
||||||
|
basename = os.path.basename(name_no_ext) if has_path else name_no_ext
|
||||||
|
best_fallback = None
|
||||||
|
|
||||||
for model in cache.raw_data:
|
for model in cache.raw_data:
|
||||||
if model["file_name"] == model_name:
|
file_name = model.get("file_name", "")
|
||||||
|
folder = model.get("folder", "")
|
||||||
|
file_name_no_ext = file_name
|
||||||
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
|
if file_name_no_ext.lower().endswith(ext):
|
||||||
|
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||||
|
|
||||||
|
if name_no_ext == file_name_no_ext or name_no_ext == path_name:
|
||||||
preview_url = model.get("preview_url")
|
preview_url = model.get("preview_url")
|
||||||
if preview_url:
|
if preview_url:
|
||||||
from ..config import config
|
from ..config import config
|
||||||
|
|
||||||
return config.get_preview_static_url(preview_url)
|
return config.get_preview_static_url(preview_url)
|
||||||
|
|
||||||
|
if has_path and file_name_no_ext == basename:
|
||||||
|
if folder and name_no_ext.startswith(folder.replace("\\", "/") + "/"):
|
||||||
|
best_fallback = model
|
||||||
|
elif best_fallback is None:
|
||||||
|
best_fallback = model
|
||||||
|
|
||||||
|
if best_fallback:
|
||||||
|
preview_url = best_fallback.get("preview_url")
|
||||||
|
if preview_url:
|
||||||
|
from ..config import config
|
||||||
|
|
||||||
|
return config.get_preview_static_url(preview_url)
|
||||||
|
|
||||||
return "/loras_static/images/no-preview.png"
|
return "/loras_static/images/no-preview.png"
|
||||||
|
|
||||||
async def get_model_civitai_url(self, model_name: str) -> Dict[str, Optional[str]]:
|
async def get_model_civitai_url(self, model_name: str) -> Dict[str, Optional[str]]:
|
||||||
"""Get the Civitai URL for a model file"""
|
"""Get the Civitai URL for a model file"""
|
||||||
cache = await self.scanner.get_cached_data()
|
cache = await self.scanner.get_cached_data()
|
||||||
|
|
||||||
|
name_normalized = model_name.replace("\\", "/")
|
||||||
|
name_no_ext = name_normalized
|
||||||
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
|
if name_no_ext.lower().endswith(ext):
|
||||||
|
name_no_ext = name_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
|
||||||
|
has_path = "/" in name_no_ext
|
||||||
|
basename = os.path.basename(name_no_ext) if has_path else name_no_ext
|
||||||
|
best_fallback = None
|
||||||
|
|
||||||
for model in cache.raw_data:
|
for model in cache.raw_data:
|
||||||
if model["file_name"] == model_name:
|
file_name = model.get("file_name", "")
|
||||||
|
folder = model.get("folder", "")
|
||||||
|
file_name_no_ext = file_name
|
||||||
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
|
if file_name_no_ext.lower().endswith(ext):
|
||||||
|
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||||
|
|
||||||
|
if name_no_ext == file_name_no_ext or name_no_ext == path_name:
|
||||||
civitai_data = model.get("civitai", {})
|
civitai_data = model.get("civitai", {})
|
||||||
model_id = civitai_data.get("modelId")
|
model_id = civitai_data.get("modelId")
|
||||||
version_id = civitai_data.get("id")
|
version_id = civitai_data.get("id")
|
||||||
@@ -904,6 +957,27 @@ class BaseModelService(ABC):
|
|||||||
"version_id": str(version_id) if version_id else None,
|
"version_id": str(version_id) if version_id else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if has_path and file_name_no_ext == basename:
|
||||||
|
if folder and name_no_ext.startswith(folder.replace("\\", "/") + "/"):
|
||||||
|
best_fallback = model
|
||||||
|
elif best_fallback is None:
|
||||||
|
best_fallback = model
|
||||||
|
|
||||||
|
if best_fallback:
|
||||||
|
civitai_data = best_fallback.get("civitai", {})
|
||||||
|
model_id = civitai_data.get("modelId")
|
||||||
|
if model_id:
|
||||||
|
version_id = civitai_data.get("id")
|
||||||
|
civitai_host = self.settings.get("civitai_host", "civitai.com")
|
||||||
|
civitai_url = build_civitai_model_page_url(
|
||||||
|
model_id, version_id, host=civitai_host
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"civitai_url": civitai_url,
|
||||||
|
"model_id": str(model_id),
|
||||||
|
"version_id": str(version_id) if version_id else None,
|
||||||
|
}
|
||||||
|
|
||||||
return {"civitai_url": None, "model_id": None, "version_id": None}
|
return {"civitai_url": None, "model_id": None, "version_id": None}
|
||||||
|
|
||||||
async def get_model_metadata(self, file_path: str) -> Optional[Dict]:
|
async def get_model_metadata(self, file_path: str) -> Optional[Dict]:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from collections import OrderedDict
|
||||||
from typing import Any, Optional, Dict, Tuple, List, Sequence
|
from typing import Any, Optional, Dict, Tuple, List, Sequence
|
||||||
from .connectivity_guard import (
|
from .connectivity_guard import (
|
||||||
OFFLINE_FRIENDLY_MESSAGE,
|
OFFLINE_FRIENDLY_MESSAGE,
|
||||||
@@ -45,6 +46,14 @@ class CivitaiClient:
|
|||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
self.base_url = "https://civitai.red/api/v1"
|
self.base_url = "https://civitai.red/api/v1"
|
||||||
|
# In-memory cache to avoid redundant get_model_version_info calls
|
||||||
|
# within the same import/scan flow. Only successful results are cached.
|
||||||
|
# Uses OrderedDict with LRU eviction at MAX_CACHE_ENTRIES to prevent
|
||||||
|
# unbounded growth in long-running server processes.
|
||||||
|
self._version_info_cache: OrderedDict[
|
||||||
|
str, Tuple[Optional[Dict], Optional[str]]
|
||||||
|
] = OrderedDict()
|
||||||
|
self._MAX_CACHE_ENTRIES = 500
|
||||||
|
|
||||||
def _build_image_info_url(self, image_id: str) -> str:
|
def _build_image_info_url(self, image_id: str) -> str:
|
||||||
return f"{self.base_url}/images?imageId={image_id}&nsfw=X"
|
return f"{self.base_url}/images?imageId={image_id}&nsfw=X"
|
||||||
@@ -57,22 +66,57 @@ class CivitaiClient:
|
|||||||
use_auth: bool = False,
|
use_auth: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Tuple[bool, Dict | str]:
|
) -> Tuple[bool, Dict | str]:
|
||||||
"""Wrapper around downloader.make_request that surfaces rate limits."""
|
"""Wrapper around downloader.make_request that surfaces rate limits,
|
||||||
|
with retry for transient server errors (5xx, Cloudflare 524, network flakiness)."""
|
||||||
|
|
||||||
downloader = await get_downloader()
|
max_retries = 3
|
||||||
success, result = await downloader.make_request(
|
for attempt in range(max_retries):
|
||||||
method,
|
downloader = await get_downloader()
|
||||||
url,
|
success, result = await downloader.make_request(
|
||||||
use_auth=use_auth,
|
method,
|
||||||
**kwargs,
|
url,
|
||||||
)
|
use_auth=use_auth,
|
||||||
if not success and isinstance(result, RateLimitError):
|
**kwargs,
|
||||||
if result.provider is None:
|
)
|
||||||
result.provider = "civitai_api"
|
if success:
|
||||||
raise result
|
return True, result
|
||||||
if not success and is_offline_cooldown_error(result):
|
|
||||||
return False, OFFLINE_FRIENDLY_MESSAGE
|
if isinstance(result, RateLimitError):
|
||||||
return success, result
|
if result.provider is None:
|
||||||
|
result.provider = "civitai_api"
|
||||||
|
raise result
|
||||||
|
|
||||||
|
if is_offline_cooldown_error(result):
|
||||||
|
return False, OFFLINE_FRIENDLY_MESSAGE
|
||||||
|
|
||||||
|
# Transient server error — retry with exponential backoff
|
||||||
|
if self._is_transient_server_error(str(result)):
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
wait = 2**attempt # 1s, 2s, 4s
|
||||||
|
logger.info(
|
||||||
|
"Transient error on %s %s, retrying in %ds "
|
||||||
|
"(attempt %d/%d): %s",
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
wait,
|
||||||
|
attempt + 1,
|
||||||
|
max_retries,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
continue
|
||||||
|
logger.warning(
|
||||||
|
"All %d retries exhausted for %s %s: %s",
|
||||||
|
max_retries,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
return False, result
|
||||||
|
|
||||||
|
return False, result
|
||||||
|
|
||||||
|
return False, "Unexpected error in _make_request"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _remove_comfy_metadata(model_version: Optional[Dict]) -> None:
|
def _remove_comfy_metadata(model_version: Optional[Dict]) -> None:
|
||||||
@@ -201,6 +245,29 @@ class CivitaiClient:
|
|||||||
|
|
||||||
return _from_value(payload)
|
return _from_value(payload)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_transient_server_error(message: str) -> bool:
|
||||||
|
"""Return True when the message indicates a transient upstream failure.
|
||||||
|
|
||||||
|
Recognises Cloudflare 524, generic 5xx, and connectivity-level flakiness
|
||||||
|
that should not be treated as a permanent failure.
|
||||||
|
"""
|
||||||
|
normalized = message.lower()
|
||||||
|
if "status 5" in normalized or "status 524" in normalized:
|
||||||
|
return True
|
||||||
|
if any(
|
||||||
|
keyword in normalized
|
||||||
|
for keyword in (
|
||||||
|
"connection refused",
|
||||||
|
"connection reset",
|
||||||
|
"temporary failure",
|
||||||
|
"name resolution",
|
||||||
|
"connection closed",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
async def get_model_versions(self, model_id: str) -> Optional[Dict]:
|
async def get_model_versions(self, model_id: str) -> Optional[Dict]:
|
||||||
"""Get all versions of a model with local availability info"""
|
"""Get all versions of a model with local availability info"""
|
||||||
try:
|
try:
|
||||||
@@ -223,6 +290,13 @@ class CivitaiClient:
|
|||||||
logger.info("Civitai request skipped: %s", OFFLINE_FRIENDLY_MESSAGE)
|
logger.info("Civitai request skipped: %s", OFFLINE_FRIENDLY_MESSAGE)
|
||||||
return None
|
return None
|
||||||
if message:
|
if message:
|
||||||
|
if self._is_transient_server_error(message):
|
||||||
|
logger.info(
|
||||||
|
"Transient server error for model %s: %s",
|
||||||
|
model_id,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
return None
|
||||||
raise RuntimeError(message)
|
raise RuntimeError(message)
|
||||||
return None
|
return None
|
||||||
except RateLimitError:
|
except RateLimitError:
|
||||||
@@ -482,6 +556,14 @@ class CivitaiClient:
|
|||||||
- The model version data or None if not found
|
- The model version data or None if not found
|
||||||
- An error message if there was an error, or None on success
|
- An error message if there was an error, or None on success
|
||||||
"""
|
"""
|
||||||
|
# In-memory cache avoids redundant API calls within the same
|
||||||
|
# import/scan flow (e.g. _resolve_base_model_from_checkpoint
|
||||||
|
# followed by _resolve_and_populate_checkpoint with the same id).
|
||||||
|
if version_id in self._version_info_cache:
|
||||||
|
logger.debug("Cache hit for model version info: %s", version_id)
|
||||||
|
self._version_info_cache.move_to_end(version_id) # LRU bump
|
||||||
|
return self._version_info_cache[version_id]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = f"{self.base_url}/model-versions/{version_id}"
|
url = f"{self.base_url}/model-versions/{version_id}"
|
||||||
|
|
||||||
@@ -491,6 +573,11 @@ class CivitaiClient:
|
|||||||
if success:
|
if success:
|
||||||
logger.debug("Successfully fetched model version info for: %s", version_id)
|
logger.debug("Successfully fetched model version info for: %s", version_id)
|
||||||
self._remove_comfy_metadata(result)
|
self._remove_comfy_metadata(result)
|
||||||
|
self._version_info_cache[version_id] = (result, None)
|
||||||
|
self._version_info_cache.move_to_end(version_id)
|
||||||
|
# Evict oldest entry when over capacity
|
||||||
|
if len(self._version_info_cache) > self._MAX_CACHE_ENTRIES:
|
||||||
|
self._version_info_cache.popitem(last=False)
|
||||||
return result, None
|
return result, None
|
||||||
|
|
||||||
# Handle specific error cases
|
# Handle specific error cases
|
||||||
@@ -532,6 +619,13 @@ class CivitaiClient:
|
|||||||
if not success:
|
if not success:
|
||||||
if is_expected_offline_error(result):
|
if is_expected_offline_error(result):
|
||||||
return None
|
return None
|
||||||
|
if self._is_transient_server_error(str(result)):
|
||||||
|
logger.info(
|
||||||
|
"Transient server error fetching image info for ID %s: %s",
|
||||||
|
image_id,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
return None
|
||||||
logger.error(
|
logger.error(
|
||||||
"Failed to fetch image info for ID %s from civitai.red: %s",
|
"Failed to fetch image info for ID %s from civitai.red: %s",
|
||||||
image_id,
|
image_id,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from ..utils.constants import (
|
|||||||
VALID_LORA_TYPES,
|
VALID_LORA_TYPES,
|
||||||
)
|
)
|
||||||
from ..utils.civitai_utils import normalize_civitai_download_url, rewrite_preview_url
|
from ..utils.civitai_utils import normalize_civitai_download_url, rewrite_preview_url
|
||||||
|
from ..utils.file_utils import calculate_sha256
|
||||||
from ..utils.preview_selection import resolve_mature_threshold, select_preview_media
|
from ..utils.preview_selection import resolve_mature_threshold, select_preview_media
|
||||||
from ..utils.utils import sanitize_folder_name
|
from ..utils.utils import sanitize_folder_name
|
||||||
from ..utils.exif_utils import ExifUtils
|
from ..utils.exif_utils import ExifUtils
|
||||||
@@ -2239,8 +2240,11 @@ class DownloadManager:
|
|||||||
entry.file_name = os.path.splitext(os.path.basename(file_path))[0]
|
entry.file_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
# Update size to actual downloaded file size
|
# Update size to actual downloaded file size
|
||||||
entry.size = os.path.getsize(file_path)
|
entry.size = os.path.getsize(file_path)
|
||||||
# Use SHA256 from API metadata (already set in from_civitai_info)
|
# Compute SHA256 locally when the API response didn't include it
|
||||||
# Do not recalculate to avoid blocking during ComfyUI execution
|
if not entry.sha256:
|
||||||
|
sha256 = await calculate_sha256(file_path)
|
||||||
|
if sha256:
|
||||||
|
entry.sha256 = sha256.lower()
|
||||||
entries.append(entry)
|
entries.append(entry)
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
|
|||||||
@@ -312,8 +312,23 @@ class LoraService(BaseModelService):
|
|||||||
"""Return cached raw metadata for a LoRA matching the given filename."""
|
"""Return cached raw metadata for a LoRA matching the given filename."""
|
||||||
cache = await self.scanner.get_cached_data(force_refresh=False)
|
cache = await self.scanner.get_cached_data(force_refresh=False)
|
||||||
|
|
||||||
|
fn_normalized = filename.replace("\\", "/")
|
||||||
|
fn_no_ext = fn_normalized
|
||||||
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
|
if fn_no_ext.lower().endswith(ext):
|
||||||
|
fn_no_ext = fn_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
|
||||||
for lora in cache.raw_data if cache else []:
|
for lora in cache.raw_data if cache else []:
|
||||||
if lora.get("file_name") == filename:
|
file_name = lora.get("file_name", "")
|
||||||
|
folder = lora.get("folder", "")
|
||||||
|
file_name_no_ext = file_name
|
||||||
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
|
if file_name_no_ext.lower().endswith(ext):
|
||||||
|
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||||
|
if fn_no_ext in (file_name_no_ext, path_name):
|
||||||
return lora
|
return lora
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -401,7 +416,10 @@ class LoraService(BaseModelService):
|
|||||||
locked_loras = locked_loras[:target_count]
|
locked_loras = locked_loras[:target_count]
|
||||||
|
|
||||||
# Filter out locked LoRAs from available pool
|
# Filter out locked LoRAs from available pool
|
||||||
locked_names = {lora["name"] for lora in locked_loras}
|
locked_names = {
|
||||||
|
os.path.basename(lora["name"]) if "/" in str(lora.get("name", "")) else lora["name"]
|
||||||
|
for lora in locked_loras
|
||||||
|
}
|
||||||
available_pool = [
|
available_pool = [
|
||||||
l for l in available_loras if l["file_name"] not in locked_names
|
l for l in available_loras if l["file_name"] not in locked_names
|
||||||
]
|
]
|
||||||
@@ -456,7 +474,7 @@ class LoraService(BaseModelService):
|
|||||||
|
|
||||||
result_loras.append(
|
result_loras.append(
|
||||||
{
|
{
|
||||||
"name": lora["file_name"],
|
"name": f"{lora['folder']}/{lora['file_name']}" if lora.get("folder") else lora["file_name"],
|
||||||
"strength": model_str,
|
"strength": model_str,
|
||||||
"clipStrength": clip_str,
|
"clipStrength": clip_str,
|
||||||
"active": True,
|
"active": True,
|
||||||
@@ -672,8 +690,9 @@ class LoraService(BaseModelService):
|
|||||||
# Return minimal data needed for cycling
|
# Return minimal data needed for cycling
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"file_name": lora["file_name"],
|
"file_name": f"{lora['folder']}/{lora['file_name']}" if lora.get("folder") else lora["file_name"],
|
||||||
"model_name": lora.get("model_name", lora["file_name"]),
|
"model_name": lora.get("model_name", lora["file_name"]),
|
||||||
|
"folder": lora.get("folder", ""),
|
||||||
}
|
}
|
||||||
for lora in available_loras
|
for lora in available_loras
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -209,7 +209,9 @@ class ModelHashIndex:
|
|||||||
return self._filename_to_hash.get(filename)
|
return self._filename_to_hash.get(filename)
|
||||||
|
|
||||||
def get_hash_by_filename(self, filename: str) -> Optional[str]:
|
def get_hash_by_filename(self, filename: str) -> Optional[str]:
|
||||||
"""Get hash for a filename without extension"""
|
"""Get hash for a filename (bare basename or path-prefixed name)"""
|
||||||
|
if "/" in filename or "\\" in filename:
|
||||||
|
filename = os.path.splitext(os.path.basename(filename.replace("\\", "/")))[0]
|
||||||
return self._filename_to_hash.get(filename)
|
return self._filename_to_hash.get(filename)
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Any, Awaitable, Callable, Dict, List, Mapping, Optional, Set,
|
|||||||
|
|
||||||
from ..utils.models import BaseModelMetadata
|
from ..utils.models import BaseModelMetadata
|
||||||
from ..config import config
|
from ..config import config
|
||||||
from ..utils.file_utils import find_preview_file, get_preview_extension
|
from ..utils.file_utils import find_preview_file, get_preview_extension, calculate_sha256
|
||||||
from ..utils.metadata_manager import MetadataManager
|
from ..utils.metadata_manager import MetadataManager
|
||||||
from ..utils.civitai_utils import resolve_license_info
|
from ..utils.civitai_utils import resolve_license_info
|
||||||
from .model_cache import ModelCache
|
from .model_cache import ModelCache
|
||||||
@@ -1067,6 +1067,19 @@ class ModelScanner:
|
|||||||
|
|
||||||
model_data = self._build_cache_entry(metadata, folder=normalized_folder)
|
model_data = self._build_cache_entry(metadata, folder=normalized_folder)
|
||||||
|
|
||||||
|
# Compute SHA256 hash when metadata provided none (e.g., CivitAI API response has empty hashes)
|
||||||
|
if not model_data.get('sha256') and file_path:
|
||||||
|
try:
|
||||||
|
logger.info(f"Computing SHA256 hash for {file_path} (was empty from metadata)")
|
||||||
|
sha256 = await calculate_sha256(file_path)
|
||||||
|
if sha256:
|
||||||
|
model_data['sha256'] = sha256.lower()
|
||||||
|
if isinstance(metadata, BaseModelMetadata):
|
||||||
|
metadata.sha256 = sha256.lower()
|
||||||
|
await MetadataManager.save_metadata(file_path, metadata)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to compute SHA256 for {file_path}: {e}")
|
||||||
|
|
||||||
# Skip excluded models
|
# Skip excluded models
|
||||||
if model_data.get('exclude', False):
|
if model_data.get('exclude', False):
|
||||||
excluded_models.append(model_data['file_path'])
|
excluded_models.append(model_data['file_path'])
|
||||||
@@ -1101,7 +1114,15 @@ class ModelScanner:
|
|||||||
|
|
||||||
def _log_duplicate_filename_summary(self) -> None:
|
def _log_duplicate_filename_summary(self) -> None:
|
||||||
"""Log a batched summary of duplicate filename conflicts once per scan."""
|
"""Log a batched summary of duplicate filename conflicts once per scan."""
|
||||||
if self._hash_index is None:
|
# Duplicate filename detection is only relevant for LoRAs, which use
|
||||||
|
# basename-only syntax (<lora:name:strength>). Checkpoints and embeddings
|
||||||
|
# use full relative paths for resolution, so conflicts are not ambiguous.
|
||||||
|
if self._hash_index is None or self.model_type != "lora":
|
||||||
|
return
|
||||||
|
|
||||||
|
# When full path syntax is active, duplicate filenames across subfolders
|
||||||
|
# are fully qualified, so there is no ambiguity — skip the warning.
|
||||||
|
if get_settings_manager().get("lora_syntax_format", "legacy") == "full":
|
||||||
return
|
return
|
||||||
|
|
||||||
duplicates = self._hash_index.get_duplicate_filenames()
|
duplicates = self._hash_index.get_duplicate_filenames()
|
||||||
@@ -1473,6 +1494,15 @@ class ModelScanner:
|
|||||||
file_path_override=normalized_new_path,
|
file_path_override=normalized_new_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Ensure sha256 is populated even when metadata doesn't have it
|
||||||
|
if not cache_entry.get('sha256') and normalized_new_path and os.path.exists(normalized_new_path):
|
||||||
|
try:
|
||||||
|
sha256 = await calculate_sha256(normalized_new_path)
|
||||||
|
if sha256:
|
||||||
|
cache_entry['sha256'] = sha256.lower()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to compute SHA256 for {normalized_new_path}: {e}")
|
||||||
|
|
||||||
if recalculate_type:
|
if recalculate_type:
|
||||||
cache_entry = self.adjust_cached_entry(cache_entry)
|
cache_entry = self.adjust_cached_entry(cache_entry)
|
||||||
|
|
||||||
@@ -1573,11 +1603,38 @@ class ModelScanner:
|
|||||||
try:
|
try:
|
||||||
cache = await self.get_cached_data()
|
cache = await self.get_cached_data()
|
||||||
|
|
||||||
|
name_normalized = name.replace("\\", "/")
|
||||||
|
name_no_ext = name_normalized
|
||||||
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
|
if name_no_ext.lower().endswith(ext):
|
||||||
|
name_no_ext = name_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
|
||||||
|
has_path = "/" in name_no_ext
|
||||||
|
basename = os.path.basename(name_no_ext) if has_path else name_no_ext
|
||||||
|
best_fallback = None
|
||||||
|
|
||||||
for model in cache.raw_data:
|
for model in cache.raw_data:
|
||||||
if model.get("file_name") == name:
|
file_name = model.get("file_name", "")
|
||||||
|
folder = model.get("folder", "")
|
||||||
|
file_name_no_ext = file_name
|
||||||
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
|
if file_name_no_ext.lower().endswith(ext):
|
||||||
|
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||||
|
|
||||||
|
if name_no_ext == file_name_no_ext or name_no_ext == path_name:
|
||||||
return model
|
return model
|
||||||
|
|
||||||
return None
|
if has_path and file_name_no_ext == basename:
|
||||||
|
if folder and name_no_ext.startswith(folder.replace("\\", "/") + "/"):
|
||||||
|
best_fallback = model
|
||||||
|
elif best_fallback is None:
|
||||||
|
best_fallback = model
|
||||||
|
|
||||||
|
return best_fallback
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting model info by name: {e}", exc_info=True)
|
logger.error(f"Error getting model info by name: {e}", exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1000,12 +1000,11 @@ class ModelUpdateService:
|
|||||||
fallback_error_message = str(exc) or "resource not found"
|
fallback_error_message = str(exc) or "resource not found"
|
||||||
mark_model_as_ignored = True
|
mark_model_as_ignored = True
|
||||||
except Exception as exc: # pragma: no cover - defensive log
|
except Exception as exc: # pragma: no cover - defensive log
|
||||||
logger.error(
|
logger.warning(
|
||||||
"Failed to fetch versions for model %s (%s): %s",
|
"Failed to fetch versions for model %s (%s): %s",
|
||||||
model_id,
|
model_id,
|
||||||
model_type,
|
model_type,
|
||||||
exc,
|
exc,
|
||||||
exc_info=True,
|
|
||||||
)
|
)
|
||||||
fallback_error_message = str(exc)
|
fallback_error_message = str(exc)
|
||||||
if response is not None:
|
if response is not None:
|
||||||
|
|||||||
@@ -2517,6 +2517,7 @@ class RecipeScanner:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
file_name = None
|
file_name = None
|
||||||
|
folder = ""
|
||||||
hash_value = (lora.get("hash") or "").lower()
|
hash_value = (lora.get("hash") or "").lower()
|
||||||
if (
|
if (
|
||||||
hash_value
|
hash_value
|
||||||
@@ -2526,6 +2527,11 @@ class RecipeScanner:
|
|||||||
file_path = self._lora_scanner._hash_index.get_path(hash_value)
|
file_path = self._lora_scanner._hash_index.get_path(hash_value)
|
||||||
if file_path:
|
if file_path:
|
||||||
file_name = os.path.splitext(os.path.basename(file_path))[0]
|
file_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
|
if lora_cache is not None:
|
||||||
|
for cached_lora in getattr(lora_cache, "raw_data", []):
|
||||||
|
if cached_lora.get("file_path") == file_path:
|
||||||
|
folder = cached_lora.get("folder", "")
|
||||||
|
break
|
||||||
|
|
||||||
if not file_name and lora.get("modelVersionId") and lora_cache is not None:
|
if not file_name and lora.get("modelVersionId") and lora_cache is not None:
|
||||||
for cached_lora in getattr(lora_cache, "raw_data", []):
|
for cached_lora in getattr(lora_cache, "raw_data", []):
|
||||||
@@ -2540,13 +2546,16 @@ class RecipeScanner:
|
|||||||
file_name = os.path.splitext(os.path.basename(cached_path))[
|
file_name = os.path.splitext(os.path.basename(cached_path))[
|
||||||
0
|
0
|
||||||
]
|
]
|
||||||
|
folder = cached_lora.get("folder", "")
|
||||||
break
|
break
|
||||||
|
|
||||||
if not file_name:
|
if not file_name:
|
||||||
file_name = lora.get("file_name", "unknown-lora")
|
file_name = lora.get("file_name", "unknown-lora")
|
||||||
|
folder = lora.get("folder", "")
|
||||||
|
|
||||||
|
lora_name = f"{folder}/{file_name}" if folder else file_name
|
||||||
strength = lora.get("strength", 1.0)
|
strength = lora.get("strength", 1.0)
|
||||||
syntax_parts.append(f"<lora:{file_name}:{strength}>")
|
syntax_parts.append(f"<lora:{lora_name}:{strength}>")
|
||||||
|
|
||||||
return syntax_parts
|
return syntax_parts
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
|
|||||||
"compact_mode": False,
|
"compact_mode": False,
|
||||||
"priority_tags": DEFAULT_PRIORITY_TAG_CONFIG.copy(),
|
"priority_tags": DEFAULT_PRIORITY_TAG_CONFIG.copy(),
|
||||||
"model_name_display": "model_name",
|
"model_name_display": "model_name",
|
||||||
|
"lora_syntax_format": "legacy",
|
||||||
"model_card_footer_action": "replace_preview",
|
"model_card_footer_action": "replace_preview",
|
||||||
"show_version_on_card": True,
|
"show_version_on_card": True,
|
||||||
"update_flag_strategy": "same_base",
|
"update_flag_strategy": "same_base",
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from typing import Awaitable, Callable, Dict, List, Sequence
|
from typing import Awaitable, Callable, Dict, List, Sequence, Tuple
|
||||||
|
|
||||||
|
from .auto_tag_service import extract_auto_tags
|
||||||
|
|
||||||
|
|
||||||
class TagUpdateService:
|
class TagUpdateService:
|
||||||
@@ -20,9 +22,8 @@ class TagUpdateService:
|
|||||||
new_tags: Sequence[str],
|
new_tags: Sequence[str],
|
||||||
metadata_loader: Callable[[str], Awaitable[Dict[str, object]]],
|
metadata_loader: Callable[[str], Awaitable[Dict[str, object]]],
|
||||||
update_cache: Callable[[str, str, Dict[str, object]], Awaitable[bool]],
|
update_cache: Callable[[str, str, Dict[str, object]], Awaitable[bool]],
|
||||||
) -> List[str]:
|
) -> Tuple[List[str], List[str]]:
|
||||||
"""Add tags to a metadata entry while keeping case-insensitive uniqueness."""
|
"""Add tags to a metadata entry and return updated tags and auto_tags."""
|
||||||
|
|
||||||
base, _ = os.path.splitext(file_path)
|
base, _ = os.path.splitext(file_path)
|
||||||
metadata_path = f"{base}.metadata.json"
|
metadata_path = f"{base}.metadata.json"
|
||||||
metadata = await metadata_loader(metadata_path)
|
metadata = await metadata_loader(metadata_path)
|
||||||
@@ -44,5 +45,6 @@ class TagUpdateService:
|
|||||||
await self._metadata_manager.save_metadata(file_path, metadata)
|
await self._metadata_manager.save_metadata(file_path, metadata)
|
||||||
await update_cache(file_path, file_path, metadata)
|
await update_cache(file_path, file_path, metadata)
|
||||||
|
|
||||||
return existing_tags
|
auto_tags = extract_auto_tags(metadata)
|
||||||
|
return existing_tags, auto_tags
|
||||||
|
|
||||||
|
|||||||
@@ -239,9 +239,9 @@ def _resolve_commercial_bits(values: Sequence[str]) -> int:
|
|||||||
normalized_values.add(normalized)
|
normalized_values.add(normalized)
|
||||||
|
|
||||||
has_sell = "sell" in normalized_values
|
has_sell = "sell" in normalized_values
|
||||||
has_rent = has_sell or "rent" in normalized_values
|
has_rent = "rent" in normalized_values
|
||||||
has_rentcivit = has_rent or "rentcivit" in normalized_values
|
has_rentcivit = "rentcivit" in normalized_values
|
||||||
has_image = has_sell or "image" in normalized_values
|
has_image = "image" in normalized_values
|
||||||
|
|
||||||
commercial_bits = (
|
commercial_bits = (
|
||||||
(1 if has_sell else 0) << 3
|
(1 if has_sell else 0) << 3
|
||||||
|
|||||||
@@ -15,30 +15,64 @@ def get_lora_info(lora_name):
|
|||||||
scanner = await ServiceRegistry.get_lora_scanner()
|
scanner = await ServiceRegistry.get_lora_scanner()
|
||||||
cache = await scanner.get_cached_data()
|
cache = await scanner.get_cached_data()
|
||||||
|
|
||||||
|
lora_name_normalized = lora_name.replace("\\", "/")
|
||||||
|
lora_name_no_ext = lora_name_normalized
|
||||||
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
|
if lora_name_no_ext.lower().endswith(ext):
|
||||||
|
lora_name_no_ext = lora_name_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
|
||||||
|
has_path = "/" in lora_name_no_ext
|
||||||
|
basename = os.path.basename(lora_name_no_ext) if has_path else lora_name_no_ext
|
||||||
|
best_fallback = None
|
||||||
|
|
||||||
for item in cache.raw_data:
|
for item in cache.raw_data:
|
||||||
if item.get("file_name") == lora_name:
|
file_name = item.get("file_name", "")
|
||||||
file_path = item.get("file_path")
|
folder = item.get("folder", "")
|
||||||
if file_path:
|
file_name_no_ext = file_name
|
||||||
# Check all lora roots including extra paths
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
all_roots = list(config.loras_roots or []) + list(
|
if file_name_no_ext.lower().endswith(ext):
|
||||||
config.extra_loras_roots or []
|
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||||
|
|
||||||
|
if lora_name_no_ext not in (file_name_no_ext, path_name):
|
||||||
|
if has_path and file_name_no_ext == basename:
|
||||||
|
if folder and lora_name_no_ext.startswith(folder.replace("\\", "/") + "/"):
|
||||||
|
best_fallback = item
|
||||||
|
elif best_fallback is None:
|
||||||
|
best_fallback = item
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_path = item.get("file_path")
|
||||||
|
if not file_path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
all_roots = list(config.loras_roots or []) + list(
|
||||||
|
config.extra_loras_roots or []
|
||||||
|
)
|
||||||
|
for root in all_roots:
|
||||||
|
root = root.replace(os.sep, "/")
|
||||||
|
if file_path.startswith(root):
|
||||||
|
relative_path = os.path.relpath(file_path, root).replace(
|
||||||
|
os.sep, "/"
|
||||||
)
|
)
|
||||||
for root in all_roots:
|
|
||||||
root = root.replace(os.sep, "/")
|
|
||||||
if file_path.startswith(root):
|
|
||||||
relative_path = os.path.relpath(file_path, root).replace(
|
|
||||||
os.sep, "/"
|
|
||||||
)
|
|
||||||
# Get trigger words from civitai metadata
|
|
||||||
civitai = item.get("civitai", {})
|
|
||||||
trigger_words = (
|
|
||||||
civitai.get("trainedWords", []) if civitai else []
|
|
||||||
)
|
|
||||||
return relative_path, trigger_words
|
|
||||||
# If not found in any root, return path with trigger words from cache
|
|
||||||
civitai = item.get("civitai", {})
|
civitai = item.get("civitai", {})
|
||||||
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
trigger_words = (
|
||||||
return file_path, trigger_words
|
civitai.get("trainedWords", []) if civitai else []
|
||||||
|
)
|
||||||
|
return relative_path, trigger_words
|
||||||
|
civitai = item.get("civitai", {})
|
||||||
|
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
||||||
|
return file_path, trigger_words
|
||||||
|
|
||||||
|
if best_fallback:
|
||||||
|
file_path = best_fallback.get("file_path")
|
||||||
|
if file_path:
|
||||||
|
civitai = best_fallback.get("civitai", {})
|
||||||
|
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
||||||
|
return file_path, trigger_words
|
||||||
|
|
||||||
return lora_name, []
|
return lora_name, []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -77,15 +111,54 @@ def get_lora_info_absolute(lora_name):
|
|||||||
scanner = await ServiceRegistry.get_lora_scanner()
|
scanner = await ServiceRegistry.get_lora_scanner()
|
||||||
cache = await scanner.get_cached_data()
|
cache = await scanner.get_cached_data()
|
||||||
|
|
||||||
|
lora_name_normalized = lora_name.replace("\\", "/")
|
||||||
|
lora_name_no_ext = lora_name_normalized
|
||||||
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
|
if lora_name_no_ext.lower().endswith(ext):
|
||||||
|
lora_name_no_ext = lora_name_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
|
||||||
|
has_path = "/" in lora_name_no_ext
|
||||||
|
basename = os.path.basename(lora_name_no_ext) if has_path else lora_name_no_ext
|
||||||
|
best_fallback = None
|
||||||
|
|
||||||
for item in cache.raw_data:
|
for item in cache.raw_data:
|
||||||
if item.get("file_name") == lora_name:
|
file_name = item.get("file_name", "")
|
||||||
|
folder = item.get("folder", "")
|
||||||
|
file_name_no_ext = file_name
|
||||||
|
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||||
|
if file_name_no_ext.lower().endswith(ext):
|
||||||
|
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||||
|
break
|
||||||
|
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||||
|
|
||||||
|
if lora_name_no_ext == file_name_no_ext:
|
||||||
file_path = item.get("file_path")
|
file_path = item.get("file_path")
|
||||||
if file_path:
|
if file_path:
|
||||||
# Return absolute path directly
|
|
||||||
# Get trigger words from civitai metadata
|
|
||||||
civitai = item.get("civitai", {})
|
civitai = item.get("civitai", {})
|
||||||
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
||||||
return file_path, trigger_words
|
return file_path, trigger_words
|
||||||
|
|
||||||
|
if lora_name_no_ext == path_name:
|
||||||
|
file_path = item.get("file_path")
|
||||||
|
if file_path:
|
||||||
|
civitai = item.get("civitai", {})
|
||||||
|
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
||||||
|
return file_path, trigger_words
|
||||||
|
|
||||||
|
if has_path and file_name_no_ext == basename:
|
||||||
|
if folder and lora_name_no_ext.startswith(folder.replace("\\", "/") + "/"):
|
||||||
|
best_fallback = item
|
||||||
|
elif best_fallback is None:
|
||||||
|
best_fallback = item
|
||||||
|
|
||||||
|
if best_fallback:
|
||||||
|
file_path = best_fallback.get("file_path")
|
||||||
|
if file_path:
|
||||||
|
civitai = best_fallback.get("civitai", {})
|
||||||
|
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
||||||
|
return file_path, trigger_words
|
||||||
|
|
||||||
return lora_name, []
|
return lora_name, []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -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.7"
|
version = "1.0.8"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -255,25 +255,28 @@
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* File name copy styles */
|
/* Editable inline field styles (file name, version name, etc.) */
|
||||||
.file-name-wrapper {
|
.file-name-wrapper,
|
||||||
|
.version-name-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 4px;
|
padding: 4px 0;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-name-content {
|
.file-name-content,
|
||||||
padding: 2px 4px;
|
.version-name-content {
|
||||||
|
padding: 2px 4px 2px 0;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-name-wrapper.editing .file-name-content {
|
.file-name-wrapper.editing .file-name-content,
|
||||||
|
.version-name-wrapper.editing .version-name-content {
|
||||||
border: 1px solid var(--lora-accent);
|
border: 1px solid var(--lora-accent);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -283,7 +286,8 @@
|
|||||||
.edit-model-name-btn,
|
.edit-model-name-btn,
|
||||||
.edit-file-name-btn,
|
.edit-file-name-btn,
|
||||||
.edit-base-model-btn,
|
.edit-base-model-btn,
|
||||||
.edit-model-description-btn {
|
.edit-model-description-btn,
|
||||||
|
.edit-version-name-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
@@ -299,9 +303,11 @@
|
|||||||
.edit-file-name-btn.visible,
|
.edit-file-name-btn.visible,
|
||||||
.edit-base-model-btn.visible,
|
.edit-base-model-btn.visible,
|
||||||
.edit-model-description-btn.visible,
|
.edit-model-description-btn.visible,
|
||||||
|
.edit-version-name-btn.visible,
|
||||||
.model-name-header:hover .edit-model-name-btn,
|
.model-name-header:hover .edit-model-name-btn,
|
||||||
.file-name-wrapper:hover .edit-file-name-btn,
|
.file-name-wrapper:hover .edit-file-name-btn,
|
||||||
.base-model-display:hover .edit-base-model-btn,
|
.base-model-display:hover .edit-base-model-btn,
|
||||||
|
.version-name-wrapper:hover .edit-version-name-btn,
|
||||||
.model-name-header:hover .edit-model-description-btn {
|
.model-name-header:hover .edit-model-description-btn {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
@@ -309,14 +315,16 @@
|
|||||||
.edit-model-name-btn:hover,
|
.edit-model-name-btn:hover,
|
||||||
.edit-file-name-btn:hover,
|
.edit-file-name-btn:hover,
|
||||||
.edit-base-model-btn:hover,
|
.edit-base-model-btn:hover,
|
||||||
.edit-model-description-btn:hover {
|
.edit-model-description-btn:hover,
|
||||||
|
.edit-version-name-btn:hover {
|
||||||
opacity: 0.8 !important;
|
opacity: 0.8 !important;
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .edit-model-name-btn:hover,
|
[data-theme="dark"] .edit-model-name-btn:hover,
|
||||||
[data-theme="dark"] .edit-file-name-btn:hover,
|
[data-theme="dark"] .edit-file-name-btn:hover,
|
||||||
[data-theme="dark"] .edit-base-model-btn:hover {
|
[data-theme="dark"] .edit-base-model-btn:hover,
|
||||||
|
[data-theme="dark"] .edit-version-name-btn:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +346,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.base-model-content {
|
.base-model-content {
|
||||||
padding: 2px 4px;
|
padding: 2px 4px 2px 0;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
|||||||
@@ -33,6 +33,39 @@
|
|||||||
animation: modalFadeIn 0.2s ease-out;
|
animation: modalFadeIn 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#resolveFilenameConflictsModal .confirmation-message {
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resolveFilenameConflictsModal .resolve-conflicts-detail {
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
font-size: 0.95em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resolveFilenameConflictsModal .resolve-conflicts-detail code {
|
||||||
|
background: var(--lora-surface);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: monospace;
|
||||||
|
border: 1px solid var(--lora-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
#resolveFilenameConflictsModal .resolve-conflicts-impact {
|
||||||
|
background: var(--lora-surface);
|
||||||
|
border: 1px solid var(--lora-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: var(--space-2);
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.delete-model-info,
|
.delete-model-info,
|
||||||
.exclude-model-info {
|
.exclude-model-info {
|
||||||
/* Update info display styling */
|
/* Update info display styling */
|
||||||
|
|||||||
@@ -1369,3 +1369,14 @@ input:checked + .toggle-slider:before {
|
|||||||
background: var(--lora-error);
|
background: var(--lora-error);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Highlight animation for setting items targeted from Doctor actions */
|
||||||
|
@keyframes settings-highlight-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(from var(--lora-accent) r g b / 0.4); }
|
||||||
|
50% { box-shadow: 0 0 0 4px rgba(from var(--lora-accent) r g b / 0.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-setting-highlight {
|
||||||
|
animation: settings-highlight-pulse 1.5s ease-in-out 3;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
}
|
||||||
|
|||||||
@@ -422,8 +422,12 @@ export class BaseModelApiClient {
|
|||||||
throw new Error('Failed to save metadata');
|
throw new Error('Failed to save metadata');
|
||||||
}
|
}
|
||||||
|
|
||||||
state.virtualScroller.updateSingleItem(filePath, data);
|
const result = await response.json();
|
||||||
return response.json();
|
state.virtualScroller.updateSingleItem(filePath, {
|
||||||
|
...data,
|
||||||
|
auto_tags: result.auto_tags,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
state.loadingManager.hide();
|
state.loadingManager.hide();
|
||||||
}
|
}
|
||||||
@@ -448,7 +452,10 @@ export class BaseModelApiClient {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success && result.tags) {
|
if (result.success && result.tags) {
|
||||||
state.virtualScroller.updateSingleItem(filePath, { tags: result.tags });
|
state.virtualScroller.updateSingleItem(filePath, {
|
||||||
|
tags: result.tags,
|
||||||
|
auto_tags: result.auto_tags,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -166,7 +166,9 @@ async function toggleFavorite(card) {
|
|||||||
function handleSendToWorkflow(card, replaceMode, modelType) {
|
function handleSendToWorkflow(card, replaceMode, modelType) {
|
||||||
if (modelType === MODEL_TYPES.LORA) {
|
if (modelType === MODEL_TYPES.LORA) {
|
||||||
const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
|
const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
|
||||||
const loraSyntax = buildLoraSyntax(card.dataset.file_name, usageTips);
|
const folder = card.dataset.folder || '';
|
||||||
|
const loraName = folder ? `${folder}/${card.dataset.file_name}` : card.dataset.file_name;
|
||||||
|
const loraSyntax = buildLoraSyntax(loraName, usageTips);
|
||||||
sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
|
sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
|
||||||
} else if (modelType === MODEL_TYPES.CHECKPOINT) {
|
} else if (modelType === MODEL_TYPES.CHECKPOINT) {
|
||||||
const modelPath = card.dataset.filepath;
|
const modelPath = card.dataset.filepath;
|
||||||
|
|||||||
@@ -66,6 +66,12 @@ function updateModalFilePathReferences(newFilePath) {
|
|||||||
fileNameContent.setAttribute('data-file-path', newFilePath);
|
fileNameContent.setAttribute('data-file-path', newFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const versionNameContent = scopedQuery('.version-name-content');
|
||||||
|
if (versionNameContent && versionNameContent.dataset) {
|
||||||
|
versionNameContent.dataset.filePath = newFilePath;
|
||||||
|
versionNameContent.setAttribute('data-file-path', newFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
const editTagsBtn = scopedQuery('.edit-tags-btn');
|
const editTagsBtn = scopedQuery('.edit-tags-btn');
|
||||||
if (editTagsBtn) {
|
if (editTagsBtn) {
|
||||||
editTagsBtn.dataset.filePath = newFilePath;
|
editTagsBtn.dataset.filePath = newFilePath;
|
||||||
@@ -516,3 +522,127 @@ export function setupFileNameEditing(filePath) {
|
|||||||
editBtn.classList.remove('visible');
|
editBtn.classList.remove('visible');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up version name editing functionality
|
||||||
|
* @param {string} filePath - File path
|
||||||
|
*/
|
||||||
|
export function setupVersionNameEditing(filePath) {
|
||||||
|
const versionNameContent = document.querySelector('.version-name-content');
|
||||||
|
const editBtn = document.querySelector('.edit-version-name-btn');
|
||||||
|
|
||||||
|
if (!versionNameContent || !editBtn) return;
|
||||||
|
|
||||||
|
// Store the file path in a data attribute for later use
|
||||||
|
versionNameContent.dataset.filePath = filePath;
|
||||||
|
|
||||||
|
// Show edit button on hover
|
||||||
|
const versionNameWrapper = document.querySelector('.version-name-wrapper');
|
||||||
|
versionNameWrapper.addEventListener('mouseenter', () => {
|
||||||
|
editBtn.classList.add('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
versionNameWrapper.addEventListener('mouseleave', () => {
|
||||||
|
if (!versionNameWrapper.classList.contains('editing')) {
|
||||||
|
editBtn.classList.remove('visible');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle edit button click
|
||||||
|
editBtn.addEventListener('click', () => {
|
||||||
|
versionNameWrapper.classList.add('editing');
|
||||||
|
versionNameContent.setAttribute('contenteditable', 'true');
|
||||||
|
// Store original value for comparison later
|
||||||
|
versionNameContent.dataset.originalValue = versionNameContent.textContent.trim();
|
||||||
|
versionNameContent.focus();
|
||||||
|
|
||||||
|
// Place cursor at the end
|
||||||
|
const range = document.createRange();
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (versionNameContent.childNodes.length > 0) {
|
||||||
|
range.setStart(versionNameContent.childNodes[0], versionNameContent.textContent.length);
|
||||||
|
range.collapse(true);
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
editBtn.classList.add('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle keyboard events in edit mode
|
||||||
|
versionNameContent.addEventListener('keydown', function(e) {
|
||||||
|
if (!this.getAttribute('contenteditable')) return;
|
||||||
|
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.blur(); // Trigger save on Enter
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
// Restore original value
|
||||||
|
this.textContent = this.dataset.originalValue;
|
||||||
|
exitEditMode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Limit version name length
|
||||||
|
versionNameContent.addEventListener('input', function() {
|
||||||
|
if (!this.getAttribute('contenteditable')) return;
|
||||||
|
|
||||||
|
if (this.textContent.length > 100) {
|
||||||
|
this.textContent = this.textContent.substring(0, 100);
|
||||||
|
// Place cursor at the end
|
||||||
|
const range = document.createRange();
|
||||||
|
const sel = window.getSelection();
|
||||||
|
range.setStart(this.childNodes[0], 100);
|
||||||
|
range.collapse(true);
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
|
||||||
|
showToast('toast.models.nameTooLong', {}, 'warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle focus out - save changes
|
||||||
|
versionNameContent.addEventListener('blur', async function() {
|
||||||
|
if (!this.getAttribute('contenteditable')) return;
|
||||||
|
|
||||||
|
const newVersionName = this.textContent.trim();
|
||||||
|
const originalValue = this.dataset.originalValue;
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (!newVersionName) {
|
||||||
|
// Restore original value if empty
|
||||||
|
this.textContent = originalValue;
|
||||||
|
showToast('toast.models.nameCannotBeEmpty', {}, 'error');
|
||||||
|
exitEditMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newVersionName === originalValue) {
|
||||||
|
// No changes, just exit edit mode
|
||||||
|
exitEditMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Resolve current file path from modal state
|
||||||
|
const filePath = getActiveModalFilePath(this.dataset.filePath);
|
||||||
|
|
||||||
|
await getModelApiClient().saveModelMetadata(filePath, { civitai: { name: newVersionName } });
|
||||||
|
|
||||||
|
showToast('toast.models.nameUpdatedSuccessfully', {}, 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating version name:', error);
|
||||||
|
this.textContent = originalValue; // Restore original version name
|
||||||
|
showToast('toast.models.nameUpdateFailed', {}, 'error');
|
||||||
|
} finally {
|
||||||
|
exitEditMode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function exitEditMode() {
|
||||||
|
versionNameContent.removeAttribute('contenteditable');
|
||||||
|
versionNameWrapper.classList.remove('editing');
|
||||||
|
editBtn.classList.remove('visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { setupTabSwitching } from './ModelDescription.js';
|
|||||||
import {
|
import {
|
||||||
setupModelNameEditing,
|
setupModelNameEditing,
|
||||||
setupBaseModelEditing,
|
setupBaseModelEditing,
|
||||||
setupFileNameEditing
|
setupFileNameEditing,
|
||||||
|
setupVersionNameEditing
|
||||||
} from './ModelMetadata.js';
|
} from './ModelMetadata.js';
|
||||||
import { setupTagEditMode } from './ModelTags.js';
|
import { setupTagEditMode } from './ModelTags.js';
|
||||||
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||||
@@ -466,7 +467,12 @@ export async function showModelModal(model, modelType) {
|
|||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<label>${translate('modals.model.metadata.version', {}, 'Version')}</label>
|
<label>${translate('modals.model.metadata.version', {}, 'Version')}</label>
|
||||||
<span>${modelWithFullData.civitai?.name || 'N/A'}</span>
|
<div class="version-name-wrapper">
|
||||||
|
<span class="version-name-content">${modelWithFullData.civitai?.name || 'N/A'}</span>
|
||||||
|
<button class="edit-version-name-btn" title="${translate('modals.model.actions.editVersionName', {}, 'Edit version name')}">
|
||||||
|
<i class="fas fa-pencil-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<label>${translate('modals.model.metadata.fileName', {}, 'File Name')}</label>
|
<label>${translate('modals.model.metadata.fileName', {}, 'File Name')}</label>
|
||||||
@@ -660,6 +666,7 @@ export async function showModelModal(model, modelType) {
|
|||||||
setupTagTooltip();
|
setupTagTooltip();
|
||||||
setupTagEditMode(modelType);
|
setupTagEditMode(modelType);
|
||||||
setupModelNameEditing(modelWithFullData.file_path);
|
setupModelNameEditing(modelWithFullData.file_path);
|
||||||
|
setupVersionNameEditing(modelWithFullData.file_path);
|
||||||
setupBaseModelEditing(modelWithFullData.file_path);
|
setupBaseModelEditing(modelWithFullData.file_path);
|
||||||
setupFileNameEditing(modelWithFullData.file_path);
|
setupFileNameEditing(modelWithFullData.file_path);
|
||||||
setupEventHandlers(modelWithFullData.file_path, modelType);
|
setupEventHandlers(modelWithFullData.file_path, modelType);
|
||||||
|
|||||||
@@ -274,7 +274,17 @@ async function saveTags() {
|
|||||||
|
|
||||||
const filePath = editBtn.dataset.filePath;
|
const filePath = editBtn.dataset.filePath;
|
||||||
const tagElements = document.querySelectorAll('.metadata-item');
|
const tagElements = document.querySelectorAll('.metadata-item');
|
||||||
const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
let tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
||||||
|
|
||||||
|
// Flush uncommitted input as a tag so it's not silently lost on save
|
||||||
|
const tagInput = document.querySelector('.metadata-input');
|
||||||
|
if (tagInput) {
|
||||||
|
const pendingTag = tagInput.value.trim().toLowerCase();
|
||||||
|
if (pendingTag && !tags.includes(pendingTag)) {
|
||||||
|
tags.push(pendingTag);
|
||||||
|
}
|
||||||
|
tagInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
// Get original tags to compare
|
// Get original tags to compare
|
||||||
const originalTagElements = document.querySelectorAll('.tooltip-tag');
|
const originalTagElements = document.querySelectorAll('.tooltip-tag');
|
||||||
@@ -465,6 +475,7 @@ function setupTagInput() {
|
|||||||
const tagInput = document.querySelector('.metadata-input');
|
const tagInput = document.querySelector('.metadata-input');
|
||||||
|
|
||||||
if (tagInput) {
|
if (tagInput) {
|
||||||
|
tagInput.focus();
|
||||||
tagInput.addEventListener('keydown', function(e) {
|
tagInput.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax, getNSF
|
|||||||
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||||
import { modalManager } from './ModalManager.js';
|
import { modalManager } from './ModalManager.js';
|
||||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||||
import { RecipeSidebarApiClient, updateRecipeMetadata } from '../api/recipeApi.js';
|
import { RecipeSidebarApiClient, updateRecipeMetadata, extractRecipeId } from '../api/recipeApi.js';
|
||||||
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
||||||
import { BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
import { BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
||||||
import { getPriorityTagSuggestions } from '../utils/priorityTagHelpers.js';
|
import { getPriorityTagSuggestions } from '../utils/priorityTagHelpers.js';
|
||||||
@@ -74,7 +74,7 @@ export class BulkManager {
|
|||||||
unfavorite: true
|
unfavorite: true
|
||||||
},
|
},
|
||||||
recipes: {
|
recipes: {
|
||||||
addTags: false,
|
addTags: true,
|
||||||
sendToWorkflow: false,
|
sendToWorkflow: false,
|
||||||
copyAll: false,
|
copyAll: false,
|
||||||
refreshAll: false,
|
refreshAll: false,
|
||||||
@@ -785,6 +785,7 @@ export class BulkManager {
|
|||||||
// Setup tag input behavior
|
// Setup tag input behavior
|
||||||
const tagInput = document.querySelector('.bulk-metadata-input');
|
const tagInput = document.querySelector('.bulk-metadata-input');
|
||||||
if (tagInput) {
|
if (tagInput) {
|
||||||
|
tagInput.focus();
|
||||||
tagInput.addEventListener('keydown', (e) => {
|
tagInput.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1008,7 +1009,17 @@ export class BulkManager {
|
|||||||
|
|
||||||
async saveBulkTags(mode = 'append') {
|
async saveBulkTags(mode = 'append') {
|
||||||
const tagElements = document.querySelectorAll('#bulkTagsItems .metadata-item');
|
const tagElements = document.querySelectorAll('#bulkTagsItems .metadata-item');
|
||||||
const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
let tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
||||||
|
|
||||||
|
// Flush uncommitted input as a tag so it's not silently lost on save
|
||||||
|
const tagInput = document.querySelector('.bulk-metadata-input');
|
||||||
|
if (tagInput) {
|
||||||
|
const pendingTag = tagInput.value.trim().toLowerCase();
|
||||||
|
if (pendingTag && !tags.includes(pendingTag)) {
|
||||||
|
tags.push(pendingTag);
|
||||||
|
}
|
||||||
|
tagInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
if (tags.length === 0) {
|
if (tags.length === 0) {
|
||||||
showToast('toast.models.noTagsToAdd', {}, 'warning');
|
showToast('toast.models.noTagsToAdd', {}, 'warning');
|
||||||
@@ -1032,6 +1043,8 @@ export class BulkManager {
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isRecipes = state.currentPageType === 'recipes';
|
||||||
|
|
||||||
// Add or replace tags for each selected model based on mode
|
// Add or replace tags for each selected model based on mode
|
||||||
for (const filePath of filePaths) {
|
for (const filePath of filePaths) {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
@@ -1039,7 +1052,9 @@ export class BulkManager {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (mode === 'replace') {
|
if (isRecipes) {
|
||||||
|
await this._saveRecipeTags(filePath, tags, mode);
|
||||||
|
} else if (mode === 'replace') {
|
||||||
await apiClient.saveModelMetadata(filePath, { tags: tags });
|
await apiClient.saveModelMetadata(filePath, { tags: tags });
|
||||||
} else {
|
} else {
|
||||||
await apiClient.addTags(filePath, { tags: tags });
|
await apiClient.addTags(filePath, { tags: tags });
|
||||||
@@ -1078,6 +1093,35 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _saveRecipeTags(filePath, newTags, mode) {
|
||||||
|
const recipeId = extractRecipeId(filePath);
|
||||||
|
if (!recipeId) throw new Error('Unable to determine recipe ID');
|
||||||
|
|
||||||
|
let finalTags = newTags;
|
||||||
|
if (mode === 'append') {
|
||||||
|
const recipeItem = state.virtualScroller?.items?.find(
|
||||||
|
item => item.file_path === filePath
|
||||||
|
);
|
||||||
|
const existingTags = recipeItem?.tags || [];
|
||||||
|
finalTags = [...new Set([...existingTags, ...newTags])];
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/lm/recipe/${encodeURIComponent(recipeId)}/update`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ tags: finalTags }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || 'Failed to update recipe tags');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.virtualScroller.updateSingleItem(filePath, { tags: finalTags });
|
||||||
|
}
|
||||||
|
|
||||||
cleanupBulkAddTagsModal() {
|
cleanupBulkAddTagsModal() {
|
||||||
// Clear tags container
|
// Clear tags container
|
||||||
const tagsContainer = document.getElementById('bulkTagsItems');
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
|
|||||||
@@ -225,6 +225,13 @@ export class DoctorManager {
|
|||||||
renderIssueCard(item) {
|
renderIssueCard(item) {
|
||||||
const status = item.status || 'ok';
|
const status = item.status || 'ok';
|
||||||
const tagLabel = this.getStatusLabel(status);
|
const tagLabel = this.getStatusLabel(status);
|
||||||
|
|
||||||
|
const titleKey = `doctor.issues.${item.id || ''}.title`;
|
||||||
|
const displayTitle = translate(titleKey, {}, item.title || '');
|
||||||
|
|
||||||
|
const summaryKey = `doctor.issues.${item.id || ''}.summary.${status}`;
|
||||||
|
const displaySummary = translate(summaryKey, {}, item.summary || '');
|
||||||
|
|
||||||
const details = Array.isArray(item.details) ? item.details : [];
|
const details = Array.isArray(item.details) ? item.details : [];
|
||||||
const listItems = details
|
const listItems = details
|
||||||
.filter((detail) => typeof detail === 'string')
|
.filter((detail) => typeof detail === 'string')
|
||||||
@@ -235,19 +242,22 @@ export class DoctorManager {
|
|||||||
.map((detail) => this.renderInlineDetail(detail))
|
.map((detail) => this.renderInlineDetail(detail))
|
||||||
.join('');
|
.join('');
|
||||||
const actions = (item.actions || [])
|
const actions = (item.actions || [])
|
||||||
.map((action) => `
|
.map((action) => {
|
||||||
|
const actionLabel = translate(`doctor.actions.${action.id}`, {}, action.label);
|
||||||
|
return `
|
||||||
<button class="${action.id === 'repair-cache' || action.id === 'reload-page' ? 'primary-btn' : 'secondary-btn'}" data-doctor-action="${escapeHtml(action.id)}">
|
<button class="${action.id === 'repair-cache' || action.id === 'reload-page' ? 'primary-btn' : 'secondary-btn'}" data-doctor-action="${escapeHtml(action.id)}">
|
||||||
${escapeHtml(action.label)}
|
${escapeHtml(actionLabel)}
|
||||||
</button>
|
</button>
|
||||||
`)
|
`;
|
||||||
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<section class="doctor-issue-card" data-status="${escapeHtml(status)}" data-issue-id="${escapeHtml(item.id || '')}">
|
<section class="doctor-issue-card" data-status="${escapeHtml(status)}" data-issue-id="${escapeHtml(item.id || '')}">
|
||||||
<div class="doctor-issue-header">
|
<div class="doctor-issue-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>${escapeHtml(item.title || '')}</h3>
|
<h3>${escapeHtml(displayTitle)}</h3>
|
||||||
<p class="doctor-issue-summary">${escapeHtml(item.summary || '')}</p>
|
<p class="doctor-issue-summary">${escapeHtml(displaySummary)}</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="doctor-issue-tag">${escapeHtml(tagLabel)}</span>
|
<span class="doctor-issue-tag">${escapeHtml(tagLabel)}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -262,7 +272,7 @@ export class DoctorManager {
|
|||||||
if (detail.conflict_groups || detail.total_conflict_files) {
|
if (detail.conflict_groups || detail.total_conflict_files) {
|
||||||
return `
|
return `
|
||||||
<div class="doctor-inline-detail">
|
<div class="doctor-inline-detail">
|
||||||
<strong>${escapeHtml(translate('doctor.status.warning', {}, 'Conflicts'))}</strong>
|
<strong>${escapeHtml(translate('doctor.labels.conflicts', {}, 'Conflicts'))}</strong>
|
||||||
<div>${escapeHtml(`${detail.conflict_groups || 0} filenames, ${detail.total_conflict_files || 0} files`)}</div>
|
<div>${escapeHtml(`${detail.conflict_groups || 0} filenames, ${detail.total_conflict_files || 0} files`)}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -324,11 +334,42 @@ export class DoctorManager {
|
|||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
break;
|
break;
|
||||||
|
case 'open-settings-syntax-format':
|
||||||
|
modalManager.showModal('settingsModal');
|
||||||
|
window.setTimeout(() => {
|
||||||
|
// Switch to Interface section
|
||||||
|
document.querySelectorAll('.settings-section').forEach((s) => s.classList.remove('active'));
|
||||||
|
const interfaceSection = document.getElementById('section-interface');
|
||||||
|
if (interfaceSection) {
|
||||||
|
interfaceSection.classList.add('active');
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.settings-nav-item').forEach((n) => n.classList.remove('active'));
|
||||||
|
const interfaceNav = document.querySelector('.settings-nav-item[data-section="interface"]');
|
||||||
|
if (interfaceNav) {
|
||||||
|
interfaceNav.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus and scroll to the LoRA Syntax Format dropdown
|
||||||
|
const select = document.getElementById('loraSyntaxFormat');
|
||||||
|
if (select) {
|
||||||
|
select.focus();
|
||||||
|
select.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
// Add temporary highlight animation
|
||||||
|
const settingItem = select.closest('.setting-item');
|
||||||
|
if (settingItem) {
|
||||||
|
settingItem.classList.add('settings-setting-highlight');
|
||||||
|
setTimeout(() => {
|
||||||
|
settingItem.classList.remove('settings-setting-highlight');
|
||||||
|
}, 4500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
break;
|
||||||
case 'repair-cache':
|
case 'repair-cache':
|
||||||
await this.repairCache();
|
await this.repairCache();
|
||||||
break;
|
break;
|
||||||
case 'resolve-filename-conflicts':
|
case 'resolve-filename-conflicts':
|
||||||
await this.resolveFilenameConflicts();
|
await this.promptResolveConflicts();
|
||||||
break;
|
break;
|
||||||
case 'reload-page':
|
case 'reload-page':
|
||||||
this.reloadUi();
|
this.reloadUi();
|
||||||
@@ -358,6 +399,62 @@ export class DoctorManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getConflictStats() {
|
||||||
|
const conflict = (this.lastDiagnostics?.diagnostics || []).find(
|
||||||
|
(d) => d.id === 'filename_conflicts'
|
||||||
|
);
|
||||||
|
if (!conflict || !Array.isArray(conflict.details)) {
|
||||||
|
return { groups: 0, files: 0 };
|
||||||
|
}
|
||||||
|
const summary = conflict.details.find(
|
||||||
|
(d) => d && typeof d === 'object' && d.conflict_groups !== undefined
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
groups: summary?.conflict_groups || 0,
|
||||||
|
files: summary?.total_conflict_files || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async promptResolveConflicts() {
|
||||||
|
const stats = this._getConflictStats();
|
||||||
|
if (stats.groups === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailEl = document.getElementById('resolveConflictsDetail');
|
||||||
|
if (detailEl) {
|
||||||
|
detailEl.innerHTML = translate(
|
||||||
|
'conflictConfirm.detail',
|
||||||
|
{},
|
||||||
|
'Example: <code>Add_Details_v1.2</code> \u2192 <code>Add_Details_v1.2-a3f7</code>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const impactEl = document.getElementById('resolveConflictsImpact');
|
||||||
|
if (impactEl) {
|
||||||
|
impactEl.innerHTML = translate(
|
||||||
|
'conflictConfirm.impact',
|
||||||
|
{ count: stats.files, groups: stats.groups },
|
||||||
|
`Will rename <strong>${stats.files}</strong> file(s) across <strong>${stats.groups}</strong> duplicate group(s).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._confirmResolveResolve = null;
|
||||||
|
modalManager.showModal('resolveFilenameConflictsModal');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this._confirmResolveResolve = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmResolveConflicts() {
|
||||||
|
modalManager.closeModal('resolveFilenameConflictsModal');
|
||||||
|
if (this._confirmResolveResolve) {
|
||||||
|
this._confirmResolveResolve(true);
|
||||||
|
this._confirmResolveResolve = null;
|
||||||
|
}
|
||||||
|
await this.resolveFilenameConflicts();
|
||||||
|
}
|
||||||
|
|
||||||
async resolveFilenameConflicts() {
|
async resolveFilenameConflicts() {
|
||||||
try {
|
try {
|
||||||
this.setLoading(true);
|
this.setLoading(true);
|
||||||
@@ -449,3 +546,8 @@ export class DoctorManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const doctorManager = new DoctorManager();
|
export const doctorManager = new DoctorManager();
|
||||||
|
|
||||||
|
// Make available globally for HTML onclick handlers
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.doctorManager = doctorManager;
|
||||||
|
}
|
||||||
|
|||||||
@@ -316,6 +316,19 @@ export class ModalManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register resolveFilenameConflictsModal
|
||||||
|
const resolveFilenameConflictsModal = document.getElementById('resolveFilenameConflictsModal');
|
||||||
|
if (resolveFilenameConflictsModal) {
|
||||||
|
this.registerModal('resolveFilenameConflictsModal', {
|
||||||
|
element: resolveFilenameConflictsModal,
|
||||||
|
onClose: () => {
|
||||||
|
this.getModal('resolveFilenameConflictsModal').element.classList.remove('show');
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
},
|
||||||
|
closeOnOutsideClick: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', this.boundHandleEscape);
|
document.addEventListener('keydown', this.boundHandleEscape);
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
@@ -396,7 +409,8 @@ export class ModalManager {
|
|||||||
id === "modelDuplicateDeleteModal" ||
|
id === "modelDuplicateDeleteModal" ||
|
||||||
id === "clearCacheModal" ||
|
id === "clearCacheModal" ||
|
||||||
id === "bulkDeleteModal" ||
|
id === "bulkDeleteModal" ||
|
||||||
id === "checkUpdatesConfirmModal"
|
id === "checkUpdatesConfirmModal" ||
|
||||||
|
id === "resolveFilenameConflictsModal"
|
||||||
) {
|
) {
|
||||||
modal.element.classList.add("show");
|
modal.element.classList.add("show");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -295,6 +295,13 @@ export class SettingsManager {
|
|||||||
// Update state
|
// Update state
|
||||||
state.global.settings[settingKey] = value;
|
state.global.settings[settingKey] = value;
|
||||||
|
|
||||||
|
if (settingKey === 'lora_syntax_format') {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('lm:lora-syntax-format-changed', Date.now().toString());
|
||||||
|
} catch (_) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isBackendSetting(settingKey)) {
|
if (!this.isBackendSetting(settingKey)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -949,6 +956,12 @@ export class SettingsManager {
|
|||||||
includeTriggerWordsCheckbox.checked = state.global.settings.include_trigger_words || false;
|
includeTriggerWordsCheckbox.checked = state.global.settings.include_trigger_words || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set lora syntax format
|
||||||
|
const loraSyntaxFormatSelect = document.getElementById('loraSyntaxFormat');
|
||||||
|
if (loraSyntaxFormatSelect) {
|
||||||
|
loraSyntaxFormatSelect.value = state.global.settings.lora_syntax_format || 'legacy';
|
||||||
|
}
|
||||||
|
|
||||||
// Load metadata archive settings
|
// Load metadata archive settings
|
||||||
await this.loadMetadataArchiveSettings();
|
await this.loadMetadataArchiveSettings();
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
|
|||||||
card_info_display: 'always',
|
card_info_display: 'always',
|
||||||
show_folder_sidebar: true,
|
show_folder_sidebar: true,
|
||||||
model_name_display: 'model_name',
|
model_name_display: 'model_name',
|
||||||
|
lora_syntax_format: 'legacy',
|
||||||
model_card_footer_action: 'example_images',
|
model_card_footer_action: 'example_images',
|
||||||
show_version_on_card: true,
|
show_version_on_card: true,
|
||||||
include_trigger_words: false,
|
include_trigger_words: false,
|
||||||
|
|||||||
@@ -420,17 +420,23 @@ export function getLoraStrengthsFromUsageTips(usageTips = {}) {
|
|||||||
export function buildLoraSyntax(fileName, usageTips = {}) {
|
export function buildLoraSyntax(fileName, usageTips = {}) {
|
||||||
const { strength, hasStrength, clipStrength, hasClipStrength } = getLoraStrengthsFromUsageTips(usageTips);
|
const { strength, hasStrength, clipStrength, hasClipStrength } = getLoraStrengthsFromUsageTips(usageTips);
|
||||||
|
|
||||||
|
const effectiveName = state.global.settings?.lora_syntax_format === 'legacy'
|
||||||
|
? fileName.split('/').pop()
|
||||||
|
: fileName;
|
||||||
|
|
||||||
if (hasClipStrength) {
|
if (hasClipStrength) {
|
||||||
const modelStrength = hasStrength ? strength : 1;
|
const modelStrength = hasStrength ? strength : 1;
|
||||||
return `<lora:${fileName}:${modelStrength}:${clipStrength}>`;
|
return `<lora:${effectiveName}:${modelStrength}:${clipStrength}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<lora:${fileName}:${strength}>`;
|
return `<lora:${effectiveName}:${strength}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyLoraSyntax(card) {
|
export function copyLoraSyntax(card) {
|
||||||
const usageTips = JSON.parse(card.dataset.usage_tips || "{}");
|
const usageTips = JSON.parse(card.dataset.usage_tips || "{}");
|
||||||
const baseSyntax = buildLoraSyntax(card.dataset.file_name, usageTips);
|
const folder = card.dataset.folder || '';
|
||||||
|
const loraName = folder ? `${folder}/${card.dataset.file_name}` : card.dataset.file_name;
|
||||||
|
const baseSyntax = buildLoraSyntax(loraName, usageTips);
|
||||||
|
|
||||||
// Check if trigger words should be included
|
// Check if trigger words should be included
|
||||||
const includeTriggerWords = state.global.settings.include_trigger_words;
|
const includeTriggerWords = state.global.settings.include_trigger_words;
|
||||||
|
|||||||
@@ -218,10 +218,10 @@
|
|||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
<h4>{{ t('header.filter.license') }}</h4>
|
<h4>{{ t('header.filter.license') }}</h4>
|
||||||
<div class="filter-tags">
|
<div class="filter-tags">
|
||||||
<div class="filter-tag license-tag" data-license="noCredit">
|
<div class="filter-tag license-tag" data-license="noCredit" title="{{ t('header.filter.noCreditRequiredTooltip') }}">
|
||||||
{{ t('header.filter.noCreditRequired') }}
|
{{ t('header.filter.noCreditRequired') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-tag license-tag" data-license="allowSelling">
|
<div class="filter-tag license-tag" data-license="allowSelling" title="{{ t('header.filter.allowSellingGeneratedContentTooltip') }}">
|
||||||
{{ t('header.filter.allowSellingGeneratedContent') }}
|
{{ t('header.filter.allowSellingGeneratedContent') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -109,3 +109,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Resolve Filename Conflicts Confirmation Modal -->
|
||||||
|
<div id="resolveFilenameConflictsModal" class="modal delete-modal">
|
||||||
|
<div class="modal-content delete-modal-content">
|
||||||
|
<h2>{{ t('conflictConfirm.title') }}</h2>
|
||||||
|
<p class="confirmation-message">{{ t('conflictConfirm.message') }}</p>
|
||||||
|
<p class="resolve-conflicts-detail" id="resolveConflictsDetail"></p>
|
||||||
|
<div class="resolve-conflicts-impact" id="resolveConflictsImpact"></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="cancel-btn" onclick="modalManager.closeModal('resolveFilenameConflictsModal')">{{ t('common.actions.cancel') }}</button>
|
||||||
|
<button class="primary-btn" id="resolveConflictsConfirmBtn" onclick="doctorManager.confirmResolveConflicts()">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
{{ t('conflictConfirm.confirm') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -595,6 +595,22 @@
|
|||||||
<div class="settings-subsection-header">
|
<div class="settings-subsection-header">
|
||||||
<h4>{{ t('settings.sections.misc') }}</h4>
|
<h4>{{ t('settings.sections.misc') }}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info">
|
||||||
|
<label for="loraSyntaxFormat">
|
||||||
|
{{ t('settings.misc.loraSyntaxFormat') }}
|
||||||
|
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.misc.loraSyntaxFormatHelp') }}"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control select-control">
|
||||||
|
<select id="loraSyntaxFormat" onchange="settingsManager.saveSelectSetting('loraSyntaxFormat', 'lora_syntax_format')">
|
||||||
|
<option value="full">{{ t('settings.misc.loraSyntaxFormatOptions.full') }}</option>
|
||||||
|
<option value="legacy">{{ t('settings.misc.loraSyntaxFormatOptions.legacy') }}</option>
|
||||||
|
</select>
|
||||||
|
</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">
|
||||||
|
|||||||
@@ -65,32 +65,26 @@ async def test_allow_selling_filter():
|
|||||||
"""Test the allow selling generated content filtering logic."""
|
"""Test the allow selling generated content filtering logic."""
|
||||||
service = DummyModelService()
|
service = DummyModelService()
|
||||||
|
|
||||||
# Create test data with different license flags
|
# CommercialUse values are independent — Sell does NOT imply Image.
|
||||||
test_data = [
|
test_data = [
|
||||||
# Model allowing selling (contains Image in allowCommercialUse)
|
|
||||||
{"file_path": "model1.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["Image"]})},
|
{"file_path": "model1.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["Image"]})},
|
||||||
# Model not allowing selling (doesn't contain Image in allowCommercialUse)
|
|
||||||
{"file_path": "model2.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["RentCivit"]})},
|
{"file_path": "model2.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["RentCivit"]})},
|
||||||
# Model with default license flags (includes Sell by default, which implies Image)
|
|
||||||
{"file_path": "model3.safetensors", "license_flags": build_license_flags(None)},
|
{"file_path": "model3.safetensors", "license_flags": build_license_flags(None)},
|
||||||
# Model allowing selling (contains Sell in allowCommercialUse, which implies Image)
|
|
||||||
{"file_path": "model4.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["Sell"]})},
|
{"file_path": "model4.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["Sell"]})},
|
||||||
# Model with empty allowCommercialUse (doesn't allow selling)
|
|
||||||
{"file_path": "model5.safetensors", "license_flags": build_license_flags({"allowCommercialUse": []})},
|
{"file_path": "model5.safetensors", "license_flags": build_license_flags({"allowCommercialUse": []})},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Test allow_selling=True (should return models that allow selling - have Image permission)
|
# Test allow_selling=True (should return only models with the Image permission)
|
||||||
# Default and Sell permissions both include Image, so model3 and model4 will be included
|
|
||||||
filtered = await service._apply_allow_selling_filter(test_data, allow_selling=True)
|
filtered = await service._apply_allow_selling_filter(test_data, allow_selling=True)
|
||||||
assert len(filtered) == 3 # model1, model3 (default includes Sell which implies Image), model4
|
assert len(filtered) == 1 # only model1 has Image permission
|
||||||
file_paths = {item["file_path"] for item in filtered}
|
file_paths = {item["file_path"] for item in filtered}
|
||||||
assert file_paths == {"model1.safetensors", "model3.safetensors", "model4.safetensors"}
|
assert file_paths == {"model1.safetensors"}
|
||||||
|
|
||||||
# Test allow_selling=False (should return models that don't allow selling - don't have Image permission)
|
# Test allow_selling=False (should return models without the Image permission)
|
||||||
filtered = await service._apply_allow_selling_filter(test_data, allow_selling=False)
|
filtered = await service._apply_allow_selling_filter(test_data, allow_selling=False)
|
||||||
assert len(filtered) == 2 # model2 and model5
|
assert len(filtered) == 4 # model2, model3, model4, model5
|
||||||
file_paths = {item["file_path"] for item in filtered}
|
file_paths = {item["file_path"] for item in filtered}
|
||||||
assert file_paths == {"model2.safetensors", "model5.safetensors"}
|
assert file_paths == {"model2.safetensors", "model3.safetensors", "model4.safetensors", "model5.safetensors"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -131,13 +131,12 @@ async def test_pool_filter_allow_selling_true(lora_service, sample_loras):
|
|||||||
filtered = await lora_service._apply_pool_filters(sample_loras, pool_config)
|
filtered = await lora_service._apply_pool_filters(sample_loras, pool_config)
|
||||||
|
|
||||||
# Should keep models with Image permission (allowSelling)
|
# Should keep models with Image permission (allowSelling)
|
||||||
# Models: no_credit_required_for_selling, credit_required_for_selling, default_license
|
# Sell alone does not imply Image, so default_license is excluded.
|
||||||
assert len(filtered) == 3
|
assert len(filtered) == 2
|
||||||
file_names = {lora["file_name"] for lora in filtered}
|
file_names = {lora["file_name"] for lora in filtered}
|
||||||
assert file_names == {
|
assert file_names == {
|
||||||
"no_credit_required_for_selling.safetensors",
|
"no_credit_required_for_selling.safetensors",
|
||||||
"credit_required_for_selling.safetensors",
|
"credit_required_for_selling.safetensors",
|
||||||
"default_license.safetensors",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -178,12 +177,11 @@ async def test_pool_filter_both_license_filters(lora_service, sample_loras):
|
|||||||
# Should keep models where both conditions are met:
|
# Should keep models where both conditions are met:
|
||||||
# - allowNoCredit=True (no credit required)
|
# - allowNoCredit=True (no credit required)
|
||||||
# - Image permission exists (allow selling)
|
# - Image permission exists (allow selling)
|
||||||
# Models: no_credit_required_for_selling, default_license
|
# default_license has ["Sell"] without Image, so it's excluded.
|
||||||
assert len(filtered) == 2
|
assert len(filtered) == 1
|
||||||
file_names = {lora["file_name"] for lora in filtered}
|
file_names = {lora["file_name"] for lora in filtered}
|
||||||
assert file_names == {
|
assert file_names == {
|
||||||
"no_credit_required_for_selling.safetensors",
|
"no_credit_required_for_selling.safetensors",
|
||||||
"default_license.safetensors",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,8 @@ async def test_initialize_cache_populates_cache(tmp_path: Path):
|
|||||||
_normalize_path(tmp_path / "one.txt"),
|
_normalize_path(tmp_path / "one.txt"),
|
||||||
_normalize_path(tmp_path / "nested" / "two.txt"),
|
_normalize_path(tmp_path / "nested" / "two.txt"),
|
||||||
}
|
}
|
||||||
assert {item["license_flags"] for item in cache.raw_data} == {DEFAULT_LICENSE_FLAGS}
|
# build_license_flags({}) returns 113 (defaults: allowNoCredit + ["Sell"] + derivatives + differentLicense)
|
||||||
|
assert {item["license_flags"] for item in cache.raw_data} == {113}
|
||||||
|
|
||||||
assert scanner._hash_index.get_path("hash-one") == _normalize_path(tmp_path / "one.txt")
|
assert scanner._hash_index.get_path("hash-one") == _normalize_path(tmp_path / "one.txt")
|
||||||
assert scanner._hash_index.get_path("hash-two") == _normalize_path(tmp_path / "nested" / "two.txt")
|
assert scanner._hash_index.get_path("hash-two") == _normalize_path(tmp_path / "nested" / "two.txt")
|
||||||
@@ -190,7 +191,8 @@ async def test_initialize_in_background_applies_scan_result(tmp_path: Path, monk
|
|||||||
_normalize_path(tmp_path / "one.txt"),
|
_normalize_path(tmp_path / "one.txt"),
|
||||||
_normalize_path(tmp_path / "nested" / "two.txt"),
|
_normalize_path(tmp_path / "nested" / "two.txt"),
|
||||||
}
|
}
|
||||||
assert {item["license_flags"] for item in cache.raw_data} == {DEFAULT_LICENSE_FLAGS}
|
# build_license_flags({}) returns 113 (defaults: allowNoCredit + ["Sell"] + derivatives + differentLicense)
|
||||||
|
assert {item["license_flags"] for item in cache.raw_data} == {113}
|
||||||
assert scanner._hash_index.get_path("hash-two") == _normalize_path(tmp_path / "nested" / "two.txt")
|
assert scanner._hash_index.get_path("hash-two") == _normalize_path(tmp_path / "nested" / "two.txt")
|
||||||
assert scanner._tags_count == {"alpha": 1, "beta": 1}
|
assert scanner._tags_count == {"alpha": 1, "beta": 1}
|
||||||
assert scanner._excluded_models == [_normalize_path(tmp_path / "skip-file.txt")]
|
assert scanner._excluded_models == [_normalize_path(tmp_path / "skip-file.txt")]
|
||||||
@@ -636,6 +638,8 @@ async def test_log_duplicate_filename_summary_logs_warning(tmp_path: Path, caplo
|
|||||||
root = tmp_path / "loras"
|
root = tmp_path / "loras"
|
||||||
root.mkdir()
|
root.mkdir()
|
||||||
scanner = DummyScanner(root)
|
scanner = DummyScanner(root)
|
||||||
|
# Duplicate filename detection is only active for LoRAs
|
||||||
|
scanner.model_type = "lora"
|
||||||
|
|
||||||
# Simulate duplicate filenames in the hash index
|
# Simulate duplicate filenames in the hash index
|
||||||
scanner._hash_index.add_entry("aaa111", str(root / "model.safetensors"))
|
scanner._hash_index.add_entry("aaa111", str(root / "model.safetensors"))
|
||||||
@@ -646,7 +650,7 @@ async def test_log_duplicate_filename_summary_logs_warning(tmp_path: Path, caplo
|
|||||||
assert len(caplog.records) >= 1
|
assert len(caplog.records) >= 1
|
||||||
log_msg = caplog.records[-1].message
|
log_msg = caplog.records[-1].message
|
||||||
assert "Duplicate filename conflict detected" in log_msg
|
assert "Duplicate filename conflict detected" in log_msg
|
||||||
assert "1 dummy filename(s)" in log_msg
|
assert "1 lora filename(s)" in log_msg
|
||||||
assert "2 files total" in log_msg
|
assert "2 files total" in log_msg
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ def test_tag_update_service_adds_unique_tags(tmp_path: Path) -> None:
|
|||||||
cache_updates.append(metadata)
|
cache_updates.append(metadata)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
tags = asyncio.run(
|
tags, auto_tags = asyncio.run(
|
||||||
service.add_tags(
|
service.add_tags(
|
||||||
file_path=str(tmp_path / "model.safetensors"),
|
file_path=str(tmp_path / "model.safetensors"),
|
||||||
new_tags=["new", "existing"],
|
new_tags=["new", "existing"],
|
||||||
@@ -265,5 +265,6 @@ def test_tag_update_service_adds_unique_tags(tmp_path: Path) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert tags == ["existing", "new"]
|
assert tags == ["existing", "new"]
|
||||||
|
assert auto_tags == []
|
||||||
assert manager.saved
|
assert manager.saved
|
||||||
assert cache_updates
|
assert cache_updates
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ async def test_tag_update_service_handles_case_insensitive_tags(tmp_path: Path)
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Try to add "Test" (different case) - should not be added since "test" already exists
|
# Try to add "Test" (different case) - should not be added since "test" already exists
|
||||||
tags = await service.add_tags(
|
tags, auto_tags = await service.add_tags(
|
||||||
file_path=str(tmp_path / "model.safetensors"),
|
file_path=str(tmp_path / "model.safetensors"),
|
||||||
new_tags=["Test"],
|
new_tags=["Test"],
|
||||||
metadata_loader=loader,
|
metadata_loader=loader,
|
||||||
@@ -52,6 +52,7 @@ async def test_tag_update_service_handles_case_insensitive_tags(tmp_path: Path)
|
|||||||
|
|
||||||
# Should still only have "test" (lowercase) in the tags
|
# Should still only have "test" (lowercase) in the tags
|
||||||
assert tags == ["test"]
|
assert tags == ["test"]
|
||||||
|
assert auto_tags == [] # no file_name/base_model in metadata, so no auto-detection
|
||||||
assert len(manager.saved) == 1
|
assert len(manager.saved) == 1
|
||||||
saved_metadata = manager.saved[0][1]
|
saved_metadata = manager.saved[0][1]
|
||||||
assert saved_metadata["tags"] == ["test"]
|
assert saved_metadata["tags"] == ["test"]
|
||||||
@@ -76,7 +77,7 @@ async def test_tag_update_service_adds_new_tags_in_lowercase(tmp_path: Path) ->
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Add new tags with mixed case
|
# Add new tags with mixed case
|
||||||
tags = await service.add_tags(
|
tags, auto_tags = await service.add_tags(
|
||||||
file_path=str(tmp_path / "model.safetensors"),
|
file_path=str(tmp_path / "model.safetensors"),
|
||||||
new_tags=["NewTag", "ANOTHER_TAG"],
|
new_tags=["NewTag", "ANOTHER_TAG"],
|
||||||
metadata_loader=loader,
|
metadata_loader=loader,
|
||||||
@@ -87,6 +88,7 @@ async def test_tag_update_service_adds_new_tags_in_lowercase(tmp_path: Path) ->
|
|||||||
assert "existing" in tags
|
assert "existing" in tags
|
||||||
assert "newtag" in tags
|
assert "newtag" in tags
|
||||||
assert "another_tag" in tags
|
assert "another_tag" in tags
|
||||||
|
assert auto_tags == []
|
||||||
assert len(manager.saved) == 1
|
assert len(manager.saved) == 1
|
||||||
saved_metadata = manager.saved[0][1]
|
saved_metadata = manager.saved[0][1]
|
||||||
assert "newtag" in saved_metadata["tags"]
|
assert "newtag" in saved_metadata["tags"]
|
||||||
|
|||||||
@@ -126,6 +126,80 @@ class TestExtractAutoTags:
|
|||||||
})
|
})
|
||||||
assert set(result) == {"HIGH", "I2V"}
|
assert set(result) == {"HIGH", "I2V"}
|
||||||
|
|
||||||
|
# ── Layer 2: user-defined tags as manual fallback ───────────
|
||||||
|
|
||||||
|
def test_user_tags_fallback_when_detection_fails(self):
|
||||||
|
result = extract_auto_tags({
|
||||||
|
"file_name": "BOTH-v1.0",
|
||||||
|
"base_model": "Wan 2.2",
|
||||||
|
"civitai": {},
|
||||||
|
"tags": ["HIGH", "I2V", "T2V"],
|
||||||
|
})
|
||||||
|
assert set(result) == {"HIGH", "I2V", "T2V"}
|
||||||
|
|
||||||
|
def test_user_tags_augment_partial_detection(self):
|
||||||
|
result = extract_auto_tags({
|
||||||
|
"file_name": "wan_i2v_hn_v2",
|
||||||
|
"base_model": "Wan 2.2 I2V",
|
||||||
|
"civitai": {},
|
||||||
|
"tags": ["HIGH"],
|
||||||
|
})
|
||||||
|
assert set(result) == {"HIGH", "I2V"}
|
||||||
|
|
||||||
|
def test_user_tags_non_auto_tag_ignored(self):
|
||||||
|
result = extract_auto_tags({
|
||||||
|
"file_name": "model_v1",
|
||||||
|
"base_model": "Wan 2.2",
|
||||||
|
"civitai": {},
|
||||||
|
"tags": ["HIGH", "character", "style", "nsfw"],
|
||||||
|
})
|
||||||
|
assert set(result) == {"HIGH"}
|
||||||
|
|
||||||
|
def test_user_tags_overrides_non_wan_gate(self):
|
||||||
|
result = extract_auto_tags({
|
||||||
|
"file_name": "flux_model_v1",
|
||||||
|
"base_model": "Flux.1 D",
|
||||||
|
"civitai": {},
|
||||||
|
"tags": ["HIGH", "LOW", "Turbo"],
|
||||||
|
})
|
||||||
|
assert set(result) == {"HIGH", "LOW", "Turbo"}
|
||||||
|
|
||||||
|
def test_user_tags_no_duplication(self):
|
||||||
|
result = extract_auto_tags({
|
||||||
|
"file_name": "wan_i2v_high_v3",
|
||||||
|
"base_model": "Wan 2.2",
|
||||||
|
"civitai": {},
|
||||||
|
"tags": ["HIGH", "I2V"],
|
||||||
|
})
|
||||||
|
assert set(result) == {"HIGH", "I2V"}
|
||||||
|
|
||||||
|
def test_user_tags_lightning_turbo_manual(self):
|
||||||
|
result = extract_auto_tags({
|
||||||
|
"file_name": "sdxl_model_v1",
|
||||||
|
"base_model": "SDXL",
|
||||||
|
"civitai": {},
|
||||||
|
"tags": ["Lightning"],
|
||||||
|
})
|
||||||
|
assert set(result) == {"Lightning"}
|
||||||
|
|
||||||
|
def test_user_tags_case_insensitive_lowercase(self):
|
||||||
|
result = extract_auto_tags({
|
||||||
|
"file_name": "wan_masterpieces_v2",
|
||||||
|
"base_model": "Wan Video 14B t2v",
|
||||||
|
"civitai": {},
|
||||||
|
"tags": ["high"],
|
||||||
|
})
|
||||||
|
assert set(result) == {"HIGH", "T2V"}
|
||||||
|
|
||||||
|
def test_user_tags_case_insensitive_mixed(self):
|
||||||
|
result = extract_auto_tags({
|
||||||
|
"file_name": "model_v1",
|
||||||
|
"base_model": "SDXL",
|
||||||
|
"civitai": {},
|
||||||
|
"tags": ["lightning", "turbo", "i2v"],
|
||||||
|
})
|
||||||
|
assert set(result) == {"Lightning", "Turbo", "I2V"}
|
||||||
|
|
||||||
|
|
||||||
class TestAutoTagCategories:
|
class TestAutoTagCategories:
|
||||||
def test_all_patterns_compile(self):
|
def test_all_patterns_compile(self):
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ def test_resolve_license_payload_defaults():
|
|||||||
assert payload["allowDerivatives"] is True
|
assert payload["allowDerivatives"] is True
|
||||||
assert payload["allowDifferentLicense"] is True
|
assert payload["allowDifferentLicense"] is True
|
||||||
assert payload["allowCommercialUse"] == ["Sell"]
|
assert payload["allowCommercialUse"] == ["Sell"]
|
||||||
assert flags == 127
|
# Default ["Sell"] only sets the Sell bit (16), plus NoCredit (1),
|
||||||
|
# Derivatives (32) and DifferentLicense (64) = 113.
|
||||||
|
assert flags == 113
|
||||||
|
|
||||||
|
|
||||||
def test_build_license_flags_custom_values():
|
def test_build_license_flags_custom_values():
|
||||||
@@ -34,11 +36,10 @@ def test_build_license_flags_custom_values():
|
|||||||
assert payload["allowDifferentLicense"] is False
|
assert payload["allowDifferentLicense"] is False
|
||||||
|
|
||||||
flags = build_license_flags(source)
|
flags = build_license_flags(source)
|
||||||
# Sell automatically enables all commercial bits including image.
|
assert flags == 18
|
||||||
assert flags == 30
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_license_flags_respects_commercial_hierarchy():
|
def test_build_license_flags_independent_values():
|
||||||
base = {
|
base = {
|
||||||
"allowNoCredit": False,
|
"allowNoCredit": False,
|
||||||
"allowDerivatives": False,
|
"allowDerivatives": False,
|
||||||
@@ -46,14 +47,10 @@ def test_build_license_flags_respects_commercial_hierarchy():
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert build_license_flags({**base, "allowCommercialUse": []}) == 0
|
assert build_license_flags({**base, "allowCommercialUse": []}) == 0
|
||||||
# Rent adds rent and rentcivit permissions.
|
assert build_license_flags({**base, "allowCommercialUse": ["Rent"]}) == 8
|
||||||
assert build_license_flags({**base, "allowCommercialUse": ["Rent"]}) == 12
|
|
||||||
# RentCivit alone should only set its own bit.
|
|
||||||
assert build_license_flags({**base, "allowCommercialUse": ["RentCivit"]}) == 4
|
assert build_license_flags({**base, "allowCommercialUse": ["RentCivit"]}) == 4
|
||||||
# Image only toggles the image bit.
|
|
||||||
assert build_license_flags({**base, "allowCommercialUse": ["Image"]}) == 2
|
assert build_license_flags({**base, "allowCommercialUse": ["Image"]}) == 2
|
||||||
# Sell forces all commercial bits regardless of image listing.
|
assert build_license_flags({**base, "allowCommercialUse": ["Sell"]}) == 16
|
||||||
assert build_license_flags({**base, "allowCommercialUse": ["Sell"]}) == 30
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_license_flags_parses_aggregate_string():
|
def test_build_license_flags_parses_aggregate_string():
|
||||||
|
|||||||
@@ -1,13 +1,43 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from py.services.settings_manager import SettingsManager, get_settings_manager
|
from py.services.settings_manager import SettingsManager, get_settings_manager
|
||||||
|
from py.services.service_registry import ServiceRegistry
|
||||||
from py.utils.utils import (
|
from py.utils.utils import (
|
||||||
calculate_recipe_fingerprint,
|
calculate_recipe_fingerprint,
|
||||||
calculate_relative_path_for_model,
|
calculate_relative_path_for_model,
|
||||||
|
get_lora_info,
|
||||||
|
get_lora_info_absolute,
|
||||||
sanitize_folder_name,
|
sanitize_folder_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeCache:
|
||||||
|
def __init__(self, items):
|
||||||
|
self.raw_data = list(items)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeScanner:
|
||||||
|
def __init__(self, items):
|
||||||
|
self._cache = _FakeCache(items)
|
||||||
|
|
||||||
|
async def get_cached_data(self):
|
||||||
|
return self._cache
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_lora_scanner(monkeypatch):
|
||||||
|
def _setup(items):
|
||||||
|
scanner = _FakeScanner(items)
|
||||||
|
|
||||||
|
async def get_scanner():
|
||||||
|
return scanner
|
||||||
|
|
||||||
|
monkeypatch.setattr(ServiceRegistry, "get_lora_scanner", get_scanner)
|
||||||
|
return scanner
|
||||||
|
|
||||||
|
return _setup
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def isolated_settings(monkeypatch):
|
def isolated_settings(monkeypatch):
|
||||||
manager = get_settings_manager()
|
manager = get_settings_manager()
|
||||||
@@ -114,3 +144,114 @@ def test_calculate_recipe_fingerprint_empty_input():
|
|||||||
)
|
)
|
||||||
def test_sanitize_folder_name(original, expected):
|
def test_sanitize_folder_name(original, expected):
|
||||||
assert sanitize_folder_name(original) == expected
|
assert sanitize_folder_name(original) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lora_info_absolute_bare_name(mock_lora_scanner):
|
||||||
|
mock_lora_scanner([
|
||||||
|
{"file_name": "mylora", "folder": "SDXL", "file_path": "/models/Lora/SDXL/mylora.safetensors", "civitai": {"trainedWords": ["trigger1"]}},
|
||||||
|
])
|
||||||
|
|
||||||
|
path, triggers = get_lora_info_absolute("mylora")
|
||||||
|
|
||||||
|
assert path == "/models/Lora/SDXL/mylora.safetensors"
|
||||||
|
assert triggers == ["trigger1"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lora_info_absolute_with_path(mock_lora_scanner):
|
||||||
|
mock_lora_scanner([
|
||||||
|
{"file_name": "mylora", "folder": "SDXL/Styles", "file_path": "/models/Lora/SDXL/Styles/mylora.safetensors", "civitai": {"trainedWords": ["artistic"]}},
|
||||||
|
{"file_name": "other", "folder": "", "file_path": "/models/Lora/other.safetensors", "civitai": {}},
|
||||||
|
])
|
||||||
|
|
||||||
|
path, triggers = get_lora_info_absolute("SDXL/Styles/mylora")
|
||||||
|
|
||||||
|
assert path == "/models/Lora/SDXL/Styles/mylora.safetensors"
|
||||||
|
assert triggers == ["artistic"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lora_info_absolute_path_fallback_to_basename(mock_lora_scanner):
|
||||||
|
mock_lora_scanner([
|
||||||
|
{"file_name": "mylora", "folder": "RenamedFolder", "file_path": "/models/Lora/RenamedFolder/mylora.safetensors", "civitai": {"trainedWords": ["trigger1"]}},
|
||||||
|
])
|
||||||
|
|
||||||
|
path, triggers = get_lora_info_absolute("OldFolder/mylora")
|
||||||
|
|
||||||
|
assert path == "/models/Lora/RenamedFolder/mylora.safetensors"
|
||||||
|
assert triggers == ["trigger1"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lora_info_absolute_prefers_folder_match(mock_lora_scanner):
|
||||||
|
mock_lora_scanner([
|
||||||
|
{"file_name": "mylora", "folder": "V1", "file_path": "/models/Lora/V1/mylora.safetensors", "civitai": {"trainedWords": ["v1"]}},
|
||||||
|
{"file_name": "mylora", "folder": "V2", "file_path": "/models/Lora/V2/mylora.safetensors", "civitai": {"trainedWords": ["v2"]}},
|
||||||
|
])
|
||||||
|
|
||||||
|
path, triggers = get_lora_info_absolute("V2/mylora")
|
||||||
|
|
||||||
|
assert path == "/models/Lora/V2/mylora.safetensors"
|
||||||
|
assert triggers == ["v2"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lora_info_absolute_no_folder_in_cache_no_path_in_name(mock_lora_scanner):
|
||||||
|
mock_lora_scanner([
|
||||||
|
{"file_name": "mylora", "folder": "", "file_path": "/models/Lora/mylora.safetensors", "civitai": {}},
|
||||||
|
])
|
||||||
|
|
||||||
|
path, triggers = get_lora_info_absolute("mylora")
|
||||||
|
|
||||||
|
assert path == "/models/Lora/mylora.safetensors"
|
||||||
|
assert triggers == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lora_info_absolute_strips_extension(mock_lora_scanner):
|
||||||
|
mock_lora_scanner([
|
||||||
|
{"file_name": "mylora", "folder": "SDXL", "file_path": "/models/Lora/SDXL/mylora.safetensors", "civitai": {"trainedWords": ["hello"]}},
|
||||||
|
])
|
||||||
|
|
||||||
|
path, triggers = get_lora_info_absolute("SDXL/mylora.safetensors")
|
||||||
|
|
||||||
|
assert path == "/models/Lora/SDXL/mylora.safetensors"
|
||||||
|
assert triggers == ["hello"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lora_info_absolute_not_found_returns_original(mock_lora_scanner):
|
||||||
|
mock_lora_scanner([
|
||||||
|
{"file_name": "mylora", "folder": "SDXL", "file_path": "/models/Lora/SDXL/mylora.safetensors", "civitai": {}},
|
||||||
|
])
|
||||||
|
|
||||||
|
path, triggers = get_lora_info_absolute("nonexistent")
|
||||||
|
|
||||||
|
assert path == "nonexistent"
|
||||||
|
assert triggers == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lora_info_bare_name(mock_lora_scanner):
|
||||||
|
mock_lora_scanner([
|
||||||
|
{"file_name": "mylora", "folder": "SDXL", "file_path": "/models/Lora/SDXL/mylora.safetensors", "civitai": {"trainedWords": ["trigger1"]}},
|
||||||
|
])
|
||||||
|
|
||||||
|
path, triggers = get_lora_info("mylora")
|
||||||
|
|
||||||
|
assert triggers == ["trigger1"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lora_info_with_path(mock_lora_scanner):
|
||||||
|
mock_lora_scanner([
|
||||||
|
{"file_name": "mylora", "folder": "SDXL/Styles", "file_path": "/models/Lora/SDXL/Styles/mylora.safetensors", "civitai": {"trainedWords": ["artistic"]}},
|
||||||
|
{"file_name": "other", "folder": "", "file_path": "/models/Lora/other.safetensors", "civitai": {}},
|
||||||
|
])
|
||||||
|
|
||||||
|
path, triggers = get_lora_info("SDXL/Styles/mylora")
|
||||||
|
|
||||||
|
assert triggers == ["artistic"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lora_info_not_found_returns_original(mock_lora_scanner):
|
||||||
|
mock_lora_scanner([
|
||||||
|
{"file_name": "mylora", "folder": "SDXL", "file_path": "/models/Lora/SDXL/mylora.safetensors", "civitai": {}},
|
||||||
|
])
|
||||||
|
|
||||||
|
path, triggers = get_lora_info("nonexistent")
|
||||||
|
|
||||||
|
assert path == "nonexistent"
|
||||||
|
assert triggers == []
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="section__toggles">
|
<div class="section__toggles">
|
||||||
<label class="toggle-item">
|
<label class="toggle-item">
|
||||||
<span class="toggle-item__label">No Credit Required</span>
|
<span class="toggle-item__label" title="Use the model without crediting the creator">No Credit Required</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="toggle-switch"
|
class="toggle-switch"
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="toggle-item">
|
<label class="toggle-item">
|
||||||
<span class="toggle-item__label">Allow Selling</span>
|
<span class="toggle-item__label" title="Allow selling generated images">Allow Selling</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="toggle-switch"
|
class="toggle-switch"
|
||||||
|
|||||||
@@ -104,6 +104,66 @@ function removeLoraExtension(fileName = '') {
|
|||||||
return fileName.replace(/\.(safetensors|ckpt|pt|bin)$/i, '');
|
return fileName.replace(/\.(safetensors|ckpt|pt|bin)$/i, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _loraSyntaxFormatCache = null;
|
||||||
|
let _loraSyntaxFormatRefreshPromise = null;
|
||||||
|
|
||||||
|
function _getLoraSyntaxFormat() {
|
||||||
|
if (_loraSyntaxFormatCache !== null) {
|
||||||
|
return _loraSyntaxFormatCache;
|
||||||
|
}
|
||||||
|
return 'legacy';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _fetchLoraSyntaxFormat() {
|
||||||
|
try {
|
||||||
|
const response = await api.fetchApi('/lm/settings');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && data.settings) {
|
||||||
|
_loraSyntaxFormatCache = data.settings.lora_syntax_format || 'legacy';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
if (_loraSyntaxFormatCache === null) {
|
||||||
|
_loraSyntaxFormatCache = 'legacy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _triggerBackgroundRefresh() {
|
||||||
|
if (_loraSyntaxFormatRefreshPromise) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_loraSyntaxFormatRefreshPromise = _fetchLoraSyntaxFormat().finally(() => {
|
||||||
|
_loraSyntaxFormatRefreshPromise = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLoraSyntaxFormat() {
|
||||||
|
await _fetchLoraSyntaxFormat();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initLoraSyntaxFormat() {
|
||||||
|
_triggerBackgroundRefresh();
|
||||||
|
}
|
||||||
|
_initLoraSyntaxFormat();
|
||||||
|
|
||||||
|
function _initLoraSyntaxFormatReactive() {
|
||||||
|
window.addEventListener('storage', (e) => {
|
||||||
|
if (e.key === 'lm:lora-syntax-format-changed') {
|
||||||
|
_triggerBackgroundRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
_triggerBackgroundRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_initLoraSyntaxFormatReactive();
|
||||||
|
|
||||||
function parseSearchTokens(term = '') {
|
function parseSearchTokens(term = '') {
|
||||||
const include = [];
|
const include = [];
|
||||||
const exclude = [];
|
const exclude = [];
|
||||||
@@ -226,7 +286,14 @@ const MODEL_BEHAVIORS = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getInsertText(_instance, relativePath) {
|
async getInsertText(_instance, relativePath) {
|
||||||
const fileName = removeLoraExtension(splitRelativePath(relativePath).fileName);
|
const { directories, fileName } = splitRelativePath(relativePath);
|
||||||
|
const baseName = removeLoraExtension(fileName);
|
||||||
|
const folder = directories.length ? directories.join('/') + '/' : '';
|
||||||
|
const loraName = folder + baseName;
|
||||||
|
|
||||||
|
const resultName = _getLoraSyntaxFormat() === 'legacy'
|
||||||
|
? baseName
|
||||||
|
: loraName;
|
||||||
|
|
||||||
let strength = 1.0;
|
let strength = 1.0;
|
||||||
let hasStrength = false;
|
let hasStrength = false;
|
||||||
@@ -262,9 +329,9 @@ const MODEL_BEHAVIORS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (clipStrength !== null) {
|
if (clipStrength !== null) {
|
||||||
return formatAutocompleteInsertion(`<lora:${fileName}:${strength}:${clipStrength}>`);
|
return formatAutocompleteInsertion(`<lora:${resultName}:${strength}:${clipStrength}>`);
|
||||||
}
|
}
|
||||||
return formatAutocompleteInsertion(`<lora:${fileName}:${strength}>`);
|
return formatAutocompleteInsertion(`<lora:${resultName}:${strength}>`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
embeddings: {
|
embeddings: {
|
||||||
@@ -1430,6 +1497,11 @@ class AutoComplete {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Prevent textarea from losing focus - same fix as createItemElement
|
||||||
|
itemEl.addEventListener('mousedown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
itemEl.addEventListener('mouseenter', () => {
|
itemEl.addEventListener('mouseenter', () => {
|
||||||
this.selectItem(index, { manual: true });
|
this.selectItem(index, { manual: true });
|
||||||
});
|
});
|
||||||
@@ -2158,6 +2230,16 @@ class AutoComplete {
|
|||||||
item.appendChild(nameSpan);
|
item.appendChild(nameSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent textarea from losing focus when clicking dropdown items.
|
||||||
|
// Without this, the blur event fires before click, and the blur handler's
|
||||||
|
// formatAutocompleteTextOnBlur() modifies the text and triggers hide()
|
||||||
|
// via suppressAutocompleteOnce, removing this item from the DOM before
|
||||||
|
// the click handler can execute. This specifically breaks the case where
|
||||||
|
// the text has a comma not followed by a space (e.g. "<lora:X:1>,search").
|
||||||
|
item.addEventListener('mousedown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
// Hover and selection handlers
|
// Hover and selection handlers
|
||||||
item.addEventListener('mouseenter', () => {
|
item.addEventListener('mouseenter', () => {
|
||||||
this.selectItem(index, { manual: true });
|
this.selectItem(index, { manual: true });
|
||||||
@@ -2745,4 +2827,4 @@ class AutoComplete {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { AutoComplete };
|
export { AutoComplete, refreshLoraSyntaxFormat };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { forwardMiddleMouseToCanvas, forwardWheelToCanvas } from "./utils.js";
|
import { forwardMiddleMouseToCanvas, forwardWheelToCanvas } from "./utils.js";
|
||||||
|
import { copyToClipboard } from "./loras_widget_utils.js";
|
||||||
|
|
||||||
const MIN_HEIGHT = 150;
|
const MIN_HEIGHT = 150;
|
||||||
const GROUP_EDITOR_ID = "lm-trigger-group-editor";
|
const GROUP_EDITOR_ID = "lm-trigger-group-editor";
|
||||||
@@ -568,6 +569,56 @@ function toggleGroupEditor(widget, index, anchorEl) {
|
|||||||
openGroupEditor(widget, index, anchorEl);
|
openGroupEditor(widget, index, anchorEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showTagContextMenu(event, tagData, index, widget, anchorEl) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
closeGroupEditor(widget);
|
||||||
|
|
||||||
|
const existingMenu = document.querySelector('.lm-lora-context-menu');
|
||||||
|
if (existingMenu) {
|
||||||
|
existingMenu.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = document.createElement('div');
|
||||||
|
menu.className = 'lm-lora-context-menu';
|
||||||
|
menu.style.left = `${event.clientX}px`;
|
||||||
|
menu.style.top = `${event.clientY}px`;
|
||||||
|
|
||||||
|
const copyOption = document.createElement('div');
|
||||||
|
copyOption.className = 'lm-lora-menu-item';
|
||||||
|
copyOption.innerHTML = `<span class="lm-lora-menu-item-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></span>Copy`;
|
||||||
|
copyOption.addEventListener('click', () => {
|
||||||
|
menu.remove();
|
||||||
|
document.removeEventListener('click', closeMenu);
|
||||||
|
copyToClipboard(tagData.text, 'Copied to clipboard');
|
||||||
|
});
|
||||||
|
menu.appendChild(copyOption);
|
||||||
|
|
||||||
|
if (isGroupTag(tagData) && Array.isArray(tagData.items) && tagData.items.length > 1) {
|
||||||
|
const editOption = document.createElement('div');
|
||||||
|
editOption.className = 'lm-lora-menu-item';
|
||||||
|
editOption.innerHTML = `<span class="lm-lora-menu-item-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></span>Edit Group`;
|
||||||
|
editOption.addEventListener('click', () => {
|
||||||
|
menu.remove();
|
||||||
|
document.removeEventListener('click', closeMenu);
|
||||||
|
toggleGroupEditor(widget, index, anchorEl);
|
||||||
|
renderGroupEditor(widget, tagData, index);
|
||||||
|
});
|
||||||
|
menu.appendChild(editOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
|
||||||
|
const closeMenu = (e) => {
|
||||||
|
if (!menu.contains(e.target)) {
|
||||||
|
menu.remove();
|
||||||
|
document.removeEventListener('click', closeMenu);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(() => document.addEventListener('click', closeMenu), 0);
|
||||||
|
}
|
||||||
|
|
||||||
export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.02, options = {}) {
|
export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.02, options = {}) {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.className = "comfy-tags-container";
|
container.className = "comfy-tags-container";
|
||||||
@@ -618,6 +669,10 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tagEl.addEventListener("contextmenu", (e) => {
|
||||||
|
showTagContextMenu(e, tagData, index, widget, tagEl);
|
||||||
|
});
|
||||||
|
|
||||||
if (showStrengthInfo) {
|
if (showStrengthInfo) {
|
||||||
tagEl.addEventListener("wheel", (e) => {
|
tagEl.addEventListener("wheel", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -728,11 +783,13 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0
|
|||||||
};
|
};
|
||||||
|
|
||||||
editButton.addEventListener("click", openEditor);
|
editButton.addEventListener("click", openEditor);
|
||||||
groupChip.addEventListener("contextmenu", openEditor);
|
|
||||||
|
|
||||||
groupChip.appendChild(editButton);
|
groupChip.appendChild(editButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupChip.addEventListener("contextmenu", (e) => {
|
||||||
|
showTagContextMenu(e, tagData, index, widget, groupChip);
|
||||||
|
});
|
||||||
|
|
||||||
groupChip.addEventListener("click", (e) => {
|
groupChip.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (editButton && e.target === editButton) {
|
if (editButton && e.target === editButton) {
|
||||||
@@ -773,6 +830,11 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0
|
|||||||
container.removeChild(container.firstChild);
|
container.removeChild(container.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingMenu = document.querySelector('.lm-lora-context-menu');
|
||||||
|
if (existingMenu) {
|
||||||
|
existingMenu.remove();
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedTags = Array.isArray(tagsData) ? tagsData : [];
|
const normalizedTags = Array.isArray(tagsData) ? tagsData : [];
|
||||||
const showStrengthInfo = widget.allowStrengthAdjustment ?? allowStrengthAdjustment;
|
const showStrengthInfo = widget.allowStrengthAdjustment ?? allowStrengthAdjustment;
|
||||||
const groupAnchors = new Map();
|
const groupAnchors = new Map();
|
||||||
|
|||||||
@@ -398,13 +398,13 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section[data-v-dea4adf6] {
|
.section[data-v-07ddd3df] {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.section__header[data-v-dea4adf6] {
|
.section__header[data-v-07ddd3df] {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.section__title[data-v-dea4adf6] {
|
.section__title[data-v-07ddd3df] {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -412,21 +412,21 @@
|
|||||||
color: var(--fg-color, #fff);
|
color: var(--fg-color, #fff);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
.section__toggles[data-v-dea4adf6] {
|
.section__toggles[data-v-07ddd3df] {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
.toggle-item[data-v-dea4adf6] {
|
.toggle-item[data-v-07ddd3df] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.toggle-item__label[data-v-dea4adf6] {
|
.toggle-item__label[data-v-07ddd3df] {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--fg-color, #fff);
|
color: var(--fg-color, #fff);
|
||||||
}
|
}
|
||||||
.toggle-switch[data-v-dea4adf6] {
|
.toggle-switch[data-v-07ddd3df] {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@@ -435,7 +435,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.toggle-switch__track[data-v-dea4adf6] {
|
.toggle-switch__track[data-v-07ddd3df] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: var(--comfy-input-bg, #333);
|
background: var(--comfy-input-bg, #333);
|
||||||
@@ -443,11 +443,11 @@
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
.toggle-switch--active .toggle-switch__track[data-v-dea4adf6] {
|
.toggle-switch--active .toggle-switch__track[data-v-07ddd3df] {
|
||||||
background: rgba(66, 153, 225, 0.3);
|
background: rgba(66, 153, 225, 0.3);
|
||||||
border-color: rgba(66, 153, 225, 0.6);
|
border-color: rgba(66, 153, 225, 0.6);
|
||||||
}
|
}
|
||||||
.toggle-switch__thumb[data-v-dea4adf6] {
|
.toggle-switch__thumb[data-v-07ddd3df] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
@@ -458,12 +458,12 @@
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
.toggle-switch--active .toggle-switch__thumb[data-v-dea4adf6] {
|
.toggle-switch--active .toggle-switch__thumb[data-v-07ddd3df] {
|
||||||
transform: translateX(16px);
|
transform: translateX(16px);
|
||||||
background: #4299e1;
|
background: #4299e1;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.toggle-switch:hover .toggle-switch__thumb[data-v-dea4adf6] {
|
.toggle-switch:hover .toggle-switch__thumb[data-v-07ddd3df] {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2223,7 +2223,7 @@ to { transform: rotate(360deg);
|
|||||||
})();
|
})();
|
||||||
var _a;
|
var _a;
|
||||||
import { app as app$1 } from "../../../scripts/app.js";
|
import { app as app$1 } from "../../../scripts/app.js";
|
||||||
import { api } from "../../../scripts/api.js";
|
import { api as api$1 } from "../../../scripts/api.js";
|
||||||
/**
|
/**
|
||||||
* @vue/shared v3.5.26
|
* @vue/shared v3.5.26
|
||||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||||
@@ -11094,7 +11094,10 @@ const _sfc_main$i = /* @__PURE__ */ defineComponent({
|
|||||||
], -1)),
|
], -1)),
|
||||||
createBaseVNode("div", _hoisted_2$e, [
|
createBaseVNode("div", _hoisted_2$e, [
|
||||||
createBaseVNode("label", _hoisted_3$c, [
|
createBaseVNode("label", _hoisted_3$c, [
|
||||||
_cache[3] || (_cache[3] = createBaseVNode("span", { class: "toggle-item__label" }, "No Credit Required", -1)),
|
_cache[3] || (_cache[3] = createBaseVNode("span", {
|
||||||
|
class: "toggle-item__label",
|
||||||
|
title: "Use the model without crediting the creator"
|
||||||
|
}, "No Credit Required", -1)),
|
||||||
createBaseVNode("button", {
|
createBaseVNode("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
class: normalizeClass(["toggle-switch", { "toggle-switch--active": __props.noCreditRequired }]),
|
class: normalizeClass(["toggle-switch", { "toggle-switch--active": __props.noCreditRequired }]),
|
||||||
@@ -11107,7 +11110,10 @@ const _sfc_main$i = /* @__PURE__ */ defineComponent({
|
|||||||
])], 10, _hoisted_4$a)
|
])], 10, _hoisted_4$a)
|
||||||
]),
|
]),
|
||||||
createBaseVNode("label", _hoisted_5$8, [
|
createBaseVNode("label", _hoisted_5$8, [
|
||||||
_cache[5] || (_cache[5] = createBaseVNode("span", { class: "toggle-item__label" }, "Allow Selling", -1)),
|
_cache[5] || (_cache[5] = createBaseVNode("span", {
|
||||||
|
class: "toggle-item__label",
|
||||||
|
title: "Allow selling generated images"
|
||||||
|
}, "Allow Selling", -1)),
|
||||||
createBaseVNode("button", {
|
createBaseVNode("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
class: normalizeClass(["toggle-switch", { "toggle-switch--active": __props.allowSelling }]),
|
class: normalizeClass(["toggle-switch", { "toggle-switch--active": __props.allowSelling }]),
|
||||||
@@ -11124,7 +11130,7 @@ const _sfc_main$i = /* @__PURE__ */ defineComponent({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const LicenseSection = /* @__PURE__ */ _export_sfc(_sfc_main$i, [["__scopeId", "data-v-dea4adf6"]]);
|
const LicenseSection = /* @__PURE__ */ _export_sfc(_sfc_main$i, [["__scopeId", "data-v-07ddd3df"]]);
|
||||||
const _hoisted_1$e = { class: "preview" };
|
const _hoisted_1$e = { class: "preview" };
|
||||||
const _hoisted_2$d = { class: "preview__title" };
|
const _hoisted_2$d = { class: "preview__title" };
|
||||||
const _hoisted_3$b = ["disabled"];
|
const _hoisted_3$b = ["disabled"];
|
||||||
@@ -15031,6 +15037,54 @@ function createModeChangeCallback(node, updateDownstreamLoaders2, nodeSpecificCa
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const app = {};
|
const app = {};
|
||||||
|
const api = {
|
||||||
|
fetchApi: (...args) => fetch(...args),
|
||||||
|
addEventListener: (eventName, handler) => document.addEventListener(eventName, handler),
|
||||||
|
removeEventListener: (eventName, handler) => document.removeEventListener(eventName, handler)
|
||||||
|
};
|
||||||
|
let _loraSyntaxFormatCache = null;
|
||||||
|
let _loraSyntaxFormatRefreshPromise = null;
|
||||||
|
async function _fetchLoraSyntaxFormat() {
|
||||||
|
try {
|
||||||
|
const response = await api.fetchApi("/lm/settings");
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && data.settings) {
|
||||||
|
_loraSyntaxFormatCache = data.settings.lora_syntax_format || "legacy";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
if (_loraSyntaxFormatCache === null) {
|
||||||
|
_loraSyntaxFormatCache = "legacy";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function _triggerBackgroundRefresh() {
|
||||||
|
if (_loraSyntaxFormatRefreshPromise) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_loraSyntaxFormatRefreshPromise = _fetchLoraSyntaxFormat().finally(() => {
|
||||||
|
_loraSyntaxFormatRefreshPromise = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function _initLoraSyntaxFormat() {
|
||||||
|
_triggerBackgroundRefresh();
|
||||||
|
}
|
||||||
|
_initLoraSyntaxFormat();
|
||||||
|
function _initLoraSyntaxFormatReactive() {
|
||||||
|
window.addEventListener("storage", (e) => {
|
||||||
|
if (e.key === "lm:lora-syntax-format-changed") {
|
||||||
|
_triggerBackgroundRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
_triggerBackgroundRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_initLoraSyntaxFormatReactive();
|
||||||
const ROOT_GRAPH_ID = "root";
|
const ROOT_GRAPH_ID = "root";
|
||||||
const LORA_PROVIDER_NODE_TYPES = [
|
const LORA_PROVIDER_NODE_TYPES = [
|
||||||
"Lora Stacker (LoraManager)",
|
"Lora Stacker (LoraManager)",
|
||||||
@@ -15407,7 +15461,7 @@ function createLoraRandomizerWidget(node) {
|
|||||||
const vueApp = createApp(LoraRandomizerWidget, {
|
const vueApp = createApp(LoraRandomizerWidget, {
|
||||||
widget,
|
widget,
|
||||||
node,
|
node,
|
||||||
api
|
api: api$1
|
||||||
});
|
});
|
||||||
vueApp.use(PrimeVue, {
|
vueApp.use(PrimeVue, {
|
||||||
unstyled: true,
|
unstyled: true,
|
||||||
@@ -15482,7 +15536,7 @@ function createLoraCyclerWidget(node) {
|
|||||||
const vueApp = createApp(LoraCyclerWidget, {
|
const vueApp = createApp(LoraCyclerWidget, {
|
||||||
widget,
|
widget,
|
||||||
node,
|
node,
|
||||||
api
|
api: api$1
|
||||||
});
|
});
|
||||||
vueApp.use(PrimeVue, {
|
vueApp.use(PrimeVue, {
|
||||||
unstyled: true,
|
unstyled: true,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user