Viperboy123 commited on
Commit
c4d144f
·
1 Parent(s): 0b40c02
Files changed (29) hide show
  1. commit.bat +30 -0
  2. extensions/Stable-Diffusion-Webui-Civitai-Helper/about_version2.md +0 -30
  3. extensions/Stable-Diffusion-Webui-Civitai-Helper/javascript/civitai_helper.js +694 -288
  4. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/__pycache__/civitai_helper.cpython-310.pyc +0 -0
  5. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/__init__.cpython-310.pyc +0 -0
  6. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/civitai.cpython-310.pyc +0 -0
  7. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/downloader.cpython-310.pyc +0 -0
  8. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/js_action_civitai.cpython-310.pyc +0 -0
  9. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/model.cpython-310.pyc +0 -0
  10. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/model_action_civitai.cpython-310.pyc +0 -0
  11. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/msg_handler.cpython-310.pyc +0 -0
  12. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/setting.cpython-310.pyc +0 -0
  13. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/util.cpython-310.pyc +0 -0
  14. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/civitai.py +534 -425
  15. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/downloader.py +334 -91
  16. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/duplicate_check.py +392 -0
  17. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/js_action_civitai.py +218 -159
  18. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/model.py +603 -94
  19. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/model_action_civitai.py +698 -381
  20. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/msg_handler.py +44 -34
  21. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/setting.py +0 -103
  22. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/templates.py +94 -0
  23. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/util.py +451 -66
  24. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/civitai_helper.py +255 -191
  25. extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/sections.py +693 -0
  26. extensions/Stable-Diffusion-Webui-Civitai-Helper/style.css +108 -1
  27. extensions/sd-webui-infinite-image-browsing/.env +3 -0
  28. extensions/sd-webui-infinite-image-browsing/.gitignore +0 -17
  29. forcePull.bat +11 -0
commit.bat ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+
3
+ git config user.name "anonymous"
4
+ git config user.email my@gmail.com
5
+
6
+ set /P comment=Comment?
7
+
8
+ if not "%comment%" == "" goto :next
9
+ set comment=auto
10
+ :next
11
+
12
+ git reset
13
+
14
+ ::git add *.* --force
15
+ ::git add *
16
+ git add --all
17
+
18
+ echo ----- git add done ------
19
+
20
+ git status
21
+
22
+ echo ready. press any key to commit START.
23
+ ::timeout /t 1
24
+ pause
25
+
26
+ git commit -m "%comment%"
27
+
28
+ git push -u origin main
29
+
30
+ pause
extensions/Stable-Diffusion-Webui-Civitai-Helper/about_version2.md DELETED
@@ -1,30 +0,0 @@
1
- ## About Civitai Helper2: Model Info Helper
2
- The first version of "Civitai Helper2" is already finished and I have used it for a while on my local machine. It works exactly as in the demo video I have released before(with some more functions).
3
-
4
- [https://www.youtube.com/watch?v=mPcKwQDDH8s](https://www.youtube.com/watch?v=mPcKwQDDH8s)
5
-
6
- But the developing process is extremely painful. Its maintenance will also be difficult. If I release it, with all those requests and issues from users, my life gonna be in hell.
7
-
8
- The reason for this is, gradio is not suitable for a professional level's management system. But under gradio's framework and SD webui's design, a lot of javascript things won't work well.
9
-
10
- So, it is too painful that I don't want to do any further maintenance and development on it. In the meantime, a lot of alternatives become popular. For example, ComfyUI and InvokeAI 3.x.
11
-
12
- Especially InvokeAI 3.x, with its Unified Canvas and build-in node system, now we have a photoshop level's SD tool for free.
13
-
14
- If you are not familiar with InvokeAI 3.x, you can check this video:
15
- [https://www.youtube.com/watch?v=1Iz4F7o6hgQ](https://www.youtube.com/watch?v=1Iz4F7o6hgQ)
16
- And its unified canvas system:
17
- [https://www.youtube.com/watch?v=aU0jGZpDIVc](https://www.youtube.com/watch?v=aU0jGZpDIVc)
18
-
19
- So, for Model Info Helper, I'm re-designing it into a stand alone model and workflow management system, which should work with most popular SD UI, not just for SD webui.
20
-
21
- And I'll take down all workflow management functions so this stand alone version can be released sooner.
22
-
23
- Since it won't be released as estimated, I have updated the old civitai helper 1.x to work with latest SD webui.
24
-
25
-
26
-
27
-
28
-
29
-
30
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/javascript/civitai_helper.js CHANGED
@@ -1,38 +1,61 @@
1
  "use strict";
2
 
3
 
4
- function ch_convert_file_path_to_url(path){
5
  let prefix = "file=";
6
  let path_to_url = path.replaceAll('\\', '/');
7
  return prefix+path_to_url;
8
  }
9
 
10
- function ch_img_node_str(path){
11
  return `<img src='${ch_convert_file_path_to_url(path)}' style="width:24px"/>`;
12
  }
13
 
14
-
15
- function ch_gradio_version(){
16
  let foot = gradioApp().getElementById("footer");
17
- if (!foot){return null;}
18
 
19
  let versions = foot.querySelector(".versions");
20
- if (!versions){return null;}
21
 
22
- if (versions.innerHTML.indexOf("gradio: 3.16.2")>0) {
23
  return "3.16.2";
24
  } else {
25
  return "3.23.0";
26
  }
27
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
 
30
 
31
  // send msg to python side by filling a hidden text box
32
  // then will click a button to trigger an action
33
  // msg is an object, not a string, will be stringify in this function
34
- function send_ch_py_msg(msg){
35
- console.log("run send_ch_py_msg")
36
  let js_msg_txtbox = gradioApp().querySelector("#ch_js_msg_txtbox textarea");
37
  if (js_msg_txtbox && msg) {
38
  // fill to msg box
@@ -44,16 +67,16 @@ function send_ch_py_msg(msg){
44
 
45
  // get msg from python side from a hidden textbox
46
  // normally this is an old msg, need to wait for a new msg
47
- function get_ch_py_msg(){
48
- console.log("run get_ch_py_msg")
49
  const py_msg_txtbox = gradioApp().querySelector("#ch_py_msg_txtbox textarea");
50
  if (py_msg_txtbox && py_msg_txtbox.value) {
51
  console.log("find py_msg_txtbox");
52
  console.log("py_msg_txtbox value: ");
53
- console.log(py_msg_txtbox.value)
54
- return py_msg_txtbox.value
55
  } else {
56
- return ""
57
  }
58
  }
59
 
@@ -61,7 +84,7 @@ function get_ch_py_msg(){
61
  // get msg from python side from a hidden textbox
62
  // it will try once in every sencond, until it reach the max try times
63
  const get_new_ch_py_msg = (max_count=5) => new Promise((resolve, reject) => {
64
- console.log("run get_new_ch_py_msg")
65
 
66
  let count = 0;
67
  let new_msg = "";
@@ -73,11 +96,11 @@ const get_new_ch_py_msg = (max_count=5) => new Promise((resolve, reject) => {
73
  if (py_msg_txtbox && py_msg_txtbox.value) {
74
  console.log("find py_msg_txtbox");
75
  console.log("py_msg_txtbox value: ");
76
- console.log(py_msg_txtbox.value)
77
 
78
- new_msg = py_msg_txtbox.value
79
  if (new_msg != "") {
80
- find_msg=true
81
  }
82
  }
83
 
@@ -98,7 +121,7 @@ const get_new_ch_py_msg = (max_count=5) => new Promise((resolve, reject) => {
98
  }
99
 
100
  }, 1000);
101
- })
102
 
103
 
104
  function getActiveTabType() {
@@ -112,7 +135,9 @@ function getActiveTabType() {
112
  return null;
113
  }
114
 
115
-
 
 
116
 
117
  function getActivePrompt() {
118
  const currentTab = get_uiCurrentTabContent();
@@ -138,16 +163,20 @@ function getActiveNegativePrompt() {
138
 
139
 
140
  //button's click function
141
- async function open_model_url(event, model_type, search_term){
142
  console.log("start open_model_url");
143
 
144
- //get hidden components of extension
 
 
 
 
145
  let js_open_url_btn = gradioApp().getElementById("ch_js_open_url_btn");
146
  if (!js_open_url_btn) {
147
- return
 
148
  }
149
 
150
-
151
  //msg to python side
152
  let msg = {
153
  "action": "",
@@ -155,8 +184,7 @@ async function open_model_url(event, model_type, search_term){
155
  "search_term": "",
156
  "prompt": "",
157
  "neg_prompt": "",
158
- }
159
-
160
 
161
  msg["action"] = "open_url";
162
  msg["model_type"] = model_type;
@@ -165,24 +193,19 @@ async function open_model_url(event, model_type, search_term){
165
  msg["neg_prompt"] = "";
166
 
167
  // fill to msg box
168
- send_ch_py_msg(msg)
169
 
170
  //click hidden button
171
  js_open_url_btn.click();
172
 
173
- // stop parent event
174
- event.stopPropagation()
175
- event.preventDefault()
176
-
177
  //check response msg from python
178
- let new_py_msg = "";
179
  try {
180
  new_py_msg = await get_new_ch_py_msg();
181
-
182
  } catch (error) {
183
- console.log(error);
184
  }
185
-
186
  console.log("new_py_msg:");
187
  console.log(new_py_msg);
188
 
@@ -194,28 +217,25 @@ async function open_model_url(event, model_type, search_term){
194
  if (py_msg_json.content.url) {
195
  window.open(py_msg_json.content.url, "_blank");
196
  }
197
-
198
  }
199
-
200
-
201
  }
202
 
203
-
204
  console.log("end open_model_url");
205
 
206
-
207
  }
208
 
209
- function add_trigger_words(event, model_type, search_term){
210
  console.log("start add_trigger_words");
211
 
212
- //get hidden components of extension
 
 
 
213
  let js_add_trigger_words_btn = gradioApp().getElementById("ch_js_add_trigger_words_btn");
214
  if (!js_add_trigger_words_btn) {
215
- return
216
  }
217
 
218
-
219
  //msg to python side
220
  let msg = {
221
  "action": "",
@@ -223,7 +243,7 @@ function add_trigger_words(event, model_type, search_term){
223
  "search_term": "",
224
  "prompt": "",
225
  "neg_prompt": "",
226
- }
227
 
228
  msg["action"] = "add_trigger_words";
229
  msg["model_type"] = model_type;
@@ -235,26 +255,25 @@ function add_trigger_words(event, model_type, search_term){
235
  msg["prompt"] = act_prompt.value;
236
 
237
  // fill to msg box
238
- send_ch_py_msg(msg)
239
 
240
  //click hidden button
241
  js_add_trigger_words_btn.click();
242
 
243
  console.log("end add_trigger_words");
244
 
245
- event.stopPropagation()
246
- event.preventDefault()
247
-
248
-
249
  }
250
 
251
- function use_preview_prompt(event, model_type, search_term){
252
  console.log("start use_preview_prompt");
253
 
254
- //get hidden components of extension
 
 
 
255
  let js_use_preview_prompt_btn = gradioApp().getElementById("ch_js_use_preview_prompt_btn");
256
  if (!js_use_preview_prompt_btn) {
257
- return
258
  }
259
 
260
  //msg to python side
@@ -264,7 +283,7 @@ function use_preview_prompt(event, model_type, search_term){
264
  "search_term": "",
265
  "prompt": "",
266
  "neg_prompt": "",
267
- }
268
 
269
  msg["action"] = "use_preview_prompt";
270
  msg["model_type"] = model_type;
@@ -279,35 +298,64 @@ function use_preview_prompt(event, model_type, search_term){
279
  msg["neg_prompt"] = neg_prompt.value;
280
 
281
  // fill to msg box
282
- send_ch_py_msg(msg)
283
 
284
  //click hidden button
285
  js_use_preview_prompt_btn.click();
286
 
287
  console.log("end use_preview_prompt");
288
 
289
- event.stopPropagation()
290
- event.preventDefault()
 
 
 
 
 
 
 
 
 
 
 
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  }
293
 
294
 
295
- async function remove_card(event, model_type, search_term){
296
  console.log("start remove_card");
297
 
298
- //get hidden components of extension
 
 
 
 
 
 
299
  let js_remove_card_btn = gradioApp().getElementById("ch_js_remove_card_btn");
300
  if (!js_remove_card_btn) {
301
- return
302
  }
303
 
304
  // must confirm before removing
305
- let rm_confirm = "\nConfirm to remove this model.\n\nCheck console log for detail.";
306
  if (!confirm(rm_confirm)) {
307
- return
308
  }
309
 
310
-
311
  //msg to python side
312
  let msg = {
313
  "action": "",
@@ -315,12 +363,9 @@ async function remove_card(event, model_type, search_term){
315
  "search_term": "",
316
  }
317
 
318
-
319
  msg["action"] = "remove_card";
320
  msg["model_type"] = model_type;
321
  msg["search_term"] = search_term;
322
- msg["prompt"] = "";
323
- msg["neg_prompt"] = "";
324
 
325
  // fill to msg box
326
  send_ch_py_msg(msg)
@@ -328,10 +373,76 @@ async function remove_card(event, model_type, search_term){
328
  //click hidden button
329
  js_remove_card_btn.click();
330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  // stop parent event
332
  event.stopPropagation()
333
  event.preventDefault()
334
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  //check response msg from python
336
  let new_py_msg = "";
337
  try {
@@ -340,7 +451,7 @@ async function remove_card(event, model_type, search_term){
340
  console.log(error);
341
  new_py_msg = error;
342
  }
343
-
344
  console.log("new_py_msg:");
345
  console.log(new_py_msg);
346
 
@@ -351,44 +462,49 @@ async function remove_card(event, model_type, search_term){
351
  result = new_py_msg;
352
  }
353
 
354
- // alert result
355
- alert(result);
356
-
357
- if (result=="Done"){
358
- console.log("refresh card list");
359
- //refresh card list
360
- let active_tab = getActiveTabType();
361
- console.log("get active tab id: " + active_tab);
362
- if (active_tab){
363
- let refresh_btn_id = active_tab + "_extra_refresh";
364
- let refresh_btn = gradioApp().getElementById(refresh_btn_id);
365
- if (refresh_btn){
366
- console.log("click button: "+refresh_btn_id);
367
- refresh_btn.click();
368
- }
369
- }
370
  }
371
-
372
- console.log("end remove_card");
 
 
373
 
374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  }
376
 
377
 
378
  // download model's new version into SD at python side
379
- function ch_dl_model_new_version(event, model_path, version_id, download_url){
380
  console.log("start ch_dl_model_new_version");
381
 
 
 
 
382
  // must confirm before downloading
383
  let dl_confirm = "\nConfirm to download.\n\nCheck Download Model Section's log and console log for detail.";
 
384
  if (!confirm(dl_confirm)) {
385
- return
386
  }
387
 
388
- //get hidden components of extension
389
  let js_dl_model_new_version_btn = gradioApp().getElementById("ch_js_dl_model_new_version_btn");
390
  if (!js_dl_model_new_version_btn) {
391
- return
392
  }
393
 
394
  //msg to python side
@@ -397,58 +513,463 @@ function ch_dl_model_new_version(event, model_path, version_id, download_url){
397
  "model_path": "",
398
  "version_id": "",
399
  "download_url": "",
400
- }
401
 
402
  msg["action"] = "dl_model_new_version";
403
  msg["model_path"] = model_path;
404
  msg["version_id"] = version_id;
405
  msg["download_url"] = download_url;
 
406
 
407
  // fill to msg box
408
- send_ch_py_msg(msg)
409
 
410
  //click hidden button
411
  js_dl_model_new_version_btn.click();
412
 
413
  console.log("end dl_model_new_version");
414
 
415
- event.stopPropagation()
416
- event.preventDefault()
417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  }
420
 
421
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  onUiLoaded(() => {
423
 
424
  //get gradio version
425
- let gradio_ver = ch_gradio_version();
426
- console.log("gradio_ver:" + gradio_ver);
427
 
428
  // get all extra network tabs
429
- let tab_prefix_list = ["txt2img", "img2img"];
430
- let model_type_list = ["textual_inversion", "hypernetworks", "checkpoints", "lora"];
431
- let cardid_suffix = "cards";
432
-
433
- //get init py msg
434
- // let init_py_msg_str = get_ch_py_msg();
435
- // let extension_path = "";
436
- // if (!init_py_msg_str) {
437
- // console.log("Can not get init_py_msg");
438
- // } else {
439
- // init_py_msg = JSON.parse(init_py_msg_str);
440
- // if (init_py_msg) {
441
- // extension_path = init_py_msg.extension_path;
442
- // console.log("get extension path: " + extension_path);
443
- // }
444
- // }
445
-
446
- // //icon image node as string
447
- // function icon(icon_name){
448
- // let icon_path = extension_path+"/icon/"+icon_name;
449
- // return ch_img_node_str(icon_path);
450
- // }
451
-
452
 
453
  // update extra network tab pages' cards
454
  // * replace "replace preview" text button into an icon
@@ -459,87 +980,39 @@ onUiLoaded(() => {
459
  // notice: javascript can not get response from python side
460
  // so, these buttons just sent request to python
461
  // then, python side gonna open url and update prompt text box, without telling js side.
462
- function update_card_for_civitai(){
463
-
464
- //css
465
- let btn_margin = "0px 5px";
466
- let btn_fontSize = "200%";
467
- let btn_thumb_fontSize = "100%";
468
- let btn_thumb_display = "inline";
469
- let btn_thumb_pos = "static";
470
- let btn_thumb_backgroundImage = "none";
471
- let btn_thumb_background = "rgba(0, 0, 0, 0.8)";
472
-
473
- let ch_btn_txts = ['🌐', '💡', '🏷️'];
474
  let replace_preview_text = getTranslation("replace preview");
475
  if (!replace_preview_text) {
476
  replace_preview_text = "replace preview";
477
  }
478
 
479
-
480
- //change all "replace preview" into an icon
481
- let extra_network_id = "";
482
  let extra_network_node = null;
483
- let button_row = null;
484
- let search_term_node = null;
485
- let search_term = "";
486
  let model_type = "";
487
  let cards = null;
488
- let need_to_add_buttons = false;
489
 
490
  //get current tab
491
  let active_tab_type = getActiveTabType();
492
- if (!active_tab_type){active_tab_type = "txt2img";}
493
 
494
  for (const tab_prefix of tab_prefix_list) {
495
  if (tab_prefix != active_tab_type) {continue;}
496
 
497
-
498
- //find out current selected model type tab
499
- let active_extra_tab_type = "";
500
- let extra_tabs = gradioApp().getElementById(tab_prefix+"_extra_tabs");
501
- if (!extra_tabs) {console.log("can not find extra_tabs: " + tab_prefix+"_extra_tabs");}
502
-
503
  //get active extratab
 
504
  const active_extra_tab = Array.from(get_uiCurrentTabContent().querySelectorAll('.extra-network-cards,.extra-network-thumbs'))
505
  .find(el => el.closest('.tabitem').style.display === 'block')
506
- ?.id.match(/^(txt2img|img2img)_(.+)_cards$/)[2]
507
-
508
-
509
- console.log("found active tab: " + active_extra_tab);
510
-
511
- switch (active_extra_tab) {
512
- case "textual_inversion":
513
- active_extra_tab_type = "ti";
514
- break;
515
- case "hypernetworks":
516
- active_extra_tab_type = "hyper";
517
- break;
518
- case "checkpoints":
519
- active_extra_tab_type = "ckp";
520
- break;
521
- case "lora":
522
- active_extra_tab_type = "lora";
523
- break;
524
- }
525
 
 
526
 
527
  for (const js_model_type of model_type_list) {
528
  //get model_type for python side
529
- switch (js_model_type) {
530
- case "textual_inversion":
531
- model_type = "ti";
532
- break;
533
- case "hypernetworks":
534
- model_type = "hyper";
535
- break;
536
- case "checkpoints":
537
- model_type = "ckp";
538
- break;
539
- case "lora":
540
- model_type = "lora";
541
- break;
542
- }
543
 
544
  if (!model_type) {
545
  console.log("can not get model_type from: " + js_model_type);
@@ -552,142 +1025,75 @@ onUiLoaded(() => {
552
  continue;
553
  }
554
 
555
- console.log("handle active extra tab");
556
-
557
 
558
- extra_network_id = tab_prefix+"_"+js_model_type+"_"+cardid_suffix;
559
  // console.log("searching extra_network_node: " + extra_network_id);
560
- extra_network_node = gradioApp().getElementById(extra_network_id);
561
-
562
- // console.log("find extra_network_node: " + extra_network_id);
563
 
564
  // get all card nodes
565
  cards = extra_network_node.querySelectorAll(".card");
566
- for (let card of cards) {
567
- //get button row
568
- button_row = card.querySelector(".button-row");
569
-
570
- if (!button_row){
571
- console.log("can not find button_row");
572
- continue;
573
- }
574
-
575
- let atags = button_row.querySelectorAll("a");
576
- if (atags && atags.length) {
577
- console.log("find atags: " + atags.length);
578
- } else {
579
- console.log("no atags");
580
- need_to_add_buttons = true;
581
- }
582
-
583
- if (!need_to_add_buttons) {
584
- console.log("do not need to add buttons");
585
- continue;
586
- }
587
-
588
-
589
- // search_term node
590
- // search_term = subfolder path + model name + ext
591
- search_term_node = card.querySelector(".actions .additional .search_term");
592
- if (!search_term_node){
593
- console.log("can not find search_term node for cards in " + extra_network_id);
594
- continue;
595
- }
596
-
597
- // get search_term
598
- search_term = search_term_node.innerHTML.trim();
599
- if (!search_term) {
600
- console.log("search_term is empty for cards in " + extra_network_id);
601
- continue;
602
  }
603
-
604
- console.log("adding buttons");
605
- // then we need to add 3 buttons to each ul node:
606
- let open_url_node = document.createElement("a");
607
- open_url_node.href = "#";
608
- open_url_node.innerHTML = "🌐";
609
- open_url_node.className = "card-button";
610
-
611
- open_url_node.title = "Open this model's civitai url";
612
- open_url_node.setAttribute("onclick","open_model_url(event, '"+model_type+"', '"+search_term+"')");
613
-
614
- let add_trigger_words_node = document.createElement("a");
615
- add_trigger_words_node.href = "#";
616
- add_trigger_words_node.innerHTML = "💡";
617
- add_trigger_words_node.className = "card-button";
618
-
619
- add_trigger_words_node.title = "Add trigger words to prompt";
620
- add_trigger_words_node.setAttribute("onclick","add_trigger_words(event, '"+model_type+"', '"+search_term+"')");
621
-
622
- let use_preview_prompt_node = document.createElement("a");
623
- use_preview_prompt_node.href = "#";
624
- use_preview_prompt_node.innerHTML = "🏷️";
625
- use_preview_prompt_node.className = "card-button";
626
-
627
- use_preview_prompt_node.title = "Use prompt from preview image";
628
- use_preview_prompt_node.setAttribute("onclick","use_preview_prompt(event, '"+model_type+"', '"+search_term+"')");
629
-
630
- let remove_card_node = document.createElement("a");
631
- remove_card_node.href = "#";
632
- remove_card_node.innerHTML = "❌";
633
- remove_card_node.className = "card-button";
634
-
635
- remove_card_node.title = "Remove this model";
636
- remove_card_node.setAttribute("onclick","remove_card(event, '"+model_type+"', '"+search_term+"')");
637
-
638
- //add to card
639
- button_row.appendChild(open_url_node);
640
- button_row.appendChild(add_trigger_words_node);
641
- button_row.appendChild(use_preview_prompt_node);
642
- button_row.appendChild(remove_card_node);
643
-
644
-
645
-
646
-
647
  }
648
 
649
-
650
  }
651
- }
652
 
 
653
 
654
  }
655
 
656
-
657
- let tab_id = ""
658
- let extra_tab = null;
659
- let extra_toolbar = null;
660
  let extra_network_refresh_btn = null;
 
 
 
661
  //add refresh button to extra network's toolbar
662
- for (let prefix of tab_prefix_list) {
663
- tab_id = prefix + "_extra_tabs";
664
- extra_tab = gradioApp().getElementById(tab_id);
665
 
666
- //get toolbar
667
- //get Refresh button
668
- extra_network_refresh_btn = gradioApp().getElementById(prefix+"_extra_refresh");
669
 
 
 
 
 
 
 
670
 
671
- if (!extra_network_refresh_btn){
672
- console.log("can not get extra network refresh button for " + tab_id);
673
  continue;
 
674
  }
675
 
676
- // add refresh button to toolbar
677
- let ch_refresh = document.createElement("button");
678
- ch_refresh.innerHTML = "🔁";
679
- ch_refresh.title = "Refresh Civitai Helper's additional buttons";
680
- ch_refresh.className = "lg secondary gradio-button";
681
- ch_refresh.style.fontSize = "200%";
682
- ch_refresh.onclick = update_card_for_civitai;
683
 
684
- extra_network_refresh_btn.parentNode.appendChild(ch_refresh);
 
685
 
686
- }
 
 
 
687
 
 
 
 
 
 
 
 
688
 
689
  //run it once
690
- update_card_for_civitai();
691
 
692
 
693
  });
 
1
  "use strict";
2
 
3
 
4
+ function ch_convert_file_path_to_url(path) {
5
  let prefix = "file=";
6
  let path_to_url = path.replaceAll('\\', '/');
7
  return prefix+path_to_url;
8
  }
9
 
10
+ function ch_img_node_str(path) {
11
  return `<img src='${ch_convert_file_path_to_url(path)}' style="width:24px"/>`;
12
  }
13
 
14
+ function ch_gradio_version() {
 
15
  let foot = gradioApp().getElementById("footer");
16
+ if (!foot) { return null; }
17
 
18
  let versions = foot.querySelector(".versions");
19
+ if (!versions) { return null; }
20
 
21
+ if (versions.textContent.indexOf("gradio: 3.16.2")>0) {
22
  return "3.16.2";
23
  } else {
24
  return "3.23.0";
25
  }
26
+ }
27
+
28
+ /*
29
+ * Functions for scan for duplicates elements.
30
+ */
31
+
32
+ let ch_path_el = null;
33
+ function display_ch_path(event, path) {
34
+ if (!ch_path_el) {
35
+ ch_path_el = document.createElement("div");
36
+ ch_path_el.id = "ch_path_el";
37
+ document.body.appendChild(ch_path_el);
38
+ }
39
+
40
+ ch_path_el.textContent = path;
41
+ ch_path_el.style.display = "block";
42
+ }
43
+
44
+ function move_ch_path(event) {
45
+ ch_path_el.style.top = `calc(${event.clientY}px - 2em)`;
46
+ ch_path_el.style.left = `calc(${event.clientX}px + 2em)`;
47
+ }
48
+
49
+ function hide_ch_path(event) {
50
+ ch_path_el.style.display = "none";
51
  }
52
 
53
 
54
  // send msg to python side by filling a hidden text box
55
  // then will click a button to trigger an action
56
  // msg is an object, not a string, will be stringify in this function
57
+ function send_ch_py_msg(msg) {
58
+ console.log("run send_ch_py_msg");
59
  let js_msg_txtbox = gradioApp().querySelector("#ch_js_msg_txtbox textarea");
60
  if (js_msg_txtbox && msg) {
61
  // fill to msg box
 
67
 
68
  // get msg from python side from a hidden textbox
69
  // normally this is an old msg, need to wait for a new msg
70
+ function get_ch_py_msg() {
71
+ console.log("run get_ch_py_msg");
72
  const py_msg_txtbox = gradioApp().querySelector("#ch_py_msg_txtbox textarea");
73
  if (py_msg_txtbox && py_msg_txtbox.value) {
74
  console.log("find py_msg_txtbox");
75
  console.log("py_msg_txtbox value: ");
76
+ console.log(py_msg_txtbox.value);
77
+ return py_msg_txtbox.value;
78
  } else {
79
+ return "";
80
  }
81
  }
82
 
 
84
  // get msg from python side from a hidden textbox
85
  // it will try once in every sencond, until it reach the max try times
86
  const get_new_ch_py_msg = (max_count=5) => new Promise((resolve, reject) => {
87
+ console.log("run get_new_ch_py_msg");
88
 
89
  let count = 0;
90
  let new_msg = "";
 
96
  if (py_msg_txtbox && py_msg_txtbox.value) {
97
  console.log("find py_msg_txtbox");
98
  console.log("py_msg_txtbox value: ");
99
+ console.log(py_msg_txtbox.value);
100
 
101
+ new_msg = py_msg_txtbox.value;
102
  if (new_msg != "") {
103
+ find_msg = true;
104
  }
105
  }
106
 
 
121
  }
122
 
123
  }, 1000);
124
+ });
125
 
126
 
127
  function getActiveTabType() {
 
135
  return null;
136
  }
137
 
138
+ function getExtraTabs(prefix) {
139
+ return gradioApp().getElementById(prefix + "_extra_tabs");
140
+ }
141
 
142
  function getActivePrompt() {
143
  const currentTab = get_uiCurrentTabContent();
 
163
 
164
 
165
  //button's click function
166
+ async function open_model_url(event, model_type, search_term) {
167
  console.log("start open_model_url");
168
 
169
+ // stop parent event
170
+ event.stopPropagation();
171
+ event.preventDefault();
172
+
173
+ //get hidden components of extension
174
  let js_open_url_btn = gradioApp().getElementById("ch_js_open_url_btn");
175
  if (!js_open_url_btn) {
176
+ console.log("Failed to find js_open_url_btn");
177
+ return;
178
  }
179
 
 
180
  //msg to python side
181
  let msg = {
182
  "action": "",
 
184
  "search_term": "",
185
  "prompt": "",
186
  "neg_prompt": "",
187
+ };
 
188
 
189
  msg["action"] = "open_url";
190
  msg["model_type"] = model_type;
 
193
  msg["neg_prompt"] = "";
194
 
195
  // fill to msg box
196
+ send_ch_py_msg(msg);
197
 
198
  //click hidden button
199
  js_open_url_btn.click();
200
 
 
 
 
 
201
  //check response msg from python
202
+ let new_py_msg = null;
203
  try {
204
  new_py_msg = await get_new_ch_py_msg();
 
205
  } catch (error) {
206
+ console.log(error);
207
  }
208
+
209
  console.log("new_py_msg:");
210
  console.log(new_py_msg);
211
 
 
217
  if (py_msg_json.content.url) {
218
  window.open(py_msg_json.content.url, "_blank");
219
  }
 
220
  }
 
 
221
  }
222
 
 
223
  console.log("end open_model_url");
224
 
 
225
  }
226
 
227
+ function add_trigger_words(event, model_type, search_term) {
228
  console.log("start add_trigger_words");
229
 
230
+ event.stopPropagation();
231
+ event.preventDefault();
232
+
233
+ //get hidden components of extension
234
  let js_add_trigger_words_btn = gradioApp().getElementById("ch_js_add_trigger_words_btn");
235
  if (!js_add_trigger_words_btn) {
236
+ return;
237
  }
238
 
 
239
  //msg to python side
240
  let msg = {
241
  "action": "",
 
243
  "search_term": "",
244
  "prompt": "",
245
  "neg_prompt": "",
246
+ };
247
 
248
  msg["action"] = "add_trigger_words";
249
  msg["model_type"] = model_type;
 
255
  msg["prompt"] = act_prompt.value;
256
 
257
  // fill to msg box
258
+ send_ch_py_msg(msg);
259
 
260
  //click hidden button
261
  js_add_trigger_words_btn.click();
262
 
263
  console.log("end add_trigger_words");
264
 
 
 
 
 
265
  }
266
 
267
+ function use_preview_prompt(event, model_type, search_term) {
268
  console.log("start use_preview_prompt");
269
 
270
+ event.stopPropagation();
271
+ event.preventDefault();
272
+
273
+ //get hidden components of extension
274
  let js_use_preview_prompt_btn = gradioApp().getElementById("ch_js_use_preview_prompt_btn");
275
  if (!js_use_preview_prompt_btn) {
276
+ return;
277
  }
278
 
279
  //msg to python side
 
283
  "search_term": "",
284
  "prompt": "",
285
  "neg_prompt": "",
286
+ };
287
 
288
  msg["action"] = "use_preview_prompt";
289
  msg["model_type"] = model_type;
 
298
  msg["neg_prompt"] = neg_prompt.value;
299
 
300
  // fill to msg box
301
+ send_ch_py_msg(msg);
302
 
303
  //click hidden button
304
  js_use_preview_prompt_btn.click();
305
 
306
  console.log("end use_preview_prompt");
307
 
308
+ }
309
+
310
+
311
+ async function remove_dup_card(event, model_type, search_term) {
312
+ event.stopPropagation();
313
+ event.preventDefault();
314
+
315
+ let el = event.currentTarget;
316
+
317
+ let success = await remove_card(event, model_type, search_term);
318
+
319
+ if (success === true) {
320
+ let parent = el.parentElement;
321
 
322
+ let sha256 = search_term.split(" ").pop().toUpperCase();
323
+ let row_id = `ch_${sha256}`;
324
+ let cards_id = `${row_id}_cards`;
325
+
326
+ let cards = document.getElementById(cards_id);
327
+
328
+ cards.removeChild(parent);
329
+
330
+ if (cards.children.length < 2) {
331
+ let row = document.getElementById(row_id);
332
+ row.parentElement.removeChild(row);
333
+ }
334
+ }
335
  }
336
 
337
 
338
+ async function remove_card(event, model_type, search_term) {
339
  console.log("start remove_card");
340
 
341
+ let status = false
342
+
343
+ // stop parent event
344
+ event.stopPropagation()
345
+ event.preventDefault()
346
+
347
+ //get hidden components of extension
348
  let js_remove_card_btn = gradioApp().getElementById("ch_js_remove_card_btn");
349
  if (!js_remove_card_btn) {
350
+ return status;
351
  }
352
 
353
  // must confirm before removing
354
+ let rm_confirm = "\nConfirm to remove this model and all related files. This process is irreversible.";
355
  if (!confirm(rm_confirm)) {
356
+ return status;
357
  }
358
 
 
359
  //msg to python side
360
  let msg = {
361
  "action": "",
 
363
  "search_term": "",
364
  }
365
 
 
366
  msg["action"] = "remove_card";
367
  msg["model_type"] = model_type;
368
  msg["search_term"] = search_term;
 
 
369
 
370
  // fill to msg box
371
  send_ch_py_msg(msg)
 
373
  //click hidden button
374
  js_remove_card_btn.click();
375
 
376
+ //check response msg from python
377
+ let new_py_msg = "";
378
+ try {
379
+ new_py_msg = await get_new_ch_py_msg();
380
+ } catch (error) {
381
+ console.log(error);
382
+ new_py_msg = error;
383
+ }
384
+
385
+ console.log("new_py_msg:");
386
+ console.log(new_py_msg);
387
+
388
+ //check msg
389
+ let result = "Done";
390
+ //check msg
391
+ if (new_py_msg) {
392
+ result = new_py_msg;
393
+ }
394
+
395
+ if (result == "Done") {
396
+ status = true;
397
+ refresh_cards_list(model_type);
398
+ }
399
+
400
+ console.log("end remove_card");
401
+
402
+ return status;
403
+
404
+ }
405
+
406
+
407
+ async function rename_card(event, model_type, search_term, model_name) {
408
+ console.log("start rename_card");
409
+
410
  // stop parent event
411
  event.stopPropagation()
412
  event.preventDefault()
413
 
414
+ //get hidden components of extension
415
+ let js_rename_card_btn = gradioApp().getElementById("ch_js_rename_card_btn");
416
+ if (!js_rename_card_btn) {
417
+ return;
418
+ }
419
+
420
+ // must confirm before removing
421
+ let rename_prompt = "\nRename this model to:";
422
+ let new_name = prompt(rename_prompt, model_name);
423
+ if (!new_name) {
424
+ return;
425
+ }
426
+
427
+ //msg to python side
428
+ let msg = {
429
+ "action": "",
430
+ "model_type": "",
431
+ "search_term": "",
432
+ "new_name": "",
433
+ }
434
+
435
+ msg["action"] = "rename_card";
436
+ msg["model_type"] = model_type;
437
+ msg["search_term"] = search_term;
438
+ msg["new_name"] = new_name;
439
+
440
+ // fill to msg box
441
+ send_ch_py_msg(msg)
442
+
443
+ //click hidden button
444
+ js_rename_card_btn.click();
445
+
446
  //check response msg from python
447
  let new_py_msg = "";
448
  try {
 
451
  console.log(error);
452
  new_py_msg = error;
453
  }
454
+
455
  console.log("new_py_msg:");
456
  console.log(new_py_msg);
457
 
 
462
  result = new_py_msg;
463
  }
464
 
465
+ if (result == "Done") {
466
+ refresh_cards_list(model_type);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  }
468
+
469
+ console.log("end rename_card");
470
+
471
+ }
472
 
473
 
474
+ function replace_preview(e, page, type, name) {
475
+ // we have to create a whole hidden editor window to access preview replace functionality
476
+ extraNetworksEditUserMetadata(e, page, type, name);
477
+
478
+ // the editor window takes quite some time to populate
479
+ waitForEditor(page, type, name).then(editor => {
480
+ // Gather the buttons we need to both replace the preview and close the editor
481
+ let cancel_button = editor.querySelector('.edit-user-metadata-buttons button:first-of-type');
482
+ let replace_preview_button = editor.querySelector('.edit-user-metadata-buttons button:nth-of-type(2)');
483
+
484
+ replace_preview_button.click();
485
+ cancel_button.click();
486
+ });
487
  }
488
 
489
 
490
  // download model's new version into SD at python side
491
+ function ch_dl_model_new_version(event, model_path, version_id, download_url, model_type) {
492
  console.log("start ch_dl_model_new_version");
493
 
494
+ event.stopPropagation();
495
+ event.preventDefault();
496
+
497
  // must confirm before downloading
498
  let dl_confirm = "\nConfirm to download.\n\nCheck Download Model Section's log and console log for detail.";
499
+
500
  if (!confirm(dl_confirm)) {
501
+ return;
502
  }
503
 
504
+ //get hidden components of extension
505
  let js_dl_model_new_version_btn = gradioApp().getElementById("ch_js_dl_model_new_version_btn");
506
  if (!js_dl_model_new_version_btn) {
507
+ return;
508
  }
509
 
510
  //msg to python side
 
513
  "model_path": "",
514
  "version_id": "",
515
  "download_url": "",
516
+ };
517
 
518
  msg["action"] = "dl_model_new_version";
519
  msg["model_path"] = model_path;
520
  msg["version_id"] = version_id;
521
  msg["download_url"] = download_url;
522
+ msg["model_type"] = model_type;
523
 
524
  // fill to msg box
525
+ send_ch_py_msg(msg);
526
 
527
  //click hidden button
528
  js_dl_model_new_version_btn.click();
529
 
530
  console.log("end dl_model_new_version");
531
 
 
 
532
 
533
+ }
534
+
535
+
536
+ function refresh_cards_list(model_type) {
537
+ console.log("refresh card list");
538
+ //refresh card list
539
+ let active_tab = getActiveTabType();
540
+ console.log(`get active tab id: ${active_tab}`);
541
+ if (active_tab) {
542
+ let refresh_btn_id = `${active_tab}_extra_refresh`;
543
+ let refresh_btn = gradioApp().getElementById(refresh_btn_id);
544
+ if (!refresh_btn) {
545
+ // webui 1.8
546
+ refresh_btn_id = `${active_tab}_${model_type}_extra_refresh`;
547
+ refresh_btn = gradioApp().getElementById(refresh_btn_id);
548
+ }
549
+ if (refresh_btn) {
550
+ console.log(`click button: ${refresh_btn_id}`);
551
+ refresh_btn.click();
552
+ }
553
+ }
554
+ }
555
+
556
+ function processCards(tab, extra_tab_els) {
557
+ if (!(opts && "ch_always_display" in opts)) {
558
+ // Lobe theme can cause a race condition.
559
+ console.log("Waiting for webui settings to become available");
560
+ console.log(opts);
561
+ const try_again = function () {
562
+ processCards(tab, extra_tab_els);
563
+ }
564
+ return setTimeout(try_again, 500);
565
+ }
566
+
567
+ const prefix_length = tab.length + 1;
568
+ for (const el of extra_tab_els) {
569
+ const model_type = el.id.slice(prefix_length, -6);
570
+ const cards = el.querySelectorAll('.card');
571
+ for (const card of cards) {
572
+ processSingleCard(tab, getShortModelTypeFromFull(model_type), card);
573
+ }
574
+ }
575
+ }
576
+
577
+
578
+ function getModelCardsEl(prefix, model_type) {
579
+ const id = prefix + "_" + model_type + "_cards";
580
+ return gradioApp().getElementById(id);
581
+ }
582
+
583
+
584
+ function waitForExtraTabs(tab, extra_tabs) {
585
+ function findTabs() {
586
+ const tab_elements = [];
587
+ for (const extra_tab of extra_tabs) {
588
+ const extra_tab_el = getModelCardsEl(tab, extra_tab);
589
+
590
+ if (extra_tab_el == null) {
591
+
592
+ // XXX lycoris models do not have their own tab in sdwebui 1.5
593
+ // most of the time. In the case that there is a LyCoris tab,
594
+ // it would have been added at the same time as the others,
595
+ // making it almost impossible to be null by the time we're at
596
+ // this point in the code if the other tabs are loaded.
597
+ if (extra_tab == 'lycoris') { continue; }
598
+
599
+ return null;
600
+ }
601
+
602
+ tab_elements.push(extra_tab_el);
603
+ }
604
+ return tab_elements;
605
+ }
606
+
607
+ const tab_elements = findTabs(tab, extra_tabs);
608
+ if (tab_elements) {
609
+ processCards(tab, tab_elements);
610
+ }
611
+
612
+ const observer = new MutationObserver(records => {
613
+ let tab_elements;
614
+ for (const record of records) {
615
+ if (record.type != "childList") {
616
+ continue;
617
+ }
618
+
619
+ tab_elements = findTabs(tab, extra_tabs);
620
+ if (!tab_elements) {
621
+ return;
622
+ }
623
+
624
+ processCards(tab, tab_elements);
625
+ return;
626
+ }
627
+ });
628
+
629
+ const extra_networks = getExtraTabs(tab);
630
+
631
+ const options = {
632
+ subtree: true,
633
+ childList: true,
634
+ };
635
+
636
+ observer.observe(extra_networks, options);
637
+
638
+ }
639
+
640
+
641
+ function waitForEditor(page, type, name) {
642
+ const id = page + '_' + type + '_edit_user_metadata';
643
+
644
+ return new Promise(resolve => {
645
+ let name_field;
646
+ const gradio = gradioApp();
647
 
648
+ const editor = gradio.getElementById(id);
649
+ const popup = gradio.querySelector(".global-popup");
650
+
651
+ if (popup != null) {
652
+ // hide the editor window so it doesn't get in the user's
653
+ // way while we wait for the replace preview functionality
654
+ // to become available.
655
+ popup.style.display = "none";
656
+ }
657
+
658
+ // not only do we need to wait for the editor,
659
+ // but also for it to populate with the model metadata.
660
+ if (editor != null) {
661
+ name_field = editor.querySelector('.extra-network-name');
662
+ if (name_field.textContent.trim() == name) {
663
+ return resolve(editor);
664
+ }
665
+ }
666
+
667
+ const observer = new MutationObserver(() => {
668
+ const editor = gradioApp().getElementById(id);
669
+ let name_field;
670
+ if (editor != null) {
671
+ name_field = editor.querySelector('.extra-network-name');
672
+ if (name_field.textContent.trim() == name) {
673
+ resolve(editor);
674
+ observer.disconnect();
675
+ }
676
+ }
677
+ });
678
+
679
+ observer.observe(document.body, {
680
+ subtree: true,
681
+ childList: true,
682
+ });
683
+ });
684
  }
685
 
686
 
687
+ function getShortModelTypeFromFull(model_type_full) {
688
+ switch (model_type_full) {
689
+ case "textual_inversion":
690
+ return "ti";
691
+ case "hypernetworks":
692
+ return "hyper";
693
+ case "checkpoints":
694
+ return "ckp";
695
+ case "lora":
696
+ case "lycoris":
697
+ return model_type_full;
698
+ }
699
+ }
700
+
701
+
702
+ function getLongModelTypeFromShort(model_type_short) {
703
+ switch (model_type_short) {
704
+ case "ti":
705
+ return "textual_inversion";
706
+ case "hyper":
707
+ return "hypernetworks";
708
+ case "ckp":
709
+ return "checkpoints";
710
+ case "lora":
711
+ case "lycoris":
712
+ return model_type_short;
713
+ }
714
+ }
715
+
716
+
717
+ function isThumbMode(extra_network_node) {
718
+ if (extra_network_node?.className == "extra-network-thumbs") {
719
+ return true;
720
+ }
721
+ return false;
722
+ }
723
+
724
+
725
+ function processSingleCard(active_tab_type, active_extra_tab_type, card) {
726
+ let metadata_button = null;
727
+ let additional_node = null;
728
+ let replace_preview_btn = null;
729
+ let ul_node = null;
730
+ let model_name = "";
731
+ let search_term = "";
732
+ let model_type = active_extra_tab_type;
733
+ let js_model_type = getLongModelTypeFromShort(model_type);
734
+ let addedNodes = [];
735
+
736
+ let is_thumb_mode = isThumbMode(getModelCardsEl(active_tab_type, js_model_type));
737
+
738
+ //metadata_buttoncard
739
+ metadata_button = card.querySelector(".metadata-button");
740
+ //additional node
741
+ additional_node = card.querySelector(".actions .additional");
742
+ //get ul node, which is the parent of all buttons
743
+ ul_node = card.querySelector(".actions .additional ul");
744
+
745
+ // check thumb mode
746
+ if (is_thumb_mode) {
747
+ additional_node.style.display = null;
748
+
749
+ if (!ul_node) {
750
+ // nothing to do.
751
+ return;
752
+ }
753
+
754
+ if (opts["ch_show_btn_on_thumb"]) {
755
+ ul_node.style.background = btn_thumb_background;
756
+ } else {
757
+ let ch_btn_txts = ["💡", "🌐", "🏷️", "✏️", "❌"];
758
+
759
+ // remove existed buttons
760
+ //reset
761
+ ul_node.style.background = null;
762
+ // find all .a child nodes
763
+ let atags = ul_node.querySelectorAll("a");
764
+
765
+ for (let atag of atags) {
766
+ //reset display
767
+ atag.style.display = null;
768
+ //remove extension's button
769
+ if (ch_btn_txts.indexOf(atag.textContent)>=0) {
770
+ //need to remove
771
+ ul_node.removeChild(atag);
772
+ } else {
773
+ //do not remove, just reset
774
+ atag.textContent = replace_preview_text;
775
+ atag.style.display = null;
776
+ atag.style.fontSize = null;
777
+ atag.style.position = null;
778
+ atag.style.backgroundImage = null;
779
+ }
780
+ }
781
+
782
+ //also remove br tag in ul
783
+ let brtag = ul_node.querySelector("br");
784
+ if (brtag) {
785
+ ul_node.removeChild(brtag);
786
+ }
787
+
788
+ //just reset and remove nodes, do nothing else
789
+ return;
790
+
791
+ }
792
+
793
+ } else {
794
+ // full preview mode
795
+
796
+ if (opts["ch_always_display"]) {
797
+ additional_node.style.display = "block";
798
+ } else {
799
+ additional_node.style.display = null;
800
+ }
801
+
802
+ if (!ul_node) {
803
+ ul_node = document.createElement("ul");
804
+ } else {
805
+ // remove br tag
806
+ let brtag = ul_node.querySelector("br");
807
+ if (brtag) {
808
+ ul_node.removeChild(brtag);
809
+ }
810
+ }
811
+
812
+ }
813
+
814
+ if (ul_node.dataset.ch_helper) {
815
+ return;
816
+ }
817
+
818
+ ul_node.dataset.ch_helper = true;
819
+
820
+ model_name = card.dataset.name;
821
+
822
+ // replace preview text button
823
+ replace_preview_btn = card.querySelector(".actions .additional a");
824
+
825
+ if ((replace_preview_btn == null) && !("replace_preview_button" in opts["ch_hide_buttons"])) {
826
+ /*
827
+ * in sdwebui 1.5, the replace preview button has been
828
+ * moved to a hard to reach location, so we have to do
829
+ * quite a lot to get to its functionality.
830
+ */
831
+
832
+ // waste memory by keeping all of this in scope, per card.
833
+ let page = active_tab_type;
834
+ let type = js_model_type;
835
+ let name = card.dataset.name.replace("'", "\\'");
836
+
837
+ // create the replace_preview_btn, as it no longer exists
838
+ replace_preview_btn = document.createElement("a");
839
+
840
+ // create an event handler to redirect a click to the real replace_preview_button
841
+ replace_preview_btn.setAttribute("onclick", `replace_preview(event, '${page}', '${type}', '${model_name}')`);
842
+ }
843
+
844
+ // change replace preview text button into icon
845
+ if (!opts["ch_hide_buttons"].includes("replace_preview_button")) {
846
+ if (replace_preview_btn.textContent !== "🖼️") {
847
+ replace_preview_btn.textContent = "🖼️";
848
+ addedNodes.push(replace_preview_btn);
849
+ }
850
+
851
+ replace_preview_btn.classList.add("card-button", "removecard");
852
+
853
+ } else if (replace_preview_btn.parentElement) {
854
+ replace_preview_btn.parentElement.removeChild(replace_preview_btn);
855
+ }
856
+
857
+ // search_term node
858
+ // search_term: /[subfolder path]/[model name].[ext] [hash]
859
+ // get search_term
860
+ let search_term_nodes = card.querySelectorAll(".actions .additional .search_term, .actions .additional .search_terms");
861
+ if (!search_term_nodes) {
862
+ console.log("can not find search_term node for cards in " + active_tab_type + "_" + active_extra_tab_type + "_cards");
863
+ return;
864
+ }
865
+
866
+ if (search_term_nodes.length > 1) {
867
+ let search_terms = [];
868
+ for (let search_term_node of search_term_nodes) {
869
+ search_terms.push(search_term_node.textContent);
870
+ }
871
+
872
+ let model_path = search_terms.join(" ");
873
+ let separator = model_path.match(/[\/\\]/)[0];
874
+ model_path = model_path.split(separator).slice(1).join(separator);
875
+
876
+ search_term = model_path;
877
+ } else {
878
+ let search_term_node = search_term_nodes[0];
879
+ search_term = search_term_node.textContent;
880
+
881
+ // for whatever reason, sometimes search_terms will not include hashes.
882
+ if (search_term_node.classList.contains("search_terms")) {
883
+ let separator = search_term.match(/[\/\\]/)[0];
884
+ search_term = search_term.split(separator).slice(1).join(
885
+ separator === "\\" ? "\\\\" : "/"
886
+ );
887
+ }
888
+ }
889
+
890
+ search_term = search_term.replaceAll("\\", "\\\\").replace("'", "\\'");
891
+
892
+ if (!search_term) {
893
+ console.log("search_term is empty for cards in " + active_tab_type + "_" + active_extra_tab_type + "_cards");
894
+ return;
895
+ }
896
+
897
+ // then we need to add buttons to each ul node:
898
+ if (!opts["ch_hide_buttons"].includes("open_url_button")) {
899
+ let open_url_node = document.createElement("a");
900
+ open_url_node.href = "#";
901
+ open_url_node.textContent = "🌐";
902
+ open_url_node.classList.add("card-button", "openurl");
903
+ open_url_node.title = "Open this model's civitai url";
904
+ open_url_node.setAttribute("onclick", `open_model_url(event, '${model_type}', '${search_term}')`);
905
+ addedNodes.push(open_url_node);
906
+ }
907
+
908
+ // add br if metadata_button exists
909
+ if (is_thumb_mode && metadata_button) {
910
+ addedNodes.push(document.createElement("br"));
911
+ }
912
+
913
+ if (!opts["ch_hide_buttons"].includes("add_trigger_words_button")) {
914
+ let add_trigger_words_node = document.createElement("a");
915
+ add_trigger_words_node.href = "#";
916
+ add_trigger_words_node.textContent = "💡";
917
+ add_trigger_words_node.classList.add("card-button", "addtriggerwords");
918
+ add_trigger_words_node.title = "Add trigger words to prompt";
919
+ add_trigger_words_node.setAttribute("onclick", `add_trigger_words(event, '${model_type}', '${search_term}')`);
920
+ addedNodes.push(add_trigger_words_node);
921
+ }
922
+
923
+ if (!opts["ch_hide_buttons"].includes("add_preview_prompt_button")) {
924
+ let use_preview_prompt_node = document.createElement("a");
925
+ use_preview_prompt_node.href = "#";
926
+ use_preview_prompt_node.textContent = "🏷️";
927
+ use_preview_prompt_node.classList.add("card-button", "usepreviewprompt");
928
+ use_preview_prompt_node.title = "Use prompt from preview image";
929
+ use_preview_prompt_node.setAttribute("onclick", `use_preview_prompt(event, '${model_type}', '${search_term}')`);
930
+ addedNodes.push(use_preview_prompt_node);
931
+ }
932
+
933
+ if (!opts["ch_hide_buttons"].includes("rename_model_button")) {
934
+ let rename_card_node = document.createElement("a");
935
+ rename_card_node.href = "#";
936
+ rename_card_node.innerHTML = "✏️";
937
+ rename_card_node.classList.add("card-button", "renamecard");
938
+ rename_card_node.title = "Rename this model";
939
+ rename_card_node.setAttribute("onclick", `rename_card(event, '${model_type}', '${search_term}', '${model_name}')`);
940
+ addedNodes.push(rename_card_node);
941
+ }
942
+
943
+ if (!opts["ch_hide_buttons"].includes("remove_model_button")) {
944
+ let remove_card_node = document.createElement("a");
945
+ remove_card_node.href = "#";
946
+ remove_card_node.innerHTML = "❌";
947
+ remove_card_node.classList.add("card-button", "removecard");
948
+ remove_card_node.title = "Remove this model";
949
+ remove_card_node.setAttribute("onclick", `remove_card(event, '${model_type}', '${search_term}')`);
950
+ addedNodes.push(remove_card_node);
951
+ }
952
+
953
+ // add to buttons row
954
+ for (const node of addedNodes) {
955
+ ul_node.appendChild(node);
956
+ }
957
+
958
+ // add buttons to card
959
+ if (!ul_node.parentElement && ul_node.children) {
960
+ additional_node.appendChild(ul_node);
961
+ }
962
+ }
963
+
964
  onUiLoaded(() => {
965
 
966
  //get gradio version
967
+ const gradio_ver = ch_gradio_version();
968
+ console.log("Running Stable-Diffusion-Webui-Civitai-Helper on Gradio Version: " + gradio_ver);
969
 
970
  // get all extra network tabs
971
+ const tab_prefix_list = ["txt2img", "img2img"];
972
+ const model_type_list = ["textual_inversion", "hypernetworks", "checkpoints", "lora", "lycoris"];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
973
 
974
  // update extra network tab pages' cards
975
  // * replace "replace preview" text button into an icon
 
980
  // notice: javascript can not get response from python side
981
  // so, these buttons just sent request to python
982
  // then, python side gonna open url and update prompt text box, without telling js side.
983
+ function update_card_for_civitai() {
984
+ if (!(opts && "ch_always_display" in opts)) {
985
+ // Lobe theme can cause a race condition.
986
+ return setTimeout(update_card_for_civitai, 500);
987
+ }
988
+
 
 
 
 
 
 
989
  let replace_preview_text = getTranslation("replace preview");
990
  if (!replace_preview_text) {
991
  replace_preview_text = "replace preview";
992
  }
993
 
 
 
 
994
  let extra_network_node = null;
 
 
 
995
  let model_type = "";
996
  let cards = null;
 
997
 
998
  //get current tab
999
  let active_tab_type = getActiveTabType();
1000
+ if (!active_tab_type) {active_tab_type = "txt2img";}
1001
 
1002
  for (const tab_prefix of tab_prefix_list) {
1003
  if (tab_prefix != active_tab_type) {continue;}
1004
 
 
 
 
 
 
 
1005
  //get active extratab
1006
+ const re = new RegExp(tab_prefix + "_(.+)_cards");
1007
  const active_extra_tab = Array.from(get_uiCurrentTabContent().querySelectorAll('.extra-network-cards,.extra-network-thumbs'))
1008
  .find(el => el.closest('.tabitem').style.display === 'block')
1009
+ ?.id.match(re)[1];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
 
1011
+ const active_extra_tab_type = getShortModelTypeFromFull(active_extra_tab);
1012
 
1013
  for (const js_model_type of model_type_list) {
1014
  //get model_type for python side
1015
+ model_type = getShortModelTypeFromFull(js_model_type);
 
 
 
 
 
 
 
 
 
 
 
 
 
1016
 
1017
  if (!model_type) {
1018
  console.log("can not get model_type from: " + js_model_type);
 
1025
  continue;
1026
  }
1027
 
1028
+ //console.log("handle active extra tab");
1029
+ extra_network_id = tab_prefix + "_" + js_model_type + "_cards";
1030
 
 
1031
  // console.log("searching extra_network_node: " + extra_network_id);
1032
+ extra_network_node = getModelCardsEl(tab_prefix, js_model_type);
 
 
1033
 
1034
  // get all card nodes
1035
  cards = extra_network_node.querySelectorAll(".card");
1036
+ for (const card of cards) {
1037
+ // don't let an issue with a single card kill functionality for following cards
1038
+ try {
1039
+ processSingleCard(active_tab_type, active_extra_tab_type, card);
1040
+ } catch(err) {
1041
+ console.log(err);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1042
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1043
  }
1044
 
 
1045
  }
 
1046
 
1047
+ }
1048
 
1049
  }
1050
 
1051
+ /*
 
 
 
1052
  let extra_network_refresh_btn = null;
1053
+ */
1054
+ let extra_networks_btn = null;
1055
+
1056
  //add refresh button to extra network's toolbar
1057
+ for (const prefix of tab_prefix_list) {
1058
+ // load extra networks button
1059
+ extra_networks_btn = gradioApp().getElementById(prefix + "_extra_networks");
1060
 
 
 
 
1061
 
1062
+ // pre-1.6
1063
+ if (extra_networks_btn) {
1064
+ function extraNetworksClick() {
1065
+ waitForExtraTabs(prefix, model_type_list);
1066
+ extra_networks_btn.removeEventListener("click", extraNetworksClick);
1067
+ }
1068
 
1069
+ // add listener to extra_networks_btn
1070
+ extra_networks_btn.addEventListener("click", extraNetworksClick);
1071
  continue;
1072
+
1073
  }
1074
 
1075
+ // 1.6 and higher
1076
+ const extra_tab = getExtraTabs(prefix);
1077
+ const headers = extra_tab.firstChild.children;
 
 
 
 
1078
 
1079
+ for (const header of headers) {
1080
+ const model_type = header.textContent.trim().replace(" ", "_").toLowerCase();
1081
 
1082
+ function extraNetworksClick() {
1083
+ waitForExtraTabs(prefix, [model_type]);
1084
+ header.removeEventListener("click", extraNetworksClick);
1085
+ }
1086
 
1087
+ header.addEventListener("click", extraNetworksClick);
1088
+ }
1089
+
1090
+ //get toolbar
1091
+ extra_networks_btn = gradioApp().getElementById(prefix + "_extra_networks");
1092
+
1093
+ }
1094
 
1095
  //run it once
1096
+ // update_card_for_civitai();
1097
 
1098
 
1099
  });
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/__pycache__/civitai_helper.cpython-310.pyc DELETED
Binary file (8.32 kB)
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/__init__.cpython-310.pyc DELETED
Binary file (186 Bytes)
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/civitai.cpython-310.pyc DELETED
Binary file (9.31 kB)
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/downloader.cpython-310.pyc DELETED
Binary file (2.58 kB)
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/js_action_civitai.cpython-310.pyc DELETED
Binary file (5.57 kB)
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/model.cpython-310.pyc DELETED
Binary file (3.52 kB)
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/model_action_civitai.cpython-310.pyc DELETED
Binary file (8.57 kB)
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/msg_handler.cpython-310.pyc DELETED
Binary file (1.22 kB)
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/setting.cpython-310.pyc DELETED
Binary file (1.64 kB)
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/__pycache__/util.cpython-310.pyc DELETED
Binary file (2.68 kB)
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/civitai.py CHANGED
@@ -1,617 +1,726 @@
1
- # -*- coding: UTF-8 -*-
2
- # handle msg between js and python side
 
 
3
  import os
4
- import time
5
- import json
6
  import re
7
- import requests
8
  from . import util
9
  from . import model
10
- from . import setting
11
 
12
- suffix = ".civitai"
13
 
14
- url_dict = {
15
  "modelPage":"https://civitai.com/models/",
16
  "modelId": "https://civitai.com/api/v1/models/",
17
  "modelVersionId": "https://civitai.com/api/v1/model-versions/",
18
  "hash": "https://civitai.com/api/v1/model-versions/by-hash/"
19
  }
20
 
21
- model_type_dict = {
22
  "Checkpoint": "ckp",
23
  "TextualInversion": "ti",
24
  "Hypernetwork": "hyper",
25
  "LORA": "lora",
26
- "LoCon": "lora",
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
 
 
30
 
31
- # get image with full size
32
- # width is in number, not string
33
- # return: url str
34
- def get_full_size_image_url(image_url, width):
35
- return re.sub('/width=\d+/', '/width=' + str(width) + '/', image_url)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
 
38
  # use this sha256 to get model info from civitai
39
  # return: model info dict
40
- def get_model_info_by_hash(hash:str):
 
 
 
 
 
 
 
 
41
  util.printD("Request model info from civitai")
42
 
43
- if not hash:
44
  util.printD("hash is empty")
45
- return
46
 
47
- r = requests.get(url_dict["hash"]+hash, headers=util.def_headers, proxies=util.proxies)
48
- if not r.ok:
49
- if r.status_code == 404:
50
- # this is not a civitai model
51
- util.printD("Civitai does not have this model")
52
- return {}
53
- else:
54
- util.printD("Get error code: " + str(r.status_code))
55
- util.printD(r.text)
56
- return
57
 
58
- # try to get content
59
- content = None
60
- try:
61
- content = r.json()
62
- except Exception as e:
63
- util.printD("Parse response json failed")
64
- util.printD(str(e))
65
- util.printD("response:")
66
- util.printD(r.text)
67
- return
68
-
69
  if not content:
70
- util.printD("error, content from civitai is None")
71
- return
72
-
 
73
  return content
74
 
75
 
 
 
 
 
 
76
 
77
- def get_model_info_by_id(id:str) -> dict:
78
- util.printD("Request model info from civitai")
79
 
80
- if not id:
81
- util.printD("id is empty")
82
- return
83
 
84
- r = requests.get(url_dict["modelId"]+str(id), headers=util.def_headers, proxies=util.proxies)
85
- if not r.ok:
86
- if r.status_code == 404:
87
- # this is not a civitai model
88
- util.printD("Civitai does not have this model")
89
- return {}
90
- else:
91
- util.printD("Get error code: " + str(r.status_code))
92
- util.printD(r.text)
93
- return
94
 
95
- # try to get content
96
- content = None
97
- try:
98
- content = r.json()
99
- except Exception as e:
100
- util.printD("Parse response json failed")
101
- util.printD(str(e))
102
- util.printD("response:")
103
- util.printD(r.text)
104
- return
105
-
106
- if not content:
107
- util.printD("error, content from civitai is None")
108
- return
109
-
110
  return content
111
 
112
 
113
- def get_version_info_by_version_id(id:str) -> dict:
 
 
 
 
114
  util.printD("Request version info from civitai")
115
 
116
- if not id:
117
- util.printD("id is empty")
118
- return
119
 
120
- r = requests.get(url_dict["modelVersionId"]+str(id), headers=util.def_headers, proxies=util.proxies)
121
- if not r.ok:
122
- if r.status_code == 404:
123
- # this is not a civitai model
124
- util.printD("Civitai does not have this model version")
125
- return {}
126
- else:
127
- util.printD("Get error code: " + str(r.status_code))
128
- util.printD(r.text)
129
- return
130
 
131
- # try to get content
132
- content = None
133
- try:
134
- content = r.json()
135
- except Exception as e:
136
- util.printD("Parse response json failed")
137
- util.printD(str(e))
138
- util.printD("response:")
139
- util.printD(r.text)
140
- return
141
-
142
- if not content:
143
- util.printD("error, content from civitai is None")
144
- return
145
-
146
  return content
147
 
148
 
149
- def get_version_info_by_model_id(id:str) -> dict:
 
 
 
 
150
 
151
- model_info = get_model_info_by_id(id)
152
  if not model_info:
153
- util.printD(f"Failed to get model info by id: {id}")
154
- return
155
-
156
  # check content to get version id
157
- if "modelVersions" not in model_info.keys():
158
- util.printD("There is no modelVersions in this model_info")
159
- return
160
-
161
- if not model_info["modelVersions"]:
162
- util.printD("modelVersions is None")
163
- return
164
-
165
- if len(model_info["modelVersions"])==0:
166
- util.printD("modelVersions is Empty")
167
- return
168
-
169
- def_version = model_info["modelVersions"][0]
170
  if not def_version:
171
  util.printD("default version is None")
172
- return
173
-
174
- if "id" not in def_version.keys():
175
- util.printD("default version has no id")
176
- return
177
-
178
- version_id = def_version["id"]
179
-
180
  if not version_id:
181
- util.printD("default version's id is None")
182
- return
183
 
184
  # get version info
185
- version_info = get_version_info_by_version_id(str(version_id))
186
  if not version_info:
187
  util.printD(f"Failed to get version info by version_id: {version_id}")
188
- return
189
 
190
  return version_info
191
 
192
 
193
-
194
-
195
- # get model info file's content by model type and search_term
196
- # parameter: model_type, search_term
197
- # return: model_info
198
  def load_model_info_by_search_term(model_type, search_term):
 
 
 
 
 
199
  util.printD(f"Load model info of {search_term} in {model_type}")
200
- if model_type not in model.folders.keys():
201
- util.printD("unknow model type: " + model_type)
202
- return
203
-
204
- # search_term = subfolderpath + model name + ext. And it always start with a / even there is no sub folder
205
- base, ext = os.path.splitext(search_term)
 
206
  model_info_base = base
207
  if base[:1] == "/":
208
  model_info_base = base[1:]
209
 
210
- model_folder = model.folders[model_type]
211
- model_info_filename = model_info_base + suffix + model.info_ext
212
- model_info_filepath = os.path.join(model_folder, model_info_filename)
 
213
 
214
- if not os.path.isfile(model_info_filepath):
215
- util.printD("Can not find model info file: " + model_info_filepath)
216
- return
217
-
218
- return model.load_model_info(model_info_filepath)
 
 
 
219
 
 
 
 
220
 
 
221
 
222
 
 
 
 
 
 
 
 
223
 
224
- # get model file names by model type
225
- # parameter: model_type - string
226
- # parameter: filter - dict, which kind of model you need
227
- # return: model name list
228
- def get_model_names_by_type_and_filter(model_type:str, filter:dict) -> list:
229
-
230
- model_folder = model.folders[model_type]
231
 
232
- # set filter
233
  # only get models don't have a civitai info file
234
  no_info_only = False
235
  empty_info_only = False
236
 
237
- if filter:
238
- if "no_info_only" in filter.keys():
239
- no_info_only = filter["no_info_only"]
240
- if "empty_info_only" in filter.keys():
241
- empty_info_only = filter["empty_info_only"]
242
-
243
-
244
 
245
  # get information from filter
246
  # only get those model names don't have a civitai model info file
247
  model_names = []
248
- for root, dirs, files in os.walk(model_folder, followlinks=True):
249
- for filename in files:
250
- item = os.path.join(root, filename)
251
- # check extension
252
- base, ext = os.path.splitext(item)
253
- if ext in model.exts:
254
- # find a model
255
-
256
- # check filter
257
- if no_info_only:
258
- # check model info file
259
- info_file = base + suffix + model.info_ext
260
- if os.path.isfile(info_file):
261
- continue
262
-
263
- if empty_info_only:
264
- # check model info file
265
- info_file = base + suffix + model.info_ext
266
- if os.path.isfile(info_file):
267
- # load model info
268
- model_info = model.load_model_info(info_file)
269
- # check content
270
- if model_info:
271
- if "id" in model_info.keys():
272
- # find a non-empty model info file
273
- continue
274
-
275
- model_names.append(filename)
276
-
277
 
278
  return model_names
279
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  def get_model_names_by_input(model_type, empty_info_only):
 
281
  return get_model_names_by_type_and_filter(model_type, {"empty_info_only":empty_info_only})
282
-
283
 
284
  # get id from url
285
  def get_model_id_from_url(url:str) -> str:
 
286
  util.printD("Run get_model_id_from_url")
287
- id = ""
288
 
289
  if not url:
290
  util.printD("url or model id can not be empty")
291
  return ""
292
 
293
  if url.isnumeric():
294
- # is already an id
295
- id = str(url)
296
- return id
297
-
298
- s = re.sub("\\?.+$", "", url).split("/")
299
- if len(s) < 2:
300
  util.printD("url is not valid")
301
  return ""
302
-
303
- if s[-2].isnumeric():
304
- id = s[-2]
305
- elif s[-1].isnumeric():
306
- id = s[-1]
307
  else:
308
  util.printD("There is no model id in this url")
309
  return ""
310
-
311
- return id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
 
314
  # get preview image by model path
315
  # image will be saved to file, so no return
316
- def get_preview_image_by_model_path(model_path:str, max_size_preview, skip_nsfw_preview):
 
 
 
 
 
 
317
  if not model_path:
318
  util.printD("model_path is empty")
319
  return
320
 
321
  if not os.path.isfile(model_path):
322
- util.printD("model_path is not a file: "+model_path)
323
  return
324
 
325
- base, ext = os.path.splitext(model_path)
326
- first_preview = base+".png"
327
- sec_preview = base+".preview.png"
328
- info_file = base + suffix + model.info_ext
329
-
330
- # check preview image
331
- if not os.path.isfile(sec_preview):
332
- # need to download preview image
333
- util.printD("Checking preview image for model: " + model_path)
334
- # load model_info file
335
- if os.path.isfile(info_file):
336
- model_info = model.load_model_info(info_file)
337
- if not model_info:
338
- util.printD("Model Info is empty")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  return
340
 
341
- if "images" in model_info.keys():
342
- if model_info["images"]:
343
- for img_dict in model_info["images"]:
344
- if "nsfw" in img_dict.keys():
345
- if img_dict["nsfw"] and img_dict["nsfw"] != "None":
346
- util.printD("This image is NSFW")
347
- if skip_nsfw_preview:
348
- util.printD("Skip NSFW image")
349
- continue
350
-
351
- preview_type = img_dict.get("type")
352
- if preview_type != "image":
353
- util.printD(f"Unsupported preview type: {preview_type}, ignore.")
354
- continue
355
-
356
- if "url" in img_dict.keys():
357
- img_url = img_dict["url"]
358
- if max_size_preview:
359
- # use max width
360
- if "width" in img_dict.keys():
361
- if img_dict["width"]:
362
- img_url = get_full_size_image_url(img_url, img_dict["width"])
363
-
364
- util.download_file(img_url, sec_preview)
365
- # we only need 1 preview image
366
- break
367
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
 
370
  # search local model by version id in 1 folder, no subfolder
371
  # return - model_info
372
  def search_local_model_info_by_version_id(folder:str, version_id:int) -> dict:
 
 
 
373
  util.printD("Searching local model by version id")
374
- util.printD("folder: " + folder)
375
- util.printD("version_id: " + str(version_id))
376
 
377
  if not folder:
378
  util.printD("folder is none")
379
- return
380
 
381
  if not os.path.isdir(folder):
382
  util.printD("folder is not a dir")
383
- return
384
-
385
  if not version_id:
386
  util.printD("version_id is none")
387
- return
388
-
389
  # search civitai model info file
390
  for filename in os.listdir(folder):
391
  # check ext
392
  base, ext = os.path.splitext(filename)
393
- if ext == model.info_ext:
394
  # find info file
395
- if len(base) < 9:
396
  # not a civitai info file
397
  continue
398
 
399
- if base[-8:] == suffix:
400
- # find a civitai info file
401
- path = os.path.join(folder, filename)
402
- model_info = model.load_model_info(path)
403
- if not model_info:
404
- continue
405
 
406
- if "id" not in model_info.keys():
407
- continue
 
408
 
409
- id = model_info["id"]
410
- if not id:
411
- continue
 
412
 
413
- # util.printD(f"Compare version id, src: {id}, target:{version_id}")
414
- if str(id) == str(version_id):
415
- # find the one
416
- return model_info
417
-
418
 
419
- return
420
 
 
 
 
 
 
421
 
 
 
422
 
 
 
 
 
423
 
 
 
424
 
425
- # check new version for a model by model path
426
- # return (model_path, model_id, model_name, new_verion_id, new_version_name, description, download_url, img_url)
427
- def check_model_new_version_by_path(model_path:str, delay:float=1) -> tuple:
428
- if not model_path:
429
- util.printD("model_path is empty")
430
- return
431
 
432
- if not os.path.isfile(model_path):
433
- util.printD("model_path is not a file: "+model_path)
434
- return
435
-
436
- # get model info file name
437
- base, ext = os.path.splitext(model_path)
438
- info_file = base + suffix + model.info_ext
439
-
440
- if not os.path.isfile(info_file):
441
- return
442
-
443
- # get model info
444
- model_info_file = model.load_model_info(info_file)
445
- if not model_info_file:
446
- return
447
 
448
- if "id" not in model_info_file.keys():
449
- return
450
-
451
- local_version_id = model_info_file["id"]
452
- if not local_version_id:
453
- return
 
 
 
 
 
 
 
 
 
 
 
 
454
 
455
- if "modelId" not in model_info_file.keys():
456
- return
457
-
458
- model_id = model_info_file["modelId"]
459
- if not model_id:
460
- return
461
-
462
  # get model info by id from civitai
463
  model_info = get_model_info_by_id(model_id)
464
- # delay before next request, to prevent to be treat as DDoS
465
- util.printD(f"delay:{delay} second")
466
- time.sleep(delay)
467
 
468
  if not model_info:
469
- return
470
-
471
- if "modelVersions" not in model_info.keys():
472
- return
473
-
474
- modelVersions = model_info["modelVersions"]
475
- if not modelVersions:
476
- return
477
-
478
- if not len(modelVersions):
479
- return
480
-
481
- current_version = modelVersions[0]
482
  if not current_version:
483
- return
484
-
485
- if "id" not in current_version.keys():
486
- return
487
-
488
- current_version_id = current_version["id"]
489
- if not current_version_id:
490
- return
491
 
492
- util.printD(f"Compare version id, local: {local_version_id}, remote: {current_version_id} ")
493
- if current_version_id == local_version_id:
494
- return
 
 
 
495
 
496
- model_name = ""
497
- if "name" in model_info.keys():
498
- model_name = model_info["name"]
499
-
500
- if not model_name:
501
- model_name = ""
502
-
503
-
504
- new_version_name = ""
505
- if "name" in current_version.keys():
506
- new_version_name = current_version["name"]
507
-
508
- if not new_version_name:
509
- new_version_name = ""
510
-
511
- description = ""
512
- if "description" in current_version.keys():
513
- description = current_version["description"]
514
-
515
- if not description:
516
- description = ""
517
-
518
- downloadUrl = ""
519
- if "downloadUrl" in current_version.keys():
520
- downloadUrl = current_version["downloadUrl"]
521
-
522
- if not downloadUrl:
523
- downloadUrl = ""
524
 
525
  # get 1 preview image
526
- img_url = ""
527
- if "images" in current_version.keys():
528
- if current_version["images"]:
529
- if current_version["images"][0]:
530
- if "url" in current_version["images"][0].keys():
531
- img_url = current_version["images"][0]["url"]
532
- if not img_url:
533
- img_url = ""
 
 
534
 
 
 
 
 
 
 
 
535
 
536
-
537
- return (model_path, model_id, model_name, current_version_id, new_version_name, description, downloadUrl, img_url)
538
 
 
 
539
 
 
 
540
 
 
541
 
542
- # check model's new version
543
- # parameter: delay - float, how many seconds to delay between each request to civitai
544
- # return: new_versions - a list for all new versions, each one is (model_path, model_id, model_name, new_verion_id, new_version_name, description, download_url, img_url)
545
- def check_models_new_version_by_model_types(model_types:list, delay:float=1) -> list:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
  util.printD("Checking models' new version")
547
 
548
  if not model_types:
549
  return []
550
 
551
- # check model types, which cloud be a string as 1 type
552
  mts = []
553
- if type(model_types) == str:
554
  mts.append(model_types)
555
- elif type(model_types) == list:
556
  mts = model_types
557
  else:
558
- util.printD("Unknow model types:")
559
  util.printD(model_types)
560
  return []
561
 
562
- # output is a markdown document string to show a list of new versions on UI
563
- output = ""
564
  # new version list
565
  new_versions = []
 
566
 
567
  # walk all models
568
  for model_type, model_folder in model.folders.items():
569
  if model_type not in mts:
570
  continue
571
 
572
- util.printD("Scanning path: " + model_folder)
573
- for root, dirs, files in os.walk(model_folder, followlinks=True):
574
  for filename in files:
575
- # check ext
576
- item = os.path.join(root, filename)
577
- base, ext = os.path.splitext(item)
578
- if ext in model.exts:
579
- # find a model
580
- r = check_model_new_version_by_path(item, delay)
581
-
582
- if not r:
583
- continue
584
-
585
- model_path, model_id, model_name, current_version_id, new_version_name, description, downloadUrl, img_url = r
586
- # check exist
587
- if not current_version_id:
588
- continue
589
-
590
- # check this version id in list
591
- is_already_in_list = False
592
- for new_version in new_versions:
593
- if current_version_id == new_version[3]:
594
- # already in list
595
- is_already_in_list = True
596
- break
597
-
598
- if is_already_in_list:
599
- util.printD("New version is already in list")
600
- continue
601
 
602
- # search this new version id to check if this model is already downloaded
603
- target_model_info = search_local_model_info_by_version_id(root, current_version_id)
604
- if target_model_info:
605
- util.printD("New version is already existed")
606
- continue
607
-
608
- # add to list
609
- new_versions.append(r)
610
 
 
 
611
 
 
 
612
 
 
 
 
613
 
614
  return new_versions
615
-
616
-
617
-
 
1
+ """ -*- coding: UTF-8 -*-
2
+ handle msg between js and python side
3
+ """
4
+
5
  import os
 
 
6
  import re
 
7
  from . import util
8
  from . import model
9
+ from . import downloader
10
 
11
+ SUFFIX = ".civitai"
12
 
13
+ URLS = {
14
  "modelPage":"https://civitai.com/models/",
15
  "modelId": "https://civitai.com/api/v1/models/",
16
  "modelVersionId": "https://civitai.com/api/v1/model-versions/",
17
  "hash": "https://civitai.com/api/v1/model-versions/by-hash/"
18
  }
19
 
20
+ MODEL_TYPES = {
21
  "Checkpoint": "ckp",
22
  "TextualInversion": "ti",
23
  "Hypernetwork": "hyper",
24
  "LORA": "lora",
25
+ "LoCon": "lycoris",
26
  }
27
 
28
+ FILE_TYPES = [
29
+ "Model", "Training Data", "Config", "VAE"
30
+ ]
31
+
32
+ NSFW_LEVELS = ["None", "Soft", "Mature", "X", "Allow All"]
33
+
34
+ def civitai_get(civitai_url:str):
35
+ """
36
+ Gets JSON from Civitai.
37
+ return: dict:json or None
38
+ """
39
+
40
+ success, response = downloader.request_get(
41
+ civitai_url
42
+ )
43
+
44
+ if not success:
45
+ return None
46
+
47
+ # try to get content
48
+ content = None
49
+ try:
50
+ content = response.json()
51
+ except ValueError as e:
52
+ util.printD(util.indented_msg(
53
+ f"""
54
+ Parse response json failed
55
+ Error: {str(e)}
56
+ Response: {response.text}
57
+ """
58
+ ))
59
+ return None
60
+
61
+ return content
62
+
63
+
64
+ def append_parent_model_metadata(content):
65
+ """
66
+ Some model metadata is stored in a "parent" context.
67
+ When we're fething a model by its hash, we're getting
68
+ the metadata for that model *file*, not the model entry
69
+ on Civitai, which may contain multiple versions.
70
 
71
+ This method gets the parent metadata and appends it to
72
+ our model file metadata.
73
 
74
+ return: model metadata with parent description, creator,
75
+ and permissions appended.
76
+ """
77
+ util.printD("Fetching Parent Model Information")
78
+ parent_model = get_model_info_by_id(content["modelId"])
79
+
80
+ metadatas = [
81
+ "description", "tags", "allowNoCredit",
82
+ "allowCommercialUse", "allowDerivatives",
83
+ "allowDifferentLicense"
84
+ ]
85
+
86
+ content["creator"] = parent_model.get("creator", "{}")
87
+
88
+ model_metadata = content["model"]
89
+ for metadata in metadatas:
90
+ model_metadata[metadata] = parent_model.get(metadata, "")
91
+
92
+ return content
93
 
94
 
95
  # use this sha256 to get model info from civitai
96
  # return: model info dict
97
+ def get_model_info_by_hash(model_hash:str):
98
+ """
99
+ use this sha256 to get model info from civitai's api
100
+
101
+ return:
102
+ model info dict if a model is found
103
+ {} if civitai does not have the model
104
+ None if an error occurs.
105
+ """
106
  util.printD("Request model info from civitai")
107
 
108
+ if not model_hash:
109
  util.printD("hash is empty")
110
+ return None
111
 
112
+ content = civitai_get(f'{URLS["hash"]}{model_hash}')
 
 
 
 
 
 
 
 
 
113
 
 
 
 
 
 
 
 
 
 
 
 
114
  if not content:
115
+ return None
116
+
117
+ content = append_parent_model_metadata(content)
118
+
119
  return content
120
 
121
 
122
+ def get_model_info_by_id(model_id:str) -> dict:
123
+ """
124
+ Fetches model info by its model id.
125
+ returns: dict:model_info
126
+ """
127
 
128
+ util.printD(f"Request model info from civitai: {model_id}")
 
129
 
130
+ if not model_id:
131
+ util.printD("model_id is empty")
132
+ return False
133
 
134
+ content = civitai_get(f'{URLS["modelId"]}{model_id}')
 
 
 
 
 
 
 
 
 
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  return content
137
 
138
 
139
+ def get_version_info_by_version_id(version_id:str) -> dict:
140
+ """
141
+ Gets model version info from Civitai by version id
142
+ return: dict:model_info
143
+ """
144
  util.printD("Request version info from civitai")
145
 
146
+ if not version_id:
147
+ util.printD("version_id is empty")
148
+ return None
149
 
150
+ content = civitai_get(f'{URLS["modelVersionId"]}{version_id}')
151
+
152
+ if content:
153
+ content = append_parent_model_metadata(content)
 
 
 
 
 
 
154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  return content
156
 
157
 
158
+ def get_version_info_by_model_id(model_id:str) -> dict:
159
+ """
160
+ Fetches version info by model id.
161
+ returns: dict:version_info
162
+ """
163
 
164
+ model_info = get_model_info_by_id(model_id)
165
  if not model_info:
166
+ util.printD(f"Failed to get model info by id: {model_id}")
167
+ return None
168
+
169
  # check content to get version id
170
+ versions = model_info.get("modelVersions", [])
171
+ if len(versions) == 0:
172
+ util.printD("Found no model versions")
173
+ return None
174
+
175
+ def_version = versions[0]
 
 
 
 
 
 
 
176
  if not def_version:
177
  util.printD("default version is None")
178
+ return None
179
+
180
+ version_id = def_version.get("id", "")
181
+
 
 
 
 
182
  if not version_id:
183
+ util.printD("Could not get valid version id")
184
+ return None
185
 
186
  # get version info
187
+ version_info = get_version_info_by_version_id(f"{version_id}")
188
  if not version_info:
189
  util.printD(f"Failed to get version info by version_id: {version_id}")
190
+ return None
191
 
192
  return version_info
193
 
194
 
 
 
 
 
 
195
  def load_model_info_by_search_term(model_type, search_term):
196
+ """
197
+ get model info file's content by model type and search_term
198
+ parameter: model_type, search_term
199
+ return: model_info
200
+ """
201
  util.printD(f"Load model info of {search_term} in {model_type}")
202
+ if model.folders.get(model_type, None) is None:
203
+ util.printD(f"unknown model type: {model_type}")
204
+ return None
205
+
206
+ # search_term = f"{subfolderpath}{model name}{ext}"
207
+ # And it always start with a / even when there is no sub folder
208
+ base, _ = os.path.splitext(search_term)
209
  model_info_base = base
210
  if base[:1] == "/":
211
  model_info_base = base[1:]
212
 
213
+ if model_type == "lora" and model.folders['lycoris']:
214
+ model_folders = [model.folders[model_type], model.folders['lycoris']]
215
+ else:
216
+ model_folders = [model.folders[model_type]]
217
 
218
+ for model_folder in model_folders:
219
+ model_info_filename = f"{model_info_base}{SUFFIX}{model.CIVITAI_EXT}"
220
+ model_info_filepath = os.path.join(model_folder, model_info_filename)
221
+
222
+ found = os.path.isfile(model_info_filepath)
223
+
224
+ if found:
225
+ break
226
 
227
+ if not found:
228
+ util.printD(f"Can not find model info file: {model_info_filepath}")
229
+ return None
230
 
231
+ return model.load_model_info(model_info_filepath)
232
 
233
 
234
+ def get_model_names_by_type_and_filter(model_type:str, metadata_filter:dict) -> list:
235
+ """
236
+ get model file names by model type
237
+ parameter: model_type - string
238
+ parameter: filter - dict, which kind of model you need
239
+ return: model name list
240
+ """
241
 
242
+ if model_type == "lora" and model.folders['lycoris']:
243
+ model_folders = [model.folders[model_type], model.folders['lycoris']]
244
+ else:
245
+ model_folders = [model.folders[model_type]]
 
 
 
246
 
247
+ # set metadata_filter
248
  # only get models don't have a civitai info file
249
  no_info_only = False
250
  empty_info_only = False
251
 
252
+ if metadata_filter:
253
+ no_info_only = metadata_filter.get("no_info_only", False)
254
+ empty_info_only = metadata_filter.get("empty_info_only", False)
 
 
 
 
255
 
256
  # get information from filter
257
  # only get those model names don't have a civitai model info file
258
  model_names = []
259
+ for model_folder in model_folders:
260
+ for root, _, files in os.walk(model_folder, followlinks=True):
261
+ for filename in files:
262
+ if is_valid_file(root, filename, no_info_only, empty_info_only):
263
+ model_names.append(filename)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
  return model_names
266
 
267
+
268
+ def is_valid_file(root, filename, no_info_only, empty_info_only):
269
+ """
270
+ Filters through model files to determine if they are
271
+ valid targets for downloading new metadata.
272
+
273
+ return: bool
274
+ """
275
+ item = os.path.join(root, filename)
276
+ # check extension
277
+ base, ext = os.path.splitext(item)
278
+ if ext not in model.EXTS:
279
+ return False
280
+
281
+ # find a model
282
+ info_file = f"{base}{SUFFIX}{model.CIVITAI_EXT}"
283
+
284
+ # check filter
285
+ if os.path.isfile(info_file):
286
+ if no_info_only:
287
+ return False
288
+
289
+ if empty_info_only:
290
+ # load model info
291
+ model_info = model.load_model_info(info_file)
292
+ # check content
293
+ if model_info and not model_info.get("id", "") == "":
294
+ # find a non-empty model info file
295
+ return False
296
+
297
+ return True
298
+
299
+
300
  def get_model_names_by_input(model_type, empty_info_only):
301
+ """ return: list of model filenames with empty civitai info files """
302
  return get_model_names_by_type_and_filter(model_type, {"empty_info_only":empty_info_only})
303
+
304
 
305
  # get id from url
306
  def get_model_id_from_url(url:str) -> str:
307
+ """ return: model_id from civitai url """
308
  util.printD("Run get_model_id_from_url")
309
+ model_id = ""
310
 
311
  if not url:
312
  util.printD("url or model id can not be empty")
313
  return ""
314
 
315
  if url.isnumeric():
316
+ # is already an model_id
317
+ model_id = f"{url}"
318
+ return model_id
319
+
320
+ split_url = re.sub("\\?.+$", "", url).split("/")
321
+ if len(split_url) < 2:
322
  util.printD("url is not valid")
323
  return ""
324
+
325
+ if split_url[-2].isnumeric():
326
+ model_id = split_url[-2]
327
+ elif split_url[-1].isnumeric():
328
+ model_id = split_url[-1]
329
  else:
330
  util.printD("There is no model id in this url")
331
  return ""
332
+
333
+ return model_id
334
+
335
+
336
+ def preview_exists(model_path):
337
+ """ Search for existing preview image. return True if it exists, else false """
338
+
339
+ previews = model.get_potential_model_preview_files(model_path)
340
+
341
+ for prev in previews:
342
+ if os.path.isfile(prev):
343
+ return True
344
+
345
+ return False
346
+
347
+
348
+ def should_skip(user_rating, image_rating):
349
+ """ return: True if preview_nsfw level higher than user threshold """
350
+ order = NSFW_LEVELS
351
+ if user_rating == "Skip":
352
+ # Old config
353
+ return False
354
+ if isinstance(image_rating, bool):
355
+ # Image using old NSFW system?
356
+ if image_rating:
357
+ image_rating = order[-1]
358
+ else:
359
+ image_rating = order[0]
360
+ return order.index(image_rating) >= order.index(user_rating)
361
+
362
+
363
+ def show_only_nsfw(user_rating, image_rating):
364
+ """ return: True if image is NSFW """
365
+ order = NSFW_LEVELS
366
+ if user_rating == "Skip":
367
+ # Old config
368
+ return True
369
+ return order.index(image_rating) <= order.index(user_rating)
370
+
371
+
372
+ def get_image_url(img_dict, max_size_preview):
373
+ """
374
+ Create the image download URL
375
+
376
+ return: url:str
377
+ """
378
+
379
+ url = img_dict["url"]
380
+ if max_size_preview:
381
+ # use max width
382
+ width = img_dict.get("width", False)
383
+ if width:
384
+ url = re.sub(r'/width=\d+/', '/width=' + str(width) + '/', url)
385
+
386
+ return url
387
+
388
+
389
+ def verify_preview(path, img_dict, max_size_preview, nsfw_preview_threshold):
390
+ """
391
+ Downloads a preview image if it meets the user's requirements.
392
+ """
393
+
394
+ img_url = img_dict.get("url", None)
395
+ if img_url is None:
396
+ yield (False, None)
397
+
398
+ image_rating = img_dict.get("nsfw", "None")
399
+ if image_rating != "None":
400
+ util.printD(f"This image is NSFW: {image_rating}")
401
+ if should_skip(nsfw_preview_threshold, image_rating):
402
+ util.printD("Skip NSFW image")
403
+ yield (False, None)
404
+
405
+ preview_type = img_dict.get("type")
406
+ if preview_type != "image":
407
+ util.printD(f"Preview is not an image. Found {preview_type} instead. Skipping.")
408
+ yield (False, None)
409
+
410
+ img_url = get_image_url(img_dict, max_size_preview)
411
+
412
+ success = False
413
+ preview_path = ""
414
+ for result in downloader.dl_file(img_url, file_path=path):
415
+ if not isinstance(result, str):
416
+ success, preview_path = result
417
+ break
418
+
419
+ yield result
420
+
421
+
422
+ if not success:
423
+ yield (False, None)
424
+
425
+ # we only need 1 preview image
426
+ yield (True, preview_path)
427
 
428
 
429
  # get preview image by model path
430
  # image will be saved to file, so no return
431
+ def get_preview_image_by_model_path(model_path:str, max_size_preview, nsfw_preview_threshold, preferred_preview=None):
432
+ """
433
+ Downloads a preview image for a model if one doesn't already exist.
434
+ Skips images that are more NSFW than the user's NSFW threshold
435
+ """
436
+ util.printD("Downloading model image.")
437
+
438
  if not model_path:
439
  util.printD("model_path is empty")
440
  return
441
 
442
  if not os.path.isfile(model_path):
443
+ util.printD(f"model_path is not a file: {model_path}")
444
  return
445
 
446
+ base, _ = os.path.splitext(model_path)
447
+ preview_path = f"{base}.preview.png" # TODO png not strictly required
448
+ info_file = f"{base}{SUFFIX}{model.CIVITAI_EXT}"
449
+
450
+ # need to download preview image
451
+ util.printD(f"Checking preview image for model: {model_path}")
452
+
453
+ if preview_exists(model_path):
454
+ output = "Existing model image found. Skipping."
455
+ util.printD(output)
456
+ yield output
457
+ return
458
+
459
+ # load model_info file
460
+ if not os.path.isfile(info_file):
461
+ return
462
+
463
+ try:
464
+ images = model.load_model_info(info_file)["images"]
465
+
466
+ except (KeyError, TypeError):
467
+ return
468
+
469
+ if preferred_preview:
470
+ img_url = preferred_preview
471
+ for img_dict in images:
472
+ if img_dict["url"] == preferred_preview:
473
+ img_url = get_image_url(img_dict, max_size_preview)
474
+ break
475
+
476
+ for result in downloader.dl_file(img_url, file_path=preview_path):
477
+ if isinstance(result, str):
478
+ yield result
479
+ continue
480
+
481
+ success, msg = result
482
+
483
+ if success:
484
  return
485
 
486
+ util.printD(msg)
487
+ util.printD("Failed to download preferred preview. Trying to find another")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
 
489
+ break
490
+
491
+ for img_dict in images:
492
+ for result in verify_preview(
493
+ preview_path, img_dict, max_size_preview, nsfw_preview_threshold
494
+ ):
495
+ if not isinstance(result, str):
496
+ success, _ = result
497
+ # Only download one image
498
+ if success:
499
+ return
500
+
501
+ break
502
+
503
+ yield result
504
+
505
+ util.printD(f"Could not find any valid preview images for model: {model_path}")
506
+ yield
507
 
508
 
509
  # search local model by version id in 1 folder, no subfolder
510
  # return - model_info
511
  def search_local_model_info_by_version_id(folder:str, version_id:int) -> dict:
512
+ """ Searches a folder for model_info files,
513
+ returns the model_info from a file if its id matches the model id.
514
+ """
515
  util.printD("Searching local model by version id")
516
+ util.printD(f"folder: {folder}")
517
+ util.printD(f"version_id: {version_id}")
518
 
519
  if not folder:
520
  util.printD("folder is none")
521
+ return None
522
 
523
  if not os.path.isdir(folder):
524
  util.printD("folder is not a dir")
525
+ return None
526
+
527
  if not version_id:
528
  util.printD("version_id is none")
529
+ return None
530
+
531
  # search civitai model info file
532
  for filename in os.listdir(folder):
533
  # check ext
534
  base, ext = os.path.splitext(filename)
535
+ if ext == model.CIVITAI_EXT:
536
  # find info file
537
+ if not (len(base) > 8 and base[-8:] == SUFFIX):
538
  # not a civitai info file
539
  continue
540
 
541
+ # find a civitai info file
542
+ path = os.path.join(folder, filename)
543
+ model_info = model.load_model_info(path)
544
+ if not model_info:
545
+ continue
 
546
 
547
+ model_id = model_info.get("id", None)
548
+ if not model_id:
549
+ continue
550
 
551
+ # util.printD(f"Compare version id, src: {model_id}, target:{version_id}")
552
+ if f"{model_id}" == f"{version_id}":
553
+ # find the one
554
+ return model_info
555
 
556
+ return None
 
 
 
 
557
 
 
558
 
559
+ def get_model_id_from_model_path(model_path:str):
560
+ """ return model_id using model_path """
561
+ # get model info file name
562
+ base, _ = os.path.splitext(model_path)
563
+ info_file = f"{base}{SUFFIX}{model.CIVITAI_EXT}"
564
 
565
+ if not os.path.isfile(info_file):
566
+ return None
567
 
568
+ # get model info
569
+ model_info_file = model.load_model_info(info_file)
570
+ local_version_id = model_info_file.get("id", None)
571
+ model_id = model_info_file.get("modelId", None)
572
 
573
+ if None in [model_id, local_version_id]:
574
+ return None
575
 
576
+ return (model_id, local_version_id)
 
 
 
 
 
577
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
 
579
+ def check_model_new_version_by_path(model_path:str, delay:float=0.2) -> tuple:
580
+ """
581
+ check new version for a model by model path
582
+ return (
583
+ model_path, model_id, model_name, new_verion_id,
584
+ new_version_name, description, download_url, img_url
585
+ )
586
+ """
587
+
588
+ if not (model_path and os.path.isfile(model_path)):
589
+ util.printD(f"model_path is not a file: {model_path}")
590
+ return None
591
+
592
+ result = get_model_id_from_model_path(model_path)
593
+ if not result:
594
+ return None
595
+
596
+ model_id, local_version_id = result
597
 
 
 
 
 
 
 
 
598
  # get model info by id from civitai
599
  model_info = get_model_info_by_id(model_id)
600
+
601
+ util.delay(delay)
 
602
 
603
  if not model_info:
604
+ return None
605
+
606
+ model_versions = model_info.get("modelVersions", [])
607
+
608
+ if len(model_versions) == 0:
609
+ return None
610
+
611
+ current_version = model_versions[0]
 
 
 
 
 
612
  if not current_version:
613
+ return None
 
 
 
 
 
 
 
614
 
615
+ current_version_id = current_version.get("id", False)
616
+
617
+ util.printD(f"Compare version id, local: {local_version_id}, remote: {current_version_id}")
618
+
619
+ if not (current_version_id and current_version_id != local_version_id):
620
+ return None
621
 
622
+ model_name = model_info.get("name", "")
623
+ new_version_name = current_version.get("name", "")
624
+ description = current_version.get("description", "")
625
+ download_url = current_version.get("downloadUrl", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
 
627
  # get 1 preview image
628
+ try:
629
+ img_url = current_version["images"][0]["url"]
630
+ except (IndexError, KeyError):
631
+ img_url = ""
632
+
633
+ return (
634
+ model_path, model_id, model_name, current_version_id,
635
+ new_version_name, description, download_url, img_url
636
+ )
637
+
638
 
639
+ def check_single_model_new_version(root, filename, model_type, delay):
640
+ """
641
+ return: True if a valid model has a new version.
642
+ """
643
+ # check ext
644
+ item = os.path.join(root, filename)
645
+ _, ext = os.path.splitext(item)
646
 
647
+ if ext not in model.EXTS:
648
+ return False
649
 
650
+ # find a model
651
+ request = check_model_new_version_by_path(item, delay)
652
 
653
+ if not request:
654
+ return False
655
 
656
+ request = request + (model_type,)
657
 
658
+ # model_path, model_id, model_name, version_id, new_version_name, description, downloadUrl, img_url = request
659
+ version_id = request[3]
660
+
661
+ # check exist
662
+ if not version_id:
663
+ return False
664
+
665
+ # search this new version id to check if this model is already downloaded
666
+ target_model_info = search_local_model_info_by_version_id(root, version_id)
667
+ if target_model_info:
668
+ util.printD("New version is already exists")
669
+ return False
670
+
671
+ return request
672
+
673
+
674
+ def check_models_new_version_by_model_types(model_types:list, delay:float=0.2) -> list:
675
+ """
676
+ check all models of model_types for new version
677
+ parameter: delay - float, how many seconds to delay between each request to civitai
678
+ return: new_versions
679
+ a list for all new versions, each one is
680
+ (model_path, model_id, model_name, new_verion_id,
681
+ new_version_name, description, download_url, img_url)
682
+ """
683
  util.printD("Checking models' new version")
684
 
685
  if not model_types:
686
  return []
687
 
688
+ # check model types, which could be a string as 1 type
689
  mts = []
690
+ if isinstance(model_types, str):
691
  mts.append(model_types)
692
+ elif isinstance(model_types, list):
693
  mts = model_types
694
  else:
695
+ util.printD("Unknown model types:")
696
  util.printD(model_types)
697
  return []
698
 
 
 
699
  # new version list
700
  new_versions = []
701
+ new_version_ids = []
702
 
703
  # walk all models
704
  for model_type, model_folder in model.folders.items():
705
  if model_type not in mts:
706
  continue
707
 
708
+ util.printD(f"Scanning path: {model_folder}")
709
+ for root, _, files in os.walk(model_folder, followlinks=True):
710
  for filename in files:
711
+ version = check_single_model_new_version(root, filename, model_type, delay)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
 
713
+ if not version:
714
+ continue
 
 
 
 
 
 
715
 
716
+ # model_path, model_id, model_name, version_id, new_version_name, description, downloadUrl, img_url = version
717
+ version_id = version[3]
718
 
719
+ if version_id in new_version_ids:
720
+ continue
721
 
722
+ # add to list
723
+ new_versions.append(version)
724
+ new_version_ids.append(version_id)
725
 
726
  return new_versions
 
 
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/downloader.py CHANGED
@@ -1,125 +1,368 @@
1
- # -*- coding: UTF-8 -*-
2
- import sys
3
- import requests
 
 
4
  import os
 
 
 
 
 
 
5
  from . import util
6
 
7
 
8
- dl_ext = ".downloading"
 
9
 
10
  # disable ssl warning info
11
- requests.packages.urllib3.disable_warnings()
12
 
13
- # output is downloaded file path
14
- def dl(url, folder, filename, filepath):
15
- util.printD("Start downloading from: " + url)
16
- # get file_path
17
- file_path = ""
18
- if filepath:
19
- file_path = filepath
20
- else:
21
- # if file_path is not in parameter, then folder must be in parameter
22
- if not folder:
23
- util.printD("folder is none")
24
- return
25
 
26
- if not os.path.isdir(folder):
27
- util.printD("folder does not exist: "+folder)
28
- return
 
 
 
 
29
 
30
- if filename:
31
- file_path = os.path.join(folder, filename)
32
 
33
- # first request for header
34
- rh = requests.get(url, stream=True, verify=False, headers=util.def_headers, proxies=util.proxies)
35
- # get file size
36
- total_size = int(rh.headers.get('Content-Length', -1))
37
- if (total_size < 0):
38
- util.printD('This model requires an API key to download. More info: https://github.com/butaixianran/Stable-Diffusion-Webui-Civitai-Helper#civitai-api-key')
39
- return
40
- util.printD(f"File size: {total_size}")
41
-
42
- # if file_path is empty, need to get file name from download url's header
43
- if not file_path:
44
- filename = ""
45
- if "Content-Disposition" in rh.headers.keys():
46
- # headers default is decoded with latin1, so need to re-decode it with utf-8
47
- cd = rh.headers["Content-Disposition"].encode('latin1').decode('utf-8', errors='ignore')
48
- # Extract the filename from the header
49
- # content of a CD: "attachment;filename=FileName.txt"
50
- # in case "" is in CD filename's start and end, need to strip them out
51
- filename = cd.split("=")[1].strip('"')
52
- if not filename:
53
- util.printD("Fail to get file name from Content-Disposition: " + cd)
54
- return
55
 
56
- if not filename:
57
- util.printD("Can not get file name from download url's header")
58
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- # with folder and filename, now we have the full file path
61
- file_path = os.path.join(folder, filename)
62
 
 
 
 
 
 
63
 
64
- util.printD("Target file path: " + file_path)
65
- base, ext = os.path.splitext(file_path)
66
 
67
- # check if file is already exist
68
- count = 2
69
- new_base = base
70
- while os.path.isfile(file_path):
71
- util.printD("Target file already exist.")
72
- # re-name
73
- new_base = base + "_" + str(count)
74
- file_path = new_base + ext
75
- count += 1
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  # use a temp file for downloading
78
- dl_file_path = new_base+dl_ext
79
 
 
 
 
 
80
 
81
- util.printD(f"Downloading to temp file: {dl_file_path}")
82
 
83
- # check if downloading file is exsited
84
  downloaded_size = 0
85
- if os.path.exists(dl_file_path):
86
- downloaded_size = os.path.getsize(dl_file_path)
 
 
 
 
 
 
 
 
87
 
88
- util.printD(f"Downloaded size: {downloaded_size}")
 
 
 
 
89
 
90
- # create header range
91
- headers = {'Range': 'bytes=%d-' % downloaded_size}
92
- headers['User-Agent'] = util.def_headers['User-Agent']
93
- if util.civitai_api_key:
94
- headers["Authorization"] = f"Bearer {util.civitai_api_key}"
 
95
 
96
- # download with header
97
- r = requests.get(url, stream=True, verify=False, headers=headers, proxies=util.proxies)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  # write to file
100
- with open(dl_file_path, "ab") as f:
101
- for chunk in r.iter_content(chunk_size=1024):
 
 
 
 
 
 
102
  if chunk:
 
103
  downloaded_size += len(chunk)
104
- f.write(chunk)
105
- # force to write to disk
106
- f.flush()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- # progress
109
- progress = int(50 * downloaded_size / total_size)
110
- sys.stdout.reconfigure(encoding='utf-8')
111
- sys.stdout.write("\r[%s%s] %d%%" % ('-' * progress, ' ' * (50 - progress), 100 * downloaded_size / total_size))
112
- sys.stdout.flush()
 
 
113
 
114
- print()
115
 
116
  # check file size
117
- downloaded_size = os.path.getsize(dl_file_path)
118
- if downloaded_size < total_size:
119
- util.printD(f"Download failed due to insufficient file size. Try again later or download it manually: {url}")
120
- return
 
 
 
 
 
 
 
 
121
 
122
  # rename file
123
- os.rename(dl_file_path, file_path)
124
- util.printD(f"File Downloaded to: {file_path}")
125
- return file_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ -*- coding: UTF-8 -*-
2
+ Used for downloading files
3
+ """
4
+ from __future__ import annotations
5
+ from collections.abc import Generator
6
  import os
7
+ import platform
8
+ import time
9
+ from typing import cast, Literal
10
+ from tqdm import tqdm
11
+ import requests
12
+ import urllib3
13
  from . import util
14
 
15
 
16
+ DL_EXT = ".downloading"
17
+ MAX_RETRIES = 3
18
 
19
  # disable ssl warning info
20
+ urllib3.disable_warnings()
21
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ def request_get(
24
+ url:str,
25
+ headers:dict | None=None,
26
+ retries=0
27
+ ) -> tuple[Literal[True], requests.Response] | tuple[Literal[False], str]:
28
+ """
29
+ Performs a GET request
30
 
31
+ returns: tuple(success:bool, response:Response or failure message:str)
32
+ """
33
 
34
+ headers = util.append_default_headers(headers or {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ try:
37
+ response = requests.get(
38
+ url,
39
+ stream=True,
40
+ verify=False,
41
+ headers=headers,
42
+ proxies=util.PROXIES,
43
+ timeout=util.REQUEST_TIMEOUT
44
+ )
45
+
46
+ except TimeoutError:
47
+ output = f"GET Request timed out for {url}"
48
+ print(output)
49
+ return (False, output)
50
+
51
+ if not response.ok:
52
+ status_code = response.status_code
53
+ reason = response.reason
54
+ util.printD(util.indented_msg(
55
+ f"""
56
+ GET Request failed with error code:
57
+ {status_code}: {reason}
58
+ """
59
+ ))
60
+
61
+ if status_code == 401:
62
+ return (
63
+ False,
64
+ "This download requires Authentication. Please add an API Key to Civitai Helper's settings to continue this download. See [Wiki](https://github.com/zixaphir/Stable-Diffusion-Webui-Civitai-Helper/wiki/Civitai-API-Key) for details on how to create an API Key."
65
+ )
66
+
67
+ if status_code == 416:
68
+ response.raise_for_status()
69
+
70
+ if status_code != 404 and retries < MAX_RETRIES:
71
+ util.printD("Retrying")
72
+ return request_get(url, headers, retries + 1)
73
+
74
+ return (False, reason)
75
+
76
+ return (True, response)
77
+
78
+
79
+ def visualize_progress(percent:int, downloaded:int, total:int, speed:int | float, show_bar=True) -> str:
80
+ """ Used to display progress in webui """
81
+
82
+ s_total = f"{total}"
83
+ s_downloaded = f"{downloaded:>{len(s_total)}}"
84
+ s_percent = f"{percent:>3}"
85
+ s_speed = f'{human_readable_filesize(speed)}Bps'
86
 
87
+ snippet = f"`{s_percent}%: {s_downloaded} / {s_total} @ {s_speed}`"
 
88
 
89
+ if not show_bar:
90
+ # Unfortunately showing a progress bar in webui
91
+ # is very weird on mobile with limited horizontal
92
+ # space
93
+ return snippet.replace(" ", "\u00a0")
94
 
95
+ progress = "\u2588" * percent
 
96
 
97
+ return f"`[{progress:<100}] {snippet}`".replace(" ", "\u00a0")
 
 
 
 
 
 
 
 
98
 
99
+
100
+ def download_progress(
101
+ url:str,
102
+ file_path:str,
103
+ total_size:int,
104
+ headers:dict | None=None,
105
+ response_without_range:requests.Response | None=None
106
+ ) -> Generator[tuple[bool, str] | str, None, None]:
107
+ """
108
+ Performs a file download.
109
+
110
+ yields: tuple(success:bool, filepath or failure message:str) or progress:str
111
+ """
112
  # use a temp file for downloading
 
113
 
114
+ if not headers:
115
+ headers = {}
116
+
117
+ dl_path = f"{file_path}{DL_EXT}"
118
 
119
+ util.printD(f"Downloading to temp file: {dl_path}")
120
 
121
+ # check if downloading file exists
122
  downloaded_size = 0
123
+ if os.path.exists(dl_path):
124
+ downloaded_size = os.path.getsize(dl_path)
125
+ util.printD(f"Resuming partially downloaded file from progress: {downloaded_size}")
126
+
127
+ # use response without range or create request with range
128
+ if response_without_range and downloaded_size == 0:
129
+ response = response_without_range
130
+ else:
131
+ if response_without_range:
132
+ response_without_range.close()
133
 
134
+ # create header range
135
+ headers_with_range = util.append_default_headers({
136
+ **headers,
137
+ "Range": f"bytes={downloaded_size:d}-",
138
+ })
139
 
140
+ # download with header
141
+ try:
142
+ success, response_or_error = request_get(
143
+ url,
144
+ headers=headers_with_range,
145
+ )
146
 
147
+ except requests.HTTPError as dl_error:
148
+ # 416 - Range Not Satisfiable
149
+ response = dl_error.response
150
+ if not response or response.status_code != 416:
151
+ raise
152
+
153
+ util.printD("Could not resume download from existing temporary file. Restarting download")
154
+
155
+ os.remove(dl_path)
156
+
157
+ yield from download_progress(url, file_path, total_size, headers)
158
+ return
159
+
160
+ if not success:
161
+ yield (False, cast(str, response_or_error))
162
+ return
163
+
164
+ response = cast(requests.Response, response_or_error)
165
+
166
+ last_tick = 0
167
+ start = time.time()
168
+
169
+ downloaded_this_session = 0
170
 
171
  # write to file
172
+ with open(dl_path, 'ab') as target, tqdm(
173
+ initial=target.tell(),
174
+ total=total_size,
175
+ unit='iB',
176
+ unit_scale=True,
177
+ unit_divisor=1024
178
+ ) as progress_bar:
179
+ for chunk in response.iter_content(chunk_size=256*1024):
180
  if chunk:
181
+ downloaded_this_session += len(chunk)
182
  downloaded_size += len(chunk)
183
+ written = target.write(chunk)
184
+
185
+ # write to disk
186
+ target.flush()
187
+
188
+ progress_bar.update(written)
189
+
190
+ percent = int(100 * (downloaded_size / total_size))
191
+ timer = time.time()
192
+
193
+ # Gradio output is a *slooowwwwwwww* asynchronous FIFO queue
194
+ if timer - last_tick > 0.2 or percent == 100:
195
+
196
+ last_tick = timer
197
+ elapsed = timer - start
198
+ speed = downloaded_this_session // elapsed if elapsed >= 1 \
199
+ else downloaded_this_session
200
 
201
+ text_progress = visualize_progress(
202
+ percent,
203
+ downloaded_size,
204
+ total_size,
205
+ speed,
206
+ False
207
+ )
208
 
209
+ yield text_progress
210
 
211
  # check file size
212
+ downloaded_size = os.path.getsize(dl_path)
213
+ if downloaded_size != total_size:
214
+ warning = util.indented_msg(
215
+ f"""
216
+ File is not the correct size: {file_path}.
217
+ Expected {total_size:d}, got {downloaded_size:d}.
218
+ The file may be corrupt. If you encounter issues,
219
+ you can try again later or download the file manually: {url}
220
+ """
221
+ )
222
+ util.warning(warning)
223
+ util.printD(warning)
224
 
225
  # rename file
226
+ os.rename(dl_path, file_path)
227
+ output = f"File Downloaded to: {file_path}"
228
+ util.printD(output)
229
+
230
+ yield (True, file_path)
231
+
232
+
233
+ def get_file_path_from_service_headers(response:requests.Response, folder:str) -> str | None:
234
+ """
235
+ Parses a response header to get a filename
236
+ then builds a file_path.
237
+
238
+ return: file_path:str
239
+ """
240
+
241
+ content_disposition = response.headers.get("Content-Disposition", None)
242
+
243
+ if content_disposition is None:
244
+ util.printD("Can not get file name from download url's header")
245
+ return None
246
+
247
+ # Extract the filename from the header
248
+ # content of a CD: "attachment;filename=FileName.txt"
249
+ # in case "" is in CD filename's start and end, need to strip them out
250
+ filename = content_disposition.split("=")[1].strip('"')
251
+ filename = filename.encode('iso8859-1').decode('utf-8')
252
+ if not filename:
253
+ util.printD(f"Fail to get file name from Content-Disposition: {content_disposition}")
254
+ return None
255
+
256
+ # with folder and filename, now we have the full file path
257
+ return os.path.join(folder, filename)
258
+
259
+
260
+ # output is downloaded file path
261
+ def dl_file(
262
+ url:str,
263
+ folder:str | None=None,
264
+ filename:str | None=None,
265
+ file_path:str | None=None,
266
+ headers:dict | None=None,
267
+ duplicate:str | None=None
268
+ ) -> Generator[tuple[bool, str] | str, None, None]:
269
+ """
270
+ Perform a download.
271
+
272
+ yields: tuple(success:bool, filepath or failure message:str) or progress:str
273
+ """
274
+
275
+ if not headers:
276
+ headers = {}
277
+
278
+ success, response_or_error = request_get(url, headers=headers)
279
+
280
+ if not success:
281
+ yield (False, cast(str, response_or_error))
282
+ return
283
+
284
+ response = cast(requests.Response, response_or_error)
285
+
286
+ util.printD(f"Start downloading from: {url}")
287
+
288
+ # close the response when the function ends
289
+ with response:
290
+
291
+ # get file_path
292
+ if not file_path:
293
+ if not (folder and os.path.isdir(folder)):
294
+ yield (
295
+ False,
296
+ "No directory to save model to."
297
+ )
298
+ return
299
+
300
+ if filename:
301
+ file_path = os.path.join(folder, filename)
302
+ else:
303
+ file_path = get_file_path_from_service_headers(response, folder)
304
+
305
+ if not file_path:
306
+ yield (
307
+ False,
308
+ "Could not get a file_path to place saved file."
309
+ )
310
+ return
311
+
312
+ util.printD(f"Target file path: {file_path}")
313
+ base, ext = os.path.splitext(file_path)
314
+
315
+ # duplicate handling
316
+ if os.path.isfile(file_path):
317
+ if duplicate == "Rename New":
318
+ # check if file is already exist
319
+ count = 2
320
+ new_base = base
321
+ while os.path.isfile(file_path):
322
+ util.printD("Target file already exist.")
323
+ # rename duplicate
324
+ new_base = f"{base}_{count}"
325
+ file_path = f"{new_base}{ext}"
326
+ count += 1
327
+
328
+ elif duplicate != "Overwrite":
329
+ yield (
330
+ False,
331
+ f"File {file_path} already exists! Download will not proceed."
332
+ )
333
+ return
334
+
335
+ # get file size
336
+ total_size = int(response.headers['Content-Length'])
337
+ util.printD(f"File size: {total_size} ({human_readable_filesize(total_size)})")
338
+
339
+ yield from download_progress(url, file_path, total_size, headers, response)
340
+
341
+
342
+ def human_readable_filesize(size:int | float) -> str:
343
+ """ Convert file size to human readable text """
344
+ prefixes = ["", "K", "M", "G"]
345
+
346
+ # Mac reports filesizes in multiples of 1000
347
+ # We should respect platform differences
348
+ unit = 1000 if platform.system() == "Darwin" else 1024
349
+
350
+ i = 0
351
+ while size > unit and i < len(prefixes) - 1:
352
+ i += 1
353
+ size = size / unit
354
+
355
+ return f"{round(size, 2)}{prefixes[i]}"
356
+
357
+
358
+ def error(download_url:str, msg:str) -> str:
359
+ """ Display a download error """
360
+ output = util.indented_msg(
361
+ f"""
362
+ Download failed.
363
+ {msg}
364
+ Download url: {download_url}
365
+ """
366
+ )
367
+ util.printD(output)
368
+ return output
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/duplicate_check.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ -*- coding: UTF-8 -*-
2
+ Check for duplicate models.
3
+ """
4
+
5
+ import os
6
+ import html
7
+ import json
8
+ import traceback
9
+ import gradio as gr
10
+ from . import util
11
+ from . import model
12
+ from . import civitai
13
+ from . import templates
14
+
15
+
16
+ def scan_for_dups(scan_model_types, cached_hash, progress=gr.Progress()):
17
+ """ Scans model metadata to detect duplicates
18
+ by using the model hash.
19
+ """
20
+
21
+ util.printD("Start scan_for_dups")
22
+ output = ""
23
+
24
+ # check model types
25
+ if not scan_model_types:
26
+ output = "Model Types is None. Will not scan."
27
+ util.printD(output)
28
+ return output
29
+
30
+ model_types = []
31
+
32
+ # check type if it is a string
33
+ if isinstance(scan_model_types, str):
34
+ model_types.append(scan_model_types)
35
+ else:
36
+ model_types = scan_model_types
37
+
38
+ result = None
39
+ for result in gather_model_data(model_types, cached_hash):
40
+ if isinstance(result, tuple):
41
+ percent, status = result
42
+ progress(percent, desc=status)
43
+ models = result
44
+
45
+ dups = check_for_dups(models)
46
+
47
+ output = create_dups_html(dups)
48
+
49
+ return f"<section class=extra-network-cards>{output}</section>"
50
+
51
+
52
+ def gather_model_data(model_types, cached_hash):
53
+ """ Collects model metadata files and parses them for metadata """
54
+
55
+ models = {}
56
+
57
+ for model_type, model_folder in model.folders.items():
58
+ if model_type not in model_types:
59
+ continue
60
+
61
+ for result in scan_dir(model_folder, model_type, cached_hash):
62
+ yield result
63
+
64
+ models[model_type] = result
65
+
66
+ yield models
67
+
68
+ def scan_dir(model_folder, model_type, cached_hash):
69
+ """
70
+ Scans dir for models and their metadata
71
+ """
72
+
73
+ suffix = f"{civitai.SUFFIX}{model.CIVITAI_EXT}"
74
+ suffix_len = -len(suffix)
75
+
76
+ metadata = []
77
+ util.printD(f"Scanning path: {model_folder}")
78
+ for root, _, files in os.walk(model_folder, followlinks=True):
79
+ for filename in files:
80
+ try:
81
+ if filename[suffix_len:] == suffix:
82
+ for result in parse_metadata(model_folder, root, filename, suffix, model_type, cached_hash):
83
+ yield result
84
+ data = result
85
+ if data:
86
+ metadata.append(data)
87
+
88
+ except (IndexError, KeyError, ValueError):
89
+ util.printD(f"Error occurred on file `{root}/{filename}`")
90
+ traceback.print_exc()
91
+ util.printD("You can probably ignore this")
92
+ continue
93
+
94
+ yield metadata
95
+
96
+
97
+ def parse_metadata(model_folder, root, filename, suffix, model_type, cached_hash):
98
+ """ Parses model JSON file for hash / other metadata """
99
+
100
+ metadata = {}
101
+
102
+ filepath = f"{root}/{filename}"
103
+ model_name = filename[:-len(suffix)]
104
+
105
+ util.printD(f"Processing {model_name}")
106
+
107
+ with open(filepath) as file:
108
+ try:
109
+ model_info = json.load(file)
110
+ except json.JSONDecodeError:
111
+ yield None
112
+ return
113
+
114
+ model_file = model_info["files"][0]
115
+ model_ext = model_file["name"].split(".").pop()
116
+
117
+ description = None
118
+
119
+ # Backwards compatibility with older model info files
120
+ # from pre civitai helper 1.7
121
+ try:
122
+ description = model_info["model"]["description"]
123
+ except (ValueError, KeyError):
124
+ description = model_info.get("description", None)
125
+
126
+ if not description:
127
+ description = ""
128
+
129
+ model_path = f"{root}/{model_name}.{model_ext}"
130
+
131
+ if not os.path.isfile(model_path):
132
+ model_path = locate_model_from_partial(root, model_name)
133
+
134
+ if not (model_path and os.path.isfile(model_path)):
135
+ util.printD(f"No model path found for {filepath}")
136
+ yield None
137
+ return
138
+
139
+ for result in get_hash(model_path, model_file, model_type, cached_hash):
140
+ yield result
141
+ sha256 = result
142
+
143
+ metadata = {
144
+ "model_name": model_name,
145
+ "civitai_name": model_info["model"]["name"],
146
+ "description": description,
147
+ "model_path": model_path,
148
+ "subpath": model_path[len(model_folder):],
149
+ "model_type": model_type,
150
+ "hash": sha256,
151
+ "search_term": make_search_term(model_type, model_path, sha256)
152
+ }
153
+
154
+ yield metadata
155
+
156
+
157
+ def get_hash(model_path, model_file, model_type, cached_hash):
158
+ """
159
+ Get or calculate hash of `model_path/model_file`
160
+ """
161
+
162
+ sha256 = None
163
+ if cached_hash:
164
+ try:
165
+ sha256 = model_file["hashes"]["SHA256"].upper()
166
+
167
+ except (KeyError, ValueError):
168
+ pass
169
+
170
+ if sha256:
171
+ yield sha256
172
+ return
173
+
174
+ util.printD(f"No sha256 hash in metadata for {model_file}. \
175
+ \n\tGenerating one. This will be slower")
176
+
177
+ model_hash_type = {
178
+ "ckp": "checkpoint",
179
+ "ti": "textual_inversion",
180
+ "hyper": "hypernet",
181
+ "lora": "lora",
182
+ "lycoris": "lycoris"
183
+ }[model_type]
184
+
185
+ result = None
186
+ for result in util.gen_file_sha256(
187
+ model_path,
188
+ model_type=model_hash_type,
189
+ use_addnet_hash=False
190
+ ):
191
+ yield result
192
+
193
+ sha256 = result.upper()
194
+
195
+ yield sha256
196
+
197
+
198
+ def make_search_term(model_type, model_path, sha256):
199
+ """ format search term for the correct model type """
200
+
201
+ folder = model.folders[model_type]
202
+ subpath = model_path[len(folder):]
203
+ sha256 = sha256.lower()
204
+
205
+ if not subpath.startswith("/"):
206
+ subpath = f"/{subpath}"
207
+
208
+ if model_type == "hyper":
209
+ snippet = subpath.split(".")
210
+ snippet.pop()
211
+ subpath = ".".join(snippet)
212
+ return f"{subpath}"
213
+
214
+ # All other supported model types seem to use this format
215
+ return f"{subpath} {sha256}"
216
+
217
+
218
+ def locate_model_from_partial(root, model_name):
219
+ """
220
+ Tries to locate a model if the extension
221
+ doesn't match the metadata
222
+ """
223
+
224
+ for ext in model.EXTS:
225
+ filename = f"{root}/{model_name}{ext}"
226
+ if os.path.isfile(filename):
227
+ return filename
228
+
229
+ return None
230
+
231
+
232
+ def check_for_dups(models):
233
+ """
234
+ returns all models that share the a stored
235
+ sha256 hash with another model
236
+ """
237
+
238
+ scanned = {}
239
+ dups = {}
240
+
241
+ for model_type, models_of_type in models.items():
242
+ scanned[model_type] = {}
243
+ scanned_type = scanned[model_type]
244
+ for model_data in models_of_type:
245
+ sha256 = model_data["hash"]
246
+
247
+ if model_type == "lycoris":
248
+ if is_lycoris_lora(model_data, scanned):
249
+ continue
250
+
251
+ if not scanned_type.get(sha256, None):
252
+ scanned_type[sha256] = [model_data]
253
+ continue
254
+
255
+ scanned_type[sha256].append(model_data)
256
+
257
+ for model_type, models_of_type in scanned.items():
258
+ dups_of_type = dups[model_type] = {}
259
+ for key, model_data in models_of_type.items():
260
+ if len(model_data) > 1:
261
+ dups_of_type[key] = model_data
262
+
263
+ return dups
264
+
265
+
266
+ def get_preview(model_path):
267
+ """ Finds the appropriate preview image for a model """
268
+
269
+ prevs = model.get_potential_model_preview_files(model_path, True)
270
+
271
+ bg_image = None
272
+ for prev in prevs:
273
+ if os.path.isfile(prev):
274
+ bg_image = prev
275
+ break
276
+
277
+ if not bg_image:
278
+ return ""
279
+
280
+ return templates.duplicate_preview.substitute(
281
+ bg_image=bg_image
282
+ )
283
+
284
+
285
+ def make_model_card(model_data):
286
+ """ Creates the HTML for a single model card """
287
+
288
+ card_t = templates.duplicate_card
289
+
290
+ bg_image = get_preview(model_data["model_path"])
291
+ style = "font-size:100%"
292
+ model_name = model_data["model_name"]
293
+ subpath = model_data["subpath"].replace("'", "\\'")
294
+ description = html.escape(model_data["description"])
295
+ search_term = model_data["search_term"].replace("'", "\\'")
296
+ model_type = model_data["model_type"]
297
+
298
+ util.printD(subpath)
299
+
300
+ card = card_t.substitute(
301
+ style=style,
302
+ name=model_name,
303
+ path=subpath,
304
+ background_image=bg_image,
305
+ description=description,
306
+ search_term=search_term,
307
+ model_type=model_type
308
+ )
309
+
310
+ return card
311
+
312
+
313
+ def create_dups_html(dups):
314
+ """ creates an HTML snippet containing duplicate models """
315
+
316
+ article_t = templates.duplicate_article
317
+ row_t = templates.duplicate_row
318
+ column_t = templates.duplicate_column
319
+
320
+ articles = []
321
+
322
+ for model_type, models_of_type in dups.items():
323
+ rows = []
324
+ for dup_data in models_of_type.values():
325
+ civitai_name = ""
326
+ sha256 = ""
327
+
328
+ columns = []
329
+ for count, model_data in enumerate(dup_data):
330
+ card = make_model_card(model_data)
331
+
332
+ column = column_t.substitute(
333
+ count=count,
334
+ card=card
335
+ )
336
+
337
+ columns.append(column)
338
+
339
+ if model_data["civitai_name"] and not civitai_name:
340
+ civitai_name = model_data["civitai_name"]
341
+ sha256 = model_data["hash"]
342
+
343
+ rows.append(
344
+ row_t.substitute(
345
+ civitai_name=civitai_name,
346
+ hash=sha256,
347
+ columns="".join(columns)
348
+ )
349
+ )
350
+
351
+ content = ""
352
+ if len(rows) > 0:
353
+ content = "".join(rows)
354
+ else:
355
+ content = f"No duplicate {model_type}s found!"
356
+
357
+ articles.append(
358
+ article_t.substitute(
359
+ section_name=model_type,
360
+ contents=content
361
+ )
362
+ )
363
+
364
+ if len(articles) < 1:
365
+ return "Found no duplicate models!"
366
+
367
+ return "".join(articles)
368
+
369
+
370
+ def is_lycoris_lora(lyco, models):
371
+ """
372
+ Compares a lycoris model to scanned loras to ensure that they're not the same file.
373
+ """
374
+ loras = None
375
+ try:
376
+ loras = models["lora"][lyco["hash"]]
377
+
378
+ except (KeyError, ValueError):
379
+ return False
380
+
381
+ try:
382
+ lyco_path = os.path.realpath(lyco["model_path"], strict=True)
383
+
384
+ for lora in loras:
385
+ lora_path = os.path.realpath(lora["model_path"], strict=True)
386
+ if lyco_path == lora_path:
387
+ return True
388
+
389
+ except OSError:
390
+ return False
391
+
392
+ return False
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/js_action_civitai.py CHANGED
@@ -1,8 +1,8 @@
1
- # -*- coding: UTF-8 -*-
2
- # handle msg between js and python side
 
3
  import os
4
- import json
5
- import requests
6
  import webbrowser
7
  from . import util
8
  from . import model
@@ -11,19 +11,21 @@ from . import msg_handler
11
  from . import downloader
12
 
13
 
14
-
15
- # get civitai's model url and open it in browser
16
- # parameter: model_type, search_term
17
- # output: python msg - will be sent to hidden textbox then picked by js side
18
- def open_model_url(msg, open_url_with_js):
 
 
19
  util.printD("Start open_model_url")
20
 
21
  output = ""
22
  result = msg_handler.parse_js_msg(msg)
23
  if not result:
24
  util.printD("Parsing js ms failed")
25
- return
26
-
27
  model_type = result["model_type"]
28
  search_term = result["search_term"]
29
 
@@ -41,16 +43,15 @@ def open_model_url(msg, open_url_with_js):
41
  util.printD(f"model id from info file of {model_type} {search_term} is None")
42
  return ""
43
 
44
- url = civitai.url_dict["modelPage"]+str(model_id)
45
-
46
 
47
  # msg content for js
48
  content = {
49
- "url":""
50
  }
51
 
52
- if not open_url_with_js:
53
- util.printD("Open Url: " + url)
54
  # open url
55
  webbrowser.open_new_tab(url)
56
  else:
@@ -62,50 +63,50 @@ def open_model_url(msg, open_url_with_js):
62
  return output
63
 
64
 
65
-
66
- # add trigger words to prompt
67
- # parameter: model_type, search_term, prompt
68
- # return: [new_prompt, new_prompt] - new prompt with trigger words, return twice for txt2img and img2img
69
  def add_trigger_words(msg):
 
 
 
 
 
 
70
  util.printD("Start add_trigger_words")
71
 
72
  result = msg_handler.parse_js_msg(msg)
73
  if not result:
74
  util.printD("Parsing js ms failed")
75
- return
76
-
77
  model_type = result["model_type"]
78
  search_term = result["search_term"]
79
  prompt = result["prompt"]
80
 
81
-
82
  model_info = civitai.load_model_info_by_search_term(model_type, search_term)
83
  if not model_info:
84
  util.printD(f"Failed to get model info for {model_type} {search_term}")
85
  return [prompt, prompt]
86
-
87
  if "trainedWords" not in model_info.keys():
88
  util.printD(f"Failed to get trainedWords from info file for {model_type} {search_term}")
89
  return [prompt, prompt]
90
-
91
- trainedWords = model_info["trainedWords"]
92
- if not trainedWords:
93
- util.printD(f"No trainedWords from info file for {model_type} {search_term}")
94
- return [prompt, prompt]
95
-
96
- if len(trainedWords) == 0:
97
  util.printD(f"trainedWords from info file for {model_type} {search_term} is empty")
98
  return [prompt, prompt]
99
-
100
- # get ful trigger words
101
- trigger_words = ""
102
- for word in trainedWords:
103
- trigger_words = trigger_words + word + ", "
104
 
105
- new_prompt = prompt + " " + trigger_words
106
- util.printD("trigger_words: " + trigger_words)
107
- util.printD("prompt: " + prompt)
108
- util.printD("new_prompt: " + new_prompt)
 
 
 
 
 
 
 
 
109
 
110
  util.printD("End add_trigger_words")
111
 
@@ -113,18 +114,20 @@ def add_trigger_words(msg):
113
  return [new_prompt, new_prompt]
114
 
115
 
116
-
117
- # use preview image's prompt as prompt
118
- # parameter: model_type, model_name, prompt, neg_prompt
119
- # return: [new_prompt, new_neg_prompt, new_prompt, new_neg_prompt,] - return twice for txt2img and img2img
120
  def use_preview_image_prompt(msg):
 
 
 
 
 
 
121
  util.printD("Start use_preview_image_prompt")
122
 
123
  result = msg_handler.parse_js_msg(msg)
124
  if not result:
125
  util.printD("Parsing js ms failed")
126
- return
127
-
128
  model_type = result["model_type"]
129
  search_term = result["search_term"]
130
  prompt = result["prompt"]
@@ -135,188 +138,244 @@ def use_preview_image_prompt(msg):
135
  if not model_info:
136
  util.printD(f"Failed to get model info for {model_type} {search_term}")
137
  return [prompt, neg_prompt, prompt, neg_prompt]
138
-
139
- if "images" not in model_info.keys():
140
- util.printD(f"Failed to get images from info file for {model_type} {search_term}")
141
- return [prompt, neg_prompt, prompt, neg_prompt]
142
-
143
- images = model_info["images"]
144
- if not images:
145
- util.printD(f"No images from info file for {model_type} {search_term}")
146
- return [prompt, neg_prompt, prompt, neg_prompt]
147
-
148
  if len(images) == 0:
149
- util.printD(f"images from info file for {model_type} {search_term} is empty")
150
  return [prompt, neg_prompt, prompt, neg_prompt]
151
-
152
  # get prompt from preview images' meta data
153
  preview_prompt = ""
154
  preview_neg_prompt = ""
155
  for img in images:
156
- if "meta" in img.keys():
157
- if img["meta"]:
158
- if "prompt" in img["meta"].keys():
159
- if img["meta"]["prompt"]:
160
- preview_prompt = img["meta"]["prompt"]
161
-
162
- if "negativePrompt" in img["meta"].keys():
163
- if img["meta"]["negativePrompt"]:
164
- preview_neg_prompt = img["meta"]["negativePrompt"]
165
-
166
- # we only need 1 prompt
167
- if preview_prompt:
168
- break
169
-
170
  if not preview_prompt:
171
  util.printD(f"There is no prompt of {model_type} {search_term} in its preview image")
172
  return [prompt, neg_prompt, prompt, neg_prompt]
173
-
174
  util.printD("End use_preview_image_prompt")
175
-
176
  return [preview_prompt, preview_neg_prompt, preview_prompt, preview_neg_prompt]
177
 
178
 
179
- # download model's new verson by model path, version id and download url
180
- # output is a md log
181
- def dl_model_new_version(msg, max_size_preview, skip_nsfw_preview):
 
 
 
 
 
 
 
 
182
  util.printD("Start dl_model_new_version")
183
 
184
  output = ""
185
 
 
 
186
  result = msg_handler.parse_js_msg(msg)
187
  if not result:
188
- output = "Parsing js ms failed"
189
  util.printD(output)
190
- return output
191
-
 
192
  model_path = result["model_path"]
193
  version_id = result["version_id"]
194
  download_url = result["download_url"]
195
-
196
- util.printD("model_path: " + model_path)
197
- util.printD("version_id: " + str(version_id))
198
- util.printD("download_url: " + download_url)
199
 
200
  # check data
201
- if not model_path:
202
- output = "model_path is empty"
 
 
 
 
 
203
  util.printD(output)
204
- return output
 
205
 
206
- if not version_id:
207
- output = "version_id is empty"
208
- util.printD(output)
209
- return output
210
-
211
- if not download_url:
212
- output = "download_url is empty"
213
- util.printD(output)
214
- return output
215
 
216
  if not os.path.isfile(model_path):
217
- output = "model_path is not a file: "+ model_path
218
  util.printD(output)
219
- return output
 
220
 
221
  # get model folder from model path
222
  model_folder = os.path.dirname(model_path)
223
 
224
- # no need to check when downloading new version, since checking new version is already checked
225
- # check if this model is already existed
226
- # r = civitai.search_local_model_info_by_version_id(model_folder, version_id)
227
- # if r:
228
- # output = "This model version is already existed"
229
- # util.printD(output)
230
- # return output
231
-
232
- # download file
233
- new_model_path = downloader.dl(download_url, model_folder, None, None)
234
- if not new_model_path:
235
- output = "Download failed, check console log for detail. Download url: " + download_url
236
  util.printD(output)
237
- return output
 
238
 
239
  # get version info
240
  version_info = civitai.get_version_info_by_version_id(version_id)
241
- if not version_info:
242
- output = "Model downloaded, but failed to get version info, check console log for detail. Model saved to: " + new_model_path
243
- util.printD(output)
244
- return output
245
 
246
- # now write version info to file
247
- base, ext = os.path.splitext(new_model_path)
248
- info_file = base + civitai.suffix + model.info_ext
249
- model.write_model_info(info_file, version_info)
250
 
251
  # then, get preview image
252
- civitai.get_preview_image_by_model_path(new_model_path, max_size_preview, skip_nsfw_preview)
253
-
254
- output = "Done. Model downloaded to: " + new_model_path
 
 
 
 
 
255
  util.printD(output)
256
- return output
257
 
258
 
 
 
 
259
 
260
- # remove a model and all related files
261
- def remove_model_by_path(msg):
262
- util.printD("Start remove_model_by_path")
263
-
264
- output = ""
265
- result = msg_handler.parse_js_msg(msg)
266
  if not result:
267
  output = "Parsing js ms failed"
 
268
  util.printD(output)
269
- return output
270
-
271
  model_type = result["model_type"]
272
  search_term = result["search_term"]
273
 
274
  model_path = model.get_model_path_by_search_term(model_type, search_term)
275
  if not model_path:
276
  output = f"Fail to get model for {model_type} {search_term}"
 
277
  util.printD(output)
278
- return output
279
-
280
 
281
  if not os.path.isfile(model_path):
282
  output = f"Model {model_type} {search_term} does not exist, no need to remove"
 
283
  util.printD(output)
284
- return output
285
-
286
- # all files need to be removed
287
- related_paths = []
288
- related_paths.append(model_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
 
 
290
 
291
- # get info file
292
- base, ext = os.path.splitext(model_path)
293
- info_path = base + model.info_ext
294
- first_preview_path = base+".png"
295
- sec_preview_path = base+".preview.png"
296
- civitai_info_path = base + civitai.suffix + model.info_ext
297
 
298
- if os.path.isfile(civitai_info_path):
299
- related_paths.append(civitai_info_path)
300
 
301
- if os.path.isfile(first_preview_path):
302
- related_paths.append(first_preview_path)
303
 
304
- if os.path.isfile(sec_preview_path):
305
- related_paths.append(sec_preview_path)
 
 
 
 
306
 
307
- if os.path.isfile(info_path):
308
- related_paths.append(info_path)
309
 
310
- # remove files
311
- for rp in related_paths:
312
- if os.path.isfile(rp):
313
- util.printD(f"Removing file {rp}")
314
- os.remove(rp)
315
 
316
- util.printD(f"{len(related_paths)} file removed")
 
 
317
 
318
- util.printD("End remove_model_by_path")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  return output
320
 
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
 
 
 
1
+ """ -*- coding: UTF-8 -*-
2
+ handle msg between js and python side
3
+ """
4
  import os
5
+ from pathlib import Path
 
6
  import webbrowser
7
  from . import util
8
  from . import model
 
11
  from . import downloader
12
 
13
 
14
+ def open_model_url(msg):
15
+ """
16
+ get civitai's model url and open it in browser
17
+ parameter: model_type, search_term
18
+ output: python msg
19
+ - will be sent to hidden textbox then picked by js side
20
+ """
21
  util.printD("Start open_model_url")
22
 
23
  output = ""
24
  result = msg_handler.parse_js_msg(msg)
25
  if not result:
26
  util.printD("Parsing js ms failed")
27
+ return None
28
+
29
  model_type = result["model_type"]
30
  search_term = result["search_term"]
31
 
 
43
  util.printD(f"model id from info file of {model_type} {search_term} is None")
44
  return ""
45
 
46
+ url = f'{civitai.URLS["modelPage"]}{model_id}'
 
47
 
48
  # msg content for js
49
  content = {
50
+ "url": ""
51
  }
52
 
53
+ if not util.get_opts("ch_open_url_with_js"):
54
+ util.printD(f"Open Url: {url}")
55
  # open url
56
  webbrowser.open_new_tab(url)
57
  else:
 
63
  return output
64
 
65
 
 
 
 
 
66
  def add_trigger_words(msg):
67
+ """
68
+ add trigger words to prompt
69
+ parameter: model_type, search_term, prompt
70
+ return: [new_prompt, new_prompt]
71
+ - new prompt with trigger words, return twice for txt2img and img2img
72
+ """
73
  util.printD("Start add_trigger_words")
74
 
75
  result = msg_handler.parse_js_msg(msg)
76
  if not result:
77
  util.printD("Parsing js ms failed")
78
+ return None
79
+
80
  model_type = result["model_type"]
81
  search_term = result["search_term"]
82
  prompt = result["prompt"]
83
 
 
84
  model_info = civitai.load_model_info_by_search_term(model_type, search_term)
85
  if not model_info:
86
  util.printD(f"Failed to get model info for {model_type} {search_term}")
87
  return [prompt, prompt]
88
+
89
  if "trainedWords" not in model_info.keys():
90
  util.printD(f"Failed to get trainedWords from info file for {model_type} {search_term}")
91
  return [prompt, prompt]
92
+
93
+ trained_words = model_info.get("trainedWords", [])
94
+ if len(trained_words) == 0:
 
 
 
 
95
  util.printD(f"trainedWords from info file for {model_type} {search_term} is empty")
96
  return [prompt, prompt]
 
 
 
 
 
97
 
98
+ # guess if trained words are a list of words or list of prompts
99
+ prompt_list = ',' in trained_words[0]
100
+
101
+ # if a list of prompts, join with a newline, else a comma and a space
102
+ separator = "\n" if prompt_list else ", "
103
+ trigger_words = separator.join(trained_words)
104
+
105
+ new_prompt = f"{prompt} {trigger_words}"
106
+
107
+ util.printD(f"trigger_words: {trigger_words}")
108
+ util.printD(f"prompt: {prompt}")
109
+ util.printD(f"new_prompt: {new_prompt}")
110
 
111
  util.printD("End add_trigger_words")
112
 
 
114
  return [new_prompt, new_prompt]
115
 
116
 
 
 
 
 
117
  def use_preview_image_prompt(msg):
118
+ """
119
+ use preview image's prompt as prompt
120
+ parameter: model_type, model_name, prompt, neg_prompt
121
+ return: [new_prompt, new_neg_prompt, new_prompt, new_neg_prompt,]
122
+ - return twice for txt2img and img2img
123
+ """
124
  util.printD("Start use_preview_image_prompt")
125
 
126
  result = msg_handler.parse_js_msg(msg)
127
  if not result:
128
  util.printD("Parsing js ms failed")
129
+ return None
130
+
131
  model_type = result["model_type"]
132
  search_term = result["search_term"]
133
  prompt = result["prompt"]
 
138
  if not model_info:
139
  util.printD(f"Failed to get model info for {model_type} {search_term}")
140
  return [prompt, neg_prompt, prompt, neg_prompt]
141
+
142
+ images = model_info.get("images", [])
 
 
 
 
 
 
 
 
143
  if len(images) == 0:
144
+ util.printD(f"No images from info file for {model_type} {search_term}")
145
  return [prompt, neg_prompt, prompt, neg_prompt]
146
+
147
  # get prompt from preview images' meta data
148
  preview_prompt = ""
149
  preview_neg_prompt = ""
150
  for img in images:
151
+ meta = img.get("meta", {})
152
+ preview_prompt = meta.get("prompt", "")
153
+ preview_neg_prompt = meta.get("negativePrompt", "")
154
+
155
+ # we only need 1 prompt
156
+ if preview_prompt:
157
+ break
158
+
 
 
 
 
 
 
159
  if not preview_prompt:
160
  util.printD(f"There is no prompt of {model_type} {search_term} in its preview image")
161
  return [prompt, neg_prompt, prompt, neg_prompt]
162
+
163
  util.printD("End use_preview_image_prompt")
164
+
165
  return [preview_prompt, preview_neg_prompt, preview_prompt, preview_neg_prompt]
166
 
167
 
168
+ def dl_model_new_version(msg, nsfw_preview_threshold):
169
+ """
170
+ download model's new verson by model path, version id and download url
171
+ output is a md log
172
+
173
+ This method is triggered by a click event on the client/js
174
+ side that sends a signal to download a single new model
175
+ version. The actual check for new models is in
176
+ `model_action_civitai.check_models_new_version_to_md`.
177
+ return: output:str
178
+ """
179
  util.printD("Start dl_model_new_version")
180
 
181
  output = ""
182
 
183
+ max_size_preview = util.get_opts("ch_max_size_preview")
184
+
185
  result = msg_handler.parse_js_msg(msg)
186
  if not result:
187
+ output = "Parsing js msg failed"
188
  util.printD(output)
189
+ yield output
190
+ return
191
+
192
  model_path = result["model_path"]
193
  version_id = result["version_id"]
194
  download_url = result["download_url"]
195
+ model_type = result["model_type"]
 
 
 
196
 
197
  # check data
198
+ if not (model_path and version_id and download_url):
199
+ output = util.indented_msg(f"""
200
+ Missing parameter:
201
+ {model_path=}
202
+ {version_id=}
203
+ {download_url=}
204
+ """)
205
  util.printD(output)
206
+ yield output
207
+ return
208
 
209
+ util.printD(f"model_path: {model_path}")
210
+ util.printD(f"version_id: {version_id}")
211
+ util.printD(f"download_url: {download_url}")
 
 
 
 
 
 
212
 
213
  if not os.path.isfile(model_path):
214
+ output = f"model_path is not a file: {model_path}"
215
  util.printD(output)
216
+ yield output
217
+ return
218
 
219
  # get model folder from model path
220
  model_folder = os.path.dirname(model_path)
221
 
222
+ success = False
223
+ # download file + webui visible progress bar
224
+ for result in downloader.dl_file(download_url, folder=model_folder):
225
+ if not isinstance(result, str):
226
+ success, output = result
227
+ break
228
+
229
+ yield result
230
+
231
+ if not success:
 
 
232
  util.printD(output)
233
+ yield "Model download failed. See console for more details."
234
+ return
235
 
236
  # get version info
237
  version_info = civitai.get_version_info_by_version_id(version_id)
 
 
 
 
238
 
239
+ # now write version info to files
240
+ model.process_model_info(output, version_info, model_type)
 
 
241
 
242
  # then, get preview image
243
+ for result in civitai.get_preview_image_by_model_path(
244
+ output,
245
+ max_size_preview,
246
+ nsfw_preview_threshold
247
+ ):
248
+ yield result
249
+
250
+ output = f"Done. Model downloaded to: {output}"
251
  util.printD(output)
252
+ yield output
253
 
254
 
255
+ def get_model_path_from_js_msg(result):
256
+ """
257
+ Gets a model path based on the webui js_msg.
258
 
259
+ return: model_path
260
+ """
 
 
 
 
261
  if not result:
262
  output = "Parsing js ms failed"
263
+ util.error(output)
264
  util.printD(output)
265
+ return None
266
+
267
  model_type = result["model_type"]
268
  search_term = result["search_term"]
269
 
270
  model_path = model.get_model_path_by_search_term(model_type, search_term)
271
  if not model_path:
272
  output = f"Fail to get model for {model_type} {search_term}"
273
+ util.error(output)
274
  util.printD(output)
275
+ return None
 
276
 
277
  if not os.path.isfile(model_path):
278
  output = f"Model {model_type} {search_term} does not exist, no need to remove"
279
+ util.error(output)
280
  util.printD(output)
281
+ return None
282
+
283
+ return model_path
284
+
285
+
286
+ def make_new_filename(candidate_file, model_name, new_name):
287
+ """
288
+ Substitutes and old model name for a new model name.
289
+ return: new_path:str or None
290
+ """
291
+ path, filename = os.path.split(candidate_file)
292
+
293
+ if filename.index(model_name) != 0:
294
+ output = util.indented_msg(f"""
295
+ Could not find model_name in candidate file
296
+ {model_name=}
297
+ {candidate_file=}
298
+ {new_name=}
299
+ """)
300
+ util.error(output)
301
+ util.printD(output)
302
+ return None
303
 
304
+ # handles [model_name].civitai.info and [model_name].preview.[ext]
305
+ new_filename = filename.replace(model_name, new_name, 1)
306
 
307
+ new_path = os.path.join(path, new_filename)
 
 
 
 
 
308
 
309
+ return new_path
 
310
 
 
 
311
 
312
+ def rename_model_by_path(msg):
313
+ """
314
+ Rename a model file and all related ch_helper/
315
+ preview image files.
316
+ """
317
+ util.printD("Start rename_model_by_path")
318
 
319
+ output = ""
320
+ result = msg_handler.parse_js_msg(msg)
321
 
322
+ model_path = get_model_path_from_js_msg(result)
 
 
 
 
323
 
324
+ if model_path is None:
325
+ output = "Could not rename model."
326
+ return output
327
 
328
+ # all files need to be renamed
329
+ model_files = model.get_model_files_from_model_path(model_path)
330
+ model_name = Path(model_path).stem
331
+ new_name = util.bash_filename(result["new_name"])
332
+
333
+ renamed = []
334
+ for candidate_file in model_files:
335
+ new_path = make_new_filename(candidate_file, model_name, new_name)
336
+ if new_path is None:
337
+ continue
338
+
339
+ renamed.append(f"* {candidate_file} to {new_path}")
340
+ util.printD(f"Renaming file {candidate_file} to {new_path}")
341
+ os.rename(candidate_file, new_path)
342
+
343
+ renamed = "\n".join(renamed)
344
+ status = f"The following files were renamed: \n{renamed}"
345
+ util.info(status)
346
+
347
+ util.printD("End rename_model_by_path")
348
  return output
349
 
350
 
351
+ def remove_model_by_path(msg):
352
+ """
353
+ Remove a model file and all related ch_helper/
354
+ preview image files.
355
+ """
356
+ output = ""
357
+ util.printD("Start remove_model_by_path")
358
+
359
+ result = msg_handler.parse_js_msg(msg)
360
+
361
+ model_path = get_model_path_from_js_msg(result)
362
+
363
+ if model_path is None:
364
+ output = "Could not remove model."
365
+ return output
366
+
367
+ # all files need to be renamed
368
+ model_files = model.get_model_files_from_model_path(model_path)
369
+
370
+ removed = []
371
+ for candidate_file in model_files:
372
+ util.printD(f"* Removing file {candidate_file}")
373
+ removed.append(candidate_file)
374
+ os.remove(candidate_file)
375
+
376
+ removed = "\n".join(removed)
377
+ status = f"The following files were removed: \n{removed}"
378
+ util.info(status)
379
 
380
+ util.printD("End remove_model_by_path")
381
+ return output
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/model.py CHANGED
@@ -1,35 +1,73 @@
1
- # -*- coding: UTF-8 -*-
2
- # handle msg between js and python side
 
3
  import os
4
  import json
5
- from . import util
 
 
 
6
  from modules import shared
 
 
 
 
7
 
8
 
9
  # this is the default root path
10
- root_path = os.getcwd()
11
-
12
- # if command line arguement is used to change model folder,
13
- # then model folder is in absolute path, not based on this root path anymore.
14
- # so to make extension work with those absolute model folder paths, model folder also need to be in absolute path
 
 
 
 
 
 
 
15
  folders = {
16
- "ti": os.path.join(root_path, "embeddings"),
17
- "hyper": os.path.join(root_path, "models", "hypernetworks"),
18
- "ckp": os.path.join(root_path, "models", "Stable-diffusion"),
19
- "lora": os.path.join(root_path, "models", "Lora"),
 
20
  }
21
 
22
- exts = (".bin", ".pt", ".safetensors", ".ckpt")
23
- info_ext = ".info"
24
- vae_suffix = ".vae"
 
 
 
 
 
 
 
 
 
 
 
25
 
 
 
 
 
 
 
 
 
 
26
 
27
- # get cusomter model path
 
28
  def get_custom_model_folder():
 
 
 
29
  util.printD("Get Custom Model Folder")
30
 
31
- global folders
32
-
33
  if shared.cmd_opts.embeddings_dir and os.path.isdir(shared.cmd_opts.embeddings_dir):
34
  folders["ti"] = shared.cmd_opts.embeddings_dir
35
 
@@ -42,131 +80,602 @@ def get_custom_model_folder():
42
  if shared.cmd_opts.lora_dir and os.path.isdir(shared.cmd_opts.lora_dir):
43
  folders["lora"] = shared.cmd_opts.lora_dir
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
 
 
 
 
 
 
 
 
46
 
 
 
47
 
 
 
 
 
 
 
 
 
48
 
49
- # write model info to file
50
- def write_model_info(path, model_info):
51
- util.printD("Write model info to file: " + path)
52
- with open(os.path.realpath(path), 'w') as f:
53
- f.write(json.dumps(model_info, indent=4))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
 
56
  def load_model_info(path):
57
- # util.printD("Load model info from file: " + path)
58
  model_info = None
59
- with open(os.path.realpath(path), 'r') as f:
60
  try:
61
- model_info = json.load(f)
62
- except Exception as e:
63
- util.printD("Selected file is not json: " + path)
64
- util.printD(e)
65
- return
66
-
67
  return model_info
68
 
69
 
70
- # get model file names by model type
71
- # parameter: model_type - string
72
- # return: model name list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def get_model_names_by_type(model_type:str) -> list:
74
-
75
- model_folder = folders[model_type]
 
 
 
 
 
 
 
 
76
 
77
  # get information from filter
78
  # only get those model names don't have a civitai model info file
79
  model_names = []
80
- for root, dirs, files in os.walk(model_folder, followlinks=True):
81
- for filename in files:
82
- item = os.path.join(root, filename)
83
- # check extension
84
- base, ext = os.path.splitext(item)
85
- if ext in exts:
86
- # find a model
87
- model_names.append(filename)
88
-
89
 
90
  return model_names
91
 
92
 
93
  # return 2 values: (model_root, model_path)
94
  def get_model_path_by_type_and_name(model_type:str, model_name:str) -> str:
 
95
  util.printD("Run get_model_path_by_type_and_name")
96
- if model_type not in folders.keys():
97
- util.printD("unknown model_type: " + model_type)
98
- return
99
-
100
  if not model_name:
101
  util.printD("model name can not be empty")
102
- return
103
-
104
- folder = folders[model_type]
105
 
106
- # model could be in subfolder, need to walk.
107
- model_root = ""
108
- model_path = ""
109
- for root, dirs, files in os.walk(folder, followlinks=True):
110
- for filename in files:
111
- if filename == model_name:
112
- # find model
113
- model_root = root
114
- model_path = os.path.join(root, filename)
115
- return (model_root, model_path)
116
 
117
- return
 
118
 
 
 
 
 
 
 
 
 
119
 
 
 
120
 
121
 
122
  # get model path by model type and search_term
123
  # parameter: model_type, search_term
124
  # return: model_path
125
- def get_model_path_by_search_term(model_type:str, search_term:str):
 
 
 
 
 
126
  util.printD(f"Search model of {search_term} in {model_type}")
127
- if model_type not in folders.keys():
128
  util.printD("unknow model type: " + model_type)
129
- return
130
-
131
- # for lora: search_term = subfolderpath + model name + ext + " " + hash. And it always start with a / even there is no sub folder
 
132
  # for ckp: search_term = subfolderpath + model name + ext + " " + hash
133
  # for ti: search_term = subfolderpath + model name + ext + " " + hash
134
  # for hyper: search_term = subfolderpath + model name
135
- has_hash = True
136
- if model_type == "hyper":
137
- has_hash = False
138
- elif search_term.endswith(".pt") or search_term.endswith(".bin") or search_term.endswith(".safetensors") or search_term.endswith(".ckpt"):
139
- has_hash = False
140
-
141
- # remove hash
142
- # model name may have multiple spaces
143
- splited_path = search_term.split()
144
- model_sub_path = splited_path[0]
145
- if has_hash and len(splited_path) > 1:
146
- model_sub_path = ""
147
- for i in range(0, len(splited_path)-1):
148
- model_sub_path += splited_path[i] + " "
149
-
150
- model_sub_path = model_sub_path.strip()
151
 
 
 
 
 
 
 
 
 
152
 
153
  if model_sub_path[:1] == "/":
154
  model_sub_path = model_sub_path[1:]
155
 
156
- if model_type == "hyper":
157
- model_sub_path = model_sub_path+".pt"
 
 
158
 
159
- model_folder = folders[model_type]
 
 
160
 
161
- model_path = os.path.join(model_folder, model_sub_path)
 
162
 
163
- print("model_folder: " + model_folder)
164
- print("model_sub_path: " + model_sub_path)
165
- print("model_path: " + model_path)
 
 
 
 
166
 
167
  if not os.path.isfile(model_path):
168
- util.printD("Can not find model file: " + model_path)
169
- return
170
-
171
  return model_path
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ -*- coding: UTF-8 -*-
2
+ Handle model operations
3
+ """
4
  import os
5
  import json
6
+ import re
7
+ from PIL import Image
8
+ import piexif
9
+ import piexif.helper
10
  from modules import shared
11
+ from modules import paths_internal
12
+ from . import civitai
13
+ from . import downloader
14
+ from . import util
15
 
16
 
17
  # this is the default root path
18
+ ROOT_PATH = paths_internal.data_path
19
+
20
+ EXTS = (".bin", ".pt", ".safetensors", ".ckpt")
21
+ CIVITAI_EXT = ".info"
22
+ SDWEBUI_EXT = ".json"
23
+
24
+ """
25
+ If command line arguement is used to change model folder,
26
+ then model folder is in absolute path, not based on this root path anymore.
27
+ so to make extension work with those absolute model folder paths, model
28
+ folder also need to be in absolute path
29
+ """
30
  folders = {
31
+ "ti": os.path.join(ROOT_PATH, "embeddings"),
32
+ "hyper": os.path.join(ROOT_PATH, "models", "hypernetworks"),
33
+ "ckp": os.path.join(ROOT_PATH, "models", "Stable-diffusion"),
34
+ "lora": os.path.join(ROOT_PATH, "models", "Lora"),
35
+ "lycoris": os.path.join(ROOT_PATH, "models", "LyCORIS"),
36
  }
37
 
38
+ # Separate because the above is used for detecting supported models
39
+ # in other features
40
+ vae_folder = os.path.join(ROOT_PATH, "models", "VAE")
41
+
42
+
43
+ class VersionMismatchException(Exception):
44
+ """ Used for version comarison failures """
45
+
46
+ def __init__(self, value):
47
+ self.value = value
48
+
49
+ def __str__(self):
50
+ return repr(self.value)
51
+
52
 
53
+ def get_model_info_paths(model_path):
54
+ """
55
+ Retrieve model info paths
56
+ return: (info_file:str, sd15_file:str)
57
+ """
58
+ base, _ = os.path.splitext(model_path)
59
+ info_file = f"{base}{civitai.SUFFIX}{CIVITAI_EXT}"
60
+ sd15_file = f"{base}{SDWEBUI_EXT}"
61
+ return (info_file, sd15_file)
62
 
63
+
64
+ # get custom model path
65
  def get_custom_model_folder():
66
+ """
67
+ Update extra network directories with user-specified values.
68
+ """
69
  util.printD("Get Custom Model Folder")
70
 
 
 
71
  if shared.cmd_opts.embeddings_dir and os.path.isdir(shared.cmd_opts.embeddings_dir):
72
  folders["ti"] = shared.cmd_opts.embeddings_dir
73
 
 
80
  if shared.cmd_opts.lora_dir and os.path.isdir(shared.cmd_opts.lora_dir):
81
  folders["lora"] = shared.cmd_opts.lora_dir
82
 
83
+ if shared.cmd_opts.vae_dir and os.path.isdir(shared.cmd_opts.vae_dir):
84
+ vae_folder = shared.cmd_opts.vae_dir
85
+
86
+ if util.get_opts("ch_dl_lyco_to_lora"):
87
+ folders["lycoris"] = folders["lora"]
88
+
89
+ try:
90
+ # pre-1.5.0
91
+ if os.path.isdir(shared.cmd_opts.lyco_dir):
92
+ folders["lycoris"] = shared.cmd_opts.lyco_dir
93
+
94
+ except AttributeError:
95
+ try:
96
+ # sd-webui v1.5.1 added a backcompat option for lyco.
97
+ if os.path.isdir(shared.cmd_opts.lyco_dir_backcompat):
98
+ folders["lycoris"] = shared.cmd_opts.lyco_dir_backcompat
99
+
100
+ except AttributeError:
101
+ # v1.5.0 has no options for the Lyco dir:
102
+ # it is hardcoded as 'os.path.join(paths.models_path, "LyCORIS")'
103
+ return
104
+
105
+
106
+ def metadata_needed(info_file, sd15_file, refetch_old):
107
+ """ return True if metadata is needed
108
+ """
109
+
110
+ need_civitai = metadata_needed_for_type(info_file, "civitai", refetch_old)
111
+ need_sdwebui = metadata_needed_for_type(sd15_file, "sdwebui", refetch_old)
112
+
113
+ return need_civitai or need_sdwebui
114
+
115
+
116
+ def metadata_needed_for_type(path, meta_type, refetch_old):
117
+ """ return True if metadata is needed for path
118
+ """
119
+
120
+ if meta_type == "sdwebui" and not util.get_opts("ch_dl_webui_metadata"):
121
+ return False
122
+
123
+ if not os.path.isfile(path):
124
+ return True
125
+
126
+ if refetch_old:
127
+ metadata = None
128
+ with open(path) as file:
129
+ metadata = json.load(file)
130
+
131
+ metadata_version = util.metadata_version(metadata)
132
+
133
+ if not metadata_version:
134
+ return True
135
+
136
+ if meta_type == "civitai":
137
+ compat_version = util.COMPAT_VERSION_CIVITAI
138
+ else:
139
+ compat_version = util.COMPAT_VERSION_SDWEBUI
140
+
141
+ return util.newer_version(compat_version, metadata_version)
142
+
143
+ return False
144
+
145
 
146
+ def verify_overwrite_eligibility(path, new_data):
147
+ """
148
+ Verifies a file is valid to be overwritten
149
+ Throws an error if the model ID does not match the new version's model ID
150
+ return: True if valid, False if not.
151
+ """
152
+ if not os.path.isfile(path):
153
+ return True
154
 
155
+ with open(path, "r") as file:
156
+ old_data = json.load(file)
157
 
158
+ if "civitai" in path:
159
+ new_id = new_data.get("id", "")
160
+ old_id = old_data.get("id", "")
161
+ if new_id != old_id:
162
+ if old_id != "":
163
+ raise VersionMismatchException(
164
+ f"New metadata id ({new_id}) does not match old metadata id ({old_id})"
165
+ )
166
 
167
+ new_description = new_data.get("description", "")
168
+ old_description = old_data.get("description", "")
169
+ if new_description == "" and old_description != "":
170
+ util.printD(
171
+ f"New description is blank while old description contains data. Skipping {path}"
172
+ )
173
+ return False
174
+
175
+ return True
176
+
177
+
178
+ def write_info(data, path, info_type):
179
+ """ Writes model info to a file """
180
+ util.printD(f"Write model {info_type} info to file: {path}")
181
+ with open(os.path.realpath(path), 'w') as info_file:
182
+ info_file.write(json.dumps(data, indent=4))
183
+
184
+
185
+ def process_model_info(model_path, model_info, model_type="ckp", refetch_old=False):
186
+ """
187
+ Write model info to file
188
+
189
+ SD1.5 Webui added saving model information to JSON files.
190
+ Much of this extension's metadata management is replicated
191
+ by this new functionality, including automatically adding
192
+ activator keywords to the prompt. It also provides a much
193
+ cleaner UI than civitai (not a high bar to clear) to
194
+ simply read a model's description.
195
+
196
+ So why not populate it with useful information?
197
+
198
+ Returns True if successful, otherwise an error message.
199
+ """
200
+
201
+ if model_info is None:
202
+ util.printD("Failed to get model info.")
203
+ return
204
+
205
+ info_file, sd15_file = get_model_info_paths(model_path)
206
+
207
+ parent = model_info["model"]
208
+
209
+ description = parent.get("description", "")
210
+ if description:
211
+ description = util.trim_html(description)
212
+ parent["description"] = description
213
+
214
+ version_description = model_info.get("description", "")
215
+ if version_description:
216
+ version_description = util.trim_html(version_description)
217
+ model_info["description"] = version_description
218
+
219
+ # Create extension versioning information so that users
220
+ # can replace stale info files without newer entries.
221
+ model_info["extensions"] = util.create_extension_block(
222
+ model_info.get("extensions", None),
223
+ model_info.get("skeleton_file", False)
224
+ )
225
+
226
+ # civitai model info file
227
+ if metadata_needed_for_type(info_file, "civitai", refetch_old):
228
+ if refetch_old:
229
+ try:
230
+ if verify_overwrite_eligibility(info_file, model_info):
231
+ write_info(model_info, info_file, "civitai")
232
+
233
+ except VersionMismatchException as e:
234
+ util.printD(f"{e}, aborting")
235
+ return
236
+
237
+ else:
238
+ write_info(model_info, info_file, "civitai")
239
+
240
+ if not util.get_opts("ch_dl_webui_metadata"):
241
+ return
242
+
243
+ # Do not overwrite user-created files!
244
+ # TODO: maybe populate empty fields in existing files?
245
+ if not metadata_needed_for_type(sd15_file, "sdwebui", refetch_old):
246
+ util.printD(f"Metadata not needed for: {sd15_file}.")
247
+ return
248
+
249
+ process_sd15_info(sd15_file, model_info, parent, model_type, refetch_old)
250
+
251
+
252
+ def process_sd15_info(sd15_file, model_info, parent, model_type, refetch_old):
253
+ """ Creates/Processes [model_name].json """
254
+
255
+ # sd v1.5 model info file
256
+ sd_data = {}
257
+
258
+ sd_data["description"] = parent.get("description", "")
259
+
260
+ # I suppose notes are more for user notes, but populating it
261
+ # with potentially useful information about this particular
262
+ # version of the model is fine too, right? The user can
263
+ # always replace these if they're unneeded or add to them
264
+ version_info = model_info.get("description", None)
265
+ if version_info is not None:
266
+ sd_data["notes"] = version_info
267
+
268
+ # AFAIK civitai model versions are currently:
269
+ # SD 1.4, SD 1.5, SD 2.0, SD 2.0 786, SD 2.1, SD 2.1 786
270
+ # SD 2.1 Unclip, SDXL 0.9, SDXL 1.0, and Other.
271
+ # Conveniently, the 4th character is all we need for webui.
272
+ #
273
+ # INFO: On Civitai, all models list base model/"sd version".
274
+ # The SD WebUI interface only displays them for Lora/Lycoris.
275
+ #
276
+ # I'm populating the field anyways in hopes it eventually gets
277
+ # added.
278
+ base_model = model_info.get("baseModel", None)
279
+ sd_version = 'Unknown'
280
+ if base_model:
281
+ version = base_model[3]
282
+
283
+ sd_version = {
284
+ "1": 'SD1',
285
+ "2": 'SD2',
286
+ "L": 'SDXL',
287
+ }.get(version, 'Unknown')
288
+
289
+ sd_data["sd version"] = sd_version
290
+
291
+ for filedata in model_info["files"]:
292
+ if filedata["type"] == "VAE":
293
+ sd_data["vae"] = filedata["name"]
294
+
295
+ # INFO: On Civitai, all non-checkpoint models can have trained words.
296
+ # The SD WebUI interface only displays them for Lora/Lycoris.
297
+ # I'm populating the field anyways in hopes it eventually gets
298
+ # added.
299
+ #
300
+ # "trained words" usage is inconsistent among model authors.
301
+ # Some use each entry as an individual activator, while others
302
+ # use them as entire prompts
303
+ activator = model_info.get("trainedWords", [])
304
+ if (activator and activator[0]):
305
+ if "," in activator[0]:
306
+ # assume trainedWords is a prompt list
307
+
308
+ # webui does not support newlines in activator text
309
+ # so this is the best hinting I can give the user at the
310
+ # moment that these are mutually-exclusive prompts.
311
+ sd_data["activation text"] = " || ".join(activator)
312
+ else:
313
+ # assume trainedWords are single keywords
314
+ sd_data["activation text"] = ", ".join(activator)
315
+
316
+ # Sadly, Civitai does not provide default weight information,
317
+ # So 0 disables this functionality on webui's end and uses
318
+ # the user's global setting
319
+ if model_type in ["lora", "lycoris"]:
320
+ sd_data["preferred weight"] = 0
321
+
322
+ sd_data["extensions"] = util.create_extension_block(
323
+ model_info.get("extensions", None),
324
+ model_info.get("skeleton_file", False)
325
+ )
326
+
327
+ if refetch_old:
328
+ if verify_overwrite_eligibility(sd15_file, sd_data):
329
+ write_info(sd_data, sd15_file, "webui")
330
+ else:
331
+ write_info(sd_data, sd15_file, "webui")
332
 
333
 
334
  def load_model_info(path):
335
+ """ Opens a JSON file and loads its JSON """
336
  model_info = None
337
+ with open(os.path.realpath(path), 'r') as json_file:
338
  try:
339
+ model_info = json.load(json_file)
340
+ except ValueError:
341
+ util.printD(f"Selected file is not json: {path}")
342
+ return None
343
+
 
344
  return model_info
345
 
346
 
347
+ def get_potential_model_preview_files(model_path, all_prevs=False):
348
+ """
349
+ Find existing preview images, if any.
350
+
351
+ Extensions from `find_preview` method in webui `modules/ui_extra_networks.py`
352
+ gif added in webui commit c602471b85d270e8c36707817d9bad92b0ff991e
353
+
354
+ return: preview_files
355
+ """
356
+ preview_exts = ["png", "jpg", "jpeg", "webp", "gif"]
357
+ preview_files = []
358
+
359
+ base, _ = os.path.splitext(model_path)
360
+
361
+ for ext in preview_exts:
362
+ if all_prevs:
363
+ preview_files.append(f"{base}.{ext}")
364
+ preview_files.append(f"{base}.preview.{ext}")
365
+
366
+ return preview_files
367
+
368
+
369
+ def get_model_files_from_model_path(model_path):
370
+ """ return: list of paths """
371
+
372
+ base, _ = os.path.splitext(model_path)
373
+
374
+ info_file, sd15_file = get_model_info_paths(model_path)
375
+ user_preview_path = f"{base}.png"
376
+
377
+ paths = [model_path, info_file, sd15_file, user_preview_path]
378
+ preview_paths = get_potential_model_preview_files(model_path)
379
+
380
+ paths = paths + preview_paths
381
+
382
+ return [path for path in paths if os.path.isfile(path)]
383
+
384
+
385
  def get_model_names_by_type(model_type:str) -> list:
386
+ """
387
+ get model file names by model type
388
+ parameter: model_type - string
389
+ return: model name list
390
+ """
391
+
392
+ if model_type == "lora" and folders['lycoris']:
393
+ model_folders = [folders[model_type], folders['lycoris']]
394
+ else:
395
+ model_folders = [folders[model_type]]
396
 
397
  # get information from filter
398
  # only get those model names don't have a civitai model info file
399
  model_names = []
400
+ for model_folder in model_folders:
401
+ for root, _, files in os.walk(model_folder, followlinks=True):
402
+ for filename in files:
403
+ item = os.path.join(root, filename)
404
+ # check extension
405
+ _, ext = os.path.splitext(item)
406
+ if ext in EXTS:
407
+ # find a model
408
+ model_names.append(filename)
409
 
410
  return model_names
411
 
412
 
413
  # return 2 values: (model_root, model_path)
414
  def get_model_path_by_type_and_name(model_type:str, model_name:str) -> str:
415
+ """ return: model_path:str matching model_name and model_type """
416
  util.printD("Run get_model_path_by_type_and_name")
 
 
 
 
417
  if not model_name:
418
  util.printD("model name can not be empty")
419
+ return None
 
 
420
 
421
+ model_folders = [folders.get(model_type, None)]
422
+
423
+ if model_folders[0] is None:
424
+ util.printD(f"unknown model_type: {model_type}")
425
+ return None
 
 
 
 
 
426
 
427
+ if model_type == "lora" and folders['lycoris']:
428
+ model_folders.append(folders['lycoris'])
429
 
430
+ # model could be in subfolder, need to walk.
431
+ model_path = util.find_file_in_folders(model_folders, model_name)
432
+
433
+ msg = util.indented_msg(f"""
434
+ Got following info:
435
+ {model_path=}
436
+ """)
437
+ util.printD(msg)
438
 
439
+ # May return `None`
440
+ return model_path
441
 
442
 
443
  # get model path by model type and search_term
444
  # parameter: model_type, search_term
445
  # return: model_path
446
+ def get_model_path_by_search_term(model_type, search_term):
447
+ """
448
+ Gets a model path based on the webui search term.
449
+
450
+ return: model_path
451
+ """
452
  util.printD(f"Search model of {search_term} in {model_type}")
453
+ if folders.get(model_type, None) is None:
454
  util.printD("unknow model type: " + model_type)
455
+ return None
456
+
457
+ # for lora: search_term = subfolderpath + model name + ext + " " + hash.
458
+ # And it always start with a / even there is no sub folder
459
  # for ckp: search_term = subfolderpath + model name + ext + " " + hash
460
  # for ti: search_term = subfolderpath + model name + ext + " " + hash
461
  # for hyper: search_term = subfolderpath + model name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
 
463
+ # this used to be
464
+ # `model_sub_path = search_term.split()[0]`
465
+ # but it was failing on models containing spaces.
466
+ model_hash = search_term.split()[-1]
467
+ model_sub_path = search_term.replace(f" {model_hash}", "")
468
+
469
+ if model_type == "hyper":
470
+ model_sub_path = f"{search_term}.pt"
471
 
472
  if model_sub_path[:1] == "/":
473
  model_sub_path = model_sub_path[1:]
474
 
475
+ if model_type == "lora" and folders['lycoris']:
476
+ model_folders = [folders[model_type], folders['lycoris']]
477
+ else:
478
+ model_folders = [folders[model_type]]
479
 
480
+ for folder in model_folders:
481
+ model_folder = folder
482
+ model_path = os.path.join(model_folder, model_sub_path)
483
 
484
+ if os.path.isfile(model_path):
485
+ break
486
 
487
+ msg = util.indented_msg(f"""
488
+ Got following info:
489
+ {model_folder=}
490
+ {model_sub_path=}
491
+ {model_path=}
492
+ """)
493
+ util.printD(msg)
494
 
495
  if not os.path.isfile(model_path):
496
+ util.printD(f"Can not find model file: {model_path}")
497
+ return None
498
+
499
  return model_path
500
 
501
+
502
+ pattern = re.compile(r"\s*([^:,]+):\s*([^,]+)")
503
+
504
+ def sd_format(data):
505
+ """
506
+ Parse image exif data for image creation parameters.
507
+
508
+ return parameters:dict or None
509
+ """
510
+
511
+ if not data:
512
+ return None
513
+
514
+ prompt = ""
515
+ negative = ""
516
+ setting = ""
517
+
518
+ steps_index = data.find("\nSteps:")
519
+
520
+ if steps_index != -1:
521
+ prompt = data[:steps_index].strip()
522
+ setting = data[steps_index:].strip()
523
+
524
+ if "Negative prompt:" in data:
525
+ prompt_index = data.find("\nNegative prompt:")
526
+
527
+ if steps_index != -1:
528
+ negative = data[
529
+ prompt_index + len("Negative prompt:") + 1 : steps_index
530
+ ].strip()
531
+
532
+ else:
533
+ negative = data[
534
+ prompt_index + len("Negative prompt:") + 1 :
535
+ ].strip()
536
+
537
+ prompt = data[:prompt_index].strip()
538
+
539
+ elif steps_index == -1:
540
+ prompt = data
541
+
542
+ setting_dict = dict(re.findall(pattern, setting))
543
+
544
+ data = {
545
+ "prompt": prompt,
546
+ "negative": negative,
547
+ "Steps": setting_dict.get("Steps", ""),
548
+ "Sampler": setting_dict.get("Sampler", ""),
549
+ "CFG_scale": setting_dict.get("CFG scale", ""),
550
+ "Seed": setting_dict.get("Seed", ""),
551
+ "Size": setting_dict.get("Size", ""),
552
+ }
553
+
554
+ return data
555
+
556
+
557
+ def parse_image(image_file):
558
+ """
559
+ Read image exif for userComment entry.
560
+ return: userComment:str
561
+ """
562
+ data = None
563
+ with Image.open(image_file) as image:
564
+ if image.format == "PNG":
565
+ # However, unlike other image formats, EXIF data is not
566
+ # guaranteed to be present in info until load() has been called.
567
+ # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#png
568
+ image.load()
569
+ data = image.info.get("parameters")
570
+
571
+ elif image.format in ["JPEG", "WEBP"]:
572
+ try:
573
+ usercomment = piexif.ExifIFD.UserComment
574
+ exif = image.info.get("exif")
575
+ if not exif:
576
+ return None
577
+ jpegexif = piexif.load(exif) or {}
578
+ data = piexif.helper.UserComment.load(
579
+ jpegexif.get("Exif", {}).get(usercomment, None)
580
+ )
581
+
582
+ except (ValueError, TypeError):
583
+ util.printD("Failed to parse image exif.")
584
+ return None
585
+
586
+ return data
587
+
588
+
589
+ def get_remote_image_info(img_src):
590
+ """
591
+ Download a remote image and parse out its creation parameters
592
+
593
+ return parameters:dict or None
594
+ """
595
+ # anti-DDOS protection
596
+ util.delay(0.2)
597
+
598
+ success, response = downloader.request_get(img_src)
599
+
600
+ if not success:
601
+ return None
602
+
603
+ image_file = response.raw
604
+ try:
605
+ data = parse_image(image_file)
606
+
607
+ except OSError: #, UnidentifiedImageError
608
+ util.printD("Failed to open image.")
609
+ return None
610
+
611
+ if not data:
612
+ return None
613
+
614
+ sd_data = sd_format(data)
615
+ return sd_data
616
+
617
+
618
+ def update_civitai_info_image_meta(filename):
619
+ """
620
+ Read model metadata and update missing image creation parameters,
621
+ if available.
622
+ """
623
+ need_update = False
624
+ data = {}
625
+
626
+ if not os.path.isfile(filename):
627
+ return
628
+
629
+ with open(filename, 'r') as model_json:
630
+ data = json.load(model_json)
631
+
632
+ for image in data.get('images', []):
633
+ metadata = image.get('meta', None)
634
+ if not metadata and metadata != {}:
635
+ url = image.get("url", "")
636
+ if not url:
637
+ continue
638
+
639
+ util.printD(f"{filename} missing generation info for {url}. Processing {url}.")
640
+
641
+ image_data = get_remote_image_info(url)
642
+ if not image_data:
643
+ util.printD(f"Failed to find generation info on remote image at {url}.")
644
+
645
+ # "mark" image so additional runs will skip it.
646
+ image["meta"] = {}
647
+ need_update = True
648
+ continue
649
+
650
+ util.printD(
651
+ "The following information will be added to "
652
+ f"{filename} for {url}:\n{image_data}"
653
+ )
654
+ metadata = image_data
655
+ image["meta"] = metadata
656
+
657
+ need_update = True
658
+
659
+ if need_update:
660
+ with open(filename, 'w') as info_file:
661
+ json.dump(data, info_file, indent=4)
662
+
663
+
664
+ def scan_civitai_info_image_meta():
665
+ """ Search for *.civitai.info files """
666
+ util.printD("Start Scan_civitai_info_image_meta")
667
+ output = ""
668
+ count = 0
669
+
670
+ directories = [y for x, y in folders.items() if os.path.isdir(y)]
671
+ util.printD(f"{directories=}")
672
+ for directory in directories:
673
+ for root, _, files in os.walk(directory):
674
+ for filename in files:
675
+ if filename.endswith('.civitai.info'):
676
+ update_civitai_info_image_meta(os.path.join(root, filename))
677
+ count = count + 1
678
+
679
+ output = f"Done. Scanned {count} files."
680
+ util.printD(output)
681
+ return output
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/model_action_civitai.py CHANGED
@@ -1,511 +1,828 @@
1
- # -*- coding: UTF-8 -*-
2
- # handle msg between js and python side
 
3
  import os
4
  import time
 
 
 
5
  from . import util
6
  from . import model
7
  from . import civitai
8
  from . import downloader
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- # scan model to generate SHA256, then use this SHA256 to get model info from civitai
12
- # return output msg
13
- def scan_model(scan_model_types, max_size_preview, skip_nsfw_preview):
14
  util.printD("Start scan_model")
15
  output = ""
16
 
 
 
17
  # check model types
18
  if not scan_model_types:
19
  output = "Model Types is None, can not scan."
20
  util.printD(output)
21
- return output
22
-
23
- model_types = []
24
- # check type if it is a string
25
- if type(scan_model_types) == str:
26
- model_types.append(scan_model_types)
27
- else:
28
- model_types = scan_model_types
29
-
30
- model_count = 0
31
- image_count = 0
32
- # scan_log = ""
33
  for model_type, model_folder in model.folders.items():
34
  if model_type not in model_types:
35
  continue
36
 
37
- util.printD("Scanning path: " + model_folder)
38
- for root, dirs, files in os.walk(model_folder, followlinks=True):
39
  for filename in files:
 
40
  # check ext
41
- item = os.path.join(root, filename)
42
- base, ext = os.path.splitext(item)
43
- if ext in model.exts:
44
- # ignore vae file
45
- if len(base) > 4:
46
- if base[-4:] == model.vae_suffix:
47
- # find .vae
48
- util.printD("This is a vae file: " + filename)
49
- continue
50
-
51
- # find a model
52
- # get info file
53
- info_file = base + civitai.suffix + model.info_ext
54
- # check info file
55
- if not os.path.isfile(info_file):
56
- util.printD("Creating model info for: " + filename)
57
- # get model's sha256
58
- hash = util.gen_file_sha256(item)
59
-
60
- if not hash:
61
- output = "failed generating SHA256 for model:" + filename
62
- util.printD(output)
63
- return output
64
-
65
- # use this sha256 to get model info from civitai
66
- model_info = civitai.get_model_info_by_hash(hash)
67
- # delay 1 second for ti
68
- if model_type == "ti":
69
- util.printD("Delay 1 second for TI")
70
- time.sleep(1)
71
-
72
- if model_info is None:
73
- output = "Connect to Civitai API service failed. Wait a while and try again"
74
- util.printD(output)
75
- return output+", check console log for detail"
76
-
77
- # write model info to file
78
- model.write_model_info(info_file, model_info)
79
-
80
- # set model_count
81
- model_count = model_count+1
82
-
83
- # check preview image
84
- civitai.get_preview_image_by_model_path(item, max_size_preview, skip_nsfw_preview)
85
- image_count = image_count+1
86
-
87
-
88
- # scan_log = "Done"
89
-
90
- output = f"Done. Scanned {model_count} models, checked {image_count} images"
91
 
92
- util.printD(output)
93
 
94
- return output
 
 
 
95
 
 
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- # Get model info by model type, name and url
99
- # output is log info to display on markdown component
100
- def get_model_info_by_input(model_type, model_name, model_url_or_id, max_size_preview, skip_nsfw_preview):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  output = ""
 
 
 
102
  # parse model id
103
  model_id = civitai.get_model_id_from_url(model_url_or_id)
104
  if not model_id:
105
- output = "failed to parse model id from url: " + model_url_or_id
106
  util.printD(output)
107
- return output
 
108
 
109
  # get model file path
110
  # model could be in subfolder
111
- result = model.get_model_path_by_type_and_name(model_type, model_name)
112
- if not result:
113
- output = "failed to get model file path"
114
- util.printD(output)
115
- return output
116
-
117
- model_root, model_path = result
118
- if not model_path:
119
- output = "model path is empty"
120
  util.printD(output)
121
- return output
122
-
123
- # get info file path
124
- base, ext = os.path.splitext(model_path)
125
- info_file = base + civitai.suffix + model.info_ext
126
 
127
- # get model info
128
  #we call it model_info, but in civitai, it is actually version info
129
  model_info = civitai.get_version_info_by_model_id(model_id)
130
 
131
- if not model_info:
132
- output = "failed to get model info from url: " + model_url_or_id
133
- util.printD(output)
134
- return output
135
-
136
- # write model info to file
137
- model.write_model_info(info_file, model_info)
138
 
139
- util.printD("Saved model info to: "+ info_file)
 
 
 
 
 
140
 
141
- # check preview image
142
- civitai.get_preview_image_by_model_path(model_path, max_size_preview, skip_nsfw_preview)
143
 
144
- output = "Model Info saved to: " + info_file
145
- return output
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
 
149
- # check models' new version and output to UI as markdown doc
150
  def check_models_new_version_to_md(model_types:list) -> str:
151
- new_versions = civitai.check_models_new_version_by_model_types(model_types, 1)
 
 
 
 
152
 
153
- count = 0
154
- output = ""
155
  if not new_versions:
156
- output = "No model has new version"
 
 
 
 
 
 
 
 
 
 
 
 
157
  else:
158
- output = "Found new version for following models: <br>"
159
- for new_version in new_versions:
160
- count = count+1
161
- model_path, model_id, model_name, new_verion_id, new_version_name, description, download_url, img_url = new_version
162
- # in md, each part is something like this:
163
- # [model_name](model_url)
164
- # [version_name](download_url)
165
- # version description
166
- url = civitai.url_dict["modelPage"]+str(model_id)
167
-
168
- part = f'<div style="font-size:20px;margin:6px 0px;"><b>Model: <a href="{url}" target="_blank"><u>{model_name}</u></a></b></div>'
169
- part = part + f'<div style="font-size:16px">File: {model_path}</div>'
170
- if download_url:
171
- # replace "\" to "/" in model_path for windows
172
- model_path = model_path.replace('\\', '\\\\')
173
- part = part + f'<div style="font-size:16px;margin:6px 0px;">New Version: <u><a href="{download_url}" target="_blank" style="margin:0px 10px;">{new_version_name}</a></u>'
174
- # add js function to download new version into SD webui by python
175
- part = part + " "
176
- # in embed HTML, onclick= will also follow a ", never a ', so have to write it as following
177
- part = part + f"<u><a href='#' style='margin:0px 10px;' onclick=\"ch_dl_model_new_version(event, '{model_path}', '{new_verion_id}', '{download_url}')\">[Download into SD]</a></u>"
178
-
179
- else:
180
- part = part + f'<div style="font-size:16px;margin:6px 0px;">New Version: {new_version_name}'
181
- part = part + '</div>'
182
 
183
- # description
184
- if description:
185
- part = part + '<blockquote style="font-size:16px;margin:6px 0px;">'+ description + '</blockquote><br>'
186
 
187
- # preview image
188
- if img_url:
189
- part = part + f"<img src='{img_url}'><br>"
190
-
191
 
192
- output = output + part
 
 
 
 
193
 
194
- util.printD(f"Done. Find {count} models have new version. Check UI for detail.")
 
 
195
 
196
- return output
 
 
197
 
 
 
198
 
199
- # get model info by url
200
- def get_model_info_by_url(model_url_or_id:str) -> tuple:
201
- util.printD("Getting model info by: " + model_url_or_id)
202
 
203
- # parse model id
204
- model_id = civitai.get_model_id_from_url(model_url_or_id)
205
- if not model_id:
206
- util.printD("failed to parse model id from url or id")
207
- return
208
 
209
- model_info = civitai.get_model_info_by_id(model_id)
210
- if model_info is None:
211
- util.printD("Connect to Civitai API service failed. Wait a while and try again")
212
- return
213
-
214
- if not model_info:
215
- util.printD("failed to get model info from url or id")
216
- return
217
-
218
- # parse model type, model name, subfolder, version from this model info
219
- # get model type
220
- if "type" not in model_info.keys():
221
- util.printD("model type is not in model_info")
222
- return
223
-
224
- civitai_model_type = model_info["type"]
225
- if civitai_model_type not in civitai.model_type_dict.keys():
226
- util.printD("This model type is not supported:"+civitai_model_type)
227
- return
228
-
229
- model_type = civitai.model_type_dict[civitai_model_type]
230
 
231
- # get model type
232
- if "name" not in model_info.keys():
233
- util.printD("model name is not in model_info")
234
- return
235
 
236
- model_name = model_info["name"]
237
- if not model_name:
238
- util.printD("model name is Empty")
239
- model_name = ""
240
 
241
- # get version list
242
- if "modelVersions" not in model_info.keys():
243
- util.printD("modelVersions is not in model_info")
244
- return
245
-
246
- modelVersions = model_info["modelVersions"]
247
- if not modelVersions:
248
- util.printD("modelVersions is Empty")
249
- return
250
-
251
  version_strs = []
252
- for version in modelVersions:
 
253
  # version name can not be used as id
254
  # version id is not readable
255
  # so , we use name_id as version string
256
- version_str = version["name"]+"_"+str(version["id"])
 
 
 
 
 
 
 
 
 
 
 
 
257
  version_strs.append(version_str)
 
258
 
259
  # get folder by model type
260
  folder = model.folders[model_type]
261
- # get subfolders
262
- subfolders = util.get_subfolders(folder)
263
- if not subfolders:
264
- subfolders = []
265
 
266
- # add default root folder
267
- subfolders.append("/")
268
-
269
- util.printD("Get following info for downloading:")
270
- util.printD(f"model_name:{model_name}")
271
- util.printD(f"model_type:{model_type}")
272
- util.printD(f"subfolders:{subfolders}")
273
- util.printD(f"version_strs:{version_strs}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
- return (model_info, model_name, model_type, subfolders, version_strs)
276
 
277
- # get version info by version string
278
  def get_ver_info_by_ver_str(version_str:str, model_info:dict) -> dict:
279
- if not version_str:
280
- util.printD("version_str is empty")
281
- return
282
-
283
- if not model_info:
284
- util.printD("model_info is None")
285
- return
286
-
287
- # get version list
288
- if "modelVersions" not in model_info.keys():
289
- util.printD("modelVersions is not in model_info")
290
- return
 
 
 
 
291
 
292
- modelVersions = model_info["modelVersions"]
293
- if not modelVersions:
 
294
  util.printD("modelVersions is Empty")
295
- return
296
-
297
  # find version by version_str
298
  version = None
299
- for ver in modelVersions:
300
  # version name can not be used as id
301
  # version id is not readable
302
  # so , we use name_id as version string
303
- ver_str = ver["name"]+"_"+str(ver["id"])
304
  if ver_str == version_str:
305
  # find version
306
  version = ver
 
 
 
 
 
307
 
308
- if not version:
309
- util.printD("can not find version by version string: " + version_str)
310
- return
311
-
312
- # get version id
313
- if "id" not in version.keys():
314
- util.printD("this version has no id")
315
- return
316
-
317
  return version
318
 
319
 
320
- # get download url from model info by version string
321
- # return - (version_id, download_url)
322
  def get_id_and_dl_url_by_version_str(version_str:str, model_info:dict) -> tuple:
323
- if not version_str:
324
- util.printD("version_str is empty")
325
- return
326
-
327
- if not model_info:
328
- util.printD("model_info is None")
329
- return
330
-
331
- # get version list
332
- if "modelVersions" not in model_info.keys():
333
- util.printD("modelVersions is not in model_info")
334
- return
335
 
336
- modelVersions = model_info["modelVersions"]
337
- if not modelVersions:
 
338
  util.printD("modelVersions is Empty")
339
- return
340
-
341
  # find version by version_str
342
  version = None
343
- for ver in modelVersions:
344
  # version name can not be used as id
345
  # version id is not readable
346
  # so , we use name_id as version string
347
- ver_str = ver["name"]+"_"+str(ver["id"])
348
  if ver_str == version_str:
349
  # find version
350
  version = ver
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
- if not version:
353
- util.printD("can not find version by version string: " + version_str)
354
- return
355
-
356
- # get version id
357
- if "id" not in version.keys():
358
- util.printD("this version has no id")
359
- return
360
-
361
- version_id = version["id"]
362
- if not version_id:
363
- util.printD("version id is Empty")
364
- return
365
 
366
- # get download url
367
- if "downloadUrl" not in version.keys():
368
- util.printD("downloadUrl is not in this version")
369
- return
370
-
371
- downloadUrl = version["downloadUrl"]
372
- if not downloadUrl:
373
- util.printD("downloadUrl is Empty")
374
- return
375
-
376
- util.printD("Get Download Url: " + downloadUrl)
377
-
378
- return (version_id, downloadUrl)
379
-
380
 
381
- # download model from civitai by input
382
- # output to markdown log
383
- def dl_model_by_input(model_info:dict, model_type:str, subfolder_str:str, version_str:str, dl_all_bool:bool, max_size_preview:bool, skip_nsfw_preview:bool) -> str:
 
384
 
385
- output = ""
 
 
386
 
387
- if not model_info:
388
- output = "model_info is None"
389
- util.printD(output)
390
- return output
391
-
392
- if not model_type:
393
- output = "model_type is None"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  util.printD(output)
395
- return output
396
-
397
- if not subfolder_str:
398
- output = "subfolder string is None"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  util.printD(output)
400
- return output
401
-
402
- if not version_str:
403
- output = "version_str is None"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  util.printD(output)
405
- return output
406
-
 
407
  # get model root folder
408
- if model_type not in model.folders.keys():
409
- output = "Unknow model type: "+model_type
410
  util.printD(output)
411
- return output
412
-
 
 
 
 
 
 
 
 
 
 
 
413
  model_root_folder = model.folders[model_type]
414
 
 
 
 
415
 
416
  # get subfolder
417
- subfolder = ""
418
- if subfolder_str == "/" or subfolder_str == "\\":
419
  subfolder = ""
420
- elif subfolder_str[:1] == "/" or subfolder_str[:1] == "\\":
421
  subfolder = subfolder_str[1:]
422
  else:
423
  subfolder = subfolder_str
424
 
425
  # get model folder for downloading
426
- model_folder = os.path.join(model_root_folder, subfolder)
427
- if not os.path.isdir(model_folder):
428
- output = "Model folder is not a dir: "+ model_folder
429
  util.printD(output)
430
- return output
431
-
 
432
  # get version info
433
  ver_info = get_ver_info_by_ver_str(version_str, model_info)
434
  if not ver_info:
435
- output = "Fail to get version info, check console log for detail"
436
  util.printD(output)
437
- return output
438
-
439
- version_id = ver_info["id"]
440
-
441
 
442
- if dl_all_bool:
443
- # get all download url from files info
444
- # some model versions have multiple files
445
- download_urls = []
446
- if "files" in ver_info.keys():
447
- for file_info in ver_info["files"]:
448
- if "downloadUrl" in file_info.keys():
449
- download_urls.append(file_info["downloadUrl"])
 
 
 
 
 
 
450
 
451
- if not len(download_urls):
452
- if "downloadUrl" in ver_info.keys():
453
- download_urls.append(ver_info["downloadUrl"])
454
 
 
455
 
456
- # check if this model is already existed
457
- r = civitai.search_local_model_info_by_version_id(model_folder, version_id)
458
- if r:
459
- output = "This model version is already existed"
460
- util.printD(output)
461
- return output
462
-
463
- # download
464
- filepath = ""
465
- for url in download_urls:
466
- model_filepath = downloader.dl(url, model_folder, None, None)
467
- if not model_filepath:
468
- output = "Downloading failed, check console log for detail"
469
- util.printD(output)
470
- return output
471
-
472
- if url == ver_info["downloadUrl"]:
473
- filepath = model_filepath
474
- else:
475
- # only download one file
476
- # get download url
477
- url = ver_info["downloadUrl"]
478
- if not url:
479
- output = "Fail to get download url, check console log for detail"
480
- util.printD(output)
481
- return output
482
-
483
- # download
484
- filepath = downloader.dl(url, model_folder, None, None)
485
- if not filepath:
486
- output = "Downloading failed, check console log for detail"
487
- util.printD(output)
488
- return output
489
-
490
 
491
- if not filepath:
492
- filepath = model_filepath
493
-
494
  # get version info
495
- version_info = civitai.get_version_info_by_version_id(version_id)
496
- if not version_info:
497
- output = "Model downloaded, but failed to get version info, check console log for detail. Model saved to: " + filepath
498
- util.printD(output)
499
- return output
500
-
501
- # write version info to file
502
- base, ext = os.path.splitext(filepath)
503
- info_file = base + civitai.suffix + model.info_ext
504
- model.write_model_info(info_file, version_info)
505
-
506
- # then, get preview image
507
- civitai.get_preview_image_by_model_path(filepath, max_size_preview, skip_nsfw_preview)
508
-
509
- output = "Done. Model downloaded to: " + filepath
510
  util.printD(output)
511
- return output
 
1
+ """ -*- coding: UTF-8 -*-
2
+ handle msg between js and python side
3
+ """
4
  import os
5
  import time
6
+ import re
7
+ import gradio as gr
8
+ from modules import sd_models
9
  from . import util
10
  from . import model
11
  from . import civitai
12
  from . import downloader
13
+ from . import templates
14
+
15
+
16
+ def get_metadata_skeleton():
17
+ """
18
+ Used to generate at least something when model is not on civitai.
19
+ """
20
+ metadata = {
21
+ "id": "",
22
+ "modelId": "",
23
+ "name": "",
24
+ "trainedWords": [],
25
+ "baseModel": "Unknown",
26
+ "description": "",
27
+ "model": {
28
+ "name": "",
29
+ "type": "",
30
+ "nsfw": "",
31
+ "poi": ""
32
+ },
33
+ "files": [
34
+ {
35
+ "name": "",
36
+ "sizeKB": 0,
37
+ "type": "Model",
38
+ "hashes": {
39
+ "AutoV2": "",
40
+ "SHA256": ""
41
+ }
42
+ }
43
+ ],
44
+ "tags": [],
45
+ "downloadUrl": "",
46
+ "skeleton_file": True
47
+ }
48
+
49
+ return metadata
50
+
51
+
52
+ def scan_single_model(filepath, model_type, refetch_old, delay):
53
+ """
54
+ Gets model info for a model by feeding its sha256 hash into civitai's api
55
+
56
+ return: success:bool
57
+ """
58
+
59
+ filename = os.path.basename(filepath)
60
+
61
+ # find a model, get info file
62
+ info_file, sd15_file = model.get_model_info_paths(filepath)
63
 
64
+ output = ""
65
+
66
+ # check info file
67
+ if model.metadata_needed(info_file, sd15_file, refetch_old):
68
+ output = f"Creating model info for: {filename}"
69
+ util.printD(output)
70
+ yield output
71
+
72
+ # get model's sha256
73
+ result = None
74
+ for result in util.gen_file_sha256(filepath):
75
+ if isinstance(result, tuple):
76
+ yield result
77
+ sha256_hash = result
78
+
79
+ util.printD(f"model action sha256: {sha256_hash}")
80
+
81
+ if not sha256_hash:
82
+ output = f"failed generating SHA256 for model: {filename}"
83
+ util.printD(output)
84
+ yield output
85
+ time.sleep(delay)
86
+ yield False
87
+
88
+ yield "Requesting model information from Civitai"
89
+ # use this sha256 to get model info from civitai
90
+ model_info = civitai.get_model_info_by_hash(sha256_hash)
91
+
92
+ if not model_info:
93
+ model_info = dummy_model_info(filepath, sha256_hash, model_type)
94
+
95
+ model.process_model_info(filepath, model_info, model_type, refetch_old=refetch_old)
96
+
97
+ # delay before next request, to prevent being treated as a DDoS attack
98
+ time.sleep(delay)
99
+
100
+ else:
101
+ util.printD(f"Model metadata not needed for {filename}")
102
+
103
+ yield True
104
+
105
+
106
+ def scan_model(scan_model_types, nsfw_preview_threshold, refetch_old, progress=gr.Progress()):
107
+ """ Scan model to generate SHA256, then use this SHA256 to get model info from civitai
108
+ return output msg
109
+ """
110
+
111
+ delay = 0.2
112
 
 
 
 
113
  util.printD("Start scan_model")
114
  output = ""
115
 
116
+ max_size_preview = util.get_opts("ch_max_size_preview")
117
+
118
  # check model types
119
  if not scan_model_types:
120
  output = "Model Types is None, can not scan."
121
  util.printD(output)
122
+ yield output
123
+ return
124
+
125
+ model_types = scan_model_types
126
+ if isinstance(scan_model_types, str):
127
+ # check if type is a string
128
+ model_types = [scan_model_types]
129
+
130
+ models = []
 
 
 
131
  for model_type, model_folder in model.folders.items():
132
  if model_type not in model_types:
133
  continue
134
 
135
+ util.printD(f"Scanning path: {model_folder}")
136
+ for root, _, files in os.walk(model_folder, followlinks=True):
137
  for filename in files:
138
+
139
  # check ext
140
+ filepath = os.path.join(root, filename)
141
+ _, ext = os.path.splitext(filepath)
142
+ if ext not in model.EXTS:
143
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ models.append((filepath, model_type))
146
 
147
+ count = [0, 0]
148
+ total = len(models)
149
+ for filepath, model_type in models:
150
+ success = None
151
 
152
+ tracker = (count[0], total)
153
 
154
+ progress(
155
+ tracker,
156
+ desc="Scanning...",
157
+ unit="models"
158
+ )
159
+
160
+ count[0] = count[0] + 1
161
+
162
+ for result in scan_single_model(filepath, model_type, refetch_old, delay):
163
+ if isinstance(result, str):
164
+ progress(tracker, desc=result, unit="models")
165
+ continue
166
+ if isinstance(result, tuple):
167
+ percent, status = result
168
+ progress(percent, desc=status)
169
+ continue
170
+
171
+ success = result
172
+ break
173
+
174
+ if not success:
175
+ continue
176
 
177
+ # set model_count
178
+ count[1] = count[1] + 1
179
+
180
+ # check preview image
181
+ for _ in civitai.get_preview_image_by_model_path(
182
+ filepath,
183
+ max_size_preview,
184
+ nsfw_preview_threshold
185
+ ):
186
+ pass
187
+
188
+ # this previously had an image count, but it always matched the model count.
189
+ output = f"Done. Successfully scanned {count[1]} of {len(models)} models."
190
+
191
+ util.printD(output)
192
+
193
+ yield output
194
+
195
+
196
+ def dummy_model_info(path, sha256_hash, model_type):
197
+ """
198
+ Fills model metadata with information we can get locally.
199
+ """
200
+ if not sha256_hash:
201
+ return {}
202
+
203
+ model_info = get_metadata_skeleton()
204
+
205
+ autov2 = sha256_hash[:10]
206
+ filename = os.path.basename(path)
207
+ filesize = os.path.getsize(path) // 1024
208
+
209
+ model_metadata = model_info["model"]
210
+ file_metadata = model_info["files"][0]
211
+
212
+ model_metadata["name"] = filename
213
+ model_metadata["type"] = model_type
214
+
215
+ file_metadata["name"] = filename
216
+ file_metadata["sizeKB"] = filesize
217
+ file_metadata["hashes"]["SHA256"] = sha256_hash
218
+ file_metadata["hashes"]["AutoV2"] = autov2
219
+
220
+ # We can't get data on the model from civitai, but some models
221
+ # do store their training data.
222
+ trained_words = model_info["trainedWords"]
223
+ tags = model_info["tags"]
224
+
225
+ try:
226
+ file_metadata = sd_models.read_metadata_from_safetensors(path)
227
+ except AssertionError:
228
+ # model is not a safetensors file. This is fine,
229
+ # it just doesn't have metadata we can read
230
+ pass
231
+
232
+ tag_frequency = file_metadata.get("ss_tag_frequency", {})
233
+
234
+ for trained_word in tag_frequency.keys():
235
+ # kohya training scripts use
236
+ # `{iterations}_{trained_word}`
237
+ # for training finetune concepts.
238
+ word = re.sub(r"^\d+_", "", trained_word)
239
+ trained_words.append(word)
240
+
241
+ # "tags" in this case are just words used in image captions
242
+ # when training the finetune model.
243
+ # They may or may not be useful for prompting
244
+ for tag in tag_frequency[trained_word].keys():
245
+ tag = tag.replace(",", "").strip()
246
+ if tag == "":
247
+ continue
248
+ tags.append(tag)
249
+
250
+ return model_info
251
+
252
+
253
+ def get_model_info_by_input(
254
+ model_type, model_name, model_url_or_id, nsfw_preview_threshold
255
+ ):
256
+ """
257
+ Get model info by model type, name and url
258
+ output is log info to display on markdown component
259
+ """
260
  output = ""
261
+
262
+ max_size_preview = util.get_opts("ch_max_size_preview")
263
+
264
  # parse model id
265
  model_id = civitai.get_model_id_from_url(model_url_or_id)
266
  if not model_id:
267
+ output = f"failed to parse model id from url: {model_url_or_id}"
268
  util.printD(output)
269
+ yield output
270
+ return
271
 
272
  # get model file path
273
  # model could be in subfolder
274
+ model_path = model.get_model_path_by_type_and_name(model_type, model_name)
275
+
276
+ if model_path is None:
277
+ output = "Could not get Model Path"
 
 
 
 
 
278
  util.printD(output)
279
+ yield output
280
+ return
 
 
 
281
 
282
+ # get model info
283
  #we call it model_info, but in civitai, it is actually version info
284
  model_info = civitai.get_version_info_by_model_id(model_id)
285
 
286
+ model.process_model_info(model_path, model_info, model_type)
 
 
 
 
 
 
287
 
288
+ # check preview image + webui-visible progress bar
289
+ yield from civitai.get_preview_image_by_model_path(
290
+ model_path,
291
+ max_size_preview,
292
+ nsfw_preview_threshold
293
+ )
294
 
295
+ yield output
 
296
 
 
 
297
 
298
+ def build_article_from_version(version):
299
+ """
300
+ Builds the HTML for displaying new model versions to the user.
301
+
302
+ return: html:str
303
+ """
304
+ (
305
+ model_path, model_id, model_name, new_version_id,
306
+ new_version_name, description, download_url,
307
+ img_url, model_type
308
+ ) = version
309
+
310
+ thumbnail = ""
311
+ if img_url:
312
+ thumbnail = templates.thumbnail.substitute(
313
+ img_url=img_url,
314
+ )
315
+
316
+ if download_url:
317
+ # replace "\" to "/" in model_path for windows
318
+ download_model_path = model_path.replace('\\', '\\\\')
319
+
320
+ download_section = templates.download.substitute(
321
+ new_version_id=new_version_id,
322
+ new_version_name=new_version_name,
323
+ model_path=download_model_path,
324
+ model_type=model_type,
325
+ download_url=download_url
326
+ )
327
+
328
+ else:
329
+ download_section = templates.no_download.substitute(
330
+ new_version_name=new_version_name,
331
+ )
332
+
333
+ description_section = ""
334
+ if description:
335
+ description_section = templates.description.substitute(
336
+ description=util.safe_html(download_section),
337
+ )
338
+
339
+ article = templates.article.substitute(
340
+ url=f'{civitai.URLS["modelPage"]}{model_id}',
341
+ thumbnail=thumbnail,
342
+ download=download_section,
343
+ description=description_section,
344
+ model_name=model_name,
345
+ model_path=model_path
346
+ )
347
+
348
+ return article
349
 
350
 
 
351
  def check_models_new_version_to_md(model_types:list) -> str:
352
+ """
353
+ check models' new version and output to UI as html doc
354
+ return: html:str
355
+ """
356
+ new_versions = civitai.check_models_new_version_by_model_types(model_types, 0.2)
357
 
 
 
358
  if not new_versions:
359
+ util.printD("Done: no new versions found.")
360
+ return "No models have new versions"
361
+
362
+ articles = []
363
+ count = 0
364
+ for count, new_version in enumerate(new_versions):
365
+ article = build_article_from_version(new_version)
366
+ articles.append(article)
367
+
368
+ output = f"Found new versions for following models: <section>{''.join(articles)}</section>"
369
+
370
+ if count != 1:
371
+ util.printD(f"Done. Found {count} models that have new versions. Check UI for detail")
372
  else:
373
+ util.printD(f"Done. Found {count} model that has a new version. Check UI for detail.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
+ return output
 
 
376
 
 
 
 
 
377
 
378
+ def get_model_info_by_url(model_url_or_id:str) -> dict:
379
+ """
380
+ Retrieves model information necessary to populate HTML
381
+ with Model Name, Model Type, valid saving directories,
382
+ and available model versions.
383
 
384
+ return: tuple or None
385
+ """
386
+ util.printD(f"Getting model info by: {model_url_or_id}")
387
 
388
+ try:
389
+ # parse model id
390
+ model_id = civitai.get_model_id_from_url(model_url_or_id)
391
 
392
+ # download model info
393
+ model_info = civitai.get_model_info_by_id(model_id)
394
 
395
+ # parse model type, model name, subfolder, version from this model info
396
+ # get model type
397
+ civitai_model_type = model_info["type"]
398
 
399
+ if civitai_model_type not in civitai.MODEL_TYPES:
400
+ util.printD(f"This model type is not supported: {civitai_model_type}")
401
+ return None
 
 
402
 
403
+ model_type = civitai.MODEL_TYPES[civitai_model_type]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
 
405
+ # get model type
406
+ model_name = model_info["name"]
 
 
407
 
408
+ # get version lists
409
+ model_versions = model_info["modelVersions"]
 
 
410
 
411
+ except (KeyError, ValueError, TypeError) as e:
412
+ util.printD(f"An error occurred while attempting to process model info: \n\t{e}")
413
+ return None
414
+
415
+ filenames = []
416
+ files = []
 
 
 
 
417
  version_strs = []
418
+ previews = {}
419
+ for version in model_versions:
420
  # version name can not be used as id
421
  # version id is not readable
422
  # so , we use name_id as version string
423
+ version_str = f'{version["name"]}_{version["id"]}'
424
+
425
+ filename = ""
426
+ try:
427
+ for filedata in version["files"]:
428
+ if filedata["type"] == "Model":
429
+ filename = filedata["name"]
430
+
431
+ except (ValueError, KeyError):
432
+ pass
433
+
434
+ files.append(version["files"])
435
+ filenames.append(filename)
436
  version_strs.append(version_str)
437
+ previews[version_str] = version["images"]
438
 
439
  # get folder by model type
440
  folder = model.folders[model_type]
 
 
 
 
441
 
442
+ # get subfolders
443
+ subfolders = ["/"] + util.get_subfolders(folder)
444
+
445
+ msg = util.indented_msg(f"""
446
+ Got following info for downloading:
447
+ {model_name=}
448
+ {model_type=}
449
+ {version_strs=}
450
+ {subfolders=}
451
+ {previews=}
452
+ """)
453
+ util.printD(msg)
454
+
455
+ return {
456
+ "model_info": model_info,
457
+ "model_name": model_name,
458
+ "model_type": model_type,
459
+ "files": files,
460
+ "filenames": filenames,
461
+ "subfolders": subfolders,
462
+ "version_strs": version_strs,
463
+ "previews": previews
464
+ }
465
 
 
466
 
 
467
  def get_ver_info_by_ver_str(version_str:str, model_info:dict) -> dict:
468
+ """
469
+ get version info by version string
470
+
471
+ return: version_info:dict
472
+ """
473
+
474
+ if not (version_str and model_info):
475
+ output = util.indented_msg(
476
+ f"""
477
+ Missing Parameter:
478
+ {model_info=}
479
+ {version_str=}
480
+ """
481
+ )
482
+ util.printD(output)
483
+ return None
484
 
485
+ # get version list
486
+ model_versions = model_info.get("modelVersions", None)
487
+ if model_versions is None:
488
  util.printD("modelVersions is Empty")
489
+ return None
490
+
491
  # find version by version_str
492
  version = None
493
+ for ver in model_versions:
494
  # version name can not be used as id
495
  # version id is not readable
496
  # so , we use name_id as version string
497
+ ver_str = f'{ver["name"]}_{ver["id"]}'
498
  if ver_str == version_str:
499
  # find version
500
  version = ver
501
+ break
502
+
503
+ if not (version and ("id" in version)):
504
+ util.printD(f"can not find version or id by version string: {version_str}")
505
+ return None
506
 
 
 
 
 
 
 
 
 
 
507
  return version
508
 
509
 
 
 
510
  def get_id_and_dl_url_by_version_str(version_str:str, model_info:dict) -> tuple:
511
+ """
512
+ get download url from model info by version string
513
+ return - (version_id, download_url)
514
+ """
515
+ if not (version_str and model_info):
516
+ output = util.indented_msg(f"""
517
+ Missing Parameter:
518
+ {model_info=}
519
+ {version_str=}
520
+ """)
521
+ util.printD(output)
522
+ return (False, output)
523
 
524
+ # get version list
525
+ model_versions = model_info.get("modelVersions", None)
526
+ if model_versions is None:
527
  util.printD("modelVersions is Empty")
528
+ return (False, output)
529
+
530
  # find version by version_str
531
  version = None
532
+ for ver in model_versions:
533
  # version name can not be used as id
534
  # version id is not readable
535
  # so , we use name_id as version string
536
+ ver_str = f'{ver["name"]}_{ver["id"]}'
537
  if ver_str == version_str:
538
  # find version
539
  version = ver
540
+ break
541
+
542
+ version_id = None
543
+ download_url = None
544
+ if version:
545
+ download_url = version.get("downloadUrl", None)
546
+ version_id = version.get("id", None)
547
+
548
+ if None in [version, version_id, download_url]:
549
+ output = util.indented_msg(f"""
550
+ Invalid Version Information:
551
+ {version=}
552
+ {version_id=}
553
+ {download_url=}
554
+ """)
555
+ util.printD(output)
556
+ return (False, output)
557
 
558
+ util.printD(f"Get Download Url: {download_url}")
 
 
 
 
 
 
 
 
 
 
 
 
559
 
560
+ return (version_id, download_url)
 
 
 
 
 
 
 
 
 
 
 
 
 
561
 
562
+ def parse_file_info(file_info, basename):
563
+ """
564
+ returns data required to download a file from civitai
565
+ """
566
 
567
+ download_url = file_info.get("downloadUrl", None)
568
+ if download_url is None:
569
+ return None
570
 
571
+ filetype = file_info["type"]
572
+ filename = file_info["name"]
573
+ if basename and not filetype == "VAE":
574
+ filename = f"{basename}.{filename.split('.')[-1]}"
575
+
576
+ return {
577
+ "url": download_url,
578
+ "filename": filename,
579
+ "type": filetype
580
+ }
581
+
582
+
583
+ def download_files(filename, model_folder, ver_info, headers, filetypes, dl_all, duplicate):
584
+ """
585
+ get download urls from files info
586
+ some model versions have multiple files
587
+ """
588
+
589
+ version_id = ver_info["id"]
590
+
591
+ downloads = []
592
+
593
+ # check if this model already exists
594
+ result = civitai.search_local_model_info_by_version_id(model_folder, version_id)
595
+ if result:
596
+ output = "This model version already exists"
597
  util.printD(output)
598
+ yield (False, output)
599
+
600
+ for file_info in ver_info.get("files", {}):
601
+ if not dl_all:
602
+ if not file_info["type"] in filetypes:
603
+ continue
604
+
605
+ dl_info = parse_file_info(file_info, filename)
606
+
607
+ if dl_info:
608
+ downloads.append(dl_info)
609
+
610
+ if len(downloads) == 0:
611
+ dl_info = parse_file_info(ver_info, filename)
612
+ if dl_info:
613
+ downloads.append(dl_info)
614
+
615
+ # download
616
+ success = False
617
+ output = ""
618
+ filepath = None
619
+ total = len(downloads)
620
+ errors = []
621
+ errors_count = 0
622
+ snippet = None
623
+
624
+ for count, dl_info in enumerate(downloads):
625
+
626
+ url = dl_info["url"]
627
+ if errors_count > 0:
628
+ snippet = f"{errors_count}/{total} files failed"
629
+
630
+ dl_folder = model_folder
631
+ if dl_info["type"] == "VAE":
632
+ dl_folder = model.vae_folder
633
+
634
+ # webui visible progress bar
635
+ for result in downloader.dl_file(
636
+ url, filename=dl_info["filename"], folder=dl_folder, duplicate=duplicate,
637
+ headers=headers
638
+ ):
639
+ if not isinstance(result, str):
640
+ success, output = result
641
+ break
642
+
643
+ output = f"{result} | {count}/{total} files"
644
+ if snippet:
645
+ " | ".join([output, snippet])
646
+
647
+ yield output
648
+
649
+ if not success:
650
+ errors.append(downloader.error(url, output))
651
+ errors_count += 1
652
+ continue
653
+
654
+ if dl_info["type"] == "Model":
655
+ filepath = output
656
+
657
+ additional = None
658
+ if errors_count > 0:
659
+ additional = "\n\n".join(errors)
660
+
661
+ if errors_count == total:
662
+ yield (False, additional)
663
+ return
664
+
665
+ yield (True, filepath, additional)
666
+
667
+
668
+ def download_one(filename, model_folder, ver_info, headers, duplicate):
669
+ """
670
+ only download one file
671
+ get download url
672
+ """
673
+
674
+ download_url = ver_info["downloadUrl"]
675
+
676
+ output = ""
677
+ if not download_url:
678
+ output = "Failed to find a download url"
679
  util.printD(output)
680
+ yield (False, output)
681
+
682
+ # download
683
+ success = False
684
+ for result in downloader.dl_file(
685
+ download_url, filename=filename, folder=model_folder,
686
+ duplicate=duplicate, headers=headers
687
+ ):
688
+ if not isinstance(result, str):
689
+ success, output = result
690
+ break
691
+
692
+ yield result
693
+
694
+ if not success:
695
+ downloader.error(download_url, output)
696
+ yield (False, output)
697
+
698
+ yield (True, output)
699
+
700
+
701
+ def dl_model_by_input(
702
+ ch_state:dict,
703
+ model_type:str,
704
+ subfolder_str:str,
705
+ version_str:str,
706
+ filename:str,
707
+ file_ext:str,
708
+ dl_all:bool,
709
+ nsfw_preview_threshold:bool,
710
+ duplicate:str,
711
+ preview:str,
712
+ *args
713
+ ) -> str:
714
+ """ download model from civitai by input
715
+ output to markdown log
716
+ """
717
+
718
+ model_info = ch_state["model_info"]
719
+ max_size_preview = util.get_opts("ch_max_size_preview")
720
+
721
+ if not (model_info and model_type and subfolder_str and version_str):
722
+ output = util.indented_msg(f"""
723
+ Missing Required Parameter in dl_model_by_input. Parameters given:
724
+ {model_type=}*
725
+ {subfolder_str=}*
726
+ {version_str=}*
727
+ {filename=}
728
+ {file_ext=}
729
+ {max_size_preview=}
730
+ {nsfw_preview_threshold=}
731
+ {duplicate=}
732
+ """)
733
+
734
+ # Keep model info away from util.indented_msg
735
+ # which can screw with complex strings
736
+ output = f"{output}\n {model_info=}*\n * Required"
737
  util.printD(output)
738
+ yield output
739
+ return
740
+
741
  # get model root folder
742
+ if model_type not in model.folders:
743
+ output = f"Unsupported model type: {model_type}"
744
  util.printD(output)
745
+ yield output
746
+ return
747
+
748
+ folder = ""
749
+ subfolder = ""
750
+ output = ""
751
+ version_info = None
752
+
753
+ filetypes = []
754
+ for filetype, will_dl in zip(civitai.FILE_TYPES, args):
755
+ if will_dl:
756
+ filetypes.append(filetype)
757
+
758
  model_root_folder = model.folders[model_type]
759
 
760
+ if not os.path.exists(model_root_folder):
761
+ # Model directories may not exist by default
762
+ os.mkdir(model_root_folder)
763
 
764
  # get subfolder
765
+ if subfolder_str in ["/", "\\"]:
 
766
  subfolder = ""
767
+ elif subfolder_str[:1] in ["/", "\\"]:
768
  subfolder = subfolder_str[1:]
769
  else:
770
  subfolder = subfolder_str
771
 
772
  # get model folder for downloading
773
+ folder = os.path.join(model_root_folder, subfolder)
774
+ if not os.path.isdir(folder):
775
+ output = f"Model folder is not a dir: {folder}"
776
  util.printD(output)
777
+ yield output
778
+ return
779
+
780
  # get version info
781
  ver_info = get_ver_info_by_ver_str(version_str, model_info)
782
  if not ver_info:
783
+ output = "Failed to get version info, check console log for detail"
784
  util.printD(output)
785
+ yield output
786
+ return
 
 
787
 
788
+ headers = {
789
+ "content-type": "application/json"
790
+ }
791
+ api_key = util.get_opts("ch_civiai_api_key")
792
+ if api_key:
793
+ headers["Authorization"] = f"Bearer {api_key}"
794
+
795
+ additional = None
796
+ for result in download_files(filename, folder, ver_info, headers, filetypes, dl_all, duplicate):
797
+ if not isinstance(result, str):
798
+ if len(result) > 2:
799
+ success, output, additional = result
800
+ else:
801
+ success, output = result
802
 
803
+ break
 
 
804
 
805
+ yield result
806
 
807
+ if not success:
808
+ yield output
809
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
810
 
 
 
 
811
  # get version info
812
+ version_info = civitai.get_version_info_by_version_id(ver_info["id"])
813
+ model.process_model_info(output, version_info, model_type)
814
+
815
+ # then, get preview image + webui-visible progress
816
+ for result in civitai.get_preview_image_by_model_path(
817
+ output,
818
+ max_size_preview,
819
+ nsfw_preview_threshold,
820
+ preferred_preview=preview
821
+ ):
822
+ yield f"Downloading model preview:\n{result}"
823
+
824
+ output = f"Done. Downloaded to: {output}"
825
+ if additional:
826
+ output = f"{output}. Additionally, the following failures occurred: \n{additional}"
827
  util.printD(output)
828
+ yield output
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/msg_handler.py CHANGED
@@ -1,64 +1,74 @@
1
- # -*- coding: UTF-8 -*-
2
- # handle msg between js and python side
 
3
  import json
4
  from . import util
5
 
6
  # action list
7
- js_actions = ("open_url", "add_trigger_words", "use_preview_prompt", "dl_model_new_version", "remove_card")
8
- py_actions = ("open_url", "remove_card")
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
 
11
- # handle request from javascript
12
- # parameter: msg - msg from js as string in a hidden textbox
13
- # return: dict for result
14
  def parse_js_msg(msg):
 
 
 
 
 
15
  util.printD("Start parse js msg")
16
  msg_dict = json.loads(msg)
17
 
18
  # in case client side run JSON.stringify twice
19
- if (type(msg_dict) == str):
20
  msg_dict = json.loads(msg_dict)
21
 
22
- if "action" not in msg_dict.keys():
23
- util.printD("Can not find action from js request")
24
- return
25
-
26
- action = msg_dict["action"]
27
  if not action:
28
- util.printD("Action from js request is None")
29
- return
30
 
31
- if action not in js_actions:
32
- util.printD("Unknow action: " + action)
33
- return
34
 
35
  util.printD("End parse js msg")
36
 
37
  return msg_dict
38
 
39
 
40
- # build python side msg for sending to js
41
- # parameter: content dict
42
- # return: msg as string, to fill into a hidden textbox
43
  def build_py_msg(action:str, content:dict):
 
 
 
 
 
44
  util.printD("Start build_msg")
45
- if not content:
46
- util.printD("Content is None")
47
- return
48
-
49
- if not action:
50
- util.printD("Action is None")
51
- return
52
-
53
- if action not in py_actions:
54
- util.printD("Unknow action: " + action)
55
- return
56
 
57
  msg = {
58
  "action" : action,
59
  "content": content
60
  }
61
 
62
-
63
  util.printD("End build_msg")
64
- return json.dumps(msg)
 
1
+ """ -*- coding: UTF-8 -*-
2
+ handle msg between js and python side
3
+ """
4
  import json
5
  from . import util
6
 
7
  # action list
8
+ JS_ACTIONS = (
9
+ "open_url",
10
+ "add_trigger_words",
11
+ "use_preview_prompt",
12
+ "dl_model_new_version",
13
+ "rename_card",
14
+ "remove_card"
15
+ )
16
+
17
+ PY_ACTIONS = (
18
+ "open_url",
19
+ "rename_card",
20
+ "remove_card"
21
+ )
22
 
23
 
 
 
 
24
  def parse_js_msg(msg):
25
+ """
26
+ handle request from javascript
27
+ parameter: msg - msg from js as string in a hidden textbox
28
+ return: dict for result
29
+ """
30
  util.printD("Start parse js msg")
31
  msg_dict = json.loads(msg)
32
 
33
  # in case client side run JSON.stringify twice
34
+ if isinstance(msg_dict, str):
35
  msg_dict = json.loads(msg_dict)
36
 
37
+ action = msg_dict.get("action", "")
 
 
 
 
38
  if not action:
39
+ util.printD("No action from js request")
40
+ return None
41
 
42
+ if action not in JS_ACTIONS:
43
+ util.printD(f"Unknown action: {action}")
44
+ return None
45
 
46
  util.printD("End parse js msg")
47
 
48
  return msg_dict
49
 
50
 
 
 
 
51
  def build_py_msg(action:str, content:dict):
52
+ """
53
+ build python side msg for sending to js
54
+ parameter: content dict
55
+ return: msg as string, to fill into a hidden textbox
56
+ """
57
  util.printD("Start build_msg")
58
+ if not (content and action and action in PY_ACTIONS):
59
+ util.indented_msg(
60
+ f"""
61
+ Could not run action on content:
62
+ {action=}
63
+ {content=}
64
+ """
65
+ )
66
+ return None
 
 
67
 
68
  msg = {
69
  "action" : action,
70
  "content": content
71
  }
72
 
 
73
  util.printD("End build_msg")
74
+ return json.dumps(msg)
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/setting.py DELETED
@@ -1,103 +0,0 @@
1
- # -*- coding: UTF-8 -*-
2
- # collecting settings to here
3
- import json
4
- import os
5
- import modules.scripts as scripts
6
- from . import util
7
-
8
-
9
- name = "setting.json"
10
- path = os.path.join(scripts.basedir(), name)
11
-
12
- data = {
13
- "model":{
14
- "max_size_preview": True,
15
- "skip_nsfw_preview": False
16
- },
17
- "general":{
18
- "open_url_with_js": True,
19
- "proxy": "",
20
- },
21
- "tool":{
22
- }
23
- }
24
-
25
-
26
-
27
- # save setting
28
- # return output msg for log
29
- def save():
30
- print("Saving setting to: " + path)
31
-
32
- json_data = json.dumps(data, indent=4)
33
-
34
- output = ""
35
-
36
- #write to file
37
- try:
38
- with open(path, 'w') as f:
39
- f.write(json_data)
40
- except Exception as e:
41
- util.printD("Error when writing file:"+path)
42
- output = str(e)
43
- util.printD(str(e))
44
- return output
45
-
46
- output = "Setting saved to: " + path
47
- util.printD(output)
48
-
49
- return output
50
-
51
-
52
- # load setting to global data
53
- def load():
54
- # load data into globel data
55
- global data
56
-
57
- util.printD("Load setting from: " + path)
58
-
59
- if not os.path.isfile(path):
60
- util.printD("No setting file, use default")
61
- return
62
-
63
- json_data = None
64
- with open(path, 'r') as f:
65
- json_data = json.load(f)
66
-
67
- # check error
68
- if not json_data:
69
- util.printD("load setting file failed")
70
- return
71
-
72
- data = json_data
73
-
74
- # check for new key
75
- if "proxy" not in data["general"].keys():
76
- data["general"]["proxy"] = ""
77
-
78
-
79
- return
80
-
81
- # save setting from parameter
82
- def save_from_input(max_size_preview, skip_nsfw_preview, open_url_with_js, proxy):
83
- global data
84
- data = {
85
- "model":{
86
- "max_size_preview": max_size_preview,
87
- "skip_nsfw_preview": skip_nsfw_preview
88
- },
89
- "general":{
90
- "open_url_with_js": open_url_with_js,
91
- "proxy": proxy,
92
- },
93
- "tool":{
94
- }
95
- }
96
-
97
- output = save()
98
-
99
- if not output:
100
- output = ""
101
-
102
- return output
103
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/templates.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HTML templates
3
+ """
4
+
5
+ from string import Template
6
+ from . import util
7
+
8
+ article = Template(util.dedent("""
9
+ <article style="margin: 5px; clear: both;">
10
+ $thumbnail
11
+ <div style="font-size:16px">
12
+ File: $model_path
13
+ </div>
14
+ $download
15
+ <div style="font-size:20px;margin:6px 0px;">
16
+ <b>Model: <a href="$url" target="_blank"><u>$model_name</u></a></b>
17
+ </div>
18
+ </article>
19
+ """).strip())
20
+
21
+ thumbnail = Template(util.dedent("""
22
+ <img src='$img_url' style='float: left; margin: 5px;'>
23
+ """).strip())
24
+
25
+ description = Template(util.dedent("""
26
+ <blockquote style="font-size:16px;margin:6px 0px;">
27
+ $description
28
+ </blockquote>
29
+ <br>
30
+ """).strip())
31
+
32
+ # add js function to download new version into SD webui by python
33
+ # in embed HTML, onclick= will also follow a ", never a ',
34
+ # so have to write it as following
35
+ download = Template(util.dedent("""
36
+ <div style="font-size:16px;margin:6px 0px;">
37
+ New Version: <u><a href="$download_url" target="_blank" style="margin:0px 10px;">$new_version_name</a></u>
38
+ <u><a href='#' style='margin:0px 10px;' onclick="ch_dl_model_new_version(event, '$model_path', '$new_version_id', '$download_url', '$model_type')">[Download into SD]</a></u>
39
+ </div>
40
+ """).strip())
41
+
42
+ no_download = Template(util.dedent("""
43
+ <div style="font-size:16px;margin:6px 0px;">
44
+ New Version: $new_version_name
45
+ </div>
46
+ """).strip())
47
+
48
+ duplicate_card = Template(util.dedent("""
49
+ <a href='#' onclick="remove_dup_card(event, '$model_type', '$search_term')" onmouseover="display_ch_path(event, '$path')" onmouseout="hide_ch_path(event)" onmousemove="move_ch_path(event)">
50
+ <div class='card' style=$style data-name="$name">
51
+ $background_image
52
+ <div class='actions'>
53
+ <div class='additional'>
54
+ <span style="display:none">$search_term</span>
55
+ </div>
56
+ <span class='name'>$name</span>
57
+ <span class='description'>$description</span>
58
+ </div>
59
+ </div>
60
+ </a>
61
+ """).strip())
62
+
63
+ duplicate_preview = Template(util.dedent("""
64
+ <img src="./sd_extra_networks/thumb?filename=$bg_image" class="preview" loading="lazy">
65
+ """).strip())
66
+
67
+ duplicate_article = Template(util.dedent("""
68
+ <article>
69
+ <h1>
70
+ $section_name
71
+ </h1>
72
+ $contents
73
+ </article>
74
+ """).strip())
75
+
76
+ duplicate_row = Template(util.dedent("""
77
+ <div id="ch_$hash">
78
+ <div class=civitai_name>
79
+ <h2>
80
+ $civitai_name
81
+ </h2>
82
+ </div>
83
+ <div class=duplicate_model id="ch_${hash}_cards">
84
+ $columns
85
+ </div>
86
+ </div>
87
+ """).strip())
88
+
89
+
90
+ duplicate_column = Template(util.dedent("""
91
+ <div class=dup_$count>
92
+ $card
93
+ </div>
94
+ """).strip())
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/ch_lib/util.py CHANGED
@@ -1,108 +1,493 @@
1
- # -*- coding: UTF-8 -*-
 
 
 
2
  import os
3
  import io
 
4
  import hashlib
5
- import requests
6
- import shutil
 
 
 
 
 
 
 
7
 
 
 
 
 
 
 
8
 
9
- version = "1.9.1"
 
10
 
11
- def_headers = {'User-Agent': 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148',
12
- "Authorization": ""}
13
 
 
 
14
 
15
- proxies = None
 
16
 
17
- civitai_api_key = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
 
20
  # print for debugging
21
- def printD(msg):
 
22
  print(f"Civitai Helper: {msg}")
23
 
24
 
25
- def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE):
26
- """Yield pieces of data from a file-like object until EOF."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  while True:
28
  chunk = file.read(size)
29
  if not chunk:
30
  break
31
  yield chunk
32
 
33
- # Now, hashing use the same way as pip's source code.
34
- def gen_file_sha256(filname):
35
- printD("Use Memory Optimized SHA256")
36
- blocksize=1 << 20
37
- h = hashlib.sha256()
38
- length = 0
39
- with open(os.path.realpath(filname), 'rb') as f:
40
- for block in read_chunks(f, size=blocksize):
41
- length += len(block)
42
- h.update(block)
43
-
44
- hash_value = h.hexdigest()
45
- printD("sha256: " + hash_value)
46
- printD("length: " + str(length))
47
- return hash_value
48
-
49
-
50
-
51
- # get preview image
52
- def download_file(url, path):
53
- printD("Downloading file from: " + url)
54
- # get file
55
- r = requests.get(url, stream=True, headers=def_headers, proxies=proxies)
56
- if not r.ok:
57
- printD("Get error code: " + str(r.status_code))
58
- printD(r.text)
59
- return
60
-
61
- # write to file
62
- with open(os.path.realpath(path), 'wb') as f:
63
- r.raw.decode_content = True
64
- shutil.copyfileobj(r.raw, f)
65
-
66
- printD("File downloaded to: " + path)
67
 
68
- # get subfolder list
69
- def get_subfolders(folder:str) -> list:
70
- printD("Get subfolder for: " + folder)
71
  if not folder:
72
  printD("folder can not be None")
73
- return
74
-
75
  if not os.path.isdir(folder):
76
  printD("path is not a folder")
77
- return
78
-
79
  prefix_len = len(folder)
 
80
  subfolders = []
81
- for root, dirs, files in os.walk(folder, followlinks=True):
82
- for dir in dirs:
83
- full_dir_path = os.path.join(root, dir)
84
- # get subfolder path from it
85
- subfolder = full_dir_path[prefix_len:]
86
- subfolders.append(subfolder)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  return subfolders
89
 
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  # get relative path
92
  def get_relative_path(item_path:str, parent_path:str) -> str:
93
- # printD("item_path:"+item_path)
94
- # printD("parent_path:"+parent_path)
95
- # item path must start with parent_path
96
- if not item_path:
97
- return ""
98
- if not parent_path:
 
99
  return ""
 
100
  if not item_path.startswith(parent_path):
 
101
  return item_path
102
 
103
  relative = item_path[len(parent_path):]
104
  if relative[:1] == "/" or relative[:1] == "\\":
105
  relative = relative[1:]
106
 
107
- # printD("relative:"+relative)
108
- return relative
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ -*- coding: UTF-8 -*-
2
+ Utility functions for Stable Diffusion Civitai Helper
3
+ """
4
+ from __future__ import annotations
5
  import os
6
  import io
7
+ import re
8
  import hashlib
9
+ import textwrap
10
+ import time
11
+ import subprocess
12
+ import gradio as gr
13
+ from modules import shared
14
+ from modules.shared import opts
15
+ from modules import hashes
16
+ import launch
17
+ from packaging.version import parse as parse_version
18
 
19
+ try:
20
+ # Automatic1111 SD WebUI
21
+ import modules.cache as sha256_cache
22
+ except ModuleNotFoundError:
23
+ # Vladmandic "SD.Next"
24
+ import modules.hashes as sha256_cache
25
 
26
+ # used to append extension information to JSON/INFO files
27
+ SHORT_NAME = "sd_civitai_helper"
28
 
29
+ # current version of the exension
30
+ VERSION = "1.8.1"
31
 
32
+ # Civitai INFO files below this version will regenerated
33
+ COMPAT_VERSION_CIVITAI = "1.7.2"
34
 
35
+ # SD webui model info JSON below this version will be regenerated
36
+ COMPAT_VERSION_SDWEBUI = "1.8.0"
37
 
38
+ DEFAULT_HEADERS = {
39
+ "User-Agent": (
40
+ "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) "
41
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
42
+ )
43
+ }
44
+
45
+ PROXIES = {
46
+ "http": None,
47
+ "https": None,
48
+ }
49
+
50
+ REQUEST_TIMEOUT = 300 # 5 minutes
51
+ REQUEST_RETRIES = 5
52
+
53
+ _MINUTE = 60
54
+ _HOUR = _MINUTE * 60
55
+ _DAY = _HOUR * 24
56
 
57
 
58
  # print for debugging
59
+ def printD(msg:any) -> str:
60
+ """ Print a message to stderr """
61
  print(f"Civitai Helper: {msg}")
62
 
63
 
64
+ def append_default_headers(headers:dict) -> dict:
65
+ """ Append extension default values to customized headers where missing """
66
+
67
+ for key, val in DEFAULT_HEADERS.items():
68
+ if key not in headers:
69
+ headers[key] = val
70
+ return headers
71
+
72
+
73
+ def indented_msg(msg:str) -> str:
74
+ """
75
+ Clean up an indented message in the format of
76
+ [header]
77
+ var1=var1
78
+ var2=var2
79
+ var3=var3
80
+
81
+ and print the results in the format of:
82
+
83
+ Civitai Helper: [header]
84
+ var1: var1
85
+ var2: var2
86
+ var3: var3
87
+
88
+ return: msg:str
89
+ """
90
+
91
+ msg_parts = textwrap.dedent(
92
+ msg
93
+ ).strip().split('\n')
94
+ msg = [msg_parts.pop(0)]
95
+ for part in msg_parts:
96
+ part = ": ".join(part.split("="))
97
+ msg.append(f" {part}")
98
+ msg = "\n".join(msg)
99
+
100
+ return msg
101
+
102
+
103
+ def delay(seconds:float) -> None:
104
+ """ delay before next request, mostly to prevent being treated as a DDoS """
105
+ time.sleep(seconds)
106
+
107
+
108
+ def is_stale(timestamp:float) -> bool:
109
+ """ Returns if a timestamp was more than a day ago. """
110
+ cur_time = ch_time()
111
+ elapsed = cur_time - timestamp
112
+
113
+ if elapsed > _DAY:
114
+ return True
115
+
116
+ return False
117
+
118
+
119
+ def info(msg:str) -> None:
120
+ """ Display an info smessage on the client DOM """
121
+ gr.Info(msg)
122
+
123
+
124
+ def warning(msg:str) -> None:
125
+ """ Display a warning message on the client DOM """
126
+ gr.Warning(msg)
127
+
128
+
129
+ def error(msg:str) -> None:
130
+ """ Display an error message on the client DOM """
131
+ gr.Error(msg)
132
+
133
+
134
+ def ch_time() -> int:
135
+ """ Unix timestamp """
136
+ return int(time.time())
137
+
138
+
139
+ def dedent(text:str) -> str:
140
+ """ alias for textwrap.dedent """
141
+ return textwrap.dedent(text)
142
+
143
+
144
+ def get_name(model_path:str, model_type:str) -> str:
145
+ """ return: {model_type}/{model_name} """
146
+
147
+ model_name = os.path.splitext(os.path.basename(model_path))[0]
148
+ return f"{model_type}/{model_name}"
149
+
150
+
151
+ def get_opts(key):
152
+ """ return: option value """
153
+ return opts.data.get(key, None)
154
+
155
+
156
+ def gen_file_sha256(filename:str, model_type="lora", use_addnet_hash=False) -> str:
157
+ """ return a sha256 hash for a file """
158
+
159
+ cache = sha256_cache.cache
160
+ dump_cache = sha256_cache.dump_cache
161
+ model_name = get_name(filename, model_type)
162
+
163
+ sha256_hashes = None
164
+ if use_addnet_hash:
165
+ sha256_hashes = cache("hashes-addnet")
166
+ else:
167
+ sha256_hashes = cache("hashes")
168
+
169
+ sha256_value = hashes.sha256_from_cache(filename, model_name, use_addnet_hash)
170
+ if sha256_value is not None:
171
+ yield sha256_value
172
+ return
173
+
174
+ if shared.cmd_opts.no_hashing:
175
+ printD("SD WebUI Civitai Helper requires hashing functions for this feature. \
176
+ Please remove the commandline argument `--no-hashing` for this functionality.")
177
+ yield None
178
+ return
179
+
180
+ with open(filename, "rb") as model_file:
181
+ result = None
182
+ for result in calculate_sha256(model_file, use_addnet_hash):
183
+ yield result
184
+ sha256_value = result
185
+
186
+ printD(f"sha256: {sha256_value}")
187
+
188
+ sha256_hashes[model_name] = {
189
+ "mtime": os.path.getmtime(filename),
190
+ "sha256": sha256_value,
191
+ }
192
+
193
+ dump_cache()
194
+
195
+ yield sha256_value
196
+
197
+ def calculate_sha256(model_file, use_addnet_hash=False):
198
+ """ calculate the sha256 hash for a model file """
199
+
200
+ blocksize= 1 << 20
201
+ sha256_hash = hashlib.sha256()
202
+
203
+ size = os.fstat(model_file.fileno()).st_size
204
+
205
+ offset = 0
206
+ if use_addnet_hash:
207
+ model_file.seek(0)
208
+ header= model_file.read(8)
209
+ offset = int.from_bytes(header, "little") + 8
210
+ model_file.seek(offset)
211
+
212
+ pos = 0
213
+ for block in read_chunks(model_file, size=blocksize):
214
+ pos += len(block)
215
+
216
+ percent = (pos - offset) / (size - offset)
217
+
218
+ yield (percent, f"hashing model {model_file.name}")
219
+
220
+ sha256_hash.update(block)
221
+
222
+ hash_value = sha256_hash.hexdigest()
223
+ yield hash_value
224
+
225
+
226
+ def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE) -> bytes:
227
+ """ Yield pieces of data from a file-like object until EOF. """
228
  while True:
229
  chunk = file.read(size)
230
  if not chunk:
231
  break
232
  yield chunk
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
+ def get_subfolders(folder:str) -> list[str]:
236
+ """ return: list of subfolders """
237
+ printD(f"Get subfolder for: {folder}")
238
  if not folder:
239
  printD("folder can not be None")
240
+ return []
241
+
242
  if not os.path.isdir(folder):
243
  printD("path is not a folder")
244
+ return []
245
+
246
  prefix_len = len(folder)
247
+ full_dirs_searched = []
248
  subfolders = []
249
+ for root, dirs, _ in os.walk(folder, followlinks=True):
250
+ if root == folder:
251
+ continue
252
+
253
+ # Prevent following recursive symlinks
254
+ follow = []
255
+ for directory in dirs:
256
+ full_dir_path = os.path.join(root, directory)
257
+ try:
258
+ canonical_dir = os.path.realpath(full_dir_path, strict=True)
259
+ if canonical_dir not in full_dirs_searched:
260
+ full_dirs_searched.append(canonical_dir)
261
+ follow.append(directory)
262
+
263
+ except OSError:
264
+ printD(f"Symlink loop: {directory}")
265
+ continue
266
+
267
+ # Get subfolder path
268
+ subfolder = root[prefix_len:]
269
+ subfolders.append(subfolder)
270
+
271
+ # Update dirs parameter to prevent following recursive symlinks
272
+ dirs[:] = follow
273
 
274
  return subfolders
275
 
276
 
277
+ def find_file_in_folders(folders:list, filename:str) -> str:
278
+ """
279
+ Searches a directory for a filename,
280
+
281
+ return: filename:str or None
282
+ """
283
+ for folder in folders:
284
+ for root, _, files in os.walk(folder, followlinks=True):
285
+ if filename in files:
286
+ # found file
287
+ return os.path.join(root, filename)
288
+
289
+ return None
290
+
291
+
292
  # get relative path
293
  def get_relative_path(item_path:str, parent_path:str) -> str:
294
+ """
295
+ Gets a relative path from an absolute path and its parent_path
296
+ item path must start with parent_path
297
+ return: relative_path:str
298
+ """
299
+
300
+ if not (item_path and parent_path):
301
  return ""
302
+
303
  if not item_path.startswith(parent_path):
304
+ # return absolute path
305
  return item_path
306
 
307
  relative = item_path[len(parent_path):]
308
  if relative[:1] == "/" or relative[:1] == "\\":
309
  relative = relative[1:]
310
 
311
+ # printD(f"relative: {relative}")
312
+ return relative
313
+
314
+
315
+ # Allowed HTML tags
316
+ whitelist = re.compile(r"</?(a|img|br|p|b|strong|i|h[0-9]|code)[^>]*>")
317
+
318
+ # Allowed HTML attributes
319
+ attrs = re.compile(r"""(?:href|src|target)=['"]?[^\s'"]*['"]?""")
320
+
321
+ def safe_html_replace(match) -> str:
322
+ """ Given a block of text, returns that block with most HTML removed
323
+ and unneeded attributes pruned.
324
+ """
325
+ tag = None
326
+ attr = None
327
+ close = False
328
+
329
+ match = whitelist.match(match.group(0))
330
+ if match is not None:
331
+ html_elem = match.group(0)
332
+ tag = match.group(1)
333
+ close = html_elem[1] == "/"
334
+ if (tag in ["a", "img"]) and not close:
335
+ sub_match = attrs.findall(html_elem)
336
+ if sub_match is not None:
337
+ attr = " ".join(sub_match)
338
+
339
+ if close:
340
+ return f"</{tag}>"
341
+
342
+ return f"<{tag} {attr}>" if attr else f"<{tag}>"
343
+
344
+ return ""
345
+
346
+ def safe_html(html:str) -> str:
347
+ """ whitelist only HTML I"m comfortable displaying in webui """
348
+
349
+ return re.sub("<[^<]+?>", safe_html_replace, html)
350
+
351
+
352
+ def trim_html(html:str) -> str:
353
+ """ Remove any HTML for a given string and, if needed, replace it with
354
+ a comparable plain-text alternative.
355
+ """
356
+
357
+ def sub_tag(match):
358
+ tag = match.group(1)
359
+ if tag == "/p":
360
+ return "\n\n"
361
+ if tag == "br":
362
+ return "\n"
363
+ if tag == "li":
364
+ return "* "
365
+ if tag in ["code", "/code"]:
366
+ return "`"
367
+ return ''
368
+
369
+ def sub_escaped(match):
370
+ escaped = match.group(1)
371
+ unescaped = {
372
+ "gt": ">",
373
+ "lt": "<",
374
+ "quot": '"',
375
+ "amp": "&"
376
+ }
377
+ return unescaped.get(escaped, "")
378
+
379
+ # non-breaking space. Useless unstyled content
380
+ html = html.replace("\u00a0", "")
381
+
382
+ # remove non-whitelisted HTML tags,
383
+ # replace whitelisted tags with text-equivalents
384
+ html = re.sub(r"<(/?[a-zA-Z]+)(?:[^>]+)?>", sub_tag, html)
385
+
386
+ # Replace HTML-escaped characters with displayables.
387
+ html = re.sub(r"\&(gt|lt|quot|amp)\;", sub_escaped, html)
388
+
389
+ # Because we encapsulate the description in HTML comment,
390
+ # We have to prevent those comments from being cancelled.
391
+ html.replace("-->", "→")
392
+
393
+ # https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/13241
394
+ return f"<!--\n{html.strip()}\n-->"
395
+
396
+
397
+ def newer_version(ver1:str, ver2:str, allow_equal=False) -> bool:
398
+ """ Returns ver1 > ver2
399
+ if allow_equal, returns ver1 >= ver2
400
+ """
401
+ if allow_equal:
402
+ return parse_version(ver1) >= parse_version(ver2)
403
+
404
+ return parse_version(ver1) > parse_version(ver2)
405
+
406
+
407
+ def metadata_version(metadata:dict) -> str | bool:
408
+ """ Attempts retrieve the extension version used to create
409
+ to create the object block
410
+ """
411
+ try:
412
+ return metadata["extensions"][SHORT_NAME]["version"]
413
+ except KeyError:
414
+ return False
415
+
416
+
417
+ def create_extension_block(data=None, skeleton=False) -> dict:
418
+ """ Creates or edits an extensions block for usage in JSON files
419
+ created or edited by this extension.
420
+
421
+ Adds the current version of this extension to the extensions block
422
+ """
423
+
424
+ cur_time = ch_time()
425
+
426
+ block = {
427
+ SHORT_NAME: {
428
+ "version": VERSION,
429
+ "last_update": cur_time,
430
+ "skeleton_file": skeleton
431
+ }
432
+ }
433
+
434
+ if not data:
435
+ return block
436
+
437
+ data[SHORT_NAME] = block[SHORT_NAME]
438
+
439
+ return data
440
+
441
+
442
+ def webui_version() -> str:
443
+ ''' Gets the current webui version using webui's launch tools
444
+
445
+ The version is expected to be in the format `v1.6.0-128-g792589fd`,
446
+ tho all that is explicitly required is `vX`.
447
+
448
+ returns the version in the form 'X.Y.Z'
449
+ '''
450
+ version = None
451
+ try:
452
+ tag = launch.git_tag()
453
+ match = re.match(r"v([\d.]+)", tag)
454
+ if match:
455
+ version = match.group(1)
456
+ else:
457
+ # XXX assume a modern SD Webui version if one cannot be found.
458
+ version = "1.6.0"
459
+
460
+ except AttributeError:
461
+ try:
462
+ return subprocess.check_output(
463
+ ["git", "describe", "--tags"],
464
+ shell=False,
465
+ encoding='utf8'
466
+ ).strip()
467
+
468
+ except subprocess.SubprocessError:
469
+ try:
470
+ changelog_md = os.path.join(
471
+ os.path.dirname(os.path.dirname(__file__)),
472
+ "CHANGELOG.md"
473
+ )
474
+ with open(changelog_md, "r", encoding="utf-8") as file:
475
+ line = next((line.strip() for line in file if line.strip()), "<none>")
476
+ line = line.replace("## ", "")
477
+ version = line
478
+
479
+ except OSError:
480
+ version = "1.6.0"
481
+
482
+ return version
483
+
484
+
485
+ filename_re = re.compile(r"[^A-Za-z\d\s\^\-\+_.\(\)\[\]]")
486
+ def bash_filename(filename:str) -> str:
487
+ """
488
+ Bashes a filename with a large fish until I'm comfortable using it.
489
+
490
+ Keeps a limited set of valid characters, but does not account for
491
+ reserved names like COM.
492
+ """
493
+ return re.sub(filename_re, "", filename)
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/civitai_helper.py CHANGED
@@ -1,59 +1,68 @@
1
- # -*- coding: UTF-8 -*-
2
- # This extension can help you manage your models from civitai. It can download preview, add trigger words, open model page and use the prompt from preview image
3
- # repo: https://github.com/butaixianran/
 
 
4
 
5
-
6
-
7
- import modules.scripts as scripts
8
- import gradio as gr
9
  import os
10
- import webbrowser
11
- import requests
12
- import random
13
- import hashlib
14
- import json
15
- import shutil
16
- import re
17
  import modules
18
- from modules import script_callbacks
19
  from modules import shared
 
20
  from scripts.ch_lib import model
21
  from scripts.ch_lib import js_action_civitai
22
- from scripts.ch_lib import model_action_civitai
23
  from scripts.ch_lib import civitai
24
  from scripts.ch_lib import util
25
-
26
 
27
  # init
28
-
29
  # root path
30
- root_path = os.getcwd()
31
 
32
  # extension path
33
- extension_path = scripts.basedir()
 
 
 
 
 
 
 
 
 
 
34
 
35
  model.get_custom_model_folder()
36
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- # Setting now can not be saved from extension tab
39
- # All settings now must be saved from setting page.
40
- def on_ui_settings():
41
- ch_section = ("civitai_helper", "Civitai Helper")
42
- # settings
43
- shared.opts.add_option("ch_max_size_preview", shared.OptionInfo(True, "Download Max Size Preview", gr.Checkbox, {"interactive": True}, section=ch_section))
44
- shared.opts.add_option("ch_skip_nsfw_preview", shared.OptionInfo(False, "Skip NSFW Preview Images", gr.Checkbox, {"interactive": True}, section=ch_section))
45
- shared.opts.add_option("ch_open_url_with_js", shared.OptionInfo(True, "Open Url At Client Side", gr.Checkbox, {"interactive": True}, section=ch_section))
46
- shared.opts.add_option("ch_proxy", shared.OptionInfo("", "Civitai Helper Proxy", gr.Textbox, {"interactive": True, "lines":1, "info":"format: socks5h://127.0.0.1:port"}, section=ch_section))
47
- shared.opts.add_option("ch_civiai_api_key", shared.OptionInfo("", "Civitai API Key", gr.Textbox, {"interactive": True, "lines":1, "info":"check doc:https://github.com/zixaphir/Stable-Diffusion-Webui-Civitai-Helper/tree/master#api-key"}, section=ch_section))
48
 
49
  def on_ui_tabs():
50
  # init
51
  # init_py_msg = {
52
  # # relative extension path
53
- # "extension_path": util.get_relative_path(extension_path, root_path),
54
  # }
55
  # init_py_msg_str = json.dumps(init_py_msg)
56
 
 
 
 
 
 
 
57
 
58
  # get prompt textarea
59
  # check modules/ui.py, search for txt2img_paste_fields
@@ -63,190 +72,245 @@ def on_ui_tabs():
63
  img2img_prompt = modules.ui.img2img_paste_fields[0][0]
64
  img2img_neg_prompt = modules.ui.img2img_paste_fields[1][0]
65
 
66
-
67
- # get settings
68
- max_size_preview = shared.opts.data.get("ch_max_size_preview", True)
69
- skip_nsfw_preview = shared.opts.data.get("ch_skip_nsfw_preview", False)
70
- open_url_with_js = shared.opts.data.get("ch_open_url_with_js", True)
71
- proxy = shared.opts.data.get("ch_proxy", "")
72
- civitai_api_key = shared.opts.data.get("ch_civiai_api_key", "")
73
-
74
- util.printD("Settings:")
75
- util.printD("max_size_preview: " + str(max_size_preview))
76
- util.printD("skip_nsfw_preview: " + str(skip_nsfw_preview))
77
- util.printD("open_url_with_js: " + str(open_url_with_js))
78
- util.printD("proxy: " + str(proxy))
79
-
80
- # set civitai_api_key
81
- has_api_key = False
82
- if civitai_api_key:
83
- has_api_key = True
84
- util.civitai_api_key = civitai_api_key
85
- util.def_headers["Authorization"] = f"Bearer {civitai_api_key}"
86
-
87
- util.printD(f"use civitai api key: {has_api_key}")
88
-
89
- # set proxy
90
- if proxy:
91
- util.proxies = {
92
- "http": proxy,
93
- "https": proxy,
94
- }
95
-
96
-
97
- # ====Event's function====
98
- def scan_model(scan_model_types):
99
- return model_action_civitai.scan_model(scan_model_types, max_size_preview, skip_nsfw_preview)
100
-
101
- def get_model_info_by_input(model_type_drop, model_name_drop, model_url_or_id_txtbox):
102
- return model_action_civitai.get_model_info_by_input(model_type_drop, model_name_drop, model_url_or_id_txtbox, max_size_preview, skip_nsfw_preview)
103
-
104
- def dl_model_by_input(dl_model_info, dl_model_type_txtbox, dl_subfolder_drop, dl_version_drop, dl_all_ckb):
105
- return model_action_civitai.dl_model_by_input(dl_model_info, dl_model_type_txtbox, dl_subfolder_drop, dl_version_drop, dl_all_ckb, max_size_preview, skip_nsfw_preview)
106
-
107
- def check_models_new_version_to_md(dl_model_info, dl_model_type_txtbox, dl_subfolder_drop, dl_version_drop, dl_all_ckb):
108
- return model_action_civitai.check_models_new_version_to_md(dl_model_info, dl_model_type_txtbox, dl_subfolder_drop, dl_version_drop, dl_all_ckb, max_size_preview, skip_nsfw_preview)
109
-
110
- def open_model_url(js_msg_txtbox):
111
- return js_action_civitai.open_model_url(js_msg_txtbox, open_url_with_js)
112
-
113
- def dl_model_new_version(js_msg_txtbox, max_size_preview):
114
- return js_action_civitai.dl_model_new_version(js_msg_txtbox, max_size_preview, skip_nsfw_preview)
115
-
116
-
117
- def get_model_names_by_input(model_type, empty_info_only):
118
- names = civitai.get_model_names_by_input(model_type, empty_info_only)
119
- return model_name_drop.update(choices=names)
120
-
121
- def get_model_info_by_url(url):
122
- r = model_action_civitai.get_model_info_by_url(url)
123
-
124
- model_info = {}
125
- model_name = ""
126
- model_type = ""
127
- subfolders = []
128
- version_strs = []
129
- if r:
130
- model_info, model_name, model_type, subfolders, version_strs = r
131
-
132
- return [model_info, model_name, model_type, dl_subfolder_drop.update(choices=subfolders), dl_version_drop.update(choices=version_strs)]
133
 
134
  # ====UI====
135
- with gr.Blocks(analytics_enabled=False) as civitai_helper:
136
-
137
- model_types = list(model.folders.keys())
138
- no_info_model_names = civitai.get_model_names_by_input("ckp", False)
139
-
140
- # session data
141
- dl_model_info = gr.State({})
142
-
143
-
144
 
 
145
  with gr.Box(elem_classes="ch_box"):
146
- with gr.Column():
147
- gr.Markdown("### Scan Models for Civitai")
148
- with gr.Row():
149
- scan_model_types_ckbg = gr.CheckboxGroup(choices=model_types, label="Model Types", value=model_types)
150
 
151
- # with gr.Row():
152
- scan_model_civitai_btn = gr.Button(value="Scan", variant="primary", elem_id="ch_scan_model_civitai_btn")
153
- # with gr.Row():
154
- scan_model_log_md = gr.Markdown(value="Scanning takes time, just wait. Check console log for detail", elem_id="ch_scan_model_log_md")
155
-
156
-
157
  with gr.Box(elem_classes="ch_box"):
158
- with gr.Column():
159
- gr.Markdown("### Get Model Info from Civitai by URL")
160
- gr.Markdown("Use this when scanning can not find a local model on civitai")
161
- with gr.Row():
162
- model_type_drop = gr.Dropdown(choices=model_types, label="Model Type", value="ckp", multiselect=False)
163
- empty_info_only_ckb = gr.Checkbox(label="Only Show Models have no Info", value=False, elem_id="ch_empty_info_only_ckb", elem_classes="ch_vpadding")
164
- model_name_drop = gr.Dropdown(choices=no_info_model_names, label="Model", value="ckp", multiselect=False)
165
-
166
- model_url_or_id_txtbox = gr.Textbox(label="Civitai URL", lines=1, value="")
167
- get_civitai_model_info_by_id_btn = gr.Button(value="Get Model Info from Civitai", variant="primary")
168
- get_model_by_id_log_md = gr.Markdown("")
169
 
170
  with gr.Box(elem_classes="ch_box"):
171
- with gr.Column():
172
- gr.Markdown("### Download Model")
173
- with gr.Row():
174
- dl_model_url_or_id_txtbox = gr.Textbox(label="Civitai URL", lines=1, value="")
175
- dl_model_info_btn = gr.Button(value="1. Get Model Info by Civitai Url", variant="primary")
176
-
177
- gr.Markdown(value="2. Pick Subfolder and Model Version")
178
- with gr.Row():
179
- dl_model_name_txtbox = gr.Textbox(label="Model Name", interactive=False, lines=1, value="")
180
- dl_model_type_txtbox = gr.Textbox(label="Model Type", interactive=False, lines=1, value="")
181
- dl_subfolder_drop = gr.Dropdown(choices=[], label="Sub-folder", value="", interactive=True, multiselect=False)
182
- dl_version_drop = gr.Dropdown(choices=[], label="Model Version", value="", interactive=True, multiselect=False)
183
- dl_all_ckb = gr.Checkbox(label="Download All files", value=False, elem_id="ch_dl_all_ckb", elem_classes="ch_vpadding")
184
-
185
- dl_civitai_model_by_id_btn = gr.Button(value="3. Download Model", variant="primary")
186
- dl_log_md = gr.Markdown(value="Check Console log for Downloading Status")
187
 
188
  with gr.Box(elem_classes="ch_box"):
189
- with gr.Column():
190
- gr.Markdown("### Check models' new version")
191
- with gr.Row():
192
- model_types_ckbg = gr.CheckboxGroup(choices=model_types, label="Model Types", value=["lora"])
193
- check_models_new_version_btn = gr.Button(value="Check New Version from Civitai", variant="primary")
194
-
195
- check_models_new_version_log_md = gr.HTML("It takes time, just wait. Check console log for detail")
196
 
197
  with gr.Box(elem_classes="ch_box"):
198
- with gr.Column():
199
- gr.Markdown("### Other")
200
- # save_setting_btn = gr.Button(value="Save Setting")
201
- gr.Markdown(value="Settings are moved into Settings Tab->Civitai Helper section")
202
-
203
 
204
  # ====Footer====
205
- gr.Markdown(f"<center>version:{util.version}</center>")
206
 
207
  # ====hidden component for js, not in any tab====
208
- js_msg_txtbox = gr.Textbox(label="Request Msg From Js", visible=False, lines=1, value="", elem_id="ch_js_msg_txtbox")
209
- py_msg_txtbox = gr.Textbox(label="Response Msg From Python", visible=False, lines=1, value="", elem_id="ch_py_msg_txtbox")
210
-
211
- js_open_url_btn = gr.Button(value="Open Model Url", visible=False, elem_id="ch_js_open_url_btn")
212
- js_add_trigger_words_btn = gr.Button(value="Add Trigger Words", visible=False, elem_id="ch_js_add_trigger_words_btn")
213
- js_use_preview_prompt_btn = gr.Button(value="Use Prompt from Preview Image", visible=False, elem_id="ch_js_use_preview_prompt_btn")
214
- js_dl_model_new_version_btn = gr.Button(value="Download Model's new version", visible=False, elem_id="ch_js_dl_model_new_version_btn")
215
- js_remove_card_btn = gr.Button(value="Remove Card", visible=False, elem_id="ch_js_remove_card_btn")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
  # ====events====
218
- # Scan Models for Civitai
219
- scan_model_civitai_btn.click(scan_model, inputs=[scan_model_types_ckbg], outputs=scan_model_log_md)
220
-
221
- # Get Civitai Model Info by Model Page URL
222
- model_type_drop.change(get_model_names_by_input, inputs=[model_type_drop, empty_info_only_ckb], outputs=model_name_drop)
223
- empty_info_only_ckb.change(get_model_names_by_input, inputs=[model_type_drop, empty_info_only_ckb], outputs=model_name_drop)
224
-
225
- get_civitai_model_info_by_id_btn.click(get_model_info_by_input, inputs=[model_type_drop, model_name_drop, model_url_or_id_txtbox], outputs=get_model_by_id_log_md)
226
-
227
- # Download Model
228
- dl_model_info_btn.click(get_model_info_by_url, inputs=dl_model_url_or_id_txtbox, outputs=[dl_model_info, dl_model_name_txtbox, dl_model_type_txtbox, dl_subfolder_drop, dl_version_drop])
229
- dl_civitai_model_by_id_btn.click(dl_model_by_input, inputs=[dl_model_info, dl_model_type_txtbox, dl_subfolder_drop, dl_version_drop, dl_all_ckb], outputs=dl_log_md)
230
-
231
- # Check models' new version
232
- check_models_new_version_btn.click(model_action_civitai.check_models_new_version_to_md, inputs=model_types_ckbg, outputs=check_models_new_version_log_md)
233
-
234
  # js action
235
- js_open_url_btn.click(open_model_url, inputs=[js_msg_txtbox], outputs=py_msg_txtbox)
236
- js_add_trigger_words_btn.click(js_action_civitai.add_trigger_words, inputs=[js_msg_txtbox], outputs=[txt2img_prompt, img2img_prompt])
237
- js_use_preview_prompt_btn.click(js_action_civitai.use_preview_image_prompt, inputs=[js_msg_txtbox], outputs=[txt2img_prompt, txt2img_neg_prompt, img2img_prompt, img2img_neg_prompt])
238
- js_dl_model_new_version_btn.click(dl_model_new_version, inputs=[js_msg_txtbox], outputs=dl_log_md)
239
- js_remove_card_btn.click(js_action_civitai.remove_model_by_path, inputs=[js_msg_txtbox], outputs=py_msg_txtbox)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
  # the third parameter is the element id on html, with a "tab_" as prefix
242
- return (civitai_helper , "Civitai Helper", "civitai_helper"),
243
-
244
 
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
 
248
  script_callbacks.on_ui_settings(on_ui_settings)
249
  script_callbacks.on_ui_tabs(on_ui_tabs)
250
-
251
-
252
-
 
1
+ """ -*- coding: UTF-8 -*-
2
+ This extension can help you manage your models from civitai.
3
+ It can download preview, add trigger words, open model page and use the prompt from preview image
4
+ repo: https://github.com/butaixianran/
5
+ """
6
 
 
 
 
 
7
  import os
8
+ import gradio as gr
 
 
 
 
 
 
9
  import modules
10
+ from modules import scripts
11
  from modules import shared
12
+ from modules import script_callbacks
13
  from scripts.ch_lib import model
14
  from scripts.ch_lib import js_action_civitai
 
15
  from scripts.ch_lib import civitai
16
  from scripts.ch_lib import util
17
+ from scripts import sections
18
 
19
  # init
 
20
  # root path
21
+ ROOT_PATH = os.getcwd()
22
 
23
  # extension path
24
+ EXTENSION_PATH = scripts.basedir()
25
+
26
+ # default hidden values for civitai helper buttons
27
+ BUTTONS = {
28
+ "replace_preview_button": False,
29
+ "open_url_button": False,
30
+ "add_trigger_words_button": False,
31
+ "add_preview_prompt_button": False,
32
+ "rename_model_button": False,
33
+ "remove_model_button": False,
34
+ }
35
 
36
  model.get_custom_model_folder()
37
 
38
+ def update_proxy():
39
+ """ Set proxy, allow for changes at runtime """
40
+ proxy = util.get_opts("ch_proxy")
41
+
42
+ util.printD(f"Set Proxy: {proxy}")
43
+ if proxy:
44
+ util.PROXIES["http"] = proxy
45
+ util.PROXIES["https"] = proxy
46
+ return
47
+
48
+ util.PROXIES["http"] = None
49
+ util.PROXIES["https"] = None
50
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  def on_ui_tabs():
53
  # init
54
  # init_py_msg = {
55
  # # relative extension path
56
+ # "EXTENSION_PATH": util.get_relative_path(EXTENSION_PATH, ROOT_PATH),
57
  # }
58
  # init_py_msg_str = json.dumps(init_py_msg)
59
 
60
+ try:
61
+ BUTTONS["add_trigger_words_button"] = util.newer_version(
62
+ util.webui_version(), '1.5.0', allow_equal=True
63
+ )
64
+ except ValueError: # packaging.version.InvalidVersion
65
+ BUTTONS["add_trigger_words_button"] = False
66
 
67
  # get prompt textarea
68
  # check modules/ui.py, search for txt2img_paste_fields
 
72
  img2img_prompt = modules.ui.img2img_paste_fields[0][0]
73
  img2img_neg_prompt = modules.ui.img2img_paste_fields[1][0]
74
 
75
+ # Used by some elements to pass messages to python
76
+ js_msg_txtbox = gr.Textbox(
77
+ label="Request Msg From Js",
78
+ visible=False,
79
+ lines=1,
80
+ value="",
81
+ elem_id="ch_js_msg_txtbox"
82
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  # ====UI====
85
+ with gr.Blocks(
86
+ analytics_enabled=False
87
+ ) as civitai_helper:
88
+ # with gr.Blocks(css=".block.padded {padding: 10px !important}") as civitai_helper:
 
 
 
 
 
89
 
90
+ # init
91
  with gr.Box(elem_classes="ch_box"):
92
+ sections.scan_models_section()
 
 
 
93
 
 
 
 
 
 
 
94
  with gr.Box(elem_classes="ch_box"):
95
+ sections.get_model_info_by_url_section()
 
 
 
 
 
 
 
 
 
 
96
 
97
  with gr.Box(elem_classes="ch_box"):
98
+ sections.download_section()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  with gr.Box(elem_classes="ch_box"):
101
+ sections.scan_for_duplicates_section()
 
 
 
 
 
 
102
 
103
  with gr.Box(elem_classes="ch_box"):
104
+ sections.check_new_versions_section(js_msg_txtbox)
 
 
 
 
105
 
106
  # ====Footer====
107
+ gr.HTML(f"<center>{util.SHORT_NAME} version: {util.VERSION}</center>")
108
 
109
  # ====hidden component for js, not in any tab====
110
+ js_msg_txtbox.render()
111
+ py_msg_txtbox = gr.Textbox(
112
+ label="Response Msg From Python",
113
+ visible=False,
114
+ lines=1,
115
+ value="",
116
+ elem_id="ch_py_msg_txtbox"
117
+ )
118
+
119
+ js_open_url_btn = gr.Button(
120
+ value="Open Model Url",
121
+ visible=False,
122
+ elem_id="ch_js_open_url_btn"
123
+ )
124
+ js_add_trigger_words_btn = gr.Button(
125
+ value="Add Trigger Words",
126
+ visible=False,
127
+ elem_id="ch_js_add_trigger_words_btn"
128
+ )
129
+ js_use_preview_prompt_btn = gr.Button(
130
+ value="Use Prompt from Preview Image",
131
+ visible=False,
132
+ elem_id="ch_js_use_preview_prompt_btn"
133
+ )
134
+ js_rename_card_btn = gr.Button(
135
+ value="Rename Card",
136
+ visible=False,
137
+ elem_id="ch_js_rename_card_btn"
138
+ )
139
+ js_remove_card_btn = gr.Button(
140
+ value="Remove Card",
141
+ visible=False,
142
+ elem_id="ch_js_remove_card_btn"
143
+ )
144
 
145
  # ====events====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  # js action
147
+ js_open_url_btn.click(
148
+ js_action_civitai.open_model_url,
149
+ inputs=[js_msg_txtbox],
150
+ outputs=py_msg_txtbox
151
+ )
152
+ js_add_trigger_words_btn.click(
153
+ js_action_civitai.add_trigger_words,
154
+ inputs=[js_msg_txtbox],
155
+ outputs=[
156
+ txt2img_prompt, img2img_prompt
157
+ ]
158
+ )
159
+ js_use_preview_prompt_btn.click(
160
+ js_action_civitai.use_preview_image_prompt,
161
+ inputs=[js_msg_txtbox],
162
+ outputs=[
163
+ txt2img_prompt, txt2img_neg_prompt,
164
+ img2img_prompt, img2img_neg_prompt
165
+ ]
166
+ )
167
+ js_rename_card_btn.click(
168
+ js_action_civitai.rename_model_by_path,
169
+ inputs=[js_msg_txtbox],
170
+ outputs=py_msg_txtbox
171
+ )
172
+ js_remove_card_btn.click(
173
+ js_action_civitai.remove_model_by_path,
174
+ inputs=[js_msg_txtbox],
175
+ outputs=py_msg_txtbox
176
+ )
177
 
178
  # the third parameter is the element id on html, with a "tab_" as prefix
179
+ return ((civitai_helper, "Civitai Helper", "civitai_helper"),)
 
180
 
181
 
182
+ def on_ui_settings():
183
+ section = ('civitai_helper', "Civitai Helper")
184
+ shared.opts.add_option(
185
+ "ch_civiai_api_key",
186
+ shared.OptionInfo(
187
+ "",
188
+ (
189
+ "API key for authenticating with Civitai. "
190
+ "This is required to download some models. "
191
+ "See Wiki for more details."
192
+ ),
193
+ gr.Textbox,
194
+ {"interactive": True, "max_lines": 1},
195
+ section=section
196
+ ).link(
197
+ "Wiki",
198
+ "https://github.com/zixaphir/Stable-Diffusion-Webui-Civitai-Helper/wiki/Civitai-API-Key"
199
+ )
200
+ )
201
+ shared.opts.add_option(
202
+ "ch_dl_lyco_to_lora",
203
+ shared.OptionInfo(
204
+ False,
205
+ (
206
+ "Save LyCORIS models to Lora directory. Do not use this if you are on "
207
+ "older versions of webui or you use an extension that handles LyCORIS "
208
+ "models."
209
+ ),
210
+ gr.Checkbox,
211
+ {"interactive": True},
212
+ section=section)
213
+ )
214
+ shared.opts.add_option(
215
+ "ch_open_url_with_js",
216
+ shared.OptionInfo(
217
+ True,
218
+ (
219
+ "Open model Url on the user's client side, rather than server side. "
220
+ "If you are running WebUI locally, disabling this may open URLs in your "
221
+ "default internet browser if it is different than the one you are running "
222
+ "WebUI in"
223
+ ),
224
+ gr.Checkbox,
225
+ {"interactive": True},
226
+ section=section
227
+ )
228
+ )
229
+ shared.opts.add_option(
230
+ "ch_hide_buttons",
231
+ shared.OptionInfo(
232
+ [x for x, y in BUTTONS.items() if y],
233
+ "Hide checked Civitai Helper buttons on model cards",
234
+ gr.CheckboxGroup,
235
+ {"choices": list(BUTTONS)},
236
+ section=section
237
+ )
238
+ )
239
+ shared.opts.add_option(
240
+ "ch_always_display",
241
+ shared.OptionInfo(
242
+ False,
243
+ "Always Display Buttons on model cards",
244
+ gr.Checkbox,
245
+ {"interactive": True},
246
+ section=section
247
+ )
248
+ )
249
+ shared.opts.add_option(
250
+ "ch_show_btn_on_thumb",
251
+ shared.OptionInfo(
252
+ True,
253
+ "Show Button On Thumb Mode in SD webui versions before 1.5.0",
254
+ gr.Checkbox,
255
+ {"interactive": True},
256
+ section=section
257
+ )
258
+ )
259
+ shared.opts.add_option(
260
+ "ch_max_size_preview",
261
+ shared.OptionInfo(
262
+ True,
263
+ "Download Max Size Preview",
264
+ gr.Checkbox,
265
+ {"interactive": True},
266
+ section=section
267
+ )
268
+ )
269
+ shared.opts.add_option(
270
+ "ch_nsfw_preview_threshold",
271
+ shared.OptionInfo(
272
+ civitai.NSFW_LEVELS[-1], # Allow all
273
+ util.dedent(
274
+ """
275
+ Block NSFW images of a certain threshold and higher.
276
+ Civitai marks all images for NSFW models as also being NSFW.
277
+ These ratings do not seem to be explicitly defined on Civitai's
278
+ end, but "Soft" seems to be suggestive, with NSFW elements but
279
+ not explicit nudity, "Mature" seems to include nudity but not
280
+ always, and "X" seems to be explicitly adult content.
281
+ """
282
+ ).strip().replace("\n", " "),
283
+ gr.Dropdown,
284
+ {
285
+ "choices": civitai.NSFW_LEVELS[1:],
286
+ "interactive": True
287
+ },
288
+ section=section
289
+ )
290
+ )
291
+ shared.opts.add_option(
292
+ "ch_dl_webui_metadata",
293
+ shared.OptionInfo(
294
+ True,
295
+ "Also add data for WebUI metadata editor",
296
+ gr.Checkbox,
297
+ {"interactive": True},
298
+ section=section)
299
+ )
300
+ shared.opts.add_option(
301
+ "ch_proxy",
302
+ shared.OptionInfo(
303
+ "",
304
+ "Proxy to use for fetching models and model data. Format: http://127.0.0.1:port",
305
+ gr.Textbox,
306
+ {"interactive": True, "max_lines": 1},
307
+ section=section)
308
+ )
309
+ shared.opts.onchange(
310
+ "ch_proxy",
311
+ update_proxy
312
+ )
313
 
314
 
315
  script_callbacks.on_ui_settings(on_ui_settings)
316
  script_callbacks.on_ui_tabs(on_ui_tabs)
 
 
 
extensions/Stable-Diffusion-Webui-Civitai-Helper/scripts/sections.py ADDED
@@ -0,0 +1,693 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ -*- coding: UTF-8 -*-
2
+ Sections for civitai_helper tab.
3
+ """
4
+
5
+ import gradio as gr
6
+ from scripts.ch_lib import model
7
+ from scripts.ch_lib import js_action_civitai
8
+ from scripts.ch_lib import model_action_civitai
9
+ from scripts.ch_lib import civitai
10
+ from scripts.ch_lib import duplicate_check
11
+ from scripts.ch_lib import util
12
+
13
+ model_types = list(model.folders.keys())
14
+
15
+ def scan_models_section():
16
+ """ Scan Models Section """
17
+ with gr.Row():
18
+ gr.Markdown("### Scan Models for Civitai")
19
+ with gr.Row():
20
+ with gr.Column():
21
+ scan_model_types_drop = gr.CheckboxGroup(
22
+ choices=model_types,
23
+ label="Model Types",
24
+ value=model_types
25
+ )
26
+ nsfw_preview_scan_drop = gr.Dropdown(
27
+ label="Block NSFW Level Above",
28
+ choices=civitai.NSFW_LEVELS[1:],
29
+ value=util.get_opts("ch_nsfw_preview_threshold"),
30
+ elem_id="ch_nsfw_preview_scan_drop"
31
+ )
32
+ with gr.Row():
33
+ with gr.Column():
34
+ refetch_old_ckb = gr.Checkbox(
35
+ label="Replace Old Metadata Formats*",
36
+ value=False,
37
+ elem_id="ch_refetch_old_ckb"
38
+ )
39
+ gr.HTML("""
40
+ * [<a href=https://github.com/zixaphir/Stable-Diffusion-Webui-Civitai-Helper/wiki/Metadata-Format-Changes>wiki</a>] Do not use this option if you have made changes with the metadata editor without backing up your data!!<br><br>
41
+ """)
42
+
43
+ with gr.Column():
44
+ scan_model_civitai_btn = gr.Button(
45
+ value="Scan",
46
+ variant="primary",
47
+ elem_id="ch_scan_model_civitai_btn"
48
+ )
49
+
50
+ scan_civitai_info_image_meta_btn = gr.Button(
51
+ value="Update image generation information (Experimental)",
52
+ variant="primary",
53
+ elem_id="ch_Scan_civitai_info_image_meta_btn"
54
+ )
55
+
56
+ with gr.Row():
57
+ scan_model_log_md = gr.Markdown(
58
+ value="Scanning takes time, just wait. Check console log for details",
59
+ elem_id="ch_scan_model_log_md"
60
+ )
61
+
62
+ # ====events====
63
+ scan_model_civitai_btn.click(
64
+ model_action_civitai.scan_model,
65
+ inputs=[
66
+ scan_model_types_drop,
67
+ nsfw_preview_scan_drop, refetch_old_ckb
68
+ ],
69
+ outputs=scan_model_log_md
70
+ )
71
+
72
+ scan_civitai_info_image_meta_btn.click(
73
+ model.scan_civitai_info_image_meta,
74
+ outputs=scan_model_log_md
75
+ )
76
+
77
+ def get_model_info_by_url_section():
78
+ """ Get Civitai Model Info by Model Page URL Section """
79
+
80
+ def get_model_names_by_input(model_type, empty_info_only):
81
+ names = civitai.get_model_names_by_input(model_type, empty_info_only)
82
+ return model_name_drop.update(choices=names)
83
+
84
+ no_info_model_names = civitai.get_model_names_by_input("ckp", False)
85
+
86
+ with gr.Column():
87
+ gr.Markdown("### Get Model Info from Civitai by URL")
88
+ gr.Markdown("Use this when scanning can not find a local model on civitai")
89
+ with gr.Row():
90
+ with gr.Column(scale=2):
91
+ model_type_drop = gr.Dropdown(
92
+ choices=model_types,
93
+ label="Model Type",
94
+ value="ckp",
95
+ multiselect=False,
96
+ elem_classes="ch_vpadding"
97
+ )
98
+ with gr.Column(scale=1):
99
+ empty_info_only_ckb = gr.Checkbox(
100
+ label="Only Show Models have no Info",
101
+ value=False,
102
+ elem_id="ch_empty_info_only_ckb",
103
+ elem_classes="ch_vpadding"
104
+ )
105
+ with gr.Row():
106
+ with gr.Column(scale=2):
107
+ model_name_drop = gr.Dropdown(
108
+ choices=no_info_model_names,
109
+ label="Model",
110
+ value="",
111
+ multiselect=False
112
+ )
113
+ with gr.Column(scale=1):
114
+ nsfw_preview_url_drop = gr.Dropdown(
115
+ label="Block NSFW Level Above",
116
+ choices=civitai.NSFW_LEVELS[1:],
117
+ value=util.get_opts("ch_nsfw_preview_threshold"),
118
+ elem_id="ch_nsfw_preview_url_drop"
119
+ )
120
+ with gr.Row():
121
+ with gr.Column(scale=2, elem_classes="justify-bottom"):
122
+ model_url_or_id_txtbox = gr.Textbox(
123
+ label="Civitai URL",
124
+ lines=1,
125
+ value=""
126
+ )
127
+ with gr.Column(scale=1, elem_classes="justify-bottom"):
128
+ get_civitai_model_info_by_id_btn = gr.Button(
129
+ value="Get Model Info from Civitai",
130
+ variant="primary"
131
+ )
132
+
133
+ get_model_by_id_log_md = gr.Markdown("")
134
+
135
+ # ====events====
136
+ model_type_drop.change(
137
+ get_model_names_by_input,
138
+ inputs=[
139
+ model_type_drop, empty_info_only_ckb
140
+ ],
141
+ outputs=model_name_drop
142
+ )
143
+ empty_info_only_ckb.change(
144
+ get_model_names_by_input,
145
+ inputs=[
146
+ model_type_drop, empty_info_only_ckb
147
+ ],
148
+ outputs=model_name_drop
149
+ )
150
+
151
+ get_civitai_model_info_by_id_btn.click(
152
+ model_action_civitai.get_model_info_by_input,
153
+ inputs=[
154
+ model_type_drop, model_name_drop,
155
+ model_url_or_id_txtbox, nsfw_preview_url_drop
156
+ ],
157
+ outputs=get_model_by_id_log_md
158
+ )
159
+
160
+ def download_section():
161
+ """ Download Models Section """
162
+
163
+ model_filetypes = civitai.FILE_TYPES
164
+ file_elems = {}
165
+
166
+ dl_state = gr.State({
167
+ "model_info": {},
168
+ "filenames": {
169
+ # dl_version_str: filename,
170
+ },
171
+ "previews": {
172
+ # dl_version_str: [{url: url, nsfw: nsfw}],
173
+ },
174
+ "files": {
175
+ # dl_version_str: {
176
+ # Model: bool,
177
+ # Config: bool,
178
+ # ...
179
+ # }
180
+ },
181
+ "files_count": {
182
+ # dl_version_str: int
183
+ },
184
+ "filtered_previews": []
185
+ })
186
+
187
+ def get_model_info_by_url(url, subfolder, state):
188
+ data = model_action_civitai.get_model_info_by_url(url)
189
+
190
+ if not data:
191
+ return None
192
+
193
+ state = {
194
+ "model_info": {},
195
+ "filenames": {},
196
+ "previews": {},
197
+ "files": {},
198
+ "files_count": {},
199
+ "filtered_previews": []
200
+ }
201
+
202
+ subfolders = sorted(data["subfolders"])
203
+ version_strs = data["version_strs"]
204
+ filenames = data["filenames"]
205
+
206
+ if subfolder == "" or subfolder not in subfolders:
207
+ subfolder = "/"
208
+
209
+ state["model_info"] = data["model_info"]
210
+ state["previews"] = data["previews"]
211
+
212
+ for filename, version in zip(filenames, version_strs):
213
+ state["filenames"][version] = filename
214
+
215
+ for version_files, version in zip(data["files"], version_strs):
216
+ filetypes = state["files"][version] = {}
217
+ files_count = 0
218
+ unhandled_files = []
219
+ for filedata in version_files:
220
+ files_count += 1
221
+ ch_filedata = {
222
+ "id": filedata["id"],
223
+ "name": filedata["name"],
224
+ }
225
+
226
+ if filedata["type"] in model_filetypes:
227
+ filetypes[filedata["type"]] = (True, ch_filedata)
228
+ continue
229
+
230
+ unhandled_files.append(f"{ch_filedata['id']}: {ch_filedata['name']}")
231
+
232
+ if len(unhandled_files) > 0:
233
+ filetypes["unhandled_files"] = "\n".join(unhandled_files)
234
+ else:
235
+ filetypes["unhandled_files"] = None
236
+
237
+ state["files_count"][version] = files_count
238
+
239
+ return [
240
+ state, data["model_name"], data["model_type"],
241
+ dl_subfolder_drop.update(
242
+ choices=subfolders,
243
+ value=subfolder
244
+ ),
245
+ dl_version_drop.update(
246
+ choices=version_strs,
247
+ value=version_strs[0]
248
+ ),
249
+ files_row.update(
250
+ visible=True
251
+ )
252
+ ]
253
+
254
+ def filter_previews(previews, nsfw_preview_url_drop):
255
+ images = []
256
+ for preview in previews:
257
+ if civitai.should_skip(nsfw_preview_url_drop, preview["nsfw"]):
258
+ continue
259
+ if preview["type"] == "image":
260
+ # Civitai added videos as previews, and webui does not like it
261
+ images.append(preview["url"])
262
+
263
+ return images
264
+
265
+ def update_dl_inputs(state, dl_version, nsfw_threshold, dl_preview_index):
266
+ filename = state["filenames"][dl_version]
267
+
268
+ if not filename:
269
+ filename = dl_filename_txtbox.value
270
+
271
+ file_parts = filename.split(".")
272
+ ext = file_parts.pop()
273
+ base = ".".join(file_parts)
274
+
275
+ previews = filter_previews(state["previews"][dl_version], nsfw_threshold)
276
+ state["filtered_previews"] = previews
277
+
278
+ preview = None
279
+ if len(previews) > dl_preview_index:
280
+ preview = previews[dl_preview_index]
281
+ elif len(previews) > 0:
282
+ preview = previews[0]
283
+
284
+ output_add = []
285
+
286
+ for key, elems in file_elems.items():
287
+ filedata = state["files"][dl_version].get(key, False)
288
+
289
+ visible = False
290
+ filename = ""
291
+ if filedata:
292
+ visible = True
293
+ if isinstance(filedata, str):
294
+ filename = ", ".join([
295
+ filedata_str.split(":")[1].strip()
296
+ for filedata_str in filedata.split("\n")
297
+ ])
298
+ elif isinstance(filedata, tuple):
299
+ _, data = filedata
300
+ filename = data["name"]
301
+ else:
302
+ raise ValueError(f"Invalid filedata: {filedata}")
303
+
304
+ output_add.append(elems["txtbx"].update(value=filename))
305
+ output_add.append(elems["row"].update(visible=visible))
306
+
307
+ return [
308
+ state,
309
+ dl_filename_txtbox.update(
310
+ value=base
311
+ ),
312
+ dl_extension_txtbox.update(
313
+ value=ext
314
+ ),
315
+ dl_preview_img.update(
316
+ value=previews
317
+ ),
318
+ dl_preview_url.update(
319
+ value=preview
320
+ ),
321
+ download_all_row.update(
322
+ visible=(state["files_count"][dl_version] > 1)
323
+ )
324
+ ] + output_add
325
+
326
+ def update_dl_files_visibility(dl_all):
327
+ files_chkboxes = []
328
+ for chkbox in ch_dl_model_types_visibility:
329
+ files_chkboxes.append(
330
+ chkbox.update(
331
+ visible=not dl_all
332
+ )
333
+ )
334
+
335
+ return files_chkboxes
336
+
337
+ def update_dl_preview_url(state, dl_preview_index):
338
+ preview_url = state["filtered_previews"][dl_preview_index]
339
+
340
+ return dl_preview_url.update(
341
+ value=preview_url
342
+ )
343
+
344
+ def update_dl_preview_index(evt: gr.SelectData):
345
+ # For some reason, you can't pass gr.SelectData and
346
+ # inputs at the same time. :/
347
+
348
+ return dl_preview_index.update(
349
+ value=evt.index
350
+ )
351
+
352
+ with gr.Row():
353
+ gr.Markdown("### Download Model")
354
+
355
+ with gr.Row():
356
+ with gr.Column(scale=2, elem_id="ch_dl_model_inputs"):
357
+
358
+ gr.Markdown(value="**1. Add URL and retrieve Model Info**")
359
+
360
+ with gr.Row():
361
+ with gr.Column(scale=2, elem_classes="justify-bottom"):
362
+ dl_model_url_or_id_txtbox = gr.Textbox(
363
+ label="Civitai URL",
364
+ lines=1,
365
+ max_lines=1,
366
+ value="",
367
+ placeholder="Model URL or Model ID"
368
+ )
369
+ with gr.Column(elem_classes="justify-bottom"):
370
+ dl_model_info_btn = gr.Button(
371
+ value="Get Model Info by Civitai Url",
372
+ variant="primary"
373
+ )
374
+
375
+ gr.Markdown(value="**2. Pick Subfolder and Model Version**")
376
+
377
+ with gr.Row(elem_classes="ch_grid"):
378
+ dl_model_name_txtbox = gr.Textbox(
379
+ label="Model Name",
380
+ interactive=False,
381
+ lines=1,
382
+ max_lines=1,
383
+ min_width=320,
384
+ value=""
385
+ )
386
+ dl_model_type_txtbox = gr.Textbox(
387
+ label="Model Type",
388
+ interactive=False,
389
+ lines=1,
390
+ max_lines=1,
391
+ min_width=320,
392
+ value=""
393
+ )
394
+ dl_version_drop = gr.Dropdown(
395
+ choices=[],
396
+ label="Model Version",
397
+ value="",
398
+ min_width=320,
399
+ multiselect=False
400
+ )
401
+ dl_subfolder_drop = gr.Dropdown(
402
+ choices=[],
403
+ label="Sub-folder",
404
+ value="",
405
+ min_width=320,
406
+ multiselect=False
407
+ )
408
+ dl_duplicate_drop = gr.Dropdown(
409
+ choices=["Skip", "Overwrite", "Rename New"],
410
+ label="Duplicate File Behavior",
411
+ value="Skip",
412
+ min_width=320,
413
+ multiselect=False
414
+ )
415
+ nsfw_preview_dl_drop = gr.Dropdown(
416
+ label="Block NSFW Level Above",
417
+ choices=civitai.NSFW_LEVELS[1:],
418
+ value=util.get_opts("ch_nsfw_preview_threshold"),
419
+ min_width=320,
420
+ elem_id="ch_nsfw_preview_dl_drop"
421
+ )
422
+
423
+ with gr.Column(
424
+ visible=False,
425
+ variant="panel"
426
+ ) as files_row:
427
+
428
+ with gr.Row(variant="compact"):
429
+ gr.Markdown("**Files**")
430
+
431
+ ch_output_add = []
432
+ ch_dl_model_types = []
433
+ ch_dl_model_types_visibility = []
434
+
435
+ for filetype in model_filetypes:
436
+ with gr.Row(
437
+ visible=False,
438
+ equal_height=True,
439
+ ) as row:
440
+ file_elems[filetype] = elems = {}
441
+ elems["row"] = row
442
+
443
+ with gr.Column(scale=0, min_width=24, elem_classes="flex-center") as ckb_column:
444
+ elems["ckb"] = filetype_ckb = gr.Checkbox(
445
+ label="",
446
+ value=True,
447
+ min_width=0,
448
+ interactive=(not filetype == "Model")
449
+ )
450
+ with gr.Column(scale=1, min_width=0):
451
+ elems["txtbx"] = gr.Textbox(
452
+ value="",
453
+ interactive=False,
454
+ label=filetype,
455
+ max_lines=1,
456
+ min_width=0
457
+ )
458
+
459
+ ch_dl_model_types_visibility.append(ckb_column)
460
+ ch_dl_model_types.append(filetype_ckb)
461
+
462
+ ch_output_add.append(elems["txtbx"])
463
+ ch_output_add.append(row)
464
+
465
+ with gr.Row(visible=False) as unhandled_files_row:
466
+ file_elems["unhandled_files"] = elems = {}
467
+ elems["row"] = row
468
+
469
+ elems["txtbx"] = gr.Textbox(
470
+ value="",
471
+ interactive=False,
472
+ label="Unhandled Files (Files that we don't know what to do with but will still downloaded with \"Download All Files\")",
473
+ )
474
+
475
+ ch_output_add.append(elems["txtbx"])
476
+ ch_output_add.append(unhandled_files_row)
477
+
478
+ with gr.Row(visible=False) as download_all_row:
479
+ dl_all_ckb = gr.Checkbox(
480
+ label="Download All Files",
481
+ value=False,
482
+ elem_id="ch_dl_all_ckb",
483
+ elem_classes="ch_vpadding"
484
+ )
485
+
486
+ with gr.Column(scale=1, elem_id="ch_preview_col", min_width=512):
487
+ with gr.Row(elem_classes="flex-center"):
488
+ dl_preview_img = gr.Gallery(
489
+ show_label=True,
490
+ label="Preview Image",
491
+ value=None,
492
+ elem_id="ch_dl_preview_img",
493
+ allow_preview=True,
494
+ preview=False,
495
+ object_fit="scale-down"
496
+ )
497
+ dl_preview_url = gr.Textbox(
498
+ value="",
499
+ visible=False,
500
+ elem_id="ch_dl_preview_url"
501
+ )
502
+ dl_preview_index = gr.Number(
503
+ value=0,
504
+ visible=False,
505
+ elem_id="ch_dl_preview_index",
506
+ precision=0
507
+ )
508
+
509
+ with gr.Row():
510
+ with gr.Column(scale=2, elem_classes="justify-bottom"):
511
+ dl_filename_txtbox = gr.Textbox(
512
+ label="Rename Model",
513
+ value="",
514
+ lines=1,
515
+ max_lines=1,
516
+ elem_id="ch_dl_filename_txtbox",
517
+ elem_classes="ch_vpadding"
518
+ )
519
+ dl_extension_txtbox = gr.Textbox(
520
+ label="Model extension",
521
+ value="",
522
+ elem_id="ch_dl_extension_txtbox",
523
+ visible=False
524
+ )
525
+
526
+ with gr.Column(elem_classes="justify-bottom"):
527
+ dl_civitai_model_by_id_btn = gr.Button(
528
+ value="3. Download Model",
529
+ elem_classes="ch_vmargin",
530
+ variant="primary"
531
+ )
532
+
533
+ with gr.Row():
534
+ dl_log_md = gr.Markdown(
535
+ value="Check Console log for Downloading Status"
536
+ )
537
+
538
+ # ====events====
539
+ dl_model_info_btn.click(
540
+ get_model_info_by_url,
541
+ inputs=[
542
+ dl_model_url_or_id_txtbox, dl_subfolder_drop, dl_state
543
+ ],
544
+ outputs=[
545
+ dl_state, dl_model_name_txtbox,
546
+ dl_model_type_txtbox, dl_subfolder_drop,
547
+ dl_version_drop, files_row
548
+ ]
549
+ )
550
+
551
+ dl_inputs = [
552
+ dl_state, dl_model_type_txtbox,
553
+ dl_subfolder_drop, dl_version_drop,
554
+ dl_filename_txtbox, dl_extension_txtbox,
555
+ dl_all_ckb, nsfw_preview_dl_drop,
556
+ dl_duplicate_drop, dl_preview_url
557
+ ] + ch_dl_model_types
558
+
559
+ dl_civitai_model_by_id_btn.click(
560
+ model_action_civitai.dl_model_by_input,
561
+ inputs=dl_inputs,
562
+ outputs=dl_log_md
563
+ )
564
+
565
+ ver_outputs = [
566
+ dl_state, dl_filename_txtbox, dl_extension_txtbox,
567
+ dl_preview_img, dl_preview_url, download_all_row,
568
+ ] + ch_output_add
569
+
570
+ dl_version_drop.change(
571
+ update_dl_inputs,
572
+ inputs=[dl_state, dl_version_drop, nsfw_preview_dl_drop, dl_preview_index],
573
+ outputs=ver_outputs
574
+ )
575
+ dl_all_ckb.change(
576
+ update_dl_files_visibility,
577
+ inputs=dl_all_ckb,
578
+ outputs=ch_dl_model_types_visibility
579
+ )
580
+ nsfw_preview_dl_drop.change(
581
+ update_dl_inputs,
582
+ inputs=[dl_version_drop, dl_state, nsfw_preview_dl_drop],
583
+ outputs=ver_outputs
584
+ )
585
+ # Gradio has so many issues with Gradio.Gallery...
586
+ dl_preview_img.select(
587
+ update_dl_preview_index,
588
+ None,
589
+ dl_preview_index
590
+ )
591
+ dl_preview_index.change(
592
+ update_dl_preview_url,
593
+ [dl_state, dl_preview_index],
594
+ dl_preview_url
595
+ )
596
+
597
+
598
+ def scan_for_duplicates_section():
599
+ """ Scan Duplicate Models Section """
600
+ with gr.Column():
601
+ gr.Markdown("### Scan for duplicate models")
602
+ with gr.Row():
603
+ with gr.Column():
604
+ scan_dup_model_types_drop = gr.CheckboxGroup(
605
+ choices=model_types,
606
+ label="Model Types",
607
+ value=model_types
608
+ )
609
+ with gr.Row():
610
+ with gr.Column(scale=2):
611
+ cached_hash_ckb = gr.Checkbox(
612
+ label="Use Hash from Metadata (May have false-positives but can be useful if you've pruned models)",
613
+ value=False,
614
+ elem_id="ch_cached_hash_ckb"
615
+ )
616
+ with gr.Column():
617
+ scan_dup_model_btn = gr.Button(
618
+ value="Scan",
619
+ variant="primary",
620
+ elem_id="ch_scan_dup_model_civitai_btn"
621
+ )
622
+
623
+ # with gr.Row():
624
+ scan_dup_model_log_md = gr.HTML(
625
+ value="Scanning takes time, just wait. Check console log for details",
626
+ elem_id="ch_scan_dup_model_log_md"
627
+ )
628
+
629
+ # ====events====
630
+ scan_dup_model_btn.click(
631
+ duplicate_check.scan_for_dups,
632
+ inputs=[
633
+ scan_dup_model_types_drop,
634
+ cached_hash_ckb
635
+ ],
636
+ outputs=scan_dup_model_log_md
637
+ )
638
+
639
+ def check_new_versions_section(js_msg_txtbox):
640
+ """ Check models' new version section """
641
+
642
+ with gr.Column():
643
+ gr.Markdown("### Check models' new version")
644
+ with gr.Row():
645
+ with gr.Column(scale=2):
646
+ model_types_ckbg = gr.CheckboxGroup(
647
+ choices=model_types,
648
+ label="Model Types",
649
+ value=[
650
+ "ti", "hyper", "ckp", "lora", "lycoris"
651
+ ]
652
+ )
653
+ nsfw_preview_update_drop = gr.Dropdown(
654
+ label="Block NSFW Level Above",
655
+ choices=civitai.NSFW_LEVELS[1:],
656
+ value=util.get_opts("ch_nsfw_preview_threshold"),
657
+ elem_id="ch_nsfw_preview_dl_drop"
658
+ )
659
+ with gr.Row():
660
+ with gr.Column(scale=2):
661
+ check_models_new_version_btn = gr.Button(
662
+ value="Check New Version from Civitai",
663
+ variant="primary"
664
+ )
665
+
666
+ with gr.Row():
667
+ with gr.Column():
668
+ dl_new_version_log_md = gr.Markdown()
669
+ check_models_new_version_log_md = gr.HTML(
670
+ "It takes time, just wait. Check console log for details"
671
+ )
672
+
673
+ # ====events====
674
+ check_models_new_version_btn.click(
675
+ model_action_civitai.check_models_new_version_to_md,
676
+ inputs=model_types_ckbg,
677
+ outputs=check_models_new_version_log_md
678
+ )
679
+
680
+ js_dl_model_new_version_btn = gr.Button(
681
+ value="Download Model's new version",
682
+ visible=False,
683
+ elem_id="ch_js_dl_model_new_version_btn"
684
+ )
685
+
686
+ js_dl_model_new_version_btn.click(
687
+ js_action_civitai.dl_model_new_version,
688
+ inputs=[
689
+ js_msg_txtbox,
690
+ nsfw_preview_update_drop
691
+ ],
692
+ outputs=dl_new_version_log_md
693
+ )
extensions/Stable-Diffusion-Webui-Civitai-Helper/style.css CHANGED
@@ -5,7 +5,7 @@ blockquote ul {
5
 
6
  blockquote ol {
7
  list-style:decimal;
8
- margin:4px 40px;
9
  }
10
 
11
  .block.padded.ch_box {
@@ -16,3 +16,110 @@ blockquote ol {
16
  padding: 10px 0 !important;
17
  }
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  blockquote ol {
7
  list-style:decimal;
8
+ margin: 4px 40px;
9
  }
10
 
11
  .block.padded.ch_box {
 
16
  padding: 10px 0 !important;
17
  }
18
 
19
+ #tab_civitai_helper .ch_vmargin {
20
+ margin-top: 10px;
21
+ margin-bottom: 10px;
22
+ }
23
+
24
+ #tab_civitai_helper .extra-network-cards {
25
+ min-height: 0;
26
+ max-height: 50vh;
27
+ }
28
+
29
+ .card-button {
30
+ margin: 0px 2px;
31
+ padding: 2px;
32
+ font-size: 150%;
33
+ }
34
+
35
+ #tab_civitai_helper .justify-bottom {
36
+ justify-content: flex-end;
37
+ }
38
+
39
+ #tab_civitai_helper .ch_grid div {
40
+ flex-basis: 30%;
41
+ }
42
+
43
+ #tab_civitai_helper .flex-center {
44
+ justify-content: center;
45
+ align-content: center;
46
+ }
47
+ @media only screen and (min-width: 600px) {
48
+ #ch_dl_preview_img {
49
+ max-width: 512px;
50
+ min-width: 512px;
51
+ max-height: 768px;
52
+ }
53
+ }
54
+
55
+ /* SD WebUI has not line height limit for model names.
56
+ * This can result in the model name filling the entire card,
57
+ * hiding our buttons. This is honestly a WebUI issue more
58
+ * than an issue on this end, but we can fix it anyways, so
59
+ * why not?
60
+ */
61
+ .extra-network-cards .card .name:not(:hover) {
62
+ display: block;
63
+ overflow: hidden;
64
+ max-height: 3em;
65
+ }
66
+
67
+ .extra-network-thumbs .card-button {
68
+ font-size: 100%;
69
+ display: inline;
70
+ position: static;
71
+ background-image: none;
72
+ background: rgba(0, 0, 0, 0.8);
73
+ }
74
+
75
+ .duplicate_model {
76
+ display: flex;
77
+ flex-flow: row wrap;
78
+ justify-content: flex-start;
79
+ align-items: flex-start;
80
+ gap: 10px;
81
+ }
82
+
83
+ #ch_path_el {
84
+ position: fixed;
85
+ border: var(--input-border-width) solid var(--input-border-color);
86
+ border-radius: var(--input-radius);
87
+ background: var(--input-background-fill);
88
+ padding: var(--input-padding);
89
+ color: var(--body-text-color);
90
+ font-weight: var(--input-text-weight);
91
+ font-size: var(--input-text-size);
92
+ line-height: var(--line-sm);
93
+ box-shadow: var(--input-shadow);
94
+ font-family:var(--font);
95
+ pointer-events: none;
96
+ }
97
+
98
+ /* Lobe theme */
99
+ .actions .additional > a {
100
+ display: none;
101
+ }
102
+
103
+ /*
104
+ * Gross, ugly, and I hate it, but the only way to override an
105
+ * !important is with a more granular !important.
106
+ */
107
+ .acss-culdhq .extra-networks .actions .additional > ul a.card-button {
108
+ margin: 0px 2px !important;
109
+ padding: 2px !important;
110
+ font-size: 150% !important;
111
+ }
112
+
113
+ /* Work around a display bug in Lobe that causes lowers opacity to 0.2 */
114
+ #txt2img_extra_tabs [style*="_cards_html"] .pending,
115
+ #img2img_extra_tabs [style*="_cards_html"] .pending {
116
+ opacity: 1;
117
+ }
118
+
119
+ /* Work around a display bug in Lobe that causes an infinite counter */
120
+ #txt2img_extra_tabs [id*="_cards_html"] .progress-text ,
121
+ #img2img_extra_tabs [id*="_cards_html"] .progress-text {
122
+ display: none;
123
+ }
124
+
125
+ /* end of Lobe theme */
extensions/sd-webui-infinite-image-browsing/.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ IIB_SECRET_KEY=
2
+ IIB_ACCESS_CONTROL=disable
3
+ IIB_ACCESS_CONTROL_PERMISSION=read-write
extensions/sd-webui-infinite-image-browsing/.gitignore DELETED
@@ -1,17 +0,0 @@
1
-
2
- *.log
3
- __pycache__
4
- iib.db
5
- tags-translate.csv
6
- launch.sh
7
- conf.json
8
- iib.db-journal
9
- .env
10
- standalone.cmd
11
- .vscode
12
- build/**/*
13
- dist/**/*
14
- *.spec
15
- out/**/*
16
- venv/**/*
17
- zip_temp/*.zip
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
forcePull.bat ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+
3
+ git fetch --all
4
+ git reset --hard origin/main
5
+ git pull
6
+ git repack -a -d --depth=250 --window=250
7
+ cd .git
8
+ rmdir /S /Q lfs
9
+ cd ..
10
+
11
+ pause